@dotdrelle/wiki-manager 0.14.12 → 0.14.16

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 (43) hide show
  1. package/.env.example +11 -0
  2. package/docker-compose.yml +1 -1
  3. package/package.json +1 -1
  4. package/src/activity/activityAggregator.js +43 -13
  5. package/src/activity/activityAggregator.test.js +51 -2
  6. package/src/agent/graph.js +79 -11
  7. package/src/agent/graph.test.js +43 -3
  8. package/src/cli/wiki-manager.js +44 -3
  9. package/src/commands/slash.js +10 -3
  10. package/src/commands/slash.test.js +24 -0
  11. package/src/core/activity.js +4 -0
  12. package/src/core/activity.test.js +31 -0
  13. package/src/core/agentEvents.js +11 -2
  14. package/src/core/buildInfo.json +2 -2
  15. package/src/core/dockerCompose.test.js +4 -0
  16. package/src/core/env.js +16 -4
  17. package/src/core/env.test.js +3 -0
  18. package/src/core/mcp.js +1 -1
  19. package/src/core/wikiSetup.js +43 -1
  20. package/src/core/wikiWorkspace.test.js +20 -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.test.js +22 -1
  26. package/src/runtime/auth.test.js +65 -1
  27. package/src/runtime/client.js +4 -0
  28. package/src/runtime/donna-contract.test.js +2 -0
  29. package/src/runtime/lifecycle.js +21 -12
  30. package/src/runtime/runner.js +130 -17
  31. package/src/runtime/runner.test.js +62 -1
  32. package/src/runtime/server.js +13 -2
  33. package/src/runtime/server.test.js +30 -1
  34. package/src/shell/FileEditorDialog.tsx +2 -2
  35. package/src/shell/LeftPane.tsx +60 -15
  36. package/src/shell/RightPane.tsx +147 -54
  37. package/src/shell/StartupScreen.tsx +3 -7
  38. package/src/shell/renderer.ts +1 -0
  39. package/src/shell/repl.js +7 -81
  40. package/src/shell/repl.test.js +128 -38
  41. package/src/shell/tui.tsx +59 -64
  42. package/src/shell/useSession.ts +45 -6
  43. package/wiki-workspace +36 -3
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { createMemo, Index, Show } from 'solid-js';
2
+ import { createMemo, createSignal, Index, Show } from 'solid-js';
3
3
  import { filterRuntimeLogs } from '../core/runtimeLog.js';
4
4
  import { fit } from './textFit';
5
5
 
@@ -19,7 +19,6 @@ type LogLineParts = { time: string | null; message: string };
19
19
  // 4 slots (was 6): items can now span up to 5 lines each (wrapped label +
20
20
  // wrapped status/error), so fewer, readable entries beat more, truncated ones.
21
21
  const ACTIVITY_SLOTS = Array.from({ length: 4 }, (_, index) => index);
22
- const LOG_SLOTS = Array.from({ length: 24 }, (_, index) => index);
23
22
  const PLAN_VIEWPORT_ROWS = 12;
24
23
 
