@astroanywhere/cli 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AstroClient
4
- } from "./chunk-7H7WD7QX.js";
4
+ } from "./chunk-SYY2HHOY.js";
5
5
 
6
6
  // src/tui/index.tsx
7
7
  import "react";
@@ -12,7 +12,7 @@ import { useMemo, useCallback as useCallback5 } from "react";
12
12
 
13
13
  // src/tui/components/layout/main-layout.tsx
14
14
  import "react";
15
- import { Box as Box10, useStdout } from "ink";
15
+ import { Box as Box12, useStdout as useStdout3 } from "ink";
16
16
 
17
17
  // src/tui/components/layout/status-bar.tsx
18
18
  import "react";
@@ -20,7 +20,7 @@ import { Box, Text } from "ink";
20
20
 
21
21
  // src/tui/stores/tui-store.ts
22
22
  import { create } from "zustand";
23
- var PANEL_ORDER = ["projects", "plan", "machines", "output"];
23
+ var PANEL_ORDER = ["projects", "plan", "machines", "output", "chat"];
24
24
  var useTuiStore = create((set, get) => ({
25
25
  mode: "normal",
26
26
  commandBuffer: "",
@@ -32,18 +32,21 @@ var useTuiStore = create((set, get) => ({
32
32
  selectedNodeId: null,
33
33
  selectedMachineId: null,
34
34
  selectedExecutionId: null,
35
- scrollIndex: { projects: 0, plan: 0, machines: 0, output: 0 },
35
+ scrollIndex: { projects: 0, plan: 0, machines: 0, output: 0, chat: 0 },
36
36
  showHelp: false,
37
37
  showSearch: false,
38
38
  showDetail: false,
39
+ showChat: false,
39
40
  detailType: null,
40
41
  detailId: null,
41
42
  connected: false,
42
43
  machineCount: 0,
43
44
  todayCost: 0,
45
+ activeView: "dashboard",
46
+ paletteIndex: 0,
44
47
  lastError: null,
45
48
  setMode: (mode) => set({ mode }),
46
- setCommandBuffer: (commandBuffer) => set({ commandBuffer }),
49
+ setCommandBuffer: (commandBuffer) => set({ commandBuffer, paletteIndex: 0 }),
47
50
  setSearchQuery: (searchQuery) => set({ searchQuery }),
48
51
  setPendingKeys: (pendingKeys) => set({ pendingKeys }),
49
52
  focusPanel: (panel) => set({ focusedPanel: panel }),
@@ -113,12 +116,15 @@ var useTuiStore = create((set, get) => ({
113
116
  },
114
117
  toggleHelp: () => set((s) => ({ showHelp: !s.showHelp, showSearch: false })),
115
118
  toggleSearch: () => set((s) => ({ showSearch: !s.showSearch, showHelp: false })),
119
+ toggleChat: () => set((s) => ({ showChat: !s.showChat })),
116
120
  openDetail: (type, id) => set({ showDetail: true, detailType: type, detailId: id, showHelp: false, showSearch: false }),
117
121
  closeDetail: () => set({ showDetail: false, detailType: null, detailId: null }),
118
- closeOverlays: () => set({ showHelp: false, showSearch: false, showDetail: false, detailType: null, detailId: null }),
122
+ closeOverlays: () => set({ showHelp: false, showSearch: false, showDetail: false, showChat: false, detailType: null, detailId: null }),
119
123
  setConnected: (connected) => set({ connected }),
120
124
  setMachineCount: (machineCount) => set({ machineCount }),
121
125
  setTodayCost: (todayCost) => set({ todayCost }),
126
+ setActiveView: (activeView) => set({ activeView }),
127
+ setPaletteIndex: (paletteIndex) => set({ paletteIndex }),
122
128
  setLastError: (lastError) => set({ lastError })
123
129
  }));
124
130
 
@@ -149,153 +155,42 @@ function truncate(str, maxLen) {
149
155
 
150
156
  // src/tui/components/layout/status-bar.tsx
151
157
  import { jsx, jsxs } from "react/jsx-runtime";
158
+ var VIEW_LABELS = {
159
+ dashboard: "Dashboard",
160
+ "plan-gen": "Plan",
161
+ projects: "Projects",
162
+ playground: "Playground",
163
+ output: "Output"
164
+ };
152
165
  function StatusBar() {
153
166
  const connected = useTuiStore((s) => s.connected);
154
167
  const machineCount = useTuiStore((s) => s.machineCount);
155
168
  const todayCost = useTuiStore((s) => s.todayCost);
156
169
  const lastError = useTuiStore((s) => s.lastError);
170
+ const activeView = useTuiStore((s) => s.activeView);
157
171
  return /* @__PURE__ */ jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [
158
172
  /* @__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" }),
173
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Astro" }),
174
+ /* @__PURE__ */ jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25CF" : "\u25CB" }),
161
175
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
162
176
  machineCount,
163
177
  " machine",
164
178
  machineCount !== 1 ? "s" : ""
165
179
  ] }),
166
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
167
- formatCost(todayCost),
168
- " today"
180
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: formatCost(todayCost) }),
181
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
182
+ "[",
183
+ VIEW_LABELS[activeView],
184
+ "]"
169
185
  ] })
170
186
  ] }),
171
- lastError && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "red", children: lastError.length > 60 ? lastError.slice(0, 57) + "..." : lastError }) })
187
+ lastError && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "red", children: lastError.length > 50 ? lastError.slice(0, 47) + "..." : lastError }) })
172
188
  ] });
173
189
  }
174
190
 
175
191
  // src/tui/components/layout/command-line.tsx
176
192
  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";
193
+ import { Box as Box2, Text as Text2, useStdout } from "ink";
299
194
 
300
195
  // src/tui/stores/search-store.ts
301
196
  import { create as create2 } from "zustand";
@@ -321,6 +216,20 @@ var useSearchStore = create2((set, get) => ({
321
216
  close: () => set({ isOpen: false, query: "", results: [], selectedIndex: 0 })
322
217
  }));
323
218
 
219
+ // src/tui/stores/projects-store.ts
220
+ import { create as create3 } from "zustand";
221
+ var useProjectsStore = create3((set) => ({
222
+ projects: [],
223
+ loading: false,
224
+ error: null,
225
+ setProjects: (projects) => set({ projects, loading: false, error: null }),
226
+ setLoading: (loading) => set({ loading }),
227
+ setError: (error) => set({ error, loading: false })
228
+ }));
229
+
230
+ // src/tui/stores/plan-store.ts
231
+ import { create as create4 } from "zustand";
232
+
324
233
  // src/tui/lib/status-colors.ts
325
234
  var STATUS_COLOR_MAP = {
326
235
  // Project statuses
@@ -386,248 +295,6 @@ function getStatusSymbol(status) {
386
295
  }
387
296
  }
388
297
 
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
298
  // src/tui/lib/tree-builder.ts
632
299
  function buildTree(nodes, edges) {
633
300
  const adj = /* @__PURE__ */ new Map();
@@ -742,84 +409,1029 @@ var usePlanStore = create4((set, get) => ({
742
409
  })
743
410
  }));
