cueclaw 0.1.2 → 0.1.4
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/README.md +203 -21
- package/dist/app-6SXWEUZZ.js +3289 -0
- package/dist/{chunk-SEYPA5M2.js → chunk-3HV3MHME.js} +185 -64
- package/dist/chunk-54BGF7G5.js +771 -0
- package/dist/{chunk-RSKXBXSJ.js → chunk-5TV4LNC3.js} +22 -1
- package/dist/{chunk-KRNAXOQ4.js → chunk-FKKDQVRE.js} +2 -1
- package/dist/{chunk-G43R5ASK.js → chunk-HKZ6IN7X.js} +40 -5
- package/dist/chunk-KBLMQZ3P.js +116 -0
- package/dist/{chunk-PZZ6FBGB.js → chunk-MEAAX2SW.js} +4 -3
- package/dist/{chunk-WE5J7GMR.js → chunk-ZOFGQYXX.js} +36 -11
- package/dist/cli.js +123 -89
- package/dist/{config-6NWFKNLW.js → config-FYL6T5JP.js} +4 -1
- package/dist/daemon-Y5HIGH44.js +28 -0
- package/dist/{executor-LS3Y4DO5.js → executor-44MSZ76T.js} +5 -4
- package/dist/logger-HKMIMPCE.js +18 -0
- package/dist/{planner-UU4T5IEN.js → planner-MJ3XBCWH.js} +3 -3
- package/dist/{service-VTUYSAAZ.js → service-Q7USNFFB.js} +4 -3
- package/dist/{setup-NWBKTQCO.js → setup-JK664Y2M.js} +8 -3
- package/dist/{telegram-EFPHL4HC.js → telegram-FH5O4F3K.js} +25 -2
- package/dist/{whatsapp-HFMOFSFI.js → whatsapp-RLNSXSFI.js} +10 -2
- package/package.json +4 -4
- package/dist/app-LWDIWH7K.js +0 -1953
- package/dist/chunk-FAT2VKMJ.js +0 -232
- package/dist/chunk-IB6TU7TP.js +0 -310
- package/dist/chunk-QBOYMF4A.js +0 -42
- package/dist/daemon-WOR4GE5C.js +0 -96
- package/dist/logger-HD23RPWS.js +0 -12
- package/dist/router-ID6RN5AT.js +0 -14
|
@@ -0,0 +1,3289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MessageRouter,
|
|
3
|
+
TriggerLoop,
|
|
4
|
+
isDaemonRunning,
|
|
5
|
+
isProcessAlive,
|
|
6
|
+
readPidFile,
|
|
7
|
+
removePidFile,
|
|
8
|
+
spawnDaemonProcess
|
|
9
|
+
} from "./chunk-54BGF7G5.js";
|
|
10
|
+
import {
|
|
11
|
+
getServiceStatus
|
|
12
|
+
} from "./chunk-MEAAX2SW.js";
|
|
13
|
+
import {
|
|
14
|
+
checkEnvironment,
|
|
15
|
+
validateAuth
|
|
16
|
+
} from "./chunk-FKKDQVRE.js";
|
|
17
|
+
import {
|
|
18
|
+
askQuestionTool,
|
|
19
|
+
buildPlannerSystemPrompt,
|
|
20
|
+
confirmPlan,
|
|
21
|
+
parsePlannerToolResponse,
|
|
22
|
+
plannerTool,
|
|
23
|
+
rejectPlan,
|
|
24
|
+
setSecretTool
|
|
25
|
+
} from "./chunk-ZOFGQYXX.js";
|
|
26
|
+
import {
|
|
27
|
+
createAnthropicClient
|
|
28
|
+
} from "./chunk-DVQFSFIZ.js";
|
|
29
|
+
import {
|
|
30
|
+
executeWorkflow
|
|
31
|
+
} from "./chunk-3HV3MHME.js";
|
|
32
|
+
import {
|
|
33
|
+
deleteWorkflow,
|
|
34
|
+
getStepRunsByRunId,
|
|
35
|
+
getWorkflow,
|
|
36
|
+
getWorkflowRunsByWorkflowId,
|
|
37
|
+
initDb,
|
|
38
|
+
listWorkflows,
|
|
39
|
+
updateWorkflowPhase,
|
|
40
|
+
upsertWorkflow
|
|
41
|
+
} from "./chunk-HKZ6IN7X.js";
|
|
42
|
+
import {
|
|
43
|
+
cueclawHome,
|
|
44
|
+
loadConfig,
|
|
45
|
+
loadExistingConfig,
|
|
46
|
+
validateConfig,
|
|
47
|
+
writeConfig
|
|
48
|
+
} from "./chunk-5TV4LNC3.js";
|
|
49
|
+
import "./chunk-BVQG3WYO.js";
|
|
50
|
+
import {
|
|
51
|
+
isDev,
|
|
52
|
+
writeEnvVar
|
|
53
|
+
} from "./chunk-ZCK3IFLC.js";
|
|
54
|
+
import {
|
|
55
|
+
logger,
|
|
56
|
+
onLogLine
|
|
57
|
+
} from "./chunk-KBLMQZ3P.js";
|
|
58
|
+
|
|
59
|
+
// src/tui/app.tsx
|
|
60
|
+
import { ThemeProvider } from "@inkjs/ui";
|
|
61
|
+
|
|
62
|
+
// src/tui/theme.ts
|
|
63
|
+
import { extendTheme, defaultTheme } from "@inkjs/ui";
|
|
64
|
+
|
|
65
|
+
// src/tui/theme/semantic-colors.ts
|
|
66
|
+
function buildSemanticColors(palette) {
|
|
67
|
+
return {
|
|
68
|
+
text: {
|
|
69
|
+
primary: palette.Foreground,
|
|
70
|
+
secondary: palette.ForegroundDim,
|
|
71
|
+
accent: palette.AccentCyan,
|
|
72
|
+
user: palette.Foreground,
|
|
73
|
+
response: palette.Foreground,
|
|
74
|
+
link: palette.AccentBlue
|
|
75
|
+
},
|
|
76
|
+
status: {
|
|
77
|
+
success: palette.AccentGreen,
|
|
78
|
+
warning: palette.AccentYellow,
|
|
79
|
+
error: palette.AccentRed,
|
|
80
|
+
info: palette.AccentCyan,
|
|
81
|
+
muted: palette.ForegroundDim
|
|
82
|
+
},
|
|
83
|
+
border: {
|
|
84
|
+
default: palette.Border,
|
|
85
|
+
accent: palette.BorderAccent,
|
|
86
|
+
focused: palette.AccentBlue
|
|
87
|
+
},
|
|
88
|
+
background: {
|
|
89
|
+
primary: palette.Background,
|
|
90
|
+
message: palette.MessageBackground,
|
|
91
|
+
input: palette.InputBackground
|
|
92
|
+
},
|
|
93
|
+
ui: {
|
|
94
|
+
comment: palette.Comment,
|
|
95
|
+
gradient: palette.GradientColors
|
|
96
|
+
},
|
|
97
|
+
prompt: palette.AccentGreen
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/tui/color-utils.ts
|
|
102
|
+
function hexToRgb(hex) {
|
|
103
|
+
const h = hex.replace("#", "");
|
|
104
|
+
return [
|
|
105
|
+
parseInt(h.slice(0, 2), 16),
|
|
106
|
+
parseInt(h.slice(2, 4), 16),
|
|
107
|
+
parseInt(h.slice(4, 6), 16)
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
function rgbToHex(r, g, b) {
|
|
111
|
+
return "#" + [r, g, b].map((c) => Math.round(c).toString(16).padStart(2, "0")).join("");
|
|
112
|
+
}
|
|
113
|
+
function interpolateColor(base, overlay, opacity) {
|
|
114
|
+
const [br, bg, bb] = hexToRgb(base);
|
|
115
|
+
const [or, og, ob] = hexToRgb(overlay);
|
|
116
|
+
return rgbToHex(
|
|
117
|
+
br + (or - br) * opacity,
|
|
118
|
+
bg + (og - bg) * opacity,
|
|
119
|
+
bb + (ob - bb) * opacity
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/tui/theme/themes.ts
|
|
124
|
+
var darkTheme = {
|
|
125
|
+
name: "dark",
|
|
126
|
+
type: "dark",
|
|
127
|
+
Foreground: "#CDD6F4",
|
|
128
|
+
ForegroundDim: "#6C7086",
|
|
129
|
+
Background: "#1E1E2E",
|
|
130
|
+
AccentCyan: "#89DCEB",
|
|
131
|
+
AccentGreen: "#A6E3A1",
|
|
132
|
+
AccentYellow: "#F9E2AF",
|
|
133
|
+
AccentRed: "#F38BA8",
|
|
134
|
+
AccentBlue: "#89B4FA",
|
|
135
|
+
AccentMagenta: "#CBA6F7",
|
|
136
|
+
Border: interpolateColor("#1E1E2E", "#6C7086", 0.4),
|
|
137
|
+
BorderAccent: "#89B4FA",
|
|
138
|
+
Comment: "#6C7086",
|
|
139
|
+
MessageBackground: interpolateColor("#1E1E2E", "#6C7086", 0.12),
|
|
140
|
+
InputBackground: interpolateColor("#1E1E2E", "#6C7086", 0.08),
|
|
141
|
+
GradientColors: ["#4796E4", "#847ACE", "#C3677F"]
|
|
142
|
+
};
|
|
143
|
+
var lightTheme = {
|
|
144
|
+
name: "light",
|
|
145
|
+
type: "light",
|
|
146
|
+
Foreground: "#4C4F69",
|
|
147
|
+
ForegroundDim: "#9CA0B0",
|
|
148
|
+
Background: "#EFF1F5",
|
|
149
|
+
AccentCyan: "#04A5E5",
|
|
150
|
+
AccentGreen: "#40A02B",
|
|
151
|
+
AccentYellow: "#DF8E1D",
|
|
152
|
+
AccentRed: "#D20F39",
|
|
153
|
+
AccentBlue: "#1E66F5",
|
|
154
|
+
AccentMagenta: "#8839EF",
|
|
155
|
+
Border: interpolateColor("#EFF1F5", "#9CA0B0", 0.4),
|
|
156
|
+
BorderAccent: "#1E66F5",
|
|
157
|
+
Comment: "#9CA0B0",
|
|
158
|
+
MessageBackground: interpolateColor("#EFF1F5", "#9CA0B0", 0.12),
|
|
159
|
+
InputBackground: interpolateColor("#EFF1F5", "#9CA0B0", 0.08),
|
|
160
|
+
GradientColors: ["#1E66F5", "#8839EF", "#D20F39"]
|
|
161
|
+
};
|
|
162
|
+
var draculaTheme = {
|
|
163
|
+
name: "dracula",
|
|
164
|
+
type: "dark",
|
|
165
|
+
Foreground: "#f8f8f2",
|
|
166
|
+
ForegroundDim: "#6272a4",
|
|
167
|
+
Background: "#282a36",
|
|
168
|
+
AccentCyan: "#8be9fd",
|
|
169
|
+
AccentGreen: "#50fa7b",
|
|
170
|
+
AccentYellow: "#f1fa8c",
|
|
171
|
+
AccentRed: "#ff5555",
|
|
172
|
+
AccentBlue: "#6272a4",
|
|
173
|
+
AccentMagenta: "#ff79c6",
|
|
174
|
+
Border: interpolateColor("#282a36", "#6272a4", 0.4),
|
|
175
|
+
BorderAccent: "#bd93f9",
|
|
176
|
+
Comment: "#6272a4",
|
|
177
|
+
MessageBackground: interpolateColor("#282a36", "#6272a4", 0.12),
|
|
178
|
+
InputBackground: interpolateColor("#282a36", "#6272a4", 0.08),
|
|
179
|
+
GradientColors: ["#bd93f9", "#ff79c6", "#ff5555"]
|
|
180
|
+
};
|
|
181
|
+
var builtinThemes = {
|
|
182
|
+
dark: darkTheme,
|
|
183
|
+
light: lightTheme,
|
|
184
|
+
dracula: draculaTheme
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/tui/theme/theme-manager.ts
|
|
188
|
+
var currentSemanticColors = buildSemanticColors(darkTheme);
|
|
189
|
+
var currentThemeName = "dark";
|
|
190
|
+
var version = 0;
|
|
191
|
+
var themeManager = {
|
|
192
|
+
getSemanticColors() {
|
|
193
|
+
return currentSemanticColors;
|
|
194
|
+
},
|
|
195
|
+
getThemeName() {
|
|
196
|
+
return currentThemeName;
|
|
197
|
+
},
|
|
198
|
+
getVersion() {
|
|
199
|
+
return version;
|
|
200
|
+
},
|
|
201
|
+
setTheme(name) {
|
|
202
|
+
const palette = builtinThemes[name];
|
|
203
|
+
if (!palette) return false;
|
|
204
|
+
currentSemanticColors = buildSemanticColors(palette);
|
|
205
|
+
currentThemeName = name;
|
|
206
|
+
version++;
|
|
207
|
+
return true;
|
|
208
|
+
},
|
|
209
|
+
getAvailableThemes() {
|
|
210
|
+
return Object.keys(builtinThemes);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/tui/theme/index.ts
|
|
215
|
+
var theme = new Proxy({}, {
|
|
216
|
+
get(_target, prop) {
|
|
217
|
+
return themeManager.getSemanticColors()[prop];
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// src/tui/theme.ts
|
|
222
|
+
var cueclawTheme = extendTheme(defaultTheme, {
|
|
223
|
+
components: {
|
|
224
|
+
Header: {
|
|
225
|
+
styles: {
|
|
226
|
+
hints: () => ({ color: theme.text.primary, dimColor: true })
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
PlanView: {
|
|
230
|
+
styles: {
|
|
231
|
+
title: () => ({ color: theme.border.accent, bold: true }),
|
|
232
|
+
stepPending: () => ({ color: theme.status.muted }),
|
|
233
|
+
stepRunning: () => ({ color: theme.status.warning }),
|
|
234
|
+
stepDone: () => ({ color: theme.status.success }),
|
|
235
|
+
stepFailed: () => ({ color: theme.status.error }),
|
|
236
|
+
border: () => ({ borderColor: theme.border.default })
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
StatusDashboard: {
|
|
240
|
+
styles: {
|
|
241
|
+
executing: () => ({ color: theme.status.warning }),
|
|
242
|
+
completed: () => ({ color: theme.status.success }),
|
|
243
|
+
failed: () => ({ color: theme.status.error }),
|
|
244
|
+
paused: () => ({ color: theme.status.muted, dimColor: true })
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
Chat: {
|
|
248
|
+
styles: {
|
|
249
|
+
userMessage: () => ({ color: theme.text.user, bold: true }),
|
|
250
|
+
systemMessage: () => ({ color: theme.status.info }),
|
|
251
|
+
assistantMessage: () => ({ color: theme.text.primary }),
|
|
252
|
+
prompt: () => ({ color: theme.prompt })
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// src/tui/use-keypress.tsx
|
|
259
|
+
import { createContext, useContext, useRef, useCallback, useEffect } from "react";
|
|
260
|
+
import { useInput } from "ink";
|
|
261
|
+
import { jsx } from "react/jsx-runtime";
|
|
262
|
+
var KeyPriority = {
|
|
263
|
+
Low: 0,
|
|
264
|
+
Normal: 100,
|
|
265
|
+
High: 200,
|
|
266
|
+
Critical: 300
|
|
267
|
+
};
|
|
268
|
+
var KeypressContext = createContext(null);
|
|
269
|
+
function KeypressProvider({ children }) {
|
|
270
|
+
const handlersRef = useRef([]);
|
|
271
|
+
const register = useCallback((entry) => {
|
|
272
|
+
handlersRef.current = [...handlersRef.current.filter((h) => h.id !== entry.id), entry];
|
|
273
|
+
}, []);
|
|
274
|
+
const unregister = useCallback((id) => {
|
|
275
|
+
handlersRef.current = handlersRef.current.filter((h) => h.id !== id);
|
|
276
|
+
}, []);
|
|
277
|
+
const update = useCallback((id, patch) => {
|
|
278
|
+
handlersRef.current = handlersRef.current.map(
|
|
279
|
+
(h) => h.id === id ? { ...h, ...patch } : h
|
|
280
|
+
);
|
|
281
|
+
}, []);
|
|
282
|
+
useInput((input, key) => {
|
|
283
|
+
const sorted = [...handlersRef.current].filter((h) => h.isActive).sort((a, b) => b.priority - a.priority);
|
|
284
|
+
for (const entry of sorted) {
|
|
285
|
+
const consumed = entry.handler(input, key);
|
|
286
|
+
if (consumed === true) break;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return /* @__PURE__ */ jsx(KeypressContext.Provider, { value: { register, unregister, update }, children });
|
|
290
|
+
}
|
|
291
|
+
var nextId = 0;
|
|
292
|
+
function useKeypress(id, priority, handler, isActive = true) {
|
|
293
|
+
const ctx = useContext(KeypressContext);
|
|
294
|
+
const stableId = useRef(id || `keypress-${nextId++}`).current;
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
if (!ctx) return;
|
|
297
|
+
ctx.register({ id: stableId, priority, handler, isActive });
|
|
298
|
+
return () => ctx.unregister(stableId);
|
|
299
|
+
}, [ctx, stableId]);
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (!ctx) return;
|
|
302
|
+
ctx.update(stableId, { handler, isActive });
|
|
303
|
+
}, [ctx, stableId, handler, isActive]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/tui/dialog-manager.tsx
|
|
307
|
+
import { createContext as createContext2, useContext as useContext2, useState, useCallback as useCallback2 } from "react";
|
|
308
|
+
import { Box, Text } from "ink";
|
|
309
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
310
|
+
var DialogPriority = {
|
|
311
|
+
Normal: 0,
|
|
312
|
+
High: 100,
|
|
313
|
+
Critical: 200
|
|
314
|
+
};
|
|
315
|
+
var DialogContext = createContext2(null);
|
|
316
|
+
function useDialog() {
|
|
317
|
+
const ctx = useContext2(DialogContext);
|
|
318
|
+
if (!ctx) throw new Error("useDialog must be used within DialogManager");
|
|
319
|
+
return ctx;
|
|
320
|
+
}
|
|
321
|
+
function DialogOverlay({ dialog, onDismiss }) {
|
|
322
|
+
useKeypress("dialog-overlay", KeyPriority.Critical, useCallback2((input, key) => {
|
|
323
|
+
for (const action of dialog.actions) {
|
|
324
|
+
if (input.toLowerCase() === action.key.toLowerCase()) {
|
|
325
|
+
action.handler();
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (key.escape) {
|
|
330
|
+
onDismiss();
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}, [dialog.actions, onDismiss]));
|
|
335
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border.focused, paddingX: 2, paddingY: 1, children: [
|
|
336
|
+
/* @__PURE__ */ jsx2(Text, { bold: true, color: theme.status.warning, children: dialog.title }),
|
|
337
|
+
/* @__PURE__ */ jsx2(Text, { color: theme.text.primary, children: dialog.message }),
|
|
338
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, gap: 2, children: [
|
|
339
|
+
dialog.actions.map((action) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
340
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.text.accent, children: [
|
|
341
|
+
"[",
|
|
342
|
+
action.key.toUpperCase(),
|
|
343
|
+
"]"
|
|
344
|
+
] }),
|
|
345
|
+
" ",
|
|
346
|
+
action.label
|
|
347
|
+
] }, action.key)),
|
|
348
|
+
/* @__PURE__ */ jsx2(Text, { color: theme.ui.comment, children: "[Esc] Dismiss" })
|
|
349
|
+
] })
|
|
350
|
+
] });
|
|
351
|
+
}
|
|
352
|
+
function DialogManager({ children }) {
|
|
353
|
+
const [dialogQueue, setDialogQueue] = useState([]);
|
|
354
|
+
const showDialog = useCallback2((dialog) => {
|
|
355
|
+
setDialogQueue((prev) => {
|
|
356
|
+
const next = [...prev, dialog];
|
|
357
|
+
next.sort((a, b) => (b.priority ?? DialogPriority.Normal) - (a.priority ?? DialogPriority.Normal));
|
|
358
|
+
return next;
|
|
359
|
+
});
|
|
360
|
+
}, []);
|
|
361
|
+
const dismissDialog = useCallback2(() => {
|
|
362
|
+
setDialogQueue((prev) => prev.slice(1));
|
|
363
|
+
}, []);
|
|
364
|
+
const activeDialog = dialogQueue[0] ?? null;
|
|
365
|
+
return /* @__PURE__ */ jsxs(DialogContext.Provider, { value: { showDialog, dismissDialog }, children: [
|
|
366
|
+
children,
|
|
367
|
+
activeDialog && /* @__PURE__ */ jsx2(DialogOverlay, { dialog: activeDialog, onDismiss: dismissDialog })
|
|
368
|
+
] });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/tui/app-provider.tsx
|
|
372
|
+
import { useReducer, useCallback as useCallback7, useMemo as useMemo2, useState as useState4, useEffect as useEffect3 } from "react";
|
|
373
|
+
import { useApp } from "ink";
|
|
374
|
+
|
|
375
|
+
// src/tui/ui-state-context.ts
|
|
376
|
+
import { createContext as createContext3, useContext as useContext3 } from "react";
|
|
377
|
+
var UIStateContext = createContext3(null);
|
|
378
|
+
function useUIState() {
|
|
379
|
+
const ctx = useContext3(UIStateContext);
|
|
380
|
+
if (!ctx) throw new Error("useUIState must be used within an AppProvider");
|
|
381
|
+
return ctx;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/tui/ui-actions-context.ts
|
|
385
|
+
import { createContext as createContext4, useContext as useContext4 } from "react";
|
|
386
|
+
var UIActionsContext = createContext4(null);
|
|
387
|
+
function useUIActions() {
|
|
388
|
+
const ctx = useContext4(UIActionsContext);
|
|
389
|
+
if (!ctx) throw new Error("useUIActions must be used within an AppProvider");
|
|
390
|
+
return ctx;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/tui/hooks/use-daemon-bridge.ts
|
|
394
|
+
import { useState as useState2, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
395
|
+
|
|
396
|
+
// src/tui/daemon-bridge.ts
|
|
397
|
+
var externalBridge = {
|
|
398
|
+
triggerLoop: null,
|
|
399
|
+
router: null,
|
|
400
|
+
botChannels: [],
|
|
401
|
+
botConnectResult: null,
|
|
402
|
+
isExternal: true
|
|
403
|
+
};
|
|
404
|
+
function waitForDaemon(pid, maxWaitMs) {
|
|
405
|
+
return new Promise((resolve) => {
|
|
406
|
+
const start = Date.now();
|
|
407
|
+
const check = () => {
|
|
408
|
+
if (isProcessAlive(pid)) return resolve(true);
|
|
409
|
+
if (Date.now() - start > maxWaitMs) return resolve(false);
|
|
410
|
+
setTimeout(check, 200);
|
|
411
|
+
};
|
|
412
|
+
check();
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async function initDaemonBridge(db, config, cwd, options) {
|
|
416
|
+
const serviceRunning = getServiceStatus() === "running";
|
|
417
|
+
const pidDaemonRunning = isDaemonRunning();
|
|
418
|
+
if (serviceRunning || pidDaemonRunning) {
|
|
419
|
+
logger.info({ via: serviceRunning ? "service" : "pid" }, "External daemon detected, TUI will operate as frontend only");
|
|
420
|
+
return externalBridge;
|
|
421
|
+
}
|
|
422
|
+
const pid = spawnDaemonProcess();
|
|
423
|
+
if (pid) {
|
|
424
|
+
const alive = await waitForDaemon(pid, 2e3);
|
|
425
|
+
if (alive) {
|
|
426
|
+
logger.info({ pid }, "Background daemon started, TUI will operate as frontend only");
|
|
427
|
+
return externalBridge;
|
|
428
|
+
}
|
|
429
|
+
logger.warn({ pid }, "Spawned daemon process died, falling back to in-process mode");
|
|
430
|
+
} else {
|
|
431
|
+
logger.warn("Failed to spawn background daemon, falling back to in-process mode");
|
|
432
|
+
}
|
|
433
|
+
const router = new MessageRouter(db, config, cwd);
|
|
434
|
+
const botChannels = [];
|
|
435
|
+
let botConnectResult = null;
|
|
436
|
+
if (!options?.skipBots) {
|
|
437
|
+
botConnectResult = await connectBotChannels(config, router, botChannels);
|
|
438
|
+
}
|
|
439
|
+
const triggerLoop = new TriggerLoop(db, router, cwd, 5);
|
|
440
|
+
triggerLoop.start();
|
|
441
|
+
router.start();
|
|
442
|
+
logger.info("In-process daemon bridge started (fallback)");
|
|
443
|
+
return {
|
|
444
|
+
triggerLoop,
|
|
445
|
+
router,
|
|
446
|
+
botChannels,
|
|
447
|
+
botConnectResult,
|
|
448
|
+
isExternal: false
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async function startBotChannels(bridge, config) {
|
|
452
|
+
if (bridge.isExternal || !bridge.router) return;
|
|
453
|
+
await connectBotChannels(config, bridge.router, bridge.botChannels);
|
|
454
|
+
}
|
|
455
|
+
async function connectBotChannels(config, router, botChannels) {
|
|
456
|
+
const connected = [];
|
|
457
|
+
const failed = [];
|
|
458
|
+
if (config.telegram?.enabled && config.telegram.token) {
|
|
459
|
+
try {
|
|
460
|
+
const { TelegramChannel } = await import("./telegram-FH5O4F3K.js");
|
|
461
|
+
const tg = new TelegramChannel(
|
|
462
|
+
config.telegram.token,
|
|
463
|
+
config.telegram.allowed_users ?? [],
|
|
464
|
+
(jid, msg) => router.handleInbound("telegram", jid, msg)
|
|
465
|
+
);
|
|
466
|
+
router.registerChannel(tg);
|
|
467
|
+
await tg.connect();
|
|
468
|
+
tg.onCallback((wfId, action, chatId) => router.handleCallbackAction("telegram", chatId, wfId, action));
|
|
469
|
+
botChannels.push(tg);
|
|
470
|
+
connected.push("Telegram");
|
|
471
|
+
logger.info("Telegram channel started (in-process)");
|
|
472
|
+
} catch (err) {
|
|
473
|
+
failed.push("Telegram");
|
|
474
|
+
logger.error({ err }, "Failed to start Telegram channel");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (config.whatsapp?.enabled) {
|
|
478
|
+
try {
|
|
479
|
+
const { WhatsAppChannel } = await import("./whatsapp-RLNSXSFI.js");
|
|
480
|
+
const wa = new WhatsAppChannel(
|
|
481
|
+
config.whatsapp.auth_dir ?? `${process.env["HOME"]}/.cueclaw/auth/whatsapp`,
|
|
482
|
+
config.whatsapp.allowed_jids ?? [],
|
|
483
|
+
(jid, msg) => router.handleInbound("whatsapp", jid, msg)
|
|
484
|
+
);
|
|
485
|
+
router.registerChannel(wa);
|
|
486
|
+
await wa.connect();
|
|
487
|
+
botChannels.push(wa);
|
|
488
|
+
connected.push("WhatsApp");
|
|
489
|
+
logger.info("WhatsApp channel started (in-process)");
|
|
490
|
+
} catch (err) {
|
|
491
|
+
failed.push("WhatsApp");
|
|
492
|
+
logger.error({ err }, "Failed to start WhatsApp channel");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return { connected, failed };
|
|
496
|
+
}
|
|
497
|
+
async function stopDaemonBridge(bridge) {
|
|
498
|
+
if (bridge.isExternal) return;
|
|
499
|
+
bridge.triggerLoop?.stop();
|
|
500
|
+
bridge.router?.stop();
|
|
501
|
+
for (const channel of bridge.botChannels) {
|
|
502
|
+
try {
|
|
503
|
+
await channel.disconnect();
|
|
504
|
+
} catch (err) {
|
|
505
|
+
logger.error({ err, channel: channel.name }, "Failed to disconnect channel");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
logger.info("Daemon bridge stopped");
|
|
509
|
+
}
|
|
510
|
+
function stopExternalDaemon() {
|
|
511
|
+
const pid = readPidFile();
|
|
512
|
+
if (pid && isProcessAlive(pid)) {
|
|
513
|
+
process.kill(pid, "SIGTERM");
|
|
514
|
+
removePidFile();
|
|
515
|
+
logger.info({ pid }, "Stopped external daemon");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/tui/hooks/use-daemon-bridge.ts
|
|
520
|
+
function useDaemonBridge(config, db, cwd, dispatch) {
|
|
521
|
+
const bridgeRef = useRef2(null);
|
|
522
|
+
const [daemonStatus, setDaemonStatus] = useState2("none");
|
|
523
|
+
const hasConfiguredBots = !!(config?.telegram?.enabled && config?.telegram?.token || config?.whatsapp?.enabled);
|
|
524
|
+
useEffect2(() => {
|
|
525
|
+
if (!config) return;
|
|
526
|
+
let cancelled = false;
|
|
527
|
+
setDaemonStatus("starting");
|
|
528
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: "Starting daemon..." } });
|
|
529
|
+
initDaemonBridge(db, config, cwd, { skipBots: !hasConfiguredBots }).then((bridge) => {
|
|
530
|
+
if (cancelled) {
|
|
531
|
+
stopDaemonBridge(bridge);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
bridgeRef.current = bridge;
|
|
535
|
+
setDaemonStatus(bridge.isExternal ? "external" : "running");
|
|
536
|
+
dispatch({
|
|
537
|
+
type: "ADD_MESSAGE",
|
|
538
|
+
message: {
|
|
539
|
+
type: "system",
|
|
540
|
+
text: bridge.isExternal ? "Background daemon running." : "Daemon started (in-process)."
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
if (!bridge.isExternal && bridge.botConnectResult) {
|
|
544
|
+
const { connected, failed } = bridge.botConnectResult;
|
|
545
|
+
if (connected.length > 0) {
|
|
546
|
+
dispatch({
|
|
547
|
+
type: "ADD_MESSAGE",
|
|
548
|
+
message: { type: "system", text: `${connected.join(", ")} bot${connected.length > 1 ? "s" : ""} connected.` }
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (failed.length > 0) {
|
|
552
|
+
dispatch({
|
|
553
|
+
type: "ADD_MESSAGE",
|
|
554
|
+
message: { type: "error", text: `Failed to connect: ${failed.join(", ")}. Check logs for details.` }
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}).catch((err) => {
|
|
559
|
+
logger.error({ err }, "Failed to start daemon bridge");
|
|
560
|
+
setDaemonStatus("none");
|
|
561
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: "Failed to start daemon." } });
|
|
562
|
+
});
|
|
563
|
+
return () => {
|
|
564
|
+
cancelled = true;
|
|
565
|
+
if (bridgeRef.current) {
|
|
566
|
+
stopDaemonBridge(bridgeRef.current);
|
|
567
|
+
bridgeRef.current = null;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}, [config, db, cwd]);
|
|
571
|
+
return { bridgeRef, daemonStatus };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/tui/hooks/use-planner-session.ts
|
|
575
|
+
import { useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
576
|
+
|
|
577
|
+
// src/planner-session.ts
|
|
578
|
+
import { nanoid } from "nanoid";
|
|
579
|
+
async function startPlannerSession(userMessage, config, callbacks, channelContext) {
|
|
580
|
+
const session = {
|
|
581
|
+
id: `ps_${nanoid()}`,
|
|
582
|
+
messages: [{ role: "user", content: userMessage }],
|
|
583
|
+
status: "conversing",
|
|
584
|
+
workflow: null
|
|
585
|
+
};
|
|
586
|
+
logger.info({ sessionId: session.id }, "Planner session started");
|
|
587
|
+
const turn = await runPlannerTurn(session, config, callbacks, channelContext);
|
|
588
|
+
return { session, turn };
|
|
589
|
+
}
|
|
590
|
+
async function continuePlannerSession(session, userMessage, config, callbacks, channelContext) {
|
|
591
|
+
session.messages.push({ role: "user", content: userMessage });
|
|
592
|
+
logger.debug({ sessionId: session.id, turnCount: session.messages.length }, "Planner session continued");
|
|
593
|
+
const turn = await runPlannerTurn(session, config, callbacks, channelContext);
|
|
594
|
+
return { session, turn };
|
|
595
|
+
}
|
|
596
|
+
function cancelPlannerSession(session) {
|
|
597
|
+
session.status = "cancelled";
|
|
598
|
+
logger.info({ sessionId: session.id }, "Planner session cancelled");
|
|
599
|
+
}
|
|
600
|
+
async function runPlannerTurn(session, config, callbacks, channelContext) {
|
|
601
|
+
const anthropic = createAnthropicClient(config);
|
|
602
|
+
const systemPrompt = buildPlannerSystemPrompt(config, channelContext) + `
|
|
603
|
+
|
|
604
|
+
## Conversation Mode
|
|
605
|
+
|
|
606
|
+
You are in multi-turn conversation mode. You have three tools:
|
|
607
|
+
|
|
608
|
+
1. **ask_question** \u2014 Ask the user clarifying questions when more information is needed.
|
|
609
|
+
Use this when the user's description is vague, missing key details (trigger type, frequency, target repos, filters, output format, etc.), or could be interpreted multiple ways.
|
|
610
|
+
Also use this to ask the user for missing credentials \u2014 e.g., "This workflow needs a GITHUB_TOKEN. Could you provide one?"
|
|
611
|
+
|
|
612
|
+
2. **set_secret** \u2014 Store a credential the user provides (e.g., API token, webhook URL).
|
|
613
|
+
Only call this AFTER the user explicitly provides the secret value. Never guess or fabricate values.
|
|
614
|
+
|
|
615
|
+
3. **create_workflow** \u2014 Generate the final workflow plan when you have sufficient information.
|
|
616
|
+
Only use this when you are confident you understand the user's requirements.
|
|
617
|
+
|
|
618
|
+
Guidelines:
|
|
619
|
+
- For simple, clear requests, you may generate the plan immediately.
|
|
620
|
+
- For complex or ambiguous requests, ask 1-3 focused questions first.
|
|
621
|
+
- If a workflow requires credentials not listed in Available Credentials, ask the user for them before creating the workflow.
|
|
622
|
+
- Be concise and helpful in your questions.
|
|
623
|
+
- Respond in the same language the user uses.`;
|
|
624
|
+
let response;
|
|
625
|
+
try {
|
|
626
|
+
if (callbacks?.onToken) {
|
|
627
|
+
const stream = anthropic.messages.stream({
|
|
628
|
+
model: config.claude.planner.model,
|
|
629
|
+
max_tokens: 4096,
|
|
630
|
+
system: systemPrompt,
|
|
631
|
+
messages: session.messages,
|
|
632
|
+
tools: [askQuestionTool, setSecretTool, plannerTool]
|
|
633
|
+
});
|
|
634
|
+
stream.on("text", (text) => {
|
|
635
|
+
callbacks.onToken?.(text);
|
|
636
|
+
});
|
|
637
|
+
response = await stream.finalMessage();
|
|
638
|
+
} else {
|
|
639
|
+
response = await anthropic.messages.create({
|
|
640
|
+
model: config.claude.planner.model,
|
|
641
|
+
max_tokens: 4096,
|
|
642
|
+
system: systemPrompt,
|
|
643
|
+
messages: session.messages,
|
|
644
|
+
tools: [askQuestionTool, setSecretTool, plannerTool]
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
} catch (err) {
|
|
648
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
649
|
+
logger.error({ err }, "Planner session API request failed");
|
|
650
|
+
return { type: "error", content: `API request failed: ${detail}` };
|
|
651
|
+
}
|
|
652
|
+
const rawResponse = response;
|
|
653
|
+
if (rawResponse.type === "error" || rawResponse.error) {
|
|
654
|
+
const errMsg = rawResponse.error?.message ?? JSON.stringify(rawResponse.error ?? rawResponse);
|
|
655
|
+
return { type: "error", content: `API error: ${errMsg}` };
|
|
656
|
+
}
|
|
657
|
+
const result = parsePlannerToolResponse(response);
|
|
658
|
+
session.messages.push({ role: "assistant", content: response.content });
|
|
659
|
+
switch (result.type) {
|
|
660
|
+
case "question": {
|
|
661
|
+
logger.debug({ sessionId: session.id }, "Planner asking clarifying question");
|
|
662
|
+
const toolBlock = response.content.find((b) => b.type === "tool_use" && b.name === "ask_question");
|
|
663
|
+
if (toolBlock && toolBlock.type === "tool_use") {
|
|
664
|
+
session.messages.push({
|
|
665
|
+
role: "user",
|
|
666
|
+
content: [{ type: "tool_result", tool_use_id: toolBlock.id, content: "Question delivered to user. Waiting for response." }]
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return { type: "question", content: result.question };
|
|
670
|
+
}
|
|
671
|
+
case "set_secret": {
|
|
672
|
+
if (isDev) {
|
|
673
|
+
writeEnvVar(result.key, result.value);
|
|
674
|
+
} else {
|
|
675
|
+
process.env[result.key] = result.value;
|
|
676
|
+
}
|
|
677
|
+
logger.info({ key: result.key }, "Secret stored via planner");
|
|
678
|
+
const secretToolBlock = response.content.find((b) => b.type === "tool_use" && b.name === "set_secret");
|
|
679
|
+
if (secretToolBlock && secretToolBlock.type === "tool_use") {
|
|
680
|
+
session.messages.push({
|
|
681
|
+
role: "user",
|
|
682
|
+
content: [{ type: "tool_result", tool_use_id: secretToolBlock.id, content: `Secret ${result.key} saved successfully.` }]
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return runPlannerTurn(session, config, callbacks, channelContext);
|
|
686
|
+
}
|
|
687
|
+
case "plan": {
|
|
688
|
+
logger.info({ sessionId: session.id }, "Planner generated plan");
|
|
689
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
690
|
+
const workflow = {
|
|
691
|
+
...result.plannerOutput,
|
|
692
|
+
schema_version: "1.0",
|
|
693
|
+
id: `wf_${nanoid()}`,
|
|
694
|
+
phase: "awaiting_confirmation",
|
|
695
|
+
created_at: now,
|
|
696
|
+
updated_at: now
|
|
697
|
+
};
|
|
698
|
+
session.workflow = workflow;
|
|
699
|
+
session.status = "plan_ready";
|
|
700
|
+
return { type: "plan", content: `Generated plan: "${workflow.name}"`, workflow };
|
|
701
|
+
}
|
|
702
|
+
case "text":
|
|
703
|
+
return { type: "text", content: result.text };
|
|
704
|
+
case "error":
|
|
705
|
+
logger.error({ sessionId: session.id, error: result.error }, "Planner session turn error");
|
|
706
|
+
return { type: "error", content: result.error };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/tui/hooks/use-planner-session.ts
|
|
711
|
+
function usePlannerSession(config, dispatch, streamingText) {
|
|
712
|
+
const plannerSessionRef = useRef3(null);
|
|
713
|
+
const handleUserMessage = useCallback3(async (text) => {
|
|
714
|
+
if (!config) return;
|
|
715
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "user", text } });
|
|
716
|
+
dispatch({ type: "SET_GENERATING", value: true });
|
|
717
|
+
dispatch({ type: "SET_STREAMING_TEXT", text: "" });
|
|
718
|
+
try {
|
|
719
|
+
let result;
|
|
720
|
+
const tuiContext = { channel: "tui" };
|
|
721
|
+
if (plannerSessionRef.current && plannerSessionRef.current.status === "conversing") {
|
|
722
|
+
result = await continuePlannerSession(
|
|
723
|
+
plannerSessionRef.current,
|
|
724
|
+
text,
|
|
725
|
+
config,
|
|
726
|
+
{
|
|
727
|
+
onToken: (token) => {
|
|
728
|
+
dispatch({ type: "SET_STREAMING_TEXT", text: (streamingText || "") + token });
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
tuiContext
|
|
732
|
+
);
|
|
733
|
+
} else {
|
|
734
|
+
result = await startPlannerSession(
|
|
735
|
+
text,
|
|
736
|
+
config,
|
|
737
|
+
{
|
|
738
|
+
onToken: (token) => {
|
|
739
|
+
dispatch({ type: "SET_STREAMING_TEXT", text: (streamingText || "") + token });
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
tuiContext
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
plannerSessionRef.current = result.session;
|
|
746
|
+
dispatch({ type: "SET_STREAMING_TEXT", text: "" });
|
|
747
|
+
switch (result.turn.type) {
|
|
748
|
+
case "question":
|
|
749
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "assistant", text: result.turn.content } });
|
|
750
|
+
dispatch({ type: "SET_GENERATING", value: false });
|
|
751
|
+
break;
|
|
752
|
+
case "plan":
|
|
753
|
+
if (result.turn.workflow) {
|
|
754
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "plan-ready", workflowName: result.turn.workflow.name } });
|
|
755
|
+
dispatch({ type: "SET_GENERATING", value: false });
|
|
756
|
+
dispatch({ type: "SHOW_PLAN", workflow: result.turn.workflow });
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
case "text":
|
|
760
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "assistant", text: result.turn.content } });
|
|
761
|
+
dispatch({ type: "SET_GENERATING", value: false });
|
|
762
|
+
break;
|
|
763
|
+
case "error":
|
|
764
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "error", text: `Error: ${result.turn.content}` } });
|
|
765
|
+
dispatch({ type: "SET_GENERATING", value: false });
|
|
766
|
+
plannerSessionRef.current = null;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
} catch (err) {
|
|
770
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
771
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "error", text: `Error: ${msg}` } });
|
|
772
|
+
dispatch({ type: "SET_GENERATING", value: false });
|
|
773
|
+
dispatch({ type: "SET_STREAMING_TEXT", text: "" });
|
|
774
|
+
plannerSessionRef.current = null;
|
|
775
|
+
logger.error({ err }, "Planner session failed");
|
|
776
|
+
}
|
|
777
|
+
}, [config, streamingText]);
|
|
778
|
+
const handleCancelGeneration = useCallback3(() => {
|
|
779
|
+
if (plannerSessionRef.current) {
|
|
780
|
+
cancelPlannerSession(plannerSessionRef.current);
|
|
781
|
+
plannerSessionRef.current = null;
|
|
782
|
+
}
|
|
783
|
+
dispatch({ type: "SET_STREAMING_TEXT", text: "" });
|
|
784
|
+
dispatch({ type: "SET_GENERATING", value: false });
|
|
785
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: "Generation cancelled." } });
|
|
786
|
+
}, []);
|
|
787
|
+
const isConversing = plannerSessionRef.current?.status === "conversing";
|
|
788
|
+
return { plannerSessionRef, handleUserMessage, handleCancelGeneration, isConversing };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/tui/hooks/use-workflow-execution.ts
|
|
792
|
+
import { useCallback as useCallback4, useRef as useRef4, useState as useState3 } from "react";
|
|
793
|
+
function useWorkflowExecution(workflow, db, cwd, bridgeRef, plannerSessionRef, dispatch) {
|
|
794
|
+
const abortRef = useRef4(null);
|
|
795
|
+
const abortMapRef = useRef4(/* @__PURE__ */ new Map());
|
|
796
|
+
const [isExecuting, setIsExecuting] = useState3(false);
|
|
797
|
+
const handleConfirm = useCallback4(async () => {
|
|
798
|
+
if (!workflow) return;
|
|
799
|
+
const confirmed = confirmPlan(workflow);
|
|
800
|
+
try {
|
|
801
|
+
upsertWorkflow(db, confirmed);
|
|
802
|
+
} catch (err) {
|
|
803
|
+
logger.error({ err }, "Failed to persist workflow");
|
|
804
|
+
}
|
|
805
|
+
const controller = new AbortController();
|
|
806
|
+
abortRef.current = controller;
|
|
807
|
+
abortMapRef.current.set(confirmed.id, controller);
|
|
808
|
+
setIsExecuting(true);
|
|
809
|
+
if (plannerSessionRef.current) {
|
|
810
|
+
plannerSessionRef.current = null;
|
|
811
|
+
}
|
|
812
|
+
dispatch({ type: "SHOW_EXECUTION", workflow: confirmed });
|
|
813
|
+
try {
|
|
814
|
+
const result = await executeWorkflow({
|
|
815
|
+
workflow: confirmed,
|
|
816
|
+
triggerData: null,
|
|
817
|
+
db,
|
|
818
|
+
cwd,
|
|
819
|
+
signal: controller.signal,
|
|
820
|
+
onProgress: (stepId, msg) => {
|
|
821
|
+
if (typeof msg === "object" && msg !== null && "status" in msg) {
|
|
822
|
+
dispatch({ type: "UPDATE_STEP", stepId, progress: { stepId, status: msg.status } });
|
|
823
|
+
} else {
|
|
824
|
+
dispatch({ type: "UPDATE_STEP", stepId, progress: { stepId, status: "running" } });
|
|
825
|
+
if (typeof msg === "string") {
|
|
826
|
+
dispatch({ type: "ADD_OUTPUT", line: msg });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
for (const [stepId, stepResult] of result.results) {
|
|
832
|
+
dispatch({ type: "UPDATE_STEP", stepId, progress: { stepId, status: stepResult.status } });
|
|
833
|
+
}
|
|
834
|
+
if (result.status === "failed") {
|
|
835
|
+
const failedSteps = [...result.results.entries()].filter(([, r]) => r.status === "failed" && r.error).map(([id, r]) => `${id}: ${r.error}`);
|
|
836
|
+
const errorDetail = failedSteps.length > 0 ? `
|
|
837
|
+
${failedSteps.join("\n")}` : "";
|
|
838
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "error", text: `Workflow execution failed.${errorDetail}` } });
|
|
839
|
+
} else {
|
|
840
|
+
const trigger = confirmed.trigger;
|
|
841
|
+
if (trigger.type === "poll") {
|
|
842
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: `Workflow completed first run. It will run every ${trigger.interval_seconds}s.` } });
|
|
843
|
+
const bridge = bridgeRef.current;
|
|
844
|
+
if (bridge?.triggerLoop && !bridge.isExternal) {
|
|
845
|
+
bridge.triggerLoop.registerTrigger(confirmed);
|
|
846
|
+
}
|
|
847
|
+
} else if (trigger.type === "cron") {
|
|
848
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: `Workflow completed first run. Scheduled: ${trigger.expression}` } });
|
|
849
|
+
const bridge = bridgeRef.current;
|
|
850
|
+
if (bridge?.triggerLoop && !bridge.isExternal) {
|
|
851
|
+
bridge.triggerLoop.registerTrigger(confirmed);
|
|
852
|
+
}
|
|
853
|
+
} else {
|
|
854
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: "Workflow execution completed." } });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
} catch (err) {
|
|
858
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
859
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "error", text: `Execution failed: ${msg}` } });
|
|
860
|
+
logger.error({ err }, "Workflow execution failed");
|
|
861
|
+
} finally {
|
|
862
|
+
abortMapRef.current.delete(confirmed.id);
|
|
863
|
+
setIsExecuting(abortMapRef.current.size > 0);
|
|
864
|
+
}
|
|
865
|
+
}, [workflow, db, cwd]);
|
|
866
|
+
const handleExecutionAbort = useCallback4(() => {
|
|
867
|
+
abortRef.current?.abort();
|
|
868
|
+
}, []);
|
|
869
|
+
const handleModify = useCallback4(() => {
|
|
870
|
+
dispatch({ type: "SHOW_CHAT" });
|
|
871
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "assistant", text: "Describe your modifications:" } });
|
|
872
|
+
}, []);
|
|
873
|
+
const handleCancel = useCallback4(() => {
|
|
874
|
+
if (workflow) {
|
|
875
|
+
const rejected = rejectPlan(workflow);
|
|
876
|
+
updateWorkflowPhase(db, workflow.id, rejected.phase);
|
|
877
|
+
}
|
|
878
|
+
if (plannerSessionRef.current) {
|
|
879
|
+
plannerSessionRef.current = null;
|
|
880
|
+
}
|
|
881
|
+
dispatch({ type: "SHOW_CHAT" });
|
|
882
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "assistant", text: "Plan cancelled." } });
|
|
883
|
+
}, [workflow, db]);
|
|
884
|
+
const handleExecutionBack = useCallback4(() => {
|
|
885
|
+
dispatch({ type: "SHOW_CHAT" });
|
|
886
|
+
}, []);
|
|
887
|
+
return {
|
|
888
|
+
isExecuting,
|
|
889
|
+
abortMapRef,
|
|
890
|
+
handleConfirm,
|
|
891
|
+
handleModify,
|
|
892
|
+
handleCancel,
|
|
893
|
+
handleExecutionAbort,
|
|
894
|
+
handleExecutionBack
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/tui/hooks/use-global-keypress.ts
|
|
899
|
+
import React3, { useCallback as useCallback5 } from "react";
|
|
900
|
+
|
|
901
|
+
// src/tui/key-bindings.ts
|
|
902
|
+
var keyBindings = {
|
|
903
|
+
ctrlC: ((input, key) => input === "c" && key.ctrl),
|
|
904
|
+
ctrlD: ((input, key) => input === "d" && key.ctrl),
|
|
905
|
+
escape: ((_input, key) => key.escape),
|
|
906
|
+
submit: ((_input, key) => key.return),
|
|
907
|
+
scrollUp: ((input, key) => key.ctrl && input === "p"),
|
|
908
|
+
scrollDown: ((input, key) => key.ctrl && input === "n"),
|
|
909
|
+
confirmPlan: ((input) => input.toLowerCase() === "y"),
|
|
910
|
+
modifyPlan: ((input) => input.toLowerCase() === "m"),
|
|
911
|
+
cancelPlan: ((input) => input.toLowerCase() === "n"),
|
|
912
|
+
abortExec: ((input) => input === "x"),
|
|
913
|
+
quit: ((input) => input === "q"),
|
|
914
|
+
upArrow: ((_input, key) => key.upArrow),
|
|
915
|
+
downArrow: ((_input, key) => key.downArrow),
|
|
916
|
+
stopWorkflow: ((input) => input === "s"),
|
|
917
|
+
deleteWorkflow: ((input) => input === "x"),
|
|
918
|
+
confirmYes: ((input) => input.toLowerCase() === "y"),
|
|
919
|
+
confirmNo: ((input, key) => input.toLowerCase() === "n" || key.escape)
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/tui/hooks/exit-helpers.ts
|
|
923
|
+
var sessionStartTime = null;
|
|
924
|
+
function markSessionStart() {
|
|
925
|
+
sessionStartTime = Date.now();
|
|
926
|
+
}
|
|
927
|
+
function formatDuration(ms) {
|
|
928
|
+
const totalSeconds = Math.floor(ms / 1e3);
|
|
929
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
930
|
+
const seconds = totalSeconds % 60;
|
|
931
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
932
|
+
return `${seconds}s`;
|
|
933
|
+
}
|
|
934
|
+
function printFarewell() {
|
|
935
|
+
if (sessionStartTime !== null) {
|
|
936
|
+
const duration = formatDuration(Date.now() - sessionStartTime);
|
|
937
|
+
process.stdout.write(`
|
|
938
|
+
Goodbye! Session: ${duration}
|
|
939
|
+
`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function stopBridgeAndExit(bridgeRef, exit) {
|
|
943
|
+
const bridge = bridgeRef.current;
|
|
944
|
+
if (bridge && !bridge.isExternal) {
|
|
945
|
+
stopDaemonBridge(bridge).finally(() => {
|
|
946
|
+
printFarewell();
|
|
947
|
+
exit();
|
|
948
|
+
process.exit(0);
|
|
949
|
+
});
|
|
950
|
+
} else {
|
|
951
|
+
printFarewell();
|
|
952
|
+
exit();
|
|
953
|
+
process.exit(0);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function handleExit(options) {
|
|
957
|
+
const { bridgeRef, exit, showDialog, dismissDialog, isExecuting } = options;
|
|
958
|
+
const daemonRunning = isDaemonRunning();
|
|
959
|
+
if (daemonRunning) {
|
|
960
|
+
showDialog({
|
|
961
|
+
title: "Exit CueClaw",
|
|
962
|
+
message: isExecuting ? "A workflow is running and will be cancelled. A background daemon is also running." : "A background daemon is running with your bot channels.",
|
|
963
|
+
actions: [
|
|
964
|
+
{ key: "k", label: "Keep daemon running", handler: () => {
|
|
965
|
+
dismissDialog();
|
|
966
|
+
stopBridgeAndExit(bridgeRef, exit);
|
|
967
|
+
} },
|
|
968
|
+
{ key: "s", label: "Stop daemon & exit", handler: () => {
|
|
969
|
+
dismissDialog();
|
|
970
|
+
stopExternalDaemon();
|
|
971
|
+
stopBridgeAndExit(bridgeRef, exit);
|
|
972
|
+
} }
|
|
973
|
+
]
|
|
974
|
+
});
|
|
975
|
+
} else {
|
|
976
|
+
stopBridgeAndExit(bridgeRef, exit);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/tui/renderers.tsx
|
|
981
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
982
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
983
|
+
function phaseColor(phase) {
|
|
984
|
+
switch (phase) {
|
|
985
|
+
case "executing":
|
|
986
|
+
return "yellow";
|
|
987
|
+
case "active":
|
|
988
|
+
return "green";
|
|
989
|
+
case "completed":
|
|
990
|
+
return "green";
|
|
991
|
+
case "failed":
|
|
992
|
+
return "red";
|
|
993
|
+
case "paused":
|
|
994
|
+
return "gray";
|
|
995
|
+
default:
|
|
996
|
+
return "white";
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function WorkflowTable({ workflows }) {
|
|
1000
|
+
if (workflows.length === 0) {
|
|
1001
|
+
return /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "No workflows found." });
|
|
1002
|
+
}
|
|
1003
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1004
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1005
|
+
/* @__PURE__ */ jsx3(Box2, { width: 14, children: /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "ID" }) }),
|
|
1006
|
+
/* @__PURE__ */ jsx3(Box2, { width: 28, children: /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "Name" }) }),
|
|
1007
|
+
/* @__PURE__ */ jsx3(Box2, { width: 14, children: /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "Phase" }) }),
|
|
1008
|
+
/* @__PURE__ */ jsx3(Box2, { width: 16, children: /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "Trigger" }) })
|
|
1009
|
+
] }),
|
|
1010
|
+
workflows.map((wf) => {
|
|
1011
|
+
const trigger = wf.trigger.type === "poll" ? `poll (${wf.trigger.interval_seconds}s)` : wf.trigger.type === "cron" ? `cron` : "manual";
|
|
1012
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1013
|
+
/* @__PURE__ */ jsx3(Box2, { width: 14, children: /* @__PURE__ */ jsx3(Text2, { children: wf.id.slice(0, 12) }) }),
|
|
1014
|
+
/* @__PURE__ */ jsx3(Box2, { width: 28, children: /* @__PURE__ */ jsx3(Text2, { children: wf.name.slice(0, 26) }) }),
|
|
1015
|
+
/* @__PURE__ */ jsx3(Box2, { width: 14, children: /* @__PURE__ */ jsx3(Text2, { color: phaseColor(wf.phase), children: wf.phase }) }),
|
|
1016
|
+
/* @__PURE__ */ jsx3(Box2, { width: 16, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: trigger }) })
|
|
1017
|
+
] }, wf.id);
|
|
1018
|
+
}),
|
|
1019
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1020
|
+
"\n",
|
|
1021
|
+
"Use /status ",
|
|
1022
|
+
"<id>",
|
|
1023
|
+
" to view details, /pause /resume /delete to manage."
|
|
1024
|
+
] })
|
|
1025
|
+
] });
|
|
1026
|
+
}
|
|
1027
|
+
function WorkflowDetail({ workflow, latestRun, stepRuns }) {
|
|
1028
|
+
const trigger = workflow.trigger.type === "poll" ? `poll (${workflow.trigger.interval_seconds}s)` : workflow.trigger.type === "cron" ? `cron (${workflow.trigger.expression})` : "manual";
|
|
1029
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1030
|
+
/* @__PURE__ */ jsx3(Text2, { bold: true, children: workflow.name }),
|
|
1031
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1032
|
+
"ID: ",
|
|
1033
|
+
workflow.id
|
|
1034
|
+
] }),
|
|
1035
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1036
|
+
"Phase: ",
|
|
1037
|
+
/* @__PURE__ */ jsx3(Text2, { color: phaseColor(workflow.phase), children: workflow.phase })
|
|
1038
|
+
] }),
|
|
1039
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1040
|
+
"Trigger: ",
|
|
1041
|
+
trigger
|
|
1042
|
+
] }),
|
|
1043
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1044
|
+
"Created: ",
|
|
1045
|
+
workflow.created_at
|
|
1046
|
+
] }),
|
|
1047
|
+
/* @__PURE__ */ jsx3(Text2, { children: "" }),
|
|
1048
|
+
/* @__PURE__ */ jsx3(Text2, { bold: true, children: "Steps:" }),
|
|
1049
|
+
workflow.steps.map((step, i) => {
|
|
1050
|
+
const deps = step.depends_on.length > 0 ? ` (after: ${step.depends_on.join(", ")})` : "";
|
|
1051
|
+
return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1052
|
+
" ",
|
|
1053
|
+
i + 1,
|
|
1054
|
+
". ",
|
|
1055
|
+
step.id,
|
|
1056
|
+
": ",
|
|
1057
|
+
step.description.slice(0, 60),
|
|
1058
|
+
deps
|
|
1059
|
+
] }, step.id);
|
|
1060
|
+
}),
|
|
1061
|
+
latestRun && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1062
|
+
/* @__PURE__ */ jsx3(Text2, { children: "" }),
|
|
1063
|
+
/* @__PURE__ */ jsx3(Text2, { bold: true, children: "Latest Run:" }),
|
|
1064
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1065
|
+
" Status: ",
|
|
1066
|
+
latestRun.status
|
|
1067
|
+
] }),
|
|
1068
|
+
latestRun.error && /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
|
|
1069
|
+
" Error: ",
|
|
1070
|
+
latestRun.error
|
|
1071
|
+
] }),
|
|
1072
|
+
stepRuns && stepRuns.length > 0 && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1073
|
+
/* @__PURE__ */ jsx3(Text2, { dimColor: true, children: " Step results:" }),
|
|
1074
|
+
stepRuns.map((sr) => /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1075
|
+
" ",
|
|
1076
|
+
sr.step_id,
|
|
1077
|
+
": ",
|
|
1078
|
+
sr.status,
|
|
1079
|
+
sr.output_json ? ` \u2014 ${sr.output_json.slice(0, 50)}` : ""
|
|
1080
|
+
] }, sr.id))
|
|
1081
|
+
] })
|
|
1082
|
+
] })
|
|
1083
|
+
] });
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/tui/hooks/use-global-keypress.ts
|
|
1087
|
+
function useGlobalKeypress({
|
|
1088
|
+
isExecuting,
|
|
1089
|
+
bridgeRef,
|
|
1090
|
+
abortMapRef,
|
|
1091
|
+
db,
|
|
1092
|
+
view,
|
|
1093
|
+
exit,
|
|
1094
|
+
showDialog,
|
|
1095
|
+
dismissDialog,
|
|
1096
|
+
dispatch
|
|
1097
|
+
}) {
|
|
1098
|
+
useKeypress("global-ctrl-c", KeyPriority.High, useCallback5((input, key) => {
|
|
1099
|
+
if (keyBindings.ctrlC(input, key)) {
|
|
1100
|
+
for (const controller of abortMapRef.current.values()) {
|
|
1101
|
+
controller.abort();
|
|
1102
|
+
}
|
|
1103
|
+
handleExit({ bridgeRef, exit, showDialog, dismissDialog, isExecuting });
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
return false;
|
|
1107
|
+
}, [isExecuting, exit, showDialog, dismissDialog]));
|
|
1108
|
+
useKeypress("global-ctrl-d", KeyPriority.Normal, useCallback5((input, key) => {
|
|
1109
|
+
if (keyBindings.ctrlD(input, key)) {
|
|
1110
|
+
const workflows = listWorkflows(db);
|
|
1111
|
+
dispatch({
|
|
1112
|
+
type: "ADD_MESSAGE",
|
|
1113
|
+
message: {
|
|
1114
|
+
type: "assistant-jsx",
|
|
1115
|
+
content: React3.createElement(WorkflowTable, { workflows })
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
return false;
|
|
1121
|
+
}, [db]), view !== "onboarding");
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/tui/hooks/use-command-dispatch.ts
|
|
1125
|
+
import React4, { useCallback as useCallback6, useMemo } from "react";
|
|
1126
|
+
|
|
1127
|
+
// src/tui/commands/registry.ts
|
|
1128
|
+
var commands = [];
|
|
1129
|
+
function registerCommand(cmd) {
|
|
1130
|
+
commands.push(cmd);
|
|
1131
|
+
}
|
|
1132
|
+
function getCommands() {
|
|
1133
|
+
return commands;
|
|
1134
|
+
}
|
|
1135
|
+
function findCommand(name) {
|
|
1136
|
+
const lower = name.toLowerCase();
|
|
1137
|
+
return commands.find((c) => c.name === lower || c.aliases.includes(lower));
|
|
1138
|
+
}
|
|
1139
|
+
function parseSlashCommand(input) {
|
|
1140
|
+
const trimmed = input.trim();
|
|
1141
|
+
if (!trimmed.startsWith("/")) return null;
|
|
1142
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
1143
|
+
if (spaceIdx === -1) {
|
|
1144
|
+
return { name: trimmed.slice(1), args: "" };
|
|
1145
|
+
}
|
|
1146
|
+
return { name: trimmed.slice(1, spaceIdx), args: trimmed.slice(spaceIdx + 1).trim() };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/tui/commands/help.ts
|
|
1150
|
+
registerCommand({
|
|
1151
|
+
name: "help",
|
|
1152
|
+
aliases: ["h", "?"],
|
|
1153
|
+
description: "Show available commands",
|
|
1154
|
+
usage: "/help",
|
|
1155
|
+
execute(_args, ctx) {
|
|
1156
|
+
const commands2 = getCommands();
|
|
1157
|
+
const lines = commands2.map((c) => {
|
|
1158
|
+
const aliases = c.aliases.length > 0 ? ` (${c.aliases.map((a) => "/" + a).join(", ")})` : "";
|
|
1159
|
+
return ` /${c.name}${aliases} \u2014 ${c.description}`;
|
|
1160
|
+
});
|
|
1161
|
+
ctx.addMessage({ type: "assistant", text: "Available commands:\n" + lines.join("\n") });
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
// src/tui/commands/list.ts
|
|
1166
|
+
registerCommand({
|
|
1167
|
+
name: "list",
|
|
1168
|
+
aliases: ["ls"],
|
|
1169
|
+
description: "List all workflows",
|
|
1170
|
+
usage: "/list",
|
|
1171
|
+
// Handled in use-command-dispatch.ts
|
|
1172
|
+
execute() {
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// src/tui/commands/status.ts
|
|
1177
|
+
registerCommand({
|
|
1178
|
+
name: "status",
|
|
1179
|
+
aliases: ["st"],
|
|
1180
|
+
description: "View workflow status",
|
|
1181
|
+
usage: "/status [id]",
|
|
1182
|
+
// Handled in use-command-dispatch.ts
|
|
1183
|
+
execute() {
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// src/tui/commands/pause.ts
|
|
1188
|
+
registerCommand({
|
|
1189
|
+
name: "pause",
|
|
1190
|
+
aliases: [],
|
|
1191
|
+
description: "Pause a workflow",
|
|
1192
|
+
usage: "/pause <id>",
|
|
1193
|
+
execute(args, ctx) {
|
|
1194
|
+
if (!args) {
|
|
1195
|
+
ctx.addMessage({ type: "assistant", text: "Usage: /pause <workflow-id>" });
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const wf = getWorkflow(ctx.db, args) ?? listWorkflows(ctx.db).find((w) => w.id.startsWith(args));
|
|
1199
|
+
if (!wf) {
|
|
1200
|
+
ctx.addMessage({ type: "assistant", text: `Workflow not found: ${args}` });
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (wf.phase !== "active") {
|
|
1204
|
+
ctx.addMessage({ type: "assistant", text: `Cannot pause workflow in phase "${wf.phase}" (must be "active")` });
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
updateWorkflowPhase(ctx.db, wf.id, "paused");
|
|
1208
|
+
ctx.addMessage({ type: "assistant", text: `Paused workflow "${wf.name}" (${wf.id})` });
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
// src/tui/commands/resume.ts
|
|
1213
|
+
registerCommand({
|
|
1214
|
+
name: "resume",
|
|
1215
|
+
aliases: [],
|
|
1216
|
+
description: "Resume a paused workflow",
|
|
1217
|
+
usage: "/resume <id>",
|
|
1218
|
+
execute(args, ctx) {
|
|
1219
|
+
if (!args) {
|
|
1220
|
+
ctx.addMessage({ type: "assistant", text: "Usage: /resume <workflow-id>" });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const wf = getWorkflow(ctx.db, args) ?? listWorkflows(ctx.db).find((w) => w.id.startsWith(args));
|
|
1224
|
+
if (!wf) {
|
|
1225
|
+
ctx.addMessage({ type: "assistant", text: `Workflow not found: ${args}` });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (wf.phase !== "paused") {
|
|
1229
|
+
ctx.addMessage({ type: "assistant", text: `Cannot resume workflow in phase "${wf.phase}" (must be "paused")` });
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const nextPhase = wf.trigger.type === "manual" ? "executing" : "active";
|
|
1233
|
+
updateWorkflowPhase(ctx.db, wf.id, nextPhase);
|
|
1234
|
+
ctx.addMessage({ type: "assistant", text: `Resumed workflow "${wf.name}" \u2014 phase: ${nextPhase}` });
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
// src/tui/commands/delete.ts
|
|
1239
|
+
registerCommand({
|
|
1240
|
+
name: "delete",
|
|
1241
|
+
aliases: ["rm"],
|
|
1242
|
+
description: "Delete a workflow",
|
|
1243
|
+
usage: "/delete <id>",
|
|
1244
|
+
execute(args, ctx) {
|
|
1245
|
+
if (!args) {
|
|
1246
|
+
ctx.addMessage({ type: "assistant", text: "Usage: /delete <workflow-id>" });
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const wf = getWorkflow(ctx.db, args) ?? listWorkflows(ctx.db).find((w) => w.id.startsWith(args));
|
|
1250
|
+
if (!wf) {
|
|
1251
|
+
ctx.addMessage({ type: "assistant", text: `Workflow not found: ${args}` });
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (wf.phase === "executing") {
|
|
1255
|
+
ctx.addMessage({ type: "assistant", text: "Cannot delete workflow while it is executing." });
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
deleteWorkflow(ctx.db, wf.id);
|
|
1259
|
+
ctx.addMessage({ type: "assistant", text: `Deleted workflow "${wf.name}" (${wf.id})` });
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
// src/tui/commands/config.ts
|
|
1264
|
+
registerCommand({
|
|
1265
|
+
name: "config",
|
|
1266
|
+
aliases: ["cfg"],
|
|
1267
|
+
description: "View or set configuration",
|
|
1268
|
+
usage: "/config get [key] | /config set <key> <value>",
|
|
1269
|
+
execute(args, ctx) {
|
|
1270
|
+
const parts = args.split(/\s+/);
|
|
1271
|
+
const subcommand = parts[0]?.toLowerCase();
|
|
1272
|
+
if (!subcommand || subcommand === "get") {
|
|
1273
|
+
const key = parts[1];
|
|
1274
|
+
try {
|
|
1275
|
+
const config = loadConfig();
|
|
1276
|
+
if (!key) {
|
|
1277
|
+
ctx.addMessage({ type: "assistant", text: "Configuration:\n" + JSON.stringify(config, null, 2) });
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const keyParts = key.split(".");
|
|
1281
|
+
let value = config;
|
|
1282
|
+
for (const p of keyParts) {
|
|
1283
|
+
if (value === null || value === void 0) break;
|
|
1284
|
+
value = value[p];
|
|
1285
|
+
}
|
|
1286
|
+
ctx.addMessage({ type: "assistant", text: value !== void 0 ? `${key} = ${JSON.stringify(value, null, 2)}` : `Key not found: ${key}` });
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
ctx.addMessage({ type: "error", text: `Error loading config: ${err instanceof Error ? err.message : String(err)}` });
|
|
1289
|
+
}
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (subcommand === "set") {
|
|
1293
|
+
const key = parts[1];
|
|
1294
|
+
const value = parts.slice(2).join(" ");
|
|
1295
|
+
if (!key || !value) {
|
|
1296
|
+
ctx.addMessage({ type: "assistant", text: "Usage: /config set <key> <value>" });
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
let parsed = value;
|
|
1301
|
+
if (value === "true") parsed = true;
|
|
1302
|
+
else if (value === "false") parsed = false;
|
|
1303
|
+
else if (/^\d+$/.test(value)) parsed = Number(value);
|
|
1304
|
+
const keyParts = key.split(".");
|
|
1305
|
+
const update = {};
|
|
1306
|
+
let target = update;
|
|
1307
|
+
for (let i = 0; i < keyParts.length - 1; i++) {
|
|
1308
|
+
target[keyParts[i]] = {};
|
|
1309
|
+
target = target[keyParts[i]];
|
|
1310
|
+
}
|
|
1311
|
+
target[keyParts[keyParts.length - 1]] = parsed;
|
|
1312
|
+
writeConfig(update);
|
|
1313
|
+
const newConfig = loadConfig();
|
|
1314
|
+
ctx.setConfig(newConfig);
|
|
1315
|
+
ctx.addMessage({ type: "assistant", text: `Set ${key} = ${JSON.stringify(parsed)}` });
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
ctx.addMessage({ type: "error", text: `Error setting config: ${err instanceof Error ? err.message : String(err)}` });
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
ctx.addMessage({ type: "assistant", text: "Usage: /config get [key] | /config set <key> <value>" });
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
// src/tui/commands/daemon.ts
|
|
1326
|
+
registerCommand({
|
|
1327
|
+
name: "daemon",
|
|
1328
|
+
aliases: [],
|
|
1329
|
+
description: "View daemon status",
|
|
1330
|
+
usage: "/daemon status|start|stop",
|
|
1331
|
+
execute(args, ctx) {
|
|
1332
|
+
const subcommand = args.trim().toLowerCase() || "status";
|
|
1333
|
+
if (subcommand === "status") {
|
|
1334
|
+
const status = getServiceStatus();
|
|
1335
|
+
const bridgeStatus = ctx.bridge ? ctx.bridge.isExternal ? "external service" : "in-process" : "not connected";
|
|
1336
|
+
ctx.addMessage({ type: "assistant", text: `Daemon status: ${status}
|
|
1337
|
+
Bridge: ${bridgeStatus}` });
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (subcommand === "start" || subcommand === "stop") {
|
|
1341
|
+
ctx.addMessage({ type: "assistant", text: `Use the CLI for daemon ${subcommand}: cueclaw daemon ${subcommand}` });
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
ctx.addMessage({ type: "assistant", text: "Usage: /daemon status|start|stop" });
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// src/tui/version.ts
|
|
1349
|
+
import { createRequire } from "module";
|
|
1350
|
+
import { fileURLToPath } from "url";
|
|
1351
|
+
var require2 = createRequire(import.meta.url);
|
|
1352
|
+
var pkg = require2("../../package.json");
|
|
1353
|
+
var isDev2 = !fileURLToPath(import.meta.url).includes("/dist/");
|
|
1354
|
+
var appVersion = isDev2 ? "dev" : pkg.version;
|
|
1355
|
+
|
|
1356
|
+
// src/tui/commands/info.ts
|
|
1357
|
+
registerCommand({
|
|
1358
|
+
name: "info",
|
|
1359
|
+
aliases: [],
|
|
1360
|
+
description: "Show system information",
|
|
1361
|
+
usage: "/info",
|
|
1362
|
+
execute(_args, ctx) {
|
|
1363
|
+
const config = ctx.config;
|
|
1364
|
+
const lines = [
|
|
1365
|
+
`CueClaw ${appVersion === "dev" ? "dev" : `v${appVersion}`}`,
|
|
1366
|
+
`Working directory: ${ctx.cwd}`,
|
|
1367
|
+
`Config directory: ${cueclawHome()}`,
|
|
1368
|
+
""
|
|
1369
|
+
];
|
|
1370
|
+
if (config) {
|
|
1371
|
+
lines.push(`Planner model: ${config.claude.planner.model}`);
|
|
1372
|
+
lines.push(`Executor model: ${config.claude.executor.model}`);
|
|
1373
|
+
lines.push(`Base URL: ${config.claude.base_url}`);
|
|
1374
|
+
if (config.telegram?.enabled) lines.push("Telegram: enabled");
|
|
1375
|
+
if (config.whatsapp?.enabled) lines.push("WhatsApp: enabled");
|
|
1376
|
+
if (config.container?.enabled) lines.push("Container isolation: enabled");
|
|
1377
|
+
}
|
|
1378
|
+
ctx.addMessage({ type: "assistant", text: lines.join("\n") });
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
// src/tui/commands/clear.ts
|
|
1383
|
+
registerCommand({
|
|
1384
|
+
name: "clear",
|
|
1385
|
+
aliases: ["cls"],
|
|
1386
|
+
description: "Clear chat messages",
|
|
1387
|
+
usage: "/clear",
|
|
1388
|
+
// Handled in use-command-dispatch.ts
|
|
1389
|
+
execute() {
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// src/tui/commands/new.ts
|
|
1394
|
+
registerCommand({
|
|
1395
|
+
name: "new",
|
|
1396
|
+
aliases: [],
|
|
1397
|
+
description: "Generate a plan directly (skip conversation)",
|
|
1398
|
+
usage: "/new <description>",
|
|
1399
|
+
// Handled in use-command-dispatch.ts
|
|
1400
|
+
execute() {
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
// src/tui/commands/cancel.ts
|
|
1405
|
+
registerCommand({
|
|
1406
|
+
name: "cancel",
|
|
1407
|
+
aliases: [],
|
|
1408
|
+
description: "Cancel current conversation",
|
|
1409
|
+
usage: "/cancel",
|
|
1410
|
+
// Handled in use-command-dispatch.ts
|
|
1411
|
+
execute() {
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// src/tui/commands/bot.ts
|
|
1416
|
+
registerCommand({
|
|
1417
|
+
name: "bot",
|
|
1418
|
+
aliases: [],
|
|
1419
|
+
description: "Manage bot channels",
|
|
1420
|
+
usage: "/bot start|status",
|
|
1421
|
+
// Handled in use-command-dispatch.ts
|
|
1422
|
+
execute() {
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
// src/tui/commands/setup.ts
|
|
1427
|
+
registerCommand({
|
|
1428
|
+
name: "setup",
|
|
1429
|
+
aliases: [],
|
|
1430
|
+
description: "Re-run configuration setup",
|
|
1431
|
+
usage: "/setup",
|
|
1432
|
+
// Handled in use-command-dispatch.ts
|
|
1433
|
+
execute() {
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
// src/tui/commands/theme.ts
|
|
1438
|
+
registerCommand({
|
|
1439
|
+
name: "theme",
|
|
1440
|
+
aliases: [],
|
|
1441
|
+
description: "Switch color theme",
|
|
1442
|
+
usage: "/theme [dark|light|dracula]",
|
|
1443
|
+
completion: ["dark", "light", "dracula"],
|
|
1444
|
+
execute(args, ctx) {
|
|
1445
|
+
const name = args.trim().toLowerCase();
|
|
1446
|
+
if (!name) {
|
|
1447
|
+
const available = themeManager.getAvailableThemes().join(", ");
|
|
1448
|
+
const current = themeManager.getThemeName();
|
|
1449
|
+
ctx.addMessage({ type: "assistant", text: `Current theme: ${current}
|
|
1450
|
+
Available: ${available}` });
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const success = themeManager.setTheme(name);
|
|
1454
|
+
if (success) {
|
|
1455
|
+
ctx.setThemeVersion((v) => v + 1);
|
|
1456
|
+
ctx.addMessage({ type: "assistant", text: `Switched to ${name} theme.` });
|
|
1457
|
+
} else {
|
|
1458
|
+
const available = themeManager.getAvailableThemes().join(", ");
|
|
1459
|
+
ctx.addMessage({ type: "error", text: `Unknown theme: ${name}. Available: ${available}` });
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// src/tui/commands/quit.ts
|
|
1465
|
+
registerCommand({
|
|
1466
|
+
name: "quit",
|
|
1467
|
+
aliases: ["exit", "q"],
|
|
1468
|
+
description: "Exit CueClaw",
|
|
1469
|
+
usage: "/quit",
|
|
1470
|
+
execute() {
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// src/tui/hooks/use-command-dispatch.ts
|
|
1475
|
+
function useCommandDispatch({
|
|
1476
|
+
config,
|
|
1477
|
+
db,
|
|
1478
|
+
cwd,
|
|
1479
|
+
bridgeRef,
|
|
1480
|
+
plannerSessionRef,
|
|
1481
|
+
dispatch,
|
|
1482
|
+
setConfig,
|
|
1483
|
+
setThemeVersion,
|
|
1484
|
+
exit,
|
|
1485
|
+
showDialog,
|
|
1486
|
+
dismissDialog
|
|
1487
|
+
}) {
|
|
1488
|
+
const commandCtx = useMemo(() => ({
|
|
1489
|
+
db,
|
|
1490
|
+
config,
|
|
1491
|
+
cwd,
|
|
1492
|
+
bridge: bridgeRef.current,
|
|
1493
|
+
addMessage: (msg) => dispatch({ type: "ADD_MESSAGE", message: msg }),
|
|
1494
|
+
clearMessages: () => dispatch({ type: "SET_MESSAGES", messages: [] }),
|
|
1495
|
+
setConfig,
|
|
1496
|
+
setThemeVersion
|
|
1497
|
+
}), [db, config, cwd]);
|
|
1498
|
+
const handleSlashCommand = useCallback6(async (text) => {
|
|
1499
|
+
if (!config) return false;
|
|
1500
|
+
const parsed = parseSlashCommand(text);
|
|
1501
|
+
if (!parsed) return false;
|
|
1502
|
+
commandCtx.bridge = bridgeRef.current;
|
|
1503
|
+
commandCtx.config = config;
|
|
1504
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "user", text } });
|
|
1505
|
+
if (parsed.name === "cancel") {
|
|
1506
|
+
if (plannerSessionRef.current) {
|
|
1507
|
+
cancelPlannerSession(plannerSessionRef.current);
|
|
1508
|
+
plannerSessionRef.current = null;
|
|
1509
|
+
}
|
|
1510
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "assistant", text: "Conversation cancelled." } });
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
if (parsed.name === "new" && !parsed.args) {
|
|
1514
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "assistant", text: "Usage: /new <workflow description>" } });
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1517
|
+
if (parsed.name === "new" && parsed.args) {
|
|
1518
|
+
plannerSessionRef.current = null;
|
|
1519
|
+
dispatch({ type: "SET_GENERATING", value: true });
|
|
1520
|
+
dispatch({ type: "SET_STREAMING_TEXT", text: "" });
|
|
1521
|
+
try {
|
|
1522
|
+
const { generatePlan } = await import("./planner-MJ3XBCWH.js");
|
|
1523
|
+
const workflow = await generatePlan(parsed.args, config, { channel: "tui" });
|
|
1524
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "plan-ready", workflowName: workflow.name } });
|
|
1525
|
+
dispatch({ type: "SET_GENERATING", value: false });
|
|
1526
|
+
dispatch({ type: "SHOW_PLAN", workflow });
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1529
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "error", text: `Error: ${msg}` } });
|
|
1530
|
+
dispatch({ type: "SET_GENERATING", value: false });
|
|
1531
|
+
}
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
if (parsed.name === "list" || parsed.name === "ls" || (parsed.name === "status" || parsed.name === "st") && !parsed.args) {
|
|
1535
|
+
const workflows = listWorkflows(db);
|
|
1536
|
+
dispatch({ type: "SHOW_STATUS", workflows });
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
if ((parsed.name === "status" || parsed.name === "st") && parsed.args) {
|
|
1540
|
+
const wf = getWorkflow(db, parsed.args) ?? listWorkflows(db).find((w) => w.id.startsWith(parsed.args));
|
|
1541
|
+
if (wf) {
|
|
1542
|
+
const runs = getWorkflowRunsByWorkflowId(db, wf.id);
|
|
1543
|
+
const latestRun = runs[0];
|
|
1544
|
+
const stepRuns = latestRun ? getStepRunsByRunId(db, latestRun.id) : void 0;
|
|
1545
|
+
dispatch({
|
|
1546
|
+
type: "ADD_MESSAGE",
|
|
1547
|
+
message: {
|
|
1548
|
+
type: "assistant-jsx",
|
|
1549
|
+
content: React4.createElement(WorkflowDetail, { workflow: wf, latestRun, stepRuns })
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
return true;
|
|
1553
|
+
}
|
|
1554
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "assistant", text: `Workflow not found: ${parsed.args}` } });
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1557
|
+
if (parsed.name === "clear" || parsed.name === "cls") {
|
|
1558
|
+
dispatch({ type: "SET_MESSAGES", messages: [] });
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
if (parsed.name === "bot" && parsed.args.trim().toLowerCase() === "start") {
|
|
1562
|
+
const bridge = bridgeRef.current;
|
|
1563
|
+
if (bridge && config) {
|
|
1564
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: "Starting bot channels..." } });
|
|
1565
|
+
try {
|
|
1566
|
+
await startBotChannels(bridge, config);
|
|
1567
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: "Bot channels started." } });
|
|
1568
|
+
} catch (err) {
|
|
1569
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "error", text: `Failed to start bots: ${err instanceof Error ? err.message : String(err)}` } });
|
|
1570
|
+
}
|
|
1571
|
+
} else {
|
|
1572
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "error", text: "Daemon not running. Cannot start bots." } });
|
|
1573
|
+
}
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
if (parsed.name === "setup") {
|
|
1577
|
+
dispatch({ type: "SET_MESSAGES", messages: [] });
|
|
1578
|
+
dispatch({ type: "SHOW_ONBOARDING" });
|
|
1579
|
+
return true;
|
|
1580
|
+
}
|
|
1581
|
+
if (parsed.name === "quit" || parsed.name === "exit" || parsed.name === "q") {
|
|
1582
|
+
handleExit({ bridgeRef, exit, showDialog, dismissDialog });
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
const cmd = findCommand(parsed.name);
|
|
1586
|
+
if (cmd) {
|
|
1587
|
+
await cmd.execute(parsed.args, commandCtx);
|
|
1588
|
+
} else {
|
|
1589
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "assistant", text: `Unknown command: /${parsed.name}. Type /help for available commands.` } });
|
|
1590
|
+
}
|
|
1591
|
+
return true;
|
|
1592
|
+
}, [config, db, commandCtx]);
|
|
1593
|
+
return { handleSlashCommand, commandCtx };
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// src/tui/app-provider.tsx
|
|
1597
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1598
|
+
function appReducer(state, action) {
|
|
1599
|
+
switch (action.type) {
|
|
1600
|
+
case "SHOW_CHAT":
|
|
1601
|
+
return { ...state, view: "chat", isGenerating: false, streamingText: "" };
|
|
1602
|
+
case "SHOW_ONBOARDING":
|
|
1603
|
+
return { ...state, view: "onboarding", isGenerating: false, streamingText: "" };
|
|
1604
|
+
case "SHOW_PLAN":
|
|
1605
|
+
return { ...state, view: "plan", workflow: action.workflow, isGenerating: false, streamingText: "" };
|
|
1606
|
+
case "SHOW_EXECUTION":
|
|
1607
|
+
return { ...state, view: "execution", previousView: state.view, workflow: action.workflow, stepProgress: /* @__PURE__ */ new Map(), executionOutput: [], isGenerating: false, streamingText: "" };
|
|
1608
|
+
case "SHOW_STATUS":
|
|
1609
|
+
return { ...state, view: "status", statusWorkflows: action.workflows, isGenerating: false, streamingText: "" };
|
|
1610
|
+
case "SHOW_DETAIL":
|
|
1611
|
+
return { ...state, view: "detail", previousView: state.view, workflow: action.workflow, detailRuns: action.runs, detailStepRuns: action.stepRuns, isGenerating: false, streamingText: "" };
|
|
1612
|
+
case "ADD_MESSAGE":
|
|
1613
|
+
return { ...state, messages: [...state.messages, action.message] };
|
|
1614
|
+
case "SET_MESSAGES":
|
|
1615
|
+
return { ...state, messages: action.messages };
|
|
1616
|
+
case "SET_GENERATING":
|
|
1617
|
+
return { ...state, isGenerating: action.value };
|
|
1618
|
+
case "SET_STREAMING_TEXT":
|
|
1619
|
+
return { ...state, streamingText: action.text };
|
|
1620
|
+
case "UPDATE_STEP":
|
|
1621
|
+
return { ...state, stepProgress: new Map(state.stepProgress).set(action.stepId, action.progress) };
|
|
1622
|
+
case "ADD_OUTPUT":
|
|
1623
|
+
return { ...state, executionOutput: [...state.executionOutput, action.line] };
|
|
1624
|
+
default:
|
|
1625
|
+
return state;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function AppProvider({ cwd, skipOnboarding, children }) {
|
|
1629
|
+
const { exit } = useApp();
|
|
1630
|
+
const { showDialog, dismissDialog } = useDialog();
|
|
1631
|
+
const validation = useMemo2(() => validateConfig(), []);
|
|
1632
|
+
const needsSetup = !skipOnboarding && !validation.valid;
|
|
1633
|
+
const [config, setConfig] = useState4(() => {
|
|
1634
|
+
if (needsSetup) return null;
|
|
1635
|
+
try {
|
|
1636
|
+
return loadConfig();
|
|
1637
|
+
} catch {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
const db = useMemo2(() => initDb(), []);
|
|
1642
|
+
const [themeVersion, setThemeVersion] = useState4(0);
|
|
1643
|
+
const initialState = {
|
|
1644
|
+
view: needsSetup ? "onboarding" : "chat",
|
|
1645
|
+
previousView: null,
|
|
1646
|
+
messages: [],
|
|
1647
|
+
workflow: null,
|
|
1648
|
+
isGenerating: false,
|
|
1649
|
+
stepProgress: /* @__PURE__ */ new Map(),
|
|
1650
|
+
executionOutput: [],
|
|
1651
|
+
streamingText: "",
|
|
1652
|
+
statusWorkflows: [],
|
|
1653
|
+
detailRuns: [],
|
|
1654
|
+
detailStepRuns: []
|
|
1655
|
+
};
|
|
1656
|
+
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
1657
|
+
useEffect3(() => {
|
|
1658
|
+
markSessionStart();
|
|
1659
|
+
}, []);
|
|
1660
|
+
useEffect3(() => {
|
|
1661
|
+
return onLogLine((line) => {
|
|
1662
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: line } });
|
|
1663
|
+
});
|
|
1664
|
+
}, []);
|
|
1665
|
+
const { bridgeRef, daemonStatus } = useDaemonBridge(config, db, cwd, dispatch);
|
|
1666
|
+
const planner = usePlannerSession(config, dispatch, state.streamingText);
|
|
1667
|
+
const execution = useWorkflowExecution(
|
|
1668
|
+
state.workflow,
|
|
1669
|
+
db,
|
|
1670
|
+
cwd,
|
|
1671
|
+
bridgeRef,
|
|
1672
|
+
planner.plannerSessionRef,
|
|
1673
|
+
dispatch
|
|
1674
|
+
);
|
|
1675
|
+
const { handleSlashCommand } = useCommandDispatch({
|
|
1676
|
+
config,
|
|
1677
|
+
db,
|
|
1678
|
+
cwd,
|
|
1679
|
+
bridgeRef,
|
|
1680
|
+
plannerSessionRef: planner.plannerSessionRef,
|
|
1681
|
+
dispatch,
|
|
1682
|
+
setConfig,
|
|
1683
|
+
setThemeVersion,
|
|
1684
|
+
exit,
|
|
1685
|
+
showDialog,
|
|
1686
|
+
dismissDialog
|
|
1687
|
+
});
|
|
1688
|
+
useGlobalKeypress({
|
|
1689
|
+
isExecuting: execution.isExecuting,
|
|
1690
|
+
bridgeRef,
|
|
1691
|
+
abortMapRef: execution.abortMapRef,
|
|
1692
|
+
db,
|
|
1693
|
+
view: state.view,
|
|
1694
|
+
exit,
|
|
1695
|
+
showDialog,
|
|
1696
|
+
dismissDialog,
|
|
1697
|
+
dispatch
|
|
1698
|
+
});
|
|
1699
|
+
const handleOnboardingComplete = useCallback7((newConfig) => {
|
|
1700
|
+
setConfig(newConfig);
|
|
1701
|
+
dispatch({ type: "SHOW_CHAT" });
|
|
1702
|
+
}, []);
|
|
1703
|
+
const handleOnboardingCancel = useCallback7(() => {
|
|
1704
|
+
try {
|
|
1705
|
+
setConfig(loadConfig());
|
|
1706
|
+
} catch {
|
|
1707
|
+
}
|
|
1708
|
+
dispatch({ type: "SHOW_CHAT" });
|
|
1709
|
+
}, []);
|
|
1710
|
+
const handleStatusBack = useCallback7(() => {
|
|
1711
|
+
dispatch({ type: "SHOW_CHAT" });
|
|
1712
|
+
}, []);
|
|
1713
|
+
const handleStatusSelect = useCallback7((workflow) => {
|
|
1714
|
+
const runs = getWorkflowRunsByWorkflowId(db, workflow.id);
|
|
1715
|
+
const latestRun = runs[0];
|
|
1716
|
+
const stepRuns = latestRun ? getStepRunsByRunId(db, latestRun.id) : [];
|
|
1717
|
+
dispatch({ type: "SHOW_DETAIL", workflow, runs, stepRuns });
|
|
1718
|
+
}, [db]);
|
|
1719
|
+
const handleStatusStop = useCallback7((workflow) => {
|
|
1720
|
+
const controller = execution.abortMapRef.current.get(workflow.id);
|
|
1721
|
+
if (controller) {
|
|
1722
|
+
controller.abort();
|
|
1723
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "system", text: `Stopping workflow: ${workflow.name}` } });
|
|
1724
|
+
} else {
|
|
1725
|
+
dispatch({ type: "ADD_MESSAGE", message: { type: "warning", text: `Workflow "${workflow.name}" is not currently executing.` } });
|
|
1726
|
+
}
|
|
1727
|
+
const workflows = listWorkflows(db);
|
|
1728
|
+
dispatch({ type: "SHOW_STATUS", workflows });
|
|
1729
|
+
}, [db, execution.abortMapRef]);
|
|
1730
|
+
const handleStatusDelete = useCallback7((workflow) => {
|
|
1731
|
+
deleteWorkflow(db, workflow.id);
|
|
1732
|
+
const updated = listWorkflows(db);
|
|
1733
|
+
dispatch({ type: "SHOW_STATUS", workflows: updated });
|
|
1734
|
+
}, [db]);
|
|
1735
|
+
const handleDetailBack = useCallback7(() => {
|
|
1736
|
+
const workflows = listWorkflows(db);
|
|
1737
|
+
dispatch({ type: "SHOW_STATUS", workflows });
|
|
1738
|
+
}, [db]);
|
|
1739
|
+
const handleDetailSelectRun = useCallback7((runId) => {
|
|
1740
|
+
if (!state.workflow) return;
|
|
1741
|
+
dispatch({ type: "SHOW_EXECUTION", workflow: state.workflow });
|
|
1742
|
+
const stepRuns = getStepRunsByRunId(db, runId);
|
|
1743
|
+
for (const sr of stepRuns) {
|
|
1744
|
+
dispatch({
|
|
1745
|
+
type: "UPDATE_STEP",
|
|
1746
|
+
stepId: sr.step_id,
|
|
1747
|
+
progress: {
|
|
1748
|
+
stepId: sr.step_id,
|
|
1749
|
+
status: sr.status,
|
|
1750
|
+
duration: sr.duration_ms ?? void 0
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
}, [db, state.workflow]);
|
|
1755
|
+
const handleChatSubmit = useCallback7(async (text) => {
|
|
1756
|
+
if (!config) return;
|
|
1757
|
+
const wasCommand = await handleSlashCommand(text);
|
|
1758
|
+
if (!wasCommand) {
|
|
1759
|
+
await planner.handleUserMessage(text);
|
|
1760
|
+
}
|
|
1761
|
+
}, [config, handleSlashCommand, planner.handleUserMessage]);
|
|
1762
|
+
const footerExtra = daemonStatus === "external" ? " | Background service detected" : daemonStatus === "running" ? " | Daemon active" : "";
|
|
1763
|
+
const footerHints = planner.isConversing ? "Enter send \xB7 /cancel abort \xB7 /help commands" : void 0;
|
|
1764
|
+
const handleExecutionBackWithNav = useCallback7(() => {
|
|
1765
|
+
if (state.previousView === "detail" && state.workflow) {
|
|
1766
|
+
const runs = getWorkflowRunsByWorkflowId(db, state.workflow.id);
|
|
1767
|
+
const latestRun = runs[0];
|
|
1768
|
+
const stepRuns = latestRun ? getStepRunsByRunId(db, latestRun.id) : [];
|
|
1769
|
+
dispatch({ type: "SHOW_DETAIL", workflow: state.workflow, runs, stepRuns });
|
|
1770
|
+
} else if (state.previousView === "status") {
|
|
1771
|
+
const workflows = listWorkflows(db);
|
|
1772
|
+
dispatch({ type: "SHOW_STATUS", workflows });
|
|
1773
|
+
} else {
|
|
1774
|
+
execution.handleExecutionBack();
|
|
1775
|
+
}
|
|
1776
|
+
}, [state.previousView, state.workflow, db, execution.handleExecutionBack]);
|
|
1777
|
+
const uiState = useMemo2(() => ({
|
|
1778
|
+
view: state.view,
|
|
1779
|
+
messages: state.messages,
|
|
1780
|
+
workflow: state.workflow,
|
|
1781
|
+
isGenerating: state.isGenerating,
|
|
1782
|
+
stepProgress: state.stepProgress,
|
|
1783
|
+
executionOutput: state.executionOutput,
|
|
1784
|
+
streamingText: state.streamingText,
|
|
1785
|
+
daemonStatus,
|
|
1786
|
+
isExecuting: execution.isExecuting,
|
|
1787
|
+
config,
|
|
1788
|
+
cwd,
|
|
1789
|
+
footerExtra,
|
|
1790
|
+
footerHints,
|
|
1791
|
+
isConversing: planner.isConversing,
|
|
1792
|
+
themeVersion,
|
|
1793
|
+
statusWorkflows: state.statusWorkflows,
|
|
1794
|
+
detailRuns: state.detailRuns,
|
|
1795
|
+
detailStepRuns: state.detailStepRuns
|
|
1796
|
+
}), [state, daemonStatus, execution.isExecuting, config, cwd, footerExtra, footerHints, planner.isConversing, themeVersion]);
|
|
1797
|
+
const uiActions = useMemo2(() => ({
|
|
1798
|
+
handleChatSubmit,
|
|
1799
|
+
handleCancelGeneration: planner.handleCancelGeneration,
|
|
1800
|
+
handleConfirm: execution.handleConfirm,
|
|
1801
|
+
handleModify: execution.handleModify,
|
|
1802
|
+
handleCancel: execution.handleCancel,
|
|
1803
|
+
handleExecutionAbort: execution.handleExecutionAbort,
|
|
1804
|
+
handleExecutionBack: handleExecutionBackWithNav,
|
|
1805
|
+
handleOnboardingComplete,
|
|
1806
|
+
handleOnboardingCancel,
|
|
1807
|
+
handleStatusBack,
|
|
1808
|
+
handleStatusSelect,
|
|
1809
|
+
handleStatusStop,
|
|
1810
|
+
handleStatusDelete,
|
|
1811
|
+
handleDetailBack,
|
|
1812
|
+
handleDetailSelectRun
|
|
1813
|
+
}), [handleChatSubmit, planner.handleCancelGeneration, execution.handleConfirm, execution.handleModify, execution.handleCancel, execution.handleExecutionAbort, handleExecutionBackWithNav, handleOnboardingComplete, handleOnboardingCancel, handleStatusBack, handleStatusSelect, handleStatusStop, handleStatusDelete, handleDetailBack, handleDetailSelectRun]);
|
|
1814
|
+
return /* @__PURE__ */ jsx4(UIStateContext.Provider, { value: uiState, children: /* @__PURE__ */ jsx4(UIActionsContext.Provider, { value: uiActions, children }) });
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// src/tui/app-layout.tsx
|
|
1818
|
+
import { useMemo as useMemo6 } from "react";
|
|
1819
|
+
import { Box as Box21, Static, useStdout as useStdout5 } from "ink";
|
|
1820
|
+
|
|
1821
|
+
// src/tui/banner.tsx
|
|
1822
|
+
import { memo } from "react";
|
|
1823
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
1824
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1825
|
+
var LOGO = ` \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
|
|
1826
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
1827
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551
|
|
1828
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551
|
|
1829
|
+
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D
|
|
1830
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D `;
|
|
1831
|
+
var Banner = memo(function Banner2({ version: version2, cwd, terminalWidth }) {
|
|
1832
|
+
const displayPath = cwd.replace(/^\/Users\/[^/]+/, "~");
|
|
1833
|
+
const gradient = theme.ui.gradient;
|
|
1834
|
+
if (terminalWidth < 64) {
|
|
1835
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
|
|
1836
|
+
/* @__PURE__ */ jsx5(GradientText, { text: "CueClaw", colors: gradient, bold: true }),
|
|
1837
|
+
/* @__PURE__ */ jsxs3(Text3, { color: theme.ui.comment, children: [
|
|
1838
|
+
version2,
|
|
1839
|
+
" \xB7 ",
|
|
1840
|
+
displayPath
|
|
1841
|
+
] })
|
|
1842
|
+
] });
|
|
1843
|
+
}
|
|
1844
|
+
const lines = LOGO.split("\n");
|
|
1845
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
|
|
1846
|
+
lines.map((line, i) => {
|
|
1847
|
+
const t = lines.length > 1 ? i / (lines.length - 1) : 0;
|
|
1848
|
+
const color = lerpGradient(gradient, t);
|
|
1849
|
+
return /* @__PURE__ */ jsx5(Text3, { color, children: line }, i);
|
|
1850
|
+
}),
|
|
1851
|
+
/* @__PURE__ */ jsxs3(Text3, { color: theme.ui.comment, children: [
|
|
1852
|
+
" ",
|
|
1853
|
+
version2,
|
|
1854
|
+
" \xB7 ",
|
|
1855
|
+
displayPath
|
|
1856
|
+
] }),
|
|
1857
|
+
/* @__PURE__ */ jsx5(Text3, { children: "" })
|
|
1858
|
+
] });
|
|
1859
|
+
});
|
|
1860
|
+
function GradientText({ text, colors: gradientColors, bold }) {
|
|
1861
|
+
if (gradientColors.length === 0) return /* @__PURE__ */ jsx5(Text3, { bold, children: text });
|
|
1862
|
+
const chars = [...text];
|
|
1863
|
+
return /* @__PURE__ */ jsx5(Text3, { bold, children: chars.map((ch, i) => {
|
|
1864
|
+
const t = chars.length > 1 ? i / (chars.length - 1) : 0;
|
|
1865
|
+
return /* @__PURE__ */ jsx5(Text3, { color: lerpGradient(gradientColors, t), children: ch }, i);
|
|
1866
|
+
}) });
|
|
1867
|
+
}
|
|
1868
|
+
function lerpGradient(stops, t) {
|
|
1869
|
+
if (stops.length === 0) return "#ffffff";
|
|
1870
|
+
if (stops.length === 1) return stops[0];
|
|
1871
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
1872
|
+
const segment = clamped * (stops.length - 1);
|
|
1873
|
+
const idx = Math.floor(segment);
|
|
1874
|
+
const frac = segment - idx;
|
|
1875
|
+
const a = stops[Math.min(idx, stops.length - 1)];
|
|
1876
|
+
const b = stops[Math.min(idx + 1, stops.length - 1)];
|
|
1877
|
+
return interpolateColor(a, b, frac);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// src/tui/chat.tsx
|
|
1881
|
+
import { Box as Box15 } from "ink";
|
|
1882
|
+
|
|
1883
|
+
// src/tui/main-content.tsx
|
|
1884
|
+
import { useState as useState6, useMemo as useMemo3, useEffect as useEffect5, useRef as useRef6, useCallback as useCallback9 } from "react";
|
|
1885
|
+
import { Box as Box13, Text as Text13, useStdout as useStdout2 } from "ink";
|
|
1886
|
+
|
|
1887
|
+
// src/tui/thinking-indicator.tsx
|
|
1888
|
+
import { useState as useState5, useEffect as useEffect4, useCallback as useCallback8, useRef as useRef5, memo as memo2 } from "react";
|
|
1889
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
1890
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1891
|
+
var DOTS = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1892
|
+
var ThinkingIndicator = memo2(function ThinkingIndicator2({ onCancel }) {
|
|
1893
|
+
const [elapsed, setElapsed] = useState5(0);
|
|
1894
|
+
const [frame, setFrame] = useState5(0);
|
|
1895
|
+
const startRef = useRef5(Date.now());
|
|
1896
|
+
useEffect4(() => {
|
|
1897
|
+
const timer = setInterval(() => {
|
|
1898
|
+
setElapsed(Math.floor((Date.now() - startRef.current) / 1e3));
|
|
1899
|
+
setFrame((f) => (f + 1) % DOTS.length);
|
|
1900
|
+
}, 80);
|
|
1901
|
+
return () => clearInterval(timer);
|
|
1902
|
+
}, []);
|
|
1903
|
+
useKeypress("thinking-cancel", KeyPriority.Normal, useCallback8((input, key) => {
|
|
1904
|
+
if (keyBindings.escape(input, key) && onCancel) {
|
|
1905
|
+
onCancel();
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
return false;
|
|
1909
|
+
}, [onCancel]));
|
|
1910
|
+
const gradient = theme.ui.gradient;
|
|
1911
|
+
const cyclePos = Date.now() / 4e3 % 1;
|
|
1912
|
+
const gradientIdx = Math.floor(cyclePos * gradient.length) % gradient.length;
|
|
1913
|
+
const spinnerColor = gradient[gradientIdx] ?? theme.text.accent;
|
|
1914
|
+
const cancelHint = onCancel ? " (esc to cancel)" : "";
|
|
1915
|
+
return /* @__PURE__ */ jsxs4(Box4, { paddingX: 1, children: [
|
|
1916
|
+
/* @__PURE__ */ jsx6(Box4, { width: 2, children: /* @__PURE__ */ jsx6(Text4, { color: spinnerColor, children: "\u2726 " }) }),
|
|
1917
|
+
/* @__PURE__ */ jsx6(Text4, { color: spinnerColor, children: DOTS[frame] }),
|
|
1918
|
+
/* @__PURE__ */ jsxs4(Text4, { color: theme.text.secondary, children: [
|
|
1919
|
+
" Thinking... ",
|
|
1920
|
+
elapsed,
|
|
1921
|
+
"s",
|
|
1922
|
+
cancelHint
|
|
1923
|
+
] })
|
|
1924
|
+
] });
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
// src/tui/messages/user-message.tsx
|
|
1928
|
+
import { memo as memo3 } from "react";
|
|
1929
|
+
import { Box as Box6, Text as Text6, useStdout } from "ink";
|
|
1930
|
+
|
|
1931
|
+
// src/tui/half-line-padded-box.tsx
|
|
1932
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
1933
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1934
|
+
function HalfLinePaddedBox({ backgroundColor, children, width }) {
|
|
1935
|
+
const bg = backgroundColor ?? theme.background.message;
|
|
1936
|
+
const termBg = theme.background.primary;
|
|
1937
|
+
return /* @__PURE__ */ jsxs5(Box5, { width, flexDirection: "column", children: [
|
|
1938
|
+
/* @__PURE__ */ jsx7(Text5, { backgroundColor: bg, color: termBg, children: "\u2580".repeat(width) }),
|
|
1939
|
+
/* @__PURE__ */ jsx7(Box5, { paddingX: 1, flexDirection: "column", children }),
|
|
1940
|
+
/* @__PURE__ */ jsx7(Text5, { color: termBg, backgroundColor: bg, children: "\u2584".repeat(width) })
|
|
1941
|
+
] });
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/tui/messages/user-message.tsx
|
|
1945
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1946
|
+
var UserMessage = memo3(function UserMessage2({ text }) {
|
|
1947
|
+
const { stdout } = useStdout();
|
|
1948
|
+
const cols = stdout?.columns ?? 80;
|
|
1949
|
+
return /* @__PURE__ */ jsx8(HalfLinePaddedBox, { width: cols, backgroundColor: theme.background.message, children: /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1950
|
+
/* @__PURE__ */ jsx8(Box6, { width: 2, children: /* @__PURE__ */ jsx8(Text6, { color: theme.text.accent, children: "> " }) }),
|
|
1951
|
+
/* @__PURE__ */ jsx8(Box6, { flexShrink: 1, children: /* @__PURE__ */ jsx8(Text6, { color: theme.text.secondary, children: text }) })
|
|
1952
|
+
] }) });
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
// src/tui/messages/assistant-message.tsx
|
|
1956
|
+
import { memo as memo4 } from "react";
|
|
1957
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
1958
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1959
|
+
var AssistantMessage = memo4(function AssistantMessage2({ text }) {
|
|
1960
|
+
return /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
|
|
1961
|
+
/* @__PURE__ */ jsx9(Box7, { width: 2, children: /* @__PURE__ */ jsx9(Text7, { color: theme.text.accent, children: "\u2726 " }) }),
|
|
1962
|
+
/* @__PURE__ */ jsx9(Box7, { flexShrink: 1, children: /* @__PURE__ */ jsx9(Text7, { color: theme.text.primary, children: text }) })
|
|
1963
|
+
] });
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
// src/tui/messages/assistant-jsx-message.tsx
|
|
1967
|
+
import { memo as memo5 } from "react";
|
|
1968
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
1969
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1970
|
+
var AssistantJsxMessage = memo5(function AssistantJsxMessage2({ content }) {
|
|
1971
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, children: [
|
|
1972
|
+
/* @__PURE__ */ jsx10(Box8, { children: /* @__PURE__ */ jsx10(Box8, { width: 2, children: /* @__PURE__ */ jsx10(Text8, { color: theme.text.accent, children: "\u2726 " }) }) }),
|
|
1973
|
+
content
|
|
1974
|
+
] });
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
// src/tui/messages/system-message.tsx
|
|
1978
|
+
import { memo as memo6 } from "react";
|
|
1979
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
1980
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1981
|
+
var SystemMessage = memo6(function SystemMessage2({ text }) {
|
|
1982
|
+
return /* @__PURE__ */ jsx11(Box9, { paddingX: 1, paddingLeft: 3, children: /* @__PURE__ */ jsx11(Text9, { color: theme.ui.comment, italic: true, children: text }) });
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
// src/tui/messages/error-message.tsx
|
|
1986
|
+
import { memo as memo7 } from "react";
|
|
1987
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
1988
|
+
import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1989
|
+
var ErrorMessage = memo7(function ErrorMessage2({ text }) {
|
|
1990
|
+
return /* @__PURE__ */ jsxs9(Box10, { paddingX: 1, children: [
|
|
1991
|
+
/* @__PURE__ */ jsx12(Box10, { width: 2, children: /* @__PURE__ */ jsx12(Text10, { color: theme.status.error, children: "\u2717 " }) }),
|
|
1992
|
+
/* @__PURE__ */ jsx12(Box10, { flexShrink: 1, children: /* @__PURE__ */ jsx12(Text10, { color: theme.status.error, children: text }) })
|
|
1993
|
+
] });
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
// src/tui/messages/warning-message.tsx
|
|
1997
|
+
import { memo as memo8 } from "react";
|
|
1998
|
+
import { Box as Box11, Text as Text11 } from "ink";
|
|
1999
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2000
|
+
var WarningMessage = memo8(function WarningMessage2({ text }) {
|
|
2001
|
+
return /* @__PURE__ */ jsxs10(Box11, { paddingX: 1, children: [
|
|
2002
|
+
/* @__PURE__ */ jsx13(Box11, { width: 2, children: /* @__PURE__ */ jsx13(Text11, { color: theme.status.warning, children: "\u26A0 " }) }),
|
|
2003
|
+
/* @__PURE__ */ jsx13(Box11, { flexShrink: 1, children: /* @__PURE__ */ jsx13(Text11, { color: theme.status.warning, children: text }) })
|
|
2004
|
+
] });
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
// src/tui/messages/plan-ready-message.tsx
|
|
2008
|
+
import { memo as memo9 } from "react";
|
|
2009
|
+
import { Box as Box12, Text as Text12 } from "ink";
|
|
2010
|
+
import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2011
|
+
var PlanReadyMessage = memo9(function PlanReadyMessage2({ workflowName }) {
|
|
2012
|
+
return /* @__PURE__ */ jsxs11(Box12, { paddingX: 1, children: [
|
|
2013
|
+
/* @__PURE__ */ jsx14(Box12, { width: 2, children: /* @__PURE__ */ jsx14(Text12, { color: theme.status.success, children: "\u2713 " }) }),
|
|
2014
|
+
/* @__PURE__ */ jsx14(Box12, { flexShrink: 1, children: /* @__PURE__ */ jsxs11(Text12, { color: theme.status.success, children: [
|
|
2015
|
+
'Plan ready: "',
|
|
2016
|
+
workflowName,
|
|
2017
|
+
'"'
|
|
2018
|
+
] }) })
|
|
2019
|
+
] });
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
// src/tui/messages/message-display.tsx
|
|
2023
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
2024
|
+
function MessageDisplay({ message }) {
|
|
2025
|
+
switch (message.type) {
|
|
2026
|
+
case "user":
|
|
2027
|
+
return /* @__PURE__ */ jsx15(UserMessage, { text: message.text });
|
|
2028
|
+
case "assistant":
|
|
2029
|
+
return /* @__PURE__ */ jsx15(AssistantMessage, { text: message.text });
|
|
2030
|
+
case "assistant-jsx":
|
|
2031
|
+
return /* @__PURE__ */ jsx15(AssistantJsxMessage, { content: message.content });
|
|
2032
|
+
case "system":
|
|
2033
|
+
return /* @__PURE__ */ jsx15(SystemMessage, { text: message.text });
|
|
2034
|
+
case "error":
|
|
2035
|
+
return /* @__PURE__ */ jsx15(ErrorMessage, { text: message.text });
|
|
2036
|
+
case "warning":
|
|
2037
|
+
return /* @__PURE__ */ jsx15(WarningMessage, { text: message.text });
|
|
2038
|
+
case "plan-ready":
|
|
2039
|
+
return /* @__PURE__ */ jsx15(PlanReadyMessage, { workflowName: message.workflowName });
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/tui/main-content.tsx
|
|
2044
|
+
import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2045
|
+
function MainContent() {
|
|
2046
|
+
const { messages, isGenerating, streamingText } = useUIState();
|
|
2047
|
+
const { handleCancelGeneration } = useUIActions();
|
|
2048
|
+
const { stdout } = useStdout2();
|
|
2049
|
+
const rows = stdout?.rows ?? 24;
|
|
2050
|
+
const [scrollOffset, setScrollOffset] = useState6(0);
|
|
2051
|
+
const prevMessageCountRef = useRef6(messages.length);
|
|
2052
|
+
useEffect5(() => {
|
|
2053
|
+
if (messages.length > prevMessageCountRef.current && scrollOffset === 0) {
|
|
2054
|
+
}
|
|
2055
|
+
prevMessageCountRef.current = messages.length;
|
|
2056
|
+
}, [messages.length, scrollOffset]);
|
|
2057
|
+
const pageSize = Math.max(1, Math.floor(rows / 2));
|
|
2058
|
+
useKeypress("chat-scroll", KeyPriority.Normal, useCallback9((input, key) => {
|
|
2059
|
+
if (keyBindings.scrollUp(input, key)) {
|
|
2060
|
+
setScrollOffset((prev) => Math.min(prev + pageSize, Math.max(0, messages.length - 1)));
|
|
2061
|
+
return true;
|
|
2062
|
+
}
|
|
2063
|
+
if (keyBindings.scrollDown(input, key)) {
|
|
2064
|
+
setScrollOffset((prev) => Math.max(0, prev - pageSize));
|
|
2065
|
+
return true;
|
|
2066
|
+
}
|
|
2067
|
+
return false;
|
|
2068
|
+
}, [pageSize, messages.length]));
|
|
2069
|
+
const visibleMessages = useMemo3(() => {
|
|
2070
|
+
if (scrollOffset === 0) return messages;
|
|
2071
|
+
const end = messages.length - scrollOffset;
|
|
2072
|
+
return messages.slice(0, Math.max(0, end));
|
|
2073
|
+
}, [messages, scrollOffset]);
|
|
2074
|
+
const hiddenAbove = messages.length - visibleMessages.length;
|
|
2075
|
+
return /* @__PURE__ */ jsxs12(Fragment2, { children: [
|
|
2076
|
+
hiddenAbove > 0 && /* @__PURE__ */ jsx16(Box13, { paddingX: 1, children: /* @__PURE__ */ jsxs12(Text13, { dimColor: true, children: [
|
|
2077
|
+
"^ ",
|
|
2078
|
+
hiddenAbove,
|
|
2079
|
+
" more message",
|
|
2080
|
+
hiddenAbove !== 1 ? "s" : "",
|
|
2081
|
+
" (Ctrl+P/Ctrl+N)"
|
|
2082
|
+
] }) }),
|
|
2083
|
+
/* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", flexGrow: 1, marginTop: hiddenAbove > 0 ? 0 : 1, children: [
|
|
2084
|
+
visibleMessages.map((msg, i) => /* @__PURE__ */ jsx16(Box13, { marginBottom: 1, flexDirection: "column", children: /* @__PURE__ */ jsx16(MessageDisplay, { message: msg }) }, i)),
|
|
2085
|
+
streamingText && /* @__PURE__ */ jsxs12(Box13, { marginBottom: 1, paddingX: 1, children: [
|
|
2086
|
+
/* @__PURE__ */ jsx16(Box13, { width: 2, children: /* @__PURE__ */ jsx16(Text13, { color: theme.text.accent, children: "\u2726 " }) }),
|
|
2087
|
+
/* @__PURE__ */ jsx16(Box13, { flexShrink: 1, children: /* @__PURE__ */ jsx16(Text13, { color: theme.text.primary, children: streamingText }) })
|
|
2088
|
+
] }),
|
|
2089
|
+
isGenerating && !streamingText && /* @__PURE__ */ jsx16(ThinkingIndicator, { onCancel: handleCancelGeneration })
|
|
2090
|
+
] })
|
|
2091
|
+
] });
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// src/tui/composer.tsx
|
|
2095
|
+
import { useState as useState7, useMemo as useMemo4 } from "react";
|
|
2096
|
+
import { Box as Box14, Text as Text15, useStdout as useStdout3 } from "ink";
|
|
2097
|
+
|
|
2098
|
+
// src/tui/resettable-input.tsx
|
|
2099
|
+
import { useReducer as useReducer2, useEffect as useEffect6, useRef as useRef7, useCallback as useCallback10 } from "react";
|
|
2100
|
+
import { Text as Text14 } from "ink";
|
|
2101
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2102
|
+
function inputReducer(state, action) {
|
|
2103
|
+
switch (action.type) {
|
|
2104
|
+
case "insert": {
|
|
2105
|
+
const value = state.value.slice(0, state.cursor) + action.text + state.value.slice(state.cursor);
|
|
2106
|
+
return { value, prevValue: state.value, cursor: state.cursor + action.text.length };
|
|
2107
|
+
}
|
|
2108
|
+
case "delete": {
|
|
2109
|
+
if (state.cursor === 0) return state;
|
|
2110
|
+
const value = state.value.slice(0, state.cursor - 1) + state.value.slice(state.cursor);
|
|
2111
|
+
return { value, prevValue: state.value, cursor: state.cursor - 1 };
|
|
2112
|
+
}
|
|
2113
|
+
case "left":
|
|
2114
|
+
return { ...state, cursor: Math.max(0, state.cursor - 1) };
|
|
2115
|
+
case "right":
|
|
2116
|
+
return { ...state, cursor: Math.min(state.value.length, state.cursor + 1) };
|
|
2117
|
+
case "reset":
|
|
2118
|
+
return { value: "", prevValue: state.value, cursor: 0 };
|
|
2119
|
+
case "set":
|
|
2120
|
+
return { value: action.value, prevValue: state.value, cursor: action.cursor };
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function ResettableInput({ placeholder, onSubmit, onChange, isDisabled, onUpArrow, onDownArrow }) {
|
|
2124
|
+
const [state, dispatch] = useReducer2(inputReducer, { value: "", prevValue: "", cursor: 0 });
|
|
2125
|
+
const submitRef = useRef7(null);
|
|
2126
|
+
useEffect6(() => {
|
|
2127
|
+
if (state.value !== state.prevValue) {
|
|
2128
|
+
onChange?.(state.value);
|
|
2129
|
+
}
|
|
2130
|
+
}, [state.value, state.prevValue, onChange]);
|
|
2131
|
+
useEffect6(() => {
|
|
2132
|
+
if (submitRef.current !== null) {
|
|
2133
|
+
const value = submitRef.current;
|
|
2134
|
+
submitRef.current = null;
|
|
2135
|
+
onSubmit(value);
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
useKeypress("resettable-input", KeyPriority.Normal, useCallback10((input, key) => {
|
|
2139
|
+
if (key.ctrl && input === "c" || key.tab || key.shift && key.tab) return false;
|
|
2140
|
+
if (key.upArrow) {
|
|
2141
|
+
if (onUpArrow) {
|
|
2142
|
+
const entry = onUpArrow(state.value);
|
|
2143
|
+
if (entry !== void 0) dispatch({ type: "set", value: entry, cursor: entry.length });
|
|
2144
|
+
}
|
|
2145
|
+
return true;
|
|
2146
|
+
}
|
|
2147
|
+
if (key.downArrow) {
|
|
2148
|
+
if (onDownArrow) {
|
|
2149
|
+
const entry = onDownArrow();
|
|
2150
|
+
if (entry !== void 0) dispatch({ type: "set", value: entry, cursor: entry.length });
|
|
2151
|
+
}
|
|
2152
|
+
return true;
|
|
2153
|
+
}
|
|
2154
|
+
if (key.return) {
|
|
2155
|
+
submitRef.current = state.value;
|
|
2156
|
+
dispatch({ type: "reset" });
|
|
2157
|
+
return true;
|
|
2158
|
+
}
|
|
2159
|
+
if (key.leftArrow) {
|
|
2160
|
+
dispatch({ type: "left" });
|
|
2161
|
+
return true;
|
|
2162
|
+
}
|
|
2163
|
+
if (key.rightArrow) {
|
|
2164
|
+
dispatch({ type: "right" });
|
|
2165
|
+
return true;
|
|
2166
|
+
}
|
|
2167
|
+
if (key.backspace || key.delete) {
|
|
2168
|
+
dispatch({ type: "delete" });
|
|
2169
|
+
return true;
|
|
2170
|
+
}
|
|
2171
|
+
dispatch({ type: "insert", text: input });
|
|
2172
|
+
return true;
|
|
2173
|
+
}, [state.value, onUpArrow, onDownArrow]), !isDisabled);
|
|
2174
|
+
if (state.value.length === 0) {
|
|
2175
|
+
return /* @__PURE__ */ jsxs13(Text14, { children: [
|
|
2176
|
+
/* @__PURE__ */ jsx17(Text14, { inverse: true, children: placeholder?.[0] ?? " " }),
|
|
2177
|
+
/* @__PURE__ */ jsx17(Text14, { dimColor: true, children: placeholder?.slice(1) ?? "" })
|
|
2178
|
+
] });
|
|
2179
|
+
}
|
|
2180
|
+
const before = state.value.slice(0, state.cursor);
|
|
2181
|
+
const cursorChar = state.value[state.cursor] ?? " ";
|
|
2182
|
+
const after = state.value.slice(state.cursor + 1);
|
|
2183
|
+
return /* @__PURE__ */ jsxs13(Text14, { children: [
|
|
2184
|
+
before,
|
|
2185
|
+
/* @__PURE__ */ jsx17(Text14, { inverse: true, children: cursorChar }),
|
|
2186
|
+
after
|
|
2187
|
+
] });
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/tui/use-input-history.ts
|
|
2191
|
+
import { useRef as useRef8, useCallback as useCallback11 } from "react";
|
|
2192
|
+
function useInputHistory() {
|
|
2193
|
+
const historyRef = useRef8([]);
|
|
2194
|
+
const indexRef = useRef8(-1);
|
|
2195
|
+
const draftRef = useRef8("");
|
|
2196
|
+
const up = useCallback11((currentValue) => {
|
|
2197
|
+
const history = historyRef.current;
|
|
2198
|
+
if (history.length === 0) return void 0;
|
|
2199
|
+
const nextIndex = indexRef.current + 1;
|
|
2200
|
+
if (nextIndex >= history.length) return void 0;
|
|
2201
|
+
if (indexRef.current === -1) {
|
|
2202
|
+
draftRef.current = currentValue;
|
|
2203
|
+
}
|
|
2204
|
+
indexRef.current = nextIndex;
|
|
2205
|
+
return history[nextIndex];
|
|
2206
|
+
}, []);
|
|
2207
|
+
const down = useCallback11(() => {
|
|
2208
|
+
if (indexRef.current === -1) return void 0;
|
|
2209
|
+
const nextIndex = indexRef.current - 1;
|
|
2210
|
+
indexRef.current = nextIndex;
|
|
2211
|
+
if (nextIndex === -1) {
|
|
2212
|
+
return draftRef.current;
|
|
2213
|
+
}
|
|
2214
|
+
return historyRef.current[nextIndex];
|
|
2215
|
+
}, []);
|
|
2216
|
+
const push = useCallback11((value) => {
|
|
2217
|
+
const trimmed = value.trim();
|
|
2218
|
+
if (!trimmed) return;
|
|
2219
|
+
if (historyRef.current[0] === trimmed) {
|
|
2220
|
+
indexRef.current = -1;
|
|
2221
|
+
draftRef.current = "";
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
historyRef.current.unshift(trimmed);
|
|
2225
|
+
indexRef.current = -1;
|
|
2226
|
+
draftRef.current = "";
|
|
2227
|
+
}, []);
|
|
2228
|
+
const resetBrowsing = useCallback11(() => {
|
|
2229
|
+
indexRef.current = -1;
|
|
2230
|
+
draftRef.current = "";
|
|
2231
|
+
}, []);
|
|
2232
|
+
return { up, down, push, resetBrowsing };
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/tui/composer.tsx
|
|
2236
|
+
import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2237
|
+
function modeLabel(state) {
|
|
2238
|
+
if (state.isExecuting) return { text: "executing", color: theme.status.warning };
|
|
2239
|
+
if (state.isGenerating) return { text: "generating", color: theme.text.accent };
|
|
2240
|
+
if (state.isConversing) return { text: "conversing", color: theme.text.accent };
|
|
2241
|
+
return { text: "idle", color: theme.text.secondary };
|
|
2242
|
+
}
|
|
2243
|
+
function daemonLabel(status) {
|
|
2244
|
+
switch (status) {
|
|
2245
|
+
case "running":
|
|
2246
|
+
return "daemon";
|
|
2247
|
+
case "external":
|
|
2248
|
+
return "external";
|
|
2249
|
+
case "starting":
|
|
2250
|
+
return "starting...";
|
|
2251
|
+
default:
|
|
2252
|
+
return "";
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
function Composer() {
|
|
2256
|
+
const { isGenerating, isExecuting, isConversing, daemonStatus, footerExtra, footerHints } = useUIState();
|
|
2257
|
+
const { handleChatSubmit } = useUIActions();
|
|
2258
|
+
const { stdout } = useStdout3();
|
|
2259
|
+
const cols = stdout?.columns ?? 80;
|
|
2260
|
+
const history = useInputHistory();
|
|
2261
|
+
const [currentInput, setCurrentInput] = useState7("");
|
|
2262
|
+
const allCommands = useMemo4(() => getCommands(), []);
|
|
2263
|
+
const matchingCommands = useMemo4(() => {
|
|
2264
|
+
if (!currentInput.startsWith("/")) return [];
|
|
2265
|
+
const prefix = currentInput.toLowerCase();
|
|
2266
|
+
return allCommands.filter((c) => {
|
|
2267
|
+
const full = `/${c.name}`;
|
|
2268
|
+
return full.startsWith(prefix) || c.aliases.some((a) => `/${a}`.startsWith(prefix));
|
|
2269
|
+
});
|
|
2270
|
+
}, [currentInput, allCommands]);
|
|
2271
|
+
const showCommandHints = currentInput.startsWith("/") && matchingCommands.length > 0 && currentInput !== "/" + matchingCommands[0]?.name;
|
|
2272
|
+
const mode = modeLabel({ isExecuting, isGenerating, isConversing });
|
|
2273
|
+
const daemon = daemonLabel(daemonStatus);
|
|
2274
|
+
return /* @__PURE__ */ jsxs14(Fragment3, { children: [
|
|
2275
|
+
/* @__PURE__ */ jsx18(Box14, { paddingX: 1, children: /* @__PURE__ */ jsx18(Text15, { color: theme.border.default, children: "\u2500".repeat(Math.max(0, cols - 2)) }) }),
|
|
2276
|
+
!isGenerating && /* @__PURE__ */ jsxs14(Box14, { paddingX: 1, justifyContent: "space-between", children: [
|
|
2277
|
+
/* @__PURE__ */ jsxs14(Box14, { gap: 2, children: [
|
|
2278
|
+
/* @__PURE__ */ jsxs14(Text15, { color: mode.color, children: [
|
|
2279
|
+
"[",
|
|
2280
|
+
mode.text,
|
|
2281
|
+
"]"
|
|
2282
|
+
] }),
|
|
2283
|
+
footerHints && /* @__PURE__ */ jsx18(Text15, { color: theme.ui.comment, children: footerHints })
|
|
2284
|
+
] }),
|
|
2285
|
+
/* @__PURE__ */ jsxs14(Box14, { gap: 2, children: [
|
|
2286
|
+
daemon && /* @__PURE__ */ jsx18(Text15, { color: theme.ui.comment, children: daemon }),
|
|
2287
|
+
footerExtra && /* @__PURE__ */ jsx18(Text15, { color: theme.ui.comment, children: footerExtra })
|
|
2288
|
+
] })
|
|
2289
|
+
] }),
|
|
2290
|
+
!isGenerating && showCommandHints && /* @__PURE__ */ jsx18(Box14, { flexDirection: "column", paddingX: 2, children: matchingCommands.slice(0, 6).map((cmd) => /* @__PURE__ */ jsxs14(Box14, { gap: 1, children: [
|
|
2291
|
+
/* @__PURE__ */ jsxs14(Text15, { color: theme.status.info, children: [
|
|
2292
|
+
"/",
|
|
2293
|
+
cmd.name
|
|
2294
|
+
] }),
|
|
2295
|
+
/* @__PURE__ */ jsxs14(Text15, { color: theme.ui.comment, children: [
|
|
2296
|
+
"\u2014",
|
|
2297
|
+
" ",
|
|
2298
|
+
cmd.description
|
|
2299
|
+
] })
|
|
2300
|
+
] }, cmd.name)) }),
|
|
2301
|
+
!isGenerating && /* @__PURE__ */ jsx18(Box14, { paddingX: 1, children: /* @__PURE__ */ jsxs14(
|
|
2302
|
+
Box14,
|
|
2303
|
+
{
|
|
2304
|
+
borderStyle: "round",
|
|
2305
|
+
borderColor: theme.border.focused,
|
|
2306
|
+
paddingX: 1,
|
|
2307
|
+
width: cols - 2,
|
|
2308
|
+
children: [
|
|
2309
|
+
/* @__PURE__ */ jsx18(Text15, { color: theme.prompt, children: "> " }),
|
|
2310
|
+
/* @__PURE__ */ jsx18(
|
|
2311
|
+
ResettableInput,
|
|
2312
|
+
{
|
|
2313
|
+
placeholder: "Describe a workflow or type /help",
|
|
2314
|
+
onChange: (value) => {
|
|
2315
|
+
setCurrentInput(value);
|
|
2316
|
+
history.resetBrowsing();
|
|
2317
|
+
},
|
|
2318
|
+
onSubmit: (value) => {
|
|
2319
|
+
const trimmed = value.trim();
|
|
2320
|
+
if (trimmed) {
|
|
2321
|
+
history.push(trimmed);
|
|
2322
|
+
setCurrentInput("");
|
|
2323
|
+
handleChatSubmit(trimmed);
|
|
2324
|
+
}
|
|
2325
|
+
},
|
|
2326
|
+
onUpArrow: history.up,
|
|
2327
|
+
onDownArrow: history.down,
|
|
2328
|
+
isDisabled: isGenerating
|
|
2329
|
+
}
|
|
2330
|
+
)
|
|
2331
|
+
]
|
|
2332
|
+
}
|
|
2333
|
+
) })
|
|
2334
|
+
] });
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/tui/chat.tsx
|
|
2338
|
+
import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2339
|
+
function Chat() {
|
|
2340
|
+
return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", flexGrow: 1, children: [
|
|
2341
|
+
/* @__PURE__ */ jsx19(MainContent, {}),
|
|
2342
|
+
/* @__PURE__ */ jsx19(Composer, {})
|
|
2343
|
+
] });
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/tui/plan-view.tsx
|
|
2347
|
+
import { useCallback as useCallback12, memo as memo10 } from "react";
|
|
2348
|
+
import { Box as Box16, Text as Text16 } from "ink";
|
|
2349
|
+
import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2350
|
+
function PlanView({ workflow, onConfirm, onModify, onCancel }) {
|
|
2351
|
+
useKeypress("plan-view-actions", KeyPriority.Normal, useCallback12((input, key) => {
|
|
2352
|
+
if (keyBindings.confirmPlan(input, key)) {
|
|
2353
|
+
onConfirm();
|
|
2354
|
+
return true;
|
|
2355
|
+
}
|
|
2356
|
+
if (keyBindings.modifyPlan(input, key)) {
|
|
2357
|
+
onModify();
|
|
2358
|
+
return true;
|
|
2359
|
+
}
|
|
2360
|
+
if (keyBindings.cancelPlan(input, key)) {
|
|
2361
|
+
onCancel();
|
|
2362
|
+
return true;
|
|
2363
|
+
}
|
|
2364
|
+
return false;
|
|
2365
|
+
}, [onConfirm, onModify, onCancel]));
|
|
2366
|
+
const trigger = workflow.trigger;
|
|
2367
|
+
const triggerLabel = trigger.type === "manual" ? "manual" : trigger.type === "cron" ? `cron (${trigger.expression})` : `poll (${trigger.interval_seconds}s)`;
|
|
2368
|
+
const failureDesc = workflow.failure_policy.on_step_failure;
|
|
2369
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
2370
|
+
/* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: theme.border.focused, children: [
|
|
2371
|
+
/* @__PURE__ */ jsxs16(Text16, { color: theme.border.accent, bold: true, children: [
|
|
2372
|
+
"Plan: ",
|
|
2373
|
+
workflow.name
|
|
2374
|
+
] }),
|
|
2375
|
+
/* @__PURE__ */ jsxs16(Text16, { color: theme.ui.comment, children: [
|
|
2376
|
+
"Trigger: ",
|
|
2377
|
+
triggerLabel
|
|
2378
|
+
] }),
|
|
2379
|
+
/* @__PURE__ */ jsx20(Text16, { children: "" }),
|
|
2380
|
+
workflow.steps.map((step, i) => /* @__PURE__ */ jsx20(StepLine, { step, index: i + 1 }, step.id)),
|
|
2381
|
+
/* @__PURE__ */ jsx20(Text16, { children: "" }),
|
|
2382
|
+
/* @__PURE__ */ jsxs16(Text16, { color: theme.ui.comment, children: [
|
|
2383
|
+
"Failure policy: ",
|
|
2384
|
+
failureDesc
|
|
2385
|
+
] })
|
|
2386
|
+
] }),
|
|
2387
|
+
/* @__PURE__ */ jsx20(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs16(Text16, { children: [
|
|
2388
|
+
/* @__PURE__ */ jsx20(Text16, { color: theme.status.success, children: "[Y] Confirm" }),
|
|
2389
|
+
" ",
|
|
2390
|
+
/* @__PURE__ */ jsx20(Text16, { color: theme.status.warning, children: "[M] Modify" }),
|
|
2391
|
+
" ",
|
|
2392
|
+
/* @__PURE__ */ jsx20(Text16, { color: theme.status.error, children: "[N] Cancel" })
|
|
2393
|
+
] }) })
|
|
2394
|
+
] });
|
|
2395
|
+
}
|
|
2396
|
+
function stepStatusSymbol(index) {
|
|
2397
|
+
return `\u25CB ${index}.`;
|
|
2398
|
+
}
|
|
2399
|
+
var StepLine = memo10(function StepLine2({ step, index }) {
|
|
2400
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
|
|
2401
|
+
/* @__PURE__ */ jsxs16(Text16, { color: theme.status.muted, children: [
|
|
2402
|
+
stepStatusSymbol(index),
|
|
2403
|
+
" ",
|
|
2404
|
+
step.description
|
|
2405
|
+
] }),
|
|
2406
|
+
step.depends_on && step.depends_on.length > 0 && /* @__PURE__ */ jsxs16(Text16, { color: theme.ui.comment, children: [
|
|
2407
|
+
" \u2514\u2500 depends on: ",
|
|
2408
|
+
step.depends_on.join(", ")
|
|
2409
|
+
] })
|
|
2410
|
+
] });
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
// src/tui/execution-view.tsx
|
|
2414
|
+
import { useCallback as useCallback13 } from "react";
|
|
2415
|
+
import { Box as Box17, Text as Text17 } from "ink";
|
|
2416
|
+
import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2417
|
+
function ExecutionView({ workflow, stepProgress, output, onBack, onAbort }) {
|
|
2418
|
+
const { isExecuting } = useUIState();
|
|
2419
|
+
const isRunning = Array.from(stepProgress.values()).some((s) => s.status === "running");
|
|
2420
|
+
useKeypress("execution-view-actions", KeyPriority.Normal, useCallback13((input, key) => {
|
|
2421
|
+
if (isExecuting && onAbort && keyBindings.abortExec(input, key)) {
|
|
2422
|
+
onAbort();
|
|
2423
|
+
return true;
|
|
2424
|
+
}
|
|
2425
|
+
if (!isExecuting && onBack && (keyBindings.submit(input, key) || keyBindings.quit(input, key) || keyBindings.escape(input, key))) {
|
|
2426
|
+
onBack();
|
|
2427
|
+
return true;
|
|
2428
|
+
}
|
|
2429
|
+
return false;
|
|
2430
|
+
}, [isExecuting, onAbort, onBack]));
|
|
2431
|
+
return /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
2432
|
+
/* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", flexGrow: 1, children: [
|
|
2433
|
+
/* @__PURE__ */ jsxs17(Box17, { justifyContent: "space-between", children: [
|
|
2434
|
+
/* @__PURE__ */ jsxs17(Text17, { color: theme.border.accent, bold: true, children: [
|
|
2435
|
+
"Workflow: ",
|
|
2436
|
+
workflow.name
|
|
2437
|
+
] }),
|
|
2438
|
+
/* @__PURE__ */ jsxs17(Text17, { color: theme.ui.comment, children: [
|
|
2439
|
+
"Status: ",
|
|
2440
|
+
isRunning ? "Running" : "Complete"
|
|
2441
|
+
] })
|
|
2442
|
+
] }),
|
|
2443
|
+
/* @__PURE__ */ jsx21(Text17, { children: "" }),
|
|
2444
|
+
/* @__PURE__ */ jsx21(Text17, { bold: true, color: theme.text.primary, children: "Steps:" }),
|
|
2445
|
+
workflow.steps.map((step, i) => {
|
|
2446
|
+
const progress = stepProgress.get(step.id);
|
|
2447
|
+
const status = progress?.status ?? "pending";
|
|
2448
|
+
const icon = statusIcon(status);
|
|
2449
|
+
const durationText = progress?.duration ? ` (${formatDuration2(progress.duration)})` : "";
|
|
2450
|
+
return /* @__PURE__ */ jsx21(Box17, { children: /* @__PURE__ */ jsxs17(Text17, { color: statusColor(status), children: [
|
|
2451
|
+
icon,
|
|
2452
|
+
" ",
|
|
2453
|
+
i + 1,
|
|
2454
|
+
". ",
|
|
2455
|
+
step.description,
|
|
2456
|
+
durationText
|
|
2457
|
+
] }) }, step.id);
|
|
2458
|
+
}),
|
|
2459
|
+
output.length > 0 && /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: theme.border.default, paddingX: 1, children: [
|
|
2460
|
+
/* @__PURE__ */ jsx21(Text17, { color: theme.ui.comment, children: "Live Output" }),
|
|
2461
|
+
output.slice(-10).map((line, i) => /* @__PURE__ */ jsx21(Text17, { color: theme.text.secondary, children: line }, i))
|
|
2462
|
+
] })
|
|
2463
|
+
] }),
|
|
2464
|
+
/* @__PURE__ */ jsx21(Box17, { marginTop: 1, children: /* @__PURE__ */ jsx21(Text17, { color: theme.ui.comment, children: isExecuting ? "Press [X] to cancel" : "Press Enter, Q, or Esc to return to chat" }) })
|
|
2465
|
+
] });
|
|
2466
|
+
}
|
|
2467
|
+
function statusIcon(status) {
|
|
2468
|
+
switch (status) {
|
|
2469
|
+
case "succeeded":
|
|
2470
|
+
return "\u2713";
|
|
2471
|
+
case "running":
|
|
2472
|
+
return "\u22B7";
|
|
2473
|
+
case "failed":
|
|
2474
|
+
return "\u2717";
|
|
2475
|
+
case "skipped":
|
|
2476
|
+
return "\u25CB";
|
|
2477
|
+
default:
|
|
2478
|
+
return "\u25CB";
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
function statusColor(status) {
|
|
2482
|
+
switch (status) {
|
|
2483
|
+
case "succeeded":
|
|
2484
|
+
return theme.status.success;
|
|
2485
|
+
case "running":
|
|
2486
|
+
return theme.status.warning;
|
|
2487
|
+
case "failed":
|
|
2488
|
+
return theme.status.error;
|
|
2489
|
+
case "skipped":
|
|
2490
|
+
return theme.status.muted;
|
|
2491
|
+
default:
|
|
2492
|
+
return theme.status.muted;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
function formatDuration2(ms) {
|
|
2496
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
2497
|
+
return `${Math.round(ms / 1e3)}s`;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/tui/workflow-detail-view.tsx
|
|
2501
|
+
import { useState as useState8, useCallback as useCallback14 } from "react";
|
|
2502
|
+
import { Box as Box18, Text as Text18 } from "ink";
|
|
2503
|
+
import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
2504
|
+
function WorkflowDetailView({ workflow, runs, latestStepRuns, onBack, onSelectRun, onStop }) {
|
|
2505
|
+
const [selectedRunIndex, setSelectedRunIndex] = useState8(0);
|
|
2506
|
+
const displayRuns = runs.slice(0, 5);
|
|
2507
|
+
useKeypress("detail-view-actions", KeyPriority.Normal, useCallback14((input, key) => {
|
|
2508
|
+
if (keyBindings.escape(input, key) || keyBindings.quit(input, key)) {
|
|
2509
|
+
onBack();
|
|
2510
|
+
return true;
|
|
2511
|
+
}
|
|
2512
|
+
if (keyBindings.submit(input, key) && displayRuns.length > 0) {
|
|
2513
|
+
onSelectRun(displayRuns[selectedRunIndex].id);
|
|
2514
|
+
return true;
|
|
2515
|
+
}
|
|
2516
|
+
if (keyBindings.stopWorkflow(input, key) && onStop) {
|
|
2517
|
+
onStop();
|
|
2518
|
+
return true;
|
|
2519
|
+
}
|
|
2520
|
+
if (keyBindings.upArrow(input, key) && selectedRunIndex > 0) {
|
|
2521
|
+
setSelectedRunIndex(selectedRunIndex - 1);
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
if (keyBindings.downArrow(input, key) && selectedRunIndex < displayRuns.length - 1) {
|
|
2525
|
+
setSelectedRunIndex(selectedRunIndex + 1);
|
|
2526
|
+
return true;
|
|
2527
|
+
}
|
|
2528
|
+
return false;
|
|
2529
|
+
}, [onBack, onSelectRun, onStop, displayRuns, selectedRunIndex]));
|
|
2530
|
+
const canStop = workflow.phase === "executing" || workflow.phase === "active";
|
|
2531
|
+
return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
2532
|
+
/* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", flexGrow: 1, children: [
|
|
2533
|
+
/* @__PURE__ */ jsxs18(Box18, { justifyContent: "space-between", children: [
|
|
2534
|
+
/* @__PURE__ */ jsx22(Text18, { color: theme.border.accent, bold: true, children: workflow.name }),
|
|
2535
|
+
/* @__PURE__ */ jsx22(Text18, { color: phaseColor2(workflow.phase), children: workflow.phase })
|
|
2536
|
+
] }),
|
|
2537
|
+
/* @__PURE__ */ jsx22(Text18, { children: "" }),
|
|
2538
|
+
/* @__PURE__ */ jsxs18(Text18, { color: theme.ui.comment, children: [
|
|
2539
|
+
"ID: ",
|
|
2540
|
+
workflow.id
|
|
2541
|
+
] }),
|
|
2542
|
+
workflow.description && /* @__PURE__ */ jsx22(Text18, { color: theme.text.secondary, children: workflow.description }),
|
|
2543
|
+
/* @__PURE__ */ jsxs18(Text18, { color: theme.ui.comment, children: [
|
|
2544
|
+
"Trigger: ",
|
|
2545
|
+
formatTrigger(workflow.trigger)
|
|
2546
|
+
] }),
|
|
2547
|
+
/* @__PURE__ */ jsxs18(Text18, { color: theme.ui.comment, children: [
|
|
2548
|
+
"Failure policy: ",
|
|
2549
|
+
workflow.failure_policy.on_step_failure,
|
|
2550
|
+
" (retries: ",
|
|
2551
|
+
workflow.failure_policy.max_retries,
|
|
2552
|
+
")"
|
|
2553
|
+
] }),
|
|
2554
|
+
/* @__PURE__ */ jsxs18(Text18, { color: theme.ui.comment, children: [
|
|
2555
|
+
"Created: ",
|
|
2556
|
+
workflow.created_at,
|
|
2557
|
+
" Updated: ",
|
|
2558
|
+
workflow.updated_at
|
|
2559
|
+
] }),
|
|
2560
|
+
/* @__PURE__ */ jsx22(Text18, { children: "" }),
|
|
2561
|
+
/* @__PURE__ */ jsxs18(Text18, { bold: true, color: theme.text.primary, children: [
|
|
2562
|
+
"Steps (",
|
|
2563
|
+
workflow.steps.length,
|
|
2564
|
+
"):"
|
|
2565
|
+
] }),
|
|
2566
|
+
workflow.steps.map((step, i) => {
|
|
2567
|
+
const deps = step.depends_on.length > 0 ? ` \u2192 depends on: ${step.depends_on.join(", ")}` : "";
|
|
2568
|
+
const stepRun = latestStepRuns.find((sr) => sr.step_id === step.id);
|
|
2569
|
+
const icon = stepRun ? statusIcon2(stepRun.status) : "\u25CB";
|
|
2570
|
+
const iconColor = stepRun ? statusColor2(stepRun.status) : theme.status.muted;
|
|
2571
|
+
return /* @__PURE__ */ jsxs18(Box18, { children: [
|
|
2572
|
+
/* @__PURE__ */ jsxs18(Text18, { color: iconColor, children: [
|
|
2573
|
+
icon,
|
|
2574
|
+
" "
|
|
2575
|
+
] }),
|
|
2576
|
+
/* @__PURE__ */ jsxs18(Text18, { color: theme.text.secondary, children: [
|
|
2577
|
+
i + 1,
|
|
2578
|
+
". ",
|
|
2579
|
+
step.description
|
|
2580
|
+
] }),
|
|
2581
|
+
deps && /* @__PURE__ */ jsx22(Text18, { color: theme.ui.comment, children: deps })
|
|
2582
|
+
] }, step.id);
|
|
2583
|
+
}),
|
|
2584
|
+
/* @__PURE__ */ jsx22(Text18, { children: "" }),
|
|
2585
|
+
/* @__PURE__ */ jsx22(Text18, { bold: true, color: theme.text.primary, children: "Recent Runs:" }),
|
|
2586
|
+
displayRuns.length === 0 ? /* @__PURE__ */ jsx22(Text18, { dimColor: true, children: "No runs yet." }) : /* @__PURE__ */ jsxs18(Fragment4, { children: [
|
|
2587
|
+
/* @__PURE__ */ jsxs18(Box18, { children: [
|
|
2588
|
+
/* @__PURE__ */ jsx22(Box18, { width: 14, children: /* @__PURE__ */ jsx22(Text18, { bold: true, children: "Status" }) }),
|
|
2589
|
+
/* @__PURE__ */ jsx22(Box18, { width: 24, children: /* @__PURE__ */ jsx22(Text18, { bold: true, children: "Started" }) }),
|
|
2590
|
+
/* @__PURE__ */ jsx22(Box18, { width: 12, children: /* @__PURE__ */ jsx22(Text18, { bold: true, children: "Duration" }) }),
|
|
2591
|
+
/* @__PURE__ */ jsx22(Box18, { children: /* @__PURE__ */ jsx22(Text18, { bold: true, children: "Error" }) })
|
|
2592
|
+
] }),
|
|
2593
|
+
displayRuns.map((run, i) => /* @__PURE__ */ jsx22(Box18, { children: /* @__PURE__ */ jsxs18(Text18, { inverse: i === selectedRunIndex, children: [
|
|
2594
|
+
/* @__PURE__ */ jsx22(Text18, { color: runStatusColor(run.status), children: run.status.padEnd(14) }),
|
|
2595
|
+
/* @__PURE__ */ jsx22(Text18, { children: run.started_at.slice(0, 22).padEnd(24) }),
|
|
2596
|
+
/* @__PURE__ */ jsx22(Text18, { children: (run.duration_ms != null ? formatDuration3(run.duration_ms) : "\u2014").padEnd(12) }),
|
|
2597
|
+
/* @__PURE__ */ jsx22(Text18, { color: theme.status.error, children: run.error ? run.error.slice(0, 40) : "" })
|
|
2598
|
+
] }) }, run.id))
|
|
2599
|
+
] })
|
|
2600
|
+
] }),
|
|
2601
|
+
/* @__PURE__ */ jsx22(Box18, { marginTop: 1, children: /* @__PURE__ */ jsxs18(Text18, { dimColor: true, children: [
|
|
2602
|
+
displayRuns.length > 0 ? "[Enter] View run " : "",
|
|
2603
|
+
canStop ? "[S] Stop " : "",
|
|
2604
|
+
"[Q/Esc] Back"
|
|
2605
|
+
] }) })
|
|
2606
|
+
] });
|
|
2607
|
+
}
|
|
2608
|
+
function phaseColor2(phase) {
|
|
2609
|
+
switch (phase) {
|
|
2610
|
+
case "executing":
|
|
2611
|
+
return theme.status.warning;
|
|
2612
|
+
case "active":
|
|
2613
|
+
return theme.status.success;
|
|
2614
|
+
case "completed":
|
|
2615
|
+
return theme.status.success;
|
|
2616
|
+
case "failed":
|
|
2617
|
+
return theme.status.error;
|
|
2618
|
+
case "paused":
|
|
2619
|
+
return theme.status.muted;
|
|
2620
|
+
default:
|
|
2621
|
+
return theme.text.primary;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
function runStatusColor(status) {
|
|
2625
|
+
switch (status) {
|
|
2626
|
+
case "completed":
|
|
2627
|
+
return theme.status.success;
|
|
2628
|
+
case "running":
|
|
2629
|
+
return theme.status.warning;
|
|
2630
|
+
case "failed":
|
|
2631
|
+
return theme.status.error;
|
|
2632
|
+
default:
|
|
2633
|
+
return theme.text.primary;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
function statusIcon2(status) {
|
|
2637
|
+
switch (status) {
|
|
2638
|
+
case "succeeded":
|
|
2639
|
+
return "\u2713";
|
|
2640
|
+
case "running":
|
|
2641
|
+
return "\u22B7";
|
|
2642
|
+
case "failed":
|
|
2643
|
+
return "\u2717";
|
|
2644
|
+
case "skipped":
|
|
2645
|
+
return "\u25CB";
|
|
2646
|
+
default:
|
|
2647
|
+
return "\u25CB";
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
function statusColor2(status) {
|
|
2651
|
+
switch (status) {
|
|
2652
|
+
case "succeeded":
|
|
2653
|
+
return theme.status.success;
|
|
2654
|
+
case "running":
|
|
2655
|
+
return theme.status.warning;
|
|
2656
|
+
case "failed":
|
|
2657
|
+
return theme.status.error;
|
|
2658
|
+
case "skipped":
|
|
2659
|
+
return theme.status.muted;
|
|
2660
|
+
default:
|
|
2661
|
+
return theme.status.muted;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
function formatTrigger(trigger) {
|
|
2665
|
+
switch (trigger.type) {
|
|
2666
|
+
case "poll":
|
|
2667
|
+
return `poll every ${trigger.interval_seconds}s (${trigger.diff_mode})`;
|
|
2668
|
+
case "cron":
|
|
2669
|
+
return `cron: ${trigger.expression}${trigger.timezone ? ` (${trigger.timezone})` : ""}`;
|
|
2670
|
+
case "manual":
|
|
2671
|
+
return "manual";
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
function formatDuration3(ms) {
|
|
2675
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
2676
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
2677
|
+
return `${Math.round(ms / 6e4)}m`;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// src/tui/onboarding.tsx
|
|
2681
|
+
import { useState as useState9, useCallback as useCallback15, useMemo as useMemo5 } from "react";
|
|
2682
|
+
import { Box as Box19, Text as Text19, useStdout as useStdout4 } from "ink";
|
|
2683
|
+
import { TextInput, PasswordInput, ConfirmInput, Spinner, StatusMessage } from "@inkjs/ui";
|
|
2684
|
+
import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
2685
|
+
function maskKey(key) {
|
|
2686
|
+
if (key.length <= 8) return "****";
|
|
2687
|
+
return key.slice(0, 4) + "****" + key.slice(-4);
|
|
2688
|
+
}
|
|
2689
|
+
function Onboarding({ onComplete, onCancel, issues }) {
|
|
2690
|
+
const { stdout } = useStdout4();
|
|
2691
|
+
const cols = stdout?.columns ?? 80;
|
|
2692
|
+
const existing = useMemo5(() => loadExistingConfig(), []);
|
|
2693
|
+
const initialStep = useMemo5(() => {
|
|
2694
|
+
if (!issues || issues.length === 0) return "welcome";
|
|
2695
|
+
const errorFields = new Set(issues.filter((i) => i.severity === "error").map((i) => i.field));
|
|
2696
|
+
if (errorFields.size === 1 && errorFields.has("claude.api_key")) {
|
|
2697
|
+
return "api_key";
|
|
2698
|
+
}
|
|
2699
|
+
return "welcome";
|
|
2700
|
+
}, [issues]);
|
|
2701
|
+
const [step, setStep] = useState9(initialStep);
|
|
2702
|
+
const [state, setState] = useState9({
|
|
2703
|
+
apiKey: existing.apiKey ?? "",
|
|
2704
|
+
baseUrl: existing.baseUrl ?? "",
|
|
2705
|
+
containerEnabled: existing.containerEnabled ?? false,
|
|
2706
|
+
telegramEnabled: existing.telegramEnabled ?? false,
|
|
2707
|
+
telegramToken: existing.telegramToken ?? "",
|
|
2708
|
+
whatsappEnabled: existing.whatsappEnabled ?? false
|
|
2709
|
+
});
|
|
2710
|
+
const env = checkEnvironment();
|
|
2711
|
+
useKeypress("onboarding-cancel", KeyPriority.Normal, useCallback15((input, key) => {
|
|
2712
|
+
if (onCancel && keyBindings.escape(input, key)) {
|
|
2713
|
+
onCancel();
|
|
2714
|
+
return true;
|
|
2715
|
+
}
|
|
2716
|
+
return false;
|
|
2717
|
+
}, [onCancel]));
|
|
2718
|
+
const gotoApiKey = useCallback15(() => {
|
|
2719
|
+
setStep(existing.apiKey ? "api_key_existing" : "api_key");
|
|
2720
|
+
}, [existing]);
|
|
2721
|
+
const gotoBaseUrl = useCallback15(() => {
|
|
2722
|
+
if (!isDev) return;
|
|
2723
|
+
setStep(existing.baseUrl ? "base_url_existing" : "base_url");
|
|
2724
|
+
}, [existing]);
|
|
2725
|
+
const gotoContainer = useCallback15(() => {
|
|
2726
|
+
if (!(env.docker && env.dockerRunning)) {
|
|
2727
|
+
gotoTelegram();
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
setStep(existing.containerEnabled !== void 0 ? "container_existing" : "container");
|
|
2731
|
+
}, [env, existing]);
|
|
2732
|
+
const gotoTelegram = useCallback15(() => {
|
|
2733
|
+
setStep(existing.telegramEnabled !== void 0 ? "telegram_existing" : "telegram");
|
|
2734
|
+
}, [existing]);
|
|
2735
|
+
const gotoTelegramToken = useCallback15(() => {
|
|
2736
|
+
setStep(existing.telegramToken ? "telegram_token_existing" : "telegram_token");
|
|
2737
|
+
}, [existing]);
|
|
2738
|
+
const gotoWhatsApp = useCallback15(() => {
|
|
2739
|
+
setStep(existing.whatsappEnabled !== void 0 ? "whatsapp_existing" : "whatsapp");
|
|
2740
|
+
}, [existing]);
|
|
2741
|
+
const afterValidation = useCallback15(() => {
|
|
2742
|
+
if (env.docker && env.dockerRunning) {
|
|
2743
|
+
gotoContainer();
|
|
2744
|
+
} else {
|
|
2745
|
+
gotoTelegram();
|
|
2746
|
+
}
|
|
2747
|
+
}, [env, gotoContainer, gotoTelegram]);
|
|
2748
|
+
const handleApiKeySubmit = useCallback15((value) => {
|
|
2749
|
+
const key = value.trim();
|
|
2750
|
+
if (!key) return;
|
|
2751
|
+
setState((s) => ({ ...s, apiKey: key }));
|
|
2752
|
+
if (isDev) {
|
|
2753
|
+
gotoBaseUrl();
|
|
2754
|
+
} else {
|
|
2755
|
+
setStep("validating");
|
|
2756
|
+
doValidation(key, "");
|
|
2757
|
+
}
|
|
2758
|
+
}, [gotoBaseUrl]);
|
|
2759
|
+
const handleBaseUrlSubmit = useCallback15((value) => {
|
|
2760
|
+
const url = value.trim();
|
|
2761
|
+
setState((s) => ({ ...s, baseUrl: url }));
|
|
2762
|
+
setStep("validating");
|
|
2763
|
+
doValidation(state.apiKey, url);
|
|
2764
|
+
}, [state.apiKey]);
|
|
2765
|
+
const doValidation = useCallback15(async (apiKey, baseUrl) => {
|
|
2766
|
+
const prevKey = process.env["ANTHROPIC_API_KEY"];
|
|
2767
|
+
const prevUrl = process.env["ANTHROPIC_BASE_URL"];
|
|
2768
|
+
process.env["ANTHROPIC_API_KEY"] = apiKey;
|
|
2769
|
+
if (baseUrl) {
|
|
2770
|
+
process.env["ANTHROPIC_BASE_URL"] = baseUrl;
|
|
2771
|
+
} else {
|
|
2772
|
+
delete process.env["ANTHROPIC_BASE_URL"];
|
|
2773
|
+
}
|
|
2774
|
+
try {
|
|
2775
|
+
const tempConfig = loadConfig();
|
|
2776
|
+
const result = await validateAuth(tempConfig);
|
|
2777
|
+
if (result.valid) {
|
|
2778
|
+
setState((s) => ({ ...s, validationError: void 0 }));
|
|
2779
|
+
afterValidation();
|
|
2780
|
+
} else {
|
|
2781
|
+
setState((s) => ({ ...s, validationError: result.error }));
|
|
2782
|
+
setStep("api_key");
|
|
2783
|
+
}
|
|
2784
|
+
} catch {
|
|
2785
|
+
setState((s) => ({ ...s, validationError: "Failed to validate API key" }));
|
|
2786
|
+
setStep("api_key");
|
|
2787
|
+
} finally {
|
|
2788
|
+
if (prevKey !== void 0) process.env["ANTHROPIC_API_KEY"] = prevKey;
|
|
2789
|
+
else delete process.env["ANTHROPIC_API_KEY"];
|
|
2790
|
+
if (prevUrl !== void 0) process.env["ANTHROPIC_BASE_URL"] = prevUrl;
|
|
2791
|
+
else delete process.env["ANTHROPIC_BASE_URL"];
|
|
2792
|
+
}
|
|
2793
|
+
}, [afterValidation]);
|
|
2794
|
+
const handleContainerYes = useCallback15(() => {
|
|
2795
|
+
setState((s) => ({ ...s, containerEnabled: true }));
|
|
2796
|
+
gotoTelegram();
|
|
2797
|
+
}, [gotoTelegram]);
|
|
2798
|
+
const handleContainerNo = useCallback15(() => {
|
|
2799
|
+
setState((s) => ({ ...s, containerEnabled: false }));
|
|
2800
|
+
gotoTelegram();
|
|
2801
|
+
}, [gotoTelegram]);
|
|
2802
|
+
const handleTelegramYes = useCallback15(() => {
|
|
2803
|
+
setState((s) => ({ ...s, telegramEnabled: true }));
|
|
2804
|
+
gotoTelegramToken();
|
|
2805
|
+
}, [gotoTelegramToken]);
|
|
2806
|
+
const handleTelegramNo = useCallback15(() => {
|
|
2807
|
+
setState((s) => ({ ...s, telegramEnabled: false }));
|
|
2808
|
+
gotoWhatsApp();
|
|
2809
|
+
}, [gotoWhatsApp]);
|
|
2810
|
+
const handleTelegramTokenSubmit = useCallback15((value) => {
|
|
2811
|
+
setState((s) => ({ ...s, telegramToken: value.trim() }));
|
|
2812
|
+
gotoWhatsApp();
|
|
2813
|
+
}, [gotoWhatsApp]);
|
|
2814
|
+
const finishWithWhatsApp = useCallback15((enabled) => {
|
|
2815
|
+
const next = { ...state, whatsappEnabled: enabled };
|
|
2816
|
+
setState((s) => ({ ...s, whatsappEnabled: enabled }));
|
|
2817
|
+
setStep("saving");
|
|
2818
|
+
doSaveConfig(next);
|
|
2819
|
+
}, [state]);
|
|
2820
|
+
const handleWhatsAppYes = useCallback15(() => finishWithWhatsApp(true), [finishWithWhatsApp]);
|
|
2821
|
+
const handleWhatsAppNo = useCallback15(() => finishWithWhatsApp(false), [finishWithWhatsApp]);
|
|
2822
|
+
const doSaveConfig = useCallback15((finalState) => {
|
|
2823
|
+
if (isDev) {
|
|
2824
|
+
writeEnvVar("ANTHROPIC_API_KEY", finalState.apiKey);
|
|
2825
|
+
if (finalState.baseUrl) {
|
|
2826
|
+
writeEnvVar("ANTHROPIC_BASE_URL", finalState.baseUrl);
|
|
2827
|
+
}
|
|
2828
|
+
} else {
|
|
2829
|
+
const configUpdates = {
|
|
2830
|
+
claude: {
|
|
2831
|
+
api_key: finalState.apiKey,
|
|
2832
|
+
...finalState.baseUrl ? { base_url: finalState.baseUrl } : {}
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2835
|
+
if (finalState.containerEnabled) {
|
|
2836
|
+
configUpdates.container = { enabled: true };
|
|
2837
|
+
}
|
|
2838
|
+
if (finalState.telegramEnabled && finalState.telegramToken) {
|
|
2839
|
+
configUpdates.telegram = {
|
|
2840
|
+
enabled: true,
|
|
2841
|
+
token: finalState.telegramToken
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
if (finalState.whatsappEnabled) {
|
|
2845
|
+
configUpdates.whatsapp = { enabled: true };
|
|
2846
|
+
}
|
|
2847
|
+
writeConfig(configUpdates);
|
|
2848
|
+
}
|
|
2849
|
+
const config = loadConfig();
|
|
2850
|
+
setStep("done");
|
|
2851
|
+
setTimeout(() => onComplete(config), 1500);
|
|
2852
|
+
}, [onComplete]);
|
|
2853
|
+
const StepLayout = ({ children, input }) => /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", flexGrow: 1, children: [
|
|
2854
|
+
/* @__PURE__ */ jsx23(Box19, { flexDirection: "column", flexGrow: 1, children }),
|
|
2855
|
+
/* @__PURE__ */ jsx23(Box19, { marginTop: 1, children: input })
|
|
2856
|
+
] });
|
|
2857
|
+
return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
2858
|
+
onCancel && /* @__PURE__ */ jsx23(Box19, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Press Esc to cancel" }) }),
|
|
2859
|
+
step === "welcome" && /* @__PURE__ */ jsxs19(
|
|
2860
|
+
StepLayout,
|
|
2861
|
+
{
|
|
2862
|
+
input: /* @__PURE__ */ jsx23(
|
|
2863
|
+
TextInput,
|
|
2864
|
+
{
|
|
2865
|
+
placeholder: "Press Enter to continue...",
|
|
2866
|
+
onSubmit: () => gotoApiKey()
|
|
2867
|
+
}
|
|
2868
|
+
),
|
|
2869
|
+
children: [
|
|
2870
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, color: "cyan", children: "Welcome to CueClaw" }),
|
|
2871
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "First time? Let's set up CueClaw." }),
|
|
2872
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "\u2500".repeat(Math.max(0, cols - 2)) }),
|
|
2873
|
+
/* @__PURE__ */ jsxs19(Box19, { marginTop: 1, children: [
|
|
2874
|
+
/* @__PURE__ */ jsx23(Text19, { children: "Press " }),
|
|
2875
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, color: "green", children: "Enter" }),
|
|
2876
|
+
/* @__PURE__ */ jsx23(Text19, { children: " to begin setup" })
|
|
2877
|
+
] })
|
|
2878
|
+
]
|
|
2879
|
+
}
|
|
2880
|
+
),
|
|
2881
|
+
step === "api_key_existing" && /* @__PURE__ */ jsxs19(
|
|
2882
|
+
StepLayout,
|
|
2883
|
+
{
|
|
2884
|
+
input: /* @__PURE__ */ jsx23(
|
|
2885
|
+
ConfirmInput,
|
|
2886
|
+
{
|
|
2887
|
+
onConfirm: () => {
|
|
2888
|
+
if (isDev) gotoBaseUrl();
|
|
2889
|
+
else {
|
|
2890
|
+
setStep("validating");
|
|
2891
|
+
doValidation(state.apiKey, state.baseUrl);
|
|
2892
|
+
}
|
|
2893
|
+
},
|
|
2894
|
+
onCancel: () => setStep("api_key")
|
|
2895
|
+
}
|
|
2896
|
+
),
|
|
2897
|
+
children: [
|
|
2898
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 1: API Key" }),
|
|
2899
|
+
/* @__PURE__ */ jsxs19(Text19, { children: [
|
|
2900
|
+
"Found existing API key: ",
|
|
2901
|
+
/* @__PURE__ */ jsx23(Text19, { color: "yellow", children: maskKey(existing.apiKey) })
|
|
2902
|
+
] }),
|
|
2903
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Keep this key?" })
|
|
2904
|
+
]
|
|
2905
|
+
}
|
|
2906
|
+
),
|
|
2907
|
+
step === "api_key" && /* @__PURE__ */ jsxs19(
|
|
2908
|
+
StepLayout,
|
|
2909
|
+
{
|
|
2910
|
+
input: /* @__PURE__ */ jsxs19(Box19, { children: [
|
|
2911
|
+
/* @__PURE__ */ jsx23(Text19, { color: "green", children: "> " }),
|
|
2912
|
+
/* @__PURE__ */ jsx23(PasswordInput, { placeholder: "sk-ant-...", onSubmit: handleApiKeySubmit })
|
|
2913
|
+
] }),
|
|
2914
|
+
children: [
|
|
2915
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 1: API Key" }),
|
|
2916
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Enter your Anthropic API key (or compatible provider key)" }),
|
|
2917
|
+
state.validationError && /* @__PURE__ */ jsx23(Box19, { marginY: 1, children: /* @__PURE__ */ jsx23(StatusMessage, { variant: "error", children: state.validationError }) })
|
|
2918
|
+
]
|
|
2919
|
+
}
|
|
2920
|
+
),
|
|
2921
|
+
step === "base_url_existing" && /* @__PURE__ */ jsxs19(
|
|
2922
|
+
StepLayout,
|
|
2923
|
+
{
|
|
2924
|
+
input: /* @__PURE__ */ jsx23(
|
|
2925
|
+
ConfirmInput,
|
|
2926
|
+
{
|
|
2927
|
+
onConfirm: () => {
|
|
2928
|
+
setStep("validating");
|
|
2929
|
+
doValidation(state.apiKey, state.baseUrl);
|
|
2930
|
+
},
|
|
2931
|
+
onCancel: () => setStep("base_url")
|
|
2932
|
+
}
|
|
2933
|
+
),
|
|
2934
|
+
children: [
|
|
2935
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 1b: Base URL" }),
|
|
2936
|
+
/* @__PURE__ */ jsxs19(Text19, { children: [
|
|
2937
|
+
"Found existing base URL: ",
|
|
2938
|
+
/* @__PURE__ */ jsx23(Text19, { color: "yellow", children: existing.baseUrl })
|
|
2939
|
+
] }),
|
|
2940
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Keep this URL?" })
|
|
2941
|
+
]
|
|
2942
|
+
}
|
|
2943
|
+
),
|
|
2944
|
+
step === "base_url" && /* @__PURE__ */ jsxs19(
|
|
2945
|
+
StepLayout,
|
|
2946
|
+
{
|
|
2947
|
+
input: /* @__PURE__ */ jsxs19(Box19, { children: [
|
|
2948
|
+
/* @__PURE__ */ jsx23(Text19, { color: "green", children: "> " }),
|
|
2949
|
+
/* @__PURE__ */ jsx23(TextInput, { placeholder: "https://api.anthropic.com", onSubmit: handleBaseUrlSubmit })
|
|
2950
|
+
] }),
|
|
2951
|
+
children: [
|
|
2952
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 1b: Base URL (optional)" }),
|
|
2953
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Custom API base URL (leave empty for api.anthropic.com)" })
|
|
2954
|
+
]
|
|
2955
|
+
}
|
|
2956
|
+
),
|
|
2957
|
+
step === "validating" && /* @__PURE__ */ jsx23(Box19, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx23(Spinner, { label: "Validating API key..." }) }),
|
|
2958
|
+
step === "container_existing" && /* @__PURE__ */ jsxs19(
|
|
2959
|
+
StepLayout,
|
|
2960
|
+
{
|
|
2961
|
+
input: /* @__PURE__ */ jsx23(
|
|
2962
|
+
ConfirmInput,
|
|
2963
|
+
{
|
|
2964
|
+
onConfirm: () => gotoTelegram(),
|
|
2965
|
+
onCancel: () => setStep("container")
|
|
2966
|
+
}
|
|
2967
|
+
),
|
|
2968
|
+
children: [
|
|
2969
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 2: Container Isolation" }),
|
|
2970
|
+
/* @__PURE__ */ jsxs19(Text19, { children: [
|
|
2971
|
+
"Container isolation is already ",
|
|
2972
|
+
/* @__PURE__ */ jsx23(Text19, { color: "yellow", children: existing.containerEnabled ? "enabled" : "disabled" }),
|
|
2973
|
+
"."
|
|
2974
|
+
] }),
|
|
2975
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Keep this setting?" })
|
|
2976
|
+
]
|
|
2977
|
+
}
|
|
2978
|
+
),
|
|
2979
|
+
step === "container" && /* @__PURE__ */ jsxs19(
|
|
2980
|
+
StepLayout,
|
|
2981
|
+
{
|
|
2982
|
+
input: /* @__PURE__ */ jsx23(ConfirmInput, { onConfirm: handleContainerYes, onCancel: handleContainerNo }),
|
|
2983
|
+
children: [
|
|
2984
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 2: Container Isolation" }),
|
|
2985
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Docker detected. Enable container isolation for safer execution?" })
|
|
2986
|
+
]
|
|
2987
|
+
}
|
|
2988
|
+
),
|
|
2989
|
+
step === "telegram_existing" && /* @__PURE__ */ jsxs19(
|
|
2990
|
+
StepLayout,
|
|
2991
|
+
{
|
|
2992
|
+
input: /* @__PURE__ */ jsx23(
|
|
2993
|
+
ConfirmInput,
|
|
2994
|
+
{
|
|
2995
|
+
onConfirm: () => {
|
|
2996
|
+
if (state.telegramEnabled) gotoTelegramToken();
|
|
2997
|
+
else gotoWhatsApp();
|
|
2998
|
+
},
|
|
2999
|
+
onCancel: () => setStep("telegram")
|
|
3000
|
+
}
|
|
3001
|
+
),
|
|
3002
|
+
children: [
|
|
3003
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 3: Telegram Bot" }),
|
|
3004
|
+
/* @__PURE__ */ jsxs19(Text19, { children: [
|
|
3005
|
+
"Telegram bot is already ",
|
|
3006
|
+
/* @__PURE__ */ jsx23(Text19, { color: "yellow", children: existing.telegramEnabled ? "enabled" : "disabled" }),
|
|
3007
|
+
"."
|
|
3008
|
+
] }),
|
|
3009
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Keep this setting?" })
|
|
3010
|
+
]
|
|
3011
|
+
}
|
|
3012
|
+
),
|
|
3013
|
+
step === "telegram" && /* @__PURE__ */ jsxs19(
|
|
3014
|
+
StepLayout,
|
|
3015
|
+
{
|
|
3016
|
+
input: /* @__PURE__ */ jsx23(ConfirmInput, { onConfirm: handleTelegramYes, onCancel: handleTelegramNo }),
|
|
3017
|
+
children: [
|
|
3018
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 3: Telegram Bot" }),
|
|
3019
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Set up a Telegram bot for remote workflow management?" })
|
|
3020
|
+
]
|
|
3021
|
+
}
|
|
3022
|
+
),
|
|
3023
|
+
step === "telegram_token_existing" && /* @__PURE__ */ jsxs19(
|
|
3024
|
+
StepLayout,
|
|
3025
|
+
{
|
|
3026
|
+
input: /* @__PURE__ */ jsx23(
|
|
3027
|
+
ConfirmInput,
|
|
3028
|
+
{
|
|
3029
|
+
onConfirm: () => gotoWhatsApp(),
|
|
3030
|
+
onCancel: () => setStep("telegram_token")
|
|
3031
|
+
}
|
|
3032
|
+
),
|
|
3033
|
+
children: [
|
|
3034
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Telegram Bot Token" }),
|
|
3035
|
+
/* @__PURE__ */ jsxs19(Text19, { children: [
|
|
3036
|
+
"Found existing token: ",
|
|
3037
|
+
/* @__PURE__ */ jsx23(Text19, { color: "yellow", children: maskKey(existing.telegramToken) })
|
|
3038
|
+
] }),
|
|
3039
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Keep this token?" })
|
|
3040
|
+
]
|
|
3041
|
+
}
|
|
3042
|
+
),
|
|
3043
|
+
step === "telegram_token" && /* @__PURE__ */ jsxs19(
|
|
3044
|
+
StepLayout,
|
|
3045
|
+
{
|
|
3046
|
+
input: /* @__PURE__ */ jsxs19(Box19, { children: [
|
|
3047
|
+
/* @__PURE__ */ jsx23(Text19, { color: "green", children: "> " }),
|
|
3048
|
+
/* @__PURE__ */ jsx23(PasswordInput, { placeholder: "123456:ABC...", onSubmit: handleTelegramTokenSubmit })
|
|
3049
|
+
] }),
|
|
3050
|
+
children: [
|
|
3051
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Telegram Bot Token" }),
|
|
3052
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Paste the token from @BotFather" })
|
|
3053
|
+
]
|
|
3054
|
+
}
|
|
3055
|
+
),
|
|
3056
|
+
step === "whatsapp_existing" && /* @__PURE__ */ jsxs19(
|
|
3057
|
+
StepLayout,
|
|
3058
|
+
{
|
|
3059
|
+
input: /* @__PURE__ */ jsx23(
|
|
3060
|
+
ConfirmInput,
|
|
3061
|
+
{
|
|
3062
|
+
onConfirm: () => finishWithWhatsApp(state.whatsappEnabled),
|
|
3063
|
+
onCancel: () => setStep("whatsapp")
|
|
3064
|
+
}
|
|
3065
|
+
),
|
|
3066
|
+
children: [
|
|
3067
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 4: WhatsApp" }),
|
|
3068
|
+
/* @__PURE__ */ jsxs19(Text19, { children: [
|
|
3069
|
+
"WhatsApp is already ",
|
|
3070
|
+
/* @__PURE__ */ jsx23(Text19, { color: "yellow", children: existing.whatsappEnabled ? "enabled" : "disabled" }),
|
|
3071
|
+
"."
|
|
3072
|
+
] }),
|
|
3073
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Keep this setting?" })
|
|
3074
|
+
]
|
|
3075
|
+
}
|
|
3076
|
+
),
|
|
3077
|
+
step === "whatsapp" && /* @__PURE__ */ jsxs19(
|
|
3078
|
+
StepLayout,
|
|
3079
|
+
{
|
|
3080
|
+
input: /* @__PURE__ */ jsx23(ConfirmInput, { onConfirm: handleWhatsAppYes, onCancel: handleWhatsAppNo }),
|
|
3081
|
+
children: [
|
|
3082
|
+
/* @__PURE__ */ jsx23(Text19, { bold: true, children: "Step 4: WhatsApp" }),
|
|
3083
|
+
/* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Enable WhatsApp channel? (Requires QR scan on daemon start)" })
|
|
3084
|
+
]
|
|
3085
|
+
}
|
|
3086
|
+
),
|
|
3087
|
+
step === "saving" && /* @__PURE__ */ jsx23(Box19, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx23(Spinner, { label: "Saving configuration..." }) }),
|
|
3088
|
+
step === "done" && /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", flexGrow: 1, children: [
|
|
3089
|
+
/* @__PURE__ */ jsx23(StatusMessage, { variant: "success", children: "Configuration saved!" }),
|
|
3090
|
+
/* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [
|
|
3091
|
+
/* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
|
|
3092
|
+
"API Key: ****",
|
|
3093
|
+
state.apiKey.slice(-4)
|
|
3094
|
+
] }),
|
|
3095
|
+
state.baseUrl && /* @__PURE__ */ jsxs19(Text19, { dimColor: true, children: [
|
|
3096
|
+
"Base URL: ",
|
|
3097
|
+
state.baseUrl
|
|
3098
|
+
] }),
|
|
3099
|
+
state.containerEnabled && /* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Container isolation: enabled" }),
|
|
3100
|
+
state.telegramEnabled && /* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "Telegram bot: configured" }),
|
|
3101
|
+
state.whatsappEnabled && /* @__PURE__ */ jsx23(Text19, { dimColor: true, children: "WhatsApp: enabled" })
|
|
3102
|
+
] }),
|
|
3103
|
+
/* @__PURE__ */ jsx23(Box19, { marginTop: 1, children: /* @__PURE__ */ jsx23(Text19, { children: "Starting CueClaw..." }) })
|
|
3104
|
+
] })
|
|
3105
|
+
] });
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// src/tui/status.tsx
|
|
3109
|
+
import { useState as useState10, useCallback as useCallback16, useEffect as useEffect7 } from "react";
|
|
3110
|
+
import { Box as Box20, Text as Text20 } from "ink";
|
|
3111
|
+
import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
3112
|
+
function Status({ workflows, onSelect, onBack, onStop, onDelete }) {
|
|
3113
|
+
const [selectedIndex, setSelectedIndex] = useState10(0);
|
|
3114
|
+
const [confirm, setConfirm] = useState10(null);
|
|
3115
|
+
const [message, setMessage] = useState10(null);
|
|
3116
|
+
useEffect7(() => {
|
|
3117
|
+
if (!message) return;
|
|
3118
|
+
const timer = setTimeout(() => setMessage(null), 3e3);
|
|
3119
|
+
return () => clearTimeout(timer);
|
|
3120
|
+
}, [message]);
|
|
3121
|
+
useKeypress("status-view", KeyPriority.Normal, useCallback16((input, key) => {
|
|
3122
|
+
if (confirm) {
|
|
3123
|
+
if (keyBindings.confirmYes(input, key)) {
|
|
3124
|
+
const wf = workflows.find((w) => w.id === confirm.workflowId);
|
|
3125
|
+
if (wf) {
|
|
3126
|
+
if (confirm.type === "stop" && onStop) {
|
|
3127
|
+
onStop(wf);
|
|
3128
|
+
setMessage(`Stopped "${wf.name}"`);
|
|
3129
|
+
} else if (confirm.type === "delete" && onDelete) {
|
|
3130
|
+
onDelete(wf);
|
|
3131
|
+
setMessage(`Deleted "${wf.name}"`);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
setConfirm(null);
|
|
3135
|
+
return true;
|
|
3136
|
+
} else if (keyBindings.confirmNo(input, key)) {
|
|
3137
|
+
setConfirm(null);
|
|
3138
|
+
return true;
|
|
3139
|
+
}
|
|
3140
|
+
return true;
|
|
3141
|
+
}
|
|
3142
|
+
if (message) setMessage(null);
|
|
3143
|
+
if (keyBindings.upArrow(input, key) && selectedIndex > 0) {
|
|
3144
|
+
setSelectedIndex(selectedIndex - 1);
|
|
3145
|
+
return true;
|
|
3146
|
+
}
|
|
3147
|
+
if (keyBindings.downArrow(input, key) && selectedIndex < workflows.length - 1) {
|
|
3148
|
+
setSelectedIndex(selectedIndex + 1);
|
|
3149
|
+
return true;
|
|
3150
|
+
}
|
|
3151
|
+
if (keyBindings.submit(input, key) && workflows.length > 0) {
|
|
3152
|
+
onSelect(workflows[selectedIndex]);
|
|
3153
|
+
return true;
|
|
3154
|
+
}
|
|
3155
|
+
const selected = workflows[selectedIndex];
|
|
3156
|
+
if (keyBindings.stopWorkflow(input, key) && onStop && selected && (selected.phase === "executing" || selected.phase === "active")) {
|
|
3157
|
+
setConfirm({ type: "stop", workflowId: selected.id });
|
|
3158
|
+
return true;
|
|
3159
|
+
}
|
|
3160
|
+
if (keyBindings.deleteWorkflow(input, key) && onDelete && selected) {
|
|
3161
|
+
setConfirm({ type: "delete", workflowId: selected.id });
|
|
3162
|
+
return true;
|
|
3163
|
+
}
|
|
3164
|
+
if (keyBindings.escape(input, key) || keyBindings.quit(input, key)) {
|
|
3165
|
+
onBack();
|
|
3166
|
+
return true;
|
|
3167
|
+
}
|
|
3168
|
+
return false;
|
|
3169
|
+
}, [confirm, message, selectedIndex, workflows, onSelect, onBack, onStop, onDelete]));
|
|
3170
|
+
const phaseColor3 = (phase) => {
|
|
3171
|
+
switch (phase) {
|
|
3172
|
+
case "executing":
|
|
3173
|
+
return theme.status.warning;
|
|
3174
|
+
case "active":
|
|
3175
|
+
return theme.status.success;
|
|
3176
|
+
case "completed":
|
|
3177
|
+
return theme.status.success;
|
|
3178
|
+
case "failed":
|
|
3179
|
+
return theme.status.error;
|
|
3180
|
+
case "paused":
|
|
3181
|
+
return theme.status.muted;
|
|
3182
|
+
default:
|
|
3183
|
+
return theme.text.primary;
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
return /* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
3187
|
+
/* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", flexGrow: 1, children: [
|
|
3188
|
+
/* @__PURE__ */ jsx24(Text20, { bold: true, color: theme.border.accent, children: "Workflows" }),
|
|
3189
|
+
/* @__PURE__ */ jsx24(Text20, { children: "" }),
|
|
3190
|
+
workflows.length === 0 ? /* @__PURE__ */ jsx24(Text20, { dimColor: true, children: "No workflows found. Type a description to create one." }) : /* @__PURE__ */ jsxs20(Fragment5, { children: [
|
|
3191
|
+
/* @__PURE__ */ jsxs20(Box20, { children: [
|
|
3192
|
+
/* @__PURE__ */ jsx24(Box20, { width: 14, children: /* @__PURE__ */ jsx24(Text20, { bold: true, children: "ID" }) }),
|
|
3193
|
+
/* @__PURE__ */ jsx24(Box20, { width: 28, children: /* @__PURE__ */ jsx24(Text20, { bold: true, children: "Name" }) }),
|
|
3194
|
+
/* @__PURE__ */ jsx24(Box20, { width: 14, children: /* @__PURE__ */ jsx24(Text20, { bold: true, children: "Phase" }) })
|
|
3195
|
+
] }),
|
|
3196
|
+
workflows.map((wf, i) => /* @__PURE__ */ jsx24(Box20, { children: /* @__PURE__ */ jsxs20(Text20, { inverse: i === selectedIndex, children: [
|
|
3197
|
+
/* @__PURE__ */ jsx24(Text20, { children: wf.id.slice(0, 12).padEnd(14) }),
|
|
3198
|
+
/* @__PURE__ */ jsx24(Text20, { children: wf.name.slice(0, 26).padEnd(28) }),
|
|
3199
|
+
/* @__PURE__ */ jsx24(Text20, { color: phaseColor3(wf.phase), children: wf.phase })
|
|
3200
|
+
] }) }, wf.id))
|
|
3201
|
+
] })
|
|
3202
|
+
] }),
|
|
3203
|
+
/* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", marginTop: 1, children: [
|
|
3204
|
+
message && /* @__PURE__ */ jsx24(Text20, { bold: true, color: theme.status.success, children: message }),
|
|
3205
|
+
confirm ? /* @__PURE__ */ jsxs20(Text20, { bold: true, color: theme.status.warning, children: [
|
|
3206
|
+
confirm.type === "stop" ? "Stop" : "Delete",
|
|
3207
|
+
" workflow ",
|
|
3208
|
+
confirm.workflowId.slice(0, 12),
|
|
3209
|
+
"? [Y]es / [N]o"
|
|
3210
|
+
] }) : /* @__PURE__ */ jsx24(Text20, { dimColor: true, children: "[Enter] View [S] Stop [X] Delete [Q] Back" })
|
|
3211
|
+
] })
|
|
3212
|
+
] });
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// src/tui/app-layout.tsx
|
|
3216
|
+
import { jsx as jsx25, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
3217
|
+
function AppLayout({ cwd }) {
|
|
3218
|
+
const { view, workflow, stepProgress, executionOutput, config, statusWorkflows, detailRuns, detailStepRuns } = useUIState();
|
|
3219
|
+
const { handleConfirm, handleModify, handleCancel, handleExecutionAbort, handleExecutionBack, handleOnboardingComplete, handleOnboardingCancel, handleStatusBack, handleStatusSelect, handleStatusStop, handleStatusDelete, handleDetailBack, handleDetailSelectRun } = useUIActions();
|
|
3220
|
+
const { stdout } = useStdout5();
|
|
3221
|
+
const rows = stdout?.rows ?? 24;
|
|
3222
|
+
const displayPath = cwd ? cwd.replace(process.env["HOME"] ?? "", "~") : "";
|
|
3223
|
+
const versionLabel = appVersion === "dev" ? "dev" : `v${appVersion}`;
|
|
3224
|
+
const configIssues = useMemo6(() => {
|
|
3225
|
+
const validation = validateConfig();
|
|
3226
|
+
return validation.issues.filter((i) => i.severity === "error");
|
|
3227
|
+
}, []);
|
|
3228
|
+
return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", height: rows, children: [
|
|
3229
|
+
/* @__PURE__ */ jsx25(Static, { items: view !== "onboarding" ? ["banner"] : [], children: (item) => /* @__PURE__ */ jsx25(
|
|
3230
|
+
Banner,
|
|
3231
|
+
{
|
|
3232
|
+
version: versionLabel,
|
|
3233
|
+
cwd: displayPath,
|
|
3234
|
+
terminalWidth: stdout?.columns ?? 80
|
|
3235
|
+
},
|
|
3236
|
+
item
|
|
3237
|
+
) }),
|
|
3238
|
+
view === "onboarding" && /* @__PURE__ */ jsx25(Onboarding, { onComplete: handleOnboardingComplete, onCancel: config ? handleOnboardingCancel : void 0, issues: configIssues }),
|
|
3239
|
+
view === "chat" && /* @__PURE__ */ jsx25(Chat, {}),
|
|
3240
|
+
view === "plan" && workflow && /* @__PURE__ */ jsx25(
|
|
3241
|
+
PlanView,
|
|
3242
|
+
{
|
|
3243
|
+
workflow,
|
|
3244
|
+
onConfirm: handleConfirm,
|
|
3245
|
+
onModify: handleModify,
|
|
3246
|
+
onCancel: handleCancel
|
|
3247
|
+
}
|
|
3248
|
+
),
|
|
3249
|
+
view === "execution" && workflow && /* @__PURE__ */ jsx25(
|
|
3250
|
+
ExecutionView,
|
|
3251
|
+
{
|
|
3252
|
+
workflow,
|
|
3253
|
+
stepProgress,
|
|
3254
|
+
output: executionOutput,
|
|
3255
|
+
onBack: handleExecutionBack,
|
|
3256
|
+
onAbort: handleExecutionAbort
|
|
3257
|
+
}
|
|
3258
|
+
),
|
|
3259
|
+
view === "status" && /* @__PURE__ */ jsx25(
|
|
3260
|
+
Status,
|
|
3261
|
+
{
|
|
3262
|
+
workflows: statusWorkflows,
|
|
3263
|
+
onBack: handleStatusBack,
|
|
3264
|
+
onSelect: handleStatusSelect,
|
|
3265
|
+
onStop: handleStatusStop,
|
|
3266
|
+
onDelete: handleStatusDelete
|
|
3267
|
+
}
|
|
3268
|
+
),
|
|
3269
|
+
view === "detail" && workflow && /* @__PURE__ */ jsx25(
|
|
3270
|
+
WorkflowDetailView,
|
|
3271
|
+
{
|
|
3272
|
+
workflow,
|
|
3273
|
+
runs: detailRuns,
|
|
3274
|
+
latestStepRuns: detailStepRuns,
|
|
3275
|
+
onBack: handleDetailBack,
|
|
3276
|
+
onSelectRun: handleDetailSelectRun
|
|
3277
|
+
}
|
|
3278
|
+
)
|
|
3279
|
+
] });
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
// src/tui/app.tsx
|
|
3283
|
+
import { jsx as jsx26 } from "react/jsx-runtime";
|
|
3284
|
+
function App({ cwd, skipOnboarding }) {
|
|
3285
|
+
return /* @__PURE__ */ jsx26(ThemeProvider, { theme: cueclawTheme, children: /* @__PURE__ */ jsx26(KeypressProvider, { children: /* @__PURE__ */ jsx26(DialogManager, { children: /* @__PURE__ */ jsx26(AppProvider, { cwd, skipOnboarding, children: /* @__PURE__ */ jsx26(AppLayout, { cwd }) }) }) }) });
|
|
3286
|
+
}
|
|
3287
|
+
export {
|
|
3288
|
+
App
|
|
3289
|
+
};
|