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.
@@ -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-C7C9e5Ds.mjs";
8
- import { $ as refineGoal, $n as ScreamHarness, $t as FooterComponent, A as renderDiffLinesClustered, An as setExperimentalFlags, At as isEmptySessionHintDismissed, B as AssistantMessageComponent, Bn as saveTuiConfig, Bt as ENABLE_TERMINAL_FOCUS_REPORTING, C as readJsonlFile, Cn as getCtrlDHint, Ct as supportsBalance, D as UserMessageComponent, Dn as BUILTIN_SLASH_COMMANDS, Dt as resolveThemeSync, En as buildSkillSlashCommands, F as NoticeMessageComponent, Fn as PIXEL_PULSE_FRAMES, Ft as getColorPalette, G as BackgroundAgentStatusComponent, Gn as detectInstallSource, Gt as OSC11_RESPONSE_PREFIX_NO_ESC, H as charsForTokenBudget, Hn as getDataDir, Ht as OSC11_QUERY, I as StatusMessageComponent, In as PULSE_WAVE_FRAMES, It as detectTerminalTheme, J as BREATHE_CYCLE_MS, Jn as CLI_USER_AGENT_PRODUCT, Jt as TERMINAL_FOCUS_OUT, K as AgentGroupComponent, Kn as CLI_COMMAND_NAME, Kt as QUERY_TERMINAL_THEME, L as replaceTabs, Ln as TuiConfigParseError, Lt as parseOsc11BackgroundTheme, M as langFromPath, Mn as getKnowledgeStore, Mt as toggleEmptySessionHint, N as CachedContainer, Nn as hasEmbeddingModelCache, Nt as createThemeStyles, O as ToolCallComponent, On as sortSlashCommands, Ot as createEditorTheme, P as ThinkingComponent, Pn as startManualEmbeddingDownload, Pt as contrastTextHex, Q as clearGoalState, Qn as saveCatalogCache, Qt as isStreaming, R as sanitizeShellOutput, Rn as TuiLikePreferencesSchema, Rt as DISABLE_TERMINAL_FOCUS_REPORTING, S as appendJsonlLine, Sn as getCtrlCHint, St as refreshProviderBalance, T as getDaemonInstructions, Tn as getNoActiveSessionMessage, U as estimateTokens, Un as getInputHistoryFile, Ut as OSC11_RESPONSE, V as ActivityGroupComponent, Vn as detectShellEnvironment, Vt as ENABLE_TERMINAL_THEME_REPORTING, W as getSharedSpeedTracker, Wn as getLogDir, Wt as OSC11_RESPONSE_PREFIX, X as resetBreathingClock, Xn as DEFAULT_CATALOG_URL, Xt as TERMINAL_THEME_LIGHT, Y as getBreathingFrame, Yn as PRODUCT_NAME, Yt as TERMINAL_THEME_DARK, Z as CompactionComponent, Zn as fetchCatalog, Zt as isBusy, _n as EMPTY_SESSION_HINT_URL, a as buildRoleAdditionalText, at as isInternalMessage, b as refreshUpdateCache, bn as SESSION_TIPS, cn as appendSessionHeaderHint, cr as resolveScreamHome, ct as getModelCycleLevel, d as isExpandable, dn as formatErrorMessage, dr as resolveGlobalLogPath, en as lerpGradient, er as encodeWorkDirKey, f as isPlanExpandable, fn as isTodoItemShape, fr as isScreamError, gn as truncateErrorMessage, hn as stringValue, hr as SCREAM_ERROR_INFO, ht as handlePlanCommand, i as openUrl, ir as removeSessionIndexEntry, j as highlightLines, jn as embeddingFailureDetail, jt as isTurnElapsedEnabled, k as renderDiffLines, kn as isExperimentalFlagEnabled, kt as createMarkdownTheme, l as disposeChildren, ln as appendStreamingArgsPreview, lr as flushDiagnosticLogs, m as formatMemoryMemoForInjection, mn as serializeToolResultOutput, mr as ErrorCodes, n as clearEvalPanelState, nr as appendSessionIndexEntry, o as syncDiagramCapabilityPrompt, on as printableChar, or as isRealUserPrompt, ot as toTerminalHyperlink, p as MoonLoader, pn as parseStreamingArgs, pr as isOrphanedToolCallError, pt as handleFusionPlanCommand, q as WelcomeComponent, qn as CLI_UI_MODE, qt as TERMINAL_FOCUS_IN, rr as readSessionIndex, sn as STATUS_BULLET, sr as parseRlmMaxDepthArg, st as changeThinkingLevel, t as dispatchInput, tn as SubagentSlots, tr as normalizeWorkDir, u as hasDispose, un as argsRecord, ur as log, vn as EXIT_CONFIRM_WINDOW_MS, wn as getLlmNotSetMessage, wt as clearInfoPanelState, x as readUpdateCache, xn as TIP_ROTATION_INTERVAL_MS, y as selectUpdateTarget, yn as MAIN_AGENT_ID, z as SkillActivationComponent, zn as loadTuiConfig, zt as DISABLE_TERMINAL_THEME_REPORTING } from "./dispatch-BgO7wAf9.mjs";
9
- import { i as t, n as getLocale, r as setLocale } from "./text-input-dialog-BvbqjvzB.mjs";
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.6"),
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
- if (!(SESSION_TIPS[host.lifecycleController.currentTipIdx]?.isAd ?? false)) return false;
1789
- openUrl(EMPTY_SESSION_HINT_URL);
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 = Math.floor(Math.random() * SESSION_TIPS.length);
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 random tip rotation timer. Picks a random tip different
9388
- * from the current one and refreshes the status bar. */
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
- if (SESSION_TIPS.length <= 1) return;
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
- /** Static list — keep in sync with the global editor bindings. */
12967
- const DEFAULT_KEYBOARD_SHORTCUTS = [
12968
- {
12969
- keys: "Shift-Tab",
12970
- description: t("help.toggle_plan")
12971
- },
12972
- {
12973
- keys: "Tab",
12974
- description: t("help.cycle_thinking")
12975
- },
12976
- {
12977
- keys: "Ctrl-G",
12978
- description: t("help.external_editor")
12979
- },
12980
- {
12981
- keys: "Ctrl-O",
12982
- description: t("help.toggle_output")
12983
- },
12984
- {
12985
- keys: "Ctrl-E",
12986
- description: t("help.toggle_plan_view")
12987
- },
12988
- {
12989
- keys: "Ctrl-S",
12990
- description: t("help.interrupt")
12991
- },
12992
- {
12993
- keys: "Ctrl-X",
12994
- description: t("help.toggle_sidebar")
12995
- },
12996
- {
12997
- keys: "Ctrl-B",
12998
- description: t("help.toggle_hint")
12999
- },
13000
- {
13001
- keys: "Ctrl-W",
13002
- description: t("help.cancel_memory")
13003
- },
13004
- {
13005
- keys: "Shift-Enter / Ctrl-J",
13006
- description: t("help.newline")
13007
- },
13008
- {
13009
- keys: "Ctrl-C",
13010
- description: t("help.cancel_stream")
13011
- },
13012
- {
13013
- keys: "Ctrl-D",
13014
- description: t("help.exit")
13015
- },
13016
- {
13017
- keys: "Esc",
13018
- description: t("help.close_dialog")
13019
- },
13020
- {
13021
- keys: "↑ / ↓",
13022
- description: t("help.browse_history")
13023
- },
13024
- {
13025
- keys: "Enter",
13026
- description: t("help.submit")
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 ?? DEFAULT_KEYBOARD_SHORTCUTS;
13065
- const kbdWidth = Math.max(8, ...shortcuts.map((s) => s.keys.length));
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.padEnd(kbdWidth))} ${dim(s.description)}`),
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 ?? 24);
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-0mihbsV3.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
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
  }