scream-code 0.18.6 → 0.18.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 +6 -3
- package/dist/{app-Ca2pwAk7.mjs → app-Db7f8Kja.mjs} +296 -205
- package/dist/{dispatch-BgO7wAf9.mjs → dispatch-BPfUTccZ.mjs} +505 -129
- package/dist/{dispatch-0mihbsV3.mjs → dispatch-zsFzFFoY.mjs} +1 -1
- package/dist/main.mjs +1 -1
- package/dist/{src-C3oCiVNb.mjs → src-CFHysu0q.mjs} +11 -11
- package/dist/{src-C7C9e5Ds.mjs → src-ClB_Hzp9.mjs} +3355 -100
- package/dist/{src-qit_jRUM.mjs → src-DA34cXSv.mjs} +1 -1
- package/dist/{text-input-dialog-DWepgNF4.mjs → text-input-dialog-CFRUSCi6.mjs} +1 -1
- package/dist/{text-input-dialog-BvbqjvzB.mjs → text-input-dialog-DHDsHCqR.mjs} +60 -4
- package/package.json +3 -4
|
@@ -4,15 +4,15 @@ 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 { A as sharedMemoStore } from "./src-
|
|
8
|
-
import { $ as
|
|
9
|
-
import { i as t, n as getLocale, r as setLocale } from "./text-input-dialog-
|
|
7
|
+
import { A as sharedMemoStore } from "./src-ClB_Hzp9.mjs";
|
|
8
|
+
import { $ as clearGoalState, $n as fetchCatalog, $t as isStreaming, A as renderDiffLines, An as isExperimentalFlagEnabled, At as createMarkdownTheme, B as SkillActivationComponent, Bn as TuiLikePreferencesSchema, Bt as DISABLE_TERMINAL_THEME_REPORTING, C as appendJsonlLine, Cn as getCtrlCHint, Ct as refreshProviderBalance, Dn as buildSkillSlashCommands, E as getDaemonInstructions, En as getNoActiveSessionMessage, F as ThinkingComponent, Fn as hasEmbeddingModelCache, Ft as contrastTextHex, G as getSharedSpeedTracker, Gn as getInputHistoryFile, Gt as OSC11_RESPONSE_PREFIX, H as ActivityGroupComponent, Hn as saveTuiConfig, Ht as ENABLE_TERMINAL_THEME_REPORTING, I as NoticeMessageComponent, In as startManualEmbeddingDownload, It as getColorPalette, J as WelcomeComponent, Jn as CLI_COMMAND_NAME, Jt as TERMINAL_FOCUS_IN, K as BackgroundAgentStatusComponent, Kn as getLogDir, Kt as OSC11_RESPONSE_PREFIX_NO_ESC, L as StatusMessageComponent, Ln as PIXEL_PULSE_FRAMES, Lt as detectTerminalTheme, M as highlightLines, Mn as embeddingProgressLabel, Mt as isTurnElapsedEnabled, N as langFromPath, Nn as embeddingFailureDetail, Nt as toggleEmptySessionHint, O as UserMessageComponent, On as BUILTIN_SLASH_COMMANDS, Ot as resolveThemeSync, P as CachedContainer, Pn as getKnowledgeStore, Pt as createThemeStyles, Q as CompactionComponent, Qn as DEFAULT_CATALOG_URL, Qt as isBusy, R as replaceTabs, Rn as PULSE_WAVE_FRAMES, Rt as parseOsc11BackgroundTheme, S as readUpdateCache, Sn as TIP_ROTATION_INTERVAL_MS, Tn as getLlmNotSetMessage, Tt as clearInfoPanelState, U as charsForTokenBudget, Un as detectShellEnvironment, Ut as OSC11_QUERY, V as AssistantMessageComponent, Vn as loadTuiConfig, Vt as ENABLE_TERMINAL_FOCUS_REPORTING, W as estimateTokens, Wn as getDataDir, Wt as OSC11_RESPONSE, X as getBreathingFrame, Xn as CLI_USER_AGENT_PRODUCT, Xt as TERMINAL_THEME_DARK, Y as BREATHE_CYCLE_MS, Yn as CLI_UI_MODE, Yt as TERMINAL_FOCUS_OUT, Z as resetBreathingClock, Zn as PRODUCT_NAME, Zt as TERMINAL_THEME_LIGHT, _n as truncateErrorMessage, _r as SCREAM_ERROR_INFO, a as buildRoleAdditionalText, ar as readSessionIndex, b as selectUpdateTarget, bn as MAIN_AGENT_ID, cn as STATUS_BULLET, cr as isRealUserPrompt, ct as changeThinkingLevel, d as hasDispose, dn as argsRecord, dr as flushDiagnosticLogs, en as FooterComponent, er as saveCatalogCache, et as refineGoal, f as isExpandable, fn as formatErrorMessage, fr as log, gn as stringValue, gr as ErrorCodes, gt as handlePlanCommand, h as formatMemoryMemoForInjection, hn as serializeToolResultOutput, hr as isOrphanedToolCallError, i as openUrl, ir as appendSessionIndexEntry, j as renderDiffLinesClustered, jn as setExperimentalFlags, jt as isEmptySessionHintDismissed, k as ToolCallComponent, kn as sortSlashCommands, kt as createEditorTheme, ln as appendSessionHeaderHint, lr as parseRlmMaxDepthArg, lt as getModelCycleLevel, m as MoonLoader, mn as parseStreamingArgs, mr as isScreamError, mt as handleFusionPlanCommand, n as clearEvalPanelState, nn as SubagentSlots, nr as encodeWorkDirKey, o as writeTextClipboard, or as removeSessionIndexEntry, ot as isInternalMessage, p as isPlanExpandable, pn as isTodoItemShape, pr as resolveGlobalLogPath, q as AgentGroupComponent, qn as detectInstallSource, qt as QUERY_TERMINAL_THEME, rr as normalizeWorkDir, s as syncDiagramCapabilityPrompt, sn as printableChar, st as toTerminalHyperlink, t as dispatchInput, tn as lerpGradient, tr as ScreamHarness, u as disposeChildren, un as appendStreamingArgsPreview, ur as resolveScreamHome, w as readJsonlFile, wn as getCtrlDHint, wt as supportsBalance, x as refreshUpdateCache, xn as SESSION_TIPS, yn as EXIT_CONFIRM_WINDOW_MS, z as sanitizeShellOutput, zn as TuiConfigParseError, zt as DISABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-BPfUTccZ.mjs";
|
|
9
|
+
import { i as t, n as getLocale, r as setLocale } from "./text-input-dialog-DHDsHCqR.mjs";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
12
12
|
import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
13
13
|
import { Writable } from "node:stream";
|
|
14
14
|
import { WebSocket, WebSocketServer } from "ws";
|
|
15
|
-
import { appendFileSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { appendFileSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
16
16
|
import * as path$1 from "node:path";
|
|
17
17
|
import path, { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
18
18
|
import { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
@@ -33,7 +33,7 @@ function optionalBuildString(value) {
|
|
|
33
33
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
34
34
|
}
|
|
35
35
|
const SCREAM_BUILD_INFO = {
|
|
36
|
-
version: optionalBuildString("0.18.
|
|
36
|
+
version: optionalBuildString("0.18.8"),
|
|
37
37
|
channel: optionalBuildString(""),
|
|
38
38
|
commit: optionalBuildString(""),
|
|
39
39
|
buildTarget: optionalBuildString("darwin-arm64")
|
|
@@ -948,6 +948,91 @@ function wrapText$1(text, width) {
|
|
|
948
948
|
return lines.length > 0 ? lines : [""];
|
|
949
949
|
}
|
|
950
950
|
//#endregion
|
|
951
|
+
//#region src/tui/utils/runtime-marker.ts
|
|
952
|
+
const MARKER_VERSION = 1;
|
|
953
|
+
/** Directory holding one marker per live TUI process. */
|
|
954
|
+
function runtimeMarkerDir(homeDir) {
|
|
955
|
+
return join(homeDir, "runtime", "instances");
|
|
956
|
+
}
|
|
957
|
+
function markerPath(homeDir, pid) {
|
|
958
|
+
return join(runtimeMarkerDir(homeDir), `${pid}.json`);
|
|
959
|
+
}
|
|
960
|
+
/** True when `pid` exists and we may signal it (EPERM means it is alive). */
|
|
961
|
+
function isProcessAlive(pid) {
|
|
962
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
963
|
+
try {
|
|
964
|
+
process.kill(pid, 0);
|
|
965
|
+
return true;
|
|
966
|
+
} catch (error) {
|
|
967
|
+
return error?.code === "EPERM";
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Register this process as running. Written atomically (tmp + rename) so a
|
|
972
|
+
* crash mid-write cannot leave a torn JSON file behind.
|
|
973
|
+
*/
|
|
974
|
+
function writeRuntimeMarker(homeDir, info) {
|
|
975
|
+
mkdirSync(runtimeMarkerDir(homeDir), { recursive: true });
|
|
976
|
+
const payload = {
|
|
977
|
+
version: MARKER_VERSION,
|
|
978
|
+
...info
|
|
979
|
+
};
|
|
980
|
+
const target = markerPath(homeDir, info.pid);
|
|
981
|
+
const staged = `${target}.tmp`;
|
|
982
|
+
writeFileSync(staged, JSON.stringify(payload), "utf-8");
|
|
983
|
+
renameSync(staged, target);
|
|
984
|
+
}
|
|
985
|
+
/** Remove this process's marker. Best effort: never blocks shutdown. */
|
|
986
|
+
function clearRuntimeMarker(homeDir, pid = process.pid) {
|
|
987
|
+
try {
|
|
988
|
+
rmSync(markerPath(homeDir, pid), { force: true });
|
|
989
|
+
} catch {}
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Collect markers left by processes that are gone, newest first, and delete
|
|
993
|
+
* them. Unreadable files are removed too: they can only be leftovers, and
|
|
994
|
+
* keeping them would make the hint repeat forever.
|
|
995
|
+
*/
|
|
996
|
+
function collectStaleMarkers(homeDir) {
|
|
997
|
+
const dir = runtimeMarkerDir(homeDir);
|
|
998
|
+
if (!existsSync(dir)) return [];
|
|
999
|
+
const stale = [];
|
|
1000
|
+
const now = Date.now();
|
|
1001
|
+
for (const fileName of readdirSync(dir)) {
|
|
1002
|
+
if (fileName.endsWith(".json.tmp")) {
|
|
1003
|
+
const draft = join(dir, fileName);
|
|
1004
|
+
try {
|
|
1005
|
+
if (now - statSync(draft).mtimeMs > 5 * 6e4) rmSync(draft, { force: true });
|
|
1006
|
+
} catch {
|
|
1007
|
+
rmSync(draft, { force: true });
|
|
1008
|
+
}
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (!fileName.endsWith(".json")) continue;
|
|
1012
|
+
const filePath = join(dir, fileName);
|
|
1013
|
+
let marker;
|
|
1014
|
+
try {
|
|
1015
|
+
marker = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
1016
|
+
} catch {
|
|
1017
|
+
rmSync(filePath, { force: true });
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (marker === null || typeof marker !== "object" || typeof marker.pid !== "number" || marker.version !== MARKER_VERSION) {
|
|
1021
|
+
rmSync(filePath, { force: true });
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (isProcessAlive(marker.pid)) continue;
|
|
1025
|
+
stale.push({
|
|
1026
|
+
pid: marker.pid,
|
|
1027
|
+
startedAt: typeof marker.startedAt === "number" ? marker.startedAt : 0,
|
|
1028
|
+
...typeof marker.sessionId === "string" ? { sessionId: marker.sessionId } : {},
|
|
1029
|
+
...typeof marker.title === "string" ? { title: marker.title } : {}
|
|
1030
|
+
});
|
|
1031
|
+
rmSync(filePath, { force: true });
|
|
1032
|
+
}
|
|
1033
|
+
return stale.toSorted((a, b) => b.startedAt - a.startedAt);
|
|
1034
|
+
}
|
|
1035
|
+
//#endregion
|
|
951
1036
|
//#region src/tui/controllers/auth-flow.ts
|
|
952
1037
|
var AuthFlowController = class {
|
|
953
1038
|
host;
|
|
@@ -1785,8 +1870,9 @@ var EditorKeyboardController = class {
|
|
|
1785
1870
|
editor.onCtrlF = () => {
|
|
1786
1871
|
if (host.state.transcriptEntries.length > 0) return false;
|
|
1787
1872
|
if (isEmptySessionHintDismissed()) return false;
|
|
1788
|
-
|
|
1789
|
-
|
|
1873
|
+
const currentTip = SESSION_TIPS[host.lifecycleController.currentTipIdx];
|
|
1874
|
+
if ((currentTip?.isAd ?? false) !== true) return false;
|
|
1875
|
+
openUrl(currentTip?.url ?? "https://github.com/LIUTod/scream-code");
|
|
1790
1876
|
return true;
|
|
1791
1877
|
};
|
|
1792
1878
|
editor.onCtrlB = () => {
|
|
@@ -7681,6 +7767,41 @@ var TranscriptController = class TranscriptController {
|
|
|
7681
7767
|
}
|
|
7682
7768
|
};
|
|
7683
7769
|
//#endregion
|
|
7770
|
+
//#region src/tui/utils/tip-hyperlink.ts
|
|
7771
|
+
/**
|
|
7772
|
+
* Composes the idle tip line shown above the editor: the raw i18n text, with
|
|
7773
|
+
* an optional clickable domain for tips that carry a `url`.
|
|
7774
|
+
*
|
|
7775
|
+
* A tip whose text mentions its bare domain inline gets that domain wrapped as
|
|
7776
|
+
* an OSC 8 hyperlink (link color + underline) so it reads as a link against
|
|
7777
|
+
* the dimmed cue text. Clicking opens through the alt-screen openUrl callback;
|
|
7778
|
+
* Ctrl+F targets the same destination for terminals without mouse reporting.
|
|
7779
|
+
* Tips without a `url`, or whose text does not mention the domain, render
|
|
7780
|
+
* unchanged (e.g. the GitHub tip is keyboard-only and has no inline URL).
|
|
7781
|
+
*/
|
|
7782
|
+
function composeTipLabel(raw, tip, linkColor) {
|
|
7783
|
+
if (tip.url === void 0) return raw;
|
|
7784
|
+
const domain = tip.url.replace(/^https?:\/\//iu, "");
|
|
7785
|
+
if (!raw.includes(domain)) return raw;
|
|
7786
|
+
const link = toTerminalHyperlink(chalk.hex(linkColor).underline(domain), tip.url);
|
|
7787
|
+
return raw.replace(domain, link);
|
|
7788
|
+
}
|
|
7789
|
+
/**
|
|
7790
|
+
* Weighted rotation pick: `OFFER_TIP_PROBABILITY` of the rolls select the
|
|
7791
|
+
* offer tip (regardless of what is currently shown — a re-pick keeps the ad
|
|
7792
|
+
* line up), everything else picks uniformly excluding the current tip.
|
|
7793
|
+
*/
|
|
7794
|
+
function pickTipIndex(tips, current) {
|
|
7795
|
+
if (tips.length <= 1) return Math.max(0, current ?? 0);
|
|
7796
|
+
const offerIndex = tips.findIndex((tip) => tip.isAd && tip.url !== void 0);
|
|
7797
|
+
if (offerIndex !== -1 && Math.random() < .8) return offerIndex;
|
|
7798
|
+
let next;
|
|
7799
|
+
do
|
|
7800
|
+
next = Math.floor(Math.random() * tips.length);
|
|
7801
|
+
while (next === current);
|
|
7802
|
+
return next;
|
|
7803
|
+
}
|
|
7804
|
+
//#endregion
|
|
7684
7805
|
//#region src/tui/components/chrome/gutter-container.ts
|
|
7685
7806
|
/**
|
|
7686
7807
|
* Container that reserves left/right gutter columns around its children,
|
|
@@ -8915,7 +9036,7 @@ var LifecycleController = class LifecycleController {
|
|
|
8915
9036
|
return usage.inputOther + usage.inputCacheRead + usage.inputCacheCreation + usage.output;
|
|
8916
9037
|
}
|
|
8917
9038
|
tipRotationTimer;
|
|
8918
|
-
currentTipIndex =
|
|
9039
|
+
currentTipIndex = pickTipIndex(SESSION_TIPS, void 0);
|
|
8919
9040
|
/** Sidebar-only git cache (the footer owns its own). Rebuilt when the
|
|
8920
9041
|
* workDir changes; `getStatus()` is TTL-cached so polling is cheap. */
|
|
8921
9042
|
sidebarGitCache;
|
|
@@ -9345,7 +9466,7 @@ var LifecycleController = class LifecycleController {
|
|
|
9345
9466
|
const tip = SESSION_TIPS[this.currentTipIndex] ?? SESSION_TIPS[0];
|
|
9346
9467
|
state.statusBarContainer.addChild(new StatusBarPaneComponent({
|
|
9347
9468
|
mode: "idle",
|
|
9348
|
-
label: t(tip.i18nKey),
|
|
9469
|
+
label: composeTipLabel(t(tip.i18nKey), tip, state.theme.colors.primary),
|
|
9349
9470
|
labelColor: "#999999"
|
|
9350
9471
|
}));
|
|
9351
9472
|
this.startTipRotation();
|
|
@@ -9354,6 +9475,9 @@ var LifecycleController = class LifecycleController {
|
|
|
9354
9475
|
}
|
|
9355
9476
|
state.ui.requestRender();
|
|
9356
9477
|
}
|
|
9478
|
+
/** The idle tip line delegates its optional domain link to composeTipLabel
|
|
9479
|
+
* (see tui/utils/tip-hyperlink.ts): linked tips render the bare domain as
|
|
9480
|
+
* a clickable OSC 8 hyperlink against the dimmed cue text. */
|
|
9357
9481
|
/** Builds the reconnect label shown next to the status-bar pulse wave
|
|
9358
9482
|
* while a step retry is in flight, e.g. "重连中 3/10 · 限流 · 20s 后重试".
|
|
9359
9483
|
* Returns an empty string when no retry is active. Static by design: the
|
|
@@ -9384,17 +9508,13 @@ var LifecycleController = class LifecycleController {
|
|
|
9384
9508
|
get currentTipIdx() {
|
|
9385
9509
|
return this.currentTipIndex;
|
|
9386
9510
|
}
|
|
9387
|
-
/** Start the
|
|
9388
|
-
*
|
|
9511
|
+
/** Start the weighted tip rotation timer: 80% of rotations land on the
|
|
9512
|
+
* offer tip (see tui/utils/tip-hyperlink.ts), the rest spread uniformly
|
|
9513
|
+
* and never repeat the line currently on screen. */
|
|
9389
9514
|
startTipRotation() {
|
|
9390
9515
|
if (this.tipRotationTimer !== void 0) return;
|
|
9391
9516
|
this.tipRotationTimer = setInterval(() => {
|
|
9392
|
-
|
|
9393
|
-
let next;
|
|
9394
|
-
do
|
|
9395
|
-
next = Math.floor(Math.random() * SESSION_TIPS.length);
|
|
9396
|
-
while (next === this.currentTipIndex);
|
|
9397
|
-
this.currentTipIndex = next;
|
|
9517
|
+
this.currentTipIndex = pickTipIndex(SESSION_TIPS, this.currentTipIndex);
|
|
9398
9518
|
this.lastActivityMode = void 0;
|
|
9399
9519
|
this.updateActivityPane();
|
|
9400
9520
|
}, TIP_ROTATION_INTERVAL_MS);
|
|
@@ -10333,116 +10453,6 @@ var QuestionController = class extends ReverseRpcController {
|
|
|
10333
10453
|
}
|
|
10334
10454
|
};
|
|
10335
10455
|
//#endregion
|
|
10336
|
-
//#region src/utils/clipboard/clipboard-text.ts
|
|
10337
|
-
/**
|
|
10338
|
-
* Write plain text to the system clipboard with platform-specific commands.
|
|
10339
|
-
*
|
|
10340
|
-
* scream-core's clipboard layer is read-only (clipboard-native / clipboard-image),
|
|
10341
|
-
* so the TUI's "copy selected text" needs its own writer. Used to back the
|
|
10342
|
-
* pi-tui `copySelection` hook — return true/false so the TUI can flash
|
|
10343
|
-
* "Copied!" or "Copy failed".
|
|
10344
|
-
*
|
|
10345
|
-
* Platform mapping:
|
|
10346
|
-
* macOS -> pbcopy
|
|
10347
|
-
* Windows-> powershell Set-Clipboard (base64 payload, avoids quoting issues)
|
|
10348
|
-
* Linux -> wl-copy (Wayland) / xclip or xsel (X11), tried in a candidate
|
|
10349
|
-
* chain so a missing tool (Ubuntu ships neither by default)
|
|
10350
|
-
* falls through to the next one instead of failing outright.
|
|
10351
|
-
*/
|
|
10352
|
-
var ClipboardTextError = class extends Error {
|
|
10353
|
-
constructor(message) {
|
|
10354
|
-
super(message);
|
|
10355
|
-
this.name = "ClipboardTextError";
|
|
10356
|
-
}
|
|
10357
|
-
};
|
|
10358
|
-
const defaultRunCommand = (command, args, input) => {
|
|
10359
|
-
const result = spawnSync(command, args, {
|
|
10360
|
-
input,
|
|
10361
|
-
encoding: "utf8",
|
|
10362
|
-
timeout: 3e3
|
|
10363
|
-
});
|
|
10364
|
-
if (result.status === 0) return { ok: true };
|
|
10365
|
-
return {
|
|
10366
|
-
ok: false,
|
|
10367
|
-
error: result.error?.message ?? result.stderr?.toString() ?? `exit ${result.status}`
|
|
10368
|
-
};
|
|
10369
|
-
};
|
|
10370
|
-
function detectPlatform() {
|
|
10371
|
-
if (process.platform === "darwin") return "macos";
|
|
10372
|
-
if (process.platform === "win32") return "windows";
|
|
10373
|
-
if (process.platform === "linux") return process.env["WAYLAND_DISPLAY"] ? "linux-wayland" : "linux-x11";
|
|
10374
|
-
return "unsupported";
|
|
10375
|
-
}
|
|
10376
|
-
function linuxCandidates(platform) {
|
|
10377
|
-
const wl = {
|
|
10378
|
-
command: "wl-copy",
|
|
10379
|
-
args: []
|
|
10380
|
-
};
|
|
10381
|
-
const xclip = {
|
|
10382
|
-
command: "xclip",
|
|
10383
|
-
args: [
|
|
10384
|
-
"-selection",
|
|
10385
|
-
"clipboard",
|
|
10386
|
-
"-i"
|
|
10387
|
-
]
|
|
10388
|
-
};
|
|
10389
|
-
const xsel = {
|
|
10390
|
-
command: "xsel",
|
|
10391
|
-
args: ["--clipboard", "--input"]
|
|
10392
|
-
};
|
|
10393
|
-
return platform === "linux-wayland" ? [
|
|
10394
|
-
wl,
|
|
10395
|
-
xclip,
|
|
10396
|
-
xsel
|
|
10397
|
-
] : [
|
|
10398
|
-
xclip,
|
|
10399
|
-
xsel,
|
|
10400
|
-
wl
|
|
10401
|
-
];
|
|
10402
|
-
}
|
|
10403
|
-
/**
|
|
10404
|
-
* Write `text` to the system clipboard. Throws ClipboardTextError when the
|
|
10405
|
-
* platform is unsupported or every candidate command fails, so callers can
|
|
10406
|
-
* surface a "Copy failed" indicator.
|
|
10407
|
-
*/
|
|
10408
|
-
async function writeTextClipboard(text, runCommand = defaultRunCommand) {
|
|
10409
|
-
const platform = detectPlatform();
|
|
10410
|
-
let command;
|
|
10411
|
-
let args;
|
|
10412
|
-
let input;
|
|
10413
|
-
switch (platform) {
|
|
10414
|
-
case "macos":
|
|
10415
|
-
command = "pbcopy";
|
|
10416
|
-
args = [];
|
|
10417
|
-
input = text;
|
|
10418
|
-
break;
|
|
10419
|
-
case "windows": {
|
|
10420
|
-
const base64 = Buffer.from(text, "utf8").toString("base64");
|
|
10421
|
-
command = "powershell";
|
|
10422
|
-
args = [
|
|
10423
|
-
"-NoProfile",
|
|
10424
|
-
"-Command",
|
|
10425
|
-
`[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${base64}')) | Set-Clipboard`
|
|
10426
|
-
];
|
|
10427
|
-
input = "";
|
|
10428
|
-
break;
|
|
10429
|
-
}
|
|
10430
|
-
case "linux-wayland":
|
|
10431
|
-
case "linux-x11": {
|
|
10432
|
-
const failures = [];
|
|
10433
|
-
for (const candidate of linuxCandidates(platform)) {
|
|
10434
|
-
const result = runCommand(candidate.command, candidate.args, text);
|
|
10435
|
-
if (result.ok) return;
|
|
10436
|
-
failures.push(`${candidate.command}: ${result.error ?? "unknown error"}`);
|
|
10437
|
-
}
|
|
10438
|
-
throw new ClipboardTextError(`failed to copy to clipboard (${failures.join(" | ")}). Install wl-clipboard (Wayland: sudo apt install wl-clipboard) or xclip/xsel (X11: sudo apt install xclip xsel)`);
|
|
10439
|
-
}
|
|
10440
|
-
default: throw new ClipboardTextError(`unsupported clipboard platform: ${process.platform}`);
|
|
10441
|
-
}
|
|
10442
|
-
const result = runCommand(command, args, input);
|
|
10443
|
-
if (!result.ok) throw new ClipboardTextError(`failed to copy to clipboard: ${command} ${result.error ?? "unknown error"}`);
|
|
10444
|
-
}
|
|
10445
|
-
//#endregion
|
|
10446
10456
|
//#region src/tui/utils/render-batcher.ts
|
|
10447
10457
|
function createRenderBatcher(doRender) {
|
|
10448
10458
|
let scheduled = false;
|
|
@@ -12963,69 +12973,86 @@ function stripLeadingSpace(s) {
|
|
|
12963
12973
|
* it as the focused component, and tears it down on the `onClose`
|
|
12964
12974
|
* callback (fired on Esc / Enter / q).
|
|
12965
12975
|
*/
|
|
12966
|
-
/**
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
12976
|
+
/**
|
|
12977
|
+
* Keyboard shortcut reference — keep in sync with the global editor bindings.
|
|
12978
|
+
*
|
|
12979
|
+
* A function, not a constant: `t()` must run on every call. As a module-level
|
|
12980
|
+
* constant the descriptions were evaluated once at import time and froze to
|
|
12981
|
+
* whichever locale happened to be active then, so a Chinese session showed an
|
|
12982
|
+
* English shortcut list (and vice versa).
|
|
12983
|
+
*/
|
|
12984
|
+
function defaultKeyboardShortcuts() {
|
|
12985
|
+
return [
|
|
12986
|
+
{
|
|
12987
|
+
keys: "Shift-Tab",
|
|
12988
|
+
description: t("help.toggle_plan")
|
|
12989
|
+
},
|
|
12990
|
+
{
|
|
12991
|
+
keys: "Tab",
|
|
12992
|
+
description: t("help.cycle_thinking")
|
|
12993
|
+
},
|
|
12994
|
+
{
|
|
12995
|
+
keys: "Ctrl-G",
|
|
12996
|
+
description: t("help.external_editor")
|
|
12997
|
+
},
|
|
12998
|
+
{
|
|
12999
|
+
keys: "Ctrl-O",
|
|
13000
|
+
description: t("help.toggle_output")
|
|
13001
|
+
},
|
|
13002
|
+
{
|
|
13003
|
+
keys: "Ctrl-E",
|
|
13004
|
+
description: t("help.toggle_plan_view")
|
|
13005
|
+
},
|
|
13006
|
+
{
|
|
13007
|
+
keys: "Ctrl-S",
|
|
13008
|
+
description: t("help.interrupt")
|
|
13009
|
+
},
|
|
13010
|
+
{
|
|
13011
|
+
keys: "Ctrl-X",
|
|
13012
|
+
description: t("help.toggle_sidebar")
|
|
13013
|
+
},
|
|
13014
|
+
{
|
|
13015
|
+
keys: "Ctrl-B",
|
|
13016
|
+
description: t("help.toggle_hint")
|
|
13017
|
+
},
|
|
13018
|
+
{
|
|
13019
|
+
keys: "Ctrl-W",
|
|
13020
|
+
description: t("help.cancel_memory")
|
|
13021
|
+
},
|
|
13022
|
+
{
|
|
13023
|
+
keys: "↑",
|
|
13024
|
+
description: t("help.recall_queued")
|
|
13025
|
+
},
|
|
13026
|
+
{
|
|
13027
|
+
keys: "Ctrl-V / Alt-V",
|
|
13028
|
+
description: t("help.paste_image")
|
|
13029
|
+
},
|
|
13030
|
+
{
|
|
13031
|
+
keys: "Shift-Enter / Ctrl-J",
|
|
13032
|
+
description: t("help.newline")
|
|
13033
|
+
},
|
|
13034
|
+
{
|
|
13035
|
+
keys: "Ctrl-C",
|
|
13036
|
+
description: t("help.cancel_stream")
|
|
13037
|
+
},
|
|
13038
|
+
{
|
|
13039
|
+
keys: "Ctrl-D",
|
|
13040
|
+
description: t("help.exit")
|
|
13041
|
+
},
|
|
13042
|
+
{
|
|
13043
|
+
keys: "Esc",
|
|
13044
|
+
description: t("help.close_dialog")
|
|
13045
|
+
},
|
|
13046
|
+
{
|
|
13047
|
+
keys: "↑ / ↓",
|
|
13048
|
+
description: t("help.browse_history")
|
|
13049
|
+
},
|
|
13050
|
+
{
|
|
13051
|
+
keys: "Enter",
|
|
13052
|
+
description: t("help.submit")
|
|
13053
|
+
}
|
|
13054
|
+
];
|
|
13055
|
+
}
|
|
13029
13056
|
var HelpPanelComponent = class extends Container {
|
|
13030
13057
|
focused = false;
|
|
13031
13058
|
opts;
|
|
@@ -13061,33 +13088,42 @@ var HelpPanelComponent = class extends Container {
|
|
|
13061
13088
|
const muted = chalk.hex(colors.textMuted);
|
|
13062
13089
|
const kbdColor = chalk.hex(colors.warning);
|
|
13063
13090
|
const slashColor = chalk.hex(colors.primary);
|
|
13064
|
-
const shortcuts = this.opts.shortcuts ??
|
|
13065
|
-
const
|
|
13091
|
+
const shortcuts = this.opts.shortcuts ?? defaultKeyboardShortcuts();
|
|
13092
|
+
const padToWidth = (text, width) => {
|
|
13093
|
+
const pad = width - visibleWidth(text);
|
|
13094
|
+
return pad > 0 ? text + " ".repeat(pad) : text;
|
|
13095
|
+
};
|
|
13096
|
+
const kbdWidth = Math.max(8, ...shortcuts.map((s) => visibleWidth(s.keys)));
|
|
13066
13097
|
const sortedCmds = [...this.opts.commands].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
13067
13098
|
const cmdLabels = sortedCmds.map((c) => {
|
|
13068
13099
|
const aliases = c.aliases.length > 0 ? ` (${c.aliases.map((a) => "/" + a).join(", ")})` : "";
|
|
13069
13100
|
return `/${c.name}${aliases}`;
|
|
13070
13101
|
});
|
|
13071
13102
|
const cmdWidth = Math.max(12, ...cmdLabels.map((l) => l.length));
|
|
13103
|
+
const hasCommands = sortedCmds.length > 0;
|
|
13072
13104
|
const lines = [
|
|
13073
13105
|
accent("─".repeat(width)),
|
|
13074
|
-
accent.bold(t("help.title")) + muted(` · ${t("help.close_hint")}`),
|
|
13075
|
-
"",
|
|
13076
|
-
` ${dim(t("help.welcome_msg"))}`,
|
|
13077
|
-
` ${muted(t("help.hidden_commands"))}`,
|
|
13106
|
+
accent.bold(this.opts.title ?? t("help.title")) + muted(` · ${t("help.close_hint")}`),
|
|
13078
13107
|
"",
|
|
13108
|
+
...hasCommands ? [
|
|
13109
|
+
` ${dim(t("help.welcome_msg"))}`,
|
|
13110
|
+
` ${muted(t("help.hidden_commands"))}`,
|
|
13111
|
+
""
|
|
13112
|
+
] : [],
|
|
13079
13113
|
` ${chalk.bold(t("help.shortcuts"))}`,
|
|
13080
|
-
...shortcuts.map((s) => ` ${kbdColor(s.keys
|
|
13081
|
-
"",
|
|
13082
|
-
` ${chalk.bold(t("help.slash_commands"))}`,
|
|
13083
|
-
...sortedCmds.map((cmd, i) => {
|
|
13084
|
-
return ` ${slashColor((cmdLabels[i] ?? `/${cmd.name}`).padEnd(cmdWidth))} ${dim(t(cmd.description))}`;
|
|
13085
|
-
}),
|
|
13114
|
+
...shortcuts.map((s) => ` ${kbdColor(padToWidth(s.keys, kbdWidth))} ${dim(s.description)}`),
|
|
13086
13115
|
"",
|
|
13116
|
+
...hasCommands ? [
|
|
13117
|
+
` ${chalk.bold(t("help.slash_commands"))}`,
|
|
13118
|
+
...sortedCmds.map((cmd, i) => {
|
|
13119
|
+
return ` ${slashColor((cmdLabels[i] ?? `/${cmd.name}`).padEnd(cmdWidth))} ${dim(t(cmd.description))}`;
|
|
13120
|
+
}),
|
|
13121
|
+
""
|
|
13122
|
+
] : [],
|
|
13087
13123
|
accent("─".repeat(width))
|
|
13088
13124
|
];
|
|
13089
13125
|
const content = lines.slice(1, lines.length - 1);
|
|
13090
|
-
const maxVisible = Math.max(5, this.opts.maxVisible ??
|
|
13126
|
+
const maxVisible = Math.max(5, this.opts.maxVisible ?? 30);
|
|
13091
13127
|
if (content.length > maxVisible) {
|
|
13092
13128
|
this.scrollTop = Math.max(0, Math.min(this.scrollTop, content.length - maxVisible));
|
|
13093
13129
|
const slice = content.slice(this.scrollTop, this.scrollTop + maxVisible);
|
|
@@ -14442,6 +14478,22 @@ var DialogManager = class {
|
|
|
14442
14478
|
this.host.state.activeDialog = null;
|
|
14443
14479
|
this.restoreEditor();
|
|
14444
14480
|
}
|
|
14481
|
+
showHotkeysPanel() {
|
|
14482
|
+
this.host.state.activeDialog = "hotkeys";
|
|
14483
|
+
this.mountEditorReplacement(new HelpPanelComponent({
|
|
14484
|
+
commands: [],
|
|
14485
|
+
shortcuts: defaultKeyboardShortcuts(),
|
|
14486
|
+
colors: this.host.state.theme.colors,
|
|
14487
|
+
title: t("help.hotkeys_title"),
|
|
14488
|
+
onClose: () => {
|
|
14489
|
+
this.hideHotkeysPanel();
|
|
14490
|
+
}
|
|
14491
|
+
}));
|
|
14492
|
+
}
|
|
14493
|
+
hideHotkeysPanel() {
|
|
14494
|
+
this.host.state.activeDialog = null;
|
|
14495
|
+
this.restoreEditor();
|
|
14496
|
+
}
|
|
14445
14497
|
async showSessionPicker() {
|
|
14446
14498
|
await this.host.fetchSessions();
|
|
14447
14499
|
this.host.state.activeDialog = "session-picker";
|
|
@@ -14715,6 +14767,7 @@ var ScreamTUI = class {
|
|
|
14715
14767
|
deferUserMessages = false;
|
|
14716
14768
|
aborted = false;
|
|
14717
14769
|
isShuttingDown = false;
|
|
14770
|
+
startedAt = Date.now();
|
|
14718
14771
|
reverseRpcDisposers = [];
|
|
14719
14772
|
startupNotice;
|
|
14720
14773
|
/** Interval handle for the periodic provider-balance refresh. */
|
|
@@ -14821,6 +14874,8 @@ var ScreamTUI = class {
|
|
|
14821
14874
|
this.lifecycleController.startEventLoop();
|
|
14822
14875
|
try {
|
|
14823
14876
|
await this.finishStartup(shouldReplayHistory);
|
|
14877
|
+
this.refreshRuntimeMarker();
|
|
14878
|
+
this.reportStaleSessions();
|
|
14824
14879
|
this.lifecycleController.startCcConnectPolling();
|
|
14825
14880
|
this.startBalancePolling();
|
|
14826
14881
|
} catch (error) {
|
|
@@ -14905,6 +14960,34 @@ var ScreamTUI = class {
|
|
|
14905
14960
|
throw error;
|
|
14906
14961
|
}
|
|
14907
14962
|
}
|
|
14963
|
+
/** Keep this instance's crash marker in sync with the running session. */
|
|
14964
|
+
refreshRuntimeMarker() {
|
|
14965
|
+
try {
|
|
14966
|
+
writeRuntimeMarker(getDataDir(), {
|
|
14967
|
+
pid: process.pid,
|
|
14968
|
+
startedAt: this.startedAt,
|
|
14969
|
+
...this.state.appState.sessionId ? { sessionId: this.state.appState.sessionId } : {},
|
|
14970
|
+
...this.state.appState.sessionTitle ? { title: this.state.appState.sessionTitle } : {}
|
|
14971
|
+
});
|
|
14972
|
+
} catch {}
|
|
14973
|
+
}
|
|
14974
|
+
/** Report a previous instance that never shut down, exactly once. */
|
|
14975
|
+
reportStaleSessions() {
|
|
14976
|
+
let stale = [];
|
|
14977
|
+
try {
|
|
14978
|
+
stale = collectStaleMarkers(getDataDir());
|
|
14979
|
+
} catch {
|
|
14980
|
+
return;
|
|
14981
|
+
}
|
|
14982
|
+
const latest = stale[0];
|
|
14983
|
+
if (latest === void 0) return;
|
|
14984
|
+
const label = latest.title ?? (latest.sessionId !== void 0 ? latest.sessionId.slice(0, 8) : "");
|
|
14985
|
+
if (label.length === 0) {
|
|
14986
|
+
this.showNotice(t("tui.crash_recovery_title"));
|
|
14987
|
+
return;
|
|
14988
|
+
}
|
|
14989
|
+
this.showNotice(t("tui.crash_recovery_title"), t("tui.crash_recovery_detail", { title: label }));
|
|
14990
|
+
}
|
|
14908
14991
|
async stop(exitCode) {
|
|
14909
14992
|
if (this.isShuttingDown) return;
|
|
14910
14993
|
this.isShuttingDown = true;
|
|
@@ -14925,6 +15008,7 @@ var ScreamTUI = class {
|
|
|
14925
15008
|
this.inputController.dispose();
|
|
14926
15009
|
this.state.footer.dispose();
|
|
14927
15010
|
this.state.sidebarContainer.dispose();
|
|
15011
|
+
clearRuntimeMarker(getDataDir());
|
|
14928
15012
|
await this.closeSession();
|
|
14929
15013
|
await this.harness.close();
|
|
14930
15014
|
this.sessionEventHandler.stopAllMcpServerStatusSpinners();
|
|
@@ -14955,6 +15039,7 @@ var ScreamTUI = class {
|
|
|
14955
15039
|
onEmergencyExit(exitCode = 129) {
|
|
14956
15040
|
this.isShuttingDown = true;
|
|
14957
15041
|
this.lifecycleController.uninstallSignalHandlers();
|
|
15042
|
+
clearRuntimeMarker(getDataDir());
|
|
14958
15043
|
process.exit(exitCode);
|
|
14959
15044
|
}
|
|
14960
15045
|
stopMemoryIdleTimer() {
|
|
@@ -15104,6 +15189,7 @@ var ScreamTUI = class {
|
|
|
15104
15189
|
const planModeChanged = "planMode" in patch && prevPlanMode !== this.state.appState.planMode;
|
|
15105
15190
|
const sessionChanged = "sessionId" in patch && this.state.appState.sessionId !== prevSessionId;
|
|
15106
15191
|
if (sessionChanged) {
|
|
15192
|
+
this.refreshRuntimeMarker();
|
|
15107
15193
|
if (!("sessionUsage" in patch)) this.state.appState.sessionUsage = {
|
|
15108
15194
|
inputOther: 0,
|
|
15109
15195
|
output: 0,
|
|
@@ -15235,7 +15321,7 @@ var ScreamTUI = class {
|
|
|
15235
15321
|
const card = new OnboardingCardComponent({
|
|
15236
15322
|
colors: this.state.theme.colors,
|
|
15237
15323
|
onConfigure: () => {
|
|
15238
|
-
import("./dispatch-
|
|
15324
|
+
import("./dispatch-zsFzFFoY.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
|
|
15239
15325
|
this.restoreEditor();
|
|
15240
15326
|
onDone?.();
|
|
15241
15327
|
});
|
|
@@ -15273,7 +15359,9 @@ var ScreamTUI = class {
|
|
|
15273
15359
|
let error;
|
|
15274
15360
|
let failureKind;
|
|
15275
15361
|
try {
|
|
15276
|
-
({ok, error, failureKind} = await startManualEmbeddingDownload()
|
|
15362
|
+
({ok, error, failureKind} = await startManualEmbeddingDownload((progress) => {
|
|
15363
|
+
spinner.setLabel(embeddingProgressLabel(progress));
|
|
15364
|
+
}));
|
|
15277
15365
|
} catch (error) {
|
|
15278
15366
|
error = error instanceof Error ? error.message : String(error);
|
|
15279
15367
|
}
|
|
@@ -15361,6 +15449,9 @@ var ScreamTUI = class {
|
|
|
15361
15449
|
showHelpPanel() {
|
|
15362
15450
|
this.dialogManager.showHelpPanel(this.getSlashCommands());
|
|
15363
15451
|
}
|
|
15452
|
+
showHotkeysPanel() {
|
|
15453
|
+
this.dialogManager.showHotkeysPanel();
|
|
15454
|
+
}
|
|
15364
15455
|
hideHelpPanel() {
|
|
15365
15456
|
this.dialogManager.hideHelpPanel();
|
|
15366
15457
|
}
|