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