@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.
- package/.env.example +11 -0
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/src/activity/activityAggregator.js +43 -13
- package/src/activity/activityAggregator.test.js +51 -2
- package/src/agent/graph.js +79 -11
- package/src/agent/graph.test.js +43 -3
- package/src/cli/wiki-manager.js +44 -3
- package/src/commands/slash.js +10 -3
- package/src/commands/slash.test.js +24 -0
- package/src/core/activity.js +4 -0
- package/src/core/activity.test.js +31 -0
- package/src/core/agentEvents.js +11 -2
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +4 -0
- package/src/core/env.js +16 -4
- package/src/core/env.test.js +3 -0
- package/src/core/mcp.js +1 -1
- package/src/core/wikiSetup.js +43 -1
- package/src/core/wikiWorkspace.test.js +20 -0
- package/src/core/workspaces.js +10 -2
- package/src/orchestrator/dependencyResolver.js +19 -1
- package/src/orchestrator/objectiveResolver.js +24 -0
- package/src/orchestrator/objectiveResolver.test.js +23 -1
- package/src/orchestrator/scheduler.test.js +22 -1
- package/src/runtime/auth.test.js +65 -1
- package/src/runtime/client.js +4 -0
- package/src/runtime/donna-contract.test.js +2 -0
- package/src/runtime/lifecycle.js +21 -12
- package/src/runtime/runner.js +130 -17
- package/src/runtime/runner.test.js +62 -1
- package/src/runtime/server.js +13 -2
- package/src/runtime/server.test.js +30 -1
- package/src/shell/FileEditorDialog.tsx +2 -2
- package/src/shell/LeftPane.tsx +60 -15
- package/src/shell/RightPane.tsx +147 -54
- package/src/shell/StartupScreen.tsx +3 -7
- package/src/shell/renderer.ts +1 -0
- package/src/shell/repl.js +7 -81
- package/src/shell/repl.test.js +128 -38
- package/src/shell/tui.tsx +59 -64
- package/src/shell/useSession.ts +45 -6
- package/wiki-workspace +36 -3
package/src/shell/RightPane.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
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 = (
|
|
139
|
-
|
|
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"
|
|
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={
|
|
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 =
|
|
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
|
|
255
|
+
const detailLines = () => {
|
|
206
256
|
const item = activity();
|
|
207
|
-
if (!item) return [
|
|
208
|
-
return wrapLine(
|
|
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"
|
|
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={
|
|
221
|
-
content={
|
|
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={
|
|
231
|
-
<text width={lineWidth()} fg={
|
|
276
|
+
<Show when={labelLines()[1]}>
|
|
277
|
+
<text width={lineWidth()} fg={activityColor(activity()?.status)} content={labelLines()[1]} />
|
|
232
278
|
</Show>
|
|
233
|
-
<
|
|
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
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
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
|
-
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
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={
|
|
300
|
-
<text width={lineWidth()} fg="#
|
|
301
|
-
<box
|
|
302
|
-
<
|
|
303
|
-
{(
|
|
304
|
-
|
|
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
|
-
</
|
|
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
|
|
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
|
|
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
|
});
|
package/src/shell/renderer.ts
CHANGED
|
@@ -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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
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));
|