scream-code 0.16.1 → 0.16.2
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-V7s7I7Nr.mjs} +576 -413
- package/dist/{dispatch-B6jbxY0D.mjs → dispatch-BRkUiHzo.mjs} +1 -1
- package/dist/{dispatch-DqeJdQZQ.mjs → dispatch-D0mYcf1X.mjs} +1172 -230
- 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-BABWY9vy.mjs → text-input-dialog-CzJKBIoN.mjs} +40 -12
- package/dist/{text-input-dialog-rSkH3JTV.mjs → text-input-dialog-Dp2TUSjQ.mjs} +1 -1
- 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 { $
|
|
8
|
-
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-
|
|
7
|
+
import { $ as getBreathingFrame, $n as encodeWorkDirKey, $t as isStreaming, A as ToolCallComponent, An as setExperimentalFlags, B as sanitizeShellOutput, Bn as detectShellEnvironment, Bt as DISABLE_TERMINAL_THEME_REPORTING, Cn as getCtrlDHint, D as isEmptySessionHintDismissed, Dn as BUILTIN_SLASH_COMMANDS, Dt as supportsBalance, E as UserMessageComponent, En as buildSkillSlashCommands, Et as refreshProviderBalance, F as CachedContainer, Fn as PULSE_WAVE_FRAMES, Ft as contrastTextHex, G as AssistantMessageComponent, Gn as CLI_COMMAND_NAME, Gt as OSC11_RESPONSE_PREFIX, H as ReadGroupComponent, Hn as getInputHistoryFile, Ht as ENABLE_TERMINAL_THEME_REPORTING, I as ThinkingComponent, In as TuiConfigParseError, It as getColorPalette, J as estimateTokens, Jn as PRODUCT_NAME, Jt as TERMINAL_FOCUS_IN, K as ActivityGroupComponent, Kn as CLI_UI_MODE, Kt as OSC11_RESPONSE_PREFIX_NO_ESC, L as NoticeMessageComponent, Ln as TuiLikePreferencesSchema, Lt as detectTerminalTheme, M as renderDiffLinesClustered, Mn as isEmbeddingModelCached, Mt as createEditorTheme, N as highlightLines, Nn as startManualEmbeddingDownload, Nt as createMarkdownTheme, O as isTurnElapsedEnabled, On as sortSlashCommands, Ot as clearInfoPanelState, P as langFromPath, Pn as PIXEL_PULSE_FRAMES, Pt as createThemeStyles, Q as BREATHE_CYCLE_MS, Qn as ScreamHarness, Qt as isBusy, R as StatusMessageComponent, Rn as loadTuiConfig, Rt as parseOsc11BackgroundTheme, S as readJsonlFile, Sn as getCtrlCHint, Tn as getNoActiveSessionMessage, U as parseReadGroupOutput, Un as getLogDir, Ut as OSC11_QUERY, V as SkillActivationComponent, Vn as getDataDir, Vt as ENABLE_TERMINAL_FOCUS_REPORTING, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE, X as AgentGroupComponent, Xn as fetchCatalog, Xt as TERMINAL_THEME_DARK, Y as getSharedSpeedTracker, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_FOCUS_OUT, Z as WelcomeComponent, Zn as saveCatalogCache, Zt as TERMINAL_THEME_LIGHT, _n as EMPTY_SESSION_HINT_URL, _t as handleFusionPlanCommand, ar as resolveScreamHome, b as readUpdateCache, bn as SESSION_TIPS, c as hasDispose, cn as STATUS_BULLET, cr as log, d as readExpanded, dn as formatErrorMessage, dr as isOrphanedToolCallError, dt as changeThinkingLevel, en as FooterComponent, er as normalizeWorkDir, et as resetBreathingClock, f as MoonLoader, fn as isTodoItemShape, fr as ErrorCodes, ft as getModelCycleLevel, gn as truncateErrorMessage, hn as stringValue, i as buildRoleAdditionalText, j as renderDiffLines, jn as getKnowledgeStore, jt as resolveThemeSync, k as toggleEmptySessionHint, kn as isExperimentalFlagEnabled, l as isExpandable, ln as appendStreamingArgsPreview, lr as resolveGlobalLogPath, lt as isInternalMessage, mn as serializeToolResultOutput, n as clearEvalPanelState, nn as SubagentSlots, nr as readSessionIndex, nt as clearGoalState, or as MemoryMemoStore, p as formatMemoryMemoForInjection, pn as parseStreamingArgs, pr as SCREAM_ERROR_INFO, q as charsForTokenBudget, qn as CLI_USER_AGENT_PRODUCT, qt as QUERY_TERMINAL_THEME, r as openUrl, rr as removeSessionIndexEntry, rt as refineGoal, s as disposeChildren, sn as printableChar, sr as flushDiagnosticLogs, t as dispatchInput, tn as lerpGradient, tr as appendSessionIndexEntry, tt as CompactionComponent, u as isPlanExpandable, un as argsRecord, ur as isScreamError, ut as toTerminalHyperlink, v as selectUpdateTarget, vn as EXIT_CONFIRM_WINDOW_MS, w as getDaemonInstructions, wn as getLlmNotSetMessage, x as appendJsonlLine, xn as TIP_ROTATION_INTERVAL_MS, y as refreshUpdateCache, yn as MAIN_AGENT_ID, yt as handlePlanCommand, z as replaceTabs, zn as saveTuiConfig, zt as DISABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-D0mYcf1X.mjs";
|
|
8
|
+
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CzJKBIoN.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.2"),
|
|
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;
|
|
@@ -4691,6 +4356,30 @@ var StreamingUIController = class {
|
|
|
4691
4356
|
_pendingReadGroupComponents = /* @__PURE__ */ new Map();
|
|
4692
4357
|
_pendingAgentGroup = null;
|
|
4693
4358
|
_pendingReadGroup = null;
|
|
4359
|
+
_activityGroup;
|
|
4360
|
+
/** Guards the single `registerLiveComponent` call for the turn's block. */
|
|
4361
|
+
_activityGroupRegistered = false;
|
|
4362
|
+
/** Block that borrowed a card, so late results refresh their own block. */
|
|
4363
|
+
_groupByToolCard = /* @__PURE__ */ new WeakMap();
|
|
4364
|
+
/** Reasoning of the turn's earlier steps, kept when a step's draft resets. */
|
|
4365
|
+
_turnThinkingHistory = "";
|
|
4366
|
+
_lastThinkingText = "";
|
|
4367
|
+
/**
|
|
4368
|
+
* Tools that keep their own transcript card instead of joining the turn's
|
|
4369
|
+
* activity block:
|
|
4370
|
+
* - `Read` / `Agent` are owned by the read/agent group machinery (and by
|
|
4371
|
+
* `/revoke`, which walks those groups);
|
|
4372
|
+
* - `AskUserQuestion` returns early above and mounts its finished card;
|
|
4373
|
+
* - `ExitPlanMode` owns the plan box, which Ctrl+E expands by walking the
|
|
4374
|
+
* transcript container's direct children.
|
|
4375
|
+
*/
|
|
4376
|
+
static STANDALONE_TOOL_NAMES = new Set([
|
|
4377
|
+
"Read",
|
|
4378
|
+
"Agent",
|
|
4379
|
+
"AskUserQuestion",
|
|
4380
|
+
"ExitPlanMode",
|
|
4381
|
+
"ReadMediaFile"
|
|
4382
|
+
]);
|
|
4694
4383
|
constructor(host) {
|
|
4695
4384
|
this.host = host;
|
|
4696
4385
|
}
|
|
@@ -4903,6 +4592,7 @@ var StreamingUIController = class {
|
|
|
4903
4592
|
}
|
|
4904
4593
|
/** Tears down replay-specific state after session history has been rendered. */
|
|
4905
4594
|
cleanupAfterReplay(completedToolCallIds) {
|
|
4595
|
+
this.endActivityGroup();
|
|
4906
4596
|
this._activeToolCalls.clear();
|
|
4907
4597
|
for (const toolCallId of completedToolCallIds) this._pendingToolComponents.delete(toolCallId);
|
|
4908
4598
|
this._pendingAgentGroup = null;
|
|
@@ -4914,6 +4604,74 @@ var StreamingUIController = class {
|
|
|
4914
4604
|
this.pendingToolCallFlushIds.clear();
|
|
4915
4605
|
this.host.state.ui.requestRender();
|
|
4916
4606
|
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Returns the turn's block, creating it on demand: a block only ever starts
|
|
4609
|
+
* where work starts (reasoning or a tool call), which keeps it positioned
|
|
4610
|
+
* right after whatever the assistant last said.
|
|
4611
|
+
*
|
|
4612
|
+
* Live turns spin, replayed turns (history) do not — the streaming phase is
|
|
4613
|
+
* the discriminator, so a second block inside the same live turn still spins.
|
|
4614
|
+
*/
|
|
4615
|
+
ensureActivityGroup() {
|
|
4616
|
+
const existing = this._activityGroup;
|
|
4617
|
+
if (existing !== void 0) return existing;
|
|
4618
|
+
const { state } = this.host;
|
|
4619
|
+
const group = new ActivityGroupComponent(state.theme.colors, state.ui);
|
|
4620
|
+
group.setRunning(state.appState.streamingPhase !== "idle");
|
|
4621
|
+
this._activityGroup = group;
|
|
4622
|
+
this._turnThinkingHistory = "";
|
|
4623
|
+
state.transcriptContainer.addChild(group);
|
|
4624
|
+
state.ui.requestRender();
|
|
4625
|
+
return group;
|
|
4626
|
+
}
|
|
4627
|
+
/**
|
|
4628
|
+
* Seals the current block as soon as the assistant starts writing visible
|
|
4629
|
+
* text, so every stretch of work stays next to the answer it produced instead
|
|
4630
|
+
* of the whole turn's process piling up above the prose.
|
|
4631
|
+
*/
|
|
4632
|
+
sealActivityGroupOnText(text) {
|
|
4633
|
+
if (this._activityGroup === void 0) return;
|
|
4634
|
+
if (text.trim().length === 0) return;
|
|
4635
|
+
this.endActivityGroup();
|
|
4636
|
+
}
|
|
4637
|
+
/**
|
|
4638
|
+
* Registers the block against the first entry it owns, so history folding can
|
|
4639
|
+
* later sweep the whole block as one entry. Marked pending so a growing block
|
|
4640
|
+
* is never folded mid-turn.
|
|
4641
|
+
*/
|
|
4642
|
+
registerActivityGroupEntry(group, entry) {
|
|
4643
|
+
if (this._activityGroupRegistered) return;
|
|
4644
|
+
this._activityGroupRegistered = true;
|
|
4645
|
+
this.host.transcriptController.registerLiveComponent(group, entry);
|
|
4646
|
+
this.host.transcriptController.markPending(group);
|
|
4647
|
+
}
|
|
4648
|
+
/**
|
|
4649
|
+
* Settles the block: stops the spinner, drops one that never got content, and
|
|
4650
|
+
* releases the reference so the next turn opens a fresh one. Blocks with
|
|
4651
|
+
* content stay mounted (replayed history keeps its blocks).
|
|
4652
|
+
*/
|
|
4653
|
+
endActivityGroup() {
|
|
4654
|
+
const group = this._activityGroup;
|
|
4655
|
+
this._activityGroup = void 0;
|
|
4656
|
+
this._activityGroupRegistered = false;
|
|
4657
|
+
this._turnThinkingHistory = "";
|
|
4658
|
+
this._lastThinkingText = "";
|
|
4659
|
+
if (group === void 0) return;
|
|
4660
|
+
group.setRunning(false);
|
|
4661
|
+
if (group.isEmpty()) {
|
|
4662
|
+
this.host.transcriptController.unmarkPending(group);
|
|
4663
|
+
this.host.state.transcriptContainer.removeChild(group);
|
|
4664
|
+
group.dispose();
|
|
4665
|
+
return;
|
|
4666
|
+
}
|
|
4667
|
+
this.host.transcriptController.unmarkPending(group);
|
|
4668
|
+
group.dispose();
|
|
4669
|
+
}
|
|
4670
|
+
/** Reasoning text of the whole turn: earlier steps plus the current draft. */
|
|
4671
|
+
activityThinkingText(draft) {
|
|
4672
|
+
if (this._turnThinkingHistory.length === 0) return draft;
|
|
4673
|
+
return `${this._turnThinkingHistory}\n${draft}`;
|
|
4674
|
+
}
|
|
4917
4675
|
disposeActiveThinkingComponent() {
|
|
4918
4676
|
if (this._activeThinkingComponent !== void 0) {
|
|
4919
4677
|
this._activeThinkingComponent.dispose();
|
|
@@ -4988,7 +4746,9 @@ var StreamingUIController = class {
|
|
|
4988
4746
|
const draft = this._assistantDraft;
|
|
4989
4747
|
if (this._shownAssistantLength >= draft.length) return this._shownAssistantLength;
|
|
4990
4748
|
const measured = getSharedSpeedTracker().getSpeed();
|
|
4991
|
-
const
|
|
4749
|
+
const tokPerSec = measured > 0 ? measured : 50;
|
|
4750
|
+
const requested = charsForTokenBudget(draft.slice(this._shownAssistantLength), tokPerSec * (50 / 1e3));
|
|
4751
|
+
const budget = Math.min(25, Math.max(1, requested));
|
|
4992
4752
|
this._shownAssistantLength = Math.min(draft.length, this._shownAssistantLength + budget);
|
|
4993
4753
|
return this._shownAssistantLength;
|
|
4994
4754
|
}
|
|
@@ -5052,6 +4812,7 @@ var StreamingUIController = class {
|
|
|
5052
4812
|
const turnStartTime = this.turnStartAt !== 0 ? this.turnStartAt : state.appState.streamingStartTime;
|
|
5053
4813
|
const completedTurnKey = this._currentTurnId ?? `local:${String(turnStartTime)}`;
|
|
5054
4814
|
this.finalizeLiveTextBuffers();
|
|
4815
|
+
this.endActivityGroup();
|
|
5055
4816
|
this.resetToolCallState();
|
|
5056
4817
|
this._currentTurnId = void 0;
|
|
5057
4818
|
const next = this.host.shiftQueuedMessage();
|
|
@@ -5125,6 +4886,7 @@ var StreamingUIController = class {
|
|
|
5125
4886
|
block.component.updateContent(fullText);
|
|
5126
4887
|
this.host.state.ui.requestRender();
|
|
5127
4888
|
}
|
|
4889
|
+
this.sealActivityGroupOnText(fullText);
|
|
5128
4890
|
}
|
|
5129
4891
|
onStreamingTextEnd() {
|
|
5130
4892
|
const block = this._streamingBlock;
|
|
@@ -5140,7 +4902,7 @@ var StreamingUIController = class {
|
|
|
5140
4902
|
if (this._activeThinkingComponent === void 0) {
|
|
5141
4903
|
this._pendingAgentGroup = null;
|
|
5142
4904
|
this._pendingReadGroup = null;
|
|
5143
|
-
const component = new ThinkingComponent(fullText, state.theme.colors, true, "
|
|
4905
|
+
const component = new ThinkingComponent(fullText, state.theme.colors, true, "finalized", state.ui);
|
|
5144
4906
|
const entry = {
|
|
5145
4907
|
id: nextTranscriptId(),
|
|
5146
4908
|
kind: "thinking",
|
|
@@ -5153,21 +4915,30 @@ var StreamingUIController = class {
|
|
|
5153
4915
|
this.host.pushTranscriptEntry(entry);
|
|
5154
4916
|
this.host.transcriptController.registerLiveComponent(component, entry);
|
|
5155
4917
|
this.host.transcriptController.markPending(component);
|
|
5156
|
-
|
|
5157
|
-
|
|
4918
|
+
const group = this.ensureActivityGroup();
|
|
4919
|
+
this.registerActivityGroupEntry(group, entry);
|
|
4920
|
+
this._lastThinkingText = fullText;
|
|
4921
|
+
group.setThinking(this.activityThinkingText(fullText), true);
|
|
5158
4922
|
} else {
|
|
5159
4923
|
this._activeThinkingComponent.setText(fullText);
|
|
5160
4924
|
if (this._thinkingEntry !== void 0) this._thinkingEntry.content = fullText;
|
|
4925
|
+
this._lastThinkingText = fullText;
|
|
4926
|
+
this.ensureActivityGroup().setThinking(this.activityThinkingText(fullText), true);
|
|
5161
4927
|
}
|
|
5162
4928
|
state.ui.requestRender();
|
|
5163
4929
|
}
|
|
5164
4930
|
onThinkingEnd() {
|
|
5165
4931
|
if (this._activeThinkingComponent === void 0) return;
|
|
5166
|
-
this._activeThinkingComponent
|
|
5167
|
-
|
|
4932
|
+
const component = this._activeThinkingComponent;
|
|
4933
|
+
component.finalize();
|
|
4934
|
+
this.host.transcriptController.unmarkPending(component);
|
|
5168
4935
|
this.host.transcriptController.commit();
|
|
4936
|
+
this.host.transcriptController.releaseLiveComponent(component);
|
|
5169
4937
|
this._activeThinkingComponent = void 0;
|
|
5170
4938
|
this._thinkingEntry = void 0;
|
|
4939
|
+
if (this._lastThinkingText.trim().length > 0) this._turnThinkingHistory = this.activityThinkingText(this._lastThinkingText);
|
|
4940
|
+
this._lastThinkingText = "";
|
|
4941
|
+
this._activityGroup?.endThinking();
|
|
5171
4942
|
this.host.state.ui.requestRender();
|
|
5172
4943
|
}
|
|
5173
4944
|
onToolCallStart(toolCall) {
|
|
@@ -5191,15 +4962,20 @@ var StreamingUIController = class {
|
|
|
5191
4962
|
this.host.pushTranscriptEntry(entry);
|
|
5192
4963
|
this.host.transcriptController.registerLiveComponent(tc, entry);
|
|
5193
4964
|
this.host.transcriptController.markPending(tc);
|
|
5194
|
-
if (state.toolOutputExpanded) tc.setExpanded(true);
|
|
5195
4965
|
if (state.planExpanded) tc.setPlanExpanded(true);
|
|
5196
4966
|
if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
|
|
5197
4967
|
if (toolCall.name !== "Read") this._pendingReadGroup = null;
|
|
5198
4968
|
let handled = this.tryAttachAgentToolCall(toolCall, tc);
|
|
5199
4969
|
if (!handled) handled = this.tryAttachReadToolCall(toolCall, tc);
|
|
5200
|
-
if (!handled) {
|
|
4970
|
+
if (!handled) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
|
|
5201
4971
|
state.transcriptContainer.addChild(tc);
|
|
5202
4972
|
state.ui.requestRender();
|
|
4973
|
+
} else {
|
|
4974
|
+
const group = this.ensureActivityGroup();
|
|
4975
|
+
this.registerActivityGroupEntry(group, entry);
|
|
4976
|
+
this._groupByToolCard.set(tc, group);
|
|
4977
|
+
group.attachTool(tc, toolCall.step ?? this._currentStep);
|
|
4978
|
+
state.ui.requestRender();
|
|
5203
4979
|
}
|
|
5204
4980
|
if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
|
|
5205
4981
|
const session = this.host.requireSession();
|
|
@@ -5254,6 +5030,9 @@ var StreamingUIController = class {
|
|
|
5254
5030
|
if (tc) {
|
|
5255
5031
|
tc.setResult(result);
|
|
5256
5032
|
tc.dispose();
|
|
5033
|
+
const owner = this._groupByToolCard.get(tc) ?? this._activityGroup;
|
|
5034
|
+
owner?.notifyToolChanged();
|
|
5035
|
+
if (owner !== void 0 && this._groupByToolCard.has(tc)) this.host.transcriptController.releaseLiveComponent(tc);
|
|
5257
5036
|
this.host.transcriptController.unmarkPending(tc);
|
|
5258
5037
|
this._pendingToolComponents.delete(toolCallId);
|
|
5259
5038
|
const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
|
|
@@ -5265,7 +5044,6 @@ var StreamingUIController = class {
|
|
|
5265
5044
|
if (matchedCall?.name === "AskUserQuestion") {
|
|
5266
5045
|
const completed = new ToolCallComponent(matchedCall, result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
|
|
5267
5046
|
completed.setPermissionMode(state.appState.permissionMode);
|
|
5268
|
-
if (state.toolOutputExpanded) completed.setExpanded(true);
|
|
5269
5047
|
if (state.planExpanded) completed.setPlanExpanded(true);
|
|
5270
5048
|
const entry = {
|
|
5271
5049
|
id: nextTranscriptId(),
|
|
@@ -6617,6 +6395,14 @@ var TranscriptController = class TranscriptController {
|
|
|
6617
6395
|
registerLiveComponent(component, entry) {
|
|
6618
6396
|
this.liveComponentToEntry.set(component, entry);
|
|
6619
6397
|
}
|
|
6398
|
+
/**
|
|
6399
|
+
* Drops the entry mapping of a component that will never be folded into the
|
|
6400
|
+
* committed history (it is not a container child), so the map cannot retain
|
|
6401
|
+
* components — and the text they hold — for the life of the session.
|
|
6402
|
+
*/
|
|
6403
|
+
releaseLiveComponent(component) {
|
|
6404
|
+
this.liveComponentToEntry.delete(component);
|
|
6405
|
+
}
|
|
6620
6406
|
markPending(component) {
|
|
6621
6407
|
this.pendingComponents.add(component);
|
|
6622
6408
|
}
|
|
@@ -6684,11 +6470,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6684
6470
|
component.updateContent(entry.content);
|
|
6685
6471
|
return component;
|
|
6686
6472
|
}
|
|
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
|
-
}
|
|
6473
|
+
case "thinking": return new ThinkingComponent(entry.content, state.theme.colors, true);
|
|
6692
6474
|
case "tool_call":
|
|
6693
6475
|
if (entry.toolCallData?.name === "ReadGroup" && entry.toolCallData.result !== void 0) {
|
|
6694
6476
|
const rgc = new ReadGroupComponent(state.theme.colors, state.ui);
|
|
@@ -6697,7 +6479,6 @@ var TranscriptController = class TranscriptController {
|
|
|
6697
6479
|
}
|
|
6698
6480
|
if (entry.toolCallData) {
|
|
6699
6481
|
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
6482
|
if (state.planExpanded) tc.setPlanExpanded(true);
|
|
6702
6483
|
return tc;
|
|
6703
6484
|
}
|
|
@@ -6781,6 +6562,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6781
6562
|
clearAndRedraw() {
|
|
6782
6563
|
const { state, streamingUI, imageStore } = this.host;
|
|
6783
6564
|
streamingUI.discardPending();
|
|
6565
|
+
streamingUI.endActivityGroup();
|
|
6784
6566
|
state.transcriptEntries = [];
|
|
6785
6567
|
streamingUI.disposeActiveCompactionBlock();
|
|
6786
6568
|
streamingUI.resetLiveText();
|
|
@@ -6837,6 +6619,18 @@ var TranscriptController = class TranscriptController {
|
|
|
6837
6619
|
}
|
|
6838
6620
|
toggleToolOutputExpansion() {
|
|
6839
6621
|
const { state } = this.host;
|
|
6622
|
+
const children = state.transcriptContainer.children;
|
|
6623
|
+
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
6624
|
+
const child = children[i];
|
|
6625
|
+
if (child === void 0) continue;
|
|
6626
|
+
if (isExpandable(child)) {
|
|
6627
|
+
state.toolOutputExpanded = !(readExpanded(child) ?? state.toolOutputExpanded);
|
|
6628
|
+
child.setExpanded(state.toolOutputExpanded);
|
|
6629
|
+
state.ui.requestRender();
|
|
6630
|
+
return;
|
|
6631
|
+
}
|
|
6632
|
+
if (child instanceof ReadGroupComponent || child instanceof AgentGroupComponent) break;
|
|
6633
|
+
}
|
|
6840
6634
|
state.toolOutputExpanded = !state.toolOutputExpanded;
|
|
6841
6635
|
state.ui.requestRender();
|
|
6842
6636
|
}
|
|
@@ -7058,17 +6852,14 @@ function formatCount$1(n) {
|
|
|
7058
6852
|
return n.toLocaleString("en-US");
|
|
7059
6853
|
}
|
|
7060
6854
|
/**
|
|
7061
|
-
* Compact token form for narrow sidebars: 2,267,680,102 → `
|
|
7062
|
-
*
|
|
7063
|
-
*
|
|
6855
|
+
* Compact token form for narrow sidebars: 2,267,680,102 → `2267.7M`. One unit
|
|
6856
|
+
* ladder for every locale (K below a million, M above) so the value column
|
|
6857
|
+
* stays comparable across languages: Chinese no longer switches to the
|
|
6858
|
+
* ten-thousand / hundred-million units it used before.
|
|
6859
|
+
* Used only when the thousands-separated form cannot fit, so a narrow sidebar
|
|
6860
|
+
* keeps every value readable instead of cutting digits.
|
|
7064
6861
|
*/
|
|
7065
6862
|
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
6863
|
if (n >= 999500) return `${(n / 1e6).toFixed(1)}M`;
|
|
7073
6864
|
if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
|
|
7074
6865
|
return String(n);
|
|
@@ -7136,6 +6927,10 @@ const sessionPanel = {
|
|
|
7136
6927
|
* with the 1s sidebar heartbeat each tick advances half the cycle, so the
|
|
7137
6928
|
* colour jump is clearly visible instead of drifting imperceptibly. */
|
|
7138
6929
|
const AGENT_GRADIENT_CYCLE_MS = 2e3;
|
|
6930
|
+
/** Display form of a slot type: 'coder' reads as 'Coder' in the panel. */
|
|
6931
|
+
function displayType(type) {
|
|
6932
|
+
return type.length === 0 ? type : type.charAt(0).toUpperCase() + type.slice(1);
|
|
6933
|
+
}
|
|
7139
6934
|
/** Localized status word per slot state. */
|
|
7140
6935
|
function statusText(status) {
|
|
7141
6936
|
switch (status) {
|
|
@@ -7165,7 +6960,7 @@ var AgentsPanelContent = class {
|
|
|
7165
6960
|
if (agents === void 0 || agents.length === 0) return [];
|
|
7166
6961
|
return layoutSidebarGrid(agents.map((slot) => ({
|
|
7167
6962
|
marker: slot.status === "idle" ? "○" : "●",
|
|
7168
|
-
label: slot.type,
|
|
6963
|
+
label: displayType(slot.type),
|
|
7169
6964
|
value: statusText(slot.status) + (slot.count > 1 ? ` ×${slot.count}` : "")
|
|
7170
6965
|
})), width).map((cell, index) => this.renderSlot(cell, agents[index], index, width));
|
|
7171
6966
|
}
|
|
@@ -7321,20 +7116,23 @@ const HUB_MODEL_STALE_MS = 5 * 6e4;
|
|
|
7321
7116
|
const HUB_ENDPOINTS = Object.freeze([
|
|
7322
7117
|
{
|
|
7323
7118
|
id: "github",
|
|
7324
|
-
url: "https://github.com/"
|
|
7119
|
+
url: "https://github.com/",
|
|
7120
|
+
label: "Github"
|
|
7325
7121
|
},
|
|
7326
7122
|
{
|
|
7327
7123
|
id: "google",
|
|
7328
|
-
url: "https://www.google.com/"
|
|
7124
|
+
url: "https://www.google.com/",
|
|
7125
|
+
label: "Google"
|
|
7329
7126
|
},
|
|
7330
7127
|
{
|
|
7331
7128
|
id: "x",
|
|
7332
7129
|
url: "https://x.com/",
|
|
7333
|
-
label: "
|
|
7130
|
+
label: "X (Twitter)"
|
|
7334
7131
|
},
|
|
7335
7132
|
{
|
|
7336
7133
|
id: "huggingface",
|
|
7337
|
-
url: "https://huggingface.co/"
|
|
7134
|
+
url: "https://huggingface.co/",
|
|
7135
|
+
label: "Huggingface"
|
|
7338
7136
|
},
|
|
7339
7137
|
{
|
|
7340
7138
|
id: "alibaba",
|
|
@@ -13977,6 +13775,7 @@ var ScreamTUI = class {
|
|
|
13977
13775
|
beginSessionRequest() {
|
|
13978
13776
|
this.streamingUI.markTurnStarted();
|
|
13979
13777
|
this.streamingUI.setTurnId(void 0);
|
|
13778
|
+
this.streamingUI.endActivityGroup();
|
|
13980
13779
|
this.streamingUI.resetLiveText();
|
|
13981
13780
|
this.streamingUI.resetToolUi();
|
|
13982
13781
|
this.streamingUI.resetToolCallState();
|
|
@@ -13988,6 +13787,7 @@ var ScreamTUI = class {
|
|
|
13988
13787
|
}
|
|
13989
13788
|
failSessionRequest(message) {
|
|
13990
13789
|
this.setAppState({ streamingPhase: "idle" });
|
|
13790
|
+
this.streamingUI.endActivityGroup();
|
|
13991
13791
|
this.resetLivePane();
|
|
13992
13792
|
this.showError(message);
|
|
13993
13793
|
}
|
|
@@ -14195,7 +13995,7 @@ var ScreamTUI = class {
|
|
|
14195
13995
|
const card = new OnboardingCardComponent({
|
|
14196
13996
|
colors: this.state.theme.colors,
|
|
14197
13997
|
onConfigure: () => {
|
|
14198
|
-
import("./dispatch-
|
|
13998
|
+
import("./dispatch-BRkUiHzo.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
14199
13999
|
this.restoreEditor();
|
|
14200
14000
|
onDone?.();
|
|
14201
14001
|
});
|
|
@@ -15864,6 +15664,53 @@ async function persistKey(filePath, record) {
|
|
|
15864
15664
|
await rename(tmp, filePath);
|
|
15865
15665
|
}
|
|
15866
15666
|
//#endregion
|
|
15667
|
+
//#region src/web/journal-writer.ts
|
|
15668
|
+
/**
|
|
15669
|
+
* Serialization gate for journal appends.
|
|
15670
|
+
*
|
|
15671
|
+
* Why it exists (an incident observed in the field): the session journal is an
|
|
15672
|
+
* append-only file holding one JSON record per line, and lines used to be
|
|
15673
|
+
* appended with `writeFile(path, line + '\n', { flag: 'a' })`. A
|
|
15674
|
+
* `web.message.finalized` entry carries an entire assistant body (hundreds of
|
|
15675
|
+
* thousands of characters in practice) while small entries such as turn.ended
|
|
15676
|
+
* are written at the same time — the two writes interleaved at the byte level,
|
|
15677
|
+
* so what landed on disk were malformed lines with the two records spliced
|
|
15678
|
+
* into each other:
|
|
15679
|
+
*
|
|
15680
|
+
* ..."content":"...{"type":"journal","seq":38354,... ← another record spliced into the body
|
|
15681
|
+
*
|
|
15682
|
+
* Consequence: the whole line fails JSON.parse and the message disappears from
|
|
15683
|
+
* the UI (the reader can only skip it), which the user perceives as a garbled
|
|
15684
|
+
* or lost body. Queueing the writes inside the process eliminates the
|
|
15685
|
+
* interleaving; the append itself still goes through appendFile (O_APPEND), so
|
|
15686
|
+
* a single write is never split by another fd.
|
|
15687
|
+
*
|
|
15688
|
+
* The queue is keyed by file path and dropped once drained, so a long-running
|
|
15689
|
+
* process does not grow it without bound. No cross-process writer exists today
|
|
15690
|
+
* (only the web server writes the journal); if one appears, this needs a
|
|
15691
|
+
* file-lock based scheme instead.
|
|
15692
|
+
*/
|
|
15693
|
+
const writeChains = /* @__PURE__ */ new Map();
|
|
15694
|
+
/**
|
|
15695
|
+
* Append one line (plus its newline) to the journal; concurrent calls for the
|
|
15696
|
+
* same file land in arrival order.
|
|
15697
|
+
*/
|
|
15698
|
+
async function appendJournalLineSerialized(path, line, onError) {
|
|
15699
|
+
const next = (writeChains.get(path) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
15700
|
+
try {
|
|
15701
|
+
await appendFile(path, line + "\n");
|
|
15702
|
+
} catch (error) {
|
|
15703
|
+
onError(error);
|
|
15704
|
+
}
|
|
15705
|
+
});
|
|
15706
|
+
writeChains.set(path, next);
|
|
15707
|
+
try {
|
|
15708
|
+
await next;
|
|
15709
|
+
} finally {
|
|
15710
|
+
if (writeChains.get(path) === next) writeChains.delete(path);
|
|
15711
|
+
}
|
|
15712
|
+
}
|
|
15713
|
+
//#endregion
|
|
15867
15714
|
//#region src/web/rateLimit.ts
|
|
15868
15715
|
function createFixedWindowLimiter(opts) {
|
|
15869
15716
|
const { windowMs, max } = opts;
|
|
@@ -16431,14 +16278,12 @@ async function ensureSessionsDir(homeDir) {
|
|
|
16431
16278
|
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
|
|
16432
16279
|
}
|
|
16433
16280
|
async function appendJournalLine(homeDir, sessionId, line) {
|
|
16434
|
-
|
|
16435
|
-
await writeFile(getJournalPath(homeDir, sessionId), line + "\n", { flag: "a" });
|
|
16436
|
-
} catch (error) {
|
|
16281
|
+
await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
|
|
16437
16282
|
log.warn("web: failed to persist journal line", {
|
|
16438
16283
|
sessionId,
|
|
16439
16284
|
error: String(error)
|
|
16440
16285
|
});
|
|
16441
|
-
}
|
|
16286
|
+
});
|
|
16442
16287
|
}
|
|
16443
16288
|
let metaWriteSeq = 0;
|
|
16444
16289
|
async function saveMetadata(homeDir, meta) {
|
|
@@ -16465,16 +16310,77 @@ async function loadMetadata(homeDir, sessionId) {
|
|
|
16465
16310
|
async function loadJournal(homeDir, sessionId) {
|
|
16466
16311
|
try {
|
|
16467
16312
|
const lines = (await readFile(getJournalPath(homeDir, sessionId), "utf-8")).split("\n").filter((l) => l.trim().length > 0);
|
|
16468
|
-
const
|
|
16313
|
+
const entries = [];
|
|
16314
|
+
let corrupt = 0;
|
|
16469
16315
|
for (const line of lines) try {
|
|
16470
|
-
|
|
16316
|
+
entries.push(JSON.parse(line));
|
|
16471
16317
|
} catch {
|
|
16318
|
+
corrupt += 1;
|
|
16472
16319
|
log.warn("web: skipping corrupt journal line", { sessionId });
|
|
16473
16320
|
}
|
|
16474
|
-
return
|
|
16321
|
+
return {
|
|
16322
|
+
entries,
|
|
16323
|
+
corrupt
|
|
16324
|
+
};
|
|
16475
16325
|
} catch {
|
|
16476
|
-
return
|
|
16326
|
+
return {
|
|
16327
|
+
entries: [],
|
|
16328
|
+
corrupt: 0
|
|
16329
|
+
};
|
|
16330
|
+
}
|
|
16331
|
+
}
|
|
16332
|
+
/**
|
|
16333
|
+
* Event families that mutate the main transcript: only the main agent's
|
|
16334
|
+
* (`agentId === 'main'`) events of these kinds may be applied, journaled and
|
|
16335
|
+
* broadcast. Subagents and background tasks each emit their own turn.started
|
|
16336
|
+
* plus hundreds of text/tool events — mixing them into the main transcript
|
|
16337
|
+
* produces a body where two streams alternate block by block, blows the tool
|
|
16338
|
+
* list up to 97 entries and the model badge to 4 lines (reproduced from a real
|
|
16339
|
+
* session). Subagent presence is carried by subagent.* events instead, never by
|
|
16340
|
+
* the transcript.
|
|
16341
|
+
*/
|
|
16342
|
+
const MAIN_TRANSCRIPT_EVENTS = new Set([
|
|
16343
|
+
"turn.started",
|
|
16344
|
+
"turn.ended",
|
|
16345
|
+
"assistant.delta",
|
|
16346
|
+
"thinking.delta",
|
|
16347
|
+
"tool.call.started",
|
|
16348
|
+
"tool.result"
|
|
16349
|
+
]);
|
|
16350
|
+
/**
|
|
16351
|
+
* Whether a journal entry already carries a message (if so it needs no seeding
|
|
16352
|
+
* from core history).
|
|
16353
|
+
*/
|
|
16354
|
+
function isMessageBearingEntry(entry) {
|
|
16355
|
+
if ("type" in entry && entry.type === "user_message") return true;
|
|
16356
|
+
const payload = entry.payload;
|
|
16357
|
+
return payload.type === "web.message.finalized" || payload.type === "turn.started";
|
|
16358
|
+
}
|
|
16359
|
+
/**
|
|
16360
|
+
* Core context history → web ChatMessage seed.
|
|
16361
|
+
*
|
|
16362
|
+
* Only "real user input + assistant bodies" are carried over: core tool records
|
|
16363
|
+
* and the web-side ToolMessage are different shapes, so a forced mapping would
|
|
16364
|
+
* invent fake cards. Messages with a non-user origin (injections, system
|
|
16365
|
+
* triggers, background-task notices, skill-activation envelopes, compaction
|
|
16366
|
+
* summaries) are protocol content meant for the model and must not be rendered
|
|
16367
|
+
* as user turns.
|
|
16368
|
+
*/
|
|
16369
|
+
function contextHistoryToChatMessages(history) {
|
|
16370
|
+
const out = [];
|
|
16371
|
+
for (const message of history) {
|
|
16372
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
16373
|
+
if (isInternalMessage(message)) continue;
|
|
16374
|
+
if (message.origin !== void 0 && message.origin.kind !== "user") continue;
|
|
16375
|
+
const text = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
|
|
16376
|
+
if (text.length === 0) continue;
|
|
16377
|
+
out.push({
|
|
16378
|
+
role: message.role,
|
|
16379
|
+
content: text,
|
|
16380
|
+
tools: []
|
|
16381
|
+
});
|
|
16477
16382
|
}
|
|
16383
|
+
return out;
|
|
16478
16384
|
}
|
|
16479
16385
|
async function listPersistedSessions(homeDir) {
|
|
16480
16386
|
const dir = getSessionsDir(homeDir);
|
|
@@ -16653,10 +16559,11 @@ const MAX_INLINE_DIFF_BYTES = 512 * 1024;
|
|
|
16653
16559
|
* Single-file patch. `repoPath` is **repository-root relative** — the same base
|
|
16654
16560
|
* `git status` reports, so the UI can hand it straight back.
|
|
16655
16561
|
*
|
|
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
|
|
16562
|
+
* Two cases used to return an empty patch, which read as "clicking did
|
|
16563
|
+
* nothing": nested files (the old code re-prefixed workdir-relative paths with
|
|
16564
|
+
* `../..`, resolving them outside the repo) and untracked files (`git diff` has
|
|
16565
|
+
* nothing to say about them). The second case now gets a synthesised added-file
|
|
16566
|
+
* patch.
|
|
16660
16567
|
*/
|
|
16661
16568
|
async function getGitFileDiff(workDir, repoPath) {
|
|
16662
16569
|
const rel = normalizeNumstatPath(repoPath);
|
|
@@ -16774,7 +16681,27 @@ var WebSession = class {
|
|
|
16774
16681
|
* snapshot (`web.message.finalized`) — the only source that survives a
|
|
16775
16682
|
* server restart (deltas are volatile by design).
|
|
16776
16683
|
*/
|
|
16777
|
-
|
|
16684
|
+
/**
|
|
16685
|
+
* In-flight main-agent turn messages, keyed by `turnId`.
|
|
16686
|
+
* A single pointer (the old liveAssistant) merged several streams into one
|
|
16687
|
+
* message when an interrupted turn overlapped a freshly started one, or when
|
|
16688
|
+
* subagents ran concurrently, producing interleaved garbage — hence one slot
|
|
16689
|
+
* per turn.
|
|
16690
|
+
*/
|
|
16691
|
+
liveAssistants = /* @__PURE__ */ new Map();
|
|
16692
|
+
/**
|
|
16693
|
+
* In-flight message of one turn; without a turnId it falls back to the most
|
|
16694
|
+
* recently started one (older-frame compatibility).
|
|
16695
|
+
*/
|
|
16696
|
+
liveFor(turnId) {
|
|
16697
|
+
if (typeof turnId === "number") {
|
|
16698
|
+
const hit = this.liveAssistants.get(turnId);
|
|
16699
|
+
if (hit) return hit;
|
|
16700
|
+
}
|
|
16701
|
+
let latest = null;
|
|
16702
|
+
for (const message of this.liveAssistants.values()) latest = message;
|
|
16703
|
+
return latest;
|
|
16704
|
+
}
|
|
16778
16705
|
liveTurnCount = 0;
|
|
16779
16706
|
/** pendingMsgId awaiting compaction completion/cancellation event. */
|
|
16780
16707
|
pendingCompactionMsgId = null;
|
|
@@ -16888,6 +16815,63 @@ var WebSession = class {
|
|
|
16888
16815
|
const msgs = this.buildMessages();
|
|
16889
16816
|
if (msgs.length > 0 && !this.customTitle) this.title = deriveTitle(msgs);
|
|
16890
16817
|
}
|
|
16818
|
+
/**
|
|
16819
|
+
* Core wire history seed: filled in once by the activation path when the
|
|
16820
|
+
* journal holds no message records.
|
|
16821
|
+
*/
|
|
16822
|
+
seededMessages = [];
|
|
16823
|
+
/**
|
|
16824
|
+
* Mark the journal untrustworthy (byte-interleaving damage, see
|
|
16825
|
+
* journal-writer.ts).
|
|
16826
|
+
*
|
|
16827
|
+
* The loaded journal entries and any legacy userMessages are dropped so the
|
|
16828
|
+
* message list is presented from the core transcript seed instead; state
|
|
16829
|
+
* snapshots (goal/todo, …) were already cached by loadFromPersisted and are
|
|
16830
|
+
* unaffected. nextSeq does not roll back: live entries keep appending and
|
|
16831
|
+
* rendering as usual after activation, so new messages are untouched (a seq
|
|
16832
|
+
* gap on the client takes the existing gap → snapshot refetch path).
|
|
16833
|
+
*/
|
|
16834
|
+
markDamaged(reason) {
|
|
16835
|
+
const droppedEntries = this.journal.length;
|
|
16836
|
+
const droppedUsers = this.userMessages.length;
|
|
16837
|
+
this.journal.length = 0;
|
|
16838
|
+
this.userMessages.length = 0;
|
|
16839
|
+
log.warn("web: session journal damaged; messages rebuilt from core transcript", {
|
|
16840
|
+
sessionId: this.sessionId,
|
|
16841
|
+
reason,
|
|
16842
|
+
droppedEntries,
|
|
16843
|
+
droppedUsers
|
|
16844
|
+
});
|
|
16845
|
+
}
|
|
16846
|
+
/**
|
|
16847
|
+
* Whether the current message list holds at least one assistant message with
|
|
16848
|
+
* a body. Used to decide how far journal remnants can be trusted: user
|
|
16849
|
+
* messages are persisted separately (they always carry text), while the
|
|
16850
|
+
* byte-interleaving damage only ever ate assistant bodies — so only the
|
|
16851
|
+
* assistant side is inspected.
|
|
16852
|
+
*/
|
|
16853
|
+
hasAnyAssistantContent() {
|
|
16854
|
+
return this.buildMessages().some((message) => message.role === "assistant" && message.content.trim().length > 0);
|
|
16855
|
+
}
|
|
16856
|
+
/**
|
|
16857
|
+
* Seed messages from the core session history. An older session's web
|
|
16858
|
+
* journal may hold nothing but state entries (or predate durable snapshots),
|
|
16859
|
+
* which leaves the chat area empty after a resume while the wire history is
|
|
16860
|
+
* intact. The seed occupies seq 1..N and lifts nextSeq past it, keeping seq
|
|
16861
|
+
* monotonic with live events and consistent with the getMessagesOlder cursor.
|
|
16862
|
+
*/
|
|
16863
|
+
seedHistory(messages) {
|
|
16864
|
+
if (messages.length === 0 || this.seededMessages.length > 0) return;
|
|
16865
|
+
let seq = 1;
|
|
16866
|
+
for (const message of messages) {
|
|
16867
|
+
this.seededMessages.push({
|
|
16868
|
+
...message,
|
|
16869
|
+
seq
|
|
16870
|
+
});
|
|
16871
|
+
seq += 1;
|
|
16872
|
+
}
|
|
16873
|
+
if (seq - 1 >= this.nextSeq) this.nextSeq = seq;
|
|
16874
|
+
}
|
|
16891
16875
|
async updateTitle() {
|
|
16892
16876
|
if (this.customTitle) return;
|
|
16893
16877
|
const newTitle = deriveTitle(this.buildMessages());
|
|
@@ -16939,11 +16923,12 @@ var WebSession = class {
|
|
|
16939
16923
|
if (!this.session) return;
|
|
16940
16924
|
this.unsubscribe = this.session.onEvent((event) => {
|
|
16941
16925
|
if ((event.type === "goal.updated" || event.type === "todo.updated") && event.agentId !== "main") return;
|
|
16926
|
+
if (MAIN_TRANSCRIPT_EVENTS.has(event.type) && event.agentId !== "main") return;
|
|
16942
16927
|
if (event.type === "turn.started") {
|
|
16943
16928
|
this.busy = true;
|
|
16944
16929
|
this.activeTurns += 1;
|
|
16945
16930
|
this.liveTurnCount += 1;
|
|
16946
|
-
this.
|
|
16931
|
+
this.liveAssistants.set(event.turnId, {
|
|
16947
16932
|
role: "assistant",
|
|
16948
16933
|
content: "",
|
|
16949
16934
|
tools: [],
|
|
@@ -16959,11 +16944,11 @@ var WebSession = class {
|
|
|
16959
16944
|
tokens: null,
|
|
16960
16945
|
tokensPerSec: null
|
|
16961
16946
|
}
|
|
16962
|
-
};
|
|
16947
|
+
});
|
|
16963
16948
|
} else if (event.type === "turn.ended") {
|
|
16964
16949
|
this.activeTurns = Math.max(0, this.activeTurns - 1);
|
|
16965
16950
|
this.busy = this.activeTurns > 0;
|
|
16966
|
-
const live = this.
|
|
16951
|
+
const live = this.liveFor(event.turnId);
|
|
16967
16952
|
if (event.reason === "failed" && live) live.isError = true;
|
|
16968
16953
|
if (live) {
|
|
16969
16954
|
live.turnStats = {
|
|
@@ -16983,13 +16968,13 @@ var WebSession = class {
|
|
|
16983
16968
|
message: live
|
|
16984
16969
|
});
|
|
16985
16970
|
}
|
|
16986
|
-
this.
|
|
16971
|
+
this.liveAssistants.delete(event.turnId);
|
|
16987
16972
|
this.trackPending(this.updateTitle());
|
|
16988
16973
|
} else if (event.type === "assistant.delta") {
|
|
16989
|
-
const live = this.
|
|
16974
|
+
const live = this.liveFor(event.turnId);
|
|
16990
16975
|
if (live) live.content += event.delta;
|
|
16991
16976
|
} else if (event.type === "thinking.delta") {
|
|
16992
|
-
const live = this.
|
|
16977
|
+
const live = this.liveFor(event.turnId);
|
|
16993
16978
|
if (live) {
|
|
16994
16979
|
const t = live.tools.find((x) => x.name === "thinking");
|
|
16995
16980
|
if (t) t.output = (t.output ?? "") + event.delta;
|
|
@@ -17001,7 +16986,7 @@ var WebSession = class {
|
|
|
17001
16986
|
});
|
|
17002
16987
|
}
|
|
17003
16988
|
} else if (event.type === "tool.call.started") {
|
|
17004
|
-
const live = this.
|
|
16989
|
+
const live = this.liveFor(event.turnId);
|
|
17005
16990
|
if (live) {
|
|
17006
16991
|
live.tools.push({
|
|
17007
16992
|
toolCallId: event.toolCallId,
|
|
@@ -17011,7 +16996,7 @@ var WebSession = class {
|
|
|
17011
16996
|
if (live.turnStats) live.turnStats.step += 1;
|
|
17012
16997
|
}
|
|
17013
16998
|
} else if (event.type === "tool.result") {
|
|
17014
|
-
const live = this.
|
|
16999
|
+
const live = this.liveFor(event.turnId);
|
|
17015
17000
|
if (live) {
|
|
17016
17001
|
const tool = live.tools.find((t) => t.toolCallId === event.toolCallId);
|
|
17017
17002
|
if (tool) {
|
|
@@ -17492,7 +17477,7 @@ var WebSession = class {
|
|
|
17492
17477
|
workDir: this.workDir,
|
|
17493
17478
|
title: this.title,
|
|
17494
17479
|
createdAt: this.createdAt,
|
|
17495
|
-
messageCount: this.buildMessages().length,
|
|
17480
|
+
messageCount: this.isActive ? this.buildMessages().length : -1,
|
|
17496
17481
|
active: this.isActive
|
|
17497
17482
|
};
|
|
17498
17483
|
}
|
|
@@ -17686,7 +17671,7 @@ var WebSession = class {
|
|
|
17686
17671
|
return this.connections.size;
|
|
17687
17672
|
}
|
|
17688
17673
|
buildMessages() {
|
|
17689
|
-
const messages = [];
|
|
17674
|
+
const messages = [...this.seededMessages];
|
|
17690
17675
|
let currentAssistant = null;
|
|
17691
17676
|
let userIndex = 0;
|
|
17692
17677
|
let turnCount = 0;
|
|
@@ -17872,6 +17857,36 @@ async function handleGoalRoute(req, res, url, resolveSession) {
|
|
|
17872
17857
|
}
|
|
17873
17858
|
return true;
|
|
17874
17859
|
}
|
|
17860
|
+
/**
|
|
17861
|
+
* Validate the optional workDir argument of session creation; undefined means
|
|
17862
|
+
* "reuse the server process directory".
|
|
17863
|
+
*
|
|
17864
|
+
* Trade-off: web is a local tool and the browser shares the CLI's trust level,
|
|
17865
|
+
* so no directory allowlist is introduced; but only an explicitly provided
|
|
17866
|
+
* absolute path is accepted — relative paths and `..` segments are rejected
|
|
17867
|
+
* (resolving them away instead of rejecting would amount to permitting
|
|
17868
|
+
* traversal), and the target must exist, be a directory and be readable and
|
|
17869
|
+
* writable. An invalid directory fails loudly with a 4xx at the moment the
|
|
17870
|
+
* session is created, rather than letting the agent wander into some
|
|
17871
|
+
* unexpected location later.
|
|
17872
|
+
*/
|
|
17873
|
+
async function validateSessionWorkDir(raw) {
|
|
17874
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
17875
|
+
if (typeof raw !== "string") throw new HttpError(400, "workDir 必须是字符串", "request.invalid");
|
|
17876
|
+
const value = raw.trim();
|
|
17877
|
+
if (!value) return void 0;
|
|
17878
|
+
if (!isAbsolute(value)) throw new HttpError(400, `workDir 必须是绝对路径:${value}`, "request.invalid");
|
|
17879
|
+
if (value.split(/[\\/]+/).includes("..")) throw new HttpError(400, `workDir 不允许包含 ".." 遍历段:${value}`, "request.invalid");
|
|
17880
|
+
const st = await stat(value).catch(() => null);
|
|
17881
|
+
if (!st) throw new HttpError(404, `目录不存在:${value}`, "request.invalid");
|
|
17882
|
+
if (!st.isDirectory()) throw new HttpError(400, `不是目录:${value}`, "request.invalid");
|
|
17883
|
+
try {
|
|
17884
|
+
await access(value, constants.R_OK | constants.W_OK);
|
|
17885
|
+
} catch {
|
|
17886
|
+
throw new HttpError(403, `目录不可读写:${value}`, "request.invalid");
|
|
17887
|
+
}
|
|
17888
|
+
return value;
|
|
17889
|
+
}
|
|
17875
17890
|
var SessionManager = class {
|
|
17876
17891
|
sessions = /* @__PURE__ */ new Map();
|
|
17877
17892
|
/** In-flight activation promises to deduplicate concurrent calls. */
|
|
@@ -17894,7 +17909,7 @@ var SessionManager = class {
|
|
|
17894
17909
|
await ensureSessionsDir(this.homeDir);
|
|
17895
17910
|
const metas = await listPersistedSessions(this.homeDir);
|
|
17896
17911
|
for (const meta of metas) {
|
|
17897
|
-
const entries = await loadJournal(this.homeDir, meta.sessionId);
|
|
17912
|
+
const { entries } = await loadJournal(this.homeDir, meta.sessionId);
|
|
17898
17913
|
const ws = new WebSession(null, {
|
|
17899
17914
|
sessionId: meta.sessionId,
|
|
17900
17915
|
coreSessionId: meta.coreSessionId ?? meta.sessionId,
|
|
@@ -17908,11 +17923,107 @@ var SessionManager = class {
|
|
|
17908
17923
|
ws.loadFromPersisted(entries);
|
|
17909
17924
|
this.sessions.set(meta.sessionId, ws);
|
|
17910
17925
|
}
|
|
17926
|
+
try {
|
|
17927
|
+
const index = await readSessionIndex(this.homeDir, join(this.homeDir, "sessions"));
|
|
17928
|
+
const managerWorkDir = normalizeWorkDir(this.workDir);
|
|
17929
|
+
for (const entry of index.values()) try {
|
|
17930
|
+
if (normalizeWorkDir(entry.workDir) !== managerWorkDir) continue;
|
|
17931
|
+
if (this.sessions.has(entry.sessionId)) continue;
|
|
17932
|
+
if (!existsSync(entry.sessionDir)) continue;
|
|
17933
|
+
let title = "未命名会话";
|
|
17934
|
+
let createdAt = Date.now();
|
|
17935
|
+
try {
|
|
17936
|
+
const raw = await readFile(join(entry.sessionDir, "state.json"), "utf8");
|
|
17937
|
+
const state = JSON.parse(raw);
|
|
17938
|
+
if (typeof state.title === "string" && state.title) title = state.title;
|
|
17939
|
+
if (state.createdAt) {
|
|
17940
|
+
const t = typeof state.createdAt === "number" ? state.createdAt : Date.parse(state.createdAt);
|
|
17941
|
+
if (Number.isFinite(t)) createdAt = t;
|
|
17942
|
+
}
|
|
17943
|
+
} catch {}
|
|
17944
|
+
const session = new WebSession(null, {
|
|
17945
|
+
sessionId: entry.sessionId,
|
|
17946
|
+
coreSessionId: entry.sessionId,
|
|
17947
|
+
workDir: entry.workDir,
|
|
17948
|
+
permission: this.permission,
|
|
17949
|
+
yolo: this.yolo,
|
|
17950
|
+
homeDir: this.homeDir,
|
|
17951
|
+
createdAt,
|
|
17952
|
+
title
|
|
17953
|
+
});
|
|
17954
|
+
this.sessions.set(entry.sessionId, session);
|
|
17955
|
+
} catch (error) {
|
|
17956
|
+
log.warn("web: skipping unreadable session index entry", {
|
|
17957
|
+
sessionId: entry.sessionId,
|
|
17958
|
+
sessionDir: entry.sessionDir,
|
|
17959
|
+
error: errorMessage(error)
|
|
17960
|
+
});
|
|
17961
|
+
}
|
|
17962
|
+
} catch (error) {
|
|
17963
|
+
log.warn("web: session index unreadable; falling back to web mirror + directory scan", {
|
|
17964
|
+
homeDir: this.homeDir,
|
|
17965
|
+
error: errorMessage(error)
|
|
17966
|
+
});
|
|
17967
|
+
}
|
|
17968
|
+
const wdKey = encodeWorkDirKey(this.workDir);
|
|
17969
|
+
const wdDir = join(this.homeDir, "sessions", wdKey);
|
|
17970
|
+
try {
|
|
17971
|
+
if (existsSync(wdDir)) for (const name of await readdir(wdDir)) {
|
|
17972
|
+
if (!name.startsWith("session_")) continue;
|
|
17973
|
+
if (this.sessions.has(name)) continue;
|
|
17974
|
+
try {
|
|
17975
|
+
const sessionDir = join(wdDir, name);
|
|
17976
|
+
let title = "未命名会话";
|
|
17977
|
+
let createdAt = Date.now();
|
|
17978
|
+
try {
|
|
17979
|
+
const raw = await readFile(join(sessionDir, "state.json"), "utf8");
|
|
17980
|
+
const state = JSON.parse(raw);
|
|
17981
|
+
if (typeof state.title === "string" && state.title) title = state.title;
|
|
17982
|
+
if (state.createdAt) {
|
|
17983
|
+
const t = typeof state.createdAt === "number" ? state.createdAt : Date.parse(state.createdAt);
|
|
17984
|
+
if (Number.isFinite(t)) createdAt = t;
|
|
17985
|
+
}
|
|
17986
|
+
} catch {}
|
|
17987
|
+
const session = new WebSession(null, {
|
|
17988
|
+
sessionId: name,
|
|
17989
|
+
coreSessionId: name,
|
|
17990
|
+
workDir: this.workDir,
|
|
17991
|
+
permission: this.permission,
|
|
17992
|
+
yolo: this.yolo,
|
|
17993
|
+
homeDir: this.homeDir,
|
|
17994
|
+
createdAt,
|
|
17995
|
+
title
|
|
17996
|
+
});
|
|
17997
|
+
this.sessions.set(name, session);
|
|
17998
|
+
} catch (error) {
|
|
17999
|
+
log.warn("web: skipping unreadable core session directory", {
|
|
18000
|
+
wdKey,
|
|
18001
|
+
sessionId: name,
|
|
18002
|
+
error: errorMessage(error)
|
|
18003
|
+
});
|
|
18004
|
+
}
|
|
18005
|
+
}
|
|
18006
|
+
} catch (error) {
|
|
18007
|
+
log.warn("web: core session bucket scan failed", {
|
|
18008
|
+
wdKey,
|
|
18009
|
+
dir: wdDir,
|
|
18010
|
+
error: errorMessage(error)
|
|
18011
|
+
});
|
|
18012
|
+
}
|
|
17911
18013
|
log.info("web: loaded persisted sessions", { count: this.sessions.size });
|
|
17912
18014
|
}
|
|
17913
|
-
|
|
18015
|
+
/** Server process directory: the default workspace when session creation omits workDir. */
|
|
18016
|
+
get defaultWorkDir() {
|
|
18017
|
+
return this.workDir;
|
|
18018
|
+
}
|
|
18019
|
+
/**
|
|
18020
|
+
* Create a session; an omitted workDir reuses the server process directory
|
|
18021
|
+
* and is passed to the harness as-is.
|
|
18022
|
+
*/
|
|
18023
|
+
async createSession(workDir) {
|
|
18024
|
+
const dir = workDir ?? this.workDir;
|
|
17914
18025
|
const session = await this.harness.createSession({
|
|
17915
|
-
workDir:
|
|
18026
|
+
workDir: dir,
|
|
17916
18027
|
model: this.model,
|
|
17917
18028
|
permission: this.permission
|
|
17918
18029
|
});
|
|
@@ -17920,7 +18031,7 @@ var SessionManager = class {
|
|
|
17920
18031
|
const createdAt = Date.now();
|
|
17921
18032
|
const webSession = new WebSession(session, {
|
|
17922
18033
|
sessionId,
|
|
17923
|
-
workDir:
|
|
18034
|
+
workDir: dir,
|
|
17924
18035
|
permission: this.permission,
|
|
17925
18036
|
yolo: this.yolo,
|
|
17926
18037
|
homeDir: this.homeDir,
|
|
@@ -17937,7 +18048,7 @@ var SessionManager = class {
|
|
|
17937
18048
|
this.sessions.set(sessionId, webSession);
|
|
17938
18049
|
log.info("web: session created", {
|
|
17939
18050
|
sessionId,
|
|
17940
|
-
workDir:
|
|
18051
|
+
workDir: dir
|
|
17941
18052
|
});
|
|
17942
18053
|
return webSession;
|
|
17943
18054
|
}
|
|
@@ -17982,8 +18093,19 @@ var SessionManager = class {
|
|
|
17982
18093
|
coreSessionId: session.id,
|
|
17983
18094
|
onFork: (id) => this.forkSession(id)
|
|
17984
18095
|
});
|
|
17985
|
-
const entries = await loadJournal(this.homeDir, sessionId);
|
|
18096
|
+
const { entries, corrupt } = await loadJournal(this.homeDir, sessionId);
|
|
17986
18097
|
reactivated.loadFromPersisted(entries);
|
|
18098
|
+
const journalHasContent = reactivated.hasAnyAssistantContent();
|
|
18099
|
+
if (corrupt > 0 && !journalHasContent) reactivated.markDamaged(`journal has ${corrupt} corrupt lines and no parsed message carries a body`);
|
|
18100
|
+
if (!entries.some(isMessageBearingEntry) || corrupt > 0 && !journalHasContent) try {
|
|
18101
|
+
const context = await session.getContext();
|
|
18102
|
+
reactivated.seedHistory(contextHistoryToChatMessages(context.history));
|
|
18103
|
+
} catch (error) {
|
|
18104
|
+
log.warn("web: seeding history from core transcript failed", {
|
|
18105
|
+
sessionId,
|
|
18106
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18107
|
+
});
|
|
18108
|
+
}
|
|
17987
18109
|
try {
|
|
17988
18110
|
await reactivated.ready;
|
|
17989
18111
|
} catch (error) {
|
|
@@ -18038,7 +18160,7 @@ var SessionManager = class {
|
|
|
18038
18160
|
const title = `${source.getTitle()} (fork)`;
|
|
18039
18161
|
const webSession = new WebSession(session, {
|
|
18040
18162
|
sessionId: newId,
|
|
18041
|
-
workDir:
|
|
18163
|
+
workDir: source.getMetadata().workDir,
|
|
18042
18164
|
permission: source.getMetadata().permission,
|
|
18043
18165
|
yolo: this.yolo,
|
|
18044
18166
|
homeDir: this.homeDir,
|
|
@@ -18052,6 +18174,16 @@ var SessionManager = class {
|
|
|
18052
18174
|
await writeFile(getJournalPath(this.homeDir, newId), lines);
|
|
18053
18175
|
}
|
|
18054
18176
|
webSession.loadFromPersisted(entries);
|
|
18177
|
+
if (!entries.some(isMessageBearingEntry)) try {
|
|
18178
|
+
const context = await session.getContext();
|
|
18179
|
+
webSession.seedHistory(contextHistoryToChatMessages(context.history));
|
|
18180
|
+
} catch (error) {
|
|
18181
|
+
log.warn("web: seeding forked history from core transcript failed", {
|
|
18182
|
+
sourceId,
|
|
18183
|
+
newId,
|
|
18184
|
+
error: errorMessage(error)
|
|
18185
|
+
});
|
|
18186
|
+
}
|
|
18055
18187
|
try {
|
|
18056
18188
|
await webSession.ready;
|
|
18057
18189
|
} catch (error) {
|
|
@@ -18087,8 +18219,33 @@ var SessionManager = class {
|
|
|
18087
18219
|
async delete(sessionId) {
|
|
18088
18220
|
const ws = this.sessions.get(sessionId);
|
|
18089
18221
|
if (!ws) return false;
|
|
18222
|
+
const workDir = ws.workDir;
|
|
18223
|
+
const coreId = ws.getCoreSessionId();
|
|
18090
18224
|
await ws.delete();
|
|
18091
18225
|
this.sessions.delete(sessionId);
|
|
18226
|
+
const coreDir = join(this.homeDir, "sessions", encodeWorkDirKey(workDir), coreId);
|
|
18227
|
+
let moved = true;
|
|
18228
|
+
try {
|
|
18229
|
+
if (existsSync(coreDir)) {
|
|
18230
|
+
const trashRoot = join(this.homeDir, "trash");
|
|
18231
|
+
await mkdir(trashRoot, { recursive: true });
|
|
18232
|
+
await rename(coreDir, join(trashRoot, `${coreId}-${Date.now()}`));
|
|
18233
|
+
} else log.warn("web: core session directory not found on delete", {
|
|
18234
|
+
sessionId,
|
|
18235
|
+
coreId,
|
|
18236
|
+
workDir,
|
|
18237
|
+
coreDir
|
|
18238
|
+
});
|
|
18239
|
+
} catch (error) {
|
|
18240
|
+
moved = false;
|
|
18241
|
+
log.warn("web: moving session directory to trash failed; index entry kept", {
|
|
18242
|
+
sessionId,
|
|
18243
|
+
coreId,
|
|
18244
|
+
coreDir,
|
|
18245
|
+
error: errorMessage(error)
|
|
18246
|
+
});
|
|
18247
|
+
}
|
|
18248
|
+
if (moved) await removeSessionIndexEntry(this.homeDir, coreId);
|
|
18092
18249
|
return true;
|
|
18093
18250
|
}
|
|
18094
18251
|
/** List available model aliases from the harness config (reloaded). */
|
|
@@ -18831,17 +18988,18 @@ async function runWebServer(opts) {
|
|
|
18831
18988
|
res.end(JSON.stringify(manager.list()));
|
|
18832
18989
|
return;
|
|
18833
18990
|
}
|
|
18991
|
+
if (url === `${API_PREFIX}/workdir` && method === "GET") {
|
|
18992
|
+
sendJson(res, 200, { workDir: manager.defaultWorkDir });
|
|
18993
|
+
return;
|
|
18994
|
+
}
|
|
18834
18995
|
if (url === `${API_PREFIX}/sessions` && method === "POST") {
|
|
18835
18996
|
try {
|
|
18836
|
-
const
|
|
18997
|
+
const dir = await validateSessionWorkDir((await readJsonBody(req))["workDir"]);
|
|
18998
|
+
const ws = await manager.createSession(dir);
|
|
18837
18999
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
18838
19000
|
res.end(JSON.stringify(ws.getListItem()));
|
|
18839
19001
|
} catch (error) {
|
|
18840
|
-
res
|
|
18841
|
-
res.end(JSON.stringify({
|
|
18842
|
-
code: 500,
|
|
18843
|
-
message: String(error)
|
|
18844
|
-
}));
|
|
19002
|
+
sendHttpError(res, error);
|
|
18845
19003
|
}
|
|
18846
19004
|
return;
|
|
18847
19005
|
}
|
|
@@ -19137,7 +19295,10 @@ async function runWebServer(opts) {
|
|
|
19137
19295
|
if (url === "/" || url === "/index.html") {
|
|
19138
19296
|
try {
|
|
19139
19297
|
const html = await readFile(join(publicDir, "index.html"), "utf-8");
|
|
19140
|
-
res.writeHead(200, {
|
|
19298
|
+
res.writeHead(200, {
|
|
19299
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
19300
|
+
"Cache-Control": "no-store"
|
|
19301
|
+
});
|
|
19141
19302
|
res.end(html);
|
|
19142
19303
|
} catch {
|
|
19143
19304
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -19150,7 +19311,9 @@ async function runWebServer(opts) {
|
|
|
19150
19311
|
const filePath = join(publicDir, safeUrl);
|
|
19151
19312
|
const contentType = contentTypes[filePath.split(".").pop() ?? ""] ?? "application/octet-stream";
|
|
19152
19313
|
const data = await readFile(filePath);
|
|
19153
|
-
|
|
19314
|
+
const headers = { "Content-Type": contentType };
|
|
19315
|
+
if (safeUrl.startsWith("/assets/")) headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
|
19316
|
+
res.writeHead(200, headers);
|
|
19154
19317
|
res.end(data);
|
|
19155
19318
|
} catch {
|
|
19156
19319
|
res.writeHead(404, { "Content-Type": "text/plain" });
|