scream-code 0.18.7 → 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 CHANGED
@@ -31,7 +31,8 @@ npm install -g scream-code
31
31
  ```
32
32
 
33
33
  > [!NOTE]
34
- > Requires **Node.js >= 22**. Git recommended. Same command installs and updates.
34
+ > Requires **Node.js >= 22**. Git recommended. Same command installs and updates. No native build steps and no dependency install scripts: the local embedding engine runs on WebAssembly.
35
+ > The first use of the knowledge/memory vector features downloads a ~53MB model into `~/.scream-code/cache` (progress is shown, and the download verifies its checksum before use).
35
36
 
36
37
  ### Quick Start
37
38
 
@@ -42,7 +43,7 @@ scream --auto # auto permission mode
42
43
  scream -y # auto-approve mode
43
44
  ```
44
45
 
45
- On first launch Scream walks you through model setup (`/config`). 130+ providers built in — or point it at any OpenAI-compatible endpoint with `/config diy`. Switch models anytime with `/model`, no restart needed — or use `/model diy` to give each sub-agent its own model, letting every model do what it does best. Generate or edit images the same way: run `/config image` once, then just ask for a picture (the `/image` skill).
46
+ On first launch Scream walks you through model setup (`/config` — pick the shortcut provider directory, a custom DIY setup, or image generation). 130+ providers built in — or point it at any OpenAI-compatible endpoint with `/config diy`. Switch models anytime with `/model`, no restart needed — or use `/model diy` to give each sub-agent its own model, letting every model do what it does best. Generate or edit images the same way: run `/config image` once, then just ask for a picture (the `/image` skill).
46
47
 
47
48
  When Scream wants to modify files or run commands, an approval panel pops up — pick a number, press Enter.
48
49
 
@@ -103,7 +104,7 @@ Type `/` in the input to browse. All 55 commands:
103
104
  | `/knowledge` (`/know`) `[query]` | Manage local knowledge base |
104
105
  | **Models & configuration** | |
105
106
  | `/model [alias]` | Switch LLM model |
106
- | `/config` (`/login`) | Browse and configure models |
107
+ | `/config` (`/login`) | Configure model services: quick provider pick, custom DIY setup, or image generation |
107
108
  | `/config image` | Configure image generation (provider, URL, key, model) |
108
109
  | `/logout` (`/disconnect`) | Remove configured models |
109
110
  | `/language` (`/lang`) | Switch interface language |
@@ -136,6 +137,8 @@ Type `/` in the input to browse. All 55 commands:
136
137
  | `/cron` | View and manage scheduled tasks — list, create (pick a time + a task), delete |
137
138
  | `/like` | Set your preferences |
138
139
  | `/help` (`/h`) | Show commands and shortcuts |
140
+ | `/hotkeys` (`/keys`) | Keyboard shortcut reference |
141
+ | `/copy` (`/yank`) | Copy the last reply to the clipboard |
139
142
  | `/exit` (`/quit`, `/q`) | Exit application |
140
143
 
141
144
  </details>
@@ -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-CQwXASmd.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, 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-7Mas_Tvb.mjs";
9
- import { i as t, n as getLocale, r as setLocale } from "./text-input-dialog-gDr3i7xz.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.7"),
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;
@@ -10368,116 +10453,6 @@ var QuestionController = class extends ReverseRpcController {
10368
10453
  }
10369
10454
  };
10370
10455
  //#endregion
