@bike4mind/cli 2.1.0 → 3.0.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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/dist/{AgentHistoryStore-DdYKfx7C.mjs → AgentHistoryStore-DVxv1kSO.mjs} +7905 -7029
  3. package/dist/{ConfigStore-BygzEI6H.mjs → ConfigStore-51UaP9pJ.mjs} +10645 -6760
  4. package/dist/{ImageStore-kVo-oHoS.mjs → ImageStore-DuI3tgwQ.mjs} +8 -6
  5. package/dist/SandboxRuntimeAdapter-Bt1wPpld.mjs +2 -0
  6. package/dist/{WorkItemsClient-ClBC49y0.mjs → WorkItemsClient-BMY0Qq1s.mjs} +27 -2
  7. package/dist/{buildAgent-B7cMwRAy.mjs → buildAgent-CrSRKYU_.mjs} +75 -43
  8. package/dist/commands/acpCommand.mjs +26 -35
  9. package/dist/commands/apiCommand.mjs +1 -1
  10. package/dist/commands/doctorCommand.mjs +3 -3
  11. package/dist/commands/envCommand.mjs +1 -1
  12. package/dist/commands/headlessCommand.mjs +15 -24
  13. package/dist/commands/mcpCommand.mjs +3 -3
  14. package/dist/commands/pluginCommand.mjs +2 -2
  15. package/dist/commands/updateCommand.mjs +5 -15
  16. package/dist/dist-BsG2Hlp-.mjs +739 -0
  17. package/dist/index.mjs +602 -324
  18. package/dist/{package-CRNnNXxq.mjs → package-DEsNSP54.mjs} +1 -1
  19. package/dist/{serve-DM7ld3-o.mjs → serve-BaHzVbcm.mjs} +300 -154
  20. package/dist/store-BkMur6uk.mjs +2 -0
  21. package/dist/{store-CvjTpQPs.mjs → store-BxmPEI3v.mjs} +10 -0
  22. package/dist/types-I45lMh7Q.mjs +2 -0
  23. package/dist/{updateChecker-CQW8bxo6.mjs → updateChecker-CCLVVdH0.mjs} +36 -13
  24. package/package.json +24 -23
  25. package/dist/ApiClient-dIB3YQc2.mjs +0 -316
  26. package/dist/SandboxRuntimeAdapter-BmHELLuM.mjs +0 -3
  27. package/dist/store-BG3e54c8.mjs +0 -3
  28. package/dist/types-CdIKgWWe.mjs +0 -3
  29. package/dist/{bashExecute-CrdPpBqk-DCATrE-D.mjs → bashExecute-CrdPpBqk-CgK1gZa2.mjs} +1 -1
  30. package/dist/{grepSearch-BgoOOwGe-DtlV8Gn-.mjs → grepSearch-BgoOOwGe-B388GxRD.mjs} +2 -2
package/dist/index.mjs CHANGED
@@ -1,31 +1,31 @@
1
1
  #!/usr/bin/env node
2
- import { $ as CustomCommandStore, A as createAgentDelegateTool, B as ALWAYS_DENIED_FOR_AGENTS, C as createTodoStore, D as createCoordinateTaskTool, E as parseAgentConfig, F as PermissionManager, H as DEFAULT_MAX_ITERATIONS, J as setWebSocketToolExecutor, K as clearFeatureModuleTools, L as wrapTools, M as substituteArguments, O as createResumeAgentTool, P as findIterationBoundary, Q as RemoteSkillSource, R as getProcessHooks, S as createFindDefinitionTool, T as createSkillTool, U as DEFAULT_RETRY_CONFIG, V as DEFAULT_AGENT_MODEL, W as DEFAULT_THOROUGHNESS, X as buildSystemPrompt, Y as getPlanModeFilePath, Z as buildSkillsPromptSection, _ as createDecisionLogTool, at as searchCommands, b as createGetFileStructureTool, ct as formatFileSize, d as createReviewGateStore, dt as COMPACTION_SUMMARY_MARKER, et as CheckpointStore, f as createReviewGateTool, g as formatBlockersOutput, h as createBlockerTools, j as FallbackLlmBackend, k as createBackgroundAgentTools, lt as searchFiles, m as createBlockerStore, nt as hasFileReferences, ot as mergeCommands, p as formatReviewGatesOutput, pt as MAX_PASTE_SIZE, q as registerFeatureModuleTools, rt as processFileReferences, s as buildSandbox, st as rewireReservedNames, tt as SessionStore, u as isTransientNetworkError, ut as warmFileCache, v as createDecisionStore, w as createWriteTodosTool, x as createWorkItemTools, y as formatDecisionsOutput } from "./AgentHistoryStore-DdYKfx7C.mjs";
3
- import { a as selectLiveSubagentTokens, i as selectLiveSubagentCredits, n as selectActiveBackgroundShells, o as useCliStore, r as selectCompletedBackgroundShells, t as selectActiveBackgroundAgents } from "./store-CvjTpQPs.mjs";
2
+ import { $ as processFileReferences, A as createAgentDelegateTool, C as createTodoStore, D as createCoordinateTaskTool, E as parseAgentConfig, F as PermissionManager, G as DEFAULT_THOROUGHNESS, H as DEFAULT_AGENT_MODEL, I as buildCliToolList, J as getPlanModeFilePath, M as substituteArguments, O as createResumeAgentTool, P as findIterationBoundary, Q as CustomCommandStore, S as createFindDefinitionTool, T as createSkillTool, U as DEFAULT_MAX_ITERATIONS, V as ALWAYS_DENIED_FOR_AGENTS, W as DEFAULT_RETRY_CONFIG, X as buildSkillsPromptSection, Y as buildSystemPrompt, Z as RemoteSkillSource, _ as createDecisionLogTool, at as rewireReservedNames, b as createGetFileStructureTool, ct as warmFileCache, d as createReviewGateStore, et as hasFileReferences, f as createReviewGateTool, ft as MAX_PASTE_SIZE, g as formatBlockersOutput, h as createBlockerTools, it as mergeCommands, j as FallbackLlmBackend, k as createBackgroundAgentTools, m as createBlockerStore, nt as SessionStore, ot as formatFileSize, p as formatReviewGatesOutput, q as setWebSocketToolExecutor, rt as searchCommands, s as buildSandbox, st as searchFiles, tt as CheckpointStore, u as isConnectionDrop, ut as COMPACTION_SUMMARY_MARKER, v as createDecisionStore, w as createWriteTodosTool, x as createWorkItemTools, y as formatDecisionsOutput, z as getProcessHooks } from "./AgentHistoryStore-DVxv1kSO.mjs";
3
+ import { a as selectLiveSubagentTokens, i as selectLiveSubagentCredits, n as selectActiveBackgroundShells, o as useCliStore, r as selectCompletedBackgroundShells, t as selectActiveBackgroundAgents } from "./store-BxmPEI3v.mjs";
4
+ import { Bt as selfClaimedActorKindSchema, F as ChatModels, Ft as DELEGATION_PAYLOAD_SCHEMA_NAME, It as actorColorIndex, Lt as actorKindMarker, P as CREDIT_DEDUCT_TRANSACTION_TYPES, Pt as ACTOR_COLOR_SLOTS, Rt as actorKindSchema, Z as REVIEW_GATE_STATUS_VALUES, _ as logger, c as getCreditsUrl, d as resolveApiEndpoint, et as SUBQUEST_STATUS_VALUES, g as parseApiUrl, ht as isEarlyStop, kt as tokenEstimateMultiplier, l as getEnvironmentName, m as LOCAL_DEV_URL, n as extractCompactInstructions, o as isReadOnlyTool, s as ApiEndpointUnconfiguredError, t as ConfigStore, u as requireApiUrl, v as SESSION_ID_PATTERN, y as isValidSessionId, zt as delegationPayloadSchema } from "./ConfigStore-51UaP9pJ.mjs";
4
5
  import { r as isFeatureEnabled, t as PluginStore } from "./PluginStore-DwvOJ-G3.mjs";
