scream-code 0.17.9 → 0.18.0

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.
@@ -5,8 +5,8 @@ const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
7
  import { A as sharedMemoStore } from "./src-C7C9e5Ds.mjs";
8
- import { $n as encodeWorkDirKey, $t as lerpGradient, A as highlightLines, An as embeddingFailureDetail, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as detectShellEnvironment, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getLlmNotSetMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as sortSlashCommands, Dt as createEditorTheme, E as UserMessageComponent, En as BUILTIN_SLASH_COMMANDS, Et as resolveThemeSync, F as StatusMessageComponent, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_COMMAND_NAME, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getInputHistoryFile, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiConfigParseError, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as PRODUCT_NAME, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_UI_MODE, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as TuiLikePreferencesSchema, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as hasEmbeddingModelCache, Mt as createThemeStyles, N as ThinkingComponent, Nn as startManualEmbeddingDownload, Nt as contrastTextHex, O as renderDiffLines, On as isExperimentalFlagEnabled, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as ScreamHarness, Qt as FooterComponent, R as SkillActivationComponent, Rn as loadTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getCtrlDHint, St as supportsBalance, Tn as buildSkillSlashCommands, U as getSharedSpeedTracker, Un as getLogDir, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getDataDir, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as fetchCatalog, Xt as isBusy, Y as resetBreathingClock, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as saveCatalogCache, Zt as isStreaming, _n as EXIT_CONFIRM_WINDOW_MS, a as syncDiagramCapabilityPrompt, an as printableChar, ar as isRealUserPrompt, at as toTerminalHyperlink, b as readUpdateCache, bn as TIP_ROTATION_INTERVAL_MS, c as disposeChildren, cn as appendStreamingArgsPreview, cr as flushDiagnosticLogs, d as isPlanExpandable, dn as isTodoItemShape, dr as isScreamError, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as isOrphanedToolCallError, ft as handleFusionPlanCommand, gn as EMPTY_SESSION_HINT_URL, hn as truncateErrorMessage, i as buildRoleAdditionalText, it as isInternalMessage, j as langFromPath, jn as getKnowledgeStore, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as setExperimentalFlags, kt as isEmptySessionHintDismissed, l as hasDispose, ln as argsRecord, lr as log, mn as stringValue, mr as SCREAM_ERROR_INFO, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as parseRlmMaxDepthArg, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, pr as ErrorCodes, q as BREATHE_CYCLE_MS, qn as CLI_USER_AGENT_PRODUCT, qt as TERMINAL_FOCUS_OUT, r as openUrl, rr as removeSessionIndexEntry, sn as appendSessionHeaderHint, sr as resolveScreamHome, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as resolveGlobalLogPath, v as selectUpdateTarget, vn as MAIN_AGENT_ID, w as getDaemonInstructions, wn as getNoActiveSessionMessage, x as appendJsonlLine, xn as getCtrlCHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as SESSION_TIPS, z as AssistantMessageComponent, zn as saveTuiConfig, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-BGYq0dnV.mjs";
9
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-BlSqBYxO.mjs";
8
+ import { $n as encodeWorkDirKey, $t as lerpGradient, A as highlightLines, An as embeddingFailureDetail, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as detectShellEnvironment, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getLlmNotSetMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as sortSlashCommands, Dt as createEditorTheme, E as UserMessageComponent, En as BUILTIN_SLASH_COMMANDS, Et as resolveThemeSync, F as StatusMessageComponent, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_COMMAND_NAME, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getInputHistoryFile, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiConfigParseError, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as PRODUCT_NAME, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_UI_MODE, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as TuiLikePreferencesSchema, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as hasEmbeddingModelCache, Mt as createThemeStyles, N as ThinkingComponent, Nn as startManualEmbeddingDownload, Nt as contrastTextHex, O as renderDiffLines, On as isExperimentalFlagEnabled, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as ScreamHarness, Qt as FooterComponent, R as SkillActivationComponent, Rn as loadTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getCtrlDHint, St as supportsBalance, Tn as buildSkillSlashCommands, U as getSharedSpeedTracker, Un as getLogDir, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getDataDir, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as fetchCatalog, Xt as isBusy, Y as resetBreathingClock, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as saveCatalogCache, Zt as isStreaming, _n as EXIT_CONFIRM_WINDOW_MS, a as syncDiagramCapabilityPrompt, an as printableChar, ar as isRealUserPrompt, at as toTerminalHyperlink, b as readUpdateCache, bn as TIP_ROTATION_INTERVAL_MS, c as disposeChildren, cn as appendStreamingArgsPreview, cr as flushDiagnosticLogs, d as isPlanExpandable, dn as isTodoItemShape, dr as isScreamError, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as isOrphanedToolCallError, ft as handleFusionPlanCommand, gn as EMPTY_SESSION_HINT_URL, hn as truncateErrorMessage, i as buildRoleAdditionalText, it as isInternalMessage, j as langFromPath, jn as getKnowledgeStore, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as setExperimentalFlags, kt as isEmptySessionHintDismissed, l as hasDispose, ln as argsRecord, lr as log, mn as stringValue, mr as SCREAM_ERROR_INFO, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as parseRlmMaxDepthArg, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, pr as ErrorCodes, q as BREATHE_CYCLE_MS, qn as CLI_USER_AGENT_PRODUCT, qt as TERMINAL_FOCUS_OUT, r as openUrl, rr as removeSessionIndexEntry, sn as appendSessionHeaderHint, sr as resolveScreamHome, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as resolveGlobalLogPath, v as selectUpdateTarget, vn as MAIN_AGENT_ID, w as getDaemonInstructions, wn as getNoActiveSessionMessage, x as appendJsonlLine, xn as getCtrlCHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as SESSION_TIPS, z as AssistantMessageComponent, zn as saveTuiConfig, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-DfHVLpfA.mjs";
9
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CK6jTftM.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
12
12
  import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -33,7 +33,7 @@ function optionalBuildString(value) {
33
33
  return typeof value === "string" && value.length > 0 ? value : void 0;
34
34
  }
35
35
  const SCREAM_BUILD_INFO = {
36
- version: optionalBuildString("0.17.9"),
36
+ version: optionalBuildString("0.18.0"),
37
37
  channel: optionalBuildString(""),
38
38
  commit: optionalBuildString(""),
39
39
  buildTarget: optionalBuildString("darwin-arm64")
@@ -4206,6 +4206,7 @@ var SessionReplayRenderer = class {
4206
4206
  this.host = host;
4207
4207
  }
4208
4208
  async hydrateFromReplay(session) {
4209
+ this.host.state.ui.resetTextSelection();
4209
4210
  this.host.setAppState({ isReplaying: true });
4210
4211
  try {
4211
4212
  const main = session.getResumeState()?.agents["main"];
@@ -6720,6 +6721,7 @@ var TasksBrowserController = class {
6720
6721
  }
6721
6722
  const current = state.tasksBrowser;
6722
6723
  if (current === void 0 || current !== browser) return;
6724
+ if (current.viewer !== void 0) return;
6723
6725
  const viewer = new TaskOutputViewer({
6724
6726
  taskId,
6725
6727
  info: this.host.backgroundTasks.get(taskId),
@@ -6729,7 +6731,7 @@ var TasksBrowserController = class {
6729
6731
  this.closeOutputViewer();
6730
6732
  }
6731
6733
  }, state.terminal);
6732
- const savedBrowserLayout = state.layoutRoot;
6734
+ const savedBrowserLayout = browser.component;
6733
6735
  state.ui.setLayoutRoot(viewer);
6734
6736
  state.ui.setFocus(viewer);
6735
6737
  state.ui.requestRender(true);
@@ -7211,6 +7213,7 @@ var TranscriptController = class TranscriptController {
7211
7213
  this.dropLiveComponent(component);
7212
7214
  }
7213
7215
  this.committedComponent.setCount(this.committedComponent.getCount() + toCommit.length);
7216
+ state.ui.resetTextSelection();
7214
7217
  if (process.env["SCREAM_CODE_DEBUG"] === "1") this.host.showStatus(`[debug] committed=${this.committedComponent.getCount()} live=${this.getLiveCount()}`);
7215
7218
  container.invalidate();
7216
7219
  state.ui.requestRender();
@@ -7385,6 +7388,7 @@ var TranscriptController = class TranscriptController {
7385
7388
  }
7386
7389
  clearAndRedraw() {
7387
7390
  const { state, streamingUI, imageStore } = this.host;
7391
+ state.ui.resetTextSelection();
7388
7392
  streamingUI.discardPending();
7389
7393
  streamingUI.endActivityGroup();
7390
7394
  streamingUI.flushPendingApprovals();
@@ -7449,12 +7453,17 @@ var TranscriptController = class TranscriptController {
7449
7453
  const { state } = this.host;
7450
7454
  const next = !state.toolOutputExpanded;
7451
7455
  const children = state.transcriptContainer.children;
7456
+ let resized = false;
7452
7457
  for (let i = children.length - 1; i >= 0; i -= 1) {
7453
7458
  const child = children[i];
7454
7459
  if (child === void 0) continue;
7455
7460
  if (child instanceof UserMessageComponent || child instanceof SkillActivationComponent || child instanceof CronMessageComponent || child instanceof CommittedTranscriptComponent) break;
7456
- if (isExpandable(child)) child.setExpanded(next);
7461
+ if (isExpandable(child)) {
7462
+ child.setExpanded(next);
7463
+ resized = true;
7464
+ }
7457
7465
  }
7466
+ if (resized) state.ui.resetTextSelection();
7458
7467
  state.toolOutputExpanded = next;
7459
7468
  state.ui.requestRender();
7460
7469
  }
@@ -7464,6 +7473,7 @@ var TranscriptController = class TranscriptController {
7464
7473
  let toggled = false;
7465
7474
  for (const child of state.transcriptContainer.children) if (isPlanExpandable(child) && child.setPlanExpanded(next)) toggled = true;
7466
7475
  if (!toggled) return false;
7476
+ state.ui.resetTextSelection();
7467
7477
  state.planExpanded = next;
7468
7478
  state.ui.requestRender();
7469
7479
  return true;
@@ -10935,6 +10945,7 @@ var CustomEditor = class extends Editor {
10935
10945
  thinkingLevel: this.thinkingLevel,
10936
10946
  toolPriority: this.toolPriority,
10937
10947
  paint: this.borderColor ?? ((s) => s),
10948
+ modeHex: getPermissionModeColor(this.permissionMode, this.colors),
10938
10949
  borderHex: this.borderHex
10939
10950
  });
10940
10951
  return lines;
@@ -11085,6 +11096,15 @@ function injectPromptSymbol(line) {
11085
11096
  }
11086
11097
  const THINK_LABEL_MIN_WIDTH = 14;
11087
11098
  const MODE_BADGE_MIN_WIDTH = 10;
11099
+ function getPermissionModeColor(mode, colors) {
11100
+ return {
11101
+ manual: colors.permissionManual,
11102
+ yolo: colors.permissionYolo,
11103
+ auto: colors.permissionAuto,
11104
+ ask: colors.permissionAsk,
11105
+ bot: colors.permissionBot
11106
+ }[mode];
11107
+ }
11088
11108
  function isLightBg(hex) {
11089
11109
  const r = parseInt(hex.slice(1, 3), 16);
11090
11110
  const g = parseInt(hex.slice(3, 5), 16);
@@ -11115,7 +11135,7 @@ function injectBorderBadges(lines, width, opts) {
11115
11135
  let right = paint("─");
11116
11136
  if (width >= MODE_BADGE_MIN_WIDTH) {
11117
11137
  const badgeText = ` ${opts.mode} `;
11118
- left = paint("──") + (opts.borderHex ? chalk.hex(opts.borderHex).bold(badgeText) : paint(badgeText));
11138
+ left = paint("──") + chalk.hex(opts.modeHex).bold(badgeText);
11119
11139
  }
11120
11140
  if (opts.thinking && width >= THINK_LABEL_MIN_WIDTH) right = makeBadge(opts.thinkingLevel !== "off" ? ` Think ${opts.thinkingLevel} ` : " Think ", opts.borderHex) + right;
11121
11141
  if (opts.toolPriority === "skill" || opts.toolPriority === "mcp") right = makeBadge(` First ${opts.toolPriority} `, opts.borderHex) + paint("─") + right;
@@ -12693,6 +12713,10 @@ const DEFAULT_KEYBOARD_SHORTCUTS = [
12693
12713
  keys: "Shift-Tab",
12694
12714
  description: t("help.toggle_plan")
12695
12715
  },
12716
+ {
12717
+ keys: "Tab",
12718
+ description: t("help.cycle_thinking")
12719
+ },
12696
12720
  {
12697
12721
  keys: "Ctrl-G",
12698
12722
  description: t("help.external_editor")
@@ -12701,10 +12725,26 @@ const DEFAULT_KEYBOARD_SHORTCUTS = [
12701
12725
  keys: "Ctrl-O",
12702
12726
  description: t("help.toggle_output")
12703
12727
  },
12728
+ {
12729
+ keys: "Ctrl-E",
12730
+ description: t("help.toggle_plan_view")
12731
+ },
12704
12732
  {
12705
12733
  keys: "Ctrl-S",
12706
12734
  description: t("help.interrupt")
12707
12735
  },
12736
+ {
12737
+ keys: "Ctrl-X",
12738
+ description: t("help.toggle_sidebar")
12739
+ },
12740
+ {
12741
+ keys: "Ctrl-B",
12742
+ description: t("help.toggle_hint")
12743
+ },
12744
+ {
12745
+ keys: "Ctrl-W",
12746
+ description: t("help.cancel_memory")
12747
+ },
12708
12748
  {
12709
12749
  keys: "Shift-Enter / Ctrl-J",
12710
12750
  description: t("help.newline")
@@ -14930,7 +14970,7 @@ var ScreamTUI = class {
14930
14970
  const card = new OnboardingCardComponent({
14931
14971
  colors: this.state.theme.colors,
14932
14972
  onConfigure: () => {
14933
- import("./dispatch-DoaBG--u.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14973
+ import("./dispatch-CUfxgLwq.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14934
14974
  this.restoreEditor();
14935
14975
  onDone?.();
14936
14976
  });
@@ -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 { rn as handleConnectCommand } from "./dispatch-BGYq0dnV.mjs";
6
+ import { rn as handleConnectCommand } from "./dispatch-DfHVLpfA.mjs";
7
7
  export { handleConnectCommand };
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
7
  import { A as sharedMemoStore, B as buildProjectTagCloud, C as applyConsolidation, D as parseMemoryMemos, E as buildExitExtractionPrompt, F as probeLocalEmbeddingSupport, G as join$1, H as basename$1, I as createMemoryMemo, J as relative$1, K as normalize, L as toSummary, M as clearEmbeddingModelCache, N as createFastEmbedEngine, O as stripMemoryTags, P as hasEmbeddingModelCache$1, R as generateTags, S as DreamTracker, T as EXIT_EXTRACTION_SYSTEM_PROMPT, U as dirname$2, V as rankMemos, W as isAbsolute$1, Y as resolve$2, a as isSupportedFile, b as sharedKnowledgeStoreReady, i as ingestFile, j as classifyEmbeddingFailure, k as MemoryMemoStore, q as parse$7, r as ingestDirectory, t as multiSearch, w as buildConsolidationPlan, y as sharedKnowledgeStore, z as normalizeTags } from "./src-C7C9e5Ds.mjs";
8
8
  import { n as require_base64_js, t as XMLParser } from "./XMLParser-DFAnGqkc.mjs";
9
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-BlSqBYxO.mjs";
9
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-CK6jTftM.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
12
12
  import * as fs$1 from "node:fs/promises";
@@ -784,9 +784,28 @@ var APIEmptyResponseError = class extends ChatProviderError {
784
784
  this.name = "APIEmptyResponseError";
785
785
  }
786
786
  };
787
+ /**
788
+ * The provider ended its stream with an in-band failure signal instead of
789
+ * throwing (`finish_reason: "error"` — Mistral-style Chat Completions
790
+ * endpoints). Normalized without a typed class this looked like an "unknown"
791
+ * stop and ended the turn silently: no retry, no error surfaced. Typed here so
792
+ * {@link isRetryableGenerateError} classifies it without message sniffing —
793
+ * the upstream generation broke, the request itself is fine, so the step is
794
+ * worth retrying.
795
+ */
796
+ var APIFinishReasonError = class extends ChatProviderError {
797
+ /** Raw provider finish reason, preserved verbatim (`"error"`). */
798
+ rawFinishReason;
799
+ constructor(message, rawFinishReason) {
800
+ super(message);
801
+ this.name = "APIFinishReasonError";
802
+ this.rawFinishReason = rawFinishReason;
803
+ }
804
+ };
787
805
  function isRetryableGenerateError(error) {
788
806
  if (error instanceof APIConnectionError$3 || error instanceof APITimeoutError) return true;
789
807
  if (error instanceof APIEmptyResponseError) return true;
808
+ if (error instanceof APIFinishReasonError) return true;
790
809
  if (error instanceof APIProviderRateLimitError) return true;
791
810
  if (error instanceof APIStatusError) return error.statusCode === 429 || error.statusCode === 408 || error.statusCode >= 500 && error.statusCode < 600;
792
811
  if (error instanceof ChatProviderError) return isRetryableProviderMessage(error.message);
@@ -823,7 +842,9 @@ const RETRYABLE_PROVIDER_MESSAGE_PATTERNS = [
823
842
  /stream ended|ended without/i,
824
843
  /you can retry your request/i,
825
844
  /try your request again/i,
826
- /please retry your request/i
845
+ /please retry your request/i,
846
+ /\bserver[\s_-]+busy\b/i,
847
+ /\bservers?\s+(?:are|is)\s+(?:currently\s+)?busy\b/i
827
848
  ];
828
849
  /**
829
850
  * Classify a status-less provider error message. Quota/billing-style wording
@@ -44674,6 +44695,29 @@ function normalizeOpenAIFinishReason(raw) {
44674
44695
  }
44675
44696
  }
44676
44697
  /**
44698
+ * Raise a retryable {@link APIFinishReasonError} when a provider signalled an
44699
+ * in-band stream failure through its terminal finish reason instead of
44700
+ * throwing.
44701
+ *
44702
+ * Mistral-style Chat Completions endpoints end a broken generation with
44703
+ * `finish_reason: "error"`. Normalizing that to `'other'` used to end the turn
44704
+ * silently (no retry, no error shown). Throwing here — from the adapter's
44705
+ * finish-reason capture, i.e. while `generate()` drains the stream — routes the
44706
+ * failure out of `provider.generate()`/`generate()` and into the engine's step
44707
+ * retry (`chatWithRetry` → `LLM.isRetryableError` → `isRetryableGenerateError`),
44708
+ * so the step is retried and, once the retry budget is exhausted, fails loudly
44709
+ * instead of ending the turn.
44710
+ *
44711
+ * The raw value is preserved twice: on the thrown error
44712
+ * ({@link APIFinishReasonError.rawFinishReason}) and — because capture happens
44713
+ * before the throw — on the streamed message's `rawFinishReason` for callers
44714
+ * that still hold it.
44715
+ */
44716
+ function throwIfErrorFinishReason(rawFinishReason, providerLabel) {
44717
+ if (rawFinishReason === null || rawFinishReason.trim().toLowerCase() !== "error") return;
44718
+ throw new APIFinishReasonError(`Provider stream ended with finish_reason "${rawFinishReason}": the provider reported an in-band generation failure instead of a normal stop. Provider: ${providerLabel}. The request itself is unchanged, so retrying it may succeed.`, rawFinishReason);
44719
+ }
44720
+ /**
44677
44721
  * Convert tool-role message content according to the chosen strategy.
44678
44722
  */
44679
44723
  function convertToolMessageContent(message, conversion) {
@@ -44959,6 +45003,7 @@ var ScreamStreamedMessage = class {
44959
45003
  const normalized = normalizeOpenAIFinishReason(raw);
44960
45004
  this._finishReason = normalized.finishReason;
44961
45005
  this._rawFinishReason = normalized.rawFinishReason;
45006
+ throwIfErrorFinishReason(normalized.rawFinishReason, "ScreamChatProvider");
44962
45007
  }
44963
45008
  async *_convertNonStreamResponse(response) {
44964
45009
  this._id = response.id;
@@ -45269,6 +45314,7 @@ var OpenAILegacyStreamedMessage = class {
45269
45314
  const normalized = normalizeOpenAIFinishReason(raw);
45270
45315
  this._finishReason = normalized.finishReason;
45271
45316
  this._rawFinishReason = normalized.rawFinishReason;
45317
+ throwIfErrorFinishReason(normalized.rawFinishReason, "OpenAILegacyChatProvider");
45272
45318
  }
45273
45319
  async *_convertNonStreamResponse(response, reasoningKey) {
45274
45320
  this._id = response.id;
@@ -55613,6 +55659,17 @@ const MAX_OUTPUT_BYTES$1 = 1024 * 1024;
55613
55659
  * finished tasks, FIFO-overflowing so long sessions stay bounded.
55614
55660
  */
55615
55661
  const RETIRED_TASK_LIMIT = 20;
55662
+ /**
55663
+ * Pre-cut bytes the bounded window read (`readOutput`) keeps as context for
55664
+ * `tailSlice`'s boundary rules, on top of the last `tail` code units it must
55665
+ * return. The rules inspect the text before the cut (the nearest ESC, a CSI
55666
+ * body run, a string-sequence opener), so a window starting exactly at the cut
55667
+ * would decide differently from the whole-file read in `getOutput`. Control
55668
+ * sequences in task output sit far closer to the cut than this margin; one
55669
+ * opened farther back is invisible to the window and is not worth paging the
55670
+ * rest of a multi-megabyte log for.
55671
+ */
55672
+ const TAIL_WINDOW_LOOKBACK_BYTES = 4096;
55616
55673
  const SIGTERM_GRACE_MS$2 = 5e3;
55617
55674
  const EXIT_SETTLE_GRACE_MS = 10;
55618
55675
  const _ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
@@ -55947,7 +56004,7 @@ var BackgroundProcessManager = class {
55947
56004
  const entry = this.processes.get(taskId);
55948
56005
  if (entry) {
55949
56006
  const full = entry.outputChunks.join("");
55950
- if (tail !== void 0 && tail < full.length) return full.slice(-tail);
56007
+ if (tail !== void 0 && tail < full.length) return tailSlice(full, tail);
55951
56008
  return full;
55952
56009
  }
55953
56010
  const retired = this.retiredTasks.get(taskId);
@@ -55957,7 +56014,7 @@ var BackgroundProcessManager = class {
55957
56014
  const persisted = readFileSync(taskOutputFile(retired.outputSessionDir, taskId), "utf-8");
55958
56015
  if (persisted.length > 0) full = persisted;
55959
56016
  } catch {}
55960
- if (tail !== void 0 && tail < full.length) return full.slice(-tail);
56017
+ if (tail !== void 0 && tail < full.length) return tailSlice(full, tail);
55961
56018
  return full;
55962
56019
  }
55963
56020
  async readOutput(taskId, tail) {
@@ -55968,7 +56025,7 @@ var BackgroundProcessManager = class {
55968
56025
  if (tail !== void 0 && tail > 0) {
55969
56026
  const size = await taskOutputSizeBytes(outputSessionDir, taskId);
55970
56027
  if (size > 0) {
55971
- const windowBytes = Math.min(size, tail * 4 + 7);
56028
+ const windowBytes = Math.min(size, tail * 4 + 7 + TAIL_WINDOW_LOOKBACK_BYTES);
55972
56029
  const start = size - windowBytes;
55973
56030
  let persisted = await readTaskOutputBytes(outputSessionDir, taskId, start, windowBytes);
55974
56031
  if (persisted.length > 0) {
@@ -55977,13 +56034,13 @@ var BackgroundProcessManager = class {
55977
56034
  while (lead < persisted.length && persisted.codePointAt(lead) === 65533 && lead < 3) lead += 1;
55978
56035
  if (lead > 0) persisted = persisted.slice(lead);
55979
56036
  }
55980
- return tail < persisted.length ? persisted.slice(-tail) : persisted;
56037
+ return tail < persisted.length ? tailSlice(persisted, tail) : persisted;
55981
56038
  }
55982
56039
  return this.getOutput(taskId, tail);
55983
56040
  }
55984
56041
  } else {
55985
56042
  const persisted = await readTaskOutput(outputSessionDir, taskId);
55986
- if (persisted.length > 0) return tail !== void 0 && tail < persisted.length ? persisted.slice(-tail) : persisted;
56043
+ if (persisted.length > 0) return tail !== void 0 && tail < persisted.length ? tailSlice(persisted, tail) : persisted;
55987
56044
  }
55988
56045
  }
55989
56046
  return this.getOutput(taskId, tail);
@@ -56462,6 +56519,243 @@ var BackgroundProcessManager = class {
56462
56519
  return true;
56463
56520
  }
56464
56521
  };
56522
+ const ESC$2 = "\x1B";
56523
+ /**
56524
+ * Largest share of a retained tail the boundary rules may discard.
56525
+ *
56526
+ * Both rules trade content for a head that is whole by construction: rule 1
56527
+ * restarts after the next `\n` and drops the torn line, and rule 2 skips the
56528
+ * remainder of a torn sequence. The trade only pays while the tail keeps a
56529
+ * (non-strict) majority — with one 5000-char line ahead, a 100-unit budget
56530
+ * would come back with the few units after it, and the caller asked for the
56531
+ * newest output, not for a tidy head. Past the bound the raw cut keeps the
56532
+ * content: a string payload stays visible, and a torn CSI or two-byte escape
56533
+ * run keeps the fragment it left in view, because skipping that remainder
56534
+ * would cost more than the fragment it hides — unless the introducer ESC sits
56535
+ * immediately before the head *and* the sequence provably closes, where one
56536
+ * unit of look-back keeps the sequence whole and the fragment never surfaces.
56537
+ * An unclosed run keeps the raw cut: with no terminator to hand the sanitizer,
56538
+ * backing onto its ESC would surface a bare ESC the sanitizer cannot strip.
56539
+ */
56540
+ const MAX_TAIL_DROP_SHARE$1 = .5;
56541
+ /**
56542
+ * `text.slice(-tail)` (clamping exactly like `slice` does for `tail` outside
56543
+ * `(0, text.length)`), with the head moved to a boundary no ANSI escape
56544
+ * sequence spans, so the first bytes a reader sees are never a torn fragment.
56545
+ */
56546
+ function tailSlice(text, tail) {
56547
+ if (tail <= 0 || tail >= text.length) return text;
56548
+ return text.slice(safeTailStart$1(text, text.length - tail));
56549
+ }
56550
+ /**
56551
+ * Move a tail cut point forward to a boundary no ANSI escape sequence spans.
56552
+ *
56553
+ * A raw `slice(-tail)` can land inside a sequence (`…\u001b[31|m…`), leaving a
56554
+ * fragment (`m`, `0m`, `[31`) whose ESC byte was cut away — the TUI sanitizer
56555
+ * only recognizes complete sequences, so the fragment renders as text. Two
56556
+ * provable rules keep the head clean:
56557
+ *
56558
+ * 1. Line alignment: a CSI sequence cannot contain `\n` (LF is not a
56559
+ * parameter, intermediate or final byte), so a cut that lands mid-line
56560
+ * restarts after the next `\n`. The only loss is the torn partial line,
56561
+ * whose head cannot be trusted; every escape after the newline is whole.
56562
+ * Bounded by `MAX_TAIL_DROP_SHARE`, so one huge line cannot eat the budget.
56563
+ * 2. Torn-sequence skip: when the retained text is one line (no `\n` after
56564
+ * the cut), or an OSC/DCS/PM/APC payload spans the newline the cut aligned
56565
+ * to, the dropped prefix is inspected for a sequence still open at the cut
56566
+ * — a run of CSI body bytes (0x20–0x3F) reaching back to `ESC [`, an `ESC`
56567
+ * whose intermediate bytes were torn (`ESC ( B`), or a string-sequence
56568
+ * opener the cut landed inside. The head then skips exactly the bytes a
56569
+ * terminal would consume for that sequence, so no visible text is dropped —
56570
+ * but only while that skip stays within `MAX_TAIL_DROP_SHARE` of the tail.
56571
+ * A CSI body or escape-intermediate run can be arbitrarily long, so past
56572
+ * the bound the head keeps the newest bytes and the fragment the raw cut
56573
+ * left with them — or one unit earlier, onto an introducer ESC sitting
56574
+ * immediately before the head, so the whole sequence survives and is
56575
+ * stripped (`withinDropShare`) — but only when the skip proved the sequence
56576
+ * closed: an unclosed run keeps the raw cut, because with no terminator to
56577
+ * hand the sanitizer the ESC would surface bare and swallow what follows
56578
+ * it. So does the head for a string that never ends or ended ahead of the
56579
+ * cut. A sliver of the tail is worse than a fragment.
56580
+ *
56581
+ * The result never splits a surrogate pair, and a non-empty input never comes
56582
+ * back empty: the head is never below `cut` (a tail never grows) except in two
56583
+ * cases — the cut splits a trailing pair, where dropping the orphan half would
56584
+ * empty the tail, so the head backs onto the pair's first half and the last
56585
+ * character survives; and a torn sequence's introducer ESC sits immediately
56586
+ * before the head *and* the sequence's skip proved it closes, where backing
56587
+ * one unit onto it keeps the sequence whole and the fragment is stripped
56588
+ * instead of surfaced. An unclosed skip takes the raw cut.
56589
+ */
56590
+ function safeTailStart$1(text, cut) {
56591
+ if (cut <= 0 || cut >= text.length) return cut;
56592
+ const tail = text.length - cut;
56593
+ let start = cut;
56594
+ if (text[start - 1] !== "\n") {
56595
+ const newline = text.indexOf("\n", start);
56596
+ if (newline !== -1 && newline + 1 - cut <= tail * MAX_TAIL_DROP_SHARE$1) start = newline + 1;
56597
+ }
56598
+ start = skipOpenEscape$1(text, start);
56599
+ const head = text.codePointAt(start);
56600
+ if (head !== void 0 && head >= 56320 && head <= 57343) return start + 1 < text.length ? start + 1 : start - 1;
56601
+ return start;
56602
+ }
56603
+ /** CSI parameter (0x30–0x3F) / intermediate (0x20–0x2F) bytes — everything before the final byte. */
56604
+ function isCsiBody$1(code) {
56605
+ return code !== void 0 && code >= 32 && code <= 63;
56606
+ }
56607
+ /** CSI final byte (0x40–0x7E) — the sequence ends here. */
56608
+ function isCsiFinal$1(code) {
56609
+ return code !== void 0 && code >= 64 && code <= 126;
56610
+ }
56611
+ /** OSC / DCS / PM / APC introducers — string sequences ended by BEL or ST. */
56612
+ function isStringIntro$1(intro) {
56613
+ return intro === "]" || intro === "P" || intro === "^" || intro === "_";
56614
+ }
56615
+ /** ESC-intermediate byte (0x20–0x2F) of a two-byte escape sequence. */
56616
+ function isEscapeIntermediate$1(code) {
56617
+ return code !== void 0 && code >= 32 && code <= 47;
56618
+ }
56619
+ /** Final byte (0x30–0x7E) that closes a two-byte escape sequence. */
56620
+ function isEscapeFinal$1(code) {
56621
+ return code !== void 0 && code >= 48 && code <= 126;
56622
+ }
56623
+ /** True when every code point in the half-open range `[from, to)` is an ESC intermediate (0x20–0x2F). */
56624
+ function allEscapeIntermediates$1(text, from, to) {
56625
+ for (let i = from; i < to; i += 1) if (!isEscapeIntermediate$1(text.codePointAt(i))) return false;
56626
+ return true;
56627
+ }
56628
+ /**
56629
+ * When the dropped prefix proves an escape sequence is still open at `cut`,
56630
+ * return the index after its remainder; otherwise return `cut` unchanged.
56631
+ */
56632
+ function skipOpenEscape$1(text, cut) {
56633
+ let run = cut;
56634
+ while (run > 0 && isCsiBody$1(text.codePointAt(run - 1))) run -= 1;
56635
+ if (run >= 2 && text[run - 1] === "[" && text[run - 2] === ESC$2) {
56636
+ const skip = skipCsiRest$1(text, cut);
56637
+ return withinDropShare$1(text, cut, skip.end, skip.closed);
56638
+ }
56639
+ if (text[run - 1] === ESC$2 && allEscapeIntermediates$1(text, run, cut)) return skipEscapeRest$1(text, cut);
56640
+ const open = text.lastIndexOf(ESC$2, cut - 1);
56641
+ if (open !== -1 && isStringIntro$1(text[open + 1])) return stringSequenceHead$1(text, open, cut);
56642
+ return cut;
56643
+ }
56644
+ /**
56645
+ * Keep a provable skip only while it stays within `MAX_TAIL_DROP_SHARE` of the
56646
+ * tail that starts at `from`: the skipped bytes are exactly what the reader
56647
+ * loses from the tail it asked for. A CSI body or escape-intermediate run can
56648
+ * be arbitrarily long, so past the bound the raw cut is worth more than the
56649
+ * clean head — except when the sequence's introducer ESC sits immediately
56650
+ * before the head (`…\u001b|[31m…`) *and* `closed` says the skip found the
56651
+ * sequence's terminator, where one unit of look-back keeps the sequence whole
56652
+ * instead: the sanitizer strips it and the visible text is the skip's, for
56653
+ * that one unit rather than a sliver of the tail. An unclosed run must keep
56654
+ * the raw cut even with that ESC one unit ahead: nothing terminates it, so
56655
+ * look-back would hand the reader a bare ESC whose sequence run a terminal
56656
+ * swallows along with the bytes behind it.
56657
+ */
56658
+ function withinDropShare$1(text, from, target, closed) {
56659
+ if (target - from <= (text.length - from) * MAX_TAIL_DROP_SHARE$1) return target;
56660
+ return closed && from > 0 && text[from - 1] === ESC$2 ? from - 1 : from;
56661
+ }
56662
+ /**
56663
+ * The rest of a CSI sequence whose body was torn at `cut`, with the closure
56664
+ * its skip proved: `closed` holds only when a final byte (0x40–0x7E) was
56665
+ * actually found, so a parameter run that reaches the end of `text` — or stops
56666
+ * on a byte no CSI can end with — reports the sequence as open.
56667
+ */
56668
+ function skipCsiRest$1(text, cut) {
56669
+ let end = cut;
56670
+ while (end < text.length && isCsiBody$1(text.codePointAt(end))) end += 1;
56671
+ const closed = end < text.length && isCsiFinal$1(text.codePointAt(end));
56672
+ return {
56673
+ end: closed ? end + 1 : end,
56674
+ closed
56675
+ };
56676
+ }
56677
+ /** Index after the escape sequence whose introducer ESC sits at `cut - 1`. */
56678
+ function skipEscapeRest$1(text, cut) {
56679
+ const intro = text[cut];
56680
+ if (intro === "[") {
56681
+ const skip = skipCsiRest$1(text, cut + 1);
56682
+ return withinDropShare$1(text, cut, skip.end, skip.closed);
56683
+ }
56684
+ if (isStringIntro$1(intro)) return stringSequenceHead$1(text, cut - 1, cut + 1);
56685
+ let end = cut;
56686
+ while (end < text.length && isEscapeIntermediate$1(text.codePointAt(end))) end += 1;
56687
+ const closed = end < text.length && isEscapeFinal$1(text.codePointAt(end));
56688
+ return withinDropShare$1(text, cut, closed ? end + 1 : end, closed);
56689
+ }
56690
+ /**
56691
+ * Head index for a cut that lands inside the string sequence opened at `open`.
56692
+ *
56693
+ * `stringEnd` gives the byte a terminal stops at; skipping to it hides the
56694
+ * payload's remainder, which is what a terminal shows — but only while that
56695
+ * costs at most `MAX_TAIL_DROP_SHARE` of the tail. A long OSC 8 URL, or any
56696
+ * payload longer than the budget, would otherwise hand the caller a sliver of
56697
+ * the tail it asked for; a payload whose extent exceeds the budget stays
56698
+ * visible instead. A cut the string already ended ahead of needs no skip, and
56699
+ * an unterminated one must not swallow the tail (`stringPayloadHead`).
56700
+ */
56701
+ function stringSequenceHead$1(text, open, cut) {
56702
+ const drop = stringEnd$1(text, open + 2) - cut;
56703
+ if (drop >= 0 && drop <= (text.length - cut) * MAX_TAIL_DROP_SHARE$1) return cut + drop;
56704
+ return stringPayloadHead$1(text, open, cut);
56705
+ }
56706
+ /**
56707
+ * Head index that keeps a string sequence's payload visible, moving the cut
56708
+ * only past the opener's own bytes when the cut landed inside them.
56709
+ *
56710
+ * A terminal swallows an unterminated payload, but a viewer must never render
56711
+ * a blank body: `tailSlice` backs the tasks-browser tail read, and a log that
56712
+ * opens with an unterminated OSC — a run killed mid-title, a truncated write —
56713
+ * would otherwise come back empty. The head stays at `cut` unless the cut
56714
+ * landed inside the opener (`\u001b]0;` would leave `0;` in front of the
56715
+ * payload), and never reaches `text.length` — the clamp to the last unit steps
56716
+ * back onto the pair's first half when that unit is a trailing surrogate
56717
+ * pair's low half — so the tail is empty only for empty input.
56718
+ */
56719
+ function stringPayloadHead$1(text, open, cut) {
56720
+ const head = Math.min(Math.max(cut, stringPayloadStart$1(text, open)), text.length - 1);
56721
+ const code = text.codePointAt(head);
56722
+ return head > 0 && code !== void 0 && code >= 56320 && code <= 57343 ? head - 1 : head;
56723
+ }
56724
+ /**
56725
+ * Index after the byte that ends the string sequence whose payload starts at
56726
+ * `from` — its BEL or `ESC \` terminator, or a CAN (0x18) / SUB (0x1A), which
56727
+ * a terminal reads as "string over, resume normal parsing". `-1` when the
56728
+ * string never ends.
56729
+ */
56730
+ function stringEnd$1(text, from) {
56731
+ for (let i = from; i < text.length; i += 1) {
56732
+ const code = text.codePointAt(i);
56733
+ if (code === 7) return i + 1;
56734
+ if (code === 27 && text[i + 1] === "\\") return i + 2;
56735
+ if (code === 24 || code === 26) return i + 1;
56736
+ }
56737
+ return -1;
56738
+ }
56739
+ /**
56740
+ * Index where the payload of the string sequence opened at `open` starts: for
56741
+ * OSC after `ESC ] Ps ;`, for DCS/PM/APC after their parameter and
56742
+ * intermediate bytes and the final byte that opens the payload.
56743
+ */
56744
+ function stringPayloadStart$1(text, open) {
56745
+ let i = open + 2;
56746
+ if (text[open + 1] === "]") {
56747
+ while (i < text.length && isOscParam$1(text.codePointAt(i))) i += 1;
56748
+ if (text[i] === ";") i += 1;
56749
+ return i;
56750
+ }
56751
+ while (i < text.length && isCsiBody$1(text.codePointAt(i))) i += 1;
56752
+ const final = text.codePointAt(i);
56753
+ return i < text.length && isCsiFinal$1(final) ? i + 1 : i;
56754
+ }
56755
+ /** OSC numeric parameter byte (0x30–0x39) — the `Ps` before the `;` separator. */
56756
+ function isOscParam$1(code) {
56757
+ return code !== void 0 && code >= 48 && code <= 57;
56758
+ }
56465
56759
  /**
56466
56760
  * Longest suffix of `text` whose UTF-8 encoding fits in `maxBytes`. Encoding
56467
56761
  * through a Buffer keeps lone surrogates accounted as the 3-byte replacement
@@ -61716,7 +62010,7 @@ var ApiError = class extends Error {
61716
62010
  this.name = "ApiError";
61717
62011
  }
61718
62012
  };
61719
- function isPlaceholder(value) {
62013
+ function isPlaceholder$1(value) {
61720
62014
  const trimmed = value.trim();
61721
62015
  return trimmed.length === 0 || trimmed.includes("replace-with") || trimmed.startsWith("<") && trimmed.endsWith(">");
61722
62016
  }
@@ -61738,11 +62032,11 @@ async function loadConfig(screamHomeDir) {
61738
62032
  try {
61739
62033
  const text = await readFile(join$1(screamHomeDir, "image-config.json"), "utf8");
61740
62034
  const parsed = JSON.parse(text);
61741
- const legacyUrl = typeof parsed.base_url === "string" && !isPlaceholder(parsed.base_url) ? `${normalizeUrl(parsed.base_url)}${GENERATIONS_SUFFIX}` : "";
61742
- const urlRaw = typeof parsed.url === "string" && !isPlaceholder(parsed.url) ? parsed.url : legacyUrl;
61743
- if (urlRaw.length === 0 || typeof parsed.api_key !== "string" || typeof parsed.model !== "string" || isPlaceholder(parsed.api_key)) return;
61744
- const legacyEdit = typeof parsed.edit_base_url === "string" && !isPlaceholder(parsed.edit_base_url) ? `${normalizeUrl(parsed.edit_base_url)}${EDITS_SUFFIX}` : "";
61745
- const editUrlRaw = typeof parsed.edit_url === "string" && !isPlaceholder(parsed.edit_url) ? parsed.edit_url : legacyEdit;
62035
+ const legacyUrl = typeof parsed.base_url === "string" && !isPlaceholder$1(parsed.base_url) ? `${normalizeUrl(parsed.base_url)}${GENERATIONS_SUFFIX}` : "";
62036
+ const urlRaw = typeof parsed.url === "string" && !isPlaceholder$1(parsed.url) ? parsed.url : legacyUrl;
62037
+ if (urlRaw.length === 0 || typeof parsed.api_key !== "string" || typeof parsed.model !== "string" || isPlaceholder$1(parsed.api_key)) return;
62038
+ const legacyEdit = typeof parsed.edit_base_url === "string" && !isPlaceholder$1(parsed.edit_base_url) ? `${normalizeUrl(parsed.edit_base_url)}${EDITS_SUFFIX}` : "";
62039
+ const editUrlRaw = typeof parsed.edit_url === "string" && !isPlaceholder$1(parsed.edit_url) ? parsed.edit_url : legacyEdit;
61746
62040
  const editModelRaw = typeof parsed.edit_model === "string" ? parsed.edit_model : "";
61747
62041
  return {
61748
62042
  provider: typeof parsed.provider === "string" ? parsed.provider : "openai-compatible",
@@ -78334,7 +78628,7 @@ var ScriptTool = class {
78334
78628
  signal: ctx.signal,
78335
78629
  store: this.store.get("scriptStore") ?? {}
78336
78630
  });
78337
- if (!result.ok) return await this.failedResult(result.error, result.output, state);
78631
+ if (!result.ok) return await this.failedResult(result.error, result.output, state, parsed.options);
78338
78632
  this.commitStoreWrites(result.storeWrites);
78339
78633
  return await this.successResult(result.output, result.value, state, parsed.options);
78340
78634
  } finally {
@@ -78397,7 +78691,7 @@ var ScriptTool = class {
78397
78691
  async successResult(output, value, state, options) {
78398
78692
  const textParts = output.filter((item) => item.type === "text").map((item) => item.text);
78399
78693
  if (value !== void 0) textParts.push(valueText(value));
78400
- const { shown } = await truncateOutput(textParts.join("\n"), (options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS) * CHARS_PER_TOKEN);
78694
+ const { shown } = await truncateOutput(textParts.join("\n"), outputBudgetChars(options));
78401
78695
  const parts = [];
78402
78696
  if (shown.length > 0) parts.push({
78403
78697
  type: "text",
@@ -78416,11 +78710,11 @@ var ScriptTool = class {
78416
78710
  ...state.records.length > 0 ? { nestedCalls: state.records } : {}
78417
78711
  };
78418
78712
  }
78419
- async failedResult(error, output, state) {
78713
+ async failedResult(error, output, state, options) {
78420
78714
  const lines = [`Script failed (${error.kind}): ${error.message}`];
78421
78715
  const partial = output.filter((item) => item.type === "text").map((item) => item.text).join("\n");
78422
78716
  if (partial.length > 0) {
78423
- const { shown } = await truncateOutput(partial, DEFAULT_MAX_OUTPUT_TOKENS * CHARS_PER_TOKEN);
78717
+ const { shown } = await truncateOutput(partial, outputBudgetChars(options));
78424
78718
  lines.push("", "Partial output before the failure:", shown);
78425
78719
  }
78426
78720
  if (state.records.length > 0) lines.push(`(${state.records.length} nested call${state.records.length === 1 ? "" : "s"} ran before the failure)`);
@@ -78452,6 +78746,15 @@ function valueText(value) {
78452
78746
  }
78453
78747
  }
78454
78748
  /**
78749
+ * The script's effective output budget in characters: the `@options.max_output_tokens`
78750
+ * annotation when the script carries one, the tool default otherwise. The success path
78751
+ * and the failure path both read this single value, so the annotation cannot apply to
78752
+ * only one of them.
78753
+ */
78754
+ function outputBudgetChars(options) {
78755
+ return (options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS) * CHARS_PER_TOKEN;
78756
+ }
78757
+ /**
78455
78758
  * Truncates to the character budget, keeping half the budget on each side
78456
78759
  * (matching the reference implementation). The full text is spilled to a
78457
78760
  * temp file and its path reported in the shown output.
@@ -132529,6 +132832,38 @@ function resolveConnectCatalogRequest(args) {
132529
132832
  };
132530
132833
  }
132531
132834
  //#endregion
132835
+ //#region src/tui/utils/image-config.ts
132836
+ function getImageConfigPath() {
132837
+ return join(resolveScreamHome(), "image-config.json");
132838
+ }
132839
+ /** Placeholder rules mirrored from the ImageGenerate tool's `isPlaceholder`. */
132840
+ function isPlaceholder(value) {
132841
+ const trimmed = value.trim();
132842
+ return trimmed.length === 0 || trimmed.includes("replace-with") || trimmed.startsWith("<") && trimmed.endsWith(">");
132843
+ }
132844
+ /**
132845
+ * Best-effort read of an existing image config (never throws, never exposes
132846
+ * the key). The `usable` predicate mirrors the ImageGenerate tool's
132847
+ * `loadConfig` (packages/agent-core/src/tools/builtin/image/image-generate.ts):
132848
+ * a config the tool would reject must never be advertised as "configured".
132849
+ */
132850
+ async function readExistingImageConfig() {
132851
+ try {
132852
+ const text = await readFile(getImageConfigPath(), "utf8");
132853
+ const parsed = JSON.parse(text);
132854
+ const urlRaw = typeof parsed["url"] === "string" ? parsed["url"] : "";
132855
+ const legacyRaw = typeof parsed["base_url"] === "string" ? parsed["base_url"] : "";
132856
+ const url = !isPlaceholder(urlRaw) ? urlRaw.trim() : !isPlaceholder(legacyRaw) ? `${legacyRaw.trim().replace(/\/+$/, "")}/images/generations` : "";
132857
+ const model = typeof parsed["model"] === "string" ? parsed["model"].trim() : "";
132858
+ const apiKeyRaw = typeof parsed["api_key"] === "string" ? parsed["api_key"] : "";
132859
+ if (url.length > 0 && model.length > 0 && !isPlaceholder(apiKeyRaw)) return {
132860
+ provider: typeof parsed["provider"] === "string" ? parsed["provider"] : "openai-compatible",
132861
+ model,
132862
+ url
132863
+ };
132864
+ } catch {}
132865
+ }
132866
+ //#endregion
132532
132867
  //#region src/tui/components/dialogs/api-key-input-dialog.ts
132533
132868
  function getFooter() {
132534
132869
  return t("apikey.footer");
@@ -132775,6 +133110,11 @@ var ModelSelectorComponent = class extends Container {
132775
133110
  lines.push(this.renderVideoControl(selected.model));
132776
133111
  lines.push(this.renderAudioControl(selected.model));
132777
133112
  }
133113
+ if (this.opts.imageGeneration !== void 0) {
133114
+ const imageGeneration = this.opts.imageGeneration;
133115
+ lines.push("");
133116
+ lines.push(chalk.hex(colors.textMuted)(imageGeneration.configured && imageGeneration.model !== void 0 ? t("model.image_gen_configured", { model: imageGeneration.model }) : t("model.image_gen_missing")));
133117
+ }
132778
133118
  lines.push("");
132779
133119
  if (view.page.pageCount > 1) lines.push(chalk.hex(colors.textMuted)(` Page ${String(view.page.page + 1)}/${String(view.page.pageCount)}`));
132780
133120
  lines.push(chalk.hex(colors.primary)("─".repeat(width)));
@@ -133309,26 +133649,6 @@ const IMAGE_PROVIDER_PRESETS = [
133309
133649
  model: "gpt-image-2"
133310
133650
  }
133311
133651
  ];
133312
- function getImageConfigPath() {
133313
- return join(resolveScreamHome(), "image-config.json");
133314
- }
133315
- /** Best-effort read of an existing image config (never throws, never exposes the key). */
133316
- async function readExistingImageConfig() {
133317
- try {
133318
- const text = await readFile(getImageConfigPath(), "utf8");
133319
- const parsed = JSON.parse(text);
133320
- const urlDirect = typeof parsed["url"] === "string" ? parsed["url"].trim() : "";
133321
- const legacyBase = typeof parsed["base_url"] === "string" ? parsed["base_url"].trim() : "";
133322
- const url = urlDirect.length > 0 ? urlDirect : legacyBase.length > 0 ? `${legacyBase.replace(/\/+$/, "")}/images/generations` : "";
133323
- const model = typeof parsed["model"] === "string" ? parsed["model"].trim() : "";
133324
- const apiKey = typeof parsed["api_key"] === "string" ? parsed["api_key"].trim() : "";
133325
- if (url.length > 0 && !url.includes("replace-with") && !(url.startsWith("<") && url.endsWith(">")) && model.length > 0 && apiKey.length > 0 && !apiKey.includes("replace-with") && !(apiKey.startsWith("<") && apiKey.endsWith(">"))) return {
133326
- provider: typeof parsed["provider"] === "string" ? parsed["provider"] : "openai-compatible",
133327
- model,
133328
- url
133329
- };
133330
- } catch {}
133331
- }
133332
133652
  /**
133333
133653
  * Normalize the URL exactly as the user typed it: trim whitespace and
133334
133654
  * trailing slashes only. Nothing is ever appended or completed — the full
@@ -135889,6 +136209,10 @@ function parseColorFgBg(value) {
135889
136209
  */
135890
136210
  const dark = {
135891
136211
  yellowGreen: "#79eb00",
136212
+ gold: "#FFD700",
136213
+ coral: "#FF7043",
136214
+ skyBlue: "#9CDCFE",
136215
+ violet: "#C084FC",
135892
136216
  pink400: "#FF6B9D",
135893
136217
  cyan400: "#56D4DD",
135894
136218
  amber400: "#E8A838",
@@ -135905,6 +136229,10 @@ const dark = {
135905
136229
  };
135906
136230
  const light = {
135907
136231
  yellowGreen700: "#4B7A06",
136232
+ gold: "#A16207",
136233
+ orange: "#C2410C",
136234
+ blue: "#1565C0",
136235
+ violet: "#7C3AED",
135908
136236
  pink700: "#C2185B",
135909
136237
  cyan800: "#006978",
135910
136238
  gray900: "#1A1A1A",
@@ -135921,14 +136249,19 @@ const darkColors = {
135921
136249
  accent: dark.pink400,
135922
136250
  planMode: dark.cyan400,
135923
136251
  fusionPlanMode: dark.amber500,
135924
- wolfpackMode: "#C084FC",
135925
- rlmMode: "#FFD700",
136252
+ wolfpackMode: dark.violet,
136253
+ rlmMode: dark.gold,
136254
+ permissionManual: dark.yellowGreen,
136255
+ permissionYolo: dark.gold,
136256
+ permissionAuto: dark.coral,
136257
+ permissionAsk: dark.skyBlue,
136258
+ permissionBot: dark.violet,
135926
136259
  text: dark.gray100,
135927
136260
  textStrong: dark.gray50,
135928
136261
  textDim: dark.gray500,
135929
136262
  textMuted: dark.gray600,
135930
136263
  mdLink: "#56B6C2",
135931
- mdCodeBlock: "#9CDCFE",
136264
+ mdCodeBlock: dark.skyBlue,
135932
136265
  mdCodeBlockBorder: "#5C6370",
135933
136266
  mdCodeBlockBg: "#16191f",
135934
136267
  mdQuote: "#7F848E",
@@ -135956,14 +136289,19 @@ const lightColors = {
135956
136289
  accent: light.pink700,
135957
136290
  planMode: light.cyan800,
135958
136291
  fusionPlanMode: light.amber700,
135959
- wolfpackMode: "#7C3AED",
135960
- rlmMode: "#A16207",
136292
+ wolfpackMode: light.violet,
136293
+ rlmMode: light.gold,
136294
+ permissionManual: light.yellowGreen700,
136295
+ permissionYolo: light.gold,
136296
+ permissionAuto: light.orange,
136297
+ permissionAsk: light.blue,
136298
+ permissionBot: light.violet,
135961
136299
  text: light.gray900,
135962
136300
  textStrong: light.gray900,
135963
136301
  textDim: light.gray700,
135964
136302
  textMuted: light.gray600,
135965
136303
  mdLink: "#007A8A",
135966
- mdCodeBlock: "#1565C0",
136304
+ mdCodeBlock: light.blue,
135967
136305
  mdCodeBlockBorder: "#848484",
135968
136306
  mdCodeBlockBg: "#f7f9fb",
135969
136307
  mdQuote: "#616161",
@@ -137282,14 +137620,14 @@ async function handleModelCommand(host, args) {
137282
137620
  const alias = trimmed;
137283
137621
  if (alias.length === 0) {
137284
137622
  await refreshModelsForPicker(host);
137285
- showModelPicker(host);
137623
+ await showModelPicker(host);
137286
137624
  return;
137287
137625
  }
137288
137626
  if (host.state.appState.availableModels[alias] === void 0) {
137289
137627
  host.showError(`Unknown model alias: ${alias}`);
137290
137628
  return;
137291
137629
  }
137292
- showModelPicker(host, alias);
137630
+ await showModelPicker(host, alias);
137293
137631
  }
137294
137632
  /**
137295
137633
  * Reload provider/model config before showing the model picker so models
@@ -137349,11 +137687,12 @@ async function applyEditorChoice(host, value) {
137349
137687
  host.setAppState({ editorCommand });
137350
137688
  host.showStatus(value.length > 0 ? `Editor set to "${value}".` : "编辑器设置为自动检测 ($VISUAL / $EDITOR)。");
137351
137689
  }
137352
- function showModelPicker(host, selectedValue = host.state.appState.model) {
137690
+ async function showModelPicker(host, selectedValue = host.state.appState.model) {
137353
137691
  if (Object.entries(host.state.appState.availableModels).length === 0) {
137354
137692
  host.showNotice("未配置模型", "运行 /config 自定义模型配置。");
137355
137693
  return;
137356
137694
  }
137695
+ const imageConfig = await readExistingImageConfig();
137357
137696
  host.mountEditorReplacement(new ModelSelectorComponent({
137358
137697
  models: host.state.appState.availableModels,
137359
137698
  currentValue: host.state.appState.model,
@@ -137361,6 +137700,10 @@ function showModelPicker(host, selectedValue = host.state.appState.model) {
137361
137700
  currentThinkingLevel: host.state.appState.thinkingLevel,
137362
137701
  colors: host.state.theme.colors,
137363
137702
  searchable: true,
137703
+ imageGeneration: imageConfig === void 0 ? { configured: false } : {
137704
+ configured: true,
137705
+ model: imageConfig.model
137706
+ },
137364
137707
  onSelect: ({ alias, thinkingLevel }) => {
137365
137708
  host.restoreEditor();
137366
137709
  performModelSwitch(host, alias, thinkingLevel);
@@ -138410,7 +138753,7 @@ async function guidedGoalSetup(host) {
138410
138753
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
138411
138754
  return;
138412
138755
  }
138413
- const { TextInputDialogComponent } = await import("./text-input-dialog-HfsxLcQf.mjs");
138756
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DZoGkbYZ.mjs");
138414
138757
  const initialDesc = await promptText(host, TextInputDialogComponent, {
138415
138758
  title: t("goal.setup_title_initial"),
138416
138759
  subtitle: t("goal.setup_desc_hint"),
@@ -138431,7 +138774,7 @@ async function guidedGoalSetup(host) {
138431
138774
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
138432
138775
  }
138433
138776
  async function showGoalConfigWizard(host, session, objective, replace) {
138434
- const { TextInputDialogComponent } = await import("./text-input-dialog-HfsxLcQf.mjs");
138777
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DZoGkbYZ.mjs");
138435
138778
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
138436
138779
  title: t("goal.wizard_title", { objective }),
138437
138780
  subtitle: t("goal.budget_turns_hint"),
@@ -141621,24 +141964,259 @@ function trimTrailingEmptyLines(lines) {
141621
141964
  return lines.slice(0, end);
141622
141965
  }
141623
141966
  /**
141624
- * Returns the tail of `text` whose UTF-8 byte length is at most `maxBytes`.
141967
+ * Returns the tail of `text` whose UTF-8 byte length is at most `maxBytes`
141968
+ * (plus one byte when a torn sequence's introducer ESC sits immediately before
141969
+ * the cut and the sequence's remainder provably closes — keeping the ESC makes
141970
+ * the sequence whole and hides its fragment), with the head aligned to a
141971
+ * boundary no ANSI escape sequence spans.
141625
141972
  * Iterates by Unicode code points so multi-byte characters and surrogate
141626
- * pairs are never split.
141973
+ * pairs are never split, then hands the cut to `safeTailStart`: success output
141974
+ * keeps its ANSI codes (only error output is stripped), and a raw byte cut
141975
+ * could land inside a sequence, leaving a fragment (`m`, `0m`, `[31`) that
141976
+ * `sanitizeShellOutput` cannot recognize because its ESC byte was cut away.
141627
141977
  */
141628
141978
  function truncateTailBytes(text, maxBytes) {
141629
141979
  if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
141630
141980
  const chars = Array.from(text);
141631
- let start = chars.length;
141632
141981
  let bytes = 0;
141982
+ let units = 0;
141633
141983
  for (let i = chars.length - 1; i >= 0; i--) {
141634
141984
  const char = chars[i];
141635
141985
  if (char === void 0) continue;
141636
141986
  const charBytes = Buffer.byteLength(char, "utf8");
141637
141987
  if (bytes + charBytes > maxBytes) break;
141638
141988
  bytes += charBytes;
141639
- start = i;
141989
+ units += char.length;
141990
+ }
141991
+ return text.slice(safeTailStart(text, text.length - units));
141992
+ }
141993
+ const ESC = "\x1B";
141994
+ /**
141995
+ * Largest share of a retained tail the boundary rules may discard.
141996
+ *
141997
+ * Both rules trade content for a head that is whole by construction: rule 1
141998
+ * restarts after the next `\n` and drops the torn line, and rule 2 skips the
141999
+ * remainder of a torn sequence. The trade only pays while the tail keeps a
142000
+ * (non-strict) majority — with one 5000-char line ahead, a 100-byte budget
142001
+ * would come back with the few bytes after it, and the reader asked for the
142002
+ * newest output, not for a tidy head. Past the bound the raw cut keeps the
142003
+ * content: a string payload stays visible, and a torn CSI or two-byte escape
142004
+ * run keeps the fragment it left in view, because skipping that remainder
142005
+ * would cost more than the fragment it hides — unless the introducer ESC sits
142006
+ * immediately before the head *and* the sequence provably closes, where one
142007
+ * unit of look-back keeps the sequence whole and the fragment never surfaces.
142008
+ * An unclosed run keeps the raw cut: with no terminator to hand the sanitizer,
142009
+ * backing onto its ESC would surface a bare ESC the sanitizer cannot strip.
142010
+ */
142011
+ const MAX_TAIL_DROP_SHARE = .5;
142012
+ /**
142013
+ * Move a tail cut point forward to a boundary no ANSI escape sequence spans.
142014
+ *
142015
+ * A raw `slice(-tail)` can land inside a sequence (`…\u001b[31|m…`), leaving a
142016
+ * fragment (`m`, `0m`, `[31`) whose ESC byte was cut away —
142017
+ * `sanitizeShellOutput` only recognizes complete sequences, so the fragment
142018
+ * renders as text. Two provable rules keep the head clean:
142019
+ *
142020
+ * 1. Line alignment: a CSI sequence cannot contain `\n` (LF is not a
142021
+ * parameter, intermediate or final byte), so a cut that lands mid-line
142022
+ * restarts after the next `\n`. The only loss is the torn partial line,
142023
+ * whose head cannot be trusted; every escape after the newline is whole.
142024
+ * Bounded by `MAX_TAIL_DROP_SHARE`, so one huge line cannot eat the budget.
142025
+ * 2. Torn-sequence skip: when the retained text is one line (no `\n` after
142026
+ * the cut), or an OSC/DCS/PM/APC payload spans the newline the cut aligned
142027
+ * to, the dropped prefix is inspected for a sequence still open at the cut
142028
+ * — a run of CSI body bytes (0x20–0x3F) reaching back to `ESC [`, an `ESC`
142029
+ * whose intermediate bytes were torn (`ESC ( B`), or a string-sequence
142030
+ * opener the cut landed inside. The head then skips exactly the bytes a
142031
+ * terminal would consume for that sequence, so no visible text is dropped —
142032
+ * but only while that skip stays within `MAX_TAIL_DROP_SHARE` of the tail.
142033
+ * A CSI body or escape-intermediate run can be arbitrarily long, so past
142034
+ * the bound the head keeps the newest bytes and the fragment the raw cut
142035
+ * left with them — or one unit earlier, onto an introducer ESC sitting
142036
+ * immediately before the head, so the whole sequence survives and is
142037
+ * stripped (`withinDropShare`) — but only when the skip proved the sequence
142038
+ * closed: an unclosed run keeps the raw cut, because with no terminator to
142039
+ * hand the sanitizer the ESC would surface bare and swallow what follows
142040
+ * it. So does the head for a string that never ends or ended ahead of the
142041
+ * cut. A sliver of the tail is worse than a fragment.
142042
+ *
142043
+ * `cut` is always a code-point boundary here (the byte loop above works in
142044
+ * code points), so unlike its manager-side twin there is no surrogate half to
142045
+ * drop, and `stringPayloadHead`'s clamp steps back onto a code point as well;
142046
+ * the one-unit look-back onto an introducer ESC (`withinDropShare`) — taken
142047
+ * only for a skip that proved its sequence closed — is a code point boundary
142048
+ * too. Mirrors `safeTailStart` in
142049
+ * `packages/agent-core/src/tools/background/manager.ts` — app code cannot
142050
+ * import agent-core directly, so the rule lives in both places.
142051
+ */
142052
+ function safeTailStart(text, cut) {
142053
+ if (cut <= 0 || cut >= text.length) return cut;
142054
+ const tail = text.length - cut;
142055
+ let start = cut;
142056
+ if (text[start - 1] !== "\n") {
142057
+ const newline = text.indexOf("\n", start);
142058
+ if (newline !== -1 && newline + 1 - cut <= tail * MAX_TAIL_DROP_SHARE) start = newline + 1;
142059
+ }
142060
+ return skipOpenEscape(text, start);
142061
+ }
142062
+ /** CSI parameter (0x30–0x3F) / intermediate (0x20–0x2F) bytes — everything before the final byte. */
142063
+ function isCsiBody(code) {
142064
+ return code !== void 0 && code >= 32 && code <= 63;
142065
+ }
142066
+ /** CSI final byte (0x40–0x7E) — the sequence ends here. */
142067
+ function isCsiFinal(code) {
142068
+ return code !== void 0 && code >= 64 && code <= 126;
142069
+ }
142070
+ /** OSC / DCS / PM / APC introducers — string sequences ended by BEL or ST. */
142071
+ function isStringIntro(intro) {
142072
+ return intro === "]" || intro === "P" || intro === "^" || intro === "_";
142073
+ }
142074
+ /** ESC-intermediate byte (0x20–0x2F) of a two-byte escape sequence. */
142075
+ function isEscapeIntermediate(code) {
142076
+ return code !== void 0 && code >= 32 && code <= 47;
142077
+ }
142078
+ /** Final byte (0x30–0x7E) that closes a two-byte escape sequence. */
142079
+ function isEscapeFinal(code) {
142080
+ return code !== void 0 && code >= 48 && code <= 126;
142081
+ }
142082
+ /** True when every code point in the half-open range `[from, to)` is an ESC intermediate (0x20–0x2F). */
142083
+ function allEscapeIntermediates(text, from, to) {
142084
+ for (let i = from; i < to; i += 1) if (!isEscapeIntermediate(text.codePointAt(i))) return false;
142085
+ return true;
142086
+ }
142087
+ /**
142088
+ * When the dropped prefix proves an escape sequence is still open at `cut`,
142089
+ * return the index after its remainder; otherwise return `cut` unchanged.
142090
+ */
142091
+ function skipOpenEscape(text, cut) {
142092
+ let run = cut;
142093
+ while (run > 0 && isCsiBody(text.codePointAt(run - 1))) run -= 1;
142094
+ if (run >= 2 && text[run - 1] === "[" && text[run - 2] === ESC) {
142095
+ const skip = skipCsiRest(text, cut);
142096
+ return withinDropShare(text, cut, skip.end, skip.closed);
142097
+ }
142098
+ if (text[run - 1] === ESC && allEscapeIntermediates(text, run, cut)) return skipEscapeRest(text, cut);
142099
+ const open = text.lastIndexOf(ESC, cut - 1);
142100
+ if (open !== -1 && isStringIntro(text[open + 1])) return stringSequenceHead(text, open, cut);
142101
+ return cut;
142102
+ }
142103
+ /**
142104
+ * Keep a provable skip only while it stays within `MAX_TAIL_DROP_SHARE` of the
142105
+ * tail that starts at `from`: the skipped bytes are exactly what the reader
142106
+ * loses from the tail it asked for. A CSI body or escape-intermediate run can
142107
+ * be arbitrarily long, so past the bound the raw cut is worth more than the
142108
+ * clean head — except when the sequence's introducer ESC sits immediately
142109
+ * before the head (`…\u001b|[31m…`) *and* `closed` says the skip found the
142110
+ * sequence's terminator, where one unit of look-back keeps the sequence whole
142111
+ * instead: the sanitizer strips it and the visible text is the skip's, for
142112
+ * that one unit rather than a sliver of the tail. An unclosed run must keep
142113
+ * the raw cut even with that ESC one unit ahead: nothing terminates it, so
142114
+ * look-back would hand the reader a bare ESC whose sequence run a terminal
142115
+ * swallows along with the bytes behind it.
142116
+ */
142117
+ function withinDropShare(text, from, target, closed) {
142118
+ if (target - from <= (text.length - from) * MAX_TAIL_DROP_SHARE) return target;
142119
+ return closed && from > 0 && text[from - 1] === ESC ? from - 1 : from;
142120
+ }
142121
+ /**
142122
+ * The rest of a CSI sequence whose body was torn at `cut`, with the closure
142123
+ * its skip proved: `closed` holds only when a final byte (0x40–0x7E) was
142124
+ * actually found, so a parameter run that reaches the end of `text` — or stops
142125
+ * on a byte no CSI can end with — reports the sequence as open.
142126
+ */
142127
+ function skipCsiRest(text, cut) {
142128
+ let end = cut;
142129
+ while (end < text.length && isCsiBody(text.codePointAt(end))) end += 1;
142130
+ const closed = end < text.length && isCsiFinal(text.codePointAt(end));
142131
+ return {
142132
+ end: closed ? end + 1 : end,
142133
+ closed
142134
+ };
142135
+ }
142136
+ /** Index after the escape sequence whose introducer ESC sits at `cut - 1`. */
142137
+ function skipEscapeRest(text, cut) {
142138
+ const intro = text[cut];
142139
+ if (intro === "[") {
142140
+ const skip = skipCsiRest(text, cut + 1);
142141
+ return withinDropShare(text, cut, skip.end, skip.closed);
142142
+ }
142143
+ if (isStringIntro(intro)) return stringSequenceHead(text, cut - 1, cut + 1);
142144
+ let end = cut;
142145
+ while (end < text.length && isEscapeIntermediate(text.codePointAt(end))) end += 1;
142146
+ const closed = end < text.length && isEscapeFinal(text.codePointAt(end));
142147
+ return withinDropShare(text, cut, closed ? end + 1 : end, closed);
142148
+ }
142149
+ /**
142150
+ * Head index for a cut that lands inside the string sequence opened at `open`.
142151
+ *
142152
+ * `stringEnd` gives the byte a terminal stops at; skipping to it hides the
142153
+ * payload's remainder, which is what a terminal shows — but only while that
142154
+ * costs at most `MAX_TAIL_DROP_SHARE` of the tail. A long OSC 8 URL, or any
142155
+ * payload longer than the budget, would otherwise hand the reader a sliver of
142156
+ * the tail it asked for; a payload whose extent exceeds the budget stays
142157
+ * visible instead. A cut the string already ended ahead of needs no skip, and
142158
+ * an unterminated one must not swallow the tail (`stringPayloadHead`).
142159
+ */
142160
+ function stringSequenceHead(text, open, cut) {
142161
+ const drop = stringEnd(text, open + 2) - cut;
142162
+ if (drop >= 0 && drop <= (text.length - cut) * MAX_TAIL_DROP_SHARE) return cut + drop;
142163
+ return stringPayloadHead(text, open, cut);
142164
+ }
142165
+ /**
142166
+ * Head index that keeps a string sequence's payload visible, moving the cut
142167
+ * only past the opener's own bytes when the cut landed inside them.
142168
+ *
142169
+ * A terminal swallows an unterminated payload, but a viewer must never render
142170
+ * a blank body: a shell card whose output opens with an unterminated OSC — a
142171
+ * command killed mid-title, a truncated write — would otherwise show nothing
142172
+ * at all. The head stays at `cut` unless the cut landed inside the opener
142173
+ * (`\u001b]0;` would leave `0;` in front of the payload), and never reaches
142174
+ * `text.length` — the clamp to the last unit steps back onto the pair's first
142175
+ * half when that unit is a trailing surrogate pair's low half (defensive here:
142176
+ * `truncateTailBytes` cuts on a code point boundary, so the clamp cannot land
142177
+ * between a pair's halves in practice) — so this rule never empties the tail;
142178
+ * the tail comes back empty only when the byte budget fits none of the last
142179
+ * character.
142180
+ */
142181
+ function stringPayloadHead(text, open, cut) {
142182
+ const head = Math.min(Math.max(cut, stringPayloadStart(text, open)), text.length - 1);
142183
+ const code = text.codePointAt(head);
142184
+ return head > 0 && code !== void 0 && code >= 56320 && code <= 57343 ? head - 1 : head;
142185
+ }
142186
+ /**
142187
+ * Index after the byte that ends the string sequence whose payload starts at
142188
+ * `from` — its BEL or `ESC \` terminator, or a CAN (0x18) / SUB (0x1A), which
142189
+ * a terminal reads as "string over, resume normal parsing". `-1` when the
142190
+ * string never ends.
142191
+ */
142192
+ function stringEnd(text, from) {
142193
+ for (let i = from; i < text.length; i += 1) {
142194
+ const code = text.codePointAt(i);
142195
+ if (code === 7) return i + 1;
142196
+ if (code === 27 && text[i + 1] === "\\") return i + 2;
142197
+ if (code === 24 || code === 26) return i + 1;
141640
142198
  }
141641
- return chars.slice(start).join("");
142199
+ return -1;
142200
+ }
142201
+ /**
142202
+ * Index where the payload of the string sequence opened at `open` starts: for
142203
+ * OSC after `ESC ] Ps ;`, for DCS/PM/APC after their parameter and
142204
+ * intermediate bytes and the final byte that opens the payload.
142205
+ */
142206
+ function stringPayloadStart(text, open) {
142207
+ let i = open + 2;
142208
+ if (text[open + 1] === "]") {
142209
+ while (i < text.length && isOscParam(text.codePointAt(i))) i += 1;
142210
+ if (text[i] === ";") i += 1;
142211
+ return i;
142212
+ }
142213
+ while (i < text.length && isCsiBody(text.codePointAt(i))) i += 1;
142214
+ const final = text.codePointAt(i);
142215
+ return i < text.length && isCsiFinal(final) ? i + 1 : i;
142216
+ }
142217
+ /** OSC numeric parameter byte (0x30–0x39) — the `Ps` before the `;` separator. */
142218
+ function isOscParam(code) {
142219
+ return code !== void 0 && code >= 48 && code <= 57;
141642
142220
  }
141643
142221
  /**
141644
142222
  * Component that renders tool output with wrap-aware line truncation.
@@ -143863,6 +144441,7 @@ async function handleRevokeCommand(host, args = "") {
143863
144441
  const preservedEntries = entries.slice(lastUserIndex).filter((entry) => !isRevokeContextEntry(entry));
143864
144442
  entries.splice(lastUserIndex, entries.length - lastUserIndex, ...preservedEntries);
143865
144443
  if (entries.length === 0) renderWelcome(host);
144444
+ host.state.ui.resetTextSelection();
143866
144445
  host.state.ui.requestRender();
143867
144446
  }
143868
144447
  function parseRevokeCount(args) {
@@ -146547,12 +147126,14 @@ function showValuePicker(host, key) {
146547
147126
  onSelect: (value) => {
146548
147127
  const parsed = Number(value);
146549
147128
  if (!Number.isFinite(parsed)) return;
147129
+ const resized = parsed !== getActivityLineValue(key);
146550
147130
  setActivityLineValue(key, parsed);
146551
147131
  host.restoreEditor();
146552
147132
  host.showStatus(t("blockrows.saved", {
146553
147133
  label: t(setting.label),
146554
147134
  value: String(parsed)
146555
147135
  }), host.state.theme.colors.success);
147136
+ if (resized) host.state.ui.resetTextSelection();
146556
147137
  repaintTranscript(host);
146557
147138
  },
146558
147139
  onCancel: () => {
@@ -147135,8 +147716,10 @@ function buildOptions() {
147135
147716
  }
147136
147717
  /** Apply a choice: redraw what is already on screen, restate the capability. */
147137
147718
  async function applyDisplay(host, value) {
147719
+ const previous = getMermaidDisplay();
147138
147720
  setMermaidDisplay(value);
147139
147721
  host.showStatus(t("mermaid.applied", { mode: optionLabel(value) }), value === "off" ? host.state.theme.colors.textDim : host.state.theme.colors.success);
147722
+ if (previous !== value && (previous === "off" || value === "off")) host.state.ui.resetTextSelection();
147140
147723
  repaintTranscript(host);
147141
147724
  await syncDiagramCapabilityPrompt(host.session);
147142
147725
  }
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-DiPC-zEu.mjs")).main();
9
+ (await import("./app-BuWHcggN.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);
@@ -602,8 +602,15 @@ const dictionaries = {
602
602
  "model.not_supported": "✗ 不支持",
603
603
  "model.video": "视频",
604
604
  "model.audio": "音频",
605
+ "model.image_gen_configured": " 生图模型:{model} · 已配置(/config image 修改)",
606
+ "model.image_gen_missing": " 生图模型:未配置 · /config image 设置",
605
607
  "help.toggle_plan": "切换计划模式",
606
608
  "help.toggle_output": "切换工具输出展开",
609
+ "help.cycle_thinking": "空输入时循环思考档位",
610
+ "help.toggle_plan_view": "展开/收起计划",
611
+ "help.toggle_sidebar": "开关侧边面板",
612
+ "help.toggle_hint": "隐藏/显示空会话提示",
613
+ "help.cancel_memory": "取消挂起的记忆提取",
607
614
  "help.interrupt": "中途干预 — 在流式传输中插入后续提示",
608
615
  "help.newline": "插入换行",
609
616
  "help.external_editor": "在外部编辑器中编辑 ($VISUAL / $EDITOR)",
@@ -615,7 +622,7 @@ const dictionaries = {
615
622
  "help.title": " 帮助 ",
616
623
  "help.close_hint": "Esc / Enter / q 关闭 · ↑↓ 滚动",
617
624
  "help.welcome_msg": "Scream 已准备好帮助您!发送消息即可开始。",
618
- "help.hidden_commands": "/config diy 自定义模型服务商 /model diy 多代理编排配置",
625
+ "help.hidden_commands": "/config diy 自定义模型服务商 /config image 生图配置 /model diy 多代理编排配置",
619
626
  "help.shortcuts": "键盘快捷键",
620
627
  "help.slash_commands": "斜杠命令",
621
628
  "help.showing_range": "显示 {start}-{end} / {total}",
@@ -1930,8 +1937,15 @@ const dictionaries = {
1930
1937
  "model.not_supported": "✗ Not supported",
1931
1938
  "model.video": "Video",
1932
1939
  "model.audio": "Audio",
1940
+ "model.image_gen_configured": " Image model: {model} · configured (/config image to change)",
1941
+ "model.image_gen_missing": " Image model: not configured · set it with /config image",
1933
1942
  "help.toggle_plan": "Toggle plan mode",
1934
1943
  "help.toggle_output": "Toggle tool output expansion",
1944
+ "help.cycle_thinking": "Cycle thinking effort (empty input)",
1945
+ "help.toggle_plan_view": "Expand / collapse the plan",
1946
+ "help.toggle_sidebar": "Toggle the sidebar panel",
1947
+ "help.toggle_hint": "Toggle the empty-session hint",
1948
+ "help.cancel_memory": "Cancel the pending memory extraction",
1935
1949
  "help.interrupt": "Interrupt — insert follow-up prompt during streaming",
1936
1950
  "help.newline": "Insert newline",
1937
1951
  "help.external_editor": "Edit in external editor ($VISUAL / $EDITOR)",
@@ -1943,7 +1957,7 @@ const dictionaries = {
1943
1957
  "help.title": " Help ",
1944
1958
  "help.close_hint": "Esc / Enter / q Close · ↑↓ Scroll",
1945
1959
  "help.welcome_msg": "Scream is ready to help! Send a message to get started.",
1946
- "help.hidden_commands": "/config diy Custom model provider /model diy Multi-agent orchestration",
1960
+ "help.hidden_commands": "/config diy Custom model provider /config image Image generation /model diy Multi-agent orchestration",
1947
1961
  "help.shortcuts": "Keyboard Shortcuts",
1948
1962
  "help.slash_commands": "Slash Commands",
1949
1963
  "help.showing_range": "Showing {start}-{end} / {total}",
@@ -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 { t as TextInputDialogComponent } from "./text-input-dialog-BlSqBYxO.mjs";
6
+ import { t as TextInputDialogComponent } from "./text-input-dialog-CK6jTftM.mjs";
7
7
  export { TextInputDialogComponent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scream-code",
3
- "version": "0.17.9",
3
+ "version": "0.18.0",
4
4
  "description": "A terminal-native AI agent for builders",
5
5
  "license": "MIT",
6
6
  "author": "ScreamCli",
@@ -57,10 +57,10 @@
57
57
  "bench:startup": "node scripts/startup-bench.mjs"
58
58
  },
59
59
  "dependencies": {
60
- "@earendil-works/pi-codemode": "^1.0.4",
60
+ "@earendil-works/pi-codemode": "^1.1.0",
61
61
  "@fontsource-variable/inter": "^5.3.0",
62
62
  "@fontsource-variable/jetbrains-mono": "^5.3.0",
63
- "@liutod-scream/pi-tui": "^1.0.4",
63
+ "@liutod-scream/pi-tui": "^1.1.0",
64
64
  "@mariozechner/clipboard": "^0.3.2",
65
65
  "chalk": "^5.4.1",
66
66
  "cli-highlight": "^2.1.11",