10371
- //#region src/utils/clipboard/clipboard-text.ts
10372
- /**
10373
- * Write plain text to the system clipboard with platform-specific commands.
10374
- *
10375
- * scream-core's clipboard layer is read-only (clipboard-native / clipboard-image),
10376
- * so the TUI's "copy selected text" needs its own writer. Used to back the
10377
- * pi-tui `copySelection` hook — return true/false so the TUI can flash
10378
- * "Copied!" or "Copy failed".
10379
- *
10380
- * Platform mapping:
10381
- * macOS -> pbcopy
10382
- * Windows-> powershell Set-Clipboard (base64 payload, avoids quoting issues)
10383
- * Linux -> wl-copy (Wayland) / xclip or xsel (X11), tried in a candidate
10384
- * chain so a missing tool (Ubuntu ships neither by default)
10385
- * falls through to the next one instead of failing outright.
10386
- */
10387
- var ClipboardTextError = class extends Error {
10388
- constructor(message) {
10389
- super(message);
10390
- this.name = "ClipboardTextError";
10391
- }
10392
- };
10393
- const defaultRunCommand = (command, args, input) => {
10394
- const result = spawnSync(command, args, {
10395
- input,
10396
- encoding: "utf8",
10397
- timeout: 3e3
10398
- });
10399
- if (result.status === 0) return { ok: true };
10400
- return {
10401
- ok: false,
10402
- error: result.error?.message ?? result.stderr?.toString() ?? `exit ${result.status}`
10403
- };
10404
- };
10405
- function detectPlatform() {
10406
- if (process.platform === "darwin") return "macos";
10407
- if (process.platform === "win32") return "windows";
10408
- if (process.platform === "linux") return process.env["WAYLAND_DISPLAY"] ? "linux-wayland" : "linux-x11";
10409
- return "unsupported";
10410
- }
10411
- function linuxCandidates(platform) {
10412
- const wl = {
10413
- command: "wl-copy",
10414
- args: []
10415
- };
10416
- const xclip = {
10417
- command: "xclip",
10418
- args: [
10419
- "-selection",
10420
- "clipboard",
10421
- "-i"
10422
- ]
10423
- };
10424
- const xsel = {
10425
- command: "xsel",
10426
- args: ["--clipboard", "--input"]
10427
- };
10428
- return platform === "linux-wayland" ? [
10429
- wl,
10430
- xclip,
10431
- xsel
10432
- ] : [
10433
- xclip,
10434
- xsel,
10435
- wl
10436
- ];
10437
- }
10438
- /**
10439
- * Write `text` to the system clipboard. Throws ClipboardTextError when the
10440
- * platform is unsupported or every candidate command fails, so callers can
10441
- * surface a "Copy failed" indicator.
10442
- */
10443
- async function writeTextClipboard(text, runCommand = defaultRunCommand) {
10444
- const platform = detectPlatform();
10445
- let command;
10446
- let args;
10447
- let input;
10448
- switch (platform) {
10449
- case "macos":
10450
- command = "pbcopy";
10451
- args = [];
10452
- input = text;
10453
- break;
10454
- case "windows": {
10455
- const base64 = Buffer.from(text, "utf8").toString("base64");
10456
- command = "powershell";
10457
- args = [
10458
- "-NoProfile",
10459
- "-Command",
10460
- `[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${base64}')) | Set-Clipboard`
10461
- ];
10462
- input = "";
10463
- break;
10464
- }
10465
- case "linux-wayland":
10466
- case "linux-x11": {
10467
- const failures = [];
10468
- for (const candidate of linuxCandidates(platform)) {
10469
- const result = runCommand(candidate.command, candidate.args, text);
10470
- if (result.ok) return;
10471
- failures.push(`${candidate.command}: ${result.error ?? "unknown error"}`);
10472
- }
10473
- 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)`);
10474
- }
10475
- default: throw new ClipboardTextError(`unsupported clipboard platform: ${process.platform}`);
10476
- }
10477
- const result = runCommand(command, args, input);
10478
- if (!result.ok) throw new ClipboardTextError(`failed to copy to clipboard: ${command} ${result.error ?? "unknown error"}`);
10479
- }
10480
- //#endregion
10481
10456
  //#region src/tui/utils/render-batcher.ts
10482
10457
  function createRenderBatcher(doRender) {
10483
10458
  let scheduled = false;
@@ -12998,69 +12973,86 @@ function stripLeadingSpace(s) {
12998
12973
  * it as the focused component, and tears it down on the `onClose`
12999
12974
  * callback (fired on Esc / Enter / q).
13000
12975
  */
13001
- /** Static list — keep in sync with the global editor bindings. */
13002
- const DEFAULT_KEYBOARD_SHORTCUTS = [
13003
- {
13004
- keys: "Shift-Tab",
13005
- description: t("help.toggle_plan")
13006
- },
13007
- {
13008
- keys: "Tab",
13009
- description: t("help.cycle_thinking")
13010
- },
13011
- {
13012
- keys: "Ctrl-G",
13013
- description: t("help.external_editor")
13014
- },
13015
- {
13016
- keys: "Ctrl-O",
13017
- description: t("help.toggle_output")
13018
- },
13019
- {
13020
- keys: "Ctrl-E",
13021
- description: t("help.toggle_plan_view")
13022
- },
13023
- {
13024
- keys: "Ctrl-S",
13025
- description: t("help.interrupt")
13026
- },
13027
- {
13028
- keys: "Ctrl-X",
13029
- description: t("help.toggle_sidebar")
13030
- },
13031
- {
13032
- keys: "Ctrl-B",
13033
- description: t("help.toggle_hint")
13034
- },
13035
- {
13036
- keys: "Ctrl-W",
13037
- description: t("help.cancel_memory")
13038
- },
13039
- {
13040
- keys: "Shift-Enter / Ctrl-J",
13041
- description: t("help.newline")
13042
- },
13043
- {
13044
- keys: "Ctrl-C",
13045
- description: t("help.cancel_stream")
13046
- },
13047
- {
13048
- keys: "Ctrl-D",
13049
- description: t("help.exit")
13050
- },
13051
- {
13052
- keys: "Esc",
13053
- description: t("help.close_dialog")
13054
- },
13055
- {
13056
- keys: "↑ / ↓",
13057
- description: t("help.browse_history")
13058
- },
13059
- {
13060
- keys: "Enter",
13061
- description: t("help.submit")
13062
- }
13063
- ];
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
+ }
13064
13056
  var HelpPanelComponent = class extends Container {
13065
13057
  focused = false;
13066
13058
  opts;
@@ -13096,33 +13088,42 @@ var HelpPanelComponent = class extends Container {
13096
13088
  const muted = chalk.hex(colors.textMuted);
13097
13089
  const kbdColor = chalk.hex(colors.warning);
13098
13090
  const slashColor = chalk.hex(colors.primary);
13099
- const shortcuts = this.opts.shortcuts ?? DEFAULT_KEYBOARD_SHORTCUTS;
13100
- 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)));
13101
13097
  const sortedCmds = [...this.opts.commands].toSorted((a, b) => a.name.localeCompare(b.name));
13102
13098
  const cmdLabels = sortedCmds.map((c) => {
13103
13099
  const aliases = c.aliases.length > 0 ? ` (${c.aliases.map((a) => "/" + a).join(", ")})` : "";
13104
13100
  return `/${c.name}${aliases}`;
13105
13101
  });
13106
13102
  const cmdWidth = Math.max(12, ...cmdLabels.map((l) => l.length));
13103
+ const hasCommands = sortedCmds.length > 0;
13107
13104
  const lines = [
13108
13105
  accent("─".repeat(width)),
13109
- accent.bold(t("help.title")) + muted(` · ${t("help.close_hint")}`),
13110
- "",
13111
- ` ${dim(t("help.welcome_msg"))}`,
13112
- ` ${muted(t("help.hidden_commands"))}`,
13106
+ accent.bold(this.opts.title ?? t("help.title")) + muted(` · ${t("help.close_hint")}`),
13113
13107
  "",
13108
+ ...hasCommands ? [
13109
+ ` ${dim(t("help.welcome_msg"))}`,
13110
+ ` ${muted(t("help.hidden_commands"))}`,
13111
+ ""
13112
+ ] : [],
13114
13113
  ` ${chalk.bold(t("help.shortcuts"))}`,
13115
- ...shortcuts.map((s) => ` ${kbdColor(s.keys.padEnd(kbdWidth))} ${dim(s.description)}`),
13116
- "",
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
- }),
13114
+ ...shortcuts.map((s) => ` ${kbdColor(padToWidth(s.keys, kbdWidth))} ${dim(s.description)}`),
13121
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
+ ] : [],
13122
13123
  accent("─".repeat(width))
13123
13124
  ];
13124
13125
  const content = lines.slice(1, lines.length - 1);
13125
- const maxVisible = Math.max(5, this.opts.maxVisible ?? 24);
13126
+ const maxVisible = Math.max(5, this.opts.maxVisible ?? 30);
13126
13127
  if (content.length > maxVisible) {
13127
13128
  this.scrollTop = Math.max(0, Math.min(this.scrollTop, content.length - maxVisible));
13128
13129
  const slice = content.slice(this.scrollTop, this.scrollTop + maxVisible);
@@ -14477,6 +14478,22 @@ var DialogManager = class {
14477
14478
  this.host.state.activeDialog = null;
14478
14479
  this.restoreEditor();
14479
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
+ }
14480
14497
  async showSessionPicker() {
14481
14498
  await this.host.fetchSessions();
14482
14499
  this.host.state.activeDialog = "session-picker";
@@ -14750,6 +14767,7 @@ var ScreamTUI = class {
14750
14767
  deferUserMessages = false;
14751
14768
  aborted = false;
14752
14769
  isShuttingDown = false;
14770
+ startedAt = Date.now();
14753
14771
  reverseRpcDisposers = [];
14754
14772
  startupNotice;
14755
14773
  /** Interval handle for the periodic provider-balance refresh. */
@@ -14856,6 +14874,8 @@ var ScreamTUI = class {
14856
14874
  this.lifecycleController.startEventLoop();
14857
14875
  try {
14858
14876
  await this.finishStartup(shouldReplayHistory);
14877
+ this.refreshRuntimeMarker();
14878
+ this.reportStaleSessions();
14859
14879
  this.lifecycleController.startCcConnectPolling();
14860
14880
  this.startBalancePolling();
14861
14881
  } catch (error) {
@@ -14940,6 +14960,34 @@ var ScreamTUI = class {
14940
14960
  throw error;
14941
14961
  }
14942
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
+ }
14943
14991
  async stop(exitCode) {
14944
14992
  if (this.isShuttingDown) return;
14945
14993
  this.isShuttingDown = true;
@@ -14960,6 +15008,7 @@ var ScreamTUI = class {
14960
15008
  this.inputController.dispose();
14961
15009
  this.state.footer.dispose();
14962
15010
  this.state.sidebarContainer.dispose();
15011
+ clearRuntimeMarker(getDataDir());
14963
15012
  await this.closeSession();
14964
15013
  await this.harness.close();
14965
15014
  this.sessionEventHandler.stopAllMcpServerStatusSpinners();
@@ -14990,6 +15039,7 @@ var ScreamTUI = class {
14990
15039
  onEmergencyExit(exitCode = 129) {
14991
15040
  this.isShuttingDown = true;
14992
15041
  this.lifecycleController.uninstallSignalHandlers();
15042
+ clearRuntimeMarker(getDataDir());
14993
15043
  process.exit(exitCode);
14994
15044
  }
14995
15045
  stopMemoryIdleTimer() {
@@ -15139,6 +15189,7 @@ var ScreamTUI = class {
15139
15189
  const planModeChanged = "planMode" in patch && prevPlanMode !== this.state.appState.planMode;
15140
15190
  const sessionChanged = "sessionId" in patch && this.state.appState.sessionId !== prevSessionId;
15141
15191
  if (sessionChanged) {
15192
+ this.refreshRuntimeMarker();
15142
15193
  if (!("sessionUsage" in patch)) this.state.appState.sessionUsage = {
15143
15194
  inputOther: 0,
15144
15195
  output: 0,
@@ -15270,7 +15321,7 @@ var ScreamTUI = class {
15270
15321
  const card = new OnboardingCardComponent({
15271
15322
  colors: this.state.theme.colors,
15272
15323
  onConfigure: () => {
15273
- import("./dispatch-BqBnrIMj.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
15324
+ import("./dispatch-zsFzFFoY.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
15274
15325
  this.restoreEditor();
15275
15326
  onDone?.();
15276
15327
  });
@@ -15308,7 +15359,9 @@ var ScreamTUI = class {
15308
15359
  let error;
15309
15360
  let failureKind;
15310
15361
  try {
15311
- ({ok, error, failureKind} = await startManualEmbeddingDownload());
15362
+ ({ok, error, failureKind} = await startManualEmbeddingDownload((progress) => {
15363
+ spinner.setLabel(embeddingProgressLabel(progress));
15364
+ }));
15312
15365
  } catch (error) {
15313
15366
  error = error instanceof Error ? error.message : String(error);
15314
15367
  }
@@ -15396,6 +15449,9 @@ var ScreamTUI = class {
15396
15449
  showHelpPanel() {
15397
15450
  this.dialogManager.showHelpPanel(this.getSlashCommands());
15398
15451
  }
15452
+ showHotkeysPanel() {
15453
+ this.dialogManager.showHotkeysPanel();
15454
+ }
15399
15455
  hideHelpPanel() {
15400
15456
  this.dialogManager.hideHelpPanel();
15401
15457
  }