25
24
  function wrapLine(value: string, width: number) {
@@ -70,13 +69,59 @@ function queueColor(status: string) {
70
69
  return activityColor(status);
71
70
  }
72
71
 
73
- function activityLine(activity: any) {
74
- const detail = activity?.progress?.detail ?? null;
75
- const phase = activity?.progress?.step ?? activity?.progress?.phase ?? activity?.progress?.currentStep ?? null;
76
- // On failure the error message is the information that matters — surface
77
- // it instead of repeating the bare status the label color already conveys.
72
+ function activityDetailText(activity: any) {
78
73
  const error = activityErrorText(activity);
79
- return [activity?.status ?? 'unknown', error ?? detail ?? phase].filter(Boolean).join(' · ');
74
+ if (error) return error;
75
+ const progress = activity?.progress ?? {};
76
+ const stepIndex = Number(progress.stepIndex);
77
+ const stepTotal = Number(progress.stepTotal);
78
+ const batchIndex = Number(progress.batchIndex);
79
+ const batchCount = Number(progress.batchCount);
80
+ const instructionCount = Number(progress.instructionCount);
81
+ const taskIndex = Number(progress.taskIndex);
82
+ const taskTotal = Number(progress.taskTotal);
83
+ const taskProgress = Number.isFinite(taskIndex) && Number.isFinite(taskTotal) && taskTotal > 0
84
+ ? `Task ${Math.max(1, taskIndex)}/${taskTotal}`
85
+ : null;
86
+ const stepProgress = Number.isFinite(stepIndex) && Number.isFinite(stepTotal) && stepTotal > 0
87
+ ? `Step ${Math.max(1, stepIndex)}/${stepTotal}`
88
+ : null;
89
+ const batchProgress = Number.isFinite(batchIndex) && Number.isFinite(batchCount) && batchCount > 0
90
+ ? `Batch ${Math.min(batchCount, Math.max(1, batchIndex + 1))}/${batchCount}`
91
+ : null;
92
+ const instructions = Number.isFinite(instructionCount) && instructionCount > 0
93
+ ? `${instructionCount} instructions`
94
+ : null;
95
+ const detail = batchProgress && /^batch\s+\d+\/\d+/i.test(String(progress.detail ?? ''))
96
+ ? null
97
+ : progress.detail;
98
+ const stabilize = [
99
+ ['kept', progress.stabilizeKept],
100
+ ['merged', progress.stabilizeMerged],
101
+ ['inserted', progress.stabilizeInserted],
102
+ ['removed', progress.stabilizeRemoved],
103
+ ].filter(([, value]) => Number.isFinite(Number(value)))
104
+ .map(([label, value]) => `${label} ${Number(value)}`)
105
+ .join(', ');
106
+ const values = [
107
+ taskProgress,
108
+ stepProgress,
109
+ batchProgress,
110
+ progress.label,
111
+ detail,
112
+ instructions,
113
+ stabilize || null,
114
+ progress.currentStep ?? progress.step ?? progress.phase,
115
+ ].map((value) => String(value ?? '').trim()).filter(Boolean);
116
+ return [...new Set(values)].join(' · ');
117
+ }
118
+
119
+ function activityDisplayLabel(activity: any) {
120
+ const businessLabel = activity?._aggregated ? activity?.progress?.label : null;
121
+ const raw = String(businessLabel ?? activity?.label ?? `${activity?.source ?? 'mcp'} ${activity?.kind ?? 'job'}`);
122
+ // The percentage has its own right-aligned badge; do not repeat it in the
123
+ // label (aggregate activities often arrive as "knowledge.update - 85 %").
124
+ return raw.replace(/\s*[-–—·]\s*\d+\s*%\s*$/, '').trim();
80
125
  }
81
126
 
82
127
  function activityErrorText(activity: any): string | null {
@@ -95,18 +140,17 @@ function activityPercentBadge(activity: any): { text: string; bg: string; fg: st
95
140
  return { text: ` ${pct}% `, bg: '#1e3a5f', fg: '#93c5fd' };
96
141
  }
97
142
 
98
- function updatedLine(activity: any) {
99
- const source = activity?.source ?? 'mcp';
100
- const id = activity?.id ? `#${activity.id}` : null;
101
- if (!activity?.updatedAt) return [source, id].filter(Boolean).join(' ');
102
- const age = Math.max(0, Math.round((Date.now() - Date.parse(activity.updatedAt)) / 1000));
103
- return [source, id, `${age}s ago`].filter(Boolean).join(' · ');
104
- }
143
+ // Single source of truth for terminal-status aliases so a plan step's icon and
144
+ // its color can never disagree (e.g. a 'succeeded' step showing done-green but a
145
+ // pending icon). Shared by planStepColor and PlanPanel's icon().
146
+ const DONE_STATUSES = ['done', 'complete', 'completed', 'success', 'succeeded'];
147
+ const FAILED_STATUSES = ['failed', 'error'];
105
148
 
106
149
  function planStepColor(step: PlanStep, firstPendingStep: number | null) {
107
- if (step.status === 'done') return '#8BD5CA';
108
- if (step.status === 'failed') return '#F38BA8';
109
- if (step.status === 'running') return '#89B4FA';
150
+ const status = String(step.status ?? '').toLowerCase();
151
+ if (DONE_STATUSES.includes(status)) return '#8BD5CA';
152
+ if (FAILED_STATUSES.includes(status)) return '#F38BA8';
153
+ if (status === 'running') return '#89B4FA';
110
154
  if (step.step === firstPendingStep) return '#89B4FA';
111
155
  return '#7F8C8D';
112
156
  }
@@ -135,8 +179,12 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
135
179
  // Keep one column for the native vertical scrollbar when the plan is long.
136
180
  const lineWidth = () => Math.max(8, props.width - 3);
137
181
  const firstPending = () => props.plan.find((s) => s.status === 'pending')?.step ?? null;
138
- const icon = (status: string) =>
139
- status === 'done' ? '[✓]' : status === 'failed' ? '[✗]' : status === 'running' ? '[…]' : '[ ]';
182
+ const icon = (rawStatus: string) => {
183
+ const status = String(rawStatus ?? '').toLowerCase();
184
+ if (DONE_STATUSES.includes(status)) return '[✓]';
185
+ if (FAILED_STATUSES.includes(status)) return '[✗]';
186
+ return status === 'running' ? '[…]' : '[ ]';
187
+ };
140
188
  const visualRows = createMemo(() => props.plan.reduce((total, step) =>
141
189
  total + wrapLine(`${icon(step.status)} ${step.step}. ${step.description}`, lineWidth()).slice(0, 2).length, 0));
142
190
  const title = () => {
@@ -149,6 +197,7 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
149
197
  <text width={lineWidth()} fg="#D6DEE8" content={fit(title(), lineWidth())} />
150
198
  <scrollbox
151
199
  height={viewportRows()}
200
+ focusable={false}
152
201
  scrollY={true}
153
202
  scrollX={false}
154
203
  stickyStart="top"
@@ -178,12 +227,13 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
178
227
  export function ActivityPanel(props: { activities: any[]; width: number }) {
179
228
  const lineWidth = () => Math.max(8, props.width - 2);
180
229
  const visible = () => props.activities.slice(-ACTIVITY_SLOTS.length).reverse();
230
+ const visibleSlots = () => visible().map((_activity, index) => index);
181
231
  const activityAt = (index: number) => visible()[index] ?? null;
182
232
  return (
183
- <box flexShrink={0} flexDirection="column" padding={1}>
233
+ <box flexShrink={0} flexDirection="column" paddingX={1}>
184
234
  <text width={lineWidth()} fg="#D6DEE8" content="Activity" />
185
235
  <Show when={visible().length > 0} fallback={<text width={lineWidth()} fg="#7F8C8D" content="no active jobs" />}>
186
- <Index each={ACTIVITY_SLOTS}>
236
+ <Index each={visibleSlots()}>
187
237
  {(slot) => {
188
238
  const activity = () => activityAt(slot());
189
239
  // Wrap instead of hard-truncating: a 40-column pane cut labels to
@@ -193,8 +243,8 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
193
243
  const labelLines = () => {
194
244
  const item = activity();
195
245
  if (!item) return [''];
196
- const label = String(item.progress?.label ?? item.label ?? `${item.source ?? 'mcp'} ${item.kind ?? 'job'}`);
197
- const lines = wrapLine(label, lineWidth()).slice(0, 2);
246
+ const label = activityDisplayLabel(item);
247
+ const lines = wrapLine(label, Math.max(8, lineWidth() - badgeLen())).slice(0, 2);
198
248
  if (lines.length === 2 && String(label).length > lines[0].length + lines[1].length) {
199
249
  lines[1] = fit(lines[1], lineWidth());
200
250
  }
@@ -202,23 +252,19 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
202
252
  };
203
253
  const badge = () => activityPercentBadge(activity());
204
254
  const badgeLen = () => badge()?.text.length ?? 0;
205
- const statusLines = () => {
255
+ const detailLines = () => {
206
256
  const item = activity();
207
- if (!item) return [''];
208
- return wrapLine(activityLine(item), Math.max(8, lineWidth() - badgeLen())).slice(0, 2);
257
+ if (!item) return [];
258
+ return wrapLine(activityDetailText(item), lineWidth()).slice(0, 2);
209
259
  };
210
260
  const statusColor = () => (activityErrorText(activity()) ? '#F38BA8' : '#AAB7C4');
211
261
  return (
212
- <box flexDirection="column" marginTop={slot() === 0 ? 1 : 0}>
213
- <text width={lineWidth()} fg={activityColor(activity()?.status)} content={labelLines()[0]} />
214
- <Show when={labelLines()[1]}>
215
- <text width={lineWidth()} fg={activityColor(activity()?.status)} content={labelLines()[1]} />
216
- </Show>
262
+ <box flexDirection="column">
217
263
  <box height={1} flexDirection="row">
218
264
  <text
219
265
  width={lineWidth() - badgeLen()}
220
- fg={statusColor()}
221
- content={activity() ? fit(statusLines()[0], lineWidth() - badgeLen()) : ''}
266
+ fg={activityColor(activity()?.status)}
267
+ content={fit(labelLines()[0], lineWidth() - badgeLen())}
222
268
  />
223
269
  <text
224
270
  width={badgeLen()}
@@ -227,10 +273,12 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
227
273
  content={badge()?.text ?? ''}
228
274
  />
229
275
  </box>
230
- <Show when={statusLines()[1]}>
231
- <text width={lineWidth()} fg={statusColor()} content={fit(statusLines()[1], lineWidth())} />
276
+ <Show when={labelLines()[1]}>
277
+ <text width={lineWidth()} fg={activityColor(activity()?.status)} content={labelLines()[1]} />
232
278
  </Show>
233
- <text width={lineWidth()} fg="#7F8C8D" content={activity() ? fit(updatedLine(activity()), lineWidth()) : ''} />
279
+ <Index each={detailLines()}>
280
+ {(line) => <text width={lineWidth()} fg={statusColor()} content={fit(line(), lineWidth())} />}
281
+ </Index>
234
282
  </box>
235
283
  );
236
284
  }}
@@ -254,9 +302,9 @@ function logMessageColor(message: string): string {
254
302
  }
255
303
 
256
304
  function logRenderLines(logs: string[], width: number): LogSegment[][] {
257
- // Newest entry FIRST (descending): the freshest information belongs at the
258
- // top of the pane. Wrapped continuation lines stay attached below their
259
- // entry's first line.
305
+ // Runtime history arrives oldest-first. Reverse complete entry blocks once
306
+ // so the newest event is at the top while wrapped continuation lines remain
307
+ // attached below their own entry.
260
308
  const blocks: LogSegment[][][] = [];
261
309
  for (const raw of logs) {
262
310
  blocks.push(logEntryLines(raw, width));
@@ -269,11 +317,12 @@ function logEntryLines(raw: string, width: number): LogSegment[][] {
269
317
  for (const item of [raw]) {
270
318
  const rawLine = item;
271
319
  const sourceMatch = String(rawLine).match(/^(runtime)\s+(.*)$/);
272
- const source = sourceMatch ? sourceMatch[1] : null;
273
320
  const rest = sourceMatch ? sourceMatch[2] : String(rawLine);
274
321
  const parts = logLineParts(rest);
275
322
  const prefix: LogSegment[] = [];
276
- if (source) prefix.push({ text: `${source} `, fg: '#C6A0F6' });
323
+ // This pane already contains runtime logs exclusively. Repeating the word
324
+ // "runtime" on every row consumed ~8 columns and squeezed the useful
325
+ // message into the middle of narrow terminals.
277
326
  if (parts.time) prefix.push({ text: `${parts.time} `, fg: '#89B4FA' });
278
327
  const prefixLength = prefix.reduce((total, segment) => total + segment.text.length, 0);
279
328
  const messageColor = logMessageColor(parts.message);
@@ -291,17 +340,44 @@ function logEntryLines(raw: string, width: number): LogSegment[][] {
291
340
  }
292
341
 
293
342
  export function LogPanel(props: { logs: string[]; width: number; filter?: string }) {
343
+ const [activeLogTab, setActiveLogTab] = createSignal<'flow' | 'agent-status'>('flow');
294
344
  const lineWidth = () => Math.max(8, props.width - 2);
295
- const filteredLogs = () => filterRuntimeLogs(props.logs, props.filter ?? '');
296
- const visibleLines = createMemo(() => logRenderLines(filteredLogs(), lineWidth()).slice(0, 24));
297
- const segmentsAt = (index: number): LogSegment[] => visibleLines()[index] ?? [];
345
+ const isAgentStatus = (line: string) => /agent[_ -]?status/i.test(line);
346
+ // Filtering preserves the runtime history order. logRenderLines performs
347
+ // the single block-level reversal shared by both tabs.
348
+ const filteredLogs = () => filterRuntimeLogs(props.logs, props.filter ?? '')
349
+ .filter((line) => activeLogTab() === 'agent-status' ? isAgentStatus(line) : !isAgentStatus(line));
350
+ const allLines = createMemo(() => logRenderLines(filteredLogs(), lineWidth()));
298
351
  return (
299
- <box flexGrow={1} flexDirection="column" padding={1}>
300
- <text width={lineWidth()} fg="#D6DEE8" content="Logs / Trace" />
301
- <box flexGrow={1} flexDirection="column" overflow="hidden">
302
- <Index each={LOG_SLOTS}>
303
- {(slot) => {
304
- const segments = () => segmentsAt(slot());
352
+ <box flexGrow={2} flexDirection="column" paddingX={1} focusable={false}>
353
+ <text width={lineWidth()} fg="#4B5563" content={'─'.repeat(lineWidth())} />
354
+ <box height={1} flexDirection="row">
355
+ <text
356
+ fg={activeLogTab() === 'flow' ? '#0B1020' : '#D6DEE8'}
357
+ bg={activeLogTab() === 'flow' ? '#89B4FA' : undefined}
358
+ content=" Runtime "
359
+ onMouseUp={() => setActiveLogTab('flow')}
360
+ />
361
+ <text fg="#4B5563" content=" " />
362
+ <text
363
+ fg={activeLogTab() === 'agent-status' ? '#0B1020' : '#D6DEE8'}
364
+ bg={activeLogTab() === 'agent-status' ? '#FBBF24' : undefined}
365
+ content=" Agent status "
366
+ onMouseUp={() => setActiveLogTab('agent-status')}
367
+ />
368
+ </box>
369
+ <scrollbox
370
+ flexGrow={1}
371
+ focusable={false}
372
+ scrollY={true}
373
+ scrollX={false}
374
+ stickyStart="top"
375
+ viewportCulling={true}
376
+ verticalScrollbarOptions={{ visible: allLines().length > 0 }}
377
+ >
378
+ <Index each={allLines()}>
379
+ {(line) => {
380
+ const segments = () => line();
305
381
  const segmentAt = (position: number): LogSegment => segments()[position] ?? { text: '', fg: '#AAB7C4' };
306
382
  const usedWidth = (upTo: number) => segments().slice(0, upTo).reduce((total, segment) => total + segment.text.length, 0);
307
383
  const widthFor = (position: number) => {
@@ -313,7 +389,7 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
313
389
  : Math.min(segment.text.length, lineWidth() - usedWidth(position));
314
390
  };
315
391
  return (
316
- <box height={1} flexDirection="row" overflow="hidden">
392
+ <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
317
393
  <text width={widthFor(0)} fg={segmentAt(0).fg} content={segmentAt(0).text} />
318
394
  <text width={widthFor(1)} fg={segmentAt(1).fg} content={segmentAt(1).text} />
319
395
  <text width={widthFor(2)} fg={segmentAt(2).fg} content={segmentAt(2).text} />
@@ -321,7 +397,7 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
321
397
  );
322
398
  }}
323
399
  </Index>
324
- </box>
400
+ </scrollbox>
325
401
  </box>
326
402
  );
327
403
  }
@@ -397,14 +473,31 @@ export function RightPane(props: {
397
473
  queueInfo: QueueInfo;
398
474
  activeTab: 'plan' | 'queue';
399
475
  logFilter?: string;
476
+ pendingApprovals: any[];
477
+ onApprove: () => void;
400
478
  onTabClick: (tab: 'plan' | 'queue') => void;
401
479
  }) {
402
480
  const planJobName = () => activityJobName(
403
481
  [...props.activities].reverse().find((activity) => !activity.terminal) ?? props.activities.at(-1),
404
482
  );
405
483
  return (
406
- <box width={props.width} height="100%" flexDirection="column" gap={1} padding={1} overflow="hidden">
484
+ <box
485
+ width={props.width}
486
+ height="100%"
487
+ flexDirection="column"
488
+ paddingLeft={1}
489
+ paddingTop={1}
490
+ paddingBottom={1}
491
+ overflow="hidden"
492
+ focusable={false}
493
+ >
407
494
  <TabHeader active={props.activeTab} queueCount={props.queueInfo.active} width={props.width} onTabClick={props.onTabClick} />
495
+ <Show when={props.pendingApprovals.length > 0}>
496
+ <box height={2} flexDirection="column" border={['left']} borderStyle="heavy" borderColor="#FBBF24" paddingX={1}>
497
+ <text fg="#FBBF24" content={`${props.pendingApprovals.length} approbation(s) requise(s)`} />
498
+ <text fg="#0B1020" bg="#FBBF24" content=" Approuver le run " onMouseUp={props.onApprove} />
499
+ </box>
500
+ </Show>
408
501
  <Show when={props.activeTab === 'queue'} fallback={(
409
502
  <>
410
503
  <Show when={props.plan && props.plan.length > 0}>
@@ -7,9 +7,7 @@ export type StartupAction =
7
7
  | 'open-workspace'
8
8
  | 'run-workflow'
9
9
  | 'init-workspace'
10
- | 'retry-preflight'
11
- | 'start-services'
12
- | 'open-logs';
10
+ | 'retry-preflight';
13
11
 
14
12
  type StartupItem = {
15
13
  action: StartupAction;
@@ -115,7 +113,7 @@ export function StartupScreen(props: {
115
113
  if (!props.hasWorkspace) {
116
114
  return [initWorkspaceItem('Create and configure the default workspace')];
117
115
  }
118
- const base = [
116
+ const base: StartupItem[] = [
119
117
  {
120
118
  action: 'new-conversation',
121
119
  label: 'Start a new conversation',
@@ -124,7 +122,7 @@ export function StartupScreen(props: {
124
122
  {
125
123
  action: 'open-workspace',
126
124
  label: 'Open workspace',
127
- detail: 'Select, load, then start agents and services',
125
+ detail: 'Select and load a workspace',
128
126
  },
129
127
  initWorkspaceItem('Create and configure a new workspace'),
130
128
  {
@@ -135,8 +133,6 @@ export function StartupScreen(props: {
135
133
  ];
136
134
  if (props.preflight?.status === 'degraded') {
137
135
  base.push({ action: 'retry-preflight', label: 'Retry pending checks', detail: 'Recheck Docker, Internet, containers and MCP' });
138
- base.push({ action: 'start-services', label: 'Start services', detail: 'Start agents and workspace containers, then recheck' });
139
- base.push({ action: 'open-logs', label: 'Open diagnostics', detail: 'Show service logs and detailed MCP status' });
140
136
  }
141
137
  return base;
142
138
  });
@@ -25,6 +25,7 @@ export function renderPlainMarkdown(value: string) {
25
25
  .replace(/\*\*([^*]+)\*\*/g, '$1')
26
26
  .replace(/__([^_]+)__/g, '$1')
27
27
  .replace(/`([^`]+)`/g, '$1')
28
+ .replace(/<((?:https?):\/\/[^>\s]+)>/g, '$1')
28
29
  .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
29
30
  // collapse consecutive blank lines left by removed elements
30
31
  .replace(/\n{3,}/g, '\n\n')
package/src/shell/repl.js CHANGED
@@ -822,73 +822,8 @@ function rememberProductionActivity(session, payload) {
822
822
  return true;
823
823
  }
824
824
 
825
- const TERMINAL_PLAN_STATUSES = new Set(['done', 'completed', 'succeeded', 'success', 'failed', 'error', 'cancelled', 'canceled']);
826
-
827
- function planTaskKey(task, index) {
828
- return String(task?.id ?? task?.step ?? `task-${index + 1}`);
829
- }
830
-
831
- function errorDescription(value) {
832
- if (!value) return null;
833
- if (typeof value === 'string') return value.trim() || null;
834
- if (value instanceof Error) return value.message;
835
- if (typeof value === 'object') {
836
- return errorDescription(value.message ?? value.reason ?? value.detail ?? value.description ?? value.error)
837
- ?? (() => { try { return JSON.stringify(value); } catch { return String(value); } })();
838
- }
839
- return String(value);
840
- }
841
-
842
- function planTaskError(task, state) {
843
- const activityKey = task?.activityKey ?? task?._activityKey ?? task?.ownerActivityKey;
844
- const activity = (state?.activities ?? []).find((item) =>
845
- (activityKey && String(item?.key) === String(activityKey))
846
- || (task?.id != null && String(item?.taskId ?? '') === String(task.id)));
847
- return errorDescription(
848
- task?.error
849
- ?? task?.result?.error
850
- ?? task?.result?.reason
851
- ?? activity?.error
852
- ?? activity?.detail
853
- ?? (['failed', 'error'].includes(String(state?.status ?? '').toLowerCase()) ? state?.evaluation?.reason : null),
854
- );
855
- }
856
-
857
- export function appendRuntimePlanCompletionMessages(session, state) {
858
- const plan = Array.isArray(state?.plan) ? state.plan : [];
859
- const runId = String(state?.runId ?? 'runtime');
860
- const current = new Map(plan.map((task, index) => [planTaskKey(task, index), String(task?.status ?? 'pending').toLowerCase()]));
861
- if (session._runtimePlanRunId !== runId || !(session._runtimePlanStatuses instanceof Map)) {
862
- session._runtimePlanRunId = runId;
863
- session._runtimePlanStatuses = current;
864
- return 0;
865
- }
866
-
867
- let appended = 0;
868
- plan.forEach((task, index) => {
869
- const key = planTaskKey(task, index);
870
- const status = current.get(key);
871
- const previous = session._runtimePlanStatuses.get(key);
872
- if (!TERMINAL_PLAN_STATUSES.has(status) || TERMINAL_PLAN_STATUSES.has(previous)) return;
873
- const label = String(task?.label ?? task?.description ?? task?.name ?? `Job ${key}`);
874
- const failed = status === 'failed' || status === 'error';
875
- const cancelled = status === 'cancelled' || status === 'canceled';
876
- const error = failed ? planTaskError(task, state) : null;
877
- const content = failed
878
- ? `✗ Job terminé en erreur : ${label}\nStatus: ${status}${error ? `\nErreur: ${error}` : ''}`
879
- : cancelled
880
- ? `○ Job terminé : ${label}\nStatus: ${status}`
881
- : `✓ Job terminé : ${label}\nStatus: ${status}`;
882
- conversationMessages(session).push({ role: 'command', content });
883
- appended += 1;
884
- });
885
- session._runtimePlanStatuses = current;
886
- return appended;
887
- }
888
-
889
825
  export function applyRuntimeStateToShellSession(session, state) {
890
826
  if (!state || typeof state !== 'object') return false;
891
- appendRuntimePlanCompletionMessages(session, state);
892
827
  const displayState = sanitizeRuntimeStateForDisplay(state);
893
828
  session.agentProjection = {
894
829
  conversation: Array.isArray(displayState.conversation) ? displayState.conversation.map((message) => ({ ...message })) : [],
@@ -937,21 +872,9 @@ export function applyRuntimeStateToShellSession(session, state) {
937
872
 
938
873
  export function sanitizeRuntimeStateForDisplay(state) {
939
874
  if (!state || typeof state !== 'object') return state;
940
- const status = String(state.status ?? 'idle').toLowerCase();
941
- const visible = ['running', 'queued', 'pending', 'waiting', 'pending_approval', 'error', 'failed']
942
- .includes(status);
943
- if (visible) return state;
944
- return {
945
- ...state,
946
- conversation: [],
947
- chain: [],
948
- plan: [],
949
- activities: [],
950
- workflow: null,
951
- logs: [],
952
- summary: null,
953
- planPatches: [],
954
- };
875
+ // Keep the last terminal run visible for post-run inspection. A new
876
+ // run_started event replaces these fields when the next run begins.
877
+ return state;
955
878
  }
956
879
 
957
880
  // In agent mode, Donna receives every free-text turn and decides whether to
@@ -1467,7 +1390,10 @@ export async function runLine(line, { agent, packageJson, session, onUpdate, onS
1467
1390
  onUpdate?.();
1468
1391
  return { exit: false, oneShotAgent: true };
1469
1392
  }
1470
- conversationMessages(session).push({ role: 'user', content: oneShotAgentInput });
1393
+ // The runtime persists and returns this same user message through /state.
1394
+ // Mark the optimistic local copy so useSession can bind that runtime entry
1395
+ // to it instead of appending a duplicate after `/agent <instruction>`.
1396
+ conversationMessages(session).push({ role: 'user', content: oneShotAgentInput, _pending: true });
1471
1397
  onUpdate?.();
1472
1398
  const outcome = await submitRuntimeRun(oneShotAgentInput, { runtime, session });
1473
1399
  applyRuntimeOutcome(session, outcome, (msg) => onStep?.(msg));