744
411
 
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);
412
+ // src/tui/stores/machines-store.ts
413
+ import { create as create5 } from "zustand";
414
+ var useMachinesStore = create5((set, get) => ({
415
+ machines: [],
416
+ loading: false,
417
+ error: null,
418
+ setMachines: (machines) => set({ machines, loading: false, error: null }),
419
+ updateMachine: (id, patch) => {
420
+ set((s) => ({
421
+ machines: s.machines.map((m) => m.id === id ? { ...m, ...patch } : m)
422
+ }));
423
+ },
424
+ removeMachine: (id) => {
425
+ set((s) => ({ machines: s.machines.filter((m) => m.id !== id) }));
426
+ },
427
+ addMachine: (machine) => {
428
+ const { machines } = get();
429
+ const existing = machines.findIndex((m) => m.id === machine.id);
430
+ if (existing >= 0) {
431
+ set({ machines: machines.map((m, i) => i === existing ? machine : m) });
432
+ } else {
433
+ set({ machines: [...machines, machine] });
434
+ }
435
+ },
436
+ setLoading: (loading) => set({ loading }),
437
+ setError: (error) => set({ error, loading: false })
438
+ }));
439
+
440
+ // src/tui/stores/execution-store.ts
441
+ import { create as create6 } from "zustand";
442
+ var MAX_LINES = 5e3;
443
+ function flushToolDots(output) {
444
+ if (output.pendingToolCount === 0) return output.lines;
445
+ const dots = "\xB7".repeat(output.pendingToolCount);
446
+ const line = `[Tool Call] ${dots}`;
447
+ const lines = [...output.lines, line];
448
+ output.pendingToolCount = 0;
449
+ return lines;
450
+ }
451
+ function trimRingBuffer(lines) {
452
+ if (lines.length > MAX_LINES) {
453
+ lines.splice(0, lines.length - MAX_LINES);
454
+ }
455
+ return lines;
456
+ }
457
+ var useExecutionStore = create6((set, get) => ({
458
+ outputs: /* @__PURE__ */ new Map(),
459
+ watchingId: null,
460
+ pendingApproval: null,
461
+ initExecution: (executionId, nodeId) => {
462
+ const { outputs } = get();
463
+ const next = new Map(outputs);
464
+ next.set(executionId, {
465
+ executionId,
466
+ nodeId,
467
+ lines: [],
468
+ status: "running",
469
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
470
+ pendingToolCount: 0
471
+ });
472
+ set({ outputs: next });
473
+ },
474
+ appendToolCall: (executionId) => {
475
+ const { outputs } = get();
476
+ const current = outputs.get(executionId);
477
+ if (!current) return;
478
+ const next = new Map(outputs);
479
+ next.set(executionId, { ...current, pendingToolCount: current.pendingToolCount + 1 });
480
+ set({ outputs: next });
481
+ },
482
+ appendFileChange: (executionId, path, action, added, removed) => {
483
+ const { outputs } = get();
484
+ const current = outputs.get(executionId);
485
+ if (!current) return;
486
+ const lines = flushToolDots({ ...current });
487
+ const stats = [
488
+ added != null && added > 0 ? `+${added}` : null,
489
+ removed != null && removed > 0 ? `-${removed}` : null
490
+ ].filter(Boolean).join(" ");
491
+ lines.push(`[${action}] ${path}${stats ? ` (${stats})` : ""}`);
492
+ trimRingBuffer(lines);
493
+ const next = new Map(outputs);
494
+ next.set(executionId, { ...current, lines, pendingToolCount: 0 });
495
+ set({ outputs: next });
496
+ },
497
+ appendLine: (executionId, line) => {
498
+ const { outputs } = get();
499
+ const current = outputs.get(executionId);
500
+ if (!current) return;
501
+ const lines = flushToolDots({ ...current });
502
+ lines.push(line);
503
+ trimRingBuffer(lines);
504
+ const next = new Map(outputs);
505
+ next.set(executionId, { ...current, lines, pendingToolCount: 0 });
506
+ set({ outputs: next });
507
+ },
508
+ appendText: (executionId, text) => {
509
+ const { outputs } = get();
510
+ const current = outputs.get(executionId);
511
+ if (!current) return;
512
+ const lines = flushToolDots({ ...current });
513
+ const newLines = text.split("\n");
514
+ if (lines.length > 0 && newLines.length > 0) {
515
+ lines[lines.length - 1] += newLines[0];
516
+ for (let i = 1; i < newLines.length; i++) {
517
+ lines.push(newLines[i]);
518
+ }
519
+ } else {
520
+ lines.push(...newLines);
521
+ }
522
+ trimRingBuffer(lines);
523
+ const next = new Map(outputs);
524
+ next.set(executionId, { ...current, lines, pendingToolCount: 0 });
525
+ set({ outputs: next });
526
+ },
527
+ setStatus: (executionId, status) => {
528
+ const { outputs } = get();
529
+ const current = outputs.get(executionId);
530
+ if (!current) return;
531
+ const lines = flushToolDots({ ...current });
532
+ const next = new Map(outputs);
533
+ next.set(executionId, { ...current, lines, status, pendingToolCount: 0 });
534
+ set({ outputs: next });
535
+ },
536
+ setWatching: (watchingId) => set({ watchingId }),
537
+ setPendingApproval: (pendingApproval) => set({ pendingApproval }),
538
+ clear: (executionId) => {
539
+ const { outputs } = get();
540
+ const next = new Map(outputs);
541
+ next.delete(executionId);
542
+ set({ outputs: next });
543
+ }
544
+ }));
545
+
546
+ // src/tui/commands/handlers.ts
547
+ var PALETTE_COMMANDS = [
548
+ { name: "project list", description: "List all projects" },
549
+ { name: "project show", description: "Show project details", usage: "project show <id>" },
550
+ { name: "project create", description: "Create a new project", usage: "project create <name>" },
551
+ { name: "project delete", description: "Delete a project", usage: "project delete <id>" },
552
+ { name: "plan tree", description: "Show plan tree for selected project" },
553
+ { name: "plan create-node", description: "Create a plan node", usage: "plan create-node <title>" },
554
+ { name: "plan update-node", description: "Update a plan node field", usage: "plan update-node <id> <field> <value>" },
555
+ { name: "dispatch", description: "Dispatch selected task for execution", usage: "dispatch [nodeId]" },
556
+ { name: "cancel", description: "Cancel running execution", usage: "cancel [executionId]" },
557
+ { name: "steer", description: "Send guidance to running task", usage: "steer <message>" },
558
+ { name: "watch", description: "Watch execution output", usage: "watch <executionId>" },
559
+ { name: "env list", description: "List machines/environments" },
560
+ { name: "env status", description: "Show relay status" },
561
+ { name: "search", description: "Search projects and tasks", usage: "search <query>" },
562
+ { name: "activity", description: "Show recent activity feed" },
563
+ { name: "playground", description: "Start a playground (Cloud Code) session", usage: "playground <description>" },
564
+ { name: "plan generate", description: "Generate a plan using AI", usage: "plan generate <description>" },
565
+ { name: "refresh", description: "Refresh all data" },
566
+ { name: "help", description: "Toggle keybinding reference" },
567
+ { name: "quit", description: "Exit the TUI" }
568
+ ];
569
+ var handlers = {
570
+ // ── Quit ──
571
+ q: async () => {
572
+ process.exit(0);
573
+ },
574
+ quit: async () => {
575
+ process.exit(0);
576
+ },
577
+ // ── Refresh ──
578
+ r: async (_args, client) => {
579
+ await refreshAll(client);
580
+ },
581
+ refresh: async (_args, client) => {
582
+ await refreshAll(client);
583
+ },
584
+ // ── Project commands ──
585
+ "project list": async (_args, client) => {
586
+ const projects = await client.listProjects();
587
+ useProjectsStore.getState().setProjects(projects);
588
+ },
589
+ "project show": async (args, client) => {
590
+ const id = args[0];
591
+ if (!id) return;
592
+ try {
593
+ const project = await client.resolveProject(id);
594
+ useTuiStore.getState().openDetail("project", project.id);
595
+ } catch {
596
+ useTuiStore.getState().setLastError(`Project not found: ${id}`);
597
+ }
598
+ },
599
+ "project create": async (args, client) => {
600
+ const name = args.join(" ");
601
+ if (!name) {
602
+ useTuiStore.getState().setLastError("Usage: :project create <name>");
603
+ return;
604
+ }
605
+ await client.createProject({ name });
606
+ const projects = await client.listProjects();
607
+ useProjectsStore.getState().setProjects(projects);
608
+ },
609
+ "project delete": async (args, client) => {
610
+ const id = args[0];
611
+ if (!id) return;
612
+ try {
613
+ const project = await client.resolveProject(id);
614
+ await client.deleteProject(project.id);
615
+ const projects = await client.listProjects();
616
+ useProjectsStore.getState().setProjects(projects);
617
+ } catch (err) {
618
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
619
+ }
620
+ },
621
+ // ── Plan commands ──
622
+ "plan tree": async (_args, client) => {
623
+ const projectId = useTuiStore.getState().selectedProjectId;
624
+ if (!projectId) {
625
+ useTuiStore.getState().setLastError("No project selected");
626
+ return;
627
+ }
628
+ const { nodes, edges } = await client.getPlan(projectId);
629
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
630
+ useTuiStore.getState().focusPanel("plan");
631
+ },
632
+ "plan create-node": async (args, client) => {
633
+ const projectId = useTuiStore.getState().selectedProjectId;
634
+ if (!projectId) {
635
+ useTuiStore.getState().setLastError("No project selected");
636
+ return;
637
+ }
638
+ const title = args.join(" ");
639
+ if (!title) {
640
+ useTuiStore.getState().setLastError("Usage: :plan create-node <title>");
641
+ return;
642
+ }
643
+ const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
644
+ await client.createPlanNode({ id, projectId, title });
645
+ const { nodes, edges } = await client.getPlan(projectId);
646
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
647
+ },
648
+ "plan update-node": async (args, client) => {
649
+ const [nodeId, field, ...rest] = args;
650
+ if (!nodeId || !field) {
651
+ useTuiStore.getState().setLastError("Usage: :plan update-node <nodeId> <field> <value>");
652
+ return;
653
+ }
654
+ const value = rest.join(" ");
655
+ await client.updatePlanNode(nodeId, { [field]: value });
656
+ const projectId = useTuiStore.getState().selectedProjectId;
657
+ if (projectId) {
658
+ const { nodes, edges } = await client.getPlan(projectId);
659
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
660
+ }
661
+ },
662
+ // ── Dispatch ──
663
+ d: async (args, client) => {
664
+ await handlers.dispatch(args, client);
665
+ },
666
+ dispatch: async (args, client) => {
667
+ const nodeId = args[0] ?? useTuiStore.getState().selectedNodeId;
668
+ const projectId = useTuiStore.getState().selectedProjectId;
669
+ if (!nodeId || !projectId) {
670
+ useTuiStore.getState().setLastError("No node/project selected for dispatch");
671
+ return;
672
+ }
673
+ try {
674
+ const response = await client.dispatchTask({ nodeId, projectId });
675
+ const execId = `exec-${Date.now()}`;
676
+ useExecutionStore.getState().initExecution(execId, nodeId);
677
+ useExecutionStore.getState().setWatching(execId);
678
+ useTuiStore.getState().focusPanel("output");
679
+ if (response.body) {
680
+ const reader = response.body.getReader();
681
+ const decoder = new TextDecoder();
682
+ let buffer = "";
683
+ while (true) {
684
+ const { done, value } = await reader.read();
685
+ if (done) break;
686
+ buffer += decoder.decode(value, { stream: true });
687
+ const lines = buffer.split("\n");
688
+ buffer = lines.pop() ?? "";
689
+ for (const line of lines) {
690
+ if (line.startsWith("data: ")) {
691
+ try {
692
+ const event = JSON.parse(line.slice(6));
693
+ const eventType = event.type;
694
+ if (eventType === "text") {
695
+ useExecutionStore.getState().appendText(execId, event.content ?? "");
696
+ } else if (eventType === "tool_use") {
697
+ useExecutionStore.getState().appendToolCall(execId, event.name ?? "");
698
+ } else if (eventType === "result") {
699
+ useExecutionStore.getState().setStatus(execId, event.status ?? "completed");
700
+ } else if (eventType === "error") {
701
+ useExecutionStore.getState().appendLine(execId, `[error] ${event.message}`);
702
+ useExecutionStore.getState().setStatus(execId, "failure");
703
+ }
704
+ } catch {
705
+ }
706
+ }
707
+ }
708
+ }
709
+ }
710
+ } catch (err) {
711
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
712
+ }
713
+ },
714
+ // ── Cancel ──
715
+ c: async (args, client) => {
716
+ await handlers.cancel(args, client);
717
+ },
718
+ cancel: async (args, client) => {
719
+ const executionId = args[0] ?? useExecutionStore.getState().watchingId;
720
+ if (!executionId) {
721
+ useTuiStore.getState().setLastError("No execution to cancel");
722
+ return;
723
+ }
724
+ try {
725
+ await client.cancelTask({ executionId });
726
+ useExecutionStore.getState().setStatus(executionId, "cancelled");
727
+ } catch (err) {
728
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
729
+ }
730
+ },
731
+ // ── Steer ──
732
+ s: async (args, client) => {
733
+ await handlers.steer(args, client);
734
+ },
735
+ steer: async (args, client) => {
736
+ const message = args.join(" ");
737
+ if (!message) {
738
+ useTuiStore.getState().setLastError("Usage: :steer <message>");
739
+ return;
740
+ }
741
+ const executionId = useExecutionStore.getState().watchingId;
742
+ const selectedMachineId = useTuiStore.getState().selectedMachineId;
743
+ if (!executionId || !selectedMachineId) {
744
+ useTuiStore.getState().setLastError("No active execution/machine to steer");
745
+ return;
746
+ }
747
+ try {
748
+ await client.steerTask({ taskId: executionId, machineId: selectedMachineId, message });
749
+ } catch (err) {
750
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
751
+ }
752
+ },
753
+ // ── Watch ──
754
+ watch: async (args) => {
755
+ const executionId = args[0];
756
+ if (!executionId) {
757
+ useTuiStore.getState().setLastError("Usage: :watch <executionId>");
758
+ return;
759
+ }
760
+ useExecutionStore.getState().setWatching(executionId);
761
+ useTuiStore.getState().focusPanel("output");
762
+ },
763
+ // ── Env ──
764
+ "env list": async (_args, client) => {
765
+ const machines = await client.listMachines();
766
+ useMachinesStore.getState().setMachines(machines);
767
+ useTuiStore.getState().focusPanel("machines");
768
+ },
769
+ "env status": async (_args, client) => {
770
+ const status = await client.getRelayStatus();
771
+ useTuiStore.getState().setLastError(JSON.stringify(status, null, 2));
772
+ },
773
+ // ── Search ──
774
+ search: async (args, client) => {
775
+ const query = args.join(" ");
776
+ if (!query) return;
777
+ try {
778
+ await client.search(query);
779
+ useTuiStore.getState().toggleSearch();
780
+ } catch (err) {
781
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
782
+ }
783
+ },
784
+ // ── Activity ──
785
+ activity: async (_args, client) => {
786
+ const projectId = useTuiStore.getState().selectedProjectId;
787
+ try {
788
+ const activities = await client.listActivities(projectId ? { projectId } : void 0);
789
+ for (const a of activities.slice(0, 20)) {
790
+ useExecutionStore.getState().appendLine("activity", `[${a.type}] ${a.title}`);
791
+ }
792
+ useExecutionStore.getState().setWatching("activity");
793
+ useTuiStore.getState().focusPanel("output");
794
+ } catch (err) {
795
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
796
+ }
797
+ },
798
+ // ── Playground ──
799
+ playground: async (args, client) => {
800
+ const description = args.join(" ");
801
+ if (!description) {
802
+ useTuiStore.getState().setLastError("Usage: playground <description>");
803
+ return;
804
+ }
805
+ const projectId = useTuiStore.getState().selectedProjectId;
806
+ if (!projectId) {
807
+ useTuiStore.getState().setLastError("No project selected. Select a project first.");
808
+ return;
809
+ }
810
+ const nodeId = `playground-${projectId}-${Date.now()}`;
811
+ try {
812
+ const response = await client.dispatchTask({
813
+ nodeId,
814
+ projectId,
815
+ skipSafetyCheck: true,
816
+ description,
817
+ title: `Playground: ${description.slice(0, 50)}`
818
+ });
819
+ useExecutionStore.getState().initExecution(nodeId, nodeId);
820
+ useExecutionStore.getState().setWatching(nodeId);
821
+ useTuiStore.getState().setActiveView("playground");
822
+ await streamExecution(nodeId, response);
823
+ } catch (err) {
824
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
825
+ }
826
+ },
827
+ // ── Plan Generate ──
828
+ "plan generate": async (args, client) => {
829
+ const description = args.join(" ");
830
+ if (!description) {
831
+ useTuiStore.getState().setLastError("Usage: plan generate <description>");
832
+ return;
833
+ }
834
+ const projectId = useTuiStore.getState().selectedProjectId;
835
+ if (!projectId) {
836
+ useTuiStore.getState().setLastError("No project selected. Select a project first.");
837
+ return;
838
+ }
839
+ const nodeId = `plan-${projectId}`;
840
+ try {
841
+ const response = await client.dispatchTask({
842
+ nodeId,
843
+ projectId,
844
+ isInteractivePlan: true,
845
+ description
846
+ });
847
+ useExecutionStore.getState().initExecution(nodeId, nodeId);
848
+ useExecutionStore.getState().setWatching(nodeId);
849
+ useTuiStore.getState().setActiveView("plan-gen");
850
+ await streamExecution(nodeId, response);
851
+ useExecutionStore.getState().appendLine(nodeId, "[plan] Refreshing plan...");
852
+ const { nodes, edges } = await client.getPlan(projectId);
853
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
854
+ useTuiStore.getState().setActiveView("dashboard");
855
+ } catch (err) {
856
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
857
+ }
858
+ },
859
+ // ── Help ──
860
+ help: async () => {
861
+ useTuiStore.getState().toggleHelp();
862
+ },
863
+ "?": async () => {
864
+ useTuiStore.getState().toggleHelp();
865
+ }
866
+ };
867
+ async function streamExecution(executionId, response) {
868
+ if (!response.body) return;
869
+ const reader = response.body.getReader();
870
+ const decoder = new TextDecoder();
871
+ let buffer = "";
872
+ while (true) {
873
+ const { done, value } = await reader.read();
874
+ if (done) break;
875
+ buffer += decoder.decode(value, { stream: true });
876
+ const lines = buffer.split("\n");
877
+ buffer = lines.pop() ?? "";
878
+ for (const line of lines) {
879
+ if (!line.startsWith("data: ")) continue;
880
+ try {
881
+ const event = JSON.parse(line.slice(6));
882
+ const eventType = event.type;
883
+ if (eventType === "text") {
884
+ useExecutionStore.getState().appendText(executionId, event.content ?? "");
885
+ } else if (eventType === "tool_use") {
886
+ useExecutionStore.getState().appendToolCall(executionId, event.name ?? "");
887
+ } else if (eventType === "file_change") {
888
+ useExecutionStore.getState().appendFileChange(
889
+ executionId,
890
+ event.path ?? "",
891
+ event.action ?? "modified",
892
+ event.linesAdded,
893
+ event.linesRemoved
894
+ );
895
+ } else if (eventType === "result" || eventType === "done") {
896
+ useExecutionStore.getState().setStatus(executionId, event.status ?? "completed");
897
+ } else if (eventType === "error") {
898
+ useExecutionStore.getState().appendLine(executionId, `[error] ${event.message}`);
899
+ useExecutionStore.getState().setStatus(executionId, "failure");
900
+ } else if (eventType === "plan_result") {
901
+ useExecutionStore.getState().appendLine(executionId, "[plan] Plan generated successfully");
902
+ } else if (eventType === "approval_request") {
903
+ useExecutionStore.getState().setPendingApproval({
904
+ requestId: event.requestId,
905
+ question: event.question,
906
+ options: event.options,
907
+ machineId: event.machineId,
908
+ taskId: event.taskId
909
+ });
910
+ }
911
+ } catch {
912
+ }
913
+ }
914
+ }
915
+ }
916
+ async function refreshAll(client) {
917
+ try {
918
+ const [projects, machines] = await Promise.all([
919
+ client.listProjects(),
920
+ client.listMachines()
921
+ ]);
922
+ useProjectsStore.getState().setProjects(projects);
923
+ useMachinesStore.getState().setMachines(machines);
924
+ useTuiStore.getState().setMachineCount(machines.filter((m) => m.isConnected).length);
925
+ const projectId = useTuiStore.getState().selectedProjectId;
926
+ if (projectId) {
927
+ const { nodes, edges } = await client.getPlan(projectId);
928
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
929
+ }
930
+ } catch (err) {
931
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
932
+ }
933
+ }
934
+
935
+ // src/tui/commands/palette-filter.ts
936
+ function getFilteredPaletteCommands(query) {
937
+ if (!query) return PALETTE_COMMANDS;
938
+ const lower = query.toLowerCase();
939
+ return PALETTE_COMMANDS.filter(
940
+ (cmd) => cmd.name.toLowerCase().includes(lower) || cmd.description.toLowerCase().includes(lower)
941
+ );
942
+ }
943
+
944
+ // src/tui/components/layout/command-line.tsx
945
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
946
+ var SHORTCUTS = [
947
+ { key: "1", label: "Dashboard" },
948
+ { key: "2", label: "Plan" },
949
+ { key: "3", label: "Projects" },
950
+ { key: "4", label: "Playground" },
951
+ { key: "5", label: "Output" },
952
+ { key: "/", label: "Search" },
953
+ { key: "C-p", label: "Commands" },
954
+ { key: "d", label: "Dispatch" },
955
+ { key: "?", label: "Help" },
956
+ { key: "q", label: "Quit" }
957
+ ];
958
+ var TYPE_LABELS = {
959
+ project: { label: "proj", color: "cyan" },
960
+ task: { label: "task", color: "yellow" },
961
+ machine: { label: "env", color: "green" },
962
+ execution: { label: "exec", color: "magenta" }
963
+ };
964
+ function CommandLine({ height }) {
965
+ const mode = useTuiStore((s) => s.mode);
966
+ const commandBuffer = useTuiStore((s) => s.commandBuffer);
967
+ const paletteIndex = useTuiStore((s) => s.paletteIndex);
968
+ const searchOpen = useSearchStore((s) => s.isOpen);
969
+ const searchQuery = useSearchStore((s) => s.query);
970
+ const searchResults = useSearchStore((s) => s.results);
971
+ const searchItems = useSearchStore((s) => s.items);
972
+ const searchIndex = useSearchStore((s) => s.selectedIndex);
973
+ const { stdout } = useStdout();
974
+ const termWidth = stdout?.columns ?? 80;
975
+ const listHeight = Math.max(1, height - 3);
976
+ if (searchOpen) {
977
+ const displayList = searchQuery.length > 0 ? searchResults : searchItems;
978
+ const visible = displayList.slice(0, listHeight);
979
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height, borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [
980
+ /* @__PURE__ */ jsxs2(Box2, { children: [
981
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "/ " }),
982
+ /* @__PURE__ */ jsx2(Text2, { children: searchQuery }),
983
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2588" }),
984
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
985
+ " (",
986
+ "\u2191\u2193",
987
+ " navigate, Enter to go, Esc to close)"
988
+ ] })
989
+ ] }),
990
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
991
+ visible.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: searchQuery.length > 0 ? " No results" : " No items" }) : visible.map((item, i) => {
992
+ const isSelected = i === searchIndex;
993
+ const typeInfo = TYPE_LABELS[item.type] ?? { label: item.type, color: "white" };
994
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
995
+ /* @__PURE__ */ jsx2(Text2, { inverse: isSelected, bold: isSelected, color: isSelected ? "cyan" : void 0, children: isSelected ? " > " : " " }),
996
+ /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, color: isSelected ? "cyan" : typeInfo.color, children: [
997
+ "[",
998
+ typeInfo.label,
999
+ "]"
1000
+ ] }),
1001
+ /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, bold: isSelected, wrap: "truncate", children: [
1002
+ " ",
1003
+ item.title
1004
+ ] }),
1005
+ item.status && /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, color: isSelected ? void 0 : getStatusColor(item.status), dimColor: !isSelected, children: [
1006
+ " ",
1007
+ item.status
1008
+ ] })
1009
+ ] }, `${item.type}-${item.id}`);
1010
+ }),
1011
+ displayList.length > listHeight && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1012
+ " ...and ",
1013
+ displayList.length - listHeight,
1014
+ " more"
1015
+ ] })
1016
+ ] })
1017
+ ] });
1018
+ }
1019
+ if (mode === "palette") {
1020
+ const filtered = getFilteredPaletteCommands(commandBuffer);
1021
+ const visible = filtered.slice(0, listHeight);
1022
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height, borderStyle: "single", borderColor: "yellow", paddingX: 1, children: [
1023
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1024
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: "> " }),
1025
+ /* @__PURE__ */ jsx2(Text2, { children: commandBuffer }),
1026
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2588" }),
1027
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1028
+ " (",
1029
+ "\u2191\u2193",
1030
+ " navigate, Enter to run, Esc to cancel)"
1031
+ ] })
1032
+ ] }),
1033
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
1034
+ visible.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " No matching commands" }) : visible.map((cmd, i) => {
1035
+ const isSelected = i === paletteIndex;
1036
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1037
+ /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, bold: isSelected, color: isSelected ? "yellow" : void 0, children: [
1038
+ isSelected ? " > " : " ",
1039
+ cmd.name.padEnd(20)
1040
+ ] }),
1041
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: !isSelected, color: isSelected ? "white" : void 0, children: [
1042
+ " ",
1043
+ cmd.description
1044
+ ] })
1045
+ ] }, cmd.name);
1046
+ }),
1047
+ filtered.length > listHeight && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1048
+ " ...and ",
1049
+ filtered.length - listHeight,
1050
+ " more"
1051
+ ] })
1052
+ ] })
1053
+ ] });
1054
+ }
1055
+ if (mode === "input") {
1056
+ return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, height, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Input active \u2014 press Esc to exit" }) });
1057
+ }
1058
+ return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, gap: 1, height, children: SHORTCUTS.map(({ key, label }) => /* @__PURE__ */ jsxs2(Box2, { children: [
1059
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, bold: true, children: ` ${key} ` }),
1060
+ /* @__PURE__ */ jsx2(Text2, { children: label })
1061
+ ] }, key)) });
1062
+ }
1063
+
1064
+ // src/tui/components/layout/help-overlay.tsx
1065
+ import "react";
1066
+ import { Box as Box3, Text as Text3 } from "ink";
1067
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1068
+ var KEYBINDINGS = [
1069
+ ["Navigation", [
1070
+ ["\u2191 / k", "Move up"],
1071
+ ["\u2193 / j", "Move down"],
1072
+ ["\u2190 / h", "Focus left panel"],
1073
+ ["\u2192 / l", "Focus right panel"],
1074
+ ["Tab", "Cycle panel focus"],
1075
+ ["PgUp / PgDn", "Page up / down"],
1076
+ ["Home / End", "Scroll to top / bottom"],
1077
+ ["Enter", "Select / open detail view"],
1078
+ ["d", "Dispatch selected task"]
1079
+ ]],
1080
+ ["Views", [
1081
+ ["1", "Dashboard (default)"],
1082
+ ["2", "Plan Generation"],
1083
+ ["3", "Projects & Plan"],
1084
+ ["4", "Playground"],
1085
+ ["5", "Output"]
1086
+ ]],
1087
+ ["Shortcuts", [
1088
+ ["Ctrl+P / :", "Open command palette (searchable)"],
1089
+ ["Ctrl+F / /", "Open search"],
1090
+ ["Ctrl+R", "Refresh all data"],
1091
+ ["?", "Toggle this help"],
1092
+ ["q / Ctrl+C", "Quit"],
1093
+ ["Esc", "Close overlay / cancel input"]
1094
+ ]],
1095
+ ["Commands (via Ctrl+P)", [
1096
+ ["project list / create / delete", "Project management"],
1097
+ ["plan create-node", "Create a plan node"],
1098
+ ["dispatch <nodeId>", "Dispatch task for execution"],
1099
+ ["cancel <execId>", "Cancel execution"],
1100
+ ["steer <message>", "Steer running task"],
1101
+ ["watch <execId>", "Watch execution output"],
1102
+ ["env list / status", "Machine management"],
1103
+ ["activity", "Show activity feed"],
1104
+ ["refresh / r", "Force refresh"],
1105
+ ["quit / q", "Exit TUI"]
1106
+ ]],
1107
+ ["Terminal Compatibility", [
1108
+ ["tmux", "Ctrl+B prefix is not captured \u2014 safe"],
1109
+ ["screen", "Ctrl+A prefix is not captured \u2014 safe"],
1110
+ ["vscode", "All bindings work in integrated terminal"]
1111
+ ]]
1112
+ ];
1113
+ function HelpOverlay() {
1114
+ return /* @__PURE__ */ jsxs3(
1115
+ Box3,
1116
+ {
1117
+ flexDirection: "column",
1118
+ borderStyle: "round",
1119
+ borderColor: "yellow",
1120
+ paddingX: 2,
1121
+ paddingY: 1,
1122
+ children: [
1123
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: " Keybindings Reference " }),
1124
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
1125
+ KEYBINDINGS.map(([section, bindings]) => /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
1126
+ /* @__PURE__ */ jsx3(Text3, { bold: true, underline: true, children: section }),
1127
+ bindings.map(([key, desc]) => /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
1128
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: key.padEnd(34) }),
1129
+ /* @__PURE__ */ jsx3(Text3, { children: desc })
1130
+ ] }, key))
1131
+ ] }, section)),
1132
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Press Esc or ? to close" })
1133
+ ]
1134
+ }
1135
+ );
1136
+ }
1137
+
1138
+ // src/tui/components/layout/search-overlay.tsx
1139
+ import { useInput } from "ink";
1140
+ function SearchOverlay() {
1141
+ const isOpen = useSearchStore((s) => s.isOpen);
1142
+ const query = useSearchStore((s) => s.query);
1143
+ const results = useSearchStore((s) => s.results);
1144
+ const items = useSearchStore((s) => s.items);
1145
+ const selectedIndex = useSearchStore((s) => s.selectedIndex);
1146
+ const { setQuery, moveUp, moveDown, close } = useSearchStore();
1147
+ const { setSelectedProject, setSelectedNode, setSelectedMachine, focusPanel, openDetail } = useTuiStore();
1148
+ useInput((input, key) => {
1149
+ if (!isOpen) return;
1150
+ if (key.escape) {
1151
+ close();
1152
+ return;
1153
+ }
1154
+ if (key.upArrow) {
1155
+ moveUp();
1156
+ return;
1157
+ }
1158
+ if (key.downArrow || key.tab) {
1159
+ moveDown();
1160
+ return;
1161
+ }
1162
+ if (key.return) {
1163
+ const displayList = query.length > 0 ? results : items;
1164
+ const item = displayList[selectedIndex];
1165
+ if (item) {
1166
+ switch (item.type) {
1167
+ case "project":
1168
+ setSelectedProject(item.id);
1169
+ focusPanel("projects");
1170
+ break;
1171
+ case "task":
1172
+ setSelectedNode(item.id);
1173
+ focusPanel("plan");
1174
+ openDetail("node", item.id);
1175
+ break;
1176
+ case "machine":
1177
+ setSelectedMachine(item.id);
1178
+ focusPanel("machines");
1179
+ break;
1180
+ }
1181
+ }
1182
+ close();
1183
+ return;
1184
+ }
1185
+ if (key.backspace || key.delete) {
1186
+ setQuery(query.slice(0, -1));
1187
+ return;
1188
+ }
1189
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
1190
+ setQuery(query + input);
1191
+ }
1192
+ }, { isActive: isOpen });
1193
+ return null;
1194
+ }
1195
+
1196
+ // src/tui/components/panels/projects-panel.tsx
1197
+ import "react";
1198
+ import { Box as Box5, Text as Text6 } from "ink";
1199
+
1200
+ // src/tui/components/layout/panel.tsx
1201
+ import "react";
1202
+ import { Box as Box4, Text as Text4 } from "ink";
1203
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1204
+ function Panel({ title, isFocused, children, width, height }) {
1205
+ const borderColor = isFocused ? "cyan" : "gray";
1206
+ return /* @__PURE__ */ jsxs4(
1207
+ Box4,
1208
+ {
1209
+ flexDirection: "column",
1210
+ borderStyle: "single",
1211
+ borderColor,
1212
+ width,
1213
+ height,
1214
+ flexGrow: 1,
1215
+ children: [
1216
+ /* @__PURE__ */ jsx4(Box4, { paddingX: 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: isFocused ? "cyan" : "white", children: title }) }),
1217
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, children })
1218
+ ]
1219
+ }
1220
+ );
1221
+ }
1222
+
1223
+ // src/tui/components/shared/spinner.tsx
1224
+ import { useState, useEffect } from "react";
1225
+ import { Text as Text5 } from "ink";
1226
+ import { jsxs as jsxs5 } from "react/jsx-runtime";
1227
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1228
+ function Spinner({ label }) {
1229
+ const [frame, setFrame] = useState(0);
1230
+ useEffect(() => {
1231
+ const timer = setInterval(() => {
1232
+ setFrame((f) => (f + 1) % FRAMES.length);
1233
+ }, 80);
1234
+ return () => clearInterval(timer);
1235
+ }, []);
1236
+ return /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
1237
+ FRAMES[frame],
1238
+ label ? ` ${label}` : ""
1239
+ ] });
1240
+ }
1241
+
1242
+ // src/tui/components/panels/projects-panel.tsx
1243
+ import { Fragment, jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
1244
+ function statusSymbol(status) {
1245
+ if (status === "running") return "\u25B6";
1246
+ if (status === "success") return "\u2713";
1247
+ if (status === "failure" || status === "error") return "\u2717";
1248
+ return "\xB7";
1249
+ }
1250
+ function ProjectsPanel({ height }) {
1251
+ const projects = useProjectsStore((s) => s.projects);
1252
+ const loading = useProjectsStore((s) => s.loading);
1253
+ const error = useProjectsStore((s) => s.error);
1254
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
1255
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.projects);
1256
+ const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
1257
+ const outputs = useExecutionStore((s) => s.outputs);
1258
+ const isFocused = focusedPanel === "projects";
1259
+ const visibleHeight = Math.max(1, height - 3);
1260
+ const planGenEntries = [];
1261
+ const playgroundEntries = [];
1262
+ for (const [id, exec] of outputs) {
1263
+ if (exec.nodeId.startsWith("plan-")) {
1264
+ planGenEntries.push({ id, label: exec.nodeId, status: exec.status });
1265
+ } else if (exec.nodeId.startsWith("playground-")) {
1266
+ playgroundEntries.push({ id, label: exec.nodeId.replace(/^playground-/, "").slice(0, 30), status: exec.status });
1267
+ }
1268
+ }
1269
+ const projectCount = projects.length;
1270
+ const clampedIndex = Math.min(scrollIndex, Math.max(0, projectCount - 1));
1271
+ const sectionLineCount = (planGenEntries.length > 0 ? 1 + planGenEntries.length : 0) + (playgroundEntries.length > 0 ? 1 + playgroundEntries.length : 0);
1272
+ const projectVisibleHeight = Math.max(1, visibleHeight - sectionLineCount);
758
1273
  let start = 0;
759
- if (scrollIndex >= visibleHeight) {
760
- start = scrollIndex - visibleHeight + 1;
1274
+ if (projectCount > projectVisibleHeight) {
1275
+ start = Math.max(0, Math.min(clampedIndex - Math.floor(projectVisibleHeight / 2), projectCount - projectVisibleHeight));
761
1276
  }
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) => {
1277
+ const visibleProjects = projects.slice(start, start + projectVisibleHeight);
1278
+ return /* @__PURE__ */ jsx5(Panel, { title: "PROJECTS", isFocused, height, children: loading && projects.length === 0 ? /* @__PURE__ */ jsx5(Spinner, { label: "Loading projects..." }) : error ? /* @__PURE__ */ jsx5(Text6, { color: "red", children: error }) : /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", children: [
1279
+ visibleProjects.length === 0 && planGenEntries.length === 0 && playgroundEntries.length === 0 && /* @__PURE__ */ jsx5(Text6, { dimColor: true, children: " No projects yet" }),
1280
+ visibleProjects.map((p, i) => {
765
1281
  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);
1282
+ const isSelected = isFocused && clampedIndex === actualIndex;
1283
+ const isActive = p.id === selectedProjectId;
1284
+ return /* @__PURE__ */ jsxs6(Box5, { children: [
1285
+ /* @__PURE__ */ jsxs6(
1286
+ Text6,
1287
+ {
1288
+ inverse: isSelected,
1289
+ bold: isSelected,
1290
+ color: isActive ? "cyan" : void 0,
1291
+ wrap: "truncate",
1292
+ children: [
1293
+ isSelected ? " > " : " ",
1294
+ truncate(p.name, 30)
1295
+ ]
1296
+ }
1297
+ ),
1298
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: !isSelected, children: [
1299
+ " ",
1300
+ formatRelativeTime(p.updatedAt)
1301
+ ] })
1302
+ ] }, p.id);
776
1303
  }),
777
- treeLines.length > visibleHeight && /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
1304
+ projectCount > projectVisibleHeight && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
778
1305
  " [",
779
1306
  start + 1,
780
1307
  "-",
781
- Math.min(start + visibleHeight, treeLines.length),
1308
+ Math.min(start + projectVisibleHeight, projectCount),
782
1309
  "/",
783
- treeLines.length,
1310
+ projectCount,
784
1311
  "]"
1312
+ ] }),
1313
+ planGenEntries.length > 0 && /* @__PURE__ */ jsxs6(Fragment, { children: [
1314
+ /* @__PURE__ */ jsx5(Text6, { bold: true, color: "yellow", children: " Plan Generation" }),
1315
+ planGenEntries.map((item) => /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate", children: [
1316
+ " ",
1317
+ statusSymbol(item.status),
1318
+ " ",
1319
+ truncate(item.label, 28),
1320
+ " ",
1321
+ item.status
1322
+ ] }, item.id))
1323
+ ] }),
1324
+ playgroundEntries.length > 0 && /* @__PURE__ */ jsxs6(Fragment, { children: [
1325
+ /* @__PURE__ */ jsx5(Text6, { bold: true, color: "green", children: " Playground" }),
1326
+ playgroundEntries.map((item) => /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate", children: [
1327
+ " ",
1328
+ statusSymbol(item.status),
1329
+ " ",
1330
+ truncate(item.label, 28),
1331
+ " ",
1332
+ item.status
1333
+ ] }, item.id))
785
1334
  ] })
