scream-code 0.16.1 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +1 -1
  2. package/dist/{app-lSa6fM8z.mjs → app-C1foLjDE.mjs} +582 -532
  3. package/dist/{dispatch-B6jbxY0D.mjs → dispatch-BGLqpKHG.mjs} +1 -1
  4. package/dist/{dispatch-DqeJdQZQ.mjs → dispatch-ChDxIz0h.mjs} +1721 -593
  5. package/dist/main.mjs +1 -1
  6. package/dist/public/assets/astro-DAp21e1Q.js +2 -0
  7. package/dist/public/assets/astro-DAp21e1Q.js.map +1 -0
  8. package/dist/public/assets/bash-Yzrsuije.js +2 -0
  9. package/dist/public/assets/bash-Yzrsuije.js.map +1 -0
  10. package/dist/public/assets/c-BIGW1oBm.js +2 -0
  11. package/dist/public/assets/c-BIGW1oBm.js.map +1 -0
  12. package/dist/public/assets/cpp-BRuaLJcg.js +2 -0
  13. package/dist/public/assets/cpp-BRuaLJcg.js.map +1 -0
  14. package/dist/public/assets/csharp-COcwbKMJ.js +2 -0
  15. package/dist/public/assets/csharp-COcwbKMJ.js.map +1 -0
  16. package/dist/public/assets/css-DEUVOdPa.js +2 -0
  17. package/dist/public/assets/css-DEUVOdPa.js.map +1 -0
  18. package/dist/public/assets/dart-CF10PKvl.js +2 -0
  19. package/dist/public/assets/dart-CF10PKvl.js.map +1 -0
  20. package/dist/public/assets/docker-BcOcwvcX.js +2 -0
  21. package/dist/public/assets/docker-BcOcwvcX.js.map +1 -0
  22. package/dist/public/assets/go-CxLEBnE3.js +2 -0
  23. package/dist/public/assets/go-CxLEBnE3.js.map +1 -0
  24. package/dist/public/assets/graphql-ChdNCCLP.js +2 -0
  25. package/dist/public/assets/graphql-ChdNCCLP.js.map +1 -0
  26. package/dist/public/assets/html-CNxCFRDw.js +2 -0
  27. package/dist/public/assets/html-CNxCFRDw.js.map +1 -0
  28. package/dist/public/assets/index-DQBhBKK1.css +1 -0
  29. package/dist/public/assets/index-DakPeNBt.js +240 -0
  30. package/dist/public/assets/index-DakPeNBt.js.map +1 -0
  31. package/dist/public/assets/ini-BEwlwnbL.js +2 -0
  32. package/dist/public/assets/ini-BEwlwnbL.js.map +1 -0
  33. package/dist/public/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  34. package/dist/public/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  35. package/dist/public/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  36. package/dist/public/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  37. package/dist/public/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  38. package/dist/public/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  39. package/dist/public/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  40. package/dist/public/assets/java-CylS5w8V.js +2 -0
  41. package/dist/public/assets/java-CylS5w8V.js.map +1 -0
  42. package/dist/public/assets/javascript-wDzz0qaB.js +2 -0
  43. package/dist/public/assets/javascript-wDzz0qaB.js.map +1 -0
  44. package/dist/public/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  45. package/dist/public/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  46. package/dist/public/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  47. package/dist/public/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  48. package/dist/public/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  49. package/dist/public/assets/json-Cp-IABpG.js +2 -0
  50. package/dist/public/assets/json-Cp-IABpG.js.map +1 -0
  51. package/dist/public/assets/jsx-g9-lgVsj.js +2 -0
  52. package/dist/public/assets/jsx-g9-lgVsj.js.map +1 -0
  53. package/dist/public/assets/kotlin-BdnUsdx6.js +2 -0
  54. package/dist/public/assets/kotlin-BdnUsdx6.js.map +1 -0
  55. package/dist/public/assets/lua-BaeVxFsk.js +2 -0
  56. package/dist/public/assets/lua-BaeVxFsk.js.map +1 -0
  57. package/dist/public/assets/make-CHLpvVh8.js +2 -0
  58. package/dist/public/assets/make-CHLpvVh8.js.map +1 -0
  59. package/dist/public/assets/markdown-Cvjx9yec.js +2 -0
  60. package/dist/public/assets/markdown-Cvjx9yec.js.map +1 -0
  61. package/dist/public/assets/perl-CptaX86O.js +2 -0
  62. package/dist/public/assets/perl-CptaX86O.js.map +1 -0
  63. package/dist/public/assets/postcss-DnzrXTVv.js +2 -0
  64. package/dist/public/assets/postcss-DnzrXTVv.js.map +1 -0
  65. package/dist/public/assets/protobuf-C7zT0LnQ.js +2 -0
  66. package/dist/public/assets/protobuf-C7zT0LnQ.js.map +1 -0
  67. package/dist/public/assets/python-B6aJPvgy.js +2 -0
  68. package/dist/public/assets/python-B6aJPvgy.js.map +1 -0
  69. package/dist/public/assets/r-Dspwwk_N.js +2 -0
  70. package/dist/public/assets/r-Dspwwk_N.js.map +1 -0
  71. package/dist/public/assets/ruby-CcrC3V9I.js +2 -0
  72. package/dist/public/assets/ruby-CcrC3V9I.js.map +1 -0
  73. package/dist/public/assets/rust-B1yitclQ.js +2 -0
  74. package/dist/public/assets/rust-B1yitclQ.js.map +1 -0
  75. package/dist/public/assets/scala-C151Ov-r.js +2 -0
  76. package/dist/public/assets/scala-C151Ov-r.js.map +1 -0
  77. package/dist/public/assets/sql-BLtJtn59.js +2 -0
  78. package/dist/public/assets/sql-BLtJtn59.js.map +1 -0
  79. package/dist/public/assets/svelte-U4fHntn6.js +2 -0
  80. package/dist/public/assets/svelte-U4fHntn6.js.map +1 -0
  81. package/dist/public/assets/swift-D82vCrfD.js +2 -0
  82. package/dist/public/assets/swift-D82vCrfD.js.map +1 -0
  83. package/dist/public/assets/toml-vGWfd6FD.js +2 -0
  84. package/dist/public/assets/toml-vGWfd6FD.js.map +1 -0
  85. package/dist/public/assets/tsx-COt5Ahok.js +2 -0
  86. package/dist/public/assets/tsx-COt5Ahok.js.map +1 -0
  87. package/dist/public/assets/typescript-BPQ3VLAy.js +2 -0
  88. package/dist/public/assets/typescript-BPQ3VLAy.js.map +1 -0
  89. package/dist/public/assets/xml-sdJ4AIDG.js +2 -0
  90. package/dist/public/assets/xml-sdJ4AIDG.js.map +1 -0
  91. package/dist/public/assets/yaml-Buea-lGh.js +2 -0
  92. package/dist/public/assets/yaml-Buea-lGh.js.map +1 -0
  93. package/dist/public/index.html +2 -2
  94. package/dist/{text-input-dialog-rSkH3JTV.mjs → text-input-dialog-DAQfJdHu.mjs} +1 -1
  95. package/dist/{text-input-dialog-BABWY9vy.mjs → text-input-dialog-GajpxjyL.mjs} +66 -20
  96. package/package.json +5 -4
  97. package/dist/public/assets/index-DwX1jGaf.js +0 -96
  98. package/dist/public/assets/index-DwX1jGaf.js.map +0 -1
  99. package/dist/public/assets/index-zIKfLBMX.css +0 -1
  100. package/dist/public/assets/wasm-CG6Dc4jp.js +0 -2
  101. package/dist/public/assets/wasm-CG6Dc4jp.js.map +0 -1
  102. package/icon.ico +0 -0
