@toddzheng024/dscode-bundle 0.7.2 → 0.7.4

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.
Files changed (36) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/cordis.patch.yml +4 -0
  3. package/package.json +4 -2
  4. package/plugins/auto-review/index.mjs +2 -1
  5. package/plugins/code-review/git.mjs +24 -2
  6. package/plugins/code-review/index.mjs +13 -6
  7. package/plugins/credentials/index.mjs +7 -5
  8. package/plugins/exec/cli.mjs +63 -0
  9. package/plugins/exec/index.mjs +1 -1
  10. package/plugins/memory/index.mjs +17 -12
  11. package/plugins/providers/catalog.mjs +134 -0
  12. package/plugins/session-cards/index.mjs +11 -8
  13. package/plugins/session-cards/manager.mjs +1 -1
  14. package/plugins/session-metrics/attribution.mjs +19 -0
  15. package/plugins/session-metrics/balance.mjs +41 -22
  16. package/plugins/session-metrics/index.mjs +26 -14
  17. package/plugins/session-metrics/pricing.mjs +24 -4
  18. package/plugins/session-metrics/view.mjs +5 -3
  19. package/plugins/tui-tools/doctor.mjs +9 -6
  20. package/plugins/tui-tools/index.mjs +2 -2
  21. package/plugins/ultra/policy.mjs +16 -0
  22. package/vendor/pi-ai/LICENSE +21 -0
  23. package/vendor/pi-ai/index.js +2702 -0
  24. package/vendor/pi-ai/types/adapter.d.ts +105 -0
  25. package/vendor/pi-ai/types/auth.d.ts +60 -0
  26. package/vendor/pi-ai/types/catalog.d.ts +355 -0
  27. package/vendor/pi-ai/types/config.d.ts +208 -0
  28. package/vendor/pi-ai/types/context.d.ts +42 -0
  29. package/vendor/pi-ai/types/discovery.d.ts +43 -0
  30. package/vendor/pi-ai/types/index.d.ts +69 -0
  31. package/vendor/pi-ai/types/login.d.ts +21 -0
  32. package/vendor/pi-ai/types/provider.d.ts +59 -0
  33. package/vendor/pi-ai/types/replay.d.ts +63 -0
  34. package/vendor/pi-ai/types/stream.d.ts +43 -0
  35. package/vendor/tui/dscode-providers/catalog.mjs +134 -0
  36. package/vendor/tui/index.mjs +146 -59
