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.
- package/dist/chat.d.ts +305 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +2 -0
- package/dist/contexts.d.ts +1 -1
- package/dist/{index-BfSdALzk.d.ts → index-BB4kuRh3.d.ts} +1 -1
- package/dist/{index-BfSdALzk.d.ts.map → index-BB4kuRh3.d.ts.map} +1 -1
- package/dist/{index-B8-yNSsk.d.ts → index-DRoG_udt.d.ts} +42 -42
- package/dist/index-DRoG_udt.d.ts.map +1 -0
- package/dist/{index-CqpNqjDy.d.ts → index-Ds5YpvfZ.d.ts} +3 -3
- package/dist/{index-CqpNqjDy.d.ts.map → index-Ds5YpvfZ.d.ts.map} +1 -1
- package/dist/{agent-BAoqUvwA.d.ts → index-bgh-k8Mv.d.ts} +1992 -1992
- package/dist/index-bgh-k8Mv.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/mcp.d.ts +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/skills.d.ts +2 -2
- package/dist/theme-BlXO6yHe.d.ts +503 -0
- package/dist/theme-BlXO6yHe.d.ts.map +1 -0
- package/dist/theme-context-MungM3SY.js +1713 -0
- package/dist/theme-context-MungM3SY.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tui.d.ts +34 -636
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +302 -1261
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/package.json +6 -3
- package/dist/agent-BAoqUvwA.d.ts.map +0 -1
- package/dist/index-B8-yNSsk.d.ts.map +0 -1
|
@@ -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
|