scream-code 0.15.5 → 0.15.6
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-ugHHjuy4.mjs → app-Y2DCZTWP.mjs} +1248 -40
- package/dist/{dispatch-B23MoYWm.mjs → dispatch-CkPcMk4b.mjs} +1 -1
- package/dist/{dispatch-aNzh78xW.mjs → dispatch-jQiqBKV2.mjs} +382 -399
- package/dist/main.mjs +1 -1
- package/dist/{text-input-dialog-C0UIQBlI.mjs → text-input-dialog-DJwYNHcs.mjs} +1 -1
- package/dist/{text-input-dialog-DbYfSAjy.mjs → text-input-dialog-LOMBSusF.mjs} +87 -1
- package/package.json +2 -2
|
@@ -4,8 +4,8 @@ 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 { $t as
|
|
8
|
-
import { a as setLocale, o as t } from "./text-input-dialog-
|
|
7
|
+
import { $t as formatErrorMessage, A as renderDiffLinesClustered, An as getDataDir, At as ENABLE_TERMINAL_FOCUS_REPORTING, B as BackgroundAgentStatusComponent, Bn as saveCatalogCache, Bt as TERMINAL_THEME_LIGHT, Cn as PIXEL_PULSE_FRAMES, Ct as createThemeStyles, D as toggleEmptySessionHint, Dn as loadTuiConfig, Dt as parseOsc11BackgroundTheme, E as isTurnElapsedEnabled, En as TuiLikePreferencesSchema, Et as detectTerminalTheme, F as estimateTokens, Fn as CLI_UI_MODE, Ft as OSC11_RESPONSE_PREFIX_NO_ESC, G as getBreathingFrame, Gn as flushDiagnosticLogs, H as AgentGroupComponent, Ht as isStreaming, I as getSharedSpeedTracker, In as CLI_USER_AGENT_PRODUCT, It as QUERY_TERMINAL_THEME, J as refineGoal, Jn as isScreamError, K as resetBreathingClock, Kn as log, L as SkillActivationComponent, Ln as PRODUCT_NAME, Lt as TERMINAL_FOCUS_IN, M as langFromPath, Mn as getLogDir, Mt as OSC11_QUERY, N as CachedContainer, Nn as detectInstallSource, Nt as OSC11_RESPONSE, O as ToolCallComponent, On as saveTuiConfig, Ot as DISABLE_TERMINAL_FOCUS_REPORTING, P as ThinkingComponent, Pn as CLI_COMMAND_NAME, Pt as OSC11_RESPONSE_PREFIX, Qt as argsRecord, R as ReadGroupComponent, Rn as DEFAULT_CATALOG_URL, Rt as TERMINAL_FOCUS_OUT, S as getDaemonInstructions, Sn as startManualEmbeddingDownload, St as createMarkdownTheme, T as isEmptySessionHintDismissed, Tn as TuiConfigParseError, Tt as getColorPalette, U as WelcomeComponent, Un as resolveScreamHome, Ut as FooterComponent, V as AssistantMessageComponent, Vn as ScreamHarness, Vt as isBusy, W as BREATHE_CYCLE_MS, Wn as MemoryMemoStore, Wt as lerpGradient, Xn as ErrorCodes, Xt as STATUS_BULLET, Yn as isOrphanedToolCallError, Yt as printableChar, Zn as SCREAM_ERROR_INFO, Zt as appendStreamingArgsPreview, _ as refreshUpdateCache, _n as sortSlashCommands, _t as clearInfoPanelState, an as CHARS_PER_TOKEN, b as readJsonlFile, bn as getKnowledgeStore, bt as resolveThemeSync, c as hasDispose, cn as MAIN_AGENT_ID, ct as handlePlanCommand, d as formatMemoryMemoForInjection, dn as getCtrlCHint, en as isTodoItemShape, et as toTerminalHyperlink, fn as getCtrlDHint, g as selectUpdateTarget, gn as BUILTIN_SLASH_COMMANDS, gt as supportsBalance, hn as buildSkillSlashCommands, ht as refreshProviderBalance, i as buildRoleAdditionalText, in as truncateErrorMessage, j as highlightLines, jn as getInputHistoryFile, jt as ENABLE_TERMINAL_THEME_REPORTING, k as renderDiffLines, kn as detectShellEnvironment, kt as DISABLE_TERMINAL_THEME_REPORTING, l as isPlanExpandable, ln as SESSION_TIPS, mn as getNoActiveSessionMessage, n as clearEvalPanelState, nn as serializeToolResultOutput, nt as getModelCycleLevel, on as EMPTY_SESSION_HINT_URL, ot as handleFusionPlanCommand, pn as getLlmNotSetMessage, q as clearGoalState, qn as resolveGlobalLogPath, r as openUrl, rn as stringValue, s as disposeChildren, sn as EXIT_CONFIRM_WINDOW_MS, t as dispatchInput, tn as parseStreamingArgs, tt as changeThinkingLevel, u as MoonLoader, un as TIP_ROTATION_INTERVAL_MS, v as readUpdateCache, vn as isExperimentalFlagEnabled, w as UserMessageComponent, wn as PULSE_WAVE_FRAMES, wt as contrastTextHex, xn as isEmbeddingModelCached, xt as createEditorTheme, y as appendJsonlLine, yn as setExperimentalFlags, z as parseReadGroupOutput, zn as fetchCatalog, zt as TERMINAL_THEME_DARK } from "./dispatch-jQiqBKV2.mjs";
|
|
8
|
+
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-LOMBSusF.mjs";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
11
11
|
import { access, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
@@ -24,7 +24,7 @@ import process$1 from "node:process";
|
|
|
24
24
|
import { Command, Option } from "commander";
|
|
25
25
|
import { createInterface } from "node:readline/promises";
|
|
26
26
|
import chalk, { chalkStderr } from "chalk";
|
|
27
|
-
import { CombinedAutocompleteProvider, Container, Editor, Input, Key, ProcessTerminal, ScrollView, Spacer, Text, TuiAltScreen, VStack, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
|
|
27
|
+
import { CombinedAutocompleteProvider, Container, Editor, HStack, Input, Key, ProcessTerminal, ScrollView, Spacer, Text, TuiAltScreen, VStack, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
|
|
28
28
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
29
29
|
import { toString } from "qrcode";
|
|
30
30
|
//#region src/cli/build-info.ts
|
|
@@ -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.15.
|
|
35
|
+
version: optionalBuildString("0.15.6"),
|
|
36
36
|
channel: optionalBuildString(""),
|
|
37
37
|
commit: optionalBuildString(""),
|
|
38
38
|
buildTarget: optionalBuildString("darwin-arm64")
|
|
@@ -193,7 +193,8 @@ function createProgram(version, onMain, onPluginNodeRunner = () => {}, onStreamJ
|
|
|
193
193
|
pluginDirs: subOpts["pluginDir"] ?? []
|
|
194
194
|
});
|
|
195
195
|
});
|
|
196
|
-
program.command("web").description("启动浏览器 Web UI").option("--port <port>", "端口号", "3210").option("--model <model>", "LLM 模型别名").option("-y, --yolo", "自动批准所有操作", false).option("--auto", "以自动权限模式启动", false).option("--no-open", "不自动打开浏览器").option("--lan", "开启局域网共享(同网设备凭网关密钥访问)", false).option("--token <key>", "自定义网关访问密钥(保存后供 --lan 使用)").option("--reset-password", "交互式重设网关访问密钥", false).option("--skills-dir <dir>", "从该目录加载技能(可多次指定)", (value, previous) => [...previous ?? [], value], []).action((subOpts) => {
|
|
196
|
+
program.command("web").description("启动浏览器 Web UI").option("--port <port>", "端口号", "3210").option("--model <model>", "LLM 模型别名").option("-y, --yolo", "自动批准所有操作", false).option("--auto", "以自动权限模式启动", false).option("--no-open", "不自动打开浏览器").option("--lan", "开启局域网共享(同网设备凭网关密钥访问)", false).option("--token <key>", "自定义网关访问密钥(保存后供 --lan 使用)").option("--reset-password", "交互式重设网关访问密钥", false).option("--idle-minutes <minutes>", "无浏览器连接 N 分钟后自动退出(0=禁用,默认 15)", "15").option("--skills-dir <dir>", "从该目录加载技能(可多次指定)", (value, previous) => [...previous ?? [], value], []).action((subOpts) => {
|
|
197
|
+
const idleMinutes = Number(subOpts["idleMinutes"] ?? "15");
|
|
197
198
|
onWeb({
|
|
198
199
|
port: parseInt(subOpts["port"], 10) || 3210,
|
|
199
200
|
model: subOpts["model"],
|
|
@@ -203,7 +204,8 @@ function createProgram(version, onMain, onPluginNodeRunner = () => {}, onStreamJ
|
|
|
203
204
|
skillsDirs: subOpts["skillsDir"] ?? [],
|
|
204
205
|
lan: subOpts["lan"] ?? false,
|
|
205
206
|
token: subOpts["token"],
|
|
206
|
-
resetPassword: subOpts["resetPassword"] ?? false
|
|
207
|
+
resetPassword: subOpts["resetPassword"] ?? false,
|
|
208
|
+
idleMinutes: Number.isFinite(idleMinutes) && idleMinutes >= 0 ? idleMinutes : 15
|
|
207
209
|
});
|
|
208
210
|
});
|
|
209
211
|
program.command("channel").description("管理 cc-connect 消息平台通道").command("setup").description("配置 cc-connect 并选择要连接的平台").action(() => {
|
|
@@ -2023,6 +2025,200 @@ function formatBackgroundAgentTranscript(phase, meta, extras = void 0) {
|
|
|
2023
2025
|
};
|
|
2024
2026
|
}
|
|
2025
2027
|
//#endregion
|
|
2028
|
+
//#region src/tui/utils/subagent-slots.ts
|
|
2029
|
+
/** Default subagent types, in the fixed display order of the slots. */
|
|
2030
|
+
const DEFAULT_SUBAGENT_TYPES = [
|
|
2031
|
+
"coder",
|
|
2032
|
+
"explore",
|
|
2033
|
+
"plan",
|
|
2034
|
+
"verify",
|
|
2035
|
+
"reviewer",
|
|
2036
|
+
"oracle",
|
|
2037
|
+
"worker",
|
|
2038
|
+
"writer"
|
|
2039
|
+
];
|
|
2040
|
+
var SubagentSlots = class {
|
|
2041
|
+
slots = /* @__PURE__ */ new Map();
|
|
2042
|
+
instances = /* @__PURE__ */ new Map();
|
|
2043
|
+
/** Live instance count per type: a slot only returns to idle when the LAST
|
|
2044
|
+
* instance of that type terminates (WolfPack spawns several coders at once
|
|
2045
|
+
* and they share one visual slot). */
|
|
2046
|
+
runningByType = /* @__PURE__ */ new Map();
|
|
2047
|
+
/** main-side SendSubagentMessage tool callId → target subagent agentId. */
|
|
2048
|
+
messagingByCall = /* @__PURE__ */ new Map();
|
|
2049
|
+
/** Status to restore per TYPE when a messaging window closes, captured from
|
|
2050
|
+
* the first open window; concurrent windows on the same type keep counting
|
|
2051
|
+
* and the slot only exits messaging when the last window closes. */
|
|
2052
|
+
messagingPrev = /* @__PURE__ */ new Map();
|
|
2053
|
+
/** Count of open messaging windows per type (handles same-type siblings). */
|
|
2054
|
+
messagingCount = /* @__PURE__ */ new Map();
|
|
2055
|
+
reset() {
|
|
2056
|
+
this.slots.clear();
|
|
2057
|
+
this.instances.clear();
|
|
2058
|
+
this.runningByType.clear();
|
|
2059
|
+
this.messagingByCall.clear();
|
|
2060
|
+
this.messagingPrev.clear();
|
|
2061
|
+
this.messagingCount.clear();
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* subagent.spawned. A first spawn starts the slot as working; a second
|
|
2065
|
+
* spawn of the SAME agentId means the main agent resumed it (返工中),
|
|
2066
|
+
* tracked via an instance generation counter.
|
|
2067
|
+
*/
|
|
2068
|
+
onSpawned(agentId, subagentType, description) {
|
|
2069
|
+
const instance = this.instances.get(agentId);
|
|
2070
|
+
if (instance !== void 0) {
|
|
2071
|
+
instance.generation += 1;
|
|
2072
|
+
this.runningByType.set(instance.type, (this.runningByType.get(instance.type) ?? 0) + 1);
|
|
2073
|
+
this.setState(instance.type, "reworking", agentId, description === void 0 ? void 0 : `resume: ${description}`);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
this.instances.set(agentId, {
|
|
2077
|
+
type: subagentType,
|
|
2078
|
+
generation: 1
|
|
2079
|
+
});
|
|
2080
|
+
this.runningByType.set(subagentType, (this.runningByType.get(subagentType) ?? 0) + 1);
|
|
2081
|
+
this.setState(subagentType, "working", agentId, description);
|
|
2082
|
+
}
|
|
2083
|
+
/** subagent.started. Keeps the current state (spawned already set it); only
|
|
2084
|
+
* used to materialize a slot if lifecycle events raced past us. */
|
|
2085
|
+
onStarted(agentId) {
|
|
2086
|
+
const instance = this.instances.get(agentId);
|
|
2087
|
+
if (instance === void 0) return;
|
|
2088
|
+
const slot = this.ensureSlot(instance.type);
|
|
2089
|
+
if (slot.status === "idle") {
|
|
2090
|
+
slot.status = "working";
|
|
2091
|
+
slot.agentId = agentId;
|
|
2092
|
+
slot.lastActivityAt = Date.now();
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/** subagent.completed / subagent.failed: the slot goes back to resting only
|
|
2096
|
+
* when the LAST live instance of that type terminates (siblings spawned by
|
|
2097
|
+
* the same tool call keep the slot busy). */
|
|
2098
|
+
onTerminated(agentId) {
|
|
2099
|
+
const instance = this.instances.get(agentId);
|
|
2100
|
+
if (instance === void 0) return;
|
|
2101
|
+
const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
|
|
2102
|
+
if (remaining > 0) {
|
|
2103
|
+
this.runningByType.set(instance.type, remaining);
|
|
2104
|
+
const slot = this.ensureSlot(instance.type);
|
|
2105
|
+
slot.detail = void 0;
|
|
2106
|
+
slot.lastActivityAt = Date.now();
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
this.runningByType.delete(instance.type);
|
|
2110
|
+
this.setState(instance.type, "idle", void 0, void 0);
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Routed subagent activity (agentId !== main):
|
|
2114
|
+
* - kind 'tool' → working (tool call in flight)
|
|
2115
|
+
* - kind 'output' → outputting (assistant/thinking delta)
|
|
2116
|
+
* - kind 'toolResult'→ detail only (the agent may keep working/outputting)
|
|
2117
|
+
*/
|
|
2118
|
+
onActivity(agentId, kind, detail) {
|
|
2119
|
+
const instance = this.instances.get(agentId);
|
|
2120
|
+
if (instance === void 0) return;
|
|
2121
|
+
if (kind === "toolResult") {
|
|
2122
|
+
const slot = this.ensureSlot(instance.type);
|
|
2123
|
+
slot.detail = detail;
|
|
2124
|
+
slot.lastActivityAt = Date.now();
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
this.setState(instance.type, kind === "tool" ? "working" : "outputting", agentId, detail);
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Main-side SendSubagentMessage tool call started: the target subagent's
|
|
2131
|
+
* slot shows messaging until the tool call returns.
|
|
2132
|
+
*/
|
|
2133
|
+
onMessagingStart(toolCallId, targetAgentId, operation) {
|
|
2134
|
+
if (targetAgentId === void 0) return;
|
|
2135
|
+
const instance = this.instances.get(targetAgentId);
|
|
2136
|
+
if (instance === void 0) return;
|
|
2137
|
+
this.messagingByCall.set(toolCallId, targetAgentId);
|
|
2138
|
+
const slot = this.ensureSlot(instance.type);
|
|
2139
|
+
if (slot.status !== "messaging") this.messagingPrev.set(instance.type, slot.status);
|
|
2140
|
+
this.messagingCount.set(instance.type, (this.messagingCount.get(instance.type) ?? 0) + 1);
|
|
2141
|
+
this.setState(instance.type, "messaging", targetAgentId, operation === void 0 ? void 0 : `message (${operation})`);
|
|
2142
|
+
}
|
|
2143
|
+
/** Tool call finished: the messaging window closes; restore the state the
|
|
2144
|
+
* slot had before the chat (working/outputting), else idle. */
|
|
2145
|
+
onMessagingEnd(toolCallId) {
|
|
2146
|
+
const target = this.messagingByCall.get(toolCallId);
|
|
2147
|
+
if (target === void 0) return;
|
|
2148
|
+
this.messagingByCall.delete(toolCallId);
|
|
2149
|
+
const instance = this.instances.get(target);
|
|
2150
|
+
if (instance === void 0) return;
|
|
2151
|
+
const slot = this.ensureSlot(instance.type);
|
|
2152
|
+
if (slot.status !== "messaging") return;
|
|
2153
|
+
const remaining = (this.messagingCount.get(instance.type) ?? 1) - 1;
|
|
2154
|
+
if (remaining > 0) {
|
|
2155
|
+
this.messagingCount.set(instance.type, remaining);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
this.messagingCount.delete(instance.type);
|
|
2159
|
+
const prev = this.messagingPrev.get(instance.type);
|
|
2160
|
+
this.messagingPrev.delete(instance.type);
|
|
2161
|
+
slot.status = prev === "working" || prev === "outputting" || prev === "reworking" ? prev : "idle";
|
|
2162
|
+
slot.detail = void 0;
|
|
2163
|
+
slot.lastActivityAt = Date.now();
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* All slots in fixed display order: the eight default types first (always
|
|
2167
|
+
* present, idle when never used), then extra types in spawn order.
|
|
2168
|
+
*/
|
|
2169
|
+
getSlots() {
|
|
2170
|
+
const ordered = [];
|
|
2171
|
+
for (const type of DEFAULT_SUBAGENT_TYPES) {
|
|
2172
|
+
const slot = this.ensureSlot(type);
|
|
2173
|
+
ordered.push({
|
|
2174
|
+
...slot,
|
|
2175
|
+
count: this.runningByType.get(type) ?? 0
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
for (const slot of this.slots.values()) {
|
|
2179
|
+
if (DEFAULT_SUBAGENT_TYPES.includes(slot.type)) continue;
|
|
2180
|
+
ordered.push({
|
|
2181
|
+
...slot,
|
|
2182
|
+
count: this.runningByType.get(slot.type) ?? 0
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
return ordered;
|
|
2186
|
+
}
|
|
2187
|
+
ensureSlot(type) {
|
|
2188
|
+
let slot = this.slots.get(type);
|
|
2189
|
+
if (slot === void 0) {
|
|
2190
|
+
if (this.slots.size >= 16) {
|
|
2191
|
+
slot = {
|
|
2192
|
+
type,
|
|
2193
|
+
status: "idle",
|
|
2194
|
+
agentId: void 0,
|
|
2195
|
+
detail: void 0,
|
|
2196
|
+
count: 0,
|
|
2197
|
+
lastActivityAt: 0
|
|
2198
|
+
};
|
|
2199
|
+
return slot;
|
|
2200
|
+
}
|
|
2201
|
+
slot = {
|
|
2202
|
+
type,
|
|
2203
|
+
status: "idle",
|
|
2204
|
+
agentId: void 0,
|
|
2205
|
+
detail: void 0,
|
|
2206
|
+
count: 0,
|
|
2207
|
+
lastActivityAt: Date.now()
|
|
2208
|
+
};
|
|
2209
|
+
this.slots.set(type, slot);
|
|
2210
|
+
}
|
|
2211
|
+
return slot;
|
|
2212
|
+
}
|
|
2213
|
+
setState(type, status, agentId, detail) {
|
|
2214
|
+
const slot = this.ensureSlot(type);
|
|
2215
|
+
slot.status = status;
|
|
2216
|
+
slot.agentId = agentId;
|
|
2217
|
+
slot.detail = detail;
|
|
2218
|
+
slot.lastActivityAt = Date.now();
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
//#endregion
|
|
2026
2222
|
//#region src/tui/utils/background-task-status.ts
|
|
2027
2223
|
const MAX_DETAIL_LENGTH = 240;
|
|
2028
2224
|
function truncate(value) {
|
|
@@ -2237,15 +2433,15 @@ function formatStepDebugTiming(input) {
|
|
|
2237
2433
|
const latency = input.llmFirstTokenLatencyMs;
|
|
2238
2434
|
const streamMs = input.llmStreamDurationMs;
|
|
2239
2435
|
if (latency === void 0 || streamMs === void 0) return void 0;
|
|
2240
|
-
const parts = [`TTFT: ${formatDuration(latency)}`];
|
|
2436
|
+
const parts = [`TTFT: ${formatDuration$1(latency)}`];
|
|
2241
2437
|
const outputTokens = input.usage?.output;
|
|
2242
2438
|
if (outputTokens !== void 0 && outputTokens > 0 && streamMs > 0) {
|
|
2243
2439
|
const tps = (outputTokens / (streamMs / 1e3)).toFixed(1);
|
|
2244
|
-
parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration(streamMs)})`);
|
|
2440
|
+
parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration$1(streamMs)})`);
|
|
2245
2441
|
}
|
|
2246
2442
|
return `[Debug] ${parts.join(" | ")}`;
|
|
2247
2443
|
}
|
|
2248
|
-
function formatDuration(ms) {
|
|
2444
|
+
function formatDuration$1(ms) {
|
|
2249
2445
|
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
2250
2446
|
return `${(ms / 1e3).toFixed(1)}s`;
|
|
2251
2447
|
}
|
|
@@ -2310,7 +2506,30 @@ function detectCompactionAnomaly(input) {
|
|
|
2310
2506
|
return null;
|
|
2311
2507
|
}
|
|
2312
2508
|
//#endregion
|
|
2509
|
+
//#region src/tui/types.ts
|
|
2510
|
+
/** Goal snapshot status, kept in sync with agent-core's GoalStatus union
|
|
2511
|
+
* (the wire type is plain string, hence the explicit union + normalizer). */
|
|
2512
|
+
const GOAL_STATUSES = [
|
|
2513
|
+
"active",
|
|
2514
|
+
"paused",
|
|
2515
|
+
"blocked",
|
|
2516
|
+
"complete"
|
|
2517
|
+
];
|
|
2518
|
+
/** Clamp an unknown wire status to a safe display value: anything outside the
|
|
2519
|
+
* four known states is rendered as paused (never as completed). */
|
|
2520
|
+
function normalizeGoalStatus(status) {
|
|
2521
|
+
return GOAL_STATUSES.includes(status) ? status : "paused";
|
|
2522
|
+
}
|
|
2523
|
+
const INITIAL_LIVE_PANE = {
|
|
2524
|
+
pendingApproval: null,
|
|
2525
|
+
pendingQuestion: null
|
|
2526
|
+
};
|
|
2527
|
+
//#endregion
|
|
2313
2528
|
//#region src/tui/controllers/session-event-handler.ts
|
|
2529
|
+
/** Short preview slice for subagent activity detail (keeps deltas small). */
|
|
2530
|
+
function sliceLeading(text, max = 48) {
|
|
2531
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
2532
|
+
}
|
|
2314
2533
|
function addTokenUsage(a, b) {
|
|
2315
2534
|
return {
|
|
2316
2535
|
inputOther: a.inputOther + b.inputOther,
|
|
@@ -2319,6 +2538,20 @@ function addTokenUsage(a, b) {
|
|
|
2319
2538
|
output: a.output + b.output
|
|
2320
2539
|
};
|
|
2321
2540
|
}
|
|
2541
|
+
/** Subtract cumulative usage: current minus previous (component-wise, clamped
|
|
2542
|
+
* at zero). Used to avoid double-counting a resumed subagent's cumulative
|
|
2543
|
+
* usage (each completed event carries the running total, not the delta). */
|
|
2544
|
+
function subtractTokenUsage(current, previous) {
|
|
2545
|
+
return {
|
|
2546
|
+
inputOther: Math.max(0, current.inputOther - previous.inputOther),
|
|
2547
|
+
inputCacheRead: Math.max(0, current.inputCacheRead - previous.inputCacheRead),
|
|
2548
|
+
inputCacheCreation: Math.max(0, current.inputCacheCreation - previous.inputCacheCreation),
|
|
2549
|
+
output: Math.max(0, current.output - previous.output)
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
function isZeroUsage(u) {
|
|
2553
|
+
return u.inputOther === 0 && u.inputCacheRead === 0 && u.inputCacheCreation === 0 && u.output === 0;
|
|
2554
|
+
}
|
|
2322
2555
|
var SessionEventHandler = class {
|
|
2323
2556
|
host;
|
|
2324
2557
|
constructor(host) {
|
|
@@ -2343,6 +2576,10 @@ var SessionEventHandler = class {
|
|
|
2343
2576
|
* (spawned by the current turn's Agent tool), background agents persist. */
|
|
2344
2577
|
foregroundSubagentCount = 0;
|
|
2345
2578
|
subagentInfo = /* @__PURE__ */ new Map();
|
|
2579
|
+
/** Sidebar subagent slot state machine (see utils/subagent-slots.ts). */
|
|
2580
|
+
subagentSlots = new SubagentSlots();
|
|
2581
|
+
/** Tool callId of the in-flight UpdateGoal(complete) (judging), if any. */
|
|
2582
|
+
goalJudgeCallId;
|
|
2346
2583
|
renderedSkillActivationIds = /* @__PURE__ */ new Set();
|
|
2347
2584
|
renderedMcpServerStatusKeys = /* @__PURE__ */ new Map();
|
|
2348
2585
|
mcpServerStatusSpinners = /* @__PURE__ */ new Map();
|
|
@@ -2351,12 +2588,18 @@ var SessionEventHandler = class {
|
|
|
2351
2588
|
this.backgroundTasks.clear();
|
|
2352
2589
|
this.backgroundTaskTranscriptedTerminal.clear();
|
|
2353
2590
|
this.subagentInfo.clear();
|
|
2591
|
+
this.subagentSlots.reset();
|
|
2354
2592
|
this.renderedSkillActivationIds.clear();
|
|
2355
2593
|
this.renderedMcpServerStatusKeys.clear();
|
|
2356
2594
|
this.stopAllMcpServerStatusSpinners();
|
|
2357
2595
|
this.pendingSkillCandidates = [];
|
|
2358
2596
|
this.promptedSkillCandidates.clear();
|
|
2359
|
-
this.
|
|
2597
|
+
this.goalJudgeCallId = void 0;
|
|
2598
|
+
this.subagentUsageSeen.clear();
|
|
2599
|
+
this.host.setAppState({
|
|
2600
|
+
subagentUsage: {},
|
|
2601
|
+
goalJudge: "awaiting"
|
|
2602
|
+
});
|
|
2360
2603
|
}
|
|
2361
2604
|
startSubscription() {
|
|
2362
2605
|
const { host } = this;
|
|
@@ -2520,9 +2763,42 @@ var SessionEventHandler = class {
|
|
|
2520
2763
|
if (this.mcpServerStatusSpinners.size > 0) state.transcriptContainer.invalidate();
|
|
2521
2764
|
this.mcpServerStatusSpinners.clear();
|
|
2522
2765
|
}
|
|
2766
|
+
/** Read-only snapshot of the sidebar subagent slots (8 default types +
|
|
2767
|
+
* extras in spawn order). Consumed by the sidebar data provider. */
|
|
2768
|
+
getSubagentSlots() {
|
|
2769
|
+
return this.subagentSlots.getSlots();
|
|
2770
|
+
}
|
|
2771
|
+
/** Feed routed subagent activity into the slot state machine. */
|
|
2772
|
+
feedSubagentSlots(event, agentId) {
|
|
2773
|
+
switch (event.type) {
|
|
2774
|
+
case "subagent.spawned":
|
|
2775
|
+
this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
|
|
2776
|
+
break;
|
|
2777
|
+
case "subagent.started":
|
|
2778
|
+
this.subagentSlots.onStarted(event.subagentId);
|
|
2779
|
+
break;
|
|
2780
|
+
case "subagent.completed":
|
|
2781
|
+
case "subagent.failed":
|
|
2782
|
+
this.subagentSlots.onTerminated(event.subagentId);
|
|
2783
|
+
break;
|
|
2784
|
+
case "tool.call.started":
|
|
2785
|
+
this.subagentSlots.onActivity(agentId, "tool", `tool: ${event.name}`);
|
|
2786
|
+
break;
|
|
2787
|
+
case "tool.call.delta": break;
|
|
2788
|
+
case "tool.result":
|
|
2789
|
+
this.subagentSlots.onActivity(agentId, "toolResult", "tool result");
|
|
2790
|
+
break;
|
|
2791
|
+
case "assistant.delta":
|
|
2792
|
+
case "thinking.delta":
|
|
2793
|
+
this.subagentSlots.onActivity(agentId, "output", sliceLeading(event.delta ?? ""));
|
|
2794
|
+
break;
|
|
2795
|
+
default: break;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2523
2798
|
routeSubagentEvent(event) {
|
|
2524
2799
|
const subagentId = event.agentId;
|
|
2525
2800
|
if (subagentId === "main") return false;
|
|
2801
|
+
this.feedSubagentSlots(event, subagentId);
|
|
2526
2802
|
const { streamingUI } = this.host;
|
|
2527
2803
|
const info = this.subagentInfo.get(subagentId);
|
|
2528
2804
|
if (info === void 0 || info.parentToolCallId.length === 0) return true;
|
|
@@ -2750,6 +3026,16 @@ var SessionEventHandler = class {
|
|
|
2750
3026
|
handleToolCall(event) {
|
|
2751
3027
|
const { streamingUI } = this.host;
|
|
2752
3028
|
streamingUI.flushNow();
|
|
3029
|
+
if (event.name === "SendSubagentMessage") {
|
|
3030
|
+
const args = argsRecord(event.args);
|
|
3031
|
+
const target = stringValue(args["agent_id"]);
|
|
3032
|
+
const operation = stringValue(args["operation"]);
|
|
3033
|
+
this.subagentSlots.onMessagingStart(event.toolCallId, target === void 0 || target.length === 0 ? void 0 : target, operation === void 0 || operation.length === 0 ? void 0 : operation);
|
|
3034
|
+
}
|
|
3035
|
+
if (event.name === "UpdateGoal" && stringValue(argsRecord(event.args)["status"]) === "complete") {
|
|
3036
|
+
this.goalJudgeCallId = event.toolCallId;
|
|
3037
|
+
this.host.setAppState({ goalJudge: "judging" });
|
|
3038
|
+
}
|
|
2753
3039
|
const { turnId, step } = streamingUI.getTurnContext();
|
|
2754
3040
|
const toolCall = {
|
|
2755
3041
|
id: event.toolCallId,
|
|
@@ -2808,6 +3094,12 @@ var SessionEventHandler = class {
|
|
|
2808
3094
|
}
|
|
2809
3095
|
handleToolResult(event) {
|
|
2810
3096
|
const { streamingUI } = this.host;
|
|
3097
|
+
this.subagentSlots.onMessagingEnd(event.toolCallId);
|
|
3098
|
+
if (event.toolCallId === this.goalJudgeCallId) {
|
|
3099
|
+
this.goalJudgeCallId = void 0;
|
|
3100
|
+
const goalStatus = this.host.state.appState.goal?.status;
|
|
3101
|
+
this.host.setAppState({ goalJudge: goalStatus === "complete" ? "adjudicated" : "awaiting" });
|
|
3102
|
+
}
|
|
2811
3103
|
streamingUI.flushNow();
|
|
2812
3104
|
const resultData = {
|
|
2813
3105
|
tool_call_id: event.toolCallId,
|
|
@@ -2948,16 +3240,28 @@ var SessionEventHandler = class {
|
|
|
2948
3240
|
}
|
|
2949
3241
|
handleGoalUpdated(event) {
|
|
2950
3242
|
const snapshot = event.snapshot;
|
|
2951
|
-
if (snapshot === null)
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
3243
|
+
if (snapshot === null) {
|
|
3244
|
+
this.host.setAppState({
|
|
3245
|
+
goal: null,
|
|
3246
|
+
goalActive: false
|
|
3247
|
+
});
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
if (snapshot.status === "complete") {
|
|
3251
|
+
this.goalJudgeCallId = void 0;
|
|
3252
|
+
this.host.setAppState({ goalJudge: "adjudicated" });
|
|
3253
|
+
} else if (this.host.state.appState.goalJudge === "adjudicated") this.host.setAppState({ goalJudge: "awaiting" });
|
|
3254
|
+
this.host.setAppState({
|
|
2956
3255
|
goal: {
|
|
2957
3256
|
objective: snapshot.objective,
|
|
3257
|
+
status: normalizeGoalStatus(snapshot.status),
|
|
2958
3258
|
turnsUsed: snapshot.turnsUsed ?? 0,
|
|
2959
3259
|
wallClockMs: snapshot.wallClockMs ?? 0,
|
|
2960
|
-
wallClockBaseAt: Date.now()
|
|
3260
|
+
wallClockBaseAt: Date.now(),
|
|
3261
|
+
completionCriterion: snapshot.completionCriterion ?? null,
|
|
3262
|
+
tokensUsed: snapshot.tokensUsed ?? 0,
|
|
3263
|
+
inputTokens: snapshot.inputTokens ?? null,
|
|
3264
|
+
outputTokens: snapshot.outputTokens ?? null
|
|
2961
3265
|
},
|
|
2962
3266
|
goalActive: snapshot.status === "active"
|
|
2963
3267
|
});
|
|
@@ -3059,6 +3363,7 @@ var SessionEventHandler = class {
|
|
|
3059
3363
|
} else this.host.setAppState({ isCompacting: false });
|
|
3060
3364
|
}
|
|
3061
3365
|
handleSubagentSpawned(event) {
|
|
3366
|
+
this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
|
|
3062
3367
|
const { streamingUI } = this.host;
|
|
3063
3368
|
if (streamingUI.getActiveToolCall(event.parentToolCallId)?.name === "WolfPack") {
|
|
3064
3369
|
const routingId = event.subagentId;
|
|
@@ -3119,6 +3424,7 @@ var SessionEventHandler = class {
|
|
|
3119
3424
|
});
|
|
3120
3425
|
}
|
|
3121
3426
|
handleSubagentStarted(event) {
|
|
3427
|
+
this.subagentSlots.onStarted(event.subagentId);
|
|
3122
3428
|
if (event.runInBackground) return;
|
|
3123
3429
|
const routingId = this.subagentInfo.get(event.subagentId)?.parentToolCallId ?? event.parentToolCallId;
|
|
3124
3430
|
const tc = this.host.streamingUI.getToolComponent(routingId);
|
|
@@ -3130,6 +3436,7 @@ var SessionEventHandler = class {
|
|
|
3130
3436
|
});
|
|
3131
3437
|
}
|
|
3132
3438
|
handleSubagentCompleted(event) {
|
|
3439
|
+
this.subagentSlots.onTerminated(event.subagentId);
|
|
3133
3440
|
this.recordSubagentUsage(event.subagentId, void 0, event.usage);
|
|
3134
3441
|
const { streamingUI } = this.host;
|
|
3135
3442
|
const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
|
|
@@ -3159,6 +3466,7 @@ var SessionEventHandler = class {
|
|
|
3159
3466
|
streamingUI.removeToolComponentIfInactive(event.parentToolCallId);
|
|
3160
3467
|
}
|
|
3161
3468
|
handleSubagentFailed(event) {
|
|
3469
|
+
this.subagentSlots.onTerminated(event.subagentId);
|
|
3162
3470
|
this.recordSubagentUsage(event.subagentId, void 0, event.usage);
|
|
3163
3471
|
const { streamingUI } = this.host;
|
|
3164
3472
|
const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
|
|
@@ -3184,13 +3492,21 @@ var SessionEventHandler = class {
|
|
|
3184
3492
|
tc.onSubagentFailed({ error: event.error });
|
|
3185
3493
|
streamingUI.removeToolComponentIfInactive(this.subagentInfo.get(event.subagentId)?.parentToolCallId ?? event.parentToolCallId);
|
|
3186
3494
|
}
|
|
3495
|
+
/** Cumulative usage already recorded per subagent instance (completed
|
|
3496
|
+
* events carry the running total — we only add the delta since the last
|
|
3497
|
+
* completed/failed event, avoiding double-counting after resume). */
|
|
3498
|
+
subagentUsageSeen = /* @__PURE__ */ new Map();
|
|
3187
3499
|
recordSubagentUsage(subagentId, subagentName, usage) {
|
|
3188
3500
|
if (usage === void 0) return;
|
|
3501
|
+
const previous = this.subagentUsageSeen.get(subagentId);
|
|
3502
|
+
const delta = previous === void 0 ? usage : subtractTokenUsage(usage, previous);
|
|
3503
|
+
this.subagentUsageSeen.set(subagentId, usage);
|
|
3504
|
+
if (isZeroUsage(delta)) return;
|
|
3189
3505
|
const name = this.subagentInfo.get(subagentId)?.name ?? subagentName ?? subagentId;
|
|
3190
3506
|
const current = this.host.state.appState.subagentUsage[name];
|
|
3191
3507
|
const next = {
|
|
3192
3508
|
...this.host.state.appState.subagentUsage,
|
|
3193
|
-
[name]: current === void 0 ?
|
|
3509
|
+
[name]: current === void 0 ? delta : addTokenUsage(current, delta)
|
|
3194
3510
|
};
|
|
3195
3511
|
this.host.setAppState({ subagentUsage: next });
|
|
3196
3512
|
}
|
|
@@ -5996,7 +6312,10 @@ var TranscriptController = class TranscriptController {
|
|
|
5996
6312
|
committedComponent;
|
|
5997
6313
|
liveComponentToEntry = /* @__PURE__ */ new Map();
|
|
5998
6314
|
pendingComponents = /* @__PURE__ */ new Set();
|
|
5999
|
-
|
|
6315
|
+
/** Max live transcript children before the oldest are folded into the
|
|
6316
|
+
* committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
|
|
6317
|
+
* (mirrors a commit-fold approach for bounding ultra-long sessions). */
|
|
6318
|
+
static LIVE_LIMIT = Number.isFinite(Number(process.env["SCREAM_TRANSCRIPT_LIVE_LIMIT"])) && Number(process.env["SCREAM_TRANSCRIPT_LIVE_LIMIT"]) > 0 ? Math.floor(Number(process.env["SCREAM_TRANSCRIPT_LIVE_LIMIT"])) : 150;
|
|
6000
6319
|
constructor(host) {
|
|
6001
6320
|
this.host = host;
|
|
6002
6321
|
}
|
|
@@ -6341,6 +6660,469 @@ var StatusBarPaneComponent = class extends Container {
|
|
|
6341
6660
|
}
|
|
6342
6661
|
}
|
|
6343
6662
|
};
|
|
6663
|
+
function clampSidebarWidth(cols) {
|
|
6664
|
+
if (!Number.isFinite(cols)) return 30;
|
|
6665
|
+
return Math.min(60, Math.max(24, Math.round(cols)));
|
|
6666
|
+
}
|
|
6667
|
+
//#endregion
|
|
6668
|
+
//#region src/tui/components/sidebar/panels/git-panel.ts
|
|
6669
|
+
/**
|
|
6670
|
+
* Summary-only Git panel (摘要式): the workdir name plus added (green) /
|
|
6671
|
+
* deleted (red) line counts and the changed-file count. Reads the current
|
|
6672
|
+
* snapshot in render so the panel never shows a stale working tree.
|
|
6673
|
+
*/
|
|
6674
|
+
var GitPanelContent = class {
|
|
6675
|
+
ctx;
|
|
6676
|
+
colors;
|
|
6677
|
+
constructor(ctx, colors) {
|
|
6678
|
+
this.ctx = ctx;
|
|
6679
|
+
this.colors = colors;
|
|
6680
|
+
}
|
|
6681
|
+
invalidate() {}
|
|
6682
|
+
render(width) {
|
|
6683
|
+
const git = this.ctx.getData().git;
|
|
6684
|
+
if (git === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.git_no_repo"))];
|
|
6685
|
+
const dim = (s) => chalk.hex(this.colors.textDim)(s);
|
|
6686
|
+
const workDir = truncateToWidth(chalk.hex(this.colors.text)(basename(git.workDir)), width);
|
|
6687
|
+
if (git.diffAdded === 0 && git.diffDeleted === 0 && git.filesCount === 0) return [workDir, dim(t("sidebar.git_clean"))];
|
|
6688
|
+
return [workDir, truncateToWidth(`${chalk.hex(this.colors.success)(`+${git.diffAdded}`)} ${chalk.hex(this.colors.error)(`−${git.diffDeleted}`)} ${dim(t("sidebar.git_files", { count: git.filesCount }))}`, width)];
|
|
6689
|
+
}
|
|
6690
|
+
};
|
|
6691
|
+
const gitPanel = {
|
|
6692
|
+
id: "git",
|
|
6693
|
+
get title() {
|
|
6694
|
+
return t("sidebar.git");
|
|
6695
|
+
},
|
|
6696
|
+
width: 30,
|
|
6697
|
+
build(ctx) {
|
|
6698
|
+
return new GitPanelContent(ctx, ctx.colors);
|
|
6699
|
+
}
|
|
6700
|
+
};
|
|
6701
|
+
//#endregion
|
|
6702
|
+
//#region src/tui/utils/display-width.ts
|
|
6703
|
+
/** Display-column width of a string, matching the terminal layout engine
|
|
6704
|
+
* (ANSI sequences stripped, CJK/wide chars counted as 2 columns). */
|
|
6705
|
+
function displayWidth(text) {
|
|
6706
|
+
return visibleWidth(text);
|
|
6707
|
+
}
|
|
6708
|
+
/** Pad `label` to `target` display columns with spaces (CJK-aware). */
|
|
6709
|
+
function padLabel(label, target) {
|
|
6710
|
+
return label + " ".repeat(Math.max(0, target - displayWidth(label)));
|
|
6711
|
+
}
|
|
6712
|
+
//#endregion
|
|
6713
|
+
//#region src/tui/components/sidebar/panels/session-panel.ts
|
|
6714
|
+
function formatDuration(ms) {
|
|
6715
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
6716
|
+
if (getLocale() === "zh") {
|
|
6717
|
+
if (totalSeconds < 60) return `${totalSeconds}秒`;
|
|
6718
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
6719
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
6720
|
+
return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`;
|
|
6721
|
+
}
|
|
6722
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
6723
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
6724
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
6725
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
6726
|
+
return `${minutes}m`;
|
|
6727
|
+
}
|
|
6728
|
+
/** 1,234 → "1,234" via the platform locale grouping. */
|
|
6729
|
+
function formatCount$1(n) {
|
|
6730
|
+
return n.toLocaleString("en-US");
|
|
6731
|
+
}
|
|
6732
|
+
/**
|
|
6733
|
+
* Session summary, live-rendered each frame: total token consumption and
|
|
6734
|
+
* total working time as the primary rows, turn/tool/message/compaction
|
|
6735
|
+
* counters as the secondary row. Uptime derives from `startedAt` so it ticks
|
|
6736
|
+
* between data updates; counts come from the snapshot in getData().
|
|
6737
|
+
*/
|
|
6738
|
+
var SessionPanelContent = class {
|
|
6739
|
+
ctx;
|
|
6740
|
+
colors;
|
|
6741
|
+
constructor(ctx, colors) {
|
|
6742
|
+
this.ctx = ctx;
|
|
6743
|
+
this.colors = colors;
|
|
6744
|
+
}
|
|
6745
|
+
invalidate() {}
|
|
6746
|
+
render(width) {
|
|
6747
|
+
const stats = this.ctx.getData().sessionStats;
|
|
6748
|
+
if (stats === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.session_empty"))];
|
|
6749
|
+
const label = (s) => chalk.hex(this.colors.textDim)(s);
|
|
6750
|
+
const value = (s) => chalk.hex(this.colors.text)(s);
|
|
6751
|
+
const now = Date.now();
|
|
6752
|
+
const rows = [
|
|
6753
|
+
[t("sidebar.tokens"), formatCount$1(stats.tokensTotal)],
|
|
6754
|
+
[t("sidebar.tokens_cache_hit"), formatCount$1(stats.tokensInputCacheHit)],
|
|
6755
|
+
[t("sidebar.tokens_cache_miss"), formatCount$1(stats.tokensInputCacheMiss)],
|
|
6756
|
+
[t("sidebar.tokens_output"), formatCount$1(stats.tokensOutput)],
|
|
6757
|
+
[t("sidebar.uptime"), formatDuration(now - stats.startedAt)]
|
|
6758
|
+
];
|
|
6759
|
+
const maxCols = Math.max(...rows.map(([k]) => displayWidth(k)));
|
|
6760
|
+
const lines = [];
|
|
6761
|
+
for (const [k, v] of rows) lines.push(label(padLabel(k, maxCols)) + value(v));
|
|
6762
|
+
lines.push(truncateToWidth(chalk.hex(this.colors.textDim)(`${t("sidebar.turns")} ${stats.turns} · ${t("sidebar.tools")} ${stats.toolCalls}`), width));
|
|
6763
|
+
lines.push(truncateToWidth(chalk.hex(this.colors.textDim)(`${t("sidebar.messages")} ${formatCount$1(stats.messages)} · ${t("sidebar.compacts")} ${stats.compactions}`), width));
|
|
6764
|
+
return lines;
|
|
6765
|
+
}
|
|
6766
|
+
};
|
|
6767
|
+
const sessionPanel = {
|
|
6768
|
+
id: "session",
|
|
6769
|
+
get title() {
|
|
6770
|
+
return t("sidebar.session");
|
|
6771
|
+
},
|
|
6772
|
+
width: 30,
|
|
6773
|
+
build(ctx) {
|
|
6774
|
+
return new SessionPanelContent(ctx, ctx.colors);
|
|
6775
|
+
}
|
|
6776
|
+
};
|
|
6777
|
+
//#endregion
|
|
6778
|
+
//#region src/tui/components/sidebar/panels/agents-panel.ts
|
|
6779
|
+
/** Gradient cycle for active agent slots. Shorter than the footer's 4s cycle:
|
|
6780
|
+
* with the 1s sidebar heartbeat each tick advances half the cycle, so the
|
|
6781
|
+
* colour jump is clearly visible instead of drifting imperceptibly. */
|
|
6782
|
+
const AGENT_GRADIENT_CYCLE_MS = 2e3;
|
|
6783
|
+
/** Localized status word per slot state. */
|
|
6784
|
+
function statusText(status) {
|
|
6785
|
+
switch (status) {
|
|
6786
|
+
case "idle": return t("sidebar.agent_idle");
|
|
6787
|
+
case "working": return t("sidebar.agent_working");
|
|
6788
|
+
case "outputting": return t("sidebar.agent_outputting");
|
|
6789
|
+
case "messaging": return t("sidebar.agent_messaging");
|
|
6790
|
+
case "reworking": return t("sidebar.agent_reworking");
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
/**
|
|
6794
|
+
* Fixed 8-slot subagent panel: one row per default subagent type (plus any
|
|
6795
|
+
* custom types appended in spawn order). Live-rendered each frame from the
|
|
6796
|
+
* current snapshot; idle slots stay greyed out.
|
|
6797
|
+
*/
|
|
6798
|
+
var AgentsPanelContent = class {
|
|
6799
|
+
ctx;
|
|
6800
|
+
colors;
|
|
6801
|
+
constructor(ctx, colors) {
|
|
6802
|
+
this.ctx = ctx;
|
|
6803
|
+
this.colors = colors;
|
|
6804
|
+
}
|
|
6805
|
+
invalidate() {}
|
|
6806
|
+
render(width) {
|
|
6807
|
+
const agents = this.ctx.getData().agents;
|
|
6808
|
+
if (agents === void 0 || agents.length === 0) return [];
|
|
6809
|
+
const lines = [];
|
|
6810
|
+
const nameWidth = Math.max(...agents.map((a) => displayWidth(a.type)));
|
|
6811
|
+
for (const [index, slot] of agents.entries()) lines.push(this.renderSlot(slot, index, nameWidth, width));
|
|
6812
|
+
return lines;
|
|
6813
|
+
}
|
|
6814
|
+
renderSlot(slot, index, nameWidth, width) {
|
|
6815
|
+
const dim = (s) => chalk.hex(this.colors.textDim)(s);
|
|
6816
|
+
if (slot.status === "idle") return `${dim("○")} ${dim(padLabel(slot.type, nameWidth))} ${dim(statusText("idle"))}`;
|
|
6817
|
+
const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .125) % 1;
|
|
6818
|
+
const active = slot.status === "working" || slot.status === "outputting" ? chalk.hex(lerpGradient(phase)).bold : void 0;
|
|
6819
|
+
return truncateToWidth(`${active ? active("●") : chalk.hex(this.statusColor(slot.status))("●")} ${chalk.hex(this.colors.textStrong)(padLabel(slot.type, nameWidth))} ${active ? active(statusText(slot.status)) : chalk.hex(this.colors.text)(statusText(slot.status))}${slot.count > 1 ? ` ${dim("×" + slot.count)}` : ""}`, width);
|
|
6820
|
+
}
|
|
6821
|
+
statusColor(status) {
|
|
6822
|
+
switch (status) {
|
|
6823
|
+
case "working":
|
|
6824
|
+
case "outputting": return this.colors.primary;
|
|
6825
|
+
case "messaging": return this.colors.accent;
|
|
6826
|
+
case "reworking": return this.colors.warning;
|
|
6827
|
+
case "idle": return this.colors.textDim;
|
|
6828
|
+
}
|
|
6829
|
+
}
|
|
6830
|
+
};
|
|
6831
|
+
const agentsPanel = {
|
|
6832
|
+
id: "agents",
|
|
6833
|
+
get title() {
|
|
6834
|
+
return t("sidebar.agents");
|
|
6835
|
+
},
|
|
6836
|
+
width: 34,
|
|
6837
|
+
build(ctx) {
|
|
6838
|
+
return new AgentsPanelContent(ctx, ctx.colors);
|
|
6839
|
+
}
|
|
6840
|
+
};
|
|
6841
|
+
//#endregion
|
|
6842
|
+
//#region src/tui/components/sidebar/panels/goal-panel.ts
|
|
6843
|
+
function formatWallClock(ms) {
|
|
6844
|
+
const totalMinutes = Math.max(0, Math.floor(ms / 6e4));
|
|
6845
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
6846
|
+
const minutes = totalMinutes % 60;
|
|
6847
|
+
if (getLocale() === "zh") return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`;
|
|
6848
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
6849
|
+
return `${minutes}m`;
|
|
6850
|
+
}
|
|
6851
|
+
function formatCount(n) {
|
|
6852
|
+
return n.toLocaleString("en-US");
|
|
6853
|
+
}
|
|
6854
|
+
function judgeText(state) {
|
|
6855
|
+
switch (state) {
|
|
6856
|
+
case "judging": return t("sidebar.goal_judge_judging");
|
|
6857
|
+
case "adjudicated": return t("sidebar.goal_judge_adjudicated");
|
|
6858
|
+
case "awaiting": return t("sidebar.goal_judge_awaiting");
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
/**
|
|
6862
|
+
* Goal status panel, always visible: state badge (in-progress / completed /
|
|
6863
|
+
* paused / blocked / judging), objective, then the same two-column metric
|
|
6864
|
+
* layout as the Session panel (tokens spent / turns / wall-clock / judge),
|
|
6865
|
+
* and the completion criterion line.
|
|
6866
|
+
*/
|
|
6867
|
+
var GoalPanelContent = class {
|
|
6868
|
+
ctx;
|
|
6869
|
+
colors;
|
|
6870
|
+
constructor(ctx, colors) {
|
|
6871
|
+
this.ctx = ctx;
|
|
6872
|
+
this.colors = colors;
|
|
6873
|
+
}
|
|
6874
|
+
invalidate() {}
|
|
6875
|
+
render(width) {
|
|
6876
|
+
const goal = this.ctx.getData().goal;
|
|
6877
|
+
if (goal === void 0) return [chalk.hex(this.colors.textDim)("○ " + t("sidebar.goal_idle"))];
|
|
6878
|
+
const dim = (s) => chalk.hex(this.colors.textDim)(s);
|
|
6879
|
+
const text = (s) => chalk.hex(this.colors.text)(s);
|
|
6880
|
+
const now = Date.now();
|
|
6881
|
+
const wallMs = goal.status === "active" ? goal.wallClockMs + Math.max(0, now - goal.wallClockBaseAt) : goal.wallClockMs;
|
|
6882
|
+
const lines = [];
|
|
6883
|
+
lines.push(this.renderBadge(goal.status, goal.judge, dim));
|
|
6884
|
+
lines.push(truncateToWidth(dim(goal.objective), width));
|
|
6885
|
+
const rows = [
|
|
6886
|
+
[t("sidebar.goal_tokens"), formatCount(goal.tokensUsed)],
|
|
6887
|
+
[t("sidebar.goal_tokens_input"), goal.inputTokens === null ? "—" : formatCount(goal.inputTokens)],
|
|
6888
|
+
[t("sidebar.goal_tokens_output"), goal.outputTokens === null ? "—" : formatCount(goal.outputTokens)],
|
|
6889
|
+
[t("sidebar.goal_turns"), String(goal.turnsUsed)],
|
|
6890
|
+
[t("sidebar.goal_wall"), formatWallClock(wallMs)],
|
|
6891
|
+
[t("sidebar.goal_judge"), judgeText(goal.judge)]
|
|
6892
|
+
];
|
|
6893
|
+
const maxCols = Math.max(...rows.map(([k]) => displayWidth(k)));
|
|
6894
|
+
for (const [label, value] of rows) lines.push(dim(padLabel(label, maxCols)) + text(value));
|
|
6895
|
+
if (goal.completionCriterion !== null) lines.push(dim(padLabel(t("sidebar.goal_criterion"), maxCols)) + truncateToWidth(text(goal.completionCriterion), Math.max(4, width - maxCols)));
|
|
6896
|
+
return lines;
|
|
6897
|
+
}
|
|
6898
|
+
/** State badge: judging wins over everything; otherwise map the snapshot
|
|
6899
|
+
* status 1:1 (paused is NOT completed — fixes the interrupted-goal bug). */
|
|
6900
|
+
renderBadge(status, judge, dim) {
|
|
6901
|
+
if (judge === "judging") return chalk.hex(this.colors.accent)("⚖") + dim(" " + t("sidebar.goal_judging"));
|
|
6902
|
+
switch (status) {
|
|
6903
|
+
case "active": return chalk.hex(this.colors.primary)("●") + dim(" " + t("sidebar.goal_active"));
|
|
6904
|
+
case "complete": return chalk.hex(this.colors.success)("✓") + dim(" " + t("sidebar.goal_completed"));
|
|
6905
|
+
case "paused": return chalk.hex(this.colors.warning)("⏸") + dim(" " + t("sidebar.goal_paused"));
|
|
6906
|
+
case "blocked": return chalk.hex(this.colors.warning)("⚠") + dim(" " + t("sidebar.goal_blocked"));
|
|
6907
|
+
default: return dim(String(status));
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
};
|
|
6911
|
+
const goalPanel = {
|
|
6912
|
+
id: "goal",
|
|
6913
|
+
get title() {
|
|
6914
|
+
return t("sidebar.goal");
|
|
6915
|
+
},
|
|
6916
|
+
width: 32,
|
|
6917
|
+
build(ctx) {
|
|
6918
|
+
return new GoalPanelContent(ctx, ctx.colors);
|
|
6919
|
+
}
|
|
6920
|
+
};
|
|
6921
|
+
//#endregion
|
|
6922
|
+
//#region src/utils/git/git-status.ts
|
|
6923
|
+
/**
|
|
6924
|
+
* Cached git branch + working-tree status (sidebar Git panel; previously
|
|
6925
|
+
* also the footer git badge, which the sidebar now replaces).
|
|
6926
|
+
*
|
|
6927
|
+
* Branch name refreshes every 5s, porcelain status every 15s. Branch
|
|
6928
|
+
* and status reads stay synchronous with short timeouts. Spawn failures
|
|
6929
|
+
* keep the previous snapshot instead of flashing an empty working tree.
|
|
6930
|
+
*/
|
|
6931
|
+
const BRANCH_TTL_MS = 5e3;
|
|
6932
|
+
const STATUS_TTL_MS = 15e3;
|
|
6933
|
+
const SPAWN_TIMEOUT_MS = 500;
|
|
6934
|
+
const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/;
|
|
6935
|
+
function createGitStatusCache(workDir) {
|
|
6936
|
+
const isRepo = detectGitRepo(workDir);
|
|
6937
|
+
let branch = {
|
|
6938
|
+
value: null,
|
|
6939
|
+
fetchedAt: 0
|
|
6940
|
+
};
|
|
6941
|
+
let status = {
|
|
6942
|
+
dirty: false,
|
|
6943
|
+
ahead: 0,
|
|
6944
|
+
behind: 0,
|
|
6945
|
+
diffAdded: 0,
|
|
6946
|
+
diffDeleted: 0,
|
|
6947
|
+
files: [],
|
|
6948
|
+
fetchedAt: 0
|
|
6949
|
+
};
|
|
6950
|
+
return { getStatus: () => {
|
|
6951
|
+
if (!isRepo) return null;
|
|
6952
|
+
const now = Date.now();
|
|
6953
|
+
if (now - branch.fetchedAt >= BRANCH_TTL_MS) branch = {
|
|
6954
|
+
value: readBranch(workDir),
|
|
6955
|
+
fetchedAt: now
|
|
6956
|
+
};
|
|
6957
|
+
if (branch.value === null) return null;
|
|
6958
|
+
if (now - status.fetchedAt >= STATUS_TTL_MS) {
|
|
6959
|
+
const next = readStatus(workDir);
|
|
6960
|
+
if (next !== null) status = {
|
|
6961
|
+
...next,
|
|
6962
|
+
fetchedAt: now
|
|
6963
|
+
};
|
|
6964
|
+
else status.fetchedAt = now;
|
|
6965
|
+
}
|
|
6966
|
+
return {
|
|
6967
|
+
branch: branch.value,
|
|
6968
|
+
dirty: status.dirty,
|
|
6969
|
+
ahead: status.ahead,
|
|
6970
|
+
behind: status.behind,
|
|
6971
|
+
diffAdded: status.diffAdded,
|
|
6972
|
+
diffDeleted: status.diffDeleted,
|
|
6973
|
+
files: status.files
|
|
6974
|
+
};
|
|
6975
|
+
} };
|
|
6976
|
+
}
|
|
6977
|
+
function detectGitRepo(workDir) {
|
|
6978
|
+
try {
|
|
6979
|
+
const result = spawnSync("git", [
|
|
6980
|
+
"-C",
|
|
6981
|
+
workDir,
|
|
6982
|
+
"rev-parse",
|
|
6983
|
+
"--is-inside-work-tree"
|
|
6984
|
+
], {
|
|
6985
|
+
encoding: "utf8",
|
|
6986
|
+
timeout: SPAWN_TIMEOUT_MS
|
|
6987
|
+
});
|
|
6988
|
+
return result.status === 0 && result.stdout.trim() === "true";
|
|
6989
|
+
} catch {
|
|
6990
|
+
return false;
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
function readBranch(workDir) {
|
|
6994
|
+
try {
|
|
6995
|
+
const result = spawnSync("git", [
|
|
6996
|
+
"-C",
|
|
6997
|
+
workDir,
|
|
6998
|
+
"branch",
|
|
6999
|
+
"--show-current"
|
|
7000
|
+
], {
|
|
7001
|
+
encoding: "utf8",
|
|
7002
|
+
timeout: SPAWN_TIMEOUT_MS
|
|
7003
|
+
});
|
|
7004
|
+
if (result.status !== 0) return null;
|
|
7005
|
+
const name = result.stdout.trim();
|
|
7006
|
+
return name.length > 0 ? name : null;
|
|
7007
|
+
} catch {
|
|
7008
|
+
return null;
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
/** Strip the quoting/escaping `git status --porcelain` applies when
|
|
7012
|
+
* `core.quotePath` is active and the path contains special characters. The
|
|
7013
|
+
* same semantics as the web server's parser: JSON.parse handles the C-style
|
|
7014
|
+
* escapes git emits; exotic cases stay literal, acceptable for display. */
|
|
7015
|
+
function unquotePorcelainPath(raw) {
|
|
7016
|
+
if (raw.length >= 2 && raw.startsWith("\"") && raw.endsWith("\"")) try {
|
|
7017
|
+
return JSON.parse(raw);
|
|
7018
|
+
} catch {
|
|
7019
|
+
return raw;
|
|
7020
|
+
}
|
|
7021
|
+
return raw;
|
|
7022
|
+
}
|
|
7023
|
+
/** Parse one porcelain entry line into a {@link GitFileEntry}. A rename entry
|
|
7024
|
+
* ("R old -> new") keeps the destination path, which is what the user wants
|
|
7025
|
+
* to see in a changed-files list. Spaces inside the XY code are dropped
|
|
7026
|
+
* ("M " → "M", "MM" stays), matching the web client's status label. */
|
|
7027
|
+
function parsePorcelainEntry(line) {
|
|
7028
|
+
const status = line.slice(0, 2).replaceAll(" ", "");
|
|
7029
|
+
let path = line.length > 3 ? line.slice(3).trim() : "";
|
|
7030
|
+
const arrow = path.indexOf(" -> ");
|
|
7031
|
+
if (arrow !== -1) path = path.slice(arrow + 4);
|
|
7032
|
+
return {
|
|
7033
|
+
status,
|
|
7034
|
+
path: unquotePorcelainPath(path)
|
|
7035
|
+
};
|
|
7036
|
+
}
|
|
7037
|
+
/** Returns null when the porcelain read fails (spawn error / non-zero exit);
|
|
7038
|
+
* the caller keeps the previous snapshot on failure. */
|
|
7039
|
+
function readStatus(workDir) {
|
|
7040
|
+
try {
|
|
7041
|
+
const result = spawnSync("git", [
|
|
7042
|
+
"-C",
|
|
7043
|
+
workDir,
|
|
7044
|
+
"status",
|
|
7045
|
+
"--porcelain",
|
|
7046
|
+
"-b"
|
|
7047
|
+
], {
|
|
7048
|
+
encoding: "utf8",
|
|
7049
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
7050
|
+
maxBuffer: 4 * 1024 * 1024
|
|
7051
|
+
});
|
|
7052
|
+
if (result.status !== 0) return null;
|
|
7053
|
+
let dirty = false;
|
|
7054
|
+
let ahead = 0;
|
|
7055
|
+
let behind = 0;
|
|
7056
|
+
const files = [];
|
|
7057
|
+
for (const line of result.stdout.split("\n")) if (line.startsWith("## ")) {
|
|
7058
|
+
const m = AHEAD_BEHIND_RE.exec(line);
|
|
7059
|
+
if (m) {
|
|
7060
|
+
ahead = Number.parseInt(m[1] ?? "0", 10) || 0;
|
|
7061
|
+
behind = Number.parseInt(m[2] ?? "0", 10) || 0;
|
|
7062
|
+
}
|
|
7063
|
+
} else if (line.trim().length > 0) {
|
|
7064
|
+
dirty = true;
|
|
7065
|
+
files.push(parsePorcelainEntry(line));
|
|
7066
|
+
}
|
|
7067
|
+
const diff = dirty ? readDiffStats(workDir) : {
|
|
7068
|
+
added: 0,
|
|
7069
|
+
deleted: 0
|
|
7070
|
+
};
|
|
7071
|
+
return {
|
|
7072
|
+
dirty,
|
|
7073
|
+
ahead,
|
|
7074
|
+
behind,
|
|
7075
|
+
diffAdded: diff.added,
|
|
7076
|
+
diffDeleted: diff.deleted,
|
|
7077
|
+
files,
|
|
7078
|
+
fetchedAt: Date.now()
|
|
7079
|
+
};
|
|
7080
|
+
} catch {
|
|
7081
|
+
return null;
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
function readDiffStats(workDir) {
|
|
7085
|
+
try {
|
|
7086
|
+
const result = spawnSync("git", [
|
|
7087
|
+
"-C",
|
|
7088
|
+
workDir,
|
|
7089
|
+
"diff",
|
|
7090
|
+
"--numstat",
|
|
7091
|
+
"HEAD",
|
|
7092
|
+
"--"
|
|
7093
|
+
], {
|
|
7094
|
+
encoding: "utf8",
|
|
7095
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
7096
|
+
maxBuffer: 4 * 1024 * 1024
|
|
7097
|
+
});
|
|
7098
|
+
if (result.status !== 0) return {
|
|
7099
|
+
added: 0,
|
|
7100
|
+
deleted: 0
|
|
7101
|
+
};
|
|
7102
|
+
let added = 0;
|
|
7103
|
+
let deleted = 0;
|
|
7104
|
+
for (const line of result.stdout.split("\n")) {
|
|
7105
|
+
if (!line) continue;
|
|
7106
|
+
const [addedText, deletedText] = line.split(" ");
|
|
7107
|
+
added += parseDiffNumstatCount(addedText);
|
|
7108
|
+
deleted += parseDiffNumstatCount(deletedText);
|
|
7109
|
+
}
|
|
7110
|
+
return {
|
|
7111
|
+
added,
|
|
7112
|
+
deleted
|
|
7113
|
+
};
|
|
7114
|
+
} catch {
|
|
7115
|
+
return {
|
|
7116
|
+
added: 0,
|
|
7117
|
+
deleted: 0
|
|
7118
|
+
};
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
function parseDiffNumstatCount(value) {
|
|
7122
|
+
if (value === void 0 || value === "-") return 0;
|
|
7123
|
+
const n = Number.parseInt(value, 10);
|
|
7124
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
7125
|
+
}
|
|
6344
7126
|
//#endregion
|
|
6345
7127
|
//#region src/tui/theme/bundle.ts
|
|
6346
7128
|
function createScreamTUIThemeBundle(theme, resolvedTheme) {
|
|
@@ -6686,8 +7468,23 @@ var LifecycleController = class LifecycleController {
|
|
|
6686
7468
|
/** The active status-bar loader (PulseWaveLoader or MoonLoader). Owned by
|
|
6687
7469
|
* this controller; stopped before replacement to avoid leaking timers. */
|
|
6688
7470
|
statusBarLoader;
|
|
7471
|
+
/** Sum all four token buckets into the session's total token consumption. */
|
|
7472
|
+
static sumTokens(usage) {
|
|
7473
|
+
return usage.inputOther + usage.inputCacheRead + usage.inputCacheCreation + usage.output;
|
|
7474
|
+
}
|
|
6689
7475
|
tipRotationTimer;
|
|
6690
7476
|
currentTipIndex = Math.floor(Math.random() * SESSION_TIPS.length);
|
|
7477
|
+
/** Sidebar-only git cache (the footer owns its own). Rebuilt when the
|
|
7478
|
+
* workDir changes; `getStatus()` is TTL-cached so polling is cheap. */
|
|
7479
|
+
sidebarGitCache;
|
|
7480
|
+
sidebarGitCacheWorkDir = "";
|
|
7481
|
+
/** Session start time for the sidebar uptime counter. Reset on session
|
|
7482
|
+
* switch so a resumed old session doesn't show the TUI process age. */
|
|
7483
|
+
sidebarSessionStartedAt = Date.now();
|
|
7484
|
+
sidebarSessionId;
|
|
7485
|
+
/** Memo for the session-stats aggregation: recomputed only when the
|
|
7486
|
+
* transcript length, compaction count or usage object changes. */
|
|
7487
|
+
sidebarStatsMemo;
|
|
6691
7488
|
static MEMORY_IDLE_MS = 900 * 1e3;
|
|
6692
7489
|
static MEMORY_COUNTDOWN_MS = 15 * 1e3;
|
|
6693
7490
|
static MEMORY_EXTRACT_COOLDOWN_MS = 600 * 1e3;
|
|
@@ -6870,8 +7667,30 @@ var LifecycleController = class LifecycleController {
|
|
|
6870
7667
|
minSize: 1
|
|
6871
7668
|
}
|
|
6872
7669
|
]);
|
|
7670
|
+
{
|
|
7671
|
+
const m = this.host.state.sidebarManager;
|
|
7672
|
+
if (!m.allPanels.some((p) => p.id === "git")) m.register(gitPanel);
|
|
7673
|
+
if (!m.allPanels.some((p) => p.id === "session")) m.register(sessionPanel);
|
|
7674
|
+
if (!m.allPanels.some((p) => p.id === "agents")) m.register(agentsPanel);
|
|
7675
|
+
if (!m.allPanels.some((p) => p.id === "goal")) m.register(goalPanel);
|
|
7676
|
+
m.setDataProvider(() => this.buildSidebarData());
|
|
7677
|
+
}
|
|
7678
|
+
const sidebar = this.host.state.sidebarPane;
|
|
6873
7679
|
const layoutRoot = new VStack([{
|
|
6874
|
-
component:
|
|
7680
|
+
component: new HStack([{
|
|
7681
|
+
component: transcriptScrollView,
|
|
7682
|
+
basis: 0,
|
|
7683
|
+
grow: 1,
|
|
7684
|
+
shrink: 1,
|
|
7685
|
+
minSize: 1
|
|
7686
|
+
}, {
|
|
7687
|
+
component: sidebar,
|
|
7688
|
+
basis: "auto",
|
|
7689
|
+
grow: 0,
|
|
7690
|
+
shrink: 0,
|
|
7691
|
+
minSize: 0,
|
|
7692
|
+
visible: (viewport) => viewport.width >= 90
|
|
7693
|
+
}]),
|
|
6875
7694
|
basis: 0,
|
|
6876
7695
|
grow: 1,
|
|
6877
7696
|
shrink: 1,
|
|
@@ -6886,6 +7705,86 @@ var LifecycleController = class LifecycleController {
|
|
|
6886
7705
|
ui.setLayoutRoot(layoutRoot);
|
|
6887
7706
|
this.host.state.layoutRoot = layoutRoot;
|
|
6888
7707
|
}
|
|
7708
|
+
/** Build the sidebar data snapshot (git + session stats + goal) read by the
|
|
7709
|
+
* stacked panels. Panels re-read this every frame via getData(), so the
|
|
7710
|
+
* snapshot stays cheap: git is TTL-cached and stats are memoized. */
|
|
7711
|
+
buildSidebarData() {
|
|
7712
|
+
const appState = this.host.state.appState;
|
|
7713
|
+
return {
|
|
7714
|
+
git: this.readSidebarGit(appState.workDir),
|
|
7715
|
+
sessionStats: this.readSidebarSessionStats(appState),
|
|
7716
|
+
agents: this.host.sessionEventHandler.getSubagentSlots(),
|
|
7717
|
+
goal: this.readSidebarGoal(appState)
|
|
7718
|
+
};
|
|
7719
|
+
}
|
|
7720
|
+
readSidebarGit(workDir) {
|
|
7721
|
+
if (this.sidebarGitCache === void 0 || this.sidebarGitCacheWorkDir !== workDir) {
|
|
7722
|
+
this.sidebarGitCache = createGitStatusCache(workDir);
|
|
7723
|
+
this.sidebarGitCacheWorkDir = workDir;
|
|
7724
|
+
}
|
|
7725
|
+
const status = this.sidebarGitCache.getStatus();
|
|
7726
|
+
if (status === null) return void 0;
|
|
7727
|
+
return {
|
|
7728
|
+
workDir,
|
|
7729
|
+
diffAdded: status.diffAdded,
|
|
7730
|
+
diffDeleted: status.diffDeleted,
|
|
7731
|
+
filesCount: status.files.length
|
|
7732
|
+
};
|
|
7733
|
+
}
|
|
7734
|
+
readSidebarSessionStats(appState) {
|
|
7735
|
+
if (this.sidebarSessionId !== appState.sessionId) {
|
|
7736
|
+
this.sidebarSessionId = appState.sessionId;
|
|
7737
|
+
this.sidebarSessionStartedAt = Date.now();
|
|
7738
|
+
}
|
|
7739
|
+
const entries = this.host.state.transcriptEntries;
|
|
7740
|
+
const usage = appState.sessionUsage;
|
|
7741
|
+
const subagentUsage = appState.subagentUsage;
|
|
7742
|
+
const memo = this.sidebarStatsMemo;
|
|
7743
|
+
if (memo !== void 0 && memo.length === entries.length && memo.compactions === appState.autoCompactionCount && memo.usage === usage && memo.subagentUsage === subagentUsage) return memo.stats;
|
|
7744
|
+
let turns = 0;
|
|
7745
|
+
let toolCalls = 0;
|
|
7746
|
+
for (const entry of entries) {
|
|
7747
|
+
if (entry.kind === "user") turns += 1;
|
|
7748
|
+
if (entry.kind === "tool_call" && entry.compactionData === void 0) toolCalls += 1;
|
|
7749
|
+
}
|
|
7750
|
+
let subagentOutput = 0;
|
|
7751
|
+
for (const sub of Object.values(subagentUsage)) subagentOutput += sub.output;
|
|
7752
|
+
const stats = {
|
|
7753
|
+
turns,
|
|
7754
|
+
toolCalls,
|
|
7755
|
+
messages: entries.length,
|
|
7756
|
+
compactions: appState.autoCompactionCount,
|
|
7757
|
+
tokensTotal: LifecycleController.sumTokens(usage) + subagentOutput,
|
|
7758
|
+
tokensInputCacheHit: usage.inputCacheRead,
|
|
7759
|
+
tokensInputCacheMiss: usage.inputOther + usage.inputCacheCreation,
|
|
7760
|
+
tokensOutput: usage.output + subagentOutput,
|
|
7761
|
+
startedAt: this.sidebarSessionStartedAt
|
|
7762
|
+
};
|
|
7763
|
+
this.sidebarStatsMemo = {
|
|
7764
|
+
length: entries.length,
|
|
7765
|
+
compactions: appState.autoCompactionCount,
|
|
7766
|
+
usage,
|
|
7767
|
+
subagentUsage,
|
|
7768
|
+
stats
|
|
7769
|
+
};
|
|
7770
|
+
return stats;
|
|
7771
|
+
}
|
|
7772
|
+
readSidebarGoal(appState) {
|
|
7773
|
+
if (appState.goal === null) return void 0;
|
|
7774
|
+
return {
|
|
7775
|
+
objective: appState.goal.objective,
|
|
7776
|
+
status: appState.goal.status,
|
|
7777
|
+
turnsUsed: appState.goal.turnsUsed,
|
|
7778
|
+
wallClockMs: appState.goal.wallClockMs,
|
|
7779
|
+
wallClockBaseAt: appState.goal.wallClockBaseAt,
|
|
7780
|
+
continuationCount: appState.goalContinuationCount,
|
|
7781
|
+
completionCriterion: appState.goal.completionCriterion,
|
|
7782
|
+
tokensUsed: appState.goal.tokensUsed,
|
|
7783
|
+
inputTokens: appState.goal.inputTokens,
|
|
7784
|
+
outputTokens: appState.goal.outputTokens,
|
|
7785
|
+
judge: appState.goalJudge
|
|
7786
|
+
};
|
|
7787
|
+
}
|
|
6889
7788
|
mountFooter() {
|
|
6890
7789
|
this.ensureFooterWrap();
|
|
6891
7790
|
this.footerWrap.addChild(this.host.state.footer);
|
|
@@ -7944,12 +8843,6 @@ var QuestionController = class extends ReverseRpcController {
|
|
|
7944
8843
|
}
|
|
7945
8844
|
};
|
|
7946
8845
|
//#endregion
|
|
7947
|
-
//#region src/tui/types.ts
|
|
7948
|
-
const INITIAL_LIVE_PANE = {
|
|
7949
|
-
pendingApproval: null,
|
|
7950
|
-
pendingQuestion: null
|
|
7951
|
-
};
|
|
7952
|
-
//#endregion
|
|
7953
8846
|
//#region src/utils/clipboard/clipboard-text.ts
|
|
7954
8847
|
/**
|
|
7955
8848
|
* Write plain text to the system clipboard with platform-specific commands.
|
|
@@ -8104,6 +8997,222 @@ function createRenderBatcher(doRender) {
|
|
|
8104
8997
|
};
|
|
8105
8998
|
}
|
|
8106
8999
|
//#endregion
|
|
9000
|
+
//#region src/tui/components/sidebar/sidebar-container.ts
|
|
9001
|
+
const SIDE_PADDING = 1;
|
|
9002
|
+
/**
|
|
9003
|
+
* Renders the stacked sidebar: every visible registered panel gets its own
|
|
9004
|
+
* full box frame (┌─┐│└┘) with the panel title embedded in the top frame, one
|
|
9005
|
+
* below the other. Panels render live each frame (their body reads the
|
|
9006
|
+
* current data snapshot), so time-based values (uptime, goal wall-clock)
|
|
9007
|
+
* tick without rebuilding. The focused panel's title is bolded; focus is
|
|
9008
|
+
* moved by the SidebarManager (`/sidebar next/prev`, Ctrl+X toggles).
|
|
9009
|
+
* The whole frame is painted in the theme accent colour and the rendered
|
|
9010
|
+
* width follows the SidebarManager so `/sidebar width` takes effect.
|
|
9011
|
+
*/
|
|
9012
|
+
var SidebarContainer = class extends Container {
|
|
9013
|
+
manager;
|
|
9014
|
+
panelContext;
|
|
9015
|
+
accentHex;
|
|
9016
|
+
/** Built panel bodies, keyed by panel id. Bodies render live so they are
|
|
9017
|
+
* only rebuilt when the visible panel set changes. */
|
|
9018
|
+
bodies = /* @__PURE__ */ new Map();
|
|
9019
|
+
builtStackKey = "";
|
|
9020
|
+
/** 1s heartbeat while the sidebar is open: keeps live values (uptime,
|
|
9021
|
+
* goal wall-clock, gradient phase of busy agent slots) ticking without
|
|
9022
|
+
* needing an event. Stopped when the sidebar closes. */
|
|
9023
|
+
heartbeat;
|
|
9024
|
+
constructor(manager, requestRender, colors) {
|
|
9025
|
+
super();
|
|
9026
|
+
this.manager = manager;
|
|
9027
|
+
this.accentHex = colors.primary;
|
|
9028
|
+
this.panelContext = {
|
|
9029
|
+
requestRender,
|
|
9030
|
+
getData: () => this.manager.getData(),
|
|
9031
|
+
colors
|
|
9032
|
+
};
|
|
9033
|
+
this.heartbeat = setInterval(() => {
|
|
9034
|
+
if (this.manager.isOpen) requestRender();
|
|
9035
|
+
}, 1e3);
|
|
9036
|
+
this.heartbeat.unref();
|
|
9037
|
+
}
|
|
9038
|
+
/** Stop the heartbeat. Called by the TUI teardown (see scream-tui.ts). */
|
|
9039
|
+
dispose() {
|
|
9040
|
+
if (this.heartbeat !== void 0) clearInterval(this.heartbeat);
|
|
9041
|
+
}
|
|
9042
|
+
/** Re-sync built bodies with the current visible stack (call on register/
|
|
9043
|
+
* unregister/activate changes; cheap idempotent check). */
|
|
9044
|
+
refresh() {
|
|
9045
|
+
const key = this.manager.isOpen ? this.manager.getStackPanels().map((panel) => panel.id).join("|") : "";
|
|
9046
|
+
if (key === this.builtStackKey) return;
|
|
9047
|
+
const keep = /* @__PURE__ */ new Set();
|
|
9048
|
+
if (this.manager.isOpen) for (const panel of this.manager.getStackPanels()) {
|
|
9049
|
+
keep.add(panel.id);
|
|
9050
|
+
if (!this.bodies.has(panel.id)) this.bodies.set(panel.id, panel.build(this.panelContext));
|
|
9051
|
+
}
|
|
9052
|
+
for (const id of [...this.bodies.keys()]) if (!keep.has(id)) this.bodies.delete(id);
|
|
9053
|
+
this.builtStackKey = key;
|
|
9054
|
+
this.clear();
|
|
9055
|
+
}
|
|
9056
|
+
render(_width) {
|
|
9057
|
+
if (!this.manager.isOpen) return [];
|
|
9058
|
+
this.refresh();
|
|
9059
|
+
const panels = this.manager.getStackPanels();
|
|
9060
|
+
if (panels.length === 0) return [];
|
|
9061
|
+
const lines = [""];
|
|
9062
|
+
const horzLen = Math.max(2, this.manager.currentWidth - 2);
|
|
9063
|
+
const contentWidth = Math.max(1, horzLen - 2 * SIDE_PADDING);
|
|
9064
|
+
const paint = (s) => chalk.hex(this.accentHex)(s);
|
|
9065
|
+
const activeId = this.manager.activePanel?.id ?? null;
|
|
9066
|
+
for (const panel of panels) {
|
|
9067
|
+
const body = this.bodies.get(panel.id);
|
|
9068
|
+
if (body === void 0) continue;
|
|
9069
|
+
const title = truncateToWidth(String(panel.title), horzLen);
|
|
9070
|
+
const titleText = panel.id === activeId ? chalk.bold(title) : title;
|
|
9071
|
+
const trailing = Math.max(0, horzLen - visibleWidth(title));
|
|
9072
|
+
lines.push(paint("┌") + paint(titleText) + paint("─".repeat(trailing)) + paint("┐"));
|
|
9073
|
+
for (const raw of body.render(contentWidth)) {
|
|
9074
|
+
const row = truncateToWidth(raw, contentWidth);
|
|
9075
|
+
const pad = Math.max(0, contentWidth - visibleWidth(row));
|
|
9076
|
+
lines.push(paint("│") + " " + row + " ".repeat(pad) + " " + paint("│"));
|
|
9077
|
+
}
|
|
9078
|
+
lines.push(paint("└") + paint("─".repeat(horzLen)) + paint("┘"));
|
|
9079
|
+
}
|
|
9080
|
+
return lines;
|
|
9081
|
+
}
|
|
9082
|
+
};
|
|
9083
|
+
//#endregion
|
|
9084
|
+
//#region src/tui/components/sidebar/sidebar-manager.ts
|
|
9085
|
+
/**
|
|
9086
|
+
* Owns which sidebar panels are registered, which one is active, whether the
|
|
9087
|
+
* sidebar is open, and its width. Rendering is delegated back to the host via
|
|
9088
|
+
* the injected `requestRender`; the manager itself holds no Component state.
|
|
9089
|
+
*/
|
|
9090
|
+
var SidebarManager = class {
|
|
9091
|
+
panels = [];
|
|
9092
|
+
byId = /* @__PURE__ */ new Map();
|
|
9093
|
+
activeId = null;
|
|
9094
|
+
open = false;
|
|
9095
|
+
width = 30;
|
|
9096
|
+
widthStore;
|
|
9097
|
+
dataProvider;
|
|
9098
|
+
requestRender;
|
|
9099
|
+
constructor(requestRender, opts) {
|
|
9100
|
+
this.requestRender = requestRender;
|
|
9101
|
+
this.widthStore = opts?.widthStore;
|
|
9102
|
+
this.dataProvider = opts?.dataProvider;
|
|
9103
|
+
if (opts?.widthStore !== void 0) this.width = clampSidebarWidth(opts.widthStore.load());
|
|
9104
|
+
}
|
|
9105
|
+
getData() {
|
|
9106
|
+
return this.dataProvider?.() ?? {};
|
|
9107
|
+
}
|
|
9108
|
+
setDataProvider(provider) {
|
|
9109
|
+
this.dataProvider = provider;
|
|
9110
|
+
}
|
|
9111
|
+
register(panel) {
|
|
9112
|
+
if (this.byId.has(panel.id)) return;
|
|
9113
|
+
this.panels.push(panel);
|
|
9114
|
+
this.byId.set(panel.id, panel);
|
|
9115
|
+
}
|
|
9116
|
+
unregister(id) {
|
|
9117
|
+
const idx = this.panels.findIndex((p) => p.id === id);
|
|
9118
|
+
if (idx === -1) return;
|
|
9119
|
+
const [panel] = this.panels.splice(idx, 1);
|
|
9120
|
+
this.byId.delete(id);
|
|
9121
|
+
if (this.activeId === id) {
|
|
9122
|
+
this.activeId = null;
|
|
9123
|
+
this.open = false;
|
|
9124
|
+
panel?.onClose?.();
|
|
9125
|
+
}
|
|
9126
|
+
this.requestRender();
|
|
9127
|
+
}
|
|
9128
|
+
get isOpen() {
|
|
9129
|
+
return this.open;
|
|
9130
|
+
}
|
|
9131
|
+
get hasPanels() {
|
|
9132
|
+
return this.panels.length > 0;
|
|
9133
|
+
}
|
|
9134
|
+
get activePanel() {
|
|
9135
|
+
return this.activeId === null ? void 0 : this.byId.get(this.activeId);
|
|
9136
|
+
}
|
|
9137
|
+
get currentWidth() {
|
|
9138
|
+
return this.width;
|
|
9139
|
+
}
|
|
9140
|
+
get allPanels() {
|
|
9141
|
+
return this.panels;
|
|
9142
|
+
}
|
|
9143
|
+
/**
|
|
9144
|
+
* Panels that should render in the current stacked sidebar, in registration
|
|
9145
|
+
* order, filtered by each panel's `visible(data)` predicate (panels without
|
|
9146
|
+
* one are always visible).
|
|
9147
|
+
*/
|
|
9148
|
+
getStackPanels() {
|
|
9149
|
+
const data = this.getData();
|
|
9150
|
+
return this.panels.filter((panel) => panel.visible?.(data) ?? true);
|
|
9151
|
+
}
|
|
9152
|
+
/** True when `panel` would currently render in the stack. */
|
|
9153
|
+
isStacked(panel) {
|
|
9154
|
+
const data = this.getData();
|
|
9155
|
+
return panel.visible?.(data) ?? true;
|
|
9156
|
+
}
|
|
9157
|
+
activate(id) {
|
|
9158
|
+
const panel = this.byId.get(id);
|
|
9159
|
+
if (panel === void 0) return false;
|
|
9160
|
+
if (this.open && this.activeId === id) return true;
|
|
9161
|
+
(this.activeId === null ? void 0 : this.byId.get(this.activeId))?.onClose?.();
|
|
9162
|
+
this.activeId = id;
|
|
9163
|
+
this.open = true;
|
|
9164
|
+
panel.onOpen?.();
|
|
9165
|
+
this.requestRender();
|
|
9166
|
+
return true;
|
|
9167
|
+
}
|
|
9168
|
+
toggle(id) {
|
|
9169
|
+
if (id === void 0 && this.activeId === null) {
|
|
9170
|
+
const [first] = this.getStackPanels();
|
|
9171
|
+
if (first === void 0) return;
|
|
9172
|
+
this.activate(first.id);
|
|
9173
|
+
return;
|
|
9174
|
+
}
|
|
9175
|
+
const target = id ?? this.activeId;
|
|
9176
|
+
if (target === null || !this.byId.has(target)) return;
|
|
9177
|
+
if (this.open && this.activeId === target) {
|
|
9178
|
+
this.close();
|
|
9179
|
+
return;
|
|
9180
|
+
}
|
|
9181
|
+
this.activate(target);
|
|
9182
|
+
}
|
|
9183
|
+
close() {
|
|
9184
|
+
if (!this.open) return;
|
|
9185
|
+
const panel = this.activePanel;
|
|
9186
|
+
this.open = false;
|
|
9187
|
+
this.activeId = null;
|
|
9188
|
+
panel?.onClose?.();
|
|
9189
|
+
this.requestRender();
|
|
9190
|
+
}
|
|
9191
|
+
next() {
|
|
9192
|
+
this.step(1);
|
|
9193
|
+
}
|
|
9194
|
+
prev() {
|
|
9195
|
+
this.step(-1);
|
|
9196
|
+
}
|
|
9197
|
+
step(dir) {
|
|
9198
|
+
const stack = this.getStackPanels();
|
|
9199
|
+
if (stack.length === 0) return;
|
|
9200
|
+
const idx = this.activeId === null ? -1 : stack.findIndex((p) => p.id === this.activeId);
|
|
9201
|
+
const nextIdx = idx === -1 ? dir === 1 ? 0 : stack.length - 1 : (idx + dir + stack.length) % stack.length;
|
|
9202
|
+
this.activate(stack[nextIdx].id);
|
|
9203
|
+
}
|
|
9204
|
+
setWidth(cols) {
|
|
9205
|
+
this.width = clampSidebarWidth(cols);
|
|
9206
|
+
this.widthStore?.save(this.width);
|
|
9207
|
+
this.requestRender();
|
|
9208
|
+
}
|
|
9209
|
+
resetWidth() {
|
|
9210
|
+
this.width = 30;
|
|
9211
|
+
this.widthStore?.save(this.width);
|
|
9212
|
+
this.requestRender();
|
|
9213
|
+
}
|
|
9214
|
+
};
|
|
9215
|
+
//#endregion
|
|
8107
9216
|
//#region src/tui/components/chrome/error-banner.ts
|
|
8108
9217
|
const MAX_BANNER_LINES = 3;
|
|
8109
9218
|
const CONTINUATION_INDENT = " ";
|
|
@@ -8426,6 +9535,7 @@ var CustomEditor = class extends Editor {
|
|
|
8426
9535
|
onCtrlB;
|
|
8427
9536
|
onToggleToolExpand;
|
|
8428
9537
|
onTogglePlanExpand;
|
|
9538
|
+
onToggleSidebar;
|
|
8429
9539
|
onOpenExternalEditor;
|
|
8430
9540
|
onCtrlS;
|
|
8431
9541
|
onCtrlW;
|
|
@@ -8605,6 +9715,10 @@ var CustomEditor = class extends Editor {
|
|
|
8605
9715
|
if (matchesKey(normalized, Key.ctrl("e"))) {
|
|
8606
9716
|
if (this.onTogglePlanExpand?.() === true) return;
|
|
8607
9717
|
}
|
|
9718
|
+
if (matchesKey(normalized, Key.ctrl("x"))) {
|
|
9719
|
+
this.onToggleSidebar?.();
|
|
9720
|
+
return;
|
|
9721
|
+
}
|
|
8608
9722
|
if (matchesKey(normalized, Key.ctrl("s"))) {
|
|
8609
9723
|
this.onCtrlS?.();
|
|
8610
9724
|
return;
|
|
@@ -8931,7 +10045,7 @@ function createTUIState(options) {
|
|
|
8931
10045
|
const base64 = Buffer.from(text, "utf8").toString("base64");
|
|
8932
10046
|
if (base64.length > OSC52_MAX_TEXT_LENGTH) return false;
|
|
8933
10047
|
try {
|
|
8934
|
-
terminal.write(`\
|
|
10048
|
+
terminal.write(`\u001B]52;c;${base64}\u0007`);
|
|
8935
10049
|
} catch {
|
|
8936
10050
|
return false;
|
|
8937
10051
|
}
|
|
@@ -8979,10 +10093,19 @@ function createTUIState(options) {
|
|
|
8979
10093
|
editor.thinkingLevel = initialAppState.thinkingLevel;
|
|
8980
10094
|
editor.permissionMode = initialAppState.permissionMode ?? "manual";
|
|
8981
10095
|
editor.toolPriority = initialAppState.like?.toolPriority;
|
|
10096
|
+
const footer = new FooterComponent({ ...initialAppState }, theme.colors, ui);
|
|
10097
|
+
const sidebarManager = new SidebarManager(() => ui.requestRender());
|
|
10098
|
+
const sidebarContainer = new SidebarContainer(sidebarManager, () => ui.requestRender(), theme.colors);
|
|
10099
|
+
const sidebarPane = new GutterContainer(1, 1);
|
|
10100
|
+
sidebarPane.addChild(sidebarContainer);
|
|
10101
|
+
editor.onToggleSidebar = () => sidebarManager.toggle();
|
|
8982
10102
|
return {
|
|
8983
10103
|
ui,
|
|
8984
10104
|
terminal,
|
|
8985
10105
|
layoutRoot: void 0,
|
|
10106
|
+
sidebarManager,
|
|
10107
|
+
sidebarContainer,
|
|
10108
|
+
sidebarPane,
|
|
8986
10109
|
transcriptContainer,
|
|
8987
10110
|
activityContainer,
|
|
8988
10111
|
statusBarContainer,
|
|
@@ -8994,9 +10117,7 @@ function createTUIState(options) {
|
|
|
8994
10117
|
planModeBanner,
|
|
8995
10118
|
planModeBannerContainer,
|
|
8996
10119
|
editorContainer,
|
|
8997
|
-
footer
|
|
8998
|
-
ui.requestRender();
|
|
8999
|
-
}),
|
|
10120
|
+
footer,
|
|
9000
10121
|
editor,
|
|
9001
10122
|
theme,
|
|
9002
10123
|
appState: { ...initialAppState },
|
|
@@ -9582,11 +10703,17 @@ var SessionManager$1 = class {
|
|
|
9582
10703
|
sessionTitle: session.summary?.title ?? null,
|
|
9583
10704
|
goal: goal ? {
|
|
9584
10705
|
objective: goal.objective,
|
|
10706
|
+
status: normalizeGoalStatus(goal.status),
|
|
9585
10707
|
turnsUsed: goal.turnsUsed ?? 0,
|
|
9586
10708
|
wallClockMs: goal.wallClockMs ?? 0,
|
|
9587
|
-
wallClockBaseAt: Date.now()
|
|
10709
|
+
wallClockBaseAt: Date.now(),
|
|
10710
|
+
completionCriterion: goal.completionCriterion ?? null,
|
|
10711
|
+
tokensUsed: goal.tokensUsed ?? 0,
|
|
10712
|
+
inputTokens: goal.inputTokens ?? null,
|
|
10713
|
+
outputTokens: goal.outputTokens ?? null
|
|
9588
10714
|
} : null,
|
|
9589
10715
|
goalActive: goal?.status === "active",
|
|
10716
|
+
goalJudge: "awaiting",
|
|
9590
10717
|
goalContinuationCount: 0,
|
|
9591
10718
|
providerBalance: null
|
|
9592
10719
|
});
|
|
@@ -11943,6 +13070,7 @@ function createInitialAppState(input) {
|
|
|
11943
13070
|
sessionTitle: null,
|
|
11944
13071
|
goal: null,
|
|
11945
13072
|
goalActive: false,
|
|
13073
|
+
goalJudge: "awaiting",
|
|
11946
13074
|
goalContinuationCount: 0,
|
|
11947
13075
|
ccConnectActive: false,
|
|
11948
13076
|
wolfpackMode: input.cliOptions.wolfpack === true,
|
|
@@ -12173,6 +13301,7 @@ var ScreamTUI = class {
|
|
|
12173
13301
|
this.lifecycleController.disposeTerminalTracking();
|
|
12174
13302
|
this.inputController.dispose();
|
|
12175
13303
|
this.state.footer.dispose();
|
|
13304
|
+
this.state.sidebarContainer.dispose();
|
|
12176
13305
|
await this.closeSession();
|
|
12177
13306
|
await this.harness.close();
|
|
12178
13307
|
this.sessionEventHandler.stopAllMcpServerStatusSpinners();
|
|
@@ -12443,7 +13572,7 @@ var ScreamTUI = class {
|
|
|
12443
13572
|
const card = new OnboardingCardComponent({
|
|
12444
13573
|
colors: this.state.theme.colors,
|
|
12445
13574
|
onConfigure: () => {
|
|
12446
|
-
import("./dispatch-
|
|
13575
|
+
import("./dispatch-CkPcMk4b.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
12447
13576
|
this.restoreEditor();
|
|
12448
13577
|
onDone?.();
|
|
12449
13578
|
});
|
|
@@ -15005,6 +16134,12 @@ var WebSession = class {
|
|
|
15005
16134
|
unsubscribe = null;
|
|
15006
16135
|
busy = false;
|
|
15007
16136
|
/**
|
|
16137
|
+
* In-flight turn count across all agents (main + subagents). Used by the
|
|
16138
|
+
* idle-exit watchdog: a single boolean is not enough because a subagent's
|
|
16139
|
+
* `turn.ended` must not clear the busy state of a still-running main turn.
|
|
16140
|
+
*/
|
|
16141
|
+
activeTurns = 0;
|
|
16142
|
+
/**
|
|
15008
16143
|
* Live assistant message under construction. Mirrors the same state machine
|
|
15009
16144
|
* used by buildMessages() so turn.ended can persist a complete durable
|
|
15010
16145
|
* snapshot (`web.message.finalized`) — the only source that survives a
|
|
@@ -15044,6 +16179,10 @@ var WebSession = class {
|
|
|
15044
16179
|
get isBusy() {
|
|
15045
16180
|
return this.busy;
|
|
15046
16181
|
}
|
|
16182
|
+
/** True while any agent turn (main or subagent) is in flight. */
|
|
16183
|
+
get isTurnActive() {
|
|
16184
|
+
return this.activeTurns > 0;
|
|
16185
|
+
}
|
|
15047
16186
|
getTitle() {
|
|
15048
16187
|
return this.title;
|
|
15049
16188
|
}
|
|
@@ -15173,6 +16312,7 @@ var WebSession = class {
|
|
|
15173
16312
|
if ((event.type === "goal.updated" || event.type === "todo.updated") && event.agentId !== "main") return;
|
|
15174
16313
|
if (event.type === "turn.started") {
|
|
15175
16314
|
this.busy = true;
|
|
16315
|
+
this.activeTurns += 1;
|
|
15176
16316
|
this.liveTurnCount += 1;
|
|
15177
16317
|
this.liveAssistant = {
|
|
15178
16318
|
role: "assistant",
|
|
@@ -15192,7 +16332,8 @@ var WebSession = class {
|
|
|
15192
16332
|
}
|
|
15193
16333
|
};
|
|
15194
16334
|
} else if (event.type === "turn.ended") {
|
|
15195
|
-
this.
|
|
16335
|
+
this.activeTurns = Math.max(0, this.activeTurns - 1);
|
|
16336
|
+
this.busy = this.activeTurns > 0;
|
|
15196
16337
|
const live = this.liveAssistant;
|
|
15197
16338
|
if (event.reason === "failed" && live) live.isError = true;
|
|
15198
16339
|
if (live) {
|
|
@@ -15410,6 +16551,7 @@ var WebSession = class {
|
|
|
15410
16551
|
}, false);
|
|
15411
16552
|
this.session.prompt(text).catch((error) => {
|
|
15412
16553
|
this.busy = false;
|
|
16554
|
+
this.activeTurns = 0;
|
|
15413
16555
|
this.sendError(ws, error, clientMessageId);
|
|
15414
16556
|
});
|
|
15415
16557
|
break;
|
|
@@ -15890,6 +17032,7 @@ var WebSession = class {
|
|
|
15890
17032
|
await session.prompt(prompt);
|
|
15891
17033
|
} catch (error) {
|
|
15892
17034
|
this.busy = false;
|
|
17035
|
+
this.activeTurns = 0;
|
|
15893
17036
|
throw error;
|
|
15894
17037
|
}
|
|
15895
17038
|
}
|
|
@@ -16269,6 +17412,10 @@ var SessionManager = class {
|
|
|
16269
17412
|
list() {
|
|
16270
17413
|
return Array.from(this.sessions.values()).map((s) => s.getListItem()).toSorted((a, b) => b.createdAt - a.createdAt);
|
|
16271
17414
|
}
|
|
17415
|
+
/** True when any session has an agent turn (main or subagent) in flight (used to defer idle exit). */
|
|
17416
|
+
anyBusy() {
|
|
17417
|
+
return Array.from(this.sessions.values()).some((s) => s.isTurnActive);
|
|
17418
|
+
}
|
|
16272
17419
|
async delete(sessionId) {
|
|
16273
17420
|
const ws = this.sessions.get(sessionId);
|
|
16274
17421
|
if (!ws) return false;
|
|
@@ -16483,6 +17630,48 @@ var SessionManager = class {
|
|
|
16483
17630
|
return this.harness.preflight();
|
|
16484
17631
|
}
|
|
16485
17632
|
};
|
|
17633
|
+
/**
|
|
17634
|
+
* Watchdog shared by both web entry points. Detects "nobody is using the UI"
|
|
17635
|
+
* as: zero WebSocket connections AND no HTTP request for `idleMs`, with an
|
|
17636
|
+
* in-flight agent turn (`busy()`) deferring the exit so background work is
|
|
17637
|
+
* never interrupted. The `shutdown` call is awaited before `exit` and must be
|
|
17638
|
+
* idempotent (it can race with a manual SIGINT/SIGTERM stop).
|
|
17639
|
+
*/
|
|
17640
|
+
function startWebIdleExit(opts) {
|
|
17641
|
+
const idleMs = opts.idleMs;
|
|
17642
|
+
if (!(idleMs > 0)) return {
|
|
17643
|
+
touch: () => {},
|
|
17644
|
+
noteConnection: () => {},
|
|
17645
|
+
noteDisconnect: () => {},
|
|
17646
|
+
stop: () => {}
|
|
17647
|
+
};
|
|
17648
|
+
const exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
17649
|
+
let activeConnections = 0;
|
|
17650
|
+
let lastActivityAt = Date.now();
|
|
17651
|
+
const checkMs = Math.min(6e4, Math.max(100, idleMs / 2));
|
|
17652
|
+
const timer = setInterval(() => {
|
|
17653
|
+
if (activeConnections > 0) return;
|
|
17654
|
+
if (opts.busy()) return;
|
|
17655
|
+
if (Date.now() - lastActivityAt < idleMs) return;
|
|
17656
|
+
Promise.resolve(opts.shutdown()).finally(() => exitFn(0));
|
|
17657
|
+
}, checkMs);
|
|
17658
|
+
return {
|
|
17659
|
+
touch: () => {
|
|
17660
|
+
lastActivityAt = Date.now();
|
|
17661
|
+
},
|
|
17662
|
+
noteConnection: () => {
|
|
17663
|
+
activeConnections += 1;
|
|
17664
|
+
lastActivityAt = Date.now();
|
|
17665
|
+
},
|
|
17666
|
+
noteDisconnect: () => {
|
|
17667
|
+
activeConnections = Math.max(0, activeConnections - 1);
|
|
17668
|
+
lastActivityAt = Date.now();
|
|
17669
|
+
},
|
|
17670
|
+
stop: () => {
|
|
17671
|
+
clearInterval(timer);
|
|
17672
|
+
}
|
|
17673
|
+
};
|
|
17674
|
+
}
|
|
16486
17675
|
async function handleSessionControlRoutes(req, res, url, method, manager) {
|
|
16487
17676
|
const statusMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/status$`).exec(url);
|
|
16488
17677
|
if (statusMatch && method === "GET") {
|
|
@@ -16938,9 +18127,15 @@ async function runWebServer(opts) {
|
|
|
16938
18127
|
publicDir = sourcePublicDir;
|
|
16939
18128
|
} catch {}
|
|
16940
18129
|
}
|
|
18130
|
+
const idleExit = startWebIdleExit({
|
|
18131
|
+
idleMs: (opts.idleMinutes ?? 15) * 6e4,
|
|
18132
|
+
busy: () => manager.anyBusy(),
|
|
18133
|
+
shutdown: () => cleanup()
|
|
18134
|
+
});
|
|
16941
18135
|
const httpServer = createServer(async (req, res) => {
|
|
16942
18136
|
const url = req.url ?? "/";
|
|
16943
18137
|
const method = req.method ?? "GET";
|
|
18138
|
+
idleExit.touch();
|
|
16944
18139
|
if (gateway) {
|
|
16945
18140
|
const verdict = gatewayVerdict({
|
|
16946
18141
|
loopback: isLoopbackAddress(req.socket.remoteAddress),
|
|
@@ -17295,7 +18490,12 @@ async function runWebServer(opts) {
|
|
|
17295
18490
|
}
|
|
17296
18491
|
});
|
|
17297
18492
|
const wss = new WebSocketServer({ server: httpServer });
|
|
18493
|
+
wss.on("error", () => {});
|
|
17298
18494
|
wss.on("connection", (ws, req) => {
|
|
18495
|
+
idleExit.noteConnection();
|
|
18496
|
+
ws.once("close", () => {
|
|
18497
|
+
idleExit.noteDisconnect();
|
|
18498
|
+
});
|
|
17299
18499
|
if (gateway && !isLoopbackAddress(req.socket.remoteAddress) && !gateway.verifySession(req.headers.cookie)) {
|
|
17300
18500
|
ws.close(1008, "unauthorized");
|
|
17301
18501
|
return;
|
|
@@ -17346,13 +18546,18 @@ async function runWebServer(opts) {
|
|
|
17346
18546
|
}
|
|
17347
18547
|
ws.send(JSON.stringify({ type: "server_empty" }));
|
|
17348
18548
|
});
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
|
|
17352
|
-
|
|
18549
|
+
try {
|
|
18550
|
+
await new Promise((resolve, reject) => {
|
|
18551
|
+
httpServer.once("error", (err) => {
|
|
18552
|
+
if (err.code === "EADDRINUSE") reject(/* @__PURE__ */ new Error(`端口 ${opts.port} 已被占用,请先关闭占用该端口的进程,或使用 --port <port> 指定其他端口。`));
|
|
18553
|
+
else reject(err);
|
|
18554
|
+
});
|
|
18555
|
+
httpServer.listen(opts.port, host, resolve);
|
|
17353
18556
|
});
|
|
17354
|
-
|
|
17355
|
-
|
|
18557
|
+
} catch (error) {
|
|
18558
|
+
idleExit.stop();
|
|
18559
|
+
throw error;
|
|
18560
|
+
}
|
|
17356
18561
|
const address = httpServer.address();
|
|
17357
18562
|
const realPort = typeof address === "object" && address !== null ? address.port : opts.port;
|
|
17358
18563
|
const url = `http://localhost:${realPort}`;
|
|
@@ -17361,6 +18566,7 @@ async function runWebServer(opts) {
|
|
|
17361
18566
|
const cleanup = async () => {
|
|
17362
18567
|
if (cleaned) return;
|
|
17363
18568
|
cleaned = true;
|
|
18569
|
+
idleExit.stop();
|
|
17364
18570
|
try {
|
|
17365
18571
|
wss.close();
|
|
17366
18572
|
await manager.closeAll();
|
|
@@ -17376,7 +18582,8 @@ async function runWebServer(opts) {
|
|
|
17376
18582
|
process.on("SIGTERM", () => {
|
|
17377
18583
|
cleanup().finally(() => process.exit(0));
|
|
17378
18584
|
});
|
|
17379
|
-
|
|
18585
|
+
const idleMinutes = opts.idleMinutes ?? 15;
|
|
18586
|
+
let banner = `\n Scream Web UI ready: ${url}\n Working directory: ${workDir}\n Sessions: ${manager.list().length} (${manager.list().filter((s) => s.active).length} active)\n Permission: ${opts.yolo ? "yolo" : opts.auto ? "auto" : "manual"}\n` + (idleMinutes > 0 ? ` Idle exit: ${idleMinutes}m without a browser connection (--idle-minutes <n> to change, 0 = disable)\n` : ` Idle exit: disabled (--idle-minutes 0)\n`);
|
|
17380
18587
|
if (gateway) {
|
|
17381
18588
|
const addrs = getLanAddresses();
|
|
17382
18589
|
banner += ` LAN mode: on — non-local devices must present the access key\n`;
|
|
@@ -17448,7 +18655,8 @@ async function runWeb(opts) {
|
|
|
17448
18655
|
open: opts.open,
|
|
17449
18656
|
skillsDirs: opts.skillsDirs,
|
|
17450
18657
|
lan: opts.lan,
|
|
17451
|
-
token
|
|
18658
|
+
token,
|
|
18659
|
+
idleMinutes: opts.idleMinutes
|
|
17452
18660
|
});
|
|
17453
18661
|
}
|
|
17454
18662
|
//#endregion
|