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