@visual-z/xeno 0.1.4 → 0.2.1
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/CHANGELOG.md +25 -0
- package/README.md +143 -33
- package/docs/install.md +23 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +45 -18
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup-input.ts +94 -0
- package/src/apps/startup.ts +413 -295
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Pi session-selector-search (MIT, 02eed88), independent of its AgentSession and clock. */
|
|
2
|
+
export type SessionSort = "threaded" | "recent" | "relevance";
|
|
3
|
+
export interface SessionRow {
|
|
4
|
+
path: string; id: string; name: string | null; updated_at: string | null;
|
|
5
|
+
workspace?: string; parent_session?: string; text?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SearchQuery {
|
|
8
|
+
tokens: { kind: "fuzzy" | "phrase"; value: string }[];
|
|
9
|
+
regex?: RegExp;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export function parseSessionQuery(query: string): SearchQuery {
|
|
13
|
+
const trimmed = query.trim();
|
|
14
|
+
if (trimmed.startsWith("re:")) {
|
|
15
|
+
if (!trimmed.slice(3).trim()) return { tokens: [], error: "Empty regex" };
|
|
16
|
+
try { return { tokens: [], regex: new RegExp(trimmed.slice(3).trim(), "i") }; }
|
|
17
|
+
catch (error) { return { tokens: [], error: String(error) }; }
|
|
18
|
+
}
|
|
19
|
+
const tokens: SearchQuery["tokens"] = [];
|
|
20
|
+
let buffer = "", quoted = false;
|
|
21
|
+
const flush = () => { if (buffer.trim()) tokens.push({ kind: quoted ? "phrase" : "fuzzy", value: buffer.trim() }); buffer = ""; };
|
|
22
|
+
for (const char of trimmed) {
|
|
23
|
+
if (char === '"') { flush(); quoted = !quoted; }
|
|
24
|
+
else if (!quoted && /\s/.test(char)) flush();
|
|
25
|
+
else buffer += char;
|
|
26
|
+
}
|
|
27
|
+
if (quoted) return { tokens: trimmed.split(/\s+/).filter(Boolean).map(value => ({ kind: "fuzzy", value })) };
|
|
28
|
+
flush(); return { tokens };
|
|
29
|
+
}
|
|
30
|
+
export function selectSessions<T extends SessionRow>(rows: readonly T[], query: string, sort: SessionSort, named: boolean,
|
|
31
|
+
fuzzy: (query: string, text: string) => { matches: boolean; score: number }): (T & { depth: number })[] {
|
|
32
|
+
const parsed = parseSessionQuery(query);
|
|
33
|
+
if (parsed.error) return [];
|
|
34
|
+
const scored = rows.flatMap(row => {
|
|
35
|
+
if (named && !row.name?.trim()) return [];
|
|
36
|
+
const text = `${row.id} ${row.name ?? ""} ${row.text ?? ""} ${row.workspace ?? ""}`;
|
|
37
|
+
let score = 0;
|
|
38
|
+
if (parsed.regex) { const i = text.search(parsed.regex); if (i < 0) return []; score = i * 0.1; }
|
|
39
|
+
else for (const token of parsed.tokens) {
|
|
40
|
+
if (token.kind === "phrase") {
|
|
41
|
+
const i = text.toLowerCase().replace(/\s+/g, " ").indexOf(token.value.toLowerCase().replace(/\s+/g, " "));
|
|
42
|
+
if (i < 0) return []; score += i * 0.1;
|
|
43
|
+
} else { const match = fuzzy(token.value, text); if (!match.matches) return []; score += match.score; }
|
|
44
|
+
}
|
|
45
|
+
return [{ row, score }];
|
|
46
|
+
});
|
|
47
|
+
const recent = (a: SessionRow, b: SessionRow) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "") || a.path.localeCompare(b.path);
|
|
48
|
+
scored.sort((a, b) => query.trim() && sort !== "recent" ? a.score - b.score || recent(a.row, b.row) : recent(a.row, b.row));
|
|
49
|
+
if (sort !== "threaded" || query.trim()) return scored.map(({ row }) => ({ ...row, depth: 0 }));
|
|
50
|
+
const byPath = new Map(scored.map(({ row }) => [row.path, row]));
|
|
51
|
+
const children = new Map<string, T[]>();
|
|
52
|
+
const roots: T[] = [];
|
|
53
|
+
for (const { row } of scored) {
|
|
54
|
+
if (row.parent_session && row.parent_session !== row.path && byPath.has(row.parent_session)) {
|
|
55
|
+
const list = children.get(row.parent_session) ?? []; list.push(row); children.set(row.parent_session, list);
|
|
56
|
+
} else roots.push(row);
|
|
57
|
+
}
|
|
58
|
+
// Allocation can be arbitrarily deep. Iterative traversal handles cycles without hiding their rows.
|
|
59
|
+
const latest = new Map(scored.map(({ row }) => [row.path, row.updated_at ?? ""]));
|
|
60
|
+
for (const { row } of scored) {
|
|
61
|
+
const visited = new Set<string>([row.path]);
|
|
62
|
+
let parent = row.parent_session;
|
|
63
|
+
while (parent && byPath.has(parent) && !visited.has(parent)) {
|
|
64
|
+
visited.add(parent);
|
|
65
|
+
if ((row.updated_at ?? "") > (latest.get(parent) ?? "")) latest.set(parent, row.updated_at ?? "");
|
|
66
|
+
parent = byPath.get(parent)?.parent_session;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const byActivity = (a: T, b: T) => (latest.get(b.path) ?? "").localeCompare(latest.get(a.path) ?? "") || recent(a, b);
|
|
70
|
+
roots.sort(byActivity); for (const list of children.values()) list.sort(byActivity);
|
|
71
|
+
const out: (T & { depth: number })[] = [], visited = new Set<string>();
|
|
72
|
+
for (const root of [...roots, ...scored.map(s => s.row)]) {
|
|
73
|
+
const stack = [{ row: root, depth: 0 }];
|
|
74
|
+
while (stack.length) {
|
|
75
|
+
const next = stack.pop()!;
|
|
76
|
+
if (visited.has(next.row.path)) continue;
|
|
77
|
+
visited.add(next.row.path); out.push({ ...next.row, depth: next.depth });
|
|
78
|
+
for (const row of [...children.get(next.row.path) ?? []].reverse()) stack.push({ row, depth: next.depth + 1 });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Session intent is decided before any log is opened (D8, Pi entry parity). */
|
|
2
|
+
export interface SessionEntryFlags {
|
|
3
|
+
explicit?: string;
|
|
4
|
+
fresh?: boolean;
|
|
5
|
+
continueLatest?: boolean;
|
|
6
|
+
noSession?: boolean;
|
|
7
|
+
session?: string;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
resume?: string | boolean;
|
|
10
|
+
forkFrom?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SessionEntry =
|
|
14
|
+
| { kind: "new" }
|
|
15
|
+
| { kind: "ephemeral" }
|
|
16
|
+
| { kind: "continue" }
|
|
17
|
+
| { kind: "select" }
|
|
18
|
+
| { kind: "named"; id: string }
|
|
19
|
+
| { kind: "open"; target: string; explicit: boolean }
|
|
20
|
+
| { kind: "fork"; target: string };
|
|
21
|
+
|
|
22
|
+
export function planSessionEntry(flags: SessionEntryFlags): SessionEntry {
|
|
23
|
+
const choices: { flag: string; entry: SessionEntry }[] = [];
|
|
24
|
+
if (flags.explicit) choices.push({ flag: "--log", entry: { kind: "open", target: flags.explicit, explicit: true } });
|
|
25
|
+
if (flags.session) choices.push({ flag: "--session", entry: { kind: "open", target: flags.session, explicit: false } });
|
|
26
|
+
if (flags.sessionId) {
|
|
27
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(flags.sessionId)) throw new Error("--session-id requires a plain session identifier");
|
|
28
|
+
choices.push({ flag: "--session-id", entry: { kind: "named", id: flags.sessionId } });
|
|
29
|
+
}
|
|
30
|
+
if (flags.resume) choices.push({ flag: "--resume", entry: typeof flags.resume === "string"
|
|
31
|
+
? { kind: "open", target: flags.resume, explicit: false } : { kind: "select" } });
|
|
32
|
+
if (flags.forkFrom) choices.push({ flag: "--fork", entry: { kind: "fork", target: flags.forkFrom } });
|
|
33
|
+
if (flags.continueLatest) choices.push({ flag: "--continue", entry: { kind: "continue" } });
|
|
34
|
+
// --fresh predates --no-session; together they still mean a fresh ephemeral log.
|
|
35
|
+
if (flags.fresh || flags.noSession) choices.push({ flag: flags.noSession ? "--no-session" : "--fresh",
|
|
36
|
+
entry: { kind: flags.noSession ? "ephemeral" : "new" } });
|
|
37
|
+
if (choices.length > 1) throw new Error(`conflicting session options: ${choices.map((c) => c.flag).join(", ")}`);
|
|
38
|
+
return choices[0]?.entry ?? { kind: "new" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SavedModel {
|
|
42
|
+
model: string;
|
|
43
|
+
provider?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Legacy logs have only an ID; accepting one requires a unique catalog match. */
|
|
47
|
+
export function restoreSessionModel<T extends { id: string; provider: string }>(
|
|
48
|
+
saved: SavedModel,
|
|
49
|
+
catalog: readonly T[],
|
|
50
|
+
): T {
|
|
51
|
+
const matches = catalog.filter((m) => m.id === saved.model && (saved.provider === undefined || m.provider === saved.provider));
|
|
52
|
+
const label = saved.provider === undefined ? saved.model : `${saved.provider}/${saved.model}`;
|
|
53
|
+
if (matches.length === 0) throw new Error(`saved model ${label} is unavailable. Restore its catalog entry, or start a new session`);
|
|
54
|
+
if (matches.length !== 1) throw new Error(`saved model ${label} is ambiguous across providers. Restore an unambiguous catalog before resuming`);
|
|
55
|
+
return matches[0]!;
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** D8: session configuration is projected from the append-only metadata journal. */
|
|
2
|
+
export interface PositionedState { node: number; at_event?: string }
|
|
3
|
+
|
|
4
|
+
export function stateAtPosition<T extends PositionedState>(
|
|
5
|
+
states: readonly T[], node: number, eventIds: readonly string[], atEvent?: string,
|
|
6
|
+
): T | null {
|
|
7
|
+
const positions = new Map(eventIds.map((id, index) => [id, index]));
|
|
8
|
+
const target = atEvent === undefined ? eventIds.length : positions.get(atEvent);
|
|
9
|
+
if (target === undefined) throw new Error(`NO_SUCH_EVENT: ${atEvent}`);
|
|
10
|
+
let result: T | null = null;
|
|
11
|
+
for (const state of states) {
|
|
12
|
+
if (state.node !== node) continue;
|
|
13
|
+
const position = state.at_event === undefined ? -1 : positions.get(state.at_event);
|
|
14
|
+
if (position !== undefined && position <= target) result = state;
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sameModel(a: { model: string; provider?: string }, b: { model: string; provider?: string }): boolean {
|
|
20
|
+
return a.model === b.model && a.provider !== undefined && a.provider === b.provider;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Selector values must remain unambiguous when two providers serve the same ID. */
|
|
24
|
+
export function modelKey(model: { provider: string; id: string }): string {
|
|
25
|
+
return `${model.provider}/${model.id}`;
|
|
26
|
+
}
|
package/src/core/settings.ts
CHANGED
|
@@ -1,174 +1,189 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* decides which keys exist and which layer wins.
|
|
4
|
-
*
|
|
5
|
-
* Write-back is a patch of keys the user actually changed. Dumping the
|
|
6
|
-
* whole default object into ~/.xeno/settings.json freezes today's defaults
|
|
7
|
-
* for that user forever (feat-theming design).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export const SETTINGS_KEYS = [
|
|
11
|
-
"theme",
|
|
12
|
-
"thinking",
|
|
13
|
-
"statusBar",
|
|
14
|
-
"defaultProvider",
|
|
15
|
-
"defaultModel",
|
|
16
|
-
"defaultThinking",
|
|
17
|
-
"scopedModels",
|
|
18
|
-
] as const;
|
|
19
|
-
|
|
20
|
-
export type SettingsKey = (typeof SETTINGS_KEYS)[number];
|
|
1
|
+
import { THINKING_LEVELS, asThinkingLevel, type ThinkingLevel } from "./thinking.ts";
|
|
2
|
+
import { packageDeclaration, type PackageDeclaration } from "./packages.ts";
|
|
21
3
|
|
|
4
|
+
/** Validated configuration. D19: display policy never mutates the log. */
|
|
22
5
|
export interface Settings {
|
|
23
6
|
readonly theme?: string;
|
|
24
7
|
readonly thinking?: string;
|
|
25
8
|
readonly statusBar?: boolean;
|
|
9
|
+
readonly hideThinkingBlock?: boolean;
|
|
10
|
+
readonly showCacheMissNotices?: boolean;
|
|
26
11
|
readonly defaultProvider?: string;
|
|
27
12
|
readonly defaultModel?: string;
|
|
28
|
-
/** Ctrl+S in the thinking selector. Distinct from `thinking` so /settings does not overwrite a saved default. */
|
|
29
13
|
readonly defaultThinking?: string;
|
|
30
|
-
|
|
14
|
+
readonly modelThinkingLevels?: Readonly<Record<string, ThinkingLevel>>;
|
|
31
15
|
readonly scopedModels?: readonly string[];
|
|
16
|
+
readonly steeringMode?: "all" | "one-at-a-time";
|
|
17
|
+
readonly followUpMode?: "all" | "one-at-a-time";
|
|
18
|
+
readonly tuiMode?: "regular" | "fullscreen";
|
|
19
|
+
readonly fullscreenExitOutput?: "transcript" | "resume-hint";
|
|
20
|
+
readonly fullscreenScrollbar?: "auto" | "always" | "hidden";
|
|
21
|
+
readonly fullscreenCopyOnSelect?: boolean;
|
|
22
|
+
readonly fullscreenWheelScrollLines?: "auto" | number;
|
|
23
|
+
readonly editorPaddingX?: number;
|
|
24
|
+
readonly outputPad?: 0 | 1;
|
|
25
|
+
readonly autocompleteMaxVisible?: number;
|
|
26
|
+
readonly showHardwareCursor?: boolean;
|
|
27
|
+
readonly quietStartup?: boolean;
|
|
28
|
+
readonly doubleEscapeAction?: "tree" | "fork" | "none";
|
|
29
|
+
readonly treeFilterMode?: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
|
30
|
+
readonly externalEditor?: string;
|
|
31
|
+
readonly skills?: readonly string[];
|
|
32
|
+
readonly prompts?: readonly string[];
|
|
33
|
+
readonly themes?: readonly string[];
|
|
34
|
+
readonly extensions?: readonly string[];
|
|
35
|
+
readonly packages?: readonly PackageDeclaration[];
|
|
36
|
+
readonly enableSkillCommands?: boolean;
|
|
37
|
+
readonly defaultProjectTrust?: "ask" | "always" | "never";
|
|
38
|
+
readonly terminal?: { readonly clearOnShrink?: boolean; readonly showTerminalProgress?: boolean; readonly showImages?: boolean; readonly imageWidthCells?: number };
|
|
39
|
+
readonly images?: { readonly autoResize?: boolean; readonly blockImages?: boolean };
|
|
40
|
+
readonly markdown?: { readonly mermaid?: "off" | "final" | "streaming" };
|
|
32
41
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
readonly
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
42
|
+
export const SETTINGS_KEYS = ["theme", "thinking", "statusBar", "hideThinkingBlock", "showCacheMissNotices", "defaultProvider", "defaultModel", "defaultThinking",
|
|
43
|
+
"modelThinkingLevels", "scopedModels", "steeringMode", "followUpMode", "tuiMode", "fullscreenExitOutput", "fullscreenScrollbar",
|
|
44
|
+
"fullscreenCopyOnSelect", "fullscreenWheelScrollLines", "editorPaddingX", "outputPad", "autocompleteMaxVisible",
|
|
45
|
+
"showHardwareCursor", "quietStartup", "doubleEscapeAction", "treeFilterMode", "externalEditor", "terminal", "images", "markdown",
|
|
46
|
+
"skills", "prompts", "themes", "extensions", "packages", "enableSkillCommands", "defaultProjectTrust"] as const;
|
|
47
|
+
export type SettingsKey = (typeof SETTINGS_KEYS)[number];
|
|
48
|
+
export interface SettingsLayers { readonly builtin: Settings; readonly user?: Settings; readonly project?: Settings; readonly cli?: Settings }
|
|
49
|
+
export interface SettingsParse { readonly settings: Settings; readonly diagnostics: readonly string[] }
|
|
50
|
+
type Rule = { readonly kind: "boolean" | "string" | "strings" } | { readonly kind: "enum"; readonly values: readonly string[] } |
|
|
51
|
+
{ readonly kind: "integer"; readonly min: number; readonly max: number; readonly auto?: boolean };
|
|
52
|
+
const levels: Rule = { kind: "enum", values: THINKING_LEVELS };
|
|
53
|
+
const delivery: Rule = { kind: "enum", values: ["all", "one-at-a-time"] };
|
|
54
|
+
const rules: Readonly<Record<string, Rule>> = {
|
|
55
|
+
theme: { kind: "string" }, thinking: levels, statusBar: { kind: "boolean" },
|
|
56
|
+
hideThinkingBlock: { kind: "boolean" }, showCacheMissNotices: { kind: "boolean" },
|
|
57
|
+
defaultProvider: { kind: "string" }, defaultModel: { kind: "string" }, defaultThinking: levels,
|
|
58
|
+
scopedModels: { kind: "strings" }, steeringMode: delivery, followUpMode: delivery,
|
|
59
|
+
tuiMode: { kind: "enum", values: ["regular", "fullscreen"] },
|
|
60
|
+
fullscreenExitOutput: { kind: "enum", values: ["transcript", "resume-hint"] },
|
|
61
|
+
fullscreenScrollbar: { kind: "enum", values: ["auto", "always", "hidden"] },
|
|
62
|
+
fullscreenCopyOnSelect: { kind: "boolean" },
|
|
63
|
+
// Pi's documented display bounds; these are not context/segment thresholds.
|
|
64
|
+
fullscreenWheelScrollLines: { kind: "integer", min: 1, max: 100, auto: true },
|
|
65
|
+
editorPaddingX: { kind: "integer", min: 0, max: 3 }, outputPad: { kind: "integer", min: 0, max: 1 },
|
|
66
|
+
autocompleteMaxVisible: { kind: "integer", min: 3, max: 20 },
|
|
67
|
+
showHardwareCursor: { kind: "boolean" }, quietStartup: { kind: "boolean" },
|
|
68
|
+
doubleEscapeAction: { kind: "enum", values: ["tree", "fork", "none"] },
|
|
69
|
+
treeFilterMode: { kind: "enum", values: ["default", "no-tools", "user-only", "labeled-only", "all"] },
|
|
70
|
+
externalEditor: { kind: "string" }, "terminal.clearOnShrink": { kind: "boolean" },
|
|
71
|
+
"terminal.showTerminalProgress": { kind: "boolean" },
|
|
72
|
+
"terminal.showImages": { kind: "boolean" }, "terminal.imageWidthCells": { kind: "integer", min: 1, max: 240 },
|
|
73
|
+
"images.autoResize": { kind: "boolean" }, "images.blockImages": { kind: "boolean" },
|
|
74
|
+
"markdown.mermaid": { kind: "enum", values: ["off", "final", "streaming"] },
|
|
75
|
+
skills: { kind: "strings" }, prompts: { kind: "strings" }, themes: { kind: "strings" }, extensions: { kind: "strings" },
|
|
76
|
+
enableSkillCommands: { kind: "boolean" }, defaultProjectTrust: { kind: "enum", values: ["ask", "always", "never"] },
|
|
77
|
+
};
|
|
78
|
+
/** One catalog supplies menu values and command validation. */
|
|
79
|
+
export const DISPLAY_SETTINGS = [
|
|
80
|
+
{ id: "hideThinkingBlock", label: "Hide thinking blocks", fallback: "on" },
|
|
81
|
+
{ id: "showCacheMissNotices", label: "Cache miss notices", fallback: "off" },
|
|
82
|
+
{ id: "enableSkillCommands", label: "Skill commands", fallback: "on" },
|
|
83
|
+
{ id: "defaultProjectTrust", label: "Default project trust", fallback: "ask" },
|
|
84
|
+
{ id: "terminal.showImages", label: "Show images", fallback: "on" },
|
|
85
|
+
{ id: "terminal.imageWidthCells", label: "Image width", fallback: "60" },
|
|
86
|
+
{ id: "images.autoResize", label: "Resize images", fallback: "on" },
|
|
87
|
+
{ id: "images.blockImages", label: "Block image transmission", fallback: "off" },
|
|
88
|
+
{ id: "markdown.mermaid", label: "Mermaid diagrams", fallback: "streaming" },
|
|
89
|
+
{ id: "tuiMode", label: "TUI mode", fallback: "regular" },
|
|
90
|
+
{ id: "fullscreenExitOutput", label: "Fullscreen exit output", fallback: "transcript" },
|
|
91
|
+
{ id: "fullscreenScrollbar", label: "Fullscreen scrollbar", fallback: "auto" },
|
|
92
|
+
{ id: "fullscreenCopyOnSelect", label: "Fullscreen copy on select", fallback: "on" },
|
|
93
|
+
{ id: "fullscreenWheelScrollLines", label: "Fullscreen wheel scroll lines", fallback: "auto" },
|
|
94
|
+
{ id: "editorPaddingX", label: "Editor padding", fallback: "0" },
|
|
95
|
+
{ id: "outputPad", label: "Output padding", fallback: "1" },
|
|
96
|
+
{ id: "autocompleteMaxVisible", label: "Autocomplete rows", fallback: "5" },
|
|
97
|
+
{ id: "showHardwareCursor", label: "Hardware cursor", fallback: "on" },
|
|
98
|
+
{ id: "quietStartup", label: "Quiet startup", fallback: "off" },
|
|
99
|
+
{ id: "doubleEscapeAction", label: "Double Escape", fallback: "tree" },
|
|
100
|
+
{ id: "treeFilterMode", label: "Tree filter", fallback: "default" },
|
|
101
|
+
{ id: "terminal.clearOnShrink", label: "Clear on shrink", fallback: "off" },
|
|
102
|
+
{ id: "terminal.showTerminalProgress", label: "Terminal progress", fallback: "on" },
|
|
103
|
+
] as const;
|
|
104
|
+
export function defaultSettings(): Settings { return { theme: "dark", thinking: "high", statusBar: true }; }
|
|
52
105
|
export function mergeSettings(layers: SettingsLayers): Settings {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
...
|
|
106
|
+
const { defaultProjectTrust: _projectCannotGrantTrust, ...project } = layers.project ?? {};
|
|
107
|
+
const all = [layers.builtin, layers.user ?? {}, project, layers.cli ?? {}];
|
|
108
|
+
const merged: Settings = Object.assign({}, ...all);
|
|
109
|
+
return { ...merged,
|
|
110
|
+
...(all.some(s => s.terminal !== undefined) ? { terminal: Object.assign({}, ...all.map(s => s.terminal ?? {})) } : {}),
|
|
111
|
+
...(all.some(s => s.images !== undefined) ? { images: Object.assign({}, ...all.map(s => s.images ?? {})) } : {}),
|
|
112
|
+
...(all.some(s => s.markdown !== undefined) ? { markdown: Object.assign({}, ...all.map(s => s.markdown ?? {})) } : {}),
|
|
113
|
+
...(all.some(s => s.modelThinkingLevels !== undefined) ? { modelThinkingLevels: Object.assign({}, ...all.map(s => s.modelThinkingLevels ?? {})) } : {}),
|
|
58
114
|
};
|
|
59
115
|
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Only keys present on `explicit`. Passing the merged object here would
|
|
63
|
-
* persist defaults and pin old users to them.
|
|
64
|
-
*/
|
|
65
116
|
export function settingsToWrite(explicit: Partial<Settings>): Record<string, unknown> {
|
|
66
|
-
|
|
67
|
-
if (explicit.theme !== undefined) out["theme"] = explicit.theme;
|
|
68
|
-
if (explicit.thinking !== undefined) out["thinking"] = explicit.thinking;
|
|
69
|
-
if (explicit.statusBar !== undefined) out["statusBar"] = explicit.statusBar;
|
|
70
|
-
if (explicit.defaultProvider !== undefined) out["defaultProvider"] = explicit.defaultProvider;
|
|
71
|
-
if (explicit.defaultModel !== undefined) out["defaultModel"] = explicit.defaultModel;
|
|
72
|
-
if (explicit.defaultThinking !== undefined) out["defaultThinking"] = explicit.defaultThinking;
|
|
73
|
-
if (explicit.scopedModels !== undefined) out["scopedModels"] = explicit.scopedModels;
|
|
74
|
-
return out;
|
|
117
|
+
return Object.fromEntries(SETTINGS_KEYS.filter(key => explicit[key] !== undefined).map(key => [key, explicit[key]]));
|
|
75
118
|
}
|
|
76
|
-
|
|
119
|
+
export function explicitSettings(raw: unknown, parsed: Settings): Partial<Settings> {
|
|
120
|
+
if (!isRecord(raw)) return {};
|
|
121
|
+
return Object.fromEntries(SETTINGS_KEYS.filter(key => (key in raw || Object.entries(aliases).some(([alias, canonical]) => canonical === key && alias in raw)) && parsed[key] !== undefined).map(key => [key, parsed[key]]));
|
|
122
|
+
}
|
|
123
|
+
const aliases: Readonly<Record<string, string>> = { defaultThinkingLevel: "defaultThinking", enabledModels: "scopedModels" };
|
|
77
124
|
export function parseSettings(json: unknown): SettingsParse {
|
|
78
|
-
if (json === null || json === undefined) {
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
else
|
|
125
|
+
if (json === null || json === undefined) return { settings: {}, diagnostics: [] };
|
|
126
|
+
if (!isRecord(json)) return { settings: {}, diagnostics: ["settings is not an object"] };
|
|
127
|
+
const out: Record<string, unknown> = {}, diagnostics: string[] = [];
|
|
128
|
+
for (const [rawKey, value] of Object.entries(json)) {
|
|
129
|
+
const key = aliases[rawKey] ?? rawKey;
|
|
130
|
+
if (rawKey !== key && key in json) continue;
|
|
131
|
+
if (value === undefined) continue;
|
|
132
|
+
if (key === "packages") {
|
|
133
|
+
if (Array.isArray(value) && value.every(packageDeclaration)) out[key] = value;
|
|
134
|
+
else diagnostics.push("settings.packages must contain source strings or package resource filters");
|
|
135
|
+
} else if (key === "modelThinkingLevels") {
|
|
136
|
+
if (!isRecord(value)) { diagnostics.push("settings.modelThinkingLevels must be an object"); continue; }
|
|
137
|
+
const mapped: Record<string, ThinkingLevel> = {};
|
|
138
|
+
for (const [id, level] of Object.entries(value)) {
|
|
139
|
+
const parsed = asThinkingLevel(level);
|
|
140
|
+
if (id.includes("/") && !id.startsWith("/") && !id.endsWith("/") && parsed) mapped[id] = parsed;
|
|
141
|
+
else diagnostics.push("settings.modelThinkingLevels requires provider/model identities and supported thinking names");
|
|
142
|
+
}
|
|
143
|
+
out[key] = mapped;
|
|
144
|
+
} else if (key === "terminal" || key === "images" || key === "markdown") {
|
|
145
|
+
if (!isRecord(value)) { diagnostics.push("settings." + key + " must be an object"); continue; }
|
|
146
|
+
const nested: Record<string, unknown> = {};
|
|
147
|
+
for (const [subkey, subvalue] of Object.entries(value)) validate(key + "." + subkey, subvalue, nested, diagnostics, subkey);
|
|
148
|
+
out[key] = nested;
|
|
149
|
+
} else validate(key, value, out, diagnostics, key);
|
|
98
150
|
}
|
|
99
|
-
|
|
100
|
-
const thinking = rec["thinking"];
|
|
101
|
-
if (thinking !== undefined) {
|
|
102
|
-
if (typeof thinking === "string" && THINKING_LEVELS.has(thinking)) settings.thinking = thinking;
|
|
103
|
-
else diagnostics.push("settings.thinking must be low|medium|high|xhigh|max");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const statusBar = rec["statusBar"];
|
|
107
|
-
if (statusBar !== undefined) {
|
|
108
|
-
if (typeof statusBar === "boolean") settings.statusBar = statusBar;
|
|
109
|
-
else diagnostics.push("settings.statusBar must be a boolean");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const defaultProvider = rec["defaultProvider"];
|
|
113
|
-
if (defaultProvider !== undefined) {
|
|
114
|
-
if (typeof defaultProvider === "string") settings.defaultProvider = defaultProvider;
|
|
115
|
-
else diagnostics.push("settings.defaultProvider must be a string");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const defaultModel = rec["defaultModel"];
|
|
119
|
-
if (defaultModel !== undefined) {
|
|
120
|
-
if (typeof defaultModel === "string") settings.defaultModel = defaultModel;
|
|
121
|
-
else diagnostics.push("settings.defaultModel must be a string");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const defaultThinking = rec["defaultThinking"];
|
|
125
|
-
if (defaultThinking !== undefined) {
|
|
126
|
-
if (typeof defaultThinking === "string" && THINKING_LEVELS.has(defaultThinking)) {
|
|
127
|
-
settings.defaultThinking = defaultThinking;
|
|
128
|
-
} else diagnostics.push("settings.defaultThinking must be low|medium|high|xhigh|max");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const scopedModels = rec["scopedModels"];
|
|
132
|
-
if (scopedModels !== undefined) {
|
|
133
|
-
const parsed = parseScopedModels(scopedModels);
|
|
134
|
-
if (parsed === undefined) diagnostics.push("settings.scopedModels must be an array of strings");
|
|
135
|
-
else settings.scopedModels = parsed;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { settings, diagnostics };
|
|
151
|
+
return { settings: out as Settings, diagnostics };
|
|
139
152
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (Array.isArray(raw) && raw.every((x) => typeof x === "string")) return raw as string[];
|
|
143
|
-
if (typeof raw === "string") {
|
|
144
|
-
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
145
|
-
}
|
|
146
|
-
return undefined;
|
|
153
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
154
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
147
155
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if ("
|
|
163
|
-
if ("
|
|
164
|
-
if ("
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
156
|
+
function validate(key: string, raw: unknown, out: Record<string, unknown>, diagnostics: string[], field: string): void {
|
|
157
|
+
const rule = rules[key];
|
|
158
|
+
if (!rule) { diagnostics.push("unknown setting: " + key); return; }
|
|
159
|
+
const value = rule.kind === "strings" && typeof raw === "string" ? raw.split(",").map(v => v.trim()).filter(Boolean) : raw;
|
|
160
|
+
const valid = rule.kind === "string" ? typeof value === "string" && value.length > 0 :
|
|
161
|
+
rule.kind === "boolean" ? typeof value === "boolean" :
|
|
162
|
+
rule.kind === "strings" ? Array.isArray(value) && value.every(v => typeof v === "string") :
|
|
163
|
+
rule.kind === "enum" ? typeof value === "string" && rule.values.includes(value) :
|
|
164
|
+
rule.kind === "integer" && ((rule.auto && value === "auto") || (typeof value === "number" && Number.isSafeInteger(value) && value >= rule.min && value <= rule.max));
|
|
165
|
+
if (valid) out[field] = value;
|
|
166
|
+
else diagnostics.push("settings." + key + " must be " + (rule.kind === "enum" ? rule.values.join("|") : rule.kind === "integer" ? rule.min + ".." + rule.max + (rule.auto ? " or auto" : "") : rule.kind));
|
|
167
|
+
}
|
|
168
|
+
export function settingValues(key: string): string[] {
|
|
169
|
+
const rule = rules[key];
|
|
170
|
+
if (rule?.kind === "enum") return [...rule.values];
|
|
171
|
+
if (rule?.kind === "boolean") return ["on", "off"];
|
|
172
|
+
if (rule?.kind === "integer") return [...(rule.auto ? ["auto"] : []), ...Array.from({ length: rule.max - rule.min + 1 }, (_, i) => String(i + rule.min))];
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
export function settingValue(settings: Settings, path: string, fallback: string): string {
|
|
176
|
+
const [key, subkey] = path.split(".");
|
|
177
|
+
const object = settings[key as SettingsKey];
|
|
178
|
+
const raw = subkey && isRecord(object) ? (object as Record<string, unknown>)[subkey] : object;
|
|
179
|
+
return typeof raw === "boolean" ? raw ? "on" : "off" : raw === undefined ? fallback : String(raw);
|
|
180
|
+
}
|
|
181
|
+
export function settingPatch(field: string, text: string): SettingsParse {
|
|
182
|
+
const key = Object.keys(rules).find(k => k.toLowerCase() === field.toLowerCase());
|
|
183
|
+
if (!key) return { settings: {}, diagnostics: ["unknown setting: " + field] };
|
|
184
|
+
const rule = rules[key]!;
|
|
185
|
+
const value = rule.kind === "boolean" ? ["on", "true", "1"].includes(text) ? true : ["off", "false", "0"].includes(text) ? false : text :
|
|
186
|
+
rule.kind === "integer" && text !== "auto" ? Number(text) : text;
|
|
187
|
+
const [first, second] = key.split(".");
|
|
188
|
+
return parseSettings(second ? { [first!]: { [second]: value } } : { [key]: value });
|
|
174
189
|
}
|
package/src/core/theme.ts
CHANGED
|
@@ -36,7 +36,8 @@ export const THEME_TOKENS = [
|
|
|
36
36
|
|
|
37
37
|
export type ThemeToken = (typeof THEME_TOKENS)[number];
|
|
38
38
|
|
|
39
|
-
export type ThemeColors = { readonly [K in ThemeToken]: string }
|
|
39
|
+
export type ThemeColors = { readonly [K in ThemeToken]: string } & Readonly<Record<string, string>>;
|
|
40
|
+
const PI_THEME_TOKENS: readonly string[] = ["accent","border","borderAccent","borderMuted","success","error","warning","muted","dim","text","thinkingText","scrollbarTrack","scrollbarThumb","searchMatchText","userMessageText","customMessageText","customMessageLabel","toolTitle","toolOutput","mdHeading","mdLink","mdLinkUrl","mdCode","mdCodeBlock","mdCodeBlockBorder","mdQuote","mdQuoteBorder","mdHr","mdListBullet","toolDiffAdded","toolDiffRemoved","toolDiffContext","syntaxComment","syntaxKeyword","syntaxFunction","syntaxVariable","syntaxString","syntaxNumber","syntaxType","syntaxOperator","syntaxPunctuation","thinkingOff","thinkingMinimal","thinkingLow","thinkingMedium","thinkingHigh","thinkingXhigh","thinkingMax","bashMode","selectedBg","searchMatchBg","userMessageBg","customMessageBg","toolPendingBg","toolSuccessBg","toolErrorBg"];
|
|
40
41
|
|
|
41
42
|
export interface Theme {
|
|
42
43
|
readonly name: string;
|
|
@@ -135,20 +136,28 @@ export function parseTheme(json: unknown): ThemeParse {
|
|
|
135
136
|
})();
|
|
136
137
|
|
|
137
138
|
for (const key of Object.keys(src)) {
|
|
138
|
-
if (!isThemeToken(key)) diagnostics.push(`unknown token: ${key}`);
|
|
139
|
+
if (!isThemeToken(key) && !PI_THEME_TOKENS.includes(key)) diagnostics.push(`unknown token: ${key}`);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
const colors: { [K in ThemeToken]: string } = { ...fallback.colors };
|
|
142
|
-
|
|
142
|
+
const colors: { [K in ThemeToken]: string } & Record<string, string> = { ...fallback.colors };
|
|
143
|
+
const vars = typeof rec["vars"] === "object" && rec["vars"] !== null ? rec["vars"] as Record<string, unknown> : {};
|
|
144
|
+
const resolve = (value: unknown, seen = new Set<string>()): unknown => {
|
|
145
|
+
if (typeof value !== "string" || !(value in vars)) return value;
|
|
146
|
+
if (seen.has(value)) return null;
|
|
147
|
+
return resolve(vars[value], new Set([...seen, value]));
|
|
148
|
+
};
|
|
149
|
+
for (const token of new Set<string>([...THEME_TOKENS, ...PI_THEME_TOKENS])) {
|
|
143
150
|
if (!(token in src)) continue;
|
|
144
|
-
const parsed = parseColor(src[token]);
|
|
151
|
+
const parsed = parseColor(resolve(src[token]));
|
|
145
152
|
if (parsed === null) {
|
|
146
153
|
diagnostics.push(`invalid colour for ${token}; using built-in`);
|
|
147
154
|
continue;
|
|
148
155
|
}
|
|
149
|
-
colors[token] = parsed;
|
|
156
|
+
colors[token] = token.endsWith("Bg") ? parsed.replace(/^38;/, "48;") : parsed;
|
|
150
157
|
}
|
|
151
158
|
|
|
159
|
+
if (!("status" in src) && "dim" in colors) colors.status = colors["dim"]!;
|
|
160
|
+
if (!("prompt" in src)) colors.prompt = colors.text;
|
|
152
161
|
return { theme: { name, colors }, diagnostics };
|
|
153
162
|
}
|
|
154
163
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Ported from Pi (MIT), ai/models.ts at 02eed88. Capability policy, independent of provider I/O. */
|
|
2
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
3
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
4
|
+
export interface ThinkingModel {
|
|
5
|
+
readonly reasoning: boolean;
|
|
6
|
+
readonly thinkingLevelMap?: Partial<Record<ThinkingLevel, string | null>>;
|
|
7
|
+
}
|
|
8
|
+
export function asThinkingLevel(value: unknown): ThinkingLevel | undefined {
|
|
9
|
+
return typeof value === "string" && (THINKING_LEVELS as readonly string[]).includes(value) ? value as ThinkingLevel : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function supportedThinkingLevels(model: ThinkingModel): ThinkingLevel[] {
|
|
12
|
+
if (!model.reasoning) return ["off"];
|
|
13
|
+
return THINKING_LEVELS.filter(level => {
|
|
14
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
15
|
+
if (mapped === null) return false;
|
|
16
|
+
return level === "xhigh" || level === "max" ? mapped !== undefined : true;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function clampThinkingLevel(model: ThinkingModel, requested: ThinkingLevel): ThinkingLevel {
|
|
20
|
+
const available = supportedThinkingLevels(model);
|
|
21
|
+
const index = THINKING_LEVELS.indexOf(requested);
|
|
22
|
+
for (const level of [...THINKING_LEVELS.slice(index), ...THINKING_LEVELS.slice(0, index).reverse()]) {
|
|
23
|
+
if (available.includes(level)) return level;
|
|
24
|
+
}
|
|
25
|
+
return "off";
|
|
26
|
+
}
|
package/src/core/tokens.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Message } from "./project.ts";
|
|
13
|
+
import { estimateImageTokens } from "./images.ts";
|
|
13
14
|
|
|
14
15
|
export interface TokenCounter {
|
|
15
16
|
readonly kind: "estimate" | "exact";
|
|
@@ -73,6 +74,7 @@ export const estimateTokens: TokenCounter = {
|
|
|
73
74
|
else if (b.type === "thinking") tokens += estimateText(JSON.stringify(b.raw));
|
|
74
75
|
else if (b.type === "tool_use") tokens += estimateText(b.name) + estimateText(JSON.stringify(b.input));
|
|
75
76
|
else if (b.type === "tool_result") tokens += estimateText(b.content);
|
|
77
|
+
else if (b.type === "image") tokens += estimateImageTokens(b.image);
|
|
76
78
|
}
|
|
77
79
|
// Per-message framing overhead, roughly.
|
|
78
80
|
tokens += 8 / 3.6;
|