@dotdrelle/wiki-manager 0.14.13 → 0.14.20

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 (46) hide show
  1. package/.env.example +29 -4
  2. package/README.md +19 -0
  3. package/docker-compose.yml +6 -1
  4. package/package.json +1 -1
  5. package/src/activity/activityAggregator.js +50 -16
  6. package/src/activity/activityAggregator.test.js +67 -4
  7. package/src/agent/graph.js +79 -11
  8. package/src/agent/graph.test.js +43 -3
  9. package/src/cli/wiki-manager.js +74 -11
  10. package/src/cli/wiki-manager.test.js +40 -1
  11. package/src/commands/slash.js +10 -3
  12. package/src/commands/slash.test.js +24 -0
  13. package/src/core/buildInfo.json +2 -2
  14. package/src/core/dockerCompose.test.js +4 -0
  15. package/src/core/env.test.js +3 -0
  16. package/src/core/mcp.js +1 -1
  17. package/src/core/wikiSetup.js +35 -0
  18. package/src/core/wikiWorkspace.test.js +20 -0
  19. package/src/core/workflow.js +72 -0
  20. package/src/core/workflow.test.js +57 -0
  21. package/src/core/workspaces.js +10 -2
  22. package/src/orchestrator/dependencyResolver.js +19 -1
  23. package/src/orchestrator/objectiveResolver.js +24 -0
  24. package/src/orchestrator/objectiveResolver.test.js +23 -1
  25. package/src/orchestrator/scheduler.js +27 -7
  26. package/src/orchestrator/scheduler.test.js +45 -1
  27. package/src/runtime/auth.test.js +65 -1
  28. package/src/runtime/client.js +4 -0
  29. package/src/runtime/donna-contract.test.js +2 -0
  30. package/src/runtime/lifecycle.js +21 -12
  31. package/src/runtime/runner.js +141 -18
  32. package/src/runtime/runner.test.js +30 -0
  33. package/src/runtime/server.js +13 -2
  34. package/src/runtime/server.test.js +30 -1
  35. package/src/runtime/store.js +54 -0
  36. package/src/runtime/store.test.js +42 -0
  37. package/src/shell/FileEditorDialog.tsx +2 -2
  38. package/src/shell/LeftPane.tsx +60 -15
  39. package/src/shell/RightPane.tsx +168 -56
  40. package/src/shell/StartupScreen.tsx +3 -7
  41. package/src/shell/renderer.ts +1 -0
  42. package/src/shell/repl.js +7 -81
  43. package/src/shell/repl.test.js +141 -38
  44. package/src/shell/tui.tsx +65 -65
  45. package/src/shell/useSession.ts +92 -6
  46. package/wiki-workspace +28 -0
@@ -321,6 +321,52 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
321
321
  ? lastVisiblePlan.map((step) => ({ ...step }))
322
322
  : current;
323
323
  });
324
+ const runSummary = createMemo(() => {
325
+ version();
326
+ const workflow = runtimeState()?.workflow;
327
+ const tasks = Array.isArray(workflow?.nodes)
328
+ ? workflow.nodes.filter((node: any) => node.type === 'task')
329
+ : [];
330
+ if (tasks.length === 0) return null;
331
+
332
+ const graphNodes = Array.isArray(workflow?.graph?.nodes) ? workflow.graph.nodes : [];
333
+ const graphEdges = Array.isArray(workflow?.graph?.edges) ? workflow.graph.edges : [];
334
+ const agents = new Set<string>();
335
+ for (const task of tasks) {
336
+ if (task.executor) agents.add(String(task.executor));
337
+ const assignmentIds = graphEdges
338
+ .filter((edge: any) => edge.type === 'assigned_to' && edge.from === task.id)
339
+ .map((edge: any) => edge.to);
340
+ for (const assignmentId of assignmentIds) {
341
+ graphEdges
342
+ .filter((edge: any) => edge.type === 'uses_agent' && edge.from === assignmentId)
343
+ .forEach((edge: any) => agents.add(String(edge.to).replace(/^agent:/, '')));
344
+ }
345
+ }
346
+
347
+ const activeParallel = tasks.filter((task: any) => String(task.status) === 'running').length;
348
+ const groupConcurrency = graphNodes
349
+ .filter((node: any) => node.type === 'task_group')
350
+ .map((node: any) => Number(node.raw?.recommendedConcurrency))
351
+ .filter((value: number) => Number.isFinite(value) && value > 0);
352
+ // Authoritative resolved concurrency published by the runtime; fall back to
353
+ // the plan-derived value for replayed/historical runs.
354
+ const resolved = runtimeState()?.concurrency;
355
+ const maxParallel = Number.isFinite(Number(resolved?.limit))
356
+ ? Number(resolved.limit)
357
+ : Math.max(1, activeParallel, ...groupConcurrency);
358
+ const done = tasks.filter((task: any) => String(task.status) === 'done').length;
359
+ const usage = workflow?.usage ?? {};
360
+ const tokens = (known: unknown, value: unknown) =>
361
+ known ? new Intl.NumberFormat().format(Number(value) || 0) : '—';
362
+
363
+ return [
364
+ `${agents.size} agent${agents.size === 1 ? '' : 's'}`,
365
+ `parallel ${activeParallel}/×${maxParallel}${resolved?.cappedByCeiling ? ' (ceiling)' : ''}`,
366
+ `${done}/${tasks.length} tasks`,
367
+ `${tokens(usage.inputKnown, usage.inputTokens)} in · ${tokens(usage.outputKnown, usage.outputTokens)} out`,
368
+ ].join(' · ');
369
+ });
324
370
  const visibleLogs = createMemo(() => {
325
371
  version();
326
372
  const runtimeLogs = runtimeState()?.logs;
@@ -329,6 +375,11 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
329
375
  const tagged = filterRuntimeLogs(runtimeLogs.map((line: any) => `runtime ${String(line)}`), filter);
330
376
  return [...logs(), ...tagged];
331
377
  });
