scream-code 0.15.5 → 0.15.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
- import { $t as isTodoItemShape, A as renderDiffLinesClustered, An as getInputHistoryFile, At as ENABLE_TERMINAL_FOCUS_REPORTING, B as BackgroundAgentStatusComponent, Bn as ScreamHarness, Bt as TERMINAL_THEME_LIGHT, Cn as PULSE_WAVE_FRAMES, Ct as createThemeStyles, D as toggleEmptySessionHint, Dn as saveTuiConfig, Dt as parseOsc11BackgroundTheme, E as isTurnElapsedEnabled, En as loadTuiConfig, Et as detectTerminalTheme, F as estimateTokens, Fn as CLI_USER_AGENT_PRODUCT, Ft as OSC11_RESPONSE_PREFIX_NO_ESC, G as getBreathingFrame, Gn as log, H as AgentGroupComponent, Hn as resolveScreamHome, Ht as isStreaming, I as getSharedSpeedTracker, In as PRODUCT_NAME, It as QUERY_TERMINAL_THEME, J as refineGoal, Jn as isOrphanedToolCallError, Jt as printableChar, K as resetBreathingClock, Kn as resolveGlobalLogPath, L as SkillActivationComponent, Ln as DEFAULT_CATALOG_URL, Lt as TERMINAL_FOCUS_IN, M as langFromPath, Mn as detectInstallSource, Mt as OSC11_QUERY, N as CachedContainer, Nn as CLI_COMMAND_NAME, Nt as OSC11_RESPONSE, O as ToolCallComponent, On as detectShellEnvironment, Ot as DISABLE_TERMINAL_FOCUS_REPORTING, P as ThinkingComponent, Pn as CLI_UI_MODE, Pt as OSC11_RESPONSE_PREFIX, Qt as formatErrorMessage, R as ReadGroupComponent, Rn as fetchCatalog, Rt as TERMINAL_FOCUS_OUT, S as getDaemonInstructions, Sn as PIXEL_PULSE_FRAMES, St as createMarkdownTheme, T as isEmptySessionHintDismissed, Tn as TuiLikePreferencesSchema, Tt as getColorPalette, U as WelcomeComponent, Un as MemoryMemoStore, Ut as FooterComponent, V as AssistantMessageComponent, Vt as isBusy, W as BREATHE_CYCLE_MS, Wn as flushDiagnosticLogs, Xn as SCREAM_ERROR_INFO, Xt as appendStreamingArgsPreview, Yn as ErrorCodes, Yt as STATUS_BULLET, Zt as argsRecord, _ as refreshUpdateCache, _n as isExperimentalFlagEnabled, _t as clearInfoPanelState, an as EMPTY_SESSION_HINT_URL, b as readJsonlFile, bn as isEmbeddingModelCached, bt as resolveThemeSync, c as hasDispose, cn as SESSION_TIPS, ct as handlePlanCommand, d as formatMemoryMemoForInjection, dn as getCtrlDHint, en as parseStreamingArgs, et as toTerminalHyperlink, fn as getLlmNotSetMessage, g as selectUpdateTarget, gn as sortSlashCommands, gt as supportsBalance, hn as BUILTIN_SLASH_COMMANDS, ht as refreshProviderBalance, i as buildRoleAdditionalText, in as CHARS_PER_TOKEN, j as highlightLines, jn as getLogDir, jt as ENABLE_TERMINAL_THEME_REPORTING, k as renderDiffLines, kn as getDataDir, kt as DISABLE_TERMINAL_THEME_REPORTING, l as isPlanExpandable, ln as TIP_ROTATION_INTERVAL_MS, mn as buildSkillSlashCommands, n as clearEvalPanelState, nn as stringValue, nt as getModelCycleLevel, on as EXIT_CONFIRM_WINDOW_MS, ot as handleFusionPlanCommand, pn as getNoActiveSessionMessage, q as clearGoalState, qn as isScreamError, r as openUrl, rn as truncateErrorMessage, s as disposeChildren, sn as MAIN_AGENT_ID, t as dispatchInput, tn as serializeToolResultOutput, tt as changeThinkingLevel, u as MoonLoader, un as getCtrlCHint, v as readUpdateCache, vn as setExperimentalFlags, w as UserMessageComponent, wn as TuiConfigParseError, wt as contrastTextHex, xn as startManualEmbeddingDownload, xt as createEditorTheme, y as appendJsonlLine, yn as getKnowledgeStore, z as parseReadGroupOutput, zn as saveCatalogCache, zt as TERMINAL_THEME_DARK } from "./dispatch-aNzh78xW.mjs";
8
- import { a as setLocale, o as t } from "./text-input-dialog-DbYfSAjy.mjs";
7
+ import { $t as argsRecord, A as renderDiffLinesClustered, An as detectShellEnvironment, At as DISABLE_TERMINAL_THEME_REPORTING, B as BackgroundAgentStatusComponent, Bn as fetchCatalog, Bt as TERMINAL_THEME_DARK, Cn as startManualEmbeddingDownload, Ct as createMarkdownTheme, D as toggleEmptySessionHint, Dn as TuiLikePreferencesSchema, Dt as detectTerminalTheme, E as isTurnElapsedEnabled, En as TuiConfigParseError, Et as getColorPalette, F as estimateTokens, Fn as CLI_COMMAND_NAME, Ft as OSC11_RESPONSE_PREFIX, G as getBreathingFrame, Gn as MemoryMemoStore, Gt as lerpGradient, H as AgentGroupComponent, Hn as ScreamHarness, Ht as isBusy, I as getSharedSpeedTracker, In as CLI_UI_MODE, It as OSC11_RESPONSE_PREFIX_NO_ESC, J as refineGoal, Jn as resolveGlobalLogPath, K as resetBreathingClock, Kn as flushDiagnosticLogs, L as SkillActivationComponent, Ln as CLI_USER_AGENT_PRODUCT, Lt as QUERY_TERMINAL_THEME, M as langFromPath, Mn as getInputHistoryFile, Mt as ENABLE_TERMINAL_THEME_REPORTING, N as CachedContainer, Nn as getLogDir, Nt as OSC11_QUERY, O as ToolCallComponent, On as loadTuiConfig, Ot as parseOsc11BackgroundTheme, P as ThinkingComponent, Pn as detectInstallSource, Pt as OSC11_RESPONSE, Qn as SCREAM_ERROR_INFO, Qt as appendStreamingArgsPreview, R as ReadGroupComponent, Rn as PRODUCT_NAME, Rt as TERMINAL_FOCUS_IN, S as getDaemonInstructions, Sn as isEmbeddingModelCached, St as createEditorTheme, T as isEmptySessionHintDismissed, Tn as PULSE_WAVE_FRAMES, Tt as contrastTextHex, U as WelcomeComponent, Ut as isStreaming, V as AssistantMessageComponent, Vn as saveCatalogCache, Vt as TERMINAL_THEME_LIGHT, W as BREATHE_CYCLE_MS, Wn as resolveScreamHome, Wt as FooterComponent, Xn as isOrphanedToolCallError, Xt as printableChar, Yn as isScreamError, Zn as ErrorCodes, Zt as STATUS_BULLET, _ as refreshUpdateCache, _n as BUILTIN_SLASH_COMMANDS, _t as supportsBalance, an as truncateErrorMessage, b as readJsonlFile, bn as setExperimentalFlags, c as hasDispose, cn as EXIT_CONFIRM_WINDOW_MS, d as formatMemoryMemoForInjection, dn as TIP_ROTATION_INTERVAL_MS, en as formatErrorMessage, et as toTerminalHyperlink, fn as getCtrlCHint, g as selectUpdateTarget, gn as buildSkillSlashCommands, gt as refreshProviderBalance, hn as getNoActiveSessionMessage, i as buildRoleAdditionalText, in as stringValue, j as highlightLines, jn as getDataDir, jt as ENABLE_TERMINAL_FOCUS_REPORTING, k as renderDiffLines, kn as saveTuiConfig, kt as DISABLE_TERMINAL_FOCUS_REPORTING, l as isPlanExpandable, ln as MAIN_AGENT_ID, lt as handlePlanCommand, mn as getLlmNotSetMessage, n as clearEvalPanelState, nn as parseStreamingArgs, nt as getModelCycleLevel, on as CHARS_PER_TOKEN, pn as getCtrlDHint, q as clearGoalState, qn as log, r as openUrl, rn as serializeToolResultOutput, s as disposeChildren, sn as EMPTY_SESSION_HINT_URL, st as handleFusionPlanCommand, t as dispatchInput, tn as isTodoItemShape, tt as changeThinkingLevel, u as MoonLoader, un as SESSION_TIPS, v as readUpdateCache, vn as sortSlashCommands, vt as clearInfoPanelState, w as UserMessageComponent, wn as PIXEL_PULSE_FRAMES, wt as createThemeStyles, xn as getKnowledgeStore, xt as resolveThemeSync, y as appendJsonlLine, yn as isExperimentalFlagEnabled, z as parseReadGroupOutput, zn as DEFAULT_CATALOG_URL, zt as TERMINAL_FOCUS_OUT } from "./dispatch-BJ6GyqMS.mjs";
8
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CnQM0DYj.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
11
11
  import { access, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -24,7 +24,7 @@ import process$1 from "node:process";
24
24
  import { Command, Option } from "commander";
25
25
  import { createInterface } from "node:readline/promises";
26
26
  import chalk, { chalkStderr } from "chalk";
27
- import { CombinedAutocompleteProvider, Container, Editor, Input, Key, ProcessTerminal, ScrollView, Spacer, Text, TuiAltScreen, VStack, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
27
+ import { CombinedAutocompleteProvider, Container, Editor, HStack, Input, Key, ProcessTerminal, ScrollView, Spacer, Text, TuiAltScreen, VStack, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
28
28
  import { createInterface as createInterface$1 } from "node:readline";
29
29
  import { toString } from "qrcode";
30
30
  //#region src/cli/build-info.ts
@@ -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.15.5"),
35
+ version: optionalBuildString("0.15.7"),
36
36
  channel: optionalBuildString(""),
37
37
  commit: optionalBuildString(""),
38
38
  buildTarget: optionalBuildString("darwin-arm64")
@@ -193,7 +193,8 @@ function createProgram(version, onMain, onPluginNodeRunner = () => {}, onStreamJ
193
193
  pluginDirs: subOpts["pluginDir"] ?? []
194
194
  });
195
195
  });
