dsh-code 1.0.3 → 1.0.5

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.
Files changed (45) hide show
  1. package/README.md +293 -285
  2. package/bin/deepseek.mjs +245 -12
  3. package/cordis.patch.yml +12 -14
  4. package/lib/index.mjs +1939 -903
  5. package/lib/types/app.d.ts +11 -2
  6. package/lib/types/commands.d.ts +13 -0
  7. package/lib/types/git-workflow.d.ts +7 -2
  8. package/lib/types/history.d.ts +18 -11
  9. package/lib/types/index.d.ts +28 -0
  10. package/lib/types/input-split.d.ts +54 -0
  11. package/lib/types/kernel-panels.d.ts +3 -1
  12. package/lib/types/keyboard.d.ts +8 -0
  13. package/lib/types/presets.d.ts +4 -1
  14. package/lib/types/provider-settings.d.ts +77 -0
  15. package/lib/types/questions.d.ts +16 -12
  16. package/lib/types/render/projection.d.ts +9 -2
  17. package/lib/types/render/status.d.ts +22 -15
  18. package/lib/types/settings-file.d.ts +8 -0
  19. package/lib/types/skills.d.ts +1 -1
  20. package/package.json +49 -46
  21. package/src/app.ts +5459 -4900
  22. package/src/approval.ts +8 -3
  23. package/src/authorization-panel.ts +2 -4
  24. package/src/commands.ts +27 -3
  25. package/src/git-workflow.ts +29 -10
  26. package/src/history.ts +22 -13
  27. package/src/index.ts +203 -61
  28. package/src/input-split.ts +191 -0
  29. package/src/internals.ts +26 -8
  30. package/src/kernel-panels.ts +26 -10
  31. package/src/keyboard.ts +123 -88
  32. package/src/mentions.ts +42 -9
  33. package/src/permissions.ts +1 -1
  34. package/src/presets.ts +19 -6
  35. package/src/provider-settings.ts +204 -0
  36. package/src/questions.ts +58 -55
  37. package/src/render/export.ts +7 -7
  38. package/src/render/lines.ts +24 -12
  39. package/src/render/markdown.ts +15 -13
  40. package/src/render/projection.ts +101 -13
  41. package/src/render/status.ts +76 -71
  42. package/src/render/text.ts +9 -3
  43. package/src/settings-file.ts +38 -6
  44. package/src/skills.ts +19 -6
  45. package/src/theme-panel.ts +79 -72
package/lib/index.mjs CHANGED
@@ -4,11 +4,11 @@ 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, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
7
+ import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
8
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
- import { MessageId, ReasoningEffortId, assertNever, boundContextSummary, createUserMessage, normalizeApiKey } from "@deepseek-ai/dsh-llm";
11
+ import { MessageId, ReasoningEffortId, boundContextSummary, createUserMessage, normalizeApiKey } from "@deepseek-ai/dsh-llm";
12
12
  import { SessionId } from "@deepseek-ai/dsh-session";
13
13
  import { PassThrough, Stream } from "node:stream";
14
14
  import process$1, { cwd, env } from "node:process";
@@ -22,6 +22,7 @@ import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
24
24
  import { isUserInvocable } from "@deepseek-ai/dsh-skill";
25
+ import { assertNever } from "@deepseek-ai/dsh-util-values";
25
26
  //#region node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
