@dotdrelle/wiki-manager 0.15.64 → 0.15.70
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/.env.example +10 -3
- package/README.md +54 -0
- package/agent-runtimes.example.json +68 -0
- package/agents.docker-compose.yml +35 -1
- package/docker-compose.yml +3 -3
- package/package.json +3 -2
- package/src/agent/graph.js +15 -14
- package/src/agent/graph.test.js +1 -1
- package/src/agent/skillRecursion.test.js +13 -12
- package/src/cli/wiki-manager.js +124 -36
- package/src/commands/slash.js +48 -13
- package/src/contracts/schemas.js +67 -0
- package/src/core/activity.js +5 -0
- package/src/core/agentEvents.js +18 -1
- package/src/core/agentLoop.js +3 -3
- package/src/core/agentLoop.test.js +1 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +8 -40
- package/src/core/env.js +14 -0
- package/src/core/env.test.js +19 -0
- package/src/core/googleGrants.test.js +1 -1
- package/src/core/mcp.js +1 -1
- package/src/core/runtimeEventAdapter.js +81 -0
- package/src/core/runtimeEventAdapter.test.js +61 -0
- package/src/core/skillChainView.test.js +2 -2
- package/src/core/skillCompiler.test.js +1 -1
- package/src/core/skillInvocation.js +13 -8
- package/src/core/startupCheck.js +58 -0
- package/src/core/startupCheck.test.js +29 -1
- package/src/orchestrator/agentRegistry.js +1 -22
- package/src/orchestrator/assignmentManager.js +16 -4
- package/src/orchestrator/capabilityRegistry.js +8 -1
- package/src/orchestrator/dispatcher.js +361 -2
- package/src/orchestrator/dispatcher.test.js +112 -1
- package/src/orchestrator/objectiveResolver.js +10 -6
- package/src/orchestrator/objectiveResolver.test.js +26 -27
- package/src/orchestrator/providers/deepAgentsProvider.js +168 -0
- package/src/orchestrator/providers/deepAgentsProvider.test.js +178 -0
- package/src/orchestrator/providers/dispatcherExternalRuntime.test.js +409 -0
- package/src/orchestrator/providers/fakeRuntimeProvider.js +164 -0
- package/src/orchestrator/providers/fakeRuntimeProvider.test.js +201 -0
- package/src/orchestrator/providers/runtimeProvider.js +101 -0
- package/src/orchestrator/providers/runtimeProviders.js +325 -0
- package/src/orchestrator/providers/runtimeProviders.test.js +361 -0
- package/src/orchestrator/resultAggregator.js +35 -2
- package/src/orchestrator/resultAggregator.test.js +62 -0
- package/src/runtime/recoveryManager.js +70 -5
- package/src/runtime/runner.js +6 -6
- package/src/runtime/runner.test.js +1 -1
- package/src/runtime/skillChain.e2e.test.js +2 -2
- package/src/runtime/supervisor.js +5 -10
- package/src/shell/RightPane.tsx +25 -9
- package/src/shell/StartupScreen.tsx +44 -7
- package/src/shell/repl.js +12 -12
- package/src/shell/repl.test.js +18 -5
- package/src/shell/tui.tsx +6 -6
- package/src/shell/useAgent.ts +1 -1
- package/src/shell/useSession.ts +1 -1
- package/wiki-workspace +19 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { openSync, readSync, closeSync, fstatSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute, join, normalize, resolve } from 'node:path';
|
|
3
|
-
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
3
|
+
import { createAgentEvent, dispatchAgentEvent, dispatchRuntimeLog } from '../core/agentEvents.js';
|
|
4
4
|
import { extractActivity, mergePolledActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
5
5
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
6
|
-
import { normalizeRuntimeLog } from '../core/runtimeLog.js';
|
|
7
6
|
import { startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
|
|
8
7
|
import { createAgentRegistry } from '../orchestrator/agentRegistry.js';
|
|
8
|
+
import { discoverRuntimeProvidersOnce } from '../orchestrator/providers/runtimeProviders.js';
|
|
9
9
|
|
|
10
10
|
export function startActivitySupervisor(session, {
|
|
11
11
|
intervalMs = 1000,
|
|
@@ -52,11 +52,13 @@ export function startActivitySupervisor(session, {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
void discoverAgentsOnce(session, { registry, signal: runSignal });
|
|
55
|
+
void discoverRuntimeProvidersOnce(session, { signal: runSignal });
|
|
55
56
|
}, agentRegistryIntervalMs)
|
|
56
57
|
: null;
|
|
57
58
|
|
|
58
59
|
void pollActivitiesOnce(session, { pollBusy, callTool, signal: runSignal });
|
|
59
60
|
void discoverAgentsOnce(session, { registry, signal: runSignal });
|
|
61
|
+
void discoverRuntimeProvidersOnce(session, { signal: runSignal });
|
|
60
62
|
|
|
61
63
|
return {
|
|
62
64
|
pollBusy,
|
|
@@ -199,14 +201,7 @@ export async function pollActivitiesOnce(session, {
|
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
export function emitRuntimeLog(session, message) {
|
|
202
|
-
|
|
203
|
-
dispatchAgentEvent(session, createAgentEvent('runtime_log', {
|
|
204
|
-
origin: 'runtime',
|
|
205
|
-
runId: payload.runId ?? null,
|
|
206
|
-
taskId: payload.taskId ?? null,
|
|
207
|
-
workspace: payload.workspaceId ?? null,
|
|
208
|
-
payload,
|
|
209
|
-
}));
|
|
204
|
+
dispatchRuntimeLog(session, message);
|
|
210
205
|
}
|
|
211
206
|
|
|
212
207
|
function registryIntervalFromEnv() {
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -263,16 +263,27 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
263
263
|
|
|
264
264
|
export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
265
265
|
const lineWidth = () => Math.max(8, props.width - 2);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
// session.activities is never pruned within a run (see core/agentEvents.js),
|
|
267
|
+
// so a long batch job's activity count is unbounded — render only the most
|
|
268
|
+
// recent slice, memoized like LogPanel's allLines, instead of copying and
|
|
269
|
+
// re-wrapping the entire lifetime history on every reactive tick.
|
|
270
|
+
const visible = createMemo(() => props.activities.slice(-200).reverse());
|
|
269
271
|
return (
|
|
270
|
-
<box
|
|
272
|
+
<box flexGrow={1} flexDirection="column" paddingX={1} backgroundColor="#111318">
|
|
271
273
|
<text width={lineWidth()} fg="#D6DEE8" content="Activity" />
|
|
272
274
|
<Show when={visible().length > 0} fallback={<text width={lineWidth()} fg="#7F8C8D" content="no active jobs" />}>
|
|
273
|
-
<
|
|
274
|
-
{
|
|
275
|
-
|
|
275
|
+
<scrollbox
|
|
276
|
+
flexGrow={1}
|
|
277
|
+
flexShrink={1}
|
|
278
|
+
focusable={false}
|
|
279
|
+
scrollY={true}
|
|
280
|
+
scrollX={false}
|
|
281
|
+
stickyStart="top"
|
|
282
|
+
viewportCulling={true}
|
|
283
|
+
verticalScrollbarOptions={{ visible: visible().length > 3 }}
|
|
284
|
+
>
|
|
285
|
+
<Index each={visible()}>
|
|
286
|
+
{(activity) => {
|
|
276
287
|
// Wrap instead of hard-truncating: a 40-column pane cut labels to
|
|
277
288
|
// "Appliquer la config recommandée (doct…" and hid the one thing
|
|
278
289
|
// that mattered. Labels get up to 2 lines, the status/error line
|
|
@@ -320,6 +331,7 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
|
320
331
|
);
|
|
321
332
|
}}
|
|
322
333
|
</Index>
|
|
334
|
+
</scrollbox>
|
|
323
335
|
</Show>
|
|
324
336
|
</box>
|
|
325
337
|
);
|
|
@@ -332,6 +344,10 @@ type LogSegment = { text: string; fg: string };
|
|
|
332
344
|
// Continuation lines of a wrapped entry are indented and dimmed so each
|
|
333
345
|
// entry reads as one visual block instead of an undifferentiated wall.
|
|
334
346
|
function logMessageColor(message: string): string {
|
|
347
|
+
// A doctor summary with ZERO errors is a warning by construction
|
|
348
|
+
// ("⚠ 0 error(s), 2 warning(s)") — amber, never red, even though it
|
|
349
|
+
// mentions the word "error".
|
|
350
|
+
if (/(?<!\d)0 error\(s\)/i.test(message)) return '#FBBF24';
|
|
335
351
|
if (/\b(?:trace:\s*)?WARN\b/i.test(message)) return '#FBBF24';
|
|
336
352
|
if (/\b(error|failed|exception|unavailable|introuvable|HTTP 4\d\d|HTTP 5\d\d)\b/i.test(message)) return '#F38BA8';
|
|
337
353
|
if (/\b(warn|warning|avertissement|fallback|retry|expired|stale)\b/i.test(message)) return '#FBBF24';
|
|
@@ -536,8 +552,8 @@ export function RightPane(props: {
|
|
|
536
552
|
<TabHeader active={props.activeTab} queueCount={props.queueInfo.active} onTabClick={props.onTabClick} />
|
|
537
553
|
<Show when={props.pendingApprovals.length > 0}>
|
|
538
554
|
<box height={2} flexDirection="column" border={['left']} borderStyle="heavy" borderColor="#FBBF24" paddingX={1}>
|
|
539
|
-
<text fg="#FBBF24" content={`${props.pendingApprovals.length}
|
|
540
|
-
<text fg="#0B1020" bg="#FBBF24" content="
|
|
555
|
+
<text fg="#FBBF24" content={`${props.pendingApprovals.length} approval(s) required`} />
|
|
556
|
+
<text fg="#0B1020" bg="#FBBF24" content=" Approve run " onMouseUp={props.onApprove} />
|
|
541
557
|
</box>
|
|
542
558
|
</Show>
|
|
543
559
|
<Show when={props.activeTab === 'queue'} fallback={(
|
|
@@ -238,9 +238,36 @@ export function StartupScreen(props: {
|
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
const preflightChecks = createMemo(() => props.preflight?.checks ?? []);
|
|
241
|
+
const checkByKind = createMemo(() => {
|
|
242
|
+
const map = new Map<string, any>();
|
|
243
|
+
for (const check of preflightChecks()) map.set(check.kind, check);
|
|
244
|
+
return map;
|
|
245
|
+
});
|
|
246
|
+
// Docker and Internet leave the check list: they are ambient state, shown in
|
|
247
|
+
// the header (top right), not among the startup rows.
|
|
248
|
+
const dockerCheck = createMemo(() => checkByKind().get('docker'));
|
|
249
|
+
const internetCheck = createMemo(() => checkByKind().get('internet'));
|
|
250
|
+
const listChecks = createMemo(() => {
|
|
251
|
+
const order = ['workspace', 'runtime', 'agentic', 'agents', 'containers', 'mcp'];
|
|
252
|
+
const seen = new Set<string>(['docker', 'internet']);
|
|
253
|
+
const ordered: Array<{ kind: string; ok?: boolean; skipped?: boolean; pending?: boolean; detail?: string }> = [];
|
|
254
|
+
for (const kind of order) {
|
|
255
|
+
const check = checkByKind().get(kind);
|
|
256
|
+
if (!check) continue;
|
|
257
|
+
// An optional engine is not part of the startup story: when nothing is
|
|
258
|
+
// enabled, its row disappears instead of showing a misleading green ✓.
|
|
259
|
+
if (kind === 'agentic' && check.skipped) continue;
|
|
260
|
+
ordered.push(check);
|
|
261
|
+
seen.add(kind);
|
|
262
|
+
}
|
|
263
|
+
for (const check of preflightChecks()) {
|
|
264
|
+
if (!seen.has(check.kind)) ordered.push(check);
|
|
265
|
+
}
|
|
266
|
+
return ordered;
|
|
267
|
+
});
|
|
241
268
|
const checkLabel = (kind: string) => ({
|
|
242
269
|
docker: 'Docker', internet: 'Internet', agents: 'Agents', workspace: 'Workspaces',
|
|
243
|
-
containers: 'Containers', mcp: 'MCP', runtime: 'Runtime',
|
|
270
|
+
containers: 'Containers', mcp: 'MCP', agentic: 'Agentic runtime', runtime: 'Runtime',
|
|
244
271
|
} as Record<string, string>)[kind] ?? kind;
|
|
245
272
|
|
|
246
273
|
const subtitle = createMemo(() => {
|
|
@@ -281,10 +308,20 @@ export function StartupScreen(props: {
|
|
|
281
308
|
overflow="hidden"
|
|
282
309
|
>
|
|
283
310
|
<box height={1} flexDirection="row">
|
|
284
|
-
<text fg="#8BD5CA" content={fit(`DONNA v${props.version}`, Math.floor(innerWidth() * 0.
|
|
285
|
-
<text fg={statusColor()} content={fit(` ${props.preflightBusy ? '◐' : statusDot(props.preflight?.status === 'ready')} ${status()}`, Math.floor(innerWidth() * 0.
|
|
311
|
+
<text fg="#8BD5CA" content={fit(`DONNA v${props.version}`, Math.floor(innerWidth() * 0.4))} />
|
|
312
|
+
<text fg={statusColor()} content={fit(` ${props.preflightBusy ? '◐' : statusDot(props.preflight?.status === 'ready')} ${status()}`, Math.floor(innerWidth() * 0.32))} />
|
|
313
|
+
<box flexGrow={1} />
|
|
314
|
+
<text fg="#7F8C8D" content={dockerCheck() ? fit(`Docker — ${dockerCheck()?.detail ?? '—'}`, Math.floor(innerWidth() * 0.28)) : ''} />
|
|
315
|
+
</box>
|
|
316
|
+
<box height={1} flexDirection="row">
|
|
317
|
+
<text height={1} fg="#7F8C8D" content={fit(subtitle(), Math.floor(innerWidth() * 0.6))} />
|
|
318
|
+
<box flexGrow={1} />
|
|
319
|
+
<text
|
|
320
|
+
height={1}
|
|
321
|
+
fg={internetCheck() ? (internetCheck()?.ok ? '#8BD5CA' : '#FBBF24') : '#7F8C8D'}
|
|
322
|
+
content={internetCheck() ? fit(`Internet — ${internetCheck()?.ok ? 'OK' : 'KO'}`, Math.floor(innerWidth() * 0.28)) : ''}
|
|
323
|
+
/>
|
|
286
324
|
</box>
|
|
287
|
-
<text height={1} fg="#7F8C8D" content={fit(subtitle(), innerWidth())} />
|
|
288
325
|
<text height={1}>{''}</text>
|
|
289
326
|
<box
|
|
290
327
|
height={8}
|
|
@@ -295,7 +332,7 @@ export function StartupScreen(props: {
|
|
|
295
332
|
overflow="hidden"
|
|
296
333
|
>
|
|
297
334
|
<text fg="#d6a85f">{DONNA_LOGO}</text>
|
|
298
|
-
<text fg="#888888">Intelligent workspace</text>
|
|
335
|
+
<text fg="#888888">Intelligent Agentic workspace(s)</text>
|
|
299
336
|
</box>
|
|
300
337
|
<text height={1}>{''}</text>
|
|
301
338
|
<text height={1} fg="#7F8C8D" content={menuTitle()} />
|
|
@@ -320,8 +357,8 @@ export function StartupScreen(props: {
|
|
|
320
357
|
</For>
|
|
321
358
|
</box>
|
|
322
359
|
<text height={1}>{''}</text>
|
|
323
|
-
<box height={Math.max(3,
|
|
324
|
-
<For each={
|
|
360
|
+
<box height={Math.max(3, listChecks().length)} flexDirection="column" border={['left']} borderStyle="heavy" borderColor="#5DADE2" paddingX={1} overflow="hidden">
|
|
361
|
+
<For each={listChecks().length ? listChecks() : [
|
|
325
362
|
{ kind: 'workspace', ok: props.wikiReady, detail: props.wikiReady ? 'default profile ready' : 'init required' },
|
|
326
363
|
{ kind: 'mcp', ok: props.connectedMcpServers > 0, detail: `${props.connectedMcpServers} server(s) configured` },
|
|
327
364
|
{ kind: 'llm', ok: Boolean(props.model), detail: props.model || 'not configured' },
|
package/src/shell/repl.js
CHANGED
|
@@ -127,12 +127,12 @@ const SUBCOMMAND_COMPLETION_DESCRIPTIONS = {
|
|
|
127
127
|
export function runtimeUnavailableReason(runtime) {
|
|
128
128
|
if (runtime?.url) return null;
|
|
129
129
|
const reason = runtime?.error ?? runtime?.unavailableReason ?? runtime?.reason ?? null;
|
|
130
|
-
return reason ? String(reason) : 'runtime
|
|
130
|
+
return reason ? String(reason) : 'runtime unavailable';
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export function runtimeUnavailableAgentMessage(runtime) {
|
|
134
134
|
const reason = runtimeUnavailableReason(runtime);
|
|
135
|
-
return reason ? `⚠ Runtime
|
|
135
|
+
return reason ? `⚠ Runtime unavailable: ${reason} — /agent disabled, /chat still available` : null;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
export function runtimeStatusLine(runtime, session) {
|
|
@@ -145,7 +145,7 @@ export function runtimeStatusLine(runtime, session) {
|
|
|
145
145
|
export function recordRuntimeUnavailableAgentInput(session, line, runtime) {
|
|
146
146
|
const message = runtimeUnavailableAgentMessage(runtime);
|
|
147
147
|
conversationMessages(session).push({ role: 'user', content: line });
|
|
148
|
-
conversationMessages(session).push({ role: 'command', content: message ?? 'Runtime
|
|
148
|
+
conversationMessages(session).push({ role: 'command', content: message ?? 'Runtime unavailable.' });
|
|
149
149
|
return message;
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -1422,10 +1422,10 @@ async function runAgentTurn(input, {
|
|
|
1422
1422
|
if (donnaMessage) {
|
|
1423
1423
|
donnaMessage.content = stripDsmlArtifacts(donnaMessage.content).trimEnd();
|
|
1424
1424
|
if (!donnaMessage.content.trim()) {
|
|
1425
|
-
donnaMessage.content = formatLlmUnavailableMessage('
|
|
1425
|
+
donnaMessage.content = formatLlmUnavailableMessage('empty stream');
|
|
1426
1426
|
}
|
|
1427
1427
|
} else {
|
|
1428
|
-
messages.push({ role: 'donna', content: formatLlmUnavailableMessage('
|
|
1428
|
+
messages.push({ role: 'donna', content: formatLlmUnavailableMessage('empty stream') });
|
|
1429
1429
|
}
|
|
1430
1430
|
onUpdate?.();
|
|
1431
1431
|
return {};
|
|
@@ -1464,7 +1464,7 @@ async function runAgentTurn(input, {
|
|
|
1464
1464
|
}
|
|
1465
1465
|
donnaMessage.content = stripDsmlArtifacts(donnaMessage.content).trimEnd();
|
|
1466
1466
|
if (!donnaMessage.content.trim()) {
|
|
1467
|
-
donnaMessage.content = formatLlmUnavailableMessage('
|
|
1467
|
+
donnaMessage.content = formatLlmUnavailableMessage('empty stream');
|
|
1468
1468
|
onUpdate?.();
|
|
1469
1469
|
}
|
|
1470
1470
|
} catch (err) {
|
|
@@ -1481,9 +1481,9 @@ async function runAgentTurn(input, {
|
|
|
1481
1481
|
}
|
|
1482
1482
|
|
|
1483
1483
|
if (donnaMessage) {
|
|
1484
|
-
donnaMessage.content = formatLlmUnavailableMessage('
|
|
1484
|
+
donnaMessage.content = formatLlmUnavailableMessage('empty response');
|
|
1485
1485
|
} else {
|
|
1486
|
-
messages.push({ role: 'donna', content: formatLlmUnavailableMessage('
|
|
1486
|
+
messages.push({ role: 'donna', content: formatLlmUnavailableMessage('empty response') });
|
|
1487
1487
|
}
|
|
1488
1488
|
onUpdate?.();
|
|
1489
1489
|
return {};
|
|
@@ -1533,8 +1533,8 @@ async function runChatToolLoop({ input, session, history, donnaMessage, onUpdate
|
|
|
1533
1533
|
onTextReset,
|
|
1534
1534
|
});
|
|
1535
1535
|
donnaMessage.content = capped
|
|
1536
|
-
? '
|
|
1537
|
-
: (stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('
|
|
1536
|
+
? 'Could not finish within the chat mode iteration limit. Switch to /agent if needed.'
|
|
1537
|
+
: (stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('empty response'));
|
|
1538
1538
|
onUpdate?.();
|
|
1539
1539
|
}
|
|
1540
1540
|
|
|
@@ -1580,7 +1580,7 @@ async function runDirectChatTurn(input, { session, onUpdate, onStep }) {
|
|
|
1580
1580
|
}
|
|
1581
1581
|
donnaMessage.content = stripDsmlArtifacts(donnaMessage.content).trimEnd();
|
|
1582
1582
|
if (!donnaMessage.content.trim()) {
|
|
1583
|
-
donnaMessage.content = formatLlmUnavailableMessage('
|
|
1583
|
+
donnaMessage.content = formatLlmUnavailableMessage('empty stream');
|
|
1584
1584
|
onUpdate?.();
|
|
1585
1585
|
}
|
|
1586
1586
|
}
|
|
@@ -1639,7 +1639,7 @@ export async function runHeadlessChatTurn(session, input, { history = [], onStep
|
|
|
1639
1639
|
const clean = stripDsmlArtifacts(delta);
|
|
1640
1640
|
if (clean) { content += clean; onTextDelta?.(clean); }
|
|
1641
1641
|
}
|
|
1642
|
-
return stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('
|
|
1642
|
+
return stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('empty stream');
|
|
1643
1643
|
}
|
|
1644
1644
|
return directChatUnavailableText(session);
|
|
1645
1645
|
}
|
package/src/shell/repl.test.js
CHANGED
|
@@ -114,7 +114,7 @@ test('Activity uses only visible jobs and leaves remaining height to Flow/Trace'
|
|
|
114
114
|
source.indexOf('export function ActivityPanel'),
|
|
115
115
|
source.indexOf('type LogSegment'),
|
|
116
116
|
);
|
|
117
|
-
assert.match(activityPanel, /<Index each=\{
|
|
117
|
+
assert.match(activityPanel, /<Index each=\{visible\(\)\}>/);
|
|
118
118
|
assert.doesNotMatch(activityPanel, /<Index each=\{ACTIVITY_SLOTS\}>/);
|
|
119
119
|
assert.match(activityPanel, /paddingX=\{1\}/);
|
|
120
120
|
assert.doesNotMatch(activityPanel, /updatedLine\(activity\(\)\)/);
|
|
@@ -144,7 +144,7 @@ test('ShellUI launcher never starts agents or workspace services implicitly', as
|
|
|
144
144
|
test('ShellUI lets the right pane extend to the terminal edge', async () => {
|
|
145
145
|
const tui = await readFile(new URL('./tui.tsx', import.meta.url), 'utf8');
|
|
146
146
|
const pane = await readFile(new URL('./RightPane.tsx', import.meta.url), 'utf8');
|
|
147
|
-
assert.match(tui, /Math\.
|
|
147
|
+
assert.match(tui, /Math\.max\(32, Math\.floor\(width \* 0\.38\) \+ 2\)/);
|
|
148
148
|
assert.match(pane, /paddingLeft=\{1\}/);
|
|
149
149
|
assert.doesNotMatch(pane, /height="100%" flexDirection="column" padding=\{1\}/);
|
|
150
150
|
});
|
|
@@ -173,6 +173,19 @@ test('Flow/Trace does not repeat the runtime source prefix on every line', async
|
|
|
173
173
|
assert.match(entryRenderer, /prefix\.push\(\{ text: `\$\{parts\.time\} /);
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
test('a doctor summary with a nonzero error count is colored as an error, not a warning', async () => {
|
|
177
|
+
const source = await readFile(new URL('./RightPane.tsx', import.meta.url), 'utf8');
|
|
178
|
+
const colorFn = source.slice(
|
|
179
|
+
source.indexOf('function logMessageColor'),
|
|
180
|
+
source.indexOf('function logRenderLines'),
|
|
181
|
+
);
|
|
182
|
+
// The "0 error(s)" amber shortcut must be digit-anchored: "10 error(s)"
|
|
183
|
+
// ends in "0 error(s)" too, and an unanchored test painted a 10-error
|
|
184
|
+
// doctor failure the same colour as a clean run.
|
|
185
|
+
assert.match(colorFn, /\(\?<!\\d\)0 error\\\(s\\\)/);
|
|
186
|
+
assert.doesNotMatch(colorFn, /\/0 error\\\(s\\\)\/i\.test/);
|
|
187
|
+
});
|
|
188
|
+
|
|
176
189
|
test('runtime logs have a separator and a concise Runtime tab label', async () => {
|
|
177
190
|
const source = await readFile(new URL('./RightPane.tsx', import.meta.url), 'utf8');
|
|
178
191
|
const logPanel = source.slice(
|
|
@@ -742,7 +755,7 @@ test('agent mode without runtime records a visible error instead of falling back
|
|
|
742
755
|
|
|
743
756
|
const message = recordRuntimeUnavailableAgentInput(session, 'salut', { error: 'port 7788 already in use' });
|
|
744
757
|
|
|
745
|
-
assert.equal(message, '⚠ Runtime
|
|
758
|
+
assert.equal(message, '⚠ Runtime unavailable: port 7788 already in use — /agent disabled, /chat still available');
|
|
746
759
|
// `at` est posé à l'insertion : on compare le reste.
|
|
747
760
|
assert.deepEqual(
|
|
748
761
|
conversationMessages(session).map(({ at, ...rest }) => rest),
|
|
@@ -760,7 +773,7 @@ test('runtime status exposes the disconnected reason', () => {
|
|
|
760
773
|
);
|
|
761
774
|
assert.equal(
|
|
762
775
|
runtimeUnavailableAgentMessage({ error: 'token mismatch' }),
|
|
763
|
-
'⚠ Runtime
|
|
776
|
+
'⚠ Runtime unavailable: token mismatch — /agent disabled, /chat still available',
|
|
764
777
|
);
|
|
765
778
|
});
|
|
766
779
|
|
|
@@ -806,7 +819,7 @@ test('/queue cancel on a runtime workflow id points to run cancellation commands
|
|
|
806
819
|
});
|
|
807
820
|
|
|
808
821
|
assert.equal(result.exit, false);
|
|
809
|
-
assert.match(conversationMessages(session).at(-1).content, /
|
|
822
|
+
assert.match(conversationMessages(session).at(-1).content, /Runtime-managed item/);
|
|
810
823
|
assert.match(conversationMessages(session).at(-1).content, /\/run kill/);
|
|
811
824
|
});
|
|
812
825
|
|
package/src/shell/tui.tsx
CHANGED
|
@@ -156,7 +156,7 @@ function App(props: {
|
|
|
156
156
|
const exitShell = () => {
|
|
157
157
|
if (exiting) return;
|
|
158
158
|
exiting = true;
|
|
159
|
-
setExitStatus('
|
|
159
|
+
setExitStatus('Shutting down…');
|
|
160
160
|
const task = Promise.resolve().then(async () => {
|
|
161
161
|
renderer.destroy();
|
|
162
162
|
console.log('[wiki-manager] shell closed; shared runtime left running.');
|
|
@@ -180,11 +180,11 @@ function App(props: {
|
|
|
180
180
|
const conversationRows = createMemo(() => Math.max(4, dimensions().height - 5 - chatInputHeight() - 4));
|
|
181
181
|
const rightColumns = createMemo(() => {
|
|
182
182
|
const width = dimensions().width;
|
|
183
|
-
// 38% + 2 columns
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
return Math.max(32, Math.
|
|
183
|
+
// 38% + 2 columns: the Plan/Activity/Logs panes carry job labels, file
|
|
184
|
+
// names and error messages. The pane scales with the terminal instead of
|
|
185
|
+
// being capped, so a wide screen widens the detail pane rather than leaving
|
|
186
|
+
// it narrow while the conversation column absorbs all the extra slack.
|
|
187
|
+
return Math.max(32, Math.floor(width * 0.38) + 2);
|
|
188
188
|
});
|
|
189
189
|
const leftColumns = createMemo(() => Math.max(32, dimensions().width - rightColumns() - 1));
|
|
190
190
|
const conversationColumns = createMemo(() => {
|
package/src/shell/useAgent.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
52
52
|
}
|
|
53
53
|
if (!props.chatMode() && !trimmed.startsWith('/') && !freeTextRouting?.local) {
|
|
54
54
|
const message = recordRuntimeUnavailableAgentInput(props.session, trimmed, {
|
|
55
|
-
error: props.runtimeUnavailableReason ?? 'runtime
|
|
55
|
+
error: props.runtimeUnavailableReason ?? 'runtime unavailable',
|
|
56
56
|
});
|
|
57
57
|
props.addLog(message ?? 'runtime: disconnected');
|
|
58
58
|
props.refresh();
|
package/src/shell/useSession.ts
CHANGED
|
@@ -158,7 +158,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
158
158
|
const runtimeHint = createMemo(() => {
|
|
159
159
|
version();
|
|
160
160
|
if (props.runtime?.url) return null;
|
|
161
|
-
return runtimeUnavailableAgentMessage({ error: runtimeUnavailableReason() ?? 'runtime
|
|
161
|
+
return runtimeUnavailableAgentMessage({ error: runtimeUnavailableReason() ?? 'runtime unavailable' });
|
|
162
162
|
});
|
|
163
163
|
const matchContext = createMemo(() => {
|
|
164
164
|
if (input() === dismissedSlashInput()) return null;
|
package/wiki-workspace
CHANGED
|
@@ -299,7 +299,12 @@ agents_compose() {
|
|
|
299
299
|
local cacert_args=()
|
|
300
300
|
local profile_args=()
|
|
301
301
|
connectors_enabled && profile_args+=(--profile connectors)
|
|
302
|
-
|
|
302
|
+
gateway_enabled && profile_args+=(--profile gateway)
|
|
303
|
+
# The gateway mounts the MANAGER's agent-runtimes.json — absolute path:
|
|
304
|
+
# compose resolves relative volume paths against the compose file's
|
|
305
|
+
# directory (the installed package), never the operator's state dir.
|
|
306
|
+
local agent_runtimes_file="$(dirname "$MANAGER_ENV_FILE")/agent-runtimes.json"
|
|
307
|
+
WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" AGENT_RUNTIMES_FILE="$agent_runtimes_file" \
|
|
303
308
|
read_lines_into_array cacert_args cacert_compose_args "$agents_compose_file" "agents.cacert.compose.yml"
|
|
304
309
|
# User-owned override: optional external agents, proxy passthrough and
|
|
305
310
|
# any local fix live in a file the user maintains under .wiki/compose —
|
|
@@ -307,7 +312,7 @@ agents_compose() {
|
|
|
307
312
|
local override_args=()
|
|
308
313
|
local user_override="$MANAGER_COMPOSE_DIR/agents.docker-compose.override.yml"
|
|
309
314
|
[[ -f "$user_override" ]] && override_args+=(-f "$user_override")
|
|
310
|
-
WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
|
|
315
|
+
WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" AGENT_RUNTIMES_FILE="$agent_runtimes_file" \
|
|
311
316
|
COMPOSE_PROFILES= \
|
|
312
317
|
docker compose --project-directory "$DEFAULT_MANAGER_DIR" \
|
|
313
318
|
${compose_env_args[@]+"${compose_env_args[@]}"} -f "$agents_compose_file" ${override_args[@]+"${override_args[@]}"} ${cacert_args[@]+"${cacert_args[@]}"} ${profile_args[@]+"${profile_args[@]}"} -p wiki-agents "$@"
|
|
@@ -331,6 +336,7 @@ agents_compose() {
|
|
|
331
336
|
[[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
|
|
332
337
|
mkdir -p "$agents_data_dir/cme" "$agents_data_dir/documents/input" "$agents_data_dir/documents/output" "$agents_data_dir/documents/uploads"
|
|
333
338
|
connectors_enabled && mkdir -p "$agents_data_dir/connectors"
|
|
339
|
+
gateway_enabled && mkdir -p "$agents_data_dir/gateway"
|
|
334
340
|
mkdir -p "$workspaces_root"
|
|
335
341
|
$do_pull && _agents_dc pull
|
|
336
342
|
local up_args=(up -d)
|
|
@@ -345,6 +351,7 @@ agents_compose() {
|
|
|
345
351
|
printf 'Agents: cme=:%s documents=:%s' \
|
|
346
352
|
"${CME_MCP_PORT:-3336}" "${DOCUMENTS_MCP_PORT:-3337}"
|
|
347
353
|
connectors_enabled && printf ' connectors=:%s' "${CONNECTORS_MCP_PORT:-3338}"
|
|
354
|
+
gateway_enabled && printf ' gateway=:%s' "${GATEWAY_PORT:-7789}"
|
|
348
355
|
printf '\n'
|
|
349
356
|
[[ ! -f "$MANAGER_COMPOSE_DIR/agents.docker-compose.override.yml" ]] || printf 'User override: %s\n' "$MANAGER_COMPOSE_DIR/agents.docker-compose.override.yml"
|
|
350
357
|
;;
|
|
@@ -509,7 +516,7 @@ ensure_agent_tokens() {
|
|
|
509
516
|
# workspace and runtime tokens are generated. Leaving them empty made a
|
|
510
517
|
# fresh install start agents nobody could talk to.
|
|
511
518
|
local key token
|
|
512
|
-
for key in CME_MCP_AUTH_TOKEN DOCUMENTS_MCP_AUTH_TOKEN; do
|
|
519
|
+
for key in CME_MCP_AUTH_TOKEN DOCUMENTS_MCP_AUTH_TOKEN GATEWAY_AUTH_TOKEN; do
|
|
513
520
|
token="$(env_value "$MANAGER_ENV_FILE" "$key" "")"
|
|
514
521
|
if [[ -z "$token" ]]; then
|
|
515
522
|
token="$(generate_token)"
|
|
@@ -558,6 +565,15 @@ connectors_enabled() {
|
|
|
558
565
|
esac
|
|
559
566
|
}
|
|
560
567
|
|
|
568
|
+
gateway_enabled() {
|
|
569
|
+
local enabled
|
|
570
|
+
enabled="$(env_value "$MANAGER_ENV_FILE" GATEWAY_ENABLED "${GATEWAY_ENABLED:-true}")"
|
|
571
|
+
case "$enabled" in
|
|
572
|
+
1|[Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]|[Oo][Nn]) return 0 ;;
|
|
573
|
+
*) return 1 ;;
|
|
574
|
+
esac
|
|
575
|
+
}
|
|
576
|
+
|
|
561
577
|
ensure_endpoints_file() {
|
|
562
578
|
# Without mcp.endpoints.json the shell never connects to the agents it
|
|
563
579
|
# just started (and `agents status` refuses to run). Seed it from the
|