dsh-code 1.0.0 → 1.0.2

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/lib/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { randomUUID } from "node:crypto";
4
4
  import * as fs from "node:fs";
5
5
  import { readFileSync, realpathSync } from "node:fs";
6
6
  import os, { homedir } from "node:os";
7
- import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
8
- import { basename, dirname, join, resolve } from "node:path";
7
+ import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
8
+ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
9
9
  import z from "@deepseek-ai/schemastery";
10
10
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
11
11
  import { MessageId, ReasoningEffortId, assertNever, boundContextSummary, createUserMessage, normalizeApiKey } from "@deepseek-ai/dsh-llm";
@@ -16,9 +16,12 @@ import { EventEmitter } from "node:events";
16
16
  import { Buffer as Buffer$1 } from "node:buffer";
17
17
  import { DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES, DEFAULT_FILE_SEARCH_MAX_ENTRIES, DEFAULT_FILE_SEARCH_MAX_RESULTS, WorkspaceFileSearch } from "@deepseek-ai/dsh-file-reference-local";
18
18
  import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
19
+ import { execFile, spawn } from "node:child_process";
20
+ import { credentialKeyId, credentialKeyScope } from "@deepseek-ai/dsh-credentials";
21
+ import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
22
+ import { fileURLToPath } from "node:url";
19
23
  import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
20
24
  import { isUserInvocable } from "@deepseek-ai/dsh-skill";
21
- import { execFile, spawn } from "node:child_process";
22
25
  //#region node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
23
26
  /**
24
27
  * @license React
@@ -24919,7 +24922,9 @@ function imageLabels(images) {
24919
24922
  if (images === void 0 || images.length === 0) return "";
24920
24923
  return images.map((image, index) => {
24921
24924
  const rawName = image.name?.trim() || `image ${index + 1}`;
24922
- return `[image: ${rawName.length <= 80 ? rawName : `${rawName.slice(0, 79)}…`} · ${image.width}×${image.height} · ${image.bytes} B]`;
24925
+ const name = rawName.length <= 80 ? rawName : `${rawName.slice(0, 79)}…`;
24926
+ const original = image.originalDimensions;
24927
+ return `[image: ${name} · ${original === void 0 ? `${image.width}×${image.height}` : `${image.width}×${image.height} · original ${original.width}×${original.height}`} · ${image.bytes} B]`;
24923
24928
  }).join("\n");
24924
24929
  }
24925
24930
  /** Prompt text with its durable image labels, without exposing local paths or bytes. */
@@ -26628,11 +26633,7 @@ function renderMarkdown(text, width, options = {}) {
26628
26633
  }
26629
26634
  //#endregion
26630
26635
  //#region src/render/animations.ts
26631
- /**
26632
- * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
26633
- * ring trail clockwise around the eight outer positions, one braille glyph
26634
- * per step — 8 frames × 125ms = the web's 1s cycle.
26635
- */
26636
+ /** Original terminal StateDot chase frames. */
26636
26637
  const BUSY_CHASE_FRAMES = [
26637
26638
  "⣾",
26638
26639
  "⣽",
@@ -26643,10 +26644,40 @@ const BUSY_CHASE_FRAMES = [
26643
26644
  "⣯",
26644
26645
  "⣷"
26645
26646
  ];
26646
- /** Chase frame for a monotonic tick (the busy composer/Deep-diving marker). */
26647
+ /** Chase frame for a monotonic tick. */
26647
26648
  function busyChaseFrame(tick) {
26648
26649
  return BUSY_CHASE_FRAMES[tick % BUSY_CHASE_FRAMES.length] ?? BUSY_CHASE_FRAMES[0];
26649
26650
  }
26651
+ /** Codex shimmer timing and geometry. */
26652
+ const DEEP_DIVING_SHIMMER_DURATION_MS = 2e3;
26653
+ const DEEP_DIVING_SPARK_BREATH_DURATION_MS = 2e3;
26654
+ /**
26655
+ * Codex's 2-second shimmer sweep, expressed in terminal ticks. The sweep has
26656
+ * ten virtual columns of padding on either side and a five-column cosine
26657
+ * highlight band, so the text changes gently rather than cycling rapidly.
26658
+ */
26659
+ function deepDivingShimmerIntensity(index, tick, graphemeCount) {
26660
+ if (graphemeCount <= 0) return 0;
26661
+ const period = graphemeCount + 20;
26662
+ const position = (tick * 33 % DEEP_DIVING_SHIMMER_DURATION_MS + DEEP_DIVING_SHIMMER_DURATION_MS) % DEEP_DIVING_SHIMMER_DURATION_MS / DEEP_DIVING_SHIMMER_DURATION_MS * period;
26663
+ const distance = Math.abs(index + 10 - position);
26664
+ if (distance > 5) return 0;
26665
+ const angle = Math.PI * distance / 5;
26666
+ return .5 * (1 + Math.cos(angle));
26667
+ }
26668
+ /** Blue RGB color for one grapheme in the Codex-style shimmer. */
26669
+ function deepDivingGradientColor(index, tick, graphemeCount, base, highlight) {
26670
+ return blendRgb(highlight, base, deepDivingShimmerIntensity(index, tick, graphemeCount));
26671
+ }
26672
+ /** Smooth breathing intensity for the always-visible Deep diving sparkle. */
26673
+ function deepDivingSparkIntensity(tick) {
26674
+ const phase = (tick * 33 % DEEP_DIVING_SPARK_BREATH_DURATION_MS + DEEP_DIVING_SPARK_BREATH_DURATION_MS) % DEEP_DIVING_SPARK_BREATH_DURATION_MS / DEEP_DIVING_SPARK_BREATH_DURATION_MS;
26675
+ return .2 + .8 * (.5 + .5 * Math.cos(Math.PI * 2 * phase));
26676
+ }
26677
+ /** Blue RGB color for the breathing Deep diving sparkle. */
26678
+ function deepDivingSparkColor(tick, base, highlight) {
26679
+ return blendRgb(highlight, base, deepDivingSparkIntensity(tick));
26680
+ }
26650
26681
  /** Caret visibility: half the ticks on, half off (530ms blink). */
26651
26682
  function caretVisible(tick) {
26652
26683
  return tick % 2 === 0;
@@ -27036,6 +27067,87 @@ function effortAboveHigh(effort) {
27036
27067
  return rank !== void 0 && rank > 3;
27037
27068
  }
27038
27069
  //#endregion
27070
+ //#region src/mentions.ts
27071
+ /** Menu cap on file rows; the service owns ranking and default rows. */
27072
+ const MAX_FILE_ROWS = 20;
27073
+ /** Whether a mention token is already navigating a filesystem path. */
27074
+ function isPathLikeMentionQuery(query) {
27075
+ return /[\\/]/u.test(query);
27076
+ }
27077
+ /**
27078
+ * Create the mention API for one agent's workspace. A missing
27079
+ * `fileReferences` service (with an agent present) or `sessionReferenceResolver`
27080
+ * degrades that half to empty rows; `prepare` passes text through untouched
27081
+ * without references. An undefined agent (a bare launch before any session
27082
+ * exists) runs the official WorkspaceFileSearch over the launch cwd — the
27083
+ * same class the mounted service uses per agent — so `@` file completion
27084
+ * works from the first keystroke; session references wait for the session.
27085
+ *
27086
+ * `candidates` never reaches for `this` — the runner hands it to the input
27087
+ * editor as a detached callback, and a `this`-bound method would throw on
27088
+ * every `@` key.
27089
+ * @param ctx - context carrying the optional `fileReferences` and
27090
+ * `sessionReferenceResolver` services.
27091
+ * @param agent - the session owner; excluded from its own session candidates.
27092
+ * @param cwd - launch working directory; bounds the pre-session search.
27093
+ */
27094
+ function createMentions(ctx, agent, cwd) {
27095
+ const resolver = ctx.get("sessionReferenceResolver");
27096
+ const fileReferences = ctx.get("fileReferences");
27097
+ const sessionCapable = agent !== void 0 && resolver !== void 0;
27098
+ let preSessionSearch;
27099
+ const preSessionFiles = (query, signal) => {
27100
+ preSessionSearch ??= new WorkspaceFileSearch(cwd, {
27101
+ maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
27102
+ maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
27103
+ excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
27104
+ });
27105
+ return preSessionSearch.list(query, signal ?? new AbortController().signal);
27106
+ };
27107
+ return {
27108
+ async candidates(query, signal) {
27109
+ const needle = query.trim();
27110
+ const [files, sessions] = await Promise.all([agent !== void 0 && fileReferences !== void 0 ? fileReferences.list(agent, needle, signal ?? new AbortController().signal).catch(() => []) : agent === void 0 ? preSessionFiles(needle, signal).catch(() => []) : Promise.resolve([]), sessionCapable && needle !== "" && !isPathLikeMentionQuery(needle) && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
27111
+ const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
27112
+ label: candidate.path,
27113
+ description: candidate.kind === "directory" ? "Folder" : "File",
27114
+ kind: candidate.kind,
27115
+ ...candidate.kind === "file" ? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) } : {}
27116
+ }));
27117
+ const sessionRows = sessions.map((candidate) => ({
27118
+ label: formatSessionReferenceMention(candidate),
27119
+ description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
27120
+ kind: "session"
27121
+ }));
27122
+ return [...fileRows, ...sessionRows];
27123
+ },
27124
+ parse(text) {
27125
+ return parseSessionReferenceText(text);
27126
+ },
27127
+ async prepare(parsed, signal) {
27128
+ if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
27129
+ text: parsed.text,
27130
+ references: parsed.references
27131
+ };
27132
+ const prepared = await resolver.prepare(agent, [{
27133
+ type: "text",
27134
+ text: parsed.text
27135
+ }], parsed.references, signal);
27136
+ return {
27137
+ text: prepared.content.filter((block) => block.type === "text").map((block) => block.text).join(""),
27138
+ references: parsed.references,
27139
+ additionalContext: prepared.additionalContext
27140
+ };
27141
+ },
27142
+ sessionMention(candidate) {
27143
+ return formatSessionReferenceMention({
27144
+ sessionId: candidate.sessionId,
27145
+ label: candidate.label
27146
+ });
27147
+ }
27148
+ };
27149
+ }
27150
+ //#endregion
27039
27151
  //#region src/session-directory.ts
27040
27152
  /** Lightweight session-directory projection for the /resume picker. */
27041
27153
  /** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
@@ -27112,7 +27224,7 @@ function mergeSessionTitles(rows, observations) {
27112
27224
  const titles = /* @__PURE__ */ new Map();
27113
27225
  for (const observation of observations) {
27114
27226
  if (observation.status !== "fulfilled") continue;
27115
- const title = observation.value?.title?.title ?? observation.value?.title?.text;
27227
+ const title = observation.value?.title?.title;
27116
27228
  if (title !== void 0 && title.trim() !== "") titles.set(observation.sessionId, title);
27117
27229
  }