26
27
  /**
27
28
  * @license React
@@ -24648,10 +24649,12 @@ function displayTail(text, columns, rows) {
24648
24649
  used += width;
24649
24650
  lastCluster = cluster;
24650
24651
  }
24651
- if (current !== "" || wrapped.length > 0 && text.endsWith("\n")) flush();
24652
+ const trailingBlank = current === "" && wrapped.length > 0 && text.endsWith("\n");
24653
+ if (current !== "") flush();
24652
24654
  const truncated = wrapped.length > rowLimit;
24655
+ const kept = truncated ? wrapped.slice(-rowLimit) : wrapped;
24653
24656
  return {
24654
- text: (truncated ? wrapped.slice(-rowLimit) : wrapped).join("\n"),
24657
+ text: (trailingBlank && kept.length < rowLimit ? [...kept, ""] : kept).join("\n"),
24655
24658
  truncated
24656
24659
  };
24657
24660
  }
@@ -24702,8 +24705,11 @@ function ThemePanel({ current, select, close }) {
24702
24705
  if (key.downArrow) return setCursor((value) => (value + 1) % THEME_ROWS.length);
24703
24706
  if (key.return) return select(THEME_ROWS[cursor].id);
24704
24707
  });
24705
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
24706
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/theme · esc close", viewport.contentColumns));
24708
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/theme · esc close", viewport.contentColumns));
24709
+ const rowBudget = Math.max(1, viewport.bodyRows);
24710
+ const first = clampScroll(cursor, THEME_ROWS.length, rowBudget);
24711
+ const visibleThemes = THEME_ROWS.slice(first, first + rowBudget);
24712
+ const hiddenThemes = THEME_ROWS.length - visibleThemes.length;
24707
24713
  return (0, import_react.createElement)(Box, {
24708
24714
  width: viewport.outerColumns,
24709
24715
  borderStyle: "round",
@@ -24713,8 +24719,8 @@ function ThemePanel({ current, select, close }) {
24713
24719
  }, (0, import_react.createElement)(Text, {
24714
24720
  color: inkColor(getPalette().brandBright),
24715
24721
  wrap: "truncate-end"
24716
- }, truncateColumns("/theme — color palette", viewport.contentColumns)), ...THEME_ROWS.map((theme, index) => {
24717
- const selected = index === cursor;
24722
+ }, truncateColumns("/theme — color palette", viewport.contentColumns)), ...visibleThemes.map((theme, index) => {
24723
+ const selected = first + index === cursor;
24718
24724
  const active = theme.id === current;
24719
24725
  return (0, import_react.createElement)(Text, {
24720
24726
  key: theme.id,
@@ -24724,7 +24730,7 @@ function ThemePanel({ current, select, close }) {
24724
24730
  }), (0, import_react.createElement)(Text, {
24725
24731
  dimColor: true,
24726
24732
  wrap: "truncate-end"
24727
- }, truncateColumns("↑↓ choose · enter apply · esc/q close", viewport.contentColumns)));
24733
+ }, truncateColumns(`↑↓ choose · enter apply · esc/q close${hiddenThemes > 0 ? ` · +${hiddenThemes} more` : ""}`, viewport.contentColumns)));
24728
24734
  }
24729
24735
  //#endregion
24730
24736
  //#region src/whale-glyph.ts
@@ -25201,6 +25207,29 @@ function indexList(map, id) {
25201
25207
  return list;
25202
25208
  }
25203
25209
  /**
25210
+ * Finalize replay rows the ended turn left `running`, mirroring the reducer's
25211
+ * turn-end orphan sweep: an orphaned retry settles `done`, an orphaned command
25212
+ * settles `error` with an interruption note. Only the id-indexed rows are
25213
+ * visited, so the sweep stays O(retries+commands of the log), never a scan.
25214
+ */
25215
+ function finalizeReplayOrphans(acc) {
25216
+ for (const list of acc.retryIndex.values()) for (const index of list) {
25217
+ const entry = acc.entries[index];
25218
+ if (entry !== void 0 && entry.kind === "retry" && entry.state === "running") acc.entries[index] = {
25219
+ ...entry,
25220
+ state: "done"
25221
+ };
25222
+ }
25223
+ for (const list of acc.commandIndex.values()) for (const index of list) {
25224
+ const entry = acc.entries[index];
25225
+ if (entry !== void 0 && entry.kind === "command" && entry.state === "running") acc.entries[index] = {
25226
+ ...entry,
25227
+ state: "error",
25228
+ summary: "interrupted before the turn ended"
25229
+ };
25230
+ }
25231
+ }
25232
+ /**
25204
25233
  * Apply an id-keyed update to every row that registered the id, mirroring the
25205
25234
  * copy-on-write reducer's full-array map semantics (all matching rows update,
25206
25235
  * in order). Each registered index is O(1), so a duplicate id costs
@@ -25303,6 +25332,7 @@ function replayProjectEvent(acc, event) {
25303
25332
  if (entry !== void 0 && entry.kind === "pending" && entry.target === target) retireReplayEntry(acc, entryIndex);
25304
25333
  }
25305
25334
  }
25335
+ ids.splice(start, 0, ...inserted.map((message) => message.id));
25306
25336
  for (const message of inserted) {
25307
25337
  const images = imagesOf(message.content);
25308
25338
  appendReplayEntry(acc, {
@@ -25313,7 +25343,6 @@ function replayProjectEvent(acc, event) {
25313
25343
  ...images.length === 0 ? {} : { images }
25314
25344
  });
25315
25345
  indexList(acc.pendingIndex, message.id).push(acc.entries.length - 1);
25316
- ids.push(message.id);
25317
25346
  acc.ops += 1;
25318
25347
  }
25319
25348
  return true;
@@ -25507,6 +25536,7 @@ function replayProjectEvent(acc, event) {
25507
25536
  for (const callId of turnToolSet) acc.toolStart.delete(callId);
25508
25537
  acc.turnTools.delete(event.data.turn);
25509
25538
  }
25539
+ finalizeReplayOrphans(acc);
25510
25540
  acc.busy = false;
25511
25541
  acc.busySince = 0;
25512
25542
  for (const entry of appended) appendReplayEntry(acc, entry);
@@ -25519,6 +25549,7 @@ function replayProjectEvent(acc, event) {
25519
25549
  appendReplayEntry(acc, {
25520
25550
  kind: "retry",
25521
25551
  retryId: data.retryId,
25552
+ mode: data.mode,
25522
25553
  attempt: data.retry,
25523
25554
  max: "maxRetries" in data ? data.maxRetries : data.retry,
25524
25555
  code: data.failure.code,
@@ -25678,14 +25709,14 @@ function materializeReplayView(acc, copy) {
25678
25709
  },
25679
25710
  stats: acc.stats,
25680
25711
  anchors: {
25681
- stepStart: acc.stepStart,
25682
- toolStart: acc.toolStart,
25683
- firstChunkAt: acc.firstChunkAt,
25684
- compactionTokens: acc.compactionTokens,
25712
+ stepStart: new Map(acc.stepStart),
25713
+ toolStart: new Map(acc.toolStart),
25714
+ firstChunkAt: new Map(acc.firstChunkAt),
25715
+ compactionTokens: new Map(acc.compactionTokens),
25685
25716
  lastPruneTokens: acc.lastPruneTokens,
25686
- turnFiles: acc.turnFiles,
25687
- turnSteps: acc.turnSteps,
25688
- turnTools: acc.turnTools
25717
+ turnFiles: new Map([...acc.turnFiles].map(([turn, files]) => [turn, new Set(files)])),
25718
+ turnSteps: new Map(acc.turnSteps),
25719
+ turnTools: new Map([...acc.turnTools].map(([turn, tools]) => [turn, new Set(tools)]))
25689
25720
  }
25690
25721
  };
25691
25722
  }
@@ -25759,19 +25790,19 @@ function wrapUnits(segments) {
25759
25790
  });
25760
25791
  word = "";
25761
25792
  };
25762
- for (const char of segment.text) if (char === " ") {
25793
+ for (const cluster of splitGraphemes$1(segment.text)) if (cluster === " ") {
25763
25794
  flushWord();
25764
25795
  units.push({
25765
- text: char,
25796
+ text: cluster,
25766
25797
  style: segment.style
25767
25798
  });
25768
- } else if (visibleColumns(char) > 1) {
25799
+ } else if (graphemeWidth(cluster) > 1) {
25769
25800
  flushWord();
25770
25801
  units.push({
25771
- text: char,
25802
+ text: cluster,
25772
25803
  style: segment.style
25773
25804
  });
25774
- } else word += char;
25805
+ } else word += cluster;
25775
25806
  flushWord();
25776
25807
  }
25777
25808
  return units;
@@ -25797,8 +25828,8 @@ function wrapSegments(segments, width) {
25797
25828
  const append = (unit) => {
25798
25829
  const columns = visibleColumns(unit.text);
25799
25830
  if (used === 0 && columns > limit) {
25800
- for (const char of unit.text) appendAtom({
25801
- text: char,
25831
+ for (const cluster of splitGraphemes$1(unit.text)) appendAtom({
25832
+ text: cluster,
25802
25833
  style: unit.style
25803
25834
  });
25804
25835
  return;
@@ -25818,8 +25849,8 @@ function wrapSegments(segments, width) {
25818
25849
  return;
25819
25850
  }
25820
25851
  flush();
25821
- if (columns > limit) for (const char of unit.text) appendAtom({
25822
- text: char,
25852
+ if (columns > limit) for (const cluster of splitGraphemes$1(unit.text)) appendAtom({
25853
+ text: cluster,
25823
25854
  style: unit.style
25824
25855
  });
25825
25856
  else appendAtom(unit);
@@ -26038,13 +26069,13 @@ function hardWrapSegments(segments, width) {
26038
26069
  current = [];
26039
26070
  used = 0;
26040
26071
  };
26041
- for (const segment of segments) for (const char of segment.text) {
26042
- const cells = visibleColumns(char);
26072
+ for (const segment of segments) for (const cluster of splitGraphemes$1(segment.text)) {
26073
+ const cells = graphemeWidth(cluster);
26043
26074
  if (used > 0 && used + cells > width) flush();
26044
26075
  const previous = current.at(-1);
26045
- if (previous?.style === segment.style) previous.text += char;
26076
+ if (previous?.style === segment.style) previous.text += cluster;
26046
26077
  else current.push({
26047
- text: char,
26078
+ text: cluster,
26048
26079
  style: segment.style
26049
26080
  });
26050
26081
  used += cells;
@@ -26713,6 +26744,556 @@ function effortAboveHigh(effort) {
26713
26744
  return rank !== void 0 && rank > 3;
26714
26745
  }
26715
26746
  //#endregion
26747
+ //#region src/commands.ts
26748
+ /**
26749
+ * Watch the live command registry. Reads the current list immediately and
26750
+ * re-reads on every registry mutation or agent retarget; notification
26751
+ * failures are contained by the registry itself, so this watcher only ever
26752
+ * re-reads. Without a `commands` service the view stays empty and all lines
26753
+ * fall through to normal prompts.
26754
+ * @param ctx - context carrying the `commands` service (optional).
26755
+ * @returns the view the completion menu subscribes to.
26756
+ */
26757
+ function watchCommands(ctx) {
26758
+ const commands = ctx.get("commands");
26759
+ let agent;
26760
+ let descriptors = [];
26761
+ let error;
26762
+ let loadedFor;
26763
+ const listeners = /* @__PURE__ */ new Set();
26764
+ const refresh = () => {
26765
+ if (commands === void 0 || agent === void 0) return;
26766
+ try {
26767
+ descriptors = commands.list(agent);
26768
+ loadedFor = agent;
26769
+ error = void 0;
26770
+ } catch (cause) {
26771
+ descriptors = loadedFor === agent ? [...descriptors] : [];
26772
+ error = cause instanceof Error ? cause.message : String(cause);
26773
+ }
26774
+ for (const listener of listeners) listener();
26775
+ };
26776
+ if (commands !== void 0) ctx.on("commands/change", () => refresh());
26777
+ return {
26778
+ get descriptors() {
26779
+ return descriptors;
26780
+ },
26781
+ get error() {
26782
+ return error;
26783
+ },
26784
+ subscribe(listener) {
26785
+ listeners.add(listener);
26786
+ return () => {
26787
+ listeners.delete(listener);
26788
+ };
26789
+ },
26790
+ setAgent(next) {
26791
+ agent = next;
26792
+ refresh();
26793
+ }
26794
+ };
26795
+ }
26796
+ /**
26797
+ * Whether one command line is a syntactically valid slash command.
26798
+ * @param line - the complete candidate line.
26799
+ * @returns true when the line parses as `/name` or `/name input`.
26800
+ */
26801
+ function isSlashLine(line) {
26802
+ return /^\/[a-z][a-z0-9_-]*(?=$|[\t ])/u.test(line);
26803
+ }
26804
+ /**
26805
+ * The submission payload for one composer line. Trim is a blank check, not a
26806
+ * rewrite: an ordinary prompt keeps its exact leading indentation, inner
26807
+ * layout, and trailing spaces (pasted code must reach the model verbatim).
26808
+ * Only trailing line terminators are stripped — a draft's final newline is a
26809
+ * paste/Enter artifact (an open bracketed paste turns Enter into an inserted
26810
+ * newline), never deliberate content. A syntactic slash line still normalizes
26811
+ * fully so command routing stays stable (completion inserts a trailing space
26812
+ * after `/name`).
26813
+ * @param line - the complete draft text.
26814
+ * @returns the text to submit verbatim.
26815
+ */
26816
+ function submissionPayload(line) {
26817
+ const withoutTrailingNewlines = line.replace(/[\r\n]+$/u, "");
26818
+ const trimmed = withoutTrailingNewlines.trim();
26819
+ return isSlashLine(trimmed) ? trimmed : withoutTrailingNewlines;
26820
+ }
26821
+ //#endregion
26822
+ //#region src/provider-settings.ts
26823
+ /** Human text for a rejection value (mirrors the web page's `messageOf`). */
26824
+ function messageOf$1(error) {
26825
+ return error instanceof Error ? error.message : String(error);
26826
+ }
26827
+ /** Collapse every whitespace/control run to one space so a notice stays one line. */
26828
+ function singleLine$1(message) {
26829
+ return message.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim();
26830
+ }
26831
+ /** Keep a misbehaving credential provider from reflecting the submitted secret. */
26832
+ function credentialWriteMessage(error, secret) {
26833
+ const message = singleLine$1(messageOf$1(error));
26834
+ return message.includes(secret) ? "credentials service rejected the API key" : message;
26835
+ }
26836
+ /** Obvious shell-assignment paste; mirrors the official Web Models editor. */
26837
+ const ENV_ASSIGNMENT = /^[A-Z][A-Z0-9_]*=[^=]/;
26838
+ /** Whether the whole draft is wrapped in one matching quote pair. */
26839
+ function hasWrappingQuotes(value) {
26840
+ const first = value[0];
26841
+ return (first === "\"" || first === "'" || first === "`") && value.length > 1 && value.endsWith(first);
26842
+ }
26843
+ /** Read the value at a path through plain objects; undefined when any segment misses. */
26844
+ function getPath$1(value, path) {
26845
+ let current = value;
26846
+ for (const segment of path) {
26847
+ if (typeof current !== "object" || current === null) return void 0;
26848
+ current = current[segment];
26849
+ }
26850
+ return current;
26851
+ }
26852
+ /** Whether a path resolves to a defined value (the empty path reads the root). */
26853
+ function hasPath(value, path) {
26854
+ return path.length === 0 ? value !== void 0 : getPath$1(value, path) !== void 0;
26855
+ }
26856
+ /** The credential reference a resolved profile names (its `apiKeyEnv` field). */
26857
+ function profileRefOf(profile) {
26858
+ if (typeof profile !== "object" || profile === null) return void 0;
26859
+ const ref = profile.apiKeyEnv;
26860
+ return typeof ref === "string" && ref.length > 0 ? ref : void 0;
26861
+ }
26862
+ /** Extract only fields the terminal can round-trip without touching provider-specific extras. */
26863
+ function configurationOf(profile) {
26864
+ if (typeof profile !== "object" || profile === null) return { models: [] };
26865
+ const record = profile;
26866
+ const models = (Array.isArray(record.models) ? record.models : []).flatMap((value) => {
26867
+ if (typeof value !== "object" || value === null) return [];
26868
+ const entry = value;
26869
+ if (typeof entry.id !== "string" || entry.id.trim() === "") return [];
26870
+ const { id: _id, name: _name, contextWindow: _contextWindow, maxTokens: _maxTokens, ...extras } = entry;
26871
+ return [{
26872
+ id: entry.id,
26873
+ ...typeof entry.name === "string" && entry.name.trim() !== "" ? { name: entry.name } : {},
26874
+ ...typeof entry.contextWindow === "number" && Number.isFinite(entry.contextWindow) ? { contextWindow: entry.contextWindow } : {},
26875
+ ...typeof entry.maxTokens === "number" && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {},
26876
+ ...Object.keys(extras).length > 0 ? { extras } : {}
26877
+ }];
26878
+ });
26879
+ return {
26880
+ ...typeof record.baseURL === "string" && record.baseURL.trim() !== "" ? { baseURL: record.baseURL } : {},
26881
+ ...typeof record.api === "string" && record.api.trim() !== "" ? { api: record.api } : {},
26882
+ models
26883
+ };
26884
+ }
26885
+ /**
26886
+ * The conventional credential reference for a provider route: `<ROUTE>_API_KEY`
26887
+ * with the route uppercased and every non-alphanumeric run collapsed to one
26888
+ * underscore — the exact derivation the official Models page uses
26889
+ * (`deriveKeyRef` in `ui-settings-models`), so a key saved here is found there.
26890
+ * @param provider - provider route id (e.g. `pi-ai`, `minimax-cn`).
26891
+ * @returns the derived reference name (e.g. `PI_AI_API_KEY`).
26892
+ */
26893
+ function deriveCredentialRef(provider) {
26894
+ return `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
26895
+ }
26896
+ /**
26897
+ * The seven canonical reasoning levels a reasoningEfforts key may name -
26898
+ * pi-ai's THINKING_LEVELS. A pi-ai upgrade that adds or removes one fails
26899
+ * upstream's own drift gate; this mirror exists so the terminal editor can
26900
+ * validate drafts without importing the pi-ai package.
26901
+ */
26902
+ const REASONING_EFFORT_LEVELS = [
26903
+ "off",
26904
+ "minimal",
26905
+ "low",
26906
+ "medium",
26907
+ "high",
26908
+ "xhigh",
26909
+ "max"
26910
+ ];
26911
+ /** Whether a raw extras value is a declared efforts dict (non-empty, non-false). */
26912
+ function isDeclaredReasoningEfforts(value) {
26913
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
26914
+ }
26915
+ /**
26916
+ * Parse the setup page's compact efforts draft into a storable declaration.
26917
+ * Grammar: empty = clear back to inherit; the single token "false" = disable
26918
+ * the picker; otherwise space-separated level:wire pairs where level is one
26919
+ * of REASONING_EFFORT_LEVELS and wire is any non-empty string or the literal
26920
+ * "null" (send no parameter).
26921
+ */
26922
+ function parseReasoningEffortsDraft(draft) {
26923
+ const text = draft.trim();
26924
+ if (text === "") return {
26925
+ ok: true,
26926
+ value: void 0
26927
+ };
26928
+ if (text === "false") return {
26929
+ ok: true,
26930
+ value: false
26931
+ };
26932
+ const value = {};
26933
+ for (const token of text.split(/\s+/u)) {
26934
+ const split = token.indexOf(":");
26935
+ if (split <= 0 || split === token.length - 1) return {
26936
+ ok: false,
26937
+ error: "each entry needs level:wire, got \"" + token + "\""
26938
+ };
26939
+ const level = token.slice(0, split);
26940
+ const wire = token.slice(split + 1);
26941
+ if (!REASONING_EFFORT_LEVELS.includes(level)) return {
26942
+ ok: false,
26943
+ error: "\"" + level + "\" is not a level; use one of " + REASONING_EFFORT_LEVELS.join("/")
26944
+ };
26945
+ if (level in value) return {
26946
+ ok: false,
26947
+ error: "level \"" + level + "\" appears twice"
26948
+ };
26949
+ value[level] = wire === "null" ? null : wire;
26950
+ }
26951
+ return {
26952
+ ok: true,
26953
+ value
26954
+ };
26955
+ }
26956
+ /** Serialize a stored declaration back to the compact draft form (stored key order preserved). */
26957
+ function serializeReasoningEfforts(value) {
26958
+ if (value === false) return "false";
26959
+ if (!isDeclaredReasoningEfforts(value)) return "";
26960
+ return Object.entries(value).map(([level, wire]) => level + ":" + (wire === null ? "null" : String(wire))).join(" ");
26961
+ }
26962
+ /** Events that invalidate the official Models provider/settings/credential join. */
26963
+ const PROVIDER_SETTINGS_EVENTS = [
26964
+ "credentials/reference-updated",
26965
+ "settings/document-updated",
26966
+ "llm/adapters-updated"
26967
+ ];
26968
+ /** Subscribe to the same provider-directory invalidations as the official Web Models page. */
26969
+ function subscribeProviderSettings(ctx, listener) {
26970
+ const events = ctx;
26971
+ const disposers = PROVIDER_SETTINGS_EVENTS.map((event) => events.on(event, () => listener()));
26972
+ return () => {
26973
+ for (const dispose of disposers) dispose();
26974
+ };
26975
+ }
26976
+ /** A single-line, bounded error from the provider-management adapter. */
26977
+ var ProviderSettingsError = class extends Error {
26978
+ constructor(message) {
26979
+ super(message);
26980
+ this.name = "ProviderSettingsError";
26981
+ }
26982
+ };
26983
+ /**
26984
+ * Join the configurable-provider directory, the redacted settings
26985
+ * namespaces, and the referenced credentials into panel rows, web-parity:
26986
+ * - directory entries merge with `listProviders()` to mark each live or
26987
+ * dormant, and routes registered without a directory declaration appear as
26988
+ * read-only/unmanaged rows (no settings address);
26989
+ * - a whole-section entry is configured whenever its namespace resolves;
26990
+ * a path-addressed one only when the profile resolves there;
26991
+ * - a row is removable when the user layer alone carries its profile;
26992
+ * - only refs named by resolved profiles are described, and a per-ref failure
26993
+ * degrades to that row's bounded error instead of losing it.
26994
+ * Absent `settings`/`credentials` services are tolerated the same way.
26995
+ * @param ctx - context carrying the `llm` service (settings/credentials optional).
26996
+ * @returns the resolved directory; empty rows when `llm` is unavailable.
26997
+ */
26998
+ async function loadProviderSettings(ctx) {
26999
+ const llm = ctx.get("llm");
27000
+ if (llm === void 0) return {
27001
+ rows: [],
27002
+ writable: false,
27003
+ failures: []
27004
+ };
27005
+ const registered = llm.listProviders();
27006
+ const failures = [];
27007
+ const directoryEntries = [];
27008
+ if (llm.listConfigurableProviders !== void 0) try {
27009
+ directoryEntries.push(...llm.listConfigurableProviders());
27010
+ } catch (error) {
27011
+ failures.push(`configurable-provider directory failed: ${singleLine$1(messageOf$1(error))}`);
27012
+ }
27013
+ const settings = ctx.get("settings");
27014
+ let descriptors = [];
27015
+ let writable = false;
27016
+ if (settings !== void 0) try {
27017
+ descriptors = settings.describe({ redactSecrets: true });
27018
+ writable = settings.writable === true;
27019
+ } catch (error) {
27020
+ failures.push(`settings describe failed: ${singleLine$1(messageOf$1(error))}`);
27021
+ }
27022
+ const namespaces = new Map(descriptors.map((descriptor) => [descriptor.ns, descriptor]));
27023
+ const active = new Set(registered.map((provider) => provider.id));
27024
+ const declared = new Set(directoryEntries.map((entry) => entry.provider));
27025
+ const rows = [...directoryEntries.map((entry) => ({
27026
+ provider: entry.provider,
27027
+ displayName: entry.displayName,
27028
+ active: active.has(entry.provider),
27029
+ settingsNs: entry.settingsNs,
27030
+ settingsPath: entry.settingsPath,
27031
+ ...entry.declared === void 0 ? {} : { declared: entry.declared }
27032
+ })), ...registered.filter((provider) => !declared.has(provider.id)).map((provider) => ({
27033
+ provider: provider.id,
27034
+ displayName: provider.name,
27035
+ active: true,
27036
+ settingsNs: "",
27037
+ settingsPath: []
27038
+ }))].map((base) => {
27039
+ const namespace = base.settingsNs.length === 0 ? void 0 : namespaces.get(base.settingsNs);
27040
+ const profile = namespace === void 0 ? void 0 : base.settingsPath.length === 0 ? namespace.value : getPath$1(namespace.value, base.settingsPath);
27041
+ const configured = namespace !== void 0 && (base.settingsPath.length === 0 || profile !== void 0);
27042
+ const removable = namespace !== void 0 && base.settingsPath.length > 0 && hasPath(namespace.user, base.settingsPath) && !hasPath(namespace.base, base.settingsPath);
27043
+ const credentialRef = profileRefOf(profile);
27044
+ return {
27045
+ provider: base.provider,
27046
+ displayName: base.displayName,
27047
+ active: base.active,
27048
+ settingsNs: base.settingsNs,
27049
+ settingsPath: base.settingsPath,
27050
+ settingsRevision: namespace?.revision ?? 0,
27051
+ configured,
27052
+ removable,
27053
+ configuration: configurationOf(profile),
27054
+ ...credentialRef === void 0 ? {} : { credentialRef },
27055
+ suggestedRef: deriveCredentialRef(base.provider),
27056
+ ...base.declared === void 0 ? {} : { declared: base.declared }
27057
+ };
27058
+ });
27059
+ const refs = [...new Set(rows.flatMap((row) => row.credentialRef === void 0 ? [] : [row.credentialRef]))];
27060
+ const credentialViews = /* @__PURE__ */ new Map();
27061
+ const credentials = ctx.get("credentials");
27062
+ if (refs.length > 0) {
27063
+ if (credentials === void 0) for (const ref of refs) credentialViews.set(ref, {
27064
+ kind: "error",
27065
+ message: "credentials service is unavailable"
27066
+ });
27067
+ else await Promise.all(refs.map(async (ref) => {
27068
+ try {
27069
+ const facts = await credentials.describe(ref);
27070
+ credentialViews.set(ref, {
27071
+ kind: "facts",
27072
+ configured: facts.configured,
27073
+ writable: facts.writable,
27074
+ ...facts.source === void 0 ? {} : { source: facts.source }
27075
+ });
27076
+ } catch (error) {
27077
+ credentialViews.set(ref, {
27078
+ kind: "error",
27079
+ message: singleLine$1(messageOf$1(error))
27080
+ });
27081
+ }
27082
+ }));
27083
+ }
27084
+ return {
27085
+ rows: rows.map((row) => ({
27086
+ ...row,
27087
+ credential: row.credentialRef === void 0 ? void 0 : credentialViews.get(row.credentialRef) ?? {
27088
+ kind: "error",
27089
+ message: "credential describe returned no view"
27090
+ }
27091
+ })),
27092
+ writable,
27093
+ failures
27094
+ };
27095
+ }
27096
+ /**
27097
+ * Store a provider API key, web-parity: validate with `normalizeApiKey`
27098
+ * (single-line, actionable errors that never echo the key), materialize the
27099
+ * profile/`apiKeyEnv` through `settings.mutate` first when the resolved
27100
+ * profile names no reference (dormant route or ref-less profile), then store
27101
+ * under the trusted named ref or the derived conventional ref. An existing
27102
+ * whole-section DeepSeek whose resolved profile already names
27103
+ * `DEEPSEEK_API_KEY` needs no settings mutation. Env-supplied read-only keys
27104
+ * are refused before any service call.
27105
+ * @param ctx - context carrying `settings` (when materializing) and `credentials`.
27106
+ * @param target - the joined row to write through.
27107
+ * @param rawKey - the key exactly as typed; surrounding whitespace is trimmed.
27108
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
27109
+ */
27110
+ async function saveProviderCredential(ctx, target, rawKey) {
27111
+ const trimmed = rawKey.trim();
27112
+ if (ENV_ASSIGNMENT.test(trimmed) || hasWrappingQuotes(trimmed)) throw new ProviderSettingsError("paste only the API key, without an environment-variable name or wrapping quotes");
27113
+ const checked = normalizeApiKey(rawKey);
27114
+ if (!checked.ok) throw new ProviderSettingsError(checked.reason === "empty" ? "the API key is empty after trimming surrounding whitespace" : "the API key contains characters an HTTP header cannot carry; type a plain printable-ASCII key");
27115
+ if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
27116
+ if (target.credential?.kind === "facts" && target.credential.writable === false) throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead of overwriting it here`);
27117
+ const credentials = ctx.get("credentials");
27118
+ if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot store the API key");
27119
+ const ref = target.credentialRef ?? deriveCredentialRef(target.provider);
27120
+ if (target.credentialRef === void 0) {
27121
+ const settings = ctx.get("settings");
27122
+ if (settings === void 0) throw new ProviderSettingsError("settings service is unavailable; cannot materialize the credential reference");
27123
+ try {
27124
+ await settings.mutate(target.settingsNs, [{
27125
+ op: "set",
27126
+ path: [...target.settingsPath, "apiKeyEnv"],
27127
+ value: ref
27128
+ }]);
27129
+ } catch (error) {
27130
+ throw new ProviderSettingsError(singleLine$1(messageOf$1(error)));
27131
+ }
27132
+ }
27133
+ try {
27134
+ await credentials.set(ref, checked.value);
27135
+ } catch (error) {
27136
+ throw new ProviderSettingsError(credentialWriteMessage(error, checked.value));
27137
+ }
27138
+ }
27139
+ /** Save the endpoint and an explicit model allow-list without rebuilding the profile. */
27140
+ async function saveProviderConfiguration(ctx, target, configuration) {
27141
+ if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
27142
+ const settings = ctx.get("settings");
27143
+ if (settings === void 0 || settings.writable !== true) throw new ProviderSettingsError("settings are read-only; provider configuration cannot be changed here");
27144
+ const baseURL = configuration.baseURL?.trim();
27145
+ if (baseURL !== void 0 && baseURL !== "") try {
27146
+ const parsed = new URL(baseURL);
27147
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
27148
+ } catch {
27149
+ throw new ProviderSettingsError("base URL must be an absolute http or https URL");
27150
+ }
27151
+ const seen = /* @__PURE__ */ new Set();
27152
+ const models = configuration.models.map((model) => {
27153
+ const id = model.id.trim();
27154
+ if (id === "" || seen.has(id)) throw new ProviderSettingsError("each selected model must have a unique non-empty id");
27155
+ seen.add(id);
27156
+ for (const [label, value] of [["context window", model.contextWindow], ["output window", model.maxTokens]]) if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new ProviderSettingsError(`${label} must be a positive integer`);
27157
+ return {
27158
+ id,
27159
+ ...model.extras,
27160
+ ...model.name === void 0 || model.name.trim() === "" ? {} : { name: model.name.trim() },
27161
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
27162
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
27163
+ };
27164
+ });
27165
+ const root = target.settingsPath;
27166
+ const ops = [baseURL === void 0 || baseURL === "" ? {
27167
+ op: "unset",
27168
+ path: [...root, "baseURL"]
27169
+ } : {
27170
+ op: "set",
27171
+ path: [...root, "baseURL"],
27172
+ value: baseURL
27173
+ }, {
27174
+ op: "set",
27175
+ path: [...root, "models"],
27176
+ value: models
27177
+ }];
27178
+ try {
27179
+ await settings.mutate(target.settingsNs, ops, target.settingsRevision);
27180
+ } catch (error) {
27181
+ throw new ProviderSettingsError(singleLine$1(messageOf$1(error)));
27182
+ }
27183
+ }
27184
+ /**
27185
+ * Interrogate a provider endpoint for the models it really serves, through
27186
+ * the model-discovery capability the provider's settings namespace
27187
+ * registered — the same pipe the official Web Models page uses. The request
27188
+ * is a draft: a typed key forces direct endpoint interrogation (gateway
27189
+ * truth), while an empty key lets the harness resolve the route's stored
27190
+ * credential; with neither baseURL nor route the adapter answers from its
27191
+ * own knowledge.
27192
+ * @param ctx - context carrying the `llm` service (optional discovery).
27193
+ * @param target - provider row whose settings namespace serves the draft.
27194
+ * @param request - typed key and/or endpoint override for this one probe.
27195
+ * @param signal - caller cancellation (panel navigation aborts the probe).
27196
+ * @returns the advertised models in endpoint order, deduplicated.
27197
+ */
27198
+ async function discoverProviderModels(ctx, target, request, signal) {
27199
+ if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; its models cannot be discovered here`);
27200
+ const llm = ctx.get("llm");
27201
+ if (llm?.discoverModels === void 0) throw new ProviderSettingsError("model discovery is unavailable in this profile; enter models by hand");
27202
+ const typedKey = request.apiKey?.trim();
27203
+ const baseURL = request.baseURL?.trim();
27204
+ const hasUrl = baseURL !== void 0 && baseURL !== "";
27205
+ let oneShotKey = typedKey;
27206
+ if (hasUrl && (oneShotKey === void 0 || oneShotKey === "")) {
27207
+ const credentialsService = ctx.get("credentials");
27208
+ const ref = target.credentialRef ?? target.suggestedRef;
27209
+ if (credentialsService?.resolve !== void 0 && ref !== void 0) try {
27210
+ oneShotKey = (await credentialsService.resolve(ref))?.value;
27211
+ } catch {}
27212
+ }
27213
+ const draft = hasUrl ? {
27214
+ ...oneShotKey !== void 0 && oneShotKey !== "" ? { apiKey: oneShotKey } : {},
27215
+ baseURL,
27216
+ ...target.configuration.api === void 0 ? {} : { api: target.configuration.api }
27217
+ } : { provider: target.provider };
27218
+ try {
27219
+ const discovered = signal === void 0 ? await llm.discoverModels(target.settingsNs, draft) : await llm.discoverModels(target.settingsNs, draft, signal);
27220
+ const seen = /* @__PURE__ */ new Set();
27221
+ const rows = [];
27222
+ for (const model of discovered) {
27223
+ if (typeof model.id !== "string" || model.id.trim() === "" || seen.has(model.id)) continue;
27224
+ seen.add(model.id);
27225
+ rows.push({
27226
+ id: model.id,
27227
+ ...model.name === void 0 ? {} : { name: model.name },
27228
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
27229
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
27230
+ });
27231
+ }
27232
+ return rows;
27233
+ } catch (error) {
27234
+ if (signal?.aborted === true) throw error;
27235
+ throw new ProviderSettingsError(singleLine$1(messageOf$1(error)));
27236
+ }
27237
+ }
27238
+ /**
27239
+ * Remove the currently named credential without touching the provider
27240
+ * profile. Only the resolved profile's own reference is unset; a dormant or
27241
+ * ref-less row (nothing to remove), an already-absent key, and an
27242
+ * env-supplied read-only key are rejected safely before any service call.
27243
+ * @param ctx - context carrying the `credentials` service.
27244
+ * @param target - the joined row whose named credential to unset.
27245
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
27246
+ */
27247
+ async function unsetProviderCredential(ctx, target) {
27248
+ const ref = target.credentialRef;
27249
+ if (ref === void 0) throw new ProviderSettingsError(`provider "${target.provider}" names no credential reference to remove`);
27250
+ const facts = target.credential;
27251
+ if (facts?.kind === "facts" && facts.configured === false) throw new ProviderSettingsError(`provider "${target.provider}" has no configured credential to remove`);
27252
+ if (facts?.kind === "facts" && facts.writable === false) throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead`);
27253
+ const credentials = ctx.get("credentials");
27254
+ if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot remove the API key");
27255
+ try {
27256
+ await credentials.unset(ref);
27257
+ } catch (error) {
27258
+ throw new ProviderSettingsError(singleLine$1(messageOf$1(error)));
27259
+ }
27260
+ }
27261
+ /**
27262
+ * Remove a user-added provider profile, web-parity: only `removable` rows may
27263
+ * be removed; a page-managed credential — the derived ref, configured and
27264
+ * writable — is unset first (so a second-step failure leaves the row visible
27265
+ * and the operation retryable), then `settings.mutate` unsets
27266
+ * `target.settingsPath`. Both steps are idempotent. A hand-named credential
27267
+ * ref may be shared elsewhere and is left alone.
27268
+ * @param ctx - context carrying `credentials` and `settings`.
27269
+ * @param target - the joined row to remove.
27270
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
27271
+ */
27272
+ async function removeProviderSettings(ctx, target) {
27273
+ if (!target.removable) throw new ProviderSettingsError(`provider "${target.provider}" is not removable from the user settings layer`);
27274
+ if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings profile to remove`);
27275
+ const managedRef = target.credentialRef === target.suggestedRef && target.credential?.kind === "facts" && target.credential.configured === true && target.credential.writable === true ? target.credentialRef : void 0;
27276
+ if (managedRef !== void 0) {
27277
+ const credentials = ctx.get("credentials");
27278
+ if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot remove the managed API key");
27279
+ try {
27280
+ await credentials.unset(managedRef);
27281
+ } catch (error) {
27282
+ throw new ProviderSettingsError(singleLine$1(messageOf$1(error)));
27283
+ }
27284
+ }
27285
+ const settings = ctx.get("settings");
27286
+ if (settings === void 0) throw new ProviderSettingsError("settings service is unavailable; cannot remove the provider profile");
27287
+ try {
27288
+ await settings.mutate(target.settingsNs, [{
27289
+ op: "unset",
27290
+ path: [...target.settingsPath]
27291
+ }]);
27292
+ } catch (error) {
27293
+ throw new ProviderSettingsError(singleLine$1(messageOf$1(error)));
27294
+ }
27295
+ }
27296
+ //#endregion
26716
27297
  //#region src/mentions.ts
26717
27298
  /** Menu cap on file rows; the service owns ranking and default rows. */
26718
27299
  const MAX_FILE_ROWS = 20;
@@ -26741,19 +27322,36 @@ function createMentions(ctx, agent, cwd) {
26741
27322
  const resolver = ctx.get("sessionReferenceResolver");
26742
27323
  const fileReferences = ctx.get("fileReferences");
26743
27324
  const sessionCapable = agent !== void 0 && resolver !== void 0;
27325
+ const PRE_SESSION_INDEX_TTL_MS = 3e4;
26744
27326
  let preSessionSearch;
27327
+ let preSessionIndexedAt = 0;
26745
27328
  const preSessionFiles = (query, signal) => {
26746
- preSessionSearch ??= new WorkspaceFileSearch(cwd, {
26747
- maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
26748
- maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
26749
- excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
26750
- });
27329
+ if (preSessionSearch === void 0 || Date.now() - preSessionIndexedAt > PRE_SESSION_INDEX_TTL_MS) {
27330
+ preSessionSearch?.invalidate();
27331
+ preSessionSearch = new WorkspaceFileSearch(cwd, {
27332
+ maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
27333
+ maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
27334
+ excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
27335
+ });
27336
+ preSessionIndexedAt = Date.now();
27337
+ }
26751
27338
  return preSessionSearch.list(query, signal ?? new AbortController().signal);
26752
27339
  };
26753
27340
  return {
26754
27341
  async candidates(query, signal) {
26755
27342
  const needle = query.trim();
26756
- 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([])]);
27343
+ let fileFailure;
27344
+ let sessionFailure;
27345
+ const [files, sessions] = await Promise.all([agent !== void 0 && fileReferences !== void 0 ? fileReferences.list(agent, needle, signal ?? new AbortController().signal).catch((error) => {
27346
+ fileFailure = error;
27347
+ return [];
27348
+ }) : agent === void 0 ? preSessionFiles(needle, signal).catch((error) => {
27349
+ fileFailure = error;
27350
+ return [];
27351
+ }) : Promise.resolve([]), sessionCapable && needle !== "" && !isPathLikeMentionQuery(needle) && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch((error) => {
27352
+ sessionFailure = error;
27353
+ return [];
27354
+ }) : Promise.resolve([])]);
26757
27355
  const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
26758
27356
  label: candidate.path,
26759
27357
  description: candidate.kind === "directory" ? "Folder" : "File",
@@ -26765,7 +27363,9 @@ function createMentions(ctx, agent, cwd) {
26765
27363
  description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
26766
27364
  kind: "session"
26767
27365
  }));
26768
- return [...fileRows, ...sessionRows];
27366
+ const rows = [...fileRows, ...sessionRows];
27367
+ if (rows.length === 0 && (fileFailure !== void 0 || sessionFailure !== void 0)) throw fileFailure instanceof Error ? fileFailure : sessionFailure instanceof Error ? sessionFailure : new Error(String(fileFailure ?? sessionFailure));
27368
+ return rows;
26769
27369
  },
26770
27370
  parse(text) {
26771
27371
  return parseSessionReferenceText(text);
@@ -27075,53 +27675,62 @@ const STATUS_ITEM_SEPARATOR = " · ";
27075
27675
  const STATUS_CYCLE_HINT = " (shift+tab to cycle)";
27076
27676
  /** Occupancy at which the usage readout flips from brand blue to amber. */
27077
27677
  const CONTEXT_WARN_PERCENT = 90;
27078
- /** Free-tail floor in columns: wide enough for the bare percent readout, so
27079
- * the warning stays visible even at 100%+ occupancy. */
27080
- const CONTEXT_MIN_FREE = 5;
27081
27678
  /**
27082
- * Narrowest bar width the drop ladder tries before giving up on the context
27083
- * group: the bar shrinks inside its own budget first (a few columns still
27084
- * show the bare percent readout) and only then drops as a whole.
27679
+ * Narrowest bar width the drop ladder keeps before dropping the whole
27680
+ * context group: the bar shrinks to this floor first (the absolute readout
27681
+ * survives), and only past it does the readout degrade and the group go.
27085
27682
  */
27086
27683
  const CONTEXT_MIN_WIDTH = 5;
27087
27684
  /**
27088
- * Render context occupancy as ONE stepless bar: a solid DeepSeek-blue fill
27089
- * run, a dim dotted free track, and the usage readout riding the track's
27090
- * right edge (`12.3K/1.0M 25%`, shrinking to the bare percent as the track
27091
- * narrows). No per-content-type segmentation. Column split is deterministic:
27092
- * the free share is `Math.round(free/window*width)` clamped to at least
27093
- * CONTEXT_MIN_FREE columns and at most the full width; the fill takes every
27094
- * remaining column, so a given occupancy always renders the identical bar.
27095
- * The readout flips to amber once occupancy reaches the warning threshold.
27096
- * @param usedTokens - reported used tokens (drives the readout and percent).
27685
+ * Render context occupancy as ONE stepless proportional bar: a solid
27686
+ * DeepSeek-blue fill run tracking the occupancy and a dim dotted free
27687
+ * track for the rest. Nothing else lives inside the bar the usage
27688
+ * readout rides outside it (see contextGroupSpans) so the geometry
27689
+ * always reads as the true remaining share. A given occupancy always
27690
+ * renders the identical bar.
27691
+ * @param usedTokens - reported used tokens.
27097
27692
  * @param contextWindow - route capacity.
27098
- * @param width - total bar interior columns.
27693
+ * @param width - total bar columns.
27099
27694
  * @returns tone-split spans for the footer to paint.
27100
27695
  */
27101
27696
  function contextBar(usedTokens, contextWindow, width) {
27102
27697
  if (width <= 0 || contextWindow <= 0) return [];
27103
- const used = Math.max(0, usedTokens);
27104
- const percent = Math.round(used / contextWindow * 100);
27105
- const readoutTone = percent >= CONTEXT_WARN_PERCENT ? "warn" : "value";
27106
- const freeShare = Math.round(Math.max(0, contextWindow - used) / contextWindow * width);
27107
- const freeColumns = Math.min(width, Math.max(freeShare, CONTEXT_MIN_FREE));
27108
- const usedColumns = Math.max(0, width - freeColumns);
27109
- const total = `${formatTokens(used)}/${formatTokens(contextWindow)}`;
27110
- const percentText = `${percent}%`;
27111
- const readout = freeColumns >= visibleColumns(`${total} ${percentText}`) ? `${total} ${percentText}` : freeColumns >= visibleColumns(percentText) ? percentText : "";
27698
+ const fill = Math.min(width, Math.max(0, Math.round(Math.max(0, usedTokens) / contextWindow * width)));
27112
27699
  const spans = [];
27113
- if (usedColumns > 0) spans.push({
27114
- text: "█".repeat(usedColumns),
27700
+ if (fill > 0) spans.push({
27701
+ text: "█".repeat(fill),
27115
27702
  tone: "ctxFill"
27116
27703
  });
27117
- const pad = freeColumns - visibleColumns(readout);
27118
- if (pad > 0) spans.push({
27119
- text: "░".repeat(pad),
27704
+ const free = width - fill;
27705
+ if (free > 0) spans.push({
27706
+ text: "░".repeat(free),
27120
27707
  tone: "label"
27121
27708
  });
27122
- if (readout !== "") spans.push({
27123
- text: readout,
27124
- tone: readoutTone
27709
+ return spans;
27710
+ }
27711
+ /**
27712
+ * Compose the context group: the proportional bar plus the usage readout
27713
+ * OUTSIDE the bar, so the dotted track keeps its proportional meaning no
27714
+ * matter how wide the readout is. `full` reads `12.3K/1.0M 25%`; `percent`
27715
+ * drops the absolute pair; `none` is the bare bar. The readout turns amber
27716
+ * once occupancy reaches the warning threshold.
27717
+ */
27718
+ function contextGroupSpans(usedTokens, contextWindow, barWidth, readout) {
27719
+ const spans = [{
27720
+ text: "context ",
27721
+ tone: "label"
27722
+ }];
27723
+ spans.push(...contextBar(usedTokens, contextWindow, barWidth));
27724
+ if (readout === "none" || barWidth <= 0 || contextWindow <= 0) return spans;
27725
+ const used = Math.max(0, usedTokens);
27726
+ const percent = Math.round(used / contextWindow * 100);
27727
+ const text = readout === "full" ? `${formatTokens(used)}/${formatTokens(contextWindow)} ${percent}%` : `${percent}%`;
27728
+ spans.push({
27729
+ text: " ",
27730
+ tone: "label"
27731
+ }, {
27732
+ text,
27733
+ tone: percent >= CONTEXT_WARN_PERCENT ? "warn" : "value"
27125
27734
  });
27126
27735
  return spans;
27127
27736
  }
@@ -27413,10 +28022,7 @@ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
27413
28022
  id: "cache"
27414
28023
  });
27415
28024
  if (stats.contextWindow > 0 && stats.lastPromptTokens > 0 && enabled.has("context")) left.push({
27416
- group: { spans: [{
27417
- text: "context ",
27418
- tone: "label"
27419
- }, ...contextBar(stats.lastPromptTokens, stats.contextWindow, contextWidth)] },
28025
+ group: { spans: contextGroupSpans(stats.lastPromptTokens, stats.contextWindow, contextWidth, "full") },
27420
28026
  rank: RANK_CONTEXT,
27421
28027
  id: "context"
27422
28028
  });
@@ -27517,15 +28123,13 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
27517
28123
  let hint = badge >= 0 && !busy;
27518
28124
  const leftKept = [...orderedLeft];
27519
28125
  const rightKept = [...orderedRight];
28126
+ let contextReadout = "full";
27520
28127
  let contextWidth = maxContextWidth;
27521
28128
  const rebuildContext = () => {
27522
28129
  const index = leftKept.findIndex((entry) => entry.id === "context");
27523
28130
  if (index < 0) return;
27524
28131
  leftKept[index] = {
27525
- group: { spans: [{
27526
- text: "context ",
27527
- tone: "label"
27528
- }, ...contextBar(stats.lastPromptTokens, stats.contextWindow, contextWidth)] },
28132
+ group: { spans: contextGroupSpans(stats.lastPromptTokens, stats.contextWindow, contextWidth, contextReadout) },
27529
28133
  rank: RANK_CONTEXT,
27530
28134
  id: "context"
27531
28135
  };
@@ -27536,15 +28140,19 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
27536
28140
  return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth;
27537
28141
  };
27538
28142
  while (width() > budget) {
27539
- if (leftKept.some((entry) => entry.id === "context") && contextWidth > CONTEXT_MIN_WIDTH) {
27540
- const overflow = width() - budget;
27541
- contextWidth = Math.max(CONTEXT_MIN_WIDTH, contextWidth - overflow);
27542
- rebuildContext();
27543
- continue;
27544
- }
27545
- const contextIndex = leftKept.findIndex((entry) => entry.id === "context");
27546
- if (contextIndex >= 0) {
27547
- leftKept.splice(contextIndex, 1);
28143
+ if (leftKept.some((entry) => entry.id === "context")) {
28144
+ if (contextWidth > CONTEXT_MIN_WIDTH) {
28145
+ const overflow = width() - budget;
28146
+ contextWidth = Math.max(CONTEXT_MIN_WIDTH, contextWidth - overflow);
28147
+ rebuildContext();
28148
+ continue;
28149
+ }
28150
+ if (contextReadout === "full") {
28151
+ contextReadout = "percent";
28152
+ rebuildContext();
28153
+ continue;
28154
+ }
28155
+ leftKept.splice(leftKept.findIndex((entry) => entry.id === "context"), 1);
27548
28156
  continue;
27549
28157
  }
27550
28158
  if (hint && rightKept.length > 0 && leftKept.length > 0) {
@@ -27672,14 +28280,14 @@ function styledLines(segments, columns) {
27672
28280
  };
27673
28281
  for (const segment of segments) {
27674
28282
  const safe = displayText(segment.text).replaceAll(" ", " ").replaceAll("\r", "");
27675
- for (const char of safe) {
27676
- if (char === "\n") {
28283
+ for (const cluster of splitGraphemes$1(safe)) {
28284
+ if (cluster === "\n") {
27677
28285
  flush();
27678
28286
  continue;
27679
28287
  }
27680
- const cells = visibleColumns(char);
28288
+ const cells = graphemeWidth(cluster);
27681
28289
  if (used > 0 && used + cells > width) flush();
27682
- appendSegment(current, char, segment.style);
28290
+ appendSegment(current, cluster, segment.style);
27683
28291
  used += cells;
27684
28292
  }
27685
28293
  }
@@ -27693,7 +28301,7 @@ function textLines(text, columns, style = "plain") {
27693
28301
  /** Prefix every wrapped physical row without exceeding the column budget. */
27694
28302
  function prefixedStyledLines(segments, columns, prefix, prefixStyle = "plain") {
27695
28303
  const width = Math.max(1, Math.floor(columns));
27696
- const prefixWidth = Math.min(width, visibleColumns(prefix));
28304
+ const prefixWidth = Math.min(width - 1, visibleColumns(prefix));
27697
28305
  return styledLines(segments, Math.max(1, width - prefixWidth)).map((line) => ({ segments: [lineSegment(prefix, prefixStyle), ...line.segments] }));
27698
28306
  }
27699
28307
  /** Text convenience for a tool row whose continuation must keep its gutter. */
@@ -27725,14 +28333,14 @@ function hangingStyledLines(segments, columns, firstPrefix, firstStyle, contPref
27725
28333
  };
27726
28334
  for (const segment of segments) {
27727
28335
  const safe = displayText(segment.text).replaceAll(" ", " ").replaceAll("\r", "");
27728
- for (const char of safe) {
27729
- if (char === "\n") {
28336
+ for (const cluster of splitGraphemes$1(safe)) {
28337
+ if (cluster === "\n") {
27730
28338
  flush();
27731
28339
  continue;
27732
28340
  }
27733
- const cells = visibleColumns(char);
28341
+ const cells = graphemeWidth(cluster);
27734
28342
  if (used > 0 && used + cells > budget) flush();
27735
- appendSegment(current, char, segment.style);
28343
+ appendSegment(current, cluster, segment.style);
27736
28344
  used += cells;
27737
28345
  }
27738
28346
  }
@@ -27746,7 +28354,7 @@ function hangingTextLines(text, columns, firstPrefix, firstStyle = "plain", cont
27746
28354
  /** Markdown rows re-hardened so a single long word cannot escape the budget. */
27747
28355
  function markdownLines(text, columns) {
27748
28356
  const width = Math.max(1, Math.floor(columns));
27749
- return renderMarkdown(displayText(text), Math.max(10, width)).flatMap((line) => styledLines(line.segments.map((segment) => lineSegment(segment.text, segment.style)), width));
28357
+ return renderMarkdown(displayText(text), width).flatMap((line) => styledLines(line.segments.map((segment) => lineSegment(segment.text, segment.style)), width));
27750
28358
  }
27751
28359
  /**
27752
28360
  * Codex-style reasoning rows: the marker occupies the reply gutter and every
@@ -27796,7 +28404,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
27796
28404
  case "pending": return hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
27797
28405
  case "assistant": {
27798
28406
  const reasoning = entry.reasoning === "" ? [] : showReasoning ? reasoningLines(entry.reasoning, width) : textLines(`✻ Thinking (${entry.reasoning.length} chars${reasoningToggleHint ? ", Ctrl/Alt+R to expand" : ""})`, width, "dim");
27799
- const body = markdownLines(entry.text, Math.max(10, width - 2)).map((line) => ({ segments: [{
28407
+ const body = markdownLines(entry.text, Math.max(1, width - 2)).map((line) => ({ segments: [{
27800
28408
  text: " ",
27801
28409
  style: "plain"
27802
28410
  }, ...line.segments] }));
@@ -27831,7 +28439,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
27831
28439
  }
27832
28440
  case "turn-marker": return textLines(` ⏹ ${entry.text}`, width, "dim");
27833
28441
  case "compaction": return textLines(entry.ok ? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens` : ` ⧉ compaction failed: ${entry.error}`, width, "dim");
