scream-code 0.15.1 → 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.
@@ -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-ATQMQq1p.mjs";
6
+ import { Kt as handleConnectCommand } from "./dispatch-DGeogib-.mjs";
7
7
  export { handleConnectCommand };
@@ -34205,14 +34205,27 @@ function convertMediaUrl(url, fallbackMimeType) {
34205
34205
  function createAbortError() {
34206
34206
  return new DOMException("The operation was aborted.", "AbortError");
34207
34207
  }
34208
- async function abortPromise(signal) {
34209
- if (signal === void 0) return new Promise(() => {});
34208
+ /**
34209
+ * Race async work against an abort signal, ALWAYS detaching the listener.
34210
+ * The Google GenAI SDK does not accept an AbortSignal, so callers race the
34211
+ * SDK call against the signal manually. A bare `addEventListener({ once })`
34212
+ * inside a losing promise leaks the listener on the caller's signal for the
34213
+ * rest of its lifetime - with a session-scoped signal that accumulates one
34214
+ * listener per request. This wrapper guarantees cleanup in both outcomes.
34215
+ */
34216
+ async function abortRace(signal, work) {
34217
+ if (signal === void 0) return work;
34210
34218
  if (signal.aborted) throw createAbortError();
34211
- return new Promise((_, reject) => {
34212
- signal.addEventListener("abort", () => {
34213
- reject(createAbortError());
34214
- }, { once: true });
34219
+ let onAbort = void 0;
34220
+ const abortPromise = new Promise((_, reject) => {
34221
+ onAbort = () => reject(createAbortError());
34222
+ signal.addEventListener("abort", onAbort, { once: true });
34215
34223
  });
34224
+ try {
34225
+ return await Promise.race([work, abortPromise]);
34226
+ } finally {
34227
+ if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
34228
+ }
34216
34229
  }
34217
34230
  function messageToGoogleGenAI(message) {
34218
34231
  if (message.role === "tool") throw new ChatProviderError("Tool messages must be converted via messagesToGoogleGenAIContents.");
@@ -34582,8 +34595,8 @@ var GoogleGenAIChatProvider = class {
34582
34595
  contents,
34583
34596
  config
34584
34597
  };
34585
- if (this._stream) return new GoogleGenAIStreamedMessage(await Promise.race([models.generateContentStream(params), abortPromise(options?.signal)]), true, options?.signal);
34586
- return new GoogleGenAIStreamedMessage(await Promise.race([models.generateContent(params), abortPromise(options?.signal)]), false, options?.signal);
34598
+ if (this._stream) return new GoogleGenAIStreamedMessage(await abortRace(options?.signal, models.generateContentStream(params)), true, options?.signal);
34599
+ return new GoogleGenAIStreamedMessage(await abortRace(options?.signal, models.generateContent(params)), false, options?.signal);
34587
34600
  } catch (error) {
34588
34601
  if (error instanceof DOMException && error.name === "AbortError") throw error;
34589
34602
  throw convertGoogleGenAIError(error);
@@ -47619,9 +47632,41 @@ function estimateTokensForMessage(message) {
47619
47632
  messageTokenCache.set(message, total);
47620
47633
  return total;
47621
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
+ }
47622
47664
  function estimateTokensForContentPart(part) {
47623
47665
  if (part.type === "text") return estimateTokens$1(part.text);
47624
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;
47625
47670
  return 0;
47626
47671
  }
47627
47672
  //#endregion
@@ -53403,6 +53448,17 @@ var MemoryMemoStore = class MemoryMemoStore {
53403
53448
  async append(entry) {
53404
53449
  return this.withWriteLock(() => this.appendInternal(entry));
53405
53450
  }
53451
+ /**
53452
+ * True when a memo with the same source session + user need + approach
53453
+ * already exists. Used by compaction-summary recovery to re-store memos
53454
+ * idempotently after a crash between compaction apply and extraction.
53455
+ * The approach field keeps two distinct memos that happen to share the
53456
+ * same user need from being collapsed into one.
53457
+ */
53458
+ existsBySourceAndNeed(sourceSessionId, userNeed, approach) {
53459
+ if (this.db === void 0) return false;
53460
+ return this.db.prepare("SELECT 1 FROM memos WHERE source_session_id = ? AND user_need = ? AND approach = ? LIMIT 1").get(sourceSessionId, userNeed, approach) !== void 0;
53461
+ }
53406
53462
  /** Delete a memo by id. */
53407
53463
  async delete(id) {
53408
53464
  return this.withWriteLock(() => this.deleteInternal(id));
@@ -78944,7 +79000,11 @@ var FullCompaction = class {
78944
79000
  this.agent.context.applyCompaction(result);
78945
79001
  this.lastCompactionFiles = fileLists;
78946
79002
  this.lowWaterMark = Math.floor(this.effectiveTokenCount * 1.1);
78947
- 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
+ }
78948
79008
  this.triggerPostCompactHook(data, result);
78949
79009
  this.detectSkillCandidates(skillCandidateSummary);
78950
79010
  this.consecutiveCompactionFailures = 0;
@@ -79164,6 +79224,31 @@ function extractPreviousSummary(history) {
79164
79224
  const text = head.content.filter((p) => p.type === "text").map((p) => p.text).join("");
79165
79225
  return text.length > 0 ? text : null;
79166
79226
  }
79227
+ /**
79228
+ * Crash-recovery path for compaction memos. `context.apply_compaction` is
79229
+ * written to wire before the extraction step runs; if the process dies in
79230
+ * that window the memos are lost forever (nothing re-extracts them). On wire
79231
+ * replay the summary is available again, so re-parse it and store anything
79232
+ * missing. Idempotent: a healthy replay finds the memos already stored
79233
+ * (existsBySourceAndNeed) and skips them, so recovery never duplicates.
79234
+ */
79235
+ async function recoverMemosFromCompactionSummary(agent, summary) {
79236
+ const memoStore = agent.memoStore;
79237
+ if (!memoStore || summary === void 0 || summary.trim().length === 0) return;
79238
+ const memos = parseMemoryMemos(summary);
79239
+ if (memos.length === 0) return;
79240
+ const sessionId = agent.homedir ? basename$1(dirname$2(dirname$2(agent.homedir))) : "unknown";
79241
+ const sessionTitle = await agent.getSessionTitle().catch(() => void 0);
79242
+ const projectDir = agent.config.cwd;
79243
+ const failed = (await Promise.allSettled(memos.map(async (memo) => {
79244
+ if (memoStore.existsBySourceAndNeed(sessionId, memo.userNeed, memo.approach)) return;
79245
+ memo.sourceSessionId = sessionId;
79246
+ memo.sourceSessionTitle = sessionTitle ?? "";
79247
+ memo.projectDir = projectDir;
79248
+ await memoStore.append(memo);
79249
+ }))).filter((result) => result.status === "rejected").length;
79250
+ if (failed > 0) agent.log.warn("Some recovered memory memos failed to store", { failed });
79251
+ }
79167
79252
  //#endregion
79168
79253
  //#region ../../packages/agent-core/src/flags/registry.ts
79169
79254
  /**
@@ -79382,7 +79467,7 @@ function findSupersededPaths(messages, cutoff) {
79382
79467
  /**
79383
79468
  * Lightweight compaction that truncates old tool results without an LLM call.
79384
79469
  *
79385
- * 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
79386
79471
  * messages are replaced with a short placeholder. This frees up tokens for the
79387
79472
  * model without the cost and latency of a full compaction.
79388
79473
  *
@@ -84844,6 +84929,7 @@ var FileSystemAgentRecordPersistence = class {
84844
84929
  pendingRecords = [];
84845
84930
  shouldClear = false;
84846
84931
  directorySynced = false;
84932
+ rewriteSeq = 0;
84847
84933
  flushPromise;
84848
84934
  error;
84849
84935
  constructor(filePath, options = {}) {
@@ -84953,7 +85039,21 @@ var FileSystemAgentRecordPersistence = class {
84953
85039
  const content = (this.options.blobStore !== void 0 ? await Promise.all(batch.map((record) => this.options.blobStore.offload(record))) : batch).map((e) => JSON.stringify(e) + "\n").join("");
84954
85040
  const directory = dirname$2(this.filePath);
84955
85041
  await mkdir(directory, { recursive: true });
84956
- const fh = await open(this.filePath, shouldClear ? "w" : "a");
85042
+ if (shouldClear) {
85043
+ const tmpPath = `${this.filePath}.${process.pid}.${this.rewriteSeq++}.tmp`;
85044
+ const tmp = await open(tmpPath, "w");
85045
+ try {
85046
+ if (content.length > 0) await tmp.writeFile(content, "utf8");
85047
+ await tmp.sync();
85048
+ } finally {
85049
+ await tmp.close();
85050
+ }
85051
+ await rename(tmpPath, this.filePath);
85052
+ await syncDir(directory);
85053
+ this.directorySynced = true;
85054
+ return;
85055
+ }
85056
+ const fh = await open(this.filePath, "a");
84957
85057
  try {
84958
85058
  if (content.length > 0) await fh.writeFile(content, "utf8");
84959
85059
  await fh.sync();
@@ -85310,6 +85410,7 @@ function restoreAgentRecord(agent, input) {
85310
85410
  return;
85311
85411
  case "context.apply_compaction":
85312
85412
  agent.context.applyCompaction(input);
85413
+ recoverMemosFromCompactionSummary(agent, input.summary);
85313
85414
  return;
85314
85415
  case "context.snapshot":
85315
85416
  agent.context.restoreJSONSnapshot(input.snapshot);
@@ -85406,11 +85507,19 @@ var AgentRecords = class {
85406
85507
  snapshotIndex = i;
85407
85508
  break;
85408
85509
  }
85510
+ let foldedCompactionSummary;
85409
85511
  for (let i = 0; i < replayedRecords.length; i++) {
85410
85512
  const record = replayedRecords[i];
85411
85513
  if (!record) continue;
85412
- if (i < snapshotIndex && isSnapshotFoldedContextRecord(record.type)) continue;
85514
+ if (i < snapshotIndex && isSnapshotFoldedContextRecord(record.type)) {
85515
+ if (record.type === "context.apply_compaction") foldedCompactionSummary = record.summary;
85516
+ continue;
85517
+ }
85413
85518
  this.restore(record);
85519
+ if (record.type === "context.snapshot" && foldedCompactionSummary !== void 0) {
85520
+ recoverMemosFromCompactionSummary(this.agent, foldedCompactionSummary);
85521
+ foldedCompactionSummary = void 0;
85522
+ }
85414
85523
  }
85415
85524
  this.agent.context.dropVacuousOpenMessages();
85416
85525
  if (shouldRewrite) {
@@ -105566,6 +105675,9 @@ var SessionSubagentHost = class {
105566
105675
  backgroundTaskTimeoutMs;
105567
105676
  modelBindings;
105568
105677
  activeChildren = /* @__PURE__ */ new Map();
105678
+ /** Per-child per-model usage already folded into the parent totals, so a
105679
+ * resumed child's aggregation only adds the delta. */
105680
+ aggregatedChildUsage = /* @__PURE__ */ new WeakMap();
105569
105681
  constructor(session, ownerAgentId, backgroundTaskTimeoutMs, modelBindings) {
105570
105682
  this.session = session;
105571
105683
  this.ownerAgentId = ownerAgentId;
@@ -105709,6 +105821,21 @@ var SessionSubagentHost = class {
105709
105821
  result = lastAssistantText$1(child);
105710
105822
  }
105711
105823
  const usage = child.usage.data().total;
105824
+ const childByModel = child.usage.data().byModel ?? {};
105825
+ const previous = this.aggregatedChildUsage.get(child) ?? {};
105826
+ for (const [model, childUsage] of Object.entries(childByModel)) {
105827
+ const delta = previous[model] === void 0 ? childUsage : subtractUsage(childUsage, previous[model]);
105828
+ if (isZeroUsage(delta)) continue;
105829
+ try {
105830
+ parent.usage.record(model, delta, "session");
105831
+ } catch (error) {
105832
+ parent.log.warn("Failed to aggregate subagent usage", {
105833
+ model,
105834
+ error: String(error)
105835
+ });
105836
+ }
105837
+ }
105838
+ this.aggregatedChildUsage.set(child, childByModel);
105712
105839
  let findingsBlock = "";
105713
105840
  if (profileName === "reviewer") {
105714
105841
  const findings = getFindingsFromStore(child.tools.toolStore);
@@ -105813,6 +105940,18 @@ var SessionSubagentHost = class {
105813
105940
  });
105814
105941
  }
105815
105942
  };
105943
+ /** Element-wise subtraction clamped at zero (usage can never go negative). */
105944
+ function subtractUsage(current, previous) {
105945
+ return {
105946
+ inputOther: Math.max(0, current.inputOther - previous.inputOther),
105947
+ output: Math.max(0, current.output - previous.output),
105948
+ inputCacheRead: Math.max(0, current.inputCacheRead - previous.inputCacheRead),
105949
+ inputCacheCreation: Math.max(0, current.inputCacheCreation - previous.inputCacheCreation)
105950
+ };
105951
+ }
105952
+ function isZeroUsage(usage) {
105953
+ return usage.inputOther === 0 && usage.output === 0 && usage.inputCacheRead === 0 && usage.inputCacheCreation === 0;
105954
+ }
105816
105955
  async function runChildTurnToCompletion(child, signal) {
105817
105956
  const completion = await child.turn.waitForCurrentTurn(signal);
105818
105957
  const turnEnded = completion.event;
@@ -123049,28 +123188,24 @@ var ScreamCore = class {
123049
123188
  return this.config;
123050
123189
  }
123051
123190
  async setScreamConfig(input) {
123052
- return this.enqueueConfigMutation(async () => {
123053
- const config = mergeConfigPatch(readConfigFile(this.configPath), input);
123054
- await writeConfigFile(this.configPath, config);
123055
- return this.config = loadRuntimeConfig(this.configPath);
123056
- });
123191
+ const config = mergeConfigPatch(readConfigFile(this.configPath), input);
123192
+ await writeConfigFile(this.configPath, config);
123193
+ return this.config = loadRuntimeConfig(this.configPath);
123057
123194
  }
123058
123195
  async removeScreamProvider(input) {
123059
- return this.enqueueConfigMutation(async () => {
123060
- const config = readConfigFile(this.configPath);
123061
- delete config.providers[input.providerId];
123062
- let removedDefault = false;
123063
- const existingModels = config.models ?? {};
123064
- for (const [key, model] of Object.entries(existingModels)) if (typeof model === "object" && model !== null && !Array.isArray(model) && model["provider"] === input.providerId) {
123065
- delete existingModels[key];
123066
- if (config.defaultModel === key) removedDefault = true;
123067
- }
123068
- config.models = existingModels;
123069
- if (removedDefault) config.defaultModel = void 0;
123070
- if (config.defaultProvider === input.providerId) config.defaultProvider = void 0;
123071
- await writeConfigFile(this.configPath, config);
123072
- return this.config = loadRuntimeConfig(this.configPath);
123073
- });
123196
+ const config = readConfigFile(this.configPath);
123197
+ delete config.providers[input.providerId];
123198
+ let removedDefault = false;
123199
+ const existingModels = config.models ?? {};
123200
+ for (const [key, model] of Object.entries(existingModels)) if (typeof model === "object" && model !== null && !Array.isArray(model) && model["provider"] === input.providerId) {
123201
+ delete existingModels[key];
123202
+ if (config.defaultModel === key) removedDefault = true;
123203
+ }
123204
+ config.models = existingModels;
123205
+ if (removedDefault) config.defaultModel = void 0;
123206
+ if (config.defaultProvider === input.providerId) config.defaultProvider = void 0;
123207
+ await writeConfigFile(this.configPath, config);
123208
+ return this.config = loadRuntimeConfig(this.configPath);
123074
123209
  }
123075
123210
  setRuntimeSystemPrompt({ sessionId, ...payload }) {
123076
123211
  return this.sessionApi(sessionId).setRuntimeSystemPrompt(payload);
@@ -123612,23 +123747,6 @@ var ScreamCore = class {
123612
123747
  reloadProviderManager() {
123613
123748
  return this.config = loadRuntimeConfig(this.configPath);
123614
123749
  }
123615
- /**
123616
- * Serialize config-file mutations. Both setScreamConfig and
123617
- * removeScreamProvider are read-modify-write cycles that read the file
123618
- * synchronously and then await the write; two calls issued together (e.g.
123619
- * cycling thinking levels plus confirming a model in the picker fire
123620
- * back-to-back setConfig patches) would otherwise read the SAME stale
123621
- * snapshot, and the later write silently reverts the earlier patch — a
123622
- * lost update that makes e.g. a just-selected default model flip back to
123623
- * the old one. The queue guarantees each cycle re-reads after the
123624
- * previous write completed.
123625
- */
123626
- configMutationQueue = Promise.resolve();
123627
- enqueueConfigMutation(mutate) {
123628
- const run = this.configMutationQueue.then(mutate, mutate);
123629
- this.configMutationQueue = run.catch(() => void 0);
123630
- return run;
123631
- }
123632
123750
  async refreshSessionRuntimeConfig(session, config) {
123633
123751
  const api = new SessionAPIImpl(session);
123634
123752
  const requested = (await api.getModel({ agentId: "main" })).trim();
@@ -125559,24 +125677,15 @@ const PIXEL_PULSE_FRAMES = [
125559
125677
  "▒",
125560
125678
  "▓"
125561
125679
  ];
125562
- const PULSE_WAVE_FRAMES = [
125563
- {
125564
- active: 0,
125565
- forward: true
125566
- },
125567
- {
125568
- active: 1,
125569
- forward: true
125570
- },
125571
- {
125572
- active: 2,
125573
- forward: true
125574
- },
125575
- {
125576
- active: 1,
125577
- forward: false
125578
- }
125579
- ];
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];
125580
125689
  //#endregion
125581
125690
  //#region src/tui/commands/knowledge-store.ts
125582
125691
  let knowledgeStoreInstance;
@@ -129362,13 +129471,22 @@ var FooterComponent = class {
129362
129471
  this.#stopStatusTimer();
129363
129472
  if (phase === "idle" && !goalActive) return;
129364
129473
  const intervalMs = 1e3 / 30;
129365
- 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();
129366
129480
  this.ui.requestRender();
129367
- }, 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;
129368
129486
  }
129369
129487
  #stopStatusTimer() {
129370
129488
  if (!this.statusTimer) return;
129371
- clearInterval(this.statusTimer);
129489
+ clearTimeout(this.statusTimer);
129372
129490
  this.statusTimer = null;
129373
129491
  }
129374
129492
  render(width) {
@@ -130817,10 +130935,11 @@ async function changeThinkingLevel(host, alias, level) {
130817
130935
  }
130818
130936
  const prevLevel = host.state.appState.thinkingLevel;
130819
130937
  const isActiveModel = alias === host.state.appState.model;
130820
- const session = host.session;
130821
130938
  if (isActiveModel && level !== prevLevel) {
130939
+ const session = host.session;
130822
130940
  try {
130823
- if (session !== void 0) await session.setThinking(level);
130941
+ if (session === void 0) await host.authFlow.activateModelAfterLogin(alias, level);
130942
+ else await session.setThinking(level);
130824
130943
  } catch (error) {
130825
130944
  const msg = formatErrorMessage(error);
130826
130945
  host.showError(`Failed to set thinking: ${msg}`);
@@ -130836,7 +130955,7 @@ async function changeThinkingLevel(host, alias, level) {
130836
130955
  host.showError(`Failed to save thinking default: ${msg}`);
130837
130956
  return;
130838
130957
  }
130839
- const status = isActiveModel && level !== prevLevel ? session === void 0 ? persisted ? `No active session - thinking ${level} for ${alias} saved as default.` : `No active session - thinking ${level} was NOT saved (${alias} is not the default model, so its level applies only when a new session selects it).` : `Thinking set to ${level} for ${alias}.` : persisted ? `Saved thinking ${level} as default for ${alias}.` : `Thinking already ${level} for ${alias}.`;
130958
+ const status = isActiveModel && level !== prevLevel ? `Thinking set to ${level} for ${alias}.` : persisted ? `Saved thinking ${level} as default for ${alias}.` : `Thinking already ${level} for ${alias}.`;
130840
130959
  host.showStatus(status, host.state.theme.colors.success);
130841
130960
  }
130842
130961
  async function persistThinkingDefault(host, alias, level) {
@@ -130865,16 +130984,18 @@ async function performModelSwitch(host, alias, thinkingLevel) {
130865
130984
  const prevThinkingLevel = host.state.appState.thinkingLevel;
130866
130985
  const modelChanged = alias !== prevModel;
130867
130986
  const thinkingChanged = thinkingLevel !== prevThinkingLevel;
130868
- const session = host.session;
130869
- const overflow = session !== void 0 && alias !== prevModel ? contextOverflowForModel(host.state.appState, alias) : null;
130987
+ const needsSessionActivation = modelChanged || thinkingChanged;
130988
+ const overflow = alias !== prevModel ? contextOverflowForModel(host.state.appState, alias) : null;
130870
130989
  if (overflow !== null) {
130871
130990
  host.showNotice("Storm Breaker(风暴守护者)", `无法切换到模型「${alias}」:当前会话上下文 ${formatTokenCount$1(overflow.currentTokens)} 已超出该模型上限 ${formatTokenCount$1(overflow.maxContextTokens)}。建议先执行 /compact 压缩上下文,或选择上下文窗口更大的模型。`);
130872
130991
  return;
130873
130992
  }
130993
+ const session = host.session;
130874
130994
  let effectiveAlias = alias;
130875
130995
  let effectiveThinking = thinkingLevel;
130876
130996
  try {
130877
- if (session !== void 0) {
130997
+ if (session === void 0 && needsSessionActivation) await host.authFlow.activateModelAfterLogin(alias, thinkingLevel);
130998
+ else if (session !== void 0) {
130878
130999
  if (modelChanged) await session.setModel(alias);
130879
131000
  if (thinkingChanged) await session.setThinking(thinkingLevel);
130880
131001
  const confirmed = await session.getStatus().catch(() => null);
@@ -130897,13 +131018,12 @@ async function performModelSwitch(host, alias, thinkingLevel) {
130897
131018
  persisted = await persistModelSelection(host, alias, thinkingLevel);
130898
131019
  } catch (error) {
130899
131020
  const msg = formatErrorMessage(error);
130900
- host.showError(`${session === void 0 ? "Selected" : "Switched to"} ${effectiveAlias}, but failed to save default: ${msg}`);
131021
+ host.showError(`Switched to ${effectiveAlias}, but failed to save default: ${msg}`);
130901
131022
  return;
130902
131023
  }
130903
131024
  const hasHistory = host.state.appState.contextTokens > 0;
130904
131025
  const cacheWarning = modelChanged && hasHistory ? " Note: switching models invalidates the existing prompt cache - use /new to avoid extra token costs." : "";
130905
131026
  const status = (() => {
130906
- if (session === void 0 && (modelChanged || thinkingChanged)) return `No active session - ${effectiveAlias} (thinking ${effectiveThinking}) saved as default. It applies when you start a new session.`;
130907
131027
  if (modelChanged) return `Switched to ${effectiveAlias} with thinking ${effectiveThinking}.${cacheWarning}`;
130908
131028
  if (thinkingChanged) return `Thinking set to ${effectiveThinking} for ${effectiveAlias}.`;
130909
131029
  if (persisted) return `Saved ${effectiveAlias} with thinking ${effectiveThinking} as default.`;
@@ -133042,7 +133162,8 @@ var ThinkingComponent = class {
133042
133162
  expanded = false;
133043
133163
  ui;
133044
133164
  spinnerFrame = 0;
133045
- spinnerInterval;
133165
+ spinnerTimer;
133166
+ lastSpinnerTickAt = 0;
133046
133167
  textComponent;
133047
133168
  cachedWidth;
133048
133169
  cachedLines;
@@ -133126,17 +133247,41 @@ var ThinkingComponent = class {
133126
133247
  }
133127
133248
  return truncated;
133128
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
+ }
133129
133276
  startSpinner() {
133130
- if (this.ui === void 0 || this.spinnerInterval !== void 0) return;
133131
- this.spinnerInterval = setInterval(() => {
133132
- this.spinnerFrame = (this.spinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
133133
- this.ui?.requestRender();
133134
- }, 80);
133277
+ if (this.ui === void 0 || this.spinnerTimer !== void 0) return;
133278
+ this.lastSpinnerTickAt = performance.now();
133279
+ this.scheduleSpinnerTick(80);
133135
133280
  }
133136
133281
  stopSpinner() {
133137
- if (this.spinnerInterval === void 0) return;
133138
- clearInterval(this.spinnerInterval);
133139
- this.spinnerInterval = void 0;
133282
+ if (this.spinnerTimer === void 0) return;
133283
+ clearTimeout(this.spinnerTimer);
133284
+ this.spinnerTimer = void 0;
133140
133285
  }
133141
133286
  };
133142
133287
  //#endregion
@@ -136028,7 +136173,11 @@ function findRevokeAnchorComponentIndex(children, count) {
136028
136173
  function removeRevokeContextComponents(children, startIndex) {
136029
136174
  for (let i = children.length - 1; i >= startIndex; i--) {
136030
136175
  const child = children[i];
136031
- 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
+ }
136032
136181
  }
136033
136182
  }
136034
136183
  function isRevokeAnchorComponent(child) {
@@ -137890,6 +138039,8 @@ var MoonLoader = class extends Text {
137890
138039
  ui;
137891
138040
  colorFn;
137892
138041
  label;
138042
+ /** Wall-clock time of the last frame advance, for drift-free scheduling. */
138043
+ lastTickAt = 0;
137893
138044
  constructor(ui, colorFn, label = "") {
137894
138045
  super("", 1, 0);
137895
138046
  this.ui = ui;
@@ -137898,18 +138049,44 @@ var MoonLoader = class extends Text {
137898
138049
  this.start();
137899
138050
  }
137900
138051
  start() {
138052
+ this.lastTickAt = performance.now();
137901
138053
  this.updateDisplay();
137902
- this.intervalId = setInterval(() => {
137903
- this.currentFrame = (this.currentFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
137904
- this.updateDisplay();
137905
- }, 80);
138054
+ this.scheduleTick(80, 80);
137906
138055
  }
137907
138056
  stop() {
137908
138057
  if (this.intervalId) {
137909
- clearInterval(this.intervalId);
138058
+ clearTimeout(this.intervalId);
137910
138059
  this.intervalId = null;
137911
138060
  }
137912
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
+ }
137913
138090
  setLabel(label) {
137914
138091
  this.label = label;
137915
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-C3aR5fdI.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);