@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,111 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+ import YAML from 'yaml';
7
+ import { patchWikircProfile } from './wikirc.js';
8
+ import { writeVectorConfig } from './wikiSetup.js';
9
+
10
+ test('patchWikircProfile merges keys and preserves existing values', () => {
11
+ const root = mkdtempSync(join(tmpdir(), 'wikirc-patch-'));
12
+ const file = join(root, '.wikirc.yaml');
13
+ writeFileSync(file, [
14
+ '# workspace config',
15
+ 'language: en-US',
16
+ 'llm:',
17
+ ' provider: openai',
18
+ ' temperature: 0.2',
19
+ '',
20
+ ].join('\n'), 'utf8');
21
+
22
+ patchWikircProfile(root, 'default', {
23
+ llm: {
24
+ model: 'gpt-5.4-mini',
25
+ apiKey: 'secret',
26
+ },
27
+ retrieval: {
28
+ vector: {
29
+ enabled: true,
30
+ embeddingModel: 'text-embedding-3-small',
31
+ },
32
+ },
33
+ });
34
+
35
+ const raw = readFileSync(file, 'utf8');
36
+ const parsed = YAML.parse(raw);
37
+ assert.match(raw, /# workspace config/);
38
+ assert.equal(parsed.language, 'en-US');
39
+ assert.equal(parsed.llm.provider, 'openai');
40
+ assert.equal(parsed.llm.temperature, 0.2);
41
+ assert.equal(parsed.llm.model, 'gpt-5.4-mini');
42
+ assert.equal(parsed.retrieval.vector.enabled, true);
43
+ });
44
+
45
+ test('writeVectorConfig writes llm-wiki vector and rerank keys', () => {
46
+ const root = mkdtempSync(join(tmpdir(), 'wikirc-vector-'));
47
+ writeFileSync(join(root, '.wikirc.yaml'), [
48
+ 'language: en',
49
+ 'llm:',
50
+ ' provider: openai-compatible',
51
+ ' baseUrl: http://localhost:8000/v1',
52
+ ' apiKey: llm-key',
53
+ ' model: chat-model',
54
+ 'retrieval:',
55
+ ' vector:',
56
+ ' enabled: false',
57
+ '',
58
+ ].join('\n'), 'utf8');
59
+
60
+ writeVectorConfig(root, 'default', {
61
+ baseUrl: 'http://localhost:7997/v1',
62
+ apiKey: 'vector-key',
63
+ embeddingModel: 'BAAI/bge-m3',
64
+ rerankEnabled: true,
65
+ rerankerModel: 'BAAI/bge-reranker-v2-m3',
66
+ });
67
+
68
+ const parsed = YAML.parse(readFileSync(join(root, '.wikirc.yaml'), 'utf8'));
69
+ assert.equal(parsed.retrieval.vector.enabled, true);
70
+ assert.equal(parsed.retrieval.vector.baseUrl, 'http://localhost:7997/v1');
71
+ assert.equal(parsed.retrieval.vector.apiKey, 'vector-key');
72
+ assert.equal(parsed.retrieval.vector.embeddingModel, 'BAAI/bge-m3');
73
+ assert.equal(parsed.retrieval.vector.rerankEnabled, true);
74
+ assert.equal(parsed.retrieval.vector.rerankerModel, 'BAAI/bge-reranker-v2-m3');
75
+ });
76
+
77
+ test('writeVectorConfig removes commented vector placeholders it replaces', () => {
78
+ const root = mkdtempSync(join(tmpdir(), 'wikirc-vector-comments-'));
79
+ writeFileSync(join(root, '.wikirc.yaml'), [
80
+ 'language: en',
81
+ 'llm:',
82
+ ' provider: openai-compatible',
83
+ ' baseUrl: http://localhost:8000/v1',
84
+ ' apiKey: llm-key',
85
+ ' model: chat-model',
86
+ 'retrieval:',
87
+ ' vector:',
88
+ ' enabled: false',
89
+ ' # Defaults to llm.baseUrl.',
90
+ ' # baseUrl: http://127.0.0.1:7997/v1',
91
+ ' # apiKey: your-vector-key',
92
+ ' embeddingModel: BAAI/bge-m3',
93
+ ' rerankerModel: BAAI/bge-reranker-v2-m3',
94
+ '',
95
+ ].join('\n'), 'utf8');
96
+
97
+ writeVectorConfig(root, 'default', {
98
+ baseUrl: 'http://localhost:7997/v1',
99
+ apiKey: 'vector-key',
100
+ embeddingModel: 'BAAI/bge-m3',
101
+ rerankEnabled: true,
102
+ rerankerModel: 'custom-reranker',
103
+ });
104
+
105
+ const raw = readFileSync(join(root, '.wikirc.yaml'), 'utf8');
106
+ const parsed = YAML.parse(raw);
107
+ assert.doesNotMatch(raw, /^\s*#\s*baseUrl:/m);
108
+ assert.doesNotMatch(raw, /^\s*#\s*apiKey:/m);
109
+ assert.equal(parsed.retrieval.vector.baseUrl, 'http://localhost:7997/v1');
110
+ assert.equal(parsed.retrieval.vector.apiKey, 'vector-key');
111
+ });
@@ -49,7 +49,7 @@ export function findWorkspace(name) {
49
49
  return listWorkspaces().find((workspace) => workspace.name === name);
50
50
  }
51
51
 
52
- function isValidWorkspaceName(name) {
52
+ export function isValidWorkspaceName(name) {
53
53
  return (
54
54
  typeof name === 'string' &&
55
55
  /^[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/.test(name) &&
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { For, createEffect, createMemo, createSignal } from 'solid-js';
2
+ import { For, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
3
3
  import { colorForRenderedLine, helpCommandParts, keyValueParts, renderPlainMarkdown } from './renderer';
4
4
 
5
5
  const LEGACY_DONNA_ROLE = 'do' + 't';
@@ -59,12 +59,12 @@ const STATUS_COLUMN_GAP = 2;
59
59
  type HelpCard = { title: string; text: string; example: string };
60
60
 
61
61
  const HELP_CARDS: HelpCard[] = [
62
+ { title: '/use', text: 'Load one workspace.', example: 'Ex: /use <workspace>' },
62
63
  { title: '/new', text: 'Create/configure a workspace.', example: 'Ex: /new <name> [path]' },
63
- { title: 'modify wikirc', text: 'Action: edit file.', example: 'File: .wikirc.yaml' },
64
- { title: 'configure CME', text: 'Add type, URL, PAT, email.', example: 'Provide source credentials' },
65
- { title: 'add MCP', text: 'Inspect and use MCP endpoints.', example: 'Ex: /mcp endpoints' },
66
- { title: '/use', text: 'Load one workspace and its tools.', example: 'Ex: /use <workspace>' },
67
- { title: 'llm call action', text: 'Ask to LLM.', example: 'Ex: ask to LLM to run an action' },
64
+ { title: '/config', text: 'Modify LLM configuration.', example: 'Ex: /config edit <profile>' },
65
+ { title: '/mcp', text: 'View MCP.', example: 'Ex: /mcp status' },
66
+ { title: 'configure CME', text: 'Add type, URL, PAT, email.', example: 'Ask to donna to set credentials' },
67
+ { title: '/status', text: 'Check config.', example: 'Ex: /status' },
68
68
  ];
69
69
 
70
70
  function HelpCardPanel(props: { card: HelpCard; width: number }) {
@@ -364,6 +364,7 @@ function conversationLines(messages: Array<{ role: string; content: string }>, c
364
364
  if (isStatusOutput(message)) {
365
365
  return [
366
366
  { segments: messageHeaderSegments(message.role, columns), copyContent: raw },
367
+ { segments: [{ text: ' ', color: '#D6DEE8' }] },
367
368
  ...raw.split('\n').map((line) => {
368
369
  const { left, right } = statusColumns(line || ' ');
369
370
  return { status: true, statusLeft: left, statusRight: right, segments: [] };
@@ -379,6 +380,7 @@ function conversationLines(messages: Array<{ role: string; content: string }>, c
379
380
  }
380
381
  return [
381
382
  { segments: messageHeaderSegments(message.role, columns), copyContent: raw },
383
+ { segments: [{ text: ' ', color: '#D6DEE8' }] },
382
384
  ...renderMarkdownLines(lines, message.role, columns),
383
385
  { segments: [{ text: ' ', color: '#D6DEE8' }] },
384
386
  ];
@@ -397,7 +399,7 @@ export function ConversationView(props: {
397
399
  const allLines = createMemo(() => conversationLines(props.messages, props.columns));
398
400
  const visibleLines = () => {
399
401
  const lines = allLines();
400
- const rows = Math.max(1, props.rows - 1);
402
+ const rows = Math.max(1, props.rows - 2);
401
403
  const maxScroll = Math.max(0, lines.length - rows);
402
404
  const scroll = Math.min(props.scroll, maxScroll);
403
405
  const end = lines.length - scroll;
@@ -406,7 +408,7 @@ export function ConversationView(props: {
406
408
  };
407
409
  const scrollHint = () => {
408
410
  const lines = allLines();
409
- const rows = Math.max(1, props.rows - 1);
411
+ const rows = Math.max(1, props.rows - 2);
410
412
  const maxScroll = Math.max(0, lines.length - rows);
411
413
  const scroll = Math.min(props.scroll, maxScroll);
412
414
  if (maxScroll === 0) return '';
@@ -483,6 +485,7 @@ export function ConversationView(props: {
483
485
  )
484
486
  )}
485
487
  </For>
488
+ <text height={1} />
486
489
  </box>
487
490
  );
488
491
  }
@@ -504,28 +507,34 @@ export function ChatInput(props: {
504
507
  const minRows = 1;
505
508
  const maxRows = 5;
506
509
  const [textareaRows, setTextareaRows] = createSignal(minRows);
510
+ let disposed = false;
507
511
  const idleColor = () => props.chatMode ? '#22C55E' : '#06B6D4';
508
512
  const promptText = () => props.busy ? `${props.spinnerFrame} ` : props.prompt;
509
513
  const boxHeight = () => textareaRows() + 2;
514
+ const inputColumns = () => Math.max(8, props.width - promptText().length - 6);
510
515
  const textareaColumns = () => {
511
516
  const measured = Number(textareaRef?.width ?? 0);
512
- if (Number.isFinite(measured) && measured > 0) return Math.max(8, measured - 1);
513
- return Math.max(8, props.width - promptText().length - 7);
517
+ const bounded = Math.min(inputColumns(), Number.isFinite(measured) && measured > 0 ? measured : inputColumns());
518
+ return Math.max(8, bounded - 1);
514
519
  };
515
520
  const estimatedVisualRows = (value: string) => {
516
521
  const columns = textareaColumns();
517
522
  return Math.max(1, value.split('\n').reduce((rows, line) => rows + Math.max(1, Math.ceil(line.length / columns)), 0));
518
523
  };
519
524
  const measuredVisualRows = (value: string) => {
520
- const virtualRows = Number(textareaRef?.virtualLineCount ?? 0);
521
- const logicalRows = Number(textareaRef?.lineCount ?? 0);
522
- const scrollRows = Number(textareaRef?.scrollHeight ?? 0);
523
- return Math.max(
524
- estimatedVisualRows(value),
525
- Number.isFinite(virtualRows) ? virtualRows : 0,
526
- Number.isFinite(logicalRows) ? logicalRows : 0,
527
- Number.isFinite(scrollRows) ? scrollRows : 0,
528
- );
525
+ try {
526
+ const virtualRows = Number(textareaRef?.virtualLineCount ?? 0);
527
+ const logicalRows = Number(textareaRef?.lineCount ?? 0);
528
+ const scrollRows = Number(textareaRef?.scrollHeight ?? 0);
529
+ return Math.max(
530
+ estimatedVisualRows(value),
531
+ Number.isFinite(virtualRows) ? virtualRows : 0,
532
+ Number.isFinite(logicalRows) ? logicalRows : 0,
533
+ Number.isFinite(scrollRows) ? scrollRows : 0,
534
+ );
535
+ } catch {
536
+ return estimatedVisualRows(value);
537
+ }
529
538
  };
530
539
  const applyHeight = (rows: number) => {
531
540
  const height = rows + 2;
@@ -533,11 +542,25 @@ export function ChatInput(props: {
533
542
  if (containerRef) containerRef.height = height;
534
543
  props.onHeightChange(height);
535
544
  };
536
- const updateRows = (value = String(textareaRef?.plainText ?? props.value ?? '')) => {
537
- const rows = Math.min(maxRows, Math.max(minRows, measuredVisualRows(value)));
545
+ const safePlainText = () => {
546
+ try {
547
+ return String(textareaRef?.plainText ?? props.value ?? '');
548
+ } catch {
549
+ return String(props.value ?? '');
550
+ }
551
+ };
552
+ const updateRows = (value?: string) => {
553
+ if (disposed) return;
554
+ const text = value ?? safePlainText();
555
+ const rows = Math.min(maxRows, Math.max(minRows, measuredVisualRows(text)));
538
556
  setTextareaRows(rows);
539
557
  applyHeight(rows);
540
558
  };
559
+ const queueUpdateRows = (value?: string) => {
560
+ queueMicrotask(() => {
561
+ if (!disposed) updateRows(value);
562
+ });
563
+ };
541
564
  const syncTextareaValue = (value: string) => {
542
565
  const current = String(textareaRef?.plainText ?? '');
543
566
  if (!textareaRef || current === value) return;
@@ -549,16 +572,16 @@ export function ChatInput(props: {
549
572
  // Some renderable states reject cursor movement while layout is settling.
550
573
  }
551
574
  updateRows(value);
552
- queueMicrotask(() => updateRows(value));
575
+ queueUpdateRows(value);
553
576
  };
554
577
  const handleContentChange = () => {
555
- const value = String(textareaRef?.plainText ?? '');
578
+ const value = safePlainText();
556
579
  props.onInput(value);
557
580
  updateRows(value);
558
- queueMicrotask(() => updateRows(value));
581
+ queueUpdateRows(value);
559
582
  };
560
583
  const submitCurrentValue = () => {
561
- props.onSubmit(String(textareaRef?.plainText ?? props.value ?? ''));
584
+ props.onSubmit(safePlainText());
562
585
  };
563
586
 
564
587
  createEffect(() => {
@@ -573,7 +596,10 @@ export function ChatInput(props: {
573
596
  props.width;
574
597
  props.prompt;
575
598
  props.spinnerFrame;
576
- queueMicrotask(() => updateRows());
599
+ queueUpdateRows();
600
+ });
601
+ onCleanup(() => {
602
+ disposed = true;
577
603
  });
578
604
 
579
605
  return (
@@ -590,7 +616,7 @@ export function ChatInput(props: {
590
616
  <text height={1} fg={props.busy ? '#FBBF24' : idleColor()}>{promptText()}</text>
591
617
  <textarea
592
618
  ref={textareaRef}
593
- flexGrow={1}
619
+ width={inputColumns()}
594
620
  height={textareaRows()}
595
621
  focused={props.focused && !props.busy}
596
622
  initialValue={props.value}
@@ -599,7 +625,7 @@ export function ChatInput(props: {
599
625
  keyBindings={[
600
626
  { name: 'return', action: 'submit' },
601
627
  { name: 'kpenter', action: 'submit' },
602
- { name: 'linefeed', action: 'newline' },
628
+ { name: 'linefeed', action: 'submit' },
603
629
  { name: 'return', shift: true, action: 'newline' },
604
630
  { name: 'kpenter', shift: true, action: 'newline' },
605
631
  { name: 'linefeed', shift: true, action: 'newline' },
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { Index, Show } from 'solid-js';
2
+ import { createMemo, Index, Show } from 'solid-js';
3
3
 
4
4
  type PlanStep = { step: number; description: string; status: string };
5
5
  type QueueItem = {
@@ -12,6 +12,7 @@ type QueueItem = {
12
12
  reason?: string;
13
13
  };
14
14
  type QueueInfo = { active: number; current: number; frozen: number };
15
+ type LogLineParts = { time: string | null; message: string };
15
16
 
16
17
  const ACTIVITY_SLOTS = Array.from({ length: 6 }, (_, index) => index);
17
18
  const LOG_SLOTS = Array.from({ length: 24 }, (_, index) => index);
@@ -51,6 +52,12 @@ function fit(value: string, width: number) {
51
52
  return value.slice(0, max - 1) + '…';
52
53
  }
53
54
 
55
+ function logLineParts(line: string): LogLineParts {
56
+ const match = line.match(/^((?:\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AP]M)?|\d{4}-\d{2}-\d{2}[T ][0-9:.]+Z?))\s+(.+)$/i);
57
+ if (!match) return { time: null, message: line };
58
+ return { time: match[1], message: match[2] };
59
+ }
60
+
54
61
  function activityColor(status: string) {
55
62
  const value = String(status ?? '').toLowerCase();
56
63
  if (['done', 'complete', 'completed', 'success'].includes(value)) return '#8BD5CA';
@@ -190,7 +197,23 @@ export function LogPanel(props: { logs: string[]; width: number }) {
190
197
  <text width={lineWidth()} fg="#D6DEE8" content="Logs / Trace" />
191
198
  <box flexGrow={1} flexDirection="column" overflow="hidden">
192
199
  <Index each={LOG_SLOTS}>
193
- {(slot) => <text width={lineWidth()} fg="#AAB7C4" content={logLineAt(slot())} />}
200
+ {(slot) => {
201
+ const line = () => logLineAt(slot());
202
+ const parts = createMemo(() => logLineParts(line()));
203
+ const timeWidth = () => parts().time ? Math.min(parts().time!.length + 1, lineWidth()) : 0;
204
+ const messageWidth = () => Math.max(1, lineWidth() - timeWidth());
205
+ return (
206
+ <Show
207
+ when={parts().time}
208
+ fallback={<text width={lineWidth()} fg="#AAB7C4" content={line()} />}
209
+ >
210
+ <box height={1} flexDirection="row" overflow="hidden">
211
+ <text width={timeWidth()} fg="#89B4FA" content={fit(`${parts().time} `, timeWidth())} />
212
+ <text width={messageWidth()} fg="#AAB7C4" content={fit(parts().message, messageWidth())} />
213
+ </box>
214
+ </Show>
215
+ );
216
+ }}
194
217
  </Index>
195
218
  </box>
196
219
  </box>