scream-code 0.16.5 → 0.16.7

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 normalizeWorkDir, $t as lerpGradient, A as highlightLines, An as getKnowledgeStore, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as getDataDir, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getNoActiveSessionMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as isExperimentalFlagEnabled, Dt as createEditorTheme, E as UserMessageComponent, En as sortSlashCommands, Et as resolveThemeSync, F as StatusMessageComponent, Fn as TuiConfigParseError, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_UI_MODE, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getLogDir, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiLikePreferencesSchema, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as DEFAULT_CATALOG_URL, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_USER_AGENT_PRODUCT, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as loadTuiConfig, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as startManualEmbeddingDownload, Mt as createThemeStyles, N as ThinkingComponent, Nn as PIXEL_PULSE_FRAMES, Nt as contrastTextHex, O as renderDiffLines, On as setExperimentalFlags, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PULSE_WAVE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as encodeWorkDirKey, Qt as FooterComponent, R as SkillActivationComponent, Rn as saveTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getLlmNotSetMessage, St as supportsBalance, Tn as BUILTIN_SLASH_COMMANDS, U as getSharedSpeedTracker, Un as detectInstallSource, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getInputHistoryFile, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as CLI_COMMAND_NAME, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as saveCatalogCache, Xt as isBusy, Y as resetBreathingClock, Yn as fetchCatalog, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as ScreamHarness, Zt as isStreaming, _n as MAIN_AGENT_ID, a as syncDiagramCapabilityPrompt, an as printableChar, ar as MemoryMemoStore, at as toTerminalHyperlink, b as readUpdateCache, bn as getCtrlCHint, c as disposeChildren, cn as argsRecord, cr as resolveGlobalLogPath, d as isPlanExpandable, dn as parseStreamingArgs, dr as ErrorCodes, en as SubagentSlots, er as appendSessionIndexEntry, f as MoonLoader, fn as serializeToolResultOutput, fr as SCREAM_ERROR_INFO, ft as handleFusionPlanCommand, gn as EXIT_CONFIRM_WINDOW_MS, hn as EMPTY_SESSION_HINT_URL, i as buildRoleAdditionalText, ir as resolveScreamHome, it as isInternalMessage, j as langFromPath, jn as hasEmbeddingModelCache, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as embeddingFailureDetail, kt as isEmptySessionHintDismissed, l as hasDispose, ln as formatErrorMessage, lr as isScreamError, mn as truncateErrorMessage, mt as handlePlanCommand, n as clearEvalPanelState, nr as removeSessionIndexEntry, on as STATUS_BULLET, or as flushDiagnosticLogs, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as stringValue, q as BREATHE_CYCLE_MS, qn as PRODUCT_NAME, qt as TERMINAL_FOCUS_OUT, r as openUrl, sn as appendStreamingArgsPreview, sr as log, st as getModelCycleLevel, t as dispatchInput, tr as readSessionIndex, u as isExpandable, un as isTodoItemShape, ur as isOrphanedToolCallError, v as selectUpdateTarget, vn as SESSION_TIPS, w as getDaemonInstructions, wn as buildSkillSlashCommands, x as appendJsonlLine, xn as getCtrlDHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as TIP_ROTATION_INTERVAL_MS, z as AssistantMessageComponent, zn as detectShellEnvironment, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-Vw9ccFm7.mjs";
8
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-D9ByqSsK.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.7"),
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
@@ -2361,15 +2363,15 @@ function formatStepDebugTiming(input) {
2361
2363
  const latency = input.llmFirstTokenLatencyMs;
2362
2364
  const streamMs = input.llmStreamDurationMs;
2363
2365
  if (latency === void 0 || streamMs === void 0) return void 0;
2364
- const parts = [`TTFT: ${formatDuration$1(latency)}`];
2366
+ const parts = [`TTFT: ${formatDuration(latency)}`];
2365
2367
  const outputTokens = input.usage?.output;
2366
2368
  if (outputTokens !== void 0 && outputTokens > 0 && streamMs > 0) {
2367
2369
  const tps = (outputTokens / (streamMs / 1e3)).toFixed(1);
2368
- parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration$1(streamMs)})`);
2370
+ parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration(streamMs)})`);
2369
2371
  }
2370
2372
  return `[Debug] ${parts.join(" | ")}`;
2371
2373
  }
