@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.
Files changed (59) hide show
  1. package/.env.example +10 -3
  2. package/README.md +54 -0
  3. package/agent-runtimes.example.json +68 -0
  4. package/agents.docker-compose.yml +35 -1
  5. package/docker-compose.yml +3 -3
  6. package/package.json +3 -2
  7. package/src/agent/graph.js +15 -14
  8. package/src/agent/graph.test.js +1 -1
  9. package/src/agent/skillRecursion.test.js +13 -12
  10. package/src/cli/wiki-manager.js +124 -36
  11. package/src/commands/slash.js +48 -13
  12. package/src/contracts/schemas.js +67 -0
  13. package/src/core/activity.js +5 -0
  14. package/src/core/agentEvents.js +18 -1
  15. package/src/core/agentLoop.js +3 -3
  16. package/src/core/agentLoop.test.js +1 -1
  17. package/src/core/buildInfo.json +2 -2
  18. package/src/core/dockerCompose.test.js +8 -40
  19. package/src/core/env.js +14 -0
  20. package/src/core/env.test.js +19 -0
  21. package/src/core/googleGrants.test.js +1 -1
  22. package/src/core/mcp.js +1 -1
  23. package/src/core/runtimeEventAdapter.js +81 -0
  24. package/src/core/runtimeEventAdapter.test.js +61 -0
  25. package/src/core/skillChainView.test.js +2 -2
  26. package/src/core/skillCompiler.test.js +1 -1
  27. package/src/core/skillInvocation.js +13 -8
  28. package/src/core/startupCheck.js +58 -0
  29. package/src/core/startupCheck.test.js +29 -1
  30. package/src/orchestrator/agentRegistry.js +1 -22
  31. package/src/orchestrator/assignmentManager.js +16 -4
  32. package/src/orchestrator/capabilityRegistry.js +8 -1
  33. package/src/orchestrator/dispatcher.js +361 -2
  34. package/src/orchestrator/dispatcher.test.js +112 -1
  35. package/src/orchestrator/objectiveResolver.js +10 -6
  36. package/src/orchestrator/objectiveResolver.test.js +26 -27
  37. package/src/orchestrator/providers/deepAgentsProvider.js +168 -0
  38. package/src/orchestrator/providers/deepAgentsProvider.test.js +178 -0
  39. package/src/orchestrator/providers/dispatcherExternalRuntime.test.js +409 -0
  40. package/src/orchestrator/providers/fakeRuntimeProvider.js +164 -0
  41. package/src/orchestrator/providers/fakeRuntimeProvider.test.js +201 -0
  42. package/src/orchestrator/providers/runtimeProvider.js +101 -0
  43. package/src/orchestrator/providers/runtimeProviders.js +325 -0
  44. package/src/orchestrator/providers/runtimeProviders.test.js +361 -0
  45. package/src/orchestrator/resultAggregator.js +35 -2
  46. package/src/orchestrator/resultAggregator.test.js +62 -0
  47. package/src/runtime/recoveryManager.js +70 -5
  48. package/src/runtime/runner.js +6 -6
  49. package/src/runtime/runner.test.js +1 -1
  50. package/src/runtime/skillChain.e2e.test.js +2 -2
  51. package/src/runtime/supervisor.js +5 -10
  52. package/src/shell/RightPane.tsx +25 -9
  53. package/src/shell/StartupScreen.tsx +44 -7
  54. package/src/shell/repl.js +12 -12
  55. package/src/shell/repl.test.js +18 -5
  56. package/src/shell/tui.tsx +6 -6
  57. package/src/shell/useAgent.ts +1 -1
  58. package/src/shell/useSession.ts +1 -1
  59. 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
- const payload = normalizeRuntimeLog(message, { session });
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() {
@@ -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
- const visible = () => props.activities.slice(-ACTIVITY_SLOTS.length).reverse();
267
- const visibleSlots = () => visible().map((_activity, index) => index);
268
- const activityAt = (index: number) => visible()[index] ?? null;
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 flexShrink={0} flexDirection="column" paddingX={1} backgroundColor="#111318">
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
- <Index each={visibleSlots()}>
274
- {(slot) => {
275
- const activity = () => activityAt(slot());
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} approbation(s) requise(s)`} />
540
- <text fg="#0B1020" bg="#FBBF24" content=" Approuver le run " onMouseUp={props.onApprove} />
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.5))} />
285
- <text fg={statusColor()} content={fit(` ${props.preflightBusy ? '◐' : statusDot(props.preflight?.status === 'ready')} ${status()}`, Math.floor(innerWidth() * 0.45))} />
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, preflightChecks().length)} flexDirection="column" border={['left']} borderStyle="heavy" borderColor="#5DADE2" paddingX={1} overflow="hidden">
324
- <For each={preflightChecks().length ? preflightChecks() : [
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 introuvable';
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 indisponible : ${reason} — /agent désactivé, /chat reste possible` : null;
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 indisponible.' });
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('flux vide');
1425
+ donnaMessage.content = formatLlmUnavailableMessage('empty stream');
1426
1426
  }
1427
1427
  } else {
1428
- messages.push({ role: 'donna', content: formatLlmUnavailableMessage('flux vide') });
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('flux vide');
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('reponse vide');
1484
+ donnaMessage.content = formatLlmUnavailableMessage('empty response');
1485
1485
  } else {
1486
- messages.push({ role: 'donna', content: formatLlmUnavailableMessage('reponse vide') });
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
- ? 'Je n’ai pas pu conclure dans la limite d’itérations du mode chat. Passe en /agent si besoin.'
1537
- : (stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('reponse vide'));
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('flux vide');
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('flux vide');
1642
+ return stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('empty stream');
1643
1643
  }
1644
1644
  return directChatUnavailableText(session);
1645
1645
  }
@@ -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=\{visibleSlots\(\)\}>/);
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\.min\(58, Math\.floor\(width \* 0\.38\) \+ 2\)/);
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 indisponible : port 7788 already in use — /agent désactivé, /chat reste possible');
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 indisponible : token mismatch — /agent désactivé, /chat reste possible',
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, /Item géré par le runtime/);
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('Fermeture enclenchée…');
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 / cap 58: the Plan/Activity/Logs panes carry job
184
- // labels, file names and error messages — 40 columns truncated everything
185
- // into unreadable stubs. The small addition uses the terminal's right-side
186
- // slack without making the conversation pane noticeably narrower.
187
- return Math.max(32, Math.min(58, Math.floor(width * 0.38) + 2));
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(() => {
@@ -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 introuvable',
55
+ error: props.runtimeUnavailableReason ?? 'runtime unavailable',
56
56
  });
57
57
  props.addLog(message ?? 'runtime: disconnected');
58
58
  props.refresh();
@@ -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 introuvable' });
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
- WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
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