@astroanywhere/cli 0.2.1 → 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-SYY2HHOY.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 Box12, useStdout as useStdout3 } 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";
@@ -149,8 +150,16 @@ function formatCost(usd) {
149
150
  return `$${usd.toFixed(2)}`;
150
151
  }
151
152
  function truncate(str, maxLen) {
152
- if (str.length <= maxLen) return str;
153
- 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
+ });
154
163
  }
155
164
 
156
165
  // src/tui/components/layout/status-bar.tsx
@@ -160,7 +169,7 @@ var VIEW_LABELS = {
160
169
  "plan-gen": "Plan",
161
170
  projects: "Projects",
162
171
  playground: "Playground",
163
- output: "Output"
172
+ active: "Active"
164
173
  };
165
174
  function StatusBar() {
166
175
  const connected = useTuiStore((s) => s.connected);
@@ -363,6 +372,11 @@ function renderTreeLines(roots, collapsedSet) {
363
372
  }
364
373
 
365
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
+ }
366
380
  var usePlanStore = create4((set, get) => ({
367
381
  projectId: null,
368
382
  nodes: [],
@@ -372,11 +386,57 @@ var usePlanStore = create4((set, get) => ({
372
386
  collapsedNodes: /* @__PURE__ */ new Set(),
373
387
  loading: false,
374
388
  error: null,
389
+ cache: /* @__PURE__ */ new Map(),
375
390
  setPlan: (projectId, nodes, edges) => {
376
- const { collapsedNodes } = get();
377
- const treeRoots = buildTree(nodes, edges);
378
- const treeLines = renderTreeLines(treeRoots, collapsedNodes);
379
- 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 });
396
+ },
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
+ }
380
440
  },
381
441
  setLoading: (loading) => set({ loading }),
382
442
  setError: (error) => set({ error, loading: false }),
@@ -392,11 +452,16 @@ var usePlanStore = create4((set, get) => ({
392
452
  set({ collapsedNodes: next, treeLines });
393
453
  },
394
454
  updateNodeStatus: (nodeId, status) => {
395
- const { nodes, edges, collapsedNodes } = get();
455
+ const { nodes, edges, collapsedNodes, projectId, cache } = get();
396
456
  const updated = nodes.map((n) => n.id === nodeId ? { ...n, status } : n);
397
- const treeRoots = buildTree(updated, edges);
398
- const treeLines = renderTreeLines(treeRoots, collapsedNodes);
399
- 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
+ }
400
465
  },
401
466
  clear: () => set({
402
467
  projectId: null,
@@ -458,12 +523,13 @@ var useExecutionStore = create6((set, get) => ({
458
523
  outputs: /* @__PURE__ */ new Map(),
459
524
  watchingId: null,
460
525
  pendingApproval: null,
461
- initExecution: (executionId, nodeId) => {
526
+ initExecution: (executionId, nodeId, title) => {
462
527
  const { outputs } = get();
463
528
  const next = new Map(outputs);
464
529
  next.set(executionId, {
465
530
  executionId,
466
531
  nodeId,
532
+ title: title ?? nodeId,
467
533
  lines: [],
468
534
  status: "running",
469
535
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -471,6 +537,23 @@ var useExecutionStore = create6((set, get) => ({
471
537
  });
472
538
  set({ outputs: next });
473
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
+ },
474
557
  appendToolCall: (executionId) => {
475
558
  const { outputs } = get();
476
559
  const current = outputs.get(executionId);
@@ -543,6 +626,53 @@ var useExecutionStore = create6((set, get) => ({
543
626
  }
544
627
  }));
545
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
+
546
676
  // src/tui/commands/handlers.ts
547
677
  var PALETTE_COMMANDS = [
548
678
  { name: "project list", description: "List all projects" },
@@ -562,6 +692,9 @@ var PALETTE_COMMANDS = [
562
692
  { name: "activity", description: "Show recent activity feed" },
563
693
  { name: "playground", description: "Start a playground (Cloud Code) session", usage: "playground <description>" },
564
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]" },
565
698
  { name: "refresh", description: "Refresh all data" },
566
699
  { name: "help", description: "Toggle keybinding reference" },
567
700
  { name: "quit", description: "Exit the TUI" }
@@ -670,44 +803,29 @@ var handlers = {
670
803
  useTuiStore.getState().setLastError("No node/project selected for dispatch");
671
804
  return;
672
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...`);
673
819
  try {
674
- const response = await client.dispatchTask({ nodeId, projectId });
675
- const execId = `exec-${Date.now()}`;
676
- useExecutionStore.getState().initExecution(execId, nodeId);
677
- useExecutionStore.getState().setWatching(execId);
678
- useTuiStore.getState().focusPanel("output");
679
- if (response.body) {
680
- const reader = response.body.getReader();
681
- const decoder = new TextDecoder();
682
- let buffer = "";
683
- while (true) {
684
- const { done, value } = await reader.read();
685
- if (done) break;
686
- buffer += decoder.decode(value, { stream: true });
687
- const lines = buffer.split("\n");
688
- buffer = lines.pop() ?? "";
689
- for (const line of lines) {
690
- if (line.startsWith("data: ")) {
691
- try {
692
- const event = JSON.parse(line.slice(6));
693
- const eventType = event.type;
694
- if (eventType === "text") {
695
- useExecutionStore.getState().appendText(execId, event.content ?? "");
696
- } else if (eventType === "tool_use") {
697
- useExecutionStore.getState().appendToolCall(execId, event.name ?? "");
698
- } else if (eventType === "result") {
699
- useExecutionStore.getState().setStatus(execId, event.status ?? "completed");
700
- } else if (eventType === "error") {
701
- useExecutionStore.getState().appendLine(execId, `[error] ${event.message}`);
702
- useExecutionStore.getState().setStatus(execId, "failure");
703
- }
704
- } catch {
705
- }
706
- }
707
- }
708
- }
709
- }
820
+ const response = await client.dispatchTask({
821
+ nodeId,
822
+ projectId,
823
+ targetMachineId: connectedMachine.id
824
+ });
825
+ await streamSSEToExecution(response, execId, client, projectId);
710
826
  } catch (err) {
827
+ useExecutionStore.getState().setStatus(execId, "error");
828
+ useExecutionStore.getState().appendLine(execId, `[error] ${err instanceof Error ? err.message : String(err)}`);
711
829
  useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
712
830
  }
713
831
  },
@@ -796,35 +914,68 @@ var handlers = {
796
914
  }
797
915
  },
798
916
  // ── Playground ──
917
+ // Matches frontend: creates a new playground project, then dispatches.
918
+ // No project selection required — uses session settings for machine + workdir.
799
919
  playground: async (args, client) => {
800
920
  const description = args.join(" ");
801
921
  if (!description) {
802
922
  useTuiStore.getState().setLastError("Usage: playground <description>");
803
923
  return;
804
924
  }
805
- const projectId = useTuiStore.getState().selectedProjectId;
806
- if (!projectId) {
807
- useTuiStore.getState().setLastError("No project selected. Select a project first.");
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.");
808
939
  return;
809
940
  }
810
- const nodeId = `playground-${projectId}-${Date.now()}`;
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...`);
811
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})...`);
812
960
  const response = await client.dispatchTask({
813
961
  nodeId,
814
- projectId,
815
- skipSafetyCheck: true,
962
+ projectId: project.id,
963
+ title,
816
964
  description,
817
- title: `Playground: ${description.slice(0, 50)}`
965
+ targetMachineId,
966
+ workingDirectory: workDir,
967
+ deliveryMode: "direct",
968
+ skipSafetyCheck: true,
969
+ force: true
818
970
  });
819
- useExecutionStore.getState().initExecution(nodeId, nodeId);
820
- useExecutionStore.getState().setWatching(nodeId);
821
- useTuiStore.getState().setActiveView("playground");
822
- await streamExecution(nodeId, response);
971
+ await streamSSEToExecution(response, execId, client, project.id);
823
972
  } catch (err) {
973
+ useExecutionStore.getState().setStatus(execId, "error");
974
+ useExecutionStore.getState().appendLine(execId, `[error] ${err instanceof Error ? err.message : String(err)}`);
824
975
  useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
825
976
  }
826
977
  },
827
- // ── Plan Generate ──
978
+ // ── Plan Generate (uses /api/dispatch/task SSE with isInteractivePlan) ──
828
979
  "plan generate": async (args, client) => {
829
980
  const description = args.join(" ");
830
981
  if (!description) {
@@ -836,26 +987,166 @@ var handlers = {
836
987
  useTuiStore.getState().setLastError("No project selected. Select a project first.");
837
988
  return;
838
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
+ }
839
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`);
840
1003
  try {
841
1004
  const response = await client.dispatchTask({
842
1005
  nodeId,
843
1006
  projectId,
1007
+ title: `Interactive planning: ${description.slice(0, 80)}`,
1008
+ description,
1009
+ targetMachineId: connectedMachine.id,
844
1010
  isInteractivePlan: true,
845
- description
1011
+ verification: "human"
846
1012
  });
847
- useExecutionStore.getState().initExecution(nodeId, nodeId);
848
- useExecutionStore.getState().setWatching(nodeId);
849
- useTuiStore.getState().setActiveView("plan-gen");
850
- await streamExecution(nodeId, response);
851
- useExecutionStore.getState().appendLine(nodeId, "[plan] Refreshing plan...");
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);
852
1045
  const { nodes, edges } = await client.getPlan(projectId);
853
- usePlanStore.getState().setPlan(projectId, nodes, edges);
854
- useTuiStore.getState().setActiveView("dashboard");
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);
855
1057
  } catch (err) {
1058
+ useChatStore.getState().setStreaming(false);
1059
+ useExecutionStore.getState().setStatus(execId, "error");
856
1060
  useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
857
1061
  }
858
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
+ },
859
1150
  // ── Help ──
860
1151
  help: async () => {
861
1152
  useTuiStore.getState().toggleHelp();
@@ -864,79 +1155,141 @@ var handlers = {
864
1155
  useTuiStore.getState().toggleHelp();
865
1156
  }
866
1157
  };
867
- async function streamExecution(executionId, response) {
868
- if (!response.body) return;
869
- const reader = response.body.getReader();
870
- const decoder = new TextDecoder();
871
- let buffer = "";
872
- while (true) {
873
- const { done, value } = await reader.read();
874
- if (done) break;
875
- buffer += decoder.decode(value, { stream: true });
876
- const lines = buffer.split("\n");
877
- buffer = lines.pop() ?? "";
878
- for (const line of lines) {
879
- if (!line.startsWith("data: ")) continue;
880
- try {
881
- const event = JSON.parse(line.slice(6));
882
- const eventType = event.type;
883
- if (eventType === "text") {
884
- useExecutionStore.getState().appendText(executionId, event.content ?? "");
885
- } else if (eventType === "tool_use") {
886
- useExecutionStore.getState().appendToolCall(executionId, event.name ?? "");
887
- } else if (eventType === "file_change") {
888
- useExecutionStore.getState().appendFileChange(
889
- executionId,
890
- event.path ?? "",
891
- event.action ?? "modified",
892
- event.linesAdded,
893
- event.linesRemoved
894
- );
895
- } else if (eventType === "result" || eventType === "done") {
896
- useExecutionStore.getState().setStatus(executionId, event.status ?? "completed");
897
- } else if (eventType === "error") {
898
- useExecutionStore.getState().appendLine(executionId, `[error] ${event.message}`);
899
- useExecutionStore.getState().setStatus(executionId, "failure");
900
- } else if (eventType === "plan_result") {
901
- useExecutionStore.getState().appendLine(executionId, "[plan] Plan generated successfully");
902
- } else if (eventType === "approval_request") {
903
- useExecutionStore.getState().setPendingApproval({
904
- requestId: event.requestId,
905
- question: event.question,
906
- options: event.options,
907
- machineId: event.machineId,
908
- taskId: event.taskId
909
- });
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})`);
910
1169
  }
911
- } catch {
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;
912
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;
913
1241
  }
1242
+ });
1243
+ const exec = useExecutionStore.getState().outputs.get(execId);
1244
+ if (exec?.status === "running") {
1245
+ useExecutionStore.getState().setStatus(execId, "completed");
914
1246
  }
915
1247
  }
916
1248
  async function refreshAll(client) {
917
1249
  try {
918
- const [projects, machines] = await Promise.all([
1250
+ const [projects, machines, fullPlan] = await Promise.all([
919
1251
  client.listProjects(),
920
- client.listMachines()
1252
+ client.listMachines(),
1253
+ client.getFullPlan()
921
1254
  ]);
922
1255
  useProjectsStore.getState().setProjects(projects);
923
1256
  useMachinesStore.getState().setMachines(machines);
924
1257
  useTuiStore.getState().setMachineCount(machines.filter((m) => m.isConnected).length);
925
- const projectId = useTuiStore.getState().selectedProjectId;
926
- if (projectId) {
927
- const { nodes, edges } = await client.getPlan(projectId);
928
- usePlanStore.getState().setPlan(projectId, nodes, edges);
929
- }
1258
+ usePlanStore.getState().setAllPlans(fullPlan.nodes, fullPlan.edges);
930
1259
  } catch (err) {
931
1260
  useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
932
1261
  }
933
1262
  }
934
1263
 
935
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
+ }
936
1287
  function getFilteredPaletteCommands(query) {
937
- if (!query) return PALETTE_COMMANDS;
1288
+ const resumeCommands = getResumeCommands();
1289
+ const allCommands = [...PALETTE_COMMANDS, ...resumeCommands];
1290
+ if (!query) return allCommands;
938
1291
  const lower = query.toLowerCase();
939
- return PALETTE_COMMANDS.filter(
1292
+ return allCommands.filter(
940
1293
  (cmd) => cmd.name.toLowerCase().includes(lower) || cmd.description.toLowerCase().includes(lower)
941
1294
  );
942
1295
  }
@@ -948,10 +1301,11 @@ var SHORTCUTS = [
948
1301
  { key: "2", label: "Plan" },
949
1302
  { key: "3", label: "Projects" },
950
1303
  { key: "4", label: "Playground" },
951
- { key: "5", label: "Output" },
1304
+ { key: "5", label: "Active" },
952
1305
  { key: "/", label: "Search" },
953
1306
  { key: "C-p", label: "Commands" },
954
1307
  { key: "d", label: "Dispatch" },
1308
+ { key: "x", label: "Stop" },
955
1309
  { key: "?", label: "Help" },
956
1310
  { key: "q", label: "Quit" }
957
1311
  ];
@@ -1053,7 +1407,17 @@ function CommandLine({ height }) {
1053
1407
  ] });
1054
1408
  }
1055
1409
  if (mode === "input") {
1056
- return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, height, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Input active \u2014 press Esc to exit" }) });
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
+ ] });
1057
1421
  }
1058
1422
  return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, gap: 1, height, children: SHORTCUTS.map(({ key, label }) => /* @__PURE__ */ jsxs2(Box2, { children: [
1059
1423
  /* @__PURE__ */ jsx2(Text2, { inverse: true, bold: true, children: ` ${key} ` }),
@@ -1194,7 +1558,7 @@ function SearchOverlay() {
1194
1558
  }
1195
1559
 
1196
1560
  // src/tui/components/panels/projects-panel.tsx
1197
- import "react";
1561
+ import { useEffect as useEffect2, useMemo } from "react";
1198
1562
  import { Box as Box5, Text as Text6 } from "ink";
1199
1563
 
1200
1564
  // src/tui/components/layout/panel.tsx
@@ -1240,13 +1604,14 @@ function Spinner({ label }) {
1240
1604
  }
1241
1605
 
1242
1606
  // src/tui/components/panels/projects-panel.tsx
1243
- import { Fragment, jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
1244
- function statusSymbol(status) {
1245
- if (status === "running") return "\u25B6";
1246
- if (status === "success") return "\u2713";
1247
- if (status === "failure" || status === "error") return "\u2717";
1248
- return "\xB7";
1249
- }
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
+ ]);
1250
1615
  function ProjectsPanel({ height }) {
1251
1616
  const projects = useProjectsStore((s) => s.projects);
1252
1617
  const loading = useProjectsStore((s) => s.loading);
@@ -1255,32 +1620,63 @@ function ProjectsPanel({ height }) {
1255
1620
  const scrollIndex = useTuiStore((s) => s.scrollIndex.projects);
1256
1621
  const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
1257
1622
  const outputs = useExecutionStore((s) => s.outputs);
1623
+ const planNodes = usePlanStore((s) => s.nodes);
1258
1624
  const isFocused = focusedPanel === "projects";
1259
1625
  const visibleHeight = Math.max(1, height - 3);
1260
- const planGenEntries = [];
1261
- const playgroundEntries = [];
1262
- for (const [id, exec] of outputs) {
1263
- if (exec.nodeId.startsWith("plan-")) {
1264
- planGenEntries.push({ id, label: exec.nodeId, status: exec.status });
1265
- } else if (exec.nodeId.startsWith("playground-")) {
1266
- playgroundEntries.push({ id, label: exec.nodeId.replace(/^playground-/, "").slice(0, 30), status: exec.status });
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
+ }
1267
1645
  }
1268
- }
1269
- const projectCount = projects.length;
1270
- const clampedIndex = Math.min(scrollIndex, Math.max(0, projectCount - 1));
1271
- const sectionLineCount = (planGenEntries.length > 0 ? 1 + planGenEntries.length : 0) + (playgroundEntries.length > 0 ? 1 + playgroundEntries.length : 0);
1272
- const projectVisibleHeight = Math.max(1, visibleHeight - sectionLineCount);
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]);
1273
1664
  let start = 0;
1274
- if (projectCount > projectVisibleHeight) {
1275
- start = Math.max(0, Math.min(clampedIndex - Math.floor(projectVisibleHeight / 2), projectCount - projectVisibleHeight));
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
+ }
1276
1671
  }
1277
- const visibleProjects = projects.slice(start, start + projectVisibleHeight);
1672
+ const visibleProjects = sorted.slice(start, start + visibleHeight);
1278
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: [
1279
- visibleProjects.length === 0 && planGenEntries.length === 0 && playgroundEntries.length === 0 && /* @__PURE__ */ jsx5(Text6, { dimColor: true, children: " No projects yet" }),
1674
+ visibleProjects.length === 0 && /* @__PURE__ */ jsx5(Text6, { dimColor: true, children: " No projects yet" }),
1280
1675
  visibleProjects.map((p, i) => {
1281
1676
  const actualIndex = start + i;
1282
- const isSelected = isFocused && clampedIndex === actualIndex;
1677
+ const isSelected = isFocused && cursor === actualIndex;
1283
1678
  const isActive = p.id === selectedProjectId;
1679
+ const hasRunning = projectsWithRunning.has(p.id);
1284
1680
  return /* @__PURE__ */ jsxs6(Box5, { children: [
1285
1681
  /* @__PURE__ */ jsxs6(
1286
1682
  Text6,
@@ -1291,7 +1687,8 @@ function ProjectsPanel({ height }) {
1291
1687
  wrap: "truncate",
1292
1688
  children: [
1293
1689
  isSelected ? " > " : " ",
1294
- truncate(p.name, 30)
1690
+ hasRunning ? "\u25B6 " : " ",
1691
+ truncate(p.name, 28)
1295
1692
  ]
1296
1693
  }
1297
1694
  ),
@@ -1301,36 +1698,14 @@ function ProjectsPanel({ height }) {
1301
1698
  ] })
1302
1699
  ] }, p.id);
1303
1700
  }),
1304
- projectCount > projectVisibleHeight && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1701
+ projectCount > visibleHeight && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1305
1702
  " [",
1306
1703
  start + 1,
1307
1704
  "-",
1308
- Math.min(start + projectVisibleHeight, projectCount),
1705
+ Math.min(start + visibleHeight, projectCount),
1309
1706
  "/",
1310
1707
  projectCount,
1311
1708
  "]"
1312
- ] }),
1313
- planGenEntries.length > 0 && /* @__PURE__ */ jsxs6(Fragment, { children: [
1314
- /* @__PURE__ */ jsx5(Text6, { bold: true, color: "yellow", children: " Plan Generation" }),
1315
- planGenEntries.map((item) => /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate", children: [
1316
- " ",
1317
- statusSymbol(item.status),
1318
- " ",
1319
- truncate(item.label, 28),
1320
- " ",
1321
- item.status
1322
- ] }, item.id))
1323
- ] }),
1324
- playgroundEntries.length > 0 && /* @__PURE__ */ jsxs6(Fragment, { children: [
1325
- /* @__PURE__ */ jsx5(Text6, { bold: true, color: "green", children: " Playground" }),
1326
- playgroundEntries.map((item) => /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate", children: [
1327
- " ",
1328
- statusSymbol(item.status),
1329
- " ",
1330
- truncate(item.label, 28),
1331
- " ",
1332
- item.status
1333
- ] }, item.id))
1334
1709
  ] })
1335
1710
  ] }) });
1336
1711
  }
@@ -1349,16 +1724,21 @@ function ScrollableList({ items, selectedIndex, height, isFocused }) {
1349
1724
  if (items.length === 0) {
1350
1725
  return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: " No items." }) });
1351
1726
  }
1727
+ const cursor = Math.min(Math.max(0, selectedIndex), items.length - 1);
1352
1728
  const visibleHeight = Math.max(1, height - 1);
1353
1729
  let start = 0;
1354
- if (selectedIndex >= visibleHeight) {
1355
- start = selectedIndex - visibleHeight + 1;
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
+ }
1356
1736
  }
1357
1737
  const visibleItems = items.slice(start, start + visibleHeight);
1358
1738
  return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", children: [
1359
1739
  visibleItems.map((item, i) => {
1360
1740
  const actualIndex = start + i;
1361
- const isSelected = actualIndex === selectedIndex && isFocused;
1741
+ const isSelected = actualIndex === cursor && isFocused;
1362
1742
  const prefix = isSelected ? " \u25B6 " : " ";
1363
1743
  const sublabel = item.sublabel ? ` ${item.sublabel}` : "";
1364
1744
  const rightLabel = item.rightLabel ?? "";
@@ -1477,10 +1857,10 @@ function OutputPanel({ height }) {
1477
1857
  const isFocused = focusedPanel === "output";
1478
1858
  const execution = watchingId ? outputs.get(watchingId) : null;
1479
1859
  const visibleHeight = Math.max(1, height - 4);
1480
- let title = "OUTPUT";
1860
+ let title = "PROCESS OUTPUT";
1481
1861
  if (execution) {
1482
1862
  const shortId = execution.nodeId.length > 20 ? execution.nodeId.slice(0, 8) + "\u2026" : execution.nodeId;
1483
- title = `OUTPUT (${shortId}) [${execution.status}]`;
1863
+ title = `${shortId} [${execution.status}]`;
1484
1864
  }
1485
1865
  if (!execution) {
1486
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" }) });
@@ -1515,40 +1895,6 @@ function OutputPanel({ height }) {
1515
1895
  import "react";
1516
1896
  import { Box as Box8, Text as Text11 } from "ink";
1517
1897
  import { TextInput } from "@inkjs/ui";
1518
-
1519
- // src/tui/stores/chat-store.ts
1520
- import { create as create7 } from "zustand";
1521
- var useChatStore = create7((set, get) => ({
1522
- messages: [],
1523
- sessionId: null,
1524
- projectId: null,
1525
- nodeId: null,
1526
- streaming: false,
1527
- streamBuffer: "",
1528
- addMessage: (role, content) => {
1529
- set((s) => ({
1530
- messages: [...s.messages, { role, content, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
1531
- }));
1532
- },
1533
- appendStream: (text) => {
1534
- set((s) => ({ streamBuffer: s.streamBuffer + text }));
1535
- },
1536
- flushStream: () => {
1537
- const { streamBuffer } = get();
1538
- if (streamBuffer.length > 0) {
1539
- set((s) => ({
1540
- messages: [...s.messages, { role: "assistant", content: streamBuffer, timestamp: (/* @__PURE__ */ new Date()).toISOString() }],
1541
- streamBuffer: ""
1542
- }));
1543
- }
1544
- },
1545
- setSessionId: (sessionId) => set({ sessionId }),
1546
- setContext: (projectId, nodeId) => set({ projectId, nodeId: nodeId ?? null }),
1547
- setStreaming: (streaming) => set({ streaming }),
1548
- clear: () => set({ messages: [], sessionId: null, streamBuffer: "", streaming: false })
1549
- }));
1550
-
1551
- // src/tui/components/panels/chat-panel.tsx
1552
1898
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1553
1899
  function ChatPanel({ height }) {
1554
1900
  const messages = useChatStore((s) => s.messages);
@@ -1603,16 +1949,24 @@ function lineColor2(line) {
1603
1949
  if (line.startsWith("> ")) return "cyan";
1604
1950
  return void 0;
1605
1951
  }
1952
+ function machineLabel(name, id) {
1953
+ if (!id) return "No machine";
1954
+ return name || id.slice(0, 12);
1955
+ }
1606
1956
  function SessionPanel({ height, title, sessionType, onSubmit }) {
1607
1957
  const watchingId = useExecutionStore((s) => s.watchingId);
1608
1958
  const outputs = useExecutionStore((s) => s.outputs);
1609
1959
  const streaming = useChatStore((s) => s.streaming);
1610
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);
1611
1964
  const execution = watchingId ? outputs.get(watchingId) : null;
1612
1965
  const isRunning = execution?.status === "running";
1966
+ const settingsHeight = pickerOpen ? 2 + Math.min(connectedMachines.length, 5) : 2;
1613
1967
  const inputHeight = 2;
1614
- const outputHeight = Math.max(1, height - 5 - inputHeight);
1615
- const isInputActive = mode === "input";
1968
+ const outputHeight = Math.max(1, height - 5 - settingsHeight - inputHeight);
1969
+ const isInputActive = mode === "input" && !focusedField && !pickerOpen;
1616
1970
  const lines = execution?.lines ?? [];
1617
1971
  const hasPendingTools = (execution?.pendingToolCount ?? 0) > 0;
1618
1972
  const start = Math.max(0, lines.length - outputHeight);
@@ -1641,6 +1995,75 @@ function SessionPanel({ height, title, sessionType, onSubmit }) {
1641
1995
  lines.length,
1642
1996
  "]"
1643
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
+ ] }),
1644
2067
  /* @__PURE__ */ jsx11(Box9, { borderStyle: "single", borderColor: isInputActive ? "cyan" : "gray", paddingX: 1, children: isRunning || streaming ? /* @__PURE__ */ jsxs10(Box9, { children: [
1645
2068
  /* @__PURE__ */ jsx11(Spinner, {}),
1646
2069
  /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: " Agent is working... (press Esc to return to navigation)" })
@@ -1661,22 +2084,235 @@ function SessionPanel({ height, title, sessionType, onSubmit }) {
1661
2084
  }
1662
2085
  }
1663
2086
  }
1664
- ) : /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "Press Enter to start typing..." })
2087
+ ) : /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: focusedField ? "Press \u2193 to return to input" : "Press Enter to start typing..." })
1665
2088
  ] }) })
1666
2089
  ] }) });
1667
2090
  }
1668
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
+
1669
2305
  // src/tui/components/panels/detail-overlay.tsx
1670
2306
  import "react";
1671
- import { Box as Box10, Text as Text13 } from "ink";
1672
- import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2307
+ import { Box as Box11, Text as Text14 } from "ink";
2308
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1673
2309
  function DetailOverlay() {
1674
2310
  const showDetail = useTuiStore((s) => s.showDetail);
1675
2311
  const detailType = useTuiStore((s) => s.detailType);
1676
2312
  const detailId = useTuiStore((s) => s.detailId);
1677
2313
  if (!showDetail || !detailType || !detailId) return null;
1678
- return /* @__PURE__ */ jsxs11(
1679
- Box10,
2314
+ return /* @__PURE__ */ jsxs12(
2315
+ Box11,
1680
2316
  {
1681
2317
  flexDirection: "column",
1682
2318
  borderStyle: "round",
@@ -1684,89 +2320,89 @@ function DetailOverlay() {
1684
2320
  paddingX: 2,
1685
2321
  paddingY: 1,
1686
2322
  children: [
1687
- detailType === "project" && /* @__PURE__ */ jsx12(ProjectDetail, { id: detailId }),
1688
- detailType === "node" && /* @__PURE__ */ jsx12(NodeDetail, { id: detailId }),
1689
- detailType === "machine" && /* @__PURE__ */ jsx12(MachineDetail, { id: detailId }),
1690
- /* @__PURE__ */ jsx12(Text13, { dimColor: true, children: "Press Esc to close" })
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" })
1691
2327
  ]
1692
2328
  }
1693
2329
  );
1694
2330
  }
1695
2331
  function ProjectDetail({ id }) {
1696
2332
  const project = useProjectsStore((s) => s.projects.find((p) => p.id === id));
1697
- if (!project) return /* @__PURE__ */ jsx12(Text13, { color: "red", children: "Project not found" });
1698
- return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
1699
- /* @__PURE__ */ jsx12(Text13, { bold: true, color: "cyan", children: project.name }),
1700
- /* @__PURE__ */ jsx12(Text13, { children: " " }),
1701
- /* @__PURE__ */ jsx12(Field, { label: "ID", value: project.id }),
1702
- /* @__PURE__ */ jsx12(Field, { label: "Status", value: project.status, color: getStatusColor(project.status) }),
1703
- /* @__PURE__ */ jsx12(Field, { label: "Description", value: project.description || "\u2014" }),
1704
- /* @__PURE__ */ jsx12(Field, { label: "Health", value: project.health ?? "\u2014" }),
1705
- /* @__PURE__ */ jsx12(Field, { label: "Progress", value: `${project.progress}%` }),
1706
- /* @__PURE__ */ jsx12(Field, { label: "Working Dir", value: project.workingDirectory ?? "\u2014" }),
1707
- /* @__PURE__ */ jsx12(Field, { label: "Repository", value: project.repository ?? "\u2014" }),
1708
- /* @__PURE__ */ jsx12(Field, { label: "Delivery", value: project.deliveryMode ?? "\u2014" }),
1709
- /* @__PURE__ */ jsx12(Field, { label: "Start Date", value: project.startDate ?? "\u2014" }),
1710
- /* @__PURE__ */ jsx12(Field, { label: "Target Date", value: project.targetDate ?? "\u2014" }),
1711
- /* @__PURE__ */ jsx12(Field, { label: "Created", value: formatRelativeTime(project.createdAt) }),
1712
- /* @__PURE__ */ jsx12(Field, { label: "Updated", value: formatRelativeTime(project.updatedAt) }),
1713
- /* @__PURE__ */ jsx12(Text13, { children: " " })
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: " " })
1714
2350
  ] });
1715
2351
  }
1716
2352
  function NodeDetail({ id }) {
1717
2353
  const node = usePlanStore((s) => s.nodes.find((n) => n.id === id));
1718
- if (!node) return /* @__PURE__ */ jsx12(Text13, { color: "red", children: "Node not found" });
1719
- return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
1720
- /* @__PURE__ */ jsx12(Text13, { bold: true, color: "cyan", children: node.title }),
1721
- /* @__PURE__ */ jsx12(Text13, { children: " " }),
1722
- /* @__PURE__ */ jsx12(Field, { label: "ID", value: node.id }),
1723
- /* @__PURE__ */ jsx12(Field, { label: "Type", value: node.type }),
1724
- /* @__PURE__ */ jsx12(Field, { label: "Status", value: node.status, color: getStatusColor(node.status) }),
1725
- /* @__PURE__ */ jsx12(Field, { label: "Description", value: node.description || "\u2014" }),
1726
- /* @__PURE__ */ jsx12(Field, { label: "Priority", value: node.priority ?? "\u2014" }),
1727
- /* @__PURE__ */ jsx12(Field, { label: "Estimate", value: node.estimate ?? "\u2014" }),
1728
- /* @__PURE__ */ jsx12(Field, { label: "Start Date", value: node.startDate ?? "\u2014" }),
1729
- /* @__PURE__ */ jsx12(Field, { label: "End Date", value: node.endDate ?? "\u2014" }),
1730
- /* @__PURE__ */ jsx12(Field, { label: "Due Date", value: node.dueDate ?? "\u2014" }),
1731
- /* @__PURE__ */ jsx12(Field, { label: "Branch", value: node.branchName ?? "\u2014" }),
1732
- /* @__PURE__ */ jsx12(Field, { label: "PR URL", value: node.prUrl ?? "\u2014" }),
1733
- /* @__PURE__ */ jsx12(Field, { label: "Execution ID", value: node.executionId ?? "\u2014" }),
1734
- /* @__PURE__ */ jsx12(Field, { label: "Exec Started", value: node.executionStartedAt ? formatRelativeTime(node.executionStartedAt) : "\u2014" }),
1735
- /* @__PURE__ */ jsx12(Field, { label: "Exec Completed", value: node.executionCompletedAt ? formatRelativeTime(node.executionCompletedAt) : "\u2014" }),
1736
- /* @__PURE__ */ jsx12(Text13, { children: " " })
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: " " })
1737
2373
  ] });
1738
2374
  }
1739
2375
  function MachineDetail({ id }) {
1740
2376
  const machine = useMachinesStore((s) => s.machines.find((m) => m.id === id));
1741
- if (!machine) return /* @__PURE__ */ jsx12(Text13, { color: "red", children: "Machine not found" });
1742
- return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
1743
- /* @__PURE__ */ jsx12(Text13, { bold: true, color: "cyan", children: machine.name }),
1744
- /* @__PURE__ */ jsx12(Text13, { children: " " }),
1745
- /* @__PURE__ */ jsx12(Field, { label: "ID", value: machine.id }),
1746
- /* @__PURE__ */ jsx12(Field, { label: "Hostname", value: machine.hostname }),
1747
- /* @__PURE__ */ jsx12(Field, { label: "Platform", value: machine.platform }),
1748
- /* @__PURE__ */ jsx12(Field, { label: "Env Type", value: machine.environmentType }),
1749
- /* @__PURE__ */ jsx12(Field, { label: "Connected", value: machine.isConnected ? "Yes" : "No", color: machine.isConnected ? "green" : "red" }),
1750
- /* @__PURE__ */ jsx12(Field, { label: "Providers", value: machine.providers.join(", ") || "\u2014" }),
1751
- /* @__PURE__ */ jsx12(Field, { label: "Registered", value: formatRelativeTime(machine.registeredAt) }),
1752
- /* @__PURE__ */ jsx12(Field, { label: "Last Seen", value: formatRelativeTime(machine.lastSeenAt) }),
1753
- /* @__PURE__ */ jsx12(Text13, { children: " " })
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: " " })
1754
2390
  ] });
1755
2391
  }
1756
2392
  function Field({ label, value, color }) {
1757
- return /* @__PURE__ */ jsxs11(Box10, { children: [
1758
- /* @__PURE__ */ jsx12(Text13, { dimColor: true, children: label.padEnd(16) }),
1759
- color ? /* @__PURE__ */ jsx12(Text13, { color, children: value }) : /* @__PURE__ */ jsx12(Text13, { children: value })
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 })
1760
2396
  ] });
1761
2397
  }
1762
2398
 
1763
2399
  // src/tui/components/shared/approval-dialog.tsx
1764
- import { useState as useState2 } from "react";
1765
- import { Box as Box11, Text as Text14, useInput as useInput2 } from "ink";
1766
- import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
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";
1767
2403
  function ApprovalDialog({ question, options, onSelect, onDismiss }) {
1768
- const [selectedIndex, setSelectedIndex] = useState2(0);
1769
- useInput2((input, key) => {
2404
+ const [selectedIndex, setSelectedIndex] = useState3(0);
2405
+ useInput3((input, key) => {
1770
2406
  if (key.escape) {
1771
2407
  onDismiss();
1772
2408
  return;
@@ -1786,8 +2422,8 @@ function ApprovalDialog({ question, options, onSelect, onDismiss }) {
1786
2422
  onSelect(num - 1);
1787
2423
  }
1788
2424
  });
1789
- return /* @__PURE__ */ jsxs12(
1790
- Box11,
2425
+ return /* @__PURE__ */ jsxs13(
2426
+ Box12,
1791
2427
  {
1792
2428
  flexDirection: "column",
1793
2429
  borderStyle: "round",
@@ -1795,16 +2431,16 @@ function ApprovalDialog({ question, options, onSelect, onDismiss }) {
1795
2431
  paddingX: 1,
1796
2432
  paddingY: 0,
1797
2433
  children: [
1798
- /* @__PURE__ */ jsx13(Text14, { color: "yellow", bold: true, children: "Approval Required" }),
1799
- /* @__PURE__ */ jsx13(Text14, { wrap: "wrap", children: question }),
1800
- /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsx13(Box11, { children: /* @__PURE__ */ jsxs12(Text14, { color: i === selectedIndex ? "cyan" : "white", children: [
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: [
1801
2437
  i === selectedIndex ? "\u25B8 " : " ",
1802
2438
  "[",
1803
2439
  i + 1,
1804
2440
  "] ",
1805
2441
  opt
1806
2442
  ] }) }, i)) }),
1807
- /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
2443
+ /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs13(Text15, { dimColor: true, children: [
1808
2444
  "\u2191\u2193/jk navigate \u2022 Enter select \u2022 1-",
1809
2445
  options.length,
1810
2446
  " quick select \u2022 Esc dismiss"
@@ -1815,7 +2451,7 @@ function ApprovalDialog({ question, options, onSelect, onDismiss }) {
1815
2451
  }
1816
2452
 
1817
2453
  // src/tui/components/layout/main-layout.tsx
1818
- import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2454
+ import { Fragment, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
1819
2455
  function MainLayout({ onSessionMessage }) {
1820
2456
  const showHelp = useTuiStore((s) => s.showHelp);
1821
2457
  const showDetail = useTuiStore((s) => s.showDetail);
@@ -1831,20 +2467,20 @@ function MainLayout({ onSessionMessage }) {
1831
2467
  const bottomPanelHeight = panelOpen ? Math.floor(termHeight / 2) : 1;
1832
2468
  const contentHeight = termHeight - 2 - bottomPanelHeight;
1833
2469
  if (showHelp) {
1834
- return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1835
- /* @__PURE__ */ jsx14(StatusBar, {}),
1836
- /* @__PURE__ */ jsx14(HelpOverlay, {}),
1837
- /* @__PURE__ */ jsx14(CommandLine, { height: 1 })
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 })
1838
2474
  ] });
1839
2475
  }
1840
2476
  if (showDetail) {
1841
- return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1842
- /* @__PURE__ */ jsx14(StatusBar, {}),
1843
- /* @__PURE__ */ jsx14(DetailOverlay, {}),
1844
- /* @__PURE__ */ jsx14(CommandLine, { height: 1 })
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 })
1845
2481
  ] });
1846
2482
  }
1847
- const approvalOverlay = pendingApproval ? /* @__PURE__ */ jsx14(Box12, { position: "absolute", marginTop: 4, marginLeft: Math.floor(termWidth / 4), children: /* @__PURE__ */ jsx14(
2483
+ const approvalOverlay = pendingApproval ? /* @__PURE__ */ jsx15(Box13, { position: "absolute", marginTop: 4, marginLeft: Math.floor(termWidth / 4), children: /* @__PURE__ */ jsx15(
1848
2484
  ApprovalDialog,
1849
2485
  {
1850
2486
  question: pendingApproval.question,
@@ -1860,42 +2496,45 @@ function MainLayout({ onSessionMessage }) {
1860
2496
  ) }) : null;
1861
2497
  let content;
1862
2498
  if (activeView === "plan-gen") {
1863
- content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(SessionPanel, { height: contentHeight, title: "PLAN GENERATION", sessionType: "plan-generate", onSubmit: onSessionMessage }) }) });
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 }) }) });
1864
2500
  } else if (activeView === "projects") {
1865
- content = /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: contentHeight, children: [
1866
- /* @__PURE__ */ jsx14(Box12, { width: "40%", children: /* @__PURE__ */ jsx14(ProjectsPanel, { height: contentHeight }) }),
1867
- /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(PlanPanel, { height: contentHeight }) })
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 }) })
1868
2504
  ] });
1869
2505
  } else if (activeView === "playground") {
1870
- content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(SessionPanel, { height: contentHeight, title: "PLAYGROUND", sessionType: "playground", onSubmit: onSessionMessage }) }) });
1871
- } else if (activeView === "output") {
1872
- content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(OutputPanel, { height: contentHeight }) }) });
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
+ ] });
1873
2512
  } else {
1874
2513
  const topRowHeight = Math.floor(contentHeight / 2);
1875
2514
  const bottomRowHeight = contentHeight - topRowHeight;
1876
- content = /* @__PURE__ */ jsxs13(Fragment2, { children: [
1877
- /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: topRowHeight, children: [
1878
- /* @__PURE__ */ jsx14(Box12, { width: "30%", children: /* @__PURE__ */ jsx14(ProjectsPanel, { height: topRowHeight }) }),
1879
- /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(PlanPanel, { height: topRowHeight }) })
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 }) })
1880
2519
  ] }),
1881
- /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: bottomRowHeight, children: [
1882
- /* @__PURE__ */ jsx14(Box12, { width: "30%", children: /* @__PURE__ */ jsx14(MachinesPanel, { height: bottomRowHeight }) }),
1883
- /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: showChat ? /* @__PURE__ */ jsx14(ChatPanel, { height: bottomRowHeight }) : /* @__PURE__ */ jsx14(OutputPanel, { height: bottomRowHeight }) })
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 }) })
1884
2523
  ] })
1885
2524
  ] });
1886
2525
  }
1887
- return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", width: termWidth, height: termHeight, children: [
1888
- /* @__PURE__ */ jsx14(StatusBar, {}),
2526
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", width: termWidth, height: termHeight, children: [
2527
+ /* @__PURE__ */ jsx15(StatusBar, {}),
1889
2528
  content,
1890
- /* @__PURE__ */ jsx14(SearchOverlay, {}),
2529
+ /* @__PURE__ */ jsx15(SearchOverlay, {}),
1891
2530
  approvalOverlay,
1892
- /* @__PURE__ */ jsx14(CommandLine, { height: bottomPanelHeight })
2531
+ /* @__PURE__ */ jsx15(CommandLine, { height: bottomPanelHeight })
1893
2532
  ] });
1894
2533
  }
1895
2534
 
1896
2535
  // src/tui/hooks/use-vim-mode.ts
1897
2536
  import { useCallback, useRef } from "react";
1898
- import { useInput as useInput3, useApp } from "ink";
2537
+ import { useInput as useInput4, useApp } from "ink";
1899
2538
 
1900
2539
  // src/tui/lib/vim-state-machine.ts
1901
2540
  function initialVimState() {
@@ -1940,7 +2579,7 @@ function handleKey(state, key, ctrl, meta) {
1940
2579
  case "search":
1941
2580
  return handleSearchMode(state, key);
1942
2581
  case "input":
1943
- return handleInputMode(state, key);
2582
+ return handleInputMode(state, key, ctrl);
1944
2583
  }
1945
2584
  }
1946
2585
  function handleNormalMode(state, key, ctrl, meta) {
@@ -1950,6 +2589,8 @@ function handleNormalMode(state, key, ctrl, meta) {
1950
2589
  return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
1951
2590
  case "f":
1952
2591
  return [state, { type: "search" }];
2592
+ case "d":
2593
+ return [state, { type: "cancel" }];
1953
2594
  case "c":
1954
2595
  return [state, { type: "quit" }];
1955
2596
  case "r":
@@ -2012,10 +2653,12 @@ function handleNormalMode(state, key, ctrl, meta) {
2012
2653
  case "4":
2013
2654
  return [state, { type: "view", value: "playground" }];
2014
2655
  case "5":
2015
- return [state, { type: "view", value: "output" }];
2656
+ return [state, { type: "view", value: "active" }];
2016
2657
  // Function-key style shortcuts (single letter, no prefix needed)
2017
2658
  case "d":
2018
2659
  return [state, { type: "dispatch" }];
2660
+ case "x":
2661
+ return [state, { type: "cancel" }];
2019
2662
  case "q":
2020
2663
  return [state, { type: "quit" }];
2021
2664
  case "?":
@@ -2077,7 +2720,10 @@ function handleSearchMode(state, key) {
2077
2720
  }
2078
2721
  return [state, { type: "none" }];
2079
2722
  }
2080
- function handleInputMode(state, _) {
2723
+ function handleInputMode(state, key, ctrl) {
2724
+ if (ctrl && key === "d") {
2725
+ return [{ ...state, mode: "normal" }, { type: "cancel" }];
2726
+ }
2081
2727
  return [state, { type: "none" }];
2082
2728
  }
2083
2729
 
@@ -2120,6 +2766,13 @@ function useVimMode(callbacks = {}) {
2120
2766
  store.pageDown();
2121
2767
  break;
2122
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
+ }
2123
2776
  break;
2124
2777
  case "focus":
2125
2778
  if (effect.panel != null) {
@@ -2132,7 +2785,7 @@ function useVimMode(callbacks = {}) {
2132
2785
  break;
2133
2786
  case "select": {
2134
2787
  const view = store.activeView;
2135
- if (view === "playground" || view === "plan-gen" || view === "output") {
2788
+ if (view === "playground" || view === "plan-gen" || view === "active") {
2136
2789
  vimState.current = { ...vimState.current, mode: "input" };
2137
2790
  store.setMode("input");
2138
2791
  } else {
@@ -2147,7 +2800,8 @@ function useVimMode(callbacks = {}) {
2147
2800
  const filtered = getFilteredPaletteCommands(vimState.current.commandBuffer);
2148
2801
  const selected = filtered[store.paletteIndex];
2149
2802
  if (selected) {
2150
- callbacks.onCommand?.(selected.name);
2803
+ const cmd = selected.usage?.startsWith("resume:") ? selected.usage : selected.name;
2804
+ callbacks.onCommand?.(cmd);
2151
2805
  }
2152
2806
  } else if (effect.value?.startsWith("__autocomplete__")) {
2153
2807
  } else if (effect.value) {
@@ -2177,7 +2831,7 @@ function useVimMode(callbacks = {}) {
2177
2831
  store.toggleChat();
2178
2832
  break;
2179
2833
  case "view":
2180
- if (effect.value === "dashboard" || effect.value === "plan-gen" || effect.value === "projects" || effect.value === "playground" || effect.value === "output") {
2834
+ if (effect.value === "dashboard" || effect.value === "plan-gen" || effect.value === "projects" || effect.value === "playground" || effect.value === "active") {
2181
2835
  store.setActiveView(effect.value);
2182
2836
  }
2183
2837
  break;
@@ -2187,7 +2841,7 @@ function useVimMode(callbacks = {}) {
2187
2841
  },
2188
2842
  [store, callbacks, exit]
2189
2843
  );
2190
- useInput3((input, key) => {
2844
+ useInput4((input, key) => {
2191
2845
  const searchOpen = useSearchStore.getState().isOpen;
2192
2846
  if (store.showHelp || store.showSearch || store.showDetail || searchOpen) {
2193
2847
  if (key.escape) {
@@ -2201,6 +2855,61 @@ function useVimMode(callbacks = {}) {
2201
2855
  }
2202
2856
  return;
2203
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
+ }
2204
2913
  let keyStr = input;
2205
2914
  if (key.escape) keyStr = "escape";
2206
2915
  else if (key.return) keyStr = "return";
@@ -2231,8 +2940,26 @@ function useVimMode(callbacks = {}) {
2231
2940
  }
2232
2941
 
2233
2942
  // src/tui/hooks/use-polling.ts
2234
- import { useEffect as useEffect2, useCallback as useCallback2 } from "react";
2235
- 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) {
2236
2963
  const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
2237
2964
  const loadProjects = useCallback2(async () => {
2238
2965
  useProjectsStore.getState().setLoading(true);
@@ -2252,6 +2979,13 @@ function usePolling(client, intervalMs = 3e4) {
2252
2979
  usePlanStore.getState().setError(err instanceof Error ? err.message : String(err));
2253
2980
  }
2254
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]);
2255
2989
  const loadMachines = useCallback2(async () => {
2256
2990
  useMachinesStore.getState().setLoading(true);
2257
2991
  try {
@@ -2262,6 +2996,25 @@ function usePolling(client, intervalMs = 3e4) {
2262
2996
  useMachinesStore.getState().setError(err instanceof Error ? err.message : String(err));
2263
2997
  }
2264
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]);
2265
3018
  const loadUsage = useCallback2(async () => {
2266
3019
  try {
2267
3020
  const history = await client.getUsageHistory(1);
@@ -2275,29 +3028,30 @@ function usePolling(client, intervalMs = 3e4) {
2275
3028
  await Promise.allSettled([
2276
3029
  loadProjects(),
2277
3030
  loadMachines(),
3031
+ loadExecutions(),
2278
3032
  loadUsage(),
2279
- ...selectedProjectId ? [loadPlan(selectedProjectId)] : []
3033
+ loadAllPlans()
2280
3034
  ]);
2281
- }, [loadProjects, loadMachines, loadUsage, loadPlan, selectedProjectId]);
2282
- useEffect2(() => {
3035
+ }, [loadProjects, loadMachines, loadExecutions, loadUsage, loadAllPlans]);
3036
+ useEffect4(() => {
2283
3037
  refreshAll2();
2284
3038
  }, [refreshAll2]);
2285
- useEffect2(() => {
3039
+ useEffect4(() => {
2286
3040
  if (selectedProjectId) {
2287
- loadPlan(selectedProjectId);
3041
+ usePlanStore.getState().selectProject(selectedProjectId);
2288
3042
  } else {
2289
3043
  usePlanStore.getState().clear();
2290
3044
  }
2291
- }, [selectedProjectId, loadPlan]);
2292
- useEffect2(() => {
3045
+ }, [selectedProjectId]);
3046
+ useEffect4(() => {
2293
3047
  const timer = setInterval(refreshAll2, intervalMs);
2294
3048
  return () => clearInterval(timer);
2295
3049
  }, [refreshAll2, intervalMs]);
2296
- return { refreshAll: refreshAll2, loadProjects, loadPlan, loadMachines };
3050
+ return { refreshAll: refreshAll2, loadProjects, loadPlan, loadAllPlans, loadMachines };
2297
3051
  }
2298
3052
 
2299
3053
  // src/tui/hooks/use-sse-stream.ts
2300
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
3054
+ import { useEffect as useEffect5, useRef as useRef2 } from "react";
2301
3055
 
2302
3056
  // src/tui/sse-client.ts
2303
3057
  var SSEClient = class {
@@ -2382,17 +3136,18 @@ var SSEClient = class {
2382
3136
  };
2383
3137
 
2384
3138
  // src/tui/hooks/use-sse-stream.ts
2385
- function useSSEStream(client) {
3139
+ function useSSEStream(client, onReconnect) {
2386
3140
  const sseRef = useRef2(null);
2387
3141
  const setConnected = useTuiStore((s) => s.setConnected);
2388
3142
  const setMachineCount = useTuiStore((s) => s.setMachineCount);
2389
3143
  const setLastError = useTuiStore((s) => s.setLastError);
2390
- useEffect3(() => {
3144
+ useEffect5(() => {
2391
3145
  const handler = (event) => {
2392
3146
  switch (event.type) {
2393
3147
  case "__connected":
2394
3148
  setConnected(true);
2395
3149
  setLastError(null);
3150
+ onReconnect?.();
2396
3151
  break;
2397
3152
  case "__disconnected":
2398
3153
  setConnected(false);
@@ -2465,7 +3220,8 @@ function useSSEStream(client) {
2465
3220
  case "task:session_init": {
2466
3221
  const taskId = event.data.taskId;
2467
3222
  const nodeId = event.data.nodeId ?? taskId;
2468
- useExecutionStore.getState().initExecution(taskId, nodeId);
3223
+ const title = event.data.title ?? nodeId;
3224
+ useExecutionStore.getState().initExecution(taskId, nodeId, title);
2469
3225
  useExecutionStore.getState().setWatching(taskId);
2470
3226
  break;
2471
3227
  }
@@ -2509,7 +3265,7 @@ function useSSEStream(client) {
2509
3265
  }
2510
3266
 
2511
3267
  // src/tui/hooks/use-fuzzy-search.ts
2512
- import { useEffect as useEffect4, useCallback as useCallback3 } from "react";
3268
+ import { useEffect as useEffect6, useCallback as useCallback3 } from "react";
2513
3269
  import Fuse from "fuse.js";
2514
3270
  var fuseInstance = null;
2515
3271
  function useFuzzySearch() {
@@ -2517,7 +3273,7 @@ function useFuzzySearch() {
2517
3273
  const nodes = usePlanStore((s) => s.nodes);
2518
3274
  const machines = useMachinesStore((s) => s.machines);
2519
3275
  const { setItems, setResults, query } = useSearchStore();
2520
- useEffect4(() => {
3276
+ useEffect6(() => {
2521
3277
  const items = [
2522
3278
  ...projects.map((p) => ({
2523
3279
  type: "project",
@@ -2556,7 +3312,7 @@ function useFuzzySearch() {
2556
3312
  const results = fuseInstance.search(q, { limit: 20 });
2557
3313
  setResults(results.map((r) => r.item));
2558
3314
  }, [setResults]);
2559
- useEffect4(() => {
3315
+ useEffect6(() => {
2560
3316
  search(query);
2561
3317
  }, [query, search]);
2562
3318
  return { search };
@@ -2606,6 +3362,19 @@ for (const key of Object.keys(handlers)) {
2606
3362
  trie.insert(key);
2607
3363
  }
2608
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
+ }
2609
3378
  const parts = input.split(/\s+/);
2610
3379
  if (parts.length >= 2) {
2611
3380
  const twoWord = `${parts[0]} ${parts[1]}`;
@@ -2653,18 +3422,28 @@ function useCommandParser(client) {
2653
3422
  }
2654
3423
 
2655
3424
  // src/tui/app.tsx
2656
- import { jsx as jsx15 } from "react/jsx-runtime";
3425
+ import { jsx as jsx16 } from "react/jsx-runtime";
2657
3426
  function App({ serverUrl }) {
2658
- const client = useMemo(() => new AstroClient({ serverUrl }), [serverUrl]);
3427
+ const client = useMemo2(() => new AstroClient({ serverUrl }), [serverUrl]);
2659
3428
  const { refreshAll: refreshAll2 } = usePolling(client);
2660
- useSSEStream(client);
3429
+ useSSEStream(client, refreshAll2);
2661
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]);
2662
3441
  const { execute } = useCommandParser(client);
2663
3442
  const onSelect = useCallback5(() => {
2664
3443
  const { focusedPanel, scrollIndex } = useTuiStore.getState();
2665
3444
  switch (focusedPanel) {
2666
3445
  case "projects": {
2667
- const projects = useProjectsStore.getState().projects;
3446
+ const projects = getVisibleProjects(useProjectsStore.getState().projects);
2668
3447
  const idx = scrollIndex.projects;
2669
3448
  if (projects[idx]) {
2670
3449
  useTuiStore.getState().setSelectedProject(projects[idx].id);
@@ -2725,70 +3504,32 @@ function App({ serverUrl }) {
2725
3504
  refreshAll2();
2726
3505
  }, [refreshAll2]);
2727
3506
  const onSessionMessage = useCallback5(async (message) => {
2728
- const { selectedProjectId, activeView } = useTuiStore.getState();
2729
- if (!selectedProjectId) {
2730
- useTuiStore.getState().setLastError("No project selected");
2731
- return;
2732
- }
3507
+ const { selectedProjectId, activeView, selectedNodeId } = useTuiStore.getState();
2733
3508
  const watchingId = useExecutionStore.getState().watchingId;
2734
- const sessionId = useChatStore.getState().sessionId;
2735
- const messages = useChatStore.getState().messages.map((m) => ({
2736
- role: m.role,
2737
- content: m.content
2738
- }));
2739
3509
  if (!watchingId) {
2740
- if (activeView === "plan-gen") {
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
+ }
2741
3517
  await execute(`plan generate ${message}`);
2742
3518
  } else {
2743
3519
  await execute(`playground ${message}`);
2744
3520
  }
2745
3521
  return;
2746
3522
  }
2747
- try {
2748
- useChatStore.getState().setStreaming(true);
2749
- const response = await client.projectChat({
2750
- message,
2751
- sessionId: sessionId ?? void 0,
2752
- projectId: selectedProjectId,
2753
- messages
2754
- });
2755
- if (!response.body) {
2756
- useChatStore.getState().setStreaming(false);
2757
- return;
2758
- }
2759
- const reader = response.body.getReader();
2760
- const decoder = new TextDecoder();
2761
- let buffer = "";
2762
- while (true) {
2763
- const { done, value } = await reader.read();
2764
- if (done) break;
2765
- buffer += decoder.decode(value, { stream: true });
2766
- const lines = buffer.split("\n");
2767
- buffer = lines.pop() ?? "";
2768
- for (const line of lines) {
2769
- if (!line.startsWith("data: ")) continue;
2770
- const data = line.slice(6);
2771
- if (data === "[DONE]") continue;
2772
- try {
2773
- const event = JSON.parse(data);
2774
- if (event.type === "text" && event.text) {
2775
- useChatStore.getState().appendStream(event.text);
2776
- useExecutionStore.getState().appendText(watchingId, event.text);
2777
- } else if (event.type === "session_init" && event.sessionId) {
2778
- useChatStore.getState().setSessionId(event.sessionId);
2779
- } else if (event.type === "done") {
2780
- }
2781
- } catch {
2782
- }
2783
- }
2784
- }
2785
- useChatStore.getState().flushStream();
2786
- useChatStore.getState().setStreaming(false);
2787
- } catch (err) {
2788
- useChatStore.getState().setStreaming(false);
2789
- useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
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}`);
2790
3531
  }
2791
- }, [client, execute]);
3532
+ }, [execute]);
2792
3533
  useVimMode({
2793
3534
  onSelect,
2794
3535
  onCommand,
@@ -2797,13 +3538,13 @@ function App({ serverUrl }) {
2797
3538
  onCancel,
2798
3539
  onRefresh
2799
3540
  });
2800
- return /* @__PURE__ */ jsx15(MainLayout, { onSessionMessage });
3541
+ return /* @__PURE__ */ jsx16(MainLayout, { onSessionMessage });
2801
3542
  }
2802
3543
 
2803
3544
  // src/tui/index.tsx
2804
- import { jsx as jsx16 } from "react/jsx-runtime";
3545
+ import { jsx as jsx17 } from "react/jsx-runtime";
2805
3546
  async function launchTui(serverUrl) {
2806
- render(/* @__PURE__ */ jsx16(App, { serverUrl }), {
3547
+ render(/* @__PURE__ */ jsx17(App, { serverUrl }), {
2807
3548
  exitOnCtrlC: true
2808
3549
  });
2809
3550
  }