5
- import { A as ChatModels, At as delegationPayloadSchema, Dt as actorColorIndex, Et as DELEGATION_PAYLOAD_SCHEMA_NAME, G as REVIEW_GATE_STATUS_VALUES, Ot as actorKindMarker, Tt as ACTOR_COLOR_SLOTS, _ as requireApiUrl, bt as tokenEstimateMultiplier, d as isReadOnlyTool, f as ApiEndpointUnconfiguredError, g as parseApiUrl, h as getEnvironmentName, i as isValidSessionId, jt as selfClaimedActorKindSchema, k as CREDIT_DEDUCT_TRANSACTION_TYPES, kt as actorKindSchema, m as getCreditsUrl, n as logger, ot as isEarlyStop, p as LOCAL_DEV_URL, q as SUBQUEST_STATUS_VALUES, r as SESSION_ID_PATTERN, s as extractCompactInstructions, t as ConfigStore, v as resolveApiEndpoint } from "./ConfigStore-BygzEI6H.mjs";
6
- import { i as getTokenCounter, n as ConversationContext, r as reconstructTurnBlocks, t as WorkItemsClient } from "./WorkItemsClient-ClBC49y0.mjs";
7
- import { a as deferredToolRegistry, i as createToolSearchTool, n as buildSupportingStores, o as NotifyingLlmBackend, r as buildLlmBackend, t as buildAgent } from "./buildAgent-B7cMwRAy.mjs";
6
+ import { i as getTokenCounter, n as ConversationContext, r as reconstructTurnBlocks, t as WorkItemsClient } from "./WorkItemsClient-BMY0Qq1s.mjs";
7
+ import { a as deferredToolRegistry, i as createToolSearchTool, n as buildSupportingStores, o as NotifyingLlmBackend, r as buildLlmBackend, t as buildAgent } from "./buildAgent-CrSRKYU_.mjs";
8
8
  import { t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
9
- import { i as OAuthClient, t as ApiClient } from "./ApiClient-dIB3YQc2.mjs";
10
- import { t as version } from "./package-CRNnNXxq.mjs";
11
- import { r as checkForUpdate } from "./updateChecker-CQW8bxo6.mjs";
9
+ import { a as ApiClient, s as OAuthClient } from "./dist-BsG2Hlp-.mjs";
10
+ import { t as version } from "./package-DEsNSP54.mjs";
11
+ import { s as getStartupUpdateStatus } from "./updateChecker-CCLVVdH0.mjs";
12
12
  import React, { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
13
13
  import { Box, Static, Text, measureElement, render, useApp, useInput, usePaste, useStdout } from "ink";
14
- import { execFile, execSync, spawn } from "child_process";
15
14
  import { randomBytes } from "crypto";
16
15
  import { constants, existsSync, promises, readFileSync, statSync } from "fs";
17
16
  import { homedir } from "os";
18
17
  import path, { basename, extname, join } from "path";
19
18
  import { v4 } from "uuid";
20
19
  import * as path$1 from "node:path";
20
+ import { z as z$1 } from "zod";
21
+ import axios, { isAxiosError } from "axios";
21
22
  import Spinner from "ink-spinner";
22
23
  import { useShallow } from "zustand/react/shallow";
23
24
  import TextInput from "ink-text-input";
24
- import { z as z$1 } from "zod";
25
- import axios, { isAxiosError } from "axios";
25
+ import { execFile, spawn } from "child_process";
26
+ import { promisify } from "util";
26
27
  import { marked } from "marked";
27
28
  import { highlight } from "cli-highlight";
28
- import { promisify } from "util";
29
29
  import SelectInput from "ink-select-input";
30
30
  import jwt from "jsonwebtoken";
31
31
  import open from "open";
@@ -121,52 +121,54 @@ function CustomTextInput({ value, onChange, onSubmit, onPaste, pasteIndicator, p
121
121
  setCursorOffset(cursorOffset + 1);
122
122
  return;
123
123
  }
124
- if (key.meta && !key.ctrl) if (key.shift) {
125
- if (key.leftArrow) {
126
- const newPos = findPreviousWordBoundary(value, cursorOffset);
127
- setCursorOffset(newPos);
128
- return;
129
- }
130
- if (key.rightArrow) {
131
- const newPos = findNextWordBoundary(value, cursorOffset);
132
- setCursorOffset(newPos);
133
- return;
134
- }
135
- if (key.backspace) {
136
- const beforeCursor = value.slice(0, cursorOffset);
137
- const afterCursor = value.slice(cursorOffset);
138
- const newPos = findPreviousWordBoundary(beforeCursor, beforeCursor.length);
139
- const newValue = beforeCursor.slice(0, newPos) + afterCursor;
140
- emitChange(newValue);
141
- setCursorOffset(newPos);
142
- return;
143
- }
144
- if (key.delete) {
145
- const beforeCursor = value.slice(0, cursorOffset);
146
- const afterCursor = value.slice(cursorOffset);
147
- const newPos = findNextWordBoundary(afterCursor, 0);
148
- const newValue = beforeCursor + afterCursor.slice(newPos);
149
- emitChange(newValue);
150
- return;
151
- }
152
- } else {
153
- if (key.leftArrow) {
154
- setCursorOffset(0);
155
- return;
156
- }
157
- if (key.rightArrow) {
158
- setCursorOffset(value.length);
159
- return;
160
- }
161
- if (key.backspace) {
162
- const afterCursor = value.slice(cursorOffset);
163
- emitChange(afterCursor);
164
- setCursorOffset(0);
165
- return;
166
- }
167
- if (key.delete) {
168
- emitChange(value.slice(0, cursorOffset));
169
- return;
124
+ if (key.meta && !key.ctrl) {
125
+ if (key.shift) {
126
+ if (key.leftArrow) {
127
+ const newPos = findPreviousWordBoundary(value, cursorOffset);
128
+ setCursorOffset(newPos);
129
+ return;
130
+ }
131
+ if (key.rightArrow) {
132
+ const newPos = findNextWordBoundary(value, cursorOffset);
133
+ setCursorOffset(newPos);
134
+ return;
135
+ }
136
+ if (key.backspace) {
137
+ const beforeCursor = value.slice(0, cursorOffset);
138
+ const afterCursor = value.slice(cursorOffset);
139
+ const newPos = findPreviousWordBoundary(beforeCursor, beforeCursor.length);
140
+ const newValue = beforeCursor.slice(0, newPos) + afterCursor;
141
+ emitChange(newValue);
142
+ setCursorOffset(newPos);
143
+ return;
144
+ }
145
+ if (key.delete) {
146
+ const beforeCursor = value.slice(0, cursorOffset);
147
+ const afterCursor = value.slice(cursorOffset);
148
+ const newPos = findNextWordBoundary(afterCursor, 0);
149
+ const newValue = beforeCursor + afterCursor.slice(newPos);
150
+ emitChange(newValue);
151
+ return;
152
+ }
153
+ } else {
154
+ if (key.leftArrow) {
155
+ setCursorOffset(0);
156
+ return;
157
+ }
158
+ if (key.rightArrow) {
159
+ setCursorOffset(value.length);
160
+ return;
161
+ }
162
+ if (key.backspace) {
163
+ const afterCursor = value.slice(cursorOffset);
164
+ emitChange(afterCursor);
165
+ setCursorOffset(0);
166
+ return;
167
+ }
168
+ if (key.delete) {
169
+ emitChange(value.slice(0, cursorOffset));
170
+ return;
171
+ }
170
172
  }
171
173
  }
172
174
  if (key.home) {
@@ -2107,6 +2109,138 @@ function McpViewer({ config, mcpManager, onClose }) {
2107
2109
  }, "Press Esc or q to close")));
2108
2110
  }
2109
2111
  //#endregion
2112
+ //#region src/storage/CommandHistoryStore.ts
2113
+ /**
2114
+ * Maximum number of history entries to keep
2115
+ */
2116
+ const MAX_HISTORY_ENTRIES = 1e3;
2117
+ /**
2118
+ * Manages command history stored in JSONL format
2119
+ * Uses append-friendly JSONL for efficient writes
2120
+ */
2121
+ var CommandHistoryStore = class {
2122
+ constructor(historyPath) {
2123
+ this.history = null;
2124
+ this.historyPath = historyPath || path.join(homedir(), ".bike4mind", "history.jsonl");
2125
+ }
2126
+ /**
2127
+ * Initialize history directory
2128
+ */
2129
+ async init() {
2130
+ const dir = path.dirname(this.historyPath);
2131
+ try {
2132
+ await promises.mkdir(dir, { recursive: true });
2133
+ } catch (error) {
2134
+ console.error("Failed to initialize history directory:", error);
2135
+ throw error;
2136
+ }
2137
+ }
2138
+ /**
2139
+ * Load command history from disk
2140
+ * Returns array of commands (newest first)
2141
+ */
2142
+ async load() {
2143
+ if (this.history) return this.history;
2144
+ try {
2145
+ const entries = (await promises.readFile(this.historyPath, "utf-8")).trim().split("\n").filter((line) => line.length > 0).map((line) => {
2146
+ try {
2147
+ return JSON.parse(line);
2148
+ } catch {
2149
+ return null;
2150
+ }
2151
+ }).filter((entry) => entry !== null);
2152
+ this.history = entries.sort((a, b) => b.timestamp - a.timestamp).map((entry) => entry.command);
2153
+ return this.history;
2154
+ } catch (error) {
2155
+ if (error.code === "ENOENT") {
2156
+ this.history = [];
2157
+ return this.history;
2158
+ }
2159
+ throw error;
2160
+ }
2161
+ }
2162
+ /**
2163
+ * Add a command to history
2164
+ * Skips if it's the same as the most recent command (deduplicate)
2165
+ */
2166
+ async add(command) {
2167
+ await this.load();
2168
+ if (!command || !command.trim()) return;
2169
+ if (this.history && this.history.length > 0 && this.history[0] === command) return;
2170
+ const entry = {
2171
+ command,
2172
+ timestamp: Date.now()
2173
+ };
2174
+ await this.init();
2175
+ const line = JSON.stringify(entry) + "\n";
2176
+ await promises.appendFile(this.historyPath, line, "utf-8");
2177
+ this.history = [command, ...this.history || []];
2178
+ if (this.history.length > MAX_HISTORY_ENTRIES) await this.trim();
2179
+ }
2180
+ /**
2181
+ * Trim history to max entries
2182
+ * Rewrites the entire file with the most recent entries
2183
+ */
2184
+ async trim() {
2185
+ if (!this.history || this.history.length <= MAX_HISTORY_ENTRIES) return;
2186
+ const trimmedCommands = this.history.slice(0, MAX_HISTORY_ENTRIES);
2187
+ const lines = trimmedCommands.reverse().map((command) => {
2188
+ return JSON.stringify({
2189
+ command,
2190
+ timestamp: Date.now()
2191
+ });
2192
+ }).join("\n");
2193
+ await promises.writeFile(this.historyPath, lines + "\n", "utf-8");
2194
+ this.history = trimmedCommands;
2195
+ }
2196
+ /**
2197
+ * Clear all command history
2198
+ */
2199
+ async clear() {
2200
+ try {
2201
+ await promises.unlink(this.historyPath);
2202
+ } catch (error) {
2203
+ if (error.code !== "ENOENT") throw error;
2204
+ }
2205
+ this.history = [];
2206
+ }
2207
+ /**
2208
+ * Get all commands (newest first)
2209
+ */
2210
+ async list() {
2211
+ return this.load();
2212
+ }
2213
+ };
2214
+ //#endregion
2215
+ //#region src/storage/buildGlobalConfigPatch.ts
2216
+ /**
2217
+ * Compute the minimal global-config patch for a /config editor save: only the
2218
+ * editor-owned fields (defaultModel, preferences, features) that the user
2219
+ * actually changed vs the current effective (merged) config. Unchanged fields
2220
+ * are omitted, so a repo-injected value the user never touched is never
2221
+ * laundered into the global layer by a save() that spreads the merged config.
2222
+ *
2223
+ * defaultModel and preferences diff per key. features stays whole-object when
2224
+ * anything changed, on purpose: ConfigStore.save() feeds config.features to
2225
+ * mergeFeatures as the caller's COMPLETE desired set (absence of a key means
2226
+ * "removed"), so a per-key diff would make save() delete untouched keys. That
2227
+ * whole-object features path is only latently launderable today (no repo layer
2228
+ * populates features); tightening it needs a coordinated change to save()'s
2229
+ * concurrent-writer merge and is tracked separately.
2230
+ */
2231
+ function buildGlobalConfigPatch(prev, next) {
2232
+ const patch = {};
2233
+ if (next.defaultModel !== prev?.defaultModel) patch.defaultModel = next.defaultModel;
2234
+ const changedPreferences = {};
2235
+ for (const [key, value] of Object.entries(next.preferences)) {
2236
+ const prevValue = (prev?.preferences)?.[key];
2237
+ if (JSON.stringify(value) !== JSON.stringify(prevValue)) changedPreferences[key] = value;
2238
+ }
2239
+ if (Object.keys(changedPreferences).length > 0) patch.preferences = changedPreferences;
2240
+ if (JSON.stringify(next.features ?? {}) !== JSON.stringify(prev?.features ?? {})) patch.features = next.features;
2241
+ return patch;
2242
+ }
2243
+ //#endregion
2110
2244
  //#region src/components/MarkdownRenderer.tsx
2111
2245
  function MarkdownRenderer({ content, columns: columnsProp }) {
2112
2246
  const columns = columnsProp ?? process.stdout.columns ?? 80;
@@ -2342,6 +2476,12 @@ function stepRows(steps, index, showThoughts) {
2342
2476
  if (step.type !== "action") return 0;
2343
2477
  return steps[index + 1]?.type === "observation" ? 3 : 2;
2344
2478
  }
2479
+ /** Rows a whole window occupies, excluding the trace Box chrome. */
2480
+ function measureTraceRows(steps, { showThoughts = true } = {}) {
2481
+ let rows = 0;
2482
+ for (let i = 0; i < steps.length; i++) rows += stepRows(steps, i, showThoughts);
2483
+ return rows;
2484
+ }
2345
2485
  const countRenderable = (steps, showThoughts) => steps.filter((step) => isRenderable(step, showThoughts)).length;
2346
2486
  const EMPTY_WINDOW = {
2347
2487
  steps: [],
@@ -2382,32 +2522,96 @@ function windowLiveTrace(steps, { rows, showThoughts = true }) {
2382
2522
  hiddenSteps: countRenderable(steps.slice(0, newest), showThoughts)
2383
2523
  };
2384
2524
  }
2525
+ /** Rows the live reply Box spends beyond its text: its bottom margin. */
2526
+ const REPLY_BOX_CHROME_ROWS = 1;
2527
+ /**
2528
+ * Hard-wraps streamed reply text into rows of at most `columns` characters.
2529
+ * MessageItem renders each row as its own `truncate-end` line, so a row is
2530
+ * exactly one terminal row; a wide character can only clip a row's end in the
2531
+ * live view, never add a row. The full markdown render lands via <Static>.
2532
+ */
2533
+ function wrapReplyRows(text, columns) {
2534
+ const width = Math.max(1, Math.floor(columns));
2535
+ const rows = [];
2536
+ for (const line of text.split("\n")) {
2537
+ if (line.length === 0) {
2538
+ rows.push("");
2539
+ continue;
2540
+ }
2541
+ for (let start = 0; start < line.length; start += width) rows.push(line.slice(start, start + width));
2542
+ }
2543
+ return rows;
2544
+ }
2545
+ /**
2546
+ * Splits the live frame's row budget between the step trace and the streaming
2547
+ * reply. The reply gets what the trace leaves over, and never less than half
2548
+ * the budget once it needs it, so a long trace cannot starve the answer the
2549
+ * user is waiting on. The reply shows its tail: the newest text is what moves.
2550
+ */
2551
+ function splitLiveRows(rows, { steps, replyRows, showThoughts = true }) {
2552
+ const budget = Math.max(0, Math.floor(rows));
2553
+ if (replyRows.length === 0) return {
2554
+ traceRows: budget,
2555
+ replyRows: []
2556
+ };
2557
+ const traceNeeded = steps.length > 0 ? measureTraceRows(steps, { showThoughts }) + TRACE_BOX_CHROME_ROWS : 0;
2558
+ const replyNeeded = replyRows.length + REPLY_BOX_CHROME_ROWS;
2559
+ const replyBudget = Math.min(replyNeeded, Math.max(budget - traceNeeded, Math.ceil(budget / 2)));
2560
+ const visibleReplyRows = replyBudget - REPLY_BOX_CHROME_ROWS;
2561
+ if (visibleReplyRows <= 0) return {
2562
+ traceRows: budget,
2563
+ replyRows: []
2564
+ };
2565
+ return {
2566
+ traceRows: budget - replyBudget,
2567
+ replyRows: replyRows.slice(-visibleReplyRows)
2568
+ };
2569
+ }
2385
2570
  //#endregion
2386
2571
  //#region src/components/MessageItem.tsx
2387
2572
  const TOOLS_WITH_HIDDEN_ARGS = /* @__PURE__ */ new Set(["edit_local_file"]);
2388
- const MessageItem = React.memo(function MessageItem({ message, showThoughts = true, liveTraceRows }) {
2573
+ /** Left padding of the reply body, in columns. */
2574
+ const REPLY_INDENT = 2;
2575
+ const MessageItem = React.memo(function MessageItem({ message, showThoughts = true, liveRows }) {
2389
2576
  const isUser = message.role === "user";
2390
2577
  const [terminalCols] = useStdoutDimensions();
2391
- const isLive = liveTraceRows !== void 0;
2578
+ const isLive = liveRows !== void 0;
2392
2579
  const traceWrap = isLive ? "truncate-end" : "wrap";
2393
2580
  const oneLine = (text) => isLive ? text.replace(/\s+/g, " ") : text;
2581
+ const hasReply = !isUser && message.content !== "...";
2394
2582
  const allSteps = message.metadata?.steps;
2583
+ const liveSplit = React.useMemo(() => {
2584
+ if (liveRows === void 0) return void 0;
2585
+ const replyRows = hasReply ? wrapReplyRows(message.content, terminalCols - REPLY_INDENT) : [];
2586
+ return splitLiveRows(liveRows, {
2587
+ steps: allSteps ?? [],
2588
+ replyRows,
2589
+ showThoughts
2590
+ });
2591
+ }, [
2592
+ liveRows,
2593
+ hasReply,
2594
+ message.content,
2595
+ terminalCols,
2596
+ allSteps,
2597
+ showThoughts
2598
+ ]);
2395
2599
  const { steps: traceSteps, hiddenSteps } = React.useMemo(() => {
2396
2600
  if (!allSteps) return {
2397
2601
  steps: [],
2398
2602
  hiddenSteps: 0
2399
2603
  };
2400
- if (liveTraceRows === void 0) return {
2604
+ if (liveSplit === void 0) return {
2401
2605
  steps: allSteps,
2402
2606
  hiddenSteps: 0
2403
2607
  };
2404
2608
  return windowLiveTrace(allSteps, {
2405
- rows: liveTraceRows,
2609
+ rows: liveSplit.traceRows,
2406
2610
  showThoughts
2407
2611
  });
2408
2612
  }, [
2409
2613
  allSteps,
2410
- liveTraceRows,
2614
+ liveSplit,
2411
2615
  showThoughts
2412
2616
  ]);
2413
2617
  const visibleTraceSteps = traceSteps.filter((s) => showThoughts && s.type === "thought" || s.type === "action").length;
@@ -2452,13 +2656,20 @@ const MessageItem = React.memo(function MessageItem({ message, showThoughts = tr
2452
2656
  }, `Result: ${oneLine(truncateValue(result, 200))}`)));
2453
2657
  }
2454
2658
  return null;
2455
- }).filter(Boolean)), !isUser && message.content !== "..." && /* @__PURE__ */ React.createElement(Box, {
2456
- paddingLeft: 2,
2659
+ }).filter(Boolean)), liveSplit && liveSplit.replyRows.length > 0 && /* @__PURE__ */ React.createElement(Box, {
2660
+ paddingLeft: REPLY_INDENT,
2661
+ marginBottom: 1,
2662
+ flexDirection: "column"
2663
+ }, liveSplit.replyRows.map((row, idx) => /* @__PURE__ */ React.createElement(Text, {
2664
+ key: idx,
2665
+ wrap: "truncate-end"
2666
+ }, row || " "))), !isLive && hasReply && /* @__PURE__ */ React.createElement(Box, {
2667
+ paddingLeft: REPLY_INDENT,
2457
2668
  marginBottom: 1
2458
2669
  }, message.metadata?.permissionDenied ? /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "⚠️ ", message.content) : /* @__PURE__ */ React.createElement(MarkdownRenderer, {
2459
2670
  content: message.content,
2460
2671
  columns: terminalCols
2461
- })), !isUser && message.metadata?.earlyStopReason && /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, message.metadata.earlyStopReason === "degenerate_repetition" ? "⚠ Response stopped early: it began repeating itself. Try rephrasing the request." : "⚠ Response was cut off at the output-token limit. Type \"continue\" to resume, or raise Max Tokens in /config.")), !isUser && message.content !== "..." && message.metadata && /* @__PURE__ */ React.createElement(Box, {
2672
+ })), !isUser && message.metadata?.earlyStopReason && /* @__PURE__ */ React.createElement(Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, message.metadata.earlyStopReason === "degenerate_repetition" ? "⚠ Response stopped early: it began repeating itself. Try rephrasing the request." : "⚠ Response was cut off at the output-token limit. Type \"continue\" to resume, or raise Max Tokens in /config.")), !isLive && hasReply && message.metadata && /* @__PURE__ */ React.createElement(Box, {
2462
2673
  paddingLeft: 2,
2463
2674
  marginBottom: 1
2464
2675
  }, (message.metadata.tokenUsage?.total || message.metadata.creditsUsed) && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "(", message.metadata.tokenUsage?.total ? `${message.metadata.tokenUsage.total.toLocaleString()} tokens` : "", message.metadata.creditsUsed && message.metadata.creditsUsed > 0 ? (message.metadata.tokenUsage?.total ? " • " : "") + `used ${message.metadata.creditsUsed.toLocaleString()} ${message.metadata.creditsUsed === 1 ? "credit" : "credits"}` : "", ")")));
