@bike4mind/cli 0.8.1 → 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 +417 -227
- package/dist/{store-B0ImnWR4.mjs → store-DLduYYGR.mjs} +14 -0
- package/dist/store-YhSkjsW4.mjs +3 -0
- package/dist/{tools-DDoiKdgk.mjs → tools-B0Y_zziv.mjs} +69 -142
- package/dist/{updateChecker-BVKr0OXs.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, usePaste } 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
|
|
@@ -916,7 +920,7 @@ function groupJobsByTurn(jobs) {
|
|
|
916
920
|
* Only renders when there are active (running/queued) background agents.
|
|
917
921
|
*/
|
|
918
922
|
function BackgroundAgentStatus() {
|
|
919
|
-
const activeJobs = useCliStore(selectActiveBackgroundAgents);
|
|
923
|
+
const activeJobs = useCliStore(useShallow(selectActiveBackgroundAgents));
|
|
920
924
|
const permissionPrompt = useCliStore((state) => state.permissionPrompt);
|
|
921
925
|
const { groups, ungrouped } = useMemo(() => groupJobsByTurn(activeJobs), [activeJobs]);
|
|
922
926
|
if (activeJobs.length === 0) return null;
|
|
@@ -1018,6 +1022,7 @@ function renderDiffPreview(preview) {
|
|
|
1018
1022
|
}, line);
|
|
1019
1023
|
});
|
|
1020
1024
|
}
|
|
1025
|
+
const TOOLS_WITH_HIDDEN_ARGS$1 = new Set(["edit_local_file"]);
|
|
1021
1026
|
/**
|
|
1022
1027
|
* Permission prompt component
|
|
1023
1028
|
*
|
|
@@ -1025,6 +1030,7 @@ function renderDiffPreview(preview) {
|
|
|
1025
1030
|
* Waits indefinitely for user response (like Claude Code).
|
|
1026
1031
|
*/
|
|
1027
1032
|
function PermissionPrompt({ toolName, toolDescription, args, preview, canBeTrusted, onResponse }) {
|
|
1033
|
+
const hideArgs = TOOLS_WITH_HIDDEN_ARGS$1.has(toolName);
|
|
1028
1034
|
const items = canBeTrusted ? [
|
|
1029
1035
|
{
|
|
1030
1036
|
label: "✓ Allow once",
|
|
@@ -1115,7 +1121,7 @@ function PermissionPrompt({ toolName, toolDescription, args, preview, canBeTrust
|
|
|
1115
1121
|
}, headerText)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Tool: "), /* @__PURE__ */ React.createElement(Text, {
|
|
1116
1122
|
bold: true,
|
|
1117
1123
|
color: "cyan"
|
|
1118
|
-
}, 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, {
|
|
1119
1125
|
marginTop: 1,
|
|
1120
1126
|
flexDirection: "column"
|
|
1121
1127
|
}, /* @__PURE__ */ React.createElement(Text, { bold: true }, "Arguments:"), /* @__PURE__ */ React.createElement(Box, {
|
|
@@ -1442,6 +1448,39 @@ function ReviewGatePrompt({ description, options, recommendation, onResponse })
|
|
|
1442
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")));
|
|
1443
1449
|
}
|
|
1444
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
|
|
1445
1484
|
//#region src/components/ConfigEditor.tsx
|
|
1446
1485
|
/**
|
|
1447
1486
|
* Max iterations options: 10, 20, 30, 40, 50, Infinite (null)
|
|
@@ -1582,6 +1621,18 @@ function buildConfigItems(availableModels) {
|
|
|
1582
1621
|
autoCompact: value
|
|
1583
1622
|
}
|
|
1584
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
|
+
})
|
|
1585
1636
|
}, {
|
|
1586
1637
|
key: "theme",
|
|
1587
1638
|
label: "Theme",
|
|
@@ -2015,34 +2066,45 @@ function truncateValue(value, maxLength) {
|
|
|
2015
2066
|
if (str.length <= maxLength) return str;
|
|
2016
2067
|
return str.slice(0, maxLength) + "...";
|
|
2017
2068
|
}
|
|
2069
|
+
const TOOLS_WITH_HIDDEN_ARGS = new Set(["edit_local_file"]);
|
|
2018
2070
|
/**
|
|
2019
2071
|
* Returns display properties for a message role
|
|
2020
2072
|
*/
|
|
2021
|
-
const MessageItem = React.memo(function MessageItem({ message }) {
|
|
2073
|
+
const MessageItem = React.memo(function MessageItem({ message, showThoughts = true }) {
|
|
2022
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);
|
|
2023
2079
|
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, isUser && message.content && /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, {
|
|
2024
2080
|
backgroundColor: "whiteBright",
|
|
2025
|
-
color: "black"
|
|
2026
|
-
|
|
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, {
|
|
2027
2084
|
paddingLeft: 2,
|
|
2028
2085
|
flexDirection: "column",
|
|
2029
2086
|
marginBottom: 1
|
|
2030
2087
|
}, message.metadata.steps.map((step, idx) => {
|
|
2031
|
-
if (step.type === "thought")
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
+
}
|
|
2036
2096
|
if (step.type === "action") {
|
|
2037
2097
|
const toolName = step.metadata?.toolName || "unknown";
|
|
2098
|
+
const formattedToolName = toolName.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2038
2099
|
const toolInput = step.metadata?.toolInput;
|
|
2100
|
+
const hideArgs = TOOLS_WITH_HIDDEN_ARGS.has(toolName);
|
|
2039
2101
|
const observationStep = message.metadata?.steps?.[idx + 1];
|
|
2040
2102
|
const result = observationStep?.type === "observation" ? observationStep.content : null;
|
|
2041
2103
|
return /* @__PURE__ */ React.createElement(Box, {
|
|
2042
2104
|
key: idx,
|
|
2043
2105
|
marginTop: 1,
|
|
2044
2106
|
flexDirection: "column"
|
|
2045
|
-
}, /* @__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)}`)));
|
|
2046
2108
|
}
|
|
2047
2109
|
return null;
|
|
2048
2110
|
}).filter(Boolean)), !isUser && message.content !== "..." && /* @__PURE__ */ React.createElement(Box, {
|
|
@@ -2056,9 +2118,9 @@ const MessageItem = React.memo(function MessageItem({ message }) {
|
|
|
2056
2118
|
//#endregion
|
|
2057
2119
|
//#region src/components/App.tsx
|
|
2058
2120
|
function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPermissionResponse, onUserQuestionResponse, onReviewGateResponse, onImageDetected, commandHistory = [], commands = [], config, availableModels = [], onSaveConfig, prefillInput, onPrefillConsumed, mcpManager }) {
|
|
2059
|
-
const messages = useCliStore((state) => state.session?.messages || []);
|
|
2121
|
+
const messages = useCliStore(useShallow((state) => state.session?.messages || []));
|
|
2060
2122
|
const pendingMessages = useCliStore((state) => state.pendingMessages);
|
|
2061
|
-
const
|
|
2123
|
+
const messageQueue = useCliStore((state) => state.messageQueue);
|
|
2062
2124
|
const currentModel = useCliStore((state) => state.session?.model || ChatModels.CLAUDE_4_5_SONNET);
|
|
2063
2125
|
const totalTokens = useCliStore((state) => state.session?.metadata.totalTokens || 0);
|
|
2064
2126
|
const totalCredits = useCliStore((state) => state.session?.metadata.totalCredits);
|
|
@@ -2066,6 +2128,8 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2066
2128
|
const permissionPrompt = useCliStore((state) => state.permissionPrompt);
|
|
2067
2129
|
const userQuestionPrompt = useCliStore((state) => state.userQuestionPrompt);
|
|
2068
2130
|
const reviewGatePrompt = useCliStore((state) => state.reviewGatePrompt);
|
|
2131
|
+
const exitHandoffPrompt = useCliStore((state) => state.exitHandoffPrompt);
|
|
2132
|
+
const setExitHandoffPrompt = useCliStore((state) => state.setExitHandoffPrompt);
|
|
2069
2133
|
const showConfigEditor = useCliStore((state) => state.showConfigEditor);
|
|
2070
2134
|
const setShowConfigEditor = useCliStore((state) => state.setShowConfigEditor);
|
|
2071
2135
|
const showMcpViewer = useCliStore((state) => state.showMcpViewer);
|
|
@@ -2090,6 +2154,9 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2090
2154
|
if (key.tab && key.shift) cycleInteractionMode();
|
|
2091
2155
|
});
|
|
2092
2156
|
const [isBashMode, setIsBashMode] = useState(false);
|
|
2157
|
+
const showThoughts = config?.preferences.showThoughts ?? true;
|
|
2158
|
+
const { stdout } = useStdout();
|
|
2159
|
+
const terminalCols = stdout?.columns ?? 80;
|
|
2093
2160
|
const handleSubmit = React.useCallback(async (input) => {
|
|
2094
2161
|
const trimmed = input.trim();
|
|
2095
2162
|
if (!trimmed) return;
|
|
@@ -2098,17 +2165,21 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2098
2165
|
await onCommand(command, args);
|
|
2099
2166
|
return;
|
|
2100
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
|
+
}
|
|
2101
2181
|
setIsThinking(true);
|
|
2102
2182
|
try {
|
|
2103
|
-
let messageToSend = trimmed;
|
|
2104
|
-
if (hasFileReferences(trimmed)) {
|
|
2105
|
-
const processed = await processFileReferences(trimmed);
|
|
2106
|
-
messageToSend = processed.content;
|
|
2107
|
-
if (processed.errors.length > 0) {
|
|
2108
|
-
const errorBlock = processed.errors.map((e) => `[Warning: ${e}]`).join("\n");
|
|
2109
|
-
messageToSend = `${messageToSend}\n\n${errorBlock}`;
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
2183
|
await onMessage(messageToSend);
|
|
2113
2184
|
} finally {
|
|
2114
2185
|
setIsThinking(false);
|
|
@@ -2118,7 +2189,10 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2118
2189
|
onCommand,
|
|
2119
2190
|
setIsThinking
|
|
2120
2191
|
]);
|
|
2121
|
-
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, {
|
|
2122
2196
|
flexDirection: "column",
|
|
2123
2197
|
paddingX: 1
|
|
2124
2198
|
}, /* @__PURE__ */ React.createElement(ConfigEditor, {
|
|
@@ -2137,11 +2211,17 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2137
2211
|
key: message.id,
|
|
2138
2212
|
flexDirection: "column",
|
|
2139
2213
|
paddingX: 1
|
|
2140
|
-
}, /* @__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, {
|
|
2141
2218
|
key: message.id,
|
|
2142
2219
|
flexDirection: "column",
|
|
2143
2220
|
paddingX: 1
|
|
2144
|
-
}, /* @__PURE__ */ React.createElement(MessageItem, {
|
|
2221
|
+
}, /* @__PURE__ */ React.createElement(MessageItem, {
|
|
2222
|
+
message,
|
|
2223
|
+
showThoughts
|
|
2224
|
+
})))), permissionPrompt && /* @__PURE__ */ React.createElement(Box, {
|
|
2145
2225
|
key: permissionPrompt.id,
|
|
2146
2226
|
flexDirection: "column",
|
|
2147
2227
|
paddingX: 1
|
|
@@ -2167,88 +2247,59 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
|
|
|
2167
2247
|
options: reviewGatePrompt.options,
|
|
2168
2248
|
recommendation: reviewGatePrompt.recommendation,
|
|
2169
2249
|
onResponse: (response) => onReviewGateResponse(response, reviewGatePrompt.id)
|
|
2170
|
-
})),
|
|
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, {
|
|
2171
2259
|
paddingX: 1,
|
|
2172
2260
|
marginBottom: 1
|
|
2173
2261
|
}, /* @__PURE__ */ React.createElement(Text, {
|
|
2174
2262
|
color: "yellow",
|
|
2175
2263
|
bold: true
|
|
2176
|
-
}, "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, {
|
|
2177
2281
|
borderStyle: "single",
|
|
2178
2282
|
borderColor: isBashMode ? "yellow" : "cyan",
|
|
2179
|
-
|
|
2283
|
+
borderTop: true,
|
|
2284
|
+
borderBottom: true
|
|
2180
2285
|
}, /* @__PURE__ */ React.createElement(InputPrompt, {
|
|
2181
2286
|
onSubmit: handleSubmit,
|
|
2182
2287
|
onBashCommand,
|
|
2183
2288
|
onImageDetected,
|
|
2184
|
-
disabled:
|
|
2289
|
+
disabled: !!permissionPrompt || !!userQuestionPrompt || !!reviewGatePrompt || !!exitHandoffPrompt,
|
|
2185
2290
|
history: commandHistory,
|
|
2186
2291
|
commands,
|
|
2187
2292
|
prefillInput,
|
|
2188
2293
|
onPrefillConsumed,
|
|
2189
2294
|
onBashModeChange: setIsBashMode
|
|
2190
2295
|
})), /* @__PURE__ */ React.createElement(StatusBar, {
|
|
2191
|
-
|
|
2296
|
+
isBashMode,
|
|
2192
2297
|
model: currentModel,
|
|
2193
2298
|
tokenUsage: totalTokens,
|
|
2194
2299
|
creditsUsage: totalCredits
|
|
2195
|
-
})));
|
|
2300
|
+
}))));
|
|
2196
2301
|
}
|
|
2197
2302
|
//#endregion
|
|
2198
|
-
//#region src/components/MessageList.tsx
|
|
2199
|
-
/**
|
|
2200
|
-
* Strip <think>...</think> tags from message content for cleaner display
|
|
2201
|
-
* The agent uses these tags internally for reasoning, but users don't need to see them
|
|
2202
|
-
*/
|
|
2203
|
-
function stripThinkingTags(content) {
|
|
2204
|
-
return content.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
2205
|
-
}
|
|
2206
|
-
React.memo(function MessageList({ messages }) {
|
|
2207
|
-
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!"));
|
|
2208
|
-
return /* @__PURE__ */ React.createElement(Box, {
|
|
2209
|
-
flexDirection: "column",
|
|
2210
|
-
gap: 1,
|
|
2211
|
-
paddingY: 1
|
|
2212
|
-
}, messages.map((message, index) => /* @__PURE__ */ React.createElement(Box, {
|
|
2213
|
-
key: index,
|
|
2214
|
-
flexDirection: "column",
|
|
2215
|
-
marginBottom: 1
|
|
2216
|
-
}, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, {
|
|
2217
|
-
bold: true,
|
|
2218
|
-
color: message.role === "user" ? "cyan" : "green"
|
|
2219
|
-
}, 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, {
|
|
2220
|
-
paddingLeft: 2,
|
|
2221
|
-
marginTop: 1,
|
|
2222
|
-
flexDirection: "column"
|
|
2223
|
-
}, /* @__PURE__ */ React.createElement(Text, {
|
|
2224
|
-
dimColor: true,
|
|
2225
|
-
bold: true
|
|
2226
|
-
}, "🔧 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) => {
|
|
2227
|
-
if (step.type === "thought") return /* @__PURE__ */ React.createElement(Box, {
|
|
2228
|
-
key: idx,
|
|
2229
|
-
paddingLeft: 2,
|
|
2230
|
-
marginTop: 1,
|
|
2231
|
-
flexDirection: "column"
|
|
2232
|
-
}, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "💭 Thought:"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` ${step.content.slice(0, 200)}${step.content.length > 200 ? "..." : ""}`));
|
|
2233
|
-
if (step.type === "action") {
|
|
2234
|
-
const toolName = step.metadata?.toolName || "unknown";
|
|
2235
|
-
const toolInput = step.metadata?.toolInput;
|
|
2236
|
-
const observationStep = message.metadata.steps[idx + 1];
|
|
2237
|
-
const result = observationStep?.type === "observation" ? observationStep.content : null;
|
|
2238
|
-
return /* @__PURE__ */ React.createElement(Box, {
|
|
2239
|
-
key: idx,
|
|
2240
|
-
paddingLeft: 2,
|
|
2241
|
-
marginTop: 1,
|
|
2242
|
-
flexDirection: "column"
|
|
2243
|
-
}, /* @__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 ? "..." : ""}`));
|
|
2244
|
-
}
|
|
2245
|
-
return null;
|
|
2246
|
-
}).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`)))));
|
|
2247
|
-
}, (prevProps, nextProps) => {
|
|
2248
|
-
if (prevProps.messages.length !== nextProps.messages.length) return false;
|
|
2249
|
-
return prevProps.messages === nextProps.messages;
|
|
2250
|
-
});
|
|
2251
|
-
//#endregion
|
|
2252
2303
|
//#region src/components/TrustLocationSelector.tsx
|
|
2253
2304
|
function TrustLocationSelector({ inProject, onSelect, onCancel }) {
|
|
2254
2305
|
const items = [];
|
|
@@ -2585,89 +2636,6 @@ function getTokenCounter() {
|
|
|
2585
2636
|
if (!tokenCounter) tokenCounter = new TokenCounter();
|
|
2586
2637
|
return tokenCounter;
|
|
2587
2638
|
}
|
|
2588
|
-
//#endregion
|
|
2589
|
-
//#region src/utils/compaction.ts
|
|
2590
|
-
/**
|
|
2591
|
-
* Build a prompt for the LLM to summarize the conversation.
|
|
2592
|
-
*
|
|
2593
|
-
* Preserves the most recent exchanges to maintain conversational flow,
|
|
2594
|
-
* while summarizing older messages to reduce context size.
|
|
2595
|
-
*
|
|
2596
|
-
* @param messages - All messages in the current session
|
|
2597
|
-
* @param options - Compaction options
|
|
2598
|
-
* @returns The summarization prompt and messages to preserve
|
|
2599
|
-
*/
|
|
2600
|
-
function buildCompactionPrompt(messages, options = {}) {
|
|
2601
|
-
const preserveCount = (options.preserveRecentExchanges ?? 2) * 2;
|
|
2602
|
-
if (messages.length <= preserveCount) return {
|
|
2603
|
-
prompt: "",
|
|
2604
|
-
preservedMessages: messages
|
|
2605
|
-
};
|
|
2606
|
-
const messagesToSummarize = messages.slice(0, -preserveCount);
|
|
2607
|
-
const preservedMessages = messages.slice(-preserveCount);
|
|
2608
|
-
let prompt = `You are summarizing a conversation for context continuity. Create a concise summary that captures:
|
|
2609
|
-
|
|
2610
|
-
- Key decisions made
|
|
2611
|
-
- Important context established
|
|
2612
|
-
- Files and code discussed
|
|
2613
|
-
- Current task state
|
|
2614
|
-
- Any pending items or next steps
|
|
2615
|
-
|
|
2616
|
-
`;
|
|
2617
|
-
if (options.claudeMdInstructions) prompt += `Project-specific compaction instructions:\n${options.claudeMdInstructions}\n\n`;
|
|
2618
|
-
if (options.userInstructions) prompt += `Additional focus: ${options.userInstructions}\n\n`;
|
|
2619
|
-
prompt += `CONVERSATION TO SUMMARIZE:\n\n`;
|
|
2620
|
-
const roleLabels = {
|
|
2621
|
-
user: "User",
|
|
2622
|
-
assistant: "Assistant",
|
|
2623
|
-
system: "System"
|
|
2624
|
-
};
|
|
2625
|
-
for (const msg of messagesToSummarize) {
|
|
2626
|
-
const roleLabel = roleLabels[msg.role] || "System";
|
|
2627
|
-
const content = msg.content.length > 2e3 ? msg.content.slice(0, 2e3) + "...[truncated]" : msg.content;
|
|
2628
|
-
prompt += `**${roleLabel}:** ${content}\n\n`;
|
|
2629
|
-
}
|
|
2630
|
-
prompt += `\nProvide a concise summary (aim for 500-1000 words) that an AI assistant can use to continue this conversation with full context.`;
|
|
2631
|
-
return {
|
|
2632
|
-
prompt,
|
|
2633
|
-
preservedMessages
|
|
2634
|
-
};
|
|
2635
|
-
}
|
|
2636
|
-
/**
|
|
2637
|
-
* Create a new compacted session from an original session.
|
|
2638
|
-
*
|
|
2639
|
-
* The new session contains:
|
|
2640
|
-
* 1. A system message with the conversation summary
|
|
2641
|
-
* 2. The preserved recent messages
|
|
2642
|
-
*
|
|
2643
|
-
* @param originalSession - The session being compacted
|
|
2644
|
-
* @param summary - The LLM-generated summary of older messages
|
|
2645
|
-
* @param preservedMessages - Recent messages to keep verbatim
|
|
2646
|
-
* @returns A new session with compacted context
|
|
2647
|
-
*/
|
|
2648
|
-
function createCompactedSession(originalSession, summary, preservedMessages) {
|
|
2649
|
-
const summaryMessage = {
|
|
2650
|
-
id: v4(),
|
|
2651
|
-
role: "user",
|
|
2652
|
-
content: `[Previous conversation summary]\n\n${summary}`,
|
|
2653
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2654
|
-
};
|
|
2655
|
-
return {
|
|
2656
|
-
id: v4(),
|
|
2657
|
-
name: `${originalSession.name} (compacted)`,
|
|
2658
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2659
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2660
|
-
model: originalSession.model,
|
|
2661
|
-
messages: [summaryMessage, ...preservedMessages],
|
|
2662
|
-
metadata: {
|
|
2663
|
-
totalTokens: 0,
|
|
2664
|
-
totalCost: 0,
|
|
2665
|
-
toolCallCount: 0,
|
|
2666
|
-
compactedFrom: originalSession.id,
|
|
2667
|
-
...originalSession.metadata.workflow ? { workflow: originalSession.metadata.workflow } : {}
|
|
2668
|
-
}
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
2639
|
/**
|
|
2672
2640
|
* Prefix tag used to mark a system message as an injected handoff. Kept as a
|
|
2673
2641
|
* single source of truth so the dedup-on-resume check and the system-message
|
|
@@ -2843,28 +2811,127 @@ function appendSection(lines, heading, items) {
|
|
|
2843
2811
|
lines.push("");
|
|
2844
2812
|
}
|
|
2845
2813
|
/**
|
|
2846
|
-
* True when a message is a previously-injected handoff
|
|
2814
|
+
* True when a message is a previously-injected handoff message.
|
|
2847
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.
|
|
2848
2820
|
*/
|
|
2849
2821
|
function isInjectedHandoff(message) {
|
|
2850
|
-
return message.role === "
|
|
2822
|
+
return message.role === "user" && message.content.startsWith("[Session handoff from previous session]");
|
|
2851
2823
|
}
|
|
2852
2824
|
/**
|
|
2853
|
-
* 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.
|
|
2854
2826
|
* Any previously-injected handoff anywhere in the list is removed so the
|
|
2855
2827
|
* message list stays stable across repeated save/resume cycles. We scan the
|
|
2856
2828
|
* whole list rather than just index 0 because compaction can prepend a
|
|
2857
|
-
* summary
|
|
2829
|
+
* summary message, pushing the prior handoff to a later index.
|
|
2858
2830
|
*/
|
|
2859
2831
|
function injectHandoffMessage(messages, handoff) {
|
|
2860
2832
|
return [{
|
|
2861
2833
|
id: v4(),
|
|
2862
|
-
role: "
|
|
2834
|
+
role: "user",
|
|
2863
2835
|
content: buildHandoffSystemMessage(handoff),
|
|
2864
2836
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2865
2837
|
}, ...messages.filter((m) => !isInjectedHandoff(m))];
|
|
2866
2838
|
}
|
|
2867
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
|
|
2868
2935
|
//#region src/utils/imageRenderer.ts
|
|
2869
2936
|
/**
|
|
2870
2937
|
* Manages image placeholder rendering in conversation
|
|
@@ -5376,7 +5443,9 @@ function summarizeUserQuestion(payload) {
|
|
|
5376
5443
|
return first ? first.slice(0, 240) : void 0;
|
|
5377
5444
|
}
|
|
5378
5445
|
let exitTimestamp = null;
|
|
5446
|
+
let exitInProgress = false;
|
|
5379
5447
|
const EXIT_TIMEOUT_MS = 2e3;
|
|
5448
|
+
const EXIT_HANDOFF_PROMPT_TIMEOUT_MS = 3e4;
|
|
5380
5449
|
let usageCache = null;
|
|
5381
5450
|
function CliApp() {
|
|
5382
5451
|
const { exit } = useApp();
|
|
@@ -5474,14 +5543,20 @@ function CliApp() {
|
|
|
5474
5543
|
abortController: null
|
|
5475
5544
|
}));
|
|
5476
5545
|
useCliStore.getState().setIsThinking(false);
|
|
5477
|
-
|
|
5546
|
+
useCliStore.getState().clearMessageQueue();
|
|
5547
|
+
} else useCliStore.getState().clearMessageQueue();
|
|
5478
5548
|
return;
|
|
5479
5549
|
}
|
|
5480
5550
|
if (key.ctrl && input === "c") {
|
|
5551
|
+
if (exitInProgress) return;
|
|
5481
5552
|
const now = Date.now();
|
|
5482
5553
|
if (exitTimestamp && now - exitTimestamp < EXIT_TIMEOUT_MS) {
|
|
5483
5554
|
logger.debug("[EXIT] Second Ctrl+C - cleaning up and exiting...");
|
|
5484
|
-
|
|
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(() => {
|
|
5485
5560
|
exit();
|
|
5486
5561
|
});
|
|
5487
5562
|
} else {
|
|
@@ -6455,7 +6530,8 @@ function CliApp() {
|
|
|
6455
6530
|
}
|
|
6456
6531
|
};
|
|
6457
6532
|
const handleMessage = async (message) => {
|
|
6458
|
-
|
|
6533
|
+
const storeSession = useCliStore.getState().session;
|
|
6534
|
+
if (!state.agent || !storeSession) {
|
|
6459
6535
|
console.error("❌ CLI failed to initialize. Try restarting b4m.\n");
|
|
6460
6536
|
return;
|
|
6461
6537
|
}
|
|
@@ -6472,7 +6548,7 @@ function CliApp() {
|
|
|
6472
6548
|
await state.commandHistoryStore.add(message);
|
|
6473
6549
|
setCommandHistory(await state.commandHistoryStore.list());
|
|
6474
6550
|
const config = state.config;
|
|
6475
|
-
let activeSession =
|
|
6551
|
+
let activeSession = storeSession;
|
|
6476
6552
|
if (config?.preferences.autoCompact !== false && activeSession.messages.length >= 6) {
|
|
6477
6553
|
const tokenCounter = getTokenCounter();
|
|
6478
6554
|
const threshold = tokenCounter.getContextWindow(activeSession.model, state.availableModels) * .8;
|
|
@@ -6649,6 +6725,15 @@ function CliApp() {
|
|
|
6649
6725
|
type: "status",
|
|
6650
6726
|
status: wasAborted ? "idle" : "awaiting_input"
|
|
6651
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
|
+
}
|
|
6652
6737
|
}
|
|
6653
6738
|
};
|
|
6654
6739
|
handleMessageRef.current = handleMessage;
|
|
@@ -6886,6 +6971,118 @@ function CliApp() {
|
|
|
6886
6971
|
useCliStore.getState().setIsThinking(false);
|
|
6887
6972
|
}
|
|
6888
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
|
+
};
|
|
6889
7086
|
const handleCommand = async (command, args) => {
|
|
6890
7087
|
const customCommand = state.customCommandStore.getCommand(command);
|
|
6891
7088
|
if (customCommand) try {
|
|
@@ -7004,7 +7201,10 @@ Multi-line Input:
|
|
|
7004
7201
|
}
|
|
7005
7202
|
case "exit":
|
|
7006
7203
|
case "quit":
|
|
7204
|
+
if (exitInProgress) break;
|
|
7007
7205
|
logger.debug("[EXIT /exit command - cleaning up and exiting...");
|
|
7206
|
+
exitInProgress = true;
|
|
7207
|
+
await maybePromptExitHandoff();
|
|
7008
7208
|
await performCleanup();
|
|
7009
7209
|
exit();
|
|
7010
7210
|
break;
|
|
@@ -8002,50 +8202,43 @@ Multi-line Input:
|
|
|
8002
8202
|
break;
|
|
8003
8203
|
}
|
|
8004
8204
|
case "decisions":
|
|
8005
|
-
|
|
8006
|
-
console.log(formatDecisionsOutput(decisionStoreRef.current.decisions));
|
|
8007
|
-
console.log("");
|
|
8205
|
+
printDecisions();
|
|
8008
8206
|
break;
|
|
8009
8207
|
case "blockers":
|
|
8010
|
-
|
|
8011
|
-
console.log(formatBlockersOutput(blockerStoreRef.current.blockers));
|
|
8012
|
-
console.log("");
|
|
8208
|
+
printBlockers();
|
|
8013
8209
|
break;
|
|
8014
8210
|
case "review-gates":
|
|
8015
|
-
|
|
8016
|
-
console.log(formatReviewGatesOutput(reviewGateStoreRef.current.reviewGates));
|
|
8017
|
-
console.log("");
|
|
8211
|
+
printReviewGates();
|
|
8018
8212
|
break;
|
|
8019
|
-
case "handoff":
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
const
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
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;
|
|
8044
8241
|
}
|
|
8045
|
-
await state.sessionStore.save(state.session);
|
|
8046
|
-
console.log("\n🤝 Session handoff\n");
|
|
8047
|
-
console.log(formatHandoffOutput(handoff));
|
|
8048
|
-
console.log("\n✅ Session saved with refreshed handoff");
|
|
8049
8242
|
break;
|
|
8050
8243
|
}
|
|
8051
8244
|
case "dirs": {
|
|
@@ -8296,9 +8489,6 @@ try {
|
|
|
8296
8489
|
} catch {}
|
|
8297
8490
|
if (import.meta.url.includes("/src/") || process.env.NODE_ENV === "development") logger.debug("🔧 Running in development mode (using TypeScript source)\n");
|
|
8298
8491
|
warmFileCache();
|
|
8299
|
-
render(/* @__PURE__ */ React.createElement(CliApp, null), {
|
|
8300
|
-
exitOnCtrlC: false,
|
|
8301
|
-
alternateScreen: true
|
|
8302
|
-
});
|
|
8492
|
+
render(/* @__PURE__ */ React.createElement(CliApp, null), { exitOnCtrlC: false });
|
|
8303
8493
|
//#endregion
|
|
8304
8494
|
export {};
|