Skip to content

Commit d32a2af

Browse files
committed
Fix ASCII tree rendering and add intuitive tree navigation
- Fix tree line drawing to preserve vertical lines (│) for nested children - Fix last child detection by deduplicating spans (prefer ended over running) - Add left/right arrow navigation to expand/collapse and navigate tree hierarchy - Add fallback navigation when tree actions aren't available (left→up, right→down) - Remove duplicate spans in tree building to fix incorrect isLastChild calculation - Add interactive example client with keyboard-driven workflow controls - Add resizable panels with drag handles for better layout control - Remove noisy metrics logging from server and runtime - Update help documentation for new navigation controls - Remove unused effect-atom dependencies in favor of solid-js store
1 parent 82599ad commit d32a2af

12 files changed

Lines changed: 806 additions & 477 deletions

File tree

.github/workflows/release.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141

4242
- name: Get version from package.json
4343
id: package-version
44-
run: echo "version=$(cat package.json | jq -r '.version')" >> $GITHUB_OUTPUT
44+
run: echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
4545

4646
- name: Create GitHub Release
4747
uses: softprops/action-gh-release@v1
@@ -50,27 +50,27 @@ jobs:
5050
name: Effect DevTools TUI v${{ steps.package-version.outputs.version }}
5151
body: |
5252
## Effect DevTools Terminal UI
53-
53+
5454
### Installation via npm
5555
```bash
5656
npm install -g effect-devtui
5757
effect-devtools
5858
```
59-
59+
6060
### Or download binary directly
6161
Download the binary for your platform below and run it directly.
62-
62+
6363
```bash
6464
# Linux x64
6565
./effect-devtools-linux-x64
66-
66+
6767
# macOS (Intel)
6868
./effect-devtools-darwin-x64
69-
69+
7070
# Windows
7171
effect-devtools-windows-x64.exe
7272
```
73-
73+
7474
Built from commit ${{ github.sha }}
7575
files: |
7676
dist/effect-devtools-linux-x64/effect-devtools-linux-x64

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ Built with [OpenTUI](https://github.com/opentui/opentui) and inspired by the [Ef
99
## Installation
1010

1111
```bash
12-
# Install globally
13-
npm install -g effect-devtui
14-
effect-devtools
12+
# Install as a dev dependency
13+
npm i -d effect-devtui
1514

16-
# Or run directly with bunx
17-
bunx effect-devtui
15+
# Or run directly with npmx
16+
npmx effect-devtui
1817
```
1918

2019
## Features
@@ -31,7 +30,8 @@ bunx effect-devtui
3130
To use Effect DevTools TUI with your Effect project, first install the required dependency:
3231

3332
```bash
34-
pnpm install @effect/experimental
33+
npm i @effect/experimental
34+
npm i -d effect-devtui
3535
```
3636

3737
Then add the `DevTools` layer to your Effect application:

bun.lock

Lines changed: 3 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/client.ts

Lines changed: 184 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
11
/**
2-
* Effect Test Application
2+
* Interactive Effect Test Client
33
*
4-
* A simple Effect app that connects to the DevTools TUI server
5-
* and generates spans for testing the tree view.
4+
* An interactive app that generates different spans based on keyboard input.
5+
* Connect this to the DevTools TUI server to test navigation.
6+
*
7+
* Controls:
8+
* 1 - Run userWorkflow (nested: fetchUser, processUser [validateEmail, enrichData], saveUser)
9+
* 2 - Run databaseQuery (nested: connect, executeQuery, parseResults)
10+
* 3 - Run apiRequest (nested: authenticate, fetchData [rateLimit, transform], cacheResponse)
11+
* t - Toggle auto-timer (runs userWorkflow every 3s)
12+
* q - Quit
613
*/
714

815
import * as Effect from "effect/Effect";
16+
import * as Ref from "effect/Ref";
17+
import * as Fiber from "effect/Fiber";
918
import * as Schedule from "effect/Schedule";
1019
import * as Layer from "effect/Layer";
1120
import { DevTools } from "@effect/experimental";
1221
import * as NodeSocket from "@effect/platform-node/NodeSocket";
22+
import * as readline from "node:readline";
1323

1424
// Connect to the DevTools TUI server
1525
const DEVTOOLS_URL = "ws://localhost:34437";
1626

17-
/**
18-
* Simulates fetching user data from a database
19-
*/
27+
// ============================================================================
28+
// Workflow 1: User Workflow
29+
// ============================================================================
30+
2031
const fetchUser = (userId: number) =>
2132
Effect.gen(function* () {
2233
yield* Effect.log(`Fetching user ${userId}`);
@@ -28,9 +39,6 @@ const fetchUser = (userId: number) =>
2839
};
2940
}).pipe(Effect.withSpan("fetchUser", { attributes: { userId } }));
3041

