@bike4mind/cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConfigStore-Bj1IOvWn.mjs → ConfigStore-CAKSUXCi.mjs} +95 -20
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +2 -2
- package/dist/commands/mcpCommand.mjs +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +464 -240
- package/dist/{store-B0ImnWR4.mjs → store-DLduYYGR.mjs} +14 -0
- package/dist/store-YhSkjsW4.mjs +3 -0
- package/dist/{tools-Dg1HL5PO.mjs → tools-B0Y_zziv.mjs} +78 -142
- package/dist/{updateChecker-Bbkc_8IL.mjs → updateChecker-CtczXQeW.mjs} +1 -1
- package/package.json +11 -8
- package/dist/store-44C_Fvdb.mjs +0 -3
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as useCliStore, t as selectActiveBackgroundAgents } from "./store-
|
|
3
|
-
import { $ as CommandHistoryStore, A as formatStep, B as DEFAULT_RETRY_CONFIG, C as WebSocketToolExecutor, D as ServerLlmBackend, E as WebSocketLlmBackend, F as PermissionManager, G as OllamaBackend, H as clearFeatureModuleTools, I as generateCliTools, J as buildSkillsPromptSection, K as getPlanModeFilePath, L as ALWAYS_DENIED_FOR_AGENTS, M as loadContextFiles, N as getApiUrl, O as McpManager, P as getEnvironmentName, Q as CheckpointStore, R as DEFAULT_AGENT_MODEL, S as ApiClient, T as FallbackLlmBackend, U as registerFeatureModuleTools, V as DEFAULT_THOROUGHNESS, W as setWebSocketToolExecutor, X as ReActAgent, Y as isReadOnlyTool, Z as CustomCommandStore, _ as createAgentDelegateTool, a as createBlockerTools, at as mergeCommands, b as createSkillTool, c as createDecisionStore, ct as warmFileCache, d as createFindDefinitionTool, et as SessionStore, f as createTodoStore, g as BackgroundAgentManager, h as createBackgroundAgentTools, i as createBlockerStore, it as searchCommands, j as extractCompactInstructions, k as substituteArguments, l as formatDecisionsOutput, m as createCoordinateTaskTool, n as createReviewGateTool, nt as hasFileReferences, o as formatBlockersOutput, ot as formatFileSize, p as createWriteTodosTool, q as buildSystemPrompt, r as formatReviewGatesOutput, rt as processFileReferences, s as createDecisionLogTool, st as searchFiles, t as createReviewGateStore, tt as OAuthClient, u as createGetFileStructureTool, v as AgentStore, w as WebSocketConnectionManager, x as parseAgentConfig, y as SubagentOrchestrator, z as DEFAULT_MAX_ITERATIONS } from "./tools-
|
|
4
|
-
import {
|
|
5
|
-
import { a as version, t as checkForUpdate } from "./updateChecker-
|
|
2
|
+
import { n as useCliStore, t as selectActiveBackgroundAgents } from "./store-DLduYYGR.mjs";
|
|
3
|
+
import { $ as CommandHistoryStore, A as formatStep, B as DEFAULT_RETRY_CONFIG, C as WebSocketToolExecutor, D as ServerLlmBackend, E as WebSocketLlmBackend, F as PermissionManager, G as OllamaBackend, H as clearFeatureModuleTools, I as generateCliTools, J as buildSkillsPromptSection, K as getPlanModeFilePath, L as ALWAYS_DENIED_FOR_AGENTS, M as loadContextFiles, N as getApiUrl, O as McpManager, P as getEnvironmentName, Q as CheckpointStore, R as DEFAULT_AGENT_MODEL, S as ApiClient, T as FallbackLlmBackend, U as registerFeatureModuleTools, V as DEFAULT_THOROUGHNESS, W as setWebSocketToolExecutor, X as ReActAgent, Y as isReadOnlyTool, Z as CustomCommandStore, _ as createAgentDelegateTool, a as createBlockerTools, at as mergeCommands, b as createSkillTool, c as createDecisionStore, ct as warmFileCache, d as createFindDefinitionTool, et as SessionStore, f as createTodoStore, g as BackgroundAgentManager, h as createBackgroundAgentTools, i as createBlockerStore, it as searchCommands, j as extractCompactInstructions, k as substituteArguments, l as formatDecisionsOutput, m as createCoordinateTaskTool, n as createReviewGateTool, nt as hasFileReferences, o as formatBlockersOutput, ot as formatFileSize, p as createWriteTodosTool, q as buildSystemPrompt, r as formatReviewGatesOutput, rt as processFileReferences, s as createDecisionLogTool, st as searchFiles, t as createReviewGateStore, tt as OAuthClient, u as createGetFileStructureTool, v as AgentStore, w as WebSocketConnectionManager, x as parseAgentConfig, y as SubagentOrchestrator, z as DEFAULT_MAX_ITERATIONS } from "./tools-B0Y_zziv.mjs";
|
|
4
|
+
import { Nt as validateJupyterKernelName, Pt as validateNotebookPath$1, g as ChatModels, m as CREDIT_DEDUCT_TRANSACTION_TYPES, n as logger, t as ConfigStore } from "./ConfigStore-CAKSUXCi.mjs";
|
|
5
|
+
import { a as version, t as checkForUpdate } from "./updateChecker-CtczXQeW.mjs";
|
|
6
6
|
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
7
|
-
import { Box, Static, Text, render, useApp, useInput } from "ink";
|
|
7
|
+
import { Box, Static, Text, render, useApp, useInput, usePaste, useStdout } from "ink";
|
|
8
8
|
import { execSync } from "child_process";
|
|
9
9
|
import { randomBytes, randomUUID } from "crypto";
|
|
10
10
|
import { existsSync, promises, readFileSync, statSync } from "fs";
|
|
@@ -13,6 +13,7 @@ import path, { basename, extname, join } from "path";
|
|
|
13
13
|
import { v4 } from "uuid";
|
|
14
14
|
import * as path$1 from "node:path";
|
|
15
15
|
import Spinner from "ink-spinner";
|
|
16
|
+
import { useShallow } from "zustand/react/shallow";
|
|
16
17
|
import TextInput from "ink-text-input";
|
|
17
18
|
import { marked } from "marked";
|
|
18
19
|
import { highlight } from "cli-highlight";
|
|
@@ -24,20 +25,23 @@ import axios, { isAxiosError } from "axios";
|
|
|
24
25
|
import { get_encoding } from "tiktoken";
|
|
25
26
|
import WsWebSocket from "ws";
|
|
26
27
|
//#region src/components/StatusBar.tsx
|
|
27
|
-
const StatusBar = React.memo(function StatusBar({
|
|
28
|
+
const StatusBar = React.memo(function StatusBar({ isBashMode, model, tokenUsage, creditsUsage }) {
|
|
28
29
|
const interactionMode = useCliStore((state) => state.interactionMode);
|
|
29
30
|
return /* @__PURE__ */ React.createElement(Box, {
|
|
30
31
|
flexDirection: "row",
|
|
31
32
|
justifyContent: "space-between",
|
|
32
33
|
width: "100%",
|
|
33
34
|
paddingX: 1
|
|
34
|
-
}, /* @__PURE__ */ React.createElement(
|
|
35
|
+
}, /* @__PURE__ */ React.createElement(Box, { gap: 2 }, isBashMode ? /* @__PURE__ */ React.createElement(Text, {
|
|
36
|
+
color: "yellow",
|
|
37
|
+
bold: true
|
|
38
|
+
}, "BASH") : null, interactionMode === "auto-accept" && /* @__PURE__ */ React.createElement(Text, {
|
|
35
39
|
color: "green",
|
|
36
40
|
bold: true
|
|
37
41
|
}, "AUTO ACCEPT: Edits"), interactionMode === "plan" && /* @__PURE__ */ React.createElement(Text, {
|
|
38
42
|
color: "yellow",
|
|
39
43
|
bold: true
|
|
40
|
-
}, "PLAN MODE"), tokenUsage > 0 && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, tokenUsage.toLocaleString(), " tokens"), creditsUsage !== void 0 && creditsUsage > 0 && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, creditsUsage.toLocaleString(), " ", creditsUsage === 1 ? "credit" : "credits"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, model)));
|
|
44
|
+
}, "PLAN MODE")), /* @__PURE__ */ React.createElement(Box, { gap: 2 }, tokenUsage > 0 && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, tokenUsage.toLocaleString(), " tokens"), creditsUsage !== void 0 && creditsUsage > 0 && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, creditsUsage.toLocaleString(), " ", creditsUsage === 1 ? "credit" : "credits"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, model)));
|
|
41
45
|
});
|
|
42
46
|
/**
|
|
43
47
|
* Maximum paste size in characters (~500KB) to prevent memory issues
|
|
@@ -77,6 +81,20 @@ function CustomTextInput({ value, onChange, onSubmit, onPaste, pasteIndicator, p
|
|
|
77
81
|
while (pos < text.length && /\s/.test(text[pos])) pos++;
|
|
78
82
|
return pos;
|
|
79
83
|
};
|
|
84
|
+
usePaste((text) => {
|
|
85
|
+
const normalized = (text.length > 5e5 ? text.slice(0, MAX_PASTE_SIZE) : text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
86
|
+
if (normalized.split("\n").length >= 5 && onPaste) {
|
|
87
|
+
onPaste(normalized);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (pasteIndicator) {
|
|
91
|
+
emitChange(normalized);
|
|
92
|
+
setCursorOffset(normalized.length);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
emitChange(value.slice(0, cursorOffset) + normalized + value.slice(cursorOffset));
|
|
96
|
+
setCursorOffset(cursorOffset + normalized.length);
|
|
97
|
+
}, { isActive: !disabled });
|
|
80
98
|
useInput((input, key) => {
|
|
81
99
|
if (key.return && !key.meta && !key.shift) {
|
|
82
100
|
if (value.length > 0 && value[cursorOffset - 1] === "\\") {
|
|
@@ -101,7 +119,7 @@ function CustomTextInput({ value, onChange, onSubmit, onPaste, pasteIndicator, p
|
|
|
101
119
|
setCursorOffset(findNextWordBoundary(value, cursorOffset));
|
|
102
120
|
return;
|
|
103
121
|
}
|
|
104
|
-
if (key.backspace
|
|
122
|
+
if (key.backspace) {
|
|
105
123
|
const beforeCursor = value.slice(0, cursorOffset);
|
|
106
124
|
const afterCursor = value.slice(cursorOffset);
|
|
107
125
|
const newPos = findPreviousWordBoundary(beforeCursor, beforeCursor.length);
|
|
@@ -109,6 +127,13 @@ function CustomTextInput({ value, onChange, onSubmit, onPaste, pasteIndicator, p
|
|
|
109
127
|
setCursorOffset(newPos);
|
|
110
128
|
return;
|
|
111
129
|
}
|
|
130
|
+
if (key.delete) {
|
|
131
|
+
const beforeCursor = value.slice(0, cursorOffset);
|
|
132
|
+
const afterCursor = value.slice(cursorOffset);
|
|
133
|
+
const newPos = findNextWordBoundary(afterCursor, 0);
|
|
134
|
+
emitChange(beforeCursor + afterCursor.slice(newPos));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
112
137
|
} else {
|
|
113
138
|
if (key.leftArrow) {
|
|
114
139
|
setCursorOffset(0);
|
|
@@ -118,11 +143,15 @@ function CustomTextInput({ value, onChange, onSubmit, onPaste, pasteIndicator, p
|
|
|
118
143
|
setCursorOffset(value.length);
|
|
119
144
|
return;
|
|
120
145
|
}
|
|
121
|
-
if (key.backspace
|
|
146
|
+
if (key.backspace) {
|
|
122
147
|
emitChange(value.slice(cursorOffset));
|
|
123
148
|
setCursorOffset(0);
|
|
124
149
|
return;
|
|
125
150
|
}
|
|
151
|
+
if (key.delete) {
|
|
152
|
+
emitChange(value.slice(0, cursorOffset));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
126
155
|
}
|
|
127
156
|
if (key.home) {
|
|
128
157
|
setCursorOffset(0);
|
|
@@ -191,7 +220,7 @@ function CustomTextInput({ value, onChange, onSubmit, onPaste, pasteIndicator, p
|
|
|
191
220
|
}
|
|
192
221
|
return;
|
|
193
222
|
}
|
|
194
|
-
if (key.backspace
|
|
223
|
+
if (key.backspace) {
|
|
195
224
|
if (pasteIndicator) {
|
|
196
225
|
emitChange("");
|
|
197
226
|
setCursorOffset(0);
|
|
@@ -203,6 +232,15 @@ function CustomTextInput({ value, onChange, onSubmit, onPaste, pasteIndicator, p
|
|
|
203
232
|
}
|
|
204
233
|
return;
|
|
205
234
|
}
|
|
235
|
+
if (key.delete) {
|
|
236
|
+
if (pasteIndicator) {
|
|
237
|
+
emitChange("");
|
|
238
|
+
setCursorOffset(0);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (cursorOffset < value.length) emitChange(value.slice(0, cursorOffset) + value.slice(cursorOffset + 1));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
206
244
|
if (key.leftArrow && !key.meta && !key.ctrl) {
|
|
207
245
|
setCursorOffset(Math.max(0, cursorOffset - 1));
|
|
208
246
|
return;
|
|
@@ -724,10 +762,9 @@ function InputPrompt({ onSubmit, onBashCommand, onImageDetected, disabled = fals
|
|
|
724
762
|
setFileAutocomplete(null);
|
|
725
763
|
};
|
|
726
764
|
const handlePaste = (content) => {
|
|
727
|
-
const
|
|
728
|
-
const lineCount = truncated.split("\n").length;
|
|
765
|
+
const lineCount = content.split("\n").length;
|
|
729
766
|
const prefix = value.trim();
|
|
730
|
-
setPastedContent(prefix ? `${prefix}\n${
|
|
767
|
+
setPastedContent(prefix ? `${prefix}\n${content}` : content, lineCount);
|
|
731
768
|
};
|
|
732
769
|
const handleChange = async (newValue) => {
|
|
733
770
|
if (pastedContent) clearPaste();
|
|
@@ -883,7 +920,7 @@ function groupJobsByTurn(jobs) {
|
|
|
883
920
|
* Only renders when there are active (running/queued) background agents.
|
|
884
921
|
*/
|
|
885
922
|
function BackgroundAgentStatus() {
|
|
886
|
-
const activeJobs = useCliStore(selectActiveBackgroundAgents);
|
|
923
|
+
const activeJobs = useCliStore(useShallow(selectActiveBackgroundAgents));
|
|
887
924
|
const permissionPrompt = useCliStore((state) => state.permissionPrompt);
|
|
888
925
|
const { groups, ungrouped } = useMemo(() => groupJobsByTurn(activeJobs), [activeJobs]);
|
|
889
926
|
if (activeJobs.length === 0) return null;
|
|
@@ -985,6 +1022,7 @@ function renderDiffPreview(preview) {
|
|
|
985
1022
|
}, line);
|
|
986
1023
|
});
|
|
987
1024
|
}
|
|
1025
|
+
const TOOLS_WITH_HIDDEN_ARGS$1 = new Set(["edit_local_file"]);
|
|
988
1026
|
/**
|
|
989
1027
|
* Permission prompt component
|
|
990
1028
|
*
|
|
@@ -992,6 +1030,7 @@ function renderDiffPreview(preview) {
|
|
|
992
1030
|
* Waits indefinitely for user response (like Claude Code).
|
|
993
1031
|
*/
|
|
994
1032
|
function PermissionPrompt({ toolName, toolDescription, args, preview, canBeTrusted, onResponse }) {
|
|
1033
|
+
const hideArgs = TOOLS_WITH_HIDDEN_ARGS$1.has(toolName);
|
|
995
1034
|
const items = canBeTrusted ? [
|
|
996
1035
|
{
|
|
997
1036
|
label: "✓ Allow once",
|
|
@@ -1082,7 +1121,7 @@ function PermissionPrompt({ toolName, toolDescription, args, preview, canBeTrust
|
|
|
1082
1121
|
}, headerText)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Tool: "), /* @__PURE__ */ React.createElement(Text, {
|
|
1083
1122
|
bold: true,
|
|
1084
1123
|
color: "cyan"
|
|
1085
|
-
}, toolName)), toolDescription && /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Action: "), /* @__PURE__ */ React.createElement(Text, null, toolDescription)), /* @__PURE__ */ React.createElement(Box, {
|
|
1124
|
+
}, toolName)), toolDescription && /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Action: "), /* @__PURE__ */ React.createElement(Text, null, toolDescription)), !hideArgs && /* @__PURE__ */ React.createElement(Box, {
|
|
1086
1125
|
marginTop: 1,
|
|
1087
1126
|
flexDirection: "column"
|
|
1088
1127
|
}, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Arguments:"), /* @__PURE__ */ React.createElement(Box, {
|
|
@@ -1409,6 +1448,39 @@ function ReviewGatePrompt({ description, options, recommendation, onResponse })
|
|
|
1409
1448
|
}))), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, noteMode ? note.trim().length === 0 ? "Note required — type a note + Enter to submit, ↑↓ to switch action" : "Type note + Enter to submit, ↑↓ to switch action" : "Press 1-4, y/n, or ↑↓ + Enter")));
|
|
1410
1449
|
}
|
|
1411
1450
|
//#endregion
|
|
1451
|
+
//#region src/components/ExitHandoffPrompt.tsx
|
|
1452
|
+
/**
|
|
1453
|
+
* Single-shot y/n prompt shown on exit when the session is eligible for a
|
|
1454
|
+
* handoff (meaningful content, no existing handoff). Defaults to "yes" on
|
|
1455
|
+
* Enter so the common case (preserve continuity) is one keystroke away.
|
|
1456
|
+
*/
|
|
1457
|
+
function ExitHandoffPrompt({ onResponse }) {
|
|
1458
|
+
const respondedRef = useRef(false);
|
|
1459
|
+
const [responded, setResponded] = useState(false);
|
|
1460
|
+
const respond = useCallback((generate) => {
|
|
1461
|
+
if (respondedRef.current) return;
|
|
1462
|
+
respondedRef.current = true;
|
|
1463
|
+
setResponded(true);
|
|
1464
|
+
onResponse(generate);
|
|
1465
|
+
}, [onResponse]);
|
|
1466
|
+
useInput((input, key) => {
|
|
1467
|
+
if (respondedRef.current) return;
|
|
1468
|
+
const lower = input.toLowerCase();
|
|
1469
|
+
if (lower === "y" || key.return) respond(true);
|
|
1470
|
+
else if (lower === "n" || key.escape) respond(false);
|
|
1471
|
+
}, { isActive: !responded });
|
|
1472
|
+
return /* @__PURE__ */ React.createElement(Box, {
|
|
1473
|
+
flexDirection: "column",
|
|
1474
|
+
borderStyle: "round",
|
|
1475
|
+
borderColor: "cyan",
|
|
1476
|
+
paddingX: 1,
|
|
1477
|
+
marginY: 1
|
|
1478
|
+
}, /* @__PURE__ */ React.createElement(Text, {
|
|
1479
|
+
bold: true,
|
|
1480
|
+
color: "cyan"
|
|
1481
|
+
}, "🤝 Generate a handoff for this session before exiting? (Y/n)"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "The handoff captures key findings, next steps, and open blockers."));
|
|
1482
|
+
}
|
|
1483
|
+
//#endregion
|
|
1412
1484
|
//#region src/components/ConfigEditor.tsx
|
|
1413
1485
|
/**
|
|
1414
1486
|
* Max iterations options: 10, 20, 30, 40, 50, Infinite (null)
|
|
@@ -1549,6 +1621,18 @@ function buildConfigItems(availableModels) {
|
|
|
1549
1621
|
autoCompact: value
|
|
1550
1622
|
}
|
|
1551
1623
|
})
|
|
1624
|
+
}, {
|
|
1625
|
+
key: "showThoughts",
|
|
1626
|
+
label: "Show Thoughts",
|
|
1627
|
+
type: "boolean",
|
|
1628
|
+
getValue: (config) => config.preferences.showThoughts ?? true,
|
|
1629
|
+
setValue: (config, value) => ({
|
|
1630
|
+
...config,
|
|
1631
|
+
preferences: {
|
|
1632
|
+
...config.preferences,
|
|
1633
|
+
showThoughts: value
|
|
1634
|
+
}
|
|
1635
|
+
})
|
|
1552
1636
|
}, {
|
|
1553
1637
|
key: "theme",
|
|
1554
1638
|
label: "Theme",
|
|
@@ -1859,21 +1943,22 @@ function McpViewer({ config, mcpManager, onClose }) {
|
|
|
1859
1943
|
}
|
|
1860
1944
|
//#endregion
|
|
1861
1945
|
//#region src/components/MarkdownRenderer.tsx
|
|
1862
|
-
function MarkdownRenderer({ content }) {
|
|
1946
|
+
function MarkdownRenderer({ content, columns: columnsProp }) {
|
|
1947
|
+
const columns = columnsProp ?? process.stdout.columns ?? 80;
|
|
1863
1948
|
const tokens = marked.lexer(content);
|
|
1864
|
-
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, tokens.map((token, idx) => renderToken(token, idx)));
|
|
1949
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, tokens.map((token, idx) => renderToken(token, idx, columns)));
|
|
1865
1950
|
}
|
|
1866
|
-
function renderToken(token, idx) {
|
|
1951
|
+
function renderToken(token, idx, columns) {
|
|
1867
1952
|
switch (token.type) {
|
|
1868
1953
|
case "heading": return renderHeading(token, idx);
|
|
1869
1954
|
case "code": return renderCodeBlock(token, idx);
|
|
1870
1955
|
case "paragraph": return renderParagraph(token, idx);
|
|
1871
1956
|
case "list": return renderList(token, idx);
|
|
1872
|
-
case "blockquote": return renderBlockquote(token, idx);
|
|
1957
|
+
case "blockquote": return renderBlockquote(token, idx, columns);
|
|
1873
1958
|
case "hr": return /* @__PURE__ */ React.createElement(Text, {
|
|
1874
1959
|
key: idx,
|
|
1875
1960
|
dimColor: true
|
|
1876
|
-
}, "─".repeat(
|
|
1961
|
+
}, "─".repeat(Math.max(1, columns - 4)));
|
|
1877
1962
|
case "space": return null;
|
|
1878
1963
|
default:
|
|
1879
1964
|
if ("text" in token) return /* @__PURE__ */ React.createElement(Text, { key: idx }, token.text);
|
|
@@ -1932,14 +2017,14 @@ function renderListItem(item, idx, ordered, number) {
|
|
|
1932
2017
|
paddingLeft: 2
|
|
1933
2018
|
}, /* @__PURE__ */ React.createElement(Text, null, bullet, " ", parseInlineText(item.text)));
|
|
1934
2019
|
}
|
|
1935
|
-
function renderBlockquote(token, idx) {
|
|
2020
|
+
function renderBlockquote(token, idx, columns) {
|
|
1936
2021
|
return /* @__PURE__ */ React.createElement(Box, {
|
|
1937
2022
|
key: idx,
|
|
1938
2023
|
paddingLeft: 2,
|
|
1939
2024
|
borderStyle: "single",
|
|
1940
2025
|
borderLeft: true,
|
|
1941
2026
|
borderColor: "gray"
|
|
1942
|
-
}, token.tokens.map((t, i) => renderToken(t, i)));
|
|
2027
|
+
}, token.tokens.map((t, i) => renderToken(t, i, columns)));
|
|
1943
2028
|
}
|
|
1944
2029
|
/**
|
|
1945
2030
|
* Parse inline markdown formatting (bold, italic, code, links)
|
|
@@ -1981,34 +2066,45 @@ function truncateValue(value, maxLength) {
|
|
|
1981
2066
|
if (str.length <= maxLength) return str;
|
|
1982
2067
|
return str.slice(0, maxLength) + "...";
|
|
1983
2068
|
}
|
|
2069
|
+
const TOOLS_WITH_HIDDEN_ARGS = new Set(["edit_local_file"]);
|
|
1984
2070
|
/**
|
|
1985
2071
|
* Returns display properties for a message role
|
|
1986
2072
|
*/
|
|
1987
|
-
const MessageItem = React.memo(function MessageItem({ message }) {
|
|
2073
|
+
const MessageItem = React.memo(function MessageItem({ message, showThoughts = true }) {
|
|
1988
2074
|
const isUser = message.role === "user";
|
|
2075
|
+
const { stdout } = useStdout();
|
|
2076
|
+
const terminalCols = stdout?.columns ?? 80;
|
|
2077
|
+
const userPromptText = `❯ ${message.content}`;
|
|
2078
|
+
const paddedUserPromptText = userPromptText.length >= terminalCols - 2 ? userPromptText : userPromptText.padEnd(terminalCols - 2);
|
|
1989
2079
|
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, isUser && message.content && /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, {
|
|
1990
2080
|
backgroundColor: "whiteBright",
|
|
1991
|
-
color: "black"
|
|
1992
|
-
|
|
2081
|
+
color: "black",
|
|
2082
|
+
wrap: "truncate-end"
|
|
2083
|
+
}, paddedUserPromptText)), !isUser && message.metadata?.steps && message.metadata.steps.filter((s) => showThoughts && s.type === "thought" || s.type === "action").length > 0 && /* @__PURE__ */ React.createElement(Box, {
|
|
1993
2084
|
paddingLeft: 2,
|
|
1994
2085
|
flexDirection: "column",
|
|
1995
2086
|
marginBottom: 1
|
|
1996
2087
|
}, message.metadata.steps.map((step, idx) => {
|
|
1997
|
-
if (step.type === "thought")
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2088
|
+
if (step.type === "thought") {
|
|
2089
|
+
if (!showThoughts) return null;
|
|
2090
|
+
return /* @__PURE__ */ React.createElement(Box, {
|
|
2091
|
+
key: idx,
|
|
2092
|
+
marginTop: 1,
|
|
2093
|
+
flexDirection: "column"
|
|
2094
|
+
}, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, `💭 ${step.content}`));
|
|
2095
|
+
}
|
|
2002
2096
|
if (step.type === "action") {
|
|
2003
2097
|
const toolName = step.metadata?.toolName || "unknown";
|
|
2098
|
+
const formattedToolName = toolName.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2004
2099
|
const toolInput = step.metadata?.toolInput;
|
|
2100
|
+
const hideArgs = TOOLS_WITH_HIDDEN_ARGS.has(toolName);
|
|
2005
2101
|
const observationStep = message.metadata?.steps?.[idx + 1];
|
|
2006
2102
|
const result = observationStep?.type === "observation" ? observationStep.content : null;
|
|
2007
2103
|
return /* @__PURE__ */ React.createElement(Box, {
|
|
2008
2104
|
key: idx,
|
|
2009
2105
|
marginTop: 1,
|
|
2010
2106
|
flexDirection: "column"
|
|
2011
|
-
}, /* @__PURE__ */ React.createElement(Text, { color: "yellow" },
|
|
2107
|
+
}, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, formattedToolName), toolInput && !hideArgs && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` • ${truncateValue(toolInput, 100)}`)), result && /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, `Result: ${truncateValue(result, 200)}`)));
|
|
2012
2108
|
}
|
|
2013
2109
|
return null;
|
|
2014
2110
|
}).filter(Boolean)), !isUser && message.content !== "..." && /* @__PURE__ */ React.createElement(Box, {
|
|
@@ -2022,9 +2118,9 @@ const MessageItem = React.memo(function MessageItem({ message }) {
|
|
|
2022
2118
|
//#endregion
|
|
2023
2119
|
//#region src/components/App.tsx
|
|
2024
2120
|
function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPermissionResponse, onUserQuestionResponse, onReviewGateResponse, onImageDetected, commandHistory = [], commands = [], config, availableModels = [], onSaveConfig, prefillInput, onPrefillConsumed, mcpManager }) {
|
|
2025
|
-
const messages = useCliStore((state) => state.session?.messages || []);
|
|
2121
|
+
const messages = useCliStore(useShallow((state) => state.session?.messages || []));
|
|
2026
2122
|
const pendingMessages = useCliStore((state) => state.pendingMessages);
|
|
2027
|
-
const
|
|
2123
|
+
const messageQueue = useCliStore((state) => state.messageQueue);
|
|
2028
2124
|
const currentModel = useCliStore((state) => state.session?.model || ChatModels.CLAUDE_4_5_SONNET);
|
|
2029
2125
|
const totalTokens = useCliStore((state) => state.session?.metadata.totalTokens || 0);
|
|
2030
2126
|
const totalCredits = useCliStore((state) => state.session?.metadata.totalCredits);
|
|
@@ -2032,6 +2128,8 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2032
2128
|
const permissionPrompt = useCliStore((state) => state.permissionPrompt);
|
|
2033
2129
|
const userQuestionPrompt = useCliStore((state) => state.userQuestionPrompt);
|
|
2034
2130
|
const reviewGatePrompt = useCliStore((state) => state.reviewGatePrompt);
|
|
2131
|
+
const exitHandoffPrompt = useCliStore((state) => state.exitHandoffPrompt);
|
|
2132
|
+
const setExitHandoffPrompt = useCliStore((state) => state.setExitHandoffPrompt);
|
|
2035
2133
|
const showConfigEditor = useCliStore((state) => state.showConfigEditor);
|
|
2036
2134
|
const setShowConfigEditor = useCliStore((state) => state.setShowConfigEditor);
|
|
2037
2135
|
const showMcpViewer = useCliStore((state) => state.showMcpViewer);
|
|
@@ -2056,6 +2154,9 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2056
2154
|
if (key.tab && key.shift) cycleInteractionMode();
|
|
2057
2155
|
});
|
|
2058
2156
|
const [isBashMode, setIsBashMode] = useState(false);
|
|
2157
|
+
const showThoughts = config?.preferences.showThoughts ?? true;
|
|
2158
|
+
const { stdout } = useStdout();
|
|
2159
|
+
const terminalCols = stdout?.columns ?? 80;
|
|
2059
2160
|
const handleSubmit = React.useCallback(async (input) => {
|
|
2060
2161
|
const trimmed = input.trim();
|
|
2061
2162
|
if (!trimmed) return;
|
|
@@ -2064,17 +2165,21 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2064
2165
|
await onCommand(command, args);
|
|
2065
2166
|
return;
|
|
2066
2167
|
}
|
|
2168
|
+
let messageToSend = trimmed;
|
|
2169
|
+
if (hasFileReferences(trimmed)) {
|
|
2170
|
+
const processed = await processFileReferences(trimmed);
|
|
2171
|
+
messageToSend = processed.content;
|
|
2172
|
+
if (processed.errors.length > 0) {
|
|
2173
|
+
const errorBlock = processed.errors.map((e) => `[Warning: ${e}]`).join("\n");
|
|
2174
|
+
messageToSend = `${messageToSend}\n\n${errorBlock}`;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (useCliStore.getState().isThinking) {
|
|
2178
|
+
useCliStore.getState().enqueueMessage(messageToSend);
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2067
2181
|
setIsThinking(true);
|
|
2068
2182
|
try {
|
|
2069
|
-
let messageToSend = trimmed;
|
|
2070
|
-
if (hasFileReferences(trimmed)) {
|
|
2071
|
-
const processed = await processFileReferences(trimmed);
|
|
2072
|
-
messageToSend = processed.content;
|
|
2073
|
-
if (processed.errors.length > 0) {
|
|
2074
|
-
const errorBlock = processed.errors.map((e) => `[Warning: ${e}]`).join("\n");
|
|
2075
|
-
messageToSend = `${messageToSend}\n\n${errorBlock}`;
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
2183
|
await onMessage(messageToSend);
|
|
2079
2184
|
} finally {
|
|
2080
2185
|
setIsThinking(false);
|
|
@@ -2084,7 +2189,10 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2084
2189
|
onCommand,
|
|
2085
2190
|
setIsThinking
|
|
2086
2191
|
]);
|
|
2087
|
-
return /* @__PURE__ */ React.createElement(Box, {
|
|
2192
|
+
return /* @__PURE__ */ React.createElement(Box, {
|
|
2193
|
+
flexDirection: "column",
|
|
2194
|
+
height: "100%"
|
|
2195
|
+
}, showConfigEditor && config && onSaveConfig ? /* @__PURE__ */ React.createElement(Box, {
|
|
2088
2196
|
flexDirection: "column",
|
|
2089
2197
|
paddingX: 1
|
|
2090
2198
|
}, /* @__PURE__ */ React.createElement(ConfigEditor, {
|
|
@@ -2103,11 +2211,17 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2103
2211
|
key: message.id,
|
|
2104
2212
|
flexDirection: "column",
|
|
2105
2213
|
paddingX: 1
|
|
2106
|
-
}, /* @__PURE__ */ React.createElement(MessageItem, {
|
|
2214
|
+
}, /* @__PURE__ */ React.createElement(MessageItem, {
|
|
2215
|
+
message,
|
|
2216
|
+
showThoughts
|
|
2217
|
+
}))), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, pendingMessages.map((message) => /* @__PURE__ */ React.createElement(Box, {
|
|
2107
2218
|
key: message.id,
|
|
2108
2219
|
flexDirection: "column",
|
|
2109
2220
|
paddingX: 1
|
|
2110
|
-
}, /* @__PURE__ */ React.createElement(MessageItem, {
|
|
2221
|
+
}, /* @__PURE__ */ React.createElement(MessageItem, {
|
|
2222
|
+
message,
|
|
2223
|
+
showThoughts
|
|
2224
|
+
})))), permissionPrompt && /* @__PURE__ */ React.createElement(Box, {
|
|
2111
2225
|
key: permissionPrompt.id,
|
|
2112
2226
|
flexDirection: "column",
|
|
2113
2227
|
paddingX: 1
|
|
@@ -2133,88 +2247,59 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2133
2247
|
options: reviewGatePrompt.options,
|
|
2134
2248
|
recommendation: reviewGatePrompt.recommendation,
|
|
2135
2249
|
onResponse: (response) => onReviewGateResponse(response, reviewGatePrompt.id)
|
|
2136
|
-
})),
|
|
2250
|
+
})), exitHandoffPrompt && /* @__PURE__ */ React.createElement(Box, {
|
|
2251
|
+
key: exitHandoffPrompt.id,
|
|
2252
|
+
flexDirection: "column",
|
|
2253
|
+
paddingX: 1
|
|
2254
|
+
}, /* @__PURE__ */ React.createElement(ExitHandoffPrompt, { onResponse: (generate) => {
|
|
2255
|
+
const target = exitHandoffPrompt;
|
|
2256
|
+
setExitHandoffPrompt(null);
|
|
2257
|
+
target.resolve(generate);
|
|
2258
|
+
} })), !permissionPrompt && !userQuestionPrompt && !reviewGatePrompt && !exitHandoffPrompt && /* @__PURE__ */ React.createElement(AgentThinking, null), /* @__PURE__ */ React.createElement(BackgroundAgentStatus, null), /* @__PURE__ */ React.createElement(CompletedGroupNotification, null), exitRequested && /* @__PURE__ */ React.createElement(Box, {
|
|
2137
2259
|
paddingX: 1,
|
|
2138
2260
|
marginBottom: 1
|
|
2139
2261
|
}, /* @__PURE__ */ React.createElement(Text, {
|
|
2140
2262
|
color: "yellow",
|
|
2141
2263
|
bold: true
|
|
2142
|
-
}, "Press Ctrl+C again to exit")), /* @__PURE__ */ React.createElement(Box, {
|
|
2264
|
+
}, "Press Ctrl+C again to exit"))), /* @__PURE__ */ React.createElement(Box, {
|
|
2265
|
+
flexDirection: "column",
|
|
2266
|
+
flexShrink: 0
|
|
2267
|
+
}, messageQueue.length > 0 && /* @__PURE__ */ React.createElement(Box, {
|
|
2268
|
+
flexDirection: "column",
|
|
2269
|
+
paddingX: 1,
|
|
2270
|
+
marginBottom: 1
|
|
2271
|
+
}, messageQueue.map((queuedMessage, idx) => {
|
|
2272
|
+
const rowText = `❯ ${queuedMessage}`;
|
|
2273
|
+
const padded = rowText.length >= terminalCols - 2 ? rowText : rowText.padEnd(terminalCols - 2);
|
|
2274
|
+
return /* @__PURE__ */ React.createElement(Text, {
|
|
2275
|
+
key: idx,
|
|
2276
|
+
backgroundColor: "gray",
|
|
2277
|
+
color: "white",
|
|
2278
|
+
wrap: "truncate-end"
|
|
2279
|
+
}, padded);
|
|
2280
|
+
})), /* @__PURE__ */ React.createElement(Box, {
|
|
2143
2281
|
borderStyle: "single",
|
|
2144
2282
|
borderColor: isBashMode ? "yellow" : "cyan",
|
|
2145
|
-
|
|
2283
|
+
borderTop: true,
|
|
2284
|
+
borderBottom: true
|
|
2146
2285
|
}, /* @__PURE__ */ React.createElement(InputPrompt, {
|
|
2147
2286
|
onSubmit: handleSubmit,
|
|
2148
2287
|
onBashCommand,
|
|
2149
2288
|
onImageDetected,
|
|
2150
|
-
disabled:
|
|
2289
|
+
disabled: !!permissionPrompt || !!userQuestionPrompt || !!reviewGatePrompt || !!exitHandoffPrompt,
|
|
2151
2290
|
history: commandHistory,
|
|
2152
2291
|
commands,
|
|
2153
2292
|
prefillInput,
|
|
2154
2293
|
onPrefillConsumed,
|
|
2155
2294
|
onBashModeChange: setIsBashMode
|
|
2156
2295
|
})), /* @__PURE__ */ React.createElement(StatusBar, {
|
|
2157
|
-
|
|
2296
|
+
isBashMode,
|
|
2158
2297
|
model: currentModel,
|
|
2159
2298
|
tokenUsage: totalTokens,
|
|
2160
2299
|
creditsUsage: totalCredits
|
|
2161
|
-
})));
|
|
2300
|
+
}))));
|
|
2162
2301
|
}
|
|
2163
2302
|
//#endregion
|
|
2164
|
-
//#region src/components/MessageList.tsx
|
|
2165
|
-
/**
|
|
2166
|
-
* Strip <think>...</think> tags from message content for cleaner display
|
|
2167
|
-
* The agent uses these tags internally for reasoning, but users don't need to see them
|
|
2168
|
-
*/
|
|
2169
|
-
function stripThinkingTags(content) {
|
|
2170
|
-
return content.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
2171
|
-
}
|
|
2172
|
-
React.memo(function MessageList({ messages }) {
|
|
2173
|
-
if (messages.length === 0) return /* @__PURE__ */ React.createElement(Box, { paddingY: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "No messages yet. Type a message to start!"));
|
|
2174
|
-
return /* @__PURE__ */ React.createElement(Box, {
|
|
2175
|
-
flexDirection: "column",
|
|
2176
|
-
gap: 1,
|
|
2177
|
-
paddingY: 1
|
|
2178
|
-
}, messages.map((message, index) => /* @__PURE__ */ React.createElement(Box, {
|
|
2179
|
-
key: index,
|
|
2180
|
-
flexDirection: "column",
|
|
2181
|
-
marginBottom: 1
|
|
2182
|
-
}, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, {
|
|
2183
|
-
bold: true,
|
|
2184
|
-
color: message.role === "user" ? "cyan" : "green"
|
|
2185
|
-
}, message.role === "user" ? "👤 You" : "🤖 Assistant"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " • ", new Date(message.timestamp).toLocaleTimeString())), /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, message.metadata?.permissionDenied ? /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "⚠️ ", stripThinkingTags(message.content)) : /* @__PURE__ */ React.createElement(Text, null, stripThinkingTags(message.content))), message.metadata?.steps && message.metadata.steps.length > 0 && /* @__PURE__ */ React.createElement(Box, {
|
|
2186
|
-
paddingLeft: 2,
|
|
2187
|
-
marginTop: 1,
|
|
2188
|
-
flexDirection: "column"
|
|
2189
|
-
}, /* @__PURE__ */ React.createElement(Text, {
|
|
2190
|
-
dimColor: true,
|
|
2191
|
-
bold: true
|
|
2192
|
-
}, "🔧 Agent Reasoning Trace (", message.metadata.steps.filter((s) => s.type === "action").length, " tools used,", " ", message.metadata.steps.length, " total steps)", message.metadata.tokenUsage && ` • ${message.metadata.tokenUsage.total} tokens`), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Step types: ", message.metadata.steps.map((s) => s.type).join(", ")), message.metadata.steps.map((step, idx) => {
|
|
2193
|
-
if (step.type === "thought") return /* @__PURE__ */ React.createElement(Box, {
|
|
2194
|
-
key: idx,
|
|
2195
|
-
paddingLeft: 2,
|
|
2196
|
-
marginTop: 1,
|
|
2197
|
-
flexDirection: "column"
|
|
2198
|
-
}, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "💭 Thought:"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` ${step.content.slice(0, 200)}${step.content.length > 200 ? "..." : ""}`));
|
|
2199
|
-
if (step.type === "action") {
|
|
2200
|
-
const toolName = step.metadata?.toolName || "unknown";
|
|
2201
|
-
const toolInput = step.metadata?.toolInput;
|
|
2202
|
-
const observationStep = message.metadata.steps[idx + 1];
|
|
2203
|
-
const result = observationStep?.type === "observation" ? observationStep.content : null;
|
|
2204
|
-
return /* @__PURE__ */ React.createElement(Box, {
|
|
2205
|
-
key: idx,
|
|
2206
|
-
paddingLeft: 2,
|
|
2207
|
-
marginTop: 1,
|
|
2208
|
-
flexDirection: "column"
|
|
2209
|
-
}, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "🔧 Action: ", toolName), toolInput && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` Input: ${typeof toolInput === "string" ? toolInput : JSON.stringify(toolInput).slice(0, 100)}`), result && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` Result: ${typeof result === "string" ? result.slice(0, 200) : JSON.stringify(result).slice(0, 200)}${(typeof result === "string" ? result.length : JSON.stringify(result).length) > 200 ? "..." : ""}`));
|
|
2210
|
-
}
|
|
2211
|
-
return null;
|
|
2212
|
-
}).filter(Boolean)), message.metadata?.tokenUsage && (!message.metadata.steps || message.metadata.steps.length === 0) && /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, `${message.metadata.tokenUsage.total} tokens`)))));
|
|
2213
|
-
}, (prevProps, nextProps) => {
|
|
2214
|
-
if (prevProps.messages.length !== nextProps.messages.length) return false;
|
|
2215
|
-
return prevProps.messages === nextProps.messages;
|
|
2216
|
-
});
|
|
2217
|
-
//#endregion
|
|
2218
2303
|
//#region src/components/TrustLocationSelector.tsx
|
|
2219
2304
|
function TrustLocationSelector({ inProject, onSelect, onCancel }) {
|
|
2220
2305
|
const items = [];
|
|
@@ -2551,89 +2636,6 @@ function getTokenCounter() {
|
|
|
2551
2636
|
if (!tokenCounter) tokenCounter = new TokenCounter();
|
|
2552
2637
|
return tokenCounter;
|
|
2553
2638
|
}
|
|
2554
|
-
//#endregion
|
|
2555
|
-
//#region src/utils/compaction.ts
|
|
2556
|
-
/**
|
|
2557
|
-
* Build a prompt for the LLM to summarize the conversation.
|
|
2558
|
-
*
|
|
2559
|
-
* Preserves the most recent exchanges to maintain conversational flow,
|
|
2560
|
-
* while summarizing older messages to reduce context size.
|
|
2561
|
-
*
|
|
2562
|
-
* @param messages - All messages in the current session
|
|
2563
|
-
* @param options - Compaction options
|
|
2564
|
-
* @returns The summarization prompt and messages to preserve
|
|
2565
|
-
*/
|
|
2566
|
-
function buildCompactionPrompt(messages, options = {}) {
|
|
2567
|
-
const preserveCount = (options.preserveRecentExchanges ?? 2) * 2;
|
|
2568
|
-
if (messages.length <= preserveCount) return {
|
|
2569
|
-
prompt: "",
|
|
2570
|
-
preservedMessages: messages
|
|
2571
|
-
};
|
|
2572
|
-
const messagesToSummarize = messages.slice(0, -preserveCount);
|
|
2573
|
-
const preservedMessages = messages.slice(-preserveCount);
|
|
2574
|
-
let prompt = `You are summarizing a conversation for context continuity. Create a concise summary that captures:
|
|
2575
|
-
|
|
2576
|
-
- Key decisions made
|
|
2577
|
-
- Important context established
|
|
2578
|
-
- Files and code discussed
|
|
2579
|
-
- Current task state
|
|
2580
|
-
- Any pending items or next steps
|
|
2581
|
-
|
|
2582
|
-
`;
|
|
2583
|
-
if (options.claudeMdInstructions) prompt += `Project-specific compaction instructions:\n${options.claudeMdInstructions}\n\n`;
|
|
2584
|
-
if (options.userInstructions) prompt += `Additional focus: ${options.userInstructions}\n\n`;
|
|
2585
|
-
prompt += `CONVERSATION TO SUMMARIZE:\n\n`;
|
|
2586
|
-
const roleLabels = {
|
|
2587
|
-
user: "User",
|
|
2588
|
-
assistant: "Assistant",
|
|
2589
|
-
system: "System"
|
|
2590
|
-
};
|
|
2591
|
-
for (const msg of messagesToSummarize) {
|
|
2592
|
-
const roleLabel = roleLabels[msg.role] || "System";
|
|
2593
|
-
const content = msg.content.length > 2e3 ? msg.content.slice(0, 2e3) + "...[truncated]" : msg.content;
|
|
2594
|
-
prompt += `**${roleLabel}:** ${content}\n\n`;
|
|
2595
|
-
}
|
|
2596
|
-
prompt += `\nProvide a concise summary (aim for 500-1000 words) that an AI assistant can use to continue this conversation with full context.`;
|
|
2597
|
-
return {
|
|
2598
|
-
prompt,
|
|
2599
|
-
preservedMessages
|
|
2600
|
-
};
|
|
2601
|
-
}
|
|
2602
|
-
/**
|
|
2603
|
-
* Create a new compacted session from an original session.
|
|
2604
|
-
*
|
|
2605
|
-
* The new session contains:
|
|
2606
|
-
* 1. A system message with the conversation summary
|
|
2607
|
-
* 2. The preserved recent messages
|
|
2608
|
-
*
|
|
2609
|
-
* @param originalSession - The session being compacted
|
|
2610
|
-
* @param summary - The LLM-generated summary of older messages
|
|
2611
|
-
* @param preservedMessages - Recent messages to keep verbatim
|
|
2612
|
-
* @returns A new session with compacted context
|
|
2613
|
-
*/
|
|
2614
|
-
function createCompactedSession(originalSession, summary, preservedMessages) {
|
|
2615
|
-
const summaryMessage = {
|
|
2616
|
-
id: v4(),
|
|
2617
|
-
role: "user",
|
|
2618
|
-
content: `[Previous conversation summary]\n\n${summary}`,
|
|
2619
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2620
|
-
};
|
|
2621
|
-
return {
|
|
2622
|
-
id: v4(),
|
|
2623
|
-
name: `${originalSession.name} (compacted)`,
|
|
2624
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2625
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2626
|
-
model: originalSession.model,
|
|
2627
|
-
messages: [summaryMessage, ...preservedMessages],
|
|
2628
|
-
metadata: {
|
|
2629
|
-
totalTokens: 0,
|
|
2630
|
-
totalCost: 0,
|
|
2631
|
-
toolCallCount: 0,
|
|
2632
|
-
compactedFrom: originalSession.id,
|
|
2633
|
-
...originalSession.metadata.workflow ? { workflow: originalSession.metadata.workflow } : {}
|
|
2634
|
-
}
|
|
2635
|
-
};
|
|
2636
|
-
}
|
|
2637
2639
|
/**
|
|
2638
2640
|
* Prefix tag used to mark a system message as an injected handoff. Kept as a
|
|
2639
2641
|
* single source of truth so the dedup-on-resume check and the system-message
|
|
@@ -2809,28 +2811,127 @@ function appendSection(lines, heading, items) {
|
|
|
2809
2811
|
lines.push("");
|
|
2810
2812
|
}
|
|
2811
2813
|
/**
|
|
2812
|
-
* True when a message is a previously-injected handoff
|
|
2814
|
+
* True when a message is a previously-injected handoff message.
|
|
2813
2815
|
* Used to deduplicate handoff injections across save/resume cycles.
|
|
2816
|
+
*
|
|
2817
|
+
* Stored as `user` (not `system`) so it survives the user/assistant filter
|
|
2818
|
+
* applied to `previousMessages` before each agent.run() call — otherwise the
|
|
2819
|
+
* handoff would be persisted but never reach the LLM.
|
|
2814
2820
|
*/
|
|
2815
2821
|
function isInjectedHandoff(message) {
|
|
2816
|
-
return message.role === "
|
|
2822
|
+
return message.role === "user" && message.content.startsWith("[Session handoff from previous session]");
|
|
2817
2823
|
}
|
|
2818
2824
|
/**
|
|
2819
|
-
* Return a new message list with the handoff prepended as a
|
|
2825
|
+
* Return a new message list with the handoff prepended as a user message.
|
|
2820
2826
|
* Any previously-injected handoff anywhere in the list is removed so the
|
|
2821
2827
|
* message list stays stable across repeated save/resume cycles. We scan the
|
|
2822
2828
|
* whole list rather than just index 0 because compaction can prepend a
|
|
2823
|
-
* summary
|
|
2829
|
+
* summary message, pushing the prior handoff to a later index.
|
|
2824
2830
|
*/
|
|
2825
2831
|
function injectHandoffMessage(messages, handoff) {
|
|
2826
2832
|
return [{
|
|
2827
2833
|
id: v4(),
|
|
2828
|
-
role: "
|
|
2834
|
+
role: "user",
|
|
2829
2835
|
content: buildHandoffSystemMessage(handoff),
|
|
2830
2836
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2831
2837
|
}, ...messages.filter((m) => !isInjectedHandoff(m))];
|
|
2832
2838
|
}
|
|
2833
2839
|
//#endregion
|
|
2840
|
+
//#region src/utils/compaction.ts
|
|
2841
|
+
/**
|
|
2842
|
+
* Build a prompt for the LLM to summarize the conversation.
|
|
2843
|
+
*
|
|
2844
|
+
* Preserves the most recent exchanges to maintain conversational flow,
|
|
2845
|
+
* while summarizing older messages to reduce context size.
|
|
2846
|
+
*
|
|
2847
|
+
* @param messages - All messages in the current session
|
|
2848
|
+
* @param options - Compaction options
|
|
2849
|
+
* @returns The summarization prompt and messages to preserve
|
|
2850
|
+
*/
|
|
2851
|
+
function buildCompactionPrompt(messages, options = {}) {
|
|
2852
|
+
const preserveCount = (options.preserveRecentExchanges ?? 2) * 2;
|
|
2853
|
+
if (messages.length <= preserveCount) return {
|
|
2854
|
+
prompt: "",
|
|
2855
|
+
preservedMessages: messages
|
|
2856
|
+
};
|
|
2857
|
+
const messagesToSummarize = messages.slice(0, -preserveCount);
|
|
2858
|
+
const preservedMessages = messages.slice(-preserveCount);
|
|
2859
|
+
let prompt = `You are summarizing a conversation for context continuity. Create a concise summary that captures:
|
|
2860
|
+
|
|
2861
|
+
- Key decisions made
|
|
2862
|
+
- Important context established
|
|
2863
|
+
- Files and code discussed
|
|
2864
|
+
- Current task state
|
|
2865
|
+
- Any pending items or next steps
|
|
2866
|
+
|
|
2867
|
+
`;
|
|
2868
|
+
if (options.claudeMdInstructions) prompt += `Project-specific compaction instructions:\n${options.claudeMdInstructions}\n\n`;
|
|
2869
|
+
if (options.userInstructions) prompt += `Additional focus: ${options.userInstructions}\n\n`;
|
|
2870
|
+
prompt += `CONVERSATION TO SUMMARIZE:\n\n`;
|
|
2871
|
+
const roleLabels = {
|
|
2872
|
+
user: "User",
|
|
2873
|
+
assistant: "Assistant",
|
|
2874
|
+
system: "System"
|
|
2875
|
+
};
|
|
2876
|
+
for (const msg of messagesToSummarize) {
|
|
2877
|
+
const roleLabel = roleLabels[msg.role] || "System";
|
|
2878
|
+
const content = msg.content.length > 2e3 ? msg.content.slice(0, 2e3) + "...[truncated]" : msg.content;
|
|
2879
|
+
prompt += `**${roleLabel}:** ${content}\n\n`;
|
|
2880
|
+
}
|
|
2881
|
+
prompt += `\nProvide a concise summary (aim for 500-1000 words) that an AI assistant can use to continue this conversation with full context.`;
|
|
2882
|
+
return {
|
|
2883
|
+
prompt,
|
|
2884
|
+
preservedMessages
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Create a new compacted session from an original session.
|
|
2889
|
+
*
|
|
2890
|
+
* The new session contains:
|
|
2891
|
+
* 1. A system message with the conversation summary
|
|
2892
|
+
* 2. The preserved recent messages
|
|
2893
|
+
*
|
|
2894
|
+
* @param originalSession - The session being compacted
|
|
2895
|
+
* @param summary - The LLM-generated summary of older messages
|
|
2896
|
+
* @param preservedMessages - Recent messages to keep verbatim
|
|
2897
|
+
* @returns A new session with compacted context
|
|
2898
|
+
*/
|
|
2899
|
+
function createCompactedSession(originalSession, summary, preservedMessages) {
|
|
2900
|
+
const summaryMessage = {
|
|
2901
|
+
id: v4(),
|
|
2902
|
+
role: "user",
|
|
2903
|
+
content: `[Previous conversation summary]\n\n${summary}`,
|
|
2904
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2905
|
+
};
|
|
2906
|
+
const handoff = originalSession.metadata.workflow?.handoff;
|
|
2907
|
+
const handoffMessage = handoff ? {
|
|
2908
|
+
id: v4(),
|
|
2909
|
+
role: "user",
|
|
2910
|
+
content: buildHandoffSystemMessage(handoff),
|
|
2911
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2912
|
+
} : null;
|
|
2913
|
+
const messages = handoffMessage ? [
|
|
2914
|
+
handoffMessage,
|
|
2915
|
+
summaryMessage,
|
|
2916
|
+
...preservedMessages
|
|
2917
|
+
] : [summaryMessage, ...preservedMessages];
|
|
2918
|
+
return {
|
|
2919
|
+
id: v4(),
|
|
2920
|
+
name: `${originalSession.name} (compacted)`,
|
|
2921
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2922
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2923
|
+
model: originalSession.model,
|
|
2924
|
+
messages,
|
|
2925
|
+
metadata: {
|
|
2926
|
+
totalTokens: 0,
|
|
2927
|
+
totalCost: 0,
|
|
2928
|
+
toolCallCount: 0,
|
|
2929
|
+
compactedFrom: originalSession.id,
|
|
2930
|
+
...originalSession.metadata.workflow ? { workflow: originalSession.metadata.workflow } : {}
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
//#endregion
|
|
2834
2935
|
//#region src/utils/imageRenderer.ts
|
|
2835
2936
|
/**
|
|
2836
2937
|
* Manages image placeholder rendering in conversation
|
|
@@ -5342,7 +5443,9 @@ function summarizeUserQuestion(payload) {
|
|
|
5342
5443
|
return first ? first.slice(0, 240) : void 0;
|
|
5343
5444
|
}
|
|
5344
5445
|
let exitTimestamp = null;
|
|
5446
|
+
let exitInProgress = false;
|
|
5345
5447
|
const EXIT_TIMEOUT_MS = 2e3;
|
|
5448
|
+
const EXIT_HANDOFF_PROMPT_TIMEOUT_MS = 3e4;
|
|
5346
5449
|
let usageCache = null;
|
|
5347
5450
|
function CliApp() {
|
|
5348
5451
|
const { exit } = useApp();
|
|
@@ -5440,14 +5543,20 @@ function CliApp() {
|
|
|
5440
5543
|
abortController: null
|
|
5441
5544
|
}));
|
|
5442
5545
|
useCliStore.getState().setIsThinking(false);
|
|
5443
|
-
|
|
5546
|
+
useCliStore.getState().clearMessageQueue();
|
|
5547
|
+
} else useCliStore.getState().clearMessageQueue();
|
|
5444
5548
|
return;
|
|
5445
5549
|
}
|
|
5446
5550
|
if (key.ctrl && input === "c") {
|
|
5551
|
+
if (exitInProgress) return;
|
|
5447
5552
|
const now = Date.now();
|
|
5448
5553
|
if (exitTimestamp && now - exitTimestamp < EXIT_TIMEOUT_MS) {
|
|
5449
5554
|
logger.debug("[EXIT] Second Ctrl+C - cleaning up and exiting...");
|
|
5450
|
-
|
|
5555
|
+
exitInProgress = true;
|
|
5556
|
+
exitTimestamp = null;
|
|
5557
|
+
maybePromptExitHandoff().catch((err) => {
|
|
5558
|
+
logger.debug(`[EXIT] Handoff prompt error: ${err instanceof Error ? err.message : String(err)}`);
|
|
5559
|
+
}).then(() => performCleanup()).then(() => {
|
|
5451
5560
|
exit();
|
|
5452
5561
|
});
|
|
5453
5562
|
} else {
|
|
@@ -6421,7 +6530,8 @@ function CliApp() {
|
|
|
6421
6530
|
}
|
|
6422
6531
|
};
|
|
6423
6532
|
const handleMessage = async (message) => {
|
|
6424
|
-
|
|
6533
|
+
const storeSession = useCliStore.getState().session;
|
|
6534
|
+
if (!state.agent || !storeSession) {
|
|
6425
6535
|
console.error("❌ CLI failed to initialize. Try restarting b4m.\n");
|
|
6426
6536
|
return;
|
|
6427
6537
|
}
|
|
@@ -6438,7 +6548,7 @@ function CliApp() {
|
|
|
6438
6548
|
await state.commandHistoryStore.add(message);
|
|
6439
6549
|
setCommandHistory(await state.commandHistoryStore.list());
|
|
6440
6550
|
const config = state.config;
|
|
6441
|
-
let activeSession =
|
|
6551
|
+
let activeSession = storeSession;
|
|
6442
6552
|
if (config?.preferences.autoCompact !== false && activeSession.messages.length >= 6) {
|
|
6443
6553
|
const tokenCounter = getTokenCounter();
|
|
6444
6554
|
const threshold = tokenCounter.getContextWindow(activeSession.model, state.availableModels) * .8;
|
|
@@ -6615,6 +6725,15 @@ function CliApp() {
|
|
|
6615
6725
|
type: "status",
|
|
6616
6726
|
status: wasAborted ? "idle" : "awaiting_input"
|
|
6617
6727
|
});
|
|
6728
|
+
if (!wasAborted) {
|
|
6729
|
+
const queued = useCliStore.getState().dequeueAllMessages();
|
|
6730
|
+
if (queued.length > 0) {
|
|
6731
|
+
const combined = queued.join("\n\n");
|
|
6732
|
+
setImmediate(() => {
|
|
6733
|
+
handleMessage(combined);
|
|
6734
|
+
});
|
|
6735
|
+
}
|
|
6736
|
+
}
|
|
6618
6737
|
}
|
|
6619
6738
|
};
|
|
6620
6739
|
handleMessageRef.current = handleMessage;
|
|
@@ -6852,6 +6971,118 @@ function CliApp() {
|
|
|
6852
6971
|
useCliStore.getState().setIsThinking(false);
|
|
6853
6972
|
}
|
|
6854
6973
|
};
|
|
6974
|
+
/**
|
|
6975
|
+
* If the active session is eligible for a handoff, prompt the user to
|
|
6976
|
+
* generate one before exiting. Eligibility: session exists, has at least
|
|
6977
|
+
* SHORT_SESSION_THRESHOLD messages, no handoff already, and an agent is
|
|
6978
|
+
* available to run the generation.
|
|
6979
|
+
*
|
|
6980
|
+
* `generateHandoff` mutates the passed-in session in place, then we save
|
|
6981
|
+
* that exact reference. We don't rely on the trailing `performCleanup()` to
|
|
6982
|
+
* persist the change because `state.session` may have been replaced while we
|
|
6983
|
+
* waited for the prompt (e.g. by a background-agent update), making the
|
|
6984
|
+
* mutated snapshot orphaned. Best-effort: any failure is logged and
|
|
6985
|
+
* swallowed so it never blocks exit.
|
|
6986
|
+
*/
|
|
6987
|
+
const maybePromptExitHandoff = async () => {
|
|
6988
|
+
const session = state.session;
|
|
6989
|
+
if (!session) return;
|
|
6990
|
+
if (!state.agent) return;
|
|
6991
|
+
if (session.messages.length < 4) return;
|
|
6992
|
+
if (session.metadata.workflow?.handoff) return;
|
|
6993
|
+
const promptId = v4();
|
|
6994
|
+
let timer;
|
|
6995
|
+
if (!await new Promise((resolve) => {
|
|
6996
|
+
let settled = false;
|
|
6997
|
+
const settle = (value) => {
|
|
6998
|
+
if (settled) return;
|
|
6999
|
+
settled = true;
|
|
7000
|
+
if (timer) clearTimeout(timer);
|
|
7001
|
+
resolve(value);
|
|
7002
|
+
};
|
|
7003
|
+
useCliStore.getState().setExitHandoffPrompt({
|
|
7004
|
+
id: promptId,
|
|
7005
|
+
resolve: settle
|
|
7006
|
+
});
|
|
7007
|
+
timer = setTimeout(() => {
|
|
7008
|
+
logger.debug("[EXIT] Handoff prompt timed out — defaulting to no handoff");
|
|
7009
|
+
if (useCliStore.getState().exitHandoffPrompt?.id === promptId) useCliStore.getState().setExitHandoffPrompt(null);
|
|
7010
|
+
settle(false);
|
|
7011
|
+
}, EXIT_HANDOFF_PROMPT_TIMEOUT_MS);
|
|
7012
|
+
})) return;
|
|
7013
|
+
try {
|
|
7014
|
+
if (await generateHandoff(session)) {
|
|
7015
|
+
await state.sessionStore.save(session);
|
|
7016
|
+
console.log("🤝 Handoff generated.");
|
|
7017
|
+
}
|
|
7018
|
+
} catch (err) {
|
|
7019
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
7020
|
+
logger.debug(`[EXIT] Handoff generation/save failed: ${reason}`);
|
|
7021
|
+
}
|
|
7022
|
+
};
|
|
7023
|
+
const printDecisions = () => {
|
|
7024
|
+
console.log("\n📋 Decision Log\n");
|
|
7025
|
+
console.log(formatDecisionsOutput(decisionStoreRef.current.decisions));
|
|
7026
|
+
console.log("");
|
|
7027
|
+
};
|
|
7028
|
+
const printBlockers = () => {
|
|
7029
|
+
console.log("\n🚧 Blockers\n");
|
|
7030
|
+
console.log(formatBlockersOutput(blockerStoreRef.current.blockers));
|
|
7031
|
+
console.log("");
|
|
7032
|
+
};
|
|
7033
|
+
const printReviewGates = () => {
|
|
7034
|
+
console.log("\n🛑 Review Gates\n");
|
|
7035
|
+
console.log(formatReviewGatesOutput(reviewGateStoreRef.current.reviewGates));
|
|
7036
|
+
console.log("");
|
|
7037
|
+
};
|
|
7038
|
+
const printWorkflowOverview = () => {
|
|
7039
|
+
const decisionCount = decisionStoreRef.current.decisions.length;
|
|
7040
|
+
const blockers = blockerStoreRef.current.blockers;
|
|
7041
|
+
const openBlockers = blockers.filter((b) => b.status === "open").length;
|
|
7042
|
+
const gateCount = reviewGateStoreRef.current.reviewGates.length;
|
|
7043
|
+
const handoff = state.session?.metadata.workflow?.handoff;
|
|
7044
|
+
console.log("\n🔧 Workflow Overview\n");
|
|
7045
|
+
console.log(` 📋 Decisions: ${decisionCount}`);
|
|
7046
|
+
console.log(` 🚧 Blockers: ${openBlockers} open / ${blockers.length} total`);
|
|
7047
|
+
console.log(` 🛑 Review gates: ${gateCount}`);
|
|
7048
|
+
console.log(` 🤝 Handoff: ${handoff ? `generated at ${handoff.generatedAt}` : "none"}`);
|
|
7049
|
+
console.log("\n Use /workflow <decisions|blockers|handoff|review-gates> for details.\n");
|
|
7050
|
+
};
|
|
7051
|
+
/**
|
|
7052
|
+
* Show the existing handoff or generate a fresh one. Shared by `/handoff` and
|
|
7053
|
+
* `/workflow handoff`. Pass `['generate']` (or `['regen']`) to force regeneration.
|
|
7054
|
+
*/
|
|
7055
|
+
const runHandoffCommand = async (args) => {
|
|
7056
|
+
if (!state.session) {
|
|
7057
|
+
console.log("No active session");
|
|
7058
|
+
return;
|
|
7059
|
+
}
|
|
7060
|
+
const existing = state.session.metadata.workflow?.handoff;
|
|
7061
|
+
const wantsRegen = args[0] === "generate" || args[0] === "regen";
|
|
7062
|
+
if (existing && !wantsRegen) {
|
|
7063
|
+
console.log("\n🤝 Session handoff\n");
|
|
7064
|
+
console.log(formatHandoffOutput(existing));
|
|
7065
|
+
console.log("Run /handoff generate to refresh.\n");
|
|
7066
|
+
return;
|
|
7067
|
+
}
|
|
7068
|
+
if (state.session.messages.length < 4) {
|
|
7069
|
+
console.log(`Not enough messages to generate a handoff (need at least 4)`);
|
|
7070
|
+
return;
|
|
7071
|
+
}
|
|
7072
|
+
if (!state.agent) {
|
|
7073
|
+
console.log("Cannot generate handoff: no active agent");
|
|
7074
|
+
return;
|
|
7075
|
+
}
|
|
7076
|
+
const handoff = await generateHandoff(state.session);
|
|
7077
|
+
if (!handoff) {
|
|
7078
|
+
console.log("❌ Failed to generate handoff");
|
|
7079
|
+
return;
|
|
7080
|
+
}
|
|
7081
|
+
await state.sessionStore.save(state.session);
|
|
7082
|
+
console.log("\n🤝 Session handoff\n");
|
|
7083
|
+
console.log(formatHandoffOutput(handoff));
|
|
7084
|
+
console.log("\n✅ Session saved with refreshed handoff");
|
|
7085
|
+
};
|
|
6855
7086
|
const handleCommand = async (command, args) => {
|
|
6856
7087
|
const customCommand = state.customCommandStore.getCommand(command);
|
|
6857
7088
|
if (customCommand) try {
|
|
@@ -6970,7 +7201,10 @@ Multi-line Input:
|
|
|
6970
7201
|
}
|
|
6971
7202
|
case "exit":
|
|
6972
7203
|
case "quit":
|
|
7204
|
+
if (exitInProgress) break;
|
|
6973
7205
|
logger.debug("[EXIT /exit command - cleaning up and exiting...");
|
|
7206
|
+
exitInProgress = true;
|
|
7207
|
+
await maybePromptExitHandoff();
|
|
6974
7208
|
await performCleanup();
|
|
6975
7209
|
exit();
|
|
6976
7210
|
break;
|
|
@@ -7968,50 +8202,43 @@ Multi-line Input:
|
|
|
7968
8202
|
break;
|
|
7969
8203
|
}
|
|
7970
8204
|
case "decisions":
|
|
7971
|
-
|
|
7972
|
-
console.log(formatDecisionsOutput(decisionStoreRef.current.decisions));
|
|
7973
|
-
console.log("");
|
|
8205
|
+
printDecisions();
|
|
7974
8206
|
break;
|
|
7975
8207
|
case "blockers":
|
|
7976
|
-
|
|
7977
|
-
console.log(formatBlockersOutput(blockerStoreRef.current.blockers));
|
|
7978
|
-
console.log("");
|
|
8208
|
+
printBlockers();
|
|
7979
8209
|
break;
|
|
7980
8210
|
case "review-gates":
|
|
7981
|
-
|
|
7982
|
-
console.log(formatReviewGatesOutput(reviewGateStoreRef.current.reviewGates));
|
|
7983
|
-
console.log("");
|
|
8211
|
+
printReviewGates();
|
|
7984
8212
|
break;
|
|
7985
|
-
case "handoff":
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
const
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8213
|
+
case "handoff":
|
|
8214
|
+
await runHandoffCommand(args);
|
|
8215
|
+
break;
|
|
8216
|
+
case "workflow": {
|
|
8217
|
+
const sub = args[0];
|
|
8218
|
+
const subArgs = args.slice(1);
|
|
8219
|
+
switch (sub) {
|
|
8220
|
+
case void 0:
|
|
8221
|
+
case "":
|
|
8222
|
+
printWorkflowOverview();
|
|
8223
|
+
break;
|
|
8224
|
+
case "decisions":
|
|
8225
|
+
printDecisions();
|
|
8226
|
+
break;
|
|
8227
|
+
case "blockers":
|
|
8228
|
+
printBlockers();
|
|
8229
|
+
break;
|
|
8230
|
+
case "review-gates":
|
|
8231
|
+
case "gates":
|
|
8232
|
+
printReviewGates();
|
|
8233
|
+
break;
|
|
8234
|
+
case "handoff":
|
|
8235
|
+
await runHandoffCommand(subArgs);
|
|
8236
|
+
break;
|
|
8237
|
+
default:
|
|
8238
|
+
console.log(`Unknown /workflow subcommand: ${sub}`);
|
|
8239
|
+
console.log("Available: decisions, blockers, handoff, review-gates (alias: gates)");
|
|
8240
|
+
break;
|
|
8010
8241
|
}
|
|
8011
|
-
await state.sessionStore.save(state.session);
|
|
8012
|
-
console.log("\n🤝 Session handoff\n");
|
|
8013
|
-
console.log(formatHandoffOutput(handoff));
|
|
8014
|
-
console.log("\n✅ Session saved with refreshed handoff");
|
|
8015
8242
|
break;
|
|
8016
8243
|
}
|
|
8017
8244
|
case "dirs": {
|
|
@@ -8262,9 +8489,6 @@ try {
|
|
|
8262
8489
|
} catch {}
|
|
8263
8490
|
if (import.meta.url.includes("/src/") || process.env.NODE_ENV === "development") logger.debug("🔧 Running in development mode (using TypeScript source)\n");
|
|
8264
8491
|
warmFileCache();
|
|
8265
|
-
render(/* @__PURE__ */ React.createElement(CliApp, null), {
|
|
8266
|
-
exitOnCtrlC: false,
|
|
8267
|
-
alternateScreen: true
|
|
8268
|
-
});
|
|
8492
|
+
render(/* @__PURE__ */ React.createElement(CliApp, null), { exitOnCtrlC: false });
|
|
8269
8493
|
//#endregion
|
|
8270
8494
|
export {};
|