acryl-cli-linux-x64 0.1.30 → 0.1.35
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/package.json +1 -1
- package/runtime/lib/bin.js +831 -220
- package/runtime/lib/index.js +831 -220
- package/runtime/lib/types/buildInfo.generated.d.ts +4 -0
- package/runtime/lib/types/tui/acrylMark.d.ts +14 -0
- package/runtime/lib/types/tui/actions.d.ts +8 -6
- package/runtime/lib/types/tui/auth-guidance.d.ts +0 -2
- package/runtime/lib/types/tui/login/LoginOverlay.d.ts +46 -11
- package/runtime/lib/types/tui/login/types.d.ts +5 -1
- package/runtime/lib/types/tui/modelProfile/ModelProfileOverlay.d.ts +55 -3
- package/runtime/lib/types/tui/modelProfile/types.d.ts +23 -0
- package/runtime/lib/types/tui/store.d.ts +10 -0
- package/runtime/node_modules/.modules.yaml +2 -2
- package/runtime/node_modules/.package-map.json +1 -1
- package/runtime/node_modules/@earendil-works/pi-ai/dist/auth/oauth/oauth-page.js +1 -1
- package/runtime/node_modules/acryl-control/package.json +1 -1
- package/runtime/node_modules/acryl-harness-runtime/lib/index.mjs +129 -2
- package/runtime/node_modules/acryl-harness-runtime/lib/types/acryl-home.d.ts +35 -0
- package/runtime/node_modules/acryl-harness-runtime/lib/types/index.d.ts +2 -0
- package/runtime/node_modules/acryl-harness-runtime/lib/types/session-bridge.d.ts +13 -0
- package/runtime/node_modules/acryl-harness-runtime/lib/types/session-log-exporter.d.ts +31 -0
- package/runtime/node_modules/acryl-harness-runtime/package.json +1 -1
- package/runtime/node_modules/acryl-harness-runtime/src/acryl-home.ts +50 -0
- package/runtime/node_modules/acryl-harness-runtime/src/index.ts +16 -0
- package/runtime/node_modules/acryl-harness-runtime/src/session-bridge.ts +24 -1
- package/runtime/node_modules/acryl-harness-runtime/src/session-log-exporter.ts +69 -0
- package/runtime/package.json +3 -1
- package/runtime/receipt.json +2 -2
package/runtime/lib/bin.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { bootAcrylHarnessProfile, createAcrylSessionBridge } from "acryl-harness-runtime";
|
|
9
9
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
10
|
import { diffLines } from "diff";
|
|
11
|
-
import { Container, Editor, HStack, Key, KeybindingsManager, ProcessTerminal, ScrollView, TUI_KEYBINDINGS, Text, TuiAltScreen, VStack, matchesKey, setKeybindings, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
11
|
+
import { Container, Editor, HStack, Key, KeybindingsManager, ProcessTerminal, ScrollView, TUI_KEYBINDINGS, Text, TuiAltScreen, VStack, fuzzyFilter, matchesKey, setKeybindings, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
12
12
|
import { readdir } from "node:fs/promises";
|
|
13
13
|
import { ManualCompactionError } from "@deepseek-ai/dsh-compaction";
|
|
14
14
|
import { GoalError } from "@deepseek-ai/dsh-goal";
|
|
@@ -750,6 +750,7 @@ var TuiStore = class {
|
|
|
750
750
|
title: void 0,
|
|
751
751
|
stats: EMPTY_STATS,
|
|
752
752
|
preset: void 0,
|
|
753
|
+
activeModel: void 0,
|
|
753
754
|
streaming: void 0,
|
|
754
755
|
pendingToolCalls: this.pendingToolCallsSnapshot(),
|
|
755
756
|
shellRun: void 0,
|
|
@@ -860,6 +861,10 @@ var TuiStore = class {
|
|
|
860
861
|
setPreset(preset) {
|
|
861
862
|
this.set({ preset });
|
|
862
863
|
}
|
|
864
|
+
/** Record a live `/model` switch so the status bar reflects it immediately. */
|
|
865
|
+
setActiveModel(activeModel) {
|
|
866
|
+
this.set({ activeModel });
|
|
867
|
+
}
|
|
863
868
|
shellRunSeq = 0;
|
|
864
869
|
/** Begin one local shell-escape run; its output accumulates via `appendShellOutput` until `finishShellRun` settles it into the transcript. */
|
|
865
870
|
startShellRun(command) {
|
|
@@ -916,7 +921,6 @@ var TuiStore = class {
|
|
|
916
921
|
kind: "login",
|
|
917
922
|
login: {
|
|
918
923
|
flows: void 0,
|
|
919
|
-
selected: 0,
|
|
920
924
|
signingIn: void 0,
|
|
921
925
|
prompt: void 0,
|
|
922
926
|
busy: true,
|
|
@@ -1040,6 +1044,36 @@ var TuiStore = class {
|
|
|
1040
1044
|
}
|
|
1041
1045
|
};
|
|
1042
1046
|
//#endregion
|
|
1047
|
+
//#region src/tui/acrylMark.ts
|
|
1048
|
+
/**
|
|
1049
|
+
* The compact ACRYL wordmark shown in the fixed header, next to the pet.
|
|
1050
|
+
*
|
|
1051
|
+
* This is the real pixelized brand wordmark (`assets/pixelized/`), rendered
|
|
1052
|
+
* natively as half-block characters (each terminal row packs 2 pixel rows
|
|
1053
|
+
* via \u2580/\u2584/\u2588) instead of downsampled or replaced with a different mark \u2014 54
|
|
1054
|
+
* columns wide at the artwork's own resolution, not simplified. Generated
|
|
1055
|
+
* from the SVG's rect grid and verified by printing it before wiring into
|
|
1056
|
+
* the header \u2014 same proven-before-embedding convention as
|
|
1057
|
+
* `yly/compile-sprites.mjs`.
|
|
1058
|
+
* @module @tomowave/dsh-tui/tui/acrylMark
|
|
1059
|
+
*/
|
|
1060
|
+
/** The plain (uncolored) rows of the ACRYL mark \u2014 11 lines, 54 columns. */
|
|
1061
|
+
const ACRYL_MARK_ROWS = [
|
|
1062
|
+
" ▄▀▀▀▀▀▀▀▀ ",
|
|
1063
|
+
" ▄▀ ▄▄▄▄▄▄▄▄▄ ",
|
|
1064
|
+
" ▄▄ ▄ ▀▄ ",
|
|
1065
|
+
" ▄█▀█▄ ▄▀ ▄▄▄▄▄█ ",
|
|
1066
|
+
" ▄█▀ ▀█▀▀▀▀█▄ ▀▄ ▄▀ ",
|
|
1067
|
+
" █▀ ▀▄ ▀█ ▀▄▄ ▄█ ",
|
|
1068
|
+
"▀▀ ▀ ▀█▄ ▄ ▄██▄▄▄▄▄▄▄▄▄▄",
|
|
1069
|
+
" ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▄ ▄▀▀ ",
|
|
1070
|
+
" ▀▄▄▄▀ ",
|
|
1071
|
+
" █ ",
|
|
1072
|
+
" ▀ "
|
|
1073
|
+
];
|
|
1074
|
+
/** ISO timestamp this build was produced at. */
|
|
1075
|
+
const BUILD_TIME = "2026-09-08T14:33:49.586Z";
|
|
1076
|
+
//#endregion
|
|
1043
1077
|
//#region src/tui/statsFormat.ts
|
|
1044
1078
|
/**
|
|
1045
1079
|
* Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three digits).
|
|
@@ -1193,7 +1227,7 @@ function stripSessionIdPrefix(id) {
|
|
|
1193
1227
|
//#region src/tui/liveText.ts
|
|
1194
1228
|
const dim = fg(theme.muted);
|
|
1195
1229
|
const accent = fg(theme.accent);
|
|
1196
|
-
const warning$
|
|
1230
|
+
const warning$2 = fg(theme.warning);
|
|
1197
1231
|
function buildStatusBarText(params) {
|
|
1198
1232
|
const { sessionId, provider, model, status, queuedCount, presetLabel, eventCount, spinnerChar } = params;
|
|
1199
1233
|
const queuedSuffix = queuedCount > 0 ? ` · ${queuedCount} queued` : "";
|
|
@@ -1236,7 +1270,7 @@ const PERMISSION_COLORS = {
|
|
|
1236
1270
|
*/
|
|
1237
1271
|
function buildUpdateHintText(currentVersion, latestVersion) {
|
|
1238
1272
|
if (latestVersion === void 0) return "";
|
|
1239
|
-
return warning$
|
|
1273
|
+
return warning$2(`⬆ ACRYL update available: v${currentVersion} → v${latestVersion}`) + dim(" (run `pnpm add -g @acryl/cli` to upgrade)");
|
|
1240
1274
|
}
|
|
1241
1275
|
function buildPermissionText(permission) {
|
|
1242
1276
|
if (permission === void 0) return "";
|
|
@@ -2856,27 +2890,92 @@ function miniTextFieldInput(state, data) {
|
|
|
2856
2890
|
cursor: state.cursor + data.length
|
|
2857
2891
|
};
|
|
2858
2892
|
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Mask a value for display while typing, revealing the first/last couple of
|
|
2895
|
+
* characters instead of hiding it entirely — a fully-masked live field looks
|
|
2896
|
+
* indistinguishable from an empty one, giving no confirmation that anything
|
|
2897
|
+
* was actually typed. Length-preserving (unlike a static preview's `sk-p…9sZ4`
|
|
2898
|
+
* ellipsis form) so cursor-position math against the masked string stays
|
|
2899
|
+
* correct: every hidden character becomes exactly one `mask` character.
|
|
2900
|
+
*/
|
|
2901
|
+
function partiallyMask(value, mask) {
|
|
2902
|
+
if (value.length <= 2) return mask.repeat(value.length);
|
|
2903
|
+
if (value.length <= 10) return `${value[0]}${mask.repeat(value.length - 2)}${value.at(-1)}`;
|
|
2904
|
+
return `${value.slice(0, 4)}${mask.repeat(value.length - 8)}${value.slice(-4)}`;
|
|
2905
|
+
}
|
|
2859
2906
|
/** Render the field's text, optionally with an inverse-video cursor block at the cursor position. */
|
|
2860
2907
|
function renderMiniTextField(state, cursorVisible, mask) {
|
|
2861
|
-
const display = mask === void 0 ? state.value :
|
|
2908
|
+
const display = mask === void 0 ? state.value : partiallyMask(state.value, mask);
|
|
2862
2909
|
if (!cursorVisible) return display;
|
|
2863
2910
|
return `${display.slice(0, state.cursor)}\x1b[7m${display[state.cursor] ?? " "}\x1b[0m${display.slice(state.cursor + 1)}`;
|
|
2864
2911
|
}
|
|
2865
2912
|
//#endregion
|
|
2913
|
+
//#region src/tui/modelProfile/types.ts
|
|
2914
|
+
/**
|
|
2915
|
+
* Data shapes for the `/model` provider-profile overlay: the read model that
|
|
2916
|
+
* joins `ctx.llm`'s provider directory with `ctx.settings`' stored sections,
|
|
2917
|
+
* the raw shape stored at each provider's settings path, and the mutable
|
|
2918
|
+
* draft one add/edit form works with before a save round-trips it back.
|
|
2919
|
+
* @module @tomowang/dsh-tui/tui/modelProfile/types
|
|
2920
|
+
*/
|
|
2921
|
+
/**
|
|
2922
|
+
* The `apiKeyEnv` reference a provider route falls back to when its settings
|
|
2923
|
+
* profile names none, e.g. `my-proxy` -> `MY_PROXY_API_KEY`. Shared between
|
|
2924
|
+
* the overlay (recomputing it for a just-typed custom route before save) and
|
|
2925
|
+
* the host wiring (deriving it for every already-configured row), so the
|
|
2926
|
+
* naming rule lives in exactly one place.
|
|
2927
|
+
*/
|
|
2928
|
+
function deriveApiKeyRef(route) {
|
|
2929
|
+
const upper = route.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
2930
|
+
return `${/^[A-Z_]/.test(upper) ? upper : `P_${upper}`}_API_KEY`;
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* A short, identifying (never full) preview of a stored secret — first 4 and
|
|
2934
|
+
* last 4 characters, so the API-key field isn't blank-looking when a key is
|
|
2935
|
+
* already set and the user can tell *which* key it is without ever seeing
|
|
2936
|
+
* enough of it to be usable on its own. Short values (<= 10 chars, where that
|
|
2937
|
+
* split would show most of the string anyway) mask everything but the first
|
|
2938
|
+
* and last single character instead.
|
|
2939
|
+
*/
|
|
2940
|
+
function maskKeyPreview(value) {
|
|
2941
|
+
if (value.length <= 10) return value.length <= 2 ? "*".repeat(value.length) : `${value[0]}${"*".repeat(value.length - 2)}${value.at(-1)}`;
|
|
2942
|
+
return `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
2943
|
+
}
|
|
2944
|
+
//#endregion
|
|
2866
2945
|
//#region src/tui/modelProfile/ModelProfileOverlay.ts
|
|
2867
2946
|
const bold$9 = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
2868
2947
|
const secondary$9 = fg(theme.secondary);
|
|
2869
2948
|
const muted$10 = fg(theme.muted);
|
|
2949
|
+
const warning$1 = fg(theme.warning);
|
|
2870
2950
|
const errorColor$5 = fg(theme.error);
|
|
2871
2951
|
const invert$5 = (s) => `\x1b[7m${s}\x1b[0m`;
|
|
2952
|
+
/**
|
|
2953
|
+
* The only wire protocols `dsh-llm-pi-ai` can speak (`supportedProtocols()`),
|
|
2954
|
+
* matching the DSH Desktop GUI's "Custom provider" dialog's own dropdown
|
|
2955
|
+
* exactly. Free-text protocol entry let a typo reach `assertServiceable` as a
|
|
2956
|
+
* runtime save failure instead of being impossible to enter in the first
|
|
2957
|
+
* place, so this is a cycling choice, never a typed field.
|
|
2958
|
+
*/
|
|
2959
|
+
const PROTOCOLS = [
|
|
2960
|
+
"openai-completions",
|
|
2961
|
+
"openai-responses",
|
|
2962
|
+
"anthropic-messages"
|
|
2963
|
+
];
|
|
2872
2964
|
var ModelProfileOverlay = class {
|
|
2965
|
+
tui;
|
|
2873
2966
|
store;
|
|
2874
2967
|
actions;
|
|
2968
|
+
mode = "picker";
|
|
2969
|
+
modelSearch = emptyMiniTextField();
|
|
2970
|
+
modelPickerCursor = 0;
|
|
2971
|
+
providerSearch = emptyMiniTextField();
|
|
2972
|
+
providerCursor = 0;
|
|
2875
2973
|
confirmDelete;
|
|
2974
|
+
pendingShowModels = false;
|
|
2876
2975
|
formKeySeen = -1;
|
|
2877
2976
|
route = emptyMiniTextField();
|
|
2878
2977
|
displayName = emptyMiniTextField();
|
|
2879
|
-
api =
|
|
2978
|
+
api = PROTOCOLS[0];
|
|
2880
2979
|
baseURL = emptyMiniTextField();
|
|
2881
2980
|
apiKeyDraft = emptyMiniTextField();
|
|
2882
2981
|
models = [];
|
|
@@ -2884,25 +2983,58 @@ var ModelProfileOverlay = class {
|
|
|
2884
2983
|
focused = 0;
|
|
2885
2984
|
modelDraftId = emptyMiniTextField();
|
|
2886
2985
|
modelSelected = 0;
|
|
2887
|
-
|
|
2888
|
-
|
|
2986
|
+
constructor(tui, store, actions) {
|
|
2987
|
+
this.tui = tui;
|
|
2889
2988
|
this.store = store;
|
|
2890
2989
|
this.actions = actions;
|
|
2891
2990
|
}
|
|
2892
2991
|
invalidate() {}
|
|
2992
|
+
/**
|
|
2993
|
+
* Rows available for a scrollable list body: terminal height minus the
|
|
2994
|
+
* lines every such screen spends on chrome (header/hint/notice/search —
|
|
2995
|
+
* `chrome` lines, caller-counted since it varies by screen). Long catalogs
|
|
2996
|
+
* (~35 providers, matching that many models) previously rendered every row
|
|
2997
|
+
* unconditionally, pushing the key-legend hint line — the only place a
|
|
2998
|
+
* shortcut is documented — past the bottom of the terminal, invisible
|
|
2999
|
+
* without scrolling back. Every list-shaped view here windows around the
|
|
3000
|
+
* current selection instead, so the hint line is always the last line
|
|
3001
|
+
* printed and always fits on screen.
|
|
3002
|
+
*/
|
|
3003
|
+
listWindow(chrome) {
|
|
3004
|
+
return Math.max(3, this.tui.terminal.rows - chrome);
|
|
3005
|
+
}
|
|
3006
|
+
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
3007
|
+
visibleRange(count, selected, maxVisible) {
|
|
3008
|
+
if (count <= maxVisible) return {
|
|
3009
|
+
start: 0,
|
|
3010
|
+
end: count
|
|
3011
|
+
};
|
|
3012
|
+
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
3013
|
+
return {
|
|
3014
|
+
start,
|
|
3015
|
+
end: start + maxVisible
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* The global one-line notice (`store.setNotice`), rendered inline here.
|
|
3020
|
+
* `/model` runs as a full-screen overlay that paints over the whole
|
|
3021
|
+
* terminal, so the notice dock underneath — where "Saved X."/"Active model
|
|
3022
|
+
* set to X." otherwise lands — is invisible for as long as this overlay
|
|
3023
|
+
* stays open (e.g. right after `saveProvider` returns to the list view
|
|
3024
|
+
* without closing the overlay). Surfacing it here is what makes a save
|
|
3025
|
+
* actually visible as "done" instead of the screen looking unchanged.
|
|
3026
|
+
*/
|
|
3027
|
+
noticeLines() {
|
|
3028
|
+
const notice = this.store.getSnapshot().notice;
|
|
3029
|
+
return notice === void 0 ? [] : [muted$10(notice)];
|
|
3030
|
+
}
|
|
2893
3031
|
textFields(draft) {
|
|
2894
|
-
|
|
3032
|
+
const base = draft.isNew ? [
|
|
2895
3033
|
"route",
|
|
2896
3034
|
"displayName",
|
|
2897
|
-
"
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
] : [
|
|
2901
|
-
"displayName",
|
|
2902
|
-
"api",
|
|
2903
|
-
"baseURL",
|
|
2904
|
-
"apiKey"
|
|
2905
|
-
];
|
|
3035
|
+
"baseURL"
|
|
3036
|
+
] : ["displayName", "baseURL"];
|
|
3037
|
+
return draft.authMethod === "oauth" ? base : [...base, "apiKey"];
|
|
2906
3038
|
}
|
|
2907
3039
|
/** Reinitialize form-local state from the store's draft when `formKey` changes — the equivalent of the old `key={formKey}` remount. */
|
|
2908
3040
|
syncFormState(mp) {
|
|
@@ -2912,24 +3044,29 @@ var ModelProfileOverlay = class {
|
|
|
2912
3044
|
this.formKeySeen = mp.formKey;
|
|
2913
3045
|
this.route = emptyMiniTextField(draft.route);
|
|
2914
3046
|
this.displayName = emptyMiniTextField(draft.displayName);
|
|
2915
|
-
this.api =
|
|
3047
|
+
this.api = PROTOCOLS.includes(draft.api) ? draft.api : PROTOCOLS[0];
|
|
2916
3048
|
this.baseURL = emptyMiniTextField(draft.baseURL);
|
|
2917
3049
|
this.apiKeyDraft = emptyMiniTextField("");
|
|
2918
3050
|
this.models = [...draft.models];
|
|
2919
|
-
this.showModels =
|
|
3051
|
+
this.showModels = this.pendingShowModels;
|
|
3052
|
+
this.pendingShowModels = false;
|
|
2920
3053
|
this.focused = 0;
|
|
2921
3054
|
this.modelDraftId = emptyMiniTextField();
|
|
2922
3055
|
this.modelSelected = 0;
|
|
2923
|
-
this.modelInputFocused = false;
|
|
2924
3056
|
}
|
|
2925
3057
|
return draft;
|
|
2926
3058
|
}
|
|
2927
3059
|
buildDraft(draft) {
|
|
3060
|
+
const route = draft.isNew ? this.route.value.trim() : draft.route;
|
|
2928
3061
|
return {
|
|
2929
3062
|
...draft,
|
|
2930
|
-
route
|
|
3063
|
+
route,
|
|
3064
|
+
...draft.isNew ? {
|
|
3065
|
+
settingsPath: ["providers", route],
|
|
3066
|
+
apiKeyRef: deriveApiKeyRef(route)
|
|
3067
|
+
} : {},
|
|
2931
3068
|
displayName: this.displayName.value,
|
|
2932
|
-
api: this.api
|
|
3069
|
+
api: this.api,
|
|
2933
3070
|
baseURL: this.baseURL.value,
|
|
2934
3071
|
apiKeyDraft: this.apiKeyDraft.value,
|
|
2935
3072
|
models: this.models
|
|
@@ -2940,111 +3077,223 @@ var ModelProfileOverlay = class {
|
|
|
2940
3077
|
if (overlay.kind !== "modelProfile") return [];
|
|
2941
3078
|
const mp = overlay.modelProfile;
|
|
2942
3079
|
const draft = this.syncFormState(mp);
|
|
2943
|
-
if (draft !== void 0)
|
|
2944
|
-
|
|
3080
|
+
if (draft !== void 0) {
|
|
3081
|
+
if (this.showModels) return this.renderModelListEditor(mp);
|
|
3082
|
+
return this.renderForm(draft, mp);
|
|
3083
|
+
}
|
|
3084
|
+
return this.mode === "picker" ? this.renderModelPicker(mp) : this.renderList(mp);
|
|
3085
|
+
}
|
|
3086
|
+
pickerItems(mp) {
|
|
3087
|
+
const items = [];
|
|
3088
|
+
for (const row of mp.providers ?? []) {
|
|
3089
|
+
if (!row.live) continue;
|
|
3090
|
+
for (const model of row.models) items.push({
|
|
3091
|
+
route: row.route,
|
|
3092
|
+
id: model.id,
|
|
3093
|
+
authMethod: row.authMethod
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
return items;
|
|
3097
|
+
}
|
|
3098
|
+
filteredPickerItems(mp) {
|
|
3099
|
+
const items = this.pickerItems(mp);
|
|
3100
|
+
const query = this.modelSearch.value.trim();
|
|
3101
|
+
return query === "" ? items : fuzzyFilter([...items], query, (item) => `${item.id} ${item.route}`);
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* pi.dev's `/model` picker (see Image #45): a flat, fuzzy-searchable list of
|
|
3105
|
+
* every model on a live (actually usable) route, not a per-provider drill-down.
|
|
3106
|
+
* Provider add/edit/delete moves to a secondary `p` mode instead of being the
|
|
3107
|
+
* default, since picking a model to make active is the far more common action.
|
|
3108
|
+
*/
|
|
3109
|
+
renderModelPicker(mp) {
|
|
3110
|
+
const lines = [warning$1("Only showing models from configured providers. Use /login to add providers.")];
|
|
3111
|
+
lines.push(...this.noticeLines());
|
|
3112
|
+
if (mp.error !== void 0) lines.push(errorColor$5(mp.error));
|
|
3113
|
+
lines.push(`> ${renderMiniTextField(this.modelSearch, true)}`);
|
|
3114
|
+
if (mp.busy && mp.providers === void 0) lines.push(muted$10("Loading…"));
|
|
3115
|
+
const all = this.pickerItems(mp);
|
|
3116
|
+
const filtered = this.filteredPickerItems(mp);
|
|
3117
|
+
const chrome = lines.length + 2;
|
|
3118
|
+
const maxVisible = this.listWindow(chrome);
|
|
3119
|
+
const { start, end } = this.visibleRange(filtered.length, this.modelPickerCursor, maxVisible);
|
|
3120
|
+
filtered.slice(start, end).forEach((item, offset) => {
|
|
3121
|
+
const isSelected = start + offset === this.modelPickerCursor;
|
|
3122
|
+
const via = item.authMethod === "oauth" ? " oauth" : item.authMethod === "api-key" ? " api" : "";
|
|
3123
|
+
const text = `${isSelected ? "→ " : " "}${item.id} [${item.route}${via}]`;
|
|
3124
|
+
lines.push(isSelected ? invert$5(text) : text);
|
|
3125
|
+
});
|
|
3126
|
+
if (filtered.length > maxVisible) lines.push(muted$10(`(${this.modelPickerCursor + 1}/${filtered.length})`));
|
|
3127
|
+
if (all.length === 0 && mp.providers !== void 0) lines.push(muted$10("No models available yet — use /login to sign in to a provider."));
|
|
3128
|
+
else if (filtered.length === 0) lines.push(muted$10("No matching models."));
|
|
3129
|
+
lines.push(muted$10("type to search · ↑↓ select · enter set active · ctrl+p manage providers (add/remove models) · esc close"));
|
|
3130
|
+
return lines;
|
|
3131
|
+
}
|
|
3132
|
+
handleModelPickerInput(data, mp) {
|
|
3133
|
+
if (matchesKey(data, Key.escape)) {
|
|
3134
|
+
this.actions.closeModelProfile();
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
if (matchesKey(data, Key.ctrl("p"))) {
|
|
3138
|
+
this.mode = "providers";
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
const filtered = this.filteredPickerItems(mp);
|
|
3142
|
+
if (matchesKey(data, Key.up)) {
|
|
3143
|
+
this.modelPickerCursor = Math.max(0, this.modelPickerCursor - 1);
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
if (matchesKey(data, Key.down)) {
|
|
3147
|
+
this.modelPickerCursor = Math.min(Math.max(0, filtered.length - 1), this.modelPickerCursor + 1);
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
if (matchesKey(data, Key.enter)) {
|
|
3151
|
+
const item = filtered[this.modelPickerCursor];
|
|
3152
|
+
if (item !== void 0) {
|
|
3153
|
+
this.actions.setActiveModel(item.route, item.id);
|
|
3154
|
+
this.actions.closeModelProfile();
|
|
3155
|
+
}
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
const next = miniTextFieldInput(this.modelSearch, data);
|
|
3159
|
+
if (next !== void 0) {
|
|
3160
|
+
this.modelSearch = next;
|
|
3161
|
+
this.modelPickerCursor = 0;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
filteredProviders(mp) {
|
|
3165
|
+
const providers = mp.providers ?? [];
|
|
3166
|
+
const query = this.providerSearch.value.trim();
|
|
3167
|
+
return query === "" ? providers : fuzzyFilter([...providers], query, (row) => `${row.displayName} ${row.route}`);
|
|
2945
3168
|
}
|
|
2946
3169
|
renderList(mp) {
|
|
2947
|
-
const { providers,
|
|
3170
|
+
const { providers, busy, error } = mp;
|
|
2948
3171
|
const lines = [bold$9(secondary$9("Model providers"))];
|
|
3172
|
+
lines.push(...this.noticeLines());
|
|
2949
3173
|
if (error !== void 0) lines.push(errorColor$5(error));
|
|
3174
|
+
lines.push(`> ${renderMiniTextField(this.providerSearch, true)}`);
|
|
2950
3175
|
if (busy && providers === void 0) lines.push(muted$10("Loading…"));
|
|
2951
|
-
|
|
3176
|
+
const filtered = this.filteredProviders(mp);
|
|
3177
|
+
const chrome = lines.length + 2;
|
|
3178
|
+
const maxVisible = this.listWindow(chrome);
|
|
3179
|
+
const { start, end } = this.visibleRange(filtered.length, this.providerCursor, maxVisible);
|
|
3180
|
+
filtered.slice(start, end).forEach((row, offset) => {
|
|
3181
|
+
const index = start + offset;
|
|
2952
3182
|
const marker = row.configured ? "● " : "○ ";
|
|
2953
3183
|
const active = row.live ? " (active)" : "";
|
|
2954
|
-
const noKey = row.apiKeyConfigured ? "" : " [no api key]";
|
|
2955
|
-
const confirm = this.confirmDelete === index ? " — press
|
|
2956
|
-
const text = `${index ===
|
|
2957
|
-
lines.push(index ===
|
|
3184
|
+
const noKey = row.apiKeyConfigured ? ` [${row.authMethod === "oauth" ? "oauth" : "api key"}]` : " [no api key]";
|
|
3185
|
+
const confirm = this.confirmDelete === index ? " — press ctrl+x again to delete" : "";
|
|
3186
|
+
const text = `${index === this.providerCursor ? "› " : " "}${marker}${row.displayName}${active}${noKey}${confirm}`;
|
|
3187
|
+
lines.push(index === this.providerCursor ? invert$5(text) : text);
|
|
2958
3188
|
});
|
|
2959
|
-
if (
|
|
2960
|
-
lines.push(muted$10("
|
|
3189
|
+
if (filtered.length > maxVisible) lines.push(muted$10(`(${this.providerCursor + 1}/${filtered.length})`));
|
|
3190
|
+
if (providers?.length === 0) lines.push(muted$10("No providers configured yet — ctrl+n to add one."));
|
|
3191
|
+
else if (filtered.length === 0) lines.push(muted$10("No matching providers."));
|
|
3192
|
+
lines.push(muted$10("type to search · ↑↓ select · enter edit · ctrl+n add · ctrl+x delete · ctrl+e edit models · ctrl+a set active model · esc back"));
|
|
2961
3193
|
return lines;
|
|
2962
3194
|
}
|
|
2963
3195
|
handleListInput(data, mp) {
|
|
2964
|
-
const { providers, selected } = mp;
|
|
2965
3196
|
if (matchesKey(data, Key.escape)) {
|
|
2966
|
-
this.
|
|
3197
|
+
this.mode = "picker";
|
|
2967
3198
|
return;
|
|
2968
3199
|
}
|
|
2969
|
-
if (
|
|
2970
|
-
|
|
3200
|
+
if (matchesKey(data, Key.ctrl("n"))) {
|
|
3201
|
+
this.actions.createProvider();
|
|
2971
3202
|
return;
|
|
2972
3203
|
}
|
|
3204
|
+
const filtered = this.filteredProviders(mp);
|
|
3205
|
+
if (filtered.length === 0) return;
|
|
2973
3206
|
if (matchesKey(data, Key.up)) {
|
|
2974
3207
|
this.confirmDelete = void 0;
|
|
2975
|
-
this.
|
|
3208
|
+
this.providerCursor = Math.max(0, this.providerCursor - 1);
|
|
2976
3209
|
return;
|
|
2977
3210
|
}
|
|
2978
3211
|
if (matchesKey(data, Key.down)) {
|
|
2979
3212
|
this.confirmDelete = void 0;
|
|
2980
|
-
this.
|
|
3213
|
+
this.providerCursor = Math.min(filtered.length - 1, this.providerCursor + 1);
|
|
2981
3214
|
return;
|
|
2982
3215
|
}
|
|
2983
3216
|
if (matchesKey(data, Key.enter)) {
|
|
2984
|
-
this.actions.editProvider(
|
|
3217
|
+
this.actions.editProvider(filtered[this.providerCursor].route);
|
|
2985
3218
|
return;
|
|
2986
3219
|
}
|
|
2987
|
-
if (data
|
|
2988
|
-
this.
|
|
3220
|
+
if (matchesKey(data, Key.ctrl("e"))) {
|
|
3221
|
+
this.pendingShowModels = true;
|
|
3222
|
+
this.actions.editProvider(filtered[this.providerCursor].route);
|
|
2989
3223
|
return;
|
|
2990
3224
|
}
|
|
2991
|
-
if (data
|
|
2992
|
-
const row =
|
|
3225
|
+
if (matchesKey(data, Key.ctrl("a"))) {
|
|
3226
|
+
const row = filtered[this.providerCursor];
|
|
2993
3227
|
const model = row.models[0];
|
|
2994
3228
|
if (model !== void 0) this.actions.setActiveModel(row.route, model.id);
|
|
2995
3229
|
return;
|
|
2996
3230
|
}
|
|
2997
|
-
if (data
|
|
2998
|
-
if (this.confirmDelete ===
|
|
3231
|
+
if (matchesKey(data, Key.ctrl("x"))) {
|
|
3232
|
+
if (this.confirmDelete === this.providerCursor) {
|
|
2999
3233
|
this.confirmDelete = void 0;
|
|
3000
|
-
this.actions.deleteProvider(
|
|
3001
|
-
} else this.confirmDelete =
|
|
3234
|
+
this.actions.deleteProvider(filtered[this.providerCursor]);
|
|
3235
|
+
} else this.confirmDelete = this.providerCursor;
|
|
3002
3236
|
return;
|
|
3003
3237
|
}
|
|
3004
3238
|
this.confirmDelete = void 0;
|
|
3239
|
+
const next = miniTextFieldInput(this.providerSearch, data);
|
|
3240
|
+
if (next !== void 0) {
|
|
3241
|
+
this.providerSearch = next;
|
|
3242
|
+
this.providerCursor = 0;
|
|
3243
|
+
}
|
|
3005
3244
|
}
|
|
3006
3245
|
renderForm(draft, mp) {
|
|
3007
3246
|
const textFields = this.textFields(draft);
|
|
3008
|
-
const
|
|
3009
|
-
const
|
|
3247
|
+
const protocolRow = textFields.length;
|
|
3248
|
+
const modelsRow = protocolRow + 1;
|
|
3249
|
+
const saveRow = protocolRow + 2;
|
|
3010
3250
|
const fieldState = {
|
|
3011
3251
|
route: this.route,
|
|
3012
3252
|
displayName: this.displayName,
|
|
3013
|
-
api: this.api,
|
|
3014
3253
|
baseURL: this.baseURL,
|
|
3015
3254
|
apiKey: this.apiKeyDraft
|
|
3016
3255
|
};
|
|
3017
3256
|
const labels = {
|
|
3018
|
-
route: "
|
|
3019
|
-
displayName: "
|
|
3020
|
-
api: "Protocol",
|
|
3257
|
+
route: "Provider ID",
|
|
3258
|
+
displayName: "Display name",
|
|
3021
3259
|
baseURL: "Base URL",
|
|
3022
|
-
apiKey: draft.apiKeyConfigured ?
|
|
3260
|
+
apiKey: draft.apiKeyConfigured ? `API key (set${draft.apiKeyPreview === void 0 ? "" : `: ${draft.apiKeyPreview}`} — leave blank to keep)` : "API key"
|
|
3023
3261
|
};
|
|
3024
|
-
const lines = [bold$9(secondary$9(draft.isNew ? "
|
|
3262
|
+
const lines = [bold$9(secondary$9(draft.isNew ? "Custom provider" : `Edit ${draft.displayName || draft.route}`))];
|
|
3263
|
+
lines.push(...this.noticeLines());
|
|
3025
3264
|
if (mp.error !== void 0) lines.push(errorColor$5(mp.error));
|
|
3265
|
+
if (draft.authMethod === "oauth") lines.push(muted$10("(signed in via OAuth — no separate API key for this route)"));
|
|
3026
3266
|
textFields.forEach((field, index) => {
|
|
3027
3267
|
const isFocused = this.focused === index;
|
|
3028
3268
|
const mask = field === "apiKey" ? "*" : void 0;
|
|
3029
3269
|
const prefix = `${isFocused ? "› " : " "}${labels[field]}: `;
|
|
3030
3270
|
lines.push(`${prefix}${renderMiniTextField(fieldState[field], isFocused, mask)}`);
|
|
3031
3271
|
});
|
|
3272
|
+
const protocolFocused = this.focused === protocolRow;
|
|
3273
|
+
const protocolText = `${protocolFocused ? "› " : " "}API protocol: ‹ ${this.api} ›`;
|
|
3274
|
+
lines.push(protocolFocused ? invert$5(protocolText) : protocolText);
|
|
3032
3275
|
const modelsText = `${this.focused === modelsRow ? "› " : " "}Models (${this.models.length}) — enter to edit`;
|
|
3033
3276
|
lines.push(this.focused === modelsRow ? invert$5(modelsText) : modelsText);
|
|
3034
|
-
const saveText = `${this.focused === saveRow ? "› " : " "}${mp.busy ? "Saving…" : "Save"}`;
|
|
3277
|
+
const saveText = `${this.focused === saveRow ? "› " : " "}${mp.busy ? "Saving…" : draft.isNew ? "Create provider" : "Save changes"}`;
|
|
3035
3278
|
lines.push(this.focused === saveRow ? invert$5(saveText) : saveText);
|
|
3036
|
-
|
|
3279
|
+
const removeHint = draft.apiKeyConfigured ? " · ctrl+d sign out (remove key/OAuth grant)" : "";
|
|
3280
|
+
lines.push(muted$10(`tab/shift+tab move · ←→ cycle protocol · enter confirm/activate · esc cancel${removeHint}`));
|
|
3037
3281
|
return lines;
|
|
3038
3282
|
}
|
|
3039
3283
|
handleFormInput(data, draft) {
|
|
3040
3284
|
const textFields = this.textFields(draft);
|
|
3041
|
-
const
|
|
3042
|
-
const
|
|
3043
|
-
const
|
|
3285
|
+
const protocolRow = textFields.length;
|
|
3286
|
+
const modelsRow = protocolRow + 1;
|
|
3287
|
+
const saveRow = protocolRow + 2;
|
|
3288
|
+
const rowCount = protocolRow + 3;
|
|
3044
3289
|
if (matchesKey(data, Key.escape)) {
|
|
3045
3290
|
this.actions.backToProviderList();
|
|
3046
3291
|
return;
|
|
3047
3292
|
}
|
|
3293
|
+
if (matchesKey(data, Key.ctrl("d")) && draft.apiKeyConfigured) {
|
|
3294
|
+
this.actions.clearApiKey(draft);
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3048
3297
|
if (matchesKey(data, "shift+tab")) {
|
|
3049
3298
|
this.focused = (this.focused - 1 + rowCount) % rowCount;
|
|
3050
3299
|
return;
|
|
@@ -3053,6 +3302,12 @@ var ModelProfileOverlay = class {
|
|
|
3053
3302
|
this.focused = (this.focused + 1) % rowCount;
|
|
3054
3303
|
return;
|
|
3055
3304
|
}
|
|
3305
|
+
if (this.focused === protocolRow && (matchesKey(data, Key.left) || matchesKey(data, Key.right))) {
|
|
3306
|
+
const direction = matchesKey(data, Key.left) ? -1 : 1;
|
|
3307
|
+
const currentIndex = PROTOCOLS.indexOf(this.api);
|
|
3308
|
+
this.api = PROTOCOLS[(currentIndex + direction + PROTOCOLS.length) % PROTOCOLS.length];
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3056
3311
|
if (matchesKey(data, Key.enter) && this.focused === modelsRow) {
|
|
3057
3312
|
this.showModels = true;
|
|
3058
3313
|
return;
|
|
@@ -3061,7 +3316,7 @@ var ModelProfileOverlay = class {
|
|
|
3061
3316
|
this.actions.saveProvider(this.buildDraft(draft));
|
|
3062
3317
|
return;
|
|
3063
3318
|
}
|
|
3064
|
-
if (matchesKey(data, Key.enter) && this.focused
|
|
3319
|
+
if (matchesKey(data, Key.enter) && this.focused <= protocolRow) {
|
|
3065
3320
|
this.focused = (this.focused + 1) % rowCount;
|
|
3066
3321
|
return;
|
|
3067
3322
|
}
|
|
@@ -3070,34 +3325,43 @@ var ModelProfileOverlay = class {
|
|
|
3070
3325
|
const next = miniTextFieldInput({
|
|
3071
3326
|
route: this.route,
|
|
3072
3327
|
displayName: this.displayName,
|
|
3073
|
-
api: this.api,
|
|
3074
3328
|
baseURL: this.baseURL,
|
|
3075
3329
|
apiKey: this.apiKeyDraft
|
|
3076
3330
|
}[field], data);
|
|
3077
3331
|
if (next === void 0) return;
|
|
3078
3332
|
if (field === "route") this.route = next;
|
|
3079
3333
|
else if (field === "displayName") this.displayName = next;
|
|
3080
|
-
else if (field === "api") this.api = next;
|
|
3081
3334
|
else if (field === "baseURL") this.baseURL = next;
|
|
3082
3335
|
else this.apiKeyDraft = next;
|
|
3083
3336
|
}
|
|
3084
3337
|
}
|
|
3338
|
+
/**
|
|
3339
|
+
* No Tab-toggled focus, on purpose: a hidden "which of two things am I
|
|
3340
|
+
* typing into right now" mode with no visible indicator (the earlier
|
|
3341
|
+
* design) is exactly the kind of invisible-state bug already fixed
|
|
3342
|
+
* elsewhere in this file. Instead the "Add id" field is *always* live —
|
|
3343
|
+
* every printable key, paste, and left/right/backspace/delete/home/end
|
|
3344
|
+
* goes there, full stop, since ↑/↓ are the only keys `miniTextFieldInput`
|
|
3345
|
+
* never claims. That leaves ↑/↓ free to always drive the list selection
|
|
3346
|
+
* (for removal) at the same time, with no mode switch and no key that can
|
|
3347
|
+
* ever mean two different things depending on invisible state.
|
|
3348
|
+
*/
|
|
3085
3349
|
renderModelListEditor(mp) {
|
|
3086
3350
|
const lines = [bold$9(secondary$9("Models"))];
|
|
3087
3351
|
this.models.forEach((model, index) => {
|
|
3088
|
-
const isSelected =
|
|
3352
|
+
const isSelected = index === this.modelSelected;
|
|
3089
3353
|
const text = `${isSelected ? "› " : " "}${model.id}${model.name === void 0 ? "" : ` — ${model.name}`}`;
|
|
3090
3354
|
lines.push(isSelected ? invert$5(text) : text);
|
|
3091
3355
|
});
|
|
3092
3356
|
if (this.models.length === 0) lines.push(muted$10("No models yet."));
|
|
3093
|
-
lines.push(
|
|
3357
|
+
lines.push(`Add id: ${renderMiniTextField(this.modelDraftId, true)}`);
|
|
3094
3358
|
if (mp.busy) lines.push(muted$10("Discovering…"));
|
|
3095
3359
|
if (mp.discovered !== void 0) if (mp.discovered.length === 0) lines.push(muted$10("No models discovered."));
|
|
3096
3360
|
else {
|
|
3097
|
-
lines.push(muted$10("Discovered —
|
|
3361
|
+
lines.push(muted$10("Discovered — type one into \"Add id\" above to adopt it:"));
|
|
3098
3362
|
for (const model of mp.discovered) lines.push(muted$10(` ${model.id}${model.name === void 0 ? "" : ` — ${model.name}`}`));
|
|
3099
3363
|
}
|
|
3100
|
-
lines.push(muted$10("
|
|
3364
|
+
lines.push(muted$10("type + enter add · ↑↓ select · ctrl+x remove · ctrl+g discover · ctrl+s save · esc back (without saving)"));
|
|
3101
3365
|
return lines;
|
|
3102
3366
|
}
|
|
3103
3367
|
addModel(id) {
|
|
@@ -3113,36 +3377,34 @@ var ModelProfileOverlay = class {
|
|
|
3113
3377
|
this.showModels = false;
|
|
3114
3378
|
return;
|
|
3115
3379
|
}
|
|
3116
|
-
if (matchesKey(data, Key.
|
|
3117
|
-
this.
|
|
3380
|
+
if (matchesKey(data, Key.ctrl("s"))) {
|
|
3381
|
+
this.actions.saveProvider(this.buildDraft(draft));
|
|
3118
3382
|
return;
|
|
3119
3383
|
}
|
|
3120
|
-
if (
|
|
3121
|
-
|
|
3122
|
-
this.addModel(this.modelDraftId.value);
|
|
3123
|
-
return;
|
|
3124
|
-
}
|
|
3125
|
-
const next = miniTextFieldInput(this.modelDraftId, data);
|
|
3126
|
-
if (next !== void 0) this.modelDraftId = next;
|
|
3384
|
+
if (matchesKey(data, Key.ctrl("g"))) {
|
|
3385
|
+
this.actions.discoverModelsForDraft(this.buildDraft(draft));
|
|
3127
3386
|
return;
|
|
3128
3387
|
}
|
|
3129
|
-
if (data
|
|
3130
|
-
this.
|
|
3388
|
+
if (matchesKey(data, Key.ctrl("x"))) {
|
|
3389
|
+
if (this.models.length === 0) return;
|
|
3390
|
+
this.models = this.models.filter((_, index) => index !== this.modelSelected);
|
|
3391
|
+
this.modelSelected = Math.max(0, Math.min(this.modelSelected, this.models.length - 1));
|
|
3131
3392
|
return;
|
|
3132
3393
|
}
|
|
3133
|
-
if (this.models.length === 0) return;
|
|
3134
3394
|
if (matchesKey(data, Key.up)) {
|
|
3135
|
-
this.modelSelected = Math.max(0, this.modelSelected - 1);
|
|
3395
|
+
if (this.models.length > 0) this.modelSelected = Math.max(0, this.modelSelected - 1);
|
|
3136
3396
|
return;
|
|
3137
3397
|
}
|
|
3138
3398
|
if (matchesKey(data, Key.down)) {
|
|
3139
|
-
this.modelSelected = Math.min(this.models.length - 1, this.modelSelected + 1);
|
|
3399
|
+
if (this.models.length > 0) this.modelSelected = Math.min(this.models.length - 1, this.modelSelected + 1);
|
|
3140
3400
|
return;
|
|
3141
3401
|
}
|
|
3142
|
-
if (data
|
|
3143
|
-
this.
|
|
3144
|
-
|
|
3402
|
+
if (matchesKey(data, Key.enter)) {
|
|
3403
|
+
this.addModel(this.modelDraftId.value);
|
|
3404
|
+
return;
|
|
3145
3405
|
}
|
|
3406
|
+
const next = miniTextFieldInput(this.modelDraftId, data);
|
|
3407
|
+
if (next !== void 0) this.modelDraftId = next;
|
|
3146
3408
|
}
|
|
3147
3409
|
handleInput(data) {
|
|
3148
3410
|
const overlay = this.store.getSnapshot().overlay;
|
|
@@ -3154,7 +3416,8 @@ var ModelProfileOverlay = class {
|
|
|
3154
3416
|
else this.handleFormInput(data, draft);
|
|
3155
3417
|
return;
|
|
3156
3418
|
}
|
|
3157
|
-
this.
|
|
3419
|
+
if (this.mode === "picker") this.handleModelPickerInput(data, mp);
|
|
3420
|
+
else this.handleListInput(data, mp);
|
|
3158
3421
|
}
|
|
3159
3422
|
};
|
|
3160
3423
|
//#endregion
|
|
@@ -3163,33 +3426,121 @@ const bold$8 = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
|
3163
3426
|
const secondary$8 = fg(theme.secondary);
|
|
3164
3427
|
const muted$9 = fg(theme.muted);
|
|
3165
3428
|
const errorColor$4 = fg(theme.error);
|
|
3429
|
+
const successColor = fg(theme.success);
|
|
3166
3430
|
const invert$4 = (s) => `\x1b[7m${s}\x1b[0m`;
|
|
3431
|
+
/** A non-secret reminder that replacing an API-key prompt will overwrite an existing stored key. */
|
|
3432
|
+
function formatStoredApiKeyHint(preview) {
|
|
3433
|
+
return preview === void 0 ? void 0 : `Stored key: ${preview}`;
|
|
3434
|
+
}
|
|
3435
|
+
const AUTH_TYPES = ["oauth", "api-key"];
|
|
3436
|
+
const AUTH_TYPE_LABELS = {
|
|
3437
|
+
oauth: "Sign in with an account",
|
|
3438
|
+
"api-key": "Sign in with an API key"
|
|
3439
|
+
};
|
|
3167
3440
|
var LoginOverlay = class {
|
|
3441
|
+
tui;
|
|
3168
3442
|
store;
|
|
3169
3443
|
actions;
|
|
3444
|
+
step = "authType";
|
|
3445
|
+
authType;
|
|
3446
|
+
authTypeCursor = 0;
|
|
3447
|
+
chooserSkipped = false;
|
|
3448
|
+
autoSkipChecked = false;
|
|
3449
|
+
listCursor = 0;
|
|
3450
|
+
searchQuery = emptyMiniTextField();
|
|
3170
3451
|
promptField = emptyMiniTextField();
|
|
3171
3452
|
promptCursor = 0;
|
|
3172
|
-
constructor(store, actions) {
|
|
3453
|
+
constructor(tui, store, actions) {
|
|
3454
|
+
this.tui = tui;
|
|
3173
3455
|
this.store = store;
|
|
3174
3456
|
this.actions = actions;
|
|
3175
3457
|
}
|
|
3176
3458
|
invalidate() {}
|
|
3177
|
-
|
|
3178
|
-
|
|
3459
|
+
/** See `ModelProfileOverlay.listWindow` — same overflow bug, same fix: a long provider list (~35 catalog entries) must never push the key-legend hint line past the bottom of the terminal. */
|
|
3460
|
+
listWindow(chrome) {
|
|
3461
|
+
return Math.max(3, this.tui.terminal.rows - chrome);
|
|
3462
|
+
}
|
|
3463
|
+
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
3464
|
+
visibleRange(count, selected, maxVisible) {
|
|
3465
|
+
if (count <= maxVisible) return {
|
|
3466
|
+
start: 0,
|
|
3467
|
+
end: count
|
|
3468
|
+
};
|
|
3469
|
+
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
3470
|
+
return {
|
|
3471
|
+
start,
|
|
3472
|
+
end: start + maxVisible
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
/**
|
|
3476
|
+
* The global one-line notice (`store.setNotice`), rendered inline here.
|
|
3477
|
+
* `/login` runs as a full-screen overlay that paints over the whole
|
|
3478
|
+
* terminal, so the notice dock underneath — where sign-in progress,
|
|
3479
|
+
* success ("Signed in to X."), and failure text otherwise land — is
|
|
3480
|
+
* completely hidden for as long as this overlay stays open. Surfacing it
|
|
3481
|
+
* here is what makes an OAuth callback's result actually visible instead
|
|
3482
|
+
* of the screen appearing to do nothing once the browser redirects back.
|
|
3483
|
+
*/
|
|
3484
|
+
noticeLines() {
|
|
3485
|
+
const notice = this.store.getSnapshot().notice;
|
|
3486
|
+
return notice === void 0 ? [] : [muted$9(notice)];
|
|
3487
|
+
}
|
|
3488
|
+
/** Partition the loaded flows by method type, once, the first time they arrive; auto-advances past the chooser if only one type is on offer. */
|
|
3489
|
+
maybeAutoSkipChooser(login) {
|
|
3490
|
+
if (this.autoSkipChecked || login.flows === void 0) return;
|
|
3491
|
+
this.autoSkipChecked = true;
|
|
3492
|
+
const hasOAuth = login.flows.some((flow) => flow.methods.some((method) => method.id === "oauth"));
|
|
3493
|
+
if (hasOAuth === login.flows.some((flow) => flow.methods.some((method) => method.id === "api-key"))) return;
|
|
3494
|
+
this.authType = hasOAuth ? "oauth" : "api-key";
|
|
3495
|
+
this.step = "list";
|
|
3496
|
+
this.chooserSkipped = true;
|
|
3497
|
+
}
|
|
3498
|
+
filteredFlows(login) {
|
|
3499
|
+
const all = login.flows ?? [];
|
|
3500
|
+
const byType = this.authType === void 0 ? all : all.filter((flow) => flow.methods.some((method) => method.id === this.authType));
|
|
3501
|
+
const query = this.searchQuery.value.trim();
|
|
3502
|
+
return query === "" ? byType : fuzzyFilter([...byType], query, (flow) => flow.label);
|
|
3503
|
+
}
|
|
3504
|
+
renderAuthTypeChooser(login) {
|
|
3505
|
+
const lines = [bold$8(secondary$8("Select authentication method"))];
|
|
3506
|
+
lines.push(...this.noticeLines());
|
|
3179
3507
|
if (login.error !== void 0) lines.push(errorColor$4(login.error));
|
|
3180
3508
|
if (login.busy && login.flows === void 0) lines.push(muted$9("Loading…"));
|
|
3181
|
-
|
|
3182
|
-
const
|
|
3509
|
+
AUTH_TYPES.forEach((type, index) => {
|
|
3510
|
+
const text = `${index === this.authTypeCursor ? "› " : " "}${AUTH_TYPE_LABELS[type]}`;
|
|
3511
|
+
lines.push(index === this.authTypeCursor ? invert$4(text) : text);
|
|
3512
|
+
});
|
|
3513
|
+
lines.push(muted$9("↑↓ select · enter continue · esc close"));
|
|
3514
|
+
return lines;
|
|
3515
|
+
}
|
|
3516
|
+
renderProviderList(login) {
|
|
3517
|
+
const lines = [bold$8(secondary$8("Select provider to configure:"))];
|
|
3518
|
+
lines.push(...this.noticeLines());
|
|
3519
|
+
if (login.error !== void 0) lines.push(errorColor$4(login.error));
|
|
3520
|
+
lines.push(`> ${renderMiniTextField(this.searchQuery, true)}`);
|
|
3521
|
+
if (login.busy && login.flows === void 0) lines.push(muted$9("Loading…"));
|
|
3522
|
+
const flows = this.filteredFlows(login);
|
|
3523
|
+
const chrome = lines.length + 2;
|
|
3524
|
+
const maxVisible = this.listWindow(chrome);
|
|
3525
|
+
const { start, end } = this.visibleRange(flows.length, this.listCursor, maxVisible);
|
|
3526
|
+
flows.slice(start, end).forEach((flow, offset) => {
|
|
3527
|
+
const index = start + offset;
|
|
3528
|
+
const marker = flow.inFlight ? "· " : flow.configured ? "✓ " : "○ ";
|
|
3183
3529
|
const signingIn = login.signingIn === flow.key ? " — signing in…" : "";
|
|
3184
|
-
const
|
|
3185
|
-
|
|
3530
|
+
const via = flow.authMethod === "oauth" ? " [oauth]" : flow.authMethod === "api-key" ? " [api]" : "";
|
|
3531
|
+
const readyNote = login.signingIn !== flow.key && flow.configured ? ` — ready to use${via}` : "";
|
|
3532
|
+
const text = `${index === this.listCursor ? "› " : " "}${marker}${flow.label}${signingIn}${readyNote}`;
|
|
3533
|
+
lines.push(index === this.listCursor ? invert$4(text) : flow.configured ? successColor(text) : text);
|
|
3186
3534
|
});
|
|
3187
|
-
if (
|
|
3188
|
-
lines.push(muted$9("
|
|
3535
|
+
if (flows.length > maxVisible) lines.push(muted$9(`(${this.listCursor + 1}/${flows.length})`));
|
|
3536
|
+
if (login.flows !== void 0 && flows.length === 0) lines.push(muted$9("No matching providers."));
|
|
3537
|
+
const back = this.chooserSkipped ? "esc close" : "esc back";
|
|
3538
|
+
lines.push(muted$9(`type to search · ↑↓ select · enter sign in (or edit key/models if already configured) · ctrl+p add custom provider · ${back}`));
|
|
3189
3539
|
return lines;
|
|
3190
3540
|
}
|
|
3191
3541
|
renderPrompt(prompt) {
|
|
3192
3542
|
const lines = [bold$8(secondary$8("Sign in"))];
|
|
3543
|
+
lines.push(...this.noticeLines());
|
|
3193
3544
|
lines.push(prompt.message);
|
|
3194
3545
|
if (prompt.kind === "select") {
|
|
3195
3546
|
prompt.options.forEach((option, index) => {
|
|
@@ -3202,26 +3553,34 @@ var LoginOverlay = class {
|
|
|
3202
3553
|
const mask = prompt.kind === "secret" ? "•" : void 0;
|
|
3203
3554
|
const placeholder = prompt.placeholder ?? (prompt.kind === "secret" ? "API key" : "code");
|
|
3204
3555
|
const field = this.promptField.value === "" ? `(${placeholder})` : renderMiniTextField(this.promptField, true, mask);
|
|
3556
|
+
const storedKeyHint = prompt.kind === "secret" ? formatStoredApiKeyHint(prompt.storedApiKeyPreview) : void 0;
|
|
3557
|
+
if (storedKeyHint !== void 0) lines.push(muted$9(storedKeyHint));
|
|
3205
3558
|
lines.push(`> ${field}`);
|
|
3206
|
-
lines.push(muted$9("
|
|
3559
|
+
lines.push(muted$9("escape/ctrl+c to cancel, enter to submit"));
|
|
3207
3560
|
}
|
|
3208
3561
|
return lines;
|
|
3209
3562
|
}
|
|
3210
3563
|
render(_width) {
|
|
3211
3564
|
const overlay = this.store.getSnapshot().overlay;
|
|
3212
3565
|
if (overlay.kind !== "login") return [];
|
|
3213
|
-
const {
|
|
3214
|
-
|
|
3566
|
+
const { login } = overlay;
|
|
3567
|
+
if (login.prompt !== void 0) return this.renderPrompt(login.prompt);
|
|
3568
|
+
this.maybeAutoSkipChooser(login);
|
|
3569
|
+
return this.step === "authType" ? this.renderAuthTypeChooser(login) : this.renderProviderList(login);
|
|
3215
3570
|
}
|
|
3216
3571
|
handleInput(data) {
|
|
3217
3572
|
const overlay = this.store.getSnapshot().overlay;
|
|
3218
3573
|
if (overlay.kind !== "login") return;
|
|
3219
|
-
const {
|
|
3220
|
-
if (prompt !== void 0)
|
|
3221
|
-
|
|
3574
|
+
const { login } = overlay;
|
|
3575
|
+
if (login.prompt !== void 0) {
|
|
3576
|
+
this.handlePromptInput(data, login.prompt);
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
if (this.step === "authType") this.handleAuthTypeChooserInput(data);
|
|
3580
|
+
else this.handleProviderListInput(data, login);
|
|
3222
3581
|
}
|
|
3223
3582
|
handlePromptInput(data, prompt) {
|
|
3224
|
-
if (matchesKey(data, Key.escape)) {
|
|
3583
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
3225
3584
|
this.actions.answerAuthorizationPrompt("");
|
|
3226
3585
|
return;
|
|
3227
3586
|
}
|
|
@@ -3244,22 +3603,65 @@ var LoginOverlay = class {
|
|
|
3244
3603
|
const next = miniTextFieldInput(this.promptField, data);
|
|
3245
3604
|
if (next !== void 0) this.promptField = next;
|
|
3246
3605
|
}
|
|
3247
|
-
|
|
3606
|
+
handleAuthTypeChooserInput(data) {
|
|
3248
3607
|
if (matchesKey(data, Key.escape)) {
|
|
3249
3608
|
this.actions.closeLogin();
|
|
3250
3609
|
return;
|
|
3251
3610
|
}
|
|
3252
|
-
const flows = login.flows;
|
|
3253
|
-
if (flows === void 0 || flows.length === 0) return;
|
|
3254
3611
|
if (matchesKey(data, Key.up)) {
|
|
3255
|
-
this.
|
|
3612
|
+
this.authTypeCursor = Math.max(0, this.authTypeCursor - 1);
|
|
3256
3613
|
return;
|
|
3257
3614
|
}
|
|
3258
3615
|
if (matchesKey(data, Key.down)) {
|
|
3259
|
-
this.
|
|
3616
|
+
this.authTypeCursor = Math.min(AUTH_TYPES.length - 1, this.authTypeCursor + 1);
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
3619
|
+
if (matchesKey(data, Key.enter)) {
|
|
3620
|
+
this.authType = AUTH_TYPES[this.authTypeCursor];
|
|
3621
|
+
this.step = "list";
|
|
3622
|
+
this.listCursor = 0;
|
|
3623
|
+
this.searchQuery = emptyMiniTextField();
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
handleProviderListInput(data, login) {
|
|
3627
|
+
if (matchesKey(data, Key.escape)) {
|
|
3628
|
+
if (this.chooserSkipped) this.actions.closeLogin();
|
|
3629
|
+
else {
|
|
3630
|
+
this.step = "authType";
|
|
3631
|
+
this.searchQuery = emptyMiniTextField();
|
|
3632
|
+
}
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
if (matchesKey(data, Key.ctrl("p"))) {
|
|
3636
|
+
this.actions.addCustomProvider();
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
const flows = this.filteredFlows(login);
|
|
3640
|
+
if (matchesKey(data, Key.up)) {
|
|
3641
|
+
this.listCursor = Math.max(0, this.listCursor - 1);
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
if (matchesKey(data, Key.down)) {
|
|
3645
|
+
this.listCursor = Math.min(Math.max(0, flows.length - 1), this.listCursor + 1);
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
if (matchesKey(data, Key.ctrl("e"))) {
|
|
3649
|
+
const flow = flows[this.listCursor];
|
|
3650
|
+
if (flow !== void 0 && flow.configured && flow.key.startsWith("llm-pi-ai/")) this.actions.openProviderEditor(flow.key.slice(10));
|
|
3260
3651
|
return;
|
|
3261
3652
|
}
|
|
3262
|
-
if (matchesKey(data, Key.enter))
|
|
3653
|
+
if (matchesKey(data, Key.enter)) {
|
|
3654
|
+
const flow = flows[this.listCursor];
|
|
3655
|
+
if (flow === void 0) return;
|
|
3656
|
+
if (flow.configured && flow.key.startsWith("llm-pi-ai/")) this.actions.openProviderEditor(flow.key.slice(10));
|
|
3657
|
+
else this.actions.beginAuthorization(flow.key, this.authType);
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
const next = miniTextFieldInput(this.searchQuery, data);
|
|
3661
|
+
if (next !== void 0) {
|
|
3662
|
+
this.searchQuery = next;
|
|
3663
|
+
this.listCursor = 0;
|
|
3664
|
+
}
|
|
3263
3665
|
}
|
|
3264
3666
|
};
|
|
3265
3667
|
//#endregion
|
|
@@ -4544,8 +4946,8 @@ var TuiApp = class {
|
|
|
4544
4946
|
const state = store.getSnapshot();
|
|
4545
4947
|
return buildStatusBarText({
|
|
4546
4948
|
sessionId: options.sessionId,
|
|
4547
|
-
provider: options.provider,
|
|
4548
|
-
model: options.model,
|
|
4949
|
+
provider: state.activeModel?.provider ?? options.provider,
|
|
4950
|
+
model: state.activeModel?.model ?? options.model,
|
|
4549
4951
|
status: state.status,
|
|
4550
4952
|
queuedCount: state.queued.length,
|
|
4551
4953
|
presetLabel: state.preset?.current,
|
|
@@ -4573,29 +4975,36 @@ var TuiApp = class {
|
|
|
4573
4975
|
updateHintText,
|
|
4574
4976
|
statsLineText
|
|
4575
4977
|
], { gap: 0 });
|
|
4576
|
-
const
|
|
4978
|
+
const headerMark = new DynamicText(() => ACRYL_MARK_ROWS.map((row) => fg(theme.primary)(row)).join("\n"));
|
|
4979
|
+
const header = new HStack([{
|
|
4980
|
+
component: this.pet,
|
|
4981
|
+
basis: 34,
|
|
4982
|
+
shrink: 0
|
|
4983
|
+
}, {
|
|
4984
|
+
component: headerMark,
|
|
4985
|
+
basis: "auto",
|
|
4986
|
+
grow: 1
|
|
4987
|
+
}], {
|
|
4988
|
+
gap: 1,
|
|
4989
|
+
align: "start"
|
|
4990
|
+
});
|
|
4991
|
+
const buildStamp = `build bf7e1ab · ${(/* @__PURE__ */ new Date(BUILD_TIME)).toLocaleTimeString()}`;
|
|
4992
|
+
const sessionInfo = new DynamicText(() => {
|
|
4577
4993
|
const { provider, model, cwd } = options;
|
|
4578
4994
|
return [
|
|
4579
|
-
fg(theme.primary)("ACRYL"),
|
|
4580
|
-
"",
|
|
4581
4995
|
`${provider}/${model}`,
|
|
4582
|
-
cwd
|
|
4996
|
+
cwd,
|
|
4997
|
+
fg(theme.muted)(buildStamp)
|
|
4583
4998
|
].join("\n");
|
|
4584
4999
|
});
|
|
4585
5000
|
const layoutRoot = new VStack([
|
|
4586
5001
|
{
|
|
4587
|
-
component:
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
basis: "auto",
|
|
4594
|
-
grow: 1
|
|
4595
|
-
}], {
|
|
4596
|
-
gap: 1,
|
|
4597
|
-
align: "start"
|
|
4598
|
-
}),
|
|
5002
|
+
component: header,
|
|
5003
|
+
basis: "auto",
|
|
5004
|
+
shrink: 0
|
|
5005
|
+
},
|
|
5006
|
+
{
|
|
5007
|
+
component: sessionInfo,
|
|
4599
5008
|
basis: "auto",
|
|
4600
5009
|
shrink: 0
|
|
4601
5010
|
},
|
|
@@ -4694,8 +5103,8 @@ var TuiApp = class {
|
|
|
4694
5103
|
const { store, actions, getTool, getToolCall } = this.options;
|
|
4695
5104
|
switch (overlay.kind) {
|
|
4696
5105
|
case "none": return;
|
|
4697
|
-
case "modelProfile": return new ModelProfileOverlay(store, actions);
|
|
4698
|
-
case "login": return new LoginOverlay(store, actions);
|
|
5106
|
+
case "modelProfile": return new ModelProfileOverlay(this.tui, store, actions);
|
|
5107
|
+
case "login": return new LoginOverlay(this.tui, store, actions);
|
|
4699
5108
|
case "trajectory": return new TrajectoryOverlay(this.tui, store, actions, getTool);
|
|
4700
5109
|
case "toolCards": return new ToolCardsOverlay(this.tui, store, actions, getTool, getToolCall);
|
|
4701
5110
|
case "context": return new ContextOverlay(store, actions);
|
|
@@ -4818,14 +5227,6 @@ async function walkDirectory(cwd) {
|
|
|
4818
5227
|
}
|
|
4819
5228
|
return results;
|
|
4820
5229
|
}
|
|
4821
|
-
//#endregion
|
|
4822
|
-
//#region src/tui/auth-guidance.ts
|
|
4823
|
-
function logoutSuccessMessage(provider) {
|
|
4824
|
-
return `Removed stored authentication for ${provider.trim() === "" ? "provider" : provider}.`;
|
|
4825
|
-
}
|
|
4826
|
-
function logoutNoneMessage() {
|
|
4827
|
-
return "No active provider is selected; nothing to log out.";
|
|
4828
|
-
}
|
|
4829
5230
|
/** Canonical ACRYL version string used by the CLI and the release smoke checks. */
|
|
4830
5231
|
const ACRYL_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
4831
5232
|
//#endregion
|
|
@@ -4881,10 +5282,33 @@ function getAtPath(value, path) {
|
|
|
4881
5282
|
}
|
|
4882
5283
|
return current;
|
|
4883
5284
|
}
|
|
4884
|
-
/**
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
5285
|
+
/**
|
|
5286
|
+
* `dsh-llm-pi-ai`'s own credential record scope (`credentialKey('llm-pi-ai', providerId)`,
|
|
5287
|
+
* i.e. `"llm-pi-ai/<providerId>"`). A `/login` sign-in (OAuth or API key) writes here through
|
|
5288
|
+
* pi-ai's own `CredentialStore`, entirely separate from the `apiKeyEnv` reference a manually
|
|
5289
|
+
* configured provider's settings profile points at — so a provider's "has credentials" status
|
|
5290
|
+
* has to check both places, and this is the address for the first.
|
|
5291
|
+
*/
|
|
5292
|
+
function piAiRecordKey(providerId) {
|
|
5293
|
+
return `llm-pi-ai/${providerId}`;
|
|
5294
|
+
}
|
|
5295
|
+
/**
|
|
5296
|
+
* Wait out `ctx.settings`' asynchronous watcher queue after a write.
|
|
5297
|
+
*
|
|
5298
|
+
* `settingsSvc.update()`'s returned promise resolves once the write commits
|
|
5299
|
+
* to the settings document — but `dsh-llm-pi-ai`'s own reactive watcher (the
|
|
5300
|
+
* thing that actually rebuilds its live `Models` registry and re-registers
|
|
5301
|
+
* routes with `ctx.llm`) runs on a *separate* promise chained off that
|
|
5302
|
+
* commit, not before it settles. A caller that reads `ctx.llm.listModels()`
|
|
5303
|
+
* or `listProviders()` immediately after `update()` resolves races that
|
|
5304
|
+
* watcher and sees the pre-write state. `setImmediate` runs after the
|
|
5305
|
+
* Node microtask queue drains, which is exactly where that chained watcher
|
|
5306
|
+
* promise settles, so awaiting one flush is enough to see the write reflected.
|
|
5307
|
+
*/
|
|
5308
|
+
function flushSettingsWatchers() {
|
|
5309
|
+
return new Promise((resolve) => {
|
|
5310
|
+
setImmediate(resolve);
|
|
5311
|
+
});
|
|
4888
5312
|
}
|
|
4889
5313
|
/** Open a URL in the platform's default browser (fire-and-forget). */
|
|
4890
5314
|
function openBrowser(url) {
|
|
@@ -4949,18 +5373,11 @@ async function attachSession(host, resumeId) {
|
|
|
4949
5373
|
const agent = host.ctx.agents?.get?.(SessionId(id));
|
|
4950
5374
|
const session = agent?.session;
|
|
4951
5375
|
const history = [];
|
|
4952
|
-
async function
|
|
5376
|
+
async function computeProviderRows() {
|
|
4953
5377
|
const settingsSvc = host.ctx.get("settings");
|
|
4954
5378
|
const credentialsSvc = host.ctx.get("credentials");
|
|
4955
5379
|
const llmSvc = host.ctx.get("llm");
|
|
4956
|
-
if (settingsSvc === void 0 || credentialsSvc === void 0 || llmSvc === void 0)
|
|
4957
|
-
store.updateModelProfile({
|
|
4958
|
-
providers: [],
|
|
4959
|
-
busy: false,
|
|
4960
|
-
error: "Model provider settings are not available in this profile."
|
|
4961
|
-
});
|
|
4962
|
-
return;
|
|
4963
|
-
}
|
|
5380
|
+
if (settingsSvc === void 0 || credentialsSvc === void 0 || llmSvc === void 0) return void 0;
|
|
4964
5381
|
const configurable = llmSvc.listConfigurableProviders();
|
|
4965
5382
|
const live = new Set(llmSvc.listProviders().map((provider) => provider.id));
|
|
4966
5383
|
const descriptors = settingsSvc.describe({ redactSecrets: true });
|
|
@@ -4972,21 +5389,38 @@ async function attachSession(host, resumeId) {
|
|
|
4972
5389
|
const userValue = descriptor === void 0 ? void 0 : getAtPath(descriptor.user, entry.settingsPath);
|
|
4973
5390
|
const apiKeyRef = value?.apiKeyEnv ?? deriveApiKeyRef(entry.provider);
|
|
4974
5391
|
const info = await credentialsSvc.describe(apiKeyRef);
|
|
5392
|
+
const loginRecord = await credentialsSvc.readRecord?.(piAiRecordKey(entry.provider));
|
|
5393
|
+
const authMethod = loginRecord !== void 0 ? loginRecord.kind === "grant" ? "oauth" : "api-key" : info.configured ? "api-key" : void 0;
|
|
5394
|
+
const isLive = live.has(entry.provider);
|
|
5395
|
+
const models = isLive ? await llmSvc.listModels(entry.provider).catch(() => value?.models ?? []) : value?.models ?? [];
|
|
4975
5396
|
rows.push({
|
|
4976
5397
|
route: entry.provider,
|
|
4977
5398
|
displayName: value?.displayName ?? entry.displayName,
|
|
4978
5399
|
settingsNs: entry.settingsNs,
|
|
4979
5400
|
settingsPath: entry.settingsPath,
|
|
4980
5401
|
configured: userValue !== void 0,
|
|
4981
|
-
live:
|
|
5402
|
+
live: isLive,
|
|
4982
5403
|
api: value?.api,
|
|
4983
5404
|
baseURL: value?.baseURL,
|
|
4984
5405
|
apiKeyRef,
|
|
4985
|
-
apiKeyConfigured: info.configured,
|
|
4986
|
-
|
|
5406
|
+
apiKeyConfigured: info.configured || loginRecord !== void 0,
|
|
5407
|
+
authMethod,
|
|
5408
|
+
models,
|
|
4987
5409
|
revision: descriptor?.revision
|
|
4988
5410
|
});
|
|
4989
5411
|
}
|
|
5412
|
+
return rows;
|
|
5413
|
+
}
|
|
5414
|
+
async function loadProviders() {
|
|
5415
|
+
const rows = await computeProviderRows();
|
|
5416
|
+
if (rows === void 0) {
|
|
5417
|
+
store.updateModelProfile({
|
|
5418
|
+
providers: [],
|
|
5419
|
+
busy: false,
|
|
5420
|
+
error: "Model provider settings are not available in this profile."
|
|
5421
|
+
});
|
|
5422
|
+
return;
|
|
5423
|
+
}
|
|
4990
5424
|
store.updateModelProfile({
|
|
4991
5425
|
providers: rows,
|
|
4992
5426
|
busy: false,
|
|
@@ -4994,8 +5428,69 @@ async function attachSession(host, resumeId) {
|
|
|
4994
5428
|
selected: 0
|
|
4995
5429
|
});
|
|
4996
5430
|
}
|
|
5431
|
+
function openEditFormForRow(row) {
|
|
5432
|
+
const overlay = store.getSnapshot().overlay;
|
|
5433
|
+
if (overlay.kind !== "modelProfile") return;
|
|
5434
|
+
const draft = {
|
|
5435
|
+
route: row.route,
|
|
5436
|
+
isNew: false,
|
|
5437
|
+
settingsNs: row.settingsNs,
|
|
5438
|
+
settingsPath: row.settingsPath,
|
|
5439
|
+
displayName: row.displayName,
|
|
5440
|
+
api: row.api ?? "",
|
|
5441
|
+
baseURL: row.baseURL ?? "",
|
|
5442
|
+
apiKeyRef: row.apiKeyRef,
|
|
5443
|
+
apiKeyConfigured: row.apiKeyConfigured,
|
|
5444
|
+
authMethod: row.authMethod,
|
|
5445
|
+
apiKeyDraft: "",
|
|
5446
|
+
apiKeyPreview: void 0,
|
|
5447
|
+
models: row.models,
|
|
5448
|
+
revision: row.revision
|
|
5449
|
+
};
|
|
5450
|
+
const formKey = overlay.modelProfile.formKey + 1;
|
|
5451
|
+
store.updateModelProfile({
|
|
5452
|
+
view: "form",
|
|
5453
|
+
draft,
|
|
5454
|
+
formKey
|
|
5455
|
+
});
|
|
5456
|
+
if (row.authMethod === "api-key") (async () => {
|
|
5457
|
+
const resolved = await host.ctx.get("credentials")?.resolve?.(row.apiKeyRef).catch(() => void 0);
|
|
5458
|
+
if (resolved?.value === void 0) return;
|
|
5459
|
+
const current = store.getSnapshot().overlay;
|
|
5460
|
+
if (current.kind !== "modelProfile" || current.modelProfile.formKey !== formKey) return;
|
|
5461
|
+
const currentDraft = current.modelProfile.draft;
|
|
5462
|
+
if (currentDraft === void 0) return;
|
|
5463
|
+
store.updateModelProfile({ draft: {
|
|
5464
|
+
...currentDraft,
|
|
5465
|
+
apiKeyPreview: maskKeyPreview(resolved.value)
|
|
5466
|
+
} });
|
|
5467
|
+
})();
|
|
5468
|
+
}
|
|
5469
|
+
async function ensureProviderActivated(providerId, method) {
|
|
5470
|
+
const settingsSvc = host.ctx.get("settings");
|
|
5471
|
+
const llmSvc = host.ctx.get("llm");
|
|
5472
|
+
if (settingsSvc === void 0 || llmSvc === void 0) return false;
|
|
5473
|
+
const entry = llmSvc.listConfigurableProviders().find((candidate) => candidate.provider === providerId);
|
|
5474
|
+
if (entry === void 0) return false;
|
|
5475
|
+
const descriptor = settingsSvc.describe({ redactSecrets: true }).find((candidate) => candidate.ns === entry.settingsNs);
|
|
5476
|
+
const existing = descriptor === void 0 ? void 0 : getAtPath(descriptor.user, entry.settingsPath);
|
|
5477
|
+
const oauthName = `${entry.displayName.replace(/(?:-oauth)+$/, "")}-oauth`;
|
|
5478
|
+
if (existing !== void 0) {
|
|
5479
|
+
if (method === "oauth" && existing.displayName !== oauthName) {
|
|
5480
|
+
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, { displayName: oauthName }), descriptor?.revision);
|
|
5481
|
+
await flushSettingsWatchers();
|
|
5482
|
+
return true;
|
|
5483
|
+
}
|
|
5484
|
+
return false;
|
|
5485
|
+
}
|
|
5486
|
+
const section = method === "oauth" ? { displayName: oauthName } : {};
|
|
5487
|
+
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, section), descriptor?.revision);
|
|
5488
|
+
await flushSettingsWatchers();
|
|
5489
|
+
return true;
|
|
5490
|
+
}
|
|
4997
5491
|
async function loadAuthorizationFlows() {
|
|
4998
5492
|
const authSvc = host.ctx.get("authorization");
|
|
5493
|
+
const credentialsSvc = host.ctx.get("credentials");
|
|
4999
5494
|
if (authSvc === void 0) {
|
|
5000
5495
|
store.updateLogin({
|
|
5001
5496
|
flows: [],
|
|
@@ -5005,12 +5500,28 @@ async function attachSession(host, resumeId) {
|
|
|
5005
5500
|
return;
|
|
5006
5501
|
}
|
|
5007
5502
|
try {
|
|
5008
|
-
const
|
|
5503
|
+
const entries = authSvc.list();
|
|
5504
|
+
const list = await Promise.all(entries.map(async (entry) => {
|
|
5505
|
+
const record = credentialsSvc === void 0 ? void 0 : await credentialsSvc.readRecord?.(entry.key);
|
|
5506
|
+
return {
|
|
5507
|
+
...entry,
|
|
5508
|
+
configured: record !== void 0,
|
|
5509
|
+
authMethod: record === void 0 ? void 0 : record.kind === "grant" ? "oauth" : "api-key"
|
|
5510
|
+
};
|
|
5511
|
+
}));
|
|
5009
5512
|
store.updateLogin({
|
|
5010
5513
|
flows: list,
|
|
5011
5514
|
busy: false,
|
|
5012
5515
|
error: void 0
|
|
5013
5516
|
});
|
|
5517
|
+
const toRepair = list.filter((flow) => flow.configured && flow.authMethod === "oauth" && flow.key.startsWith("llm-pi-ai/"));
|
|
5518
|
+
if (toRepair.length > 0) (async () => {
|
|
5519
|
+
let repaired = false;
|
|
5520
|
+
for (const flow of toRepair) try {
|
|
5521
|
+
if (await ensureProviderActivated(flow.key.slice(10), "oauth")) repaired = true;
|
|
5522
|
+
} catch {}
|
|
5523
|
+
if (repaired) refreshCredentialState();
|
|
5524
|
+
})();
|
|
5014
5525
|
} catch (error) {
|
|
5015
5526
|
store.updateLogin({
|
|
5016
5527
|
busy: false,
|
|
@@ -5018,6 +5529,41 @@ async function attachSession(host, resumeId) {
|
|
|
5018
5529
|
});
|
|
5019
5530
|
}
|
|
5020
5531
|
}
|
|
5532
|
+
function refreshCredentialState() {
|
|
5533
|
+
loadProviders();
|
|
5534
|
+
loadAuthorizationFlows();
|
|
5535
|
+
}
|
|
5536
|
+
/** Open `/model`'s blank custom-provider draft — assumes `/model` is (or is about to become) the open overlay. Shared by `createProvider` (already there) and `addCustomProvider` (getting there first). */
|
|
5537
|
+
function openCustomProviderDraft() {
|
|
5538
|
+
const settingsNs = host.ctx.get("llm")?.listConfigurableProviders?.()[0]?.settingsNs;
|
|
5539
|
+
if (settingsNs === void 0) {
|
|
5540
|
+
store.setNotice("Adding a custom provider is not available in this profile.");
|
|
5541
|
+
return;
|
|
5542
|
+
}
|
|
5543
|
+
const draft = {
|
|
5544
|
+
route: "",
|
|
5545
|
+
isNew: true,
|
|
5546
|
+
settingsNs,
|
|
5547
|
+
settingsPath: [],
|
|
5548
|
+
displayName: "",
|
|
5549
|
+
api: "",
|
|
5550
|
+
baseURL: "",
|
|
5551
|
+
apiKeyRef: "",
|
|
5552
|
+
apiKeyConfigured: false,
|
|
5553
|
+
authMethod: void 0,
|
|
5554
|
+
apiKeyDraft: "",
|
|
5555
|
+
apiKeyPreview: void 0,
|
|
5556
|
+
models: [],
|
|
5557
|
+
revision: void 0
|
|
5558
|
+
};
|
|
5559
|
+
const overlay = store.getSnapshot().overlay;
|
|
5560
|
+
const formKey = overlay.kind === "modelProfile" ? overlay.modelProfile.formKey + 1 : 1;
|
|
5561
|
+
store.updateModelProfile({
|
|
5562
|
+
view: "form",
|
|
5563
|
+
draft,
|
|
5564
|
+
formKey
|
|
5565
|
+
});
|
|
5566
|
+
}
|
|
5021
5567
|
async function loadAgentPresets() {
|
|
5022
5568
|
const presetsSvc = host.ctx.get("agentPresets");
|
|
5023
5569
|
if (presetsSvc === void 0) {
|
|
@@ -5060,7 +5606,9 @@ async function attachSession(host, resumeId) {
|
|
|
5060
5606
|
});
|
|
5061
5607
|
},
|
|
5062
5608
|
cancel() {
|
|
5063
|
-
bridge.cancel(id).catch(() => {
|
|
5609
|
+
bridge.cancel(id).catch((error) => {
|
|
5610
|
+
store.setNotice(`Could not cancel: ${error instanceof Error ? error.message : String(error)}`);
|
|
5611
|
+
});
|
|
5064
5612
|
},
|
|
5065
5613
|
shutdown() {
|
|
5066
5614
|
signal("exit");
|
|
@@ -5173,12 +5721,10 @@ async function attachSession(host, resumeId) {
|
|
|
5173
5721
|
closeLogin() {
|
|
5174
5722
|
store.closeOverlay();
|
|
5175
5723
|
},
|
|
5176
|
-
|
|
5177
|
-
store.updateLogin({ selected: index });
|
|
5178
|
-
},
|
|
5179
|
-
beginAuthorization(key) {
|
|
5724
|
+
beginAuthorization(key, method) {
|
|
5180
5725
|
(async () => {
|
|
5181
5726
|
const authSvc = host.ctx.get("authorization");
|
|
5727
|
+
const credentialsSvc = host.ctx.get("credentials");
|
|
5182
5728
|
if (authSvc === void 0) {
|
|
5183
5729
|
store.setNotice("Sign-in is not available in this profile.");
|
|
5184
5730
|
return;
|
|
@@ -5186,6 +5732,8 @@ async function attachSession(host, resumeId) {
|
|
|
5186
5732
|
const overlay = store.getSnapshot().overlay;
|
|
5187
5733
|
const flow = overlay.kind === "login" ? overlay.login.flows?.find((entry) => entry.key === key) : void 0;
|
|
5188
5734
|
if (flow === void 0) return;
|
|
5735
|
+
const existingRecord = (method ?? flow.methods[0]?.id) === "api-key" ? await credentialsSvc?.readRecord?.(flow.key) : void 0;
|
|
5736
|
+
const storedApiKeyPreview = typeof existingRecord?.key === "string" ? maskKeyPreview(existingRecord.key) : void 0;
|
|
5189
5737
|
store.updateLogin({ signingIn: key });
|
|
5190
5738
|
const interaction = {
|
|
5191
5739
|
notify(notice) {
|
|
@@ -5210,8 +5758,13 @@ async function attachSession(host, resumeId) {
|
|
|
5210
5758
|
label: option.label,
|
|
5211
5759
|
description: option.description
|
|
5212
5760
|
}))
|
|
5761
|
+
} : prompt.kind === "secret" ? {
|
|
5762
|
+
kind: "secret",
|
|
5763
|
+
message: prompt.message,
|
|
5764
|
+
placeholder: prompt.placeholder,
|
|
5765
|
+
storedApiKeyPreview
|
|
5213
5766
|
} : {
|
|
5214
|
-
kind:
|
|
5767
|
+
kind: "text",
|
|
5215
5768
|
message: prompt.message,
|
|
5216
5769
|
placeholder: prompt.placeholder
|
|
5217
5770
|
};
|
|
@@ -5227,13 +5780,15 @@ async function attachSession(host, resumeId) {
|
|
|
5227
5780
|
try {
|
|
5228
5781
|
if ((await authSvc.begin({
|
|
5229
5782
|
key,
|
|
5783
|
+
method,
|
|
5230
5784
|
interaction
|
|
5231
5785
|
})).status === "authorized") {
|
|
5232
|
-
store.setNotice(`Signed in to ${flow.label}.`);
|
|
5233
|
-
|
|
5234
|
-
|
|
5786
|
+
store.setNotice(`Signed in to ${flow.label} — credentials saved, ready to use from /model.`);
|
|
5787
|
+
if (key.startsWith("llm-pi-ai/")) await ensureProviderActivated(key.slice(10), method);
|
|
5788
|
+
refreshCredentialState();
|
|
5789
|
+
} else store.updateLogin({ error: "Sign-in cancelled." });
|
|
5235
5790
|
} catch (error) {
|
|
5236
|
-
store.
|
|
5791
|
+
store.updateLogin({ error: `Sign-in failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
5237
5792
|
} finally {
|
|
5238
5793
|
store.updateLogin({ signingIn: void 0 });
|
|
5239
5794
|
}
|
|
@@ -5246,28 +5801,9 @@ async function attachSession(host, resumeId) {
|
|
|
5246
5801
|
if (resolve !== void 0) resolve(value);
|
|
5247
5802
|
},
|
|
5248
5803
|
logout() {
|
|
5249
|
-
(
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
store.setNotice("Credentials are not available in this profile.");
|
|
5253
|
-
return;
|
|
5254
|
-
}
|
|
5255
|
-
const selection = host.ctx.get("agentDefaultModel")?.currentSelection?.();
|
|
5256
|
-
const overlay = store.getSnapshot().overlay;
|
|
5257
|
-
const rows = overlay.kind === "modelProfile" ? overlay.modelProfile.providers : void 0;
|
|
5258
|
-
const row = rows?.find((entry) => entry.route === selection?.provider) ?? (rows !== void 0 && rows.length === 1 ? rows[0] : void 0);
|
|
5259
|
-
if (row === void 0) {
|
|
5260
|
-
store.setNotice(logoutNoneMessage());
|
|
5261
|
-
return;
|
|
5262
|
-
}
|
|
5263
|
-
try {
|
|
5264
|
-
await credentialsSvc.unset(row.apiKeyRef);
|
|
5265
|
-
store.setNotice(logoutSuccessMessage(row.displayName));
|
|
5266
|
-
loadProviders();
|
|
5267
|
-
} catch (error) {
|
|
5268
|
-
store.setNotice(`logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5269
|
-
}
|
|
5270
|
-
})();
|
|
5804
|
+
store.openModelProfile();
|
|
5805
|
+
store.setNotice("Select a provider, then Ctrl+D to sign out.");
|
|
5806
|
+
loadProviders();
|
|
5271
5807
|
},
|
|
5272
5808
|
openTrajectory() {
|
|
5273
5809
|
store.openTrajectory();
|
|
@@ -5295,40 +5831,40 @@ async function attachSession(host, resumeId) {
|
|
|
5295
5831
|
},
|
|
5296
5832
|
backToProviderList() {
|
|
5297
5833
|
store.updateModelProfile({ view: "list" });
|
|
5298
|
-
|
|
5299
|
-
selectProvider(index) {
|
|
5300
|
-
store.updateModelProfile({ selected: index });
|
|
5834
|
+
refreshCredentialState();
|
|
5301
5835
|
},
|
|
5302
5836
|
createProvider() {
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5837
|
+
openCustomProviderDraft();
|
|
5838
|
+
},
|
|
5839
|
+
addCustomProvider() {
|
|
5840
|
+
store.openModelProfile();
|
|
5841
|
+
loadProviders();
|
|
5842
|
+
openCustomProviderDraft();
|
|
5307
5843
|
},
|
|
5308
5844
|
editProvider(route) {
|
|
5309
5845
|
const overlay = store.getSnapshot().overlay;
|
|
5310
5846
|
if (overlay.kind !== "modelProfile") return;
|
|
5311
5847
|
const row = overlay.modelProfile.providers?.find((entry) => entry.route === route);
|
|
5312
5848
|
if (row === void 0) return;
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
});
|
|
5849
|
+
openEditFormForRow(row);
|
|
5850
|
+
},
|
|
5851
|
+
openProviderEditor(route) {
|
|
5852
|
+
(async () => {
|
|
5853
|
+
store.openModelProfile();
|
|
5854
|
+
const rows = await computeProviderRows();
|
|
5855
|
+
store.updateModelProfile({
|
|
5856
|
+
providers: rows ?? [],
|
|
5857
|
+
busy: false,
|
|
5858
|
+
error: rows === void 0 ? "Model provider settings are not available in this profile." : void 0,
|
|
5859
|
+
selected: 0
|
|
5860
|
+
});
|
|
5861
|
+
const row = rows?.find((entry) => entry.route === route);
|
|
5862
|
+
if (row === void 0) {
|
|
5863
|
+
store.setNotice(`Provider "${route}" not found.`);
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
openEditFormForRow(row);
|
|
5867
|
+
})();
|
|
5332
5868
|
},
|
|
5333
5869
|
saveProvider(draft) {
|
|
5334
5870
|
(async () => {
|
|
@@ -5338,20 +5874,25 @@ async function attachSession(host, resumeId) {
|
|
|
5338
5874
|
store.setNotice("Provider settings are not available in this profile.");
|
|
5339
5875
|
return;
|
|
5340
5876
|
}
|
|
5877
|
+
if (draft.isNew && draft.route.trim() === "") {
|
|
5878
|
+
store.updateModelProfile({ error: "Provider ID is required." });
|
|
5879
|
+
return;
|
|
5880
|
+
}
|
|
5341
5881
|
try {
|
|
5342
5882
|
const key = draft.apiKeyDraft.trim();
|
|
5343
5883
|
if (key !== "") await credentialsSvc.set(draft.apiKeyRef, key);
|
|
5344
5884
|
const section = {
|
|
5345
|
-
displayName: draft.displayName,
|
|
5346
|
-
api: draft.api,
|
|
5347
|
-
baseURL: draft.baseURL,
|
|
5885
|
+
...draft.displayName.trim() === "" ? {} : { displayName: draft.displayName },
|
|
5886
|
+
...draft.api.trim() === "" ? {} : { api: draft.api },
|
|
5887
|
+
...draft.baseURL.trim() === "" ? {} : { baseURL: draft.baseURL },
|
|
5348
5888
|
apiKeyEnv: draft.apiKeyRef,
|
|
5349
|
-
models: draft.models
|
|
5889
|
+
...draft.models.length === 0 ? {} : { models: draft.models }
|
|
5350
5890
|
};
|
|
5351
5891
|
await settingsSvc.update(draft.settingsNs, nestAtPath(draft.settingsPath, section), draft.revision);
|
|
5352
|
-
|
|
5892
|
+
await flushSettingsWatchers();
|
|
5893
|
+
store.setNotice(`Saved ${draft.displayName || draft.route} — credentials stored, ready to use from /model.`);
|
|
5353
5894
|
store.updateModelProfile({ view: "list" });
|
|
5354
|
-
|
|
5895
|
+
refreshCredentialState();
|
|
5355
5896
|
} catch (error) {
|
|
5356
5897
|
store.setNotice(`save failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5357
5898
|
}
|
|
@@ -5368,15 +5909,85 @@ async function attachSession(host, resumeId) {
|
|
|
5368
5909
|
try {
|
|
5369
5910
|
await credentialsSvc.unset(row.apiKeyRef);
|
|
5370
5911
|
await settingsSvc.update(row.settingsNs, nestAtPath(row.settingsPath, {}), row.revision);
|
|
5912
|
+
await flushSettingsWatchers();
|
|
5371
5913
|
store.setNotice(`Removed ${row.displayName}.`);
|
|
5372
|
-
|
|
5914
|
+
refreshCredentialState();
|
|
5373
5915
|
} catch (error) {
|
|
5374
5916
|
store.setNotice(`delete failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5375
5917
|
}
|
|
5376
5918
|
})();
|
|
5377
5919
|
},
|
|
5378
|
-
|
|
5379
|
-
|
|
5920
|
+
clearApiKey(draft) {
|
|
5921
|
+
(async () => {
|
|
5922
|
+
const credentialsSvc = host.ctx.get("credentials");
|
|
5923
|
+
if (credentialsSvc === void 0) {
|
|
5924
|
+
store.setNotice("Credentials are not available in this profile.");
|
|
5925
|
+
return;
|
|
5926
|
+
}
|
|
5927
|
+
try {
|
|
5928
|
+
await credentialsSvc.unset(draft.apiKeyRef);
|
|
5929
|
+
await credentialsSvc.deleteRecord?.(piAiRecordKey(draft.route));
|
|
5930
|
+
store.setNotice(`Removed the API key for ${draft.displayName || draft.route}.`);
|
|
5931
|
+
store.updateModelProfile({ view: "list" });
|
|
5932
|
+
refreshCredentialState();
|
|
5933
|
+
} catch (error) {
|
|
5934
|
+
store.setNotice(`Could not remove the key: ${error instanceof Error ? error.message : String(error)}`);
|
|
5935
|
+
}
|
|
5936
|
+
})();
|
|
5937
|
+
},
|
|
5938
|
+
discoverModelsForDraft(draft) {
|
|
5939
|
+
(async () => {
|
|
5940
|
+
const llmSvc = host.ctx.get("llm");
|
|
5941
|
+
if (llmSvc === void 0) {
|
|
5942
|
+
store.setNotice("Model discovery is not available in this profile.");
|
|
5943
|
+
return;
|
|
5944
|
+
}
|
|
5945
|
+
store.updateModelProfile({ busy: true });
|
|
5946
|
+
try {
|
|
5947
|
+
const request = {};
|
|
5948
|
+
if (!draft.isNew) request.provider = draft.route;
|
|
5949
|
+
if (draft.baseURL.trim() !== "") request.baseURL = draft.baseURL.trim();
|
|
5950
|
+
if (draft.api.trim() !== "") request.api = draft.api.trim();
|
|
5951
|
+
if (draft.apiKeyDraft.trim() !== "") request.apiKey = draft.apiKeyDraft.trim();
|
|
5952
|
+
const discovered = await llmSvc.discoverModels(draft.settingsNs, request);
|
|
5953
|
+
store.updateModelProfile({
|
|
5954
|
+
busy: false,
|
|
5955
|
+
discovered
|
|
5956
|
+
});
|
|
5957
|
+
} catch (error) {
|
|
5958
|
+
store.updateModelProfile({
|
|
5959
|
+
busy: false,
|
|
5960
|
+
discovered: []
|
|
5961
|
+
});
|
|
5962
|
+
store.setNotice(`Model discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5963
|
+
}
|
|
5964
|
+
})();
|
|
5965
|
+
},
|
|
5966
|
+
setActiveModel(provider, model) {
|
|
5967
|
+
(async () => {
|
|
5968
|
+
try {
|
|
5969
|
+
await bridge.selectModel({
|
|
5970
|
+
sessionId: id,
|
|
5971
|
+
provider,
|
|
5972
|
+
model
|
|
5973
|
+
});
|
|
5974
|
+
store.setActiveModel({
|
|
5975
|
+
provider,
|
|
5976
|
+
model
|
|
5977
|
+
});
|
|
5978
|
+
try {
|
|
5979
|
+
await host.ctx.get("agentDefaultModel")?.saveSelection({
|
|
5980
|
+
provider,
|
|
5981
|
+
model
|
|
5982
|
+
});
|
|
5983
|
+
} catch {}
|
|
5984
|
+
store.setNotice(`Active model set to ${model} (${provider}).`);
|
|
5985
|
+
loadProviders();
|
|
5986
|
+
} catch (error) {
|
|
5987
|
+
store.setNotice(`Could not set active model: ${error instanceof Error ? error.message : String(error)}`);
|
|
5988
|
+
}
|
|
5989
|
+
})();
|
|
5990
|
+
},
|
|
5380
5991
|
closeTrajectory() {
|
|
5381
5992
|
store.closeOverlay();
|
|
5382
5993
|
},
|