@astroanywhere/cli 0.2.0 → 0.2.2

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,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- AstroClient
4
- } from "./chunk-7H7WD7QX.js";
3
+ AstroClient,
4
+ readSSEStream
5
+ } from "./chunk-MJRAJPBU.js";
5
6
 
6
7
  // src/tui/index.tsx
7
8
  import "react";
8
9
  import { render } from "ink";
9
10
 
10
11
  // src/tui/app.tsx
11
- import { useMemo, useCallback as useCallback5 } from "react";
12
+ import { useMemo as useMemo2, useCallback as useCallback5, useEffect as useEffect7 } from "react";
12
13
 
13
14
  // src/tui/components/layout/main-layout.tsx
14
15
  import "react";
15
- import { Box as Box10, useStdout } from "ink";
16
+ import { Box as Box13, useStdout as useStdout3 } from "ink";
16
17
 
17
18
  // src/tui/components/layout/status-bar.tsx
18
19
  import "react";
@@ -20,7 +21,7 @@ import { Box, Text } from "ink";
20
21
 
21
22
  // src/tui/stores/tui-store.ts
22
23
  import { create } from "zustand";
23
- var PANEL_ORDER = ["projects", "plan", "machines", "output"];
24
+ var PANEL_ORDER = ["projects", "plan", "machines", "output", "chat"];
24
25
  var useTuiStore = create((set, get) => ({
25
26
  mode: "normal",
26
27
  commandBuffer: "",
@@ -32,18 +33,21 @@ var useTuiStore = create((set, get) => ({
32
33
  selectedNodeId: null,
33
34
  selectedMachineId: null,
34
35
  selectedExecutionId: null,
35
- scrollIndex: { projects: 0, plan: 0, machines: 0, output: 0 },
36
+ scrollIndex: { projects: 0, plan: 0, machines: 0, output: 0, chat: 0 },
36
37
  showHelp: false,
37
38
  showSearch: false,
38
39
  showDetail: false,
40
+ showChat: false,
39
41
  detailType: null,
40
42
  detailId: null,
41
43
  connected: false,
42
44
  machineCount: 0,
43
45
  todayCost: 0,
46
+ activeView: "dashboard",
47
+ paletteIndex: 0,
44
48
  lastError: null,
45
49
  setMode: (mode) => set({ mode }),
46
- setCommandBuffer: (commandBuffer) => set({ commandBuffer }),
50
+ setCommandBuffer: (commandBuffer) => set({ commandBuffer, paletteIndex: 0 }),
47
51
  setSearchQuery: (searchQuery) => set({ searchQuery }),
48
52
  setPendingKeys: (pendingKeys) => set({ pendingKeys }),
49
53
  focusPanel: (panel) => set({ focusedPanel: panel }),
@@ -113,12 +117,15 @@ var useTuiStore = create((set, get) => ({
113
117
  },
114
118
  toggleHelp: () => set((s) => ({ showHelp: !s.showHelp, showSearch: false })),
115
119
  toggleSearch: () => set((s) => ({ showSearch: !s.showSearch, showHelp: false })),
120
+ toggleChat: () => set((s) => ({ showChat: !s.showChat })),
116
121
  openDetail: (type, id) => set({ showDetail: true, detailType: type, detailId: id, showHelp: false, showSearch: false }),
117
122
  closeDetail: () => set({ showDetail: false, detailType: null, detailId: null }),
118
- closeOverlays: () => set({ showHelp: false, showSearch: false, showDetail: false, detailType: null, detailId: null }),
123
+ closeOverlays: () => set({ showHelp: false, showSearch: false, showDetail: false, showChat: false, detailType: null, detailId: null }),
119
124
  setConnected: (connected) => set({ connected }),
120
125
  setMachineCount: (machineCount) => set({ machineCount }),
121
126
  setTodayCost: (todayCost) => set({ todayCost }),
127
+ setActiveView: (activeView) => set({ activeView }),
128
+ setPaletteIndex: (paletteIndex) => set({ paletteIndex }),
122
129
  setLastError: (lastError) => set({ lastError })
123
130
  }));
124
131
 
@@ -143,159 +150,56 @@ function formatCost(usd) {
143
150
  return `$${usd.toFixed(2)}`;
144
151
  }
145
152
  function truncate(str, maxLen) {
146
- if (str.length <= maxLen) return str;
147
- return str.slice(0, maxLen - 1) + "\u2026";
153
+ const clean = str.replace(/[\r\n]+/g, " ").trim();
154
+ if (clean.length <= maxLen) return clean;
155
+ return clean.slice(0, maxLen - 1) + "\u2026";
156
+ }
157
+ function getVisibleProjects(projects) {
158
+ return projects.filter((p) => p.projectType !== "playground").sort((a, b) => {
159
+ const ta = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
160
+ const tb = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
161
+ return tb - ta;
162
+ });
148
163
  }
149
164
 
150
165
  // src/tui/components/layout/status-bar.tsx
151
166
  import { jsx, jsxs } from "react/jsx-runtime";
167
+ var VIEW_LABELS = {
168
+ dashboard: "Dashboard",
169
+ "plan-gen": "Plan",
170
+ projects: "Projects",
171
+ playground: "Playground",
172
+ active: "Active"
173
+ };
152
174
  function StatusBar() {
153
175
  const connected = useTuiStore((s) => s.connected);
154
176
  const machineCount = useTuiStore((s) => s.machineCount);
155
177
  const todayCost = useTuiStore((s) => s.todayCost);
156
178
  const lastError = useTuiStore((s) => s.lastError);
179
+ const activeView = useTuiStore((s) => s.activeView);
157
180
  return /* @__PURE__ */ jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [
158
181
  /* @__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" }),
182
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Astro" }),
183
+ /* @__PURE__ */ jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25CF" : "\u25CB" }),
161
184
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
162
185
  machineCount,
163
186
  " machine",
164
187
  machineCount !== 1 ? "s" : ""
165
188
  ] }),
166
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
167
- formatCost(todayCost),
168
- " today"
189
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: formatCost(todayCost) }),
190
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
191
+ "[",
192
+ VIEW_LABELS[activeView],
193
+ "]"
169
194
  ] })
170
195
  ] }),
171
- lastError && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "red", children: lastError.length > 60 ? lastError.slice(0, 57) + "..." : lastError }) })
196
+ lastError && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "red", children: lastError.length > 50 ? lastError.slice(0, 47) + "..." : lastError }) })
172
197
  ] });
173
198
  }
174
199
 
175
200
  // src/tui/components/layout/command-line.tsx
176
201
  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";
202
+ import { Box as Box2, Text as Text2, useStdout } from "ink";
299
203
 
300
204
  // src/tui/stores/search-store.ts
301
205
  import { create as create2 } from "zustand";
@@ -321,6 +225,20 @@ var useSearchStore = create2((set, get) => ({
321
225
  close: () => set({ isOpen: false, query: "", results: [], selectedIndex: 0 })
322
226
  }));
323
227
 
228
+ // src/tui/stores/projects-store.ts
229
+ import { create as create3 } from "zustand";
230
+ var useProjectsStore = create3((set) => ({
231
+ projects: [],
232
+ loading: false,
233
+ error: null,
234
+ setProjects: (projects) => set({ projects, loading: false, error: null }),
235
+ setLoading: (loading) => set({ loading }),
236
+ setError: (error) => set({ error, loading: false })
237
+ }));
238
+
239
+ // src/tui/stores/plan-store.ts
240
+ import { create as create4 } from "zustand";
241
+
324
242
  // src/tui/lib/status-colors.ts
325
243
  var STATUS_COLOR_MAP = {
326
244
  // Project statuses
@@ -386,248 +304,6 @@ function getStatusSymbol(status) {
386
304
  }
387
305
  }
388
306
 
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
307
  // src/tui/lib/tree-builder.ts