196
- program.command("web").description("启动浏览器 Web UI").option("--port <port>", "端口号", "3210").option("--model <model>", "LLM 模型别名").option("-y, --yolo", "自动批准所有操作", false).option("--auto", "以自动权限模式启动", false).option("--no-open", "不自动打开浏览器").option("--lan", "开启局域网共享(同网设备凭网关密钥访问)", false).option("--token <key>", "自定义网关访问密钥(保存后供 --lan 使用)").option("--reset-password", "交互式重设网关访问密钥", false).option("--skills-dir <dir>", "从该目录加载技能(可多次指定)", (value, previous) => [...previous ?? [], value], []).action((subOpts) => {
196
+ program.command("web").description("启动浏览器 Web UI").option("--port <port>", "端口号", "3210").option("--model <model>", "LLM 模型别名").option("-y, --yolo", "自动批准所有操作", false).option("--auto", "以自动权限模式启动", false).option("--no-open", "不自动打开浏览器").option("--lan", "开启局域网共享(同网设备凭网关密钥访问)", false).option("--token <key>", "自定义网关访问密钥(保存后供 --lan 使用)").option("--reset-password", "交互式重设网关访问密钥", false).option("--idle-minutes <minutes>", "无浏览器连接 N 分钟后自动退出(0=禁用,默认 15)", "15").option("--skills-dir <dir>", "从该目录加载技能(可多次指定)", (value, previous) => [...previous ?? [], value], []).action((subOpts) => {
197
+ const idleMinutes = Number(subOpts["idleMinutes"] ?? "15");
197
198
  onWeb({
198
199
  port: parseInt(subOpts["port"], 10) || 3210,
199
200
  model: subOpts["model"],
@@ -203,7 +204,8 @@ function createProgram(version, onMain, onPluginNodeRunner = () => {}, onStreamJ
203
204
  skillsDirs: subOpts["skillsDir"] ?? [],
204
205
  lan: subOpts["lan"] ?? false,
205
206
  token: subOpts["token"],
206
- resetPassword: subOpts["resetPassword"] ?? false
207
+ resetPassword: subOpts["resetPassword"] ?? false,
208
+ idleMinutes: Number.isFinite(idleMinutes) && idleMinutes >= 0 ? idleMinutes : 15
207
209
  });
208
210
  });
209
211
  program.command("channel").description("管理 cc-connect 消息平台通道").command("setup").description("配置 cc-connect 并选择要连接的平台").action(() => {
@@ -2023,6 +2025,200 @@ function formatBackgroundAgentTranscript(phase, meta, extras = void 0) {
2023
2025
  };
2024
2026
  }
2025
2027
  //#endregion
2028
+ //#region src/tui/utils/subagent-slots.ts
2029
+ /** Default subagent types, in the fixed display order of the slots. */
2030
+ const DEFAULT_SUBAGENT_TYPES = [
2031
+ "coder",
2032
+ "explore",
2033
+ "plan",
2034
+ "verify",
2035
+ "reviewer",
2036
+ "oracle",
2037
+ "worker",
2038
+ "writer"
2039
+ ];
2040
+ var SubagentSlots = class {
2041
+ slots = /* @__PURE__ */ new Map();
2042
+ instances = /* @__PURE__ */ new Map();
2043
+ /** Live instance count per type: a slot only returns to idle when the LAST
2044
+ * instance of that type terminates (WolfPack spawns several coders at once
2045
+ * and they share one visual slot). */
2046
+ runningByType = /* @__PURE__ */ new Map();
2047
+ /** main-side SendSubagentMessage tool callId → target subagent agentId. */
2048
+ messagingByCall = /* @__PURE__ */ new Map();
2049
+ /** Status to restore per TYPE when a messaging window closes, captured from
2050
+ * the first open window; concurrent windows on the same type keep counting
2051
+ * and the slot only exits messaging when the last window closes. */
2052
+ messagingPrev = /* @__PURE__ */ new Map();
2053
+ /** Count of open messaging windows per type (handles same-type siblings). */
2054
+ messagingCount = /* @__PURE__ */ new Map();
2055
+ reset() {
2056
+ this.slots.clear();
2057
+ this.instances.clear();
2058
+ this.runningByType.clear();
2059
+ this.messagingByCall.clear();
2060
+ this.messagingPrev.clear();
2061
+ this.messagingCount.clear();
2062
+ }
2063
+ /**
2064
+ * subagent.spawned. A first spawn starts the slot as working; a second
2065
+ * spawn of the SAME agentId means the main agent resumed it (返工中),
2066
+ * tracked via an instance generation counter.
2067
+ */
2068
+ onSpawned(agentId, subagentType, description) {
2069
+ const instance = this.instances.get(agentId);
2070
+ if (instance !== void 0) {
2071
+ instance.generation += 1;
2072
+ this.runningByType.set(instance.type, (this.runningByType.get(instance.type) ?? 0) + 1);
2073
+ this.setState(instance.type, "reworking", agentId, description === void 0 ? void 0 : `resume: ${description}`);
2074
+ return;
2075
+ }
2076
+ this.instances.set(agentId, {
2077
+ type: subagentType,
2078
+ generation: 1
2079
+ });
2080
+ this.runningByType.set(subagentType, (this.runningByType.get(subagentType) ?? 0) + 1);
2081
+ this.setState(subagentType, "working", agentId, description);
2082
+ }
2083
+ /** subagent.started. Keeps the current state (spawned already set it); only
2084
+ * used to materialize a slot if lifecycle events raced past us. */
2085
+ onStarted(agentId) {
2086
+ const instance = this.instances.get(agentId);
2087
+ if (instance === void 0) return;
2088
+ const slot = this.ensureSlot(instance.type);
2089
+ if (slot.status === "idle") {
2090
+ slot.status = "working";
2091
+ slot.agentId = agentId;
2092
+ slot.lastActivityAt = Date.now();
2093
+ }
2094
+ }
2095
+ /** subagent.completed / subagent.failed: the slot goes back to resting only
2096
+ * when the LAST live instance of that type terminates (siblings spawned by
2097
+ * the same tool call keep the slot busy). */
2098
+ onTerminated(agentId) {
2099
+ const instance = this.instances.get(agentId);
2100
+ if (instance === void 0) return;
2101
+ const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
2102
+ if (remaining > 0) {
2103
+ this.runningByType.set(instance.type, remaining);
2104
+ const slot = this.ensureSlot(instance.type);
2105
+ slot.detail = void 0;
2106
+ slot.lastActivityAt = Date.now();
2107
+ return;
2108
+ }
2109
+ this.runningByType.delete(instance.type);
2110
+ this.setState(instance.type, "idle", void 0, void 0);
2111
+ }
2112
+ /**
2113
+ * Routed subagent activity (agentId !== main):
2114
+ * - kind 'tool' → working (tool call in flight)
2115
+ * - kind 'output' → outputting (assistant/thinking delta)
2116
+ * - kind 'toolResult'→ detail only (the agent may keep working/outputting)
2117
+ */
2118
+ onActivity(agentId, kind, detail) {
2119
+ const instance = this.instances.get(agentId);
2120
+ if (instance === void 0) return;
2121
+ if (kind === "toolResult") {
2122
+ const slot = this.ensureSlot(instance.type);
2123
+ slot.detail = detail;
2124
+ slot.lastActivityAt = Date.now();
2125
+ return;
2126
+ }
2127
+ this.setState(instance.type, kind === "tool" ? "working" : "outputting", agentId, detail);
2128
+ }
2129
+ /**
2130
+ * Main-side SendSubagentMessage tool call started: the target subagent's
2131
+ * slot shows messaging until the tool call returns.
2132
+ */
2133
+ onMessagingStart(toolCallId, targetAgentId, operation) {
2134
+ if (targetAgentId === void 0) return;
2135
+ const instance = this.instances.get(targetAgentId);
2136
+ if (instance === void 0) return;
2137
+ this.messagingByCall.set(toolCallId, targetAgentId);
2138
+ const slot = this.ensureSlot(instance.type);
2139
+ if (slot.status !== "messaging") this.messagingPrev.set(instance.type, slot.status);
2140
+ this.messagingCount.set(instance.type, (this.messagingCount.get(instance.type) ?? 0) + 1);
2141
+ this.setState(instance.type, "messaging", targetAgentId, operation === void 0 ? void 0 : `message (${operation})`);
2142
+ }
2143
+ /** Tool call finished: the messaging window closes; restore the state the
2144
+ * slot had before the chat (working/outputting), else idle. */
2145
+ onMessagingEnd(toolCallId) {
2146
+ const target = this.messagingByCall.get(toolCallId);
2147
+ if (target === void 0) return;
2148
+ this.messagingByCall.delete(toolCallId);
2149
+ const instance = this.instances.get(target);
2150
+ if (instance === void 0) return;
2151
+ const slot = this.ensureSlot(instance.type);
2152
+ if (slot.status !== "messaging") return;
2153
+ const remaining = (this.messagingCount.get(instance.type) ?? 1) - 1;
2154
+ if (remaining > 0) {
2155
+ this.messagingCount.set(instance.type, remaining);
2156
+ return;
2157
+ }
2158
+ this.messagingCount.delete(instance.type);
2159
+ const prev = this.messagingPrev.get(instance.type);
2160
+ this.messagingPrev.delete(instance.type);
2161
+ slot.status = prev === "working" || prev === "outputting" || prev === "reworking" ? prev : "idle";
2162
+ slot.detail = void 0;
2163
+ slot.lastActivityAt = Date.now();
2164
+ }
2165
+ /**
2166
+ * All slots in fixed display order: the eight default types first (always
2167
+ * present, idle when never used), then extra types in spawn order.
2168
+ */
2169
+ getSlots() {
2170
+ const ordered = [];
2171
+ for (const type of DEFAULT_SUBAGENT_TYPES) {
2172
+ const slot = this.ensureSlot(type);
2173
+ ordered.push({
2174
+ ...slot,
2175
+ count: this.runningByType.get(type) ?? 0
2176
+ });
2177
+ }
2178
+ for (const slot of this.slots.values()) {
2179
+ if (DEFAULT_SUBAGENT_TYPES.includes(slot.type)) continue;
2180
+ ordered.push({
2181
+ ...slot,
2182
+ count: this.runningByType.get(slot.type) ?? 0
2183
+ });
2184
+ }
2185
+ return ordered;
2186
+ }
2187
+ ensureSlot(type) {
2188
+ let slot = this.slots.get(type);
2189
+ if (slot === void 0) {
2190
+ if (this.slots.size >= 16) {
2191
+ slot = {
2192
+ type,
2193
+ status: "idle",
2194
+ agentId: void 0,
2195
+ detail: void 0,
2196
+ count: 0,
2197
+ lastActivityAt: 0
2198
+ };
2199
+ return slot;
2200
+ }
2201
+ slot = {
2202
+ type,
2203
+ status: "idle",
2204
+ agentId: void 0,
2205
+ detail: void 0,
2206
+ count: 0,
2207
+ lastActivityAt: Date.now()
2208
+ };
2209
+ this.slots.set(type, slot);
2210
+ }
2211
+ return slot;
2212
+ }
2213
+ setState(type, status, agentId, detail) {
2214
+ const slot = this.ensureSlot(type);
2215
+ slot.status = status;
2216
+ slot.agentId = agentId;
2217
+ slot.detail = detail;
2218
+ slot.lastActivityAt = Date.now();
2219
+ }
2220
+ };
2221
+ //#endregion
2026
2222
  //#region src/tui/utils/background-task-status.ts
2027
2223
  const MAX_DETAIL_LENGTH = 240;
2028
2224
  function truncate(value) {
@@ -2237,15 +2433,15 @@ function formatStepDebugTiming(input) {
2237
2433
  const latency = input.llmFirstTokenLatencyMs;
2238
2434
  const streamMs = input.llmStreamDurationMs;
2239
2435
  if (latency === void 0 || streamMs === void 0) return void 0;
2240
- const parts = [`TTFT: ${formatDuration(latency)}`];
2436
+ const parts = [`TTFT: ${formatDuration$1(latency)}`];
2241
2437
  const outputTokens = input.usage?.output;
2242
2438
  if (outputTokens !== void 0 && outputTokens > 0 && streamMs > 0) {
2243
2439
  const tps = (outputTokens / (streamMs / 1e3)).toFixed(1);
2244
- parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration(streamMs)})`);
2440
+ parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration$1(streamMs)})`);
2245
2441
  }
2246
2442
  return `[Debug] ${parts.join(" | ")}`;
2247
2443
  }
2248
- function formatDuration(ms) {
2444
+ function formatDuration$1(ms) {
2249
2445
  if (ms < 1e3) return `${Math.round(ms)}ms`;
2250
2446
  return `${(ms / 1e3).toFixed(1)}s`;
2251
2447
  }
@@ -2310,7 +2506,30 @@ function detectCompactionAnomaly(input) {
2310
2506
  return null;
2311
2507
  }
2312
2508
  //#endregion
2509
+ //#region src/tui/types.ts
2510
+ /** Goal snapshot status, kept in sync with agent-core's GoalStatus union
2511
+ * (the wire type is plain string, hence the explicit union + normalizer). */
2512
+ const GOAL_STATUSES = [
2513
+ "active",
2514
+ "paused",
2515
+ "blocked",
2516
+ "complete"
2517
+ ];
2518
+ /** Clamp an unknown wire status to a safe display value: anything outside the
2519
+ * four known states is rendered as paused (never as completed). */
2520
+ function normalizeGoalStatus(status) {
2521
+ return GOAL_STATUSES.includes(status) ? status : "paused";
2522
+ }
2523
+ const INITIAL_LIVE_PANE = {
2524
+ pendingApproval: null,
2525
+ pendingQuestion: null
2526
+ };
2527
+ //#endregion
2313
2528
  //#region src/tui/controllers/session-event-handler.ts
2529
+ /** Short preview slice for subagent activity detail (keeps deltas small). */
2530
+ function sliceLeading(text, max = 48) {
2531
+ return text.length > max ? `${text.slice(0, max)}…` : text;
2532
+ }
2314
2533
  function addTokenUsage(a, b) {
2315
2534
  return {
2316
2535
  inputOther: a.inputOther + b.inputOther,
@@ -2319,6 +2538,20 @@ function addTokenUsage(a, b) {
2319
2538
  output: a.output + b.output
2320
2539
  };
2321
2540
  }
2541
+ /** Subtract cumulative usage: current minus previous (component-wise, clamped
2542
+ * at zero). Used to avoid double-counting a resumed subagent's cumulative
2543
+ * usage (each completed event carries the running total, not the delta). */
2544
+ function subtractTokenUsage(current, previous) {
2545
+ return {
2546
+ inputOther: Math.max(0, current.inputOther - previous.inputOther),
2547
+ inputCacheRead: Math.max(0, current.inputCacheRead - previous.inputCacheRead),
2548
+ inputCacheCreation: Math.max(0, current.inputCacheCreation - previous.inputCacheCreation),
2549
+ output: Math.max(0, current.output - previous.output)
2550
+ };
2551
+ }
2552
+ function isZeroUsage(u) {
2553
+ return u.inputOther === 0 && u.inputCacheRead === 0 && u.inputCacheCreation === 0 && u.output === 0;
2554
+ }
2322
2555
  var SessionEventHandler = class {
2323
2556
  host;
2324
2557
  constructor(host) {
@@ -2343,6 +2576,10 @@ var SessionEventHandler = class {
2343
2576
  * (spawned by the current turn's Agent tool), background agents persist. */
2344
2577
  foregroundSubagentCount = 0;
2345
2578
  subagentInfo = /* @__PURE__ */ new Map();
2579
+ /** Sidebar subagent slot state machine (see utils/subagent-slots.ts). */
2580
+ subagentSlots = new SubagentSlots();
2581
+ /** Tool callId of the in-flight UpdateGoal(complete) (judging), if any. */
2582
+ goalJudgeCallId;
2346
2583
  renderedSkillActivationIds = /* @__PURE__ */ new Set();
2347
2584
  renderedMcpServerStatusKeys = /* @__PURE__ */ new Map();
2348
2585
  mcpServerStatusSpinners = /* @__PURE__ */ new Map();
@@ -2351,12 +2588,18 @@ var SessionEventHandler = class {
2351
2588
  this.backgroundTasks.clear();
2352
2589
  this.backgroundTaskTranscriptedTerminal.clear();
2353
2590
  this.subagentInfo.clear();
2591
+ this.subagentSlots.reset();
2354
2592
  this.renderedSkillActivationIds.clear();
2355
2593
  this.renderedMcpServerStatusKeys.clear();
2356
2594
  this.stopAllMcpServerStatusSpinners();
2357
2595
  this.pendingSkillCandidates = [];
2358
2596
  this.promptedSkillCandidates.clear();
2359
- this.host.setAppState({ subagentUsage: {} });
2597
+ this.goalJudgeCallId = void 0;
2598
+ this.subagentUsageSeen.clear();
2599
+ this.host.setAppState({
2600
+ subagentUsage: {},
2601
+ goalJudge: "awaiting"
2602
+ });
2360
2603
  }
2361
2604
  startSubscription() {
2362
2605
  const { host } = this;
@@ -2520,9 +2763,42 @@ var SessionEventHandler = class {
2520
2763
  if (this.mcpServerStatusSpinners.size > 0) state.transcriptContainer.invalidate();
2521
2764
  this.mcpServerStatusSpinners.clear();
2522
2765
  }
2766
+ /** Read-only snapshot of the sidebar subagent slots (8 default types +
2767
+ * extras in spawn order). Consumed by the sidebar data provider. */
2768
+ getSubagentSlots() {
2769
+ return this.subagentSlots.getSlots();
2770
+ }
2771
+ /** Feed routed subagent activity into the slot state machine. */
2772
+ feedSubagentSlots(event, agentId) {
2773
+ switch (event.type) {
2774
+ case "subagent.spawned":
2775
+ this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
2776
+ break;
2777
+ case "subagent.started":
2778
+ this.subagentSlots.onStarted(event.subagentId);
2779
+ break;
2780
+ case "subagent.completed":
2781
+ case "subagent.failed":
2782
+ this.subagentSlots.onTerminated(event.subagentId);
2783
+ break;
2784
+ case "tool.call.started":
2785
+ this.subagentSlots.onActivity(agentId, "tool", `tool: ${event.name}`);
2786
+ break;
2787
+ case "tool.call.delta": break;
2788
+ case "tool.result":
2789
+ this.subagentSlots.onActivity(agentId, "toolResult", "tool result");
2790
+ break;
2791
+ case "assistant.delta":
2792
+ case "thinking.delta":
2793
+ this.subagentSlots.onActivity(agentId, "output", sliceLeading(event.delta ?? ""));
2794
+ break;
2795
+ default: break;
2796
+ }
2797
+ }
2523
2798
  routeSubagentEvent(event) {
2524
2799
  const subagentId = event.agentId;
2525
2800
  if (subagentId === "main") return false;
2801
+ this.feedSubagentSlots(event, subagentId);
2526
2802
  const { streamingUI } = this.host;
2527
2803
  const info = this.subagentInfo.get(subagentId);
2528
2804
  if (info === void 0 || info.parentToolCallId.length === 0) return true;
@@ -2750,6 +3026,16 @@ var SessionEventHandler = class {
2750
3026
  handleToolCall(event) {
2751
3027
  const { streamingUI } = this.host;
2752
3028
  streamingUI.flushNow();
3029
+ if (event.name === "SendSubagentMessage") {
3030
+ const args = argsRecord(event.args);
3031
+ const target = stringValue(args["agent_id"]);
3032
+ const operation = stringValue(args["operation"]);
3033
+ this.subagentSlots.onMessagingStart(event.toolCallId, target === void 0 || target.length === 0 ? void 0 : target, operation === void 0 || operation.length === 0 ? void 0 : operation);
3034
+ }
3035
+ if (event.name === "UpdateGoal" && stringValue(argsRecord(event.args)["status"]) === "complete") {
3036
+ this.goalJudgeCallId = event.toolCallId;
3037
+ this.host.setAppState({ goalJudge: "judging" });
3038
+ }
2753
3039
  const { turnId, step } = streamingUI.getTurnContext();
2754
3040
  const toolCall = {
2755
3041
  id: event.toolCallId,
@@ -2808,6 +3094,12 @@ var SessionEventHandler = class {
2808
3094
  }
2809
3095
  handleToolResult(event) {
2810
3096
  const { streamingUI } = this.host;
3097
+ this.subagentSlots.onMessagingEnd(event.toolCallId);
3098
+ if (event.toolCallId === this.goalJudgeCallId) {
3099
+ this.goalJudgeCallId = void 0;
3100
+ const goalStatus = this.host.state.appState.goal?.status;
3101
+ this.host.setAppState({ goalJudge: goalStatus === "complete" ? "adjudicated" : "awaiting" });
3102
+ }
2811
3103
  streamingUI.flushNow();
2812
3104
  const resultData = {
2813
3105
  tool_call_id: event.toolCallId,
@@ -2948,16 +3240,28 @@ var SessionEventHandler = class {
2948
3240
  }
2949
3241
  handleGoalUpdated(event) {
2950
3242
  const snapshot = event.snapshot;
2951
- if (snapshot === null) this.host.setAppState({
2952
- goal: null,
2953
- goalActive: false
2954
- });
2955
- else this.host.setAppState({
3243
+ if (snapshot === null) {
3244
+ this.host.setAppState({
3245
+ goal: null,
3246
+ goalActive: false
3247
+ });
3248
+ return;
3249
+ }
3250
+ if (snapshot.status === "complete") {
3251
+ this.goalJudgeCallId = void 0;
3252
+ this.host.setAppState({ goalJudge: "adjudicated" });
3253
+ } else if (this.host.state.appState.goalJudge === "adjudicated") this.host.setAppState({ goalJudge: "awaiting" });
3254
+ this.host.setAppState({
2956
3255
  goal: {
2957
3256
  objective: snapshot.objective,
3257
+ status: normalizeGoalStatus(snapshot.status),
2958
3258
  turnsUsed: snapshot.turnsUsed ?? 0,
2959
3259
  wallClockMs: snapshot.wallClockMs ?? 0,
2960
- wallClockBaseAt: Date.now()
3260
+ wallClockBaseAt: Date.now(),
3261
+ completionCriterion: snapshot.completionCriterion ?? null,
3262
+ tokensUsed: snapshot.tokensUsed ?? 0,
3263
+ inputTokens: snapshot.inputTokens ?? null,
3264
+ outputTokens: snapshot.outputTokens ?? null
2961
3265
  },
2962
3266
  goalActive: snapshot.status === "active"
2963
3267
  });
@@ -3059,6 +3363,7 @@ var SessionEventHandler = class {
3059
3363
  } else this.host.setAppState({ isCompacting: false });
3060
3364
  }
3061
3365
  handleSubagentSpawned(event) {
3366
+ this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
3062
3367
  const { streamingUI } = this.host;
3063
3368
  if (streamingUI.getActiveToolCall(event.parentToolCallId)?.name === "WolfPack") {
3064
3369
  const routingId = event.subagentId;
@@ -3119,6 +3424,7 @@ var SessionEventHandler = class {
3119
3424
  });
3120
3425
  }
3121
3426
  handleSubagentStarted(event) {
3427
+ this.subagentSlots.onStarted(event.subagentId);
3122
3428
  if (event.runInBackground) return;
3123
3429
  const routingId = this.subagentInfo.get(event.subagentId)?.parentToolCallId ?? event.parentToolCallId;
3124
3430
  const tc = this.host.streamingUI.getToolComponent(routingId);
@@ -3130,6 +3436,7 @@ var SessionEventHandler = class {
3130
3436
  });
3131
3437
  }
3132
3438
  handleSubagentCompleted(event) {
3439
+ this.subagentSlots.onTerminated(event.subagentId);
3133
3440
  this.recordSubagentUsage(event.subagentId, void 0, event.usage);
3134
3441
  const { streamingUI } = this.host;
3135
3442
  const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
@@ -3159,6 +3466,7 @@ var SessionEventHandler = class {
3159
3466
  streamingUI.removeToolComponentIfInactive(event.parentToolCallId);
3160
3467
  }
3161
3468
  handleSubagentFailed(event) {
3469
+ this.subagentSlots.onTerminated(event.subagentId);
3162
3470
  this.recordSubagentUsage(event.subagentId, void 0, event.usage);
3163
3471
  const { streamingUI } = this.host;
3164
3472
  const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
@@ -3184,13 +3492,21 @@ var SessionEventHandler = class {
3184
3492
  tc.onSubagentFailed({ error: event.error });
3185
3493
  streamingUI.removeToolComponentIfInactive(this.subagentInfo.get(event.subagentId)?.parentToolCallId ?? event.parentToolCallId);
3186
3494
  }
3495
+ /** Cumulative usage already recorded per subagent instance (completed
3496
+ * events carry the running total — we only add the delta since the last
3497
+ * completed/failed event, avoiding double-counting after resume). */
3498
+ subagentUsageSeen = /* @__PURE__ */ new Map();
3187
3499
  recordSubagentUsage(subagentId, subagentName, usage) {
3188
3500
  if (usage === void 0) return;
3501
+ const previous = this.subagentUsageSeen.get(subagentId);
3502
+ const delta = previous === void 0 ? usage : subtractTokenUsage(usage, previous);
3503
+ this.subagentUsageSeen.set(subagentId, usage);
3504
+ if (isZeroUsage(delta)) return;
3189
3505
  const name = this.subagentInfo.get(subagentId)?.name ?? subagentName ?? subagentId;
3190
3506
  const current = this.host.state.appState.subagentUsage[name];
3191
3507
  const next = {
3192
3508
  ...this.host.state.appState.subagentUsage,
3193
- [name]: current === void 0 ? usage : addTokenUsage(current, usage)
3509
+ [name]: current === void 0 ? delta : addTokenUsage(current, delta)
3194
3510
  };
3195
3511
  this.host.setAppState({ subagentUsage: next });
3196
3512
  }
@@ -5996,7 +6312,10 @@ var TranscriptController = class TranscriptController {
5996
6312
  committedComponent;
5997
6313
  liveComponentToEntry = /* @__PURE__ */ new Map();
5998
6314
  pendingComponents = /* @__PURE__ */ new Set();
5999
- static LIVE_LIMIT = 150;
6315
+ /** Max live transcript children before the oldest are folded into the
6316
+ * committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
6317
+ * (mirrors a commit-fold approach for bounding ultra-long sessions). */
6318
+ static LIVE_LIMIT = Number.isFinite(Number(process.env["SCREAM_TRANSCRIPT_LIVE_LIMIT"])) && Number(process.env["SCREAM_TRANSCRIPT_LIVE_LIMIT"]) > 0 ? Math.floor(Number(process.env["SCREAM_TRANSCRIPT_LIVE_LIMIT"])) : 150;
6000
6319
  constructor(host) {
6001
6320
  this.host = host;
6002
6321
  }
@@ -6341,6 +6660,469 @@ var StatusBarPaneComponent = class extends Container {
6341
6660
  }
6342
6661
  }
6343
6662
  };
6663
+ function clampSidebarWidth(cols) {
6664
+ if (!Number.isFinite(cols)) return 30;
6665
+ return Math.min(60, Math.max(24, Math.round(cols)));
6666
+ }
6667
+ //#endregion
6668
+ //#region src/tui/components/sidebar/panels/git-panel.ts
6669
+ /**
6670
+ * Summary-only Git panel (摘要式): the workdir name plus added (green) /
6671
+ * deleted (red) line counts and the changed-file count. Reads the current
6672
+ * snapshot in render so the panel never shows a stale working tree.
6673
+ */
6674
+ var GitPanelContent = class {
6675
+ ctx;
6676
+ colors;
6677
+ constructor(ctx, colors) {
6678
+ this.ctx = ctx;
6679
+ this.colors = colors;
6680
+ }
6681
+ invalidate() {}
6682
+ render(width) {
6683
+ const git = this.ctx.getData().git;
6684
+ if (git === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.git_no_repo"))];
6685
+ const dim = (s) => chalk.hex(this.colors.textDim)(s);
6686
+ const workDir = truncateToWidth(chalk.hex(this.colors.text)(basename(git.workDir)), width);
6687
+ if (git.diffAdded === 0 && git.diffDeleted === 0 && git.filesCount === 0) return [workDir, dim(t("sidebar.git_clean"))];
6688
+ return [workDir, truncateToWidth(`${chalk.hex(this.colors.success)(`+${git.diffAdded}`)} ${chalk.hex(this.colors.error)(`−${git.diffDeleted}`)} ${dim(t("sidebar.git_files", { count: git.filesCount }))}`, width)];
6689
+ }
6690
+ };
6691
+ const gitPanel = {
6692
+ id: "git",
6693
+ get title() {
6694
+ return t("sidebar.git");
6695
+ },
6696
+ width: 30,
6697
+ build(ctx) {
6698
+ return new GitPanelContent(ctx, ctx.colors);
6699
+ }
6700
+ };
6701
+ //#endregion
6702
+ //#region src/tui/utils/display-width.ts
6703
+ /** Display-column width of a string, matching the terminal layout engine
6704
+ * (ANSI sequences stripped, CJK/wide chars counted as 2 columns). */
6705
+ function displayWidth(text) {
6706
+ return visibleWidth(text);
6707
+ }
6708
+ /** Pad `label` to `target` display columns with spaces (CJK-aware). */
6709
+ function padLabel(label, target) {
6710
+ return label + " ".repeat(Math.max(0, target - displayWidth(label)));
6711
+ }
6712
+ //#endregion
6713
+ //#region src/tui/components/sidebar/panels/session-panel.ts
6714
+ function formatDuration(ms) {
6715
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
6716
+ if (getLocale() === "zh") {
6717
+ if (totalSeconds < 60) return `${totalSeconds}秒`;
6718
+ const hours = Math.floor(totalSeconds / 3600);
6719
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
6720
+ return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`;
6721
+ }
6722
+ if (totalSeconds < 60) return `${totalSeconds}s`;
6723
+ const hours = Math.floor(totalSeconds / 3600);
6724
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
6725
+ if (hours > 0) return `${hours}h ${minutes}m`;
6726
+ return `${minutes}m`;
6727
+ }
6728
+ /** 1,234 → "1,234" via the platform locale grouping. */
6729
+ function formatCount$1(n) {
6730
+ return n.toLocaleString("en-US");
6731
+ }
6732
+ /**
6733
+ * Session summary, live-rendered each frame: total token consumption and
6734
+ * total working time as the primary rows, turn/tool/message/compaction
6735
+ * counters as the secondary row. Uptime derives from `startedAt` so it ticks
6736
+ * between data updates; counts come from the snapshot in getData().
6737
+ */
6738
+ var SessionPanelContent = class {
6739
+ ctx;
6740
+ colors;
6741
+ constructor(ctx, colors) {
6742
+ this.ctx = ctx;
6743
+ this.colors = colors;
6744
+ }
6745
+ invalidate() {}
6746
+ render(width) {
6747
+ const stats = this.ctx.getData().sessionStats;
6748
+ if (stats === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.session_empty"))];
6749
+ const label = (s) => chalk.hex(this.colors.textDim)(s);
6750
+ const value = (s) => chalk.hex(this.colors.text)(s);
6751
+ const now = Date.now();
6752
+ const rows = [
6753
+ [t("sidebar.tokens"), formatCount$1(stats.tokensTotal)],
6754
+ [t("sidebar.tokens_cache_hit"), formatCount$1(stats.tokensInputCacheHit)],
6755
+ [t("sidebar.tokens_cache_miss"), formatCount$1(stats.tokensInputCacheMiss)],
6756
+ [t("sidebar.tokens_output"), formatCount$1(stats.tokensOutput)],
6757
+ [t("sidebar.uptime"), formatDuration(now - stats.startedAt)]
6758
+ ];
6759
+ const maxCols = Math.max(...rows.map(([k]) => displayWidth(k)));
6760
+ const lines = [];
6761
+ for (const [k, v] of rows) lines.push(label(padLabel(k, maxCols)) + value(v));
6762
+ lines.push(truncateToWidth(chalk.hex(this.colors.textDim)(`${t("sidebar.turns")} ${stats.turns} · ${t("sidebar.tools")} ${stats.toolCalls}`), width));
6763
+ lines.push(truncateToWidth(chalk.hex(this.colors.textDim)(`${t("sidebar.messages")} ${formatCount$1(stats.messages)} · ${t("sidebar.compacts")} ${stats.compactions}`), width));
6764
+ return lines;
6765
+ }
6766
+ };
6767
+ const sessionPanel = {
6768
+ id: "session",
6769
+ get title() {
6770
+ return t("sidebar.session");
6771
+ },
6772
+ width: 30,
6773
+ build(ctx) {
6774
+ return new SessionPanelContent(ctx, ctx.colors);
6775
+ }
6776
+ };
6777
+ //#endregion
6778
+ //#region src/tui/components/sidebar/panels/agents-panel.ts
6779
+ /** Gradient cycle for active agent slots. Shorter than the footer's 4s cycle:
6780
+ * with the 1s sidebar heartbeat each tick advances half the cycle, so the
6781
+ * colour jump is clearly visible instead of drifting imperceptibly. */
6782
+ const AGENT_GRADIENT_CYCLE_MS = 2e3;
6783
+ /** Localized status word per slot state. */
6784
+ function statusText(status) {
6785
+ switch (status) {
6786
+ case "idle": return t("sidebar.agent_idle");
6787
+ case "working": return t("sidebar.agent_working");
6788
+ case "outputting": return t("sidebar.agent_outputting");
6789
+ case "messaging": return t("sidebar.agent_messaging");
6790
+ case "reworking": return t("sidebar.agent_reworking");
6791
+ }
6792
+ }
6793
+ /**
6794
+ * Fixed 8-slot subagent panel: one row per default subagent type (plus any
6795
+ * custom types appended in spawn order). Live-rendered each frame from the
6796
+ * current snapshot; idle slots stay greyed out.
6797
+ */
6798
+ var AgentsPanelContent = class {
6799
+ ctx;
6800
+ colors;
6801
+ constructor(ctx, colors) {
6802
+ this.ctx = ctx;
6803
+ this.colors = colors;
6804
+ }
6805
+ invalidate() {}
6806
+ render(width) {
6807
+ const agents = this.ctx.getData().agents;
6808
+ if (agents === void 0 || agents.length === 0) return [];
6809
+ const lines = [];
6810
+ const nameWidth = Math.max(...agents.map((a) => displayWidth(a.type)));
6811
+ for (const [index, slot] of agents.entries()) lines.push(this.renderSlot(slot, index, nameWidth, width));
6812
+ return lines;
6813
+ }
6814
+ renderSlot(slot, index, nameWidth, width) {
6815
+ const dim = (s) => chalk.hex(this.colors.textDim)(s);
6816
+ if (slot.status === "idle") return `${dim("○")} ${dim(padLabel(slot.type, nameWidth))} ${dim(statusText("idle"))}`;
6817
+ const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .125) % 1;
6818
+ const active = slot.status === "working" || slot.status === "outputting" ? chalk.hex(lerpGradient(phase)).bold : void 0;
6819
+ return truncateToWidth(`${active ? active("●") : chalk.hex(this.statusColor(slot.status))("●")} ${chalk.hex(this.colors.textStrong)(padLabel(slot.type, nameWidth))} ${active ? active(statusText(slot.status)) : chalk.hex(this.colors.text)(statusText(slot.status))}${slot.count > 1 ? ` ${dim("×" + slot.count)}` : ""}`, width);
6820
+ }
6821
+ statusColor(status) {
6822
+ switch (status) {
6823
+ case "working":
6824
+ case "outputting": return this.colors.primary;
6825
+ case "messaging": return this.colors.accent;
6826
+ case "reworking": return this.colors.warning;
6827
+ case "idle": return this.colors.textDim;
6828
+ }
6829
+ }
6830
+ };
6831
+ const agentsPanel = {
6832
+ id: "agents",
6833
+ get title() {
6834
+ return t("sidebar.agents");
6835
+ },
6836
+ width: 34,
6837
+ build(ctx) {
6838
+ return new AgentsPanelContent(ctx, ctx.colors);
6839
+ }
6840
+ };
6841
+ //#endregion
6842
+ //#region src/tui/components/sidebar/panels/goal-panel.ts
6843
+ function formatWallClock(ms) {
6844
+ const totalMinutes = Math.max(0, Math.floor(ms / 6e4));
6845
+ const hours = Math.floor(totalMinutes / 60);
6846
+ const minutes = totalMinutes % 60;
6847
+ if (getLocale() === "zh") return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`;
6848
+ if (hours > 0) return `${hours}h ${minutes}m`;
6849
+ return `${minutes}m`;
6850
+ }
6851
+ function formatCount(n) {
6852
+ return n.toLocaleString("en-US");
6853
+ }
6854
+ function judgeText(state) {
6855
+ switch (state) {
6856
+ case "judging": return t("sidebar.goal_judge_judging");
6857
+ case "adjudicated": return t("sidebar.goal_judge_adjudicated");
6858
+ case "awaiting": return t("sidebar.goal_judge_awaiting");
6859
+ }
6860
+ }
6861
+ /**
6862
+ * Goal status panel, always visible: state badge (in-progress / completed /
6863
+ * paused / blocked / judging), objective, then the same two-column metric
6864
+ * layout as the Session panel (tokens spent / turns / wall-clock / judge),
6865
+ * and the completion criterion line.
6866
+ */
6867
+ var GoalPanelContent = class {
6868
+ ctx;
6869
+ colors;
6870
+ constructor(ctx, colors) {
6871
+ this.ctx = ctx;
6872
+ this.colors = colors;
6873
+ }
6874
+ invalidate() {}
6875
+ render(width) {
6876
+ const goal = this.ctx.getData().goal;
6877
+ if (goal === void 0) return [chalk.hex(this.colors.textDim)("○ " + t("sidebar.goal_idle"))];
6878
+ const dim = (s) => chalk.hex(this.colors.textDim)(s);
6879
+ const text = (s) => chalk.hex(this.colors.text)(s);
6880
+ const now = Date.now();
6881
+ const wallMs = goal.status === "active" ? goal.wallClockMs + Math.max(0, now - goal.wallClockBaseAt) : goal.wallClockMs;
6882
+ const lines = [];
6883
+ lines.push(this.renderBadge(goal.status, goal.judge, dim));
6884
+ lines.push(truncateToWidth(dim(goal.objective), width));
6885
+ const rows = [
6886
+ [t("sidebar.goal_tokens"), formatCount(goal.tokensUsed)],
6887
+ [t("sidebar.goal_tokens_input"), goal.inputTokens === null ? "—" : formatCount(goal.inputTokens)],
6888
+ [t("sidebar.goal_tokens_output"), goal.outputTokens === null ? "—" : formatCount(goal.outputTokens)],
6889
+ [t("sidebar.goal_turns"), String(goal.turnsUsed)],
6890
+ [t("sidebar.goal_wall"), formatWallClock(wallMs)],
6891
+ [t("sidebar.goal_judge"), judgeText(goal.judge)]
6892
+ ];
6893
+ const maxCols = Math.max(...rows.map(([k]) => displayWidth(k)));
6894
+ for (const [label, value] of rows) lines.push(dim(padLabel(label, maxCols)) + text(value));
6895
+ if (goal.completionCriterion !== null) lines.push(dim(padLabel(t("sidebar.goal_criterion"), maxCols)) + truncateToWidth(text(goal.completionCriterion), Math.max(4, width - maxCols)));
6896
+ return lines;
6897
+ }
6898
+ /** State badge: judging wins over everything; otherwise map the snapshot
6899
+ * status 1:1 (paused is NOT completed — fixes the interrupted-goal bug). */
6900
+ renderBadge(status, judge, dim) {
6901
+ if (judge === "judging") return chalk.hex(this.colors.accent)("⚖") + dim(" " + t("sidebar.goal_judging"));
6902
+ switch (status) {
6903
+ case "active": return chalk.hex(this.colors.primary)("●") + dim(" " + t("sidebar.goal_active"));
6904
+ case "complete": return chalk.hex(this.colors.success)("✓") + dim(" " + t("sidebar.goal_completed"));
6905
+ case "paused": return chalk.hex(this.colors.warning)("⏸") + dim(" " + t("sidebar.goal_paused"));
6906
+ case "blocked": return chalk.hex(this.colors.warning)("⚠") + dim(" " + t("sidebar.goal_blocked"));
6907
+ default: return dim(String(status));
6908
+ }
6909
+ }
6910
+ };
6911
+ const goalPanel = {
6912
+ id: "goal",
6913
+ get title() {
6914
+ return t("sidebar.goal");
6915
+ },
6916
+ width: 32,
6917
+ build(ctx) {
6918
+ return new GoalPanelContent(ctx, ctx.colors);
6919
+ }
6920
+ };
6921
+ //#endregion
6922
+ //#region src/utils/git/git-status.ts
6923
+ /**
6924
+ * Cached git branch + working-tree status (sidebar Git panel; previously
6925
+ * also the footer git badge, which the sidebar now replaces).
6926
+ *
6927
+ * Branch name refreshes every 5s, porcelain status every 15s. Branch
6928
+ * and status reads stay synchronous with short timeouts. Spawn failures
6929
+ * keep the previous snapshot instead of flashing an empty working tree.
6930
+ */
6931
+ const BRANCH_TTL_MS = 5e3;
6932
+ const STATUS_TTL_MS = 15e3;
6933
+ const SPAWN_TIMEOUT_MS = 500;
6934
+ const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
6935
+ function createGitStatusCache(workDir) {
6936
+ const isRepo = detectGitRepo(workDir);
6937
+ let branch = {
6938
+ value: null,
6939
+ fetchedAt: 0
6940
+ };
6941
+ let status = {
6942
+ dirty: false,
6943
+ ahead: 0,
6944
+ behind: 0,
6945
+ diffAdded: 0,
6946
+ diffDeleted: 0,
6947
+ files: [],
6948
+ fetchedAt: 0
6949
+ };
6950
+ return { getStatus: () => {
6951
+ if (!isRepo) return null;
6952
+ const now = Date.now();
6953
+ if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
6954
+ value: readBranch(workDir),
6955
+ fetchedAt: now
6956
+ };
6957
+ if (branch.value === null) return null;
6958
+ if (now - status.fetchedAt >= STATUS_TTL_MS) {
6959
+ const next = readStatus(workDir);
6960
+ if (next !== null) status = {
6961
+ ...next,
6962
+ fetchedAt: now
6963
+ };
6964
+ else status.fetchedAt = now;
6965
+ }
6966
+ return {
6967
+ branch: branch.value,
6968
+ dirty: status.dirty,
6969
+ ahead: status.ahead,
6970
+ behind: status.behind,
6971
+ diffAdded: status.diffAdded,
6972
+ diffDeleted: status.diffDeleted,
6973
+ files: status.files
6974
+ };
6975
+ } };
6976
+ }
6977
+ function detectGitRepo(workDir) {
6978
+ try {
6979
+ const result = spawnSync("git", [
6980
+ "-C",
6981
+ workDir,
6982
+ "rev-parse",
6983
+ "--is-inside-work-tree"
6984
+ ], {
6985
+ encoding: "utf8",
6986
+ timeout: SPAWN_TIMEOUT_MS
6987
+ });
6988
+ return result.status === 0 && result.stdout.trim() === "true";
6989
+ } catch {
6990
+ return false;
6991
+ }
6992
+ }
6993
+ function readBranch(workDir) {
6994
+ try {
6995
+ const result = spawnSync("git", [
6996
+ "-C",
6997
+ workDir,
6998
+ "branch",
6999
+ "--show-current"
7000
+ ], {
7001
+ encoding: "utf8",
7002
+ timeout: SPAWN_TIMEOUT_MS
7003
+ });
7004
+ if (result.status !== 0) return null;
7005
+ const name = result.stdout.trim();
7006
+ return name.length > 0 ? name : null;
7007
+ } catch {
7008
+ return null;
7009
+ }
7010
+ }
7011
+ /** Strip the quoting/escaping `git status --porcelain` applies when
7012
+ * `core.quotePath` is active and the path contains special characters. The
7013
+ * same semantics as the web server's parser: JSON.parse handles the C-style
7014
+ * escapes git emits; exotic cases stay literal, acceptable for display. */
7015
+ function unquotePorcelainPath(raw) {
7016
+ if (raw.length >= 2 && raw.startsWith("\"") && raw.endsWith("\"")) try {
7017
+ return JSON.parse(raw);
7018
+ } catch {
7019
+ return raw;
7020
+ }
7021
+ return raw;
7022
+ }
7023
+ /** Parse one porcelain entry line into a {@link GitFileEntry}. A rename entry
7024
+ * ("R old -> new") keeps the destination path, which is what the user wants
7025
+ * to see in a changed-files list. Spaces inside the XY code are dropped
7026
+ * ("M " → "M", "MM" stays), matching the web client's status label. */
7027
+ function parsePorcelainEntry(line) {
7028
+ const status = line.slice(0, 2).replaceAll(" ", "");
7029
+ let path = line.length > 3 ? line.slice(3).trim() : "";
7030
+ const arrow = path.indexOf(" -> ");
7031
+ if (arrow !== -1) path = path.slice(arrow + 4);
7032
+ return {
7033
+ status,
7034
+ path: unquotePorcelainPath(path)
7035
+ };
7036
+ }
7037
+ /** Returns null when the porcelain read fails (spawn error / non-zero exit);
7038
+ * the caller keeps the previous snapshot on failure. */
7039
+ function readStatus(workDir) {
7040
+ try {
7041
+ const result = spawnSync("git", [
7042
+ "-C",
7043
+ workDir,
7044
+ "status",
7045
+ "--porcelain",
7046
+ "-b"
7047
+ ], {
7048
+ encoding: "utf8",
7049
+ timeout: SPAWN_TIMEOUT_MS,
7050
+ maxBuffer: 4 * 1024 * 1024
7051
+ });
7052
+ if (result.status !== 0) return null;
7053
+ let dirty = false;
7054
+ let ahead = 0;
7055
+ let behind = 0;
7056
+ const files = [];
7057
+ for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
7058
+ const m = AHEAD_BEHIND_RE.exec(line);
7059
+ if (m) {
7060
+ ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
7061
+ behind = Number.parseInt(m[2] ?? "0", 10) || 0;
7062
+ }
7063
+ } else if (line.trim().length > 0) {
7064
+ dirty = true;
7065
+ files.push(parsePorcelainEntry(line));
7066
+ }
7067
+ const diff = dirty ? readDiffStats(workDir) : {
7068
+ added: 0,
7069
+ deleted: 0
7070
+ };
7071
+ return {
7072
+ dirty,
7073
+ ahead,
7074
+ behind,
7075
+ diffAdded: diff.added,
7076
+ diffDeleted: diff.deleted,
7077
+ files,
7078
+ fetchedAt: Date.now()
7079
+ };
7080
+ } catch {
7081
+ return null;
7082
+ }
7083
+ }
7084
+ function readDiffStats(workDir) {
7085
+ try {
7086
+ const result = spawnSync("git", [
7087
+ "-C",
7088
+ workDir,
7089
+ "diff",
7090
+ "--numstat",
7091
+ "HEAD",
7092
+ "--"
7093
+ ], {
7094
+ encoding: "utf8",
7095
+ timeout: SPAWN_TIMEOUT_MS,
7096
+ maxBuffer: 4 * 1024 * 1024
7097
+ });
7098
+ if (result.status !== 0) return {
7099
+ added: 0,
7100
+ deleted: 0
7101
+ };
7102
+ let added = 0;
7103
+ let deleted = 0;
7104
+ for (const line of result.stdout.split("\n")) {
7105
+ if (!line) continue;
7106
+ const [addedText, deletedText] = line.split(" ");
7107
+ added += parseDiffNumstatCount(addedText);
7108
+ deleted += parseDiffNumstatCount(deletedText);
7109
+ }
7110
+ return {
7111
+ added,
7112
+ deleted
7113
+ };
7114
+ } catch {
7115
+ return {
7116
+ added: 0,
7117
+ deleted: 0
7118
+ };
7119
+ }
7120
+ }
7121
+ function parseDiffNumstatCount(value) {
7122
+ if (value === void 0 || value === "-") return 0;
7123
+ const n = Number.parseInt(value, 10);
7124
+ return Number.isFinite(n) && n > 0 ? n : 0;
7125
+ }
6344
7126
  //#endregion
6345
7127
  //#region src/tui/theme/bundle.ts
6346
7128
  function createScreamTUIThemeBundle(theme, resolvedTheme) {
@@ -6686,8 +7468,23 @@ var LifecycleController = class LifecycleController {
6686
7468
  /** The active status-bar loader (PulseWaveLoader or MoonLoader). Owned by
6687
7469
  * this controller; stopped before replacement to avoid leaking timers. */
6688
7470
  statusBarLoader;
7471
+ /** Sum all four token buckets into the session's total token consumption. */
7472
+ static sumTokens(usage) {
7473
+ return usage.inputOther + usage.inputCacheRead + usage.inputCacheCreation + usage.output;
7474
+ }
6689
7475
  tipRotationTimer;
6690
7476
  currentTipIndex = Math.floor(Math.random() * SESSION_TIPS.length);
7477
+ /** Sidebar-only git cache (the footer owns its own). Rebuilt when the
7478
+ * workDir changes; `getStatus()` is TTL-cached so polling is cheap. */
7479
+ sidebarGitCache;
7480
+ sidebarGitCacheWorkDir = "";
7481
+ /** Session start time for the sidebar uptime counter. Reset on session
7482
+ * switch so a resumed old session doesn't show the TUI process age. */
7483
+ sidebarSessionStartedAt = Date.now();
7484
+ sidebarSessionId;
7485
+ /** Memo for the session-stats aggregation: recomputed only when the
7486
+ * transcript length, compaction count or usage object changes. */
7487
+ sidebarStatsMemo;
6691
7488
  static MEMORY_IDLE_MS = 900 * 1e3;
6692
7489
  static MEMORY_COUNTDOWN_MS = 15 * 1e3;
6693
7490
  static MEMORY_EXTRACT_COOLDOWN_MS = 600 * 1e3;
@@ -6870,8 +7667,30 @@ var LifecycleController = class LifecycleController {
6870
7667
  minSize: 1
6871
7668
  }
6872
7669
  ]);
7670
+ {
7671
+ const m = this.host.state.sidebarManager;
7672
+ if (!m.allPanels.some((p) => p.id === "git")) m.register(gitPanel);
7673
+ if (!m.allPanels.some((p) => p.id === "session")) m.register(sessionPanel);
7674
+ if (!m.allPanels.some((p) => p.id === "agents")) m.register(agentsPanel);
7675
+ if (!m.allPanels.some((p) => p.id === "goal")) m.register(goalPanel);
7676
+ m.setDataProvider(() => this.buildSidebarData());
7677
+ }
7678
+ const sidebar = this.host.state.sidebarPane;
6873
7679
  const layoutRoot = new VStack([{
6874
- component: transcriptScrollView,
7680
+ component: new HStack([{
7681
+ component: transcriptScrollView,
7682
+ basis: 0,
7683
+ grow: 1,
7684
+ shrink: 1,
7685
+ minSize: 1
7686
+ }, {
7687
+ component: sidebar,
7688
+ basis: "auto",
7689
+ grow: 0,
7690
+ shrink: 0,
7691
+ minSize: 0,
7692
+ visible: (viewport) => viewport.width >= 90
7693
+ }]),
6875
7694
  basis: 0,
6876
7695
  grow: 1,
6877
7696
  shrink: 1,
@@ -6886,6 +7705,86 @@ var LifecycleController = class LifecycleController {
6886
7705
  ui.setLayoutRoot(layoutRoot);
6887
7706
  this.host.state.layoutRoot = layoutRoot;
6888
7707
  }
7708
+ /** Build the sidebar data snapshot (git + session stats + goal) read by the
7709
+ * stacked panels. Panels re-read this every frame via getData(), so the
7710
+ * snapshot stays cheap: git is TTL-cached and stats are memoized. */
7711
+ buildSidebarData() {
7712
+ const appState = this.host.state.appState;
7713
+ return {
7714
+ git: this.readSidebarGit(appState.workDir),
7715
+ sessionStats: this.readSidebarSessionStats(appState),
7716
+ agents: this.host.sessionEventHandler.getSubagentSlots(),
7717
+ goal: this.readSidebarGoal(appState)
7718
+ };
7719
+ }
7720
+ readSidebarGit(workDir) {
7721
+ if (this.sidebarGitCache === void 0 || this.sidebarGitCacheWorkDir !== workDir) {
7722
+ this.sidebarGitCache = createGitStatusCache(workDir);
7723
+ this.sidebarGitCacheWorkDir = workDir;
7724
+ }
7725
+ const status = this.sidebarGitCache.getStatus();
7726
+ if (status === null) return void 0;
7727
+ return {
7728
+ workDir,
7729
+ diffAdded: status.diffAdded,
7730
+ diffDeleted: status.diffDeleted,
7731
+ filesCount: status.files.length
7732
+ };
7733
+ }
7734
+ readSidebarSessionStats(appState) {
7735
+ if (this.sidebarSessionId !== appState.sessionId) {
7736
+ this.sidebarSessionId = appState.sessionId;
7737
+ this.sidebarSessionStartedAt = Date.now();
7738
+ }
7739
+ const entries = this.host.state.transcriptEntries;
7740
+ const usage = appState.sessionUsage;
7741
+ const subagentUsage = appState.subagentUsage;
7742
+ const memo = this.sidebarStatsMemo;
7743
+ if (memo !== void 0 && memo.length === entries.length && memo.compactions === appState.autoCompactionCount && memo.usage === usage && memo.subagentUsage === subagentUsage) return memo.stats;
7744
+ let turns = 0;
7745
+ let toolCalls = 0;
7746
+ for (const entry of entries) {
7747
+ if (entry.kind === "user") turns += 1;
7748
+ if (entry.kind === "tool_call" && entry.compactionData === void 0) toolCalls += 1;
7749
+ }
7750
+ let subagentOutput = 0;
7751
+ for (const sub of Object.values(subagentUsage)) subagentOutput += sub.output;
7752
+ const stats = {
7753
+ turns,
7754
+ toolCalls,
7755
+ messages: entries.length,
7756
+ compactions: appState.autoCompactionCount,
7757
+ tokensTotal: LifecycleController.sumTokens(usage) + subagentOutput,
7758
+ tokensInputCacheHit: usage.inputCacheRead,
7759
+ tokensInputCacheMiss: usage.inputOther + usage.inputCacheCreation,
7760
+ tokensOutput: usage.output + subagentOutput,
7761
+ startedAt: this.sidebarSessionStartedAt
7762
+ };
7763
+ this.sidebarStatsMemo = {
7764
+ length: entries.length,
7765
+ compactions: appState.autoCompactionCount,
7766
+ usage,
7767
+ subagentUsage,
7768
+ stats
7769
+ };
7770
+ return stats;
7771
+ }
7772
+ readSidebarGoal(appState) {
7773
+ if (appState.goal === null) return void 0;
7774
+ return {
7775
+ objective: appState.goal.objective,
7776
+ status: appState.goal.status,
7777
+ turnsUsed: appState.goal.turnsUsed,
7778
+ wallClockMs: appState.goal.wallClockMs,
7779
+ wallClockBaseAt: appState.goal.wallClockBaseAt,
7780
+ continuationCount: appState.goalContinuationCount,
7781
+ completionCriterion: appState.goal.completionCriterion,
7782
+ tokensUsed: appState.goal.tokensUsed,
7783
+ inputTokens: appState.goal.inputTokens,
7784
+ outputTokens: appState.goal.outputTokens,
7785
+ judge: appState.goalJudge
7786
+ };
7787
+ }
6889
7788
  mountFooter() {
6890
7789
  this.ensureFooterWrap();
6891
7790
  this.footerWrap.addChild(this.host.state.footer);
@@ -7944,12 +8843,6 @@ var QuestionController = class extends ReverseRpcController {
7944
8843
  }
7945
8844
  };
7946
8845
  //#endregion
7947
- //#region src/tui/types.ts
7948
- const INITIAL_LIVE_PANE = {
7949
- pendingApproval: null,
7950
- pendingQuestion: null
7951
- };
7952
- //#endregion
7953
8846
  //#region src/utils/clipboard/clipboard-text.ts
7954
8847
  /**
7955
8848
  * Write plain text to the system clipboard with platform-specific commands.
@@ -8104,6 +8997,222 @@ function createRenderBatcher(doRender) {
8104
8997
  };
8105
8998
  }
8106
8999
  //#endregion
9000
+ //#region src/tui/components/sidebar/sidebar-container.ts
9001
+ const SIDE_PADDING = 1;
9002
+ /**
9003
+ * Renders the stacked sidebar: every visible registered panel gets its own
9004
+ * full box frame (┌─┐│└┘) with the panel title embedded in the top frame, one
9005
+ * below the other. Panels render live each frame (their body reads the
9006
+ * current data snapshot), so time-based values (uptime, goal wall-clock)
9007
+ * tick without rebuilding. The focused panel's title is bolded; focus is
9008
+ * moved by the SidebarManager (`/sidebar next/prev`, Ctrl+X toggles).
9009
+ * The whole frame is painted in the theme accent colour and the rendered
9010
+ * width follows the SidebarManager so `/sidebar width` takes effect.
9011
+ */
9012
+ var SidebarContainer = class extends Container {
9013
+ manager;
9014
+ panelContext;
9015
+ accentHex;
9016
+ /** Built panel bodies, keyed by panel id. Bodies render live so they are
9017
+ * only rebuilt when the visible panel set changes. */
9018
+ bodies = /* @__PURE__ */ new Map();
9019
+ builtStackKey = "";
9020
+ /** 1s heartbeat while the sidebar is open: keeps live values (uptime,
9021
+ * goal wall-clock, gradient phase of busy agent slots) ticking without
9022
+ * needing an event. Stopped when the sidebar closes. */
9023
+ heartbeat;
9024
+ constructor(manager, requestRender, colors) {
9025
+ super();
9026
+ this.manager = manager;
9027
+ this.accentHex = colors.primary;
9028
+ this.panelContext = {
9029
+ requestRender,
9030
+ getData: () => this.manager.getData(),
9031
+ colors
9032
+ };
9033
+ this.heartbeat = setInterval(() => {
9034
+ if (this.manager.isOpen) requestRender();
9035
+ }, 1e3);
9036
+ this.heartbeat.unref();
9037
+ }
9038
+ /** Stop the heartbeat. Called by the TUI teardown (see scream-tui.ts). */
9039
+ dispose() {
9040
+ if (this.heartbeat !== void 0) clearInterval(this.heartbeat);
9041
+ }
9042
+ /** Re-sync built bodies with the current visible stack (call on register/
9043
+ * unregister/activate changes; cheap idempotent check). */
9044
+ refresh() {
9045
+ const key = this.manager.isOpen ? this.manager.getStackPanels().map((panel) => panel.id).join("|") : "";
9046
+ if (key === this.builtStackKey) return;
9047
+ const keep = /* @__PURE__ */ new Set();
9048
+ if (this.manager.isOpen) for (const panel of this.manager.getStackPanels()) {
9049
+ keep.add(panel.id);
9050
+ if (!this.bodies.has(panel.id)) this.bodies.set(panel.id, panel.build(this.panelContext));
9051
+ }
9052
+ for (const id of [...this.bodies.keys()]) if (!keep.has(id)) this.bodies.delete(id);
9053
+ this.builtStackKey = key;
9054
+ this.clear();
9055
+ }
9056
+ render(_width) {
9057
+ if (!this.manager.isOpen) return [];
9058
+ this.refresh();
9059
+ const panels = this.manager.getStackPanels();
9060
+ if (panels.length === 0) return [];
9061
+ const lines = [""];
9062
+ const horzLen = Math.max(2, this.manager.currentWidth - 2);
9063
+ const contentWidth = Math.max(1, horzLen - 2 * SIDE_PADDING);
9064
+ const paint = (s) => chalk.hex(this.accentHex)(s);
9065
+ const activeId = this.manager.activePanel?.id ?? null;
9066
+ for (const panel of panels) {
9067
+ const body = this.bodies.get(panel.id);
9068
+ if (body === void 0) continue;
9069
+ const title = truncateToWidth(String(panel.title), horzLen);
9070
+ const titleText = panel.id === activeId ? chalk.bold(title) : title;
9071
+ const trailing = Math.max(0, horzLen - visibleWidth(title));
9072
+ lines.push(paint("┌") + paint(titleText) + paint("─".repeat(trailing)) + paint("┐"));
9073
+ for (const raw of body.render(contentWidth)) {
9074
+ const row = truncateToWidth(raw, contentWidth);
9075
+ const pad = Math.max(0, contentWidth - visibleWidth(row));
9076
+ lines.push(paint("│") + " " + row + " ".repeat(pad) + " " + paint("│"));
9077
+ }
9078
+ lines.push(paint("└") + paint("─".repeat(horzLen)) + paint("┘"));
9079
+ }
9080
+ return lines;
9081
+ }
9082
+ };
9083
+ //#endregion
9084
+ //#region src/tui/components/sidebar/sidebar-manager.ts
9085
+ /**
9086
+ * Owns which sidebar panels are registered, which one is active, whether the
9087
+ * sidebar is open, and its width. Rendering is delegated back to the host via
9088
+ * the injected `requestRender`; the manager itself holds no Component state.
9089
+ */
9090
+ var SidebarManager = class {
9091
+ panels = [];
9092
+ byId = /* @__PURE__ */ new Map();
9093
+ activeId = null;
9094
+ open = false;
9095
+ width = 30;
9096
+ widthStore;
9097
+ dataProvider;
9098
+ requestRender;
9099
+ constructor(requestRender, opts) {
9100
+ this.requestRender = requestRender;
9101
+ this.widthStore = opts?.widthStore;
9102
+ this.dataProvider = opts?.dataProvider;
9103
+ if (opts?.widthStore !== void 0) this.width = clampSidebarWidth(opts.widthStore.load());
9104
+ }
9105
+ getData() {
9106
+ return this.dataProvider?.() ?? {};
9107
+ }
9108
+ setDataProvider(provider) {
9109
+ this.dataProvider = provider;
9110
+ }
9111
+ register(panel) {
9112
+ if (this.byId.has(panel.id)) return;
9113
+ this.panels.push(panel);
9114
+ this.byId.set(panel.id, panel);
9115
+ }
9116
+ unregister(id) {
9117
+ const idx = this.panels.findIndex((p) => p.id === id);
9118
+ if (idx === -1) return;
9119
+ const [panel] = this.panels.splice(idx, 1);
9120
+ this.byId.delete(id);
9121
+ if (this.activeId === id) {
9122
+ this.activeId = null;
9123
+ this.open = false;
9124
+ panel?.onClose?.();
9125
+ }
9126
+ this.requestRender();
9127
+ }
9128
+ get isOpen() {
9129
+ return this.open;
9130
+ }
9131
+ get hasPanels() {
9132
+ return this.panels.length > 0;
9133
+ }
9134
+ get activePanel() {
9135
+ return this.activeId === null ? void 0 : this.byId.get(this.activeId);
9136
+ }
9137
+ get currentWidth() {
9138
+ return this.width;
9139
+ }
9140
+ get allPanels() {
9141
+ return this.panels;
9142
+ }
9143
+ /**
9144
+ * Panels that should render in the current stacked sidebar, in registration
9145
+ * order, filtered by each panel's `visible(data)` predicate (panels without
9146
+ * one are always visible).
9147
+ */
9148
+ getStackPanels() {
9149
+ const data = this.getData();
9150
+ return this.panels.filter((panel) => panel.visible?.(data) ?? true);
9151
+ }
9152
+ /** True when `panel` would currently render in the stack. */
9153
+ isStacked(panel) {
9154
+ const data = this.getData();
9155
+ return panel.visible?.(data) ?? true;
9156
+ }
9157
+ activate(id) {
9158
+ const panel = this.byId.get(id);
9159
+ if (panel === void 0) return false;
9160
+ if (this.open && this.activeId === id) return true;
9161
+ (this.activeId === null ? void 0 : this.byId.get(this.activeId))?.onClose?.();
9162
+ this.activeId = id;
9163
+ this.open = true;
9164
+ panel.onOpen?.();
9165
+ this.requestRender();
9166
+ return true;
9167
+ }
9168
+ toggle(id) {
9169
+ if (id === void 0 && this.activeId === null) {
9170
+ const [first] = this.getStackPanels();
9171
+ if (first === void 0) return;
9172
+ this.activate(first.id);
9173
+ return;
9174
+ }
9175
+ const target = id ?? this.activeId;
9176
+ if (target === null || !this.byId.has(target)) return;
9177
+ if (this.open && this.activeId === target) {
9178
+ this.close();
9179
+ return;
9180
+ }
9181
+ this.activate(target);
9182
+ }
9183
+ close() {
9184
+ if (!this.open) return;
9185
+ const panel = this.activePanel;
9186
+ this.open = false;
9187
+ this.activeId = null;
9188
+ panel?.onClose?.();
9189
+ this.requestRender();
9190
+ }
9191
+ next() {
9192
+ this.step(1);
9193
+ }
9194
+ prev() {
9195
+ this.step(-1);
9196
+ }
9197
+ step(dir) {
9198
+ const stack = this.getStackPanels();
9199
+ if (stack.length === 0) return;
9200
+ const idx = this.activeId === null ? -1 : stack.findIndex((p) => p.id === this.activeId);
9201
+ const nextIdx = idx === -1 ? dir === 1 ? 0 : stack.length - 1 : (idx + dir + stack.length) % stack.length;
9202
+ this.activate(stack[nextIdx].id);
9203
+ }
9204
+ setWidth(cols) {
9205
+ this.width = clampSidebarWidth(cols);
9206
+ this.widthStore?.save(this.width);
9207
+ this.requestRender();
9208
+ }
9209
+ resetWidth() {
9210
+ this.width = 30;
9211
+ this.widthStore?.save(this.width);
9212
+ this.requestRender();
9213
+ }
9214
+ };
9215
+ //#endregion
8107
9216
  //#region src/tui/components/chrome/error-banner.ts
8108
9217
  const MAX_BANNER_LINES = 3;
8109
9218
  const CONTINUATION_INDENT = " ";
@@ -8426,6 +9535,7 @@ var CustomEditor = class extends Editor {
8426
9535
  onCtrlB;
8427
9536
  onToggleToolExpand;
8428
9537
  onTogglePlanExpand;
9538
+ onToggleSidebar;
8429
9539
  onOpenExternalEditor;
8430
9540
  onCtrlS;
8431
9541
  onCtrlW;
@@ -8605,6 +9715,10 @@ var CustomEditor = class extends Editor {
8605
9715
  if (matchesKey(normalized, Key.ctrl("e"))) {
8606
9716
  if (this.onTogglePlanExpand?.() === true) return;
8607
9717
  }
9718
+ if (matchesKey(normalized, Key.ctrl("x"))) {
9719
+ this.onToggleSidebar?.();
9720
+ return;
9721
+ }
8608
9722
  if (matchesKey(normalized, Key.ctrl("s"))) {
8609
9723
  this.onCtrlS?.();
8610
9724
  return;
@@ -8931,7 +10045,7 @@ function createTUIState(options) {
8931
10045
  const base64 = Buffer.from(text, "utf8").toString("base64");
8932
10046
  if (base64.length > OSC52_MAX_TEXT_LENGTH) return false;
8933
10047
  try {
8934
- terminal.write(`\x1B]52;c;${base64}\u0007`);
10048
+ terminal.write(`\u001B]52;c;${base64}\u0007`);
8935
10049
  } catch {
8936
10050
  return false;
8937
10051
  }
@@ -8979,10 +10093,19 @@ function createTUIState(options) {
8979
10093
  editor.thinkingLevel = initialAppState.thinkingLevel;
8980
10094
  editor.permissionMode = initialAppState.permissionMode ?? "manual";
8981
10095
  editor.toolPriority = initialAppState.like?.toolPriority;
10096
+ const footer = new FooterComponent({ ...initialAppState }, theme.colors, ui);
10097
+ const sidebarManager = new SidebarManager(() => ui.requestRender());
10098
+ const sidebarContainer = new SidebarContainer(sidebarManager, () => ui.requestRender(), theme.colors);
10099
+ const sidebarPane = new GutterContainer(1, 1);
10100
+ sidebarPane.addChild(sidebarContainer);
10101
+ editor.onToggleSidebar = () => sidebarManager.toggle();
8982
10102
  return {
8983
10103
  ui,
8984
10104
  terminal,
8985
10105
  layoutRoot: void 0,
10106
+ sidebarManager,
10107
+ sidebarContainer,
10108
+ sidebarPane,
8986
10109
  transcriptContainer,
8987
10110
  activityContainer,
8988
10111
  statusBarContainer,
@@ -8994,9 +10117,7 @@ function createTUIState(options) {
8994
10117
  planModeBanner,
8995
10118
  planModeBannerContainer,
8996
10119
  editorContainer,
8997
- footer: new FooterComponent({ ...initialAppState }, theme.colors, ui, () => {
8998
- ui.requestRender();
8999
- }),
10120
+ footer,
9000
10121
  editor,
9001
10122
  theme,
9002
10123
  appState: { ...initialAppState },
@@ -9582,11 +10703,17 @@ var SessionManager$1 = class {
9582
10703
  sessionTitle: session.summary?.title ?? null,
9583
10704
  goal: goal ? {
9584
10705
  objective: goal.objective,
10706
+ status: normalizeGoalStatus(goal.status),
9585
10707
  turnsUsed: goal.turnsUsed ?? 0,
9586
10708
  wallClockMs: goal.wallClockMs ?? 0,
9587
- wallClockBaseAt: Date.now()
10709
+ wallClockBaseAt: Date.now(),
10710
+ completionCriterion: goal.completionCriterion ?? null,
10711
+ tokensUsed: goal.tokensUsed ?? 0,
10712
+ inputTokens: goal.inputTokens ?? null,
10713
+ outputTokens: goal.outputTokens ?? null
9588
10714
  } : null,
9589
10715
  goalActive: goal?.status === "active",
10716
+ goalJudge: "awaiting",
9590
10717
  goalContinuationCount: 0,
9591
10718
  providerBalance: null
9592
10719
  });
@@ -11943,6 +13070,7 @@ function createInitialAppState(input) {
11943
13070
  sessionTitle: null,
11944
13071
  goal: null,
11945
13072
  goalActive: false,
13073
+ goalJudge: "awaiting",
11946
13074
  goalContinuationCount: 0,
11947
13075
  ccConnectActive: false,
11948
13076
  wolfpackMode: input.cliOptions.wolfpack === true,
@@ -12173,6 +13301,7 @@ var ScreamTUI = class {
12173
13301
  this.lifecycleController.disposeTerminalTracking();
12174
13302
  this.inputController.dispose();
12175
13303
  this.state.footer.dispose();
13304
+ this.state.sidebarContainer.dispose();
12176
13305
  await this.closeSession();
12177
13306
  await this.harness.close();
12178
13307
  this.sessionEventHandler.stopAllMcpServerStatusSpinners();
@@ -12443,7 +13572,7 @@ var ScreamTUI = class {
12443
13572
  const card = new OnboardingCardComponent({
12444
13573
  colors: this.state.theme.colors,
12445
13574
  onConfigure: () => {
12446
- import("./dispatch-B23MoYWm.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
13575
+ import("./dispatch-CPK1Lkxu.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
12447
13576
  this.restoreEditor();
12448
13577
  onDone?.();
12449
13578
  });
@@ -15005,6 +16134,12 @@ var WebSession = class {
15005
16134
  unsubscribe = null;
15006
16135
  busy = false;
15007
16136
  /**
16137
+ * In-flight turn count across all agents (main + subagents). Used by the
16138
+ * idle-exit watchdog: a single boolean is not enough because a subagent's
16139
+ * `turn.ended` must not clear the busy state of a still-running main turn.
16140
+ */
16141
+ activeTurns = 0;
16142
+ /**
15008
16143
  * Live assistant message under construction. Mirrors the same state machine
15009
16144
  * used by buildMessages() so turn.ended can persist a complete durable
15010
16145
  * snapshot (`web.message.finalized`) — the only source that survives a
@@ -15044,6 +16179,10 @@ var WebSession = class {
15044
16179
  get isBusy() {
15045
16180
  return this.busy;
15046
16181
  }
16182
+ /** True while any agent turn (main or subagent) is in flight. */
16183
+ get isTurnActive() {
16184
+ return this.activeTurns > 0;
16185
+ }
15047
16186
  getTitle() {
15048
16187
  return this.title;
15049
16188
  }
@@ -15173,6 +16312,7 @@ var WebSession = class {
15173
16312
  if ((event.type === "goal.updated" || event.type === "todo.updated") && event.agentId !== "main") return;
15174
16313
  if (event.type === "turn.started") {
15175
16314
  this.busy = true;
16315
+ this.activeTurns += 1;
15176
16316
  this.liveTurnCount += 1;
15177
16317
  this.liveAssistant = {
15178
16318
  role: "assistant",
@@ -15192,7 +16332,8 @@ var WebSession = class {
15192
16332
  }
15193
16333
  };
15194
16334
  } else if (event.type === "turn.ended") {
15195
- this.busy = false;
16335
+ this.activeTurns = Math.max(0, this.activeTurns - 1);
16336
+ this.busy = this.activeTurns > 0;
15196
16337
  const live = this.liveAssistant;
15197
16338
  if (event.reason === "failed" && live) live.isError = true;
15198
16339
  if (live) {
@@ -15410,6 +16551,7 @@ var WebSession = class {
15410
16551
  }, false);
15411
16552
  this.session.prompt(text).catch((error) => {
15412
16553
  this.busy = false;
16554
+ this.activeTurns = 0;
15413
16555
  this.sendError(ws, error, clientMessageId);
15414
16556
  });
15415
16557
  break;
@@ -15532,8 +16674,9 @@ var WebSession = class {
15532
16674
  }
15533
16675
  return;
15534
16676
  case "auto":
15535
- case "yes": {
15536
- const mode = command === "auto" ? "auto" : "yolo";
16677
+ case "yes":
16678
+ case "bot": {
16679
+ const mode = command === "auto" ? "auto" : command === "bot" ? "bot" : "yolo";
15537
16680
  try {
15538
16681
  await this.session.setPermission(mode);
15539
16682
  this.permission = mode;
@@ -15890,6 +17033,7 @@ var WebSession = class {
15890
17033
  await session.prompt(prompt);
15891
17034
  } catch (error) {
15892
17035
  this.busy = false;
17036
+ this.activeTurns = 0;
15893
17037
  throw error;
15894
17038
  }
15895
17039
  }
@@ -16269,6 +17413,11 @@ var SessionManager = class {
16269
17413
  list() {
16270
17414
  return Array.from(this.sessions.values()).map((s) => s.getListItem()).toSorted((a, b) => b.createdAt - a.createdAt);
16271
17415
  }
17416
+ /** True when any session has an agent turn (main or subagent) in flight, or
17417
+ * any session runs in unattended bot mode (defers idle exit). */
17418
+ anyBusy() {
17419
+ return Array.from(this.sessions.values()).some((s) => s.isTurnActive || s.permission === "bot");
17420
+ }
16272
17421
  async delete(sessionId) {
16273
17422
  const ws = this.sessions.get(sessionId);
16274
17423
  if (!ws) return false;
@@ -16483,6 +17632,48 @@ var SessionManager = class {
16483
17632
  return this.harness.preflight();
16484
17633
  }
16485
17634
  };
17635
+ /**
17636
+ * Watchdog shared by both web entry points. Detects "nobody is using the UI"
17637
+ * as: zero WebSocket connections AND no HTTP request for `idleMs`, with an
17638
+ * in-flight agent turn (`busy()`) deferring the exit so background work is
17639
+ * never interrupted. The `shutdown` call is awaited before `exit` and must be
17640
+ * idempotent (it can race with a manual SIGINT/SIGTERM stop).
17641
+ */
17642
+ function startWebIdleExit(opts) {
17643
+ const idleMs = opts.idleMs;
17644
+ if (!(idleMs > 0)) return {
17645
+ touch: () => {},
17646
+ noteConnection: () => {},
17647
+ noteDisconnect: () => {},
17648
+ stop: () => {}
17649
+ };
17650
+ const exitFn = opts.exit ?? ((code) => process.exit(code));
17651
+ let activeConnections = 0;
17652
+ let lastActivityAt = Date.now();
17653
+ const checkMs = Math.min(6e4, Math.max(100, idleMs / 2));
17654
+ const timer = setInterval(() => {
17655
+ if (activeConnections > 0) return;
17656
+ if (opts.busy()) return;
17657
+ if (Date.now() - lastActivityAt < idleMs) return;
17658
+ Promise.resolve(opts.shutdown()).finally(() => exitFn(0));
17659
+ }, checkMs);
17660
+ return {
17661
+ touch: () => {
17662
+ lastActivityAt = Date.now();
17663
+ },
17664
+ noteConnection: () => {
17665
+ activeConnections += 1;
17666
+ lastActivityAt = Date.now();
17667
+ },
17668
+ noteDisconnect: () => {
17669
+ activeConnections = Math.max(0, activeConnections - 1);
17670
+ lastActivityAt = Date.now();
17671
+ },
17672
+ stop: () => {
17673
+ clearInterval(timer);
17674
+ }
17675
+ };
17676
+ }
16486
17677
  async function handleSessionControlRoutes(req, res, url, method, manager) {
16487
17678
  const statusMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/status$`).exec(url);
16488
17679
  if (statusMatch && method === "GET") {
@@ -16938,9 +18129,15 @@ async function runWebServer(opts) {
16938
18129
  publicDir = sourcePublicDir;
16939
18130
  } catch {}
16940
18131
  }
18132
+ const idleExit = startWebIdleExit({
18133
+ idleMs: (opts.idleMinutes ?? 15) * 6e4,
18134
+ busy: () => manager.anyBusy(),
18135
+ shutdown: () => cleanup()
18136
+ });
16941
18137
  const httpServer = createServer(async (req, res) => {
16942
18138
  const url = req.url ?? "/";
16943
18139
  const method = req.method ?? "GET";
18140
+ idleExit.touch();
16944
18141
  if (gateway) {
16945
18142
  const verdict = gatewayVerdict({
16946
18143
  loopback: isLoopbackAddress(req.socket.remoteAddress),
@@ -17295,7 +18492,12 @@ async function runWebServer(opts) {
17295
18492
  }
17296
18493
  });
17297
18494
  const wss = new WebSocketServer({ server: httpServer });
18495
+ wss.on("error", () => {});
17298
18496
  wss.on("connection", (ws, req) => {
18497
+ idleExit.noteConnection();
18498
+ ws.once("close", () => {
18499
+ idleExit.noteDisconnect();
18500
+ });
17299
18501
  if (gateway && !isLoopbackAddress(req.socket.remoteAddress) && !gateway.verifySession(req.headers.cookie)) {
17300
18502
  ws.close(1008, "unauthorized");
17301
18503
  return;
@@ -17346,13 +18548,18 @@ async function runWebServer(opts) {
17346
18548
  }
17347
18549
  ws.send(JSON.stringify({ type: "server_empty" }));
17348
18550
  });
17349
- await new Promise((resolve, reject) => {
17350
- httpServer.once("error", (err) => {
17351
- if (err.code === "EADDRINUSE") reject(/* @__PURE__ */ new Error(`端口 ${opts.port} 已被占用,请先关闭占用该端口的进程,或使用 --port <port> 指定其他端口。`));
17352
- else reject(err);
18551
+ try {
18552
+ await new Promise((resolve, reject) => {
18553
+ httpServer.once("error", (err) => {
18554
+ if (err.code === "EADDRINUSE") reject(/* @__PURE__ */ new Error(`端口 ${opts.port} 已被占用,请先关闭占用该端口的进程,或使用 --port <port> 指定其他端口。`));
18555
+ else reject(err);
18556
+ });
18557
+ httpServer.listen(opts.port, host, resolve);
17353
18558
  });
17354
- httpServer.listen(opts.port, host, resolve);
17355
- });
18559
+ } catch (error) {
18560
+ idleExit.stop();
18561
+ throw error;
18562
+ }
17356
18563
  const address = httpServer.address();
17357
18564
  const realPort = typeof address === "object" && address !== null ? address.port : opts.port;
17358
18565
  const url = `http://localhost:${realPort}`;
@@ -17361,6 +18568,7 @@ async function runWebServer(opts) {
17361
18568
  const cleanup = async () => {
17362
18569
  if (cleaned) return;
17363
18570
  cleaned = true;
18571
+ idleExit.stop();
17364
18572
  try {
17365
18573
  wss.close();
17366
18574
  await manager.closeAll();
@@ -17376,7 +18584,8 @@ async function runWebServer(opts) {
17376
18584
  process.on("SIGTERM", () => {
17377
18585
  cleanup().finally(() => process.exit(0));
17378
18586
  });
17379
- let banner = `\n Scream Web UI ready: ${url}\n Working directory: ${workDir}\n Sessions: ${manager.list().length} (${manager.list().filter((s) => s.active).length} active)\n Permission: ${opts.yolo ? "yolo" : opts.auto ? "auto" : "manual"}\n`;
18587
+ const idleMinutes = opts.idleMinutes ?? 15;
18588
+ let banner = `\n Scream Web UI ready: ${url}\n Working directory: ${workDir}\n Sessions: ${manager.list().length} (${manager.list().filter((s) => s.active).length} active)\n Permission: ${opts.yolo ? "yolo" : opts.auto ? "auto" : "manual"}\n` + (idleMinutes > 0 ? ` Idle exit: ${idleMinutes}m without a browser connection (--idle-minutes <n> to change, 0 = disable)\n` : ` Idle exit: disabled (--idle-minutes 0)\n`);
17380
18589
  if (gateway) {
17381
18590
  const addrs = getLanAddresses();
17382
18591
  banner += ` LAN mode: on — non-local devices must present the access key\n`;
@@ -17448,7 +18657,8 @@ async function runWeb(opts) {
17448
18657
  open: opts.open,
17449
18658
  skillsDirs: opts.skillsDirs,
17450
18659
  lan: opts.lan,
17451
- token
18660
+ token,
18661
+ idleMinutes: opts.idleMinutes
17452
18662
  });
17453
18663
  }
17454
18664
  //#endregion