zidane 4.1.3 → 4.1.5
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/README.md +3 -3
- package/dist/index.js +1 -1
- package/dist/{providers-CX-R-Oy-.js → providers-CCDvIXGJ.js} +26 -5
- package/dist/providers-CCDvIXGJ.js.map +1 -0
- package/dist/providers.js +1 -1
- package/dist/tui.d.ts +257 -68
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +826 -187
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/dist/providers-CX-R-Oy-.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { d as createAgent } from "./tools-DpeWKzP1.js";
|
|
2
2
|
import { n as toolResultToText } from "./types-Bx_F8jet.js";
|
|
3
3
|
import { r as basic_default } from "./presets-Cs7_CsMk.js";
|
|
4
|
-
import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-
|
|
4
|
+
import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-CCDvIXGJ.js";
|
|
5
5
|
import { n as loadSession, t as createSession } from "./session-Cn68UASv.js";
|
|
6
6
|
import { createSqliteStore } from "./session/sqlite.js";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
7
8
|
import { dirname, resolve } from "node:path";
|
|
8
9
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
9
10
|
import { homedir } from "node:os";
|
|
11
|
+
import { anthropicOAuthProvider, openaiCodexOAuthProvider } from "@mariozechner/pi-ai/oauth";
|
|
10
12
|
import { getModel, getModels } from "@mariozechner/pi-ai";
|
|
11
13
|
import { RGBA, SyntaxStyle, createCliRenderer, defaultTextareaKeyBindings } from "@opentui/core";
|
|
12
14
|
import { createRoot, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
@@ -480,103 +482,277 @@ function ToolResultBlock({ text, indent }) {
|
|
|
480
482
|
});
|
|
481
483
|
}
|
|
482
484
|
//#endregion
|
|
483
|
-
//#region src/tui/
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
485
|
+
//#region src/tui/providers.ts
|
|
486
|
+
/** Convenience accessor — returns `credentialFileKey ?? key`. */
|
|
487
|
+
function credKeyOf(desc) {
|
|
488
|
+
return desc.credentialFileKey ?? desc.key;
|
|
489
|
+
}
|
|
490
|
+
/** Convenience accessor — returns `piProviderId ?? key`. */
|
|
491
|
+
function piIdOf(desc) {
|
|
492
|
+
return desc.piProviderId ?? desc.key;
|
|
493
|
+
}
|
|
494
|
+
const anthropicDescriptor = {
|
|
495
|
+
key: "anthropic",
|
|
496
|
+
label: "Anthropic",
|
|
497
|
+
factory: anthropic,
|
|
498
|
+
defaultModel: "claude-opus-4-7",
|
|
499
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
500
|
+
apiKeyPlaceholder: "sk-ant-…",
|
|
501
|
+
oauthProvider: anthropicOAuthProvider,
|
|
502
|
+
oauthHint: "Claude Pro/Max subscription"
|
|
503
|
+
};
|
|
504
|
+
const openaiDescriptor = {
|
|
505
|
+
key: "openai",
|
|
506
|
+
label: "OpenAI Codex",
|
|
507
|
+
factory: openai,
|
|
508
|
+
defaultModel: "gpt-5.4",
|
|
509
|
+
envKey: "OPENAI_CODEX_API_KEY",
|
|
510
|
+
credentialFileKey: "openai-codex",
|
|
511
|
+
piProviderId: "openai-codex",
|
|
512
|
+
apiKeyPlaceholder: "sk-… or eyJ… (Codex)",
|
|
513
|
+
oauthProvider: openaiCodexOAuthProvider
|
|
489
514
|
};
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
515
|
+
const openrouterDescriptor = {
|
|
516
|
+
key: "openrouter",
|
|
517
|
+
label: "OpenRouter",
|
|
518
|
+
factory: openrouter,
|
|
519
|
+
defaultModel: "anthropic/claude-sonnet-4-6",
|
|
520
|
+
envKey: "OPENROUTER_API_KEY",
|
|
521
|
+
apiKeyPlaceholder: "sk-or-…"
|
|
494
522
|
};
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
523
|
+
const cerebrasDescriptor = {
|
|
524
|
+
key: "cerebras",
|
|
525
|
+
label: "Cerebras",
|
|
526
|
+
factory: cerebras,
|
|
527
|
+
defaultModel: "zai-glm-4.7",
|
|
528
|
+
envKey: "CEREBRAS_API_KEY",
|
|
529
|
+
apiKeyPlaceholder: "csk-…"
|
|
530
|
+
};
|
|
531
|
+
/**
|
|
532
|
+
* Default provider registry. Passed verbatim when `runTui` is invoked without
|
|
533
|
+
* an explicit `providers` option. Hosts that want to override per-provider
|
|
534
|
+
* metadata can spread this and replace specific entries:
|
|
535
|
+
*
|
|
536
|
+
* ```ts
|
|
537
|
+
* runTui({ providers: { ...BUILTIN_PROVIDERS, anthropic: myOwnAnthropicDescriptor } })
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
const BUILTIN_PROVIDERS = {
|
|
541
|
+
anthropic: anthropicDescriptor,
|
|
542
|
+
openai: openaiDescriptor,
|
|
543
|
+
openrouter: openrouterDescriptor,
|
|
544
|
+
cerebras: cerebrasDescriptor
|
|
500
545
|
};
|
|
501
|
-
|
|
502
|
-
|
|
546
|
+
/**
|
|
547
|
+
* Resolve the model list for a given provider. Honors `descriptor.models`
|
|
548
|
+
* when set; otherwise queries pi-ai via `descriptor.piProviderId`. Returns
|
|
549
|
+
* `[]` for descriptors with no known mapping (custom providers without a
|
|
550
|
+
* model list) — callers should hide the model picker in that case.
|
|
551
|
+
*/
|
|
552
|
+
function modelsForDescriptor(descriptor) {
|
|
553
|
+
if (descriptor.models) return descriptor.models;
|
|
554
|
+
try {
|
|
555
|
+
return getModels(piIdOf(descriptor));
|
|
556
|
+
} catch {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Look up the model's max context window via the descriptor's model source.
|
|
562
|
+
* Returns `null` when the model isn't known (custom slugs, providers without
|
|
563
|
+
* a registry); callers should hide the context indicator in that case.
|
|
564
|
+
*/
|
|
565
|
+
function getContextWindow(descriptor, modelId) {
|
|
566
|
+
if (descriptor.models) return descriptor.models.find((m) => m.id === modelId)?.contextWindow ?? null;
|
|
567
|
+
try {
|
|
568
|
+
return getModel(piIdOf(descriptor), modelId)?.contextWindow ?? null;
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
503
572
|
}
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/tui/credentials.ts
|
|
575
|
+
/** POSIX mode for the credentials file. Ignored on Windows. */
|
|
576
|
+
const FILE_MODE = 384;
|
|
504
577
|
/**
|
|
505
|
-
*
|
|
578
|
+
* Resolve the credentials file path given the resolved TUI data directory
|
|
579
|
+
* (typically `~/.zidane`, i.e. `config.paths.dir`).
|
|
506
580
|
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
|
|
581
|
+
* Matches the convention used elsewhere in the TUI (sessions.db, state.json)
|
|
582
|
+
* so a single `ZIDANE_STORAGE_DIR` override moves the entire data root.
|
|
583
|
+
*/
|
|
584
|
+
function credentialsPath(dataDir) {
|
|
585
|
+
return resolve(dataDir, "credentials.json");
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Read credentials from disk.
|
|
510
589
|
*
|
|
511
|
-
*
|
|
590
|
+
* Returns `{}` when the file is missing or corrupt (last-ditch tolerance —
|
|
591
|
+
* a hand-edit gone wrong shouldn't lock the user out of re-authing). On first
|
|
592
|
+
* call with no file present, attempts a migration from `cwd/.credentials.json`
|
|
593
|
+
* (the legacy location used by `bun run auth`).
|
|
512
594
|
*/
|
|
513
|
-
function
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
methods.push({
|
|
533
|
-
source: "oauth",
|
|
534
|
-
detail
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
return {
|
|
539
|
-
key,
|
|
540
|
-
label: LABELS[key],
|
|
541
|
-
available: methods.length > 0,
|
|
542
|
-
methods
|
|
543
|
-
};
|
|
544
|
-
});
|
|
595
|
+
function readCredentials(dataDir) {
|
|
596
|
+
const path = credentialsPath(dataDir);
|
|
597
|
+
if (!existsSync(path)) {
|
|
598
|
+
const migrated = migrateLegacyFile(path);
|
|
599
|
+
if (migrated) return migrated;
|
|
600
|
+
return {};
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
const raw = readFileSync(path, "utf-8");
|
|
604
|
+
const parsed = JSON.parse(raw);
|
|
605
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
606
|
+
return parsed;
|
|
607
|
+
} catch {
|
|
608
|
+
return {};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/** Read a single provider's credential (translating via the descriptor). */
|
|
612
|
+
function readProviderCredential(dataDir, descriptor) {
|
|
613
|
+
return readCredentials(dataDir)[credKeyOf(descriptor)];
|
|
545
614
|
}
|
|
546
|
-
//#endregion
|
|
547
|
-
//#region src/tui/providers.ts
|
|
548
615
|
/**
|
|
549
|
-
*
|
|
616
|
+
* Write credentials atomically (write-then-rename) with mode 0o600.
|
|
550
617
|
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
618
|
+
* Atomic on the same filesystem — readers either see the previous file or the
|
|
619
|
+
* new one, never a half-written intermediate. Creates the parent dir if needed
|
|
620
|
+
* (first launch on a fresh machine: `~/.zidane/` may not exist yet).
|
|
554
621
|
*/
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
622
|
+
function writeCredentials(dataDir, creds) {
|
|
623
|
+
const path = credentialsPath(dataDir);
|
|
624
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
625
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
626
|
+
writeFileSync(tmp, `${JSON.stringify(creds, null, 2)}\n`, { mode: FILE_MODE });
|
|
627
|
+
renameSync(tmp, path);
|
|
628
|
+
}
|
|
629
|
+
function setProviderCredential(dataDir, descriptor, cred) {
|
|
630
|
+
const all = readCredentials(dataDir);
|
|
631
|
+
all[credKeyOf(descriptor)] = cred;
|
|
632
|
+
writeCredentials(dataDir, all);
|
|
633
|
+
}
|
|
634
|
+
function removeProviderCredential(dataDir, descriptor) {
|
|
635
|
+
const all = readCredentials(dataDir);
|
|
636
|
+
const fileKey = credKeyOf(descriptor);
|
|
637
|
+
if (!(fileKey in all)) return;
|
|
638
|
+
delete all[fileKey];
|
|
639
|
+
writeCredentials(dataDir, all);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Inject API-key credentials into `process.env` so the harness providers pick
|
|
643
|
+
* them up via their existing env-var resolution. Called once at TUI launch
|
|
644
|
+
* after the credentials file has been resolved. OAuth credentials are NOT
|
|
645
|
+
* injected — those reach providers via `ZIDANE_CREDENTIALS_PATH` + the file
|
|
646
|
+
* reader in `src/providers/oauth.ts`.
|
|
647
|
+
*
|
|
648
|
+
* Does not overwrite env vars that are already set — explicit user-provided
|
|
649
|
+
* env values win over stored API keys.
|
|
650
|
+
*
|
|
651
|
+
* Descriptors without an `envKey` (OAuth-only providers, custom providers
|
|
652
|
+
* that bypass env-var resolution) are skipped silently.
|
|
653
|
+
*/
|
|
654
|
+
function applyApiKeyEnv(dataDir, registry) {
|
|
655
|
+
const creds = readCredentials(dataDir);
|
|
656
|
+
for (const descriptor of Object.values(registry)) {
|
|
657
|
+
if (!descriptor.envKey || process.env[descriptor.envKey]) continue;
|
|
658
|
+
const cred = creds[credKeyOf(descriptor)];
|
|
659
|
+
if (cred?.kind === "apikey" && cred.value) process.env[descriptor.envKey] = cred.value;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
568
662
|
/**
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
663
|
+
* `bun run auth` (pre-TUI) wrote `cwd/.credentials.json` with an entry per
|
|
664
|
+
* provider mapping directly to an OAuthCredentials payload, e.g.:
|
|
665
|
+
*
|
|
666
|
+
* {
|
|
667
|
+
* "anthropic": { "access": "...", "refresh": "...", "expires": 123 },
|
|
668
|
+
* "openai-codex": { "access": "...", "refresh": "...", "expires": 123, "accountId": "..." }
|
|
669
|
+
* }
|
|
670
|
+
*
|
|
671
|
+
* We don't delete the legacy file — it might still be used by a host that
|
|
672
|
+
* imports the harness directly. We just copy its contents into the new
|
|
673
|
+
* location under the kind-tagged shape so the TUI picks them up.
|
|
674
|
+
*
|
|
675
|
+
* Migration is provider-agnostic: any top-level entry with an `access` field
|
|
676
|
+
* is preserved verbatim (extras included), under the same key. The TUI's
|
|
677
|
+
* detection then looks them up via the matching descriptor's `credentialFileKey`.
|
|
678
|
+
*
|
|
679
|
+
* Returns the migrated credentials when the migration ran, or `null` when
|
|
680
|
+
* there's no legacy file to migrate.
|
|
572
681
|
*/
|
|
573
|
-
function
|
|
682
|
+
function migrateLegacyFile(targetPath) {
|
|
683
|
+
const legacyPath = resolve(process.cwd(), ".credentials.json");
|
|
684
|
+
if (!existsSync(legacyPath)) return null;
|
|
685
|
+
let legacy;
|
|
574
686
|
try {
|
|
575
|
-
|
|
576
|
-
return getModel(providerId, modelId)?.contextWindow ?? null;
|
|
687
|
+
legacy = JSON.parse(readFileSync(legacyPath, "utf-8"));
|
|
577
688
|
} catch {
|
|
578
689
|
return null;
|
|
579
690
|
}
|
|
691
|
+
if (!legacy || typeof legacy !== "object" || Array.isArray(legacy)) return null;
|
|
692
|
+
const migrated = {};
|
|
693
|
+
for (const [fileKey, value] of Object.entries(legacy)) {
|
|
694
|
+
if (!isOAuthLegacy(value)) continue;
|
|
695
|
+
const { access, refresh, expires, ...extras } = value;
|
|
696
|
+
migrated[fileKey] = {
|
|
697
|
+
kind: "oauth",
|
|
698
|
+
access,
|
|
699
|
+
...typeof refresh === "string" ? { refresh } : {},
|
|
700
|
+
...typeof expires === "number" ? { expires } : {},
|
|
701
|
+
...extras
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
if (Object.keys(migrated).length === 0) return null;
|
|
705
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
706
|
+
const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
707
|
+
writeFileSync(tmp, `${JSON.stringify(migrated, null, 2)}\n`, { mode: FILE_MODE });
|
|
708
|
+
renameSync(tmp, targetPath);
|
|
709
|
+
return migrated;
|
|
710
|
+
}
|
|
711
|
+
function isOAuthLegacy(value) {
|
|
712
|
+
return typeof value === "object" && value !== null && "access" in value && typeof value.access === "string";
|
|
713
|
+
}
|
|
714
|
+
//#endregion
|
|
715
|
+
//#region src/tui/auth.ts
|
|
716
|
+
/**
|
|
717
|
+
* Detect available auth for every registered provider.
|
|
718
|
+
*
|
|
719
|
+
* Resolution order per provider (a method appears in `methods` for each
|
|
720
|
+
* layer that has a credential — the agent itself resolves them in the same
|
|
721
|
+
* order via its provider factories):
|
|
722
|
+
*
|
|
723
|
+
* 1. `kind: 'apikey'` from `credentials.json` (injected into env at TUI launch)
|
|
724
|
+
* 2. explicit env var (descriptor's `envKey`)
|
|
725
|
+
* 3. `kind: 'oauth'` from `credentials.json` (or legacy `cwd/.credentials.json`)
|
|
726
|
+
*
|
|
727
|
+
* Pure read — never refreshes or rewrites the credentials file.
|
|
728
|
+
*/
|
|
729
|
+
function detectAuth(dataDir, registry, env = process.env) {
|
|
730
|
+
const creds = readCredentials(dataDir);
|
|
731
|
+
return Object.values(registry).map((descriptor) => {
|
|
732
|
+
const methods = [];
|
|
733
|
+
const fileEntry = creds[credKeyOf(descriptor)];
|
|
734
|
+
if (fileEntry?.kind === "apikey" && fileEntry.value) methods.push({
|
|
735
|
+
source: "apikey",
|
|
736
|
+
detail: "credentials.json"
|
|
737
|
+
});
|
|
738
|
+
if (descriptor.envKey && env[descriptor.envKey]) methods.push({
|
|
739
|
+
source: "env",
|
|
740
|
+
detail: descriptor.envKey
|
|
741
|
+
});
|
|
742
|
+
if (fileEntry?.kind === "oauth" && fileEntry.access) {
|
|
743
|
+
const detail = typeof fileEntry.expires === "number" ? `oauth · expires ${new Date(fileEntry.expires).toLocaleString()}` : "oauth · credentials.json";
|
|
744
|
+
methods.push({
|
|
745
|
+
source: "oauth",
|
|
746
|
+
detail
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
key: descriptor.key,
|
|
751
|
+
label: descriptor.label,
|
|
752
|
+
available: methods.length > 0,
|
|
753
|
+
methods
|
|
754
|
+
};
|
|
755
|
+
});
|
|
580
756
|
}
|
|
581
757
|
//#endregion
|
|
582
758
|
//#region src/tui/store.ts
|
|
@@ -714,13 +890,12 @@ function resolveConfig(options = {}) {
|
|
|
714
890
|
const store = options.store ?? createTuiStore(paths.db);
|
|
715
891
|
const stateStore = createStateStore(paths.state);
|
|
716
892
|
const initialState = stateStore.load();
|
|
717
|
-
const providers =
|
|
718
|
-
...FACTORIES,
|
|
719
|
-
...options.providers ?? {}
|
|
720
|
-
};
|
|
893
|
+
const providers = options.providers ?? BUILTIN_PROVIDERS;
|
|
721
894
|
const preset = options.preset ?? basic_default;
|
|
722
|
-
|
|
723
|
-
|
|
895
|
+
process.env.ZIDANE_CREDENTIALS_PATH = credentialsPath(dir);
|
|
896
|
+
applyApiKeyEnv(dir, providers);
|
|
897
|
+
const modelsFor = makeModelsResolver(providers);
|
|
898
|
+
const resumeProvider = resolveResumeProvider(initialState, providers, dir);
|
|
724
899
|
const initialPicked = resumeProvider ? pickInitial(resumeProvider, providers, initialState) : null;
|
|
725
900
|
return {
|
|
726
901
|
prefix,
|
|
@@ -737,31 +912,32 @@ function resolveConfig(options = {}) {
|
|
|
737
912
|
initialPicked
|
|
738
913
|
};
|
|
739
914
|
}
|
|
740
|
-
function makeModelsResolver(
|
|
915
|
+
function makeModelsResolver(registry) {
|
|
741
916
|
return (key) => {
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
try {
|
|
745
|
-
const piId = PI_PROVIDER_ID[key];
|
|
746
|
-
return getModels(piId);
|
|
747
|
-
} catch {
|
|
748
|
-
return [];
|
|
749
|
-
}
|
|
917
|
+
const descriptor = registry[key];
|
|
918
|
+
return descriptor ? modelsForDescriptor(descriptor) : [];
|
|
750
919
|
};
|
|
751
920
|
}
|
|
752
|
-
function resolveResumeProvider(state, providers) {
|
|
921
|
+
function resolveResumeProvider(state, providers, storageDir) {
|
|
753
922
|
if (!state.lastProvider) return null;
|
|
754
923
|
if (!providers[state.lastProvider]) return null;
|
|
755
|
-
return detectAuth().find((p) => p.key === state.lastProvider && p.available) ?? null;
|
|
924
|
+
return detectAuth(storageDir, providers).find((p) => p.key === state.lastProvider && p.available) ?? null;
|
|
756
925
|
}
|
|
757
926
|
function pickInitial(auth, providers, state) {
|
|
758
|
-
const
|
|
759
|
-
if (!
|
|
760
|
-
const
|
|
761
|
-
return {
|
|
927
|
+
const descriptor = providers[auth.key];
|
|
928
|
+
if (!descriptor) return null;
|
|
929
|
+
const model = state.lastModelByProvider?.[auth.key] ?? descriptor.defaultModel ?? safeFactoryDefault(descriptor);
|
|
930
|
+
return model ? {
|
|
762
931
|
provider: auth,
|
|
763
|
-
model
|
|
764
|
-
};
|
|
932
|
+
model
|
|
933
|
+
} : null;
|
|
934
|
+
}
|
|
935
|
+
function safeFactoryDefault(descriptor) {
|
|
936
|
+
try {
|
|
937
|
+
return descriptor.factory().meta.defaultModel;
|
|
938
|
+
} catch {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
765
941
|
}
|
|
766
942
|
const ConfigContext = createContext(null);
|
|
767
943
|
function ConfigProvider({ config, children }) {
|
|
@@ -868,9 +1044,10 @@ function Modal({ title, onClose, children, maxWidth = 92, minWidth = 44, horizon
|
|
|
868
1044
|
/** Cap the visible scroll window so a 30-model list doesn't push the modal off-screen. */
|
|
869
1045
|
const VISIBLE_ROW_CAP = 12;
|
|
870
1046
|
/**
|
|
871
|
-
* Modal that lists the available models for the current provider and lets
|
|
872
|
-
* user pick one. Options come from
|
|
873
|
-
*
|
|
1047
|
+
* Modal that lists the available models for the current provider and lets
|
|
1048
|
+
* the user pick one. Options come from the active `ProviderDescriptor` —
|
|
1049
|
+
* either its declared `models` list or, when absent, pi-ai's built-in
|
|
1050
|
+
* registry looked up via `piProviderId`.
|
|
874
1051
|
*
|
|
875
1052
|
* Each row shows: `● selected · name (ctx N · reasoning · vision)`.
|
|
876
1053
|
*/
|
|
@@ -933,9 +1110,21 @@ function EmptyState() {
|
|
|
933
1110
|
children: [/* @__PURE__ */ jsx("text", {
|
|
934
1111
|
fg: COLOR.dim,
|
|
935
1112
|
children: "No models available for this provider."
|
|
936
|
-
}), /* @__PURE__ */
|
|
1113
|
+
}), /* @__PURE__ */ jsxs("text", {
|
|
937
1114
|
fg: COLOR.mute,
|
|
938
|
-
children:
|
|
1115
|
+
children: [
|
|
1116
|
+
"Set",
|
|
1117
|
+
/* @__PURE__ */ jsx("span", {
|
|
1118
|
+
fg: COLOR.model,
|
|
1119
|
+
children: " models "
|
|
1120
|
+
}),
|
|
1121
|
+
"on the provider descriptor (or a",
|
|
1122
|
+
/* @__PURE__ */ jsx("span", {
|
|
1123
|
+
fg: COLOR.model,
|
|
1124
|
+
children: " piProviderId "
|
|
1125
|
+
}),
|
|
1126
|
+
"that pi-ai recognizes) to populate this list."
|
|
1127
|
+
]
|
|
939
1128
|
})]
|
|
940
1129
|
});
|
|
941
1130
|
}
|
|
@@ -947,35 +1136,140 @@ function describeModel(m) {
|
|
|
947
1136
|
return parts.join(" · ");
|
|
948
1137
|
}
|
|
949
1138
|
//#endregion
|
|
1139
|
+
//#region src/tui/oauth.ts
|
|
1140
|
+
function supportsOAuth(descriptor) {
|
|
1141
|
+
return descriptor.oauthProvider !== void 0;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Run the OAuth login flow for a provider.
|
|
1145
|
+
*
|
|
1146
|
+
* Returns the OAuth credentials on success; caller persists them via
|
|
1147
|
+
* `setProviderCredential(dataDir, descriptor, { kind: 'oauth', ...credentials })`.
|
|
1148
|
+
* Throws when the descriptor has no `oauthProvider` configured.
|
|
1149
|
+
*/
|
|
1150
|
+
async function runOAuthLogin(descriptor, options) {
|
|
1151
|
+
if (!descriptor.oauthProvider) throw new Error(`OAuth not supported for ${descriptor.label} (${descriptor.key}) — use an API key instead.`);
|
|
1152
|
+
const callbacks = {
|
|
1153
|
+
onAuth: (info) => {
|
|
1154
|
+
options.onUrl(info.url, info.instructions);
|
|
1155
|
+
tryOpenBrowser(info.url);
|
|
1156
|
+
},
|
|
1157
|
+
onPrompt: async () => {
|
|
1158
|
+
if (!options.onCodeRequest) throw new Error("OAuth flow requires manual code input but no handler is wired.");
|
|
1159
|
+
return options.onCodeRequest();
|
|
1160
|
+
},
|
|
1161
|
+
onProgress: options.onProgress,
|
|
1162
|
+
signal: options.signal
|
|
1163
|
+
};
|
|
1164
|
+
return descriptor.oauthProvider.login(callbacks);
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Best-effort cross-platform browser open. macOS uses `open`, Linux uses
|
|
1168
|
+
* `xdg-open`, Windows uses `start`. Failures are swallowed — the callback
|
|
1169
|
+
* server is already listening, and the URL is displayed in the TUI for
|
|
1170
|
+
* manual click.
|
|
1171
|
+
*
|
|
1172
|
+
* Uses `spawn` (not `exec`) so the URL is passed as an argv element rather
|
|
1173
|
+
* than interpolated into a shell command — no need to think about quoting
|
|
1174
|
+
* URLs that contain `&`, `?`, `"` or other shell metacharacters.
|
|
1175
|
+
*/
|
|
1176
|
+
function tryOpenBrowser(url) {
|
|
1177
|
+
const [cmd, ...args] = (() => {
|
|
1178
|
+
if (process.platform === "darwin") return ["open", url];
|
|
1179
|
+
if (process.platform === "win32") return [
|
|
1180
|
+
"cmd",
|
|
1181
|
+
"/c",
|
|
1182
|
+
"start",
|
|
1183
|
+
"",
|
|
1184
|
+
url
|
|
1185
|
+
];
|
|
1186
|
+
return ["xdg-open", url];
|
|
1187
|
+
})();
|
|
1188
|
+
try {
|
|
1189
|
+
const child = spawn(cmd, args, {
|
|
1190
|
+
stdio: "ignore",
|
|
1191
|
+
detached: true
|
|
1192
|
+
});
|
|
1193
|
+
child.on("error", () => {});
|
|
1194
|
+
child.unref();
|
|
1195
|
+
} catch {}
|
|
1196
|
+
}
|
|
1197
|
+
//#endregion
|
|
950
1198
|
//#region src/tui/screens.tsx
|
|
951
1199
|
/**
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
* binding wins regardless of modifier state.
|
|
1200
|
+
* Build a key-binding set for the prompt textarea / API-key input. Strips the
|
|
1201
|
+
* default `return` action and reinstalls it with our preferred meaning, so the
|
|
1202
|
+
* binding wins regardless of modifier state. Pass `allowShiftReturnNewline`
|
|
1203
|
+
* to enable `shift+enter` → newline (multi-line input).
|
|
955
1204
|
*/
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1205
|
+
function makeSubmitBindings(allowShiftReturnNewline) {
|
|
1206
|
+
const base = defaultTextareaKeyBindings.filter((b) => b.name !== "return");
|
|
1207
|
+
return allowShiftReturnNewline ? [
|
|
1208
|
+
...base,
|
|
1209
|
+
{
|
|
1210
|
+
name: "return",
|
|
1211
|
+
action: "submit"
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
name: "return",
|
|
1215
|
+
shift: true,
|
|
1216
|
+
action: "newline"
|
|
1217
|
+
}
|
|
1218
|
+
] : [...base, {
|
|
959
1219
|
name: "return",
|
|
960
1220
|
action: "submit"
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1221
|
+
}];
|
|
1222
|
+
}
|
|
1223
|
+
const TEXTAREA_BINDINGS = makeSubmitBindings(true);
|
|
1224
|
+
const API_KEY_INPUT_BINDINGS = makeSubmitBindings(false);
|
|
1225
|
+
/**
|
|
1226
|
+
* Look up a `{ key }` item by the value of a `<select>` option. Used by every
|
|
1227
|
+
* screen that mixes keyed-entry rows with sentinel "+ new" / "← back" rows —
|
|
1228
|
+
* sentinel handling stays explicit at the call site, this helper just trims
|
|
1229
|
+
* the boilerplate `.find(i => i.key === ...)` typing.
|
|
1230
|
+
*/
|
|
1231
|
+
function findByKey(items, value) {
|
|
1232
|
+
return typeof value === "string" ? items.find((i) => i.key === value) : void 0;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Sentinel value used by the picker's "+ add / re-configure" option. Lives
|
|
1236
|
+
* outside the provider key namespace (key strings are at least one char, no
|
|
1237
|
+
* leading `__`) so we can't collide with a real registry entry.
|
|
1238
|
+
*/
|
|
1239
|
+
const WIZARD_OPTION_VALUE = "__wizard__";
|
|
968
1240
|
function AuthScreen({ onPick }) {
|
|
969
|
-
const
|
|
1241
|
+
const config = useConfig();
|
|
1242
|
+
const { providers: registry } = config;
|
|
970
1243
|
const focused = useModalAwareFocus();
|
|
971
|
-
const providers =
|
|
1244
|
+
const [providers, setProviders] = useState([]);
|
|
1245
|
+
const refresh = useCallback(() => setProviders(detectAuth(config.paths.dir, registry)), [config.paths.dir, registry]);
|
|
1246
|
+
useEffect(() => {
|
|
1247
|
+
refresh();
|
|
1248
|
+
}, [refresh]);
|
|
1249
|
+
const [forceWizard, setForceWizard] = useState(false);
|
|
972
1250
|
const available = useMemo(() => providers.filter((p) => p.available), [providers]);
|
|
973
|
-
|
|
974
|
-
|
|
1251
|
+
const onWizardDone = useCallback(() => {
|
|
1252
|
+
setForceWizard(false);
|
|
1253
|
+
refresh();
|
|
1254
|
+
}, [refresh]);
|
|
1255
|
+
if (available.length === 0 || forceWizard) {
|
|
1256
|
+
const canCancel = forceWizard && available.length > 0;
|
|
1257
|
+
return /* @__PURE__ */ jsx(SetupWizard, {
|
|
1258
|
+
registry,
|
|
1259
|
+
dataDir: config.paths.dir,
|
|
1260
|
+
onConfigured: onWizardDone,
|
|
1261
|
+
onCancel: canCancel ? () => setForceWizard(false) : void 0
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
const options = [...available.map((p) => ({
|
|
975
1265
|
name: p.label,
|
|
976
1266
|
description: p.methods.map((m) => m.detail).join(" · "),
|
|
977
1267
|
value: p.key
|
|
978
|
-
}))
|
|
1268
|
+
})), {
|
|
1269
|
+
name: "+ add or re-configure a provider",
|
|
1270
|
+
description: "launch the setup wizard",
|
|
1271
|
+
value: WIZARD_OPTION_VALUE
|
|
1272
|
+
}];
|
|
979
1273
|
return /* @__PURE__ */ jsx("box", {
|
|
980
1274
|
title: " pick a provider ",
|
|
981
1275
|
style: {
|
|
@@ -992,48 +1286,318 @@ function AuthScreen({ onPick }) {
|
|
|
992
1286
|
wrapSelection: true,
|
|
993
1287
|
onSelect: (_idx, option) => {
|
|
994
1288
|
if (!option) return;
|
|
995
|
-
|
|
1289
|
+
if (option.value === WIZARD_OPTION_VALUE) {
|
|
1290
|
+
setForceWizard(true);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const provider = findByKey(available, option.value);
|
|
996
1294
|
if (provider) onPick(provider);
|
|
997
1295
|
},
|
|
998
1296
|
style: { flexGrow: 1 }
|
|
999
1297
|
})
|
|
1000
1298
|
});
|
|
1001
1299
|
}
|
|
1002
|
-
function
|
|
1300
|
+
function SetupWizard({ registry, dataDir, onConfigured, onCancel }) {
|
|
1301
|
+
const [step, setStep] = useState({ kind: "pick-provider" });
|
|
1302
|
+
const [error, setError] = useState(null);
|
|
1303
|
+
const descriptors = useMemo(() => Object.values(registry), [registry]);
|
|
1304
|
+
const onPickProvider = useCallback((descriptor) => {
|
|
1305
|
+
setError(null);
|
|
1306
|
+
setStep({
|
|
1307
|
+
kind: "pick-method",
|
|
1308
|
+
descriptor
|
|
1309
|
+
});
|
|
1310
|
+
}, []);
|
|
1311
|
+
const onPickMethod = useCallback((descriptor, method) => {
|
|
1312
|
+
setError(null);
|
|
1313
|
+
if (method === "apikey") setStep({
|
|
1314
|
+
kind: "enter-apikey",
|
|
1315
|
+
descriptor
|
|
1316
|
+
});
|
|
1317
|
+
else setStep({
|
|
1318
|
+
kind: "oauth-running",
|
|
1319
|
+
descriptor
|
|
1320
|
+
});
|
|
1321
|
+
}, []);
|
|
1322
|
+
const onApiKeySubmit = useCallback((descriptor, value) => {
|
|
1323
|
+
const trimmed = value.trim();
|
|
1324
|
+
if (!trimmed) {
|
|
1325
|
+
setError("API key cannot be empty.");
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
try {
|
|
1329
|
+
setProviderCredential(dataDir, descriptor, {
|
|
1330
|
+
kind: "apikey",
|
|
1331
|
+
value: trimmed
|
|
1332
|
+
});
|
|
1333
|
+
if (descriptor.envKey) process.env[descriptor.envKey] = trimmed;
|
|
1334
|
+
onConfigured();
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1337
|
+
}
|
|
1338
|
+
}, [dataDir, onConfigured]);
|
|
1339
|
+
if (descriptors.length === 0) return /* @__PURE__ */ jsx(EmptyRegistryNotice, {});
|
|
1340
|
+
if (step.kind === "pick-provider") return /* @__PURE__ */ jsx(PickProviderStep, {
|
|
1341
|
+
descriptors,
|
|
1342
|
+
error,
|
|
1343
|
+
onPick: onPickProvider,
|
|
1344
|
+
onCancel
|
|
1345
|
+
});
|
|
1346
|
+
if (step.kind === "pick-method") return /* @__PURE__ */ jsx(PickMethodStep, {
|
|
1347
|
+
descriptor: step.descriptor,
|
|
1348
|
+
error,
|
|
1349
|
+
onPick: onPickMethod
|
|
1350
|
+
});
|
|
1351
|
+
if (step.kind === "enter-apikey") return /* @__PURE__ */ jsx(EnterApiKeyStep, {
|
|
1352
|
+
descriptor: step.descriptor,
|
|
1353
|
+
error,
|
|
1354
|
+
onSubmit: onApiKeySubmit
|
|
1355
|
+
});
|
|
1356
|
+
return /* @__PURE__ */ jsx(OAuthRunningStep, {
|
|
1357
|
+
descriptor: step.descriptor,
|
|
1358
|
+
dataDir,
|
|
1359
|
+
onSuccess: onConfigured,
|
|
1360
|
+
onError: (msg) => {
|
|
1361
|
+
setError(msg);
|
|
1362
|
+
setStep({
|
|
1363
|
+
kind: "pick-method",
|
|
1364
|
+
descriptor: step.descriptor
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Shared wrapper for every wizard step — same border + padding + flex layout
|
|
1371
|
+
* with a customizable title and accent color. Footnote slot at the bottom for
|
|
1372
|
+
* an error banner.
|
|
1373
|
+
*/
|
|
1374
|
+
function WizardPanel({ title, accent = COLOR.border, error, children }) {
|
|
1003
1375
|
return /* @__PURE__ */ jsxs("box", {
|
|
1004
|
-
title
|
|
1376
|
+
title,
|
|
1005
1377
|
style: {
|
|
1006
1378
|
border: true,
|
|
1007
|
-
borderColor:
|
|
1379
|
+
borderColor: accent,
|
|
1008
1380
|
padding: 1,
|
|
1009
|
-
flexDirection: "column",
|
|
1010
1381
|
gap: 1,
|
|
1382
|
+
flexDirection: "column",
|
|
1011
1383
|
flexGrow: 1
|
|
1012
1384
|
},
|
|
1385
|
+
children: [children, error && /* @__PURE__ */ jsx("text", {
|
|
1386
|
+
fg: COLOR.error,
|
|
1387
|
+
children: error
|
|
1388
|
+
})]
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
/** "esc to exit" footer hint shared by every wizard step that doesn't offer a "← back" affordance. */
|
|
1392
|
+
function WizardEscHint() {
|
|
1393
|
+
return /* @__PURE__ */ jsx("text", {
|
|
1394
|
+
fg: COLOR.dim,
|
|
1395
|
+
children: "esc to exit"
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
function EmptyRegistryNotice() {
|
|
1399
|
+
return /* @__PURE__ */ jsxs(WizardPanel, {
|
|
1400
|
+
title: " no providers configured ",
|
|
1401
|
+
accent: COLOR.error,
|
|
1402
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
1403
|
+
fg: COLOR.error,
|
|
1404
|
+
children: "This TUI has no providers registered."
|
|
1405
|
+
}), /* @__PURE__ */ jsxs("text", {
|
|
1406
|
+
fg: COLOR.dim,
|
|
1407
|
+
children: [
|
|
1408
|
+
"Pass providers via",
|
|
1409
|
+
/* @__PURE__ */ jsx("span", {
|
|
1410
|
+
fg: COLOR.model,
|
|
1411
|
+
children: " runTui({ providers }) "
|
|
1412
|
+
}),
|
|
1413
|
+
"or use the built-ins via",
|
|
1414
|
+
/* @__PURE__ */ jsx("span", {
|
|
1415
|
+
fg: COLOR.model,
|
|
1416
|
+
children: " BUILTIN_PROVIDERS "
|
|
1417
|
+
}),
|
|
1418
|
+
"."
|
|
1419
|
+
]
|
|
1420
|
+
})]
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
/** Sentinel option value used for the wizard's "← back to picker" entry. */
|
|
1424
|
+
const WIZARD_BACK_VALUE = "__back__";
|
|
1425
|
+
function PickProviderStep({ descriptors, error, onPick, onCancel }) {
|
|
1426
|
+
const focused = useModalAwareFocus();
|
|
1427
|
+
const options = [...descriptors.map((d) => {
|
|
1428
|
+
const methods = supportsOAuth(d) ? ["API key", "OAuth"] : ["API key"];
|
|
1429
|
+
return {
|
|
1430
|
+
name: d.label,
|
|
1431
|
+
description: methods.join(" · "),
|
|
1432
|
+
value: d.key
|
|
1433
|
+
};
|
|
1434
|
+
}), ...onCancel ? [{
|
|
1435
|
+
name: "← back",
|
|
1436
|
+
description: "return to the provider list",
|
|
1437
|
+
value: WIZARD_BACK_VALUE
|
|
1438
|
+
}] : []];
|
|
1439
|
+
return /* @__PURE__ */ jsxs(WizardPanel, {
|
|
1440
|
+
title: onCancel ? " add or re-configure a provider " : " welcome to zidane · pick a provider ",
|
|
1441
|
+
error,
|
|
1442
|
+
children: [!onCancel && /* @__PURE__ */ jsxs("text", {
|
|
1443
|
+
fg: COLOR.dim,
|
|
1444
|
+
children: [
|
|
1445
|
+
"No provider credentials yet. Pick a provider to configure — keys are stored in",
|
|
1446
|
+
/* @__PURE__ */ jsx("span", {
|
|
1447
|
+
fg: COLOR.model,
|
|
1448
|
+
children: " ~/.zidane/credentials.json "
|
|
1449
|
+
}),
|
|
1450
|
+
"(owner-only)."
|
|
1451
|
+
]
|
|
1452
|
+
}), /* @__PURE__ */ jsx("select", {
|
|
1453
|
+
...SELECT_THEME,
|
|
1454
|
+
focused,
|
|
1455
|
+
options,
|
|
1456
|
+
wrapSelection: true,
|
|
1457
|
+
onSelect: (_idx, option) => {
|
|
1458
|
+
if (!option) return;
|
|
1459
|
+
if (option.value === WIZARD_BACK_VALUE) {
|
|
1460
|
+
onCancel?.();
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const descriptor = findByKey(descriptors, option.value);
|
|
1464
|
+
if (descriptor) onPick(descriptor);
|
|
1465
|
+
},
|
|
1466
|
+
style: { flexGrow: 1 }
|
|
1467
|
+
})]
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
function PickMethodStep({ descriptor, error, onPick }) {
|
|
1471
|
+
const focused = useModalAwareFocus();
|
|
1472
|
+
const options = useMemo(() => {
|
|
1473
|
+
const items = [{
|
|
1474
|
+
name: "API key",
|
|
1475
|
+
description: `paste your ${descriptor.label} API key`,
|
|
1476
|
+
value: "apikey"
|
|
1477
|
+
}];
|
|
1478
|
+
if (supportsOAuth(descriptor)) {
|
|
1479
|
+
const hint = descriptor.oauthHint ? ` (${descriptor.oauthHint})` : "";
|
|
1480
|
+
items.push({
|
|
1481
|
+
name: "OAuth",
|
|
1482
|
+
description: `browser-based sign-in${hint}`,
|
|
1483
|
+
value: "oauth"
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
return items;
|
|
1487
|
+
}, [descriptor]);
|
|
1488
|
+
return /* @__PURE__ */ jsxs(WizardPanel, {
|
|
1489
|
+
title: ` configure ${descriptor.label} — pick auth method `,
|
|
1490
|
+
error,
|
|
1491
|
+
children: [/* @__PURE__ */ jsx(WizardEscHint, {}), /* @__PURE__ */ jsx("select", {
|
|
1492
|
+
...SELECT_THEME,
|
|
1493
|
+
focused,
|
|
1494
|
+
options,
|
|
1495
|
+
wrapSelection: true,
|
|
1496
|
+
onSelect: (_idx, option) => {
|
|
1497
|
+
if (option) onPick(descriptor, option.value);
|
|
1498
|
+
},
|
|
1499
|
+
style: { flexGrow: 1 }
|
|
1500
|
+
})]
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
function EnterApiKeyStep({ descriptor, error, onSubmit }) {
|
|
1504
|
+
const focused = useModalAwareFocus();
|
|
1505
|
+
const inputRef = useRef(null);
|
|
1506
|
+
const submit = useCallback(() => {
|
|
1507
|
+
onSubmit(descriptor, inputRef.current?.value ?? "");
|
|
1508
|
+
}, [descriptor, onSubmit]);
|
|
1509
|
+
return /* @__PURE__ */ jsxs(WizardPanel, {
|
|
1510
|
+
title: ` configure ${descriptor.label} — paste API key `,
|
|
1511
|
+
error,
|
|
1512
|
+
children: [/* @__PURE__ */ jsxs("text", {
|
|
1513
|
+
fg: COLOR.dim,
|
|
1514
|
+
children: [
|
|
1515
|
+
"Paste your",
|
|
1516
|
+
` ${descriptor.label} `,
|
|
1517
|
+
"API key and press",
|
|
1518
|
+
/* @__PURE__ */ jsx("span", {
|
|
1519
|
+
fg: COLOR.model,
|
|
1520
|
+
children: " enter "
|
|
1521
|
+
}),
|
|
1522
|
+
"to save. Esc to exit."
|
|
1523
|
+
]
|
|
1524
|
+
}), /* @__PURE__ */ jsx("box", {
|
|
1525
|
+
style: {
|
|
1526
|
+
border: true,
|
|
1527
|
+
borderColor: COLOR.borderActive,
|
|
1528
|
+
paddingLeft: 1,
|
|
1529
|
+
paddingRight: 1,
|
|
1530
|
+
height: 3
|
|
1531
|
+
},
|
|
1532
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
1533
|
+
ref: inputRef,
|
|
1534
|
+
focused,
|
|
1535
|
+
keyBindings: API_KEY_INPUT_BINDINGS,
|
|
1536
|
+
placeholder: descriptor.apiKeyPlaceholder ?? "API key…",
|
|
1537
|
+
onSubmit: submit,
|
|
1538
|
+
style: { flexGrow: 1 }
|
|
1539
|
+
})
|
|
1540
|
+
})]
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
function OAuthRunningStep({ descriptor, dataDir, onSuccess, onError }) {
|
|
1544
|
+
const [url, setUrl] = useState(null);
|
|
1545
|
+
const [status, setStatus] = useState("starting browser…");
|
|
1546
|
+
useEffect(() => {
|
|
1547
|
+
const ac = new AbortController();
|
|
1548
|
+
let cancelled = false;
|
|
1549
|
+
(async () => {
|
|
1550
|
+
try {
|
|
1551
|
+
const creds = await runOAuthLogin(descriptor, {
|
|
1552
|
+
onUrl: (loginUrl) => {
|
|
1553
|
+
if (cancelled) return;
|
|
1554
|
+
setUrl(loginUrl);
|
|
1555
|
+
setStatus("waiting for browser callback…");
|
|
1556
|
+
},
|
|
1557
|
+
onProgress: (message) => {
|
|
1558
|
+
if (!cancelled) setStatus(message);
|
|
1559
|
+
},
|
|
1560
|
+
signal: ac.signal
|
|
1561
|
+
});
|
|
1562
|
+
if (cancelled) return;
|
|
1563
|
+
setProviderCredential(dataDir, descriptor, {
|
|
1564
|
+
kind: "oauth",
|
|
1565
|
+
...creds
|
|
1566
|
+
});
|
|
1567
|
+
onSuccess();
|
|
1568
|
+
} catch (err) {
|
|
1569
|
+
if (cancelled) return;
|
|
1570
|
+
onError(err instanceof Error ? err.message : String(err));
|
|
1571
|
+
}
|
|
1572
|
+
})();
|
|
1573
|
+
return () => {
|
|
1574
|
+
cancelled = true;
|
|
1575
|
+
ac.abort();
|
|
1576
|
+
};
|
|
1577
|
+
}, [
|
|
1578
|
+
descriptor,
|
|
1579
|
+
dataDir,
|
|
1580
|
+
onSuccess,
|
|
1581
|
+
onError
|
|
1582
|
+
]);
|
|
1583
|
+
return /* @__PURE__ */ jsxs(WizardPanel, {
|
|
1584
|
+
title: ` configure ${descriptor.label} — OAuth `,
|
|
1013
1585
|
children: [
|
|
1014
|
-
/* @__PURE__ */ jsx(
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}),
|
|
1030
|
-
" → ",
|
|
1031
|
-
/* @__PURE__ */ jsx("span", {
|
|
1032
|
-
fg: COLOR.model,
|
|
1033
|
-
children: envKeyFor(p.key)
|
|
1034
|
-
})
|
|
1035
|
-
]
|
|
1036
|
-
}, p.key))
|
|
1586
|
+
/* @__PURE__ */ jsx(WizardEscHint, {}),
|
|
1587
|
+
/* @__PURE__ */ jsx(Spinner, { label: status }),
|
|
1588
|
+
url && /* @__PURE__ */ jsxs("box", {
|
|
1589
|
+
style: {
|
|
1590
|
+
flexDirection: "column",
|
|
1591
|
+
gap: 0
|
|
1592
|
+
},
|
|
1593
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
1594
|
+
fg: COLOR.dim,
|
|
1595
|
+
children: "If the browser didn't open, visit:"
|
|
1596
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
1597
|
+
fg: COLOR.model,
|
|
1598
|
+
children: url
|
|
1599
|
+
})]
|
|
1600
|
+
})
|
|
1037
1601
|
]
|
|
1038
1602
|
});
|
|
1039
1603
|
}
|
|
@@ -1074,7 +1638,7 @@ function SessionsScreen({ sessions, currentId, onPick, onCreate }) {
|
|
|
1074
1638
|
onSelect: (_idx, option) => {
|
|
1075
1639
|
if (!option) return;
|
|
1076
1640
|
if (option.value === NEW_VALUE) onCreate();
|
|
1077
|
-
else onPick(option.value);
|
|
1641
|
+
else if (typeof option.value === "string") onPick(option.value);
|
|
1078
1642
|
},
|
|
1079
1643
|
style: { flexGrow: 1 }
|
|
1080
1644
|
})
|
|
@@ -1087,7 +1651,7 @@ function ChatScreen({ events, busy, settings, onSubmit, session }) {
|
|
|
1087
1651
|
const title = useMemo(() => {
|
|
1088
1652
|
if (!session) return " untitled ";
|
|
1089
1653
|
const turns = `${session.turnCount} turn${session.turnCount === 1 ? "" : "s"}`;
|
|
1090
|
-
return ` ${session.title}
|
|
1654
|
+
return ` ${session.title} · #${shortId(session.id)} · ${turns} `;
|
|
1091
1655
|
}, [session]);
|
|
1092
1656
|
const userPrompts = useMemo(() => events.filter((e) => e.kind === "info").map((e) => e.text.replace(/^❯ /, "")), [events]);
|
|
1093
1657
|
return /* @__PURE__ */ jsxs("box", {
|
|
@@ -1239,40 +1803,76 @@ function useSettings() {
|
|
|
1239
1803
|
if (!ctx) throw new Error("useSettings must be used inside <SettingsProvider>");
|
|
1240
1804
|
return ctx;
|
|
1241
1805
|
}
|
|
1242
|
-
const
|
|
1806
|
+
const TOGGLES = [
|
|
1243
1807
|
{
|
|
1808
|
+
kind: "toggle",
|
|
1244
1809
|
key: "showThinking",
|
|
1245
1810
|
label: "Thinking blocks",
|
|
1246
1811
|
description: "agent reasoning shown inline"
|
|
1247
1812
|
},
|
|
1248
1813
|
{
|
|
1814
|
+
kind: "toggle",
|
|
1249
1815
|
key: "showToolCalls",
|
|
1250
1816
|
label: "Tool calls",
|
|
1251
1817
|
description: "the ↳ name(args) lines"
|
|
1252
1818
|
},
|
|
1253
1819
|
{
|
|
1820
|
+
kind: "toggle",
|
|
1254
1821
|
key: "showToolResults",
|
|
1255
1822
|
label: "Tool outputs",
|
|
1256
1823
|
description: "the ┃ result blocks under tool calls"
|
|
1257
1824
|
}
|
|
1258
1825
|
];
|
|
1259
|
-
function SettingsModal() {
|
|
1826
|
+
function SettingsModal({ actions } = {}) {
|
|
1260
1827
|
const { settings, toggle } = useSettings();
|
|
1261
|
-
const [cursor,
|
|
1828
|
+
const [cursor, setCursorRaw] = useState(0);
|
|
1829
|
+
const items = useMemo(() => {
|
|
1830
|
+
const actionItems = [];
|
|
1831
|
+
if (actions?.onReauth) actionItems.push({
|
|
1832
|
+
kind: "action",
|
|
1833
|
+
id: "reauth",
|
|
1834
|
+
label: "Authentication",
|
|
1835
|
+
description: "switch provider, add another, or re-authenticate",
|
|
1836
|
+
onPick: actions.onReauth
|
|
1837
|
+
});
|
|
1838
|
+
return [...TOGGLES, ...actionItems];
|
|
1839
|
+
}, [actions]);
|
|
1840
|
+
const safeCursor = Math.min(cursor, items.length - 1);
|
|
1841
|
+
const setCursor = useCallback((update) => setCursorRaw((prev) => Math.min(Math.max(0, update(prev)), items.length - 1)), [items.length]);
|
|
1262
1842
|
useKeyboard((key) => {
|
|
1263
|
-
if (key.name === "up" || key.ctrl && key.name === "p") setCursor((c) =>
|
|
1264
|
-
else if (key.name === "down" || key.ctrl && key.name === "n") setCursor((c) =>
|
|
1265
|
-
else if (key.name === "return" || key.name === "space")
|
|
1843
|
+
if (key.name === "up" || key.ctrl && key.name === "p") setCursor((c) => c - 1);
|
|
1844
|
+
else if (key.name === "down" || key.ctrl && key.name === "n") setCursor((c) => c + 1);
|
|
1845
|
+
else if (key.name === "return" || key.name === "space") {
|
|
1846
|
+
const item = items[safeCursor];
|
|
1847
|
+
if (!item) return;
|
|
1848
|
+
if (item.kind === "toggle") toggle(item.key);
|
|
1849
|
+
else item.onPick();
|
|
1850
|
+
}
|
|
1266
1851
|
});
|
|
1852
|
+
const firstActionIndex = items.findIndex((i) => i.kind === "action");
|
|
1267
1853
|
return /* @__PURE__ */ jsxs(Modal, {
|
|
1268
1854
|
title: "settings",
|
|
1269
1855
|
children: [/* @__PURE__ */ jsx("box", {
|
|
1270
1856
|
style: { flexDirection: "column" },
|
|
1271
|
-
children:
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1857
|
+
children: items.map((item, i) => /* @__PURE__ */ jsxs("box", {
|
|
1858
|
+
style: { flexDirection: "column" },
|
|
1859
|
+
children: [i === firstActionIndex && i > 0 && /* @__PURE__ */ jsx("box", { style: {
|
|
1860
|
+
border: ["top"],
|
|
1861
|
+
borderColor: COLOR.mute,
|
|
1862
|
+
height: 1,
|
|
1863
|
+
marginTop: 1,
|
|
1864
|
+
marginBottom: 1
|
|
1865
|
+
} }), item.kind === "toggle" ? /* @__PURE__ */ jsx(ToggleRow, {
|
|
1866
|
+
label: item.label,
|
|
1867
|
+
description: item.description,
|
|
1868
|
+
enabled: settings[item.key],
|
|
1869
|
+
focused: i === safeCursor
|
|
1870
|
+
}) : /* @__PURE__ */ jsx(ActionRow, {
|
|
1871
|
+
label: item.label,
|
|
1872
|
+
description: item.description,
|
|
1873
|
+
focused: i === safeCursor
|
|
1874
|
+
})]
|
|
1875
|
+
}, item.kind === "toggle" ? item.key : item.id))
|
|
1276
1876
|
}), /* @__PURE__ */ jsxs("text", {
|
|
1277
1877
|
fg: COLOR.mute,
|
|
1278
1878
|
children: [
|
|
@@ -1285,7 +1885,7 @@ function SettingsModal() {
|
|
|
1285
1885
|
fg: COLOR.warn,
|
|
1286
1886
|
children: "↵"
|
|
1287
1887
|
}),
|
|
1288
|
-
" toggle · ",
|
|
1888
|
+
firstActionIndex >= 0 ? " toggle/select · " : " toggle · ",
|
|
1289
1889
|
/* @__PURE__ */ jsx("span", {
|
|
1290
1890
|
fg: COLOR.warn,
|
|
1291
1891
|
children: "esc"
|
|
@@ -1296,14 +1896,13 @@ function SettingsModal() {
|
|
|
1296
1896
|
});
|
|
1297
1897
|
}
|
|
1298
1898
|
/**
|
|
1299
|
-
*
|
|
1899
|
+
* Toggle row — `▶` marker · checkbox · label · description.
|
|
1300
1900
|
*
|
|
1301
1901
|
* Rendered as one `<text>` so OpenTUI's word-wrap handles narrow terminals
|
|
1302
1902
|
* automatically: on wide screens everything sits on one line; on narrow ones
|
|
1303
|
-
* the trailing description wraps under the label without breaking the row
|
|
1304
|
-
* structure.
|
|
1903
|
+
* the trailing description wraps under the label without breaking the row.
|
|
1305
1904
|
*/
|
|
1306
|
-
function
|
|
1905
|
+
function ToggleRow({ label, description, enabled, focused }) {
|
|
1307
1906
|
return /* @__PURE__ */ jsxs("text", {
|
|
1308
1907
|
fg: focused ? COLOR.brand : COLOR.dim,
|
|
1309
1908
|
children: [
|
|
@@ -1317,11 +1916,42 @@ function SettingRowView({ row, enabled, focused }) {
|
|
|
1317
1916
|
}),
|
|
1318
1917
|
/* @__PURE__ */ jsx("span", {
|
|
1319
1918
|
fg: focused ? COLOR.brand : COLOR.dim,
|
|
1320
|
-
children:
|
|
1919
|
+
children: label
|
|
1321
1920
|
}),
|
|
1322
1921
|
/* @__PURE__ */ jsx("span", {
|
|
1323
1922
|
fg: COLOR.mute,
|
|
1324
|
-
children: ` ${
|
|
1923
|
+
children: ` ${description}`
|
|
1924
|
+
})
|
|
1925
|
+
]
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Action row — cursor marker · label · description · (focus-only) trailing arrow.
|
|
1930
|
+
*
|
|
1931
|
+
* The label sits in the same column as a toggle row's `[✓]` checkbox (right
|
|
1932
|
+
* after the 2-col cursor slot). The trailing `›` only renders when focused
|
|
1933
|
+
* so it reads as a "this row runs" affordance, not a static decoration on
|
|
1934
|
+
* every action.
|
|
1935
|
+
*/
|
|
1936
|
+
function ActionRow({ label, description, focused }) {
|
|
1937
|
+
return /* @__PURE__ */ jsxs("text", {
|
|
1938
|
+
fg: focused ? COLOR.brand : COLOR.dim,
|
|
1939
|
+
children: [
|
|
1940
|
+
/* @__PURE__ */ jsx("span", {
|
|
1941
|
+
fg: focused ? COLOR.brand : COLOR.mute,
|
|
1942
|
+
children: focused ? "▶ " : " "
|
|
1943
|
+
}),
|
|
1944
|
+
/* @__PURE__ */ jsx("span", {
|
|
1945
|
+
fg: focused ? COLOR.brand : COLOR.accent,
|
|
1946
|
+
children: label
|
|
1947
|
+
}),
|
|
1948
|
+
/* @__PURE__ */ jsx("span", {
|
|
1949
|
+
fg: COLOR.mute,
|
|
1950
|
+
children: ` ${description}`
|
|
1951
|
+
}),
|
|
1952
|
+
focused && /* @__PURE__ */ jsx("span", {
|
|
1953
|
+
fg: COLOR.brand,
|
|
1954
|
+
children: " ›"
|
|
1325
1955
|
})
|
|
1326
1956
|
]
|
|
1327
1957
|
});
|
|
@@ -1544,20 +2174,20 @@ function AppShell() {
|
|
|
1544
2174
|
const sessionRef = useRef(null);
|
|
1545
2175
|
const stream = useStreamBuffer(setEvents);
|
|
1546
2176
|
const makePicked = useCallback((provider, modelId) => {
|
|
1547
|
-
const
|
|
1548
|
-
if (!
|
|
2177
|
+
const descriptor = providerRegistry[provider.key];
|
|
2178
|
+
if (!descriptor) return null;
|
|
1549
2179
|
const remembered = initialState.lastModelByProvider?.[provider.key];
|
|
1550
2180
|
return {
|
|
1551
2181
|
provider,
|
|
1552
|
-
model: modelId ?? remembered ?? factory().meta.defaultModel
|
|
2182
|
+
model: modelId ?? remembered ?? descriptor.defaultModel ?? descriptor.factory().meta.defaultModel
|
|
1553
2183
|
};
|
|
1554
2184
|
}, [providerRegistry, initialState]);
|
|
1555
2185
|
const buildAgent = useCallback((session, key) => {
|
|
1556
|
-
const
|
|
1557
|
-
if (!
|
|
2186
|
+
const descriptor = providerRegistry[key];
|
|
2187
|
+
if (!descriptor) throw new Error(`No provider registered for key "${key}"`);
|
|
1558
2188
|
const agent = createAgent({
|
|
1559
2189
|
...preset,
|
|
1560
|
-
provider: factory(),
|
|
2190
|
+
provider: descriptor.factory(),
|
|
1561
2191
|
session
|
|
1562
2192
|
});
|
|
1563
2193
|
agent.hooks.hook("stream:thinking", ({ delta }) => stream.queueStreamDelta("thinking", delta));
|
|
@@ -1799,10 +2429,14 @@ function AppShell() {
|
|
|
1799
2429
|
events.length,
|
|
1800
2430
|
stream
|
|
1801
2431
|
]);
|
|
2432
|
+
const onReauth = useCallback(() => {
|
|
2433
|
+
modal.close();
|
|
2434
|
+
setScreen("auth");
|
|
2435
|
+
}, [modal]);
|
|
1802
2436
|
useKeyboard((key) => {
|
|
1803
2437
|
if (modal.isOpen) return;
|
|
1804
2438
|
if (key.ctrl && key.name === "," && screen !== "auth") {
|
|
1805
|
-
modal.open(/* @__PURE__ */ jsx(SettingsModal, {}));
|
|
2439
|
+
modal.open(/* @__PURE__ */ jsx(SettingsModal, { actions: { onReauth } }));
|
|
1806
2440
|
return;
|
|
1807
2441
|
}
|
|
1808
2442
|
if (key.ctrl && key.name === "m" && screen === "chat" && picked && !busy) {
|
|
@@ -1821,6 +2455,10 @@ function AppShell() {
|
|
|
1821
2455
|
else renderer.destroy();
|
|
1822
2456
|
return;
|
|
1823
2457
|
}
|
|
2458
|
+
if (picked) {
|
|
2459
|
+
setScreen(currentSession ? "chat" : "sessions");
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
1824
2462
|
renderer.destroy();
|
|
1825
2463
|
});
|
|
1826
2464
|
const hints = useMemo(() => buildHints(screen, busy, currentSession), [
|
|
@@ -1830,7 +2468,9 @@ function AppShell() {
|
|
|
1830
2468
|
]);
|
|
1831
2469
|
const contextUsage = useMemo(() => {
|
|
1832
2470
|
if (screen !== "chat" || !picked) return null;
|
|
1833
|
-
const
|
|
2471
|
+
const descriptor = providerRegistry[picked.provider.key];
|
|
2472
|
+
if (!descriptor) return null;
|
|
2473
|
+
const max = getContextWindow(descriptor, picked.model);
|
|
1834
2474
|
return max ? {
|
|
1835
2475
|
used: lastInputTokens,
|
|
1836
2476
|
max
|
|
@@ -1839,7 +2479,7 @@ function AppShell() {
|
|
|
1839
2479
|
screen,
|
|
1840
2480
|
picked,
|
|
1841
2481
|
lastInputTokens,
|
|
1842
|
-
|
|
2482
|
+
providerRegistry
|
|
1843
2483
|
]);
|
|
1844
2484
|
useEffect(() => () => {
|
|
1845
2485
|
teardown();
|
|
@@ -1971,15 +2611,14 @@ let runTuiInvoked = false;
|
|
|
1971
2611
|
* to `runTui({ storageDir, prefix })`.
|
|
1972
2612
|
*
|
|
1973
2613
|
* ```ts
|
|
1974
|
-
* import { runTui } from 'zidane/tui'
|
|
2614
|
+
* import { BUILTIN_PROVIDERS, runTui } from 'zidane/tui'
|
|
1975
2615
|
* import { createRemoteStore } from 'zidane/session' // for the `store` option
|
|
1976
2616
|
*
|
|
1977
2617
|
* await runTui() // ~/.zidane/sessions.db + state.json
|
|
1978
2618
|
* await runTui({ prefix: '.myapp' }) // ~/.myapp/...
|
|
1979
2619
|
* await runTui({ storageDir: '/data', prefix: 'myapp' })
|
|
1980
|
-
* await runTui({ providers: {
|
|
2620
|
+
* await runTui({ providers: { ...BUILTIN_PROVIDERS, mine: myDescriptor } })
|
|
1981
2621
|
* await runTui({ store: createRemoteStore({ url: '…' }) })
|
|
1982
|
-
* await runTui({ models: { anthropic: [{ id: 'claude-foo', contextWindow: 200_000 }] } })
|
|
1983
2622
|
* ```
|
|
1984
2623
|
*/
|
|
1985
2624
|
async function runTui(options = {}) {
|
|
@@ -1999,6 +2638,6 @@ async function runTui(options = {}) {
|
|
|
1999
2638
|
process.exit(0);
|
|
2000
2639
|
}
|
|
2001
2640
|
//#endregion
|
|
2002
|
-
export { App, AuthScreen, COLOR, ChatScreen, ConfigProvider, DEFAULT_SETTINGS,
|
|
2641
|
+
export { App, AuthScreen, BUILTIN_PROVIDERS, COLOR, ChatScreen, ConfigProvider, DEFAULT_SETTINGS, Footer, MD_STYLE, Modal, ModalRoot, ModelPickerModal, SELECT_THEME, SessionsScreen, SettingsModal, SettingsProvider, Spinner, Transcript, ageString, anthropicDescriptor, applyApiKeyEnv, cerebrasDescriptor, createStateStore, createTuiStore, credKeyOf, credentialsPath, detectAuth, eventsFromTurns, finalizeStreamingMarkdown, finalizeStreamingMarkdownForOwner, fmtTokens, getContextWindow, lastContextSizeFromTurns, listSessionMeta, loadState, marginTopFor, modelsForDescriptor, onInputSubmit, openaiDescriptor, openrouterDescriptor, piIdOf, readCredentials, readProviderCredential, removeProviderCredential, resolveConfig, runOAuthLogin, runTui, saveState, setProviderCredential, shortId, supportsOAuth, titleFromTurns, toolCallPreview, toolResultText, turnContextSize, useConfig, useModal, useModalAwareFocus, useSettings, useStreamBuffer, writeCredentials };
|
|
2003
2642
|
|
|
2004
2643
|
//# sourceMappingURL=tui.js.map
|