@@ -4,14 +4,14 @@ 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 isOrphanedToolCallError, $t as appendStreamingArgsPreview, A as renderDiffLinesClustered, An as saveTuiConfig, At as DISABLE_TERMINAL_THEME_REPORTING, B as BackgroundAgentStatusComponent, Bn as DEFAULT_CATALOG_URL, Bt as TERMINAL_THEME_DARK, Cn as isEmbeddingModelCached, Ct as createMarkdownTheme, D as toggleEmptySessionHint, Dn as TuiConfigParseError, Dt as detectTerminalTheme, E as isTurnElapsedEnabled, En as PULSE_WAVE_FRAMES, Et as getColorPalette, F as estimateTokens, Fn as detectInstallSource, Ft as OSC11_RESPONSE_PREFIX, G as getBreathingFrame, Gn as appendSessionIndexEntry, Gt as lerpGradient, H as AgentGroupComponent, Hn as saveCatalogCache, Ht as isBusy, I as getSharedSpeedTracker, In as CLI_COMMAND_NAME, It as OSC11_RESPONSE_PREFIX_NO_ESC, J as refineGoal, Jn as MemoryMemoStore, K as resetBreathingClock, L as SkillActivationComponent, Ln as CLI_UI_MODE, Lt as QUERY_TERMINAL_THEME, M as langFromPath, Mn as getDataDir, Mt as ENABLE_TERMINAL_THEME_REPORTING, N as CachedContainer, Nn as getInputHistoryFile, Nt as OSC11_QUERY, O as ToolCallComponent, On as TuiLikePreferencesSchema, Ot as parseOsc11BackgroundTheme, P as ThinkingComponent, Pn as getLogDir, Pt as OSC11_RESPONSE, Qn as isScreamError, Qt as STATUS_BULLET, R as ReadGroupComponent, Rn as CLI_USER_AGENT_PRODUCT, Rt as TERMINAL_FOCUS_IN, S as getDaemonInstructions, Sn as getKnowledgeStore, St as createEditorTheme, T as isEmptySessionHintDismissed, Tn as PIXEL_PULSE_FRAMES, Tt as contrastTextHex, U as WelcomeComponent, Un as ScreamHarness, Ut as isStreaming, V as AssistantMessageComponent, Vn as fetchCatalog, Vt as TERMINAL_THEME_LIGHT, W as BREATHE_CYCLE_MS, Wn as encodeWorkDirKey, Wt as FooterComponent, Xn as log, Xt as printableChar, Yn as flushDiagnosticLogs, Zn as resolveGlobalLogPath, Zt as INTERJECTION_BULLET, _ as refreshUpdateCache, _n as buildSkillSlashCommands, _t as supportsBalance, an as stringValue, b as readJsonlFile, bn as isExperimentalFlagEnabled, c as hasDispose, cn as EMPTY_SESSION_HINT_URL, d as formatMemoryMemoForInjection, dn as SESSION_TIPS, en as argsRecord, er as ErrorCodes, et as toTerminalHyperlink, fn as TIP_ROTATION_INTERVAL_MS, g as selectUpdateTarget, gn as getNoActiveSessionMessage, gt as refreshProviderBalance, hn as getLlmNotSetMessage, i as buildRoleAdditionalText, in as serializeToolResultOutput, j as highlightLines, jn as detectShellEnvironment, jt as ENABLE_TERMINAL_FOCUS_REPORTING, k as renderDiffLines, kn as loadTuiConfig, kt as DISABLE_TERMINAL_FOCUS_REPORTING, l as isPlanExpandable, ln as EXIT_CONFIRM_WINDOW_MS, lt as handlePlanCommand, mn as getCtrlDHint, n as clearEvalPanelState, nn as isTodoItemShape, nt as getModelCycleLevel, on as truncateErrorMessage, pn as getCtrlCHint, q as clearGoalState, qn as resolveScreamHome, r as openUrl, rn as parseStreamingArgs, s as disposeChildren, sn as CHARS_PER_TOKEN, st as handleFusionPlanCommand, t as dispatchInput, tn as formatErrorMessage, tr as SCREAM_ERROR_INFO, tt as changeThinkingLevel, u as MoonLoader, un as MAIN_AGENT_ID, v as readUpdateCache, vn as BUILTIN_SLASH_COMMANDS, vt as clearInfoPanelState, w as UserMessageComponent, wn as startManualEmbeddingDownload, wt as createThemeStyles, xn as setExperimentalFlags, xt as resolveThemeSync, y as appendJsonlLine, yn as sortSlashCommands, z as parseReadGroupOutput, zn as PRODUCT_NAME, zt as TERMINAL_FOCUS_OUT } from "./dispatch-DqeJdQZQ.mjs";
8
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-BABWY9vy.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-ChDxIz0h.mjs";
8
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-GajpxjyL.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
11
- import { access, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
11
+ import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
12
12
  import { Writable } from "node:stream";
13
13
  import { WebSocket, WebSocketServer } from "ws";
14
- import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
14
+ import { appendFileSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
15
15
  import * as path$1 from "node:path";
16
16
  import path, { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
17
17
  import { z } from "zod";
@@ -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.1"),
35
+ version: optionalBuildString("0.16.3"),
36
36
  channel: optionalBuildString(""),
37
37
  commit: optionalBuildString(""),
38
38
  buildTarget: optionalBuildString("darwin-arm64")
@@ -1968,42 +1968,6 @@ var EditorKeyboardController = class {
1968
1968
  }
1969
1969
  };
1970
1970
  //#endregion
1971
- //#region src/tui/utils/sanitize.ts
1972
- /** Replace tabs with spaces, keeping column alignment. Default tab width 4. */
1973
- function replaceTabs(text, tabWidth = 4) {
1974
- return text.replaceAll(" ", " ".repeat(tabWidth));
1975
- }
1976
- /**
1977
- * Strip terminal control characters from untrusted shell output before it is
1978
- * rendered inside a framed pane. Carriage returns (progress-bar redraws) and
1979
- * ANSI escape sequences (cursor moves, screen clears, SGR color codes, OSC
1980
- * title/OSC 52 clipboard sequences, charset selection) would otherwise disrupt
1981
- * the terminal borders or let task output inject escape sequences. All
1982
- * printable text is preserved.
1983
- */
1984
- function sanitizeShellOutput(output) {
1985
- return output.replaceAll("\r", "").replaceAll(/\u001B\[[0-?]*[ -/]*[@-~]/g, "").replaceAll(/\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)/g, "").replaceAll(/\u001B[P^_][^\u0007\u001B]*(?:\u0007|\u001B\\)/g, "").replaceAll(/\u001B[()][0-9A-Za-z]/g, "").replaceAll(/\u001B[=>78DEFHMc]/g, "");
1986
- }
1987
- //#endregion
1988
- //#region src/tui/components/messages/status-message.ts
1989
- var StatusMessageComponent = class extends Container {
1990
- constructor(content, colors, color) {
1991
- super();
1992
- const sanitized = replaceTabs(content);
1993
- const text = color === void 0 ? chalk.hex(colors.textDim)(sanitized) : chalk.hex(color)(sanitized);
1994
- this.addChild(new Text(` ${text}`, 0, 0));
1995
- }
1996
- };
1997
- var NoticeMessageComponent = class extends Container {
1998
- constructor(title, detail, colors, markerColor) {
1999
- super();
2000
- const marker = markerColor === void 0 ? " " : chalk.hex(markerColor)(INTERJECTION_BULLET);
2001
- this.addChild(new Spacer(1));
2002
- this.addChild(new Text(`${marker}${chalk.hex(colors.textStrong)(replaceTabs(title))}`, 0, 0));
2003
- if (detail !== void 0 && detail.length > 0) this.addChild(new Text(` ${chalk.hex(colors.textDim)(replaceTabs(detail))}`, 0, 0));
2004
- }
2005
- };
2006
- //#endregion
2007
1971
  //#region src/tui/utils/background-agent-status.ts
2008
1972
  const MAX_BACKGROUND_FIELD_LENGTH = 240;
2009
1973
  function normalizeBackgroundField(value) {
@@ -2183,233 +2147,6 @@ function renderChildRequestNotice(fields, name) {
2183
2147
  };
2184
2148
  }
2185
2149
  //#endregion
2186
- //#region src/tui/utils/subagent-slots.ts
2187
- /** Default subagent types, in the fixed display order of the slots. */
2188
- const DEFAULT_SUBAGENT_TYPES = [
2189
- "coder",
2190
- "explore",
2191
- "plan",
2192
- "verify",
2193
- "reviewer",
2194
- "oracle",
2195
- "worker",
2196
- "writer"
2197
- ];
2198
- /** How long a `requesting` overlay stays visible after a subagent asks the
2199
- * parent for help. The request itself returns at once and the agent keeps
2200
- * working, so this is a display window, not a blocked state. */
2201
- const REQUESTING_WINDOW_MS = 5e3;
2202
- var SubagentSlots = class {
2203
- slots = /* @__PURE__ */ new Map();
2204
- instances = /* @__PURE__ */ new Map();
2205
- /** Live instance count per type: a slot only returns to idle when the LAST
2206
- * instance of that type terminates (WolfPack spawns several coders at once
2207
- * and they share one visual slot). */
2208
- runningByType = /* @__PURE__ */ new Map();
2209
- /** main-side SendSubagentMessage tool callId → target subagent agentId. */
2210
- messagingByCall = /* @__PURE__ */ new Map();
2211
- /** Status to restore per TYPE when a messaging window closes, captured from
2212
- * the first open window; concurrent windows on the same type keep counting
2213
- * and the slot only exits messaging when the last window closes. */
2214
- messagingPrev = /* @__PURE__ */ new Map();
2215
- /** Slot type → open `requesting` overlay. Keyed by type so several live
2216
- * instances of one type share a single marker; the real activity state stays
2217
- * untouched in the slot and shows back up once the overlay expires. */
2218
- requesting = /* @__PURE__ */ new Map();
2219
- /** Count of open messaging windows per type (handles same-type siblings). */
2220
- messagingCount = /* @__PURE__ */ new Map();
2221
- reset() {
2222
- this.slots.clear();
2223
- this.instances.clear();
2224
- this.runningByType.clear();
2225
- this.messagingByCall.clear();
2226
- this.messagingPrev.clear();
2227
- this.messagingCount.clear();
2228
- this.requesting.clear();
2229
- }
2230
- /**
2231
- * subagent.spawned. A first spawn starts the slot as working; a second
2232
- * spawn of the SAME agentId means the main agent resumed it (返工中),
2233
- * tracked via an instance generation counter.
2234
- */
2235
- onSpawned(agentId, subagentType, description) {
2236
- const instance = this.instances.get(agentId);
2237
- if (instance !== void 0) {
2238
- instance.generation += 1;
2239
- this.runningByType.set(instance.type, (this.runningByType.get(instance.type) ?? 0) + 1);
2240
- this.setState(instance.type, "reworking", agentId, description === void 0 ? void 0 : `resume: ${description}`);
2241
- return;
2242
- }
2243
- this.instances.set(agentId, {
2244
- type: subagentType,
2245
- generation: 1
2246
- });
2247
- this.runningByType.set(subagentType, (this.runningByType.get(subagentType) ?? 0) + 1);
2248
- this.setState(subagentType, "working", agentId, description);
2249
- }
2250
- /** subagent.started. Keeps the current state (spawned already set it); only
2251
- * used to materialize a slot if lifecycle events raced past us. */
2252
- onStarted(agentId) {
2253
- const instance = this.instances.get(agentId);
2254
- if (instance === void 0) return;
2255
- const slot = this.ensureSlot(instance.type);
2256
- if (slot.status === "idle") {
2257
- slot.status = "working";
2258
- slot.agentId = agentId;
2259
- slot.lastActivityAt = Date.now();
2260
- }
2261
- }
2262
- /** subagent.completed / subagent.failed: the slot goes back to resting only
2263
- * when the LAST live instance of that type terminates (siblings spawned by
2264
- * the same tool call keep the slot busy). */
2265
- onTerminated(agentId) {
2266
- const instance = this.instances.get(agentId);
2267
- if (instance === void 0) return;
2268
- if (this.requesting.get(instance.type)?.agentId === agentId) this.requesting.delete(instance.type);
2269
- const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
2270
- if (remaining > 0) {
2271
- this.runningByType.set(instance.type, remaining);
2272
- const slot = this.ensureSlot(instance.type);
2273
- slot.detail = void 0;
2274
- slot.lastActivityAt = Date.now();
2275
- return;
2276
- }
2277
- this.runningByType.delete(instance.type);
2278
- this.setState(instance.type, "idle", void 0, void 0);
2279
- }
2280
- /**
2281
- * Routed subagent activity (agentId !== main):
2282
- * - kind 'tool' → working (tool call in flight)
2283
- * - kind 'output' → outputting (assistant/thinking delta)
2284
- * - kind 'toolResult'→ detail only (the agent may keep working/outputting)
2285
- */
2286
- onActivity(agentId, kind, detail) {
2287
- const instance = this.instances.get(agentId);
2288
- if (instance === void 0) return;
2289
- if (kind === "toolResult") {
2290
- const slot = this.ensureSlot(instance.type);
2291
- slot.detail = detail;
2292
- slot.lastActivityAt = Date.now();
2293
- return;
2294
- }
2295
- this.setState(instance.type, kind === "tool" ? "working" : "outputting", agentId, detail);
2296
- }
2297
- /** A subagent asked the parent for help. The request returns immediately and
2298
- * the agent keeps working, so this only raises a transient overlay — the real
2299
- * activity state stays in the slot and shows back up when it expires. While
2300
- * `messaging` is on screen the overlay loses (see visibleSlot). */
2301
- onRequesting(agentId, at = Date.now()) {
2302
- const instance = this.instances.get(agentId);
2303
- if (instance === void 0) return;
2304
- this.requesting.set(instance.type, {
2305
- until: at + REQUESTING_WINDOW_MS,
2306
- agentId
2307
- });
2308
- const slot = this.ensureSlot(instance.type);
2309
- slot.agentId = agentId;
2310
- slot.lastActivityAt = at;
2311
- }
2312
- /**
2313
- * Main-side SendSubagentMessage tool call started: the target subagent's
2314
- * slot shows messaging until the tool call returns.
2315
- */
2316
- onMessagingStart(toolCallId, targetAgentId, operation) {
2317
- if (targetAgentId === void 0) return;
2318
- const instance = this.instances.get(targetAgentId);
2319
- if (instance === void 0) return;
2320
- this.messagingByCall.set(toolCallId, targetAgentId);
2321
- this.requesting.delete(instance.type);
2322
- const slot = this.ensureSlot(instance.type);
2323
- if (slot.status !== "messaging") this.messagingPrev.set(instance.type, slot.status);
2324
- this.messagingCount.set(instance.type, (this.messagingCount.get(instance.type) ?? 0) + 1);
2325
- this.setState(instance.type, "messaging", targetAgentId, operation === void 0 ? void 0 : `message (${operation})`);
2326
- }
2327
- /** Tool call finished: the messaging window closes; restore the state the
2328
- * slot had before the chat (working/outputting), else idle. */
2329
- onMessagingEnd(toolCallId) {
2330
- const target = this.messagingByCall.get(toolCallId);
2331
- if (target === void 0) return;
2332
- this.messagingByCall.delete(toolCallId);
2333
- const instance = this.instances.get(target);
2334
- if (instance === void 0) return;
2335
- const slot = this.ensureSlot(instance.type);
2336
- if (slot.status !== "messaging") return;
2337
- const remaining = (this.messagingCount.get(instance.type) ?? 1) - 1;
2338
- if (remaining > 0) {
2339
- this.messagingCount.set(instance.type, remaining);
2340
- return;
2341
- }
2342
- this.messagingCount.delete(instance.type);
2343
- const prev = this.messagingPrev.get(instance.type);
2344
- this.messagingPrev.delete(instance.type);
2345
- slot.status = prev === "working" || prev === "outputting" || prev === "reworking" ? prev : "idle";
2346
- slot.detail = void 0;
2347
- slot.lastActivityAt = Date.now();
2348
- }
2349
- /**
2350
- * All slots in fixed display order: the eight default types first (always
2351
- * present, idle when never used), then extra types in spawn order.
2352
- */
2353
- getSlots(at = Date.now()) {
2354
- this.dropExpiredOverlays(at);
2355
- const ordered = [];
2356
- for (const type of DEFAULT_SUBAGENT_TYPES) ordered.push(this.visibleSlot(type));
2357
- for (const slot of this.slots.values()) {
2358
- if (DEFAULT_SUBAGENT_TYPES.includes(slot.type)) continue;
2359
- ordered.push(this.visibleSlot(slot.type));
2360
- }
2361
- return ordered;
2362
- }
2363
- /** Display view of one slot: the real state with an unexpired `requesting`
2364
- * overlay on top. Read-path only — it never mutates. Callers must run
2365
- * {@link dropExpiredOverlays} first, and messaging clears the overlay in
2366
- * {@link onMessagingStart}; the `messaging` guard only covers same-tick ordering. */
2367
- visibleSlot(type) {
2368
- const copy = {
2369
- ...this.ensureSlot(type),
2370
- count: this.runningByType.get(type) ?? 0
2371
- };
2372
- if (this.requesting.has(type) && copy.status !== "messaging") copy.status = "requesting";
2373
- return copy;
2374
- }
2375
- dropExpiredOverlays(at) {
2376
- for (const [type, overlay] of this.requesting) if (at >= overlay.until) this.requesting.delete(type);
2377
- }
2378
- ensureSlot(type) {
2379
- let slot = this.slots.get(type);
2380
- if (slot === void 0) {
2381
- if (this.slots.size >= 16) {
2382
- slot = {
2383
- type,
2384
- status: "idle",
2385
- agentId: void 0,
2386
- detail: void 0,
2387
- count: 0,
2388
- lastActivityAt: 0
2389
- };
2390
- return slot;
2391
- }
2392
- slot = {
2393
- type,
2394
- status: "idle",
2395
- agentId: void 0,
2396
- detail: void 0,
2397
- count: 0,
2398
- lastActivityAt: Date.now()
2399
- };
2400
- this.slots.set(type, slot);
2401
- }
2402
- return slot;
2403
- }
2404
- setState(type, status, agentId, detail) {
2405
- const slot = this.ensureSlot(type);
2406
- slot.status = status;
2407
- slot.agentId = agentId;
2408
- slot.detail = detail;
2409
- slot.lastActivityAt = Date.now();
2410
- }
2411
- };
2412
- //#endregion
2413
2150
  //#region src/tui/utils/background-task-status.ts
2414
2151
  const MAX_DETAIL_LENGTH = 240;
2415
2152
  function truncate(value) {
@@ -2984,7 +2721,7 @@ var SessionEventHandler = class {
2984
2721
  if (this.providerLatencySamples.length > this.providerLatencyWindow) this.providerLatencySamples.shift();
2985
2722
  this.providerLatencyAt = Date.now();
2986
2723
  }
2987
- /** Sidebar Hub: average measured response time for the provider in use. The
2724
+ /** Sidebar Hub: average measured first-token latency for the provider in use. The
2988
2725
  * caller owns tone and staleness — this stays a plain reading. */
2989
2726
  getProviderLatency() {
2990
2727
  if (this.providerLatencySamples.length === 0) return {
@@ -3137,6 +2874,7 @@ var SessionEventHandler = class {
3137
2874
  handleTurnBegin(_event) {
3138
2875
  this.host.streamingUI.resetToolUi();
3139
2876
  this.host.streamingUI.setStep(0);
2877
+ this.host.streamingUI.endActivityGroup();
3140
2878
  this.host.patchLivePane({
3141
2879
  pendingApproval: null,
3142
2880
  pendingQuestion: null
@@ -3399,6 +3137,7 @@ var SessionEventHandler = class {
3399
3137
  this.host.streamingUI.flushNow();
3400
3138
  this.host.streamingUI.resetToolUi();
3401
3139
  this.host.streamingUI.finalizeLiveTextBuffers();
3140
+ this.host.streamingUI.endActivityGroup();
3402
3141
  this.host.showError(`[${event.code}] ${event.message}`);
3403
3142
  }
3404
3143
  handleSessionWarning(event) {
@@ -4344,6 +4083,7 @@ var SessionReplayRenderer = class {
4344
4083
  context.completedToolCallIds.add(toolCallId);
4345
4084
  }
4346
4085
  advanceTurn(context) {
4086
+ this.host.streamingUI.endActivityGroup();
4347
4087
  context.turnIndex += 1;
4348
4088
  context.stepIndex = 0;
4349
4089
  context.currentTurnId = `replay:${String(context.turnIndex)}`;
@@ -4509,81 +4249,6 @@ var SessionReplayRenderer = class {
4509
4249
  }
4510
4250
  };
4511
4251
  //#endregion
4512
- //#region src/tui/components/dialogs/compaction.ts
4513
- /**
4514
- * Renders a compaction block in the transcript.
4515
- *
4516
- * Lifecycle:
4517
- * - constructed on `compaction.started` → blinking white bullet +
4518
- * "Compacting context..." and optional custom instruction
4519
- * - `markDone()` on `compaction.completed` → solid green bullet +
4520
- * "Compaction complete (X → Y tokens)"
4521
- * - `markCanceled()` on `compaction.cancelled` → solid warning bullet +
4522
- * "Compaction cancelled"
4523
- *
4524
- * Bullet animation mirrors `ToolCallComponent` (500ms blink) so the user
4525
- * reads the same "work in progress" signal across the UI.
4526
- */
4527
- const BLINK_INTERVAL = 500;
4528
- var CompactionComponent = class extends Container {
4529
- colors;
4530
- ui;
4531
- headerText;
4532
- blinkOn = true;
4533
- blinkTimer = null;
4534
- done = false;
4535
- canceled = false;
4536
- tokensBefore;
4537
- tokensAfter;
4538
- constructor(colors, ui, instruction) {
4539
- super();
4540
- this.colors = colors;
4541
- this.ui = ui;
4542
- this.addChild(new Spacer(1));
4543
- this.headerText = new Text(this.buildHeader(), 0, 0);
4544
- this.addChild(this.headerText);
4545
- if (instruction !== void 0) this.addChild(new Text(chalk.dim(` ${instruction}`), 0, 0));
4546
- this.startBlink();
4547
- }
4548
- markDone(tokensBefore, tokensAfter) {
4549
- if (this.done || this.canceled) return;
4550
- this.done = true;
4551
- this.tokensBefore = tokensBefore;
4552
- this.tokensAfter = tokensAfter;
4553
- this.stopBlink();
4554
- this.headerText.setText(this.buildHeader());
4555
- this.ui?.requestRender();
4556
- }
4557
- markCanceled() {
4558
- if (this.done || this.canceled) return;
4559
- this.canceled = true;
4560
- this.stopBlink();
4561
- this.headerText.setText(this.buildHeader());
4562
- this.ui?.requestRender();
4563
- }
4564
- dispose() {
4565
- this.stopBlink();
4566
- }
4567
- buildHeader() {
4568
- if (this.done) return `${chalk.hex(this.colors.success)(STATUS_BULLET)}${chalk.hex(this.colors.success).bold(t("compaction.done"))}${this.tokensBefore !== void 0 && this.tokensAfter !== void 0 ? chalk.dim(` (${String(this.tokensBefore)} → ${String(this.tokensAfter)} token)`) : ""}`;
4569
- if (this.canceled) return `${chalk.hex(this.colors.warning)(STATUS_BULLET)}${chalk.hex(this.colors.warning).bold(t("compaction.canceled"))}`;
4570
- return `${this.blinkOn ? chalk.hex(this.colors.roleAssistant)(STATUS_BULLET) : " "}${chalk.hex(this.colors.primary).bold(t("compaction.in_progress"))}`;
4571
- }
4572
- startBlink() {
4573
- this.blinkTimer = setInterval(() => {
4574
- this.blinkOn = !this.blinkOn;
4575
- this.headerText.setText(this.buildHeader());
4576
- this.ui?.requestRender();
4577
- }, BLINK_INTERVAL);
4578
- }
4579
- stopBlink() {
4580
- if (this.blinkTimer !== null) {
4581
- clearInterval(this.blinkTimer);
4582
- this.blinkTimer = null;
4583
- }
4584
- }
4585
- };
4586
- //#endregion
4587
4252
  //#region src/tui/utils/terminal-notification.ts
4588
4253
  function notifyTerminalOnce(state, key, notification) {
4589
4254
  const { enabled, condition } = state.appState.notifications;
@@ -4657,7 +4322,7 @@ function isControlCharacter(ch) {
4657
4322
  }
4658
4323
  //#endregion
4659
4324
  //#region src/tui/controllers/streaming-ui.ts
4660
- var StreamingUIController = class {
4325
+ var StreamingUIController = class StreamingUIController {
4661
4326
  host;
4662
4327
  flushTimer;
4663
4328
  lastFlushAt;
@@ -4688,9 +4353,30 @@ var StreamingUIController = class {
4688
4353
  _activeToolCalls = /* @__PURE__ */ new Map();
4689
4354
  _streamingToolCallArguments = /* @__PURE__ */ new Map();
4690
4355
  _pendingToolComponents = /* @__PURE__ */ new Map();
4691
- _pendingReadGroupComponents = /* @__PURE__ */ new Map();
4692
4356
  _pendingAgentGroup = null;
4693
- _pendingReadGroup = null;
4357
+ _activityGroup;
4358
+ /** Guards the single `registerLiveComponent` call for the turn's block. */
4359
+ _activityGroupRegistered = false;
4360
+ /** Block that borrowed a card, so late results refresh their own block. */
4361
+ _groupByToolCard = /* @__PURE__ */ new WeakMap();
4362
+ /**
4363
+ * Tools that keep their own transcript card instead of joining the turn's
4364
+ * activity block:
4365
+ * - `Agent` is owned by the agent group machinery (and by `/revoke`, which
4366
+ * walks those groups);
4367
+ * - `AskUserQuestion` returns early above and mounts its finished card;
4368
+ * - `ExitPlanMode` owns the plan box, which Ctrl+E expands by walking the
4369
+ * transcript container's direct children.
4370
+ *
4371
+ * Every other tool is a row inside the block, so a turn reads as one
4372
+ * timeline instead of a mix of cards and groups.
4373
+ */
4374
+ static STANDALONE_TOOL_NAMES = new Set([
4375
+ "Agent",
4376
+ "AskUserQuestion",
4377
+ "ExitPlanMode",
4378
+ "ReadMediaFile"
4379
+ ]);
4694
4380
  constructor(host) {
4695
4381
  this.host = host;
4696
4382
  }
@@ -4771,9 +4457,6 @@ var StreamingUIController = class {
4771
4457
  hasPendingAgentGroup() {
4772
4458
  return this._pendingAgentGroup !== null;
4773
4459
  }
4774
- hasPendingReadGroup() {
4775
- return this._pendingReadGroup !== null;
4776
- }
4777
4460
  removeToolComponentIfInactive(toolCallId) {
4778
4461
  if (!this._activeToolCalls.has(toolCallId)) this._pendingToolComponents.delete(toolCallId);
4779
4462
  }
@@ -4903,17 +4586,89 @@ var StreamingUIController = class {
4903
4586
  }
4904
4587
  /** Tears down replay-specific state after session history has been rendered. */
4905
4588
  cleanupAfterReplay(completedToolCallIds) {
4589
+ this.endActivityGroup();
4906
4590
  this._activeToolCalls.clear();
4907
4591
  for (const toolCallId of completedToolCallIds) this._pendingToolComponents.delete(toolCallId);
4908
4592
  this._pendingAgentGroup = null;
4909
- this._pendingReadGroup = null;
4910
- this._pendingReadGroupComponents.clear();
4911
4593
  this._currentTurnId = void 0;
4912
4594
  this._currentStep = 0;
4913
4595
  this._streamingToolCallArguments.clear();
4914
4596
  this.pendingToolCallFlushIds.clear();
4915
4597
  this.host.state.ui.requestRender();
4916
4598
  }
4599
+ /**
4600
+ * Returns the turn's block, creating it on demand: a block only ever starts
4601
+ * where work starts (reasoning or a tool call), which keeps it positioned
4602
+ * right after whatever the assistant last said.
4603
+ *
4604
+ * Live turns spin, replayed turns (history) do not — the streaming phase is
4605
+ * the discriminator, so a second block inside the same live turn still spins.
4606
+ */
4607
+ ensureActivityGroup() {
4608
+ const existing = this._activityGroup;
4609
+ if (existing !== void 0) return existing;
4610
+ const { state } = this.host;
4611
+ const group = new ActivityGroupComponent(state.theme.colors, state.ui);
4612
+ group.setRunning(state.appState.streamingPhase !== "idle");
4613
+ group.setExpanded(state.toolOutputExpanded);
4614
+ this._activityGroup = group;
4615
+ state.transcriptContainer.addChild(group);
4616
+ state.ui.requestRender();
4617
+ return group;
4618
+ }
4619
+ /**
4620
+ * Seals the current block as soon as the assistant starts writing visible
4621
+ * text, so every stretch of work stays next to the answer it produced instead
4622
+ * of the whole turn's process piling up above the prose.
4623
+ */
4624
+ sealActivityGroupOnText(text) {
4625
+ if (this._activityGroup === void 0) return;
4626
+ if (text.trim().length === 0) return;
4627
+ this.endActivityGroup();
4628
+ }
4629
+ /**
4630
+ * Registers the block against the first entry it owns, so history folding can
4631
+ * later sweep the whole block as one entry. Marked pending so a growing block
4632
+ * is never folded mid-turn.
4633
+ */
4634
+ registerActivityGroupEntry(group, entry) {
4635
+ if (this._activityGroupRegistered) return;
4636
+ this._activityGroupRegistered = true;
4637
+ this.host.transcriptController.registerLiveComponent(group, entry);
4638
+ this.host.transcriptController.markPending(group);
4639
+ }
4640
+ /**
4641
+ * Settles the block: stops the spinner, drops one that never got content, and
4642
+ * releases the reference so the next turn opens a fresh one. Blocks with
4643
+ * content stay mounted (replayed history keeps its blocks).
4644
+ */
4645
+ /**
4646
+ * Files a background task notice (started / completed / failed) as a step of
4647
+ * the turn that spawned it. Returns false when no block is open — a background
4648
+ * task that outlives its turn keeps its own row instead of reopening a settled
4649
+ * block — so the caller can fall back to mounting it on its own.
4650
+ */
4651
+ attachNotice(data) {
4652
+ const group = this._activityGroup;
4653
+ if (group === void 0) return false;
4654
+ group.attachNotice(data);
4655
+ return true;
4656
+ }
4657
+ endActivityGroup() {
4658
+ const group = this._activityGroup;
4659
+ this._activityGroup = void 0;
4660
+ this._activityGroupRegistered = false;
4661
+ if (group === void 0) return;
4662
+ group.setRunning(false);
4663
+ if (group.isEmpty()) {
4664
+ this.host.transcriptController.unmarkPending(group);
4665
+ this.host.state.transcriptContainer.removeChild(group);
4666
+ group.dispose();
4667
+ return;
4668
+ }
4669
+ this.host.transcriptController.unmarkPending(group);
4670
+ group.dispose();
4671
+ }
4917
4672
  disposeActiveThinkingComponent() {
4918
4673
  if (this._activeThinkingComponent !== void 0) {
4919
4674
  this._activeThinkingComponent.dispose();
@@ -4988,7 +4743,9 @@ var StreamingUIController = class {
4988
4743
  const draft = this._assistantDraft;
4989
4744
  if (this._shownAssistantLength >= draft.length) return this._shownAssistantLength;
4990
4745
  const measured = getSharedSpeedTracker().getSpeed();
4991
- const budget = Math.min(25, Math.max(1, Math.ceil((measured > 0 ? measured : 50) * (50 / 1e3) * CHARS_PER_TOKEN)));
4746
+ const tokPerSec = measured > 0 ? measured : 50;
4747
+ const requested = charsForTokenBudget(draft.slice(this._shownAssistantLength), tokPerSec * (50 / 1e3));
4748
+ const budget = Math.min(25, Math.max(1, requested));
4992
4749
  this._shownAssistantLength = Math.min(draft.length, this._shownAssistantLength + budget);
4993
4750
  return this._shownAssistantLength;
4994
4751
  }
@@ -5031,7 +4788,6 @@ var StreamingUIController = class {
5031
4788
  this._streamingToolCallArguments.clear();
5032
4789
  this.disposeAndClearPendingToolComponents();
5033
4790
  this._pendingAgentGroup = null;
5034
- this._pendingReadGroup = null;
5035
4791
  }
5036
4792
  resetToolCallState() {
5037
4793
  this._activeToolCalls.clear();
@@ -5052,6 +4808,7 @@ var StreamingUIController = class {
5052
4808
  const turnStartTime = this.turnStartAt !== 0 ? this.turnStartAt : state.appState.streamingStartTime;
5053
4809
  const completedTurnKey = this._currentTurnId ?? `local:${String(turnStartTime)}`;
5054
4810
  this.finalizeLiveTextBuffers();
4811
+ this.endActivityGroup();
5055
4812
  this.resetToolCallState();
5056
4813
  this._currentTurnId = void 0;
5057
4814
  const next = this.host.shiftQueuedMessage();
@@ -5099,7 +4856,6 @@ var StreamingUIController = class {
5099
4856
  onStreamingTextStart() {
5100
4857
  const { state } = this.host;
5101
4858
  this._pendingAgentGroup = null;
5102
- this._pendingReadGroup = null;
5103
4859
  const entry = {
5104
4860
  id: nextTranscriptId(),
5105
4861
  kind: "assistant",
@@ -5125,6 +4881,7 @@ var StreamingUIController = class {
5125
4881
  block.component.updateContent(fullText);
5126
4882
  this.host.state.ui.requestRender();
5127
4883
  }
4884
+ this.sealActivityGroupOnText(fullText);
5128
4885
  }
5129
4886
  onStreamingTextEnd() {
5130
4887
  const block = this._streamingBlock;
@@ -5139,8 +4896,7 @@ var StreamingUIController = class {
5139
4896
  const { state } = this.host;
5140
4897
  if (this._activeThinkingComponent === void 0) {
5141
4898
  this._pendingAgentGroup = null;
5142
- this._pendingReadGroup = null;
5143
- const component = new ThinkingComponent(fullText, state.theme.colors, true, "live", state.ui);
4899
+ const component = new ThinkingComponent(fullText, state.theme.colors, true, "finalized", state.ui);
5144
4900
  const entry = {
5145
4901
  id: nextTranscriptId(),
5146
4902
  kind: "thinking",
@@ -5153,29 +4909,30 @@ var StreamingUIController = class {
5153
4909
  this.host.pushTranscriptEntry(entry);
5154
4910
  this.host.transcriptController.registerLiveComponent(component, entry);
5155
4911
  this.host.transcriptController.markPending(component);
5156
- if (state.toolOutputExpanded) component.setExpanded(true);
5157
- state.transcriptContainer.addChild(component);
4912
+ const group = this.ensureActivityGroup();
4913
+ this.registerActivityGroupEntry(group, entry);
4914
+ group.appendThinking(fullText, true);
5158
4915
  } else {
5159
4916
  this._activeThinkingComponent.setText(fullText);
5160
4917
  if (this._thinkingEntry !== void 0) this._thinkingEntry.content = fullText;
4918
+ this.ensureActivityGroup().appendThinking(fullText, true);
5161
4919
  }
5162
4920
  state.ui.requestRender();
5163
4921
  }
5164
4922
  onThinkingEnd() {
5165
4923
  if (this._activeThinkingComponent === void 0) return;
5166
- this._activeThinkingComponent.finalize();
5167
- this.host.transcriptController.unmarkPending(this._activeThinkingComponent);
4924
+ const component = this._activeThinkingComponent;
4925
+ component.finalize();
4926
+ this.host.transcriptController.unmarkPending(component);
5168
4927
  this.host.transcriptController.commit();
4928
+ this.host.transcriptController.releaseLiveComponent(component);
5169
4929
  this._activeThinkingComponent = void 0;
5170
4930
  this._thinkingEntry = void 0;
4931
+ this._activityGroup?.endThinking();
5171
4932
  this.host.state.ui.requestRender();
5172
4933
  }
5173
4934
  onToolCallStart(toolCall) {
5174
4935
  if (toolCall.name === "AskUserQuestion") return;
5175
- if (toolCall.name === "ReadGroup") {
5176
- this.startReadGroupToolCall(toolCall);
5177
- return;
5178
- }
5179
4936
  const { state } = this.host;
5180
4937
  const tc = new ToolCallComponent(toolCall, void 0, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
5181
4938
  tc.setPermissionMode(state.appState.permissionMode);
@@ -5191,15 +4948,18 @@ var StreamingUIController = class {
5191
4948
  this.host.pushTranscriptEntry(entry);
5192
4949
  this.host.transcriptController.registerLiveComponent(tc, entry);
5193
4950
  this.host.transcriptController.markPending(tc);
5194
- if (state.toolOutputExpanded) tc.setExpanded(true);
5195
4951
  if (state.planExpanded) tc.setPlanExpanded(true);
5196
4952
  if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
5197
- if (toolCall.name !== "Read") this._pendingReadGroup = null;
5198
- let handled = this.tryAttachAgentToolCall(toolCall, tc);
5199
- if (!handled) handled = this.tryAttachReadToolCall(toolCall, tc);
5200
- if (!handled) {
4953
+ if (!this.tryAttachAgentToolCall(toolCall, tc)) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
4954
+ tc.setExpanded(state.toolOutputExpanded);
5201
4955
  state.transcriptContainer.addChild(tc);
5202
4956
  state.ui.requestRender();
4957
+ } else {
4958
+ const group = this.ensureActivityGroup();
4959
+ this.registerActivityGroupEntry(group, entry);
4960
+ this._groupByToolCard.set(tc, group);
4961
+ group.attachTool(tc, toolCall.step ?? this._currentStep);
4962
+ state.ui.requestRender();
5203
4963
  }
5204
4964
  if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
5205
4965
  const session = this.host.requireSession();
@@ -5216,44 +4976,16 @@ var StreamingUIController = class {
5216
4976
  })();
5217
4977
  }
5218
4978
  }
5219
- startReadGroupToolCall(toolCall) {
5220
- const { state, transcriptController } = this.host;
5221
- const rgc = new ReadGroupComponent(state.theme.colors, state.ui);
5222
- const entry = {
5223
- id: nextTranscriptId(),
5224
- kind: "tool_call",
5225
- turnId: toolCall.turnId ?? this._currentTurnId,
5226
- renderMode: "plain",
5227
- content: toolCall.name,
5228
- toolCallData: toolCall
5229
- };
5230
- this._pendingReadGroupComponents.set(toolCall.id, rgc);
5231
- this._pendingReadGroup = null;
5232
- this._pendingAgentGroup = null;
5233
- this.host.pushTranscriptEntry(entry);
5234
- transcriptController.registerLiveComponent(rgc, entry);
5235
- transcriptController.markPending(rgc);
5236
- state.transcriptContainer.addChild(rgc);
5237
- state.ui.requestRender();
5238
- }
5239
4979
  onToolCallEnd(toolCallId, result) {
5240
4980
  const { state } = this.host;
5241
- const rgc = this._pendingReadGroupComponents.get(toolCallId);
5242
- if (rgc !== void 0) {
5243
- rgc.setResults(parseReadGroupOutput(result.output));
5244
- this.host.transcriptController.unmarkPending(rgc);
5245
- this._pendingReadGroupComponents.delete(toolCallId);
5246
- const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
5247
- if (entry?.toolCallData !== void 0) entry.toolCallData.result = result;
5248
- this.host.transcriptController.commit();
5249
- state.ui.requestRender();
5250
- return;
5251
- }
5252
4981
  const matchedCall = this._activeToolCalls.get(toolCallId);
5253
4982
  const tc = this._pendingToolComponents.get(toolCallId);
5254
4983
  if (tc) {
5255
4984
  tc.setResult(result);
5256
4985
  tc.dispose();
4986
+ const owner = this._groupByToolCard.get(tc) ?? this._activityGroup;
4987
+ owner?.notifyToolChanged();
4988
+ if (owner !== void 0 && this._groupByToolCard.has(tc)) this.host.transcriptController.releaseLiveComponent(tc);
5257
4989
  this.host.transcriptController.unmarkPending(tc);
5258
4990
  this._pendingToolComponents.delete(toolCallId);
5259
4991
  const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
@@ -5265,7 +4997,7 @@ var StreamingUIController = class {
5265
4997
  if (matchedCall?.name === "AskUserQuestion") {
5266
4998
  const completed = new ToolCallComponent(matchedCall, result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
5267
4999
  completed.setPermissionMode(state.appState.permissionMode);
5268
- if (state.toolOutputExpanded) completed.setExpanded(true);
5000
+ completed.setExpanded(state.toolOutputExpanded);
5269
5001
  if (state.planExpanded) completed.setPlanExpanded(true);
5270
5002
  const entry = {
5271
5003
  id: nextTranscriptId(),
@@ -5374,6 +5106,7 @@ var StreamingUIController = class {
5374
5106
  turnId,
5375
5107
  solo: tc
5376
5108
  };
5109
+ tc.setExpanded(state.toolOutputExpanded);
5377
5110
  state.transcriptContainer.addChild(tc);
5378
5111
  state.ui.requestRender();
5379
5112
  return true;
@@ -5420,69 +5153,6 @@ var StreamingUIController = class {
5420
5153
  }
5421
5154
  return group;
5422
5155
  }
5423
- tryAttachReadToolCall(toolCall, tc) {
5424
- const { state } = this.host;
5425
- if (toolCall.name !== "Read") {
5426
- this._pendingReadGroup = null;
5427
- return false;
5428
- }
5429
- const step = toolCall.step ?? this._currentStep;
5430
- const turnId = toolCall.turnId ?? this._currentTurnId;
5431
- const pending = this._pendingReadGroup;
5432
- if (pending !== null && (pending.step !== step || pending.turnId !== turnId)) this._pendingReadGroup = null;
5433
- const cur = this._pendingReadGroup;
5434
- if (cur === null) {
5435
- this._pendingReadGroup = {
5436
- step,
5437
- turnId,
5438
- solo: tc
5439
- };
5440
- state.transcriptContainer.addChild(tc);
5441
- state.ui.requestRender();
5442
- return true;
5443
- }
5444
- if (cur.group !== void 0) {
5445
- cur.group.attach(toolCall.id, tc);
5446
- return true;
5447
- }
5448
- const solo = cur.solo;
5449
- if (solo === void 0) {
5450
- this._pendingReadGroup = {
5451
- step,
5452
- turnId,
5453
- solo: tc
5454
- };
5455
- state.transcriptContainer.addChild(tc);
5456
- state.ui.requestRender();
5457
- return true;
5458
- }
5459
- const group = this.upgradeSoloReadToGroup(solo);
5460
- group.attach(toolCall.id, tc);
5461
- this._pendingReadGroup = {
5462
- step,
5463
- turnId,
5464
- group
5465
- };
5466
- state.ui.requestRender();
5467
- return true;
5468
- }
5469
- upgradeSoloReadToGroup(solo) {
5470
- const { state, transcriptController } = this.host;
5471
- const group = new ReadGroupComponent(state.theme.colors, state.ui);
5472
- const children = state.transcriptContainer.children;
5473
- const idx = children.indexOf(solo);
5474
- if (idx >= 0) {
5475
- children[idx] = group;
5476
- state.transcriptContainer.invalidate();
5477
- } else state.transcriptContainer.addChild(group);
5478
- group.attach(solo.toolCallView.id, solo);
5479
- const entry = transcriptController.findEntryForComponent(solo);
5480
- if (entry !== void 0) {
5481
- transcriptController.registerLiveComponent(group, entry);
5482
- transcriptController.unmarkPending(solo);
5483
- }
5484
- return group;
5485
- }
5486
5156
  };
5487
5157
  //#endregion
5488
5158
  //#region src/tui/components/dialogs/task-output-viewer.ts
@@ -6617,6 +6287,14 @@ var TranscriptController = class TranscriptController {
6617
6287
  registerLiveComponent(component, entry) {
6618
6288
  this.liveComponentToEntry.set(component, entry);
6619
6289
  }
6290
+ /**
6291
+ * Drops the entry mapping of a component that will never be folded into the
6292
+ * committed history (it is not a container child), so the map cannot retain
6293
+ * components — and the text they hold — for the life of the session.
6294
+ */
6295
+ releaseLiveComponent(component) {
6296
+ this.liveComponentToEntry.delete(component);
6297
+ }
6620
6298
  markPending(component) {
6621
6299
  this.pendingComponents.add(component);
6622
6300
  }
@@ -6684,20 +6362,11 @@ var TranscriptController = class TranscriptController {
6684
6362
  component.updateContent(entry.content);
6685
6363
  return component;
6686
6364
  }
6687
- case "thinking": {
6688
- const thinking = new ThinkingComponent(entry.content, state.theme.colors, true);
6689
- if (state.toolOutputExpanded) thinking.setExpanded(true);
6690
- return thinking;
6691
- }
6365
+ case "thinking": return new ThinkingComponent(entry.content, state.theme.colors, true);
6692
6366
  case "tool_call":
6693
- if (entry.toolCallData?.name === "ReadGroup" && entry.toolCallData.result !== void 0) {
6694
- const rgc = new ReadGroupComponent(state.theme.colors, state.ui);
6695
- rgc.setResults(parseReadGroupOutput(entry.toolCallData.result.output));
6696
- return rgc;
6697
- }
6367
+ case "tool_result":
6698
6368
  if (entry.toolCallData) {
6699
6369
  const tc = new ToolCallComponent(entry.toolCallData, entry.toolCallData.result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
6700
- if (state.toolOutputExpanded) tc.setExpanded(true);
6701
6370
  if (state.planExpanded) tc.setPlanExpanded(true);
6702
6371
  return tc;
6703
6372
  }
@@ -6718,7 +6387,9 @@ var TranscriptController = class TranscriptController {
6718
6387
  const component = this.createComponent(entry);
6719
6388
  if (component) {
6720
6389
  this.liveComponentToEntry.set(component, entry);
6721
- this.host.state.transcriptContainer.addChild(component);
6390
+ const status = entry.backgroundAgentStatus;
6391
+ if (status !== void 0 && this.host.streamingUI.attachNotice(status)) this.releaseLiveComponent(component);
6392
+ else this.host.state.transcriptContainer.addChild(component);
6722
6393
  this.host.state.ui.requestRender();
6723
6394
  }
6724
6395
  return component ?? null;
@@ -6781,6 +6452,7 @@ var TranscriptController = class TranscriptController {
6781
6452
  clearAndRedraw() {
6782
6453
  const { state, streamingUI, imageStore } = this.host;
6783
6454
  streamingUI.discardPending();
6455
+ streamingUI.endActivityGroup();
6784
6456
  state.transcriptEntries = [];
6785
6457
  streamingUI.disposeActiveCompactionBlock();
6786
6458
  streamingUI.resetLiveText();
@@ -6790,6 +6462,7 @@ var TranscriptController = class TranscriptController {
6790
6462
  this.committedComponent = void 0;
6791
6463
  this.liveComponentToEntry.clear();
6792
6464
  this.pendingComponents.clear();
6465
+ state.toolOutputExpanded = false;
6793
6466
  disposeChildren(state.transcriptContainer);
6794
6467
  this.clearTerminalInlineImages();
6795
6468
  state.todoPanel.clear();
@@ -6837,7 +6510,15 @@ var TranscriptController = class TranscriptController {
6837
6510
  }
6838
6511
  toggleToolOutputExpansion() {
6839
6512
  const { state } = this.host;
6840
- state.toolOutputExpanded = !state.toolOutputExpanded;
6513
+ const next = !state.toolOutputExpanded;
6514
+ const children = state.transcriptContainer.children;
6515
+ for (let i = children.length - 1; i >= 0; i -= 1) {
6516
+ const child = children[i];
6517
+ if (child === void 0) continue;
6518
+ if (child instanceof UserMessageComponent || child instanceof SkillActivationComponent || child instanceof CronMessageComponent || child instanceof CommittedTranscriptComponent) break;
6519
+ if (isExpandable(child)) child.setExpanded(next);
6520
+ }
6521
+ state.toolOutputExpanded = next;
6841
6522
  state.ui.requestRender();
6842
6523
  }
6843
6524
  togglePlanExpansion() {
@@ -7058,17 +6739,14 @@ function formatCount$1(n) {
7058
6739
  return n.toLocaleString("en-US");
7059
6740
  }
7060
6741
  /**
7061
- * Compact token form for narrow sidebars: 2,267,680,102 → `22.7亿` (zh) or
7062
- * `2.3B` (en). Used only when the thousands-separated form cannot fit, so a
7063
- * narrow sidebar keeps every value readable instead of cutting digits.
6742
+ * Compact token form for narrow sidebars: 2,267,680,102 → `2267.7M`. One unit
6743
+ * ladder for every locale (K below a million, M above) so the value column
6744
+ * stays comparable across languages: Chinese no longer switches to the
6745
+ * ten-thousand / hundred-million units it used before.
6746
+ * Used only when the thousands-separated form cannot fit, so a narrow sidebar
6747
+ * keeps every value readable instead of cutting digits.
7064
6748
  */
7065
6749
  function formatCompact(n) {
7066
- if (getLocale() === "zh") {
7067
- if (n >= 9995e4) return `${(n / 1e8).toFixed(1)}亿`;
7068
- if (n >= 9500) return `${Math.round(n / 1e4)}万`;
7069
- return String(n);
7070
- }
7071
- if (n >= 9995e5) return `${(n / 1e9).toFixed(1)}B`;
7072
6750
  if (n >= 999500) return `${(n / 1e6).toFixed(1)}M`;
7073
6751
  if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
7074
6752
  return String(n);
@@ -7136,6 +6814,10 @@ const sessionPanel = {
7136
6814
  * with the 1s sidebar heartbeat each tick advances half the cycle, so the
7137
6815
  * colour jump is clearly visible instead of drifting imperceptibly. */
7138
6816
  const AGENT_GRADIENT_CYCLE_MS = 2e3;
6817
+ /** Display form of a slot type: 'coder' reads as 'Coder' in the panel. */
6818
+ function displayType(type) {
6819
+ return type.length === 0 ? type : type.charAt(0).toUpperCase() + type.slice(1);
6820
+ }
7139
6821
  /** Localized status word per slot state. */
7140
6822
  function statusText(status) {
7141
6823
  switch (status) {
@@ -7165,7 +6847,7 @@ var AgentsPanelContent = class {
7165
6847
  if (agents === void 0 || agents.length === 0) return [];
7166
6848
  return layoutSidebarGrid(agents.map((slot) => ({
7167
6849
  marker: slot.status === "idle" ? "○" : "●",
7168
- label: slot.type,
6850
+ label: displayType(slot.type),
7169
6851
  value: statusText(slot.status) + (slot.count > 1 ? ` ×${slot.count}` : "")
7170
6852
  })), width).map((cell, index) => this.renderSlot(cell, agents[index], index, width));
7171
6853
  }
@@ -7321,20 +7003,23 @@ const HUB_MODEL_STALE_MS = 5 * 6e4;
7321
7003
  const HUB_ENDPOINTS = Object.freeze([
7322
7004
  {
7323
7005
  id: "github",
7324
- url: "https://github.com/"
7006
+ url: "https://github.com/",
7007
+ label: "Github"
7325
7008
  },
7326
7009
  {
7327
7010
  id: "google",
7328
- url: "https://www.google.com/"
7011
+ url: "https://www.google.com/",
7012
+ label: "Google"
7329
7013
  },
7330
7014
  {
7331
7015
  id: "x",
7332
7016
  url: "https://x.com/",
7333
- label: "x (twitter)"
7017
+ label: "X (Twitter)"
7334
7018
  },
7335
7019
  {
7336
7020
  id: "huggingface",
7337
- url: "https://huggingface.co/"
7021
+ url: "https://huggingface.co/",
7022
+ label: "Huggingface"
7338
7023
  },
7339
7024
  {
7340
7025
  id: "alibaba",
@@ -8488,6 +8173,7 @@ var LifecycleController = class LifecycleController {
8488
8173
  case "waiting":
8489
8174
  case "composing":
8490
8175
  case "tool":
8176
+ case "thinking":
8491
8177
  {
8492
8178
  const loader = new PulseWaveLoader(state.ui, state.theme.colors.primary);
8493
8179
  this.statusBarLoader = loader;
@@ -8498,7 +8184,6 @@ var LifecycleController = class LifecycleController {
8498
8184
  }));
8499
8185
  }
8500
8186
  break;
8501
- case "thinking": break;
8502
8187
  case "idle":
8503
8188
  case "hidden":
8504
8189
  if (mode === "idle" && state.transcriptEntries.length === 0 && !isEmptySessionHintDismissed()) {
@@ -13977,6 +13662,7 @@ var ScreamTUI = class {
13977
13662
  beginSessionRequest() {
13978
13663
  this.streamingUI.markTurnStarted();
13979
13664
  this.streamingUI.setTurnId(void 0);
13665
+ this.streamingUI.endActivityGroup();
13980
13666
  this.streamingUI.resetLiveText();
13981
13667
  this.streamingUI.resetToolUi();
13982
13668
  this.streamingUI.resetToolCallState();
@@ -13988,6 +13674,7 @@ var ScreamTUI = class {
13988
13674
  }
13989
13675
  failSessionRequest(message) {
13990
13676
  this.setAppState({ streamingPhase: "idle" });
13677
+ this.streamingUI.endActivityGroup();
13991
13678
  this.resetLivePane();
13992
13679
  this.showError(message);
13993
13680
  }
@@ -14195,7 +13882,7 @@ var ScreamTUI = class {
14195
13882
  const card = new OnboardingCardComponent({
14196
13883
  colors: this.state.theme.colors,
14197
13884
  onConfigure: () => {
14198
- import("./dispatch-B6jbxY0D.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
13885
+ import("./dispatch-BGLqpKHG.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14199
13886
  this.restoreEditor();
14200
13887
  onDone?.();
14201
13888
  });
@@ -15864,6 +15551,53 @@ async function persistKey(filePath, record) {
15864
15551
  await rename(tmp, filePath);
15865
15552
  }
15866
15553
  //#endregion
15554
+ //#region src/web/journal-writer.ts
15555
+ /**
15556
+ * Serialization gate for journal appends.
15557
+ *
15558
+ * Why it exists (an incident observed in the field): the session journal is an
15559
+ * append-only file holding one JSON record per line, and lines used to be
15560
+ * appended with `writeFile(path, line + '\n', { flag: 'a' })`. A
15561
+ * `web.message.finalized` entry carries an entire assistant body (hundreds of
15562
+ * thousands of characters in practice) while small entries such as turn.ended
15563
+ * are written at the same time — the two writes interleaved at the byte level,
15564
+ * so what landed on disk were malformed lines with the two records spliced
15565
+ * into each other:
15566
+ *
15567
+ * ..."content":"...{"type":"journal","seq":38354,... ← another record spliced into the body
15568
+ *
15569
+ * Consequence: the whole line fails JSON.parse and the message disappears from
15570
+ * the UI (the reader can only skip it), which the user perceives as a garbled
15571
+ * or lost body. Queueing the writes inside the process eliminates the
15572
+ * interleaving; the append itself still goes through appendFile (O_APPEND), so
15573
+ * a single write is never split by another fd.
15574
+ *
15575
+ * The queue is keyed by file path and dropped once drained, so a long-running
15576
+ * process does not grow it without bound. No cross-process writer exists today
15577
+ * (only the web server writes the journal); if one appears, this needs a
15578
+ * file-lock based scheme instead.
15579
+ */
15580
+ const writeChains = /* @__PURE__ */ new Map();
15581
+ /**
15582
+ * Append one line (plus its newline) to the journal; concurrent calls for the
15583
+ * same file land in arrival order.
15584
+ */
15585
+ async function appendJournalLineSerialized(path, line, onError) {
15586
+ const next = (writeChains.get(path) ?? Promise.resolve()).catch(() => void 0).then(async () => {
15587
+ try {
15588
+ await appendFile(path, line + "\n");
15589
+ } catch (error) {
15590
+ onError(error);
15591
+ }
15592
+ });
15593
+ writeChains.set(path, next);
15594
+ try {
15595
+ await next;
15596
+ } finally {
15597
+ if (writeChains.get(path) === next) writeChains.delete(path);
15598
+ }
15599
+ }
15600
+ //#endregion
15867
15601
  //#region src/web/rateLimit.ts
15868
15602
  function createFixedWindowLimiter(opts) {
15869
15603
  const { windowMs, max } = opts;
@@ -16431,14 +16165,12 @@ async function ensureSessionsDir(homeDir) {
16431
16165
  if (!existsSync(dir)) await mkdir(dir, { recursive: true });
16432
16166
  }
16433
16167
  async function appendJournalLine(homeDir, sessionId, line) {
16434
- try {
16435
- await writeFile(getJournalPath(homeDir, sessionId), line + "\n", { flag: "a" });
16436
- } catch (error) {
16168
+ await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
16437
16169
  log.warn("web: failed to persist journal line", {
16438
16170
  sessionId,
16439
16171
  error: String(error)
16440
16172
  });
16441
- }
16173
+ });
16442
16174
  }
16443
16175
  let metaWriteSeq = 0;
16444
16176
  async function saveMetadata(homeDir, meta) {
@@ -16465,17 +16197,78 @@ async function loadMetadata(homeDir, sessionId) {
16465
16197
  async function loadJournal(homeDir, sessionId) {
16466
16198
  try {
16467
16199
  const lines = (await readFile(getJournalPath(homeDir, sessionId), "utf-8")).split("\n").filter((l) => l.trim().length > 0);
16468
- const results = [];
16200
+ const entries = [];
16201
+ let corrupt = 0;
16469
16202
  for (const line of lines) try {
16470
- results.push(JSON.parse(line));
16203
+ entries.push(JSON.parse(line));
16471
16204
  } catch {
16205
+ corrupt += 1;
16472
16206
  log.warn("web: skipping corrupt journal line", { sessionId });
16473
16207
  }
16474
- return results;
16208
+ return {
16209
+ entries,
16210
+ corrupt
16211
+ };
16475
16212
  } catch {
16476
- return [];
16213
+ return {
16214
+ entries: [],
16215
+ corrupt: 0
16216
+ };
16477
16217
  }
16478
16218
  }
16219
+ /**
16220
+ * Event families that mutate the main transcript: only the main agent's
16221
+ * (`agentId === 'main'`) events of these kinds may be applied, journaled and
16222
+ * broadcast. Subagents and background tasks each emit their own turn.started
16223
+ * plus hundreds of text/tool events — mixing them into the main transcript
16224
+ * produces a body where two streams alternate block by block, blows the tool
16225
+ * list up to 97 entries and the model badge to 4 lines (reproduced from a real
16226
+ * session). Subagent presence is carried by subagent.* events instead, never by
16227
+ * the transcript.
16228
+ */
16229
+ const MAIN_TRANSCRIPT_EVENTS = new Set([
16230
+ "turn.started",
16231
+ "turn.ended",
16232
+ "assistant.delta",
16233
+ "thinking.delta",
16234
+ "tool.call.started",
16235
+ "tool.result"
16236
+ ]);
16237
+ /**
16238
+ * Whether a journal entry already carries a message (if so it needs no seeding
16239
+ * from core history).
16240
+ */
16241
+ function isMessageBearingEntry(entry) {
16242
+ if ("type" in entry && entry.type === "user_message") return true;
16243
+ const payload = entry.payload;
16244
+ return payload.type === "web.message.finalized" || payload.type === "turn.started";
16245
+ }
16246
+ /**
16247
+ * Core context history → web ChatMessage seed.
16248
+ *
16249
+ * Only "real user input + assistant bodies" are carried over: core tool records
16250
+ * and the web-side ToolMessage are different shapes, so a forced mapping would
16251
+ * invent fake cards. Messages with a non-user origin (injections, system
16252
+ * triggers, background-task notices, skill-activation envelopes, compaction
16253
+ * summaries) are protocol content meant for the model and must not be rendered
16254
+ * as user turns.
16255
+ */
16256
+ function contextHistoryToChatMessages(history) {
16257
+ const out = [];
16258
+ for (const message of history) {
16259
+ if (message.role !== "user" && message.role !== "assistant") continue;
16260
+ if (isInternalMessage(message)) continue;
16261
+ if (message.origin !== void 0 && message.origin.kind !== "user") continue;
16262
+ const text = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
16263
+ if (text.length === 0) continue;
16264
+ out.push({
16265
+ role: message.role,
16266
+ content: text,
16267
+ tools: []
16268
+ });
16269
+ }
16270
+ return out;
16271
+ }
16479
16272
  async function listPersistedSessions(homeDir) {
16480
16273
  const dir = getSessionsDir(homeDir);
16481
16274
  if (!existsSync(dir)) return [];
@@ -16653,10 +16446,11 @@ const MAX_INLINE_DIFF_BYTES = 512 * 1024;
16653
16446
  * Single-file patch. `repoPath` is **repository-root relative** — the same base
16654
16447
  * `git status` reports, so the UI can hand it straight back.
16655
16448
  *
16656
- * Two cases used to return an empty patch and look like "点了没反应":
16657
- * nested files (the old code re-prefixed workdir-relative paths with `../..`,
16658
- * resolving them outside the repo) and untracked files (`git diff` has nothing
16659
- * to say about them). The second case now gets a synthesised added-file patch.
16449
+ * Two cases used to return an empty patch, which read as "clicking did
16450
+ * nothing": nested files (the old code re-prefixed workdir-relative paths with
16451
+ * `../..`, resolving them outside the repo) and untracked files (`git diff` has
16452
+ * nothing to say about them). The second case now gets a synthesised added-file
16453
+ * patch.
16660
16454
  */
16661
16455
  async function getGitFileDiff(workDir, repoPath) {
16662
16456
  const rel = normalizeNumstatPath(repoPath);
@@ -16774,7 +16568,27 @@ var WebSession = class {
16774
16568
  * snapshot (`web.message.finalized`) — the only source that survives a
16775
16569
  * server restart (deltas are volatile by design).
16776
16570
  */
16777
- liveAssistant = null;
16571
+ /**
16572
+ * In-flight main-agent turn messages, keyed by `turnId`.
16573
+ * A single pointer (the old liveAssistant) merged several streams into one
16574
+ * message when an interrupted turn overlapped a freshly started one, or when
16575
+ * subagents ran concurrently, producing interleaved garbage — hence one slot
16576
+ * per turn.
16577
+ */
16578
+ liveAssistants = /* @__PURE__ */ new Map();
16579
+ /**
16580
+ * In-flight message of one turn; without a turnId it falls back to the most
16581
+ * recently started one (older-frame compatibility).
16582
+ */
16583
+ liveFor(turnId) {
16584
+ if (typeof turnId === "number") {
16585
+ const hit = this.liveAssistants.get(turnId);
16586
+ if (hit) return hit;
16587
+ }
16588
+ let latest = null;
16589
+ for (const message of this.liveAssistants.values()) latest = message;
16590
+ return latest;
16591
+ }
16778
16592
  liveTurnCount = 0;
16779
16593
  /** pendingMsgId awaiting compaction completion/cancellation event. */
16780
16594
  pendingCompactionMsgId = null;
@@ -16888,6 +16702,63 @@ var WebSession = class {
16888
16702
  const msgs = this.buildMessages();
16889
16703
  if (msgs.length > 0 && !this.customTitle) this.title = deriveTitle(msgs);
16890
16704
  }
16705
+ /**
16706
+ * Core wire history seed: filled in once by the activation path when the
16707
+ * journal holds no message records.
16708
+ */
16709
+ seededMessages = [];
16710
+ /**
16711
+ * Mark the journal untrustworthy (byte-interleaving damage, see
16712
+ * journal-writer.ts).
16713
+ *
16714
+ * The loaded journal entries and any legacy userMessages are dropped so the
16715
+ * message list is presented from the core transcript seed instead; state
16716
+ * snapshots (goal/todo, …) were already cached by loadFromPersisted and are
16717
+ * unaffected. nextSeq does not roll back: live entries keep appending and
16718
+ * rendering as usual after activation, so new messages are untouched (a seq
16719
+ * gap on the client takes the existing gap → snapshot refetch path).
16720
+ */
16721
+ markDamaged(reason) {
16722
+ const droppedEntries = this.journal.length;
16723
+ const droppedUsers = this.userMessages.length;
16724
+ this.journal.length = 0;
16725
+ this.userMessages.length = 0;
16726
+ log.warn("web: session journal damaged; messages rebuilt from core transcript", {
16727
+ sessionId: this.sessionId,
16728
+ reason,
16729
+ droppedEntries,
16730
+ droppedUsers
16731
+ });
16732
+ }
16733
+ /**
16734
+ * Whether the current message list holds at least one assistant message with
16735
+ * a body. Used to decide how far journal remnants can be trusted: user
16736
+ * messages are persisted separately (they always carry text), while the
16737
+ * byte-interleaving damage only ever ate assistant bodies — so only the
16738
+ * assistant side is inspected.
16739
+ */
16740
+ hasAnyAssistantContent() {
16741
+ return this.buildMessages().some((message) => message.role === "assistant" && message.content.trim().length > 0);
16742
+ }
16743
+ /**
16744
+ * Seed messages from the core session history. An older session's web
16745
+ * journal may hold nothing but state entries (or predate durable snapshots),
16746
+ * which leaves the chat area empty after a resume while the wire history is
16747
+ * intact. The seed occupies seq 1..N and lifts nextSeq past it, keeping seq
16748
+ * monotonic with live events and consistent with the getMessagesOlder cursor.
16749
+ */
16750
+ seedHistory(messages) {
16751
+ if (messages.length === 0 || this.seededMessages.length > 0) return;
16752
+ let seq = 1;
16753
+ for (const message of messages) {
16754
+ this.seededMessages.push({
16755
+ ...message,
16756
+ seq
16757
+ });
16758
+ seq += 1;
16759
+ }
16760
+ if (seq - 1 >= this.nextSeq) this.nextSeq = seq;
16761
+ }
16891
16762
  async updateTitle() {
16892
16763
  if (this.customTitle) return;
16893
16764
  const newTitle = deriveTitle(this.buildMessages());
@@ -16939,11 +16810,12 @@ var WebSession = class {
16939
16810
  if (!this.session) return;
16940
16811
  this.unsubscribe = this.session.onEvent((event) => {
16941
16812
  if ((event.type === "goal.updated" || event.type === "todo.updated") && event.agentId !== "main") return;
16813
+ if (MAIN_TRANSCRIPT_EVENTS.has(event.type) && event.agentId !== "main") return;
16942
16814
  if (event.type === "turn.started") {
16943
16815
  this.busy = true;
16944
16816
  this.activeTurns += 1;
16945
16817
  this.liveTurnCount += 1;
16946
- this.liveAssistant = {
16818
+ this.liveAssistants.set(event.turnId, {
16947
16819
  role: "assistant",
16948
16820
  content: "",
16949
16821
  tools: [],
@@ -16959,11 +16831,11 @@ var WebSession = class {
16959
16831
  tokens: null,
16960
16832
  tokensPerSec: null
16961
16833
  }
16962
- };
16834
+ });
16963
16835
  } else if (event.type === "turn.ended") {
16964
16836
  this.activeTurns = Math.max(0, this.activeTurns - 1);
16965
16837
  this.busy = this.activeTurns > 0;
16966
- const live = this.liveAssistant;
16838
+ const live = this.liveFor(event.turnId);
16967
16839
  if (event.reason === "failed" && live) live.isError = true;
16968
16840
  if (live) {
16969
16841
  live.turnStats = {
@@ -16983,13 +16855,13 @@ var WebSession = class {
16983
16855
  message: live
16984
16856
  });
16985
16857
  }
16986
- this.liveAssistant = null;
16858
+ this.liveAssistants.delete(event.turnId);
16987
16859
  this.trackPending(this.updateTitle());
16988
16860
  } else if (event.type === "assistant.delta") {
16989
- const live = this.liveAssistant;
16861
+ const live = this.liveFor(event.turnId);
16990
16862
  if (live) live.content += event.delta;
16991
16863
  } else if (event.type === "thinking.delta") {
16992
- const live = this.liveAssistant;
16864
+ const live = this.liveFor(event.turnId);
16993
16865
  if (live) {
16994
16866
  const t = live.tools.find((x) => x.name === "thinking");
16995
16867
  if (t) t.output = (t.output ?? "") + event.delta;
@@ -17001,7 +16873,7 @@ var WebSession = class {
17001
16873
  });
17002
16874
  }
17003
16875
  } else if (event.type === "tool.call.started") {
17004
- const live = this.liveAssistant;
16876
+ const live = this.liveFor(event.turnId);
17005
16877
  if (live) {
17006
16878
  live.tools.push({
17007
16879
  toolCallId: event.toolCallId,
@@ -17011,7 +16883,7 @@ var WebSession = class {
17011
16883
  if (live.turnStats) live.turnStats.step += 1;
17012
16884
  }
17013
16885
  } else if (event.type === "tool.result") {
17014
- const live = this.liveAssistant;
16886
+ const live = this.liveFor(event.turnId);
17015
16887
  if (live) {
17016
16888
  const tool = live.tools.find((t) => t.toolCallId === event.toolCallId);
17017
16889
  if (tool) {
@@ -17492,7 +17364,7 @@ var WebSession = class {
17492
17364
  workDir: this.workDir,
17493
17365
  title: this.title,
17494
17366
  createdAt: this.createdAt,
17495
- messageCount: this.buildMessages().length,
17367
+ messageCount: this.isActive ? this.buildMessages().length : -1,
17496
17368
  active: this.isActive
17497
17369
  };
17498
17370
  }
@@ -17686,7 +17558,7 @@ var WebSession = class {
17686
17558
  return this.connections.size;
17687
17559
  }
17688
17560
  buildMessages() {
17689
- const messages = [];
17561
+ const messages = [...this.seededMessages];
17690
17562
  let currentAssistant = null;
17691
17563
  let userIndex = 0;
17692
17564
  let turnCount = 0;
@@ -17872,6 +17744,36 @@ async function handleGoalRoute(req, res, url, resolveSession) {
17872
17744
  }
17873
17745
  return true;
17874
17746
  }
17747
+ /**
17748
+ * Validate the optional workDir argument of session creation; undefined means
17749
+ * "reuse the server process directory".
17750
+ *
17751
+ * Trade-off: web is a local tool and the browser shares the CLI's trust level,
17752
+ * so no directory allowlist is introduced; but only an explicitly provided
17753
+ * absolute path is accepted — relative paths and `..` segments are rejected
17754
+ * (resolving them away instead of rejecting would amount to permitting
17755
+ * traversal), and the target must exist, be a directory and be readable and
17756
+ * writable. An invalid directory fails loudly with a 4xx at the moment the
17757
+ * session is created, rather than letting the agent wander into some
17758
+ * unexpected location later.
17759
+ */
17760
+ async function validateSessionWorkDir(raw) {
17761
+ if (raw === void 0 || raw === null) return void 0;
17762
+ if (typeof raw !== "string") throw new HttpError(400, "workDir 必须是字符串", "request.invalid");
17763
+ const value = raw.trim();
17764
+ if (!value) return void 0;
17765
+ if (!isAbsolute(value)) throw new HttpError(400, `workDir 必须是绝对路径:${value}`, "request.invalid");
17766
+ if (value.split(/[\\/]+/).includes("..")) throw new HttpError(400, `workDir 不允许包含 ".." 遍历段:${value}`, "request.invalid");
17767
+ const st = await stat(value).catch(() => null);
17768
+ if (!st) throw new HttpError(404, `目录不存在:${value}`, "request.invalid");
17769
+ if (!st.isDirectory()) throw new HttpError(400, `不是目录:${value}`, "request.invalid");
17770
+ try {
17771
+ await access(value, constants.R_OK | constants.W_OK);
17772
+ } catch {
17773
+ throw new HttpError(403, `目录不可读写:${value}`, "request.invalid");
17774
+ }
17775
+ return value;
17776
+ }
17875
17777
  var SessionManager = class {
17876
17778
  sessions = /* @__PURE__ */ new Map();
17877
17779
  /** In-flight activation promises to deduplicate concurrent calls. */
@@ -17894,7 +17796,7 @@ var SessionManager = class {
17894
17796
  await ensureSessionsDir(this.homeDir);
17895
17797
  const metas = await listPersistedSessions(this.homeDir);
17896
17798
  for (const meta of metas) {
17897
- const entries = await loadJournal(this.homeDir, meta.sessionId);
17799
+ const { entries } = await loadJournal(this.homeDir, meta.sessionId);
17898
17800
  const ws = new WebSession(null, {
17899
17801
  sessionId: meta.sessionId,
17900
17802
  coreSessionId: meta.coreSessionId ?? meta.sessionId,
@@ -17908,11 +17810,107 @@ var SessionManager = class {
17908
17810
  ws.loadFromPersisted(entries);
17909
17811
  this.sessions.set(meta.sessionId, ws);
17910
17812
  }
17813
+ try {
17814
+ const index = await readSessionIndex(this.homeDir, join(this.homeDir, "sessions"));
17815
+ const managerWorkDir = normalizeWorkDir(this.workDir);
17816
+ for (const entry of index.values()) try {
17817
+ if (normalizeWorkDir(entry.workDir) !== managerWorkDir) continue;
17818
+ if (this.sessions.has(entry.sessionId)) continue;
17819
+ if (!existsSync(entry.sessionDir)) continue;
17820
+ let title = "未命名会话";
17821
+ let createdAt = Date.now();
17822
+ try {
17823
+ const raw = await readFile(join(entry.sessionDir, "state.json"), "utf8");
17824
+ const state = JSON.parse(raw);
17825
+ if (typeof state.title === "string" && state.title) title = state.title;
17826
+ if (state.createdAt) {
17827
+ const t = typeof state.createdAt === "number" ? state.createdAt : Date.parse(state.createdAt);
17828
+ if (Number.isFinite(t)) createdAt = t;
17829
+ }
17830
+ } catch {}
17831
+ const session = new WebSession(null, {
17832
+ sessionId: entry.sessionId,
17833
+ coreSessionId: entry.sessionId,
17834
+ workDir: entry.workDir,
17835
+ permission: this.permission,
17836
+ yolo: this.yolo,
17837
+ homeDir: this.homeDir,
17838
+ createdAt,
17839
+ title
17840
+ });
17841
+ this.sessions.set(entry.sessionId, session);
17842
+ } catch (error) {
17843
+ log.warn("web: skipping unreadable session index entry", {
17844
+ sessionId: entry.sessionId,
17845
+ sessionDir: entry.sessionDir,
17846
+ error: errorMessage(error)
17847
+ });
17848
+ }
17849
+ } catch (error) {
17850
+ log.warn("web: session index unreadable; falling back to web mirror + directory scan", {
17851
+ homeDir: this.homeDir,
17852
+ error: errorMessage(error)
17853
+ });
17854
+ }
17855
+ const wdKey = encodeWorkDirKey(this.workDir);
17856
+ const wdDir = join(this.homeDir, "sessions", wdKey);
17857
+ try {
17858
+ if (existsSync(wdDir)) for (const name of await readdir(wdDir)) {
17859
+ if (!name.startsWith("session_")) continue;
17860
+ if (this.sessions.has(name)) continue;
17861
+ try {
17862
+ const sessionDir = join(wdDir, name);
17863
+ let title = "未命名会话";
17864
+ let createdAt = Date.now();
17865
+ try {
17866
+ const raw = await readFile(join(sessionDir, "state.json"), "utf8");
17867
+ const state = JSON.parse(raw);
17868
+ if (typeof state.title === "string" && state.title) title = state.title;
17869
+ if (state.createdAt) {
17870
+ const t = typeof state.createdAt === "number" ? state.createdAt : Date.parse(state.createdAt);
17871
+ if (Number.isFinite(t)) createdAt = t;
17872
+ }
17873
+ } catch {}
17874
+ const session = new WebSession(null, {
17875
+ sessionId: name,
17876
+ coreSessionId: name,
17877
+ workDir: this.workDir,
17878
+ permission: this.permission,
17879
+ yolo: this.yolo,
17880
+ homeDir: this.homeDir,
17881
+ createdAt,
17882
+ title
17883
+ });
17884
+ this.sessions.set(name, session);
17885
+ } catch (error) {
17886
+ log.warn("web: skipping unreadable core session directory", {
17887
+ wdKey,
17888
+ sessionId: name,
17889
+ error: errorMessage(error)
17890
+ });
17891
+ }
17892
+ }
17893
+ } catch (error) {
17894
+ log.warn("web: core session bucket scan failed", {
17895
+ wdKey,
17896
+ dir: wdDir,
17897
+ error: errorMessage(error)
17898
+ });
17899
+ }
17911
17900
  log.info("web: loaded persisted sessions", { count: this.sessions.size });
17912
17901
  }
17913
- async createSession() {
17902
+ /** Server process directory: the default workspace when session creation omits workDir. */
17903
+ get defaultWorkDir() {
17904
+ return this.workDir;
17905
+ }
17906
+ /**
17907
+ * Create a session; an omitted workDir reuses the server process directory
17908
+ * and is passed to the harness as-is.
17909
+ */
17910
+ async createSession(workDir) {
17911
+ const dir = workDir ?? this.workDir;
17914
17912
  const session = await this.harness.createSession({
17915
- workDir: this.workDir,
17913
+ workDir: dir,
17916
17914
  model: this.model,
17917
17915
  permission: this.permission
17918
17916
  });
@@ -17920,7 +17918,7 @@ var SessionManager = class {
17920
17918
  const createdAt = Date.now();
17921
17919
  const webSession = new WebSession(session, {
17922
17920
  sessionId,
17923
- workDir: this.workDir,
17921
+ workDir: dir,
17924
17922
  permission: this.permission,
17925
17923
  yolo: this.yolo,
17926
17924
  homeDir: this.homeDir,
@@ -17937,7 +17935,7 @@ var SessionManager = class {
17937
17935
  this.sessions.set(sessionId, webSession);
17938
17936
  log.info("web: session created", {
17939
17937
  sessionId,
17940
- workDir: this.workDir
17938
+ workDir: dir
17941
17939
  });
17942
17940
  return webSession;
17943
17941
  }
@@ -17982,8 +17980,19 @@ var SessionManager = class {
17982
17980
  coreSessionId: session.id,
17983
17981
  onFork: (id) => this.forkSession(id)
17984
17982
  });
17985
- const entries = await loadJournal(this.homeDir, sessionId);
17983
+ const { entries, corrupt } = await loadJournal(this.homeDir, sessionId);
17986
17984
  reactivated.loadFromPersisted(entries);
17985
+ const journalHasContent = reactivated.hasAnyAssistantContent();
17986
+ if (corrupt > 0 && !journalHasContent) reactivated.markDamaged(`journal has ${corrupt} corrupt lines and no parsed message carries a body`);
17987
+ if (!entries.some(isMessageBearingEntry) || corrupt > 0 && !journalHasContent) try {
17988
+ const context = await session.getContext();
17989
+ reactivated.seedHistory(contextHistoryToChatMessages(context.history));
17990
+ } catch (error) {
17991
+ log.warn("web: seeding history from core transcript failed", {
17992
+ sessionId,
17993
+ error: error instanceof Error ? error.message : String(error)
17994
+ });
17995
+ }
17987
17996
  try {
17988
17997
  await reactivated.ready;
17989
17998
  } catch (error) {
@@ -18038,7 +18047,7 @@ var SessionManager = class {
18038
18047
  const title = `${source.getTitle()} (fork)`;
18039
18048
  const webSession = new WebSession(session, {
18040
18049
  sessionId: newId,
18041
- workDir: this.workDir,
18050
+ workDir: source.getMetadata().workDir,
18042
18051
  permission: source.getMetadata().permission,
18043
18052
  yolo: this.yolo,
18044
18053
  homeDir: this.homeDir,
@@ -18052,6 +18061,16 @@ var SessionManager = class {
18052
18061
  await writeFile(getJournalPath(this.homeDir, newId), lines);
18053
18062
  }
18054
18063
  webSession.loadFromPersisted(entries);
18064
+ if (!entries.some(isMessageBearingEntry)) try {
18065
+ const context = await session.getContext();
18066
+ webSession.seedHistory(contextHistoryToChatMessages(context.history));
18067
+ } catch (error) {
18068
+ log.warn("web: seeding forked history from core transcript failed", {
18069
+ sourceId,
18070
+ newId,
18071
+ error: errorMessage(error)
18072
+ });
18073
+ }
18055
18074
  try {
18056
18075
  await webSession.ready;
18057
18076
  } catch (error) {
@@ -18087,8 +18106,33 @@ var SessionManager = class {
18087
18106
  async delete(sessionId) {
18088
18107
  const ws = this.sessions.get(sessionId);
18089
18108
  if (!ws) return false;
18109
+ const workDir = ws.workDir;
18110
+ const coreId = ws.getCoreSessionId();
18090
18111
  await ws.delete();
18091
18112
  this.sessions.delete(sessionId);
18113
+ const coreDir = join(this.homeDir, "sessions", encodeWorkDirKey(workDir), coreId);
18114
+ let moved = true;
18115
+ try {
18116
+ if (existsSync(coreDir)) {
18117
+ const trashRoot = join(this.homeDir, "trash");
18118
+ await mkdir(trashRoot, { recursive: true });
18119
+ await rename(coreDir, join(trashRoot, `${coreId}-${Date.now()}`));
18120
+ } else log.warn("web: core session directory not found on delete", {
18121
+ sessionId,
18122
+ coreId,
18123
+ workDir,
18124
+ coreDir
18125
+ });
18126
+ } catch (error) {
18127
+ moved = false;
18128
+ log.warn("web: moving session directory to trash failed; index entry kept", {
18129
+ sessionId,
18130
+ coreId,
18131
+ coreDir,
18132
+ error: errorMessage(error)
18133
+ });
18134
+ }
18135
+ if (moved) await removeSessionIndexEntry(this.homeDir, coreId);
18092
18136
  return true;
18093
18137
  }
18094
18138
  /** List available model aliases from the harness config (reloaded). */
@@ -18831,17 +18875,18 @@ async function runWebServer(opts) {
18831
18875
  res.end(JSON.stringify(manager.list()));
18832
18876
  return;
18833
18877
  }
18878
+ if (url === `${API_PREFIX}/workdir` && method === "GET") {
18879
+ sendJson(res, 200, { workDir: manager.defaultWorkDir });
18880
+ return;
18881
+ }
18834
18882
  if (url === `${API_PREFIX}/sessions` && method === "POST") {
18835
18883
  try {
18836
- const ws = await manager.createSession();
18884
+ const dir = await validateSessionWorkDir((await readJsonBody(req))["workDir"]);
18885
+ const ws = await manager.createSession(dir);
18837
18886
  res.writeHead(201, { "Content-Type": "application/json" });
18838
18887
  res.end(JSON.stringify(ws.getListItem()));
18839
18888
  } catch (error) {
18840
- res.writeHead(500, { "Content-Type": "application/json" });
18841
- res.end(JSON.stringify({
18842
- code: 500,
18843
- message: String(error)
18844
- }));
18889
+ sendHttpError(res, error);
18845
18890
  }
18846
18891
  return;
18847
18892
  }
@@ -19137,7 +19182,10 @@ async function runWebServer(opts) {
19137
19182
  if (url === "/" || url === "/index.html") {
19138
19183
  try {
19139
19184
  const html = await readFile(join(publicDir, "index.html"), "utf-8");
19140
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
19185
+ res.writeHead(200, {
19186
+ "Content-Type": "text/html; charset=utf-8",
19187
+ "Cache-Control": "no-store"
19188
+ });
19141
19189
  res.end(html);
19142
19190
  } catch {
19143
19191
  res.writeHead(500, { "Content-Type": "text/plain" });
@@ -19150,7 +19198,9 @@ async function runWebServer(opts) {
19150
19198
  const filePath = join(publicDir, safeUrl);
19151
19199
  const contentType = contentTypes[filePath.split(".").pop() ?? ""] ?? "application/octet-stream";
19152
19200
  const data = await readFile(filePath);
19153
- res.writeHead(200, { "Content-Type": contentType });
19201
+ const headers = { "Content-Type": contentType };
19202
+ if (safeUrl.startsWith("/assets/")) headers["Cache-Control"] = "public, max-age=31536000, immutable";
19203
+ res.writeHead(200, headers);
19154
19204
  res.end(data);
19155
19205
  } catch {
19156
19206
  res.writeHead(404, { "Content-Type": "text/plain" });