@pellux/goodvibes-agent 0.1.102 → 0.1.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +10 -0
  3. package/docs/README.md +1 -1
  4. package/docs/getting-started.md +17 -3
  5. package/package.json +1 -1
  6. package/src/agent/memory-safety.ts +16 -0
  7. package/src/cli/help.ts +86 -0
  8. package/src/cli/local-library-command.ts +516 -0
  9. package/src/cli/management.ts +17 -0
  10. package/src/cli/memory-command.ts +630 -0
  11. package/src/cli/package-verification.ts +10 -0
  12. package/src/cli/parser.ts +8 -0
  13. package/src/cli/types.ts +3 -0
  14. package/src/input/agent-workspace-activation.ts +170 -0
  15. package/src/input/agent-workspace-categories.ts +8 -1
  16. package/src/input/agent-workspace-editors.ts +36 -0
  17. package/src/input/agent-workspace-memory-editor.ts +88 -0
  18. package/src/input/agent-workspace-setup.ts +7 -5
  19. package/src/input/agent-workspace-snapshot.ts +40 -4
  20. package/src/input/agent-workspace-token.ts +51 -0
  21. package/src/input/agent-workspace-types.ts +13 -3
  22. package/src/input/agent-workspace.ts +130 -185
  23. package/src/input/feed-context-factory.ts +1 -3
  24. package/src/input/handler-feed.ts +1 -4
  25. package/src/input/handler-interactions.ts +0 -1
  26. package/src/input/handler-modal-stack.ts +0 -1
  27. package/src/input/handler-modal-token-routes.ts +0 -11
  28. package/src/input/handler-picker-routes.ts +11 -20
  29. package/src/input/handler-ui-state.ts +0 -6
  30. package/src/input/handler.ts +1 -17
  31. package/src/main.ts +0 -6
  32. package/src/panels/builtin/agent.ts +0 -17
  33. package/src/panels/index.ts +0 -2
  34. package/src/renderer/agent-workspace.ts +8 -3
  35. package/src/renderer/conversation-overlays.ts +0 -6
  36. package/src/renderer/live-tail-modal.ts +10 -69
  37. package/src/renderer/process-modal.ts +28 -530
  38. package/src/runtime/bootstrap-core.ts +1 -1
  39. package/src/runtime/services.ts +3 -4
  40. package/src/tools/{wrfc-agent-guard.ts → agent-tool-policy-guard.ts} +0 -6
  41. package/src/version.ts +1 -1
  42. package/src/panels/agent-inspector-panel.ts +0 -521
  43. package/src/panels/agent-inspector-shared.ts +0 -94
  44. package/src/panels/agent-logs-panel.ts +0 -559
  45. package/src/panels/agent-logs-shared.ts +0 -129
  46. package/src/renderer/agent-detail-modal.ts +0 -331
  47. package/src/renderer/process-summary.ts +0 -67
@@ -2,484 +2,39 @@ import { type Line } from '../types/grid.ts';
2
2
  import { ModalFactory } from './modal-factory.ts';
3
3
  import { formatDuration } from './modal-utils.ts';
