scream-code 0.16.1 → 0.16.2

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-V7s7I7Nr.mjs} +576 -413
  3. package/dist/{dispatch-B6jbxY0D.mjs → dispatch-BRkUiHzo.mjs} +1 -1
  4. package/dist/{dispatch-DqeJdQZQ.mjs → dispatch-D0mYcf1X.mjs} +1172 -230
  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-BABWY9vy.mjs → text-input-dialog-CzJKBIoN.mjs} +40 -12
  95. package/dist/{text-input-dialog-rSkH3JTV.mjs → text-input-dialog-Dp2TUSjQ.mjs} +1 -1
  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 { $ as getBreathingFrame, $n as encodeWorkDirKey, $t as isStreaming, A as ToolCallComponent, An as setExperimentalFlags, B as sanitizeShellOutput, Bn as detectShellEnvironment, Bt as DISABLE_TERMINAL_THEME_REPORTING, Cn as getCtrlDHint, D as isEmptySessionHintDismissed, Dn as BUILTIN_SLASH_COMMANDS, Dt as supportsBalance, E as UserMessageComponent, En as buildSkillSlashCommands, Et as refreshProviderBalance, F as CachedContainer, Fn as PULSE_WAVE_FRAMES, Ft as contrastTextHex, G as AssistantMessageComponent, Gn as CLI_COMMAND_NAME, Gt as OSC11_RESPONSE_PREFIX, H as ReadGroupComponent, Hn as getInputHistoryFile, Ht as ENABLE_TERMINAL_THEME_REPORTING, I as ThinkingComponent, In as TuiConfigParseError, It as getColorPalette, J as estimateTokens, Jn as PRODUCT_NAME, Jt as TERMINAL_FOCUS_IN, K as ActivityGroupComponent, Kn as CLI_UI_MODE, Kt as OSC11_RESPONSE_PREFIX_NO_ESC, L as NoticeMessageComponent, Ln as TuiLikePreferencesSchema, Lt as detectTerminalTheme, M as renderDiffLinesClustered, Mn as isEmbeddingModelCached, Mt as createEditorTheme, N as highlightLines, Nn as startManualEmbeddingDownload, Nt as createMarkdownTheme, O as isTurnElapsedEnabled, On as sortSlashCommands, Ot as clearInfoPanelState, P as langFromPath, Pn as PIXEL_PULSE_FRAMES, Pt as createThemeStyles, Q as BREATHE_CYCLE_MS, Qn as ScreamHarness, Qt as isBusy, R as StatusMessageComponent, Rn as loadTuiConfig, Rt as parseOsc11BackgroundTheme, S as readJsonlFile, Sn as getCtrlCHint, Tn as getNoActiveSessionMessage, U as parseReadGroupOutput, Un as getLogDir, Ut as OSC11_QUERY, V as SkillActivationComponent, Vn as getDataDir, Vt as ENABLE_TERMINAL_FOCUS_REPORTING, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE, X as AgentGroupComponent, Xn as fetchCatalog, Xt as TERMINAL_THEME_DARK, Y as getSharedSpeedTracker, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_FOCUS_OUT, Z as WelcomeComponent, Zn as saveCatalogCache, Zt as TERMINAL_THEME_LIGHT, _n as EMPTY_SESSION_HINT_URL, _t as handleFusionPlanCommand, ar as resolveScreamHome, b as readUpdateCache, bn as SESSION_TIPS, c as hasDispose, cn as STATUS_BULLET, cr as log, d as readExpanded, dn as formatErrorMessage, dr as isOrphanedToolCallError, dt as changeThinkingLevel, en as FooterComponent, er as normalizeWorkDir, et as resetBreathingClock, f as MoonLoader, fn as isTodoItemShape, fr as ErrorCodes, ft as getModelCycleLevel, gn as truncateErrorMessage, hn as stringValue, i as buildRoleAdditionalText, j as renderDiffLines, jn as getKnowledgeStore, jt as resolveThemeSync, k as toggleEmptySessionHint, kn as isExperimentalFlagEnabled, l as isExpandable, ln as appendStreamingArgsPreview, lr as resolveGlobalLogPath, lt as isInternalMessage, mn as serializeToolResultOutput, n as clearEvalPanelState, nn as SubagentSlots, nr as readSessionIndex, nt as clearGoalState, or as MemoryMemoStore, p as formatMemoryMemoForInjection, pn as parseStreamingArgs, pr as SCREAM_ERROR_INFO, q as charsForTokenBudget, qn as CLI_USER_AGENT_PRODUCT, qt as QUERY_TERMINAL_THEME, r as openUrl, rr as removeSessionIndexEntry, rt as refineGoal, s as disposeChildren, sn as printableChar, sr as flushDiagnosticLogs, t as dispatchInput, tn as lerpGradient, tr as appendSessionIndexEntry, tt as CompactionComponent, u as isPlanExpandable, un as argsRecord, ur as isScreamError, ut as toTerminalHyperlink, v as selectUpdateTarget, vn as EXIT_CONFIRM_WINDOW_MS, w as getDaemonInstructions, wn as getLlmNotSetMessage, x as appendJsonlLine, xn as TIP_ROTATION_INTERVAL_MS, y as refreshUpdateCache, yn as MAIN_AGENT_ID, yt as handlePlanCommand, z as replaceTabs, zn as saveTuiConfig, zt as DISABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-D0mYcf1X.mjs";
8
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CzJKBIoN.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.2"),
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;
@@ -4691,6 +4356,30 @@ var StreamingUIController = class {
4691
4356
  _pendingReadGroupComponents = /* @__PURE__ */ new Map();
4692
4357
  _pendingAgentGroup = null;
4693
4358
  _pendingReadGroup = null;
4359
+ _activityGroup;
4360
+ /** Guards the single `registerLiveComponent` call for the turn's block. */
4361
+ _activityGroupRegistered = false;
4362
+ /** Block that borrowed a card, so late results refresh their own block. */
4363
+ _groupByToolCard = /* @__PURE__ */ new WeakMap();
4364
+ /** Reasoning of the turn's earlier steps, kept when a step's draft resets. */
4365
+ _turnThinkingHistory = "";
4366
+ _lastThinkingText = "";
4367
+ /**
4368
+ * Tools that keep their own transcript card instead of joining the turn's
4369
+ * activity block:
4370
+ * - `Read` / `Agent` are owned by the read/agent group machinery (and by
4371
+ * `/revoke`, which walks those groups);
4372
+ * - `AskUserQuestion` returns early above and mounts its finished card;
4373
+ * - `ExitPlanMode` owns the plan box, which Ctrl+E expands by walking the
4374
+ * transcript container's direct children.
4375
+ */
4376
+ static STANDALONE_TOOL_NAMES = new Set([
4377
+ "Read",
4378
+ "Agent",
4379
+ "AskUserQuestion",
4380
+ "ExitPlanMode",
4381
+ "ReadMediaFile"
4382
+ ]);
4694
4383
  constructor(host) {
4695
4384
  this.host = host;
4696
4385
  }
@@ -4903,6 +4592,7 @@ var StreamingUIController = class {
4903
4592
  }
4904
4593
  /** Tears down replay-specific state after session history has been rendered. */
4905
4594
  cleanupAfterReplay(completedToolCallIds) {
4595
+ this.endActivityGroup();
4906
4596
  this._activeToolCalls.clear();
4907
4597
  for (const toolCallId of completedToolCallIds) this._pendingToolComponents.delete(toolCallId);
4908
4598
  this._pendingAgentGroup = null;
@@ -4914,6 +4604,74 @@ var StreamingUIController = class {
4914
4604
  this.pendingToolCallFlushIds.clear();
4915
4605
  this.host.state.ui.requestRender();
4916
4606
  }
4607
+ /**
4608
+ * Returns the turn's block, creating it on demand: a block only ever starts
4609
+ * where work starts (reasoning or a tool call), which keeps it positioned
4610
+ * right after whatever the assistant last said.
4611
+ *
4612
+ * Live turns spin, replayed turns (history) do not — the streaming phase is
4613
+ * the discriminator, so a second block inside the same live turn still spins.
4614
+ */
4615
+ ensureActivityGroup() {
4616
+ const existing = this._activityGroup;
4617
+ if (existing !== void 0) return existing;
4618
+ const { state } = this.host;
4619
+ const group = new ActivityGroupComponent(state.theme.colors, state.ui);
4620
+ group.setRunning(state.appState.streamingPhase !== "idle");
4621
+ this._activityGroup = group;
4622
+ this._turnThinkingHistory = "";
4623
+ state.transcriptContainer.addChild(group);
4624
+ state.ui.requestRender();
4625
+ return group;
4626
+ }
4627
+ /**
4628
+ * Seals the current block as soon as the assistant starts writing visible
4629
+ * text, so every stretch of work stays next to the answer it produced instead
4630
+ * of the whole turn's process piling up above the prose.
4631
+ */
4632
+ sealActivityGroupOnText(text) {
4633
+ if (this._activityGroup === void 0) return;
4634
+ if (text.trim().length === 0) return;
4635
+ this.endActivityGroup();
4636
+ }
4637
+ /**
4638
+ * Registers the block against the first entry it owns, so history folding can
4639
+ * later sweep the whole block as one entry. Marked pending so a growing block
4640
+ * is never folded mid-turn.
4641
+ */
4642
+ registerActivityGroupEntry(group, entry) {
4643
+ if (this._activityGroupRegistered) return;
4644
+ this._activityGroupRegistered = true;
4645
+ this.host.transcriptController.registerLiveComponent(group, entry);
4646
+ this.host.transcriptController.markPending(group);
4647
+ }
4648
+ /**
4649
+ * Settles the block: stops the spinner, drops one that never got content, and
4650
+ * releases the reference so the next turn opens a fresh one. Blocks with
4651
+ * content stay mounted (replayed history keeps its blocks).
4652
+ */
4653
+ endActivityGroup() {
4654
+ const group = this._activityGroup;
4655
+ this._activityGroup = void 0;
4656
+ this._activityGroupRegistered = false;
4657
+ this._turnThinkingHistory = "";
4658
+ this._lastThinkingText = "";
4659
+ if (group === void 0) return;
4660
+ group.setRunning(false);
4661
+ if (group.isEmpty()) {
4662
+ this.host.transcriptController.unmarkPending(group);
4663
+ this.host.state.transcriptContainer.removeChild(group);
4664
+ group.dispose();
4665
+ return;
4666
+ }
4667
+ this.host.transcriptController.unmarkPending(group);
4668
+ group.dispose();
4669
+ }
4670
+ /** Reasoning text of the whole turn: earlier steps plus the current draft. */
4671
+ activityThinkingText(draft) {
4672
+ if (this._turnThinkingHistory.length === 0) return draft;
4673
+ return `${this._turnThinkingHistory}\n${draft}`;
4674
+ }
4917
4675
  disposeActiveThinkingComponent() {
4918
4676
  if (this._activeThinkingComponent !== void 0) {
4919
4677
  this._activeThinkingComponent.dispose();
@@ -4988,7 +4746,9 @@ var StreamingUIController = class {
4988
4746
  const draft = this._assistantDraft;
4989
4747
  if (this._shownAssistantLength >= draft.length) return this._shownAssistantLength;
4990
4748
  const measured = getSharedSpeedTracker().getSpeed();
4991
- const budget = Math.min(25, Math.max(1, Math.ceil((measured > 0 ? measured : 50) * (50 / 1e3) * CHARS_PER_TOKEN)));
4749
+ const tokPerSec = measured > 0 ? measured : 50;
4750
+ const requested = charsForTokenBudget(draft.slice(this._shownAssistantLength), tokPerSec * (50 / 1e3));
4751
+ const budget = Math.min(25, Math.max(1, requested));
4992
4752
  this._shownAssistantLength = Math.min(draft.length, this._shownAssistantLength + budget);
4993
4753
  return this._shownAssistantLength;
4994
4754
  }
@@ -5052,6 +4812,7 @@ var StreamingUIController = class {
5052
4812
  const turnStartTime = this.turnStartAt !== 0 ? this.turnStartAt : state.appState.streamingStartTime;
5053
4813
  const completedTurnKey = this._currentTurnId ?? `local:${String(turnStartTime)}`;
5054
4814
  this.finalizeLiveTextBuffers();
4815
+ this.endActivityGroup();
5055
4816
  this.resetToolCallState();
5056
4817
  this._currentTurnId = void 0;
5057
4818
  const next = this.host.shiftQueuedMessage();
@@ -5125,6 +4886,7 @@ var StreamingUIController = class {
5125
4886
  block.component.updateContent(fullText);
5126
4887
  this.host.state.ui.requestRender();
5127
4888
  }
4889
+ this.sealActivityGroupOnText(fullText);
5128
4890
  }
5129
4891
  onStreamingTextEnd() {
5130
4892
  const block = this._streamingBlock;
@@ -5140,7 +4902,7 @@ var StreamingUIController = class {
5140
4902
  if (this._activeThinkingComponent === void 0) {
5141
4903
  this._pendingAgentGroup = null;
5142
4904
  this._pendingReadGroup = null;
5143
- const component = new ThinkingComponent(fullText, state.theme.colors, true, "live", state.ui);
4905
+ const component = new ThinkingComponent(fullText, state.theme.colors, true, "finalized", state.ui);
5144
4906
  const entry = {
5145
4907
  id: nextTranscriptId(),
5146
4908
  kind: "thinking",
@@ -5153,21 +4915,30 @@ var StreamingUIController = class {
5153
4915
  this.host.pushTranscriptEntry(entry);
5154
4916
  this.host.transcriptController.registerLiveComponent(component, entry);
5155
4917
  this.host.transcriptController.markPending(component);
5156
- if (state.toolOutputExpanded) component.setExpanded(true);
5157
- state.transcriptContainer.addChild(component);
4918
+ const group = this.ensureActivityGroup();
4919
+ this.registerActivityGroupEntry(group, entry);
4920
+ this._lastThinkingText = fullText;
4921
+ group.setThinking(this.activityThinkingText(fullText), true);
5158
4922
  } else {
5159
4923
  this._activeThinkingComponent.setText(fullText);
5160
4924
  if (this._thinkingEntry !== void 0) this._thinkingEntry.content = fullText;
4925
+ this._lastThinkingText = fullText;
4926
+ this.ensureActivityGroup().setThinking(this.activityThinkingText(fullText), true);
5161
4927
  }
5162
4928
  state.ui.requestRender();
5163
4929
  }
5164
4930
  onThinkingEnd() {
5165
4931
  if (this._activeThinkingComponent === void 0) return;
5166
- this._activeThinkingComponent.finalize();
5167
- this.host.transcriptController.unmarkPending(this._activeThinkingComponent);
4932
+ const component = this._activeThinkingComponent;
4933
+ component.finalize();
4934
+ this.host.transcriptController.unmarkPending(component);
5168
4935
  this.host.transcriptController.commit();
4936
+ this.host.transcriptController.releaseLiveComponent(component);
5169
4937
  this._activeThinkingComponent = void 0;
5170
4938
  this._thinkingEntry = void 0;
4939
+ if (this._lastThinkingText.trim().length > 0) this._turnThinkingHistory = this.activityThinkingText(this._lastThinkingText);
4940
+ this._lastThinkingText = "";
4941
+ this._activityGroup?.endThinking();
5171
4942
  this.host.state.ui.requestRender();
5172
4943
  }
5173
4944
  onToolCallStart(toolCall) {
@@ -5191,15 +4962,20 @@ var StreamingUIController = class {
5191
4962
  this.host.pushTranscriptEntry(entry);
5192
4963
  this.host.transcriptController.registerLiveComponent(tc, entry);
5193
4964
  this.host.transcriptController.markPending(tc);
5194
- if (state.toolOutputExpanded) tc.setExpanded(true);
5195
4965
  if (state.planExpanded) tc.setPlanExpanded(true);
5196
4966
  if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
5197
4967
  if (toolCall.name !== "Read") this._pendingReadGroup = null;
5198
4968
  let handled = this.tryAttachAgentToolCall(toolCall, tc);
5199
4969
  if (!handled) handled = this.tryAttachReadToolCall(toolCall, tc);
5200
- if (!handled) {
4970
+ if (!handled) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
5201
4971
  state.transcriptContainer.addChild(tc);
5202
4972
  state.ui.requestRender();
4973
+ } else {
4974
+ const group = this.ensureActivityGroup();
4975
+ this.registerActivityGroupEntry(group, entry);
4976
+ this._groupByToolCard.set(tc, group);
4977
+ group.attachTool(tc, toolCall.step ?? this._currentStep);
4978
+ state.ui.requestRender();
5203
4979
  }
5204
4980
  if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
5205
4981
  const session = this.host.requireSession();
@@ -5254,6 +5030,9 @@ var StreamingUIController = class {
5254
5030
  if (tc) {
5255
5031
  tc.setResult(result);
5256
5032
  tc.dispose();
5033
+ const owner = this._groupByToolCard.get(tc) ?? this._activityGroup;
5034
+ owner?.notifyToolChanged();
5035
+ if (owner !== void 0 && this._groupByToolCard.has(tc)) this.host.transcriptController.releaseLiveComponent(tc);
5257
5036
  this.host.transcriptController.unmarkPending(tc);
5258
5037
  this._pendingToolComponents.delete(toolCallId);
5259
5038
  const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
@@ -5265,7 +5044,6 @@ var StreamingUIController = class {
5265
5044
  if (matchedCall?.name === "AskUserQuestion") {
5266
5045
  const completed = new ToolCallComponent(matchedCall, result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
5267
5046
  completed.setPermissionMode(state.appState.permissionMode);
5268
- if (state.toolOutputExpanded) completed.setExpanded(true);
5269
5047
  if (state.planExpanded) completed.setPlanExpanded(true);
5270
5048
  const entry = {
5271
5049
  id: nextTranscriptId(),
@@ -6617,6 +6395,14 @@ var TranscriptController = class TranscriptController {
6617
6395
  registerLiveComponent(component, entry) {
6618
6396
  this.liveComponentToEntry.set(component, entry);
6619
6397
  }
6398
+ /**
6399
+ * Drops the entry mapping of a component that will never be folded into the
6400
+ * committed history (it is not a container child), so the map cannot retain
6401
+ * components — and the text they hold — for the life of the session.
6402
+ */
6403
+ releaseLiveComponent(component) {
6404
+ this.liveComponentToEntry.delete(component);
6405
+ }
6620
6406
  markPending(component) {
6621
6407
  this.pendingComponents.add(component);
6622
6408
  }
@@ -6684,11 +6470,7 @@ var TranscriptController = class TranscriptController {
6684
6470
  component.updateContent(entry.content);
6685
6471
  return component;
6686
6472
  }
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
- }
6473
+ case "thinking": return new ThinkingComponent(entry.content, state.theme.colors, true);
6692
6474
  case "tool_call":
6693
6475
  if (entry.toolCallData?.name === "ReadGroup" && entry.toolCallData.result !== void 0) {
6694
6476
  const rgc = new ReadGroupComponent(state.theme.colors, state.ui);
@@ -6697,7 +6479,6 @@ var TranscriptController = class TranscriptController {
6697
6479
  }
6698
6480
  if (entry.toolCallData) {
6699
6481
  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
6482
  if (state.planExpanded) tc.setPlanExpanded(true);
6702
6483
  return tc;
6703
6484
  }
@@ -6781,6 +6562,7 @@ var TranscriptController = class TranscriptController {
6781
6562
  clearAndRedraw() {
6782
6563
  const { state, streamingUI, imageStore } = this.host;
6783
6564
  streamingUI.discardPending();
6565
+ streamingUI.endActivityGroup();
6784
6566
  state.transcriptEntries = [];
6785
6567
  streamingUI.disposeActiveCompactionBlock();
6786
6568
  streamingUI.resetLiveText();
@@ -6837,6 +6619,18 @@ var TranscriptController = class TranscriptController {
6837
6619
  }
6838
6620
  toggleToolOutputExpansion() {
6839
6621
  const { state } = this.host;
6622
+ const children = state.transcriptContainer.children;
6623
+ for (let i = children.length - 1; i >= 0; i -= 1) {
6624
+ const child = children[i];
6625
+ if (child === void 0) continue;
6626
+ if (isExpandable(child)) {
6627
+ state.toolOutputExpanded = !(readExpanded(child) ?? state.toolOutputExpanded);
6628
+ child.setExpanded(state.toolOutputExpanded);
6629
+ state.ui.requestRender();
6630
+ return;
6631
+ }
6632
+ if (child instanceof ReadGroupComponent || child instanceof AgentGroupComponent) break;
6633
+ }
6840
6634
  state.toolOutputExpanded = !state.toolOutputExpanded;
6841
6635
  state.ui.requestRender();
6842
6636
  }
@@ -7058,17 +6852,14 @@ function formatCount$1(n) {
7058
6852
  return n.toLocaleString("en-US");
7059
6853
  }
7060
6854
  /**
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.
6855
+ * Compact token form for narrow sidebars: 2,267,680,102 → `2267.7M`. One unit
6856
+ * ladder for every locale (K below a million, M above) so the value column
6857
+ * stays comparable across languages: Chinese no longer switches to the
6858
+ * ten-thousand / hundred-million units it used before.
6859
+ * Used only when the thousands-separated form cannot fit, so a narrow sidebar
6860
+ * keeps every value readable instead of cutting digits.
7064
6861
  */
7065
6862
  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
6863
  if (n >= 999500) return `${(n / 1e6).toFixed(1)}M`;
7073
6864
  if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
7074
6865
  return String(n);
@@ -7136,6 +6927,10 @@ const sessionPanel = {
7136
6927
  * with the 1s sidebar heartbeat each tick advances half the cycle, so the
7137
6928
  * colour jump is clearly visible instead of drifting imperceptibly. */
7138
6929
  const AGENT_GRADIENT_CYCLE_MS = 2e3;
6930
+ /** Display form of a slot type: 'coder' reads as 'Coder' in the panel. */
6931
+ function displayType(type) {
6932
+ return type.length === 0 ? type : type.charAt(0).toUpperCase() + type.slice(1);
6933
+ }
7139
6934
  /** Localized status word per slot state. */
7140
6935
  function statusText(status) {
7141
6936
  switch (status) {
@@ -7165,7 +6960,7 @@ var AgentsPanelContent = class {
7165
6960
  if (agents === void 0 || agents.length === 0) return [];
7166
6961
  return layoutSidebarGrid(agents.map((slot) => ({
7167
6962
  marker: slot.status === "idle" ? "○" : "●",
7168
- label: slot.type,
6963
+ label: displayType(slot.type),
7169
6964
  value: statusText(slot.status) + (slot.count > 1 ? ` ×${slot.count}` : "")
7170
6965
  })), width).map((cell, index) => this.renderSlot(cell, agents[index], index, width));
7171
6966
  }
@@ -7321,20 +7116,23 @@ const HUB_MODEL_STALE_MS = 5 * 6e4;
7321
7116
  const HUB_ENDPOINTS = Object.freeze([
7322
7117
  {
7323
7118
  id: "github",
7324
- url: "https://github.com/"
7119
+ url: "https://github.com/",
7120
+ label: "Github"
7325
7121
  },
7326
7122
  {
7327
7123
  id: "google",
7328
- url: "https://www.google.com/"
7124
+ url: "https://www.google.com/",
7125
+ label: "Google"
7329
7126
  },
7330
7127
  {
7331
7128
  id: "x",
7332
7129
  url: "https://x.com/",
7333
- label: "x (twitter)"
7130
+ label: "X (Twitter)"
7334
7131
  },
7335
7132
  {
7336
7133
  id: "huggingface",
7337
- url: "https://huggingface.co/"
7134
+ url: "https://huggingface.co/",
7135
+ label: "Huggingface"
7338
7136
  },
7339
7137
  {
7340
7138
  id: "alibaba",
@@ -13977,6 +13775,7 @@ var ScreamTUI = class {
13977
13775
  beginSessionRequest() {
13978
13776
  this.streamingUI.markTurnStarted();
13979
13777
  this.streamingUI.setTurnId(void 0);
13778
+ this.streamingUI.endActivityGroup();
13980
13779
  this.streamingUI.resetLiveText();
13981
13780
  this.streamingUI.resetToolUi();
13982
13781
  this.streamingUI.resetToolCallState();
@@ -13988,6 +13787,7 @@ var ScreamTUI = class {
13988
13787
  }
13989
13788
  failSessionRequest(message) {
13990
13789
  this.setAppState({ streamingPhase: "idle" });
13790
+ this.streamingUI.endActivityGroup();
13991
13791
  this.resetLivePane();
13992
13792
  this.showError(message);
13993
13793
  }
@@ -14195,7 +13995,7 @@ var ScreamTUI = class {
14195
13995
  const card = new OnboardingCardComponent({
14196
13996
  colors: this.state.theme.colors,
14197
13997
  onConfigure: () => {
14198
- import("./dispatch-B6jbxY0D.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
13998
+ import("./dispatch-BRkUiHzo.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14199
13999
  this.restoreEditor();
14200
14000
  onDone?.();
14201
14001
  });
@@ -15864,6 +15664,53 @@ async function persistKey(filePath, record) {
15864
15664
  await rename(tmp, filePath);
15865
15665
  }
15866
15666
  //#endregion
15667
+ //#region src/web/journal-writer.ts
15668
+ /**
15669
+ * Serialization gate for journal appends.
15670
+ *
15671
+ * Why it exists (an incident observed in the field): the session journal is an
15672
+ * append-only file holding one JSON record per line, and lines used to be
15673
+ * appended with `writeFile(path, line + '\n', { flag: 'a' })`. A
15674
+ * `web.message.finalized` entry carries an entire assistant body (hundreds of
15675
+ * thousands of characters in practice) while small entries such as turn.ended
15676
+ * are written at the same time — the two writes interleaved at the byte level,
15677
+ * so what landed on disk were malformed lines with the two records spliced
15678
+ * into each other:
15679
+ *
15680
+ * ..."content":"...{"type":"journal","seq":38354,... ← another record spliced into the body
15681
+ *
15682
+ * Consequence: the whole line fails JSON.parse and the message disappears from
15683
+ * the UI (the reader can only skip it), which the user perceives as a garbled
15684
+ * or lost body. Queueing the writes inside the process eliminates the
15685
+ * interleaving; the append itself still goes through appendFile (O_APPEND), so
15686
+ * a single write is never split by another fd.
15687
+ *
15688
+ * The queue is keyed by file path and dropped once drained, so a long-running
15689
+ * process does not grow it without bound. No cross-process writer exists today
15690
+ * (only the web server writes the journal); if one appears, this needs a
15691
+ * file-lock based scheme instead.
15692
+ */
15693
+ const writeChains = /* @__PURE__ */ new Map();
15694
+ /**
15695
+ * Append one line (plus its newline) to the journal; concurrent calls for the
15696
+ * same file land in arrival order.
15697
+ */
15698
+ async function appendJournalLineSerialized(path, line, onError) {
15699
+ const next = (writeChains.get(path) ?? Promise.resolve()).catch(() => void 0).then(async () => {
15700
+ try {
15701
+ await appendFile(path, line + "\n");
15702
+ } catch (error) {
15703
+ onError(error);
15704
+ }
15705
+ });
15706
+ writeChains.set(path, next);
15707
+ try {
15708
+ await next;
15709
+ } finally {
15710
+ if (writeChains.get(path) === next) writeChains.delete(path);
15711
+ }
15712
+ }
15713
+ //#endregion
15867
15714
  //#region src/web/rateLimit.ts
15868
15715
  function createFixedWindowLimiter(opts) {
15869
15716
  const { windowMs, max } = opts;
@@ -16431,14 +16278,12 @@ async function ensureSessionsDir(homeDir) {
16431
16278
  if (!existsSync(dir)) await mkdir(dir, { recursive: true });
16432
16279
  }
16433
16280
  async function appendJournalLine(homeDir, sessionId, line) {
16434
- try {
16435
- await writeFile(getJournalPath(homeDir, sessionId), line + "\n", { flag: "a" });
16436
- } catch (error) {
16281
+ await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
16437
16282
  log.warn("web: failed to persist journal line", {
16438
16283
  sessionId,
16439
16284
  error: String(error)
16440
16285
  });
16441
- }
16286
+ });
16442
16287
  }
16443
16288
  let metaWriteSeq = 0;
16444
16289
  async function saveMetadata(homeDir, meta) {
@@ -16465,16 +16310,77 @@ async function loadMetadata(homeDir, sessionId) {
16465
16310
  async function loadJournal(homeDir, sessionId) {
16466
16311
  try {
16467
16312
  const lines = (await readFile(getJournalPath(homeDir, sessionId), "utf-8")).split("\n").filter((l) => l.trim().length > 0);
16468
- const results = [];
16313
+ const entries = [];
16314
+ let corrupt = 0;
16469
16315
  for (const line of lines) try {
16470
- results.push(JSON.parse(line));
16316
+ entries.push(JSON.parse(line));
16471
16317
  } catch {
16318
+ corrupt += 1;
16472
16319
  log.warn("web: skipping corrupt journal line", { sessionId });
16473
16320
  }
16474
- return results;
16321
+ return {
16322
+ entries,
16323
+ corrupt
16324
+ };
16475
16325
  } catch {
16476
- return [];
16326
+ return {
16327
+ entries: [],
16328
+ corrupt: 0
16329
+ };
16330
+ }
16331
+ }
16332
+ /**
16333
+ * Event families that mutate the main transcript: only the main agent's
16334
+ * (`agentId === 'main'`) events of these kinds may be applied, journaled and
16335
+ * broadcast. Subagents and background tasks each emit their own turn.started
16336
+ * plus hundreds of text/tool events — mixing them into the main transcript
16337
+ * produces a body where two streams alternate block by block, blows the tool
16338
+ * list up to 97 entries and the model badge to 4 lines (reproduced from a real
16339
+ * session). Subagent presence is carried by subagent.* events instead, never by
16340
+ * the transcript.
16341
+ */
16342
+ const MAIN_TRANSCRIPT_EVENTS = new Set([
16343
+ "turn.started",
16344
+ "turn.ended",
16345
+ "assistant.delta",
16346
+ "thinking.delta",
16347
+ "tool.call.started",
16348
+ "tool.result"
16349
+ ]);
16350
+ /**
16351
+ * Whether a journal entry already carries a message (if so it needs no seeding
16352
+ * from core history).
16353
+ */
16354
+ function isMessageBearingEntry(entry) {
16355
+ if ("type" in entry && entry.type === "user_message") return true;
16356
+ const payload = entry.payload;
16357
+ return payload.type === "web.message.finalized" || payload.type === "turn.started";
16358
+ }
16359
+ /**
16360
+ * Core context history → web ChatMessage seed.
16361
+ *
16362
+ * Only "real user input + assistant bodies" are carried over: core tool records
16363
+ * and the web-side ToolMessage are different shapes, so a forced mapping would
16364
+ * invent fake cards. Messages with a non-user origin (injections, system
16365
+ * triggers, background-task notices, skill-activation envelopes, compaction
16366
+ * summaries) are protocol content meant for the model and must not be rendered
16367
+ * as user turns.
16368
+ */
16369
+ function contextHistoryToChatMessages(history) {
16370
+ const out = [];
16371
+ for (const message of history) {
16372
+ if (message.role !== "user" && message.role !== "assistant") continue;
16373
+ if (isInternalMessage(message)) continue;
16374
+ if (message.origin !== void 0 && message.origin.kind !== "user") continue;
16375
+ const text = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
16376
+ if (text.length === 0) continue;
16377
+ out.push({
16378
+ role: message.role,
16379
+ content: text,
16380
+ tools: []
16381
+ });
16477
16382
  }
16383
+ return out;
16478
16384
  }
16479
16385
  async function listPersistedSessions(homeDir) {
16480
16386
  const dir = getSessionsDir(homeDir);
@@ -16653,10 +16559,11 @@ const MAX_INLINE_DIFF_BYTES = 512 * 1024;
16653
16559
  * Single-file patch. `repoPath` is **repository-root relative** — the same base
16654
16560
  * `git status` reports, so the UI can hand it straight back.
16655
16561
  *
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.
16562
+ * Two cases used to return an empty patch, which read as "clicking did
16563
+ * nothing": nested files (the old code re-prefixed workdir-relative paths with
16564
+ * `../..`, resolving them outside the repo) and untracked files (`git diff` has
16565
+ * nothing to say about them). The second case now gets a synthesised added-file
16566
+ * patch.
16660
16567
  */
16661
16568
  async function getGitFileDiff(workDir, repoPath) {
16662
16569
  const rel = normalizeNumstatPath(repoPath);
@@ -16774,7 +16681,27 @@ var WebSession = class {
16774
16681
  * snapshot (`web.message.finalized`) — the only source that survives a
16775
16682
  * server restart (deltas are volatile by design).
16776
16683
  */
16777
- liveAssistant = null;
16684
+ /**
16685
+ * In-flight main-agent turn messages, keyed by `turnId`.
16686
+ * A single pointer (the old liveAssistant) merged several streams into one
16687
+ * message when an interrupted turn overlapped a freshly started one, or when
16688
+ * subagents ran concurrently, producing interleaved garbage — hence one slot
16689
+ * per turn.
16690
+ */
16691
+ liveAssistants = /* @__PURE__ */ new Map();
16692
+ /**
16693
+ * In-flight message of one turn; without a turnId it falls back to the most
16694
+ * recently started one (older-frame compatibility).
16695
+ */
16696
+ liveFor(turnId) {
16697
+ if (typeof turnId === "number") {
16698
+ const hit = this.liveAssistants.get(turnId);
16699
+ if (hit) return hit;
16700
+ }
16701
+ let latest = null;
16702
+ for (const message of this.liveAssistants.values()) latest = message;
16703
+ return latest;
16704
+ }
16778
16705
  liveTurnCount = 0;
16779
16706
  /** pendingMsgId awaiting compaction completion/cancellation event. */
16780
16707
  pendingCompactionMsgId = null;
@@ -16888,6 +16815,63 @@ var WebSession = class {
16888
16815
  const msgs = this.buildMessages();
16889
16816
  if (msgs.length > 0 && !this.customTitle) this.title = deriveTitle(msgs);
16890
16817
  }
16818
+ /**
16819
+ * Core wire history seed: filled in once by the activation path when the
16820
+ * journal holds no message records.
16821
+ */
16822
+ seededMessages = [];
16823
+ /**
16824
+ * Mark the journal untrustworthy (byte-interleaving damage, see
16825
+ * journal-writer.ts).
16826
+ *
16827
+ * The loaded journal entries and any legacy userMessages are dropped so the
16828
+ * message list is presented from the core transcript seed instead; state
16829
+ * snapshots (goal/todo, …) were already cached by loadFromPersisted and are
16830
+ * unaffected. nextSeq does not roll back: live entries keep appending and
16831
+ * rendering as usual after activation, so new messages are untouched (a seq
16832
+ * gap on the client takes the existing gap → snapshot refetch path).
16833
+ */
16834
+ markDamaged(reason) {
16835
+ const droppedEntries = this.journal.length;
16836
+ const droppedUsers = this.userMessages.length;
16837
+ this.journal.length = 0;
16838
+ this.userMessages.length = 0;
16839
+ log.warn("web: session journal damaged; messages rebuilt from core transcript", {
16840
+ sessionId: this.sessionId,
16841
+ reason,
16842
+ droppedEntries,
16843
+ droppedUsers
16844
+ });
16845
+ }
16846
+ /**
16847
+ * Whether the current message list holds at least one assistant message with
16848
+ * a body. Used to decide how far journal remnants can be trusted: user
16849
+ * messages are persisted separately (they always carry text), while the
16850
+ * byte-interleaving damage only ever ate assistant bodies — so only the
16851
+ * assistant side is inspected.
16852
+ */
16853
+ hasAnyAssistantContent() {
16854
+ return this.buildMessages().some((message) => message.role === "assistant" && message.content.trim().length > 0);
16855
+ }
16856
+ /**
16857
+ * Seed messages from the core session history. An older session's web
16858
+ * journal may hold nothing but state entries (or predate durable snapshots),
16859
+ * which leaves the chat area empty after a resume while the wire history is
16860
+ * intact. The seed occupies seq 1..N and lifts nextSeq past it, keeping seq
16861
+ * monotonic with live events and consistent with the getMessagesOlder cursor.
16862
+ */
16863
+ seedHistory(messages) {
16864
+ if (messages.length === 0 || this.seededMessages.length > 0) return;
16865
+ let seq = 1;
16866
+ for (const message of messages) {
16867
+ this.seededMessages.push({
16868
+ ...message,
16869
+ seq
16870
+ });
16871
+ seq += 1;
16872
+ }
16873
+ if (seq - 1 >= this.nextSeq) this.nextSeq = seq;
16874
+ }
16891
16875
  async updateTitle() {
16892
16876
  if (this.customTitle) return;
16893
16877
  const newTitle = deriveTitle(this.buildMessages());
@@ -16939,11 +16923,12 @@ var WebSession = class {
16939
16923
  if (!this.session) return;
16940
16924
  this.unsubscribe = this.session.onEvent((event) => {
16941
16925
  if ((event.type === "goal.updated" || event.type === "todo.updated") && event.agentId !== "main") return;
16926
+ if (MAIN_TRANSCRIPT_EVENTS.has(event.type) && event.agentId !== "main") return;
16942
16927
  if (event.type === "turn.started") {
16943
16928
  this.busy = true;
16944
16929
  this.activeTurns += 1;
16945
16930
  this.liveTurnCount += 1;
16946
- this.liveAssistant = {
16931
+ this.liveAssistants.set(event.turnId, {
16947
16932
  role: "assistant",
16948
16933
  content: "",
16949
16934
  tools: [],
@@ -16959,11 +16944,11 @@ var WebSession = class {
16959
16944
  tokens: null,
16960
16945
  tokensPerSec: null
16961
16946
  }
16962
- };
16947
+ });
16963
16948
  } else if (event.type === "turn.ended") {
16964
16949
  this.activeTurns = Math.max(0, this.activeTurns - 1);
16965
16950
  this.busy = this.activeTurns > 0;
16966
- const live = this.liveAssistant;
16951
+ const live = this.liveFor(event.turnId);
16967
16952
  if (event.reason === "failed" && live) live.isError = true;
16968
16953
  if (live) {
16969
16954
  live.turnStats = {
@@ -16983,13 +16968,13 @@ var WebSession = class {
16983
16968
  message: live
16984
16969
  });
16985
16970
  }
16986
- this.liveAssistant = null;
16971
+ this.liveAssistants.delete(event.turnId);
16987
16972
  this.trackPending(this.updateTitle());
16988
16973
  } else if (event.type === "assistant.delta") {
16989
- const live = this.liveAssistant;
16974
+ const live = this.liveFor(event.turnId);
16990
16975
  if (live) live.content += event.delta;
16991
16976
  } else if (event.type === "thinking.delta") {
16992
- const live = this.liveAssistant;
16977
+ const live = this.liveFor(event.turnId);
16993
16978
  if (live) {
16994
16979
  const t = live.tools.find((x) => x.name === "thinking");
16995
16980
  if (t) t.output = (t.output ?? "") + event.delta;
@@ -17001,7 +16986,7 @@ var WebSession = class {
17001
16986
  });
17002
16987
  }
17003
16988
  } else if (event.type === "tool.call.started") {
17004
- const live = this.liveAssistant;
16989
+ const live = this.liveFor(event.turnId);
17005
16990
  if (live) {
17006
16991
  live.tools.push({
17007
16992
  toolCallId: event.toolCallId,
@@ -17011,7 +16996,7 @@ var WebSession = class {
17011
16996
  if (live.turnStats) live.turnStats.step += 1;
17012
16997
  }
17013
16998
  } else if (event.type === "tool.result") {
17014
- const live = this.liveAssistant;
16999
+ const live = this.liveFor(event.turnId);
17015
17000
  if (live) {
17016
17001
  const tool = live.tools.find((t) => t.toolCallId === event.toolCallId);
17017
17002
  if (tool) {
@@ -17492,7 +17477,7 @@ var WebSession = class {
17492
17477
  workDir: this.workDir,
17493
17478
  title: this.title,
17494
17479
  createdAt: this.createdAt,
17495
- messageCount: this.buildMessages().length,
17480
+ messageCount: this.isActive ? this.buildMessages().length : -1,
17496
17481
  active: this.isActive
17497
17482
  };
17498
17483
  }
@@ -17686,7 +17671,7 @@ var WebSession = class {
17686
17671
  return this.connections.size;
17687
17672
  }
17688
17673
  buildMessages() {
17689
- const messages = [];
17674
+ const messages = [...this.seededMessages];
17690
17675
  let currentAssistant = null;
17691
17676
  let userIndex = 0;
17692
17677
  let turnCount = 0;
@@ -17872,6 +17857,36 @@ async function handleGoalRoute(req, res, url, resolveSession) {
17872
17857
  }
17873
17858
  return true;
17874
17859
  }
17860
+ /**
17861
+ * Validate the optional workDir argument of session creation; undefined means
17862
+ * "reuse the server process directory".
17863
+ *
17864
+ * Trade-off: web is a local tool and the browser shares the CLI's trust level,
17865
+ * so no directory allowlist is introduced; but only an explicitly provided
17866
+ * absolute path is accepted — relative paths and `..` segments are rejected
17867
+ * (resolving them away instead of rejecting would amount to permitting
17868
+ * traversal), and the target must exist, be a directory and be readable and
17869
+ * writable. An invalid directory fails loudly with a 4xx at the moment the
17870
+ * session is created, rather than letting the agent wander into some
17871
+ * unexpected location later.
17872
+ */
17873
+ async function validateSessionWorkDir(raw) {
17874
+ if (raw === void 0 || raw === null) return void 0;
17875
+ if (typeof raw !== "string") throw new HttpError(400, "workDir 必须是字符串", "request.invalid");
17876
+ const value = raw.trim();
17877
+ if (!value) return void 0;
17878
+ if (!isAbsolute(value)) throw new HttpError(400, `workDir 必须是绝对路径:${value}`, "request.invalid");
17879
+ if (value.split(/[\\/]+/).includes("..")) throw new HttpError(400, `workDir 不允许包含 ".." 遍历段:${value}`, "request.invalid");
17880
+ const st = await stat(value).catch(() => null);
17881
+ if (!st) throw new HttpError(404, `目录不存在:${value}`, "request.invalid");
17882
+ if (!st.isDirectory()) throw new HttpError(400, `不是目录:${value}`, "request.invalid");
17883
+ try {
17884
+ await access(value, constants.R_OK | constants.W_OK);
17885
+ } catch {
17886
+ throw new HttpError(403, `目录不可读写:${value}`, "request.invalid");
17887
+ }
17888
+ return value;
17889
+ }
17875
17890
  var SessionManager = class {
17876
17891
  sessions = /* @__PURE__ */ new Map();
17877
17892
  /** In-flight activation promises to deduplicate concurrent calls. */
@@ -17894,7 +17909,7 @@ var SessionManager = class {
17894
17909
  await ensureSessionsDir(this.homeDir);
17895
17910
  const metas = await listPersistedSessions(this.homeDir);
17896
17911
  for (const meta of metas) {
17897
- const entries = await loadJournal(this.homeDir, meta.sessionId);
17912
+ const { entries } = await loadJournal(this.homeDir, meta.sessionId);
17898
17913
  const ws = new WebSession(null, {
17899
17914
  sessionId: meta.sessionId,
17900
17915
  coreSessionId: meta.coreSessionId ?? meta.sessionId,
@@ -17908,11 +17923,107 @@ var SessionManager = class {
17908
17923
  ws.loadFromPersisted(entries);
17909
17924
  this.sessions.set(meta.sessionId, ws);
17910
17925
  }
17926
+ try {
17927
+ const index = await readSessionIndex(this.homeDir, join(this.homeDir, "sessions"));
17928
+ const managerWorkDir = normalizeWorkDir(this.workDir);
17929
+ for (const entry of index.values()) try {
17930
+ if (normalizeWorkDir(entry.workDir) !== managerWorkDir) continue;
17931
+ if (this.sessions.has(entry.sessionId)) continue;
17932
+ if (!existsSync(entry.sessionDir)) continue;
17933
+ let title = "未命名会话";
17934
+ let createdAt = Date.now();
17935
+ try {
17936
+ const raw = await readFile(join(entry.sessionDir, "state.json"), "utf8");
17937
+ const state = JSON.parse(raw);
17938
+ if (typeof state.title === "string" && state.title) title = state.title;
17939
+ if (state.createdAt) {
17940
+ const t = typeof state.createdAt === "number" ? state.createdAt : Date.parse(state.createdAt);
17941
+ if (Number.isFinite(t)) createdAt = t;
17942
+ }
17943
+ } catch {}
17944
+ const session = new WebSession(null, {
17945
+ sessionId: entry.sessionId,
17946
+ coreSessionId: entry.sessionId,
17947
+ workDir: entry.workDir,
17948
+ permission: this.permission,
17949
+ yolo: this.yolo,
17950
+ homeDir: this.homeDir,
17951
+ createdAt,
17952
+ title
17953
+ });
17954
+ this.sessions.set(entry.sessionId, session);
17955
+ } catch (error) {
17956
+ log.warn("web: skipping unreadable session index entry", {
17957
+ sessionId: entry.sessionId,
17958
+ sessionDir: entry.sessionDir,
17959
+ error: errorMessage(error)
17960
+ });
17961
+ }
17962
+ } catch (error) {
17963
+ log.warn("web: session index unreadable; falling back to web mirror + directory scan", {
17964
+ homeDir: this.homeDir,
17965
+ error: errorMessage(error)
17966
+ });
17967
+ }
17968
+ const wdKey = encodeWorkDirKey(this.workDir);
17969
+ const wdDir = join(this.homeDir, "sessions", wdKey);
17970
+ try {
17971
+ if (existsSync(wdDir)) for (const name of await readdir(wdDir)) {
17972
+ if (!name.startsWith("session_")) continue;
17973
+ if (this.sessions.has(name)) continue;
17974
+ try {
17975
+ const sessionDir = join(wdDir, name);
17976
+ let title = "未命名会话";
17977
+ let createdAt = Date.now();
17978
+ try {
17979
+ const raw = await readFile(join(sessionDir, "state.json"), "utf8");
17980
+ const state = JSON.parse(raw);
17981
+ if (typeof state.title === "string" && state.title) title = state.title;
17982
+ if (state.createdAt) {
17983
+ const t = typeof state.createdAt === "number" ? state.createdAt : Date.parse(state.createdAt);
17984
+ if (Number.isFinite(t)) createdAt = t;
17985
+ }
17986
+ } catch {}
17987
+ const session = new WebSession(null, {
17988
+ sessionId: name,
17989
+ coreSessionId: name,
17990
+ workDir: this.workDir,
17991
+ permission: this.permission,
17992
+ yolo: this.yolo,
17993
+ homeDir: this.homeDir,
17994
+ createdAt,
17995
+ title
17996
+ });
17997
+ this.sessions.set(name, session);
17998
+ } catch (error) {
17999
+ log.warn("web: skipping unreadable core session directory", {
18000
+ wdKey,
18001
+ sessionId: name,
18002
+ error: errorMessage(error)
18003
+ });
18004
+ }
18005
+ }
18006
+ } catch (error) {
18007
+ log.warn("web: core session bucket scan failed", {
18008
+ wdKey,
18009
+ dir: wdDir,
18010
+ error: errorMessage(error)
18011
+ });
18012
+ }
17911
18013
  log.info("web: loaded persisted sessions", { count: this.sessions.size });
17912
18014
  }
17913
- async createSession() {
18015
+ /** Server process directory: the default workspace when session creation omits workDir. */
18016
+ get defaultWorkDir() {
18017
+ return this.workDir;
18018
+ }
18019
+ /**
18020
+ * Create a session; an omitted workDir reuses the server process directory
18021
+ * and is passed to the harness as-is.
18022
+ */
18023
+ async createSession(workDir) {
18024
+ const dir = workDir ?? this.workDir;
17914
18025
  const session = await this.harness.createSession({
17915
- workDir: this.workDir,
18026
+ workDir: dir,
17916
18027
  model: this.model,
17917
18028
  permission: this.permission
17918
18029
  });
@@ -17920,7 +18031,7 @@ var SessionManager = class {
17920
18031
  const createdAt = Date.now();
17921
18032
  const webSession = new WebSession(session, {
17922
18033
  sessionId,
17923
- workDir: this.workDir,
18034
+ workDir: dir,
17924
18035
  permission: this.permission,
17925
18036
  yolo: this.yolo,
17926
18037
  homeDir: this.homeDir,
@@ -17937,7 +18048,7 @@ var SessionManager = class {
17937
18048
  this.sessions.set(sessionId, webSession);
17938
18049
  log.info("web: session created", {
17939
18050
  sessionId,
17940
- workDir: this.workDir
18051
+ workDir: dir
17941
18052
  });
17942
18053
  return webSession;
17943
18054
  }
@@ -17982,8 +18093,19 @@ var SessionManager = class {
17982
18093
  coreSessionId: session.id,
17983
18094
  onFork: (id) => this.forkSession(id)
17984
18095
  });
17985
- const entries = await loadJournal(this.homeDir, sessionId);
18096
+ const { entries, corrupt } = await loadJournal(this.homeDir, sessionId);
17986
18097
  reactivated.loadFromPersisted(entries);
18098
+ const journalHasContent = reactivated.hasAnyAssistantContent();
18099
+ if (corrupt > 0 && !journalHasContent) reactivated.markDamaged(`journal has ${corrupt} corrupt lines and no parsed message carries a body`);
18100
+ if (!entries.some(isMessageBearingEntry) || corrupt > 0 && !journalHasContent) try {
18101
+ const context = await session.getContext();
18102
+ reactivated.seedHistory(contextHistoryToChatMessages(context.history));
18103
+ } catch (error) {
18104
+ log.warn("web: seeding history from core transcript failed", {
18105
+ sessionId,
18106
+ error: error instanceof Error ? error.message : String(error)
18107
+ });
18108
+ }
17987
18109
  try {
17988
18110
  await reactivated.ready;
17989
18111
  } catch (error) {
@@ -18038,7 +18160,7 @@ var SessionManager = class {
18038
18160
  const title = `${source.getTitle()} (fork)`;
18039
18161
  const webSession = new WebSession(session, {
18040
18162
  sessionId: newId,
18041
- workDir: this.workDir,
18163
+ workDir: source.getMetadata().workDir,
18042
18164
  permission: source.getMetadata().permission,
18043
18165
  yolo: this.yolo,
18044
18166
  homeDir: this.homeDir,
@@ -18052,6 +18174,16 @@ var SessionManager = class {
18052
18174
  await writeFile(getJournalPath(this.homeDir, newId), lines);
18053
18175
  }
18054
18176
  webSession.loadFromPersisted(entries);
18177
+ if (!entries.some(isMessageBearingEntry)) try {
18178
+ const context = await session.getContext();
18179
+ webSession.seedHistory(contextHistoryToChatMessages(context.history));
18180
+ } catch (error) {
18181
+ log.warn("web: seeding forked history from core transcript failed", {
18182
+ sourceId,
18183
+ newId,
18184
+ error: errorMessage(error)
18185
+ });
18186
+ }
18055
18187
  try {
18056
18188
  await webSession.ready;
18057
18189
  } catch (error) {
@@ -18087,8 +18219,33 @@ var SessionManager = class {
18087
18219
  async delete(sessionId) {
18088
18220
  const ws = this.sessions.get(sessionId);
18089
18221
  if (!ws) return false;
18222
+ const workDir = ws.workDir;
18223
+ const coreId = ws.getCoreSessionId();
18090
18224
  await ws.delete();
18091
18225
  this.sessions.delete(sessionId);
18226
+ const coreDir = join(this.homeDir, "sessions", encodeWorkDirKey(workDir), coreId);
18227
+ let moved = true;
18228
+ try {
18229
+ if (existsSync(coreDir)) {
18230
+ const trashRoot = join(this.homeDir, "trash");
18231
+ await mkdir(trashRoot, { recursive: true });
18232
+ await rename(coreDir, join(trashRoot, `${coreId}-${Date.now()}`));
18233
+ } else log.warn("web: core session directory not found on delete", {
18234
+ sessionId,
18235
+ coreId,
18236
+ workDir,
18237
+ coreDir
18238
+ });
18239
+ } catch (error) {
18240
+ moved = false;
18241
+ log.warn("web: moving session directory to trash failed; index entry kept", {
18242
+ sessionId,
18243
+ coreId,
18244
+ coreDir,
18245
+ error: errorMessage(error)
18246
+ });
18247
+ }
18248
+ if (moved) await removeSessionIndexEntry(this.homeDir, coreId);
18092
18249
  return true;
18093
18250
  }
18094
18251
  /** List available model aliases from the harness config (reloaded). */
@@ -18831,17 +18988,18 @@ async function runWebServer(opts) {
18831
18988
  res.end(JSON.stringify(manager.list()));
18832
18989
  return;
18833
18990
  }
18991
+ if (url === `${API_PREFIX}/workdir` && method === "GET") {
18992
+ sendJson(res, 200, { workDir: manager.defaultWorkDir });
18993
+ return;
18994
+ }
18834
18995
  if (url === `${API_PREFIX}/sessions` && method === "POST") {
18835
18996
  try {
18836
- const ws = await manager.createSession();
18997
+ const dir = await validateSessionWorkDir((await readJsonBody(req))["workDir"]);
18998
+ const ws = await manager.createSession(dir);
18837
18999
  res.writeHead(201, { "Content-Type": "application/json" });
18838
19000
  res.end(JSON.stringify(ws.getListItem()));
18839
19001
  } catch (error) {
18840
- res.writeHead(500, { "Content-Type": "application/json" });
18841
- res.end(JSON.stringify({
18842
- code: 500,
18843
- message: String(error)
18844
- }));
19002
+ sendHttpError(res, error);
18845
19003
  }
18846
19004
  return;
18847
19005
  }
@@ -19137,7 +19295,10 @@ async function runWebServer(opts) {
19137
19295
  if (url === "/" || url === "/index.html") {
19138
19296
  try {
19139
19297
  const html = await readFile(join(publicDir, "index.html"), "utf-8");
19140
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
19298
+ res.writeHead(200, {
19299
+ "Content-Type": "text/html; charset=utf-8",
19300
+ "Cache-Control": "no-store"
19301
+ });
19141
19302
  res.end(html);
19142
19303
  } catch {
19143
19304
  res.writeHead(500, { "Content-Type": "text/plain" });
@@ -19150,7 +19311,9 @@ async function runWebServer(opts) {
19150
19311
  const filePath = join(publicDir, safeUrl);
19151
19312
  const contentType = contentTypes[filePath.split(".").pop() ?? ""] ?? "application/octet-stream";
19152
19313
  const data = await readFile(filePath);
19153
- res.writeHead(200, { "Content-Type": contentType });
19314
+ const headers = { "Content-Type": contentType };
19315
+ if (safeUrl.startsWith("/assets/")) headers["Cache-Control"] = "public, max-age=31536000, immutable";
19316
+ res.writeHead(200, headers);
19154
19317
  res.end(data);
19155
19318
  } catch {
19156
19319
  res.writeHead(404, { "Content-Type": "text/plain" });