632
308
  function buildTree(nodes, edges) {
633
309
  const adj = /* @__PURE__ */ new Map();
@@ -696,6 +372,11 @@ function renderTreeLines(roots, collapsedSet) {
696
372
  }
697
373
 
698
374
  // src/tui/stores/plan-store.ts
375
+ function buildView(nodes, edges, collapsedNodes) {
376
+ const treeRoots = buildTree(nodes, edges);
377
+ const treeLines = renderTreeLines(treeRoots, collapsedNodes);
378
+ return { treeRoots, treeLines };
379
+ }
699
380
  var usePlanStore = create4((set, get) => ({
700
381
  projectId: null,
701
382
  nodes: [],
@@ -705,18 +386,64 @@ var usePlanStore = create4((set, get) => ({
705
386
  collapsedNodes: /* @__PURE__ */ new Set(),
706
387
  loading: false,
707
388
  error: null,
389
+ cache: /* @__PURE__ */ new Map(),
708
390
  setPlan: (projectId, nodes, edges) => {
709
- const { collapsedNodes } = get();
710
- const treeRoots = buildTree(nodes, edges);
711
- const treeLines = renderTreeLines(treeRoots, collapsedNodes);
712
- set({ projectId, nodes, edges, treeRoots, treeLines, loading: false, error: null });
391
+ const { collapsedNodes, cache } = get();
392
+ const next = new Map(cache);
393
+ next.set(projectId, { nodes, edges });
394
+ const { treeRoots, treeLines } = buildView(nodes, edges, collapsedNodes);
395
+ set({ projectId, nodes, edges, treeRoots, treeLines, loading: false, error: null, cache: next });
713
396
  },
714
- setLoading: (loading) => set({ loading }),
715
- setError: (error) => set({ error, loading: false }),
716
- toggleCollapse: (nodeId) => {
717
- const { collapsedNodes, treeRoots } = get();
718
- const next = new Set(collapsedNodes);
719
- if (next.has(nodeId)) {
397
+ setAllPlans: (allNodes, allEdges) => {
398
+ const { projectId, collapsedNodes } = get();
399
+ const next = /* @__PURE__ */ new Map();
400
+ const nodesByProject = /* @__PURE__ */ new Map();
401
+ for (const node of allNodes) {
402
+ const list = nodesByProject.get(node.projectId) ?? [];
403
+ list.push(node);
404
+ nodesByProject.set(node.projectId, list);
405
+ }
406
+ const nodeProjectMap = /* @__PURE__ */ new Map();
407
+ for (const node of allNodes) {
408
+ nodeProjectMap.set(node.id, node.projectId);
409
+ }
410
+ const edgesByProject = /* @__PURE__ */ new Map();
411
+ for (const edge of allEdges) {
412
+ const pid = nodeProjectMap.get(edge.source) ?? nodeProjectMap.get(edge.target);
413
+ if (pid) {
414
+ const list = edgesByProject.get(pid) ?? [];
415
+ list.push(edge);
416
+ edgesByProject.set(pid, list);
417
+ }
418
+ }
419
+ for (const [pid, nodes] of nodesByProject) {
420
+ next.set(pid, { nodes, edges: edgesByProject.get(pid) ?? [] });
421
+ }
422
+ const update = { cache: next, loading: false, error: null };
423
+ if (projectId && next.has(projectId)) {
424
+ const cached = next.get(projectId);
425
+ const { treeRoots, treeLines } = buildView(cached.nodes, cached.edges, collapsedNodes);
426
+ Object.assign(update, { nodes: cached.nodes, edges: cached.edges, treeRoots, treeLines });
427
+ }
428
+ set(update);
429
+ },
430
+ selectProject: (projectId) => {
431
+ const { cache, collapsedNodes, projectId: currentProjectId } = get();
432
+ if (projectId === currentProjectId) return;
433
+ const cached = cache.get(projectId);
434
+ if (cached) {
435
+ const { treeRoots, treeLines } = buildView(cached.nodes, cached.edges, collapsedNodes);
436
+ set({ projectId, nodes: cached.nodes, edges: cached.edges, treeRoots, treeLines, loading: false, error: null });
437
+ } else {
438
+ set({ projectId, nodes: [], edges: [], treeRoots: [], treeLines: [], loading: false, error: null });
439
+ }
440
+ },
441
+ setLoading: (loading) => set({ loading }),
442
+ setError: (error) => set({ error, loading: false }),
443
+ toggleCollapse: (nodeId) => {
444
+ const { collapsedNodes, treeRoots } = get();
445
+ const next = new Set(collapsedNodes);
446
+ if (next.has(nodeId)) {
720
447
  next.delete(nodeId);
721
448
  } else {
722
449
  next.add(nodeId);
@@ -725,11 +452,16 @@ var usePlanStore = create4((set, get) => ({
725
452
  set({ collapsedNodes: next, treeLines });
726
453
  },
727
454
  updateNodeStatus: (nodeId, status) => {
728
- const { nodes, edges, collapsedNodes } = get();
455
+ const { nodes, edges, collapsedNodes, projectId, cache } = get();
729
456
  const updated = nodes.map((n) => n.id === nodeId ? { ...n, status } : n);
730
- const treeRoots = buildTree(updated, edges);
731
- const treeLines = renderTreeLines(treeRoots, collapsedNodes);
732
- set({ nodes: updated, treeRoots, treeLines });
457
+ const { treeRoots, treeLines } = buildView(updated, edges, collapsedNodes);
458
+ if (projectId) {
459
+ const next = new Map(cache);
460
+ next.set(projectId, { nodes: updated, edges });
461
+ set({ nodes: updated, treeRoots, treeLines, cache: next });
462
+ } else {
463
+ set({ nodes: updated, treeRoots, treeLines });
464
+ }
733
465
  },
734
466
  clear: () => set({
735
467
  projectId: null,
@@ -742,54 +474,6 @@ var usePlanStore = create4((set, get) => ({
742
474
  })
743
475
  }));
744
476
 
745
- // src/tui/components/panels/plan-panel.tsx
746
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
747
- function PlanPanel({ height }) {
748
- const treeLines = usePlanStore((s) => s.treeLines);
749
- const loading = usePlanStore((s) => s.loading);
750
- const error = usePlanStore((s) => s.error);
751
- const focusedPanel = useTuiStore((s) => s.focusedPanel);
752
- const scrollIndex = useTuiStore((s) => s.scrollIndex.plan);
753
- const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
754
- const projects = useProjectsStore((s) => s.projects);
755
- const isFocused = focusedPanel === "plan";
756
- const projectName = projects.find((p) => p.id === selectedProjectId)?.name ?? "none";
757
- const visibleHeight = Math.max(1, height - 4);
758
- let start = 0;
759
- if (scrollIndex >= visibleHeight) {
760
- start = scrollIndex - visibleHeight + 1;
761
- }
762
- const visibleLines = treeLines.slice(start, start + visibleHeight);
763
- return /* @__PURE__ */ jsx8(Panel, { title: `PLAN (${projectName})`, isFocused, height, children: loading && treeLines.length === 0 ? /* @__PURE__ */ jsx8(Spinner, { label: "Loading plan..." }) : error ? /* @__PURE__ */ jsx8(Text9, { color: "red", children: error }) : !selectedProjectId ? /* @__PURE__ */ jsx8(Text9, { dimColor: true, children: " Select a project to view its plan" }) : treeLines.length === 0 ? /* @__PURE__ */ jsx8(Text9, { dimColor: true, children: " No plan nodes. Use :plan create-node <title>" }) : /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
764
- visibleLines.map((line, i) => {
765
- const actualIndex = start + i;
766
- const isSelected = actualIndex === scrollIndex && isFocused;
767
- return /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsx8(
768
- Text9,
769
- {
770
- color: isSelected ? "cyan" : getStatusColor(line.status),
771
- bold: isSelected,
772
- inverse: isSelected,
773
- children: line.text
774
- }
775
- ) }, line.id + "-" + actualIndex);
776
- }),
777
- treeLines.length > visibleHeight && /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
778
- " [",
779
- start + 1,
780
- "-",
781
- Math.min(start + visibleHeight, treeLines.length),
782
- "/",
783
- treeLines.length,
784
- "]"
785
- ] })
786
- ] }) });
787
- }
788
-
789
- // src/tui/components/panels/machines-panel.tsx
790
- import "react";
791
- import { Text as Text10 } from "ink";
792
-
793
477
  // src/tui/stores/machines-store.ts
794
478
  import { create as create5 } from "zustand";
795
479
  var useMachinesStore = create5((set, get) => ({
@@ -818,38 +502,6 @@ var useMachinesStore = create5((set, get) => ({
818
502
  setError: (error) => set({ error, loading: false })
819
503
  }));
820
504
 
821
- // src/tui/components/panels/machines-panel.tsx
822
- import { jsx as jsx9 } from "react/jsx-runtime";
823
- function MachinesPanel({ height }) {
824
- const machines = useMachinesStore((s) => s.machines);
825
- const loading = useMachinesStore((s) => s.loading);
826
- const error = useMachinesStore((s) => s.error);
827
- const focusedPanel = useTuiStore((s) => s.focusedPanel);
828
- const scrollIndex = useTuiStore((s) => s.scrollIndex.machines);
829
- const isFocused = focusedPanel === "machines";
830
- const activeMachines = machines.filter((m) => !m.isRevoked);
831
- const items = activeMachines.map((m) => ({
832
- id: m.id,
833
- label: m.name,
834
- sublabel: m.platform,
835
- rightLabel: m.isConnected ? "\u25CF online" : "\u25CB offline",
836
- color: m.isConnected ? "green" : "gray"
837
- }));
838
- return /* @__PURE__ */ jsx9(Panel, { title: "MACHINES", isFocused, height, children: loading && machines.length === 0 ? /* @__PURE__ */ jsx9(Spinner, { label: "Loading machines..." }) : error ? /* @__PURE__ */ jsx9(Text10, { color: "red", children: error }) : /* @__PURE__ */ jsx9(
839
- ScrollableList,
840
- {
841
- items,
842
- selectedIndex: scrollIndex,
843
- height: height - 3,
844
- isFocused
845
- }
846
- ) });
847
- }
848
-
849
- // src/tui/components/panels/output-panel.tsx
850
- import "react";
851
- import { Box as Box8, Text as Text11 } from "ink";
852
-
853
505
  // src/tui/stores/execution-store.ts
854
506
  import { create as create6 } from "zustand";
855
507
  var MAX_LINES = 5e3;
@@ -870,12 +522,14 @@ function trimRingBuffer(lines) {
870
522
  var useExecutionStore = create6((set, get) => ({
871
523
  outputs: /* @__PURE__ */ new Map(),
872
524
  watchingId: null,
873
- initExecution: (executionId, nodeId) => {
525
+ pendingApproval: null,
526
+ initExecution: (executionId, nodeId, title) => {
874
527
  const { outputs } = get();
875
528
  const next = new Map(outputs);
876
529
  next.set(executionId, {
877
530
  executionId,
878
531
  nodeId,
532
+ title: title ?? nodeId,
879
533
  lines: [],
880
534
  status: "running",
881
535
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -883,6 +537,23 @@ var useExecutionStore = create6((set, get) => ({
883
537
  });
884
538
  set({ outputs: next });
885
539
  },
540
+ seedHistorical: (entries) => {
541
+ const { outputs } = get();
542
+ const next = new Map(outputs);
543
+ for (const entry of entries) {
544
+ if (next.has(entry.executionId)) continue;
545
+ next.set(entry.executionId, {
546
+ executionId: entry.executionId,
547
+ nodeId: entry.nodeId,
548
+ title: entry.title,
549
+ lines: [],
550
+ status: entry.status,
551
+ startedAt: entry.startedAt,
552
+ pendingToolCount: 0
553
+ });
554
+ }
555
+ set({ outputs: next });
556
+ },
886
557
  appendToolCall: (executionId) => {
887
558
  const { outputs } = get();
888
559
  const current = outputs.get(executionId);
@@ -946,6 +617,7 @@ var useExecutionStore = create6((set, get) => ({
946
617
  set({ outputs: next });
947
618
  },
948
619
  setWatching: (watchingId) => set({ watchingId }),
620
+ setPendingApproval: (pendingApproval) => set({ pendingApproval }),
949
621
  clear: (executionId) => {
950
622
  const { outputs } = get();
951
623
  const next = new Map(outputs);
@@ -954,8 +626,1222 @@ var useExecutionStore = create6((set, get) => ({
954
626
  }
955
627
  }));
956
628
 
629
+ // src/tui/stores/chat-store.ts
630
+ import { create as create7 } from "zustand";
631
+ var useChatStore = create7((set, get) => ({
632
+ messages: [],
633
+ sessionId: null,
634
+ projectId: null,
635
+ nodeId: null,
636
+ streaming: false,
637
+ streamBuffer: "",
638
+ addMessage: (role, content) => {
639
+ set((s) => ({
640
+ messages: [...s.messages, { role, content, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
641
+ }));
642
+ },
643
+ appendStream: (text) => {
644
+ set((s) => ({ streamBuffer: s.streamBuffer + text }));
645
+ },
646
+ flushStream: () => {
647
+ const { streamBuffer } = get();
648
+ if (streamBuffer.length > 0) {
649
+ set((s) => ({
650
+ messages: [...s.messages, { role: "assistant", content: streamBuffer, timestamp: (/* @__PURE__ */ new Date()).toISOString() }],
651
+ streamBuffer: ""
652
+ }));
653
+ }
654
+ },
655
+ setSessionId: (sessionId) => set({ sessionId }),
656
+ setContext: (projectId, nodeId) => set({ projectId, nodeId: nodeId ?? null }),
657
+ setStreaming: (streaming) => set({ streaming }),
658
+ clear: () => set({ messages: [], sessionId: null, streamBuffer: "", streaming: false })
659
+ }));
660
+
661
+ // src/tui/stores/session-settings-store.ts
662
+ import { create as create8 } from "zustand";
663
+ var useSessionSettingsStore = create8((set) => ({
664
+ machineId: null,
665
+ machineName: null,
666
+ workingDirectory: process.cwd(),
667
+ focusedField: null,
668
+ pickerOpen: false,
669
+ setMachine: (id, name) => set({ machineId: id, machineName: name }),
670
+ setWorkingDirectory: (workingDirectory) => set({ workingDirectory }),
671
+ setFocusedField: (focusedField) => set({ focusedField, pickerOpen: false }),
672
+ setPickerOpen: (pickerOpen) => set({ pickerOpen }),
673
+ init: (machineId, machineName, workingDirectory) => set({ machineId, machineName, workingDirectory })
674
+ }));
675
+
676
+ // src/tui/commands/handlers.ts
677
+ var PALETTE_COMMANDS = [
678
+ { name: "project list", description: "List all projects" },
679
+ { name: "project show", description: "Show project details", usage: "project show <id>" },
680
+ { name: "project create", description: "Create a new project", usage: "project create <name>" },
681
+ { name: "project delete", description: "Delete a project", usage: "project delete <id>" },
682
+ { name: "plan tree", description: "Show plan tree for selected project" },
683
+ { name: "plan create-node", description: "Create a plan node", usage: "plan create-node <title>" },
684
+ { name: "plan update-node", description: "Update a plan node field", usage: "plan update-node <id> <field> <value>" },
685
+ { name: "dispatch", description: "Dispatch selected task for execution", usage: "dispatch [nodeId]" },
686
+ { name: "cancel", description: "Cancel running execution", usage: "cancel [executionId]" },
687
+ { name: "steer", description: "Send guidance to running task", usage: "steer <message>" },
688
+ { name: "watch", description: "Watch execution output", usage: "watch <executionId>" },
689
+ { name: "env list", description: "List machines/environments" },
690
+ { name: "env status", description: "Show relay status" },
691
+ { name: "search", description: "Search projects and tasks", usage: "search <query>" },
692
+ { name: "activity", description: "Show recent activity feed" },
693
+ { name: "playground", description: "Start a playground (Cloud Code) session", usage: "playground <description>" },
694
+ { name: "plan generate", description: "Generate a plan using AI", usage: "plan generate <description>" },
695
+ { name: "project chat", description: "Chat with AI about the selected project", usage: "project chat <message>" },
696
+ { name: "task chat", description: "Chat with AI about the selected task", usage: "task chat <message>" },
697
+ { name: "summarize", description: "AI-generated summary of an execution", usage: "summarize [executionId]" },
698
+ { name: "refresh", description: "Refresh all data" },
699
+ { name: "help", description: "Toggle keybinding reference" },
700
+ { name: "quit", description: "Exit the TUI" }
701
+ ];
702
+ var handlers = {
703
+ // ── Quit ──
704
+ q: async () => {
705
+ process.exit(0);
706
+ },
707
+ quit: async () => {
708
+ process.exit(0);
709
+ },
710
+ // ── Refresh ──
711
+ r: async (_args, client) => {
712
+ await refreshAll(client);
713
+ },
714
+ refresh: async (_args, client) => {
715
+ await refreshAll(client);
716
+ },
717
+ // ── Project commands ──
718
+ "project list": async (_args, client) => {
719
+ const projects = await client.listProjects();
720
+ useProjectsStore.getState().setProjects(projects);
721
+ },
722
+ "project show": async (args, client) => {
723
+ const id = args[0];
724
+ if (!id) return;
725
+ try {
726
+ const project = await client.resolveProject(id);
727
+ useTuiStore.getState().openDetail("project", project.id);
728
+ } catch {
729
+ useTuiStore.getState().setLastError(`Project not found: ${id}`);
730
+ }
731
+ },
732
+ "project create": async (args, client) => {
733
+ const name = args.join(" ");
734
+ if (!name) {
735
+ useTuiStore.getState().setLastError("Usage: :project create <name>");
736
+ return;
737
+ }
738
+ await client.createProject({ name });
739
+ const projects = await client.listProjects();
740
+ useProjectsStore.getState().setProjects(projects);
741
+ },
742
+ "project delete": async (args, client) => {
743
+ const id = args[0];
744
+ if (!id) return;
745
+ try {
746
+ const project = await client.resolveProject(id);
747
+ await client.deleteProject(project.id);
748
+ const projects = await client.listProjects();
749
+ useProjectsStore.getState().setProjects(projects);
750
+ } catch (err) {
751
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
752
+ }
753
+ },
754
+ // ── Plan commands ──
755
+ "plan tree": async (_args, client) => {
756
+ const projectId = useTuiStore.getState().selectedProjectId;
757
+ if (!projectId) {
758
+ useTuiStore.getState().setLastError("No project selected");
759
+ return;
760
+ }
761
+ const { nodes, edges } = await client.getPlan(projectId);
762
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
763
+ useTuiStore.getState().focusPanel("plan");
764
+ },
765
+ "plan create-node": async (args, client) => {
766
+ const projectId = useTuiStore.getState().selectedProjectId;
767
+ if (!projectId) {
768
+ useTuiStore.getState().setLastError("No project selected");
769
+ return;
770
+ }
771
+ const title = args.join(" ");
772
+ if (!title) {
773
+ useTuiStore.getState().setLastError("Usage: :plan create-node <title>");
774
+ return;
775
+ }
776
+ const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
777
+ await client.createPlanNode({ id, projectId, title });
778
+ const { nodes, edges } = await client.getPlan(projectId);
779
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
780
+ },
781
+ "plan update-node": async (args, client) => {
782
+ const [nodeId, field, ...rest] = args;
783
+ if (!nodeId || !field) {
784
+ useTuiStore.getState().setLastError("Usage: :plan update-node <nodeId> <field> <value>");
785
+ return;
786
+ }
787
+ const value = rest.join(" ");
788
+ await client.updatePlanNode(nodeId, { [field]: value });
789
+ const projectId = useTuiStore.getState().selectedProjectId;
790
+ if (projectId) {
791
+ const { nodes, edges } = await client.getPlan(projectId);
792
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
793
+ }
794
+ },
795
+ // ── Dispatch ──
796
+ d: async (args, client) => {
797
+ await handlers.dispatch(args, client);
798
+ },
799
+ dispatch: async (args, client) => {
800
+ const nodeId = args[0] ?? useTuiStore.getState().selectedNodeId;
801
+ const projectId = useTuiStore.getState().selectedProjectId;
802
+ if (!nodeId || !projectId) {
803
+ useTuiStore.getState().setLastError("No node/project selected for dispatch");
804
+ return;
805
+ }
806
+ const machines = useMachinesStore.getState().machines;
807
+ const connectedMachine = machines.find((m) => m.isConnected);
808
+ if (!connectedMachine) {
809
+ useTuiStore.getState().setLastError("No connected machines. Start an agent runner first.");
810
+ return;
811
+ }
812
+ const execId = `exec-${Date.now()}`;
813
+ const planNode = usePlanStore.getState().nodes.find((n) => n.id === nodeId);
814
+ const title = planNode?.title ?? nodeId;
815
+ useExecutionStore.getState().initExecution(execId, nodeId, title);
816
+ useExecutionStore.getState().setWatching(execId);
817
+ useTuiStore.getState().focusPanel("output");
818
+ useExecutionStore.getState().appendLine(execId, `[progress] Dispatching task...`);
819
+ try {
820
+ const response = await client.dispatchTask({
821
+ nodeId,
822
+ projectId,
823
+ targetMachineId: connectedMachine.id
824
+ });
825
+ await streamSSEToExecution(response, execId, client, projectId);
826
+ } catch (err) {
827
+ useExecutionStore.getState().setStatus(execId, "error");
828
+ useExecutionStore.getState().appendLine(execId, `[error] ${err instanceof Error ? err.message : String(err)}`);
829
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
830
+ }
831
+ },
832
+ // ── Cancel ──
833
+ c: async (args, client) => {
834
+ await handlers.cancel(args, client);
835
+ },
836
+ cancel: async (args, client) => {
837
+ const executionId = args[0] ?? useExecutionStore.getState().watchingId;
838
+ if (!executionId) {
839
+ useTuiStore.getState().setLastError("No execution to cancel");
840
+ return;
841
+ }
842
+ try {
843
+ await client.cancelTask({ executionId });
844
+ useExecutionStore.getState().setStatus(executionId, "cancelled");
845
+ } catch (err) {
846
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
847
+ }
848
+ },
849
+ // ── Steer ──
850
+ s: async (args, client) => {
851
+ await handlers.steer(args, client);
852
+ },
853
+ steer: async (args, client) => {
854
+ const message = args.join(" ");
855
+ if (!message) {
856
+ useTuiStore.getState().setLastError("Usage: :steer <message>");
857
+ return;
858
+ }
859
+ const executionId = useExecutionStore.getState().watchingId;
860
+ const selectedMachineId = useTuiStore.getState().selectedMachineId;
861
+ if (!executionId || !selectedMachineId) {
862
+ useTuiStore.getState().setLastError("No active execution/machine to steer");
863
+ return;
864
+ }
865
+ try {
866
+ await client.steerTask({ taskId: executionId, machineId: selectedMachineId, message });
867
+ } catch (err) {
868
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
869
+ }
870
+ },
871
+ // ── Watch ──
872
+ watch: async (args) => {
873
+ const executionId = args[0];
874
+ if (!executionId) {
875
+ useTuiStore.getState().setLastError("Usage: :watch <executionId>");
876
+ return;
877
+ }
878
+ useExecutionStore.getState().setWatching(executionId);
879
+ useTuiStore.getState().focusPanel("output");
880
+ },
881
+ // ── Env ──
882
+ "env list": async (_args, client) => {
883
+ const machines = await client.listMachines();
884
+ useMachinesStore.getState().setMachines(machines);
885
+ useTuiStore.getState().focusPanel("machines");
886
+ },
887
+ "env status": async (_args, client) => {
888
+ const status = await client.getRelayStatus();
889
+ useTuiStore.getState().setLastError(JSON.stringify(status, null, 2));
890
+ },
891
+ // ── Search ──
892
+ search: async (args, client) => {
893
+ const query = args.join(" ");
894
+ if (!query) return;
895
+ try {
896
+ await client.search(query);
897
+ useTuiStore.getState().toggleSearch();
898
+ } catch (err) {
899
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
900
+ }
901
+ },
902
+ // ── Activity ──
903
+ activity: async (_args, client) => {
904
+ const projectId = useTuiStore.getState().selectedProjectId;
905
+ try {
906
+ const activities = await client.listActivities(projectId ? { projectId } : void 0);
907
+ for (const a of activities.slice(0, 20)) {
908
+ useExecutionStore.getState().appendLine("activity", `[${a.type}] ${a.title}`);
909
+ }
910
+ useExecutionStore.getState().setWatching("activity");
911
+ useTuiStore.getState().focusPanel("output");
912
+ } catch (err) {
913
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
914
+ }
915
+ },
916
+ // ── Playground ──
917
+ // Matches frontend: creates a new playground project, then dispatches.
918
+ // No project selection required — uses session settings for machine + workdir.
919
+ playground: async (args, client) => {
920
+ const description = args.join(" ");
921
+ if (!description) {
922
+ useTuiStore.getState().setLastError("Usage: playground <description>");
923
+ return;
924
+ }
925
+ const settings = useSessionSettingsStore.getState();
926
+ if (!settings.machineId) {
927
+ const machines = useMachinesStore.getState().machines;
928
+ const localPlatform = process.platform;
929
+ const m = machines.find((m2) => m2.isConnected && m2.platform === localPlatform) ?? machines.find((m2) => m2.isConnected);
930
+ if (m) {
931
+ settings.init(m.id, m.name, settings.workingDirectory || process.cwd());
932
+ }
933
+ }
934
+ const targetMachineId = settings.machineId;
935
+ const targetMachineName = settings.machineName;
936
+ const workDir = settings.workingDirectory || process.cwd();
937
+ if (!targetMachineId) {
938
+ useTuiStore.getState().setLastError("No connected machines. Start an agent runner first.");
939
+ return;
940
+ }
941
+ const execId = `playground-${Date.now()}`;
942
+ const title = `Playground: ${description.slice(0, 50)}`;
943
+ useExecutionStore.getState().initExecution(execId, execId, title);
944
+ useExecutionStore.getState().setWatching(execId);
945
+ useTuiStore.getState().setActiveView("playground");
946
+ useExecutionStore.getState().appendLine(execId, `> ${description}`);
947
+ useExecutionStore.getState().appendLine(execId, `[progress] Creating playground session...`);
948
+ try {
949
+ const projectId = crypto.randomUUID();
950
+ const project = await client.createProject({
951
+ id: projectId,
952
+ name: description.slice(0, 60) || "Playground Session",
953
+ description,
954
+ workingDirectory: workDir,
955
+ defaultMachineId: targetMachineId,
956
+ projectType: "playground"
957
+ });
958
+ const nodeId = `playground-${project.id}`;
959
+ useExecutionStore.getState().appendLine(execId, `[progress] Dispatching to ${targetMachineName ?? targetMachineId} (${workDir})...`);
960
+ const response = await client.dispatchTask({
961
+ nodeId,
962
+ projectId: project.id,
963
+ title,
964
+ description,
965
+ targetMachineId,
966
+ workingDirectory: workDir,
967
+ deliveryMode: "direct",
968
+ skipSafetyCheck: true,
969
+ force: true
970
+ });
971
+ await streamSSEToExecution(response, execId, client, project.id);
972
+ } catch (err) {
973
+ useExecutionStore.getState().setStatus(execId, "error");
974
+ useExecutionStore.getState().appendLine(execId, `[error] ${err instanceof Error ? err.message : String(err)}`);
975
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
976
+ }
977
+ },
978
+ // ── Plan Generate (uses /api/dispatch/task SSE with isInteractivePlan) ──
979
+ "plan generate": async (args, client) => {
980
+ const description = args.join(" ");
981
+ if (!description) {
982
+ useTuiStore.getState().setLastError("Usage: plan generate <description>");
983
+ return;
984
+ }
985
+ const projectId = useTuiStore.getState().selectedProjectId;
986
+ if (!projectId) {
987
+ useTuiStore.getState().setLastError("No project selected. Select a project first.");
988
+ return;
989
+ }
990
+ const machines = useMachinesStore.getState().machines;
991
+ const connectedMachine = machines.find((m) => m.isConnected);
992
+ if (!connectedMachine) {
993
+ useTuiStore.getState().setLastError("No connected machines. Start an agent runner first.");
994
+ return;
995
+ }
996
+ const nodeId = `plan-${projectId}`;
997
+ const execId = `plan-${projectId}-${Date.now()}`;
998
+ const title = `Plan: ${description.slice(0, 50)}`;
999
+ useExecutionStore.getState().initExecution(execId, nodeId, title);
1000
+ useExecutionStore.getState().setWatching(execId);
1001
+ useTuiStore.getState().setActiveView("plan-gen");
1002
+ useExecutionStore.getState().appendLine(execId, `[progress] Plan generation started`);
1003
+ try {
1004
+ const response = await client.dispatchTask({
1005
+ nodeId,
1006
+ projectId,
1007
+ title: `Interactive planning: ${description.slice(0, 80)}`,
1008
+ description,
1009
+ targetMachineId: connectedMachine.id,
1010
+ isInteractivePlan: true,
1011
+ verification: "human"
1012
+ });
1013
+ await streamSSEToExecution(response, execId, client, projectId);
1014
+ } catch (err) {
1015
+ useExecutionStore.getState().setStatus(execId, "error");
1016
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1017
+ }
1018
+ },
1019
+ // ── Project Chat (uses /api/agent/project-chat SSE) ──
1020
+ "project chat": async (args, client) => {
1021
+ const message = args.join(" ");
1022
+ if (!message) {
1023
+ useTuiStore.getState().setLastError("Usage: project chat <message>");
1024
+ return;
1025
+ }
1026
+ const projectId = useTuiStore.getState().selectedProjectId;
1027
+ if (!projectId) {
1028
+ useTuiStore.getState().setLastError("No project selected");
1029
+ return;
1030
+ }
1031
+ const execId = `chat-project-${projectId}-${Date.now()}`;
1032
+ const title = `Chat: ${message.slice(0, 50)}`;
1033
+ const sessionId = useChatStore.getState().sessionId;
1034
+ const messages = useChatStore.getState().messages.map((m) => ({
1035
+ role: m.role,
1036
+ content: m.content
1037
+ }));
1038
+ useExecutionStore.getState().initExecution(execId, `chat-${projectId}`, title);
1039
+ useExecutionStore.getState().setWatching(execId);
1040
+ useExecutionStore.getState().appendLine(execId, `> ${message}`);
1041
+ useChatStore.getState().addMessage("user", message);
1042
+ try {
1043
+ useChatStore.getState().setStreaming(true);
1044
+ useChatStore.getState().setContext(projectId);
1045
+ const { nodes, edges } = await client.getPlan(projectId);
1046
+ const response = await client.projectChat({
1047
+ message,
1048
+ sessionId: sessionId ?? void 0,
1049
+ projectId,
1050
+ planNodes: nodes,
1051
+ planEdges: edges,
1052
+ messages
1053
+ });
1054
+ await streamSSEToExecution(response, execId, client, projectId);
1055
+ useChatStore.getState().flushStream();
1056
+ useChatStore.getState().setStreaming(false);
1057
+ } catch (err) {
1058
+ useChatStore.getState().setStreaming(false);
1059
+ useExecutionStore.getState().setStatus(execId, "error");
1060
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1061
+ }
1062
+ },
1063
+ // ── Task Chat (uses /api/agent/task-chat SSE) ──
1064
+ "task chat": async (args, client) => {
1065
+ const message = args.join(" ");
1066
+ if (!message) {
1067
+ useTuiStore.getState().setLastError("Usage: task chat <message>");
1068
+ return;
1069
+ }
1070
+ const projectId = useTuiStore.getState().selectedProjectId;
1071
+ const nodeId = useTuiStore.getState().selectedNodeId;
1072
+ if (!projectId || !nodeId) {
1073
+ useTuiStore.getState().setLastError("No project/task selected");
1074
+ return;
1075
+ }
1076
+ const planNode = usePlanStore.getState().nodes.find((n) => n.id === nodeId);
1077
+ const taskTitle = planNode?.title ?? nodeId;
1078
+ const execId = `chat-task-${nodeId}-${Date.now()}`;
1079
+ const title = `Task Chat: ${taskTitle.slice(0, 40)}`;
1080
+ const sessionId = useChatStore.getState().sessionId;
1081
+ const messages = useChatStore.getState().messages.map((m) => ({
1082
+ role: m.role,
1083
+ content: m.content
1084
+ }));
1085
+ useExecutionStore.getState().initExecution(execId, `chat-${nodeId}`, title);
1086
+ useExecutionStore.getState().setWatching(execId);
1087
+ useExecutionStore.getState().appendLine(execId, `> ${message}`);
1088
+ useChatStore.getState().addMessage("user", message);
1089
+ try {
1090
+ useChatStore.getState().setStreaming(true);
1091
+ useChatStore.getState().setContext(projectId, nodeId);
1092
+ const response = await client.taskChat({
1093
+ message,
1094
+ sessionId: sessionId ?? void 0,
1095
+ nodeId,
1096
+ projectId,
1097
+ taskTitle,
1098
+ taskDescription: planNode?.description,
1099
+ taskOutput: planNode?.executionOutput ?? void 0,
1100
+ branchName: planNode?.branchName ?? void 0,
1101
+ prUrl: planNode?.prUrl ?? void 0,
1102
+ messages
1103
+ });
1104
+ await streamSSEToExecution(response, execId, client, projectId);
1105
+ useChatStore.getState().flushStream();
1106
+ useChatStore.getState().setStreaming(false);
1107
+ } catch (err) {
1108
+ useChatStore.getState().setStreaming(false);
1109
+ useExecutionStore.getState().setStatus(execId, "error");
1110
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1111
+ }
1112
+ },
1113
+ // ── Summarize ──
1114
+ summarize: async (args, client) => {
1115
+ const executionId = args[0] ?? useExecutionStore.getState().watchingId;
1116
+ if (!executionId) {
1117
+ useTuiStore.getState().setLastError("Usage: summarize [executionId]");
1118
+ return;
1119
+ }
1120
+ try {
1121
+ const result = await client.summarize({ executionId });
1122
+ useExecutionStore.getState().appendLine(executionId, `
1123
+ --- Summary ---
1124
+ ${result.summary}`);
1125
+ } catch (err) {
1126
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1127
+ }
1128
+ },
1129
+ // ── Resume ──
1130
+ resume: async (args) => {
1131
+ const execId = args[0];
1132
+ if (!execId) {
1133
+ useTuiStore.getState().setLastError("Usage: resume <executionId>");
1134
+ return;
1135
+ }
1136
+ const exec = useExecutionStore.getState().outputs.get(execId);
1137
+ if (!exec) {
1138
+ useTuiStore.getState().setLastError(`Session not found: ${execId}`);
1139
+ return;
1140
+ }
1141
+ useExecutionStore.getState().setWatching(execId);
1142
+ if (exec.nodeId.startsWith("playground-")) {
1143
+ useTuiStore.getState().setActiveView("playground");
1144
+ } else if (exec.nodeId.startsWith("plan-")) {
1145
+ useTuiStore.getState().setActiveView("plan-gen");
1146
+ } else {
1147
+ useTuiStore.getState().setActiveView("active");
1148
+ }
1149
+ },
1150
+ // ── Help ──
1151
+ help: async () => {
1152
+ useTuiStore.getState().toggleHelp();
1153
+ },
1154
+ "?": async () => {
1155
+ useTuiStore.getState().toggleHelp();
1156
+ }
1157
+ };
1158
+ async function streamSSEToExecution(response, execId, client, projectId) {
1159
+ if (!response.body) {
1160
+ useExecutionStore.getState().setStatus(execId, "completed");
1161
+ return;
1162
+ }
1163
+ await readSSEStream(response, async (event) => {
1164
+ const type = event.type;
1165
+ switch (type) {
1166
+ case "init":
1167
+ if (event.executionId) {
1168
+ useExecutionStore.getState().appendLine(execId, `[progress] Execution started (${event.executionId})`);
1169
+ }
1170
+ break;
1171
+ case "text": {
1172
+ const content = event.content ?? event.text ?? "";
1173
+ if (content) {
1174
+ useExecutionStore.getState().appendText(execId, content);
1175
+ useChatStore.getState().appendStream(content);
1176
+ }
1177
+ break;
1178
+ }
1179
+ case "tool_use":
1180
+ useExecutionStore.getState().appendToolCall(execId, event.toolName ?? event.name);
1181
+ break;
1182
+ case "tool_result":
1183
+ break;
1184
+ case "file_change":
1185
+ useExecutionStore.getState().appendFileChange(
1186
+ execId,
1187
+ event.path,
1188
+ event.action,
1189
+ event.linesAdded,
1190
+ event.linesRemoved
1191
+ );
1192
+ break;
1193
+ case "progress":
1194
+ useExecutionStore.getState().appendLine(execId, `[progress] ${event.message}`);
1195
+ break;
1196
+ case "session_init":
1197
+ if (event.sessionId) {
1198
+ useChatStore.getState().setSessionId(event.sessionId);
1199
+ }
1200
+ break;
1201
+ case "plan_result":
1202
+ useExecutionStore.getState().appendLine(execId, "[plan] Plan generated \u2014 refreshing...");
1203
+ if (projectId) {
1204
+ setTimeout(async () => {
1205
+ try {
1206
+ const { nodes, edges } = await client.getPlan(projectId);
1207
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
1208
+ } catch {
1209
+ }
1210
+ }, 500);
1211
+ }
1212
+ break;
1213
+ case "result":
1214
+ useExecutionStore.getState().setStatus(execId, event.status === "success" ? "completed" : "error");
1215
+ if (event.error) {
1216
+ useExecutionStore.getState().appendLine(execId, `[error] ${event.error}`);
1217
+ }
1218
+ break;
1219
+ case "approval_request":
1220
+ useExecutionStore.getState().setPendingApproval({
1221
+ requestId: event.requestId,
1222
+ question: event.question,
1223
+ options: event.options,
1224
+ machineId: event.machineId,
1225
+ taskId: event.taskId
1226
+ });
1227
+ break;
1228
+ case "done":
1229
+ useExecutionStore.getState().setStatus(execId, "completed");
1230
+ break;
1231
+ case "error": {
1232
+ const msg = event.error ?? event.message ?? "Unknown error";
1233
+ useExecutionStore.getState().appendLine(execId, `[error] ${msg}`);
1234
+ useExecutionStore.getState().setStatus(execId, "error");
1235
+ break;
1236
+ }
1237
+ case "heartbeat":
1238
+ case "compaction":
1239
+ case "aborted":
1240
+ break;
1241
+ }
1242
+ });
1243
+ const exec = useExecutionStore.getState().outputs.get(execId);
1244
+ if (exec?.status === "running") {
1245
+ useExecutionStore.getState().setStatus(execId, "completed");
1246
+ }
1247
+ }
1248
+ async function refreshAll(client) {
1249
+ try {
1250
+ const [projects, machines, fullPlan] = await Promise.all([
1251
+ client.listProjects(),
1252
+ client.listMachines(),
1253
+ client.getFullPlan()
1254
+ ]);
1255
+ useProjectsStore.getState().setProjects(projects);
1256
+ useMachinesStore.getState().setMachines(machines);
1257
+ useTuiStore.getState().setMachineCount(machines.filter((m) => m.isConnected).length);
1258
+ usePlanStore.getState().setAllPlans(fullPlan.nodes, fullPlan.edges);
1259
+ } catch (err) {
1260
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1261
+ }
1262
+ }
1263
+
1264
+ // src/tui/commands/palette-filter.ts
1265
+ function getResumeCommands() {
1266
+ const outputs = useExecutionStore.getState().outputs;
1267
+ const entries = [];
1268
+ for (const [id, exec] of outputs) {
1269
+ const statusLabel = exec.status === "running" ? "\u25B6" : exec.status === "success" || exec.status === "completed" ? "\u2713" : "\xB7";
1270
+ entries.push({
1271
+ name: `resume ${exec.title}`,
1272
+ description: `${statusLabel} ${exec.status} \u2014 switch to this session`,
1273
+ usage: `resume:${id}`
1274
+ });
1275
+ }
1276
+ entries.sort((a, b) => {
1277
+ const aExec = outputs.get(a.usage.replace("resume:", ""));
1278
+ const bExec = outputs.get(b.usage.replace("resume:", ""));
1279
+ if (aExec?.status === "running" && bExec?.status !== "running") return -1;
1280
+ if (bExec?.status === "running" && aExec?.status !== "running") return 1;
1281
+ const ta = aExec?.startedAt ? new Date(aExec.startedAt).getTime() : 0;
1282
+ const tb = bExec?.startedAt ? new Date(bExec.startedAt).getTime() : 0;
1283
+ return tb - ta;
1284
+ });
1285
+ return entries;
1286
+ }
1287
+ function getFilteredPaletteCommands(query) {
1288
+ const resumeCommands = getResumeCommands();
1289
+ const allCommands = [...PALETTE_COMMANDS, ...resumeCommands];
1290
+ if (!query) return allCommands;
1291
+ const lower = query.toLowerCase();
1292
+ return allCommands.filter(
1293
+ (cmd) => cmd.name.toLowerCase().includes(lower) || cmd.description.toLowerCase().includes(lower)
1294
+ );
1295
+ }
1296
+
1297
+ // src/tui/components/layout/command-line.tsx
1298
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1299
+ var SHORTCUTS = [
1300
+ { key: "1", label: "Dashboard" },
1301
+ { key: "2", label: "Plan" },
1302
+ { key: "3", label: "Projects" },
1303
+ { key: "4", label: "Playground" },
1304
+ { key: "5", label: "Active" },
1305
+ { key: "/", label: "Search" },
1306
+ { key: "C-p", label: "Commands" },
1307
+ { key: "d", label: "Dispatch" },
1308
+ { key: "x", label: "Stop" },
1309
+ { key: "?", label: "Help" },
1310
+ { key: "q", label: "Quit" }
1311
+ ];
1312
+ var TYPE_LABELS = {
1313
+ project: { label: "proj", color: "cyan" },
1314
+ task: { label: "task", color: "yellow" },
1315
+ machine: { label: "env", color: "green" },
1316
+ execution: { label: "exec", color: "magenta" }
1317
+ };
1318
+ function CommandLine({ height }) {
1319
+ const mode = useTuiStore((s) => s.mode);
1320
+ const commandBuffer = useTuiStore((s) => s.commandBuffer);
1321
+ const paletteIndex = useTuiStore((s) => s.paletteIndex);
1322
+ const searchOpen = useSearchStore((s) => s.isOpen);
1323
+ const searchQuery = useSearchStore((s) => s.query);
1324
+ const searchResults = useSearchStore((s) => s.results);
1325
+ const searchItems = useSearchStore((s) => s.items);
1326
+ const searchIndex = useSearchStore((s) => s.selectedIndex);
1327
+ const { stdout } = useStdout();
1328
+ const termWidth = stdout?.columns ?? 80;
1329
+ const listHeight = Math.max(1, height - 3);
1330
+ if (searchOpen) {
1331
+ const displayList = searchQuery.length > 0 ? searchResults : searchItems;
1332
+ const visible = displayList.slice(0, listHeight);
1333
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height, borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [
1334
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1335
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "/ " }),
1336
+ /* @__PURE__ */ jsx2(Text2, { children: searchQuery }),
1337
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2588" }),
1338
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1339
+ " (",
1340
+ "\u2191\u2193",
1341
+ " navigate, Enter to go, Esc to close)"
1342
+ ] })
1343
+ ] }),
1344
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
1345
+ visible.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: searchQuery.length > 0 ? " No results" : " No items" }) : visible.map((item, i) => {
1346
+ const isSelected = i === searchIndex;
1347
+ const typeInfo = TYPE_LABELS[item.type] ?? { label: item.type, color: "white" };
1348
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1349
+ /* @__PURE__ */ jsx2(Text2, { inverse: isSelected, bold: isSelected, color: isSelected ? "cyan" : void 0, children: isSelected ? " > " : " " }),
1350
+ /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, color: isSelected ? "cyan" : typeInfo.color, children: [
1351
+ "[",
1352
+ typeInfo.label,
1353
+ "]"
1354
+ ] }),
1355
+ /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, bold: isSelected, wrap: "truncate", children: [
1356
+ " ",
1357
+ item.title
1358
+ ] }),
1359
+ item.status && /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, color: isSelected ? void 0 : getStatusColor(item.status), dimColor: !isSelected, children: [
1360
+ " ",
1361
+ item.status
1362
+ ] })
1363
+ ] }, `${item.type}-${item.id}`);
1364
+ }),
1365
+ displayList.length > listHeight && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1366
+ " ...and ",
1367
+ displayList.length - listHeight,
1368
+ " more"
1369
+ ] })
1370
+ ] })
1371
+ ] });
1372
+ }
1373
+ if (mode === "palette") {
1374
+ const filtered = getFilteredPaletteCommands(commandBuffer);
1375
+ const visible = filtered.slice(0, listHeight);
1376
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height, borderStyle: "single", borderColor: "yellow", paddingX: 1, children: [
1377
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1378
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: "> " }),
1379
+ /* @__PURE__ */ jsx2(Text2, { children: commandBuffer }),
1380
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2588" }),
1381
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1382
+ " (",
1383
+ "\u2191\u2193",
1384
+ " navigate, Enter to run, Esc to cancel)"
1385
+ ] })
1386
+ ] }),
1387
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
1388
+ visible.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " No matching commands" }) : visible.map((cmd, i) => {
1389
+ const isSelected = i === paletteIndex;
1390
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1391
+ /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, bold: isSelected, color: isSelected ? "yellow" : void 0, children: [
1392
+ isSelected ? " > " : " ",
1393
+ cmd.name.padEnd(20)
1394
+ ] }),
1395
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: !isSelected, color: isSelected ? "white" : void 0, children: [
1396
+ " ",
1397
+ cmd.description
1398
+ ] })
1399
+ ] }, cmd.name);
1400
+ }),
1401
+ filtered.length > listHeight && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1402
+ " ...and ",
1403
+ filtered.length - listHeight,
1404
+ " more"
1405
+ ] })
1406
+ ] })
1407
+ ] });
1408
+ }
1409
+ if (mode === "input") {
1410
+ return /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, gap: 1, height, children: [
1411
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Input active" }),
1412
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1413
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, bold: true, children: " C-d " }),
1414
+ /* @__PURE__ */ jsx2(Text2, { children: "Stop" })
1415
+ ] }),
1416
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1417
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, bold: true, children: " Esc " }),
1418
+ /* @__PURE__ */ jsx2(Text2, { children: "Exit input" })
1419
+ ] })
1420
+ ] });
1421
+ }
1422
+ return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, gap: 1, height, children: SHORTCUTS.map(({ key, label }) => /* @__PURE__ */ jsxs2(Box2, { children: [
1423
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, bold: true, children: ` ${key} ` }),
1424
+ /* @__PURE__ */ jsx2(Text2, { children: label })
1425
+ ] }, key)) });
1426
+ }
1427
+
1428
+ // src/tui/components/layout/help-overlay.tsx
1429
+ import "react";
1430
+ import { Box as Box3, Text as Text3 } from "ink";
1431
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1432
+ var KEYBINDINGS = [
1433
+ ["Navigation", [
1434
+ ["\u2191 / k", "Move up"],
1435
+ ["\u2193 / j", "Move down"],
1436
+ ["\u2190 / h", "Focus left panel"],
1437
+ ["\u2192 / l", "Focus right panel"],
1438
+ ["Tab", "Cycle panel focus"],
1439
+ ["PgUp / PgDn", "Page up / down"],
1440
+ ["Home / End", "Scroll to top / bottom"],
1441
+ ["Enter", "Select / open detail view"],
1442
+ ["d", "Dispatch selected task"]
1443
+ ]],
1444
+ ["Views", [
1445
+ ["1", "Dashboard (default)"],
1446
+ ["2", "Plan Generation"],
1447
+ ["3", "Projects & Plan"],
1448
+ ["4", "Playground"],
1449
+ ["5", "Output"]
1450
+ ]],
1451
+ ["Shortcuts", [
1452
+ ["Ctrl+P / :", "Open command palette (searchable)"],
1453
+ ["Ctrl+F / /", "Open search"],
1454
+ ["Ctrl+R", "Refresh all data"],
1455
+ ["?", "Toggle this help"],
1456
+ ["q / Ctrl+C", "Quit"],
1457
+ ["Esc", "Close overlay / cancel input"]
1458
+ ]],
1459
+ ["Commands (via Ctrl+P)", [
1460
+ ["project list / create / delete", "Project management"],
1461
+ ["plan create-node", "Create a plan node"],
1462
+ ["dispatch <nodeId>", "Dispatch task for execution"],
1463
+ ["cancel <execId>", "Cancel execution"],
1464
+ ["steer <message>", "Steer running task"],
1465
+ ["watch <execId>", "Watch execution output"],
1466
+ ["env list / status", "Machine management"],
1467
+ ["activity", "Show activity feed"],
1468
+ ["refresh / r", "Force refresh"],
1469
+ ["quit / q", "Exit TUI"]
1470
+ ]],
1471
+ ["Terminal Compatibility", [
1472
+ ["tmux", "Ctrl+B prefix is not captured \u2014 safe"],
1473
+ ["screen", "Ctrl+A prefix is not captured \u2014 safe"],
1474
+ ["vscode", "All bindings work in integrated terminal"]
1475
+ ]]
1476
+ ];
1477
+ function HelpOverlay() {
1478
+ return /* @__PURE__ */ jsxs3(
1479
+ Box3,
1480
+ {
1481
+ flexDirection: "column",
1482
+ borderStyle: "round",
1483
+ borderColor: "yellow",
1484
+ paddingX: 2,
1485
+ paddingY: 1,
1486
+ children: [
1487
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: " Keybindings Reference " }),
1488
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
1489
+ KEYBINDINGS.map(([section, bindings]) => /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
1490
+ /* @__PURE__ */ jsx3(Text3, { bold: true, underline: true, children: section }),
1491
+ bindings.map(([key, desc]) => /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
1492
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: key.padEnd(34) }),
1493
+ /* @__PURE__ */ jsx3(Text3, { children: desc })
1494
+ ] }, key))
1495
+ ] }, section)),
1496
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Press Esc or ? to close" })
1497
+ ]
1498
+ }
1499
+ );
1500
+ }
1501
+
1502
+ // src/tui/components/layout/search-overlay.tsx
1503
+ import { useInput } from "ink";
1504
+ function SearchOverlay() {
1505
+ const isOpen = useSearchStore((s) => s.isOpen);
1506
+ const query = useSearchStore((s) => s.query);
1507
+ const results = useSearchStore((s) => s.results);
1508
+ const items = useSearchStore((s) => s.items);
1509
+ const selectedIndex = useSearchStore((s) => s.selectedIndex);
1510
+ const { setQuery, moveUp, moveDown, close } = useSearchStore();
1511
+ const { setSelectedProject, setSelectedNode, setSelectedMachine, focusPanel, openDetail } = useTuiStore();
1512
+ useInput((input, key) => {
1513
+ if (!isOpen) return;
1514
+ if (key.escape) {
1515
+ close();
1516
+ return;
1517
+ }
1518
+ if (key.upArrow) {
1519
+ moveUp();
1520
+ return;
1521
+ }
1522
+ if (key.downArrow || key.tab) {
1523
+ moveDown();
1524
+ return;
1525
+ }
1526
+ if (key.return) {
1527
+ const displayList = query.length > 0 ? results : items;
1528
+ const item = displayList[selectedIndex];
1529
+ if (item) {
1530
+ switch (item.type) {
1531
+ case "project":
1532
+ setSelectedProject(item.id);
1533
+ focusPanel("projects");
1534
+ break;
1535
+ case "task":
1536
+ setSelectedNode(item.id);
1537
+ focusPanel("plan");
1538
+ openDetail("node", item.id);
1539
+ break;
1540
+ case "machine":
1541
+ setSelectedMachine(item.id);
1542
+ focusPanel("machines");
1543
+ break;
1544
+ }
1545
+ }
1546
+ close();
1547
+ return;
1548
+ }
1549
+ if (key.backspace || key.delete) {
1550
+ setQuery(query.slice(0, -1));
1551
+ return;
1552
+ }
1553
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
1554
+ setQuery(query + input);
1555
+ }
1556
+ }, { isActive: isOpen });
1557
+ return null;
1558
+ }
1559
+
1560
+ // src/tui/components/panels/projects-panel.tsx
1561
+ import { useEffect as useEffect2, useMemo } from "react";
1562
+ import { Box as Box5, Text as Text6 } from "ink";
1563
+
1564
+ // src/tui/components/layout/panel.tsx
1565
+ import "react";
1566
+ import { Box as Box4, Text as Text4 } from "ink";
1567
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1568
+ function Panel({ title, isFocused, children, width, height }) {
1569
+ const borderColor = isFocused ? "cyan" : "gray";
1570
+ return /* @__PURE__ */ jsxs4(
1571
+ Box4,
1572
+ {
1573
+ flexDirection: "column",
1574
+ borderStyle: "single",
1575
+ borderColor,
1576
+ width,
1577
+ height,
1578
+ flexGrow: 1,
1579
+ children: [
1580
+ /* @__PURE__ */ jsx4(Box4, { paddingX: 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: isFocused ? "cyan" : "white", children: title }) }),
1581
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, children })
1582
+ ]
1583
+ }
1584
+ );
1585
+ }
1586
+
1587
+ // src/tui/components/shared/spinner.tsx
1588
+ import { useState, useEffect } from "react";
1589
+ import { Text as Text5 } from "ink";
1590
+ import { jsxs as jsxs5 } from "react/jsx-runtime";
1591
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1592
+ function Spinner({ label }) {
1593
+ const [frame, setFrame] = useState(0);
1594
+ useEffect(() => {
1595
+ const timer = setInterval(() => {
1596
+ setFrame((f) => (f + 1) % FRAMES.length);
1597
+ }, 80);
1598
+ return () => clearInterval(timer);
1599
+ }, []);
1600
+ return /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
1601
+ FRAMES[frame],
1602
+ label ? ` ${label}` : ""
1603
+ ] });
1604
+ }
1605
+
1606
+ // src/tui/components/panels/projects-panel.tsx
1607
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
1608
+ var TERMINAL_NODE_STATUSES = /* @__PURE__ */ new Set([
1609
+ "completed",
1610
+ "auto_verified",
1611
+ "awaiting_judgment",
1612
+ "awaiting_approval",
1613
+ "pruned"
1614
+ ]);
1615
+ function ProjectsPanel({ height }) {
1616
+ const projects = useProjectsStore((s) => s.projects);
1617
+ const loading = useProjectsStore((s) => s.loading);
1618
+ const error = useProjectsStore((s) => s.error);
1619
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
1620
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.projects);
1621
+ const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
1622
+ const outputs = useExecutionStore((s) => s.outputs);
1623
+ const planNodes = usePlanStore((s) => s.nodes);
1624
+ const isFocused = focusedPanel === "projects";
1625
+ const visibleHeight = Math.max(1, height - 3);
1626
+ const projectsWithRunning = useMemo(() => {
1627
+ const running = /* @__PURE__ */ new Set();
1628
+ for (const [, exec] of outputs) {
1629
+ if (exec.status !== "running" && exec.status !== "pending" && exec.status !== "dispatched") continue;
1630
+ const nodeId = exec.nodeId;
1631
+ if (nodeId.startsWith("plan-")) {
1632
+ running.add(nodeId.replace(/^plan-/, "").replace(/-\d+$/, ""));
1633
+ } else if (nodeId.startsWith("playground-")) {
1634
+ const parts = nodeId.replace(/^playground-/, "").split("-");
1635
+ parts.pop();
1636
+ running.add(parts.join("-"));
1637
+ } else if (nodeId.startsWith("chat-")) {
1638
+ running.add(nodeId.replace(/^chat-(project-|task-)?/, "").replace(/-\d+$/, ""));
1639
+ } else {
1640
+ const node = planNodes.find((n) => n.id === nodeId);
1641
+ if (node && !TERMINAL_NODE_STATUSES.has(node.status)) {
1642
+ running.add(node.projectId);
1643
+ }
1644
+ }
1645
+ }
1646
+ for (const node of planNodes) {
1647
+ if ((node.status === "in_progress" || node.status === "dispatched") && !node.deletedAt) {
1648
+ running.add(node.projectId);
1649
+ }
1650
+ }
1651
+ return running;
1652
+ }, [outputs, planNodes]);
1653
+ const sorted = getVisibleProjects(projects);
1654
+ const projectCount = sorted.length;
1655
+ const maxIndex = Math.max(0, projectCount - 1);
1656
+ const cursor = projectCount === 0 ? 0 : Math.min(Math.max(0, scrollIndex), maxIndex);
1657
+ useEffect2(() => {
1658
+ if (projectCount > 0 && scrollIndex !== cursor) {
1659
+ useTuiStore.setState((s) => ({
1660
+ scrollIndex: { ...s.scrollIndex, projects: cursor }
1661
+ }));
1662
+ }
1663
+ }, [scrollIndex, cursor, projectCount]);
1664
+ let start = 0;
1665
+ if (projectCount > visibleHeight) {
1666
+ if (cursor >= projectCount - visibleHeight) {
1667
+ start = projectCount - visibleHeight;
1668
+ } else {
1669
+ start = Math.max(0, cursor - Math.floor(visibleHeight / 2));
1670
+ }
1671
+ }
1672
+ const visibleProjects = sorted.slice(start, start + visibleHeight);
1673
+ 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: [
1674
+ visibleProjects.length === 0 && /* @__PURE__ */ jsx5(Text6, { dimColor: true, children: " No projects yet" }),
1675
+ visibleProjects.map((p, i) => {
1676
+ const actualIndex = start + i;
1677
+ const isSelected = isFocused && cursor === actualIndex;
1678
+ const isActive = p.id === selectedProjectId;
1679
+ const hasRunning = projectsWithRunning.has(p.id);
1680
+ return /* @__PURE__ */ jsxs6(Box5, { children: [
1681
+ /* @__PURE__ */ jsxs6(
1682
+ Text6,
1683
+ {
1684
+ inverse: isSelected,
1685
+ bold: isSelected,
1686
+ color: isActive ? "cyan" : void 0,
1687
+ wrap: "truncate",
1688
+ children: [
1689
+ isSelected ? " > " : " ",
1690
+ hasRunning ? "\u25B6 " : " ",
1691
+ truncate(p.name, 28)
1692
+ ]
1693
+ }
1694
+ ),
1695
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: !isSelected, children: [
1696
+ " ",
1697
+ formatRelativeTime(p.updatedAt)
1698
+ ] })
1699
+ ] }, p.id);
1700
+ }),
1701
+ projectCount > visibleHeight && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1702
+ " [",
1703
+ start + 1,
1704
+ "-",
1705
+ Math.min(start + visibleHeight, projectCount),
1706
+ "/",
1707
+ projectCount,
1708
+ "]"
1709
+ ] })
1710
+ ] }) });
1711
+ }
1712
+
1713
+ // src/tui/components/panels/plan-panel.tsx
1714
+ import "react";
1715
+ import { Text as Text8 } from "ink";
1716
+
1717
+ // src/tui/components/shared/scrollable-list.tsx
1718
+ import "react";
1719
+ import { Box as Box6, Text as Text7, useStdout as useStdout2 } from "ink";
1720
+ import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
1721
+ function ScrollableList({ items, selectedIndex, height, isFocused }) {
1722
+ const { stdout } = useStdout2();
1723
+ const termWidth = stdout?.columns ?? 80;
1724
+ if (items.length === 0) {
1725
+ return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: " No items." }) });
1726
+ }
1727
+ const cursor = Math.min(Math.max(0, selectedIndex), items.length - 1);
1728
+ const visibleHeight = Math.max(1, height - 1);
1729
+ let start = 0;
1730
+ if (items.length > visibleHeight) {
1731
+ if (cursor >= items.length - visibleHeight) {
1732
+ start = items.length - visibleHeight;
1733
+ } else {
1734
+ start = Math.max(0, cursor - Math.floor(visibleHeight / 2));
1735
+ }
1736
+ }
1737
+ const visibleItems = items.slice(start, start + visibleHeight);
1738
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", children: [
1739
+ visibleItems.map((item, i) => {
1740
+ const actualIndex = start + i;
1741
+ const isSelected = actualIndex === cursor && isFocused;
1742
+ const prefix = isSelected ? " \u25B6 " : " ";
1743
+ const sublabel = item.sublabel ? ` ${item.sublabel}` : "";
1744
+ const rightLabel = item.rightLabel ?? "";
1745
+ const maxLabelWidth = Math.max(10, termWidth - prefix.length - sublabel.length - rightLabel.length - 10);
1746
+ const truncatedLabel = item.label.length > maxLabelWidth ? item.label.slice(0, maxLabelWidth - 1) + "\u2026" : item.label;
1747
+ return /* @__PURE__ */ jsxs7(Box6, { children: [
1748
+ /* @__PURE__ */ jsxs7(
1749
+ Text7,
1750
+ {
1751
+ color: isSelected ? "cyan" : item.color ?? void 0,
1752
+ bold: isSelected,
1753
+ inverse: isSelected,
1754
+ wrap: "truncate",
1755
+ children: [
1756
+ prefix,
1757
+ truncatedLabel,
1758
+ sublabel
1759
+ ]
1760
+ }
1761
+ ),
1762
+ rightLabel && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1763
+ " ",
1764
+ rightLabel
1765
+ ] })
1766
+ ] }, item.id);
1767
+ }),
1768
+ items.length > visibleHeight && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1769
+ " [",
1770
+ start + 1,
1771
+ "-",
1772
+ Math.min(start + visibleHeight, items.length),
1773
+ "/",
1774
+ items.length,
1775
+ "]"
1776
+ ] })
1777
+ ] });
1778
+ }
1779
+
1780
+ // src/tui/components/panels/plan-panel.tsx
1781
+ import { jsx as jsx7 } from "react/jsx-runtime";
1782
+ function PlanPanel({ height }) {
1783
+ const nodes = usePlanStore((s) => s.nodes);
1784
+ const loading = usePlanStore((s) => s.loading);
1785
+ const error = usePlanStore((s) => s.error);
1786
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
1787
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.plan);
1788
+ const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
1789
+ const selectedNodeId = useTuiStore((s) => s.selectedNodeId);
1790
+ const projects = useProjectsStore((s) => s.projects);
1791
+ const isFocused = focusedPanel === "plan";
1792
+ const projectName = projects.find((p) => p.id === selectedProjectId)?.name ?? "none";
1793
+ const visibleNodes = nodes.filter((n) => !n.deletedAt);
1794
+ const items = visibleNodes.map((n) => ({
1795
+ id: n.id,
1796
+ label: `${getStatusSymbol(n.status)} ${n.title}`,
1797
+ sublabel: `[${n.status}]`,
1798
+ color: n.id === selectedNodeId ? "cyan" : getStatusColor(n.status)
1799
+ }));
1800
+ 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(
1801
+ ScrollableList,
1802
+ {
1803
+ items,
1804
+ selectedIndex: scrollIndex,
1805
+ height: height - 3,
1806
+ isFocused
1807
+ }
1808
+ ) });
1809
+ }
1810
+
1811
+ // src/tui/components/panels/machines-panel.tsx
1812
+ import "react";
1813
+ import { Text as Text9 } from "ink";
1814
+ import { jsx as jsx8 } from "react/jsx-runtime";
1815
+ function MachinesPanel({ height }) {
1816
+ const machines = useMachinesStore((s) => s.machines);
1817
+ const loading = useMachinesStore((s) => s.loading);
1818
+ const error = useMachinesStore((s) => s.error);
1819
+ const focusedPanel = useTuiStore((s) => s.focusedPanel);
1820
+ const scrollIndex = useTuiStore((s) => s.scrollIndex.machines);
1821
+ const isFocused = focusedPanel === "machines";
1822
+ const activeMachines = machines.filter((m) => !m.isRevoked);
1823
+ const items = activeMachines.map((m) => ({
1824
+ id: m.id,
1825
+ label: m.name,
1826
+ sublabel: m.platform,
1827
+ rightLabel: m.isConnected ? "\u25CF online" : "\u25CB offline",
1828
+ color: m.isConnected ? "green" : "gray"
1829
+ }));
1830
+ 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(
1831
+ ScrollableList,
1832
+ {
1833
+ items,
1834
+ selectedIndex: scrollIndex,
1835
+ height: height - 3,
1836
+ isFocused
1837
+ }
1838
+ ) });
1839
+ }
1840
+
957
1841
  // src/tui/components/panels/output-panel.tsx
