scream-code 0.17.8 → 0.18.0
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.
- package/README.md +3 -3
- package/dist/{app-3vWeDa7Q.mjs → app-BuWHcggN.mjs} +982 -51
- package/dist/{dispatch-Ba8Dz_v_.mjs → dispatch-CUfxgLwq.mjs} +1 -1
- package/dist/{dispatch-B8IVXCrX.mjs → dispatch-DfHVLpfA.mjs} +2292 -381
- package/dist/main.mjs +1 -1
- package/dist/public/assets/{index-CVXe8l3P.css → index-B1g39y1G.css} +1 -1
- package/dist/public/assets/{index-CKa_o5qA.js → index-BTL8OnOo.js} +36 -36
- package/dist/public/assets/index-BTL8OnOo.js.map +1 -0
- package/dist/public/index.html +2 -2
- package/dist/{src-BhZujafw.mjs → src-C7C9e5Ds.mjs} +80 -46
- package/dist/{src-CUURS-i6.mjs → src-qit_jRUM.mjs} +1 -1
- package/dist/{text-input-dialog-CD0sakGq.mjs → text-input-dialog-CK6jTftM.mjs} +56 -6
- package/dist/{text-input-dialog-DcjQLJFz.mjs → text-input-dialog-DZoGkbYZ.mjs} +1 -1
- package/package.json +3 -3
- package/dist/public/assets/index-CKa_o5qA.js.map +0 -1
|
@@ -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 {
|
|
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
|
|
9
|
-
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-
|
|
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-DfHVLpfA.mjs";
|
|
9
|
+
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CK6jTftM.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.
|
|
36
|
+
version: optionalBuildString("0.18.0"),
|
|
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
|
|
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);
|
|
@@ -3919,6 +4206,7 @@ var SessionReplayRenderer = class {
|
|
|
3919
4206
|
this.host = host;
|
|
3920
4207
|
}
|
|
3921
4208
|
async hydrateFromReplay(session) {
|
|
4209
|
+
this.host.state.ui.resetTextSelection();
|
|
3922
4210
|
this.host.setAppState({ isReplaying: true });
|
|
3923
4211
|
try {
|
|
3924
4212
|
const main = session.getResumeState()?.agents["main"];
|
|
@@ -4380,6 +4668,89 @@ function isControlCharacter(ch) {
|
|
|
4380
4668
|
const code = ch.codePointAt(0) ?? 0;
|
|
4381
4669
|
return code >= 0 && code <= 31 || code >= 127 && code <= 159;
|
|
4382
4670
|
}
|
|
4671
|
+
/** Max characters kept across head + tail of the preview combined. */
|
|
4672
|
+
const UI_OUTPUT_MAX_CHARS = 8e3;
|
|
4673
|
+
/** Same marker string as agent-core's `result-builder.ts` TRUNCATION_MARKER. */
|
|
4674
|
+
const TRUNCATION_MARKER = "[...truncated]";
|
|
4675
|
+
function elisionMarker(elided) {
|
|
4676
|
+
return `[…${String(elided)} lines elided…]`;
|
|
4677
|
+
}
|
|
4678
|
+
/**
|
|
4679
|
+
* Bounds one tool-output string to a head + tail preview:
|
|
4680
|
+
* - at most {@link UI_OUTPUT_MAX_HEAD_LINES} lines from the start and
|
|
4681
|
+
* {@link UI_OUTPUT_MAX_TAIL_LINES} lines from the end (the middle is
|
|
4682
|
+
* replaced with `[…N lines elided…]`);
|
|
4683
|
+
* - the kept head + tail text stays within {@link UI_OUTPUT_MAX_CHARS} chars,
|
|
4684
|
+
* dropping whole lines first and hard-slicing a lone oversized line last
|
|
4685
|
+
* (marked `[...truncated]`).
|
|
4686
|
+
*
|
|
4687
|
+
* Returns the input unchanged when it already fits both budgets.
|
|
4688
|
+
*/
|
|
4689
|
+
function boundToolOutput(output) {
|
|
4690
|
+
const lines = output.split("\n");
|
|
4691
|
+
const totalLines = lines.length;
|
|
4692
|
+
const overflowLines = totalLines > 80;
|
|
4693
|
+
const overflowChars = output.length > UI_OUTPUT_MAX_CHARS;
|
|
4694
|
+
if (!overflowLines && !overflowChars) return output;
|
|
4695
|
+
const head = lines.slice(0, 40);
|
|
4696
|
+
const tail = overflowLines ? lines.slice(totalLines - 40) : lines.slice(40);
|
|
4697
|
+
let elided = totalLines - head.length - tail.length;
|
|
4698
|
+
const contentBudget = UI_OUTPUT_MAX_CHARS - 2;
|
|
4699
|
+
let headText = head.join("\n");
|
|
4700
|
+
let tailText = tail.join("\n");
|
|
4701
|
+
while (headText.length + tailText.length > contentBudget) {
|
|
4702
|
+
if (head.length > 1 && (tail.length <= 1 || headText.length >= tailText.length)) {
|
|
4703
|
+
head.pop();
|
|
4704
|
+
headText = head.join("\n");
|
|
4705
|
+
elided += 1;
|
|
4706
|
+
continue;
|
|
4707
|
+
}
|
|
4708
|
+
if (tail.length > 1) {
|
|
4709
|
+
tail.shift();
|
|
4710
|
+
tailText = tail.join("\n");
|
|
4711
|
+
elided += 1;
|
|
4712
|
+
continue;
|
|
4713
|
+
}
|
|
4714
|
+
break;
|
|
4715
|
+
}
|
|
4716
|
+
if (headText.length + tailText.length > contentBudget) {
|
|
4717
|
+
const tailKeep = Math.min(tailText.length, Math.floor(contentBudget / 2));
|
|
4718
|
+
const headKeep = Math.min(headText.length, contentBudget - tailKeep);
|
|
4719
|
+
headText = headText.slice(0, headKeep);
|
|
4720
|
+
tailText = tailText.slice(tailText.length - tailKeep);
|
|
4721
|
+
}
|
|
4722
|
+
const marker = elided > 0 ? elisionMarker(elided) : TRUNCATION_MARKER;
|
|
4723
|
+
const parts = [];
|
|
4724
|
+
if (headText.length > 0) parts.push(headText);
|
|
4725
|
+
parts.push(marker);
|
|
4726
|
+
if (tailText.length > 0) parts.push(tailText);
|
|
4727
|
+
return parts.join("\n");
|
|
4728
|
+
}
|
|
4729
|
+
/**
|
|
4730
|
+
* Returns a transcript entry safe to keep in `state.transcriptEntries`:
|
|
4731
|
+
* tool-group entries get their `result.output` replaced with the bounded
|
|
4732
|
+
* preview (a copy — message, brief, titles, args and every other field stay
|
|
4733
|
+
* as they are). Non-tool entries — and tool entries already within budget —
|
|
4734
|
+
* come back untouched (no copy).
|
|
4735
|
+
*/
|
|
4736
|
+
function boundEntryForUi(entry) {
|
|
4737
|
+
const toolCallData = entry.toolCallData;
|
|
4738
|
+
if (toolCallData === void 0) return entry;
|
|
4739
|
+
const result = toolCallData.result;
|
|
4740
|
+
if (result === void 0) return entry;
|
|
4741
|
+
const boundedOutput = boundToolOutput(result.output);
|
|
4742
|
+
if (boundedOutput === result.output) return entry;
|
|
4743
|
+
return {
|
|
4744
|
+
...entry,
|
|
4745
|
+
toolCallData: {
|
|
4746
|
+
...toolCallData,
|
|
4747
|
+
result: {
|
|
4748
|
+
...result,
|
|
4749
|
+
output: boundedOutput
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4383
4754
|
//#endregion
|
|
4384
4755
|
//#region src/tui/controllers/streaming-ui.ts
|
|
4385
4756
|
var StreamingUIController = class StreamingUIController {
|
|
@@ -5139,7 +5510,16 @@ var StreamingUIController = class StreamingUIController {
|
|
|
5139
5510
|
this.host.transcriptController.unmarkPending(tc);
|
|
5140
5511
|
this._pendingToolComponents.delete(toolCallId);
|
|
5141
5512
|
const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
|
|
5142
|
-
if (entry?.toolCallData !== void 0)
|
|
5513
|
+
if (entry?.toolCallData !== void 0) {
|
|
5514
|
+
const bounded = boundEntryForUi({
|
|
5515
|
+
...entry,
|
|
5516
|
+
toolCallData: {
|
|
5517
|
+
...entry.toolCallData,
|
|
5518
|
+
result
|
|
5519
|
+
}
|
|
5520
|
+
});
|
|
5521
|
+
entry.toolCallData.result = bounded.toolCallData?.result ?? result;
|
|
5522
|
+
}
|
|
5143
5523
|
this.host.transcriptController.commit();
|
|
5144
5524
|
state.ui.requestRender();
|
|
5145
5525
|
return;
|
|
@@ -5538,6 +5918,64 @@ function statusColor(colors, status) {
|
|
|
5538
5918
|
function isTerminal(status) {
|
|
5539
5919
|
return status === "completed" || status === "failed" || status === "killed" || status === "lost";
|
|
5540
5920
|
}
|
|
5921
|
+
/** Width of the agent-name column in the Agents list (leaves room for the
|
|
5922
|
+
* status word, the instance count and an activity hint on narrow panes). */
|
|
5923
|
+
const AGENT_NAME_COLS = 12;
|
|
5924
|
+
/** Localized word per agent row status: the six live slot states plus the two
|
|
5925
|
+
* terminal outcomes the instance registry can report. */
|
|
5926
|
+
function agentStatusText(status) {
|
|
5927
|
+
switch (status) {
|
|
5928
|
+
case "idle": return t("sidebar.agent_idle");
|
|
5929
|
+
case "working": return t("sidebar.agent_working");
|
|
5930
|
+
case "outputting": return t("sidebar.agent_outputting");
|
|
5931
|
+
case "messaging": return t("sidebar.agent_messaging");
|
|
5932
|
+
case "reworking": return t("sidebar.agent_reworking");
|
|
5933
|
+
case "requesting": return t("sidebar.agent_requesting");
|
|
5934
|
+
case "completed": return t("taskbrowser.agents_completed");
|
|
5935
|
+
case "failed": return t("taskbrowser.agents_failed");
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
function agentStatusColor(colors, status) {
|
|
5939
|
+
switch (status) {
|
|
5940
|
+
case "working":
|
|
5941
|
+
case "outputting": return colors.success;
|
|
5942
|
+
case "messaging": return colors.accent;
|
|
5943
|
+
case "reworking":
|
|
5944
|
+
case "requesting": return colors.warning;
|
|
5945
|
+
case "completed": return colors.textMuted;
|
|
5946
|
+
case "failed": return colors.error;
|
|
5947
|
+
case "idle": return colors.textDim;
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
/** "Who triggered this agent" line. Each kind names its own evidence, and a
|
|
5951
|
+
* missing clue is voiced as unknown instead of guessed. */
|
|
5952
|
+
function formatAgentSource(source) {
|
|
5953
|
+
switch (source.kind) {
|
|
5954
|
+
case "tool":
|
|
5955
|
+
if (source.name === void 0) return t("taskbrowser.agent_source_unknown");
|
|
5956
|
+
return source.description === void 0 ? source.name : `${source.name} · ${source.description}`;
|
|
5957
|
+
case "agent": {
|
|
5958
|
+
const via = source.name === void 0 ? t("taskbrowser.agent_source_unknown") : t("taskbrowser.agent_source_via", { name: source.name });
|
|
5959
|
+
return source.description === void 0 ? via : `${via} · ${source.description}`;
|
|
5960
|
+
}
|
|
5961
|
+
case "rlm": {
|
|
5962
|
+
const rlm = t("taskbrowser.agent_source_rlm");
|
|
5963
|
+
return source.name === void 0 ? rlm : `${rlm} · ${t("taskbrowser.agent_source_via", { name: source.name })}`;
|
|
5964
|
+
}
|
|
5965
|
+
case "main": return t("taskbrowser.agent_source_main");
|
|
5966
|
+
case "unknown": return t("taskbrowser.agent_source_unknown");
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
/** Full spawn chain, root first: `main → researcher → coder`, `…` when the
|
|
5970
|
+
* walk stopped early (missing link / cycle / depth cap). */
|
|
5971
|
+
function formatAgentChain(agent) {
|
|
5972
|
+
const parents = [...agent.ancestors].toReversed();
|
|
5973
|
+
return `${[
|
|
5974
|
+
t("taskbrowser.agent_main"),
|
|
5975
|
+
...parents,
|
|
5976
|
+
agent.type
|
|
5977
|
+
].join(" → ")}${agent.chainTruncated ? " → …" : ""}`;
|
|
5978
|
+
}
|
|
5541
5979
|
function formatRelativeTime$2(ts) {
|
|
5542
5980
|
if (ts === null || ts === void 0 || !Number.isFinite(ts) || ts <= 0) return "";
|
|
5543
5981
|
const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1e3);
|
|
@@ -5565,8 +6003,22 @@ function fitExactly$1(line, width) {
|
|
|
5565
6003
|
return padToWidth$1(s, width);
|
|
5566
6004
|
}
|
|
5567
6005
|
function visibleTasks(tasks, filter) {
|
|
5568
|
-
if (filter === "
|
|
5569
|
-
return tasks
|
|
6006
|
+
if (filter === "active") return tasks.filter((t) => !isTerminal(t.status));
|
|
6007
|
+
return [...tasks];
|
|
6008
|
+
}
|
|
6009
|
+
/**
|
|
6010
|
+
* Keep `selectedIndex` inside the window `scrollOffset … scrollOffset+rows-1`,
|
|
6011
|
+
* clamped to the list bounds. Shared by the task list and the agent list.
|
|
6012
|
+
*/
|
|
6013
|
+
function clampListScroll(selectedIndex, scrollOffset, total, visibleRows) {
|
|
6014
|
+
if (visibleRows <= 0) return 0;
|
|
6015
|
+
let next = scrollOffset;
|
|
6016
|
+
if (selectedIndex < next) next = selectedIndex;
|
|
6017
|
+
else if (selectedIndex >= next + visibleRows) next = selectedIndex - visibleRows + 1;
|
|
6018
|
+
const maxScroll = Math.max(0, total - visibleRows);
|
|
6019
|
+
if (next < 0) next = 0;
|
|
6020
|
+
if (next > maxScroll) next = maxScroll;
|
|
6021
|
+
return next;
|
|
5570
6022
|
}
|
|
5571
6023
|
function compareTasks(a, b) {
|
|
5572
6024
|
const aTerminal = isTerminal(a.status);
|
|
@@ -5606,6 +6058,12 @@ var TasksBrowserApp = class extends Container {
|
|
|
5606
6058
|
sortedVisible;
|
|
5607
6059
|
selectedIndex = 0;
|
|
5608
6060
|
listScroll = 0;
|
|
6061
|
+
/** Agents view selection — component-local: agent rows are read-only, so
|
|
6062
|
+
* nothing needs to travel back to the controller. `selectedAgentKey` is the
|
|
6063
|
+
* anchor: rows re-sort as statuses change, the index must not decide. */
|
|
6064
|
+
agentIndex = 0;
|
|
6065
|
+
agentScroll = 0;
|
|
6066
|
+
selectedAgentKey = void 0;
|
|
5609
6067
|
pendingStopTaskId = void 0;
|
|
5610
6068
|
pendingStopTimer = void 0;
|
|
5611
6069
|
constructor(props, terminal) {
|
|
@@ -5619,6 +6077,7 @@ var TasksBrowserApp = class extends Container {
|
|
|
5619
6077
|
this.props = next;
|
|
5620
6078
|
this.sortedVisible = visibleTasks(next.tasks, next.filter).toSorted(compareTasks);
|
|
5621
6079
|
this.syncSelectionFromProps();
|
|
6080
|
+
this.syncAgentSelectionFromProps();
|
|
5622
6081
|
if (this.pendingStopTaskId !== void 0) {
|
|
5623
6082
|
const task = next.tasks.find((t) => t.taskId === this.pendingStopTaskId);
|
|
5624
6083
|
if (task === void 0 || isTerminal(task.status)) this.clearPendingStop();
|
|
@@ -5640,6 +6099,34 @@ var TasksBrowserApp = class extends Container {
|
|
|
5640
6099
|
}
|
|
5641
6100
|
if (this.selectedIndex >= this.sortedVisible.length) this.selectedIndex = this.sortedVisible.length - 1;
|
|
5642
6101
|
}
|
|
6102
|
+
/** Keep the agents selection pinned to the same slot type across refreshes
|
|
6103
|
+
* (rows re-sort as statuses change); fall back to clamping. */
|
|
6104
|
+
syncAgentSelectionFromProps() {
|
|
6105
|
+
const agents = this.props.agents;
|
|
6106
|
+
if (agents.length === 0) {
|
|
6107
|
+
this.agentIndex = 0;
|
|
6108
|
+
this.agentScroll = 0;
|
|
6109
|
+
this.selectedAgentKey = void 0;
|
|
6110
|
+
return;
|
|
6111
|
+
}
|
|
6112
|
+
if (this.selectedAgentKey !== void 0) {
|
|
6113
|
+
const byKey = agents.findIndex((agent) => agent.key === this.selectedAgentKey);
|
|
6114
|
+
if (byKey !== -1) {
|
|
6115
|
+
this.agentIndex = byKey;
|
|
6116
|
+
return;
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
if (this.agentIndex >= agents.length) this.agentIndex = agents.length - 1;
|
|
6120
|
+
this.selectedAgentKey = agents[this.agentIndex]?.key;
|
|
6121
|
+
}
|
|
6122
|
+
/** Move the agents cursor (rows are read-only: no controller round-trip). */
|
|
6123
|
+
selectAgent(index) {
|
|
6124
|
+
const agents = this.props.agents;
|
|
6125
|
+
if (agents.length === 0) return;
|
|
6126
|
+
this.agentIndex = Math.min(agents.length - 1, Math.max(0, index));
|
|
6127
|
+
this.selectedAgentKey = agents[this.agentIndex]?.key;
|
|
6128
|
+
this.invalidate();
|
|
6129
|
+
}
|
|
5643
6130
|
clearPendingStop() {
|
|
5644
6131
|
this.pendingStopTaskId = void 0;
|
|
5645
6132
|
if (this.pendingStopTimer !== void 0) {
|
|
@@ -5647,12 +6134,21 @@ var TasksBrowserApp = class extends Container {
|
|
|
5647
6134
|
this.pendingStopTimer = void 0;
|
|
5648
6135
|
}
|
|
5649
6136
|
}
|
|
6137
|
+
/**
|
|
6138
|
+
* Component-local teardown, called by the controller's close() before the
|
|
6139
|
+
* component is swapped out: the pending-stop confirm timer must not
|
|
6140
|
+
* outlive the browser.
|
|
6141
|
+
*/
|
|
6142
|
+
close() {
|
|
6143
|
+
this.clearPendingStop();
|
|
6144
|
+
}
|
|
5650
6145
|
emitSelect() {
|
|
5651
6146
|
const task = this.sortedVisible[this.selectedIndex];
|
|
5652
6147
|
if (task) this.props.onSelect(task.taskId);
|
|
5653
6148
|
}
|
|
5654
6149
|
handleInput(data) {
|
|
5655
6150
|
const k = printableChar(data);
|
|
6151
|
+
const agentsMode = this.props.filter === "agents";
|
|
5656
6152
|
if (this.pendingStopTaskId !== void 0) {
|
|
5657
6153
|
if (k === "y" || k === "Y") {
|
|
5658
6154
|
const taskId = this.pendingStopTaskId;
|
|
@@ -5670,6 +6166,10 @@ var TasksBrowserApp = class extends Container {
|
|
|
5670
6166
|
return;
|
|
5671
6167
|
}
|
|
5672
6168
|
if (matchesKey(data, Key.up) || k === "k") {
|
|
6169
|
+
if (agentsMode) {
|
|
6170
|
+
this.selectAgent(this.agentIndex - 1);
|
|
6171
|
+
return;
|
|
6172
|
+
}
|
|
5673
6173
|
if (this.sortedVisible.length === 0) return;
|
|
5674
6174
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
5675
6175
|
this.emitSelect();
|
|
@@ -5677,6 +6177,10 @@ var TasksBrowserApp = class extends Container {
|
|
|
5677
6177
|
return;
|
|
5678
6178
|
}
|
|
5679
6179
|
if (matchesKey(data, Key.down) || k === "j") {
|
|
6180
|
+
if (agentsMode) {
|
|
6181
|
+
this.selectAgent(this.agentIndex + 1);
|
|
6182
|
+
return;
|
|
6183
|
+
}
|
|
5680
6184
|
if (this.sortedVisible.length === 0) return;
|
|
5681
6185
|
this.selectedIndex = Math.min(this.sortedVisible.length - 1, this.selectedIndex + 1);
|
|
5682
6186
|
this.emitSelect();
|
|
@@ -5691,6 +6195,7 @@ var TasksBrowserApp = class extends Container {
|
|
|
5691
6195
|
this.props.onRefresh();
|
|
5692
6196
|
return;
|
|
5693
6197
|
}
|
|
6198
|
+
if (agentsMode) return;
|
|
5694
6199
|
if (k === "s" || k === "S") {
|
|
5695
6200
|
const task = this.sortedVisible[this.selectedIndex];
|
|
5696
6201
|
if (task === void 0) return;
|
|
@@ -5734,15 +6239,26 @@ var TasksBrowserApp = class extends Container {
|
|
|
5734
6239
|
renderHeader(width) {
|
|
5735
6240
|
const colors = this.props.colors;
|
|
5736
6241
|
const title = chalk.hex(colors.primary).bold(" TASK BROWSER ");
|
|
5737
|
-
const
|
|
5738
|
-
const
|
|
5739
|
-
const
|
|
5740
|
-
if (
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
6242
|
+
const filterLabel = this.props.filter === "all" ? "ALL" : this.props.filter === "active" ? "ACTIVE" : "AGENTS";
|
|
6243
|
+
const filterText = chalk.hex(colors.textMuted)(` filter=${filterLabel} `);
|
|
6244
|
+
const segments = [];
|
|
6245
|
+
if (this.props.filter === "agents") {
|
|
6246
|
+
const live = this.props.agents.filter((agent) => agent.live).length;
|
|
6247
|
+
const ended = this.props.agents.length - live;
|
|
6248
|
+
if (live > 0) segments.push(chalk.hex(colors.success)(` ${String(live)} ${t("taskbrowser.agents_live")} `));
|
|
6249
|
+
if (ended > 0) segments.push(chalk.hex(colors.textDim)(` ${String(ended)} ${t("taskbrowser.agents_ended")} `));
|
|
6250
|
+
if (this.props.agents.length === 0) segments.push(chalk.hex(colors.textMuted)(` 0 ${t("taskbrowser.agents")} `));
|
|
6251
|
+
} else {
|
|
6252
|
+
const counts = countByStatus(this.props.tasks);
|
|
6253
|
+
if (counts.running > 0) segments.push(chalk.hex(colors.success)(` ${String(counts.running)} ${t("taskbrowser.running")} `));
|
|
6254
|
+
if (counts.awaiting > 0) segments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} ${t("taskbrowser.awaiting")} `));
|
|
6255
|
+
if (counts.completed > 0) segments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} ${t("taskbrowser.completed")} `));
|
|
6256
|
+
if (counts.terminalFailed > 0) segments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} ${t("taskbrowser.interrupted")} `));
|
|
6257
|
+
segments.push(chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} ${t("taskbrowser.total")} `));
|
|
6258
|
+
const liveAgents = this.props.agents.filter((agent) => agent.live).length;
|
|
6259
|
+
if (liveAgents > 0) segments.push(chalk.hex(colors.success)(` ${String(liveAgents)} ${t("taskbrowser.agents")} `));
|
|
6260
|
+
}
|
|
6261
|
+
return fitExactly$1(title + filterText + segments.join(""), width);
|
|
5746
6262
|
}
|
|
5747
6263
|
renderFooter(width) {
|
|
5748
6264
|
const colors = this.props.colors;
|
|
@@ -5752,14 +6268,19 @@ var TasksBrowserApp = class extends Container {
|
|
|
5752
6268
|
const warn = (text) => chalk.hex(colors.warning).bold(text);
|
|
5753
6269
|
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
6270
|
}
|
|
5755
|
-
const left = [
|
|
6271
|
+
const left = (this.props.filter === "agents" ? [
|
|
6272
|
+
` ${key("↑↓")} ${dim(t("taskbrowser.select"))}`,
|
|
6273
|
+
`${key("Tab")} ${dim(t("taskbrowser.filter"))}`,
|
|
6274
|
+
`${key("R")} ${dim(t("taskbrowser.refresh"))}`,
|
|
6275
|
+
`${key("Q/Esc")} ${dim(t("taskbrowser.exit"))} `
|
|
6276
|
+
] : [
|
|
5756
6277
|
` ${key("↑↓")} ${dim(t("taskbrowser.select"))}`,
|
|
5757
6278
|
`${key("Enter/O")} ${dim(t("taskbrowser.output"))}`,
|
|
5758
6279
|
`${key("S")} ${dim(t("taskbrowser.stop_action"))}`,
|
|
5759
6280
|
`${key("R")} ${dim(t("taskbrowser.refresh"))}`,
|
|
5760
6281
|
`${key("Tab")} ${dim(t("taskbrowser.filter"))}`,
|
|
5761
6282
|
`${key("Q/Esc")} ${dim(t("taskbrowser.exit"))} `
|
|
5762
|
-
].join(" ");
|
|
6283
|
+
]).join(" ");
|
|
5763
6284
|
const flash = this.props.flashMessage;
|
|
5764
6285
|
if (flash !== void 0 && flash.length > 0) {
|
|
5765
6286
|
const flashStyled = chalk.hex(colors.warning)(` ${flash} `);
|
|
@@ -5798,6 +6319,7 @@ var TasksBrowserApp = class extends Container {
|
|
|
5798
6319
|
return lines;
|
|
5799
6320
|
}
|
|
5800
6321
|
renderListFrame(width, height) {
|
|
6322
|
+
if (this.props.filter === "agents") return this.renderAgentsFrame(width, height);
|
|
5801
6323
|
const title = `Tasks [${this.props.filter}]`;
|
|
5802
6324
|
const innerHeight = Math.max(0, height - 2);
|
|
5803
6325
|
if (this.sortedVisible.length === 0) {
|
|
@@ -5818,6 +6340,43 @@ var TasksBrowserApp = class extends Container {
|
|
|
5818
6340
|
while (lines.length < innerHeight) lines.push("");
|
|
5819
6341
|
return this.renderFrame(title, lines, width, height);
|
|
5820
6342
|
}
|
|
6343
|
+
/** Left pane, `agents` filter: one row per subagent type with history (live
|
|
6344
|
+
* ones first), including types whose slot the cap pushed out and that only
|
|
6345
|
+
* the instance registry knows; read-only — the detail pane carries the full
|
|
6346
|
+
* provenance. */
|
|
6347
|
+
renderAgentsFrame(width, height) {
|
|
6348
|
+
const colors = this.props.colors;
|
|
6349
|
+
const agents = this.props.agents;
|
|
6350
|
+
const innerHeight = Math.max(0, height - 2);
|
|
6351
|
+
const title = `${t("taskbrowser.agents_title")} [${String(agents.length)}]`;
|
|
6352
|
+
if (agents.length === 0) {
|
|
6353
|
+
const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_empty"))];
|
|
6354
|
+
while (lines.length < innerHeight) lines.push("");
|
|
6355
|
+
return this.renderFrame(title, lines, width, height);
|
|
6356
|
+
}
|
|
6357
|
+
this.agentScroll = clampListScroll(this.agentIndex, this.agentScroll, agents.length, innerHeight);
|
|
6358
|
+
const start = this.agentScroll;
|
|
6359
|
+
const window = agents.slice(start, start + innerHeight);
|
|
6360
|
+
const innerWidth = width - 2;
|
|
6361
|
+
const lines = [];
|
|
6362
|
+
for (const [vi, agent] of window.entries()) {
|
|
6363
|
+
const index = start + vi;
|
|
6364
|
+
lines.push(this.renderAgentRow(agent, index === this.agentIndex, innerWidth));
|
|
6365
|
+
}
|
|
6366
|
+
while (lines.length < innerHeight) lines.push("");
|
|
6367
|
+
return this.renderFrame(title, lines, width, height);
|
|
6368
|
+
}
|
|
6369
|
+
renderAgentRow(agent, selected, innerWidth) {
|
|
6370
|
+
const colors = this.props.colors;
|
|
6371
|
+
const pointer = selected ? "> " : " ";
|
|
6372
|
+
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)}`) : ""}`;
|
|
6373
|
+
const budget = innerWidth - visibleWidth(head) - 1;
|
|
6374
|
+
if (budget < 6) return fitExactly$1(head, innerWidth);
|
|
6375
|
+
const hint = singleLine$2(agent.detail ?? agent.description ?? "");
|
|
6376
|
+
if (hint.length === 0) return fitExactly$1(head, innerWidth);
|
|
6377
|
+
const trimmed = truncateToWidth(hint, budget, ELLIPSIS$3);
|
|
6378
|
+
return fitExactly$1(`${head} ${chalk.hex(colors.textDim)(trimmed)}`, innerWidth);
|
|
6379
|
+
}
|
|
5821
6380
|
renderListRow(task, selected, innerWidth) {
|
|
5822
6381
|
const colors = this.props.colors;
|
|
5823
6382
|
const pointer = selected ? "> " : " ";
|
|
@@ -5834,15 +6393,7 @@ var TasksBrowserApp = class extends Container {
|
|
|
5834
6393
|
return fitExactly$1(`${prefix} ${chalk.hex(colors.text)(desc)}`, innerWidth);
|
|
5835
6394
|
}
|
|
5836
6395
|
adjustScroll(visibleRows) {
|
|
5837
|
-
|
|
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;
|
|
6396
|
+
this.listScroll = clampListScroll(this.selectedIndex, this.listScroll, this.sortedVisible.length, visibleRows);
|
|
5846
6397
|
}
|
|
5847
6398
|
renderRightStack(width, height) {
|
|
5848
6399
|
const detailHeight = Math.max(8, Math.min(Math.floor(height * .4), height - 5));
|
|
@@ -5850,6 +6401,7 @@ var TasksBrowserApp = class extends Container {
|
|
|
5850
6401
|
return [...this.renderDetailFrame(width, detailHeight), ...this.renderPreviewFrame(width, previewHeight)];
|
|
5851
6402
|
}
|
|
5852
6403
|
renderDetailFrame(width, height) {
|
|
6404
|
+
if (this.props.filter === "agents") return this.renderAgentDetailFrame(width, height);
|
|
5853
6405
|
const colors = this.props.colors;
|
|
5854
6406
|
const innerHeight = Math.max(0, height - 2);
|
|
5855
6407
|
const task = this.sortedVisible[this.selectedIndex];
|
|
@@ -5876,9 +6428,43 @@ var TasksBrowserApp = class extends Container {
|
|
|
5876
6428
|
while (lines.length < innerHeight) lines.push("");
|
|
5877
6429
|
return this.renderFrame(t("taskbrowser.detail"), lines, width, height);
|
|
5878
6430
|
}
|
|
6431
|
+
/** Right-top pane, `agents` filter: provenance of the selected agent —
|
|
6432
|
+
* type, live status, spawn description, trigger source, parent chain,
|
|
6433
|
+
* latest activity and the current instance id. */
|
|
6434
|
+
renderAgentDetailFrame(width, height) {
|
|
6435
|
+
const colors = this.props.colors;
|
|
6436
|
+
const innerHeight = Math.max(0, height - 2);
|
|
6437
|
+
const agent = this.props.agents[this.agentIndex];
|
|
6438
|
+
if (agent === void 0) {
|
|
6439
|
+
const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_empty"))];
|
|
6440
|
+
while (lines.length < innerHeight) lines.push("");
|
|
6441
|
+
return this.renderFrame(t("taskbrowser.agent_detail"), lines, width, height);
|
|
6442
|
+
}
|
|
6443
|
+
const label = (text) => chalk.hex(colors.textMuted)(text.padEnd(14));
|
|
6444
|
+
const value = (text) => chalk.hex(colors.text)(text);
|
|
6445
|
+
const countSuffix = agent.count > 1 ? chalk.hex(colors.textMuted)(` ×${String(agent.count)}`) : "";
|
|
6446
|
+
const lines = [
|
|
6447
|
+
`${label(t("taskbrowser.agent_type"))}${value(agent.type)}`,
|
|
6448
|
+
`${label(t("taskbrowser.status"))}${chalk.hex(agentStatusColor(colors, agent.status))(agentStatusText(agent.status))}${countSuffix}`,
|
|
6449
|
+
`${label(t("taskbrowser.agent_source"))}${value(singleLine$2(formatAgentSource(agent.source)))}`,
|
|
6450
|
+
`${label(t("taskbrowser.agent_chain"))}${chalk.hex(colors.textMuted)(formatAgentChain(agent))}`
|
|
6451
|
+
];
|
|
6452
|
+
if (agent.description !== void 0 && agent.description.length > 0) lines.push(`${label(t("taskbrowser.description"))}${value(singleLine$2(agent.description))}`);
|
|
6453
|
+
if (agent.detail !== void 0 && agent.detail.length > 0) lines.push(`${label(t("taskbrowser.agent_activity"))}${value(singleLine$2(agent.detail))}`);
|
|
6454
|
+
const when = formatRelativeTime$2(agent.lastActivityAt);
|
|
6455
|
+
if (when.length > 0) lines.push(`${label(t("taskbrowser.time"))}${chalk.hex(colors.textMuted)(when)}`);
|
|
6456
|
+
if (agent.instanceId !== void 0) lines.push(`${label(t("taskbrowser.agent_instance"))}${chalk.hex(colors.textMuted)(agent.instanceId)}`);
|
|
6457
|
+
while (lines.length < innerHeight) lines.push("");
|
|
6458
|
+
return this.renderFrame(t("taskbrowser.agent_detail"), lines, width, height);
|
|
6459
|
+
}
|
|
5879
6460
|
renderPreviewFrame(width, height) {
|
|
5880
6461
|
const colors = this.props.colors;
|
|
5881
6462
|
const innerHeight = Math.max(0, height - 2);
|
|
6463
|
+
if (this.props.filter === "agents") {
|
|
6464
|
+
const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_no_output"))];
|
|
6465
|
+
while (lines.length < innerHeight) lines.push("");
|
|
6466
|
+
return this.renderFrame("Preview Output", lines, width, height);
|
|
6467
|
+
}
|
|
5882
6468
|
if (this.sortedVisible[this.selectedIndex] === void 0) {
|
|
5883
6469
|
const lines = [chalk.hex(colors.textMuted)("No task selected.")];
|
|
5884
6470
|
while (lines.length < innerHeight) lines.push("");
|
|
@@ -5927,6 +6513,7 @@ var TasksBrowserController = class {
|
|
|
5927
6513
|
const selectedTaskId = this.pickInitialSelection(tasks, filter);
|
|
5928
6514
|
const component = new TasksBrowserApp({
|
|
5929
6515
|
tasks,
|
|
6516
|
+
agents: this.snapshotAgentRows(),
|
|
5930
6517
|
filter,
|
|
5931
6518
|
selectedTaskId,
|
|
5932
6519
|
tailOutput: void 0,
|
|
@@ -5964,6 +6551,7 @@ var TasksBrowserController = class {
|
|
|
5964
6551
|
if (browser.viewer !== void 0) this.closeOutputViewer();
|
|
5965
6552
|
if (browser.pollTimer !== void 0) clearInterval(browser.pollTimer);
|
|
5966
6553
|
if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
|
|
6554
|
+
browser.component.close();
|
|
5967
6555
|
state.ui.setLayoutRoot(browser.savedLayoutRoot);
|
|
5968
6556
|
this.host.setTasksBrowser(void 0);
|
|
5969
6557
|
state.ui.setFocus(state.editor);
|
|
@@ -6034,6 +6622,7 @@ var TasksBrowserController = class {
|
|
|
6034
6622
|
if (browser === void 0) return;
|
|
6035
6623
|
browser.component.setProps({
|
|
6036
6624
|
tasks,
|
|
6625
|
+
agents: this.snapshotAgentRows(),
|
|
6037
6626
|
filter: browser.filter,
|
|
6038
6627
|
selectedTaskId: browser.selectedTaskId,
|
|
6039
6628
|
tailOutput: browser.tailOutput,
|
|
@@ -6044,6 +6633,13 @@ var TasksBrowserController = class {
|
|
|
6044
6633
|
});
|
|
6045
6634
|
this.host.state.ui.requestRender();
|
|
6046
6635
|
}
|
|
6636
|
+
/** Agents view rows: the live slot state machine (authoritative status)
|
|
6637
|
+
* enriched with the per-instance provenance registry, plus the archive ring
|
|
6638
|
+
* for agents whose registry record has already been released. Rebuilt on
|
|
6639
|
+
* every refresh/repaint — all three sources are in-memory snapshots. */
|
|
6640
|
+
snapshotAgentRows() {
|
|
6641
|
+
return buildAgentRows(this.host.subagentSlots, this.host.subagentInstances, this.host.recentSubagentInstances);
|
|
6642
|
+
}
|
|
6047
6643
|
buildCallbacks() {
|
|
6048
6644
|
return {
|
|
6049
6645
|
onSelect: (taskId) => {
|
|
@@ -6082,7 +6678,7 @@ var TasksBrowserController = class {
|
|
|
6082
6678
|
handleToggleFilter() {
|
|
6083
6679
|
const browser = this.host.state.tasksBrowser;
|
|
6084
6680
|
if (browser === void 0) return;
|
|
6085
|
-
browser.filter = browser.filter === "all" ? "active" : "all";
|
|
6681
|
+
browser.filter = browser.filter === "all" ? "active" : browser.filter === "active" ? "agents" : "all";
|
|
6086
6682
|
this.repaint();
|
|
6087
6683
|
}
|
|
6088
6684
|
handleRefresh() {
|
|
@@ -6125,6 +6721,7 @@ var TasksBrowserController = class {
|
|
|
6125
6721
|
}
|
|
6126
6722
|
const current = state.tasksBrowser;
|
|
6127
6723
|
if (current === void 0 || current !== browser) return;
|
|
6724
|
+
if (current.viewer !== void 0) return;
|
|
6128
6725
|
const viewer = new TaskOutputViewer({
|
|
6129
6726
|
taskId,
|
|
6130
6727
|
info: this.host.backgroundTasks.get(taskId),
|
|
@@ -6134,7 +6731,7 @@ var TasksBrowserController = class {
|
|
|
6134
6731
|
this.closeOutputViewer();
|
|
6135
6732
|
}
|
|
6136
6733
|
}, state.terminal);
|
|
6137
|
-
const savedBrowserLayout =
|
|
6734
|
+
const savedBrowserLayout = browser.component;
|
|
6138
6735
|
state.ui.setLayoutRoot(viewer);
|
|
6139
6736
|
state.ui.setFocus(viewer);
|
|
6140
6737
|
state.ui.requestRender(true);
|
|
@@ -6351,6 +6948,28 @@ function basenameLike(raw) {
|
|
|
6351
6948
|
return raw.split(/[\\/]/).filter((part) => part.length > 0).at(-1) ?? raw;
|
|
6352
6949
|
}
|
|
6353
6950
|
//#endregion
|
|
6951
|
+
//#region src/tui/utils/collapse-stub.ts
|
|
6952
|
+
/**
|
|
6953
|
+
* The transcript bounds its memory in two places, and both stand in for the
|
|
6954
|
+
* rows they fold away with the same summary line: the shadow entry array
|
|
6955
|
+
* (`transcript-controller`) folds its oldest entries into one stub entry, and
|
|
6956
|
+
* the committed render tree (`committed-transcript`) folds its oldest child
|
|
6957
|
+
* components into one stub row. One definition of the wording keeps the two
|
|
6958
|
+
* folds reading alike — and lets the array fold read its own count back when
|
|
6959
|
+
* it folds again.
|
|
6960
|
+
*/
|
|
6961
|
+
/** Matches {@link formatCollapsedEntries} output, capturing the count. */
|
|
6962
|
+
const COLLAPSE_STUB_PATTERN = /^…\((\d+) earlier entries collapsed\)$/;
|
|
6963
|
+
/** Summary line that stands in for `count` folded-away entries. */
|
|
6964
|
+
function formatCollapsedEntries(count) {
|
|
6965
|
+
return `…(${String(count)} earlier entries collapsed)`;
|
|
6966
|
+
}
|
|
6967
|
+
/** How many entries a summary line stands in for; 0 when `text` is not one. */
|
|
6968
|
+
function parseCollapsedEntries(text) {
|
|
6969
|
+
const digits = COLLAPSE_STUB_PATTERN.exec(text)?.[1];
|
|
6970
|
+
return digits === void 0 ? 0 : Number(digits);
|
|
6971
|
+
}
|
|
6972
|
+
//#endregion
|
|
6354
6973
|
//#region src/tui/components/transcript/committed-transcript.ts
|
|
6355
6974
|
var CommittedMessageComponent = class {
|
|
6356
6975
|
entry;
|
|
@@ -6420,6 +7039,9 @@ var CommittedTranscriptComponent = class extends CachedContainer {
|
|
|
6420
7039
|
header;
|
|
6421
7040
|
colors;
|
|
6422
7041
|
committedCount = 0;
|
|
7042
|
+
/** Rows the retention cap has folded into the summary stub. */
|
|
7043
|
+
collapsedCount = 0;
|
|
7044
|
+
collapseStub;
|
|
6423
7045
|
constructor(colors) {
|
|
6424
7046
|
super();
|
|
6425
7047
|
this.colors = colors;
|
|
@@ -6429,6 +7051,12 @@ var CommittedTranscriptComponent = class extends CachedContainer {
|
|
|
6429
7051
|
getCount() {
|
|
6430
7052
|
return this.committedCount;
|
|
6431
7053
|
}
|
|
7054
|
+
/** Rows the tree folded into its summary stub. They still count toward
|
|
7055
|
+
* {@link getCount()}: folding changes what is on screen, never what was
|
|
7056
|
+
* committed. */
|
|
7057
|
+
getCollapsedCount() {
|
|
7058
|
+
return this.collapsedCount;
|
|
7059
|
+
}
|
|
6432
7060
|
setCount(count) {
|
|
6433
7061
|
this.committedCount = count;
|
|
6434
7062
|
if (count === 0) this.header.setText("");
|
|
@@ -6437,11 +7065,46 @@ var CommittedTranscriptComponent = class extends CachedContainer {
|
|
|
6437
7065
|
}
|
|
6438
7066
|
appendEntry(entry, colors) {
|
|
6439
7067
|
this.addChild(new CommittedMessageComponent(entry, colors));
|
|
7068
|
+
this.enforceRetentionCap();
|
|
7069
|
+
}
|
|
7070
|
+
/**
|
|
7071
|
+
* Holds the tree at its retention point: the entry children it renders —
|
|
7072
|
+
* the summary stub included — never exceed MAX_COMMITTED_ENTRIES. Past that
|
|
7073
|
+
* the oldest rows fold into the stub, the same semantics as the entry-array
|
|
7074
|
+
* cap, so the child list (and the render cache every child carries) stops
|
|
7075
|
+
* growing with the session. The stub takes one slot of the budget, so the
|
|
7076
|
+
* fold lands on stub + (MAX - 1) rows: the cap is the actually retained set.
|
|
7077
|
+
*/
|
|
7078
|
+
enforceRetentionCap() {
|
|
7079
|
+
if (this.children.length - 1 <= 500) return;
|
|
7080
|
+
let stub = this.collapseStub;
|
|
7081
|
+
if (stub === void 0) {
|
|
7082
|
+
stub = new Text("", 0, 0);
|
|
7083
|
+
this.collapseStub = stub;
|
|
7084
|
+
this.children.splice(1, 0, stub);
|
|
7085
|
+
}
|
|
7086
|
+
const fold = this.children.length - 2 - 499;
|
|
7087
|
+
this.children.splice(2, fold);
|
|
7088
|
+
this.collapsedCount += fold;
|
|
7089
|
+
stub.setText(` ${chalk.hex(this.colors.textDim)(formatCollapsedEntries(this.collapsedCount))}`);
|
|
6440
7090
|
this.invalidate();
|
|
6441
7091
|
}
|
|
6442
7092
|
};
|
|
6443
|
-
|
|
6444
|
-
|
|
7093
|
+
/** Id of the aggregate stub that stands in for the folded oldest entries. */
|
|
7094
|
+
const COLLAPSE_STUB_ID = "transcript-collapse-stub";
|
|
7095
|
+
function makeCollapseStub(count) {
|
|
7096
|
+
return {
|
|
7097
|
+
id: COLLAPSE_STUB_ID,
|
|
7098
|
+
kind: "status",
|
|
7099
|
+
renderMode: "plain",
|
|
7100
|
+
content: formatCollapsedEntries(count)
|
|
7101
|
+
};
|
|
7102
|
+
}
|
|
7103
|
+
/** Entries an existing stub already stands in for (0 when it is not one). */
|
|
7104
|
+
function stubCount(entry) {
|
|
7105
|
+
if (entry === void 0 || entry.id !== COLLAPSE_STUB_ID) return 0;
|
|
7106
|
+
return parseCollapsedEntries(entry.content);
|
|
7107
|
+
}
|
|
6445
7108
|
var TranscriptController = class TranscriptController {
|
|
6446
7109
|
host;
|
|
6447
7110
|
welcomeComponent;
|
|
@@ -6451,6 +7114,17 @@ var TranscriptController = class TranscriptController {
|
|
|
6451
7114
|
* ticker of the card that is still claiming the task is running. */
|
|
6452
7115
|
liveNoticesByTrackingId = /* @__PURE__ */ new Map();
|
|
6453
7116
|
pendingComponents = /* @__PURE__ */ new Set();
|
|
7117
|
+
/** Monotonic count of composition changes to `state.transcriptEntries`:
|
|
7118
|
+
* +1 per ingest and +1 per cap fold. Aggregates that memoize over the entry
|
|
7119
|
+
* list key on this — the fold holds the array length at the cap while
|
|
7120
|
+
* replacing the oldest rows, so a length-only key can go stale. */
|
|
7121
|
+
ingestCount = 0;
|
|
7122
|
+
/** Contributions of the rows the entry cap has already folded out of the
|
|
7123
|
+
* array. Kept separate from the rows still held, so aggregates that must
|
|
7124
|
+
* stay monotonic over a long session (the sidebar's session stats) can add
|
|
7125
|
+
* them back — see {@link getFoldedEntryCounts}. */
|
|
7126
|
+
foldedTurns = 0;
|
|
7127
|
+
foldedToolCalls = 0;
|
|
6454
7128
|
/** Max live transcript children before the oldest are folded into the
|
|
6455
7129
|
* committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
|
|
6456
7130
|
* (mirrors a commit-fold approach for bounding ultra-long sessions). */
|
|
@@ -6539,6 +7213,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6539
7213
|
this.dropLiveComponent(component);
|
|
6540
7214
|
}
|
|
6541
7215
|
this.committedComponent.setCount(this.committedComponent.getCount() + toCommit.length);
|
|
7216
|
+
state.ui.resetTextSelection();
|
|
6542
7217
|
if (process.env["SCREAM_CODE_DEBUG"] === "1") this.host.showStatus(`[debug] committed=${this.committedComponent.getCount()} live=${this.getLiveCount()}`);
|
|
6543
7218
|
container.invalidate();
|
|
6544
7219
|
state.ui.requestRender();
|
|
@@ -6583,12 +7258,55 @@ var TranscriptController = class TranscriptController {
|
|
|
6583
7258
|
default: return null;
|
|
6584
7259
|
}
|
|
6585
7260
|
}
|
|
7261
|
+
/**
|
|
7262
|
+
* The transcript's single bounded ingress: every entry reaches
|
|
7263
|
+
* `state.transcriptEntries` through here — `appendEntry` for rows that mount
|
|
7264
|
+
* their own component, and `ScreamTUI.pushTranscriptEntry` for the live
|
|
7265
|
+
* streaming rows that already own one. One place applies the UI preview
|
|
7266
|
+
* bound (`boundEntryForUi`) and the entry-count cap, so no producer can grow
|
|
7267
|
+
* the array without bound, whichever path it comes in on.
|
|
7268
|
+
*
|
|
7269
|
+
* Returns the entry as stored: a bounded copy when the bound rewrote it,
|
|
7270
|
+
* otherwise the very entry handed in — identity matters to callers that keep
|
|
7271
|
+
* mutating a live draft (assistant text, thinking) after pushing it.
|
|
7272
|
+
*/
|
|
7273
|
+
ingestEntry(entry) {
|
|
7274
|
+
const boundedEntry = boundEntryForUi(entry);
|
|
7275
|
+
this.ingestCount += 1;
|
|
7276
|
+
this.host.state.transcriptEntries.push(boundedEntry);
|
|
7277
|
+
this.enforceEntryCap();
|
|
7278
|
+
return boundedEntry;
|
|
7279
|
+
}
|
|
7280
|
+
/**
|
|
7281
|
+
* Monotonic count of transcript composition changes: one per ingest, plus
|
|
7282
|
+
* one per cap fold. Consumers that memoize aggregates over
|
|
7283
|
+
* `state.transcriptEntries` (the sidebar session stats) key on this instead
|
|
7284
|
+
* of `length` alone, because a fold lands the array back on the same length
|
|
7285
|
+
* while replacing its oldest rows.
|
|
7286
|
+
*/
|
|
7287
|
+
getIngestCount() {
|
|
7288
|
+
return this.ingestCount;
|
|
7289
|
+
}
|
|
7290
|
+
/**
|
|
7291
|
+
* What the entry cap has already folded out of `state.transcriptEntries`:
|
|
7292
|
+
* user turns and real tool calls (compression markers ride kind 'tool_call'
|
|
7293
|
+
* but are not invocations — same rule as the retained-row scan). Aggregates
|
|
7294
|
+
* that must be monotonic over a long session — the sidebar's session stats —
|
|
7295
|
+
* add these to the rows the array still holds, because the array stops
|
|
7296
|
+
* holding the oldest ones at the cap.
|
|
7297
|
+
*/
|
|
7298
|
+
getFoldedEntryCounts() {
|
|
7299
|
+
return {
|
|
7300
|
+
turns: this.foldedTurns,
|
|
7301
|
+
toolCalls: this.foldedToolCalls
|
|
7302
|
+
};
|
|
7303
|
+
}
|
|
6586
7304
|
appendEntry(entry) {
|
|
6587
|
-
this.
|
|
6588
|
-
const component = this.createComponent(
|
|
7305
|
+
const boundedEntry = this.ingestEntry(entry);
|
|
7306
|
+
const component = this.createComponent(boundedEntry);
|
|
6589
7307
|
if (component) {
|
|
6590
|
-
this.liveComponentToEntry.set(component,
|
|
6591
|
-
const status =
|
|
7308
|
+
this.liveComponentToEntry.set(component, boundedEntry);
|
|
7309
|
+
const status = boundedEntry.backgroundAgentStatus;
|
|
6592
7310
|
if (status !== void 0 && component instanceof BackgroundAgentStatusComponent) {
|
|
6593
7311
|
const trackingId = status.trackingId;
|
|
6594
7312
|
if (trackingId !== void 0) if (status.phase === "started") this.liveNoticesByTrackingId.set(trackingId, component);
|
|
@@ -6602,6 +7320,24 @@ var TranscriptController = class TranscriptController {
|
|
|
6602
7320
|
}
|
|
6603
7321
|
return component ?? null;
|
|
6604
7322
|
}
|
|
7323
|
+
/**
|
|
7324
|
+
* Keeps `state.transcriptEntries` within {@link MAX_TRANSCRIPT_ENTRIES} by
|
|
7325
|
+
* folding the oldest rows into one aggregate stub. A stub already at the head
|
|
7326
|
+
* keeps that slot and accumulates the count, so the array stays bounded no
|
|
7327
|
+
* matter how long the session runs.
|
|
7328
|
+
*/
|
|
7329
|
+
enforceEntryCap() {
|
|
7330
|
+
const entries = this.host.state.transcriptEntries;
|
|
7331
|
+
if (entries.length <= 500) return;
|
|
7332
|
+
const head = entries[0];
|
|
7333
|
+
const isStub = head !== void 0 && head.id === COLLAPSE_STUB_ID;
|
|
7334
|
+
const dropped = entries.splice(0, entries.length - 500 + 1);
|
|
7335
|
+
for (const entry of dropped) if (entry.kind === "user") this.foldedTurns += 1;
|
|
7336
|
+
else if (entry.kind === "tool_call" && entry.compactionData === void 0) this.foldedToolCalls += 1;
|
|
7337
|
+
const folded = dropped.length - (isStub ? 1 : 0);
|
|
7338
|
+
entries.unshift(makeCollapseStub(stubCount(head) + folded));
|
|
7339
|
+
this.ingestCount += 1;
|
|
7340
|
+
}
|
|
6605
7341
|
/** Append a suffix to the given turn's final assistant message last line.
|
|
6606
7342
|
* Used to place the turn elapsed marker flush against the last character
|
|
6607
7343
|
* of the assistant's reply. Turn-scoped so a tool-only turn never stamps
|
|
@@ -6652,10 +7388,13 @@ var TranscriptController = class TranscriptController {
|
|
|
6652
7388
|
}
|
|
6653
7389
|
clearAndRedraw() {
|
|
6654
7390
|
const { state, streamingUI, imageStore } = this.host;
|
|
7391
|
+
state.ui.resetTextSelection();
|
|
6655
7392
|
streamingUI.discardPending();
|
|
6656
7393
|
streamingUI.endActivityGroup();
|
|
6657
7394
|
streamingUI.flushPendingApprovals();
|
|
6658
7395
|
state.transcriptEntries = [];
|
|
7396
|
+
this.foldedTurns = 0;
|
|
7397
|
+
this.foldedToolCalls = 0;
|
|
6659
7398
|
streamingUI.disposeActiveCompactionBlock();
|
|
6660
7399
|
streamingUI.resetLiveText();
|
|
6661
7400
|
streamingUI.resetToolUi();
|
|
@@ -6714,12 +7453,17 @@ var TranscriptController = class TranscriptController {
|
|
|
6714
7453
|
const { state } = this.host;
|
|
6715
7454
|
const next = !state.toolOutputExpanded;
|
|
6716
7455
|
const children = state.transcriptContainer.children;
|
|
7456
|
+
let resized = false;
|
|
6717
7457
|
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
6718
7458
|
const child = children[i];
|
|
6719
7459
|
if (child === void 0) continue;
|
|
6720
7460
|
if (child instanceof UserMessageComponent || child instanceof SkillActivationComponent || child instanceof CronMessageComponent || child instanceof CommittedTranscriptComponent) break;
|
|
6721
|
-
if (isExpandable(child))
|
|
7461
|
+
if (isExpandable(child)) {
|
|
7462
|
+
child.setExpanded(next);
|
|
7463
|
+
resized = true;
|
|
7464
|
+
}
|
|
6722
7465
|
}
|
|
7466
|
+
if (resized) state.ui.resetTextSelection();
|
|
6723
7467
|
state.toolOutputExpanded = next;
|
|
6724
7468
|
state.ui.requestRender();
|
|
6725
7469
|
}
|
|
@@ -6729,6 +7473,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6729
7473
|
let toggled = false;
|
|
6730
7474
|
for (const child of state.transcriptContainer.children) if (isPlanExpandable(child) && child.setPlanExpanded(next)) toggled = true;
|
|
6731
7475
|
if (!toggled) return false;
|
|
7476
|
+
state.ui.resetTextSelection();
|
|
6732
7477
|
state.planExpanded = next;
|
|
6733
7478
|
state.ui.requestRender();
|
|
6734
7479
|
return true;
|
|
@@ -7969,7 +8714,8 @@ var LifecycleController = class LifecycleController {
|
|
|
7969
8714
|
hubProbe = createHubProbe();
|
|
7970
8715
|
sidebarGitCacheWorkDir = "";
|
|
7971
8716
|
/** Memo for the session-stats aggregation: recomputed only when the
|
|
7972
|
-
* transcript length, compaction count or usage object
|
|
8717
|
+
* transcript's ingest counter/length, compaction count or usage object
|
|
8718
|
+
* changes. */
|
|
7973
8719
|
sidebarStatsMemo;
|
|
7974
8720
|
static MEMORY_IDLE_MS = 900 * 1e3;
|
|
7975
8721
|
static MEMORY_COUNTDOWN_MS = 15 * 1e3;
|
|
@@ -8236,16 +8982,20 @@ var LifecycleController = class LifecycleController {
|
|
|
8236
8982
|
}
|
|
8237
8983
|
readSidebarSessionStats(appState) {
|
|
8238
8984
|
const entries = this.host.state.transcriptEntries;
|
|
8985
|
+
const ingestCount = this.host.transcriptController.getIngestCount();
|
|
8239
8986
|
const usage = appState.sessionUsage;
|
|
8240
8987
|
const subagentUsage = appState.subagentUsage;
|
|
8241
8988
|
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;
|
|
8989
|
+
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
8990
|
let turns = 0;
|
|
8244
8991
|
let toolCalls = 0;
|
|
8245
8992
|
for (const entry of entries) {
|
|
8246
8993
|
if (entry.kind === "user") turns += 1;
|
|
8247
8994
|
if (entry.kind === "tool_call" && entry.compactionData === void 0) toolCalls += 1;
|
|
8248
8995
|
}
|
|
8996
|
+
const foldedAway = this.host.transcriptController.getFoldedEntryCounts();
|
|
8997
|
+
turns += foldedAway.turns;
|
|
8998
|
+
toolCalls += foldedAway.toolCalls;
|
|
8249
8999
|
let subagentOutput = 0;
|
|
8250
9000
|
for (const sub of Object.values(subagentUsage)) subagentOutput += sub.output;
|
|
8251
9001
|
const stats = {
|
|
@@ -8259,6 +9009,7 @@ var LifecycleController = class LifecycleController {
|
|
|
8259
9009
|
tokensOutput: usage.output + subagentOutput
|
|
8260
9010
|
};
|
|
8261
9011
|
this.sidebarStatsMemo = {
|
|
9012
|
+
ingestCount,
|
|
8262
9013
|
length: entries.length,
|
|
8263
9014
|
compactions: appState.autoCompactionCount,
|
|
8264
9015
|
usage,
|
|
@@ -10194,6 +10945,7 @@ var CustomEditor = class extends Editor {
|
|
|
10194
10945
|
thinkingLevel: this.thinkingLevel,
|
|
10195
10946
|
toolPriority: this.toolPriority,
|
|
10196
10947
|
paint: this.borderColor ?? ((s) => s),
|
|
10948
|
+
modeHex: getPermissionModeColor(this.permissionMode, this.colors),
|
|
10197
10949
|
borderHex: this.borderHex
|
|
10198
10950
|
});
|
|
10199
10951
|
return lines;
|
|
@@ -10344,6 +11096,15 @@ function injectPromptSymbol(line) {
|
|
|
10344
11096
|
}
|
|
10345
11097
|
const THINK_LABEL_MIN_WIDTH = 14;
|
|
10346
11098
|
const MODE_BADGE_MIN_WIDTH = 10;
|
|
11099
|
+
function getPermissionModeColor(mode, colors) {
|
|
11100
|
+
return {
|
|
11101
|
+
manual: colors.permissionManual,
|
|
11102
|
+
yolo: colors.permissionYolo,
|
|
11103
|
+
auto: colors.permissionAuto,
|
|
11104
|
+
ask: colors.permissionAsk,
|
|
11105
|
+
bot: colors.permissionBot
|
|
11106
|
+
}[mode];
|
|
11107
|
+
}
|
|
10347
11108
|
function isLightBg(hex) {
|
|
10348
11109
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
10349
11110
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
@@ -10374,7 +11135,7 @@ function injectBorderBadges(lines, width, opts) {
|
|
|
10374
11135
|
let right = paint("─");
|
|
10375
11136
|
if (width >= MODE_BADGE_MIN_WIDTH) {
|
|
10376
11137
|
const badgeText = ` ${opts.mode} `;
|
|
10377
|
-
left = paint("──") +
|
|
11138
|
+
left = paint("──") + chalk.hex(opts.modeHex).bold(badgeText);
|
|
10378
11139
|
}
|
|
10379
11140
|
if (opts.thinking && width >= THINK_LABEL_MIN_WIDTH) right = makeBadge(opts.thinkingLevel !== "off" ? ` Think ${opts.thinkingLevel} ` : " Think ", opts.borderHex) + right;
|
|
10380
11141
|
if (opts.toolPriority === "skill" || opts.toolPriority === "mcp") right = makeBadge(` First ${opts.toolPriority} `, opts.borderHex) + paint("─") + right;
|
|
@@ -11274,6 +12035,7 @@ var SessionManager$1 = class {
|
|
|
11274
12035
|
planMode: status.planMode ? status.planStrategy === "fusion" ? "fusionplan" : "plan" : "off",
|
|
11275
12036
|
wolfpackMode: status.wolfpackMode,
|
|
11276
12037
|
rlmEnabled: status.rlmEnabled,
|
|
12038
|
+
rlmMaxDepth: status.rlmMaxDepth ?? null,
|
|
11277
12039
|
contextTokens: status.contextTokens,
|
|
11278
12040
|
maxContextTokens: status.maxContextTokens,
|
|
11279
12041
|
contextUsage: status.contextUsage,
|
|
@@ -11951,6 +12713,10 @@ const DEFAULT_KEYBOARD_SHORTCUTS = [
|
|
|
11951
12713
|
keys: "Shift-Tab",
|
|
11952
12714
|
description: t("help.toggle_plan")
|
|
11953
12715
|
},
|
|
12716
|
+
{
|
|
12717
|
+
keys: "Tab",
|
|
12718
|
+
description: t("help.cycle_thinking")
|
|
12719
|
+
},
|
|
11954
12720
|
{
|
|
11955
12721
|
keys: "Ctrl-G",
|
|
11956
12722
|
description: t("help.external_editor")
|
|
@@ -11959,10 +12725,26 @@ const DEFAULT_KEYBOARD_SHORTCUTS = [
|
|
|
11959
12725
|
keys: "Ctrl-O",
|
|
11960
12726
|
description: t("help.toggle_output")
|
|
11961
12727
|
},
|
|
12728
|
+
{
|
|
12729
|
+
keys: "Ctrl-E",
|
|
12730
|
+
description: t("help.toggle_plan_view")
|
|
12731
|
+
},
|
|
11962
12732
|
{
|
|
11963
12733
|
keys: "Ctrl-S",
|
|
11964
12734
|
description: t("help.interrupt")
|
|
11965
12735
|
},
|
|
12736
|
+
{
|
|
12737
|
+
keys: "Ctrl-X",
|
|
12738
|
+
description: t("help.toggle_sidebar")
|
|
12739
|
+
},
|
|
12740
|
+
{
|
|
12741
|
+
keys: "Ctrl-B",
|
|
12742
|
+
description: t("help.toggle_hint")
|
|
12743
|
+
},
|
|
12744
|
+
{
|
|
12745
|
+
keys: "Ctrl-W",
|
|
12746
|
+
description: t("help.cancel_memory")
|
|
12747
|
+
},
|
|
11966
12748
|
{
|
|
11967
12749
|
keys: "Shift-Enter / Ctrl-J",
|
|
11968
12750
|
description: t("help.newline")
|
|
@@ -13490,7 +14272,7 @@ var DialogManager = class {
|
|
|
13490
14272
|
}));
|
|
13491
14273
|
}
|
|
13492
14274
|
showMemoryPicker(preloadedMemos, preloadedTotal) {
|
|
13493
|
-
const store =
|
|
14275
|
+
const store = sharedMemoStore(getDataDir());
|
|
13494
14276
|
const hasData = preloadedMemos !== void 0;
|
|
13495
14277
|
const memos = preloadedMemos ?? [];
|
|
13496
14278
|
const total = preloadedTotal ?? 0;
|
|
@@ -13655,6 +14437,7 @@ function createInitialAppState(input) {
|
|
|
13655
14437
|
ccConnectActive: false,
|
|
13656
14438
|
wolfpackMode: input.cliOptions.wolfpack === true,
|
|
13657
14439
|
rlmEnabled: false,
|
|
14440
|
+
rlmMaxDepth: null,
|
|
13658
14441
|
reconnectAttempt: 0,
|
|
13659
14442
|
recentSessions: [],
|
|
13660
14443
|
subagentUsage: {}
|
|
@@ -13986,7 +14769,7 @@ var ScreamTUI = class {
|
|
|
13986
14769
|
return first;
|
|
13987
14770
|
}
|
|
13988
14771
|
pushTranscriptEntry(entry) {
|
|
13989
|
-
this.
|
|
14772
|
+
this.transcriptController.ingestEntry(entry);
|
|
13990
14773
|
}
|
|
13991
14774
|
setExternalEditorRunning(running) {
|
|
13992
14775
|
this.state.externalEditorRunning = running;
|
|
@@ -14003,6 +14786,21 @@ var ScreamTUI = class {
|
|
|
14003
14786
|
get backgroundTasks() {
|
|
14004
14787
|
return this.sessionEventHandler.backgroundTasks;
|
|
14005
14788
|
}
|
|
14789
|
+
/** Live per-type subagent slots (sidebar state machine) — source of the
|
|
14790
|
+
* /tasks browser's Agents view. */
|
|
14791
|
+
get subagentSlots() {
|
|
14792
|
+
return this.sessionEventHandler.getSubagentSlots();
|
|
14793
|
+
}
|
|
14794
|
+
/** Per-instance subagent provenance (spawn parent / outcome / description),
|
|
14795
|
+
* keyed by agentId — source of the /tasks browser's Agents view. */
|
|
14796
|
+
get subagentInstances() {
|
|
14797
|
+
return this.sessionEventHandler.getSubagentInstances();
|
|
14798
|
+
}
|
|
14799
|
+
/** Archive ring for the Agents view: closed instances' last derived rows
|
|
14800
|
+
* (oldest first), the bounded home of agents the live map has released. */
|
|
14801
|
+
get recentSubagentInstances() {
|
|
14802
|
+
return this.sessionEventHandler.getRecentSubagentInstances();
|
|
14803
|
+
}
|
|
14006
14804
|
getCurrentSessionId() {
|
|
14007
14805
|
return this.state.appState.sessionId;
|
|
14008
14806
|
}
|
|
@@ -14172,7 +14970,7 @@ var ScreamTUI = class {
|
|
|
14172
14970
|
const card = new OnboardingCardComponent({
|
|
14173
14971
|
colors: this.state.theme.colors,
|
|
14174
14972
|
onConfigure: () => {
|
|
14175
|
-
import("./dispatch-
|
|
14973
|
+
import("./dispatch-CUfxgLwq.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
14176
14974
|
this.restoreEditor();
|
|
14177
14975
|
onDone?.();
|
|
14178
14976
|
});
|
|
@@ -16179,6 +16977,15 @@ var FileGate = class {
|
|
|
16179
16977
|
* consumer of the same SDK (alongside run-shell TUI and run-stream-json),
|
|
16180
16978
|
* not a separate engine. Zero changes to agent-core or node-sdk.
|
|
16181
16979
|
*/
|
|
16980
|
+
/**
|
|
16981
|
+
* In-memory FIFO caps per web session. The on-disk journal stays complete;
|
|
16982
|
+
* these bounds only stop a long-lived session from growing without limit in
|
|
16983
|
+
* RAM. Dropped rows are tallied so the elision stays observable (see
|
|
16984
|
+
* `getMemoryBounds`) — the same `[…N … elided…]` accounting the transcript
|
|
16985
|
+
* uses, without re-deriving it from a cursor gap.
|
|
16986
|
+
*/
|
|
16987
|
+
const MAX_JOURNAL_ENTRIES = 200;
|
|
16988
|
+
const MAX_SESSION_MESSAGES = 200;
|
|
16182
16989
|
/** Thinking bodies longer than this are delivered as a tail excerpt + on-demand full text. */
|
|
16183
16990
|
const TRUNCATE_THINKING_LEN = 8 * 1024;
|
|
16184
16991
|
function truncateThinking(messages) {
|
|
@@ -16529,6 +17336,50 @@ const MAIN_TRANSCRIPT_EVENTS = new Set([
|
|
|
16529
17336
|
"tool.result"
|
|
16530
17337
|
]);
|
|
16531
17338
|
/**
|
|
17339
|
+
* Identity of a persisted user-message row; used by the fork copy to drop the
|
|
17340
|
+
* memory copy of a row the disk already carries.
|
|
17341
|
+
*/
|
|
17342
|
+
function persistedUserMessageKey(entry) {
|
|
17343
|
+
return JSON.stringify([
|
|
17344
|
+
entry.beforeSeq,
|
|
17345
|
+
entry.clientMessageId ?? null,
|
|
17346
|
+
entry.text
|
|
17347
|
+
]);
|
|
17348
|
+
}
|
|
17349
|
+
/**
|
|
17350
|
+
* Union of the two views of one session's durable journal, for the fork copy.
|
|
17351
|
+
*
|
|
17352
|
+
* `diskEntries` (read back from the source's journal file) is the complete
|
|
17353
|
+
* history and forms the base: the in-memory journal is FIFO-capped
|
|
17354
|
+
* (MAX_JOURNAL_ENTRIES), so a long conversation's memory only holds its tail
|
|
17355
|
+
* while the file keeps every row. `memoryEntries` fills the rows a
|
|
17356
|
+
* fire-and-forget write has not flushed yet; rows the disk already carries are
|
|
17357
|
+
* dropped by seq (journal rows) or identity (user messages), so no line is
|
|
17358
|
+
* doubled.
|
|
17359
|
+
*/
|
|
17360
|
+
function mergeForkJournalEntries(diskEntries, memoryEntries) {
|
|
17361
|
+
const journalSeqs = /* @__PURE__ */ new Set();
|
|
17362
|
+
const userMessageKeys = /* @__PURE__ */ new Set();
|
|
17363
|
+
const journalEntries = [];
|
|
17364
|
+
const userMessages = [];
|
|
17365
|
+
const push = (entry) => {
|
|
17366
|
+
if (entry.type === "user_message") {
|
|
17367
|
+
const key = persistedUserMessageKey(entry);
|
|
17368
|
+
if (userMessageKeys.has(key)) return;
|
|
17369
|
+
userMessageKeys.add(key);
|
|
17370
|
+
userMessages.push(entry);
|
|
17371
|
+
return;
|
|
17372
|
+
}
|
|
17373
|
+
if (journalSeqs.has(entry.seq)) return;
|
|
17374
|
+
journalSeqs.add(entry.seq);
|
|
17375
|
+
journalEntries.push(entry);
|
|
17376
|
+
};
|
|
17377
|
+
for (const entry of diskEntries) push(entry);
|
|
17378
|
+
for (const entry of memoryEntries) push(entry);
|
|
17379
|
+
journalEntries.sort((a, b) => a.seq - b.seq);
|
|
17380
|
+
return [...journalEntries, ...userMessages];
|
|
17381
|
+
}
|
|
17382
|
+
/**
|
|
16532
17383
|
* Whether a journal entry already carries a message (if so it needs no seeding
|
|
16533
17384
|
* from core history).
|
|
16534
17385
|
*/
|
|
@@ -16910,6 +17761,10 @@ var WebSession = class {
|
|
|
16910
17761
|
connections = /* @__PURE__ */ new Map();
|
|
16911
17762
|
journal = [];
|
|
16912
17763
|
userMessages = [];
|
|
17764
|
+
/** Aggregate count of journal entries dropped by the FIFO cap. */
|
|
17765
|
+
droppedJournalCount = 0;
|
|
17766
|
+
/** Aggregate count of message rows dropped by the FIFO cap. */
|
|
17767
|
+
droppedMessageCount = 0;
|
|
16913
17768
|
nextSeq = 1;
|
|
16914
17769
|
epoch = 1;
|
|
16915
17770
|
cachedStatus = null;
|
|
@@ -17001,7 +17856,11 @@ var WebSession = class {
|
|
|
17001
17856
|
getTitle() {
|
|
17002
17857
|
return this.title;
|
|
17003
17858
|
}
|
|
17004
|
-
/**
|
|
17859
|
+
/**
|
|
17860
|
+
* Return the in-memory durable journal entries (capped tail). The fork copy
|
|
17861
|
+
* takes these only for rows the source's journal file does not carry yet;
|
|
17862
|
+
* the file itself is the complete history.
|
|
17863
|
+
*/
|
|
17005
17864
|
getDurableJournalEntries() {
|
|
17006
17865
|
const entries = [];
|
|
17007
17866
|
for (const e of this.journal) if (!e.volatile) entries.push({
|
|
@@ -17016,6 +17875,15 @@ var WebSession = class {
|
|
|
17016
17875
|
});
|
|
17017
17876
|
return entries;
|
|
17018
17877
|
}
|
|
17878
|
+
/**
|
|
17879
|
+
* Drain in-flight persistence writes (journal rows, user messages, metadata).
|
|
17880
|
+
* A fork reads the source's journal file afterwards and must not race a write
|
|
17881
|
+
* still queued behind the serialization gate — the reader would otherwise
|
|
17882
|
+
* mistake a flushed row for unsaved state.
|
|
17883
|
+
*/
|
|
17884
|
+
async flushPendingWrites() {
|
|
17885
|
+
await Promise.allSettled(this.pendingWrites);
|
|
17886
|
+
}
|
|
17019
17887
|
async persistEntry(entry, throwOnError = false) {
|
|
17020
17888
|
if (!this.homeDir) return;
|
|
17021
17889
|
const line = JSON.stringify({
|
|
@@ -17034,6 +17902,7 @@ var WebSession = class {
|
|
|
17034
17902
|
},
|
|
17035
17903
|
beforeSeq
|
|
17036
17904
|
});
|
|
17905
|
+
this.trimMessages();
|
|
17037
17906
|
if (!this.homeDir) return;
|
|
17038
17907
|
const line = JSON.stringify({
|
|
17039
17908
|
type: "user_message",
|
|
@@ -17064,9 +17933,11 @@ var WebSession = class {
|
|
|
17064
17933
|
},
|
|
17065
17934
|
beforeSeq: um.beforeSeq
|
|
17066
17935
|
});
|
|
17936
|
+
this.trimMessages();
|
|
17067
17937
|
} else {
|
|
17068
17938
|
const je = entry;
|
|
17069
17939
|
this.journal.push(je);
|
|
17940
|
+
this.trimJournal();
|
|
17070
17941
|
this.nextSeq = Math.max(this.nextSeq, je.seq + 1);
|
|
17071
17942
|
if (this.session === null && je.payload.agentId === "main" && je.payload.type === "goal.updated") this.cachedGoal = cloneSnapshot(je.payload.snapshot);
|
|
17072
17943
|
else if (this.session === null && je.payload.agentId === "main" && je.payload.type === "todo.updated") this.cachedTodos = cloneSnapshot(je.payload.todos);
|
|
@@ -17133,6 +18004,7 @@ var WebSession = class {
|
|
|
17133
18004
|
...message,
|
|
17134
18005
|
seq
|
|
17135
18006
|
});
|
|
18007
|
+
this.trimMessages();
|
|
17136
18008
|
seq += 1;
|
|
17137
18009
|
}
|
|
17138
18010
|
if (seq - 1 >= this.nextSeq) this.nextSeq = seq;
|
|
@@ -17156,6 +18028,44 @@ var WebSession = class {
|
|
|
17156
18028
|
permission: this.permission
|
|
17157
18029
|
};
|
|
17158
18030
|
}
|
|
18031
|
+
/** FIFO-cap `journal` to `MAX_JOURNAL_ENTRIES`, tallying elided rows. */
|
|
18032
|
+
trimJournal() {
|
|
18033
|
+
while (this.journal.length > MAX_JOURNAL_ENTRIES) {
|
|
18034
|
+
this.journal.shift();
|
|
18035
|
+
this.droppedJournalCount += 1;
|
|
18036
|
+
}
|
|
18037
|
+
}
|
|
18038
|
+
/**
|
|
18039
|
+
* FIFO-cap message stores to `MAX_SESSION_MESSAGES`, tallying elided rows.
|
|
18040
|
+
*
|
|
18041
|
+
* Drop order follows the rendered order (`buildMessages` puts the seeded
|
|
18042
|
+
* core-history rows at the head, live user rows after them), so the eviction
|
|
18043
|
+
* is genuinely oldest-first and the visible tail of the conversation is what
|
|
18044
|
+
* survives.
|
|
18045
|
+
*/
|
|
18046
|
+
trimMessages() {
|
|
18047
|
+
while (this.userMessages.length + this.seededMessages.length > MAX_SESSION_MESSAGES) {
|
|
18048
|
+
if (this.seededMessages.length > 0) this.seededMessages.shift();
|
|
18049
|
+
else if (this.userMessages.length > 0) this.userMessages.shift();
|
|
18050
|
+
else break;
|
|
18051
|
+
this.droppedMessageCount += 1;
|
|
18052
|
+
}
|
|
18053
|
+
}
|
|
18054
|
+
/**
|
|
18055
|
+
* Observable surface of the in-memory FIFO caps: current occupancy, the
|
|
18056
|
+
* limits, and the aggregate elided-row tallies (`[…N … elided…]` accounting,
|
|
18057
|
+
* shared with the transcript).
|
|
18058
|
+
*/
|
|
18059
|
+
getMemoryBounds() {
|
|
18060
|
+
return {
|
|
18061
|
+
journal: this.journal.length,
|
|
18062
|
+
journalLimit: MAX_JOURNAL_ENTRIES,
|
|
18063
|
+
journalElided: this.droppedJournalCount,
|
|
18064
|
+
messages: this.userMessages.length + this.seededMessages.length,
|
|
18065
|
+
messageLimit: MAX_SESSION_MESSAGES,
|
|
18066
|
+
messagesElided: this.droppedMessageCount
|
|
18067
|
+
};
|
|
18068
|
+
}
|
|
17159
18069
|
appendEvent(event) {
|
|
17160
18070
|
const payload = event.type === "goal.updated" || event.type === "todo.updated" ? cloneSnapshot(event) : event;
|
|
17161
18071
|
const entry = {
|
|
@@ -17165,6 +18075,7 @@ var WebSession = class {
|
|
|
17165
18075
|
payload
|
|
17166
18076
|
};
|
|
17167
18077
|
this.journal.push(entry);
|
|
18078
|
+
this.trimJournal();
|
|
17168
18079
|
if (!entry.volatile) this.trackPending(this.persistEntry(entry));
|
|
17169
18080
|
return entry;
|
|
17170
18081
|
}
|
|
@@ -17181,6 +18092,7 @@ var WebSession = class {
|
|
|
17181
18092
|
payload
|
|
17182
18093
|
};
|
|
17183
18094
|
this.journal.push(entry);
|
|
18095
|
+
this.trimJournal();
|
|
17184
18096
|
const persisted = this.persistEntry(entry, throwOnPersistenceError);
|
|
17185
18097
|
this.trackPending(persisted);
|
|
17186
18098
|
return {
|
|
@@ -18488,8 +19400,9 @@ var SessionManager = class {
|
|
|
18488
19400
|
}
|
|
18489
19401
|
/**
|
|
18490
19402
|
* Fork an active session: harness.forkSession copies the agent state into a
|
|
18491
|
-
* new session, and we additionally copy the source's durable journal
|
|
18492
|
-
*
|
|
19403
|
+
* new session, and we additionally copy the source's durable journal from its
|
|
19404
|
+
* journal file — the complete history, not the FIFO-capped in-memory tail —
|
|
19405
|
+
* so the forked web session displays the same prior conversation.
|
|
18493
19406
|
*/
|
|
18494
19407
|
async forkSession(sourceId) {
|
|
18495
19408
|
const source = this.sessions.get(sourceId);
|
|
@@ -18509,13 +19422,18 @@ var SessionManager = class {
|
|
|
18509
19422
|
title,
|
|
18510
19423
|
onFork: (id) => this.forkSession(id)
|
|
18511
19424
|
});
|
|
18512
|
-
|
|
18513
|
-
|
|
19425
|
+
await source.flushPendingWrites();
|
|
19426
|
+
const { entries: diskEntries, corrupt } = await loadJournal(this.homeDir, sourceId);
|
|
19427
|
+
const entries = mergeForkJournalEntries(diskEntries, source.getDurableJournalEntries());
|
|
19428
|
+
webSession.loadFromPersisted(entries);
|
|
19429
|
+
const journalHasContent = webSession.hasAnyAssistantContent();
|
|
19430
|
+
const journalDamaged = corrupt > 0 && !journalHasContent;
|
|
19431
|
+
if (journalDamaged) webSession.markDamaged(`journal has ${corrupt} corrupt lines and no parsed message carries a body`);
|
|
19432
|
+
else if (entries.length > 0) {
|
|
18514
19433
|
const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
18515
19434
|
await writeFile(getJournalPath(this.homeDir, newId), lines);
|
|
18516
19435
|
}
|
|
18517
|
-
|
|
18518
|
-
if (!entries.some(isMessageBearingEntry)) try {
|
|
19436
|
+
if (!entries.some(isMessageBearingEntry) || journalDamaged) try {
|
|
18519
19437
|
const context = await session.getContext();
|
|
18520
19438
|
webSession.seedHistory(contextHistoryToChatMessages(context.history));
|
|
18521
19439
|
} catch (error) {
|
|
@@ -18993,6 +19911,19 @@ function startWebIdleExit(opts) {
|
|
|
18993
19911
|
}
|
|
18994
19912
|
};
|
|
18995
19913
|
}
|
|
19914
|
+
/**
|
|
19915
|
+
* Validates the optional `maxDepth` body field of the /rlm route with the same
|
|
19916
|
+
* grammar as /rlm-max-depth (a non-negative integer; 0 = unlimited) via the
|
|
19917
|
+
* shared agent-core helper. Absent / null leaves the cap untouched; any other
|
|
19918
|
+
* invalid value is a 400 so a client cannot silently send a cap that never
|
|
19919
|
+
* lands.
|
|
19920
|
+
*/
|
|
19921
|
+
function optionalRlmMaxDepth(raw) {
|
|
19922
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
19923
|
+
const parsed = parseRlmMaxDepthArg(typeof raw === "string" ? raw : String(raw));
|
|
19924
|
+
if (!parsed.ok) throw new HttpError(400, `Invalid rlm max depth: ${parsed.reason}`);
|
|
19925
|
+
return parsed.value;
|
|
19926
|
+
}
|
|
18996
19927
|
async function handleSessionControlRoutes(req, res, url, method, manager) {
|
|
18997
19928
|
const statusMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/status$`).exec(url);
|
|
18998
19929
|
if (statusMatch && method === "GET") {
|
|
@@ -19080,7 +20011,7 @@ async function handleSessionControlRoutes(req, res, url, method, manager) {
|
|
|
19080
20011
|
try {
|
|
19081
20012
|
const body = await readJsonBody(req);
|
|
19082
20013
|
const enabled = optionalBoolean(body, "enabled", true);
|
|
19083
|
-
const maxDepth =
|
|
20014
|
+
const maxDepth = optionalRlmMaxDepth(body["maxDepth"]);
|
|
19084
20015
|
await manager.setRlm(decodeURIComponent(rlmMatch[1]), enabled, maxDepth);
|
|
19085
20016
|
sendJson(res, 200, { ok: true });
|
|
19086
20017
|
} catch (error) {
|