27118
27230
  return rows.map((row) => titles.has(row.id) ? {
@@ -27253,10 +27365,10 @@ function formatRate(n) {
27253
27365
  /**
27254
27366
  * Cache-hit share of billed prompt-side input.
27255
27367
  * @param usage - cumulative token totals.
27256
- * @returns rounded integer percent, or null when no input was billed.
27368
+ * @returns percent rounded to one decimal place, or null when no input was billed.
27257
27369
  */
27258
27370
  function cacheHitPercent(usage) {
27259
- return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens * 100);
27371
+ return usage.inputTokens === 0 ? null : Math.round(usage.cacheReadTokens / usage.inputTokens * 1e3) / 10;
27260
27372
  }
27261
27373
  /** Separator between trailing state spans. */
27262
27374
  const STATUS_ITEM_SEPARATOR = " · ";
@@ -27965,13 +28077,20 @@ function toolDetailLines(detail, columns) {
27965
28077
  default: return detail;
27966
28078
  }
27967
28079
  }
28080
+ /** Default compact tool-card window used while the Ctrl+R fold is closed. */
28081
+ const DEFAULT_TOOL_ROWS = 3;
28082
+ /** Keep the invocation visible while making hidden tool output discoverable. */
28083
+ function compactToolLines(lines, columns) {
28084
+ if (lines.length <= DEFAULT_TOOL_ROWS) return lines;
28085
+ return [...lines.slice(0, 2), ...textLines(" … output hidden · Ctrl+R", columns, "dim").slice(0, 1)];
28086
+ }
27968
28087
  /**
27969
28088
  * Convert one durable transcript entry to its complete scrollable row model.
27970
28089
  * The source entry stays intact; only the caller's visible slice is rendered.
27971
28090
  * Wrapped continuations keep a hanging indent aligned under each row's
27972
28091
  * content (Codex history-cell alignment) instead of resetting to column 0.
27973
28092
  */
27974
- function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true) {
28093
+ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true, showToolDetails = showReasoning) {
27975
28094
  const width = Math.max(1, Math.floor(columns));
27976
28095
  switch (entry.kind) {
27977
28096
  case "user": return entry.notice ? hangingStyledLines([lineSegment(promptDisplayText(entry), "dim")], width, "⤷ ", "dim", " ", "dim") : hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
@@ -27993,7 +28112,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
27993
28112
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
27994
28113
  const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
27995
28114
  const summaryStyle = entry.state === "error" ? "error" : "dim";
27996
- return [
28115
+ const lines = [
27997
28116
  ...hangingStyledLines([
27998
28117
  lineSegment(`[${entry.ordinal}] `, "dim"),
27999
28118
  lineSegment(entry.name, "brand"),
@@ -28003,6 +28122,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
28003
28122
  ...entry.summary === "" ? [] : hangingTextLines(entry.state === "error" ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary, width, " ⎿ ", summaryStyle, " ", summaryStyle),
28004
28123
  ...entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width)
28005
28124
  ];
28125
+ return showToolDetails ? lines : compactToolLines(lines, width);
28006
28126
  }
28007
28127
  case "command": {
28008
28128
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
@@ -28020,7 +28140,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
28020
28140
  }
28021
28141
  /** Settled-history variant carrying the Ctrl+R reasoning fold. */
28022
28142
  function settledEntryLines(entry, columns, showReasoning) {
28023
- return transcriptEntryLines(entry, columns, showReasoning, false);
28143
+ return transcriptEntryLines(entry, columns, showReasoning, false, showReasoning);
28024
28144
  }
28025
28145
  //#endregion
28026
28146
  //#region src/render/editor.ts
@@ -28034,9 +28154,8 @@ function settledEntryLines(entry, columns, showReasoning) {
28034
28154
  * grapheme boundaries) so the React state stays two primitives
28035
28155
  * (value, cursor) and every operation here stays pure and testable.
28036
28156
  *
28037
- * Word motion deviates from Codex's UAX#29 segmentation in one deliberate
28038
- * way: a run of same-class characters is ONE piece, so a CJK run moves as a
28039
- * single word (two hanzi are one Alt+B step, not two).
28157
+ * Word motion follows Codex's piece semantics: whitespace separates runs,
28158
+ * punctuation runs stay atomic, and each Han grapheme is its own boundary.
28040
28159
  *
28041
28160
  * @module @deepseek-ai/dsh-code/render/editor
28042
28161
  */
@@ -28189,6 +28308,28 @@ function editorModel(value, columns) {
28189
28308
  length: value.length
28190
28309
  };
28191
28310
  }
28311
+ /** Split one row around the authoritative caret; every other row stays whole. */
28312
+ function editorRowParts(row, rowIndex, caretRow, cursor, caretEnabled = true) {
28313
+ if (!caretEnabled || rowIndex !== caretRow) return {
28314
+ before: "",
28315
+ caret: "",
28316
+ after: row.text,
28317
+ hasCaret: false
28318
+ };
28319
+ const at = row.offsets.indexOf(cursor);
28320
+ if (at < 0) return {
28321
+ before: "",
28322
+ caret: "",
28323
+ after: row.text,
28324
+ hasCaret: false
28325
+ };
28326
+ return {
28327
+ before: at > 0 ? row.text.slice(0, row.cuts[at]) : "",
28328
+ caret: at < row.cuts.length - 1 ? row.text.slice(row.cuts[at], row.cuts[at + 1]) : " ",
28329
+ after: at < row.cuts.length - 1 ? row.text.slice(row.cuts[at + 1]) : "",
28330
+ hasCaret: true
28331
+ };
28332
+ }
28192
28333
  /** Map a cursor offset to its caret site on the wrapped rows. */
28193
28334
  function caretSite(model, offset) {
28194
28335
  const target = Math.max(0, Math.min(model.length, offset));
@@ -28213,7 +28354,9 @@ function caretSite(model, offset) {
28213
28354
  */
28214
28355
  function moveCursorVertically(model, offset, preferredColumn, delta) {
28215
28356
  const target = caretSite(model, offset).row + delta;
28216
- if (target < 0 || target >= model.rows.length || delta === 0) return offset;
28357
+ if (delta === 0) return offset;
28358
+ if (target < 0) return 0;
28359
+ if (target >= model.rows.length) return model.length;
28217
28360
  const row = model.rows[target];
28218
28361
  const wanted = Math.max(0, Math.min(preferredColumn, row.columns[row.columns.length - 1]));
28219
28362
  let best = 0;
@@ -28233,24 +28376,28 @@ function lineBounds(value, offset) {
28233
28376
  }
28234
28377
  /** Codex WORD_SEPARATORS: punctuation runs are their own word pieces. */
28235
28378
  const WORD_SEPARATORS = /* @__PURE__ */ new Set("`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?");
28379
+ const HAN_GRAPHEME = /^\p{Script=Han}(?:\p{Mark}|\uFE0F)*$/u;
28380
+ const UNICODE_PUNCTUATION = /^\p{P}+$/u;
28236
28381
  function classifyGrapheme(text) {
28237
28382
  if (/^\s$/u.test(text)) return "space";
28238
- return WORD_SEPARATORS.has(text) ? "punct" : "word";
28383
+ return WORD_SEPARATORS.has(text) || UNICODE_PUNCTUATION.test(text) ? "punct" : "word";
28239
28384
  }
28240
- /** Maximal same-class runs of graphemes as [start, end) spans. */
28385
+ /** Maximal same-class runs; Han graphemes deliberately stay one run each. */
28241
28386
  function pieceRuns(value) {
28242
28387
  const runs = [];
28243
28388
  let current;
28244
28389
  for (const span of splitGraphemes(value)) {
28245
28390
  const klass = span.text === "\n" ? "space" : classifyGrapheme(span.text);
28246
- if (current !== void 0 && current.class === klass) {
28391
+ const atomic = klass === "word" && HAN_GRAPHEME.test(span.text);
28392
+ if (current !== void 0 && current.class === klass && !current.atomic && !atomic) {
28247
28393
  current.end = span.end;
28248
28394
  continue;
28249
28395
  }
28250
28396
  current = {
28251
28397
  start: span.start,
28252
28398
  end: span.end,
28253
- class: klass
28399
+ class: klass,
28400
+ atomic
28254
28401
  };
28255
28402
  runs.push(current);
28256
28403
  }
@@ -28261,10 +28408,9 @@ function pieceRuns(value) {
28261
28408
  * START of the trailing non-space piece (extending over separator pieces).
28262
28409
  */
28263
28410
  function moveWordLeft(value, offset) {
28264
- const cursor = Math.max(0, Math.min(value.length, offset));
28411
+ const cursor = clampCursor(value, offset);
28265
28412
  const runs = pieceRuns(value);
28266
- let index = runs.length - 1;
28267
- while (index >= 0 && runs[index].end > cursor) index -= 1;
28413
+ let index = runs.findLastIndex((run) => run.start < cursor);
28268
28414
  if (index < 0) return 0;
28269
28415
  if (runs[index].class === "space") {
28270
28416
  index -= 1;
@@ -28279,10 +28425,9 @@ function moveWordLeft(value, offset) {
28279
28425
  * the leading non-space piece (extending over separator pieces).
28280
28426
  */
28281
28427
  function moveWordRight(value, offset) {
28282
- const cursor = Math.max(0, Math.min(value.length, offset));
28428
+ const cursor = clampCursor(value, offset);
28283
28429
  const runs = pieceRuns(value);
28284
- let index = 0;
28285
- while (index < runs.length && runs[index].start < cursor) index += 1;
28430
+ let index = runs.findIndex((run) => run.end > cursor);
28286
28431
  if (index >= runs.length) return value.length;
28287
28432
  if (runs[index].class === "space") {
28288
28433
  index += 1;
@@ -28371,6 +28516,62 @@ function insertText(value, cursor, text) {
28371
28516
  killed: void 0
28372
28517
  };
28373
28518
  }
28519
+ /** Ctrl+A: current logical line start, then the previous line start on repeat. */
28520
+ function moveToLineStart(value, cursor, crossOnRepeat) {
28521
+ const site = clampCursor(value, cursor);
28522
+ const bounds = lineBounds(value, site);
28523
+ if (!crossOnRepeat || site !== bounds.start || bounds.start === 0) return bounds.start;
28524
+ return lineBounds(value, bounds.start - 1).start;
28525
+ }
28526
+ /** Ctrl+E: current logical line end, then the next line end on repeat. */
28527
+ function moveToLineEnd(value, cursor, crossOnRepeat) {
28528
+ const site = clampCursor(value, cursor);
28529
+ const bounds = lineBounds(value, site);
28530
+ if (!crossOnRepeat || site !== bounds.end || bounds.end >= value.length) return bounds.end;
28531
+ return lineBounds(value, bounds.end + 1).end;
28532
+ }
28533
+ /**
28534
+ * Remap a captured range when all intervening edits are wholly before or
28535
+ * wholly after it. An edit overlapping either boundary invalidates the
28536
+ * anchor instead of guessing and inserting content at a surprising place.
28537
+ */
28538
+ function remapStableRange(original, current, range) {
28539
+ const start = Math.max(0, Math.min(original.length, range.start));
28540
+ const end = Math.max(start, Math.min(original.length, range.end));
28541
+ if (original === current) return {
28542
+ start,
28543
+ end
28544
+ };
28545
+ let prefix = 0;
28546
+ const shared = Math.min(original.length, current.length);
28547
+ while (prefix < shared && original[prefix] === current[prefix]) prefix += 1;
28548
+ let suffix = 0;
28549
+ while (suffix < original.length - prefix && suffix < current.length - prefix && original[original.length - 1 - suffix] === current[current.length - 1 - suffix]) suffix += 1;
28550
+ const oldChangedEnd = original.length - suffix;
28551
+ if (prefix >= end) return {
28552
+ start,
28553
+ end
28554
+ };
28555
+ if (oldChangedEnd <= start) {
28556
+ const delta = current.length - original.length;
28557
+ return {
28558
+ start: start + delta,
28559
+ end: end + delta
28560
+ };
28561
+ }
28562
+ }
28563
+ /** Replace a current range while preserving a cursor moved after capture. */
28564
+ function replaceRangePreservingCursor(value, cursor, range, replacement) {
28565
+ const start = Math.max(0, Math.min(value.length, range.start));
28566
+ const end = Math.max(start, Math.min(value.length, range.end));
28567
+ const site = clampCursor(value, cursor);
28568
+ const nextCursor = site <= start ? site : site >= end ? site + replacement.length - (end - start) : start + replacement.length;
28569
+ return {
28570
+ value: value.slice(0, start) + replacement + value.slice(end),
28571
+ cursor: nextCursor,
28572
+ killed: void 0
28573
+ };
28574
+ }
28374
28575
  /**
28375
28576
  * Composer editor row budget: the editor itself never grows past this many
28376
28577
  * physical rows; deeper drafts scroll internally to keep the caret visible.
@@ -28380,14 +28581,73 @@ function composerMaxRows(terminalRows) {
28380
28581
  return Math.max(1, Math.min(6, Math.floor((Math.max(1, terminalRows) - 10) / 3)));
28381
28582
  }
28382
28583
  /**
28383
- * Codex `should_handle_navigation`: Up/Down walk history only from an empty
28384
- * draft, or from a boundary of a draft that still exactly matches the last
28385
- * recalled entry. Any interior cursor position keeps vertical caret movement.
28584
+ * History navigation starts with Up on an empty draft, or after visual
28585
+ * movement has reached the directional text edge of an unchanged recalled
28586
+ * entry. Every other position remains under textarea movement.
28386
28587
  */
28387
- function shouldRecallNavigate(value, cursor, lastRecalled) {
28388
- if (value === "") return true;
28389
- if (cursor !== 0 && cursor !== value.length) return false;
28390
- return lastRecalled === value;
28588
+ function shouldRecallNavigate(value, cursor, lastRecalled, direction) {
28589
+ if (value === "") return direction < 0;
28590
+ if (lastRecalled !== value) return false;
28591
+ return direction < 0 ? cursor === 0 : cursor === value.length;
28592
+ }
28593
+ //#endregion
28594
+ //#region src/keyboard.ts
28595
+ /**
28596
+ * Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
28597
+ * kitty CSI-u normalization layer.
28598
+ *
28599
+ * The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
28600
+ * and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`). Event types are
28601
+ * deliberately NOT requested: Ink 5's parser cannot decode the
28602
+ * `:event-type` suffix, and repeat/release reporting buys this surface
28603
+ * nothing.
28604
+ *
28605
+ * Ink 5 also cannot parse most CSI-u forms at all — they fall through its
28606
+ * regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
28607
+ * stdin read patch therefore rewrites every CSI-u form it can decode back
28608
+ * to the legacy byte or canonical sequence the existing key handling
28609
+ * already understands, before Ink ever parses the chunk.
28610
+ * @module @deepseek-ai/dsh-code/keyboard
28611
+ */
28612
+ /** Push keyboard enhancement (modifyOtherKeys off, kitty flags 1|4). */
28613
+ const KEYBOARD_ENHANCE_ENABLE = "\x1B[>4;0m\x1B[>5u";
28614
+ /** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
28615
+ const KEYBOARD_ENHANCE_DISABLE = "\x1B[<u\x1B[>4;0m";
28616
+ const DSH_ENABLE_KEYBOARD_ENHANCEMENT = "DSH_ENABLE_KEYBOARD_ENHANCEMENT";
28617
+ function parseBooleanEnv(value) {
28618
+ if (value === void 0) return void 0;
28619
+ const normalized = value.trim().toLowerCase();
28620
+ if (normalized === "1" || normalized === "true" || normalized === "yes") return true;
28621
+ if (normalized === "0" || normalized === "false" || normalized === "no") return false;
28622
+ }
28623
+ /** True when the process is running inside the VS Code integrated terminal. */
28624
+ function isVsCodeTerminalEnv(env = process.env) {
28625
+ return env.TERM_PROGRAM?.trim().toLowerCase() === "vscode" || env.VSCODE_INJECTION === "1";
28626
+ }
28627
+ /** Whether to push Kitty keyboard enhancement for the current terminal. */
28628
+ function shouldEnableKeyboardEnhancement(env = process.env) {
28629
+ if (parseBooleanEnv(env["DSH_DISABLE_KEYBOARD_ENHANCEMENT"]) === true) return false;
28630
+ const explicitEnable = parseBooleanEnv(env[DSH_ENABLE_KEYBOARD_ENHANCEMENT]);
28631
+ if (explicitEnable !== void 0) return explicitEnable;
28632
+ return !isVsCodeTerminalEnv(env);
28633
+ }
28634
+ /** Enable bracketed paste reporting. */
28635
+ const BRACKETED_PASTE_ENABLE = "\x1B[?2004h";
28636
+ /** Disable bracketed paste reporting. */
28637
+ const BRACKETED_PASTE_DISABLE = "\x1B[?2004l";
28638
+ /** Enable terminal focus-in/focus-out reporting (xterm focus protocol). */
28639
+ const TERMINAL_FOCUS_REPORT_ENABLE = "\x1B[?1004h";
28640
+ /** Disable terminal focus-in/focus-out reporting. */
28641
+ const TERMINAL_FOCUS_REPORT_DISABLE = "\x1B[?1004l";
28642
+ /**
28643
+ * Remove xterm focus reports from one input chunk and update the caller's
28644
+ * focus state. Focus reports are terminal protocol, not composer text.
28645
+ */
28646
+ function stripTerminalFocusEvents(chunk, onFocus) {
28647
+ return chunk.replace(/\x1b\[(I|O)/gu, (_whole, event) => {
28648
+ onFocus(event === "I");
28649
+ return "";
28650
+ });
28391
28651
  }
28392
28652
  /** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
28393
28653
  const PASTE_START_MARKER = "[200~";
@@ -28398,7 +28658,7 @@ const PASTE_END_MARKER = "[201~";
28398
28658
  * "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
28399
28659
  */
28400
28660
  function stripPasteMarkers(text) {
28401
- return text.replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
28661
+ return text.replaceAll(`\x1b${PASTE_START_MARKER}`, "").replaceAll(`\x1b${PASTE_END_MARKER}`, "").replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
28402
28662
  }
28403
28663
  /** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
28404
28664
  const CSI_U_SOURCE = "\x1B\\[(\\d+)(?:;(\\d+))?(?:[:;](\\d+))?u";
@@ -28409,7 +28669,6 @@ function legacyForKey(key) {
28409
28669
  const alt = (bits & 2) !== 0;
28410
28670
  const ctrl = (bits & 4) !== 0;
28411
28671
  if (key.code === 13) {
28412
- if (shift) return "\x1B[13;2u";
28413
28672
  if (ctrl) return "\n";
28414
28673
  if (alt) return "\x1B\r";
28415
28674
  return "\r";
@@ -28460,6 +28719,98 @@ function normalizeKeyboardChunk(chunk) {
28460
28719
  }) ?? whole;
28461
28720
  });
28462
28721
  }
28722
+ const HOME_SEQUENCES = [
28723
+ "\x1B[H",
28724
+ "\x1B[1~",
28725
+ "\x1B[7~",
28726
+ "\x1BOH"
28727
+ ];
28728
+ const END_SEQUENCES = [
28729
+ "\x1B[F",
28730
+ "\x1B[4~",
28731
+ "\x1B[8~",
28732
+ "\x1BOF"
28733
+ ];
28734
+ /** Parse one CSI functional-key sequence at `offset`. */
28735
+ function functionalToken(chunk, offset) {
28736
+ const tail = chunk.slice(offset);
28737
+ for (const sequence of HOME_SEQUENCES) if (tail.startsWith(sequence)) return {
28738
+ token: { kind: "home" },
28739
+ length: sequence.length
28740
+ };
28741
+ for (const sequence of END_SEQUENCES) if (tail.startsWith(sequence)) return {
28742
+ token: { kind: "end" },
28743
+ length: sequence.length
28744
+ };
28745
+ const modifiedHome = /^\x1b\[1;(\d+)H/u.exec(tail);
28746
+ if (modifiedHome !== null) return {
28747
+ token: { kind: "home" },
28748
+ length: modifiedHome[0].length
28749
+ };
28750
+ const modifiedEnd = /^\x1b\[1;(\d+)F/u.exec(tail);
28751
+ if (modifiedEnd !== null) return {
28752
+ token: { kind: "end" },
28753
+ length: modifiedEnd[0].length
28754
+ };
28755
+ const modifiedDelete = /^\x1b\[3(?:;(\d+))?~/u.exec(tail);
28756
+ if (modifiedDelete !== null) {
28757
+ const modifiers = Number.parseInt(modifiedDelete[1] ?? "1", 10) - 1;
28758
+ return {
28759
+ token: { kind: (modifiers & 2) !== 0 || (modifiers & 4) !== 0 ? "delete-word-forward" : "delete-forward" },
28760
+ length: modifiedDelete[0].length
28761
+ };
28762
+ }
28763
+ }
28764
+ /**
28765
+ * Tokenize a stdin chunk containing at least one editor-only key. Ink calls
28766
+ * `useInput` once for a pasted/batched chunk, so preserving each action here
28767
+ * prevents repeated Backspace/Home/End/Delete presses from collapsing into
28768
+ * one blurred key event. Unknown escape sequences return undefined and stay
28769
+ * under Ink's ownership.
28770
+ */
28771
+ function tokenizeRawEditorChunk(chunk) {
28772
+ const tokens = [];
28773
+ let text = "";
28774
+ let special = false;
28775
+ const flushText = () => {
28776
+ if (text === "") return;
28777
+ tokens.push({
28778
+ kind: "text",
28779
+ text
28780
+ });
28781
+ text = "";
28782
+ };
28783
+ for (let offset = 0; offset < chunk.length;) {
28784
+ if (chunk.startsWith("\x1B", offset) || chunk.startsWith("\x1B\b", offset)) {
28785
+ flushText();
28786
+ tokens.push({ kind: "delete-word-backward" });
28787
+ special = true;
28788
+ offset += 2;
28789
+ continue;
28790
+ }
28791
+ const functional = functionalToken(chunk, offset);
28792
+ if (functional !== void 0) {
28793
+ flushText();
28794
+ tokens.push(functional.token);
28795
+ special = true;
28796
+ offset += functional.length;
28797
+ continue;
28798
+ }
28799
+ const char = chunk[offset];
28800
+ if (char === "" || char === "\b") {
28801
+ flushText();
28802
+ tokens.push({ kind: "delete-backward" });
28803
+ special = true;
28804
+ offset += 1;
28805
+ continue;
28806
+ }
28807
+ if (char === "\x1B") return void 0;
28808
+ text += char;
28809
+ offset += 1;
28810
+ }
28811
+ flushText();
28812
+ return special ? tokens : void 0;
28813
+ }
28463
28814
  //#endregion
28464
28815
  //#region src/kernel-panels.ts
28465
28816
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
@@ -29446,6 +29797,511 @@ function recallNewer(state) {
29446
29797
  };
29447
29798
  }
29448
29799
  //#endregion
29800
+ //#region src/authorization.ts
29801
+ /** Terminal adapter over the Harness provider-authorization and credential-record seams. */
29802
+ /** Record scope used by the upstream pi-ai adapter for provider logins. */
29803
+ const PI_AI_RECORD_SCOPE = "llm-pi-ai";
29804
+ /** Load only model-provider flows; unrelated future authorization domains stay out of `/model`. */
29805
+ async function loadProviderAuthorizations(ctx) {
29806
+ const authorization = ctx.get("authorization");
29807
+ const credentials = ctx.get("credentials");
29808
+ if (authorization === void 0 || credentials === void 0) return {
29809
+ rows: [],
29810
+ failures: []
29811
+ };
29812
+ const entries = authorization.list().filter((entry) => credentialKeyScope(entry.key) === PI_AI_RECORD_SCOPE);
29813
+ const failures = [];
29814
+ return {
29815
+ rows: await Promise.all(entries.map(async (entry) => {
29816
+ let record;
29817
+ try {
29818
+ record = await credentials.describeRecord(entry.key);
29819
+ } catch (error) {
29820
+ failures.push(`${entry.label}: ${error instanceof Error ? error.message : String(error)}`);
29821
+ record = {
29822
+ configured: false,
29823
+ writable: false
29824
+ };
29825
+ }
29826
+ return {
29827
+ key: entry.key,
29828
+ provider: credentialKeyId(entry.key),
29829
+ label: entry.label,
29830
+ methods: entry.methods,
29831
+ inFlight: entry.inFlight,
29832
+ record
29833
+ };
29834
+ })),
29835
+ failures
29836
+ };
29837
+ }
29838
+ /** Subscribe to login settlement and credential-record changes. */
29839
+ function subscribeProviderAuthorizations(ctx, listener) {
29840
+ const settled = ctx.on("authorization/settled", (key) => {
29841
+ if (credentialKeyScope(key) === PI_AI_RECORD_SCOPE) listener();
29842
+ });
29843
+ const records = ctx.on("credentials/record-updated", (key) => {
29844
+ if (credentialKeyScope(key) === PI_AI_RECORD_SCOPE) listener();
29845
+ });
29846
+ return () => {
29847
+ settled();
29848
+ records();
29849
+ };
29850
+ }
29851
+ /** Begin one provider login through the interaction surface owned by the caller. */
29852
+ async function beginProviderAuthorization(ctx, row, method, interaction, signal) {
29853
+ const authorization = ctx.get("authorization");
29854
+ if (authorization === void 0) throw new Error("provider login is unavailable in this profile");
29855
+ return (await authorization.begin({
29856
+ key: row.key,
29857
+ method,
29858
+ interaction,
29859
+ signal
29860
+ })).status;
29861
+ }
29862
+ /** Cancel the attempt currently serving this provider, if any. */
29863
+ function cancelProviderAuthorization(ctx, key) {
29864
+ ctx.get("authorization")?.cancel(key);
29865
+ }
29866
+ /** Remove an authorization record without changing the provider's settings profile. */
29867
+ async function logoutProviderAuthorization(ctx, row) {
29868
+ const credentials = ctx.get("credentials");
29869
+ if (credentials === void 0) throw new Error("credential storage is unavailable in this profile");
29870
+ const current = await credentials.describeRecord(row.key);
29871
+ if (!current.configured) return;
29872
+ if (!current.writable) throw new Error("this login record is read-only");
29873
+ await credentials.deleteRecord(row.key);
29874
+ }
29875
+ /** Open an authorization URL with the platform default browser, without invoking a shell. */
29876
+ function openAuthorizationUrl(raw) {
29877
+ let url;
29878
+ try {
29879
+ url = new URL(raw);
29880
+ } catch {
29881
+ return false;
29882
+ }
29883
+ if (url.protocol !== "https:" && url.protocol !== "http:") return false;
29884
+ const target = url.toString();
29885
+ try {
29886
+ const child = process.platform === "win32" ? spawn("explorer.exe", [target], {
29887
+ detached: true,
29888
+ stdio: "ignore"
29889
+ }) : process.platform === "darwin" ? spawn("/usr/bin/open", [target], {
29890
+ detached: true,
29891
+ stdio: "ignore"
29892
+ }) : spawn("xdg-open", [target], {
29893
+ detached: true,
29894
+ stdio: "ignore"
29895
+ });
29896
+ child.once("error", () => {});
29897
+ child.unref();
29898
+ return true;
29899
+ } catch {
29900
+ return false;
29901
+ }
29902
+ }
29903
+ /** Compact value-free status for the provider list. */
29904
+ function providerAuthorizationStatus(row) {
29905
+ if (row === void 0) return "login unavailable";
29906
+ if (row.inFlight) return "login in progress";
29907
+ if (!row.record.configured) return "not logged in";
29908
+ return row.record.kind === "grant" ? "OAuth" : "interactive API key";
29909
+ }
29910
+ /** Find a provider's login flow from a previously loaded directory. */
29911
+ function authorizationForProvider(directory, provider) {
29912
+ return directory?.rows.find((row) => row.provider === provider);
29913
+ }
29914
+ //#endregion
29915
+ //#region src/authorization-panel.ts
29916
+ /** Bounded Ink surfaces for provider login and logout. */
29917
+ /** Run one upstream authorization flow without letting notices or prompts exceed the panel budget. */
29918
+ function ProviderAuthorizationPanel(props) {
29919
+ const stdout = useStdout().stdout;
29920
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29921
+ const [phase, setPhase] = (0, import_react.useState)("methods");
29922
+ const [cursor, setCursor] = (0, import_react.useState)(0);
29923
+ const [notices, setNotices] = (0, import_react.useState)([]);
29924
+ const [prompt, setPrompt] = (0, import_react.useState)(void 0);
29925
+ const [draft, setDraft] = (0, import_react.useState)("");
29926
+ const [promptCursor, setPromptCursor] = (0, import_react.useState)(0);
29927
+ const [error, setError] = (0, import_react.useState)(void 0);
29928
+ const [copyState, setCopyState] = (0, import_react.useState)(void 0);
29929
+ const controllerRef = (0, import_react.useRef)(void 0);
29930
+ const replyRef = (0, import_react.useRef)(void 0);
29931
+ const openedUrls = (0, import_react.useRef)(/* @__PURE__ */ new Set());
29932
+ const clearReply = () => {
29933
+ replyRef.current?.detach();
29934
+ replyRef.current = void 0;
29935
+ setPrompt(void 0);
29936
+ setDraft("");
29937
+ setPromptCursor(0);
29938
+ };
29939
+ const decline = () => {
29940
+ const reply = replyRef.current;
29941
+ clearReply();
29942
+ reply?.reject(new AuthorizationDeclinedError());
29943
+ };
29944
+ const stop = () => {
29945
+ controllerRef.current?.abort();
29946
+ controllerRef.current = void 0;
29947
+ props.cancel(props.row.key);
29948
+ decline();
29949
+ };
29950
+ (0, import_react.useEffect)(() => () => {
29951
+ controllerRef.current?.abort();
29952
+ props.cancel(props.row.key);
29953
+ const reply = replyRef.current;
29954
+ replyRef.current = void 0;
29955
+ reply?.detach();
29956
+ reply?.reject(new AuthorizationDeclinedError());
29957
+ }, [props.row.key]);
29958
+ const start = (method) => {
29959
+ setPhase("running");
29960
+ setError(void 0);
29961
+ setNotices([]);
29962
+ setCopyState(void 0);
29963
+ const controller = new AbortController();
29964
+ controllerRef.current = controller;
29965
+ props.begin(props.row, method, {
29966
+ notify(notice) {
29967
+ setNotices((current) => [...current.slice(-19), notice]);
29968
+ if (notice.url !== void 0 && !openedUrls.current.has(notice.url)) {
29969
+ openedUrls.current.add(notice.url);
29970
+ props.openUrl(notice.url);
29971
+ }
29972
+ },
29973
+ prompt(next) {
29974
+ return new Promise((resolve, reject) => {
29975
+ const onWithdraw = () => {
29976
+ if (replyRef.current?.reject !== reject) return;
29977
+ clearReply();
29978
+ reject(/* @__PURE__ */ new Error("authorization prompt was withdrawn"));
29979
+ };
29980
+ next.signal?.addEventListener("abort", onWithdraw, { once: true });
29981
+ replyRef.current = {
29982
+ resolve,
29983
+ reject,
29984
+ detach: () => next.signal?.removeEventListener("abort", onWithdraw)
29985
+ };
29986
+ setPrompt(next);
29987
+ setDraft("");
29988
+ setPromptCursor(0);
29989
+ });
29990
+ }
29991
+ }, controller.signal).then((status) => {
29992
+ controllerRef.current = void 0;
29993
+ clearReply();
29994
+ if (status === "authorized") props.done();
29995
+ else props.back();
29996
+ }, (reason) => {
29997
+ controllerRef.current = void 0;
29998
+ clearReply();
29999
+ if (controller.signal.aborted) {
30000
+ props.back();
30001
+ return;
30002
+ }
30003
+ setError(reason instanceof Error ? reason.message : String(reason));
30004
+ setPhase("methods");
30005
+ });
30006
+ };
30007
+ const answer = (value) => {
30008
+ const reply = replyRef.current;
30009
+ clearReply();
30010
+ reply?.resolve(value);
30011
+ };
30012
+ useInput((input, key) => {
30013
+ if (phase === "methods") {
30014
+ if (key.escape || input === "q") {
30015
+ props.back();
30016
+ return;
30017
+ }
30018
+ if (props.row.methods.length === 0) return;
30019
+ if (key.upArrow) {
30020
+ setCursor((current) => (current + props.row.methods.length - 1) % props.row.methods.length);
30021
+ return;
30022
+ }
30023
+ if (key.downArrow) {
30024
+ setCursor((current) => (current + 1) % props.row.methods.length);
30025
+ return;
30026
+ }
30027
+ if (key.return) start(props.row.methods[cursor]?.id ?? props.row.methods[0].id);
30028
+ return;
30029
+ }
30030
+ if (key.escape) {
30031
+ stop();
30032
+ props.back();
30033
+ return;
30034
+ }
30035
+ const copyValue = notices.at(-1)?.code ?? notices.at(-1)?.url;
30036
+ if ((input === "c" || input === "C") && copyValue !== void 0) {
30037
+ props.copy(copyValue).then(() => setCopyState("copied"), (reason) => setCopyState(`copy failed: ${reason instanceof Error ? reason.message : String(reason)}`));
30038
+ return;
30039
+ }
30040
+ if (prompt === void 0) return;
30041
+ if (prompt.kind === "select") {
30042
+ if (prompt.options.length === 0) return;
30043
+ if (key.upArrow) {
30044
+ setPromptCursor((current) => (current + prompt.options.length - 1) % prompt.options.length);
30045
+ return;
30046
+ }
30047
+ if (key.downArrow) {
30048
+ setPromptCursor((current) => (current + 1) % prompt.options.length);
30049
+ return;
30050
+ }
30051
+ if (key.return) answer(prompt.options[promptCursor]?.id ?? prompt.options[0].id);
30052
+ return;
30053
+ }
30054
+ if (key.backspace || key.delete) {
30055
+ setDraft((current) => [...current].slice(0, -1).join(""));
30056
+ return;
30057
+ }
30058
+ if (key.return) {
30059
+ if (draft.trim() !== "") answer(draft);
30060
+ return;
30061
+ }
30062
+ if (input !== "" && !key.ctrl && !key.meta) setDraft((current) => current + input);
30063
+ });
30064
+ if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
30065
+ if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("provider login · esc cancel", viewport.contentColumns));
30066
+ const rows = [];
30067
+ if (phase === "methods") {
30068
+ if (error !== void 0) rows.push({
30069
+ key: "error",
30070
+ text: ` ${singleLineText(error)}`,
30071
+ color: inkColor(getPalette().error)
30072
+ });
30073
+ props.row.methods.forEach((method, index) => {
30074
+ rows.push({
30075
+ key: method.id,
30076
+ text: `${index === cursor ? "› " : " "}${displayText(method.label)}`,
30077
+ color: inkColor(index === cursor ? getPalette().brandBright : getPalette().dim)
30078
+ });
30079
+ });
30080
+ } else {
30081
+ notices.forEach((notice, index) => {
30082
+ rows.push({
30083
+ key: `notice-${index}`,
30084
+ text: ` ${displayText(notice.message)}`
30085
+ });
30086
+ if (notice.url !== void 0) rows.push({
30087
+ key: `url-${index}`,
30088
+ text: ` ${displayText(notice.url)}`,
30089
+ color: inkColor(getPalette().brandBright)
30090
+ });
30091
+ if (notice.code !== void 0) rows.push({
30092
+ key: `code-${index}`,
30093
+ text: ` code ${displayText(notice.code)}`,
30094
+ color: inkColor(getPalette().success),
30095
+ bold: true
30096
+ });
30097
+ });
30098
+ if (prompt !== void 0) {
30099
+ rows.push({
30100
+ key: "prompt",
30101
+ text: ` ${displayText(prompt.message)}`,
30102
+ color: inkColor(getPalette().brandBright)
30103
+ });
30104
+ if (prompt.kind === "select") prompt.options.forEach((option, index) => rows.push({
30105
+ key: `option-${option.id}`,
30106
+ text: `${index === promptCursor ? "› " : " "}${displayText(option.label)}${option.description === void 0 ? "" : ` · ${displayText(option.description)}`}`,
30107
+ color: inkColor(index === promptCursor ? getPalette().brandBright : getPalette().dim)
30108
+ }));
30109
+ else {
30110
+ const shown = prompt.kind === "secret" ? "•".repeat([...draft].length) : displayText(draft);
30111
+ rows.push({
30112
+ key: "draft",
30113
+ text: ` ${shown}▏`,
30114
+ color: inkColor(getPalette().text)
30115
+ });
30116
+ }
30117
+ } else rows.push({
30118
+ key: "waiting",
30119
+ text: " waiting for provider…",
30120
+ color: inkColor(getPalette().dim)
30121
+ });
30122
+ if (copyState !== void 0) rows.push({
30123
+ key: "copy",
30124
+ text: ` ${singleLineText(copyState)}`,
30125
+ color: inkColor(copyState === "copied" ? getPalette().success : getPalette().error)
30126
+ });
30127
+ }
30128
+ const visible = rows.slice(Math.max(0, rows.length - viewport.bodyRows));
30129
+ const footer = phase === "methods" ? "↑↓ choose · enter continue · esc/q back" : "enter answer · c copy URL/code · esc cancel login";
30130
+ return (0, import_react.createElement)(Box, {
30131
+ flexDirection: "column",
30132
+ width: viewport.outerColumns,
30133
+ paddingX: 1,
30134
+ borderStyle: "round",
30135
+ borderColor: inkColor(getPalette().brand)
30136
+ }, (0, import_react.createElement)(Text, {
30137
+ color: inkColor(getPalette().brand),
30138
+ bold: true,
30139
+ wrap: "truncate-end"
30140
+ }, truncateColumns(`/model · login ${displayText(props.row.label)}`, viewport.contentColumns)), ...visible.map((row) => (0, import_react.createElement)(Text, {
30141
+ key: row.key,
30142
+ color: row.color,
30143
+ bold: row.bold,
30144
+ wrap: "truncate-end"
30145
+ }, truncateColumns(row.text, viewport.contentColumns))), (0, import_react.createElement)(Text, {
30146
+ color: inkColor(getPalette().dim),
30147
+ wrap: "truncate-end"
30148
+ }, truncateColumns(footer, viewport.contentColumns)));
30149
+ }
30150
+ function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }) {
30151
+ const stdout = useStdout().stdout;
30152
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
30153
+ const [busy, setBusy] = (0, import_react.useState)(false);
30154
+ const [error, setError] = (0, import_react.useState)(void 0);
30155
+ useInput((input, key) => {
30156
+ if (busy) return;
30157
+ if (key.escape || input === "n" || input === "N") {
30158
+ back();
30159
+ return;
30160
+ }
30161
+ if (input !== "y" && input !== "Y") return;
30162
+ setBusy(true);
30163
+ setError(void 0);
30164
+ confirm(row).then(done, (reason) => {
30165
+ setBusy(false);
30166
+ setError(reason instanceof Error ? reason.message : String(reason));
30167
+ });
30168
+ });
30169
+ if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
30170
+ if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("y logout · n/esc back", viewport.contentColumns));
30171
+ return (0, import_react.createElement)(Box, {
30172
+ flexDirection: "column",
30173
+ width: viewport.outerColumns,
30174
+ paddingX: 1,
30175
+ borderStyle: "round",
30176
+ borderColor: inkColor(getPalette().warn)
30177
+ }, (0, import_react.createElement)(Text, {
30178
+ color: inkColor(getPalette().warn),
30179
+ bold: true,
30180
+ wrap: "truncate-end"
30181
+ }, truncateColumns("/model · logout provider", viewport.contentColumns)), (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(` remove ${displayText(row.label)} login record`, viewport.contentColumns)), (0, import_react.createElement)(Text, {
30182
+ color: inkColor(getPalette().dim),
30183
+ wrap: "truncate-end"
30184
+ }, truncateColumns(" provider endpoint and model configuration stay unchanged", viewport.contentColumns)), error === void 0 ? void 0 : (0, import_react.createElement)(Text, {
30185
+ color: inkColor(getPalette().error),
30186
+ wrap: "truncate-end"
30187
+ }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns)), (0, import_react.createElement)(Text, {
30188
+ color: inkColor(getPalette().dim),
30189
+ wrap: "truncate-end"
30190
+ }, truncateColumns(busy ? "working…" : "y confirm · n/esc back", viewport.contentColumns)));
30191
+ }
30192
+ //#endregion
30193
+ //#region src/attachments.ts
30194
+ /** Terminal image-file adapter over the Harness durable attachment service. */
30195
+ const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
30196
+ ".png",
30197
+ ".jpg",
30198
+ ".jpeg",
30199
+ ".webp",
30200
+ ".gif"
30201
+ ]);
30202
+ /** Detect the supported encoded raster formats from bytes, never from a path suffix. */
30203
+ function detectImageMediaType(data) {
30204
+ if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10) return "image/png";
30205
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
30206
+ if (data.length >= 6) {
30207
+ const signature = String.fromCharCode(...data.subarray(0, 6));
30208
+ if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
30209
+ }
30210
+ if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
30211
+ }
30212
+ /** Whether a path-like token is worth probing as an image attachment. */
30213
+ function looksLikeImagePath(path) {
30214
+ return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
30215
+ }
30216
+ /** Parse a terminal paste/drop containing only one or more image paths. */
30217
+ function parsePastedImagePaths(input) {
30218
+ const text = input.trim();
30219
+ if (text === "") return [];
30220
+ const tokens = [];
30221
+ for (const match of text.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/gu)) {
30222
+ const token = match[1] ?? match[2] ?? match[3];
30223
+ if (token === void 0) continue;
30224
+ let path = token;
30225
+ if (path.startsWith("file://")) try {
30226
+ path = fileURLToPath(path);
30227
+ } catch {
30228
+ return [];
30229
+ }
30230
+ if (!looksLikeImagePath(path)) return [];
30231
+ tokens.push(path);
30232
+ }
30233
+ return tokens;
30234
+ }
30235
+ /** Validate path, byte size and encoded signature without writing an attachment object. */
30236
+ async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
30237
+ if (paths.length === 0) return [];
30238
+ if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
30239
+ if (paths.length > attachments.imageLimits.maxImagesPerMessage) throw new Error(`too many images (${paths.length}; limit ${attachments.imageLimits.maxImagesPerMessage})`);
30240
+ const inspected = [];
30241
+ let totalBytes = 0;
30242
+ for (const raw of paths) {
30243
+ const path = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
30244
+ let facts;
30245
+ try {
30246
+ facts = await stat(path);
30247
+ } catch (error) {
30248
+ throw new Error(`cannot read image "${raw}": ${error instanceof Error ? error.message : String(error)}`);
30249
+ }
30250
+ if (!facts.isFile()) throw new Error(`image path is not a file: "${raw}"`);
30251
+ if (facts.size > attachments.imageLimits.maxImageBytes) throw new Error(`image "${basename(path)}" is ${facts.size} bytes; limit ${attachments.imageLimits.maxImageBytes}`);
30252
+ totalBytes += facts.size;
30253
+ if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error(`image batch is ${totalBytes} bytes; limit ${attachments.imageLimits.maxMessageImageBytes}`);
30254
+ const handle = await open(path, "r");
30255
+ try {
30256
+ const signature = /* @__PURE__ */ new Uint8Array(16);
30257
+ const { bytesRead } = await handle.read(signature, 0, signature.length, 0);
30258
+ const mediaType = detectImageMediaType(signature.subarray(0, bytesRead));
30259
+ if (mediaType === void 0 || !attachments.imageLimits.mediaTypes.includes(mediaType)) throw new Error(`unsupported image file "${raw}" (expected PNG, JPEG, WebP, or GIF)`);
30260
+ inspected.push({
30261
+ path,
30262
+ name: basename(path),
30263
+ mediaType,
30264
+ bytes: facts.size
30265
+ });
30266
+ } finally {
30267
+ await handle.close();
30268
+ }
30269
+ }
30270
+ return inspected;
30271
+ }
30272
+ /** Read, validate, and persist an ordered image path list as model content blocks. */
30273
+ async function saveImagePaths(paths, attachments, signal) {
30274
+ if (paths.length === 0) return [];
30275
+ if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
30276
+ const checkCancelled = () => {
30277
+ if (signal?.aborted === true) throw new Error("image submission cancelled");
30278
+ };
30279
+ const inputs = [];
30280
+ for (const path of paths) {
30281
+ checkCancelled();
30282
+ let data;
30283
+ try {
30284
+ data = await readFile(path);
30285
+ } catch (error) {
30286
+ throw new Error(`cannot read image "${path}": ${error instanceof Error ? error.message : String(error)}`);
30287
+ }
30288
+ const mediaType = detectImageMediaType(data);
30289
+ if (mediaType === void 0) throw new Error(`unsupported image file "${path}" (expected PNG, JPEG, WebP, or GIF)`);
30290
+ inputs.push({
30291
+ data,
30292
+ mediaType,
30293
+ name: basename(path)
30294
+ });
30295
+ }
30296
+ checkCancelled();
30297
+ const refs = await attachments.saveImages(inputs);
30298
+ checkCancelled();
30299
+ return refs.map((attachment) => ({
30300
+ type: "image",
30301
+ attachment
30302
+ }));
30303
+ }
30304
+ //#endregion
29449
30305
  //#region src/app.ts
29450
30306
  /**
29451
30307
  * The Ink terminal app: whale-and-wordmark header in DeepSeek blue, the live
@@ -29490,20 +30346,121 @@ function readSettledRowCap() {
29490
30346
  const PASTE_BRACKET_TIMEOUT_MS = 1e3;
29491
30347
  /** Release the held frame after Ink has replayed the source-backed Static rows. */
29492
30348
  const SYNCHRONIZED_UPDATE_END = "\x1B[?2026l";
30349
+ /** One source of truth for TUI-owned slash commands in completion and `/help`. */
30350
+ const LOCAL_COMMANDS = [
30351
+ {
30352
+ label: "/help",
30353
+ description: "show this overlay"
30354
+ },
30355
+ {
30356
+ label: "/model",
30357
+ description: "switch the model and manage providers"
30358
+ },
30359
+ {
30360
+ label: "/effort",
30361
+ description: "adjust reasoning effort for the current model"
30362
+ },
30363
+ {
30364
+ label: "/mode",
30365
+ description: "inspect or select the agent preset (/mode [preset])"
30366
+ },
30367
+ {
30368
+ label: "/permission",
30369
+ description: "inspect or select the permission preset (/permission [preset])"
30370
+ },
30371
+ {
30372
+ label: "/new",
30373
+ description: "create and switch to a fresh session (/new [preset])"
30374
+ },
30375
+ {
30376
+ label: "/fork",
30377
+ description: "fork at the latest completed turn (/fork [event-seq])"
30378
+ },
30379
+ {
30380
+ label: "/resume",
30381
+ description: "browse or switch root sessions (/resume [id|prefix])"
30382
+ },
30383
+ {
30384
+ label: "/plugin",
30385
+ description: "inspect the live plugin composition"
30386
+ },
30387
+ {
30388
+ label: "/jobs",
30389
+ description: "inspect background jobs"
30390
+ },
30391
+ {
30392
+ label: "/statusline",
30393
+ description: "customize the status line items"
30394
+ },
30395
+ {
30396
+ label: "/theme",
30397
+ description: "switch the color theme"
30398
+ },
30399
+ {
30400
+ label: "/history",
30401
+ description: "search and recall past prompts"
30402
+ },
30403
+ {
30404
+ label: "/agents",
30405
+ description: "inspect subagent sessions of this conversation"
30406
+ },
30407
+ {
30408
+ label: "/todos",
30409
+ description: "inspect the full todo list"
30410
+ },
30411
+ {
30412
+ label: "/subagent",
30413
+ description: "choose the model delegated subagents run on"
30414
+ },
30415
+ {
30416
+ label: "/delete",
30417
+ description: "delete a session and its subagent threads"
30418
+ },
30419
+ {
30420
+ label: "/clear",
30421
+ description: "clear the screen"
30422
+ },
30423
+ {
30424
+ label: "/export",
30425
+ description: "export the transcript to markdown (/export [path])"
30426
+ },
30427
+ {
30428
+ label: "/title",
30429
+ description: "rename this session (/title <text>)"
30430
+ },
30431
+ {
30432
+ label: "/copy",
30433
+ description: "copy the latest assistant response"
30434
+ },
30435
+ {
30436
+ label: "/diff",
30437
+ description: "inspect Git changes (/diff [--staged|ref])"
30438
+ },
30439
+ {
30440
+ label: "/review",
30441
+ description: "review Git changes under read-only permissions"
30442
+ },
30443
+ {
30444
+ label: "/quit",
30445
+ description: "exit"
30446
+ }
30447
+ ];
30448
+ const LOCAL_COMMAND_NAMES = new Set(LOCAL_COMMANDS.map((command) => command.label.slice(1)));
29493
30449
  /** Pad text with spaces to a visible-column target (menu name column). */
29494
30450
  function padColumns(text, width) {
29495
30451
  const clipped = truncateColumns(singleLineText(text), width);
29496
30452
  return clipped + " ".repeat(Math.max(0, width - visibleColumns(clipped)));
29497
30453
  }
29498
30454
  /** Interval-driven frame counter for one self-contained animated leaf. */
29499
- function useFrames(intervalMs) {
30455
+ function useFrames(intervalMs, active = true) {
29500
30456
  const [tick, setTick] = (0, import_react.useState)(0);
29501
30457
  (0, import_react.useEffect)(() => {
30458
+ if (!active) return;
29502
30459
  const id = setInterval(() => setTick((current) => current + 1), intervalMs);
29503
30460
  return () => {
29504
30461
  clearInterval(id);
29505
30462
  };
29506
- }, [intervalMs]);
30463
+ }, [active, intervalMs]);
29507
30464
  return tick;
29508
30465
  }
29509
30466
  /**
@@ -29520,12 +30477,7 @@ function useStableInput(handler, active) {
29520
30477
  }, []);
29521
30478
  useInput(stableHandler, { isActive: active });
29522
30479
  }
29523
- /**
29524
- * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
29525
- * ring trail clockwise around the eight outer positions (8 frames × 125ms =
29526
- * the web's 1s cycle). Replaces the plain busy ellipsis as the composer's
29527
- * prompt marker and leads the Deep-diving line.
29528
- */
30480
+ /** The original web StateDot chase used by the busy composer marker. */
29529
30481
  function BusyChase() {
29530
30482
  const tick = useFrames(125);
29531
30483
  return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + " ");
@@ -29535,21 +30487,46 @@ function Caret() {
29535
30487
  const tick = useFrames(530);
29536
30488
  return (0, import_react.createElement)(Text, null, caretVisible(tick) ? "▍" : " ");
29537
30489
  }
29538
- /** Blinking input cursor: inverse block while the caret phase is on. */
29539
- function CursorBlock({ char }) {
29540
- const tick = useFrames(530);
29541
- return (0, import_react.createElement)(Text, { inverse: caretVisible(tick) || void 0 }, char);
30490
+ /** One resettable input-caret phase shared by the entire composer. */
30491
+ function useCursorBlink(active) {
30492
+ const [epoch, setEpoch] = (0, import_react.useState)(0);
30493
+ const [visible, setVisible] = (0, import_react.useState)(true);
30494
+ (0, import_react.useEffect)(() => {
30495
+ setVisible(true);
30496
+ if (!active) return;
30497
+ const id = setInterval(() => setVisible((current) => !current), 530);
30498
+ return () => {
30499
+ clearInterval(id);
30500
+ };
30501
+ }, [active, epoch]);
30502
+ return {
30503
+ visible,
30504
+ reset: (0, import_react.useCallback)(() => {
30505
+ setVisible(true);
30506
+ setEpoch((current) => current + 1);
30507
+ }, [])
30508
+ };
29542
30509
  }
29543
30510
  /**
29544
- * The busy line, web TurnStatus contract: the StateDot chase leads the plain
29545
- * `Deep diving...` label, with the elapsed clock appended only once the turn
29546
- * has clearly been running (15s) — anchored to `turn/start` so a resumed
29547
- * mid-turn keeps the real time.
30511
+ * The busy line, web TurnStatus contract: a continuously moving blue gradient
30512
+ * paints the complete `Deep diving...` label, with the elapsed clock appended
30513
+ * only once the turn has clearly been running (15s) — anchored to `turn/start`
30514
+ * so a resumed mid-turn keeps the real time.
29548
30515
  */
29549
30516
  function DeepDivingLine({ since }) {
29550
- useFrames(1e3);
30517
+ const tick = useFrames(33);
29551
30518
  const elapsed = since === 0 ? 0 : Date.now() - since;
29552
- return (0, import_react.createElement)(Box, { flexDirection: "row" }, (0, import_react.createElement)(BusyChase), (0, import_react.createElement)(Text, { dimColor: true }, elapsed >= 15e3 ? `Deep diving... ${runClock(elapsed)}` : "Deep diving..."));
30519
+ const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "Deep diving...";
30520
+ const palette = getPalette();
30521
+ const graphemes = splitGraphemes(text);
30522
+ return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...graphemes.map((grapheme, index) => {
30523
+ const sparkle = grapheme.text === "✻";
30524
+ return (0, import_react.createElement)(Text, {
30525
+ key: `${grapheme.start}-${grapheme.end}`,
30526
+ color: inkColor(sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
30527
+ bold: sparkle || void 0
30528
+ }, grapheme.text);
30529
+ }));
29553
30530
  }
29554
30531
  /**
29555
30532
  * The streaming buffer rendered with a hard size cap: the live region must
@@ -30544,7 +31521,8 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
30544
31521
  wrap: "truncate-end"
30545
31522
  }, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
30546
31523
  const index = rows.indexOf(row);
30547
- const label = displayText(`${row.providerName} · ${row.modelName}`);
31524
+ const capability = row.inputModalities?.includes("image") === true ? " · image" : "";
31525
+ const label = displayText(`${row.providerName} · ${row.modelName}${capability}`);
30548
31526
  return (0, import_react.createElement)(Text, {
30549
31527
  key: `${row.provider}/${row.model}`,
30550
31528
  color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
@@ -30567,7 +31545,7 @@ function providerStateLabel(row) {
30567
31545
  return `${route} · ${row.configured ? "provider auth" : "not configured"}`;
30568
31546
  }
30569
31547
  /** The provider-management stage reached from /model with `a`. */
30570
- function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, onRemove, onRetry, onBack }) {
31548
+ function ProviderPanel({ directory, error, authorizations, authorizationError, onCredential, onConfigure, onUnset, onRemove, onLogin, onLogout, onRetry, onBack }) {
30571
31549
  const stdout = useStdout().stdout;
30572
31550
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
30573
31551
  const rows = directory?.rows ?? [];
@@ -30630,6 +31608,19 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
30630
31608
  else onRemove(target);
30631
31609
  return;
30632
31610
  }
31611
+ const authorization = authorizationForProvider(authorizations, target.provider);
31612
+ if (input === "l" || input === "L") {
31613
+ if (authorization === void 0) setActionError("this provider offers no interactive login flow");
31614
+ else if (authorization.inFlight) setActionError("a login attempt is already running for this provider");
31615
+ else onLogin(target, authorization);
31616
+ return;
31617
+ }
31618
+ if (input === "o" || input === "O") {
31619
+ if (authorization === void 0 || !authorization.record.configured) setActionError("this provider has no login record to remove");
31620
+ else if (!authorization.record.writable) setActionError("this login record is read-only");
31621
+ else onLogout(target, authorization);
31622
+ return;
31623
+ }
30633
31624
  if (key.return) {
30634
31625
  if (target.settingsNs.length === 0) setActionError("this provider is not managed by Harness settings");
30635
31626
  else if (target.credential?.kind === "error") setActionError("credential status is unavailable; retry before writing");
@@ -30659,6 +31650,16 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
30659
31650
  color: inkColor(getPalette().warn),
30660
31651
  wrap: "truncate-end"
30661
31652
  }, truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns))),
31653
+ ...authorizationError === void 0 ? [] : [(0, import_react.createElement)(Text, {
31654
+ key: "authorization-error",
31655
+ color: inkColor(getPalette().warn),
31656
+ wrap: "truncate-end"
31657
+ }, truncateColumns(` login status unavailable: ${singleLineText(authorizationError)}`, viewport.contentColumns))],
31658
+ ...(authorizations?.failures ?? []).map((failure, index) => (0, import_react.createElement)(Text, {
31659
+ key: `authorization-failure-${index}`,
31660
+ color: inkColor(getPalette().warn),
31661
+ wrap: "truncate-end"
31662
+ }, truncateColumns(` ${singleLineText(failure)}`, viewport.contentColumns))),
30662
31663
  ...rows.length === 0 ? [(0, import_react.createElement)(Text, {
30663
31664
  key: "empty",
30664
31665
  color: inkColor(getPalette().dim),
@@ -30680,7 +31681,10 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
30680
31681
  wrap: "truncate-end"
30681
31682
  }, truncateColumns(`/model — providers${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
30682
31683
  const index = rows.indexOf(row);
30683
- const label = `${row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`} · ${providerStateLabel(row)}${row.removable ? " · custom" : ""}`;
31684
+ const identity = row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`;
31685
+ const authorization = authorizationForProvider(authorizations, row.provider);
31686
+ const authLabel = !(row.credential?.kind === "facts" && row.credential.configured) || authorization?.record.configured === true || authorization?.inFlight === true ? ` · ${providerAuthorizationStatus(authorization)}` : "";
31687
+ const label = `${identity} · ${providerStateLabel(row)}${authLabel}${row.removable ? " · custom" : ""}`;
30684
31688
  return (0, import_react.createElement)(Text, {
30685
31689
  key: row.provider,
30686
31690
  color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
@@ -30689,7 +31693,7 @@ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, o
30689
31693
  }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
30690
31694
  color: inkColor(getPalette().dim),
30691
31695
  wrap: "truncate-end"
30692
- }, truncateColumns("↑↓ move · tab configure · enter add/update key · d remove key · x remove custom provider · r retry · esc back", viewport.contentColumns)));
31696
+ }, truncateColumns("↑↓ move · enter key · l login · o logout · tab configure · d remove key · x remove provider · r retry · esc back", viewport.contentColumns)));
30693
31697
  }
30694
31698
  /** Provider configuration editor: only explicit models are written to settings. */
30695
31699
  function ProviderConfigurationPanel({ target, catalog, save, done, back }) {
@@ -31002,7 +32006,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31002
32006
  key: "key-submit",
31003
32007
  dimColor: true,
31004
32008
  wrap: "truncate-end"
31005
- }, " enter submit · alt+enter / ctrl+j newline · up/down history · tab complete"),
32009
+ }, " enter submit · up/down history · tab complete"),
31006
32010
  (0, import_react.createElement)(Text, {
31007
32011
  key: "key-mentions",
31008
32012
  dimColor: true,
@@ -31039,31 +32043,8 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31039
32043
  color: inkColor(getPalette().error),
31040
32044
  wrap: "truncate-end"
31041
32045
  }, truncateColumns(` command catalog unavailable: ${singleLineText(commandError)}`, viewport.contentColumns))],
31042
- (0, import_react.createElement)(Box, { key: "local-help" }, row("/help", "show this overlay")),
31043
- (0, import_react.createElement)(Box, { key: "local-model" }, row("/model", "switch the model")),
31044
- (0, import_react.createElement)(Box, { key: "local-effort" }, row("/effort", "adjust reasoning effort for the current model")),
31045
- (0, import_react.createElement)(Box, { key: "local-mode" }, row("/mode", "inspect or select the agent preset (/mode [preset])")),
31046
- (0, import_react.createElement)(Box, { key: "local-permission" }, row("/permission", "inspect or select the permission preset (/permission [preset])")),
31047
- (0, import_react.createElement)(Box, { key: "local-new" }, row("/new", "create and switch to a fresh session (/new [preset])")),
31048
- (0, import_react.createElement)(Box, { key: "local-fork" }, row("/fork", "fork at the latest completed turn (/fork [event-seq])")),
31049
- (0, import_react.createElement)(Box, { key: "local-resume" }, row("/resume", "browse or switch root sessions (/resume [id|prefix])")),
31050
- (0, import_react.createElement)(Box, { key: "local-plugin" }, row("/plugin", "inspect the live plugin composition")),
31051
- (0, import_react.createElement)(Box, { key: "local-jobs" }, row("/jobs", "inspect background jobs")),
31052
- (0, import_react.createElement)(Box, { key: "local-statusline" }, row("/statusline", "customize the status line items")),
31053
- (0, import_react.createElement)(Box, { key: "local-theme" }, row("/theme", "switch the color theme")),
31054
- (0, import_react.createElement)(Box, { key: "local-history" }, row("/history", "search and recall past prompts")),
31055
- (0, import_react.createElement)(Box, { key: "local-agents" }, row("/agents", "inspect subagent sessions of this conversation")),
31056
- (0, import_react.createElement)(Box, { key: "local-todos" }, row("/todos", "inspect the full todo list")),
31057
- (0, import_react.createElement)(Box, { key: "local-subagent" }, row("/subagent", "choose the model delegated subagents run on")),
31058
- (0, import_react.createElement)(Box, { key: "local-delete" }, row("/delete", "delete a session and its subagent threads")),
31059
- (0, import_react.createElement)(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
31060
- (0, import_react.createElement)(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
31061
- (0, import_react.createElement)(Box, { key: "local-title" }, row("/title", "rename this session (/title <text>)")),
31062
- (0, import_react.createElement)(Box, { key: "local-copy" }, row("/copy", "copy the latest assistant response")),
31063
- (0, import_react.createElement)(Box, { key: "local-diff" }, row("/diff", "inspect Git changes (/diff [--staged|ref])")),
31064
- (0, import_react.createElement)(Box, { key: "local-review" }, row("/review", "review Git changes under read-only permissions")),
31065
- (0, import_react.createElement)(Box, { key: "local-quit" }, row("/quit", "exit")),
31066
- ...descriptors.map((descriptor) => (0, import_react.createElement)(Text, {
32046
+ ...LOCAL_COMMANDS.map((command) => (0, import_react.createElement)(Box, { key: `local-${command.label.slice(1)}` }, row(command.label, command.description))),
32047
+ ...descriptors.filter((descriptor) => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map((descriptor) => (0, import_react.createElement)(Text, {
31067
32048
  key: `command-${descriptor.name}`,
31068
32049
  dimColor: true,
31069
32050
  wrap: "truncate-end"
@@ -31124,49 +32105,6 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31124
32105
  function verboseLine(text, columns) {
31125
32106
  return truncateColumns(displayText(text).replace(/\n/gu, " ↵ ").replace(/\t/gu, " "), Math.max(1, columns));
31126
32107
  }
31127
- /** Identify one whole-chunk key sequence Ink drops or blurs. */
31128
- function annotateRawKey(chunk) {
31129
- switch (chunk) {
31130
- case "": return "delete-backward";
31131
- case "\x1B":
31132
- case "\x1B\b": return "delete-word-backward";
31133
- case "\x1B[3~":
31134
- case "\x1B[3;2~": return "delete-forward";
31135
- case "\x1B[3;3~":
31136
- case "\x1B[3;5~": return "delete-word-forward";
31137
- case "\x1B[H":
31138
- case "\x1B[1~":
31139
- case "\x1B[7~":
31140
- case "\x1BOH": return "home";
31141
- case "\x1B[F":
31142
- case "\x1B[4~":
31143
- case "\x1B[8~":
31144
- case "\x1BOF": return "end";
31145
- default: return;
31146
- }
31147
- }
31148
- /**
31149
- * One-row editor window keeping the logical cursor visible in long drafts.
31150
- * The caret and its surroundings slice at grapheme boundaries: splitting a
31151
- * star-plane surrogate pair would render an isolated half under the block
31152
- * caret with a width the terminal never draws.
31153
- */
31154
- function editorWindow(value, cursor, columns) {
31155
- const width = Math.max(1, columns);
31156
- const normalize = (text) => displayText(text).replace(/\n/gu, "↵").replace(/\t/gu, " ");
31157
- const site = clampCursor(value, cursor);
31158
- const caretSpan = splitGraphemes(value).find((span) => span.start === site);
31159
- const caret = caretSpan === void 0 ? " " : normalize(caretSpan.text);
31160
- const rest = value.slice(caretSpan === void 0 ? site : caretSpan.end);
31161
- const remaining = Math.max(0, width - visibleColumns(caret));
31162
- const afterBudget = Math.min(Math.floor(remaining / 3), visibleColumns(normalize(rest)));
31163
- const beforeBudget = Math.max(0, remaining - afterBudget);
31164
- return {
31165
- before: beforeBudget === 0 ? "" : displayTail(normalize(value.slice(0, site)), beforeBudget, 1).text,
31166
- caret,
31167
- after: afterBudget === 0 ? "" : truncateColumns(normalize(rest), afterBudget)
31168
- };
31169
- }
31170
32108
  /** The empty-composer placeholder text (shared by the static and wave paths). */
31171
32109
  const COMPOSER_PLACEHOLDER = "type a message · / commands · @ mentions";
31172
32110
  /** Adjacent cells with identical styling merge into one styled Text span. */
@@ -31332,130 +32270,11 @@ const MemoStaticTranscript = (0, import_react.memo)(StaticTranscript);
31332
32270
  function completionCandidates(value, descriptors, skills) {
31333
32271
  if (!value.startsWith("/")) return [];
31334
32272
  const prefix = value.slice(1).split(" ")[0] ?? "";
31335
- const local = [
31336
- {
31337
- label: "/help",
31338
- description: "show commands",
31339
- origin: "command"
31340
- },
31341
- {
31342
- label: "/model",
31343
- description: "switch the model",
31344
- origin: "command"
31345
- },
31346
- {
31347
- label: "/effort",
31348
- description: "adjust reasoning effort for the current model",
31349
- origin: "command"
31350
- },
31351
- {
31352
- label: "/mode",
31353
- description: "select the agent preset",
31354
- origin: "command"
31355
- },
31356
- {
31357
- label: "/permission",
31358
- description: "inspect or select the permission preset",
31359
- origin: "command"
31360
- },
31361
- {
31362
- label: "/new",
31363
- description: "start a fresh session",
31364
- origin: "command"
31365
- },
31366
- {
31367
- label: "/fork",
31368
- description: "fork at a completed turn",
31369
- origin: "command"
31370
- },
31371
- {
31372
- label: "/resume",
31373
- description: "browse or switch sessions",
31374
- origin: "command"
31375
- },
31376
- {
31377
- label: "/plugin",
31378
- description: "inspect the plugin composition",
31379
- origin: "command"
31380
- },
31381
- {
31382
- label: "/jobs",
31383
- description: "inspect background jobs",
31384
- origin: "command"
31385
- },
31386
- {
31387
- label: "/statusline",
31388
- description: "customize the status line",
31389
- origin: "command"
31390
- },
31391
- {
31392
- label: "/theme",
31393
- description: "switch the color theme",
31394
- origin: "command"
31395
- },
31396
- {
31397
- label: "/history",
31398
- description: "search and recall past prompts",
31399
- origin: "command"
31400
- },
31401
- {
31402
- label: "/agents",
31403
- description: "inspect subagent sessions of this conversation",
31404
- origin: "command"
31405
- },
31406
- {
31407
- label: "/todos",
31408
- description: "inspect the full todo list",
31409
- origin: "command"
31410
- },
31411
- {
31412
- label: "/subagent",
31413
- description: "choose the model delegated subagents run on",
31414
- origin: "command"
31415
- },
31416
- {
31417
- label: "/delete",
31418
- description: "delete a session and its subagent threads",
31419
- origin: "command"
31420
- },
31421
- {
31422
- label: "/clear",
31423
- description: "clear the screen",
31424
- origin: "command"
31425
- },
31426
- {
31427
- label: "/export",
31428
- description: "export the transcript to markdown",
31429
- origin: "command"
31430
- },
31431
- {
31432
- label: "/title",
31433
- description: "rename this session",
31434
- origin: "command"
31435
- },
31436
- {
31437
- label: "/copy",
31438
- description: "copy the latest assistant response",
31439
- origin: "command"
31440
- },
31441
- {
31442
- label: "/diff",
31443
- description: "inspect Git changes",
31444
- origin: "command"
31445
- },
31446
- {
31447
- label: "/review",
31448
- description: "review changes read-only",
31449
- origin: "command"
31450
- },
31451
- {
31452
- label: "/quit",
31453
- description: "exit",
31454
- origin: "command"
31455
- }
31456
- ];
31457
- const localNames = new Set(local.map((candidate) => candidate.label.slice(1)));
31458
- const registry = descriptors.filter((descriptor) => !localNames.has(descriptor.name)).map((descriptor) => ({
32273
+ const local = LOCAL_COMMANDS.map((command) => ({
32274
+ ...command,
32275
+ origin: "command"
32276
+ }));
32277
+ const registry = descriptors.filter((descriptor) => !LOCAL_COMMAND_NAMES.has(descriptor.name)).map((descriptor) => ({
31459
32278
  label: `/${descriptor.name}`,
31460
32279
  description: descriptor.description,
31461
32280
  origin: "command"
@@ -31534,25 +32353,52 @@ function CompletionMenu({ active, mention, index, rows }) {
31534
32353
  * While a modal (approval / question / model panel) owns the keys, the
31535
32354
  * box passes every key through untouched.
31536
32355
  */
31537
- function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
32356
+ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
31538
32357
  const columns = useStdout().stdout?.columns ?? 80;
32358
+ const editorColumns = Math.max(1, columns - 6);
31539
32359
  const stdin = useStdin().stdin;
32360
+ const focusReporting = isVsCodeTerminalEnv();
31540
32361
  const [value, setValue] = (0, import_react.useState)("");
31541
32362
  const [cursor, setCursor] = (0, import_react.useState)(0);
32363
+ const valueRef = (0, import_react.useRef)(value);
32364
+ const cursorRef = (0, import_react.useRef)(cursor);
32365
+ valueRef.current = value;
32366
+ cursorRef.current = cursor;
32367
+ const [draftImages, setDraftImages] = (0, import_react.useState)([]);
32368
+ const draftImagesRef = (0, import_react.useRef)(draftImages);
32369
+ draftImagesRef.current = draftImages;
32370
+ const [preparingImages, setPreparingImages] = (0, import_react.useState)(false);
32371
+ const prepareAbortRef = (0, import_react.useRef)(void 0);
32372
+ const prepareEpochRef = (0, import_react.useRef)(0);
32373
+ const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages);
32374
+ (0, import_react.useEffect)(() => () => {
32375
+ prepareEpochRef.current += 1;
32376
+ prepareAbortRef.current?.abort();
32377
+ }, []);
31542
32378
  const killRef = (0, import_react.useRef)("");
31543
32379
  const preferredColumnRef = (0, import_react.useRef)(null);
31544
32380
  const editorScrollRef = (0, import_react.useRef)(0);
31545
32381
  const pasteBracketRef = (0, import_react.useRef)(false);
31546
32382
  /** Cancels the pending lost-paste safety timer (undefined when disarmed). */
31547
32383
  const pasteBracketCancelRef = (0, import_react.useRef)(void 0);
31548
- /** Annotation of the stdin chunk Ink is about to deliver to useInput. */
31549
- const rawAnnotation = (0, import_react.useRef)(void 0);
32384
+ /** Ordered editor tokens from the stdin chunk Ink is about to deliver. */
32385
+ const rawEditorTokens = (0, import_react.useRef)(void 0);
32386
+ /** VS Code focus state from xterm focus-report events; starts focused. */
32387
+ const terminalFocusedRef = (0, import_react.useRef)(true);
31550
32388
  const recall = (0, import_react.useRef)(beginRecall([], ""));
32389
+ (0, import_react.useEffect)(() => {
32390
+ preferredColumnRef.current = null;
32391
+ }, [editorColumns]);
31551
32392
  (0, import_react.useEffect)(() => {
31552
32393
  if (historyFill === void 0) return;
31553
32394
  const safe = sanitizeDraftText(historyFill.text);
32395
+ draftImagesRef.current = [];
32396
+ setDraftImages([]);
32397
+ valueRef.current = safe;
32398
+ cursorRef.current = safe.length;
31554
32399
  setValue(safe);
31555
32400
  setCursor(safe.length);
32401
+ resetCursorBlink();
31556
32402
  preferredColumnRef.current = null;
31557
32403
  setDismissedMenuValue(void 0);
31558
32404
  recall.current = {
@@ -31565,8 +32411,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31565
32411
  }, [
31566
32412
  historyFill,
31567
32413
  recallSpace,
31568
- historyConsumed
32414
+ historyConsumed,
32415
+ resetCursorBlink
31569
32416
  ]);
32417
+ (0, import_react.useEffect)(() => {
32418
+ setDraftImages((current) => {
32419
+ const next = current.filter((image) => value.includes(image.marker));
32420
+ draftImagesRef.current = next;
32421
+ return next.length === current.length ? current : next;
32422
+ });
32423
+ }, [value]);
31570
32424
  (0, import_react.useEffect)(() => {
31571
32425
  if (stdin === void 0) return;
31572
32426
  const originalRead = stdin.read.bind(stdin);
@@ -31574,13 +32428,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31574
32428
  const chunk = originalRead(...args);
31575
32429
  if (chunk === null) return chunk;
31576
32430
  const normalized = normalizeKeyboardChunk(typeof chunk === "string" ? chunk : String(chunk));
31577
- rawAnnotation.current = annotateRawKey(normalized);
31578
- return normalized;
32431
+ const input = focusReporting ? stripTerminalFocusEvents(normalized, (focused) => {
32432
+ terminalFocusedRef.current = focused;
32433
+ }) : normalized;
32434
+ rawEditorTokens.current = tokenizeRawEditorChunk(input);
32435
+ return input;
31579
32436
  };
31580
32437
  return () => {
31581
32438
  stdin.read = originalRead;
31582
32439
  };
31583
- }, [stdin]);
32440
+ }, [focusReporting, stdin]);
31584
32441
  if (recall.current.entries !== recallSpace) {
31585
32442
  const index = recall.current.index === null || recall.current.index < recallSpace.length ? recall.current.index : null;
31586
32443
  recall.current = {
@@ -31602,15 +32459,93 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31602
32459
  };
31603
32460
  const mentionActive = mentionToken !== void 0;
31604
32461
  const [mentionRows, setMentionRows] = (0, import_react.useState)([]);
32462
+ const mentionRequestRef = (0, import_react.useRef)(0);
32463
+ const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
32464
+ const uniqueImageMarker = (name, source, reserved = []) => {
32465
+ const safeName = singleLineText(sanitizeDraftText(name));
32466
+ let marker = source === "mention" ? `@${safeName}` : `[image: ${safeName}]`;
32467
+ let suffix = 2;
32468
+ while (valueRef.current.includes(marker) || draftImagesRef.current.some((image) => image.marker === marker) || reserved.includes(marker)) {
32469
+ marker = source === "mention" ? `@${safeName} (${suffix})` : `[image: ${safeName} ${suffix}]`;
32470
+ suffix += 1;
32471
+ }
32472
+ return marker;
32473
+ };
32474
+ const registerDraftImage = (inspection, marker) => {
32475
+ if (draftImagesRef.current.some((image) => sameImagePath(image.path, inspection.path))) {
32476
+ notify(`${inspection.name} is already attached`, "warning");
32477
+ return false;
32478
+ }
32479
+ const next = [...draftImagesRef.current, {
32480
+ ...inspection,
32481
+ marker
32482
+ }];
32483
+ draftImagesRef.current = next;
32484
+ setDraftImages(next);
32485
+ return true;
32486
+ };
32487
+ const insertDroppedImages = (paths) => {
32488
+ const originalValue = valueRef.current;
32489
+ const originalCursor = cursorRef.current;
32490
+ notify(`checking ${paths.length} image${paths.length === 1 ? "" : "s"}…`);
32491
+ inspectImages(paths).then((inspected) => {
32492
+ const additions = [];
32493
+ const markers = [];
32494
+ for (const inspection of inspected) {
32495
+ if ([...draftImagesRef.current, ...additions].some((image) => sameImagePath(image.path, inspection.path))) continue;
32496
+ const marker = uniqueImageMarker(inspection.name, "drop", markers);
32497
+ additions.push({
32498
+ ...inspection,
32499
+ marker
32500
+ });
32501
+ markers.push(marker);
32502
+ }
32503
+ if (additions.length === 0) {
32504
+ notify("those images are already attached", "warning");
32505
+ return;
32506
+ }
32507
+ const current = valueRef.current;
32508
+ const anchor = remapStableRange(originalValue, current, {
32509
+ start: originalCursor,
32510
+ end: originalCursor
32511
+ });
32512
+ if (anchor === void 0) {
32513
+ notify("draft changed at the image drop point; drop the images again", "warning");
32514
+ return;
32515
+ }
32516
+ const at = anchor.start;
32517
+ const insertion = `${at > 0 && !/\s$/u.test(current.slice(0, at)) ? " " : ""}${markers.join(" ")}${current.slice(at) === "" ? "" : " "}`;
32518
+ const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, insertion);
32519
+ const nextCursor = current === originalValue && cursorRef.current === originalCursor ? at + insertion.length : edit.cursor;
32520
+ valueRef.current = edit.value;
32521
+ cursorRef.current = nextCursor;
32522
+ setValue(edit.value);
32523
+ setCursor(nextCursor);
32524
+ resetCursorBlink();
32525
+ const nextImages = [...draftImagesRef.current, ...additions];
32526
+ draftImagesRef.current = nextImages;
32527
+ setDraftImages(nextImages);
32528
+ notify(`${additions.length} image${additions.length === 1 ? "" : "s"} ready for the next message`);
32529
+ }, (reason) => {
32530
+ notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
32531
+ });
32532
+ };
31605
32533
  (0, import_react.useEffect)(() => {
32534
+ const requestId = mentionRequestRef.current + 1;
32535
+ mentionRequestRef.current = requestId;
31606
32536
  if (!active || !mentionActive) {
31607
32537
  setMentionRows([]);
31608
32538
  return;
31609
32539
  }
31610
32540
  const controller = new AbortController();
31611
- setMentionRows([]);
31612
- loadMentions(mentionToken.query, controller.signal).then((rows) => setMentionRows(rows), () => {});
32541
+ const query = mentionToken.query;
32542
+ const timer = setTimeout(() => {
32543
+ loadMentions(query, controller.signal).then((rows) => {
32544
+ if (!controller.signal.aborted && mentionRequestRef.current === requestId) setMentionRows(rows);
32545
+ }, () => {});
32546
+ }, 50);
31613
32547
  return () => {
32548
+ clearTimeout(timer);
31614
32549
  controller.abort();
31615
32550
  };
31616
32551
  }, [
@@ -31618,8 +32553,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31618
32553
  mentionActive,
31619
32554
  mentionToken?.query
31620
32555
  ]);
31621
- const menuActive = (slashActive || mentionActive) && dismissedMenuValue !== value;
31622
- const menuRows = mentionActive ? mentionRows.map((row) => ({
32556
+ const menuActive = !preparingImages && (slashActive || mentionActive) && dismissedMenuValue !== value;
32557
+ const visibleMentionRows = mentionToken !== void 0 && isPathLikeMentionQuery(mentionToken.query) ? mentionRows.filter((row) => row.kind !== "session") : mentionRows;
32558
+ const menuRows = mentionActive ? visibleMentionRows.map((row) => ({
31623
32559
  label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
31624
32560
  description: row.description,
31625
32561
  origin: "mention"
@@ -31627,17 +32563,74 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31627
32563
  /** Accept the highlighted completion-menu candidate into the draft. */
31628
32564
  const acceptMenuCandidate = () => {
31629
32565
  if (mentionActive && mentionToken !== void 0) {
31630
- const row = mentionRows[completionIndex % mentionRows.length];
32566
+ if (visibleMentionRows.length === 0) return;
32567
+ const row = visibleMentionRows[completionIndex % visibleMentionRows.length];
31631
32568
  if (row !== void 0) {
32569
+ if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) {
32570
+ const tokenText = value.slice(mentionToken.start, cursor);
32571
+ const start = mentionToken.start;
32572
+ const originalValue = value;
32573
+ notify(`checking image ${basename(row.path)}…`);
32574
+ inspectImages([row.path]).then((inspected) => {
32575
+ const inspection = inspected[0];
32576
+ if (inspection === void 0) return;
32577
+ const current = valueRef.current;
32578
+ const anchor = remapStableRange(originalValue, current, {
32579
+ start,
32580
+ end: start + tokenText.length
32581
+ });
32582
+ if (anchor === void 0 || current.slice(anchor.start, anchor.end) !== tokenText) {
32583
+ notify("draft changed around the image mention; select it again", "warning");
32584
+ return;
32585
+ }
32586
+ if (draftImagesRef.current.some((image) => sameImagePath(image.path, inspection.path))) {
32587
+ const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, "");
32588
+ valueRef.current = edit.value;
32589
+ cursorRef.current = edit.cursor;
32590
+ setValue(edit.value);
32591
+ setCursor(edit.cursor);
32592
+ resetCursorBlink();
32593
+ setDismissedMenuValue(edit.value);
32594
+ notify(`${inspection.name} is already attached`, "warning");
32595
+ return;
32596
+ }
32597
+ const marker = uniqueImageMarker(inspection.name, "mention");
32598
+ const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, marker);
32599
+ valueRef.current = edit.value;
32600
+ cursorRef.current = edit.cursor;
32601
+ setValue(edit.value);
32602
+ setCursor(edit.cursor);
32603
+ resetCursorBlink();
32604
+ setDismissedMenuValue(edit.value);
32605
+ registerDraftImage(inspection, marker);
32606
+ notify(`${inspection.name} ready for the next message`);
32607
+ }, (reason) => {
32608
+ notify(`image attachment failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
32609
+ });
32610
+ setCompletionIndex(0);
32611
+ setDismissedMenuValue(void 0);
32612
+ return;
32613
+ }
31632
32614
  const insertion = row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
31633
- setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
31634
- setCursor(mentionToken.start + insertion.length);
32615
+ const nextValue = value.slice(0, mentionToken.start) + insertion + value.slice(cursor);
32616
+ const nextCursor = mentionToken.start + insertion.length;
32617
+ valueRef.current = nextValue;
32618
+ cursorRef.current = nextCursor;
32619
+ setValue(nextValue);
32620
+ setCursor(nextCursor);
32621
+ resetCursorBlink();
31635
32622
  }
31636
32623
  } else {
32624
+ if (candidates.length === 0) return;
31637
32625
  const candidate = candidates[completionIndex % candidates.length];
31638
32626
  if (candidate !== void 0) {
31639
- setValue(`${candidate.label} `);
31640
- setCursor(candidate.label.length + 1);
32627
+ const nextValue = `${candidate.label} `;
32628
+ const nextCursor = candidate.label.length + 1;
32629
+ valueRef.current = nextValue;
32630
+ cursorRef.current = nextCursor;
32631
+ setValue(nextValue);
32632
+ setCursor(nextCursor);
32633
+ resetCursorBlink();
31641
32634
  }
31642
32635
  }
31643
32636
  setCompletionIndex(0);
@@ -31646,20 +32639,101 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31646
32639
  /** Apply one editor edit: draft, cursor, kill buffer, menu reset. */
31647
32640
  const applyEdit = (edit) => {
31648
32641
  if (edit.killed !== void 0 && edit.killed !== "") killRef.current = edit.killed;
32642
+ valueRef.current = edit.value;
32643
+ cursorRef.current = edit.cursor;
31649
32644
  setValue(edit.value);
31650
32645
  setCursor(edit.cursor);
32646
+ resetCursorBlink();
31651
32647
  preferredColumnRef.current = null;
31652
32648
  setCompletionIndex(0);
31653
32649
  setDismissedMenuValue(void 0);
31654
32650
  };
31655
32651
  /** Move the cursor without editing; horizontal moves clear the column preference. */
31656
32652
  const moveCursorTo = (next) => {
31657
- if (next === cursor) return;
32653
+ resetCursorBlink();
32654
+ if (next === cursorRef.current) return;
32655
+ cursorRef.current = next;
31658
32656
  setCursor(next);
31659
32657
  preferredColumnRef.current = null;
31660
32658
  };
31661
- useInput((input, key) => {
32659
+ /** Apply an ordered raw-key batch against one current draft snapshot. */
32660
+ const applyRawEditorTokens = (tokens) => {
32661
+ let nextValue = valueRef.current;
32662
+ let nextCursor = cursorRef.current;
32663
+ for (const token of tokens) {
32664
+ if (token.kind === "text") {
32665
+ const edit = insertText(nextValue, nextCursor, token.text);
32666
+ nextValue = edit.value;
32667
+ nextCursor = edit.cursor;
32668
+ continue;
32669
+ }
32670
+ if (token.kind === "home") {
32671
+ nextCursor = moveToLineStart(nextValue, nextCursor, false);
32672
+ continue;
32673
+ }
32674
+ if (token.kind === "end") {
32675
+ nextCursor = moveToLineEnd(nextValue, nextCursor, false);
32676
+ continue;
32677
+ }
32678
+ const edit = token.kind === "delete-backward" ? deleteBackward(nextValue, nextCursor) : token.kind === "delete-word-backward" ? deleteWordBackward(nextValue, nextCursor) : token.kind === "delete-forward" ? deleteForward(nextValue, nextCursor) : deleteWordForward(nextValue, nextCursor);
32679
+ if (edit.killed !== void 0 && edit.killed !== "") killRef.current = edit.killed;
32680
+ nextValue = edit.value;
32681
+ nextCursor = edit.cursor;
32682
+ }
32683
+ valueRef.current = nextValue;
32684
+ cursorRef.current = nextCursor;
32685
+ setValue(nextValue);
32686
+ setCursor(nextCursor);
32687
+ resetCursorBlink();
32688
+ preferredColumnRef.current = null;
32689
+ setCompletionIndex(0);
32690
+ setDismissedMenuValue(void 0);
32691
+ };
32692
+ const cancelImageSubmission = () => {
32693
+ prepareEpochRef.current += 1;
32694
+ prepareAbortRef.current?.abort();
32695
+ prepareAbortRef.current = void 0;
32696
+ setPreparingImages(false);
32697
+ dismissNotice();
32698
+ notify("image submission cancelled", "warning");
32699
+ };
32700
+ /** Move through visual rows first, then cross history at the true edge. */
32701
+ const navigateVertical = (direction) => {
32702
+ const currentValue = valueRef.current;
32703
+ const currentCursor = cursorRef.current;
32704
+ const model = editorModel(currentValue, editorColumns);
32705
+ const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column;
32706
+ const next = moveCursorVertically(model, currentCursor, preferred, direction);
32707
+ if (next !== currentCursor) {
32708
+ cursorRef.current = next;
32709
+ setCursor(next);
32710
+ resetCursorBlink();
32711
+ preferredColumnRef.current = preferred;
32712
+ return;
32713
+ }
32714
+ if (recall.current.entries.length > 0 && shouldRecallNavigate(currentValue, currentCursor, recall.current.lastRecalled, direction)) {
32715
+ const step = direction < 0 ? recallOlder(recall.current, currentValue) : recallNewer(recall.current);
32716
+ recall.current = step.state;
32717
+ if (step.entry !== void 0) {
32718
+ const safe = sanitizeDraftText(step.entry);
32719
+ valueRef.current = safe;
32720
+ cursorRef.current = safe.length;
32721
+ setValue(safe);
32722
+ setCursor(safe.length);
32723
+ preferredColumnRef.current = null;
32724
+ setDismissedMenuValue(void 0);
32725
+ }
32726
+ }
32727
+ resetCursorBlink();
32728
+ };
32729
+ useStableInput((input, key) => {
31662
32730
  if (!active) return;
32731
+ const liveValue = valueRef.current;
32732
+ const liveCursor = cursorRef.current;
32733
+ if (preparingImages) {
32734
+ if (key.escape || key.ctrl && input === "c") cancelImageSubmission();
32735
+ return;
32736
+ }
31663
32737
  if (deleteConfirm !== void 0) {
31664
32738
  if (input === "y" || input === "Y") confirmDelete();
31665
32739
  else cancelDelete();
@@ -31675,6 +32749,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31675
32749
  return;
31676
32750
  }
31677
32751
  if (key.ctrl && input === "r") {
32752
+ if (focusReporting && !terminalFocusedRef.current) return;
31678
32753
  toggleReasoning();
31679
32754
  return;
31680
32755
  }
@@ -31684,17 +32759,22 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31684
32759
  }
31685
32760
  if (key.ctrl && input === "c") {
31686
32761
  if (busy) interrupt();
31687
- else if (value !== "") {
32762
+ else if (liveValue !== "") {
32763
+ valueRef.current = "";
32764
+ cursorRef.current = 0;
31688
32765
  setValue("");
31689
32766
  setCursor(0);
32767
+ resetCursorBlink();
32768
+ draftImagesRef.current = [];
32769
+ setDraftImages([]);
31690
32770
  setCompletionIndex(0);
31691
32771
  setDismissedMenuValue(void 0);
31692
32772
  } else quit();
31693
32773
  return;
31694
32774
  }
31695
32775
  if (key.ctrl && input === "d") {
31696
- if (value !== "") {
31697
- applyEdit(deleteForward(value, cursor));
32776
+ if (liveValue !== "") {
32777
+ applyEdit(deleteForward(liveValue, liveCursor));
31698
32778
  return;
31699
32779
  }
31700
32780
  if (busy) notify("cancel the running turn before exiting (Esc or Ctrl+C)", "warning");
@@ -31703,7 +32783,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31703
32783
  }
31704
32784
  if (key.escape) {
31705
32785
  if (menuActive) {
31706
- setDismissedMenuValue(value);
32786
+ setDismissedMenuValue(liveValue);
31707
32787
  return;
31708
32788
  }
31709
32789
  if (hasNotice) {
@@ -31713,31 +32793,63 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31713
32793
  if (busy) interrupt();
31714
32794
  return;
31715
32795
  }
31716
- if (key.delete && value === "" && queued.length > 0) {
32796
+ if (key.delete && liveValue === "" && queued.length > 0) {
31717
32797
  cancelQueued(queued[queued.length - 1].messageId);
31718
32798
  return;
31719
32799
  }
31720
- const shiftEnterSequence = input === "[13;2u" || input === "[27;2;13~";
31721
- if (shiftEnterSequence || key.return) {
32800
+ if (key.return) {
31722
32801
  if (pasteBracketRef.current) {
31723
- applyEdit(insertText(value, cursor, "\n"));
31724
- return;
31725
- }
31726
- if (shiftEnterSequence || key.shift || key.meta || key.ctrl && input === "j") {
31727
- setValue(value.slice(0, cursor) + "\n" + value.slice(cursor));
31728
- setCursor(cursor + 1);
31729
- setDismissedMenuValue(void 0);
32802
+ applyEdit(insertText(liveValue, liveCursor, "\n"));
31730
32803
  return;
31731
32804
  }
31732
32805
  if (menuActive) {
31733
- if (!(!mentionActive && candidates.some((candidate) => candidate.label === value))) {
32806
+ if (!(!mentionActive && candidates.some((candidate) => candidate.label === liveValue))) {
31734
32807
  acceptMenuCandidate();
31735
32808
  return;
31736
32809
  }
31737
32810
  }
31738
- const text = value.trim();
32811
+ const text = liveValue.trim();
32812
+ if (draftImagesRef.current.length > 0) {
32813
+ const controller = new AbortController();
32814
+ const epoch = prepareEpochRef.current + 1;
32815
+ prepareEpochRef.current = epoch;
32816
+ prepareAbortRef.current = controller;
32817
+ setPreparingImages(true);
32818
+ notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? "" : "s"}…`);
32819
+ const snapshot = draftImagesRef.current;
32820
+ prepareImages(snapshot.map((image) => image.path), controller.signal).then((images) => {
32821
+ if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
32822
+ prepareAbortRef.current = void 0;
32823
+ setPreparingImages(false);
32824
+ valueRef.current = "";
32825
+ cursorRef.current = 0;
32826
+ setValue("");
32827
+ setCursor(0);
32828
+ draftImagesRef.current = [];
32829
+ setDraftImages([]);
32830
+ setCompletionIndex(0);
32831
+ setDismissedMenuValue(void 0);
32832
+ dismissNotice();
32833
+ if (text !== "") {
32834
+ recordLocal(text);
32835
+ recordHistory(text);
32836
+ }
32837
+ recall.current = beginRecall(recallSpace, "");
32838
+ if (busy) steer(text, images);
32839
+ else dispatch(text, images);
32840
+ }, (reason) => {
32841
+ if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
32842
+ prepareAbortRef.current = void 0;
32843
+ setPreparingImages(false);
32844
+ notify(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
32845
+ });
32846
+ return;
32847
+ }
32848
+ valueRef.current = "";
32849
+ cursorRef.current = 0;
31739
32850
  setValue("");
31740
32851
  setCursor(0);
32852
+ resetCursorBlink();
31741
32853
  setCompletionIndex(0);
31742
32854
  setDismissedMenuValue(void 0);
31743
32855
  if (text === "") return;
@@ -31863,6 +32975,13 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31863
32975
  dispatch(text);
31864
32976
  return;
31865
32977
  }
32978
+ if (input === "\n" || input === "\r") return;
32979
+ if (menuActive && (key.tab || input.startsWith(" "))) {
32980
+ const remainder = key.tab ? "" : input.slice(1);
32981
+ acceptMenuCandidate();
32982
+ if (remainder !== "") applyEdit(insertText(valueRef.current, cursorRef.current, remainder));
32983
+ return;
32984
+ }
31866
32985
  if (menuActive && key.upArrow) {
31867
32986
  setCompletionIndex((index) => (index + menuRows.length - 1) % menuRows.length);
31868
32987
  return;
@@ -31871,119 +32990,74 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31871
32990
  setCompletionIndex((index) => (index + 1) % menuRows.length);
31872
32991
  return;
31873
32992
  }
31874
- const rawKey = rawAnnotation.current;
31875
- if (rawKey !== void 0) {
31876
- if (rawKey === "home") moveCursorTo(lineBounds(value, cursor).start);
31877
- else if (rawKey === "end") moveCursorTo(lineBounds(value, cursor).end);
31878
- else if (rawKey === "delete-backward") applyEdit(deleteBackward(value, cursor));
31879
- else if (rawKey === "delete-word-backward") applyEdit(deleteWordBackward(value, cursor));
31880
- else if (rawKey === "delete-forward") applyEdit(deleteForward(value, cursor));
31881
- else applyEdit(deleteWordForward(value, cursor));
32993
+ const rawTokens = rawEditorTokens.current;
32994
+ rawEditorTokens.current = void 0;
32995
+ if (rawTokens !== void 0) {
32996
+ applyRawEditorTokens(rawTokens);
31882
32997
  return;
31883
32998
  }
31884
32999
  if (key.upArrow || key.downArrow) {
31885
- if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
31886
- const step = key.upArrow ? recallOlder(recall.current, value) : recallNewer(recall.current);
31887
- recall.current = step.state;
31888
- if (step.entry !== void 0) {
31889
- const safe = sanitizeDraftText(step.entry);
31890
- setValue(safe);
31891
- setCursor(safe.length);
31892
- preferredColumnRef.current = null;
31893
- setDismissedMenuValue(void 0);
31894
- }
31895
- return;
31896
- }
31897
- const model = editorModel(value, Math.max(1, columns - 6));
31898
- const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
31899
- const next = moveCursorVertically(model, cursor, preferred, key.upArrow ? -1 : 1);
31900
- if (next !== cursor) {
31901
- setCursor(next);
31902
- preferredColumnRef.current = preferred;
31903
- }
33000
+ navigateVertical(key.upArrow ? -1 : 1);
31904
33001
  return;
31905
33002
  }
31906
33003
  if (key.ctrl && (input === "p" || input === "n")) {
31907
- const up = input === "p";
31908
- if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
31909
- const step = up ? recallOlder(recall.current, value) : recallNewer(recall.current);
31910
- recall.current = step.state;
31911
- if (step.entry !== void 0) {
31912
- const safe = sanitizeDraftText(step.entry);
31913
- setValue(safe);
31914
- setCursor(safe.length);
31915
- preferredColumnRef.current = null;
31916
- setDismissedMenuValue(void 0);
31917
- }
31918
- return;
31919
- }
31920
- const model = editorModel(value, Math.max(1, columns - 6));
31921
- const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
31922
- const next = moveCursorVertically(model, cursor, preferred, up ? -1 : 1);
31923
- if (next !== cursor) {
31924
- setCursor(next);
31925
- preferredColumnRef.current = preferred;
31926
- }
31927
- return;
31928
- }
31929
- if (key.tab && menuActive) {
31930
- acceptMenuCandidate();
33004
+ navigateVertical(input === "p" ? -1 : 1);
31931
33005
  return;
31932
33006
  }
31933
33007
  if (key.leftArrow) {
31934
- moveCursorTo(key.meta || key.ctrl ? moveWordLeft(value, cursor) : moveCursorBy(value, cursor, -1));
33008
+ moveCursorTo(key.meta || key.ctrl ? moveWordLeft(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, -1));
31935
33009
  return;
31936
33010
  }
31937
33011
  if (key.rightArrow) {
31938
- moveCursorTo(key.meta || key.ctrl ? moveWordRight(value, cursor) : moveCursorBy(value, cursor, 1));
33012
+ moveCursorTo(key.meta || key.ctrl ? moveWordRight(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, 1));
31939
33013
  return;
31940
33014
  }
31941
33015
  if (key.meta && input === "b") {
31942
- moveCursorTo(moveWordLeft(value, cursor));
33016
+ moveCursorTo(moveWordLeft(liveValue, liveCursor));
31943
33017
  return;
31944
33018
  }
31945
33019
  if (key.meta && input === "f") {
31946
- moveCursorTo(moveWordRight(value, cursor));
33020
+ moveCursorTo(moveWordRight(liveValue, liveCursor));
31947
33021
  return;
31948
33022
  }
31949
33023
  if (key.ctrl && input === "b") {
31950
- moveCursorTo(moveCursorBy(value, cursor, -1));
33024
+ moveCursorTo(moveCursorBy(liveValue, liveCursor, -1));
31951
33025
  return;
31952
33026
  }
31953
33027
  if (key.ctrl && input === "f") {
31954
- moveCursorTo(moveCursorBy(value, cursor, 1));
33028
+ moveCursorTo(moveCursorBy(liveValue, liveCursor, 1));
31955
33029
  return;
31956
33030
  }
31957
33031
  if (key.ctrl && input === "w") {
31958
- applyEdit(deleteWordBackward(value, cursor));
33032
+ applyEdit(deleteWordBackward(liveValue, liveCursor));
31959
33033
  return;
31960
33034
  }
31961
33035
  if (key.meta && input === "d") {
31962
- applyEdit(deleteWordForward(value, cursor));
33036
+ applyEdit(deleteWordForward(liveValue, liveCursor));
31963
33037
  return;
31964
33038
  }
31965
33039
  if (key.backspace || key.delete) {
31966
- applyEdit(deleteBackward(value, cursor));
33040
+ applyEdit(deleteBackward(liveValue, liveCursor));
31967
33041
  return;
31968
33042
  }
31969
33043
  if (key.ctrl && input === "a") {
31970
- moveCursorTo(lineBounds(value, cursor).start);
33044
+ moveCursorTo(moveToLineStart(liveValue, liveCursor, true));
31971
33045
  return;
31972
33046
  }
31973
33047
  if (key.ctrl && input === "e") {
31974
- moveCursorTo(lineBounds(value, cursor).end);
33048
+ moveCursorTo(moveToLineEnd(liveValue, liveCursor, true));
31975
33049
  return;
31976
33050
  }
31977
33051
  if (key.ctrl && input === "u") {
31978
- applyEdit(killToLineStart(value, cursor));
33052
+ applyEdit(killToLineStart(liveValue, liveCursor));
31979
33053
  return;
31980
33054
  }
31981
33055
  if (key.ctrl && input === "k") {
31982
- applyEdit(killToLineEnd(value, cursor));
33056
+ applyEdit(killToLineEnd(liveValue, liveCursor));
31983
33057
  return;
31984
33058
  }
31985
33059
  if (key.ctrl && input === "y") {
31986
- if (killRef.current !== "") applyEdit(insertText(value, cursor, killRef.current));
33060
+ if (killRef.current !== "") applyEdit(insertText(liveValue, liveCursor, killRef.current));
31987
33061
  return;
31988
33062
  }
31989
33063
  if (key.ctrl && input === "l") {
@@ -32011,9 +33085,14 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32011
33085
  text = text.replaceAll(PASTE_END_MARKER, "");
32012
33086
  }
32013
33087
  if (text === "") return;
32014
- applyEdit(insertText(value, cursor, text));
33088
+ const droppedPaths = text.length > 1 ? parsePastedImagePaths(text) : [];
33089
+ if (droppedPaths.length > 0) {
33090
+ insertDroppedImages(droppedPaths);
33091
+ return;
33092
+ }
33093
+ applyEdit(insertText(valueRef.current, cursorRef.current, text));
32015
33094
  }
32016
- });
33095
+ }, active);
32017
33096
  const [waveTick, setWaveTick] = (0, import_react.useState)(null);
32018
33097
  const wavePrevious = (0, import_react.useRef)({
32019
33098
  tier: null,
@@ -32031,7 +33110,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32031
33110
  }
32032
33111
  if (previous.tier !== waveTier || previous.style !== waveStyle) setWaveTick(0);
32033
33112
  }, [waveTier, waveStyle]);
32034
- const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
33113
+ const waveActive = !preparingImages && waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
32035
33114
  (0, import_react.useEffect)(() => {
32036
33115
  if (!waveActive) return;
32037
33116
  const id = setInterval(() => {
@@ -32052,7 +33131,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32052
33131
  const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
32053
33132
  const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
32054
33133
  const promptGlyph = waveTier === "flash" ? "›" : waveTier === "deepseek" ? "»" : "❯";
32055
- const editorViewModel = editorModel(value, Math.max(1, columns - 6));
33134
+ const editorViewModel = editorModel(value, editorColumns);
32056
33135
  const clampedCursor = clampCursor(value, cursor);
32057
33136
  const caret = caretSite(editorViewModel, clampedCursor);
32058
33137
  const editorWindowRows = Math.min(editorViewModel.rows.length, Math.max(1, maxRows));
@@ -32103,123 +33182,120 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32103
33182
  const editorRows = [];
32104
33183
  for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
32105
33184
  const row = editorViewModel.rows[index];
32106
- const caretAt = index === caret.row ? row.offsets.indexOf(clampedCursor) : -1;
32107
- const before = caretAt > 0 ? row.text.slice(0, row.cuts[caretAt]) : "";
32108
- const caretChar = caretAt >= 0 && caretAt < row.cuts.length - 1 ? row.text.slice(row.cuts[caretAt], row.cuts[caretAt + 1]) : " ";
32109
- const after = caretAt < 0 ? row.text : caretAt < row.cuts.length - 1 ? row.text.slice(row.cuts[caretAt + 1]) : "";
32110
- const placeholder = index === 0 && value === "" && !busy;
32111
- const tail = placeholder ? COMPOSER_PLACEHOLDER : after;
32112
- const consumed = 2 + visibleColumns(before) + visibleColumns(caretChar) + visibleColumns(tail);
33185
+ const parts = editorRowParts(row, index, caret.row, clampedCursor, !preparingImages);
33186
+ const placeholder = index === 0 && value === "" && !busy && !preparingImages;
33187
+ const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
33188
+ const consumed = 2 + visibleColumns(parts.before) + visibleColumns(parts.caret) + visibleColumns(tail);
32113
33189
  editorRows.push((0, import_react.createElement)(Text, {
32114
33190
  key: index,
32115
33191
  backgroundColor: bandBg,
32116
33192
  wrap: "truncate-end"
32117
- }, index === 0 ? busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
33193
+ }, index === 0 ? preparingImages ? (0, import_react.createElement)(Text, {
33194
+ color: inkColor(getPalette().warn),
33195
+ bold: true
33196
+ }, "… ") : busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
32118
33197
  color: promptColor,
32119
33198
  bold: tierActive ? true : void 0
32120
- }, `${promptGlyph} `) : " ", before, (0, import_react.createElement)(CursorBlock, {
33199
+ }, `${promptGlyph} `) : " ", parts.before, parts.hasCaret ? (0, import_react.createElement)(Text, {
32121
33200
  key: "caret",
32122
- char: caretChar
32123
- }), placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : after, bandFill(consumed)));
33201
+ inverse: cursorVisible || void 0
33202
+ }, parts.caret) : null, placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : parts.after, bandFill(consumed)));
32124
33203
  }
32125
33204
  const staticEditor = (0, import_react.createElement)(Box, { flexDirection: "column" }, ...editorRows);
32126
33205
  const waveRow = () => {
32127
33206
  const hues = deepseekWaveHues(waveTier);
32128
33207
  const style = waveStyle;
32129
33208
  const bandRgb = getPalette().composerBand;
33209
+ const visibleRows = editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows);
33210
+ const totalBandRows = visibleRows.length + 2;
32130
33211
  const waveBg = (row, column) => {
32131
- const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row, 3);
33212
+ const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row, totalBandRows);
32132
33213
  return rgb === null ? bandBg : inkColor(rgb);
32133
33214
  };
32134
33215
  const blankBandRow = (row) => {
32135
33216
  const blanks = [];
32136
- while (blanks.length < bandWidth) blanks.push({
33217
+ for (let column = 0; column < bandWidth; column += 1) blanks.push({
32137
33218
  char: " ",
32138
- backgroundColor: waveBg(row, blanks.length)
33219
+ width: 1,
33220
+ backgroundColor: waveBg(row, column)
32139
33221
  });
32140
- return (0, import_react.createElement)(Text, { key: row }, ...waveRowSpans(blanks));
33222
+ return (0, import_react.createElement)(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks));
32141
33223
  };
32142
- const waveEditor = editorWindow(value, cursor, Math.max(1, columns - 7));
32143
- const cells = [
32144
- {
32145
- char: " ",
32146
- backgroundColor: waveBg(1, 0)
32147
- },
32148
- {
32149
- char: " ",
32150
- backgroundColor: waveBg(1, 1)
32151
- },
32152
- {
32153
- char: promptGlyph,
32154
- color: promptColor,
32155
- bold: true,
32156
- backgroundColor: waveBg(1, 2)
32157
- },
32158
- {
32159
- char: " ",
32160
- color: promptColor,
32161
- backgroundColor: waveBg(1, 3)
33224
+ const cellIndexAtColumn = (cells, target) => {
33225
+ let column = 0;
33226
+ for (let index = 0; index < cells.length; index += 1) {
33227
+ if (column === target) return index;
33228
+ column += cells[index].width ?? visibleColumns(cells[index].char);
33229
+ if (column > target) return void 0;
32162
33230
  }
32163
- ];
32164
- for (const char of waveEditor.before) cells.push({
32165
- char,
32166
- backgroundColor: waveBg(1, cells.length)
32167
- });
32168
- cells.push({
32169
- char: waveEditor.caret,
32170
- inverse: true,
32171
- backgroundColor: waveBg(1, cells.length)
32172
- });
32173
- if (value === "" && !busy) for (let at = 0; at < 40; at += 1) cells.push({
32174
- char: COMPOSER_PLACEHOLDER[at],
32175
- dim: true,
32176
- backgroundColor: waveBg(1, cells.length)
32177
- });
32178
- else for (const char of waveEditor.after) cells.push({
32179
- char,
32180
- backgroundColor: waveBg(1, cells.length)
32181
- });
32182
- while (cells.length < bandWidth) cells.push({
32183
- char: " ",
32184
- backgroundColor: waveBg(1, cells.length)
33231
+ };
33232
+ const editorWaveRows = visibleRows.map((row, visibleIndex) => {
33233
+ const sourceIndex = editorWindowStart + visibleIndex;
33234
+ const bandRow = visibleIndex + 1;
33235
+ const parts = editorRowParts(row, sourceIndex, caret.row, clampedCursor);
33236
+ const placeholder = sourceIndex === 0 && value === "" && !busy;
33237
+ const cells = [];
33238
+ let usedColumns = 0;
33239
+ const push = (char, extra = {}) => {
33240
+ const width = visibleColumns(char);
33241
+ cells.push({
33242
+ char,
33243
+ width,
33244
+ backgroundColor: waveBg(bandRow, usedColumns),
33245
+ ...extra
33246
+ });
33247
+ usedColumns += width;
33248
+ };
33249
+ if (sourceIndex === 0) {
33250
+ push(promptGlyph, {
33251
+ color: promptColor,
33252
+ bold: true
33253
+ });
33254
+ push(" ", { color: promptColor });
33255
+ } else {
33256
+ push(" ");
33257
+ push(" ");
33258
+ }
33259
+ for (const span of splitGraphemes(parts.before)) push(span.text);
33260
+ if (parts.hasCaret) push(parts.caret, { inverse: cursorVisible });
33261
+ const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
33262
+ for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {});
33263
+ while (usedColumns < bandWidth) push(" ");
33264
+ const middleBandRow = Math.floor(totalBandRows / 2);
33265
+ if (bandRow === middleBandRow && deepseekWaveWordVisible(waveTick, waveTier, style)) {
33266
+ const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
33267
+ const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
33268
+ const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at));
33269
+ if (indices.every((index) => index !== void 0 && (cells[index].char === " " || cells[index].dim === true))) for (let at = 0; at < word.length; at += 1) {
33270
+ const cell = cells[indices[at]];
33271
+ cell.char = word[at];
33272
+ cell.width = 1;
33273
+ cell.color = inkColor(deepseekWaveWordHue(at, hues));
33274
+ cell.bold = true;
33275
+ cell.dim = false;
33276
+ }
33277
+ }
33278
+ if (bandRow === middleBandRow && (waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
33279
+ const spark = deepseekWaveSpark(waveTick);
33280
+ const lastIndex = cellIndexAtColumn(cells, bandWidth - 1);
33281
+ if (spark !== null && lastIndex !== void 0 && cells[lastIndex].char === " ") {
33282
+ cells[lastIndex].char = spark;
33283
+ cells[lastIndex].color = promptColor;
33284
+ cells[lastIndex].bold = true;
33285
+ cells[lastIndex].dim = false;
33286
+ }
33287
+ }
33288
+ return (0, import_react.createElement)(Text, {
33289
+ key: `editor-${sourceIndex}`,
33290
+ wrap: "truncate-end"
33291
+ }, ...waveRowSpans(cells));
32185
33292
  });
32186
- if (deepseekWaveWordVisible(waveTick, waveTier, style)) {
32187
- const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
32188
- const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
32189
- let clear = true;
32190
- for (let at = 0; at < word.length; at += 1) {
32191
- const cell = cells[start + at];
32192
- if (cell === void 0 || cell.char !== " " && cell.dim !== true) {
32193
- clear = false;
32194
- break;
32195
- }
32196
- }
32197
- if (clear) for (let at = 0; at < word.length; at += 1) {
32198
- const cell = cells[start + at];
32199
- cell.char = word[at];
32200
- cell.color = inkColor(deepseekWaveWordHue(at, hues));
32201
- cell.bold = true;
32202
- cell.dim = false;
32203
- }
32204
- }
32205
- if ((waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
32206
- const spark = deepseekWaveSpark(waveTick);
32207
- if (spark !== null) {
32208
- const last = cells[cells.length - 1];
32209
- if (last !== void 0 && last.char === " ") {
32210
- last.char = spark;
32211
- last.color = promptColor;
32212
- last.bold = true;
32213
- last.dim = false;
32214
- }
32215
- }
32216
- }
32217
33293
  return (0, import_react.createElement)(Box, {
32218
33294
  flexDirection: "column",
32219
33295
  width: bandWidth
32220
- }, blankBandRow(0), (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...waveRowSpans(cells)), blankBandRow(2));
33296
+ }, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
32221
33297
  };
32222
- return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && !busy ? waveRow() : band(staticEditor));
33298
+ return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && waveStyle !== null && !busy && !preparingImages ? waveRow() : band(staticEditor));
32223
33299
  }
32224
33300
  /** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
32225
33301
  function buildSettledRow(entry, index, showReasoning, columns) {
@@ -32429,6 +33505,8 @@ function App(props) {
32429
33505
  const [modelError, setModelError] = (0, import_react.useState)(void 0);
32430
33506
  const [providerDirectory, setProviderDirectory] = (0, import_react.useState)(void 0);
32431
33507
  const [providerError, setProviderError] = (0, import_react.useState)(void 0);
33508
+ const [authorizationDirectory, setAuthorizationDirectory] = (0, import_react.useState)(void 0);
33509
+ const [authorizationError, setAuthorizationError] = (0, import_react.useState)(void 0);
32432
33510
  const [modelLoadEpoch, setModelLoadEpoch] = (0, import_react.useState)(0);
32433
33511
  const [notice, setNotice] = (0, import_react.useState)(void 0);
32434
33512
  const notify = (0, import_react.useCallback)((text, tone = "info") => {
@@ -32476,6 +33554,24 @@ function App(props) {
32476
33554
  modelLoadEpoch,
32477
33555
  props.loadModelProviders
32478
33556
  ]);
33557
+ (0, import_react.useEffect)(() => {
33558
+ if (!modelOpen || props.loadProviderAuthorizations === void 0) return;
33559
+ let cancelled = false;
33560
+ setAuthorizationDirectory(void 0);
33561
+ setAuthorizationError(void 0);
33562
+ Promise.resolve().then(() => props.loadProviderAuthorizations()).then((loaded) => {
33563
+ if (!cancelled) setAuthorizationDirectory(loaded);
33564
+ }, (error) => {
33565
+ if (!cancelled) setAuthorizationError(error instanceof Error ? error.message : String(error));
33566
+ });
33567
+ return () => {
33568
+ cancelled = true;
33569
+ };
33570
+ }, [
33571
+ modelOpen,
33572
+ modelLoadEpoch,
33573
+ props.loadProviderAuthorizations
33574
+ ]);
32479
33575
  (0, import_react.useEffect)(() => {
32480
33576
  const subscribe = props.subscribeModelProviders;
32481
33577
  if (!modelOpen || subscribe === void 0) return;
@@ -32485,6 +33581,15 @@ function App(props) {
32485
33581
  setProviderError(error instanceof Error ? error.message : String(error));
32486
33582
  }
32487
33583
  }, [modelOpen, props.subscribeModelProviders]);
33584
+ (0, import_react.useEffect)(() => {
33585
+ const subscribe = props.subscribeProviderAuthorizations;
33586
+ if (!modelOpen || subscribe === void 0) return;
33587
+ try {
33588
+ return subscribe(() => setModelLoadEpoch((epoch) => epoch + 1));
33589
+ } catch (error) {
33590
+ setAuthorizationError(error instanceof Error ? error.message : String(error));
33591
+ }
33592
+ }, [modelOpen, props.subscribeProviderAuthorizations]);
32488
33593
  const busy = view.busy;
32489
33594
  const [showReasoning, setShowReasoning] = (0, import_react.useState)(false);
32490
33595
  const [verboseOpen, setVerboseOpen] = (0, import_react.useState)(false);
@@ -32681,13 +33786,16 @@ function App(props) {
32681
33786
  busy,
32682
33787
  streamingActive
32683
33788
  ]);
33789
+ const sessionHasImages = (0, import_react.useMemo)(() => view.entries.some((entry) => (entry.kind === "user" || entry.kind === "pending") && (entry.images?.length ?? 0) > 0), [view.entries]);
32684
33790
  /** Apply one /model pick: record the selection, close the panel, report via notice. */
32685
33791
  const applyModel = (row, effortId) => {
32686
33792
  try {
32687
33793
  const label = props.selectModel(row, effortId);
32688
33794
  setModelLabel(label);
32689
33795
  setEffortLabel(effortId);
32690
- notify(`model next step uses ${label}${effortId === void 0 || effortId === "" ? "" : `@${effortId}`}`);
33796
+ const selected = `${label}${effortId === void 0 || effortId === "" ? "" : `@${effortId}`}`;
33797
+ if (sessionHasImages && row.inputModalities !== void 0 && !row.inputModalities.includes("image")) notify(`model → ${selected} · image history will be sent as text placeholders`, "warning");
33798
+ else notify(`model → next step uses ${selected}`);
32691
33799
  setModelOpen(false);
32692
33800
  setProviderOpen(false);
32693
33801
  setProviderAction(void 0);
@@ -32707,7 +33815,37 @@ function App(props) {
32707
33815
  };
32708
33816
  let modelSurface;
32709
33817
  if (modelOpen && !approvalPending && !questionPending) {
32710
- if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfigurationPanel, {
33818
+ if (providerAction?.kind === "login" && props.beginProviderAuthorization !== void 0 && props.cancelProviderAuthorization !== void 0 && props.openAuthorizationUrl !== void 0 && props.copyTextValue !== void 0) modelSurface = (0, import_react.createElement)(ProviderAuthorizationPanel, {
33819
+ row: providerAction.authorization,
33820
+ begin: props.beginProviderAuthorization,
33821
+ cancel: () => props.cancelProviderAuthorization(providerAction.authorization),
33822
+ openUrl: props.openAuthorizationUrl,
33823
+ copy: props.copyTextValue,
33824
+ done: () => {
33825
+ const authorization = providerAction.authorization;
33826
+ setProviderAction(void 0);
33827
+ setProviderOpen(false);
33828
+ reloadModelSurfaces();
33829
+ notify(`logged in to ${authorization.label}; select a model`);
33830
+ },
33831
+ back: () => {
33832
+ setProviderAction(void 0);
33833
+ setProviderOpen(true);
33834
+ }
33835
+ });
33836
+ else if (providerAction?.kind === "logout" && props.logoutProviderAuthorization !== void 0) modelSurface = (0, import_react.createElement)(ProviderAuthorizationLogoutPanel, {
33837
+ row: providerAction.authorization,
33838
+ confirm: props.logoutProviderAuthorization,
33839
+ done: () => {
33840
+ const authorization = providerAction.authorization;
33841
+ setProviderAction(void 0);
33842
+ setProviderOpen(true);
33843
+ reloadModelSurfaces();
33844
+ notify(`logged out from ${authorization.label}`);
33845
+ },
33846
+ back: () => setProviderAction(void 0)
33847
+ });
33848
+ else if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfigurationPanel, {
32711
33849
  target: providerAction.target,
32712
33850
  catalog: directory?.rows ?? [],
32713
33851
  save: props.saveModelProviderConfiguration,
@@ -32761,6 +33899,8 @@ function App(props) {
32761
33899
  else if (providerOpen) modelSurface = (0, import_react.createElement)(ProviderPanel, {
32762
33900
  directory: providerDirectory,
32763
33901
  error: providerError,
33902
+ authorizations: authorizationDirectory,
33903
+ authorizationError,
32764
33904
  onCredential: (target) => {
32765
33905
  if (props.saveModelProviderCredential === void 0) {
32766
33906
  notify("API key storage is unavailable in this profile", "warning");
@@ -32801,6 +33941,32 @@ function App(props) {
32801
33941
  target
32802
33942
  });
32803
33943
  },
33944
+ onLogin: (target, authorization) => {
33945
+ if (busy) {
33946
+ notify("provider login is available only while the agent is idle", "warning");
33947
+ return;
33948
+ }
33949
+ if (props.beginProviderAuthorization === void 0 || props.cancelProviderAuthorization === void 0 || props.openAuthorizationUrl === void 0 || props.copyTextValue === void 0) {
33950
+ notify("provider login is unavailable in this profile", "warning");
33951
+ return;
33952
+ }
33953
+ setProviderAction({
33954
+ kind: "login",
33955
+ target,
33956
+ authorization
33957
+ });
33958
+ },
33959
+ onLogout: (target, authorization) => {
33960
+ if (props.logoutProviderAuthorization === void 0) {
33961
+ notify("provider logout is unavailable in this profile", "warning");
33962
+ return;
33963
+ }
33964
+ setProviderAction({
33965
+ kind: "logout",
33966
+ target,
33967
+ authorization
33968
+ });
33969
+ },
32804
33970
  onRetry: reloadModelSurfaces,
32805
33971
  onBack: () => setProviderOpen(false)
32806
33972
  });
@@ -32983,6 +34149,8 @@ function App(props) {
32983
34149
  setModelError(void 0);
32984
34150
  setProviderDirectory(void 0);
32985
34151
  setProviderError(void 0);
34152
+ setAuthorizationDirectory(void 0);
34153
+ setAuthorizationError(void 0);
32986
34154
  setProviderOpen(false);
32987
34155
  setProviderAction(void 0);
32988
34156
  setEffortFor(void 0);
@@ -33070,6 +34238,8 @@ function App(props) {
33070
34238
  if (!busy && !streamingActive) refreshScreen();
33071
34239
  },
33072
34240
  loadMentions: props.loadMentions,
34241
+ inspectImages: props.inspectImages,
34242
+ prepareImages: props.prepareImages,
33073
34243
  cyclePermission: props.cyclePermission,
33074
34244
  exportTranscript: props.exportTranscript,
33075
34245
  renameTitle: props.renameTitle,
@@ -33271,7 +34441,9 @@ function isSlashLine(line) {
33271
34441
  /** Substitutable runner effects; production values write to the real terminal. */
33272
34442
  const internals = {
33273
34443
  mount: (element) => {
33274
- process.stdout.write("\x1B[>4;0m\x1B[>5u\x1B[?2004h");
34444
+ const keyboardEnhanced = shouldEnableKeyboardEnhancement();
34445
+ const focusReporting = isVsCodeTerminalEnv();
34446
+ process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
33275
34447
  const instance = render(element, { exitOnCtrlC: false });
33276
34448
  return {
33277
34449
  rerender(element) {
@@ -33279,7 +34451,7 @@ const internals = {
33279
34451
  },
33280
34452
  unmount() {
33281
34453
  instance.unmount();
33282
- process.stdout.write("\x1B[<u\x1B[>4;0m\x1B[?2004l");
34454
+ process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
33283
34455
  }
33284
34456
  };
33285
34457
  },
@@ -33394,14 +34566,16 @@ async function loadModelDirectory(ctx) {
33394
34566
  provider: provider.id,
33395
34567
  providerName: provider.name,
33396
34568
  model: model.id,
33397
- modelName: model.name
34569
+ modelName: model.name,
34570
+ ...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
33398
34571
  };
33399
34572
  if (llmResolve.resolveModelInfo === void 0) return row;
33400
34573
  try {
33401
34574
  const resolved = await llmResolve.resolveModelInfo(provider.id, model.id);
33402
- return resolved.reasoning === void 0 ? row : {
34575
+ return {
33403
34576
  ...row,
33404
- reasoning: mapReasoning(resolved.reasoning)
34577
+ ...resolved.inputModalities === void 0 ? {} : { inputModalities: [...resolved.inputModalities] },
34578
+ ...resolved.reasoning === void 0 ? {} : { reasoning: mapReasoning(resolved.reasoning) }
33405
34579
  };
33406
34580
  } catch {
33407
34581
  reasoningFailures.push(`${provider.id}/${model.id}`);
@@ -33503,7 +34677,7 @@ function deriveCredentialRef(provider) {
33503
34677
  }
33504
34678
  /** Events that invalidate the official Models provider/settings/credential join. */
33505
34679
  const PROVIDER_SETTINGS_EVENTS = [
33506
- "credentials/updated",
34680
+ "credentials/reference-updated",
33507
34681
  "settings/document-updated",
33508
34682
  "llm/adapters-updated"
33509
34683
  ];
@@ -33781,82 +34955,6 @@ async function removeProviderSettings(ctx, target) {
33781
34955
  }
33782
34956
  }
33783
34957
  //#endregion
33784
- //#region src/mentions.ts
33785
- /** Menu cap on file rows; the service owns ranking and default rows. */
33786
- const MAX_FILE_ROWS = 20;
33787
- /**
33788
- * Create the mention API for one agent's workspace. A missing
33789
- * `fileReferences` service (with an agent present) or `sessionReferenceResolver`
33790
- * degrades that half to empty rows; `prepare` passes text through untouched
33791
- * without references. An undefined agent (a bare launch before any session
33792
- * exists) runs the official WorkspaceFileSearch over the launch cwd — the
33793
- * same class the mounted service uses per agent — so `@` file completion
33794
- * works from the first keystroke; session references wait for the session.
33795
- *
33796
- * `candidates` never reaches for `this` — the runner hands it to the input
33797
- * editor as a detached callback, and a `this`-bound method would throw on
33798
- * every `@` key.
33799
- * @param ctx - context carrying the optional `fileReferences` and
33800
- * `sessionReferenceResolver` services.
33801
- * @param agent - the session owner; excluded from its own session candidates.
33802
- * @param cwd - launch working directory; bounds the pre-session search.
33803
- */
33804
- function createMentions(ctx, agent, cwd) {
33805
- const resolver = ctx.get("sessionReferenceResolver");
33806
- const fileReferences = ctx.get("fileReferences");
33807
- const sessionCapable = agent !== void 0 && resolver !== void 0;
33808
- let preSessionSearch;
33809
- const preSessionFiles = (query, signal) => {
33810
- preSessionSearch ??= new WorkspaceFileSearch(cwd, {
33811
- maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
33812
- maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
33813
- excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
33814
- });
33815
- return preSessionSearch.list(query, signal ?? new AbortController().signal);
33816
- };
33817
- return {
33818
- async candidates(query, signal) {
33819
- const needle = query.trim();
33820
- const [files, sessions] = await Promise.all([agent !== void 0 && fileReferences !== void 0 ? fileReferences.list(agent, needle, signal ?? new AbortController().signal).catch(() => []) : agent === void 0 ? preSessionFiles(needle, signal).catch(() => []) : Promise.resolve([]), sessionCapable && needle !== "" && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
33821
- const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
33822
- label: candidate.path,
33823
- description: candidate.kind === "directory" ? "Folder" : "File",
33824
- kind: candidate.kind
33825
- }));
33826
- const sessionRows = sessions.map((candidate) => ({
33827
- label: formatSessionReferenceMention(candidate),
33828
- description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
33829
- kind: "session"
33830
- }));
33831
- return [...fileRows, ...sessionRows];
33832
- },
33833
- parse(text) {
33834
- return parseSessionReferenceText(text);
33835
- },
33836
- async prepare(parsed, signal) {
33837
- if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
33838
- text: parsed.text,
33839
- references: parsed.references
33840
- };
33841
- const prepared = await resolver.prepare(agent, [{
33842
- type: "text",
33843
- text: parsed.text
33844
- }], parsed.references, signal);
33845
- return {
33846
- text: prepared.content.filter((block) => block.type === "text").map((block) => block.text).join(""),
33847
- references: parsed.references,
33848
- additionalContext: prepared.additionalContext
33849
- };
33850
- },
33851
- sessionMention(candidate) {
33852
- return formatSessionReferenceMention({
33853
- sessionId: candidate.sessionId,
33854
- label: candidate.label
33855
- });
33856
- }
33857
- };
33858
- }
33859
- //#endregion
33860
34958
  //#region src/questions.ts
33861
34959
  const ABORT_ERROR = new UserQuestionError("ask_user_question was interrupted before the user answered", "ASK_ABORTED");
33862
34960
  /**
@@ -34284,44 +35382,6 @@ function buildExportMarkdown(view, sessionId) {
34284
35382
  return out.join("\n");
34285
35383
  }
34286
35384
  //#endregion
34287
- //#region src/attachments.ts
34288
- /** Terminal image-file adapter over the Harness durable attachment service. */
34289
- /** Detect the supported encoded raster formats from bytes, never from a path suffix. */
34290
- function detectImageMediaType(data) {
34291
- if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10) return "image/png";
34292
- if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
34293
- if (data.length >= 6) {
34294
- const signature = String.fromCharCode(...data.subarray(0, 6));
34295
- if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
34296
- }
34297
- if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
34298
- }
34299
- /** Read, validate, and persist an ordered image path list as model content blocks. */
34300
- async function saveImagePaths(paths, attachments) {
34301
- if (paths.length === 0) return [];
34302
- if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
34303
- const inputs = [];
34304
- for (const path of paths) {
34305
- let data;
34306
- try {
34307
- data = await readFile(path);
34308
- } catch (error) {
34309
- throw new Error(`cannot read image "${path}": ${error instanceof Error ? error.message : String(error)}`);
34310
- }
34311
- const mediaType = detectImageMediaType(data);
34312
- if (mediaType === void 0) throw new Error(`unsupported image file "${path}" (expected PNG, JPEG, WebP, or GIF)`);
34313
- inputs.push({
34314
- data,
34315
- mediaType,
34316
- name: basename(path)
34317
- });
34318
- }
34319
- return (await attachments.saveImages(inputs)).map((attachment) => ({
34320
- type: "image",
34321
- attachment
34322
- }));
34323
- }
34324
- //#endregion
34325
35385
  //#region src/editor.ts
34326
35386
  /** Host editor and clipboard adapters used by the terminal surface. */
34327
35387
  function waitForProcess(command, args, input) {
@@ -34602,7 +35662,6 @@ function applyPendingPermission(service, session, pending) {
34602
35662
  */
34603
35663
  function listPermissionRows(service) {
34604
35664
  return service.names.map((id) => {
34605
- if (service.optionOf === void 0) return { id };
34606
35665
  try {
34607
35666
  return {
34608
35667
  id,
@@ -35216,16 +36275,16 @@ async function run(ctx, startup, io) {
35216
36275
  deliverLine(line, mode, images);
35217
36276
  };
35218
36277
  /** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
35219
- const dispatch = (text) => {
35220
- send(text, "followup");
36278
+ const dispatch = (text, images = []) => {
36279
+ send(text, "followup", images);
35221
36280
  };
35222
36281
  /**
35223
36282
  * Submit steering: a running driver consumes the text at its next step
35224
36283
  * boundary (the inbox delivers between steps); an idle driver just starts
35225
36284
  * a turn, so this doubles as the busy-state submit path.
35226
36285
  */
35227
- const steer = (text) => {
35228
- send(text, "steer");
36286
+ const steer = (text, images = []) => {
36287
+ send(text, "steer", images);
35229
36288
  };
35230
36289
  /** Interrupt the running turn (Esc); true when a turn was actually cancelled. */
35231
36290
  const interrupt = () => {
@@ -35637,7 +36696,16 @@ async function run(ctx, startup, io) {
35637
36696
  saveModelProviderConfiguration: (target, configuration) => saveProviderConfiguration(ctx, target, configuration),
35638
36697
  unsetModelProviderCredential: (target) => unsetProviderCredential(ctx, target),
35639
36698
  removeModelProvider: (target) => removeProviderSettings(ctx, target),
36699
+ loadProviderAuthorizations: () => loadProviderAuthorizations(ctx),
36700
+ subscribeProviderAuthorizations: (listener) => subscribeProviderAuthorizations(ctx, listener),
36701
+ beginProviderAuthorization: (row, method, interaction, signal) => beginProviderAuthorization(ctx, row, method, interaction, signal),
36702
+ cancelProviderAuthorization: (row) => cancelProviderAuthorization(ctx, row.key),
36703
+ logoutProviderAuthorization: (row) => logoutProviderAuthorization(ctx, row),
36704
+ openAuthorizationUrl,
36705
+ copyTextValue: copyText,
35640
36706
  loadMentions: (query, signal) => mentions.candidates(query, signal),
36707
+ inspectImages: (paths) => inspectImagePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
36708
+ prepareImages: (paths, signal) => saveImagePaths(paths, ctx.get("attachments"), signal),
35641
36709
  cyclePermission: cyclePermission$1,
35642
36710
  setPermission: setPermissionAction,
35643
36711
  selectModel,
@@ -35687,7 +36755,13 @@ async function run(ctx, startup, io) {
35687
36755
  mountRef.current?.rerender(appElement());
35688
36756
  };
35689
36757
  mountRef.current = io.mount(appElement());
35690
- if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0) saveImagePaths(startup.images ?? [], ctx.get("attachments")).then((images) => send(startup.prompt ?? "", "followup", images), (error) => bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
36758
+ if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0) {
36759
+ if ((startup.images?.length ?? 0) > 0) bridge.notify(`processing ${startup.images.length} startup image${startup.images.length === 1 ? "" : "s"}…`);
36760
+ saveImagePaths(startup.images ?? [], ctx.get("attachments")).then((images) => {
36761
+ if (images.length > 0) bridge.notify(`${images.length} startup image${images.length === 1 ? "" : "s"} attached`);
36762
+ send(startup.prompt ?? "", "followup", images);
36763
+ }, (error) => bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
36764
+ }
35691
36765
  async function copyLastResponse() {
35692
36766
  const text = latestAssistantText(store.getView());
35693
36767
  if (text === void 0) return "nothing to copy yet";