786
1335
  ] }) });
787
1336
  }
788
1337
 
789
- // src/tui/components/panels/machines-panel.tsx
1338
+ // src/tui/components/panels/plan-panel.tsx
1339
+ import "react";
1340
+ import { Text as Text8 } from "ink";
1341
+
1342
+ // src/tui/components/shared/scrollable-list.tsx
790
1343
  import "react";
791
- import { Text as Text10 } from "ink";
1344
+ import { Box as Box6, Text as Text7, useStdout as useStdout2 } from "ink";
1345
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
1346
+ function ScrollableList({ items, selectedIndex, height, isFocused }) {
1347
+ const { stdout } = useStdout2();
1348
+ const termWidth = stdout?.columns ?? 80;
1349
+ if (items.length === 0) {
1350
+ return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: " No items." }) });
1351
+ }
1352
+ const visibleHeight = Math.max(1, height - 1);
1353
+ let start = 0;
1354
+ if (selectedIndex >= visibleHeight) {
1355
+ start = selectedIndex - visibleHeight + 1;
1356
+ }
1357
+ const visibleItems = items.slice(start, start + visibleHeight);
1358
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", children: [
1359
+ visibleItems.map((item, i) => {
1360
+ const actualIndex = start + i;
1361
+ const isSelected = actualIndex === selectedIndex && isFocused;
1362
+ const prefix = isSelected ? " \u25B6 " : " ";
1363
+ const sublabel = item.sublabel ? ` ${item.sublabel}` : "";
1364
+ const rightLabel = item.rightLabel ?? "";
1365
+ const maxLabelWidth = Math.max(10, termWidth - prefix.length - sublabel.length - rightLabel.length - 10);
1366
+ const truncatedLabel = item.label.length > maxLabelWidth ? item.label.slice(0, maxLabelWidth - 1) + "\u2026" : item.label;
1367
+ return /* @__PURE__ */ jsxs7(Box6, { children: [
1368
+ /* @__PURE__ */ jsxs7(
1369
+ Text7,
1370
+ {
1371
+ color: isSelected ? "cyan" : item.color ?? void 0,
1372
+ bold: isSelected,
1373
+ inverse: isSelected,
1374
+ wrap: "truncate",
1375
+ children: [
1376
+ prefix,
1377
+ truncatedLabel,
1378
+ sublabel
1379
+ ]
1380
+ }
1381
+ ),
1382
+ rightLabel && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1383
+ " ",
1384
+ rightLabel
1385
+ ] })
1386
+ ] }, item.id);
1387
+ }),
1388
+ items.length > visibleHeight && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1389
+ " [",
1390
+ start + 1,
1391
+ "-",
1392
+ Math.min(start + visibleHeight, items.length),
1393
+ "/",
1394
+ items.length,
1395
+ "]"
1396
+ ] })
1397
+ ] });
1398
+ }
792
1399
 
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] });
1400
+ // src/tui/components/panels/plan-panel.tsx
1401
+ import { jsx as jsx7 } from "react/jsx-runtime";
1402
+ function PlanPanel({ height }) {
1403
+ const nodes = usePlanStore((s) => s.nodes);
1404
+ const loading = usePlanStore((s) => s.loading);
1405
+ const error = usePlanStore((s) => s.error);
1406
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
1407
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.plan);
1408
+ const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
1409
+ const selectedNodeId = useTuiStore((s) => s.selectedNodeId);
1410
+ const projects = useProjectsStore((s) => s.projects);
1411
+ const isFocused = focusedPanel === "plan";
1412
+ const projectName = projects.find((p) => p.id === selectedProjectId)?.name ?? "none";
1413
+ const visibleNodes = nodes.filter((n) => !n.deletedAt);
1414
+ const items = visibleNodes.map((n) => ({
1415
+ id: n.id,
1416
+ label: `${getStatusSymbol(n.status)} ${n.title}`,
1417
+ sublabel: `[${n.status}]`,
1418
+ color: n.id === selectedNodeId ? "cyan" : getStatusColor(n.status)
1419
+ }));
1420
+ return /* @__PURE__ */ jsx7(Panel, { title: `PLAN (${projectName}) d:dispatch`, isFocused, height, children: loading && nodes.length === 0 ? /* @__PURE__ */ jsx7(Spinner, { label: "Loading plan..." }) : error ? /* @__PURE__ */ jsx7(Text8, { color: "red", children: error }) : !selectedProjectId ? /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: " Select a project to view its plan" }) : visibleNodes.length === 0 ? /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: " No plan nodes. Use Ctrl+P \u2192 plan create-node" }) : /* @__PURE__ */ jsx7(
1421
+ ScrollableList,
1422
+ {
1423
+ items,
1424
+ selectedIndex: scrollIndex,
1425
+ height: height - 3,
1426
+ isFocused
815
1427
  }
816
- },
817
- setLoading: (loading) => set({ loading }),
818
- setError: (error) => set({ error, loading: false })
819
- }));
1428
+ ) });
1429
+ }
820
1430
 
