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.
@@ -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
+ };