scream-code 0.16.0 → 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-N_PRvDIk.mjs → app-V7s7I7Nr.mjs} +581 -413
- package/dist/{dispatch-5S52okPl.mjs → dispatch-BRkUiHzo.mjs} +1 -1
- package/dist/{dispatch-CsrteWRw.mjs → dispatch-D0mYcf1X.mjs} +1299 -268
- 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-BVQaKy1Z.mjs → text-input-dialog-CzJKBIoN.mjs} +42 -12
- package/dist/{text-input-dialog-B0PROY20.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) {
|
|
@@ -2102,6 +2066,7 @@ function childRequestFieldsFromArgs(args) {
|
|
|
2102
2066
|
requestType: kindOf(args["request_type"]),
|
|
2103
2067
|
message,
|
|
2104
2068
|
needs: textOf(args["needs"]),
|
|
2069
|
+
expecting: textOf(payload["expecting"]),
|
|
2105
2070
|
artifacts: listOf("artifacts"),
|
|
2106
2071
|
evidence: listOf("evidence"),
|
|
2107
2072
|
missing: listOf("missing")
|
|
@@ -2134,6 +2099,7 @@ function childRequestFieldsFromNotification(text) {
|
|
|
2134
2099
|
const requestType = head !== null ? kindOf(head[1]) : "info";
|
|
2135
2100
|
const messageParts = [head !== null && head[2] !== void 0 ? head[2] : first];
|
|
2136
2101
|
let needs;
|
|
2102
|
+
let expecting;
|
|
2137
2103
|
const lists = {};
|
|
2138
2104
|
for (const line of body.slice(1)) {
|
|
2139
2105
|
const separator = line.indexOf(": ");
|
|
@@ -2141,6 +2107,7 @@ function childRequestFieldsFromNotification(text) {
|
|
|
2141
2107
|
const value = separator < 0 ? "" : line.slice(separator + 2).trim();
|
|
2142
2108
|
const listKey = (key === "artifacts" || key === "evidence" || key === "missing") && value.startsWith("[") && value.endsWith("]") ? key : void 0;
|
|
2143
2109
|
if (key === "needs" && value.length > 0) needs = value;
|
|
2110
|
+
else if (key === "expecting" && value.length > 0) expecting = value;
|
|
2144
2111
|
else if (listKey !== void 0) lists[listKey] = bracketedListOf(value);
|
|
2145
2112
|
else messageParts.push(line);
|
|
2146
2113
|
}
|
|
@@ -2150,6 +2117,7 @@ function childRequestFieldsFromNotification(text) {
|
|
|
2150
2117
|
requestType,
|
|
2151
2118
|
message,
|
|
2152
2119
|
needs,
|
|
2120
|
+
expecting,
|
|
2153
2121
|
artifacts: lists["artifacts"],
|
|
2154
2122
|
evidence: lists["evidence"],
|
|
2155
2123
|
missing: lists["missing"]
|
|
@@ -2164,6 +2132,7 @@ function renderChildRequestNotice(fields, name) {
|
|
|
2164
2132
|
});
|
|
2165
2133
|
const parts = [previewText(fields.message)];
|
|
2166
2134
|
if (fields.needs !== void 0) parts.push(t("transcript.child_request_needs", { value: previewText(fields.needs) }));
|
|
2135
|
+
if (fields.expecting !== void 0) parts.push(t("transcript.child_request_expecting", { value: previewText(fields.expecting) }));
|
|
2167
2136
|
for (const entry of [
|
|
2168
2137
|
["artifacts", fields.artifacts],
|
|
2169
2138
|
["evidence", fields.evidence],
|
|
@@ -2178,233 +2147,6 @@ function renderChildRequestNotice(fields, name) {
|
|
|
2178
2147
|
};
|
|
2179
2148
|
}
|
|
2180
2149
|
//#endregion
|
|
2181
|
-
//#region src/tui/utils/subagent-slots.ts
|
|
2182
|
-
/** Default subagent types, in the fixed display order of the slots. */
|
|
2183
|
-
const DEFAULT_SUBAGENT_TYPES = [
|
|
2184
|
-
"coder",
|
|
2185
|
-
"explore",
|
|
2186
|
-
"plan",
|
|
2187
|
-
"verify",
|
|
2188
|
-
"reviewer",
|
|
2189
|
-
"oracle",
|
|
2190
|
-
"worker",
|
|
2191
|
-
"writer"
|
|
2192
|
-
];
|
|
2193
|
-
/** How long a `requesting` overlay stays visible after a subagent asks the
|
|
2194
|
-
* parent for help. The request itself returns at once and the agent keeps
|
|
2195
|
-
* working, so this is a display window, not a blocked state. */
|
|
2196
|
-
const REQUESTING_WINDOW_MS = 5e3;
|
|
2197
|
-
var SubagentSlots = class {
|
|
2198
|
-
slots = /* @__PURE__ */ new Map();
|
|
2199
|
-
instances = /* @__PURE__ */ new Map();
|
|
2200
|
-
/** Live instance count per type: a slot only returns to idle when the LAST
|
|
2201
|
-
* instance of that type terminates (WolfPack spawns several coders at once
|
|
2202
|
-
* and they share one visual slot). */
|
|
2203
|
-
runningByType = /* @__PURE__ */ new Map();
|
|
2204
|
-
/** main-side SendSubagentMessage tool callId → target subagent agentId. */
|
|
2205
|
-
messagingByCall = /* @__PURE__ */ new Map();
|
|
2206
|
-
/** Status to restore per TYPE when a messaging window closes, captured from
|
|
2207
|
-
* the first open window; concurrent windows on the same type keep counting
|
|
2208
|
-
* and the slot only exits messaging when the last window closes. */
|
|
2209
|
-
messagingPrev = /* @__PURE__ */ new Map();
|
|
2210
|
-
/** Slot type → open `requesting` overlay. Keyed by type so several live
|
|
2211
|
-
* instances of one type share a single marker; the real activity state stays
|
|
2212
|
-
* untouched in the slot and shows back up once the overlay expires. */
|
|
2213
|
-
requesting = /* @__PURE__ */ new Map();
|
|
2214
|
-
/** Count of open messaging windows per type (handles same-type siblings). */
|
|
2215
|
-
messagingCount = /* @__PURE__ */ new Map();
|
|
2216
|
-
reset() {
|
|
2217
|
-
this.slots.clear();
|
|
2218
|
-
this.instances.clear();
|
|
2219
|
-
this.runningByType.clear();
|
|
2220
|
-
this.messagingByCall.clear();
|
|
2221
|
-
this.messagingPrev.clear();
|
|
2222
|
-
this.messagingCount.clear();
|
|
2223
|
-
this.requesting.clear();
|
|
2224
|
-
}
|
|
2225
|
-
/**
|
|
2226
|
-
* subagent.spawned. A first spawn starts the slot as working; a second
|
|
2227
|
-
* spawn of the SAME agentId means the main agent resumed it (返工中),
|
|
2228
|
-
* tracked via an instance generation counter.
|
|
2229
|
-
*/
|
|
2230
|
-
onSpawned(agentId, subagentType, description) {
|
|
2231
|
-
const instance = this.instances.get(agentId);
|
|
2232
|
-
if (instance !== void 0) {
|
|
2233
|
-
instance.generation += 1;
|
|
2234
|
-
this.runningByType.set(instance.type, (this.runningByType.get(instance.type) ?? 0) + 1);
|
|
2235
|
-
this.setState(instance.type, "reworking", agentId, description === void 0 ? void 0 : `resume: ${description}`);
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
this.instances.set(agentId, {
|
|
2239
|
-
type: subagentType,
|
|
2240
|
-
generation: 1
|
|
2241
|
-
});
|
|
2242
|
-
this.runningByType.set(subagentType, (this.runningByType.get(subagentType) ?? 0) + 1);
|
|
2243
|
-
this.setState(subagentType, "working", agentId, description);
|
|
2244
|
-
}
|
|
2245
|
-
/** subagent.started. Keeps the current state (spawned already set it); only
|
|
2246
|
-
* used to materialize a slot if lifecycle events raced past us. */
|
|
2247
|
-
onStarted(agentId) {
|
|
2248
|
-
const instance = this.instances.get(agentId);
|
|
2249
|
-
if (instance === void 0) return;
|
|
2250
|
-
const slot = this.ensureSlot(instance.type);
|
|
2251
|
-
if (slot.status === "idle") {
|
|
2252
|
-
slot.status = "working";
|
|
2253
|
-
slot.agentId = agentId;
|
|
2254
|
-
slot.lastActivityAt = Date.now();
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
/** subagent.completed / subagent.failed: the slot goes back to resting only
|
|
2258
|
-
* when the LAST live instance of that type terminates (siblings spawned by
|
|
2259
|
-
* the same tool call keep the slot busy). */
|
|
2260
|
-
onTerminated(agentId) {
|
|
2261
|
-
const instance = this.instances.get(agentId);
|
|
2262
|
-
if (instance === void 0) return;
|
|
2263
|
-
if (this.requesting.get(instance.type)?.agentId === agentId) this.requesting.delete(instance.type);
|
|
2264
|
-
const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
|
|
2265
|
-
if (remaining > 0) {
|
|
2266
|
-
this.runningByType.set(instance.type, remaining);
|
|
2267
|
-
const slot = this.ensureSlot(instance.type);
|
|
2268
|
-
slot.detail = void 0;
|
|
2269
|
-
slot.lastActivityAt = Date.now();
|
|
2270
|
-
return;
|
|
2271
|
-
}
|
|
2272
|
-
this.runningByType.delete(instance.type);
|
|
2273
|
-
this.setState(instance.type, "idle", void 0, void 0);
|
|
2274
|
-
}
|
|
2275
|
-
/**
|
|
2276
|
-
* Routed subagent activity (agentId !== main):
|
|
2277
|
-
* - kind 'tool' → working (tool call in flight)
|
|
2278
|
-
* - kind 'output' → outputting (assistant/thinking delta)
|
|
2279
|
-
* - kind 'toolResult'→ detail only (the agent may keep working/outputting)
|
|
2280
|
-
*/
|
|
2281
|
-
onActivity(agentId, kind, detail) {
|
|
2282
|
-
const instance = this.instances.get(agentId);
|
|
2283
|
-
if (instance === void 0) return;
|
|
2284
|
-
if (kind === "toolResult") {
|
|
2285
|
-
const slot = this.ensureSlot(instance.type);
|
|
2286
|
-
slot.detail = detail;
|
|
2287
|
-
slot.lastActivityAt = Date.now();
|
|
2288
|
-
return;
|
|
2289
|
-
}
|
|
2290
|
-
this.setState(instance.type, kind === "tool" ? "working" : "outputting", agentId, detail);
|
|
2291
|
-
}
|
|
2292
|
-
/** A subagent asked the parent for help. The request returns immediately and
|
|
2293
|
-
* the agent keeps working, so this only raises a transient overlay — the real
|
|
2294
|
-
* activity state stays in the slot and shows back up when it expires. While
|
|
2295
|
-
* `messaging` is on screen the overlay loses (see visibleSlot). */
|
|
2296
|
-
onRequesting(agentId, at = Date.now()) {
|
|
2297
|
-
const instance = this.instances.get(agentId);
|
|
2298
|
-
if (instance === void 0) return;
|
|
2299
|
-
this.requesting.set(instance.type, {
|
|
2300
|
-
until: at + REQUESTING_WINDOW_MS,
|
|
2301
|
-
agentId
|
|
2302
|
-
});
|
|
2303
|
-
const slot = this.ensureSlot(instance.type);
|
|
2304
|
-
slot.agentId = agentId;
|
|
2305
|
-
slot.lastActivityAt = at;
|
|
2306
|
-
}
|
|
2307
|
-
/**
|
|
2308
|
-
* Main-side SendSubagentMessage tool call started: the target subagent's
|
|
2309
|
-
* slot shows messaging until the tool call returns.
|
|
2310
|
-
*/
|
|
2311
|
-
onMessagingStart(toolCallId, targetAgentId, operation) {
|
|
2312
|
-
if (targetAgentId === void 0) return;
|
|
2313
|
-
const instance = this.instances.get(targetAgentId);
|
|
2314
|
-
if (instance === void 0) return;
|
|
2315
|
-
this.messagingByCall.set(toolCallId, targetAgentId);
|
|
2316
|
-
this.requesting.delete(instance.type);
|
|
2317
|
-
const slot = this.ensureSlot(instance.type);
|
|
2318
|
-
if (slot.status !== "messaging") this.messagingPrev.set(instance.type, slot.status);
|
|
2319
|
-
this.messagingCount.set(instance.type, (this.messagingCount.get(instance.type) ?? 0) + 1);
|
|
2320
|
-
this.setState(instance.type, "messaging", targetAgentId, operation === void 0 ? void 0 : `message (${operation})`);
|
|
2321
|
-
}
|
|
2322
|
-
/** Tool call finished: the messaging window closes; restore the state the
|
|
2323
|
-
* slot had before the chat (working/outputting), else idle. */
|
|
2324
|
-
onMessagingEnd(toolCallId) {
|
|
2325
|
-
const target = this.messagingByCall.get(toolCallId);
|
|
2326
|
-
if (target === void 0) return;
|
|
2327
|
-
this.messagingByCall.delete(toolCallId);
|
|
2328
|
-
const instance = this.instances.get(target);
|
|
2329
|
-
if (instance === void 0) return;
|
|
2330
|
-
const slot = this.ensureSlot(instance.type);
|
|
2331
|
-
if (slot.status !== "messaging") return;
|
|
2332
|
-
const remaining = (this.messagingCount.get(instance.type) ?? 1) - 1;
|
|
2333
|
-
if (remaining > 0) {
|
|
2334
|
-
this.messagingCount.set(instance.type, remaining);
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
this.messagingCount.delete(instance.type);
|
|
2338
|
-
const prev = this.messagingPrev.get(instance.type);
|
|
2339
|
-
this.messagingPrev.delete(instance.type);
|
|
2340
|
-
slot.status = prev === "working" || prev === "outputting" || prev === "reworking" ? prev : "idle";
|
|
2341
|
-
slot.detail = void 0;
|
|
2342
|
-
slot.lastActivityAt = Date.now();
|
|
2343
|
-
}
|
|
2344
|
-
/**
|
|
2345
|
-
* All slots in fixed display order: the eight default types first (always
|
|
2346
|
-
* present, idle when never used), then extra types in spawn order.
|
|
2347
|
-
*/
|
|
2348
|
-
getSlots(at = Date.now()) {
|
|
2349
|
-
this.dropExpiredOverlays(at);
|
|
2350
|
-
const ordered = [];
|
|
2351
|
-
for (const type of DEFAULT_SUBAGENT_TYPES) ordered.push(this.visibleSlot(type));
|
|
2352
|
-
for (const slot of this.slots.values()) {
|
|
2353
|
-
if (DEFAULT_SUBAGENT_TYPES.includes(slot.type)) continue;
|
|
2354
|
-
ordered.push(this.visibleSlot(slot.type));
|
|
2355
|
-
}
|
|
2356
|
-
return ordered;
|
|
2357
|
-
}
|
|
2358
|
-
/** Display view of one slot: the real state with an unexpired `requesting`
|
|
2359
|
-
* overlay on top. Read-path only — it never mutates. Callers must run
|
|
2360
|
-
* {@link dropExpiredOverlays} first, and messaging clears the overlay in
|
|
2361
|
-
* {@link onMessagingStart}; the `messaging` guard only covers same-tick ordering. */
|
|
2362
|
-
visibleSlot(type) {
|
|
2363
|
-
const copy = {
|
|
2364
|
-
...this.ensureSlot(type),
|
|
2365
|
-
count: this.runningByType.get(type) ?? 0
|
|
2366
|
-
};
|
|
2367
|
-
if (this.requesting.has(type) && copy.status !== "messaging") copy.status = "requesting";
|
|
2368
|
-
return copy;
|
|
2369
|
-
}
|
|
2370
|
-
dropExpiredOverlays(at) {
|
|
2371
|
-
for (const [type, overlay] of this.requesting) if (at >= overlay.until) this.requesting.delete(type);
|
|
2372
|
-
}
|
|
2373
|
-
ensureSlot(type) {
|
|
2374
|
-
let slot = this.slots.get(type);
|
|
2375
|
-
if (slot === void 0) {
|
|
2376
|
-
if (this.slots.size >= 16) {
|
|
2377
|
-
slot = {
|
|
2378
|
-
type,
|
|
2379
|
-
status: "idle",
|
|
2380
|
-
agentId: void 0,
|
|
2381
|
-
detail: void 0,
|
|
2382
|
-
count: 0,
|
|
2383
|
-
lastActivityAt: 0
|
|
2384
|
-
};
|
|
2385
|
-
return slot;
|
|
2386
|
-
}
|
|
2387
|
-
slot = {
|
|
2388
|
-
type,
|
|
2389
|
-
status: "idle",
|
|
2390
|
-
agentId: void 0,
|
|
2391
|
-
detail: void 0,
|
|
2392
|
-
count: 0,
|
|
2393
|
-
lastActivityAt: Date.now()
|
|
2394
|
-
};
|
|
2395
|
-
this.slots.set(type, slot);
|
|
2396
|
-
}
|
|
2397
|
-
return slot;
|
|
2398
|
-
}
|
|
2399
|
-
setState(type, status, agentId, detail) {
|
|
2400
|
-
const slot = this.ensureSlot(type);
|
|
2401
|
-
slot.status = status;
|
|
2402
|
-
slot.agentId = agentId;
|
|
2403
|
-
slot.detail = detail;
|
|
2404
|
-
slot.lastActivityAt = Date.now();
|
|
2405
|
-
}
|
|
2406
|
-
};
|
|
2407
|
-
//#endregion
|
|
2408
2150
|
//#region src/tui/utils/background-task-status.ts
|
|
2409
2151
|
const MAX_DETAIL_LENGTH = 240;
|
|
2410
2152
|
function truncate(value) {
|
|
@@ -2979,7 +2721,7 @@ var SessionEventHandler = class {
|
|
|
2979
2721
|
if (this.providerLatencySamples.length > this.providerLatencyWindow) this.providerLatencySamples.shift();
|
|
2980
2722
|
this.providerLatencyAt = Date.now();
|
|
2981
2723
|
}
|
|
2982
|
-
/** Sidebar Hub: average measured
|
|
2724
|
+
/** Sidebar Hub: average measured first-token latency for the provider in use. The
|
|
2983
2725
|
* caller owns tone and staleness — this stays a plain reading. */
|
|
2984
2726
|
getProviderLatency() {
|
|
2985
2727
|
if (this.providerLatencySamples.length === 0) return {
|
|
@@ -3132,6 +2874,7 @@ var SessionEventHandler = class {
|
|
|
3132
2874
|
handleTurnBegin(_event) {
|
|
3133
2875
|
this.host.streamingUI.resetToolUi();
|
|
3134
2876
|
this.host.streamingUI.setStep(0);
|
|
2877
|
+
this.host.streamingUI.endActivityGroup();
|
|
3135
2878
|
this.host.patchLivePane({
|
|
3136
2879
|
pendingApproval: null,
|
|
3137
2880
|
pendingQuestion: null
|
|
@@ -3394,6 +3137,7 @@ var SessionEventHandler = class {
|
|
|
3394
3137
|
this.host.streamingUI.flushNow();
|
|
3395
3138
|
this.host.streamingUI.resetToolUi();
|
|
3396
3139
|
this.host.streamingUI.finalizeLiveTextBuffers();
|
|
3140
|
+
this.host.streamingUI.endActivityGroup();
|
|
3397
3141
|
this.host.showError(`[${event.code}] ${event.message}`);
|
|
3398
3142
|
}
|
|
3399
3143
|
handleSessionWarning(event) {
|
|
@@ -4339,6 +4083,7 @@ var SessionReplayRenderer = class {
|
|
|
4339
4083
|
context.completedToolCallIds.add(toolCallId);
|
|
4340
4084
|
}
|
|
4341
4085
|
advanceTurn(context) {
|
|
4086
|
+
this.host.streamingUI.endActivityGroup();
|
|
4342
4087
|
context.turnIndex += 1;
|
|
4343
4088
|
context.stepIndex = 0;
|
|
4344
4089
|
context.currentTurnId = `replay:${String(context.turnIndex)}`;
|
|
@@ -4504,81 +4249,6 @@ var SessionReplayRenderer = class {
|
|
|
4504
4249
|
}
|
|
4505
4250
|
};
|
|
4506
4251
|
//#endregion
|
|
4507
|
-
//#region src/tui/components/dialogs/compaction.ts
|
|
4508
|
-
/**
|
|
4509
|
-
* Renders a compaction block in the transcript.
|
|
4510
|
-
*
|
|
4511
|
-
* Lifecycle:
|
|
4512
|
-
* - constructed on `compaction.started` → blinking white bullet +
|
|
4513
|
-
* "Compacting context..." and optional custom instruction
|
|
4514
|
-
* - `markDone()` on `compaction.completed` → solid green bullet +
|
|
4515
|
-
* "Compaction complete (X → Y tokens)"
|
|
4516
|
-
* - `markCanceled()` on `compaction.cancelled` → solid warning bullet +
|
|
4517
|
-
* "Compaction cancelled"
|
|
4518
|
-
*
|
|
4519
|
-
* Bullet animation mirrors `ToolCallComponent` (500ms blink) so the user
|
|
4520
|
-
* reads the same "work in progress" signal across the UI.
|
|
4521
|
-
*/
|
|
4522
|
-
const BLINK_INTERVAL = 500;
|
|
4523
|
-
var CompactionComponent = class extends Container {
|
|
4524
|
-
colors;
|
|
4525
|
-
ui;
|
|
4526
|
-
headerText;
|
|
4527
|
-
blinkOn = true;
|
|
4528
|
-
blinkTimer = null;
|
|
4529
|
-
done = false;
|
|
4530
|
-
canceled = false;
|
|
4531
|
-
tokensBefore;
|
|
4532
|
-
tokensAfter;
|
|
4533
|
-
constructor(colors, ui, instruction) {
|
|
4534
|
-
super();
|
|
4535
|
-
this.colors = colors;
|
|
4536
|
-
this.ui = ui;
|
|
4537
|
-
this.addChild(new Spacer(1));
|
|
4538
|
-
this.headerText = new Text(this.buildHeader(), 0, 0);
|
|
4539
|
-
this.addChild(this.headerText);
|
|
4540
|
-
if (instruction !== void 0) this.addChild(new Text(chalk.dim(` ${instruction}`), 0, 0));
|
|
4541
|
-
this.startBlink();
|
|
4542
|
-
}
|
|
4543
|
-
markDone(tokensBefore, tokensAfter) {
|
|
4544
|
-
if (this.done || this.canceled) return;
|
|
4545
|
-
this.done = true;
|
|
4546
|
-
this.tokensBefore = tokensBefore;
|
|
4547
|
-
this.tokensAfter = tokensAfter;
|
|
4548
|
-
this.stopBlink();
|
|
4549
|
-
this.headerText.setText(this.buildHeader());
|
|
4550
|
-
this.ui?.requestRender();
|
|
4551
|
-
}
|
|
4552
|
-
markCanceled() {
|
|
4553
|
-
if (this.done || this.canceled) return;
|
|
4554
|
-
this.canceled = true;
|
|
4555
|
-
this.stopBlink();
|
|
4556
|
-
this.headerText.setText(this.buildHeader());
|
|
4557
|
-
this.ui?.requestRender();
|
|
4558
|
-
}
|
|
4559
|
-
dispose() {
|
|
4560
|
-
this.stopBlink();
|
|
4561
|
-
}
|
|
4562
|
-
buildHeader() {
|
|
4563
|
-
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)`) : ""}`;
|
|
4564
|
-
if (this.canceled) return `${chalk.hex(this.colors.warning)(STATUS_BULLET)}${chalk.hex(this.colors.warning).bold(t("compaction.canceled"))}`;
|
|
4565
|
-
return `${this.blinkOn ? chalk.hex(this.colors.roleAssistant)(STATUS_BULLET) : " "}${chalk.hex(this.colors.primary).bold(t("compaction.in_progress"))}`;
|
|
4566
|
-
}
|
|
4567
|
-
startBlink() {
|
|
4568
|
-
this.blinkTimer = setInterval(() => {
|
|
4569
|
-
this.blinkOn = !this.blinkOn;
|
|
4570
|
-
this.headerText.setText(this.buildHeader());
|
|
4571
|
-
this.ui?.requestRender();
|
|
4572
|
-
}, BLINK_INTERVAL);
|
|
4573
|
-
}
|
|
4574
|
-
stopBlink() {
|
|
4575
|
-
if (this.blinkTimer !== null) {
|
|
4576
|
-
clearInterval(this.blinkTimer);
|
|
4577
|
-
this.blinkTimer = null;
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
|
-
};
|
|
4581
|
-
//#endregion
|
|
4582
4252
|
//#region src/tui/utils/terminal-notification.ts
|
|
4583
4253
|
function notifyTerminalOnce(state, key, notification) {
|
|
4584
4254
|
const { enabled, condition } = state.appState.notifications;
|
|
@@ -4652,7 +4322,7 @@ function isControlCharacter(ch) {
|
|
|
4652
4322
|
}
|
|
4653
4323
|
//#endregion
|
|
4654
4324
|
//#region src/tui/controllers/streaming-ui.ts
|
|
4655
|
-
var StreamingUIController = class {
|
|
4325
|
+
var StreamingUIController = class StreamingUIController {
|
|
4656
4326
|
host;
|
|
4657
4327
|
flushTimer;
|
|
4658
4328
|
lastFlushAt;
|
|
@@ -4686,6 +4356,30 @@ var StreamingUIController = class {
|
|
|
4686
4356
|
_pendingReadGroupComponents = /* @__PURE__ */ new Map();
|
|
4687
4357
|
_pendingAgentGroup = null;
|
|
4688
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
|
+
]);
|
|
4689
4383
|
constructor(host) {
|
|
4690
4384
|
this.host = host;
|
|
4691
4385
|
}
|
|
@@ -4898,6 +4592,7 @@ var StreamingUIController = class {
|
|
|
4898
4592
|
}
|
|
4899
4593
|
/** Tears down replay-specific state after session history has been rendered. */
|
|
4900
4594
|
cleanupAfterReplay(completedToolCallIds) {
|
|
4595
|
+
this.endActivityGroup();
|
|
4901
4596
|
this._activeToolCalls.clear();
|
|
4902
4597
|
for (const toolCallId of completedToolCallIds) this._pendingToolComponents.delete(toolCallId);
|
|
4903
4598
|
this._pendingAgentGroup = null;
|
|
@@ -4909,6 +4604,74 @@ var StreamingUIController = class {
|
|
|
4909
4604
|
this.pendingToolCallFlushIds.clear();
|
|
4910
4605
|
this.host.state.ui.requestRender();
|
|
4911
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
|
+
}
|
|
4912
4675
|
disposeActiveThinkingComponent() {
|
|
4913
4676
|
if (this._activeThinkingComponent !== void 0) {
|
|
4914
4677
|
this._activeThinkingComponent.dispose();
|
|
@@ -4983,7 +4746,9 @@ var StreamingUIController = class {
|
|
|
4983
4746
|
const draft = this._assistantDraft;
|
|
4984
4747
|
if (this._shownAssistantLength >= draft.length) return this._shownAssistantLength;
|
|
4985
4748
|
const measured = getSharedSpeedTracker().getSpeed();
|
|
4986
|
-
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));
|
|
4987
4752
|
this._shownAssistantLength = Math.min(draft.length, this._shownAssistantLength + budget);
|
|
4988
4753
|
return this._shownAssistantLength;
|
|
4989
4754
|
}
|
|
@@ -5047,6 +4812,7 @@ var StreamingUIController = class {
|
|
|
5047
4812
|
const turnStartTime = this.turnStartAt !== 0 ? this.turnStartAt : state.appState.streamingStartTime;
|
|
5048
4813
|
const completedTurnKey = this._currentTurnId ?? `local:${String(turnStartTime)}`;
|
|
5049
4814
|
this.finalizeLiveTextBuffers();
|
|
4815
|
+
this.endActivityGroup();
|
|
5050
4816
|
this.resetToolCallState();
|
|
5051
4817
|
this._currentTurnId = void 0;
|
|
5052
4818
|
const next = this.host.shiftQueuedMessage();
|
|
@@ -5120,6 +4886,7 @@ var StreamingUIController = class {
|
|
|
5120
4886
|
block.component.updateContent(fullText);
|
|
5121
4887
|
this.host.state.ui.requestRender();
|
|
5122
4888
|
}
|
|
4889
|
+
this.sealActivityGroupOnText(fullText);
|
|
5123
4890
|
}
|
|
5124
4891
|
onStreamingTextEnd() {
|
|
5125
4892
|
const block = this._streamingBlock;
|
|
@@ -5135,7 +4902,7 @@ var StreamingUIController = class {
|
|
|
5135
4902
|
if (this._activeThinkingComponent === void 0) {
|
|
5136
4903
|
this._pendingAgentGroup = null;
|
|
5137
4904
|
this._pendingReadGroup = null;
|
|
5138
|
-
const component = new ThinkingComponent(fullText, state.theme.colors, true, "
|
|
4905
|
+
const component = new ThinkingComponent(fullText, state.theme.colors, true, "finalized", state.ui);
|
|
5139
4906
|
const entry = {
|
|
5140
4907
|
id: nextTranscriptId(),
|
|
5141
4908
|
kind: "thinking",
|
|
@@ -5148,21 +4915,30 @@ var StreamingUIController = class {
|
|
|
5148
4915
|
this.host.pushTranscriptEntry(entry);
|
|
5149
4916
|
this.host.transcriptController.registerLiveComponent(component, entry);
|
|
5150
4917
|
this.host.transcriptController.markPending(component);
|
|
5151
|
-
|
|
5152
|
-
|
|
4918
|
+
const group = this.ensureActivityGroup();
|
|
4919
|
+
this.registerActivityGroupEntry(group, entry);
|
|
4920
|
+
this._lastThinkingText = fullText;
|
|
4921
|
+
group.setThinking(this.activityThinkingText(fullText), true);
|
|
5153
4922
|
} else {
|
|
5154
4923
|
this._activeThinkingComponent.setText(fullText);
|
|
5155
4924
|
if (this._thinkingEntry !== void 0) this._thinkingEntry.content = fullText;
|
|
4925
|
+
this._lastThinkingText = fullText;
|
|
4926
|
+
this.ensureActivityGroup().setThinking(this.activityThinkingText(fullText), true);
|
|
5156
4927
|
}
|
|
5157
4928
|
state.ui.requestRender();
|
|
5158
4929
|
}
|
|
5159
4930
|
onThinkingEnd() {
|
|
5160
4931
|
if (this._activeThinkingComponent === void 0) return;
|
|
5161
|
-
this._activeThinkingComponent
|
|
5162
|
-
|
|
4932
|
+
const component = this._activeThinkingComponent;
|
|
4933
|
+
component.finalize();
|
|
4934
|
+
this.host.transcriptController.unmarkPending(component);
|
|
5163
4935
|
this.host.transcriptController.commit();
|
|
4936
|
+
this.host.transcriptController.releaseLiveComponent(component);
|
|
5164
4937
|
this._activeThinkingComponent = void 0;
|
|
5165
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();
|
|
5166
4942
|
this.host.state.ui.requestRender();
|
|
5167
4943
|
}
|
|
5168
4944
|
onToolCallStart(toolCall) {
|
|
@@ -5186,15 +4962,20 @@ var StreamingUIController = class {
|
|
|
5186
4962
|
this.host.pushTranscriptEntry(entry);
|
|
5187
4963
|
this.host.transcriptController.registerLiveComponent(tc, entry);
|
|
5188
4964
|
this.host.transcriptController.markPending(tc);
|
|
5189
|
-
if (state.toolOutputExpanded) tc.setExpanded(true);
|
|
5190
4965
|
if (state.planExpanded) tc.setPlanExpanded(true);
|
|
5191
4966
|
if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
|
|
5192
4967
|
if (toolCall.name !== "Read") this._pendingReadGroup = null;
|
|
5193
4968
|
let handled = this.tryAttachAgentToolCall(toolCall, tc);
|
|
5194
4969
|
if (!handled) handled = this.tryAttachReadToolCall(toolCall, tc);
|
|
5195
|
-
if (!handled) {
|
|
4970
|
+
if (!handled) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
|
|
5196
4971
|
state.transcriptContainer.addChild(tc);
|
|
5197
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();
|
|
5198
4979
|
}
|
|
5199
4980
|
if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
|
|
5200
4981
|
const session = this.host.requireSession();
|
|
@@ -5249,6 +5030,9 @@ var StreamingUIController = class {
|
|
|
5249
5030
|
if (tc) {
|
|
5250
5031
|
tc.setResult(result);
|
|
5251
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);
|
|
5252
5036
|
this.host.transcriptController.unmarkPending(tc);
|
|
5253
5037
|
this._pendingToolComponents.delete(toolCallId);
|
|
5254
5038
|
const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
|
|
@@ -5260,7 +5044,6 @@ var StreamingUIController = class {
|
|
|
5260
5044
|
if (matchedCall?.name === "AskUserQuestion") {
|
|
5261
5045
|
const completed = new ToolCallComponent(matchedCall, result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
|
|
5262
5046
|
completed.setPermissionMode(state.appState.permissionMode);
|
|
5263
|
-
if (state.toolOutputExpanded) completed.setExpanded(true);
|
|
5264
5047
|
if (state.planExpanded) completed.setPlanExpanded(true);
|
|
5265
5048
|
const entry = {
|
|
5266
5049
|
id: nextTranscriptId(),
|
|
@@ -6612,6 +6395,14 @@ var TranscriptController = class TranscriptController {
|
|
|
6612
6395
|
registerLiveComponent(component, entry) {
|
|
6613
6396
|
this.liveComponentToEntry.set(component, entry);
|
|
6614
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
|
+
}
|
|
6615
6406
|
markPending(component) {
|
|
6616
6407
|
this.pendingComponents.add(component);
|
|
6617
6408
|
}
|
|
@@ -6679,11 +6470,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6679
6470
|
component.updateContent(entry.content);
|
|
6680
6471
|
return component;
|
|
6681
6472
|
}
|
|
6682
|
-
case "thinking":
|
|
6683
|
-
const thinking = new ThinkingComponent(entry.content, state.theme.colors, true);
|
|
6684
|
-
if (state.toolOutputExpanded) thinking.setExpanded(true);
|
|
6685
|
-
return thinking;
|
|
6686
|
-
}
|
|
6473
|
+
case "thinking": return new ThinkingComponent(entry.content, state.theme.colors, true);
|
|
6687
6474
|
case "tool_call":
|
|
6688
6475
|
if (entry.toolCallData?.name === "ReadGroup" && entry.toolCallData.result !== void 0) {
|
|
6689
6476
|
const rgc = new ReadGroupComponent(state.theme.colors, state.ui);
|
|
@@ -6692,7 +6479,6 @@ var TranscriptController = class TranscriptController {
|
|
|
6692
6479
|
}
|
|
6693
6480
|
if (entry.toolCallData) {
|
|
6694
6481
|
const tc = new ToolCallComponent(entry.toolCallData, entry.toolCallData.result, state.theme.colors, state.ui, state.theme.markdownTheme, state.appState.workDir);
|
|
6695
|
-
if (state.toolOutputExpanded) tc.setExpanded(true);
|
|
6696
6482
|
if (state.planExpanded) tc.setPlanExpanded(true);
|
|
6697
6483
|
return tc;
|
|
6698
6484
|
}
|
|
@@ -6776,6 +6562,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6776
6562
|
clearAndRedraw() {
|
|
6777
6563
|
const { state, streamingUI, imageStore } = this.host;
|
|
6778
6564
|
streamingUI.discardPending();
|
|
6565
|
+
streamingUI.endActivityGroup();
|
|
6779
6566
|
state.transcriptEntries = [];
|
|
6780
6567
|
streamingUI.disposeActiveCompactionBlock();
|
|
6781
6568
|
streamingUI.resetLiveText();
|
|
@@ -6832,6 +6619,18 @@ var TranscriptController = class TranscriptController {
|
|
|
6832
6619
|
}
|
|
6833
6620
|
toggleToolOutputExpansion() {
|
|
6834
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
|
+
}
|
|
6835
6634
|
state.toolOutputExpanded = !state.toolOutputExpanded;
|
|
6836
6635
|
state.ui.requestRender();
|
|
6837
6636
|
}
|
|
@@ -7053,17 +6852,14 @@ function formatCount$1(n) {
|
|
|
7053
6852
|
return n.toLocaleString("en-US");
|
|
7054
6853
|
}
|
|
7055
6854
|
/**
|
|
7056
|
-
* Compact token form for narrow sidebars: 2,267,680,102 → `
|
|
7057
|
-
*
|
|
7058
|
-
*
|
|
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.
|
|
7059
6861
|
*/
|
|
7060
6862
|
function formatCompact(n) {
|
|
7061
|
-
if (getLocale() === "zh") {
|
|
7062
|
-
if (n >= 9995e4) return `${(n / 1e8).toFixed(1)}亿`;
|
|
7063
|
-
if (n >= 9500) return `${Math.round(n / 1e4)}万`;
|
|
7064
|
-
return String(n);
|
|
7065
|
-
}
|
|
7066
|
-
if (n >= 9995e5) return `${(n / 1e9).toFixed(1)}B`;
|
|
7067
6863
|
if (n >= 999500) return `${(n / 1e6).toFixed(1)}M`;
|
|
7068
6864
|
if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
|
|
7069
6865
|
return String(n);
|
|
@@ -7131,6 +6927,10 @@ const sessionPanel = {
|
|
|
7131
6927
|
* with the 1s sidebar heartbeat each tick advances half the cycle, so the
|
|
7132
6928
|
* colour jump is clearly visible instead of drifting imperceptibly. */
|
|
7133
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
|
+
}
|
|
7134
6934
|
/** Localized status word per slot state. */
|
|
7135
6935
|
function statusText(status) {
|
|
7136
6936
|
switch (status) {
|
|
@@ -7160,7 +6960,7 @@ var AgentsPanelContent = class {
|
|
|
7160
6960
|
if (agents === void 0 || agents.length === 0) return [];
|
|
7161
6961
|
return layoutSidebarGrid(agents.map((slot) => ({
|
|
7162
6962
|
marker: slot.status === "idle" ? "○" : "●",
|
|
7163
|
-
label: slot.type,
|
|
6963
|
+
label: displayType(slot.type),
|
|
7164
6964
|
value: statusText(slot.status) + (slot.count > 1 ? ` ×${slot.count}` : "")
|
|
7165
6965
|
})), width).map((cell, index) => this.renderSlot(cell, agents[index], index, width));
|
|
7166
6966
|
}
|
|
@@ -7316,20 +7116,23 @@ const HUB_MODEL_STALE_MS = 5 * 6e4;
|
|
|
7316
7116
|
const HUB_ENDPOINTS = Object.freeze([
|
|
7317
7117
|
{
|
|
7318
7118
|
id: "github",
|
|
7319
|
-
url: "https://github.com/"
|
|
7119
|
+
url: "https://github.com/",
|
|
7120
|
+
label: "Github"
|
|
7320
7121
|
},
|
|
7321
7122
|
{
|
|
7322
7123
|
id: "google",
|
|
7323
|
-
url: "https://www.google.com/"
|
|
7124
|
+
url: "https://www.google.com/",
|
|
7125
|
+
label: "Google"
|
|
7324
7126
|
},
|
|
7325
7127
|
{
|
|
7326
7128
|
id: "x",
|
|
7327
7129
|
url: "https://x.com/",
|
|
7328
|
-
label: "
|
|
7130
|
+
label: "X (Twitter)"
|
|
7329
7131
|
},
|
|
7330
7132
|
{
|
|
7331
7133
|
id: "huggingface",
|
|
7332
|
-
url: "https://huggingface.co/"
|
|
7134
|
+
url: "https://huggingface.co/",
|
|
7135
|
+
label: "Huggingface"
|
|
7333
7136
|
},
|
|
7334
7137
|
{
|
|
7335
7138
|
id: "alibaba",
|
|
@@ -13972,6 +13775,7 @@ var ScreamTUI = class {
|
|
|
13972
13775
|
beginSessionRequest() {
|
|
13973
13776
|
this.streamingUI.markTurnStarted();
|
|
13974
13777
|
this.streamingUI.setTurnId(void 0);
|
|
13778
|
+
this.streamingUI.endActivityGroup();
|
|
13975
13779
|
this.streamingUI.resetLiveText();
|
|
13976
13780
|
this.streamingUI.resetToolUi();
|
|
13977
13781
|
this.streamingUI.resetToolCallState();
|
|
@@ -13983,6 +13787,7 @@ var ScreamTUI = class {
|
|
|
13983
13787
|
}
|
|
13984
13788
|
failSessionRequest(message) {
|
|
13985
13789
|
this.setAppState({ streamingPhase: "idle" });
|
|
13790
|
+
this.streamingUI.endActivityGroup();
|
|
13986
13791
|
this.resetLivePane();
|
|
13987
13792
|
this.showError(message);
|
|
13988
13793
|
}
|
|
@@ -14190,7 +13995,7 @@ var ScreamTUI = class {
|
|
|
14190
13995
|
const card = new OnboardingCardComponent({
|
|
14191
13996
|
colors: this.state.theme.colors,
|
|
14192
13997
|
onConfigure: () => {
|
|
14193
|
-
import("./dispatch-
|
|
13998
|
+
import("./dispatch-BRkUiHzo.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
14194
13999
|
this.restoreEditor();
|
|
14195
14000
|
onDone?.();
|
|
14196
14001
|
});
|
|
@@ -15859,6 +15664,53 @@ async function persistKey(filePath, record) {
|
|
|
15859
15664
|
await rename(tmp, filePath);
|
|
15860
15665
|
}
|
|
15861
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
|
|
15862
15714
|
//#region src/web/rateLimit.ts
|
|
15863
15715
|
function createFixedWindowLimiter(opts) {
|
|
15864
15716
|
const { windowMs, max } = opts;
|
|
@@ -16426,14 +16278,12 @@ async function ensureSessionsDir(homeDir) {
|
|
|
16426
16278
|
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
|
|
16427
16279
|
}
|
|
16428
16280
|
async function appendJournalLine(homeDir, sessionId, line) {
|
|
16429
|
-
|
|
16430
|
-
await writeFile(getJournalPath(homeDir, sessionId), line + "\n", { flag: "a" });
|
|
16431
|
-
} catch (error) {
|
|
16281
|
+
await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
|
|
16432
16282
|
log.warn("web: failed to persist journal line", {
|
|
16433
16283
|
sessionId,
|
|
16434
16284
|
error: String(error)
|
|
16435
16285
|
});
|
|
16436
|
-
}
|
|
16286
|
+
});
|
|
16437
16287
|
}
|
|
16438
16288
|
let metaWriteSeq = 0;
|
|
16439
16289
|
async function saveMetadata(homeDir, meta) {
|
|
@@ -16460,16 +16310,77 @@ async function loadMetadata(homeDir, sessionId) {
|
|
|
16460
16310
|
async function loadJournal(homeDir, sessionId) {
|
|
16461
16311
|
try {
|
|
16462
16312
|
const lines = (await readFile(getJournalPath(homeDir, sessionId), "utf-8")).split("\n").filter((l) => l.trim().length > 0);
|
|
16463
|
-
const
|
|
16313
|
+
const entries = [];
|
|
16314
|
+
let corrupt = 0;
|
|
16464
16315
|
for (const line of lines) try {
|
|
16465
|
-
|
|
16316
|
+
entries.push(JSON.parse(line));
|
|
16466
16317
|
} catch {
|
|
16318
|
+
corrupt += 1;
|
|
16467
16319
|
log.warn("web: skipping corrupt journal line", { sessionId });
|
|
16468
16320
|
}
|
|
16469
|
-
return
|
|
16321
|
+
return {
|
|
16322
|
+
entries,
|
|
16323
|
+
corrupt
|
|
16324
|
+
};
|
|
16470
16325
|
} catch {
|
|
16471
|
-
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
|
+
});
|
|
16472
16382
|
}
|
|
16383
|
+
return out;
|
|
16473
16384
|
}
|
|
16474
16385
|
async function listPersistedSessions(homeDir) {
|
|
16475
16386
|
const dir = getSessionsDir(homeDir);
|
|
@@ -16648,10 +16559,11 @@ const MAX_INLINE_DIFF_BYTES = 512 * 1024;
|
|
|
16648
16559
|
* Single-file patch. `repoPath` is **repository-root relative** — the same base
|
|
16649
16560
|
* `git status` reports, so the UI can hand it straight back.
|
|
16650
16561
|
*
|
|
16651
|
-
* Two cases used to return an empty patch
|
|
16652
|
-
* nested files (the old code re-prefixed workdir-relative paths with
|
|
16653
|
-
* resolving them outside the repo) and untracked files (`git diff` has
|
|
16654
|
-
* 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.
|
|
16655
16567
|
*/
|
|
16656
16568
|
async function getGitFileDiff(workDir, repoPath) {
|
|
16657
16569
|
const rel = normalizeNumstatPath(repoPath);
|
|
@@ -16769,7 +16681,27 @@ var WebSession = class {
|
|
|
16769
16681
|
* snapshot (`web.message.finalized`) — the only source that survives a
|
|
16770
16682
|
* server restart (deltas are volatile by design).
|
|
16771
16683
|
*/
|
|
16772
|
-
|
|
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
|
+
}
|
|
16773
16705
|
liveTurnCount = 0;
|
|
16774
16706
|
/** pendingMsgId awaiting compaction completion/cancellation event. */
|
|
16775
16707
|
pendingCompactionMsgId = null;
|
|
@@ -16883,6 +16815,63 @@ var WebSession = class {
|
|
|
16883
16815
|
const msgs = this.buildMessages();
|
|
16884
16816
|
if (msgs.length > 0 && !this.customTitle) this.title = deriveTitle(msgs);
|
|
16885
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
|
+
}
|
|
16886
16875
|
async updateTitle() {
|
|
16887
16876
|
if (this.customTitle) return;
|
|
16888
16877
|
const newTitle = deriveTitle(this.buildMessages());
|
|
@@ -16934,11 +16923,12 @@ var WebSession = class {
|
|
|
16934
16923
|
if (!this.session) return;
|
|
16935
16924
|
this.unsubscribe = this.session.onEvent((event) => {
|
|
16936
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;
|
|
16937
16927
|
if (event.type === "turn.started") {
|
|
16938
16928
|
this.busy = true;
|
|
16939
16929
|
this.activeTurns += 1;
|
|
16940
16930
|
this.liveTurnCount += 1;
|
|
16941
|
-
this.
|
|
16931
|
+
this.liveAssistants.set(event.turnId, {
|
|
16942
16932
|
role: "assistant",
|
|
16943
16933
|
content: "",
|
|
16944
16934
|
tools: [],
|
|
@@ -16954,11 +16944,11 @@ var WebSession = class {
|
|
|
16954
16944
|
tokens: null,
|
|
16955
16945
|
tokensPerSec: null
|
|
16956
16946
|
}
|
|
16957
|
-
};
|
|
16947
|
+
});
|
|
16958
16948
|
} else if (event.type === "turn.ended") {
|
|
16959
16949
|
this.activeTurns = Math.max(0, this.activeTurns - 1);
|
|
16960
16950
|
this.busy = this.activeTurns > 0;
|
|
16961
|
-
const live = this.
|
|
16951
|
+
const live = this.liveFor(event.turnId);
|
|
16962
16952
|
if (event.reason === "failed" && live) live.isError = true;
|
|
16963
16953
|
if (live) {
|
|
16964
16954
|
live.turnStats = {
|
|
@@ -16978,13 +16968,13 @@ var WebSession = class {
|
|
|
16978
16968
|
message: live
|
|
16979
16969
|
});
|
|
16980
16970
|
}
|
|
16981
|
-
this.
|
|
16971
|
+
this.liveAssistants.delete(event.turnId);
|
|
16982
16972
|
this.trackPending(this.updateTitle());
|
|
16983
16973
|
} else if (event.type === "assistant.delta") {
|
|
16984
|
-
const live = this.
|
|
16974
|
+
const live = this.liveFor(event.turnId);
|
|
16985
16975
|
if (live) live.content += event.delta;
|
|
16986
16976
|
} else if (event.type === "thinking.delta") {
|
|
16987
|
-
const live = this.
|
|
16977
|
+
const live = this.liveFor(event.turnId);
|
|
16988
16978
|
if (live) {
|
|
16989
16979
|
const t = live.tools.find((x) => x.name === "thinking");
|
|
16990
16980
|
if (t) t.output = (t.output ?? "") + event.delta;
|
|
@@ -16996,7 +16986,7 @@ var WebSession = class {
|
|
|
16996
16986
|
});
|
|
16997
16987
|
}
|
|
16998
16988
|
} else if (event.type === "tool.call.started") {
|
|
16999
|
-
const live = this.
|
|
16989
|
+
const live = this.liveFor(event.turnId);
|
|
17000
16990
|
if (live) {
|
|
17001
16991
|
live.tools.push({
|
|
17002
16992
|
toolCallId: event.toolCallId,
|
|
@@ -17006,7 +16996,7 @@ var WebSession = class {
|
|
|
17006
16996
|
if (live.turnStats) live.turnStats.step += 1;
|
|
17007
16997
|
}
|
|
17008
16998
|
} else if (event.type === "tool.result") {
|
|
17009
|
-
const live = this.
|
|
16999
|
+
const live = this.liveFor(event.turnId);
|
|
17010
17000
|
if (live) {
|
|
17011
17001
|
const tool = live.tools.find((t) => t.toolCallId === event.toolCallId);
|
|
17012
17002
|
if (tool) {
|
|
@@ -17487,7 +17477,7 @@ var WebSession = class {
|
|
|
17487
17477
|
workDir: this.workDir,
|
|
17488
17478
|
title: this.title,
|
|
17489
17479
|
createdAt: this.createdAt,
|
|
17490
|
-
messageCount: this.buildMessages().length,
|
|
17480
|
+
messageCount: this.isActive ? this.buildMessages().length : -1,
|
|
17491
17481
|
active: this.isActive
|
|
17492
17482
|
};
|
|
17493
17483
|
}
|
|
@@ -17681,7 +17671,7 @@ var WebSession = class {
|
|
|
17681
17671
|
return this.connections.size;
|
|
17682
17672
|
}
|
|
17683
17673
|
buildMessages() {
|
|
17684
|
-
const messages = [];
|
|
17674
|
+
const messages = [...this.seededMessages];
|
|
17685
17675
|
let currentAssistant = null;
|
|
17686
17676
|
let userIndex = 0;
|
|
17687
17677
|
let turnCount = 0;
|
|
@@ -17867,6 +17857,36 @@ async function handleGoalRoute(req, res, url, resolveSession) {
|
|
|
17867
17857
|
}
|
|
17868
17858
|
return true;
|
|
17869
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
|
+
}
|
|
17870
17890
|
var SessionManager = class {
|
|
17871
17891
|
sessions = /* @__PURE__ */ new Map();
|
|
17872
17892
|
/** In-flight activation promises to deduplicate concurrent calls. */
|
|
@@ -17889,7 +17909,7 @@ var SessionManager = class {
|
|
|
17889
17909
|
await ensureSessionsDir(this.homeDir);
|
|
17890
17910
|
const metas = await listPersistedSessions(this.homeDir);
|
|
17891
17911
|
for (const meta of metas) {
|
|
17892
|
-
const entries = await loadJournal(this.homeDir, meta.sessionId);
|
|
17912
|
+
const { entries } = await loadJournal(this.homeDir, meta.sessionId);
|
|
17893
17913
|
const ws = new WebSession(null, {
|
|
17894
17914
|
sessionId: meta.sessionId,
|
|
17895
17915
|
coreSessionId: meta.coreSessionId ?? meta.sessionId,
|
|
@@ -17903,11 +17923,107 @@ var SessionManager = class {
|
|
|
17903
17923
|
ws.loadFromPersisted(entries);
|
|
17904
17924
|
this.sessions.set(meta.sessionId, ws);
|
|
17905
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
|
+
}
|
|
17906
18013
|
log.info("web: loaded persisted sessions", { count: this.sessions.size });
|
|
17907
18014
|
}
|
|
17908
|
-
|
|
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;
|
|
17909
18025
|
const session = await this.harness.createSession({
|
|
17910
|
-
workDir:
|
|
18026
|
+
workDir: dir,
|
|
17911
18027
|
model: this.model,
|
|
17912
18028
|
permission: this.permission
|
|
17913
18029
|
});
|
|
@@ -17915,7 +18031,7 @@ var SessionManager = class {
|
|
|
17915
18031
|
const createdAt = Date.now();
|
|
17916
18032
|
const webSession = new WebSession(session, {
|
|
17917
18033
|
sessionId,
|
|
17918
|
-
workDir:
|
|
18034
|
+
workDir: dir,
|
|
17919
18035
|
permission: this.permission,
|
|
17920
18036
|
yolo: this.yolo,
|
|
17921
18037
|
homeDir: this.homeDir,
|
|
@@ -17932,7 +18048,7 @@ var SessionManager = class {
|
|
|
17932
18048
|
this.sessions.set(sessionId, webSession);
|
|
17933
18049
|
log.info("web: session created", {
|
|
17934
18050
|
sessionId,
|
|
17935
|
-
workDir:
|
|
18051
|
+
workDir: dir
|
|
17936
18052
|
});
|
|
17937
18053
|
return webSession;
|
|
17938
18054
|
}
|
|
@@ -17977,8 +18093,19 @@ var SessionManager = class {
|
|
|
17977
18093
|
coreSessionId: session.id,
|
|
17978
18094
|
onFork: (id) => this.forkSession(id)
|
|
17979
18095
|
});
|
|
17980
|
-
const entries = await loadJournal(this.homeDir, sessionId);
|
|
18096
|
+
const { entries, corrupt } = await loadJournal(this.homeDir, sessionId);
|
|
17981
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
|
+
}
|
|
17982
18109
|
try {
|
|
17983
18110
|
await reactivated.ready;
|
|
17984
18111
|
} catch (error) {
|
|
@@ -18033,7 +18160,7 @@ var SessionManager = class {
|
|
|
18033
18160
|
const title = `${source.getTitle()} (fork)`;
|
|
18034
18161
|
const webSession = new WebSession(session, {
|
|
18035
18162
|
sessionId: newId,
|
|
18036
|
-
workDir:
|
|
18163
|
+
workDir: source.getMetadata().workDir,
|
|
18037
18164
|
permission: source.getMetadata().permission,
|
|
18038
18165
|
yolo: this.yolo,
|
|
18039
18166
|
homeDir: this.homeDir,
|
|
@@ -18047,6 +18174,16 @@ var SessionManager = class {
|
|
|
18047
18174
|
await writeFile(getJournalPath(this.homeDir, newId), lines);
|
|
18048
18175
|
}
|
|
18049
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
|
+
}
|
|
18050
18187
|
try {
|
|
18051
18188
|
await webSession.ready;
|
|
18052
18189
|
} catch (error) {
|
|
@@ -18082,8 +18219,33 @@ var SessionManager = class {
|
|
|
18082
18219
|
async delete(sessionId) {
|
|
18083
18220
|
const ws = this.sessions.get(sessionId);
|
|
18084
18221
|
if (!ws) return false;
|
|
18222
|
+
const workDir = ws.workDir;
|
|
18223
|
+
const coreId = ws.getCoreSessionId();
|
|
18085
18224
|
await ws.delete();
|
|
18086
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);
|
|
18087
18249
|
return true;
|
|
18088
18250
|
}
|
|
18089
18251
|
/** List available model aliases from the harness config (reloaded). */
|
|
@@ -18826,17 +18988,18 @@ async function runWebServer(opts) {
|
|
|
18826
18988
|
res.end(JSON.stringify(manager.list()));
|
|
18827
18989
|
return;
|
|
18828
18990
|
}
|
|
18991
|
+
if (url === `${API_PREFIX}/workdir` && method === "GET") {
|
|
18992
|
+
sendJson(res, 200, { workDir: manager.defaultWorkDir });
|
|
18993
|
+
return;
|
|
18994
|
+
}
|
|
18829
18995
|
if (url === `${API_PREFIX}/sessions` && method === "POST") {
|
|
18830
18996
|
try {
|
|
18831
|
-
const
|
|
18997
|
+
const dir = await validateSessionWorkDir((await readJsonBody(req))["workDir"]);
|
|
18998
|
+
const ws = await manager.createSession(dir);
|
|
18832
18999
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
18833
19000
|
res.end(JSON.stringify(ws.getListItem()));
|
|
18834
19001
|
} catch (error) {
|
|
18835
|
-
res
|
|
18836
|
-
res.end(JSON.stringify({
|
|
18837
|
-
code: 500,
|
|
18838
|
-
message: String(error)
|
|
18839
|
-
}));
|
|
19002
|
+
sendHttpError(res, error);
|
|
18840
19003
|
}
|
|
18841
19004
|
return;
|
|
18842
19005
|
}
|
|
@@ -19132,7 +19295,10 @@ async function runWebServer(opts) {
|
|
|
19132
19295
|
if (url === "/" || url === "/index.html") {
|
|
19133
19296
|
try {
|
|
19134
19297
|
const html = await readFile(join(publicDir, "index.html"), "utf-8");
|
|
19135
|
-
res.writeHead(200, {
|
|
19298
|
+
res.writeHead(200, {
|
|
19299
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
19300
|
+
"Cache-Control": "no-store"
|
|
19301
|
+
});
|
|
19136
19302
|
res.end(html);
|
|
19137
19303
|
} catch {
|
|
19138
19304
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -19145,7 +19311,9 @@ async function runWebServer(opts) {
|
|
|
19145
19311
|
const filePath = join(publicDir, safeUrl);
|
|
19146
19312
|
const contentType = contentTypes[filePath.split(".").pop() ?? ""] ?? "application/octet-stream";
|
|
19147
19313
|
const data = await readFile(filePath);
|
|
19148
|
-
|
|
19314
|
+
const headers = { "Content-Type": contentType };
|
|
19315
|
+
if (safeUrl.startsWith("/assets/")) headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
|
19316
|
+
res.writeHead(200, headers);
|
|
19149
19317
|
res.end(data);
|
|
19150
19318
|
} catch {
|
|
19151
19319
|
res.writeHead(404, { "Content-Type": "text/plain" });
|