@@ -2542,7 +2753,7 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
2542
2753
  useInput(() => setPromptInputTick((tick) => tick + 1), { isActive: promptKey !== "" });
2543
2754
  const promptRows = promptKey === "" ? 0 : measuredPrompt?.key === promptKey ? measuredPrompt.rows : Infinity;
2544
2755
  const [terminalCols, terminalRows] = useStdoutDimensions();
2545
- const liveTraceRows = Math.max(0, terminalRows - LIVE_CHROME_ROWS - messageQueue.length - promptRows);
2756
+ const liveRows = Math.max(0, terminalRows - LIVE_CHROME_ROWS - messageQueue.length - promptRows);
2546
2757
  const handleSubmit = React.useCallback(async (input) => {
2547
2758
  const trimmed = input.trim();
2548
2759
  if (!trimmed) return;
@@ -2610,7 +2821,7 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
2610
2821
  }, /* @__PURE__ */ React.createElement(MessageItem, {
2611
2822
  message,
2612
2823
  showThoughts,
2613
- liveTraceRows
2824
+ liveRows
2614
2825
  })))), /* @__PURE__ */ React.createElement(Box, {
2615
2826
  ref: promptRef,
2616
2827
  flexDirection: "column"
@@ -2813,10 +3024,12 @@ function RewindSelector({ messages, onSelect, onCancel }) {
2813
3024
  const [step, setStep] = useState("selection");
2814
3025
  const [selectedMessageIndex, setSelectedMessageIndex] = useState(null);
2815
3026
  useInput((input, key) => {
2816
- if (key.escape) if (step === "confirmation") {
2817
- setStep("selection");
2818
- setSelectedMessageIndex(null);
2819
- } else onCancel();
3027
+ if (key.escape) {
3028
+ if (step === "confirmation") {
3029
+ setStep("selection");
3030
+ setSelectedMessageIndex(null);
3031
+ } else onCancel();
3032
+ }
2820
3033
  });
2821
3034
  const items = messages.map((msg, idx) => ({
2822
3035
  msg,
@@ -2879,10 +3092,12 @@ function SessionSelector({ sessions, currentSession, onSelect, onCancel }) {
2879
3092
  const [step, setStep] = useState("selection");
2880
3093
  const [selectedSession, setSelectedSession] = useState(null);
2881
3094
  useInput((_input, key) => {
2882
- if (key.escape) if (step === "confirmation") {
2883
- setStep("selection");
2884
- setSelectedSession(null);
2885
- } else onCancel();
3095
+ if (key.escape) {
3096
+ if (step === "confirmation") {
3097
+ setStep("selection");
3098
+ setSelectedSession(null);
3099
+ } else onCancel();
3100
+ }
2886
3101
  });
2887
3102
  const hasUnsavedWork = currentSession && currentSession.messages.length > 0;
2888
3103
  const formatTimeAgo = (timestamp) => {
@@ -3206,138 +3421,6 @@ function LoginFlow({ apiUrl = "http://localhost:3000", configStore, onSuccess, o
3206
3421
  }, deviceFlow?.user_code)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" }), " ", statusMessage)), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Expires in ", deviceFlow ? Math.floor(deviceFlow.expires_in / 60) : 0, " minutes")));
3207
3422
  }
3208
3423
  //#endregion
3209
- //#region src/storage/CommandHistoryStore.ts
3210
- /**
3211
- * Maximum number of history entries to keep
3212
- */
3213
- const MAX_HISTORY_ENTRIES = 1e3;
3214
- /**
3215
- * Manages command history stored in JSONL format
3216
- * Uses append-friendly JSONL for efficient writes
3217
- */
3218
- var CommandHistoryStore = class {
3219
- constructor(historyPath) {
3220
- this.history = null;
3221
- this.historyPath = historyPath || path.join(homedir(), ".bike4mind", "history.jsonl");
3222
- }
3223
- /**
3224
- * Initialize history directory
3225
- */
3226
- async init() {
3227
- const dir = path.dirname(this.historyPath);
3228
- try {
3229
- await promises.mkdir(dir, { recursive: true });
3230
- } catch (error) {
3231
- console.error("Failed to initialize history directory:", error);
3232
- throw error;
3233
- }
3234
- }
3235
- /**
3236
- * Load command history from disk
3237
- * Returns array of commands (newest first)
3238
- */
3239
- async load() {
3240
- if (this.history) return this.history;
3241
- try {
3242
- const entries = (await promises.readFile(this.historyPath, "utf-8")).trim().split("\n").filter((line) => line.length > 0).map((line) => {
3243
- try {
3244
- return JSON.parse(line);
3245
- } catch {
3246
- return null;
3247
- }
3248
- }).filter((entry) => entry !== null);
3249
- this.history = entries.sort((a, b) => b.timestamp - a.timestamp).map((entry) => entry.command);
3250
- return this.history;
3251
- } catch (error) {
3252
- if (error.code === "ENOENT") {
3253
- this.history = [];
3254
- return this.history;
3255
- }
3256
- throw error;
3257
- }
3258
- }
3259
- /**
3260
- * Add a command to history
3261
- * Skips if it's the same as the most recent command (deduplicate)
3262
- */
3263
- async add(command) {
3264
- await this.load();
3265
- if (!command || !command.trim()) return;
3266
- if (this.history && this.history.length > 0 && this.history[0] === command) return;
3267
- const entry = {
3268
- command,
3269
- timestamp: Date.now()
3270
- };
3271
- await this.init();
3272
- const line = JSON.stringify(entry) + "\n";
3273
- await promises.appendFile(this.historyPath, line, "utf-8");
3274
- this.history = [command, ...this.history || []];
3275
- if (this.history.length > MAX_HISTORY_ENTRIES) await this.trim();
3276
- }
3277
- /**
3278
- * Trim history to max entries
3279
- * Rewrites the entire file with the most recent entries
3280
- */
3281
- async trim() {
3282
- if (!this.history || this.history.length <= MAX_HISTORY_ENTRIES) return;
3283
- const trimmedCommands = this.history.slice(0, MAX_HISTORY_ENTRIES);
3284
- const lines = trimmedCommands.reverse().map((command) => {
3285
- return JSON.stringify({
3286
- command,
3287
- timestamp: Date.now()
3288
- });
3289
- }).join("\n");
3290
- await promises.writeFile(this.historyPath, lines + "\n", "utf-8");
3291
- this.history = trimmedCommands;
3292
- }
3293
- /**
3294
- * Clear all command history
3295
- */
3296
- async clear() {
3297
- try {
3298
- await promises.unlink(this.historyPath);
3299
- } catch (error) {
3300
- if (error.code !== "ENOENT") throw error;
3301
- }
3302
- this.history = [];
3303
- }
3304
- /**
3305
- * Get all commands (newest first)
3306
- */
3307
- async list() {
3308
- return this.load();
3309
- }
3310
- };
3311
- //#endregion
3312
- //#region src/storage/buildGlobalConfigPatch.ts
3313
- /**
3314
- * Compute the minimal global-config patch for a /config editor save: only the
3315
- * editor-owned fields (defaultModel, preferences, features) that the user
3316
- * actually changed vs the current effective (merged) config. Unchanged fields
3317
- * are omitted, so a repo-injected value the user never touched is never
3318
- * laundered into the global layer by a save() that spreads the merged config.
3319
- *
3320
- * defaultModel and preferences diff per key. features stays whole-object when
3321
- * anything changed, on purpose: ConfigStore.save() feeds config.features to
3322
- * mergeFeatures as the caller's COMPLETE desired set (absence of a key means
3323
- * "removed"), so a per-key diff would make save() delete untouched keys. That
3324
- * whole-object features path is only latently launderable today (no repo layer
3325
- * populates features); tightening it needs a coordinated change to save()'s
3326
- * concurrent-writer merge and is tracked separately.
3327
- */
3328
- function buildGlobalConfigPatch(prev, next) {
3329
- const patch = {};
3330
- if (next.defaultModel !== prev?.defaultModel) patch.defaultModel = next.defaultModel;
3331
- const changedPreferences = {};
3332
- for (const [key, value] of Object.entries(next.preferences)) {
3333
- const prevValue = (prev?.preferences)?.[key];
3334
- if (JSON.stringify(value) !== JSON.stringify(prevValue)) changedPreferences[key] = value;
3335
- }
3336
- if (Object.keys(changedPreferences).length > 0) patch.preferences = changedPreferences;
3337
- if (JSON.stringify(next.features ?? {}) !== JSON.stringify(prev?.features ?? {})) patch.features = next.features;
3338
- return patch;
3339
- }
3340
- //#endregion
3341
3424
  //#region src/utils/formatStep.ts
3342
3425
  const MAX_INPUT_LENGTH = 500;
3343
3426
  /**
@@ -4054,7 +4137,7 @@ async function writeLocalHandoffFile(session, options = {}) {
4054
4137
  function isLlmUnavailableError(error) {
4055
4138
  if (!(error instanceof Error)) return false;
4056
4139
  const message = error.message;
4057
- return message.includes("Rate limit exceeded") || message.includes("Authentication expired") || message.includes("Authentication failed") || message.includes("Cannot connect to Bike4Mind server") || message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("ENOTFOUND") || message.includes("ECONNRESET") || message.includes("Failed to complete LLM request") || /Request failed with status 5\d\d/.test(message) || /^5\d\d\b/.test(message);
4140
+ return message.includes("Rate limit exceeded") || message.includes("Authentication expired") || message.includes("Authentication failed") || message.includes("Cannot connect to Bike4Mind server") || message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("ENOTFOUND") || message.includes("ECONNRESET") || message.includes("Failed to complete LLM request") || /Request failed with status 5\d\d/.test(message) || /^5\d\d\b/.test(message) || /\(upstream 5\d\d\)/.test(message);
4058
4141
  }
4059
4142
  //#endregion
4060
4143
  //#region src/utils/compaction.ts
@@ -4187,18 +4270,14 @@ function flattenContent(content) {
4187
4270
  * @param session - The session this turn belongs to. Only used for
4188
4271
  * `createCompactedSession`'s summary-message formatting (and workflow
4189
4272
  * handoff parity); its `messages` are not read or mutated.
4190
- * @param initialMessageCount - Length of the protected prefix (system +
4191
- * previousMessages + current query) passed into the matching `agent.run()`
4192
- * call. Must match exactly - it is the boundary below which messages are
4193
- * never touched, so the turn's own query is never summarized away.
4194
4273
  *
4195
4274
  * Workflow state (decisions/blockers) needs no flush here: this handler only
4196
4275
  * rewrites the in-flight `IMessage[]` working history and discards the
4197
4276
  * compacted session's metadata, leaving the stores and `session.metadata`
4198
4277
  * untouched. The turn's own post-run save syncs the stores afterwards.
4199
4278
  */
4200
- function createReactiveCompactionHandler(agent, session, initialMessageCount) {
4201
- return async (messages) => {
4279
+ function createReactiveCompactionHandler(agent, session) {
4280
+ return async (messages, initialMessageCount) => {
4202
4281
  const cutIndex = findIterationBoundary(messages, PRESERVE_RECENT_ITERATIONS, initialMessageCount);
4203
4282
  const toSummarize = messages.slice(initialMessageCount, cutIndex);
4204
4283
  if (toSummarize.length === 0) return null;
@@ -4419,13 +4498,147 @@ var MessageBuilder = class {
4419
4498
  if (typeof message.content === "string") return message.content;
4420
4499
  return message.content.map((item) => {
4421
4500
  if (item.type === "text") return item.text;
4422
- else if (item.type === "image") if (item.source.type === "url") return `[Image: ${item.source.url}]`;
4423
- else return `[Image: base64 ${item.source.media_type}]`;
4501
+ else if (item.type === "image") {
4502
+ if (item.source.type === "url") return `[Image: ${item.source.url}]`;
4503
+ else return `[Image: base64 ${item.source.media_type}]`;
4504
+ }
4424
4505
  return "";
4425
4506
  }).join("");
4426
4507
  }
4427
4508
  };
4428
4509
  //#endregion
4510
+ //#region src/utils/runBangCommand.ts
4511
+ /** How long a `!command` may run before it is killed (ms). */
4512
+ const BANG_COMMAND_TIMEOUT_MS = 3e5;
4513
+ const BANG_COMMAND_MAX_OUTPUT_CHARS = 10485760;
4514
+ const USE_PROCESS_GROUP = process.platform !== "win32";
4515
+ const runningCommands = /* @__PURE__ */ new Map();
4516
+ /** Signal the command's whole process tree, so `make && ./server` does not outlive a kill. */
4517
+ function killCommand(child) {
4518
+ try {
4519
+ if (USE_PROCESS_GROUP && child.pid !== void 0) process.kill(-child.pid, "SIGTERM");
4520
+ else child.kill("SIGTERM");
4521
+ } catch {}
4522
+ }
4523
+ /** Kills every `!command` still running; called on CLI exit so none is orphaned. Their results resolve `null`. */
4524
+ function killRunningBangCommands() {
4525
+ for (const stop of runningCommands.values()) stop("exit");
4526
+ runningCommands.clear();
4527
+ }
4528
+ /**
4529
+ * Runs a `!command` typed in the TUI without blocking the event loop, through the platform shell
4530
+ * like the execSync it replaces. Never rejects: every failure comes back as `isError` output, except
4531
+ * a command killed because the CLI is exiting, which resolves `null` (nothing should be recorded).
4532
+ */
4533
+ function runBangCommand(command, { cwd, timeoutMs = BANG_COMMAND_TIMEOUT_MS, signal }) {
4534
+ return new Promise((resolve) => {
4535
+ let child;
4536
+ try {
4537
+ child = spawn(command, {
4538
+ cwd,
4539
+ shell: true,
4540
+ detached: USE_PROCESS_GROUP,
4541
+ stdio: [
4542
+ "ignore",
4543
+ "pipe",
4544
+ "pipe"
4545
+ ]
4546
+ });
4547
+ } catch (error) {
4548
+ resolve({
4549
+ output: error instanceof Error ? error.message : String(error),
4550
+ isError: true
4551
+ });
4552
+ return;
4553
+ }
4554
+ let stdout = "";
4555
+ let stderr = "";
4556
+ let failure = null;
4557
+ const stop = (reason) => {
4558
+ failure ??= reason;
4559
+ killCommand(child);
4560
+ };
4561
+ runningCommands.set(child, stop);
4562
+ const collect = (append) => (text) => {
4563
+ if (failure) return;
4564
+ append(text);
4565
+ if (stdout.length + stderr.length > BANG_COMMAND_MAX_OUTPUT_CHARS) stop("overflow");
4566
+ };
4567
+ child.stdout?.setEncoding("utf-8");
4568
+ child.stderr?.setEncoding("utf-8");
4569
+ child.stdout?.on("data", collect((text) => stdout += text));
4570
+ child.stderr?.on("data", collect((text) => stderr += text));
4571
+ const timer = setTimeout(() => stop("timeout"), timeoutMs);
4572
+ const cancel = () => stop("cancelled");
4573
+ if (signal?.aborted) cancel();
4574
+ signal?.addEventListener("abort", cancel, { once: true });
4575
+ const finish = (result) => {
4576
+ clearTimeout(timer);
4577
+ signal?.removeEventListener("abort", cancel);
4578
+ runningCommands.delete(child);
4579
+ resolve(result);
4580
+ };
4581
+ child.on("error", (error) => finish({
4582
+ output: error.message,
4583
+ isError: true
4584
+ }));
4585
+ child.on("close", (code) => {
4586
+ if (failure === "exit") finish(null);
4587
+ else if (failure === "timeout") {
4588
+ const partial = stdout ? `${stdout.trimEnd()}\n` : "";
4589
+ finish({
4590
+ output: `${partial}Command timed out after ${Math.round(timeoutMs / 1e3)}s`,
4591
+ isError: true
4592
+ });
4593
+ } else if (failure === "cancelled") finish({
4594
+ output: "Command cancelled",
4595
+ isError: true
4596
+ });
4597
+ else if (failure === "overflow") finish({
4598
+ output: `Command output exceeded ${BANG_COMMAND_MAX_OUTPUT_CHARS / 1024 / 1024} MB`,
4599
+ isError: true
4600
+ });
4601
+ else if (code === 0) finish({
4602
+ output: stdout,
4603
+ isError: false
4604
+ });
4605
+ else finish({
4606
+ output: stderr || stdout || `Command failed: ${command}`,
4607
+ isError: true
4608
+ });
4609
+ });
4610
+ });
4611
+ }
4612
+ //#endregion
4613
+ //#region src/session/whenTurnIdle.ts
4614
+ /**
4615
+ * Runs `callback` once nothing holds the session (see `holdSession` in store/index.ts). Agent
4616
+ * turns, custom commands, background completion and compaction write the session back from a
4617
+ * snapshot taken at their start, so anything appended to it meanwhile would be clobbered.
4618
+ *
4619
+ * The callback runs synchronously as the last hold is released, before a queued turn can start
4620
+ * and take its own snapshot. It runs inside that release, so a throw is logged rather than
4621
+ * escaping into the releasing operation's `finally`.
4622
+ */
4623
+ function whenTurnIdle(callback) {
4624
+ const runCallback = () => {
4625
+ try {
4626
+ callback();
4627
+ } catch (error) {
4628
+ logger.debug(`[whenTurnIdle] callback failed: ${error instanceof Error ? error.message : String(error)}`);
4629
+ }
4630
+ };
4631
+ if (useCliStore.getState().sessionHolds === 0) {
4632
+ runCallback();
4633
+ return;
4634
+ }
4635
+ const unsubscribe = useCliStore.subscribe((state) => {
4636
+ if (state.sessionHolds > 0) return;
4637
+ unsubscribe();
4638
+ runCallback();
4639
+ });
4640
+ }
4641
+ //#endregion
4429
4642
  //#region src/utils/peonNotifier.ts
4430
4643
  /**
4431
4644
  * peon-ping notifier - native b4m adapter for peon-ping (https://peonping.com).
@@ -7767,6 +7980,12 @@ var BridgePresence = class {
7767
7980
  const bridgePresence = new BridgePresence();
7768
7981
  //#endregion
7769
7982
  //#region src/bootstrap/wireAgentEvents.ts
7983
+ /** Apply `update` to the in-flight assistant message, if one is pending. */
7984
+ function updatePendingAssistant(update) {
7985
+ const { pendingMessages, updatePendingMessage } = useCliStore.getState();
7986
+ const lastIdx = pendingMessages.length - 1;
7987
+ if (lastIdx >= 0 && pendingMessages[lastIdx].role === "assistant") updatePendingMessage(lastIdx, update(pendingMessages[lastIdx]));
7988
+ }
7770
7989
  /**
7771
7990
  * Wire the main agent's step events to the UI store and the tavern transcript,
7772
7991
  * and mirror the same handlers onto delegated subagents via the orchestrator's
@@ -7778,26 +7997,52 @@ const bridgePresence = new BridgePresence();
7778
7997
  * singleton; neither owns React-lifecycle state.
7779
7998
  */
7780
7999
  function wireAgentEvents(input) {
7781
- const { agent, agentContext, orchestrator } = input;
7782
- agent.observationQueue = agentContext.observationQueue;
8000
+ const { agent, orchestrator } = input;
7783
8001
  const stepHandler = (step) => {
7784
- const { pendingMessages, updatePendingMessage } = useCliStore.getState();
7785
- const lastIdx = pendingMessages.length - 1;
7786
- if (lastIdx >= 0 && pendingMessages[lastIdx].role === "assistant") {
7787
- const existingSteps = pendingMessages[lastIdx].metadata?.steps || [];
7788
- const formattedStep = formatStep(step);
7789
- updatePendingMessage(lastIdx, {
7790
- ...pendingMessages[lastIdx],
7791
- metadata: {
7792
- ...pendingMessages[lastIdx].metadata,
7793
- steps: [...existingSteps, formattedStep]
7794
- }
7795
- });
7796
- }
8002
+ updatePendingAssistant((message) => ({
8003
+ ...message,
8004
+ metadata: {
8005
+ ...message.metadata,
8006
+ steps: [...message.metadata?.steps || [], formatStep(step)]
8007
+ }
8008
+ }));
8009
+ };
8010
+ let liveText = {
8011
+ messageId: "",
8012
+ iteration: -1,
8013
+ text: ""
8014
+ };
8015
+ const textDeltaHandler = ({ delta, iteration }) => {
8016
+ updatePendingAssistant((message) => {
8017
+ const sameStream = liveText.messageId === message.id && liveText.iteration === iteration;
8018
+ liveText = {
8019
+ messageId: message.id,
8020
+ iteration,
8021
+ text: (sameStream ? liveText.text : "") + delta
8022
+ };
8023
+ return {
8024
+ ...message,
8025
+ content: liveText.text
8026
+ };
8027
+ });
8028
+ };
8029
+ const clearLiveText = () => {
8030
+ if (!liveText.text) return;
8031
+ liveText = {
8032
+ ...liveText,
8033
+ text: ""
8034
+ };
8035
+ updatePendingAssistant((message) => ({
8036
+ ...message,
8037
+ content: "..."
8038
+ }));
7797
8039
  };
7798
8040
  agent.on("thought", stepHandler);
7799
8041
  agent.on("observation", stepHandler);
7800
8042
  agent.on("action", stepHandler);
8043
+ agent.on("text_delta", textDeltaHandler);
8044
+ agent.on("thought", clearLiveText);
8045
+ agent.on("action", clearLiveText);
7801
8046
  const pendingToolUseIds = /* @__PURE__ */ new Map();
7802
8047
  const summarizeToolInput = (input) => {
7803
8048
  if (input == null) return void 0;
@@ -8152,6 +8397,14 @@ async function handleSandboxTrustDomainCommand(deps, domains) {
8152
8397
  * recurses with the same context for messages submitted mid-turn.
8153
8398
  */
8154
8399
  async function runTurn(message, ctx) {
8400
+ const releaseSession = useCliStore.getState().holdSession();
8401
+ try {
8402
+ await runHeldTurn(message, ctx);
8403
+ } finally {
8404
+ releaseSession();
8405
+ }
8406
+ }
8407
+ async function runHeldTurn(message, ctx) {
8155
8408
  const { agent, sessionStore, configStore, commandHistoryStore, customCommandStore, messageBuilder, config, availableModels, agentStore, contextContent, additionalDirectories, featureRegistry, backgroundManager, todoStore, decisionStore, blockerStore, postEditDiagnostics, workflowStores, setCommandHistory, setAbortController } = ctx;
8156
8409
  const storeSession = useCliStore.getState().session;
8157
8410
  if (!agent || !storeSession) {
@@ -8194,7 +8447,10 @@ async function runTurn(message, ctx) {
8194
8447
  try {
8195
8448
  const { prompt: compactionPrompt, preservedMessages } = buildCompactionPrompt(activeSession.messages, { claudeMdInstructions: extractCompactInstructions(contextContent || "") });
8196
8449
  if (compactionPrompt) {
8197
- const result = await agent.run(compactionPrompt, { maxIterations: 1 });
8450
+ const result = await agent.run(compactionPrompt, {
8451
+ maxIterations: 1,
8452
+ streamText: false
8453
+ });
8198
8454
  const sessionToCompact = withFlushedWorkflowState(activeSession, workflowStores);
8199
8455
  await sessionStore.save(sessionToCompact);
8200
8456
  const newSession = createCompactedSession(sessionToCompact, result.finalAnswer, preservedMessages, !!(process.env.B4M_SESSION_ID || process.env.B4M_RESUME_ID));
@@ -8270,7 +8526,7 @@ async function runTurn(message, ctx) {
8270
8526
  isReadOnlyTool,
8271
8527
  workflowReminder,
8272
8528
  drainFeedback: diagnostics ? (phase) => diagnostics.drain(DRAIN_BUDGET_MS[phase], abortController.signal) : void 0,
8273
- onContextLimit: createReactiveCompactionHandler(agent, activeSession, 1 + previousMessages.length + 1)
8529
+ onContextLimit: createReactiveCompactionHandler(agent, activeSession)
8274
8530
  });
8275
8531
  } finally {
8276
8532
  diagnostics?.endTurn();
@@ -8348,7 +8604,7 @@ async function runTurn(message, ctx) {
8348
8604
  }
8349
8605
  }
8350
8606
  const rawMessage = error instanceof Error ? error.message : String(error);
8351
- if (error instanceof Error && isTransientNetworkError(error)) {
8607
+ if (isConnectionDrop(error)) {
8352
8608
  console.error("\n❌ The connection to the server dropped mid-response. Type \"continue\" to resume.\n");
8353
8609
  logger.debug(`Full error details: ${error.stack || error.message}`);
8354
8610
  return;
@@ -8500,6 +8756,14 @@ async function resumeSession(ctx, selected) {
8500
8756
  * console notice) when there is no session/agent or too little to compact.
8501
8757
  */
8502
8758
  async function compactSession(ctx, options = {}) {
8759
+ const releaseSession = useCliStore.getState().holdSession();
8760
+ try {
8761
+ await compactHeldSession(ctx, options);
8762
+ } finally {
8763
+ releaseSession();
8764
+ }
8765
+ }
8766
+ async function compactHeldSession(ctx, options) {
8503
8767
  const session = useCliStore.getState().session;
8504
8768
  if (!session || !ctx.agent) {
8505
8769
  console.log("No active session");
@@ -8520,7 +8784,10 @@ async function compactSession(ctx, options = {}) {
8520
8784
  console.log("🗜️ Compacting conversation...\n");
8521
8785
  useCliStore.getState().setIsThinking(true);
8522
8786
  try {
8523
- const summary = (await ctx.agent.run(compactionPrompt, { maxIterations: 1 })).finalAnswer;
8787
+ const summary = (await ctx.agent.run(compactionPrompt, {
8788
+ maxIterations: 1,
8789
+ streamText: false
8790
+ })).finalAnswer;
8524
8791
  await ctx.sessionStore.save(session);
8525
8792
  const oldSessionName = session.name;
8526
8793
  const newSession = createCompactedSession(session, summary, preservedMessages, !!(process.env.B4M_SESSION_ID || process.env.B4M_RESUME_ID));
@@ -8735,6 +9002,7 @@ function CliApp() {
8735
9002
  } catch (err) {
8736
9003
  logger.debug(`[CLEANUP] Shell session reap error: ${err}`);
8737
9004
  }
9005
+ killRunningBangCommands();
8738
9006
  const session = useCliStore.getState().session;
8739
9007
  if (session) cleanupTasks.push(state.sessionStore.save(session).catch((err) => {
8740
9008
  logger.debug(`[CLEANUP] Session save error: ${err.message}`);
@@ -9028,7 +9296,6 @@ function CliApp() {
9028
9296
  };
9029
9297
  const agentContext = {
9030
9298
  currentAgent: null,
9031
- observationQueue: [],
9032
9299
  onFileChanged: (filePath) => postEditDiagnosticsRef.current.enqueue(filePath)
9033
9300
  };
9034
9301
  const { mcpManager, agentStore, contextResult, mcpTools, loadedB4mTools, deferredB4mTools, orchestrator, backgroundManager, historyStore } = await buildSupportingStores({
@@ -9095,7 +9362,7 @@ function CliApp() {
9095
9362
  allowedDirectories: additionalDirectories
9096
9363
  };
9097
9364
  const enableSkillTool = config.preferences.enableSkillTool !== false;
9098
- const skillTool = enableSkillTool ? wrapTools([createSkillTool({
9365
+ const skillTool = enableSkillTool ? createSkillTool({
9099
9366
  customCommandStore: state.customCommandStore,
9100
9367
  subagentOrchestrator: orchestrator,
9101
9368
  sessionId: newSession.id,
@@ -9104,28 +9371,29 @@ function CliApp() {
9104
9371
  promptFn
9105
9372
  },
9106
9373
  allowedDirectories: additionalDirectories
9107
- })], cliWrapDeps)[0] : null;
9108
- const findDefinitionTool = wrapTools([createFindDefinitionTool(additionalDirectories)], cliWrapDeps)[0];
9109
- const getFileStructureTool = wrapTools([createGetFileStructureTool(additionalDirectories)], cliWrapDeps)[0];
9374
+ }) : null;
9110
9375
  const workItemTools = config.preferences.enableWorkItemTools ? createWorkItemTools(new WorkItemsClient(apiClient)) : [];
9111
- const cliTools = [
9376
+ const agentToolsRef = { current: null };
9377
+ const toolSearchTool = deferredToolRegistry.size() > 0 ? createToolSearchTool(() => {
9378
+ if (!agentToolsRef.current) throw new Error("tool_search invoked before agent context was wired");
9379
+ return agentToolsRef.current;
9380
+ }) : null;
9381
+ const cliTools = buildCliToolList({
9382
+ toolSearchTool,
9112
9383
  agentDelegateTool,
9113
- ...backgroundTools,
9384
+ backgroundTools,
9114
9385
  resumeAgentTool,
9115
9386
  writeTodosTool,
9116
9387
  decisionLogTool,
9117
- ...blockerTools,
9388
+ blockerTools,
9118
9389
  reviewGateTool,
9119
- findDefinitionTool,
9120
- getFileStructureTool,
9121
- ...workItemTools
9122
- ];
9123
- if (skillTool) cliTools.push(skillTool);
9124
- if (dynamicAgentTool) cliTools.push(dynamicAgentTool);
9125
- if (config.preferences.enableCoordinatorMode === true) {
9126
- const coordinateTaskTool = createCoordinateTaskTool(orchestrator, agentStore, newSession.id);
9127
- cliTools.push(coordinateTaskTool);
9128
- }
9390
+ findDefinitionTool: createFindDefinitionTool(additionalDirectories),
9391
+ getFileStructureTool: createGetFileStructureTool(additionalDirectories),
9392
+ workItemTools,
9393
+ skillTool,
9394
+ dynamicAgentTool,
9395
+ coordinateTaskTool: config.preferences.enableCoordinatorMode === true ? createCoordinateTaskTool(orchestrator, agentStore, newSession.id) : null
9396
+ }, cliWrapDeps);
9129
9397
  const pluginDescriptors = await new PluginStore().discover();
9130
9398
  const { registry: featureRegistry, loaded: loadedPlugins, skipped: skippedPlugins } = await buildFeatureRegistry({
9131
9399
  builtins: createBuiltinModules(config, apiClient),
@@ -9135,18 +9403,10 @@ function CliApp() {
9135
9403
  reservedToolNames: [...loadedB4mTools, ...cliTools].map((t) => t.toolSchema.name).concat("tool_search")
9136
9404
  });
9137
9405
  rewireReservedNames(state, featureRegistry);
9138
- const featureModuleToolNames = featureRegistry.getAllToolNames();
9139
- if (featureModuleToolNames.length > 0) registerFeatureModuleTools(featureModuleToolNames);
9140
9406
  if (wsManager && featureRegistry.hasModules) featureRegistry.registerAllWsHandlers(wsManager);
9141
9407
  const featureTools = featureRegistry.getAllTools();
9142
- const agentToolsRef = { current: null };
9143
- const toolSearchTool = deferredToolRegistry.size() > 0 ? createToolSearchTool(() => {
9144
- if (!agentToolsRef.current) throw new Error("tool_search invoked before agent context was wired");
9145
- return agentToolsRef.current;
9146
- }) : null;
9147
9408
  const allTools = [
9148
9409
  ...loadedB4mTools,
9149
- ...toolSearchTool ? [toolSearchTool] : [],
9150
9410
  ...cliTools,
9151
9411
  ...featureTools
9152
9412
  ];
@@ -9163,7 +9423,7 @@ function CliApp() {
9163
9423
  }
9164
9424
  if (loadedPlugins.length > 0) startupLog.push(`🔌 Plugins loaded: ${loadedPlugins.join(", ")}`);
9165
9425
  if (skippedPlugins.length > 0) startupLog.push(`⚠️ Plugins skipped: ${skippedPlugins.map((s) => s.name).join(", ")} (see debug log)`);
9166
- logger.debug(`Total tools available to agent: ${allTools.length} (${loadedB4mTools.length} B4M loaded + ${cliTools.length} CLI + ${featureTools.length} feature + ${toolSearchTool ? 1 : 0} tool_search, ${deferredB4mTools.length} B4M + ${mcpTools.length} MCP deferred)`);
9426
+ logger.debug(`Total tools available to agent: ${allTools.length} (${loadedB4mTools.length} B4M loaded + ${cliTools.length} CLI + ${featureTools.length} feature, ${deferredB4mTools.length} B4M + ${mcpTools.length} MCP deferred)`);
9167
9427
  if (contextResult.globalContext) startupLog.push(`📄 Global context: ${contextResult.globalContext.filename}`);
9168
9428
  if (contextResult.projectContext) startupLog.push(`📄 Project context: ${contextResult.projectContext.filename}`);
9169
9429
  for (const error of contextResult.errors) startupLog.push(`⚠️ Context file error: ${error}`);
@@ -9193,7 +9453,6 @@ function CliApp() {
9193
9453
  });
9194
9454
  wireAgentEvents({
9195
9455
  agent,
9196
- agentContext,
9197
9456
  orchestrator
9198
9457
  });
9199
9458
  setState((prev) => ({
@@ -9329,6 +9588,7 @@ function CliApp() {
9329
9588
  return;
9330
9589
  }
9331
9590
  useCliStore.getState().setIsThinking(true);
9591
+ const releaseSession = useCliStore.getState().holdSession();
9332
9592
  const abortController = new AbortController();
9333
9593
  setState((prev) => ({
9334
9594
  ...prev,
@@ -9403,7 +9663,7 @@ function CliApp() {
9403
9663
  signal: abortController.signal,
9404
9664
  parallelExecution: cliConfig.preferences.enableParallelToolExecution === true,
9405
9665
  isReadOnlyTool,
9406
- onContextLimit: createReactiveCompactionHandler(state.agent, session, 1 + previousMessages.length + 1)
9666
+ onContextLimit: createReactiveCompactionHandler(state.agent, session)
9407
9667
  });
9408
9668
  } finally {
9409
9669
  state.backgroundManager?.setCurrentTurn(null);
@@ -9520,6 +9780,7 @@ function CliApp() {
9520
9780
  }));
9521
9781
  state.agent.off("thought", stepHandler);
9522
9782
  state.agent.off("action", stepHandler);
9783
+ releaseSession();
9523
9784
  }
9524
9785
  };
9525
9786
  const handleMessage = (message) => runTurn(message, {
@@ -9572,11 +9833,12 @@ function CliApp() {
9572
9833
  * without adding a user message to the conversation.
9573
9834
  */
9574
9835
  const handleBackgroundCompletion = async () => {
9575
- const session = useCliStore.getState().session;
9576
- if (!state.agent || !session) return;
9577
9836
  const authTokens = await state.configStore.getAuthTokens();
9578
9837
  if (!authTokens || new Date(authTokens.expiresAt) <= /* @__PURE__ */ new Date()) return;
9838
+ const session = useCliStore.getState().session;
9839
+ if (!state.agent || !session) return;
9579
9840
  useCliStore.getState().setIsThinking(true);
9841
+ const releaseSession = useCliStore.getState().holdSession();
9580
9842
  const abortController = new AbortController();
9581
9843
  setState((prev) => ({
9582
9844
  ...prev,
@@ -9649,59 +9911,74 @@ function CliApp() {
9649
9911
  abortController: null
9650
9912
  }));
9651
9913
  useCliStore.getState().setIsThinking(false);
9914
+ releaseSession();
9652
9915
  }
9653
9916
  };
9654
- const handleBashCommand = useCallback((command) => {
9655
- const session = useCliStore.getState().session;
9656
- if (!session) return;
9657
- let output;
9658
- let isError = false;
9659
- try {
9660
- output = execSync(command, {
9661
- encoding: "utf-8",
9662
- stdio: [
9663
- "pipe",
9664
- "pipe",
9665
- "pipe"
9917
+ const handleBashCommand = useCallback(async (command) => {
9918
+ if (!useCliStore.getState().session) return;
9919
+ const ownsInput = !useCliStore.getState().isThinking;
9920
+ const sessionId = useCliStore.getState().session?.id;
9921
+ const abortController = new AbortController();
9922
+ if (ownsInput) {
9923
+ useCliStore.getState().setIsThinking(true);
9924
+ setState((prev) => ({
9925
+ ...prev,
9926
+ abortController
9927
+ }));
9928
+ }
9929
+ const result = await runBangCommand(command, {
9930
+ cwd: process.cwd(),
9931
+ signal: abortController.signal
9932
+ });
9933
+ if (!result) return;
9934
+ const { output, isError } = result;
9935
+ whenTurnIdle(() => {
9936
+ const session = useCliStore.getState().session;
9937
+ if (!session || session.id !== sessionId) return;
9938
+ const userMessage = {
9939
+ id: v4(),
9940
+ role: "user",
9941
+ content: `$ ${command}`,
9942
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9943
+ };
9944
+ const assistantMessage = {
9945
+ id: v4(),
9946
+ role: "assistant",
9947
+ content: isError ? `\u274c Error:\n${output}` : output.trim() || "(no output)",
9948
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9949
+ };
9950
+ const updatedSession = {
9951
+ ...session,
9952
+ messages: [
9953
+ ...session.messages,
9954
+ userMessage,
9955
+ assistantMessage
9666
9956
  ],
9667
- cwd: process.cwd(),
9668
- maxBuffer: 10485760
9957
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9958
+ };
9959
+ setStoreSession(updatedSession);
9960
+ state.sessionStore.save(updatedSession).catch((error) => {
9961
+ logger.debug(`[BASH] Session save error: ${error instanceof Error ? error.message : String(error)}`);
9962
+ });
9963
+ });
9964
+ setState((prev) => prev.abortController === abortController ? {
9965
+ ...prev,
9966
+ abortController: null
9967
+ } : prev);
9968
+ if (ownsInput && !abortController.signal.aborted && useCliStore.getState().sessionHolds === 0) {
9969
+ useCliStore.getState().setIsThinking(false);
9970
+ const queued = useCliStore.getState().dequeueAllMessages();
9971
+ if (queued.length > 0) setImmediate(() => {
9972
+ handleMessageRef.current?.(queued.join("\n\n"));
9669
9973
  });
9670
- } catch (error) {
9671
- const execError = error;
9672
- output = execError.stderr || execError.message || "Command failed";
9673
- isError = true;
9674
9974
  }
9675
- const userMessage = {
9676
- id: v4(),
9677
- role: "user",
9678
- content: `$ ${command}`,
9679
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
9680
- };
9681
- const assistantMessage = {
9682
- id: v4(),
9683
- role: "assistant",
9684
- content: isError ? `❌ Error:\n${output}` : output.trim() || "(no output)",
9685
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
9686
- };
9687
- const updatedSession = {
9688
- ...session,
9689
- messages: [
9690
- ...session.messages,
9691
- userMessage,
9692
- assistantMessage
9693
- ],
9694
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9695
- };
9696
- setStoreSession(updatedSession);
9697
- state.sessionStore.save(updatedSession);
9698
9975
  }, [state.sessionStore]);
9699
9976
  const handleImageDetected = async (imageData) => {
9700
9977
  try {
9701
9978
  let imageStore = state.imageStore;
9702
9979
  if (!imageStore) {
9703
9980
  if (!imageStoreInitPromise.current) imageStoreInitPromise.current = (async () => {
9704
- const { ImageStore: ImageStoreClass } = await import("./ImageStore-kVo-oHoS.mjs");
9981
+ const { ImageStore: ImageStoreClass } = await import("./ImageStore-DuI3tgwQ.mjs");
9705
9982
  const newImageStore = new ImageStoreClass();
9706
9983
  setState((prev) => ({
9707
9984
  ...prev,
@@ -9820,8 +10097,12 @@ function CliApp() {
9820
10097
  if (!prompt) return null;
9821
10098
  console.log("📝 Generating session handoff...");
9822
10099
  useCliStore.getState().setIsThinking(true);
10100
+ const releaseSession = useCliStore.getState().holdSession();
9823
10101
  try {
9824
- const result = await state.agent.run(prompt, { maxIterations: 1 });
10102
+ const result = await state.agent.run(prompt, {
10103
+ maxIterations: 1,
10104
+ streamText: false
10105
+ });
9825
10106
  const handoff = parseHandoffResponse(result.finalAnswer);
9826
10107
  if (!handoff) {
9827
10108
  console.warn("⚠️ Handoff generation returned no parseable JSON; skipping.");
@@ -9852,6 +10133,7 @@ function CliApp() {
9852
10133
  return null;
9853
10134
  } finally {
9854
10135
  useCliStore.getState().setIsThinking(false);
10136
+ releaseSession();
9855
10137
  }
9856
10138
  };
9857
10139
  /**
@@ -10424,7 +10706,7 @@ function CliApp() {
10424
10706
  return;
10425
10707
  }
10426
10708
  try {
10427
- const diffOutput = state.checkpointStore.getCheckpointDiff(diffTarget);
10709
+ const diffOutput = await state.checkpointStore.getCheckpointDiff(diffTarget);
10428
10710
  if (!diffOutput.trim()) console.log("No differences found between checkpoint and current state.");
10429
10711
  else console.log(diffOutput);
10430
10712
  } catch (err) {
@@ -10462,12 +10744,13 @@ function CliApp() {
10462
10744
  timestamp: now
10463
10745
  };
10464
10746
  } catch (error) {
10465
- if (isAxiosError(error)) if (error.response?.status === 401) console.log("\n❌ Authentication failed. Run /login to re-authenticate.\n");
10466
- else if (error.response?.status === 403) console.log("\n❌ Access denied. You may not have permission to view usage data.\n");
10467
- else if (error.response?.status === 404) console.log("\n❌ Usage endpoint not available on this server.\n");
10468
- else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") console.log("\n❌ Cannot connect to server. Check your API configuration with /api-info\n");
10469
- else console.log(`\n❌ Error fetching usage data: ${error.message}\n`);
10470
- else if (error instanceof Error) console.log(`\n❌ Error: ${error.message}\n`);
10747
+ if (isAxiosError(error)) {
10748
+ if (error.response?.status === 401) console.log("\n❌ Authentication failed. Run /login to re-authenticate.\n");
10749
+ else if (error.response?.status === 403) console.log("\n❌ Access denied. You may not have permission to view usage data.\n");
10750
+ else if (error.response?.status === 404) console.log("\n❌ Usage endpoint not available on this server.\n");
10751
+ else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") console.log("\n❌ Cannot connect to server. Check your API configuration with /api-info\n");
10752
+ else console.log(`\n❌ Error fetching usage data: ${error.message}\n`);
10753
+ } else if (error instanceof Error) console.log(`\n❌ Error: ${error.message}\n`);
10471
10754
  else console.log("\n❌ Unexpected error fetching usage data. Please try again.\n");
10472
10755
  break;
10473
10756
  }
@@ -11038,7 +11321,6 @@ function CliApp() {
11038
11321
  let newFeatureRegistry = state.featureRegistry;
11039
11322
  if (featuresChanged && state.agent) {
11040
11323
  state.featureRegistry?.disposeAll();
11041
- clearFeatureModuleTools();
11042
11324
  const apiClient = new ApiClient(requireApiUrl(updatedConfig.apiConfig), state.configStore);
11043
11325
  const oldFeatureToolNames = new Set(state.featureRegistry?.getAllToolNames() ?? []);
11044
11326
  const baseTools = state.agent.getTools().filter((t) => !oldFeatureToolNames.has(t.toolSchema.name));
@@ -11053,8 +11335,6 @@ function CliApp() {
11053
11335
  newFeatureRegistry = rebuiltRegistry;
11054
11336
  rewireReservedNames(state, rebuiltRegistry);
11055
11337
  for (const skippedPlugin of rebuilt.skipped) console.error(`\n\x1b[33m⚠️ Plugin ${skippedPlugin.name} skipped: ${skippedPlugin.reason}\x1b[0m`);
11056
- const newToolNames = newFeatureRegistry.getAllToolNames();
11057
- if (newToolNames.length > 0) registerFeatureModuleTools(newToolNames);
11058
11338
  if (state.wsManager && newFeatureRegistry.hasModules) newFeatureRegistry.registerAllWsHandlers(state.wsManager);
11059
11339
  const newFeatureTools = newFeatureRegistry.getAllTools();
11060
11340
  state.agent.setTools([...baseTools, ...newFeatureTools]);
@@ -11360,10 +11640,8 @@ function CliApp() {
11360
11640
  }
11361
11641
  });
11362
11642
  }
11363
- try {
11364
- const updateResult = await Promise.race([checkForUpdate(version), new Promise((resolve) => setTimeout(() => resolve(null), 3e3))]);
11365
- if (updateResult?.updateAvailable) console.log(`\x1b[33m ⬆ Update available: v${updateResult.currentVersion} → v${updateResult.latestVersion} (run: b4m update)\x1b[0m\n`);
11366
- } catch {}
11643
+ const cachedUpdate = await getStartupUpdateStatus(version);
11644
+ if (cachedUpdate?.updateAvailable) console.log(`\x1b[33m \u2b06 Update available: v${cachedUpdate.currentVersion} \u2192 v${cachedUpdate.latestVersion} (run: b4m update)\x1b[0m\n`);
11367
11645
  if (import.meta.url.includes("/src/") || process.env.NODE_ENV === "development") logger.debug("🔧 Running in development mode (using TypeScript source)\n");
11368
11646
  warmFileCache();
11369
11647
  render(/* @__PURE__ */ React.createElement(CliApp, null), { exitOnCtrlC: false });