378
+ const pendingApprovals = createMemo(() => {
379
+ version();
380
+ return (Array.isArray(runtimeState()?.approvals) ? runtimeState().approvals : [])
381
+ .filter((approval: any) => approval.status === 'pending_approval');
382
+ });
332
383
 
333
384
  // Index-aligned with each workspace's runtimeConversation array — not just
334
385
  // a length count. The server sometimes finalizes a streaming placeholder
@@ -379,16 +430,23 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
379
430
  continue;
380
431
  }
381
432
  if (role === 'user') {
382
- // The user's own message was already pushed optimistically (see
383
- // useAgent.ts) and marked _pending — confirm that entry instead of
384
- // adding a second copy. No matching pending entry (e.g. history
385
- // restored after a fresh reconnect) falls through to a normal push.
433
+ // The user's own message was already pushed optimistically — either
434
+ // marked _pending (web / runtime-submit path, useAgent.ts) OR as a plain
435
+ // entry by the interactive agent turn (repl.js runAgentTurn) that then
436
+ // delegated. Adopt whichever already exists instead of appending a
437
+ // second identical copy (the delegation seeds the run with the same
438
+ // objective, which comes back through this merge).
386
439
  const pendingIndex = target.findIndex((entry: any) => entry._pending && entry.role === 'user' && entry.content === content);
387
440
  if (pendingIndex !== -1) {
388
441
  delete target[pendingIndex]._pending;
389
442
  backed.push(target[pendingIndex]);
390
443
  continue;
391
444
  }
445
+ const localIndex = target.findIndex((entry: any) => entry.role === 'user' && entry.content === content && !backed.includes(entry));
446
+ if (localIndex !== -1) {
447
+ backed.push(target[localIndex]);
448
+ continue;
449
+ }
392
450
  }
393
451
  const entry = { role, content };
394
452
  target.push(entry);
@@ -397,19 +455,37 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
397
455
  }
398
456
 
399
457
  function syncRuntimeState() {
400
- void fetchRuntimeState({ url: props.runtime.url, workspace: (session as any).workspace ?? null })
458
+ const workspace = (session as any).workspace ?? null;
459
+ const workspaceKey = workspace ?? '__global__';
460
+ if (runtimeSyncInFlight.has(workspaceKey)) return;
461
+ runtimeSyncInFlight.add(workspaceKey);
462
+ void fetchRuntimeState({ url: props.runtime.url, workspace })
401
463
  .then((state) => {
464
+ if (((session as any).workspace ?? null) !== workspace) return;
402
465
  const displayState = sanitizeRuntimeStateForDisplay(state);
403
466
  setRuntimeState(displayState);
467
+ // The per-job "Job terminé" lines and the canned plan summary are gone:
468
+ // the runtime now emits ONE natural-language Donna message at completion
469
+ // (see announceRunOutcome in runner.js), which arrives through the
470
+ // conversation merge below. Detect that the chat grew to keep the
471
+ // welcome-hidden / scroll-to-latest behavior.
472
+ const beforeLength = conversationMessages(session).length;
404
473
  mergeRuntimeConversation(displayState);
474
+ if (conversationMessages(session).length > beforeLength) {
475
+ setShowWelcome(false);
476
+ setConversationScroll(0);
477
+ }
405
478
  setRuntimeStatus('connected');
406
479
  refresh();
407
480
  })
408
481
  .catch(() => {
482
+ if (((session as any).workspace ?? null) !== workspace) return;
409
483
  setRuntimeStatus('disconnected');
410
484
  refresh();
411
- });
485
+ })
486
+ .finally(() => { runtimeSyncInFlight.delete(workspaceKey); });
412
487
  }
