scream-code 0.16.0 → 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-N_PRvDIk.mjs → app-V7s7I7Nr.mjs} +581 -413
  3. package/dist/{dispatch-5S52okPl.mjs → dispatch-BRkUiHzo.mjs} +1 -1
  4. package/dist/{dispatch-CsrteWRw.mjs → dispatch-D0mYcf1X.mjs} +1299 -268
  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-BVQaKy1Z.mjs → text-input-dialog-CzJKBIoN.mjs} +42 -12
  95. package/dist/{text-input-dialog-B0PROY20.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-CsrteWRw.mjs";
8
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-BVQaKy1Z.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.0"),
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) {
@@ -2102,6 +2066,7 @@ function childRequestFieldsFromArgs(args) {
2102
2066
  requestType: kindOf(args["request_type"]),
2103
2067
  message,
2104
2068
  needs: textOf(args["needs"]),
2069
+ expecting: textOf(payload["expecting"]),
2105
2070
  artifacts: listOf("artifacts"),
2106
2071
  evidence: listOf("evidence"),
2107
2072
  missing: listOf("missing")
@@ -2134,6 +2099,7 @@ function childRequestFieldsFromNotification(text) {
2134
2099
  const requestType = head !== null ? kindOf(head[1]) : "info";
2135
2100
  const messageParts = [head !== null && head[2] !== void 0 ? head[2] : first];
2136
2101
  let needs;
2102
+ let expecting;
2137
2103
  const lists = {};
2138
2104
  for (const line of body.slice(1)) {
2139
2105
  const separator = line.indexOf(": ");
@@ -2141,6 +2107,7 @@ function childRequestFieldsFromNotification(text) {
2141
2107
  const value = separator < 0 ? "" : line.slice(separator + 2).trim();
2142
2108
  const listKey = (key === "artifacts" || key === "evidence" || key === "missing") && value.startsWith("[") && value.endsWith("]") ? key : void 0;
2143
2109
  if (key === "needs" && value.length > 0) needs = value;
2110
+ else if (key === "expecting" && value.length > 0) expecting = value;
2144
2111
  else if (listKey !== void 0) lists[listKey] = bracketedListOf(value);
2145
2112
  else messageParts.push(line);
2146
2113
  }
@@ -2150,6 +2117,7 @@ function childRequestFieldsFromNotification(text) {
2150
2117
  requestType,
2151
2118
  message,
2152
2119
  needs,
2120
+ expecting,
2153
2121
  artifacts: lists["artifacts"],
2154
2122
  evidence: lists["evidence"],
2155
2123
  missing: lists["missing"]
@@ -2164,6 +2132,7 @@ function renderChildRequestNotice(fields, name) {
2164
2132
  });
2165
2133
  const parts = [previewText(fields.message)];
2166
2134
  if (fields.needs !== void 0) parts.push(t("transcript.child_request_needs", { value: previewText(fields.needs) }));
2135
+ if (fields.expecting !== void 0) parts.push(t("transcript.child_request_expecting", { value: previewText(fields.expecting) }));
2167
2136
  for (const entry of [
2168
2137
  ["artifacts", fields.artifacts],
2169
2138
  ["evidence", fields.evidence],
@@ -2178,233 +2147,6 @@ function renderChildRequestNotice(fields, name) {
2178
2147
  };
2179
2148
  }
2180
2149
  //#endregion
2181
- //#region src/tui/utils/subagent-slots.ts
2182
- /** Default subagent types, in the fixed display order of the slots. */
2183
- const DEFAULT_SUBAGENT_TYPES = [
2184
- "coder",
2185
- "explore",
2186
- "plan",
2187
- "verify",
2188
- "reviewer",
2189
- "oracle",
2190
- "worker",
2191
- "writer"
2192
- ];
2193
- /** How long a `requesting` overlay stays visible after a subagent asks the
2194
- * parent for help. The request itself returns at once and the agent keeps
2195
- * working, so this is a display window, not a blocked state. */
2196
- const REQUESTING_WINDOW_MS = 5e3;
2197
- var SubagentSlots = class {
2198
- slots = /* @__PURE__ */ new Map();
2199
- instances = /* @__PURE__ */ new Map();
2200
- /** Live instance count per type: a slot only returns to idle when the LAST
2201
- * instance of that type terminates (WolfPack spawns several coders at once
2202
- * and they share one visual slot). */
2203
- runningByType = /* @__PURE__ */ new Map();
2204
- /** main-side SendSubagentMessage tool callId → target subagent agentId. */
2205
- messagingByCall = /* @__PURE__ */ new Map();
2206
- /** Status to restore per TYPE when a messaging window closes, captured from
2207
- * the first open window; concurrent windows on the same type keep counting
2208
- * and the slot only exits messaging when the last window closes. */
2209
- messagingPrev = /* @__PURE__ */ new Map();
2210
- /** Slot type → open `requesting` overlay. Keyed by type so several live
2211
- * instances of one type share a single marker; the real activity state stays
2212
- * untouched in the slot and shows back up once the overlay expires. */
2213
- requesting = /* @__PURE__ */ new Map();
2214
- /** Count of open messaging windows per type (handles same-type siblings). */
2215
- messagingCount = /* @__PURE__ */ new Map();
2216
- reset() {
2217
- this.slots.clear();
2218
- this.instances.clear();
2219
- this.runningByType.clear();
2220
- this.messagingByCall.clear();
2221
- this.messagingPrev.clear();
2222
- this.messagingCount.clear();
2223
- this.requesting.clear();
2224
- }
2225
- /**
2226
- * subagent.spawned. A first spawn starts the slot as working; a second
2227
- * spawn of the SAME agentId means the main agent resumed it (返工中),
2228
- * tracked via an instance generation counter.
2229
- */
2230
- onSpawned(agentId, subagentType, description) {
2231
- const instance = this.instances.get(agentId);
2232
- if (instance !== void 0) {
2233
- instance.generation += 1;
2234
- this.runningByType.set(instance.type, (this.runningByType.get(instance.type) ?? 0) + 1);
2235
- this.setState(instance.type, "reworking", agentId, description === void 0 ? void 0 : `resume: ${description}`);
2236
- return;
2237
- }
2238
- this.instances.set(agentId, {
2239
- type: subagentType,
2240
- generation: 1
2241
- });
2242
- this.runningByType.set(subagentType, (this.runningByType.get(subagentType) ?? 0) + 1);
2243
- this.setState(subagentType, "working", agentId, description);
2244
- }
2245
- /** subagent.started. Keeps the current state (spawned already set it); only
2246
- * used to materialize a slot if lifecycle events raced past us. */
2247
- onStarted(agentId) {
2248
- const instance = this.instances.get(agentId);
2249
- if (instance === void 0) return;
2250
- const slot = this.ensureSlot(instance.type);
2251
- if (slot.status === "idle") {
2252
- slot.status = "working";
2253
- slot.agentId = agentId;
2254
- slot.lastActivityAt = Date.now();
2255
- }
2256
- }
2257
- /** subagent.completed / subagent.failed: the slot goes back to resting only
2258
- * when the LAST live instance of that type terminates (siblings spawned by
2259
- * the same tool call keep the slot busy). */
2260
- onTerminated(agentId) {
2261
- const instance = this.instances.get(agentId);
2262
- if (instance === void 0) return;
2263
- if (this.requesting.get(instance.type)?.agentId === agentId) this.requesting.delete(instance.type);
2264
- const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
2265
- if (remaining > 0) {
2266
- this.runningByType.set(instance.type, remaining);
2267
- const slot = this.ensureSlot(instance.type);
2268
- slot.detail = void 0;
2269
- slot.lastActivityAt = Date.now();
2270
- return;
2271
- }
2272
- this.runningByType.delete(instance.type);
2273
- this.setState(instance.type, "idle", void 0, void 0);
2274
- }
2275
- /**
2276
- * Routed subagent activity (agentId !== main):
2277
- * - kind 'tool' → working (tool call in flight)
2278
- * - kind 'output' → outputting (assistant/thinking delta)
2279
- * - kind 'toolResult'→ detail only (the agent may keep working/outputting)
2280
- */
2281
- onActivity(agentId, kind, detail) {
2282
- const instance = this.instances.get(agentId);
2283
- if (instance === void 0) return;
2284
- if (kind === "toolResult") {
2285
- const slot = this.ensureSlot(instance.type);
2286
- slot.detail = detail;
2287
- slot.lastActivityAt = Date.now();
2288
- return;
2289
- }
2290
- this.setState(instance.type, kind === "tool" ? "working" : "outputting", agentId, detail);
2291
- }
2292
- /** A subagent asked the parent for help. The request returns immediately and
2293
- * the agent keeps working, so this only raises a transient overlay — the real
2294
- * activity state stays in the slot and shows back up when it expires. While
2295
- * `messaging` is on screen the overlay loses (see visibleSlot). */
2296
- onRequesting(agentId, at = Date.now()) {
2297
- const instance = this.instances.get(agentId);
2298
- if (instance === void 0) return;
2299
- this.requesting.set(instance.type, {
2300
- until: at + REQUESTING_WINDOW_MS,
2301
- agentId
2302
- });
2303
- const slot = this.ensureSlot(instance.type);
2304
- slot.agentId = agentId;
2305
- slot.lastActivityAt = at;
2306
- }
2307
- /**
2308
- * Main-side SendSubagentMessage tool call started: the target subagent's
2309
- * slot shows messaging until the tool call returns.
2310
- */
2311
- onMessagingStart(toolCallId, targetAgentId, operation) {
2312
- if (targetAgentId === void 0) return;
2313
- const instance = this.instances.get(targetAgentId);
2314
- if (instance === void 0) return;
2315
- this.messagingByCall.set(toolCallId, targetAgentId);
2316
- this.requesting.delete(instance.type);
2317
- const slot = this.ensureSlot(instance.type);
2318
- if (slot.status !== "messaging") this.messagingPrev.set(instance.type, slot.status);
2319
- this.messagingCount.set(instance.type, (this.messagingCount.get(instance.type) ?? 0) + 1);
2320
- this.setState(instance.type, "messaging", targetAgentId, operation === void 0 ? void 0 : `message (${operation})`);
2321
- }
2322
- /** Tool call finished: the messaging window closes; restore the state the
2323
- * slot had before the chat (working/outputting), else idle. */
2324
- onMessagingEnd(toolCallId) {
2325
- const target = this.messagingByCall.get(toolCallId);
2326
- if (target === void 0) return;
2327
- this.messagingByCall.delete(toolCallId);
2328
- const instance = this.instances.get(target);
2329
- if (instance === void 0) return;
2330
- const slot = this.ensureSlot(instance.type);
2331
- if (slot.status !== "messaging") return;
2332
- const remaining = (this.messagingCount.get(instance.type) ?? 1) - 1;
2333
- if (remaining > 0) {
2334
- this.messagingCount.set(instance.type, remaining);
2335
- return;
2336
- }
2337
- this.messagingCount.delete(instance.type);
2338
- const prev = this.messagingPrev.get(instance.type);
2339
- this.messagingPrev.delete(instance.type);
2340
- slot.status = prev === "working" || prev === "outputting" || prev === "reworking" ? prev : "idle";
2341
- slot.detail = void 0;
2342
- slot.lastActivityAt = Date.now();
2343
- }
2344
- /**
2345
- * All slots in fixed display order: the eight default types first (always
2346
- * present, idle when never used), then extra types in spawn order.
2347
- */
2348
- getSlots(at = Date.now()) {
2349
- this.dropExpiredOverlays(at);
2350
- const ordered = [];
2351
- for (const type of DEFAULT_SUBAGENT_TYPES) ordered.push(this.visibleSlot(type));
2352
- for (const slot of this.slots.values()) {
2353
- if (DEFAULT_SUBAGENT_TYPES.includes(slot.type)) continue;
2354
- ordered.push(this.visibleSlot(slot.type));
2355
- }
2356
- return ordered;
2357
- }
2358
- /** Display view of one slot: the real state with an unexpired `requesting`
2359
- * overlay on top. Read-path only — it never mutates. Callers must run
2360
- * {@link dropExpiredOverlays} first, and messaging clears the overlay in
2361
- * {@link onMessagingStart}; the `messaging` guard only covers same-tick ordering. */
2362
- visibleSlot(type) {
2363
- const copy = {
2364
- ...this.ensureSlot(type),
2365
- count: this.runningByType.get(type) ?? 0
2366
- };
2367
- if (this.requesting.has(type) && copy.status !== "messaging") copy.status = "requesting";
2368
- return copy;
2369
- }
2370
- dropExpiredOverlays(at) {
2371
- for (const [type, overlay] of this.requesting) if (at >= overlay.until) this.requesting.delete(type);
2372
- }
2373
- ensureSlot(type) {
2374
- let slot = this.slots.get(type);
2375
- if (slot === void 0) {
2376
- if (this.slots.size >= 16) {
2377
- slot = {
2378
- type,
2379
- status: "idle",
2380
- agentId: void 0,
2381
- detail: void 0,
2382
- count: 0,
2383
- lastActivityAt: 0
2384
- };
2385
- return slot;
2386
- }
2387
- slot = {
2388
- type,
2389
- status: "idle",
2390
- agentId: void 0,
2391
- detail: void 0,
2392
- count: 0,
2393
- lastActivityAt: Date.now()
2394
- };
2395
- this.slots.set(type, slot);
2396
- }
2397
- return slot;
2398
- }
2399
- setState(type, status, agentId, detail) {
2400
- const slot = this.ensureSlot(type);
2401
- slot.status = status;
2402
- slot.agentId = agentId;
2403
- slot.detail = detail;
2404
- slot.lastActivityAt = Date.now();
2405
- }
2406
- };
2407
- //#endregion
2408
2150
  //#region src/tui/utils/background-task-status.ts
2409
2151
  const MAX_DETAIL_LENGTH = 240;
2410
2152
  function truncate(value) {
@@ -2979,7 +2721,7 @@ var SessionEventHandler = class {
2979
2721
  if (this.providerLatencySamples.length > this.providerLatencyWindow) this.providerLatencySamples.shift();
2980
2722
  this.providerLatencyAt = Date.now();
2981
2723
  }
2982
- /** 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
2983
2725
  * caller owns tone and staleness — this stays a plain reading. */
2984
2726
  getProviderLatency() {
2985
2727
  if (this.providerLatencySamples.length === 0) return {
@@ -3132,6 +2874,7 @@ var SessionEventHandler = class {
3132
2874
  handleTurnBegin(_event) {
3133
2875
  this.host.streamingUI.resetToolUi();
3134
2876
  this.host.streamingUI.setStep(0);
2877
+ this.host.streamingUI.endActivityGroup();
3135
2878
  this.host.patchLivePane({
3136
2879
  pendingApproval: null,
3137
2880
  pendingQuestion: null
@@ -3394,6 +3137,7 @@ var SessionEventHandler = class {
3394
3137
  this.host.streamingUI.flushNow();
3395
3138
  this.host.streamingUI.resetToolUi();
3396
3139
  this.host.streamingUI.finalizeLiveTextBuffers();
3140
+ this.host.streamingUI.endActivityGroup();
3397
3141
  this.host.showError(`[${event.code}] ${event.message}`);
3398
3142
  }
3399
3143
  handleSessionWarning(event) {
@@ -4339,6 +4083,7 @@ var SessionReplayRenderer = class {
4339
4083
  context.completedToolCallIds.add(toolCallId);
4340
4084
  }
4341
4085
  advanceTurn(context) {
4086
+ this.host.streamingUI.endActivityGroup();
4342
4087
  context.turnIndex += 1;
4343
4088
  context.stepIndex = 0;
4344
4089
  context.currentTurnId = `replay:${String(context.turnIndex)}`;
@@ -4504,81 +4249,6 @@ var SessionReplayRenderer = class {
4504
4249
  }
4505
4250
  };
4506
4251
  //#endregion
4507
- //#region src/tui/components/dialogs/compaction.ts
4508
- /**
4509
- * Renders a compaction block in the transcript.
4510
- *
4511
- * Lifecycle:
4512
- * - constructed on `compaction.started` → blinking white bullet +
4513
- * "Compacting context..." and optional custom instruction
4514
- * - `markDone()` on `compaction.completed` → solid green bullet +
4515
- * "Compaction complete (X → Y tokens)"
4516
- * - `markCanceled()` on `compaction.cancelled` → solid warning bullet +
4517
- * "Compaction cancelled"
4518
- *
4519
- * Bullet animation mirrors `ToolCallComponent` (500ms blink) so the user
4520
- * reads the same "work in progress" signal across the UI.
4521
- */
4522
- const BLINK_INTERVAL = 500;
4523
- var CompactionComponent = class extends Container {
4524
- colors;
4525
- ui;
4526
- headerText;
4527
- blinkOn = true;
4528
- blinkTimer = null;
4529
- done = false;
4530
- canceled = false;
4531
- tokensBefore;
4532
- tokensAfter;
4533
- constructor(colors, ui, instruction) {
4534
- super();
4535
- this.colors = colors;
4536
- this.ui = ui;
4537
- this.addChild(new Spacer(1));
4538
- this.headerText = new Text(this.buildHeader(), 0, 0);
4539
- this.addChild(this.headerText);
4540
- if (instruction !== void 0) this.addChild(new Text(chalk.dim(` ${instruction}`), 0, 0));
4541
- this.startBlink();
4542
- }
4543
- markDone(tokensBefore, tokensAfter) {
4544
- if (this.done || this.canceled) return;
4545
- this.done = true;
4546
- this.tokensBefore = tokensBefore;
4547
- this.tokensAfter = tokensAfter;
4548
- this.stopBlink();
4549
- this.headerText.setText(this.buildHeader());
4550
- this.ui?.requestRender();
4551
- }
4552
- markCanceled() {
4553
- if (this.done || this.canceled) return;
4554
- this.canceled = true;
4555
- this.stopBlink();
4556
- this.headerText.setText(this.buildHeader());
4557
- this.ui?.requestRender();
4558
- }
4559
- dispose() {
4560
- this.stopBlink();
4561
- }
4562
- buildHeader() {
4563
- 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)`) : ""}`;
4564
- if (this.canceled) return `${chalk.hex(this.colors.warning)(STATUS_BULLET)}${chalk.hex(this.colors.warning).bold(t("compaction.canceled"))}`;
4565
- return `${this.blinkOn ? chalk.hex(this.colors.roleAssistant)(STATUS_BULLET) : " "}${chalk.hex(this.colors.primary).bold(t("compaction.in_progress"))}`;
4566
- }
4567
- startBlink() {
4568
- this.blinkTimer = setInterval(() => {
4569
- this.blinkOn = !this.blinkOn;
4570
- this.headerText.setText(this.buildHeader());
4571
- this.ui?.requestRender();
4572
- }, BLINK_INTERVAL);
4573
- }
4574
- stopBlink() {
4575
- if (this.blinkTimer !== null) {
4576
- clearInterval(this.blinkTimer);
4577
- this.blinkTimer = null;
4578
- }
4579
- }
4580
- };
4581
- //#endregion
4582
4252
  //#region src/tui/utils/terminal-notification.ts
4583
4253
  function notifyTerminalOnce(state, key, notification) {
4584
4254
  const { enabled, condition } = state.appState.notifications;
@@ -4652,7 +4322,7 @@ function isControlCharacter(ch) {
4652
4322
  }
4653
4323
  //#endregion
4654
4324
  //#region src/tui/controllers/streaming-ui.ts
4655
- var StreamingUIController = class {
4325
+ var StreamingUIController = class StreamingUIController {
4656
4326
  host;
4657
4327
  flushTimer;
4658
4328
  lastFlushAt;
@@ -4686,6 +4356,30 @@ var StreamingUIController = class {
4686
4356
  _pendingReadGroupComponents = /* @__PURE__ */ new Map();
4687
4357
  _pendingAgentGroup = null;
4688
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
+ ]);
4689
4383
  constructor(host) {
4690
4384
  this.host = host;
4691
4385
  }
@@ -4898,6 +4592,7 @@ var StreamingUIController = class {
4898
4592
  }
4899
4593
  /** Tears down replay-specific state after session history has been rendered. */
4900
4594
  cleanupAfterReplay(completedToolCallIds) {
4595
+ this.endActivityGroup();
4901
4596
  this._activeToolCalls.clear();
4902
4597
  for (const toolCallId of completedToolCallIds) this._pendingToolComponents.delete(toolCallId);
4903
4598
  this._pendingAgentGroup = null;
@@ -4909,6 +4604,74 @@ var StreamingUIController = class {
4909
4604
  this.pendingToolCallFlushIds.clear();
4910
4605
  this.host.state.ui.requestRender();
4911
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
+ }
4912
4675
  disposeActiveThinkingComponent() {
4913
4676
  if (this._activeThinkingComponent !== void 0) {
4914
4677
  this._activeThinkingComponent.dispose();
@@ -4983,7 +4746,9 @@ var StreamingUIController = class {
4983
4746
  const draft = this._assistantDraft;
4984
4747
  if (this._shownAssistantLength >= draft.length) return this._shownAssistantLength;
4985
4748
  const measured = getSharedSpeedTracker().getSpeed();
4986
- 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));
4987
4752
  this._shownAssistantLength = Math.min(draft.length, this._shownAssistantLength + budget);
4988
4753
  return this._shownAssistantLength;
4989
4754
  }
@@ -5047,6 +4812,7 @@ var StreamingUIController = class {
5047
4812
  const turnStartTime = this.turnStartAt !== 0 ? this.turnStartAt : state.appState.streamingStartTime;
5048
4813
  const completedTurnKey = this._currentTurnId ?? `local:${String(turnStartTime)}`;
5049
4814
  this.finalizeLiveTextBuffers();
4815
+ this.endActivityGroup();
5050
4816
  this.resetToolCallState();
5051
4817
  this._currentTurnId = void 0;
5052
4818
  const next = this.host.shiftQueuedMessage();
@@ -5120,6 +4886,7 @@ var StreamingUIController = class {
5120
4886
  block.component.updateContent(fullText);
5121
4887
  this.host.state.ui.requestRender();
5122
4888
  }
4889
+ this.sealActivityGroupOnText(fullText);
5123
4890
  }
5124
4891
  onStreamingTextEnd() {
5125
4892
  const block = this._streamingBlock;
@@ -5135,7 +4902,7 @@ var StreamingUIController = class {
5135
4902
  if (this._activeThinkingComponent === void 0) {
5136
4903
  this._pendingAgentGroup = null;
5137
4904
  this._pendingReadGroup = null;
5138
- 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);
5139
4906
  const entry = {
5140
4907
  id: nextTranscriptId(),
5141
4908
  kind: "thinking",
@@ -5148,21 +4915,30 @@ var StreamingUIController = class {
5148
4915
  this.host.pushTranscriptEntry(entry);
5149
4916
  this.host.transcriptController.registerLiveComponent(component, entry);
5150
4917
  this.host.transcriptController.markPending(component);
5151
- if (state.toolOutputExpanded) component.setExpanded(true);
5152
- 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);
5153
4922
  } else {
5154
4923
  this._activeThinkingComponent.setText(fullText);
5155
4924
  if (this._thinkingEntry !== void 0) this._thinkingEntry.content = fullText;
4925
+ this._lastThinkingText = fullText;
4926
+ this.ensureActivityGroup().setThinking(this.activityThinkingText(fullText), true);
5156
4927
  }
5157
4928
  state.ui.requestRender();
5158
4929
  }
5159
4930
  onThinkingEnd() {
5160
4931
  if (this._activeThinkingComponent === void 0) return;
5161
- this._activeThinkingComponent.finalize();
5162
- this.host.transcriptController.unmarkPending(this._activeThinkingComponent);
4932
+ const component = this._activeThinkingComponent;
4933
+ component.finalize();
4934
+ this.host.transcriptController.unmarkPending(component);
5163
4935
  this.host.transcriptController.commit();
4936
+ this.host.transcriptController.releaseLiveComponent(component);
5164
4937
  this._activeThinkingComponent = void 0;
5165
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();
5166
4942
  this.host.state.ui.requestRender();
5167
4943
  }
5168
4944
  onToolCallStart(toolCall) {
@@ -5186,15 +4962,20 @@ var StreamingUIController = class {
5186
4962
  this.host.pushTranscriptEntry(entry);
5187
4963
  this.host.transcriptController.registerLiveComponent(tc, entry);
5188
4964
  this.host.transcriptController.markPending(tc);
5189
- if (state.toolOutputExpanded) tc.setExpanded(true);
5190
4965
  if (state.planExpanded) tc.setPlanExpanded(true);
5191
4966
  if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
5192
4967
  if (toolCall.name !== "Read") this._pendingReadGroup = null;
5193
4968
  let handled = this.tryAttachAgentToolCall(toolCall, tc);
5194
4969
  if (!handled) handled = this.tryAttachReadToolCall(toolCall, tc);
5195
- if (!handled) {
4970
+ if (!handled) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
5196
4971
  state.transcriptContainer.addChild(tc);
5197
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();
5198
4979
  }
5199
4980
  if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
5200
4981
  const session = this.host.requireSession();
@@ -5249,6 +5030,9 @@ var StreamingUIController = class {
5249
5030
  if (tc) {
5250
5031
  tc.setResult(result);
5251
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);
5252
5036
  this.host.transcriptController.unmarkPending(tc);
5253
5037
  this._pendingToolComponents.delete(toolCallId);
5254
5038
  const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
@@ -5260,7 +5044,6 @@ var StreamingUIController = class {
5260
5044
  if (matchedCall?.name === "AskUserQuestion") {
5261
5045
  const completed = new ToolCallComponent(matchedCall, result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
5262
5046
  completed.setPermissionMode(state.appState.permissionMode);
5263
- if (state.toolOutputExpanded) completed.setExpanded(true);
5264
5047
  if (state.planExpanded) completed.setPlanExpanded(true);
5265
5048
  const entry = {
5266
5049
  id: nextTranscriptId(),
@@ -6612,6 +6395,14 @@ var TranscriptController = class TranscriptController {
6612
6395
  registerLiveComponent(component, entry) {
6613
6396
  this.liveComponentToEntry.set(component, entry);
6614
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
+ }
6615
6406
  markPending(component) {
6616
6407
  this.pendingComponents.add(component);
6617
6408
  }
@@ -6679,11 +6470,7 @@ var TranscriptController = class TranscriptController {
6679
6470
  component.updateContent(entry.content);
6680
6471
  return component;
6681
6472
  }
6682
- case "thinking": {
6683
- const thinking = new ThinkingComponent(entry.content, state.theme.colors, true);
6684
- if (state.toolOutputExpanded) thinking.setExpanded(true);
6685
- return thinking;
6686
- }
6473
+ case "thinking": return new ThinkingComponent(entry.content, state.theme.colors, true);
6687
6474
  case "tool_call":
6688
6475
  if (entry.toolCallData?.name === "ReadGroup" && entry.toolCallData.result !== void 0) {
6689
6476
  const rgc = new ReadGroupComponent(state.theme.colors, state.ui);
@@ -6692,7 +6479,6 @@ var TranscriptController = class TranscriptController {
6692
6479
  }
6693
6480
  if (entry.toolCallData) {
6694
6481
  const tc = new ToolCallComponent(entry.toolCallData, entry.toolCallData.result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
6695
- if (state.toolOutputExpanded) tc.setExpanded(true);
6696
6482
  if (state.planExpanded) tc.setPlanExpanded(true);
6697
6483
  return tc;
6698
6484
  }
@@ -6776,6 +6562,7 @@ var TranscriptController = class TranscriptController {
6776
6562
  clearAndRedraw() {
6777
6563
  const { state, streamingUI, imageStore } = this.host;
6778
6564
  streamingUI.discardPending();
6565
+ streamingUI.endActivityGroup();
6779
6566
  state.transcriptEntries = [];
6780
6567
  streamingUI.disposeActiveCompactionBlock();
6781
6568
  streamingUI.resetLiveText();
@@ -6832,6 +6619,18 @@ var TranscriptController = class TranscriptController {
6832
6619
  }
6833
6620
  toggleToolOutputExpansion() {
6834
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
+ }
6835
6634
  state.toolOutputExpanded = !state.toolOutputExpanded;
6836
6635
  state.ui.requestRender();
6837
6636
  }
@@ -7053,17 +6852,14 @@ function formatCount$1(n) {
7053
6852
  return n.toLocaleString("en-US");
7054
6853
  }
7055
6854
  /**
7056
- * Compact token form for narrow sidebars: 2,267,680,102 → `22.7亿` (zh) or
7057
- * `2.3B` (en). Used only when the thousands-separated form cannot fit, so a
7058
- * 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.
7059
6861
  */
7060
6862
  function formatCompact(n) {
7061
- if (getLocale() === "zh") {
7062
- if (n >= 9995e4) return `${(n / 1e8).toFixed(1)}亿`;
7063
- if (n >= 9500) return `${Math.round(n / 1e4)}万`;
7064
- return String(n);
7065
- }
7066
- if (n >= 9995e5) return `${(n / 1e9).toFixed(1)}B`;
7067
6863
  if (n >= 999500) return `${(n / 1e6).toFixed(1)}M`;
7068
6864
  if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
7069
6865
  return String(n);
@@ -7131,6 +6927,10 @@ const sessionPanel = {
7131
6927
  * with the 1s sidebar heartbeat each tick advances half the cycle, so the
7132
6928
  * colour jump is clearly visible instead of drifting imperceptibly. */
7133
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
+ }
7134
6934
  /** Localized status word per slot state. */
7135
6935
  function statusText(status) {
7136
6936
  switch (status) {
@@ -7160,7 +6960,7 @@ var AgentsPanelContent = class {
7160
6960
  if (agents === void 0 || agents.length === 0) return [];
7161
6961
  return layoutSidebarGrid(agents.map((slot) => ({
7162
6962
  marker: slot.status === "idle" ? "○" : "●",
7163
- label: slot.type,
6963
+ label: displayType(slot.type),
7164
6964
  value: statusText(slot.status) + (slot.count > 1 ? ` ×${slot.count}` : "")
7165
6965
  })), width).map((cell, index) => this.renderSlot(cell, agents[index], index, width));
7166
6966
  }
@@ -7316,20 +7116,23 @@ const HUB_MODEL_STALE_MS = 5 * 6e4;
7316
7116
  const HUB_ENDPOINTS = Object.freeze([
7317
7117
  {
7318
7118
  id: "github",
7319
- url: "https://github.com/"
7119
+ url: "https://github.com/",
7120
+ label: "Github"
7320
7121
  },
7321
7122
  {
7322
7123
  id: "google",
7323
- url: "https://www.google.com/"
7124
+ url: "https://www.google.com/",
7125
+ label: "Google"
7324
7126
  },
7325
7127
  {
7326
7128
  id: "x",
7327
7129
  url: "https://x.com/",
7328
- label: "x (twitter)"
7130
+ label: "X (Twitter)"
7329
7131
  },
7330
7132
  {
7331
7133
  id: "huggingface",
7332
- url: "https://huggingface.co/"
7134
+ url: "https://huggingface.co/",
7135
+ label: "Huggingface"
7333
7136
  },
7334
7137
  {
7335
7138
  id: "alibaba",
@@ -13972,6 +13775,7 @@ var ScreamTUI = class {
13972
13775
  beginSessionRequest() {
13973
13776
  this.streamingUI.markTurnStarted();
13974
13777
  this.streamingUI.setTurnId(void 0);
13778
+ this.streamingUI.endActivityGroup();
13975
13779
  this.streamingUI.resetLiveText();
13976
13780
  this.streamingUI.resetToolUi();
13977
13781
  this.streamingUI.resetToolCallState();
@@ -13983,6 +13787,7 @@ var ScreamTUI = class {
13983
13787
  }
13984
13788
  failSessionRequest(message) {
13985
13789
  this.setAppState({ streamingPhase: "idle" });
13790
+ this.streamingUI.endActivityGroup();
13986
13791
  this.resetLivePane();
13987
13792
  this.showError(message);
13988
13793
  }
@@ -14190,7 +13995,7 @@ var ScreamTUI = class {
14190
13995
  const card = new OnboardingCardComponent({
14191
13996
  colors: this.state.theme.colors,
14192
13997
  onConfigure: () => {
14193
- import("./dispatch-5S52okPl.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
13998
+ import("./dispatch-BRkUiHzo.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14194
13999
  this.restoreEditor();
14195
14000
  onDone?.();
14196
14001
  });
@@ -15859,6 +15664,53 @@ async function persistKey(filePath, record) {
15859
15664
  await rename(tmp, filePath);
15860
15665
  }
15861
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
15862
15714
  //#region src/web/rateLimit.ts
15863
15715
  function createFixedWindowLimiter(opts) {
15864
15716
  const { windowMs, max } = opts;
@@ -16426,14 +16278,12 @@ async function ensureSessionsDir(homeDir) {
16426
16278
  if (!existsSync(dir)) await mkdir(dir, { recursive: true });
16427
16279
  }
16428
16280
  async function appendJournalLine(homeDir, sessionId, line) {
16429
- try {
16430
- await writeFile(getJournalPath(homeDir, sessionId), line + "\n", { flag: "a" });
16431
- } catch (error) {
16281
+ await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
16432
16282
  log.warn("web: failed to persist journal line", {
16433
16283
  sessionId,
16434
16284
  error: String(error)
16435
16285
  });
16436
- }
16286
+ });
16437
16287
  }
16438
16288
  let metaWriteSeq = 0;
16439
16289
  async function saveMetadata(homeDir, meta) {
@@ -16460,16 +16310,77 @@ async function loadMetadata(homeDir, sessionId) {
16460
16310
  async function loadJournal(homeDir, sessionId) {
16461
16311
  try {
16462
16312
  const lines = (await readFile(getJournalPath(homeDir, sessionId), "utf-8")).split("\n").filter((l) => l.trim().length > 0);
16463
- const results = [];
16313
+ const entries = [];
16314
+ let corrupt = 0;
16464
16315
  for (const line of lines) try {
16465
- results.push(JSON.parse(line));
16316
+ entries.push(JSON.parse(line));
16466
16317
  } catch {
16318
+ corrupt += 1;
16467
16319
  log.warn("web: skipping corrupt journal line", { sessionId });
16468
16320
  }
16469
- return results;
16321
+ return {
16322
+ entries,
16323
+ corrupt
16324
+ };
16470
16325
  } catch {
16471
- 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
+ });
16472
16382
  }
16383
+ return out;
16473
16384
  }
16474
16385
  async function listPersistedSessions(homeDir) {
16475
16386
  const dir = getSessionsDir(homeDir);
@@ -16648,10 +16559,11 @@ const MAX_INLINE_DIFF_BYTES = 512 * 1024;
16648
16559
  * Single-file patch. `repoPath` is **repository-root relative** — the same base
16649
16560
  * `git status` reports, so the UI can hand it straight back.
16650
16561
  *
16651
- * Two cases used to return an empty patch and look like "点了没反应":
16652
- * nested files (the old code re-prefixed workdir-relative paths with `../..`,
16653
- * resolving them outside the repo) and untracked files (`git diff` has nothing
16654
- * 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.
16655
16567
  */
16656
16568
  async function getGitFileDiff(workDir, repoPath) {
16657
16569
  const rel = normalizeNumstatPath(repoPath);
@@ -16769,7 +16681,27 @@ var WebSession = class {
16769
16681
  * snapshot (`web.message.finalized`) — the only source that survives a
16770
16682
  * server restart (deltas are volatile by design).
16771
16683
  */
16772
- 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
+ }
16773
16705
  liveTurnCount = 0;
16774
16706
  /** pendingMsgId awaiting compaction completion/cancellation event. */
16775
16707
  pendingCompactionMsgId = null;
@@ -16883,6 +16815,63 @@ var WebSession = class {
16883
16815
  const msgs = this.buildMessages();
16884
16816
  if (msgs.length > 0 && !this.customTitle) this.title = deriveTitle(msgs);
16885
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
+ }
16886
16875
  async updateTitle() {
16887
16876
  if (this.customTitle) return;
16888
16877
  const newTitle = deriveTitle(this.buildMessages());
@@ -16934,11 +16923,12 @@ var WebSession = class {
16934
16923
  if (!this.session) return;
16935
16924
  this.unsubscribe = this.session.onEvent((event) => {
16936
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;
16937
16927
  if (event.type === "turn.started") {
16938
16928
  this.busy = true;
16939
16929
  this.activeTurns += 1;
16940
16930
  this.liveTurnCount += 1;
16941
- this.liveAssistant = {
16931
+ this.liveAssistants.set(event.turnId, {
16942
16932
  role: "assistant",
16943
16933
  content: "",
16944
16934
  tools: [],
@@ -16954,11 +16944,11 @@ var WebSession = class {
16954
16944
  tokens: null,
16955
16945
  tokensPerSec: null
16956
16946
  }
16957
- };
16947
+ });
16958
16948
  } else if (event.type === "turn.ended") {
16959
16949
  this.activeTurns = Math.max(0, this.activeTurns - 1);
16960
16950
  this.busy = this.activeTurns > 0;
16961
- const live = this.liveAssistant;
16951
+ const live = this.liveFor(event.turnId);
16962
16952
  if (event.reason === "failed" && live) live.isError = true;
16963
16953
  if (live) {
16964
16954
  live.turnStats = {
@@ -16978,13 +16968,13 @@ var WebSession = class {
16978
16968
  message: live
16979
16969
  });
16980
16970
  }
16981
- this.liveAssistant = null;
16971
+ this.liveAssistants.delete(event.turnId);
16982
16972
  this.trackPending(this.updateTitle());
16983
16973
  } else if (event.type === "assistant.delta") {
16984
- const live = this.liveAssistant;
16974
+ const live = this.liveFor(event.turnId);
16985
16975
  if (live) live.content += event.delta;
16986
16976
  } else if (event.type === "thinking.delta") {
16987
- const live = this.liveAssistant;
16977
+ const live = this.liveFor(event.turnId);
16988
16978
  if (live) {
16989
16979
  const t = live.tools.find((x) => x.name === "thinking");
16990
16980
  if (t) t.output = (t.output ?? "") + event.delta;
@@ -16996,7 +16986,7 @@ var WebSession = class {
16996
16986
  });
16997
16987
  }
16998
16988
  } else if (event.type === "tool.call.started") {
16999
- const live = this.liveAssistant;
16989
+ const live = this.liveFor(event.turnId);
17000
16990
  if (live) {
17001
16991
  live.tools.push({
17002
16992
  toolCallId: event.toolCallId,
@@ -17006,7 +16996,7 @@ var WebSession = class {
17006
16996
  if (live.turnStats) live.turnStats.step += 1;
17007
16997
  }
17008
16998
  } else if (event.type === "tool.result") {
17009
- const live = this.liveAssistant;
16999
+ const live = this.liveFor(event.turnId);
17010
17000
  if (live) {
17011
17001
  const tool = live.tools.find((t) => t.toolCallId === event.toolCallId);
17012
17002
  if (tool) {
@@ -17487,7 +17477,7 @@ var WebSession = class {
17487
17477
  workDir: this.workDir,
17488
17478
  title: this.title,
17489
17479
  createdAt: this.createdAt,
17490
- messageCount: this.buildMessages().length,
17480
+ messageCount: this.isActive ? this.buildMessages().length : -1,
17491
17481
  active: this.isActive
17492
17482
  };
17493
17483
  }
@@ -17681,7 +17671,7 @@ var WebSession = class {
17681
17671
  return this.connections.size;
17682
17672
  }
17683
17673
  buildMessages() {
17684
- const messages = [];
17674
+ const messages = [...this.seededMessages];
17685
17675
  let currentAssistant = null;
17686
17676
  let userIndex = 0;
17687
17677
  let turnCount = 0;
@@ -17867,6 +17857,36 @@ async function handleGoalRoute(req, res, url, resolveSession) {
17867
17857
  }
17868
17858
  return true;
17869
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
+ }
17870
17890
  var SessionManager = class {
17871
17891
  sessions = /* @__PURE__ */ new Map();
17872
17892
  /** In-flight activation promises to deduplicate concurrent calls. */
@@ -17889,7 +17909,7 @@ var SessionManager = class {
17889
17909
  await ensureSessionsDir(this.homeDir);
17890
17910
  const metas = await listPersistedSessions(this.homeDir);
17891
17911
  for (const meta of metas) {
17892
- const entries = await loadJournal(this.homeDir, meta.sessionId);
17912
+ const { entries } = await loadJournal(this.homeDir, meta.sessionId);
17893
17913
  const ws = new WebSession(null, {
17894
17914
  sessionId: meta.sessionId,
17895
17915
  coreSessionId: meta.coreSessionId ?? meta.sessionId,
@@ -17903,11 +17923,107 @@ var SessionManager = class {
17903
17923
  ws.loadFromPersisted(entries);
17904
17924
  this.sessions.set(meta.sessionId, ws);
17905
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
+ }
17906
18013
  log.info("web: loaded persisted sessions", { count: this.sessions.size });
17907
18014
  }
17908
- 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;
17909
18025
  const session = await this.harness.createSession({
17910
- workDir: this.workDir,
18026
+ workDir: dir,
17911
18027
  model: this.model,
17912
18028
  permission: this.permission
17913
18029
  });
@@ -17915,7 +18031,7 @@ var SessionManager = class {
17915
18031
  const createdAt = Date.now();
17916
18032
  const webSession = new WebSession(session, {
17917
18033
  sessionId,
17918
- workDir: this.workDir,
18034
+ workDir: dir,
17919
18035
  permission: this.permission,
17920
18036
  yolo: this.yolo,
17921
18037
  homeDir: this.homeDir,
@@ -17932,7 +18048,7 @@ var SessionManager = class {
17932
18048
  this.sessions.set(sessionId, webSession);
17933
18049
  log.info("web: session created", {
17934
18050
  sessionId,
17935
- workDir: this.workDir
18051
+ workDir: dir
17936
18052
  });
17937
18053
  return webSession;
17938
18054
  }
@@ -17977,8 +18093,19 @@ var SessionManager = class {
17977
18093
  coreSessionId: session.id,
17978
18094
  onFork: (id) => this.forkSession(id)
17979
18095
  });
17980
- const entries = await loadJournal(this.homeDir, sessionId);
18096
+ const { entries, corrupt } = await loadJournal(this.homeDir, sessionId);
17981
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
+ }
17982
18109
  try {
17983
18110
  await reactivated.ready;
17984
18111
  } catch (error) {
@@ -18033,7 +18160,7 @@ var SessionManager = class {
18033
18160
  const title = `${source.getTitle()} (fork)`;
18034
18161
  const webSession = new WebSession(session, {
18035
18162
  sessionId: newId,
18036
- workDir: this.workDir,
18163
+ workDir: source.getMetadata().workDir,
18037
18164
  permission: source.getMetadata().permission,
18038
18165
  yolo: this.yolo,
18039
18166
  homeDir: this.homeDir,
@@ -18047,6 +18174,16 @@ var SessionManager = class {
18047
18174
  await writeFile(getJournalPath(this.homeDir, newId), lines);
18048
18175
  }
18049
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
+ }
18050
18187
  try {
18051
18188
  await webSession.ready;
18052
18189
  } catch (error) {
@@ -18082,8 +18219,33 @@ var SessionManager = class {
18082
18219
  async delete(sessionId) {
18083
18220
  const ws = this.sessions.get(sessionId);
18084
18221
  if (!ws) return false;
18222
+ const workDir = ws.workDir;
18223
+ const coreId = ws.getCoreSessionId();
18085
18224
  await ws.delete();
18086
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);
18087
18249
  return true;
18088
18250
  }
18089
18251
  /** List available model aliases from the harness config (reloaded). */
@@ -18826,17 +18988,18 @@ async function runWebServer(opts) {
18826
18988
  res.end(JSON.stringify(manager.list()));
18827
18989
  return;
18828
18990
  }
18991
+ if (url === `${API_PREFIX}/workdir` && method === "GET") {
18992
+ sendJson(res, 200, { workDir: manager.defaultWorkDir });
18993
+ return;
18994
+ }
18829
18995
  if (url === `${API_PREFIX}/sessions` && method === "POST") {
18830
18996
  try {
18831
- const ws = await manager.createSession();
18997
+ const dir = await validateSessionWorkDir((await readJsonBody(req))["workDir"]);
18998
+ const ws = await manager.createSession(dir);
18832
18999
  res.writeHead(201, { "Content-Type": "application/json" });
18833
19000
  res.end(JSON.stringify(ws.getListItem()));
18834
19001
  } catch (error) {
18835
- res.writeHead(500, { "Content-Type": "application/json" });
18836
- res.end(JSON.stringify({
18837
- code: 500,
18838
- message: String(error)
18839
- }));
19002
+ sendHttpError(res, error);
18840
19003
  }
18841
19004
  return;
18842
19005
  }
@@ -19132,7 +19295,10 @@ async function runWebServer(opts) {
19132
19295
  if (url === "/" || url === "/index.html") {
19133
19296
  try {
19134
19297
  const html = await readFile(join(publicDir, "index.html"), "utf-8");
19135
- 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
+ });
19136
19302
  res.end(html);
19137
19303
  } catch {
19138
19304
  res.writeHead(500, { "Content-Type": "text/plain" });
@@ -19145,7 +19311,9 @@ async function runWebServer(opts) {
19145
19311
  const filePath = join(publicDir, safeUrl);
19146
19312
  const contentType = contentTypes[filePath.split(".").pop() ?? ""] ?? "application/octet-stream";
19147
19313
  const data = await readFile(filePath);
19148
- 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);
19149
19317
  res.end(data);
19150
19318
  } catch {
19151
19319
  res.writeHead(404, { "Content-Type": "text/plain" });