4
4
  import type { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
5
- import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
6
- import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
7
5
  import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
8
6
  import { getVisibleWindow } from './surface-layout.ts';
9
7
 
10
- // ─── ProcessEntry ─────────────────────────────────────────────────────────────
11
-
12
8
  export interface ProcessEntry {
13
- /** Unique process identifier */
14
- id: string;
15
- /** Display label (agent task or exec command) */
16
- label: string;
17
- /** Tree prefix for child processes, e.g. "└─ " under a WRFC owner. */
18
- treePrefix?: string;
19
- /** Process type */
20
- type: 'agent' | 'exec';
21
- /** Current status string */
22
- status: string;
23
- /** Elapsed milliseconds since start */
24
- elapsedMs: number;
25
- /** Live streaming snippet for tracked delegated sessions (last ~60 chars of current turn output). */
26
- streamSnippet?: string;
9
+ readonly id: string;
10
+ readonly label: string;
11
+ readonly type: 'exec';
12
+ readonly status: string;
13
+ readonly elapsedMs: number;
27
14
  }
28
15
 
29
- // ─── Constants ────────────────────────────────────────────────────────────────
30
-
31
- /** Maximum characters from agent task / exec command stored in ProcessEntry.label. */
32
16
  const MAX_LABEL_LENGTH = 80;
33
- /** Border and margin width subtracted from terminal width to get modal content width. */
34
17
  const MODAL_BORDER_WIDTH = 8;
35
18
 
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
-
44
19
  export interface ProcessModalDeps {
45
- readonly agentManager: Pick<AgentManager, 'list' | 'getStatus'>;
46
20
  readonly processManager: Pick<ProcessManager, 'list' | 'getStatus' | 'stop'>;
47
- readonly wrfcController: Pick<WrfcController, 'getChain'> & Partial<Pick<WrfcController, 'listChains'>>;
48
- /**
49
- * GoodVibes Agent must not present local AgentManager records as an owned
50
- * execution lane. Tests for copied TUI primitives can opt into read-only
51
- * display, but product runtime should keep this hidden.
52
- */
53
- readonly agentEntries: 'hidden' | 'read-only';
54
- }
55
-
56
- type WrfcChainLike = {
57
- readonly id: string;
58
- readonly state: string;
59
- readonly task: string;
60
- readonly ownerAgentId: string;
61
- readonly engineerAgentId?: string;
62
- readonly reviewerAgentId?: string;
63
- readonly fixerAgentId?: string;
64
- readonly allAgentIds?: readonly string[];
65
- readonly constraints?: readonly unknown[];
66
- };
67
-
68
- /** Build a display label for an agent based on its task and template. */
69
- function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
70
- const task = rec.task;
71
-
72
- // Look up the original task from the WRFC chain if available
73
- const originalTask = getChainTask(rec.wrfcId, deps);
74
-
75
- if (rec.wrfcRole === 'owner') {
76
- const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
77
- return `[WRFC owner] ${desc}`;
78
- }
79
-
80
- if (rec.wrfcRole === 'engineer') {
81
- const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 11);
82
- return `[Engineer] ${desc}`;
83
- }
84
-
85
- if (rec.wrfcRole === 'verifier') {
86
- const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
87
- return `[Verifier] ${desc}`;
88
- }
89
-
90
- // WRFC Review agent
91
- if (task.startsWith('WRFC Review Request')) {
92
- const thresholdMatch = task.match(/threshold is (\d+(?:\.\d+)?)/);
93
- const threshold = thresholdMatch ? thresholdMatch[1] : '9.9';
94
- const desc = truncateFirst(originalTask ?? 'review in progress', 50);
95
- return `[Review] ${desc} (target: ${threshold}/10)`;
96
- }
97
-
98
- // WRFC Fix agent
99
- if (task.startsWith('WRFC Fix Request')) {
100
- const scoreMatch = task.match(/Review score:\s*(\d+(?:\.\d+)?)\/(\d+)\s*\(threshold:\s*(\d+(?:\.\d+)?)/);
101
- const fromScore = scoreMatch ? scoreMatch[1] : '?';
102
- const toScore = scoreMatch ? scoreMatch[3] : '?';
103
- const attemptMatch = task.match(/Fix attempt:\s*(\d+)/);
104
- const attempt = attemptMatch ? attemptMatch[1] : '?';
105
- const desc = truncateFirst(originalTask ?? 'fix in progress', 45);
106
- // Show constraint count when the chain has constraints to target (SDK 0.23.0)
107
- const chain = rec.wrfcId ? safeGetChain(rec.wrfcId, deps) : null;
108
- const constraintCount = chain && (chain.constraints?.length ?? 0) > 0 ? chain.constraints?.length ?? 0 : 0;
109
- const constraintSuffix = constraintCount > 0 ? ` [${constraintCount}c]` : '';
110
- return `[Fix #${attempt}] ${desc} (${fromScore} \u2192 ${toScore}/10)${constraintSuffix}`;
111
- }
112
-
113
- // Regular agent — show template and truncated first line
114
- const templateLabels: Record<string, string> = {
115
- engineer: 'Engineer', reviewer: 'Reviewer', tester: 'Tester',
116
- researcher: 'Researcher', general: 'Agent',
117
- };
118
- const tag = templateLabels[rec.template] ?? 'Agent';
119
- const maxDesc = MAX_LABEL_LENGTH - tag.length - 3;
120
- return `[${tag}] ${truncateFirst(task, maxDesc)}`;
121
- }
122
-
123
- function isActiveAgent(rec: AgentRecord): boolean {
124
- return rec.status !== 'completed' && rec.status !== 'failed' && rec.status !== 'cancelled';
125
- }
126
-
127
- function isActiveWrfcState(state: string): boolean {
128
- return state !== 'passed' && state !== 'failed';
129
- }
130
-
131
- function getStreamSnippet(rec: AgentRecord): string | undefined {
132
- if (!rec.streamingContent) return undefined;
133
- const raw = rec.streamingContent.replace(/\n/g, ' ').trim();
134
- return raw.length > 60 ? '...' + raw.slice(-57) : raw;
135
- }
136
-
137
- function compareAgents(a: AgentRecord, b: AgentRecord): number {
138
- const roleDelta = (WRFC_ROLE_ORDER[a.wrfcRole ?? ''] ?? 50) - (WRFC_ROLE_ORDER[b.wrfcRole ?? ''] ?? 50);
139
- if (roleDelta !== 0) return roleDelta;
140
- return a.startedAt - b.startedAt || a.id.localeCompare(b.id);
141
- }
142
-
143
- function buildAgentEntry(
144
- rec: AgentRecord,
145
- deps: ProcessModalDeps,
146
- now: number,
147
- treePrefix = '',
148
- ): ProcessEntry {
149
- return {
150
- id: rec.id,
151
- label: buildAgentLabel(rec, deps),
152
- treePrefix,
153
- type: 'agent',
154
- status: rec.status,
155
- elapsedMs: now - rec.startedAt,
156
- streamSnippet: getStreamSnippet(rec),
157
- };
158
- }
159
-
160
- function appendAgentSubtree(
161
- result: ProcessEntry[],
162
- rec: AgentRecord,
163
- childrenByParent: Map<string, AgentRecord[]>,
164
- deps: ProcessModalDeps,
165
- now: number,
166
- prefix: string,
167
- connector: string,
168
- visited: Set<string>,
169
- ): void {
170
- if (visited.has(rec.id)) return;
171
- visited.add(rec.id);
172
- result.push(buildAgentEntry(rec, deps, now, `${prefix}${connector}`));
173
-
174
- const children = (childrenByParent.get(rec.id) ?? []).slice().sort(compareAgents);
175
- const descendantPrefix = connector === '├─ ' ? '│ ' : connector === '└─ ' ? ' ' : '';
176
- children.forEach((child, index) => {
177
- const last = index === children.length - 1;
178
- appendAgentSubtree(
179
- result,
180
- child,
181
- childrenByParent,
182
- deps,
183
- now,
184
- `${prefix}${descendantPrefix}`,
185
- last ? '└─ ' : '├─ ',
186
- visited,
187
- );
188
- });
189
- }
190
-
191
- function appendAgentGroupEntries(
192
- result: ProcessEntry[],
193
- records: AgentRecord[],
194
- deps: ProcessModalDeps,
195
- now: number,
196
- ): void {
197
- const group = records.slice().sort(compareAgents);
198
- const byId = new Map(group.map((rec) => [rec.id, rec]));
199
- const childrenByParent = new Map<string, AgentRecord[]>();
200
-
201
- for (const rec of group) {
202
- if (!rec.parentAgentId || !byId.has(rec.parentAgentId)) continue;
203
- const children = childrenByParent.get(rec.parentAgentId) ?? [];
204
- children.push(rec);
205
- childrenByParent.set(rec.parentAgentId, children);
206
- }
207
-
208
- const chain = group[0]?.wrfcId ? safeGetChain(group[0].wrfcId, deps) : null;
209
- const owner = group.find((rec) => rec.id === chain?.ownerAgentId)
210
- ?? group.find((rec) => rec.wrfcRole === 'owner');
211
- const roots = owner
212
- ? [owner]
213
- : group.filter((rec) => !rec.parentAgentId || !byId.has(rec.parentAgentId));
214
- const visited = new Set<string>();
215
-
216
- roots.forEach((root, index) => {
217
- const connector = owner || roots.length === 1 ? '' : (index === roots.length - 1 ? '└─ ' : '├─ ');
218
- appendAgentSubtree(result, root, childrenByParent, deps, now, '', connector, visited);
219
- });
220
-
221
- const leftovers = group.filter((rec) => !visited.has(rec.id));
222
- leftovers.forEach((rec, index) => {
223
- appendAgentSubtree(
224
- result,
225
- rec,
226
- childrenByParent,
227
- deps,
228
- now,
229
- '',
230
- index === leftovers.length - 1 ? '└─ ' : '├─ ',
231
- visited,
232
- );
233
- });
234
- }
235
-
236
- function buildAgentEntries(
237
- agents: AgentRecord[],
238
- deps: ProcessModalDeps,
239
- now: number,
240
- getGroupOrder?: (key: string) => number | undefined,
241
- ensureGroupOrder?: (key: string) => number,
242
- ): ProcessEntry[] {
243
- const result: ProcessEntry[] = [];
244
- const displayAgents = prepareAgentRecordsForDisplay(agents, deps);
245
- const activeById = new Map(displayAgents.map((agent) => [agent.id, agent]));
246
- const groups = new Map<string, AgentRecord[]>();
247
-
248
- for (const agent of displayAgents) {
249
- const groupKey = getAgentGroupKey(agent, activeById);
250
- const group = groups.get(groupKey) ?? [];
251
- group.push(agent);
252
- groups.set(groupKey, group);
253
- }
254
-
255
- const sortedGroups = Array.from(groups.entries()).sort(([aKey, a], [bKey, b]) => {
256
- const aOrder = getGroupOrder?.(aKey);
257
- const bOrder = getGroupOrder?.(bKey);
258
- if (aOrder !== undefined || bOrder !== undefined) {
259
- if (aOrder === undefined) return 1;
260
- if (bOrder === undefined) return -1;
261
- return aOrder - bOrder;
262
- }
263
- const aStarted = Math.min(...a.map((rec) => rec.startedAt));
264
- const bStarted = Math.min(...b.map((rec) => rec.startedAt));
265
- return aStarted - bStarted || aKey.localeCompare(bKey);
266
- });
267
- for (const [key, group] of sortedGroups) {
268
- ensureGroupOrder?.(key);
269
- appendAgentGroupEntries(result, group, deps, now);
270
- }
271
-
272
- return result;
273
- }
274
-
275
- function prepareAgentRecordsForDisplay(agents: AgentRecord[], deps: ProcessModalDeps): AgentRecord[] {
276
- const chains = listWrfcChains(deps);
277
- const agentById = new Map(agents.map((agent) => [agent.id, agent]));
278
- const normalizedById = new Map<string, AgentRecord>();
279
-
280
- for (const agent of agents) {
281
- if (!isActiveAgent(agent)) continue;
282
- normalizedById.set(agent.id, normalizeWrfcAgentRecord(agent, chains));
283
- }
284
-
285
- // A WRFC owner is the durable root of the chain. Keep it visible until the
286
- // chain itself is terminal, even if the underlying owner agent has already
287
- // emitted a completed phase event before reviewer/fixer/gate work finishes.
288
- for (const chain of chains) {
289
- if (!isActiveWrfcState(chain.state)) continue;
290
- const owner = agentById.get(chain.ownerAgentId);
291
- if (!owner || normalizedById.has(owner.id)) continue;
292
- const chainHasActiveMember = agents.some((agent) =>
293
- agent.id !== owner.id
294
- && isActiveAgent(agent)
295
- && isAgentInChain(agent, chain)
296
- );
297
- if (!chainHasActiveMember) continue;
298
- normalizedById.set(owner.id, normalizeWrfcAgentRecord({
299
- ...owner,
300
- status: 'running',
301
- completedAt: undefined,
302
- progress: owner.progress ?? `WRFC chain ${chain.state}`,
303
- }, chains));
304
- }
305
-
306
- const normalized = Array.from(normalizedById.values());
307
- return inferDuplicateWrfcOwnerRows(normalized);
308
- }
309
-
310
- function normalizeWrfcAgentRecord(agent: AgentRecord, chains: WrfcChainLike[]): AgentRecord {
311
- const chain = findChainForAgent(agent, chains);
312
- if (!chain) return agent;
313
-
314
- const role = inferWrfcRole(agent, chain);
315
- const parentAgentId = role && role !== 'owner'
316
- ? agent.parentAgentId ?? chain.ownerAgentId
317
- : agent.parentAgentId;
318
-
319
- return {
320
- ...agent,
321
- wrfcId: agent.wrfcId ?? chain.id,
322
- wrfcRole: agent.wrfcRole ?? role,
323
- parentAgentId,
324
- };
325
- }
326
-
327
- function inferDuplicateWrfcOwnerRows(agents: AgentRecord[]): AgentRecord[] {
328
- const byTask = new Map<string, AgentRecord[]>();
329
- for (const agent of agents) {
330
- if (agent.wrfcId || agent.wrfcRole || agent.parentAgentId) continue;
331
- if (agent.reviewMode !== 'wrfc') continue;
332
- const key = agent.task.trim();
333
- if (!key) continue;
334
- const group = byTask.get(key) ?? [];
335
- group.push(agent);
336
- byTask.set(key, group);
337
- }
338
-
339
- const inferredIds = new Set<string>();
340
- const inferred = new Map<string, AgentRecord>();
341
- for (const [task, group] of byTask) {
342
- if (group.length < 2) continue;
343
- const sorted = group.slice().sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id));
344
- const owner = sorted[0]!;
345
- const syntheticWrfcId = `inferred:${owner.id}`;
346
- inferred.set(owner.id, {
347
- ...owner,
348
- wrfcId: syntheticWrfcId,
349
- wrfcRole: 'owner',
350
- });
351
- inferredIds.add(owner.id);
352
- for (const child of sorted.slice(1)) {
353
- inferred.set(child.id, {
354
- ...child,
355
- wrfcId: syntheticWrfcId,
356
- wrfcRole: child.template === 'reviewer' ? 'reviewer' : 'engineer',
357
- parentAgentId: owner.id,
358
- });
359
- inferredIds.add(child.id);
360
- }
361
-
362
- // Avoid accidentally grouping unrelated long-running WRFC roots that just
363
- // happen to share an empty or generic task after this exact duplicate group.
364
- byTask.delete(task);
365
- }
366
-
367
- if (inferredIds.size === 0) return agents;
368
- return agents.map((agent) => inferred.get(agent.id) ?? agent);
369
- }
370
-
371
- function listWrfcChains(deps: ProcessModalDeps): WrfcChainLike[] {
372
- const controller = deps.wrfcController as ProcessModalDeps['wrfcController'] & {
373
- listChains?: () => unknown;
374
- };
375
- if (typeof controller.listChains !== 'function') return [];
376
- try {
377
- const value = controller.listChains();
378
- return Array.isArray(value) ? value.filter(isWrfcChainLike) : [];
379
- } catch {
380
- return [];
381
- }
382
- }
383
-
384
- function isWrfcChainLike(value: unknown): value is WrfcChainLike {
385
- if (!value || typeof value !== 'object') return false;
386
- const record = value as Record<string, unknown>;
387
- return typeof record.id === 'string'
388
- && typeof record.state === 'string'
389
- && typeof record.task === 'string'
390
- && typeof record.ownerAgentId === 'string';
391
- }
392
-
393
- function findChainForAgent(agent: AgentRecord, chains: WrfcChainLike[]): WrfcChainLike | null {
394
- if (agent.wrfcId) {
395
- const direct = chains.find((chain) => chain.id === agent.wrfcId);
396
- if (direct) return direct;
397
- }
398
- return chains.find((chain) => isAgentInChain(agent, chain)) ?? null;
399
21
  }