821
1431
  // src/tui/components/panels/machines-panel.tsx
822
- import { jsx as jsx9 } from "react/jsx-runtime";
1432
+ import "react";
1433
+ import { Text as Text9 } from "ink";
1434
+ import { jsx as jsx8 } from "react/jsx-runtime";
823
1435
  function MachinesPanel({ height }) {
824
1436
  const machines = useMachinesStore((s) => s.machines);
825
1437
  const loading = useMachinesStore((s) => s.loading);
@@ -835,7 +1447,7 @@ function MachinesPanel({ height }) {
835
1447
  rightLabel: m.isConnected ? "\u25CF online" : "\u25CB offline",
836
1448
  color: m.isConnected ? "green" : "gray"
837
1449
  }));
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(
1450
+ return /* @__PURE__ */ jsx8(Panel, { title: "MACHINES", isFocused, height, children: loading && machines.length === 0 ? /* @__PURE__ */ jsx8(Spinner, { label: "Loading machines..." }) : error ? /* @__PURE__ */ jsx8(Text9, { color: "red", children: error }) : /* @__PURE__ */ jsx8(
839
1451
  ScrollableList,
840
1452
  {
841
1453
  items,
@@ -848,114 +1460,8 @@ function MachinesPanel({ height }) {
848
1460
 
849
1461
  // src/tui/components/panels/output-panel.tsx
850
1462
  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";
1463
+ import { Box as Box7, Text as Text10 } from "ink";
1464
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
959
1465
  function lineColor(line) {
960
1466
  if (line.startsWith("[Tool Call]")) return "gray";
961
1467
  if (line.startsWith("[error]")) return "red";
@@ -977,7 +1483,7 @@ function OutputPanel({ height }) {
977
1483
  title = `OUTPUT (${shortId}) [${execution.status}]`;
978
1484
  }
979
1485
  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" }) });
1486
+ return /* @__PURE__ */ jsx9(Panel, { title, isFocused, height, children: /* @__PURE__ */ jsx9(Text10, { dimColor: true, children: " No active execution. Dispatch a task with 'd' or :dispatch" }) });
981
1487
  }
982
1488
  const lines = execution.lines;
983
1489
  const isRunning = execution.status === "running";
@@ -989,11 +1495,11 @@ function OutputPanel({ height }) {
989
1495
  start = Math.max(0, scrollIndex);
990
1496
  }
991
1497
  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: [
1498
+ return /* @__PURE__ */ jsx9(Panel, { title, isFocused, height, children: /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
1499
+ visibleLines.map((line, i) => /* @__PURE__ */ jsx9(Text10, { color: lineColor(line), dimColor: line.startsWith("[Tool Call]"), wrap: "truncate", children: truncate(line, 200) }, start + i)),
1500
+ hasPendingTools && /* @__PURE__ */ jsx9(Text10, { dimColor: true, children: "[Tool Call] " + "\xB7".repeat(execution.pendingToolCount) }),
1501
+ isRunning && !hasPendingTools && /* @__PURE__ */ jsx9(Spinner, { label: "Running..." }),
1502
+ lines.length > visibleHeight && /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
997
1503
  " [",
998
1504
  start + 1,
999
1505
  "-",
@@ -1005,17 +1511,172 @@ function OutputPanel({ height }) {
1005
1511
  ] }) });
1006
1512
  }
1007
1513
 
1008
- // src/tui/components/panels/detail-overlay.tsx
1514
+ // src/tui/components/panels/chat-panel.tsx
1515
+ import "react";
1516
+ import { Box as Box8, Text as Text11 } from "ink";
1517
+ import { TextInput } from "@inkjs/ui";
1518
+
1519
+ // src/tui/stores/chat-store.ts
1520
+ import { create as create7 } from "zustand";
1521
+ var useChatStore = create7((set, get) => ({
1522
+ messages: [],
1523
+ sessionId: null,
1524
+ projectId: null,
1525
+ nodeId: null,
1526
+ streaming: false,
1527
+ streamBuffer: "",
1528
+ addMessage: (role, content) => {
1529
+ set((s) => ({
1530
+ messages: [...s.messages, { role, content, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
1531
+ }));
1532
+ },
1533
+ appendStream: (text) => {
1534
+ set((s) => ({ streamBuffer: s.streamBuffer + text }));
1535
+ },
1536
+ flushStream: () => {
1537
+ const { streamBuffer } = get();
1538
+ if (streamBuffer.length > 0) {
1539
+ set((s) => ({
1540
+ messages: [...s.messages, { role: "assistant", content: streamBuffer, timestamp: (/* @__PURE__ */ new Date()).toISOString() }],
1541
+ streamBuffer: ""
1542
+ }));
1543
+ }
1544
+ },
1545
+ setSessionId: (sessionId) => set({ sessionId }),
1546
+ setContext: (projectId, nodeId) => set({ projectId, nodeId: nodeId ?? null }),
1547
+ setStreaming: (streaming) => set({ streaming }),
1548
+ clear: () => set({ messages: [], sessionId: null, streamBuffer: "", streaming: false })
1549
+ }));
1550
+
1551
+ // src/tui/components/panels/chat-panel.tsx
1552
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1553
+ function ChatPanel({ height }) {
1554
+ const messages = useChatStore((s) => s.messages);
1555
+ const streaming = useChatStore((s) => s.streaming);
1556
+ const streamBuffer = useChatStore((s) => s.streamBuffer);
1557
+ const sessionId = useChatStore((s) => s.sessionId);
1558
+ const displayLines = [];
1559
+ for (let i = 0; i < messages.length; i++) {
1560
+ const m = messages[i];
1561
+ const prefix = m.role === "user" ? "> " : " ";
1562
+ displayLines.push({ key: `msg-${i}`, text: `${prefix}${m.content}`, color: m.role === "user" ? "cyan" : void 0 });
1563
+ }
1564
+ if (streaming && streamBuffer) {
1565
+ displayLines.push({ key: "stream", text: ` ${streamBuffer}` });
1566
+ }
1567
+ const contentHeight = height - 4;
1568
+ const visibleLines = displayLines.slice(-Math.max(1, contentHeight));
1569
+ const titleSuffix = sessionId ? ` (${sessionId.slice(0, 8)})` : "";
1570
+ return /* @__PURE__ */ jsx10(Panel, { title: `Chat${titleSuffix}`, isFocused: false, height, children: /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
1571
+ /* @__PURE__ */ jsx10(Box8, { flexDirection: "column", height: Math.max(1, contentHeight - 1), children: visibleLines.map((line) => /* @__PURE__ */ jsx10(Text11, { color: line.color, wrap: "truncate", children: line.text }, line.key)) }),
1572
+ /* @__PURE__ */ jsx10(Box8, { children: streaming ? /* @__PURE__ */ jsxs9(Box8, { children: [
1573
+ /* @__PURE__ */ jsx10(Spinner, {}),
1574
+ /* @__PURE__ */ jsx10(Text11, { dimColor: true, children: " Streaming..." })
1575
+ ] }) : /* @__PURE__ */ jsxs9(Box8, { children: [
1576
+ /* @__PURE__ */ jsx10(Text11, { color: "cyan", children: "> " }),
1577
+ /* @__PURE__ */ jsx10(
1578
+ TextInput,
1579
+ {
1580
+ placeholder: "Type a message...",
1581
+ onSubmit: (value) => {
1582
+ if (value.trim()) {
1583
+ useChatStore.getState().addMessage("user", value.trim());
1584
+ }
1585
+ }
1586
+ }
1587
+ )
1588
+ ] }) })
1589
+ ] }) });
1590
+ }
1591
+
1592
+ // src/tui/components/panels/session-panel.tsx
1009
1593
  import "react";
1010
1594
  import { Box as Box9, Text as Text12 } from "ink";
1595
+ import { TextInput as TextInput2 } from "@inkjs/ui";
1011
1596
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1597
+ function lineColor2(line) {
1598
+ if (line.startsWith("[Tool Call]")) return "gray";
1599
+ if (line.startsWith("[error]")) return "red";
1600
+ if (line.startsWith("[progress]")) return "cyan";
1601
+ if (line.startsWith("[plan]")) return "magenta";
1602
+ if (line.startsWith("[created]") || line.startsWith("[modified]") || line.startsWith("[deleted]")) return "yellow";
1603
+ if (line.startsWith("> ")) return "cyan";
1604
+ return void 0;
1605
+ }
1606
+ function SessionPanel({ height, title, sessionType, onSubmit }) {
1607
+ const watchingId = useExecutionStore((s) => s.watchingId);
1608
+ const outputs = useExecutionStore((s) => s.outputs);
1609
+ const streaming = useChatStore((s) => s.streaming);
1610
+ const mode = useTuiStore((s) => s.mode);
1611
+ const execution = watchingId ? outputs.get(watchingId) : null;
1612
+ const isRunning = execution?.status === "running";
1613
+ const inputHeight = 2;
1614
+ const outputHeight = Math.max(1, height - 5 - inputHeight);
1615
+ const isInputActive = mode === "input";
1616
+ const lines = execution?.lines ?? [];
1617
+ const hasPendingTools = (execution?.pendingToolCount ?? 0) > 0;
1618
+ const start = Math.max(0, lines.length - outputHeight);
1619
+ const visibleLines = lines.slice(start, start + outputHeight);
1620
+ let statusLabel = "";
1621
+ if (execution) {
1622
+ statusLabel = ` [${execution.status}]`;
1623
+ }
1624
+ const hint = sessionType === "playground" ? "Describe what you want to build or explore" : "Describe the plan you want to generate";
1625
+ return /* @__PURE__ */ jsx11(Panel, { title: `${title}${statusLabel}`, isFocused: true, height, children: /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1626
+ /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", height: outputHeight, children: [
1627
+ visibleLines.length === 0 && !isRunning ? /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
1628
+ " ",
1629
+ hint,
1630
+ ". Type below and press Enter."
1631
+ ] }) : visibleLines.map((line, i) => /* @__PURE__ */ jsx11(Text12, { color: lineColor2(line), dimColor: line.startsWith("[Tool Call]"), wrap: "truncate", children: truncate(line, 200) }, start + i)),
1632
+ hasPendingTools && /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "[Tool Call] " + "\xB7".repeat(execution.pendingToolCount) }),
1633
+ isRunning && !hasPendingTools && /* @__PURE__ */ jsx11(Spinner, { label: "Running..." })
1634
+ ] }),
1635
+ lines.length > outputHeight && /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
1636
+ " [",
1637
+ start + 1,
1638
+ "-",
1639
+ Math.min(start + outputHeight, lines.length),
1640
+ "/",
1641
+ lines.length,
1642
+ "]"
1643
+ ] }),
1644
+ /* @__PURE__ */ jsx11(Box9, { borderStyle: "single", borderColor: isInputActive ? "cyan" : "gray", paddingX: 1, children: isRunning || streaming ? /* @__PURE__ */ jsxs10(Box9, { children: [
1645
+ /* @__PURE__ */ jsx11(Spinner, {}),
1646
+ /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: " Agent is working... (press Esc to return to navigation)" })
1647
+ ] }) : /* @__PURE__ */ jsxs10(Box9, { children: [
1648
+ /* @__PURE__ */ jsx11(Text12, { color: "cyan", children: "> " }),
1649
+ isInputActive ? /* @__PURE__ */ jsx11(
1650
+ TextInput2,
1651
+ {
1652
+ placeholder: hint,
1653
+ onSubmit: (value) => {
1654
+ if (value.trim()) {
1655
+ const msg = value.trim();
1656
+ if (watchingId) {
1657
+ useExecutionStore.getState().appendLine(watchingId, `> ${msg}`);
1658
+ }
1659
+ useChatStore.getState().addMessage("user", msg);
1660
+ onSubmit?.(msg);
1661
+ }
1662
+ }
1663
+ }
1664
+ ) : /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "Press Enter to start typing..." })
1665
+ ] }) })
1666
+ ] }) });
1667
+ }
1668
+
1669
+ // src/tui/components/panels/detail-overlay.tsx
1670
+ import "react";
1671
+ import { Box as Box10, Text as Text13 } from "ink";
1672
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1012
1673
  function DetailOverlay() {
1013
1674
  const showDetail = useTuiStore((s) => s.showDetail);
1014
1675
  const detailType = useTuiStore((s) => s.detailType);
1015
1676
  const detailId = useTuiStore((s) => s.detailId);
1016
1677
  if (!showDetail || !detailType || !detailId) return null;
1017
- return /* @__PURE__ */ jsxs10(
1018
- Box9,
1678
+ return /* @__PURE__ */ jsxs11(
1679
+ Box10,
1019
1680
  {
1020
1681
  flexDirection: "column",
1021
1682
  borderStyle: "round",
@@ -1023,124 +1684,218 @@ function DetailOverlay() {
1023
1684
  paddingX: 2,
1024
1685
  paddingY: 1,
1025
1686
  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" })
1687
+ detailType === "project" && /* @__PURE__ */ jsx12(ProjectDetail, { id: detailId }),
1688
+ detailType === "node" && /* @__PURE__ */ jsx12(NodeDetail, { id: detailId }),
1689
+ detailType === "machine" && /* @__PURE__ */ jsx12(MachineDetail, { id: detailId }),
1690
+ /* @__PURE__ */ jsx12(Text13, { dimColor: true, children: "Press Esc to close" })
1030
1691
  ]
1031
1692
  }
1032
1693
  );
1033
1694
  }
1034
1695
  function ProjectDetail({ id }) {
1035
1696
  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: " " })