2372
- function formatDuration$1(ms) {
2374
+ function formatDuration(ms) {
2373
2375
  if (ms < 1e3) return `${Math.round(ms)}ms`;
2374
2376
  return `${(ms / 1e3).toFixed(1)}s`;
2375
2377
  }
@@ -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;
@@ -4787,6 +4907,7 @@ var StreamingUIController = class StreamingUIController {
4787
4907
  this._assistantDraft = "";
4788
4908
  this._shownAssistantLength = 0;
4789
4909
  this.clearFlushTimerIfIdle();
4910
+ this._streamingBlock?.component.setStreaming(false);
4790
4911
  this._streamingBlock = null;
4791
4912
  this._thinkingDraft = "";
4792
4913
  this.lastDeltaAt = void 0;
@@ -4799,6 +4920,7 @@ var StreamingUIController = class StreamingUIController {
4799
4920
  this._streamingToolCallArguments.clear();
4800
4921
  this.disposeAndClearPendingToolComponents();
4801
4922
  this._pendingAgentGroup = null;
4923
+ this.flushPendingApprovals();
4802
4924
  }
4803
4925
  resetToolCallState() {
4804
4926
  this._activeToolCalls.clear();
@@ -4889,6 +5011,7 @@ var StreamingUIController = class StreamingUIController {
4889
5011
  const block = this._streamingBlock;
4890
5012
  if (block !== null) {
4891
5013
  block.entry.content = fullText;
5014
+ block.component.setStreaming(true);
4892
5015
  block.component.updateContent(fullText);
4893
5016
  this.host.state.ui.requestRender();
4894
5017
  }
@@ -4898,6 +5021,7 @@ var StreamingUIController = class StreamingUIController {
4898
5021
  const block = this._streamingBlock;
4899
5022
  this._streamingBlock = null;
4900
5023
  if (block !== null) {
5024
+ block.component.setStreaming(false);
4901
5025
  this.host.transcriptController.unmarkPending(block.component);
4902
5026
  this.host.transcriptController.commit();
4903
5027
  }
@@ -4962,6 +5086,7 @@ var StreamingUIController = class StreamingUIController {
4962
5086
  if (state.planExpanded) tc.setPlanExpanded(true);
4963
5087
  if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
4964
5088
  if (!this.tryAttachAgentToolCall(toolCall, tc)) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
5089
+ if (toolCall.name === "ExitPlanMode") this.endActivityGroup();
4965
5090
  tc.setExpanded(state.toolOutputExpanded);
4966
5091
  state.transcriptContainer.addChild(tc);
4967
5092
  state.ui.requestRender();
@@ -4969,7 +5094,8 @@ var StreamingUIController = class StreamingUIController {
4969
5094
  const group = this.ensureActivityGroup();
4970
5095
  this.registerActivityGroupEntry(group, entry);
4971
5096
  this._groupByToolCard.set(tc, group);
4972
- group.attachTool(tc, toolCall.step ?? this._currentStep);
5097
+ group.attachTool(tc, toolCall.step ?? this._currentStep, toolCall.id);
5098
+ this.adoptPendingApproval(group, toolCall.id);
4973
5099
  state.ui.requestRender();
4974
5100
  }
4975
5101
  if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
@@ -6281,6 +6407,9 @@ var TranscriptController = class TranscriptController {
6281
6407
  welcomeComponent;
6282
6408
  committedComponent;
6283
6409
  liveComponentToEntry = /* @__PURE__ */ new Map();
6410
+ /** Live `started` notice cards by task id, so the terminal notice can stop the
6411
+ * ticker of the card that is still claiming the task is running. */
6412
+ liveNoticesByTrackingId = /* @__PURE__ */ new Map();
6284
6413
  pendingComponents = /* @__PURE__ */ new Set();
6285
6414
  /** Max live transcript children before the oldest are folded into the
6286
6415
  * committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
@@ -6306,6 +6435,26 @@ var TranscriptController = class TranscriptController {
6306
6435
  releaseLiveComponent(component) {
6307
6436
  this.liveComponentToEntry.delete(component);
6308
6437
  }
6438
+ /**
6439
+ * Stop everything a component owns once it leaves the live area. Committed
6440
+ * views re-render from the entry data, so disposing cannot lose content, but
6441
+ * skipping it would let an animation timer outlive the row it belonged to.
6442
+ */
6443
+ dropLiveComponent(component) {
6444
+ for (const [trackingId, tracked] of this.liveNoticesByTrackingId) if (tracked === component) this.liveNoticesByTrackingId.delete(trackingId);
6445
+ if (hasDispose(component)) component.dispose();
6446
+ }
6447
+ /**
6448
+ * Stops the ticker of the card that announced `trackingId`. Called when the
6449
+ * task reaches a terminal state: that card is history now, and a glyph that
6450
+ * keeps rotating would claim the task is still running.
6451
+ */
6452
+ settleLiveNotice(trackingId) {
6453
+ const tracked = this.liveNoticesByTrackingId.get(trackingId);
6454
+ if (tracked === void 0) return;
6455
+ this.liveNoticesByTrackingId.delete(trackingId);
6456
+ tracked.settle();
6457
+ }
6309
6458
  markPending(component) {
6310
6459
  this.pendingComponents.add(component);
6311
6460
  }
@@ -6347,6 +6496,7 @@ var TranscriptController = class TranscriptController {
6347
6496
  this.committedComponent.appendEntry(entry, state.theme.colors);
6348
6497
  container.removeChild(component);
6349
6498
  this.liveComponentToEntry.delete(component);
6499
+ this.dropLiveComponent(component);
6350
6500
  }
6351
6501
  this.committedComponent.setCount(this.committedComponent.getCount() + toCommit.length);
6352
6502
  if (process.env["SCREAM_CODE_DEBUG"] === "1") this.host.showStatus(`[debug] committed=${this.committedComponent.getCount()} live=${this.getLiveCount()}`);
@@ -6381,10 +6531,10 @@ var TranscriptController = class TranscriptController {
6381
6531
  if (state.planExpanded) tc.setPlanExpanded(true);
6382
6532
  return tc;
6383
6533
  }
6384
- 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);
6385
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);
6386
6536
  case "status":
6387
- if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors);
6537
+ if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors, state.ui);
6388
6538
  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
6539
  case "cron":
6390
6540
  if (entry.cronData === void 0) return null;
@@ -6399,8 +6549,15 @@ var TranscriptController = class TranscriptController {
6399
6549
  if (component) {
6400
6550
  this.liveComponentToEntry.set(component, entry);
6401
6551
  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);
6552
+ if (status !== void 0 && component instanceof BackgroundAgentStatusComponent) {
6553
+ const trackingId = status.trackingId;
6554
+ if (trackingId !== void 0) if (status.phase === "started") this.liveNoticesByTrackingId.set(trackingId, component);
6555
+ else this.settleLiveNotice(trackingId);
6556
+ }
6557
+ if (status !== void 0 && this.host.streamingUI.attachNotice(status)) {
6558
+ this.releaseLiveComponent(component);
6559
+ this.dropLiveComponent(component);
6560
+ } else this.host.state.transcriptContainer.addChild(component);
6404
6561
  this.host.state.ui.requestRender();
6405
6562
  }
6406
6563
  return component ?? null;
@@ -6426,26 +6583,19 @@ var TranscriptController = class TranscriptController {
6426
6583
  }
6427
6584
  appendApprovalEntry(request, response) {
6428
6585
  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("")
6586
+ const notice = buildApprovalNotice({
6587
+ decision: response.decision,
6588
+ scope: response.scope,
6589
+ action: request.action,
6590
+ feedback: response.feedback,
6591
+ labels: {
6592
+ approved: t("tc.approved"),
6593
+ approvedSession: t("tc.approved_session"),
6594
+ rejected: t("tc.rejected"),
6595
+ cancelled: t("tc.cancelled")
6596
+ }
6448
6597
  });
6598
+ this.host.streamingUI.recordApproval(request.toolCallId, notice.label, notice.detail, notice.tone);
6449
6599
  }
6450
6600
  renderWelcome() {
6451
6601
  const { state } = this.host;
@@ -6464,6 +6614,7 @@ var TranscriptController = class TranscriptController {
6464
6614
  const { state, streamingUI, imageStore } = this.host;
6465
6615
  streamingUI.discardPending();
6466
6616
  streamingUI.endActivityGroup();
6617
+ streamingUI.flushPendingApprovals();
6467
6618
  state.transcriptEntries = [];
6468
6619
  streamingUI.disposeActiveCompactionBlock();
6469
6620
  streamingUI.resetLiveText();
@@ -6731,20 +6882,6 @@ const gitPanel = {
6731
6882
  };
6732
6883
  //#endregion
6733
6884
  //#region src/tui/components/sidebar/panels/session-panel.ts
6734
- function formatDuration(ms) {
6735
- const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
6736
- if (getLocale() === "zh") {
6737
- if (totalSeconds < 60) return `${totalSeconds}秒`;
6738
- const hours = Math.floor(totalSeconds / 3600);
6739
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6740
- return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`;
6741
- }
6742
- if (totalSeconds < 60) return `${totalSeconds}s`;
6743
- const hours = Math.floor(totalSeconds / 3600);
6744
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6745
- if (hours > 0) return `${hours}h ${minutes}m`;
6746
- return `${minutes}m`;
6747
- }
6748
6885
  /** 1,234 → "1,234" via the platform locale grouping. */
6749
6886
  function formatCount$1(n) {
6750
6887
  return n.toLocaleString("en-US");
@@ -6770,8 +6907,8 @@ function gridFits(rows, width) {
6770
6907
  /**
6771
6908
  * Session summary, live-rendered each frame as a strict two-column table:
6772
6909
  * one field per row, labels sharing a left column, values sharing a right
6773
- * edge. Uptime derives from `startedAt` so it ticks between data updates;
6774
- * every other value comes from the snapshot in getData().
6910
+ * edge. Every value comes from the snapshot in getData(); nothing here is
6911
+ * derived from wall-clock time.
6775
6912
  */
6776
6913
  var SessionPanelContent = class {
6777
6914
  ctx;
@@ -6786,7 +6923,6 @@ var SessionPanelContent = class {
6786
6923
  if (stats === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.session_empty"))];
6787
6924
  const label = (s) => chalk.hex(this.colors.textDim)(s);
6788
6925
  const value = (s) => chalk.hex(this.colors.text)(s);
6789
- const now = Date.now();
6790
6926
  const tokenRows = [
6791
6927
  [t("sidebar.tokens"), stats.tokensTotal],
6792
6928
  [t("sidebar.tokens_cache_hit"), stats.tokensInputCacheHit],
@@ -6794,7 +6930,6 @@ var SessionPanelContent = class {
6794
6930
  [t("sidebar.tokens_output"), stats.tokensOutput]
6795
6931
  ];
6796
6932
  const countRows = [
6797
- [t("sidebar.uptime"), formatDuration(now - stats.startedAt)],
6798
6933
  [t("sidebar.turns"), formatCount$1(stats.turns)],
6799
6934
  [t("sidebar.tools"), formatCount$1(stats.toolCalls)],
6800
6935
  [t("sidebar.calls"), formatCount$1(stats.apiCalls)],
@@ -6841,8 +6976,8 @@ function statusText(status) {
6841
6976
  }
6842
6977
  }
6843
6978
  /**
6844
- * Fixed 8-slot subagent panel: one row per default subagent type (plus any
6845
- * custom types appended in spawn order). Live-rendered each frame from the
6979
+ * Fixed default-slot subagent panel: one row per default subagent type (plus
6980
+ * any custom types appended in spawn order). Live-rendered each frame from the
6846
6981
  * current snapshot; idle slots stay greyed out.
6847
6982
  */
6848
6983
  var AgentsPanelContent = class {
@@ -6865,7 +7000,7 @@ var AgentsPanelContent = class {
6865
7000
  renderSlot(cell, slot, index, width) {
6866
7001
  const dim = (s) => chalk.hex(this.colors.textDim)(s);
6867
7002
  const idle = slot.status === "idle";
6868
- const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .125) % 1;
7003
+ const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .6180339887498949) % 1;
6869
7004
  const active = slot.status === "working" || slot.status === "outputting" ? chalk.hex(lerpGradient(phase)).bold : void 0;
6870
7005
  const marker = padLabel((active ?? chalk.hex(this.statusColor(slot.status)))(idle ? "○" : "●"), 2);
6871
7006
  const label = (idle ? dim : chalk.hex(this.colors.textStrong))(cell.label);
@@ -7125,23 +7260,38 @@ function toneFor(site, stale) {
7125
7260
  /**
7126
7261
  * Merge the measured provider reading with the probe samples into Hub rows.
7127
7262
  * The provider row always leads (it answers "is the model I'm using reachable"),
7128
- * and it staleness-checks against its own budget: a long idle stretch means
7129
- * nothing was asked, not that the network died.
7263
+ * it staleness-checks against its own budget: a long idle stretch means nothing
7264
+ * was asked, not that the network died — and it is coloured on its own ladder
7265
+ * (see {@link hubModelTone}), not on the probe bar.
7130
7266
  */
7131
7267
  function buildHubSamples(snapshot, provider, now = Date.now()) {
7132
7268
  const ageMs = provider.sampledAt === void 0 ? 0 : now - provider.sampledAt;
7133
7269
  return [{
7134
7270
  id: HUB_MODEL_ROW_ID,
7135
7271
  ms: provider.ms,
7136
- tone: hubLatencyTone(provider.ms, ageMs >= HUB_MODEL_STALE_MS)
7272
+ tone: hubModelTone(provider.ms, ageMs >= HUB_MODEL_STALE_MS)
7137
7273
  }, ...snapshot.samples];
7138
7274
  }
7139
- /** Shared tone rule for every Hub row (probe sites and the measured provider). */
7275
+ /** Tone rule for the probed endpoint rows: green ≤{@link HUB_GOOD_MS}, amber above. */
7140
7276
  function hubLatencyTone(ms, stale = false) {
7141
7277
  if (ms === void 0 || stale) return "dim";
7142
7278
  return ms <= 100 ? "ok" : "warn";
7143
7279
  }
7144
7280
  /**
7281
+ * Tone rule for the measured provider row (model first token), deliberately
7282
+ * separate from the probe ladder: a first token and a HEAD request are not
7283
+ * comparable, and one shared bar would call every healthy answer slow. Green
7284
+ * ≤{@link HUB_MODEL_GOOD_MS}, amber up to {@link HUB_MODEL_SLOW_MS}, red past
7285
+ * it — that long is worth flagging, not merely dimming. `undefined` (nothing
7286
+ * measured yet, or a provider just switched) and a stale reading stay dim:
7287
+ * neither is an outage.
7288
+ */
7289
+ function hubModelTone(ms, stale = false) {
7290
+ if (ms === void 0 || stale) return "dim";
7291
+ if (ms <= 5e3) return "ok";
7292
+ return ms <= 15e3 ? "warn" : "down";
7293
+ }
7294
+ /**
7145
7295
  * Compact latency reading: whole milliseconds below a second, one-decimal
7146
7296
  * seconds above — a probe that never answers must never read as `65400ms`.
7147
7297
  */
@@ -7162,9 +7312,11 @@ const EMPTY_MARK = "--";
7162
7312
  *
7163
7313
  * Values share one right edge through the same two-column engine the Session
7164
7314
  * panel uses, so rows never jitter as numbers change width, and only the value
7165
- * carries colour (green ≤100ms, amber above it, red when a site stopped
7166
- * answering). The panel is a pure view: probing happens in the data provider,
7167
- * which is what keeps a slow endpoint from ever touching a frame.
7315
+ * carries colour. Two ladders, because two different things are timed: probe
7316
+ * rows are green ≤100ms, amber above it, red once a site stopped answering,
7317
+ * while the measured provider row (model first token) is green ≤5s, amber up to
7318
+ * 15s and red past that. The panel is a pure view: probing happens in the data
7319
+ * provider, which is what keeps a slow endpoint from ever touching a frame.
7168
7320
  */
7169
7321
  var HubPanelContent = class {
7170
7322
  ctx;
@@ -7776,10 +7928,6 @@ var LifecycleController = class LifecycleController {
7776
7928
  * frame asked, so a closed sidebar produces no traffic. */
7777
7929
  hubProbe = createHubProbe();
7778
7930
  sidebarGitCacheWorkDir = "";
7779
- /** Session start time for the sidebar uptime counter. Reset on session
7780
- * switch so a resumed old session doesn't show the TUI process age. */
7781
- sidebarSessionStartedAt = Date.now();
7782
- sidebarSessionId;
7783
7931
  /** Memo for the session-stats aggregation: recomputed only when the
7784
7932
  * transcript length, compaction count or usage object changes. */
7785
7933
  sidebarStatsMemo;
@@ -8047,10 +8195,6 @@ var LifecycleController = class LifecycleController {
8047
8195
  };
8048
8196
  }
8049
8197
  readSidebarSessionStats(appState) {
8050
- if (this.sidebarSessionId !== appState.sessionId) {
8051
- this.sidebarSessionId = appState.sessionId;
8052
- this.sidebarSessionStartedAt = Date.now();
8053
- }
8054
8198
  const entries = this.host.state.transcriptEntries;
8055
8199
  const usage = appState.sessionUsage;
8056
8200
  const subagentUsage = appState.subagentUsage;
@@ -8072,8 +8216,7 @@ var LifecycleController = class LifecycleController {
8072
8216
  tokensTotal: LifecycleController.sumTokens(usage) + subagentOutput,
8073
8217
  tokensInputCacheHit: usage.inputCacheRead,
8074
8218
  tokensInputCacheMiss: usage.inputOther + usage.inputCacheCreation,
8075
- tokensOutput: usage.output + subagentOutput,
8076
- startedAt: this.sidebarSessionStartedAt
8219
+ tokensOutput: usage.output + subagentOutput
8077
8220
  };
8078
8221
  this.sidebarStatsMemo = {
8079
8222
  length: entries.length,
@@ -8367,7 +8510,7 @@ var FileMentionProvider = class {
8367
8510
  const atPrefix = extractAtPrefix(textBeforeCursor);
8368
8511
  if (!options.force && atPrefix === null && textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
8369
8512
  const query = textBeforeCursor.slice(1);
8370
- const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => item.value).slice(0, MAX_SUGGESTIONS_WHEN_QUERY);
8513
+ const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => !query.startsWith("skill:") && item.value.startsWith("skill:") ? item.value.slice(6) : item.value);
8371
8514
  if (filtered.length === 0) return null;
8372
8515
  return {
8373
8516
  items: filtered,
@@ -9319,7 +9462,7 @@ const SIDE_PADDING = 1;
9319
9462
  * Renders the stacked sidebar: every visible registered panel gets its own
9320
9463
  * full box frame (┌─┐│└┘) with the panel title embedded in the top frame, one
9321
9464
  * below the other. Panels render live each frame (their body reads the
9322
- * current data snapshot), so time-based values (uptime, goal wall-clock)
9465
+ * current data snapshot), so time-based values (goal wall-clock)
9323
9466
  * tick without rebuilding. The focused panel's title is bolded; focus is
9324
9467
  * moved by the SidebarManager (`/sidebar next/prev`, Ctrl+X toggles).
9325
9468
  * The whole frame is painted in the theme accent colour and the rendered
@@ -9333,9 +9476,10 @@ var SidebarContainer = class extends Container {
9333
9476
  * only rebuilt when the visible panel set changes. */
9334
9477
  bodies = /* @__PURE__ */ new Map();
9335
9478
  builtStackKey = "";
9336
- /** 1s heartbeat while the sidebar is open: keeps live values (uptime,
9337
- * goal wall-clock, gradient phase of busy agent slots) ticking without
9338
- * needing an event. Stopped when the sidebar closes. */
9479
+ /** 1s heartbeat for the whole container lifetime: keeps live values (goal
9480
+ * wall-clock, gradient phase of busy agent slots) ticking while the sidebar
9481
+ * is open — the tick is a no-op when it is closed, and only `dispose()`
9482
+ * (TUI teardown) clears the interval. */
9339
9483
  heartbeat;
9340
9484
  constructor(manager, requestRender, colors) {
9341
9485
  super();
@@ -11002,6 +11146,7 @@ var SessionManager$1 = class {
11002
11146
  await this.unloadCurrentSession("switching session")?.close({ extractMemories: false });
11003
11147
  this.host.session = session;
11004
11148
  this.registerSessionHandlers(session);
11149
+ await syncDiagramCapabilityPrompt(session);
11005
11150
  }
11006
11151
  async syncRuntimeState(session = this.requireSession()) {
11007
11152
  const status = await session.getStatus();
@@ -13373,6 +13518,7 @@ function createInitialAppState(input) {
13373
13518
  isSwitchingSession: false,
13374
13519
  streamingPhase: "idle",
13375
13520
  streamingStartTime: 0,
13521
+ pendingUserAction: null,
13376
13522
  theme: input.tuiConfig.theme,
13377
13523
  version: input.version,
13378
13524
  hasNewVersion: false,
@@ -13809,14 +13955,28 @@ var ScreamTUI = class {
13809
13955
  patchLivePane(patch) {
13810
13956
  if (!hasPatchChanges(this.state.livePane, patch)) return;
13811
13957
  Object.assign(this.state.livePane, patch);
13958
+ this.syncPendingUserAction();
13812
13959
  this.lifecycleController.updateActivityPane();
13813
13960
  this.state.ui.requestRender();
13814
13961
  }
13815
13962
  resetLivePane() {
13816
13963
  this.state.livePane = { ...INITIAL_LIVE_PANE };
13964
+ this.syncPendingUserAction();
13817
13965
  this.lifecycleController.updateActivityPane();
13818
13966
  this.state.ui.requestRender();
13819
13967
  }
13968
+ /**
13969
+ * Mirror the live pane's pending panels onto `AppState.pendingUserAction` so
13970
+ * the footer status block can report "等待批准/等待回答" instead of claiming a
13971
+ * running phase while a panel is blocked on the user. Every mutation of
13972
+ * `livePane` goes through patchLivePane/resetLivePane, so this stays the
13973
+ * single writer of that field.
13974
+ */
13975
+ syncPendingUserAction() {
13976
+ const { pendingApproval, pendingQuestion } = this.state.livePane;
13977
+ const next = pendingApproval !== null ? "approval" : pendingQuestion !== null ? "question" : null;
13978
+ if (next !== this.state.appState.pendingUserAction) this.setAppState({ pendingUserAction: next });
13979
+ }
13820
13980
  requireSession() {
13821
13981
  if (this.session === void 0) throw new Error(getNoActiveSessionMessage());
13822
13982
  return this.session;
@@ -13893,7 +14053,7 @@ var ScreamTUI = class {
13893
14053
  const card = new OnboardingCardComponent({
13894
14054
  colors: this.state.theme.colors,
13895
14055
  onConfigure: () => {
13896
- import("./dispatch-BNVgLsrQ.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14056
+ import("./dispatch-CIGyjBDa.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
13897
14057
  this.restoreEditor();
13898
14058
  onDone?.();
13899
14059
  });
@@ -13914,7 +14074,7 @@ var ScreamTUI = class {
13914
14074
  if (this.state.startupState === "picker") return;
13915
14075
  try {
13916
14076
  await getKnowledgeStore();
13917
- if (isEmbeddingModelCached()) {
14077
+ if (hasEmbeddingModelCache()) {
13918
14078
  startManualEmbeddingDownload().catch(() => {});
13919
14079
  return;
13920
14080
  }
@@ -13929,8 +14089,9 @@ var ScreamTUI = class {
13929
14089
  const spinner = this.showProgressSpinner(t("kw.embedding_downloading"));
13930
14090
  let ok = false;
13931
14091
  let error;
14092
+ let failureKind;
13932
14093
  try {
13933
- ({ok, error} = await startManualEmbeddingDownload());
14094
+ ({ok, error, failureKind} = await startManualEmbeddingDownload());
13934
14095
  } catch (error) {
13935
14096
  error = error instanceof Error ? error.message : String(error);
13936
14097
  }
@@ -13940,7 +14101,7 @@ var ScreamTUI = class {
13940
14101
  });
13941
14102
  if (ok) this.showStatus(t("kw.embedding_ready"));
13942
14103
  else {
13943
- const detail = [t("knowledge.download_model_retry_hint"), error].filter(Boolean).join("\n");
14104
+ const detail = embeddingFailureDetail(failureKind, error);
13944
14105
  this.showNotice(t("kw.embedding_failed"), detail);
13945
14106
  }
13946
14107
  })();
@@ -16175,13 +16336,16 @@ async function ensureSessionsDir(homeDir) {
16175
16336
  const dir = getSessionsDir(homeDir);
16176
16337
  if (!existsSync(dir)) await mkdir(dir, { recursive: true });
16177
16338
  }
16178
- async function appendJournalLine(homeDir, sessionId, line) {
16339
+ async function appendJournalLine(homeDir, sessionId, line, throwOnError = false) {
16340
+ let writeError;
16179
16341
  await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
16342
+ writeError = error;
16180
16343
  log.warn("web: failed to persist journal line", {
16181
16344
  sessionId,
16182
16345
  error: String(error)
16183
16346
  });
16184
16347
  });
16348
+ if (throwOnError && writeError !== void 0) throw writeError;
16185
16349
  }
16186
16350
  let metaWriteSeq = 0;
16187
16351
  async function saveMetadata(homeDir, meta) {
@@ -16266,16 +16430,22 @@ function isMessageBearingEntry(entry) {
16266
16430
  */
16267
16431
  function contextHistoryToChatMessages(history) {
16268
16432
  const out = [];
16433
+ let pendingAnchor = "other";
16269
16434
  for (const message of history) {
16270
16435
  if (message.role !== "user" && message.role !== "assistant") continue;
16271
16436
  if (isInternalMessage(message)) continue;
16272
- if (message.origin !== void 0 && message.origin.kind !== "user") continue;
16437
+ const anchor = classifyUndoAnchor(message.origin);
16438
+ if (message.role === "user") {
16439
+ pendingAnchor = anchor;
16440
+ if (anchor === "skill" || anchor === "other") continue;
16441
+ }
16273
16442
  const text = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
16274
16443
  if (text.length === 0) continue;
16275
16444
  out.push({
16276
16445
  role: message.role,
16277
16446
  content: text,
16278
- tools: []
16447
+ tools: [],
16448
+ undoAnchor: message.role === "assistant" ? pendingAnchor : anchor
16279
16449
  });
16280
16450
  }
16281
16451
  return out;
@@ -16305,6 +16475,65 @@ function deriveTitle(messages) {
16305
16475
  const text = firstUser.content.trim();
16306
16476
  return text.length > 40 ? text.slice(0, 40) + "..." : text;
16307
16477
  }
16478
+ /**
16479
+ * Remove complete user-turn blocks from a rendered transcript. The core
16480
+ * context owns the authoritative undo operation; the web journal is append
16481
+ * only, so a durable undo marker asks this projection to hide the same turns
16482
+ * when snapshots are rebuilt (including after a restart).
16483
+ *
16484
+ * `beforeSeq` is an inclusive cursor captured before the core mutation. It
16485
+ * also covers a user message that reserved the next sequence but never
16486
+ * emitted a turn event.
16487
+ */
16488
+ function removeLastTurnsBefore(messages, count, beforeSeq) {
16489
+ if (count <= 0 || messages.length === 0) return;
16490
+ const firstFutureIndex = messages.findIndex((message) => typeof message.seq === "number" && message.seq > beforeSeq);
16491
+ const prefixLength = firstFutureIndex === -1 ? messages.length : firstFutureIndex;
16492
+ const prefix = messages.slice(0, prefixLength);
16493
+ const tail = messages.slice(prefixLength);
16494
+ let remaining = count;
16495
+ while (remaining > 0) {
16496
+ let anchor = -1;
16497
+ for (let i = prefix.length - 1; i >= 0; i -= 1) {
16498
+ const message = prefix[i];
16499
+ if (message?.role === "assistant" && message.undoAnchor === "skill") {
16500
+ anchor = i;
16501
+ while (anchor > 0) {
16502
+ const previous = prefix[anchor - 1];
16503
+ if (previous?.role !== "assistant" || previous.undoAnchor !== "skill") break;
16504
+ anchor -= 1;
16505
+ }
16506
+ break;
16507
+ }
16508
+ if (message?.role === "user") {
16509
+ anchor = i;
16510
+ break;
16511
+ }
16512
+ }
16513
+ if (anchor < 0) break;
16514
+ prefix.splice(anchor);
16515
+ remaining -= 1;
16516
+ }
16517
+ messages.splice(0, messages.length, ...prefix, ...tail);
16518
+ }
16519
+ function classifyUndoAnchor(origin) {
16520
+ if (origin === void 0 || origin === null) return "user";
16521
+ if (typeof origin !== "object") return "other";
16522
+ const kind = origin.kind;
16523
+ if (kind === "user") return "user";
16524
+ if (kind === "skill_activation" && origin.trigger === "user-slash") return "skill";
16525
+ return "other";
16526
+ }
16527
+ /** Match the core `/undo` anchor rule without importing agent-core internals. */
16528
+ function countUndoAnchors(history) {
16529
+ let count = 0;
16530
+ for (const message of history) {
16531
+ if (message.role !== "user") continue;
16532
+ const origin = message.origin;
16533
+ if (origin === void 0 || origin.kind === "user" || origin.kind === "skill_activation" && origin.trigger === "user-slash") count += 1;
16534
+ }
16535
+ return count;
16536
+ }
16308
16537
  const execFileAsync = promisify(execFile);
16309
16538
  /** Raw porcelain rows: repo-root-relative path + XY status. `getGitStatus`
16310
16539
  * enriches these into full `GitFileChange` records. */
@@ -16567,6 +16796,8 @@ var WebSession = class {
16567
16796
  heartbeatTimer = null;
16568
16797
  unsubscribe = null;
16569
16798
  busy = false;
16799
+ /** Prevent two concurrent REST `/undo` calls from mutating core history together. */
16800
+ undoInFlight = false;
16570
16801
  /**
16571
16802
  * In-flight turn count across all agents (main + subagents). Used by the
16572
16803
  * idle-exit watchdog: a single boolean is not enough because a subagent's
@@ -16655,13 +16886,13 @@ var WebSession = class {
16655
16886
  });
16656
16887
  return entries;
16657
16888
  }
16658
- async persistEntry(entry) {
16889
+ async persistEntry(entry, throwOnError = false) {
16659
16890
  if (!this.homeDir) return;
16660
16891
  const line = JSON.stringify({
16661
16892
  type: "journal",
16662
16893
  ...entry
16663
16894
  });
16664
- await appendJournalLine(this.homeDir, this.sessionId, line);
16895
+ await appendJournalLine(this.homeDir, this.sessionId, line, throwOnError);
16665
16896
  }
16666
16897
  async persistUserMessage(text, beforeSeq, clientMessageId) {
16667
16898
  this.userMessages.push({
@@ -16718,6 +16949,9 @@ var WebSession = class {
16718
16949
  * journal holds no message records.
16719
16950
  */
16720
16951
  seededMessages = [];
16952
+ /** Undo markers already reflected by a core-history seed must not be applied twice. */
16953
+ seedCoveredUndoMarkers = /* @__PURE__ */ new Set();
16954
+ historySeeded = false;
16721
16955
  /**
16722
16956
  * Mark the journal untrustworthy (byte-interleaving damage, see
16723
16957
  * journal-writer.ts).
@@ -16759,7 +16993,10 @@ var WebSession = class {
16759
16993
  * monotonic with live events and consistent with the getMessagesOlder cursor.
16760
16994
  */
16761
16995
  seedHistory(messages) {
16762
- if (messages.length === 0 || this.seededMessages.length > 0) return;
16996
+ if (this.historySeeded) return;
16997
+ this.historySeeded = true;
16998
+ for (const entry of this.journal) if (entry.payload.type === "web.history.undone") this.seedCoveredUndoMarkers.add(entry.seq);
16999
+ if (messages.length === 0) return;
16763
17000
  let seq = 1;
16764
17001
  for (const message of messages) {
16765
17002
  this.seededMessages.push({
@@ -16806,7 +17043,7 @@ var WebSession = class {
16806
17043
  * clients. Used for server-owned reconstruction material (finalized
16807
17044
  * assistant snapshots) that clients never consume as live events.
16808
17045
  */
16809
- appendDurableSilent(payload) {
17046
+ appendDurableSilent(payload, throwOnPersistenceError = false) {
16810
17047
  const entry = {
16811
17048
  seq: this.nextSeq++,
16812
17049
  epoch: this.epoch,
@@ -16814,8 +17051,12 @@ var WebSession = class {
16814
17051
  payload
16815
17052
  };
16816
17053
  this.journal.push(entry);
16817
- this.trackPending(this.persistEntry(entry));
16818
- return entry;
17054
+ const persisted = this.persistEntry(entry, throwOnPersistenceError);
17055
+ this.trackPending(persisted);
17056
+ return {
17057
+ entry,
17058
+ persisted
17059
+ };
16819
17060
  }
16820
17061
  subscribeEvents() {
16821
17062
  if (!this.session) return;
@@ -17144,13 +17385,14 @@ var WebSession = class {
17144
17385
  });
17145
17386
  }
17146
17387
  async handleCommand(ws, command, args, pendingMsgId) {
17147
- const ok = (message) => {
17388
+ const ok = (message, extra) => {
17148
17389
  this.broadcast({
17149
17390
  type: "command_result",
17150
17391
  command,
17151
17392
  ok: true,
17152
17393
  message,
17153
- pendingMsgId
17394
+ pendingMsgId,
17395
+ ...extra
17154
17396
  }, false);
17155
17397
  };
17156
17398
  const fail = (message) => {
@@ -17186,9 +17428,10 @@ var WebSession = class {
17186
17428
  }
17187
17429
  return;
17188
17430
  case "auto":
17431
+ case "ask":
17189
17432
  case "yes":
17190
17433
  case "bot": {
17191
- const mode = command === "auto" ? "auto" : command === "bot" ? "bot" : "yolo";
17434
+ const mode = command === "auto" ? "auto" : command === "ask" ? "ask" : command === "bot" ? "bot" : "yolo";
17192
17435
  try {
17193
17436
  await this.session.setPermission(mode);
17194
17437
  this.permission = mode;
@@ -17231,7 +17474,7 @@ var WebSession = class {
17231
17474
  fail("Fork 失败:无法复制当前会话。");
17232
17475
  return;
17233
17476
  }
17234
- ok(`会话已 fork,新会话 ID:${result.sessionId}`);
17477
+ ok(`会话已 fork,新会话 ID:${result.sessionId}`, { sessionId: result.sessionId });
17235
17478
  } catch (error) {
17236
17479
  fail(`Fork 失败:${errMsg(error)}`);
17237
17480
  }
@@ -17342,6 +17585,7 @@ var WebSession = class {
17342
17585
  planMode: false,
17343
17586
  wolfpackMode: false,
17344
17587
  rlmEnabled: false,
17588
+ rlmMaxDepth: null,
17345
17589
  contextTokens: 0,
17346
17590
  maxContextTokens: 0,
17347
17591
  contextUsage: 0
@@ -17518,6 +17762,64 @@ var WebSession = class {
17518
17762
  if (!this.session) throw new HttpError(409, "Session is archived (read-only)", ErrorCodes.SESSION_CLOSED);
17519
17763
  return this.session;
17520
17764
  }
17765
+ /**
17766
+ * Undo user turns in the core session and append a durable web projection
17767
+ * marker. The core RPC deliberately has no UI knowledge, so without the
17768
+ * marker the old append-only web journal would resurrect the removed turns
17769
+ * on the next snapshot or after a restart.
17770
+ */
17771
+ async undoHistory(count) {
17772
+ if (!Number.isSafeInteger(count) || count < 1) throw new HttpError(400, "Undo count must be a positive integer.");
17773
+ if (this.busy) throw new HttpError(409, "Session is busy", ErrorCodes.TURN_AGENT_BUSY);
17774
+ const session = this.requireLiveSession();
17775
+ if (this.undoInFlight) throw new HttpError(409, "Undo already in progress");
17776
+ this.undoInFlight = true;
17777
+ try {
17778
+ const beforeSeq = this.nextSeq;
17779
+ let beforeAnchors = null;
17780
+ try {
17781
+ beforeAnchors = countUndoAnchors((await session.getContext()).history);
17782
+ } catch (error) {
17783
+ log.warn("web: failed to read context before undo", {
17784
+ sessionId: this.sessionId,
17785
+ error: String(error)
17786
+ });
17787
+ }
17788
+ await session.undoHistory(count);
17789
+ let removed = count;
17790
+ if (beforeAnchors !== null) try {
17791
+ const afterAnchors = countUndoAnchors((await session.getContext()).history);
17792
+ removed = Math.max(0, beforeAnchors - afterAnchors);
17793
+ } catch (error) {
17794
+ log.warn("web: failed to read context after undo", {
17795
+ sessionId: this.sessionId,
17796
+ error: String(error)
17797
+ });
17798
+ }
17799
+ if (removed <= 0) {
17800
+ await this.refreshStatus();
17801
+ this.broadcastStatus();
17802
+ return;
17803
+ }
17804
+ const { entry, persisted } = this.appendDurableSilent({
17805
+ type: "web.history.undone",
17806
+ count: removed,
17807
+ beforeSeq
17808
+ }, true);
17809
+ await persisted;
17810
+ this.broadcast({
17811
+ type: "event",
17812
+ seq: entry.seq,
17813
+ epoch: entry.epoch,
17814
+ payload: entry.payload
17815
+ }, false);
17816
+ this.trackPending(this.updateTitle());
17817
+ await this.refreshStatus();
17818
+ this.broadcastStatus();
17819
+ } finally {
17820
+ this.undoInFlight = false;
17821
+ }
17822
+ }
17521
17823
  requireGoalSession(allowBusy = false) {
17522
17824
  if (!this.session) throw new HttpError(409, "Session is archived (read-only)", ErrorCodes.SESSION_CLOSED);
17523
17825
  if (!allowBusy && this.busy) throw new HttpError(409, "Session is busy", ErrorCodes.TURN_AGENT_BUSY);
@@ -17595,6 +17897,7 @@ var WebSession = class {
17595
17897
  role: "assistant",
17596
17898
  content: "",
17597
17899
  tools: [],
17900
+ undoAnchor: classifyUndoAnchor(event.origin),
17598
17901
  seq: entry.seq,
17599
17902
  model: this.cachedStatus?.model,
17600
17903
  turnStats: {
@@ -17648,13 +17951,20 @@ var WebSession = class {
17648
17951
  break;
17649
17952
  case "web.message.finalized": {
17650
17953
  const finalized = event.message;
17651
- finalized.seq = messages.length > 0 && messages.at(-1)?.role === "assistant" ? messages.at(-1).seq : entry.seq;
17954
+ const lastSkeletonSeq = messages.length > 0 && messages.at(-1)?.role === "assistant" ? messages.at(-1).seq : entry.seq;
17955
+ if (finalized.undoAnchor === void 0 && messages.at(-1)?.role === "assistant") finalized.undoAnchor = messages.at(-1).undoAnchor;
17956
+ finalized.seq = lastSkeletonSeq;
17652
17957
  if (messages.length > 0 && messages.at(-1)?.role === "assistant") messages[messages.length - 1] = finalized;
17653
17958
  else messages.push(finalized);
17654
17959
  currentAssistant = null;
17655
17960
  turnFinalized = true;
17656
17961
  break;
17657
17962
  }
17963
+ case "web.history.undone":
17964
+ if (!this.seedCoveredUndoMarkers.has(entry.seq)) removeLastTurnsBefore(messages, event.count, event.beforeSeq);
17965
+ currentAssistant = null;
17966
+ turnFinalized = false;
17967
+ break;
17658
17968
  case "turn.ended":
17659
17969
  if (event.reason === "failed" && currentAssistant) currentAssistant.isError = true;
17660
17970
  if (currentAssistant && !turnFinalized && currentAssistant.content === "") currentAssistant.degraded = true;
@@ -17666,7 +17976,10 @@ var WebSession = class {
17666
17976
  }
17667
17977
  for (let i = userIndex; i < this.userMessages.length; i++) {
17668
17978
  const item = this.userMessages[i];
17669
- if (item !== void 0) messages.push(item.msg);
17979
+ if (item !== void 0) messages.push({
17980
+ ...item.msg,
17981
+ seq: item.beforeSeq
17982
+ });
17670
17983
  }
17671
17984
  return messages;
17672
17985
  }
@@ -18263,7 +18576,9 @@ var SessionManager = class {
18263
18576
  await ws.switchRlm(enabled, maxDepth);
18264
18577
  }
18265
18578
  async undoHistory(sessionId, count) {
18266
- await this.getLiveSession(sessionId).undoHistory(count);
18579
+ const ws = this.sessions.get(sessionId);
18580
+ if (!ws) throw new HttpError(404, "Session not found", ErrorCodes.SESSION_NOT_FOUND);
18581
+ await ws.undoHistory(count);
18267
18582
  }
18268
18583
  async compact(sessionId, instruction) {
18269
18584
  await this.getLiveSession(sessionId).compact(instruction ? { instruction } : {});
@@ -18433,16 +18748,32 @@ async function handleSessionDataRoutes(req, res, url, method, resolveSession, wo
18433
18748
  }
18434
18749
  return true;
18435
18750
  }
18436
- if (url === `${API_PREFIX}/git/status` && method === "GET") {
18437
- const gs = await getGitStatus(workDir);
18438
- res.writeHead(200, { "Content-Type": "application/json" });
18439
- res.end(JSON.stringify(gs));
18440
- return true;
18441
- }
18442
- if (url.startsWith(`${API_PREFIX}/git/diff?`) && method === "GET") {
18443
- const relPath = new URLSearchParams(url.split("?")[1] ?? "").get("path") ?? "";
18751
+ const gitPath = url.split("?")[0];
18752
+ const isGitStatusRoute = gitPath === `${API_PREFIX}/git/status` && method === "GET";
18753
+ if (isGitStatusRoute || gitPath === `${API_PREFIX}/git/diff` && method === "GET") {
18754
+ const gitQuery = new URLSearchParams(url.split("?")[1] ?? "");
18755
+ const requestedGitSessionId = gitQuery.get("sessionId");
18756
+ let gitWorkDir = workDir;
18757
+ if (requestedGitSessionId !== null) {
18758
+ const gitSession = resolveSession(requestedGitSessionId);
18759
+ if (!gitSession) {
18760
+ sendJson(res, 404, {
18761
+ code: ErrorCodes.SESSION_NOT_FOUND,
18762
+ message: "Session not found"
18763
+ });
18764
+ return true;
18765
+ }
18766
+ gitWorkDir = gitSession.workDir;
18767
+ }
18768
+ if (isGitStatusRoute) {
18769
+ const gs = await getGitStatus(gitWorkDir);
18770
+ res.writeHead(200, { "Content-Type": "application/json" });
18771
+ res.end(JSON.stringify(gs));
18772
+ return true;
18773
+ }
18774
+ const relPath = gitQuery.get("path") ?? "";
18444
18775
  try {
18445
- const result = await getGitFileDiff(workDir, relPath);
18776
+ const result = await getGitFileDiff(gitWorkDir, relPath);
18446
18777
  res.writeHead(200, { "Content-Type": "application/json" });
18447
18778
  res.end(JSON.stringify({
18448
18779
  path: relPath,
@@ -18653,6 +18984,7 @@ async function handleSessionControlRoutes(req, res, url, method, manager) {
18653
18984
  return false;
18654
18985
  }
18655
18986
  async function handleResourceRoutes(req, res, url, method, manager) {
18987
+ const routePath = url.split("?")[0] ?? url;
18656
18988
  const skillsMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/skills$`).exec(url);
18657
18989
  if (skillsMatch && method === "GET") {
18658
18990
  try {
@@ -18840,7 +19172,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
18840
19172
  }
18841
19173
  return true;
18842
19174
  }
18843
- const tasksMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks$`).exec(url);
19175
+ const tasksMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks$`).exec(routePath);
18844
19176
  if (tasksMatch && method === "GET") {
18845
19177
  try {
18846
19178
  const query = new URLSearchParams(url.split("?")[1] ?? "");
@@ -18852,7 +19184,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
18852
19184
  }
18853
19185
  return true;
18854
19186
  }
18855
- const taskOutputMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/output$`).exec(url);
19187
+ const taskOutputMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/output$`).exec(routePath);
18856
19188
  if (taskOutputMatch && method === "GET") {
18857
19189
  try {
18858
19190
  const query = new URLSearchParams(url.split("?")[1] ?? "");
@@ -18863,7 +19195,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
18863
19195
  }
18864
19196
  return true;
18865
19197
  }
18866
- const taskStopMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/stop$`).exec(url);
19198
+ const taskStopMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/stop$`).exec(routePath);
18867
19199
  if (taskStopMatch && method === "POST") {
18868
19200
  try {
18869
19201
  const body = await readJsonBody(req);