@dotdrelle/wiki-manager 0.6.17

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.
@@ -0,0 +1,236 @@
1
+ export function parseJsonText(text) {
2
+ try {
3
+ return JSON.parse(text);
4
+ } catch {
5
+ return null;
6
+ }
7
+ }
8
+
9
+ function basename(value) {
10
+ return String(value ?? '').split('/').filter(Boolean).pop() ?? '';
11
+ }
12
+
13
+ function terminalStatus(status) {
14
+ return ['done', 'failed', 'cancelled', 'canceled', 'complete', 'completed', 'success', 'error'].includes(String(status ?? '').toLowerCase());
15
+ }
16
+
17
+ function activityKey(activity) {
18
+ const id = activity?.id ?? activity?.jobId ?? activity?.job_id;
19
+ const source = activity?.source ?? activity?.agent ?? activity?.poll?.server ?? 'mcp';
20
+ return `${source}:${id ?? activity?.kind ?? activity?.label ?? 'activity'}`;
21
+ }
22
+
23
+ function normalizePlanSteps(steps) {
24
+ if (!Array.isArray(steps) || steps.length === 0) return null;
25
+ return steps.map((s, i) => ({
26
+ id: s != null && s.id != null ? String(s.id) : String(i + 1),
27
+ label: s != null ? String(s.label ?? s.description ?? s.name ?? s.id ?? (i + 1)) : String(i + 1),
28
+ }));
29
+ }
30
+
31
+ function normalizePoll(poll) {
32
+ if (!poll || typeof poll !== 'object') return null;
33
+ const server = poll.server ?? poll.source ?? poll.agent;
34
+ const tool = poll.tool ?? poll.name;
35
+ if (!server || !tool) return null;
36
+ return {
37
+ server: String(server),
38
+ tool: String(tool),
39
+ args: poll.args && typeof poll.args === 'object' ? poll.args : {},
40
+ intervalMs: Number.isFinite(Number(poll.intervalMs)) ? Math.max(1000, Number(poll.intervalMs)) : 2500,
41
+ };
42
+ }
43
+
44
+ export function normalizeActivity(activity, fallback = {}) {
45
+ if (!activity || typeof activity !== 'object') return null;
46
+ const id = activity.id ?? activity.jobId ?? activity.job_id ?? fallback.id ?? fallback.jobId ?? null;
47
+ const source = activity.source ?? activity.agent ?? fallback.source ?? activity.poll?.server ?? 'mcp';
48
+ const kind = activity.kind ?? activity.type ?? fallback.kind ?? 'job';
49
+ const status = activity.status ?? activity.state ?? fallback.status ?? 'running';
50
+ const progress = activity.progress && typeof activity.progress === 'object' ? activity.progress : {};
51
+ const step = progress.step ?? progress.phase ?? progress.currentStep ?? activity.step ?? activity.currentStep ?? null;
52
+ const percent = Number.isFinite(Number(progress.percent ?? activity.percent))
53
+ ? Number(progress.percent ?? activity.percent)
54
+ : null;
55
+ const stepId = progress.stepId != null ? String(progress.stepId) : null;
56
+ const stepIndex = Number.isFinite(Number(progress.stepIndex)) ? Number(progress.stepIndex) : null;
57
+ const stepTotal = Number.isFinite(Number(progress.stepTotal)) ? Number(progress.stepTotal) : null;
58
+ const rawPlan = activity.plan && typeof activity.plan === 'object' ? activity.plan : null;
59
+ const planSteps = normalizePlanSteps(rawPlan?.steps);
60
+ const label = activity.label ?? [
61
+ source,
62
+ kind,
63
+ step,
64
+ ].filter(Boolean).join(' ');
65
+ const normalized = {
66
+ key: null,
67
+ id,
68
+ source: String(source),
69
+ kind: String(kind),
70
+ label: String(label || `${source} ${kind}`),
71
+ status: String(status),
72
+ progress: {
73
+ ...progress,
74
+ ...(step ? { step: String(step) } : {}),
75
+ ...(percent !== null ? { percent } : {}),
76
+ ...(stepId !== null ? { stepId } : {}),
77
+ ...(stepIndex !== null ? { stepIndex } : {}),
78
+ ...(stepTotal !== null ? { stepTotal } : {}),
79
+ },
80
+ plan: planSteps ? { steps: planSteps } : null,
81
+ poll: normalizePoll(activity.poll ?? fallback.poll),
82
+ startedAt: activity.startedAt ?? fallback.startedAt ?? null,
83
+ updatedAt: activity.updatedAt ?? new Date().toISOString(),
84
+ error: activity.error ?? null,
85
+ terminal: Boolean(activity.terminal ?? terminalStatus(status)),
86
+ };
87
+ normalized.key = activityKey(normalized);
88
+ return normalized;
89
+ }
90
+
91
+ function productionActivityFromPayload(payload) {
92
+ const progress = payload?.progress;
93
+ const job = payload?.job;
94
+ const jobId = payload?.jobId ?? job?.jobId;
95
+ if (!progress && !job && !jobId) return null;
96
+ const status = job?.status ?? payload?.status ?? progress?.status ?? 'running';
97
+ const percent = Number.isFinite(Number(progress?.percent)) ? Number(progress.percent) : null;
98
+ const sourceCount = Number(progress?.sourceCount);
99
+ const sourceIndex = Number(progress?.sourceIndex);
100
+ const sourceDoneCount = Number(progress?.sourceDoneCount);
101
+ const fileProgress = Number.isFinite(sourceCount) && sourceCount > 0
102
+ ? Number.isFinite(sourceIndex)
103
+ ? `file ${Math.min(sourceCount, sourceIndex + 1)}/${sourceCount}`
104
+ : Number.isFinite(sourceDoneCount)
105
+ ? `files ${Math.min(sourceCount, sourceDoneCount)}/${sourceCount}`
106
+ : null
107
+ : null;
108
+ const batchProgress = progress?.batchCount
109
+ ? `batch ${Number(progress.batchIndex ?? 0) + 1}/${progress.batchCount}`
110
+ : null;
111
+ const progressDetail = batchProgress && /^batch\s+\d+\/\d+/i.test(String(progress?.detail ?? ''))
112
+ ? null
113
+ : progress?.detail;
114
+ const step = progress?.phase ?? progress?.currentStep ?? job?.type ?? 'production';
115
+ const detail = [
116
+ step,
117
+ status,
118
+ percent !== null ? `${Math.round(percent)}%` : null,
119
+ fileProgress,
120
+ batchProgress,
121
+ progress?.source ? basename(progress.source) : null,
122
+ progress?.template ? basename(progress.template) : null,
123
+ progress?.deliverable ? basename(progress.deliverable) : null,
124
+ progressDetail,
125
+ progress?.lastEvent ? `last ${progress.lastEvent}` : null,
126
+ ].filter(Boolean).join(' · ');
127
+ return normalizeActivity({
128
+ id: jobId,
129
+ source: 'production',
130
+ kind: job?.type ?? payload?.type ?? progress?.phase ?? progress?.currentStep ?? 'job',
131
+ label: detail ? `Production: ${detail}` : `Production: ${status}`,
132
+ status,
133
+ progress: {
134
+ ...(progress ?? {}),
135
+ ...(percent !== null ? { percent } : {}),
136
+ step,
137
+ },
138
+ poll: jobId ? {
139
+ server: 'production',
140
+ tool: 'production_job_status',
141
+ args: { jobId },
142
+ intervalMs: 2500,
143
+ } : null,
144
+ error: job?.error ?? payload?.error ?? null,
145
+ });
146
+ }
147
+
148
+ export function extractActivity(payload, context = {}) {
149
+ if (!payload || typeof payload !== 'object') return null;
150
+ if (payload._activity) {
151
+ return normalizeActivity(payload._activity, { source: context.server });
152
+ }
153
+ if (context.server === 'production') {
154
+ return productionActivityFromPayload(payload);
155
+ }
156
+ return null;
157
+ }
158
+
159
+ export function rememberActivity(session, activity) {
160
+ const normalized = normalizeActivity(activity);
161
+ if (!normalized) return null;
162
+ session.activities ??= {};
163
+ session.activities[normalized.key] = {
164
+ ...(session.activities[normalized.key] ?? {}),
165
+ ...normalized,
166
+ };
167
+ if (normalized.source === 'production') {
168
+ session.productionActivity = {
169
+ jobId: normalized.id,
170
+ status: normalized.status,
171
+ label: normalized.label,
172
+ terminal: normalized.terminal,
173
+ updatedAt: normalized.updatedAt,
174
+ };
175
+ }
176
+ return normalized;
177
+ }
178
+
179
+ export function rememberActivityFromPayload(session, payload, context = {}) {
180
+ const activity = extractActivity(payload, context);
181
+ return rememberActivity(session, activity);
182
+ }
183
+
184
+ export function sessionActivities(session) {
185
+ return Object.values(session.activities ?? {})
186
+ .sort((a, b) => String(a.updatedAt ?? '').localeCompare(String(b.updatedAt ?? '')));
187
+ }
188
+
189
+ export function formatActivityLine(activity) {
190
+ if (!activity) return '';
191
+ const percent = Number.isFinite(Number(activity.progress?.percent))
192
+ ? `${Math.round(Number(activity.progress.percent))}%`
193
+ : null;
194
+ const step = activity.progress?.step ?? activity.progress?.phase ?? activity.progress?.currentStep ?? null;
195
+ const parts = [
196
+ activity.label,
197
+ activity.status,
198
+ step && !String(activity.label).includes(String(step)) ? step : null,
199
+ percent && !String(activity.label).includes(percent) ? percent : null,
200
+ activity.error ? `error ${activity.error}` : null,
201
+ ].filter(Boolean);
202
+ return parts.join(' · ');
203
+ }
204
+
205
+ export function formatActivitySummary(source, action, resultText) {
206
+ const text = String(resultText ?? '').trim();
207
+ if (!text) return null;
208
+ const payload = parseJsonText(text);
209
+ const activity = extractActivity(payload, { server: source, tool: action });
210
+ if (activity) return formatActivityLine(activity);
211
+ const jobId = payload?.jobId ?? payload?.job_id ?? payload?.job?.jobId ?? payload?.job?.job_id;
212
+ const status = payload?.status ?? payload?.job?.status ?? payload?.progress?.status;
213
+ const detail = payload?.message ?? payload?.detail ?? payload?.progress?.detail;
214
+ const structured = [status, jobId ? `job ${jobId}` : null, detail].filter(Boolean).join(' · ');
215
+ if (structured) return `${source}.${action}: ${structured}`;
216
+
217
+ const usefulLine = text
218
+ .split('\n')
219
+ .map((line) => line.trim())
220
+ .find((line) => /\b(job|job_id|jobId|status|started|running|done|failed|error|warning|complete|completed|created|indexed)\b/i.test(line))
221
+ ?? text.split('\n').map((line) => line.trim()).find(Boolean);
222
+ return usefulLine ? `${source}.${action}: ${usefulLine.slice(0, 120)}` : null;
223
+ }
224
+
225
+ export function formatActivityError(source, action, err) {
226
+ const message = err instanceof Error ? err.message : String(err);
227
+ const lines = message
228
+ .split('\n')
229
+ .map((line) => line.trim())
230
+ .filter(Boolean);
231
+ const cause = lines.find((line) => /cannot connect to the docker daemon/i.test(line))
232
+ ?? lines.find((line) => /\b(error|failed|cannot|unable|denied|missing|not found)\b/i.test(line))
233
+ ?? lines.at(-1)
234
+ ?? message;
235
+ return `${source}.${action}: error · ${cause.slice(0, 120)}`;
236
+ }
@@ -0,0 +1,127 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { normalizeActivity, extractActivity, rememberActivity, rememberActivityFromPayload } from './activity.js';
4
+
5
+ test('normalizeActivity: plan.steps preserved with id and label', () => {
6
+ const a = normalizeActivity({
7
+ id: 'job-1',
8
+ source: 'prod',
9
+ label: 'Test',
10
+ status: 'running',
11
+ plan: { steps: [{ id: 'extract', label: 'Extraction' }, { id: 'build', label: 'Build' }] },
12
+ progress: {},
13
+ });
14
+ assert.deepEqual(a.plan.steps, [
15
+ { id: 'extract', label: 'Extraction' },
16
+ { id: 'build', label: 'Build' },
17
+ ]);
18
+ });
19
+
20
+ test('normalizeActivity: plan null when steps is empty array', () => {
21
+ const a = normalizeActivity({ id: '1', status: 'running', plan: { steps: [] } });
22
+ assert.equal(a.plan, null);
23
+ });
24
+
25
+ test('normalizeActivity: plan null when no plan field', () => {
26
+ const a = normalizeActivity({ id: '1', status: 'running' });
27
+ assert.equal(a.plan, null);
28
+ });
29
+
30
+ test('normalizeActivity: step id falls back to 1-based index when missing', () => {
31
+ const a = normalizeActivity({
32
+ id: '1',
33
+ status: 'running',
34
+ plan: { steps: [{ label: 'Step A' }, { label: 'Step B' }] },
35
+ });
36
+ assert.equal(a.plan.steps[0].id, '1');
37
+ assert.equal(a.plan.steps[0].label, 'Step A');
38
+ assert.equal(a.plan.steps[1].id, '2');
39
+ });
40
+
41
+ test('normalizeActivity: stepId/stepIndex/stepTotal normalized from progress', () => {
42
+ const a = normalizeActivity({
43
+ id: '1',
44
+ status: 'running',
45
+ progress: { stepId: 'build', stepIndex: 2, stepTotal: 3 },
46
+ });
47
+ assert.equal(a.progress.stepId, 'build');
48
+ assert.equal(a.progress.stepIndex, 2);
49
+ assert.equal(a.progress.stepTotal, 3);
50
+ });
51
+
52
+ test('normalizeActivity: stepId/stepIndex absent when not provided', () => {
53
+ const a = normalizeActivity({ id: '1', status: 'running', progress: {} });
54
+ assert.equal(a.progress.stepId, undefined);
55
+ assert.equal(a.progress.stepIndex, undefined);
56
+ });
57
+
58
+ test('extractActivity: extracts _activity.plan.steps from payload', () => {
59
+ const payload = {
60
+ _activity: {
61
+ id: 'j1',
62
+ source: 'custom',
63
+ label: 'Custom job',
64
+ status: 'running',
65
+ plan: { steps: [{ id: 's1', label: 'Step 1' }] },
66
+ progress: { stepId: 's1', stepIndex: 1, stepTotal: 1 },
67
+ },
68
+ };
69
+ const a = extractActivity(payload);
70
+ assert.equal(a.plan.steps[0].id, 's1');
71
+ assert.equal(a.progress.stepId, 's1');
72
+ assert.equal(a.progress.stepIndex, 1);
73
+ assert.equal(a.progress.stepTotal, 1);
74
+ });
75
+
76
+ test('extractActivity: documents conversion activity carries plan and percent', () => {
77
+ const payload = {
78
+ _activity: {
79
+ id: 'documents:schema.png',
80
+ source: 'documents',
81
+ kind: 'conversion',
82
+ label: 'Documents: conversion schema.png',
83
+ status: 'done',
84
+ progress: { percent: 100, stepId: 'write', stepIndex: 3, stepTotal: 3 },
85
+ plan: {
86
+ steps: [
87
+ { id: 'resolve', label: 'Résoudre le fichier source' },
88
+ { id: 'convert', label: 'Convertir en Markdown' },
89
+ { id: 'write', label: 'Écrire le Markdown converti' },
90
+ ],
91
+ },
92
+ terminal: true,
93
+ },
94
+ };
95
+ const activity = extractActivity(payload, { server: 'documents' });
96
+ assert.equal(activity.source, 'documents');
97
+ assert.equal(activity.progress.percent, 100);
98
+ assert.equal(activity.terminal, true);
99
+ assert.deepEqual(activity.plan.steps.map((step) => step.id), ['resolve', 'convert', 'write']);
100
+ });
101
+
102
+ test('rememberActivity: returns normalized activity on success', () => {
103
+ const session = {};
104
+ const result = rememberActivity(session, { id: '1', status: 'running', source: 'x', kind: 'job' });
105
+ assert.ok(result);
106
+ assert.equal(typeof result, 'object');
107
+ assert.equal(result.status, 'running');
108
+ });
109
+
110
+ test('rememberActivity: returns null for invalid input', () => {
111
+ const session = {};
112
+ assert.equal(rememberActivity(session, null), null);
113
+ assert.equal(rememberActivity(session, 'bad'), null);
114
+ });
115
+
116
+ test('rememberActivityFromPayload: returns activity for _activity payload', () => {
117
+ const session = {};
118
+ const payload = { _activity: { id: '1', source: 'x', status: 'running' } };
119
+ const result = rememberActivityFromPayload(session, payload);
120
+ assert.ok(result);
121
+ assert.equal(result.source, 'x');
122
+ });
123
+
124
+ test('rememberActivityFromPayload: returns null for irrelevant payload', () => {
125
+ const session = {};
126
+ assert.equal(rememberActivityFromPayload(session, { message: 'ok' }), null);
127
+ });
@@ -0,0 +1,238 @@
1
+ import { normalizeActivity } from './activity.js';
2
+ import { syncActivitiesToPlan } from './plan.js';
3
+
4
+ const SESSION_PROJECTION_EVENTS = new Set([
5
+ 'run_started',
6
+ 'plan_set',
7
+ 'plan_step_updated',
8
+ 'activity_upserted',
9
+ 'run_error',
10
+ ]);
11
+
12
+ export function createAgentEvent(type, { origin = 'system', payload = {}, runId = null, turnId = null } = {}) {
13
+ return {
14
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
15
+ ts: new Date().toISOString(),
16
+ type,
17
+ origin,
18
+ runId,
19
+ turnId,
20
+ payload,
21
+ };
22
+ }
23
+
24
+ export function dispatchAgentEvent(session, event) {
25
+ const normalized = event.id && event.ts ? event : createAgentEvent(event.type, event);
26
+ const previousPlan = JSON.stringify(session.headlessPlan ?? null);
27
+ session.agentEvents ??= [];
28
+ session.agentEvents.push(normalized);
29
+ session._agentProjectionState ??= createProjectionState();
30
+ applyEvent(session._agentProjectionState, normalized);
31
+ session.agentProjection = publicProjection(session._agentProjectionState);
32
+ if (SESSION_PROJECTION_EVENTS.has(normalized.type)) {
33
+ applyAgentProjectionToSession(session, session.agentProjection);
34
+ if (JSON.stringify(session.headlessPlan ?? null) !== previousPlan) {
35
+ session._onPlanUpdate?.();
36
+ }
37
+ }
38
+ return normalized;
39
+ }
40
+
41
+ export function reduceAgentEvents(events = []) {
42
+ const state = createProjectionState();
43
+
44
+ for (const event of events) {
45
+ applyEvent(state, event);
46
+ }
47
+
48
+ return publicProjection(state);
49
+ }
50
+
51
+ function createProjectionState() {
52
+ return {
53
+ conversation: [],
54
+ chain: [],
55
+ plan: null,
56
+ activities: {},
57
+ logs: [],
58
+ summary: null,
59
+ status: 'idle',
60
+ };
61
+ }
62
+
63
+ function publicProjection(state) {
64
+ return {
65
+ conversation: state.conversation.map((message) => ({ ...message })),
66
+ chain: state.chain.map((step) => ({ ...step })),
67
+ plan: state.plan ? state.plan.map((step) => ({ ...step })) : null,
68
+ activities: sortedActivities(state.activities).map((activity) => ({ ...activity })),
69
+ logs: [...state.logs],
70
+ summary: state.summary,
71
+ status: state.status,
72
+ };
73
+ }
74
+
75
+ export function applyAgentProjectionToSession(session, projection) {
76
+ session.headlessPlan = projection.plan ? projection.plan.map((step) => ({ ...step })) : null;
77
+ session.activities = Object.fromEntries((projection.activities ?? []).map((activity) => [activity.key, { ...activity }]));
78
+ const production = (projection.activities ?? []).filter((activity) => activity.source === 'production').at(-1);
79
+ session.productionActivity = production ? {
80
+ jobId: production.id,
81
+ status: production.status,
82
+ label: production.label,
83
+ terminal: production.terminal,
84
+ updatedAt: production.updatedAt,
85
+ } : session.productionActivity ?? null;
86
+ }
87
+
88
+ function applyEvent(state, event) {
89
+ switch (event.type) {
90
+ case 'run_started':
91
+ state.status = 'running';
92
+ state.plan = null;
93
+ state.chain = [];
94
+ state.activities = {};
95
+ state.logs = [];
96
+ state.summary = null;
97
+ return;
98
+ case 'user_message':
99
+ state.conversation.push({ role: 'user', content: String(event.payload?.content ?? '') });
100
+ return;
101
+ case 'assistant_message':
102
+ finalizeAssistantMessage(state, String(event.payload?.content ?? ''));
103
+ return;
104
+ case 'assistant_delta':
105
+ appendAssistantDelta(state, String(event.payload?.delta ?? ''));
106
+ return;
107
+ case 'tool_call_started':
108
+ state.chain.push({
109
+ type: 'tool',
110
+ status: 'running',
111
+ callId: event.payload?.callId ?? null,
112
+ name: event.payload?.name ?? null,
113
+ args: event.payload?.args ?? null,
114
+ summary: event.payload?.summary ?? 'calling...',
115
+ });
116
+ return;
117
+ case 'tool_call_result':
118
+ finishToolCall(state, event.payload);
119
+ return;
120
+ case 'activity_upserted':
121
+ upsertActivity(state, event.payload?.activity);
122
+ return;
123
+ case 'plan_set':
124
+ state.plan = normalizePlan(event.payload?.steps, event.payload ?? {});
125
+ return;
126
+ case 'plan_step_updated':
127
+ updatePlanStep(state.plan, event.payload ?? {});
128
+ return;
129
+ case 'run_summary':
130
+ state.summary = String(event.payload?.content ?? '');
131
+ if (state.summary) state.conversation.push({ role: 'assistant', content: state.summary });
132
+ return;
133
+ case 'run_done':
134
+ state.status = 'done';
135
+ return;
136
+ case 'run_error':
137
+ state.status = 'error';
138
+ state.logs.push(String(event.payload?.message ?? 'Agent run failed.'));
139
+ return;
140
+ default:
141
+ return;
142
+ }
143
+ }
144
+
145
+ function appendAssistantDelta(state, delta) {
146
+ if (!delta) return;
147
+ const last = state.conversation.at(-1);
148
+ if (last?.role === 'assistant' && last.streaming) {
149
+ last.content += delta;
150
+ } else {
151
+ state.conversation.push({ role: 'assistant', content: delta, streaming: true });
152
+ }
153
+ }
154
+
155
+ function finalizeAssistantMessage(state, content) {
156
+ const last = state.conversation.at(-1);
157
+ if (last?.role === 'assistant' && last.streaming) {
158
+ last.content = content || last.content;
159
+ delete last.streaming;
160
+ return;
161
+ }
162
+ if (content) state.conversation.push({ role: 'assistant', content });
163
+ }
164
+
165
+ function finishToolCall(state, payload = {}) {
166
+ const callId = payload.callId ?? null;
167
+ const existing = callId ? state.chain.find((step) => step.callId === callId) : null;
168
+ const step = existing ?? {
169
+ type: 'tool',
170
+ callId,
171
+ name: payload.name ?? null,
172
+ };
173
+ step.status = payload.ok === false ? 'failed' : 'done';
174
+ step.result = payload.result ?? null;
175
+ step.summary = payload.summary ?? step.summary ?? step.status;
176
+ if (!existing) state.chain.push(step);
177
+ }
178
+
179
+ function upsertActivity(state, rawActivity) {
180
+ const activity = normalizeActivity(rawActivity);
181
+ if (!activity) return;
182
+ state.activities[activity.key] = {
183
+ ...(state.activities[activity.key] ?? {}),
184
+ ...activity,
185
+ };
186
+ ensurePlanFromActivityProjection(state, activity);
187
+ syncActivitiesToPlan(state.plan, sortedActivities(state.activities));
188
+ }
189
+
190
+ function ensurePlanFromActivityProjection(state, activity) {
191
+ const actKey = activity.key ?? null;
192
+ if (state.plan && actKey !== null && state.plan[0]?._activityKey === actKey) return;
193
+ const steps = activity.plan?.steps;
194
+ if (Array.isArray(steps) && steps.length > 0) {
195
+ state.plan = steps.map((step, i) => ({
196
+ step: i + 1,
197
+ id: step.id ?? null,
198
+ description: step.label,
199
+ status: 'pending',
200
+ _activityKey: activity.key,
201
+ }));
202
+ return;
203
+ }
204
+ state.plan = [{
205
+ step: 1,
206
+ id: null,
207
+ description: activity.label,
208
+ status: 'pending',
209
+ _activityKey: activity.key,
210
+ }];
211
+ }
212
+
213
+ function normalizePlan(steps, payload = {}) {
214
+ if (!Array.isArray(steps)) return null;
215
+ return steps.map((raw, i) => {
216
+ const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
217
+ return {
218
+ step: Number(item.step ?? i + 1),
219
+ id: item.id ?? null,
220
+ description: String(item.description ?? item.label ?? item.name ?? `Step ${i + 1}`),
221
+ status: item.status ?? 'pending',
222
+ _activityKey: item._activityKey ?? payload.activityKey ?? null,
223
+ };
224
+ });
225
+ }
226
+
227
+ function updatePlanStep(plan, payload) {
228
+ if (!plan) return;
229
+ const step = plan.find((item) => item.step === Number(payload.step));
230
+ if (!step) return;
231
+ step.status = payload.status === 'failed' ? 'failed' : payload.status === 'running' ? 'running' : 'done';
232
+ if (payload.activityKey) step.activityKey = payload.activityKey;
233
+ }
234
+
235
+ function sortedActivities(activities) {
236
+ return Object.values(activities ?? {})
237
+ .sort((a, b) => String(a.updatedAt ?? '').localeCompare(String(b.updatedAt ?? '')));
238
+ }