27834
- case "retry": return textLines(` ↻ retry ${entry.attempt}/${entry.max} · ${entry.code} · ${Math.round(entry.delayMs / 100) / 10}s`, width, entry.state === "running" ? "warn" : "dim");
28442
+ case "retry": return textLines(entry.mode === "always" ? ` ↻ retry ${entry.attempt} · ${entry.code} · ${Math.round(entry.delayMs / 100) / 10}s` : ` ↻ retry ${entry.attempt}/${entry.max} · ${entry.code} · ${Math.round(entry.delayMs / 100) / 10}s`, width, entry.state === "running" ? "warn" : "dim");
27835
28443
  case "files": return entry.paths.length === 0 ? textLines(" ⎄ no changed files", width, "dim") : [...textLines(` ⎄ ${entry.paths.length} changed file${entry.paths.length === 1 ? "" : "s"}`, width, "dim"), ...entry.paths.flatMap((path) => hangingTextLines(path, width, " ", "dim", " "))];
27836
28444
  case "error": return textLines(entry.text, width, "error");
27837
28445
  default: return entry;
@@ -28390,6 +28998,14 @@ function stripTerminalFocusEvents(chunk, onFocus) {
28390
28998
  const PASTE_START_MARKER = "[200~";
28391
28999
  const PASTE_END_MARKER = "[201~";
28392
29000
  /**
29001
+ * How long an unterminated bracketed-paste block may hold buffered bytes
29002
+ * before the input splitter strips its start marker and releases them: a
29003
+ * terminal that loses the end marker must never take the whole keyboard
29004
+ * hostage (Esc/Ctrl+C included). Shared by the splitter and the composer's
29005
+ * lost-paste safety net so both use one window.
29006
+ */
29007
+ const PASTE_BRACKET_TIMEOUT_MS = 1e3;
29008
+ /**
28393
29009
  * Remove bracketed paste markers from one input chunk. Panel drafts accept raw
28394
29010
  * `input` text, where an unhandled paste would otherwise persist the literal
28395
29011
  * "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
@@ -28398,7 +29014,21 @@ function stripPasteMarkers(text) {
28398
29014
  return text.replaceAll(`\x1b${PASTE_START_MARKER}`, "").replaceAll(`\x1b${PASTE_END_MARKER}`, "").replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
28399
29015
  }
28400
29016
  /** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
28401
- const CSI_U_SOURCE = "\x1B\\[(\\d+)(?:;(\\d+))?(?:[:;](\\d+))?u";
29017
+ const CSI_U_SOURCE = "\x1B\\[(\\d+)(?:;(\\d+))?(?:(:|;)(\\d+))?u";
29018
+ /** Kitty private-use keycodes for the numeric keypad and keypad Enter. */
29019
+ const KITTY_KEYPAD_CODES = {
29020
+ 57399: "0",
29021
+ 57400: "1",
29022
+ 57401: "2",
29023
+ 57402: "3",
29024
+ 57403: "4",
29025
+ 57404: "5",
29026
+ 57405: "6",
29027
+ 57406: "7",
29028
+ 57407: "8",
29029
+ 57408: "9",
29030
+ 57414: "\r"
29031
+ };
28402
29032
  /** Legacy equivalent for one decoded CSI-u key, or undefined to pass through. */
28403
29033
  function legacyForKey(key) {
28404
29034
  const bits = Math.max(0, key.modifiers - 1);
@@ -28413,6 +29043,11 @@ function legacyForKey(key) {
28413
29043
  if (key.code === 27) return "\x1B";
28414
29044
  if (key.code === 9) return shift ? "\x1B[Z" : " ";
28415
29045
  if (key.code === 127) return alt || ctrl ? "\x1B" : "";
29046
+ const keypad = KITTY_KEYPAD_CODES[key.code];
29047
+ if (keypad !== void 0) {
29048
+ if (alt) return "\x1B" + keypad;
29049
+ return keypad;
29050
+ }
28416
29051
  if (key.code >= 1 && key.code <= 6) {
28417
29052
  const mask = (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
28418
29053
  const mods = mask === 0 ? "" : `;${mask + 1}`;
@@ -28432,11 +29067,11 @@ function legacyForKey(key) {
28432
29067
  if (alt) return "\x1B" + String.fromCodePoint(key.code + 32);
28433
29068
  return String.fromCodePoint(key.code);
28434
29069
  }
28435
- if (key.code >= 32 && key.code <= 126 && key.alternate !== void 0) {
28436
- const base = key.alternate >= 97 && key.alternate <= 122 ? key.alternate : key.code;
29070
+ if (key.code >= 32 && key.code <= 126) {
29071
+ const base = key.alternate ?? key.code;
28437
29072
  if (ctrl && base - 96 >= 1 && base - 96 <= 26) return String.fromCodePoint(base - 96);
28438
- if (alt) return "\x1B" + String.fromCodePoint(key.alternate);
28439
- return String.fromCodePoint(key.alternate);
29073
+ if (alt) return "\x1B" + String.fromCodePoint(base);
29074
+ return String.fromCodePoint(base);
28440
29075
  }
28441
29076
  }
28442
29077
  /**
@@ -28448,11 +29083,11 @@ function legacyForKey(key) {
28448
29083
  function normalizeKeyboardChunk(chunk) {
28449
29084
  if (!chunk.includes("\x1B[") || !chunk.includes("u")) return chunk;
28450
29085
  const pattern = new RegExp(CSI_U_SOURCE, "g");
28451
- return chunk.replace(pattern, (whole, code, mods, third) => {
29086
+ return chunk.replace(pattern, (whole, code, mods, separator, third) => {
28452
29087
  return legacyForKey({
28453
29088
  code: Number.parseInt(code, 10),
28454
29089
  modifiers: mods === void 0 || mods === "" ? 1 : Math.max(1, Number.parseInt(mods, 10)),
28455
- alternate: third !== void 0 && third !== "" ? Number.parseInt(third, 10) : void 0
29090
+ alternate: separator === ";" && third !== void 0 && third !== "" ? Number.parseInt(third, 10) : void 0
28456
29091
  }) ?? whole;
28457
29092
  });
28458
29093
  }
@@ -28565,8 +29200,10 @@ function searchLine(searching, query) {
28565
29200
  function ListFrame(props) {
28566
29201
  const stdout = useStdout().stdout;
28567
29202
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
28568
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
28569
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(singleLineText(`${props.title} · esc close`), viewport.contentColumns));
29203
+ if (viewport.maxHeight === 0 || viewport.compact) {
29204
+ const body = props.loading ? `${props.title} · loading…` : props.error !== void 0 ? `${props.title} · load failed` : props.rows.length === 0 ? `${props.title} · no matching entries` : `❯ ${singleLineText(props.rows[props.cursor]?.text ?? "")}`;
29205
+ return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(singleLineText(`${body} · esc close`), viewport.contentColumns));
29206
+ }
28570
29207
  const stateRows = props.loading ? [{
28571
29208
  key: "loading",
28572
29209
  text: " loading…"
@@ -28641,7 +29278,7 @@ function ModePanel({ current, load, select, close }) {
28641
29278
  if (input === "r" && query === "") return refresh();
28642
29279
  if (key.upArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + visible.length - 1) % visible.length);
28643
29280
  if (key.downArrow) return setCursor((value) => visible.length === 0 ? 0 : (value + 1) % visible.length);
28644
- if (key.return && visible[cursor]?.broken === void 0) return select(visible[cursor].id);
29281
+ if (key.return && visible[cursor] !== void 0 && visible[cursor].broken === void 0) return select(visible[cursor].id);
28645
29282
  const next = editQuery(query, input, key);
28646
29283
  if (next !== void 0) {
28647
29284
  setQuery(next);
@@ -29034,8 +29671,10 @@ function HistoryPanel({ entries, fill, close }) {
29034
29671
  });
29035
29672
  const stdout = useStdout().stdout;
29036
29673
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29037
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/history · esc close", viewport.contentColumns));
29038
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
29674
+ if (viewport.maxHeight === 0 || viewport.compact) {
29675
+ const picked = matches[cursor];
29676
+ return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/history · " + (picked === void 0 ? "no matching prompts" : singleLineText(picked)) + " · esc close", viewport.contentColumns));
29677
+ }
29039
29678
  const bodyRows = Math.max(1, viewport.bodyRows - 1);
29040
29679
  const offset = revealRow(0, cursor, matches.length, bodyRows);
29041
29680
  const visible = matches.slice(offset, offset + bodyRows);
@@ -29120,8 +29759,7 @@ function StatuslinePanel({ enabled, change, close }) {
29120
29759
  });
29121
29760
  const stdout = useStdout().stdout;
29122
29761
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29123
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/statusline · esc close", viewport.contentColumns));
29124
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
29762
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/statusline · esc close", viewport.contentColumns));
29125
29763
  const bodyRows = Math.max(1, viewport.bodyRows - 1);
29126
29764
  const offset = revealRow(0, cursor, order.length, bodyRows);
29127
29765
  const visible = order.slice(offset, offset + bodyRows);
@@ -29162,7 +29800,7 @@ function StatuslinePanel({ enabled, change, close }) {
29162
29800
  * instead of a bare failure notice. Enter applies one level; Esc returns to
29163
29801
  * the model list without applying.
29164
29802
  */
29165
- function EffortPanel({ row, current, select, back }) {
29803
+ function EffortPanel({ row, current, select, back, onExit }) {
29166
29804
  const advertised = row.reasoning?.efforts ?? [];
29167
29805
  const empty = row.reasoning === void 0 || advertised.length === 0;
29168
29806
  const hasDefaultRow = row.reasoning !== void 0 && row.reasoning.defaultEffort === void 0;
@@ -29186,6 +29824,7 @@ function EffortPanel({ row, current, select, back }) {
29186
29824
  }, [rows.length, cursor]);
29187
29825
  useInput((input, key) => {
29188
29826
  if (key.escape || input === "q") return back();
29827
+ if (key.ctrl && input === "c") return onExit();
29189
29828
  if (empty) return;
29190
29829
  if (input === "g") {
29191
29830
  setCursor(0);
@@ -29375,7 +30014,8 @@ function SubagentPanel({ current, load, pick, inherit, close }) {
29375
30014
  row: effortFor,
29376
30015
  current: current === "" ? void 0 : current.split("@")[1],
29377
30016
  select: (effortId) => pick(effortFor, effortId),
29378
- back: () => setEffortFor(void 0)
30017
+ back: () => setEffortFor(void 0),
30018
+ onExit: close
29379
30019
  });
29380
30020
  return (0, import_react.createElement)(ListFrame, {
29381
30021
  title: `/subagent — model for delegated agents${current === "" ? "" : ` · override ${current}`}`,
@@ -29422,6 +30062,26 @@ function parseHistoryFile(raw, max = 100) {
29422
30062
  return kept.slice(-max);
29423
30063
  }
29424
30064
  /**
30065
+ * The append unit for the persistent file: one JSON line, so a multi-line
30066
+ * draft still occupies exactly one physical line. Each submission appends
30067
+ * this unit at the end of the file, so concurrent terminals add entries
30068
+ * after each other. Node chunks one append at 512 KiB: a pasted entry
30069
+ * beyond that size could interleave mid-line with another writer's
30070
+ * chunks, and the damaged line then drops out at the next parse —
30071
+ * recall tolerates the loss by design.
30072
+ */
30073
+ function historyLine(text) {
30074
+ return serializeHistoryEntry(text) + "\n";
30075
+ }
30076
+ /**
30077
+ * Whether the file on disk differs from its canonical form (deduped and
30078
+ * capped). True means stale lines have accumulated and the next boot
30079
+ * should rewrite it once, atomically.
30080
+ */
30081
+ function needsCompaction(raw, max = 100) {
30082
+ return serializeHistoryList(parseHistoryFile(raw, max)) !== raw;
30083
+ }
30084
+ /**
29425
30085
  * Record one in-session submission: empty text is ignored and an adjacent
29426
30086
  * duplicate collapses (Codex `record_local_submission` semantics). The local
29427
30087
  * pool shares the persistent pool's cap so the recall space stays bounded.
@@ -29436,10 +30096,10 @@ function recordLocalEntry(local, text, max = 100) {
29436
30096
  return [...local, text].slice(-max);
29437
30097
  }
29438
30098
  /**
29439
- * Serialize a capped entry list to the history file format (one JSON line per
29440
- * entry, trailing newline). The runner writes the in-memory list as the whole
29441
- * file, so rapid same-process submissions cannot lose entries to a
29442
- * read-modify-write race (the file is never read back before writing).
30099
+ * Serialize a capped entry list to the history file format (one JSON line
30100
+ * per entry, trailing newline). The boot-time compaction writes this
30101
+ * canonical form once when stale lines have accumulated; submissions
30102
+ * themselves only ever append a single line.
29443
30103
  * @param entries - the entries to persist, oldest first.
29444
30104
  * @returns the file content, '' for an empty list.
29445
30105
  */
@@ -29798,8 +30458,7 @@ function ProviderAuthorizationPanel(props) {
29798
30458
  }
29799
30459
  if (input !== "" && !key.ctrl && !key.meta) setDraft((current) => current + input);
29800
30460
  });
29801
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
29802
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("provider login · esc cancel", viewport.contentColumns));
30461
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("provider login · esc cancel", viewport.contentColumns));
29803
30462
  const rows = [];
29804
30463
  if (phase === "methods") {
29805
30464
  if (error !== void 0) rows.push({
@@ -29903,8 +30562,7 @@ function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }) {
29903
30562
  setError(reason instanceof Error ? reason.message : String(reason));
29904
30563
  });
29905
30564
  });
29906
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
29907
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("y logout · n/esc back", viewport.contentColumns));
30565
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("y logout · n/esc back", viewport.contentColumns));
29908
30566
  return (0, import_react.createElement)(Box, {
29909
30567
  flexDirection: "column",
29910
30568
  width: viewport.outerColumns,
@@ -30075,12 +30733,6 @@ function readSettledRowCap() {
30075
30733
  const parsed = Number.parseInt(raw, 10);
30076
30734
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 3e3;
30077
30735
  }
30078
- /**
30079
- * Safety net for a bracketed paste whose end marker never arrives (terminal
30080
- * defect or crash mid-paste): past this window the open-paste flag resets so
30081
- * Enter submits again instead of inserting newlines forever.
30082
- */
30083
- const PASTE_BRACKET_TIMEOUT_MS = 1e3;
30084
30736
  /** Release the held frame after Ink has replayed the source-backed Static rows. */
30085
30737
  const SYNCHRONIZED_UPDATE_END = "\x1B[?2026l";
30086
30738
  /** One source of truth for TUI-owned slash commands in completion and `/help`. */
@@ -30290,13 +30942,14 @@ function StreamTail({ text, dim, maxRows, prefix = "", continuationPrefix = pref
30290
30942
  const columns = useStdout().stdout?.columns ?? 80;
30291
30943
  const safeRows = Math.max(1, maxRows);
30292
30944
  const prefixColumns = Math.max(visibleColumns(prefix), visibleColumns(continuationPrefix));
30293
- const contentColumns = Math.max(10, columns - 1 - prefixColumns);
30945
+ const contentColumns = Math.max(1, columns - 1 - prefixColumns);
30294
30946
  const initial = displayTail(text, contentColumns, safeRows);
30295
30947
  const tail = initial.truncated && safeRows > 1 ? displayTail(text, contentColumns, safeRows - 1) : initial;
30296
30948
  const rows = tail.text.split("\n");
30297
30949
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, tail.truncated && safeRows > 1 ? (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, continuationPrefix, "…") : void 0, ...rows.map((row, index) => (0, import_react.createElement)(Text, {
30298
30950
  key: index,
30299
- dimColor: dim || void 0
30951
+ dimColor: dim || void 0,
30952
+ wrap: "truncate-end"
30300
30953
  }, index === 0 ? prefix : continuationPrefix, row, index + 1 === rows.length ? children : void 0)));
30301
30954
  }
30302
30955
  /** Ink props for one markdown style class. */
@@ -30606,8 +31259,7 @@ function TodoListPanel({ todos, onClose }) {
30606
31259
  else if (input === "g") setScroll(0);
30607
31260
  else if (input === "G") setScroll(Math.max(0, rows.length - viewport.bodyRows));
30608
31261
  });
30609
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
30610
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("todos · esc/q close", viewport.contentColumns));
31262
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("todos · esc/q close", viewport.contentColumns));
30611
31263
  return (0, import_react.createElement)(Box, {
30612
31264
  flexDirection: "column",
30613
31265
  width: viewport.outerColumns,
@@ -30833,7 +31485,7 @@ const APPROVAL_OPTIONS = [
30833
31485
  * `rejected`): "tell it what to do differently" rejects and hands the
30834
31486
  * composer back with a hint notice, exactly Codex's decline-then-type flow.
30835
31487
  */
30836
- function ApprovalBar({ snapshot, locked, notify }) {
31488
+ function ApprovalBar({ snapshot, locked, notify, interrupt, summarize }) {
30837
31489
  const stdout = useStdout().stdout;
30838
31490
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
30839
31491
  const [cursor, setCursor] = (0, import_react.useState)(0);
@@ -30855,6 +31507,10 @@ function ApprovalBar({ snapshot, locked, notify }) {
30855
31507
  };
30856
31508
  useInput((input, key) => {
30857
31509
  if (snapshot.pending === void 0 || snapshot.answered) return;
31510
+ if (key.ctrl && input === "c") {
31511
+ interrupt();
31512
+ return;
31513
+ }
30858
31514
  if (key.upArrow) {
30859
31515
  setCursor((current) => (current + APPROVAL_OPTIONS.length - 1) % APPROVAL_OPTIONS.length);
30860
31516
  return;
@@ -30889,9 +31545,8 @@ function ApprovalBar({ snapshot, locked, notify }) {
30889
31545
  }
30890
31546
  }, { isActive: active });
30891
31547
  if (pending === void 0) return void 0;
30892
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
30893
31548
  const queuedSuffix = snapshot.queued > 0 ? ` · +${snapshot.queued} queued` : "";
30894
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`approval${queuedSuffix} · enter/y allow · esc/n reject`, viewport.contentColumns));
31549
+ if (viewport.maxHeight === 0 || viewport.compact || summarize === true) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`approval${queuedSuffix} · enter/y allow · esc/n reject`, viewport.contentColumns));
30895
31550
  const reservedRows = 3 + APPROVAL_OPTIONS.length;
30896
31551
  const bodyBudget = Math.max(1, viewport.bodyRows - reservedRows);
30897
31552
  const visibleBody = body.slice(0, bodyBudget);
@@ -30926,13 +31581,40 @@ function ApprovalBar({ snapshot, locked, notify }) {
30926
31581
  wrap: "truncate-end"
30927
31582
  }, truncateColumns(snapshot.answered ? "submitted…" : "↑↓ choose · enter confirm · y/n/d quick · esc reject", viewport.contentColumns)));
30928
31583
  }
31584
+ function initialQuestionDraft(question) {
31585
+ const hasOptions = (question?.options?.length ?? 0) > 0;
31586
+ return {
31587
+ selected: [],
31588
+ custom: "",
31589
+ cursor: 0,
31590
+ mode: hasOptions ? "options" : "custom",
31591
+ scroll: 0,
31592
+ manualScroll: false,
31593
+ followCustomTail: !hasOptions,
31594
+ committed: false
31595
+ };
31596
+ }
31597
+ function answerFromQuestionDraft(question, draft) {
31598
+ if (!(question.multiSelect === true ? draft.committed || draft.selected.length > 0 || draft.custom.trim() !== "" : draft.committed)) return {
31599
+ id: question.id,
31600
+ selected: []
31601
+ };
31602
+ const options = question.options ?? [];
31603
+ const selected = draft.selected.map((at) => options[at]?.label).filter((label) => label !== void 0);
31604
+ const custom = draft.custom.trim();
31605
+ return {
31606
+ id: question.id,
31607
+ selected,
31608
+ ...custom === "" ? {} : { custom }
31609
+ };
31610
+ }
30929
31611
  /**
30930
31612
  * The ask_user_question bar: walks one request question by question,
30931
- * renders the option menu (Claude-Code style: arrows move, space toggles a
30932
- * multi-select, enter submits, `c` opens the custom-answer box, Esc
30933
- * interrupts the question as aborted). Plan reviews arrive through the same
30934
- * service with a `plan-review` intent — the approve option gets a ✓ mark,
30935
- * the answer encoding stays identical.
31613
+ * retaining an independent draft for every question. Options use Space/1-9
31614
+ * to toggle a multi-select, Enter to confirm, and arrows/Ctrl+P/N to move
31615
+ * between questions. Plan reviews arrive through the same service with a
31616
+ * `plan-review` intent — the approve option gets a ✓ mark, the answer
31617
+ * encoding stays identical.
30936
31618
  */
