scream-code 0.16.5 → 0.16.7
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 +9 -3
- package/dist/{app-F12Iraus.mjs → app-DDepBvBx.mjs} +455 -123
- package/dist/{dispatch-BNVgLsrQ.mjs → dispatch-CIGyjBDa.mjs} +1 -1
- package/dist/{dispatch-VY-K2oXV.mjs → dispatch-Vw9ccFm7.mjs} +1912 -233
- package/dist/main.mjs +1 -1
- package/dist/public/assets/index-DMGV8LA-.js +240 -0
- package/dist/public/assets/index-DMGV8LA-.js.map +1 -0
- package/dist/public/assets/index-DdPmkwIL.css +1 -0
- package/dist/public/index.html +2 -2
- package/dist/{text-input-dialog-CQ6R2lns.mjs → text-input-dialog-B-6Pgo6m.mjs} +1 -1
- package/dist/{text-input-dialog-KaFgI_Ko.mjs → text-input-dialog-D9ByqSsK.mjs} +154 -10
- package/package.json +3 -2
- package/dist/public/assets/index-DQBhBKK1.css +0 -1
- package/dist/public/assets/index-DakPeNBt.js +0 -240
- package/dist/public/assets/index-DakPeNBt.js.map +0 -1
|
@@ -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 { $n as
|
|
8
|
-
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-
|
|
7
|
+
import { $n as normalizeWorkDir, $t as lerpGradient, A as highlightLines, An as getKnowledgeStore, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as getDataDir, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getNoActiveSessionMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as isExperimentalFlagEnabled, Dt as createEditorTheme, E as UserMessageComponent, En as sortSlashCommands, Et as resolveThemeSync, F as StatusMessageComponent, Fn as TuiConfigParseError, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_UI_MODE, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getLogDir, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiLikePreferencesSchema, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as DEFAULT_CATALOG_URL, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_USER_AGENT_PRODUCT, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as loadTuiConfig, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as startManualEmbeddingDownload, Mt as createThemeStyles, N as ThinkingComponent, Nn as PIXEL_PULSE_FRAMES, Nt as contrastTextHex, O as renderDiffLines, On as setExperimentalFlags, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PULSE_WAVE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as encodeWorkDirKey, Qt as FooterComponent, R as SkillActivationComponent, Rn as saveTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getLlmNotSetMessage, St as supportsBalance, Tn as BUILTIN_SLASH_COMMANDS, U as getSharedSpeedTracker, Un as detectInstallSource, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getInputHistoryFile, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as CLI_COMMAND_NAME, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as saveCatalogCache, Xt as isBusy, Y as resetBreathingClock, Yn as fetchCatalog, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as ScreamHarness, Zt as isStreaming, _n as MAIN_AGENT_ID, a as syncDiagramCapabilityPrompt, an as printableChar, ar as MemoryMemoStore, at as toTerminalHyperlink, b as readUpdateCache, bn as getCtrlCHint, c as disposeChildren, cn as argsRecord, cr as resolveGlobalLogPath, d as isPlanExpandable, dn as parseStreamingArgs, dr as ErrorCodes, en as SubagentSlots, er as appendSessionIndexEntry, f as MoonLoader, fn as serializeToolResultOutput, fr as SCREAM_ERROR_INFO, ft as handleFusionPlanCommand, gn as EXIT_CONFIRM_WINDOW_MS, hn as EMPTY_SESSION_HINT_URL, i as buildRoleAdditionalText, ir as resolveScreamHome, it as isInternalMessage, j as langFromPath, jn as hasEmbeddingModelCache, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as embeddingFailureDetail, kt as isEmptySessionHintDismissed, l as hasDispose, ln as formatErrorMessage, lr as isScreamError, mn as truncateErrorMessage, mt as handlePlanCommand, n as clearEvalPanelState, nr as removeSessionIndexEntry, on as STATUS_BULLET, or as flushDiagnosticLogs, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as stringValue, q as BREATHE_CYCLE_MS, qn as PRODUCT_NAME, qt as TERMINAL_FOCUS_OUT, r as openUrl, sn as appendStreamingArgsPreview, sr as log, st as getModelCycleLevel, t as dispatchInput, tr as readSessionIndex, u as isExpandable, un as isTodoItemShape, ur as isOrphanedToolCallError, v as selectUpdateTarget, vn as SESSION_TIPS, w as getDaemonInstructions, wn as buildSkillSlashCommands, x as appendJsonlLine, xn as getCtrlDHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as TIP_ROTATION_INTERVAL_MS, z as AssistantMessageComponent, zn as detectShellEnvironment, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-Vw9ccFm7.mjs";
|
|
8
|
+
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-D9ByqSsK.mjs";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
11
11
|
import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
@@ -32,7 +32,7 @@ function optionalBuildString(value) {
|
|
|
32
32
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
33
33
|
}
|
|
34
34
|
const SCREAM_BUILD_INFO = {
|
|
35
|
-
version: optionalBuildString("0.16.
|
|
35
|
+
version: optionalBuildString("0.16.7"),
|
|
36
36
|
channel: optionalBuildString(""),
|
|
37
37
|
commit: optionalBuildString(""),
|
|
38
38
|
buildTarget: optionalBuildString("darwin-arm64")
|
|
@@ -1986,7 +1986,8 @@ function formatBackgroundAgentTranscript(phase, meta, extras = void 0) {
|
|
|
1986
1986
|
return {
|
|
1987
1987
|
phase,
|
|
1988
1988
|
headline,
|
|
1989
|
-
detail: detailParts.length > 0 ? detailParts.join(" · ") : void 0
|
|
1989
|
+
detail: detailParts.length > 0 ? detailParts.join(" · ") : void 0,
|
|
1990
|
+
trackingId: meta.agentId
|
|
1990
1991
|
};
|
|
1991
1992
|
}
|
|
1992
1993
|
//#endregion
|
|
@@ -2209,7 +2210,8 @@ function formatBackgroundTaskTranscript(info) {
|
|
|
2209
2210
|
return {
|
|
2210
2211
|
phase: phaseFromStatus(info.status),
|
|
2211
2212
|
headline: headlineFor(info),
|
|
2212
|
-
detail: detailFor(info)
|
|
2213
|
+
detail: detailFor(info),
|
|
2214
|
+
trackingId: info.taskId
|
|
2213
2215
|
};
|
|
2214
2216
|
}
|
|
2215
2217
|
//#endregion
|
|
@@ -2361,15 +2363,15 @@ function formatStepDebugTiming(input) {
|
|
|
2361
2363
|
const latency = input.llmFirstTokenLatencyMs;
|
|
2362
2364
|
const streamMs = input.llmStreamDurationMs;
|
|
2363
2365
|
if (latency === void 0 || streamMs === void 0) return void 0;
|
|
2364
|
-
const parts = [`TTFT: ${formatDuration
|
|
2366
|
+
const parts = [`TTFT: ${formatDuration(latency)}`];
|
|
2365
2367
|
const outputTokens = input.usage?.output;
|
|
2366
2368
|
if (outputTokens !== void 0 && outputTokens > 0 && streamMs > 0) {
|
|
2367
2369
|
const tps = (outputTokens / (streamMs / 1e3)).toFixed(1);
|
|
2368
|
-
parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration
|
|
2370
|
+
parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration(streamMs)})`);
|
|
2369
2371
|
}
|
|
2370
2372
|
return `[Debug] ${parts.join(" | ")}`;
|
|
2371
2373
|
}
|
|
2372
|
-
function formatDuration
|
|
2374
|
+
function formatDuration(ms) {
|
|
2373
2375
|
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
2374
2376
|
return `${(ms / 1e3).toFixed(1)}s`;
|
|
2375
2377
|
}
|
|
@@ -2910,6 +2912,7 @@ var SessionEventHandler = class {
|
|
|
2910
2912
|
}
|
|
2911
2913
|
handleStepCompleted(event) {
|
|
2912
2914
|
this.host.streamingUI.flushNow();
|
|
2915
|
+
this.host.streamingUI.flushPendingApprovals();
|
|
2913
2916
|
this.maybeShowDebugTiming(event);
|
|
2914
2917
|
this.recordProviderLatency(event);
|
|
2915
2918
|
this.drainQueuedMessagesIntoSteer();
|
|
@@ -2936,6 +2939,24 @@ var SessionEventHandler = class {
|
|
|
2936
2939
|
sessionApiCalls: this.host.state.appState.sessionApiCalls + 1
|
|
2937
2940
|
});
|
|
2938
2941
|
}
|
|
2942
|
+
/**
|
|
2943
|
+
* A retried attempt that produces output again is no longer reconnecting:
|
|
2944
|
+
* drop the retry label as soon as it streams a delta or emits a tool call, so
|
|
2945
|
+
* neither the footer status block nor the status bar keeps claiming
|
|
2946
|
+
* "重连中 N/M" for the rest of the step. The fields are only ever set by
|
|
2947
|
+
* handleStepRetrying, so this is the single clearing point besides
|
|
2948
|
+
* step/turn boundaries.
|
|
2949
|
+
*/
|
|
2950
|
+
clearReconnectState() {
|
|
2951
|
+
if (!(this.host.state.appState.reconnectAttempt > 0)) return;
|
|
2952
|
+
this.host.setAppState({
|
|
2953
|
+
reconnectAttempt: 0,
|
|
2954
|
+
reconnectMaxAttempts: void 0,
|
|
2955
|
+
reconnectDelayMs: void 0,
|
|
2956
|
+
reconnectStatusCode: void 0,
|
|
2957
|
+
reconnectErrorName: void 0
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2939
2960
|
maybeShowDebugTiming(event) {
|
|
2940
2961
|
if (process.env["SCREAM_CODE_DEBUG"] !== "1") return;
|
|
2941
2962
|
const text = formatStepDebugTiming(event);
|
|
@@ -2995,12 +3016,14 @@ var SessionEventHandler = class {
|
|
|
2995
3016
|
}
|
|
2996
3017
|
handleThinkingDelta(event) {
|
|
2997
3018
|
const { state, streamingUI } = this.host;
|
|
3019
|
+
this.clearReconnectState();
|
|
2998
3020
|
streamingUI.appendThinkingDelta(event.delta);
|
|
2999
3021
|
if (canTransitionTo(state.appState.streamingPhase, "thinking")) this.host.setAppState({ streamingPhase: "thinking" });
|
|
3000
3022
|
streamingUI.scheduleFlush();
|
|
3001
3023
|
}
|
|
3002
3024
|
handleAssistantDelta(event) {
|
|
3003
3025
|
const { state, streamingUI } = this.host;
|
|
3026
|
+
this.clearReconnectState();
|
|
3004
3027
|
if (streamingUI.hasThinkingDraft()) streamingUI.flushThinkingToTranscript();
|
|
3005
3028
|
streamingUI.appendAssistantDelta(event.delta);
|
|
3006
3029
|
this.host.patchLivePane({
|
|
@@ -3028,6 +3051,7 @@ var SessionEventHandler = class {
|
|
|
3028
3051
|
}
|
|
3029
3052
|
handleToolCall(event) {
|
|
3030
3053
|
const { streamingUI } = this.host;
|
|
3054
|
+
this.clearReconnectState();
|
|
3031
3055
|
streamingUI.flushNow();
|
|
3032
3056
|
if (event.name === "SendSubagentMessage") {
|
|
3033
3057
|
const args = argsRecord(event.args);
|
|
@@ -3059,6 +3083,7 @@ var SessionEventHandler = class {
|
|
|
3059
3083
|
handleToolCallDelta(event) {
|
|
3060
3084
|
if (event.toolCallId.length === 0) return;
|
|
3061
3085
|
const { state, streamingUI } = this.host;
|
|
3086
|
+
this.clearReconnectState();
|
|
3062
3087
|
streamingUI.accumulateToolCallDelta(event.toolCallId, event.name, event.argumentsPart);
|
|
3063
3088
|
this.host.patchLivePane({
|
|
3064
3089
|
pendingApproval: null,
|
|
@@ -3113,7 +3138,7 @@ var SessionEventHandler = class {
|
|
|
3113
3138
|
message: event.message
|
|
3114
3139
|
};
|
|
3115
3140
|
streamingUI.completeToolResult(event.toolCallId, resultData);
|
|
3116
|
-
if (canTransitionTo(this.host.state.appState.streamingPhase, "waiting")) this.host.setAppState({ streamingPhase: "waiting" });
|
|
3141
|
+
if (!streamingUI.hasPendingToolCalls() && canTransitionTo(this.host.state.appState.streamingPhase, "waiting")) this.host.setAppState({ streamingPhase: "waiting" });
|
|
3117
3142
|
}
|
|
3118
3143
|
handleStatusUpdate(event) {
|
|
3119
3144
|
const patch = {};
|
|
@@ -3134,11 +3159,16 @@ var SessionEventHandler = class {
|
|
|
3134
3159
|
}
|
|
3135
3160
|
}
|
|
3136
3161
|
handleSessionError(event) {
|
|
3162
|
+
const hadActiveTurn = this.host.streamingUI.hasActiveTurn();
|
|
3137
3163
|
this.host.streamingUI.flushNow();
|
|
3138
3164
|
this.host.streamingUI.resetToolUi();
|
|
3139
3165
|
this.host.streamingUI.finalizeLiveTextBuffers();
|
|
3140
3166
|
this.host.streamingUI.endActivityGroup();
|
|
3141
3167
|
this.host.showError(`[${event.code}] ${event.message}`);
|
|
3168
|
+
if (event.code === "turn.agent_busy" && !hadActiveTurn && this.host.state.appState.streamingPhase !== "idle") {
|
|
3169
|
+
this.host.setAppState({ streamingPhase: "idle" });
|
|
3170
|
+
this.host.resetLivePane();
|
|
3171
|
+
}
|
|
3142
3172
|
}
|
|
3143
3173
|
handleSessionWarning(event) {
|
|
3144
3174
|
this.host.showStatus(t("handler.warning_prefix", { message: event.message }), this.host.state.theme.colors.warning);
|
|
@@ -3637,6 +3667,19 @@ var SessionEventHandler = class {
|
|
|
3637
3667
|
}
|
|
3638
3668
|
};
|
|
3639
3669
|
//#endregion
|
|
3670
|
+
//#region src/tui/utils/approval-notice.ts
|
|
3671
|
+
function buildApprovalNotice(args) {
|
|
3672
|
+
const sessionScoped = args.decision === "approved" && args.scope === "session";
|
|
3673
|
+
const tone = sessionScoped ? "approved_session" : args.decision;
|
|
3674
|
+
const label = args.decision === "approved" ? sessionScoped ? args.labels.approvedSession : args.labels.approved : args.decision === "rejected" ? args.labels.rejected : args.labels.cancelled;
|
|
3675
|
+
const feedback = args.feedback !== void 0 && args.feedback.length > 0 ? ` — "${args.feedback}"` : "";
|
|
3676
|
+
return {
|
|
3677
|
+
label,
|
|
3678
|
+
detail: `${args.action}${feedback}`,
|
|
3679
|
+
tone
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
//#endregion
|
|
3640
3683
|
//#region src/tui/utils/media-url.ts
|
|
3641
3684
|
function getMediaKindLabels() {
|
|
3642
3685
|
return {
|
|
@@ -4095,6 +4138,7 @@ var SessionReplayRenderer = class {
|
|
|
4095
4138
|
}
|
|
4096
4139
|
advanceTurn(context) {
|
|
4097
4140
|
this.host.streamingUI.endActivityGroup();
|
|
4141
|
+
this.host.streamingUI.flushPendingApprovals();
|
|
4098
4142
|
context.turnIndex += 1;
|
|
4099
4143
|
context.stepIndex = 0;
|
|
4100
4144
|
context.currentTurnId = `replay:${String(context.turnIndex)}`;
|
|
@@ -4173,21 +4217,19 @@ var SessionReplayRenderer = class {
|
|
|
4173
4217
|
return;
|
|
4174
4218
|
}
|
|
4175
4219
|
const { result } = record;
|
|
4176
|
-
const
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
}
|
|
4188
|
-
|
|
4189
|
-
if (result.feedback !== void 0 && result.feedback.length > 0) parts.push(` — "${result.feedback}"`);
|
|
4190
|
-
this.host.appendTranscriptEntry(replayEntry(context, "status", parts.join(""), "notice"));
|
|
4220
|
+
const notice = buildApprovalNotice({
|
|
4221
|
+
decision: result.decision,
|
|
4222
|
+
scope: result.scope,
|
|
4223
|
+
action: record.action,
|
|
4224
|
+
feedback: result.feedback,
|
|
4225
|
+
labels: {
|
|
4226
|
+
approved: t("replay.approved"),
|
|
4227
|
+
approvedSession: t("replay.approved_session"),
|
|
4228
|
+
rejected: t("replay.rejected"),
|
|
4229
|
+
cancelled: t("replay.cancelled")
|
|
4230
|
+
}
|
|
4231
|
+
});
|
|
4232
|
+
this.host.streamingUI.recordApproval(record.toolCallId, notice.label, notice.detail, notice.tone);
|
|
4191
4233
|
}
|
|
4192
4234
|
renderPlanReviewResult(context, record) {
|
|
4193
4235
|
const { result } = record;
|
|
@@ -4366,6 +4408,11 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4366
4408
|
_pendingToolComponents = /* @__PURE__ */ new Map();
|
|
4367
4409
|
_pendingAgentGroup = null;
|
|
4368
4410
|
_activityGroup;
|
|
4411
|
+
/**
|
|
4412
|
+
* Approval outcomes answered before their tool call was dispatched, waiting
|
|
4413
|
+
* for the call's row to appear (see `recordApproval`). Keyed by call id.
|
|
4414
|
+
*/
|
|
4415
|
+
_pendingApprovals = /* @__PURE__ */ new Map();
|
|
4369
4416
|
/** Guards the single `registerLiveComponent` call for the turn's block. */
|
|
4370
4417
|
_activityGroupRegistered = false;
|
|
4371
4418
|
/** Block that borrowed a card, so late results refresh their own block. */
|
|
@@ -4578,6 +4625,19 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4578
4625
|
this._streamingToolCallArguments.delete(toolCallId);
|
|
4579
4626
|
return matchedCall;
|
|
4580
4627
|
}
|
|
4628
|
+
/** True while a tool call of the step being handled has no result yet. A
|
|
4629
|
+
* parallel batch dispatches its results in provider order, so this is what
|
|
4630
|
+
* keeps the footer in "执行中" until the whole batch settles instead of
|
|
4631
|
+
* flipping to "等待中" on the first result. Entries from an earlier step or
|
|
4632
|
+
* turn are ignored: nothing can settle them any more, and counting them
|
|
4633
|
+
* would pin the footer in "执行中" for the rest of the turn. */
|
|
4634
|
+
hasPendingToolCalls() {
|
|
4635
|
+
for (const call of this._activeToolCalls.values()) {
|
|
4636
|
+
if (call.turnId !== this._currentTurnId || call.step !== this._currentStep) continue;
|
|
4637
|
+
if (call.result === void 0) return true;
|
|
4638
|
+
}
|
|
4639
|
+
return false;
|
|
4640
|
+
}
|
|
4581
4641
|
/** Marks in-flight tool calls as truncated when a step hits max_tokens.
|
|
4582
4642
|
* Returns the count of tool calls that were truncated. */
|
|
4583
4643
|
markStepTruncated(turnId, step) {
|
|
@@ -4598,6 +4658,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4598
4658
|
/** Tears down replay-specific state after session history has been rendered. */
|
|
4599
4659
|
cleanupAfterReplay(completedToolCallIds) {
|
|
4600
4660
|
this.endActivityGroup();
|
|
4661
|
+
this.flushPendingApprovals();
|
|
4601
4662
|
this._activeToolCalls.clear();
|
|
4602
4663
|
for (const toolCallId of completedToolCallIds) this._pendingToolComponents.delete(toolCallId);
|
|
4603
4664
|
this._pendingAgentGroup = null;
|
|
@@ -4665,6 +4726,65 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4665
4726
|
group.attachNotice(data);
|
|
4666
4727
|
return true;
|
|
4667
4728
|
}
|
|
4729
|
+
/**
|
|
4730
|
+
* Files an approval outcome with the call it allowed.
|
|
4731
|
+
*
|
|
4732
|
+
* The prompt is answered before its tool call is dispatched, so the row it
|
|
4733
|
+
* belongs under usually does not exist yet: while a turn is running the
|
|
4734
|
+
* outcome waits here for its own call to appear, and is filed under it the
|
|
4735
|
+
* moment it does. Two cases keep the row they always had instead — an outcome
|
|
4736
|
+
* answered between turns (a background agent's call, where no block will ever
|
|
4737
|
+
* open), and one whose call's row is not in the block that is open now (a
|
|
4738
|
+
* subagent's call, a card that stays standalone, a block that already
|
|
4739
|
+
* settled), which is mounted when the step settles.
|
|
4740
|
+
*/
|
|
4741
|
+
recordApproval(callId, label, detail, tone) {
|
|
4742
|
+
const card = this._pendingToolComponents.get(callId);
|
|
4743
|
+
const owner = card === void 0 ? void 0 : this._groupByToolCard.get(card);
|
|
4744
|
+
if (owner !== void 0 && this.host.state.transcriptContainer.children.includes(owner) && owner.attachApproval(callId, label, detail, tone)) return;
|
|
4745
|
+
const group = this._activityGroup;
|
|
4746
|
+
if (group !== void 0 && group.attachApproval(callId, label, detail, tone)) return;
|
|
4747
|
+
if (!this.hasActiveTurn()) {
|
|
4748
|
+
this.mountApprovalNotices([{
|
|
4749
|
+
label,
|
|
4750
|
+
detail,
|
|
4751
|
+
tone
|
|
4752
|
+
}]);
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
this._pendingApprovals.set(callId, {
|
|
4756
|
+
label,
|
|
4757
|
+
detail,
|
|
4758
|
+
tone
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
/** Files the outcome waiting for this call, now that its row exists. */
|
|
4762
|
+
adoptPendingApproval(group, callId) {
|
|
4763
|
+
const pending = this._pendingApprovals.get(callId);
|
|
4764
|
+
if (pending === void 0) return;
|
|
4765
|
+
this._pendingApprovals.delete(callId);
|
|
4766
|
+
if (!group.attachApproval(callId, pending.label, pending.detail, pending.tone)) this.mountApprovalNotices([pending]);
|
|
4767
|
+
}
|
|
4768
|
+
/**
|
|
4769
|
+
* Mounts outcomes as their own notice row — the row an approval used to get
|
|
4770
|
+
* straight away, and the fallback for anything no block ever claims.
|
|
4771
|
+
*/
|
|
4772
|
+
mountApprovalNotices(approvals) {
|
|
4773
|
+
for (const approval of approvals) this.host.transcriptController.appendEntry({
|
|
4774
|
+
id: nextTranscriptId(),
|
|
4775
|
+
kind: "status",
|
|
4776
|
+
renderMode: "notice",
|
|
4777
|
+
content: `${approval.label}: ${approval.detail}`
|
|
4778
|
+
});
|
|
4779
|
+
this.host.state.ui.requestRender();
|
|
4780
|
+
}
|
|
4781
|
+
/** Settles anything still waiting: mounted as notices, in the order answered. */
|
|
4782
|
+
flushPendingApprovals() {
|
|
4783
|
+
if (this._pendingApprovals.size === 0) return;
|
|
4784
|
+
const parked = [...this._pendingApprovals.values()];
|
|
4785
|
+
this._pendingApprovals.clear();
|
|
4786
|
+
this.mountApprovalNotices(parked);
|
|
4787
|
+
}
|
|
4668
4788
|
endActivityGroup() {
|
|
4669
4789
|
const group = this._activityGroup;
|
|
4670
4790
|
this._activityGroup = void 0;
|
|
@@ -4787,6 +4907,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4787
4907
|
this._assistantDraft = "";
|
|
4788
4908
|
this._shownAssistantLength = 0;
|
|
4789
4909
|
this.clearFlushTimerIfIdle();
|
|
4910
|
+
this._streamingBlock?.component.setStreaming(false);
|
|
4790
4911
|
this._streamingBlock = null;
|
|
4791
4912
|
this._thinkingDraft = "";
|
|
4792
4913
|
this.lastDeltaAt = void 0;
|
|
@@ -4799,6 +4920,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4799
4920
|
this._streamingToolCallArguments.clear();
|
|
4800
4921
|
this.disposeAndClearPendingToolComponents();
|
|
4801
4922
|
this._pendingAgentGroup = null;
|
|
4923
|
+
this.flushPendingApprovals();
|
|
4802
4924
|
}
|
|
4803
4925
|
resetToolCallState() {
|
|
4804
4926
|
this._activeToolCalls.clear();
|
|
@@ -4889,6 +5011,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4889
5011
|
const block = this._streamingBlock;
|
|
4890
5012
|
if (block !== null) {
|
|
4891
5013
|
block.entry.content = fullText;
|
|
5014
|
+
block.component.setStreaming(true);
|
|
4892
5015
|
block.component.updateContent(fullText);
|
|
4893
5016
|
this.host.state.ui.requestRender();
|
|
4894
5017
|
}
|
|
@@ -4898,6 +5021,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4898
5021
|
const block = this._streamingBlock;
|
|
4899
5022
|
this._streamingBlock = null;
|
|
4900
5023
|
if (block !== null) {
|
|
5024
|
+
block.component.setStreaming(false);
|
|
4901
5025
|
this.host.transcriptController.unmarkPending(block.component);
|
|
4902
5026
|
this.host.transcriptController.commit();
|
|
4903
5027
|
}
|
|
@@ -4962,6 +5086,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4962
5086
|
if (state.planExpanded) tc.setPlanExpanded(true);
|
|
4963
5087
|
if (toolCall.name !== "Agent") this._pendingAgentGroup = null;
|
|
4964
5088
|
if (!this.tryAttachAgentToolCall(toolCall, tc)) if (StreamingUIController.STANDALONE_TOOL_NAMES.has(toolCall.name)) {
|
|
5089
|
+
if (toolCall.name === "ExitPlanMode") this.endActivityGroup();
|
|
4965
5090
|
tc.setExpanded(state.toolOutputExpanded);
|
|
4966
5091
|
state.transcriptContainer.addChild(tc);
|
|
4967
5092
|
state.ui.requestRender();
|
|
@@ -4969,7 +5094,8 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4969
5094
|
const group = this.ensureActivityGroup();
|
|
4970
5095
|
this.registerActivityGroupEntry(group, entry);
|
|
4971
5096
|
this._groupByToolCard.set(tc, group);
|
|
4972
|
-
group.attachTool(tc, toolCall.step ?? this._currentStep);
|
|
5097
|
+
group.attachTool(tc, toolCall.step ?? this._currentStep, toolCall.id);
|
|
5098
|
+
this.adoptPendingApproval(group, toolCall.id);
|
|
4973
5099
|
state.ui.requestRender();
|
|
4974
5100
|
}
|
|
4975
5101
|
if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
|
|
@@ -6281,6 +6407,9 @@ var TranscriptController = class TranscriptController {
|
|
|
6281
6407
|
welcomeComponent;
|
|
6282
6408
|
committedComponent;
|
|
6283
6409
|
liveComponentToEntry = /* @__PURE__ */ new Map();
|
|
6410
|
+
/** Live `started` notice cards by task id, so the terminal notice can stop the
|
|
6411
|
+
* ticker of the card that is still claiming the task is running. */
|
|
6412
|
+
liveNoticesByTrackingId = /* @__PURE__ */ new Map();
|
|
6284
6413
|
pendingComponents = /* @__PURE__ */ new Set();
|
|
6285
6414
|
/** Max live transcript children before the oldest are folded into the
|
|
6286
6415
|
* committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
|
|
@@ -6306,6 +6435,26 @@ var TranscriptController = class TranscriptController {
|
|
|
6306
6435
|
releaseLiveComponent(component) {
|
|
6307
6436
|
this.liveComponentToEntry.delete(component);
|
|
6308
6437
|
}
|
|
6438
|
+
/**
|
|
6439
|
+
* Stop everything a component owns once it leaves the live area. Committed
|
|
6440
|
+
* views re-render from the entry data, so disposing cannot lose content, but
|
|
6441
|
+
* skipping it would let an animation timer outlive the row it belonged to.
|
|
6442
|
+
*/
|
|
6443
|
+
dropLiveComponent(component) {
|
|
6444
|
+
for (const [trackingId, tracked] of this.liveNoticesByTrackingId) if (tracked === component) this.liveNoticesByTrackingId.delete(trackingId);
|
|
6445
|
+
if (hasDispose(component)) component.dispose();
|
|
6446
|
+
}
|
|
6447
|
+
/**
|
|
6448
|
+
* Stops the ticker of the card that announced `trackingId`. Called when the
|
|
6449
|
+
* task reaches a terminal state: that card is history now, and a glyph that
|
|
6450
|
+
* keeps rotating would claim the task is still running.
|
|
6451
|
+
*/
|
|
6452
|
+
settleLiveNotice(trackingId) {
|
|
6453
|
+
const tracked = this.liveNoticesByTrackingId.get(trackingId);
|
|
6454
|
+
if (tracked === void 0) return;
|
|
6455
|
+
this.liveNoticesByTrackingId.delete(trackingId);
|
|
6456
|
+
tracked.settle();
|
|
6457
|
+
}
|
|
6309
6458
|
markPending(component) {
|
|
6310
6459
|
this.pendingComponents.add(component);
|
|
6311
6460
|
}
|
|
@@ -6347,6 +6496,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6347
6496
|
this.committedComponent.appendEntry(entry, state.theme.colors);
|
|
6348
6497
|
container.removeChild(component);
|
|
6349
6498
|
this.liveComponentToEntry.delete(component);
|
|
6499
|
+
this.dropLiveComponent(component);
|
|
6350
6500
|
}
|
|
6351
6501
|
this.committedComponent.setCount(this.committedComponent.getCount() + toCommit.length);
|
|
6352
6502
|
if (process.env["SCREAM_CODE_DEBUG"] === "1") this.host.showStatus(`[debug] committed=${this.committedComponent.getCount()} live=${this.getLiveCount()}`);
|
|
@@ -6381,10 +6531,10 @@ var TranscriptController = class TranscriptController {
|
|
|
6381
6531
|
if (state.planExpanded) tc.setPlanExpanded(true);
|
|
6382
6532
|
return tc;
|
|
6383
6533
|
}
|
|
6384
|
-
if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors);
|
|
6534
|
+
if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors, state.ui);
|
|
6385
6535
|
return entry.renderMode === "notice" ? new NoticeMessageComponent(entry.content, entry.detail, state.theme.colors, entry.noticeMarkerColor) : new StatusMessageComponent(entry.content, state.theme.colors, entry.color);
|
|
6386
6536
|
case "status":
|
|
6387
|
-
if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors);
|
|
6537
|
+
if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.theme.colors, state.ui);
|
|
6388
6538
|
return entry.renderMode === "notice" ? new NoticeMessageComponent(entry.content, entry.detail, state.theme.colors, entry.noticeMarkerColor) : new StatusMessageComponent(entry.content, state.theme.colors, entry.color);
|
|
6389
6539
|
case "cron":
|
|
6390
6540
|
if (entry.cronData === void 0) return null;
|
|
@@ -6399,8 +6549,15 @@ var TranscriptController = class TranscriptController {
|
|
|
6399
6549
|
if (component) {
|
|
6400
6550
|
this.liveComponentToEntry.set(component, entry);
|
|
6401
6551
|
const status = entry.backgroundAgentStatus;
|
|
6402
|
-
if (status !== void 0 &&
|
|
6403
|
-
|
|
6552
|
+
if (status !== void 0 && component instanceof BackgroundAgentStatusComponent) {
|
|
6553
|
+
const trackingId = status.trackingId;
|
|
6554
|
+
if (trackingId !== void 0) if (status.phase === "started") this.liveNoticesByTrackingId.set(trackingId, component);
|
|
6555
|
+
else this.settleLiveNotice(trackingId);
|
|
6556
|
+
}
|
|
6557
|
+
if (status !== void 0 && this.host.streamingUI.attachNotice(status)) {
|
|
6558
|
+
this.releaseLiveComponent(component);
|
|
6559
|
+
this.dropLiveComponent(component);
|
|
6560
|
+
} else this.host.state.transcriptContainer.addChild(component);
|
|
6404
6561
|
this.host.state.ui.requestRender();
|
|
6405
6562
|
}
|
|
6406
6563
|
return component ?? null;
|
|
@@ -6426,26 +6583,19 @@ var TranscriptController = class TranscriptController {
|
|
|
6426
6583
|
}
|
|
6427
6584
|
appendApprovalEntry(request, response) {
|
|
6428
6585
|
if (request.toolName === "ExitPlanMode" || request.display.kind === "plan_review") return;
|
|
6429
|
-
const
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
}
|
|
6441
|
-
parts.push(`: ${request.action}`);
|
|
6442
|
-
if (response.feedback !== void 0 && response.feedback.length > 0) parts.push(` — "${response.feedback}"`);
|
|
6443
|
-
this.appendEntry({
|
|
6444
|
-
id: nextTranscriptId(),
|
|
6445
|
-
kind: "status",
|
|
6446
|
-
renderMode: "notice",
|
|
6447
|
-
content: parts.join("")
|
|
6586
|
+
const notice = buildApprovalNotice({
|
|
6587
|
+
decision: response.decision,
|
|
6588
|
+
scope: response.scope,
|
|
6589
|
+
action: request.action,
|
|
6590
|
+
feedback: response.feedback,
|
|
6591
|
+
labels: {
|
|
6592
|
+
approved: t("tc.approved"),
|
|
6593
|
+
approvedSession: t("tc.approved_session"),
|
|
6594
|
+
rejected: t("tc.rejected"),
|
|
6595
|
+
cancelled: t("tc.cancelled")
|
|
6596
|
+
}
|
|
6448
6597
|
});
|
|
6598
|
+
this.host.streamingUI.recordApproval(request.toolCallId, notice.label, notice.detail, notice.tone);
|
|
6449
6599
|
}
|
|
6450
6600
|
renderWelcome() {
|
|
6451
6601
|
const { state } = this.host;
|
|
@@ -6464,6 +6614,7 @@ var TranscriptController = class TranscriptController {
|
|
|
6464
6614
|
const { state, streamingUI, imageStore } = this.host;
|
|
6465
6615
|
streamingUI.discardPending();
|
|
6466
6616
|
streamingUI.endActivityGroup();
|
|
6617
|
+
streamingUI.flushPendingApprovals();
|
|
6467
6618
|
state.transcriptEntries = [];
|
|
6468
6619
|
streamingUI.disposeActiveCompactionBlock();
|
|
6469
6620
|
streamingUI.resetLiveText();
|
|
@@ -6731,20 +6882,6 @@ const gitPanel = {
|
|
|
6731
6882
|
};
|
|
6732
6883
|
//#endregion
|
|
6733
6884
|
//#region src/tui/components/sidebar/panels/session-panel.ts
|
|
6734
|
-
function formatDuration(ms) {
|
|
6735
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
6736
|
-
if (getLocale() === "zh") {
|
|
6737
|
-
if (totalSeconds < 60) return `${totalSeconds}秒`;
|
|
6738
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
6739
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
6740
|
-
return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`;
|
|
6741
|
-
}
|
|
6742
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
6743
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
6744
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
6745
|
-
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
6746
|
-
return `${minutes}m`;
|
|
6747
|
-
}
|
|
6748
6885
|
/** 1,234 → "1,234" via the platform locale grouping. */
|
|
6749
6886
|
function formatCount$1(n) {
|
|
6750
6887
|
return n.toLocaleString("en-US");
|
|
@@ -6770,8 +6907,8 @@ function gridFits(rows, width) {
|
|
|
6770
6907
|
/**
|
|
6771
6908
|
* Session summary, live-rendered each frame as a strict two-column table:
|
|
6772
6909
|
* one field per row, labels sharing a left column, values sharing a right
|
|
6773
|
-
* edge.
|
|
6774
|
-
*
|
|
6910
|
+
* edge. Every value comes from the snapshot in getData(); nothing here is
|
|
6911
|
+
* derived from wall-clock time.
|
|
6775
6912
|
*/
|
|
6776
6913
|
var SessionPanelContent = class {
|
|
6777
6914
|
ctx;
|
|
@@ -6786,7 +6923,6 @@ var SessionPanelContent = class {
|
|
|
6786
6923
|
if (stats === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.session_empty"))];
|
|
6787
6924
|
const label = (s) => chalk.hex(this.colors.textDim)(s);
|
|
6788
6925
|
const value = (s) => chalk.hex(this.colors.text)(s);
|
|
6789
|
-
const now = Date.now();
|
|
6790
6926
|
const tokenRows = [
|
|
6791
6927
|
[t("sidebar.tokens"), stats.tokensTotal],
|
|
6792
6928
|
[t("sidebar.tokens_cache_hit"), stats.tokensInputCacheHit],
|
|
@@ -6794,7 +6930,6 @@ var SessionPanelContent = class {
|
|
|
6794
6930
|
[t("sidebar.tokens_output"), stats.tokensOutput]
|
|
6795
6931
|
];
|
|
6796
6932
|
const countRows = [
|
|
6797
|
-
[t("sidebar.uptime"), formatDuration(now - stats.startedAt)],
|
|
6798
6933
|
[t("sidebar.turns"), formatCount$1(stats.turns)],
|
|
6799
6934
|
[t("sidebar.tools"), formatCount$1(stats.toolCalls)],
|
|
6800
6935
|
[t("sidebar.calls"), formatCount$1(stats.apiCalls)],
|
|
@@ -6841,8 +6976,8 @@ function statusText(status) {
|
|
|
6841
6976
|
}
|
|
6842
6977
|
}
|
|
6843
6978
|
/**
|
|
6844
|
-
* Fixed
|
|
6845
|
-
* custom types appended in spawn order). Live-rendered each frame from the
|
|
6979
|
+
* Fixed default-slot subagent panel: one row per default subagent type (plus
|
|
6980
|
+
* any custom types appended in spawn order). Live-rendered each frame from the
|
|
6846
6981
|
* current snapshot; idle slots stay greyed out.
|
|
6847
6982
|
*/
|
|
6848
6983
|
var AgentsPanelContent = class {
|
|
@@ -6865,7 +7000,7 @@ var AgentsPanelContent = class {
|
|
|
6865
7000
|
renderSlot(cell, slot, index, width) {
|
|
6866
7001
|
const dim = (s) => chalk.hex(this.colors.textDim)(s);
|
|
6867
7002
|
const idle = slot.status === "idle";
|
|
6868
|
-
const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .
|
|
7003
|
+
const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .6180339887498949) % 1;
|
|
6869
7004
|
const active = slot.status === "working" || slot.status === "outputting" ? chalk.hex(lerpGradient(phase)).bold : void 0;
|
|
6870
7005
|
const marker = padLabel((active ?? chalk.hex(this.statusColor(slot.status)))(idle ? "○" : "●"), 2);
|
|
6871
7006
|
const label = (idle ? dim : chalk.hex(this.colors.textStrong))(cell.label);
|
|
@@ -7125,23 +7260,38 @@ function toneFor(site, stale) {
|
|
|
7125
7260
|
/**
|
|
7126
7261
|
* Merge the measured provider reading with the probe samples into Hub rows.
|
|
7127
7262
|
* The provider row always leads (it answers "is the model I'm using reachable"),
|
|
7128
|
-
*
|
|
7129
|
-
*
|
|
7263
|
+
* it staleness-checks against its own budget: a long idle stretch means nothing
|
|
7264
|
+
* was asked, not that the network died — and it is coloured on its own ladder
|
|
7265
|
+
* (see {@link hubModelTone}), not on the probe bar.
|
|
7130
7266
|
*/
|
|
7131
7267
|
function buildHubSamples(snapshot, provider, now = Date.now()) {
|
|
7132
7268
|
const ageMs = provider.sampledAt === void 0 ? 0 : now - provider.sampledAt;
|
|
7133
7269
|
return [{
|
|
7134
7270
|
id: HUB_MODEL_ROW_ID,
|
|
7135
7271
|
ms: provider.ms,
|
|
7136
|
-
tone:
|
|
7272
|
+
tone: hubModelTone(provider.ms, ageMs >= HUB_MODEL_STALE_MS)
|
|
7137
7273
|
}, ...snapshot.samples];
|
|
7138
7274
|
}
|
|
7139
|
-
/**
|
|
7275
|
+
/** Tone rule for the probed endpoint rows: green ≤{@link HUB_GOOD_MS}, amber above. */
|
|
7140
7276
|
function hubLatencyTone(ms, stale = false) {
|
|
7141
7277
|
if (ms === void 0 || stale) return "dim";
|
|
7142
7278
|
return ms <= 100 ? "ok" : "warn";
|
|
7143
7279
|
}
|
|
7144
7280
|
/**
|
|
7281
|
+
* Tone rule for the measured provider row (model first token), deliberately
|
|
7282
|
+
* separate from the probe ladder: a first token and a HEAD request are not
|
|
7283
|
+
* comparable, and one shared bar would call every healthy answer slow. Green
|
|
7284
|
+
* ≤{@link HUB_MODEL_GOOD_MS}, amber up to {@link HUB_MODEL_SLOW_MS}, red past
|
|
7285
|
+
* it — that long is worth flagging, not merely dimming. `undefined` (nothing
|
|
7286
|
+
* measured yet, or a provider just switched) and a stale reading stay dim:
|
|
7287
|
+
* neither is an outage.
|
|
7288
|
+
*/
|
|
7289
|
+
function hubModelTone(ms, stale = false) {
|
|
7290
|
+
if (ms === void 0 || stale) return "dim";
|
|
7291
|
+
if (ms <= 5e3) return "ok";
|
|
7292
|
+
return ms <= 15e3 ? "warn" : "down";
|
|
7293
|
+
}
|
|
7294
|
+
/**
|
|
7145
7295
|
* Compact latency reading: whole milliseconds below a second, one-decimal
|
|
7146
7296
|
* seconds above — a probe that never answers must never read as `65400ms`.
|
|
7147
7297
|
*/
|
|
@@ -7162,9 +7312,11 @@ const EMPTY_MARK = "--";
|
|
|
7162
7312
|
*
|
|
7163
7313
|
* Values share one right edge through the same two-column engine the Session
|
|
7164
7314
|
* panel uses, so rows never jitter as numbers change width, and only the value
|
|
7165
|
-
* carries colour
|
|
7166
|
-
*
|
|
7167
|
-
*
|
|
7315
|
+
* carries colour. Two ladders, because two different things are timed: probe
|
|
7316
|
+
* rows are green ≤100ms, amber above it, red once a site stopped answering,
|
|
7317
|
+
* while the measured provider row (model first token) is green ≤5s, amber up to
|
|
7318
|
+
* 15s and red past that. The panel is a pure view: probing happens in the data
|
|
7319
|
+
* provider, which is what keeps a slow endpoint from ever touching a frame.
|
|
7168
7320
|
*/
|
|
7169
7321
|
var HubPanelContent = class {
|
|
7170
7322
|
ctx;
|
|
@@ -7776,10 +7928,6 @@ var LifecycleController = class LifecycleController {
|
|
|
7776
7928
|
* frame asked, so a closed sidebar produces no traffic. */
|
|
7777
7929
|
hubProbe = createHubProbe();
|
|
7778
7930
|
sidebarGitCacheWorkDir = "";
|
|
7779
|
-
/** Session start time for the sidebar uptime counter. Reset on session
|
|
7780
|
-
* switch so a resumed old session doesn't show the TUI process age. */
|
|
7781
|
-
sidebarSessionStartedAt = Date.now();
|
|
7782
|
-
sidebarSessionId;
|
|
7783
7931
|
/** Memo for the session-stats aggregation: recomputed only when the
|
|
7784
7932
|
* transcript length, compaction count or usage object changes. */
|
|
7785
7933
|
sidebarStatsMemo;
|
|
@@ -8047,10 +8195,6 @@ var LifecycleController = class LifecycleController {
|
|
|
8047
8195
|
};
|
|
8048
8196
|
}
|
|
8049
8197
|
readSidebarSessionStats(appState) {
|
|
8050
|
-
if (this.sidebarSessionId !== appState.sessionId) {
|
|
8051
|
-
this.sidebarSessionId = appState.sessionId;
|
|
8052
|
-
this.sidebarSessionStartedAt = Date.now();
|
|
8053
|
-
}
|
|
8054
8198
|
const entries = this.host.state.transcriptEntries;
|
|
8055
8199
|
const usage = appState.sessionUsage;
|
|
8056
8200
|
const subagentUsage = appState.subagentUsage;
|
|
@@ -8072,8 +8216,7 @@ var LifecycleController = class LifecycleController {
|
|
|
8072
8216
|
tokensTotal: LifecycleController.sumTokens(usage) + subagentOutput,
|
|
8073
8217
|
tokensInputCacheHit: usage.inputCacheRead,
|
|
8074
8218
|
tokensInputCacheMiss: usage.inputOther + usage.inputCacheCreation,
|
|
8075
|
-
tokensOutput: usage.output + subagentOutput
|
|
8076
|
-
startedAt: this.sidebarSessionStartedAt
|
|
8219
|
+
tokensOutput: usage.output + subagentOutput
|
|
8077
8220
|
};
|
|
8078
8221
|
this.sidebarStatsMemo = {
|
|
8079
8222
|
length: entries.length,
|
|
@@ -8367,7 +8510,7 @@ var FileMentionProvider = class {
|
|
|
8367
8510
|
const atPrefix = extractAtPrefix(textBeforeCursor);
|
|
8368
8511
|
if (!options.force && atPrefix === null && textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
|
|
8369
8512
|
const query = textBeforeCursor.slice(1);
|
|
8370
|
-
const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => item.value).slice(
|
|
8513
|
+
const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => !query.startsWith("skill:") && item.value.startsWith("skill:") ? item.value.slice(6) : item.value);
|
|
8371
8514
|
if (filtered.length === 0) return null;
|
|
8372
8515
|
return {
|
|
8373
8516
|
items: filtered,
|
|
@@ -9319,7 +9462,7 @@ const SIDE_PADDING = 1;
|
|
|
9319
9462
|
* Renders the stacked sidebar: every visible registered panel gets its own
|
|
9320
9463
|
* full box frame (┌─┐│└┘) with the panel title embedded in the top frame, one
|
|
9321
9464
|
* below the other. Panels render live each frame (their body reads the
|
|
9322
|
-
* current data snapshot), so time-based values (
|
|
9465
|
+
* current data snapshot), so time-based values (goal wall-clock)
|
|
9323
9466
|
* tick without rebuilding. The focused panel's title is bolded; focus is
|
|
9324
9467
|
* moved by the SidebarManager (`/sidebar next/prev`, Ctrl+X toggles).
|
|
9325
9468
|
* The whole frame is painted in the theme accent colour and the rendered
|
|
@@ -9333,9 +9476,10 @@ var SidebarContainer = class extends Container {
|
|
|
9333
9476
|
* only rebuilt when the visible panel set changes. */
|
|
9334
9477
|
bodies = /* @__PURE__ */ new Map();
|
|
9335
9478
|
builtStackKey = "";
|
|
9336
|
-
/** 1s heartbeat
|
|
9337
|
-
*
|
|
9338
|
-
*
|
|
9479
|
+
/** 1s heartbeat for the whole container lifetime: keeps live values (goal
|
|
9480
|
+
* wall-clock, gradient phase of busy agent slots) ticking while the sidebar
|
|
9481
|
+
* is open — the tick is a no-op when it is closed, and only `dispose()`
|
|
9482
|
+
* (TUI teardown) clears the interval. */
|
|
9339
9483
|
heartbeat;
|
|
9340
9484
|
constructor(manager, requestRender, colors) {
|
|
9341
9485
|
super();
|
|
@@ -11002,6 +11146,7 @@ var SessionManager$1 = class {
|
|
|
11002
11146
|
await this.unloadCurrentSession("switching session")?.close({ extractMemories: false });
|
|
11003
11147
|
this.host.session = session;
|
|
11004
11148
|
this.registerSessionHandlers(session);
|
|
11149
|
+
await syncDiagramCapabilityPrompt(session);
|
|
11005
11150
|
}
|
|
11006
11151
|
async syncRuntimeState(session = this.requireSession()) {
|
|
11007
11152
|
const status = await session.getStatus();
|
|
@@ -13373,6 +13518,7 @@ function createInitialAppState(input) {
|
|
|
13373
13518
|
isSwitchingSession: false,
|
|
13374
13519
|
streamingPhase: "idle",
|
|
13375
13520
|
streamingStartTime: 0,
|
|
13521
|
+
pendingUserAction: null,
|
|
13376
13522
|
theme: input.tuiConfig.theme,
|
|
13377
13523
|
version: input.version,
|
|
13378
13524
|
hasNewVersion: false,
|
|
@@ -13809,14 +13955,28 @@ var ScreamTUI = class {
|
|
|
13809
13955
|
patchLivePane(patch) {
|
|
13810
13956
|
if (!hasPatchChanges(this.state.livePane, patch)) return;
|
|
13811
13957
|
Object.assign(this.state.livePane, patch);
|
|
13958
|
+
this.syncPendingUserAction();
|
|
13812
13959
|
this.lifecycleController.updateActivityPane();
|
|
13813
13960
|
this.state.ui.requestRender();
|
|
13814
13961
|
}
|
|
13815
13962
|
resetLivePane() {
|
|
13816
13963
|
this.state.livePane = { ...INITIAL_LIVE_PANE };
|
|
13964
|
+
this.syncPendingUserAction();
|
|
13817
13965
|
this.lifecycleController.updateActivityPane();
|
|
13818
13966
|
this.state.ui.requestRender();
|
|
13819
13967
|
}
|
|
13968
|
+
/**
|
|
13969
|
+
* Mirror the live pane's pending panels onto `AppState.pendingUserAction` so
|
|
13970
|
+
* the footer status block can report "等待批准/等待回答" instead of claiming a
|
|
13971
|
+
* running phase while a panel is blocked on the user. Every mutation of
|
|
13972
|
+
* `livePane` goes through patchLivePane/resetLivePane, so this stays the
|
|
13973
|
+
* single writer of that field.
|
|
13974
|
+
*/
|
|
13975
|
+
syncPendingUserAction() {
|
|
13976
|
+
const { pendingApproval, pendingQuestion } = this.state.livePane;
|
|
13977
|
+
const next = pendingApproval !== null ? "approval" : pendingQuestion !== null ? "question" : null;
|
|
13978
|
+
if (next !== this.state.appState.pendingUserAction) this.setAppState({ pendingUserAction: next });
|
|
13979
|
+
}
|
|
13820
13980
|
requireSession() {
|
|
13821
13981
|
if (this.session === void 0) throw new Error(getNoActiveSessionMessage());
|
|
13822
13982
|
return this.session;
|
|
@@ -13893,7 +14053,7 @@ var ScreamTUI = class {
|
|
|
13893
14053
|
const card = new OnboardingCardComponent({
|
|
13894
14054
|
colors: this.state.theme.colors,
|
|
13895
14055
|
onConfigure: () => {
|
|
13896
|
-
import("./dispatch-
|
|
14056
|
+
import("./dispatch-CIGyjBDa.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
13897
14057
|
this.restoreEditor();
|
|
13898
14058
|
onDone?.();
|
|
13899
14059
|
});
|
|
@@ -13914,7 +14074,7 @@ var ScreamTUI = class {
|
|
|
13914
14074
|
if (this.state.startupState === "picker") return;
|
|
13915
14075
|
try {
|
|
13916
14076
|
await getKnowledgeStore();
|
|
13917
|
-
if (
|
|
14077
|
+
if (hasEmbeddingModelCache()) {
|
|
13918
14078
|
startManualEmbeddingDownload().catch(() => {});
|
|
13919
14079
|
return;
|
|
13920
14080
|
}
|
|
@@ -13929,8 +14089,9 @@ var ScreamTUI = class {
|
|
|
13929
14089
|
const spinner = this.showProgressSpinner(t("kw.embedding_downloading"));
|
|
13930
14090
|
let ok = false;
|
|
13931
14091
|
let error;
|
|
14092
|
+
let failureKind;
|
|
13932
14093
|
try {
|
|
13933
|
-
({ok, error} = await startManualEmbeddingDownload());
|
|
14094
|
+
({ok, error, failureKind} = await startManualEmbeddingDownload());
|
|
13934
14095
|
} catch (error) {
|
|
13935
14096
|
error = error instanceof Error ? error.message : String(error);
|
|
13936
14097
|
}
|
|
@@ -13940,7 +14101,7 @@ var ScreamTUI = class {
|
|
|
13940
14101
|
});
|
|
13941
14102
|
if (ok) this.showStatus(t("kw.embedding_ready"));
|
|
13942
14103
|
else {
|
|
13943
|
-
const detail =
|
|
14104
|
+
const detail = embeddingFailureDetail(failureKind, error);
|
|
13944
14105
|
this.showNotice(t("kw.embedding_failed"), detail);
|
|
13945
14106
|
}
|
|
13946
14107
|
})();
|
|
@@ -16175,13 +16336,16 @@ async function ensureSessionsDir(homeDir) {
|
|
|
16175
16336
|
const dir = getSessionsDir(homeDir);
|
|
16176
16337
|
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
|
|
16177
16338
|
}
|
|
16178
|
-
async function appendJournalLine(homeDir, sessionId, line) {
|
|
16339
|
+
async function appendJournalLine(homeDir, sessionId, line, throwOnError = false) {
|
|
16340
|
+
let writeError;
|
|
16179
16341
|
await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
|
|
16342
|
+
writeError = error;
|
|
16180
16343
|
log.warn("web: failed to persist journal line", {
|
|
16181
16344
|
sessionId,
|
|
16182
16345
|
error: String(error)
|
|
16183
16346
|
});
|
|
16184
16347
|
});
|
|
16348
|
+
if (throwOnError && writeError !== void 0) throw writeError;
|
|
16185
16349
|
}
|
|
16186
16350
|
let metaWriteSeq = 0;
|
|
16187
16351
|
async function saveMetadata(homeDir, meta) {
|
|
@@ -16266,16 +16430,22 @@ function isMessageBearingEntry(entry) {
|
|
|
16266
16430
|
*/
|
|
16267
16431
|
function contextHistoryToChatMessages(history) {
|
|
16268
16432
|
const out = [];
|
|
16433
|
+
let pendingAnchor = "other";
|
|
16269
16434
|
for (const message of history) {
|
|
16270
16435
|
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
16271
16436
|
if (isInternalMessage(message)) continue;
|
|
16272
|
-
|
|
16437
|
+
const anchor = classifyUndoAnchor(message.origin);
|
|
16438
|
+
if (message.role === "user") {
|
|
16439
|
+
pendingAnchor = anchor;
|
|
16440
|
+
if (anchor === "skill" || anchor === "other") continue;
|
|
16441
|
+
}
|
|
16273
16442
|
const text = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
|
|
16274
16443
|
if (text.length === 0) continue;
|
|
16275
16444
|
out.push({
|
|
16276
16445
|
role: message.role,
|
|
16277
16446
|
content: text,
|
|
16278
|
-
tools: []
|
|
16447
|
+
tools: [],
|
|
16448
|
+
undoAnchor: message.role === "assistant" ? pendingAnchor : anchor
|
|
16279
16449
|
});
|
|
16280
16450
|
}
|
|
16281
16451
|
return out;
|
|
@@ -16305,6 +16475,65 @@ function deriveTitle(messages) {
|
|
|
16305
16475
|
const text = firstUser.content.trim();
|
|
16306
16476
|
return text.length > 40 ? text.slice(0, 40) + "..." : text;
|
|
16307
16477
|
}
|
|
16478
|
+
/**
|
|
16479
|
+
* Remove complete user-turn blocks from a rendered transcript. The core
|
|
16480
|
+
* context owns the authoritative undo operation; the web journal is append
|
|
16481
|
+
* only, so a durable undo marker asks this projection to hide the same turns
|
|
16482
|
+
* when snapshots are rebuilt (including after a restart).
|
|
16483
|
+
*
|
|
16484
|
+
* `beforeSeq` is an inclusive cursor captured before the core mutation. It
|
|
16485
|
+
* also covers a user message that reserved the next sequence but never
|
|
16486
|
+
* emitted a turn event.
|
|
16487
|
+
*/
|
|
16488
|
+
function removeLastTurnsBefore(messages, count, beforeSeq) {
|
|
16489
|
+
if (count <= 0 || messages.length === 0) return;
|
|
16490
|
+
const firstFutureIndex = messages.findIndex((message) => typeof message.seq === "number" && message.seq > beforeSeq);
|
|
16491
|
+
const prefixLength = firstFutureIndex === -1 ? messages.length : firstFutureIndex;
|
|
16492
|
+
const prefix = messages.slice(0, prefixLength);
|
|
16493
|
+
const tail = messages.slice(prefixLength);
|
|
16494
|
+
let remaining = count;
|
|
16495
|
+
while (remaining > 0) {
|
|
16496
|
+
let anchor = -1;
|
|
16497
|
+
for (let i = prefix.length - 1; i >= 0; i -= 1) {
|
|
16498
|
+
const message = prefix[i];
|
|
16499
|
+
if (message?.role === "assistant" && message.undoAnchor === "skill") {
|
|
16500
|
+
anchor = i;
|
|
16501
|
+
while (anchor > 0) {
|
|
16502
|
+
const previous = prefix[anchor - 1];
|
|
16503
|
+
if (previous?.role !== "assistant" || previous.undoAnchor !== "skill") break;
|
|
16504
|
+
anchor -= 1;
|
|
16505
|
+
}
|
|
16506
|
+
break;
|
|
16507
|
+
}
|
|
16508
|
+
if (message?.role === "user") {
|
|
16509
|
+
anchor = i;
|
|
16510
|
+
break;
|
|
16511
|
+
}
|
|
16512
|
+
}
|
|
16513
|
+
if (anchor < 0) break;
|
|
16514
|
+
prefix.splice(anchor);
|
|
16515
|
+
remaining -= 1;
|
|
16516
|
+
}
|
|
16517
|
+
messages.splice(0, messages.length, ...prefix, ...tail);
|
|
16518
|
+
}
|
|
16519
|
+
function classifyUndoAnchor(origin) {
|
|
16520
|
+
if (origin === void 0 || origin === null) return "user";
|
|
16521
|
+
if (typeof origin !== "object") return "other";
|
|
16522
|
+
const kind = origin.kind;
|
|
16523
|
+
if (kind === "user") return "user";
|
|
16524
|
+
if (kind === "skill_activation" && origin.trigger === "user-slash") return "skill";
|
|
16525
|
+
return "other";
|
|
16526
|
+
}
|
|
16527
|
+
/** Match the core `/undo` anchor rule without importing agent-core internals. */
|
|
16528
|
+
function countUndoAnchors(history) {
|
|
16529
|
+
let count = 0;
|
|
16530
|
+
for (const message of history) {
|
|
16531
|
+
if (message.role !== "user") continue;
|
|
16532
|
+
const origin = message.origin;
|
|
16533
|
+
if (origin === void 0 || origin.kind === "user" || origin.kind === "skill_activation" && origin.trigger === "user-slash") count += 1;
|
|
16534
|
+
}
|
|
16535
|
+
return count;
|
|
16536
|
+
}
|
|
16308
16537
|
const execFileAsync = promisify(execFile);
|
|
16309
16538
|
/** Raw porcelain rows: repo-root-relative path + XY status. `getGitStatus`
|
|
16310
16539
|
* enriches these into full `GitFileChange` records. */
|
|
@@ -16567,6 +16796,8 @@ var WebSession = class {
|
|
|
16567
16796
|
heartbeatTimer = null;
|
|
16568
16797
|
unsubscribe = null;
|
|
16569
16798
|
busy = false;
|
|
16799
|
+
/** Prevent two concurrent REST `/undo` calls from mutating core history together. */
|
|
16800
|
+
undoInFlight = false;
|
|
16570
16801
|
/**
|
|
16571
16802
|
* In-flight turn count across all agents (main + subagents). Used by the
|
|
16572
16803
|
* idle-exit watchdog: a single boolean is not enough because a subagent's
|
|
@@ -16655,13 +16886,13 @@ var WebSession = class {
|
|
|
16655
16886
|
});
|
|
16656
16887
|
return entries;
|
|
16657
16888
|
}
|
|
16658
|
-
async persistEntry(entry) {
|
|
16889
|
+
async persistEntry(entry, throwOnError = false) {
|
|
16659
16890
|
if (!this.homeDir) return;
|
|
16660
16891
|
const line = JSON.stringify({
|
|
16661
16892
|
type: "journal",
|
|
16662
16893
|
...entry
|
|
16663
16894
|
});
|
|
16664
|
-
await appendJournalLine(this.homeDir, this.sessionId, line);
|
|
16895
|
+
await appendJournalLine(this.homeDir, this.sessionId, line, throwOnError);
|
|
16665
16896
|
}
|
|
16666
16897
|
async persistUserMessage(text, beforeSeq, clientMessageId) {
|
|
16667
16898
|
this.userMessages.push({
|
|
@@ -16718,6 +16949,9 @@ var WebSession = class {
|
|
|
16718
16949
|
* journal holds no message records.
|
|
16719
16950
|
*/
|
|
16720
16951
|
seededMessages = [];
|
|
16952
|
+
/** Undo markers already reflected by a core-history seed must not be applied twice. */
|
|
16953
|
+
seedCoveredUndoMarkers = /* @__PURE__ */ new Set();
|
|
16954
|
+
historySeeded = false;
|
|
16721
16955
|
/**
|
|
16722
16956
|
* Mark the journal untrustworthy (byte-interleaving damage, see
|
|
16723
16957
|
* journal-writer.ts).
|
|
@@ -16759,7 +16993,10 @@ var WebSession = class {
|
|
|
16759
16993
|
* monotonic with live events and consistent with the getMessagesOlder cursor.
|
|
16760
16994
|
*/
|
|
16761
16995
|
seedHistory(messages) {
|
|
16762
|
-
if (
|
|
16996
|
+
if (this.historySeeded) return;
|
|
16997
|
+
this.historySeeded = true;
|
|
16998
|
+
for (const entry of this.journal) if (entry.payload.type === "web.history.undone") this.seedCoveredUndoMarkers.add(entry.seq);
|
|
16999
|
+
if (messages.length === 0) return;
|
|
16763
17000
|
let seq = 1;
|
|
16764
17001
|
for (const message of messages) {
|
|
16765
17002
|
this.seededMessages.push({
|
|
@@ -16806,7 +17043,7 @@ var WebSession = class {
|
|
|
16806
17043
|
* clients. Used for server-owned reconstruction material (finalized
|
|
16807
17044
|
* assistant snapshots) that clients never consume as live events.
|
|
16808
17045
|
*/
|
|
16809
|
-
appendDurableSilent(payload) {
|
|
17046
|
+
appendDurableSilent(payload, throwOnPersistenceError = false) {
|
|
16810
17047
|
const entry = {
|
|
16811
17048
|
seq: this.nextSeq++,
|
|
16812
17049
|
epoch: this.epoch,
|
|
@@ -16814,8 +17051,12 @@ var WebSession = class {
|
|
|
16814
17051
|
payload
|
|
16815
17052
|
};
|
|
16816
17053
|
this.journal.push(entry);
|
|
16817
|
-
this.
|
|
16818
|
-
|
|
17054
|
+
const persisted = this.persistEntry(entry, throwOnPersistenceError);
|
|
17055
|
+
this.trackPending(persisted);
|
|
17056
|
+
return {
|
|
17057
|
+
entry,
|
|
17058
|
+
persisted
|
|
17059
|
+
};
|
|
16819
17060
|
}
|
|
16820
17061
|
subscribeEvents() {
|
|
16821
17062
|
if (!this.session) return;
|
|
@@ -17144,13 +17385,14 @@ var WebSession = class {
|
|
|
17144
17385
|
});
|
|
17145
17386
|
}
|
|
17146
17387
|
async handleCommand(ws, command, args, pendingMsgId) {
|
|
17147
|
-
const ok = (message) => {
|
|
17388
|
+
const ok = (message, extra) => {
|
|
17148
17389
|
this.broadcast({
|
|
17149
17390
|
type: "command_result",
|
|
17150
17391
|
command,
|
|
17151
17392
|
ok: true,
|
|
17152
17393
|
message,
|
|
17153
|
-
pendingMsgId
|
|
17394
|
+
pendingMsgId,
|
|
17395
|
+
...extra
|
|
17154
17396
|
}, false);
|
|
17155
17397
|
};
|
|
17156
17398
|
const fail = (message) => {
|
|
@@ -17186,9 +17428,10 @@ var WebSession = class {
|
|
|
17186
17428
|
}
|
|
17187
17429
|
return;
|
|
17188
17430
|
case "auto":
|
|
17431
|
+
case "ask":
|
|
17189
17432
|
case "yes":
|
|
17190
17433
|
case "bot": {
|
|
17191
|
-
const mode = command === "auto" ? "auto" : command === "bot" ? "bot" : "yolo";
|
|
17434
|
+
const mode = command === "auto" ? "auto" : command === "ask" ? "ask" : command === "bot" ? "bot" : "yolo";
|
|
17192
17435
|
try {
|
|
17193
17436
|
await this.session.setPermission(mode);
|
|
17194
17437
|
this.permission = mode;
|
|
@@ -17231,7 +17474,7 @@ var WebSession = class {
|
|
|
17231
17474
|
fail("Fork 失败:无法复制当前会话。");
|
|
17232
17475
|
return;
|
|
17233
17476
|
}
|
|
17234
|
-
ok(`会话已 fork,新会话 ID:${result.sessionId}
|
|
17477
|
+
ok(`会话已 fork,新会话 ID:${result.sessionId}`, { sessionId: result.sessionId });
|
|
17235
17478
|
} catch (error) {
|
|
17236
17479
|
fail(`Fork 失败:${errMsg(error)}`);
|
|
17237
17480
|
}
|
|
@@ -17342,6 +17585,7 @@ var WebSession = class {
|
|
|
17342
17585
|
planMode: false,
|
|
17343
17586
|
wolfpackMode: false,
|
|
17344
17587
|
rlmEnabled: false,
|
|
17588
|
+
rlmMaxDepth: null,
|
|
17345
17589
|
contextTokens: 0,
|
|
17346
17590
|
maxContextTokens: 0,
|
|
17347
17591
|
contextUsage: 0
|
|
@@ -17518,6 +17762,64 @@ var WebSession = class {
|
|
|
17518
17762
|
if (!this.session) throw new HttpError(409, "Session is archived (read-only)", ErrorCodes.SESSION_CLOSED);
|
|
17519
17763
|
return this.session;
|
|
17520
17764
|
}
|
|
17765
|
+
/**
|
|
17766
|
+
* Undo user turns in the core session and append a durable web projection
|
|
17767
|
+
* marker. The core RPC deliberately has no UI knowledge, so without the
|
|
17768
|
+
* marker the old append-only web journal would resurrect the removed turns
|
|
17769
|
+
* on the next snapshot or after a restart.
|
|
17770
|
+
*/
|
|
17771
|
+
async undoHistory(count) {
|
|
17772
|
+
if (!Number.isSafeInteger(count) || count < 1) throw new HttpError(400, "Undo count must be a positive integer.");
|
|
17773
|
+
if (this.busy) throw new HttpError(409, "Session is busy", ErrorCodes.TURN_AGENT_BUSY);
|
|
17774
|
+
const session = this.requireLiveSession();
|
|
17775
|
+
if (this.undoInFlight) throw new HttpError(409, "Undo already in progress");
|
|
17776
|
+
this.undoInFlight = true;
|
|
17777
|
+
try {
|
|
17778
|
+
const beforeSeq = this.nextSeq;
|
|
17779
|
+
let beforeAnchors = null;
|
|
17780
|
+
try {
|
|
17781
|
+
beforeAnchors = countUndoAnchors((await session.getContext()).history);
|
|
17782
|
+
} catch (error) {
|
|
17783
|
+
log.warn("web: failed to read context before undo", {
|
|
17784
|
+
sessionId: this.sessionId,
|
|
17785
|
+
error: String(error)
|
|
17786
|
+
});
|
|
17787
|
+
}
|
|
17788
|
+
await session.undoHistory(count);
|
|
17789
|
+
let removed = count;
|
|
17790
|
+
if (beforeAnchors !== null) try {
|
|
17791
|
+
const afterAnchors = countUndoAnchors((await session.getContext()).history);
|
|
17792
|
+
removed = Math.max(0, beforeAnchors - afterAnchors);
|
|
17793
|
+
} catch (error) {
|
|
17794
|
+
log.warn("web: failed to read context after undo", {
|
|
17795
|
+
sessionId: this.sessionId,
|
|
17796
|
+
error: String(error)
|
|
17797
|
+
});
|
|
17798
|
+
}
|
|
17799
|
+
if (removed <= 0) {
|
|
17800
|
+
await this.refreshStatus();
|
|
17801
|
+
this.broadcastStatus();
|
|
17802
|
+
return;
|
|
17803
|
+
}
|
|
17804
|
+
const { entry, persisted } = this.appendDurableSilent({
|
|
17805
|
+
type: "web.history.undone",
|
|
17806
|
+
count: removed,
|
|
17807
|
+
beforeSeq
|
|
17808
|
+
}, true);
|
|
17809
|
+
await persisted;
|
|
17810
|
+
this.broadcast({
|
|
17811
|
+
type: "event",
|
|
17812
|
+
seq: entry.seq,
|
|
17813
|
+
epoch: entry.epoch,
|
|
17814
|
+
payload: entry.payload
|
|
17815
|
+
}, false);
|
|
17816
|
+
this.trackPending(this.updateTitle());
|
|
17817
|
+
await this.refreshStatus();
|
|
17818
|
+
this.broadcastStatus();
|
|
17819
|
+
} finally {
|
|
17820
|
+
this.undoInFlight = false;
|
|
17821
|
+
}
|
|
17822
|
+
}
|
|
17521
17823
|
requireGoalSession(allowBusy = false) {
|
|
17522
17824
|
if (!this.session) throw new HttpError(409, "Session is archived (read-only)", ErrorCodes.SESSION_CLOSED);
|
|
17523
17825
|
if (!allowBusy && this.busy) throw new HttpError(409, "Session is busy", ErrorCodes.TURN_AGENT_BUSY);
|
|
@@ -17595,6 +17897,7 @@ var WebSession = class {
|
|
|
17595
17897
|
role: "assistant",
|
|
17596
17898
|
content: "",
|
|
17597
17899
|
tools: [],
|
|
17900
|
+
undoAnchor: classifyUndoAnchor(event.origin),
|
|
17598
17901
|
seq: entry.seq,
|
|
17599
17902
|
model: this.cachedStatus?.model,
|
|
17600
17903
|
turnStats: {
|
|
@@ -17648,13 +17951,20 @@ var WebSession = class {
|
|
|
17648
17951
|
break;
|
|
17649
17952
|
case "web.message.finalized": {
|
|
17650
17953
|
const finalized = event.message;
|
|
17651
|
-
|
|
17954
|
+
const lastSkeletonSeq = messages.length > 0 && messages.at(-1)?.role === "assistant" ? messages.at(-1).seq : entry.seq;
|
|
17955
|
+
if (finalized.undoAnchor === void 0 && messages.at(-1)?.role === "assistant") finalized.undoAnchor = messages.at(-1).undoAnchor;
|
|
17956
|
+
finalized.seq = lastSkeletonSeq;
|
|
17652
17957
|
if (messages.length > 0 && messages.at(-1)?.role === "assistant") messages[messages.length - 1] = finalized;
|
|
17653
17958
|
else messages.push(finalized);
|
|
17654
17959
|
currentAssistant = null;
|
|
17655
17960
|
turnFinalized = true;
|
|
17656
17961
|
break;
|
|
17657
17962
|
}
|
|
17963
|
+
case "web.history.undone":
|
|
17964
|
+
if (!this.seedCoveredUndoMarkers.has(entry.seq)) removeLastTurnsBefore(messages, event.count, event.beforeSeq);
|
|
17965
|
+
currentAssistant = null;
|
|
17966
|
+
turnFinalized = false;
|
|
17967
|
+
break;
|
|
17658
17968
|
case "turn.ended":
|
|
17659
17969
|
if (event.reason === "failed" && currentAssistant) currentAssistant.isError = true;
|
|
17660
17970
|
if (currentAssistant && !turnFinalized && currentAssistant.content === "") currentAssistant.degraded = true;
|
|
@@ -17666,7 +17976,10 @@ var WebSession = class {
|
|
|
17666
17976
|
}
|
|
17667
17977
|
for (let i = userIndex; i < this.userMessages.length; i++) {
|
|
17668
17978
|
const item = this.userMessages[i];
|
|
17669
|
-
if (item !== void 0) messages.push(
|
|
17979
|
+
if (item !== void 0) messages.push({
|
|
17980
|
+
...item.msg,
|
|
17981
|
+
seq: item.beforeSeq
|
|
17982
|
+
});
|
|
17670
17983
|
}
|
|
17671
17984
|
return messages;
|
|
17672
17985
|
}
|
|
@@ -18263,7 +18576,9 @@ var SessionManager = class {
|
|
|
18263
18576
|
await ws.switchRlm(enabled, maxDepth);
|
|
18264
18577
|
}
|
|
18265
18578
|
async undoHistory(sessionId, count) {
|
|
18266
|
-
|
|
18579
|
+
const ws = this.sessions.get(sessionId);
|
|
18580
|
+
if (!ws) throw new HttpError(404, "Session not found", ErrorCodes.SESSION_NOT_FOUND);
|
|
18581
|
+
await ws.undoHistory(count);
|
|
18267
18582
|
}
|
|
18268
18583
|
async compact(sessionId, instruction) {
|
|
18269
18584
|
await this.getLiveSession(sessionId).compact(instruction ? { instruction } : {});
|
|
@@ -18433,16 +18748,32 @@ async function handleSessionDataRoutes(req, res, url, method, resolveSession, wo
|
|
|
18433
18748
|
}
|
|
18434
18749
|
return true;
|
|
18435
18750
|
}
|
|
18436
|
-
|
|
18437
|
-
|
|
18438
|
-
|
|
18439
|
-
|
|
18440
|
-
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
|
|
18751
|
+
const gitPath = url.split("?")[0];
|
|
18752
|
+
const isGitStatusRoute = gitPath === `${API_PREFIX}/git/status` && method === "GET";
|
|
18753
|
+
if (isGitStatusRoute || gitPath === `${API_PREFIX}/git/diff` && method === "GET") {
|
|
18754
|
+
const gitQuery = new URLSearchParams(url.split("?")[1] ?? "");
|
|
18755
|
+
const requestedGitSessionId = gitQuery.get("sessionId");
|
|
18756
|
+
let gitWorkDir = workDir;
|
|
18757
|
+
if (requestedGitSessionId !== null) {
|
|
18758
|
+
const gitSession = resolveSession(requestedGitSessionId);
|
|
18759
|
+
if (!gitSession) {
|
|
18760
|
+
sendJson(res, 404, {
|
|
18761
|
+
code: ErrorCodes.SESSION_NOT_FOUND,
|
|
18762
|
+
message: "Session not found"
|
|
18763
|
+
});
|
|
18764
|
+
return true;
|
|
18765
|
+
}
|
|
18766
|
+
gitWorkDir = gitSession.workDir;
|
|
18767
|
+
}
|
|
18768
|
+
if (isGitStatusRoute) {
|
|
18769
|
+
const gs = await getGitStatus(gitWorkDir);
|
|
18770
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
18771
|
+
res.end(JSON.stringify(gs));
|
|
18772
|
+
return true;
|
|
18773
|
+
}
|
|
18774
|
+
const relPath = gitQuery.get("path") ?? "";
|
|
18444
18775
|
try {
|
|
18445
|
-
const result = await getGitFileDiff(
|
|
18776
|
+
const result = await getGitFileDiff(gitWorkDir, relPath);
|
|
18446
18777
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
18447
18778
|
res.end(JSON.stringify({
|
|
18448
18779
|
path: relPath,
|
|
@@ -18653,6 +18984,7 @@ async function handleSessionControlRoutes(req, res, url, method, manager) {
|
|
|
18653
18984
|
return false;
|
|
18654
18985
|
}
|
|
18655
18986
|
async function handleResourceRoutes(req, res, url, method, manager) {
|
|
18987
|
+
const routePath = url.split("?")[0] ?? url;
|
|
18656
18988
|
const skillsMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/skills$`).exec(url);
|
|
18657
18989
|
if (skillsMatch && method === "GET") {
|
|
18658
18990
|
try {
|
|
@@ -18840,7 +19172,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
|
|
|
18840
19172
|
}
|
|
18841
19173
|
return true;
|
|
18842
19174
|
}
|
|
18843
|
-
const tasksMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks$`).exec(
|
|
19175
|
+
const tasksMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks$`).exec(routePath);
|
|
18844
19176
|
if (tasksMatch && method === "GET") {
|
|
18845
19177
|
try {
|
|
18846
19178
|
const query = new URLSearchParams(url.split("?")[1] ?? "");
|
|
@@ -18852,7 +19184,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
|
|
|
18852
19184
|
}
|
|
18853
19185
|
return true;
|
|
18854
19186
|
}
|
|
18855
|
-
const taskOutputMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/output$`).exec(
|
|
19187
|
+
const taskOutputMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/output$`).exec(routePath);
|
|
18856
19188
|
if (taskOutputMatch && method === "GET") {
|
|
18857
19189
|
try {
|
|
18858
19190
|
const query = new URLSearchParams(url.split("?")[1] ?? "");
|
|
@@ -18863,7 +19195,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
|
|
|
18863
19195
|
}
|
|
18864
19196
|
return true;
|
|
18865
19197
|
}
|
|
18866
|
-
const taskStopMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/stop$`).exec(
|
|
19198
|
+
const taskStopMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/stop$`).exec(routePath);
|
|
18867
19199
|
if (taskStopMatch && method === "POST") {
|
|
18868
19200
|
try {
|
|
18869
19201
|
const body = await readJsonBody(req);
|