@@ -0,0 +1,134 @@
1
+ // Model providers `/provider` switches between. DeepSeek's official API is the
2
+ // native `llm-deepseek` route; OpenRouter reaches the same DeepSeek models
3
+ // through pi-ai's catalog route, which the base composition mounts dormant until
4
+ // a `llm-pi-ai:` settings section declares it.
5
+
6
+ export const PROVIDERS = Object.freeze([
7
+ { id: 'deepseek-official', name: 'DeepSeek', aliases: ['deepseek', 'deepseek-official', 'official'], credentialRef: 'DEEPSEEK_API_KEY', defaultModel: 'deepseek-flash' },
8
+ { id: 'openrouter', name: 'OpenRouter', aliases: ['openrouter', 'open-router'], credentialRef: 'OPENROUTER_API_KEY', defaultModel: 'deepseek/deepseek-v4-flash' },
9
+ ]);
10
+
11
+ const PI_AI_NS = 'llm-pi-ai';
12
+
13
+ // OpenRouter serves DeepSeek V4 thinking as none/high/xhigh. DeepSeek itself
14
+ // answers `low` as high and `max` as xhigh, so the route offers the official
15
+ // low/high/max detents (and Ultra on top of max) with the wire spelling OpenRouter
16
+ // accepts; session cards and delegated children that ask for `low` keep working.
17
+ const OPENROUTER_EFFORTS = Object.freeze({ off: 'none', low: 'high', high: 'high', max: 'xhigh' });
18
+
19
+ /** The DeepSeek models the OpenRouter route declares, with their official-route counterparts. */
20
+ export const OPENROUTER_MODELS = Object.freeze([
21
+ { id: 'deepseek/deepseek-v4-flash', name: 'DeepSeek V4 Flash', official: ['deepseek-flash', 'deepseek-v4-flash'] },
22
+ { id: 'deepseek/deepseek-v4-pro', name: 'DeepSeek V4 Pro', official: ['deepseek-v4-pro'] },
23
+ { id: 'deepseek/deepseek-v4-flash-vision-exp', name: 'DeepSeek V4 Flash Vision Exp', official: ['deepseek-v4-flash-vision-exp'] },
24
+ ]);
25
+
26
+ /** The `llm-pi-ai` profile `/provider openrouter` writes: installed-catalog models narrowed to DeepSeek. */
27
+ export function openRouterProfile() {
28
+ return {
29
+ displayName: 'OpenRouter',
30
+ apiKeyEnv: 'OPENROUTER_API_KEY',
31
+ // Like the official route, requests default to high; it also gives /effort the DSCODE detent bar.
32
+ reasoning: 'high',
33
+ models: OPENROUTER_MODELS.map(({ id, name }) => ({ id, name, reasoningEfforts: { ...OPENROUTER_EFFORTS } })),
34
+ };
35
+ }
36
+
37
+ export function providerSpec(id) {
38
+ return PROVIDERS.find(provider => provider.id === id);
39
+ }
40
+
41
+ /**
42
+ * Resolve a `/provider` or `/login` argument.
43
+ * @param raw - text after the command name.
44
+ * @returns a provider id, `undefined` for no argument, or `null` when unrecognized. Callers must not echo the text: it may be a pasted key.
45
+ */
46
+ export function providerArgument(raw) {
47
+ const value = String(raw ?? '').trim().toLowerCase();
48
+ if (value === '') return undefined;
49
+ return PROVIDERS.find(provider => provider.aliases.includes(value))?.id ?? null;
50
+ }
51
+
52
+ /** Split a `provider/model` label at the first slash: OpenRouter model ids carry their own `vendor/` segment. */
53
+ export function splitModelLabel(label) {
54
+ const text = typeof label === 'string' ? label : '';
55
+ const cut = text.indexOf('/');
56
+ return cut > 0 ? { provider: text.slice(0, cut), model: text.slice(cut + 1) } : { provider: '', model: text };
57
+ }
58
+
59
+ /** The switchable provider a label names, defaulting to DeepSeek for unknown or bare labels. */
60
+ export function providerOfLabel(label) {
61
+ return providerSpec(splitModelLabel(label).provider)?.id ?? PROVIDERS[0].id;
62
+ }
63
+
64
+ /** Provider id leading a footer header (`provider: model @ effort`), if any. */
65
+ export function providerOfHeader(header) {
66
+ return typeof header === 'string' ? header.match(/^([^\s:/]+): /)?.[1] : undefined;
67
+ }
68
+
69
+ function counterpart(from, model, to) {
70
+ if (from === to) return model;
71
+ if (from === 'deepseek-official' && to === 'openrouter') return OPENROUTER_MODELS.find(entry => entry.official.includes(model))?.id;
72
+ if (from === 'openrouter' && to === 'deepseek-official') return OPENROUTER_MODELS.find(entry => entry.id === model)?.official[0];
73
+ return undefined;
74
+ }
75
+
76
+ /**
77
+ * The model a provider switch lands on: the current model's counterpart, else the
78
+ * provider default, else its first model. The effort carries over only when the
79
+ * target offers it; otherwise the model's own default applies.
80
+ * @param rows - model directory rows (`provider`, `model`, `reasoning`).
81
+ * @returns `{ row, effort }`, or `undefined` when the provider serves no model yet.
82
+ */
83
+ export function pickModel(rows, provider, currentLabel, effort) {
84
+ const candidates = rows.filter(row => row.provider === provider);
85
+ if (candidates.length === 0) return undefined;
86
+ const current = splitModelLabel(currentLabel);
87
+ const wanted = counterpart(current.provider, current.model, provider);
88
+ const row = candidates.find(candidate => candidate.model === wanted)
89
+ ?? candidates.find(candidate => candidate.model === providerSpec(provider)?.defaultModel)
90
+ ?? candidates[0];
91
+ const offered = row.reasoning?.efforts.map(level => level.id) ?? [];
92
+ return { row, effort: effort && offered.includes(effort) ? effort : undefined };
93
+ }
94
+
95
+ /**
96
+ * Credential status of a provider-settings row.
97
+ * @returns `saved`, `env`, `missing`, `readonly` (an empty read-only source), `error`, or `unavailable` (no row).
98
+ */
99
+ export function credentialState(row) {
100
+ if (!row) return 'unavailable';
101
+ const credential = row.credential;
102
+ if (credential?.kind === 'error') return 'error';
103
+ if (credential?.kind !== 'facts') return 'missing';
104
+ if (credential.configured) return credential.source === 'env' ? 'env' : 'saved';
105
+ return credential.writable ? 'missing' : 'readonly';
106
+ }
107
+
108
+ /**
109
+ * Declare a provider's route before it is used. Only OpenRouter needs one; a
110
+ * profile the user already has (their own models or endpoint) is left alone.
111
+ * @param settings - the host settings service.
112
+ * @returns whether the settings changed.
113
+ */
114
+ export async function ensureProviderRoute(settings, provider) {
115
+ if (provider !== 'openrouter') return false;
116
+ if (typeof settings?.describe !== 'function' || typeof settings.mutate !== 'function') throw new Error('settings are unavailable; OpenRouter cannot be configured in this profile');
117
+ const descriptor = settings.describe({ redactSecrets: true }).find(entry => entry.ns === PI_AI_NS);
118
+ if (!descriptor) throw new Error('the OpenRouter adapter (llm-pi-ai) is not mounted in this profile');
119
+ if (descriptor.value?.providers?.openrouter !== undefined) return false;
120
+ if (settings.writable !== true) throw new Error('settings are read-only; OpenRouter cannot be configured here');
121
+ await settings.mutate(PI_AI_NS, [{ op: 'set', path: ['providers', 'openrouter'], value: openRouterProfile() }], descriptor.revision);
122
+ return true;
123
+ }
124
+
125
+ /** Wait for a freshly declared route to reach the model directory. */
126
+ export async function waitForModels(loadModels, provider, { attempts = 30, delayMs = 100 } = {}) {
127
+ let directory;
128
+ for (let attempt = 0; attempt < attempts; attempt++) {
129
+ directory = await loadModels();
130
+ if (directory.rows.some(row => row.provider === provider)) return directory;
131
+ await new Promise(resolve => setTimeout(resolve, delayMs));
132
+ }
133
+ return directory;
134
+ }
@@ -39,6 +39,8 @@ function dscodeLoadFlag(name, fallback = false) {
39
39
  function dscodeSaveFlag(name, value) {
40
40
  try { fs.mkdirSync(join(homedir(), ".dsh", "dsh-code"), { recursive: true }); fs.writeFileSync(dscodeFlagFile(name), JSON.stringify({ [name]: value }, null, 2) + "\n"); } catch {}
41
41
  }
42
+ // dscode-provider-v1
43
+ import { PROVIDERS as DSCODE_PROVIDERS, providerSpec as dscodeProviderSpec, providerArgument as dscodeProviderArgument, providerOfLabel as dscodeProviderOfLabel, splitModelLabel as dscodeSplitModelLabel, pickModel as dscodePickModel, credentialState as dscodeCredentialState, ensureProviderRoute as dscodeEnsureProviderRoute, waitForModels as dscodeWaitForModels } from "./dscode-providers/catalog.mjs";
42
44
  import { readClipboardImage as dscodeReadClipboardImage } from "./dscode-clipboard-image/index.mjs";
43
45
  // dscode-clipboard-image-v1
44
46
  // dscode-large-paste-v1
@@ -26123,7 +26125,7 @@ function replayProjectEvent(acc, event) {
26123
26125
  acc.streamingReasoning = "";
26124
26126
  acc.streaming = "";
26125
26127
  if (reason.kind === "error") {
26126
- const recovery = reason.error.code === "MISSING_CREDENTIAL" ? " · open /model to add an API key" : "";
26128
+ const recovery = reason.error.code === "MISSING_CREDENTIAL" ? " · run /login to add an API key" : "";
26127
26129
  appended.push({
26128
26130
  kind: "error",
26129
26131
  text: `${reason.error.code}: ${reason.error.message}${recovery}`
@@ -31929,7 +31931,8 @@ const SYNCHRONIZED_UPDATE_END = "\x1B[?2026l";
31929
31931
  /** One source of truth for TUI-owned slash commands in completion and `/help`. */
31930
31932
  const LOCAL_COMMANDS = [
31931
31933
  { label: "/email", description: "browse email and steer into this session" },
31932
- { label: "/login", description: "save a DeepSeek API key locally" },
31934
+ { label: "/login", description: "save a provider API key locally (/login [deepseek|openrouter])" },
31935
+ { label: "/provider", description: "switch between DeepSeek and OpenRouter" },
31933
31936
  // dscode: startup command discovery
31934
31937
  {"label":"/status","description":"session, model, permissions and usage"},
31935
31938
  {"label":"/doctor","description":"read-only runtime diagnostics"},
@@ -32371,7 +32374,7 @@ function Header({ cwd = "", model = "", effort = "", animated = false }) {
32371
32374
  if (!full) return (0, import_react.createElement)(Box, { flexDirection: "column", paddingX: 2, marginBottom: 1 },
32372
32375
  (0, import_react.createElement)(Text, { wrap: "truncate-end" },
32373
32376
  (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright), bold: true }, "❄ DSCODE"),
32374
- (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, " v0.7.2")),
32377
+ (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, " v0.7.4")),
32375
32378
  (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(modelName + " · " + effortName, width)),
32376
32379
  (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim), wrap: "truncate-end" }, welcomePath(cwd, width)));
32377
32380
  return (0, import_react.createElement)(Box, { flexDirection: "column", width, borderStyle: "round", borderColor: inkColor(getPalette().brand), paddingX: 1 },
@@ -32382,7 +32385,7 @@ function Header({ cwd = "", model = "", effort = "", animated = false }) {
32382
32385
  (0, import_react.createElement)(Box, { flexDirection: "column", width: detailsWidth, marginTop: 2 },
32383
32386
  (0, import_react.createElement)(Text, { color: inkColor(getPalette().text), bold: true }, "DSCODE"),
32384
32387
  (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandDeep) }, "────────────"),
32385
- (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, "v0.7.2"),
32388
+ (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, "v0.7.4"),
32386
32389
  (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(dscodePadEnd(dscodeT("welcome.model"), 9) + modelName, detailsWidth)),
32387
32390
  (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(dscodePadEnd(dscodeT("welcome.effort"), 9) + effortName, detailsWidth)),
32388
32391
  (0, import_react.createElement)(Text, null, " "),
@@ -33452,53 +33455,89 @@ function ProviderPanel({ directory, error, authorizations, authorizationError, o
33452
33455
  }, truncateColumns("↑↓ move · enter configure · l login · o logout · d remove key · x remove provider · r retry · esc back", viewport.contentColumns)));
33453
33456
  }
33454
33457
  // dscode-login-v1
33455
- function DscodeLoginPanel({ load, save, done, back }) {
33456
- const [draft, setDraft] = (0, import_react.useState)("");
33457
- const [target, setTarget] = (0, import_react.useState)(void 0);
33458
- const [error, setError] = (0, import_react.useState)("");
33459
- const [busy, setBusy] = (0, import_react.useState)(false);
33460
- const saving = (0, import_react.useRef)(false);
33461
- (0, import_react.useEffect)(() => {
33462
- let active = true;
33463
- Promise.resolve().then(() => load()).then(directory => {
33464
- if (!active) return;
33465
- const row = directory.rows.find(row => row.provider === "deepseek-official");
33466
- if (!row || !save) { setError("DeepSeek credential storage is unavailable."); return; }
33467
- if (row.credential?.kind !== "facts") { setError("Could not read credential status. Check local file permissions."); return; }
33468
- if (!row.credential.writable) { setError("DEEPSEEK_API_KEY is set by your environment. Remove it and restart to use /login."); return; }
33469
- setTarget(row);
33470
- }, () => { if (active) setError("Could not load DeepSeek credential settings."); });
33471
- return () => { active = false; };
33472
- }, [load, save]);
33473
- useStableInput((input, key) => {
33474
- if (saving.current) return;
33475
- if (key.escape || key.ctrl && input === "c") { setDraft(""); back(); return; }
33476
- if (!target) return;
33477
- if (key.return) {
33478
- const raw = draft.trim();
33479
- if (!raw || /[\s\x00-\x1f\x7f-\uffff]/.test(raw) || ENV_ASSIGNMENT.test(raw) || hasWrappingQuotes(raw)) {
33480
- setError("Paste only the API key, without quotes, spaces or an environment-variable name."); return;
33481
- }
33482
- saving.current = true; setBusy(true); setError(""); setDraft("");
33483
- Promise.resolve().then(() => save(target, raw)).then(done, () => {
33484
- saving.current = false; setBusy(false);
33485
- setError("Could not save the API key. Check file permissions and available disk space, then paste again.");
33486
- });
33487
- return;
33488
- }
33489
- if (key.ctrl && input === "u") { setDraft(""); setError(""); return; }
33490
- if (key.backspace || key.delete) { setDraft(current => [...current].slice(0, -1).join("")); return; }
33491
- if (key.ctrl || key.meta || !input) return;
33492
- const pasted = stripPasteMarkers(input);
33493
- setDraft(current => (current + pasted).slice(0, 4096));
33494
- setError("");
33495
- });
33496
- return (0, import_react.createElement)(Box, { flexDirection: "column", paddingX: 2 },
33497
- (0, import_react.createElement)(Text, { bold: true }, "DeepSeek login"),
33498
- (0, import_react.createElement)(Text, { dimColor: true }, "Saved on this Mac in ~/.dscode/credentials.yaml"),
33499
- (0, import_react.createElement)(Text, null, busy ? "Saving…" : target ? "API key › " + (draft ? "••••••••" : "paste your key") : error ? "" : "Loading…"),
33500
- error ? (0, import_react.createElement)(Text, { color: "red" }, error) : void 0,
33501
- (0, import_react.createElement)(Text, { dimColor: true }, "Enter save · Esc cancel · Ctrl+U clear"));
33458
+ function DscodeLoginPanel({ provider, load, save, done, back }) {
33459
+ const spec = dscodeProviderSpec(provider) ?? DSCODE_PROVIDERS[0];
33460
+ const [draft, setDraft] = (0, import_react.useState)("");
33461
+ const [target, setTarget] = (0, import_react.useState)(void 0);
33462
+ const [error, setError] = (0, import_react.useState)("");
33463
+ const [busy, setBusy] = (0, import_react.useState)(false);
33464
+ const saving = (0, import_react.useRef)(false);
33465
+ (0, import_react.useEffect)(() => {
33466
+ let active = true;
33467
+ Promise.resolve().then(() => load()).then(directory => {
33468
+ if (!active) return;
33469
+ const row = directory.rows.find(row => row.provider === spec.id);
33470
+ if (!row || !save) { setError(spec.name + " credential storage is unavailable."); return; }
33471
+ if (row.credential?.kind === "error") { setError("Could not read credential status. Check local file permissions."); return; }
33472
+ if (row.credential?.kind === "facts" && !row.credential.writable) { setError(spec.credentialRef + " is set by your environment. Remove it and restart to use /login."); return; }
33473
+ setTarget(row);
33474
+ }, () => { if (active) setError("Could not load " + spec.name + " credential settings."); });
33475
+ return () => { active = false; };
33476
+ }, [spec.id]);
33477
+ useStableInput((input, key) => {
33478
+ if (saving.current) return;
33479
+ if (key.escape || key.ctrl && input === "c") { setDraft(""); back(); return; }
33480
+ if (!target) return;
33481
+ if (key.return) {
33482
+ const raw = draft.trim();
33483
+ if (!raw || /[\s\x00-\x1f\x7f-￿]/.test(raw) || ENV_ASSIGNMENT.test(raw) || hasWrappingQuotes(raw)) {
33484
+ setError("Paste only the API key, without quotes, spaces or an environment-variable name."); return;
33485
+ }
33486
+ saving.current = true; setBusy(true); setError(""); setDraft("");
33487
+ Promise.resolve().then(() => save(target, raw)).then(done, () => {
33488
+ saving.current = false; setBusy(false);
33489
+ setError("Could not save the API key. Check file permissions and available disk space, then paste again.");
33490
+ });
33491
+ return;
33492
+ }
33493
+ if (key.ctrl && input === "u") { setDraft(""); setError(""); return; }
33494
+ if (key.backspace || key.delete) { setDraft(current => [...current].slice(0, -1).join("")); return; }
33495
+ if (key.ctrl || key.meta || !input) return;
33496
+ const pasted = stripPasteMarkers(input);
33497
+ setDraft(current => (current + pasted).slice(0, 4096));
33498
+ setError("");
33499
+ });
33500
+ return (0, import_react.createElement)(Box, { flexDirection: "column", paddingX: 2 },
33501
+ (0, import_react.createElement)(Text, { bold: true }, spec.name + " login"),
33502
+ (0, import_react.createElement)(Text, { dimColor: true }, "Saved on this Mac in ~/.dscode/credentials.yaml"),
33503
+ (0, import_react.createElement)(Text, null, busy ? "Saving…" : target ? "API key › " + (draft ? "••••••••" : "paste your key") : error ? "" : "Loading…"),
33504
+ error ? (0, import_react.createElement)(Text, { color: "red" }, error) : void 0,
33505
+ (0, import_react.createElement)(Text, { dimColor: true }, "Enter save · Esc cancel · Ctrl+U clear"));
33506
+ }
33507
+ function DscodeProviderPanel({ current, load, choose, back }) {
33508
+ const [directory, setDirectory] = (0, import_react.useState)(void 0);
33509
+ const [failed, setFailed] = (0, import_react.useState)(false);
33510
+ const [cursor, setCursor] = (0, import_react.useState)(() => Math.max(0, DSCODE_PROVIDERS.findIndex(provider => provider.id === current)));
33511
+ (0, import_react.useEffect)(() => {
33512
+ let active = true;
33513
+ Promise.resolve().then(() => load()).then(loaded => { if (active) setDirectory(loaded); }, () => { if (active) setFailed(true); });
33514
+ return () => { active = false; };
33515
+ }, []);
33516
+ useStableInput((input, key) => {
33517
+ const count = DSCODE_PROVIDERS.length;
33518
+ if (key.escape || key.ctrl && input === "c" || input === "q") { back(); return; }
33519
+ if (key.upArrow || input === "k") { setCursor(index => (index + count - 1) % count); return; }
33520
+ if (key.downArrow || input === "j") { setCursor(index => (index + 1) % count); return; }
33521
+ if (key.return) { back(); choose(DSCODE_PROVIDERS[cursor].id); }
33522
+ });
33523
+ const status = provider => {
33524
+ if (failed) return "status unavailable";
33525
+ if (directory === void 0) return "…";
33526
+ const row = directory.rows.find(row => row.provider === provider.id);
33527
+ const state = dscodeCredentialState(row);
33528
+ if (state === "saved") return "key saved";
33529
+ if (state === "env") return "key from " + provider.credentialRef;
33530
+ if (state === "readonly") return provider.credentialRef + " is empty";
33531
+ if (state === "error") return "credential status unavailable";
33532
+ if (state === "unavailable") return "unavailable in this profile";
33533
+ return row?.configured === true || provider.id === "deepseek-official" ? "needs an API key" : "not set up · Enter sets it up";
33534
+ };
33535
+ return (0, import_react.createElement)(Box, { flexDirection: "column", paddingX: 2 },
33536
+ (0, import_react.createElement)(Text, { bold: true }, "Provider"),
33537
+ (0, import_react.createElement)(Text, { dimColor: true, wrap: "truncate-end" }, "The next step uses the chosen provider; /model picks among its models."),
33538
+ ...DSCODE_PROVIDERS.map((provider, index) => (0, import_react.createElement)(Text, { key: provider.id, bold: index === cursor, wrap: "truncate-end" },
33539
+ (index === cursor ? "› " : " ") + (provider.id === current ? "● " : "○ ") + provider.name.padEnd(11) + provider.id + " · " + status(provider))),
33540
+ (0, import_react.createElement)(Text, { dimColor: true }, "↑↓ choose · Enter switch · Esc cancel"));
33502
33541
  }
33503
33542
 
33504
33543
  function ProviderSetupPanel({ target, save, saveCredential, discover, effortDonors, done, back, onExit }) {
@@ -34628,7 +34667,7 @@ function CompletionMenu({ active, mention, index, rows, error }) {
34628
34667
  * While a modal (approval / question / model panel) owns the keys, the
34629
34668
  * box passes every key through untouched.
34630
34669
  */
34631
- function Input({ effortSurface, ultraPulse, active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openEmail, emailFill, emailConsumed, openLogin, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openLanguage, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, inspectFiles, prepareFiles, readClipboardImage, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, animations, applyAnimations, waveTier, waveStyle, maxRows, onEditorRows, onMenuRows, sessionKey }) {
34670
+ function Input({ effortSurface, ultraPulse, active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openEmail, emailFill, emailConsumed, openLogin, openProvider, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openLanguage, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, inspectFiles, prepareFiles, readClipboardImage, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, animations, applyAnimations, waveTier, waveStyle, maxRows, onEditorRows, onMenuRows, sessionKey }) {
34632
34671
  const columns = useStdout().stdout?.columns ?? 80;
34633
34672
  const inputTerminalRows = useStdout().stdout?.rows ?? 30;
34634
34673
  const dscodeImeStdout = useStdout().stdout;
@@ -35191,9 +35230,19 @@ function Input({ effortSurface, ultraPulse, active, frozen, busy, descriptors, s
35191
35230
  valueRef.current = ""; cursorRef.current = 0;
35192
35231
  setValue(""); setCursor(0); setCompletionIndex(0); setDismissedMenuValue(void 0);
35193
35232
  recall.current = beginRecall(recallSpace, "");
35194
- if (trimmed !== "/login") { notify("Use /login alone, then paste the key in the private input.", "warning"); return; }
35233
+ const dscodeLoginTarget = dscodeProviderArgument(trimmed.slice(6));
35234
+ if (dscodeLoginTarget === null) { notify("Use /login, /login deepseek or /login openrouter, then paste the key in the private input.", "warning"); return; }
35195
35235
  if (busy) { notify("Stop the running turn before /login.", "warning"); return; }
35196
- openLogin(); return;
35236
+ openLogin(dscodeLoginTarget); return;
35237
+ }
35238
+ if (trimmed === "/provider" || /^\/provider\s/.test(trimmed)) {
35239
+ valueRef.current = ""; cursorRef.current = 0;
35240
+ setValue(""); setCursor(0); setCompletionIndex(0); setDismissedMenuValue(void 0);
35241
+ recall.current = beginRecall(recallSpace, "");
35242
+ const dscodeProviderTarget = dscodeProviderArgument(trimmed.slice(9));
35243
+ if (dscodeProviderTarget === null) { notify("Usage: /provider [deepseek|openrouter]", "warning"); return; }
35244
+ if (busy) { notify("Stop the running turn before /provider.", "warning"); return; }
35245
+ openProvider(dscodeProviderTarget); return;
35197
35246
  }
35198
35247
  if (draftImagesRef.current.length > 0 || draftFilesRef.current.length > 0) {
35199
35248
  if (isSlashLine(text)) notify("commands cannot carry attachments; the line will be sent to the model as a prompt", "warning");
@@ -36190,6 +36239,27 @@ function App(props) {
36190
36239
  notify(`model switch failed: ${error instanceof Error ? error.message : String(error)}`, "error");
36191
36240
  }
36192
36241
  };
36242
+ // dscode-provider-switch
36243
+ const dscodeSwitchProvider = (provider) => {
36244
+ const spec = dscodeProviderSpec(provider);
36245
+ if (props.loadModelProviders === void 0) { notify("provider switching is unavailable in this profile", "warning"); return; }
36246
+ Promise.resolve().then(async () => {
36247
+ if (props.dscodeEnsureProviderRoute !== void 0 && await props.dscodeEnsureProviderRoute(provider)) reloadModelSurfaces();
36248
+ const providers = await props.loadModelProviders();
36249
+ const state = dscodeCredentialState(providers.rows.find((row) => row.provider === provider));
36250
+ if (state === "unavailable") { notify(spec.name + " is unavailable in this profile", "error"); return; }
36251
+ if (state === "missing") {
36252
+ setProviderOpen(false); setEffortFor(void 0);
36253
+ setProviderAction({ kind: "dscode-key", provider, then: () => dscodeSwitchProvider(provider) }); setModelOpen(true);
36254
+ return;
36255
+ }
36256
+ if (state === "error" || state === "readonly") notify("could not confirm the " + spec.name + " API key; requests may fail until /login " + provider + " succeeds", "warning");
36257
+ const models = await dscodeWaitForModels(props.loadModels, provider);
36258
+ const pick = dscodePickModel(models.rows, provider, modelLabel, effortLabel);
36259
+ if (pick === void 0) { notify(spec.name + " serves no models yet — check /model", "error"); return; }
36260
+ applyModel(pick.row, pick.effort);
36261
+ }).catch((error) => notify("provider switch failed: " + (error instanceof Error ? error.message : String(error)), "error"));
36262
+ };
36193
36263
  const reloadModelSurfaces = () => {
36194
36264
  setModelLoadEpoch((epoch) => epoch + 1);
36195
36265
  };
@@ -36232,10 +36302,21 @@ function App(props) {
36232
36302
  }, [providerDirectory, directory]);
36233
36303
  let modelSurface;
36234
36304
  if (modelOpen && !approvalPending && !questionPending) {
36235
- if (providerAction?.kind === "dscode-key") modelSurface = (0, import_react.createElement)(DscodeLoginPanel, {
36305
+ if (providerAction?.kind === "dscode-provider") modelSurface = (0, import_react.createElement)(DscodeProviderPanel, {
36306
+ current: dscodeProviderOfLabel(modelLabel),
36236
36307
  load: props.loadModelProviders,
36308
+ choose: dscodeSwitchProvider,
36309
+ back: closeModelSurface
36310
+ });
36311
+ else if (providerAction?.kind === "dscode-key") modelSurface = (0, import_react.createElement)(DscodeLoginPanel, {
36312
+ provider: providerAction.provider,
36313
+ load: async () => { await props.dscodeEnsureProviderRoute?.(providerAction.provider); return props.loadModelProviders(); },
36237
36314
  save: props.saveModelProviderCredential,
36238
- done: () => { closeModelSurface(); reloadModelSurfaces(); notify("DeepSeek API key saved locally; ready to use."); },
36315
+ done: () => {
36316
+ const then = providerAction.then;
36317
+ closeModelSurface(); reloadModelSurfaces();
36318
+ if (then !== void 0) then(); else notify(dscodeProviderSpec(providerAction.provider).name + " API key saved locally; ready to use.");
36319
+ },
36239
36320
  back: closeModelSurface
36240
36321
  });
36241
36322
  else if (providerAction?.kind === "login" && props.beginProviderAuthorization !== void 0 && props.cancelProviderAuthorization !== void 0 && props.openAuthorizationUrl !== void 0 && props.copyTextValue !== void 0) modelSurface = (0, import_react.createElement)(ProviderAuthorizationPanel, {
@@ -36587,9 +36668,14 @@ function App(props) {
36587
36668
  quit: props.quit,
36588
36669
  openEmail: () => setEmailOpen(true),
36589
36670
  emailFill, emailConsumed,
36590
- openLogin: () => {
36671
+ openLogin: (provider) => {
36672
+ setProviderOpen(false); setEffortFor(void 0);
36673
+ setProviderAction({ kind: "dscode-key", provider: provider ?? dscodeProviderOfLabel(modelLabel) }); setModelOpen(true);
36674
+ },
36675
+ openProvider: (provider) => {
36676
+ if (provider !== void 0) { dscodeSwitchProvider(provider); return; }
36591
36677
  setProviderOpen(false); setEffortFor(void 0);
36592
- setProviderAction({ kind: "dscode-key" }); setModelOpen(true);
36678
+ setProviderAction({ kind: "dscode-provider" }); setModelOpen(true);
36593
36679
  },
36594
36680
  openModel: () => {
36595
36681
  setDirectory(void 0);
@@ -36605,7 +36691,7 @@ function App(props) {
36605
36691
  },
36606
36692
  openEffort: () => {
36607
36693
  props.loadModels().then((loaded) => {
36608
- const [provider, model] = modelLabel.split("/");
36694
+ const { provider, model } = dscodeSplitModelLabel(modelLabel);
36609
36695
  const row = loaded.rows.find((candidate) => candidate.provider === provider && candidate.model === model) ?? loaded.rows.find((candidate) => candidate.model === model && candidate.reasoning !== void 0) ?? loaded.rows.find((candidate) => candidate.model === model);
36610
36696
  if (row === void 0) {
36611
36697
  notify("current model is not in the catalog", "warning");
@@ -39792,6 +39878,7 @@ async function run(ctx, startup, io) {
39792
39878
  interrupt,
39793
39879
  quit,
39794
39880
  loadModels: () => loadModelDirectory(ctx),
39881
+ dscodeEnsureProviderRoute: (provider) => dscodeEnsureProviderRoute(ctx.get("settings"), provider),
39795
39882
  loadModelProviders: () => loadProviderSettings(ctx),
39796
39883
  subscribeModelProviders: (listener) => subscribeProviderSettings(ctx, listener),
39797
39884
  saveModelProviderCredential: (target, key) => saveProviderCredential(ctx, target, key),