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.
- package/README.md +293 -285
- package/bin/deepseek.mjs +245 -12
- package/cordis.patch.yml +12 -14
- package/lib/index.mjs +1939 -903
- package/lib/types/app.d.ts +11 -2
- package/lib/types/commands.d.ts +13 -0
- package/lib/types/git-workflow.d.ts +7 -2
- package/lib/types/history.d.ts +18 -11
- package/lib/types/index.d.ts +28 -0
- package/lib/types/input-split.d.ts +54 -0
- package/lib/types/kernel-panels.d.ts +3 -1
- package/lib/types/keyboard.d.ts +8 -0
- package/lib/types/presets.d.ts +4 -1
- package/lib/types/provider-settings.d.ts +77 -0
- package/lib/types/questions.d.ts +16 -12
- package/lib/types/render/projection.d.ts +9 -2
- package/lib/types/render/status.d.ts +22 -15
- package/lib/types/settings-file.d.ts +8 -0
- package/lib/types/skills.d.ts +1 -1
- package/package.json +49 -46
- package/src/app.ts +5459 -4900
- package/src/approval.ts +8 -3
- package/src/authorization-panel.ts +2 -4
- package/src/commands.ts +27 -3
- package/src/git-workflow.ts +29 -10
- package/src/history.ts +22 -13
- package/src/index.ts +203 -61
- package/src/input-split.ts +191 -0
- package/src/internals.ts +26 -8
- package/src/kernel-panels.ts +26 -10
- package/src/keyboard.ts +123 -88
- package/src/mentions.ts +42 -9
- package/src/permissions.ts +1 -1
- package/src/presets.ts +19 -6
- package/src/provider-settings.ts +204 -0
- package/src/questions.ts +58 -55
- package/src/render/export.ts +7 -7
- package/src/render/lines.ts +24 -12
- package/src/render/markdown.ts +15 -13
- package/src/render/projection.ts +101 -13
- package/src/render/status.ts +76 -71
- package/src/render/text.ts +9 -3
- package/src/settings-file.ts +38 -6
- package/src/skills.ts +19 -6
- 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,
|
|
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
|
-
|
|
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: (
|
|
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)(
|
|
24706
|
-
|
|
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)), ...
|
|
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(
|
|
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
|
|
25793
|
+
for (const cluster of splitGraphemes$1(segment.text)) if (cluster === " ") {
|
|
25763
25794
|
flushWord();
|
|
25764
25795
|
units.push({
|
|
25765
|
-
text:
|
|
25796
|
+
text: cluster,
|
|
25766
25797
|
style: segment.style
|
|
25767
25798
|
});
|
|
25768
|
-
} else if (
|
|
25799
|
+
} else if (graphemeWidth(cluster) > 1) {
|
|
25769
25800
|
flushWord();
|
|
25770
25801
|
units.push({
|
|
25771
|
-
text:
|
|
25802
|
+
text: cluster,
|
|
25772
25803
|
style: segment.style
|
|
25773
25804
|
});
|
|
25774
|
-
} else word +=
|
|
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
|
|
25801
|
-
text:
|
|
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
|
|
25822
|
-
text:
|
|
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
|
|
26042
|
-
const cells =
|
|
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 +=
|
|
26076
|
+
if (previous?.style === segment.style) previous.text += cluster;
|
|
26046
26077
|
else current.push({
|
|
26047
|
-
text:
|
|
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
|
|
26747
|
-
|
|
26748
|
-
|
|
26749
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
27083
|
-
* group: the bar shrinks
|
|
27084
|
-
*
|
|
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
|
|
27089
|
-
*
|
|
27090
|
-
*
|
|
27091
|
-
*
|
|
27092
|
-
* the
|
|
27093
|
-
*
|
|
27094
|
-
*
|
|
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
|
|
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
|
|
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 (
|
|
27114
|
-
text: "█".repeat(
|
|
27700
|
+
if (fill > 0) spans.push({
|
|
27701
|
+
text: "█".repeat(fill),
|
|
27115
27702
|
tone: "ctxFill"
|
|
27116
27703
|
});
|
|
27117
|
-
const
|
|
27118
|
-
if (
|
|
27119
|
-
text: "░".repeat(
|
|
27704
|
+
const free = width - fill;
|
|
27705
|
+
if (free > 0) spans.push({
|
|
27706
|
+
text: "░".repeat(free),
|
|
27120
27707
|
tone: "label"
|
|
27121
27708
|
});
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
|
|
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")
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
|
|
27546
|
-
|
|
27547
|
-
|
|
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
|
|
27676
|
-
if (
|
|
28283
|
+
for (const cluster of splitGraphemes$1(safe)) {
|
|
28284
|
+
if (cluster === "\n") {
|
|
27677
28285
|
flush();
|
|
27678
28286
|
continue;
|
|
27679
28287
|
}
|
|
27680
|
-
const cells =
|
|
28288
|
+
const cells = graphemeWidth(cluster);
|
|
27681
28289
|
if (used > 0 && used + cells > width) flush();
|
|
27682
|
-
appendSegment(current,
|
|
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
|
|
27729
|
-
if (
|
|
28336
|
+
for (const cluster of splitGraphemes$1(safe)) {
|
|
28337
|
+
if (cluster === "\n") {
|
|
27730
28338
|
flush();
|
|
27731
28339
|
continue;
|
|
27732
28340
|
}
|
|
27733
|
-
const cells =
|
|
28341
|
+
const cells = graphemeWidth(cluster);
|
|
27734
28342
|
if (used > 0 && used + cells > budget) flush();
|
|
27735
|
-
appendSegment(current,
|
|
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),
|
|
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(
|
|
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+))?(?:
|
|
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
|
|
28436
|
-
const base = key.alternate
|
|
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(
|
|
28439
|
-
return String.fromCodePoint(
|
|
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
|
|
28569
|
-
|
|
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]
|
|
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.
|
|
29038
|
-
|
|
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
|
|
29440
|
-
* entry, trailing newline). The
|
|
29441
|
-
*
|
|
29442
|
-
*
|
|
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)(
|
|
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)(
|
|
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(
|
|
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)(
|
|
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
|
-
*
|
|
30932
|
-
* multi-select,
|
|
30933
|
-
*
|
|
30934
|
-
*
|
|
30935
|
-
*
|
|
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 [
|
|
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
|
|
30950
|
-
const
|
|
30951
|
-
|
|
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
|
|
30954
|
-
|
|
30955
|
-
|
|
30956
|
-
|
|
30957
|
-
|
|
30958
|
-
|
|
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
|
|
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 (
|
|
31724
|
+
if (currentIndex + 1 >= total) {
|
|
31022
31725
|
setSubmitted(true);
|
|
31023
|
-
store.submit(pending, { answers:
|
|
31726
|
+
store.submit(pending, { answers: pending.request.questions.map((item, at) => answerFromQuestionDraft(item, nextDrafts[at] ?? initialQuestionDraft(item))) });
|
|
31024
31727
|
return;
|
|
31025
31728
|
}
|
|
31026
|
-
|
|
31027
|
-
|
|
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
|
|
31061
|
-
*
|
|
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
|
-
|
|
31066
|
-
|
|
31067
|
-
|
|
31068
|
-
|
|
31069
|
-
|
|
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
|
-
|
|
31083
|
-
|
|
31084
|
-
|
|
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
|
-
|
|
31089
|
-
|
|
31090
|
-
|
|
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
|
-
|
|
31100
|
-
|
|
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
|
-
|
|
31105
|
-
|
|
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
|
-
|
|
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 !== "")
|
|
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
|
-
|
|
31136
|
-
|
|
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
|
-
|
|
31141
|
-
|
|
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
|
-
|
|
31150
|
-
|
|
31151
|
-
|
|
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
|
|
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)(
|
|
31158
|
-
|
|
31159
|
-
const footer =
|
|
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 (
|
|
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
|
|
31242
|
-
|
|
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 ? "" : " ·
|
|
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,
|
|
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 =
|
|
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
|
|
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)(
|
|
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
|
|
31422
|
-
const
|
|
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.
|
|
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
|
|
32237
|
+
}, truncateColumns("↑↓ move · enter configure · l login · o logout · d remove key · x remove provider · r retry · esc back", viewport.contentColumns)));
|
|
31448
32238
|
}
|
|
31449
|
-
|
|
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 [
|
|
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
|
|
31460
|
-
|
|
31461
|
-
|
|
31462
|
-
|
|
31463
|
-
|
|
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.
|
|
32263
|
+
setModels((current) => current.map((model, index) => index === cursor ? {
|
|
31479
32264
|
...model,
|
|
31480
32265
|
...change
|
|
31481
|
-
} : model)
|
|
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
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
|
|
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
|
-
|
|
32395
|
+
setPage("discover");
|
|
31507
32396
|
return;
|
|
31508
32397
|
}
|
|
31509
32398
|
if (key.return) {
|
|
31510
32399
|
submit();
|
|
31511
32400
|
return;
|
|
31512
32401
|
}
|
|
31513
|
-
if (
|
|
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
|
|
31519
|
-
|
|
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
|
|
31523
|
-
|
|
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 (
|
|
31527
|
-
if (
|
|
31528
|
-
|
|
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 (
|
|
31532
|
-
|
|
31533
|
-
|
|
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({ [
|
|
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
|
-
},
|
|
31546
|
-
if (viewport.maxHeight === 0) return (0, import_react.createElement)(
|
|
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(
|
|
31552
|
-
const
|
|
31553
|
-
const
|
|
31554
|
-
|
|
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(
|
|
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("
|
|
32620
|
+
}, truncateColumns("↑↓ move · ←→ in/out · space remove · e efforts · c copy efforts · tab discover · enter save · esc back", viewport.contentColumns)));
|
|
31584
32621
|
}
|
|
31585
|
-
/**
|
|
31586
|
-
|
|
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 [
|
|
31590
|
-
const [
|
|
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
|
|
31593
|
-
|
|
31594
|
-
|
|
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
|
-
|
|
31597
|
-
|
|
31598
|
-
|
|
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
|
-
|
|
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 (
|
|
31606
|
-
if (key.escape) {
|
|
31607
|
-
setDraft("");
|
|
32663
|
+
if (key.escape || input === "q") {
|
|
31608
32664
|
back();
|
|
31609
32665
|
return;
|
|
31610
32666
|
}
|
|
31611
|
-
if (key.
|
|
31612
|
-
|
|
32667
|
+
if (key.ctrl && input === "c") {
|
|
32668
|
+
onExit();
|
|
31613
32669
|
return;
|
|
31614
32670
|
}
|
|
31615
|
-
if (
|
|
31616
|
-
|
|
31617
|
-
|
|
32671
|
+
if (input === "f") {
|
|
32672
|
+
setChecked(/* @__PURE__ */ new Set());
|
|
32673
|
+
setEpoch((current) => current + 1);
|
|
31618
32674
|
return;
|
|
31619
32675
|
}
|
|
31620
|
-
if (
|
|
31621
|
-
|
|
31622
|
-
|
|
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.
|
|
31626
|
-
|
|
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
|
-
|
|
31632
|
-
|
|
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)(
|
|
31635
|
-
const
|
|
31636
|
-
|
|
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(
|
|
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(
|
|
31659
|
-
|
|
31660
|
-
|
|
31661
|
-
|
|
31662
|
-
|
|
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 —
|
|
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("
|
|
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)(
|
|
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)(
|
|
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
|
|
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…")
|
|
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
|
-
|
|
32549
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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(
|
|
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)(
|
|
34764
|
+
else if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderSetupPanel, {
|
|
33617
34765
|
target: providerAction.target,
|
|
33618
|
-
|
|
34766
|
+
effortDonors,
|
|
33619
34767
|
save: props.saveModelProviderConfiguration,
|
|
33620
|
-
|
|
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.
|
|
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
|
|
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/
|
|
35292
|
+
//#region src/input-split.ts
|
|
34156
35293
|
/**
|
|
34157
|
-
*
|
|
34158
|
-
*
|
|
34159
|
-
*
|
|
34160
|
-
*
|
|
34161
|
-
*
|
|
34162
|
-
*
|
|
34163
|
-
*
|
|
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
|
-
|
|
34166
|
-
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
|
|
34170
|
-
|
|
34171
|
-
|
|
34172
|
-
|
|
34173
|
-
|
|
34174
|
-
|
|
34175
|
-
|
|
34176
|
-
|
|
34177
|
-
|
|
34178
|
-
|
|
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
|
-
|
|
35363
|
+
return units;
|
|
34181
35364
|
};
|
|
34182
|
-
if (commands !== void 0) ctx.on("commands/change", () => refresh());
|
|
34183
35365
|
return {
|
|
34184
|
-
|
|
34185
|
-
|
|
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
|
-
|
|
34197
|
-
|
|
34198
|
-
|
|
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
|
-
*
|
|
34204
|
-
*
|
|
34205
|
-
* @
|
|
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
|
|
34208
|
-
|
|
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
|
|
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
|
-
|
|
34232
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
34939
|
-
* @param ctx - context
|
|
34940
|
-
* @
|
|
34941
|
-
*
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
35931
|
-
|
|
35932
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
36448
|
-
|
|
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(() =>
|
|
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
|
-
|
|
36618
|
-
|
|
36619
|
-
|
|
36620
|
-
|
|
36621
|
-
|
|
36622
|
-
|
|
36623
|
-
|
|
36624
|
-
|
|
36625
|
-
|
|
36626
|
-
|
|
36627
|
-
|
|
36628
|
-
|
|
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
|
|
36643
|
-
const line = text
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
37193
|
-
|
|
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 };
|