1697
+ if (!project) return /* @__PURE__ */ jsx12(Text13, { color: "red", children: "Project not found" });
1698
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
1699
+ /* @__PURE__ */ jsx12(Text13, { bold: true, color: "cyan", children: project.name }),
1700
+ /* @__PURE__ */ jsx12(Text13, { children: " " }),
1701
+ /* @__PURE__ */ jsx12(Field, { label: "ID", value: project.id }),
1702
+ /* @__PURE__ */ jsx12(Field, { label: "Status", value: project.status, color: getStatusColor(project.status) }),
1703
+ /* @__PURE__ */ jsx12(Field, { label: "Description", value: project.description || "\u2014" }),
1704
+ /* @__PURE__ */ jsx12(Field, { label: "Health", value: project.health ?? "\u2014" }),
1705
+ /* @__PURE__ */ jsx12(Field, { label: "Progress", value: `${project.progress}%` }),
1706
+ /* @__PURE__ */ jsx12(Field, { label: "Working Dir", value: project.workingDirectory ?? "\u2014" }),
1707
+ /* @__PURE__ */ jsx12(Field, { label: "Repository", value: project.repository ?? "\u2014" }),
1708
+ /* @__PURE__ */ jsx12(Field, { label: "Delivery", value: project.deliveryMode ?? "\u2014" }),
1709
+ /* @__PURE__ */ jsx12(Field, { label: "Start Date", value: project.startDate ?? "\u2014" }),
1710
+ /* @__PURE__ */ jsx12(Field, { label: "Target Date", value: project.targetDate ?? "\u2014" }),
1711
+ /* @__PURE__ */ jsx12(Field, { label: "Created", value: formatRelativeTime(project.createdAt) }),
1712
+ /* @__PURE__ */ jsx12(Field, { label: "Updated", value: formatRelativeTime(project.updatedAt) }),
1713
+ /* @__PURE__ */ jsx12(Text13, { children: " " })
1053
1714
  ] });
