scream-code 0.17.8 → 0.17.9

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,9 +4,9 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
- import { k as MemoryMemoStore } from "./src-BhZujafw.mjs";
8
- import { $n as encodeWorkDirKey, $t as lerpGradient, A as highlightLines, An as embeddingFailureDetail, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as detectShellEnvironment, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getLlmNotSetMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as sortSlashCommands, Dt as createEditorTheme, E as UserMessageComponent, En as BUILTIN_SLASH_COMMANDS, Et as resolveThemeSync, F as StatusMessageComponent, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_COMMAND_NAME, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getInputHistoryFile, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiConfigParseError, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as PRODUCT_NAME, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_UI_MODE, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as TuiLikePreferencesSchema, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as hasEmbeddingModelCache, Mt as createThemeStyles, N as ThinkingComponent, Nn as startManualEmbeddingDownload, Nt as contrastTextHex, O as renderDiffLines, On as isExperimentalFlagEnabled, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as ScreamHarness, Qt as FooterComponent, R as SkillActivationComponent, Rn as loadTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getCtrlDHint, St as supportsBalance, Tn as buildSkillSlashCommands, U as getSharedSpeedTracker, Un as getLogDir, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getDataDir, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as fetchCatalog, Xt as isBusy, Y as resetBreathingClock, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as saveCatalogCache, Zt as isStreaming, _n as EXIT_CONFIRM_WINDOW_MS, a as syncDiagramCapabilityPrompt, an as printableChar, ar as isRealUserPrompt, at as toTerminalHyperlink, b as readUpdateCache, bn as TIP_ROTATION_INTERVAL_MS, c as disposeChildren, cn as appendStreamingArgsPreview, cr as log, d as isPlanExpandable, dn as isTodoItemShape, dr as isOrphanedToolCallError, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as ErrorCodes, ft as handleFusionPlanCommand, gn as EMPTY_SESSION_HINT_URL, hn as truncateErrorMessage, i as buildRoleAdditionalText, it as isInternalMessage, j as langFromPath, jn as getKnowledgeStore, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as setExperimentalFlags, kt as isEmptySessionHintDismissed, l as hasDispose, ln as argsRecord, lr as resolveGlobalLogPath, mn as stringValue, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as resolveScreamHome, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, pr as SCREAM_ERROR_INFO, q as BREATHE_CYCLE_MS, qn as CLI_USER_AGENT_PRODUCT, qt as TERMINAL_FOCUS_OUT, r as openUrl, rr as removeSessionIndexEntry, sn as appendSessionHeaderHint, sr as flushDiagnosticLogs, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as isScreamError, v as selectUpdateTarget, vn as MAIN_AGENT_ID, w as getDaemonInstructions, wn as getNoActiveSessionMessage, x as appendJsonlLine, xn as getCtrlCHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as SESSION_TIPS, z as AssistantMessageComponent, zn as saveTuiConfig, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-B8IVXCrX.mjs";
9
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CD0sakGq.mjs";
7
+ import { A as sharedMemoStore } from "./src-C7C9e5Ds.mjs";
8
+ import { $n as encodeWorkDirKey, $t as lerpGradient, A as highlightLines, An as embeddingFailureDetail, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as detectShellEnvironment, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getLlmNotSetMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as sortSlashCommands, Dt as createEditorTheme, E as UserMessageComponent, En as BUILTIN_SLASH_COMMANDS, Et as resolveThemeSync, F as StatusMessageComponent, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_COMMAND_NAME, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getInputHistoryFile, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiConfigParseError, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as PRODUCT_NAME, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_UI_MODE, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as TuiLikePreferencesSchema, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as hasEmbeddingModelCache, Mt as createThemeStyles, N as ThinkingComponent, Nn as startManualEmbeddingDownload, Nt as contrastTextHex, O as renderDiffLines, On as isExperimentalFlagEnabled, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as ScreamHarness, Qt as FooterComponent, R as SkillActivationComponent, Rn as loadTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getCtrlDHint, St as supportsBalance, Tn as buildSkillSlashCommands, U as getSharedSpeedTracker, Un as getLogDir, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getDataDir, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as fetchCatalog, Xt as isBusy, Y as resetBreathingClock, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as saveCatalogCache, Zt as isStreaming, _n as EXIT_CONFIRM_WINDOW_MS, a as syncDiagramCapabilityPrompt, an as printableChar, ar as isRealUserPrompt, at as toTerminalHyperlink, b as readUpdateCache, bn as TIP_ROTATION_INTERVAL_MS, c as disposeChildren, cn as appendStreamingArgsPreview, cr as flushDiagnosticLogs, d as isPlanExpandable, dn as isTodoItemShape, dr as isScreamError, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as isOrphanedToolCallError, ft as handleFusionPlanCommand, gn as EMPTY_SESSION_HINT_URL, hn as truncateErrorMessage, i as buildRoleAdditionalText, it as isInternalMessage, j as langFromPath, jn as getKnowledgeStore, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as setExperimentalFlags, kt as isEmptySessionHintDismissed, l as hasDispose, ln as argsRecord, lr as log, mn as stringValue, mr as SCREAM_ERROR_INFO, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as parseRlmMaxDepthArg, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, pr as ErrorCodes, q as BREATHE_CYCLE_MS, qn as CLI_USER_AGENT_PRODUCT, qt as TERMINAL_FOCUS_OUT, r as openUrl, rr as removeSessionIndexEntry, sn as appendSessionHeaderHint, sr as resolveScreamHome, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as resolveGlobalLogPath, v as selectUpdateTarget, vn as MAIN_AGENT_ID, w as getDaemonInstructions, wn as getNoActiveSessionMessage, x as appendJsonlLine, xn as getCtrlCHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as SESSION_TIPS, z as AssistantMessageComponent, zn as saveTuiConfig, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-BGYq0dnV.mjs";
9
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-BlSqBYxO.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
12
12
  import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -33,7 +33,7 @@ function optionalBuildString(value) {
33
33
  return typeof value === "string" && value.length > 0 ? value : void 0;
34
34
  }
35
35
  const SCREAM_BUILD_INFO = {
36
- version: optionalBuildString("0.17.8"),
36
+ version: optionalBuildString("0.17.9"),
37
37
  channel: optionalBuildString(""),
38
38
  commit: optionalBuildString(""),
39
39
  buildTarget: optionalBuildString("darwin-arm64")
@@ -2171,6 +2171,211 @@ function renderChildRequestNotice(fields, name) {
2171
2171
  };
2172
2172
  }
2173
2173
  //#endregion
2174
+ //#region src/tui/utils/subagent-instances.ts
2175
+ /**
2176
+ * agent-core synthesizes `rlm_<timestamp>_<rand>` parent tool-call ids for
2177
+ * rlm() spawns (packages/agent-core/src/agent/tool/index.ts:374), so no
2178
+ * tool-call row ever exists for them — the id shape is the only RLM marker
2179
+ * available to the TUI. A renamed prefix degrades to the generic "unknown"
2180
+ * attribution, never to a wrong one.
2181
+ */
2182
+ const RLM_TOOL_CALL_PREFIX = "rlm_";
2183
+ /** Guard against a corrupt or cyclic parent chain hanging the render path. */
2184
+ const MAX_CHAIN_DEPTH = 16;
2185
+ function descriptionFromToolCall(call) {
2186
+ const fromArgs = call?.args["description"];
2187
+ if (typeof call?.description === "string" && call.description.length > 0) return call.description;
2188
+ return typeof fromArgs === "string" && fromArgs.length > 0 ? fromArgs : void 0;
2189
+ }
2190
+ /** Snapshot a spawn event (+ the spawning tool call, still in flight at that
2191
+ * moment) into a registry record. `spawnedAt` is passed in so a resume keeps
2192
+ * the instance's original start time. */
2193
+ function createSubagentInstanceInfo(event, parentToolCall, spawnedAt = Date.now()) {
2194
+ return {
2195
+ agentId: event.subagentId,
2196
+ type: event.subagentName,
2197
+ description: event.description,
2198
+ parentAgentId: event.parentAgentId,
2199
+ parentToolCallId: event.parentToolCallId,
2200
+ parentToolName: parentToolCall?.name,
2201
+ parentToolDescription: descriptionFromToolCall(parentToolCall),
2202
+ spawnedAt
2203
+ };
2204
+ }
2205
+ /** Close a registry record on subagent.completed / subagent.failed. */
2206
+ function withSubagentInstanceEnded(info, outcome, endedAt = Date.now()) {
2207
+ return {
2208
+ ...info,
2209
+ endedAt,
2210
+ outcome
2211
+ };
2212
+ }
2213
+ /** Walk `parentAgentId` upwards, collecting parent types (nearest first). */
2214
+ function buildAncestorChain(instance, instances) {
2215
+ const names = [];
2216
+ const seen = new Set([instance.agentId]);
2217
+ let parentId = instance.parentAgentId;
2218
+ while (parentId !== void 0 && parentId !== "main" && parentId.length > 0) {
2219
+ if (seen.has(parentId) || names.length >= MAX_CHAIN_DEPTH) return {
2220
+ names,
2221
+ truncated: true
2222
+ };
2223
+ seen.add(parentId);
2224
+ const parent = instances.get(parentId);
2225
+ if (parent === void 0) return {
2226
+ names,
2227
+ truncated: true
2228
+ };
2229
+ names.push(parent.type);
2230
+ parentId = parent.parentAgentId;
2231
+ }
2232
+ return {
2233
+ names,
2234
+ truncated: false
2235
+ };
2236
+ }
2237
+ /** Best-effort "who triggered this instance" resolution. */
2238
+ function resolveSource(instance, instances) {
2239
+ const parentId = instance.parentAgentId;
2240
+ const parentInstance = parentId !== void 0 && parentId !== "main" ? instances.get(parentId) : void 0;
2241
+ const isRlm = instance.parentToolCallId.startsWith(RLM_TOOL_CALL_PREFIX);
2242
+ if (parentInstance !== void 0) return {
2243
+ kind: isRlm ? "rlm" : "agent",
2244
+ name: parentInstance.type,
2245
+ description: parentInstance.description
2246
+ };
2247
+ if (isRlm) return {
2248
+ kind: "rlm",
2249
+ name: void 0,
2250
+ description: instance.description
2251
+ };
2252
+ if (instance.parentToolName !== void 0) return {
2253
+ kind: "tool",
2254
+ name: instance.parentToolName,
2255
+ description: instance.parentToolDescription
2256
+ };
2257
+ if (parentId === void 0 || parentId === "main") return {
2258
+ kind: "main",
2259
+ name: void 0,
2260
+ description: void 0
2261
+ };
2262
+ return {
2263
+ kind: "unknown",
2264
+ name: void 0,
2265
+ description: void 0
2266
+ };
2267
+ }
2268
+ /**
2269
+ * Map the slot snapshot + instance registry into Agents-view rows.
2270
+ *
2271
+ * One row per slot that is live or has any instance history in this session
2272
+ * (never-used slots stay out), plus one row per *registry-only* type: past
2273
+ * `MAX_SUBAGENT_SLOTS` the slot machine returns a detached slot that never
2274
+ * reaches `slots`, so the 17th+ type would otherwise spawn and finish
2275
+ * invisibly while the registry still records every instance. A synthesized
2276
+ * row is never live, takes its status from the type's latest instance
2277
+ * (`completed` / `failed`, else `idle` when nothing terminal was recorded),
2278
+ * counts every recorded instance of the type and has no `instanceId`; its
2279
+ * source and ancestors use the same derivation as slot-backed rows. Live rows
2280
+ * sort first (most recent activity on top), then ended ones by their end time.
2281
+ *
2282
+ * `recent` is the archive ring (newest last): rows derived when an instance
2283
+ * closed. A type the live snapshot no longer covers keeps its archived row, so
2284
+ * splitting the registry into live + ring does not change what the view shows
2285
+ * for anything inside the ring window. Called without `recent` (as before the
2286
+ * ring existed) the output depends on `slots` + `instances` alone.
2287
+ */
2288
+ function buildAgentRows(slots, instances, recent = []) {
2289
+ const byType = /* @__PURE__ */ new Map();
2290
+ for (const info of instances.values()) {
2291
+ const aggregate = byType.get(info.type);
2292
+ if (aggregate === void 0) {
2293
+ byType.set(info.type, {
2294
+ latest: info,
2295
+ count: 1
2296
+ });
2297
+ continue;
2298
+ }
2299
+ aggregate.count += 1;
2300
+ if (info.spawnedAt >= aggregate.latest.spawnedAt) aggregate.latest = info;
2301
+ }
2302
+ const rows = [];
2303
+ const slottedTypes = /* @__PURE__ */ new Set();
2304
+ for (const slot of slots) {
2305
+ slottedTypes.add(slot.type);
2306
+ const latest = byType.get(slot.type)?.latest;
2307
+ const live = slot.count > 0 || slot.status !== "idle";
2308
+ if (!live && latest === void 0) continue;
2309
+ const current = (slot.agentId === void 0 ? void 0 : instances.get(slot.agentId)) ?? latest;
2310
+ const status = live ? slot.status : latest?.outcome ?? "idle";
2311
+ const chain = current === void 0 ? {
2312
+ names: [],
2313
+ truncated: false
2314
+ } : buildAncestorChain(current, instances);
2315
+ const row = {
2316
+ key: slot.type,
2317
+ type: slot.type,
2318
+ status,
2319
+ live,
2320
+ count: slot.count,
2321
+ detail: slot.detail,
2322
+ lastActivityAt: slot.lastActivityAt,
2323
+ description: current?.description,
2324
+ instanceId: live ? slot.agentId : void 0,
2325
+ source: current === void 0 ? {
2326
+ kind: "unknown",
2327
+ name: void 0,
2328
+ description: void 0
2329
+ } : resolveSource(current, instances),
2330
+ ancestors: chain.names,
2331
+ chainTruncated: chain.truncated
2332
+ };
2333
+ const sortAt = live ? slot.lastActivityAt : latest?.endedAt ?? latest?.spawnedAt ?? slot.lastActivityAt;
2334
+ rows.push({
2335
+ row,
2336
+ sortAt
2337
+ });
2338
+ }
2339
+ for (const [type, aggregate] of byType) {
2340
+ if (slottedTypes.has(type)) continue;
2341
+ const { latest } = aggregate;
2342
+ const activityAt = latest.endedAt ?? latest.spawnedAt;
2343
+ const chain = buildAncestorChain(latest, instances);
2344
+ rows.push({
2345
+ row: {
2346
+ key: type,
2347
+ type,
2348
+ status: latest.outcome ?? "idle",
2349
+ live: false,
2350
+ count: aggregate.count,
2351
+ detail: void 0,
2352
+ lastActivityAt: activityAt,
2353
+ description: latest.description,
2354
+ instanceId: void 0,
2355
+ source: resolveSource(latest, instances),
2356
+ ancestors: chain.names,
2357
+ chainTruncated: chain.truncated
2358
+ },
2359
+ sortAt: activityAt
2360
+ });
2361
+ }
2362
+ const coveredTypes = new Set(rows.map((entry) => entry.row.key));
2363
+ const archivedByType = /* @__PURE__ */ new Map();
2364
+ for (const row of recent) archivedByType.set(row.key, row);
2365
+ for (const row of archivedByType.values()) {
2366
+ if (coveredTypes.has(row.key)) continue;
2367
+ rows.push({
2368
+ row,
2369
+ sortAt: row.lastActivityAt
2370
+ });
2371
+ }
2372
+ rows.sort((a, b) => {
2373
+ if (a.row.live !== b.row.live) return a.row.live ? -1 : 1;
2374
+ return b.sortAt - a.sortAt;
2375
+ });
2376
+ return rows.map((entry) => entry.row);
2377
+ }
2378
+ //#endregion
2174
2379
  //#region src/tui/utils/background-task-status.ts
2175
2380
  const MAX_DETAIL_LENGTH = 240;
2176
2381
  function truncate(value) {
@@ -2505,6 +2710,17 @@ function subtractTokenUsage(current, previous) {
2505
2710
  function isZeroUsage(u) {
2506
2711
  return u.inputOther === 0 && u.inputCacheRead === 0 && u.inputCacheCreation === 0 && u.output === 0;
2507
2712
  }
2713
+ /**
2714
+ * Whether a status event may patch main-agent state (RLM mode / recursion cap).
2715
+ * Only the main agent's own events qualify; subagent status updates carry their
2716
+ * own agentId and must never overwrite the main display. Events without an
2717
+ * agentId (older emitters) are accepted for backward compatibility.
2718
+ *
2719
+ * Exported (pure) so the filtering rule is unit-testable without a TUI.
2720
+ */
2721
+ function isMainAgentStatusEvent(event) {
2722
+ return event.agentId === void 0 || event.agentId === "main";
2723
+ }
2508
2724
  var SessionEventHandler = class {
2509
2725
  host;
2510
2726
  constructor(host) {
@@ -2531,6 +2747,22 @@ var SessionEventHandler = class {
2531
2747
  subagentInfo = /* @__PURE__ */ new Map();
2532
2748
  /** Sidebar subagent slot state machine (see utils/subagent-slots.ts). */
2533
2749
  subagentSlots = new SubagentSlots();
2750
+ /**
2751
+ * Per-instance provenance registry (see utils/subagent-instances.ts), read
2752
+ * by the /tasks browser's Agents view. Captured at spawn because the two
2753
+ * clues stop being available later: `subagentInfo.parentToolCallId` is
2754
+ * rewritten to a WolfPack routing id for WolfPack children, and the
2755
+ * spawning tool call leaves the active set as soon as it returns.
2756
+ */
2757
+ subagentInstances = /* @__PURE__ */ new Map();
2758
+ /**
2759
+ * Archive ring behind {@link getRecentSubagentInstances}: the last derived
2760
+ * Agents-view row of every closed instance, oldest first, capped at
2761
+ * {@link MAX_RECENT_SUBAGENT_INSTANCES}. Closing an instance releases its
2762
+ * registry record, so this is what keeps recently-ended agents visible in the
2763
+ * /tasks Agents view instead of letting the registry grow for the session.
2764
+ */
2765
+ recentSubagentInstances = [];
2534
2766
  /** Tool callId of the in-flight UpdateGoal(complete) (judging), if any. */
2535
2767
  goalJudgeCallId;
2536
2768
  /** Rolling first-token latencies of real requests, feeding the sidebar Hub
@@ -2551,6 +2783,8 @@ var SessionEventHandler = class {
2551
2783
  this.backgroundTaskTranscriptedTerminal.clear();
2552
2784
  this.subagentInfo.clear();
2553
2785
  this.subagentSlots.reset();
2786
+ this.subagentInstances.clear();
2787
+ this.recentSubagentInstances.length = 0;
2554
2788
  this.renderedSkillActivationIds.clear();
2555
2789
  this.renderedMcpServerStatusKeys.clear();
2556
2790
  this.stopAllMcpServerStatusSpinners();
@@ -2733,6 +2967,50 @@ var SessionEventHandler = class {
2733
2967
  getSubagentSlots() {
2734
2968
  return this.subagentSlots.getSlots();
2735
2969
  }
2970
+ /** Read-only snapshot of the per-instance provenance registry (Agents view
2971
+ * of the /tasks browser). Keyed by subagent agentId. */
2972
+ getSubagentInstances() {
2973
+ return this.subagentInstances;
2974
+ }
2975
+ /** Read-only snapshot of the archive ring: the last derived Agents-view row
2976
+ * of each closed instance, oldest first, bounded to
2977
+ * {@link MAX_RECENT_SUBAGENT_INSTANCES}. */
2978
+ getRecentSubagentInstances() {
2979
+ return this.recentSubagentInstances;
2980
+ }
2981
+ /** Remember (or refresh, on resume) one spawned instance. The spawning tool
2982
+ * call is resolved here, while it is still in flight. */
2983
+ rememberSubagentInstance(event, fallbackParentAgentId) {
2984
+ const previous = this.subagentInstances.get(event.subagentId);
2985
+ const parentToolCall = this.host.streamingUI.getActiveToolCall(event.parentToolCallId);
2986
+ this.subagentInstances.set(event.subagentId, createSubagentInstanceInfo({
2987
+ ...event,
2988
+ parentAgentId: event.parentAgentId ?? fallbackParentAgentId
2989
+ }, parentToolCall, previous?.spawnedAt));
2990
+ }
2991
+ /**
2992
+ * Close an instance record on subagent.completed / subagent.failed: archive
2993
+ * its last display row in the ring, then release the registry record — the
2994
+ * registry is reset per session, not kept per spawned agent.
2995
+ *
2996
+ * The archived row is derived by the very builder the view uses (over the
2997
+ * registry snapshot that still contains this instance and its terminal
2998
+ * outcome), so it is field-identical to what the view rendered while the
2999
+ * record was live; the deletion below is what makes that derivation the last
3000
+ * chance to produce it.
3001
+ */
3002
+ closeSubagentInstance(agentId, outcome) {
3003
+ const info = this.subagentInstances.get(agentId);
3004
+ if (info === void 0) return;
3005
+ const ended = withSubagentInstanceEnded(info, outcome);
3006
+ const snapshot = new Map([...this.subagentInstances, [agentId, ended]]);
3007
+ const row = buildAgentRows(this.subagentSlots.getSlots(), snapshot).find((candidate) => candidate.key === ended.type);
3008
+ if (row !== void 0) {
3009
+ this.recentSubagentInstances.push(row);
3010
+ if (this.recentSubagentInstances.length > 50) this.recentSubagentInstances.shift();
3011
+ }
3012
+ this.subagentInstances.delete(agentId);
3013
+ }
2736
3014
  /** Keep the last few first-token latencies, per provider. Steps without a
2737
3015
  * measured latency (no stream, cached turn) are ignored rather than counted
2738
3016
  * as a zero. */
@@ -2773,6 +3051,7 @@ var SessionEventHandler = class {
2773
3051
  switch (event.type) {
2774
3052
  case "subagent.spawned":
2775
3053
  this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
3054
+ this.rememberSubagentInstance(event, agentId);
2776
3055
  break;
2777
3056
  case "subagent.started":
2778
3057
  this.subagentSlots.onStarted(event.subagentId);
@@ -2780,6 +3059,7 @@ var SessionEventHandler = class {
2780
3059
  case "subagent.completed":
2781
3060
  case "subagent.failed":
2782
3061
  this.subagentSlots.onTerminated(event.subagentId);
3062
+ this.closeSubagentInstance(event.subagentId, event.type === "subagent.failed" ? "failed" : "completed");
2783
3063
  break;
2784
3064
  case "tool.call.started":
2785
3065
  if (event.name === "ContactParent") {
@@ -3170,7 +3450,11 @@ var SessionEventHandler = class {
3170
3450
  if (event.maxContextTokens !== void 0) patch.maxContextTokens = event.maxContextTokens;
3171
3451
  if (event.planMode !== void 0) patch.planMode = event.planMode ? event.planStrategy === "fusion" ? "fusionplan" : "plan" : "off";
3172
3452
  if (event.wolfpackMode !== void 0) patch.wolfpackMode = event.wolfpackMode;
3173
- if (event.rlmEnabled !== void 0) patch.rlmEnabled = event.rlmEnabled;
3453
+ if (event.rlmEnabled !== void 0 || event.rlmMaxDepth !== void 0) {
3454
+ const fromMain = isMainAgentStatusEvent(event);
3455
+ if (fromMain && event.rlmEnabled !== void 0) patch.rlmEnabled = event.rlmEnabled;
3456
+ if (fromMain && event.rlmMaxDepth !== void 0) patch.rlmMaxDepth = event.rlmMaxDepth;
3457
+ }
3174
3458
  if (event.permission !== void 0) patch.permissionMode = event.permission;
3175
3459
  if (Object.keys(patch).length > 0) this.host.setAppState(patch);
3176
3460
  }
@@ -3422,6 +3706,7 @@ var SessionEventHandler = class {
3422
3706
  }
3423
3707
  handleSubagentSpawned(event) {
3424
3708
  this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
3709
+ this.rememberSubagentInstance(event);
3425
3710
  const { streamingUI } = this.host;
3426
3711
  if (streamingUI.getActiveToolCall(event.parentToolCallId)?.name === "WolfPack") {
3427
3712
  const routingId = event.subagentId;
@@ -3495,6 +3780,7 @@ var SessionEventHandler = class {
3495
3780
  }
3496
3781
  handleSubagentCompleted(event) {
3497
3782
  this.subagentSlots.onTerminated(event.subagentId);
3783
+ this.closeSubagentInstance(event.subagentId, "completed");
3498
3784
  this.recordSubagentUsage(event.subagentId, void 0, event.usage);
3499
3785
  const { streamingUI } = this.host;
3500
3786
  const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
@@ -3525,6 +3811,7 @@ var SessionEventHandler = class {
3525
3811
  }
3526
3812
  handleSubagentFailed(event) {
3527
3813
  this.subagentSlots.onTerminated(event.subagentId);
3814
+ this.closeSubagentInstance(event.subagentId, "failed");
3528
3815
  this.recordSubagentUsage(event.subagentId, void 0, event.usage);
3529
3816
  const { streamingUI } = this.host;
3530
3817
  const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
@@ -4380,6 +4667,89 @@ function isControlCharacter(ch) {
4380
4667
  const code = ch.codePointAt(0) ?? 0;
4381
4668
  return code >= 0 && code <= 31 || code >= 127 && code <= 159;
4382
4669
  }
4670
+ /** Max characters kept across head + tail of the preview combined. */
4671
+ const UI_OUTPUT_MAX_CHARS = 8e3;
4672
+ /** Same marker string as agent-core's `result-builder.ts` TRUNCATION_MARKER. */
4673
+ const TRUNCATION_MARKER = "[...truncated]";
4674
+ function elisionMarker(elided) {
4675
+ return `[…${String(elided)} lines elided…]`;
4676
+ }
4677
+ /**
4678
+ * Bounds one tool-output string to a head + tail preview:
4679
+ * - at most {@link UI_OUTPUT_MAX_HEAD_LINES} lines from the start and
4680
+ * {@link UI_OUTPUT_MAX_TAIL_LINES} lines from the end (the middle is
4681
+ * replaced with `[…N lines elided…]`);
4682
+ * - the kept head + tail text stays within {@link UI_OUTPUT_MAX_CHARS} chars,
4683
+ * dropping whole lines first and hard-slicing a lone oversized line last
4684
+ * (marked `[...truncated]`).
4685
+ *
4686
+ * Returns the input unchanged when it already fits both budgets.
4687
+ */
4688
+ function boundToolOutput(output) {
4689
+ const lines = output.split("\n");
4690
+ const totalLines = lines.length;
4691
+ const overflowLines = totalLines > 80;
4692
+ const overflowChars = output.length > UI_OUTPUT_MAX_CHARS;
4693
+ if (!overflowLines && !overflowChars) return output;
4694
+ const head = lines.slice(0, 40);
4695
+ const tail = overflowLines ? lines.slice(totalLines - 40) : lines.slice(40);
4696
+ let elided = totalLines - head.length - tail.length;
4697
+ const contentBudget = UI_OUTPUT_MAX_CHARS - 2;
4698
+ let headText = head.join("\n");
4699
+ let tailText = tail.join("\n");
4700
+ while (headText.length + tailText.length > contentBudget) {
4701
+ if (head.length > 1 && (tail.length <= 1 || headText.length >= tailText.length)) {
4702
+ head.pop();
4703
+ headText = head.join("\n");
4704
+ elided += 1;
4705
+ continue;
4706
+ }
4707
+ if (tail.length > 1) {
4708
+ tail.shift();
4709
+ tailText = tail.join("\n");
4710
+ elided += 1;
4711
+ continue;
4712
+ }
4713
+ break;
4714
+ }
4715
+ if (headText.length + tailText.length > contentBudget) {
4716
+ const tailKeep = Math.min(tailText.length, Math.floor(contentBudget / 2));
4717
+ const headKeep = Math.min(headText.length, contentBudget - tailKeep);
4718
+ headText = headText.slice(0, headKeep);
4719
+ tailText = tailText.slice(tailText.length - tailKeep);
4720
+ }
4721
+ const marker = elided > 0 ? elisionMarker(elided) : TRUNCATION_MARKER;
4722
+ const parts = [];
4723
+ if (headText.length > 0) parts.push(headText);
4724
+ parts.push(marker);
4725
+ if (tailText.length > 0) parts.push(tailText);
4726
+ return parts.join("\n");
4727
+ }
4728
+ /**
4729
+ * Returns a transcript entry safe to keep in `state.transcriptEntries`:
4730
+ * tool-group entries get their `result.output` replaced with the bounded
4731
+ * preview (a copy — message, brief, titles, args and every other field stay
4732
+ * as they are). Non-tool entries — and tool entries already within budget —
4733
+ * come back untouched (no copy).
4734
+ */
4735
+ function boundEntryForUi(entry) {
4736
+ const toolCallData = entry.toolCallData;
4737
+ if (toolCallData === void 0) return entry;
4738
+ const result = toolCallData.result;
4739
+ if (result === void 0) return entry;
4740
+ const boundedOutput = boundToolOutput(result.output);
4741
+ if (boundedOutput === result.output) return entry;
4742
+ return {
4743
+ ...entry,
4744
+ toolCallData: {
4745
+ ...toolCallData,
4746
+ result: {
4747
+ ...result,
4748
+ output: boundedOutput
4749
+ }
4750
+ }
4751
+ };
4752
+ }
4383
4753
  //#endregion
4384
4754
  //#region src/tui/controllers/streaming-ui.ts
4385
4755
  var StreamingUIController = class StreamingUIController {
@@ -5139,7 +5509,16 @@ var StreamingUIController = class StreamingUIController {
5139
5509
  this.host.transcriptController.unmarkPending(tc);
5140
5510
  this._pendingToolComponents.delete(toolCallId);
5141
5511
  const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
5142
- if (entry?.toolCallData !== void 0) entry.toolCallData.result = result;
5512
+ if (entry?.toolCallData !== void 0) {
5513
+ const bounded = boundEntryForUi({
5514
+ ...entry,
5515
+ toolCallData: {
5516
+ ...entry.toolCallData,
5517
+ result
5518
+ }
5519
+ });
5520
+ entry.toolCallData.result = bounded.toolCallData?.result ?? result;
5521
+ }
5143
5522
  this.host.transcriptController.commit();
5144
5523
  state.ui.requestRender();
5145
5524
  return;
@@ -5538,6 +5917,64 @@ function statusColor(colors, status) {
5538
5917
  function isTerminal(status) {
5539
5918
  return status === "completed" || status === "failed" || status === "killed" || status === "lost";
5540
5919
  }
5920
+ /** Width of the agent-name column in the Agents list (leaves room for the
5921
+ * status word, the instance count and an activity hint on narrow panes). */
5922
+ const AGENT_NAME_COLS = 12;
5923
+ /** Localized word per agent row status: the six live slot states plus the two
5924
+ * terminal outcomes the instance registry can report. */
5925
+ function agentStatusText(status) {
5926
+ switch (status) {
5927
+ case "idle": return t("sidebar.agent_idle");
5928
+ case "working": return t("sidebar.agent_working");
5929
+ case "outputting": return t("sidebar.agent_outputting");
5930
+ case "messaging": return t("sidebar.agent_messaging");
5931
+ case "reworking": return t("sidebar.agent_reworking");
5932
+ case "requesting": return t("sidebar.agent_requesting");
5933
+ case "completed": return t("taskbrowser.agents_completed");
5934
+ case "failed": return t("taskbrowser.agents_failed");
5935
+ }
5936
+ }
5937
+ function agentStatusColor(colors, status) {
5938
+ switch (status) {
5939
+ case "working":
5940
+ case "outputting": return colors.success;
5941
+ case "messaging": return colors.accent;
5942
+ case "reworking":
5943
+ case "requesting": return colors.warning;
5944
+ case "completed": return colors.textMuted;
5945
+ case "failed": return colors.error;
5946
+ case "idle": return colors.textDim;
5947
+ }
5948
+ }
5949
+ /** "Who triggered this agent" line. Each kind names its own evidence, and a
5950
+ * missing clue is voiced as unknown instead of guessed. */
5951
+ function formatAgentSource(source) {
5952
+ switch (source.kind) {
5953
+ case "tool":
5954
+ if (source.name === void 0) return t("taskbrowser.agent_source_unknown");
5955
+ return source.description === void 0 ? source.name : `${source.name} · ${source.description}`;
5956
+ case "agent": {
5957
+ const via = source.name === void 0 ? t("taskbrowser.agent_source_unknown") : t("taskbrowser.agent_source_via", { name: source.name });
5958
+ return source.description === void 0 ? via : `${via} · ${source.description}`;
5959
+ }
5960
+ case "rlm": {
5961
+ const rlm = t("taskbrowser.agent_source_rlm");
5962
+ return source.name === void 0 ? rlm : `${rlm} · ${t("taskbrowser.agent_source_via", { name: source.name })}`;
5963
+ }
5964
+ case "main": return t("taskbrowser.agent_source_main");
5965
+ case "unknown": return t("taskbrowser.agent_source_unknown");
5966
+ }
5967
+ }
5968
+ /** Full spawn chain, root first: `main → researcher → coder`, `…` when the
5969
+ * walk stopped early (missing link / cycle / depth cap). */
5970
+ function formatAgentChain(agent) {
5971
+ const parents = [...agent.ancestors].toReversed();
5972
+ return `${[
5973
+ t("taskbrowser.agent_main"),
5974
+ ...parents,
5975
+ agent.type
5976
+ ].join(" → ")}${agent.chainTruncated ? " → …" : ""}`;
5977
+ }
5541
5978
  function formatRelativeTime$2(ts) {
5542
5979
  if (ts === null || ts === void 0 || !Number.isFinite(ts) || ts <= 0) return "";
5543
5980
  const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1e3);
@@ -5565,8 +6002,22 @@ function fitExactly$1(line, width) {
5565
6002
  return padToWidth$1(s, width);
5566
6003
  }
5567
6004
  function visibleTasks(tasks, filter) {
5568
- if (filter === "all") return [...tasks];
5569
- return tasks.filter((t) => !isTerminal(t.status));
6005
+ if (filter === "active") return tasks.filter((t) => !isTerminal(t.status));
6006
+ return [...tasks];
6007
+ }
6008
+ /**
6009
+ * Keep `selectedIndex` inside the window `scrollOffset … scrollOffset+rows-1`,
6010
+ * clamped to the list bounds. Shared by the task list and the agent list.
6011
+ */
6012
+ function clampListScroll(selectedIndex, scrollOffset, total, visibleRows) {
6013
+ if (visibleRows <= 0) return 0;
6014
+ let next = scrollOffset;
6015
+ if (selectedIndex < next) next = selectedIndex;
6016
+ else if (selectedIndex >= next + visibleRows) next = selectedIndex - visibleRows + 1;
6017
+ const maxScroll = Math.max(0, total - visibleRows);
6018
+ if (next < 0) next = 0;
6019
+ if (next > maxScroll) next = maxScroll;
6020
+ return next;
5570
6021
  }
5571
6022
  function compareTasks(a, b) {
5572
6023
  const aTerminal = isTerminal(a.status);
@@ -5606,6 +6057,12 @@ var TasksBrowserApp = class extends Container {
5606
6057
  sortedVisible;
5607
6058
  selectedIndex = 0;
5608
6059
  listScroll = 0;
6060
+ /** Agents view selection — component-local: agent rows are read-only, so
6061
+ * nothing needs to travel back to the controller. `selectedAgentKey` is the
6062
+ * anchor: rows re-sort as statuses change, the index must not decide. */
6063
+ agentIndex = 0;
6064
+ agentScroll = 0;
6065
+ selectedAgentKey = void 0;
5609
6066
  pendingStopTaskId = void 0;
5610
6067
  pendingStopTimer = void 0;
5611
6068
  constructor(props, terminal) {
@@ -5619,6 +6076,7 @@ var TasksBrowserApp = class extends Container {
5619
6076
  this.props = next;
5620
6077
  this.sortedVisible = visibleTasks(next.tasks, next.filter).toSorted(compareTasks);
5621
6078
  this.syncSelectionFromProps();
6079
+ this.syncAgentSelectionFromProps();
5622
6080
  if (this.pendingStopTaskId !== void 0) {
5623
6081
  const task = next.tasks.find((t) => t.taskId === this.pendingStopTaskId);
5624
6082
  if (task === void 0 || isTerminal(task.status)) this.clearPendingStop();
@@ -5640,6 +6098,34 @@ var TasksBrowserApp = class extends Container {
5640
6098
  }
5641
6099
  if (this.selectedIndex >= this.sortedVisible.length) this.selectedIndex = this.sortedVisible.length - 1;
5642
6100
  }
6101
+ /** Keep the agents selection pinned to the same slot type across refreshes
6102
+ * (rows re-sort as statuses change); fall back to clamping. */
6103
+ syncAgentSelectionFromProps() {
6104
+ const agents = this.props.agents;
6105
+ if (agents.length === 0) {
6106
+ this.agentIndex = 0;
6107
+ this.agentScroll = 0;
6108
+ this.selectedAgentKey = void 0;
6109
+ return;
6110
+ }
6111
+ if (this.selectedAgentKey !== void 0) {
6112
+ const byKey = agents.findIndex((agent) => agent.key === this.selectedAgentKey);
6113
+ if (byKey !== -1) {
6114
+ this.agentIndex = byKey;
6115
+ return;
6116
+ }
6117
+ }
6118
+ if (this.agentIndex >= agents.length) this.agentIndex = agents.length - 1;
6119
+ this.selectedAgentKey = agents[this.agentIndex]?.key;
6120
+ }
6121
+ /** Move the agents cursor (rows are read-only: no controller round-trip). */
6122
+ selectAgent(index) {
6123
+ const agents = this.props.agents;
6124
+ if (agents.length === 0) return;
6125
+ this.agentIndex = Math.min(agents.length - 1, Math.max(0, index));
6126
+ this.selectedAgentKey = agents[this.agentIndex]?.key;
6127
+ this.invalidate();
6128
+ }
5643
6129
  clearPendingStop() {
5644
6130
  this.pendingStopTaskId = void 0;
5645
6131
  if (this.pendingStopTimer !== void 0) {
@@ -5647,12 +6133,21 @@ var TasksBrowserApp = class extends Container {
5647
6133
  this.pendingStopTimer = void 0;
5648
6134
  }
5649
6135
  }
6136
+ /**
6137
+ * Component-local teardown, called by the controller's close() before the
6138
+ * component is swapped out: the pending-stop confirm timer must not
6139
+ * outlive the browser.
6140
+ */
6141
+ close() {
6142
+ this.clearPendingStop();
6143
+ }
5650
6144
  emitSelect() {
5651
6145
  const task = this.sortedVisible[this.selectedIndex];
5652
6146
  if (task) this.props.onSelect(task.taskId);
5653
6147
  }
5654
6148
  handleInput(data) {
5655
6149
  const k = printableChar(data);
6150
+ const agentsMode = this.props.filter === "agents";
5656
6151
  if (this.pendingStopTaskId !== void 0) {
5657
6152
  if (k === "y" || k === "Y") {
5658
6153
  const taskId = this.pendingStopTaskId;
@@ -5670,6 +6165,10 @@ var TasksBrowserApp = class extends Container {
5670
6165
  return;
5671
6166
  }
5672
6167
  if (matchesKey(data, Key.up) || k === "k") {
6168
+ if (agentsMode) {
6169
+ this.selectAgent(this.agentIndex - 1);
6170
+ return;
6171
+ }
5673
6172
  if (this.sortedVisible.length === 0) return;
5674
6173
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
5675
6174
  this.emitSelect();
@@ -5677,6 +6176,10 @@ var TasksBrowserApp = class extends Container {
5677
6176
  return;
5678
6177
  }
5679
6178
  if (matchesKey(data, Key.down) || k === "j") {
6179
+ if (agentsMode) {
6180
+ this.selectAgent(this.agentIndex + 1);
6181
+ return;
6182
+ }
5680
6183
  if (this.sortedVisible.length === 0) return;
5681
6184
  this.selectedIndex = Math.min(this.sortedVisible.length - 1, this.selectedIndex + 1);
5682
6185
  this.emitSelect();
@@ -5691,6 +6194,7 @@ var TasksBrowserApp = class extends Container {
5691
6194
  this.props.onRefresh();
5692
6195
  return;
5693
6196
  }
6197
+ if (agentsMode) return;
5694
6198
  if (k === "s" || k === "S") {
5695
6199
  const task = this.sortedVisible[this.selectedIndex];
5696
6200
  if (task === void 0) return;
@@ -5734,15 +6238,26 @@ var TasksBrowserApp = class extends Container {
5734
6238
  renderHeader(width) {
5735
6239
  const colors = this.props.colors;
5736
6240
  const title = chalk.hex(colors.primary).bold(" TASK BROWSER ");
5737
- const filterText = chalk.hex(colors.textMuted)(` filter=${this.props.filter === "all" ? "ALL" : "ACTIVE"} `);
5738
- const counts = countByStatus(this.props.tasks);
5739
- const countSegments = [];
5740
- if (counts.running > 0) countSegments.push(chalk.hex(colors.success)(` ${String(counts.running)} ${t("taskbrowser.running")} `));
5741
- if (counts.awaiting > 0) countSegments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} ${t("taskbrowser.awaiting")} `));
5742
- if (counts.completed > 0) countSegments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} ${t("taskbrowser.completed")} `));
5743
- if (counts.terminalFailed > 0) countSegments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} ${t("taskbrowser.interrupted")} `));
5744
- const totals = chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} ${t("taskbrowser.total")} `);
5745
- return fitExactly$1(title + filterText + countSegments.join("") + totals, width);
6241
+ const filterLabel = this.props.filter === "all" ? "ALL" : this.props.filter === "active" ? "ACTIVE" : "AGENTS";
6242
+ const filterText = chalk.hex(colors.textMuted)(` filter=${filterLabel} `);
6243
+ const segments = [];
6244
+ if (this.props.filter === "agents") {
6245
+ const live = this.props.agents.filter((agent) => agent.live).length;
6246
+ const ended = this.props.agents.length - live;
6247
+ if (live > 0) segments.push(chalk.hex(colors.success)(` ${String(live)} ${t("taskbrowser.agents_live")} `));
6248
+ if (ended > 0) segments.push(chalk.hex(colors.textDim)(` ${String(ended)} ${t("taskbrowser.agents_ended")} `));
6249
+ if (this.props.agents.length === 0) segments.push(chalk.hex(colors.textMuted)(` 0 ${t("taskbrowser.agents")} `));
6250
+ } else {
6251
+ const counts = countByStatus(this.props.tasks);
6252
+ if (counts.running > 0) segments.push(chalk.hex(colors.success)(` ${String(counts.running)} ${t("taskbrowser.running")} `));
6253
+ if (counts.awaiting > 0) segments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} ${t("taskbrowser.awaiting")} `));
6254
+ if (counts.completed > 0) segments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} ${t("taskbrowser.completed")} `));
6255
+ if (counts.terminalFailed > 0) segments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} ${t("taskbrowser.interrupted")} `));
6256
+ segments.push(chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} ${t("taskbrowser.total")} `));
6257
+ const liveAgents = this.props.agents.filter((agent) => agent.live).length;
6258
+ if (liveAgents > 0) segments.push(chalk.hex(colors.success)(` ${String(liveAgents)} ${t("taskbrowser.agents")} `));
6259
+ }
6260
+ return fitExactly$1(title + filterText + segments.join(""), width);
5746
6261
  }
5747
6262
  renderFooter(width) {
5748
6263
  const colors = this.props.colors;
@@ -5752,14 +6267,19 @@ var TasksBrowserApp = class extends Container {
5752
6267
  const warn = (text) => chalk.hex(colors.warning).bold(text);
5753
6268
  return fitExactly$1(` ${warn(t("taskbrowser.stop"))} ${chalk.hex(colors.text)(this.pendingStopTaskId)}? ${key("Y")} ${dim(t("taskbrowser.confirm"))} ${key("N")} ${dim(t("taskbrowser.cancel"))} `, width);
5754
6269
  }
5755
- const left = [
6270
+ const left = (this.props.filter === "agents" ? [
6271
+ ` ${key("↑↓")} ${dim(t("taskbrowser.select"))}`,
6272
+ `${key("Tab")} ${dim(t("taskbrowser.filter"))}`,
6273
+ `${key("R")} ${dim(t("taskbrowser.refresh"))}`,
6274
+ `${key("Q/Esc")} ${dim(t("taskbrowser.exit"))} `
6275
+ ] : [
5756
6276
  ` ${key("↑↓")} ${dim(t("taskbrowser.select"))}`,
5757
6277
  `${key("Enter/O")} ${dim(t("taskbrowser.output"))}`,
5758
6278
  `${key("S")} ${dim(t("taskbrowser.stop_action"))}`,
5759
6279
  `${key("R")} ${dim(t("taskbrowser.refresh"))}`,
5760
6280
  `${key("Tab")} ${dim(t("taskbrowser.filter"))}`,
5761
6281
  `${key("Q/Esc")} ${dim(t("taskbrowser.exit"))} `
5762
- ].join(" ");
6282
+ ]).join(" ");
5763
6283
  const flash = this.props.flashMessage;
5764
6284
  if (flash !== void 0 && flash.length > 0) {
5765
6285
  const flashStyled = chalk.hex(colors.warning)(` ${flash} `);
@@ -5798,6 +6318,7 @@ var TasksBrowserApp = class extends Container {
5798
6318
  return lines;
5799
6319
  }
5800
6320
  renderListFrame(width, height) {
6321
+ if (this.props.filter === "agents") return this.renderAgentsFrame(width, height);
5801
6322
  const title = `Tasks [${this.props.filter}]`;
5802
6323
  const innerHeight = Math.max(0, height - 2);
5803
6324
  if (this.sortedVisible.length === 0) {
@@ -5818,6 +6339,43 @@ var TasksBrowserApp = class extends Container {
5818
6339
  while (lines.length < innerHeight) lines.push("");
5819
6340
  return this.renderFrame(title, lines, width, height);
5820
6341
  }
6342
+ /** Left pane, `agents` filter: one row per subagent type with history (live
6343
+ * ones first), including types whose slot the cap pushed out and that only
6344
+ * the instance registry knows; read-only — the detail pane carries the full
6345
+ * provenance. */
6346
+ renderAgentsFrame(width, height) {
6347
+ const colors = this.props.colors;
6348
+ const agents = this.props.agents;
6349
+ const innerHeight = Math.max(0, height - 2);
6350
+ const title = `${t("taskbrowser.agents_title")} [${String(agents.length)}]`;
6351
+ if (agents.length === 0) {
6352
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_empty"))];
6353
+ while (lines.length < innerHeight) lines.push("");
6354
+ return this.renderFrame(title, lines, width, height);
6355
+ }
6356
+ this.agentScroll = clampListScroll(this.agentIndex, this.agentScroll, agents.length, innerHeight);
6357
+ const start = this.agentScroll;
6358
+ const window = agents.slice(start, start + innerHeight);
6359
+ const innerWidth = width - 2;
6360
+ const lines = [];
6361
+ for (const [vi, agent] of window.entries()) {
6362
+ const index = start + vi;
6363
+ lines.push(this.renderAgentRow(agent, index === this.agentIndex, innerWidth));
6364
+ }
6365
+ while (lines.length < innerHeight) lines.push("");
6366
+ return this.renderFrame(title, lines, width, height);
6367
+ }
6368
+ renderAgentRow(agent, selected, innerWidth) {
6369
+ const colors = this.props.colors;
6370
+ const pointer = selected ? "> " : " ";
6371
+ const head = `${chalk.hex(selected ? colors.primary : colors.textDim)(pointer)}${chalk.hex(agentStatusColor(colors, agent.status))(agent.status === "idle" ? "○" : "●")} ${chalk.hex(selected ? colors.primary : colors.textStrong)(padToWidth$1(truncateToWidth(agent.type, AGENT_NAME_COLS, ELLIPSIS$3), AGENT_NAME_COLS))} ${chalk.hex(agentStatusColor(colors, agent.status))(agentStatusText(agent.status))}${agent.count > 1 ? chalk.hex(colors.textMuted)(` ×${String(agent.count)}`) : ""}`;
6372
+ const budget = innerWidth - visibleWidth(head) - 1;
6373
+ if (budget < 6) return fitExactly$1(head, innerWidth);
6374
+ const hint = singleLine$2(agent.detail ?? agent.description ?? "");
6375
+ if (hint.length === 0) return fitExactly$1(head, innerWidth);
6376
+ const trimmed = truncateToWidth(hint, budget, ELLIPSIS$3);
6377
+ return fitExactly$1(`${head} ${chalk.hex(colors.textDim)(trimmed)}`, innerWidth);
6378
+ }
5821
6379
  renderListRow(task, selected, innerWidth) {
5822
6380
  const colors = this.props.colors;
5823
6381
  const pointer = selected ? "> " : " ";
@@ -5834,15 +6392,7 @@ var TasksBrowserApp = class extends Container {
5834
6392
  return fitExactly$1(`${prefix} ${chalk.hex(colors.text)(desc)}`, innerWidth);
5835
6393
  }
5836
6394
  adjustScroll(visibleRows) {
5837
- if (visibleRows <= 0) {
5838
- this.listScroll = 0;
5839
- return;
5840
- }
5841
- if (this.selectedIndex < this.listScroll) this.listScroll = this.selectedIndex;
5842
- else if (this.selectedIndex >= this.listScroll + visibleRows) this.listScroll = this.selectedIndex - visibleRows + 1;
5843
- const maxScroll = Math.max(0, this.sortedVisible.length - visibleRows);
5844
- if (this.listScroll < 0) this.listScroll = 0;
5845
- if (this.listScroll > maxScroll) this.listScroll = maxScroll;
6395
+ this.listScroll = clampListScroll(this.selectedIndex, this.listScroll, this.sortedVisible.length, visibleRows);
5846
6396
  }
5847
6397
  renderRightStack(width, height) {
5848
6398
  const detailHeight = Math.max(8, Math.min(Math.floor(height * .4), height - 5));
@@ -5850,6 +6400,7 @@ var TasksBrowserApp = class extends Container {
5850
6400
  return [...this.renderDetailFrame(width, detailHeight), ...this.renderPreviewFrame(width, previewHeight)];
5851
6401
  }
5852
6402
  renderDetailFrame(width, height) {
6403
+ if (this.props.filter === "agents") return this.renderAgentDetailFrame(width, height);
5853
6404
  const colors = this.props.colors;
5854
6405
  const innerHeight = Math.max(0, height - 2);
5855
6406
  const task = this.sortedVisible[this.selectedIndex];
@@ -5876,9 +6427,43 @@ var TasksBrowserApp = class extends Container {
5876
6427
  while (lines.length < innerHeight) lines.push("");
5877
6428
  return this.renderFrame(t("taskbrowser.detail"), lines, width, height);
5878
6429
  }
6430
+ /** Right-top pane, `agents` filter: provenance of the selected agent —
6431
+ * type, live status, spawn description, trigger source, parent chain,
6432
+ * latest activity and the current instance id. */
6433
+ renderAgentDetailFrame(width, height) {
6434
+ const colors = this.props.colors;
6435
+ const innerHeight = Math.max(0, height - 2);
6436
+ const agent = this.props.agents[this.agentIndex];
6437
+ if (agent === void 0) {
6438
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_empty"))];
6439
+ while (lines.length < innerHeight) lines.push("");
6440
+ return this.renderFrame(t("taskbrowser.agent_detail"), lines, width, height);
6441
+ }
6442
+ const label = (text) => chalk.hex(colors.textMuted)(text.padEnd(14));
6443
+ const value = (text) => chalk.hex(colors.text)(text);
6444
+ const countSuffix = agent.count > 1 ? chalk.hex(colors.textMuted)(` ×${String(agent.count)}`) : "";
6445
+ const lines = [
6446
+ `${label(t("taskbrowser.agent_type"))}${value(agent.type)}`,
6447
+ `${label(t("taskbrowser.status"))}${chalk.hex(agentStatusColor(colors, agent.status))(agentStatusText(agent.status))}${countSuffix}`,
6448
+ `${label(t("taskbrowser.agent_source"))}${value(singleLine$2(formatAgentSource(agent.source)))}`,
6449
+ `${label(t("taskbrowser.agent_chain"))}${chalk.hex(colors.textMuted)(formatAgentChain(agent))}`
6450
+ ];
6451
+ if (agent.description !== void 0 && agent.description.length > 0) lines.push(`${label(t("taskbrowser.description"))}${value(singleLine$2(agent.description))}`);
6452
+ if (agent.detail !== void 0 && agent.detail.length > 0) lines.push(`${label(t("taskbrowser.agent_activity"))}${value(singleLine$2(agent.detail))}`);
6453
+ const when = formatRelativeTime$2(agent.lastActivityAt);
6454
+ if (when.length > 0) lines.push(`${label(t("taskbrowser.time"))}${chalk.hex(colors.textMuted)(when)}`);
6455
+ if (agent.instanceId !== void 0) lines.push(`${label(t("taskbrowser.agent_instance"))}${chalk.hex(colors.textMuted)(agent.instanceId)}`);
6456
+ while (lines.length < innerHeight) lines.push("");
6457
+ return this.renderFrame(t("taskbrowser.agent_detail"), lines, width, height);
6458
+ }
5879
6459
  renderPreviewFrame(width, height) {
5880
6460
  const colors = this.props.colors;
5881
6461
  const innerHeight = Math.max(0, height - 2);
6462
+ if (this.props.filter === "agents") {
6463
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_no_output"))];
6464
+ while (lines.length < innerHeight) lines.push("");
6465
+ return this.renderFrame("Preview Output", lines, width, height);
6466
+ }
5882
6467
  if (this.sortedVisible[this.selectedIndex] === void 0) {
5883
6468
  const lines = [chalk.hex(colors.textMuted)("No task selected.")];
5884
6469
  while (lines.length < innerHeight) lines.push("");
@@ -5927,6 +6512,7 @@ var TasksBrowserController = class {
5927
6512
  const selectedTaskId = this.pickInitialSelection(tasks, filter);
5928
6513
  const component = new TasksBrowserApp({
5929
6514
  tasks,
6515
+ agents: this.snapshotAgentRows(),
5930
6516
  filter,
5931
6517
  selectedTaskId,
5932
6518
  tailOutput: void 0,
@@ -5964,6 +6550,7 @@ var TasksBrowserController = class {
5964
6550
  if (browser.viewer !== void 0) this.closeOutputViewer();
5965
6551
  if (browser.pollTimer !== void 0) clearInterval(browser.pollTimer);
5966
6552
  if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
6553
+ browser.component.close();
5967
6554
  state.ui.setLayoutRoot(browser.savedLayoutRoot);
5968
6555
  this.host.setTasksBrowser(void 0);
5969
6556
  state.ui.setFocus(state.editor);
@@ -6034,6 +6621,7 @@ var TasksBrowserController = class {
6034
6621
  if (browser === void 0) return;
6035
6622
  browser.component.setProps({
6036
6623
  tasks,
6624
+ agents: this.snapshotAgentRows(),
6037
6625
  filter: browser.filter,
6038
6626
  selectedTaskId: browser.selectedTaskId,
6039
6627
  tailOutput: browser.tailOutput,
@@ -6044,6 +6632,13 @@ var TasksBrowserController = class {
6044
6632
  });
6045
6633
  this.host.state.ui.requestRender();
6046
6634
  }
6635
+ /** Agents view rows: the live slot state machine (authoritative status)
6636
+ * enriched with the per-instance provenance registry, plus the archive ring
6637
+ * for agents whose registry record has already been released. Rebuilt on
6638
+ * every refresh/repaint — all three sources are in-memory snapshots. */
6639
+ snapshotAgentRows() {
6640
+ return buildAgentRows(this.host.subagentSlots, this.host.subagentInstances, this.host.recentSubagentInstances);
6641
+ }
6047
6642
  buildCallbacks() {
6048
6643
  return {
6049
6644
  onSelect: (taskId) => {
@@ -6082,7 +6677,7 @@ var TasksBrowserController = class {
6082
6677
  handleToggleFilter() {
6083
6678
  const browser = this.host.state.tasksBrowser;
6084
6679
  if (browser === void 0) return;
6085
- browser.filter = browser.filter === "all" ? "active" : "all";
6680
+ browser.filter = browser.filter === "all" ? "active" : browser.filter === "active" ? "agents" : "all";
6086
6681
  this.repaint();
6087
6682
  }
6088
6683
  handleRefresh() {
@@ -6351,6 +6946,28 @@ function basenameLike(raw) {
6351
6946
  return raw.split(/[\\/]/).filter((part) => part.length > 0).at(-1) ?? raw;
6352
6947
  }
6353
6948
  //#endregion
6949
+ //#region src/tui/utils/collapse-stub.ts
6950
+ /**
6951
+ * The transcript bounds its memory in two places, and both stand in for the
6952
+ * rows they fold away with the same summary line: the shadow entry array
6953
+ * (`transcript-controller`) folds its oldest entries into one stub entry, and
6954
+ * the committed render tree (`committed-transcript`) folds its oldest child
6955
+ * components into one stub row. One definition of the wording keeps the two
6956
+ * folds reading alike — and lets the array fold read its own count back when
6957
+ * it folds again.
6958
+ */
6959
+ /** Matches {@link formatCollapsedEntries} output, capturing the count. */
6960
+ const COLLAPSE_STUB_PATTERN = /^…\((\d+) earlier entries collapsed\)$/;
6961
+ /** Summary line that stands in for `count` folded-away entries. */
6962
+ function formatCollapsedEntries(count) {
6963
+ return `…(${String(count)} earlier entries collapsed)`;
6964
+ }
6965
+ /** How many entries a summary line stands in for; 0 when `text` is not one. */
6966
+ function parseCollapsedEntries(text) {
6967
+ const digits = COLLAPSE_STUB_PATTERN.exec(text)?.[1];
6968
+ return digits === void 0 ? 0 : Number(digits);
6969
+ }
6970
+ //#endregion
6354
6971
  //#region src/tui/components/transcript/committed-transcript.ts
6355
6972
  var CommittedMessageComponent = class {
6356
6973
  entry;
@@ -6420,6 +7037,9 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6420
7037
  header;
6421
7038
  colors;
6422
7039
  committedCount = 0;
7040
+ /** Rows the retention cap has folded into the summary stub. */
7041
+ collapsedCount = 0;
7042
+ collapseStub;
6423
7043
  constructor(colors) {
6424
7044
  super();
6425
7045
  this.colors = colors;
@@ -6429,6 +7049,12 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6429
7049
  getCount() {
6430
7050
  return this.committedCount;
6431
7051
  }
7052
+ /** Rows the tree folded into its summary stub. They still count toward
7053
+ * {@link getCount()}: folding changes what is on screen, never what was
7054
+ * committed. */
7055
+ getCollapsedCount() {
7056
+ return this.collapsedCount;
7057
+ }
6432
7058
  setCount(count) {
6433
7059
  this.committedCount = count;
6434
7060
  if (count === 0) this.header.setText("");
@@ -6437,11 +7063,46 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6437
7063
  }
6438
7064
  appendEntry(entry, colors) {
6439
7065
  this.addChild(new CommittedMessageComponent(entry, colors));
7066
+ this.enforceRetentionCap();
7067
+ }
7068
+ /**
7069
+ * Holds the tree at its retention point: the entry children it renders —
7070
+ * the summary stub included — never exceed MAX_COMMITTED_ENTRIES. Past that
7071
+ * the oldest rows fold into the stub, the same semantics as the entry-array
7072
+ * cap, so the child list (and the render cache every child carries) stops
7073
+ * growing with the session. The stub takes one slot of the budget, so the
7074
+ * fold lands on stub + (MAX - 1) rows: the cap is the actually retained set.
7075
+ */
7076
+ enforceRetentionCap() {
7077
+ if (this.children.length - 1 <= 500) return;
7078
+ let stub = this.collapseStub;
7079
+ if (stub === void 0) {
7080
+ stub = new Text("", 0, 0);
7081
+ this.collapseStub = stub;
7082
+ this.children.splice(1, 0, stub);
7083
+ }
7084
+ const fold = this.children.length - 2 - 499;
7085
+ this.children.splice(2, fold);
7086
+ this.collapsedCount += fold;
7087
+ stub.setText(` ${chalk.hex(this.colors.textDim)(formatCollapsedEntries(this.collapsedCount))}`);
6440
7088
  this.invalidate();
6441
7089
  }
6442
7090
  };
6443
- //#endregion
6444
- //#region src/tui/controllers/transcript-controller.ts
7091
+ /** Id of the aggregate stub that stands in for the folded oldest entries. */
7092
+ const COLLAPSE_STUB_ID = "transcript-collapse-stub";
7093
+ function makeCollapseStub(count) {
7094
+ return {
7095
+ id: COLLAPSE_STUB_ID,
7096
+ kind: "status",
7097
+ renderMode: "plain",
7098
+ content: formatCollapsedEntries(count)
7099
+ };
7100
+ }
7101
+ /** Entries an existing stub already stands in for (0 when it is not one). */
7102
+ function stubCount(entry) {
7103
+ if (entry === void 0 || entry.id !== COLLAPSE_STUB_ID) return 0;
7104
+ return parseCollapsedEntries(entry.content);
7105
+ }
6445
7106
  var TranscriptController = class TranscriptController {
6446
7107
  host;
6447
7108
  welcomeComponent;
@@ -6451,6 +7112,17 @@ var TranscriptController = class TranscriptController {
6451
7112
  * ticker of the card that is still claiming the task is running. */
6452
7113
  liveNoticesByTrackingId = /* @__PURE__ */ new Map();
6453
7114
  pendingComponents = /* @__PURE__ */ new Set();
7115
+ /** Monotonic count of composition changes to `state.transcriptEntries`:
7116
+ * +1 per ingest and +1 per cap fold. Aggregates that memoize over the entry
7117
+ * list key on this — the fold holds the array length at the cap while
7118
+ * replacing the oldest rows, so a length-only key can go stale. */
7119
+ ingestCount = 0;
7120
+ /** Contributions of the rows the entry cap has already folded out of the
7121
+ * array. Kept separate from the rows still held, so aggregates that must
7122
+ * stay monotonic over a long session (the sidebar's session stats) can add
7123
+ * them back — see {@link getFoldedEntryCounts}. */
7124
+ foldedTurns = 0;
7125
+ foldedToolCalls = 0;
6454
7126
  /** Max live transcript children before the oldest are folded into the
6455
7127
  * committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
6456
7128
  * (mirrors a commit-fold approach for bounding ultra-long sessions). */
@@ -6583,12 +7255,55 @@ var TranscriptController = class TranscriptController {
6583
7255
  default: return null;
6584
7256
  }
6585
7257
  }
7258
+ /**
7259
+ * The transcript's single bounded ingress: every entry reaches
7260
+ * `state.transcriptEntries` through here — `appendEntry` for rows that mount
7261
+ * their own component, and `ScreamTUI.pushTranscriptEntry` for the live
7262
+ * streaming rows that already own one. One place applies the UI preview
7263
+ * bound (`boundEntryForUi`) and the entry-count cap, so no producer can grow
7264
+ * the array without bound, whichever path it comes in on.
7265
+ *
7266
+ * Returns the entry as stored: a bounded copy when the bound rewrote it,
7267
+ * otherwise the very entry handed in — identity matters to callers that keep
7268
+ * mutating a live draft (assistant text, thinking) after pushing it.
7269
+ */
7270
+ ingestEntry(entry) {
7271
+ const boundedEntry = boundEntryForUi(entry);
7272
+ this.ingestCount += 1;
7273
+ this.host.state.transcriptEntries.push(boundedEntry);
7274
+ this.enforceEntryCap();
7275
+ return boundedEntry;
7276
+ }
7277
+ /**
7278
+ * Monotonic count of transcript composition changes: one per ingest, plus
7279
+ * one per cap fold. Consumers that memoize aggregates over
7280
+ * `state.transcriptEntries` (the sidebar session stats) key on this instead
7281
+ * of `length` alone, because a fold lands the array back on the same length
7282
+ * while replacing its oldest rows.
7283
+ */
7284
+ getIngestCount() {
7285
+ return this.ingestCount;
7286
+ }
7287
+ /**
7288
+ * What the entry cap has already folded out of `state.transcriptEntries`:
7289
+ * user turns and real tool calls (compression markers ride kind 'tool_call'
7290
+ * but are not invocations — same rule as the retained-row scan). Aggregates
7291
+ * that must be monotonic over a long session — the sidebar's session stats —
7292
+ * add these to the rows the array still holds, because the array stops
7293
+ * holding the oldest ones at the cap.
7294
+ */
7295
+ getFoldedEntryCounts() {
7296
+ return {
7297
+ turns: this.foldedTurns,
7298
+ toolCalls: this.foldedToolCalls
7299
+ };
7300
+ }
6586
7301
  appendEntry(entry) {
6587
- this.host.state.transcriptEntries.push(entry);
6588
- const component = this.createComponent(entry);
7302
+ const boundedEntry = this.ingestEntry(entry);
7303
+ const component = this.createComponent(boundedEntry);
6589
7304
  if (component) {
6590
- this.liveComponentToEntry.set(component, entry);
6591
- const status = entry.backgroundAgentStatus;
7305
+ this.liveComponentToEntry.set(component, boundedEntry);
7306
+ const status = boundedEntry.backgroundAgentStatus;
6592
7307
  if (status !== void 0 && component instanceof BackgroundAgentStatusComponent) {
6593
7308
  const trackingId = status.trackingId;
6594
7309
  if (trackingId !== void 0) if (status.phase === "started") this.liveNoticesByTrackingId.set(trackingId, component);
@@ -6602,6 +7317,24 @@ var TranscriptController = class TranscriptController {
6602
7317
  }
6603
7318
  return component ?? null;
6604
7319
  }
7320
+ /**
7321
+ * Keeps `state.transcriptEntries` within {@link MAX_TRANSCRIPT_ENTRIES} by
7322
+ * folding the oldest rows into one aggregate stub. A stub already at the head
7323
+ * keeps that slot and accumulates the count, so the array stays bounded no
7324
+ * matter how long the session runs.
7325
+ */
7326
+ enforceEntryCap() {
7327
+ const entries = this.host.state.transcriptEntries;
7328
+ if (entries.length <= 500) return;
7329
+ const head = entries[0];
7330
+ const isStub = head !== void 0 && head.id === COLLAPSE_STUB_ID;
7331
+ const dropped = entries.splice(0, entries.length - 500 + 1);
7332
+ for (const entry of dropped) if (entry.kind === "user") this.foldedTurns += 1;
7333
+ else if (entry.kind === "tool_call" && entry.compactionData === void 0) this.foldedToolCalls += 1;
7334
+ const folded = dropped.length - (isStub ? 1 : 0);
7335
+ entries.unshift(makeCollapseStub(stubCount(head) + folded));
7336
+ this.ingestCount += 1;
7337
+ }
6605
7338
  /** Append a suffix to the given turn's final assistant message last line.
6606
7339
  * Used to place the turn elapsed marker flush against the last character
6607
7340
  * of the assistant's reply. Turn-scoped so a tool-only turn never stamps
@@ -6656,6 +7389,8 @@ var TranscriptController = class TranscriptController {
6656
7389
  streamingUI.endActivityGroup();
6657
7390
  streamingUI.flushPendingApprovals();
6658
7391
  state.transcriptEntries = [];
7392
+ this.foldedTurns = 0;
7393
+ this.foldedToolCalls = 0;
6659
7394
  streamingUI.disposeActiveCompactionBlock();
6660
7395
  streamingUI.resetLiveText();
6661
7396
  streamingUI.resetToolUi();
@@ -7969,7 +8704,8 @@ var LifecycleController = class LifecycleController {
7969
8704
  hubProbe = createHubProbe();
7970
8705
  sidebarGitCacheWorkDir = "";
7971
8706
  /** Memo for the session-stats aggregation: recomputed only when the
7972
- * transcript length, compaction count or usage object changes. */
8707
+ * transcript's ingest counter/length, compaction count or usage object
8708
+ * changes. */
7973
8709
  sidebarStatsMemo;
7974
8710
  static MEMORY_IDLE_MS = 900 * 1e3;
7975
8711
  static MEMORY_COUNTDOWN_MS = 15 * 1e3;
@@ -8236,16 +8972,20 @@ var LifecycleController = class LifecycleController {
8236
8972
  }
8237
8973
  readSidebarSessionStats(appState) {
8238
8974
  const entries = this.host.state.transcriptEntries;
8975
+ const ingestCount = this.host.transcriptController.getIngestCount();
8239
8976
  const usage = appState.sessionUsage;
8240
8977
  const subagentUsage = appState.subagentUsage;
8241
8978
  const memo = this.sidebarStatsMemo;
8242
- if (memo !== void 0 && memo.length === entries.length && memo.compactions === appState.autoCompactionCount && memo.usage === usage && memo.subagentUsage === subagentUsage && memo.apiCalls === appState.sessionApiCalls) return memo.stats;
8979
+ if (memo !== void 0 && memo.ingestCount === ingestCount && memo.length === entries.length && memo.compactions === appState.autoCompactionCount && memo.usage === usage && memo.subagentUsage === subagentUsage && memo.apiCalls === appState.sessionApiCalls) return memo.stats;
8243
8980
  let turns = 0;
8244
8981
  let toolCalls = 0;
8245
8982
  for (const entry of entries) {
8246
8983
  if (entry.kind === "user") turns += 1;
8247
8984
  if (entry.kind === "tool_call" && entry.compactionData === void 0) toolCalls += 1;
8248
8985
  }
8986
+ const foldedAway = this.host.transcriptController.getFoldedEntryCounts();
8987
+ turns += foldedAway.turns;
8988
+ toolCalls += foldedAway.toolCalls;
8249
8989
  let subagentOutput = 0;
8250
8990
  for (const sub of Object.values(subagentUsage)) subagentOutput += sub.output;
8251
8991
  const stats = {
@@ -8259,6 +8999,7 @@ var LifecycleController = class LifecycleController {
8259
8999
  tokensOutput: usage.output + subagentOutput
8260
9000
  };
8261
9001
  this.sidebarStatsMemo = {
9002
+ ingestCount,
8262
9003
  length: entries.length,
8263
9004
  compactions: appState.autoCompactionCount,
8264
9005
  usage,
@@ -11274,6 +12015,7 @@ var SessionManager$1 = class {
11274
12015
  planMode: status.planMode ? status.planStrategy === "fusion" ? "fusionplan" : "plan" : "off",
11275
12016
  wolfpackMode: status.wolfpackMode,
11276
12017
  rlmEnabled: status.rlmEnabled,
12018
+ rlmMaxDepth: status.rlmMaxDepth ?? null,
11277
12019
  contextTokens: status.contextTokens,
11278
12020
  maxContextTokens: status.maxContextTokens,
11279
12021
  contextUsage: status.contextUsage,
@@ -13490,7 +14232,7 @@ var DialogManager = class {
13490
14232
  }));
13491
14233
  }
13492
14234
  showMemoryPicker(preloadedMemos, preloadedTotal) {
13493
- const store = new MemoryMemoStore(getDataDir());
14235
+ const store = sharedMemoStore(getDataDir());
13494
14236
  const hasData = preloadedMemos !== void 0;
13495
14237
  const memos = preloadedMemos ?? [];
13496
14238
  const total = preloadedTotal ?? 0;
@@ -13655,6 +14397,7 @@ function createInitialAppState(input) {
13655
14397
  ccConnectActive: false,
13656
14398
  wolfpackMode: input.cliOptions.wolfpack === true,
13657
14399
  rlmEnabled: false,
14400
+ rlmMaxDepth: null,
13658
14401
  reconnectAttempt: 0,
13659
14402
  recentSessions: [],
13660
14403
  subagentUsage: {}
@@ -13986,7 +14729,7 @@ var ScreamTUI = class {
13986
14729
  return first;
13987
14730
  }
13988
14731
  pushTranscriptEntry(entry) {
13989
- this.state.transcriptEntries.push(entry);
14732
+ this.transcriptController.ingestEntry(entry);
13990
14733
  }
13991
14734
  setExternalEditorRunning(running) {
13992
14735
  this.state.externalEditorRunning = running;
@@ -14003,6 +14746,21 @@ var ScreamTUI = class {
14003
14746
  get backgroundTasks() {
14004
14747
  return this.sessionEventHandler.backgroundTasks;
14005
14748
  }
14749
+ /** Live per-type subagent slots (sidebar state machine) — source of the
14750
+ * /tasks browser's Agents view. */
14751
+ get subagentSlots() {
14752
+ return this.sessionEventHandler.getSubagentSlots();
14753
+ }
14754
+ /** Per-instance subagent provenance (spawn parent / outcome / description),
14755
+ * keyed by agentId — source of the /tasks browser's Agents view. */
14756
+ get subagentInstances() {
14757
+ return this.sessionEventHandler.getSubagentInstances();
14758
+ }
14759
+ /** Archive ring for the Agents view: closed instances' last derived rows
14760
+ * (oldest first), the bounded home of agents the live map has released. */
14761
+ get recentSubagentInstances() {
14762
+ return this.sessionEventHandler.getRecentSubagentInstances();
14763
+ }
14006
14764
  getCurrentSessionId() {
14007
14765
  return this.state.appState.sessionId;
14008
14766
  }
@@ -14172,7 +14930,7 @@ var ScreamTUI = class {
14172
14930
  const card = new OnboardingCardComponent({
14173
14931
  colors: this.state.theme.colors,
14174
14932
  onConfigure: () => {
14175
- import("./dispatch-Ba8Dz_v_.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14933
+ import("./dispatch-DoaBG--u.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14176
14934
  this.restoreEditor();
14177
14935
  onDone?.();
14178
14936
  });
@@ -16179,6 +16937,15 @@ var FileGate = class {
16179
16937
  * consumer of the same SDK (alongside run-shell TUI and run-stream-json),
16180
16938
  * not a separate engine. Zero changes to agent-core or node-sdk.
16181
16939
  */
16940
+ /**
16941
+ * In-memory FIFO caps per web session. The on-disk journal stays complete;
16942
+ * these bounds only stop a long-lived session from growing without limit in
16943
+ * RAM. Dropped rows are tallied so the elision stays observable (see
16944
+ * `getMemoryBounds`) — the same `[…N … elided…]` accounting the transcript
16945
+ * uses, without re-deriving it from a cursor gap.
16946
+ */
16947
+ const MAX_JOURNAL_ENTRIES = 200;
16948
+ const MAX_SESSION_MESSAGES = 200;
16182
16949
  /** Thinking bodies longer than this are delivered as a tail excerpt + on-demand full text. */
16183
16950
  const TRUNCATE_THINKING_LEN = 8 * 1024;
16184
16951
  function truncateThinking(messages) {
@@ -16529,6 +17296,50 @@ const MAIN_TRANSCRIPT_EVENTS = new Set([
16529
17296
  "tool.result"
16530
17297
  ]);
16531
17298
  /**
17299
+ * Identity of a persisted user-message row; used by the fork copy to drop the
17300
+ * memory copy of a row the disk already carries.
17301
+ */
17302
+ function persistedUserMessageKey(entry) {
17303
+ return JSON.stringify([
17304
+ entry.beforeSeq,
17305
+ entry.clientMessageId ?? null,
17306
+ entry.text
17307
+ ]);
17308
+ }
17309
+ /**
17310
+ * Union of the two views of one session's durable journal, for the fork copy.
17311
+ *
17312
+ * `diskEntries` (read back from the source's journal file) is the complete
17313
+ * history and forms the base: the in-memory journal is FIFO-capped
17314
+ * (MAX_JOURNAL_ENTRIES), so a long conversation's memory only holds its tail
17315
+ * while the file keeps every row. `memoryEntries` fills the rows a
17316
+ * fire-and-forget write has not flushed yet; rows the disk already carries are
17317
+ * dropped by seq (journal rows) or identity (user messages), so no line is
17318
+ * doubled.
17319
+ */
17320
+ function mergeForkJournalEntries(diskEntries, memoryEntries) {
17321
+ const journalSeqs = /* @__PURE__ */ new Set();
17322
+ const userMessageKeys = /* @__PURE__ */ new Set();
17323
+ const journalEntries = [];
17324
+ const userMessages = [];
17325
+ const push = (entry) => {
17326
+ if (entry.type === "user_message") {
17327
+ const key = persistedUserMessageKey(entry);
17328
+ if (userMessageKeys.has(key)) return;
17329
+ userMessageKeys.add(key);
17330
+ userMessages.push(entry);
17331
+ return;
17332
+ }
17333
+ if (journalSeqs.has(entry.seq)) return;
17334
+ journalSeqs.add(entry.seq);
17335
+ journalEntries.push(entry);
17336
+ };
17337
+ for (const entry of diskEntries) push(entry);
17338
+ for (const entry of memoryEntries) push(entry);
17339
+ journalEntries.sort((a, b) => a.seq - b.seq);
17340
+ return [...journalEntries, ...userMessages];
17341
+ }
17342
+ /**
16532
17343
  * Whether a journal entry already carries a message (if so it needs no seeding
16533
17344
  * from core history).
16534
17345
  */
@@ -16910,6 +17721,10 @@ var WebSession = class {
16910
17721
  connections = /* @__PURE__ */ new Map();
16911
17722
  journal = [];
16912
17723
  userMessages = [];
17724
+ /** Aggregate count of journal entries dropped by the FIFO cap. */
17725
+ droppedJournalCount = 0;
17726
+ /** Aggregate count of message rows dropped by the FIFO cap. */
17727
+ droppedMessageCount = 0;
16913
17728
  nextSeq = 1;
16914
17729
  epoch = 1;
16915
17730
  cachedStatus = null;
@@ -17001,7 +17816,11 @@ var WebSession = class {
17001
17816
  getTitle() {
17002
17817
  return this.title;
17003
17818
  }
17004
- /** Return in-memory durable journal entries for fork copy (avoids file persistence race). */
17819
+ /**
17820
+ * Return the in-memory durable journal entries (capped tail). The fork copy
17821
+ * takes these only for rows the source's journal file does not carry yet;
17822
+ * the file itself is the complete history.
17823
+ */
17005
17824
  getDurableJournalEntries() {
17006
17825
  const entries = [];
17007
17826
  for (const e of this.journal) if (!e.volatile) entries.push({
@@ -17016,6 +17835,15 @@ var WebSession = class {
17016
17835
  });
17017
17836
  return entries;
17018
17837
  }
17838
+ /**
17839
+ * Drain in-flight persistence writes (journal rows, user messages, metadata).
17840
+ * A fork reads the source's journal file afterwards and must not race a write
17841
+ * still queued behind the serialization gate — the reader would otherwise
17842
+ * mistake a flushed row for unsaved state.
17843
+ */
17844
+ async flushPendingWrites() {
17845
+ await Promise.allSettled(this.pendingWrites);
17846
+ }
17019
17847
  async persistEntry(entry, throwOnError = false) {
17020
17848
  if (!this.homeDir) return;
17021
17849
  const line = JSON.stringify({
@@ -17034,6 +17862,7 @@ var WebSession = class {
17034
17862
  },
17035
17863
  beforeSeq
17036
17864
  });
17865
+ this.trimMessages();
17037
17866
  if (!this.homeDir) return;
17038
17867
  const line = JSON.stringify({
17039
17868
  type: "user_message",
@@ -17064,9 +17893,11 @@ var WebSession = class {
17064
17893
  },
17065
17894
  beforeSeq: um.beforeSeq
17066
17895
  });
17896
+ this.trimMessages();
17067
17897
  } else {
17068
17898
  const je = entry;
17069
17899
  this.journal.push(je);
17900
+ this.trimJournal();
17070
17901
  this.nextSeq = Math.max(this.nextSeq, je.seq + 1);
17071
17902
  if (this.session === null && je.payload.agentId === "main" && je.payload.type === "goal.updated") this.cachedGoal = cloneSnapshot(je.payload.snapshot);
17072
17903
  else if (this.session === null && je.payload.agentId === "main" && je.payload.type === "todo.updated") this.cachedTodos = cloneSnapshot(je.payload.todos);
@@ -17133,6 +17964,7 @@ var WebSession = class {
17133
17964
  ...message,
17134
17965
  seq
17135
17966
  });
17967
+ this.trimMessages();
17136
17968
  seq += 1;
17137
17969
  }
17138
17970
  if (seq - 1 >= this.nextSeq) this.nextSeq = seq;
@@ -17156,6 +17988,44 @@ var WebSession = class {
17156
17988
  permission: this.permission
17157
17989
  };
17158
17990
  }
17991
+ /** FIFO-cap `journal` to `MAX_JOURNAL_ENTRIES`, tallying elided rows. */
17992
+ trimJournal() {
17993
+ while (this.journal.length > MAX_JOURNAL_ENTRIES) {
17994
+ this.journal.shift();
17995
+ this.droppedJournalCount += 1;
17996
+ }
17997
+ }
17998
+ /**
17999
+ * FIFO-cap message stores to `MAX_SESSION_MESSAGES`, tallying elided rows.
18000
+ *
18001
+ * Drop order follows the rendered order (`buildMessages` puts the seeded
18002
+ * core-history rows at the head, live user rows after them), so the eviction
18003
+ * is genuinely oldest-first and the visible tail of the conversation is what
18004
+ * survives.
18005
+ */
18006
+ trimMessages() {
18007
+ while (this.userMessages.length + this.seededMessages.length > MAX_SESSION_MESSAGES) {
18008
+ if (this.seededMessages.length > 0) this.seededMessages.shift();
18009
+ else if (this.userMessages.length > 0) this.userMessages.shift();
18010
+ else break;
18011
+ this.droppedMessageCount += 1;
18012
+ }
18013
+ }
18014
+ /**
18015
+ * Observable surface of the in-memory FIFO caps: current occupancy, the
18016
+ * limits, and the aggregate elided-row tallies (`[…N … elided…]` accounting,
18017
+ * shared with the transcript).
18018
+ */
18019
+ getMemoryBounds() {
18020
+ return {
18021
+ journal: this.journal.length,
18022
+ journalLimit: MAX_JOURNAL_ENTRIES,
18023
+ journalElided: this.droppedJournalCount,
18024
+ messages: this.userMessages.length + this.seededMessages.length,
18025
+ messageLimit: MAX_SESSION_MESSAGES,
18026
+ messagesElided: this.droppedMessageCount
18027
+ };
18028
+ }
17159
18029
  appendEvent(event) {
17160
18030
  const payload = event.type === "goal.updated" || event.type === "todo.updated" ? cloneSnapshot(event) : event;
17161
18031
  const entry = {
@@ -17165,6 +18035,7 @@ var WebSession = class {
17165
18035
  payload
17166
18036
  };
17167
18037
  this.journal.push(entry);
18038
+ this.trimJournal();
17168
18039
  if (!entry.volatile) this.trackPending(this.persistEntry(entry));
17169
18040
  return entry;
17170
18041
  }
@@ -17181,6 +18052,7 @@ var WebSession = class {
17181
18052
  payload
17182
18053
  };
17183
18054
  this.journal.push(entry);
18055
+ this.trimJournal();
17184
18056
  const persisted = this.persistEntry(entry, throwOnPersistenceError);
17185
18057
  this.trackPending(persisted);
17186
18058
  return {
@@ -18488,8 +19360,9 @@ var SessionManager = class {
18488
19360
  }
18489
19361
  /**
18490
19362
  * Fork an active session: harness.forkSession copies the agent state into a
18491
- * new session, and we additionally copy the source's durable journal so the
18492
- * forked web session displays the same prior conversation.
19363
+ * new session, and we additionally copy the source's durable journal from its
19364
+ * journal file — the complete history, not the FIFO-capped in-memory tail —
19365
+ * so the forked web session displays the same prior conversation.
18493
19366
  */
18494
19367
  async forkSession(sourceId) {
18495
19368
  const source = this.sessions.get(sourceId);
@@ -18509,13 +19382,18 @@ var SessionManager = class {
18509
19382
  title,
18510
19383
  onFork: (id) => this.forkSession(id)
18511
19384
  });
18512
- const entries = source.getDurableJournalEntries();
18513
- if (entries.length > 0) {
19385
+ await source.flushPendingWrites();
19386
+ const { entries: diskEntries, corrupt } = await loadJournal(this.homeDir, sourceId);
19387
+ const entries = mergeForkJournalEntries(diskEntries, source.getDurableJournalEntries());
19388
+ webSession.loadFromPersisted(entries);
19389
+ const journalHasContent = webSession.hasAnyAssistantContent();
19390
+ const journalDamaged = corrupt > 0 && !journalHasContent;
19391
+ if (journalDamaged) webSession.markDamaged(`journal has ${corrupt} corrupt lines and no parsed message carries a body`);
19392
+ else if (entries.length > 0) {
18514
19393
  const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
18515
19394
  await writeFile(getJournalPath(this.homeDir, newId), lines);
18516
19395
  }
18517
- webSession.loadFromPersisted(entries);
18518
- if (!entries.some(isMessageBearingEntry)) try {
19396
+ if (!entries.some(isMessageBearingEntry) || journalDamaged) try {
18519
19397
  const context = await session.getContext();
18520
19398
  webSession.seedHistory(contextHistoryToChatMessages(context.history));
18521
19399
  } catch (error) {
@@ -18993,6 +19871,19 @@ function startWebIdleExit(opts) {
18993
19871
  }
18994
19872
  };
18995
19873
  }
19874
+ /**
19875
+ * Validates the optional `maxDepth` body field of the /rlm route with the same
19876
+ * grammar as /rlm-max-depth (a non-negative integer; 0 = unlimited) via the
19877
+ * shared agent-core helper. Absent / null leaves the cap untouched; any other
19878
+ * invalid value is a 400 so a client cannot silently send a cap that never
19879
+ * lands.
19880
+ */
19881
+ function optionalRlmMaxDepth(raw) {
19882
+ if (raw === void 0 || raw === null) return void 0;
19883
+ const parsed = parseRlmMaxDepthArg(typeof raw === "string" ? raw : String(raw));
19884
+ if (!parsed.ok) throw new HttpError(400, `Invalid rlm max depth: ${parsed.reason}`);
19885
+ return parsed.value;
19886
+ }
18996
19887
  async function handleSessionControlRoutes(req, res, url, method, manager) {
18997
19888
  const statusMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/status$`).exec(url);
18998
19889
  if (statusMatch && method === "GET") {
@@ -19080,7 +19971,7 @@ async function handleSessionControlRoutes(req, res, url, method, manager) {
19080
19971
  try {
19081
19972
  const body = await readJsonBody(req);
19082
19973
  const enabled = optionalBoolean(body, "enabled", true);
19083
- const maxDepth = typeof body["maxDepth"] === "number" ? Number(body["maxDepth"]) : void 0;
19974
+ const maxDepth = optionalRlmMaxDepth(body["maxDepth"]);
19084
19975
  await manager.setRlm(decodeURIComponent(rlmMatch[1]), enabled, maxDepth);
19085
19976
  sendJson(res, 200, { ok: true });
19086
19977
  } catch (error) {