scream-code 0.15.2 → 0.15.3

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,7 +4,7 @@ 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 { $t as isTodoItemShape, A as renderDiffLinesClustered, An as getInputHistoryFile, At as ENABLE_TERMINAL_FOCUS_REPORTING, B as BackgroundAgentStatusComponent, Bn as ScreamHarness, Bt as TERMINAL_THEME_LIGHT, Cn as PULSE_WAVE_FRAMES, Ct as createThemeStyles, D as toggleEmptySessionHint, Dn as saveTuiConfig, Dt as parseOsc11BackgroundTheme, E as isTurnElapsedEnabled, En as loadTuiConfig, Et as detectTerminalTheme, F as estimateTokens, Fn as CLI_USER_AGENT_PRODUCT, Ft as OSC11_RESPONSE_PREFIX_NO_ESC, G as getBreathingFrame, Gn as log, H as AgentGroupComponent, Hn as resolveScreamHome, Ht as isStreaming, I as getSharedSpeedTracker, In as PRODUCT_NAME, It as QUERY_TERMINAL_THEME, J as refineGoal, Jn as isOrphanedToolCallError, Jt as printableChar, K as resetBreathingClock, Kn as resolveGlobalLogPath, L as SkillActivationComponent, Ln as DEFAULT_CATALOG_URL, Lt as TERMINAL_FOCUS_IN, M as langFromPath, Mn as detectInstallSource, Mt as OSC11_QUERY, N as CachedContainer, Nn as CLI_COMMAND_NAME, Nt as OSC11_RESPONSE, O as ToolCallComponent, On as detectShellEnvironment, Ot as DISABLE_TERMINAL_FOCUS_REPORTING, P as ThinkingComponent, Pn as CLI_UI_MODE, Pt as OSC11_RESPONSE_PREFIX, Qt as formatErrorMessage, R as ReadGroupComponent, Rn as fetchCatalog, Rt as TERMINAL_FOCUS_OUT, S as getDaemonInstructions, Sn as PIXEL_PULSE_FRAMES, St as createMarkdownTheme, T as isEmptySessionHintDismissed, Tn as TuiLikePreferencesSchema, Tt as getColorPalette, U as WelcomeComponent, Un as MemoryMemoStore, Ut as FooterComponent, V as AssistantMessageComponent, Vt as isBusy, W as BREATHE_CYCLE_MS, Wn as flushDiagnosticLogs, Xn as SCREAM_ERROR_INFO, Xt as appendStreamingArgsPreview, Yn as ErrorCodes, Yt as STATUS_BULLET, Zt as argsRecord, _ as refreshUpdateCache, _n as isExperimentalFlagEnabled, _t as clearInfoPanelState, an as EMPTY_SESSION_HINT_URL, b as readJsonlFile, bn as isEmbeddingModelCached, bt as resolveThemeSync, c as hasDispose, cn as SESSION_TIPS, ct as handlePlanCommand, d as formatMemoryMemoForInjection, dn as getCtrlDHint, en as parseStreamingArgs, et as toTerminalHyperlink, fn as getLlmNotSetMessage, g as selectUpdateTarget, gn as sortSlashCommands, gt as supportsBalance, hn as BUILTIN_SLASH_COMMANDS, ht as refreshProviderBalance, i as buildRoleAdditionalText, in as CHARS_PER_TOKEN, j as highlightLines, jn as getLogDir, jt as ENABLE_TERMINAL_THEME_REPORTING, k as renderDiffLines, kn as getDataDir, kt as DISABLE_TERMINAL_THEME_REPORTING, l as isPlanExpandable, ln as TIP_ROTATION_INTERVAL_MS, mn as buildSkillSlashCommands, n as clearEvalPanelState, nn as stringValue, nt as getModelCycleLevel, on as EXIT_CONFIRM_WINDOW_MS, ot as handleFusionPlanCommand, pn as getNoActiveSessionMessage, q as clearGoalState, qn as isScreamError, r as openUrl, rn as truncateErrorMessage, s as disposeChildren, sn as MAIN_AGENT_ID, t as dispatchInput, tn as serializeToolResultOutput, tt as changeThinkingLevel, u as MoonLoader, un as getCtrlCHint, v as readUpdateCache, vn as setExperimentalFlags, w as UserMessageComponent, wn as TuiConfigParseError, wt as contrastTextHex, xn as startManualEmbeddingDownload, xt as createEditorTheme, y as appendJsonlLine, yn as getKnowledgeStore, z as parseReadGroupOutput, zn as saveCatalogCache, zt as TERMINAL_THEME_DARK } from "./dispatch-B_ViYZer.mjs";
7
+ import { $t as isTodoItemShape, A as renderDiffLinesClustered, An as getInputHistoryFile, At as ENABLE_TERMINAL_FOCUS_REPORTING, B as BackgroundAgentStatusComponent, Bn as ScreamHarness, Bt as TERMINAL_THEME_LIGHT, Cn as PULSE_WAVE_FRAMES, Ct as createThemeStyles, D as toggleEmptySessionHint, Dn as saveTuiConfig, Dt as parseOsc11BackgroundTheme, E as isTurnElapsedEnabled, En as loadTuiConfig, Et as detectTerminalTheme, F as estimateTokens, Fn as CLI_USER_AGENT_PRODUCT, Ft as OSC11_RESPONSE_PREFIX_NO_ESC, G as getBreathingFrame, Gn as log, H as AgentGroupComponent, Hn as resolveScreamHome, Ht as isStreaming, I as getSharedSpeedTracker, In as PRODUCT_NAME, It as QUERY_TERMINAL_THEME, J as refineGoal, Jn as isOrphanedToolCallError, Jt as printableChar, K as resetBreathingClock, Kn as resolveGlobalLogPath, L as SkillActivationComponent, Ln as DEFAULT_CATALOG_URL, Lt as TERMINAL_FOCUS_IN, M as langFromPath, Mn as detectInstallSource, Mt as OSC11_QUERY, N as CachedContainer, Nn as CLI_COMMAND_NAME, Nt as OSC11_RESPONSE, O as ToolCallComponent, On as detectShellEnvironment, Ot as DISABLE_TERMINAL_FOCUS_REPORTING, P as ThinkingComponent, Pn as CLI_UI_MODE, Pt as OSC11_RESPONSE_PREFIX, Qt as formatErrorMessage, R as ReadGroupComponent, Rn as fetchCatalog, Rt as TERMINAL_FOCUS_OUT, S as getDaemonInstructions, Sn as PIXEL_PULSE_FRAMES, St as createMarkdownTheme, T as isEmptySessionHintDismissed, Tn as TuiLikePreferencesSchema, Tt as getColorPalette, U as WelcomeComponent, Un as MemoryMemoStore, Ut as FooterComponent, V as AssistantMessageComponent, Vt as isBusy, W as BREATHE_CYCLE_MS, Wn as flushDiagnosticLogs, Xn as SCREAM_ERROR_INFO, Xt as appendStreamingArgsPreview, Yn as ErrorCodes, Yt as STATUS_BULLET, Zt as argsRecord, _ as refreshUpdateCache, _n as isExperimentalFlagEnabled, _t as clearInfoPanelState, an as EMPTY_SESSION_HINT_URL, b as readJsonlFile, bn as isEmbeddingModelCached, bt as resolveThemeSync, c as hasDispose, cn as SESSION_TIPS, ct as handlePlanCommand, d as formatMemoryMemoForInjection, dn as getCtrlDHint, en as parseStreamingArgs, et as toTerminalHyperlink, fn as getLlmNotSetMessage, g as selectUpdateTarget, gn as sortSlashCommands, gt as supportsBalance, hn as BUILTIN_SLASH_COMMANDS, ht as refreshProviderBalance, i as buildRoleAdditionalText, in as CHARS_PER_TOKEN, j as highlightLines, jn as getLogDir, jt as ENABLE_TERMINAL_THEME_REPORTING, k as renderDiffLines, kn as getDataDir, kt as DISABLE_TERMINAL_THEME_REPORTING, l as isPlanExpandable, ln as TIP_ROTATION_INTERVAL_MS, mn as buildSkillSlashCommands, n as clearEvalPanelState, nn as stringValue, nt as getModelCycleLevel, on as EXIT_CONFIRM_WINDOW_MS, ot as handleFusionPlanCommand, pn as getNoActiveSessionMessage, q as clearGoalState, qn as isScreamError, r as openUrl, rn as truncateErrorMessage, s as disposeChildren, sn as MAIN_AGENT_ID, t as dispatchInput, tn as serializeToolResultOutput, tt as changeThinkingLevel, u as MoonLoader, un as getCtrlCHint, v as readUpdateCache, vn as setExperimentalFlags, w as UserMessageComponent, wn as TuiConfigParseError, wt as contrastTextHex, xn as startManualEmbeddingDownload, xt as createEditorTheme, y as appendJsonlLine, yn as getKnowledgeStore, z as parseReadGroupOutput, zn as saveCatalogCache, zt as TERMINAL_THEME_DARK } from "./dispatch-DGeogib-.mjs";
8
8
  import { a as setLocale, o as t } from "./text-input-dialog-BUXAKycx.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