1054
1715
  }
1055
1716
  function NodeDetail({ id }) {
1056
1717
  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: " " })
1718
+ if (!node) return /* @__PURE__ */ jsx12(Text13, { color: "red", children: "Node not found" });
1719
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
1720
+ /* @__PURE__ */ jsx12(Text13, { bold: true, color: "cyan", children: node.title }),
1721
+ /* @__PURE__ */ jsx12(Text13, { children: " " }),
1722
+ /* @__PURE__ */ jsx12(Field, { label: "ID", value: node.id }),
1723
+ /* @__PURE__ */ jsx12(Field, { label: "Type", value: node.type }),
1724
+ /* @__PURE__ */ jsx12(Field, { label: "Status", value: node.status, color: getStatusColor(node.status) }),
1725
+ /* @__PURE__ */ jsx12(Field, { label: "Description", value: node.description || "\u2014" }),
1726
+ /* @__PURE__ */ jsx12(Field, { label: "Priority", value: node.priority ?? "\u2014" }),
1727
+ /* @__PURE__ */ jsx12(Field, { label: "Estimate", value: node.estimate ?? "\u2014" }),
1728
+ /* @__PURE__ */ jsx12(Field, { label: "Start Date", value: node.startDate ?? "\u2014" }),
1729
+ /* @__PURE__ */ jsx12(Field, { label: "End Date", value: node.endDate ?? "\u2014" }),
1730
+ /* @__PURE__ */ jsx12(Field, { label: "Due Date", value: node.dueDate ?? "\u2014" }),
1731
+ /* @__PURE__ */ jsx12(Field, { label: "Branch", value: node.branchName ?? "\u2014" }),
1732
+ /* @__PURE__ */ jsx12(Field, { label: "PR URL", value: node.prUrl ?? "\u2014" }),
1733
+ /* @__PURE__ */ jsx12(Field, { label: "Execution ID", value: node.executionId ?? "\u2014" }),
1734
+ /* @__PURE__ */ jsx12(Field, { label: "Exec Started", value: node.executionStartedAt ? formatRelativeTime(node.executionStartedAt) : "\u2014" }),
1735
+ /* @__PURE__ */ jsx12(Field, { label: "Exec Completed", value: node.executionCompletedAt ? formatRelativeTime(node.executionCompletedAt) : "\u2014" }),
1736
+ /* @__PURE__ */ jsx12(Text13, { children: " " })
1076
1737
  ] });
1077
1738
  }
1078
1739
  function MachineDetail({ id }) {
1079
1740
  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: " " })
1741
+ if (!machine) return /* @__PURE__ */ jsx12(Text13, { color: "red", children: "Machine not found" });
1742
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
1743
+ /* @__PURE__ */ jsx12(Text13, { bold: true, color: "cyan", children: machine.name }),
1744
+ /* @__PURE__ */ jsx12(Text13, { children: " " }),
1745
+ /* @__PURE__ */ jsx12(Field, { label: "ID", value: machine.id }),
1746
+ /* @__PURE__ */ jsx12(Field, { label: "Hostname", value: machine.hostname }),
1747
+ /* @__PURE__ */ jsx12(Field, { label: "Platform", value: machine.platform }),
1748
+ /* @__PURE__ */ jsx12(Field, { label: "Env Type", value: machine.environmentType }),
1749
+ /* @__PURE__ */ jsx12(Field, { label: "Connected", value: machine.isConnected ? "Yes" : "No", color: machine.isConnected ? "green" : "red" }),
1750
+ /* @__PURE__ */ jsx12(Field, { label: "Providers", value: machine.providers.join(", ") || "\u2014" }),
1751
+ /* @__PURE__ */ jsx12(Field, { label: "Registered", value: formatRelativeTime(machine.registeredAt) }),
1752
+ /* @__PURE__ */ jsx12(Field, { label: "Last Seen", value: formatRelativeTime(machine.lastSeenAt) }),
1753
+ /* @__PURE__ */ jsx12(Text13, { children: " " })
1093
1754
  ] });
1094
1755
  }
1095
1756
  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 })
1757
+ return /* @__PURE__ */ jsxs11(Box10, { children: [
1758
+ /* @__PURE__ */ jsx12(Text13, { dimColor: true, children: label.padEnd(16) }),
1759
+ color ? /* @__PURE__ */ jsx12(Text13, { color, children: value }) : /* @__PURE__ */ jsx12(Text13, { children: value })
1099
1760
  ] });
1100
1761
  }
1101
1762
 
1763
+ // src/tui/components/shared/approval-dialog.tsx
1764
+ import { useState as useState2 } from "react";
1765
+ import { Box as Box11, Text as Text14, useInput as useInput2 } from "ink";
1766
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1767
+ function ApprovalDialog({ question, options, onSelect, onDismiss }) {
1768
+ const [selectedIndex, setSelectedIndex] = useState2(0);
1769
+ useInput2((input, key) => {
1770
+ if (key.escape) {
1771
+ onDismiss();
1772
+ return;
1773
+ }
1774
+ if (key.return) {
1775
+ onSelect(selectedIndex);
1776
+ return;
1777
+ }
1778
+ if (key.upArrow || input === "k") {
1779
+ setSelectedIndex((i) => Math.max(0, i - 1));
1780
+ }
1781
+ if (key.downArrow || input === "j") {
1782
+ setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
1783
+ }
1784
+ const num = parseInt(input, 10);
1785
+ if (num >= 1 && num <= options.length) {
1786
+ onSelect(num - 1);
1787
+ }
1788
+ });
1789
+ return /* @__PURE__ */ jsxs12(
1790
+ Box11,
1791
+ {
1792
+ flexDirection: "column",
1793
+ borderStyle: "round",
1794
+ borderColor: "yellow",
1795
+ paddingX: 1,
1796
+ paddingY: 0,
1797
+ children: [
1798
+ /* @__PURE__ */ jsx13(Text14, { color: "yellow", bold: true, children: "Approval Required" }),
1799
+ /* @__PURE__ */ jsx13(Text14, { wrap: "wrap", children: question }),
1800
+ /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsx13(Box11, { children: /* @__PURE__ */ jsxs12(Text14, { color: i === selectedIndex ? "cyan" : "white", children: [
1801
+ i === selectedIndex ? "\u25B8 " : " ",
1802
+ "[",
1803
+ i + 1,
1804
+ "] ",
1805
+ opt
1806
+ ] }) }, i)) }),
1807
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
1808
+ "\u2191\u2193/jk navigate \u2022 Enter select \u2022 1-",
1809
+ options.length,
1810
+ " quick select \u2022 Esc dismiss"
1811
+ ] }) })
1812
+ ]
1813
+ }
1814
+ );
1815
+ }
1816
+
1102
1817
  // src/tui/components/layout/main-layout.tsx
1103
- import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1104
- function MainLayout() {
1818
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1819
+ function MainLayout({ onSessionMessage }) {
1105
1820
  const showHelp = useTuiStore((s) => s.showHelp);
1106
1821
  const showDetail = useTuiStore((s) => s.showDetail);
1107
- const { stdout } = useStdout();
1822
+ const showChat = useTuiStore((s) => s.showChat);
1823
+ const activeView = useTuiStore((s) => s.activeView);
1824
+ const mode = useTuiStore((s) => s.mode);
1825
+ const searchOpen = useSearchStore((s) => s.isOpen);
1826
+ const pendingApproval = useExecutionStore((s) => s.pendingApproval);
1827
+ const { stdout } = useStdout3();
1108
1828
  const termHeight = stdout?.rows ?? 24;
1109
1829
  const termWidth = stdout?.columns ?? 80;
1110
- const topRowHeight = Math.floor((termHeight - 4) / 2);
1111
- const bottomRowHeight = termHeight - 4 - topRowHeight;
1830
+ const panelOpen = searchOpen || mode === "palette";
1831
+ const bottomPanelHeight = panelOpen ? Math.floor(termHeight / 2) : 1;
1832
+ const contentHeight = termHeight - 2 - bottomPanelHeight;
1112
1833
  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, {})
1834
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1835
+ /* @__PURE__ */ jsx14(StatusBar, {}),
1836
+ /* @__PURE__ */ jsx14(HelpOverlay, {}),
1837
+ /* @__PURE__ */ jsx14(CommandLine, { height: 1 })
1117
1838
  ] });
1118
1839
  }
1119
1840
  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, {})
1841
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1842
+ /* @__PURE__ */ jsx14(StatusBar, {}),
1843
+ /* @__PURE__ */ jsx14(DetailOverlay, {}),
1844
+ /* @__PURE__ */ jsx14(CommandLine, { height: 1 })
1124
1845
  ] });
1125
1846
  }
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, {})
1847
+ const approvalOverlay = pendingApproval ? /* @__PURE__ */ jsx14(Box12, { position: "absolute", marginTop: 4, marginLeft: Math.floor(termWidth / 4), children: /* @__PURE__ */ jsx14(
1848
+ ApprovalDialog,
1849
+ {
1850
+ question: pendingApproval.question,
1851
+ options: pendingApproval.options,
1852
+ onSelect: (index) => {
1853
+ useExecutionStore.getState().setPendingApproval(null);
1854
+ void index;
1855
+ },
1856
+ onDismiss: () => {
1857
+ useExecutionStore.getState().setPendingApproval(null);
1858
+ }
1859
+ }
1860
+ ) }) : null;
1861
+ let content;
1862
+ if (activeView === "plan-gen") {
1863
+ content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(SessionPanel, { height: contentHeight, title: "PLAN GENERATION", sessionType: "plan-generate", onSubmit: onSessionMessage }) }) });
1864
+ } else if (activeView === "projects") {
1865
+ content = /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: contentHeight, children: [
1866
+ /* @__PURE__ */ jsx14(Box12, { width: "40%", children: /* @__PURE__ */ jsx14(ProjectsPanel, { height: contentHeight }) }),
1867
+ /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(PlanPanel, { height: contentHeight }) })
1868
+ ] });
1869
+ } else if (activeView === "playground") {
1870
+ content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(SessionPanel, { height: contentHeight, title: "PLAYGROUND", sessionType: "playground", onSubmit: onSessionMessage }) }) });
1871
+ } else if (activeView === "output") {
1872
+ content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(OutputPanel, { height: contentHeight }) }) });
1873
+ } else {
1874
+ const topRowHeight = Math.floor(contentHeight / 2);
1875
+ const bottomRowHeight = contentHeight - topRowHeight;
1876
+ content = /* @__PURE__ */ jsxs13(Fragment2, { children: [
1877
+ /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: topRowHeight, children: [
1878
+ /* @__PURE__ */ jsx14(Box12, { width: "30%", children: /* @__PURE__ */ jsx14(ProjectsPanel, { height: topRowHeight }) }),
1879
+ /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(PlanPanel, { height: topRowHeight }) })
1880
+ ] }),
1881
+ /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: bottomRowHeight, children: [
1882
+ /* @__PURE__ */ jsx14(Box12, { width: "30%", children: /* @__PURE__ */ jsx14(MachinesPanel, { height: bottomRowHeight }) }),
1883
+ /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: showChat ? /* @__PURE__ */ jsx14(ChatPanel, { height: bottomRowHeight }) : /* @__PURE__ */ jsx14(OutputPanel, { height: bottomRowHeight }) })
1884
+ ] })
1885
+ ] });
1886
+ }
1887
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1888
+ /* @__PURE__ */ jsx14(StatusBar, {}),
1889
+ content,
1890
+ /* @__PURE__ */ jsx14(SearchOverlay, {}),
1891
+ approvalOverlay,
1892
+ /* @__PURE__ */ jsx14(CommandLine, { height: bottomPanelHeight })
1138
1893
  ] });
