zidane 4.1.6 → 4.1.7

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.
@@ -0,0 +1,1713 @@
1
+ import { n as toolResultToText } from "./types-Bx_F8jet.js";
2
+ import { n as formatTokenUsage } from "./stats-BT9l57RS.js";
3
+ import { r as basic_default } from "./presets-BzkJDW1K.js";
4
+ import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-CCDvIXGJ.js";
5
+ import { createSqliteStore } from "./session/sqlite.js";
6
+ import { spawn } from "node:child_process";
7
+ import { dirname, resolve } from "node:path";
8
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { anthropicOAuthProvider, openaiCodexOAuthProvider } from "@mariozechner/pi-ai/oauth";
11
+ import { getModel, getModels } from "@mariozechner/pi-ai";
12
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
13
+ import { jsx } from "@opentui/react/jsx-runtime";
14
+ //#region src/chat/providers.ts
15
+ /** Convenience accessor — returns `credentialFileKey ?? key`. */
16
+ function credKeyOf(desc) {
17
+ return desc.credentialFileKey ?? desc.key;
18
+ }
19
+ /** Convenience accessor — returns `piProviderId ?? key`. */
20
+ function piIdOf(desc) {
21
+ return desc.piProviderId ?? desc.key;
22
+ }
23
+ const anthropicDescriptor = {
24
+ key: "anthropic",
25
+ label: "Anthropic",
26
+ factory: anthropic,
27
+ defaultModel: "claude-opus-4-7",
28
+ envKey: "ANTHROPIC_API_KEY",
29
+ apiKeyPlaceholder: "sk-ant-…",
30
+ oauthProvider: anthropicOAuthProvider,
31
+ oauthHint: "Claude Pro/Max subscription"
32
+ };
33
+ const openaiDescriptor = {
34
+ key: "openai",
35
+ label: "OpenAI Codex",
36
+ factory: openai,
37
+ defaultModel: "gpt-5.4",
38
+ envKey: "OPENAI_CODEX_API_KEY",
39
+ credentialFileKey: "openai-codex",
40
+ piProviderId: "openai-codex",
41
+ apiKeyPlaceholder: "sk-… or eyJ… (Codex)",
42
+ oauthProvider: openaiCodexOAuthProvider
43
+ };
44
+ const openrouterDescriptor = {
45
+ key: "openrouter",
46
+ label: "OpenRouter",
47
+ factory: openrouter,
48
+ defaultModel: "anthropic/claude-sonnet-4-6",
49
+ envKey: "OPENROUTER_API_KEY",
50
+ apiKeyPlaceholder: "sk-or-…"
51
+ };
52
+ const cerebrasDescriptor = {
53
+ key: "cerebras",
54
+ label: "Cerebras",
55
+ factory: cerebras,
56
+ defaultModel: "zai-glm-4.7",
57
+ envKey: "CEREBRAS_API_KEY",
58
+ apiKeyPlaceholder: "csk-…"
59
+ };
60
+ /**
61
+ * Default provider registry. Passed verbatim when `runTui` is invoked without
62
+ * an explicit `providers` option. Hosts that want to override per-provider
63
+ * metadata can spread this and replace specific entries:
64
+ *
65
+ * ```ts
66
+ * runTui({ providers: { ...BUILTIN_PROVIDERS, anthropic: myOwnAnthropicDescriptor } })
67
+ * ```
68
+ */
69
+ const BUILTIN_PROVIDERS = {
70
+ anthropic: anthropicDescriptor,
71
+ openai: openaiDescriptor,
72
+ openrouter: openrouterDescriptor,
73
+ cerebras: cerebrasDescriptor
74
+ };
75
+ /**
76
+ * Resolve the model list for a given provider. Honors `descriptor.models`
77
+ * when set; otherwise queries pi-ai via `descriptor.piProviderId`. Returns
78
+ * `[]` for descriptors with no known mapping (custom providers without a
79
+ * model list) — callers should hide the model picker in that case.
80
+ */
81
+ function modelsForDescriptor(descriptor) {
82
+ if (descriptor.models) return descriptor.models;
83
+ try {
84
+ return getModels(piIdOf(descriptor));
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+ /**
90
+ * Look up the model's max context window via the descriptor's model source.
91
+ * Returns `null` when the model isn't known (custom slugs, providers without
92
+ * a registry); callers should hide the context indicator in that case.
93
+ */
94
+ function getContextWindow(descriptor, modelId) {
95
+ if (descriptor.models) return descriptor.models.find((m) => m.id === modelId)?.contextWindow ?? null;
96
+ try {
97
+ return getModel(piIdOf(descriptor), modelId)?.contextWindow ?? null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ //#endregion
103
+ //#region src/chat/credentials.ts
104
+ /** POSIX mode for the credentials file. Ignored on Windows. */
105
+ const FILE_MODE = 384;
106
+ /**
107
+ * Resolve the credentials file path given the resolved TUI data directory
108
+ * (typically `~/.zidane`, i.e. `config.paths.dir`).
109
+ *
110
+ * Matches the convention used elsewhere in the TUI (sessions.db, state.json)
111
+ * so a single `ZIDANE_STORAGE_DIR` override moves the entire data root.
112
+ */
113
+ function credentialsPath(dataDir) {
114
+ return resolve(dataDir, "credentials.json");
115
+ }
116
+ /**
117
+ * Read credentials from disk.
118
+ *
119
+ * Returns `{}` when the file is missing or corrupt (last-ditch tolerance —
120
+ * a hand-edit gone wrong shouldn't lock the user out of re-authing). On first
121
+ * call with no file present, attempts a migration from `cwd/.credentials.json`
122
+ * (the legacy location used by `bun run auth`).
123
+ */
124
+ function readCredentials(dataDir) {
125
+ const path = credentialsPath(dataDir);
126
+ if (!existsSync(path)) {
127
+ const migrated = migrateLegacyFile(path);
128
+ if (migrated) return migrated;
129
+ return {};
130
+ }
131
+ try {
132
+ const raw = readFileSync(path, "utf-8");
133
+ const parsed = JSON.parse(raw);
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
135
+ return parsed;
136
+ } catch {
137
+ return {};
138
+ }
139
+ }
140
+ /** Read a single provider's credential (translating via the descriptor). */
141
+ function readProviderCredential(dataDir, descriptor) {
142
+ return readCredentials(dataDir)[credKeyOf(descriptor)];
143
+ }
144
+ /**
145
+ * Write credentials atomically (write-then-rename) with mode 0o600.
146
+ *
147
+ * Atomic on the same filesystem — readers either see the previous file or the
148
+ * new one, never a half-written intermediate. Creates the parent dir if needed
149
+ * (first launch on a fresh machine: `~/.zidane/` may not exist yet).
150
+ */
151
+ function writeCredentials(dataDir, creds) {
152
+ const path = credentialsPath(dataDir);
153
+ mkdirSync(dirname(path), { recursive: true });
154
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
155
+ writeFileSync(tmp, `${JSON.stringify(creds, null, 2)}\n`, { mode: FILE_MODE });
156
+ renameSync(tmp, path);
157
+ }
158
+ function setProviderCredential(dataDir, descriptor, cred) {
159
+ const all = readCredentials(dataDir);
160
+ all[credKeyOf(descriptor)] = cred;
161
+ writeCredentials(dataDir, all);
162
+ }
163
+ function removeProviderCredential(dataDir, descriptor) {
164
+ const all = readCredentials(dataDir);
165
+ const fileKey = credKeyOf(descriptor);
166
+ if (!(fileKey in all)) return;
167
+ delete all[fileKey];
168
+ writeCredentials(dataDir, all);
169
+ }
170
+ /**
171
+ * Inject API-key credentials into `process.env` so the harness providers pick
172
+ * them up via their existing env-var resolution. Called once at TUI launch
173
+ * after the credentials file has been resolved. OAuth credentials are NOT
174
+ * injected — those reach providers via `ZIDANE_CREDENTIALS_PATH` + the file
175
+ * reader in `src/providers/oauth.ts`.
176
+ *
177
+ * Does not overwrite env vars that are already set — explicit user-provided
178
+ * env values win over stored API keys.
179
+ *
180
+ * Descriptors without an `envKey` (OAuth-only providers, custom providers
181
+ * that bypass env-var resolution) are skipped silently.
182
+ */
183
+ function applyApiKeyEnv(dataDir, registry) {
184
+ const creds = readCredentials(dataDir);
185
+ for (const descriptor of Object.values(registry)) {
186
+ if (!descriptor.envKey || process.env[descriptor.envKey]) continue;
187
+ const cred = creds[credKeyOf(descriptor)];
188
+ if (cred?.kind === "apikey" && cred.value) process.env[descriptor.envKey] = cred.value;
189
+ }
190
+ }
191
+ /**
192
+ * `bun run auth` (pre-TUI) wrote `cwd/.credentials.json` with an entry per
193
+ * provider mapping directly to an OAuthCredentials payload, e.g.:
194
+ *
195
+ * {
196
+ * "anthropic": { "access": "...", "refresh": "...", "expires": 123 },
197
+ * "openai-codex": { "access": "...", "refresh": "...", "expires": 123, "accountId": "..." }
198
+ * }
199
+ *
200
+ * We don't delete the legacy file — it might still be used by a host that
201
+ * imports the harness directly. We just copy its contents into the new
202
+ * location under the kind-tagged shape so the TUI picks them up.
203
+ *
204
+ * Migration is provider-agnostic: any top-level entry with an `access` field
205
+ * is preserved verbatim (extras included), under the same key. The TUI's
206
+ * detection then looks them up via the matching descriptor's `credentialFileKey`.
207
+ *
208
+ * Returns the migrated credentials when the migration ran, or `null` when
209
+ * there's no legacy file to migrate.
210
+ */
211
+ function migrateLegacyFile(targetPath) {
212
+ const legacyPath = resolve(process.cwd(), ".credentials.json");
213
+ if (!existsSync(legacyPath)) return null;
214
+ let legacy;
215
+ try {
216
+ legacy = JSON.parse(readFileSync(legacyPath, "utf-8"));
217
+ } catch {
218
+ return null;
219
+ }
220
+ if (!legacy || typeof legacy !== "object" || Array.isArray(legacy)) return null;
221
+ const migrated = {};
222
+ for (const [fileKey, value] of Object.entries(legacy)) {
223
+ if (!isOAuthLegacy(value)) continue;
224
+ const { access, refresh, expires, ...extras } = value;
225
+ migrated[fileKey] = {
226
+ kind: "oauth",
227
+ access,
228
+ ...typeof refresh === "string" ? { refresh } : {},
229
+ ...typeof expires === "number" ? { expires } : {},
230
+ ...extras
231
+ };
232
+ }
233
+ if (Object.keys(migrated).length === 0) return null;
234
+ mkdirSync(dirname(targetPath), { recursive: true });
235
+ const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
236
+ writeFileSync(tmp, `${JSON.stringify(migrated, null, 2)}\n`, { mode: FILE_MODE });
237
+ renameSync(tmp, targetPath);
238
+ return migrated;
239
+ }
240
+ function isOAuthLegacy(value) {
241
+ return typeof value === "object" && value !== null && "access" in value && typeof value.access === "string";
242
+ }
243
+ //#endregion
244
+ //#region src/chat/auth.ts
245
+ /**
246
+ * Detect available auth for every registered provider.
247
+ *
248
+ * Resolution order per provider (a method appears in `methods` for each
249
+ * layer that has a credential — the agent itself resolves them in the same
250
+ * order via its provider factories):
251
+ *
252
+ * 1. `kind: 'apikey'` from `credentials.json` (injected into env at TUI launch)
253
+ * 2. explicit env var (descriptor's `envKey`)
254
+ * 3. `kind: 'oauth'` from `credentials.json` (or legacy `cwd/.credentials.json`)
255
+ *
256
+ * Pure read — never refreshes or rewrites the credentials file.
257
+ */
258
+ function detectAuth(dataDir, registry, env = process.env) {
259
+ const creds = readCredentials(dataDir);
260
+ return Object.values(registry).map((descriptor) => {
261
+ const methods = [];
262
+ const fileEntry = creds[credKeyOf(descriptor)];
263
+ if (fileEntry?.kind === "apikey" && fileEntry.value) methods.push({
264
+ source: "apikey",
265
+ detail: "credentials.json"
266
+ });
267
+ if (descriptor.envKey && env[descriptor.envKey]) methods.push({
268
+ source: "env",
269
+ detail: descriptor.envKey
270
+ });
271
+ if (fileEntry?.kind === "oauth" && fileEntry.access) {
272
+ const detail = typeof fileEntry.expires === "number" ? `oauth · expires ${new Date(fileEntry.expires).toLocaleString()}` : "oauth · credentials.json";
273
+ methods.push({
274
+ source: "oauth",
275
+ detail
276
+ });
277
+ }
278
+ return {
279
+ key: descriptor.key,
280
+ label: descriptor.label,
281
+ available: methods.length > 0,
282
+ methods
283
+ };
284
+ });
285
+ }
286
+ //#endregion
287
+ //#region src/chat/store.ts
288
+ function ensureDir$1(path) {
289
+ const dir = dirname(path);
290
+ if (existsSync(dir)) return;
291
+ try {
292
+ mkdirSync(dir, { recursive: true });
293
+ } catch (err) {
294
+ const message = err instanceof Error ? err.message : String(err);
295
+ throw new Error(`Could not create TUI storage directory at "${dir}". Override the location via \`runTui({ storageDir, prefix })\` or the \`ZIDANE_STORAGE_DIR\` env var. Original error: ${message}`);
296
+ }
297
+ }
298
+ function createTuiStore(dbPath) {
299
+ ensureDir$1(dbPath);
300
+ return createSqliteStore({ path: dbPath });
301
+ }
302
+ function createStateStore(path) {
303
+ return {
304
+ load: () => loadState(path),
305
+ save: (state) => saveState(path, state)
306
+ };
307
+ }
308
+ function loadState(path) {
309
+ if (!existsSync(path)) return {};
310
+ try {
311
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
312
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
313
+ } catch {}
314
+ return {};
315
+ }
316
+ function saveState(path, state) {
317
+ ensureDir$1(path);
318
+ const tmp = `${path}.${process.pid}.tmp`;
319
+ writeFileSync(tmp, JSON.stringify(state, null, 2));
320
+ renameSync(tmp, path);
321
+ }
322
+ /**
323
+ * Load every session and project it to the compact `SessionMeta` shape used by
324
+ * the picker. Sorted by recency via the underlying store's `list()` contract
325
+ * (sqlite store returns by `updated_at DESC`).
326
+ */
327
+ async function listSessionMeta(store) {
328
+ const ids = await store.list();
329
+ return (await Promise.all(ids.map(async (id) => {
330
+ const data = await store.load(id);
331
+ if (!data) return null;
332
+ return {
333
+ id,
334
+ title: titleFromTurns(data.turns) ?? "untitled",
335
+ turnCount: data.turns.length,
336
+ updatedAt: data.updatedAt
337
+ };
338
+ }))).filter((m) => m !== null);
339
+ }
340
+ /** Derive a short title from the first user message — returns null when empty. */
341
+ function titleFromTurns(turns) {
342
+ const first = turns.find((t) => t.role === "user");
343
+ if (!first) return null;
344
+ for (const block of first.content) if (block.type === "text" && block.text.trim()) {
345
+ const oneLine = block.text.replace(/\s+/g, " ").trim();
346
+ return oneLine.length > 60 ? `${oneLine.slice(0, 60)}…` : oneLine;
347
+ }
348
+ return null;
349
+ }
350
+ /**
351
+ * Replay persisted turns as a viewable transcript. Mirrors the event shape
352
+ * produced live by the agent hooks so loaded and streaming history render
353
+ * identically — including subagent ancestry when `runs` is supplied.
354
+ *
355
+ * Subagent reconstruction:
356
+ * - Every turn carries a `runId`. We look that up in `runs` to get the
357
+ * run's `depth` and tag the resulting events with `{ depth, childId }`
358
+ * — the same shape the live `child:*` bubble hooks produce.
359
+ * - We synthesize `spawn-start` / `spawn-end` markers at each child-run
360
+ * boundary so the transcript reads the same as a live run did
361
+ * (`🌱 [run-id] task` … child events … `🌳 [run-id] done · tokens`).
362
+ * - For child runs (`depth > 0`), the user-role "task" text is suppressed
363
+ * because `spawn-start` already shows it.
364
+ *
365
+ * Without `runs` (legacy callers / tests), the function falls back to the
366
+ * old behavior: depth-0 events with no subagent grouping.
367
+ */
368
+ function eventsFromTurns(turns, runs = []) {
369
+ const runById = /* @__PURE__ */ new Map();
370
+ for (const run of runs) runById.set(run.id, run);
371
+ const childLabelByRunId = /* @__PURE__ */ new Map();
372
+ runs.filter((r) => (r.depth ?? 0) > 0).slice().sort((a, b) => a.startedAt - b.startedAt).forEach((r, i) => childLabelByRunId.set(r.id, `child-${i + 1}`));
373
+ const labelFor = (runId) => childLabelByRunId.get(runId) ?? runId;
374
+ const toolByCallId = /* @__PURE__ */ new Map();
375
+ for (const turn of turns) {
376
+ if (turn.role !== "assistant") continue;
377
+ for (const block of turn.content) if (block.type === "tool_call") toolByCallId.set(block.id, block.name);
378
+ }
379
+ const events = [];
380
+ let lastRunId;
381
+ let lastDepth = 0;
382
+ const closeRun = (runId, depth) => {
383
+ if (!runId || depth <= 0) return;
384
+ const run = runById.get(runId);
385
+ if (!run) return;
386
+ const tag = run.status === "aborted" || run.status === "error" ? run.status : "done";
387
+ const usage = formatTokenUsage({
388
+ totalIn: run.tokensIn ?? run.totalUsage?.input ?? 0,
389
+ totalOut: run.tokensOut ?? run.totalUsage?.output ?? 0,
390
+ totalCacheRead: run.totalUsage?.cacheRead ?? 0,
391
+ totalCacheCreation: run.totalUsage?.cacheCreation ?? 0
392
+ });
393
+ events.push({
394
+ kind: "spawn-end",
395
+ text: `${tag} ${usage}`,
396
+ childId: labelFor(runId),
397
+ depth
398
+ });
399
+ };
400
+ const openRun = (runId, depth) => {
401
+ if (depth <= 0) return;
402
+ const run = runById.get(runId);
403
+ if (!run) return;
404
+ const taskPreview = run.prompt.length > 80 ? `${run.prompt.slice(0, 80)}…` : run.prompt;
405
+ events.push({
406
+ kind: "spawn-start",
407
+ text: taskPreview,
408
+ childId: labelFor(runId),
409
+ depth
410
+ });
411
+ };
412
+ for (let i = 0; i < turns.length; i++) {
413
+ const turn = turns[i];
414
+ const depth = (turn.runId ? runById.get(turn.runId) : void 0)?.depth ?? 0;
415
+ const tag = depth > 0 && turn.runId ? {
416
+ childId: labelFor(turn.runId),
417
+ depth
418
+ } : void 0;
419
+ if (turn.runId !== lastRunId) {
420
+ closeRun(lastRunId, lastDepth);
421
+ if (depth === 0 && lastDepth === 0 && i > 0) events.push({
422
+ kind: "separator",
423
+ text: ""
424
+ });
425
+ if (turn.runId) openRun(turn.runId, depth);
426
+ lastRunId = turn.runId;
427
+ lastDepth = depth;
428
+ } else if (i > 0 && depth === 0) events.push({
429
+ kind: "separator",
430
+ text: ""
431
+ });
432
+ if (turn.role === "user") {
433
+ for (const block of turn.content) if (block.type === "text" && block.text.trim()) {
434
+ if (depth === 0) events.push({
435
+ kind: "info",
436
+ text: `❯ ${block.text}`
437
+ });
438
+ } else if (block.type === "tool_result") {
439
+ const tool = toolByCallId.get(block.callId);
440
+ const raw = toolResultText(block.output);
441
+ const text = tool === "spawn" ? stripSpawnTokensLine(raw) : raw;
442
+ events.push({
443
+ kind: "tool-result",
444
+ text,
445
+ ...tool ? { tool } : {},
446
+ ...tag
447
+ });
448
+ }
449
+ continue;
450
+ }
451
+ if (turn.role === "assistant") {
452
+ for (const block of turn.content) if (block.type === "text" && block.text.trim()) events.push({
453
+ kind: "markdown",
454
+ text: block.text,
455
+ streaming: false,
456
+ ...tag
457
+ });
458
+ else if (block.type === "tool_call") events.push({
459
+ kind: "tool",
460
+ text: toolCallPreview(block.name, block.input),
461
+ tool: block.name,
462
+ ...tag
463
+ });
464
+ }
465
+ }
466
+ closeRun(lastRunId, lastDepth);
467
+ return events;
468
+ }
469
+ /** Shared formatter for the `↳ name(args)` line shown on tool calls. */
470
+ function toolCallPreview(name, input) {
471
+ const args = JSON.stringify(input);
472
+ return args && args !== "{}" ? `${name}(${args})` : name;
473
+ }
474
+ /** Render tool output as plain text, whether it's a string or structured content. */
475
+ function toolResultText(output) {
476
+ return typeof output === "string" ? output : toolResultToText(output);
477
+ }
478
+ /**
479
+ * Strip the `Tokens: …` line from a spawn tool-result. The spawn-end marker
480
+ * displayed right above already shows the same stats; keeping the line in the
481
+ * rendered tool-result body just produces a visible duplicate (and, on
482
+ * reloaded pre-fix sessions, an *inconsistent* duplicate — the persisted line
483
+ * uses the old `13 in / 4075 out` shape while the freshly synthesized
484
+ * spawn-end uses the cache-aware `in 92615 (cache 92602) / 4075 out` shape).
485
+ *
486
+ * Display-only: the persisted tool_result content is untouched, so the LLM
487
+ * still sees the full string in its context window. Anchored to start-of-line
488
+ * and matches both `Tokens: 13 in / 4075 out` (legacy) and `Tokens: in 13 …`
489
+ * (post-`formatTokenUsage`) shapes.
490
+ */
491
+ function stripSpawnTokensLine(text) {
492
+ return text.replace(/^Tokens:[^\n]*\n?/m, "");
493
+ }
494
+ /** Effective context size of the most recent assistant turn — drives the footer indicator. */
495
+ function lastContextSizeFromTurns(turns) {
496
+ for (let i = turns.length - 1; i >= 0; i--) {
497
+ const turn = turns[i];
498
+ if (turn.role === "assistant" && turn.usage) return (turn.usage.input ?? 0) + (turn.usage.cacheRead ?? 0) + (turn.usage.cacheCreation ?? 0);
499
+ }
500
+ return 0;
501
+ }
502
+ //#endregion
503
+ //#region src/chat/config.ts
504
+ /** Resolve user options into a fully-bound runtime config. Pure aside from disk reads. */
505
+ function resolveConfig(options = {}) {
506
+ const prefix = options.prefix ?? process.env.ZIDANE_PREFIX ?? ".zidane";
507
+ const storageDir = options.storageDir ?? process.env.ZIDANE_STORAGE_DIR ?? homedir();
508
+ const dir = resolve(storageDir, prefix);
509
+ const paths = {
510
+ dir,
511
+ db: resolve(dir, "sessions.db"),
512
+ state: resolve(dir, "state.json")
513
+ };
514
+ const store = options.store ?? createTuiStore(paths.db);
515
+ const stateStore = createStateStore(paths.state);
516
+ const initialState = stateStore.load();
517
+ const providers = options.providers ?? BUILTIN_PROVIDERS;
518
+ const preset = options.preset ?? basic_default;
519
+ process.env.ZIDANE_CREDENTIALS_PATH = credentialsPath(dir);
520
+ applyApiKeyEnv(dir, providers);
521
+ const modelsFor = makeModelsResolver(providers);
522
+ const resumeProvider = resolveResumeProvider(initialState, providers, dir);
523
+ const initialPicked = resumeProvider ? pickInitial(resumeProvider, providers, initialState) : null;
524
+ return {
525
+ prefix,
526
+ storageDir,
527
+ paths,
528
+ providers,
529
+ preset,
530
+ store,
531
+ stateStore,
532
+ modelsFor,
533
+ initialState,
534
+ initialSettings: initialState.settings ?? {},
535
+ resumeProvider,
536
+ initialPicked
537
+ };
538
+ }
539
+ function makeModelsResolver(registry) {
540
+ return (key) => {
541
+ const descriptor = registry[key];
542
+ return descriptor ? modelsForDescriptor(descriptor) : [];
543
+ };
544
+ }
545
+ function resolveResumeProvider(state, providers, storageDir) {
546
+ if (!state.lastProvider) return null;
547
+ if (!providers[state.lastProvider]) return null;
548
+ return detectAuth(storageDir, providers).find((p) => p.key === state.lastProvider && p.available) ?? null;
549
+ }
550
+ function pickInitial(auth, providers, state) {
551
+ const descriptor = providers[auth.key];
552
+ if (!descriptor) return null;
553
+ const model = state.lastModelByProvider?.[auth.key] ?? descriptor.defaultModel ?? safeFactoryDefault(descriptor);
554
+ return model ? {
555
+ provider: auth,
556
+ model
557
+ } : null;
558
+ }
559
+ function safeFactoryDefault(descriptor) {
560
+ try {
561
+ return descriptor.factory().meta.defaultModel;
562
+ } catch {
563
+ return;
564
+ }
565
+ }
566
+ //#endregion
567
+ //#region src/chat/config-context.tsx
568
+ const ConfigContext = createContext(null);
569
+ function ConfigProvider({ config, children }) {
570
+ return /* @__PURE__ */ jsx(ConfigContext.Provider, {
571
+ value: config,
572
+ children
573
+ });
574
+ }
575
+ function useConfig() {
576
+ const ctx = useContext(ConfigContext);
577
+ if (!ctx) throw new Error("useConfig must be used inside <ConfigProvider>");
578
+ return ctx;
579
+ }
580
+ //#endregion
581
+ //#region src/chat/format.ts
582
+ /** Compact token formatter — 12_415 → "12.4k", 1_234_567 → "1.23M". */
583
+ function fmtTokens(n) {
584
+ if (n < 1e3) return String(n);
585
+ if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 2 : 1)}k`;
586
+ return `${(n / 1e6).toFixed(2)}M`;
587
+ }
588
+ /** Compact relative-time formatter — "just now / 5m / 3h / 2d". */
589
+ function ageString(ts, now = Date.now()) {
590
+ const m = Math.floor((now - ts) / 6e4);
591
+ if (m < 1) return "just now";
592
+ if (m < 60) return `${m}m ago`;
593
+ const h = Math.floor(m / 60);
594
+ if (h < 24) return `${h}h ago`;
595
+ return `${Math.floor(h / 24)}d ago`;
596
+ }
597
+ /** Six-char short form of a session id for headers and lists. */
598
+ function shortId(id) {
599
+ return id.replace(/-/g, "").slice(0, 6);
600
+ }
601
+ //#endregion
602
+ //#region src/chat/oauth.ts
603
+ function supportsOAuth(descriptor) {
604
+ return descriptor.oauthProvider !== void 0;
605
+ }
606
+ /**
607
+ * Run the OAuth login flow for a provider.
608
+ *
609
+ * Returns the OAuth credentials on success; caller persists them via
610
+ * `setProviderCredential(dataDir, descriptor, { kind: 'oauth', ...credentials })`.
611
+ * Throws when the descriptor has no `oauthProvider` configured.
612
+ */
613
+ async function runOAuthLogin(descriptor, options) {
614
+ if (!descriptor.oauthProvider) throw new Error(`OAuth not supported for ${descriptor.label} (${descriptor.key}) — use an API key instead.`);
615
+ const callbacks = {
616
+ onAuth: (info) => {
617
+ options.onUrl(info.url, info.instructions);
618
+ tryOpenBrowser(info.url);
619
+ },
620
+ onPrompt: async () => {
621
+ if (!options.onCodeRequest) throw new Error("OAuth flow requires manual code input but no handler is wired.");
622
+ return options.onCodeRequest();
623
+ },
624
+ onProgress: options.onProgress,
625
+ signal: options.signal
626
+ };
627
+ return descriptor.oauthProvider.login(callbacks);
628
+ }
629
+ /**
630
+ * Best-effort cross-platform browser open. macOS uses `open`, Linux uses
631
+ * `xdg-open`, Windows uses `start`. Failures are swallowed — the callback
632
+ * server is already listening, and the URL is displayed in the TUI for
633
+ * manual click.
634
+ *
635
+ * Uses `spawn` (not `exec`) so the URL is passed as an argv element rather
636
+ * than interpolated into a shell command — no need to think about quoting
637
+ * URLs that contain `&`, `?`, `"` or other shell metacharacters.
638
+ */
639
+ function tryOpenBrowser(url) {
640
+ const [cmd, ...args] = (() => {
641
+ if (process.platform === "darwin") return ["open", url];
642
+ if (process.platform === "win32") return [
643
+ "cmd",
644
+ "/c",
645
+ "start",
646
+ "",
647
+ url
648
+ ];
649
+ return ["xdg-open", url];
650
+ })();
651
+ try {
652
+ const child = spawn(cmd, args, {
653
+ stdio: "ignore",
654
+ detached: true
655
+ });
656
+ child.on("error", () => {});
657
+ child.unref();
658
+ } catch {}
659
+ }
660
+ //#endregion
661
+ //#region src/chat/safe-mode.ts
662
+ /**
663
+ * Safe-mode storage + matching for the TUI.
664
+ *
665
+ * Lives at `<dataDir>/projects.json` (default `~/.zidane/projects.json`). Each
666
+ * top-level key is an absolute project directory; the value carries that
667
+ * project's persisted tool-call `safelist`.
668
+ *
669
+ * ```json
670
+ * {
671
+ * "/Users/me/proj-a": { "safelist": ["read_file", "shell:git:*"] }
672
+ * }
673
+ * ```
674
+ *
675
+ * Two granularities for safelist entries:
676
+ * - **bare tool name** — `"read_file"` matches every `read_file` call.
677
+ * - **tool + first-arg token + wildcard** — `"shell:git:*"` matches `shell`
678
+ * calls whose primary string argument starts with the token `git`
679
+ * (followed by whitespace or end-of-string). Modelled on Claude Code's
680
+ * `Bash(git:*)` syntax.
681
+ *
682
+ * A short list of read-only tools is **implicitly safe** without being
683
+ * persisted — see {@link IMPLICITLY_SAFE_TOOLS}.
684
+ */
685
+ /** Resolve `projects.json`'s on-disk path given the TUI data directory. */
686
+ function projectsFilePath(dataDir) {
687
+ return resolve(dataDir, "projects.json");
688
+ }
689
+ function readProjects(dataDir) {
690
+ const path = projectsFilePath(dataDir);
691
+ if (!existsSync(path)) return {};
692
+ try {
693
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
694
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
695
+ } catch {}
696
+ return {};
697
+ }
698
+ function ensureDir(path) {
699
+ const dir = dirname(path);
700
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
701
+ }
702
+ /** Atomic write — tmp + rename so a crash never leaves a half-file. */
703
+ function writeProjects(dataDir, file) {
704
+ const path = projectsFilePath(dataDir);
705
+ ensureDir(path);
706
+ const tmp = `${path}.${process.pid}.tmp`;
707
+ writeFileSync(tmp, JSON.stringify(file, null, 2));
708
+ renameSync(tmp, path);
709
+ }
710
+ /**
711
+ * Append `entry` to the safelist for `projectDir`, dedup-aware. Returns the
712
+ * updated entry list (post-write) so callers can render it without re-reading.
713
+ */
714
+ function addToSafelist(dataDir, projectDir, entry) {
715
+ const file = readProjects(dataDir);
716
+ const existing = file[projectDir]?.safelist ?? [];
717
+ if (existing.includes(entry)) return existing;
718
+ const next = [...existing, entry];
719
+ file[projectDir] = {
720
+ ...file[projectDir],
721
+ safelist: next
722
+ };
723
+ writeProjects(dataDir, file);
724
+ return next;
725
+ }
726
+ /** Read the safelist for one project. Returns `[]` for unknown projects. */
727
+ function getSafelist(dataDir, projectDir) {
728
+ return readProjects(dataDir)[projectDir]?.safelist ?? [];
729
+ }
730
+ /**
731
+ * Tools that always pass without prompting — pure file/dir reads with no
732
+ * side effects. Users who want to gate them must disable safe-mode entirely
733
+ * (or fork this list in their own embedding).
734
+ */
735
+ const IMPLICITLY_SAFE_TOOLS = [
736
+ "read_file",
737
+ "list_files",
738
+ "glob",
739
+ "grep"
740
+ ];
741
+ /** Common input keys carrying the "primary argument" we scope safelists on. */
742
+ const PRIMARY_ARG_KEYS = [
743
+ "command",
744
+ "path",
745
+ "pattern",
746
+ "query"
747
+ ];
748
+ function primaryArgValue(input) {
749
+ for (const key of PRIMARY_ARG_KEYS) {
750
+ const v = input[key];
751
+ if (typeof v === "string" && v.length > 0) return v;
752
+ }
753
+ return "";
754
+ }
755
+ /** Extract the first whitespace-delimited token of the primary arg. */
756
+ function primaryArgToken(input) {
757
+ return primaryArgValue(input).split(/\s+/)[0] ?? "";
758
+ }
759
+ /**
760
+ * Shell metacharacters that turn a single command into a compound: pipes,
761
+ * sequencing, redirects, substitutions, line breaks, subshells. A `shell:git:*`
762
+ * entry is meant to greenlight "any git invocation" — without this guard,
763
+ * `git status && rm -rf /` would tokenize to `git` and pass the safelist
764
+ * unchallenged. Reject any command that's not a single program call.
765
+ *
766
+ * The regex is intentionally generous: false positives (e.g. `echo "hi & bye"`)
767
+ * just prompt the user again, which is the safe failure mode.
768
+ */
769
+ const SHELL_COMPOUND_RE = /[;&|<>`$\n\r()]/;
770
+ function isCompoundShellCommand(command) {
771
+ return SHELL_COMPOUND_RE.test(command);
772
+ }
773
+ /**
774
+ * Test whether a `{ tool, input }` pair is covered by one safelist entry.
775
+ *
776
+ * Supported entry shapes:
777
+ * - `"<tool>"` — broad match on tool name. For `shell` this still requires
778
+ * a single-program command (compound forms always prompt).
779
+ * - `"<tool>:<token>:*"` — match when the primary arg's first token equals
780
+ * `<token>`. For `shell`, also requires the command to be free of
781
+ * metacharacters (`;`, `&&`, `||`, `|`, `$(`, backticks, `>`, `<`,
782
+ * newlines, subshells) — otherwise a `shell:git:*` entry would silently
783
+ * greenlight `git status && rm -rf /`.
784
+ *
785
+ * Entries that don't fit either shape are ignored (forward-compat for future
786
+ * pattern syntax — readers shouldn't choke on entries written by a newer
787
+ * version of the TUI).
788
+ */
789
+ function matchesSafelistEntry(entry, tool, input) {
790
+ if (tool === "shell") {
791
+ if (isCompoundShellCommand(typeof input.command === "string" ? input.command : "")) return false;
792
+ }
793
+ if (entry === tool) return true;
794
+ const sep = entry.indexOf(":");
795
+ if (sep <= 0) return false;
796
+ if (entry.slice(0, sep) !== tool) return false;
797
+ const scope = entry.slice(sep + 1);
798
+ if (scope.endsWith(":*")) return primaryArgToken(input) === scope.slice(0, -2);
799
+ return false;
800
+ }
801
+ /** True when a call matches ANY entry in the project's safelist (or is implicitly safe). */
802
+ function isOnSafelist(entries, tool, input) {
803
+ if (IMPLICITLY_SAFE_TOOLS.includes(tool)) return true;
804
+ return entries.some((e) => matchesSafelistEntry(e, tool, input));
805
+ }
806
+ /**
807
+ * Suggest the safelist entry to write when the user picks "accept and
808
+ * remember" for a `{ tool, input }`. Heuristic:
809
+ *
810
+ * - `shell` → scope by first command token (`shell:git:*`).
811
+ * - anything else → bare tool name (broad).
812
+ *
813
+ * Returning a string ensures the UI always has a concrete entry to display
814
+ * as the button label.
815
+ */
816
+ function suggestSafelistEntry(tool, input) {
817
+ if (tool === "shell") {
818
+ const token = primaryArgToken(input);
819
+ if (token) return `${tool}:${token}:*`;
820
+ }
821
+ return tool;
822
+ }
823
+ //#endregion
824
+ //#region src/chat/safe-mode-context.tsx
825
+ const SafeModeQueueContext = createContext([]);
826
+ const SafeModeActionsContext = createContext(null);
827
+ let approvalIdCounter = 0;
828
+ function nextApprovalId() {
829
+ approvalIdCounter += 1;
830
+ return `approval-${approvalIdCounter}`;
831
+ }
832
+ /**
833
+ * Owns the queue + actions. Splits the value across two contexts so a queue
834
+ * change doesn't invalidate every callback memo that closes over the actions.
835
+ */
836
+ function SafeModeProvider({ children }) {
837
+ const [queue, setQueue] = useState([]);
838
+ const requestApproval = useCallback((tool, input) => new Promise((resolve) => {
839
+ setQueue((prev) => [...prev, {
840
+ id: nextApprovalId(),
841
+ tool,
842
+ input,
843
+ resolve
844
+ }]);
845
+ }), []);
846
+ const resolveHead = useCallback((decision) => {
847
+ setQueue((prev) => {
848
+ const [head, ...rest] = prev;
849
+ if (head) head.resolve(decision);
850
+ return rest;
851
+ });
852
+ }, []);
853
+ const denyAll = useCallback(() => {
854
+ setQueue((prev) => {
855
+ for (const p of prev) p.resolve("deny");
856
+ return [];
857
+ });
858
+ }, []);
859
+ const actionsRef = useRef(null);
860
+ if (!actionsRef.current) actionsRef.current = {
861
+ requestApproval,
862
+ resolveHead,
863
+ denyAll
864
+ };
865
+ return /* @__PURE__ */ jsx(SafeModeActionsContext.Provider, {
866
+ value: actionsRef.current,
867
+ children: /* @__PURE__ */ jsx(SafeModeQueueContext.Provider, {
868
+ value: queue,
869
+ children
870
+ })
871
+ });
872
+ }
873
+ function useSafeModeQueue() {
874
+ return useContext(SafeModeQueueContext);
875
+ }
876
+ function useSafeModeActions() {
877
+ const ctx = useContext(SafeModeActionsContext);
878
+ if (!ctx) throw new Error("useSafeModeActions must be used inside <SafeModeProvider>");
879
+ return ctx;
880
+ }
881
+ //#endregion
882
+ //#region src/chat/themes/catppuccin.ts
883
+ const LATTE = {
884
+ rosewater: "#dc8a78",
885
+ flamingo: "#dd7878",
886
+ pink: "#ea76cb",
887
+ mauve: "#8839ef",
888
+ red: "#d20f39",
889
+ maroon: "#e64553",
890
+ peach: "#fe640b",
891
+ yellow: "#df8e1d",
892
+ green: "#40a02b",
893
+ teal: "#179299",
894
+ sky: "#04a5e5",
895
+ sapphire: "#209fb5",
896
+ blue: "#1e66f5",
897
+ lavender: "#7287fd",
898
+ text: "#4c4f69",
899
+ subtext1: "#5c5f77",
900
+ subtext0: "#6c6f85",
901
+ overlay2: "#7c7f93",
902
+ overlay1: "#8c8fa1",
903
+ overlay0: "#9ca0b0",
904
+ surface2: "#acb0be",
905
+ surface1: "#bcc0cc",
906
+ surface0: "#ccd0da",
907
+ base: "#eff1f5",
908
+ mantle: "#e6e9ef",
909
+ crust: "#dce0e8"
910
+ };
911
+ const FRAPPE = {
912
+ rosewater: "#f2d5cf",
913
+ flamingo: "#eebebe",
914
+ pink: "#f4b8e4",
915
+ mauve: "#ca9ee6",
916
+ red: "#e78284",
917
+ maroon: "#ea999c",
918
+ peach: "#ef9f76",
919
+ yellow: "#e5c890",
920
+ green: "#a6d189",
921
+ teal: "#81c8be",
922
+ sky: "#99d1db",
923
+ sapphire: "#85c1dc",
924
+ blue: "#8caaee",
925
+ lavender: "#babbf1",
926
+ text: "#c6d0f5",
927
+ subtext1: "#b5bfe2",
928
+ subtext0: "#a5adce",
929
+ overlay2: "#949cbb",
930
+ overlay1: "#838ba7",
931
+ overlay0: "#737994",
932
+ surface2: "#626880",
933
+ surface1: "#51576d",
934
+ surface0: "#414559",
935
+ base: "#303446",
936
+ mantle: "#292c3c",
937
+ crust: "#232634"
938
+ };
939
+ const MACCHIATO = {
940
+ rosewater: "#f4dbd6",
941
+ flamingo: "#f0c6c6",
942
+ pink: "#f5bde6",
943
+ mauve: "#c6a0f6",
944
+ red: "#ed8796",
945
+ maroon: "#ee99a0",
946
+ peach: "#f5a97f",
947
+ yellow: "#eed49f",
948
+ green: "#a6da95",
949
+ teal: "#8bd5ca",
950
+ sky: "#91d7e3",
951
+ sapphire: "#7dc4e4",
952
+ blue: "#8aadf4",
953
+ lavender: "#b7bdf8",
954
+ text: "#cad3f5",
955
+ subtext1: "#b8c0e0",
956
+ subtext0: "#a5adcb",
957
+ overlay2: "#939ab7",
958
+ overlay1: "#8087a2",
959
+ overlay0: "#6e738d",
960
+ surface2: "#5b6078",
961
+ surface1: "#494d64",
962
+ surface0: "#363a4f",
963
+ base: "#24273a",
964
+ mantle: "#1e2030",
965
+ crust: "#181926"
966
+ };
967
+ const MOCHA = {
968
+ rosewater: "#f5e0dc",
969
+ flamingo: "#f2cdcd",
970
+ pink: "#f5c2e7",
971
+ mauve: "#cba6f7",
972
+ red: "#f38ba8",
973
+ maroon: "#eba0ac",
974
+ peach: "#fab387",
975
+ yellow: "#f9e2af",
976
+ green: "#a6e3a1",
977
+ teal: "#94e2d5",
978
+ sky: "#89dceb",
979
+ sapphire: "#74c7ec",
980
+ blue: "#89b4fa",
981
+ lavender: "#b4befe",
982
+ text: "#cdd6f4",
983
+ subtext1: "#bac2de",
984
+ subtext0: "#a6adc8",
985
+ overlay2: "#9399b2",
986
+ overlay1: "#7f849c",
987
+ overlay0: "#6c7086",
988
+ surface2: "#585b70",
989
+ surface1: "#45475a",
990
+ surface0: "#313244",
991
+ base: "#1e1e2e",
992
+ mantle: "#181825",
993
+ crust: "#11111b"
994
+ };
995
+ /**
996
+ * Compose a `Theme` from a Catppuccin palette flavor.
997
+ *
998
+ * Role-color picks follow the upstream Catppuccin styleguide:
999
+ * - `mauve` is the canonical accent — used here as the brand color.
1000
+ * - `green` / `red` / `yellow` keep their universal semantic meaning.
1001
+ * - `blue` carries function / model identity (matches the VSCode plugin's
1002
+ * `support.function` mapping).
1003
+ * - `subtext1` / `overlay0` form the dim/mute pair (one step apart so the
1004
+ * two tiers stay visually distinct on every flavor).
1005
+ * - `surface1` / `overlay0` give the resting/active border pair.
1006
+ *
1007
+ * Syntax token mappings line up with the official Catppuccin token rules
1008
+ * (keyword = mauve, string = green, function = blue, type = yellow, …) so
1009
+ * code fences match what users see in their editor.
1010
+ */
1011
+ function catppuccinTheme(id, label, p) {
1012
+ return {
1013
+ id,
1014
+ label,
1015
+ colors: {
1016
+ brand: p.mauve,
1017
+ accent: p.green,
1018
+ model: p.blue,
1019
+ warn: p.yellow,
1020
+ error: p.red,
1021
+ dim: p.subtext1,
1022
+ mute: p.overlay0,
1023
+ border: p.surface1,
1024
+ borderActive: p.overlay0
1025
+ },
1026
+ select: {
1027
+ backgroundColor: "transparent",
1028
+ focusedBackgroundColor: "transparent",
1029
+ selectedBackgroundColor: "transparent",
1030
+ selectedTextColor: p.mauve,
1031
+ textColor: p.subtext1,
1032
+ descriptionColor: p.overlay0,
1033
+ selectedDescriptionColor: p.subtext0
1034
+ },
1035
+ surfaces: { modal: p.mantle },
1036
+ syntax: {
1037
+ "default": { fg: p.text },
1038
+ "markup.heading": {
1039
+ fg: p.mauve,
1040
+ bold: true
1041
+ },
1042
+ "markup.heading.1": {
1043
+ fg: p.mauve,
1044
+ bold: true
1045
+ },
1046
+ "markup.heading.2": {
1047
+ fg: p.lavender,
1048
+ bold: true
1049
+ },
1050
+ "markup.heading.3": {
1051
+ fg: p.blue,
1052
+ bold: true
1053
+ },
1054
+ "markup.bold": {
1055
+ fg: p.text,
1056
+ bold: true
1057
+ },
1058
+ "markup.strong": {
1059
+ fg: p.text,
1060
+ bold: true
1061
+ },
1062
+ "markup.italic": {
1063
+ fg: p.text,
1064
+ italic: true
1065
+ },
1066
+ "markup.link": {
1067
+ fg: p.sky,
1068
+ underline: true
1069
+ },
1070
+ "markup.link.url": {
1071
+ fg: p.sky,
1072
+ underline: true
1073
+ },
1074
+ "markup.list": { fg: p.peach },
1075
+ "markup.raw": { fg: p.green },
1076
+ "markup.raw.block": { fg: p.green },
1077
+ "markup.quote": {
1078
+ fg: p.overlay2,
1079
+ italic: true
1080
+ },
1081
+ "keyword": {
1082
+ fg: p.mauve,
1083
+ bold: true
1084
+ },
1085
+ "keyword.import": {
1086
+ fg: p.pink,
1087
+ bold: true
1088
+ },
1089
+ "keyword.operator": { fg: p.sky },
1090
+ "string": { fg: p.green },
1091
+ "string.escape": {
1092
+ fg: p.pink,
1093
+ bold: true
1094
+ },
1095
+ "character": { fg: p.teal },
1096
+ "comment": {
1097
+ fg: p.overlay1,
1098
+ italic: true
1099
+ },
1100
+ "number": { fg: p.peach },
1101
+ "boolean": { fg: p.peach },
1102
+ "constant": { fg: p.peach },
1103
+ "constant.builtin": { fg: p.peach },
1104
+ "function": { fg: p.blue },
1105
+ "function.call": { fg: p.blue },
1106
+ "function.method": { fg: p.blue },
1107
+ "function.method.call": { fg: p.blue },
1108
+ "function.builtin": { fg: p.blue },
1109
+ "function.macro": { fg: p.teal },
1110
+ "type": { fg: p.yellow },
1111
+ "type.builtin": { fg: p.yellow },
1112
+ "constructor": { fg: p.yellow },
1113
+ "attribute": { fg: p.yellow },
1114
+ "tag": { fg: p.lavender },
1115
+ "variable": { fg: p.text },
1116
+ "variable.builtin": { fg: p.red },
1117
+ "variable.parameter": {
1118
+ fg: p.maroon,
1119
+ italic: true
1120
+ },
1121
+ "variable.member": { fg: p.text },
1122
+ "property": { fg: p.lavender },
1123
+ "operator": { fg: p.sky },
1124
+ "punctuation": { fg: p.overlay2 },
1125
+ "punctuation.bracket": { fg: p.overlay2 },
1126
+ "punctuation.delimiter": { fg: p.overlay2 },
1127
+ "label": { fg: p.sapphire }
1128
+ }
1129
+ };
1130
+ }
1131
+ const CATPPUCCIN_MOCHA = catppuccinTheme("catppuccin-mocha", "Catppuccin Mocha", MOCHA);
1132
+ const CATPPUCCIN_MACCHIATO = catppuccinTheme("catppuccin-macchiato", "Catppuccin Macchiato", MACCHIATO);
1133
+ const CATPPUCCIN_FRAPPE = catppuccinTheme("catppuccin-frappe", "Catppuccin Frappé", FRAPPE);
1134
+ const CATPPUCCIN_LATTE = catppuccinTheme("catppuccin-latte", "Catppuccin Latte", LATTE);
1135
+ //#endregion
1136
+ //#region src/chat/themes/vaporwave.ts
1137
+ const PALETTE = {
1138
+ pink: "#E95378",
1139
+ pinkBright: "#FF71CE",
1140
+ cyan: "#01CDFE",
1141
+ cyanBright: "#59E1E3",
1142
+ blue: "#94D0FF",
1143
+ green: "#29D398",
1144
+ greenBright: "#09F7A0",
1145
+ yellow: "#FFFB96",
1146
+ red: "#F43E5C",
1147
+ text: "#D5D8DA",
1148
+ textBright: "#EEFFFF",
1149
+ comment: "#BBBBBB",
1150
+ muted: "#6C6F93",
1151
+ surface: "#2E303E",
1152
+ panel: "#232530"
1153
+ };
1154
+ const VAPORWAVE_THEME = {
1155
+ id: "vaporwave",
1156
+ label: "Vaporwave",
1157
+ colors: {
1158
+ brand: PALETTE.pink,
1159
+ accent: PALETTE.greenBright,
1160
+ model: PALETTE.blue,
1161
+ warn: PALETTE.yellow,
1162
+ error: PALETTE.red,
1163
+ dim: PALETTE.text,
1164
+ mute: PALETTE.muted,
1165
+ border: PALETTE.surface,
1166
+ borderActive: PALETTE.muted
1167
+ },
1168
+ select: {
1169
+ backgroundColor: "transparent",
1170
+ focusedBackgroundColor: "transparent",
1171
+ selectedBackgroundColor: "transparent",
1172
+ selectedTextColor: PALETTE.pink,
1173
+ textColor: PALETTE.text,
1174
+ descriptionColor: PALETTE.muted,
1175
+ selectedDescriptionColor: PALETTE.text
1176
+ },
1177
+ surfaces: { modal: PALETTE.panel },
1178
+ syntax: {
1179
+ "default": { fg: PALETTE.text },
1180
+ "markup.heading": {
1181
+ fg: PALETTE.pink,
1182
+ bold: true
1183
+ },
1184
+ "markup.heading.1": {
1185
+ fg: PALETTE.pink,
1186
+ bold: true
1187
+ },
1188
+ "markup.heading.2": {
1189
+ fg: PALETTE.pinkBright,
1190
+ bold: true
1191
+ },
1192
+ "markup.heading.3": {
1193
+ fg: PALETTE.blue,
1194
+ bold: true
1195
+ },
1196
+ "markup.bold": {
1197
+ fg: PALETTE.textBright,
1198
+ bold: true
1199
+ },
1200
+ "markup.strong": {
1201
+ fg: PALETTE.textBright,
1202
+ bold: true
1203
+ },
1204
+ "markup.italic": {
1205
+ fg: PALETTE.greenBright,
1206
+ italic: true
1207
+ },
1208
+ "markup.link": {
1209
+ fg: PALETTE.yellow,
1210
+ underline: true
1211
+ },
1212
+ "markup.link.url": {
1213
+ fg: PALETTE.yellow,
1214
+ underline: true
1215
+ },
1216
+ "markup.list": { fg: PALETTE.textBright },
1217
+ "markup.raw": { fg: PALETTE.yellow },
1218
+ "markup.raw.block": { fg: PALETTE.yellow },
1219
+ "markup.quote": {
1220
+ fg: PALETTE.yellow,
1221
+ italic: true
1222
+ },
1223
+ "keyword": {
1224
+ fg: PALETTE.pinkBright,
1225
+ bold: true
1226
+ },
1227
+ "keyword.import": {
1228
+ fg: PALETTE.pinkBright,
1229
+ bold: true
1230
+ },
1231
+ "keyword.operator": { fg: PALETTE.comment },
1232
+ "string": { fg: PALETTE.yellow },
1233
+ "string.escape": {
1234
+ fg: PALETTE.greenBright,
1235
+ bold: true
1236
+ },
1237
+ "character": { fg: PALETTE.yellow },
1238
+ "comment": {
1239
+ fg: PALETTE.comment,
1240
+ italic: true
1241
+ },
1242
+ "number": { fg: PALETTE.textBright },
1243
+ "boolean": { fg: PALETTE.textBright },
1244
+ "constant": { fg: PALETTE.textBright },
1245
+ "constant.builtin": { fg: PALETTE.textBright },
1246
+ "function": { fg: PALETTE.greenBright },
1247
+ "function.call": { fg: PALETTE.greenBright },
1248
+ "function.method": { fg: PALETTE.greenBright },
1249
+ "function.method.call": { fg: PALETTE.greenBright },
1250
+ "function.builtin": { fg: PALETTE.greenBright },
1251
+ "function.macro": { fg: PALETTE.greenBright },
1252
+ "type": { fg: PALETTE.greenBright },
1253
+ "type.builtin": { fg: PALETTE.greenBright },
1254
+ "constructor": { fg: PALETTE.greenBright },
1255
+ "attribute": { fg: PALETTE.yellow },
1256
+ "tag": { fg: PALETTE.blue },
1257
+ "variable": { fg: PALETTE.blue },
1258
+ "variable.builtin": { fg: PALETTE.cyanBright },
1259
+ "variable.parameter": { fg: PALETTE.text },
1260
+ "variable.member": { fg: PALETTE.blue },
1261
+ "property": { fg: PALETTE.blue },
1262
+ "operator": { fg: PALETTE.comment },
1263
+ "punctuation": { fg: PALETTE.comment },
1264
+ "punctuation.bracket": { fg: PALETTE.textBright },
1265
+ "punctuation.delimiter": { fg: PALETTE.comment },
1266
+ "label": { fg: PALETTE.cyan }
1267
+ }
1268
+ };
1269
+ //#endregion
1270
+ //#region src/chat/theme.ts
1271
+ /**
1272
+ * Renderer-agnostic theme system.
1273
+ *
1274
+ * A `Theme` bundles every variable that can change visually between
1275
+ * "themes" — colors, select-row styling, code/markdown syntax highlight
1276
+ * tokens, panel backgrounds. Plain JSON: no OpenTUI dependency, no React,
1277
+ * no functions. The TUI consumes the theme by reading from `useTheme()`
1278
+ * and converting hex strings into OpenTUI's `RGBA`/`SyntaxStyle`; a future
1279
+ * GUI consumes the same theme by converting into CSS variables or Tailwind
1280
+ * tokens.
1281
+ *
1282
+ * Components should always read theme values through `useTheme()` /
1283
+ * `useColors()` so a runtime theme switch (Settings → Theme) re-paints
1284
+ * the whole tree. Importing a raw theme object directly bypasses the
1285
+ * context and pins the component to a single look.
1286
+ *
1287
+ * Built-in flavors (Catppuccin, Vaporwave) live in `./themes/`. This file
1288
+ * just defines the shape + the default theme + the registry.
1289
+ */
1290
+ const DEFAULT_COLORS = {
1291
+ brand: "#FFCC00",
1292
+ accent: "#00FF88",
1293
+ model: "#88CCFF",
1294
+ warn: "#FFAA66",
1295
+ error: "#FF6666",
1296
+ dim: "#888888",
1297
+ mute: "#555555",
1298
+ border: "#333333",
1299
+ borderActive: "#555555"
1300
+ };
1301
+ const DEFAULT_THEME = {
1302
+ id: "default",
1303
+ label: "Default",
1304
+ colors: DEFAULT_COLORS,
1305
+ select: {
1306
+ backgroundColor: "transparent",
1307
+ focusedBackgroundColor: "transparent",
1308
+ selectedBackgroundColor: "transparent",
1309
+ selectedTextColor: DEFAULT_COLORS.brand,
1310
+ textColor: DEFAULT_COLORS.dim,
1311
+ descriptionColor: DEFAULT_COLORS.mute,
1312
+ selectedDescriptionColor: DEFAULT_COLORS.dim
1313
+ },
1314
+ surfaces: { modal: "#101010" },
1315
+ syntax: {
1316
+ "default": { fg: "#E6EDF3" },
1317
+ "markup.heading": {
1318
+ fg: DEFAULT_COLORS.brand,
1319
+ bold: true
1320
+ },
1321
+ "markup.heading.1": {
1322
+ fg: DEFAULT_COLORS.brand,
1323
+ bold: true
1324
+ },
1325
+ "markup.heading.2": {
1326
+ fg: "#FFD84D",
1327
+ bold: true
1328
+ },
1329
+ "markup.heading.3": {
1330
+ fg: "#FFE680",
1331
+ bold: true
1332
+ },
1333
+ "markup.bold": {
1334
+ fg: "#FFFFFF",
1335
+ bold: true
1336
+ },
1337
+ "markup.strong": {
1338
+ fg: "#FFFFFF",
1339
+ bold: true
1340
+ },
1341
+ "markup.italic": {
1342
+ fg: "#E6EDF3",
1343
+ italic: true
1344
+ },
1345
+ "markup.link": {
1346
+ fg: DEFAULT_COLORS.model,
1347
+ underline: true
1348
+ },
1349
+ "markup.link.url": {
1350
+ fg: DEFAULT_COLORS.model,
1351
+ underline: true
1352
+ },
1353
+ "markup.list": { fg: DEFAULT_COLORS.warn },
1354
+ "markup.raw": { fg: "#A5D6FF" },
1355
+ "markup.raw.block": { fg: "#A5D6FF" },
1356
+ "markup.quote": {
1357
+ fg: DEFAULT_COLORS.dim,
1358
+ italic: true
1359
+ },
1360
+ "keyword": {
1361
+ fg: "#FF7B72",
1362
+ bold: true
1363
+ },
1364
+ "keyword.import": {
1365
+ fg: "#FF7B72",
1366
+ bold: true
1367
+ },
1368
+ "keyword.operator": { fg: "#FF7B72" },
1369
+ "string": { fg: "#A5D6FF" },
1370
+ "string.escape": {
1371
+ fg: "#A5D6FF",
1372
+ bold: true
1373
+ },
1374
+ "character": { fg: "#A5D6FF" },
1375
+ "comment": {
1376
+ fg: "#8B949E",
1377
+ italic: true
1378
+ },
1379
+ "number": { fg: "#79C0FF" },
1380
+ "boolean": { fg: "#79C0FF" },
1381
+ "constant": { fg: "#79C0FF" },
1382
+ "constant.builtin": { fg: "#79C0FF" },
1383
+ "function": { fg: "#D2A8FF" },
1384
+ "function.call": { fg: "#D2A8FF" },
1385
+ "function.method": { fg: "#D2A8FF" },
1386
+ "function.method.call": { fg: "#D2A8FF" },
1387
+ "function.builtin": { fg: "#D2A8FF" },
1388
+ "function.macro": { fg: "#D2A8FF" },
1389
+ "type": { fg: "#FFA657" },
1390
+ "type.builtin": { fg: "#FFA657" },
1391
+ "constructor": { fg: "#FFA657" },
1392
+ "attribute": { fg: "#FFA657" },
1393
+ "tag": { fg: "#7EE787" },
1394
+ "variable": { fg: "#E6EDF3" },
1395
+ "variable.builtin": { fg: "#79C0FF" },
1396
+ "variable.parameter": { fg: "#FFA657" },
1397
+ "variable.member": { fg: "#79C0FF" },
1398
+ "property": { fg: "#79C0FF" },
1399
+ "operator": { fg: "#FF7B72" },
1400
+ "punctuation": { fg: DEFAULT_COLORS.mute },
1401
+ "punctuation.bracket": { fg: "#F0F6FC" },
1402
+ "punctuation.delimiter": { fg: "#C9D1D9" },
1403
+ "label": { fg: "#79C0FF" }
1404
+ }
1405
+ };
1406
+ /**
1407
+ * Built-in theme registry, keyed by `theme.id`. The TUI looks up the active
1408
+ * theme here using `Settings.theme`; unknown ids fall back to
1409
+ * `DEFAULT_THEME`. Hosts can extend this by passing additional themes to a
1410
+ * future `runTui({ themes })` option (not yet wired).
1411
+ *
1412
+ * Insertion order is the picker cycle order — keep `default` first so a
1413
+ * fresh install (no `theme` in `state.json`) sees the familiar yellow theme
1414
+ * before the others.
1415
+ */
1416
+ const BUILTIN_THEMES = {
1417
+ [DEFAULT_THEME.id]: DEFAULT_THEME,
1418
+ [CATPPUCCIN_MOCHA.id]: CATPPUCCIN_MOCHA,
1419
+ [CATPPUCCIN_MACCHIATO.id]: CATPPUCCIN_MACCHIATO,
1420
+ [CATPPUCCIN_FRAPPE.id]: CATPPUCCIN_FRAPPE,
1421
+ [CATPPUCCIN_LATTE.id]: CATPPUCCIN_LATTE,
1422
+ [VAPORWAVE_THEME.id]: VAPORWAVE_THEME
1423
+ };
1424
+ /** Resolve a theme id to its full `Theme`, falling back to default on unknown ids. */
1425
+ function resolveTheme(id) {
1426
+ if (id && BUILTIN_THEMES[id]) return BUILTIN_THEMES[id];
1427
+ return DEFAULT_THEME;
1428
+ }
1429
+ //#endregion
1430
+ //#region src/chat/settings-context.tsx
1431
+ const DEFAULT_SETTINGS = {
1432
+ showThinking: true,
1433
+ showToolCalls: true,
1434
+ showToolResults: true,
1435
+ safeMode: true,
1436
+ hideSubagentOutput: true,
1437
+ theme: DEFAULT_THEME.id
1438
+ };
1439
+ const SettingsContext = createContext(null);
1440
+ function SettingsProvider({ initial, onChange, children }) {
1441
+ const [settings, setSettings] = useState(initial);
1442
+ const toggle = useCallback((key) => {
1443
+ setSettings((prev) => {
1444
+ const next = {
1445
+ ...prev,
1446
+ [key]: !prev[key]
1447
+ };
1448
+ onChange?.(next);
1449
+ return next;
1450
+ });
1451
+ }, [onChange]);
1452
+ const setSetting = useCallback((key, value) => {
1453
+ setSettings((prev) => {
1454
+ if (prev[key] === value) return prev;
1455
+ const next = {
1456
+ ...prev,
1457
+ [key]: value
1458
+ };
1459
+ onChange?.(next);
1460
+ return next;
1461
+ });
1462
+ }, [onChange]);
1463
+ const value = useMemo(() => ({
1464
+ settings,
1465
+ toggle,
1466
+ setSetting
1467
+ }), [
1468
+ settings,
1469
+ toggle,
1470
+ setSetting
1471
+ ]);
1472
+ return /* @__PURE__ */ jsx(SettingsContext.Provider, {
1473
+ value,
1474
+ children
1475
+ });
1476
+ }
1477
+ function useSettings() {
1478
+ const ctx = useContext(SettingsContext);
1479
+ if (!ctx) throw new Error("useSettings must be used inside <SettingsProvider>");
1480
+ return ctx;
1481
+ }
1482
+ const SETTINGS_TOGGLES = [
1483
+ {
1484
+ key: "safeMode",
1485
+ label: "Safe mode",
1486
+ description: "prompt before each tool call (unless safelisted)"
1487
+ },
1488
+ {
1489
+ key: "hideSubagentOutput",
1490
+ label: "Hide subagent output",
1491
+ description: "collapse subagent runs to start/done markers"
1492
+ },
1493
+ {
1494
+ key: "showThinking",
1495
+ label: "Thinking blocks",
1496
+ description: "agent reasoning shown inline"
1497
+ },
1498
+ {
1499
+ key: "showToolCalls",
1500
+ label: "Tool calls",
1501
+ description: "the ↳ name(args) lines"
1502
+ },
1503
+ {
1504
+ key: "showToolResults",
1505
+ label: "Tool outputs",
1506
+ description: "the ┃ result blocks under tool calls"
1507
+ }
1508
+ ];
1509
+ const SETTINGS_CHOICES = [{
1510
+ key: "theme",
1511
+ label: "Theme",
1512
+ description: "colors + markdown / syntax styles",
1513
+ options: Object.values(BUILTIN_THEMES).map((t) => ({
1514
+ value: t.id,
1515
+ label: t.label
1516
+ }))
1517
+ }];
1518
+ //#endregion
1519
+ //#region src/chat/streaming.ts
1520
+ /** Target one flush per ~33ms (one frame at the default renderer targetFps=30). */
1521
+ const FLUSH_INTERVAL_MS = 33;
1522
+ const PARENT_OWNER = "parent";
1523
+ function emptyBucket(owner, depth) {
1524
+ return {
1525
+ markdown: "",
1526
+ thinking: "",
1527
+ owner,
1528
+ depth
1529
+ };
1530
+ }
1531
+ function applyBucket(prev, bucket) {
1532
+ let result = prev;
1533
+ if (bucket.thinking) result = appendThinkingLines(result, bucket.thinking, bucket.owner, bucket.depth);
1534
+ if (bucket.markdown) result = appendMarkdownDelta(result, bucket.markdown, bucket.owner, bucket.depth);
1535
+ return result;
1536
+ }
1537
+ function appendMarkdownDelta(prev, delta, owner, depth) {
1538
+ const last = prev[prev.length - 1];
1539
+ if (last && last.kind === "markdown" && last.streaming && ownerOf(last) === owner) {
1540
+ const next = prev.slice(0, -1);
1541
+ next.push({
1542
+ ...last,
1543
+ text: last.text + delta
1544
+ });
1545
+ return next;
1546
+ }
1547
+ return [...prev, eventWithOwner({
1548
+ kind: "markdown",
1549
+ text: delta,
1550
+ streaming: true
1551
+ }, owner, depth)];
1552
+ }
1553
+ function appendThinkingLines(prev, delta, owner, depth) {
1554
+ const lines = delta.split("\n");
1555
+ const result = [...prev];
1556
+ const last = result[result.length - 1];
1557
+ if (last && last.kind === "thinking" && ownerOf(last) === owner) result[result.length - 1] = {
1558
+ ...last,
1559
+ text: last.text + lines[0]
1560
+ };
1561
+ else if (lines[0] || lines.length > 1) result.push(eventWithOwner({
1562
+ kind: "thinking",
1563
+ text: lines[0]
1564
+ }, owner, depth));
1565
+ for (let i = 1; i < lines.length; i++) result.push(eventWithOwner({
1566
+ kind: "thinking",
1567
+ text: lines[i]
1568
+ }, owner, depth));
1569
+ return result;
1570
+ }
1571
+ function ownerOf(evt) {
1572
+ return evt.childId ?? PARENT_OWNER;
1573
+ }
1574
+ function eventWithOwner(evt, owner, depth) {
1575
+ if (owner === PARENT_OWNER) return evt;
1576
+ return {
1577
+ ...evt,
1578
+ childId: owner,
1579
+ depth
1580
+ };
1581
+ }
1582
+ /** Flip any trailing streaming markdown blocks (any owner) to finalized. */
1583
+ function finalizeStreamingMarkdown(events) {
1584
+ let changed = false;
1585
+ const next = events.map((e) => {
1586
+ if (e.kind === "markdown" && e.streaming) {
1587
+ changed = true;
1588
+ return {
1589
+ ...e,
1590
+ streaming: false
1591
+ };
1592
+ }
1593
+ return e;
1594
+ });
1595
+ return changed ? next : events;
1596
+ }
1597
+ /** Flip the trailing streaming markdown block for one specific owner. */
1598
+ function finalizeStreamingMarkdownForOwner(events, owner) {
1599
+ for (let i = events.length - 1; i >= 0; i--) {
1600
+ const e = events[i];
1601
+ if (e.kind !== "markdown") continue;
1602
+ if (!e.streaming) continue;
1603
+ if (ownerOf(e) !== owner) continue;
1604
+ const next = events.slice();
1605
+ next[i] = {
1606
+ ...e,
1607
+ streaming: false
1608
+ };
1609
+ return next;
1610
+ }
1611
+ return events;
1612
+ }
1613
+ /**
1614
+ * Effective context size for a single turn.
1615
+ *
1616
+ * `usage.input` is misleading on its own when prompt caching is active: providers
1617
+ * (Anthropic, OpenRouter→Anthropic, Gemini) report `input` as the *new uncached*
1618
+ * tokens only — the cached prefix shows up in `cacheRead`, and newly-cached
1619
+ * tokens in `cacheCreation`. The model still saw all three buckets, so the real
1620
+ * context-window utilization is their sum.
1621
+ *
1622
+ * Non-caching providers leave `cacheRead`/`cacheCreation` undefined, so this
1623
+ * collapses to plain `input` for them.
1624
+ */
1625
+ function turnContextSize(usage) {
1626
+ if (!usage) return 0;
1627
+ return (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheCreation ?? 0);
1628
+ }
1629
+ function useStreamBuffer(setEvents) {
1630
+ const bucketsRef = useRef(/* @__PURE__ */ new Map());
1631
+ const flushTimerRef = useRef(null);
1632
+ const drainPendingInto = useCallback((updater) => {
1633
+ if (flushTimerRef.current) {
1634
+ clearTimeout(flushTimerRef.current);
1635
+ flushTimerRef.current = null;
1636
+ }
1637
+ const buckets = Array.from(bucketsRef.current.values());
1638
+ bucketsRef.current.clear();
1639
+ if (!buckets.some((b) => b.markdown.length > 0 || b.thinking.length > 0) && !updater) return;
1640
+ setEvents((prev) => {
1641
+ let merged = prev;
1642
+ for (const bucket of buckets) merged = applyBucket(merged, bucket);
1643
+ return updater ? updater(merged) : merged;
1644
+ });
1645
+ }, [setEvents]);
1646
+ const flush = useCallback(() => drainPendingInto(), [drainPendingInto]);
1647
+ const flushAndUpdate = useCallback((update) => drainPendingInto(update), [drainPendingInto]);
1648
+ const appendImmediate = useCallback((evt) => drainPendingInto((events) => [...events, evt]), [drainPendingInto]);
1649
+ const queueStreamDelta = useCallback((kind, delta, source) => {
1650
+ if (!delta) return;
1651
+ const owner = source?.childId ?? PARENT_OWNER;
1652
+ const depth = source?.depth ?? 0;
1653
+ let bucket = bucketsRef.current.get(owner);
1654
+ if (!bucket) {
1655
+ bucket = emptyBucket(owner, depth);
1656
+ bucketsRef.current.set(owner, bucket);
1657
+ }
1658
+ bucket[kind] += delta;
1659
+ if (!flushTimerRef.current) flushTimerRef.current = setTimeout(flush, FLUSH_INTERVAL_MS);
1660
+ }, [flush]);
1661
+ const reset = useCallback(() => {
1662
+ if (flushTimerRef.current) {
1663
+ clearTimeout(flushTimerRef.current);
1664
+ flushTimerRef.current = null;
1665
+ }
1666
+ bucketsRef.current.clear();
1667
+ }, []);
1668
+ return useMemo(() => ({
1669
+ queueStreamDelta,
1670
+ appendImmediate,
1671
+ flushAndUpdate,
1672
+ flush,
1673
+ reset
1674
+ }), [
1675
+ queueStreamDelta,
1676
+ appendImmediate,
1677
+ flushAndUpdate,
1678
+ flush,
1679
+ reset
1680
+ ]);
1681
+ }
1682
+ //#endregion
1683
+ //#region src/chat/theme-context.tsx
1684
+ const ThemeContext = createContext(DEFAULT_THEME);
1685
+ function ThemeProvider({ theme, children }) {
1686
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, {
1687
+ value: theme,
1688
+ children
1689
+ });
1690
+ }
1691
+ function useTheme() {
1692
+ return useContext(ThemeContext);
1693
+ }
1694
+ /** Color palette only — equivalent to `useTheme().colors`. */
1695
+ function useColors() {
1696
+ return useContext(ThemeContext).colors;
1697
+ }
1698
+ /** Select-row styling — equivalent to `useTheme().select`. */
1699
+ function useSelectStyle() {
1700
+ return useContext(ThemeContext).select;
1701
+ }
1702
+ /** Panel / surface backgrounds — equivalent to `useTheme().surfaces`. */
1703
+ function useSurfaces() {
1704
+ return useContext(ThemeContext).surfaces;
1705
+ }
1706
+ /** Raw syntax style table — `useTheme().syntax`. Renderer converts to its native style type. */
1707
+ function useSyntaxStyles() {
1708
+ return useContext(ThemeContext).syntax;
1709
+ }
1710
+ //#endregion
1711
+ export { toolCallPreview as $, isOnSafelist as A, shortId as B, CATPPUCCIN_MOCHA as C, IMPLICITLY_SAFE_TOOLS as D, useSafeModeQueue as E, writeProjects as F, createTuiStore as G, useConfig as H, runOAuthLogin as I, listSessionMeta as J, eventsFromTurns as K, supportsOAuth as L, projectsFilePath as M, readProjects as N, addToSafelist as O, suggestSafelistEntry as P, titleFromTurns as Q, ageString as R, CATPPUCCIN_MACCHIATO as S, useSafeModeActions as T, resolveConfig as U, ConfigProvider as V, createStateStore as W, saveState as X, loadState as Y, stripSpawnTokensLine as Z, DEFAULT_THEME as _, piIdOf as _t, useSyntaxStyles as a, readProviderCredential as at, CATPPUCCIN_FRAPPE as b, finalizeStreamingMarkdownForOwner as c, writeCredentials as ct, DEFAULT_SETTINGS as d, cerebrasDescriptor as dt, toolResultText as et, SETTINGS_CHOICES as f, credKeyOf as ft, BUILTIN_THEMES as g, openrouterDescriptor as gt, useSettings as h, openaiDescriptor as ht, useSurfaces as i, readCredentials as it, matchesSafelistEntry as j, getSafelist as k, turnContextSize as l, BUILTIN_PROVIDERS as lt, SettingsProvider as m, modelsForDescriptor as mt, useColors as n, applyApiKeyEnv as nt, useTheme as o, removeProviderCredential as ot, SETTINGS_TOGGLES as p, getContextWindow as pt, lastContextSizeFromTurns as q, useSelectStyle as r, credentialsPath as rt, finalizeStreamingMarkdown as s, setProviderCredential as st, ThemeProvider as t, detectAuth as tt, useStreamBuffer as u, anthropicDescriptor as ut, resolveTheme as v, SafeModeProvider as w, CATPPUCCIN_LATTE as x, VAPORWAVE_THEME as y, fmtTokens as z };
1712
+
1713
+ //# sourceMappingURL=theme-context-MungM3SY.js.map