400
22
 
401
- function isAgentInChain(agent: AgentRecord, chain: WrfcChainLike): boolean {
402
- return chain.ownerAgentId === agent.id
403
- || chain.engineerAgentId === agent.id
404
- || chain.reviewerAgentId === agent.id
405
- || chain.fixerAgentId === agent.id
406
- || (chain.allAgentIds?.includes(agent.id) ?? false)
407
- || agent.wrfcId === chain.id;
408
- }
409
-
410
- function inferWrfcRole(agent: AgentRecord, chain: WrfcChainLike): AgentRecord['wrfcRole'] {
411
- if (agent.wrfcRole) return agent.wrfcRole;
412
- if (chain.ownerAgentId === agent.id) return 'owner';
413
- if (chain.engineerAgentId === agent.id) return 'engineer';
414
- if (chain.reviewerAgentId === agent.id) return 'reviewer';
415
- if (chain.fixerAgentId === agent.id) return 'fixer';
416
- if (agent.template === 'reviewer') return 'reviewer';
417
- return 'engineer';
418
- }
419
-
420
- function getAgentGroupKey(agent: AgentRecord, activeById: Map<string, AgentRecord>): string {
421
- if (agent.wrfcId) return `wrfc:${agent.wrfcId}`;
422
-
423
- const seen = new Set<string>();
424
- let root = agent;
425
- while (root.parentAgentId && activeById.has(root.parentAgentId) && !seen.has(root.parentAgentId)) {
426
- seen.add(root.id);
427
- root = activeById.get(root.parentAgentId)!;
428
- }
429
-
430
- // If the active root is an orphaned child, keep it anchored to its missing parent id
431
- // so it does not jump to a new group when the parent exits before its children.
432
- return `root:${root.parentAgentId ?? root.id}`;
433
- }
434
-
435
- function safeGetChain(wrfcId: string, deps: Pick<ProcessModalDeps, 'wrfcController'>): WrfcChainLike | null {
436
- try {
437
- const chain = deps.wrfcController.getChain(wrfcId);
438
- return isWrfcChainLike(chain) ? chain : null;
439
- } catch {
440
- return null;
441
- }
442
- }
443
-
444
- /** Get the original task description from a WRFC chain. */
445
- function getChainTask(wrfcId: string | undefined, deps: Pick<ProcessModalDeps, 'wrfcController'>): string | null {
446
- if (!wrfcId) return null;
447
- return safeGetChain(wrfcId, deps)?.task ?? null;
448
- }
449
-
450
- /** Truncate to first line, capped at max chars. */
451
- function truncateFirst(text: string, max: number): string {
452
- const line = text.split('\n')[0].trim();
453
- return line.length > max ? line.slice(0, Math.max(0, max - 3)) + '...' : line;
454
- }
455
-
456
- /** Truncate a command string to first line, capped at MAX_LABEL_LENGTH. */
457
23
  function truncateCmd(text: string): string {
458
- const firstLine = text.split('\n')[0].trim();
459
- if (firstLine.length > MAX_LABEL_LENGTH) return firstLine.slice(0, MAX_LABEL_LENGTH - 3) + '...';
24
+ const firstLine = text.split('\n')[0]?.trim() ?? '';
25
+ if (firstLine.length > MAX_LABEL_LENGTH) return `${firstLine.slice(0, MAX_LABEL_LENGTH - 3)}...`;
460
26
  return firstLine;
461
27
  }