958
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1842
+ import "react";
1843
+ import { Box as Box7, Text as Text10 } from "ink";
1844
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
959
1845
  function lineColor(line) {
960
1846
  if (line.startsWith("[Tool Call]")) return "gray";
961
1847
  if (line.startsWith("[error]")) return "red";
@@ -971,13 +1857,13 @@ function OutputPanel({ height }) {
971
1857
  const isFocused = focusedPanel === "output";
972
1858
  const execution = watchingId ? outputs.get(watchingId) : null;
973
1859
  const visibleHeight = Math.max(1, height - 4);
974
- let title = "OUTPUT";
1860
+ let title = "PROCESS OUTPUT";
975
1861
  if (execution) {
976
1862
  const shortId = execution.nodeId.length > 20 ? execution.nodeId.slice(0, 8) + "\u2026" : execution.nodeId;
977
- title = `OUTPUT (${shortId}) [${execution.status}]`;
1863
+ title = `${shortId} [${execution.status}]`;
978
1864
  }
979
1865
  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" }) });
1866
+ 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
1867
  }
982
1868
  const lines = execution.lines;
983
1869
  const isRunning = execution.status === "running";
@@ -989,11 +1875,11 @@ function OutputPanel({ height }) {
989
1875
  start = Math.max(0, scrollIndex);
990
1876
  }
991
1877
  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: [
1878
+ return /* @__PURE__ */ jsx9(Panel, { title, isFocused, height, children: /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
1879
+ visibleLines.map((line, i) => /* @__PURE__ */ jsx9(Text10, { color: lineColor(line), dimColor: line.startsWith("[Tool Call]"), wrap: "truncate", children: truncate(line, 200) }, start + i)),
1880
+ hasPendingTools && /* @__PURE__ */ jsx9(Text10, { dimColor: true, children: "[Tool Call] " + "\xB7".repeat(execution.pendingToolCount) }),
1881
+ isRunning && !hasPendingTools && /* @__PURE__ */ jsx9(Spinner, { label: "Running..." }),
1882
+ lines.length > visibleHeight && /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
997
1883
  " [",
998
1884
  start + 1,
999
1885
  "-",
@@ -1005,17 +1891,428 @@ function OutputPanel({ height }) {
1005
1891
  ] }) });