488
+ const runtimeSyncInFlight = new Set<string>();
413
489
  let runtimeStreamAbort: AbortController | null = null;
414
490
  let runtimeSyncDebounce: ReturnType<typeof setTimeout> | null = null;
415
491
  let runtimeReconnectTimer: ReturnType<typeof setTimeout> | null = null;
@@ -446,6 +522,13 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
446
522
  void subscribeRuntimeEvents();
447
523
  }
448
524
 
525
+ // Safety net only — the SSE stream (subscribeRuntimeEvents) is the primary
526
+ // sync path and already debounces on every agent_event. This just catches
527
+ // a missed/dropped event, so it can poll far less often than that.
528
+ const runtimeCompletionWatchdog = setInterval(() => {
529
+ if (props.runtime?.url && runtimeRunStatus() === 'running') syncRuntimeState();
530
+ }, 10000);
531
+
449
532
  let currentRuntimeWorkspace = (session as any).workspace ?? null;
450
533
  function resyncRuntimeWorkspaceIfChanged() {
451
534
  const workspace = (session as any).workspace ?? null;
@@ -466,6 +549,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
466
549
  runtimeStreamAbort?.abort();
467
550
  if (runtimeSyncDebounce) clearTimeout(runtimeSyncDebounce);
468
551
  if (runtimeReconnectTimer) clearTimeout(runtimeReconnectTimer);
552
+ clearInterval(runtimeCompletionWatchdog);
469
553
  });
470
554
 
471
555
  const activityPollTimer = setInterval(() => {
@@ -690,6 +774,8 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
690
774
  toggleRightTab,
691
775
  selectRightTab,
692
776
  plan,
777
+ runSummary,
778
+ pendingApprovals,
693
779
  conversationScroll,
694
780
  scrollConversation,
695
781
  conversationBusy,
package/wiki-workspace CHANGED
@@ -95,6 +95,7 @@ Commands:
95
95
  agents down Stop all external agents
96
96
  agents logs [args...] Follow external agent logs
97
97
  agents pull Pull latest external agent images
98
+ agents refresh Update only external agents currently running
98
99
  agents status Check reachability of configured MCP endpoints
99
100
 
100
101
  runtime up Start the single host agent-runtime on Node.js 22
@@ -276,6 +277,10 @@ agents_compose() {
276
277
  [[ $# -eq 0 ]] || die "agents pull does not take arguments"
277
278
  _agents_dc pull
278
279
  ;;
280
+ refresh)
281
+ [[ $# -eq 0 ]] || die "agents refresh does not take arguments"
282
+ refresh_running_services 'No running external agent containers to refresh' 'Refreshed running agents: %s' _agents_dc
283
+ ;;
279
284
  status)
280
285
  [[ $# -eq 0 ]] || die "agents status does not take arguments"
281
286
  if [[ ! -f "$MANAGER_ENDPOINTS_FILE" ]]; then
@@ -751,10 +756,30 @@ read_lines_into_array() {
751
756
  eval "$array_name=()"
752
757
  local line
753
758
  while IFS= read -r line; do
759
+ [[ -n "${line//[[:space:]]/}" ]] || continue
754
760
  eval "$array_name+=(\"\$line\")"
755
761
  done < <("$@")
756
762
  }
757
763
 
764
+ # Shared by the "agents refresh" and per-workspace "refresh" subcommands:
765
+ # pull and recreate only the compose services already running, skipping the
766
+ # rest. "$@" is the compose invocation (e.g. `_agents_dc` or
767
+ # `compose_for_workspace "$workspace"`) to run ps/pull/up through.
768
+ refresh_running_services() {
769
+ local no_running_msg="$1"
770
+ local refreshed_msg_fmt="$2"
771
+ shift 2
772
+ local running_services=()
773
+ read_lines_into_array running_services "$@" ps --status running --services
774
+ if [[ ${#running_services[@]} -eq 0 ]]; then
775
+ printf '%s\n' "$no_running_msg"
776
+ return
777
+ fi
778
+ "$@" pull "${running_services[@]}"
779
+ "$@" up -d "${running_services[@]}"
780
+ printf "$refreshed_msg_fmt\n" "${running_services[*]}"
781
+ }
782
+
758
783
  cacert_compose_args() {
759
784
  local compose_file="$1"
760
785
  local override_name="$2"
@@ -1074,6 +1099,9 @@ main() {
1074
1099
  down)
1075
1100
  compose_for_workspace "$workspace" down
1076
1101
  ;;
1102
+ refresh)
1103
+ refresh_running_services "No running workspace containers to refresh: $workspace" "Refreshed running workspace containers: $workspace (%s)" compose_for_workspace "$workspace"
1104
+ ;;
1077
1105
  logs)
1078
1106
  local log_args=()
1079
1107
  read_lines_into_array log_args logs_args "$@"