@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.
- package/.env.example +29 -4
- package/README.md +19 -0
- package/docker-compose.yml +6 -1
- package/package.json +1 -1
- package/src/activity/activityAggregator.js +50 -16
- package/src/activity/activityAggregator.test.js +67 -4
- package/src/agent/graph.js +79 -11
- package/src/agent/graph.test.js +43 -3
- package/src/cli/wiki-manager.js +74 -11
- package/src/cli/wiki-manager.test.js +40 -1
- package/src/commands/slash.js +10 -3
- package/src/commands/slash.test.js +24 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +4 -0
- package/src/core/env.test.js +3 -0
- package/src/core/mcp.js +1 -1
- package/src/core/wikiSetup.js +35 -0
- package/src/core/wikiWorkspace.test.js +20 -0
- package/src/core/workflow.js +72 -0
- package/src/core/workflow.test.js +57 -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.js +27 -7
- package/src/orchestrator/scheduler.test.js +45 -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 +141 -18
- package/src/runtime/runner.test.js +30 -0
- package/src/runtime/server.js +13 -2
- package/src/runtime/server.test.js +30 -1
- package/src/runtime/store.js +54 -0
- package/src/runtime/store.test.js +42 -0
- package/src/shell/FileEditorDialog.tsx +2 -2
- package/src/shell/LeftPane.tsx +60 -15
- package/src/shell/RightPane.tsx +168 -56
- 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 +141 -38
- package/src/shell/tui.tsx +65 -65
- package/src/shell/useSession.ts +92 -6
- package/wiki-workspace +28 -0
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,22 @@ 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
|
-
|
|
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'];
|
|
148
|
+
// A task blocked on a human decision — distinct from both running and pending,
|
|
149
|
+
// so it must not render as a neutral gray "[ ]" (indistinguishable from
|
|
150
|
+
// not-started) nor as a running step.
|
|
151
|
+
const APPROVAL_STATUSES = ['pending_approval', 'waiting_approval'];
|
|
105
152
|
|
|
106
153
|
function planStepColor(step: PlanStep, firstPendingStep: number | null) {
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
154
|
+
const status = String(step.status ?? '').toLowerCase();
|
|
155
|
+
if (DONE_STATUSES.includes(status)) return '#8BD5CA';
|
|
156
|
+
if (FAILED_STATUSES.includes(status)) return '#F38BA8';
|
|
157
|
+
if (APPROVAL_STATUSES.includes(status)) return '#FBBF24';
|
|
158
|
+
if (status === 'running') return '#89B4FA';
|
|
110
159
|
if (step.step === firstPendingStep) return '#89B4FA';
|
|
111
160
|
return '#7F8C8D';
|
|
112
161
|
}
|
|
@@ -131,12 +180,20 @@ function queueSummary(item: QueueItem) {
|
|
|
131
180
|
return parts.join(' ');
|
|
132
181
|
}
|
|
133
182
|
|
|
134
|
-
export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: string }) {
|
|
183
|
+
export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: string; summary?: string | null; spinnerFrame?: string }) {
|
|
135
184
|
// Keep one column for the native vertical scrollbar when the plan is long.
|
|
136
185
|
const lineWidth = () => Math.max(8, props.width - 3);
|
|
137
186
|
const firstPending = () => props.plan.find((s) => s.status === 'pending')?.step ?? null;
|
|
138
|
-
const icon = (
|
|
139
|
-
|
|
187
|
+
const icon = (rawStatus: string) => {
|
|
188
|
+
const status = String(rawStatus ?? '').toLowerCase();
|
|
189
|
+
if (DONE_STATUSES.includes(status)) return '[✓]';
|
|
190
|
+
if (FAILED_STATUSES.includes(status)) return '[✗]';
|
|
191
|
+
if (APPROVAL_STATUSES.includes(status)) return '[⏸]';
|
|
192
|
+
// Animated braille spinner on the running step: a long production job (LLM
|
|
193
|
+
// building a template) otherwise leaves the whole plan visually frozen. Same
|
|
194
|
+
// frames as the chat spinner. Falls back to a static glyph if not supplied.
|
|
195
|
+
return status === 'running' ? `[${props.spinnerFrame ?? '…'}]` : '[ ]';
|
|
196
|
+
};
|
|
140
197
|
const visualRows = createMemo(() => props.plan.reduce((total, step) =>
|
|
141
198
|
total + wrapLine(`${icon(step.status)} ${step.step}. ${step.description}`, lineWidth()).slice(0, 2).length, 0));
|
|
142
199
|
const title = () => {
|
|
@@ -144,11 +201,20 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
144
201
|
return visualRows() > PLAN_VIEWPORT_ROWS ? `${label} (${props.plan.length}) · scroll` : label;
|
|
145
202
|
};
|
|
146
203
|
const viewportRows = () => Math.min(PLAN_VIEWPORT_ROWS, Math.max(1, visualRows()));
|
|
204
|
+
const summaryLines = () => props.summary ? wrapLine(props.summary, lineWidth()).slice(0, 2) : [];
|
|
147
205
|
return (
|
|
148
206
|
<box flexShrink={0} flexDirection="column" padding={1}>
|
|
149
207
|
<text width={lineWidth()} fg="#D6DEE8" content={fit(title(), lineWidth())} />
|
|
208
|
+
<Show when={summaryLines().length > 0}>
|
|
209
|
+
<box flexShrink={0} flexDirection="column">
|
|
210
|
+
<Index each={summaryLines()}>
|
|
211
|
+
{(line) => <text width={lineWidth()} fg="#89B4FA" content={line()} />}
|
|
212
|
+
</Index>
|
|
213
|
+
</box>
|
|
214
|
+
</Show>
|
|
150
215
|
<scrollbox
|
|
151
216
|
height={viewportRows()}
|
|
217
|
+
focusable={false}
|
|
152
218
|
scrollY={true}
|
|
153
219
|
scrollX={false}
|
|
154
220
|
stickyStart="top"
|
|
@@ -178,12 +244,13 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
178
244
|
export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
179
245
|
const lineWidth = () => Math.max(8, props.width - 2);
|
|
180
246
|
const visible = () => props.activities.slice(-ACTIVITY_SLOTS.length).reverse();
|
|
247
|
+
const visibleSlots = () => visible().map((_activity, index) => index);
|
|
181
248
|
const activityAt = (index: number) => visible()[index] ?? null;
|
|
182
249
|
return (
|
|
183
|
-
<box flexShrink={0} flexDirection="column"
|
|
250
|
+
<box flexShrink={0} flexDirection="column" paddingX={1}>
|
|
184
251
|
<text width={lineWidth()} fg="#D6DEE8" content="Activity" />
|
|
185
252
|
<Show when={visible().length > 0} fallback={<text width={lineWidth()} fg="#7F8C8D" content="no active jobs" />}>
|
|
186
|
-
<Index each={
|
|
253
|
+
<Index each={visibleSlots()}>
|
|
187
254
|
{(slot) => {
|
|
188
255
|
const activity = () => activityAt(slot());
|
|
189
256
|
// Wrap instead of hard-truncating: a 40-column pane cut labels to
|
|
@@ -193,8 +260,8 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
|
193
260
|
const labelLines = () => {
|
|
194
261
|
const item = activity();
|
|
195
262
|
if (!item) return [''];
|
|
196
|
-
const label =
|
|
197
|
-
const lines = wrapLine(label, lineWidth()).slice(0, 2);
|
|
263
|
+
const label = activityDisplayLabel(item);
|
|
264
|
+
const lines = wrapLine(label, Math.max(8, lineWidth() - badgeLen())).slice(0, 2);
|
|
198
265
|
if (lines.length === 2 && String(label).length > lines[0].length + lines[1].length) {
|
|
199
266
|
lines[1] = fit(lines[1], lineWidth());
|
|
200
267
|
}
|
|
@@ -202,23 +269,19 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
|
202
269
|
};
|
|
203
270
|
const badge = () => activityPercentBadge(activity());
|
|
204
271
|
const badgeLen = () => badge()?.text.length ?? 0;
|
|
205
|
-
const
|
|
272
|
+
const detailLines = () => {
|
|
206
273
|
const item = activity();
|
|
207
|
-
if (!item) return [
|
|
208
|
-
return wrapLine(
|
|
274
|
+
if (!item) return [];
|
|
275
|
+
return wrapLine(activityDetailText(item), lineWidth()).slice(0, 2);
|
|
209
276
|
};
|
|
210
277
|
const statusColor = () => (activityErrorText(activity()) ? '#F38BA8' : '#AAB7C4');
|
|
211
278
|
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>
|
|
279
|
+
<box flexDirection="column">
|
|
217
280
|
<box height={1} flexDirection="row">
|
|
218
281
|
<text
|
|
219
282
|
width={lineWidth() - badgeLen()}
|
|
220
|
-
fg={
|
|
221
|
-
content={
|
|
283
|
+
fg={activityColor(activity()?.status)}
|
|
284
|
+
content={fit(labelLines()[0], lineWidth() - badgeLen())}
|
|
222
285
|
/>
|
|
223
286
|
<text
|
|
224
287
|
width={badgeLen()}
|
|
@@ -227,10 +290,12 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
|
227
290
|
content={badge()?.text ?? ''}
|
|
228
291
|
/>
|
|
229
292
|
</box>
|
|
230
|
-
<Show when={
|
|
231
|
-
<text width={lineWidth()} fg={
|
|
293
|
+
<Show when={labelLines()[1]}>
|
|
294
|
+
<text width={lineWidth()} fg={activityColor(activity()?.status)} content={labelLines()[1]} />
|
|
232
295
|
</Show>
|
|
233
|
-
<
|
|
296
|
+
<Index each={detailLines()}>
|
|
297
|
+
{(line) => <text width={lineWidth()} fg={statusColor()} content={fit(line(), lineWidth())} />}
|
|
298
|
+
</Index>
|
|
234
299
|
</box>
|
|
235
300
|
);
|
|
236
301
|
}}
|
|
@@ -254,9 +319,9 @@ function logMessageColor(message: string): string {
|
|
|
254
319
|
}
|
|
255
320
|
|
|
256
321
|
function logRenderLines(logs: string[], width: number): LogSegment[][] {
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
322
|
+
// Runtime history arrives oldest-first. Reverse complete entry blocks once
|
|
323
|
+
// so the newest event is at the top while wrapped continuation lines remain
|
|
324
|
+
// attached below their own entry.
|
|
260
325
|
const blocks: LogSegment[][][] = [];
|
|
261
326
|
for (const raw of logs) {
|
|
262
327
|
blocks.push(logEntryLines(raw, width));
|
|
@@ -269,11 +334,12 @@ function logEntryLines(raw: string, width: number): LogSegment[][] {
|
|
|
269
334
|
for (const item of [raw]) {
|
|
270
335
|
const rawLine = item;
|
|
271
336
|
const sourceMatch = String(rawLine).match(/^(runtime)\s+(.*)$/);
|
|
272
|
-
const source = sourceMatch ? sourceMatch[1] : null;
|
|
273
337
|
const rest = sourceMatch ? sourceMatch[2] : String(rawLine);
|
|
274
338
|
const parts = logLineParts(rest);
|
|
275
339
|
const prefix: LogSegment[] = [];
|
|
276
|
-
|
|
340
|
+
// This pane already contains runtime logs exclusively. Repeating the word
|
|
341
|
+
// "runtime" on every row consumed ~8 columns and squeezed the useful
|
|
342
|
+
// message into the middle of narrow terminals.
|
|
277
343
|
if (parts.time) prefix.push({ text: `${parts.time} `, fg: '#89B4FA' });
|
|
278
344
|
const prefixLength = prefix.reduce((total, segment) => total + segment.text.length, 0);
|
|
279
345
|
const messageColor = logMessageColor(parts.message);
|
|
@@ -291,17 +357,44 @@ function logEntryLines(raw: string, width: number): LogSegment[][] {
|
|
|
291
357
|
}
|
|
292
358
|
|
|
293
359
|
export function LogPanel(props: { logs: string[]; width: number; filter?: string }) {
|
|
360
|
+
const [activeLogTab, setActiveLogTab] = createSignal<'flow' | 'agent-status'>('flow');
|
|
294
361
|
const lineWidth = () => Math.max(8, props.width - 2);
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
362
|
+
const isAgentStatus = (line: string) => /agent[_ -]?status/i.test(line);
|
|
363
|
+
// Filtering preserves the runtime history order. logRenderLines performs
|
|
364
|
+
// the single block-level reversal shared by both tabs.
|
|
365
|
+
const filteredLogs = () => filterRuntimeLogs(props.logs, props.filter ?? '')
|
|
366
|
+
.filter((line) => activeLogTab() === 'agent-status' ? isAgentStatus(line) : !isAgentStatus(line));
|
|
367
|
+
const allLines = createMemo(() => logRenderLines(filteredLogs(), lineWidth()));
|
|
298
368
|
return (
|
|
299
|
-
<box flexGrow={
|
|
300
|
-
<text width={lineWidth()} fg="#
|
|
301
|
-
<box
|
|
302
|
-
<
|
|
303
|
-
{(
|
|
304
|
-
|
|
369
|
+
<box flexGrow={2} flexDirection="column" paddingX={1} focusable={false}>
|
|
370
|
+
<text width={lineWidth()} fg="#4B5563" content={'─'.repeat(lineWidth())} />
|
|
371
|
+
<box height={1} flexDirection="row">
|
|
372
|
+
<text
|
|
373
|
+
fg={activeLogTab() === 'flow' ? '#0B1020' : '#D6DEE8'}
|
|
374
|
+
bg={activeLogTab() === 'flow' ? '#89B4FA' : undefined}
|
|
375
|
+
content=" Runtime "
|
|
376
|
+
onMouseUp={() => setActiveLogTab('flow')}
|
|
377
|
+
/>
|
|
378
|
+
<text fg="#4B5563" content=" " />
|
|
379
|
+
<text
|
|
380
|
+
fg={activeLogTab() === 'agent-status' ? '#0B1020' : '#D6DEE8'}
|
|
381
|
+
bg={activeLogTab() === 'agent-status' ? '#FBBF24' : undefined}
|
|
382
|
+
content=" Agent status "
|
|
383
|
+
onMouseUp={() => setActiveLogTab('agent-status')}
|
|
384
|
+
/>
|
|
385
|
+
</box>
|
|
386
|
+
<scrollbox
|
|
387
|
+
flexGrow={1}
|
|
388
|
+
focusable={false}
|
|
389
|
+
scrollY={true}
|
|
390
|
+
scrollX={false}
|
|
391
|
+
stickyStart="top"
|
|
392
|
+
viewportCulling={true}
|
|
393
|
+
verticalScrollbarOptions={{ visible: allLines().length > 0 }}
|
|
394
|
+
>
|
|
395
|
+
<Index each={allLines()}>
|
|
396
|
+
{(line) => {
|
|
397
|
+
const segments = () => line();
|
|
305
398
|
const segmentAt = (position: number): LogSegment => segments()[position] ?? { text: '', fg: '#AAB7C4' };
|
|
306
399
|
const usedWidth = (upTo: number) => segments().slice(0, upTo).reduce((total, segment) => total + segment.text.length, 0);
|
|
307
400
|
const widthFor = (position: number) => {
|
|
@@ -313,7 +406,7 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
|
|
|
313
406
|
: Math.min(segment.text.length, lineWidth() - usedWidth(position));
|
|
314
407
|
};
|
|
315
408
|
return (
|
|
316
|
-
<box height={1} flexDirection="row" overflow="hidden">
|
|
409
|
+
<box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
|
|
317
410
|
<text width={widthFor(0)} fg={segmentAt(0).fg} content={segmentAt(0).text} />
|
|
318
411
|
<text width={widthFor(1)} fg={segmentAt(1).fg} content={segmentAt(1).text} />
|
|
319
412
|
<text width={widthFor(2)} fg={segmentAt(2).fg} content={segmentAt(2).text} />
|
|
@@ -321,7 +414,7 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
|
|
|
321
414
|
);
|
|
322
415
|
}}
|
|
323
416
|
</Index>
|
|
324
|
-
</
|
|
417
|
+
</scrollbox>
|
|
325
418
|
</box>
|
|
326
419
|
);
|
|
327
420
|
}
|
|
@@ -393,22 +486,41 @@ export function RightPane(props: {
|
|
|
393
486
|
activities: any[];
|
|
394
487
|
logs: string[];
|
|
395
488
|
plan: PlanStep[] | null;
|
|
489
|
+
runSummary?: string | null;
|
|
396
490
|
queueItems: QueueItem[];
|
|
397
491
|
queueInfo: QueueInfo;
|
|
398
492
|
activeTab: 'plan' | 'queue';
|
|
399
493
|
logFilter?: string;
|
|
494
|
+
pendingApprovals: any[];
|
|
495
|
+
onApprove: () => void;
|
|
400
496
|
onTabClick: (tab: 'plan' | 'queue') => void;
|
|
497
|
+
spinnerFrame?: string;
|
|
401
498
|
}) {
|
|
402
499
|
const planJobName = () => activityJobName(
|
|
403
500
|
[...props.activities].reverse().find((activity) => !activity.terminal) ?? props.activities.at(-1),
|
|
404
501
|
);
|
|
405
502
|
return (
|
|
406
|
-
<box
|
|
503
|
+
<box
|
|
504
|
+
width={props.width}
|
|
505
|
+
height="100%"
|
|
506
|
+
flexDirection="column"
|
|
507
|
+
paddingLeft={1}
|
|
508
|
+
paddingTop={1}
|
|
509
|
+
paddingBottom={1}
|
|
510
|
+
overflow="hidden"
|
|
511
|
+
focusable={false}
|
|
512
|
+
>
|
|
407
513
|
<TabHeader active={props.activeTab} queueCount={props.queueInfo.active} width={props.width} onTabClick={props.onTabClick} />
|
|
514
|
+
<Show when={props.pendingApprovals.length > 0}>
|
|
515
|
+
<box height={2} flexDirection="column" border={['left']} borderStyle="heavy" borderColor="#FBBF24" paddingX={1}>
|
|
516
|
+
<text fg="#FBBF24" content={`${props.pendingApprovals.length} approbation(s) requise(s)`} />
|
|
517
|
+
<text fg="#0B1020" bg="#FBBF24" content=" Approuver le run " onMouseUp={props.onApprove} />
|
|
518
|
+
</box>
|
|
519
|
+
</Show>
|
|
408
520
|
<Show when={props.activeTab === 'queue'} fallback={(
|
|
409
521
|
<>
|
|
410
522
|
<Show when={props.plan && props.plan.length > 0}>
|
|
411
|
-
<PlanPanel width={props.width} plan={props.plan!} jobName={planJobName()} />
|
|
523
|
+
<PlanPanel width={props.width} plan={props.plan!} jobName={planJobName()} summary={props.runSummary} spinnerFrame={props.spinnerFrame} />
|
|
412
524
|
</Show>
|
|
413
525
|
<ActivityPanel width={props.width} activities={props.activities} />
|
|
414
526
|
</>
|
|
@@ -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));
|