1006
1892
  }
1007
1893
 
1008
- // src/tui/components/panels/detail-overlay.tsx
1894
+ // src/tui/components/panels/chat-panel.tsx
1895
+ import "react";
1896
+ import { Box as Box8, Text as Text11 } from "ink";
1897
+ import { TextInput } from "@inkjs/ui";
1898
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1899
+ function ChatPanel({ height }) {
1900
+ const messages = useChatStore((s) => s.messages);
1901
+ const streaming = useChatStore((s) => s.streaming);
1902
+ const streamBuffer = useChatStore((s) => s.streamBuffer);
1903
+ const sessionId = useChatStore((s) => s.sessionId);
1904
+ const displayLines = [];
1905
+ for (let i = 0; i < messages.length; i++) {
1906
+ const m = messages[i];
1907
+ const prefix = m.role === "user" ? "> " : " ";
1908
+ displayLines.push({ key: `msg-${i}`, text: `${prefix}${m.content}`, color: m.role === "user" ? "cyan" : void 0 });
1909
+ }
1910
+ if (streaming && streamBuffer) {
1911
+ displayLines.push({ key: "stream", text: ` ${streamBuffer}` });
1912
+ }
1913
+ const contentHeight = height - 4;
1914
+ const visibleLines = displayLines.slice(-Math.max(1, contentHeight));
1915
+ const titleSuffix = sessionId ? ` (${sessionId.slice(0, 8)})` : "";
1916
+ return /* @__PURE__ */ jsx10(Panel, { title: `Chat${titleSuffix}`, isFocused: false, height, children: /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
1917
+ /* @__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)) }),
1918
+ /* @__PURE__ */ jsx10(Box8, { children: streaming ? /* @__PURE__ */ jsxs9(Box8, { children: [
1919
+ /* @__PURE__ */ jsx10(Spinner, {}),
1920
+ /* @__PURE__ */ jsx10(Text11, { dimColor: true, children: " Streaming..." })
1921
+ ] }) : /* @__PURE__ */ jsxs9(Box8, { children: [
1922
+ /* @__PURE__ */ jsx10(Text11, { color: "cyan", children: "> " }),
1923
+ /* @__PURE__ */ jsx10(
1924
+ TextInput,
1925
+ {
1926
+ placeholder: "Type a message...",
1927
+ onSubmit: (value) => {
1928
+ if (value.trim()) {
1929
+ useChatStore.getState().addMessage("user", value.trim());
1930
+ }
1931
+ }
1932
+ }
1933
+ )
1934
+ ] }) })
1935
+ ] }) });
1936
+ }
1937
+
1938
+ // src/tui/components/panels/session-panel.tsx
1009
1939
  import "react";
