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