462
28
 
463
- // ─── ProcessModalState ────────────────────────────────────────────────────────
464
-
465
- /**
466
- * ProcessModal — manages the state for the background-process list modal.
467
- *
468
- * Holds the list of ProcessEntry items, selected index, and active flag.
469
- * Rendering is done by renderProcessModal().
470
- */
471
29
  export class ProcessModal {
472
30
  public active = false;
473
31
  public selectedIndex = 0;
474
32
  public entries: ProcessEntry[] = [];
475
33
  private refreshTimer: ReturnType<typeof setInterval> | null = null;
476
34
  private onRefresh: (() => void) | null = null;
477
- private groupOrder = new Map<string, number>();
478
- private nextGroupOrder = 0;
479
35
 
480
36
  constructor(private readonly deps: ProcessModalDeps) {}
481
37
 
482
- /** Set a callback to trigger re-render on timer tick. */
483
38
  setOnRefresh(fn: () => void): void {
484
39
  this.onRefresh = fn;
485
40
  }
@@ -503,55 +58,28 @@ export class ProcessModal {
503
58
  }
504
59
  }
505
60
 
506
- /** Rebuild entries from the currently owned runtime services. */
507
61
  refresh(): void {
508
- const manager = this.deps.agentManager;
509
- if (typeof manager?.list !== 'function') return; // Guard against test mock pollution
510
62
  const now = Date.now();
511
63
  const result: ProcessEntry[] = [];
512
64
 
513
- // Local AgentManager activity is hidden in the Agent product. Build/fix/review
514
- // execution belongs to explicit GoodVibes TUI delegation, not a local lane.
515
- if (this.deps.agentEntries === 'read-only') {
516
- result.push(...buildAgentEntries(
517
- manager.list(),
518
- this.deps,
519
- now,
520
- (key) => this.groupOrder.get(key),
521
- (key) => this.ensureGroupOrder(key),
522
- ));
523
- }
524
-
525
- // Background exec processes — only show running
526
- const pm = this.deps.processManager;
527
- for (const p of pm.list()) {
528
- if (p.status.startsWith('done')) continue;
529
- const startTime = pm.getStatus(p.id)?.startTime ?? now;
65
+ for (const process of this.deps.processManager.list()) {
66
+ if (process.status.startsWith('done')) continue;
67
+ const startTime = this.deps.processManager.getStatus(process.id)?.startTime ?? now;
530
68
  result.push({
531
- id: p.id,
532
- label: truncateCmd(p.cmd),
69
+ id: process.id,
70
+ label: truncateCmd(process.cmd),
533
71
  type: 'exec',
534
- status: p.status,
72
+ status: process.status,
535
73
  elapsedMs: now - startTime,
536
74
  });
537
75
  }
538
76
 
539
77
  this.entries = result;
540
-
541
- // Keep selection in-bounds
542
78
  if (this.selectedIndex >= this.entries.length) {
543
79
  this.selectedIndex = Math.max(0, this.entries.length - 1);
544
80
  }
545
81
  }
546
82
 
547
- private ensureGroupOrder(key: string): number {
548
- const existing = this.groupOrder.get(key);
549
- if (existing !== undefined) return existing;
550
- const next = this.nextGroupOrder++;
551
- this.groupOrder.set(key, next);
552
- return next;
553
- }
554
-
555
83
  moveUp(): void {
556
84
  if (this.entries.length === 0) return;
557
85
  this.selectedIndex = (this.selectedIndex - 1 + this.entries.length) % this.entries.length;
@@ -566,30 +94,13 @@ export class ProcessModal {
566
94
  return this.entries[this.selectedIndex];
567
95
  }
568
96
 
569
- /**
570
- * Stop the selected shell process.
571
- * Agent entries are read-only in GoodVibes Agent; build execution and
572
- * cancellation belong to GoodVibes TUI/shared-session owners.
573
- */
574
- killSelected(): boolean {
97
+ stopSelected(): boolean {
575
98
  const entry = this.getSelected();
576
99
  if (!entry) return false;
577
-
578
- if (entry.type === 'exec') {
579
- return this.deps.processManager.stop(entry.id);
580
- }
581
- return false;
100
+ return this.deps.processManager.stop(entry.id);
582
101
  }
583
102
  }
584
103
 
585
- // ─── renderProcessModal ───────────────────────────────────────────────────────
586
-
587
- /**
588
- * Render the process list modal as Line[] for overlay in the viewport.
589
- *
590
- * @param modal ProcessModal state
591
- * @param width Terminal width
592
- */
593
104
  export function renderProcessModal(modal: ProcessModal, width: number, viewportHeight = 24): Line[] {
594
105
  modal.refresh();
595
106
 
@@ -612,7 +123,7 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
612
123
  margin: boxMargin,
613
124
  targetContentRows,
614
125
  sections: [
615
- { type: 'text', content: 'No runtime activity.' },
126
+ { type: 'text', content: 'No running shell processes.' },
616
127
  ],
617
128
  hints: ['[Esc] Close'],
618
129
  }, width);
@@ -622,34 +133,21 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
622
133
  const window = getVisibleWindow(modal.entries.length, modal.selectedIndex, maxVisibleRows);
623
134
  const visibleEntries = modal.entries.slice(window.start, window.end);
624
135
 
625
- const items = visibleEntries.map((e, i) => {
626
- const absoluteIndex = window.start + i;
627
- const statusIcon = {
628
- running: '●',
629
- pending: '•',
630
- completed: '',
631
- failed: '✗',
632
- cancelled: '–',
633
- }[e.status] ?? '•';
634
- const typeTag = e.type === 'agent' ? '[agent]' : '[exec]';
635
- const dur = formatDuration(e.elapsedMs);
636
- const statusStr = e.streamSnippet ? `streaming ${dur}` : `${e.status} ${dur}`;
637
- const suffix = ` ${statusStr}`;
638
- const treePrefix = e.treePrefix ?? '';
639
- const maxDescW = maxLabelW - typeTag.length - treePrefix.length - suffix.length - 4; // icon + spaces
640
- const desc = e.label.length > maxDescW ? e.label.slice(0, Math.max(0, maxDescW - 3)) + '...' : e.label;
641
- const label = `${statusIcon} ${typeTag} ${treePrefix}${desc}${suffix}`;
136
+ const items = visibleEntries.map((entry, index) => {
137
+ const absoluteIndex = window.start + index;
138
+ const statusIcon = entry.status === 'running' ? '*' : entry.status === 'failed' ? '!' : '-';
139
+ const dur = formatDuration(entry.elapsedMs);
140
+ const suffix = ` ${entry.status} ${dur}`;
141
+ const typeTag = '[exec]';
142
+ const maxDescW = Math.max(0, maxLabelW - typeTag.length - suffix.length - 4);
143
+ const desc = entry.label.length > maxDescW ? `${entry.label.slice(0, Math.max(0, maxDescW - 3))}...` : entry.label;
642
144
  return {
643
- label,
145
+ label: `${statusIcon} ${typeTag} ${desc}${suffix}`,
644
146
  selected: absoluteIndex === modal.selectedIndex,
645
147
  };
646
148
  });
647
- const sections: import('./modal-factory.ts').ModalSection[] = [
648
- { type: 'list', items },
649
- ];
650
- if (modal.entries.length > maxVisibleRows) {
651
- sections.push({ type: 'separator' });
652
- }
149
+ const sections: import('./modal-factory.ts').ModalSection[] = [{ type: 'list', items }];
150
+ if (modal.entries.length > maxVisibleRows) sections.push({ type: 'separator' });
653
151
 
654
152
  return ModalFactory.createModal({
655
153
  title: 'Runtime Activity',
@@ -660,6 +158,6 @@ export function renderProcessModal(modal: ProcessModal, width: number, viewportH
660
158
  helpers: modal.entries.length > maxVisibleRows
661
159
  ? [{ content: `[${window.start + 1}-${window.end} of ${modal.entries.length}]` }]
662
160
  : undefined,
663
- hints: ['[Up/Down] Navigate', '[Enter] Details/output', '[k] Stop exec only', '[Esc] Close'],
161
+ hints: ['[Up/Down] Navigate', '[Enter] Output', '[k] Stop process', '[Esc] Close'],
664
162
  }, width);
665
163
  }
@@ -29,7 +29,7 @@ import { registerBootstrapHookBridge } from '@/runtime/index.ts';
29
29
  import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
30
30
  import { createRuntimeServices, type RuntimeServices } from './services.ts';
31
31
  import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
32
- import { installAgentToolPolicyGuard } from '../tools/wrfc-agent-guard.ts';
32
+ import { installAgentToolPolicyGuard } from '../tools/agent-tool-policy-guard.ts';
33
33
  import { registerAgentLocalRegistryTool } from '../tools/agent-local-registry-tool.ts';
34
34
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
35
35
 
@@ -341,7 +341,8 @@ export interface RuntimeServices {
341
341
  readonly fileUndoManager: FileUndoManager;
342
342
  readonly integrationHelpers: IntegrationHelperService;
343
343
  /**
344
- * Re-root path-bound stores (MemoryStore, ProjectIndex) to a new working directory.
344
+ * Re-root workspace-bound stores to a new working directory.
345
+ * Agent memory is home/profile-owned and intentionally does not move on workspace swap.
345
346
  * Called by WorkspaceSwapManager after the new directory has been verified.
346
347
  * Stores that require a process restart emit a warn-level log; they continue serving
347
348
  * the old path until the daemon restarts with the new --working-dir.
@@ -472,7 +473,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
472
473
  });
473
474
  const artifactStore = new ArtifactStore({ configManager });
474
475
  const memoryEmbeddingRegistry = new MemoryEmbeddingProviderRegistry({ configManager });
475
- const memoryDbPath = join(workingDirectory, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
476
+ const memoryDbPath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
476
477
  const memoryStore = new MemoryStore(memoryDbPath, {
477
478
  embeddingRegistry: memoryEmbeddingRegistry,
478
479
  });
@@ -733,8 +734,6 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
733
734
  fileUndoManager,
734
735
  integrationHelpers,
735
736
  async rerootStores(newWorkingDir: string): Promise<void> {
736
- const newMemoryDbPath = join(newWorkingDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
737
- await memoryStore.reroot(newMemoryDbPath);
738
737
  await projectIndex.reroot(newWorkingDir);
739
738
  },
740
739
  };