@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/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { n as useCliStore, t as selectActiveBackgroundAgents } from "./store-B0ImnWR4.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-Dg1HL5PO.mjs";
4
- import { Mt as validateNotebookPath$1, g as ChatModels, jt as validateJupyterKernelName, m as CREDIT_DEDUCT_TRANSACTION_TYPES, n as logger, t as ConfigStore } from "./ConfigStore-Bj1IOvWn.mjs";
5
- import { a as version, t as checkForUpdate } from "./updateChecker-Bbkc_8IL.mjs";
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({ sessionName, model, tokenUsage, creditsUsage }) {
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(Text, { dimColor: true }, sessionName), /* @__PURE__ */ React.createElement(Box, { gap: 2 }, interactionMode === "auto-accept" && /* @__PURE__ */ React.createElement(Text, {
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 || key.delete) {
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 || key.delete) {
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 || key.delete) {
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 truncated = content.length > 5e5 ? content.slice(0, MAX_PASTE_SIZE) : content;
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${truncated}` : truncated, lineCount);
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(50));
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
- }, "", message.content, " ")), !isUser && message.metadata?.steps && message.metadata.steps.filter((s) => ["thought", "action"].includes(s.type)).length > 0 && /* @__PURE__ */ React.createElement(Box, {
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") return /* @__PURE__ */ React.createElement(Box, {
1998
- key: idx,
1999
- marginTop: 1,
2000
- flexDirection: "column"
2001
- }, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "💭 Thought:"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` ${step.content}`));
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" }, "🔧 ", toolName), toolInput && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` Input: ${truncateValue(toolInput, 100)}`), result && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` Result: ${truncateValue(result, 200)}`));
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 sessionName = useCliStore((state) => state.session?.name || "New Session");
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, { flexDirection: "column" }, showConfigEditor && config && onSaveConfig ? /* @__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, { message }))), /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, pendingMessages.map((message) => /* @__PURE__ */ React.createElement(Box, {
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, { message })))), permissionPrompt && /* @__PURE__ */ React.createElement(Box, {
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
- })), !permissionPrompt && !userQuestionPrompt && !reviewGatePrompt && /* @__PURE__ */ React.createElement(AgentThinking, null), /* @__PURE__ */ React.createElement(BackgroundAgentStatus, null), /* @__PURE__ */ React.createElement(CompletedGroupNotification, null), exitRequested && /* @__PURE__ */ React.createElement(Box, {
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
- paddingX: 1
2283
+ borderTop: true,
2284
+ borderBottom: true
2146
2285
  }, /* @__PURE__ */ React.createElement(InputPrompt, {
2147
2286
  onSubmit: handleSubmit,
2148
2287
  onBashCommand,
2149
2288
  onImageDetected,
2150
- disabled: isThinking || !!permissionPrompt || !!userQuestionPrompt || !!reviewGatePrompt,
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
- sessionName,
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 system message.
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 === "system" && message.content.startsWith("[Session handoff from previous session]");
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 system message.
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 system message, pushing the prior handoff to a later index.
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: "system",
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
- performCleanup().then(() => {
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
- if (!state.agent || !state.session) {
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 = state.session;
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
- console.log("\n📋 Decision Log\n");
7972
- console.log(formatDecisionsOutput(decisionStoreRef.current.decisions));
7973
- console.log("");
8205
+ printDecisions();
7974
8206
  break;
7975
8207
  case "blockers":
7976
- console.log("\n🚧 Blockers\n");
7977
- console.log(formatBlockersOutput(blockerStoreRef.current.blockers));
7978
- console.log("");
8208
+ printBlockers();
7979
8209
  break;
7980
8210
  case "review-gates":
7981
- console.log("\n🛑 Review Gates\n");
7982
- console.log(formatReviewGatesOutput(reviewGateStoreRef.current.reviewGates));
7983
- console.log("");
8211
+ printReviewGates();
7984
8212
  break;
7985
- case "handoff": {
7986
- if (!state.session) {
7987
- console.log("No active session");
7988
- break;
7989
- }
7990
- const existing = state.session.metadata.workflow?.handoff;
7991
- const wantsRegen = args[0] === "generate" || args[0] === "regen";
7992
- if (existing && !wantsRegen) {
7993
- console.log("\n🤝 Session handoff\n");
7994
- console.log(formatHandoffOutput(existing));
7995
- console.log("Run /handoff generate to refresh.\n");
7996
- break;
7997
- }
7998
- if (state.session.messages.length < 4) {
7999
- console.log(`Not enough messages to generate a handoff (need at least 4)`);
8000
- break;
8001
- }
8002
- if (!state.agent) {
8003
- console.log("Cannot generate handoff: no active agent");
8004
- break;
8005
- }
8006
- const handoff = await generateHandoff(state.session);
8007
- if (!handoff) {
8008
- console.log("❌ Failed to generate handoff");
8009
- break;
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 {};