@xynogen/pix-runtime 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +5 -1
- package/src/hashline.check.mjs +11 -0
- package/src/hashline.ts +71 -0
- package/src/migrations.ts +41 -10
- package/src/pix-command.ts +23 -8
- package/src/sections/index.ts +16 -0
- package/src/sections/pretty.ts +37 -2
- package/src/sections/services.ts +71 -0
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@ See `DESIGN.md` for the full contract.
|
|
|
10
10
|
## What it does
|
|
11
11
|
|
|
12
12
|
- Versioned, sparse `pix.json` (`$version: 1`) — defaults resolve in code.
|
|
13
|
-
- Typed sections: `collapse`, `pretty`, `io`, `compaction`, `optimizer`, `gate`.
|
|
13
|
+
- Typed sections: `collapse`, `pretty`, `io`, `compaction`, `optimizer`, `gate`, `fetch`, `search`, `voice`, `toolbox`.
|
|
14
|
+
- `/pix` has a Footer tab for `pretty.footer` visibility.
|
|
15
|
+
- Startup imports legacy service config files and archives them only after a successful save.
|
|
14
16
|
- Atomic writes behind a serialized in-process queue and a short-lived
|
|
15
17
|
cross-process lock. A failed write leaves the old file intact.
|
|
16
18
|
- Immutable, deeply frozen config snapshots with a monotonic revision.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Pix shared runtime — versioned pix.json config, atomic persistence, typed change events",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"./collapse": "./src/collapse.ts",
|
|
29
29
|
"./icon-catalog": "./src/icon-catalog.ts",
|
|
30
30
|
"./io": "./src/io.ts",
|
|
31
|
+
"./hashline": "./src/hashline.ts",
|
|
31
32
|
"./lfid": "./src/lfid.ts",
|
|
32
33
|
"./once": "./src/once.ts",
|
|
33
34
|
"./paths": "./src/paths.ts",
|
|
@@ -63,5 +64,8 @@
|
|
|
63
64
|
"peerDependencies": {
|
|
64
65
|
"@earendil-works/pi-coding-agent": "*",
|
|
65
66
|
"@earendil-works/pi-tui": "*"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"xxhash-wasm": "1.1.0"
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import xxhash from "xxhash-wasm";
|
|
3
|
+
|
|
4
|
+
const { h32, h32Raw } = await xxhash();
|
|
5
|
+
// Official xxHash v0.8.3 tests/sanity_test_vectors.h, empty-input vectors.
|
|
6
|
+
assert.equal(h32("", 0), 0x02cc5d05);
|
|
7
|
+
assert.equal(h32("", 0x9e3779b1), 0x36b78ae7);
|
|
8
|
+
for (const text of ["", "hello", "中文🙂", " a ", "\tconst x = 1; "]) {
|
|
9
|
+
assert.equal(h32(text, 0), h32Raw(new TextEncoder().encode(text), 0));
|
|
10
|
+
console.log(`${(h32(text, 0) & 0xfff).toString(16).toUpperCase().padStart(3, "0")}|${text}`);
|
|
11
|
+
}
|
package/src/hashline.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import xxhash from "xxhash-wasm";
|
|
2
|
+
|
|
3
|
+
// Pi reevaluates shared modules with Jiti. Both tools must share one WASM instance.
|
|
4
|
+
const global = globalThis as typeof globalThis & {
|
|
5
|
+
__pixHashline?: {
|
|
6
|
+
initialization?: Promise<void>;
|
|
7
|
+
h32?: (input: string, seed?: number) => number;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
const state = global.__pixHashline ?? {};
|
|
11
|
+
global.__pixHashline = state;
|
|
12
|
+
|
|
13
|
+
export function initHashline(): Promise<void> {
|
|
14
|
+
state.initialization ??= xxhash()
|
|
15
|
+
.then((hash) => {
|
|
16
|
+
state.h32 = hash.h32;
|
|
17
|
+
})
|
|
18
|
+
.catch((error) => {
|
|
19
|
+
throw new Error("Hashline WASM initialization failed", { cause: error });
|
|
20
|
+
});
|
|
21
|
+
return state.initialization;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hashLine(text: string): string {
|
|
25
|
+
if (!state.h32) throw new Error("Hashline WASM is not initialized");
|
|
26
|
+
return (state.h32(text, 0) & 0xfff).toString(16).toUpperCase().padStart(3, "0");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function anchor(line: number, text: string): string {
|
|
30
|
+
return `${line}#${hashLine(text)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseAnchor(value: unknown): { line: number; hash: string } {
|
|
34
|
+
const match = typeof value === "string" ? /^([1-9]\d*)#([0-9A-F]{3})$/.exec(value) : null;
|
|
35
|
+
if (!match || !Number.isSafeInteger(Number(match[1])))
|
|
36
|
+
throw new Error(`Invalid anchor: ${String(value)}`);
|
|
37
|
+
return { line: Number(match[1]), hash: match[2]! };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type SourceLine = { text: string; ending: string };
|
|
41
|
+
|
|
42
|
+
export function decodeLines(bytes: Uint8Array): {
|
|
43
|
+
bom: string;
|
|
44
|
+
lines: SourceLine[];
|
|
45
|
+
delimiter: string;
|
|
46
|
+
finalNewline: boolean;
|
|
47
|
+
} {
|
|
48
|
+
const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
49
|
+
if (decoded.includes("\0")) throw new Error("Unsupported binary text: NUL byte");
|
|
50
|
+
const bom = decoded.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
51
|
+
const text = decoded.slice(bom.length);
|
|
52
|
+
const lines: SourceLine[] = [];
|
|
53
|
+
const counts = new Map<string, number>();
|
|
54
|
+
let start = 0;
|
|
55
|
+
for (const match of text.matchAll(/\r\n|\r|\n/g)) {
|
|
56
|
+
const ending = match[0];
|
|
57
|
+
lines.push({ text: text.slice(start, match.index), ending });
|
|
58
|
+
counts.set(ending, (counts.get(ending) ?? 0) + 1);
|
|
59
|
+
start = match.index + ending.length;
|
|
60
|
+
}
|
|
61
|
+
if (start < text.length) lines.push({ text: text.slice(start), ending: "" });
|
|
62
|
+
let delimiter = "\n";
|
|
63
|
+
let highest = 0;
|
|
64
|
+
for (const [ending, count] of counts) {
|
|
65
|
+
if (count > highest) {
|
|
66
|
+
delimiter = ending;
|
|
67
|
+
highest = count;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { bom, lines, delimiter, finalNewline: lines.length > 0 && lines.at(-1)!.ending !== "" };
|
|
71
|
+
}
|
package/src/migrations.ts
CHANGED
|
@@ -106,7 +106,31 @@ export function importOptimizerSidecar(
|
|
|
106
106
|
agentDir: string,
|
|
107
107
|
ctx: ParseContext,
|
|
108
108
|
): { changed: boolean; archive?: () => void } {
|
|
109
|
-
const
|
|
109
|
+
const imports = ["optimizer", "voice", "fetch", "search", "toolbox"];
|
|
110
|
+
let changed = false;
|
|
111
|
+
const archives: (() => void)[] = [];
|
|
112
|
+
for (const section of imports) {
|
|
113
|
+
const result = importSidecar(doc, agentDir, ctx, section);
|
|
114
|
+
changed ||= result.changed;
|
|
115
|
+
if (result.archive) archives.push(result.archive);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
changed,
|
|
119
|
+
archive: archives.length
|
|
120
|
+
? () => {
|
|
121
|
+
for (const archive of archives) archive();
|
|
122
|
+
}
|
|
123
|
+
: undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function importSidecar(
|
|
128
|
+
doc: RawDocument,
|
|
129
|
+
agentDir: string,
|
|
130
|
+
ctx: ParseContext,
|
|
131
|
+
section: string,
|
|
132
|
+
): { changed: boolean; archive?: () => void } {
|
|
133
|
+
const sidecarPath = join(agentDir, `${section}.json`);
|
|
110
134
|
if (!existsSync(sidecarPath)) return { changed: false };
|
|
111
135
|
|
|
112
136
|
let raw: Record<string, unknown>;
|
|
@@ -118,32 +142,39 @@ export function importOptimizerSidecar(
|
|
|
118
142
|
ctx.diagnostic({
|
|
119
143
|
code: "MIGRATION_FAILED",
|
|
120
144
|
severity: "warning",
|
|
121
|
-
path:
|
|
122
|
-
message:
|
|
145
|
+
path: `${section}.json`,
|
|
146
|
+
message: `malformed ${section} config left unchanged`,
|
|
123
147
|
cause: err,
|
|
124
148
|
});
|
|
125
149
|
return { changed: false };
|
|
126
150
|
}
|
|
127
151
|
|
|
128
|
-
const existing = isObj(doc
|
|
152
|
+
const existing = isObj(doc[section]) ? { ...doc[section] } : {};
|
|
129
153
|
let changed = false;
|
|
130
|
-
for (const key of OPTIMIZER_KEYS) {
|
|
154
|
+
for (const key of section === "optimizer" ? OPTIMIZER_KEYS : Object.keys(raw)) {
|
|
131
155
|
const value = raw[key];
|
|
132
|
-
if (typeof value === "string" && !(key in existing)) {
|
|
156
|
+
if ((section !== "optimizer" || typeof value === "string") && !(key in existing)) {
|
|
133
157
|
existing[key] = value;
|
|
134
158
|
changed = true;
|
|
135
159
|
}
|
|
136
160
|
}
|
|
137
|
-
if (changed) doc
|
|
161
|
+
if (changed) doc[section] = existing;
|
|
138
162
|
|
|
139
|
-
const hasLegacyToon = typeof raw.toon === "string";
|
|
163
|
+
const hasLegacyToon = section === "optimizer" && typeof raw.toon === "string";
|
|
140
164
|
const archive = () => {
|
|
141
165
|
let target = `${sidecarPath}.migrated-v1`;
|
|
142
166
|
if (existsSync(target)) target = `${target}.${Date.now()}`;
|
|
143
167
|
try {
|
|
144
168
|
renameSync(sidecarPath, target);
|
|
145
|
-
} catch {
|
|
146
|
-
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT")
|
|
171
|
+
ctx.diagnostic({
|
|
172
|
+
code: "MIGRATION_FAILED",
|
|
173
|
+
severity: "warning",
|
|
174
|
+
path: sidecarPath,
|
|
175
|
+
message: "config backup failed; original file remains",
|
|
176
|
+
cause: err,
|
|
177
|
+
});
|
|
147
178
|
}
|
|
148
179
|
};
|
|
149
180
|
|
package/src/pix-command.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { createBinariesTab } from "./binaries-tab.ts";
|
|
20
20
|
import { icon } from "./icon-catalog.ts";
|
|
21
21
|
import type { PixRuntime } from "./runtime.ts";
|
|
22
|
-
import type
|
|
22
|
+
import { type DeepPartial, deepMerge, type SectionHandle } from "./schema.ts";
|
|
23
23
|
import { collapseSection } from "./sections/collapse.ts";
|
|
24
24
|
import { compactionSection } from "./sections/compaction.ts";
|
|
25
25
|
import { gateSection } from "./sections/gate.ts";
|
|
@@ -184,6 +184,17 @@ const SETTINGS: SettingRow<unknown>[] = [
|
|
|
184
184
|
read: (v) => v.guardrails,
|
|
185
185
|
patch: (guardrails) => ({ guardrails: guardrails as "on" | "off" }),
|
|
186
186
|
}),
|
|
187
|
+
...Object.keys(prettySection.defaults.footer).map((key) => {
|
|
188
|
+
const part = key as keyof typeof prettySection.defaults.footer;
|
|
189
|
+
return row({
|
|
190
|
+
section: "Footer",
|
|
191
|
+
label: part,
|
|
192
|
+
handle: prettySection,
|
|
193
|
+
values: ["show", "hide"],
|
|
194
|
+
read: (v) => (v.footer[part] ? "show" : "hide"),
|
|
195
|
+
patch: (value) => ({ footer: { [part]: value === "show" } }),
|
|
196
|
+
});
|
|
197
|
+
}),
|
|
187
198
|
];
|
|
188
199
|
|
|
189
200
|
/** Env whose agent dir is the folder holding pix.json (binary.json lives beside it). */
|
|
@@ -191,7 +202,7 @@ function binariesEnv(runtime: PixRuntime): NodeJS.ProcessEnv {
|
|
|
191
202
|
return { ...process.env, PI_CODING_AGENT_DIR: dirname(runtime.path) };
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
const TABS = ["Settings", "Binaries"] as const;
|
|
205
|
+
const TABS = ["Settings", "Binaries", "Footer"] as const;
|
|
195
206
|
|
|
196
207
|
function buildSummary(runtime: PixRuntime): string {
|
|
197
208
|
const lines = [`Pix Settings (${runtime.path})`, ""];
|
|
@@ -270,13 +281,16 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
270
281
|
const switchTab = (direction: -1 | 1) => {
|
|
271
282
|
const next = (TABS.indexOf(tab) + direction + TABS.length) % TABS.length;
|
|
272
283
|
tab = TABS[next] ?? "Settings";
|
|
284
|
+
selected = 0;
|
|
273
285
|
bodyOffset = 0;
|
|
274
286
|
inspectingPage = false;
|
|
275
287
|
if (tab === "Binaries") binariesTab().refresh();
|
|
276
288
|
};
|
|
277
289
|
|
|
290
|
+
const rows = () =>
|
|
291
|
+
SETTINGS.filter((r) => (r.section === "Footer") === (tab === "Footer"));
|
|
278
292
|
const cycle = (direction: -1 | 1) => {
|
|
279
|
-
const row =
|
|
293
|
+
const row = rows()[selected];
|
|
280
294
|
if (!row) return;
|
|
281
295
|
// Functional updater: the runtime queue serializes writes, and the
|
|
282
296
|
// callback sees the latest committed value — so rapid presses each
|
|
@@ -296,7 +310,7 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
296
310
|
const next = (cur + direction + row.values.length) % row.values.length;
|
|
297
311
|
const val = row.values[next];
|
|
298
312
|
if (val === undefined) return state;
|
|
299
|
-
return
|
|
313
|
+
return deepMerge(state, row.patch(val) as DeepPartial<SettingsState>);
|
|
300
314
|
};
|
|
301
315
|
// Re-render AFTER the commit lands; the synchronous render in
|
|
302
316
|
// handleInput fires before the queued write and shows a stale value.
|
|
@@ -306,7 +320,7 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
306
320
|
.catch(() => ui.notify(`pix: failed to update ${row.label}`, "error"));
|
|
307
321
|
};
|
|
308
322
|
const move = (direction: -1 | 1) => {
|
|
309
|
-
selected = (selected + direction +
|
|
323
|
+
selected = (selected + direction + rows().length) % rows().length;
|
|
310
324
|
inspectingPage = false;
|
|
311
325
|
};
|
|
312
326
|
|
|
@@ -346,12 +360,13 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
346
360
|
maxBodyOffset = result.maxBodyOffset;
|
|
347
361
|
return result.lines;
|
|
348
362
|
}
|
|
349
|
-
const
|
|
363
|
+
const settings = rows();
|
|
364
|
+
const labelW = Math.max(...settings.map((r) => r.label.length));
|
|
350
365
|
const body: string[] = [];
|
|
351
366
|
const settingBodyLines: number[] = [];
|
|
352
367
|
let lastSection = "";
|
|
353
|
-
for (let i = 0; i <
|
|
354
|
-
const row =
|
|
368
|
+
for (let i = 0; i < settings.length; i++) {
|
|
369
|
+
const row = settings[i];
|
|
355
370
|
if (!row) continue;
|
|
356
371
|
if (row.section !== lastSection) {
|
|
357
372
|
if (lastSection) body.push("");
|
package/src/sections/index.ts
CHANGED
|
@@ -30,6 +30,18 @@ import { ioSection } from "./io.ts";
|
|
|
30
30
|
import { optimizerSection } from "./optimizer.ts";
|
|
31
31
|
import { prettySection } from "./pretty.ts";
|
|
32
32
|
|
|
33
|
+
export {
|
|
34
|
+
fetchSection,
|
|
35
|
+
searchSection,
|
|
36
|
+
type ToolboxConfig,
|
|
37
|
+
toolboxSection,
|
|
38
|
+
type VoiceConfig,
|
|
39
|
+
voiceSection,
|
|
40
|
+
type WebConfig,
|
|
41
|
+
} from "./services.ts";
|
|
42
|
+
|
|
43
|
+
import { fetchSection, searchSection, toolboxSection, voiceSection } from "./services.ts";
|
|
44
|
+
|
|
33
45
|
/** All built-in sections, in stable registration order. */
|
|
34
46
|
export const builtinSections = [
|
|
35
47
|
collapseSection,
|
|
@@ -38,4 +50,8 @@ export const builtinSections = [
|
|
|
38
50
|
optimizerSection,
|
|
39
51
|
gateSection,
|
|
40
52
|
compactionSection,
|
|
53
|
+
fetchSection,
|
|
54
|
+
searchSection,
|
|
55
|
+
voiceSection,
|
|
56
|
+
toolboxSection,
|
|
41
57
|
] as const;
|
package/src/sections/pretty.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineSection, enumOr, isObj, posNumOr } from "../schema.ts";
|
|
1
|
+
import { boolOr, defineSection, enumOr, isObj, posNumOr } from "../schema.ts";
|
|
2
2
|
|
|
3
3
|
export type IconMode = "nerd" | "unicode" | "ascii";
|
|
4
4
|
export type LsStyle = "grid" | "tree";
|
|
@@ -19,6 +19,21 @@ export interface PrettyConfig {
|
|
|
19
19
|
maxHighlightChars: number;
|
|
20
20
|
cacheLimit: number;
|
|
21
21
|
diff: DiffConfig;
|
|
22
|
+
footer: Record<
|
|
23
|
+
| "mode"
|
|
24
|
+
| "cwd"
|
|
25
|
+
| "git"
|
|
26
|
+
| "context"
|
|
27
|
+
| "model"
|
|
28
|
+
| "thinking"
|
|
29
|
+
| "price"
|
|
30
|
+
| "score"
|
|
31
|
+
| "statuses"
|
|
32
|
+
| "tokens"
|
|
33
|
+
| "cost"
|
|
34
|
+
| "tps",
|
|
35
|
+
boolean
|
|
36
|
+
>;
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
const ICON_MODES: readonly IconMode[] = ["nerd", "unicode", "ascii"];
|
|
@@ -43,15 +58,35 @@ const DEFAULTS: Readonly<PrettyConfig> = {
|
|
|
43
58
|
maxHighlightChars: 80_000,
|
|
44
59
|
cacheLimit: 128,
|
|
45
60
|
diff: { splitMinWidth: 150, splitMinCodeWidth: 60 },
|
|
61
|
+
footer: {
|
|
62
|
+
mode: true,
|
|
63
|
+
cwd: true,
|
|
64
|
+
git: true,
|
|
65
|
+
context: true,
|
|
66
|
+
model: true,
|
|
67
|
+
thinking: true,
|
|
68
|
+
price: true,
|
|
69
|
+
score: true,
|
|
70
|
+
statuses: true,
|
|
71
|
+
tokens: true,
|
|
72
|
+
cost: true,
|
|
73
|
+
tps: true,
|
|
74
|
+
},
|
|
46
75
|
};
|
|
47
76
|
|
|
48
77
|
export const prettySection = defineSection<"pretty", PrettyConfig>({
|
|
49
78
|
key: "pretty",
|
|
50
79
|
defaults: DEFAULTS,
|
|
51
80
|
parse(raw) {
|
|
52
|
-
if (!isObj(raw)) return
|
|
81
|
+
if (!isObj(raw)) return structuredClone(DEFAULTS);
|
|
53
82
|
const rawDiff = isObj(raw.diff) ? raw.diff : {};
|
|
83
|
+
const rawFooter = isObj(raw.footer) ? raw.footer : {};
|
|
84
|
+
const footer = { ...DEFAULTS.footer };
|
|
85
|
+
for (const key of Object.keys(footer) as (keyof typeof footer)[]) {
|
|
86
|
+
footer[key] = boolOr(rawFooter[key], DEFAULTS.footer[key]);
|
|
87
|
+
}
|
|
54
88
|
return {
|
|
89
|
+
footer,
|
|
55
90
|
icons: enumOr(raw.icons, ICON_MODES, DEFAULTS.icons),
|
|
56
91
|
lsStyle: enumOr(raw.lsStyle, LS_STYLES, DEFAULTS.lsStyle),
|
|
57
92
|
maxRenderWidth: renderSizeOr(raw.maxRenderWidth, DEFAULTS.maxRenderWidth),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { boolOr, defineSection, isObj, strArr } from "../schema.ts";
|
|
2
|
+
|
|
3
|
+
export interface WebConfig {
|
|
4
|
+
provider: string;
|
|
5
|
+
nineRouterModel: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const webDefaults: WebConfig = { provider: "auto", nineRouterModel: "exa" };
|
|
9
|
+
function parseWeb(raw: unknown): WebConfig {
|
|
10
|
+
const item = isObj(raw) ? raw : {};
|
|
11
|
+
return {
|
|
12
|
+
provider: typeof item.provider === "string" ? item.provider : webDefaults.provider,
|
|
13
|
+
nineRouterModel:
|
|
14
|
+
typeof item.nineRouterModel === "string" ? item.nineRouterModel : webDefaults.nineRouterModel,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export const fetchSection = defineSection({ key: "fetch", defaults: webDefaults, parse: parseWeb });
|
|
18
|
+
export const searchSection = defineSection({
|
|
19
|
+
key: "search",
|
|
20
|
+
defaults: webDefaults,
|
|
21
|
+
parse: parseWeb,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const voiceDefaults = {
|
|
25
|
+
sttProvider: "auto",
|
|
26
|
+
ttsProvider: "auto",
|
|
27
|
+
sttNineRouterModel: "dg/nova-3",
|
|
28
|
+
ttsNineRouterModel: "edge-tts/en-US-AriaNeural",
|
|
29
|
+
ttsPlay: true,
|
|
30
|
+
sttDevice: "default",
|
|
31
|
+
sttLanguage: "auto",
|
|
32
|
+
sttShortcut: "ctrl+alt+z",
|
|
33
|
+
sttCleanup: "off",
|
|
34
|
+
};
|
|
35
|
+
export type VoiceConfig = typeof voiceDefaults;
|
|
36
|
+
export const voiceSection = defineSection({
|
|
37
|
+
key: "voice",
|
|
38
|
+
defaults: voiceDefaults,
|
|
39
|
+
parse(raw): VoiceConfig {
|
|
40
|
+
const item = isObj(raw) ? raw : {};
|
|
41
|
+
const result = { ...voiceDefaults };
|
|
42
|
+
for (const key of Object.keys(result) as (keyof VoiceConfig)[]) {
|
|
43
|
+
if (key === "ttsPlay") result.ttsPlay = boolOr(item.ttsPlay, true);
|
|
44
|
+
else if (typeof item[key] === "string" && item[key].trim()) result[key] = item[key].trim();
|
|
45
|
+
}
|
|
46
|
+
result.sttLanguage = result.sttLanguage.toLowerCase();
|
|
47
|
+
return result;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
export interface ToolboxConfig {
|
|
51
|
+
disabledTools?: string[];
|
|
52
|
+
loadedTools?: string[];
|
|
53
|
+
enabledTools?: string[];
|
|
54
|
+
}
|
|
55
|
+
export const toolboxSection = defineSection<"toolbox", ToolboxConfig>({
|
|
56
|
+
key: "toolbox",
|
|
57
|
+
defaults: {},
|
|
58
|
+
parse(raw) {
|
|
59
|
+
const result: ToolboxConfig = {};
|
|
60
|
+
if (isObj(raw)) {
|
|
61
|
+
for (const key of ["disabledTools", "loadedTools", "enabledTools"] as const) {
|
|
62
|
+
if (
|
|
63
|
+
Array.isArray(raw[key]) &&
|
|
64
|
+
raw[key].every((value: unknown) => typeof value === "string")
|
|
65
|
+
)
|
|
66
|
+
result[key] = strArr(raw[key]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
},
|
|
71
|
+
});
|