@astroanywhere/cli 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-7H7WD7QX.js → chunk-SYY2HHOY.js} +217 -29
- package/dist/client.js +3 -1
- package/dist/index.js +313 -7
- package/dist/tui.js +2812 -0
- package/package.json +13 -4
package/dist/tui.js
ADDED
|
@@ -0,0 +1,2812 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AstroClient
|
|
4
|
+
} from "./chunk-SYY2HHOY.js";
|
|
5
|
+
|
|
6
|
+
// src/tui/index.tsx
|
|
7
|
+
import "react";
|
|
8
|
+
import { render } from "ink";
|
|
9
|
+
|
|
10
|
+
// src/tui/app.tsx
|
|
11
|
+
import { useMemo, useCallback as useCallback5 } from "react";
|
|
12
|
+
|
|
13
|
+
// src/tui/components/layout/main-layout.tsx
|
|
14
|
+
import "react";
|
|
15
|
+
import { Box as Box12, useStdout as useStdout3 } from "ink";
|
|
16
|
+
|
|
17
|
+
// src/tui/components/layout/status-bar.tsx
|
|
18
|
+
import "react";
|
|
19
|
+
import { Box, Text } from "ink";
|
|
20
|
+
|
|
21
|
+
// src/tui/stores/tui-store.ts
|
|
22
|
+
import { create } from "zustand";
|
|
23
|
+
var PANEL_ORDER = ["projects", "plan", "machines", "output", "chat"];
|
|
24
|
+
var useTuiStore = create((set, get) => ({
|
|
25
|
+
mode: "normal",
|
|
26
|
+
commandBuffer: "",
|
|
27
|
+
searchQuery: "",
|
|
28
|
+
pendingKeys: "",
|
|
29
|
+
focusedPanel: "projects",
|
|
30
|
+
panelOrder: PANEL_ORDER,
|
|
31
|
+
selectedProjectId: null,
|
|
32
|
+
selectedNodeId: null,
|
|
33
|
+
selectedMachineId: null,
|
|
34
|
+
selectedExecutionId: null,
|
|
35
|
+
scrollIndex: { projects: 0, plan: 0, machines: 0, output: 0, chat: 0 },
|
|
36
|
+
showHelp: false,
|
|
37
|
+
showSearch: false,
|
|
38
|
+
showDetail: false,
|
|
39
|
+
showChat: false,
|
|
40
|
+
detailType: null,
|
|
41
|
+
detailId: null,
|
|
42
|
+
connected: false,
|
|
43
|
+
machineCount: 0,
|
|
44
|
+
todayCost: 0,
|
|
45
|
+
activeView: "dashboard",
|
|
46
|
+
paletteIndex: 0,
|
|
47
|
+
lastError: null,
|
|
48
|
+
setMode: (mode) => set({ mode }),
|
|
49
|
+
setCommandBuffer: (commandBuffer) => set({ commandBuffer, paletteIndex: 0 }),
|
|
50
|
+
setSearchQuery: (searchQuery) => set({ searchQuery }),
|
|
51
|
+
setPendingKeys: (pendingKeys) => set({ pendingKeys }),
|
|
52
|
+
focusPanel: (panel) => set({ focusedPanel: panel }),
|
|
53
|
+
focusNext: () => {
|
|
54
|
+
const { focusedPanel, panelOrder } = get();
|
|
55
|
+
const idx = panelOrder.indexOf(focusedPanel);
|
|
56
|
+
set({ focusedPanel: panelOrder[(idx + 1) % panelOrder.length] });
|
|
57
|
+
},
|
|
58
|
+
focusPrev: () => {
|
|
59
|
+
const { focusedPanel, panelOrder } = get();
|
|
60
|
+
const idx = panelOrder.indexOf(focusedPanel);
|
|
61
|
+
set({ focusedPanel: panelOrder[(idx - 1 + panelOrder.length) % panelOrder.length] });
|
|
62
|
+
},
|
|
63
|
+
focusByIndex: (idx) => {
|
|
64
|
+
const { panelOrder } = get();
|
|
65
|
+
if (idx >= 0 && idx < panelOrder.length) {
|
|
66
|
+
set({ focusedPanel: panelOrder[idx] });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
setSelectedProject: (selectedProjectId) => set({ selectedProjectId }),
|
|
70
|
+
setSelectedNode: (selectedNodeId) => set({ selectedNodeId }),
|
|
71
|
+
setSelectedMachine: (selectedMachineId) => set({ selectedMachineId }),
|
|
72
|
+
setSelectedExecution: (selectedExecutionId) => set({ selectedExecutionId }),
|
|
73
|
+
scrollUp: (panel) => {
|
|
74
|
+
const p = panel ?? get().focusedPanel;
|
|
75
|
+
set((s) => ({
|
|
76
|
+
scrollIndex: { ...s.scrollIndex, [p]: Math.max(0, s.scrollIndex[p] - 1) }
|
|
77
|
+
}));
|
|
78
|
+
},
|
|
79
|
+
scrollDown: (panel, max) => {
|
|
80
|
+
const p = panel ?? get().focusedPanel;
|
|
81
|
+
set((s) => ({
|
|
82
|
+
scrollIndex: {
|
|
83
|
+
...s.scrollIndex,
|
|
84
|
+
[p]: max != null ? Math.min(max - 1, s.scrollIndex[p] + 1) : s.scrollIndex[p] + 1
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
},
|
|
88
|
+
scrollToTop: (panel) => {
|
|
89
|
+
const p = panel ?? get().focusedPanel;
|
|
90
|
+
set((s) => ({
|
|
91
|
+
scrollIndex: { ...s.scrollIndex, [p]: 0 }
|
|
92
|
+
}));
|
|
93
|
+
},
|
|
94
|
+
scrollToBottom: (panel, max) => {
|
|
95
|
+
const p = panel ?? get().focusedPanel;
|
|
96
|
+
if (max != null && max > 0) {
|
|
97
|
+
set((s) => ({
|
|
98
|
+
scrollIndex: { ...s.scrollIndex, [p]: max - 1 }
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
pageUp: (panel, pageSize = 10) => {
|
|
103
|
+
const p = panel ?? get().focusedPanel;
|
|
104
|
+
set((s) => ({
|
|
105
|
+
scrollIndex: { ...s.scrollIndex, [p]: Math.max(0, s.scrollIndex[p] - pageSize) }
|
|
106
|
+
}));
|
|
107
|
+
},
|
|
108
|
+
pageDown: (panel, max, pageSize = 10) => {
|
|
109
|
+
const p = panel ?? get().focusedPanel;
|
|
110
|
+
set((s) => ({
|
|
111
|
+
scrollIndex: {
|
|
112
|
+
...s.scrollIndex,
|
|
113
|
+
[p]: max != null ? Math.min(max - 1, s.scrollIndex[p] + pageSize) : s.scrollIndex[p] + pageSize
|
|
114
|
+
}
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
117
|
+
toggleHelp: () => set((s) => ({ showHelp: !s.showHelp, showSearch: false })),
|
|
118
|
+
toggleSearch: () => set((s) => ({ showSearch: !s.showSearch, showHelp: false })),
|
|
119
|
+
toggleChat: () => set((s) => ({ showChat: !s.showChat })),
|
|
120
|
+
openDetail: (type, id) => set({ showDetail: true, detailType: type, detailId: id, showHelp: false, showSearch: false }),
|
|
121
|
+
closeDetail: () => set({ showDetail: false, detailType: null, detailId: null }),
|
|
122
|
+
closeOverlays: () => set({ showHelp: false, showSearch: false, showDetail: false, showChat: false, detailType: null, detailId: null }),
|
|
123
|
+
setConnected: (connected) => set({ connected }),
|
|
124
|
+
setMachineCount: (machineCount) => set({ machineCount }),
|
|
125
|
+
setTodayCost: (todayCost) => set({ todayCost }),
|
|
126
|
+
setActiveView: (activeView) => set({ activeView }),
|
|
127
|
+
setPaletteIndex: (paletteIndex) => set({ paletteIndex }),
|
|
128
|
+
setLastError: (lastError) => set({ lastError })
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
// src/tui/lib/format.ts
|
|
132
|
+
function formatRelativeTime(date) {
|
|
133
|
+
if (!date) return "\u2014";
|
|
134
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
const diff = now - d.getTime();
|
|
137
|
+
const seconds = Math.floor(diff / 1e3);
|
|
138
|
+
const minutes = Math.floor(seconds / 60);
|
|
139
|
+
const hours = Math.floor(minutes / 60);
|
|
140
|
+
const days = Math.floor(hours / 24);
|
|
141
|
+
if (seconds < 60) return "just now";
|
|
142
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
143
|
+
if (hours < 24) return `${hours}h ago`;
|
|
144
|
+
if (days < 30) return `${days}d ago`;
|
|
145
|
+
return d.toLocaleDateString();
|
|
146
|
+
}
|
|
147
|
+
function formatCost(usd) {
|
|
148
|
+
if (usd == null) return "\u2014";
|
|
149
|
+
return `$${usd.toFixed(2)}`;
|
|
150
|
+
}
|
|
151
|
+
function truncate(str, maxLen) {
|
|
152
|
+
if (str.length <= maxLen) return str;
|
|
153
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/tui/components/layout/status-bar.tsx
|
|
157
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
158
|
+
var VIEW_LABELS = {
|
|
159
|
+
dashboard: "Dashboard",
|
|
160
|
+
"plan-gen": "Plan",
|
|
161
|
+
projects: "Projects",
|
|
162
|
+
playground: "Playground",
|
|
163
|
+
output: "Output"
|
|
164
|
+
};
|
|
165
|
+
function StatusBar() {
|
|
166
|
+
const connected = useTuiStore((s) => s.connected);
|
|
167
|
+
const machineCount = useTuiStore((s) => s.machineCount);
|
|
168
|
+
const todayCost = useTuiStore((s) => s.todayCost);
|
|
169
|
+
const lastError = useTuiStore((s) => s.lastError);
|
|
170
|
+
const activeView = useTuiStore((s) => s.activeView);
|
|
171
|
+
return /* @__PURE__ */ jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [
|
|
172
|
+
/* @__PURE__ */ jsxs(Box, { gap: 2, children: [
|
|
173
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Astro" }),
|
|
174
|
+
/* @__PURE__ */ jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25CF" : "\u25CB" }),
|
|
175
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
176
|
+
machineCount,
|
|
177
|
+
" machine",
|
|
178
|
+
machineCount !== 1 ? "s" : ""
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: formatCost(todayCost) }),
|
|
181
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
|
|
182
|
+
"[",
|
|
183
|
+
VIEW_LABELS[activeView],
|
|
184
|
+
"]"
|
|
185
|
+
] })
|
|
186
|
+
] }),
|
|
187
|
+
lastError && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "red", children: lastError.length > 50 ? lastError.slice(0, 47) + "..." : lastError }) })
|
|
188
|
+
] });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/tui/components/layout/command-line.tsx
|
|
192
|
+
import "react";
|
|
193
|
+
import { Box as Box2, Text as Text2, useStdout } from "ink";
|
|
194
|
+
|
|
195
|
+
// src/tui/stores/search-store.ts
|
|
196
|
+
import { create as create2 } from "zustand";
|
|
197
|
+
var useSearchStore = create2((set, get) => ({
|
|
198
|
+
items: [],
|
|
199
|
+
query: "",
|
|
200
|
+
results: [],
|
|
201
|
+
selectedIndex: 0,
|
|
202
|
+
isOpen: false,
|
|
203
|
+
setItems: (items) => set({ items }),
|
|
204
|
+
setQuery: (query) => set({ query, selectedIndex: 0 }),
|
|
205
|
+
setResults: (results) => set({ results }),
|
|
206
|
+
setSelectedIndex: (selectedIndex) => set({ selectedIndex }),
|
|
207
|
+
moveUp: () => {
|
|
208
|
+
const { selectedIndex } = get();
|
|
209
|
+
set({ selectedIndex: Math.max(0, selectedIndex - 1) });
|
|
210
|
+
},
|
|
211
|
+
moveDown: () => {
|
|
212
|
+
const { selectedIndex, results } = get();
|
|
213
|
+
set({ selectedIndex: Math.min(results.length - 1, selectedIndex + 1) });
|
|
214
|
+
},
|
|
215
|
+
open: () => set({ isOpen: true, query: "", results: [], selectedIndex: 0 }),
|
|
216
|
+
close: () => set({ isOpen: false, query: "", results: [], selectedIndex: 0 })
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
// src/tui/stores/projects-store.ts
|
|
220
|
+
import { create as create3 } from "zustand";
|
|
221
|
+
var useProjectsStore = create3((set) => ({
|
|
222
|
+
projects: [],
|
|
223
|
+
loading: false,
|
|
224
|
+
error: null,
|
|
225
|
+
setProjects: (projects) => set({ projects, loading: false, error: null }),
|
|
226
|
+
setLoading: (loading) => set({ loading }),
|
|
227
|
+
setError: (error) => set({ error, loading: false })
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
// src/tui/stores/plan-store.ts
|
|
231
|
+
import { create as create4 } from "zustand";
|
|
232
|
+
|
|
233
|
+
// src/tui/lib/status-colors.ts
|
|
234
|
+
var STATUS_COLOR_MAP = {
|
|
235
|
+
// Project statuses
|
|
236
|
+
active: "green",
|
|
237
|
+
archived: "gray",
|
|
238
|
+
// Plan node statuses
|
|
239
|
+
planned: "yellow",
|
|
240
|
+
dispatched: "cyan",
|
|
241
|
+
in_progress: "cyan",
|
|
242
|
+
auto_verified: "green",
|
|
243
|
+
awaiting_approval: "magenta",
|
|
244
|
+
awaiting_judgment: "magenta",
|
|
245
|
+
completed: "blue",
|
|
246
|
+
pruned: "gray",
|
|
247
|
+
// Execution statuses
|
|
248
|
+
pending: "yellow",
|
|
249
|
+
running: "cyan",
|
|
250
|
+
success: "green",
|
|
251
|
+
failure: "red",
|
|
252
|
+
cancelled: "gray",
|
|
253
|
+
error: "red",
|
|
254
|
+
timeout: "red",
|
|
255
|
+
// Machine connection
|
|
256
|
+
connected: "green",
|
|
257
|
+
disconnected: "gray",
|
|
258
|
+
// Health
|
|
259
|
+
on_track: "green",
|
|
260
|
+
at_risk: "yellow",
|
|
261
|
+
off_track: "red"
|
|
262
|
+
};
|
|
263
|
+
function getStatusColor(status) {
|
|
264
|
+
return STATUS_COLOR_MAP[status] ?? "white";
|
|
265
|
+
}
|
|
266
|
+
function getStatusSymbol(status) {
|
|
267
|
+
switch (status) {
|
|
268
|
+
case "completed":
|
|
269
|
+
case "auto_verified":
|
|
270
|
+
case "success":
|
|
271
|
+
return "\u2713";
|
|
272
|
+
// ✓
|
|
273
|
+
case "in_progress":
|
|
274
|
+
case "running":
|
|
275
|
+
case "dispatched":
|
|
276
|
+
return "\u25CB";
|
|
277
|
+
// ○ (spinning handled separately)
|
|
278
|
+
case "planned":
|
|
279
|
+
case "pending":
|
|
280
|
+
return "\u2022";
|
|
281
|
+
// •
|
|
282
|
+
case "failure":
|
|
283
|
+
case "error":
|
|
284
|
+
return "\u2717";
|
|
285
|
+
// ✗
|
|
286
|
+
case "pruned":
|
|
287
|
+
case "cancelled":
|
|
288
|
+
return "\u2500";
|
|
289
|
+
// ─
|
|
290
|
+
case "awaiting_approval":
|
|
291
|
+
case "awaiting_judgment":
|
|
292
|
+
return "?";
|
|
293
|
+
default:
|
|
294
|
+
return "\u2022";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/tui/lib/tree-builder.ts
|
|
299
|
+
function buildTree(nodes, edges) {
|
|
300
|
+
const adj = /* @__PURE__ */ new Map();
|
|
301
|
+
const hasParent = /* @__PURE__ */ new Set();
|
|
302
|
+
for (const edge of edges) {
|
|
303
|
+
const children = adj.get(edge.source) ?? [];
|
|
304
|
+
children.push(edge.target);
|
|
305
|
+
adj.set(edge.source, children);
|
|
306
|
+
hasParent.add(edge.target);
|
|
307
|
+
}
|
|
308
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
309
|
+
for (const node of nodes) {
|
|
310
|
+
if (!node.deletedAt) lookup.set(node.id, node);
|
|
311
|
+
}
|
|
312
|
+
function buildSubtree(nodeId) {
|
|
313
|
+
const node = lookup.get(nodeId);
|
|
314
|
+
if (!node) return null;
|
|
315
|
+
const childIds = adj.get(nodeId) ?? [];
|
|
316
|
+
const children = [];
|
|
317
|
+
for (const childId of childIds) {
|
|
318
|
+
const child = buildSubtree(childId);
|
|
319
|
+
if (child) children.push(child);
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
id: node.id,
|
|
323
|
+
title: node.title,
|
|
324
|
+
status: node.status,
|
|
325
|
+
type: node.type,
|
|
326
|
+
children
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const roots = [];
|
|
330
|
+
for (const node of nodes) {
|
|
331
|
+
if (!node.deletedAt && !hasParent.has(node.id)) {
|
|
332
|
+
const tree = buildSubtree(node.id);
|
|
333
|
+
if (tree) roots.push(tree);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return roots;
|
|
337
|
+
}
|
|
338
|
+
function renderTreeLines(roots, collapsedSet) {
|
|
339
|
+
const lines = [];
|
|
340
|
+
function walk(node, prefix, isLast, depth) {
|
|
341
|
+
const connector = depth === 0 ? isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 " : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
342
|
+
const symbol = getStatusSymbol(node.status);
|
|
343
|
+
const collapsed = collapsedSet?.has(node.id) && node.children.length > 0;
|
|
344
|
+
const expandIcon = node.children.length > 0 ? collapsed ? "\u25B6 " : "\u25BC " : " ";
|
|
345
|
+
lines.push({
|
|
346
|
+
id: node.id,
|
|
347
|
+
text: `${prefix}${connector}${expandIcon}${symbol} ${node.title} [${node.status}]`,
|
|
348
|
+
status: node.status,
|
|
349
|
+
depth,
|
|
350
|
+
isLeaf: node.children.length === 0,
|
|
351
|
+
nodeTitle: node.title
|
|
352
|
+
});
|
|
353
|
+
if (collapsed) return;
|
|
354
|
+
const childPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
355
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
356
|
+
walk(node.children[i], childPrefix, i === node.children.length - 1, depth + 1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (let i = 0; i < roots.length; i++) {
|
|
360
|
+
walk(roots[i], "", i === roots.length - 1, 0);
|
|
361
|
+
}
|
|
362
|
+
return lines;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/tui/stores/plan-store.ts
|
|
366
|
+
var usePlanStore = create4((set, get) => ({
|
|
367
|
+
projectId: null,
|
|
368
|
+
nodes: [],
|
|
369
|
+
edges: [],
|
|
370
|
+
treeRoots: [],
|
|
371
|
+
treeLines: [],
|
|
372
|
+
collapsedNodes: /* @__PURE__ */ new Set(),
|
|
373
|
+
loading: false,
|
|
374
|
+
error: null,
|
|
375
|
+
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 });
|
|
380
|
+
},
|
|
381
|
+
setLoading: (loading) => set({ loading }),
|
|
382
|
+
setError: (error) => set({ error, loading: false }),
|
|
383
|
+
toggleCollapse: (nodeId) => {
|
|
384
|
+
const { collapsedNodes, treeRoots } = get();
|
|
385
|
+
const next = new Set(collapsedNodes);
|
|
386
|
+
if (next.has(nodeId)) {
|
|
387
|
+
next.delete(nodeId);
|
|
388
|
+
} else {
|
|
389
|
+
next.add(nodeId);
|
|
390
|
+
}
|
|
391
|
+
const treeLines = renderTreeLines(treeRoots, next);
|
|
392
|
+
set({ collapsedNodes: next, treeLines });
|
|
393
|
+
},
|
|
394
|
+
updateNodeStatus: (nodeId, status) => {
|
|
395
|
+
const { nodes, edges, collapsedNodes } = get();
|
|
396
|
+
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 });
|
|
400
|
+
},
|
|
401
|
+
clear: () => set({
|
|
402
|
+
projectId: null,
|
|
403
|
+
nodes: [],
|
|
404
|
+
edges: [],
|
|
405
|
+
treeRoots: [],
|
|
406
|
+
treeLines: [],
|
|
407
|
+
loading: false,
|
|
408
|
+
error: null
|
|
409
|
+
})
|
|
410
|
+
}));
|
|
411
|
+
|
|
412
|
+
// src/tui/stores/machines-store.ts
|
|
413
|
+
import { create as create5 } from "zustand";
|
|
414
|
+
var useMachinesStore = create5((set, get) => ({
|
|
415
|
+
machines: [],
|
|
416
|
+
loading: false,
|
|
417
|
+
error: null,
|
|
418
|
+
setMachines: (machines) => set({ machines, loading: false, error: null }),
|
|
419
|
+
updateMachine: (id, patch) => {
|
|
420
|
+
set((s) => ({
|
|
421
|
+
machines: s.machines.map((m) => m.id === id ? { ...m, ...patch } : m)
|
|
422
|
+
}));
|
|
423
|
+
},
|
|
424
|
+
removeMachine: (id) => {
|
|
425
|
+
set((s) => ({ machines: s.machines.filter((m) => m.id !== id) }));
|
|
426
|
+
},
|
|
427
|
+
addMachine: (machine) => {
|
|
428
|
+
const { machines } = get();
|
|
429
|
+
const existing = machines.findIndex((m) => m.id === machine.id);
|
|
430
|
+
if (existing >= 0) {
|
|
431
|
+
set({ machines: machines.map((m, i) => i === existing ? machine : m) });
|
|
432
|
+
} else {
|
|
433
|
+
set({ machines: [...machines, machine] });
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
setLoading: (loading) => set({ loading }),
|
|
437
|
+
setError: (error) => set({ error, loading: false })
|
|
438
|
+
}));
|
|
439
|
+
|
|
440
|
+
// src/tui/stores/execution-store.ts
|
|
441
|
+
import { create as create6 } from "zustand";
|
|
442
|
+
var MAX_LINES = 5e3;
|
|
443
|
+
function flushToolDots(output) {
|
|
444
|
+
if (output.pendingToolCount === 0) return output.lines;
|
|
445
|
+
const dots = "\xB7".repeat(output.pendingToolCount);
|
|
446
|
+
const line = `[Tool Call] ${dots}`;
|
|
447
|
+
const lines = [...output.lines, line];
|
|
448
|
+
output.pendingToolCount = 0;
|
|
449
|
+
return lines;
|
|
450
|
+
}
|
|
451
|
+
function trimRingBuffer(lines) {
|
|
452
|
+
if (lines.length > MAX_LINES) {
|
|
453
|
+
lines.splice(0, lines.length - MAX_LINES);
|
|
454
|
+
}
|
|
455
|
+
return lines;
|
|
456
|
+
}
|
|
457
|
+
var useExecutionStore = create6((set, get) => ({
|
|
458
|
+
outputs: /* @__PURE__ */ new Map(),
|
|
459
|
+
watchingId: null,
|
|
460
|
+
pendingApproval: null,
|
|
461
|
+
initExecution: (executionId, nodeId) => {
|
|
462
|
+
const { outputs } = get();
|
|
463
|
+
const next = new Map(outputs);
|
|
464
|
+
next.set(executionId, {
|
|
465
|
+
executionId,
|
|
466
|
+
nodeId,
|
|
467
|
+
lines: [],
|
|
468
|
+
status: "running",
|
|
469
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
470
|
+
pendingToolCount: 0
|
|
471
|
+
});
|
|
472
|
+
set({ outputs: next });
|
|
473
|
+
},
|
|
474
|
+
appendToolCall: (executionId) => {
|
|
475
|
+
const { outputs } = get();
|
|
476
|
+
const current = outputs.get(executionId);
|
|
477
|
+
if (!current) return;
|
|
478
|
+
const next = new Map(outputs);
|
|
479
|
+
next.set(executionId, { ...current, pendingToolCount: current.pendingToolCount + 1 });
|
|
480
|
+
set({ outputs: next });
|
|
481
|
+
},
|
|
482
|
+
appendFileChange: (executionId, path, action, added, removed) => {
|
|
483
|
+
const { outputs } = get();
|
|
484
|
+
const current = outputs.get(executionId);
|
|
485
|
+
if (!current) return;
|
|
486
|
+
const lines = flushToolDots({ ...current });
|
|
487
|
+
const stats = [
|
|
488
|
+
added != null && added > 0 ? `+${added}` : null,
|
|
489
|
+
removed != null && removed > 0 ? `-${removed}` : null
|
|
490
|
+
].filter(Boolean).join(" ");
|
|
491
|
+
lines.push(`[${action}] ${path}${stats ? ` (${stats})` : ""}`);
|
|
492
|
+
trimRingBuffer(lines);
|
|
493
|
+
const next = new Map(outputs);
|
|
494
|
+
next.set(executionId, { ...current, lines, pendingToolCount: 0 });
|
|
495
|
+
set({ outputs: next });
|
|
496
|
+
},
|
|
497
|
+
appendLine: (executionId, line) => {
|
|
498
|
+
const { outputs } = get();
|
|
499
|
+
const current = outputs.get(executionId);
|
|
500
|
+
if (!current) return;
|
|
501
|
+
const lines = flushToolDots({ ...current });
|
|
502
|
+
lines.push(line);
|
|
503
|
+
trimRingBuffer(lines);
|
|
504
|
+
const next = new Map(outputs);
|
|
505
|
+
next.set(executionId, { ...current, lines, pendingToolCount: 0 });
|
|
506
|
+
set({ outputs: next });
|
|
507
|
+
},
|
|
508
|
+
appendText: (executionId, text) => {
|
|
509
|
+
const { outputs } = get();
|
|
510
|
+
const current = outputs.get(executionId);
|
|
511
|
+
if (!current) return;
|
|
512
|
+
const lines = flushToolDots({ ...current });
|
|
513
|
+
const newLines = text.split("\n");
|
|
514
|
+
if (lines.length > 0 && newLines.length > 0) {
|
|
515
|
+
lines[lines.length - 1] += newLines[0];
|
|
516
|
+
for (let i = 1; i < newLines.length; i++) {
|
|
517
|
+
lines.push(newLines[i]);
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
lines.push(...newLines);
|
|
521
|
+
}
|
|
522
|
+
trimRingBuffer(lines);
|
|
523
|
+
const next = new Map(outputs);
|
|
524
|
+
next.set(executionId, { ...current, lines, pendingToolCount: 0 });
|
|
525
|
+
set({ outputs: next });
|
|
526
|
+
},
|
|
527
|
+
setStatus: (executionId, status) => {
|
|
528
|
+
const { outputs } = get();
|
|
529
|
+
const current = outputs.get(executionId);
|
|
530
|
+
if (!current) return;
|
|
531
|
+
const lines = flushToolDots({ ...current });
|
|
532
|
+
const next = new Map(outputs);
|
|
533
|
+
next.set(executionId, { ...current, lines, status, pendingToolCount: 0 });
|
|
534
|
+
set({ outputs: next });
|
|
535
|
+
},
|
|
536
|
+
setWatching: (watchingId) => set({ watchingId }),
|
|
537
|
+
setPendingApproval: (pendingApproval) => set({ pendingApproval }),
|
|
538
|
+
clear: (executionId) => {
|
|
539
|
+
const { outputs } = get();
|
|
540
|
+
const next = new Map(outputs);
|
|
541
|
+
next.delete(executionId);
|
|
542
|
+
set({ outputs: next });
|
|
543
|
+
}
|
|
544
|
+
}));
|
|
545
|
+
|
|
546
|
+
// src/tui/commands/handlers.ts
|
|
547
|
+
var PALETTE_COMMANDS = [
|
|
548
|
+
{ name: "project list", description: "List all projects" },
|
|
549
|
+
{ name: "project show", description: "Show project details", usage: "project show <id>" },
|
|
550
|
+
{ name: "project create", description: "Create a new project", usage: "project create <name>" },
|
|
551
|
+
{ name: "project delete", description: "Delete a project", usage: "project delete <id>" },
|
|
552
|
+
{ name: "plan tree", description: "Show plan tree for selected project" },
|
|
553
|
+
{ name: "plan create-node", description: "Create a plan node", usage: "plan create-node <title>" },
|
|
554
|
+
{ name: "plan update-node", description: "Update a plan node field", usage: "plan update-node <id> <field> <value>" },
|
|
555
|
+
{ name: "dispatch", description: "Dispatch selected task for execution", usage: "dispatch [nodeId]" },
|
|
556
|
+
{ name: "cancel", description: "Cancel running execution", usage: "cancel [executionId]" },
|
|
557
|
+
{ name: "steer", description: "Send guidance to running task", usage: "steer <message>" },
|
|
558
|
+
{ name: "watch", description: "Watch execution output", usage: "watch <executionId>" },
|
|
559
|
+
{ name: "env list", description: "List machines/environments" },
|
|
560
|
+
{ name: "env status", description: "Show relay status" },
|
|
561
|
+
{ name: "search", description: "Search projects and tasks", usage: "search <query>" },
|
|
562
|
+
{ name: "activity", description: "Show recent activity feed" },
|
|
563
|
+
{ name: "playground", description: "Start a playground (Cloud Code) session", usage: "playground <description>" },
|
|
564
|
+
{ name: "plan generate", description: "Generate a plan using AI", usage: "plan generate <description>" },
|
|
565
|
+
{ name: "refresh", description: "Refresh all data" },
|
|
566
|
+
{ name: "help", description: "Toggle keybinding reference" },
|
|
567
|
+
{ name: "quit", description: "Exit the TUI" }
|
|
568
|
+
];
|
|
569
|
+
var handlers = {
|
|
570
|
+
// ── Quit ──
|
|
571
|
+
q: async () => {
|
|
572
|
+
process.exit(0);
|
|
573
|
+
},
|
|
574
|
+
quit: async () => {
|
|
575
|
+
process.exit(0);
|
|
576
|
+
},
|
|
577
|
+
// ── Refresh ──
|
|
578
|
+
r: async (_args, client) => {
|
|
579
|
+
await refreshAll(client);
|
|
580
|
+
},
|
|
581
|
+
refresh: async (_args, client) => {
|
|
582
|
+
await refreshAll(client);
|
|
583
|
+
},
|
|
584
|
+
// ── Project commands ──
|
|
585
|
+
"project list": async (_args, client) => {
|
|
586
|
+
const projects = await client.listProjects();
|
|
587
|
+
useProjectsStore.getState().setProjects(projects);
|
|
588
|
+
},
|
|
589
|
+
"project show": async (args, client) => {
|
|
590
|
+
const id = args[0];
|
|
591
|
+
if (!id) return;
|
|
592
|
+
try {
|
|
593
|
+
const project = await client.resolveProject(id);
|
|
594
|
+
useTuiStore.getState().openDetail("project", project.id);
|
|
595
|
+
} catch {
|
|
596
|
+
useTuiStore.getState().setLastError(`Project not found: ${id}`);
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
"project create": async (args, client) => {
|
|
600
|
+
const name = args.join(" ");
|
|
601
|
+
if (!name) {
|
|
602
|
+
useTuiStore.getState().setLastError("Usage: :project create <name>");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
await client.createProject({ name });
|
|
606
|
+
const projects = await client.listProjects();
|
|
607
|
+
useProjectsStore.getState().setProjects(projects);
|
|
608
|
+
},
|
|
609
|
+
"project delete": async (args, client) => {
|
|
610
|
+
const id = args[0];
|
|
611
|
+
if (!id) return;
|
|
612
|
+
try {
|
|
613
|
+
const project = await client.resolveProject(id);
|
|
614
|
+
await client.deleteProject(project.id);
|
|
615
|
+
const projects = await client.listProjects();
|
|
616
|
+
useProjectsStore.getState().setProjects(projects);
|
|
617
|
+
} catch (err) {
|
|
618
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
// ── Plan commands ──
|
|
622
|
+
"plan tree": async (_args, client) => {
|
|
623
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
624
|
+
if (!projectId) {
|
|
625
|
+
useTuiStore.getState().setLastError("No project selected");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const { nodes, edges } = await client.getPlan(projectId);
|
|
629
|
+
usePlanStore.getState().setPlan(projectId, nodes, edges);
|
|
630
|
+
useTuiStore.getState().focusPanel("plan");
|
|
631
|
+
},
|
|
632
|
+
"plan create-node": async (args, client) => {
|
|
633
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
634
|
+
if (!projectId) {
|
|
635
|
+
useTuiStore.getState().setLastError("No project selected");
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const title = args.join(" ");
|
|
639
|
+
if (!title) {
|
|
640
|
+
useTuiStore.getState().setLastError("Usage: :plan create-node <title>");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
644
|
+
await client.createPlanNode({ id, projectId, title });
|
|
645
|
+
const { nodes, edges } = await client.getPlan(projectId);
|
|
646
|
+
usePlanStore.getState().setPlan(projectId, nodes, edges);
|
|
647
|
+
},
|
|
648
|
+
"plan update-node": async (args, client) => {
|
|
649
|
+
const [nodeId, field, ...rest] = args;
|
|
650
|
+
if (!nodeId || !field) {
|
|
651
|
+
useTuiStore.getState().setLastError("Usage: :plan update-node <nodeId> <field> <value>");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const value = rest.join(" ");
|
|
655
|
+
await client.updatePlanNode(nodeId, { [field]: value });
|
|
656
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
657
|
+
if (projectId) {
|
|
658
|
+
const { nodes, edges } = await client.getPlan(projectId);
|
|
659
|
+
usePlanStore.getState().setPlan(projectId, nodes, edges);
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
// ── Dispatch ──
|
|
663
|
+
d: async (args, client) => {
|
|
664
|
+
await handlers.dispatch(args, client);
|
|
665
|
+
},
|
|
666
|
+
dispatch: async (args, client) => {
|
|
667
|
+
const nodeId = args[0] ?? useTuiStore.getState().selectedNodeId;
|
|
668
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
669
|
+
if (!nodeId || !projectId) {
|
|
670
|
+
useTuiStore.getState().setLastError("No node/project selected for dispatch");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const response = await client.dispatchTask({ nodeId, projectId });
|
|
675
|
+
const execId = `exec-${Date.now()}`;
|
|
676
|
+
useExecutionStore.getState().initExecution(execId, nodeId);
|
|
677
|
+
useExecutionStore.getState().setWatching(execId);
|
|
678
|
+
useTuiStore.getState().focusPanel("output");
|
|
679
|
+
if (response.body) {
|
|
680
|
+
const reader = response.body.getReader();
|
|
681
|
+
const decoder = new TextDecoder();
|
|
682
|
+
let buffer = "";
|
|
683
|
+
while (true) {
|
|
684
|
+
const { done, value } = await reader.read();
|
|
685
|
+
if (done) break;
|
|
686
|
+
buffer += decoder.decode(value, { stream: true });
|
|
687
|
+
const lines = buffer.split("\n");
|
|
688
|
+
buffer = lines.pop() ?? "";
|
|
689
|
+
for (const line of lines) {
|
|
690
|
+
if (line.startsWith("data: ")) {
|
|
691
|
+
try {
|
|
692
|
+
const event = JSON.parse(line.slice(6));
|
|
693
|
+
const eventType = event.type;
|
|
694
|
+
if (eventType === "text") {
|
|
695
|
+
useExecutionStore.getState().appendText(execId, event.content ?? "");
|
|
696
|
+
} else if (eventType === "tool_use") {
|
|
697
|
+
useExecutionStore.getState().appendToolCall(execId, event.name ?? "");
|
|
698
|
+
} else if (eventType === "result") {
|
|
699
|
+
useExecutionStore.getState().setStatus(execId, event.status ?? "completed");
|
|
700
|
+
} else if (eventType === "error") {
|
|
701
|
+
useExecutionStore.getState().appendLine(execId, `[error] ${event.message}`);
|
|
702
|
+
useExecutionStore.getState().setStatus(execId, "failure");
|
|
703
|
+
}
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} catch (err) {
|
|
711
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
// ── Cancel ──
|
|
715
|
+
c: async (args, client) => {
|
|
716
|
+
await handlers.cancel(args, client);
|
|
717
|
+
},
|
|
718
|
+
cancel: async (args, client) => {
|
|
719
|
+
const executionId = args[0] ?? useExecutionStore.getState().watchingId;
|
|
720
|
+
if (!executionId) {
|
|
721
|
+
useTuiStore.getState().setLastError("No execution to cancel");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
await client.cancelTask({ executionId });
|
|
726
|
+
useExecutionStore.getState().setStatus(executionId, "cancelled");
|
|
727
|
+
} catch (err) {
|
|
728
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
// ── Steer ──
|
|
732
|
+
s: async (args, client) => {
|
|
733
|
+
await handlers.steer(args, client);
|
|
734
|
+
},
|
|
735
|
+
steer: async (args, client) => {
|
|
736
|
+
const message = args.join(" ");
|
|
737
|
+
if (!message) {
|
|
738
|
+
useTuiStore.getState().setLastError("Usage: :steer <message>");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const executionId = useExecutionStore.getState().watchingId;
|
|
742
|
+
const selectedMachineId = useTuiStore.getState().selectedMachineId;
|
|
743
|
+
if (!executionId || !selectedMachineId) {
|
|
744
|
+
useTuiStore.getState().setLastError("No active execution/machine to steer");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
await client.steerTask({ taskId: executionId, machineId: selectedMachineId, message });
|
|
749
|
+
} catch (err) {
|
|
750
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
// ── Watch ──
|
|
754
|
+
watch: async (args) => {
|
|
755
|
+
const executionId = args[0];
|
|
756
|
+
if (!executionId) {
|
|
757
|
+
useTuiStore.getState().setLastError("Usage: :watch <executionId>");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
useExecutionStore.getState().setWatching(executionId);
|
|
761
|
+
useTuiStore.getState().focusPanel("output");
|
|
762
|
+
},
|
|
763
|
+
// ── Env ──
|
|
764
|
+
"env list": async (_args, client) => {
|
|
765
|
+
const machines = await client.listMachines();
|
|
766
|
+
useMachinesStore.getState().setMachines(machines);
|
|
767
|
+
useTuiStore.getState().focusPanel("machines");
|
|
768
|
+
},
|
|
769
|
+
"env status": async (_args, client) => {
|
|
770
|
+
const status = await client.getRelayStatus();
|
|
771
|
+
useTuiStore.getState().setLastError(JSON.stringify(status, null, 2));
|
|
772
|
+
},
|
|
773
|
+
// ── Search ──
|
|
774
|
+
search: async (args, client) => {
|
|
775
|
+
const query = args.join(" ");
|
|
776
|
+
if (!query) return;
|
|
777
|
+
try {
|
|
778
|
+
await client.search(query);
|
|
779
|
+
useTuiStore.getState().toggleSearch();
|
|
780
|
+
} catch (err) {
|
|
781
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
// ── Activity ──
|
|
785
|
+
activity: async (_args, client) => {
|
|
786
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
787
|
+
try {
|
|
788
|
+
const activities = await client.listActivities(projectId ? { projectId } : void 0);
|
|
789
|
+
for (const a of activities.slice(0, 20)) {
|
|
790
|
+
useExecutionStore.getState().appendLine("activity", `[${a.type}] ${a.title}`);
|
|
791
|
+
}
|
|
792
|
+
useExecutionStore.getState().setWatching("activity");
|
|
793
|
+
useTuiStore.getState().focusPanel("output");
|
|
794
|
+
} catch (err) {
|
|
795
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
// ── Playground ──
|
|
799
|
+
playground: async (args, client) => {
|
|
800
|
+
const description = args.join(" ");
|
|
801
|
+
if (!description) {
|
|
802
|
+
useTuiStore.getState().setLastError("Usage: playground <description>");
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
806
|
+
if (!projectId) {
|
|
807
|
+
useTuiStore.getState().setLastError("No project selected. Select a project first.");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const nodeId = `playground-${projectId}-${Date.now()}`;
|
|
811
|
+
try {
|
|
812
|
+
const response = await client.dispatchTask({
|
|
813
|
+
nodeId,
|
|
814
|
+
projectId,
|
|
815
|
+
skipSafetyCheck: true,
|
|
816
|
+
description,
|
|
817
|
+
title: `Playground: ${description.slice(0, 50)}`
|
|
818
|
+
});
|
|
819
|
+
useExecutionStore.getState().initExecution(nodeId, nodeId);
|
|
820
|
+
useExecutionStore.getState().setWatching(nodeId);
|
|
821
|
+
useTuiStore.getState().setActiveView("playground");
|
|
822
|
+
await streamExecution(nodeId, response);
|
|
823
|
+
} catch (err) {
|
|
824
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
// ── Plan Generate ──
|
|
828
|
+
"plan generate": async (args, client) => {
|
|
829
|
+
const description = args.join(" ");
|
|
830
|
+
if (!description) {
|
|
831
|
+
useTuiStore.getState().setLastError("Usage: plan generate <description>");
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
835
|
+
if (!projectId) {
|
|
836
|
+
useTuiStore.getState().setLastError("No project selected. Select a project first.");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const nodeId = `plan-${projectId}`;
|
|
840
|
+
try {
|
|
841
|
+
const response = await client.dispatchTask({
|
|
842
|
+
nodeId,
|
|
843
|
+
projectId,
|
|
844
|
+
isInteractivePlan: true,
|
|
845
|
+
description
|
|
846
|
+
});
|
|
847
|
+
useExecutionStore.getState().initExecution(nodeId, nodeId);
|
|
848
|
+
useExecutionStore.getState().setWatching(nodeId);
|
|
849
|
+
useTuiStore.getState().setActiveView("plan-gen");
|
|
850
|
+
await streamExecution(nodeId, response);
|
|
851
|
+
useExecutionStore.getState().appendLine(nodeId, "[plan] Refreshing plan...");
|
|
852
|
+
const { nodes, edges } = await client.getPlan(projectId);
|
|
853
|
+
usePlanStore.getState().setPlan(projectId, nodes, edges);
|
|
854
|
+
useTuiStore.getState().setActiveView("dashboard");
|
|
855
|
+
} catch (err) {
|
|
856
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
// ── Help ──
|
|
860
|
+
help: async () => {
|
|
861
|
+
useTuiStore.getState().toggleHelp();
|
|
862
|
+
},
|
|
863
|
+
"?": async () => {
|
|
864
|
+
useTuiStore.getState().toggleHelp();
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
async function streamExecution(executionId, response) {
|
|
868
|
+
if (!response.body) return;
|
|
869
|
+
const reader = response.body.getReader();
|
|
870
|
+
const decoder = new TextDecoder();
|
|
871
|
+
let buffer = "";
|
|
872
|
+
while (true) {
|
|
873
|
+
const { done, value } = await reader.read();
|
|
874
|
+
if (done) break;
|
|
875
|
+
buffer += decoder.decode(value, { stream: true });
|
|
876
|
+
const lines = buffer.split("\n");
|
|
877
|
+
buffer = lines.pop() ?? "";
|
|
878
|
+
for (const line of lines) {
|
|
879
|
+
if (!line.startsWith("data: ")) continue;
|
|
880
|
+
try {
|
|
881
|
+
const event = JSON.parse(line.slice(6));
|
|
882
|
+
const eventType = event.type;
|
|
883
|
+
if (eventType === "text") {
|
|
884
|
+
useExecutionStore.getState().appendText(executionId, event.content ?? "");
|
|
885
|
+
} else if (eventType === "tool_use") {
|
|
886
|
+
useExecutionStore.getState().appendToolCall(executionId, event.name ?? "");
|
|
887
|
+
} else if (eventType === "file_change") {
|
|
888
|
+
useExecutionStore.getState().appendFileChange(
|
|
889
|
+
executionId,
|
|
890
|
+
event.path ?? "",
|
|
891
|
+
event.action ?? "modified",
|
|
892
|
+
event.linesAdded,
|
|
893
|
+
event.linesRemoved
|
|
894
|
+
);
|
|
895
|
+
} else if (eventType === "result" || eventType === "done") {
|
|
896
|
+
useExecutionStore.getState().setStatus(executionId, event.status ?? "completed");
|
|
897
|
+
} else if (eventType === "error") {
|
|
898
|
+
useExecutionStore.getState().appendLine(executionId, `[error] ${event.message}`);
|
|
899
|
+
useExecutionStore.getState().setStatus(executionId, "failure");
|
|
900
|
+
} else if (eventType === "plan_result") {
|
|
901
|
+
useExecutionStore.getState().appendLine(executionId, "[plan] Plan generated successfully");
|
|
902
|
+
} else if (eventType === "approval_request") {
|
|
903
|
+
useExecutionStore.getState().setPendingApproval({
|
|
904
|
+
requestId: event.requestId,
|
|
905
|
+
question: event.question,
|
|
906
|
+
options: event.options,
|
|
907
|
+
machineId: event.machineId,
|
|
908
|
+
taskId: event.taskId
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async function refreshAll(client) {
|
|
917
|
+
try {
|
|
918
|
+
const [projects, machines] = await Promise.all([
|
|
919
|
+
client.listProjects(),
|
|
920
|
+
client.listMachines()
|
|
921
|
+
]);
|
|
922
|
+
useProjectsStore.getState().setProjects(projects);
|
|
923
|
+
useMachinesStore.getState().setMachines(machines);
|
|
924
|
+
useTuiStore.getState().setMachineCount(machines.filter((m) => m.isConnected).length);
|
|
925
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
926
|
+
if (projectId) {
|
|
927
|
+
const { nodes, edges } = await client.getPlan(projectId);
|
|
928
|
+
usePlanStore.getState().setPlan(projectId, nodes, edges);
|
|
929
|
+
}
|
|
930
|
+
} catch (err) {
|
|
931
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/tui/commands/palette-filter.ts
|
|
936
|
+
function getFilteredPaletteCommands(query) {
|
|
937
|
+
if (!query) return PALETTE_COMMANDS;
|
|
938
|
+
const lower = query.toLowerCase();
|
|
939
|
+
return PALETTE_COMMANDS.filter(
|
|
940
|
+
(cmd) => cmd.name.toLowerCase().includes(lower) || cmd.description.toLowerCase().includes(lower)
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/tui/components/layout/command-line.tsx
|
|
945
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
946
|
+
var SHORTCUTS = [
|
|
947
|
+
{ key: "1", label: "Dashboard" },
|
|
948
|
+
{ key: "2", label: "Plan" },
|
|
949
|
+
{ key: "3", label: "Projects" },
|
|
950
|
+
{ key: "4", label: "Playground" },
|
|
951
|
+
{ key: "5", label: "Output" },
|
|
952
|
+
{ key: "/", label: "Search" },
|
|
953
|
+
{ key: "C-p", label: "Commands" },
|
|
954
|
+
{ key: "d", label: "Dispatch" },
|
|
955
|
+
{ key: "?", label: "Help" },
|
|
956
|
+
{ key: "q", label: "Quit" }
|
|
957
|
+
];
|
|
958
|
+
var TYPE_LABELS = {
|
|
959
|
+
project: { label: "proj", color: "cyan" },
|
|
960
|
+
task: { label: "task", color: "yellow" },
|
|
961
|
+
machine: { label: "env", color: "green" },
|
|
962
|
+
execution: { label: "exec", color: "magenta" }
|
|
963
|
+
};
|
|
964
|
+
function CommandLine({ height }) {
|
|
965
|
+
const mode = useTuiStore((s) => s.mode);
|
|
966
|
+
const commandBuffer = useTuiStore((s) => s.commandBuffer);
|
|
967
|
+
const paletteIndex = useTuiStore((s) => s.paletteIndex);
|
|
968
|
+
const searchOpen = useSearchStore((s) => s.isOpen);
|
|
969
|
+
const searchQuery = useSearchStore((s) => s.query);
|
|
970
|
+
const searchResults = useSearchStore((s) => s.results);
|
|
971
|
+
const searchItems = useSearchStore((s) => s.items);
|
|
972
|
+
const searchIndex = useSearchStore((s) => s.selectedIndex);
|
|
973
|
+
const { stdout } = useStdout();
|
|
974
|
+
const termWidth = stdout?.columns ?? 80;
|
|
975
|
+
const listHeight = Math.max(1, height - 3);
|
|
976
|
+
if (searchOpen) {
|
|
977
|
+
const displayList = searchQuery.length > 0 ? searchResults : searchItems;
|
|
978
|
+
const visible = displayList.slice(0, listHeight);
|
|
979
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height, borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [
|
|
980
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
981
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "/ " }),
|
|
982
|
+
/* @__PURE__ */ jsx2(Text2, { children: searchQuery }),
|
|
983
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2588" }),
|
|
984
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
985
|
+
" (",
|
|
986
|
+
"\u2191\u2193",
|
|
987
|
+
" navigate, Enter to go, Esc to close)"
|
|
988
|
+
] })
|
|
989
|
+
] }),
|
|
990
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
991
|
+
visible.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: searchQuery.length > 0 ? " No results" : " No items" }) : visible.map((item, i) => {
|
|
992
|
+
const isSelected = i === searchIndex;
|
|
993
|
+
const typeInfo = TYPE_LABELS[item.type] ?? { label: item.type, color: "white" };
|
|
994
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
995
|
+
/* @__PURE__ */ jsx2(Text2, { inverse: isSelected, bold: isSelected, color: isSelected ? "cyan" : void 0, children: isSelected ? " > " : " " }),
|
|
996
|
+
/* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, color: isSelected ? "cyan" : typeInfo.color, children: [
|
|
997
|
+
"[",
|
|
998
|
+
typeInfo.label,
|
|
999
|
+
"]"
|
|
1000
|
+
] }),
|
|
1001
|
+
/* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, bold: isSelected, wrap: "truncate", children: [
|
|
1002
|
+
" ",
|
|
1003
|
+
item.title
|
|
1004
|
+
] }),
|
|
1005
|
+
item.status && /* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, color: isSelected ? void 0 : getStatusColor(item.status), dimColor: !isSelected, children: [
|
|
1006
|
+
" ",
|
|
1007
|
+
item.status
|
|
1008
|
+
] })
|
|
1009
|
+
] }, `${item.type}-${item.id}`);
|
|
1010
|
+
}),
|
|
1011
|
+
displayList.length > listHeight && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1012
|
+
" ...and ",
|
|
1013
|
+
displayList.length - listHeight,
|
|
1014
|
+
" more"
|
|
1015
|
+
] })
|
|
1016
|
+
] })
|
|
1017
|
+
] });
|
|
1018
|
+
}
|
|
1019
|
+
if (mode === "palette") {
|
|
1020
|
+
const filtered = getFilteredPaletteCommands(commandBuffer);
|
|
1021
|
+
const visible = filtered.slice(0, listHeight);
|
|
1022
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height, borderStyle: "single", borderColor: "yellow", paddingX: 1, children: [
|
|
1023
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1024
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: "> " }),
|
|
1025
|
+
/* @__PURE__ */ jsx2(Text2, { children: commandBuffer }),
|
|
1026
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2588" }),
|
|
1027
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1028
|
+
" (",
|
|
1029
|
+
"\u2191\u2193",
|
|
1030
|
+
" navigate, Enter to run, Esc to cancel)"
|
|
1031
|
+
] })
|
|
1032
|
+
] }),
|
|
1033
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
1034
|
+
visible.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " No matching commands" }) : visible.map((cmd, i) => {
|
|
1035
|
+
const isSelected = i === paletteIndex;
|
|
1036
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1037
|
+
/* @__PURE__ */ jsxs2(Text2, { inverse: isSelected, bold: isSelected, color: isSelected ? "yellow" : void 0, children: [
|
|
1038
|
+
isSelected ? " > " : " ",
|
|
1039
|
+
cmd.name.padEnd(20)
|
|
1040
|
+
] }),
|
|
1041
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: !isSelected, color: isSelected ? "white" : void 0, children: [
|
|
1042
|
+
" ",
|
|
1043
|
+
cmd.description
|
|
1044
|
+
] })
|
|
1045
|
+
] }, cmd.name);
|
|
1046
|
+
}),
|
|
1047
|
+
filtered.length > listHeight && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1048
|
+
" ...and ",
|
|
1049
|
+
filtered.length - listHeight,
|
|
1050
|
+
" more"
|
|
1051
|
+
] })
|
|
1052
|
+
] })
|
|
1053
|
+
] });
|
|
1054
|
+
}
|
|
1055
|
+
if (mode === "input") {
|
|
1056
|
+
return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, height, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Input active \u2014 press Esc to exit" }) });
|
|
1057
|
+
}
|
|
1058
|
+
return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, gap: 1, height, children: SHORTCUTS.map(({ key, label }) => /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1059
|
+
/* @__PURE__ */ jsx2(Text2, { inverse: true, bold: true, children: ` ${key} ` }),
|
|
1060
|
+
/* @__PURE__ */ jsx2(Text2, { children: label })
|
|
1061
|
+
] }, key)) });
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/tui/components/layout/help-overlay.tsx
|
|
1065
|
+
import "react";
|
|
1066
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
1067
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1068
|
+
var KEYBINDINGS = [
|
|
1069
|
+
["Navigation", [
|
|
1070
|
+
["\u2191 / k", "Move up"],
|
|
1071
|
+
["\u2193 / j", "Move down"],
|
|
1072
|
+
["\u2190 / h", "Focus left panel"],
|
|
1073
|
+
["\u2192 / l", "Focus right panel"],
|
|
1074
|
+
["Tab", "Cycle panel focus"],
|
|
1075
|
+
["PgUp / PgDn", "Page up / down"],
|
|
1076
|
+
["Home / End", "Scroll to top / bottom"],
|
|
1077
|
+
["Enter", "Select / open detail view"],
|
|
1078
|
+
["d", "Dispatch selected task"]
|
|
1079
|
+
]],
|
|
1080
|
+
["Views", [
|
|
1081
|
+
["1", "Dashboard (default)"],
|
|
1082
|
+
["2", "Plan Generation"],
|
|
1083
|
+
["3", "Projects & Plan"],
|
|
1084
|
+
["4", "Playground"],
|
|
1085
|
+
["5", "Output"]
|
|
1086
|
+
]],
|
|
1087
|
+
["Shortcuts", [
|
|
1088
|
+
["Ctrl+P / :", "Open command palette (searchable)"],
|
|
1089
|
+
["Ctrl+F / /", "Open search"],
|
|
1090
|
+
["Ctrl+R", "Refresh all data"],
|
|
1091
|
+
["?", "Toggle this help"],
|
|
1092
|
+
["q / Ctrl+C", "Quit"],
|
|
1093
|
+
["Esc", "Close overlay / cancel input"]
|
|
1094
|
+
]],
|
|
1095
|
+
["Commands (via Ctrl+P)", [
|
|
1096
|
+
["project list / create / delete", "Project management"],
|
|
1097
|
+
["plan create-node", "Create a plan node"],
|
|
1098
|
+
["dispatch <nodeId>", "Dispatch task for execution"],
|
|
1099
|
+
["cancel <execId>", "Cancel execution"],
|
|
1100
|
+
["steer <message>", "Steer running task"],
|
|
1101
|
+
["watch <execId>", "Watch execution output"],
|
|
1102
|
+
["env list / status", "Machine management"],
|
|
1103
|
+
["activity", "Show activity feed"],
|
|
1104
|
+
["refresh / r", "Force refresh"],
|
|
1105
|
+
["quit / q", "Exit TUI"]
|
|
1106
|
+
]],
|
|
1107
|
+
["Terminal Compatibility", [
|
|
1108
|
+
["tmux", "Ctrl+B prefix is not captured \u2014 safe"],
|
|
1109
|
+
["screen", "Ctrl+A prefix is not captured \u2014 safe"],
|
|
1110
|
+
["vscode", "All bindings work in integrated terminal"]
|
|
1111
|
+
]]
|
|
1112
|
+
];
|
|
1113
|
+
function HelpOverlay() {
|
|
1114
|
+
return /* @__PURE__ */ jsxs3(
|
|
1115
|
+
Box3,
|
|
1116
|
+
{
|
|
1117
|
+
flexDirection: "column",
|
|
1118
|
+
borderStyle: "round",
|
|
1119
|
+
borderColor: "yellow",
|
|
1120
|
+
paddingX: 2,
|
|
1121
|
+
paddingY: 1,
|
|
1122
|
+
children: [
|
|
1123
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: " Keybindings Reference " }),
|
|
1124
|
+
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
1125
|
+
KEYBINDINGS.map(([section, bindings]) => /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
|
|
1126
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, underline: true, children: section }),
|
|
1127
|
+
bindings.map(([key, desc]) => /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
|
|
1128
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: key.padEnd(34) }),
|
|
1129
|
+
/* @__PURE__ */ jsx3(Text3, { children: desc })
|
|
1130
|
+
] }, key))
|
|
1131
|
+
] }, section)),
|
|
1132
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Press Esc or ? to close" })
|
|
1133
|
+
]
|
|
1134
|
+
}
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/tui/components/layout/search-overlay.tsx
|
|
1139
|
+
import { useInput } from "ink";
|
|
1140
|
+
function SearchOverlay() {
|
|
1141
|
+
const isOpen = useSearchStore((s) => s.isOpen);
|
|
1142
|
+
const query = useSearchStore((s) => s.query);
|
|
1143
|
+
const results = useSearchStore((s) => s.results);
|
|
1144
|
+
const items = useSearchStore((s) => s.items);
|
|
1145
|
+
const selectedIndex = useSearchStore((s) => s.selectedIndex);
|
|
1146
|
+
const { setQuery, moveUp, moveDown, close } = useSearchStore();
|
|
1147
|
+
const { setSelectedProject, setSelectedNode, setSelectedMachine, focusPanel, openDetail } = useTuiStore();
|
|
1148
|
+
useInput((input, key) => {
|
|
1149
|
+
if (!isOpen) return;
|
|
1150
|
+
if (key.escape) {
|
|
1151
|
+
close();
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (key.upArrow) {
|
|
1155
|
+
moveUp();
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (key.downArrow || key.tab) {
|
|
1159
|
+
moveDown();
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (key.return) {
|
|
1163
|
+
const displayList = query.length > 0 ? results : items;
|
|
1164
|
+
const item = displayList[selectedIndex];
|
|
1165
|
+
if (item) {
|
|
1166
|
+
switch (item.type) {
|
|
1167
|
+
case "project":
|
|
1168
|
+
setSelectedProject(item.id);
|
|
1169
|
+
focusPanel("projects");
|
|
1170
|
+
break;
|
|
1171
|
+
case "task":
|
|
1172
|
+
setSelectedNode(item.id);
|
|
1173
|
+
focusPanel("plan");
|
|
1174
|
+
openDetail("node", item.id);
|
|
1175
|
+
break;
|
|
1176
|
+
case "machine":
|
|
1177
|
+
setSelectedMachine(item.id);
|
|
1178
|
+
focusPanel("machines");
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
close();
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (key.backspace || key.delete) {
|
|
1186
|
+
setQuery(query.slice(0, -1));
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
1190
|
+
setQuery(query + input);
|
|
1191
|
+
}
|
|
1192
|
+
}, { isActive: isOpen });
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/tui/components/panels/projects-panel.tsx
|
|
1197
|
+
import "react";
|
|
1198
|
+
import { Box as Box5, Text as Text6 } from "ink";
|
|
1199
|
+
|
|
1200
|
+
// src/tui/components/layout/panel.tsx
|
|
1201
|
+
import "react";
|
|
1202
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
1203
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1204
|
+
function Panel({ title, isFocused, children, width, height }) {
|
|
1205
|
+
const borderColor = isFocused ? "cyan" : "gray";
|
|
1206
|
+
return /* @__PURE__ */ jsxs4(
|
|
1207
|
+
Box4,
|
|
1208
|
+
{
|
|
1209
|
+
flexDirection: "column",
|
|
1210
|
+
borderStyle: "single",
|
|
1211
|
+
borderColor,
|
|
1212
|
+
width,
|
|
1213
|
+
height,
|
|
1214
|
+
flexGrow: 1,
|
|
1215
|
+
children: [
|
|
1216
|
+
/* @__PURE__ */ jsx4(Box4, { paddingX: 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: isFocused ? "cyan" : "white", children: title }) }),
|
|
1217
|
+
/* @__PURE__ */ jsx4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, children })
|
|
1218
|
+
]
|
|
1219
|
+
}
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/tui/components/shared/spinner.tsx
|
|
1224
|
+
import { useState, useEffect } from "react";
|
|
1225
|
+
import { Text as Text5 } from "ink";
|
|
1226
|
+
import { jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1227
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1228
|
+
function Spinner({ label }) {
|
|
1229
|
+
const [frame, setFrame] = useState(0);
|
|
1230
|
+
useEffect(() => {
|
|
1231
|
+
const timer = setInterval(() => {
|
|
1232
|
+
setFrame((f) => (f + 1) % FRAMES.length);
|
|
1233
|
+
}, 80);
|
|
1234
|
+
return () => clearInterval(timer);
|
|
1235
|
+
}, []);
|
|
1236
|
+
return /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
1237
|
+
FRAMES[frame],
|
|
1238
|
+
label ? ` ${label}` : ""
|
|
1239
|
+
] });
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// src/tui/components/panels/projects-panel.tsx
|
|
1243
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1244
|
+
function statusSymbol(status) {
|
|
1245
|
+
if (status === "running") return "\u25B6";
|
|
1246
|
+
if (status === "success") return "\u2713";
|
|
1247
|
+
if (status === "failure" || status === "error") return "\u2717";
|
|
1248
|
+
return "\xB7";
|
|
1249
|
+
}
|
|
1250
|
+
function ProjectsPanel({ height }) {
|
|
1251
|
+
const projects = useProjectsStore((s) => s.projects);
|
|
1252
|
+
const loading = useProjectsStore((s) => s.loading);
|
|
1253
|
+
const error = useProjectsStore((s) => s.error);
|
|
1254
|
+
const focusedPanel = useTuiStore((s) => s.focusedPanel);
|
|
1255
|
+
const scrollIndex = useTuiStore((s) => s.scrollIndex.projects);
|
|
1256
|
+
const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
|
|
1257
|
+
const outputs = useExecutionStore((s) => s.outputs);
|
|
1258
|
+
const isFocused = focusedPanel === "projects";
|
|
1259
|
+
const visibleHeight = Math.max(1, height - 3);
|
|
1260
|
+
const planGenEntries = [];
|
|
1261
|
+
const playgroundEntries = [];
|
|
1262
|
+
for (const [id, exec] of outputs) {
|
|
1263
|
+
if (exec.nodeId.startsWith("plan-")) {
|
|
1264
|
+
planGenEntries.push({ id, label: exec.nodeId, status: exec.status });
|
|
1265
|
+
} else if (exec.nodeId.startsWith("playground-")) {
|
|
1266
|
+
playgroundEntries.push({ id, label: exec.nodeId.replace(/^playground-/, "").slice(0, 30), status: exec.status });
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const projectCount = projects.length;
|
|
1270
|
+
const clampedIndex = Math.min(scrollIndex, Math.max(0, projectCount - 1));
|
|
1271
|
+
const sectionLineCount = (planGenEntries.length > 0 ? 1 + planGenEntries.length : 0) + (playgroundEntries.length > 0 ? 1 + playgroundEntries.length : 0);
|
|
1272
|
+
const projectVisibleHeight = Math.max(1, visibleHeight - sectionLineCount);
|
|
1273
|
+
let start = 0;
|
|
1274
|
+
if (projectCount > projectVisibleHeight) {
|
|
1275
|
+
start = Math.max(0, Math.min(clampedIndex - Math.floor(projectVisibleHeight / 2), projectCount - projectVisibleHeight));
|
|
1276
|
+
}
|
|
1277
|
+
const visibleProjects = projects.slice(start, start + projectVisibleHeight);
|
|
1278
|
+
return /* @__PURE__ */ jsx5(Panel, { title: "PROJECTS", isFocused, height, children: loading && projects.length === 0 ? /* @__PURE__ */ jsx5(Spinner, { label: "Loading projects..." }) : error ? /* @__PURE__ */ jsx5(Text6, { color: "red", children: error }) : /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", children: [
|
|
1279
|
+
visibleProjects.length === 0 && planGenEntries.length === 0 && playgroundEntries.length === 0 && /* @__PURE__ */ jsx5(Text6, { dimColor: true, children: " No projects yet" }),
|
|
1280
|
+
visibleProjects.map((p, i) => {
|
|
1281
|
+
const actualIndex = start + i;
|
|
1282
|
+
const isSelected = isFocused && clampedIndex === actualIndex;
|
|
1283
|
+
const isActive = p.id === selectedProjectId;
|
|
1284
|
+
return /* @__PURE__ */ jsxs6(Box5, { children: [
|
|
1285
|
+
/* @__PURE__ */ jsxs6(
|
|
1286
|
+
Text6,
|
|
1287
|
+
{
|
|
1288
|
+
inverse: isSelected,
|
|
1289
|
+
bold: isSelected,
|
|
1290
|
+
color: isActive ? "cyan" : void 0,
|
|
1291
|
+
wrap: "truncate",
|
|
1292
|
+
children: [
|
|
1293
|
+
isSelected ? " > " : " ",
|
|
1294
|
+
truncate(p.name, 30)
|
|
1295
|
+
]
|
|
1296
|
+
}
|
|
1297
|
+
),
|
|
1298
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: !isSelected, children: [
|
|
1299
|
+
" ",
|
|
1300
|
+
formatRelativeTime(p.updatedAt)
|
|
1301
|
+
] })
|
|
1302
|
+
] }, p.id);
|
|
1303
|
+
}),
|
|
1304
|
+
projectCount > projectVisibleHeight && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
1305
|
+
" [",
|
|
1306
|
+
start + 1,
|
|
1307
|
+
"-",
|
|
1308
|
+
Math.min(start + projectVisibleHeight, projectCount),
|
|
1309
|
+
"/",
|
|
1310
|
+
projectCount,
|
|
1311
|
+
"]"
|
|
1312
|
+
] }),
|
|
1313
|
+
planGenEntries.length > 0 && /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
1314
|
+
/* @__PURE__ */ jsx5(Text6, { bold: true, color: "yellow", children: " Plan Generation" }),
|
|
1315
|
+
planGenEntries.map((item) => /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate", children: [
|
|
1316
|
+
" ",
|
|
1317
|
+
statusSymbol(item.status),
|
|
1318
|
+
" ",
|
|
1319
|
+
truncate(item.label, 28),
|
|
1320
|
+
" ",
|
|
1321
|
+
item.status
|
|
1322
|
+
] }, item.id))
|
|
1323
|
+
] }),
|
|
1324
|
+
playgroundEntries.length > 0 && /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
1325
|
+
/* @__PURE__ */ jsx5(Text6, { bold: true, color: "green", children: " Playground" }),
|
|
1326
|
+
playgroundEntries.map((item) => /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "truncate", children: [
|
|
1327
|
+
" ",
|
|
1328
|
+
statusSymbol(item.status),
|
|
1329
|
+
" ",
|
|
1330
|
+
truncate(item.label, 28),
|
|
1331
|
+
" ",
|
|
1332
|
+
item.status
|
|
1333
|
+
] }, item.id))
|
|
1334
|
+
] })
|
|
1335
|
+
] }) });
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// src/tui/components/panels/plan-panel.tsx
|
|
1339
|
+
import "react";
|
|
1340
|
+
import { Text as Text8 } from "ink";
|
|
1341
|
+
|
|
1342
|
+
// src/tui/components/shared/scrollable-list.tsx
|
|
1343
|
+
import "react";
|
|
1344
|
+
import { Box as Box6, Text as Text7, useStdout as useStdout2 } from "ink";
|
|
1345
|
+
import { jsx as jsx6, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1346
|
+
function ScrollableList({ items, selectedIndex, height, isFocused }) {
|
|
1347
|
+
const { stdout } = useStdout2();
|
|
1348
|
+
const termWidth = stdout?.columns ?? 80;
|
|
1349
|
+
if (items.length === 0) {
|
|
1350
|
+
return /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text7, { dimColor: true, children: " No items." }) });
|
|
1351
|
+
}
|
|
1352
|
+
const visibleHeight = Math.max(1, height - 1);
|
|
1353
|
+
let start = 0;
|
|
1354
|
+
if (selectedIndex >= visibleHeight) {
|
|
1355
|
+
start = selectedIndex - visibleHeight + 1;
|
|
1356
|
+
}
|
|
1357
|
+
const visibleItems = items.slice(start, start + visibleHeight);
|
|
1358
|
+
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", children: [
|
|
1359
|
+
visibleItems.map((item, i) => {
|
|
1360
|
+
const actualIndex = start + i;
|
|
1361
|
+
const isSelected = actualIndex === selectedIndex && isFocused;
|
|
1362
|
+
const prefix = isSelected ? " \u25B6 " : " ";
|
|
1363
|
+
const sublabel = item.sublabel ? ` ${item.sublabel}` : "";
|
|
1364
|
+
const rightLabel = item.rightLabel ?? "";
|
|
1365
|
+
const maxLabelWidth = Math.max(10, termWidth - prefix.length - sublabel.length - rightLabel.length - 10);
|
|
1366
|
+
const truncatedLabel = item.label.length > maxLabelWidth ? item.label.slice(0, maxLabelWidth - 1) + "\u2026" : item.label;
|
|
1367
|
+
return /* @__PURE__ */ jsxs7(Box6, { children: [
|
|
1368
|
+
/* @__PURE__ */ jsxs7(
|
|
1369
|
+
Text7,
|
|
1370
|
+
{
|
|
1371
|
+
color: isSelected ? "cyan" : item.color ?? void 0,
|
|
1372
|
+
bold: isSelected,
|
|
1373
|
+
inverse: isSelected,
|
|
1374
|
+
wrap: "truncate",
|
|
1375
|
+
children: [
|
|
1376
|
+
prefix,
|
|
1377
|
+
truncatedLabel,
|
|
1378
|
+
sublabel
|
|
1379
|
+
]
|
|
1380
|
+
}
|
|
1381
|
+
),
|
|
1382
|
+
rightLabel && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
1383
|
+
" ",
|
|
1384
|
+
rightLabel
|
|
1385
|
+
] })
|
|
1386
|
+
] }, item.id);
|
|
1387
|
+
}),
|
|
1388
|
+
items.length > visibleHeight && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
1389
|
+
" [",
|
|
1390
|
+
start + 1,
|
|
1391
|
+
"-",
|
|
1392
|
+
Math.min(start + visibleHeight, items.length),
|
|
1393
|
+
"/",
|
|
1394
|
+
items.length,
|
|
1395
|
+
"]"
|
|
1396
|
+
] })
|
|
1397
|
+
] });
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/tui/components/panels/plan-panel.tsx
|
|
1401
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1402
|
+
function PlanPanel({ height }) {
|
|
1403
|
+
const nodes = usePlanStore((s) => s.nodes);
|
|
1404
|
+
const loading = usePlanStore((s) => s.loading);
|
|
1405
|
+
const error = usePlanStore((s) => s.error);
|
|
1406
|
+
const focusedPanel = useTuiStore((s) => s.focusedPanel);
|
|
1407
|
+
const scrollIndex = useTuiStore((s) => s.scrollIndex.plan);
|
|
1408
|
+
const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
|
|
1409
|
+
const selectedNodeId = useTuiStore((s) => s.selectedNodeId);
|
|
1410
|
+
const projects = useProjectsStore((s) => s.projects);
|
|
1411
|
+
const isFocused = focusedPanel === "plan";
|
|
1412
|
+
const projectName = projects.find((p) => p.id === selectedProjectId)?.name ?? "none";
|
|
1413
|
+
const visibleNodes = nodes.filter((n) => !n.deletedAt);
|
|
1414
|
+
const items = visibleNodes.map((n) => ({
|
|
1415
|
+
id: n.id,
|
|
1416
|
+
label: `${getStatusSymbol(n.status)} ${n.title}`,
|
|
1417
|
+
sublabel: `[${n.status}]`,
|
|
1418
|
+
color: n.id === selectedNodeId ? "cyan" : getStatusColor(n.status)
|
|
1419
|
+
}));
|
|
1420
|
+
return /* @__PURE__ */ jsx7(Panel, { title: `PLAN (${projectName}) d:dispatch`, isFocused, height, children: loading && nodes.length === 0 ? /* @__PURE__ */ jsx7(Spinner, { label: "Loading plan..." }) : error ? /* @__PURE__ */ jsx7(Text8, { color: "red", children: error }) : !selectedProjectId ? /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: " Select a project to view its plan" }) : visibleNodes.length === 0 ? /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: " No plan nodes. Use Ctrl+P \u2192 plan create-node" }) : /* @__PURE__ */ jsx7(
|
|
1421
|
+
ScrollableList,
|
|
1422
|
+
{
|
|
1423
|
+
items,
|
|
1424
|
+
selectedIndex: scrollIndex,
|
|
1425
|
+
height: height - 3,
|
|
1426
|
+
isFocused
|
|
1427
|
+
}
|
|
1428
|
+
) });
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/tui/components/panels/machines-panel.tsx
|
|
1432
|
+
import "react";
|
|
1433
|
+
import { Text as Text9 } from "ink";
|
|
1434
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
1435
|
+
function MachinesPanel({ height }) {
|
|
1436
|
+
const machines = useMachinesStore((s) => s.machines);
|
|
1437
|
+
const loading = useMachinesStore((s) => s.loading);
|
|
1438
|
+
const error = useMachinesStore((s) => s.error);
|
|
1439
|
+
const focusedPanel = useTuiStore((s) => s.focusedPanel);
|
|
1440
|
+
const scrollIndex = useTuiStore((s) => s.scrollIndex.machines);
|
|
1441
|
+
const isFocused = focusedPanel === "machines";
|
|
1442
|
+
const activeMachines = machines.filter((m) => !m.isRevoked);
|
|
1443
|
+
const items = activeMachines.map((m) => ({
|
|
1444
|
+
id: m.id,
|
|
1445
|
+
label: m.name,
|
|
1446
|
+
sublabel: m.platform,
|
|
1447
|
+
rightLabel: m.isConnected ? "\u25CF online" : "\u25CB offline",
|
|
1448
|
+
color: m.isConnected ? "green" : "gray"
|
|
1449
|
+
}));
|
|
1450
|
+
return /* @__PURE__ */ jsx8(Panel, { title: "MACHINES", isFocused, height, children: loading && machines.length === 0 ? /* @__PURE__ */ jsx8(Spinner, { label: "Loading machines..." }) : error ? /* @__PURE__ */ jsx8(Text9, { color: "red", children: error }) : /* @__PURE__ */ jsx8(
|
|
1451
|
+
ScrollableList,
|
|
1452
|
+
{
|
|
1453
|
+
items,
|
|
1454
|
+
selectedIndex: scrollIndex,
|
|
1455
|
+
height: height - 3,
|
|
1456
|
+
isFocused
|
|
1457
|
+
}
|
|
1458
|
+
) });
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/tui/components/panels/output-panel.tsx
|
|
1462
|
+
import "react";
|
|
1463
|
+
import { Box as Box7, Text as Text10 } from "ink";
|
|
1464
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1465
|
+
function lineColor(line) {
|
|
1466
|
+
if (line.startsWith("[Tool Call]")) return "gray";
|
|
1467
|
+
if (line.startsWith("[error]")) return "red";
|
|
1468
|
+
if (line.startsWith("[progress]")) return "cyan";
|
|
1469
|
+
if (line.startsWith("[created]") || line.startsWith("[modified]") || line.startsWith("[deleted]")) return "yellow";
|
|
1470
|
+
return void 0;
|
|
1471
|
+
}
|
|
1472
|
+
function OutputPanel({ height }) {
|
|
1473
|
+
const watchingId = useExecutionStore((s) => s.watchingId);
|
|
1474
|
+
const outputs = useExecutionStore((s) => s.outputs);
|
|
1475
|
+
const focusedPanel = useTuiStore((s) => s.focusedPanel);
|
|
1476
|
+
const scrollIndex = useTuiStore((s) => s.scrollIndex.output);
|
|
1477
|
+
const isFocused = focusedPanel === "output";
|
|
1478
|
+
const execution = watchingId ? outputs.get(watchingId) : null;
|
|
1479
|
+
const visibleHeight = Math.max(1, height - 4);
|
|
1480
|
+
let title = "OUTPUT";
|
|
1481
|
+
if (execution) {
|
|
1482
|
+
const shortId = execution.nodeId.length > 20 ? execution.nodeId.slice(0, 8) + "\u2026" : execution.nodeId;
|
|
1483
|
+
title = `OUTPUT (${shortId}) [${execution.status}]`;
|
|
1484
|
+
}
|
|
1485
|
+
if (!execution) {
|
|
1486
|
+
return /* @__PURE__ */ jsx9(Panel, { title, isFocused, height, children: /* @__PURE__ */ jsx9(Text10, { dimColor: true, children: " No active execution. Dispatch a task with 'd' or :dispatch" }) });
|
|
1487
|
+
}
|
|
1488
|
+
const lines = execution.lines;
|
|
1489
|
+
const isRunning = execution.status === "running";
|
|
1490
|
+
const hasPendingTools = execution.pendingToolCount > 0;
|
|
1491
|
+
let start;
|
|
1492
|
+
if (scrollIndex >= lines.length - visibleHeight) {
|
|
1493
|
+
start = Math.max(0, lines.length - visibleHeight);
|
|
1494
|
+
} else {
|
|
1495
|
+
start = Math.max(0, scrollIndex);
|
|
1496
|
+
}
|
|
1497
|
+
const visibleLines = lines.slice(start, start + visibleHeight);
|
|
1498
|
+
return /* @__PURE__ */ jsx9(Panel, { title, isFocused, height, children: /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
|
|
1499
|
+
visibleLines.map((line, i) => /* @__PURE__ */ jsx9(Text10, { color: lineColor(line), dimColor: line.startsWith("[Tool Call]"), wrap: "truncate", children: truncate(line, 200) }, start + i)),
|
|
1500
|
+
hasPendingTools && /* @__PURE__ */ jsx9(Text10, { dimColor: true, children: "[Tool Call] " + "\xB7".repeat(execution.pendingToolCount) }),
|
|
1501
|
+
isRunning && !hasPendingTools && /* @__PURE__ */ jsx9(Spinner, { label: "Running..." }),
|
|
1502
|
+
lines.length > visibleHeight && /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
|
|
1503
|
+
" [",
|
|
1504
|
+
start + 1,
|
|
1505
|
+
"-",
|
|
1506
|
+
Math.min(start + visibleHeight, lines.length),
|
|
1507
|
+
"/",
|
|
1508
|
+
lines.length,
|
|
1509
|
+
"]"
|
|
1510
|
+
] })
|
|
1511
|
+
] }) });
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/tui/components/panels/chat-panel.tsx
|
|
1515
|
+
import "react";
|
|
1516
|
+
import { Box as Box8, Text as Text11 } from "ink";
|
|
1517
|
+
import { TextInput } from "@inkjs/ui";
|
|
1518
|
+
|
|
1519
|
+
// src/tui/stores/chat-store.ts
|
|
1520
|
+
import { create as create7 } from "zustand";
|
|
1521
|
+
var useChatStore = create7((set, get) => ({
|
|
1522
|
+
messages: [],
|
|
1523
|
+
sessionId: null,
|
|
1524
|
+
projectId: null,
|
|
1525
|
+
nodeId: null,
|
|
1526
|
+
streaming: false,
|
|
1527
|
+
streamBuffer: "",
|
|
1528
|
+
addMessage: (role, content) => {
|
|
1529
|
+
set((s) => ({
|
|
1530
|
+
messages: [...s.messages, { role, content, timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
1531
|
+
}));
|
|
1532
|
+
},
|
|
1533
|
+
appendStream: (text) => {
|
|
1534
|
+
set((s) => ({ streamBuffer: s.streamBuffer + text }));
|
|
1535
|
+
},
|
|
1536
|
+
flushStream: () => {
|
|
1537
|
+
const { streamBuffer } = get();
|
|
1538
|
+
if (streamBuffer.length > 0) {
|
|
1539
|
+
set((s) => ({
|
|
1540
|
+
messages: [...s.messages, { role: "assistant", content: streamBuffer, timestamp: (/* @__PURE__ */ new Date()).toISOString() }],
|
|
1541
|
+
streamBuffer: ""
|
|
1542
|
+
}));
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
setSessionId: (sessionId) => set({ sessionId }),
|
|
1546
|
+
setContext: (projectId, nodeId) => set({ projectId, nodeId: nodeId ?? null }),
|
|
1547
|
+
setStreaming: (streaming) => set({ streaming }),
|
|
1548
|
+
clear: () => set({ messages: [], sessionId: null, streamBuffer: "", streaming: false })
|
|
1549
|
+
}));
|
|
1550
|
+
|
|
1551
|
+
// src/tui/components/panels/chat-panel.tsx
|
|
1552
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1553
|
+
function ChatPanel({ height }) {
|
|
1554
|
+
const messages = useChatStore((s) => s.messages);
|
|
1555
|
+
const streaming = useChatStore((s) => s.streaming);
|
|
1556
|
+
const streamBuffer = useChatStore((s) => s.streamBuffer);
|
|
1557
|
+
const sessionId = useChatStore((s) => s.sessionId);
|
|
1558
|
+
const displayLines = [];
|
|
1559
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1560
|
+
const m = messages[i];
|
|
1561
|
+
const prefix = m.role === "user" ? "> " : " ";
|
|
1562
|
+
displayLines.push({ key: `msg-${i}`, text: `${prefix}${m.content}`, color: m.role === "user" ? "cyan" : void 0 });
|
|
1563
|
+
}
|
|
1564
|
+
if (streaming && streamBuffer) {
|
|
1565
|
+
displayLines.push({ key: "stream", text: ` ${streamBuffer}` });
|
|
1566
|
+
}
|
|
1567
|
+
const contentHeight = height - 4;
|
|
1568
|
+
const visibleLines = displayLines.slice(-Math.max(1, contentHeight));
|
|
1569
|
+
const titleSuffix = sessionId ? ` (${sessionId.slice(0, 8)})` : "";
|
|
1570
|
+
return /* @__PURE__ */ jsx10(Panel, { title: `Chat${titleSuffix}`, isFocused: false, height, children: /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
|
|
1571
|
+
/* @__PURE__ */ jsx10(Box8, { flexDirection: "column", height: Math.max(1, contentHeight - 1), children: visibleLines.map((line) => /* @__PURE__ */ jsx10(Text11, { color: line.color, wrap: "truncate", children: line.text }, line.key)) }),
|
|
1572
|
+
/* @__PURE__ */ jsx10(Box8, { children: streaming ? /* @__PURE__ */ jsxs9(Box8, { children: [
|
|
1573
|
+
/* @__PURE__ */ jsx10(Spinner, {}),
|
|
1574
|
+
/* @__PURE__ */ jsx10(Text11, { dimColor: true, children: " Streaming..." })
|
|
1575
|
+
] }) : /* @__PURE__ */ jsxs9(Box8, { children: [
|
|
1576
|
+
/* @__PURE__ */ jsx10(Text11, { color: "cyan", children: "> " }),
|
|
1577
|
+
/* @__PURE__ */ jsx10(
|
|
1578
|
+
TextInput,
|
|
1579
|
+
{
|
|
1580
|
+
placeholder: "Type a message...",
|
|
1581
|
+
onSubmit: (value) => {
|
|
1582
|
+
if (value.trim()) {
|
|
1583
|
+
useChatStore.getState().addMessage("user", value.trim());
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
)
|
|
1588
|
+
] }) })
|
|
1589
|
+
] }) });
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/tui/components/panels/session-panel.tsx
|
|
1593
|
+
import "react";
|
|
1594
|
+
import { Box as Box9, Text as Text12 } from "ink";
|
|
1595
|
+
import { TextInput as TextInput2 } from "@inkjs/ui";
|
|
1596
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1597
|
+
function lineColor2(line) {
|
|
1598
|
+
if (line.startsWith("[Tool Call]")) return "gray";
|
|
1599
|
+
if (line.startsWith("[error]")) return "red";
|
|
1600
|
+
if (line.startsWith("[progress]")) return "cyan";
|
|
1601
|
+
if (line.startsWith("[plan]")) return "magenta";
|
|
1602
|
+
if (line.startsWith("[created]") || line.startsWith("[modified]") || line.startsWith("[deleted]")) return "yellow";
|
|
1603
|
+
if (line.startsWith("> ")) return "cyan";
|
|
1604
|
+
return void 0;
|
|
1605
|
+
}
|
|
1606
|
+
function SessionPanel({ height, title, sessionType, onSubmit }) {
|
|
1607
|
+
const watchingId = useExecutionStore((s) => s.watchingId);
|
|
1608
|
+
const outputs = useExecutionStore((s) => s.outputs);
|
|
1609
|
+
const streaming = useChatStore((s) => s.streaming);
|
|
1610
|
+
const mode = useTuiStore((s) => s.mode);
|
|
1611
|
+
const execution = watchingId ? outputs.get(watchingId) : null;
|
|
1612
|
+
const isRunning = execution?.status === "running";
|
|
1613
|
+
const inputHeight = 2;
|
|
1614
|
+
const outputHeight = Math.max(1, height - 5 - inputHeight);
|
|
1615
|
+
const isInputActive = mode === "input";
|
|
1616
|
+
const lines = execution?.lines ?? [];
|
|
1617
|
+
const hasPendingTools = (execution?.pendingToolCount ?? 0) > 0;
|
|
1618
|
+
const start = Math.max(0, lines.length - outputHeight);
|
|
1619
|
+
const visibleLines = lines.slice(start, start + outputHeight);
|
|
1620
|
+
let statusLabel = "";
|
|
1621
|
+
if (execution) {
|
|
1622
|
+
statusLabel = ` [${execution.status}]`;
|
|
1623
|
+
}
|
|
1624
|
+
const hint = sessionType === "playground" ? "Describe what you want to build or explore" : "Describe the plan you want to generate";
|
|
1625
|
+
return /* @__PURE__ */ jsx11(Panel, { title: `${title}${statusLabel}`, isFocused: true, height, children: /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
|
|
1626
|
+
/* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", height: outputHeight, children: [
|
|
1627
|
+
visibleLines.length === 0 && !isRunning ? /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
|
|
1628
|
+
" ",
|
|
1629
|
+
hint,
|
|
1630
|
+
". Type below and press Enter."
|
|
1631
|
+
] }) : visibleLines.map((line, i) => /* @__PURE__ */ jsx11(Text12, { color: lineColor2(line), dimColor: line.startsWith("[Tool Call]"), wrap: "truncate", children: truncate(line, 200) }, start + i)),
|
|
1632
|
+
hasPendingTools && /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "[Tool Call] " + "\xB7".repeat(execution.pendingToolCount) }),
|
|
1633
|
+
isRunning && !hasPendingTools && /* @__PURE__ */ jsx11(Spinner, { label: "Running..." })
|
|
1634
|
+
] }),
|
|
1635
|
+
lines.length > outputHeight && /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
|
|
1636
|
+
" [",
|
|
1637
|
+
start + 1,
|
|
1638
|
+
"-",
|
|
1639
|
+
Math.min(start + outputHeight, lines.length),
|
|
1640
|
+
"/",
|
|
1641
|
+
lines.length,
|
|
1642
|
+
"]"
|
|
1643
|
+
] }),
|
|
1644
|
+
/* @__PURE__ */ jsx11(Box9, { borderStyle: "single", borderColor: isInputActive ? "cyan" : "gray", paddingX: 1, children: isRunning || streaming ? /* @__PURE__ */ jsxs10(Box9, { children: [
|
|
1645
|
+
/* @__PURE__ */ jsx11(Spinner, {}),
|
|
1646
|
+
/* @__PURE__ */ jsx11(Text12, { dimColor: true, children: " Agent is working... (press Esc to return to navigation)" })
|
|
1647
|
+
] }) : /* @__PURE__ */ jsxs10(Box9, { children: [
|
|
1648
|
+
/* @__PURE__ */ jsx11(Text12, { color: "cyan", children: "> " }),
|
|
1649
|
+
isInputActive ? /* @__PURE__ */ jsx11(
|
|
1650
|
+
TextInput2,
|
|
1651
|
+
{
|
|
1652
|
+
placeholder: hint,
|
|
1653
|
+
onSubmit: (value) => {
|
|
1654
|
+
if (value.trim()) {
|
|
1655
|
+
const msg = value.trim();
|
|
1656
|
+
if (watchingId) {
|
|
1657
|
+
useExecutionStore.getState().appendLine(watchingId, `> ${msg}`);
|
|
1658
|
+
}
|
|
1659
|
+
useChatStore.getState().addMessage("user", msg);
|
|
1660
|
+
onSubmit?.(msg);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
) : /* @__PURE__ */ jsx11(Text12, { dimColor: true, children: "Press Enter to start typing..." })
|
|
1665
|
+
] }) })
|
|
1666
|
+
] }) });
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/tui/components/panels/detail-overlay.tsx
|
|
1670
|
+
import "react";
|
|
1671
|
+
import { Box as Box10, Text as Text13 } from "ink";
|
|
1672
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1673
|
+
function DetailOverlay() {
|
|
1674
|
+
const showDetail = useTuiStore((s) => s.showDetail);
|
|
1675
|
+
const detailType = useTuiStore((s) => s.detailType);
|
|
1676
|
+
const detailId = useTuiStore((s) => s.detailId);
|
|
1677
|
+
if (!showDetail || !detailType || !detailId) return null;
|
|
1678
|
+
return /* @__PURE__ */ jsxs11(
|
|
1679
|
+
Box10,
|
|
1680
|
+
{
|
|
1681
|
+
flexDirection: "column",
|
|
1682
|
+
borderStyle: "round",
|
|
1683
|
+
borderColor: "cyan",
|
|
1684
|
+
paddingX: 2,
|
|
1685
|
+
paddingY: 1,
|
|
1686
|
+
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" })
|
|
1691
|
+
]
|
|
1692
|
+
}
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
function ProjectDetail({ id }) {
|
|
1696
|
+
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: " " })
|
|
1714
|
+
] });
|
|
1715
|
+
}
|
|
1716
|
+
function NodeDetail({ id }) {
|
|
1717
|
+
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: " " })
|
|
1737
|
+
] });
|
|
1738
|
+
}
|
|
1739
|
+
function MachineDetail({ id }) {
|
|
1740
|
+
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: " " })
|
|
1754
|
+
] });
|
|
1755
|
+
}
|
|
1756
|
+
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 })
|
|
1760
|
+
] });
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// src/tui/components/shared/approval-dialog.tsx
|
|
1764
|
+
import { useState as useState2 } from "react";
|
|
1765
|
+
import { Box as Box11, Text as Text14, useInput as useInput2 } from "ink";
|
|
1766
|
+
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1767
|
+
function ApprovalDialog({ question, options, onSelect, onDismiss }) {
|
|
1768
|
+
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
1769
|
+
useInput2((input, key) => {
|
|
1770
|
+
if (key.escape) {
|
|
1771
|
+
onDismiss();
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (key.return) {
|
|
1775
|
+
onSelect(selectedIndex);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (key.upArrow || input === "k") {
|
|
1779
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
1780
|
+
}
|
|
1781
|
+
if (key.downArrow || input === "j") {
|
|
1782
|
+
setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
|
|
1783
|
+
}
|
|
1784
|
+
const num = parseInt(input, 10);
|
|
1785
|
+
if (num >= 1 && num <= options.length) {
|
|
1786
|
+
onSelect(num - 1);
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
return /* @__PURE__ */ jsxs12(
|
|
1790
|
+
Box11,
|
|
1791
|
+
{
|
|
1792
|
+
flexDirection: "column",
|
|
1793
|
+
borderStyle: "round",
|
|
1794
|
+
borderColor: "yellow",
|
|
1795
|
+
paddingX: 1,
|
|
1796
|
+
paddingY: 0,
|
|
1797
|
+
children: [
|
|
1798
|
+
/* @__PURE__ */ jsx13(Text14, { color: "yellow", bold: true, children: "Approval Required" }),
|
|
1799
|
+
/* @__PURE__ */ jsx13(Text14, { wrap: "wrap", children: question }),
|
|
1800
|
+
/* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsx13(Box11, { children: /* @__PURE__ */ jsxs12(Text14, { color: i === selectedIndex ? "cyan" : "white", children: [
|
|
1801
|
+
i === selectedIndex ? "\u25B8 " : " ",
|
|
1802
|
+
"[",
|
|
1803
|
+
i + 1,
|
|
1804
|
+
"] ",
|
|
1805
|
+
opt
|
|
1806
|
+
] }) }, i)) }),
|
|
1807
|
+
/* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
|
|
1808
|
+
"\u2191\u2193/jk navigate \u2022 Enter select \u2022 1-",
|
|
1809
|
+
options.length,
|
|
1810
|
+
" quick select \u2022 Esc dismiss"
|
|
1811
|
+
] }) })
|
|
1812
|
+
]
|
|
1813
|
+
}
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// src/tui/components/layout/main-layout.tsx
|
|
1818
|
+
import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1819
|
+
function MainLayout({ onSessionMessage }) {
|
|
1820
|
+
const showHelp = useTuiStore((s) => s.showHelp);
|
|
1821
|
+
const showDetail = useTuiStore((s) => s.showDetail);
|
|
1822
|
+
const showChat = useTuiStore((s) => s.showChat);
|
|
1823
|
+
const activeView = useTuiStore((s) => s.activeView);
|
|
1824
|
+
const mode = useTuiStore((s) => s.mode);
|
|
1825
|
+
const searchOpen = useSearchStore((s) => s.isOpen);
|
|
1826
|
+
const pendingApproval = useExecutionStore((s) => s.pendingApproval);
|
|
1827
|
+
const { stdout } = useStdout3();
|
|
1828
|
+
const termHeight = stdout?.rows ?? 24;
|
|
1829
|
+
const termWidth = stdout?.columns ?? 80;
|
|
1830
|
+
const panelOpen = searchOpen || mode === "palette";
|
|
1831
|
+
const bottomPanelHeight = panelOpen ? Math.floor(termHeight / 2) : 1;
|
|
1832
|
+
const contentHeight = termHeight - 2 - bottomPanelHeight;
|
|
1833
|
+
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 })
|
|
1838
|
+
] });
|
|
1839
|
+
}
|
|
1840
|
+
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 })
|
|
1845
|
+
] });
|
|
1846
|
+
}
|
|
1847
|
+
const approvalOverlay = pendingApproval ? /* @__PURE__ */ jsx14(Box12, { position: "absolute", marginTop: 4, marginLeft: Math.floor(termWidth / 4), children: /* @__PURE__ */ jsx14(
|
|
1848
|
+
ApprovalDialog,
|
|
1849
|
+
{
|
|
1850
|
+
question: pendingApproval.question,
|
|
1851
|
+
options: pendingApproval.options,
|
|
1852
|
+
onSelect: (index) => {
|
|
1853
|
+
useExecutionStore.getState().setPendingApproval(null);
|
|
1854
|
+
void index;
|
|
1855
|
+
},
|
|
1856
|
+
onDismiss: () => {
|
|
1857
|
+
useExecutionStore.getState().setPendingApproval(null);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
) }) : null;
|
|
1861
|
+
let content;
|
|
1862
|
+
if (activeView === "plan-gen") {
|
|
1863
|
+
content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(SessionPanel, { height: contentHeight, title: "PLAN GENERATION", sessionType: "plan-generate", onSubmit: onSessionMessage }) }) });
|
|
1864
|
+
} else if (activeView === "projects") {
|
|
1865
|
+
content = /* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: contentHeight, children: [
|
|
1866
|
+
/* @__PURE__ */ jsx14(Box12, { width: "40%", children: /* @__PURE__ */ jsx14(ProjectsPanel, { height: contentHeight }) }),
|
|
1867
|
+
/* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(PlanPanel, { height: contentHeight }) })
|
|
1868
|
+
] });
|
|
1869
|
+
} else if (activeView === "playground") {
|
|
1870
|
+
content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(SessionPanel, { height: contentHeight, title: "PLAYGROUND", sessionType: "playground", onSubmit: onSessionMessage }) }) });
|
|
1871
|
+
} else if (activeView === "output") {
|
|
1872
|
+
content = /* @__PURE__ */ jsx14(Box12, { flexDirection: "row", height: contentHeight, children: /* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(OutputPanel, { height: contentHeight }) }) });
|
|
1873
|
+
} else {
|
|
1874
|
+
const topRowHeight = Math.floor(contentHeight / 2);
|
|
1875
|
+
const bottomRowHeight = contentHeight - topRowHeight;
|
|
1876
|
+
content = /* @__PURE__ */ jsxs13(Fragment2, { children: [
|
|
1877
|
+
/* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: topRowHeight, children: [
|
|
1878
|
+
/* @__PURE__ */ jsx14(Box12, { width: "30%", children: /* @__PURE__ */ jsx14(ProjectsPanel, { height: topRowHeight }) }),
|
|
1879
|
+
/* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: /* @__PURE__ */ jsx14(PlanPanel, { height: topRowHeight }) })
|
|
1880
|
+
] }),
|
|
1881
|
+
/* @__PURE__ */ jsxs13(Box12, { flexDirection: "row", height: bottomRowHeight, children: [
|
|
1882
|
+
/* @__PURE__ */ jsx14(Box12, { width: "30%", children: /* @__PURE__ */ jsx14(MachinesPanel, { height: bottomRowHeight }) }),
|
|
1883
|
+
/* @__PURE__ */ jsx14(Box12, { flexGrow: 1, children: showChat ? /* @__PURE__ */ jsx14(ChatPanel, { height: bottomRowHeight }) : /* @__PURE__ */ jsx14(OutputPanel, { height: bottomRowHeight }) })
|
|
1884
|
+
] })
|
|
1885
|
+
] });
|
|
1886
|
+
}
|
|
1887
|
+
return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", width: termWidth, height: termHeight, children: [
|
|
1888
|
+
/* @__PURE__ */ jsx14(StatusBar, {}),
|
|
1889
|
+
content,
|
|
1890
|
+
/* @__PURE__ */ jsx14(SearchOverlay, {}),
|
|
1891
|
+
approvalOverlay,
|
|
1892
|
+
/* @__PURE__ */ jsx14(CommandLine, { height: bottomPanelHeight })
|
|
1893
|
+
] });
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/tui/hooks/use-vim-mode.ts
|
|
1897
|
+
import { useCallback, useRef } from "react";
|
|
1898
|
+
import { useInput as useInput3, useApp } from "ink";
|
|
1899
|
+
|
|
1900
|
+
// src/tui/lib/vim-state-machine.ts
|
|
1901
|
+
function initialVimState() {
|
|
1902
|
+
return {
|
|
1903
|
+
mode: "normal",
|
|
1904
|
+
commandBuffer: "",
|
|
1905
|
+
searchQuery: "",
|
|
1906
|
+
pendingKeys: ""
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
function vimReducer(state, action) {
|
|
1910
|
+
switch (action.type) {
|
|
1911
|
+
case "set_mode":
|
|
1912
|
+
return [{ ...state, mode: action.mode, pendingKeys: "" }, { type: "none" }];
|
|
1913
|
+
case "clear_command":
|
|
1914
|
+
return [{ ...state, commandBuffer: "", mode: "normal", pendingKeys: "" }, { type: "none" }];
|
|
1915
|
+
case "clear_search":
|
|
1916
|
+
return [{ ...state, searchQuery: "", mode: "normal", pendingKeys: "" }, { type: "none" }];
|
|
1917
|
+
case "submit_command":
|
|
1918
|
+
return [
|
|
1919
|
+
{ ...state, mode: "normal", pendingKeys: "" },
|
|
1920
|
+
{ type: "command", value: state.commandBuffer }
|
|
1921
|
+
];
|
|
1922
|
+
case "submit_search":
|
|
1923
|
+
return [
|
|
1924
|
+
{ ...state, mode: "normal", pendingKeys: "" },
|
|
1925
|
+
{ type: "search", value: state.searchQuery }
|
|
1926
|
+
];
|
|
1927
|
+
case "key":
|
|
1928
|
+
return handleKey(state, action.key, action.ctrl, action.meta);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
function handleKey(state, key, ctrl, meta) {
|
|
1932
|
+
if (key === "escape") {
|
|
1933
|
+
return [{ ...state, mode: "normal", pendingKeys: "", commandBuffer: "", searchQuery: "" }, { type: "none" }];
|
|
1934
|
+
}
|
|
1935
|
+
switch (state.mode) {
|
|
1936
|
+
case "normal":
|
|
1937
|
+
return handleNormalMode(state, key, ctrl, meta);
|
|
1938
|
+
case "palette":
|
|
1939
|
+
return handlePaletteMode(state, key);
|
|
1940
|
+
case "search":
|
|
1941
|
+
return handleSearchMode(state, key);
|
|
1942
|
+
case "input":
|
|
1943
|
+
return handleInputMode(state, key);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
function handleNormalMode(state, key, ctrl, meta) {
|
|
1947
|
+
if (ctrl) {
|
|
1948
|
+
switch (key) {
|
|
1949
|
+
case "p":
|
|
1950
|
+
return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
|
|
1951
|
+
case "f":
|
|
1952
|
+
return [state, { type: "search" }];
|
|
1953
|
+
case "c":
|
|
1954
|
+
return [state, { type: "quit" }];
|
|
1955
|
+
case "r":
|
|
1956
|
+
return [state, { type: "refresh" }];
|
|
1957
|
+
default:
|
|
1958
|
+
return [state, { type: "none" }];
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (meta) {
|
|
1962
|
+
switch (key) {
|
|
1963
|
+
case "x":
|
|
1964
|
+
return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
|
|
1965
|
+
default:
|
|
1966
|
+
return [state, { type: "none" }];
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
switch (key) {
|
|
1970
|
+
// Arrow navigation (primary — no j/k needed)
|
|
1971
|
+
case "up":
|
|
1972
|
+
return [state, { type: "scroll", direction: "up" }];
|
|
1973
|
+
case "down":
|
|
1974
|
+
return [state, { type: "scroll", direction: "down" }];
|
|
1975
|
+
case "left":
|
|
1976
|
+
return [state, { type: "focus", direction: "left" }];
|
|
1977
|
+
case "right":
|
|
1978
|
+
return [state, { type: "focus", direction: "right" }];
|
|
1979
|
+
// Also keep j/k/h/l as secondary navigation for power users
|
|
1980
|
+
case "j":
|
|
1981
|
+
return [state, { type: "scroll", direction: "down" }];
|
|
1982
|
+
case "k":
|
|
1983
|
+
return [state, { type: "scroll", direction: "up" }];
|
|
1984
|
+
case "h":
|
|
1985
|
+
return [state, { type: "focus", direction: "left" }];
|
|
1986
|
+
case "l":
|
|
1987
|
+
return [state, { type: "focus", direction: "right" }];
|
|
1988
|
+
// Selection
|
|
1989
|
+
case "return":
|
|
1990
|
+
return [state, { type: "select" }];
|
|
1991
|
+
case " ":
|
|
1992
|
+
return [state, { type: "select" }];
|
|
1993
|
+
// Page navigation
|
|
1994
|
+
case "pageup":
|
|
1995
|
+
return [state, { type: "scroll", direction: "page_up" }];
|
|
1996
|
+
case "pagedown":
|
|
1997
|
+
return [state, { type: "scroll", direction: "page_down" }];
|
|
1998
|
+
case "home":
|
|
1999
|
+
return [state, { type: "scroll", direction: "top" }];
|
|
2000
|
+
case "end":
|
|
2001
|
+
return [state, { type: "scroll", direction: "bottom" }];
|
|
2002
|
+
// Tab cycles panels
|
|
2003
|
+
case "tab":
|
|
2004
|
+
return [state, { type: "focus", direction: "right" }];
|
|
2005
|
+
// View switch by number
|
|
2006
|
+
case "1":
|
|
2007
|
+
return [state, { type: "view", value: "dashboard" }];
|
|
2008
|
+
case "2":
|
|
2009
|
+
return [state, { type: "view", value: "plan-gen" }];
|
|
2010
|
+
case "3":
|
|
2011
|
+
return [state, { type: "view", value: "projects" }];
|
|
2012
|
+
case "4":
|
|
2013
|
+
return [state, { type: "view", value: "playground" }];
|
|
2014
|
+
case "5":
|
|
2015
|
+
return [state, { type: "view", value: "output" }];
|
|
2016
|
+
// Function-key style shortcuts (single letter, no prefix needed)
|
|
2017
|
+
case "d":
|
|
2018
|
+
return [state, { type: "dispatch" }];
|
|
2019
|
+
case "q":
|
|
2020
|
+
return [state, { type: "quit" }];
|
|
2021
|
+
case "?":
|
|
2022
|
+
return [state, { type: "help" }];
|
|
2023
|
+
case "/":
|
|
2024
|
+
return [state, { type: "search" }];
|
|
2025
|
+
// Legacy `:` still works — enters palette mode
|
|
2026
|
+
case ":":
|
|
2027
|
+
return [{ ...state, mode: "palette", commandBuffer: "" }, { type: "palette" }];
|
|
2028
|
+
default:
|
|
2029
|
+
return [state, { type: "none" }];
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function handlePaletteMode(state, key) {
|
|
2033
|
+
if (key === "return") {
|
|
2034
|
+
return [
|
|
2035
|
+
{ ...state, mode: "normal" },
|
|
2036
|
+
{ type: "command", value: "__palette_select__" }
|
|
2037
|
+
];
|
|
2038
|
+
}
|
|
2039
|
+
if (key === "backspace" || key === "delete") {
|
|
2040
|
+
const newBuffer = state.commandBuffer.slice(0, -1);
|
|
2041
|
+
if (newBuffer.length === 0) {
|
|
2042
|
+
return [{ ...state, mode: "normal", commandBuffer: "" }, { type: "none" }];
|
|
2043
|
+
}
|
|
2044
|
+
return [{ ...state, commandBuffer: newBuffer }, { type: "none" }];
|
|
2045
|
+
}
|
|
2046
|
+
if (key === "up") {
|
|
2047
|
+
return [state, { type: "scroll", direction: "up" }];
|
|
2048
|
+
}
|
|
2049
|
+
if (key === "down") {
|
|
2050
|
+
return [state, { type: "scroll", direction: "down" }];
|
|
2051
|
+
}
|
|
2052
|
+
if (key === "tab") {
|
|
2053
|
+
return [state, { type: "scroll", direction: "down" }];
|
|
2054
|
+
}
|
|
2055
|
+
if (key.length === 1) {
|
|
2056
|
+
return [{ ...state, commandBuffer: state.commandBuffer + key }, { type: "none" }];
|
|
2057
|
+
}
|
|
2058
|
+
return [state, { type: "none" }];
|
|
2059
|
+
}
|
|
2060
|
+
function handleSearchMode(state, key) {
|
|
2061
|
+
if (key === "return") {
|
|
2062
|
+
return [
|
|
2063
|
+
{ ...state, mode: "normal" },
|
|
2064
|
+
{ type: "search", value: state.searchQuery }
|
|
2065
|
+
];
|
|
2066
|
+
}
|
|
2067
|
+
if (key === "backspace" || key === "delete") {
|
|
2068
|
+
const newQuery = state.searchQuery.slice(0, -1);
|
|
2069
|
+
if (newQuery.length === 0) {
|
|
2070
|
+
return [{ ...state, mode: "normal", searchQuery: "" }, { type: "none" }];
|
|
2071
|
+
}
|
|
2072
|
+
return [{ ...state, searchQuery: newQuery }, { type: "search", value: newQuery }];
|
|
2073
|
+
}
|
|
2074
|
+
if (key.length === 1) {
|
|
2075
|
+
const newQuery = state.searchQuery + key;
|
|
2076
|
+
return [{ ...state, searchQuery: newQuery }, { type: "search", value: newQuery }];
|
|
2077
|
+
}
|
|
2078
|
+
return [state, { type: "none" }];
|
|
2079
|
+
}
|
|
2080
|
+
function handleInputMode(state, _) {
|
|
2081
|
+
return [state, { type: "none" }];
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/tui/hooks/use-vim-mode.ts
|
|
2085
|
+
function useVimMode(callbacks = {}) {
|
|
2086
|
+
const { exit } = useApp();
|
|
2087
|
+
const vimState = useRef(initialVimState());
|
|
2088
|
+
const store = useTuiStore();
|
|
2089
|
+
const handleEffect = useCallback(
|
|
2090
|
+
(effect) => {
|
|
2091
|
+
switch (effect.type) {
|
|
2092
|
+
case "scroll":
|
|
2093
|
+
if (vimState.current.mode === "palette") {
|
|
2094
|
+
const filtered = getFilteredPaletteCommands(vimState.current.commandBuffer);
|
|
2095
|
+
const idx = store.paletteIndex;
|
|
2096
|
+
if (effect.direction === "up") {
|
|
2097
|
+
store.setPaletteIndex(Math.max(0, idx - 1));
|
|
2098
|
+
} else if (effect.direction === "down") {
|
|
2099
|
+
store.setPaletteIndex(Math.min(filtered.length - 1, idx + 1));
|
|
2100
|
+
}
|
|
2101
|
+
break;
|
|
2102
|
+
}
|
|
2103
|
+
switch (effect.direction) {
|
|
2104
|
+
case "up":
|
|
2105
|
+
store.scrollUp();
|
|
2106
|
+
break;
|
|
2107
|
+
case "down":
|
|
2108
|
+
store.scrollDown();
|
|
2109
|
+
break;
|
|
2110
|
+
case "top":
|
|
2111
|
+
store.scrollToTop();
|
|
2112
|
+
break;
|
|
2113
|
+
case "bottom":
|
|
2114
|
+
store.scrollToBottom();
|
|
2115
|
+
break;
|
|
2116
|
+
case "page_up":
|
|
2117
|
+
store.pageUp();
|
|
2118
|
+
break;
|
|
2119
|
+
case "page_down":
|
|
2120
|
+
store.pageDown();
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2123
|
+
break;
|
|
2124
|
+
case "focus":
|
|
2125
|
+
if (effect.panel != null) {
|
|
2126
|
+
store.focusByIndex(effect.panel);
|
|
2127
|
+
} else if (effect.direction === "left") {
|
|
2128
|
+
store.focusPrev();
|
|
2129
|
+
} else if (effect.direction === "right") {
|
|
2130
|
+
store.focusNext();
|
|
2131
|
+
}
|
|
2132
|
+
break;
|
|
2133
|
+
case "select": {
|
|
2134
|
+
const view = store.activeView;
|
|
2135
|
+
if (view === "playground" || view === "plan-gen" || view === "output") {
|
|
2136
|
+
vimState.current = { ...vimState.current, mode: "input" };
|
|
2137
|
+
store.setMode("input");
|
|
2138
|
+
} else {
|
|
2139
|
+
callbacks.onSelect?.();
|
|
2140
|
+
}
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
case "palette":
|
|
2144
|
+
break;
|
|
2145
|
+
case "command":
|
|
2146
|
+
if (effect.value === "__palette_select__") {
|
|
2147
|
+
const filtered = getFilteredPaletteCommands(vimState.current.commandBuffer);
|
|
2148
|
+
const selected = filtered[store.paletteIndex];
|
|
2149
|
+
if (selected) {
|
|
2150
|
+
callbacks.onCommand?.(selected.name);
|
|
2151
|
+
}
|
|
2152
|
+
} else if (effect.value?.startsWith("__autocomplete__")) {
|
|
2153
|
+
} else if (effect.value) {
|
|
2154
|
+
callbacks.onCommand?.(effect.value);
|
|
2155
|
+
}
|
|
2156
|
+
break;
|
|
2157
|
+
case "search":
|
|
2158
|
+
useSearchStore.getState().open();
|
|
2159
|
+
break;
|
|
2160
|
+
case "dispatch":
|
|
2161
|
+
callbacks.onDispatch?.();
|
|
2162
|
+
break;
|
|
2163
|
+
case "cancel":
|
|
2164
|
+
callbacks.onCancel?.();
|
|
2165
|
+
break;
|
|
2166
|
+
case "refresh":
|
|
2167
|
+
callbacks.onRefresh?.();
|
|
2168
|
+
break;
|
|
2169
|
+
case "quit":
|
|
2170
|
+
exit();
|
|
2171
|
+
setTimeout(() => process.exit(0), 100);
|
|
2172
|
+
break;
|
|
2173
|
+
case "help":
|
|
2174
|
+
store.toggleHelp();
|
|
2175
|
+
break;
|
|
2176
|
+
case "chat":
|
|
2177
|
+
store.toggleChat();
|
|
2178
|
+
break;
|
|
2179
|
+
case "view":
|
|
2180
|
+
if (effect.value === "dashboard" || effect.value === "plan-gen" || effect.value === "projects" || effect.value === "playground" || effect.value === "output") {
|
|
2181
|
+
store.setActiveView(effect.value);
|
|
2182
|
+
}
|
|
2183
|
+
break;
|
|
2184
|
+
case "none":
|
|
2185
|
+
break;
|
|
2186
|
+
}
|
|
2187
|
+
},
|
|
2188
|
+
[store, callbacks, exit]
|
|
2189
|
+
);
|
|
2190
|
+
useInput3((input, key) => {
|
|
2191
|
+
const searchOpen = useSearchStore.getState().isOpen;
|
|
2192
|
+
if (store.showHelp || store.showSearch || store.showDetail || searchOpen) {
|
|
2193
|
+
if (key.escape) {
|
|
2194
|
+
if (searchOpen) useSearchStore.getState().close();
|
|
2195
|
+
store.closeOverlays();
|
|
2196
|
+
vimState.current = initialVimState();
|
|
2197
|
+
store.setMode("normal");
|
|
2198
|
+
store.setCommandBuffer("");
|
|
2199
|
+
store.setSearchQuery("");
|
|
2200
|
+
store.setPendingKeys("");
|
|
2201
|
+
}
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
let keyStr = input;
|
|
2205
|
+
if (key.escape) keyStr = "escape";
|
|
2206
|
+
else if (key.return) keyStr = "return";
|
|
2207
|
+
else if (key.backspace || key.delete) keyStr = "backspace";
|
|
2208
|
+
else if (key.tab) keyStr = "tab";
|
|
2209
|
+
else if (key.upArrow) keyStr = "up";
|
|
2210
|
+
else if (key.downArrow) keyStr = "down";
|
|
2211
|
+
else if (key.leftArrow) keyStr = "left";
|
|
2212
|
+
else if (key.rightArrow) keyStr = "right";
|
|
2213
|
+
else if (key.pageUp) keyStr = "pageup";
|
|
2214
|
+
else if (key.pageDown) keyStr = "pagedown";
|
|
2215
|
+
else if (key.home) keyStr = "home";
|
|
2216
|
+
else if (key.end) keyStr = "end";
|
|
2217
|
+
const [nextState, effect] = vimReducer(vimState.current, {
|
|
2218
|
+
type: "key",
|
|
2219
|
+
key: keyStr,
|
|
2220
|
+
ctrl: key.ctrl,
|
|
2221
|
+
shift: key.shift,
|
|
2222
|
+
meta: key.meta
|
|
2223
|
+
});
|
|
2224
|
+
vimState.current = nextState;
|
|
2225
|
+
store.setMode(nextState.mode);
|
|
2226
|
+
store.setCommandBuffer(nextState.commandBuffer);
|
|
2227
|
+
store.setSearchQuery(nextState.searchQuery);
|
|
2228
|
+
store.setPendingKeys(nextState.pendingKeys);
|
|
2229
|
+
handleEffect(effect);
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// src/tui/hooks/use-polling.ts
|
|
2234
|
+
import { useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
2235
|
+
function usePolling(client, intervalMs = 3e4) {
|
|
2236
|
+
const selectedProjectId = useTuiStore((s) => s.selectedProjectId);
|
|
2237
|
+
const loadProjects = useCallback2(async () => {
|
|
2238
|
+
useProjectsStore.getState().setLoading(true);
|
|
2239
|
+
try {
|
|
2240
|
+
const projects = await client.listProjects();
|
|
2241
|
+
useProjectsStore.getState().setProjects(projects);
|
|
2242
|
+
} catch (err) {
|
|
2243
|
+
useProjectsStore.getState().setError(err instanceof Error ? err.message : String(err));
|
|
2244
|
+
}
|
|
2245
|
+
}, [client]);
|
|
2246
|
+
const loadPlan = useCallback2(async (projectId) => {
|
|
2247
|
+
usePlanStore.getState().setLoading(true);
|
|
2248
|
+
try {
|
|
2249
|
+
const { nodes, edges } = await client.getPlan(projectId);
|
|
2250
|
+
usePlanStore.getState().setPlan(projectId, nodes, edges);
|
|
2251
|
+
} catch (err) {
|
|
2252
|
+
usePlanStore.getState().setError(err instanceof Error ? err.message : String(err));
|
|
2253
|
+
}
|
|
2254
|
+
}, [client]);
|
|
2255
|
+
const loadMachines = useCallback2(async () => {
|
|
2256
|
+
useMachinesStore.getState().setLoading(true);
|
|
2257
|
+
try {
|
|
2258
|
+
const machines = await client.listMachines();
|
|
2259
|
+
useMachinesStore.getState().setMachines(machines);
|
|
2260
|
+
useTuiStore.getState().setMachineCount(machines.filter((m) => m.isConnected).length);
|
|
2261
|
+
} catch (err) {
|
|
2262
|
+
useMachinesStore.getState().setError(err instanceof Error ? err.message : String(err));
|
|
2263
|
+
}
|
|
2264
|
+
}, [client]);
|
|
2265
|
+
const loadUsage = useCallback2(async () => {
|
|
2266
|
+
try {
|
|
2267
|
+
const history = await client.getUsageHistory(1);
|
|
2268
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2269
|
+
const todayEntry = history.find((d) => d.date === today);
|
|
2270
|
+
useTuiStore.getState().setTodayCost(todayEntry?.totalCostUsd ?? 0);
|
|
2271
|
+
} catch {
|
|
2272
|
+
}
|
|
2273
|
+
}, [client]);
|
|
2274
|
+
const refreshAll2 = useCallback2(async () => {
|
|
2275
|
+
await Promise.allSettled([
|
|
2276
|
+
loadProjects(),
|
|
2277
|
+
loadMachines(),
|
|
2278
|
+
loadUsage(),
|
|
2279
|
+
...selectedProjectId ? [loadPlan(selectedProjectId)] : []
|
|
2280
|
+
]);
|
|
2281
|
+
}, [loadProjects, loadMachines, loadUsage, loadPlan, selectedProjectId]);
|
|
2282
|
+
useEffect2(() => {
|
|
2283
|
+
refreshAll2();
|
|
2284
|
+
}, [refreshAll2]);
|
|
2285
|
+
useEffect2(() => {
|
|
2286
|
+
if (selectedProjectId) {
|
|
2287
|
+
loadPlan(selectedProjectId);
|
|
2288
|
+
} else {
|
|
2289
|
+
usePlanStore.getState().clear();
|
|
2290
|
+
}
|
|
2291
|
+
}, [selectedProjectId, loadPlan]);
|
|
2292
|
+
useEffect2(() => {
|
|
2293
|
+
const timer = setInterval(refreshAll2, intervalMs);
|
|
2294
|
+
return () => clearInterval(timer);
|
|
2295
|
+
}, [refreshAll2, intervalMs]);
|
|
2296
|
+
return { refreshAll: refreshAll2, loadProjects, loadPlan, loadMachines };
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// src/tui/hooks/use-sse-stream.ts
|
|
2300
|
+
import { useEffect as useEffect3, useRef as useRef2 } from "react";
|
|
2301
|
+
|
|
2302
|
+
// src/tui/sse-client.ts
|
|
2303
|
+
var SSEClient = class {
|
|
2304
|
+
client;
|
|
2305
|
+
abortController = null;
|
|
2306
|
+
handler;
|
|
2307
|
+
reconnectDelay = 1e3;
|
|
2308
|
+
maxReconnectDelay = 3e4;
|
|
2309
|
+
_connected = false;
|
|
2310
|
+
_stopped = false;
|
|
2311
|
+
constructor(client, handler) {
|
|
2312
|
+
this.client = client;
|
|
2313
|
+
this.handler = handler;
|
|
2314
|
+
}
|
|
2315
|
+
get connected() {
|
|
2316
|
+
return this._connected;
|
|
2317
|
+
}
|
|
2318
|
+
async start() {
|
|
2319
|
+
this._stopped = false;
|
|
2320
|
+
this.reconnectDelay = 1e3;
|
|
2321
|
+
await this.connect();
|
|
2322
|
+
}
|
|
2323
|
+
stop() {
|
|
2324
|
+
this._stopped = true;
|
|
2325
|
+
this._connected = false;
|
|
2326
|
+
if (this.abortController) {
|
|
2327
|
+
this.abortController.abort();
|
|
2328
|
+
this.abortController = null;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
async connect() {
|
|
2332
|
+
if (this._stopped) return;
|
|
2333
|
+
try {
|
|
2334
|
+
const response = await this.client.streamEvents();
|
|
2335
|
+
this._connected = true;
|
|
2336
|
+
this.reconnectDelay = 1e3;
|
|
2337
|
+
this.handler({ type: "__connected", data: {} });
|
|
2338
|
+
if (!response.body) return;
|
|
2339
|
+
const reader = response.body.getReader();
|
|
2340
|
+
const decoder = new TextDecoder();
|
|
2341
|
+
let buffer = "";
|
|
2342
|
+
let eventType = "";
|
|
2343
|
+
while (!this._stopped) {
|
|
2344
|
+
const { done, value } = await reader.read();
|
|
2345
|
+
if (done) break;
|
|
2346
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2347
|
+
const lines = buffer.split("\n");
|
|
2348
|
+
buffer = lines.pop() ?? "";
|
|
2349
|
+
for (const line of lines) {
|
|
2350
|
+
if (line.startsWith("event: ")) {
|
|
2351
|
+
eventType = line.slice(7).trim();
|
|
2352
|
+
} else if (line.startsWith("data: ")) {
|
|
2353
|
+
const dataStr = line.slice(6);
|
|
2354
|
+
try {
|
|
2355
|
+
const data = JSON.parse(dataStr);
|
|
2356
|
+
this.handler({ type: eventType || "message", data });
|
|
2357
|
+
} catch {
|
|
2358
|
+
this.handler({ type: eventType || "message", data: { raw: dataStr } });
|
|
2359
|
+
}
|
|
2360
|
+
eventType = "";
|
|
2361
|
+
} else if (line === "") {
|
|
2362
|
+
eventType = "";
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
} catch (err) {
|
|
2367
|
+
this._connected = false;
|
|
2368
|
+
if (this._stopped) return;
|
|
2369
|
+
this.handler({
|
|
2370
|
+
type: "__disconnected",
|
|
2371
|
+
data: { error: err instanceof Error ? err.message : String(err) }
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
if (!this._stopped) {
|
|
2375
|
+
this._connected = false;
|
|
2376
|
+
this.handler({ type: "__reconnecting", data: { delay: this.reconnectDelay } });
|
|
2377
|
+
await new Promise((r) => setTimeout(r, this.reconnectDelay));
|
|
2378
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
2379
|
+
await this.connect();
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
|
|
2384
|
+
// src/tui/hooks/use-sse-stream.ts
|
|
2385
|
+
function useSSEStream(client) {
|
|
2386
|
+
const sseRef = useRef2(null);
|
|
2387
|
+
const setConnected = useTuiStore((s) => s.setConnected);
|
|
2388
|
+
const setMachineCount = useTuiStore((s) => s.setMachineCount);
|
|
2389
|
+
const setLastError = useTuiStore((s) => s.setLastError);
|
|
2390
|
+
useEffect3(() => {
|
|
2391
|
+
const handler = (event) => {
|
|
2392
|
+
switch (event.type) {
|
|
2393
|
+
case "__connected":
|
|
2394
|
+
setConnected(true);
|
|
2395
|
+
setLastError(null);
|
|
2396
|
+
break;
|
|
2397
|
+
case "__disconnected":
|
|
2398
|
+
setConnected(false);
|
|
2399
|
+
setLastError(event.data.error);
|
|
2400
|
+
break;
|
|
2401
|
+
case "__reconnecting":
|
|
2402
|
+
setConnected(false);
|
|
2403
|
+
break;
|
|
2404
|
+
case "machines:snapshot": {
|
|
2405
|
+
const machines = event.data ?? [];
|
|
2406
|
+
useMachinesStore.getState().setMachines(machines);
|
|
2407
|
+
setMachineCount(machines.filter((m) => m.isConnected).length);
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
case "machine:connected": {
|
|
2411
|
+
const machine = event.data;
|
|
2412
|
+
useMachinesStore.getState().addMachine(machine);
|
|
2413
|
+
setMachineCount(useMachinesStore.getState().machines.filter((m) => m.isConnected).length);
|
|
2414
|
+
break;
|
|
2415
|
+
}
|
|
2416
|
+
case "machine:disconnected": {
|
|
2417
|
+
const id = event.data.machineId;
|
|
2418
|
+
useMachinesStore.getState().updateMachine(id, { isConnected: false });
|
|
2419
|
+
setMachineCount(useMachinesStore.getState().machines.filter((m) => m.isConnected).length);
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
case "task:stdout":
|
|
2423
|
+
case "task:text": {
|
|
2424
|
+
const taskId = event.data.taskId;
|
|
2425
|
+
const text = event.data.data ?? event.data.output ?? "";
|
|
2426
|
+
if (typeof text === "string" && text.length > 0) {
|
|
2427
|
+
useExecutionStore.getState().appendText(taskId, text);
|
|
2428
|
+
}
|
|
2429
|
+
break;
|
|
2430
|
+
}
|
|
2431
|
+
case "task:progress": {
|
|
2432
|
+
const taskId = event.data.taskId;
|
|
2433
|
+
const message = event.data.message;
|
|
2434
|
+
if (!message) break;
|
|
2435
|
+
if (/^Using tool:/i.test(message)) break;
|
|
2436
|
+
useExecutionStore.getState().appendLine(taskId, `[progress] ${message}`);
|
|
2437
|
+
break;
|
|
2438
|
+
}
|
|
2439
|
+
case "task:result": {
|
|
2440
|
+
const taskId = event.data.taskId;
|
|
2441
|
+
const status = event.data.status;
|
|
2442
|
+
useExecutionStore.getState().setStatus(taskId, status);
|
|
2443
|
+
const nodeId = event.data.nodeId;
|
|
2444
|
+
if (nodeId && status) {
|
|
2445
|
+
const mappedStatus = status === "success" ? "completed" : status === "failure" ? "planned" : status;
|
|
2446
|
+
usePlanStore.getState().updateNodeStatus(nodeId, mappedStatus);
|
|
2447
|
+
}
|
|
2448
|
+
break;
|
|
2449
|
+
}
|
|
2450
|
+
case "task:tool_trace": {
|
|
2451
|
+
const taskId = event.data.taskId;
|
|
2452
|
+
const toolName = event.data.toolName;
|
|
2453
|
+
useExecutionStore.getState().appendToolCall(taskId, toolName);
|
|
2454
|
+
break;
|
|
2455
|
+
}
|
|
2456
|
+
case "task:file_change": {
|
|
2457
|
+
const taskId = event.data.taskId;
|
|
2458
|
+
const path = event.data.path;
|
|
2459
|
+
const action = event.data.action;
|
|
2460
|
+
const added = event.data.linesAdded;
|
|
2461
|
+
const removed = event.data.linesRemoved;
|
|
2462
|
+
useExecutionStore.getState().appendFileChange(taskId, path, action, added, removed);
|
|
2463
|
+
break;
|
|
2464
|
+
}
|
|
2465
|
+
case "task:session_init": {
|
|
2466
|
+
const taskId = event.data.taskId;
|
|
2467
|
+
const nodeId = event.data.nodeId ?? taskId;
|
|
2468
|
+
useExecutionStore.getState().initExecution(taskId, nodeId);
|
|
2469
|
+
useExecutionStore.getState().setWatching(taskId);
|
|
2470
|
+
break;
|
|
2471
|
+
}
|
|
2472
|
+
case "task:plan_result": {
|
|
2473
|
+
const taskId = event.data.taskId;
|
|
2474
|
+
useExecutionStore.getState().appendLine(taskId, "[plan] Plan generated \u2014 refreshing...");
|
|
2475
|
+
const projectId = event.data.projectId ?? useTuiStore.getState().selectedProjectId;
|
|
2476
|
+
if (projectId) {
|
|
2477
|
+
setTimeout(async () => {
|
|
2478
|
+
try {
|
|
2479
|
+
const { nodes, edges } = await client.getPlan(projectId);
|
|
2480
|
+
usePlanStore.getState().setPlan(projectId, nodes, edges);
|
|
2481
|
+
} catch {
|
|
2482
|
+
}
|
|
2483
|
+
}, 500);
|
|
2484
|
+
}
|
|
2485
|
+
break;
|
|
2486
|
+
}
|
|
2487
|
+
case "task:approval_request": {
|
|
2488
|
+
useExecutionStore.getState().setPendingApproval({
|
|
2489
|
+
requestId: event.data.requestId,
|
|
2490
|
+
question: event.data.question,
|
|
2491
|
+
options: event.data.options,
|
|
2492
|
+
machineId: event.data.machineId,
|
|
2493
|
+
taskId: event.data.taskId
|
|
2494
|
+
});
|
|
2495
|
+
break;
|
|
2496
|
+
}
|
|
2497
|
+
case "heartbeat":
|
|
2498
|
+
break;
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
const sse = new SSEClient(client, handler);
|
|
2502
|
+
sseRef.current = sse;
|
|
2503
|
+
sse.start().catch(() => {
|
|
2504
|
+
});
|
|
2505
|
+
return () => {
|
|
2506
|
+
sse.stop();
|
|
2507
|
+
};
|
|
2508
|
+
}, [client, setConnected, setMachineCount, setLastError]);
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// src/tui/hooks/use-fuzzy-search.ts
|
|
2512
|
+
import { useEffect as useEffect4, useCallback as useCallback3 } from "react";
|
|
2513
|
+
import Fuse from "fuse.js";
|
|
2514
|
+
var fuseInstance = null;
|
|
2515
|
+
function useFuzzySearch() {
|
|
2516
|
+
const projects = useProjectsStore((s) => s.projects);
|
|
2517
|
+
const nodes = usePlanStore((s) => s.nodes);
|
|
2518
|
+
const machines = useMachinesStore((s) => s.machines);
|
|
2519
|
+
const { setItems, setResults, query } = useSearchStore();
|
|
2520
|
+
useEffect4(() => {
|
|
2521
|
+
const items = [
|
|
2522
|
+
...projects.map((p) => ({
|
|
2523
|
+
type: "project",
|
|
2524
|
+
id: p.id,
|
|
2525
|
+
title: p.name,
|
|
2526
|
+
subtitle: p.description,
|
|
2527
|
+
status: p.status
|
|
2528
|
+
})),
|
|
2529
|
+
...nodes.filter((n) => !n.deletedAt).map((n) => ({
|
|
2530
|
+
type: "task",
|
|
2531
|
+
id: n.id,
|
|
2532
|
+
title: n.title,
|
|
2533
|
+
subtitle: n.description,
|
|
2534
|
+
status: n.status
|
|
2535
|
+
})),
|
|
2536
|
+
...machines.filter((m) => !m.isRevoked).map((m) => ({
|
|
2537
|
+
type: "machine",
|
|
2538
|
+
id: m.id,
|
|
2539
|
+
title: m.name,
|
|
2540
|
+
subtitle: `${m.platform} - ${m.hostname}`,
|
|
2541
|
+
status: m.isConnected ? "connected" : "disconnected"
|
|
2542
|
+
}))
|
|
2543
|
+
];
|
|
2544
|
+
setItems(items);
|
|
2545
|
+
fuseInstance = new Fuse(items, {
|
|
2546
|
+
keys: ["title", "subtitle", "id"],
|
|
2547
|
+
threshold: 0.4,
|
|
2548
|
+
includeScore: true
|
|
2549
|
+
});
|
|
2550
|
+
}, [projects, nodes, machines, setItems]);
|
|
2551
|
+
const search = useCallback3((q) => {
|
|
2552
|
+
if (!q || !fuseInstance) {
|
|
2553
|
+
setResults([]);
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
const results = fuseInstance.search(q, { limit: 20 });
|
|
2557
|
+
setResults(results.map((r) => r.item));
|
|
2558
|
+
}, [setResults]);
|
|
2559
|
+
useEffect4(() => {
|
|
2560
|
+
search(query);
|
|
2561
|
+
}, [query, search]);
|
|
2562
|
+
return { search };
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// src/tui/hooks/use-command-parser.ts
|
|
2566
|
+
import { useCallback as useCallback4 } from "react";
|
|
2567
|
+
|
|
2568
|
+
// src/tui/commands/autocomplete.ts
|
|
2569
|
+
var PrefixTrie = class {
|
|
2570
|
+
root;
|
|
2571
|
+
constructor() {
|
|
2572
|
+
this.root = { children: /* @__PURE__ */ new Map(), isEnd: false, value: "" };
|
|
2573
|
+
}
|
|
2574
|
+
insert(word) {
|
|
2575
|
+
let node = this.root;
|
|
2576
|
+
for (const char of word) {
|
|
2577
|
+
if (!node.children.has(char)) {
|
|
2578
|
+
node.children.set(char, { children: /* @__PURE__ */ new Map(), isEnd: false, value: "" });
|
|
2579
|
+
}
|
|
2580
|
+
node = node.children.get(char);
|
|
2581
|
+
}
|
|
2582
|
+
node.isEnd = true;
|
|
2583
|
+
node.value = word;
|
|
2584
|
+
}
|
|
2585
|
+
search(prefix) {
|
|
2586
|
+
let node = this.root;
|
|
2587
|
+
for (const char of prefix) {
|
|
2588
|
+
if (!node.children.has(char)) return [];
|
|
2589
|
+
node = node.children.get(char);
|
|
2590
|
+
}
|
|
2591
|
+
return this.collect(node);
|
|
2592
|
+
}
|
|
2593
|
+
collect(node) {
|
|
2594
|
+
const results = [];
|
|
2595
|
+
if (node.isEnd) results.push(node.value);
|
|
2596
|
+
for (const child of node.children.values()) {
|
|
2597
|
+
results.push(...this.collect(child));
|
|
2598
|
+
}
|
|
2599
|
+
return results;
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
// src/tui/commands/registry.ts
|
|
2604
|
+
var trie = new PrefixTrie();
|
|
2605
|
+
for (const key of Object.keys(handlers)) {
|
|
2606
|
+
trie.insert(key);
|
|
2607
|
+
}
|
|
2608
|
+
async function executeCommand(input, client) {
|
|
2609
|
+
const parts = input.split(/\s+/);
|
|
2610
|
+
if (parts.length >= 2) {
|
|
2611
|
+
const twoWord = `${parts[0]} ${parts[1]}`;
|
|
2612
|
+
if (handlers[twoWord]) {
|
|
2613
|
+
try {
|
|
2614
|
+
await handlers[twoWord](parts.slice(2), client);
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
2617
|
+
}
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
const cmd = parts[0];
|
|
2622
|
+
if (handlers[cmd]) {
|
|
2623
|
+
try {
|
|
2624
|
+
await handlers[cmd](parts.slice(1), client);
|
|
2625
|
+
} catch (err) {
|
|
2626
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
2627
|
+
}
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
useTuiStore.getState().setLastError(`Unknown command: ${cmd}`);
|
|
2631
|
+
}
|
|
2632
|
+
function getCompletions(partial) {
|
|
2633
|
+
return trie.search(partial);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// src/tui/hooks/use-command-parser.ts
|
|
2637
|
+
function useCommandParser(client) {
|
|
2638
|
+
const execute = useCallback4(
|
|
2639
|
+
async (input) => {
|
|
2640
|
+
const trimmed = input.trim();
|
|
2641
|
+
if (!trimmed) return;
|
|
2642
|
+
await executeCommand(trimmed, client);
|
|
2643
|
+
},
|
|
2644
|
+
[client]
|
|
2645
|
+
);
|
|
2646
|
+
const autocomplete = useCallback4(
|
|
2647
|
+
(partial) => {
|
|
2648
|
+
return getCompletions(partial);
|
|
2649
|
+
},
|
|
2650
|
+
[]
|
|
2651
|
+
);
|
|
2652
|
+
return { execute, autocomplete };
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/tui/app.tsx
|
|
2656
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
2657
|
+
function App({ serverUrl }) {
|
|
2658
|
+
const client = useMemo(() => new AstroClient({ serverUrl }), [serverUrl]);
|
|
2659
|
+
const { refreshAll: refreshAll2 } = usePolling(client);
|
|
2660
|
+
useSSEStream(client);
|
|
2661
|
+
useFuzzySearch();
|
|
2662
|
+
const { execute } = useCommandParser(client);
|
|
2663
|
+
const onSelect = useCallback5(() => {
|
|
2664
|
+
const { focusedPanel, scrollIndex } = useTuiStore.getState();
|
|
2665
|
+
switch (focusedPanel) {
|
|
2666
|
+
case "projects": {
|
|
2667
|
+
const projects = useProjectsStore.getState().projects;
|
|
2668
|
+
const idx = scrollIndex.projects;
|
|
2669
|
+
if (projects[idx]) {
|
|
2670
|
+
useTuiStore.getState().setSelectedProject(projects[idx].id);
|
|
2671
|
+
}
|
|
2672
|
+
break;
|
|
2673
|
+
}
|
|
2674
|
+
case "plan": {
|
|
2675
|
+
const nodes = usePlanStore.getState().nodes.filter((n) => !n.deletedAt);
|
|
2676
|
+
const idx = scrollIndex.plan;
|
|
2677
|
+
const node = nodes[idx];
|
|
2678
|
+
if (node) {
|
|
2679
|
+
useTuiStore.getState().setSelectedNode(node.id);
|
|
2680
|
+
useTuiStore.getState().openDetail("node", node.id);
|
|
2681
|
+
}
|
|
2682
|
+
break;
|
|
2683
|
+
}
|
|
2684
|
+
case "machines": {
|
|
2685
|
+
break;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}, []);
|
|
2689
|
+
const onCommand = useCallback5(
|
|
2690
|
+
(cmd) => {
|
|
2691
|
+
execute(cmd);
|
|
2692
|
+
},
|
|
2693
|
+
[execute]
|
|
2694
|
+
);
|
|
2695
|
+
const onSearch = useCallback5(
|
|
2696
|
+
(query) => {
|
|
2697
|
+
useSearchStore.getState().setQuery(query);
|
|
2698
|
+
if (query.length > 0) {
|
|
2699
|
+
useSearchStore.getState().open();
|
|
2700
|
+
}
|
|
2701
|
+
},
|
|
2702
|
+
[]
|
|
2703
|
+
);
|
|
2704
|
+
const onDispatch = useCallback5(async () => {
|
|
2705
|
+
const { focusedPanel, scrollIndex, selectedProjectId } = useTuiStore.getState();
|
|
2706
|
+
if (focusedPanel === "plan") {
|
|
2707
|
+
const nodes = usePlanStore.getState().nodes.filter((n) => !n.deletedAt);
|
|
2708
|
+
const node = nodes[scrollIndex.plan];
|
|
2709
|
+
if (node && selectedProjectId) {
|
|
2710
|
+
useTuiStore.getState().setSelectedNode(node.id);
|
|
2711
|
+
await execute(`dispatch ${node.id}`);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
const nodeId = useTuiStore.getState().selectedNodeId;
|
|
2716
|
+
const projectId = useTuiStore.getState().selectedProjectId;
|
|
2717
|
+
if (nodeId && projectId) {
|
|
2718
|
+
await execute(`dispatch ${nodeId}`);
|
|
2719
|
+
}
|
|
2720
|
+
}, [execute]);
|
|
2721
|
+
const onCancel = useCallback5(async () => {
|
|
2722
|
+
await execute("cancel");
|
|
2723
|
+
}, [execute]);
|
|
2724
|
+
const onRefresh = useCallback5(() => {
|
|
2725
|
+
refreshAll2();
|
|
2726
|
+
}, [refreshAll2]);
|
|
2727
|
+
const onSessionMessage = useCallback5(async (message) => {
|
|
2728
|
+
const { selectedProjectId, activeView } = useTuiStore.getState();
|
|
2729
|
+
if (!selectedProjectId) {
|
|
2730
|
+
useTuiStore.getState().setLastError("No project selected");
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
const watchingId = useExecutionStore.getState().watchingId;
|
|
2734
|
+
const sessionId = useChatStore.getState().sessionId;
|
|
2735
|
+
const messages = useChatStore.getState().messages.map((m) => ({
|
|
2736
|
+
role: m.role,
|
|
2737
|
+
content: m.content
|
|
2738
|
+
}));
|
|
2739
|
+
if (!watchingId) {
|
|
2740
|
+
if (activeView === "plan-gen") {
|
|
2741
|
+
await execute(`plan generate ${message}`);
|
|
2742
|
+
} else {
|
|
2743
|
+
await execute(`playground ${message}`);
|
|
2744
|
+
}
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
try {
|
|
2748
|
+
useChatStore.getState().setStreaming(true);
|
|
2749
|
+
const response = await client.projectChat({
|
|
2750
|
+
message,
|
|
2751
|
+
sessionId: sessionId ?? void 0,
|
|
2752
|
+
projectId: selectedProjectId,
|
|
2753
|
+
messages
|
|
2754
|
+
});
|
|
2755
|
+
if (!response.body) {
|
|
2756
|
+
useChatStore.getState().setStreaming(false);
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
const reader = response.body.getReader();
|
|
2760
|
+
const decoder = new TextDecoder();
|
|
2761
|
+
let buffer = "";
|
|
2762
|
+
while (true) {
|
|
2763
|
+
const { done, value } = await reader.read();
|
|
2764
|
+
if (done) break;
|
|
2765
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2766
|
+
const lines = buffer.split("\n");
|
|
2767
|
+
buffer = lines.pop() ?? "";
|
|
2768
|
+
for (const line of lines) {
|
|
2769
|
+
if (!line.startsWith("data: ")) continue;
|
|
2770
|
+
const data = line.slice(6);
|
|
2771
|
+
if (data === "[DONE]") continue;
|
|
2772
|
+
try {
|
|
2773
|
+
const event = JSON.parse(data);
|
|
2774
|
+
if (event.type === "text" && event.text) {
|
|
2775
|
+
useChatStore.getState().appendStream(event.text);
|
|
2776
|
+
useExecutionStore.getState().appendText(watchingId, event.text);
|
|
2777
|
+
} else if (event.type === "session_init" && event.sessionId) {
|
|
2778
|
+
useChatStore.getState().setSessionId(event.sessionId);
|
|
2779
|
+
} else if (event.type === "done") {
|
|
2780
|
+
}
|
|
2781
|
+
} catch {
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
useChatStore.getState().flushStream();
|
|
2786
|
+
useChatStore.getState().setStreaming(false);
|
|
2787
|
+
} catch (err) {
|
|
2788
|
+
useChatStore.getState().setStreaming(false);
|
|
2789
|
+
useTuiStore.getState().setLastError(err instanceof Error ? err.message : String(err));
|
|
2790
|
+
}
|
|
2791
|
+
}, [client, execute]);
|
|
2792
|
+
useVimMode({
|
|
2793
|
+
onSelect,
|
|
2794
|
+
onCommand,
|
|
2795
|
+
onSearch,
|
|
2796
|
+
onDispatch,
|
|
2797
|
+
onCancel,
|
|
2798
|
+
onRefresh
|
|
2799
|
+
});
|
|
2800
|
+
return /* @__PURE__ */ jsx15(MainLayout, { onSessionMessage });
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// src/tui/index.tsx
|
|
2804
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
2805
|
+
async function launchTui(serverUrl) {
|
|
2806
|
+
render(/* @__PURE__ */ jsx16(App, { serverUrl }), {
|
|
2807
|
+
exitOnCtrlC: true
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
export {
|
|
2811
|
+
launchTui
|
|
2812
|
+
};
|