@@ -32,7 +32,7 @@ function optionalBuildString(value) {
32
32
  return typeof value === "string" && value.length > 0 ? value : void 0;
33
33
  }
34
34
  const SCREAM_BUILD_INFO = {
35
- version: optionalBuildString("0.15.2"),
35
+ version: optionalBuildString("0.15.3"),
36
36
  channel: optionalBuildString(""),
37
37
  commit: optionalBuildString(""),
38
38
  buildTarget: optionalBuildString("darwin-arm64")
@@ -6553,27 +6553,34 @@ function installTerminalThemeTracking(state, onTheme) {
6553
6553
  }
6554
6554
  //#endregion
6555
6555
  //#region src/tui/components/chrome/pulse-wave-loader.ts
6556
- const FULL_BOX = "■";
6557
- const DIM_DOT = "⬝";
6556
+ /** Leading/trailing glyph: U+2588 (Block Elements — guaranteed single-column
6557
+ * width). The remaining stage cells render muted middle dots (U+00B7). */
6558
+ const LEADING_BOX = "■";
6558
6559
  /**
6559
- * 3-box pulse-wave loading indicator.
6560
+ * 8-box flowing pulse-wave loading indicator.
6560
6561
  *
6561
- * Cycles through a breathing wave pattern:
6562
- * ⬝ → ■ ⬝ → ⬝ ■ → (bounce back)
6562
+ * A leading box (full primary colour) sweeps across an 8-cell stage, trailed
6563
+ * by a dim box, over a row of muted middle dots:
6564
+ * ■ 淡 ■ · · · · · · → ■ ■ 淡 ■ · · · · → … → (bounce back)
6565
+ * (淡 shown here only to mark the trailing box; it renders as primary+dim.)
6563
6566
  *
6564
- * Colouring mirrors Grok's PromptLoadingBoxes:
6565
- * - active box (distance 0) → full primary colour
6566
- * - trailing box (distance 1) → ~72 % opacity via chalk dim
6567
- * - dim dot (distance 2) → muted
6567
+ * 14-frame ping-pong loop @ 80 ms — the leading edge never teleports, it
6568
+ * walks cell by cell.
6569
+ *
6570
+ * All glyphs are width-safe in terminal fonts (U+2588 Block Elements and
6571
+ * U+00B7 Latin-1) — the earlier ■/⬝ mix reflowed the whole line on every
6572
+ * frame because U+2B1D is not single-column guaranteed.
6568
6573
  *
6569
6574
  * The component auto-starts on construction. Call `stop()` to tear
6570
- * down the interval timer.
6575
+ * down the timer chain.
6571
6576
  */
6572
6577
  var PulseWaveLoader = class extends Text {
6573
6578
  currentFrame = 0;
6574
6579
  intervalId = null;
6575
6580
  ui;
6576
6581
  colorHex;
6582
+ /** Wall-clock time of the last frame advance, for drift-free scheduling. */
6583
+ lastTickAt = 0;
6577
6584
  /** Raw frame text without Text's full-width render padding, exposed so
6578
6585
  * callers can compose the wave inline with trailing content (e.g. the
6579
6586
  * reconnect label in the status bar). */
@@ -6589,39 +6596,60 @@ var PulseWaveLoader = class extends Text {
6589
6596
  this.start();
6590
6597
  }
6591
6598
  start() {
6599
+ this.lastTickAt = performance.now();
6592
6600
  this.updateDisplay();
6593
- this.intervalId = setInterval(() => {
6594
- this.currentFrame = (this.currentFrame + 1) % PULSE_WAVE_FRAMES.length;
6595
- this.updateDisplay();
6596
- }, 120);
6601
+ this.scheduleTick(80, 80);
6597
6602
  }
6598
6603
  stop() {
6599
6604
  if (this.intervalId) {
6600
- clearInterval(this.intervalId);
6605
+ clearTimeout(this.intervalId);
6601
6606
  this.intervalId = null;
6602
6607
  }
6603
6608
  }
6609
+ dispose() {
6610
+ this.stop();
6611
+ }
6612
+ /**
6613
+ * setTimeout self-rescheduling chain with drift-free frame advancement and
6614
+ * paint-cost backpressure (same pattern as the pi-tui Loader): a slow tick
6615
+ * defers the next one by `max(0, interval - cost, cost × 9)` instead of
6616
+ * stacking intervals, keeping the wave ≤ ~10% CPU under slow writes.
6617
+ */
6618
+ scheduleTick(intervalMs, delayMs) {
6619
+ const timer = setTimeout(() => {
6620
+ if (this.intervalId !== timer) return;
6621
+ const startedAt = performance.now();
6622
+ const elapsed = startedAt - this.lastTickAt;
6623
+ if (elapsed >= intervalMs) {
6624
+ const steps = Math.floor(elapsed / intervalMs);
6625
+ this.currentFrame = (this.currentFrame + steps) % PULSE_WAVE_FRAMES.length;
6626
+ this.lastTickAt += steps * intervalMs;
6627
+ this.updateDisplay();
6628
+ }
6629
+ const frameCostMs = performance.now() - startedAt;
6630
+ if (this.intervalId !== timer) return;
6631
+ const cadenceDelayMs = Math.max(0, intervalMs - frameCostMs);
6632
+ const backpressureDelayMs = frameCostMs * 9;
6633
+ this.scheduleTick(intervalMs, Math.max(cadenceDelayMs, backpressureDelayMs));
6634
+ }, delayMs);
6635
+ this.intervalId = timer;
6636
+ }
6604
6637
  setColorHex(colorHex) {
6605
6638
  this.colorHex = colorHex;
6606
6639
  this.updateDisplay();
6607
6640
  }
6608
6641
  updateDisplay() {
6609
6642
  const step = PULSE_WAVE_FRAMES[this.currentFrame] ?? PULSE_WAVE_FRAMES[0];
6610
- const cells = [
6611
- 0,
6612
- 1,
6613
- 2
6614
- ].map((idx) => this.renderCell(idx, step.active, step.forward));
6615
- this.frameText = cells.join(" ");
6643
+ const cells = Array.from({ length: 8 }, (_, idx) => this.renderCell(idx, step.active, step.forward));
6644
+ this.frameText = cells.join("");
6616
6645
  this.setText(this.frameText);
6617
6646
  this.ui.requestRender();
6618
6647
  }
6619
6648
  renderCell(index, active, forward) {
6620
6649
  const distance = forward ? active - index : index - active;
6621
- const glyph = distance >= 0 && distance < 2 ? FULL_BOX : DIM_DOT;
6622
- if (distance === 0) return chalk.hex(this.colorHex)(glyph);
6623
- if (distance === 1) return chalk.hex(this.colorHex).dim(glyph);
6624
- return chalk.dim(glyph);
6650
+ if (distance === 0) return chalk.hex(this.colorHex)(LEADING_BOX);
6651
+ if (distance === 1) return chalk.hex(this.colorHex).dim(LEADING_BOX);
6652
+ return chalk.dim("·");
6625
6653
  }
6626
6654
  };
6627
6655
  //#endregion
@@ -12397,7 +12425,7 @@ var ScreamTUI = class {
12397
12425
  const card = new OnboardingCardComponent({
12398
12426
  colors: this.state.theme.colors,
12399
12427
  onConfigure: () => {
12400
- import("./dispatch-Dur5jHVw.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
12428
+ import("./dispatch-CweYoXh3.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
12401
12429
  this.restoreEditor();
12402
12430
  onDone?.();
12403
12431
  });
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
3
  import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
- import { Kt as handleConnectCommand } from "./dispatch-B_ViYZer.mjs";
6
+ import { Kt as handleConnectCommand } from "./dispatch-DGeogib-.mjs";
7
7
  export { handleConnectCommand };
@@ -47632,9 +47632,41 @@ function estimateTokensForMessage(message) {
47632
47632
  messageTokenCache.set(message, total);
47633
47633
  return total;
47634
47634
  }
47635
+ /**
47636
+ * Media parts carry no width/height metadata, and providers differ on how they
47637
+ * tokenize media (image token counts range from a few hundred to ~1600 for a
47638
+ * single image depending on provider and resolution). There is no cross-
47639
+ * provider formula, so these estimates are deliberately conservative — for
47640
+ * watermark detection an OVERestimate only compacts slightly earlier
47641
+ * (harmless), whereas counting media as 0 systematically UNDERESTIMATED the
47642
+ * watermark and delayed compaction on media-heavy sessions.
47643
+ *
47644
+ * These estimates are consumed only by watermark detection and micro-compaction
47645
+ * recycling decisions — the real usage reported by the provider re-anchors the
47646
+ * context token count at each step end, so billing/usage accounting is
47647
+ * unaffected.
47648
+ */
47649
+ const IMAGE_BASE_TOKENS = 1600;
47650
+ const IMAGE_MAX_TOKENS = 8e3;
47651
+ /** data-URI payloads above this many bytes are treated as high-resolution/long captures. */
47652
+ const IMAGE_LARGE_PAYLOAD_BYTES = 512e3;
47653
+ const AUDIO_TOKENS = 2e3;
47654
+ const VIDEO_TOKENS = 5e3;
47655
+ /** Long screenshots (large data URIs) carry more visual content than a single
47656
+ * bounded-resolution image; grow the estimate linearly and cap it. */
47657
+ function estimateDataUriTokens(url, baseTokens, maxTokens) {
47658
+ const start = url.indexOf("base64,");
47659
+ if (start < 0) return baseTokens;
47660
+ const bytes = Math.floor((url.length - start - 7) * 3 / 4);
47661
+ if (bytes <= IMAGE_LARGE_PAYLOAD_BYTES) return baseTokens;
47662
+ return Math.min(maxTokens, baseTokens + Math.floor((bytes - IMAGE_LARGE_PAYLOAD_BYTES) / 1e3));
47663
+ }
47635
47664
  function estimateTokensForContentPart(part) {
47636
47665
  if (part.type === "text") return estimateTokens$1(part.text);
47637
47666
  else if (part.type === "think") return estimateTokens$1(part.think);
47667
+ else if (part.type === "image_url") return estimateDataUriTokens(part.imageUrl.url, IMAGE_BASE_TOKENS, IMAGE_MAX_TOKENS);
47668
+ else if (part.type === "audio_url") return AUDIO_TOKENS;
47669
+ else if (part.type === "video_url") return VIDEO_TOKENS;
47638
47670
  return 0;
47639
47671
  }
47640
47672
  //#endregion
@@ -78968,7 +79000,11 @@ var FullCompaction = class {
78968
79000
  this.agent.context.applyCompaction(result);
78969
79001
  this.lastCompactionFiles = fileLists;
78970
79002
  this.lowWaterMark = Math.floor(this.effectiveTokenCount * 1.1);
78971
- await this.extractAndStoreMemos(processedSummary, messagesToCompactForOps);
79003
+ try {
79004
+ await this.extractAndStoreMemos(processedSummary, messagesToCompactForOps);
79005
+ } catch (error) {
79006
+ this.agent.log.warn("Memory memo extraction failed (compaction itself succeeded)", { error });
79007
+ }
78972
79008
  this.triggerPostCompactHook(data, result);
78973
79009
  this.detectSkillCandidates(skillCandidateSummary);
78974
79010
  this.consecutiveCompactionFailures = 0;
@@ -79431,7 +79467,7 @@ function findSupersededPaths(messages, cutoff) {
79431
79467
  /**
79432
79468
  * Lightweight compaction that truncates old tool results without an LLM call.
79433
79469
  *
79434
- * When the context window is filling up (>= 50% by default), old tool result
79470
+ * When the context window is filling up (>= 30% by default), old tool result
79435
79471
  * messages are replaced with a short placeholder. This frees up tokens for the
79436
79472
  * model without the cost and latency of a full compaction.
79437
79473
  *
@@ -125641,24 +125677,15 @@ const PIXEL_PULSE_FRAMES = [
125641
125677
  "▒",
125642
125678
  "▓"
125643
125679
  ];
125644
- const PULSE_WAVE_FRAMES = [
125645
- {
125646
- active: 0,
125647
- forward: true
125648
- },
125649
- {
125650
- active: 1,
125651
- forward: true
125652
- },
125653
- {
125654
- active: 2,
125655
- forward: true
125656
- },
125657
- {
125658
- active: 1,
125659
- forward: false
125660
- }
125661
- ];
125680
+ const forwardFrames = Array.from({ length: 8 }, (_, active) => ({
125681
+ active,
125682
+ forward: true
125683
+ }));
125684
+ const backwardFrames = Array.from({ length: 6 }, (_, i) => ({
125685
+ active: 6 - i,
125686
+ forward: false
125687
+ }));
125688
+ const PULSE_WAVE_FRAMES = [...forwardFrames, ...backwardFrames];
125662
125689
  //#endregion
125663
125690
  //#region src/tui/commands/knowledge-store.ts
125664
125691
  let knowledgeStoreInstance;
@@ -129444,13 +129471,22 @@ var FooterComponent = class {
129444
129471
  this.#stopStatusTimer();
129445
129472
  if (phase === "idle" && !goalActive) return;
129446
129473
  const intervalMs = 1e3 / 30;
129447
- this.statusTimer = setInterval(() => {
129474
+ this.#scheduleStatusTick(intervalMs, intervalMs);
129475
+ }
129476
+ #scheduleStatusTick(intervalMs, delayMs) {
129477
+ const timer = setTimeout(() => {
129478
+ if (this.statusTimer !== timer) return;
129479
+ const startedAt = performance.now();
129448
129480
  this.ui.requestRender();
129449
- }, intervalMs);
129481
+ const tickCostMs = performance.now() - startedAt;
129482
+ if (this.statusTimer !== timer) return;
129483
+ this.#scheduleStatusTick(intervalMs, Math.max(0, intervalMs - tickCostMs));
129484
+ }, delayMs);
129485
+ this.statusTimer = timer;
129450
129486
  }
129451
129487
  #stopStatusTimer() {
129452
129488
  if (!this.statusTimer) return;
129453
- clearInterval(this.statusTimer);
129489
+ clearTimeout(this.statusTimer);
129454
129490
  this.statusTimer = null;
129455
129491
  }
129456
129492
  render(width) {
@@ -133126,7 +133162,8 @@ var ThinkingComponent = class {
133126
133162
  expanded = false;
133127
133163
  ui;
133128
133164
  spinnerFrame = 0;
133129
- spinnerInterval;
133165
+ spinnerTimer;
133166
+ lastSpinnerTickAt = 0;
133130
133167
  textComponent;
133131
133168
  cachedWidth;
133132
133169
  cachedLines;
@@ -133210,17 +133247,41 @@ var ThinkingComponent = class {
133210
133247
  }
133211
133248
  return truncated;
133212
133249
  }
133250
+ /**
133251
+ * setTimeout self-rescheduling chain with drift-free frame advancement and
133252
+ * paint-cost backpressure (same pattern as the pi-tui Loader): a slow tick
133253
+ * defers the next one instead of stacking intervals, keeping the live
133254
+ * thinking indicator ≤ ~10% CPU even under slow terminal writes.
133255
+ */
133256
+ scheduleSpinnerTick(delayMs) {
133257
+ if (this.ui === void 0) return;
133258
+ const timer = setTimeout(() => {
133259
+ if (this.spinnerTimer !== timer) return;
133260
+ const startedAt = performance.now();
133261
+ const elapsed = startedAt - this.lastSpinnerTickAt;
133262
+ if (elapsed >= 80) {
133263
+ const steps = Math.floor(elapsed / 80);
133264
+ this.spinnerFrame = (this.spinnerFrame + steps) % BRAILLE_SPINNER_FRAMES.length;
133265
+ this.lastSpinnerTickAt += steps * 80;
133266
+ this.ui?.requestRender();
133267
+ }
133268
+ const frameCostMs = performance.now() - startedAt;
133269
+ if (this.spinnerTimer !== timer) return;
133270
+ const cadenceDelayMs = Math.max(0, 80 - frameCostMs);
133271
+ const backpressureDelayMs = frameCostMs * 9;
133272
+ this.scheduleSpinnerTick(Math.max(cadenceDelayMs, backpressureDelayMs));
133273
+ }, delayMs);
133274
+ this.spinnerTimer = timer;
133275
+ }
133213
133276
  startSpinner() {
133214
- if (this.ui === void 0 || this.spinnerInterval !== void 0) return;
133215
- this.spinnerInterval = setInterval(() => {
133216
- this.spinnerFrame = (this.spinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
133217
- this.ui?.requestRender();
133218
- }, 80);
133277
+ if (this.ui === void 0 || this.spinnerTimer !== void 0) return;
133278
+ this.lastSpinnerTickAt = performance.now();
133279
+ this.scheduleSpinnerTick(80);
133219
133280
  }
133220
133281
  stopSpinner() {
133221
- if (this.spinnerInterval === void 0) return;
133222
- clearInterval(this.spinnerInterval);
133223
- this.spinnerInterval = void 0;
133282
+ if (this.spinnerTimer === void 0) return;
133283
+ clearTimeout(this.spinnerTimer);
133284
+ this.spinnerTimer = void 0;
133224
133285
  }
133225
133286
  };
133226
133287
  //#endregion
@@ -136112,7 +136173,11 @@ function findRevokeAnchorComponentIndex(children, count) {
136112
136173
  function removeRevokeContextComponents(children, startIndex) {
136113
136174
  for (let i = children.length - 1; i >= startIndex; i--) {
136114
136175
  const child = children[i];
136115
- if (child !== void 0 && isRevokeContextComponent(child)) children.splice(i, 1);
136176
+ if (child !== void 0 && isRevokeContextComponent(child)) {
136177
+ const disposable = child;
136178
+ if (typeof disposable.dispose === "function") disposable.dispose();
136179
+ children.splice(i, 1);
136180
+ }
136116
136181
  }
136117
136182
  }
136118
136183
  function isRevokeAnchorComponent(child) {
@@ -137974,6 +138039,8 @@ var MoonLoader = class extends Text {
137974
138039
  ui;
137975
138040
  colorFn;
137976
138041
  label;
138042
+ /** Wall-clock time of the last frame advance, for drift-free scheduling. */
138043
+ lastTickAt = 0;
137977
138044
  constructor(ui, colorFn, label = "") {
137978
138045
  super("", 1, 0);
137979
138046
  this.ui = ui;
@@ -137982,18 +138049,44 @@ var MoonLoader = class extends Text {
137982
138049
  this.start();
137983
138050
  }
137984
138051
  start() {
138052
+ this.lastTickAt = performance.now();
137985
138053
  this.updateDisplay();
137986
- this.intervalId = setInterval(() => {
137987
- this.currentFrame = (this.currentFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
137988
- this.updateDisplay();
137989
- }, 80);
138054
+ this.scheduleTick(80, 80);
137990
138055
  }
137991
138056
  stop() {
137992
138057
  if (this.intervalId) {
137993
- clearInterval(this.intervalId);
138058
+ clearTimeout(this.intervalId);
137994
138059
  this.intervalId = null;
137995
138060
  }
137996
138061
  }
138062
+ dispose() {
138063
+ this.stop();
138064
+ }
138065
+ /**
138066
+ * setTimeout self-rescheduling chain with drift-free frame advancement and
138067
+ * paint-cost backpressure (same pattern as the pi-tui Loader): a slow tick
138068
+ * defers the next one by `max(0, interval - cost, cost × 9)` instead of
138069
+ * stacking intervals, keeping the spinner ≤ ~10% CPU under slow writes.
138070
+ */
138071
+ scheduleTick(intervalMs, delayMs) {
138072
+ const timer = setTimeout(() => {
138073
+ if (this.intervalId !== timer) return;
138074
+ const startedAt = performance.now();
138075
+ const elapsed = startedAt - this.lastTickAt;
138076
+ if (elapsed >= intervalMs) {
138077
+ const steps = Math.floor(elapsed / intervalMs);
138078
+ this.currentFrame = (this.currentFrame + steps) % BRAILLE_SPINNER_FRAMES.length;
138079
+ this.lastTickAt += steps * intervalMs;
138080
+ this.updateDisplay();
138081
+ }
138082
+ const frameCostMs = performance.now() - startedAt;
138083
+ if (this.intervalId !== timer) return;
138084
+ const cadenceDelayMs = Math.max(0, intervalMs - frameCostMs);
138085
+ const backpressureDelayMs = frameCostMs * 9;
138086
+ this.scheduleTick(intervalMs, Math.max(cadenceDelayMs, backpressureDelayMs));
138087
+ }, delayMs);
138088
+ this.intervalId = timer;
138089
+ }
137997
138090
  setLabel(label) {
137998
138091
  this.label = label;
137999
138092
  this.updateDisplay();
package/dist/main.mjs CHANGED
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
7
  //#region src/main.ts
8
8
  try {
9
- (await import("./app-6Hcxs2tP.mjs")).main();
9
+ (await import("./app-Bqc3MVGh.mjs")).main();
10
10
  } catch (error) {
11
11
  process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
12
12
  process.exit(1);