@dotdrelle/wiki-manager 0.6.17

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,291 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { Index, Show } from 'solid-js';
3
+
4
+ type PlanStep = { step: number; description: string; status: string };
5
+ type QueueItem = {
6
+ id: string;
7
+ workspace?: string | null;
8
+ status: string;
9
+ args?: Record<string, any>;
10
+ jobId?: string;
11
+ error?: string;
12
+ reason?: string;
13
+ };
14
+ type QueueInfo = { active: number; current: number; frozen: number };
15
+
16
+ const ACTIVITY_SLOTS = Array.from({ length: 6 }, (_, index) => index);
17
+ const LOG_SLOTS = Array.from({ length: 24 }, (_, index) => index);
18
+
19
+ function wrapLine(value: string, width: number) {
20
+ const max = Math.max(8, width);
21
+ const text = String(value);
22
+ if (text.length <= max) return [text];
23
+ const lines = [];
24
+ let rest = text;
25
+ while (rest.length > max) {
26
+ const slice = rest.slice(0, max + 1);
27
+ const spaceAt = Math.max(slice.lastIndexOf(' '), slice.lastIndexOf('\t'));
28
+ const slashAt = slice.lastIndexOf('/');
29
+ const threshold = Math.floor(max * 0.45);
30
+ let index: number;
31
+ if (spaceAt > threshold && (slashAt < 0 || spaceAt <= slashAt)) {
32
+ index = spaceAt;
33
+ } else if (slashAt > threshold) {
34
+ index = slashAt + 1;
35
+ } else if (spaceAt > threshold) {
36
+ index = spaceAt;
37
+ } else {
38
+ index = max;
39
+ }
40
+ lines.push(rest.slice(0, index).trimEnd());
41
+ rest = rest.slice(index).trimStart();
42
+ }
43
+ if (rest) lines.push(rest);
44
+ return lines;
45
+ }
46
+
47
+ function fit(value: string, width: number) {
48
+ const max = Math.max(1, width);
49
+ if (value.length <= max) return value;
50
+ if (max <= 1) return '…';
51
+ return value.slice(0, max - 1) + '…';
52
+ }
53
+
54
+ function activityColor(status: string) {
55
+ const value = String(status ?? '').toLowerCase();
56
+ if (['done', 'complete', 'completed', 'success'].includes(value)) return '#8BD5CA';
57
+ if (['failed', 'error', 'cancelled', 'canceled'].includes(value)) return '#F38BA8';
58
+ if (['running', 'queued', 'starting', 'cancelling'].includes(value)) return '#89B4FA';
59
+ return '#AAB7C4';
60
+ }
61
+
62
+ function queueColor(status: string) {
63
+ const value = String(status ?? '').toLowerCase();
64
+ if (value === 'waiting') return '#FBBF24';
65
+ return activityColor(status);
66
+ }
67
+
68
+ function activityLine(activity: any) {
69
+ const detail = activity?.progress?.detail ?? null;
70
+ const phase = activity?.progress?.step ?? activity?.progress?.phase ?? activity?.progress?.currentStep ?? null;
71
+ return [activity?.status ?? 'unknown', detail ?? phase].filter(Boolean).join(' · ');
72
+ }
73
+
74
+ function activityPercentBadge(activity: any): { text: string; bg: string; fg: string } | null {
75
+ const val = Number(activity?.progress?.percent);
76
+ if (!Number.isFinite(val)) return null;
77
+ const pct = Math.round(val);
78
+ if (pct >= 100) return { text: ` 100% `, bg: '#15803d', fg: '#dcfce7' };
79
+ return { text: ` ${pct}% `, bg: '#1e3a5f', fg: '#93c5fd' };
80
+ }
81
+
82
+ function updatedLine(activity: any) {
83
+ const source = activity?.source ?? 'mcp';
84
+ const id = activity?.id ? `#${activity.id}` : null;
85
+ if (!activity?.updatedAt) return [source, id].filter(Boolean).join(' ');
86
+ const age = Math.max(0, Math.round((Date.now() - Date.parse(activity.updatedAt)) / 1000));
87
+ return [source, id, `${age}s ago`].filter(Boolean).join(' · ');
88
+ }
89
+
90
+ function planStepColor(step: PlanStep, firstPendingStep: number | null) {
91
+ if (step.status === 'done') return '#8BD5CA';
92
+ if (step.status === 'failed') return '#F38BA8';
93
+ if (step.status === 'running') return '#89B4FA';
94
+ if (step.step === firstPendingStep) return '#89B4FA';
95
+ return '#7F8C8D';
96
+ }
97
+
98
+ function activityJobName(activity: any) {
99
+ if (!activity) return '';
100
+ const id = activity.id ?? activity.jobId ?? activity.job_id;
101
+ if (id) return `Job ${id}`;
102
+ return activity.label
103
+ ?? [activity.source, activity.kind, activity.id ? `#${activity.id}` : null].filter(Boolean).join(' ')
104
+ ?? '';
105
+ }
106
+
107
+ function queueSummary(item: QueueItem) {
108
+ const args = item.args ?? {};
109
+ const parts = [
110
+ args.type ?? 'production',
111
+ Array.isArray(args.steps) && args.steps.length ? args.steps.join('+') : null,
112
+ Array.isArray(args.templates) && args.templates.length ? `tpl:${args.templates.length}` : null,
113
+ Array.isArray(args.deliverables) && args.deliverables.length ? `del:${args.deliverables.length}` : null,
114
+ ].filter(Boolean);
115
+ return parts.join(' ');
116
+ }
117
+
118
+ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: string }) {
119
+ const lineWidth = () => Math.max(8, props.width - 2);
120
+ const firstPending = () => props.plan.find((s) => s.status === 'pending')?.step ?? null;
121
+ const icon = (status: string) =>
122
+ status === 'done' ? '[✓]' : status === 'failed' ? '[✗]' : status === 'running' ? '[…]' : '[ ]';
123
+ const title = () => props.jobName ? `Plan : ${props.jobName}` : 'Plan';
124
+ return (
125
+ <box flexShrink={0} flexDirection="column" padding={1}>
126
+ <text width={lineWidth()} fg="#D6DEE8" content={fit(title(), lineWidth())} />
127
+ <Index each={props.plan}>
128
+ {(step) => (
129
+ <text
130
+ width={lineWidth()}
131
+ fg={planStepColor(step(), firstPending())}
132
+ content={fit(`${icon(step().status)} ${step().step}. ${step().description}`, lineWidth())}
133
+ />
134
+ )}
135
+ </Index>
136
+ </box>
137
+ );
138
+ }
139
+
140
+ export function ActivityPanel(props: { activities: any[]; width: number }) {
141
+ const lineWidth = () => Math.max(8, props.width - 2);
142
+ const visible = () => props.activities.slice(-6).reverse();
143
+ const activityAt = (index: number) => visible()[index] ?? null;
144
+ return (
145
+ <box flexShrink={0} flexDirection="column" padding={1}>
146
+ <text width={lineWidth()} fg="#D6DEE8" content="Activity" />
147
+ <Show when={visible().length > 0} fallback={<text width={lineWidth()} fg="#7F8C8D" content="no active jobs" />}>
148
+ <Index each={ACTIVITY_SLOTS}>
149
+ {(slot) => {
150
+ const activity = () => activityAt(slot());
151
+ const label = () => {
152
+ const item = activity();
153
+ if (!item) return '';
154
+ return fit(item.progress?.label ?? item.label ?? `${item.source ?? 'mcp'} ${item.kind ?? 'job'}`, lineWidth());
155
+ };
156
+ const badge = () => activityPercentBadge(activity());
157
+ const badgeLen = () => badge()?.text.length ?? 0;
158
+ return (
159
+ <box flexDirection="column" marginTop={slot() === 0 ? 1 : 0}>
160
+ <text width={lineWidth()} fg={activityColor(activity()?.status)} content={label()} />
161
+ <box height={1} flexDirection="row">
162
+ <text
163
+ width={lineWidth() - badgeLen()}
164
+ fg="#AAB7C4"
165
+ content={activity() ? fit(activityLine(activity()), lineWidth() - badgeLen()) : ''}
166
+ />
167
+ <text
168
+ width={badgeLen()}
169
+ bg={badge()?.bg ?? '#111827'}
170
+ fg={badge()?.fg ?? '#111827'}
171
+ content={badge()?.text ?? ''}
172
+ />
173
+ </box>
174
+ <text width={lineWidth()} fg="#7F8C8D" content={activity() ? fit(updatedLine(activity()), lineWidth()) : ''} />
175
+ </box>
176
+ );
177
+ }}
178
+ </Index>
179
+ </Show>
180
+ </box>
181
+ );
182
+ }
183
+
184
+ export function LogPanel(props: { logs: string[]; width: number }) {
185
+ const lineWidth = () => Math.max(8, props.width - 2);
186
+ const visibleLines = () => props.logs.slice(-12).flatMap((line) => wrapLine(line, lineWidth())).slice(-24);
187
+ const logLineAt = (index: number) => visibleLines()[index] ?? '';
188
+ return (
189
+ <box flexGrow={1} flexDirection="column" padding={1}>
190
+ <text width={lineWidth()} fg="#D6DEE8" content="Logs / Trace" />
191
+ <box flexGrow={1} flexDirection="column" overflow="hidden">
192
+ <Index each={LOG_SLOTS}>
193
+ {(slot) => <text width={lineWidth()} fg="#AAB7C4" content={logLineAt(slot())} />}
194
+ </Index>
195
+ </box>
196
+ </box>
197
+ );
198
+ }
199
+
200
+ export function QueuePanel(props: { items: QueueItem[]; info: QueueInfo; width: number }) {
201
+ const lineWidth = () => Math.max(8, props.width - 2);
202
+ const visible = () => props.items.slice(-6).reverse();
203
+ return (
204
+ <box flexShrink={0} flexDirection="column" padding={1}>
205
+ <text width={lineWidth()} fg="#D6DEE8" content="Queue" />
206
+ <Show when={props.info.frozen > 0}>
207
+ <text width={lineWidth()} fg="#FBBF24" content={fit(`Queue frozen: ${props.info.frozen} item(s) in another workspace`, lineWidth())} />
208
+ </Show>
209
+ <Show when={visible().length > 0} fallback={<text width={lineWidth()} fg="#7F8C8D" content="no queued jobs" />}>
210
+ <Index each={ACTIVITY_SLOTS}>
211
+ {(slot) => {
212
+ const item = () => visible()[slot()] ?? null;
213
+ return (
214
+ <box flexDirection="column" marginTop={slot() === 0 ? 1 : 0}>
215
+ <text
216
+ width={lineWidth()}
217
+ fg={queueColor(item()?.status)}
218
+ content={item() ? fit(`${item()!.id} ${item()!.status} ${queueSummary(item()!)}`, lineWidth()) : ''}
219
+ />
220
+ <text
221
+ width={lineWidth()}
222
+ fg="#AAB7C4"
223
+ content={item() ? fit([item()!.workspace, item()!.jobId ? `job ${item()!.jobId}` : item()!.reason].filter(Boolean).join(' · '), lineWidth()) : ''}
224
+ />
225
+ <text
226
+ width={lineWidth()}
227
+ fg="#7F8C8D"
228
+ content={item()?.error ? fit(item()!.error!, lineWidth()) : ''}
229
+ />
230
+ </box>
231
+ );
232
+ }}
233
+ </Index>
234
+ </Show>
235
+ </box>
236
+ );
237
+ }
238
+
239
+ function TabHeader(props: { active: 'plan' | 'queue'; queueCount: number; width: number; onTabClick: (tab: 'plan' | 'queue') => void }) {
240
+ const lineWidth = () => Math.max(8, props.width - 2);
241
+ const planActive = () => props.active === 'plan';
242
+ return (
243
+ <box height={1} flexDirection="row" paddingX={1}>
244
+ <text
245
+ fg={planActive() ? '#0B1020' : '#D6DEE8'}
246
+ bg={planActive() ? '#89B4FA' : undefined}
247
+ content=" Plan "
248
+ onMouseUp={() => props.onTabClick('plan')}
249
+ />
250
+ <text fg="#4B5563" content=" " />
251
+ <text
252
+ fg={!planActive() ? '#0B1020' : '#D6DEE8'}
253
+ bg={!planActive() ? '#FBBF24' : undefined}
254
+ content={` Queue (${props.queueCount}) `}
255
+ onMouseUp={() => props.onTabClick('queue')}
256
+ />
257
+ <text fg="#7F8C8D" content={fit(' Ctrl+Q', Math.max(0, lineWidth() - 24))} />
258
+ </box>
259
+ );
260
+ }
261
+
262
+ export function RightPane(props: {
263
+ width: number;
264
+ activities: any[];
265
+ logs: string[];
266
+ plan: PlanStep[] | null;
267
+ queueItems: QueueItem[];
268
+ queueInfo: QueueInfo;
269
+ activeTab: 'plan' | 'queue';
270
+ onTabClick: (tab: 'plan' | 'queue') => void;
271
+ }) {
272
+ const planJobName = () => activityJobName(
273
+ [...props.activities].reverse().find((activity) => !activity.terminal) ?? props.activities.at(-1),
274
+ );
275
+ return (
276
+ <box width={props.width} height="100%" flexDirection="column" gap={1} padding={1} overflow="hidden">
277
+ <TabHeader active={props.activeTab} queueCount={props.queueInfo.active} width={props.width} onTabClick={props.onTabClick} />
278
+ <Show when={props.activeTab === 'queue'} fallback={(
279
+ <>
280
+ <Show when={props.plan && props.plan.length > 0}>
281
+ <PlanPanel width={props.width} plan={props.plan!} jobName={planJobName()} />
282
+ </Show>
283
+ <ActivityPanel width={props.width} activities={props.activities} />
284
+ </>
285
+ )}>
286
+ <QueuePanel width={props.width} items={props.queueItems} info={props.queueInfo} />
287
+ </Show>
288
+ <LogPanel width={props.width} logs={props.logs} />
289
+ </box>
290
+ );
291
+ }
@@ -0,0 +1,39 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { For, Show } from 'solid-js';
3
+
4
+ export function SlashDialog(props: { context: any }) {
5
+ return (
6
+ <Show when={props.context}>
7
+ {(context) => (
8
+ <box
9
+ position="absolute"
10
+ left={1}
11
+ bottom={4}
12
+ width="64%"
13
+ height={Math.min(16, context().items.length + 5)}
14
+ zIndex={10}
15
+ border
16
+ borderStyle="rounded"
17
+ borderColor="#5DADE2"
18
+ backgroundColor="#111318"
19
+ padding={1}
20
+ flexDirection="column"
21
+ >
22
+ <text fg="#7F8C8D">Completions</text>
23
+ <For each={context().items}>
24
+ {(item: any, index) => (
25
+ <text
26
+ height={1}
27
+ fg={index() === context().selected ? '#111318' : '#D6DEE8'}
28
+ bg={index() === context().selected ? '#8BD5CA' : '#111318'}
29
+ >
30
+ {item.value.padEnd(16, ' ')} {item.description}
31
+ </text>
32
+ )}
33
+ </For>
34
+ <text fg="#7F8C8D">[up/down navigate Tab complete Enter confirm Esc clear]</text>
35
+ </box>
36
+ )}
37
+ </Show>
38
+ );
39
+ }
@@ -0,0 +1,69 @@
1
+ export function stripAnsi(value: string) {
2
+ return String(value).replace(/\u001b\[[0-9;]*m/g, '');
3
+ }
4
+
5
+ export function stripDsmlArtifacts(value: string) {
6
+ return String(value ?? '')
7
+ .replace(/<\s*[||]{2}\s*DSML\s*[||]{2}[^>\r\n]*(?:>|$)/gi, '')
8
+ .replace(/^[^\S\r\n]*.*[||]{2}\s*DSML\s*[||]{2}.*(?:\r?\n|$)/gim, '')
9
+ .replace(/\n{3,}/g, '\n\n');
10
+ }
11
+
12
+ export function renderPlainMarkdown(value: string) {
13
+ return stripAnsi(stripDsmlArtifacts(value))
14
+ .replace(/\r\n/g, '\n')
15
+ .replace(/^#{1,6}\s+/gm, '')
16
+ .replace(/^\s*[-*_]{3,}\s*$/gm, '')
17
+ // table: remove separator rows (| :--- | --- |), then convert data rows
18
+ .replace(/^\|[\s|:-]+\|$/gm, '')
19
+ .replace(/^\|(.+)\|$/gm, (_match, inner) => {
20
+ const cells = inner.split('|').map((c: string) => c.trim()).filter(Boolean);
21
+ if (cells.length === 0) return '';
22
+ if (cells.length === 2) return `${cells[0]}: ${cells[1]}`;
23
+ return cells.join(' ');
24
+ })
25
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
26
+ .replace(/__([^_]+)__/g, '$1')
27
+ .replace(/`([^`]+)`/g, '$1')
28
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
29
+ // collapse consecutive blank lines left by removed elements
30
+ .replace(/\n{3,}/g, '\n\n')
31
+ .trimEnd();
32
+ }
33
+
34
+ export function colorForRenderedLine(line: string, role: string) {
35
+ const text = String(line ?? '').trim();
36
+ if (!text) return '#D6DEE8';
37
+ if (role === 'user') return '#5DADE2';
38
+ if (/^[●◐○]/.test(text)) {
39
+ if (/\bconnected\b/i.test(text)) return '#8BD5CA';
40
+ if (/\bconfigured\b/i.test(text)) return '#9CA3AF';
41
+ return '#7F8C8D';
42
+ }
43
+ return '#D6DEE8';
44
+ }
45
+
46
+ export function helpCommandParts(line: string) {
47
+ const trimmed = String(line ?? '').trim();
48
+ if (!/^(\/[a-z0-9_-]+|Ctrl\+|-[a-z-]|--[a-z-])/i.test(trimmed)) return null;
49
+ const parts = trimmed.split(/\s{2,}/).filter(Boolean);
50
+ if (parts.length < 2) return null;
51
+ if (!/^(\/[a-z0-9_-]+|Ctrl\+|-[a-z-]|--[a-z-])/i.test(parts[0])) return null;
52
+ return parts;
53
+ }
54
+
55
+ export function keyValueParts(line: string) {
56
+ // Matches KEY=value, Key: value, or "Multi Word Key: value" (up to 4 words, max 40 chars before separator)
57
+ // Optionally preceded by a list marker (- or *)
58
+ const match = String(line ?? '').match(/^(\s*(?:[-*]\s+)?)([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ0-9_.() -]{0,38}?(?:=|:[ \t]+))(.*)$/);
59
+ if (!match) return null;
60
+ // Reject if key part looks like prose (more than 4 space-separated words)
61
+ const keyPart = match[2].replace(/[=:].*/, '').trim();
62
+ if (keyPart.split(/\s+/).length > 4) return null;
63
+ return { prefix: match[1], key: match[2], value: match[3] ?? '' };
64
+ }
65
+
66
+ export function truncate(value: string, max = 96) {
67
+ const text = stripAnsi(String(value ?? '')).replace(/\s+/g, ' ').trim();
68
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text;
69
+ }