@pellux/goodvibes-tui 0.19.84 → 0.19.86
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5009 -290
- package/package.json +2 -2
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/work-plan-runtime.ts +169 -0
- package/src/input/commands.ts +2 -0
- package/src/main.ts +4 -13
- package/src/panels/builtin/agent.ts +11 -0
- package/src/panels/builtin/shared.ts +8 -0
- package/src/panels/work-plan-panel.ts +175 -0
- package/src/renderer/process-modal.ts +383 -26
- package/src/renderer/process-summary.ts +67 -0
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/services.ts +8 -0
- package/src/runtime/ui-services.ts +2 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +373 -0
|
@@ -14,6 +14,8 @@ export interface ProcessEntry {
|
|
|
14
14
|
id: string;
|
|
15
15
|
/** Display label (agent task or exec command) */
|
|
16
16
|
label: string;
|
|
17
|
+
/** Tree prefix for child processes, e.g. "└─ " under a WRFC owner. */
|
|
18
|
+
treePrefix?: string;
|
|
17
19
|
/** Process type */
|
|
18
20
|
type: 'agent' | 'exec';
|
|
19
21
|
/** Current status string */
|
|
@@ -31,12 +33,32 @@ const MAX_LABEL_LENGTH = 80;
|
|
|
31
33
|
/** Border and margin width subtracted from terminal width to get modal content width. */
|
|
32
34
|
const MODAL_BORDER_WIDTH = 8;
|
|
33
35
|
|
|
36
|
+
const WRFC_ROLE_ORDER: Record<string, number> = {
|
|
37
|
+
owner: 0,
|
|
38
|
+
engineer: 1,
|
|
39
|
+
reviewer: 2,
|
|
40
|
+
fixer: 3,
|
|
41
|
+
verifier: 4,
|
|
42
|
+
};
|
|
43
|
+
|
|
34
44
|
export interface ProcessModalDeps {
|
|
35
45
|
readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'cancel'>;
|
|
36
46
|
readonly processManager: Pick<ProcessManager, 'list' | 'getStatus' | 'stop'>;
|
|
37
|
-
readonly wrfcController: Pick<WrfcController, 'getChain'
|
|
47
|
+
readonly wrfcController: Pick<WrfcController, 'getChain'> & Partial<Pick<WrfcController, 'listChains'>>;
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
type WrfcChainLike = {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly state: string;
|
|
53
|
+
readonly task: string;
|
|
54
|
+
readonly ownerAgentId: string;
|
|
55
|
+
readonly engineerAgentId?: string;
|
|
56
|
+
readonly reviewerAgentId?: string;
|
|
57
|
+
readonly fixerAgentId?: string;
|
|
58
|
+
readonly allAgentIds?: readonly string[];
|
|
59
|
+
readonly constraints?: readonly unknown[];
|
|
60
|
+
};
|
|
61
|
+
|
|
40
62
|
/** Build a display label for an agent based on its task and template. */
|
|
41
63
|
function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
|
|
42
64
|
const task = rec.task;
|
|
@@ -44,6 +66,21 @@ function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
|
|
|
44
66
|
// Look up the original task from the WRFC chain if available
|
|
45
67
|
const originalTask = getChainTask(rec.wrfcId, deps);
|
|
46
68
|
|
|
69
|
+
if (rec.wrfcRole === 'owner') {
|
|
70
|
+
const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
|
|
71
|
+
return `[WRFC owner] ${desc}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (rec.wrfcRole === 'engineer') {
|
|
75
|
+
const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 11);
|
|
76
|
+
return `[Engineer] ${desc}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (rec.wrfcRole === 'verifier') {
|
|
80
|
+
const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
|
|
81
|
+
return `[Verifier] ${desc}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
47
84
|
// WRFC Review agent
|
|
48
85
|
if (task.startsWith('WRFC Review Request')) {
|
|
49
86
|
const thresholdMatch = task.match(/threshold is (\d+(?:\.\d+)?)/);
|
|
@@ -61,8 +98,8 @@ function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
|
|
|
61
98
|
const attempt = attemptMatch ? attemptMatch[1] : '?';
|
|
62
99
|
const desc = truncateFirst(originalTask ?? 'fix in progress', 45);
|
|
63
100
|
// Show constraint count when the chain has constraints to target (SDK 0.23.0)
|
|
64
|
-
const chain = rec.wrfcId ? (
|
|
65
|
-
const constraintCount = chain && chain.constraints
|
|
101
|
+
const chain = rec.wrfcId ? safeGetChain(rec.wrfcId, deps) : null;
|
|
102
|
+
const constraintCount = chain && (chain.constraints?.length ?? 0) > 0 ? chain.constraints?.length ?? 0 : 0;
|
|
66
103
|
const constraintSuffix = constraintCount > 0 ? ` [${constraintCount}c]` : '';
|
|
67
104
|
return `[Fix #${attempt}] ${desc} (${fromScore} \u2192 ${toScore}/10)${constraintSuffix}`;
|
|
68
105
|
}
|
|
@@ -77,13 +114,331 @@ function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
|
|
|
77
114
|
return `[${tag}] ${truncateFirst(task, maxDesc)}`;
|
|
78
115
|
}
|
|
79
116
|
|
|
117
|
+
function isActiveAgent(rec: AgentRecord): boolean {
|
|
118
|
+
return rec.status !== 'completed' && rec.status !== 'failed' && rec.status !== 'cancelled';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isActiveWrfcState(state: string): boolean {
|
|
122
|
+
return state !== 'passed' && state !== 'failed';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getStreamSnippet(rec: AgentRecord): string | undefined {
|
|
126
|
+
if (!rec.streamingContent) return undefined;
|
|
127
|
+
const raw = rec.streamingContent.replace(/\n/g, ' ').trim();
|
|
128
|
+
return raw.length > 60 ? '...' + raw.slice(-57) : raw;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compareAgents(a: AgentRecord, b: AgentRecord): number {
|
|
132
|
+
const roleDelta = (WRFC_ROLE_ORDER[a.wrfcRole ?? ''] ?? 50) - (WRFC_ROLE_ORDER[b.wrfcRole ?? ''] ?? 50);
|
|
133
|
+
if (roleDelta !== 0) return roleDelta;
|
|
134
|
+
return a.startedAt - b.startedAt || a.id.localeCompare(b.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildAgentEntry(
|
|
138
|
+
rec: AgentRecord,
|
|
139
|
+
deps: ProcessModalDeps,
|
|
140
|
+
now: number,
|
|
141
|
+
treePrefix = '',
|
|
142
|
+
): ProcessEntry {
|
|
143
|
+
return {
|
|
144
|
+
id: rec.id,
|
|
145
|
+
label: buildAgentLabel(rec, deps),
|
|
146
|
+
treePrefix,
|
|
147
|
+
type: 'agent',
|
|
148
|
+
status: rec.status,
|
|
149
|
+
elapsedMs: now - rec.startedAt,
|
|
150
|
+
streamSnippet: getStreamSnippet(rec),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function appendAgentSubtree(
|
|
155
|
+
result: ProcessEntry[],
|
|
156
|
+
rec: AgentRecord,
|
|
157
|
+
childrenByParent: Map<string, AgentRecord[]>,
|
|
158
|
+
deps: ProcessModalDeps,
|
|
159
|
+
now: number,
|
|
160
|
+
prefix: string,
|
|
161
|
+
connector: string,
|
|
162
|
+
visited: Set<string>,
|
|
163
|
+
): void {
|
|
164
|
+
if (visited.has(rec.id)) return;
|
|
165
|
+
visited.add(rec.id);
|
|
166
|
+
result.push(buildAgentEntry(rec, deps, now, `${prefix}${connector}`));
|
|
167
|
+
|
|
168
|
+
const children = (childrenByParent.get(rec.id) ?? []).slice().sort(compareAgents);
|
|
169
|
+
const descendantPrefix = connector === '├─ ' ? '│ ' : connector === '└─ ' ? ' ' : '';
|
|
170
|
+
children.forEach((child, index) => {
|
|
171
|
+
const last = index === children.length - 1;
|
|
172
|
+
appendAgentSubtree(
|
|
173
|
+
result,
|
|
174
|
+
child,
|
|
175
|
+
childrenByParent,
|
|
176
|
+
deps,
|
|
177
|
+
now,
|
|
178
|
+
`${prefix}${descendantPrefix}`,
|
|
179
|
+
last ? '└─ ' : '├─ ',
|
|
180
|
+
visited,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function appendAgentGroupEntries(
|
|
186
|
+
result: ProcessEntry[],
|
|
187
|
+
records: AgentRecord[],
|
|
188
|
+
deps: ProcessModalDeps,
|
|
189
|
+
now: number,
|
|
190
|
+
): void {
|
|
191
|
+
const group = records.slice().sort(compareAgents);
|
|
192
|
+
const byId = new Map(group.map((rec) => [rec.id, rec]));
|
|
193
|
+
const childrenByParent = new Map<string, AgentRecord[]>();
|
|
194
|
+
|
|
195
|
+
for (const rec of group) {
|
|
196
|
+
if (!rec.parentAgentId || !byId.has(rec.parentAgentId)) continue;
|
|
197
|
+
const children = childrenByParent.get(rec.parentAgentId) ?? [];
|
|
198
|
+
children.push(rec);
|
|
199
|
+
childrenByParent.set(rec.parentAgentId, children);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const chain = group[0]?.wrfcId ? safeGetChain(group[0].wrfcId, deps) : null;
|
|
203
|
+
const owner = group.find((rec) => rec.id === chain?.ownerAgentId)
|
|
204
|
+
?? group.find((rec) => rec.wrfcRole === 'owner');
|
|
205
|
+
const roots = owner
|
|
206
|
+
? [owner]
|
|
207
|
+
: group.filter((rec) => !rec.parentAgentId || !byId.has(rec.parentAgentId));
|
|
208
|
+
const visited = new Set<string>();
|
|
209
|
+
|
|
210
|
+
roots.forEach((root, index) => {
|
|
211
|
+
const connector = owner || roots.length === 1 ? '' : (index === roots.length - 1 ? '└─ ' : '├─ ');
|
|
212
|
+
appendAgentSubtree(result, root, childrenByParent, deps, now, '', connector, visited);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const leftovers = group.filter((rec) => !visited.has(rec.id));
|
|
216
|
+
leftovers.forEach((rec, index) => {
|
|
217
|
+
appendAgentSubtree(
|
|
218
|
+
result,
|
|
219
|
+
rec,
|
|
220
|
+
childrenByParent,
|
|
221
|
+
deps,
|
|
222
|
+
now,
|
|
223
|
+
'',
|
|
224
|
+
index === leftovers.length - 1 ? '└─ ' : '├─ ',
|
|
225
|
+
visited,
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildAgentEntries(
|
|
231
|
+
agents: AgentRecord[],
|
|
232
|
+
deps: ProcessModalDeps,
|
|
233
|
+
now: number,
|
|
234
|
+
getGroupOrder?: (key: string) => number | undefined,
|
|
235
|
+
ensureGroupOrder?: (key: string) => number,
|
|
236
|
+
): ProcessEntry[] {
|
|
237
|
+
const result: ProcessEntry[] = [];
|
|
238
|
+
const displayAgents = prepareAgentRecordsForDisplay(agents, deps);
|
|
239
|
+
const activeById = new Map(displayAgents.map((agent) => [agent.id, agent]));
|
|
240
|
+
const groups = new Map<string, AgentRecord[]>();
|
|
241
|
+
|
|
242
|
+
for (const agent of displayAgents) {
|
|
243
|
+
const groupKey = getAgentGroupKey(agent, activeById);
|
|
244
|
+
const group = groups.get(groupKey) ?? [];
|
|
245
|
+
group.push(agent);
|
|
246
|
+
groups.set(groupKey, group);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const sortedGroups = Array.from(groups.entries()).sort(([aKey, a], [bKey, b]) => {
|
|
250
|
+
const aOrder = getGroupOrder?.(aKey);
|
|
251
|
+
const bOrder = getGroupOrder?.(bKey);
|
|
252
|
+
if (aOrder !== undefined || bOrder !== undefined) {
|
|
253
|
+
if (aOrder === undefined) return 1;
|
|
254
|
+
if (bOrder === undefined) return -1;
|
|
255
|
+
return aOrder - bOrder;
|
|
256
|
+
}
|
|
257
|
+
const aStarted = Math.min(...a.map((rec) => rec.startedAt));
|
|
258
|
+
const bStarted = Math.min(...b.map((rec) => rec.startedAt));
|
|
259
|
+
return aStarted - bStarted || aKey.localeCompare(bKey);
|
|
260
|
+
});
|
|
261
|
+
for (const [key, group] of sortedGroups) {
|
|
262
|
+
ensureGroupOrder?.(key);
|
|
263
|
+
appendAgentGroupEntries(result, group, deps, now);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function prepareAgentRecordsForDisplay(agents: AgentRecord[], deps: ProcessModalDeps): AgentRecord[] {
|
|
270
|
+
const chains = listWrfcChains(deps);
|
|
271
|
+
const agentById = new Map(agents.map((agent) => [agent.id, agent]));
|
|
272
|
+
const normalizedById = new Map<string, AgentRecord>();
|
|
273
|
+
|
|
274
|
+
for (const agent of agents) {
|
|
275
|
+
if (!isActiveAgent(agent)) continue;
|
|
276
|
+
normalizedById.set(agent.id, normalizeWrfcAgentRecord(agent, chains));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A WRFC owner is the durable root of the chain. Keep it visible until the
|
|
280
|
+
// chain itself is terminal, even if the underlying owner agent has already
|
|
281
|
+
// emitted a completed phase event before reviewer/fixer/gate work finishes.
|
|
282
|
+
for (const chain of chains) {
|
|
283
|
+
if (!isActiveWrfcState(chain.state)) continue;
|
|
284
|
+
const owner = agentById.get(chain.ownerAgentId);
|
|
285
|
+
if (!owner || normalizedById.has(owner.id)) continue;
|
|
286
|
+
const chainHasActiveMember = agents.some((agent) =>
|
|
287
|
+
agent.id !== owner.id
|
|
288
|
+
&& isActiveAgent(agent)
|
|
289
|
+
&& isAgentInChain(agent, chain)
|
|
290
|
+
);
|
|
291
|
+
if (!chainHasActiveMember) continue;
|
|
292
|
+
normalizedById.set(owner.id, normalizeWrfcAgentRecord({
|
|
293
|
+
...owner,
|
|
294
|
+
status: 'running',
|
|
295
|
+
completedAt: undefined,
|
|
296
|
+
progress: owner.progress ?? `WRFC chain ${chain.state}`,
|
|
297
|
+
}, chains));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const normalized = Array.from(normalizedById.values());
|
|
301
|
+
return inferDuplicateWrfcOwnerRows(normalized);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeWrfcAgentRecord(agent: AgentRecord, chains: WrfcChainLike[]): AgentRecord {
|
|
305
|
+
const chain = findChainForAgent(agent, chains);
|
|
306
|
+
if (!chain) return agent;
|
|
307
|
+
|
|
308
|
+
const role = inferWrfcRole(agent, chain);
|
|
309
|
+
const parentAgentId = role && role !== 'owner'
|
|
310
|
+
? agent.parentAgentId ?? chain.ownerAgentId
|
|
311
|
+
: agent.parentAgentId;
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
...agent,
|
|
315
|
+
wrfcId: agent.wrfcId ?? chain.id,
|
|
316
|
+
wrfcRole: agent.wrfcRole ?? role,
|
|
317
|
+
parentAgentId,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function inferDuplicateWrfcOwnerRows(agents: AgentRecord[]): AgentRecord[] {
|
|
322
|
+
const byTask = new Map<string, AgentRecord[]>();
|
|
323
|
+
for (const agent of agents) {
|
|
324
|
+
if (agent.wrfcId || agent.wrfcRole || agent.parentAgentId) continue;
|
|
325
|
+
if (agent.reviewMode !== 'wrfc') continue;
|
|
326
|
+
const key = agent.task.trim();
|
|
327
|
+
if (!key) continue;
|
|
328
|
+
const group = byTask.get(key) ?? [];
|
|
329
|
+
group.push(agent);
|
|
330
|
+
byTask.set(key, group);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const inferredIds = new Set<string>();
|
|
334
|
+
const inferred = new Map<string, AgentRecord>();
|
|
335
|
+
for (const [task, group] of byTask) {
|
|
336
|
+
if (group.length < 2) continue;
|
|
337
|
+
const sorted = group.slice().sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id));
|
|
338
|
+
const owner = sorted[0]!;
|
|
339
|
+
const syntheticWrfcId = `inferred:${owner.id}`;
|
|
340
|
+
inferred.set(owner.id, {
|
|
341
|
+
...owner,
|
|
342
|
+
wrfcId: syntheticWrfcId,
|
|
343
|
+
wrfcRole: 'owner',
|
|
344
|
+
});
|
|
345
|
+
inferredIds.add(owner.id);
|
|
346
|
+
for (const child of sorted.slice(1)) {
|
|
347
|
+
inferred.set(child.id, {
|
|
348
|
+
...child,
|
|
349
|
+
wrfcId: syntheticWrfcId,
|
|
350
|
+
wrfcRole: child.template === 'reviewer' ? 'reviewer' : 'engineer',
|
|
351
|
+
parentAgentId: owner.id,
|
|
352
|
+
});
|
|
353
|
+
inferredIds.add(child.id);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Avoid accidentally grouping unrelated long-running WRFC roots that just
|
|
357
|
+
// happen to share an empty or generic task after this exact duplicate group.
|
|
358
|
+
byTask.delete(task);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (inferredIds.size === 0) return agents;
|
|
362
|
+
return agents.map((agent) => inferred.get(agent.id) ?? agent);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function listWrfcChains(deps: ProcessModalDeps): WrfcChainLike[] {
|
|
366
|
+
const controller = deps.wrfcController as ProcessModalDeps['wrfcController'] & {
|
|
367
|
+
listChains?: () => unknown;
|
|
368
|
+
};
|
|
369
|
+
if (typeof controller.listChains !== 'function') return [];
|
|
370
|
+
try {
|
|
371
|
+
const value = controller.listChains();
|
|
372
|
+
return Array.isArray(value) ? value.filter(isWrfcChainLike) : [];
|
|
373
|
+
} catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isWrfcChainLike(value: unknown): value is WrfcChainLike {
|
|
379
|
+
if (!value || typeof value !== 'object') return false;
|
|
380
|
+
const record = value as Record<string, unknown>;
|
|
381
|
+
return typeof record.id === 'string'
|
|
382
|
+
&& typeof record.state === 'string'
|
|
383
|
+
&& typeof record.task === 'string'
|
|
384
|
+
&& typeof record.ownerAgentId === 'string';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function findChainForAgent(agent: AgentRecord, chains: WrfcChainLike[]): WrfcChainLike | null {
|
|
388
|
+
if (agent.wrfcId) {
|
|
389
|
+
const direct = chains.find((chain) => chain.id === agent.wrfcId);
|
|
390
|
+
if (direct) return direct;
|
|
391
|
+
}
|
|
392
|
+
return chains.find((chain) => isAgentInChain(agent, chain)) ?? null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isAgentInChain(agent: AgentRecord, chain: WrfcChainLike): boolean {
|
|
396
|
+
return chain.ownerAgentId === agent.id
|
|
397
|
+
|| chain.engineerAgentId === agent.id
|
|
398
|
+
|| chain.reviewerAgentId === agent.id
|
|
399
|
+
|| chain.fixerAgentId === agent.id
|
|
400
|
+
|| (chain.allAgentIds?.includes(agent.id) ?? false)
|
|
401
|
+
|| agent.wrfcId === chain.id;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function inferWrfcRole(agent: AgentRecord, chain: WrfcChainLike): AgentRecord['wrfcRole'] {
|
|
405
|
+
if (agent.wrfcRole) return agent.wrfcRole;
|
|
406
|
+
if (chain.ownerAgentId === agent.id) return 'owner';
|
|
407
|
+
if (chain.engineerAgentId === agent.id) return 'engineer';
|
|
408
|
+
if (chain.reviewerAgentId === agent.id) return 'reviewer';
|
|
409
|
+
if (chain.fixerAgentId === agent.id) return 'fixer';
|
|
410
|
+
if (agent.template === 'reviewer') return 'reviewer';
|
|
411
|
+
return 'engineer';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function getAgentGroupKey(agent: AgentRecord, activeById: Map<string, AgentRecord>): string {
|
|
415
|
+
if (agent.wrfcId) return `wrfc:${agent.wrfcId}`;
|
|
416
|
+
|
|
417
|
+
const seen = new Set<string>();
|
|
418
|
+
let root = agent;
|
|
419
|
+
while (root.parentAgentId && activeById.has(root.parentAgentId) && !seen.has(root.parentAgentId)) {
|
|
420
|
+
seen.add(root.id);
|
|
421
|
+
root = activeById.get(root.parentAgentId)!;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If the active root is an orphaned child, keep it anchored to its missing parent id
|
|
425
|
+
// so it does not jump to a new group when the parent exits before its children.
|
|
426
|
+
return `root:${root.parentAgentId ?? root.id}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function safeGetChain(wrfcId: string, deps: Pick<ProcessModalDeps, 'wrfcController'>): WrfcChainLike | null {
|
|
430
|
+
try {
|
|
431
|
+
const chain = deps.wrfcController.getChain(wrfcId);
|
|
432
|
+
return isWrfcChainLike(chain) ? chain : null;
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
80
438
|
/** Get the original task description from a WRFC chain. */
|
|
81
439
|
function getChainTask(wrfcId: string | undefined, deps: Pick<ProcessModalDeps, 'wrfcController'>): string | null {
|
|
82
440
|
if (!wrfcId) return null;
|
|
83
|
-
|
|
84
|
-
const chain = deps.wrfcController.getChain(wrfcId);
|
|
85
|
-
return chain?.task ?? null;
|
|
86
|
-
} catch { return null; }
|
|
441
|
+
return safeGetChain(wrfcId, deps)?.task ?? null;
|
|
87
442
|
}
|
|
88
443
|
|
|
89
444
|
/** Truncate to first line, capped at max chars. */
|
|
@@ -113,6 +468,8 @@ export class ProcessModal {
|
|
|
113
468
|
public entries: ProcessEntry[] = [];
|
|
114
469
|
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
115
470
|
private onRefresh: (() => void) | null = null;
|
|
471
|
+
private groupOrder = new Map<string, number>();
|
|
472
|
+
private nextGroupOrder = 0;
|
|
116
473
|
|
|
117
474
|
constructor(private readonly deps: ProcessModalDeps) {}
|
|
118
475
|
|
|
@@ -147,23 +504,14 @@ export class ProcessModal {
|
|
|
147
504
|
const now = Date.now();
|
|
148
505
|
const result: ProcessEntry[] = [];
|
|
149
506
|
|
|
150
|
-
// Agents — only show active (pending/running)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
result.push({
|
|
159
|
-
id: a.id,
|
|
160
|
-
label: buildAgentLabel(a, this.deps),
|
|
161
|
-
type: 'agent',
|
|
162
|
-
status: a.status,
|
|
163
|
-
elapsedMs: now - a.startedAt,
|
|
164
|
-
streamSnippet,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
507
|
+
// Agents — only show active (pending/running), grouped by stable parent/child hierarchy.
|
|
508
|
+
result.push(...buildAgentEntries(
|
|
509
|
+
manager.list(),
|
|
510
|
+
this.deps,
|
|
511
|
+
now,
|
|
512
|
+
(key) => this.groupOrder.get(key),
|
|
513
|
+
(key) => this.ensureGroupOrder(key),
|
|
514
|
+
));
|
|
167
515
|
|
|
168
516
|
// Background exec processes — only show running
|
|
169
517
|
const pm = this.deps.processManager;
|
|
@@ -187,6 +535,14 @@ export class ProcessModal {
|
|
|
187
535
|
}
|
|
188
536
|
}
|
|
189
537
|
|
|
538
|
+
private ensureGroupOrder(key: string): number {
|
|
539
|
+
const existing = this.groupOrder.get(key);
|
|
540
|
+
if (existing !== undefined) return existing;
|
|
541
|
+
const next = this.nextGroupOrder++;
|
|
542
|
+
this.groupOrder.set(key, next);
|
|
543
|
+
return next;
|
|
544
|
+
}
|
|
545
|
+
|
|
190
546
|
moveUp(): void {
|
|
191
547
|
if (this.entries.length === 0) return;
|
|
192
548
|
this.selectedIndex = (this.selectedIndex - 1 + this.entries.length) % this.entries.length;
|
|
@@ -270,9 +626,10 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
|
|
|
270
626
|
const dur = formatDuration(e.elapsedMs);
|
|
271
627
|
const statusStr = e.streamSnippet ? `streaming ${dur}` : `${e.status} ${dur}`;
|
|
272
628
|
const suffix = ` ${statusStr}`;
|
|
273
|
-
const
|
|
629
|
+
const treePrefix = e.treePrefix ?? '';
|
|
630
|
+
const maxDescW = maxLabelW - typeTag.length - treePrefix.length - suffix.length - 4; // icon + spaces
|
|
274
631
|
const desc = e.label.length > maxDescW ? e.label.slice(0, Math.max(0, maxDescW - 3)) + '...' : e.label;
|
|
275
|
-
const label = `${statusIcon} ${typeTag} ${desc}${suffix}`;
|
|
632
|
+
const label = `${statusIcon} ${typeTag} ${treePrefix}${desc}${suffix}`;
|
|
276
633
|
return {
|
|
277
634
|
label,
|
|
278
635
|
selected: absoluteIndex === modal.selectedIndex,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type ProcessSummaryAgent = {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly progress?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type RuntimeProcessSummaryAgent = {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly latestProgress?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type WrfcProcessSummaryChain = {
|
|
12
|
+
readonly state?: string;
|
|
13
|
+
readonly ownerAgentId?: string;
|
|
14
|
+
readonly engineerAgentId?: string;
|
|
15
|
+
readonly reviewerAgentId?: string;
|
|
16
|
+
readonly fixerAgentId?: string;
|
|
17
|
+
readonly allAgentIds?: readonly unknown[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type RunningAgentSummary = {
|
|
21
|
+
readonly count: number;
|
|
22
|
+
readonly progress?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function summarizeRunningAgents(
|
|
26
|
+
managerAgents: readonly ProcessSummaryAgent[],
|
|
27
|
+
runtimeAgents: readonly RuntimeProcessSummaryAgent[],
|
|
28
|
+
wrfcChains: readonly WrfcProcessSummaryChain[],
|
|
29
|
+
): RunningAgentSummary {
|
|
30
|
+
const runningAgentIds = new Set<string>();
|
|
31
|
+
let progress: string | undefined;
|
|
32
|
+
|
|
33
|
+
for (const agent of managerAgents) {
|
|
34
|
+
runningAgentIds.add(agent.id);
|
|
35
|
+
if (!progress && agent.progress) progress = agent.progress;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const agent of runtimeAgents) {
|
|
39
|
+
runningAgentIds.add(agent.id);
|
|
40
|
+
if (!progress && agent.latestProgress) progress = agent.latestProgress;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const chain of wrfcChains) {
|
|
44
|
+
if (isTerminalWrfcState(chain.state)) continue;
|
|
45
|
+
const chainAgentIds = collectChainAgentIds(chain);
|
|
46
|
+
const hasVisibleChainWork = Array.from(chainAgentIds).some((id) => runningAgentIds.has(id));
|
|
47
|
+
if (!hasVisibleChainWork || !chain.ownerAgentId) continue;
|
|
48
|
+
runningAgentIds.add(chain.ownerAgentId);
|
|
49
|
+
if (!progress) progress = `WRFC chain ${chain.state ?? 'running'}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { count: runningAgentIds.size, progress };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isTerminalWrfcState(state: string | undefined): boolean {
|
|
56
|
+
return state === 'passed' || state === 'failed';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectChainAgentIds(chain: WrfcProcessSummaryChain): Set<string> {
|
|
60
|
+
return new Set([
|
|
61
|
+
chain.ownerAgentId,
|
|
62
|
+
chain.engineerAgentId,
|
|
63
|
+
chain.reviewerAgentId,
|
|
64
|
+
chain.fixerAgentId,
|
|
65
|
+
...(chain.allAgentIds ?? []),
|
|
66
|
+
].filter((id): id is string => typeof id === 'string' && id.length > 0));
|
|
67
|
+
}
|
|
@@ -74,6 +74,7 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
74
74
|
knowledgeService?: KnowledgeService;
|
|
75
75
|
projectPlanningService?: import('@pellux/goodvibes-sdk/platform/knowledge').ProjectPlanningService;
|
|
76
76
|
projectPlanningProjectId?: string;
|
|
77
|
+
workPlanStore?: import('../work-plans/work-plan-store.ts').WorkPlanStore;
|
|
77
78
|
providerOptimizer?: import('@pellux/goodvibes-sdk/platform/providers').ProviderOptimizer;
|
|
78
79
|
pluginManager?: PluginManager;
|
|
79
80
|
hookWorkbench?: HookWorkbench;
|
|
@@ -142,6 +143,7 @@ export function createBootstrapCommandContext(
|
|
|
142
143
|
knowledgeService,
|
|
143
144
|
projectPlanningService,
|
|
144
145
|
projectPlanningProjectId,
|
|
146
|
+
workPlanStore,
|
|
145
147
|
providerOptimizer,
|
|
146
148
|
pluginManager,
|
|
147
149
|
hookWorkbench,
|
|
@@ -233,6 +235,7 @@ export function createBootstrapCommandContext(
|
|
|
233
235
|
bookmarkManager,
|
|
234
236
|
projectPlanningService,
|
|
235
237
|
projectPlanningProjectId,
|
|
238
|
+
workPlanStore,
|
|
236
239
|
}, shellServices);
|
|
237
240
|
const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService }, shellServices);
|
|
238
241
|
const extensions = createBootstrapCommandExtensionsSection({
|
|
@@ -87,6 +87,7 @@ export interface BootstrapCommandSectionOptions {
|
|
|
87
87
|
readonly knowledgeService?: KnowledgeService;
|
|
88
88
|
readonly projectPlanningService?: import('@pellux/goodvibes-sdk/platform/knowledge').ProjectPlanningService;
|
|
89
89
|
readonly projectPlanningProjectId?: string;
|
|
90
|
+
readonly workPlanStore?: import('../work-plans/work-plan-store.ts').WorkPlanStore;
|
|
90
91
|
readonly pluginManager?: PluginManager;
|
|
91
92
|
readonly hookWorkbench?: HookWorkbench;
|
|
92
93
|
readonly providerOptimizer?: import('@pellux/goodvibes-sdk/platform/providers').ProviderOptimizer;
|
|
@@ -313,7 +314,7 @@ export function createBootstrapCommandWorkspaceSection(
|
|
|
313
314
|
options: Pick<
|
|
314
315
|
BootstrapCommandSectionOptions,
|
|
315
316
|
'keybindingsManager' | 'fileUndoManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
|
|
316
|
-
| 'projectPlanningService' | 'projectPlanningProjectId'
|
|
317
|
+
| 'projectPlanningService' | 'projectPlanningProjectId' | 'workPlanStore'
|
|
317
318
|
>,
|
|
318
319
|
shellServices: BootstrapCommandShellServices,
|
|
319
320
|
): BootstrapCommandWorkspaceSection {
|
|
@@ -325,6 +326,7 @@ export function createBootstrapCommandWorkspaceSection(
|
|
|
325
326
|
bookmarkManager: options.bookmarkManager,
|
|
326
327
|
projectPlanningService: options.projectPlanningService,
|
|
327
328
|
projectPlanningProjectId: options.projectPlanningProjectId,
|
|
329
|
+
workPlanStore: options.workPlanStore,
|
|
328
330
|
...shellServices.workspace,
|
|
329
331
|
};
|
|
330
332
|
}
|
|
@@ -205,6 +205,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
205
205
|
knowledgeService: services.knowledgeService,
|
|
206
206
|
projectPlanningService: services.projectPlanningService,
|
|
207
207
|
projectPlanningProjectId: services.projectPlanningProjectId,
|
|
208
|
+
workPlanStore: services.workPlanStore,
|
|
208
209
|
providerOptimizer: services.providerOptimizer,
|
|
209
210
|
pluginManager: services.pluginManager,
|
|
210
211
|
hookWorkbench: services.hookWorkbench,
|
package/src/runtime/services.ts
CHANGED
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
createWorkflowServices,
|
|
85
85
|
type WorkflowServices,
|
|
86
86
|
} from '@pellux/goodvibes-sdk/platform/tools';
|
|
87
|
+
import { WorkPlanStore } from '../work-plans/work-plan-store.ts';
|
|
87
88
|
|
|
88
89
|
const REGULAR_KNOWLEDGE_DB_FILE = 'knowledge-wiki.sqlite';
|
|
89
90
|
const HOME_GRAPH_KNOWLEDGE_DB_FILE = 'knowledge-home-graph.sqlite';
|
|
@@ -171,6 +172,7 @@ export interface RuntimeServices {
|
|
|
171
172
|
readonly homeGraphService: HomeGraphService;
|
|
172
173
|
readonly projectPlanningService: ProjectPlanningService;
|
|
173
174
|
readonly projectPlanningProjectId: string;
|
|
175
|
+
readonly workPlanStore: WorkPlanStore;
|
|
174
176
|
readonly memoryStore: MemoryStore;
|
|
175
177
|
readonly memoryRegistry: MemoryRegistry;
|
|
176
178
|
readonly serviceRegistry: ServiceRegistry;
|
|
@@ -447,6 +449,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
447
449
|
const projectPlanningService = new ProjectPlanningService(knowledgeStore, {
|
|
448
450
|
defaultProjectId: projectPlanningProjectId,
|
|
449
451
|
});
|
|
452
|
+
const workPlanStore = new WorkPlanStore({
|
|
453
|
+
homeDirectory,
|
|
454
|
+
projectId: projectPlanningProjectId,
|
|
455
|
+
projectRoot: workingDirectory,
|
|
456
|
+
});
|
|
450
457
|
const voiceProviders = new VoiceProviderRegistry();
|
|
451
458
|
ensureBuiltinVoiceProviders(voiceProviders);
|
|
452
459
|
const voiceService = new VoiceService(voiceProviders);
|
|
@@ -596,6 +603,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
596
603
|
homeGraphService,
|
|
597
604
|
projectPlanningService,
|
|
598
605
|
projectPlanningProjectId,
|
|
606
|
+
workPlanStore,
|
|
599
607
|
memoryStore,
|
|
600
608
|
memoryRegistry,
|
|
601
609
|
serviceRegistry,
|
|
@@ -81,6 +81,7 @@ export interface UiPlanningServices {
|
|
|
81
81
|
readonly adaptivePlanner: RuntimeServices['adaptivePlanner'];
|
|
82
82
|
readonly projectPlanningService: RuntimeServices['projectPlanningService'];
|
|
83
83
|
readonly projectPlanningProjectId: RuntimeServices['projectPlanningProjectId'];
|
|
84
|
+
readonly workPlanStore: RuntimeServices['workPlanStore'];
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export interface UiCoordinationServices {
|
|
@@ -173,6 +174,7 @@ export function createUiRuntimeServices(
|
|
|
173
174
|
adaptivePlanner: runtimeServices.adaptivePlanner,
|
|
174
175
|
projectPlanningService: runtimeServices.projectPlanningService,
|
|
175
176
|
projectPlanningProjectId: runtimeServices.projectPlanningProjectId,
|
|
177
|
+
workPlanStore: runtimeServices.workPlanStore,
|
|
176
178
|
},
|
|
177
179
|
coordination: {
|
|
178
180
|
approvalBroker: runtimeServices.approvalBroker,
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.19.
|
|
9
|
+
let _version = '0.19.86';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|