@dotdrelle/wiki-manager 0.6.30 → 0.6.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,806 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { execFileSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ import { useKeyboard } from '@opentui/solid';
5
+ import { createEffect, createMemo, createSignal, For, Show } from 'solid-js';
6
+ import { fallbackModels, normalizeProvider } from '../core/modelFetch.js';
7
+ import {
8
+ createNewWorkspace,
9
+ deleteWorkspaceAndFiles,
10
+ renameWorkspace,
11
+ startAgents,
12
+ unregisterWorkspace,
13
+ writeLanguageConfig,
14
+ writeLlmConfig,
15
+ writeVectorConfig,
16
+ } from '../core/wikiSetup.js';
17
+ import { listWorkspaces, workspacesDir } from '../core/workspaces.js';
18
+ import { loadWikircProfile } from '../core/wikirc.js';
19
+
20
+ type Gap = { kind: 'agents' | 'workspace' | 'llm' | 'vector'; context?: Record<string, any> };
21
+ type Mode = 'startup' | 'setup';
22
+ type Step =
23
+ | { kind: 'menu'; title: string; items: Array<{ label: string; value: string; muted?: boolean }> }
24
+ | { kind: 'confirm'; title: string; message: string; yesLabel: string; noLabel: string }
25
+ | { kind: 'select'; title: string; label: string; options: string[]; note?: string }
26
+ | { kind: 'text'; title: string; label: string; placeholder?: string; prefill?: string; secret?: boolean }
27
+ | { kind: 'done' };
28
+ type LogEntry = { icon: string; label: string; detail?: string };
29
+
30
+ const PROVIDERS = ['OpenAI', 'Anthropic', 'Ollama (local)', 'Other (OpenAI-compatible)'];
31
+ const PLACEHOLDER_MODEL_RE = /YOUR_|<your/i;
32
+ const MAIN_MENU = ['Agents', 'Workspaces', 'LLM configuration', 'Vector search', '---', 'Close'];
33
+
34
+ function defaultBaseUrl(provider: string) {
35
+ if (provider === 'ollama') return 'http://localhost:11434';
36
+ if (provider === 'anthropic') return 'https://api.anthropic.com';
37
+ if (provider === 'openai') return 'https://api.openai.com';
38
+ if (provider === 'openai-compatible') return 'http://localhost:8000';
39
+ return '';
40
+ }
41
+
42
+ function currentWorkspaceContext(session: any, fallback?: any) {
43
+ if (fallback?.workspacePath) {
44
+ return {
45
+ workspaceName: fallback.workspaceName ?? fallback.name ?? fallback.workspace ?? null,
46
+ workspacePath: fallback.workspacePath,
47
+ profileName: fallback.profileName ?? fallback.profile ?? 'default',
48
+ configError: fallback.configError ?? null,
49
+ };
50
+ }
51
+ if (session?.workspacePath) {
52
+ return {
53
+ workspaceName: session.workspace,
54
+ workspacePath: session.workspacePath,
55
+ profileName: session.wikirc?.profile ?? 'default',
56
+ };
57
+ }
58
+ const workspace = listWorkspaces()[0];
59
+ if (!workspace) return null;
60
+ return {
61
+ workspaceName: workspace.name,
62
+ workspacePath: workspace.workspacePath,
63
+ profileName: 'default',
64
+ };
65
+ }
66
+
67
+ function selectable(items: string[]) {
68
+ return items.map((label) => ({ label, value: label, muted: label === '---' }));
69
+ }
70
+
71
+ function workspaceItems() {
72
+ const workspaces = listWorkspaces();
73
+ return [
74
+ { label: 'Create new workspace', value: 'create' },
75
+ { label: '---', value: '---', muted: true },
76
+ ...workspaces.map((workspace) => ({
77
+ label: workspace.name,
78
+ value: `workspace:${workspace.name}`,
79
+ })),
80
+ { label: '---', value: '---', muted: true },
81
+ { label: '<- Back', value: 'back' },
82
+ ];
83
+ }
84
+
85
+ function defaultWorkspacePath(name: string) {
86
+ return join(workspacesDir(), name || 'my-project');
87
+ }
88
+
89
+ function firstSelectableIndex(items: Array<{ muted?: boolean }>, from = 0, delta = 1) {
90
+ if (items.length === 0) return 0;
91
+ let index = from;
92
+ for (let i = 0; i < items.length; i += 1) {
93
+ index = (index + items.length) % items.length;
94
+ if (!items[index]?.muted) return index;
95
+ index += delta;
96
+ }
97
+ return 0;
98
+ }
99
+
100
+ function stepTitle(step: Step) {
101
+ return step.kind === 'done' ? 'Setup complete' : step.title;
102
+ }
103
+
104
+ export function SetupWizard(props: {
105
+ mode: Mode;
106
+ session?: any;
107
+ gaps?: Gap[];
108
+ width: number;
109
+ height: number;
110
+ initialRoute?: string;
111
+ initialWorkspaceName?: string;
112
+ initialWorkspacePath?: string | null;
113
+ closeOnDone?: boolean;
114
+ onComplete: () => void;
115
+ onClose: () => void;
116
+ }) {
117
+ const [route, setRoute] = createSignal(props.initialRoute ?? 'startup');
118
+ const [routeHistory, setRouteHistory] = createSignal<string[]>([]);
119
+ const [stepIndex, setStepIndex] = createSignal(0);
120
+ const [selected, setSelected] = createSignal(0);
121
+ const [input, setInput] = createSignal('');
122
+ const [busy, setBusy] = createSignal(false);
123
+ const [error, setError] = createSignal<string | null>(null);
124
+
125
+ const [logs, setLogs] = createSignal<LogEntry[]>([]);
126
+ const [targetWorkspace, setTargetWorkspace] = createSignal<any>(null);
127
+ const [creationFlow, setCreationFlow] = createSignal(false);
128
+ const [language, setLanguage] = createSignal('');
129
+ const [llm, setLlm] = createSignal<any>({});
130
+ const [vector, setVector] = createSignal<any>({});
131
+
132
+ const startupGaps = createMemo(() => props.gaps ?? []);
133
+ createEffect(() => {
134
+ if (props.mode === 'startup' && startupGaps().length === 0) props.onComplete();
135
+ });
136
+
137
+ const currentGap = () => startupGaps()[stepIndex()];
138
+ const dialogWidth = () => Math.max(44, Math.min(72, Math.floor(props.width * 0.72)));
139
+ const dialogHeight = () => Math.max(22, Math.min(30, Math.floor(props.height * 0.72)));
140
+ const left = () => Math.max(1, Math.floor((props.width - dialogWidth()) / 2));
141
+ const top = () => Math.max(1, Math.floor((props.height - dialogHeight()) / 2));
142
+
143
+ const step = createMemo<Step>(() => {
144
+ const currentRoute = route();
145
+ if (props.mode === 'setup' && currentRoute === 'main') {
146
+ return { kind: 'menu', title: 'wiki-manager - Setup', items: selectable(MAIN_MENU) };
147
+ }
148
+ if (currentRoute === 'workspaces') {
149
+ return { kind: 'menu', title: 'Manage workspaces', items: workspaceItems() };
150
+ }
151
+ if (currentRoute.startsWith('workspace:')) {
152
+ const workspace = listWorkspaces().find((item) => item.name === currentRoute.slice('workspace:'.length));
153
+ if (!workspace) return { kind: 'menu', title: 'Workspace not found', items: selectable(['<- Back']) };
154
+ return {
155
+ kind: 'menu',
156
+ title: workspace.name,
157
+ items: [
158
+ { label: 'Edit LLM configuration', value: 'llm' },
159
+ { label: 'Edit vector search', value: 'vector' },
160
+ { label: 'Rename', value: 'rename' },
161
+ { label: 'Unregister', value: 'unregister' },
162
+ { label: 'Delete all files', value: 'delete' },
163
+ { label: '<- Back', value: 'back' },
164
+ ],
165
+ };
166
+ }
167
+ if (currentRoute === 'agents') {
168
+ const agentContext = props.mode === 'startup' ? currentGap()?.context : null;
169
+ if (agentContext?.dockerMissing) {
170
+ return {
171
+ kind: 'confirm',
172
+ title: 'Docker not installed',
173
+ message: 'Docker is required to run agents.\nInstall Docker Desktop and restart wiki-manager.',
174
+ yesLabel: 'Try anyway',
175
+ noLabel: 'Skip',
176
+ };
177
+ }
178
+ if (agentContext?.dockerUnavailable) {
179
+ return {
180
+ kind: 'confirm',
181
+ title: 'Docker not responding',
182
+ message: 'Docker daemon is not running.\nStart Docker Desktop, then retry.',
183
+ yesLabel: 'Retry',
184
+ noLabel: 'Skip',
185
+ };
186
+ }
187
+ const serviceList = agentContext?.downServices?.join(', ');
188
+ return {
189
+ kind: 'confirm',
190
+ title: 'Agents',
191
+ message: serviceList
192
+ ? `Agents not running: ${serviceList}.\nStart them now?`
193
+ : 'Start external agents?',
194
+ yesLabel: 'Start',
195
+ noLabel: 'Skip',
196
+ };
197
+ }
198
+ if (currentRoute === 'workspace-confirm') {
199
+ return { kind: 'confirm', title: 'Workspace', message: 'No workspace configured.', yesLabel: 'Create', noLabel: 'Skip' };
200
+ }
201
+ if (currentRoute === 'workspace-name') {
202
+ return { kind: 'text', title: 'Workspace', label: 'Workspace name', prefill: props.initialWorkspaceName ?? '' };
203
+ }
204
+ if (currentRoute === 'language') {
205
+ return { kind: 'text', title: 'Workspace', label: 'Language (2 chars, e.g. fr, en)', prefill: language() };
206
+ }
207
+ if (currentRoute === 'workspace-rename') {
208
+ return { kind: 'text', title: 'Rename workspace', label: 'New workspace name', prefill: targetWorkspace()?.name ?? '' };
209
+ }
210
+ if (currentRoute === 'llm-provider') {
211
+ const context = currentWorkspaceContext(props.session, currentGap()?.context ?? targetWorkspace());
212
+ return {
213
+ kind: 'select',
214
+ title: 'LLM configuration',
215
+ label: context?.configError
216
+ ? `${context.configError} Select a provider after creating or fixing the config:`
217
+ : `No LLM configured${context?.workspaceName ? ` for ${context.workspaceName}` : ''}. Select a provider:`,
218
+ options: PROVIDERS,
219
+ };
220
+ }
221
+ if (currentRoute === 'llm-baseurl') {
222
+ const baseUrl = llm().baseUrl || defaultBaseUrl(llm().provider);
223
+ return { kind: 'text', title: 'LLM configuration', label: 'Base URL', prefill: baseUrl, placeholder: baseUrl };
224
+ }
225
+ if (currentRoute === 'llm-apikey') {
226
+ return { kind: 'text', title: 'LLM configuration', label: 'API key (required)', secret: true };
227
+ }
228
+ if (currentRoute === 'llm-model') {
229
+ const defaultModel = llm().model || fallbackModels(llm().provider)[0] || '';
230
+ return { kind: 'text', title: 'LLM configuration', label: 'Model', prefill: defaultModel };
231
+ }
232
+ if (currentRoute === 'vector-confirm') {
233
+ return { kind: 'confirm', title: 'Vector search', message: 'Configure vector search?', yesLabel: 'Enable', noLabel: 'Skip' };
234
+ }
235
+ if (currentRoute === 'vector-baseurl') {
236
+ const baseUrl = vector().baseUrl || llm().baseUrl || defaultBaseUrl(llm().provider);
237
+ return { kind: 'text', title: 'Vector search', label: 'Embeddings/rerank base URL', prefill: baseUrl, placeholder: baseUrl };
238
+ }
239
+ if (currentRoute === 'vector-apikey') {
240
+ const hint = llm().apiKey ? '(leave empty to reuse LLM key)' : undefined;
241
+ return { kind: 'text', title: 'Vector search', label: 'Vector API key', placeholder: hint, secret: true };
242
+ }
243
+ if (currentRoute === 'vector-model') {
244
+ const defaultEmbedding = vector().embeddingModel || fallbackModels(vector().provider || llm().provider, 'embedding')[0] || '';
245
+ return { kind: 'text', title: 'Vector search', label: 'Embedding model', prefill: defaultEmbedding };
246
+ }
247
+ if (currentRoute === 'vector-rerank') {
248
+ return { kind: 'confirm', title: 'Vector search', message: 'Enable reranking?', yesLabel: 'Enable', noLabel: 'Skip' };
249
+ }
250
+ if (currentRoute === 'vector-rerank-model') {
251
+ const defaultReranker = vector().rerankerModel || 'BAAI/bge-reranker-v2-m3';
252
+ return { kind: 'text', title: 'Vector search', label: 'Rerank model', prefill: defaultReranker };
253
+ }
254
+ if (currentRoute === 'unregister-confirm') {
255
+ const workspace = targetWorkspace();
256
+ return {
257
+ kind: 'select',
258
+ title: 'Unregister workspace',
259
+ label: `Remove ${workspace?.name ?? 'workspace'} from registry. Source files at ${workspace?.workspacePath ?? '-'} are kept.`,
260
+ options: ['Cancel', 'Confirm'],
261
+ };
262
+ }
263
+ if (currentRoute === 'delete-confirm') {
264
+ const workspace = targetWorkspace();
265
+ return {
266
+ kind: 'select',
267
+ title: 'Delete workspace files',
268
+ label: `Permanently delete ${workspace?.workspacePath ?? '-'} and remove from registry. This cannot be undone.`,
269
+ options: ['Cancel', 'Confirm'],
270
+ };
271
+ }
272
+ return { kind: 'done' };
273
+ });
274
+
275
+ createEffect(() => {
276
+ const s = step();
277
+ setError(null);
278
+ setInput((s as any).prefill ?? '');
279
+ const items = (s as any).items ?? (s as any).options?.map((label: string) => ({ label })) ?? [{ label: 'x' }];
280
+ let preferred = -1;
281
+ if (route() === 'llm-provider' && llm().provider) {
282
+ preferred = PROVIDERS.findIndex((p) => normalizeProvider(p) === normalizeProvider(llm().provider));
283
+ }
284
+ setSelected(preferred >= 0 ? preferred : firstSelectableIndex(items));
285
+ });
286
+
287
+ function preloadWikirc(context: any) {
288
+ const workspacePath = context?.workspacePath;
289
+ if (!workspacePath) return;
290
+ try {
291
+ const { config } = loadWikircProfile(workspacePath, context?.profileName ?? 'default');
292
+ if (config?.language) setLanguage(String(config.language));
293
+ if (config?.llm?.provider) {
294
+ const model = config.llm.model;
295
+ setLlm({
296
+ provider: normalizeProvider(config.llm.provider),
297
+ baseUrl: config.llm.baseUrl,
298
+ apiKey: config.llm.apiKey,
299
+ model: (model && !PLACEHOLDER_MODEL_RE.test(model)) ? model : null,
300
+ });
301
+ }
302
+ if (config?.retrieval?.vector) {
303
+ setVector({
304
+ provider: config.retrieval.vector.provider,
305
+ baseUrl: config.retrieval.vector.baseUrl,
306
+ apiKey: config.retrieval.vector.apiKey,
307
+ embeddingModel: config.retrieval.vector.embeddingModel,
308
+ rerankEnabled: config.retrieval.vector.rerankEnabled,
309
+ rerankerModel: config.retrieval.vector.rerankerModel,
310
+ });
311
+ }
312
+ } catch { /* ignore — new workspace or unreadable profile */ }
313
+ }
314
+
315
+ createEffect(() => {
316
+ if (props.mode === 'setup') setRoute(props.initialRoute ?? 'main');
317
+ if (props.mode === 'startup') {
318
+ const gap = startupGaps()[0];
319
+ if (gap?.kind === 'llm' || gap?.kind === 'vector') {
320
+ preloadWikirc(currentWorkspaceContext(props.session, gap.context));
321
+ }
322
+ setRoute(startupRoute(gap));
323
+ }
324
+ });
325
+
326
+ function startupRoute(gap?: Gap) {
327
+ if (!gap) return 'done';
328
+ if (gap.kind === 'agents') return 'agents';
329
+ if (gap.kind === 'workspace') return 'workspace-confirm';
330
+ if (gap.kind === 'llm') return 'llm-provider';
331
+ if (gap.kind === 'vector') return 'vector-confirm';
332
+ return 'done';
333
+ }
334
+
335
+ function nextStartup(label?: string) {
336
+ if (label) setLogs((items) => [...items, { icon: '✓', label }]);
337
+ setRouteHistory([]);
338
+ if (props.mode !== 'startup') {
339
+ if (props.closeOnDone) {
340
+ props.onComplete();
341
+ return;
342
+ }
343
+ setRoute('main');
344
+ return;
345
+ }
346
+ const next = stepIndex() + 1;
347
+ setStepIndex(next);
348
+ const nextGap = startupGaps()[next];
349
+ if (!nextGap) props.onComplete();
350
+ else {
351
+ if (nextGap.kind === 'llm' || nextGap.kind === 'vector') {
352
+ preloadWikirc(currentWorkspaceContext(props.session, nextGap.context));
353
+ }
354
+ setRoute(startupRoute(nextGap));
355
+ }
356
+ }
357
+
358
+ function skipCurrent() {
359
+ const s = step();
360
+ setRouteHistory([]);
361
+ if (props.mode === 'setup') {
362
+ if (props.closeOnDone) {
363
+ props.onClose();
364
+ return;
365
+ }
366
+ if (route() === 'main') props.onClose();
367
+ else setRoute(route().startsWith('workspace:') ? 'workspaces' : 'main');
368
+ return;
369
+ }
370
+ setLogs((items) => [...items, { icon: '->', label: stepTitle(s), detail: 'skipped' }]);
371
+ nextStartup();
372
+ }
373
+
374
+ async function runAction(fn: () => Promise<void>) {
375
+ setBusy(true);
376
+ setError(null);
377
+ try {
378
+ await fn();
379
+ } catch (err) {
380
+ setError(err instanceof Error ? err.message : String(err));
381
+ } finally {
382
+ setBusy(false);
383
+ }
384
+ }
385
+
386
+ function navigate(newRoute: string) {
387
+ setRouteHistory((h) => [...h, route()]);
388
+ setRoute(newRoute);
389
+ }
390
+
391
+ function jumpTo(newRoute: string) {
392
+ setRouteHistory([]);
393
+ setRoute(newRoute);
394
+ }
395
+
396
+ function goBack() {
397
+ const history = routeHistory();
398
+ if (!history.length) { props.onClose(); return; }
399
+ setRouteHistory(history.slice(0, -1));
400
+ setError(null);
401
+ setRoute(history[history.length - 1]);
402
+ }
403
+
404
+ async function commitLlmModel(value: string) {
405
+ const context = currentWorkspaceContext(props.session, currentGap()?.context ?? targetWorkspace());
406
+ if (!context?.workspacePath) return setError('No workspace available.');
407
+ await runAction(async () => {
408
+ writeLlmConfig(context.workspacePath, context.profileName ?? 'default', { ...llm(), model: value });
409
+ setLogs((items) => [...items, { icon: '✓', label: 'LLM configured', detail: value }]);
410
+ if (creationFlow()) navigate('vector-confirm');
411
+ else nextStartup();
412
+ });
413
+ }
414
+
415
+ async function commitVectorRerank(rerankerModel: string) {
416
+ const context = currentWorkspaceContext(props.session, currentGap()?.context ?? targetWorkspace());
417
+ if (!context?.workspacePath) return setError('No workspace available.');
418
+ await runAction(async () => {
419
+ writeVectorConfig(context.workspacePath, context.profileName ?? 'default', {
420
+ ...vector(),
421
+ rerankEnabled: true,
422
+ rerankerModel,
423
+ });
424
+ setCreationFlow(false);
425
+ nextStartup('Vector search configured');
426
+ });
427
+ }
428
+
429
+ async function submitSelect(value: string) {
430
+ const currentRoute = route();
431
+ if (currentRoute === 'main') {
432
+ if (value === 'Close') return props.onClose();
433
+ if (value === 'Agents') return navigate('agents');
434
+ if (value === 'Workspaces') return navigate('workspaces');
435
+ if (value === 'LLM configuration') return navigate('llm-provider');
436
+ if (value === 'Vector search') return navigate('vector-confirm');
437
+ return;
438
+ }
439
+ if (currentRoute === 'workspaces') {
440
+ if (value === 'back') return goBack();
441
+ if (value === 'create') return navigate('workspace-name');
442
+ if (value.startsWith('workspace:')) return navigate(value);
443
+ return;
444
+ }
445
+ if (currentRoute.startsWith('workspace:')) {
446
+ if (value === 'back') return goBack();
447
+ const workspace = listWorkspaces().find((item) => item.name === currentRoute.slice('workspace:'.length));
448
+ setTargetWorkspace(workspace);
449
+ if (value === 'llm') {
450
+ preloadWikirc(currentWorkspaceContext(props.session, targetWorkspace()));
451
+ return navigate('llm-provider');
452
+ }
453
+ if (value === 'vector') {
454
+ preloadWikirc(currentWorkspaceContext(props.session, targetWorkspace()));
455
+ return navigate('vector-confirm');
456
+ }
457
+ if (value === 'rename') return navigate('workspace-rename');
458
+ if (value === 'unregister') return navigate('unregister-confirm');
459
+ if (value === 'delete') return navigate('delete-confirm');
460
+ return;
461
+ }
462
+ if (currentRoute === 'llm-provider') {
463
+ const provider = normalizeProvider(value);
464
+ setLlm((old: any) => {
465
+ const baseUrl = (old.provider === provider && old.baseUrl) ? old.baseUrl : defaultBaseUrl(provider);
466
+ return { ...old, provider, baseUrl, ...(provider === 'ollama' && !old.apiKey ? { apiKey: 'ollama' } : {}) };
467
+ });
468
+ if (provider === 'ollama' || provider === 'openai-compatible') return navigate('llm-baseurl');
469
+ return navigate('llm-apikey');
470
+ }
471
+ if (currentRoute === 'unregister-confirm') {
472
+ if (value === 'Cancel') return goBack();
473
+ await runAction(async () => {
474
+ await unregisterWorkspace(targetWorkspace()?.name);
475
+ setLogs((items) => [...items, { icon: '✓', label: 'Workspace unregistered', detail: targetWorkspace()?.name }]);
476
+ jumpTo('workspaces');
477
+ });
478
+ return;
479
+ }
480
+ if (currentRoute === 'delete-confirm') {
481
+ if (value === 'Cancel') return goBack();
482
+ await runAction(async () => {
483
+ await deleteWorkspaceAndFiles(targetWorkspace()?.name, targetWorkspace()?.workspacePath);
484
+ setLogs((items) => [...items, { icon: '✓', label: 'Workspace deleted', detail: targetWorkspace()?.name }]);
485
+ jumpTo('workspaces');
486
+ });
487
+ }
488
+ }
489
+
490
+ async function submitConfirm(yes: boolean) {
491
+ const currentRoute = route();
492
+ if (!yes && currentRoute === 'vector-rerank') {
493
+ const context = currentWorkspaceContext(props.session, currentGap()?.context ?? targetWorkspace());
494
+ if (!context?.workspacePath) return setError('No workspace available.');
495
+ await runAction(async () => {
496
+ writeVectorConfig(context.workspacePath, context.profileName ?? 'default', {
497
+ ...vector(),
498
+ rerankEnabled: false,
499
+ });
500
+ setCreationFlow(false);
501
+ nextStartup('Vector search configured');
502
+ });
503
+ return;
504
+ }
505
+ if (!yes) {
506
+ if (currentRoute === 'vector-confirm') setCreationFlow(false);
507
+ return skipCurrent();
508
+ }
509
+ if (currentRoute === 'agents') {
510
+ await runAction(async () => {
511
+ await startAgents();
512
+ nextStartup('Agents running');
513
+ });
514
+ return;
515
+ }
516
+ if (currentRoute === 'workspace-confirm') return navigate('workspace-name');
517
+ if (currentRoute === 'vector-rerank') return navigate('vector-rerank-model');
518
+ if (currentRoute === 'vector-confirm') {
519
+ setVector((old: any) => ({
520
+ ...old,
521
+ provider: old.provider || llm().provider,
522
+ baseUrl: old.baseUrl || llm().baseUrl || defaultBaseUrl(llm().provider),
523
+ }));
524
+ return navigate('vector-baseurl');
525
+ }
526
+ }
527
+
528
+ async function submitText() {
529
+ const currentRoute = route();
530
+ const value = input().trim();
531
+ if (currentRoute === 'language') {
532
+ const lang = value.toLowerCase().replace(/[^a-z]/g, '').slice(0, 2);
533
+ if (lang.length < 2) return setError('Please enter a 2-character language code (e.g. fr, en).');
534
+ const context = currentWorkspaceContext(props.session, currentGap()?.context ?? targetWorkspace());
535
+ if (context?.workspacePath) writeLanguageConfig(context.workspacePath, context.profileName ?? 'default', lang);
536
+ navigate('llm-provider');
537
+ return;
538
+ }
539
+ if (currentRoute === 'workspace-name') {
540
+ if (!value) return setError('Workspace name is required.');
541
+ await runAction(async () => {
542
+ const created = await createNewWorkspace(value, props.initialWorkspacePath ?? null);
543
+ const workspacePath = created.workspace?.workspacePath ?? defaultWorkspacePath(value);
544
+ const newTarget = { workspaceName: value, workspacePath, profileName: 'default' };
545
+ setTargetWorkspace(newTarget);
546
+ preloadWikirc(newTarget);
547
+ setCreationFlow(true);
548
+ setLogs((items) => [...items, { icon: '✓', label: `Workspace: ${value}` }]);
549
+ navigate('language');
550
+ });
551
+ return;
552
+ }
553
+ if (currentRoute === 'workspace-rename') {
554
+ if (!value) return setError('Workspace name is required.');
555
+ await runAction(async () => {
556
+ const renamed = await renameWorkspace(targetWorkspace()?.name, value);
557
+ setLogs((items) => [...items, { icon: '✓', label: 'Workspace renamed', detail: `${renamed.previousName} -> ${renamed.name}` }]);
558
+ jumpTo('workspaces');
559
+ });
560
+ return;
561
+ }
562
+ if (currentRoute === 'llm-baseurl') {
563
+ setLlm((old: any) => ({ ...old, baseUrl: value || old.baseUrl || defaultBaseUrl(old.provider) }));
564
+ if (llm().provider === 'ollama') return navigate('llm-model');
565
+ return navigate('llm-apikey');
566
+ }
567
+ if (currentRoute === 'llm-apikey') {
568
+ if (!value) return setError('API key is required.');
569
+ setLlm((old: any) => ({ ...old, apiKey: value }));
570
+ return navigate('llm-model');
571
+ }
572
+ if (currentRoute === 'vector-baseurl') {
573
+ const baseUrl = value || vector().baseUrl || llm().baseUrl || defaultBaseUrl(llm().provider);
574
+ setVector((old: any) => ({ ...old, provider: llm().provider, baseUrl }));
575
+ return navigate('vector-apikey');
576
+ }
577
+ if (currentRoute === 'vector-apikey') {
578
+ const apiKey = value || llm().apiKey || undefined;
579
+ if (!apiKey) return setError('API key is required (or set LLM key first).');
580
+ setVector((old: any) => ({ ...old, apiKey }));
581
+ return navigate('vector-model');
582
+ }
583
+ if (currentRoute === 'llm-model') {
584
+ if (!value) return setError('Model name is required.');
585
+ await commitLlmModel(value);
586
+ return;
587
+ }
588
+ if (currentRoute === 'vector-model') {
589
+ if (!value) return setError('Model name is required.');
590
+ setVector((old: any) => ({ ...old, embeddingModel: value }));
591
+ return navigate('vector-rerank');
592
+ }
593
+ if (currentRoute === 'vector-rerank-model') {
594
+ if (!value) return setError('Model name is required.');
595
+ await commitVectorRerank(value);
596
+ return;
597
+ }
598
+ }
599
+
600
+ function readClipboard(): string {
601
+ try {
602
+ if (process.platform === 'darwin') return execFileSync('pbpaste', [], { encoding: 'utf8' }).replace(/\n$/, '');
603
+ if (process.platform === 'win32') return execFileSync('powershell', ['-command', 'Get-Clipboard'], { encoding: 'utf8' }).trimEnd();
604
+ try { return execFileSync('wl-paste', ['--no-newline'], { encoding: 'utf8' }); } catch { /**/ }
605
+ return execFileSync('xclip', ['-selection', 'clipboard', '-o'], { encoding: 'utf8' });
606
+ } catch { return ''; }
607
+ }
608
+
609
+ useKeyboard((key: any) => {
610
+ if (busy()) return;
611
+ const s = step();
612
+ const keyName = String(key.name ?? '').toLowerCase();
613
+ const sequence = String(key.sequence ?? '');
614
+ const lowerSequence = sequence.toLowerCase();
615
+ const isCopyExit = ((key.ctrl || key.meta) && keyName === 'c') || sequence === '\x03' || (key.meta && lowerSequence === '\x1bc');
616
+ const isPaste = ((key.ctrl || key.meta) && keyName === 'v') || (key.meta && lowerSequence === '\x1bv');
617
+ const isBack = key.ctrl && keyName === 'z';
618
+ const isEnter = keyName === 'return' || keyName === 'enter' || keyName === 'linefeed';
619
+ if (isCopyExit) {
620
+ props.onClose();
621
+ return;
622
+ }
623
+ if (isBack && routeHistory().length > 0) {
624
+ goBack();
625
+ return;
626
+ }
627
+ if (keyName === 'escape') {
628
+ skipCurrent();
629
+ return;
630
+ }
631
+ if (s.kind === 'menu') {
632
+ if (keyName === 'up') setSelected((value) => firstSelectableIndex(s.items, value - 1, -1));
633
+ else if (keyName === 'down') setSelected((value) => firstSelectableIndex(s.items, value + 1, 1));
634
+ else if (isEnter) void submitSelect(s.items[selected()]?.value);
635
+ return;
636
+ }
637
+ if (s.kind === 'select') {
638
+ if (keyName === 'up') setSelected((value) => (value + s.options.length - 1) % s.options.length);
639
+ else if (keyName === 'down') setSelected((value) => (value + 1) % s.options.length);
640
+ else if (isEnter) void submitSelect(s.options[selected()]);
641
+ return;
642
+ }
643
+ if (s.kind === 'confirm') {
644
+ if (keyName === 'up' || keyName === 'down' || keyName === 'tab') setSelected((value) => value === 0 ? 1 : 0);
645
+ else if (isEnter) void submitConfirm(selected() === 0);
646
+ return;
647
+ }
648
+ if (s.kind === 'text') {
649
+ // Bracketed paste: ESC[200~...text...ESC[201~
650
+ if (sequence.startsWith('\x1b[200~')) {
651
+ let pasted = sequence.slice(6);
652
+ const closeIdx = pasted.indexOf('\x1b[201~');
653
+ if (closeIdx !== -1) pasted = pasted.slice(0, closeIdx);
654
+ pasted = pasted.split('\r').join('');
655
+ if (pasted) setInput((value) => value + pasted);
656
+ return;
657
+ }
658
+ // Explicit clipboard paste (Ctrl+V or Cmd+V on macOS)
659
+ if (isPaste) {
660
+ const pasted = readClipboard();
661
+ if (pasted) setInput((value) => value + pasted);
662
+ return;
663
+ }
664
+ if (isEnter) {
665
+ void submitText();
666
+ return;
667
+ }
668
+ if (keyName === 'backspace') {
669
+ setInput((value) => value.slice(0, -1));
670
+ return;
671
+ }
672
+ if (sequence.length >= 1 && !sequence.startsWith('\x1b') && sequence >= ' ') {
673
+ setInput((value) => value + sequence);
674
+ }
675
+ }
676
+ });
677
+
678
+ const currentItems = () => {
679
+ const s = step();
680
+ if (s.kind === 'menu') return s.items;
681
+ if (s.kind === 'select') return s.options.map((label) => ({ label, value: label }));
682
+ if (s.kind === 'confirm') return [{ label: s.yesLabel, value: 'yes' }, { label: s.noLabel, value: 'no' }];
683
+ return [];
684
+ };
685
+
686
+ const displayValue = () => {
687
+ const s = step();
688
+ if (s.kind !== 'text') return '';
689
+ const value = input();
690
+ return value || (s.secret ? (s.placeholder ?? '') : '');
691
+ };
692
+ const inputHasValue = () => step().kind === 'text' && input().length > 0;
693
+ const lineWidth = () => Math.max(10, dialogWidth() - 10);
694
+ const displayLine1 = () => displayValue().slice(0, lineWidth());
695
+ const displayLine2 = () => displayValue().slice(lineWidth(), lineWidth() * 2);
696
+ const displayLine3 = () => displayValue().slice(lineWidth() * 2);
697
+ const showLine2 = () => displayValue().length > lineWidth();
698
+ const showLine3 = () => displayValue().length > lineWidth() * 2;
699
+ const contextPath = () => targetWorkspace()?.workspacePath ?? (currentGap()?.context?.workspacePath ?? null);
700
+
701
+ const contextSummary = createMemo(() => {
702
+ const parts: string[] = [];
703
+ const wp = targetWorkspace()?.workspacePath ?? (currentGap()?.context?.workspacePath ?? null);
704
+ if (wp) parts.push(wp);
705
+ if (language()) parts.push(`lang:${language()}`);
706
+ const p = llm().provider;
707
+ if (p) parts.push(p);
708
+ if (llm().baseUrl) parts.push(llm().baseUrl);
709
+ if (llm().apiKey) parts.push('key:***');
710
+ const model = llm().model;
711
+ if (model) parts.push(model);
712
+ if (vector().baseUrl && vector().baseUrl !== llm().baseUrl) parts.push(`vec:${vector().baseUrl}`);
713
+ if (vector().embeddingModel) parts.push(vector().embeddingModel);
714
+ return parts.join(' ');
715
+ });
716
+
717
+ return (
718
+ <box
719
+ position="absolute"
720
+ left={left()}
721
+ top={top()}
722
+ width={dialogWidth()}
723
+ height={dialogHeight()}
724
+ zIndex={40}
725
+ border
726
+ borderStyle="rounded"
727
+ borderColor="#8BD5CA"
728
+ backgroundColor="#111318"
729
+ padding={1}
730
+ flexDirection="column"
731
+ overflow="hidden"
732
+ >
733
+ <For each={logs().slice(-4)}>
734
+ {(entry) => <text height={1} fg={entry.icon === '✓' ? '#8BD5CA' : '#9CA3AF'}>{entry.icon} {entry.label}{entry.detail ? ` - ${entry.detail}` : ''}</text>}
735
+ </For>
736
+ <text height={1} fg="#FBBF24">{busy() ? `${stepTitle(step())} - working...` : stepTitle(step())}</text>
737
+ <text height={1}>{''}</text>
738
+ <Show when={(step() as any).message || (step() as any).label}>
739
+ <text height={1} fg="#D6DEE8">{(step() as any).message ?? (step() as any).label}</text>
740
+ </Show>
741
+ <Show when={step().kind === 'text'}>
742
+ <box
743
+ height={5}
744
+ border
745
+ borderStyle="single"
746
+ borderColor="#8BD5CA"
747
+ backgroundColor="#0B1220"
748
+ padding={1}
749
+ flexDirection="column"
750
+ overflow="hidden"
751
+ >
752
+ <box flexDirection="row" height={1}>
753
+ <text fg="#8BD5CA">{'> '}</text>
754
+ <text fg={inputHasValue() ? '#D6DEE8' : '#7F8C8D'}>{displayLine1()}</text>
755
+ <Show when={!showLine2()}>
756
+ <text fg="#111318" bg="#8BD5CA"> </text>
757
+ </Show>
758
+ </box>
759
+ <box flexDirection="row" height={1}>
760
+ <text fg="#8BD5CA">{' '}</text>
761
+ <text fg={inputHasValue() ? '#D6DEE8' : '#7F8C8D'}>{displayLine2()}</text>
762
+ <Show when={showLine2() && !showLine3()}>
763
+ <text fg="#111318" bg="#8BD5CA"> </text>
764
+ </Show>
765
+ </box>
766
+ <box flexDirection="row" height={1}>
767
+ <text fg="#8BD5CA">{' '}</text>
768
+ <text fg={inputHasValue() ? '#D6DEE8' : '#7F8C8D'}>{displayLine3()}</text>
769
+ <Show when={showLine3()}>
770
+ <text fg="#111318" bg="#8BD5CA"> </text>
771
+ </Show>
772
+ </box>
773
+ </box>
774
+ </Show>
775
+ <Show when={step().kind !== 'text'}>
776
+ <For each={currentItems()}>
777
+ {(item, index) => (
778
+ <text
779
+ height={1}
780
+ fg={(item as any).muted ? '#4B5563' : index() === selected() ? '#111318' : '#D6DEE8'}
781
+ bg={index() === selected() && !(item as any).muted ? '#8BD5CA' : '#111318'}
782
+ >
783
+ {(item as any).muted ? ' ---' : `${index() === selected() ? '> ' : ' '}${item.label}`}
784
+ </text>
785
+ )}
786
+ </For>
787
+ </Show>
788
+
789
+ <Show when={error()}>
790
+ {(message) => <text height={6} fg="#F87171">{message()}</text>}
791
+ </Show>
792
+ <box flexGrow={1} />
793
+ <Show when={contextSummary()}>
794
+ <text height={1} fg="#374151">{contextSummary()}</text>
795
+ </Show>
796
+ <text height={1}>{''}</text>
797
+ <box height={1} flexDirection="row">
798
+ <text fg="#7F8C8D">{step().kind === 'text' ? 'Enter Confirm Esc Skip' : 'Up/Down Enter Select Esc Skip'}</text>
799
+ <box flexGrow={1} />
800
+ <Show when={routeHistory().length > 0}>
801
+ <text fg="#7F8C8D">Ctrl+Z ←</text>
802
+ </Show>
803
+ </box>
804
+ </box>
805
+ );
806
+ }