1010
1940
  import { Box as Box9, Text as Text12 } from "ink";
1941
+ import { TextInput as TextInput2 } from "@inkjs/ui";
1011
1942
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1943
+ function lineColor2(line) {
1944
+ if (line.startsWith("[Tool Call]")) return "gray";
1945
+ if (line.startsWith("[error]")) return "red";
1946
+ if (line.startsWith("[progress]")) return "cyan";
1947
+ if (line.startsWith("[plan]")) return "magenta";
1948
+ if (line.startsWith("[created]") || line.startsWith("[modified]") || line.startsWith("[deleted]")) return "yellow";
1949
+ if (line.startsWith("> ")) return "cyan";
1950
+ return void 0;
1951
+ }
1952
+ function machineLabel(name, id) {
1953
+ if (!id) return "No machine";
1954
+ return name || id.slice(0, 12);
1955
+ }
1956
+ function SessionPanel({ height, title, sessionType, onSubmit }) {
1957
+ const watchingId = useExecutionStore((s) => s.watchingId);
1958
+ const outputs = useExecutionStore((s) => s.outputs);
1959
+ const streaming = useChatStore((s) => s.streaming);
1960
+ const mode = useTuiStore((s) => s.mode);
1961
+ const { machineId, machineName, workingDirectory, focusedField, pickerOpen } = useSessionSettingsStore();
1962
+ const machines = useMachinesStore((s) => s.machines);
1963
+ const connectedMachines = machines.filter((m) => m.isConnected);
1964
+ const execution = watchingId ? outputs.get(watchingId) : null;
1965
+ const isRunning = execution?.status === "running";
1966
+ const settingsHeight = pickerOpen ? 2 + Math.min(connectedMachines.length, 5) : 2;
1967
+ const inputHeight = 2;
1968
+ const outputHeight = Math.max(1, height - 5 - settingsHeight - inputHeight);
1969
+ const isInputActive = mode === "input" && !focusedField && !pickerOpen;
1970
+ const lines = execution?.lines ?? [];
1971
+ const hasPendingTools = (execution?.pendingToolCount ?? 0) > 0;
1972
+ const start = Math.max(0, lines.length - outputHeight);
1973
+ const visibleLines = lines.slice(start, start + outputHeight);
1974
+ let statusLabel = "";
1975
+ if (execution) {
1976
+ statusLabel = ` [${execution.status}]`;
1977
+ }
1978
+ const hint = sessionType === "playground" ? "Describe what you want to build or explore" : "Describe the plan you want to generate";
1979
+ return /* @__PURE__ */ jsx11(Panel, { title: `${title}${statusLabel}`, isFocused: true, height, children: /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1980
+ /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", height: outputHeight, children: [
1981
+ visibleLines.length === 0 && !isRunning ? /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
1982
+ " ",
1983
+ hint,
1984
+ ". Type below and press Enter."
1985
+ ] }) : visibleLines.map((line, i) => /* @__PURE__ */ jsx11(Text12, { color: lineColor2(line), dimColor: line.startsWith("[Tool Call]"), wrap: "truncate", children: truncate(line, 200) }, start + i)),
1986
+ hasPendingTools && /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "[Tool Call] " + "\xB7".repeat(execution.pendingToolCount) }),
1987
+ isRunning && !hasPendingTools && /* @__PURE__ */ jsx11(Spinner, { label: "Running..." })
1988
+ ] }),
1989
+ lines.length > outputHeight && /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
1990
+ " [",
1991
+ start + 1,
1992
+ "-",
1993
+ Math.min(start + outputHeight, lines.length),
1994
+ "/",
1995
+ lines.length,
1996
+ "]"
1997
+ ] }),
1998
+ /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
1999
+ /* @__PURE__ */ jsxs10(Box9, { gap: 2, children: [
2000
+ /* @__PURE__ */ jsxs10(Box9, { children: [
2001
+ /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "Machine: " }),
2002
+ /* @__PURE__ */ jsxs10(
2003
+ Text12,
2004
+ {
2005
+ color: focusedField === "machine" ? "cyan" : void 0,
2006
+ bold: focusedField === "machine",
2007
+ inverse: focusedField === "machine",
2008
+ children: [
2009
+ " ",
2010
+ machineLabel(machineName, machineId),
2011
+ " "
2012
+ ]
2013
+ }
2014
+ )
2015
+ ] }),
2016
+ /* @__PURE__ */ jsxs10(Box9, { children: [
2017
+ /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "Dir: " }),
2018
+ /* @__PURE__ */ jsxs10(
2019
+ Text12,
2020
+ {
2021
+ color: focusedField === "workdir" ? "cyan" : void 0,
2022
+ bold: focusedField === "workdir",
2023
+ inverse: focusedField === "workdir",
2024
+ children: [
2025
+ " ",
2026
+ truncate(workingDirectory, 60),
2027
+ " "
2028
+ ]
2029
+ }
2030
+ )
2031
+ ] }),
2032
+ !focusedField && /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: " (\u2191 to change settings)" }),
2033
+ focusedField && !pickerOpen && /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: " (Enter to edit, \u2193 to input)" })
2034
+ ] }),
2035
+ pickerOpen && focusedField === "machine" && /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", marginLeft: 2, children: connectedMachines.length === 0 ? /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: " No connected machines" }) : connectedMachines.slice(0, 5).map((m) => /* @__PURE__ */ jsxs10(
2036
+ Text12,
2037
+ {
2038
+ color: m.id === machineId ? "green" : void 0,
2039
+ children: [
2040
+ m.id === machineId ? "\u25CF " : " ",
2041
+ m.name || m.id.slice(0, 12),
2042
+ " (",
2043
+ m.hostname,
2044
+ ", ",
2045
+ m.platform,
2046
+ ")"
2047
+ ]
2048
+ },
2049
+ m.id
2050
+ )) }),
2051
+ pickerOpen && focusedField === "workdir" && /* @__PURE__ */ jsxs10(Box9, { marginLeft: 2, children: [
2052
+ /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "Path: " }),
2053
+ /* @__PURE__ */ jsx11(
2054
+ TextInput2,
2055
+ {
2056
+ defaultValue: workingDirectory,
2057
+ onSubmit: (val) => {
2058
+ if (val.trim()) {
2059
+ useSessionSettingsStore.getState().setWorkingDirectory(val.trim());
2060
+ }
2061
+ useSessionSettingsStore.getState().setPickerOpen(false);
2062
+ }
2063
+ }
2064
+ )
2065
+ ] })
2066
+ ] }),
2067
+ /* @__PURE__ */ jsx11(Box9, { borderStyle: "single", borderColor: isInputActive ? "cyan" : "gray", paddingX: 1, children: isRunning || streaming ? /* @__PURE__ */ jsxs10(Box9, { children: [
2068
+ /* @__PURE__ */ jsx11(Spinner, {}),
2069
+ /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: " Agent is working... (press Esc to return to navigation)" })
2070
+ ] }) : /* @__PURE__ */ jsxs10(Box9, { children: [
2071
+ /* @__PURE__ */ jsx11(Text12, { color: "cyan", children: "> " }),
2072
+ isInputActive ? /* @__PURE__ */ jsx11(
2073
+ TextInput2,
2074
+ {
2075
+ placeholder: hint,
2076
+ onSubmit: (value) => {
2077
+ if (value.trim()) {
2078
+ const msg = value.trim();
2079
+ if (watchingId) {
2080
+ useExecutionStore.getState().appendLine(watchingId, `> ${msg}`);
2081
+ }
2082
+ useChatStore.getState().addMessage("user", msg);
2083
+ onSubmit?.(msg);
2084
+ }
2085
+ }
2086
+ }
2087
+ ) : /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: focusedField ? "Press \u2193 to return to input" : "Press Enter to start typing..." })
2088
+ ] }) })
2089
+ ] }) });
2090
+ }
2091
+
2092
+ // src/tui/components/panels/active-list-panel.tsx
2093
+ import { useState as useState2, useEffect as useEffect3 } from "react";
2094
+ import { Box as Box10, Text as Text13, useInput as useInput2 } from "ink";
2095
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2096
+ var TERMINAL_NODE_STATUSES2 = /* @__PURE__ */ new Set([
2097
+ "completed",
2098
+ "auto_verified",
2099
+ "awaiting_judgment",
2100
+ "awaiting_approval",
2101
+ "pruned"
2102
+ ]);
2103
+ function statusSymbol(status) {
2104
+ if (status === "generating") return "\u2728";
2105
+ if (status === "running") return "\u25B6";
2106
+ if (status === "pending") return "\u25CB";
2107
+ return "\xB7";
2108
+ }
2109
+ function statusColor(status) {
2110
+ if (status === "generating") return "magenta";
2111
+ if (status === "running") return "cyan";
2112
+ if (status === "pending") return "yellow";
2113
+ return "gray";
2114
+ }
2115
+ function elapsedLabel(startedAt) {
2116
+ if (!startedAt) return "";
2117
+ const ms = Date.now() - new Date(startedAt).getTime();
2118
+ if (ms < 0) return "";
2119
+ const s = Math.floor(ms / 1e3);
2120
+ const m = Math.floor(s / 60);
2121
+ if (m > 0) return `${m}m${(s % 60).toString().padStart(2, "0")}s`;
2122
+ return `${s}s`;
2123
+ }
2124
+ function ActiveListPanel({ height }) {
2125
+ const outputs = useExecutionStore((s) => s.outputs);
2126
+ const watchingId = useExecutionStore((s) => s.watchingId);
2127
+ const pendingApproval = useExecutionStore((s) => s.pendingApproval);
2128
+ const mode = useTuiStore((s) => s.mode);
2129
+ const projects = useProjectsStore((s) => s.projects);
2130
+ const planNodes = usePlanStore((s) => s.nodes);
2131
+ const [cursor, setCursor] = useState2(0);
2132
+ const tasksByProject = /* @__PURE__ */ new Map();
2133
+ for (const [id, exec] of outputs) {
2134
+ if (exec.status !== "running" && exec.status !== "pending" && exec.status !== "dispatched") continue;
2135
+ const isPlanSession = exec.nodeId.startsWith("plan-");
2136
+ const isPlayground = exec.nodeId.startsWith("playground-");
2137
+ const isChatSession = exec.nodeId.startsWith("chat-");
2138
+ const node = isPlanSession || isPlayground || isChatSession ? void 0 : planNodes.find((n) => n.id === exec.nodeId);
2139
+ if (node && TERMINAL_NODE_STATUSES2.has(node.status)) continue;
2140
+ let projectId = null;
2141
+ if (node?.projectId) {
2142
+ projectId = node.projectId;
2143
+ } else if (isPlanSession) {
2144
+ projectId = exec.nodeId.replace(/^plan-/, "").replace(/-\d+$/, "");
2145
+ } else if (isPlayground) {
2146
+ const parts = exec.nodeId.replace(/^playground-/, "").split("-");
2147
+ parts.pop();
2148
+ projectId = parts.join("-");
2149
+ } else if (isChatSession) {
2150
+ projectId = exec.nodeId.replace(/^chat-/, "").replace(/-\d+$/, "");
2151
+ }
2152
+ if (!projectId) continue;
2153
+ let title = exec.title;
2154
+ if (!title || title === exec.nodeId) {
2155
+ if (isPlanSession) {
2156
+ title = "Interactive Planning";
2157
+ } else if (isPlayground) {
2158
+ const proj = projects.find((p) => p.id === projectId);
2159
+ title = proj?.name ? `Playground: ${proj.name}` : "Playground Session";
2160
+ } else {
2161
+ title = node?.title ?? `Task ${exec.nodeId.slice(0, 8)}`;
2162
+ }
2163
+ }
2164
+ const task = {
2165
+ nodeId: exec.nodeId,
2166
+ projectId,
2167
+ title,
2168
+ status: isPlanSession ? "generating" : exec.status === "running" ? "running" : "pending",
2169
+ executionId: id,
2170
+ startedAt: exec.startedAt
2171
+ };
2172
+ const list = tasksByProject.get(projectId) ?? [];
2173
+ list.push(task);
2174
+ tasksByProject.set(projectId, list);
2175
+ }
2176
+ for (const node of planNodes) {
2177
+ if (node.status !== "in_progress" && node.status !== "dispatched") continue;
2178
+ if (node.deletedAt) continue;
2179
+ const alreadyCovered = Array.from(outputs.values()).some(
2180
+ (exec) => exec.nodeId === node.id && (exec.status === "running" || exec.status === "pending" || exec.status === "dispatched")
2181
+ );
2182
+ if (alreadyCovered) continue;
2183
+ const task = {
2184
+ nodeId: node.id,
2185
+ projectId: node.projectId,
2186
+ title: node.title,
2187
+ status: node.status === "in_progress" ? "running" : "pending",
2188
+ startedAt: node.executionStartedAt
2189
+ };
2190
+ const list = tasksByProject.get(node.projectId) ?? [];
2191
+ list.push(task);
2192
+ tasksByProject.set(node.projectId, list);
2193
+ }
2194
+ const sortedProjects = Array.from(tasksByProject.entries()).map(([projectId, tasks]) => {
2195
+ const project = projects.find((p) => p.id === projectId);
2196
+ return {
2197
+ projectId,
2198
+ projectName: project?.name ?? `Project ${projectId.slice(0, 8)}`,
2199
+ projectColor: project?.color,
2200
+ tasks
2201
+ };
2202
+ }).sort((a, b) => b.tasks.length - a.tasks.length);
2203
+ const rows = [];
2204
+ for (const group of sortedProjects) {
2205
+ rows.push({
2206
+ type: "header",
2207
+ label: group.projectName,
2208
+ color: "blue",
2209
+ taskCount: group.tasks.length
2210
+ });
2211
+ for (const task of group.tasks) {
2212
+ rows.push({
2213
+ type: "entry",
2214
+ label: task.title,
2215
+ executionId: task.executionId,
2216
+ status: task.status,
2217
+ startedAt: task.startedAt
2218
+ });
2219
+ }
2220
+ }
2221
+ const selectableIndices = rows.map((r, i) => r.type === "entry" ? i : -1).filter((i) => i >= 0);
2222
+ useEffect3(() => {
2223
+ if (selectableIndices.length > 0 && cursor >= selectableIndices.length) {
2224
+ setCursor(selectableIndices.length - 1);
2225
+ }
2226
+ }, [selectableIndices.length, cursor]);
2227
+ useInput2((input, key) => {
2228
+ if (mode !== "normal") return;
2229
+ if (key.upArrow || input === "k") {
2230
+ setCursor((c) => Math.max(0, c - 1));
2231
+ } else if (key.downArrow || input === "j") {
2232
+ setCursor((c) => Math.min(selectableIndices.length - 1, c + 1));
2233
+ } else if (key.return) {
2234
+ const rowIdx = selectableIndices[cursor];
2235
+ const row = rows[rowIdx];
2236
+ if (row?.executionId) {
2237
+ useExecutionStore.getState().setWatching(row.executionId);
2238
+ }
2239
+ }
2240
+ });
2241
+ const totalCount = selectableIndices.length;
2242
+ const visibleHeight = Math.max(1, height - 3);
2243
+ const cursorRowIdx = selectableIndices[cursor] ?? 0;
2244
+ let start = 0;
2245
+ if (rows.length > visibleHeight) {
2246
+ if (cursorRowIdx >= rows.length - visibleHeight) {
2247
+ start = rows.length - visibleHeight;
2248
+ } else {
2249
+ start = Math.max(0, cursorRowIdx - Math.floor(visibleHeight / 2));
2250
+ }
2251
+ }
2252
+ const visibleRows = rows.slice(start, start + visibleHeight);
2253
+ const titleSuffix = totalCount > 0 ? ` (${totalCount})` : "";
2254
+ return /* @__PURE__ */ jsx12(Panel, { title: `ACTIVE${titleSuffix}`, isFocused: true, height, children: rows.length === 0 ? /* @__PURE__ */ jsx12(Text13, { dimColor: true, children: " No active tasks" }) : /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
2255
+ visibleRows.map((row, i) => {
2256
+ const actualIndex = start + i;
2257
+ if (row.type === "header") {
2258
+ return /* @__PURE__ */ jsxs11(Text13, { bold: true, color: "blue", children: [
2259
+ ` ${truncate(row.label, 24)} `,
2260
+ /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
2261
+ "(",
2262
+ row.taskCount,
2263
+ ")"
2264
+ ] })
2265
+ ] }, `hdr-${actualIndex}`);
2266
+ }
2267
+ const isCursor = actualIndex === cursorRowIdx;
2268
+ const isWatched = row.executionId === watchingId;
2269
+ const hasApproval = pendingApproval?.taskId === row.executionId;
2270
+ const elapsed = elapsedLabel(row.startedAt);
2271
+ return /* @__PURE__ */ jsxs11(Box10, { children: [
2272
+ /* @__PURE__ */ jsxs11(
2273
+ Text13,
2274
+ {
2275
+ color: hasApproval ? "yellow" : isCursor ? "cyan" : isWatched ? "green" : statusColor(row.status),
2276
+ bold: isCursor,
2277
+ inverse: isCursor,
2278
+ wrap: "truncate",
2279
+ children: [
2280
+ isCursor ? " > " : isWatched ? " * " : " ",
2281
+ hasApproval ? "!" : statusSymbol(row.status),
2282
+ " ",
2283
+ truncate(row.label, 20)
2284
+ ]
2285
+ }
2286
+ ),
2287
+ elapsed && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
2288
+ " ",
2289
+ elapsed
2290
+ ] })
2291
+ ] }, row.executionId ?? `row-${actualIndex}`);
2292
+ }),
2293
+ rows.length > visibleHeight && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
2294
+ " [",
2295
+ start + 1,
2296
+ "-",
2297
+ Math.min(start + visibleHeight, rows.length),
2298
+ "/",
2299
+ rows.length,
2300
+ "]"
2301
+ ] })
2302
+ ] }) });
2303
+ }
2304
+
2305
+ // src/tui/components/panels/detail-overlay.tsx
2306
+ import "react";
2307
+ import { Box as Box11, Text as Text14 } from "ink";
2308
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1012
2309
  function DetailOverlay() {
1013
2310
  const showDetail = useTuiStore((s) => s.showDetail);
1014
2311
  const detailType = useTuiStore((s) => s.detailType);
1015
2312
  const detailId = useTuiStore((s) => s.detailId);
1016
2313
  if (!showDetail || !detailType || !detailId) return null;
1017
- return /* @__PURE__ */ jsxs10(
1018
- Box9,
2314
+ return /* @__PURE__ */ jsxs12(
2315
+ Box11,
1019
2316
  {
1020
2317
  flexDirection: "column",
1021
2318
  borderStyle: "round",
@@ -1023,124 +2320,221 @@ function DetailOverlay() {
1023
2320
  paddingX: 2,
1024
2321
  paddingY: 1,
1025
2322
  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" })
2323
+ detailType === "project" && /* @__PURE__ */ jsx13(ProjectDetail, { id: detailId }),
2324
+ detailType === "node" && /* @__PURE__ */ jsx13(NodeDetail, { id: detailId }),
2325
+ detailType === "machine" && /* @__PURE__ */ jsx13(MachineDetail, { id: detailId }),
2326
+ /* @__PURE__ */ jsx13(Text14, { dimColor: true, children: "Press Esc to close" })
1030
2327
  ]
1031
2328
  }
1032
2329
  );
1033
2330
  }
1034
2331
  function ProjectDetail({ id }) {
1035
2332
  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: " " })
