@astroanywhere/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui.js ADDED
@@ -0,0 +1,2136 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AstroClient
4
+ } from "./chunk-7H7WD7QX.js";
5
+
6
+ // src/tui/index.tsx
7
+ import "react";
8
+ import { render } from "ink";
9
+
10
+ // src/tui/app.tsx
11
+ import { useMemo, useCallback as useCallback5 } from "react";
12
+
13
+ // src/tui/components/layout/main-layout.tsx
14
+ import "react";
15
+ import { Box as Box10, useStdout } from "ink";
16
+
17
+ // src/tui/components/layout/status-bar.tsx
18
+ import "react";
19
+ import { Box, Text } from "ink";
20
+
21
+ // src/tui/stores/tui-store.ts
22
+ import { create } from "zustand";
23
+ var PANEL_ORDER = ["projects", "plan", "machines", "output"];
24
+ var useTuiStore = create((set, get) => ({
25
+ mode: "normal",
26
+ commandBuffer: "",
27
+ searchQuery: "",
28
+ pendingKeys: "",
29
+ focusedPanel: "projects",
30
+ panelOrder: PANEL_ORDER,
31
+ selectedProjectId: null,
32
+ selectedNodeId: null,
33
+ selectedMachineId: null,
34
+ selectedExecutionId: null,
35
+ scrollIndex: { projects: 0, plan: 0, machines: 0, output: 0 },
36
+ showHelp: false,
37
+ showSearch: false,
38
+ showDetail: false,
39
+ detailType: null,
40
+ detailId: null,
41
+ connected: false,
42
+ machineCount: 0,
43
+ todayCost: 0,
44
+ lastError: null,
45
+ setMode: (mode) => set({ mode }),
46
+ setCommandBuffer: (commandBuffer) => set({ commandBuffer }),
47
+ setSearchQuery: (searchQuery) => set({ searchQuery }),
48
+ setPendingKeys: (pendingKeys) => set({ pendingKeys }),
49
+ focusPanel: (panel) => set({ focusedPanel: panel }),
50
+ focusNext: () => {
51
+ const { focusedPanel, panelOrder } = get();
52
+ const idx = panelOrder.indexOf(focusedPanel);
53
+ set({ focusedPanel: panelOrder[(idx + 1) % panelOrder.length] });
54
+ },
55
+ focusPrev: () => {
56
+ const { focusedPanel, panelOrder } = get();
57
+ const idx = panelOrder.indexOf(focusedPanel);
58
+ set({ focusedPanel: panelOrder[(idx - 1 + panelOrder.length) % panelOrder.length] });
59
+ },
60
+ focusByIndex: (idx) => {
61
+ const { panelOrder } = get();
62
+ if (idx >= 0 && idx < panelOrder.length) {
63
+ set({ focusedPanel: panelOrder[idx] });
64
+ }
65
+ },
66
+ setSelectedProject: (selectedProjectId) => set({ selectedProjectId }),
67
+ setSelectedNode: (selectedNodeId) => set({ selectedNodeId }),
68
+ setSelectedMachine: (selectedMachineId) => set({ selectedMachineId }),
69
+ setSelectedExecution: (selectedExecutionId) => set({ selectedExecutionId }),
70
+ scrollUp: (panel) => {
71
+ const p = panel ?? get().focusedPanel;
72
+ set((s) => ({
73
+ scrollIndex: { ...s.scrollIndex, [p]: Math.max(0, s.scrollIndex[p] - 1) }
74
+ }));
75
+ },
76
+ scrollDown: (panel, max) => {
77
+ const p = panel ?? get().focusedPanel;
78
+ set((s) => ({
79
+ scrollIndex: {
80
+ ...s.scrollIndex,
81
+ [p]: max != null ? Math.min(max - 1, s.scrollIndex[p] + 1) : s.scrollIndex[p] + 1
82
+ }
83
+ }));
84
+ },
85
+ scrollToTop: (panel) => {
86
+ const p = panel ?? get().focusedPanel;
87
+ set((s) => ({
88
+ scrollIndex: { ...s.scrollIndex, [p]: 0 }
89
+ }));
90
+ },
91
+ scrollToBottom: (panel, max) => {
92
+ const p = panel ?? get().focusedPanel;
93
+ if (max != null && max > 0) {
94
+ set((s) => ({
95
+ scrollIndex: { ...s.scrollIndex, [p]: max - 1 }
96
+ }));
97
+ }
98
+ },
99
+ pageUp: (panel, pageSize = 10) => {
100
+ const p = panel ?? get().focusedPanel;
101
+ set((s) => ({
102
+ scrollIndex: { ...s.scrollIndex, [p]: Math.max(0, s.scrollIndex[p] - pageSize) }
103
+ }));
104
+ },
105
+ pageDown: (panel, max, pageSize = 10) => {
106
+ const p = panel ?? get().focusedPanel;
107
+ set((s) => ({
108
+ scrollIndex: {
109
+ ...s.scrollIndex,
110
+ [p]: max != null ? Math.min(max - 1, s.scrollIndex[p] + pageSize) : s.scrollIndex[p] + pageSize
111
+ }
112
+ }));
113
+ },
114
+ toggleHelp: () => set((s) => ({ showHelp: !s.showHelp, showSearch: false })),
115
+ toggleSearch: () => set((s) => ({ showSearch: !s.showSearch, showHelp: false })),
116
+ openDetail: (type, id) => set({ showDetail: true, detailType: type, detailId: id, showHelp: false, showSearch: false }),
117
+ closeDetail: () => set({ showDetail: false, detailType: null, detailId: null }),
118
+ closeOverlays: () => set({ showHelp: false, showSearch: false, showDetail: false, detailType: null, detailId: null }),
119
+ setConnected: (connected) => set({ connected }),
120
+ setMachineCount: (machineCount) => set({ machineCount }),
121
+ setTodayCost: (todayCost) => set({ todayCost }),
122
+ setLastError: (lastError) => set({ lastError })
123
+ }));
124
+
125
+ // src/tui/lib/format.ts
126
+ function formatRelativeTime(date) {
127
+ if (!date) return "\u2014";
128
+ const d = typeof date === "string" ? new Date(date) : date;
129
+ const now = Date.now();
130
+ const diff = now - d.getTime();
131
+ const seconds = Math.floor(diff / 1e3);
132
+ const minutes = Math.floor(seconds / 60);
133
+ const hours = Math.floor(minutes / 60);
134
+ const days = Math.floor(hours / 24);
135
+ if (seconds < 60) return "just now";
136
+ if (minutes < 60) return `${minutes}m ago`;
137
+ if (hours < 24) return `${hours}h ago`;
138
+ if (days < 30) return `${days}d ago`;
139
+ return d.toLocaleDateString();
140
+ }
141
+ function formatCost(usd) {
142
+ if (usd == null) return "\u2014";
143
+ return `$${usd.toFixed(2)}`;
144
+ }
145
+ function truncate(str, maxLen) {
146
+ if (str.length <= maxLen) return str;
147
+ return str.slice(0, maxLen - 1) + "\u2026";
148
+ }
149
+
150
+ // src/tui/components/layout/status-bar.tsx
151
+ import { jsx, jsxs } from "react/jsx-runtime";
152
+ function StatusBar() {
153
+ const connected = useTuiStore((s) => s.connected);
154
+ const machineCount = useTuiStore((s) => s.machineCount);
155
+ const todayCost = useTuiStore((s) => s.todayCost);
156
+ const lastError = useTuiStore((s) => s.lastError);
157
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [
158
+ /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
159
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Astro TUI" }),
160
+ /* @__PURE__ */ jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25CF connected" : "\u25CB disconnected" }),
161
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
162
+ machineCount,
163
+ " machine",
164
+ machineCount !== 1 ? "s" : ""
165
+ ] }),
166
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
167
+ formatCost(todayCost),
168
+ " today"
169
+ ] })
170
+ ] }),
171
+ lastError && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "red", children: lastError.length > 60 ? lastError.slice(0, 57) + "..." : lastError }) })
172
+ ] });
173
+ }
174
+
175
+ // src/tui/components/layout/command-line.tsx
176
+ import "react";
177
+ import { Box as Box2, Text as Text2 } from "ink";
178
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
179
+ var MODE_LABELS = {
180
+ normal: { label: "NORMAL", color: "blue" },
181
+ command: { label: "COMMAND", color: "yellow" },
182
+ search: { label: "SEARCH", color: "green" },
183
+ insert: { label: "INSERT", color: "magenta" }
184
+ };
185
+ function CommandLine() {
186
+ const mode = useTuiStore((s) => s.mode);
187
+ const commandBuffer = useTuiStore((s) => s.commandBuffer);
188
+ const searchQuery = useTuiStore((s) => s.searchQuery);
189
+ const pendingKeys = useTuiStore((s) => s.pendingKeys);
190
+ const modeInfo = MODE_LABELS[mode] ?? MODE_LABELS.normal;
191
+ let content = "";
192
+ let prefix = "";
193
+ switch (mode) {
194
+ case "command":
195
+ prefix = ":";
196
+ content = commandBuffer;
197
+ break;
198
+ case "search":
199
+ prefix = "/";
200
+ content = searchQuery;
201
+ break;
202
+ case "normal":
203
+ if (pendingKeys) {
204
+ content = pendingKeys;
205
+ } else {
206
+ content = "Press : for commands, / to search, ? for help";
207
+ }
208
+ break;
209
+ case "insert":
210
+ content = "-- INSERT MODE -- (Esc to exit)";
211
+ break;
212
+ }
213
+ return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, justifyContent: "space-between", children: /* @__PURE__ */ jsxs2(Box2, { gap: 1, children: [
214
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, color: modeInfo.color, inverse: true, children: [
215
+ " ",
216
+ modeInfo.label,
217
+ " "
218
+ ] }),
219
+ mode === "command" || mode === "search" ? /* @__PURE__ */ jsxs2(Text2, { children: [
220
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: prefix }),
221
+ /* @__PURE__ */ jsx2(Text2, { children: content }),
222
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2588" })
223
+ ] }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: content })
224
+ ] }) });
225
+ }
226
+
227
+ // src/tui/components/layout/help-overlay.tsx
228
+ import "react";
229
+ import { Box as Box3, Text as Text3 } from "ink";
230
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
231
+ var KEYBINDINGS = [
232
+ ["Navigation", [
233
+ ["j / \u2193", "Move down"],
234
+ ["k / \u2191", "Move up"],
235
+ ["h / \u2190", "Focus left panel"],
236
+ ["l / \u2192", "Focus right panel"],
237
+ ["Tab", "Cycle panel focus"],
238
+ ["1-4", "Jump to panel"],
239
+ ["gg", "Scroll to top"],
240
+ ["G", "Scroll to bottom"],
241
+ ["Ctrl+u", "Page up"],
242
+ ["Ctrl+d", "Page down"],
243
+ ["Enter", "Select / expand"]
244
+ ]],
245
+ ["Actions", [
246
+ ["d", "Dispatch selected task"],
247
+ ["c", "Cancel running task"],
248
+ ["r", "Refresh all data"],
249
+ ["q", "Quit"]
250
+ ]],
251
+ ["Modes", [
252
+ [":", "Command mode"],
253
+ ["/", "Search mode"],
254
+ ["i", "Insert mode (steer)"],
255
+ ["?", "Toggle this help"],
256
+ ["Esc", "Return to normal mode"]
257
+ ]],
258
+ ["Commands", [
259
+ [":project list/create/delete", "Project management"],
260
+ [":plan tree/create-node", "Plan operations"],
261
+ [":dispatch <nodeId>", "Dispatch task"],
262
+ [":cancel <execId>", "Cancel execution"],
263
+ [":steer <message>", "Steer running task"],
264
+ [":watch <execId>", "Watch execution output"],
265
+ [":env list/status", "Machine management"],
266
+ [":activity", "Activity feed"],
267
+ [":refresh / :r", "Force refresh"],
268
+ [":quit / :q", "Exit TUI"]
269
+ ]]
270
+ ];
271
+ function HelpOverlay() {
272
+ return /* @__PURE__ */ jsxs3(
273
+ Box3,
274
+ {
275
+ flexDirection: "column",
276
+ borderStyle: "round",
277
+ borderColor: "yellow",
278
+ paddingX: 2,
279
+ paddingY: 1,
280
+ children: [
281
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: " Keybindings Reference " }),
282
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
283
+ KEYBINDINGS.map(([section, bindings]) => /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
284
+ /* @__PURE__ */ jsx3(Text3, { bold: true, underline: true, children: section }),
285
+ bindings.map(([key, desc]) => /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
286
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: key.padEnd(28) }),
287
+ /* @__PURE__ */ jsx3(Text3, { children: desc })
288
+ ] }, key))
289
+ ] }, section)),
290
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Press Esc or ? to close" })
291
+ ]
292
+ }
293
+ );
294
+ }
295
+
296
+ // src/tui/components/layout/search-overlay.tsx
297
+ import "react";
298
+ import { Box as Box4, Text as Text4, useInput } from "ink";
299
+
300
+ // src/tui/stores/search-store.ts
301
+ import { create as create2 } from "zustand";
302
+ var useSearchStore = create2((set, get) => ({
303
+ items: [],
304
+ query: "",
305
+ results: [],
306
+ selectedIndex: 0,
307
+ isOpen: false,
308
+ setItems: (items) => set({ items }),
309
+ setQuery: (query) => set({ query, selectedIndex: 0 }),
310
+ setResults: (results) => set({ results }),
311
+ setSelectedIndex: (selectedIndex) => set({ selectedIndex }),
312
+ moveUp: () => {
313
+ const { selectedIndex } = get();
314
+ set({ selectedIndex: Math.max(0, selectedIndex - 1) });
315
+ },
316
+ moveDown: () => {
317
+ const { selectedIndex, results } = get();
318
+ set({ selectedIndex: Math.min(results.length - 1, selectedIndex + 1) });
319
+ },
320
+ open: () => set({ isOpen: true, query: "", results: [], selectedIndex: 0 }),
321
+ close: () => set({ isOpen: false, query: "", results: [], selectedIndex: 0 })
322
+ }));
323
+
324
+ // src/tui/lib/status-colors.ts
325
+ var STATUS_COLOR_MAP = {
326
+ // Project statuses
327
+ active: "green",
328
+ archived: "gray",
329
+ // Plan node statuses
330
+ planned: "yellow",
331
+ dispatched: "cyan",
332
+ in_progress: "cyan",
333
+ auto_verified: "green",
334
+ awaiting_approval: "magenta",
335
+ awaiting_judgment: "magenta",
336
+ completed: "blue",
337
+ pruned: "gray",
338
+ // Execution statuses
339
+ pending: "yellow",
340
+ running: "cyan",
341
+ success: "green",
342
+ failure: "red",
343
+ cancelled: "gray",
344
+ error: "red",
345
+ timeout: "red",
346
+ // Machine connection
347
+ connected: "green",
348
+ disconnected: "gray",
349
+ // Health
350
+ on_track: "green",
351
+ at_risk: "yellow",
352
+ off_track: "red"
353
+ };
354
+ function getStatusColor(status) {
355
+ return STATUS_COLOR_MAP[status] ?? "white";
356
+ }
357
+ function getStatusSymbol(status) {
358
+ switch (status) {
359
+ case "completed":
360
+ case "auto_verified":
361
+ case "success":
362
+ return "\u2713";
363
+ // ✓
364
+ case "in_progress":
365
+ case "running":
366
+ case "dispatched":
367
+ return "\u25CB";
368
+ // ○ (spinning handled separately)
369
+ case "planned":
370
+ case "pending":
371
+ return "\u2022";
372
+ // •
373
+ case "failure":
374
+ case "error":
375
+ return "\u2717";
376
+ // ✗
377
+ case "pruned":
378
+ case "cancelled":
379
+ return "\u2500";
380
+ // ─
381
+ case "awaiting_approval":
382
+ case "awaiting_judgment":
383
+ return "?";
384
+ default:
385
+ return "\u2022";
386
+ }
387
+ }
388
+
389
+ // src/tui/components/layout/search-overlay.tsx
390
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
391
+ function SearchOverlay() {
392
+ const isOpen = useSearchStore((s) => s.isOpen);
393
+ const query = useSearchStore((s) => s.query);
394
+ const results = useSearchStore((s) => s.results);
395
+ const selectedIndex = useSearchStore((s) => s.selectedIndex);
396
+ const { setQuery, moveUp, moveDown, close } = useSearchStore();
397
+ const { setSelectedProject, setSelectedNode, setSelectedMachine, focusPanel } = useTuiStore();
398
+ useInput((input, key) => {
399
+ if (!isOpen) return;
400
+ if (key.escape) {
401
+ close();
402
+ return;
403
+ }
404
+ if (key.upArrow) {
405
+ moveUp();
406
+ return;
407
+ }
408
+ if (key.downArrow) {
409
+ moveDown();
410
+ return;
411
+ }
412
+ if (key.return && results.length > 0) {
413
+ const item = results[selectedIndex];
414
+ if (item) {
415
+ switch (item.type) {
416
+ case "project":
417
+ setSelectedProject(item.id);
418
+ focusPanel("projects");
419
+ break;
420
+ case "task":
421
+ setSelectedNode(item.id);
422
+ focusPanel("plan");
423
+ break;
424
+ case "machine":
425
+ setSelectedMachine(item.id);
426
+ focusPanel("machines");
427
+ break;
428
+ }
429
+ }
430
+ close();
431
+ return;
432
+ }
433
+ if (key.backspace || key.delete) {
434
+ setQuery(query.slice(0, -1));
435
+ return;
436
+ }
437
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
438
+ setQuery(query + input);
439
+ }
440
+ }, { isActive: isOpen });
441
+ if (!isOpen) return null;
442
+ return /* @__PURE__ */ jsxs4(
443
+ Box4,
444
+ {
445
+ flexDirection: "column",
446
+ borderStyle: "round",
447
+ borderColor: "cyan",
448
+ paddingX: 1,
449
+ paddingY: 0,
450
+ width: "60%",
451
+ height: Math.min(results.length + 4, 15),
452
+ children: [
453
+ /* @__PURE__ */ jsxs4(Box4, { children: [
454
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Search: " }),
455
+ /* @__PURE__ */ jsx4(Text4, { children: query }),
456
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u2588" })
457
+ ] }),
458
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
459
+ results.length === 0 && query.length > 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " No results" }),
460
+ results.slice(0, 10).map((item, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(
461
+ Text4,
462
+ {
463
+ inverse: i === selectedIndex,
464
+ bold: i === selectedIndex,
465
+ color: i === selectedIndex ? "cyan" : void 0,
466
+ children: [
467
+ i === selectedIndex ? " \u25B6 " : " ",
468
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
469
+ "[",
470
+ item.type,
471
+ "]"
472
+ ] }),
473
+ " ",
474
+ item.title,
475
+ item.status && /* @__PURE__ */ jsxs4(Text4, { color: getStatusColor(item.status), children: [
476
+ " [",
477
+ item.status,
478
+ "]"
479
+ ] })
480
+ ]
481
+ }
482
+ ) }, item.id))
483
+ ] })
484
+ ]
485
+ }
486
+ );
487
+ }
488
+
489
+ // src/tui/components/panels/projects-panel.tsx
490
+ import "react";
491
+ import { Text as Text8 } from "ink";
492
+
493
+ // src/tui/components/layout/panel.tsx
494
+ import "react";
495
+ import { Box as Box5, Text as Text5 } from "ink";
496
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
497
+ function Panel({ title, isFocused, children, width, height }) {
498
+ const borderColor = isFocused ? "cyan" : "gray";
499
+ return /* @__PURE__ */ jsxs5(
500
+ Box5,
501
+ {
502
+ flexDirection: "column",
503
+ borderStyle: "single",
504
+ borderColor,
505
+ width,
506
+ height,
507
+ flexGrow: 1,
508
+ children: [
509
+ /* @__PURE__ */ jsx5(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: isFocused ? "cyan" : "white", children: title }) }),
510
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", paddingX: 1, flexGrow: 1, children })
511
+ ]
512
+ }
513
+ );
514
+ }
515
+
516
+ // src/tui/components/shared/scrollable-list.tsx
517
+ import "react";
518
+ import { Box as Box6, Text as Text6 } from "ink";
519
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
520
+ function ScrollableList({ items, selectedIndex, height, isFocused }) {
521
+ if (items.length === 0) {
522
+ return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " No items." }) });
523
+ }
524
+ const visibleHeight = Math.max(1, height - 1);
525
+ let start = 0;
526
+ if (selectedIndex >= visibleHeight) {
527
+ start = selectedIndex - visibleHeight + 1;
528
+ }
529
+ const visibleItems = items.slice(start, start + visibleHeight);
530
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
531
+ visibleItems.map((item, i) => {
532
+ const actualIndex = start + i;
533
+ const isSelected = actualIndex === selectedIndex && isFocused;
534
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
535
+ /* @__PURE__ */ jsxs6(
536
+ Text6,
537
+ {
538
+ color: isSelected ? "cyan" : item.color ?? void 0,
539
+ bold: isSelected,
540
+ inverse: isSelected,
541
+ children: [
542
+ isSelected ? " \u25B6 " : " ",
543
+ item.label,
544
+ item.sublabel ? ` ${item.sublabel}` : ""
545
+ ]
546
+ }
547
+ ),
548
+ item.rightLabel && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
549
+ " ",
550
+ item.rightLabel
551
+ ] })
552
+ ] }, item.id);
553
+ }),
554
+ items.length > visibleHeight && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
555
+ " [",
556
+ start + 1,
557
+ "-",
558
+ Math.min(start + visibleHeight, items.length),
559
+ "/",
560
+ items.length,
561
+ "]"
562
+ ] })
563
+ ] });
564
+ }
565
+
566
+ // src/tui/components/shared/spinner.tsx
567
+ import { useState, useEffect } from "react";
568
+ import { Text as Text7 } from "ink";
569
+ import { jsxs as jsxs7 } from "react/jsx-runtime";
570
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
571
+ function Spinner({ label }) {
572
+ const [frame, setFrame] = useState(0);
573
+ useEffect(() => {
574
+ const timer = setInterval(() => {
575
+ setFrame((f) => (f + 1) % FRAMES.length);
576
+ }, 80);
577
+ return () => clearInterval(timer);
578
+ }, []);
579
+ return /* @__PURE__ */ jsxs7(Text7, { color: "cyan", children: [
580
+ FRAMES[frame],
581
+ label ? ` ${label}` : ""
582
+ ] });
583
+ }
584
+
585
+ // src/tui/stores/projects-store.ts
586
+ import { create as create3 } from "zustand";
587
+ var useProjectsStore = create3((set) => ({
588
+ projects: [],
589
+ loading: false,
590
+ error: null,
591
+ setProjects: (projects) => set({ projects, loading: false, error: null }),
592
+ setLoading: (loading) => set({ loading }),
593
+ setError: (error) => set({ error, loading: false })
594
+ }));
595
+
596
+ // src/tui/components/panels/projects-panel.tsx
597
+ import { jsx as jsx7 } from "react/jsx-runtime";
598
+ function ProjectsPanel({ height }) {
599
+ const projects = useProjectsStore((s) => s.projects);
600
+ const loading = useProjectsStore((s) => s.loading);
601
+ const error = useProjectsStore((s) => s.error);
602
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
603
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.projects);
604
+ const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
605
+ const isFocused = focusedPanel === "projects";
606
+ const items = projects.map((p) => ({
607
+ id: p.id,
608
+ label: p.name,
609
+ sublabel: p.status,
610
+ rightLabel: formatRelativeTime(p.updatedAt),
611
+ color: p.id === selectedProjectId ? "cyan" : getStatusColor(p.status)
612
+ }));
613
+ return /* @__PURE__ */ jsx7(Panel, { title: "PROJECTS", isFocused, height, children: loading && projects.length === 0 ? /* @__PURE__ */ jsx7(Spinner, { label: "Loading projects..." }) : error ? /* @__PURE__ */ jsx7(Text8, { color: "red", children: error }) : /* @__PURE__ */ jsx7(
614
+ ScrollableList,
615
+ {
616
+ items,
617
+ selectedIndex: scrollIndex,
618
+ height: height - 3,
619
+ isFocused
620
+ }
621
+ ) });
622
+ }
623
+
624
+ // src/tui/components/panels/plan-panel.tsx
625
+ import "react";
626
+ import { Box as Box7, Text as Text9 } from "ink";
627
+
628
+ // src/tui/stores/plan-store.ts
629
+ import { create as create4 } from "zustand";
630
+
631
+ // src/tui/lib/tree-builder.ts
632
+ function buildTree(nodes, edges) {
633
+ const adj = /* @__PURE__ */ new Map();
634
+ const hasParent = /* @__PURE__ */ new Set();
635
+ for (const edge of edges) {
636
+ const children = adj.get(edge.source) ?? [];
637
+ children.push(edge.target);
638
+ adj.set(edge.source, children);
639
+ hasParent.add(edge.target);
640
+ }
641
+ const lookup = /* @__PURE__ */ new Map();
642
+ for (const node of nodes) {
643
+ if (!node.deletedAt) lookup.set(node.id, node);
644
+ }
645
+ function buildSubtree(nodeId) {
646
+ const node = lookup.get(nodeId);
647
+ if (!node) return null;
648
+ const childIds = adj.get(nodeId) ?? [];
649
+ const children = [];
650
+ for (const childId of childIds) {
651
+ const child = buildSubtree(childId);
652
+ if (child) children.push(child);
653
+ }
654
+ return {
655
+ id: node.id,
656
+ title: node.title,
657
+ status: node.status,
658
+ type: node.type,
659
+ children
660
+ };
661
+ }
662
+ const roots = [];
663
+ for (const node of nodes) {
664
+ if (!node.deletedAt && !hasParent.has(node.id)) {
665
+ const tree = buildSubtree(node.id);
666
+ if (tree) roots.push(tree);
667
+ }
668
+ }
669
+ return roots;
670
+ }
671
+ function renderTreeLines(roots, collapsedSet) {
672
+ const lines = [];
673
+ function walk(node, prefix, isLast, depth) {
674
+ const connector = depth === 0 ? isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 " : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
675
+ const symbol = getStatusSymbol(node.status);
676
+ const collapsed = collapsedSet?.has(node.id) && node.children.length > 0;
677
+ const expandIcon = node.children.length > 0 ? collapsed ? "\u25B6 " : "\u25BC " : " ";
678
+ lines.push({
679
+ id: node.id,
680
+ text: `${prefix}${connector}${expandIcon}${symbol} ${node.title} [${node.status}]`,
681
+ status: node.status,
682
+ depth,
683
+ isLeaf: node.children.length === 0,
684
+ nodeTitle: node.title
685
+ });
686
+ if (collapsed) return;
687
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
688
+ for (let i = 0; i < node.children.length; i++) {
689
+ walk(node.children[i], childPrefix, i === node.children.length - 1, depth + 1);
690
+ }
691
+ }
692
+ for (let i = 0; i < roots.length; i++) {
693
+ walk(roots[i], "", i === roots.length - 1, 0);
694
+ }
695
+ return lines;
696
+ }
697
+
698
+ // src/tui/stores/plan-store.ts
699
+ var usePlanStore = create4((set, get) => ({
700
+ projectId: null,
701
+ nodes: [],
702
+ edges: [],
703
+ treeRoots: [],
704
+ treeLines: [],
705
+ collapsedNodes: /* @__PURE__ */ new Set(),
706
+ loading: false,
707
+ error: null,
708
+ setPlan: (projectId, nodes, edges) => {
709
+ const { collapsedNodes } = get();
710
+ const treeRoots = buildTree(nodes, edges);
711
+ const treeLines = renderTreeLines(treeRoots, collapsedNodes);
712
+ set({ projectId, nodes, edges, treeRoots, treeLines, loading: false, error: null });
713
+ },
714
+ setLoading: (loading) => set({ loading }),
715
+ setError: (error) => set({ error, loading: false }),
716
+ toggleCollapse: (nodeId) => {
717
+ const { collapsedNodes, treeRoots } = get();
718
+ const next = new Set(collapsedNodes);
719
+ if (next.has(nodeId)) {
720
+ next.delete(nodeId);
721
+ } else {
722
+ next.add(nodeId);
723
+ }
724
+ const treeLines = renderTreeLines(treeRoots, next);
725
+ set({ collapsedNodes: next, treeLines });
726
+ },
727
+ updateNodeStatus: (nodeId, status) => {
728
+ const { nodes, edges, collapsedNodes } = get();
729
+ const updated = nodes.map((n) => n.id === nodeId ? { ...n, status } : n);
730
+ const treeRoots = buildTree(updated, edges);
731
+ const treeLines = renderTreeLines(treeRoots, collapsedNodes);
732
+ set({ nodes: updated, treeRoots, treeLines });
733
+ },
734
+ clear: () => set({
735
+ projectId: null,
736
+ nodes: [],
737
+ edges: [],
738
+ treeRoots: [],
739
+ treeLines: [],
740
+ loading: false,
741
+ error: null
742
+ })
743
+ }));
744
+
745
+ // src/tui/components/panels/plan-panel.tsx
746
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
747
+ function PlanPanel({ height }) {
748
+ const treeLines = usePlanStore((s) => s.treeLines);
749
+ const loading = usePlanStore((s) => s.loading);
750
+ const error = usePlanStore((s) => s.error);
751
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
752
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.plan);
753
+ const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
754
+ const projects = useProjectsStore((s) => s.projects);
755
+ const isFocused = focusedPanel === "plan";
756
+ const projectName = projects.find((p) => p.id === selectedProjectId)?.name ?? "none";
757
+ const visibleHeight = Math.max(1, height - 4);
758
+ let start = 0;
759
+ if (scrollIndex >= visibleHeight) {
760
+ start = scrollIndex - visibleHeight + 1;
761
+ }
762
+ const visibleLines = treeLines.slice(start, start + visibleHeight);
763
+ return /* @__PURE__ */ jsx8(Panel, { title: `PLAN (${projectName})`, isFocused, height, children: loading && treeLines.length === 0 ? /* @__PURE__ */ jsx8(Spinner, { label: "Loading plan..." }) : error ? /* @__PURE__ */ jsx8(Text9, { color: "red", children: error }) : !selectedProjectId ? /* @__PURE__ */ jsx8(Text9, { dimColor: true, children: " Select a project to view its plan" }) : treeLines.length === 0 ? /* @__PURE__ */ jsx8(Text9, { dimColor: true, children: " No plan nodes. Use :plan create-node <title>" }) : /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
764
+ visibleLines.map((line, i) => {
765
+ const actualIndex = start + i;
766
+ const isSelected = actualIndex === scrollIndex && isFocused;
767
+ return /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsx8(
768
+ Text9,
769
+ {
770
+ color: isSelected ? "cyan" : getStatusColor(line.status),
771
+ bold: isSelected,
772
+ inverse: isSelected,
773
+ children: line.text
774
+ }
775
+ ) }, line.id + "-" + actualIndex);
776
+ }),
777
+ treeLines.length > visibleHeight && /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
778
+ " [",
779
+ start + 1,
780
+ "-",
781
+ Math.min(start + visibleHeight, treeLines.length),
782
+ "/",
783
+ treeLines.length,
784
+ "]"
785
+ ] })
786
+ ] }) });
787
+ }
788
+
789
+ // src/tui/components/panels/machines-panel.tsx
790
+ import "react";
791
+ import { Text as Text10 } from "ink";
792
+
793
+ // src/tui/stores/machines-store.ts
794
+ import { create as create5 } from "zustand";
795
+ var useMachinesStore = create5((set, get) => ({
796
+ machines: [],
797
+ loading: false,
798
+ error: null,
799
+ setMachines: (machines) => set({ machines, loading: false, error: null }),
800
+ updateMachine: (id, patch) => {
801
+ set((s) => ({
802
+ machines: s.machines.map((m) => m.id === id ? { ...m, ...patch } : m)
803
+ }));
804
+ },
805
+ removeMachine: (id) => {
806
+ set((s) => ({ machines: s.machines.filter((m) => m.id !== id) }));
807
+ },
808
+ addMachine: (machine) => {
809
+ const { machines } = get();
810
+ const existing = machines.findIndex((m) => m.id === machine.id);
811
+ if (existing >= 0) {
812
+ set({ machines: machines.map((m, i) => i === existing ? machine : m) });
813
+ } else {
814
+ set({ machines: [...machines, machine] });
815
+ }
816
+ },
817
+ setLoading: (loading) => set({ loading }),
818
+ setError: (error) => set({ error, loading: false })
819
+ }));
820
+
821
+ // src/tui/components/panels/machines-panel.tsx
822
+ import { jsx as jsx9 } from "react/jsx-runtime";
823
+ function MachinesPanel({ height }) {
824
+ const machines = useMachinesStore((s) => s.machines);
825
+ const loading = useMachinesStore((s) => s.loading);
826
+ const error = useMachinesStore((s) => s.error);
827
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
828
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.machines);
829
+ const isFocused = focusedPanel === "machines";
830
+ const activeMachines = machines.filter((m) => !m.isRevoked);
831
+ const items = activeMachines.map((m) => ({
832
+ id: m.id,
833
+ label: m.name,
834
+ sublabel: m.platform,
835
+ rightLabel: m.isConnected ? "\u25CF online" : "\u25CB offline",
836
+ color: m.isConnected ? "green" : "gray"
837
+ }));
838
+ return /* @__PURE__ */ jsx9(Panel, { title: "MACHINES", isFocused, height, children: loading && machines.length === 0 ? /* @__PURE__ */ jsx9(Spinner, { label: "Loading machines..." }) : error ? /* @__PURE__ */ jsx9(Text10, { color: "red", children: error }) : /* @__PURE__ */ jsx9(
839
+ ScrollableList,
840
+ {
841
+ items,
842
+ selectedIndex: scrollIndex,
843
+ height: height - 3,
844
+ isFocused
845
+ }
846
+ ) });
847
+ }
848
+
849
+ // src/tui/components/panels/output-panel.tsx
850
+ import "react";
851
+ import { Box as Box8, Text as Text11 } from "ink";
852
+
853
+ // src/tui/stores/execution-store.ts
854
+ import { create as create6 } from "zustand";
855
+ var MAX_LINES = 5e3;
856
+ function flushToolDots(output) {
857
+ if (output.pendingToolCount === 0) return output.lines;
858
+ const dots = "\xB7".repeat(output.pendingToolCount);
859
+ const line = `[Tool Call] ${dots}`;
860
+ const lines = [...output.lines, line];
861
+ output.pendingToolCount = 0;
862
+ return lines;
863
+ }
864
+ function trimRingBuffer(lines) {
865
+ if (lines.length > MAX_LINES) {
866
+ lines.splice(0, lines.length - MAX_LINES);
867
+ }
868
+ return lines;
869
+ }
870
+ var useExecutionStore = create6((set, get) => ({
871
+ outputs: /* @__PURE__ */ new Map(),
872
+ watchingId: null,
873
+ initExecution: (executionId, nodeId) => {
874
+ const { outputs } = get();
875
+ const next = new Map(outputs);
876
+ next.set(executionId, {
877
+ executionId,
878
+ nodeId,
879
+ lines: [],
880
+ status: "running",
881
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
882
+ pendingToolCount: 0
883
+ });
884
+ set({ outputs: next });
885
+ },
886
+ appendToolCall: (executionId) => {
887
+ const { outputs } = get();
888
+ const current = outputs.get(executionId);
889
+ if (!current) return;
890
+ const next = new Map(outputs);
891
+ next.set(executionId, { ...current, pendingToolCount: current.pendingToolCount + 1 });
892
+ set({ outputs: next });
893
+ },
894
+ appendFileChange: (executionId, path, action, added, removed) => {
895
+ const { outputs } = get();
896
+ const current = outputs.get(executionId);
897
+ if (!current) return;
898
+ const lines = flushToolDots({ ...current });
899
+ const stats = [
900
+ added != null && added > 0 ? `+${added}` : null,
901
+ removed != null && removed > 0 ? `-${removed}` : null
902
+ ].filter(Boolean).join(" ");
903
+ lines.push(`[${action}] ${path}${stats ? ` (${stats})` : ""}`);
904
+ trimRingBuffer(lines);
905
+ const next = new Map(outputs);
906
+ next.set(executionId, { ...current, lines, pendingToolCount: 0 });
907
+ set({ outputs: next });
908
+ },
909
+ appendLine: (executionId, line) => {
910
+ const { outputs } = get();
911
+ const current = outputs.get(executionId);
912
+ if (!current) return;
913
+ const lines = flushToolDots({ ...current });
914
+ lines.push(line);
915
+ trimRingBuffer(lines);
916
+ const next = new Map(outputs);
917
+ next.set(executionId, { ...current, lines, pendingToolCount: 0 });
918
+ set({ outputs: next });
919
+ },
920
+ appendText: (executionId, text) => {
921
+ const { outputs } = get();
922
+ const current = outputs.get(executionId);
923
+ if (!current) return;
924
+ const lines = flushToolDots({ ...current });
925
+ const newLines = text.split("\n");
926
+ if (lines.length > 0 && newLines.length > 0) {
927
+ lines[lines.length - 1] += newLines[0];
928
+ for (let i = 1; i < newLines.length; i++) {
929
+ lines.push(newLines[i]);
930
+ }
931
+ } else {
932
+ lines.push(...newLines);
933
+ }
934
+ trimRingBuffer(lines);
935
+ const next = new Map(outputs);
936
+ next.set(executionId, { ...current, lines, pendingToolCount: 0 });
937
+ set({ outputs: next });
938
+ },
939
+ setStatus: (executionId, status) => {
940
+ const { outputs } = get();
941
+ const current = outputs.get(executionId);
942
+ if (!current) return;
943
+ const lines = flushToolDots({ ...current });
944
+ const next = new Map(outputs);
945
+ next.set(executionId, { ...current, lines, status, pendingToolCount: 0 });
946
+ set({ outputs: next });
947
+ },
948
+ setWatching: (watchingId) => set({ watchingId }),
949
+ clear: (executionId) => {
950
+ const { outputs } = get();
951
+ const next = new Map(outputs);
952
+ next.delete(executionId);
953
+ set({ outputs: next });
954
+ }
955
+ }));
956
+
957
+ // src/tui/components/panels/output-panel.tsx
958
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
959
+ function lineColor(line) {
960
+ if (line.startsWith("[Tool Call]")) return "gray";
961
+ if (line.startsWith("[error]")) return "red";
962
+ if (line.startsWith("[progress]")) return "cyan";
963
+ if (line.startsWith("[created]") || line.startsWith("[modified]") || line.startsWith("[deleted]")) return "yellow";
964
+ return void 0;
965
+ }
966
+ function OutputPanel({ height }) {
967
+ const watchingId = useExecutionStore((s) => s.watchingId);
968
+ const outputs = useExecutionStore((s) => s.outputs);
969
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
970
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.output);
971
+ const isFocused = focusedPanel === "output";
972
+ const execution = watchingId ? outputs.get(watchingId) : null;
973
+ const visibleHeight = Math.max(1, height - 4);
974
+ let title = "OUTPUT";
975
+ if (execution) {
976
+ const shortId = execution.nodeId.length > 20 ? execution.nodeId.slice(0, 8) + "\u2026" : execution.nodeId;
977
+ title = `OUTPUT (${shortId}) [${execution.status}]`;
978
+ }
979
+ if (!execution) {
980
+ return /* @__PURE__ */ jsx10(Panel, { title, isFocused, height, children: /* @__PURE__ */ jsx10(Text11, { dimColor: true, children: " No active execution. Dispatch a task with 'd' or :dispatch" }) });
981
+ }
982
+ const lines = execution.lines;
983
+ const isRunning = execution.status === "running";
984
+ const hasPendingTools = execution.pendingToolCount > 0;
985
+ let start;
986
+ if (scrollIndex >= lines.length - visibleHeight) {
987
+ start = Math.max(0, lines.length - visibleHeight);
988
+ } else {
989
+ start = Math.max(0, scrollIndex);
990
+ }
991
+ const visibleLines = lines.slice(start, start + visibleHeight);
992
+ return /* @__PURE__ */ jsx10(Panel, { title, isFocused, height, children: /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
993
+ visibleLines.map((line, i) => /* @__PURE__ */ jsx10(Text11, { color: lineColor(line), dimColor: line.startsWith("[Tool Call]"), wrap: "truncate", children: truncate(line, 200) }, start + i)),
994
+ hasPendingTools && /* @__PURE__ */ jsx10(Text11, { dimColor: true, children: "[Tool Call] " + "\xB7".repeat(execution.pendingToolCount) }),
995
+ isRunning && !hasPendingTools && /* @__PURE__ */ jsx10(Spinner, { label: "Running..." }),
996
+ lines.length > visibleHeight && /* @__PURE__ */ jsxs9(Text11, { dimColor: true, children: [
997
+ " [",
998
+ start + 1,
999
+ "-",
1000
+ Math.min(start + visibleHeight, lines.length),
1001
+ "/",
1002
+ lines.length,
1003
+ "]"
1004
+ ] })
1005
+ ] }) });
1006
+ }
1007
+
1008
+ // src/tui/components/panels/detail-overlay.tsx
1009
+ import "react";
1010
+ import { Box as Box9, Text as Text12 } from "ink";
1011
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1012
+ function DetailOverlay() {
1013
+ const showDetail = useTuiStore((s) => s.showDetail);
1014
+ const detailType = useTuiStore((s) => s.detailType);
1015
+ const detailId = useTuiStore((s) => s.detailId);
1016
+ if (!showDetail || !detailType || !detailId) return null;
1017
+ return /* @__PURE__ */ jsxs10(
1018
+ Box9,
1019
+ {
1020
+ flexDirection: "column",
1021
+ borderStyle: "round",
1022
+ borderColor: "cyan",
1023
+ paddingX: 2,
1024
+ paddingY: 1,
1025
+ children: [
1026
+ detailType === "project" && /* @__PURE__ */ jsx11(ProjectDetail, { id: detailId }),
1027
+ detailType === "node" && /* @__PURE__ */ jsx11(NodeDetail, { id: detailId }),
1028
+ detailType === "machine" && /* @__PURE__ */ jsx11(MachineDetail, { id: detailId }),
1029
+ /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "Press Esc to close" })
1030
+ ]
1031
+ }
1032
+ );
1033
+ }
1034
+ function ProjectDetail({ id }) {
1035
+ const project = useProjectsStore((s) => s.projects.find((p) => p.id === id));
1036
+ if (!project) return /* @__PURE__ */ jsx11(Text12, { color: "red", children: "Project not found" });
1037
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1038
+ /* @__PURE__ */ jsx11(Text12, { bold: true, color: "cyan", children: project.name }),
1039
+ /* @__PURE__ */ jsx11(Text12, { children: " " }),
1040
+ /* @__PURE__ */ jsx11(Field, { label: "ID", value: project.id }),
1041
+ /* @__PURE__ */ jsx11(Field, { label: "Status", value: project.status, color: getStatusColor(project.status) }),
1042
+ /* @__PURE__ */ jsx11(Field, { label: "Description", value: project.description || "\u2014" }),
1043
+ /* @__PURE__ */ jsx11(Field, { label: "Health", value: project.health ?? "\u2014" }),
1044
+ /* @__PURE__ */ jsx11(Field, { label: "Progress", value: `${project.progress}%` }),
1045
+ /* @__PURE__ */ jsx11(Field, { label: "Working Dir", value: project.workingDirectory ?? "\u2014" }),
1046
+ /* @__PURE__ */ jsx11(Field, { label: "Repository", value: project.repository ?? "\u2014" }),
1047
+ /* @__PURE__ */ jsx11(Field, { label: "Delivery", value: project.deliveryMode ?? "\u2014" }),
1048
+ /* @__PURE__ */ jsx11(Field, { label: "Start Date", value: project.startDate ?? "\u2014" }),
1049
+ /* @__PURE__ */ jsx11(Field, { label: "Target Date", value: project.targetDate ?? "\u2014" }),
1050
+ /* @__PURE__ */ jsx11(Field, { label: "Created", value: formatRelativeTime(project.createdAt) }),
1051
+ /* @__PURE__ */ jsx11(Field, { label: "Updated", value: formatRelativeTime(project.updatedAt) }),
1052
+ /* @__PURE__ */ jsx11(Text12, { children: " " })
1053
+ ] });
1054
+ }
1055
+ function NodeDetail({ id }) {
1056
+ const node = usePlanStore((s) => s.nodes.find((n) => n.id === id));
1057
+ if (!node) return /* @__PURE__ */ jsx11(Text12, { color: "red", children: "Node not found" });
1058
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1059
+ /* @__PURE__ */ jsx11(Text12, { bold: true, color: "cyan", children: node.title }),
1060
+ /* @__PURE__ */ jsx11(Text12, { children: " " }),
1061
+ /* @__PURE__ */ jsx11(Field, { label: "ID", value: node.id }),
1062
+ /* @__PURE__ */ jsx11(Field, { label: "Type", value: node.type }),
1063
+ /* @__PURE__ */ jsx11(Field, { label: "Status", value: node.status, color: getStatusColor(node.status) }),
1064
+ /* @__PURE__ */ jsx11(Field, { label: "Description", value: node.description || "\u2014" }),
1065
+ /* @__PURE__ */ jsx11(Field, { label: "Priority", value: node.priority ?? "\u2014" }),
1066
+ /* @__PURE__ */ jsx11(Field, { label: "Estimate", value: node.estimate ?? "\u2014" }),
1067
+ /* @__PURE__ */ jsx11(Field, { label: "Start Date", value: node.startDate ?? "\u2014" }),
1068
+ /* @__PURE__ */ jsx11(Field, { label: "End Date", value: node.endDate ?? "\u2014" }),
1069
+ /* @__PURE__ */ jsx11(Field, { label: "Due Date", value: node.dueDate ?? "\u2014" }),
1070
+ /* @__PURE__ */ jsx11(Field, { label: "Branch", value: node.branchName ?? "\u2014" }),
1071
+ /* @__PURE__ */ jsx11(Field, { label: "PR URL", value: node.prUrl ?? "\u2014" }),
1072
+ /* @__PURE__ */ jsx11(Field, { label: "Execution ID", value: node.executionId ?? "\u2014" }),
1073
+ /* @__PURE__ */ jsx11(Field, { label: "Exec Started", value: node.executionStartedAt ? formatRelativeTime(node.executionStartedAt) : "\u2014" }),
1074
+ /* @__PURE__ */ jsx11(Field, { label: "Exec Completed", value: node.executionCompletedAt ? formatRelativeTime(node.executionCompletedAt) : "\u2014" }),
1075
+ /* @__PURE__ */ jsx11(Text12, { children: " " })
1076
+ ] });
1077
+ }
1078
+ function MachineDetail({ id }) {
1079
+ const machine = useMachinesStore((s) => s.machines.find((m) => m.id === id));
1080
+ if (!machine) return /* @__PURE__ */ jsx11(Text12, { color: "red", children: "Machine not found" });
1081
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1082
+ /* @__PURE__ */ jsx11(Text12, { bold: true, color: "cyan", children: machine.name }),
1083
+ /* @__PURE__ */ jsx11(Text12, { children: " " }),
1084
+ /* @__PURE__ */ jsx11(Field, { label: "ID", value: machine.id }),
1085
+ /* @__PURE__ */ jsx11(Field, { label: "Hostname", value: machine.hostname }),
1086
+ /* @__PURE__ */ jsx11(Field, { label: "Platform", value: machine.platform }),
1087
+ /* @__PURE__ */ jsx11(Field, { label: "Env Type", value: machine.environmentType }),
1088
+ /* @__PURE__ */ jsx11(Field, { label: "Connected", value: machine.isConnected ? "Yes" : "No", color: machine.isConnected ? "green" : "red" }),
1089
+ /* @__PURE__ */ jsx11(Field, { label: "Providers", value: machine.providers.join(", ") || "\u2014" }),
1090
+ /* @__PURE__ */ jsx11(Field, { label: "Registered", value: formatRelativeTime(machine.registeredAt) }),
1091
+ /* @__PURE__ */ jsx11(Field, { label: "Last Seen", value: formatRelativeTime(machine.lastSeenAt) }),
1092
+ /* @__PURE__ */ jsx11(Text12, { children: " " })
1093
+ ] });
1094
+ }
1095
+ function Field({ label, value, color }) {
1096
+ return /* @__PURE__ */ jsxs10(Box9, { children: [
1097
+ /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: label.padEnd(16) }),
1098
+ color ? /* @__PURE__ */ jsx11(Text12, { color, children: value }) : /* @__PURE__ */ jsx11(Text12, { children: value })
1099
+ ] });
1100
+ }
1101
+
1102
+ // src/tui/components/layout/main-layout.tsx
1103
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1104
+ function MainLayout() {
1105
+ const showHelp = useTuiStore((s) => s.showHelp);
1106
+ const showDetail = useTuiStore((s) => s.showDetail);
1107
+ const { stdout } = useStdout();
1108
+ const termHeight = stdout?.rows ?? 24;
1109
+ const termWidth = stdout?.columns ?? 80;
1110
+ const topRowHeight = Math.floor((termHeight - 4) / 2);
1111
+ const bottomRowHeight = termHeight - 4 - topRowHeight;
1112
+ if (showHelp) {
1113
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1114
+ /* @__PURE__ */ jsx12(StatusBar, {}),
1115
+ /* @__PURE__ */ jsx12(HelpOverlay, {}),
1116
+ /* @__PURE__ */ jsx12(CommandLine, {})
1117
+ ] });
1118
+ }
1119
+ if (showDetail) {
1120
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1121
+ /* @__PURE__ */ jsx12(StatusBar, {}),
1122
+ /* @__PURE__ */ jsx12(DetailOverlay, {}),
1123
+ /* @__PURE__ */ jsx12(CommandLine, {})
1124
+ ] });
1125
+ }
1126
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1127
+ /* @__PURE__ */ jsx12(StatusBar, {}),
1128
+ /* @__PURE__ */ jsxs11(Box10, { flexDirection: "row", height: topRowHeight, children: [
1129
+ /* @__PURE__ */ jsx12(Box10, { width: "30%", children: /* @__PURE__ */ jsx12(ProjectsPanel, { height: topRowHeight }) }),
1130
+ /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, children: /* @__PURE__ */ jsx12(PlanPanel, { height: topRowHeight }) })
1131
+ ] }),
1132
+ /* @__PURE__ */ jsxs11(Box10, { flexDirection: "row", height: bottomRowHeight, children: [
1133
+ /* @__PURE__ */ jsx12(Box10, { width: "30%", children: /* @__PURE__ */ jsx12(MachinesPanel, { height: bottomRowHeight }) }),
1134
+ /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, children: /* @__PURE__ */ jsx12(OutputPanel, { height: bottomRowHeight }) })
1135
+ ] }),
1136
+ /* @__PURE__ */ jsx12(SearchOverlay, {}),
1137
+ /* @__PURE__ */ jsx12(CommandLine, {})
1138
+ ] });
1139
+ }
1140
+
1141
+ // src/tui/hooks/use-vim-mode.ts
1142
+ import { useCallback, useRef } from "react";
1143
+ import { useInput as useInput2, useApp } from "ink";
1144
+
1145
+ // src/tui/lib/vim-state-machine.ts
1146
+ function initialVimState() {
1147
+ return {
1148
+ mode: "normal",
1149
+ commandBuffer: "",
1150
+ searchQuery: "",
1151
+ pendingKeys: ""
1152
+ };
1153
+ }
1154
+ function vimReducer(state, action) {
1155
+ switch (action.type) {
1156
+ case "set_mode":
1157
+ return [{ ...state, mode: action.mode, pendingKeys: "" }, { type: "none" }];
1158
+ case "clear_command":
1159
+ return [{ ...state, commandBuffer: "", mode: "normal", pendingKeys: "" }, { type: "none" }];
1160
+ case "clear_search":
1161
+ return [{ ...state, searchQuery: "", mode: "normal", pendingKeys: "" }, { type: "none" }];
1162
+ case "submit_command":
1163
+ return [
1164
+ { ...state, mode: "normal", pendingKeys: "" },
1165
+ { type: "command", value: state.commandBuffer }
1166
+ ];
1167
+ case "submit_search":
1168
+ return [
1169
+ { ...state, mode: "normal", pendingKeys: "" },
1170
+ { type: "search", value: state.searchQuery }
1171
+ ];
1172
+ case "key":
1173
+ return handleKey(state, action.key, action.ctrl);
1174
+ }
1175
+ }
1176
+ function handleKey(state, key, ctrl) {
1177
+ if (key === "escape") {
1178
+ return [{ ...state, mode: "normal", pendingKeys: "", commandBuffer: "", searchQuery: "" }, { type: "none" }];
1179
+ }
1180
+ switch (state.mode) {
1181
+ case "normal":
1182
+ return handleNormalMode(state, key, ctrl);
1183
+ case "command":
1184
+ return handleCommandMode(state, key);
1185
+ case "search":
1186
+ return handleSearchMode(state, key);
1187
+ case "insert":
1188
+ return handleInsertMode(state, key);
1189
+ }
1190
+ }
1191
+ function handleNormalMode(state, key, ctrl) {
1192
+ if (ctrl) {
1193
+ if (key === "u") return [state, { type: "scroll", direction: "page_up" }];
1194
+ if (key === "d") return [state, { type: "scroll", direction: "page_down" }];
1195
+ if (key === "c") return [state, { type: "quit" }];
1196
+ return [state, { type: "none" }];
1197
+ }
1198
+ if (state.pendingKeys === "g") {
1199
+ if (key === "g") {
1200
+ return [{ ...state, pendingKeys: "" }, { type: "scroll", direction: "top" }];
1201
+ }
1202
+ return [{ ...state, pendingKeys: "" }, { type: "none" }];
1203
+ }
1204
+ switch (key) {
1205
+ // Navigation
1206
+ case "j":
1207
+ case "return":
1208
+ if (key === "j") return [state, { type: "scroll", direction: "down" }];
1209
+ return [state, { type: "select" }];
1210
+ case "k":
1211
+ return [state, { type: "scroll", direction: "up" }];
1212
+ case "h":
1213
+ return [state, { type: "focus", direction: "left" }];
1214
+ case "l":
1215
+ return [state, { type: "focus", direction: "right" }];
1216
+ // Panel jump
1217
+ case "1":
1218
+ return [state, { type: "focus", panel: 0 }];
1219
+ case "2":
1220
+ return [state, { type: "focus", panel: 1 }];
1221
+ case "3":
1222
+ return [state, { type: "focus", panel: 2 }];
1223
+ case "4":
1224
+ return [state, { type: "focus", panel: 3 }];
1225
+ case "tab":
1226
+ return [state, { type: "focus", direction: "right" }];
1227
+ // Top/bottom
1228
+ case "g":
1229
+ return [{ ...state, pendingKeys: "g" }, { type: "none" }];
1230
+ case "G":
1231
+ return [state, { type: "scroll", direction: "bottom" }];
1232
+ // Mode switches
1233
+ case ":":
1234
+ return [{ ...state, mode: "command", commandBuffer: "" }, { type: "none" }];
1235
+ case "/":
1236
+ return [{ ...state, mode: "search", searchQuery: "" }, { type: "none" }];
1237
+ case "i":
1238
+ return [{ ...state, mode: "insert" }, { type: "none" }];
1239
+ // Actions
1240
+ case "d":
1241
+ return [state, { type: "dispatch" }];
1242
+ case "c":
1243
+ return [state, { type: "cancel" }];
1244
+ case "r":
1245
+ return [state, { type: "refresh" }];
1246
+ case "q":
1247
+ return [state, { type: "quit" }];
1248
+ case "?":
1249
+ return [state, { type: "help" }];
1250
+ default:
1251
+ return [state, { type: "none" }];
1252
+ }
1253
+ }
1254
+ function handleCommandMode(state, key) {
1255
+ if (key === "return") {
1256
+ return [
1257
+ { ...state, mode: "normal" },
1258
+ { type: "command", value: state.commandBuffer }
1259
+ ];
1260
+ }
1261
+ if (key === "backspace" || key === "delete") {
1262
+ const newBuffer = state.commandBuffer.slice(0, -1);
1263
+ if (newBuffer.length === 0) {
1264
+ return [{ ...state, mode: "normal", commandBuffer: "" }, { type: "none" }];
1265
+ }
1266
+ return [{ ...state, commandBuffer: newBuffer }, { type: "none" }];
1267
+ }
1268
+ if (key === "tab") {
1269
+ return [state, { type: "command", value: `__autocomplete__${state.commandBuffer}` }];
1270
+ }
1271
+ if (key.length === 1) {
1272
+ return [{ ...state, commandBuffer: state.commandBuffer + key }, { type: "none" }];
1273
+ }
1274
+ return [state, { type: "none" }];
1275
+ }
1276
+ function handleSearchMode(state, key) {
1277
+ if (key === "return") {
1278
+ return [
1279
+ { ...state, mode: "normal" },
1280
+ { type: "search", value: state.searchQuery }
1281
+ ];
1282
+ }
1283
+ if (key === "backspace" || key === "delete") {
1284
+ const newQuery = state.searchQuery.slice(0, -1);
1285
+ if (newQuery.length === 0) {
1286
+ return [{ ...state, mode: "normal", searchQuery: "" }, { type: "none" }];
1287
+ }
1288
+ return [{ ...state, searchQuery: newQuery }, { type: "search", value: newQuery }];
1289
+ }
1290
+ if (key.length === 1) {
1291
+ const newQuery = state.searchQuery + key;
1292
+ return [{ ...state, searchQuery: newQuery }, { type: "search", value: newQuery }];
1293
+ }
1294
+ return [state, { type: "none" }];
1295
+ }
1296
+ function handleInsertMode(state, _) {
1297
+ return [state, { type: "none" }];
1298
+ }
1299
+
1300
+ // src/tui/hooks/use-vim-mode.ts
1301
+ function useVimMode(callbacks = {}) {
1302
+ const { exit } = useApp();
1303
+ const vimState = useRef(initialVimState());
1304
+ const store = useTuiStore();
1305
+ const handleEffect = useCallback(
1306
+ (effect) => {
1307
+ switch (effect.type) {
1308
+ case "scroll":
1309
+ switch (effect.direction) {
1310
+ case "up":
1311
+ store.scrollUp();
1312
+ break;
1313
+ case "down":
1314
+ store.scrollDown();
1315
+ break;
1316
+ case "top":
1317
+ store.scrollToTop();
1318
+ break;
1319
+ case "bottom":
1320
+ store.scrollToBottom();
1321
+ break;
1322
+ case "page_up":
1323
+ store.pageUp();
1324
+ break;
1325
+ case "page_down":
1326
+ store.pageDown();
1327
+ break;
1328
+ }
1329
+ break;
1330
+ case "focus":
1331
+ if (effect.panel != null) {
1332
+ store.focusByIndex(effect.panel);
1333
+ } else if (effect.direction === "left") {
1334
+ store.focusPrev();
1335
+ } else if (effect.direction === "right") {
1336
+ store.focusNext();
1337
+ }
1338
+ break;
1339
+ case "select":
1340
+ callbacks.onSelect?.();
1341
+ break;
1342
+ case "command":
1343
+ if (effect.value?.startsWith("__autocomplete__")) {
1344
+ } else if (effect.value) {
1345
+ callbacks.onCommand?.(effect.value);
1346
+ }
1347
+ break;
1348
+ case "search":
1349
+ if (effect.value) {
1350
+ callbacks.onSearch?.(effect.value);
1351
+ }
1352
+ break;
1353
+ case "dispatch":
1354
+ callbacks.onDispatch?.();
1355
+ break;
1356
+ case "cancel":
1357
+ callbacks.onCancel?.();
1358
+ break;
1359
+ case "refresh":
1360
+ callbacks.onRefresh?.();
1361
+ break;
1362
+ case "quit":
1363
+ exit();
1364
+ break;
1365
+ case "help":
1366
+ store.toggleHelp();
1367
+ break;
1368
+ case "none":
1369
+ break;
1370
+ }
1371
+ },
1372
+ [store, callbacks, exit]
1373
+ );
1374
+ useInput2((input, key) => {
1375
+ if (store.showHelp || store.showSearch || store.showDetail) {
1376
+ if (key.escape) {
1377
+ store.closeOverlays();
1378
+ vimState.current = initialVimState();
1379
+ store.setMode("normal");
1380
+ store.setCommandBuffer("");
1381
+ store.setSearchQuery("");
1382
+ store.setPendingKeys("");
1383
+ }
1384
+ return;
1385
+ }
1386
+ let keyStr = input;
1387
+ if (key.escape) keyStr = "escape";
1388
+ else if (key.return) keyStr = "return";
1389
+ else if (key.backspace || key.delete) keyStr = "backspace";
1390
+ else if (key.tab) keyStr = "tab";
1391
+ else if (key.upArrow) keyStr = "k";
1392
+ else if (key.downArrow) keyStr = "j";
1393
+ else if (key.leftArrow) keyStr = "h";
1394
+ else if (key.rightArrow) keyStr = "l";
1395
+ const [nextState, effect] = vimReducer(vimState.current, {
1396
+ type: "key",
1397
+ key: keyStr,
1398
+ ctrl: key.ctrl,
1399
+ shift: key.shift
1400
+ });
1401
+ vimState.current = nextState;
1402
+ store.setMode(nextState.mode);
1403
+ store.setCommandBuffer(nextState.commandBuffer);
1404
+ store.setSearchQuery(nextState.searchQuery);
1405
+ store.setPendingKeys(nextState.pendingKeys);
1406
+ handleEffect(effect);
1407
+ });
1408
+ }
1409
+
1410
+ // src/tui/hooks/use-polling.ts
1411
+ import { useEffect as useEffect2, useCallback as useCallback2 } from "react";
1412
+ function usePolling(client, intervalMs = 3e4) {
1413
+ const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
1414
+ const loadProjects = useCallback2(async () => {
1415
+ useProjectsStore.getState().setLoading(true);
1416
+ try {
1417
+ const projects = await client.listProjects();
1418
+ useProjectsStore.getState().setProjects(projects);
1419
+ } catch (err) {
1420
+ useProjectsStore.getState().setError(err instanceof Error ? err.message : String(err));
1421
+ }
1422
+ }, [client]);
1423
+ const loadPlan = useCallback2(async (projectId) => {
1424
+ usePlanStore.getState().setLoading(true);
1425
+ try {
1426
+ const { nodes, edges } = await client.getPlan(projectId);
1427
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
1428
+ } catch (err) {
1429
+ usePlanStore.getState().setError(err instanceof Error ? err.message : String(err));
1430
+ }
1431
+ }, [client]);
1432
+ const loadMachines = useCallback2(async () => {
1433
+ useMachinesStore.getState().setLoading(true);
1434
+ try {
1435
+ const machines = await client.listMachines();
1436
+ useMachinesStore.getState().setMachines(machines);
1437
+ useTuiStore.getState().setMachineCount(machines.filter((m) => m.isConnected).length);
1438
+ } catch (err) {
1439
+ useMachinesStore.getState().setError(err instanceof Error ? err.message : String(err));
1440
+ }
1441
+ }, [client]);
1442
+ const refreshAll2 = useCallback2(async () => {
1443
+ await Promise.allSettled([
1444
+ loadProjects(),
1445
+ loadMachines(),
1446
+ ...selectedProjectId ? [loadPlan(selectedProjectId)] : []
1447
+ ]);
1448
+ }, [loadProjects, loadMachines, loadPlan, selectedProjectId]);
1449
+ useEffect2(() => {
1450
+ refreshAll2();
1451
+ }, [refreshAll2]);
1452
+ useEffect2(() => {
1453
+ if (selectedProjectId) {
1454
+ loadPlan(selectedProjectId);
1455
+ } else {
1456
+ usePlanStore.getState().clear();
1457
+ }
1458
+ }, [selectedProjectId, loadPlan]);
1459
+ useEffect2(() => {
1460
+ const timer = setInterval(refreshAll2, intervalMs);
1461
+ return () => clearInterval(timer);
1462
+ }, [refreshAll2, intervalMs]);
1463
+ return { refreshAll: refreshAll2, loadProjects, loadPlan, loadMachines };
1464
+ }
1465
+
1466
+ // src/tui/hooks/use-sse-stream.ts
1467
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
1468
+
1469
+ // src/tui/sse-client.ts
1470
+ var SSEClient = class {
1471
+ client;
1472
+ abortController = null;
1473
+ handler;
1474
+ reconnectDelay = 1e3;
1475
+ maxReconnectDelay = 3e4;
1476
+ _connected = false;
1477
+ _stopped = false;
1478
+ constructor(client, handler) {
1479
+ this.client = client;
1480
+ this.handler = handler;
1481
+ }
1482
+ get connected() {
1483
+ return this._connected;
1484
+ }
1485
+ async start() {
1486
+ this._stopped = false;
1487
+ this.reconnectDelay = 1e3;
1488
+ await this.connect();
1489
+ }
1490
+ stop() {
1491
+ this._stopped = true;
1492
+ this._connected = false;
1493
+ if (this.abortController) {
1494
+ this.abortController.abort();
1495
+ this.abortController = null;
1496
+ }
1497
+ }
1498
+ async connect() {
1499
+ if (this._stopped) return;
1500
+ try {
1501
+ const response = await this.client.streamEvents();
1502
+ this._connected = true;
1503
+ this.reconnectDelay = 1e3;
1504
+ this.handler({ type: "__connected", data: {} });
1505
+ if (!response.body) return;
1506
+ const reader = response.body.getReader();
1507
+ const decoder = new TextDecoder();
1508
+ let buffer = "";
1509
+ let eventType = "";
1510
+ while (!this._stopped) {
1511
+ const { done, value } = await reader.read();
1512
+ if (done) break;
1513
+ buffer += decoder.decode(value, { stream: true });
1514
+ const lines = buffer.split("\n");
1515
+ buffer = lines.pop() ?? "";
1516
+ for (const line of lines) {
1517
+ if (line.startsWith("event: ")) {
1518
+ eventType = line.slice(7).trim();
1519
+ } else if (line.startsWith("data: ")) {
1520
+ const dataStr = line.slice(6);
1521
+ try {
1522
+ const data = JSON.parse(dataStr);
1523
+ this.handler({ type: eventType || "message", data });
1524
+ } catch {
1525
+ this.handler({ type: eventType || "message", data: { raw: dataStr } });
1526
+ }
1527
+ eventType = "";
1528
+ } else if (line === "") {
1529
+ eventType = "";
1530
+ }
1531
+ }
1532
+ }
1533
+ } catch (err) {
1534
+ this._connected = false;
1535
+ if (this._stopped) return;
1536
+ this.handler({
1537
+ type: "__disconnected",
1538
+ data: { error: err instanceof Error ? err.message : String(err) }
1539
+ });
1540
+ }
1541
+ if (!this._stopped) {
1542
+ this._connected = false;
1543
+ this.handler({ type: "__reconnecting", data: { delay: this.reconnectDelay } });
1544
+ await new Promise((r) => setTimeout(r, this.reconnectDelay));
1545
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
1546
+ await this.connect();
1547
+ }
1548
+ }
1549
+ };
1550
+
1551
+ // src/tui/hooks/use-sse-stream.ts
1552
+ function useSSEStream(client) {
1553
+ const sseRef = useRef2(null);
1554
+ const setConnected = useTuiStore((s) => s.setConnected);
1555
+ const setMachineCount = useTuiStore((s) => s.setMachineCount);
1556
+ const setLastError = useTuiStore((s) => s.setLastError);
1557
+ useEffect3(() => {
1558
+ const handler = (event) => {
1559
+ switch (event.type) {
1560
+ case "__connected":
1561
+ setConnected(true);
1562
+ setLastError(null);
1563
+ break;
1564
+ case "__disconnected":
1565
+ setConnected(false);
1566
+ setLastError(event.data.error);
1567
+ break;
1568
+ case "__reconnecting":
1569
+ setConnected(false);
1570
+ break;
1571
+ case "machines:snapshot": {
1572
+ const machines = event.data ?? [];
1573
+ useMachinesStore.getState().setMachines(machines);
1574
+ setMachineCount(machines.filter((m) => m.isConnected).length);
1575
+ break;
1576
+ }
1577
+ case "machine:connected": {
1578
+ const machine = event.data;
1579
+ useMachinesStore.getState().addMachine(machine);
1580
+ setMachineCount(useMachinesStore.getState().machines.filter((m) => m.isConnected).length);
1581
+ break;
1582
+ }
1583
+ case "machine:disconnected": {
1584
+ const id = event.data.machineId;
1585
+ useMachinesStore.getState().updateMachine(id, { isConnected: false });
1586
+ setMachineCount(useMachinesStore.getState().machines.filter((m) => m.isConnected).length);
1587
+ break;
1588
+ }
1589
+ case "task:stdout":
1590
+ case "task:text": {
1591
+ const taskId = event.data.taskId;
1592
+ const text = event.data.data ?? event.data.output ?? "";
1593
+ if (typeof text === "string" && text.length > 0) {
1594
+ useExecutionStore.getState().appendText(taskId, text);
1595
+ }
1596
+ break;
1597
+ }
1598
+ case "task:progress": {
1599
+ const taskId = event.data.taskId;
1600
+ const message = event.data.message;
1601
+ if (!message) break;
1602
+ if (/^Using tool:/i.test(message)) break;
1603
+ useExecutionStore.getState().appendLine(taskId, `[progress] ${message}`);
1604
+ break;
1605
+ }
1606
+ case "task:result": {
1607
+ const taskId = event.data.taskId;
1608
+ const status = event.data.status;
1609
+ useExecutionStore.getState().setStatus(taskId, status);
1610
+ const nodeId = event.data.nodeId;
1611
+ if (nodeId && status) {
1612
+ const mappedStatus = status === "success" ? "completed" : status === "failure" ? "planned" : status;
1613
+ usePlanStore.getState().updateNodeStatus(nodeId, mappedStatus);
1614
+ }
1615
+ break;
1616
+ }
1617
+ case "task:tool_trace": {
1618
+ const taskId = event.data.taskId;
1619
+ const toolName = event.data.toolName;
1620
+ useExecutionStore.getState().appendToolCall(taskId, toolName);
1621
+ break;
1622
+ }
1623
+ case "task:file_change": {
1624
+ const taskId = event.data.taskId;
1625
+ const path = event.data.path;
1626
+ const action = event.data.action;
1627
+ const added = event.data.linesAdded;
1628
+ const removed = event.data.linesRemoved;
1629
+ useExecutionStore.getState().appendFileChange(taskId, path, action, added, removed);
1630
+ break;
1631
+ }
1632
+ case "task:session_init": {
1633
+ const taskId = event.data.taskId;
1634
+ const nodeId = event.data.nodeId ?? taskId;
1635
+ useExecutionStore.getState().initExecution(taskId, nodeId);
1636
+ useExecutionStore.getState().setWatching(taskId);
1637
+ break;
1638
+ }
1639
+ case "heartbeat":
1640
+ break;
1641
+ }
1642
+ };
1643
+ const sse = new SSEClient(client, handler);
1644
+ sseRef.current = sse;
1645
+ sse.start().catch(() => {
1646
+ });
1647
+ return () => {
1648
+ sse.stop();
1649
+ };
1650
+ }, [client, setConnected, setMachineCount, setLastError]);
1651
+ }
1652
+
1653
+ // src/tui/hooks/use-fuzzy-search.ts
1654
+ import { useEffect as useEffect4, useCallback as useCallback3 } from "react";
1655
+ import Fuse from "fuse.js";
1656
+ var fuseInstance = null;
1657
+ function useFuzzySearch() {
1658
+ const projects = useProjectsStore((s) => s.projects);
1659
+ const nodes = usePlanStore((s) => s.nodes);
1660
+ const machines = useMachinesStore((s) => s.machines);
1661
+ const { setItems, setResults, query } = useSearchStore();
1662
+ useEffect4(() => {
1663
+ const items = [
1664
+ ...projects.map((p) => ({
1665
+ type: "project",
1666
+ id: p.id,
1667
+ title: p.name,
1668
+ subtitle: p.description,
1669
+ status: p.status
1670
+ })),
1671
+ ...nodes.filter((n) => !n.deletedAt).map((n) => ({
1672
+ type: "task",
1673
+ id: n.id,
1674
+ title: n.title,
1675
+ subtitle: n.description,
1676
+ status: n.status
1677
+ })),
1678
+ ...machines.filter((m) => !m.isRevoked).map((m) => ({
1679
+ type: "machine",
1680
+ id: m.id,
1681
+ title: m.name,
1682
+ subtitle: `${m.platform} - ${m.hostname}`,
1683
+ status: m.isConnected ? "connected" : "disconnected"
1684
+ }))
1685
+ ];
1686
+ setItems(items);
1687
+ fuseInstance = new Fuse(items, {
1688
+ keys: ["title", "subtitle", "id"],
1689
+ threshold: 0.4,
1690
+ includeScore: true
1691
+ });
1692
+ }, [projects, nodes, machines, setItems]);
1693
+ const search = useCallback3((q) => {
1694
+ if (!q || !fuseInstance) {
1695
+ setResults([]);
1696
+ return;
1697
+ }
1698
+ const results = fuseInstance.search(q, { limit: 20 });
1699
+ setResults(results.map((r) => r.item));
1700
+ }, [setResults]);
1701
+ useEffect4(() => {
1702
+ search(query);
1703
+ }, [query, search]);
1704
+ return { search };
1705
+ }
1706
+
1707
+ // src/tui/hooks/use-command-parser.ts
1708
+ import { useCallback as useCallback4 } from "react";
1709
+
1710
+ // src/tui/commands/handlers.ts
1711
+ var handlers = {
1712
+ // ── Quit ──
1713
+ q: async () => {
1714
+ process.exit(0);
1715
+ },
1716
+ quit: async () => {
1717
+ process.exit(0);
1718
+ },
1719
+ // ── Refresh ──
1720
+ r: async (_args, client) => {
1721
+ await refreshAll(client);
1722
+ },
1723
+ refresh: async (_args, client) => {
1724
+ await refreshAll(client);
1725
+ },
1726
+ // ── Project commands ──
1727
+ "project list": async (_args, client) => {
1728
+ const projects = await client.listProjects();
1729
+ useProjectsStore.getState().setProjects(projects);
1730
+ },
1731
+ "project show": async (args, client) => {
1732
+ const id = args[0];
1733
+ if (!id) return;
1734
+ try {
1735
+ const project = await client.resolveProject(id);
1736
+ useTuiStore.getState().openDetail("project", project.id);
1737
+ } catch {
1738
+ useTuiStore.getState().setLastError(`Project not found: ${id}`);
1739
+ }
1740
+ },
1741
+ "project create": async (args, client) => {
1742
+ const name = args.join(" ");
1743
+ if (!name) {
1744
+ useTuiStore.getState().setLastError("Usage: :project create <name>");
1745
+ return;
1746
+ }
1747
+ await client.createProject({ name });
1748
+ const projects = await client.listProjects();
1749
+ useProjectsStore.getState().setProjects(projects);
1750
+ },
1751
+ "project delete": async (args, client) => {
1752
+ const id = args[0];
1753
+ if (!id) return;
1754
+ try {
1755
+ const project = await client.resolveProject(id);
1756
+ await client.deleteProject(project.id);
1757
+ const projects = await client.listProjects();
1758
+ useProjectsStore.getState().setProjects(projects);
1759
+ } catch (err) {
1760
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1761
+ }
1762
+ },
1763
+ // ── Plan commands ──
1764
+ "plan tree": async (_args, client) => {
1765
+ const projectId = useTuiStore.getState().selectedProjectId;
1766
+ if (!projectId) {
1767
+ useTuiStore.getState().setLastError("No project selected");
1768
+ return;
1769
+ }
1770
+ const { nodes, edges } = await client.getPlan(projectId);
1771
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
1772
+ useTuiStore.getState().focusPanel("plan");
1773
+ },
1774
+ "plan create-node": async (args, client) => {
1775
+ const projectId = useTuiStore.getState().selectedProjectId;
1776
+ if (!projectId) {
1777
+ useTuiStore.getState().setLastError("No project selected");
1778
+ return;
1779
+ }
1780
+ const title = args.join(" ");
1781
+ if (!title) {
1782
+ useTuiStore.getState().setLastError("Usage: :plan create-node <title>");
1783
+ return;
1784
+ }
1785
+ const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1786
+ await client.createPlanNode({ id, projectId, title });
1787
+ const { nodes, edges } = await client.getPlan(projectId);
1788
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
1789
+ },
1790
+ "plan update-node": async (args, client) => {
1791
+ const [nodeId, field, ...rest] = args;
1792
+ if (!nodeId || !field) {
1793
+ useTuiStore.getState().setLastError("Usage: :plan update-node <nodeId> <field> <value>");
1794
+ return;
1795
+ }
1796
+ const value = rest.join(" ");
1797
+ await client.updatePlanNode(nodeId, { [field]: value });
1798
+ const projectId = useTuiStore.getState().selectedProjectId;
1799
+ if (projectId) {
1800
+ const { nodes, edges } = await client.getPlan(projectId);
1801
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
1802
+ }
1803
+ },
1804
+ // ── Dispatch ──
1805
+ d: async (args, client) => {
1806
+ await handlers.dispatch(args, client);
1807
+ },
1808
+ dispatch: async (args, client) => {
1809
+ const nodeId = args[0] ?? useTuiStore.getState().selectedNodeId;
1810
+ const projectId = useTuiStore.getState().selectedProjectId;
1811
+ if (!nodeId || !projectId) {
1812
+ useTuiStore.getState().setLastError("No node/project selected for dispatch");
1813
+ return;
1814
+ }
1815
+ try {
1816
+ const response = await client.dispatchTask({ nodeId, projectId });
1817
+ const execId = `exec-${Date.now()}`;
1818
+ useExecutionStore.getState().initExecution(execId, nodeId);
1819
+ useExecutionStore.getState().setWatching(execId);
1820
+ useTuiStore.getState().focusPanel("output");
1821
+ if (response.body) {
1822
+ const reader = response.body.getReader();
1823
+ const decoder = new TextDecoder();
1824
+ let buffer = "";
1825
+ while (true) {
1826
+ const { done, value } = await reader.read();
1827
+ if (done) break;
1828
+ buffer += decoder.decode(value, { stream: true });
1829
+ const lines = buffer.split("\n");
1830
+ buffer = lines.pop() ?? "";
1831
+ for (const line of lines) {
1832
+ if (line.startsWith("data: ")) {
1833
+ try {
1834
+ const event = JSON.parse(line.slice(6));
1835
+ const eventType = event.type;
1836
+ if (eventType === "text") {
1837
+ useExecutionStore.getState().appendText(execId, event.content ?? "");
1838
+ } else if (eventType === "tool_use") {
1839
+ useExecutionStore.getState().appendToolCall(execId, event.name ?? "");
1840
+ } else if (eventType === "result") {
1841
+ useExecutionStore.getState().setStatus(execId, event.status ?? "completed");
1842
+ } else if (eventType === "error") {
1843
+ useExecutionStore.getState().appendLine(execId, `[error] ${event.message}`);
1844
+ useExecutionStore.getState().setStatus(execId, "failure");
1845
+ }
1846
+ } catch {
1847
+ }
1848
+ }
1849
+ }
1850
+ }
1851
+ }
1852
+ } catch (err) {
1853
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1854
+ }
1855
+ },
1856
+ // ── Cancel ──
1857
+ c: async (args, client) => {
1858
+ await handlers.cancel(args, client);
1859
+ },
1860
+ cancel: async (args, client) => {
1861
+ const executionId = args[0] ?? useExecutionStore.getState().watchingId;
1862
+ if (!executionId) {
1863
+ useTuiStore.getState().setLastError("No execution to cancel");
1864
+ return;
1865
+ }
1866
+ try {
1867
+ await client.cancelTask({ executionId });
1868
+ useExecutionStore.getState().setStatus(executionId, "cancelled");
1869
+ } catch (err) {
1870
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1871
+ }
1872
+ },
1873
+ // ── Steer ──
1874
+ s: async (args, client) => {
1875
+ await handlers.steer(args, client);
1876
+ },
1877
+ steer: async (args, client) => {
1878
+ const message = args.join(" ");
1879
+ if (!message) {
1880
+ useTuiStore.getState().setLastError("Usage: :steer <message>");
1881
+ return;
1882
+ }
1883
+ const executionId = useExecutionStore.getState().watchingId;
1884
+ const selectedMachineId = useTuiStore.getState().selectedMachineId;
1885
+ if (!executionId || !selectedMachineId) {
1886
+ useTuiStore.getState().setLastError("No active execution/machine to steer");
1887
+ return;
1888
+ }
1889
+ try {
1890
+ await client.steerTask({ taskId: executionId, machineId: selectedMachineId, message });
1891
+ } catch (err) {
1892
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1893
+ }
1894
+ },
1895
+ // ── Watch ──
1896
+ watch: async (args) => {
1897
+ const executionId = args[0];
1898
+ if (!executionId) {
1899
+ useTuiStore.getState().setLastError("Usage: :watch <executionId>");
1900
+ return;
1901
+ }
1902
+ useExecutionStore.getState().setWatching(executionId);
1903
+ useTuiStore.getState().focusPanel("output");
1904
+ },
1905
+ // ── Env ──
1906
+ "env list": async (_args, client) => {
1907
+ const machines = await client.listMachines();
1908
+ useMachinesStore.getState().setMachines(machines);
1909
+ useTuiStore.getState().focusPanel("machines");
1910
+ },
1911
+ "env status": async (_args, client) => {
1912
+ const status = await client.getRelayStatus();
1913
+ useTuiStore.getState().setLastError(JSON.stringify(status, null, 2));
1914
+ },
1915
+ // ── Search ──
1916
+ search: async (args, client) => {
1917
+ const query = args.join(" ");
1918
+ if (!query) return;
1919
+ try {
1920
+ await client.search(query);
1921
+ useTuiStore.getState().toggleSearch();
1922
+ } catch (err) {
1923
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1924
+ }
1925
+ },
1926
+ // ── Activity ──
1927
+ activity: async (_args, client) => {
1928
+ const projectId = useTuiStore.getState().selectedProjectId;
1929
+ try {
1930
+ const activities = await client.listActivities(projectId ? { projectId } : void 0);
1931
+ for (const a of activities.slice(0, 20)) {
1932
+ useExecutionStore.getState().appendLine("activity", `[${a.type}] ${a.title}`);
1933
+ }
1934
+ useExecutionStore.getState().setWatching("activity");
1935
+ useTuiStore.getState().focusPanel("output");
1936
+ } catch (err) {
1937
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1938
+ }
1939
+ },
1940
+ // ── Help ──
1941
+ help: async () => {
1942
+ useTuiStore.getState().toggleHelp();
1943
+ },
1944
+ "?": async () => {
1945
+ useTuiStore.getState().toggleHelp();
1946
+ }
1947
+ };
1948
+ async function refreshAll(client) {
1949
+ try {
1950
+ const [projects, machines] = await Promise.all([
1951
+ client.listProjects(),
1952
+ client.listMachines()
1953
+ ]);
1954
+ useProjectsStore.getState().setProjects(projects);
1955
+ useMachinesStore.getState().setMachines(machines);
1956
+ useTuiStore.getState().setMachineCount(machines.filter((m) => m.isConnected).length);
1957
+ const projectId = useTuiStore.getState().selectedProjectId;
1958
+ if (projectId) {
1959
+ const { nodes, edges } = await client.getPlan(projectId);
1960
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
1961
+ }
1962
+ } catch (err) {
1963
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1964
+ }
1965
+ }
1966
+
1967
+ // src/tui/commands/autocomplete.ts
1968
+ var PrefixTrie = class {
1969
+ root;
1970
+ constructor() {
1971
+ this.root = { children: /* @__PURE__ */ new Map(), isEnd: false, value: "" };
1972
+ }
1973
+ insert(word) {
1974
+ let node = this.root;
1975
+ for (const char of word) {
1976
+ if (!node.children.has(char)) {
1977
+ node.children.set(char, { children: /* @__PURE__ */ new Map(), isEnd: false, value: "" });
1978
+ }
1979
+ node = node.children.get(char);
1980
+ }
1981
+ node.isEnd = true;
1982
+ node.value = word;
1983
+ }
1984
+ search(prefix) {
1985
+ let node = this.root;
1986
+ for (const char of prefix) {
1987
+ if (!node.children.has(char)) return [];
1988
+ node = node.children.get(char);
1989
+ }
1990
+ return this.collect(node);
1991
+ }
1992
+ collect(node) {
1993
+ const results = [];
1994
+ if (node.isEnd) results.push(node.value);
1995
+ for (const child of node.children.values()) {
1996
+ results.push(...this.collect(child));
1997
+ }
1998
+ return results;
1999
+ }
2000
+ };
2001
+
2002
+ // src/tui/commands/registry.ts
2003
+ var trie = new PrefixTrie();
2004
+ for (const key of Object.keys(handlers)) {
2005
+ trie.insert(key);
2006
+ }
2007
+ async function executeCommand(input, client) {
2008
+ const parts = input.split(/\s+/);
2009
+ if (parts.length >= 2) {
2010
+ const twoWord = `${parts[0]} ${parts[1]}`;
2011
+ if (handlers[twoWord]) {
2012
+ try {
2013
+ await handlers[twoWord](parts.slice(2), client);
2014
+ } catch (err) {
2015
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
2016
+ }
2017
+ return;
2018
+ }
2019
+ }
2020
+ const cmd = parts[0];
2021
+ if (handlers[cmd]) {
2022
+ try {
2023
+ await handlers[cmd](parts.slice(1), client);
2024
+ } catch (err) {
2025
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
2026
+ }
2027
+ return;
2028
+ }
2029
+ useTuiStore.getState().setLastError(`Unknown command: ${cmd}`);
2030
+ }
2031
+ function getCompletions(partial) {
2032
+ return trie.search(partial);
2033
+ }
2034
+
2035
+ // src/tui/hooks/use-command-parser.ts
2036
+ function useCommandParser(client) {
2037
+ const execute = useCallback4(
2038
+ async (input) => {
2039
+ const trimmed = input.trim();
2040
+ if (!trimmed) return;
2041
+ await executeCommand(trimmed, client);
2042
+ },
2043
+ [client]
2044
+ );
2045
+ const autocomplete = useCallback4(
2046
+ (partial) => {
2047
+ return getCompletions(partial);
2048
+ },
2049
+ []
2050
+ );
2051
+ return { execute, autocomplete };
2052
+ }
2053
+
2054
+ // src/tui/app.tsx
2055
+ import { jsx as jsx13 } from "react/jsx-runtime";
2056
+ function App({ serverUrl }) {
2057
+ const client = useMemo(() => new AstroClient({ serverUrl }), [serverUrl]);
2058
+ const { refreshAll: refreshAll2 } = usePolling(client);
2059
+ useSSEStream(client);
2060
+ useFuzzySearch();
2061
+ const { execute } = useCommandParser(client);
2062
+ const onSelect = useCallback5(() => {
2063
+ const { focusedPanel, scrollIndex } = useTuiStore.getState();
2064
+ switch (focusedPanel) {
2065
+ case "projects": {
2066
+ const projects = useProjectsStore.getState().projects;
2067
+ const idx = scrollIndex.projects;
2068
+ if (projects[idx]) {
2069
+ useTuiStore.getState().setSelectedProject(projects[idx].id);
2070
+ }
2071
+ break;
2072
+ }
2073
+ case "plan": {
2074
+ const treeLines = usePlanStore.getState().treeLines;
2075
+ const idx = scrollIndex.plan;
2076
+ const line = treeLines[idx];
2077
+ if (line) {
2078
+ useTuiStore.getState().setSelectedNode(line.id);
2079
+ usePlanStore.getState().toggleCollapse(line.id);
2080
+ }
2081
+ break;
2082
+ }
2083
+ case "machines": {
2084
+ break;
2085
+ }
2086
+ }
2087
+ }, []);
2088
+ const onCommand = useCallback5(
2089
+ (cmd) => {
2090
+ execute(cmd);
2091
+ },
2092
+ [execute]
2093
+ );
2094
+ const onSearch = useCallback5(
2095
+ (query) => {
2096
+ useSearchStore.getState().setQuery(query);
2097
+ if (query.length > 0) {
2098
+ useSearchStore.getState().open();
2099
+ }
2100
+ },
2101
+ []
2102
+ );
2103
+ const onDispatch = useCallback5(async () => {
2104
+ const nodeId = useTuiStore.getState().selectedNodeId;
2105
+ const projectId = useTuiStore.getState().selectedProjectId;
2106
+ if (nodeId && projectId) {
2107
+ await execute(`dispatch ${nodeId}`);
2108
+ }
2109
+ }, [execute]);
2110
+ const onCancel = useCallback5(async () => {
2111
+ await execute("cancel");
2112
+ }, [execute]);
2113
+ const onRefresh = useCallback5(() => {
2114
+ refreshAll2();
2115
+ }, [refreshAll2]);
2116
+ useVimMode({
2117
+ onSelect,
2118
+ onCommand,
2119
+ onSearch,
2120
+ onDispatch,
2121
+ onCancel,
2122
+ onRefresh
2123
+ });
2124
+ return /* @__PURE__ */ jsx13(MainLayout, {});
2125
+ }
2126
+
2127
+ // src/tui/index.tsx
2128
+ import { jsx as jsx14 } from "react/jsx-runtime";
2129
+ async function launchTui(serverUrl) {
2130
+ render(/* @__PURE__ */ jsx14(App, { serverUrl }), {
2131
+ exitOnCtrlC: true
2132
+ });
2133
+ }
2134
+ export {
2135
+ launchTui
2136
+ };