claude-code-session-manager 0.2.7 → 0.3.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/dist/assets/{cssMode-BSA8IXdh.js → cssMode-CmcpKapf.js} +1 -1
- package/dist/assets/{editor.main-DNyTQ8C2.js → editor.main-QdhRqSRj.js} +3 -3
- package/dist/assets/{freemarker2-Dic49d2Z.js → freemarker2-MB42wuNH.js} +1 -1
- package/dist/assets/{handlebars-B0ttxNtX.js → handlebars-BNqpiTGG.js} +1 -1
- package/dist/assets/{html-BglndHCa.js → html-Uip3YcGr.js} +1 -1
- package/dist/assets/{htmlMode-DQDBWc2B.js → htmlMode-DYyTepoa.js} +1 -1
- package/dist/assets/index-BzbwWnyF.css +32 -0
- package/dist/assets/index-DIBKIDmx.js +2971 -0
- package/dist/assets/{javascript-mHLCLV2P.js → javascript-DrEGK9I4.js} +1 -1
- package/dist/assets/{jsonMode-BjtyoJTt.js → jsonMode-CSTtGACB.js} +1 -1
- package/dist/assets/{liquid-BOVepZ_L.js → liquid-RBrA4NW9.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-CsKny4JJ.js → lspLanguageFeatures-BduwC_yH.js} +1 -1
- package/dist/assets/{mdx-BTgcnA78.js → mdx-DkPoTolY.js} +1 -1
- package/dist/assets/{python-ZXDfZDk7.js → python-CVO-ynpa.js} +1 -1
- package/dist/assets/{razor-CnSo5CZS.js → razor-M76CcRjZ.js} +1 -1
- package/dist/assets/{tsMode-BggS4HL2.js → tsMode-D8OXqf9n.js} +1 -1
- package/dist/assets/{typescript-B4jtvSCe.js → typescript-CBXprweT.js} +1 -1
- package/dist/assets/{whisperWorker-BhltUYOx.js → whisperWorker-HvcbMQn6.js} +17 -17
- package/dist/assets/{xml-CoM1qQrw.js → xml-EEhcqUb-.js} +1 -1
- package/dist/assets/{yaml-CSgZykpA.js → yaml-B8BKq6X4.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/index.cjs +50 -1
- package/src/main/lib/voice-hotkey-log.cjs +60 -0
- package/src/main/logs.cjs +82 -0
- package/src/main/voiceHotkey.cjs +346 -0
- package/src/main/voiceSettings.cjs +337 -0
- package/src/main/voiceWizard.cjs +54 -0
- package/src/preload/api.d.ts +86 -0
- package/src/preload/index.cjs +33 -0
- package/dist/assets/index-BYHTuGIu.css +0 -32
- package/dist/assets/index-CBPfy0j1.js +0 -2971
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoiceSettings — persists voice hotkey configuration to disk.
|
|
3
|
+
*
|
|
4
|
+
* Storage: ~/.config/session-manager/voice.json
|
|
5
|
+
* Shape: {
|
|
6
|
+
* accelerator: string, mode: 'hold'|'toggle', global: boolean, schemaVersion: 1,
|
|
7
|
+
* // F5 device picker (optional, additive):
|
|
8
|
+
* device?: { selectedDeviceId: string|null, selectedLabel: string|null, schemaVersion: 1 },
|
|
9
|
+
* // F7 first-run wizard (optional, additive):
|
|
10
|
+
* wizard?: { completedSchema: number|null, completedAt: string|null }
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the atomic write pattern from sessionsStore.cjs (tmp file + rename).
|
|
14
|
+
*
|
|
15
|
+
* F1 v1: schema migrations are out of scope; we just stamp `schemaVersion: 1`
|
|
16
|
+
* on every write so a future loader can branch. See PRD F1 v2 §Open Questions.
|
|
17
|
+
*
|
|
18
|
+
* F5/F7 invariant: writes preserve unknown top-level keys so the hotkey-config
|
|
19
|
+
* writer doesn't clobber the device/wizard subtrees (and vice versa).
|
|
20
|
+
* Last-writer-wins within a single subtree is acceptable per F5 PRD v2 §Persistence.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const fsp = require('node:fs/promises');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const os = require('node:os');
|
|
27
|
+
|
|
28
|
+
const SCHEMA_VERSION = 1;
|
|
29
|
+
const DEVICE_SCHEMA_VERSION = 1;
|
|
30
|
+
// F7: bumping this constant invalidates all completed wizards globally.
|
|
31
|
+
// Trade-off accepted per F7 PRD v2 §Schema-version bump policy.
|
|
32
|
+
const WIZARD_SCHEMA = 1;
|
|
33
|
+
// F8: turn-detector subtree. Bumping invalidates persisted state and falls
|
|
34
|
+
// back to defaults (mode='off', enabled=false) on next load.
|
|
35
|
+
const TURN_DETECTOR_SCHEMA = 1;
|
|
36
|
+
|
|
37
|
+
function defaultsForPlatform() {
|
|
38
|
+
// Cmd+Option+V is unbound by default on macOS (PRD F1 v2). Linux/Windows
|
|
39
|
+
// share Ctrl+Shift+Space — also unbound on default GNOME 45 / KDE 6 / Win.
|
|
40
|
+
if (process.platform === 'darwin') {
|
|
41
|
+
return { accelerator: 'Cmd+Option+V', mode: 'hold', global: false, schemaVersion: SCHEMA_VERSION };
|
|
42
|
+
}
|
|
43
|
+
return { accelerator: 'Ctrl+Shift+Space', mode: 'hold', global: false, schemaVersion: SCHEMA_VERSION };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function storePath() {
|
|
47
|
+
return path.join(os.homedir(), '.config', 'session-manager', 'voice.json');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// PRD F1 v2 §Chord schema. Anchored, requires ≥1 modifier so a bare
|
|
51
|
+
// alphanumeric can't bind a hotkey that fires while typing in a text box.
|
|
52
|
+
// Modifier order matches Electron's accelerator grammar; key part is letter,
|
|
53
|
+
// digit, F1-F24, or one of a small whitelist.
|
|
54
|
+
const ACCELERATOR_RE = /^(CommandOrControl|CmdOrCtrl|Cmd|Command|Ctrl|Control|Alt|Option|Shift|Super|Meta)(\+(CommandOrControl|CmdOrCtrl|Cmd|Command|Ctrl|Control|Alt|Option|Shift|Super|Meta))*\+([A-Z]|[0-9]|F([1-9]|1[0-9]|2[0-4])|Space|Tab|Enter|Backspace|Delete|Escape|Esc)$/;
|
|
55
|
+
|
|
56
|
+
function isValidConfig(cfg) {
|
|
57
|
+
if (!cfg || typeof cfg !== 'object') return false;
|
|
58
|
+
if (typeof cfg.accelerator !== 'string' || cfg.accelerator.length === 0) return false;
|
|
59
|
+
if (!ACCELERATOR_RE.test(cfg.accelerator)) return false;
|
|
60
|
+
if (cfg.mode !== 'hold' && cfg.mode !== 'toggle') return false;
|
|
61
|
+
if (typeof cfg.global !== 'boolean') return false;
|
|
62
|
+
// PRD §"Hold-mode-global removed": globalShortcut has no keyup so hold
|
|
63
|
+
// can't be implemented faithfully. Reject the combo at write time.
|
|
64
|
+
if (cfg.global === true && cfg.mode === 'hold') return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalize(cfg) {
|
|
69
|
+
return {
|
|
70
|
+
accelerator: String(cfg.accelerator),
|
|
71
|
+
mode: cfg.mode === 'toggle' ? 'toggle' : 'hold',
|
|
72
|
+
global: !!cfg.global,
|
|
73
|
+
schemaVersion: SCHEMA_VERSION,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read the raw on-disk JSON object (or null if absent / unparseable). Used
|
|
79
|
+
* internally so writers can preserve unknown subtrees on partial updates.
|
|
80
|
+
*/
|
|
81
|
+
async function readRaw() {
|
|
82
|
+
const p = storePath();
|
|
83
|
+
try {
|
|
84
|
+
const raw = await fsp.readFile(p, 'utf8');
|
|
85
|
+
const data = JSON.parse(raw);
|
|
86
|
+
return (data && typeof data === 'object') ? data : null;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (e.code !== 'ENOENT') {
|
|
89
|
+
console.warn('[voiceSettings] readRaw failed:', e.message);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Atomic full-file write of a merged object. Preserves any keys not touched
|
|
97
|
+
* by the caller. Complexity: O(k) over top-level keys; k is tiny.
|
|
98
|
+
*
|
|
99
|
+
* Serialized through `writeQueue` so concurrent F1/F5/F7 saves cannot
|
|
100
|
+
* read-then-merge-then-write atop each other and silently drop a patch.
|
|
101
|
+
* The queue is per-process; cross-process coordination is not in scope.
|
|
102
|
+
*/
|
|
103
|
+
let writeQueue = Promise.resolve();
|
|
104
|
+
async function writeMerged(patch) {
|
|
105
|
+
const run = async () => {
|
|
106
|
+
const p = storePath();
|
|
107
|
+
await fsp.mkdir(path.dirname(p), { recursive: true }).catch(() => {});
|
|
108
|
+
const existing = (await readRaw()) || {};
|
|
109
|
+
const next = { ...existing, ...patch };
|
|
110
|
+
const body = JSON.stringify(next, null, 2) + '\n';
|
|
111
|
+
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
112
|
+
await fsp.writeFile(tmp, body, 'utf8', { mode: 0o600 });
|
|
113
|
+
// chmod tmp explicitly because some platforms ignore the mode arg on
|
|
114
|
+
// writeFile when the file pre-exists. Then rename for atomicity.
|
|
115
|
+
try { await fsp.chmod(tmp, 0o600); } catch { /* */ }
|
|
116
|
+
await fsp.rename(tmp, p);
|
|
117
|
+
return { ok: true };
|
|
118
|
+
};
|
|
119
|
+
// Tail-promise pattern: each call awaits the previous, so writes are
|
|
120
|
+
// ordered. Errors do not poison the queue.
|
|
121
|
+
const next = writeQueue.then(run, run);
|
|
122
|
+
writeQueue = next.catch(() => {});
|
|
123
|
+
return next;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Read voice config from disk; create with defaults if absent. */
|
|
127
|
+
async function load() {
|
|
128
|
+
const data = await readRaw();
|
|
129
|
+
if (data === null) {
|
|
130
|
+
const def = defaultsForPlatform();
|
|
131
|
+
await save(def);
|
|
132
|
+
return def;
|
|
133
|
+
}
|
|
134
|
+
if (!isValidConfig(data)) {
|
|
135
|
+
// Treat malformed file as "create defaults" — we never lose user data
|
|
136
|
+
// because we don't overwrite their accelerator silently; we just don't
|
|
137
|
+
// honor a broken config. The user can hand-edit voice.json in v1.
|
|
138
|
+
console.warn('[voiceSettings] invalid config on disk, using defaults');
|
|
139
|
+
const def = defaultsForPlatform();
|
|
140
|
+
await save(def);
|
|
141
|
+
return def;
|
|
142
|
+
}
|
|
143
|
+
return normalize(data);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Atomic write of the hotkey config keys, preserving any other subtrees
|
|
148
|
+
* (notably F5 `device`). Tmp file + rename per sessionsStore.cjs:46-48.
|
|
149
|
+
*/
|
|
150
|
+
async function save(cfg) {
|
|
151
|
+
const n = normalize(cfg);
|
|
152
|
+
return writeMerged({
|
|
153
|
+
accelerator: n.accelerator,
|
|
154
|
+
mode: n.mode,
|
|
155
|
+
global: n.global,
|
|
156
|
+
schemaVersion: n.schemaVersion,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ──────────────────────────────────────────── F5 device-pref helpers
|
|
161
|
+
// Device prefs share `voice.json` to keep one canonical settings file. They
|
|
162
|
+
// live under the `device` subtree so the F1 normalizer doesn't strip them.
|
|
163
|
+
|
|
164
|
+
const DEFAULT_DEVICE = Object.freeze({
|
|
165
|
+
selectedDeviceId: null,
|
|
166
|
+
selectedLabel: null,
|
|
167
|
+
schemaVersion: DEVICE_SCHEMA_VERSION,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
function isValidDevicePref(d) {
|
|
171
|
+
if (!d || typeof d !== 'object') return false;
|
|
172
|
+
const id = d.selectedDeviceId;
|
|
173
|
+
const lbl = d.selectedLabel;
|
|
174
|
+
if (id !== null && typeof id !== 'string') return false;
|
|
175
|
+
if (lbl !== null && typeof lbl !== 'string') return false;
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeDevice(d) {
|
|
180
|
+
return {
|
|
181
|
+
selectedDeviceId: typeof d.selectedDeviceId === 'string' && d.selectedDeviceId ? d.selectedDeviceId : null,
|
|
182
|
+
selectedLabel: typeof d.selectedLabel === 'string' && d.selectedLabel ? d.selectedLabel : null,
|
|
183
|
+
schemaVersion: DEVICE_SCHEMA_VERSION,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Read F5 device pref. Defaults are returned (and persisted) on absence. */
|
|
188
|
+
async function loadDevice() {
|
|
189
|
+
const raw = await readRaw();
|
|
190
|
+
const sub = raw && typeof raw === 'object' ? raw.device : null;
|
|
191
|
+
if (!isValidDevicePref(sub)) {
|
|
192
|
+
// Don't auto-write on load — empty/missing device is the legitimate
|
|
193
|
+
// fresh-install state. The first setDevicePref will materialize it.
|
|
194
|
+
return { ...DEFAULT_DEVICE };
|
|
195
|
+
}
|
|
196
|
+
return normalizeDevice(sub);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Atomic write of just the device subtree, preserving other keys. */
|
|
200
|
+
async function saveDevice(d) {
|
|
201
|
+
if (!isValidDevicePref(d)) {
|
|
202
|
+
throw new Error('Invalid device pref shape');
|
|
203
|
+
}
|
|
204
|
+
return writeMerged({ device: normalizeDevice(d) });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ──────────────────────────────────────────── F7 wizard-state helpers
|
|
208
|
+
// Wizard state lives under the `wizard` subtree of voice.json. Mirrors the F5
|
|
209
|
+
// device pattern: additive, preserves other subtrees on write, returns a
|
|
210
|
+
// safe default when absent so the wizard arms on first launch.
|
|
211
|
+
|
|
212
|
+
const DEFAULT_WIZARD = Object.freeze({
|
|
213
|
+
completedSchema: null,
|
|
214
|
+
completedAt: null,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
function isValidWizardState(w) {
|
|
218
|
+
if (!w || typeof w !== 'object') return false;
|
|
219
|
+
const cs = w.completedSchema;
|
|
220
|
+
const ca = w.completedAt;
|
|
221
|
+
if (cs !== null && typeof cs !== 'number') return false;
|
|
222
|
+
if (ca !== null && typeof ca !== 'string') return false;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeWizard(w) {
|
|
227
|
+
return {
|
|
228
|
+
completedSchema: typeof w.completedSchema === 'number' ? w.completedSchema : null,
|
|
229
|
+
completedAt: typeof w.completedAt === 'string' && w.completedAt ? w.completedAt : null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Read the F7 wizard subtree. Returns the default (un-completed) shape when
|
|
235
|
+
* the subtree is missing or malformed — the wizard arms in that case. We do
|
|
236
|
+
* NOT auto-write a default on load; the first markComplete materializes it.
|
|
237
|
+
*/
|
|
238
|
+
async function loadWizard() {
|
|
239
|
+
const raw = await readRaw();
|
|
240
|
+
const sub = raw && typeof raw === 'object' ? raw.wizard : null;
|
|
241
|
+
if (!isValidWizardState(sub)) {
|
|
242
|
+
return { ...DEFAULT_WIZARD };
|
|
243
|
+
}
|
|
244
|
+
return normalizeWizard(sub);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Atomic write of just the wizard subtree, preserving other keys. */
|
|
248
|
+
async function saveWizard(state) {
|
|
249
|
+
if (!isValidWizardState(state)) {
|
|
250
|
+
throw new Error('Invalid wizard state shape');
|
|
251
|
+
}
|
|
252
|
+
return writeMerged({ wizard: normalizeWizard(state) });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ──────────────────────────────────────────── F8 turn-detector helpers
|
|
256
|
+
// Lives under the `turnDetector` subtree of voice.json. MVP-only fields;
|
|
257
|
+
// the actual smart-turn-v3 ONNX model is NOT loaded in v1 — see F8 PRD v2
|
|
258
|
+
// and the follow-up TODO in turnDetectorWorker.ts. Mirrors F5 device pattern.
|
|
259
|
+
|
|
260
|
+
const DEFAULT_TURN_DETECTOR = Object.freeze({
|
|
261
|
+
enabled: false,
|
|
262
|
+
mode: 'off', // 'audio' | 'text' | 'off'
|
|
263
|
+
dictationMode: false,
|
|
264
|
+
schemaVersion: TURN_DETECTOR_SCHEMA,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const TURN_DETECTOR_MODES = new Set(['audio', 'text', 'off']);
|
|
268
|
+
|
|
269
|
+
function isValidTurnDetectorState(t) {
|
|
270
|
+
if (!t || typeof t !== 'object') return false;
|
|
271
|
+
if (typeof t.enabled !== 'boolean') return false;
|
|
272
|
+
if (typeof t.mode !== 'string' || !TURN_DETECTOR_MODES.has(t.mode)) return false;
|
|
273
|
+
if (typeof t.dictationMode !== 'boolean') return false;
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeTurnDetector(t) {
|
|
278
|
+
const mode = TURN_DETECTOR_MODES.has(t.mode) ? t.mode : 'off';
|
|
279
|
+
// Cross-field invariant: `enabled` and `mode === 'off'` are inconsistent.
|
|
280
|
+
// We normalize so callers can be sloppy: if mode is 'off' then enabled=false.
|
|
281
|
+
const enabled = mode === 'off' ? false : !!t.enabled;
|
|
282
|
+
return {
|
|
283
|
+
enabled,
|
|
284
|
+
mode,
|
|
285
|
+
dictationMode: !!t.dictationMode,
|
|
286
|
+
schemaVersion: TURN_DETECTOR_SCHEMA,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Read the F8 turn-detector subtree. Returns DEFAULT (`mode: 'off'`) when the
|
|
292
|
+
* subtree is missing or malformed — F8 MVP is opt-in, so a missing config
|
|
293
|
+
* yields the safe pure-VAD path. We do NOT auto-write a default on load.
|
|
294
|
+
*/
|
|
295
|
+
async function loadTurnDetector() {
|
|
296
|
+
const raw = await readRaw();
|
|
297
|
+
const sub = raw && typeof raw === 'object' ? raw.turnDetector : null;
|
|
298
|
+
if (!isValidTurnDetectorState(sub)) {
|
|
299
|
+
return { ...DEFAULT_TURN_DETECTOR };
|
|
300
|
+
}
|
|
301
|
+
return normalizeTurnDetector(sub);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Atomic write of just the turnDetector subtree, preserving other keys. */
|
|
305
|
+
async function saveTurnDetector(state) {
|
|
306
|
+
if (!isValidTurnDetectorState(state)) {
|
|
307
|
+
throw new Error('Invalid turn-detector state shape');
|
|
308
|
+
}
|
|
309
|
+
return writeMerged({ turnDetector: normalizeTurnDetector(state) });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
load,
|
|
314
|
+
save,
|
|
315
|
+
storePath,
|
|
316
|
+
defaultsForPlatform,
|
|
317
|
+
isValidConfig,
|
|
318
|
+
SCHEMA_VERSION,
|
|
319
|
+
// F5
|
|
320
|
+
loadDevice,
|
|
321
|
+
saveDevice,
|
|
322
|
+
isValidDevicePref,
|
|
323
|
+
DEFAULT_DEVICE,
|
|
324
|
+
DEVICE_SCHEMA_VERSION,
|
|
325
|
+
// F7
|
|
326
|
+
loadWizard,
|
|
327
|
+
saveWizard,
|
|
328
|
+
isValidWizardState,
|
|
329
|
+
DEFAULT_WIZARD,
|
|
330
|
+
WIZARD_SCHEMA,
|
|
331
|
+
// F8
|
|
332
|
+
loadTurnDetector,
|
|
333
|
+
saveTurnDetector,
|
|
334
|
+
isValidTurnDetectorState,
|
|
335
|
+
DEFAULT_TURN_DETECTOR,
|
|
336
|
+
TURN_DETECTOR_SCHEMA,
|
|
337
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* voiceWizard — F7 first-run mic-check wizard, main-process side.
|
|
3
|
+
*
|
|
4
|
+
* Owns three IPC channels:
|
|
5
|
+
* - voice:wizard-state → returns persisted state + current schema constant
|
|
6
|
+
* - voice:wizard-complete → stamps completedAt + completedSchema to voice.json
|
|
7
|
+
* - app:is-e2e → exposes process.env.SM_E2E === '1' to the renderer
|
|
8
|
+
*
|
|
9
|
+
* No window state held here; persistence is delegated to voiceSettings.cjs
|
|
10
|
+
* (additive `wizard` subtree on the same file as F1/F5).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { ipcMain } = require('electron');
|
|
14
|
+
const voiceSettings = require('./voiceSettings.cjs');
|
|
15
|
+
|
|
16
|
+
function registerWizardHandlers() {
|
|
17
|
+
ipcMain.handle('voice:wizard-state', async () => {
|
|
18
|
+
const w = await voiceSettings.loadWizard();
|
|
19
|
+
return {
|
|
20
|
+
completedSchema: w.completedSchema,
|
|
21
|
+
completedAt: w.completedAt,
|
|
22
|
+
currentSchema: voiceSettings.WIZARD_SCHEMA,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
ipcMain.handle('voice:wizard-complete', async () => {
|
|
27
|
+
const next = {
|
|
28
|
+
completedSchema: voiceSettings.WIZARD_SCHEMA,
|
|
29
|
+
completedAt: new Date().toISOString(),
|
|
30
|
+
};
|
|
31
|
+
await voiceSettings.saveWizard(next);
|
|
32
|
+
return { ok: true, ...next, currentSchema: voiceSettings.WIZARD_SCHEMA };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// E2E plumbing: tests set SM_E2E=1 to suppress the wizard auto-trigger.
|
|
36
|
+
// The renderer reads this once on mount.
|
|
37
|
+
ipcMain.handle('app:is-e2e', () => process.env.SM_E2E === '1');
|
|
38
|
+
|
|
39
|
+
// F8 — turn-detector settings (additive subtree on voice.json).
|
|
40
|
+
// MVP: persistence only; no model loaded in v1 (see PRD §Loading & inference).
|
|
41
|
+
ipcMain.handle('voice:get-turn-detector', async () => {
|
|
42
|
+
return await voiceSettings.loadTurnDetector();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
ipcMain.handle('voice:set-turn-detector', async (_e, state) => {
|
|
46
|
+
if (!voiceSettings.isValidTurnDetectorState(state)) {
|
|
47
|
+
throw new Error('Invalid turn-detector state payload');
|
|
48
|
+
}
|
|
49
|
+
await voiceSettings.saveTurnDetector(state);
|
|
50
|
+
return { ok: true, state: await voiceSettings.loadTurnDetector() };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { registerWizardHandlers };
|
package/src/preload/api.d.ts
CHANGED
|
@@ -119,6 +119,66 @@ export interface BillingFetchResult {
|
|
|
119
119
|
error?: string;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
export interface VoiceHotkeyConfig {
|
|
123
|
+
accelerator: string;
|
|
124
|
+
mode: 'hold' | 'toggle';
|
|
125
|
+
global: boolean;
|
|
126
|
+
schemaVersion: 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface VoiceHotkeyEvent {
|
|
130
|
+
phase: 'down' | 'up';
|
|
131
|
+
source: 'window' | 'global';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface VoiceSetHotkeyResult {
|
|
135
|
+
ok: boolean;
|
|
136
|
+
config: VoiceHotkeyConfig;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** F5 — persisted audio-input device preference. */
|
|
140
|
+
export interface VoiceDevicePref {
|
|
141
|
+
selectedDeviceId: string | null;
|
|
142
|
+
selectedLabel: string | null;
|
|
143
|
+
schemaVersion: 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** F7 — first-run mic-check wizard state. */
|
|
147
|
+
export interface VoiceWizardState {
|
|
148
|
+
/** Schema version stamped at the time the user completed the wizard, or null. */
|
|
149
|
+
completedSchema: number | null;
|
|
150
|
+
/** ISO-8601 completion timestamp, or null if never completed. */
|
|
151
|
+
completedAt: string | null;
|
|
152
|
+
/** Hardcoded current schema constant; bumping invalidates all completed wizards. */
|
|
153
|
+
currentSchema: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface VoiceMarkWizardCompleteResult {
|
|
157
|
+
ok: boolean;
|
|
158
|
+
completedSchema: number;
|
|
159
|
+
completedAt: string;
|
|
160
|
+
currentSchema: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* F8 — semantic turn-detection settings. MVP shape only; the actual
|
|
165
|
+
* smart-turn-v3 ONNX model is not loaded in v1. Persisted at
|
|
166
|
+
* ~/.config/session-manager/voice.json under `turnDetector`. See PRD F8 v2.
|
|
167
|
+
*/
|
|
168
|
+
export interface VoiceTurnDetectorState {
|
|
169
|
+
enabled: boolean;
|
|
170
|
+
/** 'audio' = smart-turn-v3 (planned); 'text' = rejected on license; 'off' = pure-VAD (default). */
|
|
171
|
+
mode: 'audio' | 'text' | 'off';
|
|
172
|
+
/** When true, bypass the model and use pure-VAD endpointing (out-of-distribution code). */
|
|
173
|
+
dictationMode: boolean;
|
|
174
|
+
schemaVersion: 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface VoiceSetTurnDetectorResult {
|
|
178
|
+
ok: boolean;
|
|
179
|
+
state: VoiceTurnDetectorState;
|
|
180
|
+
}
|
|
181
|
+
|
|
122
182
|
export interface SessionManagerAPI {
|
|
123
183
|
app: {
|
|
124
184
|
version: () => Promise<string>;
|
|
@@ -127,6 +187,8 @@ export interface SessionManagerAPI {
|
|
|
127
187
|
engageRulesPath: () => Promise<string | null>;
|
|
128
188
|
pickDirectory: () => Promise<string | null>;
|
|
129
189
|
rebootApp: () => void;
|
|
190
|
+
/** F7 — true under SM_E2E=1; renderer uses this to suppress wizard auto-trigger. */
|
|
191
|
+
isE2E: () => Promise<boolean>;
|
|
130
192
|
onNewSession: (handler: () => void) => () => void;
|
|
131
193
|
onRebootSession: (handler: () => void) => () => void;
|
|
132
194
|
};
|
|
@@ -152,6 +214,10 @@ export interface SessionManagerAPI {
|
|
|
152
214
|
billing: {
|
|
153
215
|
fetch: () => Promise<BillingFetchResult>;
|
|
154
216
|
};
|
|
217
|
+
logs: {
|
|
218
|
+
write: (scope: string, level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: unknown) => void;
|
|
219
|
+
dir: () => Promise<string>;
|
|
220
|
+
};
|
|
155
221
|
config: {
|
|
156
222
|
readJson: (path: string) => Promise<ReadJsonResult>;
|
|
157
223
|
readText: (path: string) => Promise<ReadTextResult>;
|
|
@@ -163,6 +229,26 @@ export interface SessionManagerAPI {
|
|
|
163
229
|
unwatch: (paths: string[]) => void;
|
|
164
230
|
onChanged: (handler: (info: ConfigChangedEvent) => void) => () => void;
|
|
165
231
|
};
|
|
232
|
+
voice: {
|
|
233
|
+
onHotkey: (handler: (event: VoiceHotkeyEvent) => void) => () => void;
|
|
234
|
+
onHotkeyConfigChanged: (handler: (cfg: VoiceHotkeyConfig) => void) => () => void;
|
|
235
|
+
getHotkeyConfig: () => Promise<VoiceHotkeyConfig>;
|
|
236
|
+
setHotkeyConfig: (cfg: VoiceHotkeyConfig) => Promise<VoiceSetHotkeyResult>;
|
|
237
|
+
getHotkeyConfigPath: () => Promise<string>;
|
|
238
|
+
setRecording: (recording: boolean) => void;
|
|
239
|
+
/** F5: read persisted audio-input device preference. */
|
|
240
|
+
getDevicePref: () => Promise<VoiceDevicePref>;
|
|
241
|
+
/** F5: persist audio-input device preference. */
|
|
242
|
+
setDevicePref: (pref: { selectedDeviceId: string | null; selectedLabel: string | null }) => Promise<{ ok: boolean }>;
|
|
243
|
+
/** F7: read first-run wizard state + current schema constant. */
|
|
244
|
+
getWizardState: () => Promise<VoiceWizardState>;
|
|
245
|
+
/** F7: stamp wizard as complete with the current schema. */
|
|
246
|
+
markWizardComplete: () => Promise<VoiceMarkWizardCompleteResult>;
|
|
247
|
+
/** F8 — read persisted turn-detector settings. */
|
|
248
|
+
getTurnDetector: () => Promise<VoiceTurnDetectorState>;
|
|
249
|
+
/** F8 — persist turn-detector settings. */
|
|
250
|
+
setTurnDetector: (state: VoiceTurnDetectorState) => Promise<VoiceSetTurnDetectorResult>;
|
|
251
|
+
};
|
|
166
252
|
}
|
|
167
253
|
|
|
168
254
|
declare global {
|
package/src/preload/index.cjs
CHANGED
|
@@ -8,6 +8,8 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
8
8
|
engageRulesPath: () => ipcRenderer.invoke('app:engage-rules-path'),
|
|
9
9
|
pickDirectory: () => ipcRenderer.invoke('app:pick-directory'),
|
|
10
10
|
rebootApp: () => ipcRenderer.send('app:reboot-app'),
|
|
11
|
+
// F7: lets the renderer suppress the wizard auto-trigger under SM_E2E=1.
|
|
12
|
+
isE2E: () => ipcRenderer.invoke('app:is-e2e'),
|
|
11
13
|
onNewSession: (handler) => {
|
|
12
14
|
const listener = () => handler();
|
|
13
15
|
ipcRenderer.on('app:new-session', listener);
|
|
@@ -57,6 +59,11 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
57
59
|
billing: {
|
|
58
60
|
fetch: () => ipcRenderer.invoke('billing:fetch'),
|
|
59
61
|
},
|
|
62
|
+
logs: {
|
|
63
|
+
write: (scope, level, message, meta) =>
|
|
64
|
+
ipcRenderer.send('log:write', { scope, level, message, meta }),
|
|
65
|
+
dir: () => ipcRenderer.invoke('log:dir'),
|
|
66
|
+
},
|
|
60
67
|
config: {
|
|
61
68
|
readJson: (path) => ipcRenderer.invoke('config:read-json', { path }),
|
|
62
69
|
readText: (path) => ipcRenderer.invoke('config:read-text', { path }),
|
|
@@ -72,4 +79,30 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
72
79
|
return () => ipcRenderer.removeListener('config:changed', listener);
|
|
73
80
|
},
|
|
74
81
|
},
|
|
82
|
+
voice: {
|
|
83
|
+
onHotkey: (handler) => {
|
|
84
|
+
const listener = (_e, payload) => handler(payload);
|
|
85
|
+
ipcRenderer.on('voice:hotkey', listener);
|
|
86
|
+
return () => ipcRenderer.removeListener('voice:hotkey', listener);
|
|
87
|
+
},
|
|
88
|
+
onHotkeyConfigChanged: (handler) => {
|
|
89
|
+
const listener = (_e, cfg) => handler(cfg);
|
|
90
|
+
ipcRenderer.on('voice:hotkey-changed', listener);
|
|
91
|
+
return () => ipcRenderer.removeListener('voice:hotkey-changed', listener);
|
|
92
|
+
},
|
|
93
|
+
getHotkeyConfig: () => ipcRenderer.invoke('voice:get-hotkey-config'),
|
|
94
|
+
setHotkeyConfig: (cfg) => ipcRenderer.invoke('voice:set-hotkey', cfg),
|
|
95
|
+
getHotkeyConfigPath: () => ipcRenderer.invoke('voice:get-hotkey-config-path'),
|
|
96
|
+
setRecording: (recording) => ipcRenderer.send('voice:set-recording', !!recording),
|
|
97
|
+
// F5 device picker prefs (~/.config/session-manager/voice.json `device` key).
|
|
98
|
+
getDevicePref: () => ipcRenderer.invoke('voice:get-device-pref'),
|
|
99
|
+
setDevicePref: (pref) => ipcRenderer.invoke('voice:set-device-pref', pref),
|
|
100
|
+
// F7 first-run wizard state (~/.config/session-manager/voice.json `wizard` key).
|
|
101
|
+
getWizardState: () => ipcRenderer.invoke('voice:wizard-state'),
|
|
102
|
+
markWizardComplete: () => ipcRenderer.invoke('voice:wizard-complete'),
|
|
103
|
+
// F8 turn-detector settings (~/.config/session-manager/voice.json `turnDetector` key).
|
|
104
|
+
// MVP: settings persistence + kill switch only; no model is actually loaded in v1.
|
|
105
|
+
getTurnDetector: () => ipcRenderer.invoke('voice:get-turn-detector'),
|
|
106
|
+
setTurnDetector: (state) => ipcRenderer.invoke('voice:set-turn-detector', state),
|
|
107
|
+
},
|
|
75
108
|
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
|
-
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
|
-
* https://github.com/chjj/term.js
|
|
5
|
-
* @license MIT
|
|
6
|
-
*
|
|
7
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
-
* in the Software without restriction, including without limitation the rights
|
|
10
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
-
* furnished to do so, subject to the following conditions:
|
|
13
|
-
*
|
|
14
|
-
* The above copyright notice and this permission notice shall be included in
|
|
15
|
-
* all copies or substantial portions of the Software.
|
|
16
|
-
*
|
|
17
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
-
* THE SOFTWARE.
|
|
24
|
-
*
|
|
25
|
-
* Originally forked from (with the author's permission):
|
|
26
|
-
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
27
|
-
* http://bellard.org/jslinux/
|
|
28
|
-
* Copyright (c) 2011 Fabrice Bellard
|
|
29
|
-
* The original design remains. The terminal itself
|
|
30
|
-
* has been extended to include xterm CSI codes, among
|
|
31
|
-
* other features.
|
|
32
|
-
*/.xterm{cursor:text;position:relative;-moz-user-select:none;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::-moz-selection{color:transparent}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;-moz-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:double underline;text-decoration:double underline}.xterm-underline-3{-webkit-text-decoration:wavy underline;text-decoration:wavy underline}.xterm-underline-4{-webkit-text-decoration:dotted underline;text-decoration:dotted underline}.xterm-underline-5{-webkit-text-decoration:dashed underline;text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,Menlo,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.-bottom-\[5px\]{bottom:-5px}.-bottom-\[7px\]{bottom:-7px}.-top-20{top:-5rem}.-top-28{top:-7rem}.-top-48{top:-12rem}.bottom-0{bottom:0}.bottom-24{bottom:6rem}.bottom-3{bottom:.75rem}.bottom-\[88px\]{bottom:88px}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.left-\[72\%\]{left:72%}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-0\.5{top:.125rem}.top-10{top:2.5rem}.top-4{top:1rem}.z-10{z-index:10}.mx-2{margin-left:.5rem;margin-right:.5rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.h-px{height:1px}.min-h-0{min-height:0px}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-52{width:13rem}.w-56{width:14rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-md{max-width:28rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0\.5{--tw-translate-x: .125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-3\.5{--tw-translate-x: .875rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-45{--tw-rotate: 45deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-wait{cursor:wait}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-1{row-gap:.25rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-line>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(36 42 51 / var(--tw-divide-opacity, 1))}.divide-line\/60>:not([hidden])~:not([hidden]){border-color:#242a3399}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-xl{border-radius:.75rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-accent{--tw-border-opacity: 1;border-color:rgb(217 119 87 / var(--tw-border-opacity, 1))}.border-accent\/30{border-color:#d977574d}.border-accent\/40{border-color:#d9775766}.border-accent\/50{border-color:#d9775780}.border-blue-900\/40{border-color:#1e3a8a66}.border-line{--tw-border-opacity: 1;border-color:rgb(36 42 51 / var(--tw-border-opacity, 1))}.border-line\/50{border-color:#242a3380}.border-line\/60{border-color:#242a3399}.border-purple-900\/40{border-color:#581c8766}.border-red-900\/40{border-color:#7f1d1d66}.border-transparent{border-color:transparent}.border-yellow-600\/50{border-color:#ca8a0480}.border-yellow-600\/60{border-color:#ca8a0499}.border-yellow-900\/40{border-color:#713f1266}.bg-accent{--tw-bg-opacity: 1;background-color:rgb(217 119 87 / var(--tw-bg-opacity, 1))}.bg-accent\/15{background-color:#d9775726}.bg-bg{--tw-bg-opacity: 1;background-color:rgb(11 13 16 / var(--tw-bg-opacity, 1))}.bg-bg-elev{--tw-bg-opacity: 1;background-color:rgb(18 21 26 / var(--tw-bg-opacity, 1))}.bg-bg-elev\/40{background-color:#12151a66}.bg-bg-elev\/80{background-color:#12151acc}.bg-bg-hi{--tw-bg-opacity: 1;background-color:rgb(26 31 39 / var(--tw-bg-opacity, 1))}.bg-bg-hi\/30{background-color:#1a1f274d}.bg-blue-950\/30{background-color:#1725544d}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-fg{--tw-bg-opacity: 1;background-color:rgb(230 232 236 / var(--tw-bg-opacity, 1))}.bg-fg-dim{--tw-bg-opacity: 1;background-color:rgb(138 147 160 / var(--tw-bg-opacity, 1))}.bg-fg-faint{--tw-bg-opacity: 1;background-color:rgb(84 92 104 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-line\/60{background-color:#242a3399}.bg-purple-950\/30{background-color:#3b07644d}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-950\/20{background-color:#450a0a33}.bg-red-950\/30{background-color:#450a0a4d}.bg-yellow-400{--tw-bg-opacity: 1;background-color:rgb(250 204 21 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-yellow-950\/20{background-color:#42200633}.bg-yellow-950\/30{background-color:#4220064d}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-bg{--tw-gradient-from: #0b0d10 var(--tw-gradient-from-position);--tw-gradient-to: rgb(11 13 16 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-bg{--tw-gradient-to: rgb(11 13 16 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #0b0d10 var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-bg-elev{--tw-gradient-to: #12151a var(--tw-gradient-to-position)}.p-1\.5{padding:.375rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-1{padding-bottom:.25rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,Menlo,monospace}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-accent{--tw-text-opacity: 1;color:rgb(217 119 87 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-bg{--tw-text-opacity: 1;color:rgb(11 13 16 / var(--tw-text-opacity, 1))}.text-blue-400\/80{color:#60a5facc}.text-fg{--tw-text-opacity: 1;color:rgb(230 232 236 / var(--tw-text-opacity, 1))}.text-fg-dim{--tw-text-opacity: 1;color:rgb(138 147 160 / var(--tw-text-opacity, 1))}.text-fg-faint{--tw-text-opacity: 1;color:rgb(84 92 104 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-400\/80{color:#c084fccc}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-400\/80{color:#f87171cc}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-yellow-500\/70{color:#eab308b3}.text-yellow-500\/80{color:#eab308cc}.text-yellow-500\/90{color:#eab308e6}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.placeholder-fg-faint::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(84 92 104 / var(--tw-placeholder-opacity, 1))}.placeholder-fg-faint::placeholder{--tw-placeholder-opacity: 1;color:rgb(84 92 104 / var(--tw-placeholder-opacity, 1))}.accent-accent{accent-color:#d97757}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-red-400{--tw-ring-opacity: 1;--tw-ring-color: rgb(248 113 113 / var(--tw-ring-opacity, 1))}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-2xl{--tw-drop-shadow: drop-shadow(0 25px 25px rgb(0 0 0 / .15));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}html,body,#root{height:100%;margin:0;overflow:hidden}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:#0b0d10}::-webkit-scrollbar-thumb{background:#242a33;border-radius:5px}::-webkit-scrollbar-thumb:hover{background:#2f3742}.hover\:border-fg-faint:hover{--tw-border-opacity: 1;border-color:rgb(84 92 104 / var(--tw-border-opacity, 1))}.hover\:bg-accent:hover{--tw-bg-opacity: 1;background-color:rgb(217 119 87 / var(--tw-bg-opacity, 1))}.hover\:bg-accent\/25:hover{background-color:#d9775740}.hover\:bg-bg-elev:hover{--tw-bg-opacity: 1;background-color:rgb(18 21 26 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-elev\/20:hover{background-color:#12151a33}.hover\:bg-bg-elev\/50:hover{background-color:#12151a80}.hover\:bg-bg-hi:hover{--tw-bg-opacity: 1;background-color:rgb(26 31 39 / var(--tw-bg-opacity, 1))}.hover\:bg-bg-hi\/50:hover{background-color:#1a1f2780}.hover\:bg-transparent:hover{background-color:transparent}.hover\:bg-yellow-600:hover{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity, 1))}.hover\:text-bg:hover{--tw-text-opacity: 1;color:rgb(11 13 16 / var(--tw-text-opacity, 1))}.hover\:text-fg:hover{--tw-text-opacity: 1;color:rgb(230 232 236 / var(--tw-text-opacity, 1))}.hover\:text-fg-dim:hover{--tw-text-opacity: 1;color:rgb(138 147 160 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.group:hover .group-hover\:opacity-100{opacity:1}
|