scream-code 0.18.7 → 0.18.9

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-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-DIVvoAYO.mjs";
8
+ import { $ as clearGoalState, $n as DEFAULT_CATALOG_URL, $t as isStreaming, A as renderDiffLines, An as sortSlashCommands, At as createMarkdownTheme, B as SkillActivationComponent, Bn as TuiConfigParseError, Bt as DISABLE_TERMINAL_THEME_REPORTING, C as appendJsonlLine, Cn as TIP_ROTATION_INTERVAL_MS, Ct as refreshProviderBalance, Dn as getNoActiveSessionMessage, E as getDaemonInstructions, En as getLlmNotSetMessage, F as ThinkingComponent, Fn as getKnowledgeStore, Ft as contrastTextHex, G as getSharedSpeedTracker, Gn as getDataDir, Gt as OSC11_RESPONSE_PREFIX, H as ActivityGroupComponent, Hn as loadTuiConfig, Ht as ENABLE_TERMINAL_THEME_REPORTING, I as NoticeMessageComponent, In as hasEmbeddingModelCache, It as getColorPalette, J as WelcomeComponent, Jn as detectInstallSource, Jt as TERMINAL_FOCUS_IN, K as BackgroundAgentStatusComponent, Kn as getInputHistoryFile, Kt as OSC11_RESPONSE_PREFIX_NO_ESC, L as StatusMessageComponent, Ln as startManualEmbeddingDownload, Lt as detectTerminalTheme, M as highlightLines, Mn as setExperimentalFlags, Mt as isTurnElapsedEnabled, N as langFromPath, Nn as embeddingProgressLabel, Nt as toggleEmptySessionHint, O as UserMessageComponent, On as buildSkillSlashCommands, Ot as resolveThemeSync, P as CachedContainer, Pn as embeddingFailureDetail, Pt as createThemeStyles, Q as CompactionComponent, Qn as PRODUCT_NAME, Qt as isBusy, R as replaceTabs, Rn as PIXEL_PULSE_FRAMES, Rt as parseOsc11BackgroundTheme, S as readUpdateCache, Sn as SESSION_TIPS, Tn as getCtrlDHint, Tt as clearInfoPanelState, U as charsForTokenBudget, Un as saveTuiConfig, Ut as OSC11_QUERY, V as AssistantMessageComponent, Vn as TuiLikePreferencesSchema, Vt as ENABLE_TERMINAL_FOCUS_REPORTING, W as estimateTokens, Wn as detectShellEnvironment, Wt as OSC11_RESPONSE, X as getBreathingFrame, Xn as CLI_UI_MODE, Xt as TERMINAL_THEME_DARK, Y as BREATHE_CYCLE_MS, Yn as CLI_COMMAND_NAME, Yt as TERMINAL_FOCUS_OUT, Z as resetBreathingClock, Zn as CLI_USER_AGENT_PRODUCT, Zt as TERMINAL_THEME_LIGHT, _n as stringValue, _r as ErrorCodes, a as buildRoleAdditionalText, ar as appendSessionIndexEntry, b as selectUpdateTarget, bn as EXIT_CONFIRM_WINDOW_MS, cn as printableChar, ct as changeThinkingLevel, d as hasDispose, dn as appendStreamingArgsPreview, dr as resolveScreamHome, en as FooterComponent, er as fetchCatalog, et as refineGoal, f as isExpandable, fn as argsRecord, fr as flushDiagnosticLogs, gn as serializeToolResultOutput, gr as isOrphanedToolCallError, gt as handlePlanCommand, h as formatMemoryMemoForInjection, hn as parseStreamingArgs, hr as isScreamError, i as openUrl, ir as normalizeWorkDir, j as renderDiffLinesClustered, jn as isExperimentalFlagEnabled, jt as isEmptySessionHintDismissed, k as ToolCallComponent, kn as BUILTIN_SLASH_COMMANDS, kt as createEditorTheme, ln as STATUS_BULLET, lr as isRealUserPrompt, lt as getModelCycleLevel, m as MoonLoader, mn as isTodoItemShape, mr as resolveGlobalLogPath, mt as handleFusionPlanCommand, n as clearEvalPanelState, nn as lerpHex, nr as ScreamHarness, o as writeTextClipboard, or as readSessionIndex, ot as isInternalMessage, p as isPlanExpandable, pn as formatErrorMessage, pr as log, q as AgentGroupComponent, qn as getLogDir, qt as QUERY_TERMINAL_THEME, rn as SubagentSlots, rr as encodeWorkDirKey, s as syncDiagramCapabilityPrompt, sr as removeSessionIndexEntry, st as toTerminalHyperlink, t as dispatchInput, tn as lerpGradient, tr as saveCatalogCache, u as disposeChildren, un as appendSessionHeaderHint, ur as parseRlmMaxDepthArg, vn as truncateErrorMessage, vr as SCREAM_ERROR_INFO, w as readJsonlFile, wn as getCtrlCHint, wt as supportsBalance, x as refreshUpdateCache, xn as MAIN_AGENT_ID, z as sanitizeShellOutput, zn as PULSE_WAVE_FRAMES, zt as DISABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-BcRHd4ya.mjs";
9
+ import { i as t, n as getLocale, r as setLocale } from "./text-input-dialog-CqagQQtW.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.9"),
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;
@@ -2912,6 +2997,19 @@ const INITIAL_LIVE_PANE = {
2912
2997
  function sliceLeading(text, max = 48) {
2913
2998
  return text.length > max ? `${text.slice(0, max)}…` : text;
2914
2999
  }
3000
+ /**
3001
+ * Inbound events that prove the stream is still delivering. `TUIState.lastActivityAt`
3002
+ * is stamped from this set so the status-bar wave can colour a stream that went
3003
+ * silent mid-reasoning — before the 60s transport-level stall timeout fires.
3004
+ */
3005
+ const ACTIVITY_EVENT_TYPES = new Set([
3006
+ "assistant.delta",
3007
+ "thinking.delta",
3008
+ "tool.call.started",
3009
+ "tool.call.delta",
3010
+ "tool.progress",
3011
+ "tool.result"
3012
+ ]);
2915
3013
  function addTokenUsage(a, b) {
2916
3014
  return {
2917
3015
  inputOther: a.inputOther + b.inputOther,
@@ -3101,6 +3199,7 @@ var SessionEventHandler = class {
3101
3199
  if (hidden.length > 0) host.showStatus(formatMcpStartupStatusSummary(hidden, visible.length), host.state.theme.colors.textMuted);
3102
3200
  }
3103
3201
  handleEvent(event, sendQueued) {
3202
+ if (ACTIVITY_EVENT_TYPES.has(event.type)) this.host.state.lastActivityAt = Date.now();
3104
3203
  if (this.routeSubagentEvent(event)) return;
3105
3204
  if ("turnId" in event && event.turnId !== void 0) this.host.streamingUI.setTurnId(String(event.turnId));
3106
3205
  switch (event.type) {
@@ -3972,7 +4071,10 @@ var SessionEventHandler = class {
3972
4071
  if (next !== void 0) setTimeout(() => {
3973
4072
  sendQueued(next);
3974
4073
  }, 0);
3975
- } else this.host.setAppState({ isCompacting: false });
4074
+ } else this.host.setAppState({
4075
+ isCompacting: false,
4076
+ streamingStartTime: Date.now()
4077
+ });
3976
4078
  }
3977
4079
  handleSubagentSpawned(event) {
3978
4080
  this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
@@ -8814,6 +8916,49 @@ function installTerminalThemeTracking(state, onTheme) {
8814
8916
  };
8815
8917
  }
8816
8918
  //#endregion
8919
+ //#region src/tui/utils/wave-color.ts
8920
+ /**
8921
+ * Silence ramp for the status-bar pulse wave.
8922
+ *
8923
+ * The wave reports one thing: how long the agent has been working without
8924
+ * receiving anything. That covers a request awaiting its first token, a
8925
+ * reasoning or text stream that stopped delivering, and a tool that has been
8926
+ * running without progress. Any inbound event resets the clock, so the wave
8927
+ * returns to the brand colour the moment work resumes.
8928
+ *
8929
+ * Timeline (elapsed since the last inbound event, or since the request was
8930
+ * sent while no token has arrived yet):
8931
+ * 0 – 10s brand green, nothing to report
8932
+ * 10 – 15s green → amber
8933
+ * 15 – 25s amber → red (15s is pure amber, 25s pure red)
8934
+ * ≥ 25s red
8935
+ *
8936
+ * Red is an early warning, not an error: retries are driven by real transport
8937
+ * failures (the stall timeout, disconnects, rate limits), never by this colour.
8938
+ *
8939
+ * Colours come from the active palette (primary / warning / error), so light
8940
+ * and dark themes each ramp between their own contrast-checked values and a
8941
+ * theme switch takes effect on the next frame.
8942
+ */
8943
+ /** Green is held this long before the ramp starts. */
8944
+ const WAVE_GREEN_HOLD_MS = 1e4;
8945
+ /** Pure amber is reached here. */
8946
+ const WAVE_AMBER_AT_MS = 15e3;
8947
+ /** Pure red is reached here. */
8948
+ const WAVE_RED_AT_MS = 25e3;
8949
+ /**
8950
+ * Colour for a request that has been waiting `elapsedMs`.
8951
+ *
8952
+ * Non-finite or negative input falls back to green: a broken clock must never
8953
+ * make the indicator claim a stall.
8954
+ */
8955
+ function resolveWaveColor(elapsedMs, stops) {
8956
+ if (!Number.isFinite(elapsedMs) || elapsedMs < 1e4) return stops.green;
8957
+ if (elapsedMs < 15e3) return lerpHex(stops.green, stops.amber, (elapsedMs - WAVE_GREEN_HOLD_MS) / (WAVE_AMBER_AT_MS - WAVE_GREEN_HOLD_MS));
8958
+ if (elapsedMs < 25e3) return lerpHex(stops.amber, stops.red, (elapsedMs - WAVE_AMBER_AT_MS) / (WAVE_RED_AT_MS - WAVE_AMBER_AT_MS));
8959
+ return stops.red;
8960
+ }
8961
+ //#endregion
8817
8962
  //#region src/tui/components/chrome/pulse-wave-loader.ts
8818
8963
  /** Leading/trailing glyph: U+2588 (Block Elements — guaranteed single-column
8819
8964
  * width). The remaining stage cells render muted middle dots (U+00B7). */
@@ -8840,7 +8985,9 @@ var PulseWaveLoader = class extends Text {
8840
8985
  currentFrame = 0;
8841
8986
  intervalId = null;
8842
8987
  ui;
8843
- colorHex;
8988
+ /** Resolved on every frame so a theme switch lands immediately and the
8989
+ * first-token ramp can advance without rebuilding the status bar. */
8990
+ resolveColor;
8844
8991
  /** Wall-clock time of the last frame advance, for drift-free scheduling. */
8845
8992
  lastTickAt = 0;
8846
8993
  /** Raw frame text without Text's full-width render padding, exposed so
@@ -8851,10 +8998,10 @@ var PulseWaveLoader = class extends Text {
8851
8998
  getFrameText() {
8852
8999
  return this.frameText;
8853
9000
  }
8854
- constructor(ui, colorHex) {
9001
+ constructor(ui, resolveColor) {
8855
9002
  super("", 1, 0);
8856
9003
  this.ui = ui;
8857
- this.colorHex = colorHex;
9004
+ this.resolveColor = resolveColor;
8858
9005
  this.start();
8859
9006
  }
8860
9007
  start() {
@@ -8868,6 +9015,12 @@ var PulseWaveLoader = class extends Text {
8868
9015
  this.intervalId = null;
8869
9016
  }
8870
9017
  }
9018
+ /** Re-resolve the colour and repaint now. The cached frame text still carries
9019
+ * the previous colour, and the next scheduled tick can be up to 80ms away —
9020
+ * a palette swapped in place should land on the frame that follows it. */
9021
+ repaint() {
9022
+ this.updateDisplay();
9023
+ }
8871
9024
  dispose() {
8872
9025
  this.stop();
8873
9026
  }
@@ -8896,21 +9049,18 @@ var PulseWaveLoader = class extends Text {
8896
9049
  }, delayMs);
8897
9050
  this.intervalId = timer;
8898
9051
  }
8899
- setColorHex(colorHex) {
8900
- this.colorHex = colorHex;
8901
- this.updateDisplay();
8902
- }
8903
9052
  updateDisplay() {
8904
9053
  const step = PULSE_WAVE_FRAMES[this.currentFrame] ?? PULSE_WAVE_FRAMES[0];
8905
- const cells = Array.from({ length: 8 }, (_, idx) => this.renderCell(idx, step.active, step.forward));
9054
+ const color = this.resolveColor();
9055
+ const cells = Array.from({ length: 8 }, (_, idx) => this.renderCell(idx, step.active, step.forward, color));
8906
9056
  this.frameText = cells.join("");
8907
9057
  this.setText(this.frameText);
8908
9058
  this.ui.requestRender();
8909
9059
  }
8910
- renderCell(index, active, forward) {
9060
+ renderCell(index, active, forward, color) {
8911
9061
  const distance = forward ? active - index : index - active;
8912
- if (distance === 0) return chalk.hex(this.colorHex)(LEADING_BOX);
8913
- if (distance === 1) return chalk.hex(this.colorHex).dim(LEADING_BOX);
9062
+ if (distance === 0) return chalk.hex(color)(LEADING_BOX);
9063
+ if (distance === 1) return chalk.hex(color).dim(LEADING_BOX);
8914
9064
  return chalk.dim("·");
8915
9065
  }
8916
9066
  };
@@ -9312,6 +9462,7 @@ var LifecycleController = class LifecycleController {
9312
9462
  state.theme.resolvedTheme = nextTheme.resolvedTheme;
9313
9463
  state.theme.styles = nextTheme.styles;
9314
9464
  state.theme.markdownTheme = nextTheme.markdownTheme;
9465
+ this.statusBarLoader?.repaint();
9315
9466
  this.host.setAppState({ theme });
9316
9467
  this.host.updateEditorBorderHighlight();
9317
9468
  for (const child of state.transcriptContainer.children) child.invalidate?.();
@@ -9366,7 +9517,7 @@ var LifecycleController = class LifecycleController {
9366
9517
  case "tool":
9367
9518
  case "thinking":
9368
9519
  {
9369
- const loader = new PulseWaveLoader(state.ui, state.theme.colors.primary);
9520
+ const loader = new PulseWaveLoader(state.ui, () => this.currentWaveColor());
9370
9521
  this.statusBarLoader = loader;
9371
9522
  state.statusBarContainer.addChild(new StatusBarPaneComponent({
9372
9523
  mode,
@@ -9459,6 +9610,32 @@ var LifecycleController = class LifecycleController {
9459
9610
  this.host.state.terminal.setProgress(active);
9460
9611
  this.host.state.terminalState.progressActive = active;
9461
9612
  }
9613
+ /**
9614
+ * Colour for the status-bar pulse wave.
9615
+ *
9616
+ * Measures silence in whatever the agent is doing: a request awaiting its
9617
+ * first token, a reasoning/text stream that stopped delivering, or a tool
9618
+ * that has been running without emitting progress. Red is an early warning
9619
+ * only — it never triggers a retry, because retries are driven by real
9620
+ * transport errors (the 60s stall timeout, disconnects, rate limits).
9621
+ *
9622
+ * Resolved per frame, so the ramp advances and a live theme switch applies
9623
+ * without rebuilding the status bar.
9624
+ */
9625
+ currentWaveColor() {
9626
+ const { appState, theme, lastActivityAt } = this.host.state;
9627
+ const colors = theme.colors;
9628
+ const { streamingPhase, streamingStartTime, reconnectAttempt } = appState;
9629
+ if (streamingPhase === "idle") return colors.primary;
9630
+ const anchor = Math.max(lastActivityAt, streamingStartTime);
9631
+ if (anchor === 0) return colors.primary;
9632
+ const elapsed = Date.now() - anchor;
9633
+ return resolveWaveColor(reconnectAttempt > 0 ? Math.max(elapsed, WAVE_AMBER_AT_MS) : elapsed, {
9634
+ green: colors.primary,
9635
+ amber: colors.warning,
9636
+ red: colors.error
9637
+ });
9638
+ }
9462
9639
  stopActivitySpinner() {
9463
9640
  if (this.host.state.activitySpinner !== null) {
9464
9641
  this.host.state.activitySpinner.instance.stop();
@@ -10368,116 +10545,6 @@ var QuestionController = class extends ReverseRpcController {
10368
10545
  }
10369
10546
  };
10370
10547
  //#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
10548
  //#region src/tui/utils/render-batcher.ts
10482
10549
  function createRenderBatcher(doRender) {
10483
10550
  let scheduled = false;
@@ -11694,6 +11761,7 @@ function createTUIState(options) {
11694
11761
  activeDialog: null,
11695
11762
  tasksBrowser: void 0,
11696
11763
  externalEditorRunning: false,
11764
+ lastActivityAt: 0,
11697
11765
  queuedMessages: [],
11698
11766
  fdPath: detectFdPath(),
11699
11767
  gitLsFilesCache: createGitLsFilesCache(initialAppState.workDir),
@@ -12499,6 +12567,11 @@ var SessionManager$1 = class {
12499
12567
  resetSessionRuntime() {
12500
12568
  this.host.state.queuedMessages = [];
12501
12569
  this.host.harness.interactiveAgentId = MAIN_AGENT_ID;
12570
+ this.host.setAppState({
12571
+ streamingPhase: "idle",
12572
+ streamingStartTime: 0
12573
+ });
12574
+ this.host.state.lastActivityAt = 0;
12502
12575
  this.host.streamingUI.discardPending();
12503
12576
  this.host.streamingUI.resetToolCallState();
12504
12577
  this.host.streamingUI.resetToolUi();
@@ -12998,69 +13071,86 @@ function stripLeadingSpace(s) {
12998
13071
  * it as the focused component, and tears it down on the `onClose`
12999
13072
  * callback (fired on Esc / Enter / q).
13000
13073
  */
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
- ];
13074
+ /**
13075
+ * Keyboard shortcut reference — keep in sync with the global editor bindings.
13076
+ *
13077
+ * A function, not a constant: `t()` must run on every call. As a module-level
13078
+ * constant the descriptions were evaluated once at import time and froze to
13079
+ * whichever locale happened to be active then, so a Chinese session showed an
13080
+ * English shortcut list (and vice versa).
13081
+ */
13082
+ function defaultKeyboardShortcuts() {
13083
+ return [
13084
+ {
13085
+ keys: "Shift-Tab",
13086
+ description: t("help.toggle_plan")
13087
+ },
13088
+ {
13089
+ keys: "Tab",
13090
+ description: t("help.cycle_thinking")
13091
+ },
13092
+ {
13093
+ keys: "Ctrl-G",
13094
+ description: t("help.external_editor")
13095
+ },
13096
+ {
13097
+ keys: "Ctrl-O",
13098
+ description: t("help.toggle_output")
13099
+ },
13100
+ {
13101
+ keys: "Ctrl-E",
13102
+ description: t("help.toggle_plan_view")
13103
+ },
13104
+ {
13105
+ keys: "Ctrl-S",
13106
+ description: t("help.interrupt")
13107
+ },
13108
+ {
13109
+ keys: "Ctrl-X",
13110
+ description: t("help.toggle_sidebar")
13111
+ },
13112
+ {
13113
+ keys: "Ctrl-B",
13114
+ description: t("help.toggle_hint")
13115
+ },
13116
+ {
13117
+ keys: "Ctrl-W",
13118
+ description: t("help.cancel_memory")
13119
+ },
13120
+ {
13121
+ keys: "↑",
13122
+ description: t("help.recall_queued")
13123
+ },
13124
+ {
13125
+ keys: "Ctrl-V / Alt-V",
13126
+ description: t("help.paste_image")
13127
+ },
13128
+ {
13129
+ keys: "Shift-Enter / Ctrl-J",
13130
+ description: t("help.newline")
13131
+ },
13132
+ {
13133
+ keys: "Ctrl-C",
13134
+ description: t("help.cancel_stream")
13135
+ },
13136
+ {
13137
+ keys: "Ctrl-D",
13138
+ description: t("help.exit")
13139
+ },
13140
+ {
13141
+ keys: "Esc",
13142
+ description: t("help.close_dialog")
13143
+ },
13144
+ {
13145
+ keys: "↑ / ↓",
13146
+ description: t("help.browse_history")
13147
+ },
13148
+ {
13149
+ keys: "Enter",
13150
+ description: t("help.submit")
13151
+ }
13152
+ ];
13153
+ }
13064
13154
  var HelpPanelComponent = class extends Container {
13065
13155
  focused = false;
13066
13156
  opts;
@@ -13096,33 +13186,42 @@ var HelpPanelComponent = class extends Container {
13096
13186
  const muted = chalk.hex(colors.textMuted);
13097
13187
  const kbdColor = chalk.hex(colors.warning);
13098
13188
  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));
13189
+ const shortcuts = this.opts.shortcuts ?? defaultKeyboardShortcuts();
13190
+ const padToWidth = (text, width) => {
13191
+ const pad = width - visibleWidth(text);
13192
+ return pad > 0 ? text + " ".repeat(pad) : text;
13193
+ };
13194
+ const kbdWidth = Math.max(8, ...shortcuts.map((s) => visibleWidth(s.keys)));
13101
13195
  const sortedCmds = [...this.opts.commands].toSorted((a, b) => a.name.localeCompare(b.name));
13102
13196
  const cmdLabels = sortedCmds.map((c) => {
13103
13197
  const aliases = c.aliases.length > 0 ? ` (${c.aliases.map((a) => "/" + a).join(", ")})` : "";
13104
13198
  return `/${c.name}${aliases}`;
13105
13199
  });
13106
13200
  const cmdWidth = Math.max(12, ...cmdLabels.map((l) => l.length));
13201
+ const hasCommands = sortedCmds.length > 0;
13107
13202
  const lines = [
13108
13203
  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"))}`,
13204
+ accent.bold(this.opts.title ?? t("help.title")) + muted(` · ${t("help.close_hint")}`),
13113
13205
  "",
13206
+ ...hasCommands ? [
13207
+ ` ${dim(t("help.welcome_msg"))}`,
13208
+ ` ${muted(t("help.hidden_commands"))}`,
13209
+ ""
13210
+ ] : [],
13114
13211
  ` ${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
- }),
13212
+ ...shortcuts.map((s) => ` ${kbdColor(padToWidth(s.keys, kbdWidth))} ${dim(s.description)}`),
13121
13213
  "",
13214
+ ...hasCommands ? [
13215
+ ` ${chalk.bold(t("help.slash_commands"))}`,
13216
+ ...sortedCmds.map((cmd, i) => {
13217
+ return ` ${slashColor((cmdLabels[i] ?? `/${cmd.name}`).padEnd(cmdWidth))} ${dim(t(cmd.description))}`;
13218
+ }),
13219
+ ""
13220
+ ] : [],
13122
13221
  accent("─".repeat(width))
13123
13222
  ];
13124
13223
  const content = lines.slice(1, lines.length - 1);
13125
- const maxVisible = Math.max(5, this.opts.maxVisible ?? 24);
13224
+ const maxVisible = Math.max(5, this.opts.maxVisible ?? 30);
13126
13225
  if (content.length > maxVisible) {
13127
13226
  this.scrollTop = Math.max(0, Math.min(this.scrollTop, content.length - maxVisible));
13128
13227
  const slice = content.slice(this.scrollTop, this.scrollTop + maxVisible);
@@ -13159,6 +13258,8 @@ var HelpPanelComponent = class extends Container {
13159
13258
  * Esc Cancel / back
13160
13259
  */
13161
13260
  const ELLIPSIS$1 = "…";
13261
+ /** One-sentence conclusion: generous for a rule, bounded for prompt injection. */
13262
+ const MAX_DISTILLED_CHARS = 200;
13162
13263
  function formatRelativeTime$1(ts) {
13163
13264
  if (!Number.isFinite(ts) || ts <= 0) return "";
13164
13265
  const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1e3);
@@ -13169,6 +13270,13 @@ function formatRelativeTime$1(ts) {
13169
13270
  if (hours < 24) return t("memory.hours_ago", { hours });
13170
13271
  return t("memory.days_ago", { days: Math.floor(hours / 24) });
13171
13272
  }
13273
+ /** Local calendar date as YYYY-MM-DD; avoids the UTC off-by-one of toISOString. */
13274
+ function formatDate(ts) {
13275
+ const date = new Date(ts);
13276
+ const month = String(date.getMonth() + 1).padStart(2, "0");
13277
+ const day = String(date.getDate()).padStart(2, "0");
13278
+ return `${date.getFullYear()}-${month}-${day}`;
13279
+ }
13172
13280
  function singleLine$1(text) {
13173
13281
  return text.replaceAll(/\s+/g, " ").trim();
13174
13282
  }
@@ -13226,6 +13334,12 @@ var MemoryPickerComponent = class extends Container {
13226
13334
  searchQuery = "";
13227
13335
  isSearching = false;
13228
13336
  searchInput = "";
13337
+ /** Draft text for the in-place edit of the latest distillation. */
13338
+ editBuffer = "";
13339
+ /** Set when the last save failed; keeps the draft on screen for a retry. */
13340
+ editError = false;
13341
+ /** Guards against a second Enter while the update is in flight. */
13342
+ saving = false;
13229
13343
  /** Memo ids ticked for batch delete (space toggles, a selects all). */
13230
13344
  selectedIds = /* @__PURE__ */ new Set();
13231
13345
  memos;
@@ -13286,6 +13400,29 @@ var MemoryPickerComponent = class extends Container {
13286
13400
  if (ch.length > 0) this.searchInput += ch;
13287
13401
  return;
13288
13402
  }
13403
+ if (this.mode === "editDistilled") {
13404
+ if (matchesKey(data, Key.escape)) {
13405
+ this.editBuffer = "";
13406
+ this.mode = "detail";
13407
+ this.ui?.requestRender();
13408
+ return;
13409
+ }
13410
+ if (matchesKey(data, Key.enter)) {
13411
+ if (!this.saving) this.saveDistilled();
13412
+ return;
13413
+ }
13414
+ if (matchesKey(data, Key.backspace) || matchesKey(data, Key.delete)) {
13415
+ this.editBuffer = Array.from(this.editBuffer).slice(0, -1).join("");
13416
+ this.ui?.requestRender();
13417
+ return;
13418
+ }
13419
+ const typed = printableChar(data);
13420
+ if (typed.length > 0) {
13421
+ if (Array.from(this.editBuffer).length < MAX_DISTILLED_CHARS) this.editBuffer += typed;
13422
+ this.ui?.requestRender();
13423
+ }
13424
+ return;
13425
+ }
13289
13426
  if (this.mode === "confirmDelete") {
13290
13427
  if (matchesKey(data, Key.enter)) {
13291
13428
  if (this.selectedIds.size > 0) this.deleteSelectedAndReload();
@@ -13307,6 +13444,14 @@ var MemoryPickerComponent = class extends Container {
13307
13444
  this.detailMemo = null;
13308
13445
  return;
13309
13446
  }
13447
+ const key = printableChar(data);
13448
+ if ((key === "e" || key === "E") && this.detailMemo !== null) {
13449
+ this.editBuffer = this.detailMemo.distilled ?? "";
13450
+ this.editError = false;
13451
+ this.mode = "editDistilled";
13452
+ this.ui?.requestRender();
13453
+ return;
13454
+ }
13310
13455
  }
13311
13456
  if (matchesKey(data, Key.escape)) {
13312
13457
  if (this.searchQuery.length > 0) {
@@ -13375,6 +13520,52 @@ var MemoryPickerComponent = class extends Container {
13375
13520
  if (this.detailMemo) this.mode = "detail";
13376
13521
  this.ui?.requestRender();
13377
13522
  }
13523
+ /**
13524
+ * Persist the edited distillation. A non-empty text becomes a user-authored
13525
+ * rule; clearing it drops back to a plain extracted memo.
13526
+ */
13527
+ async saveDistilled() {
13528
+ const memo = this.detailMemo;
13529
+ if (memo === null) {
13530
+ this.mode = "list";
13531
+ return;
13532
+ }
13533
+ this.saving = true;
13534
+ const text = this.editBuffer.trim();
13535
+ if (text === (memo.distilled ?? "").trim()) {
13536
+ this.saving = false;
13537
+ this.editBuffer = "";
13538
+ this.mode = "detail";
13539
+ this.ui?.requestRender();
13540
+ return;
13541
+ }
13542
+ try {
13543
+ await this.store.update(memo.id, {
13544
+ distilled: text.length > 0 ? text : void 0,
13545
+ distilledFrom: text.length > 0 ? memo.distilledFrom : void 0,
13546
+ origin: text.length > 0 ? "user" : "session",
13547
+ distilledAt: text.length > 0 ? Date.now() : void 0
13548
+ });
13549
+ } catch {
13550
+ this.editError = true;
13551
+ this.saving = false;
13552
+ this.ui?.requestRender();
13553
+ return;
13554
+ }
13555
+ this.saving = false;
13556
+ this.editError = false;
13557
+ this.editBuffer = "";
13558
+ await this.loadMemos();
13559
+ const index = this.memos.findIndex((item) => item.id === memo.id);
13560
+ if (index < 0) {
13561
+ this.detailMemo = null;
13562
+ this.mode = "list";
13563
+ this.ui?.requestRender();
13564
+ return;
13565
+ }
13566
+ this.selectedIndex = index;
13567
+ await this.showDetail();
13568
+ }
13378
13569
  async deleteAndReload(id) {
13379
13570
  try {
13380
13571
  await this.store.delete(id);
@@ -13395,8 +13586,7 @@ var MemoryPickerComponent = class extends Container {
13395
13586
  }
13396
13587
  render(width) {
13397
13588
  const c = this.colors;
13398
- const lines = [];
13399
- lines.push(chalk.hex(c.primary)("─".repeat(width)));
13589
+ const lines = [chalk.hex(c.primary)("─".repeat(width))];
13400
13590
  if (this.isSearching) {
13401
13591
  const prompt = t("memory.search_label");
13402
13592
  const input = this.searchInput.length > 0 ? this.searchInput : "█";
@@ -13425,15 +13615,18 @@ var MemoryPickerComponent = class extends Container {
13425
13615
  return lines;
13426
13616
  }
13427
13617
  if (this.mode === "detail" && this.detailMemo) return this.renderDetail(lines, width, c);
13618
+ if (this.mode === "editDistilled" && this.detailMemo) return this.renderEditDistilled(lines, width, c);
13428
13619
  if (this.mode === "confirmDelete") {
13429
13620
  if (this.selectedIds.size > 0) {
13430
13621
  lines.push(truncateToWidth(chalk.hex(c.warning).bold(t("memory.batch_delete_confirm", { count: String(this.selectedIds.size) })), width, ELLIPSIS$1));
13431
13622
  lines.push(chalk.hex(c.warning)(t("memory.batch_delete_hint")));
13623
+ if (this.memos.some((memo) => this.selectedIds.has(memo.id) && memo.origin !== void 0 && memo.origin !== "session")) lines.push(chalk.hex(c.warning)(truncateToWidth(` ${t("memory.delete_rule_warning")}`, width, ELLIPSIS$1)));
13432
13624
  } else {
13433
13625
  const memo = this.memos[this.selectedIndex];
13434
13626
  if (memo) {
13435
13627
  lines.push(truncateToWidth(chalk.hex(c.warning).bold(` ${t("memory.deleting")}${memo.userNeed}`), width, ELLIPSIS$1));
13436
13628
  lines.push(chalk.hex(c.warning)(t("memory.delete_confirm_hint")));
13629
+ if (memo.origin !== void 0 && memo.origin !== "session") lines.push(chalk.hex(c.warning)(truncateToWidth(` ${t("memory.delete_rule_warning")}`, width, ELLIPSIS$1)));
13437
13630
  }
13438
13631
  }
13439
13632
  lines.push(chalk.hex(c.primary)("─".repeat(width)));
@@ -13467,7 +13660,10 @@ var MemoryPickerComponent = class extends Container {
13467
13660
  const indentWidth = visibleWidth(indent);
13468
13661
  const titleColor = isSelected ? c.primary : c.text;
13469
13662
  const titleStyle = isSelected ? chalk.hex(titleColor).bold : chalk.hex(titleColor);
13470
- const trailingParts = [formatRelativeTime$1(memo.recordedAt), sourceLabel(memo.extractionSource)].filter((p) => p.length > 0);
13663
+ const sources = memo.distilledFrom ?? [];
13664
+ const isRule = memo.origin !== void 0 && memo.origin !== "session";
13665
+ const trailingParts = [formatRelativeTime$1(isRule ? memo.distilledAt ?? memo.recordedAt : memo.recordedAt), isRule ? memo.origin === "user" ? t("memory.origin_user") : t("memory.origin_dream") : sourceLabel(memo.extractionSource)].filter((p) => p.length > 0);
13666
+ if (isRule && sources.length > 0) trailingParts.push(t("memory.source_count", { count: String(sources.length) }));
13471
13667
  const trailingText = trailingParts.length > 0 ? " " + trailingParts.join(" ") : "";
13472
13668
  const trailingWidth = visibleWidth(trailingText);
13473
13669
  const headerPrefixWidth = visibleWidth(pointer) + 1 + visibleWidth(check) + 1;
@@ -13493,17 +13689,22 @@ var MemoryPickerComponent = class extends Container {
13493
13689
  const approachPreview = t("memory.plan_label") + singleLine$1(memo.approach);
13494
13690
  card.push(indent + chalk.hex(c.textDim)(truncateToWidth(approachPreview, Math.max(8, width - indentWidth), ELLIPSIS$1)));
13495
13691
  }
13692
+ if (memo.distilled !== void 0 && memo.distilled.length > 0) {
13693
+ const distilledPreview = t("memory.distilled_label") + singleLine$1(memo.distilled);
13694
+ card.push(indent + chalk.hex(c.success ?? c.primary)(truncateToWidth(distilledPreview, Math.max(8, width - indentWidth), ELLIPSIS$1)));
13695
+ }
13496
13696
  return card;
13497
13697
  }
13498
13698
  renderDetail(lines, width, c) {
13499
13699
  const memo = this.detailMemo;
13500
- const time = new Date(memo.recordedAt).toLocaleString("zh-CN");
13700
+ const time = formatDate(memo.recordedAt);
13501
13701
  const sessionLabel = memo.sourceSessionTitle && memo.sourceSessionTitle.length > 0 ? `${memo.sourceSessionTitle} (${memo.sourceSessionId.slice(0, 12)})` : memo.sourceSessionId.slice(0, 12);
13502
13702
  const indent = " ";
13503
13703
  const contentWidth = Math.max(8, width - visibleWidth(indent));
13504
13704
  lines.push(truncateToWidth(chalk.hex(c.primary).bold(`${indent}${t("memory.requirement_label")}${memo.userNeed}`), width, ELLIPSIS$1));
13505
13705
  lines.push("");
13506
- lines.push(chalk.hex(c.textMuted)(truncateToWidth(`${indent}${t("memory.result_label")}${memo.outcome} ${t("memory.source_label")}${sourceLabel(memo.extractionSource)} ${time}`, width, ELLIPSIS$1)));
13706
+ const sourceName = memo.origin !== void 0 && memo.origin !== "session" ? memo.origin === "user" ? t("memory.origin_user") : t("memory.origin_dream") : sourceLabel(memo.extractionSource);
13707
+ lines.push(chalk.hex(c.textMuted)(truncateToWidth(`${indent}${t("memory.result_label")}${memo.outcome} ${t("memory.source_label")}${sourceName} ${time}`, width, ELLIPSIS$1)));
13507
13708
  lines.push(chalk.hex(c.textMuted)(truncateToWidth(`${indent}${t("memory.session_label")}${sessionLabel}`, width, ELLIPSIS$1)));
13508
13709
  const project = formatProject(memo);
13509
13710
  const tags = formatTags(memo);
@@ -13538,6 +13739,27 @@ var MemoryPickerComponent = class extends Container {
13538
13739
  }
13539
13740
  lines.push("");
13540
13741
  }
13742
+ if (memo.distilled !== void 0 && memo.distilled.length > 0) {
13743
+ const label = t("memory.distilled_label");
13744
+ const wrapped = wrapText(memo.distilled, contentWidth - visibleWidth(label));
13745
+ for (let i = 0; i < wrapped.length; i++) {
13746
+ const prefix = i === 0 ? `${indent}${label}` : indent + " ".repeat(visibleWidth(label));
13747
+ lines.push(truncateToWidth(chalk.hex(c.success ?? c.primary)(prefix + wrapped[i]), width, ELLIPSIS$1));
13748
+ }
13749
+ lines.push("");
13750
+ }
13751
+ if (memo.origin !== void 0 && memo.origin !== "session") {
13752
+ const distilledAtLabel = memo.distilledAt !== void 0 ? `${t("memory.distilled_at")} ${formatDate(memo.distilledAt)}` : t("memory.distilled_at_unknown");
13753
+ lines.push(chalk.hex(c.textMuted)(truncateToWidth(`${indent}${distilledAtLabel}`, width, ELLIPSIS$1)));
13754
+ const sources = memo.distilledFrom ?? [];
13755
+ lines.push(chalk.hex(c.textMuted)(truncateToWidth(`${indent}${sources.length > 0 ? t("memory.source_count", { count: String(sources.length) }) : t("memory.source_unknown")}`, width, ELLIPSIS$1)));
13756
+ for (const source of sources.slice(0, 5)) {
13757
+ const date = formatDate(source.at);
13758
+ lines.push(chalk.hex(c.textMuted)(truncateToWidth(`${indent}- ${date} · ${singleLine$1(source.title)}`, width, ELLIPSIS$1)));
13759
+ }
13760
+ if (sources.length > 5) lines.push(chalk.hex(c.textDim)(truncateToWidth(`${indent}${t("memory.source_more", { count: String(sources.length - 5) })}`, width, ELLIPSIS$1)));
13761
+ lines.push("");
13762
+ }
13541
13763
  if (memo.note !== void 0 && memo.note.length > 0) {
13542
13764
  const label = t("memory.note_label");
13543
13765
  const wrapped = wrapText(memo.note, contentWidth - visibleWidth(label));
@@ -13551,6 +13773,20 @@ var MemoryPickerComponent = class extends Container {
13551
13773
  lines.push(chalk.hex(c.primary)("─".repeat(width)));
13552
13774
  return lines;
13553
13775
  }
13776
+ renderEditDistilled(lines, width, c) {
13777
+ const memo = this.detailMemo;
13778
+ const indent = " ";
13779
+ lines.push(truncateToWidth(chalk.hex(c.primary).bold(`${indent}${t("memory.requirement_label")}${memo.userNeed}`), width, ELLIPSIS$1));
13780
+ lines.push("");
13781
+ lines.push(chalk.hex(c.textMuted)(truncateToWidth(`${indent}${t("memory.distilled_label")}`, width, ELLIPSIS$1)));
13782
+ const contentWidth = Math.max(8, width - 4);
13783
+ for (const line of wrapText(`${this.editBuffer}█`, contentWidth)) lines.push(chalk.hex(c.success ?? c.primary)(`${indent}${line}`));
13784
+ lines.push("");
13785
+ if (this.editError) lines.push(chalk.hex(c.error)(truncateToWidth(`${indent}${t("memory.edit_save_failed")}`, width, ELLIPSIS$1)));
13786
+ lines.push(chalk.hex(c.textMuted)(truncateToWidth(`${indent}${t("memory.edit_distilled_hint")}`, width, ELLIPSIS$1)));
13787
+ lines.push(chalk.hex(c.primary)("─".repeat(width)));
13788
+ return lines;
13789
+ }
13554
13790
  };
13555
13791
  //#endregion
13556
13792
  //#region src/tui/utils/session-label.ts
@@ -14477,6 +14713,22 @@ var DialogManager = class {
14477
14713
  this.host.state.activeDialog = null;
14478
14714
  this.restoreEditor();
14479
14715
  }
14716
+ showHotkeysPanel() {
14717
+ this.host.state.activeDialog = "hotkeys";
14718
+ this.mountEditorReplacement(new HelpPanelComponent({
14719
+ commands: [],
14720
+ shortcuts: defaultKeyboardShortcuts(),
14721
+ colors: this.host.state.theme.colors,
14722
+ title: t("help.hotkeys_title"),
14723
+ onClose: () => {
14724
+ this.hideHotkeysPanel();
14725
+ }
14726
+ }));
14727
+ }
14728
+ hideHotkeysPanel() {
14729
+ this.host.state.activeDialog = null;
14730
+ this.restoreEditor();
14731
+ }
14480
14732
  async showSessionPicker() {
14481
14733
  await this.host.fetchSessions();
14482
14734
  this.host.state.activeDialog = "session-picker";
@@ -14750,6 +15002,7 @@ var ScreamTUI = class {
14750
15002
  deferUserMessages = false;
14751
15003
  aborted = false;
14752
15004
  isShuttingDown = false;
15005
+ startedAt = Date.now();
14753
15006
  reverseRpcDisposers = [];
14754
15007
  startupNotice;
14755
15008
  /** Interval handle for the periodic provider-balance refresh. */
@@ -14856,6 +15109,8 @@ var ScreamTUI = class {
14856
15109
  this.lifecycleController.startEventLoop();
14857
15110
  try {
14858
15111
  await this.finishStartup(shouldReplayHistory);
15112
+ this.refreshRuntimeMarker();
15113
+ this.reportStaleSessions();
14859
15114
  this.lifecycleController.startCcConnectPolling();
14860
15115
  this.startBalancePolling();
14861
15116
  } catch (error) {
@@ -14940,6 +15195,34 @@ var ScreamTUI = class {
14940
15195
  throw error;
14941
15196
  }
14942
15197
  }
15198
+ /** Keep this instance's crash marker in sync with the running session. */
15199
+ refreshRuntimeMarker() {
15200
+ try {
15201
+ writeRuntimeMarker(getDataDir(), {
15202
+ pid: process.pid,
15203
+ startedAt: this.startedAt,
15204
+ ...this.state.appState.sessionId ? { sessionId: this.state.appState.sessionId } : {},
15205
+ ...this.state.appState.sessionTitle ? { title: this.state.appState.sessionTitle } : {}
15206
+ });
15207
+ } catch {}
15208
+ }
15209
+ /** Report a previous instance that never shut down, exactly once. */
15210
+ reportStaleSessions() {
15211
+ let stale = [];
15212
+ try {
15213
+ stale = collectStaleMarkers(getDataDir());
15214
+ } catch {
15215
+ return;
15216
+ }
15217
+ const latest = stale[0];
15218
+ if (latest === void 0) return;
15219
+ const label = latest.title ?? (latest.sessionId !== void 0 ? latest.sessionId.slice(0, 8) : "");
15220
+ if (label.length === 0) {
15221
+ this.showNotice(t("tui.crash_recovery_title"));
15222
+ return;
15223
+ }
15224
+ this.showNotice(t("tui.crash_recovery_title"), t("tui.crash_recovery_detail", { title: label }));
15225
+ }
14943
15226
  async stop(exitCode) {
14944
15227
  if (this.isShuttingDown) return;
14945
15228
  this.isShuttingDown = true;
@@ -14960,6 +15243,7 @@ var ScreamTUI = class {
14960
15243
  this.inputController.dispose();
14961
15244
  this.state.footer.dispose();
14962
15245
  this.state.sidebarContainer.dispose();
15246
+ clearRuntimeMarker(getDataDir());
14963
15247
  await this.closeSession();
14964
15248
  await this.harness.close();
14965
15249
  this.sessionEventHandler.stopAllMcpServerStatusSpinners();
@@ -14990,6 +15274,7 @@ var ScreamTUI = class {
14990
15274
  onEmergencyExit(exitCode = 129) {
14991
15275
  this.isShuttingDown = true;
14992
15276
  this.lifecycleController.uninstallSignalHandlers();
15277
+ clearRuntimeMarker(getDataDir());
14993
15278
  process.exit(exitCode);
14994
15279
  }
14995
15280
  stopMemoryIdleTimer() {
@@ -15139,6 +15424,7 @@ var ScreamTUI = class {
15139
15424
  const planModeChanged = "planMode" in patch && prevPlanMode !== this.state.appState.planMode;
15140
15425
  const sessionChanged = "sessionId" in patch && this.state.appState.sessionId !== prevSessionId;
15141
15426
  if (sessionChanged) {
15427
+ this.refreshRuntimeMarker();
15142
15428
  if (!("sessionUsage" in patch)) this.state.appState.sessionUsage = {
15143
15429
  inputOther: 0,
15144
15430
  output: 0,
@@ -15270,7 +15556,7 @@ var ScreamTUI = class {
15270
15556
  const card = new OnboardingCardComponent({
15271
15557
  colors: this.state.theme.colors,
15272
15558
  onConfigure: () => {
15273
- import("./dispatch-BqBnrIMj.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
15559
+ import("./dispatch-JYsyD0aN.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
15274
15560
  this.restoreEditor();
15275
15561
  onDone?.();
15276
15562
  });
@@ -15308,7 +15594,9 @@ var ScreamTUI = class {
15308
15594
  let error;
15309
15595
  let failureKind;
15310
15596
  try {
15311
- ({ok, error, failureKind} = await startManualEmbeddingDownload());
15597
+ ({ok, error, failureKind} = await startManualEmbeddingDownload((progress) => {
15598
+ spinner.setLabel(embeddingProgressLabel(progress));
15599
+ }));
15312
15600
  } catch (error) {
15313
15601
  error = error instanceof Error ? error.message : String(error);
15314
15602
  }
@@ -15396,6 +15684,9 @@ var ScreamTUI = class {
15396
15684
  showHelpPanel() {
15397
15685
  this.dialogManager.showHelpPanel(this.getSlashCommands());
15398
15686
  }
15687
+ showHotkeysPanel() {
15688
+ this.dialogManager.showHotkeysPanel();
15689
+ }
15399
15690
  hideHelpPanel() {
15400
15691
  this.dialogManager.hideHelpPanel();
15401
15692
  }