scream-code 0.16.5 → 0.16.6

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.
@@ -4,8 +4,8 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
- import { $n as readSessionIndex, $t as SubagentSlots, A as langFromPath, An as startManualEmbeddingDownload, At as toggleEmptySessionHint, B as charsForTokenBudget, Bn as getLogDir, Bt as OSC11_QUERY, C as getDaemonInstructions, Cn as buildSkillSlashCommands, D as renderDiffLines, Dn as setExperimentalFlags, Dt as createMarkdownTheme, E as ToolCallComponent, En as isExperimentalFlagEnabled, Et as createEditorTheme, F as replaceTabs, Fn as loadTuiConfig, Ft as parseOsc11BackgroundTheme, G as WelcomeComponent, Gn as PRODUCT_NAME, Gt as TERMINAL_FOCUS_IN, H as getSharedSpeedTracker, Hn as CLI_COMMAND_NAME, Ht as OSC11_RESPONSE_PREFIX, I as sanitizeShellOutput, In as saveTuiConfig, It as DISABLE_TERMINAL_FOCUS_REPORTING, J as resetBreathingClock, Jn as saveCatalogCache, Jt as TERMINAL_THEME_LIGHT, K as BREATHE_CYCLE_MS, Kn as DEFAULT_CATALOG_URL, Kt as TERMINAL_FOCUS_OUT, L as SkillActivationComponent, Ln as detectShellEnvironment, Lt as DISABLE_TERMINAL_THEME_REPORTING, M as ThinkingComponent, Mn as PULSE_WAVE_FRAMES, Mt as contrastTextHex, N as NoticeMessageComponent, Nn as TuiConfigParseError, Nt as getColorPalette, O as renderDiffLinesClustered, On as getKnowledgeStore, Ot as isEmptySessionHintDismissed, P as StatusMessageComponent, Pn as TuiLikePreferencesSchema, Pt as detectTerminalTheme, Qn as appendSessionIndexEntry, Qt as lerpGradient, R as AssistantMessageComponent, Rn as getDataDir, Rt as ENABLE_TERMINAL_FOCUS_REPORTING, Sn as getNoActiveSessionMessage, St as clearInfoPanelState, T as UserMessageComponent, Tn as sortSlashCommands, Tt as resolveThemeSync, U as BackgroundAgentStatusComponent, Un as CLI_UI_MODE, Ut as OSC11_RESPONSE_PREFIX_NO_ESC, V as estimateTokens, Vn as detectInstallSource, Vt as OSC11_RESPONSE, W as AgentGroupComponent, Wn as CLI_USER_AGENT_PRODUCT, Wt as QUERY_TERMINAL_THEME, X as clearGoalState, Xn as encodeWorkDirKey, Xt as isStreaming, Y as CompactionComponent, Yn as ScreamHarness, Yt as isBusy, Z as refineGoal, Zn as normalizeWorkDir, Zt as FooterComponent, _ as selectUpdateTarget, _n as SESSION_TIPS, an as STATUS_BULLET, ar as log, at as changeThinkingLevel, b as appendJsonlLine, bn as getCtrlDHint, bt as refreshProviderBalance, c as hasDispose, cn as formatErrorMessage, cr as isOrphanedToolCallError, d as MoonLoader, dn as serializeToolResultOutput, dt as handleFusionPlanCommand, er as removeSessionIndexEntry, f as formatMemoryMemoForInjection, fn as stringValue, gn as MAIN_AGENT_ID, hn as EXIT_CONFIRM_WINDOW_MS, i as buildRoleAdditionalText, in as printableChar, ir as flushDiagnosticLogs, it as toTerminalHyperlink, j as CachedContainer, jn as PIXEL_PULSE_FRAMES, jt as createThemeStyles, k as highlightLines, kn as isEmbeddingModelCached, kt as isTurnElapsedEnabled, l as isExpandable, ln as isTodoItemShape, lr as ErrorCodes, mn as EMPTY_SESSION_HINT_URL, n as clearEvalPanelState, nr as resolveScreamHome, on as appendStreamingArgsPreview, or as resolveGlobalLogPath, ot as getModelCycleLevel, pn as truncateErrorMessage, pt as handlePlanCommand, q as getBreathingFrame, qn as fetchCatalog, qt as TERMINAL_THEME_DARK, r as openUrl, rr as MemoryMemoStore, rt as isInternalMessage, s as disposeChildren, sn as argsRecord, sr as isScreamError, t as dispatchInput, u as isPlanExpandable, un as parseStreamingArgs, ur as SCREAM_ERROR_INFO, v as refreshUpdateCache, vn as TIP_ROTATION_INTERVAL_MS, wn as BUILTIN_SLASH_COMMANDS, x as readJsonlFile, xn as getLlmNotSetMessage, xt as supportsBalance, y as readUpdateCache, yn as getCtrlCHint, z as ActivityGroupComponent, zn as getInputHistoryFile, zt as ENABLE_TERMINAL_THEME_REPORTING } from "./dispatch-VY-K2oXV.mjs";
8
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-KaFgI_Ko.mjs";
7
+ import { $n as appendSessionIndexEntry, $t as SubagentSlots, A as langFromPath, An as hasEmbeddingModelCache, At as toggleEmptySessionHint, B as charsForTokenBudget, Bn as getInputHistoryFile, Bt as OSC11_QUERY, C as getDaemonInstructions, Cn as buildSkillSlashCommands, D as renderDiffLines, Dn as setExperimentalFlags, Dt as createMarkdownTheme, E as ToolCallComponent, En as isExperimentalFlagEnabled, Et as createEditorTheme, F as replaceTabs, Fn as TuiLikePreferencesSchema, Ft as parseOsc11BackgroundTheme, G as WelcomeComponent, Gn as CLI_USER_AGENT_PRODUCT, Gt as TERMINAL_FOCUS_IN, H as getSharedSpeedTracker, Hn as detectInstallSource, Ht as OSC11_RESPONSE_PREFIX, I as sanitizeShellOutput, In as loadTuiConfig, It as DISABLE_TERMINAL_FOCUS_REPORTING, J as resetBreathingClock, Jn as fetchCatalog, Jt as TERMINAL_THEME_LIGHT, K as BREATHE_CYCLE_MS, Kn as PRODUCT_NAME, Kt as TERMINAL_FOCUS_OUT, L as SkillActivationComponent, Ln as saveTuiConfig, Lt as DISABLE_TERMINAL_THEME_REPORTING, M as ThinkingComponent, Mn as PIXEL_PULSE_FRAMES, Mt as contrastTextHex, N as NoticeMessageComponent, Nn as PULSE_WAVE_FRAMES, Nt as getColorPalette, O as renderDiffLinesClustered, On as embeddingFailureDetail, Ot as isEmptySessionHintDismissed, P as StatusMessageComponent, Pn as TuiConfigParseError, Pt as detectTerminalTheme, Qn as normalizeWorkDir, Qt as lerpGradient, R as AssistantMessageComponent, Rn as detectShellEnvironment, Rt as ENABLE_TERMINAL_FOCUS_REPORTING, Sn as getNoActiveSessionMessage, St as clearInfoPanelState, T as UserMessageComponent, Tn as sortSlashCommands, Tt as resolveThemeSync, U as BackgroundAgentStatusComponent, Un as CLI_COMMAND_NAME, Ut as OSC11_RESPONSE_PREFIX_NO_ESC, V as estimateTokens, Vn as getLogDir, Vt as OSC11_RESPONSE, W as AgentGroupComponent, Wn as CLI_UI_MODE, Wt as QUERY_TERMINAL_THEME, X as clearGoalState, Xn as ScreamHarness, Xt as isStreaming, Y as CompactionComponent, Yn as saveCatalogCache, Yt as isBusy, Z as refineGoal, Zn as encodeWorkDirKey, Zt as FooterComponent, _ as selectUpdateTarget, _n as SESSION_TIPS, an as STATUS_BULLET, ar as flushDiagnosticLogs, at as changeThinkingLevel, b as appendJsonlLine, bn as getCtrlDHint, bt as refreshProviderBalance, c as hasDispose, cn as formatErrorMessage, cr as isScreamError, d as MoonLoader, dn as serializeToolResultOutput, dr as SCREAM_ERROR_INFO, dt as handleFusionPlanCommand, er as readSessionIndex, f as formatMemoryMemoForInjection, fn as stringValue, gn as MAIN_AGENT_ID, hn as EXIT_CONFIRM_WINDOW_MS, i as buildRoleAdditionalText, in as printableChar, ir as MemoryMemoStore, it as toTerminalHyperlink, j as CachedContainer, jn as startManualEmbeddingDownload, jt as createThemeStyles, k as highlightLines, kn as getKnowledgeStore, kt as isTurnElapsedEnabled, l as isExpandable, ln as isTodoItemShape, lr as isOrphanedToolCallError, mn as EMPTY_SESSION_HINT_URL, n as clearEvalPanelState, on as appendStreamingArgsPreview, or as log, ot as getModelCycleLevel, pn as truncateErrorMessage, pt as handlePlanCommand, q as getBreathingFrame, qn as DEFAULT_CATALOG_URL, qt as TERMINAL_THEME_DARK, r as openUrl, rr as resolveScreamHome, rt as isInternalMessage, s as disposeChildren, sn as argsRecord, sr as resolveGlobalLogPath, t as dispatchInput, tr as removeSessionIndexEntry, u as isPlanExpandable, un as parseStreamingArgs, ur as ErrorCodes, v as refreshUpdateCache, vn as TIP_ROTATION_INTERVAL_MS, wn as BUILTIN_SLASH_COMMANDS, x as readJsonlFile, xn as getLlmNotSetMessage, xt as supportsBalance, y as readUpdateCache, yn as getCtrlCHint, z as ActivityGroupComponent, zn as getDataDir, zt as ENABLE_TERMINAL_THEME_REPORTING } from "./dispatch-fjwbqaYI.mjs";
8
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-DBlx8pMR.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
11
11
  import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -32,7 +32,7 @@ function optionalBuildString(value) {
32
32
  return typeof value === "string" && value.length > 0 ? value : void 0;
33
33
  }
34
34
  const SCREAM_BUILD_INFO = {
35
- version: optionalBuildString("0.16.5"),
35
+ version: optionalBuildString("0.16.6"),
36
36
  channel: optionalBuildString(""),
37
37
  commit: optionalBuildString(""),
38
38
  buildTarget: optionalBuildString("darwin-arm64")
@@ -1986,7 +1986,8 @@ function formatBackgroundAgentTranscript(phase, meta, extras = void 0) {
1986
1986
  return {
1987
1987
  phase,
1988
1988
  headline,
1989
- detail: detailParts.length > 0 ? detailParts.join(" · ") : void 0
1989
+ detail: detailParts.length > 0 ? detailParts.join(" · ") : void 0,
1990
+ trackingId: meta.agentId
1990
1991
  };
1991
1992
  }
1992
1993
  //#endregion
@@ -2209,7 +2210,8 @@ function formatBackgroundTaskTranscript(info) {
2209
2210
  return {
2210
2211
  phase: phaseFromStatus(info.status),
2211
2212
  headline: headlineFor(info),
2212
- detail: detailFor(info)
2213
+ detail: detailFor(info),
2214
+ trackingId: info.taskId
2213
2215
  };
2214
2216
  }
2215
2217
  //#endregion
@@ -2910,6 +2912,7 @@ var SessionEventHandler = class {
2910
2912
  }
2911
2913
  handleStepCompleted(event) {
2912
2914
  this.host.streamingUI.flushNow();
2915
+ this.host.streamingUI.flushPendingApprovals();
2913
2916
  this.maybeShowDebugTiming(event);
2914
2917
  this.recordProviderLatency(event);
2915
2918
  this.drainQueuedMessagesIntoSteer();
@@ -2936,6 +2939,24 @@ var SessionEventHandler = class {
2936
2939
  sessionApiCalls: this.host.state.appState.sessionApiCalls + 1
2937
2940
  });
2938
2941
  }