30937
31619
  function QuestionBar({ store, snapshot, locked }) {
30938
31620
  const stdout = useStdout().stdout;
@@ -30940,33 +31622,25 @@ function QuestionBar({ store, snapshot, locked }) {
30940
31622
  const pending = snapshot.pending;
30941
31623
  const request = pending?.request;
30942
31624
  const [index, setIndex] = (0, import_react.useState)(0);
30943
- const [cursor, setCursor] = (0, import_react.useState)(0);
30944
- const [selected, setSelected] = (0, import_react.useState)([]);
30945
- const [mode, setMode] = (0, import_react.useState)("options");
30946
- const [custom, setCustom] = (0, import_react.useState)("");
30947
- const [answers, setAnswers] = (0, import_react.useState)([]);
31625
+ const [drafts, setDrafts] = (0, import_react.useState)(() => request?.questions.map((question) => initialQuestionDraft(question)) ?? []);
30948
31626
  const [submitted, setSubmitted] = (0, import_react.useState)(false);
30949
- const [scroll, setScroll] = (0, import_react.useState)(0);
30950
- const [manualScroll, setManualScroll] = (0, import_react.useState)(false);
30951
- const [followCustomTail, setFollowCustomTail] = (0, import_react.useState)(false);
31627
+ const draftsRef = (0, import_react.useRef)([]);
31628
+ const indexRef = (0, import_react.useRef)(0);
31629
+ draftsRef.current = drafts;
31630
+ indexRef.current = index;
30952
31631
  (0, import_react.useEffect)(() => {
30953
- const first = request?.questions[0];
30954
- const initialMode = first?.options === void 0 || first.options.length === 0 ? "custom" : "options";
30955
- setIndex((current) => current === 0 ? current : 0);
30956
- setCursor((current) => current === 0 ? current : 0);
30957
- setSelected((current) => current.length === 0 ? current : []);
30958
- setMode((current) => current === initialMode ? current : initialMode);
30959
- setCustom((current) => current === "" ? current : "");
30960
- setAnswers((current) => current.length === 0 ? current : []);
30961
- setSubmitted((current) => current ? false : current);
30962
- setScroll((current) => current === 0 ? current : 0);
30963
- setManualScroll((current) => current ? false : current);
30964
- setFollowCustomTail((current) => current === (initialMode === "custom") ? current : initialMode === "custom");
31632
+ const next = request?.questions.map((question) => initialQuestionDraft(question)) ?? [];
31633
+ draftsRef.current = next;
31634
+ indexRef.current = 0;
31635
+ setDrafts(next);
31636
+ setIndex(0);
31637
+ setSubmitted(false);
30965
31638
  }, [request]);
30966
31639
  const question = pending?.request.questions[index];
30967
31640
  const options = question?.options ?? [];
30968
31641
  const isPlan = question?.intent?.kind === "plan-review";
30969
31642
  const isMulti = question?.multiSelect === true;
31643
+ const { cursor, selected, mode, custom, scroll, manualScroll, followCustomTail } = drafts[index] ?? initialQuestionDraft(question);
30970
31644
  const active = !locked && pending !== void 0 && question !== void 0 && !submitted;
30971
31645
  const rendered = (0, import_react.useMemo)(() => {
30972
31646
  if (question === void 0) return {
@@ -30992,6 +31666,7 @@ function QuestionBar({ store, snapshot, locked }) {
30992
31666
  const style = at === cursor ? "brand" : chosen || approve ? "success" : "plain";
30993
31667
  lines.push(...styledLines([
30994
31668
  lineSegment(mark, style),
31669
+ lineSegment(at < 9 ? `${at + 1}. ` : "", "dim"),
30995
31670
  lineSegment(option.label, style),
30996
31671
  lineSegment(option.description === void 0 ? "" : ` — ${option.description}`, "dim")
30997
31672
  ], viewport.contentColumns));
@@ -31014,32 +31689,54 @@ function QuestionBar({ store, snapshot, locked }) {
31014
31689
  ]);
31015
31690
  const focusedRow = rendered.optionRows[cursor] ?? 0;
31016
31691
  const visibleScroll = clampScroll(mode === "options" && options.length > 0 && !manualScroll ? revealRow(scroll, focusedRow, rendered.lines.length, viewport.bodyRows) : (mode === "custom" || options.length === 0) && followCustomTail ? Math.max(0, rendered.lines.length - viewport.bodyRows) : scroll, rendered.lines.length, viewport.bodyRows);
31692
+ const updateDrafts = (update) => {
31693
+ const next = update(draftsRef.current);
31694
+ draftsRef.current = next;
31695
+ setDrafts(next);
31696
+ };
31697
+ const updateCurrentDraft = (update) => {
31698
+ const currentIndex = indexRef.current;
31699
+ updateDrafts((current) => current.map((draft, at) => at === currentIndex ? update(draft) : draft));
31700
+ };
31701
+ const moveQuestion = (direction) => {
31702
+ const total = request?.questions.length ?? 0;
31703
+ if (total <= 1) return;
31704
+ const currentIndex = indexRef.current;
31705
+ const nextIndex = Math.max(0, Math.min(total - 1, currentIndex + direction));
31706
+ if (nextIndex === currentIndex) return;
31707
+ indexRef.current = nextIndex;
31708
+ setIndex(nextIndex);
31709
+ };
31017
31710
  const commit = (answer) => {
31018
- if (pending === void 0) return;
31019
- const next = [...answers, answer];
31711
+ if (pending === void 0 || question === void 0) return;
31712
+ const currentIndex = indexRef.current;
31713
+ const optionLabels = new Set(answer.selected);
31714
+ const selectedIndices = (question.options ?? []).map((option, at) => optionLabels.has(option.label) ? at : -1).filter((at) => at >= 0);
31715
+ const nextDrafts = draftsRef.current.map((draft, at) => at === currentIndex ? {
31716
+ ...draft,
31717
+ selected: selectedIndices,
31718
+ custom: answer.custom ?? "",
31719
+ committed: true
31720
+ } : draft);
31721
+ draftsRef.current = nextDrafts;
31722
+ setDrafts(nextDrafts);
31020
31723
  const total = pending.request.questions.length;
31021
- if (index + 1 >= total) {
31724
+ if (currentIndex + 1 >= total) {
31022
31725
  setSubmitted(true);
31023
- store.submit(pending, { answers: next });
31726
+ store.submit(pending, { answers: pending.request.questions.map((item, at) => answerFromQuestionDraft(item, nextDrafts[at] ?? initialQuestionDraft(item))) });
31024
31727
  return;
31025
31728
  }
31026
- setAnswers(next);
31027
- const nextIndex = index + 1;
31028
- const nextQuestion = pending.request.questions[nextIndex];
31729
+ const nextIndex = currentIndex + 1;
31730
+ indexRef.current = nextIndex;
31029
31731
  setIndex(nextIndex);
31030
- setCursor(0);
31031
- setSelected([]);
31032
- setMode(nextQuestion?.options === void 0 || nextQuestion.options.length === 0 ? "custom" : "options");
31033
- setCustom("");
31034
- setScroll(0);
31035
- setManualScroll(false);
31036
- setFollowCustomTail(nextQuestion?.options === void 0 || nextQuestion.options.length === 0);
31037
31732
  };
31038
31733
  const commitOption = () => {
31039
31734
  if (pending === void 0 || question === void 0) return;
31735
+ const currentIndex = indexRef.current;
31736
+ const current = draftsRef.current[currentIndex] ?? initialQuestionDraft(question);
31040
31737
  if (isMulti) {
31041
- const labels = selected.map((at) => options[at]?.label).filter((label) => label !== void 0);
31042
- const customText = custom.trim();
31738
+ const labels = current.selected.map((at) => options[at]?.label).filter((label) => label !== void 0);
31739
+ const customText = current.custom.trim();
31043
31740
  commit({
31044
31741
  id: question.id,
31045
31742
  selected: labels,
@@ -31047,7 +31744,7 @@ function QuestionBar({ store, snapshot, locked }) {
31047
31744
  });
31048
31745
  return;
31049
31746
  }
31050
- const option = options[cursor];
31747
+ const option = options[current.cursor];
31051
31748
  if (option === void 0) return;
31052
31749
  commit({
31053
31750
  id: question.id,
@@ -31057,19 +31754,30 @@ function QuestionBar({ store, snapshot, locked }) {
31057
31754
  /**
31058
31755
  * A question with choices has two local focus surfaces, just like Codex:
31059
31756
  * the choice list and the optional custom-answer editor. Returning to the
31060
- * list keeps the user's current choice (and multi-select state), but drops
31061
- * the transient custom draft so a second Escape can cancel the question.
31757
+ * list keeps the user's current choice and multi-select state, but drops the
31758
+ * transient custom draft so a second Escape can cancel the question.
31062
31759
  */
31063
31760
  const returnToOptions = () => {
31064
31761
  if (options.length === 0) return;
31065
- setMode("options");
31066
- setCustom("");
31067
- setScroll(0);
31068
- setManualScroll(false);
31069
- setFollowCustomTail(false);
31762
+ updateCurrentDraft((current) => ({
31763
+ ...current,
31764
+ mode: "options",
31765
+ custom: "",
31766
+ scroll: 0,
31767
+ manualScroll: false,
31768
+ followCustomTail: false
31769
+ }));
31070
31770
  };
31071
31771
  useStableInput((input, key) => {
31072
31772
  if (pending === void 0 || question === void 0 || submitted) return;
31773
+ if (viewport.maxHeight === 0) {
31774
+ if (key.escape || key.ctrl && input === "c") store.cancel(pending);
31775
+ return;
31776
+ }
31777
+ if (key.ctrl && input === "c") {
31778
+ store.cancel(pending);
31779
+ return;
31780
+ }
31073
31781
  if (key.escape) {
31074
31782
  if (mode === "custom" && options.length > 0) {
31075
31783
  returnToOptions();
@@ -31078,16 +31786,34 @@ function QuestionBar({ store, snapshot, locked }) {
31078
31786
  store.cancel(pending);
31079
31787
  return;
31080
31788
  }
31789
+ if (key.tab && key.shift) {
31790
+ moveQuestion(-1);
31791
+ return;
31792
+ }
31793
+ if (key.leftArrow || key.ctrl && input === "p") {
31794
+ moveQuestion(-1);
31795
+ return;
31796
+ }
31797
+ if (key.rightArrow || key.ctrl && input === "n") {
31798
+ moveQuestion(1);
31799
+ return;
31800
+ }
31081
31801
  if (key.pageUp) {
31082
- setManualScroll(true);
31083
- setFollowCustomTail(false);
31084
- setScroll(moveScroll(visibleScroll, -Math.max(1, viewport.bodyRows - 1), rendered.lines.length, viewport.bodyRows));
31802
+ updateCurrentDraft((current) => ({
31803
+ ...current,
31804
+ manualScroll: true,
31805
+ followCustomTail: false,
31806
+ scroll: moveScroll(visibleScroll, -Math.max(1, viewport.bodyRows - 1), rendered.lines.length, viewport.bodyRows)
31807
+ }));
31085
31808
  return;
31086
31809
  }
31087
31810
  if (key.pageDown) {
31088
- setManualScroll(true);
31089
- setFollowCustomTail(false);
31090
- setScroll(moveScroll(visibleScroll, Math.max(1, viewport.bodyRows - 1), rendered.lines.length, viewport.bodyRows));
31811
+ updateCurrentDraft((current) => ({
31812
+ ...current,
31813
+ manualScroll: true,
31814
+ followCustomTail: false,
31815
+ scroll: moveScroll(visibleScroll, Math.max(1, viewport.bodyRows - 1), rendered.lines.length, viewport.bodyRows)
31816
+ }));
31091
31817
  return;
31092
31818
  }
31093
31819
  if (mode === "custom" || options.length === 0) {
@@ -31096,13 +31822,19 @@ function QuestionBar({ store, snapshot, locked }) {
31096
31822
  return;
31097
31823
  }
31098
31824
  if (key.upArrow) {
31099
- setFollowCustomTail(false);
31100
- setScroll(moveScroll(visibleScroll, -1, rendered.lines.length, viewport.bodyRows));
31825
+ updateCurrentDraft((current) => ({
31826
+ ...current,
31827
+ followCustomTail: false,
31828
+ scroll: moveScroll(visibleScroll, -1, rendered.lines.length, viewport.bodyRows)
31829
+ }));
31101
31830
  return;
31102
31831
  }
31103
31832
  if (key.downArrow) {
31104
- setFollowCustomTail(false);
31105
- setScroll(moveScroll(visibleScroll, 1, rendered.lines.length, viewport.bodyRows));
31833
+ updateCurrentDraft((current) => ({
31834
+ ...current,
31835
+ followCustomTail: false,
31836
+ scroll: moveScroll(visibleScroll, 1, rendered.lines.length, viewport.bodyRows)
31837
+ }));
31106
31838
  return;
31107
31839
  }
31108
31840
  if (key.return) {
@@ -31122,23 +31854,37 @@ function QuestionBar({ store, snapshot, locked }) {
31122
31854
  returnToOptions();
31123
31855
  return;
31124
31856
  }
31125
- setCustom((current) => deleteLastGrapheme(current));
31857
+ updateCurrentDraft((current) => ({
31858
+ ...current,
31859
+ custom: deleteLastGrapheme(current.custom),
31860
+ committed: false
31861
+ }));
31126
31862
  return;
31127
31863
  }
31128
31864
  if (input !== "" && !key.ctrl && !key.meta) {
31129
31865
  const text = stripPasteMarkers(input);
31130
- if (text !== "") setCustom((current) => current + text);
31866
+ if (text !== "") updateCurrentDraft((current) => ({
31867
+ ...current,
31868
+ custom: current.custom + text,
31869
+ committed: false
31870
+ }));
31131
31871
  }
31132
31872
  return;
31133
31873
  }
31134
31874
  if (key.upArrow) {
31135
- setManualScroll(false);
31136
- setCursor((current) => (current + options.length - 1) % options.length);
31875
+ updateCurrentDraft((current) => ({
31876
+ ...current,
31877
+ cursor: (current.cursor + options.length - 1) % options.length,
31878
+ manualScroll: false
31879
+ }));
31137
31880
  return;
31138
31881
  }
31139
31882
  if (key.downArrow) {
31140
- setManualScroll(false);
31141
- setCursor((current) => (current + 1) % options.length);
31883
+ updateCurrentDraft((current) => ({
31884
+ ...current,
31885
+ cursor: (current.cursor + 1) % options.length,
31886
+ manualScroll: false
31887
+ }));
31142
31888
  return;
31143
31889
  }
31144
31890
  if (key.return) {
@@ -31146,17 +31892,43 @@ function QuestionBar({ store, snapshot, locked }) {
31146
31892
  return;
31147
31893
  }
31148
31894
  if (key.tab || input === "c" || input === "C") {
31149
- setMode("custom");
31150
- setManualScroll(false);
31151
- setFollowCustomTail(true);
31895
+ updateCurrentDraft((current) => ({
31896
+ ...current,
31897
+ mode: "custom",
31898
+ manualScroll: false,
31899
+ followCustomTail: true
31900
+ }));
31901
+ return;
31902
+ }
31903
+ if (input === " " && isMulti) {
31904
+ updateCurrentDraft((current) => ({
31905
+ ...current,
31906
+ selected: current.selected.includes(current.cursor) ? current.selected.filter((at) => at !== current.cursor) : [...current.selected, current.cursor],
31907
+ committed: false
31908
+ }));
31152
31909
  return;
31153
31910
  }
31154
- if (input === " " && isMulti) setSelected((current) => current.includes(cursor) ? current.filter((at) => at !== cursor) : [...current, cursor]);
31911
+ if (/^[1-9]$/.test(input)) {
31912
+ const at = Number(input) - 1;
31913
+ if (at >= options.length) return;
31914
+ if (isMulti) updateCurrentDraft((current) => ({
31915
+ ...current,
31916
+ selected: current.selected.includes(at) ? current.selected.filter((row) => row !== at) : [...current.selected, at],
31917
+ committed: false
31918
+ }));
31919
+ else {
31920
+ const option = options[at];
31921
+ if (option !== void 0) commit({
31922
+ id: question.id,
31923
+ selected: [option.label]
31924
+ });
31925
+ }
31926
+ }
31155
31927
  }, active);
31156
31928
  if (pending === void 0 || question === void 0) return void 0;
31157
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
31158
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(isPlan ? "plan review · esc cancel" : "question · esc cancel", viewport.contentColumns));
31159
- const footer = submitted ? "submitted…" : mode === "custom" ? options.length === 0 ? "↑↓/pgup/pgdn scroll · type answer · enter submit · esc interrupt" : "↑↓/pgup/pgdn scroll · type answer · enter submit · tab/esc or empty backspace: options" : options.length === 0 ? "↑↓/pgup/pgdn scroll · type answer · enter submit · esc interrupt" : isMulti ? "↑↓ choose · pgup/pgdn scroll · space toggle · enter submit · c custom · esc interrupt" : "↑↓ choose · pgup/pgdn scroll · enter submit · c custom · esc interrupt";
31929
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(isPlan ? "plan review · esc cancel" : "question · esc cancel", viewport.contentColumns));
31930
+ const footerBase = submitted ? "submitted…" : mode === "custom" ? options.length === 0 ? "↑↓/pgup/pgdn scroll · type answer · enter submit · esc interrupt" : "↑↓/pgup/pgdn scroll · type answer · enter submit · tab/esc or empty backspace: options" : options.length === 0 ? "↑↓/pgup/pgdn scroll · type answer · enter submit · esc interrupt" : isMulti ? "↑↓ choose · pgup/pgdn scroll · space/1-9 toggle · enter submit · c custom · esc interrupt" : "↑↓ choose · pgup/pgdn scroll · 1-9 pick · enter submit · c custom · esc interrupt";
31931
+ const footer = pending.request.questions.length > 1 && !submitted ? `${footerBase} · ←→/ctrl+p/n switch question` : footerBase;
31160
31932
  return (0, import_react.createElement)(Box, {
31161
31933
  flexDirection: "column",
31162
31934
  width: viewport.outerColumns,
@@ -31207,10 +31979,14 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
31207
31979
  onRetry();
31208
31980
  return;
31209
31981
  }
31210
- if (input === "a" && onProviders !== void 0) {
31982
+ if (key.tab && onProviders !== void 0) {
31211
31983
  onProviders();
31212
31984
  return;
31213
31985
  }
31986
+ if (key.ctrl && input === "c") {
31987
+ onClose();
31988
+ return;
31989
+ }
31214
31990
  if (rows.length === 0) return;
31215
31991
  if (key.upArrow) {
31216
31992
  setCursor(cursor > 0 ? cursor - 1 : rows.length - 1);
@@ -31238,8 +32014,11 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
31238
32014
  }
31239
32015
  if (key.return && rows[cursor] !== void 0) onSelect(rows[cursor]);
31240
32016
  });
31241
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
31242
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`/model${onProviders === void 0 ? "" : " · a providers"} · r retry · esc/q close`, viewport.contentColumns));
32017
+ if (viewport.maxHeight === 0 || viewport.compact) {
32018
+ const providers = onProviders === void 0 ? "" : " · tab providers";
32019
+ const state = rows.length === 0 ? directory === void 0 && error === void 0 ? "loading…" : error !== void 0 ? "error" : "no models" : `❯ ${rows[cursor]?.modelName ?? rows[cursor]?.model ?? ""}`;
32020
+ return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`/model · ${state}${providers} · r retry · esc/q close`, viewport.contentColumns));
32021
+ }
31243
32022
  const visibleStateRows = (directory === void 0 && error === void 0 ? [(0, import_react.createElement)(Text, {
31244
32023
  key: "loading",
31245
32024
  dimColor: true,
@@ -31282,7 +32061,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
31282
32061
  }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
31283
32062
  dimColor: true,
31284
32063
  wrap: "truncate-end"
31285
- }, dim(truncateColumns(`↑↓ move · pgup/pgdn page · enter select${onProviders === void 0 ? "" : " · a providers"} · r retry · esc/q close`, viewport.contentColumns))));
32064
+ }, dim(truncateColumns(`↑↓ move · pgup/pgdn page · enter select${onProviders === void 0 ? "" : " · tab providers"} · r retry · esc/q close`, viewport.contentColumns))));
31286
32065
  }
31287
32066
  /** Compact provider-state copy; only value-free credential facts cross this boundary. */
31288
32067
  function providerStateLabel(row) {
@@ -31296,10 +32075,13 @@ function providerStateLabel(row) {
31296
32075
  return `${route} · ${row.configured ? "provider auth" : "not configured"}`;
31297
32076
  }
31298
32077
  /** The provider-management stage reached from /model with `a`. */
31299
- function ProviderPanel({ directory, error, authorizations, authorizationError, onCredential, onConfigure, onUnset, onRemove, onLogin, onLogout, onRetry, onBack }) {
32078
+ function ProviderPanel({ directory, error, authorizations, authorizationError, onConfigure, onUnset, onRemove, onLogin, onLogout, onRetry, onBack, onExit }) {
31300
32079
  const stdout = useStdout().stdout;
31301
32080
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
31302
32081
  const rows = directory?.rows ?? [];
32082
+ const sorted = [...rows].sort((left, right) => (left.configured ? 0 : 1) - (right.configured ? 0 : 1));
32083
+ const configuredCount = sorted.filter((row) => row.configured).length;
32084
+ const hasSeparator = configuredCount > 0 && configuredCount < sorted.length;
31303
32085
  const [cursor, setCursor] = (0, import_react.useState)(0);
31304
32086
  const [actionError, setActionError] = (0, import_react.useState)(void 0);
31305
32087
  (0, import_react.useEffect)(() => {
@@ -31314,6 +32096,10 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
31314
32096
  onBack();
31315
32097
  return;
31316
32098
  }
32099
+ if (key.ctrl && input === "c") {
32100
+ onExit();
32101
+ return;
32102
+ }
31317
32103
  if (input === "r") {
31318
32104
  setActionError(void 0);
31319
32105
  onRetry();
@@ -31340,13 +32126,8 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
31340
32126
  setCursor((current) => Math.min(rows.length - 1, current + Math.max(1, viewport.bodyRows - 1)));
31341
32127
  return;
31342
32128
  }
31343
- const target = rows[cursor];
32129
+ const target = sorted[cursor];
31344
32130
  if (target === void 0) return;
31345
- if (key.tab) {
31346
- if (target.settingsNs.length === 0) setActionError("this provider is not managed by Harness settings");
31347
- else onConfigure(target);
31348
- return;
31349
- }
31350
32131
  if (input === "d") {
31351
32132
  const facts = target.credential;
31352
32133
  if (facts?.kind !== "facts" || !facts.configured) setActionError("this provider has no configured API key to remove");
@@ -31374,14 +32155,10 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
31374
32155
  }
31375
32156
  if (key.return) {
31376
32157
  if (target.settingsNs.length === 0) setActionError("this provider is not managed by Harness settings");
31377
- else if (target.credential?.kind === "error") setActionError("credential status is unavailable; retry before writing");
31378
- else if (target.credential?.kind === "facts" && !target.credential.writable) setActionError("this API key is supplied read-only by the environment");
31379
- else if (target.credentialRef === void 0 && directory?.writable !== true) setActionError("settings are read-only; this provider cannot be activated here");
31380
- else onCredential(target);
32158
+ else onConfigure(target);
31381
32159
  }
31382
32160
  }, true);
31383
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
31384
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/model providers · enter key · d remove key · esc back", viewport.contentColumns));
32161
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/model providers · enter configure · d remove key · esc back", viewport.contentColumns));
31385
32162
  const visibleStateRows = (directory === void 0 && error === void 0 ? [(0, import_react.createElement)(Text, {
31386
32163
  key: "loading",
31387
32164
  color: inkColor(getPalette().dim),
@@ -31418,8 +32195,32 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
31418
32195
  }, " no configurable providers")] : []
31419
32196
  ]).slice(0, viewport.bodyRows);
31420
32197
  const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length);
31421
- const first = selectionWindow(cursor, rows.length, rowBudget);
31422
- const visible = rowBudget === 0 ? [] : rows.slice(first, first + rowBudget);
32198
+ const displayLength = sorted.length + (hasSeparator ? 1 : 0);
32199
+ const first = selectionWindow(cursor + (hasSeparator && cursor >= configuredCount ? 1 : 0), displayLength, rowBudget);
32200
+ const itemRows = [];
32201
+ for (let display = first; display < first + rowBudget && display < displayLength; display += 1) {
32202
+ if (hasSeparator && display === configuredCount) {
32203
+ itemRows.push((0, import_react.createElement)(Text, {
32204
+ key: "separator",
32205
+ color: inkColor(getPalette().dim),
32206
+ wrap: "truncate-end"
32207
+ }, truncateColumns(" ── not configured ──", viewport.contentColumns)));
32208
+ continue;
32209
+ }
32210
+ const index = hasSeparator && display > configuredCount ? display - 1 : display;
32211
+ const row = sorted[index];
32212
+ if (row === void 0) continue;
32213
+ const identity = row.displayName === row.provider ? row.provider : row.displayName + " (" + row.provider + ")";
32214
+ const authorization = authorizationForProvider(authorizations, row.provider);
32215
+ const authLabel = !(row.credential?.kind === "facts" && row.credential.configured) || authorization?.record.configured === true || authorization?.inFlight === true ? " · " + providerAuthorizationStatus(authorization) : "";
32216
+ const label = identity + " · " + providerStateLabel(row) + authLabel + (row.removable ? " · custom" : "");
32217
+ const idleColor = row.configured ? inkColor(getPalette().brandMid) : inkColor(getPalette().dim);
32218
+ itemRows.push((0, import_react.createElement)(Text, {
32219
+ key: row.provider,
32220
+ color: index === cursor ? inkColor(getPalette().brandBright) : idleColor,
32221
+ wrap: "truncate-end"
32222
+ }, truncateColumns((index === cursor ? "❯ " : " ") + displayText(label), viewport.contentColumns)));
32223
+ }
31423
32224
  return (0, import_react.createElement)(Box, {
31424
32225
  flexDirection: "column",
31425
32226
  width: viewport.outerColumns,
@@ -31430,128 +32231,379 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
31430
32231
  color: inkColor(getPalette().brand),
31431
32232
  bold: true,
31432
32233
  wrap: "truncate-end"
31433
- }, 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) => {
31434
- const index = rows.indexOf(row);
31435
- const identity = row.displayName === row.provider ? row.provider : `${row.displayName} (${row.provider})`;
31436
- const authorization = authorizationForProvider(authorizations, row.provider);
31437
- const authLabel = !(row.credential?.kind === "facts" && row.credential.configured) || authorization?.record.configured === true || authorization?.inFlight === true ? ` · ${providerAuthorizationStatus(authorization)}` : "";
31438
- const label = `${identity} · ${providerStateLabel(row)}${authLabel}${row.removable ? " · custom" : ""}`;
31439
- return (0, import_react.createElement)(Text, {
31440
- key: row.provider,
31441
- color: index === cursor ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
31442
- wrap: "truncate-end"
31443
- }, truncateColumns(`${index === cursor ? "❯ " : " "}${displayText(label)}`, viewport.contentColumns));
31444
- }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
32234
+ }, truncateColumns(`/model — providers${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...itemRows, (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
31445
32235
  color: inkColor(getPalette().dim),
31446
32236
  wrap: "truncate-end"
31447
- }, truncateColumns("↑↓ move · enter key · l login · o logout · tab configure · d remove key · x remove provider · r retry · esc back", viewport.contentColumns)));
32237
+ }, truncateColumns("↑↓ move · enter configure · l login · o logout · d remove key · x remove provider · r retry · esc back", viewport.contentColumns)));
31448
32238
  }
31449
- /** Provider configuration editor: only explicit models are written to settings. */
31450
- function ProviderConfigurationPanel({ target, catalog, save, done, back }) {
32239
+ function ProviderSetupPanel({ target, save, saveCredential, discover, effortDonors, done, back, onExit }) {
31451
32240
  const stdout = useStdout().stdout;
31452
32241
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
32242
+ const [page, setPage] = (0, import_react.useState)("setup");
32243
+ const [keyDraft, setKeyDraft] = (0, import_react.useState)("");
31453
32244
  const [baseURL, setBaseURL] = (0, import_react.useState)(target.configuration.baseURL ?? "");
31454
32245
  const [models, setModels] = (0, import_react.useState)(target.configuration.models);
31455
32246
  const [cursor, setCursor] = (0, import_react.useState)(0);
31456
- const [focus, setFocus] = (0, import_react.useState)("url");
32247
+ const [zone, setZone] = (0, import_react.useState)("key");
32248
+ const [field, setField] = (0, import_react.useState)("none");
32249
+ const [addDraft, setAddDraft] = (0, import_react.useState)("");
32250
+ /** Micro-editor for the selected model's reasoningEfforts declaration. */
32251
+ const [effEditing, setEffEditing] = (0, import_react.useState)(false);
32252
+ const [effDraft, setEffDraft] = (0, import_react.useState)("");
32253
+ const [donorCursor, setDonorCursor] = (0, import_react.useState)(0);
31457
32254
  const [busy, setBusy] = (0, import_react.useState)(false);
31458
32255
  const [error, setError] = (0, import_react.useState)(void 0);
31459
- const choices = (0, import_react.useMemo)(() => {
31460
- const known = catalog.filter((row) => row.provider === target.provider);
31461
- const ids = new Set(known.map((row) => row.model));
31462
- return [...known.map((row) => ({
31463
- id: row.model,
31464
- name: row.modelName
31465
- })), ...models.filter((model) => !ids.has(model.id)).map((model) => ({
31466
- id: model.id,
31467
- name: model.name ?? model.id
31468
- }))];
31469
- }, [
31470
- catalog,
31471
- models,
31472
- target.provider
31473
- ]);
31474
- const selected = choices[cursor];
31475
- const selectedModel = selected === void 0 ? void 0 : models.find((model) => model.id === selected.id);
32256
+ const credential = target.credential;
32257
+ const keyStatus = saveCredential === void 0 ? "key storage unavailable" : credential?.kind === "error" ? "key status unavailable" : credential?.kind === "facts" && credential.configured ? "key saved" + (credential.source === void 0 ? "" : " · " + credential.source) : "no key set";
32258
+ const keyEditable = saveCredential !== void 0 && (credential === void 0 || credential.kind === "facts" && credential.writable);
32259
+ const onAddRow = cursor >= models.length;
32260
+ const selected = onAddRow ? void 0 : models[cursor];
31476
32261
  const updateSelected = (change) => {
31477
32262
  if (selected === void 0) return;
31478
- setModels((current) => current.some((model) => model.id === selected.id) ? current.map((model) => model.id === selected.id ? {
32263
+ setModels((current) => current.map((model, index) => index === cursor ? {
31479
32264
  ...model,
31480
32265
  ...change
31481
- } : model) : [...current, {
31482
- id: selected.id,
31483
- name: selected.name,
31484
- ...change
31485
- }]);
32266
+ } : model));
31486
32267
  };
32268
+ const commitAddDraft = () => {
32269
+ const id = addDraft.trim();
32270
+ if (id === "") return;
32271
+ if (models.some((model) => model.id === id)) {
32272
+ setError("model \"" + id + "\" is already in the list");
32273
+ return;
32274
+ }
32275
+ setError(void 0);
32276
+ setModels([...models, { id }]);
32277
+ setCursor(models.length);
32278
+ setAddDraft("");
32279
+ };
32280
+ /** Compact declaration summary for the row label: count, off, or inherit. */
32281
+ const effortsSummary = (model) => {
32282
+ const raw = model.extras?.reasoningEfforts;
32283
+ if (raw === false) return "off";
32284
+ if (isDeclaredReasoningEfforts(raw)) return String(Object.keys(raw).length);
32285
+ return "~";
32286
+ };
32287
+ /** Write (or clear) the selected model's declaration through extras. */
32288
+ const applyDeclaration = (value) => {
32289
+ setModels((current) => current.map((model, index) => {
32290
+ if (index !== cursor) return model;
32291
+ const extras = { ...model.extras };
32292
+ if (value === void 0) delete extras.reasoningEfforts;
32293
+ else extras.reasoningEfforts = value;
32294
+ return {
32295
+ ...model,
32296
+ ...Object.keys(extras).length === 0 ? {} : { extras }
32297
+ };
32298
+ }));
32299
+ };
32300
+ /** Donors excluding the row being edited (copying from itself is a no-op). */
32301
+ const donorRows = selected === void 0 ? [] : effortDonors.filter((donor) => !(donor.provider === target.provider && donor.id === selected.id));
32302
+ const donorIndex = Math.min(donorCursor, Math.max(0, donorRows.length - 1));
32303
+ const donorRow = donorRows[donorIndex];
31487
32304
  const submit = () => {
31488
32305
  if (busy) return;
32306
+ const key = keyDraft.trim();
32307
+ if (key !== "" && !keyEditable) {
32308
+ setError("this API key cannot be written here (read-only or status unavailable); clear the key field to save the endpoint and models alone");
32309
+ return;
32310
+ }
31489
32311
  setBusy(true);
31490
32312
  setError(void 0);
31491
- Promise.resolve().then(() => save(target, {
31492
- ...baseURL.trim() === "" ? {} : { baseURL },
31493
- models
31494
- })).then(done, (reason) => {
32313
+ const keySave = key !== "" ? saveCredential : void 0;
32314
+ (async () => {
32315
+ if (keySave !== void 0) await keySave(target, key);
32316
+ await save(target, {
32317
+ ...baseURL.trim() === "" ? {} : { baseURL },
32318
+ models
32319
+ });
32320
+ return keySave !== void 0;
32321
+ })().then((keySaved) => done({ key: keySaved }), (reason) => {
31495
32322
  setBusy(false);
31496
32323
  setError(singleLineText(reason instanceof Error ? reason.message : String(reason)));
31497
32324
  });
31498
32325
  };
31499
32326
  useStableInput((input, key) => {
31500
32327
  if (busy) return;
32328
+ if (key.ctrl && input === "c") {
32329
+ onExit();
32330
+ return;
32331
+ }
32332
+ if (effEditing) {
32333
+ if (key.escape) {
32334
+ setEffEditing(false);
32335
+ setEffDraft("");
32336
+ return;
32337
+ }
32338
+ if (key.return) {
32339
+ const parsed = parseReasoningEffortsDraft(effDraft);
32340
+ if (!parsed.ok) {
32341
+ setError(parsed.error);
32342
+ return;
32343
+ }
32344
+ setError(void 0);
32345
+ applyDeclaration(parsed.value);
32346
+ setEffEditing(false);
32347
+ setEffDraft("");
32348
+ return;
32349
+ }
32350
+ if (key.backspace || key.delete) {
32351
+ setError(void 0);
32352
+ setEffDraft((current) => deleteLastGrapheme(current));
32353
+ return;
32354
+ }
32355
+ if (key.ctrl && input === "u") {
32356
+ setError(void 0);
32357
+ setEffDraft("");
32358
+ return;
32359
+ }
32360
+ if (key.ctrl || key.meta || input.length === 0) return;
32361
+ if (effDraft.length > 200) {
32362
+ setError("efforts draft is too long");
32363
+ return;
32364
+ }
32365
+ setError(void 0);
32366
+ setEffDraft((current) => current + stripPasteMarkers(input));
32367
+ return;
32368
+ }
32369
+ if (page === "donor") {
32370
+ if (key.escape || input === "q") {
32371
+ setPage("setup");
32372
+ return;
32373
+ }
32374
+ if (donorRows.length === 0) return;
32375
+ if (key.upArrow) {
32376
+ setDonorCursor((current) => current > 0 ? current - 1 : donorRows.length - 1);
32377
+ return;
32378
+ }
32379
+ if (key.downArrow) {
32380
+ setDonorCursor((current) => current < donorRows.length - 1 ? current + 1 : 0);
32381
+ return;
32382
+ }
32383
+ if (key.return && donorRow !== void 0) {
32384
+ applyDeclaration(donorRow.efforts);
32385
+ setError(void 0);
32386
+ setPage("setup");
32387
+ }
32388
+ return;
32389
+ }
31501
32390
  if (key.escape || input === "q") {
31502
32391
  back();
31503
32392
  return;
31504
32393
  }
31505
32394
  if (key.tab) {
31506
- setFocus((current) => current === "url" ? "models" : current === "models" ? "context" : current === "context" ? "output" : "url");
32395
+ setPage("discover");
31507
32396
  return;
31508
32397
  }
31509
32398
  if (key.return) {
31510
32399
  submit();
31511
32400
  return;
31512
32401
  }
31513
- if (focus === "url") {
32402
+ if (zone === "key") {
32403
+ if (key.downArrow) {
32404
+ setZone("url");
32405
+ return;
32406
+ }
32407
+ if (key.backspace || key.delete) {
32408
+ setError(void 0);
32409
+ setKeyDraft((current) => [...current].slice(0, -1).join(""));
32410
+ return;
32411
+ }
32412
+ if (key.ctrl && input === "u") {
32413
+ setError(void 0);
32414
+ setKeyDraft("");
32415
+ return;
32416
+ }
32417
+ if (key.ctrl || key.meta || input.length === 0) return;
32418
+ const next = keyDraft + stripPasteMarkers(input);
32419
+ if (next.length > 4096) {
32420
+ setError("API key input is too long");
32421
+ return;
32422
+ }
32423
+ setError(void 0);
32424
+ setKeyDraft(next);
32425
+ return;
32426
+ }
32427
+ if (zone === "url") {
32428
+ if (key.upArrow) {
32429
+ setZone("key");
32430
+ return;
32431
+ }
32432
+ if (key.downArrow) {
32433
+ setZone("models");
32434
+ return;
32435
+ }
31514
32436
  if (key.backspace || key.delete) setBaseURL((current) => deleteLastGrapheme(current));
31515
32437
  else if (!key.ctrl && !key.meta && input !== "") setBaseURL((current) => current + stripPasteMarkers(input));
31516
32438
  return;
31517
32439
  }
31518
- if (key.upArrow && choices.length > 0) {
31519
- setCursor((current) => Math.max(0, current - 1));
32440
+ if (key.upArrow) {
32441
+ setError(void 0);
32442
+ if (cursor === 0) setZone("url");
32443
+ else {
32444
+ setCursor((current) => current - 1);
32445
+ setField("none");
32446
+ }
31520
32447
  return;
31521
32448
  }
31522
- if (key.downArrow && choices.length > 0) {
31523
- setCursor((current) => Math.min(choices.length - 1, current + 1));
32449
+ if (key.downArrow) {
32450
+ setError(void 0);
32451
+ if (!onAddRow) {
32452
+ setCursor((current) => current + 1);
32453
+ setField("none");
32454
+ }
31524
32455
  return;
31525
32456
  }
31526
- if (focus === "models" && input === " ") {
31527
- if (selectedModel === void 0) updateSelected({});
31528
- else setModels((current) => current.filter((model) => model.id !== selectedModel.id));
32457
+ if (key.leftArrow || key.rightArrow) {
32458
+ if (selected === void 0) return;
32459
+ const cycle = key.rightArrow ? (current) => current === "none" ? "ctx" : current === "ctx" ? "out" : "none" : (current) => current === "none" ? "out" : current === "out" ? "ctx" : "none";
32460
+ setField((current) => cycle(current));
31529
32461
  return;
31530
32462
  }
31531
- if ((focus === "context" || focus === "output") && selectedModel !== void 0) {
31532
- const field = focus === "context" ? "contextWindow" : "maxTokens";
31533
- const current = String(selectedModel[field] ?? "");
32463
+ if (input === " ") {
32464
+ if (selected === void 0) commitAddDraft();
32465
+ else {
32466
+ setError(void 0);
32467
+ setField("none");
32468
+ setModels((current) => current.filter((_model, index) => index !== cursor));
32469
+ setCursor((current) => Math.min(current, Math.max(0, models.length - 1)));
32470
+ }
32471
+ return;
32472
+ }
32473
+ if (onAddRow) {
32474
+ if (key.backspace || key.delete) {
32475
+ setError(void 0);
32476
+ setAddDraft((current) => deleteLastGrapheme(current));
32477
+ return;
32478
+ }
32479
+ if (key.ctrl || key.meta || input.length === 0) return;
32480
+ setError(void 0);
32481
+ setAddDraft((current) => current + stripPasteMarkers(input));
32482
+ return;
32483
+ }
32484
+ if (input === "e" && selected !== void 0) {
32485
+ setError(void 0);
32486
+ setEffDraft(serializeReasoningEfforts(selected.extras?.reasoningEfforts));
32487
+ setEffEditing(true);
32488
+ return;
32489
+ }
32490
+ if ((input === "c" || input === "C") && !key.ctrl && selected !== void 0) {
32491
+ setError(void 0);
32492
+ setDonorCursor(0);
32493
+ setPage("donor");
32494
+ return;
32495
+ }
32496
+ if (field !== "none" && selected !== void 0) {
32497
+ const name = field === "ctx" ? "contextWindow" : "maxTokens";
32498
+ const current = String(selected[name] ?? "");
31534
32499
  if (key.backspace || key.delete) {
31535
32500
  const next = current.slice(0, -1);
31536
- updateSelected({ [field]: next === "" ? void 0 : Number(next) });
32501
+ updateSelected({ [name]: next === "" ? void 0 : Number(next) });
31537
32502
  } else {
31538
32503
  const digits = stripPasteMarkers(input);
31539
- if (/^[0-9]+$/u.test(digits)) {
31540
- const next = `${current}${digits}`;
31541
- updateSelected({ [field]: Number(next) });
31542
- }
32504
+ if (/^[0-9]+$/u.test(digits)) updateSelected({ [name]: Number(current + digits) });
31543
32505
  }
31544
32506
  }
31545
- }, true);
31546
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
32507
+ }, page !== "discover");
32508
+ if (viewport.maxHeight === 0 || viewport.bodyRows < 3) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("provider setup · terminal too small · esc back", viewport.contentColumns));
32509
+ if (page === "donor") {
32510
+ const stateRow = donorRows.length === 0 ? (0, import_react.createElement)(Text, {
32511
+ key: "empty",
32512
+ color: inkColor(getPalette().dim),
32513
+ wrap: "truncate-end"
32514
+ }, truncateColumns(" no model with declared efforts yet; declare one with e, or hand-write settings", viewport.contentColumns)) : (0, import_react.createElement)(Text, {
32515
+ key: "hint",
32516
+ color: inkColor(getPalette().dim),
32517
+ wrap: "truncate-end"
32518
+ }, truncateColumns(" copy verbatim into " + displayText(selected?.id ?? ""), viewport.contentColumns));
32519
+ const donorBudget = Math.max(0, viewport.bodyRows - 2);
32520
+ const donorFirst = selectionWindow(donorIndex, donorRows.length, donorBudget);
32521
+ const donorVisible = donorRows.slice(donorFirst, donorFirst + donorBudget);
32522
+ return (0, import_react.createElement)(Box, {
32523
+ flexDirection: "column",
32524
+ width: viewport.outerColumns,
32525
+ paddingX: 1,
32526
+ borderStyle: "round",
32527
+ borderColor: inkColor(getPalette().brand)
32528
+ }, (0, import_react.createElement)(Text, {
32529
+ color: inkColor(getPalette().brand),
32530
+ bold: true,
32531
+ wrap: "truncate-end"
32532
+ }, truncateColumns("/model — copy efforts", viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), stateRow, ...donorVisible.map((donor, index) => {
32533
+ const active = donorFirst + index === donorIndex;
32534
+ const label = (active ? ">" : " ") + " " + donor.provider + "/" + displayText(donor.id) + " · " + serializeReasoningEfforts(donor.efforts);
32535
+ return (0, import_react.createElement)(Text, {
32536
+ key: donor.provider + "/" + donor.id,
32537
+ color: active ? inkColor(getPalette().brandBright) : inkColor(getPalette().text),
32538
+ wrap: "truncate-end"
32539
+ }, truncateColumns(label, viewport.contentColumns));
32540
+ }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
32541
+ color: inkColor(getPalette().dim),
32542
+ wrap: "truncate-end"
32543
+ }, truncateColumns("↑↓ move · enter copy · esc back", viewport.contentColumns)));
32544
+ }
32545
+ if (page === "discover") return (0, import_react.createElement)(ProviderDiscoveryPanel, {
32546
+ target,
32547
+ baseURL,
32548
+ apiKey: keyDraft,
32549
+ configured: models.map((model) => model.id),
32550
+ discover,
32551
+ onAdopt: (adopted) => {
32552
+ const existing = new Set(models.map((model) => model.id));
32553
+ const fresh = adopted.filter((model) => !existing.has(model.id));
32554
+ if (fresh.length > 0) {
32555
+ setModels([...models, ...fresh.map((model) => ({
32556
+ id: model.id,
32557
+ ...model.name === void 0 ? {} : { name: model.name },
32558
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
32559
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
32560
+ }))]);
32561
+ setCursor(models.length);
32562
+ }
32563
+ setPage("setup");
32564
+ },
32565
+ back: () => setPage("setup"),
32566
+ onExit
32567
+ });
31547
32568
  const stateRows = error === void 0 ? [] : [(0, import_react.createElement)(Text, {
31548
32569
  key: "error",
31549
32570
  color: inkColor(getPalette().error),
31550
32571
  wrap: "truncate-end"
31551
- }, truncateColumns(` ${error}`, viewport.contentColumns))];
31552
- const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 1);
31553
- const first = selectionWindow(cursor, choices.length, rowBudget);
31554
- const visible = choices.slice(first, first + rowBudget);
32572
+ }, truncateColumns(" " + error, viewport.contentColumns))];
32573
+ const keyBullets = "•".repeat(Math.min([...keyDraft].length, Math.max(1, viewport.contentColumns - 14)));
32574
+ const keyRow = (0, import_react.createElement)(Text, {
32575
+ key: "key",
32576
+ color: zone === "key" ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
32577
+ wrap: "truncate-end"
32578
+ }, truncateColumns((" " + (zone === "key" ? ">" : " ") + " key " + keyBullets + (zone === "key" && !busy ? "▏" : "") + (keyDraft === "" ? " (" + keyStatus + ")" : busy ? " saving…" : "")).replace(/ +$/u, ""), viewport.contentColumns));
32579
+ const urlRow = (0, import_react.createElement)(Text, {
32580
+ key: "url",
32581
+ color: zone === "url" ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
32582
+ wrap: "truncate-end"
32583
+ }, truncateColumns(" " + (zone === "url" ? ">" : " ") + " url " + (baseURL === "" ? "(official default)" : baseURL) + (zone === "url" ? "▏" : ""), viewport.contentColumns));
32584
+ const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 3);
32585
+ const first = selectionWindow(cursor, models.length + 1, rowBudget);
32586
+ const modelRows = [];
32587
+ for (let index = first; index < first + Math.max(0, Math.min(models.length + 1 - first, rowBudget)); index += 1) {
32588
+ if (index >= models.length) {
32589
+ modelRows.push((0, import_react.createElement)(Text, {
32590
+ key: "add",
32591
+ color: cursor === index ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
32592
+ wrap: "truncate-end"
32593
+ }, truncateColumns(" " + (cursor === index ? ">" : " ") + " + add by id" + (addDraft === "" ? "" : " " + addDraft + "▏"), viewport.contentColumns)));
32594
+ continue;
32595
+ }
32596
+ const model = models[index];
32597
+ const active = index === cursor;
32598
+ const context = model.contextWindow === void 0 ? "-" : String(model.contextWindow);
32599
+ const output = model.maxTokens === void 0 ? "-" : String(model.maxTokens);
32600
+ const tail = active && effEditing ? " eff:" + effDraft + "▏" : " in:" + (active && field === "ctx" ? "[" + context + "]" : context) + " out:" + (active && field === "out" ? "[" + output + "]" : output) + " eff:" + effortsSummary(model);
32601
+ modelRows.push((0, import_react.createElement)(Text, {
32602
+ key: model.id,
32603
+ color: active ? inkColor(getPalette().brandBright) : inkColor(getPalette().success),
32604
+ wrap: "truncate-end"
32605
+ }, truncateColumns(" " + (active ? ">" : " ") + " [x] " + displayText(model.id) + tail, viewport.contentColumns)));
32606
+ }
31555
32607
  return (0, import_react.createElement)(Box, {
31556
32608
  flexDirection: "column",
31557
32609
  width: viewport.outerColumns,
@@ -31562,104 +32614,110 @@ function ProviderConfigurationPanel({ target, catalog, save, done, back }) {
31562
32614
  color: inkColor(getPalette().brand),
31563
32615
  bold: true,
31564
32616
  wrap: "truncate-end"
31565
- }, truncateColumns(`/model - ${target.displayName} configuration`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
31566
- color: focus === "url" ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
31567
- wrap: "truncate-end"
31568
- }, truncateColumns(` ${focus === "url" ? ">" : " "} endpoint: ${baseURL === "" ? "(adapter default)" : baseURL}`, viewport.contentColumns)), ...stateRows, ...visible.map((choice, index) => {
31569
- const absolute = first + index;
31570
- const model = models.find((item) => item.id === choice.id);
31571
- const selectedMark = model === void 0 ? "[ ]" : "[x]";
31572
- const context = model?.contextWindow === void 0 ? "-" : String(model.contextWindow);
31573
- const output = model?.maxTokens === void 0 ? "-" : String(model.maxTokens);
31574
- const active = absolute === cursor && focus !== "url";
31575
- return (0, import_react.createElement)(Text, {
31576
- key: choice.id,
31577
- color: active ? inkColor(getPalette().brandBright) : model === void 0 ? inkColor(getPalette().dim) : inkColor(getPalette().success),
31578
- wrap: "truncate-end"
31579
- }, truncateColumns(`${active ? ">" : " "} ${selectedMark} ${choice.name} in:${context} out:${output}`, viewport.contentColumns));
31580
- }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
32617
+ }, truncateColumns("/model configure " + target.displayName, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), keyRow, urlRow, ...stateRows, ...modelRows, (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
31581
32618
  color: inkColor(getPalette().dim),
31582
32619
  wrap: "truncate-end"
31583
- }, truncateColumns("tab endpoint/models/input/output - space select - arrows model - digits set window - enter save - esc back", viewport.contentColumns)));
32620
+ }, truncateColumns("↑↓ move · ←→ in/out · space remove · e efforts · c copy efforts · tab discover · enter save · esc back", viewport.contentColumns)));
31584
32621
  }
31585
- /** Write-only masked API-key editor; the secret lives only in this mounted component. */
31586
- function ProviderCredentialPanel({ target, save, done, back }) {
32622
+ /**
32623
+ * The discovery stage of the provider setup page: interrogates the endpoint
32624
+ * the drafts describe (typed key wins over the stored credential) and offers
32625
+ * the advertised models as a checkable list. Already-configured ids render
32626
+ * verified but untoggleable; Enter adopts every checked model back into the
32627
+ * setup page's list — selective adoption, never a bulk import.
32628
+ */
32629
+ function ProviderDiscoveryPanel({ target, baseURL, apiKey, configured, discover, onAdopt, back, onExit }) {
31587
32630
  const stdout = useStdout().stdout;
31588
32631
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
31589
- const [draft, setDraft] = (0, import_react.useState)("");
31590
- const [busy, setBusy] = (0, import_react.useState)(false);
32632
+ const [epoch, setEpoch] = (0, import_react.useState)(0);
32633
+ const [loading, setLoading] = (0, import_react.useState)(true);
31591
32634
  const [error, setError] = (0, import_react.useState)(void 0);
31592
- const submit = () => {
31593
- if (busy) return;
31594
- setBusy(true);
32635
+ const [rows, setRows] = (0, import_react.useState)([]);
32636
+ const [checked, setChecked] = (0, import_react.useState)(/* @__PURE__ */ new Set());
32637
+ const [cursor, setCursor] = (0, import_react.useState)(0);
32638
+ (0, import_react.useEffect)(() => {
32639
+ const controller = new AbortController();
32640
+ setLoading(true);
31595
32641
  setError(void 0);
31596
- Promise.resolve().then(() => save(target, draft)).then(() => {
31597
- setDraft("");
31598
- done();
32642
+ discover(target, {
32643
+ ...baseURL.trim() === "" ? {} : { baseURL: baseURL.trim() },
32644
+ ...apiKey.trim() === "" ? {} : { apiKey: apiKey.trim() }
32645
+ }, controller.signal).then((discovered) => {
32646
+ if (controller.signal.aborted) return;
32647
+ setRows(discovered);
32648
+ const alreadyKnown = new Set(configured);
32649
+ const firstNew = discovered.findIndex((model) => !alreadyKnown.has(model.id));
32650
+ setCursor(firstNew < 0 ? 0 : firstNew);
32651
+ setLoading(false);
31599
32652
  }, (reason) => {
32653
+ if (controller.signal.aborted) return;
31600
32654
  setError(singleLineText(reason instanceof Error ? reason.message : String(reason)));
31601
- setBusy(false);
32655
+ setLoading(false);
31602
32656
  });
31603
- };
32657
+ return () => {
32658
+ controller.abort();
32659
+ };
32660
+ }, [epoch]);
32661
+ const known = new Set(configured);
31604
32662
  useStableInput((input, key) => {
31605
- if (busy) return;
31606
- if (key.escape) {
31607
- setDraft("");
32663
+ if (key.escape || input === "q") {
31608
32664
  back();
31609
32665
  return;
31610
32666
  }
31611
- if (key.return) {
31612
- submit();
32667
+ if (key.ctrl && input === "c") {
32668
+ onExit();
31613
32669
  return;
31614
32670
  }
31615
- if (key.backspace || key.delete) {
31616
- setError(void 0);
31617
- setDraft((current) => [...current].slice(0, -1).join(""));
32671
+ if (input === "f") {
32672
+ setChecked(/* @__PURE__ */ new Set());
32673
+ setEpoch((current) => current + 1);
31618
32674
  return;
31619
32675
  }
31620
- if (key.ctrl && input === "u") {
31621
- setError(void 0);
31622
- setDraft("");
32676
+ if (loading || error !== void 0) return;
32677
+ if (rows.length === 0) return;
32678
+ if (key.upArrow) {
32679
+ setCursor((current) => current > 0 ? current - 1 : rows.length - 1);
31623
32680
  return;
31624
32681
  }
31625
- if (key.ctrl || key.meta || input.length === 0) return;
31626
- const next = draft + stripPasteMarkers(input);
31627
- if (next.length > 4096) {
31628
- setError("API key input is too long");
32682
+ if (key.downArrow) {
32683
+ setCursor((current) => current < rows.length - 1 ? current + 1 : 0);
31629
32684
  return;
31630
32685
  }
31631
- setError(void 0);
31632
- setDraft(next);
32686
+ const row = rows[cursor];
32687
+ if (row === void 0) return;
32688
+ if (input === " ") {
32689
+ if (known.has(row.id)) return;
32690
+ setChecked((current) => {
32691
+ const next = new Set(current);
32692
+ if (next.has(row.id)) next.delete(row.id);
32693
+ else next.add(row.id);
32694
+ return next;
32695
+ });
32696
+ return;
32697
+ }
32698
+ if (key.return) onAdopt(rows.filter((model) => checked.has(model.id)));
31633
32699
  }, true);
31634
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
31635
- const keyBudget = Math.max(1, viewport.contentColumns - 4);
31636
- const bullets = "".repeat(Math.min([...draft].length, keyBudget));
31637
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`API key ${bullets}${busy ? " saving…" : " ▏"} · esc back`, viewport.contentColumns));
31638
- const identity = target.displayName === target.provider ? target.provider : `${target.displayName} (${target.provider})`;
31639
- const source = target.credential?.kind === "facts" && target.credential.configured ? `replaces ${singleLineText(target.credential.source ?? "stored key")}` : "new key";
31640
- const providerRow = (0, import_react.createElement)(Text, {
31641
- key: "provider",
31642
- wrap: "truncate-end"
31643
- }, truncateColumns(` provider ${displayText(identity)}`, viewport.contentColumns));
31644
- const referenceRow = (0, import_react.createElement)(Text, {
31645
- key: "reference",
32700
+ if (viewport.maxHeight === 0) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("model discovery · terminal too small · esc back", viewport.contentColumns));
32701
+ const stateRows = loading ? [(0, import_react.createElement)(Text, {
32702
+ key: "loading",
31646
32703
  color: inkColor(getPalette().dim),
31647
32704
  wrap: "truncate-end"
31648
- }, truncateColumns(` reference ${displayText(target.credentialRef ?? target.suggestedRef)} · ${source}`, viewport.contentColumns));
31649
- const keyRow = (0, import_react.createElement)(Text, {
31650
- key: "key",
31651
- color: error === void 0 ? inkColor(getPalette().brandBright) : inkColor(getPalette().error),
31652
- wrap: "truncate-end"
31653
- }, truncateColumns(` key ${bullets}${busy ? " saving…" : " ▏"}`, viewport.contentColumns));
31654
- const errorRow = error === void 0 ? void 0 : (0, import_react.createElement)(Text, {
32705
+ }, truncateColumns(" discovering models…", viewport.contentColumns))] : error !== void 0 ? [(0, import_react.createElement)(Text, {
31655
32706
  key: "error",
31656
32707
  color: inkColor(getPalette().error),
31657
32708
  wrap: "truncate-end"
31658
- }, truncateColumns(` ${error}`, viewport.contentColumns));
31659
- const detailRows = errorRow === void 0 ? [providerRow, referenceRow] : [providerRow, errorRow];
31660
- const primaryRow = viewport.bodyRows === 1 && errorRow !== void 0 ? errorRow : keyRow;
31661
- const detailBudget = Math.max(0, viewport.bodyRows - 1);
31662
- const bodyRows = [...detailBudget === 0 ? [] : detailRows.slice(-detailBudget), ...viewport.bodyRows === 0 ? [] : [primaryRow]];
32709
+ }, truncateColumns(" " + error, viewport.contentColumns))] : rows.length === 0 ? [(0, import_react.createElement)(Text, {
32710
+ key: "empty",
32711
+ color: inkColor(getPalette().dim),
32712
+ wrap: "truncate-end"
32713
+ }, truncateColumns(" the endpoint advertised no models; add ids by hand on the setup page", viewport.contentColumns))] : [(0, import_react.createElement)(Text, {
32714
+ key: "summary",
32715
+ color: inkColor(getPalette().dim),
32716
+ wrap: "truncate-end"
32717
+ }, truncateColumns(" " + rows.length + " advertised · " + rows.filter((model) => !known.has(model.id)).length + " new · " + checked.size + " checked", viewport.contentColumns))];
32718
+ const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 1);
32719
+ const first = selectionWindow(cursor, rows.length, rowBudget);
32720
+ const visible = rows.slice(first, first + rowBudget);
31663
32721
  return (0, import_react.createElement)(Box, {
31664
32722
  flexDirection: "column",
31665
32723
  width: viewport.outerColumns,
@@ -31670,10 +32728,20 @@ function ProviderCredentialPanel({ target, save, done, back }) {
31670
32728
  color: inkColor(getPalette().brand),
31671
32729
  bold: true,
31672
32730
  wrap: "truncate-end"
31673
- }, truncateColumns("/model — add API key", viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...bodyRows, (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
32731
+ }, truncateColumns("/model — discover " + target.displayName, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...stateRows, ...visible.map((model, index) => {
32732
+ const active = first + index === cursor;
32733
+ const added = known.has(model.id);
32734
+ const mark = added ? "✓" : checked.has(model.id) ? "☑" : "☐";
32735
+ const label = (active ? ">" : " ") + " " + mark + " " + displayText(model.id) + (model.name === void 0 || model.name === model.id ? "" : " · " + displayText(model.name));
32736
+ return (0, import_react.createElement)(Text, {
32737
+ key: model.id,
32738
+ color: added ? inkColor(getPalette().dim) : active ? inkColor(getPalette().brandBright) : inkColor(getPalette().text),
32739
+ wrap: "truncate-end"
32740
+ }, truncateColumns(label, viewport.contentColumns));
32741
+ }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
31674
32742
  color: inkColor(getPalette().dim),
31675
32743
  wrap: "truncate-end"
31676
- }, truncateColumns("type or paste key · enter save · ctrl+u clear · esc back", viewport.contentColumns)));
32744
+ }, truncateColumns("↑↓ move · space check · enter adopt · f refetch · esc back", viewport.contentColumns)));
31677
32745
  }
31678
32746
  /** Bounded destructive-action confirmation for credential or provider removal. */
31679
32747
  function ProviderConfirmPanel({ target, kind, confirm, done, back }) {
@@ -31698,9 +32766,8 @@ function ProviderConfirmPanel({ target, kind, confirm, done, back }) {
31698
32766
  }
31699
32767
  if (input === "y") run();
31700
32768
  }, true);
31701
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
31702
32769
  const action = kind === "credential" ? "remove API key" : "remove provider";
31703
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`${action} ${target.displayName}? · y confirm · n/esc back`, viewport.contentColumns));
32770
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`${action} ${target.displayName}? · y confirm · n/esc back`, viewport.contentColumns));
31704
32771
  const identity = target.displayName === target.provider ? target.provider : `${target.displayName} (${target.provider})`;
31705
32772
  const identityRow = (0, import_react.createElement)(Text, {
31706
32773
  key: "identity",
@@ -31835,8 +32902,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31835
32902
  else if (input === "g") setScroll(0);
31836
32903
  else if (input === "G") setScroll(Math.max(0, content.length - viewport.bodyRows));
31837
32904
  });
31838
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
31839
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/help · esc/q close", viewport.contentColumns));
32905
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("/help · esc/q close", viewport.contentColumns));
31840
32906
  return (0, import_react.createElement)(Box, {
31841
32907
  flexDirection: "column",
31842
32908
  width: viewport.outerColumns,
@@ -31975,8 +33041,7 @@ function VerbosePanel({ entries, onClose }) {
31975
33041
  }
31976
33042
  if (input === "G") setScroll(Math.max(0, allLines.length - viewport.bodyRows));
31977
33043
  });
31978
- if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
31979
- if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("history details · ctrl+o / esc / q close", viewport.contentColumns));
33044
+ if (viewport.maxHeight === 0 || viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("history details · ctrl+o / esc / q close", viewport.contentColumns));
31980
33045
  const title = entries.length === 0 ? "history details · empty" : `history details · entry ${cursor + 1}/${entries.length} · lines ${allLines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(allLines.length, visibleScroll + viewport.bodyRows)}/${allLines.length}`;
31981
33046
  const visible = allLines.slice(visibleScroll, visibleScroll + viewport.bodyRows);
31982
33047
  return (0, import_react.createElement)(Box, {
@@ -32052,6 +33117,31 @@ function completionCandidates(value, descriptors, skills) {
32052
33117
  return all.filter((candidate) => candidate.label.slice(1).startsWith(prefix));
32053
33118
  }
32054
33119
  /**
33120
+ * Shared completion-menu geometry: the menu view and the App's dynamic-row
33121
+ * budget MUST derive the exact same physical height, or an open menu silently
33122
+ * overflows the terminal during streaming (the cursor creeps past the top and
33123
+ * the live region freezes). One helper, two consumers — never drift.
33124
+ */
33125
+ function completionMenuMetrics(terminalRows) {
33126
+ const showFooter = terminalRows >= 12;
33127
+ const verticalPadding = terminalRows >= 14 ? 1 : 0;
33128
+ return {
33129
+ limit: Math.max(1, Math.min(6, terminalRows - (showFooter ? 11 : 10) - verticalPadding * 2)),
33130
+ showFooter,
33131
+ verticalPadding
33132
+ };
33133
+ }
33134
+ /**
33135
+ * The menu's total physical row count at this terminal height: visible
33136
+ * candidates (or the single "searching…" row), the overflow marker, the
33137
+ * footer, and both padding rows.
33138
+ */
33139
+ function completionMenuRowCount(terminalRows, rowCount) {
33140
+ const { limit, showFooter, verticalPadding } = completionMenuMetrics(terminalRows);
33141
+ const visible = rowCount === 0 ? 1 : Math.min(rowCount, limit);
33142
+ return visible + ((rowCount === 0 ? 0 : rowCount - visible) > 0 ? 1 : 0) + (showFooter ? 1 : 0) + verticalPadding * 2;
33143
+ }
33144
+ /**
32055
33145
  * The completion menu, rendered inside the composer's subtree directly above
32056
33146
  * the composer band — attached the way Claude-Code anchors its dropdown. Opening
32057
33147
  * it grows the stack downward: the composer stays the last element on screen
@@ -32060,7 +33150,7 @@ function completionCandidates(value, descriptors, skills) {
32060
33150
  * editor's live completion state, so no cross-component effect ever resyncs
32061
33151
  * it (a state lift here previously deadlocked the menu after a resize).
32062
33152
  */
32063
- function CompletionMenu({ active, mention, index, rows }) {
33153
+ function CompletionMenu({ active, mention, index, rows, error }) {
32064
33154
  const stdout = useStdout().stdout;
32065
33155
  const columns = stdout?.columns ?? 80;
32066
33156
  const terminalRows = stdout?.rows ?? 30;
@@ -32068,9 +33158,7 @@ function CompletionMenu({ active, mention, index, rows }) {
32068
33158
  const contentColumns = Math.max(1, columns - 4);
32069
33159
  const nameWidth = mention ? Math.max(1, contentColumns - 2) : Math.min(18, Math.max(1, contentColumns - 2), Math.max(0, ...rows.map((row) => visibleColumns(row.label))) + 2);
32070
33160
  const descBudget = Math.max(0, contentColumns - nameWidth - 2);
32071
- const showFooter = terminalRows >= 12;
32072
- const verticalPadding = terminalRows >= 14 ? 1 : 0;
32073
- const limit = Math.max(1, Math.min(6, terminalRows - (showFooter ? 11 : 10) - verticalPadding * 2));
33161
+ const { limit, showFooter, verticalPadding } = completionMenuMetrics(terminalRows);
32074
33162
  const selected = rows.length === 0 ? 0 : index % rows.length;
32075
33163
  const first = selectionWindow(selected, rows.length, limit);
32076
33164
  const visible = rows.slice(first, first + limit);
@@ -32079,10 +33167,14 @@ function CompletionMenu({ active, mention, index, rows }) {
32079
33167
  flexDirection: "column",
32080
33168
  marginLeft: 2,
32081
33169
  paddingY: verticalPadding
32082
- }, ...rows.length === 0 ? [(0, import_react.createElement)(Text, {
33170
+ }, ...rows.length === 0 ? [error === void 0 ? (0, import_react.createElement)(Text, {
32083
33171
  key: "loading",
32084
33172
  dimColor: true
32085
- }, "searching…")] : visible.map((candidate, at) => {
33173
+ }, "searching…") : (0, import_react.createElement)(Text, {
33174
+ key: "error",
33175
+ color: inkColor(getPalette().error),
33176
+ wrap: "truncate-end"
33177
+ }, truncateColumns(`workspace search unavailable: ${singleLineText(error)} · keep typing to retry`, contentColumns))] : visible.map((candidate, at) => {
32086
33178
  const absolute = first + at;
32087
33179
  return (0, import_react.createElement)(Text, {
32088
33180
  key: candidate.label,
@@ -32104,8 +33196,9 @@ function CompletionMenu({ active, mention, index, rows }) {
32104
33196
  * While a modal (approval / question / model panel) owns the keys, the
32105
33197
  * box passes every key through untouched.
32106
33198
  */
32107
- 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, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
33199
+ 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, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows, onMenuRows }) {
32108
33200
  const columns = useStdout().stdout?.columns ?? 80;
33201
+ const inputTerminalRows = useStdout().stdout?.rows ?? 30;
32109
33202
  const editorColumns = Math.max(1, columns - 6);
32110
33203
  const stdin = useStdin().stdin;
32111
33204
  const focusReporting = isVsCodeTerminalEnv();
@@ -32210,6 +33303,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32210
33303
  };
32211
33304
  const mentionActive = mentionToken !== void 0;
32212
33305
  const [mentionRows, setMentionRows] = (0, import_react.useState)([]);
33306
+ /** Latest mention-discovery failure; shown in the menu instead of an empty list. */
33307
+ const [mentionError, setMentionError] = (0, import_react.useState)(void 0);
32213
33308
  const mentionRequestRef = (0, import_react.useRef)(0);
32214
33309
  const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
32215
33310
  const uniqueImageMarker = (name, source, reserved = []) => {
@@ -32286,14 +33381,21 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32286
33381
  mentionRequestRef.current = requestId;
32287
33382
  if (!active || !mentionActive) {
32288
33383
  setMentionRows([]);
33384
+ setMentionError(void 0);
32289
33385
  return;
32290
33386
  }
33387
+ setMentionError(void 0);
32291
33388
  const controller = new AbortController();
32292
33389
  const query = mentionToken.query;
32293
33390
  const timer = setTimeout(() => {
32294
33391
  loadMentions(query, controller.signal).then((rows) => {
32295
33392
  if (!controller.signal.aborted && mentionRequestRef.current === requestId) setMentionRows(rows);
32296
- }, () => {});
33393
+ }, (reason) => {
33394
+ if (!controller.signal.aborted && mentionRequestRef.current === requestId) {
33395
+ setMentionRows([]);
33396
+ setMentionError(reason instanceof Error ? reason.message : String(reason));
33397
+ }
33398
+ });
32297
33399
  }, 50);
32298
33400
  return () => {
32299
33401
  clearTimeout(timer);
@@ -32311,6 +33413,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32311
33413
  description: row.description,
32312
33414
  origin: "mention"
32313
33415
  })) : candidates;
33416
+ const menuHeightRows = menuActive ? completionMenuRowCount(inputTerminalRows, menuRows.length) : 0;
32314
33417
  /** Accept the highlighted completion-menu candidate into the draft. */
32315
33418
  const acceptMenuCandidate = () => {
32316
33419
  if (mentionActive && mentionToken !== void 0) {
@@ -32545,8 +33648,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32545
33648
  return;
32546
33649
  }
32547
33650
  if (key.delete && liveValue === "" && queued.length > 0) {
32548
- cancelQueued(queued[queued.length - 1].messageId);
32549
- return;
33651
+ if (rawEditorTokens.current?.some((token) => token.kind === "delete-forward" || token.kind === "delete-word-forward") === true) {
33652
+ cancelQueued(queued[queued.length - 1].messageId);
33653
+ return;
33654
+ }
32550
33655
  }
32551
33656
  if (key.return) {
32552
33657
  if (pasteBracketRef.current) {
@@ -32559,7 +33664,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32559
33664
  return;
32560
33665
  }
32561
33666
  }
32562
- const text = liveValue.trim();
33667
+ const trimmed = liveValue.trim();
33668
+ const text = submissionPayload(liveValue);
32563
33669
  if (draftImagesRef.current.length > 0) {
32564
33670
  const controller = new AbortController();
32565
33671
  const epoch = prepareEpochRef.current + 1;
@@ -32581,7 +33687,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32581
33687
  setCompletionIndex(0);
32582
33688
  setDismissedMenuValue(void 0);
32583
33689
  dismissNotice();
32584
- if (text !== "") {
33690
+ if (trimmed !== "") {
32585
33691
  recordLocal(text);
32586
33692
  recordHistory(text);
32587
33693
  }
@@ -32603,7 +33709,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32603
33709
  resetCursorBlink();
32604
33710
  setCompletionIndex(0);
32605
33711
  setDismissedMenuValue(void 0);
32606
- if (text === "") return;
33712
+ if (trimmed === "") return;
32607
33713
  dismissNotice();
32608
33714
  if (!text.startsWith("/")) {
32609
33715
  recordLocal(text);
@@ -32898,6 +34004,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32898
34004
  (0, import_react.useEffect)(() => {
32899
34005
  onEditorRows(editorRowCount);
32900
34006
  }, [editorRowCount, onEditorRows]);
34007
+ (0, import_react.useEffect)(() => {
34008
+ onMenuRows(menuHeightRows);
34009
+ return () => onMenuRows(0);
34010
+ }, [menuHeightRows, onMenuRows]);
32901
34011
  const bandWidth = Math.max(1, columns - 1);
32902
34012
  const bandBg = inkColor(getPalette().composerBand);
32903
34013
  const bandFill = (consumed) => " ".repeat(Math.max(0, bandWidth - consumed));
@@ -32932,7 +34042,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32932
34042
  active: menuActive,
32933
34043
  mention: mentionActive,
32934
34044
  index: completionIndex,
32935
- rows: menuRows
34045
+ rows: menuRows,
34046
+ error: mentionActive ? mentionError : void 0
32936
34047
  });
32937
34048
  const editorRows = [];
32938
34049
  for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
@@ -33206,6 +34317,7 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
33206
34317
  /** The whole terminal app; state arrives via the store, output via Ink. */
33207
34318
  function App(props) {
33208
34319
  const view = (0, import_react.useSyncExternalStore)(props.store.subscribe, props.store.getView);
34320
+ useStableInput(() => {}, true);
33209
34321
  const readDescriptors = (0, import_react.useCallback)(() => props.commands.descriptors, [props.commands]);
33210
34322
  const readSkills = (0, import_react.useCallback)(() => props.skills.rows, [props.skills]);
33211
34323
  const descriptors = (0, import_react.useSyncExternalStore)(props.commands.subscribe, readDescriptors);
@@ -33501,11 +34613,16 @@ function App(props) {
33501
34613
  const handleEditorRows = (0, import_react.useCallback)((rows) => {
33502
34614
  setComposerRows((current) => current === rows ? current : rows);
33503
34615
  }, []);
34616
+ const [menuRows, setMenuRows] = (0, import_react.useState)(0);
34617
+ const handleMenuRows = (0, import_react.useCallback)((rows) => {
34618
+ setMenuRows((current) => current === rows ? current : rows);
34619
+ }, []);
33504
34620
  const composerEditorCap = composerMaxRows(terminalRows);
33505
- const dynamicRows = Math.max(1, terminalRows - 13 - composerGutterRows - (composerRows - 1));
34621
+ const MENU_RESERVE_ROWS = 5;
34622
+ const dynamicRows = Math.max(1, terminalRows - 8 - MENU_RESERVE_ROWS - composerGutterRows - (composerRows - 1) - Math.max(0, menuRows - MENU_RESERVE_ROWS));
33506
34623
  const streamingActive = view.streaming !== "" || view.streamingReasoning !== "";
33507
34624
  const deepDivingVisible = busy && !streamingActive;
33508
- const allLiveLines = (0, import_react.useMemo)(() => view.entries.slice(settled).flatMap((entry) => transcriptEntryLines(entry, Math.max(10, terminalColumns - 2), showReasoning)), [
34625
+ const allLiveLines = (0, import_react.useMemo)(() => view.entries.slice(settled).flatMap((entry) => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2), showReasoning)), [
33509
34626
  view.entries,
33510
34627
  settled,
33511
34628
  terminalColumns,
@@ -33581,6 +34698,37 @@ function App(props) {
33581
34698
  setProviderAction(void 0);
33582
34699
  setEffortFor(void 0);
33583
34700
  };
34701
+ const effortDonors = (0, import_react.useMemo)(() => {
34702
+ const donors = [];
34703
+ const seen = /* @__PURE__ */ new Set();
34704
+ for (const row of providerDirectory?.rows ?? []) for (const model of row.configuration.models) {
34705
+ const raw = model.extras?.reasoningEfforts;
34706
+ if (!isDeclaredReasoningEfforts(raw)) continue;
34707
+ const key = row.provider + "/" + model.id;
34708
+ if (seen.has(key)) continue;
34709
+ seen.add(key);
34710
+ donors.push({
34711
+ provider: row.provider,
34712
+ id: model.id,
34713
+ efforts: raw
34714
+ });
34715
+ }
34716
+ for (const row of directory?.rows ?? []) {
34717
+ const levels = row.reasoning?.efforts.map((effort) => effort.id) ?? [];
34718
+ if (levels.filter((level) => level !== "off").length === 0) continue;
34719
+ const key = row.provider + "/" + row.model;
34720
+ if (seen.has(key)) continue;
34721
+ seen.add(key);
34722
+ const efforts = {};
34723
+ for (const level of levels) efforts[level] = level === "off" ? null : level;
34724
+ donors.push({
34725
+ provider: row.provider,
34726
+ id: row.model,
34727
+ efforts
34728
+ });
34729
+ }
34730
+ return donors;
34731
+ }, [providerDirectory, directory]);
33584
34732
  let modelSurface;
33585
34733
  if (modelOpen && !approvalPending && !questionPending) {
33586
34734
  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, {
@@ -33613,30 +34761,23 @@ function App(props) {
33613
34761
  },
33614
34762
  back: () => setProviderAction(void 0)
33615
34763
  });
33616
- else if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfigurationPanel, {
34764
+ else if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderSetupPanel, {
33617
34765
  target: providerAction.target,
33618
- catalog: directory?.rows ?? [],
34766
+ effortDonors,
33619
34767
  save: props.saveModelProviderConfiguration,
33620
- done: () => {
34768
+ saveCredential: props.saveModelProviderCredential,
34769
+ discover: props.discoverModelProvider ?? (async () => {
34770
+ throw new Error("model discovery is unavailable in this profile; enter models by hand");
34771
+ }),
34772
+ done: (result) => {
33621
34773
  const target = providerAction.target;
33622
34774
  setProviderAction(void 0);
33623
34775
  setProviderOpen(true);
33624
34776
  reloadModelSurfaces();
33625
- notify(`provider configuration saved: ${target.displayName}`);
33626
- },
33627
- back: () => setProviderAction(void 0)
33628
- });
33629
- else if (providerAction?.kind === "credential" && props.saveModelProviderCredential !== void 0) modelSurface = (0, import_react.createElement)(ProviderCredentialPanel, {
33630
- target: providerAction.target,
33631
- save: props.saveModelProviderCredential,
33632
- done: () => {
33633
- const target = providerAction.target;
33634
- setProviderAction(void 0);
33635
- setProviderOpen(false);
33636
- reloadModelSurfaces();
33637
- notify(`API key saved for ${target.displayName}; select a model`);
34777
+ notify(`provider configuration saved: ${target.displayName}` + (result.key ? " · API key updated" : ""));
33638
34778
  },
33639
- back: () => setProviderAction(void 0)
34779
+ back: () => setProviderAction(void 0),
34780
+ onExit: closeModelSurface
33640
34781
  });
33641
34782
  else if (providerAction?.kind === "unset" && props.unsetModelProviderCredential !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfirmPanel, {
33642
34783
  target: providerAction.target,
@@ -33669,16 +34810,6 @@ function App(props) {
33669
34810
  error: providerError,
33670
34811
  authorizations: authorizationDirectory,
33671
34812
  authorizationError,
33672
- onCredential: (target) => {
33673
- if (props.saveModelProviderCredential === void 0) {
33674
- notify("API key storage is unavailable in this profile", "warning");
33675
- return;
33676
- }
33677
- setProviderAction({
33678
- kind: "credential",
33679
- target
33680
- });
33681
- },
33682
34813
  onConfigure: (target) => {
33683
34814
  if (props.saveModelProviderConfiguration === void 0) {
33684
34815
  notify("provider configuration is unavailable in this profile", "warning");
@@ -33736,14 +34867,16 @@ function App(props) {
33736
34867
  });
33737
34868
  },
33738
34869
  onRetry: reloadModelSurfaces,
33739
- onBack: () => setProviderOpen(false)
34870
+ onBack: () => setProviderOpen(false),
34871
+ onExit: closeModelSurface
33740
34872
  });
33741
34873
  else if (effortFor !== void 0) modelSurface = (0, import_react.createElement)(EffortPanel, {
33742
34874
  key: `${effortFor.provider}/${effortFor.model}`,
33743
34875
  row: effortFor,
33744
34876
  current: effortLabel,
33745
34877
  select: (effortId) => applyModel(effortFor, effortId),
33746
- back: () => setEffortFor(void 0)
34878
+ back: () => setEffortFor(void 0),
34879
+ onExit: closeModelSurface
33747
34880
  });
33748
34881
  else modelSurface = (0, import_react.createElement)(ModelPanel, {
33749
34882
  directory,
@@ -33757,7 +34890,7 @@ function App(props) {
33757
34890
  const effortId = row.reasoning?.efforts.length === 1 ? row.reasoning.efforts[0].id : void 0;
33758
34891
  applyModel(row, effortId);
33759
34892
  },
33760
- ...props.loadModelProviders === void 0 || props.saveModelProviderCredential === void 0 ? {} : { onProviders: () => setProviderOpen(true) },
34893
+ ...props.loadModelProviders === void 0 || props.saveModelProviderConfiguration === void 0 ? {} : { onProviders: () => setProviderOpen(true) },
33761
34894
  onRetry: reloadModelSurfaces,
33762
34895
  onClose: closeModelSurface
33763
34896
  });
@@ -33797,7 +34930,9 @@ function App(props) {
33797
34930
  }), (0, import_react.createElement)(ApprovalBar, {
33798
34931
  snapshot: approvalSnapshot,
33799
34932
  locked: questionPending,
33800
- notify
34933
+ notify,
34934
+ interrupt: props.interrupt,
34935
+ summarize: questionPending
33801
34936
  }), modelSurface, helpOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(HelpPanel, {
33802
34937
  descriptors,
33803
34938
  skills,
@@ -34032,7 +35167,8 @@ function App(props) {
34032
35167
  waveTier,
34033
35168
  waveStyle,
34034
35169
  maxRows: composerEditorCap,
34035
- onEditorRows: handleEditorRows
35170
+ onEditorRows: handleEditorRows,
35171
+ onMenuRows: handleMenuRows
34036
35172
  }), (0, import_react.createElement)(StatusLine, {
34037
35173
  facts: {
34038
35174
  model: modelLabel,
@@ -34097,6 +35233,9 @@ function mountApprovalAnswerer(ctx, owns, preview) {
34097
35233
  if (request.signal?.aborted === true) return Promise.resolve("cancelled");
34098
35234
  let resolved = false;
34099
35235
  let settle;
35236
+ const settled = new Promise((resolve) => {
35237
+ settle = resolve;
35238
+ });
34100
35239
  const signal = request.signal;
34101
35240
  const onAbort = () => withdraw();
34102
35241
  const detachAbort = () => {
@@ -34129,9 +35268,7 @@ function mountApprovalAnswerer(ctx, owns, preview) {
34129
35268
  };
34130
35269
  queue.push(slot);
34131
35270
  publish();
34132
- return new Promise((resolve) => {
34133
- settle = resolve;
34134
- }).then((outcome) => {
35271
+ return settled.then((outcome) => {
34135
35272
  if (outcome !== "cancelled") {
34136
35273
  removeSlot(slot);
34137
35274
  publish();
@@ -34152,60 +35289,143 @@ function mountApprovalAnswerer(ctx, owns, preview) {
34152
35289
  };
34153
35290
  }
34154
35291
  //#endregion
34155
- //#region src/commands.ts
35292
+ //#region src/input-split.ts
34156
35293
  /**
34157
- * Watch the live command registry. Reads the current list immediately and
34158
- * re-reads on every registry mutation or agent retarget; notification
34159
- * failures are contained by the registry itself, so this watcher only ever
34160
- * re-reads. Without a `commands` service the view stays empty and all lines
34161
- * fall through to normal prompts.
34162
- * @param ctx - context carrying the `commands` service (optional).
34163
- * @returns the view the completion menu subscribes to.
35294
+ * Terminal input arrives as byte chunks, and one chunk can carry several
35295
+ * keypresses: a fast space-then-enter, a bridged stdin that batches reads, a
35296
+ * middle-click paste. Ink parses each chunk as exactly one keypress
35297
+ * `parseKeypress(' \r')` matches neither member, so both keys silently
35298
+ * vanish (a multi-select question answered with an empty set). The splitter
35299
+ * below cuts every chunk into the individual keypress units Ink's parser
35300
+ * expects, keeping escape sequences and bracketed-paste blocks intact, and
35301
+ * the stdin proxy feeds the split stream to the Ink mount.
35302
+ *
35303
+ * @module @deepseek-ai/dsh-tui/input-split
34164
35304
  */
34165
- function watchCommands(ctx) {
34166
- const commands = ctx.get("commands");
34167
- let agent;
34168
- let descriptors = [];
34169
- let error;
34170
- const listeners = /* @__PURE__ */ new Set();
34171
- const refresh = () => {
34172
- if (commands === void 0 || agent === void 0) return;
34173
- try {
34174
- descriptors = commands.list(agent);
34175
- error = void 0;
34176
- } catch (cause) {
34177
- descriptors = [...descriptors];
34178
- error = cause instanceof Error ? cause.message : String(cause);
35305
+ /** Bracketed-paste wrapper bytes; the whole block travels as one unit. */
35306
+ const PASTE_START = "\x1B[200~";
35307
+ const PASTE_END = "\x1B[201~";
35308
+ /** Final byte of a CSI sequence (\x40-\x7e per ECMA-48). */
35309
+ const isCsiFinal = (char) => char >= "@" && char <= "~";
35310
+ /**
35311
+ * Build a stateful chunk splitter. A partial unit at the end of one chunk
35312
+ * (a cut CSI sequence, an open paste block) waits in the buffer for the
35313
+ * rest. A chunk-trailing lone ESC emits as the Escape key right away:
35314
+ * terminals send Escape as its own chunk, and holding it hostage for a
35315
+ * sequence that may never continue would break every Esc cancel.
35316
+ */
35317
+ function createKeypressSplitter() {
35318
+ let buffer = "";
35319
+ const push = (chunk) => {
35320
+ buffer += chunk;
35321
+ const units = [];
35322
+ while (buffer !== "") {
35323
+ if (buffer.startsWith(PASTE_START)) {
35324
+ const end = buffer.indexOf(PASTE_END, 6);
35325
+ if (end < 0) break;
35326
+ const stop = end + 6;
35327
+ units.push(buffer.slice(0, stop));
35328
+ buffer = buffer.slice(stop);
35329
+ continue;
35330
+ }
35331
+ const head = buffer[0];
35332
+ if (head !== "\x1B") {
35333
+ const take = head >= "\ud800" && head <= "\udbff" && buffer[1] !== void 0 ? 2 : 1;
35334
+ units.push(buffer.slice(0, take));
35335
+ buffer = buffer.slice(take);
35336
+ continue;
35337
+ }
35338
+ if (buffer[1] === "[") {
35339
+ let end = -1;
35340
+ for (let at = 2; at < buffer.length; at += 1) if (isCsiFinal(buffer[at])) {
35341
+ end = at;
35342
+ break;
35343
+ }
35344
+ if (end < 0) break;
35345
+ units.push(buffer.slice(0, end + 1));
35346
+ buffer = buffer.slice(end + 1);
35347
+ continue;
35348
+ }
35349
+ if (buffer[1] === "O") {
35350
+ if (buffer[2] === void 0) break;
35351
+ units.push(buffer.slice(0, 3));
35352
+ buffer = buffer.slice(3);
35353
+ continue;
35354
+ }
35355
+ if (buffer[1] === void 0) {
35356
+ units.push(buffer);
35357
+ buffer = "";
35358
+ continue;
35359
+ }
35360
+ units.push(buffer.slice(0, 2));
35361
+ buffer = buffer.slice(2);
34179
35362
  }
34180
- for (const listener of listeners) listener();
35363
+ return units;
34181
35364
  };
34182
- if (commands !== void 0) ctx.on("commands/change", () => refresh());
34183
35365
  return {
34184
- get descriptors() {
34185
- return descriptors;
34186
- },
34187
- get error() {
34188
- return error;
34189
- },
34190
- subscribe(listener) {
34191
- listeners.add(listener);
34192
- return () => {
34193
- listeners.delete(listener);
34194
- };
35366
+ push,
35367
+ openPaste() {
35368
+ return buffer.startsWith(PASTE_START);
34195
35369
  },
34196
- setAgent(next) {
34197
- agent = next;
34198
- refresh();
35370
+ releaseStalePaste() {
35371
+ if (!buffer.startsWith(PASTE_START)) return [];
35372
+ buffer = buffer.slice(6);
35373
+ return push("");
34199
35374
  }
34200
35375
  };
34201
35376
  }
34202
35377
  /**
34203
- * Whether one command line is a syntactically valid slash command.
34204
- * @param line - the complete candidate line.
34205
- * @returns true when the line parses as `/name` or `/name input`.
35378
+ * Wrap one real stdin in the splitting proxy: keypress units flow into a
35379
+ * PassThrough Ink reads, while raw-mode/ref calls forward to the source.
35380
+ * @param source - the process (or harness) input stream in raw mode.
35381
+ * @returns the proxy stream plus a dispose that detaches the tap.
34206
35382
  */
34207
- function isSlashLine(line) {
34208
- return /^\/[a-z][a-z0-9_-]*(?=$|[\t ])/u.test(line);
35383
+ function createSplitStdin(source) {
35384
+ const stream = new PassThrough({ objectMode: true });
35385
+ const splitter = createKeypressSplitter();
35386
+ let stalePasteTimer;
35387
+ const disarmStalePasteTimer = () => {
35388
+ if (stalePasteTimer === void 0) return;
35389
+ clearTimeout(stalePasteTimer);
35390
+ stalePasteTimer = void 0;
35391
+ };
35392
+ const armStalePasteTimer = () => {
35393
+ if (stalePasteTimer !== void 0 || !splitter.openPaste()) return;
35394
+ stalePasteTimer = setTimeout(() => {
35395
+ stalePasteTimer = void 0;
35396
+ for (const unit of splitter.releaseStalePaste()) stream.write(unit);
35397
+ armStalePasteTimer();
35398
+ }, PASTE_BRACKET_TIMEOUT_MS);
35399
+ stalePasteTimer.unref?.();
35400
+ };
35401
+ const onChunk = (chunk) => {
35402
+ for (const unit of splitter.push(String(chunk))) stream.write(unit);
35403
+ if (splitter.openPaste()) armStalePasteTimer();
35404
+ else disarmStalePasteTimer();
35405
+ };
35406
+ const proxy = Object.assign(stream, {
35407
+ isTTY: source.isTTY === true,
35408
+ setRawMode(value) {
35409
+ source.setRawMode?.(value);
35410
+ return stream;
35411
+ },
35412
+ ref() {
35413
+ source.ref?.();
35414
+ },
35415
+ unref() {
35416
+ source.unref?.();
35417
+ }
35418
+ });
35419
+ source.setEncoding("utf8");
35420
+ source.on("data", onChunk);
35421
+ return {
35422
+ stdin: proxy,
35423
+ dispose() {
35424
+ disarmStalePasteTimer();
35425
+ source.removeListener("data", onChunk);
35426
+ source.pause();
35427
+ }
35428
+ };
34209
35429
  }
34210
35430
  //#endregion
34211
35431
  //#region src/internals.ts
@@ -34222,14 +35442,23 @@ const internals = {
34222
35442
  const keyboardEnhanced = shouldEnableKeyboardEnhancement();
34223
35443
  const focusReporting = isVsCodeTerminalEnv();
34224
35444
  process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
34225
- const instance = render(element, { exitOnCtrlC: false });
35445
+ const tuiStdin = createSplitStdin(process.stdin);
35446
+ const instance = render(element, {
35447
+ exitOnCtrlC: false,
35448
+ stdin: tuiStdin.stdin,
35449
+ stdout: process.stdout
35450
+ });
34226
35451
  return {
34227
35452
  rerender(element) {
34228
35453
  instance.rerender(element);
34229
35454
  },
34230
35455
  unmount() {
34231
- instance.unmount();
34232
- process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
35456
+ try {
35457
+ instance.unmount();
35458
+ } finally {
35459
+ tuiStdin.dispose();
35460
+ process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
35461
+ }
34233
35462
  }
34234
35463
  };
34235
35464
  },
@@ -34388,18 +35617,18 @@ const PI_AI_SETTINGS_NS = "llm-pi-ai";
34388
35617
  /** The effort level that means "send no reasoning parameter"; its wire value is always null. */
34389
35618
  const OFF_LEVEL = "off";
34390
35619
  /** Single-line a misbehaving error for the notice channel. */
34391
- function singleLine$1(message) {
35620
+ function singleLine(message) {
34392
35621
  return [...message].map((ch) => {
34393
35622
  const code = ch.charCodeAt(0);
34394
35623
  return code < 32 || code === 127 ? " " : ch;
34395
35624
  }).join("").split(" ").filter((part) => part !== "").join(" ");
34396
35625
  }
34397
35626
  /** Human text for a rejection value. */
34398
- function messageOf$1(error) {
35627
+ function messageOf(error) {
34399
35628
  return error instanceof Error ? error.message : String(error);
34400
35629
  }
34401
35630
  /** Read the value at a path through plain objects; undefined when any segment misses. */
34402
- function getPath$1(value, path) {
35631
+ function getPath(value, path) {
34403
35632
  let current = value;
34404
35633
  for (const segment of path) {
34405
35634
  if (typeof current !== "object" || current === null) return void 0;
@@ -34538,7 +35767,7 @@ async function syncModelCapabilities(ctx, notify) {
34538
35767
  for (const entry of routed) {
34539
35768
  const namespace = namespaces.get(entry.settingsNs);
34540
35769
  if (namespace === void 0) continue;
34541
- const profile = entry.settingsPath.length === 0 ? namespace.value : getPath$1(namespace.value, entry.settingsPath);
35770
+ const profile = entry.settingsPath.length === 0 ? namespace.value : getPath(namespace.value, entry.settingsPath);
34542
35771
  const models = Array.isArray(profile?.models) ? profile.models : void 0;
34543
35772
  if (models === void 0) continue;
34544
35773
  profiles.set(entry.provider, {
@@ -34572,376 +35801,24 @@ async function syncModelCapabilities(ctx, notify) {
34572
35801
  const more = plan.sources.length > 3 ? `, +${plan.sources.length - 3}` : "";
34573
35802
  notify?.(`inherited reasoning levels for ${plan.inherited.length} model${plan.inherited.length === 1 ? "" : "s"} on ${plan.provider} (from ${shown}${more})`, "info");
34574
35803
  } catch (error) {
34575
- notify?.(`capability inheritance failed on ${plan.provider}: ${singleLine$1(messageOf$1(error))}`, "warning");
35804
+ notify?.(`capability inheritance failed on ${plan.provider}: ${singleLine(messageOf(error))}`, "warning");
34576
35805
  }
34577
35806
  }
34578
35807
  } catch {}
34579
35808
  }
34580
35809
  //#endregion
34581
- //#region src/provider-settings.ts
34582
- /** Human text for a rejection value (mirrors the web page's `messageOf`). */
34583
- function messageOf(error) {
34584
- return error instanceof Error ? error.message : String(error);
34585
- }
34586
- /** Collapse every whitespace/control run to one space so a notice stays one line. */
34587
- function singleLine(message) {
34588
- return message.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim();
34589
- }
34590
- /** Keep a misbehaving credential provider from reflecting the submitted secret. */
34591
- function credentialWriteMessage(error, secret) {
34592
- const message = singleLine(messageOf(error));
34593
- return message.includes(secret) ? "credentials service rejected the API key" : message;
34594
- }
34595
- /** Obvious shell-assignment paste; mirrors the official Web Models editor. */
34596
- const ENV_ASSIGNMENT = /^[A-Z][A-Z0-9_]*=[^=]/;
34597
- /** Whether the whole draft is wrapped in one matching quote pair. */
34598
- function hasWrappingQuotes(value) {
34599
- const first = value[0];
34600
- return (first === "\"" || first === "'" || first === "`") && value.length > 1 && value.endsWith(first);
34601
- }
34602
- /** Read the value at a path through plain objects; undefined when any segment misses. */
34603
- function getPath(value, path) {
34604
- let current = value;
34605
- for (const segment of path) {
34606
- if (typeof current !== "object" || current === null) return void 0;
34607
- current = current[segment];
34608
- }
34609
- return current;
34610
- }
34611
- /** Whether a path resolves to a defined value (the empty path reads the root). */
34612
- function hasPath(value, path) {
34613
- return path.length === 0 ? value !== void 0 : getPath(value, path) !== void 0;
34614
- }
34615
- /** The credential reference a resolved profile names (its `apiKeyEnv` field). */
34616
- function profileRefOf(profile) {
34617
- if (typeof profile !== "object" || profile === null) return void 0;
34618
- const ref = profile.apiKeyEnv;
34619
- return typeof ref === "string" && ref.length > 0 ? ref : void 0;
34620
- }
34621
- /** Extract only fields the terminal can round-trip without touching provider-specific extras. */
34622
- function configurationOf(profile) {
34623
- if (typeof profile !== "object" || profile === null) return { models: [] };
34624
- const record = profile;
34625
- const models = (Array.isArray(record.models) ? record.models : []).flatMap((value) => {
34626
- if (typeof value !== "object" || value === null) return [];
34627
- const entry = value;
34628
- if (typeof entry.id !== "string" || entry.id.trim() === "") return [];
34629
- const { id: _id, name: _name, contextWindow: _contextWindow, maxTokens: _maxTokens, ...extras } = entry;
34630
- return [{
34631
- id: entry.id,
34632
- ...typeof entry.name === "string" && entry.name.trim() !== "" ? { name: entry.name } : {},
34633
- ...typeof entry.contextWindow === "number" && Number.isFinite(entry.contextWindow) ? { contextWindow: entry.contextWindow } : {},
34634
- ...typeof entry.maxTokens === "number" && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {},
34635
- ...Object.keys(extras).length > 0 ? { extras } : {}
34636
- }];
34637
- });
34638
- return {
34639
- ...typeof record.baseURL === "string" && record.baseURL.trim() !== "" ? { baseURL: record.baseURL } : {},
34640
- models
34641
- };
34642
- }
34643
- /**
34644
- * The conventional credential reference for a provider route: `<ROUTE>_API_KEY`
34645
- * with the route uppercased and every non-alphanumeric run collapsed to one
34646
- * underscore — the exact derivation the official Models page uses
34647
- * (`deriveKeyRef` in `ui-settings-models`), so a key saved here is found there.
34648
- * @param provider - provider route id (e.g. `pi-ai`, `minimax-cn`).
34649
- * @returns the derived reference name (e.g. `PI_AI_API_KEY`).
34650
- */
34651
- function deriveCredentialRef(provider) {
34652
- return `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
34653
- }
34654
- /** Events that invalidate the official Models provider/settings/credential join. */
34655
- const PROVIDER_SETTINGS_EVENTS = [
34656
- "credentials/reference-updated",
34657
- "settings/document-updated",
34658
- "llm/adapters-updated"
34659
- ];
34660
- /** Subscribe to the same provider-directory invalidations as the official Web Models page. */
34661
- function subscribeProviderSettings(ctx, listener) {
34662
- const events = ctx;
34663
- const disposers = PROVIDER_SETTINGS_EVENTS.map((event) => events.on(event, () => listener()));
34664
- return () => {
34665
- for (const dispose of disposers) dispose();
34666
- };
34667
- }
34668
- /** A single-line, bounded error from the provider-management adapter. */
34669
- var ProviderSettingsError = class extends Error {
34670
- constructor(message) {
34671
- super(message);
34672
- this.name = "ProviderSettingsError";
34673
- }
34674
- };
34675
- /**
34676
- * Join the configurable-provider directory, the redacted settings
34677
- * namespaces, and the referenced credentials into panel rows, web-parity:
34678
- * - directory entries merge with `listProviders()` to mark each live or
34679
- * dormant, and routes registered without a directory declaration appear as
34680
- * read-only/unmanaged rows (no settings address);
34681
- * - a whole-section entry is configured whenever its namespace resolves;
34682
- * a path-addressed one only when the profile resolves there;
34683
- * - a row is removable when the user layer alone carries its profile;
34684
- * - only refs named by resolved profiles are described, and a per-ref failure
34685
- * degrades to that row's bounded error instead of losing it.
34686
- * Absent `settings`/`credentials` services are tolerated the same way.
34687
- * @param ctx - context carrying the `llm` service (settings/credentials optional).
34688
- * @returns the resolved directory; empty rows when `llm` is unavailable.
34689
- */
34690
- async function loadProviderSettings(ctx) {
34691
- const llm = ctx.get("llm");
34692
- if (llm === void 0) return {
34693
- rows: [],
34694
- writable: false,
34695
- failures: []
34696
- };
34697
- const registered = llm.listProviders();
34698
- const failures = [];
34699
- const directoryEntries = [];
34700
- if (llm.listConfigurableProviders !== void 0) try {
34701
- directoryEntries.push(...llm.listConfigurableProviders());
34702
- } catch (error) {
34703
- failures.push(`configurable-provider directory failed: ${singleLine(messageOf(error))}`);
34704
- }
34705
- const settings = ctx.get("settings");
34706
- let descriptors = [];
34707
- let writable = false;
34708
- if (settings !== void 0) try {
34709
- descriptors = settings.describe({ redactSecrets: true });
34710
- writable = settings.writable === true;
34711
- } catch (error) {
34712
- failures.push(`settings describe failed: ${singleLine(messageOf(error))}`);
34713
- }
34714
- const namespaces = new Map(descriptors.map((descriptor) => [descriptor.ns, descriptor]));
34715
- const active = new Set(registered.map((provider) => provider.id));
34716
- const declared = new Set(directoryEntries.map((entry) => entry.provider));
34717
- const rows = [...directoryEntries.map((entry) => ({
34718
- provider: entry.provider,
34719
- displayName: entry.displayName,
34720
- active: active.has(entry.provider),
34721
- settingsNs: entry.settingsNs,
34722
- settingsPath: entry.settingsPath,
34723
- ...entry.declared === void 0 ? {} : { declared: entry.declared }
34724
- })), ...registered.filter((provider) => !declared.has(provider.id)).map((provider) => ({
34725
- provider: provider.id,
34726
- displayName: provider.name,
34727
- active: true,
34728
- settingsNs: "",
34729
- settingsPath: []
34730
- }))].map((base) => {
34731
- const namespace = base.settingsNs.length === 0 ? void 0 : namespaces.get(base.settingsNs);
34732
- const profile = namespace === void 0 ? void 0 : base.settingsPath.length === 0 ? namespace.value : getPath(namespace.value, base.settingsPath);
34733
- const configured = namespace !== void 0 && (base.settingsPath.length === 0 || profile !== void 0);
34734
- const removable = namespace !== void 0 && base.settingsPath.length > 0 && hasPath(namespace.user, base.settingsPath) && !hasPath(namespace.base, base.settingsPath);
34735
- const credentialRef = profileRefOf(profile);
34736
- return {
34737
- provider: base.provider,
34738
- displayName: base.displayName,
34739
- active: base.active,
34740
- settingsNs: base.settingsNs,
34741
- settingsPath: base.settingsPath,
34742
- settingsRevision: namespace?.revision ?? 0,
34743
- configured,
34744
- removable,
34745
- configuration: configurationOf(profile),
34746
- ...credentialRef === void 0 ? {} : { credentialRef },
34747
- suggestedRef: deriveCredentialRef(base.provider),
34748
- ...base.declared === void 0 ? {} : { declared: base.declared }
34749
- };
34750
- });
34751
- const refs = [...new Set(rows.flatMap((row) => row.credentialRef === void 0 ? [] : [row.credentialRef]))];
34752
- const credentialViews = /* @__PURE__ */ new Map();
34753
- const credentials = ctx.get("credentials");
34754
- if (refs.length > 0) {
34755
- if (credentials === void 0) for (const ref of refs) credentialViews.set(ref, {
34756
- kind: "error",
34757
- message: "credentials service is unavailable"
34758
- });
34759
- else await Promise.all(refs.map(async (ref) => {
34760
- try {
34761
- const facts = await credentials.describe(ref);
34762
- credentialViews.set(ref, {
34763
- kind: "facts",
34764
- configured: facts.configured,
34765
- writable: facts.writable,
34766
- ...facts.source === void 0 ? {} : { source: facts.source }
34767
- });
34768
- } catch (error) {
34769
- credentialViews.set(ref, {
34770
- kind: "error",
34771
- message: singleLine(messageOf(error))
34772
- });
34773
- }
34774
- }));
34775
- }
34776
- return {
34777
- rows: rows.map((row) => ({
34778
- ...row,
34779
- credential: row.credentialRef === void 0 ? void 0 : credentialViews.get(row.credentialRef) ?? {
34780
- kind: "error",
34781
- message: "credential describe returned no view"
34782
- }
34783
- })),
34784
- writable,
34785
- failures
34786
- };
34787
- }
34788
- /**
34789
- * Store a provider API key, web-parity: validate with `normalizeApiKey`
34790
- * (single-line, actionable errors that never echo the key), materialize the
34791
- * profile/`apiKeyEnv` through `settings.mutate` first when the resolved
34792
- * profile names no reference (dormant route or ref-less profile), then store
34793
- * under the trusted named ref or the derived conventional ref. An existing
34794
- * whole-section DeepSeek whose resolved profile already names
34795
- * `DEEPSEEK_API_KEY` needs no settings mutation. Env-supplied read-only keys
34796
- * are refused before any service call.
34797
- * @param ctx - context carrying `settings` (when materializing) and `credentials`.
34798
- * @param target - the joined row to write through.
34799
- * @param rawKey - the key exactly as typed; surrounding whitespace is trimmed.
34800
- * @throws {@link ProviderSettingsError} with a single-line, key-free message.
34801
- */
34802
- async function saveProviderCredential(ctx, target, rawKey) {
34803
- const trimmed = rawKey.trim();
34804
- if (ENV_ASSIGNMENT.test(trimmed) || hasWrappingQuotes(trimmed)) throw new ProviderSettingsError("paste only the API key, without an environment-variable name or wrapping quotes");
34805
- const checked = normalizeApiKey(rawKey);
34806
- if (!checked.ok) throw new ProviderSettingsError(checked.reason === "empty" ? "the API key is empty after trimming surrounding whitespace" : "the API key contains characters an HTTP header cannot carry; type a plain printable-ASCII key");
34807
- if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
34808
- if (target.credential?.kind === "facts" && target.credential.writable === false) throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead of overwriting it here`);
34809
- const credentials = ctx.get("credentials");
34810
- if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot store the API key");
34811
- const ref = target.credentialRef ?? deriveCredentialRef(target.provider);
34812
- if (target.credentialRef === void 0) {
34813
- const settings = ctx.get("settings");
34814
- if (settings === void 0) throw new ProviderSettingsError("settings service is unavailable; cannot materialize the credential reference");
34815
- try {
34816
- await settings.mutate(target.settingsNs, [{
34817
- op: "set",
34818
- path: [...target.settingsPath, "apiKeyEnv"],
34819
- value: ref
34820
- }]);
34821
- } catch (error) {
34822
- throw new ProviderSettingsError(singleLine(messageOf(error)));
34823
- }
34824
- }
34825
- try {
34826
- await credentials.set(ref, checked.value);
34827
- } catch (error) {
34828
- throw new ProviderSettingsError(credentialWriteMessage(error, checked.value));
34829
- }
34830
- }
34831
- /** Save the endpoint and an explicit model allow-list without rebuilding the profile. */
34832
- async function saveProviderConfiguration(ctx, target, configuration) {
34833
- if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
34834
- const settings = ctx.get("settings");
34835
- if (settings === void 0 || settings.writable !== true) throw new ProviderSettingsError("settings are read-only; provider configuration cannot be changed here");
34836
- const baseURL = configuration.baseURL?.trim();
34837
- if (baseURL !== void 0 && baseURL !== "") try {
34838
- const parsed = new URL(baseURL);
34839
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
34840
- } catch {
34841
- throw new ProviderSettingsError("base URL must be an absolute http or https URL");
34842
- }
34843
- const seen = /* @__PURE__ */ new Set();
34844
- const models = configuration.models.map((model) => {
34845
- const id = model.id.trim();
34846
- if (id === "" || seen.has(id)) throw new ProviderSettingsError("each selected model must have a unique non-empty id");
34847
- seen.add(id);
34848
- for (const [label, value] of [["context window", model.contextWindow], ["output window", model.maxTokens]]) if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new ProviderSettingsError(`${label} must be a positive integer`);
34849
- return {
34850
- id,
34851
- ...model.extras,
34852
- ...model.name === void 0 || model.name.trim() === "" ? {} : { name: model.name.trim() },
34853
- ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
34854
- ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
34855
- };
34856
- });
34857
- const root = target.settingsPath;
34858
- const ops = [baseURL === void 0 || baseURL === "" ? {
34859
- op: "unset",
34860
- path: [...root, "baseURL"]
34861
- } : {
34862
- op: "set",
34863
- path: [...root, "baseURL"],
34864
- value: baseURL
34865
- }, {
34866
- op: "set",
34867
- path: [...root, "models"],
34868
- value: models
34869
- }];
34870
- try {
34871
- await settings.mutate(target.settingsNs, ops, target.settingsRevision);
34872
- } catch (error) {
34873
- throw new ProviderSettingsError(singleLine(messageOf(error)));
34874
- }
34875
- }
34876
- /**
34877
- * Remove the currently named credential without touching the provider
34878
- * profile. Only the resolved profile's own reference is unset; a dormant or
34879
- * ref-less row (nothing to remove), an already-absent key, and an
34880
- * env-supplied read-only key are rejected safely before any service call.
34881
- * @param ctx - context carrying the `credentials` service.
34882
- * @param target - the joined row whose named credential to unset.
34883
- * @throws {@link ProviderSettingsError} with a single-line, key-free message.
34884
- */
34885
- async function unsetProviderCredential(ctx, target) {
34886
- const ref = target.credentialRef;
34887
- if (ref === void 0) throw new ProviderSettingsError(`provider "${target.provider}" names no credential reference to remove`);
34888
- const facts = target.credential;
34889
- if (facts?.kind === "facts" && facts.configured === false) throw new ProviderSettingsError(`provider "${target.provider}" has no configured credential to remove`);
34890
- if (facts?.kind === "facts" && facts.writable === false) throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead`);
34891
- const credentials = ctx.get("credentials");
34892
- if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot remove the API key");
34893
- try {
34894
- await credentials.unset(ref);
34895
- } catch (error) {
34896
- throw new ProviderSettingsError(singleLine(messageOf(error)));
34897
- }
34898
- }
34899
- /**
34900
- * Remove a user-added provider profile, web-parity: only `removable` rows may
34901
- * be removed; a page-managed credential — the derived ref, configured and
34902
- * writable — is unset first (so a second-step failure leaves the row visible
34903
- * and the operation retryable), then `settings.mutate` unsets
34904
- * `target.settingsPath`. Both steps are idempotent. A hand-named credential
34905
- * ref may be shared elsewhere and is left alone.
34906
- * @param ctx - context carrying `credentials` and `settings`.
34907
- * @param target - the joined row to remove.
34908
- * @throws {@link ProviderSettingsError} with a single-line, key-free message.
34909
- */
34910
- async function removeProviderSettings(ctx, target) {
34911
- if (!target.removable) throw new ProviderSettingsError(`provider "${target.provider}" is not removable from the user settings layer`);
34912
- if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings profile to remove`);
34913
- const managedRef = target.credentialRef === target.suggestedRef && target.credential?.kind === "facts" && target.credential.configured === true && target.credential.writable === true ? target.credentialRef : void 0;
34914
- if (managedRef !== void 0) {
34915
- const credentials = ctx.get("credentials");
34916
- if (credentials === void 0) throw new ProviderSettingsError("credentials service is unavailable; cannot remove the managed API key");
34917
- try {
34918
- await credentials.unset(managedRef);
34919
- } catch (error) {
34920
- throw new ProviderSettingsError(singleLine(messageOf(error)));
34921
- }
34922
- }
34923
- const settings = ctx.get("settings");
34924
- if (settings === void 0) throw new ProviderSettingsError("settings service is unavailable; cannot remove the provider profile");
34925
- try {
34926
- await settings.mutate(target.settingsNs, [{
34927
- op: "unset",
34928
- path: [...target.settingsPath]
34929
- }]);
34930
- } catch (error) {
34931
- throw new ProviderSettingsError(singleLine(messageOf(error)));
34932
- }
34933
- }
34934
- //#endregion
34935
35810
  //#region src/questions.ts
34936
35811
  const ABORT_ERROR = new UserQuestionError("ask_user_question was interrupted before the user answered", "ASK_ABORTED");
34937
35812
  /**
34938
- * Mount the single `ctx.userQuestions` UI provider over a FIFO queue.
34939
- * @param ctx - context carrying the `userQuestions` service (dsh-base).
34940
- * @returns the store the renderer subscribes to; a context without the
34941
- * service yields a permanently empty store.
35813
+ * Mount the `user-questions/request` answerer over a FIFO queue.
35814
+ * @param ctx - plugin context whose event bus carries the waterfall.
35815
+ * @param owns - agents this terminal answers for; every other request is
35816
+ * deferred back into the waterfall (`next()`), so sibling answerers stay
35817
+ * usable. Agent-less asks are claimed: this TUI is the only human surface
35818
+ * in the process.
35819
+ * @returns the store the renderer subscribes to.
34942
35820
  */
34943
- function mountQuestionProvider(ctx) {
34944
- const service = ctx.get("userQuestions");
35821
+ function mountQuestionProvider(ctx, owns) {
34945
35822
  let snapshot = { pending: void 0 };
34946
35823
  let active;
34947
35824
  const queue = [];
@@ -34956,7 +35833,8 @@ function mountQuestionProvider(ctx) {
34956
35833
  active = next;
34957
35834
  set({ pending: next });
34958
35835
  };
34959
- if (service !== void 0) service.registerProvider({ ask(request) {
35836
+ ctx.on("user-questions/request", (request, next) => {
35837
+ if (request.agent !== void 0 && !owns(request.agent)) return next();
34960
35838
  return new Promise((resolve, reject) => {
34961
35839
  const onAbort = () => {
34962
35840
  if (active === pending) {
@@ -34988,7 +35866,7 @@ function mountQuestionProvider(ctx) {
34988
35866
  set({ pending });
34989
35867
  } else queue.push(pending);
34990
35868
  });
34991
- } });
35869
+ });
34992
35870
  return {
34993
35871
  subscribe(listener) {
34994
35872
  listeners.add(listener);
@@ -35271,30 +36149,33 @@ function toRows(skills) {
35271
36149
  * @param ctx - context carrying the `skills` service (optional).
35272
36150
  * @returns the view the completion menu subscribes to.
35273
36151
  */
35274
- function watchSkills(ctx) {
36152
+ function watchSkills(ctx, fallbackCwd) {
35275
36153
  const skills = ctx.get("skills");
35276
36154
  let agent;
35277
36155
  let rows = [];
35278
36156
  let error;
36157
+ let loadedFor;
35279
36158
  const listeners = /* @__PURE__ */ new Set();
35280
36159
  const reload = () => {
35281
36160
  const target = agent;
35282
36161
  if (skills === void 0 || target === void 0) return;
35283
36162
  Promise.resolve().then(() => skills.list({
35284
- cwd: target.session.header.cwd,
36163
+ cwd: target.session.header.cwd ?? fallbackCwd,
35285
36164
  scope: target
35286
36165
  })).then((summaries) => {
35287
36166
  if (agent !== target) return;
35288
36167
  const next = toRows(summaries);
35289
- const unchanged = next.length === rows.length && next.every((row, index) => row.name === rows[index]?.name);
36168
+ const unchanged = next.length === rows.length && next.every((row, index) => row.name === rows[index]?.name && row.description === rows[index]?.description && row.modelInvocable === rows[index]?.modelInvocable);
35290
36169
  rows = next;
36170
+ loadedFor = target;
35291
36171
  const recovered = error !== void 0;
35292
36172
  error = void 0;
35293
36173
  if (unchanged && !recovered) return;
35294
36174
  for (const listener of listeners) listener();
35295
36175
  }).catch((cause) => {
35296
36176
  if (agent !== target) return;
35297
- rows = [...rows];
36177
+ if (loadedFor !== target) rows = [];
36178
+ else rows = [...rows];
35298
36179
  error = cause instanceof Error ? cause.message : String(cause);
35299
36180
  for (const listener of listeners) listener();
35300
36181
  });
@@ -35761,7 +36642,12 @@ function selectForkSeed(events, atSeq) {
35761
36642
  }
35762
36643
  //#endregion
35763
36644
  //#region src/git-workflow.ts
35764
- /** Read-only Git inspection used by /diff and /review. */
36645
+ /**
36646
+ * Read-only Git inspection used by /diff and /review. Every diff
36647
+ * invocation carries --no-ext-diff and --no-textconv, so configured
36648
+ * external diff drivers and text converters can never execute as a
36649
+ * side effect of reading a diff.
36650
+ */
35765
36651
  /** Split Git's stable `diff --git` framing without interpreting patch content. */
35766
36652
  function parseGitDiffFiles(text) {
35767
36653
  if (text === "") return [];
@@ -35784,6 +36670,7 @@ function parseGitDiffSpec(argument) {
35784
36670
  args: [
35785
36671
  "diff",
35786
36672
  "--no-ext-diff",
36673
+ "--no-textconv",
35787
36674
  "--unified=3",
35788
36675
  "HEAD",
35789
36676
  "--"
@@ -35794,6 +36681,7 @@ function parseGitDiffSpec(argument) {
35794
36681
  args: [
35795
36682
  "diff",
35796
36683
  "--no-ext-diff",
36684
+ "--no-textconv",
35797
36685
  "--unified=3",
35798
36686
  "--cached",
35799
36687
  "--"
@@ -35805,6 +36693,7 @@ function parseGitDiffSpec(argument) {
35805
36693
  args: [
35806
36694
  "diff",
35807
36695
  "--no-ext-diff",
36696
+ "--no-textconv",
35808
36697
  "--unified=3",
35809
36698
  value,
35810
36699
  "--"
@@ -35828,8 +36717,25 @@ function executeGit(cwd, args, signal) {
35828
36717
  });
35829
36718
  });
35830
36719
  }
36720
+ /** Arguments for the unstaged-only fallback below. */
36721
+ const UNSTAGED_DIFF_ARGS = [
36722
+ "diff",
36723
+ "--no-ext-diff",
36724
+ "--no-textconv",
36725
+ "--unified=3",
36726
+ "--"
36727
+ ];
36728
+ /** Whether the repository has at least one commit (a HEAD revision). */
36729
+ function hasHeadRevision(cwd, signal) {
36730
+ return executeGit(cwd, [
36731
+ "rev-parse",
36732
+ "--verify",
36733
+ "--quiet",
36734
+ "HEAD"
36735
+ ], signal).then(() => true).catch(() => false);
36736
+ }
35831
36737
  /**
35832
- * Load one complete textual diff without invoking external diff drivers.
36738
+ * Load one complete textual diff without invoking external programs.
35833
36739
  * @param signal - aborted by the caller on session switches/quit, killing the
35834
36740
  * git subprocess instead of letting a stale repository's diff land later.
35835
36741
  */
@@ -35842,15 +36748,10 @@ async function loadGitDiff(cwd, argument, signal) {
35842
36748
  files: parseGitDiffFiles(text)
35843
36749
  };
35844
36750
  } catch (error) {
35845
- if (argument.trim() !== "") throw error;
36751
+ if (argument.trim() !== "" || signal?.aborted === true || await hasHeadRevision(cwd, signal)) throw error;
35846
36752
  return {
35847
- title: "git diff - working tree",
35848
- files: parseGitDiffFiles(await executeGit(cwd, [
35849
- "diff",
35850
- "--no-ext-diff",
35851
- "--unified=3",
35852
- "--"
35853
- ], signal))
36753
+ title: "git diff - working tree (no commits yet)",
36754
+ files: parseGitDiffFiles(await executeGit(cwd, UNSTAGED_DIFF_ARGS, signal))
35854
36755
  };
35855
36756
  }
35856
36757
  }
@@ -35925,16 +36826,24 @@ function agentPresetsFrom(ctx) {
35925
36826
  function isBlankSession(events) {
35926
36827
  return !events.some((event) => event.type === "turn/start");
35927
36828
  }
36829
+ /** Upstream renamed the shipped `code` preset to `ptc` in 0.1.2-rc.1; sessions
36830
+ * and CLI choices recorded before the rename keep resolving through this map. */
36831
+ const LEGACY_PRESET_IDS = { code: "ptc" };
36832
+ function normalizePresetId(id) {
36833
+ return id === void 0 ? void 0 : LEGACY_PRESET_IDS[id] ?? id;
36834
+ }
35928
36835
  /** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
35929
36836
  function resolvePreset(session) {
35930
- for (let index = session.events.length - 1; index >= 0; index -= 1) {
35931
- const event = session.events[index];
35932
- if (event.type === "agent-preset/selected" && event.data?.agentPreset !== void 0) return event.data.agentPreset;
36837
+ const events = session.snapshotEvents();
36838
+ for (let index = events.length - 1; index >= 0; index -= 1) {
36839
+ const event = events[index];
36840
+ if (event.type === "agent-preset/selected" && event.data?.agentPreset !== void 0) return normalizePresetId(event.data.agentPreset);
35933
36841
  }
35934
- return session.header.agentPreset ?? "standard";
36842
+ return normalizePresetId(session.header.agentPreset) ?? "standard";
35935
36843
  }
35936
36844
  /** Resolve a pre-session choice, or recompose an active blank Agent. */
35937
36845
  async function selectPreset(service, agent, presetId) {
36846
+ presetId = normalizePresetId(presetId);
35938
36847
  if (agent !== void 0) return switchPreset(service, agent, presetId);
35939
36848
  const preset = await service.resolve(presetId);
35940
36849
  if (preset.broken !== void 0) throw new Error(preset.broken);
@@ -35942,7 +36851,7 @@ async function selectPreset(service, agent, presetId) {
35942
36851
  }
35943
36852
  /** Recompose atomically from the caller's perspective, logging only success. */
35944
36853
  async function switchPreset(service, agent, presetId) {
35945
- if (!isBlankSession(agent.session.events)) throw new Error("mode is locked after the first turn; use /new <mode>");
36854
+ if (!isBlankSession(agent.session.snapshotEvents())) throw new Error("mode is locked after the first turn; use /new <mode>");
35946
36855
  const preset = await service.recompose(agent.ctx, presetId);
35947
36856
  agent.session.append("agent-preset/selected", { agentPreset: preset.id });
35948
36857
  return preset;
@@ -35955,7 +36864,7 @@ function permissionPresetsFrom(ctx) {
35955
36864
  }
35956
36865
  /** Effective label for either an active session or the not-yet-created first one. */
35957
36866
  function effectivePermission(service, session, pending) {
35958
- return session === void 0 ? pending ?? service.defaultPreset : service.current(session.events);
36867
+ return session === void 0 ? pending ?? service.defaultPreset : service.current(session);
35959
36868
  }
35960
36869
  /** Validate a preset and write it only when a durable session already exists. */
35961
36870
  function selectPermission(service, session, preset) {
@@ -36037,6 +36946,38 @@ function listPluginRows(ctx) {
36037
36946
  * @module @deepseek-ai/dsh-code/settings-file
36038
36947
  */
36039
36948
  /**
36949
+ * Run one file operation with a bounded retry: one initial try plus at
36950
+ * most `retries` more. Creating or replacing a file can fail transiently
36951
+ * with EPERM/EACCES while an antivirus scanner or search indexer holds
36952
+ * it — the standard graceful-fs remedy, not a workaround for a
36953
+ * persistent permission problem. A save that still fails leaves its
36954
+ * uniquely named temp file behind, so repeated crashed saves accumulate
36955
+ * distinct leftovers rather than corrupting a shared one.
36956
+ */
36957
+ async function withTransientRetry(operation, retries = 5) {
36958
+ for (let attempt = 0;; attempt += 1) try {
36959
+ await operation();
36960
+ return;
36961
+ } catch (error) {
36962
+ const code = error.code;
36963
+ if (attempt >= retries || code !== "EPERM" && code !== "EACCES") throw error;
36964
+ await new Promise((resolve) => setTimeout(resolve, 30 * (attempt + 1)));
36965
+ }
36966
+ }
36967
+ /**
36968
+ * Write one file atomically: create the parent directory, write to a
36969
+ * uniquely named temp file, and rename it into place. A crash midway
36970
+ * can never leave a half-written document behind. Unique temp names
36971
+ * keep concurrent writers (two terminals, two chains in one process)
36972
+ * from sharing one temp path.
36973
+ */
36974
+ async function writeFileAtomically(path, text) {
36975
+ await mkdir(dirname(path), { recursive: true });
36976
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
36977
+ await withTransientRetry(() => writeFile(temp, text, "utf8"));
36978
+ await withTransientRetry(() => rename(temp, path));
36979
+ }
36980
+ /**
36040
36981
  * Create the shared settings-write chain. One instance per process keeps
36041
36982
  * every user-level JSON file mutually serialized.
36042
36983
  * @returns the persistence handle.
@@ -36045,12 +36986,7 @@ function createUserSettingsPersistence() {
36045
36986
  let chain = Promise.resolve();
36046
36987
  return {
36047
36988
  save(path, text) {
36048
- const write = chain.then(async () => {
36049
- await mkdir(dirname(path), { recursive: true });
36050
- const temp = `${path}.tmp`;
36051
- await writeFile(temp, text, "utf8");
36052
- await rename(temp, path);
36053
- });
36989
+ const write = chain.then(() => writeFileAtomically(path, text));
36054
36990
  chain = write.catch(() => {});
36055
36991
  return write;
36056
36992
  },
@@ -36172,6 +37108,41 @@ async function runQuitSequence(steps, exit, onError) {
36172
37108
  return started;
36173
37109
  }
36174
37110
  /**
37111
+ * Order-preserving gate for composer input while the startup prompt/images
37112
+ * are still preparing. Anything submitted before the startup delivery settles
37113
+ * queues and flushes afterwards in submit order, so the initial request can
37114
+ * never be overtaken by typing that raced a slow image preparation. The flush
37115
+ * also runs when the startup delivery fails: user input is never stranded.
37116
+ */
37117
+ var StartupInputGate = class {
37118
+ deliver;
37119
+ queued = [];
37120
+ pending = false;
37121
+ constructor(deliver) {
37122
+ this.deliver = deliver;
37123
+ }
37124
+ /** Submit one line: delivered now while idle, queued behind the startup delivery otherwise. */
37125
+ submit(submission) {
37126
+ if (this.pending) this.queued.push(submission);
37127
+ else this.deliver(submission);
37128
+ }
37129
+ /**
37130
+ * Run the startup delivery — the callback receives the direct-delivery sink
37131
+ * for the startup prompt itself — then flush everything that queued behind
37132
+ * it, in order, even when the callback rejects.
37133
+ */
37134
+ async run(startup) {
37135
+ this.pending = true;
37136
+ try {
37137
+ await startup((submission) => this.deliver(submission));
37138
+ } finally {
37139
+ this.pending = false;
37140
+ const queued = this.queued.splice(0);
37141
+ for (const submission of queued) this.deliver(submission);
37142
+ }
37143
+ }
37144
+ };
37145
+ /**
36175
37146
  * Resolve the invocation's target session against the persisted headers.
36176
37147
  * @param startup - the parsed startup flags.
36177
37148
  * @param persistence - the persistence service; required for resume/latest.
@@ -36254,7 +37225,7 @@ async function run(ctx, startup, io) {
36254
37225
  const nextCwd = next.cwd ?? cwd;
36255
37226
  const selectionState = pendingSelection === void 0 ? {} : { picked: pendingSelection };
36256
37227
  let mode = next.resume ? next.mode : next.mode ?? pendingMode;
36257
- if (!next.resume) mode = (await presets.resolve(mode)).id;
37228
+ if (!next.resume) mode = (await presets.resolve(normalizePresetId(mode ?? presets.defaultId))).id;
36258
37229
  const setup = async (agentCtx) => {
36259
37230
  const sessionPreset = next.resume ? resolvePreset(agentCtx.agent.session) : mode;
36260
37231
  mode = (await presets.mount(agentCtx, sessionPreset)).id;
@@ -36299,7 +37270,7 @@ async function run(ctx, startup, io) {
36299
37270
  handle,
36300
37271
  agent: handle.agent,
36301
37272
  session,
36302
- store: createTranscriptStore(session.events),
37273
+ store: createTranscriptStore(session.snapshotEvents()),
36303
37274
  mentions: createMentions(ctx, handle.agent, session.header.cwd ?? nextCwd),
36304
37275
  mode: mode ?? "standard",
36305
37276
  selection: selectionState,
@@ -36370,7 +37341,7 @@ async function run(ctx, startup, io) {
36370
37341
  });
36371
37342
  const commands = watchCommands(ctx);
36372
37343
  if (agent !== void 0) commands.setAgent(agent);
36373
- const skills = watchSkills(ctx);
37344
+ const skills = watchSkills(ctx, cwd);
36374
37345
  if (agent !== void 0) skills.setAgent(agent);
36375
37346
  const approval = mountApprovalAnswerer(ctx, (candidate) => agent !== void 0 && candidate.id === agent.id, (request) => approvalCommandPreview(store.getView().entries, request.callId, request.toolName));
36376
37347
  let subagentOverride;
@@ -36378,10 +37349,12 @@ async function run(ctx, startup, io) {
36378
37349
  const subject = payload.agent;
36379
37350
  const header = subject.session.header;
36380
37351
  if (header.parentSession === void 0 && header.origin !== "subagent") return next();
36381
- const picked = subagentOverride ?? resolveEffectiveSelection(active?.selection.picked ?? pendingSelection, subject.session.requestHeader()?.config, currentDefaults());
37352
+ const activeAgent = active;
37353
+ const belongsToActive = activeAgent !== void 0 && (header.parentSession ?? subject.session.id) === activeAgent.session.id;
37354
+ const picked = subagentOverride ?? resolveEffectiveSelection(belongsToActive && activeAgent !== void 0 ? activeAgent.selection.picked ?? pendingSelection : void 0, subject.session.requestHeader()?.config, currentDefaults());
36382
37355
  return next().then((resolved) => applyModelSelectionToConfig(resolved, picked));
36383
37356
  });
36384
- const questions = mountQuestionProvider(ctx);
37357
+ const questions = mountQuestionProvider(ctx, (candidate) => agent !== void 0 && candidate.id === agent.id);
36385
37358
  const bridge = { notify: () => {} };
36386
37359
  const capabilitySyncDebounceMs = 400;
36387
37360
  const runCapabilitySync = () => {
@@ -36439,17 +37412,27 @@ async function run(ctx, startup, io) {
36439
37412
  };
36440
37413
  const historyPath = join(homedir(), ".dsh", "dsh-code", "history.jsonl");
36441
37414
  let inputHistory = [];
37415
+ let historyWriteChain = Promise.resolve();
36442
37416
  try {
36443
- inputHistory = parseHistoryFile(readFileSync(historyPath, "utf8"));
37417
+ const rawHistory = readFileSync(historyPath, "utf8");
37418
+ inputHistory = parseHistoryFile(rawHistory);
37419
+ if (needsCompaction(rawHistory)) historyWriteChain = historyWriteChain.then(() => writeFileAtomically(historyPath, serializeHistoryList(inputHistory))).catch(() => {});
36444
37420
  } catch {
36445
37421
  inputHistory = [];
36446
37422
  }
36447
- /** Serialized history writes: each submission rewrites the latest in-memory snapshot. */
36448
- let historyWriteChain = Promise.resolve();
37423
+ /**
37424
+ * Serialized history writes: each submission appends one JSON line at the
37425
+ * end of the file, so concurrent terminals add entries after each other
37426
+ * instead of overwriting snapshots they read at their own boot. A
37427
+ * multi-line draft still occupies one physical line (JSON escapes the
37428
+ * newline), and a regular-length line reaches the disk as one positioned
37429
+ * write; an oversized paste may interleave mid-line, which the next
37430
+ * parse simply drops.
37431
+ */
36449
37432
  const recordHistory = (text) => {
36450
37433
  if (text === "") return;
36451
37434
  inputHistory = [...inputHistory, text].slice(-100);
36452
- historyWriteChain = historyWriteChain.then(() => mkdir(dirname(historyPath), { recursive: true })).then(() => writeFile(historyPath, serializeHistoryList(inputHistory), "utf8")).catch((writeError) => {
37435
+ historyWriteChain = historyWriteChain.then(() => mkdir(dirname(historyPath), { recursive: true })).then(() => appendFile(historyPath, historyLine(text), "utf8")).catch((writeError) => {
36453
37436
  bridge.notify("history save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
36454
37437
  });
36455
37438
  };
@@ -36474,12 +37457,16 @@ async function run(ctx, startup, io) {
36474
37457
  off();
36475
37458
  for (const dispose of offCapabilitySync) dispose();
36476
37459
  if (capabilitySyncTimer !== void 0) clearTimeout(capabilitySyncTimer);
36477
- mountRef.current?.unmount();
36478
37460
  const currentSession = session;
36479
37461
  const currentActive = active;
36480
37462
  const report = (name, error) => {
36481
37463
  internals.stderr.write(`dsh: quit ${name} failed: ${error instanceof Error ? error.message : String(error)}\n`);
36482
37464
  };
37465
+ try {
37466
+ mountRef.current?.unmount();
37467
+ } catch (error) {
37468
+ report("unmount", error);
37469
+ }
36483
37470
  runQuitSequence([
36484
37471
  ...currentSession === void 0 || currentActive === void 0 ? [] : [{
36485
37472
  name: "flush",
@@ -36543,6 +37530,11 @@ async function run(ctx, startup, io) {
36543
37530
  bridge.notify(`command failed: ${error instanceof Error ? error.message : String(error)}`, "error");
36544
37531
  });
36545
37532
  };
37533
+ /** Delivery serialization state: the chain's epoch pins it to one session. */
37534
+ let deliveryChain = {
37535
+ epoch: 0,
37536
+ tail: Promise.resolve()
37537
+ };
36546
37538
  /** Deliver one trimmed line to the live session, expanding mentions first. */
36547
37539
  const deliverLine = (line, mode, images = []) => {
36548
37540
  const currentAgent = agent;
@@ -36558,6 +37550,13 @@ async function run(ctx, startup, io) {
36558
37550
  bridge.notify(`invalid session reference: ${error instanceof Error ? error.message : String(error)}`, "error");
36559
37551
  return;
36560
37552
  }
37553
+ if (deliveryChain.epoch !== epoch) deliveryChain = {
37554
+ epoch,
37555
+ tail: Promise.resolve()
37556
+ };
37557
+ const enqueueDelivery = (run) => {
37558
+ deliveryChain.tail = deliveryChain.tail.then(run);
37559
+ };
36561
37560
  const atEpoch = epoch;
36562
37561
  const deliver = (readable, context) => {
36563
37562
  if (epoch !== atEpoch || agent !== currentAgent) return;
@@ -36578,19 +37577,19 @@ async function run(ctx, startup, io) {
36578
37577
  }
36579
37578
  };
36580
37579
  if (parsed.references.length === 0) {
36581
- deliver(parsed.text);
37580
+ enqueueDelivery(() => deliver(parsed.text));
36582
37581
  return;
36583
37582
  }
36584
37583
  const controller = new AbortController();
36585
37584
  pendingControllers.add(controller);
36586
- currentMentions.prepare(parsed, controller.signal).then((prepared) => {
37585
+ enqueueDelivery(() => currentMentions.prepare(parsed, controller.signal).then((prepared) => {
36587
37586
  pendingControllers.delete(controller);
36588
37587
  deliver(prepared.text, prepared.additionalContext);
36589
37588
  }, (error) => {
36590
37589
  pendingControllers.delete(controller);
36591
37590
  if (controller.signal.aborted || epoch !== atEpoch) return;
36592
37591
  bridge.notify(`session reference failed: ${error instanceof Error ? error.message : String(error)}`, "error");
36593
- });
37592
+ }));
36594
37593
  };
36595
37594
  const pendingInputs = [];
36596
37595
  let creating = false;
@@ -36614,18 +37613,41 @@ async function run(ctx, startup, io) {
36614
37613
  next.handle.dispose().catch(() => {});
36615
37614
  return;
36616
37615
  }
36617
- active = next;
36618
- agent = next.agent;
36619
- session = next.session;
36620
- store = next.store;
36621
- mentions = next.mentions;
36622
- subagents.reset();
36623
- pendingMode = void 0;
36624
- pendingPermission = void 0;
36625
- commands.setAgent(agent);
36626
- skills.setAgent(agent);
36627
- process.stdout.write("\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
36628
- renderCurrent();
37616
+ const previous = {
37617
+ active,
37618
+ agent,
37619
+ session,
37620
+ store,
37621
+ mentions
37622
+ };
37623
+ try {
37624
+ active = next;
37625
+ agent = next.agent;
37626
+ session = next.session;
37627
+ store = next.store;
37628
+ mentions = next.mentions;
37629
+ subagents.reset();
37630
+ pendingMode = void 0;
37631
+ pendingPermission = void 0;
37632
+ commands.setAgent(agent);
37633
+ skills.setAgent(agent);
37634
+ process.stdout.write("\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
37635
+ renderCurrent();
37636
+ } catch (error) {
37637
+ active = previous.active;
37638
+ agent = previous.agent;
37639
+ session = previous.session;
37640
+ store = previous.store === void 0 ? createTranscriptStore() : previous.store;
37641
+ mentions = previous.mentions === void 0 ? createMentions(ctx, void 0, cwd) : previous.mentions;
37642
+ if (agent !== void 0) {
37643
+ commands.setAgent(agent);
37644
+ skills.setAgent(agent);
37645
+ }
37646
+ await next.handle.dispose().catch(() => {});
37647
+ if (!quitting) renderCurrent();
37648
+ bridge.notify(`session activation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
37649
+ return;
37650
+ }
36629
37651
  abortPendingControllers();
36630
37652
  epoch += 1;
36631
37653
  const queued = pendingInputs.splice(0);
@@ -36639,9 +37661,9 @@ async function run(ctx, startup, io) {
36639
37661
  });
36640
37662
  };
36641
37663
  /** Deliver one readable line to the agent, expanding session mentions first. */
36642
- const send = (text, mode, images = []) => {
36643
- const line = text.trim();
36644
- if (line === "" && images.length === 0) return;
37664
+ const sendNow = (text, mode, images = []) => {
37665
+ const line = submissionPayload(text);
37666
+ if (line.trim() === "" && images.length === 0) return;
36645
37667
  if (images.length === 0 && line.startsWith("/mode ")) {
36646
37668
  switchModeAction(line.slice(6).trim()).then((selected) => bridge.notify(`mode → ${selected}`), (error) => bridge.notify(`mode switch failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
36647
37669
  return;
@@ -36666,6 +37688,14 @@ async function run(ctx, startup, io) {
36666
37688
  }
36667
37689
  deliverLine(line, mode, images);
36668
37690
  };
37691
+ const inputGate = new StartupInputGate(({ text, mode, images }) => sendNow(text, mode, images));
37692
+ const send = (text, mode, images = []) => {
37693
+ inputGate.submit({
37694
+ text,
37695
+ mode,
37696
+ images
37697
+ });
37698
+ };
36669
37699
  /** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
36670
37700
  const dispatch = (text, images = []) => {
36671
37701
  send(text, "followup", images);
@@ -36920,14 +37950,14 @@ async function run(ctx, startup, io) {
36920
37950
  session = next.session;
36921
37951
  store = next.store;
36922
37952
  mentions = next.mentions;
36923
- subagents.reset();
36924
- pendingMode = void 0;
36925
- pendingPermission = void 0;
36926
37953
  commands.setAgent(agent);
36927
37954
  skills.setAgent(agent);
36928
37955
  try {
36929
37956
  process.stdout.write("\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
36930
37957
  renderCurrent();
37958
+ subagents.reset();
37959
+ pendingMode = void 0;
37960
+ pendingPermission = void 0;
36931
37961
  } catch (error) {
36932
37962
  active = previous;
36933
37963
  agent = previous?.agent;
@@ -36943,7 +37973,9 @@ async function run(ctx, startup, io) {
36943
37973
  abortPendingControllers();
36944
37974
  epoch += 1;
36945
37975
  if (previous === void 0) {
36946
- bridge.notify(`${next.resumed ? "resumed" : "created"} ${next.session.id.slice(-12)} · mode ${next.mode}`);
37976
+ setTimeout(() => {
37977
+ bridge.notify(`${next.resumed ? "resumed" : "created"} ${next.session.id.slice(-12)} · mode ${next.mode}`);
37978
+ }, 0);
36947
37979
  return;
36948
37980
  }
36949
37981
  let cleanupWarning;
@@ -37019,10 +38051,6 @@ async function run(ctx, startup, io) {
37019
38051
  };
37020
38052
  const reviewChanges = (argument) => {
37021
38053
  const currentAgent = agent;
37022
- if (currentAgent === void 0) {
37023
- bridge.notify("no session yet - submit a message to start", "warning");
37024
- return;
37025
- }
37026
38054
  const atEpoch = epoch;
37027
38055
  const reviewCwd = session?.header.cwd ?? cwd;
37028
38056
  const controller = new AbortController();
@@ -37056,7 +38084,7 @@ async function run(ctx, startup, io) {
37056
38084
  const text = argument.trim();
37057
38085
  const atSeq = text === "" ? void 0 : Number(text);
37058
38086
  if (text !== "" && (!Number.isSafeInteger(atSeq) || (atSeq ?? -1) < 0)) throw new Error("usage: /fork [event-seq]");
37059
- const seed = selectForkSeed(session.events, atSeq);
38087
+ const seed = selectForkSeed(session.snapshotEvents(), atSeq);
37060
38088
  const id = `session-${randomUUID()}`;
37061
38089
  requestSwitch({
37062
38090
  target: {
@@ -37112,7 +38140,7 @@ async function run(ctx, startup, io) {
37112
38140
  branch: gitBranch(sessionCwd),
37113
38141
  sessionId: session === void 0 ? "" : session.id.slice(-8),
37114
38142
  resumed: active?.resumed ?? false,
37115
- mode: active?.mode ?? pendingMode ?? presets.defaultId,
38143
+ mode: active?.mode ?? pendingMode ?? normalizePresetId(presets.defaultId),
37116
38144
  permission,
37117
38145
  dispatch,
37118
38146
  steer,
@@ -37123,6 +38151,7 @@ async function run(ctx, startup, io) {
37123
38151
  subscribeModelProviders: (listener) => subscribeProviderSettings(ctx, listener),
37124
38152
  saveModelProviderCredential: (target, key) => saveProviderCredential(ctx, target, key),
37125
38153
  saveModelProviderConfiguration: (target, configuration) => saveProviderConfiguration(ctx, target, configuration),
38154
+ discoverModelProvider: (target, request, signal) => discoverProviderModels(ctx, target, request, signal),
37126
38155
  unsetModelProviderCredential: (target) => unsetProviderCredential(ctx, target),
37127
38156
  removeModelProvider: (target) => removeProviderSettings(ctx, target),
37128
38157
  loadProviderAuthorizations: () => loadProviderAuthorizations(ctx),
@@ -37187,10 +38216,17 @@ async function run(ctx, startup, io) {
37187
38216
  mountRef.current = io.mount(appElement());
37188
38217
  if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0) {
37189
38218
  if ((startup.images?.length ?? 0) > 0) bridge.notify(`processing ${startup.images.length} startup image${startup.images.length === 1 ? "" : "s"}…`);
37190
- saveImagePaths(startup.images ?? [], ctx.get("attachments")).then((images) => {
38219
+ inputGate.run(async (deliver) => {
38220
+ const images = await saveImagePaths(startup.images ?? [], ctx.get("attachments"));
37191
38221
  if (images.length > 0) bridge.notify(`${images.length} startup image${images.length === 1 ? "" : "s"} attached`);
37192
- send(startup.prompt ?? "", "followup", images);
37193
- }, (error) => bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
38222
+ deliver({
38223
+ text: startup.prompt ?? "",
38224
+ mode: "followup",
38225
+ images
38226
+ });
38227
+ }).catch((error) => {
38228
+ bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error");
38229
+ });
37194
38230
  }
37195
38231
  async function copyLastResponse() {
37196
38232
  const text = latestAssistantText(store.getView());
@@ -37251,4 +38287,4 @@ function apply(ctx, config) {
37251
38287
  });
37252
38288
  }
37253
38289
  //#endregion
37254
- export { Config, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence };
38290
+ export { Config, StartupInputGate, apply, exportSessionIdSuffix, inject, name, resolveTarget, runQuitSequence };