@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/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-DDoiKdgk.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-BVKr0OXs.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, 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({ 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
@@ -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
- }, "", 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, {
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") return /* @__PURE__ */ React.createElement(Box, {
2032
- key: idx,
2033
- marginTop: 1,
2034
- flexDirection: "column"
2035
- }, /* @__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
+ }
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" }, "🔧 ", 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)}`)));
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 sessionName = useCliStore((state) => state.session?.name || "New Session");
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, { 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, {
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, { 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, {
2141
2218
  key: message.id,
2142
2219
  flexDirection: "column",
2143
2220
  paddingX: 1
2144
- }, /* @__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, {
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
- })), !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, {
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
- paddingX: 1
2283
+ borderTop: true,
2284
+ borderBottom: true
2180
2285
  }, /* @__PURE__ */ React.createElement(InputPrompt, {
2181
2286
  onSubmit: handleSubmit,
2182
2287
  onBashCommand,
2183
2288
  onImageDetected,
2184
- disabled: isThinking || !!permissionPrompt || !!userQuestionPrompt || !!reviewGatePrompt,
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
- sessionName,
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 system message.
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 === "system" && message.content.startsWith("[Session handoff from previous session]");
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 system message.
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 system message, pushing the prior handoff to a later index.
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: "system",
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
- 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(() => {
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
- if (!state.agent || !state.session) {
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 = state.session;
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
- console.log("\n📋 Decision Log\n");
8006
- console.log(formatDecisionsOutput(decisionStoreRef.current.decisions));
8007
- console.log("");
8205
+ printDecisions();
8008
8206
  break;
8009
8207
  case "blockers":
8010
- console.log("\n🚧 Blockers\n");
8011
- console.log(formatBlockersOutput(blockerStoreRef.current.blockers));
8012
- console.log("");
8208
+ printBlockers();
8013
8209
  break;
8014
8210
  case "review-gates":
8015
- console.log("\n🛑 Review Gates\n");
8016
- console.log(formatReviewGatesOutput(reviewGateStoreRef.current.reviewGates));
8017
- console.log("");
8211
+ printReviewGates();
8018
8212
  break;
8019
- case "handoff": {
8020
- if (!state.session) {
8021
- console.log("No active session");
8022
- break;
8023
- }
8024
- const existing = state.session.metadata.workflow?.handoff;
8025
- const wantsRegen = args[0] === "generate" || args[0] === "regen";
8026
- if (existing && !wantsRegen) {
8027
- console.log("\n🤝 Session handoff\n");
8028
- console.log(formatHandoffOutput(existing));
8029
- console.log("Run /handoff generate to refresh.\n");
8030
- break;
8031
- }
8032
- if (state.session.messages.length < 4) {
8033
- console.log(`Not enough messages to generate a handoff (need at least 4)`);
8034
- break;
8035
- }
8036
- if (!state.agent) {
8037
- console.log("Cannot generate handoff: no active agent");
8038
- break;
8039
- }
8040
- const handoff = await generateHandoff(state.session);
8041
- if (!handoff) {
8042
- console.log("❌ Failed to generate handoff");
8043
- 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;
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 {};