scream-code 0.16.7 → 0.16.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
  </p>
20
20
 
21
21
  <p align="center">
22
- <img width="1007" height="890" alt="企业微信20260909-170226@2x" src="https://github.com/user-attachments/assets/a8c1beda-3e73-41d1-855e-7dd80a3cc0e9" />
22
+ <img width="2880" height="2880" alt="scream" src="https://github.com/user-attachments/assets/7268e848-b1b8-4540-9627-d21c94332229" />
23
23
  </p>
24
24
 
25
25
  ---
@@ -4,8 +4,9 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
- import { $n as normalizeWorkDir, $t as lerpGradient, A as highlightLines, An as getKnowledgeStore, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as getDataDir, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getNoActiveSessionMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as isExperimentalFlagEnabled, Dt as createEditorTheme, E as UserMessageComponent, En as sortSlashCommands, Et as resolveThemeSync, F as StatusMessageComponent, Fn as TuiConfigParseError, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_UI_MODE, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getLogDir, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiLikePreferencesSchema, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as DEFAULT_CATALOG_URL, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_USER_AGENT_PRODUCT, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as loadTuiConfig, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as startManualEmbeddingDownload, Mt as createThemeStyles, N as ThinkingComponent, Nn as PIXEL_PULSE_FRAMES, Nt as contrastTextHex, O as renderDiffLines, On as setExperimentalFlags, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PULSE_WAVE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as encodeWorkDirKey, Qt as FooterComponent, R as SkillActivationComponent, Rn as saveTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getLlmNotSetMessage, St as supportsBalance, Tn as BUILTIN_SLASH_COMMANDS, U as getSharedSpeedTracker, Un as detectInstallSource, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getInputHistoryFile, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as CLI_COMMAND_NAME, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as saveCatalogCache, Xt as isBusy, Y as resetBreathingClock, Yn as fetchCatalog, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as ScreamHarness, Zt as isStreaming, _n as MAIN_AGENT_ID, a as syncDiagramCapabilityPrompt, an as printableChar, ar as MemoryMemoStore, at as toTerminalHyperlink, b as readUpdateCache, bn as getCtrlCHint, c as disposeChildren, cn as argsRecord, cr as resolveGlobalLogPath, d as isPlanExpandable, dn as parseStreamingArgs, dr as ErrorCodes, en as SubagentSlots, er as appendSessionIndexEntry, f as MoonLoader, fn as serializeToolResultOutput, fr as SCREAM_ERROR_INFO, ft as handleFusionPlanCommand, gn as EXIT_CONFIRM_WINDOW_MS, hn as EMPTY_SESSION_HINT_URL, i as buildRoleAdditionalText, ir as resolveScreamHome, it as isInternalMessage, j as langFromPath, jn as hasEmbeddingModelCache, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as embeddingFailureDetail, kt as isEmptySessionHintDismissed, l as hasDispose, ln as formatErrorMessage, lr as isScreamError, mn as truncateErrorMessage, mt as handlePlanCommand, n as clearEvalPanelState, nr as removeSessionIndexEntry, on as STATUS_BULLET, or as flushDiagnosticLogs, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as stringValue, q as BREATHE_CYCLE_MS, qn as PRODUCT_NAME, qt as TERMINAL_FOCUS_OUT, r as openUrl, sn as appendStreamingArgsPreview, sr as log, st as getModelCycleLevel, t as dispatchInput, tr as readSessionIndex, u as isExpandable, un as isTodoItemShape, ur as isOrphanedToolCallError, v as selectUpdateTarget, vn as SESSION_TIPS, w as getDaemonInstructions, wn as buildSkillSlashCommands, x as appendJsonlLine, xn as getCtrlDHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as TIP_ROTATION_INTERVAL_MS, z as AssistantMessageComponent, zn as detectShellEnvironment, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-Vw9ccFm7.mjs";
8
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-D9ByqSsK.mjs";
7
+ import { k as MemoryMemoStore } from "./src-BhZujafw.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 log, d as isPlanExpandable, dn as isTodoItemShape, dr as isOrphanedToolCallError, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as ErrorCodes, 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 resolveGlobalLogPath, mn as stringValue, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as resolveScreamHome, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, pr as SCREAM_ERROR_INFO, 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 flushDiagnosticLogs, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as isScreamError, 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-CIOuINvn.mjs";
9
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-XT3cOEjD.mjs";
9
10
  import { createRequire } from "node:module";
10
11
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
11
12
  import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -14,10 +15,10 @@ import { WebSocket, WebSocketServer } from "ws";
14
15
  import { appendFileSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
15
16
  import * as path$1 from "node:path";
16
17
  import path, { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
18
+ import { homedir, networkInterfaces, tmpdir } from "node:os";
17
19
  import { z } from "zod";
18
20
  import { exec, execFile, execSync, spawn, spawnSync } from "node:child_process";
19
21
  import { promisify } from "node:util";
20
- import { homedir, networkInterfaces, tmpdir } from "node:os";
21
22
  import { fileURLToPath, pathToFileURL } from "node:url";
22
23
  import { createServer } from "node:http";
23
24
  import process$1 from "node:process";
@@ -32,7 +33,7 @@ function optionalBuildString(value) {
32
33
  return typeof value === "string" && value.length > 0 ? value : void 0;
33
34
  }
34
35
  const SCREAM_BUILD_INFO = {
35
- version: optionalBuildString("0.16.7"),
36
+ version: optionalBuildString("0.16.9"),
36
37
  channel: optionalBuildString(""),
37
38
  commit: optionalBuildString(""),
38
39
  buildTarget: optionalBuildString("darwin-arm64")
@@ -1675,20 +1676,33 @@ function resolveEditorCommand(configured) {
1675
1676
  for (const c of candidates) if (typeof c === "string" && c.trim().length > 0) return c.trim();
1676
1677
  }
1677
1678
  /**
1678
- * Launch `command` (tokenised via a shell) against a temp file seeded
1679
- * with `initialText`. Returns the edited contents on success, or
1680
- * `undefined` if the editor exited non-zero / the file disappeared.
1679
+ * Launch `command` (tokenised by the platform's command interpreter) against
1680
+ * a temp file seeded with `initialText`. Returns the edited contents on
1681
+ * success, or `undefined` if the editor exited non-zero / the file
1682
+ * disappeared.
1681
1683
  *
1682
- * The command is passed to `/bin/sh -c "<cmd> <tmpfile>"` so users can
1683
- * supply argv-style strings like `"code --wait"` or `"nvim +set ft=markdown"`.
1684
+ * The command may carry argv-style arguments (`"code --wait"`,
1685
+ * `"nvim +set ft=markdown"`), so it is handed to the interpreter the host
1686
+ * platform actually has: `/bin/sh -c` on POSIX, `cmd.exe /d /s /c` on
1687
+ * Windows. The two disagree on quoting, so each branch carries its own
1688
+ * escaping rather than borrowing the other's. `platform` is injectable so
1689
+ * both branches stay testable off their native host.
1684
1690
  */
1685
- async function editInExternalEditor(initialText, command) {
1691
+ async function editInExternalEditor(initialText, command, platform = process.platform) {
1686
1692
  const dir = await mkdtemp(join(tmpdir(), "scream-edit-"));
1687
1693
  const file = join(dir, "prompt.md");
1688
1694
  await writeFile(file, initialText, "utf-8");
1689
1695
  try {
1690
1696
  if (await new Promise((resolve, reject) => {
1691
- const child = spawn("/bin/sh", ["-c", `${command} ${shellQuote(file)}`], { stdio: "inherit" });
1697
+ const child = platform === "win32" ? spawn(windowsCommandShell(), [
1698
+ "/d",
1699
+ "/s",
1700
+ "/c",
1701
+ `"${command} ${cmdQuote(file)}"`
1702
+ ], {
1703
+ stdio: "inherit",
1704
+ windowsVerbatimArguments: true
1705
+ }) : spawn("/bin/sh", ["-c", `${command} ${shellQuote(file)}`], { stdio: "inherit" });
1692
1706
  child.on("exit", (c) => {
1693
1707
  resolve(c ?? 0);
1694
1708
  });
@@ -1702,9 +1716,18 @@ async function editInExternalEditor(initialText, command) {
1702
1716
  }).catch(() => {});
1703
1717
  }
1704
1718
  }
1719
+ /** The interpreter `cmd.exe`-style command lines run through, as Node's own
1720
+ * `shell: true` resolves it on Windows. */
1721
+ function windowsCommandShell() {
1722
+ const comspec = process.env["ComSpec"];
1723
+ return comspec !== void 0 && comspec.trim().length > 0 ? comspec : "cmd.exe";
1724
+ }
1705
1725
  function shellQuote(path) {
1706
1726
  return `'${path.replaceAll("'", "'\\''")}'`;
1707
1727
  }
1728
+ function cmdQuote(path) {
1729
+ return `"${path.replaceAll("\"", "\"\"")}"`;
1730
+ }
1708
1731
  //#endregion
1709
1732
  //#region src/tui/controllers/editor-keyboard.ts
1710
1733
  var EditorKeyboardController = class {
@@ -1877,7 +1900,7 @@ var EditorKeyboardController = class {
1877
1900
  if (media === null) return;
1878
1901
  const meta = parseImageMeta(media.bytes);
1879
1902
  if (meta === null) return;
1880
- const attachment = this.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height);
1903
+ const attachment = this.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height, path);
1881
1904
  this.host.state.editor.insertTextAtCursor?.(`${attachment.placeholder} `);
1882
1905
  this.host.state.ui.requestRender();
1883
1906
  }
@@ -3164,7 +3187,8 @@ var SessionEventHandler = class {
3164
3187
  this.host.streamingUI.resetToolUi();
3165
3188
  this.host.streamingUI.finalizeLiveTextBuffers();
3166
3189
  this.host.streamingUI.endActivityGroup();
3167
- this.host.showError(`[${event.code}] ${event.message}`);
3190
+ const shown = appendSessionHeaderHint(`[${event.code}] ${event.message}`, (name) => t("error.session_header_hint", { name }));
3191
+ this.host.showError(shown);
3168
3192
  if (event.code === "turn.agent_busy" && !hadActiveTurn && this.host.state.appState.streamingPhase !== "idle") {
3169
3193
  this.host.setAppState({ streamingPhase: "idle" });
3170
3194
  this.host.resetLivePane();
@@ -3787,7 +3811,7 @@ function createReplayRenderContext() {
3787
3811
  }
3788
3812
  function limitReplayRecordsByTurn(records, maxTurns) {
3789
3813
  if (maxTurns <= 0) return [];
3790
- const turnStarts = records.flatMap((record, index) => isReplayUserTurnRecord(record) ? [index] : []);
3814
+ const turnStarts = records.flatMap((record, index) => record.type === "message" && isRealUserPrompt(record.message) ? [index] : []);
3791
3815
  if (turnStarts.length <= maxTurns) return records;
3792
3816
  return records.slice(turnStarts[turnStarts.length - maxTurns]);
3793
3817
  }
@@ -3862,23 +3886,6 @@ function formatHookResultMessageForTranscript(text, fallbackEvent, blocked) {
3862
3886
  if (results.length === 0 || text.slice(lastIndex).trim().length > 0) return formatHookResultBlock(fallbackEvent, text, blocked);
3863
3887
  return results.map(({ event, body }) => formatHookResultBlock(event, body, blocked)).join("\n\n");
3864
3888
  }
3865
- function isReplayUserTurnRecord(record) {
3866
- if (record.type !== "message") return false;
3867
- const { message } = record;
3868
- if (message.role !== "user") return false;
3869
- switch (message.origin?.kind) {
3870
- case void 0:
3871
- case "user": return true;
3872
- case "skill_activation": return message.origin.trigger === "user-slash";
3873
- case "background_task":
3874
- case "compaction_summary":
3875
- case "cron_job":
3876
- case "cron_missed":
3877
- case "hook_result":
3878
- case "injection":
3879
- case "system_trigger": return false;
3880
- }
3881
- }
3882
3889
  function parseReplayToolArguments(value) {
3883
3890
  if (value === null || value.length === 0) return {};
3884
3891
  try {
@@ -4621,6 +4628,7 @@ var StreamingUIController = class StreamingUIController {
4621
4628
  completeToolResult(toolCallId, result) {
4622
4629
  const matchedCall = this._activeToolCalls.get(toolCallId);
4623
4630
  if (matchedCall !== void 0) this.onToolCallEnd(toolCallId, result);
4631
+ else if (this._pendingToolComponents.has(toolCallId)) this.onToolCallEnd(toolCallId, result);
4624
4632
  this._activeToolCalls.delete(toolCallId);
4625
4633
  this._streamingToolCallArguments.delete(toolCallId);
4626
4634
  return matchedCall;
@@ -6246,10 +6254,28 @@ function cronDetail(data) {
6246
6254
  }
6247
6255
  //#endregion
6248
6256
  //#region src/tui/utils/image-attachment-store.ts
6257
+ /**
6258
+ * Registry for media pasted into the input box.
6259
+ *
6260
+ * Each paste produces an `ImageAttachment` with an auto-incrementing id
6261
+ * or `VideoAttachment` with a human-readable placeholder (`[image #1
6262
+ * (640×480)]` / `[video #2 sample.mov]`). The placeholder is what the
6263
+ * user sees in the input field; on submit, `extractMediaAttachments`
6264
+ * walks the text and expands image placeholders to image content parts
6265
+ * and video placeholders to file-path tags for `ReadMediaFile`.
6266
+ *
6267
+ * Scope is per-`ScreamTUI` instance. Reloads (`/new`, `/clear`,
6268
+ * session switch) call `clear()` so ids restart from 1 and stale
6269
+ * prompt attachments are dropped. We intentionally do NOT persist
6270
+ * attachments across sessions — coding-agent doesn't either, and
6271
+ * `--resume` wouldn't know how to materialize the files anyway.
6272
+ */
6273
+ /** Temp-file basename prefix used by path-mode image materialization. */
6274
+ const TEMP_ATTACHMENT_PREFIX = "scream-attachment-";
6249
6275
  var ImageAttachmentStore = class {
6250
6276
  nextId = 1;
6251
6277
  byId = /* @__PURE__ */ new Map();
6252
- addImage(bytes, mime, width, height) {
6278
+ addImage(bytes, mime, width, height, sourcePath) {
6253
6279
  const id = this.nextId;
6254
6280
  this.nextId += 1;
6255
6281
  const attachment = {
@@ -6259,7 +6285,8 @@ var ImageAttachmentStore = class {
6259
6285
  mime,
6260
6286
  width,
6261
6287
  height,
6262
- placeholder: formatPlaceholder(id, width, height)
6288
+ placeholder: formatPlaceholder(id, width, height),
6289
+ ...sourcePath !== void 0 && sourcePath.length > 0 ? { sourcePath } : {}
6263
6290
  };
6264
6291
  this.byId.set(id, attachment);
6265
6292
  return attachment;
@@ -6285,6 +6312,14 @@ var ImageAttachmentStore = class {
6285
6312
  return this.byId.get(id);
6286
6313
  }
6287
6314
  clear() {
6315
+ const tempBase = join(tmpdir(), TEMP_ATTACHMENT_PREFIX);
6316
+ for (const attachment of this.byId.values()) {
6317
+ if (attachment.kind !== "image") continue;
6318
+ const path = attachment.sourcePath;
6319
+ if (path !== void 0 && path.startsWith(tempBase)) try {
6320
+ unlinkSync(path);
6321
+ } catch {}
6322
+ }
6288
6323
  this.byId.clear();
6289
6324
  this.nextId = 1;
6290
6325
  }
@@ -8688,8 +8723,25 @@ var QueuePaneComponent = class extends Container {
8688
8723
  };
8689
8724
  //#endregion
8690
8725
  //#region src/tui/utils/image-placeholder.ts
8726
+ /**
8727
+ * Scan submitted text for media placeholders and produce
8728
+ * the `PromptPart[]` we'll send to the SDK prompt endpoint.
8729
+ *
8730
+ * Rules:
8731
+ * - Only placeholders that resolve against `store` get extracted.
8732
+ * A literal `[image #999 ...]` the user typed themselves stays in
8733
+ * the text (we can't hallucinate files for it).
8734
+ * - Order is preserved for text/image/video segments. Image placeholders
8735
+ * expand to image content parts so the prompt reaches the provider
8736
+ * without relying on a model tool call. Video placeholders still expand
8737
+ * to file-path tags so `ReadMediaFile` can own video upload behavior.
8738
+ * - Adjacent text segments are flattened — empty / whitespace-only
8739
+ * segments drop out so we never emit `{type:'text', text:' '}`
8740
+ * noise between two media parts.
8741
+ */
8691
8742
  const PLACEHOLDER_REGEX = /\[(image|video) #(\d+) (?:(\(\d+×\d+\))|([^\]]+))\]/g;
8692
- function extractMediaAttachments(text, store) {
8743
+ function extractMediaAttachments(text, store, options = {}) {
8744
+ const imageMode = options.imageMode ?? "inline";
8693
8745
  const parts = [];
8694
8746
  const imageAttachmentIds = [];
8695
8747
  const videoAttachmentIds = [];
@@ -8709,6 +8761,13 @@ function extractMediaAttachments(text, store) {
8709
8761
  if (attachment.kind === "video") {
8710
8762
  pushText(parts, tagTextForVideo(attachment));
8711
8763
  videoAttachmentIds.push(id);
8764
+ } else if (imageMode === "path") {
8765
+ try {
8766
+ pushText(parts, formatMediaTag("image", ensureImagePath(attachment)));
8767
+ } catch {
8768
+ parts.push(imagePartForAttachment(attachment));
8769
+ }
8770
+ imageAttachmentIds.push(id);
8712
8771
  } else {
8713
8772
  parts.push(imagePartForAttachment(attachment));
8714
8773
  imageAttachmentIds.push(id);
@@ -8747,6 +8806,15 @@ function imagePartForAttachment(att) {
8747
8806
  imageUrl: { url: `data:${att.mime};base64,${base64}` }
8748
8807
  };
8749
8808
  }
8809
+ /** Persist bytes to a temp file when path delivery needs a disk path. */
8810
+ function ensureImagePath(att) {
8811
+ if (att.sourcePath !== void 0 && att.sourcePath.length > 0) return att.sourcePath;
8812
+ const ext = att.mime === "image/jpeg" ? "jpg" : att.mime.split("/")[1]?.replace("+xml", "").replace("+json", "") ?? "png";
8813
+ const path = join(tmpdir(), `${TEMP_ATTACHMENT_PREFIX}${randomUUID()}.${ext}`);
8814
+ writeFileSync(path, att.bytes);
8815
+ att.sourcePath = path;
8816
+ return path;
8817
+ }
8750
8818
  function tagTextForVideo(att) {
8751
8819
  return formatMediaTag("video", att.sourcePath);
8752
8820
  }
@@ -8914,7 +8982,8 @@ var InputController = class InputController {
8914
8982
  this.dispatchUserInput(text, session);
8915
8983
  }
8916
8984
  dispatchUserInput(text, session) {
8917
- const extraction = extractMediaAttachments(text, this.host.imageStore);
8985
+ const canInlineImages = this.supportsCurrentModelCapability("image_in");
8986
+ const extraction = extractMediaAttachments(text, this.host.imageStore, { imageMode: canInlineImages ? "inline" : "path" });
8918
8987
  if (!this.validateMediaCapabilities(extraction)) return;
8919
8988
  if (extraction.hasMedia) this.sendMessage(session, text, {
8920
8989
  hasMedia: true,
@@ -9091,10 +9160,6 @@ var InputController = class InputController {
9091
9160
  }
9092
9161
  validateMediaCapabilities(extraction) {
9093
9162
  if (!extraction.hasMedia) return true;
9094
- if (extraction.imageAttachmentIds.length > 0 && !this.supportsCurrentModelCapability("image_in")) {
9095
- this.host.showError(t("error.image_not_supported"));
9096
- return false;
9097
- }
9098
9163
  if (extraction.videoAttachmentIds.length > 0 && !this.supportsCurrentModelCapability("video_in")) {
9099
9164
  this.host.showError(t("error.video_not_supported"));
9100
9165
  return false;
@@ -9926,6 +9991,20 @@ function extractBracketedImagePastePaths(data) {
9926
9991
  if (paths.length === 0 || segmentStart !== pasted.length) return void 0;
9927
9992
  return paths;
9928
9993
  }
9994
+ /**
9995
+ * True only when bracketed-paste markers span the entire input and the
9996
+ * body is strictly empty — Cmd+V with an image-only clipboard (macOS
9997
+ * wraps nothing). Whitespace-only pastes and coalesced keystrokes around
9998
+ * the marker pair are not image pastes.
9999
+ */
10000
+ function isCompleteEmptyBracketedPaste(data) {
10001
+ if (!data.includes(BRACKET_PASTE_START)) return false;
10002
+ const startIndex = data.indexOf(BRACKET_PASTE_START);
10003
+ const endIndex = data.indexOf(BRACKET_PASTE_END, startIndex + 6);
10004
+ if (endIndex === -1) return false;
10005
+ if (startIndex !== 0 || endIndex + 6 !== data.length) return false;
10006
+ return data.slice(startIndex + 6, endIndex).length === 0;
10007
+ }
9929
10008
  const KITTY_CSI_U = /^\[(\d+);(\d+)((?::\d+)*)u$/;
9930
10009
  const CAPS_LOCK_BIT = 64;
9931
10010
  const CTRL_BIT = 4;
@@ -10138,6 +10217,11 @@ var CustomEditor = class extends Editor {
10138
10217
  if (!normalized.includes(BRACKET_PASTE_END)) this.consumingPaste = true;
10139
10218
  return;
10140
10219
  }
10220
+ if (this.onPasteImage !== void 0 && isCompleteEmptyBracketedPaste(normalized)) {
10221
+ const handler = this.onPasteImage;
10222
+ handler();
10223
+ return;
10224
+ }
10141
10225
  if (matchesKey(normalized, process.platform === "win32" ? "alt+v" : Key.ctrl("v"))) {
10142
10226
  if (this.expandPasteMarkerAtCursor()) return;
10143
10227
  if (this.onPasteImage !== void 0) {
@@ -14053,7 +14137,7 @@ var ScreamTUI = class {
14053
14137
  const card = new OnboardingCardComponent({
14054
14138
  colors: this.state.theme.colors,
14055
14139
  onConfigure: () => {
14056
- import("./dispatch-CIGyjBDa.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14140
+ import("./dispatch-ChbwC2I5.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14057
14141
  this.restoreEditor();
14058
14142
  onDone?.();
14059
14143
  });
@@ -16516,23 +16600,34 @@ function removeLastTurnsBefore(messages, count, beforeSeq) {
16516
16600
  }
16517
16601
  messages.splice(0, messages.length, ...prefix, ...tail);
16518
16602
  }
16603
+ /**
16604
+ * The Web projection's anchor kind for one prompt origin.
16605
+ *
16606
+ * "Does this origin start a turn the user could `/undo`?" is the core rule, and
16607
+ * it now comes from `isRealUserPrompt` — public surface through the SDK, so this
16608
+ * projection no longer needs a copy of it. A copy is exactly what drifts from
16609
+ * the core and makes `/undo N` delete a different number of rows than the
16610
+ * history it mirrors. What is left here is the rendering split the core has no
16611
+ * opinion about: a user-typed skill turn carries no visible user row in the Web
16612
+ * transcript, so it is tagged `skill` and `/undo` removes that assistant block on
16613
+ * its own.
16614
+ *
16615
+ * Both callers ask about a *user* prompt, which is what an anchor is: the seed
16616
+ * path classifies each history message and only reads the answer for the user
16617
+ * ones, and a `turn.started` origin is the origin of the turn's prompt.
16618
+ */
16519
16619
  function classifyUndoAnchor(origin) {
16520
- if (origin === void 0 || origin === null) return "user";
16521
- if (typeof origin !== "object") return "other";
16522
- const kind = origin.kind;
16523
- if (kind === "user") return "user";
16524
- if (kind === "skill_activation" && origin.trigger === "user-slash") return "skill";
16525
- return "other";
16526
- }
16527
- /** Match the core `/undo` anchor rule without importing agent-core internals. */
16620
+ if (!isRealUserPrompt({
16621
+ role: "user",
16622
+ content: [],
16623
+ toolCalls: [],
16624
+ origin
16625
+ })) return "other";
16626
+ return origin?.kind === "skill_activation" ? "skill" : "user";
16627
+ }
16628
+ /** The core `/undo` anchor rule, applied to the whole history. */
16528
16629
  function countUndoAnchors(history) {
16529
- let count = 0;
16530
- for (const message of history) {
16531
- if (message.role !== "user") continue;
16532
- const origin = message.origin;
16533
- if (origin === void 0 || origin.kind === "user" || origin.kind === "skill_activation" && origin.trigger === "user-slash") count += 1;
16534
- }
16535
- return count;
16630
+ return history.filter(isRealUserPrompt).length;
16536
16631
  }
16537
16632
  const execFileAsync = promisify(execFile);
16538
16633
  /** Raw porcelain rows: repo-root-relative path + XY status. `getGitStatus`