acryl-cli-linux-x64 0.1.31 → 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/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { bootAcrylHarnessProfile, createAcrylSessionBridge } from "acryl-harness
|
|
|
4
4
|
import { execFile, spawn } from "node:child_process";
|
|
5
5
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { diffLines } from "diff";
|
|
7
|
-
import { Container, Editor, HStack, Key, KeybindingsManager, ProcessTerminal, ScrollView, TUI_KEYBINDINGS, Text, TuiAltScreen, VStack, matchesKey, setKeybindings, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
7
|
+
import { Container, Editor, HStack, Key, KeybindingsManager, ProcessTerminal, ScrollView, TUI_KEYBINDINGS, Text, TuiAltScreen, VStack, fuzzyFilter, matchesKey, setKeybindings, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
8
8
|
import { readdir } from "node:fs/promises";
|
|
9
9
|
import { join, relative } from "node:path";
|
|
10
10
|
import { ManualCompactionError } from "@deepseek-ai/dsh-compaction";
|
|
@@ -799,6 +799,7 @@ var TuiStore = class {
|
|
|
799
799
|
title: void 0,
|
|
800
800
|
stats: EMPTY_STATS,
|
|
801
801
|
preset: void 0,
|
|
802
|
+
activeModel: void 0,
|
|
802
803
|
streaming: void 0,
|
|
803
804
|
pendingToolCalls: this.pendingToolCallsSnapshot(),
|
|
804
805
|
shellRun: void 0,
|
|
@@ -909,6 +910,10 @@ var TuiStore = class {
|
|
|
909
910
|
setPreset(preset) {
|
|
910
911
|
this.set({ preset });
|
|
911
912
|
}
|
|
913
|
+
/** Record a live `/model` switch so the status bar reflects it immediately. */
|
|
914
|
+
setActiveModel(activeModel) {
|
|
915
|
+
this.set({ activeModel });
|
|
916
|
+
}
|
|
912
917
|
shellRunSeq = 0;
|
|
913
918
|
/** Begin one local shell-escape run; its output accumulates via `appendShellOutput` until `finishShellRun` settles it into the transcript. */
|
|
914
919
|
startShellRun(command) {
|
|
@@ -965,7 +970,6 @@ var TuiStore = class {
|
|
|
965
970
|
kind: "login",
|
|
966
971
|
login: {
|
|
967
972
|
flows: void 0,
|
|
968
|
-
selected: 0,
|
|
969
973
|
signingIn: void 0,
|
|
970
974
|
prompt: void 0,
|
|
971
975
|
busy: true,
|
|
@@ -1089,6 +1093,36 @@ var TuiStore = class {
|
|
|
1089
1093
|
}
|
|
1090
1094
|
};
|
|
1091
1095
|
//#endregion
|
|
1096
|
+
//#region src/tui/acrylMark.ts
|
|
1097
|
+
/**
|
|
1098
|
+
* The compact ACRYL wordmark shown in the fixed header, next to the pet.
|
|
1099
|
+
*
|
|
1100
|
+
* This is the real pixelized brand wordmark (`assets/pixelized/`), rendered
|
|
1101
|
+
* natively as half-block characters (each terminal row packs 2 pixel rows
|
|
1102
|
+
* via \u2580/\u2584/\u2588) instead of downsampled or replaced with a different mark \u2014 54
|
|
1103
|
+
* columns wide at the artwork's own resolution, not simplified. Generated
|
|
1104
|
+
* from the SVG's rect grid and verified by printing it before wiring into
|
|
1105
|
+
* the header \u2014 same proven-before-embedding convention as
|
|
1106
|
+
* `yly/compile-sprites.mjs`.
|
|
1107
|
+
* @module @tomowave/dsh-tui/tui/acrylMark
|
|
1108
|
+
*/
|
|
1109
|
+
/** The plain (uncolored) rows of the ACRYL mark \u2014 11 lines, 54 columns. */
|
|
1110
|
+
const ACRYL_MARK_ROWS = [
|
|
1111
|
+
" ▄▀▀▀▀▀▀▀▀ ",
|
|
1112
|
+
" ▄▀ ▄▄▄▄▄▄▄▄▄ ",
|
|
1113
|
+
" ▄▄ ▄ ▀▄ ",
|
|
1114
|
+
" ▄█▀█▄ ▄▀ ▄▄▄▄▄█ ",
|
|
1115
|
+
" ▄█▀ ▀█▀▀▀▀█▄ ▀▄ ▄▀ ",
|
|
1116
|
+
" █▀ ▀▄ ▀█ ▀▄▄ ▄█ ",
|
|
1117
|
+
"▀▀ ▀ ▀█▄ ▄ ▄██▄▄▄▄▄▄▄▄▄▄",
|
|
1118
|
+
" ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▄ ▄▀▀ ",
|
|
1119
|
+
" ▀▄▄▄▀ ",
|
|
1120
|
+
" █ ",
|
|
1121
|
+
" ▀ "
|
|
1122
|
+
];
|
|
1123
|
+
/** ISO timestamp this build was produced at. */
|
|
1124
|
+
const BUILD_TIME = "2026-09-08T14:33:49.586Z";
|
|
1125
|
+
//#endregion
|
|
1092
1126
|
//#region src/tui/statsFormat.ts
|
|
1093
1127
|
/**
|
|
1094
1128
|
* Compact token count: 517 / 12.2K / 517K / 1.2M (one decimal under three digits).
|
|
@@ -1242,7 +1276,7 @@ function stripSessionIdPrefix(id) {
|
|
|
1242
1276
|
//#region src/tui/liveText.ts
|
|
1243
1277
|
const dim = fg(theme.muted);
|
|
1244
1278
|
const accent = fg(theme.accent);
|
|
1245
|
-
const warning$
|
|
1279
|
+
const warning$2 = fg(theme.warning);
|
|
1246
1280
|
function buildStatusBarText(params) {
|
|
1247
1281
|
const { sessionId, provider, model, status, queuedCount, presetLabel, eventCount, spinnerChar } = params;
|
|
1248
1282
|
const queuedSuffix = queuedCount > 0 ? ` · ${queuedCount} queued` : "";
|
|
@@ -1285,7 +1319,7 @@ const PERMISSION_COLORS = {
|
|
|
1285
1319
|
*/
|
|
1286
1320
|
function buildUpdateHintText(currentVersion, latestVersion) {
|
|
1287
1321
|
if (latestVersion === void 0) return "";
|
|
1288
|
-
return warning$
|
|
1322
|
+
return warning$2(`⬆ ACRYL update available: v${currentVersion} → v${latestVersion}`) + dim(" (run `pnpm add -g @acryl/cli` to upgrade)");
|
|
1289
1323
|
}
|
|
1290
1324
|
function buildPermissionText(permission) {
|
|
1291
1325
|
if (permission === void 0) return "";
|
|
@@ -2905,27 +2939,92 @@ function miniTextFieldInput(state, data) {
|
|
|
2905
2939
|
cursor: state.cursor + data.length
|
|
2906
2940
|
};
|
|
2907
2941
|
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Mask a value for display while typing, revealing the first/last couple of
|
|
2944
|
+
* characters instead of hiding it entirely — a fully-masked live field looks
|
|
2945
|
+
* indistinguishable from an empty one, giving no confirmation that anything
|
|
2946
|
+
* was actually typed. Length-preserving (unlike a static preview's `sk-p…9sZ4`
|
|
2947
|
+
* ellipsis form) so cursor-position math against the masked string stays
|
|
2948
|
+
* correct: every hidden character becomes exactly one `mask` character.
|
|
2949
|
+
*/
|
|
2950
|
+
function partiallyMask(value, mask) {
|
|
2951
|
+
if (value.length <= 2) return mask.repeat(value.length);
|
|
2952
|
+
if (value.length <= 10) return `${value[0]}${mask.repeat(value.length - 2)}${value.at(-1)}`;
|
|
2953
|
+
return `${value.slice(0, 4)}${mask.repeat(value.length - 8)}${value.slice(-4)}`;
|
|
2954
|
+
}
|
|
2908
2955
|
/** Render the field's text, optionally with an inverse-video cursor block at the cursor position. */
|
|
2909
2956
|
function renderMiniTextField(state, cursorVisible, mask) {
|
|
2910
|
-
const display = mask === void 0 ? state.value :
|
|
2957
|
+
const display = mask === void 0 ? state.value : partiallyMask(state.value, mask);
|
|
2911
2958
|
if (!cursorVisible) return display;
|
|
2912
2959
|
return `${display.slice(0, state.cursor)}\x1b[7m${display[state.cursor] ?? " "}\x1b[0m${display.slice(state.cursor + 1)}`;
|
|
2913
2960
|
}
|
|
2914
2961
|
//#endregion
|
|
2962
|
+
//#region src/tui/modelProfile/types.ts
|
|
2963
|
+
/**
|
|
2964
|
+
* Data shapes for the `/model` provider-profile overlay: the read model that
|
|
2965
|
+
* joins `ctx.llm`'s provider directory with `ctx.settings`' stored sections,
|
|
2966
|
+
* the raw shape stored at each provider's settings path, and the mutable
|
|
2967
|
+
* draft one add/edit form works with before a save round-trips it back.
|
|
2968
|
+
* @module @tomowang/dsh-tui/tui/modelProfile/types
|
|
2969
|
+
*/
|
|
2970
|
+
/**
|
|
2971
|
+
* The `apiKeyEnv` reference a provider route falls back to when its settings
|
|
2972
|
+
* profile names none, e.g. `my-proxy` -> `MY_PROXY_API_KEY`. Shared between
|
|
2973
|
+
* the overlay (recomputing it for a just-typed custom route before save) and
|
|
2974
|
+
* the host wiring (deriving it for every already-configured row), so the
|
|
2975
|
+
* naming rule lives in exactly one place.
|
|
2976
|
+
*/
|
|
2977
|
+
function deriveApiKeyRef(route) {
|
|
2978
|
+
const upper = route.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
2979
|
+
return `${/^[A-Z_]/.test(upper) ? upper : `P_${upper}`}_API_KEY`;
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* A short, identifying (never full) preview of a stored secret — first 4 and
|
|
2983
|
+
* last 4 characters, so the API-key field isn't blank-looking when a key is
|
|
2984
|
+
* already set and the user can tell *which* key it is without ever seeing
|
|
2985
|
+
* enough of it to be usable on its own. Short values (<= 10 chars, where that
|
|
2986
|
+
* split would show most of the string anyway) mask everything but the first
|
|
2987
|
+
* and last single character instead.
|
|
2988
|
+
*/
|
|
2989
|
+
function maskKeyPreview(value) {
|
|
2990
|
+
if (value.length <= 10) return value.length <= 2 ? "*".repeat(value.length) : `${value[0]}${"*".repeat(value.length - 2)}${value.at(-1)}`;
|
|
2991
|
+
return `${value.slice(0, 4)}…${value.slice(-4)}`;
|
|
2992
|
+
}
|
|
2993
|
+
//#endregion
|
|
2915
2994
|
//#region src/tui/modelProfile/ModelProfileOverlay.ts
|
|
2916
2995
|
const bold$9 = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
2917
2996
|
const secondary$9 = fg(theme.secondary);
|
|
2918
2997
|
const muted$10 = fg(theme.muted);
|
|
2998
|
+
const warning$1 = fg(theme.warning);
|
|
2919
2999
|
const errorColor$5 = fg(theme.error);
|
|
2920
3000
|
const invert$5 = (s) => `\x1b[7m${s}\x1b[0m`;
|
|
3001
|
+
/**
|
|
3002
|
+
* The only wire protocols `dsh-llm-pi-ai` can speak (`supportedProtocols()`),
|
|
3003
|
+
* matching the DSH Desktop GUI's "Custom provider" dialog's own dropdown
|
|
3004
|
+
* exactly. Free-text protocol entry let a typo reach `assertServiceable` as a
|
|
3005
|
+
* runtime save failure instead of being impossible to enter in the first
|
|
3006
|
+
* place, so this is a cycling choice, never a typed field.
|
|
3007
|
+
*/
|
|
3008
|
+
const PROTOCOLS = [
|
|
3009
|
+
"openai-completions",
|
|
3010
|
+
"openai-responses",
|
|
3011
|
+
"anthropic-messages"
|
|
3012
|
+
];
|
|
2921
3013
|
var ModelProfileOverlay = class {
|
|
3014
|
+
tui;
|
|
2922
3015
|
store;
|
|
2923
3016
|
actions;
|
|
3017
|
+
mode = "picker";
|
|
3018
|
+
modelSearch = emptyMiniTextField();
|
|
3019
|
+
modelPickerCursor = 0;
|
|
3020
|
+
providerSearch = emptyMiniTextField();
|
|
3021
|
+
providerCursor = 0;
|
|
2924
3022
|
confirmDelete;
|
|
3023
|
+
pendingShowModels = false;
|
|
2925
3024
|
formKeySeen = -1;
|
|
2926
3025
|
route = emptyMiniTextField();
|
|
2927
3026
|
displayName = emptyMiniTextField();
|
|
2928
|
-
api =
|
|
3027
|
+
api = PROTOCOLS[0];
|
|
2929
3028
|
baseURL = emptyMiniTextField();
|
|
2930
3029
|
apiKeyDraft = emptyMiniTextField();
|
|
2931
3030
|
models = [];
|
|
@@ -2933,25 +3032,58 @@ var ModelProfileOverlay = class {
|
|
|
2933
3032
|
focused = 0;
|
|
2934
3033
|
modelDraftId = emptyMiniTextField();
|
|
2935
3034
|
modelSelected = 0;
|
|
2936
|
-
|
|
2937
|
-
|
|
3035
|
+
constructor(tui, store, actions) {
|
|
3036
|
+
this.tui = tui;
|
|
2938
3037
|
this.store = store;
|
|
2939
3038
|
this.actions = actions;
|
|
2940
3039
|
}
|
|
2941
3040
|
invalidate() {}
|
|
3041
|
+
/**
|
|
3042
|
+
* Rows available for a scrollable list body: terminal height minus the
|
|
3043
|
+
* lines every such screen spends on chrome (header/hint/notice/search —
|
|
3044
|
+
* `chrome` lines, caller-counted since it varies by screen). Long catalogs
|
|
3045
|
+
* (~35 providers, matching that many models) previously rendered every row
|
|
3046
|
+
* unconditionally, pushing the key-legend hint line — the only place a
|
|
3047
|
+
* shortcut is documented — past the bottom of the terminal, invisible
|
|
3048
|
+
* without scrolling back. Every list-shaped view here windows around the
|
|
3049
|
+
* current selection instead, so the hint line is always the last line
|
|
3050
|
+
* printed and always fits on screen.
|
|
3051
|
+
*/
|
|
3052
|
+
listWindow(chrome) {
|
|
3053
|
+
return Math.max(3, this.tui.terminal.rows - chrome);
|
|
3054
|
+
}
|
|
3055
|
+
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
3056
|
+
visibleRange(count, selected, maxVisible) {
|
|
3057
|
+
if (count <= maxVisible) return {
|
|
3058
|
+
start: 0,
|
|
3059
|
+
end: count
|
|
3060
|
+
};
|
|
3061
|
+
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
3062
|
+
return {
|
|
3063
|
+
start,
|
|
3064
|
+
end: start + maxVisible
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* The global one-line notice (`store.setNotice`), rendered inline here.
|
|
3069
|
+
* `/model` runs as a full-screen overlay that paints over the whole
|
|
3070
|
+
* terminal, so the notice dock underneath — where "Saved X."/"Active model
|
|
3071
|
+
* set to X." otherwise lands — is invisible for as long as this overlay
|
|
3072
|
+
* stays open (e.g. right after `saveProvider` returns to the list view
|
|
3073
|
+
* without closing the overlay). Surfacing it here is what makes a save
|
|
3074
|
+
* actually visible as "done" instead of the screen looking unchanged.
|
|
3075
|
+
*/
|
|
3076
|
+
noticeLines() {
|
|
3077
|
+
const notice = this.store.getSnapshot().notice;
|
|
3078
|
+
return notice === void 0 ? [] : [muted$10(notice)];
|
|
3079
|
+
}
|
|
2942
3080
|
textFields(draft) {
|
|
2943
|
-
|
|
3081
|
+
const base = draft.isNew ? [
|
|
2944
3082
|
"route",
|
|
2945
3083
|
"displayName",
|
|
2946
|
-
"
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
] : [
|
|
2950
|
-
"displayName",
|
|
2951
|
-
"api",
|
|
2952
|
-
"baseURL",
|
|
2953
|
-
"apiKey"
|
|
2954
|
-
];
|
|
3084
|
+
"baseURL"
|
|
3085
|
+
] : ["displayName", "baseURL"];
|
|
3086
|
+
return draft.authMethod === "oauth" ? base : [...base, "apiKey"];
|
|
2955
3087
|
}
|
|
2956
3088
|
/** Reinitialize form-local state from the store's draft when `formKey` changes — the equivalent of the old `key={formKey}` remount. */
|
|
2957
3089
|
syncFormState(mp) {
|
|
@@ -2961,24 +3093,29 @@ var ModelProfileOverlay = class {
|
|
|
2961
3093
|
this.formKeySeen = mp.formKey;
|
|
2962
3094
|
this.route = emptyMiniTextField(draft.route);
|
|
2963
3095
|
this.displayName = emptyMiniTextField(draft.displayName);
|
|
2964
|
-
this.api =
|
|
3096
|
+
this.api = PROTOCOLS.includes(draft.api) ? draft.api : PROTOCOLS[0];
|
|
2965
3097
|
this.baseURL = emptyMiniTextField(draft.baseURL);
|
|
2966
3098
|
this.apiKeyDraft = emptyMiniTextField("");
|
|
2967
3099
|
this.models = [...draft.models];
|
|
2968
|
-
this.showModels =
|
|
3100
|
+
this.showModels = this.pendingShowModels;
|
|
3101
|
+
this.pendingShowModels = false;
|
|
2969
3102
|
this.focused = 0;
|
|
2970
3103
|
this.modelDraftId = emptyMiniTextField();
|
|
2971
3104
|
this.modelSelected = 0;
|
|
2972
|
-
this.modelInputFocused = false;
|
|
2973
3105
|
}
|
|
2974
3106
|
return draft;
|
|
2975
3107
|
}
|
|
2976
3108
|
buildDraft(draft) {
|
|
3109
|
+
const route = draft.isNew ? this.route.value.trim() : draft.route;
|
|
2977
3110
|
return {
|
|
2978
3111
|
...draft,
|
|
2979
|
-
route
|
|
3112
|
+
route,
|
|
3113
|
+
...draft.isNew ? {
|
|
3114
|
+
settingsPath: ["providers", route],
|
|
3115
|
+
apiKeyRef: deriveApiKeyRef(route)
|
|
3116
|
+
} : {},
|
|
2980
3117
|
displayName: this.displayName.value,
|
|
2981
|
-
api: this.api
|
|
3118
|
+
api: this.api,
|
|
2982
3119
|
baseURL: this.baseURL.value,
|
|
2983
3120
|
apiKeyDraft: this.apiKeyDraft.value,
|
|
2984
3121
|
models: this.models
|
|
@@ -2989,111 +3126,223 @@ var ModelProfileOverlay = class {
|
|
|
2989
3126
|
if (overlay.kind !== "modelProfile") return [];
|
|
2990
3127
|
const mp = overlay.modelProfile;
|
|
2991
3128
|
const draft = this.syncFormState(mp);
|
|
2992
|
-
if (draft !== void 0)
|
|
2993
|
-
|
|
3129
|
+
if (draft !== void 0) {
|
|
3130
|
+
if (this.showModels) return this.renderModelListEditor(mp);
|
|
3131
|
+
return this.renderForm(draft, mp);
|
|
3132
|
+
}
|
|
3133
|
+
return this.mode === "picker" ? this.renderModelPicker(mp) : this.renderList(mp);
|
|
3134
|
+
}
|
|
3135
|
+
pickerItems(mp) {
|
|
3136
|
+
const items = [];
|
|
3137
|
+
for (const row of mp.providers ?? []) {
|
|
3138
|
+
if (!row.live) continue;
|
|
3139
|
+
for (const model of row.models) items.push({
|
|
3140
|
+
route: row.route,
|
|
3141
|
+
id: model.id,
|
|
3142
|
+
authMethod: row.authMethod
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
return items;
|
|
3146
|
+
}
|
|
3147
|
+
filteredPickerItems(mp) {
|
|
3148
|
+
const items = this.pickerItems(mp);
|
|
3149
|
+
const query = this.modelSearch.value.trim();
|
|
3150
|
+
return query === "" ? items : fuzzyFilter([...items], query, (item) => `${item.id} ${item.route}`);
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* pi.dev's `/model` picker (see Image #45): a flat, fuzzy-searchable list of
|
|
3154
|
+
* every model on a live (actually usable) route, not a per-provider drill-down.
|
|
3155
|
+
* Provider add/edit/delete moves to a secondary `p` mode instead of being the
|
|
3156
|
+
* default, since picking a model to make active is the far more common action.
|
|
3157
|
+
*/
|
|
3158
|
+
renderModelPicker(mp) {
|
|
3159
|
+
const lines = [warning$1("Only showing models from configured providers. Use /login to add providers.")];
|
|
3160
|
+
lines.push(...this.noticeLines());
|
|
3161
|
+
if (mp.error !== void 0) lines.push(errorColor$5(mp.error));
|
|
3162
|
+
lines.push(`> ${renderMiniTextField(this.modelSearch, true)}`);
|
|
3163
|
+
if (mp.busy && mp.providers === void 0) lines.push(muted$10("Loading…"));
|
|
3164
|
+
const all = this.pickerItems(mp);
|
|
3165
|
+
const filtered = this.filteredPickerItems(mp);
|
|
3166
|
+
const chrome = lines.length + 2;
|
|
3167
|
+
const maxVisible = this.listWindow(chrome);
|
|
3168
|
+
const { start, end } = this.visibleRange(filtered.length, this.modelPickerCursor, maxVisible);
|
|
3169
|
+
filtered.slice(start, end).forEach((item, offset) => {
|
|
3170
|
+
const isSelected = start + offset === this.modelPickerCursor;
|
|
3171
|
+
const via = item.authMethod === "oauth" ? " oauth" : item.authMethod === "api-key" ? " api" : "";
|
|
3172
|
+
const text = `${isSelected ? "→ " : " "}${item.id} [${item.route}${via}]`;
|
|
3173
|
+
lines.push(isSelected ? invert$5(text) : text);
|
|
3174
|
+
});
|
|
3175
|
+
if (filtered.length > maxVisible) lines.push(muted$10(`(${this.modelPickerCursor + 1}/${filtered.length})`));
|
|
3176
|
+
if (all.length === 0 && mp.providers !== void 0) lines.push(muted$10("No models available yet — use /login to sign in to a provider."));
|
|
3177
|
+
else if (filtered.length === 0) lines.push(muted$10("No matching models."));
|
|
3178
|
+
lines.push(muted$10("type to search · ↑↓ select · enter set active · ctrl+p manage providers (add/remove models) · esc close"));
|
|
3179
|
+
return lines;
|
|
3180
|
+
}
|
|
3181
|
+
handleModelPickerInput(data, mp) {
|
|
3182
|
+
if (matchesKey(data, Key.escape)) {
|
|
3183
|
+
this.actions.closeModelProfile();
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
if (matchesKey(data, Key.ctrl("p"))) {
|
|
3187
|
+
this.mode = "providers";
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
const filtered = this.filteredPickerItems(mp);
|
|
3191
|
+
if (matchesKey(data, Key.up)) {
|
|
3192
|
+
this.modelPickerCursor = Math.max(0, this.modelPickerCursor - 1);
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
if (matchesKey(data, Key.down)) {
|
|
3196
|
+
this.modelPickerCursor = Math.min(Math.max(0, filtered.length - 1), this.modelPickerCursor + 1);
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
if (matchesKey(data, Key.enter)) {
|
|
3200
|
+
const item = filtered[this.modelPickerCursor];
|
|
3201
|
+
if (item !== void 0) {
|
|
3202
|
+
this.actions.setActiveModel(item.route, item.id);
|
|
3203
|
+
this.actions.closeModelProfile();
|
|
3204
|
+
}
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
const next = miniTextFieldInput(this.modelSearch, data);
|
|
3208
|
+
if (next !== void 0) {
|
|
3209
|
+
this.modelSearch = next;
|
|
3210
|
+
this.modelPickerCursor = 0;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
filteredProviders(mp) {
|
|
3214
|
+
const providers = mp.providers ?? [];
|
|
3215
|
+
const query = this.providerSearch.value.trim();
|
|
3216
|
+
return query === "" ? providers : fuzzyFilter([...providers], query, (row) => `${row.displayName} ${row.route}`);
|
|
2994
3217
|
}
|
|
2995
3218
|
renderList(mp) {
|
|
2996
|
-
const { providers,
|
|
3219
|
+
const { providers, busy, error } = mp;
|
|
2997
3220
|
const lines = [bold$9(secondary$9("Model providers"))];
|
|
3221
|
+
lines.push(...this.noticeLines());
|
|
2998
3222
|
if (error !== void 0) lines.push(errorColor$5(error));
|
|
3223
|
+
lines.push(`> ${renderMiniTextField(this.providerSearch, true)}`);
|
|
2999
3224
|
if (busy && providers === void 0) lines.push(muted$10("Loading…"));
|
|
3000
|
-
|
|
3225
|
+
const filtered = this.filteredProviders(mp);
|
|
3226
|
+
const chrome = lines.length + 2;
|
|
3227
|
+
const maxVisible = this.listWindow(chrome);
|
|
3228
|
+
const { start, end } = this.visibleRange(filtered.length, this.providerCursor, maxVisible);
|
|
3229
|
+
filtered.slice(start, end).forEach((row, offset) => {
|
|
3230
|
+
const index = start + offset;
|
|
3001
3231
|
const marker = row.configured ? "● " : "○ ";
|
|
3002
3232
|
const active = row.live ? " (active)" : "";
|
|
3003
|
-
const noKey = row.apiKeyConfigured ? "" : " [no api key]";
|
|
3004
|
-
const confirm = this.confirmDelete === index ? " — press
|
|
3005
|
-
const text = `${index ===
|
|
3006
|
-
lines.push(index ===
|
|
3233
|
+
const noKey = row.apiKeyConfigured ? ` [${row.authMethod === "oauth" ? "oauth" : "api key"}]` : " [no api key]";
|
|
3234
|
+
const confirm = this.confirmDelete === index ? " — press ctrl+x again to delete" : "";
|
|
3235
|
+
const text = `${index === this.providerCursor ? "› " : " "}${marker}${row.displayName}${active}${noKey}${confirm}`;
|
|
3236
|
+
lines.push(index === this.providerCursor ? invert$5(text) : text);
|
|
3007
3237
|
});
|
|
3008
|
-
if (
|
|
3009
|
-
lines.push(muted$10("
|
|
3238
|
+
if (filtered.length > maxVisible) lines.push(muted$10(`(${this.providerCursor + 1}/${filtered.length})`));
|
|
3239
|
+
if (providers?.length === 0) lines.push(muted$10("No providers configured yet — ctrl+n to add one."));
|
|
3240
|
+
else if (filtered.length === 0) lines.push(muted$10("No matching providers."));
|
|
3241
|
+
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"));
|
|
3010
3242
|
return lines;
|
|
3011
3243
|
}
|
|
3012
3244
|
handleListInput(data, mp) {
|
|
3013
|
-
const { providers, selected } = mp;
|
|
3014
3245
|
if (matchesKey(data, Key.escape)) {
|
|
3015
|
-
this.
|
|
3246
|
+
this.mode = "picker";
|
|
3016
3247
|
return;
|
|
3017
3248
|
}
|
|
3018
|
-
if (
|
|
3019
|
-
|
|
3249
|
+
if (matchesKey(data, Key.ctrl("n"))) {
|
|
3250
|
+
this.actions.createProvider();
|
|
3020
3251
|
return;
|
|
3021
3252
|
}
|
|
3253
|
+
const filtered = this.filteredProviders(mp);
|
|
3254
|
+
if (filtered.length === 0) return;
|
|
3022
3255
|
if (matchesKey(data, Key.up)) {
|
|
3023
3256
|
this.confirmDelete = void 0;
|
|
3024
|
-
this.
|
|
3257
|
+
this.providerCursor = Math.max(0, this.providerCursor - 1);
|
|
3025
3258
|
return;
|
|
3026
3259
|
}
|
|
3027
3260
|
if (matchesKey(data, Key.down)) {
|
|
3028
3261
|
this.confirmDelete = void 0;
|
|
3029
|
-
this.
|
|
3262
|
+
this.providerCursor = Math.min(filtered.length - 1, this.providerCursor + 1);
|
|
3030
3263
|
return;
|
|
3031
3264
|
}
|
|
3032
3265
|
if (matchesKey(data, Key.enter)) {
|
|
3033
|
-
this.actions.editProvider(
|
|
3266
|
+
this.actions.editProvider(filtered[this.providerCursor].route);
|
|
3034
3267
|
return;
|
|
3035
3268
|
}
|
|
3036
|
-
if (data
|
|
3037
|
-
this.
|
|
3269
|
+
if (matchesKey(data, Key.ctrl("e"))) {
|
|
3270
|
+
this.pendingShowModels = true;
|
|
3271
|
+
this.actions.editProvider(filtered[this.providerCursor].route);
|
|
3038
3272
|
return;
|
|
3039
3273
|
}
|
|
3040
|
-
if (data
|
|
3041
|
-
const row =
|
|
3274
|
+
if (matchesKey(data, Key.ctrl("a"))) {
|
|
3275
|
+
const row = filtered[this.providerCursor];
|
|
3042
3276
|
const model = row.models[0];
|
|
3043
3277
|
if (model !== void 0) this.actions.setActiveModel(row.route, model.id);
|
|
3044
3278
|
return;
|
|
3045
3279
|
}
|
|
3046
|
-
if (data
|
|
3047
|
-
if (this.confirmDelete ===
|
|
3280
|
+
if (matchesKey(data, Key.ctrl("x"))) {
|
|
3281
|
+
if (this.confirmDelete === this.providerCursor) {
|
|
3048
3282
|
this.confirmDelete = void 0;
|
|
3049
|
-
this.actions.deleteProvider(
|
|
3050
|
-
} else this.confirmDelete =
|
|
3283
|
+
this.actions.deleteProvider(filtered[this.providerCursor]);
|
|
3284
|
+
} else this.confirmDelete = this.providerCursor;
|
|
3051
3285
|
return;
|
|
3052
3286
|
}
|
|
3053
3287
|
this.confirmDelete = void 0;
|
|
3288
|
+
const next = miniTextFieldInput(this.providerSearch, data);
|
|
3289
|
+
if (next !== void 0) {
|
|
3290
|
+
this.providerSearch = next;
|
|
3291
|
+
this.providerCursor = 0;
|
|
3292
|
+
}
|
|
3054
3293
|
}
|
|
3055
3294
|
renderForm(draft, mp) {
|
|
3056
3295
|
const textFields = this.textFields(draft);
|
|
3057
|
-
const
|
|
3058
|
-
const
|
|
3296
|
+
const protocolRow = textFields.length;
|
|
3297
|
+
const modelsRow = protocolRow + 1;
|
|
3298
|
+
const saveRow = protocolRow + 2;
|
|
3059
3299
|
const fieldState = {
|
|
3060
3300
|
route: this.route,
|
|
3061
3301
|
displayName: this.displayName,
|
|
3062
|
-
api: this.api,
|
|
3063
3302
|
baseURL: this.baseURL,
|
|
3064
3303
|
apiKey: this.apiKeyDraft
|
|
3065
3304
|
};
|
|
3066
3305
|
const labels = {
|
|
3067
|
-
route: "
|
|
3068
|
-
displayName: "
|
|
3069
|
-
api: "Protocol",
|
|
3306
|
+
route: "Provider ID",
|
|
3307
|
+
displayName: "Display name",
|
|
3070
3308
|
baseURL: "Base URL",
|
|
3071
|
-
apiKey: draft.apiKeyConfigured ?
|
|
3309
|
+
apiKey: draft.apiKeyConfigured ? `API key (set${draft.apiKeyPreview === void 0 ? "" : `: ${draft.apiKeyPreview}`} — leave blank to keep)` : "API key"
|
|
3072
3310
|
};
|
|
3073
|
-
const lines = [bold$9(secondary$9(draft.isNew ? "
|
|
3311
|
+
const lines = [bold$9(secondary$9(draft.isNew ? "Custom provider" : `Edit ${draft.displayName || draft.route}`))];
|
|
3312
|
+
lines.push(...this.noticeLines());
|
|
3074
3313
|
if (mp.error !== void 0) lines.push(errorColor$5(mp.error));
|
|
3314
|
+
if (draft.authMethod === "oauth") lines.push(muted$10("(signed in via OAuth — no separate API key for this route)"));
|
|
3075
3315
|
textFields.forEach((field, index) => {
|
|
3076
3316
|
const isFocused = this.focused === index;
|
|
3077
3317
|
const mask = field === "apiKey" ? "*" : void 0;
|
|
3078
3318
|
const prefix = `${isFocused ? "› " : " "}${labels[field]}: `;
|
|
3079
3319
|
lines.push(`${prefix}${renderMiniTextField(fieldState[field], isFocused, mask)}`);
|
|
3080
3320
|
});
|
|
3321
|
+
const protocolFocused = this.focused === protocolRow;
|
|
3322
|
+
const protocolText = `${protocolFocused ? "› " : " "}API protocol: ‹ ${this.api} ›`;
|
|
3323
|
+
lines.push(protocolFocused ? invert$5(protocolText) : protocolText);
|
|
3081
3324
|
const modelsText = `${this.focused === modelsRow ? "› " : " "}Models (${this.models.length}) — enter to edit`;
|
|
3082
3325
|
lines.push(this.focused === modelsRow ? invert$5(modelsText) : modelsText);
|
|
3083
|
-
const saveText = `${this.focused === saveRow ? "› " : " "}${mp.busy ? "Saving…" : "Save"}`;
|
|
3326
|
+
const saveText = `${this.focused === saveRow ? "› " : " "}${mp.busy ? "Saving…" : draft.isNew ? "Create provider" : "Save changes"}`;
|
|
3084
3327
|
lines.push(this.focused === saveRow ? invert$5(saveText) : saveText);
|
|
3085
|
-
|
|
3328
|
+
const removeHint = draft.apiKeyConfigured ? " · ctrl+d sign out (remove key/OAuth grant)" : "";
|
|
3329
|
+
lines.push(muted$10(`tab/shift+tab move · ←→ cycle protocol · enter confirm/activate · esc cancel${removeHint}`));
|
|
3086
3330
|
return lines;
|
|
3087
3331
|
}
|
|
3088
3332
|
handleFormInput(data, draft) {
|
|
3089
3333
|
const textFields = this.textFields(draft);
|
|
3090
|
-
const
|
|
3091
|
-
const
|
|
3092
|
-
const
|
|
3334
|
+
const protocolRow = textFields.length;
|
|
3335
|
+
const modelsRow = protocolRow + 1;
|
|
3336
|
+
const saveRow = protocolRow + 2;
|
|
3337
|
+
const rowCount = protocolRow + 3;
|
|
3093
3338
|
if (matchesKey(data, Key.escape)) {
|
|
3094
3339
|
this.actions.backToProviderList();
|
|
3095
3340
|
return;
|
|
3096
3341
|
}
|
|
3342
|
+
if (matchesKey(data, Key.ctrl("d")) && draft.apiKeyConfigured) {
|
|
3343
|
+
this.actions.clearApiKey(draft);
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3097
3346
|
if (matchesKey(data, "shift+tab")) {
|
|
3098
3347
|
this.focused = (this.focused - 1 + rowCount) % rowCount;
|
|
3099
3348
|
return;
|
|
@@ -3102,6 +3351,12 @@ var ModelProfileOverlay = class {
|
|
|
3102
3351
|
this.focused = (this.focused + 1) % rowCount;
|
|
3103
3352
|
return;
|
|
3104
3353
|
}
|
|
3354
|
+
if (this.focused === protocolRow && (matchesKey(data, Key.left) || matchesKey(data, Key.right))) {
|
|
3355
|
+
const direction = matchesKey(data, Key.left) ? -1 : 1;
|
|
3356
|
+
const currentIndex = PROTOCOLS.indexOf(this.api);
|
|
3357
|
+
this.api = PROTOCOLS[(currentIndex + direction + PROTOCOLS.length) % PROTOCOLS.length];
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3105
3360
|
if (matchesKey(data, Key.enter) && this.focused === modelsRow) {
|
|
3106
3361
|
this.showModels = true;
|
|
3107
3362
|
return;
|
|
@@ -3110,7 +3365,7 @@ var ModelProfileOverlay = class {
|
|
|
3110
3365
|
this.actions.saveProvider(this.buildDraft(draft));
|
|
3111
3366
|
return;
|
|
3112
3367
|
}
|
|
3113
|
-
if (matchesKey(data, Key.enter) && this.focused
|
|
3368
|
+
if (matchesKey(data, Key.enter) && this.focused <= protocolRow) {
|
|
3114
3369
|
this.focused = (this.focused + 1) % rowCount;
|
|
3115
3370
|
return;
|
|
3116
3371
|
}
|
|
@@ -3119,34 +3374,43 @@ var ModelProfileOverlay = class {
|
|
|
3119
3374
|
const next = miniTextFieldInput({
|
|
3120
3375
|
route: this.route,
|
|
3121
3376
|
displayName: this.displayName,
|
|
3122
|
-
api: this.api,
|
|
3123
3377
|
baseURL: this.baseURL,
|
|
3124
3378
|
apiKey: this.apiKeyDraft
|
|
3125
3379
|
}[field], data);
|
|
3126
3380
|
if (next === void 0) return;
|
|
3127
3381
|
if (field === "route") this.route = next;
|
|
3128
3382
|
else if (field === "displayName") this.displayName = next;
|
|
3129
|
-
else if (field === "api") this.api = next;
|
|
3130
3383
|
else if (field === "baseURL") this.baseURL = next;
|
|
3131
3384
|
else this.apiKeyDraft = next;
|
|
3132
3385
|
}
|
|
3133
3386
|
}
|
|
3387
|
+
/**
|
|
3388
|
+
* No Tab-toggled focus, on purpose: a hidden "which of two things am I
|
|
3389
|
+
* typing into right now" mode with no visible indicator (the earlier
|
|
3390
|
+
* design) is exactly the kind of invisible-state bug already fixed
|
|
3391
|
+
* elsewhere in this file. Instead the "Add id" field is *always* live —
|
|
3392
|
+
* every printable key, paste, and left/right/backspace/delete/home/end
|
|
3393
|
+
* goes there, full stop, since ↑/↓ are the only keys `miniTextFieldInput`
|
|
3394
|
+
* never claims. That leaves ↑/↓ free to always drive the list selection
|
|
3395
|
+
* (for removal) at the same time, with no mode switch and no key that can
|
|
3396
|
+
* ever mean two different things depending on invisible state.
|
|
3397
|
+
*/
|
|
3134
3398
|
renderModelListEditor(mp) {
|
|
3135
3399
|
const lines = [bold$9(secondary$9("Models"))];
|
|
3136
3400
|
this.models.forEach((model, index) => {
|
|
3137
|
-
const isSelected =
|
|
3401
|
+
const isSelected = index === this.modelSelected;
|
|
3138
3402
|
const text = `${isSelected ? "› " : " "}${model.id}${model.name === void 0 ? "" : ` — ${model.name}`}`;
|
|
3139
3403
|
lines.push(isSelected ? invert$5(text) : text);
|
|
3140
3404
|
});
|
|
3141
3405
|
if (this.models.length === 0) lines.push(muted$10("No models yet."));
|
|
3142
|
-
lines.push(
|
|
3406
|
+
lines.push(`Add id: ${renderMiniTextField(this.modelDraftId, true)}`);
|
|
3143
3407
|
if (mp.busy) lines.push(muted$10("Discovering…"));
|
|
3144
3408
|
if (mp.discovered !== void 0) if (mp.discovered.length === 0) lines.push(muted$10("No models discovered."));
|
|
3145
3409
|
else {
|
|
3146
|
-
lines.push(muted$10("Discovered —
|
|
3410
|
+
lines.push(muted$10("Discovered — type one into \"Add id\" above to adopt it:"));
|
|
3147
3411
|
for (const model of mp.discovered) lines.push(muted$10(` ${model.id}${model.name === void 0 ? "" : ` — ${model.name}`}`));
|
|
3148
3412
|
}
|
|
3149
|
-
lines.push(muted$10("
|
|
3413
|
+
lines.push(muted$10("type + enter add · ↑↓ select · ctrl+x remove · ctrl+g discover · ctrl+s save · esc back (without saving)"));
|
|
3150
3414
|
return lines;
|
|
3151
3415
|
}
|
|
3152
3416
|
addModel(id) {
|
|
@@ -3162,36 +3426,34 @@ var ModelProfileOverlay = class {
|
|
|
3162
3426
|
this.showModels = false;
|
|
3163
3427
|
return;
|
|
3164
3428
|
}
|
|
3165
|
-
if (matchesKey(data, Key.
|
|
3166
|
-
this.
|
|
3429
|
+
if (matchesKey(data, Key.ctrl("s"))) {
|
|
3430
|
+
this.actions.saveProvider(this.buildDraft(draft));
|
|
3167
3431
|
return;
|
|
3168
3432
|
}
|
|
3169
|
-
if (
|
|
3170
|
-
|
|
3171
|
-
this.addModel(this.modelDraftId.value);
|
|
3172
|
-
return;
|
|
3173
|
-
}
|
|
3174
|
-
const next = miniTextFieldInput(this.modelDraftId, data);
|
|
3175
|
-
if (next !== void 0) this.modelDraftId = next;
|
|
3433
|
+
if (matchesKey(data, Key.ctrl("g"))) {
|
|
3434
|
+
this.actions.discoverModelsForDraft(this.buildDraft(draft));
|
|
3176
3435
|
return;
|
|
3177
3436
|
}
|
|
3178
|
-
if (data
|
|
3179
|
-
this.
|
|
3437
|
+
if (matchesKey(data, Key.ctrl("x"))) {
|
|
3438
|
+
if (this.models.length === 0) return;
|
|
3439
|
+
this.models = this.models.filter((_, index) => index !== this.modelSelected);
|
|
3440
|
+
this.modelSelected = Math.max(0, Math.min(this.modelSelected, this.models.length - 1));
|
|
3180
3441
|
return;
|
|
3181
3442
|
}
|
|
3182
|
-
if (this.models.length === 0) return;
|
|
3183
3443
|
if (matchesKey(data, Key.up)) {
|
|
3184
|
-
this.modelSelected = Math.max(0, this.modelSelected - 1);
|
|
3444
|
+
if (this.models.length > 0) this.modelSelected = Math.max(0, this.modelSelected - 1);
|
|
3185
3445
|
return;
|
|
3186
3446
|
}
|
|
3187
3447
|
if (matchesKey(data, Key.down)) {
|
|
3188
|
-
this.modelSelected = Math.min(this.models.length - 1, this.modelSelected + 1);
|
|
3448
|
+
if (this.models.length > 0) this.modelSelected = Math.min(this.models.length - 1, this.modelSelected + 1);
|
|
3189
3449
|
return;
|
|
3190
3450
|
}
|
|
3191
|
-
if (data
|
|
3192
|
-
this.
|
|
3193
|
-
|
|
3451
|
+
if (matchesKey(data, Key.enter)) {
|
|
3452
|
+
this.addModel(this.modelDraftId.value);
|
|
3453
|
+
return;
|
|
3194
3454
|
}
|
|
3455
|
+
const next = miniTextFieldInput(this.modelDraftId, data);
|
|
3456
|
+
if (next !== void 0) this.modelDraftId = next;
|
|
3195
3457
|
}
|
|
3196
3458
|
handleInput(data) {
|
|
3197
3459
|
const overlay = this.store.getSnapshot().overlay;
|
|
@@ -3203,7 +3465,8 @@ var ModelProfileOverlay = class {
|
|
|
3203
3465
|
else this.handleFormInput(data, draft);
|
|
3204
3466
|
return;
|
|
3205
3467
|
}
|
|
3206
|
-
this.
|
|
3468
|
+
if (this.mode === "picker") this.handleModelPickerInput(data, mp);
|
|
3469
|
+
else this.handleListInput(data, mp);
|
|
3207
3470
|
}
|
|
3208
3471
|
};
|
|
3209
3472
|
//#endregion
|
|
@@ -3212,33 +3475,121 @@ const bold$8 = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
|
3212
3475
|
const secondary$8 = fg(theme.secondary);
|
|
3213
3476
|
const muted$9 = fg(theme.muted);
|
|
3214
3477
|
const errorColor$4 = fg(theme.error);
|
|
3478
|
+
const successColor = fg(theme.success);
|
|
3215
3479
|
const invert$4 = (s) => `\x1b[7m${s}\x1b[0m`;
|
|
3480
|
+
/** A non-secret reminder that replacing an API-key prompt will overwrite an existing stored key. */
|
|
3481
|
+
function formatStoredApiKeyHint(preview) {
|
|
3482
|
+
return preview === void 0 ? void 0 : `Stored key: ${preview}`;
|
|
3483
|
+
}
|
|
3484
|
+
const AUTH_TYPES = ["oauth", "api-key"];
|
|
3485
|
+
const AUTH_TYPE_LABELS = {
|
|
3486
|
+
oauth: "Sign in with an account",
|
|
3487
|
+
"api-key": "Sign in with an API key"
|
|
3488
|
+
};
|
|
3216
3489
|
var LoginOverlay = class {
|
|
3490
|
+
tui;
|
|
3217
3491
|
store;
|
|
3218
3492
|
actions;
|
|
3493
|
+
step = "authType";
|
|
3494
|
+
authType;
|
|
3495
|
+
authTypeCursor = 0;
|
|
3496
|
+
chooserSkipped = false;
|
|
3497
|
+
autoSkipChecked = false;
|
|
3498
|
+
listCursor = 0;
|
|
3499
|
+
searchQuery = emptyMiniTextField();
|
|
3219
3500
|
promptField = emptyMiniTextField();
|
|
3220
3501
|
promptCursor = 0;
|
|
3221
|
-
constructor(store, actions) {
|
|
3502
|
+
constructor(tui, store, actions) {
|
|
3503
|
+
this.tui = tui;
|
|
3222
3504
|
this.store = store;
|
|
3223
3505
|
this.actions = actions;
|
|
3224
3506
|
}
|
|
3225
3507
|
invalidate() {}
|
|
3226
|
-
|
|
3227
|
-
|
|
3508
|
+
/** 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. */
|
|
3509
|
+
listWindow(chrome) {
|
|
3510
|
+
return Math.max(3, this.tui.terminal.rows - chrome);
|
|
3511
|
+
}
|
|
3512
|
+
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
3513
|
+
visibleRange(count, selected, maxVisible) {
|
|
3514
|
+
if (count <= maxVisible) return {
|
|
3515
|
+
start: 0,
|
|
3516
|
+
end: count
|
|
3517
|
+
};
|
|
3518
|
+
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
3519
|
+
return {
|
|
3520
|
+
start,
|
|
3521
|
+
end: start + maxVisible
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* The global one-line notice (`store.setNotice`), rendered inline here.
|
|
3526
|
+
* `/login` runs as a full-screen overlay that paints over the whole
|
|
3527
|
+
* terminal, so the notice dock underneath — where sign-in progress,
|
|
3528
|
+
* success ("Signed in to X."), and failure text otherwise land — is
|
|
3529
|
+
* completely hidden for as long as this overlay stays open. Surfacing it
|
|
3530
|
+
* here is what makes an OAuth callback's result actually visible instead
|
|
3531
|
+
* of the screen appearing to do nothing once the browser redirects back.
|
|
3532
|
+
*/
|
|
3533
|
+
noticeLines() {
|
|
3534
|
+
const notice = this.store.getSnapshot().notice;
|
|
3535
|
+
return notice === void 0 ? [] : [muted$9(notice)];
|
|
3536
|
+
}
|
|
3537
|
+
/** 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. */
|
|
3538
|
+
maybeAutoSkipChooser(login) {
|
|
3539
|
+
if (this.autoSkipChecked || login.flows === void 0) return;
|
|
3540
|
+
this.autoSkipChecked = true;
|
|
3541
|
+
const hasOAuth = login.flows.some((flow) => flow.methods.some((method) => method.id === "oauth"));
|
|
3542
|
+
if (hasOAuth === login.flows.some((flow) => flow.methods.some((method) => method.id === "api-key"))) return;
|
|
3543
|
+
this.authType = hasOAuth ? "oauth" : "api-key";
|
|
3544
|
+
this.step = "list";
|
|
3545
|
+
this.chooserSkipped = true;
|
|
3546
|
+
}
|
|
3547
|
+
filteredFlows(login) {
|
|
3548
|
+
const all = login.flows ?? [];
|
|
3549
|
+
const byType = this.authType === void 0 ? all : all.filter((flow) => flow.methods.some((method) => method.id === this.authType));
|
|
3550
|
+
const query = this.searchQuery.value.trim();
|
|
3551
|
+
return query === "" ? byType : fuzzyFilter([...byType], query, (flow) => flow.label);
|
|
3552
|
+
}
|
|
3553
|
+
renderAuthTypeChooser(login) {
|
|
3554
|
+
const lines = [bold$8(secondary$8("Select authentication method"))];
|
|
3555
|
+
lines.push(...this.noticeLines());
|
|
3228
3556
|
if (login.error !== void 0) lines.push(errorColor$4(login.error));
|
|
3229
3557
|
if (login.busy && login.flows === void 0) lines.push(muted$9("Loading…"));
|
|
3230
|
-
|
|
3231
|
-
const
|
|
3558
|
+
AUTH_TYPES.forEach((type, index) => {
|
|
3559
|
+
const text = `${index === this.authTypeCursor ? "› " : " "}${AUTH_TYPE_LABELS[type]}`;
|
|
3560
|
+
lines.push(index === this.authTypeCursor ? invert$4(text) : text);
|
|
3561
|
+
});
|
|
3562
|
+
lines.push(muted$9("↑↓ select · enter continue · esc close"));
|
|
3563
|
+
return lines;
|
|
3564
|
+
}
|
|
3565
|
+
renderProviderList(login) {
|
|
3566
|
+
const lines = [bold$8(secondary$8("Select provider to configure:"))];
|
|
3567
|
+
lines.push(...this.noticeLines());
|
|
3568
|
+
if (login.error !== void 0) lines.push(errorColor$4(login.error));
|
|
3569
|
+
lines.push(`> ${renderMiniTextField(this.searchQuery, true)}`);
|
|
3570
|
+
if (login.busy && login.flows === void 0) lines.push(muted$9("Loading…"));
|
|
3571
|
+
const flows = this.filteredFlows(login);
|
|
3572
|
+
const chrome = lines.length + 2;
|
|
3573
|
+
const maxVisible = this.listWindow(chrome);
|
|
3574
|
+
const { start, end } = this.visibleRange(flows.length, this.listCursor, maxVisible);
|
|
3575
|
+
flows.slice(start, end).forEach((flow, offset) => {
|
|
3576
|
+
const index = start + offset;
|
|
3577
|
+
const marker = flow.inFlight ? "· " : flow.configured ? "✓ " : "○ ";
|
|
3232
3578
|
const signingIn = login.signingIn === flow.key ? " — signing in…" : "";
|
|
3233
|
-
const
|
|
3234
|
-
|
|
3579
|
+
const via = flow.authMethod === "oauth" ? " [oauth]" : flow.authMethod === "api-key" ? " [api]" : "";
|
|
3580
|
+
const readyNote = login.signingIn !== flow.key && flow.configured ? ` — ready to use${via}` : "";
|
|
3581
|
+
const text = `${index === this.listCursor ? "› " : " "}${marker}${flow.label}${signingIn}${readyNote}`;
|
|
3582
|
+
lines.push(index === this.listCursor ? invert$4(text) : flow.configured ? successColor(text) : text);
|
|
3235
3583
|
});
|
|
3236
|
-
if (
|
|
3237
|
-
lines.push(muted$9("
|
|
3584
|
+
if (flows.length > maxVisible) lines.push(muted$9(`(${this.listCursor + 1}/${flows.length})`));
|
|
3585
|
+
if (login.flows !== void 0 && flows.length === 0) lines.push(muted$9("No matching providers."));
|
|
3586
|
+
const back = this.chooserSkipped ? "esc close" : "esc back";
|
|
3587
|
+
lines.push(muted$9(`type to search · ↑↓ select · enter sign in (or edit key/models if already configured) · ctrl+p add custom provider · ${back}`));
|
|
3238
3588
|
return lines;
|
|
3239
3589
|
}
|
|
3240
3590
|
renderPrompt(prompt) {
|
|
3241
3591
|
const lines = [bold$8(secondary$8("Sign in"))];
|
|
3592
|
+
lines.push(...this.noticeLines());
|
|
3242
3593
|
lines.push(prompt.message);
|
|
3243
3594
|
if (prompt.kind === "select") {
|
|
3244
3595
|
prompt.options.forEach((option, index) => {
|
|
@@ -3251,26 +3602,34 @@ var LoginOverlay = class {
|
|
|
3251
3602
|
const mask = prompt.kind === "secret" ? "•" : void 0;
|
|
3252
3603
|
const placeholder = prompt.placeholder ?? (prompt.kind === "secret" ? "API key" : "code");
|
|
3253
3604
|
const field = this.promptField.value === "" ? `(${placeholder})` : renderMiniTextField(this.promptField, true, mask);
|
|
3605
|
+
const storedKeyHint = prompt.kind === "secret" ? formatStoredApiKeyHint(prompt.storedApiKeyPreview) : void 0;
|
|
3606
|
+
if (storedKeyHint !== void 0) lines.push(muted$9(storedKeyHint));
|
|
3254
3607
|
lines.push(`> ${field}`);
|
|
3255
|
-
lines.push(muted$9("
|
|
3608
|
+
lines.push(muted$9("escape/ctrl+c to cancel, enter to submit"));
|
|
3256
3609
|
}
|
|
3257
3610
|
return lines;
|
|
3258
3611
|
}
|
|
3259
3612
|
render(_width) {
|
|
3260
3613
|
const overlay = this.store.getSnapshot().overlay;
|
|
3261
3614
|
if (overlay.kind !== "login") return [];
|
|
3262
|
-
const {
|
|
3263
|
-
|
|
3615
|
+
const { login } = overlay;
|
|
3616
|
+
if (login.prompt !== void 0) return this.renderPrompt(login.prompt);
|
|
3617
|
+
this.maybeAutoSkipChooser(login);
|
|
3618
|
+
return this.step === "authType" ? this.renderAuthTypeChooser(login) : this.renderProviderList(login);
|
|
3264
3619
|
}
|
|
3265
3620
|
handleInput(data) {
|
|
3266
3621
|
const overlay = this.store.getSnapshot().overlay;
|
|
3267
3622
|
if (overlay.kind !== "login") return;
|
|
3268
|
-
const {
|
|
3269
|
-
if (prompt !== void 0)
|
|
3270
|
-
|
|
3623
|
+
const { login } = overlay;
|
|
3624
|
+
if (login.prompt !== void 0) {
|
|
3625
|
+
this.handlePromptInput(data, login.prompt);
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
if (this.step === "authType") this.handleAuthTypeChooserInput(data);
|
|
3629
|
+
else this.handleProviderListInput(data, login);
|
|
3271
3630
|
}
|
|
3272
3631
|
handlePromptInput(data, prompt) {
|
|
3273
|
-
if (matchesKey(data, Key.escape)) {
|
|
3632
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
3274
3633
|
this.actions.answerAuthorizationPrompt("");
|
|
3275
3634
|
return;
|
|
3276
3635
|
}
|
|
@@ -3293,22 +3652,65 @@ var LoginOverlay = class {
|
|
|
3293
3652
|
const next = miniTextFieldInput(this.promptField, data);
|
|
3294
3653
|
if (next !== void 0) this.promptField = next;
|
|
3295
3654
|
}
|
|
3296
|
-
|
|
3655
|
+
handleAuthTypeChooserInput(data) {
|
|
3297
3656
|
if (matchesKey(data, Key.escape)) {
|
|
3298
3657
|
this.actions.closeLogin();
|
|
3299
3658
|
return;
|
|
3300
3659
|
}
|
|
3301
|
-
const flows = login.flows;
|
|
3302
|
-
if (flows === void 0 || flows.length === 0) return;
|
|
3303
3660
|
if (matchesKey(data, Key.up)) {
|
|
3304
|
-
this.
|
|
3661
|
+
this.authTypeCursor = Math.max(0, this.authTypeCursor - 1);
|
|
3305
3662
|
return;
|
|
3306
3663
|
}
|
|
3307
3664
|
if (matchesKey(data, Key.down)) {
|
|
3308
|
-
this.
|
|
3665
|
+
this.authTypeCursor = Math.min(AUTH_TYPES.length - 1, this.authTypeCursor + 1);
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
if (matchesKey(data, Key.enter)) {
|
|
3669
|
+
this.authType = AUTH_TYPES[this.authTypeCursor];
|
|
3670
|
+
this.step = "list";
|
|
3671
|
+
this.listCursor = 0;
|
|
3672
|
+
this.searchQuery = emptyMiniTextField();
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
handleProviderListInput(data, login) {
|
|
3676
|
+
if (matchesKey(data, Key.escape)) {
|
|
3677
|
+
if (this.chooserSkipped) this.actions.closeLogin();
|
|
3678
|
+
else {
|
|
3679
|
+
this.step = "authType";
|
|
3680
|
+
this.searchQuery = emptyMiniTextField();
|
|
3681
|
+
}
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
if (matchesKey(data, Key.ctrl("p"))) {
|
|
3685
|
+
this.actions.addCustomProvider();
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
const flows = this.filteredFlows(login);
|
|
3689
|
+
if (matchesKey(data, Key.up)) {
|
|
3690
|
+
this.listCursor = Math.max(0, this.listCursor - 1);
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
if (matchesKey(data, Key.down)) {
|
|
3694
|
+
this.listCursor = Math.min(Math.max(0, flows.length - 1), this.listCursor + 1);
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
if (matchesKey(data, Key.ctrl("e"))) {
|
|
3698
|
+
const flow = flows[this.listCursor];
|
|
3699
|
+
if (flow !== void 0 && flow.configured && flow.key.startsWith("llm-pi-ai/")) this.actions.openProviderEditor(flow.key.slice(10));
|
|
3309
3700
|
return;
|
|
3310
3701
|
}
|
|
3311
|
-
if (matchesKey(data, Key.enter))
|
|
3702
|
+
if (matchesKey(data, Key.enter)) {
|
|
3703
|
+
const flow = flows[this.listCursor];
|
|
3704
|
+
if (flow === void 0) return;
|
|
3705
|
+
if (flow.configured && flow.key.startsWith("llm-pi-ai/")) this.actions.openProviderEditor(flow.key.slice(10));
|
|
3706
|
+
else this.actions.beginAuthorization(flow.key, this.authType);
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
const next = miniTextFieldInput(this.searchQuery, data);
|
|
3710
|
+
if (next !== void 0) {
|
|
3711
|
+
this.searchQuery = next;
|
|
3712
|
+
this.listCursor = 0;
|
|
3713
|
+
}
|
|
3312
3714
|
}
|
|
3313
3715
|
};
|
|
3314
3716
|
//#endregion
|
|
@@ -4593,8 +4995,8 @@ var TuiApp = class {
|
|
|
4593
4995
|
const state = store.getSnapshot();
|
|
4594
4996
|
return buildStatusBarText({
|
|
4595
4997
|
sessionId: options.sessionId,
|
|
4596
|
-
provider: options.provider,
|
|
4597
|
-
model: options.model,
|
|
4998
|
+
provider: state.activeModel?.provider ?? options.provider,
|
|
4999
|
+
model: state.activeModel?.model ?? options.model,
|
|
4598
5000
|
status: state.status,
|
|
4599
5001
|
queuedCount: state.queued.length,
|
|
4600
5002
|
presetLabel: state.preset?.current,
|
|
@@ -4622,29 +5024,36 @@ var TuiApp = class {
|
|
|
4622
5024
|
updateHintText,
|
|
4623
5025
|
statsLineText
|
|
4624
5026
|
], { gap: 0 });
|
|
4625
|
-
const
|
|
5027
|
+
const headerMark = new DynamicText(() => ACRYL_MARK_ROWS.map((row) => fg(theme.primary)(row)).join("\n"));
|
|
5028
|
+
const header = new HStack([{
|
|
5029
|
+
component: this.pet,
|
|
5030
|
+
basis: 34,
|
|
5031
|
+
shrink: 0
|
|
5032
|
+
}, {
|
|
5033
|
+
component: headerMark,
|
|
5034
|
+
basis: "auto",
|
|
5035
|
+
grow: 1
|
|
5036
|
+
}], {
|
|
5037
|
+
gap: 1,
|
|
5038
|
+
align: "start"
|
|
5039
|
+
});
|
|
5040
|
+
const buildStamp = `build bf7e1ab · ${(/* @__PURE__ */ new Date(BUILD_TIME)).toLocaleTimeString()}`;
|
|
5041
|
+
const sessionInfo = new DynamicText(() => {
|
|
4626
5042
|
const { provider, model, cwd } = options;
|
|
4627
5043
|
return [
|
|
4628
|
-
fg(theme.primary)("ACRYL"),
|
|
4629
|
-
"",
|
|
4630
5044
|
`${provider}/${model}`,
|
|
4631
|
-
cwd
|
|
5045
|
+
cwd,
|
|
5046
|
+
fg(theme.muted)(buildStamp)
|
|
4632
5047
|
].join("\n");
|
|
4633
5048
|
});
|
|
4634
5049
|
const layoutRoot = new VStack([
|
|
4635
5050
|
{
|
|
4636
|
-
component:
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
basis: "auto",
|
|
4643
|
-
grow: 1
|
|
4644
|
-
}], {
|
|
4645
|
-
gap: 1,
|
|
4646
|
-
align: "start"
|
|
4647
|
-
}),
|
|
5051
|
+
component: header,
|
|
5052
|
+
basis: "auto",
|
|
5053
|
+
shrink: 0
|
|
5054
|
+
},
|
|
5055
|
+
{
|
|
5056
|
+
component: sessionInfo,
|
|
4648
5057
|
basis: "auto",
|
|
4649
5058
|
shrink: 0
|
|
4650
5059
|
},
|
|
@@ -4743,8 +5152,8 @@ var TuiApp = class {
|
|
|
4743
5152
|
const { store, actions, getTool, getToolCall } = this.options;
|
|
4744
5153
|
switch (overlay.kind) {
|
|
4745
5154
|
case "none": return;
|
|
4746
|
-
case "modelProfile": return new ModelProfileOverlay(store, actions);
|
|
4747
|
-
case "login": return new LoginOverlay(store, actions);
|
|
5155
|
+
case "modelProfile": return new ModelProfileOverlay(this.tui, store, actions);
|
|
5156
|
+
case "login": return new LoginOverlay(this.tui, store, actions);
|
|
4748
5157
|
case "trajectory": return new TrajectoryOverlay(this.tui, store, actions, getTool);
|
|
4749
5158
|
case "toolCards": return new ToolCardsOverlay(this.tui, store, actions, getTool, getToolCall);
|
|
4750
5159
|
case "context": return new ContextOverlay(store, actions);
|
|
@@ -4868,14 +5277,6 @@ async function walkDirectory(cwd) {
|
|
|
4868
5277
|
return results;
|
|
4869
5278
|
}
|
|
4870
5279
|
//#endregion
|
|
4871
|
-
//#region src/tui/auth-guidance.ts
|
|
4872
|
-
function logoutSuccessMessage(provider) {
|
|
4873
|
-
return `Removed stored authentication for ${provider.trim() === "" ? "provider" : provider}.`;
|
|
4874
|
-
}
|
|
4875
|
-
function logoutNoneMessage() {
|
|
4876
|
-
return "No active provider is selected; nothing to log out.";
|
|
4877
|
-
}
|
|
4878
|
-
//#endregion
|
|
4879
5280
|
//#region src/tui-app/session.ts
|
|
4880
5281
|
/**
|
|
4881
5282
|
* ACRYL terminal host adapter: brings up one normal local runtime, opens or
|
|
@@ -4928,10 +5329,33 @@ function getAtPath(value, path) {
|
|
|
4928
5329
|
}
|
|
4929
5330
|
return current;
|
|
4930
5331
|
}
|
|
4931
|
-
/**
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
5332
|
+
/**
|
|
5333
|
+
* `dsh-llm-pi-ai`'s own credential record scope (`credentialKey('llm-pi-ai', providerId)`,
|
|
5334
|
+
* i.e. `"llm-pi-ai/<providerId>"`). A `/login` sign-in (OAuth or API key) writes here through
|
|
5335
|
+
* pi-ai's own `CredentialStore`, entirely separate from the `apiKeyEnv` reference a manually
|
|
5336
|
+
* configured provider's settings profile points at — so a provider's "has credentials" status
|
|
5337
|
+
* has to check both places, and this is the address for the first.
|
|
5338
|
+
*/
|
|
5339
|
+
function piAiRecordKey(providerId) {
|
|
5340
|
+
return `llm-pi-ai/${providerId}`;
|
|
5341
|
+
}
|
|
5342
|
+
/**
|
|
5343
|
+
* Wait out `ctx.settings`' asynchronous watcher queue after a write.
|
|
5344
|
+
*
|
|
5345
|
+
* `settingsSvc.update()`'s returned promise resolves once the write commits
|
|
5346
|
+
* to the settings document — but `dsh-llm-pi-ai`'s own reactive watcher (the
|
|
5347
|
+
* thing that actually rebuilds its live `Models` registry and re-registers
|
|
5348
|
+
* routes with `ctx.llm`) runs on a *separate* promise chained off that
|
|
5349
|
+
* commit, not before it settles. A caller that reads `ctx.llm.listModels()`
|
|
5350
|
+
* or `listProviders()` immediately after `update()` resolves races that
|
|
5351
|
+
* watcher and sees the pre-write state. `setImmediate` runs after the
|
|
5352
|
+
* Node microtask queue drains, which is exactly where that chained watcher
|
|
5353
|
+
* promise settles, so awaiting one flush is enough to see the write reflected.
|
|
5354
|
+
*/
|
|
5355
|
+
function flushSettingsWatchers() {
|
|
5356
|
+
return new Promise((resolve) => {
|
|
5357
|
+
setImmediate(resolve);
|
|
5358
|
+
});
|
|
4935
5359
|
}
|
|
4936
5360
|
/** Open a URL in the platform's default browser (fire-and-forget). */
|
|
4937
5361
|
function openBrowser(url) {
|
|
@@ -4996,18 +5420,11 @@ async function attachSession(host, resumeId) {
|
|
|
4996
5420
|
const agent = host.ctx.agents?.get?.(SessionId(id));
|
|
4997
5421
|
const session = agent?.session;
|
|
4998
5422
|
const history = [];
|
|
4999
|
-
async function
|
|
5423
|
+
async function computeProviderRows() {
|
|
5000
5424
|
const settingsSvc = host.ctx.get("settings");
|
|
5001
5425
|
const credentialsSvc = host.ctx.get("credentials");
|
|
5002
5426
|
const llmSvc = host.ctx.get("llm");
|
|
5003
|
-
if (settingsSvc === void 0 || credentialsSvc === void 0 || llmSvc === void 0)
|
|
5004
|
-
store.updateModelProfile({
|
|
5005
|
-
providers: [],
|
|
5006
|
-
busy: false,
|
|
5007
|
-
error: "Model provider settings are not available in this profile."
|
|
5008
|
-
});
|
|
5009
|
-
return;
|
|
5010
|
-
}
|
|
5427
|
+
if (settingsSvc === void 0 || credentialsSvc === void 0 || llmSvc === void 0) return void 0;
|
|
5011
5428
|
const configurable = llmSvc.listConfigurableProviders();
|
|
5012
5429
|
const live = new Set(llmSvc.listProviders().map((provider) => provider.id));
|
|
5013
5430
|
const descriptors = settingsSvc.describe({ redactSecrets: true });
|
|
@@ -5019,21 +5436,38 @@ async function attachSession(host, resumeId) {
|
|
|
5019
5436
|
const userValue = descriptor === void 0 ? void 0 : getAtPath(descriptor.user, entry.settingsPath);
|
|
5020
5437
|
const apiKeyRef = value?.apiKeyEnv ?? deriveApiKeyRef(entry.provider);
|
|
5021
5438
|
const info = await credentialsSvc.describe(apiKeyRef);
|
|
5439
|
+
const loginRecord = await credentialsSvc.readRecord?.(piAiRecordKey(entry.provider));
|
|
5440
|
+
const authMethod = loginRecord !== void 0 ? loginRecord.kind === "grant" ? "oauth" : "api-key" : info.configured ? "api-key" : void 0;
|
|
5441
|
+
const isLive = live.has(entry.provider);
|
|
5442
|
+
const models = isLive ? await llmSvc.listModels(entry.provider).catch(() => value?.models ?? []) : value?.models ?? [];
|
|
5022
5443
|
rows.push({
|
|
5023
5444
|
route: entry.provider,
|
|
5024
5445
|
displayName: value?.displayName ?? entry.displayName,
|
|
5025
5446
|
settingsNs: entry.settingsNs,
|
|
5026
5447
|
settingsPath: entry.settingsPath,
|
|
5027
5448
|
configured: userValue !== void 0,
|
|
5028
|
-
live:
|
|
5449
|
+
live: isLive,
|
|
5029
5450
|
api: value?.api,
|
|
5030
5451
|
baseURL: value?.baseURL,
|
|
5031
5452
|
apiKeyRef,
|
|
5032
|
-
apiKeyConfigured: info.configured,
|
|
5033
|
-
|
|
5453
|
+
apiKeyConfigured: info.configured || loginRecord !== void 0,
|
|
5454
|
+
authMethod,
|
|
5455
|
+
models,
|
|
5034
5456
|
revision: descriptor?.revision
|
|
5035
5457
|
});
|
|
5036
5458
|
}
|
|
5459
|
+
return rows;
|
|
5460
|
+
}
|
|
5461
|
+
async function loadProviders() {
|
|
5462
|
+
const rows = await computeProviderRows();
|
|
5463
|
+
if (rows === void 0) {
|
|
5464
|
+
store.updateModelProfile({
|
|
5465
|
+
providers: [],
|
|
5466
|
+
busy: false,
|
|
5467
|
+
error: "Model provider settings are not available in this profile."
|
|
5468
|
+
});
|
|
5469
|
+
return;
|
|
5470
|
+
}
|
|
5037
5471
|
store.updateModelProfile({
|
|
5038
5472
|
providers: rows,
|
|
5039
5473
|
busy: false,
|
|
@@ -5041,8 +5475,69 @@ async function attachSession(host, resumeId) {
|
|
|
5041
5475
|
selected: 0
|
|
5042
5476
|
});
|
|
5043
5477
|
}
|
|
5478
|
+
function openEditFormForRow(row) {
|
|
5479
|
+
const overlay = store.getSnapshot().overlay;
|
|
5480
|
+
if (overlay.kind !== "modelProfile") return;
|
|
5481
|
+
const draft = {
|
|
5482
|
+
route: row.route,
|
|
5483
|
+
isNew: false,
|
|
5484
|
+
settingsNs: row.settingsNs,
|
|
5485
|
+
settingsPath: row.settingsPath,
|
|
5486
|
+
displayName: row.displayName,
|
|
5487
|
+
api: row.api ?? "",
|
|
5488
|
+
baseURL: row.baseURL ?? "",
|
|
5489
|
+
apiKeyRef: row.apiKeyRef,
|
|
5490
|
+
apiKeyConfigured: row.apiKeyConfigured,
|
|
5491
|
+
authMethod: row.authMethod,
|
|
5492
|
+
apiKeyDraft: "",
|
|
5493
|
+
apiKeyPreview: void 0,
|
|
5494
|
+
models: row.models,
|
|
5495
|
+
revision: row.revision
|
|
5496
|
+
};
|
|
5497
|
+
const formKey = overlay.modelProfile.formKey + 1;
|
|
5498
|
+
store.updateModelProfile({
|
|
5499
|
+
view: "form",
|
|
5500
|
+
draft,
|
|
5501
|
+
formKey
|
|
5502
|
+
});
|
|
5503
|
+
if (row.authMethod === "api-key") (async () => {
|
|
5504
|
+
const resolved = await host.ctx.get("credentials")?.resolve?.(row.apiKeyRef).catch(() => void 0);
|
|
5505
|
+
if (resolved?.value === void 0) return;
|
|
5506
|
+
const current = store.getSnapshot().overlay;
|
|
5507
|
+
if (current.kind !== "modelProfile" || current.modelProfile.formKey !== formKey) return;
|
|
5508
|
+
const currentDraft = current.modelProfile.draft;
|
|
5509
|
+
if (currentDraft === void 0) return;
|
|
5510
|
+
store.updateModelProfile({ draft: {
|
|
5511
|
+
...currentDraft,
|
|
5512
|
+
apiKeyPreview: maskKeyPreview(resolved.value)
|
|
5513
|
+
} });
|
|
5514
|
+
})();
|
|
5515
|
+
}
|
|
5516
|
+
async function ensureProviderActivated(providerId, method) {
|
|
5517
|
+
const settingsSvc = host.ctx.get("settings");
|
|
5518
|
+
const llmSvc = host.ctx.get("llm");
|
|
5519
|
+
if (settingsSvc === void 0 || llmSvc === void 0) return false;
|
|
5520
|
+
const entry = llmSvc.listConfigurableProviders().find((candidate) => candidate.provider === providerId);
|
|
5521
|
+
if (entry === void 0) return false;
|
|
5522
|
+
const descriptor = settingsSvc.describe({ redactSecrets: true }).find((candidate) => candidate.ns === entry.settingsNs);
|
|
5523
|
+
const existing = descriptor === void 0 ? void 0 : getAtPath(descriptor.user, entry.settingsPath);
|
|
5524
|
+
const oauthName = `${entry.displayName.replace(/(?:-oauth)+$/, "")}-oauth`;
|
|
5525
|
+
if (existing !== void 0) {
|
|
5526
|
+
if (method === "oauth" && existing.displayName !== oauthName) {
|
|
5527
|
+
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, { displayName: oauthName }), descriptor?.revision);
|
|
5528
|
+
await flushSettingsWatchers();
|
|
5529
|
+
return true;
|
|
5530
|
+
}
|
|
5531
|
+
return false;
|
|
5532
|
+
}
|
|
5533
|
+
const section = method === "oauth" ? { displayName: oauthName } : {};
|
|
5534
|
+
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, section), descriptor?.revision);
|
|
5535
|
+
await flushSettingsWatchers();
|
|
5536
|
+
return true;
|
|
5537
|
+
}
|
|
5044
5538
|
async function loadAuthorizationFlows() {
|
|
5045
5539
|
const authSvc = host.ctx.get("authorization");
|
|
5540
|
+
const credentialsSvc = host.ctx.get("credentials");
|
|
5046
5541
|
if (authSvc === void 0) {
|
|
5047
5542
|
store.updateLogin({
|
|
5048
5543
|
flows: [],
|
|
@@ -5052,12 +5547,28 @@ async function attachSession(host, resumeId) {
|
|
|
5052
5547
|
return;
|
|
5053
5548
|
}
|
|
5054
5549
|
try {
|
|
5055
|
-
const
|
|
5550
|
+
const entries = authSvc.list();
|
|
5551
|
+
const list = await Promise.all(entries.map(async (entry) => {
|
|
5552
|
+
const record = credentialsSvc === void 0 ? void 0 : await credentialsSvc.readRecord?.(entry.key);
|
|
5553
|
+
return {
|
|
5554
|
+
...entry,
|
|
5555
|
+
configured: record !== void 0,
|
|
5556
|
+
authMethod: record === void 0 ? void 0 : record.kind === "grant" ? "oauth" : "api-key"
|
|
5557
|
+
};
|
|
5558
|
+
}));
|
|
5056
5559
|
store.updateLogin({
|
|
5057
5560
|
flows: list,
|
|
5058
5561
|
busy: false,
|
|
5059
5562
|
error: void 0
|
|
5060
5563
|
});
|
|
5564
|
+
const toRepair = list.filter((flow) => flow.configured && flow.authMethod === "oauth" && flow.key.startsWith("llm-pi-ai/"));
|
|
5565
|
+
if (toRepair.length > 0) (async () => {
|
|
5566
|
+
let repaired = false;
|
|
5567
|
+
for (const flow of toRepair) try {
|
|
5568
|
+
if (await ensureProviderActivated(flow.key.slice(10), "oauth")) repaired = true;
|
|
5569
|
+
} catch {}
|
|
5570
|
+
if (repaired) refreshCredentialState();
|
|
5571
|
+
})();
|
|
5061
5572
|
} catch (error) {
|
|
5062
5573
|
store.updateLogin({
|
|
5063
5574
|
busy: false,
|
|
@@ -5065,6 +5576,41 @@ async function attachSession(host, resumeId) {
|
|
|
5065
5576
|
});
|
|
5066
5577
|
}
|
|
5067
5578
|
}
|
|
5579
|
+
function refreshCredentialState() {
|
|
5580
|
+
loadProviders();
|
|
5581
|
+
loadAuthorizationFlows();
|
|
5582
|
+
}
|
|
5583
|
+
/** 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). */
|
|
5584
|
+
function openCustomProviderDraft() {
|
|
5585
|
+
const settingsNs = host.ctx.get("llm")?.listConfigurableProviders?.()[0]?.settingsNs;
|
|
5586
|
+
if (settingsNs === void 0) {
|
|
5587
|
+
store.setNotice("Adding a custom provider is not available in this profile.");
|
|
5588
|
+
return;
|
|
5589
|
+
}
|
|
5590
|
+
const draft = {
|
|
5591
|
+
route: "",
|
|
5592
|
+
isNew: true,
|
|
5593
|
+
settingsNs,
|
|
5594
|
+
settingsPath: [],
|
|
5595
|
+
displayName: "",
|
|
5596
|
+
api: "",
|
|
5597
|
+
baseURL: "",
|
|
5598
|
+
apiKeyRef: "",
|
|
5599
|
+
apiKeyConfigured: false,
|
|
5600
|
+
authMethod: void 0,
|
|
5601
|
+
apiKeyDraft: "",
|
|
5602
|
+
apiKeyPreview: void 0,
|
|
5603
|
+
models: [],
|
|
5604
|
+
revision: void 0
|
|
5605
|
+
};
|
|
5606
|
+
const overlay = store.getSnapshot().overlay;
|
|
5607
|
+
const formKey = overlay.kind === "modelProfile" ? overlay.modelProfile.formKey + 1 : 1;
|
|
5608
|
+
store.updateModelProfile({
|
|
5609
|
+
view: "form",
|
|
5610
|
+
draft,
|
|
5611
|
+
formKey
|
|
5612
|
+
});
|
|
5613
|
+
}
|
|
5068
5614
|
async function loadAgentPresets() {
|
|
5069
5615
|
const presetsSvc = host.ctx.get("agentPresets");
|
|
5070
5616
|
if (presetsSvc === void 0) {
|
|
@@ -5107,7 +5653,9 @@ async function attachSession(host, resumeId) {
|
|
|
5107
5653
|
});
|
|
5108
5654
|
},
|
|
5109
5655
|
cancel() {
|
|
5110
|
-
bridge.cancel(id).catch(() => {
|
|
5656
|
+
bridge.cancel(id).catch((error) => {
|
|
5657
|
+
store.setNotice(`Could not cancel: ${error instanceof Error ? error.message : String(error)}`);
|
|
5658
|
+
});
|
|
5111
5659
|
},
|
|
5112
5660
|
shutdown() {
|
|
5113
5661
|
signal("exit");
|
|
@@ -5220,12 +5768,10 @@ async function attachSession(host, resumeId) {
|
|
|
5220
5768
|
closeLogin() {
|
|
5221
5769
|
store.closeOverlay();
|
|
5222
5770
|
},
|
|
5223
|
-
|
|
5224
|
-
store.updateLogin({ selected: index });
|
|
5225
|
-
},
|
|
5226
|
-
beginAuthorization(key) {
|
|
5771
|
+
beginAuthorization(key, method) {
|
|
5227
5772
|
(async () => {
|
|
5228
5773
|
const authSvc = host.ctx.get("authorization");
|
|
5774
|
+
const credentialsSvc = host.ctx.get("credentials");
|
|
5229
5775
|
if (authSvc === void 0) {
|
|
5230
5776
|
store.setNotice("Sign-in is not available in this profile.");
|
|
5231
5777
|
return;
|
|
@@ -5233,6 +5779,8 @@ async function attachSession(host, resumeId) {
|
|
|
5233
5779
|
const overlay = store.getSnapshot().overlay;
|
|
5234
5780
|
const flow = overlay.kind === "login" ? overlay.login.flows?.find((entry) => entry.key === key) : void 0;
|
|
5235
5781
|
if (flow === void 0) return;
|
|
5782
|
+
const existingRecord = (method ?? flow.methods[0]?.id) === "api-key" ? await credentialsSvc?.readRecord?.(flow.key) : void 0;
|
|
5783
|
+
const storedApiKeyPreview = typeof existingRecord?.key === "string" ? maskKeyPreview(existingRecord.key) : void 0;
|
|
5236
5784
|
store.updateLogin({ signingIn: key });
|
|
5237
5785
|
const interaction = {
|
|
5238
5786
|
notify(notice) {
|
|
@@ -5257,8 +5805,13 @@ async function attachSession(host, resumeId) {
|
|
|
5257
5805
|
label: option.label,
|
|
5258
5806
|
description: option.description
|
|
5259
5807
|
}))
|
|
5808
|
+
} : prompt.kind === "secret" ? {
|
|
5809
|
+
kind: "secret",
|
|
5810
|
+
message: prompt.message,
|
|
5811
|
+
placeholder: prompt.placeholder,
|
|
5812
|
+
storedApiKeyPreview
|
|
5260
5813
|
} : {
|
|
5261
|
-
kind:
|
|
5814
|
+
kind: "text",
|
|
5262
5815
|
message: prompt.message,
|
|
5263
5816
|
placeholder: prompt.placeholder
|
|
5264
5817
|
};
|
|
@@ -5274,13 +5827,15 @@ async function attachSession(host, resumeId) {
|
|
|
5274
5827
|
try {
|
|
5275
5828
|
if ((await authSvc.begin({
|
|
5276
5829
|
key,
|
|
5830
|
+
method,
|
|
5277
5831
|
interaction
|
|
5278
5832
|
})).status === "authorized") {
|
|
5279
|
-
store.setNotice(`Signed in to ${flow.label}.`);
|
|
5280
|
-
|
|
5281
|
-
|
|
5833
|
+
store.setNotice(`Signed in to ${flow.label} — credentials saved, ready to use from /model.`);
|
|
5834
|
+
if (key.startsWith("llm-pi-ai/")) await ensureProviderActivated(key.slice(10), method);
|
|
5835
|
+
refreshCredentialState();
|
|
5836
|
+
} else store.updateLogin({ error: "Sign-in cancelled." });
|
|
5282
5837
|
} catch (error) {
|
|
5283
|
-
store.
|
|
5838
|
+
store.updateLogin({ error: `Sign-in failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
5284
5839
|
} finally {
|
|
5285
5840
|
store.updateLogin({ signingIn: void 0 });
|
|
5286
5841
|
}
|
|
@@ -5293,28 +5848,9 @@ async function attachSession(host, resumeId) {
|
|
|
5293
5848
|
if (resolve !== void 0) resolve(value);
|
|
5294
5849
|
},
|
|
5295
5850
|
logout() {
|
|
5296
|
-
(
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
store.setNotice("Credentials are not available in this profile.");
|
|
5300
|
-
return;
|
|
5301
|
-
}
|
|
5302
|
-
const selection = host.ctx.get("agentDefaultModel")?.currentSelection?.();
|
|
5303
|
-
const overlay = store.getSnapshot().overlay;
|
|
5304
|
-
const rows = overlay.kind === "modelProfile" ? overlay.modelProfile.providers : void 0;
|
|
5305
|
-
const row = rows?.find((entry) => entry.route === selection?.provider) ?? (rows !== void 0 && rows.length === 1 ? rows[0] : void 0);
|
|
5306
|
-
if (row === void 0) {
|
|
5307
|
-
store.setNotice(logoutNoneMessage());
|
|
5308
|
-
return;
|
|
5309
|
-
}
|
|
5310
|
-
try {
|
|
5311
|
-
await credentialsSvc.unset(row.apiKeyRef);
|
|
5312
|
-
store.setNotice(logoutSuccessMessage(row.displayName));
|
|
5313
|
-
loadProviders();
|
|
5314
|
-
} catch (error) {
|
|
5315
|
-
store.setNotice(`logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5316
|
-
}
|
|
5317
|
-
})();
|
|
5851
|
+
store.openModelProfile();
|
|
5852
|
+
store.setNotice("Select a provider, then Ctrl+D to sign out.");
|
|
5853
|
+
loadProviders();
|
|
5318
5854
|
},
|
|
5319
5855
|
openTrajectory() {
|
|
5320
5856
|
store.openTrajectory();
|
|
@@ -5342,40 +5878,40 @@ async function attachSession(host, resumeId) {
|
|
|
5342
5878
|
},
|
|
5343
5879
|
backToProviderList() {
|
|
5344
5880
|
store.updateModelProfile({ view: "list" });
|
|
5345
|
-
|
|
5346
|
-
selectProvider(index) {
|
|
5347
|
-
store.updateModelProfile({ selected: index });
|
|
5881
|
+
refreshCredentialState();
|
|
5348
5882
|
},
|
|
5349
5883
|
createProvider() {
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5884
|
+
openCustomProviderDraft();
|
|
5885
|
+
},
|
|
5886
|
+
addCustomProvider() {
|
|
5887
|
+
store.openModelProfile();
|
|
5888
|
+
loadProviders();
|
|
5889
|
+
openCustomProviderDraft();
|
|
5354
5890
|
},
|
|
5355
5891
|
editProvider(route) {
|
|
5356
5892
|
const overlay = store.getSnapshot().overlay;
|
|
5357
5893
|
if (overlay.kind !== "modelProfile") return;
|
|
5358
5894
|
const row = overlay.modelProfile.providers?.find((entry) => entry.route === route);
|
|
5359
5895
|
if (row === void 0) return;
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
});
|
|
5896
|
+
openEditFormForRow(row);
|
|
5897
|
+
},
|
|
5898
|
+
openProviderEditor(route) {
|
|
5899
|
+
(async () => {
|
|
5900
|
+
store.openModelProfile();
|
|
5901
|
+
const rows = await computeProviderRows();
|
|
5902
|
+
store.updateModelProfile({
|
|
5903
|
+
providers: rows ?? [],
|
|
5904
|
+
busy: false,
|
|
5905
|
+
error: rows === void 0 ? "Model provider settings are not available in this profile." : void 0,
|
|
5906
|
+
selected: 0
|
|
5907
|
+
});
|
|
5908
|
+
const row = rows?.find((entry) => entry.route === route);
|
|
5909
|
+
if (row === void 0) {
|
|
5910
|
+
store.setNotice(`Provider "${route}" not found.`);
|
|
5911
|
+
return;
|
|
5912
|
+
}
|
|
5913
|
+
openEditFormForRow(row);
|
|
5914
|
+
})();
|
|
5379
5915
|
},
|
|
5380
5916
|
saveProvider(draft) {
|
|
5381
5917
|
(async () => {
|
|
@@ -5385,20 +5921,25 @@ async function attachSession(host, resumeId) {
|
|
|
5385
5921
|
store.setNotice("Provider settings are not available in this profile.");
|
|
5386
5922
|
return;
|
|
5387
5923
|
}
|
|
5924
|
+
if (draft.isNew && draft.route.trim() === "") {
|
|
5925
|
+
store.updateModelProfile({ error: "Provider ID is required." });
|
|
5926
|
+
return;
|
|
5927
|
+
}
|
|
5388
5928
|
try {
|
|
5389
5929
|
const key = draft.apiKeyDraft.trim();
|
|
5390
5930
|
if (key !== "") await credentialsSvc.set(draft.apiKeyRef, key);
|
|
5391
5931
|
const section = {
|
|
5392
|
-
displayName: draft.displayName,
|
|
5393
|
-
api: draft.api,
|
|
5394
|
-
baseURL: draft.baseURL,
|
|
5932
|
+
...draft.displayName.trim() === "" ? {} : { displayName: draft.displayName },
|
|
5933
|
+
...draft.api.trim() === "" ? {} : { api: draft.api },
|
|
5934
|
+
...draft.baseURL.trim() === "" ? {} : { baseURL: draft.baseURL },
|
|
5395
5935
|
apiKeyEnv: draft.apiKeyRef,
|
|
5396
|
-
models: draft.models
|
|
5936
|
+
...draft.models.length === 0 ? {} : { models: draft.models }
|
|
5397
5937
|
};
|
|
5398
5938
|
await settingsSvc.update(draft.settingsNs, nestAtPath(draft.settingsPath, section), draft.revision);
|
|
5399
|
-
|
|
5939
|
+
await flushSettingsWatchers();
|
|
5940
|
+
store.setNotice(`Saved ${draft.displayName || draft.route} — credentials stored, ready to use from /model.`);
|
|
5400
5941
|
store.updateModelProfile({ view: "list" });
|
|
5401
|
-
|
|
5942
|
+
refreshCredentialState();
|
|
5402
5943
|
} catch (error) {
|
|
5403
5944
|
store.setNotice(`save failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5404
5945
|
}
|
|
@@ -5415,15 +5956,85 @@ async function attachSession(host, resumeId) {
|
|
|
5415
5956
|
try {
|
|
5416
5957
|
await credentialsSvc.unset(row.apiKeyRef);
|
|
5417
5958
|
await settingsSvc.update(row.settingsNs, nestAtPath(row.settingsPath, {}), row.revision);
|
|
5959
|
+
await flushSettingsWatchers();
|
|
5418
5960
|
store.setNotice(`Removed ${row.displayName}.`);
|
|
5419
|
-
|
|
5961
|
+
refreshCredentialState();
|
|
5420
5962
|
} catch (error) {
|
|
5421
5963
|
store.setNotice(`delete failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5422
5964
|
}
|
|
5423
5965
|
})();
|
|
5424
5966
|
},
|
|
5425
|
-
|
|
5426
|
-
|
|
5967
|
+
clearApiKey(draft) {
|
|
5968
|
+
(async () => {
|
|
5969
|
+
const credentialsSvc = host.ctx.get("credentials");
|
|
5970
|
+
if (credentialsSvc === void 0) {
|
|
5971
|
+
store.setNotice("Credentials are not available in this profile.");
|
|
5972
|
+
return;
|
|
5973
|
+
}
|
|
5974
|
+
try {
|
|
5975
|
+
await credentialsSvc.unset(draft.apiKeyRef);
|
|
5976
|
+
await credentialsSvc.deleteRecord?.(piAiRecordKey(draft.route));
|
|
5977
|
+
store.setNotice(`Removed the API key for ${draft.displayName || draft.route}.`);
|
|
5978
|
+
store.updateModelProfile({ view: "list" });
|
|
5979
|
+
refreshCredentialState();
|
|
5980
|
+
} catch (error) {
|
|
5981
|
+
store.setNotice(`Could not remove the key: ${error instanceof Error ? error.message : String(error)}`);
|
|
5982
|
+
}
|
|
5983
|
+
})();
|
|
5984
|
+
},
|
|
5985
|
+
discoverModelsForDraft(draft) {
|
|
5986
|
+
(async () => {
|
|
5987
|
+
const llmSvc = host.ctx.get("llm");
|
|
5988
|
+
if (llmSvc === void 0) {
|
|
5989
|
+
store.setNotice("Model discovery is not available in this profile.");
|
|
5990
|
+
return;
|
|
5991
|
+
}
|
|
5992
|
+
store.updateModelProfile({ busy: true });
|
|
5993
|
+
try {
|
|
5994
|
+
const request = {};
|
|
5995
|
+
if (!draft.isNew) request.provider = draft.route;
|
|
5996
|
+
if (draft.baseURL.trim() !== "") request.baseURL = draft.baseURL.trim();
|
|
5997
|
+
if (draft.api.trim() !== "") request.api = draft.api.trim();
|
|
5998
|
+
if (draft.apiKeyDraft.trim() !== "") request.apiKey = draft.apiKeyDraft.trim();
|
|
5999
|
+
const discovered = await llmSvc.discoverModels(draft.settingsNs, request);
|
|
6000
|
+
store.updateModelProfile({
|
|
6001
|
+
busy: false,
|
|
6002
|
+
discovered
|
|
6003
|
+
});
|
|
6004
|
+
} catch (error) {
|
|
6005
|
+
store.updateModelProfile({
|
|
6006
|
+
busy: false,
|
|
6007
|
+
discovered: []
|
|
6008
|
+
});
|
|
6009
|
+
store.setNotice(`Model discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6010
|
+
}
|
|
6011
|
+
})();
|
|
6012
|
+
},
|
|
6013
|
+
setActiveModel(provider, model) {
|
|
6014
|
+
(async () => {
|
|
6015
|
+
try {
|
|
6016
|
+
await bridge.selectModel({
|
|
6017
|
+
sessionId: id,
|
|
6018
|
+
provider,
|
|
6019
|
+
model
|
|
6020
|
+
});
|
|
6021
|
+
store.setActiveModel({
|
|
6022
|
+
provider,
|
|
6023
|
+
model
|
|
6024
|
+
});
|
|
6025
|
+
try {
|
|
6026
|
+
await host.ctx.get("agentDefaultModel")?.saveSelection({
|
|
6027
|
+
provider,
|
|
6028
|
+
model
|
|
6029
|
+
});
|
|
6030
|
+
} catch {}
|
|
6031
|
+
store.setNotice(`Active model set to ${model} (${provider}).`);
|
|
6032
|
+
loadProviders();
|
|
6033
|
+
} catch (error) {
|
|
6034
|
+
store.setNotice(`Could not set active model: ${error instanceof Error ? error.message : String(error)}`);
|
|
6035
|
+
}
|
|
6036
|
+
})();
|
|
6037
|
+
},
|
|
5427
6038
|
closeTrajectory() {
|
|
5428
6039
|
store.closeOverlay();
|
|
5429
6040
|
},
|