scream-code 0.15.4 → 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 +3 -2
- package/dist/{app-DHA9hN93.mjs → app-Y2DCZTWP.mjs} +1252 -41
- package/dist/{dispatch-CULU8xVW.mjs → dispatch-CkPcMk4b.mjs} +1 -1
- package/dist/{dispatch-m6mYTI1M.mjs → dispatch-jQiqBKV2.mjs} +544 -450
- package/dist/main.mjs +1 -1
- package/dist/{text-input-dialog-CR_kzyR3.mjs → text-input-dialog-DJwYNHcs.mjs} +1 -1
- package/dist/{text-input-dialog-B2a6Ks8f.mjs → text-input-dialog-LOMBSusF.mjs} +89 -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);
|
|
@@ -3151,11 +3458,15 @@ var SessionEventHandler = class {
|
|
|
3151
3458
|
tc.onSubagentCompleted({
|
|
3152
3459
|
contextTokens: event.contextTokens,
|
|
3153
3460
|
usage: event.usage,
|
|
3154
|
-
resultSummary: event.resultSummary
|
|
3461
|
+
resultSummary: event.resultSummary,
|
|
3462
|
+
turns: event.turns,
|
|
3463
|
+
durationMs: event.durationMs,
|
|
3464
|
+
toolCallCount: event.toolCallCount
|
|
3155
3465
|
});
|
|
3156
3466
|
streamingUI.removeToolComponentIfInactive(event.parentToolCallId);
|
|
3157
3467
|
}
|
|
3158
3468
|
handleSubagentFailed(event) {
|
|
3469
|
+
this.subagentSlots.onTerminated(event.subagentId);
|
|
3159
3470
|
this.recordSubagentUsage(event.subagentId, void 0, event.usage);
|
|
3160
3471
|
const { streamingUI } = this.host;
|
|
3161
3472
|
const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
|
|
@@ -3181,13 +3492,21 @@ var SessionEventHandler = class {
|
|
|
3181
3492
|
tc.onSubagentFailed({ error: event.error });
|
|
3182
3493
|
streamingUI.removeToolComponentIfInactive(this.subagentInfo.get(event.subagentId)?.parentToolCallId ?? event.parentToolCallId);
|
|
3183
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();
|
|
3184
3499
|
recordSubagentUsage(subagentId, subagentName, usage) {
|
|
3185
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;
|
|
3186
3505
|
const name = this.subagentInfo.get(subagentId)?.name ?? subagentName ?? subagentId;
|
|
3187
3506
|
const current = this.host.state.appState.subagentUsage[name];
|
|
3188
3507
|
const next = {
|
|
3189
3508
|
...this.host.state.appState.subagentUsage,
|
|
3190
|
-
[name]: current === void 0 ?
|
|
3509
|
+
[name]: current === void 0 ? delta : addTokenUsage(current, delta)
|
|
3191
3510
|
};
|
|
3192
3511
|
this.host.setAppState({ subagentUsage: next });
|
|
3193
3512
|
}
|
|
@@ -5993,7 +6312,10 @@ var TranscriptController = class TranscriptController {
|
|
|
5993
6312
|
committedComponent;
|
|
5994
6313
|
liveComponentToEntry = /* @__PURE__ */ new Map();
|
|
5995
6314
|
pendingComponents = /* @__PURE__ */ new Set();
|
|
5996
|
-
|
|
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;
|
|
5997
6319
|
constructor(host) {
|
|
5998
6320
|
this.host = host;
|
|
5999
6321
|
}
|
|
@@ -6338,6 +6660,469 @@ var StatusBarPaneComponent = class extends Container {
|
|
|
6338
6660
|
}
|
|
6339
6661
|
}
|
|
6340
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
|
+
}
|
|
6341
7126
|
//#endregion
|
|
6342
7127
|
//#region src/tui/theme/bundle.ts
|
|
6343
7128
|
function createScreamTUIThemeBundle(theme, resolvedTheme) {
|
|
@@ -6683,8 +7468,23 @@ var LifecycleController = class LifecycleController {
|
|
|
6683
7468
|
/** The active status-bar loader (PulseWaveLoader or MoonLoader). Owned by
|
|
6684
7469
|
* this controller; stopped before replacement to avoid leaking timers. */
|
|
6685
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
|
+
}
|
|
6686
7475
|
tipRotationTimer;
|
|
6687
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;
|
|
6688
7488
|
static MEMORY_IDLE_MS = 900 * 1e3;
|
|
6689
7489
|
static MEMORY_COUNTDOWN_MS = 15 * 1e3;
|
|
6690
7490
|
static MEMORY_EXTRACT_COOLDOWN_MS = 600 * 1e3;
|
|
@@ -6867,8 +7667,30 @@ var LifecycleController = class LifecycleController {
|
|
|
6867
7667
|
minSize: 1
|
|
6868
7668
|
}
|
|
6869
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;
|
|
6870
7679
|
const layoutRoot = new VStack([{
|
|
6871
|
-
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
|
+
}]),
|
|
6872
7694
|
basis: 0,
|
|
6873
7695
|
grow: 1,
|
|
6874
7696
|
shrink: 1,
|
|
@@ -6883,6 +7705,86 @@ var LifecycleController = class LifecycleController {
|
|
|
6883
7705
|
ui.setLayoutRoot(layoutRoot);
|
|
6884
7706
|
this.host.state.layoutRoot = layoutRoot;
|
|
6885
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
|
+
}
|
|
6886
7788
|
mountFooter() {
|
|
6887
7789
|
this.ensureFooterWrap();
|
|
6888
7790
|
this.footerWrap.addChild(this.host.state.footer);
|
|
@@ -7941,12 +8843,6 @@ var QuestionController = class extends ReverseRpcController {
|
|
|
7941
8843
|
}
|
|
7942
8844
|
};
|
|
7943
8845
|
//#endregion
|
|
7944
|
-
//#region src/tui/types.ts
|
|
7945
|
-
const INITIAL_LIVE_PANE = {
|
|
7946
|
-
pendingApproval: null,
|
|
7947
|
-
pendingQuestion: null
|
|
7948
|
-
};
|
|
7949
|
-
//#endregion
|
|
7950
8846
|
//#region src/utils/clipboard/clipboard-text.ts
|
|
7951
8847
|
/**
|
|
7952
8848
|
* Write plain text to the system clipboard with platform-specific commands.
|
|
@@ -8101,6 +8997,222 @@ function createRenderBatcher(doRender) {
|
|
|
8101
8997
|
};
|
|
8102
8998
|
}
|
|
8103
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
|
|
8104
9216
|
//#region src/tui/components/chrome/error-banner.ts
|
|
8105
9217
|
const MAX_BANNER_LINES = 3;
|
|
8106
9218
|
const CONTINUATION_INDENT = " ";
|
|
@@ -8423,6 +9535,7 @@ var CustomEditor = class extends Editor {
|
|
|
8423
9535
|
onCtrlB;
|
|
8424
9536
|
onToggleToolExpand;
|
|
8425
9537
|
onTogglePlanExpand;
|
|
9538
|
+
onToggleSidebar;
|
|
8426
9539
|
onOpenExternalEditor;
|
|
8427
9540
|
onCtrlS;
|
|
8428
9541
|
onCtrlW;
|
|
@@ -8602,6 +9715,10 @@ var CustomEditor = class extends Editor {
|
|
|
8602
9715
|
if (matchesKey(normalized, Key.ctrl("e"))) {
|
|
8603
9716
|
if (this.onTogglePlanExpand?.() === true) return;
|
|
8604
9717
|
}
|
|
9718
|
+
if (matchesKey(normalized, Key.ctrl("x"))) {
|
|
9719
|
+
this.onToggleSidebar?.();
|
|
9720
|
+
return;
|
|
9721
|
+
}
|
|
8605
9722
|
if (matchesKey(normalized, Key.ctrl("s"))) {
|
|
8606
9723
|
this.onCtrlS?.();
|
|
8607
9724
|
return;
|
|
@@ -8928,7 +10045,7 @@ function createTUIState(options) {
|
|
|
8928
10045
|
const base64 = Buffer.from(text, "utf8").toString("base64");
|
|
8929
10046
|
if (base64.length > OSC52_MAX_TEXT_LENGTH) return false;
|
|
8930
10047
|
try {
|
|
8931
|
-
terminal.write(`\
|
|
10048
|
+
terminal.write(`\u001B]52;c;${base64}\u0007`);
|
|
8932
10049
|
} catch {
|
|
8933
10050
|
return false;
|
|
8934
10051
|
}
|
|
@@ -8976,10 +10093,19 @@ function createTUIState(options) {
|
|
|
8976
10093
|
editor.thinkingLevel = initialAppState.thinkingLevel;
|
|
8977
10094
|
editor.permissionMode = initialAppState.permissionMode ?? "manual";
|
|
8978
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();
|
|
8979
10102
|
return {
|
|
8980
10103
|
ui,
|
|
8981
10104
|
terminal,
|
|
8982
10105
|
layoutRoot: void 0,
|
|
10106
|
+
sidebarManager,
|
|
10107
|
+
sidebarContainer,
|
|
10108
|
+
sidebarPane,
|
|
8983
10109
|
transcriptContainer,
|
|
8984
10110
|
activityContainer,
|
|
8985
10111
|
statusBarContainer,
|
|
@@ -8991,9 +10117,7 @@ function createTUIState(options) {
|
|
|
8991
10117
|
planModeBanner,
|
|
8992
10118
|
planModeBannerContainer,
|
|
8993
10119
|
editorContainer,
|
|
8994
|
-
footer
|
|
8995
|
-
ui.requestRender();
|
|
8996
|
-
}),
|
|
10120
|
+
footer,
|
|
8997
10121
|
editor,
|
|
8998
10122
|
theme,
|
|
8999
10123
|
appState: { ...initialAppState },
|
|
@@ -9579,11 +10703,17 @@ var SessionManager$1 = class {
|
|
|
9579
10703
|
sessionTitle: session.summary?.title ?? null,
|
|
9580
10704
|
goal: goal ? {
|
|
9581
10705
|
objective: goal.objective,
|
|
10706
|
+
status: normalizeGoalStatus(goal.status),
|
|
9582
10707
|
turnsUsed: goal.turnsUsed ?? 0,
|
|
9583
10708
|
wallClockMs: goal.wallClockMs ?? 0,
|
|
9584
|
-
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
|
|
9585
10714
|
} : null,
|
|
9586
10715
|
goalActive: goal?.status === "active",
|
|
10716
|
+
goalJudge: "awaiting",
|
|
9587
10717
|
goalContinuationCount: 0,
|
|
9588
10718
|
providerBalance: null
|
|
9589
10719
|
});
|
|
@@ -11940,6 +13070,7 @@ function createInitialAppState(input) {
|
|
|
11940
13070
|
sessionTitle: null,
|
|
11941
13071
|
goal: null,
|
|
11942
13072
|
goalActive: false,
|
|
13073
|
+
goalJudge: "awaiting",
|
|
11943
13074
|
goalContinuationCount: 0,
|
|
11944
13075
|
ccConnectActive: false,
|
|
11945
13076
|
wolfpackMode: input.cliOptions.wolfpack === true,
|
|
@@ -12170,6 +13301,7 @@ var ScreamTUI = class {
|
|
|
12170
13301
|
this.lifecycleController.disposeTerminalTracking();
|
|
12171
13302
|
this.inputController.dispose();
|
|
12172
13303
|
this.state.footer.dispose();
|
|
13304
|
+
this.state.sidebarContainer.dispose();
|
|
12173
13305
|
await this.closeSession();
|
|
12174
13306
|
await this.harness.close();
|
|
12175
13307
|
this.sessionEventHandler.stopAllMcpServerStatusSpinners();
|
|
@@ -12440,7 +13572,7 @@ var ScreamTUI = class {
|
|
|
12440
13572
|
const card = new OnboardingCardComponent({
|
|
12441
13573
|
colors: this.state.theme.colors,
|
|
12442
13574
|
onConfigure: () => {
|
|
12443
|
-
import("./dispatch-
|
|
13575
|
+
import("./dispatch-CkPcMk4b.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
12444
13576
|
this.restoreEditor();
|
|
12445
13577
|
onDone?.();
|
|
12446
13578
|
});
|
|
@@ -15002,6 +16134,12 @@ var WebSession = class {
|
|
|
15002
16134
|
unsubscribe = null;
|
|
15003
16135
|
busy = false;
|
|
15004
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
|
+
/**
|
|
15005
16143
|
* Live assistant message under construction. Mirrors the same state machine
|
|
15006
16144
|
* used by buildMessages() so turn.ended can persist a complete durable
|
|
15007
16145
|
* snapshot (`web.message.finalized`) — the only source that survives a
|
|
@@ -15041,6 +16179,10 @@ var WebSession = class {
|
|
|
15041
16179
|
get isBusy() {
|
|
15042
16180
|
return this.busy;
|
|
15043
16181
|
}
|
|
16182
|
+
/** True while any agent turn (main or subagent) is in flight. */
|
|
16183
|
+
get isTurnActive() {
|
|
16184
|
+
return this.activeTurns > 0;
|
|
16185
|
+
}
|
|
15044
16186
|
getTitle() {
|
|
15045
16187
|
return this.title;
|
|
15046
16188
|
}
|
|
@@ -15170,6 +16312,7 @@ var WebSession = class {
|
|
|
15170
16312
|
if ((event.type === "goal.updated" || event.type === "todo.updated") && event.agentId !== "main") return;
|
|
15171
16313
|
if (event.type === "turn.started") {
|
|
15172
16314
|
this.busy = true;
|
|
16315
|
+
this.activeTurns += 1;
|
|
15173
16316
|
this.liveTurnCount += 1;
|
|
15174
16317
|
this.liveAssistant = {
|
|
15175
16318
|
role: "assistant",
|
|
@@ -15189,7 +16332,8 @@ var WebSession = class {
|
|
|
15189
16332
|
}
|
|
15190
16333
|
};
|
|
15191
16334
|
} else if (event.type === "turn.ended") {
|
|
15192
|
-
this.
|
|
16335
|
+
this.activeTurns = Math.max(0, this.activeTurns - 1);
|
|
16336
|
+
this.busy = this.activeTurns > 0;
|
|
15193
16337
|
const live = this.liveAssistant;
|
|
15194
16338
|
if (event.reason === "failed" && live) live.isError = true;
|
|
15195
16339
|
if (live) {
|
|
@@ -15407,6 +16551,7 @@ var WebSession = class {
|
|
|
15407
16551
|
}, false);
|
|
15408
16552
|
this.session.prompt(text).catch((error) => {
|
|
15409
16553
|
this.busy = false;
|
|
16554
|
+
this.activeTurns = 0;
|
|
15410
16555
|
this.sendError(ws, error, clientMessageId);
|
|
15411
16556
|
});
|
|
15412
16557
|
break;
|
|
@@ -15887,6 +17032,7 @@ var WebSession = class {
|
|
|
15887
17032
|
await session.prompt(prompt);
|
|
15888
17033
|
} catch (error) {
|
|
15889
17034
|
this.busy = false;
|
|
17035
|
+
this.activeTurns = 0;
|
|
15890
17036
|
throw error;
|
|
15891
17037
|
}
|
|
15892
17038
|
}
|
|
@@ -16266,6 +17412,10 @@ var SessionManager = class {
|
|
|
16266
17412
|
list() {
|
|
16267
17413
|
return Array.from(this.sessions.values()).map((s) => s.getListItem()).toSorted((a, b) => b.createdAt - a.createdAt);
|
|
16268
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
|
+
}
|
|
16269
17419
|
async delete(sessionId) {
|
|
16270
17420
|
const ws = this.sessions.get(sessionId);
|
|
16271
17421
|
if (!ws) return false;
|
|
@@ -16480,6 +17630,48 @@ var SessionManager = class {
|
|
|
16480
17630
|
return this.harness.preflight();
|
|
16481
17631
|
}
|
|
16482
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
|
+
}
|
|
16483
17675
|
async function handleSessionControlRoutes(req, res, url, method, manager) {
|
|
16484
17676
|
const statusMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/status$`).exec(url);
|
|
16485
17677
|
if (statusMatch && method === "GET") {
|
|
@@ -16935,9 +18127,15 @@ async function runWebServer(opts) {
|
|
|
16935
18127
|
publicDir = sourcePublicDir;
|
|
16936
18128
|
} catch {}
|
|
16937
18129
|
}
|
|
18130
|
+
const idleExit = startWebIdleExit({
|
|
18131
|
+
idleMs: (opts.idleMinutes ?? 15) * 6e4,
|
|
18132
|
+
busy: () => manager.anyBusy(),
|
|
18133
|
+
shutdown: () => cleanup()
|
|
18134
|
+
});
|
|
16938
18135
|
const httpServer = createServer(async (req, res) => {
|
|
16939
18136
|
const url = req.url ?? "/";
|
|
16940
18137
|
const method = req.method ?? "GET";
|
|
18138
|
+
idleExit.touch();
|
|
16941
18139
|
if (gateway) {
|
|
16942
18140
|
const verdict = gatewayVerdict({
|
|
16943
18141
|
loopback: isLoopbackAddress(req.socket.remoteAddress),
|
|
@@ -17292,7 +18490,12 @@ async function runWebServer(opts) {
|
|
|
17292
18490
|
}
|
|
17293
18491
|
});
|
|
17294
18492
|
const wss = new WebSocketServer({ server: httpServer });
|
|
18493
|
+
wss.on("error", () => {});
|
|
17295
18494
|
wss.on("connection", (ws, req) => {
|
|
18495
|
+
idleExit.noteConnection();
|
|
18496
|
+
ws.once("close", () => {
|
|
18497
|
+
idleExit.noteDisconnect();
|
|
18498
|
+
});
|
|
17296
18499
|
if (gateway && !isLoopbackAddress(req.socket.remoteAddress) && !gateway.verifySession(req.headers.cookie)) {
|
|
17297
18500
|
ws.close(1008, "unauthorized");
|
|
17298
18501
|
return;
|
|
@@ -17343,13 +18546,18 @@ async function runWebServer(opts) {
|
|
|
17343
18546
|
}
|
|
17344
18547
|
ws.send(JSON.stringify({ type: "server_empty" }));
|
|
17345
18548
|
});
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
17349
|
-
|
|
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);
|
|
17350
18556
|
});
|
|
17351
|
-
|
|
17352
|
-
|
|
18557
|
+
} catch (error) {
|
|
18558
|
+
idleExit.stop();
|
|
18559
|
+
throw error;
|
|
18560
|
+
}
|
|
17353
18561
|
const address = httpServer.address();
|
|
17354
18562
|
const realPort = typeof address === "object" && address !== null ? address.port : opts.port;
|
|
17355
18563
|
const url = `http://localhost:${realPort}`;
|
|
@@ -17358,6 +18566,7 @@ async function runWebServer(opts) {
|
|
|
17358
18566
|
const cleanup = async () => {
|
|
17359
18567
|
if (cleaned) return;
|
|
17360
18568
|
cleaned = true;
|
|
18569
|
+
idleExit.stop();
|
|
17361
18570
|
try {
|
|
17362
18571
|
wss.close();
|
|
17363
18572
|
await manager.closeAll();
|
|
@@ -17373,7 +18582,8 @@ async function runWebServer(opts) {
|
|
|
17373
18582
|
process.on("SIGTERM", () => {
|
|
17374
18583
|
cleanup().finally(() => process.exit(0));
|
|
17375
18584
|
});
|
|
17376
|
-
|
|
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`);
|
|
17377
18587
|
if (gateway) {
|
|
17378
18588
|
const addrs = getLanAddresses();
|
|
17379
18589
|
banner += ` LAN mode: on — non-local devices must present the access key\n`;
|
|
@@ -17445,7 +18655,8 @@ async function runWeb(opts) {
|
|
|
17445
18655
|
open: opts.open,
|
|
17446
18656
|
skillsDirs: opts.skillsDirs,
|
|
17447
18657
|
lan: opts.lan,
|
|
17448
|
-
token
|
|
18658
|
+
token,
|
|
18659
|
+
idleMinutes: opts.idleMinutes
|
|
17449
18660
|
});
|
|
17450
18661
|
}
|
|
17451
18662
|
//#endregion
|