2333
+ if (!project) return /* @__PURE__ */ jsx13(Text14, { color: "red", children: "Project not found" });
2334
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
2335
+ /* @__PURE__ */ jsx13(Text14, { bold: true, color: "cyan", children: project.name }),
2336
+ /* @__PURE__ */ jsx13(Text14, { children: " " }),
2337
+ /* @__PURE__ */ jsx13(Field, { label: "ID", value: project.id }),
2338
+ /* @__PURE__ */ jsx13(Field, { label: "Status", value: project.status, color: getStatusColor(project.status) }),
2339
+ /* @__PURE__ */ jsx13(Field, { label: "Description", value: project.description || "\u2014" }),
2340
+ /* @__PURE__ */ jsx13(Field, { label: "Health", value: project.health ?? "\u2014" }),
2341
+ /* @__PURE__ */ jsx13(Field, { label: "Progress", value: `${project.progress}%` }),
2342
+ /* @__PURE__ */ jsx13(Field, { label: "Working Dir", value: project.workingDirectory ?? "\u2014" }),
2343
+ /* @__PURE__ */ jsx13(Field, { label: "Repository", value: project.repository ?? "\u2014" }),
2344
+ /* @__PURE__ */ jsx13(Field, { label: "Delivery", value: project.deliveryMode ?? "\u2014" }),
2345
+ /* @__PURE__ */ jsx13(Field, { label: "Start Date", value: project.startDate ?? "\u2014" }),
2346
+ /* @__PURE__ */ jsx13(Field, { label: "Target Date", value: project.targetDate ?? "\u2014" }),
2347
+ /* @__PURE__ */ jsx13(Field, { label: "Created", value: formatRelativeTime(project.createdAt) }),
2348
+ /* @__PURE__ */ jsx13(Field, { label: "Updated", value: formatRelativeTime(project.updatedAt) }),
2349
+ /* @__PURE__ */ jsx13(Text14, { children: " " })
1053
2350
  ] });
1054
2351
  }
1055
2352
  function NodeDetail({ id }) {
1056
2353
  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: " " })
2354
+ if (!node) return /* @__PURE__ */ jsx13(Text14, { color: "red", children: "Node not found" });
2355
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
2356
+ /* @__PURE__ */ jsx13(Text14, { bold: true, color: "cyan", children: node.title }),
2357
+ /* @__PURE__ */ jsx13(Text14, { children: " " }),
2358
+ /* @__PURE__ */ jsx13(Field, { label: "ID", value: node.id }),
2359
+ /* @__PURE__ */ jsx13(Field, { label: "Type", value: node.type }),
2360
+ /* @__PURE__ */ jsx13(Field, { label: "Status", value: node.status, color: getStatusColor(node.status) }),
2361
+ /* @__PURE__ */ jsx13(Field, { label: "Description", value: node.description || "\u2014" }),
2362
+ /* @__PURE__ */ jsx13(Field, { label: "Priority", value: node.priority ?? "\u2014" }),
2363
+ /* @__PURE__ */ jsx13(Field, { label: "Estimate", value: node.estimate ?? "\u2014" }),
2364
+ /* @__PURE__ */ jsx13(Field, { label: "Start Date", value: node.startDate ?? "\u2014" }),
2365
+ /* @__PURE__ */ jsx13(Field, { label: "End Date", value: node.endDate ?? "\u2014" }),
2366
+ /* @__PURE__ */ jsx13(Field, { label: "Due Date", value: node.dueDate ?? "\u2014" }),
2367
+ /* @__PURE__ */ jsx13(Field, { label: "Branch", value: node.branchName ?? "\u2014" }),
2368
+ /* @__PURE__ */ jsx13(Field, { label: "PR URL", value: node.prUrl ?? "\u2014" }),
2369
+ /* @__PURE__ */ jsx13(Field, { label: "Execution ID", value: node.executionId ?? "\u2014" }),
2370
+ /* @__PURE__ */ jsx13(Field, { label: "Exec Started", value: node.executionStartedAt ? formatRelativeTime(node.executionStartedAt) : "\u2014" }),
2371
+ /* @__PURE__ */ jsx13(Field, { label: "Exec Completed", value: node.executionCompletedAt ? formatRelativeTime(node.executionCompletedAt) : "\u2014" }),
2372
+ /* @__PURE__ */ jsx13(Text14, { children: " " })
1076
2373
  ] });
1077
2374
  }
1078
2375
  function MachineDetail({ id }) {
1079
2376
  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: " " })
2377
+ if (!machine) return /* @__PURE__ */ jsx13(Text14, { color: "red", children: "Machine not found" });
2378
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
2379
+ /* @__PURE__ */ jsx13(Text14, { bold: true, color: "cyan", children: machine.name }),
2380
+ /* @__PURE__ */ jsx13(Text14, { children: " " }),
2381
+ /* @__PURE__ */ jsx13(Field, { label: "ID", value: machine.id }),
2382
+ /* @__PURE__ */ jsx13(Field, { label: "Hostname", value: machine.hostname }),
2383
+ /* @__PURE__ */ jsx13(Field, { label: "Platform", value: machine.platform }),
2384
+ /* @__PURE__ */ jsx13(Field, { label: "Env Type", value: machine.environmentType }),
2385
+ /* @__PURE__ */ jsx13(Field, { label: "Connected", value: machine.isConnected ? "Yes" : "No", color: machine.isConnected ? "green" : "red" }),
2386
+ /* @__PURE__ */ jsx13(Field, { label: "Providers", value: machine.providers.join(", ") || "\u2014" }),
2387
+ /* @__PURE__ */ jsx13(Field, { label: "Registered", value: formatRelativeTime(machine.registeredAt) }),
2388
+ /* @__PURE__ */ jsx13(Field, { label: "Last Seen", value: formatRelativeTime(machine.lastSeenAt) }),
2389
+ /* @__PURE__ */ jsx13(Text14, { children: " " })
1093
2390
  ] });
1094
2391
  }
1095
2392
  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 })
2393
+ return /* @__PURE__ */ jsxs12(Box11, { children: [
2394
+ /* @__PURE__ */ jsx13(Text14, { dimColor: true, children: label.padEnd(16) }),
2395
+ color ? /* @__PURE__ */ jsx13(Text14, { color, children: value }) : /* @__PURE__ */ jsx13(Text14, { children: value })
1099
2396
  ] });
1100
2397
  }
1101
2398
 
2399
+ // src/tui/components/shared/approval-dialog.tsx
2400
+ import { useState as useState3 } from "react";
2401
+ import { Box as Box12, Text as Text15, useInput as useInput3 } from "ink";
2402
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2403
+ function ApprovalDialog({ question, options, onSelect, onDismiss }) {
2404
+ const [selectedIndex, setSelectedIndex] = useState3(0);
2405
+ useInput3((input, key) => {
2406
+ if (key.escape) {
2407
+ onDismiss();
2408
+ return;
2409
+ }
2410
+ if (key.return) {
2411
+ onSelect(selectedIndex);
2412
+ return;
2413
+ }
2414
+ if (key.upArrow || input === "k") {
2415
+ setSelectedIndex((i) => Math.max(0, i - 1));
2416
+ }
2417
+ if (key.downArrow || input === "j") {
2418
+ setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
2419
+ }
2420
+ const num = parseInt(input, 10);
2421
+ if (num >= 1 && num <= options.length) {
2422
+ onSelect(num - 1);
2423
+ }
2424
+ });
2425
+ return /* @__PURE__ */ jsxs13(
2426
+ Box12,
2427
+ {
2428
+ flexDirection: "column",
2429
+ borderStyle: "round",
2430
+ borderColor: "yellow",
2431
+ paddingX: 1,
2432
+ paddingY: 0,
2433
+ children: [
2434
+ /* @__PURE__ */ jsx14(Text15, { color: "yellow", bold: true, children: "Approval Required" }),
2435
+ /* @__PURE__ */ jsx14(Text15, { wrap: "wrap", children: question }),
2436
+ /* @__PURE__ */ jsx14(Box12, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsx14(Box12, { children: /* @__PURE__ */ jsxs13(Text15, { color: i === selectedIndex ? "cyan" : "white", children: [
2437
+ i === selectedIndex ? "\u25B8 " : " ",
2438
+ "[",
2439
+ i + 1,
2440
+ "] ",
2441
+ opt
2442
+ ] }) }, i)) }),
2443
+ /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text15, { dimColor: true, children: [
2444
+ "\u2191\u2193/jk navigate \u2022 Enter select \u2022 1-",
2445
+ options.length,
2446
+ " quick select \u2022 Esc dismiss"
2447
+ ] }) })
2448
+ ]
2449
+ }
2450
+ );
2451
+ }
2452
+
1102
2453
  // src/tui/components/layout/main-layout.tsx
1103
- import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1104
- function MainLayout() {
2454
+ import { Fragment, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2455
+ function MainLayout({ onSessionMessage }) {
1105
2456
  const showHelp = useTuiStore((s) => s.showHelp);
1106
2457
  const showDetail = useTuiStore((s) => s.showDetail);
1107
- const { stdout } = useStdout();
2458
+ const showChat = useTuiStore((s) => s.showChat);
2459
+ const activeView = useTuiStore((s) => s.activeView);
2460
+ const mode = useTuiStore((s) => s.mode);
2461
+ const searchOpen = useSearchStore((s) => s.isOpen);
2462
+ const pendingApproval = useExecutionStore((s) => s.pendingApproval);
2463
+ const { stdout } = useStdout3();
1108
2464
  const termHeight = stdout?.rows ?? 24;
1109
2465
  const termWidth = stdout?.columns ?? 80;
1110
- const topRowHeight = Math.floor((termHeight - 4) / 2);
1111
- const bottomRowHeight = termHeight - 4 - topRowHeight;
2466
+ const panelOpen = searchOpen || mode === "palette";
2467
+ const bottomPanelHeight = panelOpen ? Math.floor(termHeight / 2) : 1;
2468
+ const contentHeight = termHeight - 2 - bottomPanelHeight;
1112
2469
  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, {})
2470
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", width: termWidth, height: termHeight, children: [
2471
+ /* @__PURE__ */ jsx15(StatusBar, {}),
2472
+ /* @__PURE__ */ jsx15(HelpOverlay, {}),
2473
+ /* @__PURE__ */ jsx15(CommandLine, { height: 1 })
1117
2474
  ] });
1118
2475
  }
1119
2476
  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, {})
2477
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", width: termWidth, height: termHeight, children: [
2478
+ /* @__PURE__ */ jsx15(StatusBar, {}),
2479
+ /* @__PURE__ */ jsx15(DetailOverlay, {}),
2480
+ /* @__PURE__ */ jsx15(CommandLine, { height: 1 })
1124
2481
  ] });
1125
2482
  }
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, {})
2483
+ const approvalOverlay = pendingApproval ? /* @__PURE__ */ jsx15(Box13, { position: "absolute", marginTop: 4, marginLeft: Math.floor(termWidth / 4), children: /* @__PURE__ */ jsx15(
2484
+ ApprovalDialog,
2485
+ {
2486
+ question: pendingApproval.question,
2487
+ options: pendingApproval.options,
2488
+ onSelect: (index) => {
2489
+ useExecutionStore.getState().setPendingApproval(null);
2490
+ void index;
2491
+ },
2492
+ onDismiss: () => {
2493
+ useExecutionStore.getState().setPendingApproval(null);
2494
+ }
2495
+ }
2496
+ ) }) : null;
2497
+ let content;
2498
+ if (activeView === "plan-gen") {
2499
+ content = /* @__PURE__ */ jsx15(Box13, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx15(Box13, { flexGrow: 1, children: /* @__PURE__ */ jsx15(SessionPanel, { height: contentHeight, title: "PLAN GENERATION", sessionType: "plan-generate", onSubmit: onSessionMessage }) }) });
2500
+ } else if (activeView === "projects") {
2501
+ content = /* @__PURE__ */ jsxs14(Box13, { flexDirection: "row", height: contentHeight, children: [
2502
+ /* @__PURE__ */ jsx15(Box13, { width: "40%", children: /* @__PURE__ */ jsx15(ProjectsPanel, { height: contentHeight }) }),
2503
+ /* @__PURE__ */ jsx15(Box13, { flexGrow: 1, children: /* @__PURE__ */ jsx15(PlanPanel, { height: contentHeight }) })
2504
+ ] });
2505
+ } else if (activeView === "playground") {
2506
+ content = /* @__PURE__ */ jsx15(Box13, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx15(Box13, { flexGrow: 1, children: /* @__PURE__ */ jsx15(SessionPanel, { height: contentHeight, title: "PLAYGROUND", sessionType: "playground", onSubmit: onSessionMessage }) }) });
2507
+ } else if (activeView === "active") {
2508
+ content = /* @__PURE__ */ jsxs14(Box13, { flexDirection: "row", height: contentHeight, children: [
2509
+ /* @__PURE__ */ jsx15(Box13, { width: "25%", children: /* @__PURE__ */ jsx15(ActiveListPanel, { height: contentHeight }) }),
2510
+ /* @__PURE__ */ jsx15(Box13, { flexGrow: 1, children: /* @__PURE__ */ jsx15(OutputPanel, { height: contentHeight }) })
2511
+ ] });
2512
+ } else {
2513
+ const topRowHeight = Math.floor(contentHeight / 2);
2514
+ const bottomRowHeight = contentHeight - topRowHeight;
2515
+ content = /* @__PURE__ */ jsxs14(Fragment, { children: [
2516
+ /* @__PURE__ */ jsxs14(Box13, { flexDirection: "row", height: topRowHeight, children: [
2517
+ /* @__PURE__ */ jsx15(Box13, { width: "30%", children: /* @__PURE__ */ jsx15(ProjectsPanel, { height: topRowHeight }) }),
2518
+ /* @__PURE__ */ jsx15(Box13, { flexGrow: 1, children: /* @__PURE__ */ jsx15(PlanPanel, { height: topRowHeight }) })
2519
+ ] }),
2520
+ /* @__PURE__ */ jsxs14(Box13, { flexDirection: "row", height: bottomRowHeight, children: [
2521
+ /* @__PURE__ */ jsx15(Box13, { width: "30%", children: /* @__PURE__ */ jsx15(MachinesPanel, { height: bottomRowHeight }) }),
2522
+ /* @__PURE__ */ jsx15(Box13, { flexGrow: 1, children: showChat ? /* @__PURE__ */ jsx15(ChatPanel, { height: bottomRowHeight }) : /* @__PURE__ */ jsx15(OutputPanel, { height: bottomRowHeight }) })
2523
+ ] })
2524
+ ] });
2525
+ }
2526
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", width: termWidth, height: termHeight, children: [
2527
+ /* @__PURE__ */ jsx15(StatusBar, {}),
2528
+ content,
2529
+ /* @__PURE__ */ jsx15(SearchOverlay, {}),
2530
+ approvalOverlay,
2531
+ /* @__PURE__ */ jsx15(CommandLine, { height: bottomPanelHeight })
1138
2532
  ] });
1139
2533
  }
1140
2534
 
1141
2535
  // src/tui/hooks/use-vim-mode.ts
1142
2536
  import { useCallback, useRef } from "react";
1143
- import { useInput as useInput2, useApp } from "ink";
2537
+ import { useInput as useInput4, useApp } from "ink";
1144
2538
 
1145
2539
  // src/tui/lib/vim-state-machine.ts
1146
2540
  function initialVimState() {
@@ -1170,92 +2564,119 @@ function vimReducer(state, action) {
1170
2564
  { type: "search", value: state.searchQuery }
1171
2565
  ];
1172
2566
  case "key":
1173
- return handleKey(state, action.key, action.ctrl);
2567
+ return handleKey(state, action.key, action.ctrl, action.meta);
1174
2568
  }
1175
2569
  }
