scream-code 0.16.1 → 0.16.3
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 +1 -1
- package/dist/{app-lSa6fM8z.mjs → app-C1foLjDE.mjs} +582 -532
- package/dist/{dispatch-B6jbxY0D.mjs → dispatch-BGLqpKHG.mjs} +1 -1
- package/dist/{dispatch-DqeJdQZQ.mjs → dispatch-ChDxIz0h.mjs} +1721 -593
- package/dist/main.mjs +1 -1
- package/dist/public/assets/astro-DAp21e1Q.js +2 -0
- package/dist/public/assets/astro-DAp21e1Q.js.map +1 -0
- package/dist/public/assets/bash-Yzrsuije.js +2 -0
- package/dist/public/assets/bash-Yzrsuije.js.map +1 -0
- package/dist/public/assets/c-BIGW1oBm.js +2 -0
- package/dist/public/assets/c-BIGW1oBm.js.map +1 -0
- package/dist/public/assets/cpp-BRuaLJcg.js +2 -0
- package/dist/public/assets/cpp-BRuaLJcg.js.map +1 -0
- package/dist/public/assets/csharp-COcwbKMJ.js +2 -0
- package/dist/public/assets/csharp-COcwbKMJ.js.map +1 -0
- package/dist/public/assets/css-DEUVOdPa.js +2 -0
- package/dist/public/assets/css-DEUVOdPa.js.map +1 -0
- package/dist/public/assets/dart-CF10PKvl.js +2 -0
- package/dist/public/assets/dart-CF10PKvl.js.map +1 -0
- package/dist/public/assets/docker-BcOcwvcX.js +2 -0
- package/dist/public/assets/docker-BcOcwvcX.js.map +1 -0
- package/dist/public/assets/go-CxLEBnE3.js +2 -0
- package/dist/public/assets/go-CxLEBnE3.js.map +1 -0
- package/dist/public/assets/graphql-ChdNCCLP.js +2 -0
- package/dist/public/assets/graphql-ChdNCCLP.js.map +1 -0
- package/dist/public/assets/html-CNxCFRDw.js +2 -0
- package/dist/public/assets/html-CNxCFRDw.js.map +1 -0
- package/dist/public/assets/index-DQBhBKK1.css +1 -0
- package/dist/public/assets/index-DakPeNBt.js +240 -0
- package/dist/public/assets/index-DakPeNBt.js.map +1 -0
- package/dist/public/assets/ini-BEwlwnbL.js +2 -0
- package/dist/public/assets/ini-BEwlwnbL.js.map +1 -0
- package/dist/public/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/public/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/public/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/public/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/public/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/public/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/public/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/public/assets/java-CylS5w8V.js +2 -0
- package/dist/public/assets/java-CylS5w8V.js.map +1 -0
- package/dist/public/assets/javascript-wDzz0qaB.js +2 -0
- package/dist/public/assets/javascript-wDzz0qaB.js.map +1 -0
- package/dist/public/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/public/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/public/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/public/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/public/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/public/assets/json-Cp-IABpG.js +2 -0
- package/dist/public/assets/json-Cp-IABpG.js.map +1 -0
- package/dist/public/assets/jsx-g9-lgVsj.js +2 -0
- package/dist/public/assets/jsx-g9-lgVsj.js.map +1 -0
- package/dist/public/assets/kotlin-BdnUsdx6.js +2 -0
- package/dist/public/assets/kotlin-BdnUsdx6.js.map +1 -0
- package/dist/public/assets/lua-BaeVxFsk.js +2 -0
- package/dist/public/assets/lua-BaeVxFsk.js.map +1 -0
- package/dist/public/assets/make-CHLpvVh8.js +2 -0
- package/dist/public/assets/make-CHLpvVh8.js.map +1 -0
- package/dist/public/assets/markdown-Cvjx9yec.js +2 -0
- package/dist/public/assets/markdown-Cvjx9yec.js.map +1 -0
- package/dist/public/assets/perl-CptaX86O.js +2 -0
- package/dist/public/assets/perl-CptaX86O.js.map +1 -0
- package/dist/public/assets/postcss-DnzrXTVv.js +2 -0
- package/dist/public/assets/postcss-DnzrXTVv.js.map +1 -0
- package/dist/public/assets/protobuf-C7zT0LnQ.js +2 -0
- package/dist/public/assets/protobuf-C7zT0LnQ.js.map +1 -0
- package/dist/public/assets/python-B6aJPvgy.js +2 -0
- package/dist/public/assets/python-B6aJPvgy.js.map +1 -0
- package/dist/public/assets/r-Dspwwk_N.js +2 -0
- package/dist/public/assets/r-Dspwwk_N.js.map +1 -0
- package/dist/public/assets/ruby-CcrC3V9I.js +2 -0
- package/dist/public/assets/ruby-CcrC3V9I.js.map +1 -0
- package/dist/public/assets/rust-B1yitclQ.js +2 -0
- package/dist/public/assets/rust-B1yitclQ.js.map +1 -0
- package/dist/public/assets/scala-C151Ov-r.js +2 -0
- package/dist/public/assets/scala-C151Ov-r.js.map +1 -0
- package/dist/public/assets/sql-BLtJtn59.js +2 -0
- package/dist/public/assets/sql-BLtJtn59.js.map +1 -0
- package/dist/public/assets/svelte-U4fHntn6.js +2 -0
- package/dist/public/assets/svelte-U4fHntn6.js.map +1 -0
- package/dist/public/assets/swift-D82vCrfD.js +2 -0
- package/dist/public/assets/swift-D82vCrfD.js.map +1 -0
- package/dist/public/assets/toml-vGWfd6FD.js +2 -0
- package/dist/public/assets/toml-vGWfd6FD.js.map +1 -0
- package/dist/public/assets/tsx-COt5Ahok.js +2 -0
- package/dist/public/assets/tsx-COt5Ahok.js.map +1 -0
- package/dist/public/assets/typescript-BPQ3VLAy.js +2 -0
- package/dist/public/assets/typescript-BPQ3VLAy.js.map +1 -0
- package/dist/public/assets/xml-sdJ4AIDG.js +2 -0
- package/dist/public/assets/xml-sdJ4AIDG.js.map +1 -0
- package/dist/public/assets/yaml-Buea-lGh.js +2 -0
- package/dist/public/assets/yaml-Buea-lGh.js.map +1 -0
- package/dist/public/index.html +2 -2
- package/dist/{text-input-dialog-rSkH3JTV.mjs → text-input-dialog-DAQfJdHu.mjs} +1 -1
- package/dist/{text-input-dialog-BABWY9vy.mjs → text-input-dialog-GajpxjyL.mjs} +66 -20
- package/package.json +5 -4
- package/dist/public/assets/index-DwX1jGaf.js +0 -96
- package/dist/public/assets/index-DwX1jGaf.js.map +0 -1
- package/dist/public/assets/index-zIKfLBMX.css +0 -1
- package/dist/public/assets/wasm-CG6Dc4jp.js +0 -2
- package/dist/public/assets/wasm-CG6Dc4jp.js.map +0 -1
- package/icon.ico +0 -0
|
@@ -4,14 +4,14 @@ import { dirname as __cjsShimDirname } from 'node:path';
|
|
|
4
4
|
const __filename = __cjsShimFileURLToPath(import.meta.url);
|
|
5
5
|
const __dirname = __cjsShimDirname(__filename);
|
|
6
6
|
import "./suppress-sqlite-warning-C2VB0doZ.mjs";
|
|
7
|
-
import { $n as
|
|
8
|
-
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-
|
|
7
|
+
import { $n as readSessionIndex, $t as SubagentSlots, A as langFromPath, An as startManualEmbeddingDownload, At as toggleEmptySessionHint, B as charsForTokenBudget, Bn as getLogDir, Bt as OSC11_QUERY, C as getDaemonInstructions, Cn as buildSkillSlashCommands, D as renderDiffLines, Dn as setExperimentalFlags, Dt as createMarkdownTheme, E as ToolCallComponent, En as isExperimentalFlagEnabled, Et as createEditorTheme, F as replaceTabs, Fn as loadTuiConfig, Ft as parseOsc11BackgroundTheme, G as WelcomeComponent, Gn as PRODUCT_NAME, Gt as TERMINAL_FOCUS_IN, H as getSharedSpeedTracker, Hn as CLI_COMMAND_NAME, Ht as OSC11_RESPONSE_PREFIX, I as sanitizeShellOutput, In as saveTuiConfig, It as DISABLE_TERMINAL_FOCUS_REPORTING, J as resetBreathingClock, Jn as saveCatalogCache, Jt as TERMINAL_THEME_LIGHT, K as BREATHE_CYCLE_MS, Kn as DEFAULT_CATALOG_URL, Kt as TERMINAL_FOCUS_OUT, L as SkillActivationComponent, Ln as detectShellEnvironment, Lt as DISABLE_TERMINAL_THEME_REPORTING, M as ThinkingComponent, Mn as PULSE_WAVE_FRAMES, Mt as contrastTextHex, N as NoticeMessageComponent, Nn as TuiConfigParseError, Nt as getColorPalette, O as renderDiffLinesClustered, On as getKnowledgeStore, Ot as isEmptySessionHintDismissed, P as StatusMessageComponent, Pn as TuiLikePreferencesSchema, Pt as detectTerminalTheme, Qn as appendSessionIndexEntry, Qt as lerpGradient, R as AssistantMessageComponent, Rn as getDataDir, Rt as ENABLE_TERMINAL_FOCUS_REPORTING, Sn as getNoActiveSessionMessage, St as clearInfoPanelState, T as UserMessageComponent, Tn as sortSlashCommands, Tt as resolveThemeSync, U as BackgroundAgentStatusComponent, Un as CLI_UI_MODE, Ut as OSC11_RESPONSE_PREFIX_NO_ESC, V as estimateTokens, Vn as detectInstallSource, Vt as OSC11_RESPONSE, W as AgentGroupComponent, Wn as CLI_USER_AGENT_PRODUCT, Wt as QUERY_TERMINAL_THEME, X as clearGoalState, Xn as encodeWorkDirKey, Xt as isStreaming, Y as CompactionComponent, Yn as ScreamHarness, Yt as isBusy, Z as refineGoal, Zn as normalizeWorkDir, Zt as FooterComponent, _ as selectUpdateTarget, _n as SESSION_TIPS, an as STATUS_BULLET, ar as log, at as changeThinkingLevel, b as appendJsonlLine, bn as getCtrlDHint, bt as refreshProviderBalance, c as hasDispose, cn as formatErrorMessage, cr as isOrphanedToolCallError, d as MoonLoader, dn as serializeToolResultOutput, dt as handleFusionPlanCommand, er as removeSessionIndexEntry, f as formatMemoryMemoForInjection, fn as stringValue, gn as MAIN_AGENT_ID, hn as EXIT_CONFIRM_WINDOW_MS, i as buildRoleAdditionalText, in as printableChar, ir as flushDiagnosticLogs, it as toTerminalHyperlink, j as CachedContainer, jn as PIXEL_PULSE_FRAMES, jt as createThemeStyles, k as highlightLines, kn as isEmbeddingModelCached, kt as isTurnElapsedEnabled, l as isExpandable, ln as isTodoItemShape, lr as ErrorCodes, mn as EMPTY_SESSION_HINT_URL, n as clearEvalPanelState, nr as resolveScreamHome, on as appendStreamingArgsPreview, or as resolveGlobalLogPath, ot as getModelCycleLevel, pn as truncateErrorMessage, pt as handlePlanCommand, q as getBreathingFrame, qn as fetchCatalog, qt as TERMINAL_THEME_DARK, r as openUrl, rr as MemoryMemoStore, rt as isInternalMessage, s as disposeChildren, sn as argsRecord, sr as isScreamError, t as dispatchInput, u as isPlanExpandable, un as parseStreamingArgs, ur as SCREAM_ERROR_INFO, v as refreshUpdateCache, vn as TIP_ROTATION_INTERVAL_MS, wn as BUILTIN_SLASH_COMMANDS, x as readJsonlFile, xn as getLlmNotSetMessage, xt as supportsBalance, y as readUpdateCache, yn as getCtrlCHint, z as ActivityGroupComponent, zn as getInputHistoryFile, zt as ENABLE_TERMINAL_THEME_REPORTING } from "./dispatch-ChDxIz0h.mjs";
|
|
8
|
+
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-GajpxjyL.mjs";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
11
|
-
import { access, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
11
|
+
import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
12
12
|
import { Writable } from "node:stream";
|
|
13
13
|
import { WebSocket, WebSocketServer } from "ws";
|
|
14
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { appendFileSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
15
|
import * as path$1 from "node:path";
|
|
16
16
|
import path, { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
17
17
|
import { z } from "zod";
|
|
@@ -32,7 +32,7 @@ function optionalBuildString(value) {
|
|
|
32
32
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
33
33
|
}
|
|
34
34
|
const SCREAM_BUILD_INFO = {
|
|
35
|
-
version: optionalBuildString("0.16.
|
|
35
|
+
version: optionalBuildString("0.16.3"),
|
|
36
36
|
channel: optionalBuildString(""),
|
|
37
37
|
commit: optionalBuildString(""),
|
|
38
38
|
buildTarget: optionalBuildString("darwin-arm64")
|
|
@@ -1968,42 +1968,6 @@ var EditorKeyboardController = class {
|
|
|
1968
1968
|
}
|
|
1969
1969
|
};
|
|
1970
1970
|
//#endregion
|
|
1971
|
-
//#region src/tui/utils/sanitize.ts
|
|
1972
|
-
/** Replace tabs with spaces, keeping column alignment. Default tab width 4. */
|
|
1973
|
-
function replaceTabs(text, tabWidth = 4) {
|
|
1974
|
-
return text.replaceAll(" ", " ".repeat(tabWidth));
|
|
1975
|
-
}
|
|
1976
|
-
/**
|
|
1977
|
-
* Strip terminal control characters from untrusted shell output before it is
|
|
1978
|
-
* rendered inside a framed pane. Carriage returns (progress-bar redraws) and
|
|
1979
|
-
* ANSI escape sequences (cursor moves, screen clears, SGR color codes, OSC
|
|
1980
|
-
* title/OSC 52 clipboard sequences, charset selection) would otherwise disrupt
|
|
1981
|
-
* the terminal borders or let task output inject escape sequences. All
|
|
1982
|
-
* printable text is preserved.
|
|
1983
|
-
*/
|
|
1984
|
-
function sanitizeShellOutput(output) {
|
|
1985
|
-
return output.replaceAll("\r", "").replaceAll(/\u001B\[[0-?]*[ -/]*[@-~]/g, "").replaceAll(/\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)/g, "").replaceAll(/\u001B[P^_][^\u0007\u001B]*(?:\u0007|\u001B\\)/g, "").replaceAll(/\u001B[()][0-9A-Za-z]/g, "").replaceAll(/\u001B[=>78DEFHMc]/g, "");
|
|
1986
|
-
}
|
|
1987
|
-
//#endregion
|
|
1988
|
-
//#region src/tui/components/messages/status-message.ts
|
|
1989
|
-
var StatusMessageComponent = class extends Container {
|
|
1990
|
-
constructor(content, colors, color) {
|
|
1991
|
-
super();
|
|
1992
|
-
const sanitized = replaceTabs(content);
|
|
1993
|
-
const text = color === void 0 ? chalk.hex(colors.textDim)(sanitized) : chalk.hex(color)(sanitized);
|
|
1994
|
-
this.addChild(new Text(` ${text}`, 0, 0));
|
|
1995
|
-
}
|
|
1996
|
-
};
|
|
1997
|
-
var NoticeMessageComponent = class extends Container {
|
|
1998
|
-
constructor(title, detail, colors, markerColor) {
|
|
1999
|
-
super();
|
|
2000
|
-
const marker = markerColor === void 0 ? " " : chalk.hex(markerColor)(INTERJECTION_BULLET);
|
|
2001
|
-
this.addChild(new Spacer(1));
|
|
2002
|
-
this.addChild(new Text(`${marker}${chalk.hex(colors.textStrong)(replaceTabs(title))}`, 0, 0));
|
|
2003
|
-
if (detail !== void 0 && detail.length > 0) this.addChild(new Text(` ${chalk.hex(colors.textDim)(replaceTabs(detail))}`, 0, 0));
|
|
2004
|
-
}
|
|
2005
|
-
};
|
|
2006
|
-
//#endregion
|
|
2007
1971
|
//#region src/tui/utils/background-agent-status.ts
|
|
2008
1972
|
const MAX_BACKGROUND_FIELD_LENGTH = 240;
|
|
2009
1973
|
function normalizeBackgroundField(value) {
|
|
@@ -2183,233 +2147,6 @@ function renderChildRequestNotice(fields, name) {
|
|
|
2183
2147
|
};
|
|
2184
2148
|
}
|
|
2185
2149
|
//#endregion
|
|
2186
|
-
//#region src/tui/utils/subagent-slots.ts
|
|
2187
|
-
/** Default subagent types, in the fixed display order of the slots. */
|
|
2188
|
-
const DEFAULT_SUBAGENT_TYPES = [
|
|
2189
|
-
"coder",
|
|
2190
|
-
"explore",
|
|
2191
|
-
"plan",
|
|
2192
|
-
"verify",
|
|
2193
|
-
"reviewer",
|
|
2194
|
-
"oracle",
|
|
2195
|
-
"worker",
|
|
2196
|
-
"writer"
|
|
2197
|
-
];
|
|
2198
|
-
/** How long a `requesting` overlay stays visible after a subagent asks the
|
|
2199
|
-
* parent for help. The request itself returns at once and the agent keeps
|
|
2200
|
-
* working, so this is a display window, not a blocked state. */
|
|
2201
|
-
const REQUESTING_WINDOW_MS = 5e3;
|
|
2202
|
-
var SubagentSlots = class {
|
|
2203
|
-
slots = /* @__PURE__ */ new Map();
|
|
2204
|
-
instances = /* @__PURE__ */ new Map();
|
|
2205
|
-
/** Live instance count per type: a slot only returns to idle when the LAST
|
|
2206
|
-
* instance of that type terminates (WolfPack spawns several coders at once
|
|
2207
|
-
* and they share one visual slot). */
|
|
2208
|
-
runningByType = /* @__PURE__ */ new Map();
|
|
2209
|
-
/** main-side SendSubagentMessage tool callId → target subagent agentId. */
|
|
2210
|
-
messagingByCall = /* @__PURE__ */ new Map();
|
|
2211
|
-
/** Status to restore per TYPE when a messaging window closes, captured from
|
|
2212
|
-
* the first open window; concurrent windows on the same type keep counting
|
|
2213
|
-
* and the slot only exits messaging when the last window closes. */
|
|
2214
|
-
messagingPrev = /* @__PURE__ */ new Map();
|
|
2215
|
-
/** Slot type → open `requesting` overlay. Keyed by type so several live
|
|
2216
|
-
* instances of one type share a single marker; the real activity state stays
|
|
2217
|
-
* untouched in the slot and shows back up once the overlay expires. */
|
|
2218
|
-
requesting = /* @__PURE__ */ new Map();
|
|
2219
|
-
/** Count of open messaging windows per type (handles same-type siblings). */
|
|
2220
|
-
messagingCount = /* @__PURE__ */ new Map();
|
|
2221
|
-
reset() {
|
|
2222
|
-
this.slots.clear();
|
|
2223
|
-
this.instances.clear();
|
|
2224
|
-
this.runningByType.clear();
|
|
2225
|
-
this.messagingByCall.clear();
|
|
2226
|
-
this.messagingPrev.clear();
|
|
2227
|
-
this.messagingCount.clear();
|
|
2228
|
-
this.requesting.clear();
|
|
2229
|
-
}
|
|
2230
|
-
/**
|
|
2231
|
-
* subagent.spawned. A first spawn starts the slot as working; a second
|
|
2232
|
-
* spawn of the SAME agentId means the main agent resumed it (返工中),
|
|
2233
|
-
* tracked via an instance generation counter.
|
|
2234
|
-
*/
|
|
2235
|
-
onSpawned(agentId, subagentType, description) {
|
|
2236
|
-
const instance = this.instances.get(agentId);
|
|
2237
|
-
if (instance !== void 0) {
|
|
2238
|
-
instance.generation += 1;
|
|
2239
|
-
this.runningByType.set(instance.type, (this.runningByType.get(instance.type) ?? 0) + 1);
|
|
2240
|
-
this.setState(instance.type, "reworking", agentId, description === void 0 ? void 0 : `resume: ${description}`);
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
this.instances.set(agentId, {
|
|
2244
|
-
type: subagentType,
|
|
2245
|
-
generation: 1
|
|
2246
|
-
});
|
|
2247
|
-
this.runningByType.set(subagentType, (this.runningByType.get(subagentType) ?? 0) + 1);
|
|
2248
|
-
this.setState(subagentType, "working", agentId, description);
|
|
2249
|
-
}
|
|
2250
|
-
/** subagent.started. Keeps the current state (spawned already set it); only
|
|
2251
|
-
* used to materialize a slot if lifecycle events raced past us. */
|
|
2252
|
-
onStarted(agentId) {
|
|
2253
|
-
const instance = this.instances.get(agentId);
|
|
2254
|
-
if (instance === void 0) return;
|
|
2255
|
-
const slot = this.ensureSlot(instance.type);
|
|
2256
|
-
if (slot.status === "idle") {
|
|
2257
|
-
slot.status = "working";
|
|
2258
|
-
slot.agentId = agentId;
|
|
2259
|
-
slot.lastActivityAt = Date.now();
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
/** subagent.completed / subagent.failed: the slot goes back to resting only
|
|
2263
|
-
* when the LAST live instance of that type terminates (siblings spawned by
|
|
2264
|
-
* the same tool call keep the slot busy). */
|
|
2265
|
-
onTerminated(agentId) {
|
|
2266
|
-
const instance = this.instances.get(agentId);
|
|
2267
|
-
if (instance === void 0) return;
|
|
2268
|
-
if (this.requesting.get(instance.type)?.agentId === agentId) this.requesting.delete(instance.type);
|
|
2269
|
-
const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
|
|
2270
|
-
if (remaining > 0) {
|
|
2271
|
-
this.runningByType.set(instance.type, remaining);
|
|
2272
|
-
const slot = this.ensureSlot(instance.type);
|
|
2273
|
-
slot.detail = void 0;
|
|
2274
|
-
slot.lastActivityAt = Date.now();
|
|
2275
|
-
return;
|
|
2276
|
-
}
|
|
2277
|
-
this.runningByType.delete(instance.type);
|
|
2278
|
-
this.setState(instance.type, "idle", void 0, void 0);
|
|
2279
|
-
}
|
|
2280
|
-
/**
|
|
2281
|
-
* Routed subagent activity (agentId !== main):
|
|
2282
|
-
* - kind 'tool' → working (tool call in flight)
|
|
2283
|
-
* - kind 'output' → outputting (assistant/thinking delta)
|
|
2284
|
-
* - kind 'toolResult'→ detail only (the agent may keep working/outputting)
|
|
2285
|
-
*/
|
|
2286
|
-
onActivity(agentId, kind, detail) {
|
|
2287
|
-
const instance = this.instances.get(agentId);
|
|
2288
|
-
if (instance === void 0) return;
|
|
2289
|
-
if (kind === "toolResult") {
|
|
2290
|
-
const slot = this.ensureSlot(instance.type);
|
|
2291
|
-
slot.detail = detail;
|
|
2292
|
-
slot.lastActivityAt = Date.now();
|
|
2293
|
-
return;
|
|
2294
|
-
}
|
|
2295
|
-
this.setState(instance.type, kind === "tool" ? "working" : "outputting", agentId, detail);
|
|
2296
|
-
}
|
|
2297
|
-
/** A subagent asked the parent for help. The request returns immediately and
|
|
2298
|
-
* the agent keeps working, so this only raises a transient overlay — the real
|
|
2299
|
-
* activity state stays in the slot and shows back up when it expires. While
|
|
2300
|
-
* `messaging` is on screen the overlay loses (see visibleSlot). */
|
|
2301
|
-
onRequesting(agentId, at = Date.now()) {
|
|
2302
|
-
const instance = this.instances.get(agentId);
|
|
2303
|
-
if (instance === void 0) return;
|
|
2304
|
-
this.requesting.set(instance.type, {
|
|
2305
|
-
until: at + REQUESTING_WINDOW_MS,
|
|
2306
|
-
agentId
|
|
2307
|
-
});
|
|
2308
|
-
const slot = this.ensureSlot(instance.type);
|
|
2309
|
-
slot.agentId = agentId;
|
|
2310
|
-
slot.lastActivityAt = at;
|
|
2311
|
-
}
|
|
2312
|
-
/**
|
|
2313
|
-
* Main-side SendSubagentMessage tool call started: the target subagent's
|
|
2314
|
-
* slot shows messaging until the tool call returns.
|
|
2315
|
-
*/
|
|
2316
|
-
onMessagingStart(toolCallId, targetAgentId, operation) {
|
|
2317
|
-
if (targetAgentId === void 0) return;
|
|
2318
|
-
const instance = this.instances.get(targetAgentId);
|
|
2319
|
-
if (instance === void 0) return;
|
|
2320
|
-
this.messagingByCall.set(toolCallId, targetAgentId);
|
|
2321
|
-
this.requesting.delete(instance.type);
|
|
2322
|
-
const slot = this.ensureSlot(instance.type);
|
|
2323
|
-
if (slot.status !== "messaging") this.messagingPrev.set(instance.type, slot.status);
|
|
2324
|
-
this.messagingCount.set(instance.type, (this.messagingCount.get(instance.type) ?? 0) + 1);
|
|
2325
|
-
this.setState(instance.type, "messaging", targetAgentId, operation === void 0 ? void 0 : `message (${operation})`);
|
|
2326
|
-
}
|
|
2327
|
-
/** Tool call finished: the messaging window closes; restore the state the
|
|
2328
|
-
* slot had before the chat (working/outputting), else idle. */
|
|
2329
|
-
onMessagingEnd(toolCallId) {
|
|
2330
|
-
const target = this.messagingByCall.get(toolCallId);
|
|
2331
|
-
if (target === void 0) return;
|
|
2332
|
-
this.messagingByCall.delete(toolCallId);
|
|
2333
|
-
const instance = this.instances.get(target);
|
|
2334
|
-
if (instance === void 0) return;
|
|
2335
|
-
const slot = this.ensureSlot(instance.type);
|
|
2336
|
-
if (slot.status !== "messaging") return;
|
|
2337
|
-
const remaining = (this.messagingCount.get(instance.type) ?? 1) - 1;
|
|
2338
|
-
if (remaining > 0) {
|
|
2339
|
-
this.messagingCount.set(instance.type, remaining);
|
|
2340
|
-
return;
|
|
2341
|
-
}
|
|
2342
|
-
this.messagingCount.delete(instance.type);
|
|
2343
|
-
const prev = this.messagingPrev.get(instance.type);
|
|
2344
|
-
this.messagingPrev.delete(instance.type);
|
|
2345
|
-
slot.status = prev === "working" || prev === "outputting" || prev === "reworking" ? prev : "idle";
|
|
2346
|
-
slot.detail = void 0;
|
|
2347
|
-
slot.lastActivityAt = Date.now();
|
|
2348
|
-
}
|
|
2349
|
-
/**
|
|
2350
|
-
* All slots in fixed display order: the eight default types first (always
|
|
2351
|
-
* present, idle when never used), then extra types in spawn order.
|
|
2352
|
-
*/
|
|
2353
|
-
getSlots(at = Date.now()) {
|
|
2354
|
-
this.dropExpiredOverlays(at);
|
|
2355
|
-
const ordered = [];
|
|
2356
|
-
for (const type of DEFAULT_SUBAGENT_TYPES) ordered.push(this.visibleSlot(type));
|
|
2357
|
-
for (const slot of this.slots.values()) {
|
|
2358
|
-
if (DEFAULT_SUBAGENT_TYPES.includes(slot.type)) continue;
|
|
2359
|
-
ordered.push(this.visibleSlot(slot.type));
|
|
2360
|
-
}
|
|
2361
|
-
return ordered;
|
|
2362
|
-
}
|
|
2363
|
-
/** Display view of one slot: the real state with an unexpired `requesting`
|
|
2364
|
-
* overlay on top. Read-path only — it never mutates. Callers must run
|
|
2365
|
-
* {@link dropExpiredOverlays} first, and messaging clears the overlay in
|
|
2366
|
-
* {@link onMessagingStart}; the `messaging` guard only covers same-tick ordering. */
|
|
2367
|
-
visibleSlot(type) {
|
|
2368
|
-
const copy = {
|
|
2369
|
-
...this.ensureSlot(type),
|
|
2370
|
-
count: this.runningByType.get(type) ?? 0
|
|
2371
|
-
};
|
|
2372
|
-
if (this.requesting.has(type) && copy.status !== "messaging") copy.status = "requesting";
|
|
2373
|
-
return copy;
|
|
2374
|
-
}
|
|
2375
|
-
dropExpiredOverlays(at) {
|
|
2376
|
-
for (const [type, overlay] of this.requesting) if (at >= overlay.until) this.requesting.delete(type);
|
|
2377
|
-
}
|
|
2378
|
-
ensureSlot(type) {
|
|
2379
|
-
let slot = this.slots.get(type);
|
|
2380
|
-
if (slot === void 0) {
|
|
2381
|
-
if (this.slots.size >= 16) {
|
|
2382
|
-
slot = {
|
|
2383
|
-
type,
|
|
2384
|
-
status: "idle",
|
|
2385
|
-
agentId: void 0,
|
|
2386
|
-
detail: void 0,
|
|
2387
|
-
count: 0,
|
|
2388
|
-
lastActivityAt: 0
|
|
2389
|
-
};
|
|
2390
|
-
return slot;
|
|
2391
|
-
}
|
|
2392
|
-
slot = {
|
|
2393
|
-
type,
|
|
2394
|
-
status: "idle",
|
|
2395
|
-
agentId: void 0,
|
|
2396
|
-
detail: void 0,
|
|
2397
|
-
count: 0,
|
|
2398
|
-
lastActivityAt: Date.now()
|
|
2399
|
-
};
|
|
2400
|
-
this.slots.set(type, slot);
|
|
2401
|
-
}
|
|
2402
|
-
return slot;
|
|
2403
|
-
}
|
|
2404
|
-
setState(type, status, agentId, detail) {
|
|
2405
|
-
const slot = this.ensureSlot(type);
|
|
2406
|
-
slot.status = status;
|
|
2407
|
-
slot.agentId = agentId;
|
|
2408
|
-
slot.detail = detail;
|
|
2409
|
-
slot.lastActivityAt = Date.now();
|
|
2410
|
-
}
|
|
2411
|
-
};
|
|
2412
|
-
//#endregion
|
|
2413
2150
|
//#region src/tui/utils/background-task-status.ts
|
|
2414
2151
|
const MAX_DETAIL_LENGTH = 240;
|
|
2415
2152
|
function truncate(value) {
|
|
@@ -2984,7 +2721,7 @@ var SessionEventHandler = class {
|
|
|
2984
2721
|
if (this.providerLatencySamples.length > this.providerLatencyWindow) this.providerLatencySamples.shift();
|
|
2985
2722
|
this.providerLatencyAt = Date.now();
|
|
2986
2723
|
}
|
|
2987
|
-
/** Sidebar Hub: average measured
|
|
2724
|
+
/** Sidebar Hub: average measured first-token latency for the provider in use. The
|
|
2988
2725
|
* caller owns tone and staleness — this stays a plain reading. */
|
|
2989
2726
|
getProviderLatency() {
|
|
2990
2727
|
if (this.providerLatencySamples.length === 0) return {
|
|
@@ -3137,6 +2874,7 @@ var SessionEventHandler = class {
|
|
|
3137
2874
|
handleTurnBegin(_event) {
|
|
3138
2875
|
this.host.streamingUI.resetToolUi();
|
|
3139
2876
|
this.host.streamingUI.setStep(0);
|
|
2877
|
+
this.host.streamingUI.endActivityGroup();
|
|
3140
2878
|
this.host.patchLivePane({
|
|
3141
2879
|
pendingApproval: null,
|
|
3142
2880
|
pendingQuestion: null
|
|
@@ -3399,6 +3137,7 @@ var SessionEventHandler = class {
|
|
|
3399
3137
|
this.host.streamingUI.flushNow();
|
|
3400
3138
|
this.host.streamingUI.resetToolUi();
|
|
3401
3139
|
this.host.streamingUI.finalizeLiveTextBuffers();
|
|
3140
|
+
this.host.streamingUI.endActivityGroup();
|
|
3402
3141
|
this.host.showError(`[${event.code}] ${event.message}`);
|
|
3403
3142
|
}
|
|
3404
3143
|
handleSessionWarning(event) {
|
|
@@ -4344,6 +4083,7 @@ var SessionReplayRenderer = class {
|
|
|
4344
4083
|
context.completedToolCallIds.add(toolCallId);
|
|
4345
4084
|
}
|
|
4346
4085
|
advanceTurn(context) {
|
|
4086
|
+
this.host.streamingUI.endActivityGroup();
|
|
4347
4087
|
context.turnIndex += 1;
|
|
4348
4088
|
context.stepIndex = 0;
|
|
4349
4089
|
context.currentTurnId = `replay:${String(context.turnIndex)}`;
|
|
@@ -4509,81 +4249,6 @@ var SessionReplayRenderer = class {
|
|
|
4509
4249
|
}
|
|
4510
4250
|
};
|
|
4511
4251
|
//#endregion
|
|
4512
|
-
//#region src/tui/components/dialogs/compaction.ts
|
|
4513
|
-
/**
|
|
4514
|
-
* Renders a compaction block in the transcript.
|
|
4515
|
-
*
|
|
4516
|
-
* Lifecycle:
|
|
4517
|
-
* - constructed on `compaction.started` → blinking white bullet +
|
|
4518
|
-
* "Compacting context..." and optional custom instruction
|
|
4519
|
-
* - `markDone()` on `compaction.completed` → solid green bullet +
|
|
4520
|
-
* "Compaction complete (X → Y tokens)"
|
|
4521
|
-
* - `markCanceled()` on `compaction.cancelled` → solid warning bullet +
|
|
4522
|
-
* "Compaction cancelled"
|
|
4523
|
-
*
|
|
4524
|
-
* Bullet animation mirrors `ToolCallComponent` (500ms blink) so the user
|
|
4525
|
-
* reads the same "work in progress" signal across the UI.
|
|
4526
|
-
*/
|
|
4527
|
-
const BLINK_INTERVAL = 500;
|
|
4528
|
-
var CompactionComponent = class extends Container {
|
|
4529
|
-
colors;
|
|
4530
|
-
ui;
|
|
4531
|
-
headerText;
|
|
4532
|
-
blinkOn = true;
|
|
4533
|
-
blinkTimer = null;
|
|
4534
|
-
done = false;
|
|
4535
|
-
canceled = false;
|
|
4536
|
-
tokensBefore;
|
|
4537
|
-
tokensAfter;
|
|
4538
|
-
constructor(colors, ui, instruction) {
|
|
4539
|
-
super();
|
|
4540
|
-
this.colors = colors;
|
|
4541
|
-
this.ui = ui;
|
|
4542
|
-
this.addChild(new Spacer(1));
|
|
4543
|
-
this.headerText = new Text(this.buildHeader(), 0, 0);
|
|
4544
|
-
this.addChild(this.headerText);
|
|
4545
|
-
if (instruction !== void 0) this.addChild(new Text(chalk.dim(` ${instruction}`), 0, 0));
|
|
4546
|
-
this.startBlink();
|
|
4547
|
-
}
|
|
4548
|
-
markDone(tokensBefore, tokensAfter) {
|
|
4549
|
-
if (this.done || this.canceled) return;
|
|
4550
|
-
this.done = true;
|
|
4551
|
-
this.tokensBefore = tokensBefore;
|
|
4552
|
-
this.tokensAfter = tokensAfter;
|
|
4553
|
-
this.stopBlink();
|
|
4554
|
-
this.headerText.setText(this.buildHeader());
|
|
4555
|
-
this.ui?.requestRender();
|
|
4556
|
-
}
|
|
4557
|
-
markCanceled() {
|
|
4558
|
-
if (this.done || this.canceled) return;
|
|
4559
|
-
this.canceled = true;
|
|
4560
|
-
this.stopBlink();
|
|
4561
|
-
this.headerText.setText(this.buildHeader());
|
|
4562
|
-
this.ui?.requestRender();
|
|
4563
|
-
}
|
|
4564
|
-
dispose() {
|
|
4565
|
-
this.stopBlink();
|
|
4566
|
-
}
|
|
4567
|
-
buildHeader() {
|
|
4568
|
-
if (this.done) return `${chalk.hex(this.colors.success)(STATUS_BULLET)}${chalk.hex(this.colors.success).bold(t("compaction.done"))}${this.tokensBefore !== void 0 && this.tokensAfter !== void 0 ? chalk.dim(` (${String(this.tokensBefore)} → ${String(this.tokensAfter)} token)`) : ""}`;
|
|
4569
|
-
if (this.canceled) return `${chalk.hex(this.colors.warning)(STATUS_BULLET)}${chalk.hex(this.colors.warning).bold(t("compaction.canceled"))}`;
|
|
4570
|
-
return `${this.blinkOn ? chalk.hex(this.colors.roleAssistant)(STATUS_BULLET) : " "}${chalk.hex(this.colors.primary).bold(t("compaction.in_progress"))}`;
|
|
4571
|
-
}
|
|
4572
|
-
startBlink() {
|
|
4573
|
-
this.blinkTimer = setInterval(() => {
|
|
4574
|
-
this.blinkOn = !this.blinkOn;
|
|
4575
|
-
this.headerText.setText(this.buildHeader());
|
|
4576
|
-
this.ui?.requestRender();
|
|
4577
|
-
}, BLINK_INTERVAL);
|
|
4578
|
-
}
|
|
4579
|
-
stopBlink() {
|
|
4580
|
-
if (this.blinkTimer !== null) {
|
|
4581
|
-
clearInterval(this.blinkTimer);
|
|
4582
|
-
this.blinkTimer = null;
|
|
4583
|
-
}
|
|
4584
|
-
}
|
|
4585
|
-
};
|
|
4586
|
-
//#endregion
|
|
4587
4252
|
//#region src/tui/utils/terminal-notification.ts
|
|
4588
4253
|
function notifyTerminalOnce(state, key, notification) {
|
|
4589
4254
|
const { enabled, condition } = state.appState.notifications;
|
|
@@ -4657,7 +4322,7 @@ function isControlCharacter(ch) {
|
|
|
4657
4322
|
}
|
|
4658
4323
|
//#endregion
|
|
4659
4324
|
//#region src/tui/controllers/streaming-ui.ts
|
|
4660
|
-
var StreamingUIController = class {
|
|
4325
|
+
var StreamingUIController = class StreamingUIController {
|
|
4661
4326
|
host;
|
|
4662
4327
|
flushTimer;
|
|
4663
4328
|
lastFlushAt;
|
|
@@ -4688,9 +4353,30 @@ var StreamingUIController = class {
|
|
|
4688
4353
|
_activeToolCalls = /* @__PURE__ */ new Map();
|
|
4689
4354
|
_streamingToolCallArguments = /* @__PURE__ */ new Map();
|
|
4690
4355
|
_pendingToolComponents = /* @__PURE__ */ new Map();
|
|
4691
|
-
_pendingReadGroupComponents = /* @__PURE__ */ new Map();
|
|
4692
4356
|
_pendingAgentGroup = null;
|
|
4693
|
-
|
|
4357
|
+
_activityGroup;
|
|
4358
|
+
/** Guards the single `registerLiveComponent` call for the turn's block. */
|
|
4359
|
+
_activityGroupRegistered = false;
|
|
4360
|
+
/** Block that borrowed a card, so late results refresh their own block. */
|
|
4361
|
+
_groupByToolCard = /* @__PURE__ */ new WeakMap();
|
|
4362
|
+
/**
|
|
4363
|
+
* Tools that keep their own transcript card instead of joining the turn's
|
|
4364
|
+
* activity block:
|
|
4365
|
+
* - `Agent` is owned by the agent group machinery (and by `/revoke`, which
|
|
4366
|
+
* walks those groups);
|
|
4367
|
+
* - `AskUserQuestion` returns early above and mounts its finished card;
|
|
4368
|
+
* - `ExitPlanMode` owns the plan box, which Ctrl+E expands by walking the
|
|
4369
|
+
* transcript container's direct children.
|
|
4370
|
+
*
|
|
4371
|
+
* Every other tool is a row inside the block, so a turn reads as one
|
|
4372
|
+
* timeline instead of a mix of cards and groups.
|
|
4373
|
+
*/
|
|
4374
|
+
static STANDALONE_TOOL_NAMES = new Set([
|
|
4375
|
+
"Agent",
|
|
4376
|
+
"AskUserQuestion",
|
|
4377
|
+
"ExitPlanMode",
|
|
4378
|
+
"ReadMediaFile"
|
|
4379
|
+
]);
|
|
4694
4380
|
constructor(host) {
|
|
4695
4381
|
this.host = host;
|
|
4696
4382
|
}
|
|
@@ -4771,9 +4457,6 @@ var StreamingUIController = class {
|
|
|
4771
4457
|
hasPendingAgentGroup() {
|
|
4772
4458
|
return this._pendingAgentGroup !== null;
|
|
4773
4459
|
}
|
|
4774
|
-
hasPendingReadGroup() {
|
|
4775
|
-
return this._pendingReadGroup !== null;
|
|
4776
|
-
}
|
|
4777
4460
|
removeToolComponentIfInactive(toolCallId) {
|
|
4778
4461
|
if (!this._activeToolCalls.has(toolCallId)) this._pendingToolComponents.delete(toolCallId);
|
|
4779
4462
|
}
|
|
@@ -4903,17 +4586,89 @@ var StreamingUIController = class {
|
|
|
4903
4586
|
}
|
|
4904
4587
|
/** Tears down replay-specific state after session history has been rendered. */
|
|
4905
4588
|
cleanupAfterReplay(completedToolCallIds) {
|
|
4589
|
+
this.endActivityGroup();
|
|
4906
4590
|
this._activeToolCalls.clear();
|
|
4907
4591
|
for (const toolCallId of completedToolCallIds) this._pendingToolComponents.delete(toolCallId);
|
|
4908
4592
|
this._pendingAgentGroup = null;
|
|
4909
|
-
this._pendingReadGroup = null;
|
|
4910
|
-
this._pendingReadGroupComponents.clear();
|
|
4911
4593
|
this._currentTurnId = void 0;
|
|
4912
4594
|
this._currentStep = 0;
|
|
4913
4595
|
this._streamingToolCallArguments.clear();
|
|
4914
4596
|
this.pendingToolCallFlushIds.clear();
|
|
4915
4597
|
this.host.state.ui.requestRender();
|
|
4916
4598
|
}
|
|
4599
|
+
/**
|
|
4600
|
+
* Returns the turn's block, creating it on demand: a block only ever starts
|
|
4601
|
+
* where work starts (reasoning or a tool call), which keeps it positioned
|
|
4602
|
+
* right after whatever the assistant last said.
|
|
4603
|
+
*
|
|
4604
|
+
* Live turns spin, replayed turns (history) do not — the streaming phase is
|
|
4605
|
+
* the discriminator, so a second block inside the same live turn still spins.
|
|
4606
|
+
*/
|
|
4607
|
+
ensureActivityGroup() {
|
|
4608
|
+
const existing = this._activityGroup;
|
|
4609
|
+
if (existing !== void 0) return existing;
|
|
4610
|
+
const { state } = this.host;
|
|
4611
|
+
const group = new ActivityGroupComponent(state.theme.colors, state.ui);
|
|
4612
|
+
group.setRunning(state.appState.streamingPhase !== "idle");
|
|
4613
|
+
group.setExpanded(state.toolOutputExpanded);
|
|
4614
|
+
this._activityGroup = group;
|
|
4615
|
+
state.transcriptContainer.addChild(group);
|
|
4616
|
+
state.ui.requestRender();
|
|
4617
|
+
return group;
|
|
4618
|
+
}
|
|
4619
|
+
/**
|
|
4620
|
+
* Seals the current block as soon as the assistant starts writing visible
|
|
4621
|
+
* text, so every stretch of work stays next to the answer it produced instead
|
|
4622
|
+
* of the whole turn's process piling up above the prose.
|
|
4623
|
+
*/
|
|
4624
|
+
sealActivityGroupOnText(text) {
|
|
4625
|
+
if (this._activityGroup === void 0) return;
|
|
4626
|
+
if (text.trim().length === 0) return;
|
|
4627
|
+
this.endActivityGroup();
|
|
4628
|
+
}
|
|
4629
|
+
/**
|
|
4630
|
+
* Registers the block against the first entry it owns, so history folding can
|
|
4631
|
+
* later sweep the whole block as one entry. Marked pending so a growing block
|
|
4632
|
+
* is never folded mid-turn.
|
|
4633
|
+
*/
|
|
4634
|
+
registerActivityGroupEntry(group, entry) {
|
|
4635
|
+
if (this._activityGroupRegistered) return;
|
|
4636
|
+
this._activityGroupRegistered = true;
|
|
4637
|
+
this.host.transcriptController.registerLiveComponent(group, entry);
|
|
4638
|
+
this.host.transcriptController.markPending(group);
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Settles the block: stops the spinner, drops one that never got content, and
|
|
4642
|
+
* releases the reference so the next turn opens a fresh one. Blocks with
|
|
4643
|
+
* content stay mounted (replayed history keeps its blocks).
|
|
4644
|
+
*/
|
|
4645
|
+
/**
|
|
4646
|
+
* Files a background task notice (started / completed / failed) as a step of
|
|
4647
|
+
* the turn that spawned it. Returns false when no block is open — a background
|
|
4648
|
+
* task that outlives its turn keeps its own row instead of reopening a settled
|
|
4649
|
+
* block — so the caller can fall back to mounting it on its own.
|
|
4650
|
+
*/
|
|
4651
|
+
attachNotice(data) {
|
|
4652
|
+
const group = this._activityGroup;
|
|
4653
|
+
if (group === void 0) return false;
|
|
4654
|
+
group.attachNotice(data);
|
|
4655
|
+
return true;
|
|
4656
|
+
}
|
|
4657
|
+
endActivityGroup() {
|
|
4658
|
+
const group = this._activityGroup;
|
|
4659
|
+
this._activityGroup = void 0;
|
|
4660
|
+
this._activityGroupRegistered = false;
|
|
4661
|
+
if (group === void 0) return;
|
|
4662
|
+
group.setRunning(false);
|
|
4663
|
+
if (group.isEmpty()) {
|
|
4664
|
+
this.host.transcriptController.unmarkPending(group);
|
|
4665
|
+
this.host.state.transcriptContainer.removeChild(group);
|
|
4666
|
+
group.dispose();
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
this.host.transcriptController.unmarkPending(group);
|
|
4670
|
+
group.dispose();
|
|
4671
|
+
}
|
|
4917
4672
|
disposeActiveThinkingComponent() {
|
|
4918
4673
|
if (this._activeThinkingComponent !== void 0) {
|
|
4919
4674
|
this._activeThinkingComponent.dispose();
|
|
@@ -4988,7 +4743,9 @@ var StreamingUIController = class {
|
|
|
4988
4743
|
const draft = this._assistantDraft;
|
|
4989
4744
|
if (this._shownAssistantLength >= draft.length) return this._shownAssistantLength;
|
|
4990
4745
|
const measured = getSharedSpeedTracker().getSpeed();
|
|
4991
|
-
const
|
|
4746
|
+
const tokPerSec = measured > 0 ? measured : 50;
|
|
4747
|
+
const requested = charsForTokenBudget(draft.slice(this._shownAssistantLength), tokPerSec * (50 / 1e3));
|
|
4748
|
+
const budget = Math.min(25, Math.max(1, requested));
|
|
4992
4749
|
this._shownAssistantLength = Math.min(draft.length, this._shownAssistantLength + budget);
|
|
4993
4750
|
return this._shownAssistantLength;
|
|
4994
4751
|
}
|
|
@@ -5031,7 +4788,6 @@ var StreamingUIController = class {
|
|
|
5031
4788
|
this._streamingToolCallArguments.clear();
|
|
5032
4789
|
this.disposeAndClearPendingToolComponents();
|
|
5033
4790
|
this._pendingAgentGroup = null;
|
|
5034
|
-
this._pendingReadGroup = null;
|
|
5035
4791
|
}
|
|
5036
4792
|
resetToolCallState() {
|
|
5037
4793
|
this._activeToolCalls.clear();
|
|
@@ -5052,6 +4808,7 @@ var StreamingUIController = class {
|
|
|
5052
4808
|
const turnStartTime = this.turnStartAt !== 0 ? this.turnStartAt : state.appState.streamingStartTime;
|
|
5053
4809
|
const completedTurnKey = this._currentTurnId ?? `local:${String(turnStartTime)}`;
|
|
5054
4810
|
this.finalizeLiveTextBuffers();
|
|
4811
|
+
this.endActivityGroup();
|
|
5055
4812
|
this.resetToolCallState();
|
|
5056
4813
|
this._currentTurnId = void 0;
|
|
5057
4814
|
const next = this.host.shiftQueuedMessage();
|
|
@@ -5099,7 +4856,6 @@ var StreamingUIController = class {
|
|
|
5099
4856
|
onStreamingTextStart() {
|
|
5100
4857
|
const { state } = this.host;
|
|
5101
4858
|
this._pendingAgentGroup = null;
|
|
5102
|
-
this._pendingReadGroup = null;
|
|
5103
4859
|
const entry = {
|
|
5104
4860
|
id: nextTranscriptId(),
|
|
5105
4861
|
kind: "assistant",
|
|
@@ -5125,6 +4881,7 @@ var StreamingUIController = class {
|
|
|
5125
4881
|
block.component.updateContent(fullText);
|
|
5126
4882
|
this.host.state.ui.requestRender();
|
|
5127
4883
|
}
|
|
4884
|
+
this.sealActivityGroupOnText(fullText);
|
|
5128
4885
|
}
|
|
5129
4886
|
onStreamingTextEnd() {
|
|
5130
4887
|
const block = this._streamingBlock;
|
|
@@ -5139,8 +4896,7 @@ var StreamingUIController = class {
|
|
|
5139
4896
|
const { state } = this.host;
|
|
5140
4897
|
if (this._activeThinkingComponent === void 0) {
|
|
5141
4898
|
this._pendingAgentGroup = null;
|
|
5142
|
-
|
|
5143
|
-
const component = new ThinkingComponent(fullText, state.theme.colors, true, "live", state.ui);
|
|
4899
|
+
const component = new ThinkingComponent(fullText, state.theme.colors, true, "finalized", state.ui);
|
|
5144
4900
|
const entry = {
|
|
5145
4901
|
id: nextTranscriptId(),
|
|
5146
4902
|
kind: "thinking",
|
|
@@ -5153,29 +4909,30 @@ var StreamingUIController = class {
|
|
|
5153
4909
|
this.host.pushTranscriptEntry(entry);
|
|
5154
4910
|
this.host.transcriptController.registerLiveComponent(component, entry);
|
|
5155
4911
|
this.host.transcriptController.markPending(component);
|
|
5156
|
-
|
|
5157
|
-
|
|
4912
|
+
const group = this.ensureActivityGroup();
|
|
4913
|
+
this.registerActivityGroupEntry(group, entry);
|
|
4914
|
+
group.appendThinking(fullText, true);
|
|
5158
4915
|
} else {
|
|
5159
4916
|
this._activeThinkingComponent.setText(fullText);
|
|
5160
4917
|
if (this._thinkingEntry !== void 0) this._thinkingEntry.content = fullText;
|
|
4918
|
+
this.ensureActivityGroup().appendThinking(fullText, true);
|
|
5161
4919
|
}
|
|
5162
4920
|
state.ui.requestRender();
|
|
5163
4921
|
}
|
|
5164
4922
|
onThinkingEnd() {
|
|
5165
4923
|
if (this._activeThinkingComponent === void 0) return;
|
|
5166
|
-
this._activeThinkingComponent
|
|
5167
|
-
|
|
4924
|
+
const component = this._activeThinkingComponent;
|
|
4925
|
+
component.finalize();
|
|
4926
|
+
this.host.transcriptController.unmarkPending(component);
|
|
5168
4927
|
this.host.transcriptController.commit();
|
|
4928
|
+
this.host.transcriptController.releaseLiveComponent(component);
|
|
5169
4929
|
this._activeThinkingComponent = void 0;
|
|
5170
4930
|
this._thinkingEntry = void 0;
|
|
4931
|
+
this._activityGroup?.endThinking();
|
|
5171
4932
|
this.host.state.ui.requestRender();
|
|
5172
4933
|
}
|
|
5173
4934
|
onToolCallStart(toolCall) {
|
|
5174
4935
|
if (toolCall.name === "AskUserQuestion") return;
|
|
5175
|
-
if (toolCall.name === "ReadGroup") {
|
|
5176
|
-
this.startReadGroupToolCall(toolCall);
|
|
5177
|
-
return;
|
|
5178
|
-
}
|
|
5179
4936
|
const { state } = this.host;
|
|
5180
4937
|
const tc = new ToolCallComponent(toolCall, void 0, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
|
|
5181
4938
|
tc.setPermissionMode(state.appState.permissionMode);
|
|
@@ -5191,15 +4948,18 @@ var StreamingUIController = class {
|
|
|
5191
4948
|
this.host.pushTranscriptEntry(entry);
|
|
5192
4949
|
this.host.transcriptController.registerLiveComponent(tc, entry);
|
|
5193
4950
|
this.host.transcriptController.markPending(tc);
|
|
5194
|
-
if (state.toolOutputExpanded) tc.setExpanded(true);
|
|
5195
4951
|
if (state.planExpanded) tc.setPlanExpanded(true);
|
|
5196
4952
|
if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
|
|
5197
|
-
if (toolCall
|
|
5198
|
-
|
|
5199
|
-
if (!handled) handled = this.tryAttachReadToolCall(toolCall, tc);
|
|
5200
|
-
if (!handled) {
|
|
4953
|
+
if (!this.tryAttachAgentToolCall(toolCall, tc)) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
|
|
4954
|
+
tc.setExpanded(state.toolOutputExpanded);
|
|
5201
4955
|
state.transcriptContainer.addChild(tc);
|
|
5202
4956
|
state.ui.requestRender();
|
|
4957
|
+
} else {
|
|
4958
|
+
const group = this.ensureActivityGroup();
|
|
4959
|
+
this.registerActivityGroupEntry(group, entry);
|
|
4960
|
+
this._groupByToolCard.set(tc, group);
|
|
4961
|
+
group.attachTool(tc, toolCall.step ?? this._currentStep);
|
|
4962
|
+
state.ui.requestRender();
|
|
5203
4963
|
}
|
|
5204
4964
|
if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
|
|
5205
4965
|
const session = this.host.requireSession();
|
|
@@ -5216,44 +4976,16 @@ var StreamingUIController = class {
|
|
|
5216
4976
|
})();
|
|
5217
4977
|
}
|
|
5218
4978
|
}
|
|
5219
|
-
startReadGroupToolCall(toolCall) {
|
|
5220
|
-
const { state, transcriptController } = this.host;
|
|
5221
|
-
const rgc = new ReadGroupComponent(state.theme.colors, state.ui);
|
|
5222
|
-
const entry = {
|
|
5223
|
-
id: nextTranscriptId(),
|
|
5224
|
-
kind: "tool_call",
|
|
5225
|
-
turnId: toolCall.turnId ?? this._currentTurnId,
|
|
5226
|
-
renderMode: "plain",
|
|
5227
|
-
content: toolCall.name,
|
|
5228
|
-
toolCallData: toolCall
|
|
5229
|
-
};
|
|
5230
|
-
this._pendingReadGroupComponents.set(toolCall.id, rgc);
|
|
5231
|
-
this._pendingReadGroup = null;
|
|
5232
|
-
this._pendingAgentGroup = null;
|
|
5233
|
-
this.host.pushTranscriptEntry(entry);
|
|
5234
|
-
transcriptController.registerLiveComponent(rgc, entry);
|
|
5235
|
-
transcriptController.markPending(rgc);
|
|
5236
|
-
state.transcriptContainer.addChild(rgc);
|
|
5237
|
-
state.ui.requestRender();
|
|
5238
|
-
}
|
|
5239
4979
|
onToolCallEnd(toolCallId, result) {
|
|
5240
4980
|
const { state } = this.host;
|
|
5241
|
-
const rgc = this._pendingReadGroupComponents.get(toolCallId);
|
|
5242
|
-
if (rgc !== void 0) {
|
|
5243
|
-
rgc.setResults(parseReadGroupOutput(result.output));
|
|
5244
|
-
this.host.transcriptController.unmarkPending(rgc);
|
|
5245
|
-
this._pendingReadGroupComponents.delete(toolCallId);
|
|
5246
|
-
const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
|
|
5247
|
-
if (entry?.toolCallData !== void 0) entry.toolCallData.result = result;
|
|
5248
|
-
this.host.transcriptController.commit();
|
|
5249
|
-
state.ui.requestRender();
|
|
5250
|
-
return;
|
|
5251
|
-
}
|
|
5252
4981
|
const matchedCall = this._activeToolCalls.get(toolCallId);
|
|
5253
4982
|
const tc = this._pendingToolComponents.get(toolCallId);
|
|
5254
4983
|
if (tc) {
|
|
5255
4984
|
tc.setResult(result);
|
|
5256
4985
|
tc.dispose();
|
|
4986
|
+
const owner = this._groupByToolCard.get(tc) ?? this._activityGroup;
|
|
4987
|
+
owner?.notifyToolChanged();
|
|
4988
|
+
if (owner !== void 0 && this._groupByToolCard.has(tc)) this.host.transcriptController.releaseLiveComponent(tc);
|
|
5257
4989
|
this.host.transcriptController.unmarkPending(tc);
|
|
5258
4990
|
this._pendingToolComponents.delete(toolCallId);
|
|
5259
4991
|
const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
|
|
@@ -5265,7 +4997,7 @@ var StreamingUIController = class {
|
|
|
5265
4997
|
if (matchedCall?.name === "AskUserQuestion") {
|
|
5266
4998
|
const completed = new ToolCallComponent(matchedCall, result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
|
|
5267
4999
|
completed.setPermissionMode(state.appState.permissionMode);
|
|
5268
|
-
|
|
5000
|
+
completed.setExpanded(state.toolOutputExpanded);
|
|
5269
5001
|
if (state.planExpanded) completed.setPlanExpanded(true);
|
|
5270
5002
|
const entry = {
|
|
5271
5003
|
id: nextTranscriptId(),
|
|
@@ -5374,6 +5106,7 @@ var StreamingUIController = class {
|
|
|
5374
5106
|
turnId,
|
|
5375
5107
|
solo: tc
|
|
5376
5108
|
};
|
|
5109
|
+
tc.setExpanded(state.toolOutputExpanded);
|
|
5377
5110
|
state.transcriptContainer.addChild(tc);
|
|
5378
5111
|
state.ui.requestRender();
|
|
5379
5112
|
return true;
|
|
@@ -5420,69 +5153,6 @@ var StreamingUIController = class {
|
|
|
5420
5153
|
}
|
|
5421
5154
|
return group;
|
|
5422
5155
|
}
|
|
5423
|
-
tryAttachReadToolCall(toolCall, tc) {
|
|
5424
|
-
const { state } = this.host;
|
|
5425
|
-
if (toolCall.name !== "Read") {
|
|
5426
|
-
this._pendingReadGroup = null;
|
|
5427
|
-
return false;
|
|
5428
|
-
}
|
|
5429
|
-
const step = toolCall.step ?? this._currentStep;
|
|
5430
|
-
const turnId = toolCall.turnId ?? this._currentTurnId;
|
|
5431
|
-
const pending = this._pendingReadGroup;
|
|
5432
|
-
if (pending !== null && (pending.step !== step || pending.turnId !== turnId)) this._pendingReadGroup = null;
|
|
5433
|
-
const cur = this._pendingReadGroup;
|
|
5434
|
-
if (cur === null) {
|
|
5435
|
-
this._pendingReadGroup = {
|
|
5436
|
-
step,
|
|
5437
|
-
turnId,
|
|
5438
|
-
solo: tc
|
|
5439
|
-
};
|
|
5440
|
-
state.transcriptContainer.addChild(tc);
|
|
5441
|
-
state.ui.requestRender();
|
|
5442
|
-
return true;
|
|
5443
|
-
}
|
|
5444
|
-
if (cur.group !== void 0) {
|
|
5445
|
-
cur.group.attach(toolCall.id, tc);
|
|
5446
|
-
return true;
|
|
5447
|
-
}
|
|
5448
|
-
const solo = cur.solo;
|
|
5449
|
-
if (solo === void 0) {
|
|
5450
|
-
this._pendingReadGroup = {
|
|
5451
|
-
step,
|
|
5452
|
-
turnId,
|
|
5453
|
-
solo: tc
|
|
5454
|
-
};
|
|
5455
|
-
state.transcriptContainer.addChild(tc);
|
|
5456
|
-
state.ui.requestRender();
|
|
5457
|
-
return true;
|
|
5458
|
-
}
|
|
5459
|
-
const group = this.upgradeSoloReadToGroup(solo);
|
|
5460
|
-
group.attach(toolCall.id, tc);
|
|
5461
|
-
this._pendingReadGroup = {
|
|
5462
|
-
step,
|
|
5463
|
-
turnId,
|
|
5464
|
-
group
|
|
5465
|
-
};
|
|
5466
|
-
state.ui.requestRender();
|
|
5467
|
-
return true;
|
|
5468
|
-
}
|
|
5469
|
-
upgradeSoloReadToGroup(solo) {
|
|
5470
|
-
const { state, transcriptController } = this.host;
|
|
5471
|
-
const group = new ReadGroupComponent(state.theme.colors, state.ui);
|
|
5472
|
-
const children = state.transcriptContainer.children;
|
|
5473
|
-
const idx = children.indexOf(solo);
|
|
5474
|
-
if (idx >= 0) {
|
|
5475
|
-
children[idx] = group;
|
|
5476
|
-
state.transcriptContainer.invalidate();
|
|
5477
|
-
} else state.transcriptContainer.addChild(group);
|
|
5478
|
-
group.attach(solo.toolCallView.id, solo);
|
|
5479
|
-
const entry = transcriptController.findEntryForComponent(solo);
|
|
5480
|
-
if (entry !== void 0) {
|
|
5481
|
-
transcriptController.registerLiveComponent(group, entry);
|
|
5482
|
-
transcriptController.unmarkPending(solo);
|
|
5483
|
-
}
|
|
5484
|
-
return group;
|
|
5485
|
-
}
|
|
5486
5156
|
};
|
|
5487
5157
|
//#endregion
|
|
5488
5158
|
//#region src/tui/components/dialogs/task-output-viewer.ts
|
|
@@ -6617,6 +6287,14 @@ var TranscriptController = class TranscriptController {
|
|
|
6617
6287
|
registerLiveComponent(component, entry) {
|
|
6618
6288
|
this.liveComponentToEntry.set(component, entry);
|
|
6619
6289
|
}
|
|
6290
|
+
/**
|
|
6291
|
+
* Drops the entry mapping of a component that will never be folded into the
|
|
6292
|
+
* committed history (it is not a container child), so the map cannot retain
|
|
6293
|
+
* components — and the text they hold — for the life of the session.
|
|
6294
|
+
*/
|
|
6295
|
+
releaseLiveComponent(component) {
|
|
6296
|
+
this.liveComponentToEntry.delete(component);
|
|
6297
|
+
}
|
|
6620
6298
|
markPending(component) {
|
|
6621
6299
|
this.pendingComponents.add(component);
|
|
6622
6300
|
}
|
|
@@ -6684,20 +6362,11 @@ var TranscriptController = class TranscriptController {
|
|
|
6684
6362
|
component.updateContent(entry.content);
|
|
6685
6363
|
return component;
|
|
6686
6364
|
}
|
|
6687
|
-
case "thinking":
|
|
6688
|
-
const thinking = new ThinkingComponent(entry.content, state.theme.colors, true);
|
|
6689
|
-
if (state.toolOutputExpanded) thinking.setExpanded(true);
|
|
6690
|
-
return thinking;
|
|
6691
|
-
}
|
|
6365
|
+
case "thinking": return new ThinkingComponent(entry.content, state.theme.colors, true);
|
|
6692
6366
|
case "tool_call":
|
|
6693
|
-
|
|
6694
|
-
const rgc = new ReadGroupComponent(state.theme.colors, state.ui);
|
|
6695
|
-
rgc.setResults(parseReadGroupOutput(entry.toolCallData.result.output));
|
|
6696
|
-
return rgc;
|
|
6697
|
-
}
|
|
6367
|
+
case "tool_result":
|
|
6698
6368
|
if (entry.toolCallData) {
|
|
6699
6369
|
const tc = new ToolCallComponent(entry.toolCallData, entry.toolCallData.result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
|
|
6700
|
-
if (state.toolOutputExpanded) tc.setExpanded(true);
|
|
6701
6370
|
if (state.planExpanded) tc.setPlanExpanded(true);
|
|
6702
6371
|
return tc;
|
|
6703
6372
|
}
|
|
@@ -6718,7 +6387,9 @@ var TranscriptController = class TranscriptController {
|
|
|
6718
6387
|
const component = this.createComponent(entry);
|
|
6719
6388
|
if (component) {
|
|
6720
6389
|
this.liveComponentToEntry.set(component, entry);
|
|
6721
|
-
|
|
6390
|
+
const status = entry.backgroundAgentStatus;
|
|
6391
|
+
if (status !== void 0 && this.host.streamingUI.attachNotice(status)) this.releaseLiveComponent(component);
|
|
6392
|
+
else this.host.state.transcriptContainer.addChild(component);
|
|
6722
6393
|
this.host.state.ui.requestRender();
|
|
6723
6394
|
}
|
|
6724
6395
|
return component ?? null;
|
|
@@ -6781,6 +6452,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6781
6452
|
clearAndRedraw() {
|
|
6782
6453
|
const { state, streamingUI, imageStore } = this.host;
|
|
6783
6454
|
streamingUI.discardPending();
|
|
6455
|
+
streamingUI.endActivityGroup();
|
|
6784
6456
|
state.transcriptEntries = [];
|
|
6785
6457
|
streamingUI.disposeActiveCompactionBlock();
|
|
6786
6458
|
streamingUI.resetLiveText();
|
|
@@ -6790,6 +6462,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6790
6462
|
this.committedComponent = void 0;
|
|
6791
6463
|
this.liveComponentToEntry.clear();
|
|
6792
6464
|
this.pendingComponents.clear();
|
|
6465
|
+
state.toolOutputExpanded = false;
|
|
6793
6466
|
disposeChildren(state.transcriptContainer);
|
|
6794
6467
|
this.clearTerminalInlineImages();
|
|
6795
6468
|
state.todoPanel.clear();
|
|
@@ -6837,7 +6510,15 @@ var TranscriptController = class TranscriptController {
|
|
|
6837
6510
|
}
|
|
6838
6511
|
toggleToolOutputExpansion() {
|
|
6839
6512
|
const { state } = this.host;
|
|
6840
|
-
|
|
6513
|
+
const next = !state.toolOutputExpanded;
|
|
6514
|
+
const children = state.transcriptContainer.children;
|
|
6515
|
+
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
6516
|
+
const child = children[i];
|
|
6517
|
+
if (child === void 0) continue;
|
|
6518
|
+
if (child instanceof UserMessageComponent || child instanceof SkillActivationComponent || child instanceof CronMessageComponent || child instanceof CommittedTranscriptComponent) break;
|
|
6519
|
+
if (isExpandable(child)) child.setExpanded(next);
|
|
6520
|
+
}
|
|
6521
|
+
state.toolOutputExpanded = next;
|
|
6841
6522
|
state.ui.requestRender();
|
|
6842
6523
|
}
|
|
6843
6524
|
togglePlanExpansion() {
|
|
@@ -7058,17 +6739,14 @@ function formatCount$1(n) {
|
|
|
7058
6739
|
return n.toLocaleString("en-US");
|
|
7059
6740
|
}
|
|
7060
6741
|
/**
|
|
7061
|
-
* Compact token form for narrow sidebars: 2,267,680,102 → `
|
|
7062
|
-
*
|
|
7063
|
-
*
|
|
6742
|
+
* Compact token form for narrow sidebars: 2,267,680,102 → `2267.7M`. One unit
|
|
6743
|
+
* ladder for every locale (K below a million, M above) so the value column
|
|
6744
|
+
* stays comparable across languages: Chinese no longer switches to the
|
|
6745
|
+
* ten-thousand / hundred-million units it used before.
|
|
6746
|
+
* Used only when the thousands-separated form cannot fit, so a narrow sidebar
|
|
6747
|
+
* keeps every value readable instead of cutting digits.
|
|
7064
6748
|
*/
|
|
7065
6749
|
function formatCompact(n) {
|
|
7066
|
-
if (getLocale() === "zh") {
|
|
7067
|
-
if (n >= 9995e4) return `${(n / 1e8).toFixed(1)}亿`;
|
|
7068
|
-
if (n >= 9500) return `${Math.round(n / 1e4)}万`;
|
|
7069
|
-
return String(n);
|
|
7070
|
-
}
|
|
7071
|
-
if (n >= 9995e5) return `${(n / 1e9).toFixed(1)}B`;
|
|
7072
6750
|
if (n >= 999500) return `${(n / 1e6).toFixed(1)}M`;
|
|
7073
6751
|
if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
|
|
7074
6752
|
return String(n);
|
|
@@ -7136,6 +6814,10 @@ const sessionPanel = {
|
|
|
7136
6814
|
* with the 1s sidebar heartbeat each tick advances half the cycle, so the
|
|
7137
6815
|
* colour jump is clearly visible instead of drifting imperceptibly. */
|
|
7138
6816
|
const AGENT_GRADIENT_CYCLE_MS = 2e3;
|
|
6817
|
+
/** Display form of a slot type: 'coder' reads as 'Coder' in the panel. */
|
|
6818
|
+
function displayType(type) {
|
|
6819
|
+
return type.length === 0 ? type : type.charAt(0).toUpperCase() + type.slice(1);
|
|
6820
|
+
}
|
|
7139
6821
|
/** Localized status word per slot state. */
|
|
7140
6822
|
function statusText(status) {
|
|
7141
6823
|
switch (status) {
|
|
@@ -7165,7 +6847,7 @@ var AgentsPanelContent = class {
|
|
|
7165
6847
|
if (agents === void 0 || agents.length === 0) return [];
|
|
7166
6848
|
return layoutSidebarGrid(agents.map((slot) => ({
|
|
7167
6849
|
marker: slot.status === "idle" ? "○" : "●",
|
|
7168
|
-
label: slot.type,
|
|
6850
|
+
label: displayType(slot.type),
|
|
7169
6851
|
value: statusText(slot.status) + (slot.count > 1 ? ` ×${slot.count}` : "")
|
|
7170
6852
|
})), width).map((cell, index) => this.renderSlot(cell, agents[index], index, width));
|
|
7171
6853
|
}
|
|
@@ -7321,20 +7003,23 @@ const HUB_MODEL_STALE_MS = 5 * 6e4;
|
|
|
7321
7003
|
const HUB_ENDPOINTS = Object.freeze([
|
|
7322
7004
|
{
|
|
7323
7005
|
id: "github",
|
|
7324
|
-
url: "https://github.com/"
|
|
7006
|
+
url: "https://github.com/",
|
|
7007
|
+
label: "Github"
|
|
7325
7008
|
},
|
|
7326
7009
|
{
|
|
7327
7010
|
id: "google",
|
|
7328
|
-
url: "https://www.google.com/"
|
|
7011
|
+
url: "https://www.google.com/",
|
|
7012
|
+
label: "Google"
|
|
7329
7013
|
},
|
|
7330
7014
|
{
|
|
7331
7015
|
id: "x",
|
|
7332
7016
|
url: "https://x.com/",
|
|
7333
|
-
label: "
|
|
7017
|
+
label: "X (Twitter)"
|
|
7334
7018
|
},
|
|
7335
7019
|
{
|
|
7336
7020
|
id: "huggingface",
|
|
7337
|
-
url: "https://huggingface.co/"
|
|
7021
|
+
url: "https://huggingface.co/",
|
|
7022
|
+
label: "Huggingface"
|
|
7338
7023
|
},
|
|
7339
7024
|
{
|
|
7340
7025
|
id: "alibaba",
|
|
@@ -8488,6 +8173,7 @@ var LifecycleController = class LifecycleController {
|
|
|
8488
8173
|
case "waiting":
|
|
8489
8174
|
case "composing":
|
|
8490
8175
|
case "tool":
|
|
8176
|
+
case "thinking":
|
|
8491
8177
|
{
|
|
8492
8178
|
const loader = new PulseWaveLoader(state.ui, state.theme.colors.primary);
|
|
8493
8179
|
this.statusBarLoader = loader;
|
|
@@ -8498,7 +8184,6 @@ var LifecycleController = class LifecycleController {
|
|
|
8498
8184
|
}));
|
|
8499
8185
|
}
|
|
8500
8186
|
break;
|
|
8501
|
-
case "thinking": break;
|
|
8502
8187
|
case "idle":
|
|
8503
8188
|
case "hidden":
|
|
8504
8189
|
if (mode === "idle" && state.transcriptEntries.length === 0 && !isEmptySessionHintDismissed()) {
|
|
@@ -13977,6 +13662,7 @@ var ScreamTUI = class {
|
|
|
13977
13662
|
beginSessionRequest() {
|
|
13978
13663
|
this.streamingUI.markTurnStarted();
|
|
13979
13664
|
this.streamingUI.setTurnId(void 0);
|
|
13665
|
+
this.streamingUI.endActivityGroup();
|
|
13980
13666
|
this.streamingUI.resetLiveText();
|
|
13981
13667
|
this.streamingUI.resetToolUi();
|
|
13982
13668
|
this.streamingUI.resetToolCallState();
|
|
@@ -13988,6 +13674,7 @@ var ScreamTUI = class {
|
|
|
13988
13674
|
}
|
|
13989
13675
|
failSessionRequest(message) {
|
|
13990
13676
|
this.setAppState({ streamingPhase: "idle" });
|
|
13677
|
+
this.streamingUI.endActivityGroup();
|
|
13991
13678
|
this.resetLivePane();
|
|
13992
13679
|
this.showError(message);
|
|
13993
13680
|
}
|
|
@@ -14195,7 +13882,7 @@ var ScreamTUI = class {
|
|
|
14195
13882
|
const card = new OnboardingCardComponent({
|
|
14196
13883
|
colors: this.state.theme.colors,
|
|
14197
13884
|
onConfigure: () => {
|
|
14198
|
-
import("./dispatch-
|
|
13885
|
+
import("./dispatch-BGLqpKHG.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
14199
13886
|
this.restoreEditor();
|
|
14200
13887
|
onDone?.();
|
|
14201
13888
|
});
|
|
@@ -15864,6 +15551,53 @@ async function persistKey(filePath, record) {
|
|
|
15864
15551
|
await rename(tmp, filePath);
|
|
15865
15552
|
}
|
|
15866
15553
|
//#endregion
|
|
15554
|
+
//#region src/web/journal-writer.ts
|
|
15555
|
+
/**
|
|
15556
|
+
* Serialization gate for journal appends.
|
|
15557
|
+
*
|
|
15558
|
+
* Why it exists (an incident observed in the field): the session journal is an
|
|
15559
|
+
* append-only file holding one JSON record per line, and lines used to be
|
|
15560
|
+
* appended with `writeFile(path, line + '\n', { flag: 'a' })`. A
|
|
15561
|
+
* `web.message.finalized` entry carries an entire assistant body (hundreds of
|
|
15562
|
+
* thousands of characters in practice) while small entries such as turn.ended
|
|
15563
|
+
* are written at the same time — the two writes interleaved at the byte level,
|
|
15564
|
+
* so what landed on disk were malformed lines with the two records spliced
|
|
15565
|
+
* into each other:
|
|
15566
|
+
*
|
|
15567
|
+
* ..."content":"...{"type":"journal","seq":38354,... ← another record spliced into the body
|
|
15568
|
+
*
|
|
15569
|
+
* Consequence: the whole line fails JSON.parse and the message disappears from
|
|
15570
|
+
* the UI (the reader can only skip it), which the user perceives as a garbled
|
|
15571
|
+
* or lost body. Queueing the writes inside the process eliminates the
|
|
15572
|
+
* interleaving; the append itself still goes through appendFile (O_APPEND), so
|
|
15573
|
+
* a single write is never split by another fd.
|
|
15574
|
+
*
|
|
15575
|
+
* The queue is keyed by file path and dropped once drained, so a long-running
|
|
15576
|
+
* process does not grow it without bound. No cross-process writer exists today
|
|
15577
|
+
* (only the web server writes the journal); if one appears, this needs a
|
|
15578
|
+
* file-lock based scheme instead.
|
|
15579
|
+
*/
|
|
15580
|
+
const writeChains = /* @__PURE__ */ new Map();
|
|
15581
|
+
/**
|
|
15582
|
+
* Append one line (plus its newline) to the journal; concurrent calls for the
|
|
15583
|
+
* same file land in arrival order.
|
|
15584
|
+
*/
|
|
15585
|
+
async function appendJournalLineSerialized(path, line, onError) {
|
|
15586
|
+
const next = (writeChains.get(path) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
15587
|
+
try {
|
|
15588
|
+
await appendFile(path, line + "\n");
|
|
15589
|
+
} catch (error) {
|
|
15590
|
+
onError(error);
|
|
15591
|
+
}
|
|
15592
|
+
});
|
|
15593
|
+
writeChains.set(path, next);
|
|
15594
|
+
try {
|
|
15595
|
+
await next;
|
|
15596
|
+
} finally {
|
|
15597
|
+
if (writeChains.get(path) === next) writeChains.delete(path);
|
|
15598
|
+
}
|
|
15599
|
+
}
|
|
15600
|
+
//#endregion
|
|
15867
15601
|
//#region src/web/rateLimit.ts
|
|
15868
15602
|
function createFixedWindowLimiter(opts) {
|
|
15869
15603
|
const { windowMs, max } = opts;
|
|
@@ -16431,14 +16165,12 @@ async function ensureSessionsDir(homeDir) {
|
|
|
16431
16165
|
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
|
|
16432
16166
|
}
|
|
16433
16167
|
async function appendJournalLine(homeDir, sessionId, line) {
|
|
16434
|
-
|
|
16435
|
-
await writeFile(getJournalPath(homeDir, sessionId), line + "\n", { flag: "a" });
|
|
16436
|
-
} catch (error) {
|
|
16168
|
+
await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
|
|
16437
16169
|
log.warn("web: failed to persist journal line", {
|
|
16438
16170
|
sessionId,
|
|
16439
16171
|
error: String(error)
|
|
16440
16172
|
});
|
|
16441
|
-
}
|
|
16173
|
+
});
|
|
16442
16174
|
}
|
|
16443
16175
|
let metaWriteSeq = 0;
|
|
16444
16176
|
async function saveMetadata(homeDir, meta) {
|
|
@@ -16465,17 +16197,78 @@ async function loadMetadata(homeDir, sessionId) {
|
|
|
16465
16197
|
async function loadJournal(homeDir, sessionId) {
|
|
16466
16198
|
try {
|
|
16467
16199
|
const lines = (await readFile(getJournalPath(homeDir, sessionId), "utf-8")).split("\n").filter((l) => l.trim().length > 0);
|
|
16468
|
-
const
|
|
16200
|
+
const entries = [];
|
|
16201
|
+
let corrupt = 0;
|
|
16469
16202
|
for (const line of lines) try {
|
|
16470
|
-
|
|
16203
|
+
entries.push(JSON.parse(line));
|
|
16471
16204
|
} catch {
|
|
16205
|
+
corrupt += 1;
|
|
16472
16206
|
log.warn("web: skipping corrupt journal line", { sessionId });
|
|
16473
16207
|
}
|
|
16474
|
-
return
|
|
16208
|
+
return {
|
|
16209
|
+
entries,
|
|
16210
|
+
corrupt
|
|
16211
|
+
};
|
|
16475
16212
|
} catch {
|
|
16476
|
-
return
|
|
16213
|
+
return {
|
|
16214
|
+
entries: [],
|
|
16215
|
+
corrupt: 0
|
|
16216
|
+
};
|
|
16477
16217
|
}
|
|
16478
16218
|
}
|
|
16219
|
+
/**
|
|
16220
|
+
* Event families that mutate the main transcript: only the main agent's
|
|
16221
|
+
* (`agentId === 'main'`) events of these kinds may be applied, journaled and
|
|
16222
|
+
* broadcast. Subagents and background tasks each emit their own turn.started
|
|
16223
|
+
* plus hundreds of text/tool events — mixing them into the main transcript
|
|
16224
|
+
* produces a body where two streams alternate block by block, blows the tool
|
|
16225
|
+
* list up to 97 entries and the model badge to 4 lines (reproduced from a real
|
|
16226
|
+
* session). Subagent presence is carried by subagent.* events instead, never by
|
|
16227
|
+
* the transcript.
|
|
16228
|
+
*/
|
|
16229
|
+
const MAIN_TRANSCRIPT_EVENTS = new Set([
|
|
16230
|
+
"turn.started",
|
|
16231
|
+
"turn.ended",
|
|
16232
|
+
"assistant.delta",
|
|
16233
|
+
"thinking.delta",
|
|
16234
|
+
"tool.call.started",
|
|
16235
|
+
"tool.result"
|
|
16236
|
+
]);
|
|
16237
|
+
/**
|
|
16238
|
+
* Whether a journal entry already carries a message (if so it needs no seeding
|
|
16239
|
+
* from core history).
|
|
16240
|
+
*/
|
|
16241
|
+
function isMessageBearingEntry(entry) {
|
|
16242
|
+
if ("type" in entry && entry.type === "user_message") return true;
|
|
16243
|
+
const payload = entry.payload;
|
|
16244
|
+
return payload.type === "web.message.finalized" || payload.type === "turn.started";
|
|
16245
|
+
}
|
|
16246
|
+
/**
|
|
16247
|
+
* Core context history → web ChatMessage seed.
|
|
16248
|
+
*
|
|
16249
|
+
* Only "real user input + assistant bodies" are carried over: core tool records
|
|
16250
|
+
* and the web-side ToolMessage are different shapes, so a forced mapping would
|
|
16251
|
+
* invent fake cards. Messages with a non-user origin (injections, system
|
|
16252
|
+
* triggers, background-task notices, skill-activation envelopes, compaction
|
|
16253
|
+
* summaries) are protocol content meant for the model and must not be rendered
|
|
16254
|
+
* as user turns.
|
|
16255
|
+
*/
|
|
16256
|
+
function contextHistoryToChatMessages(history) {
|
|
16257
|
+
const out = [];
|
|
16258
|
+
for (const message of history) {
|
|
16259
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
16260
|
+
if (isInternalMessage(message)) continue;
|
|
16261
|
+
if (message.origin !== void 0 && message.origin.kind !== "user") continue;
|
|
16262
|
+
const text = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
|
|
16263
|
+
if (text.length === 0) continue;
|
|
16264
|
+
out.push({
|
|
16265
|
+
role: message.role,
|
|
16266
|
+
content: text,
|
|
16267
|
+
tools: []
|
|
16268
|
+
});
|
|
16269
|
+
}
|
|
16270
|
+
return out;
|
|
16271
|
+
}
|
|
16479
16272
|
async function listPersistedSessions(homeDir) {
|
|
16480
16273
|
const dir = getSessionsDir(homeDir);
|
|
16481
16274
|
if (!existsSync(dir)) return [];
|
|
@@ -16653,10 +16446,11 @@ const MAX_INLINE_DIFF_BYTES = 512 * 1024;
|
|
|
16653
16446
|
* Single-file patch. `repoPath` is **repository-root relative** — the same base
|
|
16654
16447
|
* `git status` reports, so the UI can hand it straight back.
|
|
16655
16448
|
*
|
|
16656
|
-
* Two cases used to return an empty patch
|
|
16657
|
-
* nested files (the old code re-prefixed workdir-relative paths with
|
|
16658
|
-
* resolving them outside the repo) and untracked files (`git diff` has
|
|
16659
|
-
* to say about them). The second case now gets a synthesised added-file
|
|
16449
|
+
* Two cases used to return an empty patch, which read as "clicking did
|
|
16450
|
+
* nothing": nested files (the old code re-prefixed workdir-relative paths with
|
|
16451
|
+
* `../..`, resolving them outside the repo) and untracked files (`git diff` has
|
|
16452
|
+
* nothing to say about them). The second case now gets a synthesised added-file
|
|
16453
|
+
* patch.
|
|
16660
16454
|
*/
|
|
16661
16455
|
async function getGitFileDiff(workDir, repoPath) {
|
|
16662
16456
|
const rel = normalizeNumstatPath(repoPath);
|
|
@@ -16774,7 +16568,27 @@ var WebSession = class {
|
|
|
16774
16568
|
* snapshot (`web.message.finalized`) — the only source that survives a
|
|
16775
16569
|
* server restart (deltas are volatile by design).
|
|
16776
16570
|
*/
|
|
16777
|
-
|
|
16571
|
+
/**
|
|
16572
|
+
* In-flight main-agent turn messages, keyed by `turnId`.
|
|
16573
|
+
* A single pointer (the old liveAssistant) merged several streams into one
|
|
16574
|
+
* message when an interrupted turn overlapped a freshly started one, or when
|
|
16575
|
+
* subagents ran concurrently, producing interleaved garbage — hence one slot
|
|
16576
|
+
* per turn.
|
|
16577
|
+
*/
|
|
16578
|
+
liveAssistants = /* @__PURE__ */ new Map();
|
|
16579
|
+
/**
|
|
16580
|
+
* In-flight message of one turn; without a turnId it falls back to the most
|
|
16581
|
+
* recently started one (older-frame compatibility).
|
|
16582
|
+
*/
|
|
16583
|
+
liveFor(turnId) {
|
|
16584
|
+
if (typeof turnId === "number") {
|
|
16585
|
+
const hit = this.liveAssistants.get(turnId);
|
|
16586
|
+
if (hit) return hit;
|
|
16587
|
+
}
|
|
16588
|
+
let latest = null;
|
|
16589
|
+
for (const message of this.liveAssistants.values()) latest = message;
|
|
16590
|
+
return latest;
|
|
16591
|
+
}
|
|
16778
16592
|
liveTurnCount = 0;
|
|
16779
16593
|
/** pendingMsgId awaiting compaction completion/cancellation event. */
|
|
16780
16594
|
pendingCompactionMsgId = null;
|
|
@@ -16888,6 +16702,63 @@ var WebSession = class {
|
|
|
16888
16702
|
const msgs = this.buildMessages();
|
|
16889
16703
|
if (msgs.length > 0 && !this.customTitle) this.title = deriveTitle(msgs);
|
|
16890
16704
|
}
|
|
16705
|
+
/**
|
|
16706
|
+
* Core wire history seed: filled in once by the activation path when the
|
|
16707
|
+
* journal holds no message records.
|
|
16708
|
+
*/
|
|
16709
|
+
seededMessages = [];
|
|
16710
|
+
/**
|
|
16711
|
+
* Mark the journal untrustworthy (byte-interleaving damage, see
|
|
16712
|
+
* journal-writer.ts).
|
|
16713
|
+
*
|
|
16714
|
+
* The loaded journal entries and any legacy userMessages are dropped so the
|
|
16715
|
+
* message list is presented from the core transcript seed instead; state
|
|
16716
|
+
* snapshots (goal/todo, …) were already cached by loadFromPersisted and are
|
|
16717
|
+
* unaffected. nextSeq does not roll back: live entries keep appending and
|
|
16718
|
+
* rendering as usual after activation, so new messages are untouched (a seq
|
|
16719
|
+
* gap on the client takes the existing gap → snapshot refetch path).
|
|
16720
|
+
*/
|
|
16721
|
+
markDamaged(reason) {
|
|
16722
|
+
const droppedEntries = this.journal.length;
|
|
16723
|
+
const droppedUsers = this.userMessages.length;
|
|
16724
|
+
this.journal.length = 0;
|
|
16725
|
+
this.userMessages.length = 0;
|
|
16726
|
+
log.warn("web: session journal damaged; messages rebuilt from core transcript", {
|
|
16727
|
+
sessionId: this.sessionId,
|
|
16728
|
+
reason,
|
|
16729
|
+
droppedEntries,
|
|
16730
|
+
droppedUsers
|
|
16731
|
+
});
|
|
16732
|
+
}
|
|
16733
|
+
/**
|
|
16734
|
+
* Whether the current message list holds at least one assistant message with
|
|
16735
|
+
* a body. Used to decide how far journal remnants can be trusted: user
|
|
16736
|
+
* messages are persisted separately (they always carry text), while the
|
|
16737
|
+
* byte-interleaving damage only ever ate assistant bodies — so only the
|
|
16738
|
+
* assistant side is inspected.
|
|
16739
|
+
*/
|
|
16740
|
+
hasAnyAssistantContent() {
|
|
16741
|
+
return this.buildMessages().some((message) => message.role === "assistant" && message.content.trim().length > 0);
|
|
16742
|
+
}
|
|
16743
|
+
/**
|
|
16744
|
+
* Seed messages from the core session history. An older session's web
|
|
16745
|
+
* journal may hold nothing but state entries (or predate durable snapshots),
|
|
16746
|
+
* which leaves the chat area empty after a resume while the wire history is
|
|
16747
|
+
* intact. The seed occupies seq 1..N and lifts nextSeq past it, keeping seq
|
|
16748
|
+
* monotonic with live events and consistent with the getMessagesOlder cursor.
|
|
16749
|
+
*/
|
|
16750
|
+
seedHistory(messages) {
|
|
16751
|
+
if (messages.length === 0 || this.seededMessages.length > 0) return;
|
|
16752
|
+
let seq = 1;
|
|
16753
|
+
for (const message of messages) {
|
|
16754
|
+
this.seededMessages.push({
|
|
16755
|
+
...message,
|
|
16756
|
+
seq
|
|
16757
|
+
});
|
|
16758
|
+
seq += 1;
|
|
16759
|
+
}
|
|
16760
|
+
if (seq - 1 >= this.nextSeq) this.nextSeq = seq;
|
|
16761
|
+
}
|
|
16891
16762
|
async updateTitle() {
|
|
16892
16763
|
if (this.customTitle) return;
|
|
16893
16764
|
const newTitle = deriveTitle(this.buildMessages());
|
|
@@ -16939,11 +16810,12 @@ var WebSession = class {
|
|
|
16939
16810
|
if (!this.session) return;
|
|
16940
16811
|
this.unsubscribe = this.session.onEvent((event) => {
|
|
16941
16812
|
if ((event.type === "goal.updated" || event.type === "todo.updated") && event.agentId !== "main") return;
|
|
16813
|
+
if (MAIN_TRANSCRIPT_EVENTS.has(event.type) && event.agentId !== "main") return;
|
|
16942
16814
|
if (event.type === "turn.started") {
|
|
16943
16815
|
this.busy = true;
|
|
16944
16816
|
this.activeTurns += 1;
|
|
16945
16817
|
this.liveTurnCount += 1;
|
|
16946
|
-
this.
|
|
16818
|
+
this.liveAssistants.set(event.turnId, {
|
|
16947
16819
|
role: "assistant",
|
|
16948
16820
|
content: "",
|
|
16949
16821
|
tools: [],
|
|
@@ -16959,11 +16831,11 @@ var WebSession = class {
|
|
|
16959
16831
|
tokens: null,
|
|
16960
16832
|
tokensPerSec: null
|
|
16961
16833
|
}
|
|
16962
|
-
};
|
|
16834
|
+
});
|
|
16963
16835
|
} else if (event.type === "turn.ended") {
|
|
16964
16836
|
this.activeTurns = Math.max(0, this.activeTurns - 1);
|
|
16965
16837
|
this.busy = this.activeTurns > 0;
|
|
16966
|
-
const live = this.
|
|
16838
|
+
const live = this.liveFor(event.turnId);
|
|
16967
16839
|
if (event.reason === "failed" && live) live.isError = true;
|
|
16968
16840
|
if (live) {
|
|
16969
16841
|
live.turnStats = {
|
|
@@ -16983,13 +16855,13 @@ var WebSession = class {
|
|
|
16983
16855
|
message: live
|
|
16984
16856
|
});
|
|
16985
16857
|
}
|
|
16986
|
-
this.
|
|
16858
|
+
this.liveAssistants.delete(event.turnId);
|
|
16987
16859
|
this.trackPending(this.updateTitle());
|
|
16988
16860
|
} else if (event.type === "assistant.delta") {
|
|
16989
|
-
const live = this.
|
|
16861
|
+
const live = this.liveFor(event.turnId);
|
|
16990
16862
|
if (live) live.content += event.delta;
|
|
16991
16863
|
} else if (event.type === "thinking.delta") {
|
|
16992
|
-
const live = this.
|
|
16864
|
+
const live = this.liveFor(event.turnId);
|
|
16993
16865
|
if (live) {
|
|
16994
16866
|
const t = live.tools.find((x) => x.name === "thinking");
|
|
16995
16867
|
if (t) t.output = (t.output ?? "") + event.delta;
|
|
@@ -17001,7 +16873,7 @@ var WebSession = class {
|
|
|
17001
16873
|
});
|
|
17002
16874
|
}
|
|
17003
16875
|
} else if (event.type === "tool.call.started") {
|
|
17004
|
-
const live = this.
|
|
16876
|
+
const live = this.liveFor(event.turnId);
|
|
17005
16877
|
if (live) {
|
|
17006
16878
|
live.tools.push({
|
|
17007
16879
|
toolCallId: event.toolCallId,
|
|
@@ -17011,7 +16883,7 @@ var WebSession = class {
|
|
|
17011
16883
|
if (live.turnStats) live.turnStats.step += 1;
|
|
17012
16884
|
}
|
|
17013
16885
|
} else if (event.type === "tool.result") {
|
|
17014
|
-
const live = this.
|
|
16886
|
+
const live = this.liveFor(event.turnId);
|
|
17015
16887
|
if (live) {
|
|
17016
16888
|
const tool = live.tools.find((t) => t.toolCallId === event.toolCallId);
|
|
17017
16889
|
if (tool) {
|
|
@@ -17492,7 +17364,7 @@ var WebSession = class {
|
|
|
17492
17364
|
workDir: this.workDir,
|
|
17493
17365
|
title: this.title,
|
|
17494
17366
|
createdAt: this.createdAt,
|
|
17495
|
-
messageCount: this.buildMessages().length,
|
|
17367
|
+
messageCount: this.isActive ? this.buildMessages().length : -1,
|
|
17496
17368
|
active: this.isActive
|
|
17497
17369
|
};
|
|
17498
17370
|
}
|
|
@@ -17686,7 +17558,7 @@ var WebSession = class {
|
|
|
17686
17558
|
return this.connections.size;
|
|
17687
17559
|
}
|
|
17688
17560
|
buildMessages() {
|
|
17689
|
-
const messages = [];
|
|
17561
|
+
const messages = [...this.seededMessages];
|
|
17690
17562
|
let currentAssistant = null;
|
|
17691
17563
|
let userIndex = 0;
|
|
17692
17564
|
let turnCount = 0;
|
|
@@ -17872,6 +17744,36 @@ async function handleGoalRoute(req, res, url, resolveSession) {
|
|
|
17872
17744
|
}
|
|
17873
17745
|
return true;
|
|
17874
17746
|
}
|
|
17747
|
+
/**
|
|
17748
|
+
* Validate the optional workDir argument of session creation; undefined means
|
|
17749
|
+
* "reuse the server process directory".
|
|
17750
|
+
*
|
|
17751
|
+
* Trade-off: web is a local tool and the browser shares the CLI's trust level,
|
|
17752
|
+
* so no directory allowlist is introduced; but only an explicitly provided
|
|
17753
|
+
* absolute path is accepted — relative paths and `..` segments are rejected
|
|
17754
|
+
* (resolving them away instead of rejecting would amount to permitting
|
|
17755
|
+
* traversal), and the target must exist, be a directory and be readable and
|
|
17756
|
+
* writable. An invalid directory fails loudly with a 4xx at the moment the
|
|
17757
|
+
* session is created, rather than letting the agent wander into some
|
|
17758
|
+
* unexpected location later.
|
|
17759
|
+
*/
|
|
17760
|
+
async function validateSessionWorkDir(raw) {
|
|
17761
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
17762
|
+
if (typeof raw !== "string") throw new HttpError(400, "workDir 必须是字符串", "request.invalid");
|
|
17763
|
+
const value = raw.trim();
|
|
17764
|
+
if (!value) return void 0;
|
|
17765
|
+
if (!isAbsolute(value)) throw new HttpError(400, `workDir 必须是绝对路径:${value}`, "request.invalid");
|
|
17766
|
+
if (value.split(/[\\/]+/).includes("..")) throw new HttpError(400, `workDir 不允许包含 ".." 遍历段:${value}`, "request.invalid");
|
|
17767
|
+
const st = await stat(value).catch(() => null);
|
|
17768
|
+
if (!st) throw new HttpError(404, `目录不存在:${value}`, "request.invalid");
|
|
17769
|
+
if (!st.isDirectory()) throw new HttpError(400, `不是目录:${value}`, "request.invalid");
|
|
17770
|
+
try {
|
|
17771
|
+
await access(value, constants.R_OK | constants.W_OK);
|
|
17772
|
+
} catch {
|
|
17773
|
+
throw new HttpError(403, `目录不可读写:${value}`, "request.invalid");
|
|
17774
|
+
}
|
|
17775
|
+
return value;
|
|
17776
|
+
}
|
|
17875
17777
|
var SessionManager = class {
|
|
17876
17778
|
sessions = /* @__PURE__ */ new Map();
|
|
17877
17779
|
/** In-flight activation promises to deduplicate concurrent calls. */
|
|
@@ -17894,7 +17796,7 @@ var SessionManager = class {
|
|
|
17894
17796
|
await ensureSessionsDir(this.homeDir);
|
|
17895
17797
|
const metas = await listPersistedSessions(this.homeDir);
|
|
17896
17798
|
for (const meta of metas) {
|
|
17897
|
-
const entries = await loadJournal(this.homeDir, meta.sessionId);
|
|
17799
|
+
const { entries } = await loadJournal(this.homeDir, meta.sessionId);
|
|
17898
17800
|
const ws = new WebSession(null, {
|
|
17899
17801
|
sessionId: meta.sessionId,
|
|
17900
17802
|
coreSessionId: meta.coreSessionId ?? meta.sessionId,
|
|
@@ -17908,11 +17810,107 @@ var SessionManager = class {
|
|
|
17908
17810
|
ws.loadFromPersisted(entries);
|
|
17909
17811
|
this.sessions.set(meta.sessionId, ws);
|
|
17910
17812
|
}
|
|
17813
|
+
try {
|
|
17814
|
+
const index = await readSessionIndex(this.homeDir, join(this.homeDir, "sessions"));
|
|
17815
|
+
const managerWorkDir = normalizeWorkDir(this.workDir);
|
|
17816
|
+
for (const entry of index.values()) try {
|
|
17817
|
+
if (normalizeWorkDir(entry.workDir) !== managerWorkDir) continue;
|
|
17818
|
+
if (this.sessions.has(entry.sessionId)) continue;
|
|
17819
|
+
if (!existsSync(entry.sessionDir)) continue;
|
|
17820
|
+
let title = "未命名会话";
|
|
17821
|
+
let createdAt = Date.now();
|
|
17822
|
+
try {
|
|
17823
|
+
const raw = await readFile(join(entry.sessionDir, "state.json"), "utf8");
|
|
17824
|
+
const state = JSON.parse(raw);
|
|
17825
|
+
if (typeof state.title === "string" && state.title) title = state.title;
|
|
17826
|
+
if (state.createdAt) {
|
|
17827
|
+
const t = typeof state.createdAt === "number" ? state.createdAt : Date.parse(state.createdAt);
|
|
17828
|
+
if (Number.isFinite(t)) createdAt = t;
|
|
17829
|
+
}
|
|
17830
|
+
} catch {}
|
|
17831
|
+
const session = new WebSession(null, {
|
|
17832
|
+
sessionId: entry.sessionId,
|
|
17833
|
+
coreSessionId: entry.sessionId,
|
|
17834
|
+
workDir: entry.workDir,
|
|
17835
|
+
permission: this.permission,
|
|
17836
|
+
yolo: this.yolo,
|
|
17837
|
+
homeDir: this.homeDir,
|
|
17838
|
+
createdAt,
|
|
17839
|
+
title
|
|
17840
|
+
});
|
|
17841
|
+
this.sessions.set(entry.sessionId, session);
|
|
17842
|
+
} catch (error) {
|
|
17843
|
+
log.warn("web: skipping unreadable session index entry", {
|
|
17844
|
+
sessionId: entry.sessionId,
|
|
17845
|
+
sessionDir: entry.sessionDir,
|
|
17846
|
+
error: errorMessage(error)
|
|
17847
|
+
});
|
|
17848
|
+
}
|
|
17849
|
+
} catch (error) {
|
|
17850
|
+
log.warn("web: session index unreadable; falling back to web mirror + directory scan", {
|
|
17851
|
+
homeDir: this.homeDir,
|
|
17852
|
+
error: errorMessage(error)
|
|
17853
|
+
});
|
|
17854
|
+
}
|
|
17855
|
+
const wdKey = encodeWorkDirKey(this.workDir);
|
|
17856
|
+
const wdDir = join(this.homeDir, "sessions", wdKey);
|
|
17857
|
+
try {
|
|
17858
|
+
if (existsSync(wdDir)) for (const name of await readdir(wdDir)) {
|
|
17859
|
+
if (!name.startsWith("session_")) continue;
|
|
17860
|
+
if (this.sessions.has(name)) continue;
|
|
17861
|
+
try {
|
|
17862
|
+
const sessionDir = join(wdDir, name);
|
|
17863
|
+
let title = "未命名会话";
|
|
17864
|
+
let createdAt = Date.now();
|
|
17865
|
+
try {
|
|
17866
|
+
const raw = await readFile(join(sessionDir, "state.json"), "utf8");
|
|
17867
|
+
const state = JSON.parse(raw);
|
|
17868
|
+
if (typeof state.title === "string" && state.title) title = state.title;
|
|
17869
|
+
if (state.createdAt) {
|
|
17870
|
+
const t = typeof state.createdAt === "number" ? state.createdAt : Date.parse(state.createdAt);
|
|
17871
|
+
if (Number.isFinite(t)) createdAt = t;
|
|
17872
|
+
}
|
|
17873
|
+
} catch {}
|
|
17874
|
+
const session = new WebSession(null, {
|
|
17875
|
+
sessionId: name,
|
|
17876
|
+
coreSessionId: name,
|
|
17877
|
+
workDir: this.workDir,
|
|
17878
|
+
permission: this.permission,
|
|
17879
|
+
yolo: this.yolo,
|
|
17880
|
+
homeDir: this.homeDir,
|
|
17881
|
+
createdAt,
|
|
17882
|
+
title
|
|
17883
|
+
});
|
|
17884
|
+
this.sessions.set(name, session);
|
|
17885
|
+
} catch (error) {
|
|
17886
|
+
log.warn("web: skipping unreadable core session directory", {
|
|
17887
|
+
wdKey,
|
|
17888
|
+
sessionId: name,
|
|
17889
|
+
error: errorMessage(error)
|
|
17890
|
+
});
|
|
17891
|
+
}
|
|
17892
|
+
}
|
|
17893
|
+
} catch (error) {
|
|
17894
|
+
log.warn("web: core session bucket scan failed", {
|
|
17895
|
+
wdKey,
|
|
17896
|
+
dir: wdDir,
|
|
17897
|
+
error: errorMessage(error)
|
|
17898
|
+
});
|
|
17899
|
+
}
|
|
17911
17900
|
log.info("web: loaded persisted sessions", { count: this.sessions.size });
|
|
17912
17901
|
}
|
|
17913
|
-
|
|
17902
|
+
/** Server process directory: the default workspace when session creation omits workDir. */
|
|
17903
|
+
get defaultWorkDir() {
|
|
17904
|
+
return this.workDir;
|
|
17905
|
+
}
|
|
17906
|
+
/**
|
|
17907
|
+
* Create a session; an omitted workDir reuses the server process directory
|
|
17908
|
+
* and is passed to the harness as-is.
|
|
17909
|
+
*/
|
|
17910
|
+
async createSession(workDir) {
|
|
17911
|
+
const dir = workDir ?? this.workDir;
|
|
17914
17912
|
const session = await this.harness.createSession({
|
|
17915
|
-
workDir:
|
|
17913
|
+
workDir: dir,
|
|
17916
17914
|
model: this.model,
|
|
17917
17915
|
permission: this.permission
|
|
17918
17916
|
});
|
|
@@ -17920,7 +17918,7 @@ var SessionManager = class {
|
|
|
17920
17918
|
const createdAt = Date.now();
|
|
17921
17919
|
const webSession = new WebSession(session, {
|
|
17922
17920
|
sessionId,
|
|
17923
|
-
workDir:
|
|
17921
|
+
workDir: dir,
|
|
17924
17922
|
permission: this.permission,
|
|
17925
17923
|
yolo: this.yolo,
|
|
17926
17924
|
homeDir: this.homeDir,
|
|
@@ -17937,7 +17935,7 @@ var SessionManager = class {
|
|
|
17937
17935
|
this.sessions.set(sessionId, webSession);
|
|
17938
17936
|
log.info("web: session created", {
|
|
17939
17937
|
sessionId,
|
|
17940
|
-
workDir:
|
|
17938
|
+
workDir: dir
|
|
17941
17939
|
});
|
|
17942
17940
|
return webSession;
|
|
17943
17941
|
}
|
|
@@ -17982,8 +17980,19 @@ var SessionManager = class {
|
|
|
17982
17980
|
coreSessionId: session.id,
|
|
17983
17981
|
onFork: (id) => this.forkSession(id)
|
|
17984
17982
|
});
|
|
17985
|
-
const entries = await loadJournal(this.homeDir, sessionId);
|
|
17983
|
+
const { entries, corrupt } = await loadJournal(this.homeDir, sessionId);
|
|
17986
17984
|
reactivated.loadFromPersisted(entries);
|
|
17985
|
+
const journalHasContent = reactivated.hasAnyAssistantContent();
|
|
17986
|
+
if (corrupt > 0 && !journalHasContent) reactivated.markDamaged(`journal has ${corrupt} corrupt lines and no parsed message carries a body`);
|
|
17987
|
+
if (!entries.some(isMessageBearingEntry) || corrupt > 0 && !journalHasContent) try {
|
|
17988
|
+
const context = await session.getContext();
|
|
17989
|
+
reactivated.seedHistory(contextHistoryToChatMessages(context.history));
|
|
17990
|
+
} catch (error) {
|
|
17991
|
+
log.warn("web: seeding history from core transcript failed", {
|
|
17992
|
+
sessionId,
|
|
17993
|
+
error: error instanceof Error ? error.message : String(error)
|
|
17994
|
+
});
|
|
17995
|
+
}
|
|
17987
17996
|
try {
|
|
17988
17997
|
await reactivated.ready;
|
|
17989
17998
|
} catch (error) {
|
|
@@ -18038,7 +18047,7 @@ var SessionManager = class {
|
|
|
18038
18047
|
const title = `${source.getTitle()} (fork)`;
|
|
18039
18048
|
const webSession = new WebSession(session, {
|
|
18040
18049
|
sessionId: newId,
|
|
18041
|
-
workDir:
|
|
18050
|
+
workDir: source.getMetadata().workDir,
|
|
18042
18051
|
permission: source.getMetadata().permission,
|
|
18043
18052
|
yolo: this.yolo,
|
|
18044
18053
|
homeDir: this.homeDir,
|
|
@@ -18052,6 +18061,16 @@ var SessionManager = class {
|
|
|
18052
18061
|
await writeFile(getJournalPath(this.homeDir, newId), lines);
|
|
18053
18062
|
}
|
|
18054
18063
|
webSession.loadFromPersisted(entries);
|
|
18064
|
+
if (!entries.some(isMessageBearingEntry)) try {
|
|
18065
|
+
const context = await session.getContext();
|
|
18066
|
+
webSession.seedHistory(contextHistoryToChatMessages(context.history));
|
|
18067
|
+
} catch (error) {
|
|
18068
|
+
log.warn("web: seeding forked history from core transcript failed", {
|
|
18069
|
+
sourceId,
|
|
18070
|
+
newId,
|
|
18071
|
+
error: errorMessage(error)
|
|
18072
|
+
});
|
|
18073
|
+
}
|
|
18055
18074
|
try {
|
|
18056
18075
|
await webSession.ready;
|
|
18057
18076
|
} catch (error) {
|
|
@@ -18087,8 +18106,33 @@ var SessionManager = class {
|
|
|
18087
18106
|
async delete(sessionId) {
|
|
18088
18107
|
const ws = this.sessions.get(sessionId);
|
|
18089
18108
|
if (!ws) return false;
|
|
18109
|
+
const workDir = ws.workDir;
|
|
18110
|
+
const coreId = ws.getCoreSessionId();
|
|
18090
18111
|
await ws.delete();
|
|
18091
18112
|
this.sessions.delete(sessionId);
|
|
18113
|
+
const coreDir = join(this.homeDir, "sessions", encodeWorkDirKey(workDir), coreId);
|
|
18114
|
+
let moved = true;
|
|
18115
|
+
try {
|
|
18116
|
+
if (existsSync(coreDir)) {
|
|
18117
|
+
const trashRoot = join(this.homeDir, "trash");
|
|
18118
|
+
await mkdir(trashRoot, { recursive: true });
|
|
18119
|
+
await rename(coreDir, join(trashRoot, `${coreId}-${Date.now()}`));
|
|
18120
|
+
} else log.warn("web: core session directory not found on delete", {
|
|
18121
|
+
sessionId,
|
|
18122
|
+
coreId,
|
|
18123
|
+
workDir,
|
|
18124
|
+
coreDir
|
|
18125
|
+
});
|
|
18126
|
+
} catch (error) {
|
|
18127
|
+
moved = false;
|
|
18128
|
+
log.warn("web: moving session directory to trash failed; index entry kept", {
|
|
18129
|
+
sessionId,
|
|
18130
|
+
coreId,
|
|
18131
|
+
coreDir,
|
|
18132
|
+
error: errorMessage(error)
|
|
18133
|
+
});
|
|
18134
|
+
}
|
|
18135
|
+
if (moved) await removeSessionIndexEntry(this.homeDir, coreId);
|
|
18092
18136
|
return true;
|
|
18093
18137
|
}
|
|
18094
18138
|
/** List available model aliases from the harness config (reloaded). */
|
|
@@ -18831,17 +18875,18 @@ async function runWebServer(opts) {
|
|
|
18831
18875
|
res.end(JSON.stringify(manager.list()));
|
|
18832
18876
|
return;
|
|
18833
18877
|
}
|
|
18878
|
+
if (url === `${API_PREFIX}/workdir` && method === "GET") {
|
|
18879
|
+
sendJson(res, 200, { workDir: manager.defaultWorkDir });
|
|
18880
|
+
return;
|
|
18881
|
+
}
|
|
18834
18882
|
if (url === `${API_PREFIX}/sessions` && method === "POST") {
|
|
18835
18883
|
try {
|
|
18836
|
-
const
|
|
18884
|
+
const dir = await validateSessionWorkDir((await readJsonBody(req))["workDir"]);
|
|
18885
|
+
const ws = await manager.createSession(dir);
|
|
18837
18886
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
18838
18887
|
res.end(JSON.stringify(ws.getListItem()));
|
|
18839
18888
|
} catch (error) {
|
|
18840
|
-
res
|
|
18841
|
-
res.end(JSON.stringify({
|
|
18842
|
-
code: 500,
|
|
18843
|
-
message: String(error)
|
|
18844
|
-
}));
|
|
18889
|
+
sendHttpError(res, error);
|
|
18845
18890
|
}
|
|
18846
18891
|
return;
|
|
18847
18892
|
}
|
|
@@ -19137,7 +19182,10 @@ async function runWebServer(opts) {
|
|
|
19137
19182
|
if (url === "/" || url === "/index.html") {
|
|
19138
19183
|
try {
|
|
19139
19184
|
const html = await readFile(join(publicDir, "index.html"), "utf-8");
|
|
19140
|
-
res.writeHead(200, {
|
|
19185
|
+
res.writeHead(200, {
|
|
19186
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
19187
|
+
"Cache-Control": "no-store"
|
|
19188
|
+
});
|
|
19141
19189
|
res.end(html);
|
|
19142
19190
|
} catch {
|
|
19143
19191
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -19150,7 +19198,9 @@ async function runWebServer(opts) {
|
|
|
19150
19198
|
const filePath = join(publicDir, safeUrl);
|
|
19151
19199
|
const contentType = contentTypes[filePath.split(".").pop() ?? ""] ?? "application/octet-stream";
|
|
19152
19200
|
const data = await readFile(filePath);
|
|
19153
|
-
|
|
19201
|
+
const headers = { "Content-Type": contentType };
|
|
19202
|
+
if (safeUrl.startsWith("/assets/")) headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
|
19203
|
+
res.writeHead(200, headers);
|
|
19154
19204
|
res.end(data);
|
|
19155
19205
|
} catch {
|
|
19156
19206
|
res.writeHead(404, { "Content-Type": "text/plain" });
|