dsh-code 1.0.1 → 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
@@ -14,12 +14,12 @@ import { PassThrough, Stream } from "node:stream";
14
14
  import process$1, { cwd, env } from "node:process";
15
15
  import { EventEmitter } from "node:events";
16
16
  import { Buffer as Buffer$1 } from "node:buffer";
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
+ import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
17
19
  import { execFile, spawn } from "node:child_process";
18
20
  import { credentialKeyId, credentialKeyScope } from "@deepseek-ai/dsh-credentials";
19
21
  import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
20
22
  import { fileURLToPath } from "node:url";
21
- import { DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES, DEFAULT_FILE_SEARCH_MAX_ENTRIES, DEFAULT_FILE_SEARCH_MAX_RESULTS, WorkspaceFileSearch } from "@deepseek-ai/dsh-file-reference-local";
22
- import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
23
23
  import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
24
24
  import { isUserInvocable } from "@deepseek-ai/dsh-skill";
25
25
  //#region node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
@@ -26633,11 +26633,7 @@ function renderMarkdown(text, width, options = {}) {
26633
26633
  }
26634
26634
  //#endregion
26635
26635
  //#region src/render/animations.ts
26636
- /**
26637
- * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
26638
- * ring trail clockwise around the eight outer positions, one braille glyph
26639
- * per step — 8 frames × 125ms = the web's 1s cycle.
26640
- */
26636
+ /** Original terminal StateDot chase frames. */
26641
26637
  const BUSY_CHASE_FRAMES = [
26642
26638
  "⣾",
26643
26639
  "⣽",
@@ -26648,10 +26644,40 @@ const BUSY_CHASE_FRAMES = [
26648
26644
  "⣯",
26649
26645
  "⣷"
26650
26646
  ];
26651
- /** Chase frame for a monotonic tick (the busy composer/Deep-diving marker). */
26647
+ /** Chase frame for a monotonic tick. */
26652
26648
  function busyChaseFrame(tick) {
26653
26649
  return BUSY_CHASE_FRAMES[tick % BUSY_CHASE_FRAMES.length] ?? BUSY_CHASE_FRAMES[0];
26654
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
+ }
26655
26681
  /** Caret visibility: half the ticks on, half off (530ms blink). */
26656
26682
  function caretVisible(tick) {
26657
26683
  return tick % 2 === 0;
@@ -27041,6 +27067,87 @@ function effortAboveHigh(effort) {
27041
27067
  return rank !== void 0 && rank > 3;
27042
27068
  }
27043
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
27044
27151
  //#region src/session-directory.ts
27045
27152
  /** Lightweight session-directory projection for the /resume picker. */
27046
27153
  /** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
@@ -27970,13 +28077,20 @@ function toolDetailLines(detail, columns) {
27970
28077
  default: return detail;
27971
28078
  }
27972
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
+ }
27973
28087
  /**
27974
28088
  * Convert one durable transcript entry to its complete scrollable row model.
27975
28089
  * The source entry stays intact; only the caller's visible slice is rendered.
27976
28090
  * Wrapped continuations keep a hanging indent aligned under each row's
27977
28091
  * content (Codex history-cell alignment) instead of resetting to column 0.
27978
28092
  */
27979
- function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true) {
28093
+ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true, showToolDetails = showReasoning) {
27980
28094
  const width = Math.max(1, Math.floor(columns));
27981
28095
  switch (entry.kind) {
27982
28096
  case "user": return entry.notice ? hangingStyledLines([lineSegment(promptDisplayText(entry), "dim")], width, "⤷ ", "dim", " ", "dim") : hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
@@ -27998,7 +28112,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
27998
28112
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
27999
28113
  const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
28000
28114
  const summaryStyle = entry.state === "error" ? "error" : "dim";
28001
- return [
28115
+ const lines = [
28002
28116
  ...hangingStyledLines([
28003
28117
  lineSegment(`[${entry.ordinal}] `, "dim"),
28004
28118
  lineSegment(entry.name, "brand"),
@@ -28008,6 +28122,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
28008
28122
  ...entry.summary === "" ? [] : hangingTextLines(entry.state === "error" ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary, width, " ⎿ ", summaryStyle, " ", summaryStyle),
28009
28123
  ...entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width)
28010
28124
  ];
28125
+ return showToolDetails ? lines : compactToolLines(lines, width);
28011
28126
  }
28012
28127
  case "command": {
28013
28128
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
@@ -28025,7 +28140,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
28025
28140
  }
28026
28141
  /** Settled-history variant carrying the Ctrl+R reasoning fold. */
28027
28142
  function settledEntryLines(entry, columns, showReasoning) {
28028
- return transcriptEntryLines(entry, columns, showReasoning, false);
28143
+ return transcriptEntryLines(entry, columns, showReasoning, false, showReasoning);
28029
28144
  }
28030
28145
  //#endregion
28031
28146
  //#region src/render/editor.ts
@@ -28039,9 +28154,8 @@ function settledEntryLines(entry, columns, showReasoning) {
28039
28154
  * grapheme boundaries) so the React state stays two primitives
28040
28155
  * (value, cursor) and every operation here stays pure and testable.
28041
28156
  *
28042
- * Word motion deviates from Codex's UAX#29 segmentation in one deliberate
28043
- * way: a run of same-class characters is ONE piece, so a CJK run moves as a
28044
- * 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.
28045
28159
  *
28046
28160
  * @module @deepseek-ai/dsh-code/render/editor
28047
28161
  */
@@ -28194,6 +28308,28 @@ function editorModel(value, columns) {
28194
28308
  length: value.length
28195
28309
  };
28196
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
+ }
28197
28333
  /** Map a cursor offset to its caret site on the wrapped rows. */
28198
28334
  function caretSite(model, offset) {
28199
28335
  const target = Math.max(0, Math.min(model.length, offset));
@@ -28218,7 +28354,9 @@ function caretSite(model, offset) {
28218
28354
  */
28219
28355
  function moveCursorVertically(model, offset, preferredColumn, delta) {
28220
28356
  const target = caretSite(model, offset).row + delta;
28221
- 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;
28222
28360
  const row = model.rows[target];
28223
28361
  const wanted = Math.max(0, Math.min(preferredColumn, row.columns[row.columns.length - 1]));
28224
28362
  let best = 0;
@@ -28238,24 +28376,28 @@ function lineBounds(value, offset) {
28238
28376
  }
28239
28377
  /** Codex WORD_SEPARATORS: punctuation runs are their own word pieces. */
28240
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;
28241
28381
  function classifyGrapheme(text) {
28242
28382
  if (/^\s$/u.test(text)) return "space";
28243
- return WORD_SEPARATORS.has(text) ? "punct" : "word";
28383
+ return WORD_SEPARATORS.has(text) || UNICODE_PUNCTUATION.test(text) ? "punct" : "word";
28244
28384
  }
28245
- /** Maximal same-class runs of graphemes as [start, end) spans. */
28385
+ /** Maximal same-class runs; Han graphemes deliberately stay one run each. */
28246
28386
  function pieceRuns(value) {
28247
28387
  const runs = [];
28248
28388
  let current;
28249
28389
  for (const span of splitGraphemes(value)) {
28250
28390
  const klass = span.text === "\n" ? "space" : classifyGrapheme(span.text);
28251
- 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) {
28252
28393
  current.end = span.end;
28253
28394
  continue;
28254
28395
  }
28255
28396
  current = {
28256
28397
  start: span.start,
28257
28398
  end: span.end,
28258
- class: klass
28399
+ class: klass,
28400
+ atomic
28259
28401
  };
28260
28402
  runs.push(current);
28261
28403
  }
@@ -28266,10 +28408,9 @@ function pieceRuns(value) {
28266
28408
  * START of the trailing non-space piece (extending over separator pieces).
28267
28409
  */
28268
28410
  function moveWordLeft(value, offset) {
28269
- const cursor = Math.max(0, Math.min(value.length, offset));
28411
+ const cursor = clampCursor(value, offset);
28270
28412
  const runs = pieceRuns(value);
28271
- let index = runs.length - 1;
28272
- while (index >= 0 && runs[index].end > cursor) index -= 1;
28413
+ let index = runs.findLastIndex((run) => run.start < cursor);
28273
28414
  if (index < 0) return 0;
28274
28415
  if (runs[index].class === "space") {
28275
28416
  index -= 1;
@@ -28284,10 +28425,9 @@ function moveWordLeft(value, offset) {
28284
28425
  * the leading non-space piece (extending over separator pieces).
28285
28426
  */
28286
28427
  function moveWordRight(value, offset) {
28287
- const cursor = Math.max(0, Math.min(value.length, offset));
28428
+ const cursor = clampCursor(value, offset);
28288
28429
  const runs = pieceRuns(value);
28289
- let index = 0;
28290
- while (index < runs.length && runs[index].start < cursor) index += 1;
28430
+ let index = runs.findIndex((run) => run.end > cursor);
28291
28431
  if (index >= runs.length) return value.length;
28292
28432
  if (runs[index].class === "space") {
28293
28433
  index += 1;
@@ -28376,6 +28516,62 @@ function insertText(value, cursor, text) {
28376
28516
  killed: void 0
28377
28517
  };
28378
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
+ }
28379
28575
  /**
28380
28576
  * Composer editor row budget: the editor itself never grows past this many
28381
28577
  * physical rows; deeper drafts scroll internally to keep the caret visible.
@@ -28385,14 +28581,73 @@ function composerMaxRows(terminalRows) {
28385
28581
  return Math.max(1, Math.min(6, Math.floor((Math.max(1, terminalRows) - 10) / 3)));
28386
28582
  }
28387
28583
  /**
28388
- * Codex `should_handle_navigation`: Up/Down walk history only from an empty
28389
- * draft, or from a boundary of a draft that still exactly matches the last
28390
- * 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.
28391
28587
  */
28392
- function shouldRecallNavigate(value, cursor, lastRecalled) {
28393
- if (value === "") return true;
28394
- if (cursor !== 0 && cursor !== value.length) return false;
28395
- 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
+ });
28396
28651
  }
28397
28652
  /** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
28398
28653
  const PASTE_START_MARKER = "[200~";
@@ -28464,6 +28719,98 @@ function normalizeKeyboardChunk(chunk) {
28464
28719
  }) ?? whole;
28465
28720
  });
28466
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
+ }
28467
28814
  //#endregion
28468
28815
  //#region src/kernel-panels.ts
28469
28816
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
@@ -29923,11 +30270,15 @@ async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
29923
30270
  return inspected;
29924
30271
  }
29925
30272
  /** Read, validate, and persist an ordered image path list as model content blocks. */
29926
- async function saveImagePaths(paths, attachments) {
30273
+ async function saveImagePaths(paths, attachments, signal) {
29927
30274
  if (paths.length === 0) return [];
29928
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
+ };
29929
30279
  const inputs = [];
29930
30280
  for (const path of paths) {
30281
+ checkCancelled();
29931
30282
  let data;
29932
30283
  try {
29933
30284
  data = await readFile(path);
@@ -29942,7 +30293,10 @@ async function saveImagePaths(paths, attachments) {
29942
30293
  name: basename(path)
29943
30294
  });
29944
30295
  }
29945
- return (await attachments.saveImages(inputs)).map((attachment) => ({
30296
+ checkCancelled();
30297
+ const refs = await attachments.saveImages(inputs);
30298
+ checkCancelled();
30299
+ return refs.map((attachment) => ({
29946
30300
  type: "image",
29947
30301
  attachment
29948
30302
  }));
@@ -30098,14 +30452,15 @@ function padColumns(text, width) {
30098
30452
  return clipped + " ".repeat(Math.max(0, width - visibleColumns(clipped)));
30099
30453
  }
30100
30454
  /** Interval-driven frame counter for one self-contained animated leaf. */
30101
- function useFrames(intervalMs) {
30455
+ function useFrames(intervalMs, active = true) {
30102
30456
  const [tick, setTick] = (0, import_react.useState)(0);
30103
30457
  (0, import_react.useEffect)(() => {
30458
+ if (!active) return;
30104
30459
  const id = setInterval(() => setTick((current) => current + 1), intervalMs);
30105
30460
  return () => {
30106
30461
  clearInterval(id);
30107
30462
  };
30108
- }, [intervalMs]);
30463
+ }, [active, intervalMs]);
30109
30464
  return tick;
30110
30465
  }
30111
30466
  /**
@@ -30122,12 +30477,7 @@ function useStableInput(handler, active) {
30122
30477
  }, []);
30123
30478
  useInput(stableHandler, { isActive: active });
30124
30479
  }
30125
- /**
30126
- * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
30127
- * ring trail clockwise around the eight outer positions (8 frames × 125ms =
30128
- * the web's 1s cycle). Replaces the plain busy ellipsis as the composer's
30129
- * prompt marker and leads the Deep-diving line.
30130
- */
30480
+ /** The original web StateDot chase used by the busy composer marker. */
30131
30481
  function BusyChase() {
30132
30482
  const tick = useFrames(125);
30133
30483
  return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + " ");
@@ -30137,21 +30487,46 @@ function Caret() {
30137
30487
  const tick = useFrames(530);
30138
30488
  return (0, import_react.createElement)(Text, null, caretVisible(tick) ? "▍" : " ");
30139
30489
  }
30140
- /** Blinking input cursor: inverse block while the caret phase is on. */
30141
- function CursorBlock({ char }) {
30142
- const tick = useFrames(530);
30143
- 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
+ };
30144
30509
  }
30145
30510
  /**
30146
- * The busy line, web TurnStatus contract: the StateDot chase leads the plain
30147
- * `Deep diving...` label, with the elapsed clock appended only once the turn
30148
- * has clearly been running (15s) — anchored to `turn/start` so a resumed
30149
- * 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.
30150
30515
  */
30151
30516
  function DeepDivingLine({ since }) {
30152
- useFrames(1e3);
30517
+ const tick = useFrames(33);
30153
30518
  const elapsed = since === 0 ? 0 : Date.now() - since;
30154
- 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
+ }));
30155
30530
  }
30156
30531
  /**
30157
30532
  * The streaming buffer rendered with a hard size cap: the live region must
@@ -31730,49 +32105,6 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31730
32105
  function verboseLine(text, columns) {
31731
32106
  return truncateColumns(displayText(text).replace(/\n/gu, " ↵ ").replace(/\t/gu, " "), Math.max(1, columns));
31732
32107
  }
31733
- /** Identify one whole-chunk key sequence Ink drops or blurs. */
31734
- function annotateRawKey(chunk) {
31735
- switch (chunk) {
31736
- case "": return "delete-backward";
31737
- case "\x1B":
31738
- case "\x1B\b": return "delete-word-backward";
31739
- case "\x1B[3~":
31740
- case "\x1B[3;2~": return "delete-forward";
31741
- case "\x1B[3;3~":
31742
- case "\x1B[3;5~": return "delete-word-forward";
31743
- case "\x1B[H":
31744
- case "\x1B[1~":
31745
- case "\x1B[7~":
31746
- case "\x1BOH": return "home";
31747
- case "\x1B[F":
31748
- case "\x1B[4~":
31749
- case "\x1B[8~":
31750
- case "\x1BOF": return "end";
31751
- default: return;
31752
- }
31753
- }
31754
- /**
31755
- * One-row editor window keeping the logical cursor visible in long drafts.
31756
- * The caret and its surroundings slice at grapheme boundaries: splitting a
31757
- * star-plane surrogate pair would render an isolated half under the block
31758
- * caret with a width the terminal never draws.
31759
- */
31760
- function editorWindow(value, cursor, columns) {
31761
- const width = Math.max(1, columns);
31762
- const normalize = (text) => displayText(text).replace(/\n/gu, "↵").replace(/\t/gu, " ");
31763
- const site = clampCursor(value, cursor);
31764
- const caretSpan = splitGraphemes(value).find((span) => span.start === site);
31765
- const caret = caretSpan === void 0 ? " " : normalize(caretSpan.text);
31766
- const rest = value.slice(caretSpan === void 0 ? site : caretSpan.end);
31767
- const remaining = Math.max(0, width - visibleColumns(caret));
31768
- const afterBudget = Math.min(Math.floor(remaining / 3), visibleColumns(normalize(rest)));
31769
- const beforeBudget = Math.max(0, remaining - afterBudget);
31770
- return {
31771
- before: beforeBudget === 0 ? "" : displayTail(normalize(value.slice(0, site)), beforeBudget, 1).text,
31772
- caret,
31773
- after: afterBudget === 0 ? "" : truncateColumns(normalize(rest), afterBudget)
31774
- };
31775
- }
31776
32108
  /** The empty-composer placeholder text (shared by the static and wave paths). */
31777
32109
  const COMPOSER_PLACEHOLDER = "type a message · / commands · @ mentions";
31778
32110
  /** Adjacent cells with identical styling merge into one styled Text span. */
@@ -32023,7 +32355,9 @@ function CompletionMenu({ active, mention, index, rows }) {
32023
32355
  */
32024
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 }) {
32025
32357
  const columns = useStdout().stdout?.columns ?? 80;
32358
+ const editorColumns = Math.max(1, columns - 6);
32026
32359
  const stdin = useStdin().stdin;
32360
+ const focusReporting = isVsCodeTerminalEnv();
32027
32361
  const [value, setValue] = (0, import_react.useState)("");
32028
32362
  const [cursor, setCursor] = (0, import_react.useState)(0);
32029
32363
  const valueRef = (0, import_react.useRef)(value);
@@ -32034,22 +32368,37 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32034
32368
  const draftImagesRef = (0, import_react.useRef)(draftImages);
32035
32369
  draftImagesRef.current = draftImages;
32036
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
+ }, []);
32037
32378
  const killRef = (0, import_react.useRef)("");
32038
32379
  const preferredColumnRef = (0, import_react.useRef)(null);
32039
32380
  const editorScrollRef = (0, import_react.useRef)(0);
32040
32381
  const pasteBracketRef = (0, import_react.useRef)(false);
32041
32382
  /** Cancels the pending lost-paste safety timer (undefined when disarmed). */
32042
32383
  const pasteBracketCancelRef = (0, import_react.useRef)(void 0);
32043
- /** Annotation of the stdin chunk Ink is about to deliver to useInput. */
32044
- 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);
32045
32388
  const recall = (0, import_react.useRef)(beginRecall([], ""));
32389
+ (0, import_react.useEffect)(() => {
32390
+ preferredColumnRef.current = null;
32391
+ }, [editorColumns]);
32046
32392
  (0, import_react.useEffect)(() => {
32047
32393
  if (historyFill === void 0) return;
32048
32394
  const safe = sanitizeDraftText(historyFill.text);
32049
32395
  draftImagesRef.current = [];
32050
32396
  setDraftImages([]);
32397
+ valueRef.current = safe;
32398
+ cursorRef.current = safe.length;
32051
32399
  setValue(safe);
32052
32400
  setCursor(safe.length);
32401
+ resetCursorBlink();
32053
32402
  preferredColumnRef.current = null;
32054
32403
  setDismissedMenuValue(void 0);
32055
32404
  recall.current = {
@@ -32062,7 +32411,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32062
32411
  }, [
32063
32412
  historyFill,
32064
32413
  recallSpace,
32065
- historyConsumed
32414
+ historyConsumed,
32415
+ resetCursorBlink
32066
32416
  ]);
32067
32417
  (0, import_react.useEffect)(() => {
32068
32418
  setDraftImages((current) => {
@@ -32078,13 +32428,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32078
32428
  const chunk = originalRead(...args);
32079
32429
  if (chunk === null) return chunk;
32080
32430
  const normalized = normalizeKeyboardChunk(typeof chunk === "string" ? chunk : String(chunk));
32081
- rawAnnotation.current = annotateRawKey(normalized);
32082
- return normalized;
32431
+ const input = focusReporting ? stripTerminalFocusEvents(normalized, (focused) => {
32432
+ terminalFocusedRef.current = focused;
32433
+ }) : normalized;
32434
+ rawEditorTokens.current = tokenizeRawEditorChunk(input);
32435
+ return input;
32083
32436
  };
32084
32437
  return () => {
32085
32438
  stdin.read = originalRead;
32086
32439
  };
32087
- }, [stdin]);
32440
+ }, [focusReporting, stdin]);
32088
32441
  if (recall.current.entries !== recallSpace) {
32089
32442
  const index = recall.current.index === null || recall.current.index < recallSpace.length ? recall.current.index : null;
32090
32443
  recall.current = {
@@ -32106,6 +32459,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32106
32459
  };
32107
32460
  const mentionActive = mentionToken !== void 0;
32108
32461
  const [mentionRows, setMentionRows] = (0, import_react.useState)([]);
32462
+ const mentionRequestRef = (0, import_react.useRef)(0);
32109
32463
  const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
32110
32464
  const uniqueImageMarker = (name, source, reserved = []) => {
32111
32465
  const safeName = singleLineText(sanitizeDraftText(name));
@@ -32131,6 +32485,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32131
32485
  return true;
32132
32486
  };
32133
32487
  const insertDroppedImages = (paths) => {
32488
+ const originalValue = valueRef.current;
32489
+ const originalCursor = cursorRef.current;
32134
32490
  notify(`checking ${paths.length} image${paths.length === 1 ? "" : "s"}…`);
32135
32491
  inspectImages(paths).then((inspected) => {
32136
32492
  const additions = [];
@@ -32148,14 +32504,24 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32148
32504
  notify("those images are already attached", "warning");
32149
32505
  return;
32150
32506
  }
32151
- const at = cursorRef.current;
32152
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;
32153
32517
  const insertion = `${at > 0 && !/\s$/u.test(current.slice(0, at)) ? " " : ""}${markers.join(" ")}${current.slice(at) === "" ? "" : " "}`;
32154
- const next = current.slice(0, at) + insertion + current.slice(at);
32155
- valueRef.current = next;
32156
- cursorRef.current = at + insertion.length;
32157
- setValue(next);
32158
- setCursor(cursorRef.current);
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();
32159
32525
  const nextImages = [...draftImagesRef.current, ...additions];
32160
32526
  draftImagesRef.current = nextImages;
32161
32527
  setDraftImages(nextImages);
@@ -32165,14 +32531,21 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32165
32531
  });
32166
32532
  };
32167
32533
  (0, import_react.useEffect)(() => {
32534
+ const requestId = mentionRequestRef.current + 1;
32535
+ mentionRequestRef.current = requestId;
32168
32536
  if (!active || !mentionActive) {
32169
32537
  setMentionRows([]);
32170
32538
  return;
32171
32539
  }
32172
32540
  const controller = new AbortController();
32173
- setMentionRows([]);
32174
- 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);
32175
32547
  return () => {
32548
+ clearTimeout(timer);
32176
32549
  controller.abort();
32177
32550
  };
32178
32551
  }, [
@@ -32180,8 +32553,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32180
32553
  mentionActive,
32181
32554
  mentionToken?.query
32182
32555
  ]);
32183
- const menuActive = (slashActive || mentionActive) && dismissedMenuValue !== value;
32184
- 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) => ({
32185
32559
  label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
32186
32560
  description: row.description,
32187
32561
  origin: "mention"
@@ -32189,34 +32563,45 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32189
32563
  /** Accept the highlighted completion-menu candidate into the draft. */
32190
32564
  const acceptMenuCandidate = () => {
32191
32565
  if (mentionActive && mentionToken !== void 0) {
32192
- const row = mentionRows[completionIndex % mentionRows.length];
32566
+ if (visibleMentionRows.length === 0) return;
32567
+ const row = visibleMentionRows[completionIndex % visibleMentionRows.length];
32193
32568
  if (row !== void 0) {
32194
32569
  if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) {
32195
32570
  const tokenText = value.slice(mentionToken.start, cursor);
32196
32571
  const start = mentionToken.start;
32572
+ const originalValue = value;
32197
32573
  notify(`checking image ${basename(row.path)}…`);
32198
32574
  inspectImages([row.path]).then((inspected) => {
32199
32575
  const inspection = inspected[0];
32200
32576
  if (inspection === void 0) return;
32201
32577
  const current = valueRef.current;
32202
- if (current.slice(start, start + tokenText.length) !== tokenText) return;
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
+ }
32203
32586
  if (draftImagesRef.current.some((image) => sameImagePath(image.path, inspection.path))) {
32204
- const next = current.slice(0, start) + current.slice(start + tokenText.length);
32205
- valueRef.current = next;
32206
- cursorRef.current = start;
32207
- setValue(next);
32208
- setCursor(start);
32209
- setDismissedMenuValue(next);
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);
32210
32594
  notify(`${inspection.name} is already attached`, "warning");
32211
32595
  return;
32212
32596
  }
32213
32597
  const marker = uniqueImageMarker(inspection.name, "mention");
32214
- const next = current.slice(0, start) + marker + current.slice(start + tokenText.length);
32215
- valueRef.current = next;
32216
- cursorRef.current = start + marker.length;
32217
- setValue(next);
32218
- setCursor(cursorRef.current);
32219
- setDismissedMenuValue(next);
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);
32220
32605
  registerDraftImage(inspection, marker);
32221
32606
  notify(`${inspection.name} ready for the next message`);
32222
32607
  }, (reason) => {
@@ -32227,14 +32612,25 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32227
32612
  return;
32228
32613
  }
32229
32614
  const insertion = row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
32230
- setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
32231
- 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();
32232
32622
  }
32233
32623
  } else {
32624
+ if (candidates.length === 0) return;
32234
32625
  const candidate = candidates[completionIndex % candidates.length];
32235
32626
  if (candidate !== void 0) {
32236
- setValue(`${candidate.label} `);
32237
- 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();
32238
32634
  }
32239
32635
  }
32240
32636
  setCompletionIndex(0);
@@ -32243,21 +32639,101 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32243
32639
  /** Apply one editor edit: draft, cursor, kill buffer, menu reset. */
32244
32640
  const applyEdit = (edit) => {
32245
32641
  if (edit.killed !== void 0 && edit.killed !== "") killRef.current = edit.killed;
32642
+ valueRef.current = edit.value;
32643
+ cursorRef.current = edit.cursor;
32246
32644
  setValue(edit.value);
32247
32645
  setCursor(edit.cursor);
32646
+ resetCursorBlink();
32248
32647
  preferredColumnRef.current = null;
32249
32648
  setCompletionIndex(0);
32250
32649
  setDismissedMenuValue(void 0);
32251
32650
  };
32252
32651
  /** Move the cursor without editing; horizontal moves clear the column preference. */
32253
32652
  const moveCursorTo = (next) => {
32254
- if (next === cursor) return;
32653
+ resetCursorBlink();
32654
+ if (next === cursorRef.current) return;
32655
+ cursorRef.current = next;
32255
32656
  setCursor(next);
32256
32657
  preferredColumnRef.current = null;
32257
32658
  };
32258
- 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) => {
32259
32730
  if (!active) return;
32260
- if (preparingImages) 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
+ }
32261
32737
  if (deleteConfirm !== void 0) {
32262
32738
  if (input === "y" || input === "Y") confirmDelete();
32263
32739
  else cancelDelete();
@@ -32273,6 +32749,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32273
32749
  return;
32274
32750
  }
32275
32751
  if (key.ctrl && input === "r") {
32752
+ if (focusReporting && !terminalFocusedRef.current) return;
32276
32753
  toggleReasoning();
32277
32754
  return;
32278
32755
  }
@@ -32282,9 +32759,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32282
32759
  }
32283
32760
  if (key.ctrl && input === "c") {
32284
32761
  if (busy) interrupt();
32285
- else if (value !== "") {
32762
+ else if (liveValue !== "") {
32763
+ valueRef.current = "";
32764
+ cursorRef.current = 0;
32286
32765
  setValue("");
32287
32766
  setCursor(0);
32767
+ resetCursorBlink();
32288
32768
  draftImagesRef.current = [];
32289
32769
  setDraftImages([]);
32290
32770
  setCompletionIndex(0);
@@ -32293,8 +32773,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32293
32773
  return;
32294
32774
  }
32295
32775
  if (key.ctrl && input === "d") {
32296
- if (value !== "") {
32297
- applyEdit(deleteForward(value, cursor));
32776
+ if (liveValue !== "") {
32777
+ applyEdit(deleteForward(liveValue, liveCursor));
32298
32778
  return;
32299
32779
  }
32300
32780
  if (busy) notify("cancel the running turn before exiting (Esc or Ctrl+C)", "warning");
@@ -32303,7 +32783,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32303
32783
  }
32304
32784
  if (key.escape) {
32305
32785
  if (menuActive) {
32306
- setDismissedMenuValue(value);
32786
+ setDismissedMenuValue(liveValue);
32307
32787
  return;
32308
32788
  }
32309
32789
  if (hasNotice) {
@@ -32313,27 +32793,33 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32313
32793
  if (busy) interrupt();
32314
32794
  return;
32315
32795
  }
32316
- if (key.delete && value === "" && queued.length > 0) {
32796
+ if (key.delete && liveValue === "" && queued.length > 0) {
32317
32797
  cancelQueued(queued[queued.length - 1].messageId);
32318
32798
  return;
32319
32799
  }
32320
32800
  if (key.return) {
32321
32801
  if (pasteBracketRef.current) {
32322
- applyEdit(insertText(value, cursor, "\n"));
32802
+ applyEdit(insertText(liveValue, liveCursor, "\n"));
32323
32803
  return;
32324
32804
  }
32325
32805
  if (menuActive) {
32326
- if (!(!mentionActive && candidates.some((candidate) => candidate.label === value))) {
32806
+ if (!(!mentionActive && candidates.some((candidate) => candidate.label === liveValue))) {
32327
32807
  acceptMenuCandidate();
32328
32808
  return;
32329
32809
  }
32330
32810
  }
32331
- const text = value.trim();
32811
+ const text = liveValue.trim();
32332
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;
32333
32817
  setPreparingImages(true);
32334
32818
  notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? "" : "s"}…`);
32335
32819
  const snapshot = draftImagesRef.current;
32336
- prepareImages(snapshot.map((image) => image.path)).then((images) => {
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;
32337
32823
  setPreparingImages(false);
32338
32824
  valueRef.current = "";
32339
32825
  cursorRef.current = 0;
@@ -32352,13 +32838,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32352
32838
  if (busy) steer(text, images);
32353
32839
  else dispatch(text, images);
32354
32840
  }, (reason) => {
32841
+ if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
32842
+ prepareAbortRef.current = void 0;
32355
32843
  setPreparingImages(false);
32356
32844
  notify(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
32357
32845
  });
32358
32846
  return;
32359
32847
  }
32848
+ valueRef.current = "";
32849
+ cursorRef.current = 0;
32360
32850
  setValue("");
32361
32851
  setCursor(0);
32852
+ resetCursorBlink();
32362
32853
  setCompletionIndex(0);
32363
32854
  setDismissedMenuValue(void 0);
32364
32855
  if (text === "") return;
@@ -32485,6 +32976,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32485
32976
  return;
32486
32977
  }
32487
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
+ }
32488
32985
  if (menuActive && key.upArrow) {
32489
32986
  setCompletionIndex((index) => (index + menuRows.length - 1) % menuRows.length);
32490
32987
  return;
@@ -32493,119 +32990,74 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32493
32990
  setCompletionIndex((index) => (index + 1) % menuRows.length);
32494
32991
  return;
32495
32992
  }
32496
- const rawKey = rawAnnotation.current;
32497
- if (rawKey !== void 0) {
32498
- if (rawKey === "home") moveCursorTo(lineBounds(value, cursor).start);
32499
- else if (rawKey === "end") moveCursorTo(lineBounds(value, cursor).end);
32500
- else if (rawKey === "delete-backward") applyEdit(deleteBackward(value, cursor));
32501
- else if (rawKey === "delete-word-backward") applyEdit(deleteWordBackward(value, cursor));
32502
- else if (rawKey === "delete-forward") applyEdit(deleteForward(value, cursor));
32503
- else applyEdit(deleteWordForward(value, cursor));
32993
+ const rawTokens = rawEditorTokens.current;
32994
+ rawEditorTokens.current = void 0;
32995
+ if (rawTokens !== void 0) {
32996
+ applyRawEditorTokens(rawTokens);
32504
32997
  return;
32505
32998
  }
32506
32999
  if (key.upArrow || key.downArrow) {
32507
- if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
32508
- const step = key.upArrow ? recallOlder(recall.current, value) : recallNewer(recall.current);
32509
- recall.current = step.state;
32510
- if (step.entry !== void 0) {
32511
- const safe = sanitizeDraftText(step.entry);
32512
- setValue(safe);
32513
- setCursor(safe.length);
32514
- preferredColumnRef.current = null;
32515
- setDismissedMenuValue(void 0);
32516
- }
32517
- return;
32518
- }
32519
- const model = editorModel(value, Math.max(1, columns - 6));
32520
- const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
32521
- const next = moveCursorVertically(model, cursor, preferred, key.upArrow ? -1 : 1);
32522
- if (next !== cursor) {
32523
- setCursor(next);
32524
- preferredColumnRef.current = preferred;
32525
- }
33000
+ navigateVertical(key.upArrow ? -1 : 1);
32526
33001
  return;
32527
33002
  }
32528
33003
  if (key.ctrl && (input === "p" || input === "n")) {
32529
- const up = input === "p";
32530
- if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
32531
- const step = up ? recallOlder(recall.current, value) : recallNewer(recall.current);
32532
- recall.current = step.state;
32533
- if (step.entry !== void 0) {
32534
- const safe = sanitizeDraftText(step.entry);
32535
- setValue(safe);
32536
- setCursor(safe.length);
32537
- preferredColumnRef.current = null;
32538
- setDismissedMenuValue(void 0);
32539
- }
32540
- return;
32541
- }
32542
- const model = editorModel(value, Math.max(1, columns - 6));
32543
- const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
32544
- const next = moveCursorVertically(model, cursor, preferred, up ? -1 : 1);
32545
- if (next !== cursor) {
32546
- setCursor(next);
32547
- preferredColumnRef.current = preferred;
32548
- }
32549
- return;
32550
- }
32551
- if (key.tab && menuActive) {
32552
- acceptMenuCandidate();
33004
+ navigateVertical(input === "p" ? -1 : 1);
32553
33005
  return;
32554
33006
  }
32555
33007
  if (key.leftArrow) {
32556
- 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));
32557
33009
  return;
32558
33010
  }
32559
33011
  if (key.rightArrow) {
32560
- 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));
32561
33013
  return;
32562
33014
  }
32563
33015
  if (key.meta && input === "b") {
32564
- moveCursorTo(moveWordLeft(value, cursor));
33016
+ moveCursorTo(moveWordLeft(liveValue, liveCursor));
32565
33017
  return;
32566
33018
  }
32567
33019
  if (key.meta && input === "f") {
32568
- moveCursorTo(moveWordRight(value, cursor));
33020
+ moveCursorTo(moveWordRight(liveValue, liveCursor));
32569
33021
  return;
32570
33022
  }
32571
33023
  if (key.ctrl && input === "b") {
32572
- moveCursorTo(moveCursorBy(value, cursor, -1));
33024
+ moveCursorTo(moveCursorBy(liveValue, liveCursor, -1));
32573
33025
  return;
32574
33026
  }
32575
33027
  if (key.ctrl && input === "f") {
32576
- moveCursorTo(moveCursorBy(value, cursor, 1));
33028
+ moveCursorTo(moveCursorBy(liveValue, liveCursor, 1));
32577
33029
  return;
32578
33030
  }
32579
33031
  if (key.ctrl && input === "w") {
32580
- applyEdit(deleteWordBackward(value, cursor));
33032
+ applyEdit(deleteWordBackward(liveValue, liveCursor));
32581
33033
  return;
32582
33034
  }
32583
33035
  if (key.meta && input === "d") {
32584
- applyEdit(deleteWordForward(value, cursor));
33036
+ applyEdit(deleteWordForward(liveValue, liveCursor));
32585
33037
  return;
32586
33038
  }
32587
33039
  if (key.backspace || key.delete) {
32588
- applyEdit(deleteBackward(value, cursor));
33040
+ applyEdit(deleteBackward(liveValue, liveCursor));
32589
33041
  return;
32590
33042
  }
32591
33043
  if (key.ctrl && input === "a") {
32592
- moveCursorTo(lineBounds(value, cursor).start);
33044
+ moveCursorTo(moveToLineStart(liveValue, liveCursor, true));
32593
33045
  return;
32594
33046
  }
32595
33047
  if (key.ctrl && input === "e") {
32596
- moveCursorTo(lineBounds(value, cursor).end);
33048
+ moveCursorTo(moveToLineEnd(liveValue, liveCursor, true));
32597
33049
  return;
32598
33050
  }
32599
33051
  if (key.ctrl && input === "u") {
32600
- applyEdit(killToLineStart(value, cursor));
33052
+ applyEdit(killToLineStart(liveValue, liveCursor));
32601
33053
  return;
32602
33054
  }
32603
33055
  if (key.ctrl && input === "k") {
32604
- applyEdit(killToLineEnd(value, cursor));
33056
+ applyEdit(killToLineEnd(liveValue, liveCursor));
32605
33057
  return;
32606
33058
  }
32607
33059
  if (key.ctrl && input === "y") {
32608
- if (killRef.current !== "") applyEdit(insertText(value, cursor, killRef.current));
33060
+ if (killRef.current !== "") applyEdit(insertText(liveValue, liveCursor, killRef.current));
32609
33061
  return;
32610
33062
  }
32611
33063
  if (key.ctrl && input === "l") {
@@ -32638,9 +33090,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32638
33090
  insertDroppedImages(droppedPaths);
32639
33091
  return;
32640
33092
  }
32641
- applyEdit(insertText(value, cursor, text));
33093
+ applyEdit(insertText(valueRef.current, cursorRef.current, text));
32642
33094
  }
32643
- });
33095
+ }, active);
32644
33096
  const [waveTick, setWaveTick] = (0, import_react.useState)(null);
32645
33097
  const wavePrevious = (0, import_react.useRef)({
32646
33098
  tier: null,
@@ -32658,7 +33110,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32658
33110
  }
32659
33111
  if (previous.tier !== waveTier || previous.style !== waveStyle) setWaveTick(0);
32660
33112
  }, [waveTier, waveStyle]);
32661
- 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);
32662
33114
  (0, import_react.useEffect)(() => {
32663
33115
  if (!waveActive) return;
32664
33116
  const id = setInterval(() => {
@@ -32679,7 +33131,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32679
33131
  const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
32680
33132
  const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
32681
33133
  const promptGlyph = waveTier === "flash" ? "›" : waveTier === "deepseek" ? "»" : "❯";
32682
- const editorViewModel = editorModel(value, Math.max(1, columns - 6));
33134
+ const editorViewModel = editorModel(value, editorColumns);
32683
33135
  const clampedCursor = clampCursor(value, cursor);
32684
33136
  const caret = caretSite(editorViewModel, clampedCursor);
32685
33137
  const editorWindowRows = Math.min(editorViewModel.rows.length, Math.max(1, maxRows));
@@ -32730,123 +33182,120 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32730
33182
  const editorRows = [];
32731
33183
  for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
32732
33184
  const row = editorViewModel.rows[index];
32733
- const caretAt = index === caret.row ? row.offsets.indexOf(clampedCursor) : -1;
32734
- const before = caretAt > 0 ? row.text.slice(0, row.cuts[caretAt]) : "";
32735
- const caretChar = caretAt >= 0 && caretAt < row.cuts.length - 1 ? row.text.slice(row.cuts[caretAt], row.cuts[caretAt + 1]) : " ";
32736
- const after = caretAt < 0 ? row.text : caretAt < row.cuts.length - 1 ? row.text.slice(row.cuts[caretAt + 1]) : "";
32737
- const placeholder = index === 0 && value === "" && !busy;
32738
- const tail = placeholder ? COMPOSER_PLACEHOLDER : after;
32739
- 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);
32740
33189
  editorRows.push((0, import_react.createElement)(Text, {
32741
33190
  key: index,
32742
33191
  backgroundColor: bandBg,
32743
33192
  wrap: "truncate-end"
32744
- }, 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, {
32745
33197
  color: promptColor,
32746
33198
  bold: tierActive ? true : void 0
32747
- }, `${promptGlyph} `) : " ", before, (0, import_react.createElement)(CursorBlock, {
33199
+ }, `${promptGlyph} `) : " ", parts.before, parts.hasCaret ? (0, import_react.createElement)(Text, {
32748
33200
  key: "caret",
32749
- char: caretChar
32750
- }), 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)));
32751
33203
  }
32752
33204
  const staticEditor = (0, import_react.createElement)(Box, { flexDirection: "column" }, ...editorRows);
32753
33205
  const waveRow = () => {
32754
33206
  const hues = deepseekWaveHues(waveTier);
32755
33207
  const style = waveStyle;
32756
33208
  const bandRgb = getPalette().composerBand;
33209
+ const visibleRows = editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows);
33210
+ const totalBandRows = visibleRows.length + 2;
32757
33211
  const waveBg = (row, column) => {
32758
- 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);
32759
33213
  return rgb === null ? bandBg : inkColor(rgb);
32760
33214
  };
32761
33215
  const blankBandRow = (row) => {
32762
33216
  const blanks = [];
32763
- while (blanks.length < bandWidth) blanks.push({
33217
+ for (let column = 0; column < bandWidth; column += 1) blanks.push({
32764
33218
  char: " ",
32765
- backgroundColor: waveBg(row, blanks.length)
33219
+ width: 1,
33220
+ backgroundColor: waveBg(row, column)
32766
33221
  });
32767
- return (0, import_react.createElement)(Text, { key: row }, ...waveRowSpans(blanks));
33222
+ return (0, import_react.createElement)(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks));
32768
33223
  };
32769
- const waveEditor = editorWindow(value, cursor, Math.max(1, columns - 7));
32770
- const cells = [
32771
- {
32772
- char: " ",
32773
- backgroundColor: waveBg(1, 0)
32774
- },
32775
- {
32776
- char: " ",
32777
- backgroundColor: waveBg(1, 1)
32778
- },
32779
- {
32780
- char: promptGlyph,
32781
- color: promptColor,
32782
- bold: true,
32783
- backgroundColor: waveBg(1, 2)
32784
- },
32785
- {
32786
- char: " ",
32787
- color: promptColor,
32788
- 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;
32789
33230
  }
32790
- ];
32791
- for (const char of waveEditor.before) cells.push({
32792
- char,
32793
- backgroundColor: waveBg(1, cells.length)
32794
- });
32795
- cells.push({
32796
- char: waveEditor.caret,
32797
- inverse: true,
32798
- backgroundColor: waveBg(1, cells.length)
32799
- });
32800
- if (value === "" && !busy) for (let at = 0; at < 40; at += 1) cells.push({
32801
- char: COMPOSER_PLACEHOLDER[at],
32802
- dim: true,
32803
- backgroundColor: waveBg(1, cells.length)
32804
- });
32805
- else for (const char of waveEditor.after) cells.push({
32806
- char,
32807
- backgroundColor: waveBg(1, cells.length)
32808
- });
32809
- while (cells.length < bandWidth) cells.push({
32810
- char: " ",
32811
- 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));
32812
33292
  });
32813
- if (deepseekWaveWordVisible(waveTick, waveTier, style)) {
32814
- const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
32815
- const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
32816
- let clear = true;
32817
- for (let at = 0; at < word.length; at += 1) {
32818
- const cell = cells[start + at];
32819
- if (cell === void 0 || cell.char !== " " && cell.dim !== true) {
32820
- clear = false;
32821
- break;
32822
- }
32823
- }
32824
- if (clear) for (let at = 0; at < word.length; at += 1) {
32825
- const cell = cells[start + at];
32826
- cell.char = word[at];
32827
- cell.color = inkColor(deepseekWaveWordHue(at, hues));
32828
- cell.bold = true;
32829
- cell.dim = false;
32830
- }
32831
- }
32832
- if ((waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
32833
- const spark = deepseekWaveSpark(waveTick);
32834
- if (spark !== null) {
32835
- const last = cells[cells.length - 1];
32836
- if (last !== void 0 && last.char === " ") {
32837
- last.char = spark;
32838
- last.color = promptColor;
32839
- last.bold = true;
32840
- last.dim = false;
32841
- }
32842
- }
32843
- }
32844
33293
  return (0, import_react.createElement)(Box, {
32845
33294
  flexDirection: "column",
32846
33295
  width: bandWidth
32847
- }, blankBandRow(0), (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...waveRowSpans(cells)), blankBandRow(2));
33296
+ }, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
32848
33297
  };
32849
- 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));
32850
33299
  }
32851
33300
  /** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
32852
33301
  function buildSettledRow(entry, index, showReasoning, columns) {
@@ -33992,7 +34441,9 @@ function isSlashLine(line) {
33992
34441
  /** Substitutable runner effects; production values write to the real terminal. */
33993
34442
  const internals = {
33994
34443
  mount: (element) => {
33995
- 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 : ""));
33996
34447
  const instance = render(element, { exitOnCtrlC: false });
33997
34448
  return {
33998
34449
  rerender(element) {
@@ -34000,7 +34451,7 @@ const internals = {
34000
34451
  },
34001
34452
  unmount() {
34002
34453
  instance.unmount();
34003
- 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 : ""));
34004
34455
  }
34005
34456
  };
34006
34457
  },
@@ -34504,83 +34955,6 @@ async function removeProviderSettings(ctx, target) {
34504
34955
  }
34505
34956
  }
34506
34957
  //#endregion
34507
- //#region src/mentions.ts
34508
- /** Menu cap on file rows; the service owns ranking and default rows. */
34509
- const MAX_FILE_ROWS = 20;
34510
- /**
34511
- * Create the mention API for one agent's workspace. A missing
34512
- * `fileReferences` service (with an agent present) or `sessionReferenceResolver`
34513
- * degrades that half to empty rows; `prepare` passes text through untouched
34514
- * without references. An undefined agent (a bare launch before any session
34515
- * exists) runs the official WorkspaceFileSearch over the launch cwd — the
34516
- * same class the mounted service uses per agent — so `@` file completion
34517
- * works from the first keystroke; session references wait for the session.
34518
- *
34519
- * `candidates` never reaches for `this` — the runner hands it to the input
34520
- * editor as a detached callback, and a `this`-bound method would throw on
34521
- * every `@` key.
34522
- * @param ctx - context carrying the optional `fileReferences` and
34523
- * `sessionReferenceResolver` services.
34524
- * @param agent - the session owner; excluded from its own session candidates.
34525
- * @param cwd - launch working directory; bounds the pre-session search.
34526
- */
34527
- function createMentions(ctx, agent, cwd) {
34528
- const resolver = ctx.get("sessionReferenceResolver");
34529
- const fileReferences = ctx.get("fileReferences");
34530
- const sessionCapable = agent !== void 0 && resolver !== void 0;
34531
- let preSessionSearch;
34532
- const preSessionFiles = (query, signal) => {
34533
- preSessionSearch ??= new WorkspaceFileSearch(cwd, {
34534
- maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
34535
- maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
34536
- excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
34537
- });
34538
- return preSessionSearch.list(query, signal ?? new AbortController().signal);
34539
- };
34540
- return {
34541
- async candidates(query, signal) {
34542
- const needle = query.trim();
34543
- 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([])]);
34544
- const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
34545
- label: candidate.path,
34546
- description: candidate.kind === "directory" ? "Folder" : "File",
34547
- kind: candidate.kind,
34548
- ...candidate.kind === "file" ? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) } : {}
34549
- }));
34550
- const sessionRows = sessions.map((candidate) => ({
34551
- label: formatSessionReferenceMention(candidate),
34552
- description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
34553
- kind: "session"
34554
- }));
34555
- return [...fileRows, ...sessionRows];
34556
- },
34557
- parse(text) {
34558
- return parseSessionReferenceText(text);
34559
- },
34560
- async prepare(parsed, signal) {
34561
- if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
34562
- text: parsed.text,
34563
- references: parsed.references
34564
- };
34565
- const prepared = await resolver.prepare(agent, [{
34566
- type: "text",
34567
- text: parsed.text
34568
- }], parsed.references, signal);
34569
- return {
34570
- text: prepared.content.filter((block) => block.type === "text").map((block) => block.text).join(""),
34571
- references: parsed.references,
34572
- additionalContext: prepared.additionalContext
34573
- };
34574
- },
34575
- sessionMention(candidate) {
34576
- return formatSessionReferenceMention({
34577
- sessionId: candidate.sessionId,
34578
- label: candidate.label
34579
- });
34580
- }
34581
- };
34582
- }
34583
- //#endregion
34584
34958
  //#region src/questions.ts
34585
34959
  const ABORT_ERROR = new UserQuestionError("ask_user_question was interrupted before the user answered", "ASK_ABORTED");
34586
34960
  /**
@@ -36331,7 +36705,7 @@ async function run(ctx, startup, io) {
36331
36705
  copyTextValue: copyText,
36332
36706
  loadMentions: (query, signal) => mentions.candidates(query, signal),
36333
36707
  inspectImages: (paths) => inspectImagePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
36334
- prepareImages: (paths) => saveImagePaths(paths, ctx.get("attachments")),
36708
+ prepareImages: (paths, signal) => saveImagePaths(paths, ctx.get("attachments"), signal),
36335
36709
  cyclePermission: cyclePermission$1,
36336
36710
  setPermission: setPermissionAction,
36337
36711
  selectModel,