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