31-
/**
32-
* Simulates processing user data
33-
*/
3442
const processUser = (user: { id: number; name: string; email: string }) =>
3543
Effect.gen(function* () {
3644
yield* Effect.log(`Processing user ${user.name}`);
@@ -51,18 +59,12 @@ const processUser = (user: { id: number; name: string; email: string }) =>
5159
return { ...user, processed: true };
5260
}).pipe(Effect.withSpan("processUser"));
5361

54-
/**
55-
* Simulates saving to database
56-
*/
5762
const saveUser = (user: any) =>
5863
Effect.gen(function* () {
5964
yield* Effect.log(`Saving user ${user.name}`);
6065
yield* Effect.sleep("75 millis");
6166
}).pipe(Effect.withSpan("saveUser", { attributes: { userId: user.id } }));
6267

63-
/**
64-
* Main workflow that orchestrates the operations
65-
*/
6668
const userWorkflow = (userId: number) =>
6769
Effect.gen(function* () {
6870
const user = yield* fetchUser(userId);
@@ -71,25 +73,174 @@ const userWorkflow = (userId: number) =>
7173
yield* Effect.log(`Completed workflow for user ${userId}`);
7274
}).pipe(Effect.withSpan("userWorkflow", { attributes: { userId } }));
7375

74-
/**
75-
* Main program that runs multiple workflows with DevTools connection retry
76-
*/
76+
// ============================================================================
77+
// Workflow 2: Database Query
78+
// ============================================================================
79+
80+
const connectToDatabase = Effect.gen(function* () {
81+
yield* Effect.log("Connecting to database");
82+
yield* Effect.sleep("80 millis");
83+
}).pipe(Effect.withSpan("connect"));
84+
85+
const executeQuery = (query: string) =>
86+
Effect.gen(function* () {
87+
yield* Effect.log(`Executing query: ${query}`);
88+
yield* Effect.sleep("120 millis");
89+
return [
90+
{ id: 1, data: "row1" },
91+
{ id: 2, data: "row2" },
92+
];
93+
}).pipe(Effect.withSpan("executeQuery", { attributes: { query } }));
94+
95+
const parseResults = (results: any[]) =>
96+
Effect.gen(function* () {
97+
yield* Effect.log(`Parsing ${results.length} results`);
98+
yield* Effect.sleep("40 millis");
99+
return results.map((r) => ({ ...r, parsed: true }));
100+
}).pipe(Effect.withSpan("parseResults"));
101+
102+
const databaseQuery = (query: string) =>
103+
Effect.gen(function* () {
104+
yield* connectToDatabase;
105+
const results = yield* executeQuery(query);
106+
const parsed = yield* parseResults(results);
107+
yield* Effect.log(`Database query completed with ${parsed.length} rows`);
108+
}).pipe(Effect.withSpan("databaseQuery", { attributes: { query } }));
109+
110+
// ============================================================================
111+
// Workflow 3: API Request
112+
// ============================================================================
113+
114+
const authenticate = Effect.gen(function* () {
115+
yield* Effect.log("Authenticating API request");
116+
yield* Effect.sleep("60 millis");
117+
return "token-12345";
118+
}).pipe(Effect.withSpan("authenticate"));
119+
120+
const fetchData = (_token: string, endpoint: string) =>
121+
Effect.gen(function* () {
122+
yield* Effect.log(`Fetching data from ${endpoint}`);
123+
yield* Effect.sleep("90 millis");
124+
125+
// Nested: check rate limit
126+
yield* Effect.gen(function* () {
127+
yield* Effect.log("Checking rate limit");
128+
yield* Effect.sleep("15 millis");
129+
}).pipe(Effect.withSpan("rateLimit"));
130+
131+
// Nested: transform data
132+
yield* Effect.gen(function* () {
133+
yield* Effect.log("Transforming response data");
134+
yield* Effect.sleep("25 millis");
135+
}).pipe(Effect.withSpan("transform"));
136+
137+
return { data: "api-response", endpoint };
138+
}).pipe(Effect.withSpan("fetchData", { attributes: { endpoint } }));
139+
140+
const cacheResponse = (_response: any) =>
141+
Effect.gen(function* () {
142+
yield* Effect.log("Caching API response");
143+
yield* Effect.sleep("35 millis");
144+
}).pipe(Effect.withSpan("cacheResponse"));
145+
146+
const apiRequest = (endpoint: string) =>
147+
Effect.gen(function* () {
148+
const token = yield* authenticate;
149+
const response = yield* fetchData(token, endpoint);
150+
yield* cacheResponse(response);
151+
yield* Effect.log(`API request to ${endpoint} completed`);
152+
}).pipe(Effect.withSpan("apiRequest", { attributes: { endpoint } }));
153+
154+
// ============================================================================
155+
// Helper for reading input
156+
// ============================================================================
157+
158+
const readLine = (prompt: string): Effect.Effect<string> =>
159+
Effect.async<string>((resume) => {
160+
const rl = readline.createInterface({
161+
input: process.stdin,
162+
output: process.stdout,
163+
});
164+
rl.question(prompt, (answer) => {
165+
rl.close();
166+
resume(Effect.succeed(answer));
167+
});
168+
});
169+
170+
// ============================================================================
171+
// Interactive Program
172+
// ============================================================================
173+
77174
const program = Effect.gen(function* () {
78-
yield* Effect.log("Starting Effect test app");
79-
yield* Effect.log(`Connecting to DevTools at ${DEVTOOLS_URL}`);
80-
81-
// Run workflows periodically
82-
yield* Effect.repeat(
83-
Effect.gen(function* () {
84-
// Process 3 users in parallel
85-
yield* Effect.all([userWorkflow(1), userWorkflow(2), userWorkflow(3)], {
86-
concurrency: 3,
87-
});
88-
}),
89-
Schedule.spaced("3 seconds"),
175+
const timerRunning = yield* Ref.make(false);
176+
const timerFiber = yield* Ref.make<Fiber.RuntimeFiber<any, any> | null>(null);
177+
178+
console.log("\n=== Interactive Effect Test Client ===");
179+
console.log("Connected to DevTools at " + DEVTOOLS_URL);
180+
console.log("\nControls:");
181+
console.log(
182+
" 1 - Run userWorkflow (fetchUser -> processUser [validateEmail, enrichData] -> saveUser)",
183+
);
184+
console.log(
185+
" 2 - Run databaseQuery (connect -> executeQuery -> parseResults)",
90186
);
187+
console.log(
188+
" 3 - Run apiRequest (authenticate -> fetchData [rateLimit, transform] -> cacheResponse)",
189+
);
190+
console.log(" t - Toggle auto-timer (runs userWorkflow every 3s)");
191+
console.log(" q - Quit\n");
192+
193+
// Main input loop
194+
while (true) {
195+
const input = yield* readLine("> ");
196+
197+
if (input === "q") {
198+
console.log("Quitting...");
199+
// Stop timer if running
200+
const fiber = yield* Ref.get(timerFiber);
201+
if (fiber) {
202+
yield* Fiber.interrupt(fiber);
203+
}
204+
break;
205+
} else if (input === "1") {
206+
console.log("Running userWorkflow...");
207+
yield* Effect.fork(userWorkflow(1));
208+
} else if (input === "2") {
209+
console.log("Running databaseQuery...");
210+
yield* Effect.fork(databaseQuery("SELECT * FROM users"));
211+
} else if (input === "3") {
212+
console.log("Running apiRequest...");
213+
yield* Effect.fork(apiRequest("/api/v1/data"));
214+
} else if (input === "t") {
215+
const running = yield* Ref.get(timerRunning);
216+
if (running) {
217+
// Stop timer
218+
const fiber = yield* Ref.get(timerFiber);
219+
if (fiber) {
220+
yield* Fiber.interrupt(fiber);
221+
}
222+
yield* Ref.set(timerRunning, false);
223+
console.log("Auto-timer stopped.");
224+
} else {
225+
// Start timer
226+
const fiber = yield* Effect.fork(
227+
Effect.repeat(
228+
Effect.gen(function* () {
229+
console.log("[Timer] Running userWorkflow...");
230+
yield* userWorkflow(1);
231+
}),
232+
Schedule.spaced("3 seconds"),
233+
),
234+
);
235+
yield* Ref.set(timerFiber, fiber);
236+
yield* Ref.set(timerRunning, true);
237+
console.log("Auto-timer started (runs every 3s).");
238+
}
239+
} else {
240+
console.log(`Unknown command: ${input}`);
241+
}
242+
}
91243
}).pipe(
92-
// Retry the entire program if DevTools connection fails
93244
Effect.retry(
94245
Schedule.spaced("2 seconds").pipe(Schedule.intersect(Schedule.recurs(5))),
95246
),
@@ -103,9 +254,5 @@ const DevToolsLive = DevTools.layer(DEVTOOLS_URL);
103254
const WebSocketLive = NodeSocket.layerWebSocketConstructor;
104255
const MainLive = Layer.provide(DevToolsLive, WebSocketLive);
105256

106-
// Run the program with retry logic
107-
Effect.runFork(Effect.provide(program, MainLive));
108-
109-
console.log(
110-
`Effect test app started. Will retry connection up to 5 times. Press Ctrl+C to exit.`,
111-
);
257+
// Run the program
258+
Effect.runFork(program.pipe(Effect.provide(MainLive)));

0 commit comments

Comments
 (0)