2942
+ /**
2943
+ * A retried attempt that produces output again is no longer reconnecting:
2944
+ * drop the retry label as soon as it streams a delta or emits a tool call, so
2945
+ * neither the footer status block nor the status bar keeps claiming
2946
+ * "重连中 N/M" for the rest of the step. The fields are only ever set by
2947
+ * handleStepRetrying, so this is the single clearing point besides
2948
+ * step/turn boundaries.
2949
+ */
2950
+ clearReconnectState() {
2951
+ if (!(this.host.state.appState.reconnectAttempt > 0)) return;
2952
+ this.host.setAppState({
2953
+ reconnectAttempt: 0,
2954
+ reconnectMaxAttempts: void 0,
2955
+ reconnectDelayMs: void 0,
2956
+ reconnectStatusCode: void 0,
2957
+ reconnectErrorName: void 0
2958
+ });
2959
+ }
2939
2960
  maybeShowDebugTiming(event) {
2940
2961
  if (process.env["SCREAM_CODE_DEBUG"] !== "1") return;
2941
2962
  const text = formatStepDebugTiming(event);
@@ -2995,12 +3016,14 @@ var SessionEventHandler = class {
2995
3016
  }
2996
3017
  handleThinkingDelta(event) {
2997
3018
  const { state, streamingUI } = this.host;
3019
+ this.clearReconnectState();
2998
3020
  streamingUI.appendThinkingDelta(event.delta);
2999
3021
  if (canTransitionTo(state.appState.streamingPhase, "thinking")) this.host.setAppState({ streamingPhase: "thinking" });
3000
3022
  streamingUI.scheduleFlush();
3001
3023
  }
3002
3024
  handleAssistantDelta(event) {
3003
3025
  const { state, streamingUI } = this.host;
3026
+ this.clearReconnectState();
3004
3027
  if (streamingUI.hasThinkingDraft()) streamingUI.flushThinkingToTranscript();
3005
3028
  streamingUI.appendAssistantDelta(event.delta);
3006
3029
  this.host.patchLivePane({
@@ -3028,6 +3051,7 @@ var SessionEventHandler = class {
3028
3051
  }
3029
3052
  handleToolCall(event) {
3030
3053
  const { streamingUI } = this.host;
3054
+ this.clearReconnectState();
3031
3055
  streamingUI.flushNow();
3032
3056
  if (event.name === "SendSubagentMessage") {
3033
3057
  const args = argsRecord(event.args);
@@ -3059,6 +3083,7 @@ var SessionEventHandler = class {
3059
3083
  handleToolCallDelta(event) {
3060
3084
  if (event.toolCallId.length === 0) return;
3061
3085
  const { state, streamingUI } = this.host;
3086
+ this.clearReconnectState();
3062
3087
  streamingUI.accumulateToolCallDelta(event.toolCallId, event.name, event.argumentsPart);
3063
3088
  this.host.patchLivePane({
3064
3089
  pendingApproval: null,
@@ -3113,7 +3138,7 @@ var SessionEventHandler = class {
3113
3138
  message: event.message
3114
3139
  };
3115
3140
  streamingUI.completeToolResult(event.toolCallId, resultData);
3116
- if (canTransitionTo(this.host.state.appState.streamingPhase, "waiting")) this.host.setAppState({ streamingPhase: "waiting" });
3141
+ if (!streamingUI.hasPendingToolCalls() && canTransitionTo(this.host.state.appState.streamingPhase, "waiting")) this.host.setAppState({ streamingPhase: "waiting" });
3117
3142
  }
3118
3143
  handleStatusUpdate(event) {
3119
3144
  const patch = {};
@@ -3134,11 +3159,16 @@ var SessionEventHandler = class {
3134
3159
  }
3135
3160
  }
3136
3161
  handleSessionError(event) {
3162
+ const hadActiveTurn = this.host.streamingUI.hasActiveTurn();
3137
3163
  this.host.streamingUI.flushNow();
3138
3164
  this.host.streamingUI.resetToolUi();
3139
3165
  this.host.streamingUI.finalizeLiveTextBuffers();
3140
3166
  this.host.streamingUI.endActivityGroup();
3141
3167
  this.host.showError(`[${event.code}] ${event.message}`);
3168
+ if (event.code === "turn.agent_busy" && !hadActiveTurn && this.host.state.appState.streamingPhase !== "idle") {
3169
+ this.host.setAppState({ streamingPhase: "idle" });
3170
+ this.host.resetLivePane();
3171
+ }
3142
3172
  }
3143
3173
  handleSessionWarning(event) {
3144
3174
  this.host.showStatus(t("handler.warning_prefix", { message: event.message }), this.host.state.theme.colors.warning);
@@ -3637,6 +3667,19 @@ var SessionEventHandler = class {
3637
3667
  }
3638
3668
  };
3639
3669
  //#endregion
3670
+ //#region src/tui/utils/approval-notice.ts
3671
+ function buildApprovalNotice(args) {
3672
+ const sessionScoped = args.decision === "approved" && args.scope === "session";
3673
+ const tone = sessionScoped ? "approved_session" : args.decision;
3674
+ const label = args.decision === "approved" ? sessionScoped ? args.labels.approvedSession : args.labels.approved : args.decision === "rejected" ? args.labels.rejected : args.labels.cancelled;
3675
+ const feedback = args.feedback !== void 0 && args.feedback.length > 0 ? ` — "${args.feedback}"` : "";
3676
+ return {
3677
+ label,
3678
+ detail: `${args.action}${feedback}`,
3679
+ tone
3680
+ };
3681
+ }
3682
+ //#endregion
3640
3683
  //#region src/tui/utils/media-url.ts
3641
3684
  function getMediaKindLabels() {
3642
3685
  return {
@@ -4095,6 +4138,7 @@ var SessionReplayRenderer = class {
4095
4138
  }
4096
4139
  advanceTurn(context) {
4097
4140
  this.host.streamingUI.endActivityGroup();
4141
+ this.host.streamingUI.flushPendingApprovals();
4098
4142
  context.turnIndex += 1;
4099
4143
  context.stepIndex = 0;
4100
4144
  context.currentTurnId = `replay:${String(context.turnIndex)}`;
@@ -4173,21 +4217,19 @@ var SessionReplayRenderer = class {
4173
4217
  return;
4174
4218
  }
4175
4219
  const { result } = record;
4176
- const parts = [];
4177
- switch (result.decision) {
4178
- case "approved":
4179
- parts.push(result.scope === "session" ? t("replay.approved_session") : t("replay.approved"));
4180
- break;
4181
- case "rejected":
4182
- parts.push(t("replay.rejected"));
4183
- break;
4184
- case "cancelled":
4185
- parts.push(t("replay.cancelled"));
4186
- break;
4187
- }
4188
- parts.push(`: ${record.action}`);
4189
- if (result.feedback !== void 0 && result.feedback.length > 0) parts.push(` — "${result.feedback}"`);
4190
- this.host.appendTranscriptEntry(replayEntry(context, "status", parts.join(""), "notice"));
4220
+ const notice = buildApprovalNotice({
4221
+ decision: result.decision,
4222
+ scope: result.scope,
4223
+ action: record.action,
4224
+ feedback: result.feedback,
4225
+ labels: {
4226
+ approved: t("replay.approved"),
4227
+ approvedSession: t("replay.approved_session"),
4228
+ rejected: t("replay.rejected"),
4229
+ cancelled: t("replay.cancelled")
4230
+ }
4231
+ });
4232
+ this.host.streamingUI.recordApproval(record.toolCallId, notice.label, notice.detail, notice.tone);
4191
4233
  }
4192
4234
  renderPlanReviewResult(context, record) {
4193
4235
  const { result } = record;
@@ -4366,6 +4408,11 @@ var StreamingUIController = class StreamingUIController {
4366
4408
  _pendingToolComponents = /* @__PURE__ */ new Map();
4367
4409
  _pendingAgentGroup = null;
4368
4410
  _activityGroup;
4411
+ /**
4412
+ * Approval outcomes answered before their tool call was dispatched, waiting
4413
+ * for the call's row to appear (see `recordApproval`). Keyed by call id.
4414
+ */
4415
+ _pendingApprovals = /* @__PURE__ */ new Map();
4369
4416
  /** Guards the single `registerLiveComponent` call for the turn's block. */
4370
4417
  _activityGroupRegistered = false;
4371
4418
  /** Block that borrowed a card, so late results refresh their own block. */
@@ -4578,6 +4625,19 @@ var StreamingUIController = class StreamingUIController {
4578
4625
  this._streamingToolCallArguments.delete(toolCallId);
4579
4626
  return matchedCall;
4580
4627
  }
4628
+ /** True while a tool call of the step being handled has no result yet. A
4629
+ * parallel batch dispatches its results in provider order, so this is what
4630
+ * keeps the footer in "执行中" until the whole batch settles instead of
4631
+ * flipping to "等待中" on the first result. Entries from an earlier step or
4632
+ * turn are ignored: nothing can settle them any more, and counting them
4633
+ * would pin the footer in "执行中" for the rest of the turn. */
4634
+ hasPendingToolCalls() {
4635
+ for (const call of this._activeToolCalls.values()) {
4636
+ if (call.turnId !== this._currentTurnId || call.step !== this._currentStep) continue;
4637
+ if (call.result === void 0) return true;
4638
+ }
4639
+ return false;
4640
+ }
4581
4641
  /** Marks in-flight tool calls as truncated when a step hits max_tokens.
4582
4642
  * Returns the count of tool calls that were truncated. */
4583
4643
  markStepTruncated(turnId, step) {
@@ -4598,6 +4658,7 @@ var StreamingUIController = class StreamingUIController {
4598
4658
  /** Tears down replay-specific state after session history has been rendered. */
4599
4659
  cleanupAfterReplay(completedToolCallIds) {
4600
4660
  this.endActivityGroup();
4661
+ this.flushPendingApprovals();
4601
4662
  this._activeToolCalls.clear();
4602
4663
  for (const toolCallId of completedToolCallIds) this._pendingToolComponents.delete(toolCallId);
4603
4664
  this._pendingAgentGroup = null;
@@ -4665,6 +4726,65 @@ var StreamingUIController = class StreamingUIController {
4665
4726
  group.attachNotice(data);
4666
4727
  return true;
4667
4728
  }
4729
+ /**
4730
+ * Files an approval outcome with the call it allowed.
4731
+ *
4732
+ * The prompt is answered before its tool call is dispatched, so the row it
4733
+ * belongs under usually does not exist yet: while a turn is running the
4734
+ * outcome waits here for its own call to appear, and is filed under it the
4735
+ * moment it does. Two cases keep the row they always had instead — an outcome
4736
+ * answered between turns (a background agent's call, where no block will ever
4737
+ * open), and one whose call's row is not in the block that is open now (a
4738
+ * subagent's call, a card that stays standalone, a block that already
4739
+ * settled), which is mounted when the step settles.
4740
+ */
4741
+ recordApproval(callId, label, detail, tone) {
4742
+ const card = this._pendingToolComponents.get(callId);
4743
+ const owner = card === void 0 ? void 0 : this._groupByToolCard.get(card);
4744
+ if (owner !== void 0 && this.host.state.transcriptContainer.children.includes(owner) && owner.attachApproval(callId, label, detail, tone)) return;
4745
+ const group = this._activityGroup;
4746
+ if (group !== void 0 && group.attachApproval(callId, label, detail, tone)) return;
4747
+ if (!this.hasActiveTurn()) {
4748
+ this.mountApprovalNotices([{
4749
+ label,
4750
+ detail,
4751
+ tone
4752
+ }]);
4753
+ return;
4754
+ }
4755
+ this._pendingApprovals.set(callId, {
4756
+ label,
4757
+ detail,
4758
+ tone
4759
+ });
4760
+ }
4761
+ /** Files the outcome waiting for this call, now that its row exists. */
4762
+ adoptPendingApproval(group, callId) {
4763
+ const pending = this._pendingApprovals.get(callId);
4764
+ if (pending === void 0) return;
4765
+ this._pendingApprovals.delete(callId);
4766
+ if (!group.attachApproval(callId, pending.label, pending.detail, pending.tone)) this.mountApprovalNotices([pending]);
4767
+ }
4768
+ /**
4769
+ * Mounts outcomes as their own notice row — the row an approval used to get
4770
+ * straight away, and the fallback for anything no block ever claims.
4771
+ */
4772
+ mountApprovalNotices(approvals) {
4773
+ for (const approval of approvals) this.host.transcriptController.appendEntry({
4774
+ id: nextTranscriptId(),
4775
+ kind: "status",
4776
+ renderMode: "notice",
4777
+ content: `${approval.label}: ${approval.detail}`
4778
+ });
4779
+ this.host.state.ui.requestRender();
4780
+ }
4781
+ /** Settles anything still waiting: mounted as notices, in the order answered. */
4782
+ flushPendingApprovals() {
4783
+ if (this._pendingApprovals.size === 0) return;
4784
+ const parked = [...this._pendingApprovals.values()];
4785
+ this._pendingApprovals.clear();
4786
+ this.mountApprovalNotices(parked);
4787
+ }
4668
4788
  endActivityGroup() {
4669
4789
  const group = this._activityGroup;
4670
4790
  this._activityGroup = void 0;
@@ -4799,6 +4919,7 @@ var StreamingUIController = class StreamingUIController {
4799
4919
  this._streamingToolCallArguments.clear();
4800
4920
  this.disposeAndClearPendingToolComponents();
4801
4921
  this._pendingAgentGroup = null;
4922
+ this.flushPendingApprovals();
4802
4923
  }
4803
4924
  resetToolCallState() {
4804
4925
  this._activeToolCalls.clear();
@@ -4962,6 +5083,7 @@ var StreamingUIController = class StreamingUIController {
4962
5083
  if (state.planExpanded) tc.setPlanExpanded(true);
4963
5084
  if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
4964
5085
  if (!this.tryAttachAgentToolCall(toolCall, tc)) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
5086
+ if (toolCall.name === "ExitPlanMode") this.endActivityGroup();
4965
5087
  tc.setExpanded(state.toolOutputExpanded);
4966
5088
  state.transcriptContainer.addChild(tc);
4967
5089
  state.ui.requestRender();
@@ -4969,7 +5091,8 @@ var StreamingUIController = class StreamingUIController {
4969
5091
  const group = this.ensureActivityGroup();
4970
5092
  this.registerActivityGroupEntry(group, entry);
4971
5093
  this._groupByToolCard.set(tc, group);
4972
- group.attachTool(tc, toolCall.step ?? this._currentStep);
5094
+ group.attachTool(tc, toolCall.step ?? this._currentStep, toolCall.id);
5095
+ this.adoptPendingApproval(group, toolCall.id);
4973
5096
  state.ui.requestRender();
4974
5097
  }
4975
5098
  if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
@@ -6281,6 +6404,9 @@ var TranscriptController = class TranscriptController {
6281
6404
  welcomeComponent;
6282
6405
  committedComponent;
6283
6406
  liveComponentToEntry = /* @__PURE__ */ new Map();
6407
+ /** Live `started` notice cards by task id, so the terminal notice can stop the
6408
+ * ticker of the card that is still claiming the task is running. */
6409
+ liveNoticesByTrackingId = /* @__PURE__ */ new Map();
6284
6410
  pendingComponents = /* @__PURE__ */ new Set();
6285
6411
  /** Max live transcript children before the oldest are folded into the
6286
6412
  * committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
@@ -6306,6 +6432,26 @@ var TranscriptController = class TranscriptController {
6306
6432
  releaseLiveComponent(component) {
6307
6433
  this.liveComponentToEntry.delete(component);
6308
6434
  }
6435
+ /**
6436
+ * Stop everything a component owns once it leaves the live area. Committed
6437
+ * views re-render from the entry data, so disposing cannot lose content, but
6438
+ * skipping it would let an animation timer outlive the row it belonged to.
6439
+ */
6440
+ dropLiveComponent(component) {
6441
+ for (const [trackingId, tracked] of this.liveNoticesByTrackingId) if (tracked === component) this.liveNoticesByTrackingId.delete(trackingId);
6442
+ if (hasDispose(component)) component.dispose();
6443
+ }
6444
+ /**
6445
+ * Stops the ticker of the card that announced `trackingId`. Called when the
6446
+ * task reaches a terminal state: that card is history now, and a glyph that
6447
+ * keeps rotating would claim the task is still running.
6448
+ */
6449
+ settleLiveNotice(trackingId) {
6450
+ const tracked = this.liveNoticesByTrackingId.get(trackingId);
6451
+ if (tracked === void 0) return;
6452
+ this.liveNoticesByTrackingId.delete(trackingId);
6453
+ tracked.settle();
6454
+ }
6309
6455
  markPending(component) {
6310
6456
  this.pendingComponents.add(component);
6311
6457
  }
@@ -6347,6 +6493,7 @@ var TranscriptController = class TranscriptController {
6347
6493
  this.committedComponent.appendEntry(entry, state.theme.colors);
6348
6494
  container.removeChild(component);
6349
6495
  this.liveComponentToEntry.delete(component);
6496
+ this.dropLiveComponent(component);
6350
6497
  }
6351
6498
  this.committedComponent.setCount(this.committedComponent.getCount() + toCommit.length);
6352
6499
  if (process.env["SCREAM_CODE_DEBUG"] === "1") this.host.showStatus(`[debug] committed=${this.committedComponent.getCount()} live=${this.getLiveCount()}`);
@@ -6381,10 +6528,10 @@ var TranscriptController = class TranscriptController {
6381
6528
  if (state.planExpanded) tc.setPlanExpanded(true);
6382
6529
  return tc;
6383
6530
  }
6384
- if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors);
6531
+ if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors, state.ui);
6385
6532
  return entry.renderMode === "notice" ? new NoticeMessageComponent(entry.content, entry.detail, state.theme.colors, entry.noticeMarkerColor) : new StatusMessageComponent(entry.content, state.theme.colors, entry.color);
6386
6533
  case "status":
6387
- if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors);
6534
+ if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors, state.ui);
6388
6535
  return entry.renderMode === "notice" ? new NoticeMessageComponent(entry.content, entry.detail, state.theme.colors, entry.noticeMarkerColor) : new StatusMessageComponent(entry.content, state.theme.colors, entry.color);
6389
6536
  case "cron":
6390
6537
  if (entry.cronData === void 0) return null;
@@ -6399,8 +6546,15 @@ var TranscriptController = class TranscriptController {
6399
6546
  if (component) {
6400
6547
  this.liveComponentToEntry.set(component, entry);
6401
6548
  const status = entry.backgroundAgentStatus;
6402
- if (status !== void 0 && this.host.streamingUI.attachNotice(status)) this.releaseLiveComponent(component);
6403
- else this.host.state.transcriptContainer.addChild(component);
6549
+ if (status !== void 0 && component instanceof BackgroundAgentStatusComponent) {
6550
+ const trackingId = status.trackingId;
6551
+ if (trackingId !== void 0) if (status.phase === "started") this.liveNoticesByTrackingId.set(trackingId, component);
6552
+ else this.settleLiveNotice(trackingId);
6553
+ }
6554
+ if (status !== void 0 && this.host.streamingUI.attachNotice(status)) {
6555
+ this.releaseLiveComponent(component);
6556
+ this.dropLiveComponent(component);
6557
+ } else this.host.state.transcriptContainer.addChild(component);
6404
6558
  this.host.state.ui.requestRender();
6405
6559
  }
6406
6560
  return component ?? null;
@@ -6426,26 +6580,19 @@ var TranscriptController = class TranscriptController {
6426
6580
  }
6427
6581
  appendApprovalEntry(request, response) {
6428
6582
  if (request.toolName === "ExitPlanMode" || request.display.kind === "plan_review") return;
6429
- const parts = [];
6430
- switch (response.decision) {
6431
- case "approved":
6432
- parts.push(response.scope === "session" ? t("tc.approved_session") : t("tc.approved"));
6433
- break;
6434
- case "rejected":
6435
- parts.push(t("tc.rejected"));
6436
- break;
6437
- case "cancelled":
6438
- parts.push(t("tc.cancelled"));
6439
- break;
6440
- }
6441
- parts.push(`: ${request.action}`);
6442
- if (response.feedback !== void 0 && response.feedback.length > 0) parts.push(` — "${response.feedback}"`);
6443
- this.appendEntry({
6444
- id: nextTranscriptId(),
6445
- kind: "status",
6446
- renderMode: "notice",
6447
- content: parts.join("")
6583
+ const notice = buildApprovalNotice({
6584
+ decision: response.decision,
6585
+ scope: response.scope,
6586
+ action: request.action,
6587
+ feedback: response.feedback,
6588
+ labels: {
6589
+ approved: t("tc.approved"),
6590
+ approvedSession: t("tc.approved_session"),
6591
+ rejected: t("tc.rejected"),
6592
+ cancelled: t("tc.cancelled")
6593
+ }
6448
6594
  });
6595
+ this.host.streamingUI.recordApproval(request.toolCallId, notice.label, notice.detail, notice.tone);
6449
6596
  }
6450
6597
  renderWelcome() {
6451
6598
  const { state } = this.host;
@@ -6464,6 +6611,7 @@ var TranscriptController = class TranscriptController {
6464
6611
  const { state, streamingUI, imageStore } = this.host;
6465
6612
  streamingUI.discardPending();
6466
6613
  streamingUI.endActivityGroup();
6614
+ streamingUI.flushPendingApprovals();
6467
6615
  state.transcriptEntries = [];
6468
6616
  streamingUI.disposeActiveCompactionBlock();
6469
6617
  streamingUI.resetLiveText();
@@ -13373,6 +13521,7 @@ function createInitialAppState(input) {
13373
13521
  isSwitchingSession: false,
13374
13522
  streamingPhase: "idle",
13375
13523
  streamingStartTime: 0,
13524
+ pendingUserAction: null,
13376
13525
  theme: input.tuiConfig.theme,
13377
13526
  version: input.version,
13378
13527
  hasNewVersion: false,
@@ -13809,14 +13958,28 @@ var ScreamTUI = class {
13809
13958
  patchLivePane(patch) {
13810
13959
  if (!hasPatchChanges(this.state.livePane, patch)) return;
13811
13960
  Object.assign(this.state.livePane, patch);
13961
+ this.syncPendingUserAction();
13812
13962
  this.lifecycleController.updateActivityPane();
13813
13963
  this.state.ui.requestRender();
13814
13964
  }
13815
13965
  resetLivePane() {
13816
13966
  this.state.livePane = { ...INITIAL_LIVE_PANE };
13967
+ this.syncPendingUserAction();
13817
13968
  this.lifecycleController.updateActivityPane();
13818
13969
  this.state.ui.requestRender();
13819
13970
  }
13971
+ /**
13972
+ * Mirror the live pane's pending panels onto `AppState.pendingUserAction` so
13973
+ * the footer status block can report "等待批准/等待回答" instead of claiming a
13974
+ * running phase while a panel is blocked on the user. Every mutation of
13975
+ * `livePane` goes through patchLivePane/resetLivePane, so this stays the
13976
+ * single writer of that field.
13977
+ */
13978
+ syncPendingUserAction() {
13979
+ const { pendingApproval, pendingQuestion } = this.state.livePane;
13980
+ const next = pendingApproval !== null ? "approval" : pendingQuestion !== null ? "question" : null;
13981
+ if (next !== this.state.appState.pendingUserAction) this.setAppState({ pendingUserAction: next });
13982
+ }
13820
13983
  requireSession() {
13821
13984
  if (this.session === void 0) throw new Error(getNoActiveSessionMessage());
13822
13985
  return this.session;
@@ -13893,7 +14056,7 @@ var ScreamTUI = class {
13893
14056
  const card = new OnboardingCardComponent({
13894
14057
  colors: this.state.theme.colors,
13895
14058
  onConfigure: () => {
13896
- import("./dispatch-BNVgLsrQ.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14059
+ import("./dispatch-dDbUBD7J.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
13897
14060
  this.restoreEditor();
13898
14061
  onDone?.();
13899
14062
  });
@@ -13914,7 +14077,7 @@ var ScreamTUI = class {
13914
14077
  if (this.state.startupState === "picker") return;
13915
14078
  try {
13916
14079
  await getKnowledgeStore();
13917
- if (isEmbeddingModelCached()) {
14080
+ if (hasEmbeddingModelCache()) {
13918
14081
  startManualEmbeddingDownload().catch(() => {});
13919
14082
  return;
13920
14083
  }
@@ -13929,8 +14092,9 @@ var ScreamTUI = class {
13929
14092
  const spinner = this.showProgressSpinner(t("kw.embedding_downloading"));
13930
14093
  let ok = false;
13931
14094
  let error;
14095
+ let failureKind;
13932
14096
  try {
13933
- ({ok, error} = await startManualEmbeddingDownload());
14097
+ ({ok, error, failureKind} = await startManualEmbeddingDownload());
13934
14098
  } catch (error) {
13935
14099
  error = error instanceof Error ? error.message : String(error);
13936
14100
  }
@@ -13940,7 +14104,7 @@ var ScreamTUI = class {
13940
14104
  });
13941
14105
  if (ok) this.showStatus(t("kw.embedding_ready"));
13942
14106
  else {
13943
- const detail = [t("knowledge.download_model_retry_hint"), error].filter(Boolean).join("\n");
14107
+ const detail = embeddingFailureDetail(failureKind, error);
13944
14108
  this.showNotice(t("kw.embedding_failed"), detail);
13945
14109
  }
13946
14110
  })();
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
3
  import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
- import { nn as handleConnectCommand } from "./dispatch-VY-K2oXV.mjs";
6
+ import { nn as handleConnectCommand } from "./dispatch-fjwbqaYI.mjs";
7
7
  export { handleConnectCommand };
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
7
  import { C as join$1, D as resolve$2, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize$1, x as dirname$2, y as KnowledgeStore } from "./src-tDEINaMV.mjs";
8
8
  import { t as require_base64_js } from "./base64-js-DzVmk6Nb.mjs";
9
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-KaFgI_Ko.mjs";
9
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-DBlx8pMR.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
12
12
  import * as fs$1 from "node:fs/promises";
@@ -53627,16 +53627,68 @@ async function loadEmbedder(cacheDir) {
53627
53627
  }
53628
53628
  }
53629
53629
  /**
53630
- * Remove any previously downloaded model files for the fixed BGESmallZH model.
53631
- * Called before a manual re-download so that a corrupted/partial cache does not
53632
- * cause fastembed to fail repeatedly.
53630
+ * Cache entry names fastembed uses for the fixed BGESmallZH model: it extracts
53631
+ * into `<cacheDir>/<name>/` from the staged archive `<cacheDir>/<name>.tar.gz`.
53632
+ */
53633
+ const BGESMALLZH_CACHE_NAME = "fast-bge-small-zh-v1.5";
53634
+ const PLATFORM_FAILURE = /@anush008\/tokenizers|Failed to load native binding|Unsupported (?:OS|platform|architecture)|ERR_DLOPEN_FAILED|invalid ELF header|Exec format error|is not a valid Win32 application|GLIBC_\d+[^"\n]*not found|Error loading shared library|cannot open shared object file/iu;
53635
+ const ARCHIVE_FAILURE = /\bTAR_[A-Z_]+|incorrect header check|unexpected end of file|invalid tar|zlib/iu;
53636
+ const NETWORK_FAILURE = /\b(?:ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|socket hang up|fetch failed)\b|network error|HTTP \d{3}/iu;
53637
+ /**
53638
+ * Classify a load failure so callers can say something true about it: the four
53639
+ * causes need four different answers (unsupported platform, corrupt cache,
53640
+ * network, unknown), and they used to be reported as one network problem.
53641
+ */
53642
+ function classifyEmbeddingFailure(message) {
53643
+ if (message === void 0) return "other";
53644
+ if (PLATFORM_FAILURE.test(message)) return "platform";
53645
+ if (ARCHIVE_FAILURE.test(message)) return "archive";
53646
+ if (NETWORK_FAILURE.test(message)) return "network";
53647
+ return "other";
53648
+ }
53649
+ /**
53650
+ * Loads only the module entry point — no model download — so callers can tell
53651
+ * "this machine cannot run the local model at all" apart from "the download
53652
+ * failed". fastembed imports its native tokenizer binding at module scope, which
53653
+ * is exactly the step a platform without a published binary cannot pass.
53654
+ */
53655
+ async function probeLocalEmbeddingSupport() {
53656
+ try {
53657
+ await import("fastembed");
53658
+ return { supported: true };
53659
+ } catch (error) {
53660
+ return {
53661
+ supported: false,
53662
+ error: error instanceof Error ? error.message : String(error)
53663
+ };
53664
+ }
53665
+ }
53666
+ /**
53667
+ * Remove every trace of a previous BGESmallZH download: the extracted model
53668
+ * directory *and* the staged archive it came from.
53669
+ *
53670
+ * The archive has to go with it. fastembed skips the download whenever
53671
+ * `<cacheDir>/<name>.tar.gz` exists, and it only deletes that file after a
53672
+ * *successful* extract — its download error paths (response error, file stream
53673
+ * error) reject and leave whatever was written behind. So an archive sitting
53674
+ * next to a failed attempt is usually a partial leftover, and keeping it makes
53675
+ * every retry extract the same junk locally: no network traffic, a few
53676
+ * milliseconds, the same failure forever.
53677
+ *
53678
+ * Never throws: a file locked by another process (on Windows a mapped ONNX file
53679
+ * stays locked) must not replace the download error the caller is reporting.
53633
53680
  */
53634
53681
  function clearEmbeddingModelCache(cacheDir) {
53635
- const modelDir = join(cacheDir, "fast-bge-small-zh-v1.5");
53636
- if (existsSync(modelDir)) rmSync(modelDir, {
53637
- recursive: true,
53638
- force: true
53639
- });
53682
+ for (const name of [BGESMALLZH_CACHE_NAME, `${BGESMALLZH_CACHE_NAME}.tar.gz`]) {
53683
+ const target = join(cacheDir, name);
53684
+ if (!existsSync(target)) continue;
53685
+ try {
53686
+ rmSync(target, {
53687
+ recursive: true,
53688
+ force: true
53689
+ });
53690
+ } catch {}
53691
+ }
53640
53692
  }
53641
53693
  /**
53642
53694
  * Small config/tokenizer files that fastembed expects alongside model.onnx.
@@ -128401,7 +128453,7 @@ const backwardFrames = Array.from({ length: 6 }, (_, i) => ({
128401
128453
  const PULSE_WAVE_FRAMES = [...forwardFrames, ...backwardFrames];
128402
128454
  //#endregion
128403
128455
  //#region src/tui/commands/knowledge-store.ts
128404
- let knowledgeStoreInstance;
128456
+ let knowledgeStorePromise;
128405
128457
  let embeddingEngineInstance;
128406
128458
  let embeddingStatus = "idle";
128407
128459
  function getEmbeddingCacheDir() {
@@ -128412,24 +128464,40 @@ function getEmbeddingCacheDir() {
128412
128464
  /** Subdirectory fastembed uses for the BGESmallZH model inside the cache dir. */
128413
128465
  const EMBEDDING_MODEL_DIR = "fast-bge-small-zh-v1.5";
128414
128466
  /**
128415
- * Whether the embedding model has been downloaded to the local cache.
128416
- * Checks the ONNX weights plus the config and tokenizer sidecars — all
128417
- * required for FlagEmbedding.init to load without network access (a missing
128418
- * tokenizer would trigger a network fetch and violate the warm-up's
128419
- * offline-load guarantee).
128467
+ * Any usable cache: the model directory with its weights present.
128468
+ *
128469
+ * Such a cache can still be missing sidecars, which FlagEmbedding repairs from
128470
+ * the Hub while loading — so callers must not treat it as "no model here".
128471
+ * Flagging it as missing would block the very path that repairs it, and
128472
+ * fastembed itself skips downloading whenever the model directory exists.
128420
128473
  */
128421
- function isEmbeddingModelCached() {
128474
+ function hasEmbeddingModelCache() {
128422
128475
  const modelDir = join(getEmbeddingCacheDir(), EMBEDDING_MODEL_DIR);
128423
- return existsSync(join(modelDir, "model_optimized.onnx")) && existsSync(join(modelDir, "config.json")) && existsSync(join(modelDir, "tokenizer.json"));
128424
- }
128425
- async function getKnowledgeStore() {
128426
- if (knowledgeStoreInstance === void 0) {
128427
- knowledgeStoreInstance = new KnowledgeStore(getDataDir());
128428
- await knowledgeStoreInstance.init();
128429
- embeddingEngineInstance = createFastEmbedEngine(getEmbeddingCacheDir());
128430
- knowledgeStoreInstance.setEmbeddingEngine(embeddingEngineInstance);
128431
- }
128432
- return knowledgeStoreInstance;
128476
+ return existsSync(modelDir) && existsSync(join(modelDir, "model_optimized.onnx"));
128477
+ }
128478
+ /**
128479
+ * The store and its embedding engine are built together and published only once
128480
+ * both exist. Publishing the store first meant a single transient failure (a
128481
+ * locked knowledge.db, a full disk, a denied directory) left a store with no
128482
+ * engine that no later call could repair: every download then failed instantly
128483
+ * with "embedding engine not initialized" for the rest of the process.
128484
+ *
128485
+ * Concurrent callers share one build, and a failed build clears the slot so the
128486
+ * next caller retries from scratch.
128487
+ */
128488
+ function getKnowledgeStore() {
128489
+ knowledgeStorePromise ??= (async () => {
128490
+ const store = new KnowledgeStore(getDataDir());
128491
+ await store.init();
128492
+ const engine = createFastEmbedEngine(getEmbeddingCacheDir());
128493
+ store.setEmbeddingEngine(engine);
128494
+ embeddingEngineInstance = engine;
128495
+ return store;
128496
+ })().catch((error) => {
128497
+ knowledgeStorePromise = void 0;
128498
+ throw error;
128499
+ });
128500
+ return knowledgeStorePromise;
128433
128501
  }
128434
128502
  function getEmbeddingStatus() {
128435
128503
  return embeddingStatus;
@@ -128438,16 +128506,12 @@ function getEmbeddingStatus() {
128438
128506
  * Manually trigger the embedding model download/load.
128439
128507
  * Only mutates embeddingStatus; the actual download is delegated to
128440
128508
  * EmbeddingEngine.ensureReady() and saves the model to the shared cache dir.
128441
- * Returns { ok: true } on success, or { ok: false, error } on failure.
128442
128509
  * Concurrent calls join the in-flight download and share its result instead
128443
128510
  * of failing — the startup warm-up and a user-initiated download must never
128444
128511
  * race into a spurious "download already in progress" error.
128445
128512
  */
128446
128513
  async function startManualEmbeddingDownload() {
128447
- if (embeddingEngineInstance === void 0) return {
128448
- ok: false,
128449
- error: "embedding engine not initialized"
128450
- };
128514
+ if (embeddingEngineInstance === void 0) return failureResult("embedding engine not initialized");
128451
128515
  if (embeddingEngineInstance.available) {
128452
128516
  embeddingStatus = "ready";
128453
128517
  return {
@@ -128463,29 +128527,71 @@ async function startManualEmbeddingDownload() {
128463
128527
  return downloadPromise;
128464
128528
  }
128465
128529
  let downloadPromise;
128530
+ function failureResult(error) {
128531
+ return {
128532
+ ok: false,
128533
+ error,
128534
+ failureKind: classifyEmbeddingFailure(error)
128535
+ };
128536
+ }
128466
128537
  async function performDownload() {
128467
128538
  try {
128539
+ const support = await probeLocalEmbeddingSupport();
128540
+ if (!support.supported) {
128541
+ embeddingStatus = "failed";
128542
+ return {
128543
+ ok: false,
128544
+ error: support.error,
128545
+ failureKind: classifyEmbeddingFailure(support.error)
128546
+ };
128547
+ }
128468
128548
  let ok = await embeddingEngineInstance.ensureReady();
128469
128549
  let error = ok ? void 0 : embeddingEngineInstance.lastError;
128470
128550
  if (!ok) {
128471
- clearEmbeddingModelCache(getEmbeddingCacheDir());
128472
- ok = await embeddingEngineInstance.ensureReady();
128473
- error = ok ? void 0 : embeddingEngineInstance.lastError;
128551
+ if (classifyEmbeddingFailure(error) !== "platform") {
128552
+ clearEmbeddingModelCache(getEmbeddingCacheDir());
128553
+ ok = await embeddingEngineInstance.ensureReady();
128554
+ error = ok ? void 0 : embeddingEngineInstance.lastError;
128555
+ }
128474
128556
  }
128475
128557
  embeddingStatus = ok ? "ready" : "failed";
128476
- return {
128477
- ok,
128478
- error
128479
- };
128558
+ return ok ? { ok } : failureResult(error);
128480
128559
  } catch (error) {
128481
128560
  embeddingStatus = "failed";
128482
- return {
128483
- ok: false,
128484
- error: error instanceof Error ? error.message : String(error)
128485
- };
128561
+ return failureResult(error instanceof Error ? error.message : String(error));
128486
128562
  }
128487
128563
  }
128488
128564
  //#endregion
128565
+ //#region src/tui/commands/embedding-failure-hint.ts
128566
+ /**
128567
+ * The line shown above the raw error in a failed embedding download.
128568
+ *
128569
+ * Every cause used to share one hint that blamed the network and suggested a
128570
+ * proxy, which is wrong for a machine that simply has no local model binary —
128571
+ * that single sentence is what made four different failures look like the same
128572
+ * connectivity complaint. Kept apart from the store so the copy can be read and
128573
+ * tested on its own.
128574
+ */
128575
+ function embeddingFailureHint(kind) {
128576
+ switch (kind) {
128577
+ case "platform": return t("knowledge.embedding_platform_hint");
128578
+ case "archive": return t("knowledge.embedding_cache_corrupt_hint");
128579
+ case "network": return t("knowledge.embedding_network_hint");
128580
+ default: return t("knowledge.embedding_unknown_hint");
128581
+ }
128582
+ }
128583
+ /**
128584
+ * The detail block a failure notice shows: the hint, then the first line of the
128585
+ * raw error.
128586
+ *
128587
+ * Only the first line is displayed — Node appends a "Require stack:" block to
128588
+ * module errors, a local path dump no user needs (and the line we ask people to
128589
+ * send us) — while the engine keeps the full text for diagnosis.
128590
+ */
128591
+ function embeddingFailureDetail(kind, error) {
128592
+ return [embeddingFailureHint(kind), error?.split("\n")[0]].filter(Boolean).join("\n");
128593
+ }
128594
+ //#endregion
128489
128595
  //#region src/tui/commands/experimental-flags.ts
128490
128596
  let snapshot = {};
128491
128597
  /** Replace the cached flag snapshot. Call once after fetching via `harness.getExperimentalFlags()`. */
@@ -132053,8 +132159,15 @@ const SPINNER_FRAMES$1 = [
132053
132159
  "◉"
132054
132160
  ];
132055
132161
  const SPINNER_TICK_MS = 60;
132056
- function buildStatusLine(streamingPhase, streamingStartTime) {
132162
+ function buildStatusLine(streamingPhase, streamingStartTime, statusFlags) {
132163
+ if (statusFlags.pendingUserAction === "approval") return t("status.awaiting_approval");
132164
+ if (statusFlags.pendingUserAction === "question") return t("status.awaiting_answer");
132165
+ if (statusFlags.isCompacting) return t("status.compacting");
132057
132166
  if (streamingPhase === "idle") return t("status.idle");
132167
+ if (statusFlags.reconnectAttempt > 0) return t("status.reconnecting_short", {
132168
+ attempt: String(statusFlags.reconnectAttempt),
132169
+ max: String(statusFlags.reconnectMaxAttempts ?? 0)
132170
+ });
132058
132171
  let label;
132059
132172
  if (streamingPhase === "tool") label = t("status.tool");
132060
132173
  else if (streamingPhase === "waiting") label = t("status.waiting");
@@ -132229,7 +132342,12 @@ var FooterComponent = class {
132229
132342
  let rightText;
132230
132343
  if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
132231
132344
  else {
132232
- const statusLine = buildStatusLine(state.streamingPhase, state.streamingStartTime);
132345
+ const statusLine = buildStatusLine(state.streamingPhase, state.streamingStartTime, {
132346
+ isCompacting: state.isCompacting === true,
132347
+ pendingUserAction: state.pendingUserAction ?? null,
132348
+ reconnectAttempt: state.reconnectAttempt ?? 0,
132349
+ reconnectMaxAttempts: state.reconnectMaxAttempts
132350
+ });
132233
132351
  const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
132234
132352
  const sessionUsage = state.sessionUsage ?? {
132235
132353
  inputOther: 0,
@@ -134769,7 +134887,7 @@ async function guidedGoalSetup(host) {
134769
134887
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
134770
134888
  return;
134771
134889
  }
134772
- const { TextInputDialogComponent } = await import("./text-input-dialog-CQ6R2lns.mjs");
134890
+ const { TextInputDialogComponent } = await import("./text-input-dialog-Cj0egbsU.mjs");
134773
134891
  const initialDesc = await promptText(host, TextInputDialogComponent, {
134774
134892
  title: t("goal.setup_title_initial"),
134775
134893
  subtitle: t("goal.setup_desc_hint"),
@@ -134790,7 +134908,7 @@ async function guidedGoalSetup(host) {
134790
134908
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
134791
134909
  }
134792
134910
  async function showGoalConfigWizard(host, session, objective, replace) {
134793
- const { TextInputDialogComponent } = await import("./text-input-dialog-CQ6R2lns.mjs");
134911
+ const { TextInputDialogComponent } = await import("./text-input-dialog-Cj0egbsU.mjs");
134794
134912
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
134795
134913
  title: t("goal.wizard_title", { objective }),
134796
134914
  subtitle: t("goal.budget_turns_hint"),
@@ -135644,9 +135762,9 @@ function formatTokens$1(n) {
135644
135762
  * Builds the bullet + text of a background task notice. Shared by the standalone
135645
135763
  * card and the activity block row so a notice looks the same in both places.
135646
135764
  */
135647
- function renderBackgroundStatus(data, colors, maxCells) {
135765
+ function renderBackgroundStatus(data, colors, maxCells, frameIndex = 0) {
135648
135766
  const tone = data.phase === "started" ? colors.primary : data.phase === "completed" ? colors.success : colors.error;
135649
- const bullet = data.phase === "completed" ? chalk.hex(colors.success)(DONE_MARK) : data.phase === "failed" ? chalk.hex(colors.error)(FAILURE_MARK) : chalk.hex(colors.textDim)(`${BRAILLE_SPINNER_FRAMES[0] ?? "⠋"} `);
135767
+ const bullet = data.phase === "completed" ? chalk.hex(colors.success)(DONE_MARK) : data.phase === "failed" ? chalk.hex(colors.error)(FAILURE_MARK) : chalk.hex(colors.textDim)(`${BRAILLE_SPINNER_FRAMES[frameIndex] ?? BRAILLE_SPINNER_FRAMES[0] ?? "⠋"} `);
135650
135768
  const hasDetail = data.detail !== void 0 && data.detail.length > 0;
135651
135769
  const plain = hasDetail ? `${data.headline} (${data.detail ?? ""})` : data.headline;
135652
135770
  if (maxCells !== void 0 && visibleWidth(plain) > maxCells) return {
@@ -135661,29 +135779,81 @@ function renderBackgroundStatus(data, colors, maxCells) {
135661
135779
  var BackgroundAgentStatusComponent = class {
135662
135780
  data;
135663
135781
  colors;
135664
- bullet;
135782
+ ui;
135665
135783
  textComponent;
135666
135784
  cachedWidth;
135667
135785
  cachedLines;
135668
- constructor(data, colors) {
135786
+ spinnerFrame = 0;
135787
+ spinnerTimer;
135788
+ lastSpinnerTickAt = 0;
135789
+ /** Only a notice that is still running animates; a finished one is static. */
135790
+ live;
135791
+ constructor(data, colors, ui) {
135669
135792
  this.data = data;
135670
135793
  this.colors = colors;
135671
- const view = renderBackgroundStatus(data, colors);
135672
- this.bullet = view.bullet;
135673
- this.textComponent = new Text(view.text, 0, 0);
135794
+ this.ui = ui;
135795
+ this.textComponent = new Text(renderBackgroundStatus(data, colors).text, 0, 0);
135796
+ this.live = data.phase === "started" && ui !== void 0;
135797
+ if (this.live) this.startSpinner();
135674
135798
  }
135675
135799
  invalidate() {
135676
135800
  this.cachedWidth = void 0;
135677
135801
  this.cachedLines = void 0;
135678
135802
  }
135803
+ dispose() {
135804
+ this.stopSpinner();
135805
+ }
135806
+ /**
135807
+ * The task this card announced is over: stop the ticker and fall back to the
135808
+ * first frame, then let the cache answer again. Without the reset a settled
135809
+ * card would keep whatever frame it froze on, which reads as a stuck spinner.
135810
+ */
135811
+ settle() {
135812
+ this.stopSpinner();
135813
+ this.live = false;
135814
+ if (this.spinnerFrame === 0) return;
135815
+ this.spinnerFrame = 0;
135816
+ this.invalidate();
135817
+ }
135679
135818
  render(width) {
135680
- if (this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
135819
+ if (!this.live && this.cachedLines !== void 0 && this.cachedWidth === width) return this.cachedLines;
135820
+ const view = renderBackgroundStatus(this.data, this.colors, void 0, this.spinnerFrame);
135681
135821
  const contentWidth = Math.max(1, width - 2);
135682
- const lines = ["", ...this.textComponent.render(contentWidth).map((line, index) => (index === 0 ? this.bullet : " ") + line)];
135822
+ const lines = ["", ...this.textComponent.render(contentWidth).map((line, index) => (index === 0 ? view.bullet : " ") + line)];
135683
135823
  this.cachedWidth = width;
135684
135824
  this.cachedLines = lines;
135685
135825
  return lines;
135686
135826
  }
135827
+ startSpinner() {
135828
+ if (this.spinnerTimer !== void 0) return;
135829
+ this.lastSpinnerTickAt = performance.now();
135830
+ this.scheduleSpinnerTick(80);
135831
+ }
135832
+ stopSpinner() {
135833
+ if (this.spinnerTimer === void 0) return;
135834
+ clearTimeout(this.spinnerTimer);
135835
+ this.spinnerTimer = void 0;
135836
+ }
135837
+ scheduleSpinnerTick(delayMs) {
135838
+ if (this.ui === void 0) return;
135839
+ const timer = setTimeout(() => {
135840
+ if (this.spinnerTimer !== timer) return;
135841
+ const startedAt = performance.now();
135842
+ const elapsed = startedAt - this.lastSpinnerTickAt;
135843
+ if (elapsed >= 80) {
135844
+ const steps = Math.floor(elapsed / 80);
135845
+ this.spinnerFrame = (this.spinnerFrame + steps) % BRAILLE_SPINNER_FRAMES.length;
135846
+ this.lastSpinnerTickAt += steps * 80;
135847
+ this.invalidate();
135848
+ this.ui?.requestRender();
135849
+ }
135850
+ const frameCostMs = performance.now() - startedAt;
135851
+ if (this.spinnerTimer !== timer) return;
135852
+ const cadenceDelayMs = Math.max(0, 80 - frameCostMs);
135853
+ this.scheduleSpinnerTick(Math.max(cadenceDelayMs, frameCostMs * 9));
135854
+ }, delayMs);
135855
+ this.spinnerTimer = timer;
135856
+ }
135687
135857
  };
135688
135858
  //#endregion
135689
135859
  //#region src/tui/utils/activity-lines.ts
@@ -136116,7 +136286,7 @@ var ActivityGroupComponent = class extends Container {
136116
136286
  this.addChild(this.bodyContainer);
136117
136287
  this.startSpinner();
136118
136288
  }
136119
- /** True when the group owns neither reasoning nor tool calls. */
136289
+ /** True when the group owns neither reasoning nor tool calls nor approvals. */
136120
136290
  isEmpty() {
136121
136291
  return this.segments.every((segment) => segment.kind === "thinking" && segment.text.trim().length === 0);
136122
136292
  }
@@ -136125,14 +136295,16 @@ var ActivityGroupComponent = class extends Container {
136125
136295
  }
136126
136296
  /**
136127
136297
  * Borrows a tool card as a hidden state container. Re-attaching the same
136128
- * card is a no-op.
136298
+ * card is a no-op. `callId` is what later lets an approval be filed under the
136299
+ * call it allowed.
136129
136300
  */
136130
- attachTool(tc, step) {
136301
+ attachTool(tc, step, callId) {
136131
136302
  if (this.segments.some((segment) => segment.kind === "tool" && segment.tc === tc)) return;
136132
136303
  this.segments.push({
136133
136304
  kind: "tool",
136134
136305
  tc,
136135
- step
136306
+ step,
136307
+ callId
136136
136308
  });
136137
136309
  if (step !== void 0) this.steps.add(step);
136138
136310
  tc.setExpanded(this.expanded);
@@ -136143,14 +136315,7 @@ var ActivityGroupComponent = class extends Container {
136143
136315
  }
136144
136316
  /** Adds a background task notice as the newest step of the timeline. */
136145
136317
  attachNotice(data) {
136146
- const last = this.segments.at(-1);
136147
- if (last !== void 0 && last.kind === "thinking" && last.live) {
136148
- last.live = false;
136149
- this.interruptedThinking = {
136150
- text: last.text,
136151
- length: last.text.length
136152
- };
136153
- }
136318
+ this.splitLiveThinking();
136154
136319
  this.segments.push({
136155
136320
  kind: "notice",
136156
136321
  data
@@ -136158,6 +136323,38 @@ var ActivityGroupComponent = class extends Container {
136158
136323
  this.flushNow();
136159
136324
  }
136160
136325
  /**
136326
+ * Adds an approval outcome as the step directly under the call it allowed, so
136327
+ * a run of approved calls reads as call/approval pairs. Returns false when
136328
+ * that call has no row here — a subagent's call, a card that stays standalone
136329
+ * — leaving the caller to decide where the outcome goes.
136330
+ */
136331
+ attachApproval(callId, label, detail, tone) {
136332
+ const index = this.segments.findIndex((segment) => segment.kind === "tool" && segment.callId === callId);
136333
+ if (index < 0) return false;
136334
+ this.segments.splice(index + 1, 0, {
136335
+ kind: "approval",
136336
+ label,
136337
+ detail,
136338
+ tone
136339
+ });
136340
+ this.flushNow();
136341
+ return true;
136342
+ }
136343
+ /**
136344
+ * A step landing mid-reasoning splits the run in two: the part that already
136345
+ * streamed is closed and its length remembered, so the continuation keeps only
136346
+ * what is new instead of repeating the whole run.
136347
+ */
136348
+ splitLiveThinking() {
136349
+ const last = this.segments.at(-1);
136350
+ if (last === void 0 || last.kind !== "thinking" || !last.live) return;
136351
+ last.live = false;
136352
+ this.interruptedThinking = {
136353
+ text: last.text,
136354
+ length: last.text.length
136355
+ };
136356
+ }
136357
+ /**
136161
136358
  * Appends to the reasoning run streaming right now, opening a new step of the
136162
136359
  * timeline when the last step is a tool call or a reasoning run that already
136163
136360
  * ended. `live` drives the streaming rate in the header.
@@ -136340,6 +136537,10 @@ var ActivityGroupComponent = class extends Container {
136340
136537
  this.bodyContainer.addChild(this.noticeRow(segment.data, width, isLast));
136341
136538
  return;
136342
136539
  }
136540
+ if (segment.kind === "approval") {
136541
+ this.bodyContainer.addChild(this.approvalRow(segment, width, isLast));
136542
+ return;
136543
+ }
136343
136544
  this.bodyContainer.addChild(this.thinkingSummaryRow(segment, width, isLast));
136344
136545
  });
136345
136546
  }
@@ -136385,15 +136586,30 @@ var ActivityGroupComponent = class extends Container {
136385
136586
  components: [this.noticeRow(segment.data, width, isLast)],
136386
136587
  cost: 1
136387
136588
  };
136589
+ if (segment.kind === "approval") return {
136590
+ components: [this.approvalRow(segment, width, isLast)],
136591
+ cost: 1
136592
+ };
136388
136593
  return this.thinkingSegmentRows(segment, width, isLast);
136389
136594
  }
136390
136595
  /** One background task notice, kept to a single row. */
136391
136596
  noticeRow(data, width, isLast) {
136392
136597
  const prefix = isLast ? BRANCH_LAST : BRANCH_FIRST;
136393
- const view = renderBackgroundStatus(data, this.colors, Math.max(1, width - visibleWidth(prefix) - NOTICE_BULLET_WIDTH));
136598
+ const view = renderBackgroundStatus(data, this.colors, Math.max(1, width - visibleWidth(prefix) - NOTICE_BULLET_WIDTH), this.spinnerFrame);
136394
136599
  return new Text(`${prefix}${view.bullet}${view.text}`, 0, 0);
136395
136600
  }
136396
136601
  /**
136602
+ * One approval outcome, kept to a single row. Approvals read as a quiet note
136603
+ * next to the work they allowed — dim, unlike a rejection, which keeps the
136604
+ * error tone so a denied call is never mistaken for an allowed one.
136605
+ */
136606
+ approvalRow(segment, width, isLast) {
136607
+ const prefix = isLast ? BRANCH_LAST : BRANCH_FIRST;
136608
+ const tone = segment.tone === "rejected" ? this.colors.error : this.colors.textDim;
136609
+ const plain = `${segment.label}${SEPARATOR}${segment.detail}`;
136610
+ return new Text(`${prefix}${chalk.hex(tone)(truncateToWidth(plain, Math.max(1, width - visibleWidth(prefix)), "…"))}`, 0, 0);
136611
+ }
136612
+ /**
136397
136613
  * One reasoning run: a label row plus as many of its lines as its own budget
136398
136614
  * allows. Lines keep their order; the oldest end of a live run (or the newest
136399
136615
  * end of a finished one) is dropped until the budget fits.
@@ -136406,7 +136622,7 @@ var ActivityGroupComponent = class extends Container {
136406
136622
  cost: 0
136407
136623
  };
136408
136624
  const continuation = isLast ? THINKING_BODY_PREFIX : BRANCH_PIPE;
136409
- const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${chalk.hex(colors.roleThinking)(t("activitygroup.thinking_label"))}`, 0, 0)];
136625
+ const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.thinkingGlyph(segment)}${chalk.hex(colors.roleThinking)(t("activitygroup.thinking_label"))}`, 0, 0)];
136410
136626
  let cost = 1;
136411
136627
  const budget = getActivityLines().expandedThinking;
136412
136628
  const inner = Math.max(1, width - visibleWidth(continuation));
@@ -136467,6 +136683,16 @@ var ActivityGroupComponent = class extends Container {
136467
136683
  aborted: cardMarkerChar(rawHeader) === "⊙"
136468
136684
  };
136469
136685
  }
136686
+ /** Mirrors a tool row's status prefix so reasoning and tool rows line up: a
136687
+ * live run spins with the block frame, a finished run takes the success mark
136688
+ * (`DONE_MARK` carries its own trailing space, so the column stays 2 cells). */
136689
+ thinkingGlyph(segment) {
136690
+ if (segment.live) {
136691
+ const frame = BRAILLE_SPINNER_FRAMES[this.spinnerFrame] ?? BRAILLE_SPINNER_FRAMES[0];
136692
+ return `${chalk.hex(this.colors.roleThinking)(frame)} `;
136693
+ }
136694
+ return chalk.hex(this.colors.success)(DONE_MARK);
136695
+ }
136470
136696
  /** Mirrors the card's own status: running spins, failed/aborted stay visible. */
136471
136697
  statusGlyph(row) {
136472
136698
  if (row.aborted) return chalk.hex(this.colors.error)("⊙");
@@ -136485,10 +136711,27 @@ var ActivityGroupComponent = class extends Container {
136485
136711
  thinkingSummaryRow(segment, width, isLast) {
136486
136712
  const first = thinkingLines(segment.text)[0] ?? "";
136487
136713
  const label = t("activitygroup.thinking_summary", { summary: "" });
136488
- const used = visibleWidth(isLast ? BRANCH_LAST : BRANCH_FIRST) + visibleWidth(label);
136714
+ const prefix = isLast ? BRANCH_LAST : BRANCH_FIRST;
136715
+ const used = visibleWidth(prefix) + visibleWidth(this.thinkingGlyph(segment)) + visibleWidth(label);
136489
136716
  const cells = Math.max(1, width - used - 1);
136490
136717
  const summary = chalk.hex(this.colors.roleThinking)(t("activitygroup.thinking_summary", { summary: truncateToWidth(first, cells, "…") }));
136491
- return new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${summary}`, 0, 0);
136718
+ return new Text(`${prefix}${this.thinkingGlyph(segment)}${summary}`, 0, 0);
136719
+ }
136720
+ /**
136721
+ * True while at least one visible row carries a live animation. Reasoning rows
136722
+ * spin too now, and a background task that is still running keeps its glyph
136723
+ * moving, so this can no longer be "collapsed with a pending tool": the only
136724
+ * question is whether repainting the body on this tick would show anything new.
136725
+ * Finished rows stay static by design.
136726
+ */
136727
+ needsRowAnimation() {
136728
+ if (!this.running) return false;
136729
+ for (const segment of this.segments) {
136730
+ if (segment.kind === "thinking" && segment.live) return true;
136731
+ if (segment.kind === "notice" && segment.data.phase === "started") return true;
136732
+ if (segment.kind === "tool" && segment.tc.resultView === void 0) return true;
136733
+ }
136734
+ return false;
136492
136735
  }
136493
136736
  scheduleSpinnerTick(delayMs) {
136494
136737
  if (this.ui === void 0) return;
@@ -136501,7 +136744,7 @@ var ActivityGroupComponent = class extends Container {
136501
136744
  this.spinnerFrame = (this.spinnerFrame + steps) % BRAILLE_SPINNER_FRAMES.length;
136502
136745
  this.lastSpinnerTickAt += steps * 80;
136503
136746
  const width = this.renderedWidth ?? 80;
136504
- if (!this.expanded && this.segments.some((segment) => segment.kind === "tool" && segment.tc.resultView === void 0)) this.rebuild(width);
136747
+ if (this.needsRowAnimation()) this.rebuild(width);
136505
136748
  else this.headerText.setText(this.buildHeader(width));
136506
136749
  this.ui?.requestRender();
136507
136750
  }
@@ -139585,7 +139828,7 @@ var ToolCallComponent = class ToolCallComponent extends CachedContainer {
139585
139828
  else this.addChild(new Text(chalk.dim(plan), 2, 0));
139586
139829
  }
139587
139830
  computePlanBoxMaxContentLines() {
139588
- const rows = this.ui?.terminal.rows;
139831
+ const rows = this.ui?.terminal?.rows;
139589
139832
  if (rows === void 0 || !Number.isFinite(rows) || rows <= 0) return void 0;
139590
139833
  return Math.max(8, Math.floor(rows * .6) - 4);
139591
139834
  }
@@ -144461,18 +144704,18 @@ async function handleIngest(host) {
144461
144704
  async function ensureEmbeddingReadyInteractive(host) {
144462
144705
  const engine = (await getKnowledgeStore()).getEmbeddingEngine();
144463
144706
  if (engine !== void 0 && engine.available) return true;
144464
- if (!isEmbeddingModelCached()) {
144707
+ if (!hasEmbeddingModelCache()) {
144465
144708
  host.showError(t("knowledge.model_missing"));
144466
144709
  return false;
144467
144710
  }
144468
144711
  const spinner = host.showProgressSpinner(t("knowledge.loading_model"));
144469
- const { ok, error } = await startManualEmbeddingDownload();
144712
+ const { ok, error, failureKind } = await startManualEmbeddingDownload();
144470
144713
  spinner.stop({
144471
144714
  ok,
144472
144715
  label: ok ? t("kw.embedding_ready") : t("kw.embedding_failed")
144473
144716
  });
144474
144717
  if (!ok) {
144475
- const detail = [t("knowledge.download_model_retry_hint"), error].filter(Boolean).join("\n");
144718
+ const detail = embeddingFailureDetail(failureKind, error);
144476
144719
  host.showNotice(t("knowledge.model_load_failed"), detail);
144477
144720
  return false;
144478
144721
  }
@@ -144719,17 +144962,17 @@ async function handleReembed(host) {
144719
144962
  }
144720
144963
  }
144721
144964
  async function handleDownloadModel(host) {
144722
- await getKnowledgeStore();
144723
144965
  const spinner = host.showProgressSpinner(t("kw.embedding_downloading"));
144724
144966
  try {
144725
- const { ok, alreadyReady, error } = await startManualEmbeddingDownload();
144967
+ await getKnowledgeStore();
144968
+ const { ok, alreadyReady, error, failureKind } = await startManualEmbeddingDownload();
144726
144969
  spinner.stop({
144727
144970
  ok,
144728
144971
  label: ok ? t("kw.embedding_ready") : t("kw.embedding_failed")
144729
144972
  });
144730
144973
  if (ok) host.showStatus(alreadyReady ? t("kw.embedding_already_installed") : t("kw.embedding_ready"));
144731
144974
  else {
144732
- const detail = [t("knowledge.download_model_retry_hint"), error].filter(Boolean).join("\n");
144975
+ const detail = embeddingFailureDetail(failureKind, error);
144733
144976
  host.showNotice(t("kw.embedding_failed"), detail);
144734
144977
  }
144735
144978
  } catch (error) {
@@ -145497,4 +145740,4 @@ async function handleBuiltInSlashCommand(host, name, args) {
145497
145740
  }
145498
145741
  }
145499
145742
  //#endregion
145500
- export { handleExportMdCommand as $, readSessionIndex as $n, SubagentSlots as $t, langFromPath as A, startManualEmbeddingDownload as An, toggleEmptySessionHint as At, charsForTokenBudget as B, getLogDir as Bn, OSC11_QUERY as Bt, getDaemonInstructions as C, buildSkillSlashCommands as Cn, showStatusReport as Ct, renderDiffLines as D, setExperimentalFlags as Dn, createMarkdownTheme as Dt, ToolCallComponent as E, isExperimentalFlagEnabled as En, createEditorTheme as Et, replaceTabs as F, loadTuiConfig as Fn, parseOsc11BackgroundTheme as Ft, WelcomeComponent as G, PRODUCT_NAME as Gn, TERMINAL_FOCUS_IN as Gt, getSharedSpeedTracker as H, CLI_COMMAND_NAME as Hn, OSC11_RESPONSE_PREFIX as Ht, sanitizeShellOutput as I, saveTuiConfig as In, DISABLE_TERMINAL_FOCUS_REPORTING as It, resetBreathingClock as J, saveCatalogCache as Jn, TERMINAL_THEME_LIGHT as Jt, BREATHE_CYCLE_MS as K, DEFAULT_CATALOG_URL as Kn, TERMINAL_FOCUS_OUT as Kt, SkillActivationComponent as L, detectShellEnvironment as Ln, DISABLE_TERMINAL_THEME_REPORTING as Lt, ThinkingComponent as M, PULSE_WAVE_FRAMES as Mn, contrastTextHex as Mt, NoticeMessageComponent as N, TuiConfigParseError as Nn, getColorPalette as Nt, renderDiffLinesClustered as O, getKnowledgeStore as On, isEmptySessionHintDismissed as Ot, StatusMessageComponent as P, TuiLikePreferencesSchema as Pn, detectTerminalTheme as Pt, handleExportDebugZipCommand as Q, appendSessionIndexEntry as Qn, lerpGradient as Qt, AssistantMessageComponent as R, getDataDir as Rn, ENABLE_TERMINAL_FOCUS_REPORTING as Rt, handleCcCommand as S, getNoActiveSessionMessage as Sn, clearInfoPanelState as St, UserMessageComponent as T, sortSlashCommands as Tn, resolveThemeSync as Tt, BackgroundAgentStatusComponent as U, CLI_UI_MODE as Un, OSC11_RESPONSE_PREFIX_NO_ESC as Ut, estimateTokens as V, detectInstallSource as Vn, OSC11_RESPONSE as Vt, AgentGroupComponent as W, CLI_USER_AGENT_PRODUCT as Wn, QUERY_TERMINAL_THEME as Wt, clearGoalState as X, encodeWorkDirKey as Xn, isStreaming as Xt, CompactionComponent as Y, ScreamHarness as Yn, isBusy as Yt, refineGoal as Z, normalizeWorkDir as Zn, FooterComponent as Zt, selectUpdateTarget as _, SESSION_TIPS as _n, showModelPicker as _t, handleExtensionCommand as a, STATUS_BULLET as an, log as ar, changeThinkingLevel as at, appendJsonlLine as b, getCtrlDHint as bn, refreshProviderBalance as bt, hasDispose as c, formatErrorMessage as cn, isOrphanedToolCallError as cr, handleBotCommand as ct, MoonLoader as d, serializeToolResultOutput as dn, handleFusionPlanCommand as dt, handleTraceCommand as en, removeSessionIndexEntry as er, handleForkCommand as et, formatMemoryMemoForInjection as f, stringValue as fn, handleModelCommand as ft, handleUpdateCommand as g, MAIN_AGENT_ID$1 as gn, handleYoloCommand as gt, handleMcpCommand as h, EXIT_CONFIRM_WINDOW_MS as hn, handleWolfpackCommand as ht, buildRoleAdditionalText as i, printableChar as in, flushDiagnosticLogs as ir, toTerminalHyperlink as it, CachedContainer as j, PIXEL_PULSE_FRAMES as jn, createThemeStyles as jt, highlightLines as k, isEmbeddingModelCached as kn, isTurnElapsedEnabled as kt, isExpandable as l, isTodoItemShape as ln, ErrorCodes as lr, handleCompactCommand as lt, handleChannelCommand as m, EMPTY_SESSION_HINT_URL as mn, handleThemeCommand as mt, clearEvalPanelState as n, handleConnectCommand as nn, resolveScreamHome as nr, handleTitleCommand as nt, handleSkillCommand as o, appendStreamingArgsPreview as on, resolveGlobalLogPath as or, getModelCycleLevel as ot, handleMemoryCommand as p, truncateErrorMessage as pn, handlePlanCommand as pt, getBreathingFrame as q, fetchCatalog as qn, TERMINAL_THEME_DARK as qt, openUrl as r, handleLogoutCommand as rn, MemoryMemoStore as rr, isInternalMessage as rt, disposeChildren as s, argsRecord as sn, isScreamError as sr, handleAutoCommand as st, dispatchInput as t, handleSearchCommand as tn, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as tr, handleInitCommand as tt, isPlanExpandable as u, parseStreamingArgs as un, SCREAM_ERROR_INFO as ur, handleEditorCommand as ut, refreshUpdateCache as v, TIP_ROTATION_INTERVAL_MS as vn, showPermissionPicker as vt, handleRevokeCommand as w, BUILTIN_SLASH_COMMANDS as wn, showUsage as wt, readJsonlFile as x, getLlmNotSetMessage as xn, supportsBalance as xt, readUpdateCache as y, getCtrlCHint as yn, showSettingsSelector as yt, ActivityGroupComponent as z, getInputHistoryFile as zn, ENABLE_TERMINAL_THEME_REPORTING as zt };
145743
+ export { handleExportMdCommand as $, appendSessionIndexEntry as $n, SubagentSlots as $t, langFromPath as A, hasEmbeddingModelCache as An, toggleEmptySessionHint as At, charsForTokenBudget as B, getInputHistoryFile as Bn, OSC11_QUERY as Bt, getDaemonInstructions as C, buildSkillSlashCommands as Cn, showStatusReport as Ct, renderDiffLines as D, setExperimentalFlags as Dn, createMarkdownTheme as Dt, ToolCallComponent as E, isExperimentalFlagEnabled as En, createEditorTheme as Et, replaceTabs as F, TuiLikePreferencesSchema as Fn, parseOsc11BackgroundTheme as Ft, WelcomeComponent as G, CLI_USER_AGENT_PRODUCT as Gn, TERMINAL_FOCUS_IN as Gt, getSharedSpeedTracker as H, detectInstallSource as Hn, OSC11_RESPONSE_PREFIX as Ht, sanitizeShellOutput as I, loadTuiConfig as In, DISABLE_TERMINAL_FOCUS_REPORTING as It, resetBreathingClock as J, fetchCatalog as Jn, TERMINAL_THEME_LIGHT as Jt, BREATHE_CYCLE_MS as K, PRODUCT_NAME as Kn, TERMINAL_FOCUS_OUT as Kt, SkillActivationComponent as L, saveTuiConfig as Ln, DISABLE_TERMINAL_THEME_REPORTING as Lt, ThinkingComponent as M, PIXEL_PULSE_FRAMES as Mn, contrastTextHex as Mt, NoticeMessageComponent as N, PULSE_WAVE_FRAMES as Nn, getColorPalette as Nt, renderDiffLinesClustered as O, embeddingFailureDetail as On, isEmptySessionHintDismissed as Ot, StatusMessageComponent as P, TuiConfigParseError as Pn, detectTerminalTheme as Pt, handleExportDebugZipCommand as Q, normalizeWorkDir as Qn, lerpGradient as Qt, AssistantMessageComponent as R, detectShellEnvironment as Rn, ENABLE_TERMINAL_FOCUS_REPORTING as Rt, handleCcCommand as S, getNoActiveSessionMessage as Sn, clearInfoPanelState as St, UserMessageComponent as T, sortSlashCommands as Tn, resolveThemeSync as Tt, BackgroundAgentStatusComponent as U, CLI_COMMAND_NAME as Un, OSC11_RESPONSE_PREFIX_NO_ESC as Ut, estimateTokens as V, getLogDir as Vn, OSC11_RESPONSE as Vt, AgentGroupComponent as W, CLI_UI_MODE as Wn, QUERY_TERMINAL_THEME as Wt, clearGoalState as X, ScreamHarness as Xn, isStreaming as Xt, CompactionComponent as Y, saveCatalogCache as Yn, isBusy as Yt, refineGoal as Z, encodeWorkDirKey as Zn, FooterComponent as Zt, selectUpdateTarget as _, SESSION_TIPS as _n, showModelPicker as _t, handleExtensionCommand as a, STATUS_BULLET as an, flushDiagnosticLogs as ar, changeThinkingLevel as at, appendJsonlLine as b, getCtrlDHint as bn, refreshProviderBalance as bt, hasDispose as c, formatErrorMessage as cn, isScreamError as cr, handleBotCommand as ct, MoonLoader as d, serializeToolResultOutput as dn, SCREAM_ERROR_INFO as dr, handleFusionPlanCommand as dt, handleTraceCommand as en, readSessionIndex as er, handleForkCommand as et, formatMemoryMemoForInjection as f, stringValue as fn, handleModelCommand as ft, handleUpdateCommand as g, MAIN_AGENT_ID$1 as gn, handleYoloCommand as gt, handleMcpCommand as h, EXIT_CONFIRM_WINDOW_MS as hn, handleWolfpackCommand as ht, buildRoleAdditionalText as i, printableChar as in, MemoryMemoStore as ir, toTerminalHyperlink as it, CachedContainer as j, startManualEmbeddingDownload as jn, createThemeStyles as jt, highlightLines as k, getKnowledgeStore as kn, isTurnElapsedEnabled as kt, isExpandable as l, isTodoItemShape as ln, isOrphanedToolCallError as lr, handleCompactCommand as lt, handleChannelCommand as m, EMPTY_SESSION_HINT_URL as mn, handleThemeCommand as mt, clearEvalPanelState as n, handleConnectCommand as nn, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as nr, handleTitleCommand as nt, handleSkillCommand as o, appendStreamingArgsPreview as on, log as or, getModelCycleLevel as ot, handleMemoryCommand as p, truncateErrorMessage as pn, handlePlanCommand as pt, getBreathingFrame as q, DEFAULT_CATALOG_URL as qn, TERMINAL_THEME_DARK as qt, openUrl as r, handleLogoutCommand as rn, resolveScreamHome as rr, isInternalMessage as rt, disposeChildren as s, argsRecord as sn, resolveGlobalLogPath as sr, handleAutoCommand as st, dispatchInput as t, handleSearchCommand as tn, removeSessionIndexEntry as tr, handleInitCommand as tt, isPlanExpandable as u, parseStreamingArgs as un, ErrorCodes as ur, handleEditorCommand as ut, refreshUpdateCache as v, TIP_ROTATION_INTERVAL_MS as vn, showPermissionPicker as vt, handleRevokeCommand as w, BUILTIN_SLASH_COMMANDS as wn, showUsage as wt, readJsonlFile as x, getLlmNotSetMessage as xn, supportsBalance as xt, readUpdateCache as y, getCtrlCHint as yn, showSettingsSelector as yt, ActivityGroupComponent as z, getDataDir as zn, ENABLE_TERMINAL_THEME_REPORTING as zt };
package/dist/main.mjs CHANGED
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
7
  //#region src/main.ts
8
8
  try {
9
- (await import("./app-F12Iraus.mjs")).main();
9
+ (await import("./app-D9oJ-Y8W.mjs")).main();
10
10
  } catch (error) {
11
11
  process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
12
12
  process.exit(1);
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
3
  import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
- import { t as TextInputDialogComponent } from "./text-input-dialog-KaFgI_Ko.mjs";
6
+ import { t as TextInputDialogComponent } from "./text-input-dialog-DBlx8pMR.mjs";
7
7
  export { TextInputDialogComponent };
@@ -126,6 +126,10 @@ const dictionaries = {
126
126
  "status.composing": "输出中",
127
127
  "status.tool": "执行中",
128
128
  "status.waiting": "等待中",
129
+ "status.compacting": "压缩中",
130
+ "status.awaiting_approval": "等待批准",
131
+ "status.awaiting_answer": "等待回答",
132
+ "status.reconnecting_short": "重连中 {attempt}/{max}",
129
133
  "status.reconnecting_detail": "重连中 {attempt}/{max} · {reason} · {delay}s 后重试",
130
134
  "status.reason_rate_limit": "限流",
131
135
  "status.reason_server": "服务器错误",
@@ -305,7 +309,7 @@ const dictionaries = {
305
309
  "knowledge.search_fail": "搜索失败",
306
310
  "knowledge.search_result": "搜索结果",
307
311
  "knowledge.no_hits": "查询 \"{query}\" 未命中任何 chunk",
308
- "knowledge.vector_degraded": "向量模型未就绪,当前为关键词检索模式。如下载失败,建议开启科学上网后重启。",
312
+ "knowledge.vector_degraded": "向量模型未就绪,当前为关键词检索模式。可在 /knowledge 菜单重新下载模型。",
309
313
  "knowledge.query_label": "查询",
310
314
  "knowledge.no_title": "(无标题)",
311
315
  "knowledge.source_label": "来源",
@@ -340,7 +344,10 @@ const dictionaries = {
340
344
  "knowledge.download_model": "下载向量模型",
341
345
  "knowledge.download_model_desc": "手动下载 bge-small-zh-v1.5 模型(约 95 MB)以启用语义搜索",
342
346
  "knowledge.download_model_installed": "(已安装)",
343
- "knowledge.download_model_retry_hint": "可返回菜单重新选择「下载向量模型」重试,建议科学上网",
347
+ "knowledge.embedding_platform_hint": "当前平台缺少本地向量模型二进制;知识库仍可用关键词检索",
348
+ "knowledge.embedding_cache_corrupt_hint": "本地模型缓存已损坏;清理并重试仍未成功,请稍后再试",
349
+ "knowledge.embedding_network_hint": "可返回菜单重选「下载向量模型」重试;网络受限时可用代理",
350
+ "knowledge.embedding_unknown_hint": "请再试一次;若持续失败,请把下面这行错误信息反馈给我们",
344
351
  "knowledge.menu_title": "SAG知识库管理",
345
352
  "knowledge.menu_hint": "选择操作(esc 退出)",
346
353
  "knowledge.op_failed": "操作失败: {msg}",
@@ -1299,7 +1306,7 @@ const dictionaries = {
1299
1306
  "badge.yes": "YES",
1300
1307
  "kw.lang_toggle": "English",
1301
1308
  "kw.embedding_downloading": "向量模型下载中…",
1302
- "kw.embedding_failed": "向量模型加载失败,请返回菜单选择重新下载(建议科学上网)",
1309
+ "kw.embedding_failed": "向量模型加载失败",
1303
1310
  "kw.embedding_ready": "向量模型已就绪",
1304
1311
  "kw.embedding_not_downloaded": "向量模型未下载(选择「下载向量模型」手动下载)",
1305
1312
  "kw.embedding_data_intact": " —— 你的知识数据仍然存在,下载模型后即可正常使用",
@@ -1336,6 +1343,10 @@ const dictionaries = {
1336
1343
  "status.composing": "Writing",
1337
1344
  "status.tool": "Running",
1338
1345
  "status.waiting": "Waiting",
1346
+ "status.compacting": "Compacting",
1347
+ "status.awaiting_approval": "Awaiting approval",
1348
+ "status.awaiting_answer": "Awaiting answer",
1349
+ "status.reconnecting_short": "Retrying {attempt}/{max}",
1339
1350
  "status.reconnecting_detail": "Reconnecting {attempt}/{max} · {reason} · retry in {delay}s",
1340
1351
  "status.reason_rate_limit": "rate limited",
1341
1352
  "status.reason_server": "server error",
@@ -1515,7 +1526,7 @@ const dictionaries = {
1515
1526
  "knowledge.search_fail": "Search failed",
1516
1527
  "knowledge.search_result": "Search Results",
1517
1528
  "knowledge.no_hits": "Query \"{query}\" matched no chunks",
1518
- "knowledge.vector_degraded": "Vector model not ready, using keyword search mode. If download failed, try enabling a proxy and restart.",
1529
+ "knowledge.vector_degraded": "Vector model not ready, using keyword search mode. You can retry the download from the /knowledge menu.",
1519
1530
  "knowledge.query_label": "Query",
1520
1531
  "knowledge.no_title": "(untitled)",
1521
1532
  "knowledge.source_label": "Source",
@@ -1550,7 +1561,10 @@ const dictionaries = {
1550
1561
  "knowledge.download_model": "Download vector model",
1551
1562
  "knowledge.download_model_desc": "Manually download the bge-small-zh-v1.5 model (~95 MB) to enable semantic search",
1552
1563
  "knowledge.download_model_installed": " (installed)",
1553
- "knowledge.download_model_retry_hint": "Return to the menu and select Download vector model to retry. A proxy is recommended.",
1564
+ "knowledge.embedding_platform_hint": "No local model binary on this platform; keyword search still works",
1565
+ "knowledge.embedding_cache_corrupt_hint": "Corrupt model cache; cleared and retried, still failing — try again later",
1566
+ "knowledge.embedding_network_hint": "Return to the menu and retry the download; a proxy helps when the network is restricted.",
1567
+ "knowledge.embedding_unknown_hint": "Please retry; if it keeps failing, send us the error line below.",
1554
1568
  "knowledge.menu_title": "SAG Knowledge Base",
1555
1569
  "knowledge.menu_hint": "Select an action (Esc to exit)",
1556
1570
  "knowledge.op_failed": "Operation failed: {msg}",
@@ -2509,7 +2523,7 @@ const dictionaries = {
2509
2523
  "badge.yes": "YES",
2510
2524
  "kw.lang_toggle": "中文",
2511
2525
  "kw.embedding_downloading": "Vector model downloading…",
2512
- "kw.embedding_failed": "Vector model load failed. Return to the menu and select Download to retry.",
2526
+ "kw.embedding_failed": "Vector model load failed",
2513
2527
  "kw.embedding_ready": "Vector model ready",
2514
2528
  "kw.embedding_not_downloaded": "Vector model not downloaded (select Download vector model)",
2515
2529
  "kw.embedding_data_intact": " — your knowledge data is still there; download the model to use it again",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scream-code",
3
- "version": "0.16.5",
3
+ "version": "0.16.6",
4
4
  "description": "A terminal-native AI agent for builders",
5
5
  "license": "MIT",
6
6
  "author": "ScreamCli",