scream-code 0.16.6 → 0.16.8

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,9 @@ 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 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";
7
+ import { k as MemoryMemoStore } from "./src-CXmZg1VT.mjs";
8
+ import { $n as encodeWorkDirKey, $t as lerpGradient, A as highlightLines, An as embeddingFailureDetail, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as detectShellEnvironment, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getLlmNotSetMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as sortSlashCommands, Dt as createEditorTheme, E as UserMessageComponent, En as BUILTIN_SLASH_COMMANDS, Et as resolveThemeSync, F as StatusMessageComponent, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_COMMAND_NAME, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getInputHistoryFile, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiConfigParseError, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as PRODUCT_NAME, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_UI_MODE, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as TuiLikePreferencesSchema, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as hasEmbeddingModelCache, Mt as createThemeStyles, N as ThinkingComponent, Nn as startManualEmbeddingDownload, Nt as contrastTextHex, O as renderDiffLines, On as isExperimentalFlagEnabled, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as ScreamHarness, Qt as FooterComponent, R as SkillActivationComponent, Rn as loadTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getCtrlDHint, St as supportsBalance, Tn as buildSkillSlashCommands, U as getSharedSpeedTracker, Un as getLogDir, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getDataDir, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as fetchCatalog, Xt as isBusy, Y as resetBreathingClock, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as saveCatalogCache, Zt as isStreaming, _n as EXIT_CONFIRM_WINDOW_MS, a as syncDiagramCapabilityPrompt, an as printableChar, ar as resolveScreamHome, at as toTerminalHyperlink, b as readUpdateCache, bn as TIP_ROTATION_INTERVAL_MS, c as disposeChildren, cn as appendStreamingArgsPreview, cr as resolveGlobalLogPath, d as isPlanExpandable, dn as isTodoItemShape, dr as ErrorCodes, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as SCREAM_ERROR_INFO, ft as handleFusionPlanCommand, gn as EMPTY_SESSION_HINT_URL, hn as truncateErrorMessage, i as buildRoleAdditionalText, it as isInternalMessage, j as langFromPath, jn as getKnowledgeStore, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as setExperimentalFlags, kt as isEmptySessionHintDismissed, l as hasDispose, ln as argsRecord, lr as isScreamError, mn as stringValue, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as flushDiagnosticLogs, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, q as BREATHE_CYCLE_MS, qn as CLI_USER_AGENT_PRODUCT, qt as TERMINAL_FOCUS_OUT, r as openUrl, rr as removeSessionIndexEntry, sn as appendSessionHeaderHint, sr as log, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as isOrphanedToolCallError, v as selectUpdateTarget, vn as MAIN_AGENT_ID, w as getDaemonInstructions, wn as getNoActiveSessionMessage, x as appendJsonlLine, xn as getCtrlCHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as SESSION_TIPS, z as AssistantMessageComponent, zn as saveTuiConfig, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-CNitkOqX.mjs";
9
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-6v2667rm.mjs";
9
10
  import { createRequire } from "node:module";
10
11
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
11
12
  import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -32,7 +33,7 @@ function optionalBuildString(value) {
32
33
  return typeof value === "string" && value.length > 0 ? value : void 0;
33
34
  }
34
35
  const SCREAM_BUILD_INFO = {
35
- version: optionalBuildString("0.16.6"),
36
+ version: optionalBuildString("0.16.8"),
36
37
  channel: optionalBuildString(""),
37
38
  commit: optionalBuildString(""),
38
39
  buildTarget: optionalBuildString("darwin-arm64")
@@ -1877,7 +1878,7 @@ var EditorKeyboardController = class {
1877
1878
  if (media === null) return;
1878
1879
  const meta = parseImageMeta(media.bytes);
1879
1880
  if (meta === null) return;
1880
- const attachment = this.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height);
1881
+ const attachment = this.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height, path);
1881
1882
  this.host.state.editor.insertTextAtCursor?.(`${attachment.placeholder} `);
1882
1883
  this.host.state.ui.requestRender();
1883
1884
  }
@@ -2363,15 +2364,15 @@ function formatStepDebugTiming(input) {
2363
2364
  const latency = input.llmFirstTokenLatencyMs;
2364
2365
  const streamMs = input.llmStreamDurationMs;
2365
2366
  if (latency === void 0 || streamMs === void 0) return void 0;
2366
- const parts = [`TTFT: ${formatDuration$1(latency)}`];
2367
+ const parts = [`TTFT: ${formatDuration(latency)}`];
2367
2368
  const outputTokens = input.usage?.output;
2368
2369
  if (outputTokens !== void 0 && outputTokens > 0 && streamMs > 0) {
2369
2370
  const tps = (outputTokens / (streamMs / 1e3)).toFixed(1);
2370
- parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration$1(streamMs)})`);
2371
+ parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration(streamMs)})`);
2371
2372
  }
2372
2373
  return `[Debug] ${parts.join(" | ")}`;
2373
2374
  }
