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