scream-code 0.16.6 → 0.16.8
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-D9oJ-Y8W.mjs → app-GeQQHNLx.mjs} +335 -88
- package/dist/{dispatch-fjwbqaYI.mjs → dispatch-CNitkOqX.mjs} +1934 -2022
- package/dist/{dispatch-dDbUBD7J.mjs → dispatch-Du8ddILK.mjs} +1 -1
- 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/src-CXmZg1VT.mjs +4115 -0
- package/dist/{src-h6ZExh4A.mjs → src-DIAKpkXO.mjs} +1 -1
- package/dist/{text-input-dialog-DBlx8pMR.mjs → text-input-dialog-6v2667rm.mjs} +140 -6
- package/dist/{text-input-dialog-Cj0egbsU.mjs → text-input-dialog-DDUarM0_.mjs} +1 -1
- 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
- package/dist/src-tDEINaMV.mjs +0 -2249
|
@@ -4,8 +4,9 @@ import { dirname as __cjsShimDirname } from 'node:path';
|
|
|
4
4
|
const __filename = __cjsShimFileURLToPath(import.meta.url);
|
|
5
5
|
const __dirname = __cjsShimDirname(__filename);
|
|
6
6
|
import "./suppress-sqlite-warning-C2VB0doZ.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import { a as
|
|
7
|
+
import { k as MemoryMemoStore } from "./src-CXmZg1VT.mjs";
|
|
8
|
+
import { $n as encodeWorkDirKey, $t as lerpGradient, A as highlightLines, An as embeddingFailureDetail, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as detectShellEnvironment, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getLlmNotSetMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as sortSlashCommands, Dt as createEditorTheme, E as UserMessageComponent, En as BUILTIN_SLASH_COMMANDS, Et as resolveThemeSync, F as StatusMessageComponent, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_COMMAND_NAME, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getInputHistoryFile, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiConfigParseError, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as PRODUCT_NAME, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_UI_MODE, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as TuiLikePreferencesSchema, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as hasEmbeddingModelCache, Mt as createThemeStyles, N as ThinkingComponent, Nn as startManualEmbeddingDownload, Nt as contrastTextHex, O as renderDiffLines, On as isExperimentalFlagEnabled, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as ScreamHarness, Qt as FooterComponent, R as SkillActivationComponent, Rn as loadTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getCtrlDHint, St as supportsBalance, Tn as buildSkillSlashCommands, U as getSharedSpeedTracker, Un as getLogDir, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getDataDir, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as fetchCatalog, Xt as isBusy, Y as resetBreathingClock, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as saveCatalogCache, Zt as isStreaming, _n as EXIT_CONFIRM_WINDOW_MS, a as syncDiagramCapabilityPrompt, an as printableChar, ar as resolveScreamHome, at as toTerminalHyperlink, b as readUpdateCache, bn as TIP_ROTATION_INTERVAL_MS, c as disposeChildren, cn as appendStreamingArgsPreview, cr as resolveGlobalLogPath, d as isPlanExpandable, dn as isTodoItemShape, dr as ErrorCodes, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as SCREAM_ERROR_INFO, ft as handleFusionPlanCommand, gn as EMPTY_SESSION_HINT_URL, hn as truncateErrorMessage, i as buildRoleAdditionalText, it as isInternalMessage, j as langFromPath, jn as getKnowledgeStore, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as setExperimentalFlags, kt as isEmptySessionHintDismissed, l as hasDispose, ln as argsRecord, lr as isScreamError, mn as stringValue, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as flushDiagnosticLogs, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, q as BREATHE_CYCLE_MS, qn as CLI_USER_AGENT_PRODUCT, qt as TERMINAL_FOCUS_OUT, r as openUrl, rr as removeSessionIndexEntry, sn as appendSessionHeaderHint, sr as log, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as isOrphanedToolCallError, v as selectUpdateTarget, vn as MAIN_AGENT_ID, w as getDaemonInstructions, wn as getNoActiveSessionMessage, x as appendJsonlLine, xn as getCtrlCHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as SESSION_TIPS, z as AssistantMessageComponent, zn as saveTuiConfig, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-CNitkOqX.mjs";
|
|
9
|
+
import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-6v2667rm.mjs";
|
|
9
10
|
import { createRequire } from "node:module";
|
|
10
11
|
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
11
12
|
import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
@@ -32,7 +33,7 @@ function optionalBuildString(value) {
|
|
|
32
33
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
33
34
|
}
|
|
34
35
|
const SCREAM_BUILD_INFO = {
|
|
35
|
-
version: optionalBuildString("0.16.
|
|
36
|
+
version: optionalBuildString("0.16.8"),
|
|
36
37
|
channel: optionalBuildString(""),
|
|
37
38
|
commit: optionalBuildString(""),
|
|
38
39
|
buildTarget: optionalBuildString("darwin-arm64")
|
|
@@ -1877,7 +1878,7 @@ var EditorKeyboardController = class {
|
|
|
1877
1878
|
if (media === null) return;
|
|
1878
1879
|
const meta = parseImageMeta(media.bytes);
|
|
1879
1880
|
if (meta === null) return;
|
|
1880
|
-
const attachment = this.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height);
|
|
1881
|
+
const attachment = this.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height, path);
|
|
1881
1882
|
this.host.state.editor.insertTextAtCursor?.(`${attachment.placeholder} `);
|
|
1882
1883
|
this.host.state.ui.requestRender();
|
|
1883
1884
|
}
|
|
@@ -2363,15 +2364,15 @@ function formatStepDebugTiming(input) {
|
|
|
2363
2364
|
const latency = input.llmFirstTokenLatencyMs;
|
|
2364
2365
|
const streamMs = input.llmStreamDurationMs;
|
|
2365
2366
|
if (latency === void 0 || streamMs === void 0) return void 0;
|
|
2366
|
-
const parts = [`TTFT: ${formatDuration
|
|
2367
|
+
const parts = [`TTFT: ${formatDuration(latency)}`];
|
|
2367
2368
|
const outputTokens = input.usage?.output;
|
|
2368
2369
|
if (outputTokens !== void 0 && outputTokens > 0 && streamMs > 0) {
|
|
2369
2370
|
const tps = (outputTokens / (streamMs / 1e3)).toFixed(1);
|
|
2370
|
-
parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration
|
|
2371
|
+
parts.push(`TPS: ${tps} tok/s (${outputTokens} tokens in ${formatDuration(streamMs)})`);
|
|
2371
2372
|
}
|
|
2372
2373
|
return `[Debug] ${parts.join(" | ")}`;
|
|
2373
2374
|
}
|
|
2374
|
-
function formatDuration
|
|
2375
|
+
function formatDuration(ms) {
|
|
2375
2376
|
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
2376
2377
|
return `${(ms / 1e3).toFixed(1)}s`;
|
|
2377
2378
|
}
|
|
@@ -3164,7 +3165,8 @@ var SessionEventHandler = class {
|
|
|
3164
3165
|
this.host.streamingUI.resetToolUi();
|
|
3165
3166
|
this.host.streamingUI.finalizeLiveTextBuffers();
|
|
3166
3167
|
this.host.streamingUI.endActivityGroup();
|
|
3167
|
-
|
|
3168
|
+
const shown = appendSessionHeaderHint(`[${event.code}] ${event.message}`, (name) => t("error.session_header_hint", { name }));
|
|
3169
|
+
this.host.showError(shown);
|
|
3168
3170
|
if (event.code === "turn.agent_busy" && !hadActiveTurn && this.host.state.appState.streamingPhase !== "idle") {
|
|
3169
3171
|
this.host.setAppState({ streamingPhase: "idle" });
|
|
3170
3172
|
this.host.resetLivePane();
|
|
@@ -4621,6 +4623,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4621
4623
|
completeToolResult(toolCallId, result) {
|
|
4622
4624
|
const matchedCall = this._activeToolCalls.get(toolCallId);
|
|
4623
4625
|
if (matchedCall !== void 0) this.onToolCallEnd(toolCallId, result);
|
|
4626
|
+
else if (this._pendingToolComponents.has(toolCallId)) this.onToolCallEnd(toolCallId, result);
|
|
4624
4627
|
this._activeToolCalls.delete(toolCallId);
|
|
4625
4628
|
this._streamingToolCallArguments.delete(toolCallId);
|
|
4626
4629
|
return matchedCall;
|
|
@@ -4907,6 +4910,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
4907
4910
|
this._assistantDraft = "";
|
|
4908
4911
|
this._shownAssistantLength = 0;
|
|
4909
4912
|
this.clearFlushTimerIfIdle();
|
|
4913
|
+
this._streamingBlock?.component.setStreaming(false);
|
|
4910
4914
|
this._streamingBlock = null;
|
|
4911
4915
|
this._thinkingDraft = "";
|
|
4912
4916
|
this.lastDeltaAt = void 0;
|
|
@@ -5010,6 +5014,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
5010
5014
|
const block = this._streamingBlock;
|
|
5011
5015
|
if (block !== null) {
|
|
5012
5016
|
block.entry.content = fullText;
|
|
5017
|
+
block.component.setStreaming(true);
|
|
5013
5018
|
block.component.updateContent(fullText);
|
|
5014
5019
|
this.host.state.ui.requestRender();
|
|
5015
5020
|
}
|
|
@@ -5019,6 +5024,7 @@ var StreamingUIController = class StreamingUIController {
|
|
|
5019
5024
|
const block = this._streamingBlock;
|
|
5020
5025
|
this._streamingBlock = null;
|
|
5021
5026
|
if (block !== null) {
|
|
5027
|
+
block.component.setStreaming(false);
|
|
5022
5028
|
this.host.transcriptController.unmarkPending(block.component);
|
|
5023
5029
|
this.host.transcriptController.commit();
|
|
5024
5030
|
}
|
|
@@ -6243,10 +6249,28 @@ function cronDetail(data) {
|
|
|
6243
6249
|
}
|
|
6244
6250
|
//#endregion
|
|
6245
6251
|
//#region src/tui/utils/image-attachment-store.ts
|
|
6252
|
+
/**
|
|
6253
|
+
* Registry for media pasted into the input box.
|
|
6254
|
+
*
|
|
6255
|
+
* Each paste produces an `ImageAttachment` with an auto-incrementing id
|
|
6256
|
+
* or `VideoAttachment` with a human-readable placeholder (`[image #1
|
|
6257
|
+
* (640×480)]` / `[video #2 sample.mov]`). The placeholder is what the
|
|
6258
|
+
* user sees in the input field; on submit, `extractMediaAttachments`
|
|
6259
|
+
* walks the text and expands image placeholders to image content parts
|
|
6260
|
+
* and video placeholders to file-path tags for `ReadMediaFile`.
|
|
6261
|
+
*
|
|
6262
|
+
* Scope is per-`ScreamTUI` instance. Reloads (`/new`, `/clear`,
|
|
6263
|
+
* session switch) call `clear()` so ids restart from 1 and stale
|
|
6264
|
+
* prompt attachments are dropped. We intentionally do NOT persist
|
|
6265
|
+
* attachments across sessions — coding-agent doesn't either, and
|
|
6266
|
+
* `--resume` wouldn't know how to materialize the files anyway.
|
|
6267
|
+
*/
|
|
6268
|
+
/** Temp-file basename prefix used by path-mode image materialization. */
|
|
6269
|
+
const TEMP_ATTACHMENT_PREFIX = "scream-attachment-";
|
|
6246
6270
|
var ImageAttachmentStore = class {
|
|
6247
6271
|
nextId = 1;
|
|
6248
6272
|
byId = /* @__PURE__ */ new Map();
|
|
6249
|
-
addImage(bytes, mime, width, height) {
|
|
6273
|
+
addImage(bytes, mime, width, height, sourcePath) {
|
|
6250
6274
|
const id = this.nextId;
|
|
6251
6275
|
this.nextId += 1;
|
|
6252
6276
|
const attachment = {
|
|
@@ -6256,7 +6280,8 @@ var ImageAttachmentStore = class {
|
|
|
6256
6280
|
mime,
|
|
6257
6281
|
width,
|
|
6258
6282
|
height,
|
|
6259
|
-
placeholder: formatPlaceholder(id, width, height)
|
|
6283
|
+
placeholder: formatPlaceholder(id, width, height),
|
|
6284
|
+
...sourcePath !== void 0 && sourcePath.length > 0 ? { sourcePath } : {}
|
|
6260
6285
|
};
|
|
6261
6286
|
this.byId.set(id, attachment);
|
|
6262
6287
|
return attachment;
|
|
@@ -6282,6 +6307,14 @@ var ImageAttachmentStore = class {
|
|
|
6282
6307
|
return this.byId.get(id);
|
|
6283
6308
|
}
|
|
6284
6309
|
clear() {
|
|
6310
|
+
const tempBase = join(tmpdir(), TEMP_ATTACHMENT_PREFIX);
|
|
6311
|
+
for (const attachment of this.byId.values()) {
|
|
6312
|
+
if (attachment.kind !== "image") continue;
|
|
6313
|
+
const path = attachment.sourcePath;
|
|
6314
|
+
if (path !== void 0 && path.startsWith(tempBase)) try {
|
|
6315
|
+
unlinkSync(path);
|
|
6316
|
+
} catch {}
|
|
6317
|
+
}
|
|
6285
6318
|
this.byId.clear();
|
|
6286
6319
|
this.nextId = 1;
|
|
6287
6320
|
}
|
|
@@ -6879,20 +6912,6 @@ const gitPanel = {
|
|
|
6879
6912
|
};
|
|
6880
6913
|
//#endregion
|
|
6881
6914
|
//#region src/tui/components/sidebar/panels/session-panel.ts
|
|
6882
|
-
function formatDuration(ms) {
|
|
6883
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
6884
|
-
if (getLocale() === "zh") {
|
|
6885
|
-
if (totalSeconds < 60) return `${totalSeconds}秒`;
|
|
6886
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
6887
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
6888
|
-
return hours > 0 ? `${hours}小时${minutes}分` : `${minutes}分钟`;
|
|
6889
|
-
}
|
|
6890
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
6891
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
6892
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
6893
|
-
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
6894
|
-
return `${minutes}m`;
|
|
6895
|
-
}
|
|
6896
6915
|
/** 1,234 → "1,234" via the platform locale grouping. */
|
|
6897
6916
|
function formatCount$1(n) {
|
|
6898
6917
|
return n.toLocaleString("en-US");
|
|
@@ -6918,8 +6937,8 @@ function gridFits(rows, width) {
|
|
|
6918
6937
|
/**
|
|
6919
6938
|
* Session summary, live-rendered each frame as a strict two-column table:
|
|
6920
6939
|
* one field per row, labels sharing a left column, values sharing a right
|
|
6921
|
-
* edge.
|
|
6922
|
-
*
|
|
6940
|
+
* edge. Every value comes from the snapshot in getData(); nothing here is
|
|
6941
|
+
* derived from wall-clock time.
|
|
6923
6942
|
*/
|
|
6924
6943
|
var SessionPanelContent = class {
|
|
6925
6944
|
ctx;
|
|
@@ -6934,7 +6953,6 @@ var SessionPanelContent = class {
|
|
|
6934
6953
|
if (stats === void 0) return [chalk.hex(this.colors.textDim)(t("sidebar.session_empty"))];
|
|
6935
6954
|
const label = (s) => chalk.hex(this.colors.textDim)(s);
|
|
6936
6955
|
const value = (s) => chalk.hex(this.colors.text)(s);
|
|
6937
|
-
const now = Date.now();
|
|
6938
6956
|
const tokenRows = [
|
|
6939
6957
|
[t("sidebar.tokens"), stats.tokensTotal],
|
|
6940
6958
|
[t("sidebar.tokens_cache_hit"), stats.tokensInputCacheHit],
|
|
@@ -6942,7 +6960,6 @@ var SessionPanelContent = class {
|
|
|
6942
6960
|
[t("sidebar.tokens_output"), stats.tokensOutput]
|
|
6943
6961
|
];
|
|
6944
6962
|
const countRows = [
|
|
6945
|
-
[t("sidebar.uptime"), formatDuration(now - stats.startedAt)],
|
|
6946
6963
|
[t("sidebar.turns"), formatCount$1(stats.turns)],
|
|
6947
6964
|
[t("sidebar.tools"), formatCount$1(stats.toolCalls)],
|
|
6948
6965
|
[t("sidebar.calls"), formatCount$1(stats.apiCalls)],
|
|
@@ -6989,8 +7006,8 @@ function statusText(status) {
|
|
|
6989
7006
|
}
|
|
6990
7007
|
}
|
|
6991
7008
|
/**
|
|
6992
|
-
* Fixed
|
|
6993
|
-
* custom types appended in spawn order). Live-rendered each frame from the
|
|
7009
|
+
* Fixed default-slot subagent panel: one row per default subagent type (plus
|
|
7010
|
+
* any custom types appended in spawn order). Live-rendered each frame from the
|
|
6994
7011
|
* current snapshot; idle slots stay greyed out.
|
|
6995
7012
|
*/
|
|
6996
7013
|
var AgentsPanelContent = class {
|
|
@@ -7013,7 +7030,7 @@ var AgentsPanelContent = class {
|
|
|
7013
7030
|
renderSlot(cell, slot, index, width) {
|
|
7014
7031
|
const dim = (s) => chalk.hex(this.colors.textDim)(s);
|
|
7015
7032
|
const idle = slot.status === "idle";
|
|
7016
|
-
const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .
|
|
7033
|
+
const phase = (Date.now() % AGENT_GRADIENT_CYCLE_MS / AGENT_GRADIENT_CYCLE_MS + index * .6180339887498949) % 1;
|
|
7017
7034
|
const active = slot.status === "working" || slot.status === "outputting" ? chalk.hex(lerpGradient(phase)).bold : void 0;
|
|
7018
7035
|
const marker = padLabel((active ?? chalk.hex(this.statusColor(slot.status)))(idle ? "○" : "●"), 2);
|
|
7019
7036
|
const label = (idle ? dim : chalk.hex(this.colors.textStrong))(cell.label);
|
|
@@ -7273,23 +7290,38 @@ function toneFor(site, stale) {
|
|
|
7273
7290
|
/**
|
|
7274
7291
|
* Merge the measured provider reading with the probe samples into Hub rows.
|
|
7275
7292
|
* The provider row always leads (it answers "is the model I'm using reachable"),
|
|
7276
|
-
*
|
|
7277
|
-
*
|
|
7293
|
+
* it staleness-checks against its own budget: a long idle stretch means nothing
|
|
7294
|
+
* was asked, not that the network died — and it is coloured on its own ladder
|
|
7295
|
+
* (see {@link hubModelTone}), not on the probe bar.
|
|
7278
7296
|
*/
|
|
7279
7297
|
function buildHubSamples(snapshot, provider, now = Date.now()) {
|
|
7280
7298
|
const ageMs = provider.sampledAt === void 0 ? 0 : now - provider.sampledAt;
|
|
7281
7299
|
return [{
|
|
7282
7300
|
id: HUB_MODEL_ROW_ID,
|
|
7283
7301
|
ms: provider.ms,
|
|
7284
|
-
tone:
|
|
7302
|
+
tone: hubModelTone(provider.ms, ageMs >= HUB_MODEL_STALE_MS)
|
|
7285
7303
|
}, ...snapshot.samples];
|
|
7286
7304
|
}
|
|
7287
|
-
/**
|
|
7305
|
+
/** Tone rule for the probed endpoint rows: green ≤{@link HUB_GOOD_MS}, amber above. */
|
|
7288
7306
|
function hubLatencyTone(ms, stale = false) {
|
|
7289
7307
|
if (ms === void 0 || stale) return "dim";
|
|
7290
7308
|
return ms <= 100 ? "ok" : "warn";
|
|
7291
7309
|
}
|
|
7292
7310
|
/**
|
|
7311
|
+
* Tone rule for the measured provider row (model first token), deliberately
|
|
7312
|
+
* separate from the probe ladder: a first token and a HEAD request are not
|
|
7313
|
+
* comparable, and one shared bar would call every healthy answer slow. Green
|
|
7314
|
+
* ≤{@link HUB_MODEL_GOOD_MS}, amber up to {@link HUB_MODEL_SLOW_MS}, red past
|
|
7315
|
+
* it — that long is worth flagging, not merely dimming. `undefined` (nothing
|
|
7316
|
+
* measured yet, or a provider just switched) and a stale reading stay dim:
|
|
7317
|
+
* neither is an outage.
|
|
7318
|
+
*/
|
|
7319
|
+
function hubModelTone(ms, stale = false) {
|
|
7320
|
+
if (ms === void 0 || stale) return "dim";
|
|
7321
|
+
if (ms <= 5e3) return "ok";
|
|
7322
|
+
return ms <= 15e3 ? "warn" : "down";
|
|
7323
|
+
}
|
|
7324
|
+
/**
|
|
7293
7325
|
* Compact latency reading: whole milliseconds below a second, one-decimal
|
|
7294
7326
|
* seconds above — a probe that never answers must never read as `65400ms`.
|
|
7295
7327
|
*/
|
|
@@ -7310,9 +7342,11 @@ const EMPTY_MARK = "--";
|
|
|
7310
7342
|
*
|
|
7311
7343
|
* Values share one right edge through the same two-column engine the Session
|
|
7312
7344
|
* panel uses, so rows never jitter as numbers change width, and only the value
|
|
7313
|
-
* carries colour
|
|
7314
|
-
*
|
|
7315
|
-
*
|
|
7345
|
+
* carries colour. Two ladders, because two different things are timed: probe
|
|
7346
|
+
* rows are green ≤100ms, amber above it, red once a site stopped answering,
|
|
7347
|
+
* while the measured provider row (model first token) is green ≤5s, amber up to
|
|
7348
|
+
* 15s and red past that. The panel is a pure view: probing happens in the data
|
|
7349
|
+
* provider, which is what keeps a slow endpoint from ever touching a frame.
|
|
7316
7350
|
*/
|
|
7317
7351
|
var HubPanelContent = class {
|
|
7318
7352
|
ctx;
|
|
@@ -7924,10 +7958,6 @@ var LifecycleController = class LifecycleController {
|
|
|
7924
7958
|
* frame asked, so a closed sidebar produces no traffic. */
|
|
7925
7959
|
hubProbe = createHubProbe();
|
|
7926
7960
|
sidebarGitCacheWorkDir = "";
|
|
7927
|
-
/** Session start time for the sidebar uptime counter. Reset on session
|
|
7928
|
-
* switch so a resumed old session doesn't show the TUI process age. */
|
|
7929
|
-
sidebarSessionStartedAt = Date.now();
|
|
7930
|
-
sidebarSessionId;
|
|
7931
7961
|
/** Memo for the session-stats aggregation: recomputed only when the
|
|
7932
7962
|
* transcript length, compaction count or usage object changes. */
|
|
7933
7963
|
sidebarStatsMemo;
|
|
@@ -8195,10 +8225,6 @@ var LifecycleController = class LifecycleController {
|
|
|
8195
8225
|
};
|
|
8196
8226
|
}
|
|
8197
8227
|
readSidebarSessionStats(appState) {
|
|
8198
|
-
if (this.sidebarSessionId !== appState.sessionId) {
|
|
8199
|
-
this.sidebarSessionId = appState.sessionId;
|
|
8200
|
-
this.sidebarSessionStartedAt = Date.now();
|
|
8201
|
-
}
|
|
8202
8228
|
const entries = this.host.state.transcriptEntries;
|
|
8203
8229
|
const usage = appState.sessionUsage;
|
|
8204
8230
|
const subagentUsage = appState.subagentUsage;
|
|
@@ -8220,8 +8246,7 @@ var LifecycleController = class LifecycleController {
|
|
|
8220
8246
|
tokensTotal: LifecycleController.sumTokens(usage) + subagentOutput,
|
|
8221
8247
|
tokensInputCacheHit: usage.inputCacheRead,
|
|
8222
8248
|
tokensInputCacheMiss: usage.inputOther + usage.inputCacheCreation,
|
|
8223
|
-
tokensOutput: usage.output + subagentOutput
|
|
8224
|
-
startedAt: this.sidebarSessionStartedAt
|
|
8249
|
+
tokensOutput: usage.output + subagentOutput
|
|
8225
8250
|
};
|
|
8226
8251
|
this.sidebarStatsMemo = {
|
|
8227
8252
|
length: entries.length,
|
|
@@ -8515,7 +8540,7 @@ var FileMentionProvider = class {
|
|
|
8515
8540
|
const atPrefix = extractAtPrefix(textBeforeCursor);
|
|
8516
8541
|
if (!options.force && atPrefix === null && textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
|
|
8517
8542
|
const query = textBeforeCursor.slice(1);
|
|
8518
|
-
const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => item.value).slice(
|
|
8543
|
+
const filtered = fuzzyFilter(this.slashCommandItems, query, (item) => !query.startsWith("skill:") && item.value.startsWith("skill:") ? item.value.slice(6) : item.value);
|
|
8519
8544
|
if (filtered.length === 0) return null;
|
|
8520
8545
|
return {
|
|
8521
8546
|
items: filtered,
|
|
@@ -8693,8 +8718,25 @@ var QueuePaneComponent = class extends Container {
|
|
|
8693
8718
|
};
|
|
8694
8719
|
//#endregion
|
|
8695
8720
|
//#region src/tui/utils/image-placeholder.ts
|
|
8721
|
+
/**
|
|
8722
|
+
* Scan submitted text for media placeholders and produce
|
|
8723
|
+
* the `PromptPart[]` we'll send to the SDK prompt endpoint.
|
|
8724
|
+
*
|
|
8725
|
+
* Rules:
|
|
8726
|
+
* - Only placeholders that resolve against `store` get extracted.
|
|
8727
|
+
* A literal `[image #999 ...]` the user typed themselves stays in
|
|
8728
|
+
* the text (we can't hallucinate files for it).
|
|
8729
|
+
* - Order is preserved for text/image/video segments. Image placeholders
|
|
8730
|
+
* expand to image content parts so the prompt reaches the provider
|
|
8731
|
+
* without relying on a model tool call. Video placeholders still expand
|
|
8732
|
+
* to file-path tags so `ReadMediaFile` can own video upload behavior.
|
|
8733
|
+
* - Adjacent text segments are flattened — empty / whitespace-only
|
|
8734
|
+
* segments drop out so we never emit `{type:'text', text:' '}`
|
|
8735
|
+
* noise between two media parts.
|
|
8736
|
+
*/
|
|
8696
8737
|
const PLACEHOLDER_REGEX = /\[(image|video) #(\d+) (?:(\(\d+×\d+\))|([^\]]+))\]/g;
|
|
8697
|
-
function extractMediaAttachments(text, store) {
|
|
8738
|
+
function extractMediaAttachments(text, store, options = {}) {
|
|
8739
|
+
const imageMode = options.imageMode ?? "inline";
|
|
8698
8740
|
const parts = [];
|
|
8699
8741
|
const imageAttachmentIds = [];
|
|
8700
8742
|
const videoAttachmentIds = [];
|
|
@@ -8714,6 +8756,13 @@ function extractMediaAttachments(text, store) {
|
|
|
8714
8756
|
if (attachment.kind === "video") {
|
|
8715
8757
|
pushText(parts, tagTextForVideo(attachment));
|
|
8716
8758
|
videoAttachmentIds.push(id);
|
|
8759
|
+
} else if (imageMode === "path") {
|
|
8760
|
+
try {
|
|
8761
|
+
pushText(parts, formatMediaTag("image", ensureImagePath(attachment)));
|
|
8762
|
+
} catch {
|
|
8763
|
+
parts.push(imagePartForAttachment(attachment));
|
|
8764
|
+
}
|
|
8765
|
+
imageAttachmentIds.push(id);
|
|
8717
8766
|
} else {
|
|
8718
8767
|
parts.push(imagePartForAttachment(attachment));
|
|
8719
8768
|
imageAttachmentIds.push(id);
|
|
@@ -8752,6 +8801,15 @@ function imagePartForAttachment(att) {
|
|
|
8752
8801
|
imageUrl: { url: `data:${att.mime};base64,${base64}` }
|
|
8753
8802
|
};
|
|
8754
8803
|
}
|
|
8804
|
+
/** Persist bytes to a temp file when path delivery needs a disk path. */
|
|
8805
|
+
function ensureImagePath(att) {
|
|
8806
|
+
if (att.sourcePath !== void 0 && att.sourcePath.length > 0) return att.sourcePath;
|
|
8807
|
+
const ext = att.mime === "image/jpeg" ? "jpg" : att.mime.split("/")[1]?.replace("+xml", "").replace("+json", "") ?? "png";
|
|
8808
|
+
const path = join(tmpdir(), `${TEMP_ATTACHMENT_PREFIX}${randomUUID()}.${ext}`);
|
|
8809
|
+
writeFileSync(path, att.bytes);
|
|
8810
|
+
att.sourcePath = path;
|
|
8811
|
+
return path;
|
|
8812
|
+
}
|
|
8755
8813
|
function tagTextForVideo(att) {
|
|
8756
8814
|
return formatMediaTag("video", att.sourcePath);
|
|
8757
8815
|
}
|
|
@@ -8919,7 +8977,8 @@ var InputController = class InputController {
|
|
|
8919
8977
|
this.dispatchUserInput(text, session);
|
|
8920
8978
|
}
|
|
8921
8979
|
dispatchUserInput(text, session) {
|
|
8922
|
-
const
|
|
8980
|
+
const canInlineImages = this.supportsCurrentModelCapability("image_in");
|
|
8981
|
+
const extraction = extractMediaAttachments(text, this.host.imageStore, { imageMode: canInlineImages ? "inline" : "path" });
|
|
8923
8982
|
if (!this.validateMediaCapabilities(extraction)) return;
|
|
8924
8983
|
if (extraction.hasMedia) this.sendMessage(session, text, {
|
|
8925
8984
|
hasMedia: true,
|
|
@@ -9096,10 +9155,6 @@ var InputController = class InputController {
|
|
|
9096
9155
|
}
|
|
9097
9156
|
validateMediaCapabilities(extraction) {
|
|
9098
9157
|
if (!extraction.hasMedia) return true;
|
|
9099
|
-
if (extraction.imageAttachmentIds.length > 0 && !this.supportsCurrentModelCapability("image_in")) {
|
|
9100
|
-
this.host.showError(t("error.image_not_supported"));
|
|
9101
|
-
return false;
|
|
9102
|
-
}
|
|
9103
9158
|
if (extraction.videoAttachmentIds.length > 0 && !this.supportsCurrentModelCapability("video_in")) {
|
|
9104
9159
|
this.host.showError(t("error.video_not_supported"));
|
|
9105
9160
|
return false;
|
|
@@ -9467,7 +9522,7 @@ const SIDE_PADDING = 1;
|
|
|
9467
9522
|
* Renders the stacked sidebar: every visible registered panel gets its own
|
|
9468
9523
|
* full box frame (┌─┐│└┘) with the panel title embedded in the top frame, one
|
|
9469
9524
|
* below the other. Panels render live each frame (their body reads the
|
|
9470
|
-
* current data snapshot), so time-based values (
|
|
9525
|
+
* current data snapshot), so time-based values (goal wall-clock)
|
|
9471
9526
|
* tick without rebuilding. The focused panel's title is bolded; focus is
|
|
9472
9527
|
* moved by the SidebarManager (`/sidebar next/prev`, Ctrl+X toggles).
|
|
9473
9528
|
* The whole frame is painted in the theme accent colour and the rendered
|
|
@@ -9481,9 +9536,10 @@ var SidebarContainer = class extends Container {
|
|
|
9481
9536
|
* only rebuilt when the visible panel set changes. */
|
|
9482
9537
|
bodies = /* @__PURE__ */ new Map();
|
|
9483
9538
|
builtStackKey = "";
|
|
9484
|
-
/** 1s heartbeat
|
|
9485
|
-
*
|
|
9486
|
-
*
|
|
9539
|
+
/** 1s heartbeat for the whole container lifetime: keeps live values (goal
|
|
9540
|
+
* wall-clock, gradient phase of busy agent slots) ticking while the sidebar
|
|
9541
|
+
* is open — the tick is a no-op when it is closed, and only `dispose()`
|
|
9542
|
+
* (TUI teardown) clears the interval. */
|
|
9487
9543
|
heartbeat;
|
|
9488
9544
|
constructor(manager, requestRender, colors) {
|
|
9489
9545
|
super();
|
|
@@ -9930,6 +9986,20 @@ function extractBracketedImagePastePaths(data) {
|
|
|
9930
9986
|
if (paths.length === 0 || segmentStart !== pasted.length) return void 0;
|
|
9931
9987
|
return paths;
|
|
9932
9988
|
}
|
|
9989
|
+
/**
|
|
9990
|
+
* True only when bracketed-paste markers span the entire input and the
|
|
9991
|
+
* body is strictly empty — Cmd+V with an image-only clipboard (macOS
|
|
9992
|
+
* wraps nothing). Whitespace-only pastes and coalesced keystrokes around
|
|
9993
|
+
* the marker pair are not image pastes.
|
|
9994
|
+
*/
|
|
9995
|
+
function isCompleteEmptyBracketedPaste(data) {
|
|
9996
|
+
if (!data.includes(BRACKET_PASTE_START)) return false;
|
|
9997
|
+
const startIndex = data.indexOf(BRACKET_PASTE_START);
|
|
9998
|
+
const endIndex = data.indexOf(BRACKET_PASTE_END, startIndex + 6);
|
|
9999
|
+
if (endIndex === -1) return false;
|
|
10000
|
+
if (startIndex !== 0 || endIndex + 6 !== data.length) return false;
|
|
10001
|
+
return data.slice(startIndex + 6, endIndex).length === 0;
|
|
10002
|
+
}
|
|
9933
10003
|
const KITTY_CSI_U = /^\[(\d+);(\d+)((?::\d+)*)u$/;
|
|
9934
10004
|
const CAPS_LOCK_BIT = 64;
|
|
9935
10005
|
const CTRL_BIT = 4;
|
|
@@ -10142,6 +10212,11 @@ var CustomEditor = class extends Editor {
|
|
|
10142
10212
|
if (!normalized.includes(BRACKET_PASTE_END)) this.consumingPaste = true;
|
|
10143
10213
|
return;
|
|
10144
10214
|
}
|
|
10215
|
+
if (this.onPasteImage !== void 0 && isCompleteEmptyBracketedPaste(normalized)) {
|
|
10216
|
+
const handler = this.onPasteImage;
|
|
10217
|
+
handler();
|
|
10218
|
+
return;
|
|
10219
|
+
}
|
|
10145
10220
|
if (matchesKey(normalized, process.platform === "win32" ? "alt+v" : Key.ctrl("v"))) {
|
|
10146
10221
|
if (this.expandPasteMarkerAtCursor()) return;
|
|
10147
10222
|
if (this.onPasteImage !== void 0) {
|
|
@@ -11150,6 +11225,7 @@ var SessionManager$1 = class {
|
|
|
11150
11225
|
await this.unloadCurrentSession("switching session")?.close({ extractMemories: false });
|
|
11151
11226
|
this.host.session = session;
|
|
11152
11227
|
this.registerSessionHandlers(session);
|
|
11228
|
+
await syncDiagramCapabilityPrompt(session);
|
|
11153
11229
|
}
|
|
11154
11230
|
async syncRuntimeState(session = this.requireSession()) {
|
|
11155
11231
|
const status = await session.getStatus();
|
|
@@ -14056,7 +14132,7 @@ var ScreamTUI = class {
|
|
|
14056
14132
|
const card = new OnboardingCardComponent({
|
|
14057
14133
|
colors: this.state.theme.colors,
|
|
14058
14134
|
onConfigure: () => {
|
|
14059
|
-
import("./dispatch-
|
|
14135
|
+
import("./dispatch-Du8ddILK.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
14060
14136
|
this.restoreEditor();
|
|
14061
14137
|
onDone?.();
|
|
14062
14138
|
});
|
|
@@ -16339,13 +16415,16 @@ async function ensureSessionsDir(homeDir) {
|
|
|
16339
16415
|
const dir = getSessionsDir(homeDir);
|
|
16340
16416
|
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
|
|
16341
16417
|
}
|
|
16342
|
-
async function appendJournalLine(homeDir, sessionId, line) {
|
|
16418
|
+
async function appendJournalLine(homeDir, sessionId, line, throwOnError = false) {
|
|
16419
|
+
let writeError;
|
|
16343
16420
|
await appendJournalLineSerialized(getJournalPath(homeDir, sessionId), line, (error) => {
|
|
16421
|
+
writeError = error;
|
|
16344
16422
|
log.warn("web: failed to persist journal line", {
|
|
16345
16423
|
sessionId,
|
|
16346
16424
|
error: String(error)
|
|
16347
16425
|
});
|
|
16348
16426
|
});
|
|
16427
|
+
if (throwOnError && writeError !== void 0) throw writeError;
|
|
16349
16428
|
}
|
|
16350
16429
|
let metaWriteSeq = 0;
|
|
16351
16430
|
async function saveMetadata(homeDir, meta) {
|
|
@@ -16430,16 +16509,22 @@ function isMessageBearingEntry(entry) {
|
|
|
16430
16509
|
*/
|
|
16431
16510
|
function contextHistoryToChatMessages(history) {
|
|
16432
16511
|
const out = [];
|
|
16512
|
+
let pendingAnchor = "other";
|
|
16433
16513
|
for (const message of history) {
|
|
16434
16514
|
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
16435
16515
|
if (isInternalMessage(message)) continue;
|
|
16436
|
-
|
|
16516
|
+
const anchor = classifyUndoAnchor(message.origin);
|
|
16517
|
+
if (message.role === "user") {
|
|
16518
|
+
pendingAnchor = anchor;
|
|
16519
|
+
if (anchor === "skill" || anchor === "other") continue;
|
|
16520
|
+
}
|
|
16437
16521
|
const text = message.content.map((part) => part.type === "text" ? part.text : "").join("").trim();
|
|
16438
16522
|
if (text.length === 0) continue;
|
|
16439
16523
|
out.push({
|
|
16440
16524
|
role: message.role,
|
|
16441
16525
|
content: text,
|
|
16442
|
-
tools: []
|
|
16526
|
+
tools: [],
|
|
16527
|
+
undoAnchor: message.role === "assistant" ? pendingAnchor : anchor
|
|
16443
16528
|
});
|
|
16444
16529
|
}
|
|
16445
16530
|
return out;
|
|
@@ -16469,6 +16554,65 @@ function deriveTitle(messages) {
|
|
|
16469
16554
|
const text = firstUser.content.trim();
|
|
16470
16555
|
return text.length > 40 ? text.slice(0, 40) + "..." : text;
|
|
16471
16556
|
}
|
|
16557
|
+
/**
|
|
16558
|
+
* Remove complete user-turn blocks from a rendered transcript. The core
|
|
16559
|
+
* context owns the authoritative undo operation; the web journal is append
|
|
16560
|
+
* only, so a durable undo marker asks this projection to hide the same turns
|
|
16561
|
+
* when snapshots are rebuilt (including after a restart).
|
|
16562
|
+
*
|
|
16563
|
+
* `beforeSeq` is an inclusive cursor captured before the core mutation. It
|
|
16564
|
+
* also covers a user message that reserved the next sequence but never
|
|
16565
|
+
* emitted a turn event.
|
|
16566
|
+
*/
|
|
16567
|
+
function removeLastTurnsBefore(messages, count, beforeSeq) {
|
|
16568
|
+
if (count <= 0 || messages.length === 0) return;
|
|
16569
|
+
const firstFutureIndex = messages.findIndex((message) => typeof message.seq === "number" && message.seq > beforeSeq);
|
|
16570
|
+
const prefixLength = firstFutureIndex === -1 ? messages.length : firstFutureIndex;
|
|
16571
|
+
const prefix = messages.slice(0, prefixLength);
|
|
16572
|
+
const tail = messages.slice(prefixLength);
|
|
16573
|
+
let remaining = count;
|
|
16574
|
+
while (remaining > 0) {
|
|
16575
|
+
let anchor = -1;
|
|
16576
|
+
for (let i = prefix.length - 1; i >= 0; i -= 1) {
|
|
16577
|
+
const message = prefix[i];
|
|
16578
|
+
if (message?.role === "assistant" && message.undoAnchor === "skill") {
|
|
16579
|
+
anchor = i;
|
|
16580
|
+
while (anchor > 0) {
|
|
16581
|
+
const previous = prefix[anchor - 1];
|
|
16582
|
+
if (previous?.role !== "assistant" || previous.undoAnchor !== "skill") break;
|
|
16583
|
+
anchor -= 1;
|
|
16584
|
+
}
|
|
16585
|
+
break;
|
|
16586
|
+
}
|
|
16587
|
+
if (message?.role === "user") {
|
|
16588
|
+
anchor = i;
|
|
16589
|
+
break;
|
|
16590
|
+
}
|
|
16591
|
+
}
|
|
16592
|
+
if (anchor < 0) break;
|
|
16593
|
+
prefix.splice(anchor);
|
|
16594
|
+
remaining -= 1;
|
|
16595
|
+
}
|
|
16596
|
+
messages.splice(0, messages.length, ...prefix, ...tail);
|
|
16597
|
+
}
|
|
16598
|
+
function classifyUndoAnchor(origin) {
|
|
16599
|
+
if (origin === void 0 || origin === null) return "user";
|
|
16600
|
+
if (typeof origin !== "object") return "other";
|
|
16601
|
+
const kind = origin.kind;
|
|
16602
|
+
if (kind === "user") return "user";
|
|
16603
|
+
if (kind === "skill_activation" && origin.trigger === "user-slash") return "skill";
|
|
16604
|
+
return "other";
|
|
16605
|
+
}
|
|
16606
|
+
/** Match the core `/undo` anchor rule without importing agent-core internals. */
|
|
16607
|
+
function countUndoAnchors(history) {
|
|
16608
|
+
let count = 0;
|
|
16609
|
+
for (const message of history) {
|
|
16610
|
+
if (message.role !== "user") continue;
|
|
16611
|
+
const origin = message.origin;
|
|
16612
|
+
if (origin === void 0 || origin.kind === "user" || origin.kind === "skill_activation" && origin.trigger === "user-slash") count += 1;
|
|
16613
|
+
}
|
|
16614
|
+
return count;
|
|
16615
|
+
}
|
|
16472
16616
|
const execFileAsync = promisify(execFile);
|
|
16473
16617
|
/** Raw porcelain rows: repo-root-relative path + XY status. `getGitStatus`
|
|
16474
16618
|
* enriches these into full `GitFileChange` records. */
|
|
@@ -16731,6 +16875,8 @@ var WebSession = class {
|
|
|
16731
16875
|
heartbeatTimer = null;
|
|
16732
16876
|
unsubscribe = null;
|
|
16733
16877
|
busy = false;
|
|
16878
|
+
/** Prevent two concurrent REST `/undo` calls from mutating core history together. */
|
|
16879
|
+
undoInFlight = false;
|
|
16734
16880
|
/**
|
|
16735
16881
|
* In-flight turn count across all agents (main + subagents). Used by the
|
|
16736
16882
|
* idle-exit watchdog: a single boolean is not enough because a subagent's
|
|
@@ -16819,13 +16965,13 @@ var WebSession = class {
|
|
|
16819
16965
|
});
|
|
16820
16966
|
return entries;
|
|
16821
16967
|
}
|
|
16822
|
-
async persistEntry(entry) {
|
|
16968
|
+
async persistEntry(entry, throwOnError = false) {
|
|
16823
16969
|
if (!this.homeDir) return;
|
|
16824
16970
|
const line = JSON.stringify({
|
|
16825
16971
|
type: "journal",
|
|
16826
16972
|
...entry
|
|
16827
16973
|
});
|
|
16828
|
-
await appendJournalLine(this.homeDir, this.sessionId, line);
|
|
16974
|
+
await appendJournalLine(this.homeDir, this.sessionId, line, throwOnError);
|
|
16829
16975
|
}
|
|
16830
16976
|
async persistUserMessage(text, beforeSeq, clientMessageId) {
|
|
16831
16977
|
this.userMessages.push({
|
|
@@ -16882,6 +17028,9 @@ var WebSession = class {
|
|
|
16882
17028
|
* journal holds no message records.
|
|
16883
17029
|
*/
|
|
16884
17030
|
seededMessages = [];
|
|
17031
|
+
/** Undo markers already reflected by a core-history seed must not be applied twice. */
|
|
17032
|
+
seedCoveredUndoMarkers = /* @__PURE__ */ new Set();
|
|
17033
|
+
historySeeded = false;
|
|
16885
17034
|
/**
|
|
16886
17035
|
* Mark the journal untrustworthy (byte-interleaving damage, see
|
|
16887
17036
|
* journal-writer.ts).
|
|
@@ -16923,7 +17072,10 @@ var WebSession = class {
|
|
|
16923
17072
|
* monotonic with live events and consistent with the getMessagesOlder cursor.
|
|
16924
17073
|
*/
|
|
16925
17074
|
seedHistory(messages) {
|
|
16926
|
-
if (
|
|
17075
|
+
if (this.historySeeded) return;
|
|
17076
|
+
this.historySeeded = true;
|
|
17077
|
+
for (const entry of this.journal) if (entry.payload.type === "web.history.undone") this.seedCoveredUndoMarkers.add(entry.seq);
|
|
17078
|
+
if (messages.length === 0) return;
|
|
16927
17079
|
let seq = 1;
|
|
16928
17080
|
for (const message of messages) {
|
|
16929
17081
|
this.seededMessages.push({
|
|
@@ -16970,7 +17122,7 @@ var WebSession = class {
|
|
|
16970
17122
|
* clients. Used for server-owned reconstruction material (finalized
|
|
16971
17123
|
* assistant snapshots) that clients never consume as live events.
|
|
16972
17124
|
*/
|
|
16973
|
-
appendDurableSilent(payload) {
|
|
17125
|
+
appendDurableSilent(payload, throwOnPersistenceError = false) {
|
|
16974
17126
|
const entry = {
|
|
16975
17127
|
seq: this.nextSeq++,
|
|
16976
17128
|
epoch: this.epoch,
|
|
@@ -16978,8 +17130,12 @@ var WebSession = class {
|
|
|
16978
17130
|
payload
|
|
16979
17131
|
};
|
|
16980
17132
|
this.journal.push(entry);
|
|
16981
|
-
this.
|
|
16982
|
-
|
|
17133
|
+
const persisted = this.persistEntry(entry, throwOnPersistenceError);
|
|
17134
|
+
this.trackPending(persisted);
|
|
17135
|
+
return {
|
|
17136
|
+
entry,
|
|
17137
|
+
persisted
|
|
17138
|
+
};
|
|
16983
17139
|
}
|
|
16984
17140
|
subscribeEvents() {
|
|
16985
17141
|
if (!this.session) return;
|
|
@@ -17308,13 +17464,14 @@ var WebSession = class {
|
|
|
17308
17464
|
});
|
|
17309
17465
|
}
|
|
17310
17466
|
async handleCommand(ws, command, args, pendingMsgId) {
|
|
17311
|
-
const ok = (message) => {
|
|
17467
|
+
const ok = (message, extra) => {
|
|
17312
17468
|
this.broadcast({
|
|
17313
17469
|
type: "command_result",
|
|
17314
17470
|
command,
|
|
17315
17471
|
ok: true,
|
|
17316
17472
|
message,
|
|
17317
|
-
pendingMsgId
|
|
17473
|
+
pendingMsgId,
|
|
17474
|
+
...extra
|
|
17318
17475
|
}, false);
|
|
17319
17476
|
};
|
|
17320
17477
|
const fail = (message) => {
|
|
@@ -17350,9 +17507,10 @@ var WebSession = class {
|
|
|
17350
17507
|
}
|
|
17351
17508
|
return;
|
|
17352
17509
|
case "auto":
|
|
17510
|
+
case "ask":
|
|
17353
17511
|
case "yes":
|
|
17354
17512
|
case "bot": {
|
|
17355
|
-
const mode = command === "auto" ? "auto" : command === "bot" ? "bot" : "yolo";
|
|
17513
|
+
const mode = command === "auto" ? "auto" : command === "ask" ? "ask" : command === "bot" ? "bot" : "yolo";
|
|
17356
17514
|
try {
|
|
17357
17515
|
await this.session.setPermission(mode);
|
|
17358
17516
|
this.permission = mode;
|
|
@@ -17395,7 +17553,7 @@ var WebSession = class {
|
|
|
17395
17553
|
fail("Fork 失败:无法复制当前会话。");
|
|
17396
17554
|
return;
|
|
17397
17555
|
}
|
|
17398
|
-
ok(`会话已 fork,新会话 ID:${result.sessionId}
|
|
17556
|
+
ok(`会话已 fork,新会话 ID:${result.sessionId}`, { sessionId: result.sessionId });
|
|
17399
17557
|
} catch (error) {
|
|
17400
17558
|
fail(`Fork 失败:${errMsg(error)}`);
|
|
17401
17559
|
}
|
|
@@ -17506,6 +17664,7 @@ var WebSession = class {
|
|
|
17506
17664
|
planMode: false,
|
|
17507
17665
|
wolfpackMode: false,
|
|
17508
17666
|
rlmEnabled: false,
|
|
17667
|
+
rlmMaxDepth: null,
|
|
17509
17668
|
contextTokens: 0,
|
|
17510
17669
|
maxContextTokens: 0,
|
|
17511
17670
|
contextUsage: 0
|
|
@@ -17682,6 +17841,64 @@ var WebSession = class {
|
|
|
17682
17841
|
if (!this.session) throw new HttpError(409, "Session is archived (read-only)", ErrorCodes.SESSION_CLOSED);
|
|
17683
17842
|
return this.session;
|
|
17684
17843
|
}
|
|
17844
|
+
/**
|
|
17845
|
+
* Undo user turns in the core session and append a durable web projection
|
|
17846
|
+
* marker. The core RPC deliberately has no UI knowledge, so without the
|
|
17847
|
+
* marker the old append-only web journal would resurrect the removed turns
|
|
17848
|
+
* on the next snapshot or after a restart.
|
|
17849
|
+
*/
|
|
17850
|
+
async undoHistory(count) {
|
|
17851
|
+
if (!Number.isSafeInteger(count) || count < 1) throw new HttpError(400, "Undo count must be a positive integer.");
|
|
17852
|
+
if (this.busy) throw new HttpError(409, "Session is busy", ErrorCodes.TURN_AGENT_BUSY);
|
|
17853
|
+
const session = this.requireLiveSession();
|
|
17854
|
+
if (this.undoInFlight) throw new HttpError(409, "Undo already in progress");
|
|
17855
|
+
this.undoInFlight = true;
|
|
17856
|
+
try {
|
|
17857
|
+
const beforeSeq = this.nextSeq;
|
|
17858
|
+
let beforeAnchors = null;
|
|
17859
|
+
try {
|
|
17860
|
+
beforeAnchors = countUndoAnchors((await session.getContext()).history);
|
|
17861
|
+
} catch (error) {
|
|
17862
|
+
log.warn("web: failed to read context before undo", {
|
|
17863
|
+
sessionId: this.sessionId,
|
|
17864
|
+
error: String(error)
|
|
17865
|
+
});
|
|
17866
|
+
}
|
|
17867
|
+
await session.undoHistory(count);
|
|
17868
|
+
let removed = count;
|
|
17869
|
+
if (beforeAnchors !== null) try {
|
|
17870
|
+
const afterAnchors = countUndoAnchors((await session.getContext()).history);
|
|
17871
|
+
removed = Math.max(0, beforeAnchors - afterAnchors);
|
|
17872
|
+
} catch (error) {
|
|
17873
|
+
log.warn("web: failed to read context after undo", {
|
|
17874
|
+
sessionId: this.sessionId,
|
|
17875
|
+
error: String(error)
|
|
17876
|
+
});
|
|
17877
|
+
}
|
|
17878
|
+
if (removed <= 0) {
|
|
17879
|
+
await this.refreshStatus();
|
|
17880
|
+
this.broadcastStatus();
|
|
17881
|
+
return;
|
|
17882
|
+
}
|
|
17883
|
+
const { entry, persisted } = this.appendDurableSilent({
|
|
17884
|
+
type: "web.history.undone",
|
|
17885
|
+
count: removed,
|
|
17886
|
+
beforeSeq
|
|
17887
|
+
}, true);
|
|
17888
|
+
await persisted;
|
|
17889
|
+
this.broadcast({
|
|
17890
|
+
type: "event",
|
|
17891
|
+
seq: entry.seq,
|
|
17892
|
+
epoch: entry.epoch,
|
|
17893
|
+
payload: entry.payload
|
|
17894
|
+
}, false);
|
|
17895
|
+
this.trackPending(this.updateTitle());
|
|
17896
|
+
await this.refreshStatus();
|
|
17897
|
+
this.broadcastStatus();
|
|
17898
|
+
} finally {
|
|
17899
|
+
this.undoInFlight = false;
|
|
17900
|
+
}
|
|
17901
|
+
}
|
|
17685
17902
|
requireGoalSession(allowBusy = false) {
|
|
17686
17903
|
if (!this.session) throw new HttpError(409, "Session is archived (read-only)", ErrorCodes.SESSION_CLOSED);
|
|
17687
17904
|
if (!allowBusy && this.busy) throw new HttpError(409, "Session is busy", ErrorCodes.TURN_AGENT_BUSY);
|
|
@@ -17759,6 +17976,7 @@ var WebSession = class {
|
|
|
17759
17976
|
role: "assistant",
|
|
17760
17977
|
content: "",
|
|
17761
17978
|
tools: [],
|
|
17979
|
+
undoAnchor: classifyUndoAnchor(event.origin),
|
|
17762
17980
|
seq: entry.seq,
|
|
17763
17981
|
model: this.cachedStatus?.model,
|
|
17764
17982
|
turnStats: {
|
|
@@ -17812,13 +18030,20 @@ var WebSession = class {
|
|
|
17812
18030
|
break;
|
|
17813
18031
|
case "web.message.finalized": {
|
|
17814
18032
|
const finalized = event.message;
|
|
17815
|
-
|
|
18033
|
+
const lastSkeletonSeq = messages.length > 0 && messages.at(-1)?.role === "assistant" ? messages.at(-1).seq : entry.seq;
|
|
18034
|
+
if (finalized.undoAnchor === void 0 && messages.at(-1)?.role === "assistant") finalized.undoAnchor = messages.at(-1).undoAnchor;
|
|
18035
|
+
finalized.seq = lastSkeletonSeq;
|
|
17816
18036
|
if (messages.length > 0 && messages.at(-1)?.role === "assistant") messages[messages.length - 1] = finalized;
|
|
17817
18037
|
else messages.push(finalized);
|
|
17818
18038
|
currentAssistant = null;
|
|
17819
18039
|
turnFinalized = true;
|
|
17820
18040
|
break;
|
|
17821
18041
|
}
|
|
18042
|
+
case "web.history.undone":
|
|
18043
|
+
if (!this.seedCoveredUndoMarkers.has(entry.seq)) removeLastTurnsBefore(messages, event.count, event.beforeSeq);
|
|
18044
|
+
currentAssistant = null;
|
|
18045
|
+
turnFinalized = false;
|
|
18046
|
+
break;
|
|
17822
18047
|
case "turn.ended":
|
|
17823
18048
|
if (event.reason === "failed" && currentAssistant) currentAssistant.isError = true;
|
|
17824
18049
|
if (currentAssistant && !turnFinalized && currentAssistant.content === "") currentAssistant.degraded = true;
|
|
@@ -17830,7 +18055,10 @@ var WebSession = class {
|
|
|
17830
18055
|
}
|
|
17831
18056
|
for (let i = userIndex; i < this.userMessages.length; i++) {
|
|
17832
18057
|
const item = this.userMessages[i];
|
|
17833
|
-
if (item !== void 0) messages.push(
|
|
18058
|
+
if (item !== void 0) messages.push({
|
|
18059
|
+
...item.msg,
|
|
18060
|
+
seq: item.beforeSeq
|
|
18061
|
+
});
|
|
17834
18062
|
}
|
|
17835
18063
|
return messages;
|
|
17836
18064
|
}
|
|
@@ -18427,7 +18655,9 @@ var SessionManager = class {
|
|
|
18427
18655
|
await ws.switchRlm(enabled, maxDepth);
|
|
18428
18656
|
}
|
|
18429
18657
|
async undoHistory(sessionId, count) {
|
|
18430
|
-
|
|
18658
|
+
const ws = this.sessions.get(sessionId);
|
|
18659
|
+
if (!ws) throw new HttpError(404, "Session not found", ErrorCodes.SESSION_NOT_FOUND);
|
|
18660
|
+
await ws.undoHistory(count);
|
|
18431
18661
|
}
|
|
18432
18662
|
async compact(sessionId, instruction) {
|
|
18433
18663
|
await this.getLiveSession(sessionId).compact(instruction ? { instruction } : {});
|
|
@@ -18597,16 +18827,32 @@ async function handleSessionDataRoutes(req, res, url, method, resolveSession, wo
|
|
|
18597
18827
|
}
|
|
18598
18828
|
return true;
|
|
18599
18829
|
}
|
|
18600
|
-
|
|
18601
|
-
|
|
18602
|
-
|
|
18603
|
-
|
|
18604
|
-
|
|
18605
|
-
|
|
18606
|
-
|
|
18607
|
-
|
|
18830
|
+
const gitPath = url.split("?")[0];
|
|
18831
|
+
const isGitStatusRoute = gitPath === `${API_PREFIX}/git/status` && method === "GET";
|
|
18832
|
+
if (isGitStatusRoute || gitPath === `${API_PREFIX}/git/diff` && method === "GET") {
|
|
18833
|
+
const gitQuery = new URLSearchParams(url.split("?")[1] ?? "");
|
|
18834
|
+
const requestedGitSessionId = gitQuery.get("sessionId");
|
|
18835
|
+
let gitWorkDir = workDir;
|
|
18836
|
+
if (requestedGitSessionId !== null) {
|
|
18837
|
+
const gitSession = resolveSession(requestedGitSessionId);
|
|
18838
|
+
if (!gitSession) {
|
|
18839
|
+
sendJson(res, 404, {
|
|
18840
|
+
code: ErrorCodes.SESSION_NOT_FOUND,
|
|
18841
|
+
message: "Session not found"
|
|
18842
|
+
});
|
|
18843
|
+
return true;
|
|
18844
|
+
}
|
|
18845
|
+
gitWorkDir = gitSession.workDir;
|
|
18846
|
+
}
|
|
18847
|
+
if (isGitStatusRoute) {
|
|
18848
|
+
const gs = await getGitStatus(gitWorkDir);
|
|
18849
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
18850
|
+
res.end(JSON.stringify(gs));
|
|
18851
|
+
return true;
|
|
18852
|
+
}
|
|
18853
|
+
const relPath = gitQuery.get("path") ?? "";
|
|
18608
18854
|
try {
|
|
18609
|
-
const result = await getGitFileDiff(
|
|
18855
|
+
const result = await getGitFileDiff(gitWorkDir, relPath);
|
|
18610
18856
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
18611
18857
|
res.end(JSON.stringify({
|
|
18612
18858
|
path: relPath,
|
|
@@ -18817,6 +19063,7 @@ async function handleSessionControlRoutes(req, res, url, method, manager) {
|
|
|
18817
19063
|
return false;
|
|
18818
19064
|
}
|
|
18819
19065
|
async function handleResourceRoutes(req, res, url, method, manager) {
|
|
19066
|
+
const routePath = url.split("?")[0] ?? url;
|
|
18820
19067
|
const skillsMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/skills$`).exec(url);
|
|
18821
19068
|
if (skillsMatch && method === "GET") {
|
|
18822
19069
|
try {
|
|
@@ -19004,7 +19251,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
|
|
|
19004
19251
|
}
|
|
19005
19252
|
return true;
|
|
19006
19253
|
}
|
|
19007
|
-
const tasksMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks$`).exec(
|
|
19254
|
+
const tasksMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks$`).exec(routePath);
|
|
19008
19255
|
if (tasksMatch && method === "GET") {
|
|
19009
19256
|
try {
|
|
19010
19257
|
const query = new URLSearchParams(url.split("?")[1] ?? "");
|
|
@@ -19016,7 +19263,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
|
|
|
19016
19263
|
}
|
|
19017
19264
|
return true;
|
|
19018
19265
|
}
|
|
19019
|
-
const taskOutputMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/output$`).exec(
|
|
19266
|
+
const taskOutputMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/output$`).exec(routePath);
|
|
19020
19267
|
if (taskOutputMatch && method === "GET") {
|
|
19021
19268
|
try {
|
|
19022
19269
|
const query = new URLSearchParams(url.split("?")[1] ?? "");
|
|
@@ -19027,7 +19274,7 @@ async function handleResourceRoutes(req, res, url, method, manager) {
|
|
|
19027
19274
|
}
|
|
19028
19275
|
return true;
|
|
19029
19276
|
}
|
|
19030
|
-
const taskStopMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/stop$`).exec(
|
|
19277
|
+
const taskStopMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/tasks/([^/]+)/stop$`).exec(routePath);
|
|
19031
19278
|
if (taskStopMatch && method === "POST") {
|
|
19032
19279
|
try {
|
|
19033
19280
|
const body = await readJsonBody(req);
|