2374
- function formatDuration$1(ms) {
2375
+ function formatDuration(ms) {
2375
2376
  if (ms < 1e3) return `${Math.round(ms)}ms`;
2376
2377
  return `${(ms / 1e3).toFixed(1)}s`;
2377
2378
  }
@@ -3164,7 +3165,8 @@ var SessionEventHandler = class {
3164
3165
  this.host.streamingUI.resetToolUi();
3165
3166
  this.host.streamingUI.finalizeLiveTextBuffers();
3166
3167
  this.host.streamingUI.endActivityGroup();
3167
- this.host.showError(`[${event.code}] ${event.message}`);
3168
+ const shown = appendSessionHeaderHint(`[${event.code}] ${event.message}`, (name) => t("error.session_header_hint", { name }));
3169
+ this.host.showError(shown);
3168
3170
  if (event.code === "turn.agent_busy" && !hadActiveTurn && this.host.state.appState.streamingPhase !== "idle") {
3169
3171
  this.host.setAppState({ streamingPhase: "idle" });
3170
3172
  this.host.resetLivePane();
@@ -4621,6 +4623,7 @@ var StreamingUIController = class StreamingUIController {
4621
4623
  completeToolResult(toolCallId, result) {
4622
4624
  const matchedCall = this._activeToolCalls.get(toolCallId);
4623
4625
  if (matchedCall !== void 0) this.onToolCallEnd(toolCallId, result);
4626
+ else if (this._pendingToolComponents.has(toolCallId)) this.onToolCallEnd(toolCallId, result);
4624
4627
  this._activeToolCalls.delete(toolCallId);
4625
4628
  this._streamingToolCallArguments.delete(toolCallId);
4626
4629
  return matchedCall;
@@ -4907,6 +4910,7 @@ var StreamingUIController = class StreamingUIController {
4907
4910
  this._assistantDraft = "";
4908
4911
  this._shownAssistantLength = 0;
4909
4912
  this.clearFlushTimerIfIdle();
4913
+ this._streamingBlock?.component.setStreaming(false);
4910
4914
  this._streamingBlock = null;
4911
4915
  this._thinkingDraft = "";
4912
4916
  this.lastDeltaAt = void 0;
@@ -5010,6 +5014,7 @@ var StreamingUIController = class StreamingUIController {
5010
5014
  const block = this._streamingBlock;
5011
5015
  if (block !== null) {
5012
5016
  block.entry.content = fullText;
5017
+ block.component.setStreaming(true);
5013
5018
  block.component.updateContent(fullText);
5014
5019
  this.host.state.ui.requestRender();
5015
5020
  }
@@ -5019,6 +5024,7 @@ var StreamingUIController = class StreamingUIController {
5019
5024
  const block = this._streamingBlock;
5020
5025
  this._streamingBlock = null;
5021
5026
  if (block !== null) {
5027
+ block.component.setStreaming(false);
5022
5028
  this.host.transcriptController.unmarkPending(block.component);
5023
5029
  this.host.transcriptController.commit();
5024
5030
  }
@@ -6243,10 +6249,28 @@ function cronDetail(data) {
6243
6249
  }
6244
6250
  //#endregion
6245
6251
  //#region src/tui/utils/image-attachment-store.ts
6252
+ /**
6253
+ * Registry for media pasted into the input box.
6254
+ *
6255
+ * Each paste produces an `ImageAttachment` with an auto-incrementing id
6256
+ * or `VideoAttachment` with a human-readable placeholder (`[image #1
6257
+ * (640×480)]` / `[video #2 sample.mov]`). The placeholder is what the
6258
+ * user sees in the input field; on submit, `extractMediaAttachments`
6259
+ * walks the text and expands image placeholders to image content parts
6260
+ * and video placeholders to file-path tags for `ReadMediaFile`.
6261
+ *
6262
+ * Scope is per-`ScreamTUI` instance. Reloads (`/new`, `/clear`,
6263
+ * session switch) call `clear()` so ids restart from 1 and stale
6264
+ * prompt attachments are dropped. We intentionally do NOT persist
6265
+ * attachments across sessions — coding-agent doesn't either, and
6266
+ * `--resume` wouldn't know how to materialize the files anyway.
6267
+ */
6268
+ /** Temp-file basename prefix used by path-mode image materialization. */
6269
+ const TEMP_ATTACHMENT_PREFIX = "scream-attachment-";
6246
6270
  var ImageAttachmentStore = class {
6247
6271
  nextId = 1;
6248
6272
  byId = /* @__PURE__ */ new Map();
6249
- addImage(bytes, mime, width, height) {
6273
+ addImage(bytes, mime, width, height, sourcePath) {
6250
6274
  const id = this.nextId;
6251
6275
  this.nextId += 1;
6252
6276
  const attachment = {
@@ -6256,7 +6280,8 @@ var ImageAttachmentStore = class {
6256
6280
  mime,
6257
6281
  width,
6258
6282
  height,
6259
- placeholder: formatPlaceholder(id, width, height)
6283
+ placeholder: formatPlaceholder(id, width, height),
6284
+ ...sourcePath !== void 0 && sourcePath.length > 0 ? { sourcePath } : {}
6260
6285
  };
6261
6286
  this.byId.set(id, attachment);
6262
6287
  return attachment;
@@ -6282,6 +6307,14 @@ var ImageAttachmentStore = class {
6282
6307
  return this.byId.get(id);
6283
6308
  }
6284
6309
  clear() {
6310
+ const tempBase = join(tmpdir(), TEMP_ATTACHMENT_PREFIX);
6311
+ for (const attachment of this.byId.values()) {
6312
+ if (attachment.kind !== "image") continue;
6313
+ const path = attachment.sourcePath;
6314
+ if (path !== void 0 && path.startsWith(tempBase)) try {
6315
+ unlinkSync(path);
6316
+ } catch {}
6317
+ }
6285
6318
  this.byId.clear();
6286
6319
  this.nextId = 1;
6287
6320
  }
@@ -6879,20 +6912,6 @@ const gitPanel = {
6879
6912
  };
6880
6913
  //#endregion
6881
6914
  //#region src/tui/components/sidebar/panels/session-panel.ts
6882
- function formatDuration(ms) {
6883
- const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
6884
- if (getLocale() === "zh") {
6885
- if (totalSeconds < 60) return `${totalSeconds}秒`;
6886
- const hours = Math.floor(totalSeconds / 3600);
6887
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6888
- return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`;
6889
- }
6890
- if (totalSeconds < 60) return `${totalSeconds}s`;
6891
- const hours = Math.floor(totalSeconds / 3600);
6892
- const minutes = Math.floor(totalSeconds % 3600 / 60);
6893
- if (hours > 0) return `${hours}h ${minutes}m`;
6894
- return `${minutes}m`;
6895
- }
6896
6915
  /** 1,234 → "1,234" via the platform locale grouping. */
6897
6916
  function formatCount$1(n) {
6898
6917
  return n.toLocaleString("en-US");
@@ -6918,8 +6937,8 @@ function gridFits(rows, width) {
6918
6937
  /**
6919
6938
  * Session summary, live-rendered each frame as a strict two-column table:
6920
6939
  * one field per row, labels sharing a left column, values sharing a right
6921
- * edge. Uptime derives from `startedAt` so it ticks between data updates;
6922
- * every other value comes from the snapshot in getData().
6940
+ * edge. Every value comes from the snapshot in getData(); nothing here is
6941
+ * derived from wall-clock time.
6923
6942
  */
6924
6943
  var SessionPanelContent = class {
6925
6944
  ctx;
@@ -6934,7 +6953,6 @@ var SessionPanelContent = class {
6934
6953
  if (stats === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.session_empty"))];
6935
6954
  const label = (s) => chalk.hex(this.colors.textDim)(s);
6936
6955
  const value = (s) => chalk.hex(this.colors.text)(s);
6937
- const now = Date.now();
6938
6956
  const tokenRows = [
6939
6957
  [t("sidebar.tokens"), stats.tokensTotal],
6940
6958
  [t("sidebar.tokens_cache_hit"), stats.tokensInputCacheHit],
@@ -6942,7 +6960,6 @@ var SessionPanelContent = class {
6942
6960
  [t("sidebar.tokens_output"), stats.tokensOutput]
6943
6961
  ];
6944
6962
  const countRows = [
6945
- [t("sidebar.uptime"), formatDuration(now - stats.startedAt)],
6946
6963
  [t("sidebar.turns"), formatCount$1(stats.turns)],
6947
6964
  [t("sidebar.tools"), formatCount$1(stats.toolCalls)],
6948
6965
  [t("sidebar.calls"), formatCount$1(stats.apiCalls)],
@@ -6989,8 +7006,8 @@ function statusText(status) {
6989
7006
  }
6990
7007
  }
6991
7008
  /**
6992
- * Fixed 8-slot subagent panel: one row per default subagent type (plus any
6993
- * custom types appended in spawn order). Live-rendered each frame from the
7009
+ * Fixed default-slot subagent panel: one row per default subagent type (plus
7010
+ * any custom types appended in spawn order). Live-rendered each frame from the
6994
7011
  * current snapshot; idle slots stay greyed out.
6995
7012
  */
6996
7013
  var AgentsPanelContent = class {
@@ -7013,7 +7030,7 @@ var AgentsPanelContent = class {
7013
7030
  renderSlot(cell, slot, index, width) {
7014
7031
  const dim = (s) => chalk.hex(this.colors.textDim)(s);
7015
7032
  const idle = slot.status === "idle";
7016
- const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .125) % 1;
7033
+ const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .6180339887498949) % 1;
7017
7034
  const active = slot.status === "working" || slot.status === "outputting" ? chalk.hex(lerpGradient(phase)).bold : void 0;
7018
7035
  const marker = padLabel((active ?? chalk.hex(this.statusColor(slot.status)))(idle ? "○" : "●"), 2);
7019
7036
  const label = (idle ? dim : chalk.hex(this.colors.textStrong))(cell.label);
@@ -7273,23 +7290,38 @@ function toneFor(site, stale) {
7273
7290
  /**
7274
7291
  * Merge the measured provider reading with the probe samples into Hub rows.
7275
7292
  * The provider row always leads (it answers "is the model I'm using reachable"),
7276
- * and it staleness-checks against its own budget: a long idle stretch means
7277
- * nothing was asked, not that the network died.
7293
+ * it staleness-checks against its own budget: a long idle stretch means nothing
7294
+ * was asked, not that the network died — and it is coloured on its own ladder
7295
+ * (see {@link hubModelTone}), not on the probe bar.
7278
7296
  */
7279
7297
  function buildHubSamples(snapshot, provider, now = Date.now()) {
7280
7298
  const ageMs = provider.sampledAt === void 0 ? 0 : now - provider.sampledAt;
7281
7299
  return [{
7282
7300
  id: HUB_MODEL_ROW_ID,
7283
7301
  ms: provider.ms,
7284
- tone: hubLatencyTone(provider.ms, ageMs >= HUB_MODEL_STALE_MS)
7302
+ tone: hubModelTone(provider.ms, ageMs >= HUB_MODEL_STALE_MS)
7285
7303
  }, ...snapshot.samples];
7286
7304
  }
7287
- /** Shared tone rule for every Hub row (probe sites and the measured provider). */
7305
+ /** Tone rule for the probed endpoint rows: green ≤{@link HUB_GOOD_MS}, amber above. */
7288
7306
  function hubLatencyTone(ms, stale = false) {
7289
7307
  if (ms === void 0 || stale) return "dim";
7290
7308
  return ms <= 100 ? "ok" : "warn";
7291
7309
  }
7292
7310
  /**
7311
+ * Tone rule for the measured provider row (model first token), deliberately
7312
+ * separate from the probe ladder: a first token and a HEAD request are not
7313
+ * comparable, and one shared bar would call every healthy answer slow. Green
7314
+ * ≤{@link HUB_MODEL_GOOD_MS}, amber up to {@link HUB_MODEL_SLOW_MS}, red past
7315
+ * it — that long is worth flagging, not merely dimming. `undefined` (nothing
7316
+ * measured yet, or a provider just switched) and a stale reading stay dim:
7317
+ * neither is an outage.
7318
+ */
7319
+ function hubModelTone(ms, stale = false) {
7320
+ if (ms === void 0 || stale) return "dim";
7321
+ if (ms <= 5e3) return "ok";
7322
+ return ms <= 15e3 ? "warn" : "down";
7323
+ }
7324
+ /**
7293
7325
  * Compact latency reading: whole milliseconds below a second, one-decimal
7294
7326
  * seconds above — a probe that never answers must never read as `65400ms`.
7295
7327
  */
@@ -7310,9 +7342,11 @@ const EMPTY_MARK = "--";
7310
7342
  *
7311
7343
  * Values share one right edge through the same two-column engine the Session
7312
7344
  * panel uses, so rows never jitter as numbers change width, and only the value
7313
- * carries colour (green ≤100ms, amber above it, red when a site stopped
7314
- * answering). The panel is a pure view: probing happens in the data provider,
7315
- * which is what keeps a slow endpoint from ever touching a frame.
7345
+ * carries colour. Two ladders, because two different things are timed: probe
7346
+ * rows are green ≤100ms, amber above it, red once a site stopped answering,
7347
+ * while the measured provider row (model first token) is green ≤5s, amber up to
7348
+ * 15s and red past that. The panel is a pure view: probing happens in the data
7349
+ * provider, which is what keeps a slow endpoint from ever touching a frame.
7316
7350
  */
7317
7351
  var HubPanelContent = class {
7318
7352
  ctx;
@@ -7924,10 +7958,6 @@ var LifecycleController = class LifecycleController {
7924
7958
  * frame asked, so a closed sidebar produces no traffic. */
7925
7959
  hubProbe = createHubProbe();
7926
7960
  sidebarGitCacheWorkDir = "";
7927
- /** Session start time for the sidebar uptime counter. Reset on session
7928
- * switch so a resumed old session doesn't show the TUI process age. */
7929
- sidebarSessionStartedAt = Date.now();
7930
- sidebarSessionId;
7931
7961
  /** Memo for the session-stats aggregation: recomputed only when the
7932
7962
  * transcript length, compaction count or usage object changes. */
7933
7963
  sidebarStatsMemo;
@@ -8195,10 +8225,6 @@ var LifecycleController = class LifecycleController {
8195
8225
  };
8196
8226
  }
8197
8227
  readSidebarSessionStats(appState) {
8198
- if (this.sidebarSessionId !== appState.sessionId) {
8199
- this.sidebarSessionId = appState.sessionId;
8200
- this.sidebarSessionStartedAt = Date.now();
8201
- }
8202
8228
  const entries = this.host.state.transcriptEntries;
8203
8229
  const usage = appState.sessionUsage;
8204
8230
  const subagentUsage = appState.subagentUsage;
@@ -8220,8 +8246,7 @@ var LifecycleController = class LifecycleController {
8220
8246
  tokensTotal: LifecycleController.sumTokens(usage) + subagentOutput,
8221
8247
  tokensInputCacheHit: usage.inputCacheRead,
8222
8248
  tokensInputCacheMiss: usage.inputOther + usage.inputCacheCreation,
8223
- tokensOutput: usage.output + subagentOutput,
8224
- startedAt: this.sidebarSessionStartedAt
8249
+ tokensOutput: usage.output + subagentOutput
8225
8250
  };
8226
8251
  this.sidebarStatsMemo = {
8227
8252
  length: entries.length,
@@ -8515,7 +8540,7 @@ var FileMentionProvider = class {
8515
8540
  const atPrefix = extractAtPrefix(textBeforeCursor);
8516
8541
  if (!options.force && atPrefix === null && textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
8517
8542
  const query = textBeforeCursor.slice(1);
8518
- const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => item.value).slice(0, MAX_SUGGESTIONS_WHEN_QUERY);
8543
+ const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => !query.startsWith("skill:") && item.value.startsWith("skill:") ? item.value.slice(6) : item.value);
8519
8544
  if (filtered.length === 0) return null;
8520
8545
  return {
8521
8546
  items: filtered,
@@ -8693,8 +8718,25 @@ var QueuePaneComponent = class extends Container {
8693
8718
  };
8694
8719
  //#endregion
8695
8720
  //#region src/tui/utils/image-placeholder.ts
8721
+ /**
8722
+ * Scan submitted text for media placeholders and produce
8723
+ * the `PromptPart[]` we'll send to the SDK prompt endpoint.
8724
+ *
8725
+ * Rules:
8726
+ * - Only placeholders that resolve against `store` get extracted.
8727
+ * A literal `[image #999 ...]` the user typed themselves stays in
8728
+ * the text (we can't hallucinate files for it).
8729
+ * - Order is preserved for text/image/video segments. Image placeholders
8730
+ * expand to image content parts so the prompt reaches the provider
8731
+ * without relying on a model tool call. Video placeholders still expand
8732
+ * to file-path tags so `ReadMediaFile` can own video upload behavior.
8733
+ * - Adjacent text segments are flattened — empty / whitespace-only
8734
+ * segments drop out so we never emit `{type:'text', text:' '}`
8735
+ * noise between two media parts.
8736
+ */
8696
8737
  const PLACEHOLDER_REGEX = /\[(image|video) #(\d+) (?:(\(\d+×\d+\))|([^\]]+))\]/g;
8697
- function extractMediaAttachments(text, store) {
8738
+ function extractMediaAttachments(text, store, options = {}) {
8739
+ const imageMode = options.imageMode ?? "inline";
8698
8740
  const parts = [];
8699
8741
  const imageAttachmentIds = [];
8700
8742
  const videoAttachmentIds = [];
@@ -8714,6 +8756,13 @@ function extractMediaAttachments(text, store) {
8714
8756
  if (attachment.kind === "video") {
8715
8757
  pushText(parts, tagTextForVideo(attachment));
8716
8758
  videoAttachmentIds.push(id);
8759
+ } else if (imageMode === "path") {
8760
+ try {
8761
+ pushText(parts, formatMediaTag("image", ensureImagePath(attachment)));
8762
+ } catch {
8763
+ parts.push(imagePartForAttachment(attachment));
8764
+ }
8765
+ imageAttachmentIds.push(id);
8717
8766
  } else {
8718
8767
  parts.push(imagePartForAttachment(attachment));
8719
8768
  imageAttachmentIds.push(id);
@@ -8752,6 +8801,15 @@ function imagePartForAttachment(att) {
8752
8801
  imageUrl: { url: `data:${att.mime};base64,${base64}` }
8753
8802
  };
8754
8803
  }
8804
+ /** Persist bytes to a temp file when path delivery needs a disk path. */
8805
+ function ensureImagePath(att) {
8806
+ if (att.sourcePath !== void 0 && att.sourcePath.length > 0) return att.sourcePath;
8807
+ const ext = att.mime === "image/jpeg" ? "jpg" : att.mime.split("/")[1]?.replace("+xml", "").replace("+json", "") ?? "png";
8808
+ const path = join(tmpdir(), `${TEMP_ATTACHMENT_PREFIX}${randomUUID()}.${ext}`);
8809
+ writeFileSync(path, att.bytes);
8810
+ att.sourcePath = path;
8811
+ return path;
8812
+ }
8755
8813
  function tagTextForVideo(att) {
8756
8814
  return formatMediaTag("video", att.sourcePath);
8757
8815
  }
@@ -8919,7 +8977,8 @@ var InputController = class InputController {
8919
8977
  this.dispatchUserInput(text, session);
8920
8978
  }
8921
8979
  dispatchUserInput(text, session) {
8922
- const extraction = extractMediaAttachments(text, this.host.imageStore);
8980
+ const canInlineImages = this.supportsCurrentModelCapability("image_in");
8981
+ const extraction = extractMediaAttachments(text, this.host.imageStore, { imageMode: canInlineImages ? "inline" : "path" });
8923
8982
  if (!this.validateMediaCapabilities(extraction)) return;
8924
8983
  if (extraction.hasMedia) this.sendMessage(session, text, {
8925
8984
  hasMedia: true,
@@ -9096,10 +9155,6 @@ var InputController = class InputController {
9096
9155
  }
9097
9156
  validateMediaCapabilities(extraction) {
9098
9157
  if (!extraction.hasMedia) return true;
9099
- if (extraction.imageAttachmentIds.length > 0 && !this.supportsCurrentModelCapability("image_in")) {
9100
- this.host.showError(t("error.image_not_supported"));
9101
- return false;
9102
- }
9103
9158
  if (extraction.videoAttachmentIds.length > 0 && !this.supportsCurrentModelCapability("video_in")) {
9104
9159
  this.host.showError(t("error.video_not_supported"));
9105
9160
  return false;
@@ -9467,7 +9522,7 @@ const SIDE_PADDING = 1;
9467
9522
  * Renders the stacked sidebar: every visible registered panel gets its own
9468
9523
  * full box frame (┌─┐│└┘) with the panel title embedded in the top frame, one
9469
9524
  * below the other. Panels render live each frame (their body reads the
9470
- * current data snapshot), so time-based values (uptime, goal wall-clock)
9525
+ * current data snapshot), so time-based values (goal wall-clock)
9471
9526
  * tick without rebuilding. The focused panel's title is bolded; focus is
9472
9527
  * moved by the SidebarManager (`/sidebar next/prev`, Ctrl+X toggles).
9473
9528
  * The whole frame is painted in the theme accent colour and the rendered
@@ -9481,9 +9536,10 @@ var SidebarContainer = class extends Container {
9481
9536
  * only rebuilt when the visible panel set changes. */
9482
9537
  bodies = /* @__PURE__ */ new Map();
9483
9538
  builtStackKey = "";
9484
- /** 1s heartbeat while the sidebar is open: keeps live values (uptime,
9485
- * goal wall-clock, gradient phase of busy agent slots) ticking without
9486
- * needing an event. Stopped when the sidebar closes. */
9539
+ /** 1s heartbeat for the whole container lifetime: keeps live values (goal
9540
+ * wall-clock, gradient phase of busy agent slots) ticking while the sidebar
9541
+ * is open — the tick is a no-op when it is closed, and only `dispose()`
9542
+ * (TUI teardown) clears the interval. */
9487
9543
  heartbeat;
9488
9544
  constructor(manager, requestRender, colors) {
9489
9545
  super();
@@ -9930,6 +9986,20 @@ function extractBracketedImagePastePaths(data) {
9930
9986
  if (paths.length === 0 || segmentStart !== pasted.length) return void 0;
9931
9987
  return paths;
9932
9988
  }
9989
+ /**
9990
+ * True only when bracketed-paste markers span the entire input and the
9991
+ * body is strictly empty — Cmd+V with an image-only clipboard (macOS
9992
+ * wraps nothing). Whitespace-only pastes and coalesced keystrokes around
9993
+ * the marker pair are not image pastes.
9994
+ */
9995
+ function isCompleteEmptyBracketedPaste(data) {
9996
+ if (!data.includes(BRACKET_PASTE_START)) return false;
9997
+ const startIndex = data.indexOf(BRACKET_PASTE_START);
9998
+ const endIndex = data.indexOf(BRACKET_PASTE_END, startIndex + 6);
9999
+ if (endIndex === -1) return false;
10000
+ if (startIndex !== 0 || endIndex + 6 !== data.length) return false;
10001
+ return data.slice(startIndex + 6, endIndex).length === 0;
10002
+ }
9933
10003
  const KITTY_CSI_U = /^\[(\d+);(\d+)((?::\d+)*)u$/;
9934
10004
  const CAPS_LOCK_BIT = 64;
9935
10005
  const CTRL_BIT = 4;
@@ -10142,6 +10212,11 @@ var CustomEditor = class extends Editor {
10142
10212
  if (!normalized.includes(BRACKET_PASTE_END)) this.consumingPaste = true;
10143
10213
  return;
10144
10214
  }
10215
+ if (this.onPasteImage !== void 0 && isCompleteEmptyBracketedPaste(normalized)) {
10216
+ const handler = this.onPasteImage;
10217
+ handler();
10218
+ return;
10219
+ }
10145
10220
  if (matchesKey(normalized, process.platform === "win32" ? "alt+v" : Key.ctrl("v"))) {
10146
10221
  if (this.expandPasteMarkerAtCursor()) return;
10147
10222
  if (this.onPasteImage !== void 0) {
@@ -11150,6 +11225,7 @@ var SessionManager$1 = class {
11150
11225
  await this.unloadCurrentSession("switching session")?.close({ extractMemories: false });
11151
11226
  this.host.session = session;
11152
11227
  this.registerSessionHandlers(session);
11228
+ await syncDiagramCapabilityPrompt(session);
11153
11229
  }
11154
11230
  async syncRuntimeState(session = this.requireSession()) {
11155
11231
  const status = await session.getStatus();
@@ -14056,7 +14132,7 @@ var ScreamTUI = class {
14056
14132
  const card = new OnboardingCardComponent({
14057
14133
  colors: this.state.theme.colors,
14058
14134
  onConfigure: () => {
14059
- import("./dispatch-dDbUBD7J.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14135
+ import("./dispatch-Du8ddILK.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14060
14136
  this.restoreEditor();
14061
14137
  onDone?.();
14062
14138
  });
@@ -16339,13 +16415,16 @@ async function ensureSessionsDir(homeDir) {
16339
16415
  const dir = getSessionsDir(homeDir);
16340
16416
  if (!existsSync(dir)) await mkdir(dir, { recursive: true });
16341
16417
  }
16342
- async function appendJournalLine(homeDir, sessionId, line) {
16418
+ async function appendJournalLine(homeDir, sessionId, line, throwOnError = false) {
16419
+ let writeError;
16343
16420
  await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
16421
+ writeError = error;
16344
16422
  log.warn("web: failed to persist journal line", {
16345
16423
  sessionId,
16346
16424
  error: String(error)
16347
16425
  });
16348
16426
  });
16427
+ if (throwOnError && writeError !== void 0) throw writeError;
16349
16428
  }
16350
16429
  let metaWriteSeq = 0;
16351
16430
  async function saveMetadata(homeDir, meta) {
@@ -16430,16 +16509,22 @@ function isMessageBearingEntry(entry) {
16430
16509
  */
16431
16510
  function contextHistoryToChatMessages(history) {
16432
16511
  const out = [];
16512
+ let pendingAnchor = "other";
16433
16513
  for (const message of history) {
16434
16514
  if (message.role !== "user" && message.role !== "assistant") continue;
16435
16515
  if (isInternalMessage(message)) continue;
16436
- if (message.origin !== void 0 && message.origin.kind !== "user") continue;
16516
+ const anchor = classifyUndoAnchor(message.origin);
16517
+ if (message.role === "user") {
16518
+ pendingAnchor = anchor;
16519
+ if (anchor === "skill" || anchor === "other") continue;
16520
+ }
16437
16521
  const text = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
16438
16522
  if (text.length === 0) continue;
16439
16523
  out.push({
16440
16524
  role: message.role,
16441
16525
  content: text,
16442
- tools: []
16526
+ tools: [],
16527
+ undoAnchor: message.role === "assistant" ? pendingAnchor : anchor
16443
16528
  });
16444
16529
  }
16445
16530
  return out;
@@ -16469,6 +16554,65 @@ function deriveTitle(messages) {
16469
16554
  const text = firstUser.content.trim();
16470
16555
  return text.length > 40 ? text.slice(0, 40) + "..." : text;
16471
16556
  }
16557
+ /**
16558
+ * Remove complete user-turn blocks from a rendered transcript. The core
16559
+ * context owns the authoritative undo operation; the web journal is append
16560
+ * only, so a durable undo marker asks this projection to hide the same turns
16561
+ * when snapshots are rebuilt (including after a restart).
16562
+ *
16563
+ * `beforeSeq` is an inclusive cursor captured before the core mutation. It
16564
+ * also covers a user message that reserved the next sequence but never
16565
+ * emitted a turn event.
16566
+ */
16567
+ function removeLastTurnsBefore(messages, count, beforeSeq) {
16568
+ if (count <= 0 || messages.length === 0) return;
16569
+ const firstFutureIndex = messages.findIndex((message) => typeof message.seq === "number" && message.seq > beforeSeq);
16570
+ const prefixLength = firstFutureIndex === -1 ? messages.length : firstFutureIndex;
16571
+ const prefix = messages.slice(0, prefixLength);
16572
+ const tail = messages.slice(prefixLength);
16573
+ let remaining = count;
16574
+ while (remaining > 0) {
16575
+ let anchor = -1;
16576
+ for (let i = prefix.length - 1; i >= 0; i -= 1) {
16577
+ const message = prefix[i];
16578
+ if (message?.role === "assistant" && message.undoAnchor === "skill") {
16579
+ anchor = i;
16580
+ while (anchor > 0) {
16581
+ const previous = prefix[anchor - 1];
16582
+ if (previous?.role !== "assistant" || previous.undoAnchor !== "skill") break;
16583
+ anchor -= 1;
16584
+ }
16585
+ break;
16586
+ }
16587
+ if (message?.role === "user") {
16588
+ anchor = i;
16589
+ break;
16590
+ }
16591
+ }
16592
+ if (anchor < 0) break;
16593
+ prefix.splice(anchor);
16594
+ remaining -= 1;
16595
+ }
16596
+ messages.splice(0, messages.length, ...prefix, ...tail);
16597
+ }
16598
+ function classifyUndoAnchor(origin) {
16599
+ if (origin === void 0 || origin === null) return "user";
16600
+ if (typeof origin !== "object") return "other";
16601
+ const kind = origin.kind;
16602
+ if (kind === "user") return "user";
16603
+ if (kind === "skill_activation" && origin.trigger === "user-slash") return "skill";
16604
+ return "other";
16605
+ }
16606
+ /** Match the core `/undo` anchor rule without importing agent-core internals. */
16607
+ function countUndoAnchors(history) {
16608
+ let count = 0;
16609
+ for (const message of history) {
16610
+ if (message.role !== "user") continue;
16611
+ const origin = message.origin;
16612
+ if (origin === void 0 || origin.kind === "user" || origin.kind === "skill_activation" && origin.trigger === "user-slash") count += 1;
16613
+ }
16614
+ return count;
16615
+ }
16472
16616
  const execFileAsync = promisify(execFile);
16473
16617
  /** Raw porcelain rows: repo-root-relative path + XY status. `getGitStatus`
16474
16618
  * enriches these into full `GitFileChange` records. */
@@ -16731,6 +16875,8 @@ var WebSession = class {
16731
16875
  heartbeatTimer = null;
16732
16876
  unsubscribe = null;
16733
16877
  busy = false;
16878
+ /** Prevent two concurrent REST `/undo` calls from mutating core history together. */
16879
+ undoInFlight = false;
16734
16880
  /**
16735
16881
  * In-flight turn count across all agents (main + subagents). Used by the
16736
16882
  * idle-exit watchdog: a single boolean is not enough because a subagent's
@@ -16819,13 +16965,13 @@ var WebSession = class {
16819
16965
  });
16820
16966
  return entries;
16821
16967
  }
16822
- async persistEntry(entry) {
16968
+ async persistEntry(entry, throwOnError = false) {
16823
16969
  if (!this.homeDir) return;
16824
16970
  const line = JSON.stringify({
16825
16971
  type: "journal",
16826
16972
  ...entry
16827
16973
  });
16828
- await appendJournalLine(this.homeDir, this.sessionId, line);
16974
+ await appendJournalLine(this.homeDir, this.sessionId, line, throwOnError);
16829
16975
  }
16830
16976
  async persistUserMessage(text, beforeSeq, clientMessageId) {
16831
16977
  this.userMessages.push({
@@ -16882,6 +17028,9 @@ var WebSession = class {
16882
17028
  * journal holds no message records.
16883
17029
  */
16884
17030
  seededMessages = [];
17031
+ /** Undo markers already reflected by a core-history seed must not be applied twice. */
17032
+ seedCoveredUndoMarkers = /* @__PURE__ */ new Set();
17033
+ historySeeded = false;
16885
17034
  /**
16886
17035
  * Mark the journal untrustworthy (byte-interleaving damage, see
16887
17036
  * journal-writer.ts).
@@ -16923,7 +17072,10 @@ var WebSession = class {
16923
17072
  * monotonic with live events and consistent with the getMessagesOlder cursor.
16924
17073
  */
16925
17074
  seedHistory(messages) {
16926
- if (messages.length === 0 || this.seededMessages.length > 0) return;
17075
+ if (this.historySeeded) return;
17076
+ this.historySeeded = true;
17077
+ for (const entry of this.journal) if (entry.payload.type === "web.history.undone") this.seedCoveredUndoMarkers.add(entry.seq);
17078
+ if (messages.length === 0) return;
16927
17079
  let seq = 1;
16928
17080
  for (const message of messages) {
16929
17081
  this.seededMessages.push({
@@ -16970,7 +17122,7 @@ var WebSession = class {
16970
17122
  * clients. Used for server-owned reconstruction material (finalized
16971
17123
  * assistant snapshots) that clients never consume as live events.
16972
17124
  */
16973
- appendDurableSilent(payload) {
17125
+ appendDurableSilent(payload, throwOnPersistenceError = false) {
16974
17126
  const entry = {
16975
17127
  seq: this.nextSeq++,
16976
17128
  epoch: this.epoch,
@@ -16978,8 +17130,12 @@ var WebSession = class {
16978
17130
  payload
16979
17131
  };
16980
17132
  this.journal.push(entry);
16981
- this.trackPending(this.persistEntry(entry));
16982
- return entry;
17133
+ const persisted = this.persistEntry(entry, throwOnPersistenceError);
17134
+ this.trackPending(persisted);
17135
+ return {
17136
+ entry,
17137
+ persisted
17138
+ };
16983
17139
  }
16984
17140
  subscribeEvents() {
16985
17141
  if (!this.session) return;
@@ -17308,13 +17464,14 @@ var WebSession = class {
17308
17464
  });
17309
17465
  }
17310
17466
  async handleCommand(ws, command, args, pendingMsgId) {
17311
- const ok = (message) => {
17467
+ const ok = (message, extra) => {
17312
17468
  this.broadcast({
17313
17469
  type: "command_result",
17314
17470
  command,
17315
17471
  ok: true,
17316
17472
  message,
17317
- pendingMsgId
17473
+ pendingMsgId,
17474
+ ...extra
17318
17475
  }, false);
17319
17476
  };
17320
17477
  const fail = (message) => {
@@ -17350,9 +17507,10 @@ var WebSession = class {
17350
17507
  }
17351
17508
  return;
17352
17509
  case "auto":
17510
+ case "ask":
17353
17511
  case "yes":
17354
17512
  case "bot": {
17355
- const mode = command === "auto" ? "auto" : command === "bot" ? "bot" : "yolo";
17513
+ const mode = command === "auto" ? "auto" : command === "ask" ? "ask" : command === "bot" ? "bot" : "yolo";
17356
17514
  try {
17357
17515
  await this.session.setPermission(mode);
17358
17516
  this.permission = mode;
@@ -17395,7 +17553,7 @@ var WebSession = class {
17395
17553
  fail("Fork 失败:无法复制当前会话。");
17396
17554
  return;
17397
17555
  }
17398
- ok(`会话已 fork,新会话 ID:${result.sessionId}`);
17556
+ ok(`会话已 fork,新会话 ID:${result.sessionId}`, { sessionId: result.sessionId });
17399
17557
  } catch (error) {
17400
17558
  fail(`Fork 失败:${errMsg(error)}`);
17401
17559
  }
@@ -17506,6 +17664,7 @@ var WebSession = class {
17506
17664
  planMode: false,
17507
17665
  wolfpackMode: false,
17508
17666
  rlmEnabled: false,
17667
+ rlmMaxDepth: null,
17509
17668
  contextTokens: 0,
17510
17669
  maxContextTokens: 0,
17511
17670
  contextUsage: 0
@@ -17682,6 +17841,64 @@ var WebSession = class {
17682
17841
  if (!this.session) throw new HttpError(409, "Session is archived (read-only)", ErrorCodes.SESSION_CLOSED);
17683
17842
  return this.session;
17684
17843
  }
17844
+ /**
17845
+ * Undo user turns in the core session and append a durable web projection
17846
+ * marker. The core RPC deliberately has no UI knowledge, so without the
17847
+ * marker the old append-only web journal would resurrect the removed turns
17848
+ * on the next snapshot or after a restart.
17849
+ */
17850
+ async undoHistory(count) {
17851
+ if (!Number.isSafeInteger(count) || count < 1) throw new HttpError(400, "Undo count must be a positive integer.");
17852
+ if (this.busy) throw new HttpError(409, "Session is busy", ErrorCodes.TURN_AGENT_BUSY);
17853
+ const session = this.requireLiveSession();
17854
+ if (this.undoInFlight) throw new HttpError(409, "Undo already in progress");
17855
+ this.undoInFlight = true;
17856
+ try {
17857
+ const beforeSeq = this.nextSeq;
17858
+ let beforeAnchors = null;
17859
+ try {
17860
+ beforeAnchors = countUndoAnchors((await session.getContext()).history);
17861
+ } catch (error) {
17862
+ log.warn("web: failed to read context before undo", {
17863
+ sessionId: this.sessionId,
17864
+ error: String(error)
17865
+ });
17866
+ }
17867
+ await session.undoHistory(count);
17868
+ let removed = count;
17869
+ if (beforeAnchors !== null) try {
17870
+ const afterAnchors = countUndoAnchors((await session.getContext()).history);
17871
+ removed = Math.max(0, beforeAnchors - afterAnchors);
17872
+ } catch (error) {
17873
+ log.warn("web: failed to read context after undo", {
17874
+ sessionId: this.sessionId,
17875
+ error: String(error)
17876
+ });
17877
+ }
17878
+ if (removed <= 0) {
17879
+ await this.refreshStatus();
17880
+ this.broadcastStatus();
17881
+ return;
17882
+ }
17883
+ const { entry, persisted } = this.appendDurableSilent({
17884
+ type: "web.history.undone",
17885
+ count: removed,
17886
+ beforeSeq
17887
+ }, true);
17888
+ await persisted;
17889
+ this.broadcast({
17890
+ type: "event",
17891
+ seq: entry.seq,
17892
+ epoch: entry.epoch,
17893
+ payload: entry.payload
17894
+ }, false);
17895
+ this.trackPending(this.updateTitle());
17896
+ await this.refreshStatus();
17897
+ this.broadcastStatus();
17898
+ } finally {
17899
+ this.undoInFlight = false;
17900
+ }
17901
+ }
17685
17902
  requireGoalSession(allowBusy = false) {
17686
17903
  if (!this.session) throw new HttpError(409, "Session is archived (read-only)", ErrorCodes.SESSION_CLOSED);
17687
17904
  if (!allowBusy && this.busy) throw new HttpError(409, "Session is busy", ErrorCodes.TURN_AGENT_BUSY);
@@ -17759,6 +17976,7 @@ var WebSession = class {
17759
17976
  role: "assistant",
17760
17977
  content: "",
17761
17978
  tools: [],
17979
+ undoAnchor: classifyUndoAnchor(event.origin),
17762
17980
  seq: entry.seq,
17763
17981
  model: this.cachedStatus?.model,
17764
17982
  turnStats: {
@@ -17812,13 +18030,20 @@ var WebSession = class {
17812
18030
  break;
17813
18031
  case "web.message.finalized": {
17814
18032
  const finalized = event.message;
17815
- finalized.seq = messages.length > 0 && messages.at(-1)?.role === "assistant" ? messages.at(-1).seq : entry.seq;
18033
+ const lastSkeletonSeq = messages.length > 0 && messages.at(-1)?.role === "assistant" ? messages.at(-1).seq : entry.seq;
18034
+ if (finalized.undoAnchor === void 0 && messages.at(-1)?.role === "assistant") finalized.undoAnchor = messages.at(-1).undoAnchor;
18035
+ finalized.seq = lastSkeletonSeq;
17816
18036
  if (messages.length > 0 && messages.at(-1)?.role === "assistant") messages[messages.length - 1] = finalized;
17817
18037
  else messages.push(finalized);
17818
18038
  currentAssistant = null;
17819
18039
  turnFinalized = true;
17820
18040
  break;
17821
18041
  }
18042
+ case "web.history.undone":
18043
+ if (!this.seedCoveredUndoMarkers.has(entry.seq)) removeLastTurnsBefore(messages, event.count, event.beforeSeq);
18044
+ currentAssistant = null;
18045
+ turnFinalized = false;
18046
+ break;
17822
18047
  case "turn.ended":
17823
18048
  if (event.reason === "failed" && currentAssistant) currentAssistant.isError = true;
17824
18049
  if (currentAssistant && !turnFinalized && currentAssistant.content === "") currentAssistant.degraded = true;
@@ -17830,7 +18055,10 @@ var WebSession = class {
17830
18055
  }
17831
18056
  for (let i = userIndex; i < this.userMessages.length; i++) {
17832
18057
  const item = this.userMessages[i];
17833
- if (item !== void 0) messages.push(item.msg);
18058
+ if (item !== void 0) messages.push({
18059
+ ...item.msg,
18060
+ seq: item.beforeSeq
18061
+ });
17834
18062
  }
17835
18063
  return messages;
17836
18064
  }
@@ -18427,7 +18655,9 @@ var SessionManager = class {
18427
18655
  await ws.switchRlm(enabled, maxDepth);
18428
18656
  }
18429
18657
  async undoHistory(sessionId, count) {
18430
- await this.getLiveSession(sessionId).undoHistory(count);
18658
+ const ws = this.sessions.get(sessionId);
18659
+ if (!ws) throw new HttpError(404, "Session not found", ErrorCodes.SESSION_NOT_FOUND);
18660
+ await ws.undoHistory(count);
18431
18661
  }
18432
18662
  async compact(sessionId, instruction) {
18433
18663
  await this.getLiveSession(sessionId).compact(instruction ? { instruction } : {});
@@ -18597,16 +18827,32 @@ async function handleSessionDataRoutes(req, res, url, method, resolveSession, wo
18597
18827
  }
18598
18828
  return true;
18599
18829
  }
18600
- if (url === `${API_PREFIX}/git/status` && method === "GET") {
18601
- const gs = await getGitStatus(workDir);
18602
- res.writeHead(200, { "Content-Type": "application/json" });
18603
- res.end(JSON.stringify(gs));
18604
- return true;
18605
- }
18606
- if (url.startsWith(`${API_PREFIX}/git/diff?`) && method === "GET") {
18607
- const relPath = new URLSearchParams(url.split("?")[1] ?? "").get("path") ?? "";
18830
+ const gitPath = url.split("?")[0];
18831
+ const isGitStatusRoute = gitPath === `${API_PREFIX}/git/status` && method === "GET";
18832
+ if (isGitStatusRoute || gitPath === `${API_PREFIX}/git/diff` && method === "GET") {
18833
+ const gitQuery = new URLSearchParams(url.split("?")[1] ?? "");
18834
+ const requestedGitSessionId = gitQuery.get("sessionId");
18835
+ let gitWorkDir = workDir;
18836
+ if (requestedGitSessionId !== null) {
18837
+ const gitSession = resolveSession(requestedGitSessionId);
18838
+ if (!gitSession) {
18839
+ sendJson(res, 404, {
18840
+ code: ErrorCodes.SESSION_NOT_FOUND,
18841
+ message: "Session not found"
18842
+ });
18843
+ return true;
18844
+ }
18845
+ gitWorkDir = gitSession.workDir;
18846
+ }
18847
+ if (isGitStatusRoute) {
18848
+ const gs = await getGitStatus(gitWorkDir);
18849
+ res.writeHead(200, { "Content-Type": "application/json" });
18850
+ res.end(JSON.stringify(gs));
18851
+ return true;
18852
+ }
18853
+ const relPath = gitQuery.get("path") ?? "";
18608
18854
  try {
18609
- const result = await getGitFileDiff(workDir, relPath);
18855
+ const result = await getGitFileDiff(gitWorkDir, relPath);
18610
18856
  res.writeHead(200, { "Content-Type": "application/json" });
18611
18857
  res.end(JSON.stringify({
18612
18858
  path: relPath,
@@ -18817,6 +19063,7 @@ async function handleSessionControlRoutes(req, res, url, method, manager) {
18817
19063
  return false;
18818
19064
  }
18819
19065
  async function handleResourceRoutes(req, res, url, method, manager) {
19066
+ const routePath = url.split("?")[0] ?? url;
18820
19067
  const skillsMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/skills$`).exec(url);
18821
19068
  if (skillsMatch && method === "GET") {
18822
19069
  try {
@@ -19004,7 +19251,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
19004
19251
  }
19005
19252
  return true;
19006
19253
  }
19007
- const tasksMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks$`).exec(url);
19254
+ const tasksMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks$`).exec(routePath);
19008
19255
  if (tasksMatch && method === "GET") {
19009
19256
  try {
19010
19257
  const query = new URLSearchParams(url.split("?")[1] ?? "");
@@ -19016,7 +19263,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
19016
19263
  }
19017
19264
  return true;
19018
19265
  }
19019
- const taskOutputMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/output$`).exec(url);
19266
+ const taskOutputMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/output$`).exec(routePath);
19020
19267
  if (taskOutputMatch && method === "GET") {
19021
19268
  try {
19022
19269
  const query = new URLSearchParams(url.split("?")[1] ?? "");
@@ -19027,7 +19274,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
19027
19274
  }
19028
19275
  return true;
19029
19276
  }
19030
- const taskStopMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/stop$`).exec(url);
19277
+ const taskStopMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/stop$`).exec(routePath);
19031
19278
  if (taskStopMatch && method === "POST") {
19032
19279
  try {
19033
19280
  const body = await readJsonBody(req);