1176
- function handleKey(state, key, ctrl) {
2570
+ function handleKey(state, key, ctrl, meta) {
1177
2571
  if (key === "escape") {
1178
2572
  return [{ ...state, mode: "normal", pendingKeys: "", commandBuffer: "", searchQuery: "" }, { type: "none" }];
1179
2573
  }
1180
2574
  switch (state.mode) {
1181
2575
  case "normal":
1182
- return handleNormalMode(state, key, ctrl);
1183
- case "command":
1184
- return handleCommandMode(state, key);
2576
+ return handleNormalMode(state, key, ctrl, meta);
2577
+ case "palette":
2578
+ return handlePaletteMode(state, key);
1185
2579
  case "search":
1186
2580
  return handleSearchMode(state, key);
1187
- case "insert":
1188
- return handleInsertMode(state, key);
2581
+ case "input":
2582
+ return handleInputMode(state, key, ctrl);
1189
2583
  }
1190
2584
  }
1191
- function handleNormalMode(state, key, ctrl) {
2585
+ function handleNormalMode(state, key, ctrl, meta) {
1192
2586
  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" }];
2587
+ switch (key) {
2588
+ case "p":
2589
+ return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
2590
+ case "f":
2591
+ return [state, { type: "search" }];
2592
+ case "d":
2593
+ return [state, { type: "cancel" }];
2594
+ case "c":
2595
+ return [state, { type: "quit" }];
2596
+ case "r":
2597
+ return [state, { type: "refresh" }];
2598
+ default:
2599
+ return [state, { type: "none" }];
2600
+ }
1197
2601
  }
1198
- if (state.pendingKeys === "g") {
1199
- if (key === "g") {
1200
- return [{ ...state, pendingKeys: "" }, { type: "scroll", direction: "top" }];
2602
+ if (meta) {
2603
+ switch (key) {
2604
+ case "x":
2605
+ return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
2606
+ default:
2607
+ return [state, { type: "none" }];
1201
2608
  }
1202
- return [{ ...state, pendingKeys: "" }, { type: "none" }];
1203
2609
  }
1204
2610
  switch (key) {
1205
- // Navigation
2611
+ // Arrow navigation (primary — no j/k needed)
2612
+ case "up":
2613
+ return [state, { type: "scroll", direction: "up" }];
2614
+ case "down":
2615
+ return [state, { type: "scroll", direction: "down" }];
2616
+ case "left":
2617
+ return [state, { type: "focus", direction: "left" }];
2618
+ case "right":
2619
+ return [state, { type: "focus", direction: "right" }];
2620
+ // Also keep j/k/h/l as secondary navigation for power users
1206
2621
  case "j":
1207
- case "return":
1208
- if (key === "j") return [state, { type: "scroll", direction: "down" }];
1209
- return [state, { type: "select" }];
2622
+ return [state, { type: "scroll", direction: "down" }];
1210
2623
  case "k":
1211
2624
  return [state, { type: "scroll", direction: "up" }];
1212
2625
  case "h":
1213
2626
  return [state, { type: "focus", direction: "left" }];
1214
2627
  case "l":
1215
2628
  return [state, { type: "focus", direction: "right" }];
1216
- // Panel jump
2629
+ // Selection
2630
+ case "return":
2631
+ return [state, { type: "select" }];
2632
+ case " ":
2633
+ return [state, { type: "select" }];
2634
+ // Page navigation
2635
+ case "pageup":
2636
+ return [state, { type: "scroll", direction: "page_up" }];
2637
+ case "pagedown":
2638
+ return [state, { type: "scroll", direction: "page_down" }];
2639
+ case "home":
2640
+ return [state, { type: "scroll", direction: "top" }];
2641
+ case "end":
2642
+ return [state, { type: "scroll", direction: "bottom" }];
2643
+ // Tab cycles panels
2644
+ case "tab":
2645
+ return [state, { type: "focus", direction: "right" }];
2646
+ // View switch by number
1217
2647
  case "1":
1218
- return [state, { type: "focus", panel: 0 }];
2648
+ return [state, { type: "view", value: "dashboard" }];
1219
2649
  case "2":
1220
- return [state, { type: "focus", panel: 1 }];
2650
+ return [state, { type: "view", value: "plan-gen" }];
1221
2651
  case "3":
1222
- return [state, { type: "focus", panel: 2 }];
2652
+ return [state, { type: "view", value: "projects" }];
1223
2653
  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
2654
+ return [state, { type: "view", value: "playground" }];
2655
+ case "5":
2656
+ return [state, { type: "view", value: "active" }];
2657
+ // Function-key style shortcuts (single letter, no prefix needed)
1240
2658
  case "d":
1241
2659
  return [state, { type: "dispatch" }];
1242
- case "c":
2660
+ case "x":
1243
2661
  return [state, { type: "cancel" }];
1244
- case "r":
1245
- return [state, { type: "refresh" }];
1246
2662
  case "q":
1247
2663
  return [state, { type: "quit" }];
1248
2664
  case "?":
1249
2665
  return [state, { type: "help" }];
2666
+ case "/":
2667
+ return [state, { type: "search" }];
2668
+ // Legacy `:` still works — enters palette mode
2669
+ case ":":
2670
+ return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
1250
2671
  default:
1251
2672
  return [state, { type: "none" }];
1252
2673
  }
1253
2674
  }
1254
- function handleCommandMode(state, key) {
2675
+ function handlePaletteMode(state, key) {
1255
2676
  if (key === "return") {
1256
2677
  return [
1257
2678
  { ...state, mode: "normal" },
1258
- { type: "command", value: state.commandBuffer }
2679
+ { type: "command", value: "__palette_select__" }
1259
2680
  ];
1260
2681
  }
1261
2682
  if (key === "backspace" || key === "delete") {
@@ -1265,8 +2686,14 @@ function handleCommandMode(state, key) {
1265
2686
  }
1266
2687
  return [{ ...state, commandBuffer: newBuffer }, { type: "none" }];
1267
2688
  }
2689
+ if (key === "up") {
2690
+ return [state, { type: "scroll", direction: "up" }];
2691
+ }
2692
+ if (key === "down") {
2693
+ return [state, { type: "scroll", direction: "down" }];
2694
+ }
1268
2695
  if (key === "tab") {
1269
- return [state, { type: "command", value: `__autocomplete__${state.commandBuffer}` }];
2696
+ return [state, { type: "scroll", direction: "down" }];
1270
2697
  }
1271
2698
  if (key.length === 1) {
1272
2699
  return [{ ...state, commandBuffer: state.commandBuffer + key }, { type: "none" }];
@@ -1293,7 +2720,10 @@ function handleSearchMode(state, key) {
1293
2720
  }
1294
2721
  return [state, { type: "none" }];
1295
2722
  }
1296
- function handleInsertMode(state, _) {
2723
+ function handleInputMode(state, key, ctrl) {
2724
+ if (ctrl && key === "d") {
2725
+ return [{ ...state, mode: "normal" }, { type: "cancel" }];
2726
+ }
1297
2727
  return [state, { type: "none" }];
1298
2728
  }
1299
2729
 
@@ -1306,6 +2736,16 @@ function useVimMode(callbacks = {}) {
1306
2736
  (effect) => {
1307
2737
  switch (effect.type) {
1308
2738
  case "scroll":
2739
+ if (vimState.current.mode === "palette") {
2740
+ const filtered = getFilteredPaletteCommands(vimState.current.commandBuffer);
2741
+ const idx = store.paletteIndex;
2742
+ if (effect.direction === "up") {
2743
+ store.setPaletteIndex(Math.max(0, idx - 1));
2744
+ } else if (effect.direction === "down") {
2745
+ store.setPaletteIndex(Math.min(filtered.length - 1, idx + 1));
2746
+ }
2747
+ break;
2748
+ }
1309
2749
  switch (effect.direction) {
1310
2750
  case "up":
1311
2751
  store.scrollUp();
@@ -1326,6 +2766,13 @@ function useVimMode(callbacks = {}) {
1326
2766
  store.pageDown();
1327
2767
  break;
1328
2768
  }
2769
+ if (store.focusedPanel === "projects") {
2770
+ const sorted = getVisibleProjects(useProjectsStore.getState().projects);
2771
+ const idx = useTuiStore.getState().scrollIndex.projects;
2772
+ if (sorted[idx]) {
2773
+ useTuiStore.getState().setSelectedProject(sorted[idx].id);
2774
+ }
2775
+ }
1329
2776
  break;
1330
2777
  case "focus":
1331
2778
  if (effect.panel != null) {
@@ -1336,19 +2783,33 @@ function useVimMode(callbacks = {}) {
1336
2783
  store.focusNext();
1337
2784
  }
1338
2785
  break;
1339
- case "select":
1340
- callbacks.onSelect?.();
2786
+ case "select": {
2787
+ const view = store.activeView;
2788
+ if (view === "playground" || view === "plan-gen" || view === "active") {
2789
+ vimState.current = { ...vimState.current, mode: "input" };
2790
+ store.setMode("input");
2791
+ } else {
2792
+ callbacks.onSelect?.();
2793
+ }
2794
+ break;
2795
+ }
2796
+ case "palette":
1341
2797
  break;
1342
2798
  case "command":
1343
- if (effect.value?.startsWith("__autocomplete__")) {
2799
+ if (effect.value === "__palette_select__") {
2800
+ const filtered = getFilteredPaletteCommands(vimState.current.commandBuffer);
2801
+ const selected = filtered[store.paletteIndex];
2802
+ if (selected) {
2803
+ const cmd = selected.usage?.startsWith("resume:") ? selected.usage : selected.name;
2804
+ callbacks.onCommand?.(cmd);
2805
+ }
2806
+ } else if (effect.value?.startsWith("__autocomplete__")) {
1344
2807
  } else if (effect.value) {
1345
2808
  callbacks.onCommand?.(effect.value);
1346
2809
  }
1347
2810
  break;
1348
2811
  case "search":
1349
- if (effect.value) {
1350
- callbacks.onSearch?.(effect.value);
1351
- }
2812
+ useSearchStore.getState().open();
1352
2813
  break;
1353
2814
  case "dispatch":
1354
2815
  callbacks.onDispatch?.();
@@ -1361,19 +2822,30 @@ function useVimMode(callbacks = {}) {
1361
2822
  break;
1362
2823
  case "quit":
1363
2824
  exit();
2825
+ setTimeout(() => process.exit(0), 100);
1364
2826
  break;
1365
2827
  case "help":
1366
2828
  store.toggleHelp();
1367
2829
  break;
2830
+ case "chat":
2831
+ store.toggleChat();
2832
+ break;
2833
+ case "view":
2834
+ if (effect.value === "dashboard" || effect.value === "plan-gen" || effect.value === "projects" || effect.value === "playground" || effect.value === "active") {
2835
+ store.setActiveView(effect.value);
2836
+ }
2837
+ break;
1368
2838
  case "none":
1369
2839
  break;
1370
2840
  }
1371
2841
  },
1372
2842
  [store, callbacks, exit]
1373
2843
  );
1374
- useInput2((input, key) => {
1375
- if (store.showHelp || store.showSearch || store.showDetail) {
2844
+ useInput4((input, key) => {
2845
+ const searchOpen = useSearchStore.getState().isOpen;
2846
+ if (store.showHelp || store.showSearch || store.showDetail || searchOpen) {
1376
2847
  if (key.escape) {
2848
+ if (searchOpen) useSearchStore.getState().close();
1377
2849
  store.closeOverlays();
1378
2850
  vimState.current = initialVimState();
1379
2851
  store.setMode("normal");
@@ -1383,20 +2855,80 @@ function useVimMode(callbacks = {}) {
1383
2855
  }
1384
2856
  return;
1385
2857
  }
2858
+ const isSessionView = store.activeView === "playground" || store.activeView === "plan-gen";
2859
+ if (isSessionView && vimState.current.mode === "input") {
2860
+ const settings = useSessionSettingsStore.getState();
2861
+ if (key.escape) {
2862
+ if (settings.pickerOpen) {
2863
+ settings.setPickerOpen(false);
2864
+ return;
2865
+ }
2866
+ if (settings.focusedField) {
2867
+ settings.setFocusedField(null);
2868
+ return;
2869
+ }
2870
+ }
2871
+ if (settings.pickerOpen && settings.focusedField === "machine") {
2872
+ const machines = useMachinesStore.getState().machines.filter((m) => m.isConnected);
2873
+ const currentIdx = machines.findIndex((m) => m.id === settings.machineId);
2874
+ if (key.upArrow) {
2875
+ const newIdx = Math.max(0, currentIdx - 1);
2876
+ if (machines[newIdx]) settings.setMachine(machines[newIdx].id, machines[newIdx].name);
2877
+ return;
2878
+ }
2879
+ if (key.downArrow) {
2880
+ const newIdx = Math.min(machines.length - 1, currentIdx + 1);
2881
+ if (machines[newIdx]) settings.setMachine(machines[newIdx].id, machines[newIdx].name);
2882
+ return;
2883
+ }
2884
+ if (key.return) {
2885
+ settings.setPickerOpen(false);
2886
+ return;
2887
+ }
2888
+ return;
2889
+ }
2890
+ if (settings.pickerOpen && settings.focusedField === "workdir") {
2891
+ return;
2892
+ }
2893
+ if (settings.focusedField) {
2894
+ if (key.return) {
2895
+ settings.setPickerOpen(true);
2896
+ return;
2897
+ }
2898
+ if (key.leftArrow || key.rightArrow) {
2899
+ settings.setFocusedField(settings.focusedField === "machine" ? "workdir" : "machine");
2900
+ return;
2901
+ }
2902
+ if (key.downArrow) {
2903
+ settings.setFocusedField(null);
2904
+ return;
2905
+ }
2906
+ if (key.upArrow) return;
2907
+ }
2908
+ if (!settings.focusedField && key.upArrow) {
2909
+ settings.setFocusedField("machine");
2910
+ return;
2911
+ }
2912
+ }
1386
2913
  let keyStr = input;
1387
2914
  if (key.escape) keyStr = "escape";
1388
2915
  else if (key.return) keyStr = "return";
1389
2916
  else if (key.backspace || key.delete) keyStr = "backspace";
1390
2917
  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";
2918
+ else if (key.upArrow) keyStr = "up";
2919
+ else if (key.downArrow) keyStr = "down";
2920
+ else if (key.leftArrow) keyStr = "left";
2921
+ else if (key.rightArrow) keyStr = "right";
2922
+ else if (key.pageUp) keyStr = "pageup";
2923
+ else if (key.pageDown) keyStr = "pagedown";
2924
+ else if (key.home) keyStr = "home";
2925
+ else if (key.end) keyStr = "end";
1395
2926
  const [nextState, effect] = vimReducer(vimState.current, {
1396
2927
  type: "key",
1397
2928
  key: keyStr,
1398
2929
  ctrl: key.ctrl,
1399
- shift: key.shift
2930
+ shift: key.shift,
2931
+ meta: key.meta
1400
2932
  });
1401
2933
  vimState.current = nextState;
1402
2934
  store.setMode(nextState.mode);
@@ -1408,8 +2940,26 @@ function useVimMode(callbacks = {}) {
1408
2940
  }
1409
2941
 
1410
2942
  // src/tui/hooks/use-polling.ts
1411
- import { useEffect as useEffect2, useCallback as useCallback2 } from "react";
1412
- function usePolling(client, intervalMs = 3e4) {
2943
+ import { useEffect as useEffect4, useCallback as useCallback2 } from "react";
2944
+ function deriveTitle(nodeId, exec, projects, planNodes) {
2945
+ const projectName = projects.find((p) => p.id === exec.projectId)?.name;
2946
+ if (nodeId.startsWith("playground-")) {
2947
+ const firstLine = exec.streamText?.split("\n").find((l) => l.trim().length > 0)?.trim();
2948
+ if (firstLine && firstLine.length > 5) {
2949
+ return `Playground: ${firstLine.slice(0, 50)}`;
2950
+ }
2951
+ return `Playground${projectName ? ` \u2014 ${projectName}` : ""}`;
2952
+ }
2953
+ if (nodeId.startsWith("plan-")) {
2954
+ return `Plan${projectName ? ` \u2014 ${projectName}` : ""}`;
2955
+ }
2956
+ const planNode = planNodes.find((n) => n.id === nodeId);
2957
+ if (planNode) {
2958
+ return planNode.title;
2959
+ }
2960
+ return projectName ? `Task \u2014 ${projectName}` : nodeId.slice(0, 30);
2961
+ }
2962
+ function usePolling(client, intervalMs = 1e4) {
1413
2963
  const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
1414
2964
  const loadProjects = useCallback2(async () => {
1415
2965
  useProjectsStore.getState().setLoading(true);
@@ -1429,6 +2979,13 @@ function usePolling(client, intervalMs = 3e4) {
1429
2979
  usePlanStore.getState().setError(err instanceof Error ? err.message : String(err));
1430
2980
  }
1431
2981
  }, [client]);
2982
+ const loadAllPlans = useCallback2(async () => {
2983
+ try {
2984
+ const { nodes, edges } = await client.getFullPlan();
2985
+ usePlanStore.getState().setAllPlans(nodes, edges);
2986
+ } catch {
2987
+ }
2988
+ }, [client]);
1432
2989
  const loadMachines = useCallback2(async () => {
1433
2990
  useMachinesStore.getState().setLoading(true);
1434
2991
  try {
@@ -1439,32 +2996,62 @@ function usePolling(client, intervalMs = 3e4) {
1439
2996
  useMachinesStore.getState().setError(err instanceof Error ? err.message : String(err));
1440
2997
  }
1441
2998
  }, [client]);
2999
+ const loadExecutions = useCallback2(async () => {
3000
+ try {
3001
+ const execMap = await client.getExecutions();
3002
+ const projects = useProjectsStore.getState().projects;
3003
+ const planNodes = usePlanStore.getState().nodes;
3004
+ const entries = Object.values(execMap).map((e) => {
3005
+ const nodeId = e.nodeClientId ?? e.nodeId ?? e.executionId;
3006
+ return {
3007
+ executionId: e.executionId,
3008
+ nodeId,
3009
+ title: deriveTitle(nodeId, e, projects, planNodes),
3010
+ status: e.status,
3011
+ startedAt: e.startedAt
3012
+ };
3013
+ });
3014
+ useExecutionStore.getState().seedHistorical(entries);
3015
+ } catch {
3016
+ }
3017
+ }, [client]);
3018
+ const loadUsage = useCallback2(async () => {
3019
+ try {
3020
+ const history = await client.getUsageHistory(1);
3021
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3022
+ const todayEntry = history.find((d) => d.date === today);
3023
+ useTuiStore.getState().setTodayCost(todayEntry?.totalCostUsd ?? 0);
3024
+ } catch {
3025
+ }
3026
+ }, [client]);
1442
3027
  const refreshAll2 = useCallback2(async () => {
1443
3028
  await Promise.allSettled([
1444
3029
  loadProjects(),
1445
3030
  loadMachines(),
1446
- ...selectedProjectId ? [loadPlan(selectedProjectId)] : []
3031
+ loadExecutions(),
3032
+ loadUsage(),
3033
+ loadAllPlans()
1447
3034
  ]);
1448
- }, [loadProjects, loadMachines, loadPlan, selectedProjectId]);
1449
- useEffect2(() => {
3035
+ }, [loadProjects, loadMachines, loadExecutions, loadUsage, loadAllPlans]);
3036
+ useEffect4(() => {
1450
3037
  refreshAll2();
1451
3038
  }, [refreshAll2]);
1452
- useEffect2(() => {
3039
+ useEffect4(() => {
1453
3040
  if (selectedProjectId) {
1454
- loadPlan(selectedProjectId);
3041
+ usePlanStore.getState().selectProject(selectedProjectId);
1455
3042
  } else {
1456
3043
  usePlanStore.getState().clear();
1457
3044
  }
1458
- }, [selectedProjectId, loadPlan]);
1459
- useEffect2(() => {
3045
+ }, [selectedProjectId]);
3046
+ useEffect4(() => {
1460
3047
  const timer = setInterval(refreshAll2, intervalMs);
1461
3048
  return () => clearInterval(timer);
1462
3049
  }, [refreshAll2, intervalMs]);
1463
- return { refreshAll: refreshAll2, loadProjects, loadPlan, loadMachines };
3050
+ return { refreshAll: refreshAll2, loadProjects, loadPlan, loadAllPlans, loadMachines };
1464
3051
  }
1465
3052
 
1466
3053
  // src/tui/hooks/use-sse-stream.ts
1467
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
3054
+ import { useEffect as useEffect5, useRef as useRef2 } from "react";
1468
3055
 
1469
3056
  // src/tui/sse-client.ts
1470
3057
  var SSEClient = class {
@@ -1549,17 +3136,18 @@ var SSEClient = class {
1549
3136
  };
1550
3137
 
1551
3138
  // src/tui/hooks/use-sse-stream.ts
1552
- function useSSEStream(client) {
3139
+ function useSSEStream(client, onReconnect) {
1553
3140
  const sseRef = useRef2(null);
1554
3141
  const setConnected = useTuiStore((s) => s.setConnected);
1555
3142
  const setMachineCount = useTuiStore((s) => s.setMachineCount);
1556
3143
  const setLastError = useTuiStore((s) => s.setLastError);
1557
- useEffect3(() => {
3144
+ useEffect5(() => {
1558
3145
  const handler = (event) => {
1559
3146
  switch (event.type) {
1560
3147
  case "__connected":
1561
3148
  setConnected(true);
1562
3149
  setLastError(null);
3150
+ onReconnect?.();
1563
3151
  break;
1564
3152
  case "__disconnected":
1565
3153
  setConnected(false);
@@ -1629,11 +3217,37 @@ function useSSEStream(client) {
1629
3217
  useExecutionStore.getState().appendFileChange(taskId, path, action, added, removed);
1630
3218
  break;
1631
3219
  }
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);
3220
+ case "task:session_init": {
3221
+ const taskId = event.data.taskId;
3222
+ const nodeId = event.data.nodeId ?? taskId;
3223
+ const title = event.data.title ?? nodeId;
3224
+ useExecutionStore.getState().initExecution(taskId, nodeId, title);
3225
+ useExecutionStore.getState().setWatching(taskId);
3226
+ break;
3227
+ }
3228
+ case "task:plan_result": {
3229
+ const taskId = event.data.taskId;
3230
+ useExecutionStore.getState().appendLine(taskId, "[plan] Plan generated \u2014 refreshing...");
3231
+ const projectId = event.data.projectId ?? useTuiStore.getState().selectedProjectId;
3232
+ if (projectId) {
3233
+ setTimeout(async () => {
3234
+ try {
3235
+ const { nodes, edges } = await client.getPlan(projectId);
3236
+ usePlanStore.getState().setPlan(projectId, nodes, edges);
3237
+ } catch {
3238
+ }
3239
+ }, 500);
3240
+ }
3241
+ break;
3242
+ }
3243
+ case "task:approval_request": {
3244
+ useExecutionStore.getState().setPendingApproval({
3245
+ requestId: event.data.requestId,
3246
+ question: event.data.question,
3247
+ options: event.data.options,
3248
+ machineId: event.data.machineId,
3249
+ taskId: event.data.taskId
3250
+ });
1637
3251
  break;
1638
3252
  }
1639
3253
  case "heartbeat":
@@ -1651,7 +3265,7 @@ function useSSEStream(client) {
1651
3265
  }
1652
3266
 
1653
3267
  // src/tui/hooks/use-fuzzy-search.ts
1654
- import { useEffect as useEffect4, useCallback as useCallback3 } from "react";
3268
+ import { useEffect as useEffect6, useCallback as useCallback3 } from "react";
1655
3269
  import Fuse from "fuse.js";
1656
3270
  var fuseInstance = null;
1657
3271
  function useFuzzySearch() {
@@ -1659,7 +3273,7 @@ function useFuzzySearch() {
1659
3273
  const nodes = usePlanStore((s) => s.nodes);
1660
3274
  const machines = useMachinesStore((s) => s.machines);
1661
3275
  const { setItems, setResults, query } = useSearchStore();
1662
- useEffect4(() => {
3276
+ useEffect6(() => {
1663
3277
  const items = [
1664
3278
  ...projects.map((p) => ({
1665
3279
  type: "project",
@@ -1698,7 +3312,7 @@ function useFuzzySearch() {
1698
3312
  const results = fuseInstance.search(q, { limit: 20 });
1699
3313
  setResults(results.map((r) => r.item));
1700
3314
  }, [setResults]);
1701
- useEffect4(() => {
3315
+ useEffect6(() => {
1702
3316
  search(query);
1703
3317
  }, [query, search]);
1704
3318
  return { search };
@@ -1707,263 +3321,6 @@ function useFuzzySearch() {
1707
3321
  // src/tui/hooks/use-command-parser.ts
1708
3322
  import { useCallback as useCallback4 } from "react";
1709
3323
 
1710
- // src/tui/commands/handlers.ts
1711
- var handlers = {
1712
- // ── Quit ──
1713
- q: async () => {
1714
- process.exit(0);
1715
- },
1716
- quit: async () => {
1717
- process.exit(0);
1718
- },
1719
- // ── Refresh ──
1720
- r: async (_args, client) => {
1721
- await refreshAll(client);
1722
- },
1723
- refresh: async (_args, client) => {
1724
- await refreshAll(client);
1725
- },
1726
- // ── Project commands ──
1727
- "project list": async (_args, client) => {
1728
- const projects = await client.listProjects();
1729
- useProjectsStore.getState().setProjects(projects);
1730
- },
1731
- "project show": async (args, client) => {
1732
- const id = args[0];
1733
- if (!id) return;
1734
- try {
1735
- const project = await client.resolveProject(id);
1736
- useTuiStore.getState().openDetail("project", project.id);
1737
- } catch {
1738
- useTuiStore.getState().setLastError(`Project not found: ${id}`);
1739
- }
1740
- },
1741
- "project create": async (args, client) => {
1742
- const name = args.join(" ");
1743
- if (!name) {
1744
- useTuiStore.getState().setLastError("Usage: :project create <name>");
1745
- return;
1746
- }
1747
- await client.createProject({ name });
1748
- const projects = await client.listProjects();
1749
- useProjectsStore.getState().setProjects(projects);
1750
- },
1751
- "project delete": async (args, client) => {
1752
- const id = args[0];
1753
- if (!id) return;
1754
- try {
1755
- const project = await client.resolveProject(id);
1756
- await client.deleteProject(project.id);
1757
- const projects = await client.listProjects();
1758
- useProjectsStore.getState().setProjects(projects);
1759
- } catch (err) {
1760
- useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1761
- }
1762
- },
1763
- // ── Plan commands ──
1764
- "plan tree": async (_args, client) => {
1765
- const projectId = useTuiStore.getState().selectedProjectId;
1766
- if (!projectId) {
1767
- useTuiStore.getState().setLastError("No project selected");
1768
- return;
1769
- }
1770
- const { nodes, edges } = await client.getPlan(projectId);
1771
- usePlanStore.getState().setPlan(projectId, nodes, edges);
1772
- useTuiStore.getState().focusPanel("plan");
1773
- },
1774
- "plan create-node": async (args, client) => {
1775
- const projectId = useTuiStore.getState().selectedProjectId;
1776
- if (!projectId) {
1777
- useTuiStore.getState().setLastError("No project selected");
1778
- return;
1779
- }
1780
- const title = args.join(" ");
1781
- if (!title) {
1782
- useTuiStore.getState().setLastError("Usage: :plan create-node <title>");
1783
- return;
1784
- }
1785
- const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1786
- await client.createPlanNode({ id, projectId, title });
1787
- const { nodes, edges } = await client.getPlan(projectId);
1788
- usePlanStore.getState().setPlan(projectId, nodes, edges);
1789
- },
1790
- "plan update-node": async (args, client) => {
1791
- const [nodeId, field, ...rest] = args;
1792
- if (!nodeId || !field) {
1793
- useTuiStore.getState().setLastError("Usage: :plan update-node <nodeId> <field> <value>");
1794
- return;
1795
- }
1796
- const value = rest.join(" ");
1797
- await client.updatePlanNode(nodeId, { [field]: value });
1798
- const projectId = useTuiStore.getState().selectedProjectId;
1799
- if (projectId) {
1800
- const { nodes, edges } = await client.getPlan(projectId);
1801
- usePlanStore.getState().setPlan(projectId, nodes, edges);
1802
- }
1803
- },
1804
- // ── Dispatch ──
1805
- d: async (args, client) => {
1806
- await handlers.dispatch(args, client);
1807
- },
1808
- dispatch: async (args, client) => {
1809
- const nodeId = args[0] ?? useTuiStore.getState().selectedNodeId;
1810
- const projectId = useTuiStore.getState().selectedProjectId;
1811
- if (!nodeId || !projectId) {
1812
- useTuiStore.getState().setLastError("No node/project selected for dispatch");
1813
- return;
1814
- }
1815
- try {
1816
- const response = await client.dispatchTask({ nodeId, projectId });
1817
- const execId = `exec-${Date.now()}`;
1818
- useExecutionStore.getState().initExecution(execId, nodeId);
1819
- useExecutionStore.getState().setWatching(execId);
1820
- useTuiStore.getState().focusPanel("output");
1821
- if (response.body) {
1822
- const reader = response.body.getReader();
1823
- const decoder = new TextDecoder();
1824
- let buffer = "";
1825
- while (true) {
1826
- const { done, value } = await reader.read();
1827
- if (done) break;
1828
- buffer += decoder.decode(value, { stream: true });
1829
- const lines = buffer.split("\n");
1830
- buffer = lines.pop() ?? "";
1831
- for (const line of lines) {
1832
- if (line.startsWith("data: ")) {
1833
- try {
1834
- const event = JSON.parse(line.slice(6));
1835
- const eventType = event.type;
1836
- if (eventType === "text") {
1837
- useExecutionStore.getState().appendText(execId, event.content ?? "");
1838
- } else if (eventType === "tool_use") {
1839
- useExecutionStore.getState().appendToolCall(execId, event.name ?? "");
1840
- } else if (eventType === "result") {
1841
- useExecutionStore.getState().setStatus(execId, event.status ?? "completed");
1842
- } else if (eventType === "error") {
1843
- useExecutionStore.getState().appendLine(execId, `[error] ${event.message}`);
1844
- useExecutionStore.getState().setStatus(execId, "failure");
1845
- }
1846
- } catch {
1847
- }
1848
- }
1849
- }
1850
- }
1851
- }
1852
- } catch (err) {
1853
- useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1854
- }
1855
- },
1856
- // ── Cancel ──
1857
- c: async (args, client) => {
1858
- await handlers.cancel(args, client);
1859
- },
1860
- cancel: async (args, client) => {
1861
- const executionId = args[0] ?? useExecutionStore.getState().watchingId;
1862
- if (!executionId) {
1863
- useTuiStore.getState().setLastError("No execution to cancel");
1864
- return;
1865
- }
1866
- try {
1867
- await client.cancelTask({ executionId });
1868
- useExecutionStore.getState().setStatus(executionId, "cancelled");
1869
- } catch (err) {
1870
- useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1871
- }
1872
- },
1873
- // ── Steer ──
1874
- s: async (args, client) => {
1875
- await handlers.steer(args, client);
1876
- },
1877
- steer: async (args, client) => {
1878
- const message = args.join(" ");
1879
- if (!message) {
1880
- useTuiStore.getState().setLastError("Usage: :steer <message>");
1881
- return;
1882
- }
1883
- const executionId = useExecutionStore.getState().watchingId;
1884
- const selectedMachineId = useTuiStore.getState().selectedMachineId;
1885
- if (!executionId || !selectedMachineId) {
1886
- useTuiStore.getState().setLastError("No active execution/machine to steer");
1887
- return;
1888
- }
1889
- try {
1890
- await client.steerTask({ taskId: executionId, machineId: selectedMachineId, message });
1891
- } catch (err) {
1892
- useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1893
- }
1894
- },
1895
- // ── Watch ──
1896
- watch: async (args) => {
1897
- const executionId = args[0];
1898
- if (!executionId) {
1899
- useTuiStore.getState().setLastError("Usage: :watch <executionId>");
1900
- return;
1901
- }
1902
- useExecutionStore.getState().setWatching(executionId);
1903
- useTuiStore.getState().focusPanel("output");
1904
- },
1905
- // ── Env ──
1906
- "env list": async (_args, client) => {
1907
- const machines = await client.listMachines();
1908
- useMachinesStore.getState().setMachines(machines);
1909
- useTuiStore.getState().focusPanel("machines");
1910
- },
1911
- "env status": async (_args, client) => {
1912
- const status = await client.getRelayStatus();
1913
- useTuiStore.getState().setLastError(JSON.stringify(status, null, 2));
1914
- },
1915
- // ── Search ──
1916
- search: async (args, client) => {
1917
- const query = args.join(" ");
1918
- if (!query) return;
1919
- try {
1920
- await client.search(query);
1921
- useTuiStore.getState().toggleSearch();
1922
- } catch (err) {
1923
- useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1924
- }
1925
- },
1926
- // ── Activity ──
1927
- activity: async (_args, client) => {
1928
- const projectId = useTuiStore.getState().selectedProjectId;
1929
- try {
1930
- const activities = await client.listActivities(projectId ? { projectId } : void 0);
1931
- for (const a of activities.slice(0, 20)) {
1932
- useExecutionStore.getState().appendLine("activity", `[${a.type}] ${a.title}`);
1933
- }
1934
- useExecutionStore.getState().setWatching("activity");
1935
- useTuiStore.getState().focusPanel("output");
1936
- } catch (err) {
1937
- useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1938
- }
1939
- },
1940
- // ── Help ──
1941
- help: async () => {
1942
- useTuiStore.getState().toggleHelp();
1943
- },
1944
- "?": async () => {
1945
- useTuiStore.getState().toggleHelp();
1946
- }
1947
- };
1948
- async function refreshAll(client) {
1949
- try {
1950
- const [projects, machines] = await Promise.all([
1951
- client.listProjects(),
1952
- client.listMachines()
1953
- ]);
1954
- useProjectsStore.getState().setProjects(projects);
1955
- useMachinesStore.getState().setMachines(machines);
1956
- useTuiStore.getState().setMachineCount(machines.filter((m) => m.isConnected).length);
1957
- const projectId = useTuiStore.getState().selectedProjectId;
1958
- if (projectId) {
1959
- const { nodes, edges } = await client.getPlan(projectId);
1960
- usePlanStore.getState().setPlan(projectId, nodes, edges);
1961
- }
1962
- } catch (err) {
1963
- useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
1964
- }
1965
- }
1966
-
1967
3324
  // src/tui/commands/autocomplete.ts
1968
3325
  var PrefixTrie = class {
1969
3326
  root;
@@ -2005,6 +3362,19 @@ for (const key of Object.keys(handlers)) {
2005
3362
  trie.insert(key);
2006
3363
  }
2007
3364
  async function executeCommand(input, client) {
3365
+ const colonIdx = input.indexOf(":");
3366
+ if (colonIdx > 0) {
3367
+ const prefix = input.slice(0, colonIdx);
3368
+ const value = input.slice(colonIdx + 1);
3369
+ if (handlers[prefix]) {
3370
+ try {
3371
+ await handlers[prefix]([value], client);
3372
+ } catch (err) {
3373
+ useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
3374
+ }
3375
+ return;
3376
+ }
3377
+ }
2008
3378
  const parts = input.split(/\s+/);
2009
3379
  if (parts.length >= 2) {
2010
3380
  const twoWord = `${parts[0]} ${parts[1]}`;
@@ -2052,18 +3422,28 @@ function useCommandParser(client) {
2052
3422
  }
2053
3423
 
2054
3424
  // src/tui/app.tsx
2055
- import { jsx as jsx13 } from "react/jsx-runtime";
3425
+ import { jsx as jsx16 } from "react/jsx-runtime";
2056
3426
  function App({ serverUrl }) {
2057
- const client = useMemo(() => new AstroClient({ serverUrl }), [serverUrl]);
3427
+ const client = useMemo2(() => new AstroClient({ serverUrl }), [serverUrl]);
2058
3428
  const { refreshAll: refreshAll2 } = usePolling(client);
2059
- useSSEStream(client);
3429
+ useSSEStream(client, refreshAll2);
2060
3430
  useFuzzySearch();
3431
+ const machines = useMachinesStore((s) => s.machines);
3432
+ useEffect7(() => {
3433
+ const settings = useSessionSettingsStore.getState();
3434
+ if (settings.machineId) return;
3435
+ const localPlatform = process.platform;
3436
+ const m = machines.find((m2) => m2.isConnected && m2.platform === localPlatform) ?? machines.find((m2) => m2.isConnected);
3437
+ if (m) {
3438
+ settings.init(m.id, m.name, process.cwd());
3439
+ }
3440
+ }, [machines]);
2061
3441
  const { execute } = useCommandParser(client);
2062
3442
  const onSelect = useCallback5(() => {
2063
3443
  const { focusedPanel, scrollIndex } = useTuiStore.getState();
2064
3444
  switch (focusedPanel) {
2065
3445
  case "projects": {
2066
- const projects = useProjectsStore.getState().projects;
3446
+ const projects = getVisibleProjects(useProjectsStore.getState().projects);
2067
3447
  const idx = scrollIndex.projects;
2068
3448
  if (projects[idx]) {
2069
3449
  useTuiStore.getState().setSelectedProject(projects[idx].id);
@@ -2071,12 +3451,12 @@ function App({ serverUrl }) {
2071
3451
  break;
2072
3452
  }
2073
3453
  case "plan": {
2074
- const treeLines = usePlanStore.getState().treeLines;
3454
+ const nodes = usePlanStore.getState().nodes.filter((n) => !n.deletedAt);
2075
3455
  const idx = scrollIndex.plan;
2076
- const line = treeLines[idx];
2077
- if (line) {
2078
- useTuiStore.getState().setSelectedNode(line.id);
2079
- usePlanStore.getState().toggleCollapse(line.id);
3456
+ const node = nodes[idx];
3457
+ if (node) {
3458
+ useTuiStore.getState().setSelectedNode(node.id);
3459
+ useTuiStore.getState().openDetail("node", node.id);
2080
3460
  }
2081
3461
  break;
2082
3462
  }
@@ -2101,6 +3481,16 @@ function App({ serverUrl }) {
2101
3481
  []
2102
3482
  );
2103
3483
  const onDispatch = useCallback5(async () => {
3484
+ const { focusedPanel, scrollIndex, selectedProjectId } = useTuiStore.getState();
3485
+ if (focusedPanel === "plan") {
3486
+ const nodes = usePlanStore.getState().nodes.filter((n) => !n.deletedAt);
3487
+ const node = nodes[scrollIndex.plan];
3488
+ if (node && selectedProjectId) {
3489
+ useTuiStore.getState().setSelectedNode(node.id);
3490
+ await execute(`dispatch ${node.id}`);
3491
+ return;
3492
+ }
3493
+ }
2104
3494
  const nodeId = useTuiStore.getState().selectedNodeId;
2105
3495
  const projectId = useTuiStore.getState().selectedProjectId;
2106
3496
  if (nodeId && projectId) {
@@ -2113,6 +3503,33 @@ function App({ serverUrl }) {
2113
3503
  const onRefresh = useCallback5(() => {
2114
3504
  refreshAll2();
2115
3505
  }, [refreshAll2]);
3506
+ const onSessionMessage = useCallback5(async (message) => {
3507
+ const { selectedProjectId, activeView, selectedNodeId } = useTuiStore.getState();
3508
+ const watchingId = useExecutionStore.getState().watchingId;
3509
+ if (!watchingId) {
3510
+ if (activeView === "playground") {
3511
+ await execute(`playground ${message}`);
3512
+ } else if (activeView === "plan-gen") {
3513
+ if (!selectedProjectId) {
3514
+ useTuiStore.getState().setLastError("No project selected for plan generation");
3515
+ return;
3516
+ }
3517
+ await execute(`plan generate ${message}`);
3518
+ } else {
3519
+ await execute(`playground ${message}`);
3520
+ }
3521
+ return;
3522
+ }
3523
+ if (!selectedProjectId) {
3524
+ useTuiStore.getState().setLastError("No project selected");
3525
+ return;
3526
+ }
3527
+ if (selectedNodeId) {
3528
+ await execute(`task chat ${message}`);
3529
+ } else {
3530
+ await execute(`project chat ${message}`);
3531
+ }
3532
+ }, [execute]);
2116
3533
  useVimMode({
2117
3534
  onSelect,
2118
3535
  onCommand,
@@ -2121,13 +3538,13 @@ function App({ serverUrl }) {
2121
3538
  onCancel,
2122
3539
  onRefresh
2123
3540
  });
2124
- return /* @__PURE__ */ jsx13(MainLayout, {});
3541
+ return /* @__PURE__ */ jsx16(MainLayout, { onSessionMessage });
2125
3542
  }
2126
3543
 
2127
3544
  // src/tui/index.tsx
2128
- import { jsx as jsx14 } from "react/jsx-runtime";
3545
+ import { jsx as jsx17 } from "react/jsx-runtime";
2129
3546
  async function launchTui(serverUrl) {
2130
- render(/* @__PURE__ */ jsx14(App, { serverUrl }), {
3547
+ render(/* @__PURE__ */ jsx17(App, { serverUrl }), {
2131
3548
  exitOnCtrlC: true
2132
3549
  });
2133
3550
  }