1139
1894
  }
1140
1895
 
1141
1896
  // src/tui/hooks/use-vim-mode.ts
1142
1897
  import { useCallback, useRef } from "react";
1143
- import { useInput as useInput2, useApp } from "ink";
1898
+ import { useInput as useInput3, useApp } from "ink";
1144
1899
 
1145
1900
  // src/tui/lib/vim-state-machine.ts
1146
1901
  function initialVimState() {
@@ -1170,92 +1925,115 @@ function vimReducer(state, action) {
1170
1925
  { type: "search", value: state.searchQuery }
1171
1926
  ];
1172
1927
  case "key":
1173
- return handleKey(state, action.key, action.ctrl);
1928
+ return handleKey(state, action.key, action.ctrl, action.meta);
1174
1929
  }
1175
1930
  }
1176
- function handleKey(state, key, ctrl) {
1931
+ function handleKey(state, key, ctrl, meta) {
1177
1932
  if (key === "escape") {
1178
1933
  return [{ ...state, mode: "normal", pendingKeys: "", commandBuffer: "", searchQuery: "" }, { type: "none" }];
1179
1934
  }
1180
1935
  switch (state.mode) {
1181
1936
  case "normal":
1182
- return handleNormalMode(state, key, ctrl);
1183
- case "command":
1184
- return handleCommandMode(state, key);
1937
+ return handleNormalMode(state, key, ctrl, meta);
1938
+ case "palette":
1939
+ return handlePaletteMode(state, key);
1185
1940
  case "search":
1186
1941
  return handleSearchMode(state, key);
1187
- case "insert":
1188
- return handleInsertMode(state, key);
1942
+ case "input":
1943
+ return handleInputMode(state, key);
1189
1944
  }
1190
1945
  }
1191
- function handleNormalMode(state, key, ctrl) {
1946
+ function handleNormalMode(state, key, ctrl, meta) {
1192
1947
  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" }];
1948
+ switch (key) {
1949
+ case "p":
1950
+ return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
1951
+ case "f":
1952
+ return [state, { type: "search" }];
1953
+ case "c":
1954
+ return [state, { type: "quit" }];
1955
+ case "r":
1956
+ return [state, { type: "refresh" }];
1957
+ default:
1958
+ return [state, { type: "none" }];
1959
+ }
1197
1960
  }
1198
- if (state.pendingKeys === "g") {
1199
- if (key === "g") {
1200
- return [{ ...state, pendingKeys: "" }, { type: "scroll", direction: "top" }];
1961
+ if (meta) {
1962
+ switch (key) {
1963
+ case "x":
1964
+ return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
1965
+ default:
1966
+ return [state, { type: "none" }];
1201
1967
  }
1202
- return [{ ...state, pendingKeys: "" }, { type: "none" }];
1203
1968
  }
1204
1969
  switch (key) {
1205
- // Navigation
1970
+ // Arrow navigation (primary — no j/k needed)
1971
+ case "up":
1972
+ return [state, { type: "scroll", direction: "up" }];
1973
+ case "down":
1974
+ return [state, { type: "scroll", direction: "down" }];
1975
+ case "left":
1976
+ return [state, { type: "focus", direction: "left" }];
1977
+ case "right":
1978
+ return [state, { type: "focus", direction: "right" }];
1979
+ // Also keep j/k/h/l as secondary navigation for power users
1206
1980
  case "j":
1207
- case "return":
1208
- if (key === "j") return [state, { type: "scroll", direction: "down" }];
1209
- return [state, { type: "select" }];
1981
+ return [state, { type: "scroll", direction: "down" }];
1210
1982
  case "k":
1211
1983
  return [state, { type: "scroll", direction: "up" }];
1212
1984
  case "h":
1213
1985
  return [state, { type: "focus", direction: "left" }];
1214
1986
  case "l":
1215
1987
  return [state, { type: "focus", direction: "right" }];
1216
- // Panel jump
1988
+ // Selection
1989
+ case "return":
1990
+ return [state, { type: "select" }];
1991
+ case " ":
1992
+ return [state, { type: "select" }];
1993
+ // Page navigation
1994
+ case "pageup":
1995
+ return [state, { type: "scroll", direction: "page_up" }];
1996
+ case "pagedown":
1997
+ return [state, { type: "scroll", direction: "page_down" }];
1998
+ case "home":
1999
+ return [state, { type: "scroll", direction: "top" }];
2000
+ case "end":
2001
+ return [state, { type: "scroll", direction: "bottom" }];
2002
+ // Tab cycles panels
2003
+ case "tab":
2004
+ return [state, { type: "focus", direction: "right" }];
2005
+ // View switch by number
1217
2006
  case "1":
1218
- return [state, { type: "focus", panel: 0 }];
2007
+ return [state, { type: "view", value: "dashboard" }];
1219
2008
  case "2":
1220
- return [state, { type: "focus", panel: 1 }];
2009
+ return [state, { type: "view", value: "plan-gen" }];
1221
2010
  case "3":
1222
- return [state, { type: "focus", panel: 2 }];
2011
+ return [state, { type: "view", value: "projects" }];
1223
2012
  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
2013
+ return [state, { type: "view", value: "playground" }];
2014
+ case "5":
2015
+ return [state, { type: "view", value: "output" }];
2016
+ // Function-key style shortcuts (single letter, no prefix needed)
1240
2017
  case "d":
1241
2018
  return [state, { type: "dispatch" }];
1242
- case "c":
1243
- return [state, { type: "cancel" }];
1244
- case "r":
1245
- return [state, { type: "refresh" }];
1246
2019
  case "q":
1247
2020
  return [state, { type: "quit" }];
1248
2021
  case "?":
1249
2022
  return [state, { type: "help" }];
2023
+ case "/":
2024
+ return [state, { type: "search" }];
2025
+ // Legacy `:` still works — enters palette mode
2026
+ case ":":
2027
+ return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
1250
2028
  default:
1251
2029
  return [state, { type: "none" }];
1252
2030
  }
1253
2031
  }
1254
- function handleCommandMode(state, key) {
2032
+ function handlePaletteMode(state, key) {
1255
2033
  if (key === "return") {
1256
2034
  return [
1257
2035
  { ...state, mode: "normal" },
1258
- { type: "command", value: state.commandBuffer }
2036
+ { type: "command", value: "__palette_select__" }
1259
2037
  ];
1260
2038
  }
1261
2039
  if (key === "backspace" || key === "delete") {
@@ -1265,8 +2043,14 @@ function handleCommandMode(state, key) {
1265
2043
  }
1266
2044
  return [{ ...state, commandBuffer: newBuffer }, { type: "none" }];
1267
2045
  }
2046
+ if (key === "up") {
2047
+ return [state, { type: "scroll", direction: "up" }];
2048
+ }
2049
+ if (key === "down") {
2050
+ return [state, { type: "scroll", direction: "down" }];
2051
+ }
1268
2052
  if (key === "tab") {
1269
- return [state, { type: "command", value: `__autocomplete__${state.commandBuffer}` }];
2053
+ return [state, { type: "scroll", direction: "down" }];
1270
2054
  }
1271
2055
  if (key.length === 1) {
1272
2056
  return [{ ...state, commandBuffer: state.commandBuffer + key }, { type: "none" }];
@@ -1293,7 +2077,7 @@ function handleSearchMode(state, key) {
1293
2077
  }
1294
2078
  return [state, { type: "none" }];
1295
2079
  }
1296
- function handleInsertMode(state, _) {
2080
+ function handleInputMode(state, _) {
1297
2081
  return [state, { type: "none" }];
1298
2082
  }
1299
2083
 
@@ -1306,6 +2090,16 @@ function useVimMode(callbacks = {}) {
1306
2090
  (effect) => {
1307
2091
  switch (effect.type) {
1308
2092
  case "scroll":
2093
+ if (vimState.current.mode === "palette") {
2094
+ const filtered = getFilteredPaletteCommands(vimState.current.commandBuffer);
2095
+ const idx = store.paletteIndex;
2096
+ if (effect.direction === "up") {
2097
+ store.setPaletteIndex(Math.max(0, idx - 1));
2098
+ } else if (effect.direction === "down") {
2099
+ store.setPaletteIndex(Math.min(filtered.length - 1, idx + 1));
2100
+ }
2101
+ break;
2102
+ }
1309
2103
  switch (effect.direction) {
1310
2104
  case "up":
1311
2105
  store.scrollUp();
@@ -1336,19 +2130,32 @@ function useVimMode(callbacks = {}) {
1336
2130
  store.focusNext();
1337
2131
  }
1338
2132
  break;
1339
- case "select":
1340
- callbacks.onSelect?.();
2133
+ case "select": {
2134
+ const view = store.activeView;
2135
+ if (view === "playground" || view === "plan-gen" || view === "output") {
2136
+ vimState.current = { ...vimState.current, mode: "input" };
2137
+ store.setMode("input");
2138
+ } else {
2139
+ callbacks.onSelect?.();
2140
+ }
2141
+ break;
2142
+ }
2143
+ case "palette":
1341
2144
  break;
1342
2145
  case "command":
1343
- if (effect.value?.startsWith("__autocomplete__")) {
2146
+ if (effect.value === "__palette_select__") {
2147
+ const filtered = getFilteredPaletteCommands(vimState.current.commandBuffer);
2148
+ const selected = filtered[store.paletteIndex];
2149
+ if (selected) {
2150
+ callbacks.onCommand?.(selected.name);
2151
+ }
2152
+ } else if (effect.value?.startsWith("__autocomplete__")) {
1344
2153
  } else if (effect.value) {
1345
2154
  callbacks.onCommand?.(effect.value);
1346
2155
  }
1347
2156
  break;
1348
2157
  case "search":
1349
- if (effect.value) {
1350
- callbacks.onSearch?.(effect.value);
1351
- }
2158
+ useSearchStore.getState().open();
1352
2159
  break;
1353
2160
  case "dispatch":
1354
2161
  callbacks.onDispatch?.();
@@ -1361,19 +2168,30 @@ function useVimMode(callbacks = {}) {
1361
2168
  break;
1362
2169
  case "quit":
1363
2170
  exit();
2171
+ setTimeout(() => process.exit(0), 100);
1364
2172
  break;
1365
2173
  case "help":
1366
2174
  store.toggleHelp();
1367
2175
  break;
2176
+ case "chat":
2177
+ store.toggleChat();
2178
+ break;
2179
+ case "view":
2180
+ if (effect.value === "dashboard" || effect.value === "plan-gen" || effect.value === "projects" || effect.value === "playground" || effect.value === "output") {
2181
+ store.setActiveView(effect.value);
2182
+ }
2183
+ break;
1368
2184
  case "none":
1369
2185
  break;
1370
2186
  }
1371
2187
  },
1372
2188
  [store, callbacks, exit]
1373
2189
  );
1374
- useInput2((input, key) => {
1375
- if (store.showHelp || store.showSearch || store.showDetail) {
2190
+ useInput3((input, key) => {
2191
+ const searchOpen = useSearchStore.getState().isOpen;
2192
+ if (store.showHelp || store.showSearch || store.showDetail || searchOpen) {
1376
2193
  if (key.escape) {
2194
+ if (searchOpen) useSearchStore.getState().close();
1377
2195
  store.closeOverlays();
1378
2196
  vimState.current = initialVimState();
1379
2197
  store.setMode("normal");
@@ -1388,15 +2206,20 @@ function useVimMode(callbacks = {}) {
1388
2206
  else if (key.return) keyStr = "return";
1389
2207
  else if (key.backspace || key.delete) keyStr = "backspace";
1390
2208
  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";
2209
+ else if (key.upArrow) keyStr = "up";
2210
+ else if (key.downArrow) keyStr = "down";
2211
+ else if (key.leftArrow) keyStr = "left";
2212
+ else if (key.rightArrow) keyStr = "right";
2213
+ else if (key.pageUp) keyStr = "pageup";
2214
+ else if (key.pageDown) keyStr = "pagedown";
2215
+ else if (key.home) keyStr = "home";
2216
+ else if (key.end) keyStr = "end";
1395
2217
  const [nextState, effect] = vimReducer(vimState.current, {
1396
2218
  type: "key",
1397
2219
  key: keyStr,
1398
2220
  ctrl: key.ctrl,
1399
- shift: key.shift
2221
+ shift: key.shift,
2222
+ meta: key.meta
1400
2223
  });
1401
2224
  vimState.current = nextState;
1402
2225
  store.setMode(nextState.mode);
@@ -1439,13 +2262,23 @@ function usePolling(client, intervalMs = 3e4) {
1439
2262
  useMachinesStore.getState().setError(err instanceof Error ? err.message : String(err));
1440
2263
  }
1441
2264
  }, [client]);
2265
+ const loadUsage = useCallback2(async () => {
2266
+ try {
2267
+ const history = await client.getUsageHistory(1);
2268
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2269
+ const todayEntry = history.find((d) => d.date === today);
2270
+ useTuiStore.getState().setTodayCost(todayEntry?.totalCostUsd ?? 0);
2271
+ } catch {
2272
+ }
2273
+ }, [client]);
1442
2274
  const refreshAll2 = useCallback2(async () => {
1443
2275
  await Promise.allSettled([
1444
2276
  loadProjects(),
1445
2277
  loadMachines(),
2278
+ loadUsage(),
1446
2279
  ...selectedProjectId ? [loadPlan(selectedProjectId)] : []
1447
2280
  ]);
1448
- }, [loadProjects, loadMachines, loadPlan, selectedProjectId]);
2281
+ }, [loadProjects, loadMachines, loadUsage, loadPlan, selectedProjectId]);
1449
2282
  useEffect2(() => {
1450
2283
  refreshAll2();
1451
2284
  }, [refreshAll2]);
@@ -1603,367 +2436,135 @@ function useSSEStream(client) {
1603
2436
  useExecutionStore.getState().appendLine(taskId, `[progress] ${message}`);
1604
2437
  break;
1605
2438
  }
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: ")) {
2439
+ case "task:result": {
2440
+ const taskId = event.data.taskId;
2441
+ const status = event.data.status;
2442
+ useExecutionStore.getState().setStatus(taskId, status);
2443
+ const nodeId = event.data.nodeId;
2444
+ if (nodeId && status) {
2445
+ const mappedStatus = status === "success" ? "completed" : status === "failure" ? "planned" : status;
2446
+ usePlanStore.getState().updateNodeStatus(nodeId, mappedStatus);
2447
+ }
2448
+ break;
2449
+ }
2450
+ case "task:tool_trace": {
2451
+ const taskId = event.data.taskId;
2452
+ const toolName = event.data.toolName;
2453
+ useExecutionStore.getState().appendToolCall(taskId, toolName);
2454
+ break;
2455
+ }
2456
+ case "task:file_change": {
2457
+ const taskId = event.data.taskId;
2458
+ const path = event.data.path;
2459
+ const action = event.data.action;
2460
+ const added = event.data.linesAdded;
2461
+ const removed = event.data.linesRemoved;
2462
+ useExecutionStore.getState().appendFileChange(taskId, path, action, added, removed);
2463
+ break;
2464
+ }
2465
+ case "task:session_init": {
2466
+ const taskId = event.data.taskId;
2467
+ const nodeId = event.data.nodeId ?? taskId;
2468
+ useExecutionStore.getState().initExecution(taskId, nodeId);
2469
+ useExecutionStore.getState().setWatching(taskId);
2470
+ break;
2471
+ }
2472
+ case "task:plan_result": {
2473
+ const taskId = event.data.taskId;
2474
+ useExecutionStore.getState().appendLine(taskId, "[plan] Plan generated \u2014 refreshing...");
2475
+ const projectId = event.data.projectId ?? useTuiStore.getState().selectedProjectId;
2476
+ if (projectId) {
2477
+ setTimeout(async () => {
1833
2478
  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
- }
2479
+ const { nodes, edges } = await client.getPlan(projectId);
2480
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
1846
2481
  } catch {
1847
2482
  }
1848
- }
2483
+ }, 500);
1849
2484
  }
2485
+ break;
1850
2486
  }
2487
+ case "task:approval_request": {
2488
+ useExecutionStore.getState().setPendingApproval({
2489
+ requestId: event.data.requestId,
2490
+ question: event.data.question,
2491
+ options: event.data.options,
2492
+ machineId: event.data.machineId,
2493
+ taskId: event.data.taskId
2494
+ });
2495
+ break;
2496
+ }
2497
+ case "heartbeat":
2498
+ break;
1851
2499
  }
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>");
2500
+ };
2501
+ const sse = new SSEClient(client, handler);
2502
+ sseRef.current = sse;
2503
+ sse.start().catch(() => {
2504
+ });
2505
+ return () => {
2506
+ sse.stop();
2507
+ };
2508
+ }, [client, setConnected, setMachineCount, setLastError]);
2509
+ }
2510
+
2511
+ // src/tui/hooks/use-fuzzy-search.ts
2512
+ import { useEffect as useEffect4, useCallback as useCallback3 } from "react";
2513
+ import Fuse from "fuse.js";
2514
+ var fuseInstance = null;
2515
+ function useFuzzySearch() {
2516
+ const projects = useProjectsStore((s) => s.projects);
2517
+ const nodes = usePlanStore((s) => s.nodes);
2518
+ const machines = useMachinesStore((s) => s.machines);
2519
+ const { setItems, setResults, query } = useSearchStore();
2520
+ useEffect4(() => {
2521
+ const items = [
2522
+ ...projects.map((p) => ({
2523
+ type: "project",
2524
+ id: p.id,
2525
+ title: p.name,
2526
+ subtitle: p.description,
2527
+ status: p.status
2528
+ })),
2529
+ ...nodes.filter((n) => !n.deletedAt).map((n) => ({
2530
+ type: "task",
2531
+ id: n.id,
2532
+ title: n.title,
2533
+ subtitle: n.description,
2534
+ status: n.status
2535
+ })),
2536
+ ...machines.filter((m) => !m.isRevoked).map((m) => ({
2537
+ type: "machine",
2538
+ id: m.id,
2539
+ title: m.name,
2540
+ subtitle: `${m.platform} - ${m.hostname}`,
2541
+ status: m.isConnected ? "connected" : "disconnected"
2542
+ }))
2543
+ ];
2544
+ setItems(items);
2545
+ fuseInstance = new Fuse(items, {
2546
+ keys: ["title", "subtitle", "id"],
2547
+ threshold: 0.4,
2548
+ includeScore: true
2549
+ });
2550
+ }, [projects, nodes, machines, setItems]);
2551
+ const search = useCallback3((q) => {
2552
+ if (!q || !fuseInstance) {
2553
+ setResults([]);
1900
2554
  return;
1901
2555
  }
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
- }
2556
+ const results = fuseInstance.search(q, { limit: 20 });
2557
+ setResults(results.map((r) => r.item));
2558
+ }, [setResults]);
2559
+ useEffect4(() => {
2560
+ search(query);
2561
+ }, [query, search]);
2562
+ return { search };
1965
2563
  }
1966
2564
 
2565
+ // src/tui/hooks/use-command-parser.ts
2566
+ import { useCallback as useCallback4 } from "react";
2567
+
1967
2568
  // src/tui/commands/autocomplete.ts
1968
2569
  var PrefixTrie = class {
1969
2570
  root;
@@ -2052,7 +2653,7 @@ function useCommandParser(client) {
2052
2653
  }
2053
2654
 
2054
2655
  // src/tui/app.tsx
2055
- import { jsx as jsx13 } from "react/jsx-runtime";
2656
+ import { jsx as jsx15 } from "react/jsx-runtime";
2056
2657
  function App({ serverUrl }) {
2057
2658
  const client = useMemo(() => new AstroClient({ serverUrl }), [serverUrl]);
2058
2659
  const { refreshAll: refreshAll2 } = usePolling(client);
@@ -2071,12 +2672,12 @@ function App({ serverUrl }) {
2071
2672
  break;
2072
2673
  }
2073
2674
  case "plan": {
2074
- const treeLines = usePlanStore.getState().treeLines;
2675
+ const nodes = usePlanStore.getState().nodes.filter((n) => !n.deletedAt);
2075
2676
  const idx = scrollIndex.plan;
2076
- const line = treeLines[idx];
2077
- if (line) {
2078
- useTuiStore.getState().setSelectedNode(line.id);
2079
- usePlanStore.getState().toggleCollapse(line.id);
2677
+ const node = nodes[idx];
2678
+ if (node) {
2679
+ useTuiStore.getState().setSelectedNode(node.id);
2680
+ useTuiStore.getState().openDetail("node", node.id);
2080
2681
  }
2081
2682
  break;
2082
2683
  }
@@ -2101,6 +2702,16 @@ function App({ serverUrl }) {
2101
2702
  []
2102
2703
  );
2103
2704
  const onDispatch = useCallback5(async () => {
2705
+ const { focusedPanel, scrollIndex, selectedProjectId } = useTuiStore.getState();
2706
+ if (focusedPanel === "plan") {
2707
+ const nodes = usePlanStore.getState().nodes.filter((n) => !n.deletedAt);
2708
+ const node = nodes[scrollIndex.plan];
2709
+ if (node && selectedProjectId) {
2710
+ useTuiStore.getState().setSelectedNode(node.id);
2711
+ await execute(`dispatch ${node.id}`);
2712
+ return;
2713
+ }
2714
+ }
2104
2715
  const nodeId = useTuiStore.getState().selectedNodeId;
2105
2716
  const projectId = useTuiStore.getState().selectedProjectId;
2106
2717
  if (nodeId && projectId) {
@@ -2113,6 +2724,71 @@ function App({ serverUrl }) {
2113
2724
  const onRefresh = useCallback5(() => {
2114
2725
  refreshAll2();
2115
2726
  }, [refreshAll2]);
2727
+ const onSessionMessage = useCallback5(async (message) => {
2728
+ const { selectedProjectId, activeView } = useTuiStore.getState();
2729
+ if (!selectedProjectId) {
2730
+ useTuiStore.getState().setLastError("No project selected");
2731
+ return;
2732
+ }
2733
+ const watchingId = useExecutionStore.getState().watchingId;
2734
+ const sessionId = useChatStore.getState().sessionId;
2735
+ const messages = useChatStore.getState().messages.map((m) => ({
2736
+ role: m.role,
2737
+ content: m.content
2738
+ }));
2739
+ if (!watchingId) {
2740
+ if (activeView === "plan-gen") {
2741
+ await execute(`plan generate ${message}`);
2742
+ } else {
2743
+ await execute(`playground ${message}`);
2744
+ }
2745
+ return;
2746
+ }
2747
+ try {
2748
+ useChatStore.getState().setStreaming(true);
2749
+ const response = await client.projectChat({
2750
+ message,
2751
+ sessionId: sessionId ?? void 0,
2752
+ projectId: selectedProjectId,
2753
+ messages
2754
+ });
2755
+ if (!response.body) {
2756
+ useChatStore.getState().setStreaming(false);
2757
+ return;
2758
+ }
2759
+ const reader = response.body.getReader();
2760
+ const decoder = new TextDecoder();
2761
+ let buffer = "";
2762
+ while (true) {
2763
+ const { done, value } = await reader.read();
2764
+ if (done) break;
2765
+ buffer += decoder.decode(value, { stream: true });
2766
+ const lines = buffer.split("\n");
2767
+ buffer = lines.pop() ?? "";
2768
+ for (const line of lines) {
2769
+ if (!line.startsWith("data: ")) continue;
2770
+ const data = line.slice(6);
2771
+ if (data === "[DONE]") continue;
2772
+ try {
2773
+ const event = JSON.parse(data);
2774
+ if (event.type === "text" && event.text) {
2775
+ useChatStore.getState().appendStream(event.text);
2776
+ useExecutionStore.getState().appendText(watchingId, event.text);
2777
+ } else if (event.type === "session_init" && event.sessionId) {
2778
+ useChatStore.getState().setSessionId(event.sessionId);
2779
+ } else if (event.type === "done") {
2780
+ }
2781
+ } catch {
2782
+ }
2783
+ }
2784
+ }
2785
+ useChatStore.getState().flushStream();
2786
+ useChatStore.getState().setStreaming(false);
2787
+ } catch (err) {
2788
+ useChatStore.getState().setStreaming(false);
2789
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
2790
+ }
2791
+ }, [client, execute]);
2116
2792
  useVimMode({
2117
2793
  onSelect,
2118
2794
  onCommand,
@@ -2121,13 +2797,13 @@ function App({ serverUrl }) {
2121
2797
  onCancel,
2122
2798
  onRefresh
2123
2799
  });
2124
- return /* @__PURE__ */ jsx13(MainLayout, {});
2800
+ return /* @__PURE__ */ jsx15(MainLayout, { onSessionMessage });
2125
2801
  }
2126
2802
 
2127
2803
  // src/tui/index.tsx
2128
- import { jsx as jsx14 } from "react/jsx-runtime";
2804
+ import { jsx as jsx16 } from "react/jsx-runtime";
2129
2805
  async function launchTui(serverUrl) {
2130
- render(/* @__PURE__ */ jsx14(App, { serverUrl }), {
2806
+ render(/* @__PURE__ */ jsx16(App, { serverUrl }), {
2131
2807
  exitOnCtrlC: true
2132
2808
  });
2133
2809
  }