scream-code 0.7.3 → 0.7.4

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.
@@ -97094,7 +97094,8 @@ function isMutatingTool(toolName) {
97094
97094
  * to try a different approach.
97095
97095
  * - Storm Breaker: when a mutating tool is called repeatedly on the same target
97096
97096
  * within a sliding window (default size 6), the call is suppressed entirely
97097
- * and an error result is returned to the model.
97097
+ * and a friendly advisory result is returned to the model (non-error, so the
97098
+ * TUI renders it as normal output rather than a failed tool call).
97098
97099
  */
97099
97100
  var ToolCallDeduplicator = class {
97100
97101
  stepDeferreds = /* @__PURE__ */ new Map();
@@ -97154,10 +97155,7 @@ var ToolCallDeduplicator = class {
97154
97155
  const key = makeKey(toolName, args);
97155
97156
  if (isMutatingTool(toolName)) {
97156
97157
  const priorCount = this.stormWindow.filter((e) => e.key === key).length;
97157
- if (priorCount >= this.stormThreshold - 1) return {
97158
- output: `Storm Breaker: ${toolName} 对同一目标已连续调用 ${String(priorCount + 1)} 次。请制定计划后再执行,或尝试不同的方法。`,
97159
- isError: true
97160
- };
97158
+ if (priorCount >= this.stormThreshold - 1) return { output: `Storm Breaker(风暴守护者):检测到无效循环调用风险——${toolName} 对同一目标已连续调用 ${String(priorCount + 1)} 次。建议先制定计划方案,或更换其他方法后再继续。` };
97161
97159
  }
97162
97160
  const index = this.stepCalls.length;
97163
97161
  this.stepCalls.push(key);
@@ -122177,6 +122175,7 @@ async function runPrompt(opts, version, io = {}) {
122177
122175
  workDir
122178
122176
  });
122179
122177
  let restorePromptSessionPermission = async () => {};
122178
+ let cleanupEphemeralSession = async () => {};
122180
122179
  let removeTerminationCleanup;
122181
122180
  let cleanupPromise;
122182
122181
  const cleanupPromptRun = async () => {
@@ -122185,7 +122184,11 @@ async function runPrompt(opts, version, io = {}) {
122185
122184
  try {
122186
122185
  await restorePromptSessionPermission();
122187
122186
  } finally {
122188
- await harness.close();
122187
+ try {
122188
+ await cleanupEphemeralSession();
122189
+ } finally {
122190
+ await harness.close();
122191
+ }
122189
122192
  }
122190
122193
  })();
122191
122194
  await cleanupPromise;
@@ -122193,13 +122196,19 @@ async function runPrompt(opts, version, io = {}) {
122193
122196
  removeTerminationCleanup = installPromptTerminationCleanup(promptProcess, cleanupPromptRun);
122194
122197
  try {
122195
122198
  await harness.ensureConfigFile();
122196
- const { session, restorePermission } = await resolvePromptSession(harness, opts, workDir, (await harness.getConfig()).defaultModel, stderr, (restorePermission) => {
122199
+ const { session, resumed, restorePermission } = await resolvePromptSession(harness, opts, workDir, (await harness.getConfig()).defaultModel, stderr, (restorePermission) => {
122197
122200
  restorePromptSessionPermission = restorePermission;
122198
122201
  });
122199
122202
  restorePromptSessionPermission = restorePermission;
122203
+ if (process.env["SCREAM_FUSIONPLAN_SUBAGENT"] === "1" && !resumed) {
122204
+ const ephemeralSessionId = session.id;
122205
+ cleanupEphemeralSession = async () => {
122206
+ await harness.deleteSession(ephemeralSessionId).catch(() => {});
122207
+ };
122208
+ }
122200
122209
  const outputFormat = opts.outputFormat ?? "text";
122201
122210
  await runPromptTurn(session, opts.prompt, outputFormat, stdout, stderr);
122202
- writeResumeHint(session.id, outputFormat, stdout, stderr);
122211
+ if (process.env["SCREAM_FUSIONPLAN_SUBAGENT"] !== "1") writeResumeHint(session.id, outputFormat, stdout, stderr);
122203
122212
  } finally {
122204
122213
  await cleanupPromptRun();
122205
122214
  }
@@ -122631,13 +122640,21 @@ const TuiConfigFileSchema = z.object({
122631
122640
  enabled: z.boolean().optional(),
122632
122641
  notification_condition: NotificationConditionSchema.optional()
122633
122642
  }).optional(),
122634
- like: TuiLikePreferencesSchema.optional()
122643
+ like: TuiLikePreferencesSchema.optional(),
122644
+ fusionPlan: z.object({
122645
+ timeoutSeconds: z.number().int().min(30).max(3600).optional(),
122646
+ workerCount: z.number().int().min(1).max(8).optional()
122647
+ }).optional()
122635
122648
  });
122636
122649
  const TuiConfigSchema = z.object({
122637
122650
  theme: TuiThemeSchema,
122638
122651
  editorCommand: z.string().nullable(),
122639
122652
  notifications: NotificationsConfigSchema,
122640
- like: TuiLikePreferencesSchema
122653
+ like: TuiLikePreferencesSchema,
122654
+ fusionPlan: z.object({
122655
+ timeoutSeconds: z.number().int().min(30).max(3600),
122656
+ workerCount: z.number().int().min(1).max(8)
122657
+ })
122641
122658
  });
122642
122659
  const DEFAULT_NOTIFICATIONS_CONFIG = {
122643
122660
  enabled: true,
@@ -122647,13 +122664,16 @@ const DEFAULT_TUI_CONFIG = TuiConfigSchema.parse({
122647
122664
  theme: "auto",
122648
122665
  editorCommand: null,
122649
122666
  notifications: DEFAULT_NOTIFICATIONS_CONFIG,
122650
- like: {}
122667
+ like: {},
122668
+ fusionPlan: {
122669
+ timeoutSeconds: 600,
122670
+ workerCount: 3
122671
+ }
122651
122672
  });
122652
122673
  /**
122653
122674
  * Thrown by `loadTuiConfig` when the on-disk TOML cannot be parsed.
122654
122675
  * Carries `fallback` so the caller can recover without re-running the
122655
- * I/O, and use `message` (== `INVALID_TUI_CONFIG_MESSAGE`) as a
122656
- * user-facing notice.
122676
+ * discovery code.
122657
122677
  */
122658
122678
  var TuiConfigParseError = class extends Error {
122659
122679
  name = "TuiConfigParseError";
@@ -122689,6 +122709,7 @@ async function saveTuiConfig(config, filePath = getTuiConfigPath()) {
122689
122709
  function normalizeTuiConfig(config) {
122690
122710
  const command = config.editor?.command?.trim();
122691
122711
  const like = config.like ?? {};
122712
+ const fusionPlan = config.fusionPlan ?? {};
122692
122713
  return TuiConfigSchema.parse({
122693
122714
  theme: config.theme ?? DEFAULT_TUI_CONFIG.theme,
122694
122715
  editorCommand: command === void 0 || command.length === 0 ? null : command,
@@ -122700,6 +122721,10 @@ function normalizeTuiConfig(config) {
122700
122721
  nickname: normalizeOptionalString(like.nickname),
122701
122722
  tone: normalizeOptionalString(like.tone),
122702
122723
  other: normalizeOptionalString(like.other)
122724
+ },
122725
+ fusionPlan: {
122726
+ timeoutSeconds: fusionPlan.timeoutSeconds ?? DEFAULT_TUI_CONFIG.fusionPlan.timeoutSeconds,
122727
+ workerCount: fusionPlan.workerCount ?? DEFAULT_TUI_CONFIG.fusionPlan.workerCount
122703
122728
  }
122704
122729
  });
122705
122730
  }
@@ -122729,6 +122754,10 @@ notification_condition = "${config.notifications.condition}" # "unfocused" | "al
122729
122754
  nickname = "${nickname}"
122730
122755
  tone = "${tone}"
122731
122756
  other = "${other}"
122757
+
122758
+ [fusionPlan]
122759
+ timeoutSeconds = ${config.fusionPlan.timeoutSeconds} # 30..3600, default 600
122760
+ workerCount = ${config.fusionPlan.workerCount} # 1..8, default 3
122732
122761
  `;
122733
122762
  }
122734
122763
  function escapeTomlBasicString(value) {
@@ -124385,6 +124414,52 @@ var ThemeSelectorComponent = class extends ChoicePickerComponent {
124385
124414
  }
124386
124415
  };
124387
124416
  //#endregion
124417
+ //#region src/tui/utils/app-state.ts
124418
+ /** True when a model turn is in progress (waiting / thinking / composing). */
124419
+ function isStreaming(state) {
124420
+ return state.streamingPhase !== "idle";
124421
+ }
124422
+ /** True when the session is busy and should reject state-changing operations.
124423
+ * Covers both active streaming and context compaction. */
124424
+ function isBusy(state) {
124425
+ return isStreaming(state) || state.isCompacting;
124426
+ }
124427
+ //#endregion
124428
+ //#region src/utils/usage/usage-format.ts
124429
+ /**
124430
+ * Formatting helpers for the `/usage` slash command.
124431
+ *
124432
+ * Kept pure + ANSI-free so they're trivial to unit-test; the slash
124433
+ * command itself chalks the colour afterwards.
124434
+ */
124435
+ function formatTokenCount$1(n) {
124436
+ if (!Number.isFinite(n) || n < 0) return "0";
124437
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
124438
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
124439
+ return String(Math.round(n));
124440
+ }
124441
+ /**
124442
+ * Build a `[███░░░░░░░]` style bar. Returns a plain-ASCII string with
124443
+ * `filled`/`empty` glyphs — colouring is the caller's responsibility.
124444
+ */
124445
+ function renderProgressBar(ratio, width = 20, filled = "█", empty = "░") {
124446
+ const clamped = safeUsageRatio(ratio);
124447
+ const filledCount = Math.round(clamped * width);
124448
+ return filled.repeat(filledCount) + empty.repeat(Math.max(0, width - filledCount));
124449
+ }
124450
+ function safeUsageRatio(ratio) {
124451
+ return Number.isFinite(ratio) ? Math.max(0, Math.min(ratio, 1)) : 0;
124452
+ }
124453
+ /**
124454
+ * Map a usage ratio to a semantic colour token — the `/usage` renderer
124455
+ * translates these into palette hex values.
124456
+ */
124457
+ function ratioSeverity(ratio) {
124458
+ if (ratio >= .85) return "danger";
124459
+ if (ratio >= .5) return "warn";
124460
+ return "ok";
124461
+ }
124462
+ //#endregion
124388
124463
  //#region src/tui/constant/terminal.ts
124389
124464
  const QUERY_TERMINAL_THEME = `[?996n`;
124390
124465
  const TERMINAL_THEME_DARK = `[?997;1n`;
@@ -124714,41 +124789,6 @@ function resolveThemeSync(theme) {
124714
124789
  return theme;
124715
124790
  }
124716
124791
  //#endregion
124717
- //#region src/utils/usage/usage-format.ts
124718
- /**
124719
- * Formatting helpers for the `/usage` slash command.
124720
- *
124721
- * Kept pure + ANSI-free so they're trivial to unit-test; the slash
124722
- * command itself chalks the colour afterwards.
124723
- */
124724
- function formatTokenCount$1(n) {
124725
- if (!Number.isFinite(n) || n < 0) return "0";
124726
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
124727
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
124728
- return String(Math.round(n));
124729
- }
124730
- /**
124731
- * Build a `[███░░░░░░░]` style bar. Returns a plain-ASCII string with
124732
- * `filled`/`empty` glyphs — colouring is the caller's responsibility.
124733
- */
124734
- function renderProgressBar(ratio, width = 20, filled = "█", empty = "░") {
124735
- const clamped = safeUsageRatio(ratio);
124736
- const filledCount = Math.round(clamped * width);
124737
- return filled.repeat(filledCount) + empty.repeat(Math.max(0, width - filledCount));
124738
- }
124739
- function safeUsageRatio(ratio) {
124740
- return Number.isFinite(ratio) ? Math.max(0, Math.min(ratio, 1)) : 0;
124741
- }
124742
- /**
124743
- * Map a usage ratio to a semantic colour token — the `/usage` renderer
124744
- * translates these into palette hex values.
124745
- */
124746
- function ratioSeverity(ratio) {
124747
- if (ratio >= .85) return "danger";
124748
- if (ratio >= .5) return "warn";
124749
- return "ok";
124750
- }
124751
- //#endregion
124752
124792
  //#region src/tui/components/messages/usage-panel.ts
124753
124793
  const LEFT_MARGIN$1 = 2;
124754
124794
  const SIDE_PADDING$1 = 1;
@@ -125041,6 +125081,46 @@ async function loadManagedUsageReport(host) {
125041
125081
  }
125042
125082
  //#endregion
125043
125083
  //#region src/tui/commands/config.ts
125084
+ /**
125085
+ * Storm Breaker guard for model switches. Returns the (currentTokens,
125086
+ * maxContextTokens) pair when switching to `alias` would overflow its
125087
+ * context window, or `null` when the switch is safe / unknown.
125088
+ *
125089
+ * Exported (and kept pure) so the guard is unit-testable without spinning
125090
+ * up a full ScreamTUI + session mock.
125091
+ */
125092
+ function contextOverflowForModel(state, alias) {
125093
+ const targetModel = state.availableModels[alias];
125094
+ if (targetModel === void 0) return null;
125095
+ const currentTokens = state.contextTokens;
125096
+ if (currentTokens <= 0) return null;
125097
+ if (currentTokens <= targetModel.maxContextSize) return null;
125098
+ return {
125099
+ currentTokens,
125100
+ maxContextTokens: targetModel.maxContextSize
125101
+ };
125102
+ }
125103
+ /**
125104
+ * Storm Breaker guard for /compact. Returns the (currentTokens,
125105
+ * maxContextTokens, ratio) triple when context usage is below 5% — compressing
125106
+ * at this point yields no benefit and discards useful history. Returns `null`
125107
+ * when compression is legitimate or when the window size is unknown.
125108
+ *
125109
+ * Exported (and kept pure) so the guard is unit-testable without a session.
125110
+ */
125111
+ function shouldGuardCompaction(state) {
125112
+ const max = state.maxContextTokens;
125113
+ if (max <= 0) return null;
125114
+ const currentTokens = state.contextTokens;
125115
+ if (currentTokens <= 0) return null;
125116
+ const ratio = currentTokens / max;
125117
+ if (ratio >= .05) return null;
125118
+ return {
125119
+ currentTokens,
125120
+ maxContextTokens: max,
125121
+ ratio
125122
+ };
125123
+ }
125044
125124
  async function handlePlanCommand(host, args) {
125045
125125
  const session = host.session;
125046
125126
  if (session === void 0) {
@@ -125211,6 +125291,12 @@ async function handleCompactCommand(host, args) {
125211
125291
  return;
125212
125292
  }
125213
125293
  const customInstruction = args.trim() || void 0;
125294
+ const guard = shouldGuardCompaction(host.state.appState);
125295
+ if (guard !== null) {
125296
+ const pct = (guard.ratio * 100).toFixed(1);
125297
+ host.showNotice("Storm Breaker(风暴守护者)", `当前上下文仅 ${formatTokenCount$1(guard.currentTokens)} / ${formatTokenCount$1(guard.maxContextTokens)}(${pct}%),压缩无收益。建议继续对话,待上下文增长至 5% 以上再执行 /compact。`);
125298
+ return;
125299
+ }
125214
125300
  await session.compact({ instruction: customInstruction });
125215
125301
  }
125216
125302
  async function handleEditorCommand(host, args) {
@@ -125270,7 +125356,8 @@ async function applyEditorChoice(host, value) {
125270
125356
  theme: host.state.appState.theme,
125271
125357
  editorCommand,
125272
125358
  notifications: host.state.appState.notifications,
125273
- like: host.state.appState.like
125359
+ like: host.state.appState.like,
125360
+ fusionPlan: host.state.appState.fusionPlan
125274
125361
  });
125275
125362
  } catch (error) {
125276
125363
  host.showStatus(`Failed to save editor: ${formatErrorMessage(error)}`, host.state.theme.colors.error);
@@ -125301,13 +125388,18 @@ function showModelPicker(host, selectedValue = host.state.appState.model) {
125301
125388
  }));
125302
125389
  }
125303
125390
  async function performModelSwitch(host, alias, thinkingLevel) {
125304
- if (host.state.appState.streamingPhase !== "idle") {
125391
+ if (isBusy(host.state.appState)) {
125305
125392
  host.showError("Cannot switch models while streaming — press Esc or Ctrl-C first.");
125306
125393
  return;
125307
125394
  }
125308
125395
  const prevModel = host.state.appState.model;
125309
125396
  const prevThinkingLevel = host.state.appState.thinkingLevel;
125310
125397
  const runtimeChanged = alias !== prevModel || thinkingLevel !== prevThinkingLevel;
125398
+ const overflow = alias !== prevModel ? contextOverflowForModel(host.state.appState, alias) : null;
125399
+ if (overflow !== null) {
125400
+ host.showNotice("Storm Breaker(风暴守护者)", `无法切换到模型「${alias}」:当前会话上下文 ${formatTokenCount$1(overflow.currentTokens)} 已超出该模型上限 ${formatTokenCount$1(overflow.maxContextTokens)}。建议先执行 /compact 压缩上下文,或选择上下文窗口更大的模型。`);
125401
+ return;
125402
+ }
125311
125403
  const session = host.session;
125312
125404
  try {
125313
125405
  if (session === void 0 && runtimeChanged) await host.authFlow.activateModelAfterLogin(alias, thinkingLevel);
@@ -125376,7 +125468,8 @@ async function applyThemeChoice(host, theme) {
125376
125468
  theme,
125377
125469
  editorCommand: host.state.appState.editorCommand,
125378
125470
  notifications: host.state.appState.notifications,
125379
- like: host.state.appState.like
125471
+ like: host.state.appState.like,
125472
+ fusionPlan: host.state.appState.fusionPlan
125380
125473
  });
125381
125474
  } catch (error) {
125382
125475
  host.showStatus(`Failed to save theme: ${formatErrorMessage(error)}`, host.state.theme.colors.error);
@@ -125902,6 +125995,13 @@ var GoalStatusMessageComponent = class {
125902
125995
  }
125903
125996
  };
125904
125997
  //#endregion
125998
+ //#region src/tui/utils/goal-loop-conflict.ts
125999
+ function detectGoalLoopConflict(state, action) {
126000
+ if (action === "enable_loop" && state.goalActive) return "goal_active";
126001
+ if (action === "enable_goal" && state.loopModeEnabled) return "loop_active";
126002
+ return null;
126003
+ }
126004
+ //#endregion
125905
126005
  //#region src/tui/commands/goal.ts
125906
126006
  const GOAL_STATUS_DISMISS_MS = 1e4;
125907
126007
  let activeGoalPanel;
@@ -125973,10 +126073,14 @@ async function createGoal(host, parsed) {
125973
126073
  host.showError("没有活跃的会话。");
125974
126074
  return;
125975
126075
  }
126076
+ if (detectGoalLoopConflict(host.state.appState, "enable_goal") === "loop_active") {
126077
+ host.showNotice("Storm Breaker(风暴守护者)", "当前已开启循环模式(/loop)。/goal 与 /loop 语义冲突:loop 每轮重置上下文,会破坏 goal 的工作笔记迭代。请先 /loop 关闭循环模式,再设置目标。");
126078
+ return;
126079
+ }
125976
126080
  try {
125977
126081
  await session.createGoal(parsed.objective, { replace: parsed.replace });
125978
126082
  host.showStatus(`🎯 目标已设置:${parsed.objective}`);
125979
- if (host.state.appState.streamingPhase === "idle") host.sendQueuedMessage(session, {
126083
+ if (!isBusy(host.state.appState)) host.sendQueuedMessage(session, {
125980
126084
  text: parsed.objective,
125981
126085
  agentId: void 0
125982
126086
  });
@@ -126020,7 +126124,7 @@ async function resumeGoal(host) {
126020
126124
  }
126021
126125
  await session.updateGoalStatus("active");
126022
126126
  host.showStatus("🎯 目标已恢复。");
126023
- if (host.state.appState.streamingPhase === "idle") host.sendQueuedMessage(session, {
126127
+ if (!isBusy(host.state.appState)) host.sendQueuedMessage(session, {
126024
126128
  text: "继续执行当前目标。",
126025
126129
  agentId: void 0
126026
126130
  });
@@ -126081,9 +126185,9 @@ function dismissGoalPanel(host) {
126081
126185
  }
126082
126186
  //#endregion
126083
126187
  //#region src/tui/components/chrome/welcome.ts
126084
- const HUE_STOPS = 24;
126085
- const SUB_STEPS = 5;
126086
- const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
126188
+ const HUE_STOPS$1 = 24;
126189
+ const SUB_STEPS$1 = 5;
126190
+ const BREATHE_STEPS$1 = HUE_STOPS$1 * SUB_STEPS$1;
126087
126191
  const BREATHE_INTERVAL_MS$1 = 40;
126088
126192
  const WELCOME_TIPS = [
126089
126193
  "/config 配置模型",
@@ -126107,7 +126211,7 @@ function hexToRgb$2(hex) {
126107
126211
  parseInt(hex.slice(5, 7), 16)
126108
126212
  ];
126109
126213
  }
126110
- function rgbToHsl$1(r, g, b) {
126214
+ function rgbToHsl$2(r, g, b) {
126111
126215
  const rf = r / 255, gf = g / 255, bf = b / 255;
126112
126216
  const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
126113
126217
  const l = (max + min) / 2;
@@ -126128,7 +126232,7 @@ function rgbToHsl$1(r, g, b) {
126128
126232
  l * 100
126129
126233
  ];
126130
126234
  }
126131
- function hslToRgb(h, s, l) {
126235
+ function hslToRgb$1(h, s, l) {
126132
126236
  const hf = (h % 360 + 360) % 360 / 360;
126133
126237
  const sf = s / 100, lf = l / 100;
126134
126238
  if (sf === 0) {
@@ -126159,13 +126263,13 @@ function rgbToHex(r, g, b) {
126159
126263
  const c = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
126160
126264
  return `#${c(r)}${c(g)}${c(b)}`;
126161
126265
  }
126162
- function buildBreathingPalette(primaryHex, hueStops, subSteps) {
126266
+ function buildBreathingPalette$1(primaryHex, hueStops, subSteps) {
126163
126267
  const [r, g, b] = hexToRgb$2(primaryHex);
126164
- const [baseHue] = rgbToHsl$1(r, g, b);
126268
+ const [baseHue] = rgbToHsl$2(r, g, b);
126165
126269
  const steps = hueStops * subSteps;
126166
126270
  const palette = [];
126167
126271
  for (let i = 0; i < steps; i++) {
126168
- const [rr, gg, bb] = hslToRgb((baseHue + i / steps * 360) % 360, 90, 70);
126272
+ const [rr, gg, bb] = hslToRgb$1((baseHue + i / steps * 360) % 360, 90, 70);
126169
126273
  palette.push(rgbToHex(rr, gg, bb));
126170
126274
  }
126171
126275
  return palette;
@@ -126203,7 +126307,7 @@ var WelcomeComponent = class {
126203
126307
  this.colors = colors;
126204
126308
  this.ui = ui;
126205
126309
  this.recentSessions = recentSessions;
126206
- this.breathePalette = buildBreathingPalette(colors.primary, HUE_STOPS, SUB_STEPS);
126310
+ this.breathePalette = buildBreathingPalette$1(colors.primary, HUE_STOPS$1, SUB_STEPS$1);
126207
126311
  this.startBreathing();
126208
126312
  }
126209
126313
  stopBreathing() {
@@ -126218,7 +126322,7 @@ var WelcomeComponent = class {
126218
126322
  }
126219
126323
  startBreathing() {
126220
126324
  this.breatheTimer = setInterval(() => {
126221
- this.breatheFrame = (this.breatheFrame + 1) % BREATHE_STEPS;
126325
+ this.breatheFrame = (this.breatheFrame + 1) % BREATHE_STEPS$1;
126222
126326
  this.ui.requestRender();
126223
126327
  }, BREATHE_INTERVAL_MS$1);
126224
126328
  }
@@ -129342,7 +129446,7 @@ function getTranscriptComponentEntry(component) {
129342
129446
  //#endregion
129343
129447
  //#region src/tui/commands/revoke.ts
129344
129448
  async function handleRevokeCommand(host, args = "") {
129345
- if (host.state.appState.streamingPhase !== "idle") {
129449
+ if (isBusy(host.state.appState)) {
129346
129450
  host.showError("无法在 streaming 中撤回 — 请先按 Esc 或 Ctrl-C 取消。");
129347
129451
  return;
129348
129452
  }
@@ -130083,7 +130187,7 @@ async function writeUpdateCache(value, filePath = getUpdateStateFile()) {
130083
130187
  }
130084
130188
  //#endregion
130085
130189
  //#region src/cli/update/cdn.ts
130086
- const NPM_TIMEOUT_MS = 15e3;
130190
+ const NPM_TIMEOUT_MS = 3e3;
130087
130191
  /**
130088
130192
  * Resolve the npm executable name for the current platform.
130089
130193
  *
@@ -130245,7 +130349,7 @@ async function runInstallStep(cmd, args, cwd, label, timeoutMs = INSTALL_TIMEOUT
130245
130349
  });
130246
130350
  }
130247
130351
  async function handleUpdateCommand(host) {
130248
- if (host.state.appState.streamingPhase !== "idle") {
130352
+ if (isBusy(host.state.appState)) {
130249
130353
  host.showError("请在空闲时执行更新。");
130250
130354
  return;
130251
130355
  }
@@ -130941,7 +131045,7 @@ async function handleMakeSkillCommand(host, args) {
130941
131045
  host.showError(NO_ACTIVE_SESSION_MESSAGE);
130942
131046
  return;
130943
131047
  }
130944
- if (host.state.appState.streamingPhase !== "idle") {
131048
+ if (isBusy(host.state.appState)) {
130945
131049
  host.showError("请等待当前回复完成后再使用 /make-skill");
130946
131050
  return;
130947
131051
  }
@@ -131966,6 +132070,10 @@ async function handleLoopCommand(host, args) {
131966
132070
  host.showError(parsed);
131967
132071
  return;
131968
132072
  }
132073
+ if (detectGoalLoopConflict(host.state.appState, "enable_loop") === "goal_active") {
132074
+ host.showNotice("Storm Breaker(风暴守护者)", "当前已有激活的目标(/goal)。/loop 与 /goal 语义冲突:loop 每轮重置上下文,会破坏 goal 的工作笔记迭代。请先 /goal off 关闭目标,再开启循环模式。");
132075
+ return;
132076
+ }
131969
132077
  const loopLimit = createLoopLimitRuntime(parsed.limit);
131970
132078
  host.setAppState({
131971
132079
  loopModeEnabled: true,
@@ -131973,7 +132081,8 @@ async function handleLoopCommand(host, args) {
131973
132081
  loopLimit,
131974
132082
  loopVerifier: parsed.verifier ? makeVerifier(parsed.verifier.command) : void 0,
131975
132083
  loopIteration: 0,
131976
- loopLastVerifyPassed: void 0
132084
+ loopLastVerifyPassed: void 0,
132085
+ loopVerifying: false
131977
132086
  });
131978
132087
  await ensureAutoPermission(host);
131979
132088
  const limitSuffix = parsed.limit ? ` 限制:${describeLoopLimit(parsed.limit)}。` : "";
@@ -131998,7 +132107,8 @@ function disableLoopMode(host, message) {
131998
132107
  loopLimit: void 0,
131999
132108
  loopVerifier: void 0,
132000
132109
  loopIteration: 0,
132001
- loopLastVerifyPassed: void 0
132110
+ loopLastVerifyPassed: void 0,
132111
+ loopVerifying: false
132002
132112
  });
132003
132113
  if (message) host.showStatus(message);
132004
132114
  }
@@ -133024,7 +133134,7 @@ var EditorKeyboardController = class {
133024
133134
  this.cancelCurrentCompaction();
133025
133135
  return;
133026
133136
  }
133027
- if (host.state.appState.streamingPhase !== "idle") {
133137
+ if (isStreaming(host.state.appState)) {
133028
133138
  this.clearPendingExit();
133029
133139
  this.cancelCurrentStream();
133030
133140
  return;
@@ -133055,7 +133165,7 @@ var EditorKeyboardController = class {
133055
133165
  this.cancelCurrentCompaction();
133056
133166
  return;
133057
133167
  }
133058
- if (host.state.appState.streamingPhase !== "idle") {
133168
+ if (isStreaming(host.state.appState)) {
133059
133169
  this.cancelCurrentStream();
133060
133170
  return;
133061
133171
  }
@@ -133081,7 +133191,7 @@ var EditorKeyboardController = class {
133081
133191
  };
133082
133192
  editor.onTogglePlanExpand = () => host.togglePlanExpansion();
133083
133193
  editor.onCtrlS = () => {
133084
- if (host.state.appState.streamingPhase === "idle" || host.state.appState.isCompacting) return;
133194
+ if (!isBusy(host.state.appState)) return;
133085
133195
  const text = editor.getText().trim();
133086
133196
  const queuedTexts = host.state.queuedMessages.map((m) => m.text);
133087
133197
  host.clearQueuedMessages();
@@ -133104,7 +133214,7 @@ var EditorKeyboardController = class {
133104
133214
  host.cancelPendingMemoryExtraction();
133105
133215
  };
133106
133216
  editor.onUpArrowEmpty = () => {
133107
- if (host.state.appState.streamingPhase === "idle" && !host.state.appState.isCompacting) return false;
133217
+ if (!isBusy(host.state.appState)) return false;
133108
133218
  const recalled = host.recallLastQueued();
133109
133219
  if (recalled !== void 0) {
133110
133220
  editor.setText(recalled);
@@ -133520,6 +133630,53 @@ function nextTranscriptId() {
133520
133630
  return `entry-${String(transcriptIdCounter)}`;
133521
133631
  }
133522
133632
  //#endregion
133633
+ //#region src/tui/streaming-phase.ts
133634
+ /**
133635
+ * Returns true when transitioning `from → to` is a valid directed edge.
133636
+ * Self-loops are no-ops and return false so callers can use this as an
133637
+ * idempotency guard:
133638
+ * if (canTransitionTo(state.appState.streamingPhase, 'thinking')) {
133639
+ * setAppState({ streamingPhase: 'thinking' });
133640
+ * }
133641
+ */
133642
+ function canTransitionTo(from, to) {
133643
+ return from !== to;
133644
+ }
133645
+ //#endregion
133646
+ //#region src/tui/utils/compaction-anomaly.ts
133647
+ /** Rapid-refire window: < 30s between end of previous compaction and start of next. */
133648
+ const RAPID_REFIRE_MS = 3e4;
133649
+ /** First-step-blowup: ratio of current tokens to window when first auto-compaction fires. */
133650
+ const FIRST_STEP_BLOWUP_RATIO = .7;
133651
+ /**
133652
+ * Inspects one auto-compaction start for pathological patterns. Returns `null`
133653
+ * when the compaction looks routine.
133654
+ *
133655
+ * Two signals:
133656
+ * - `rapid_refire`: previous auto-compaction finished < 30s ago — model is likely
133657
+ * emitting a runaway stream or looping on a tool that explodes context.
133658
+ * - `first_step_blowup`: very first auto-compaction of the session, and context
133659
+ * is already above 70% — likely a giant system prompt, a huge file read, or
133660
+ * similar one-shot inflation.
133661
+ */
133662
+ function detectCompactionAnomaly(input) {
133663
+ if (input.lastFinishedAt !== void 0) {
133664
+ const elapsed = input.now - input.lastFinishedAt;
133665
+ if (elapsed >= 0 && elapsed < RAPID_REFIRE_MS) return {
133666
+ kind: "rapid_refire",
133667
+ detail: `上次压缩结束仅 ${(elapsed / 1e3).toFixed(1)} 秒后再次触发自动压缩。`
133668
+ };
133669
+ }
133670
+ if (input.autoCompactionCount === 0 && input.maxContextTokens > 0) {
133671
+ const ratio = input.currentTokens / input.maxContextTokens;
133672
+ if (ratio >= FIRST_STEP_BLOWUP_RATIO) return {
133673
+ kind: "first_step_blowup",
133674
+ detail: `会话首次自动压缩时上下文已达 ${(ratio * 100).toFixed(0)}%,可能存在巨型文件读取或初始 prompt 过大。`
133675
+ };
133676
+ }
133677
+ return null;
133678
+ }
133679
+ //#endregion
133523
133680
  //#region src/tui/controllers/session-event-handler.ts
133524
133681
  var SessionEventHandler = class {
133525
133682
  host;
@@ -133528,6 +133685,17 @@ var SessionEventHandler = class {
133528
133685
  }
133529
133686
  backgroundAgentMetadata = /* @__PURE__ */ new Map();
133530
133687
  backgroundTasks = /* @__PURE__ */ new Map();
133688
+ /**
133689
+ * Compaction trigger of the in-flight compaction, captured in
133690
+ * `handleCompactionBegin` (CompactionStartedEvent carries `trigger`) and
133691
+ * read in `handleCompactionEnd` (CompactionCompletedEvent does not). Used so
133692
+ * Storm Breaker cadence tracking only counts auto-compactions — manual
133693
+ * /compact must not inflate `autoCompactionCount` or set
133694
+ * `lastCompactionFinishedAt`, otherwise the first genuine auto-compaction
133695
+ * would silently fail to trigger `first_step_blowup`, and a manual compact
133696
+ * followed shortly by an auto one would falsely trip `rapid_refire`.
133697
+ */
133698
+ lastCompactionTrigger;
133531
133699
  backgroundTaskTranscriptedTerminal = /* @__PURE__ */ new Set();
133532
133700
  subagentInfo = /* @__PURE__ */ new Map();
133533
133701
  renderedSkillActivationIds = /* @__PURE__ */ new Set();
@@ -133777,14 +133945,10 @@ var SessionEventHandler = class {
133777
133945
  this.host.streamingUI.resetToolUi();
133778
133946
  this.host.streamingUI.setStep(0);
133779
133947
  this.host.patchLivePane({
133780
- mode: "waiting",
133781
133948
  pendingApproval: null,
133782
133949
  pendingQuestion: null
133783
133950
  });
133784
- this.host.setAppState({
133785
- streamingPhase: "waiting",
133786
- streamingStartTime: Date.now()
133787
- });
133951
+ this.host.setAppState({ streamingPhase: "waiting" });
133788
133952
  }
133789
133953
  handleTurnEnd(event, sendQueued) {
133790
133954
  this.host.streamingUI.flushNow();
@@ -133813,7 +133977,8 @@ var SessionEventHandler = class {
133813
133977
  loopLimit: void 0,
133814
133978
  loopVerifier: void 0,
133815
133979
  loopIteration: 0,
133816
- loopLastVerifyPassed: void 0
133980
+ loopLastVerifyPassed: void 0,
133981
+ loopVerifying: false
133817
133982
  });
133818
133983
  this.host.showStatus(message);
133819
133984
  }
@@ -133824,14 +133989,22 @@ var SessionEventHandler = class {
133824
133989
  this.host.setAppState({ loopIteration: currentIteration });
133825
133990
  const verifier = state.loopVerifier;
133826
133991
  if (verifier) {
133992
+ this.host.setAppState({ loopVerifying: true });
133827
133993
  const result = await runShellVerifier(verifier, state.workDir);
133828
133994
  const after = this.host.state.appState;
133829
- if (!after.loopModeEnabled || after.loopPrompt !== loopPrompt) return;
133995
+ if (!after.loopModeEnabled || after.loopPrompt !== loopPrompt) {
133996
+ this.host.setAppState({ loopVerifying: false });
133997
+ return;
133998
+ }
133830
133999
  if (result.passed) {
134000
+ this.host.setAppState({ loopVerifying: false });
133831
134001
  this.disableLoop(`✓ 验证通过,循环结束(${currentIteration} 次迭代)。`);
133832
134002
  return;
133833
134003
  }
133834
- this.host.setAppState({ loopLastVerifyPassed: false });
134004
+ this.host.setAppState({
134005
+ loopVerifying: false,
134006
+ loopLastVerifyPassed: false
134007
+ });
133835
134008
  }
133836
134009
  if (!consumeLoopLimitIteration(this.host.state.appState.loopLimit)) {
133837
134010
  const reason = this.host.state.appState.loopLimit?.kind === "duration" ? "时间" : "次数";
@@ -133845,16 +134018,12 @@ var SessionEventHandler = class {
133845
134018
  this.host.streamingUI.flushNow();
133846
134019
  this.host.streamingUI.setStep(event.step);
133847
134020
  this.host.streamingUI.resetToolUi();
133848
- this.host.streamingUI.finalizeLiveTextBuffers("waiting");
134021
+ this.host.streamingUI.finalizeLiveTextBuffers();
133849
134022
  this.host.patchLivePane({
133850
- mode: "waiting",
133851
134023
  pendingApproval: null,
133852
134024
  pendingQuestion: null
133853
134025
  });
133854
- this.host.setAppState({
133855
- streamingPhase: "waiting",
133856
- streamingStartTime: Date.now()
133857
- });
134026
+ this.host.setAppState({ streamingPhase: "waiting" });
133858
134027
  }
133859
134028
  handleStepCompleted(event) {
133860
134029
  this.host.streamingUI.flushNow();
@@ -133878,7 +134047,7 @@ var SessionEventHandler = class {
133878
134047
  handleStepInterrupted(event) {
133879
134048
  this.host.streamingUI.flushNow();
133880
134049
  this.host.streamingUI.resetToolUi();
133881
- this.host.streamingUI.finalizeLiveTextBuffers("idle");
134050
+ this.host.streamingUI.finalizeLiveTextBuffers();
133882
134051
  const reason = event.reason;
133883
134052
  if (reason === "error") return;
133884
134053
  if (reason === "aborted" || reason === void 0 || reason === "") {
@@ -133890,31 +134059,23 @@ var SessionEventHandler = class {
133890
134059
  handleThinkingDelta(event) {
133891
134060
  const { state, streamingUI } = this.host;
133892
134061
  streamingUI.appendThinkingDelta(event.delta);
133893
- this.host.patchLivePane({ mode: "idle" });
133894
- if (state.appState.streamingPhase !== "thinking") this.host.setAppState({
133895
- streamingPhase: "thinking",
133896
- streamingStartTime: Date.now()
133897
- });
134062
+ if (canTransitionTo(state.appState.streamingPhase, "thinking")) this.host.setAppState({ streamingPhase: "thinking" });
133898
134063
  streamingUI.scheduleFlush();
133899
134064
  }
133900
134065
  handleAssistantDelta(event) {
133901
134066
  const { state, streamingUI } = this.host;
133902
- if (streamingUI.hasThinkingDraft()) streamingUI.flushThinkingToTranscript("idle");
134067
+ if (streamingUI.hasThinkingDraft()) streamingUI.flushThinkingToTranscript();
133903
134068
  streamingUI.appendAssistantDelta(event.delta);
133904
134069
  this.host.patchLivePane({
133905
- mode: "idle",
133906
134070
  pendingApproval: null,
133907
134071
  pendingQuestion: null
133908
134072
  });
133909
- if (state.appState.streamingPhase !== "composing") this.host.setAppState({
133910
- streamingPhase: "composing",
133911
- streamingStartTime: Date.now()
133912
- });
134073
+ if (canTransitionTo(state.appState.streamingPhase, "composing")) this.host.setAppState({ streamingPhase: "composing" });
133913
134074
  streamingUI.scheduleFlush();
133914
134075
  }
133915
134076
  handleHookResult(event) {
133916
134077
  this.host.streamingUI.flushNow();
133917
- if (this.host.streamingUI.hasThinkingDraft()) this.host.streamingUI.flushThinkingToTranscript("idle");
134078
+ if (this.host.streamingUI.hasThinkingDraft()) this.host.streamingUI.flushThinkingToTranscript();
133918
134079
  this.host.streamingUI.finalizeAssistantStream();
133919
134080
  this.host.appendTranscriptEntry({
133920
134081
  id: nextTranscriptId(),
@@ -133924,7 +134085,6 @@ var SessionEventHandler = class {
133924
134085
  content: formatHookResultMarkdown(event)
133925
134086
  });
133926
134087
  this.host.patchLivePane({
133927
- mode: "idle",
133928
134088
  pendingApproval: null,
133929
134089
  pendingQuestion: null
133930
134090
  });
@@ -133944,24 +134104,20 @@ var SessionEventHandler = class {
133944
134104
  };
133945
134105
  streamingUI.registerToolCall(toolCall);
133946
134106
  this.host.patchLivePane({
133947
- mode: "tool",
133948
134107
  pendingApproval: null,
133949
134108
  pendingQuestion: null
133950
134109
  });
134110
+ if (canTransitionTo(this.host.state.appState.streamingPhase, "tool")) this.host.setAppState({ streamingPhase: "tool" });
133951
134111
  }
133952
134112
  handleToolCallDelta(event) {
133953
134113
  if (event.toolCallId.length === 0) return;
133954
134114
  const { state, streamingUI } = this.host;
133955
134115
  streamingUI.accumulateToolCallDelta(event.toolCallId, event.name, event.argumentsPart);
133956
134116
  this.host.patchLivePane({
133957
- mode: "tool",
133958
134117
  pendingApproval: null,
133959
134118
  pendingQuestion: null
133960
134119
  });
133961
- if (state.appState.streamingPhase !== "composing") this.host.setAppState({
133962
- streamingPhase: "composing",
133963
- streamingStartTime: Date.now()
133964
- });
134120
+ if (canTransitionTo(state.appState.streamingPhase, "tool")) this.host.setAppState({ streamingPhase: "tool" });
133965
134121
  streamingUI.scheduleFlush();
133966
134122
  }
133967
134123
  handleToolProgress(event) {
@@ -133993,7 +134149,7 @@ var SessionEventHandler = class {
133993
134149
  streamingUI.setTodoList(sanitized);
133994
134150
  }
133995
134151
  }
133996
- this.host.patchLivePane({ mode: "waiting" });
134152
+ if (canTransitionTo(this.host.state.appState.streamingPhase, "waiting")) this.host.setAppState({ streamingPhase: "waiting" });
133997
134153
  }
133998
134154
  handleStatusUpdate(event) {
133999
134155
  const patch = {};
@@ -134016,7 +134172,7 @@ var SessionEventHandler = class {
134016
134172
  handleSessionError(event) {
134017
134173
  this.host.streamingUI.flushNow();
134018
134174
  this.host.streamingUI.resetToolUi();
134019
- this.host.streamingUI.finalizeLiveTextBuffers("idle");
134175
+ this.host.streamingUI.finalizeLiveTextBuffers();
134020
134176
  this.host.showError(`[${event.code}] ${event.message}`);
134021
134177
  }
134022
134178
  handleSessionWarning(event) {
@@ -134130,22 +134286,38 @@ var SessionEventHandler = class {
134130
134286
  });
134131
134287
  }
134132
134288
  handleCompactionBegin(event) {
134133
- this.host.streamingUI.finalizeLiveTextBuffers("waiting");
134289
+ this.host.streamingUI.finalizeLiveTextBuffers();
134290
+ this.lastCompactionTrigger = event.trigger;
134291
+ if (event.trigger === "auto") {
134292
+ const anomaly = detectCompactionAnomaly({
134293
+ lastFinishedAt: this.host.state.appState.lastCompactionFinishedAt,
134294
+ autoCompactionCount: this.host.state.appState.autoCompactionCount,
134295
+ currentTokens: this.host.state.appState.contextTokens,
134296
+ maxContextTokens: this.host.state.appState.maxContextTokens,
134297
+ now: Date.now()
134298
+ });
134299
+ if (anomaly !== null) this.host.showNotice("Storm Breaker(风暴守护者)", `检测到异常压缩节奏:${anomaly.detail}可能存在工具调用循环或超长输出,建议查看最近几步的对话记录。`);
134300
+ }
134134
134301
  this.host.setAppState({
134135
134302
  isCompacting: true,
134136
- streamingPhase: "waiting",
134137
- streamingStartTime: Date.now()
134303
+ streamingPhase: "waiting"
134138
134304
  });
134139
134305
  this.host.streamingUI.beginCompaction(event.instruction);
134140
134306
  }
134141
134307
  handleCompactionEnd(event, sendQueued) {
134142
134308
  this.host.streamingUI.endCompaction(event.result.tokensBefore, event.result.tokensAfter);
134143
134309
  this.host.markMemoryExtracted();
134310
+ if (this.lastCompactionTrigger === "auto") this.host.setAppState({
134311
+ lastCompactionFinishedAt: Date.now(),
134312
+ autoCompactionCount: this.host.state.appState.autoCompactionCount + 1
134313
+ });
134314
+ this.lastCompactionTrigger = void 0;
134144
134315
  this.finishCompaction(sendQueued);
134145
134316
  }
134146
134317
  handleCompactionCancel(event, sendQueued) {
134147
134318
  if (event.reason) this.host.showStatus(event.reason, this.host.state.theme.colors.warning);
134148
134319
  this.host.streamingUI.cancelCompaction();
134320
+ this.lastCompactionTrigger = void 0;
134149
134321
  this.finishCompaction(sendQueued);
134150
134322
  }
134151
134323
  finishCompaction(sendQueued) {
@@ -135297,7 +135469,7 @@ var StreamingUIController = class {
135297
135469
  const existingComponent = this._pendingToolComponents.get(toolCall.id);
135298
135470
  if (existingComponent !== void 0) existingComponent.updateToolCall(toolCall);
135299
135471
  else if (existing === void 0) {
135300
- this.finalizeLiveTextBuffers("tool");
135472
+ this.finalizeLiveTextBuffers();
135301
135473
  if (toolCall.name !== "Agent") this.onToolCallStart(toolCall);
135302
135474
  }
135303
135475
  return existing === void 0;
@@ -135420,11 +135592,10 @@ var StreamingUIController = class {
135420
135592
  markThinkingDirty() {
135421
135593
  this.pendingThinkingFlush = true;
135422
135594
  }
135423
- flushThinkingToTranscript(nextMode = "idle") {
135595
+ flushThinkingToTranscript() {
135424
135596
  this.flushNow();
135425
135597
  this._thinkingDraft = "";
135426
135598
  this.onThinkingEnd();
135427
- this.host.patchLivePane({ mode: nextMode });
135428
135599
  }
135429
135600
  finalizeAssistantStream() {
135430
135601
  this.flushNow();
@@ -135455,8 +135626,8 @@ var StreamingUIController = class {
135455
135626
  resetToolCallState() {
135456
135627
  this._activeToolCalls.clear();
135457
135628
  }
135458
- finalizeLiveTextBuffers(nextMode = "idle") {
135459
- this.flushThinkingToTranscript(nextMode);
135629
+ finalizeLiveTextBuffers() {
135630
+ this.flushThinkingToTranscript();
135460
135631
  this.finalizeAssistantStream();
135461
135632
  }
135462
135633
  finalizeTurn(sendQueued) {
@@ -135464,7 +135635,7 @@ var StreamingUIController = class {
135464
135635
  if (state.appState.streamingPhase === "idle") return;
135465
135636
  this.host.deferUserMessages = false;
135466
135637
  const completedTurnKey = this._currentTurnId ?? `local:${String(state.appState.streamingStartTime)}`;
135467
- this.finalizeLiveTextBuffers("idle");
135638
+ this.finalizeLiveTextBuffers();
135468
135639
  this.resetToolCallState();
135469
135640
  this._currentTurnId = void 0;
135470
135641
  const next = this.host.shiftQueuedMessage();
@@ -135729,7 +135900,7 @@ var StreamingUIController = class {
135729
135900
  turnId: this._currentTurnId
135730
135901
  };
135731
135902
  this._activeToolCalls.set(id, toolCall);
135732
- if (this._thinkingDraft.length > 0 || this._streamingBlock !== null) this.finalizeLiveTextBuffers("tool");
135903
+ if (this._thinkingDraft.length > 0 || this._streamingBlock !== null) this.finalizeLiveTextBuffers();
135733
135904
  const existingComponent = this._pendingToolComponents.get(id);
135734
135905
  if (existingComponent !== void 0) existingComponent.updateToolCall(toolCall);
135735
135906
  else if (toolCall.name !== "Agent") this.onToolCallStart(toolCall);
@@ -137142,7 +137313,7 @@ var TranscriptController = class TranscriptController {
137142
137313
  commit() {
137143
137314
  this.host.batchUpdate(() => {
137144
137315
  const { state } = this.host;
137145
- if (state.appState.streamingPhase !== "idle") return;
137316
+ if (isStreaming(state.appState)) return;
137146
137317
  const container = state.transcriptContainer;
137147
137318
  const children = container.children;
137148
137319
  if (children.length <= TranscriptController.LIVE_LIMIT) return;
@@ -137824,7 +137995,7 @@ var LifecycleController = class LifecycleController {
137824
137995
  async performIdleMemoryExtraction() {
137825
137996
  if (Date.now() - this.lastMemoryExtractionTime < LifecycleController.MEMORY_EXTRACT_COOLDOWN_MS) return;
137826
137997
  const { state, session } = this.host;
137827
- if (state.appState.streamingPhase !== "idle") return;
137998
+ if (isStreaming(state.appState)) return;
137828
137999
  if (state.appState.isCompacting) return;
137829
138000
  if (state.appState.isReplaying) return;
137830
138001
  if (session === void 0) return;
@@ -137938,7 +138109,6 @@ var LifecycleController = class LifecycleController {
137938
138109
  break;
137939
138110
  }
137940
138111
  case "idle":
137941
- case "session":
137942
138112
  this.stopActivitySpinner();
137943
138113
  this.stopPulseWave();
137944
138114
  break;
@@ -137952,10 +138122,8 @@ var LifecycleController = class LifecycleController {
137952
138122
  if (state.appState.isCompacting) return "hidden";
137953
138123
  if (state.livePane.pendingQuestion !== null) return "hidden";
137954
138124
  const streamingPhase = state.appState.streamingPhase;
137955
- if (state.livePane.mode === "idle") {
137956
- if (streamingPhase === "thinking" || streamingPhase === "composing") return streamingPhase;
137957
- }
137958
- return state.livePane.mode;
138125
+ if (streamingPhase === "waiting" || streamingPhase === "thinking" || streamingPhase === "composing" || streamingPhase === "tool") return streamingPhase;
138126
+ return "idle";
137959
138127
  }
137960
138128
  shouldShowTerminalProgress(effectiveMode) {
137961
138129
  if (this.host.state.appState.isCompacting) return true;
@@ -138287,6 +138455,35 @@ function trySourceOrDist() {
138287
138455
  };
138288
138456
  }
138289
138457
  const SCREAM_FUSIONPLAN_SUBAGENT_ENV = "SCREAM_FUSIONPLAN_SUBAGENT";
138458
+ /**
138459
+ * Terminate a child process tree reliably on both POSIX and Windows.
138460
+ * On Windows `proc.kill()` only signals the wrapper, so use `taskkill /T`.
138461
+ * On POSIX, kill the process group so grandchildren inherit the signal.
138462
+ */
138463
+ function killProcessTree(proc, signal) {
138464
+ if (proc.pid === void 0) return;
138465
+ if (process.platform === "win32") {
138466
+ const child = spawn("taskkill", [
138467
+ ...signal === "SIGKILL" ? ["/F"] : [],
138468
+ "/T",
138469
+ "/PID",
138470
+ String(proc.pid)
138471
+ ], {
138472
+ stdio: "ignore",
138473
+ windowsHide: true,
138474
+ detached: true
138475
+ });
138476
+ if (typeof child.unref === "function") child.unref();
138477
+ return;
138478
+ }
138479
+ try {
138480
+ process.kill(-proc.pid, signal);
138481
+ } catch {
138482
+ try {
138483
+ proc.kill(signal);
138484
+ } catch {}
138485
+ }
138486
+ }
138290
138487
  const WORKER_ANGLES = [
138291
138488
  {
138292
138489
  angle: "Focus on correctness and edge cases. Identify risks, invariants, and safety checks.",
@@ -138302,9 +138499,17 @@ const WORKER_ANGLES = [
138302
138499
  }
138303
138500
  ];
138304
138501
  const DEFAULT_WORKER_COUNT = 3;
138305
- const DEFAULT_TIMEOUT_MS = 12e4;
138502
+ const DEFAULT_TIMEOUT_MS = 6e5;
138306
138503
  const DEFAULT_MAX_OUTPUT_BYTES = 8e3;
138307
138504
  const DEFAULT_SYNTHESIS_MAX_OUTPUT_BYTES = 12e3;
138505
+ /** Override via `SCREAM_FUSIONPLAN_TIMEOUT_SECONDS` env var (30..3600). */
138506
+ function resolveDefaultTimeoutMs() {
138507
+ const env = process.env["SCREAM_FUSIONPLAN_TIMEOUT_SECONDS"];
138508
+ if (env === void 0) return DEFAULT_TIMEOUT_MS;
138509
+ const parsed = Number.parseInt(env, 10);
138510
+ if (Number.isNaN(parsed)) return DEFAULT_TIMEOUT_MS;
138511
+ return Math.max(3e4, Math.min(36e5, parsed * 1e3));
138512
+ }
138308
138513
  function buildPlannerPrompt(input) {
138309
138514
  return [
138310
138515
  "You are a planning specialist. Create an implementation plan for the request below.",
@@ -138426,7 +138631,7 @@ async function runWorker(input) {
138426
138631
  model: input.model
138427
138632
  });
138428
138633
  const { command, prefixArgs } = resolveScreamCommand(input.screamBin);
138429
- const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
138634
+ const timeoutMs = input.timeoutMs ?? resolveDefaultTimeoutMs();
138430
138635
  result.command = [
138431
138636
  command,
138432
138637
  ...prefixArgs,
@@ -138488,12 +138693,13 @@ async function runWorker(input) {
138488
138693
  });
138489
138694
  timeout = setTimeout(() => {
138490
138695
  result.timedOut = true;
138491
- proc.kill("SIGTERM");
138492
- setTimeout(() => proc.kill("SIGKILL"), 5e3).unref();
138696
+ killProcessTree(proc, "SIGTERM");
138697
+ setTimeout(() => killProcessTree(proc, "SIGKILL"), 5e3).unref();
138493
138698
  }, timeoutMs);
138494
138699
  timeout.unref();
138495
138700
  });
138496
138701
  result.exitCode = exitCode;
138702
+ result.timeoutMs = timeoutMs;
138497
138703
  result.ok = exitCode === 0 && !result.timedOut && result.output.trim().length > 0;
138498
138704
  if (!result.output.trim()) result.output = result.stderr.trim() || "(worker produced no final assistant output)";
138499
138705
  return result;
@@ -138539,7 +138745,7 @@ async function runFusionPlan(input) {
138539
138745
  };
138540
138746
  const workerCount = Math.max(1, Math.min(8, input.workerCount ?? DEFAULT_WORKER_COUNT));
138541
138747
  const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
138542
- const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
138748
+ const timeoutMs = input.timeoutMs ?? resolveDefaultTimeoutMs();
138543
138749
  const promptDir = await createPromptDir();
138544
138750
  const workerStates = [];
138545
138751
  for (let i = 0; i < workerCount; i += 1) {
@@ -138725,7 +138931,7 @@ function hexToRgb$1(hex) {
138725
138931
  v & 255
138726
138932
  ];
138727
138933
  }
138728
- function rgbToHsl(r, g, b) {
138934
+ function rgbToHsl$1(r, g, b) {
138729
138935
  const rf = r / 255, gf = g / 255, bf = b / 255;
138730
138936
  const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
138731
138937
  const l = (max + min) / 2;
@@ -138853,6 +139059,8 @@ var InputController = class {
138853
139059
  cwd: this.host.state.appState.workDir,
138854
139060
  model: this.host.state.appState.model,
138855
139061
  thinkingLevel: this.host.state.appState.thinkingLevel === "off" ? void 0 : this.host.state.appState.thinkingLevel,
139062
+ workerCount: this.host.state.appState.fusionPlan.workerCount,
139063
+ timeoutMs: this.host.state.appState.fusionPlan.timeoutSeconds * 1e3,
138856
139064
  onProgress: (event) => {
138857
139065
  if (this.fusionPlanEntry === void 0) {
138858
139066
  this.fusionPlanEntry = {
@@ -138873,7 +139081,10 @@ var InputController = class {
138873
139081
  if (!result.ok) {
138874
139082
  const details = result.workerResults.map((r, i) => {
138875
139083
  if (r.ok) return `worker ${i + 1}: ok`;
138876
- if (r.timedOut) return `worker ${i + 1}: timed out`;
139084
+ if (r.timedOut) {
139085
+ const timeoutS = r.timeoutMs !== void 0 ? Math.round(r.timeoutMs / 1e3) : 600;
139086
+ return `worker ${i + 1}: timed out after ${timeoutS}s`;
139087
+ }
138877
139088
  const reason = r.exitCode !== null ? `exit ${r.exitCode}` : r.stderr.trim() || "no output";
138878
139089
  const commandHint = r.command ? ` [${r.command}]` : "";
138879
139090
  return `worker ${i + 1}: failed (${reason})${commandHint}`;
@@ -138939,7 +139150,7 @@ var InputController = class {
138939
139150
  for (const part of input) this.enqueueMessage(part);
138940
139151
  return;
138941
139152
  }
138942
- if (this.host.state.appState.streamingPhase === "idle") {
139153
+ if (!isStreaming(this.host.state.appState)) {
138943
139154
  for (const part of input) this.sendMessageInternal(session, part);
138944
139155
  return;
138945
139156
  }
@@ -138989,7 +139200,7 @@ var InputController = class {
138989
139200
  if (this.breatheOnceStopped) return;
138990
139201
  const primaryHex = this.host.state.theme.colors.primary;
138991
139202
  const [r, g, b] = hexToRgb$1(primaryHex);
138992
- const [baseHue] = rgbToHsl(r, g, b);
139203
+ const [baseHue] = rgbToHsl$1(r, g, b);
138993
139204
  this.breatheFrame = 0;
138994
139205
  const editor = this.host.state.editor;
138995
139206
  const ui = this.host.state.ui;
@@ -139068,7 +139279,7 @@ var InputController = class {
139068
139279
  });
139069
139280
  }
139070
139281
  sendMessage(session, input, options) {
139071
- if (this.host.deferUserMessages || this.host.state.appState.streamingPhase !== "idle" || this.host.state.appState.isCompacting) {
139282
+ if (this.host.deferUserMessages || isBusy(this.host.state.appState)) {
139072
139283
  this.enqueueMessage(input, options);
139073
139284
  return;
139074
139285
  }
@@ -139289,7 +139500,6 @@ var QuestionController = class extends ReverseRpcController {
139289
139500
  //#endregion
139290
139501
  //#region src/tui/types.ts
139291
139502
  const INITIAL_LIVE_PANE = {
139292
- mode: "idle",
139293
139503
  pendingApproval: null,
139294
139504
  pendingQuestion: null
139295
139505
  };
@@ -139372,6 +139582,14 @@ var ErrorBannerComponent = class {
139372
139582
  }
139373
139583
  };
139374
139584
  //#endregion
139585
+ //#region src/tui/loop-state.ts
139586
+ function resolveLoopSubstate(state) {
139587
+ if (!state.loopModeEnabled) return "idle";
139588
+ if (state.loopVerifying) return "verifying";
139589
+ if (state.loopPrompt === void 0) return "paused";
139590
+ return "running";
139591
+ }
139592
+ //#endregion
139375
139593
  //#region src/tui/utils/shimmer.ts
139376
139594
  const SHIMMER_SPEED_CELLS_PER_S = 30;
139377
139595
  const PADDING = 10;
@@ -139924,10 +140142,10 @@ function lerpGradient(t) {
139924
140142
  const b = Math.round(b0 + (b1 - b0) * localT);
139925
140143
  return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
139926
140144
  }
139927
- function buildStatusLine(streamingPhase, livePaneMode, streamingStartTime) {
139928
- if (streamingPhase === "idle" && livePaneMode !== "tool") return "○ 空闲";
140145
+ function buildStatusLine(streamingPhase, streamingStartTime) {
140146
+ if (streamingPhase === "idle") return "○ 空闲";
139929
140147
  let label;
139930
- if (livePaneMode === "tool") label = "执行中";
140148
+ if (streamingPhase === "tool") label = "执行中";
139931
140149
  else if (streamingPhase === "waiting") label = "等待响应";
139932
140150
  else if (streamingPhase === "thinking") label = "思考中";
139933
140151
  else if (streamingPhase === "composing") label = "输出中";
@@ -140042,6 +140260,7 @@ var FooterComponent = class {
140042
140260
  const remainMs = Math.max(0, limit.deadlineMs - Date.now());
140043
140261
  badge = remainMs >= 6e4 ? `loop ${Math.ceil(remainMs / 6e4)}m` : `loop ${Math.max(1, Math.ceil(remainMs / 1e3))}s`;
140044
140262
  }
140263
+ if (resolveLoopSubstate(state) === "verifying") badge += " · 验证中";
140045
140264
  if (state.loopLastVerifyPassed === false) badge += " · ✗";
140046
140265
  left.push(chalk.hex(colors.primary).bold(badge));
140047
140266
  }
@@ -140066,7 +140285,7 @@ var FooterComponent = class {
140066
140285
  let rightText;
140067
140286
  if (this.transientHint) rightText = chalk.hex(colors.warning).bold(this.transientHint);
140068
140287
  else {
140069
- const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
140288
+ const statusLine = buildStatusLine(state.streamingPhase, state.streamingStartTime);
140070
140289
  const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
140071
140290
  const contextColor = pickContextColor(state.contextUsage, colors);
140072
140291
  rightText = `${ccDot} ${chalk.hex(contextColor)(formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens))}${chalk.hex(colors.textDim)(` ${statusLine}`)}`;
@@ -141315,7 +141534,7 @@ var SessionManager = class {
141315
141534
  this.host.showStatus("已在该会话中。");
141316
141535
  return { switched: true };
141317
141536
  }
141318
- if (this.host.state.appState.streamingPhase !== "idle") {
141537
+ if (isBusy(this.host.state.appState)) {
141319
141538
  this.host.showError("流式传输期间无法切换会话 — 请先按 Esc 或 Ctrl-C。");
141320
141539
  return { switched: false };
141321
141540
  }
@@ -143337,10 +143556,11 @@ function createInitialAppState(input) {
143337
143556
  contextTokens: 0,
143338
143557
  maxContextTokens: 0,
143339
143558
  isCompacting: false,
143559
+ lastCompactionFinishedAt: void 0,
143560
+ autoCompactionCount: 0,
143340
143561
  isReplaying: false,
143341
143562
  streamingPhase: "idle",
143342
143563
  streamingStartTime: 0,
143343
- livePaneMode: "idle",
143344
143564
  theme: input.tuiConfig.theme,
143345
143565
  version: input.version,
143346
143566
  hasNewVersion: false,
@@ -143348,6 +143568,7 @@ function createInitialAppState(input) {
143348
143568
  editorCommand: input.tuiConfig.editorCommand,
143349
143569
  notifications: input.tuiConfig.notifications,
143350
143570
  like: input.tuiConfig.like,
143571
+ fusionPlan: input.tuiConfig.fusionPlan,
143351
143572
  availableModels: {},
143352
143573
  availableProviders: {},
143353
143574
  sessionTitle: null,
@@ -143362,6 +143583,7 @@ function createInitialAppState(input) {
143362
143583
  loopVerifier: void 0,
143363
143584
  loopIteration: 0,
143364
143585
  loopLastVerifyPassed: void 0,
143586
+ loopVerifying: false,
143365
143587
  recentSessions: []
143366
143588
  };
143367
143589
  }
@@ -143382,6 +143604,7 @@ var ScreamTUI = class {
143382
143604
  isShuttingDown = false;
143383
143605
  reverseRpcDisposers = [];
143384
143606
  startupNotice;
143607
+ updatePrefetched;
143385
143608
  sessionManager;
143386
143609
  dialogManager;
143387
143610
  streamingUI;
@@ -143419,6 +143642,7 @@ var ScreamTUI = class {
143419
143642
  };
143420
143643
  this.options = tuiOptions;
143421
143644
  this.startupNotice = startupInput.startupNotice;
143645
+ this.updatePrefetched = startupInput.updatePrefetched === true;
143422
143646
  this.state = createTUIState(tuiOptions);
143423
143647
  this.reverseRpcDisposers.push(...registerReverseRPCHandlers(this.approvalController, this.questionController, {
143424
143648
  showApprovalPanel: (payload) => {
@@ -143491,6 +143715,7 @@ var ScreamTUI = class {
143491
143715
  }
143492
143716
  async initMainTui() {
143493
143717
  const shouldReplayHistory = await this.init();
143718
+ await this.checkForUpdates();
143494
143719
  try {
143495
143720
  const sessions = await this.harness.listSessions({ workDir: this.state.appState.workDir });
143496
143721
  this.state.appState.recentSessions = sessions.slice(0, 3).map((s) => ({
@@ -143524,11 +143749,7 @@ var ScreamTUI = class {
143524
143749
  if (shouldReplayHistory) await this.sessionReplay.hydrateFromReplay(this.requireSession());
143525
143750
  const resumeState = this.session?.getResumeState();
143526
143751
  if (resumeState?.warning !== void 0) this.showStatus(`警告:${resumeState.warning}`, this.state.theme.colors.warning);
143527
- if (this.session !== void 0) this.sessionEventHandler.startSubscription();
143528
- this.fetchSessions();
143529
- if (this.session !== void 0) this.refreshSessionTitle();
143530
143752
  this.refreshSkillCommands(this.session);
143531
- this.checkForUpdates();
143532
143753
  }
143533
143754
  async showTmuxKeyboardWarningIfNeeded() {
143534
143755
  const warning = await detectTmuxKeyboardWarning();
@@ -143622,14 +143843,10 @@ var ScreamTUI = class {
143622
143843
  this.streamingUI.resetToolUi();
143623
143844
  this.streamingUI.resetToolCallState();
143624
143845
  this.patchLivePane({
143625
- mode: "waiting",
143626
143846
  pendingApproval: null,
143627
143847
  pendingQuestion: null
143628
143848
  });
143629
- this.setAppState({
143630
- streamingPhase: "waiting",
143631
- streamingStartTime: Date.now()
143632
- });
143849
+ this.setAppState({ streamingPhase: "waiting" });
143633
143850
  }
143634
143851
  failSessionRequest(message) {
143635
143852
  this.setAppState({ streamingPhase: "idle" });
@@ -143711,6 +143928,10 @@ var ScreamTUI = class {
143711
143928
  }
143712
143929
  }
143713
143930
  setAppState(patch) {
143931
+ if ("streamingPhase" in patch && patch.streamingPhase !== void 0 && patch.streamingPhase !== this.state.appState.streamingPhase) patch = {
143932
+ ...patch,
143933
+ streamingStartTime: patch.streamingPhase === "idle" ? 0 : Date.now()
143934
+ };
143714
143935
  if (!hasPatchChanges(this.state.appState, patch)) return;
143715
143936
  const busyChanged = "streamingPhase" in patch || "isCompacting" in patch;
143716
143937
  Object.assign(this.state.appState, patch);
@@ -143728,10 +143949,6 @@ var ScreamTUI = class {
143728
143949
  patchLivePane(patch) {
143729
143950
  if (!hasPatchChanges(this.state.livePane, patch)) return;
143730
143951
  Object.assign(this.state.livePane, patch);
143731
- if ("mode" in patch) {
143732
- this.state.appState.livePaneMode = patch.mode;
143733
- this.state.footer.setState(this.state.appState);
143734
- }
143735
143952
  this.lifecycleController.updateActivityPane();
143736
143953
  this.state.ui.requestRender();
143737
143954
  }
@@ -143758,7 +143975,7 @@ var ScreamTUI = class {
143758
143975
  }
143759
143976
  async checkForUpdates() {
143760
143977
  try {
143761
- await refreshUpdateCache().catch(() => {});
143978
+ if (!this.updatePrefetched) await refreshUpdateCache().catch(() => {});
143762
143979
  const cache = await readUpdateCache();
143763
143980
  const target = selectUpdateTarget(this.state.appState.version, cache.latest);
143764
143981
  if (target !== null) this.setAppState({
@@ -143902,10 +144119,10 @@ const SHADOW_CHARS = new Set([
143902
144119
  "╩",
143903
144120
  "╬"
143904
144121
  ]);
143905
- const SHEEN_STEP = 2;
143906
- const SHEEN_INTERVAL_MS = 150;
144122
+ const SHEEN_STEP = 4;
144123
+ const SHEEN_INTERVAL_MS = 60;
143907
144124
  const LOADING_DURATION_MS = 1500;
143908
- const THEME_ACCENT = {
144125
+ const THEME_PRIMARY = {
143909
144126
  dark: [
143910
144127
  78,
143911
144128
  200,
@@ -143932,39 +144149,98 @@ const DIM_RGB = [
143932
144149
  85,
143933
144150
  85
143934
144151
  ];
144152
+ const HUE_STOPS = 24;
144153
+ const SUB_STEPS = 5;
144154
+ const BREATHE_STEPS = HUE_STOPS * SUB_STEPS;
144155
+ function rgbToHsl(r, g, b) {
144156
+ const rf = r / 255, gf = g / 255, bf = b / 255;
144157
+ const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
144158
+ const l = (max + min) / 2;
144159
+ if (max === min) return [
144160
+ 0,
144161
+ 0,
144162
+ l * 100
144163
+ ];
144164
+ const d = max - min;
144165
+ const s = l > .5 ? d / (2 - max - min) : d / (max + min);
144166
+ let h = 0;
144167
+ if (max === rf) h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
144168
+ else if (max === gf) h = ((bf - rf) / d + 2) / 6;
144169
+ else h = ((rf - gf) / d + 4) / 6;
144170
+ return [
144171
+ h * 360,
144172
+ s * 100,
144173
+ l * 100
144174
+ ];
144175
+ }
144176
+ function hslToRgb(h, s, l) {
144177
+ const hf = (h % 360 + 360) % 360 / 360;
144178
+ const sf = s / 100, lf = l / 100;
144179
+ if (sf === 0) {
144180
+ const v = Math.round(lf * 255);
144181
+ return [
144182
+ v,
144183
+ v,
144184
+ v
144185
+ ];
144186
+ }
144187
+ const q = lf < .5 ? lf * (1 + sf) : lf + sf - lf * sf;
144188
+ const p = 2 * lf - q;
144189
+ const hue = (t) => {
144190
+ if (t < 0) t += 1;
144191
+ if (t > 1) t -= 1;
144192
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
144193
+ if (t < 1 / 2) return q;
144194
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
144195
+ return p;
144196
+ };
144197
+ return [
144198
+ Math.round(hue(hf + 1 / 3) * 255),
144199
+ Math.round(hue(hf) * 255),
144200
+ Math.round(hue(hf - 1 / 3) * 255)
144201
+ ];
144202
+ }
144203
+ function buildBreathingPalette(primary) {
144204
+ const [r, g, b] = primary;
144205
+ const [baseHue] = rgbToHsl(r, g, b);
144206
+ const steps = HUE_STOPS * SUB_STEPS;
144207
+ const palette = [];
144208
+ for (let i = 0; i < steps; i++) {
144209
+ const hueAngle = (baseHue + i / steps * 360) % 360;
144210
+ palette.push(hslToRgb(hueAngle, 90, 70));
144211
+ }
144212
+ return palette;
144213
+ }
143935
144214
  function fg(r, g, b) {
143936
144215
  return `\u001B[38;2;${r};${g};${b}m`;
143937
144216
  }
143938
144217
  const RESET = "\x1B[0m";
143939
144218
  const BOLD = "\x1B[1m";
143940
144219
  const DIM = "\x1B[2m";
143941
- function renderSheen(char, charIndex, sheenPos, isReversing, accent) {
144220
+ function renderSheen(char, charIndex, sheenPos, isReversing, breatheColor) {
143942
144221
  if (char === " ") return " ";
143943
144222
  if (char === "█") return `${fg(...BLOCK_RGB)}█${RESET}`;
143944
144223
  if (!SHADOW_CHARS.has(char)) return `${fg(...LOGO_RGB)}${char}${RESET}`;
143945
- let color;
143946
- if (isReversing) color = charIndex <= sheenPos ? LOGO_RGB : accent;
143947
- else color = charIndex <= sheenPos ? accent : LOGO_RGB;
143948
- return `${fg(...color)}${char}${RESET}`;
144224
+ return `${fg(...isReversing ? charIndex <= sheenPos ? LOGO_RGB : breatheColor : charIndex <= sheenPos ? breatheColor : LOGO_RGB)}${char}${RESET}`;
143949
144225
  }
143950
144226
  const LOADING_TEXT = "Ai正在加载中...";
143951
- function buildShimmerPalette(n, accent) {
144227
+ function buildShimmerPalette(n, breatheColor) {
143952
144228
  const size = Math.max(8, Math.min(20, Math.ceil(n * 1.5)));
143953
144229
  const palette = [];
143954
144230
  for (let i = 0; i < size; i++) {
143955
144231
  const t = i / (size - 1);
143956
144232
  palette.push([
143957
- Math.round(accent[0] - t * accent[0] * .35),
143958
- Math.round(accent[1] - t * accent[1] * .6),
143959
- Math.round(accent[2] - t * accent[2] * .33)
144233
+ Math.round(breatheColor[0] - t * breatheColor[0] * .35),
144234
+ Math.round(breatheColor[1] - t * breatheColor[1] * .6),
144235
+ Math.round(breatheColor[2] - t * breatheColor[2] * .33)
143960
144236
  ]);
143961
144237
  }
143962
144238
  return palette;
143963
144239
  }
143964
- function renderShimmer(pulse, accent) {
144240
+ function renderShimmer(pulse, breatheColor) {
143965
144241
  const chars = LOADING_TEXT.split("");
143966
144242
  const n = chars.length;
143967
- const palette = buildShimmerPalette(n, accent);
144243
+ const palette = buildShimmerPalette(n, breatheColor);
143968
144244
  let out = "";
143969
144245
  for (let i = 0; i < n; i++) {
143970
144246
  const phase = (pulse - i + n) % n;
@@ -144030,34 +144306,44 @@ function supportsAnsi() {
144030
144306
  ansiSupported = false;
144031
144307
  return false;
144032
144308
  }
144033
- function runLoadingAnimation(theme = "dark") {
144309
+ function runLoadingAnimation(theme = "dark", prefetch) {
144034
144310
  if (!supportsAnsi()) {
144035
- for (const line of LOGO) stdout.write(`${fg(...LOGO_RGB)}${line}${RESET}\n`);
144036
- stdout.write(`${BOLD}${fg(...THEME_ACCENT[theme])}正在唤醒核心...${RESET}\n`);
144037
- return Promise.resolve();
144311
+ const finish = () => {
144312
+ for (const line of LOGO) stdout.write(`${fg(...LOGO_RGB)}${line}${RESET}\n`);
144313
+ stdout.write(`${BOLD}${fg(...THEME_PRIMARY[theme])}正在唤醒核心...${RESET}\n`);
144314
+ };
144315
+ if (prefetch === void 0) {
144316
+ finish();
144317
+ return Promise.resolve();
144318
+ }
144319
+ return prefetch.catch(() => {}).finally(() => finish()).then(() => void 0);
144038
144320
  }
144039
144321
  return new Promise((resolve) => {
144040
144322
  if (process$1.platform !== "win32") stdout.write("\x1B[?1049h");
144041
144323
  stdout.write("\x1B[2J");
144042
144324
  stdout.write("\x1B[?25l");
144043
- const accent = THEME_ACCENT[theme];
144325
+ const primary = THEME_PRIMARY[theme];
144326
+ const breathePalette = buildBreathingPalette(primary);
144044
144327
  let sheenPos = 0;
144045
144328
  let isReversing = false;
144046
144329
  let shimmerPulse = 0;
144330
+ let breatheFrame = 0;
144047
144331
  let phase = "loading";
144048
144332
  function render() {
144049
144333
  const { cols, rows } = getTerminalSize();
144050
144334
  const lines = [];
144051
- const contentHeight = LOGO.length + 4;
144335
+ const contentHeight = LOGO.length + 5;
144052
144336
  const topPad = Math.max(0, Math.floor((rows - contentHeight) / 2));
144053
144337
  for (let i = 0; i < topPad; i++) lines.push("");
144338
+ const breatheColor = breathePalette[breatheFrame] ?? primary;
144054
144339
  for (const line of LOGO) {
144055
144340
  let colored = "";
144056
- for (let ci = 0; ci < line.length; ci++) colored += renderSheen(line[ci], ci, sheenPos, isReversing, accent);
144341
+ for (let ci = 0; ci < line.length; ci++) colored += renderSheen(line[ci], ci, sheenPos, isReversing, breatheColor);
144057
144342
  lines.push(centerPad(colored, cols));
144058
144343
  }
144059
- if (phase === "loading") lines.push(centerPad(renderShimmer(shimmerPulse, accent), cols));
144060
- else lines.push(centerPad(`${BOLD}${fg(...accent)}按下 ENTER 唤醒核心${RESET}`, cols));
144344
+ lines.push("");
144345
+ if (phase === "loading") lines.push(centerPad(renderShimmer(shimmerPulse, breatheColor), cols));
144346
+ else lines.push(centerPad(`${BOLD}${fg(...breatheColor)}按下 ENTER 唤醒核心${RESET}`, cols));
144061
144347
  lines.push("");
144062
144348
  lines.push("");
144063
144349
  lines.push(centerPad(`${fg(...DIM_RGB)}按住 Ctrl+C 即可退出 Scream Code${RESET}`, cols));
@@ -144072,6 +144358,7 @@ function runLoadingAnimation(theme = "dark") {
144072
144358
  sheenPos = 0;
144073
144359
  }
144074
144360
  shimmerPulse = (shimmerPulse + 1) % 10;
144361
+ breatheFrame = (breatheFrame + 1) % BREATHE_STEPS;
144075
144362
  render();
144076
144363
  }
144077
144364
  function onData(data) {
@@ -144111,17 +144398,20 @@ function runLoadingAnimation(theme = "dark") {
144111
144398
  stdout.write("\x1B[?25h");
144112
144399
  if (process$1.platform !== "win32") stdout.write("\x1B[?1049l");
144113
144400
  for (const line of LOGO) stdout.write(`${fg(...LOGO_RGB)}${line}${RESET}\n`);
144114
- stdout.write(`${BOLD}${fg(...accent)}正在唤醒核心...${RESET}\n`);
144401
+ stdout.write(`${BOLD}${fg(...primary)}正在唤醒核心...${RESET}\n`);
144115
144402
  resolve();
144116
144403
  return;
144117
144404
  }
144118
144405
  stdin.on("data", onData);
144119
144406
  render();
144120
144407
  const timer = setInterval(tick, SHEEN_INTERVAL_MS);
144121
- setTimeout(() => {
144408
+ const minDelay = new Promise((resolve) => {
144409
+ setTimeout(resolve, LOADING_DURATION_MS);
144410
+ });
144411
+ (prefetch === void 0 ? minDelay : Promise.all([minDelay, prefetch.catch(() => {})])).then(() => {
144122
144412
  phase = "ready";
144123
144413
  render();
144124
- }, LOADING_DURATION_MS);
144414
+ });
144125
144415
  });
144126
144416
  }
144127
144417
  //#endregion
@@ -144151,14 +144441,15 @@ async function runShell(opts, version) {
144151
144441
  });
144152
144442
  await harness.ensureConfigFile();
144153
144443
  await harness.preflight();
144154
- await runLoadingAnimation(resolvedTheme);
144444
+ await runLoadingAnimation(resolvedTheme, refreshUpdateCache().catch(() => {}));
144155
144445
  const tui = new ScreamTUI(harness, {
144156
144446
  cliOptions: opts,
144157
144447
  tuiConfig,
144158
144448
  version,
144159
144449
  workDir,
144160
144450
  startupNotice: configWarning,
144161
- resolvedTheme
144451
+ resolvedTheme,
144452
+ updatePrefetched: true
144162
144453
  });
144163
144454
  tui.onExit = async (exitCode = 0) => {
144164
144455
  const sessionId = tui.getCurrentSessionId();