@pellux/goodvibes-agent 1.4.1 → 1.4.2

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.
@@ -1,191 +0,0 @@
1
- import type { CommandRegistry } from '../command-registry.ts';
2
- import type { WorkPlanItemStatus, WorkPlanStore } from '../../work-plans/work-plan-store.ts';
3
- import { WORK_PLAN_STATUSES } from '../../work-plans/work-plan-store.ts';
4
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
- import { requireYesFlag, stripYesFlag } from './confirmation.ts';
6
-
7
- const STATUS_COMMANDS: Record<string, WorkPlanItemStatus> = {
8
- pending: 'pending',
9
- todo: 'pending',
10
- start: 'in_progress',
11
- active: 'in_progress',
12
- progress: 'in_progress',
13
- block: 'blocked',
14
- blocked: 'blocked',
15
- done: 'done',
16
- complete: 'done',
17
- fail: 'failed',
18
- failed: 'failed',
19
- cancel: 'cancelled',
20
- cancelled: 'cancelled',
21
- };
22
-
23
- function getStore(ctx: import('../command-registry.ts').CommandContext): WorkPlanStore | null {
24
- return ctx.workspace.workPlanStore ?? null;
25
- }
26
-
27
- function formatList(store: WorkPlanStore): string {
28
- const items = store.listItems();
29
- if (items.length === 0) {
30
- return [
31
- 'Work plan is empty',
32
- ' next /workplan add <title>',
33
- ].join('\n');
34
- }
35
- return [
36
- `Work Plan (${items.length})`,
37
- ...items.map((item) => {
38
- const owner = item.owner ? ` @${item.owner}` : '';
39
- return ` ${item.id} ${item.status.padEnd(11)} ${item.title}${owner}`;
40
- }),
41
- ].join('\n');
42
- }
43
-
44
- function parseAddArgs(args: string[]): { title: string; owner?: string; source?: string; notes?: string } {
45
- const titleParts: string[] = [];
46
- let owner: string | undefined;
47
- let source: string | undefined;
48
- let notes: string | undefined;
49
- for (let i = 0; i < args.length; i++) {
50
- const part = args[i] ?? '';
51
- if (part === '--owner' && args[i + 1]) {
52
- owner = args[++i];
53
- continue;
54
- }
55
- if (part === '--source' && args[i + 1]) {
56
- source = args[++i];
57
- continue;
58
- }
59
- if (part === '--notes' && args[i + 1]) {
60
- notes = args.slice(i + 1).join(' ').trim();
61
- break;
62
- }
63
- titleParts.push(part);
64
- }
65
- return {
66
- title: titleParts.join(' ').trim(),
67
- ...(owner ? { owner } : {}),
68
- ...(source ? { source } : {}),
69
- ...(notes ? { notes } : {}),
70
- };
71
- }
72
-
73
- export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void {
74
- registry.register({
75
- name: 'workplan',
76
- aliases: ['wp', 'todo'],
77
- description: 'Track a persistent workspace-scoped work plan',
78
- usage: '[list|show|add <title> [--owner name] [--source label] [--notes text]|done <id>|start <id>|block <id>|fail <id>|cancel <id>|pending <id>|remove <id> --yes|clear-done --yes]',
79
- argsHint: '[list|add|show|done]',
80
- handler(args, ctx) {
81
- const parsed = stripYesFlag(args);
82
- const commandArgs = [...parsed.rest];
83
- const store = getStore(ctx);
84
- if (!store) {
85
- ctx.print('Work plan store is not available in this runtime.');
86
- return;
87
- }
88
- const subcommand = (commandArgs[0] ?? 'list').toLowerCase();
89
- try {
90
- if (subcommand === 'panel' || subcommand === 'open') {
91
- ctx.print('Open Agent Workspace -> Work -> Work plan for the workspace view, or run /workplan list for compact command output.');
92
- return;
93
- }
94
- if (subcommand === 'list') {
95
- ctx.print(formatList(store));
96
- return;
97
- }
98
- if (subcommand === 'show' || subcommand === 'markdown') {
99
- ctx.print(store.toMarkdown());
100
- return;
101
- }
102
- if (subcommand === 'add') {
103
- const addArgs = parseAddArgs(commandArgs.slice(1));
104
- if (!addArgs.title) {
105
- ctx.print('Usage: /workplan add <title> [--owner name] [--source label] [--notes text]');
106
- return;
107
- }
108
- const addOptions = {
109
- ...(addArgs.owner ? { owner: addArgs.owner } : {}),
110
- source: addArgs.source ?? 'manual',
111
- ...(addArgs.notes ? { notes: addArgs.notes } : {}),
112
- };
113
- const item = store.addItem(addArgs.title, addOptions);
114
- ctx.print([
115
- 'Added work plan item',
116
- ` id ${item.id}`,
117
- ` title ${item.title}`,
118
- ' next open Agent Workspace -> Work -> Work plan to review',
119
- ].join('\n'));
120
- return;
121
- }
122
- if (subcommand === 'remove' || subcommand === 'delete' || subcommand === 'rm') {
123
- const id = commandArgs[1];
124
- if (!id) {
125
- ctx.print(`Usage: /workplan ${subcommand} <id> --yes`);
126
- return;
127
- }
128
- if (!parsed.yes) {
129
- requireYesFlag(ctx, `remove work plan item ${id}`, `/workplan ${subcommand} <id> --yes`);
130
- return;
131
- }
132
- const item = store.removeItem(id);
133
- ctx.print([
134
- 'Removed work plan item',
135
- ` id ${item.id}`,
136
- ` title ${item.title}`,
137
- ].join('\n'));
138
- return;
139
- }
140
- if (subcommand === 'clear-done' || subcommand === 'clear-completed') {
141
- if (!parsed.yes) {
142
- requireYesFlag(ctx, 'clear completed work plan items', `/workplan ${subcommand} --yes`);
143
- return;
144
- }
145
- const count = store.clearCompleted();
146
- ctx.print([
147
- 'Cleared completed/cancelled work plan items',
148
- ` count ${count}`,
149
- ].join('\n'));
150
- return;
151
- }
152
- if (subcommand === 'cycle' || subcommand === 'toggle') {
153
- const id = commandArgs[1];
154
- if (!id) {
155
- ctx.print(`Usage: /workplan ${subcommand} <id>`);
156
- return;
157
- }
158
- const item = store.cycleItemStatus(id);
159
- ctx.print([
160
- 'Updated work plan item',
161
- ` id ${item.id}`,
162
- ` status ${item.status}`,
163
- ].join('\n'));
164
- return;
165
- }
166
- const status = STATUS_COMMANDS[subcommand];
167
- if (status) {
168
- const id = commandArgs[1];
169
- if (!id) {
170
- ctx.print(`Usage: /workplan ${subcommand} <id>`);
171
- return;
172
- }
173
- const item = store.setItemStatus(id, status);
174
- ctx.print([
175
- 'Updated work plan item',
176
- ` id ${item.id}`,
177
- ` status ${item.status}`,
178
- ].join('\n'));
179
- return;
180
- }
181
- if (WORK_PLAN_STATUSES.includes(subcommand as WorkPlanItemStatus)) {
182
- ctx.print(`Usage: /workplan ${subcommand} <id>`);
183
- return;
184
- }
185
- ctx.print(`Unknown workplan subcommand ${subcommand}`);
186
- } catch (error) {
187
- ctx.print(summarizeError(error));
188
- }
189
- },
190
- });
191
- }
@@ -1,274 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import type { ExecutionPlan, PlanItem, PlanItemStatus } from '@pellux/goodvibes-sdk/platform/core';
3
- import { BasePanel } from './base-panel.ts';
4
- import type { PlanDashboardQuery } from '../runtime/ui-service-queries.ts';
5
- import {
6
- buildEmptyState,
7
- buildPanelLine,
8
- buildPanelWorkspace,
9
- resolveScrollablePanelSection,
10
- DEFAULT_PANEL_PALETTE,
11
- type PanelWorkspaceSection,
12
- } from './polish.ts';
13
-
14
- // ---------------------------------------------------------------------------
15
- // Status display maps
16
- // ---------------------------------------------------------------------------
17
-
18
- const STATUS_ICON: Record<PlanItemStatus, string> = {
19
- complete: '✓',
20
- in_progress: '▸',
21
- pending: '•',
22
- failed: '✗',
23
- skipped: '–',
24
- };
25
-
26
- const STATUS_FG: Record<PlanItemStatus, string> = {
27
- complete: '#22c55e',
28
- in_progress: '#00ffff',
29
- pending: '244',
30
- failed: '#ef4444',
31
- skipped: '238',
32
- };
33
-
34
- // ---------------------------------------------------------------------------
35
- // PlanDashboardPanel
36
- // ---------------------------------------------------------------------------
37
-
38
- export class PlanDashboardPanel extends BasePanel {
39
- private scrollOffset = 0;
40
- private selectedIndex = 0;
41
-
42
- // Flat list of navigable row indices (set during render)
43
- private totalRows = 0;
44
- private readonly planManager: PlanDashboardQuery;
45
-
46
- constructor(planManager: PlanDashboardQuery) {
47
- super('plan', 'Plan', 'P', 'agent');
48
- this.planManager = planManager;
49
- }
50
-
51
- override onActivate(): void {
52
- super.onActivate();
53
- // Re-read plan from disk each time the panel is activated
54
- this.markDirty();
55
- }
56
-
57
- // --------------------------------------------------------------------------
58
- // Input
59
- // --------------------------------------------------------------------------
60
-
61
- handleInput(key: string): boolean {
62
- if (key === 'up' || key === 'k') {
63
- if (this.selectedIndex > 0) {
64
- this.selectedIndex--;
65
- this.markDirty();
66
- return true;
67
- }
68
- } else if (key === 'down' || key === 'j') {
69
- if (this.selectedIndex < this.totalRows - 1) {
70
- this.selectedIndex++;
71
- this.markDirty();
72
- return true;
73
- }
74
- }
75
- return false;
76
- }
77
-
78
- // --------------------------------------------------------------------------
79
- // Render
80
- // --------------------------------------------------------------------------
81
-
82
- render(width: number, height: number): Line[] {
83
- return this.trackedRender(() => {
84
- const plan = this.planManager.getActive();
85
- if (!plan) {
86
- return buildPanelWorkspace(width, height, {
87
- title: ' Plan Dashboard',
88
- intro: 'Track the active execution plan, item status, phase grouping, and overall completion.',
89
- sections: [
90
- {
91
- lines: buildEmptyState(
92
- width,
93
- ' No active execution plan',
94
- 'Use /plan to create a plan and the execution dashboard will populate here.',
95
- [],
96
- DEFAULT_PANEL_PALETTE,
97
- ),
98
- },
99
- ],
100
- palette: DEFAULT_PANEL_PALETTE,
101
- });
102
- }
103
- return this.renderPlan(plan, width, height);
104
- });
105
- }
106
-
107
- // --------------------------------------------------------------------------
108
- // Empty state
109
- // --------------------------------------------------------------------------
110
-
111
- private renderPlan(plan: ExecutionPlan, width: number, height: number): Line[] {
112
- const phaseOrder: string[] = [];
113
- const byPhase = new Map<string, PlanItem[]>();
114
- for (const item of plan.items) {
115
- if (!byPhase.has(item.phase)) {
116
- byPhase.set(item.phase, []);
117
- phaseOrder.push(item.phase);
118
- }
119
- byPhase.get(item.phase)!.push(item);
120
- }
121
-
122
- // Build a set of complete item IDs for dependency blocking detection
123
- const completeIds = new Set(
124
- plan.items
125
- .filter((i) => i.status === 'complete' || i.status === 'skipped')
126
- .map((i) => i.id),
127
- );
128
-
129
- let rowCount = 0;
130
- let selectedLineIndex = 0;
131
- const planLines: Line[] = [];
132
-
133
- for (const phase of phaseOrder) {
134
- const items = byPhase.get(phase)!;
135
- planLines.push(this.renderPhaseHeaderLine(phase, items, width));
136
- for (const item of items) {
137
- const isSelected = rowCount === this.selectedIndex;
138
- if (isSelected) selectedLineIndex = planLines.length;
139
- const isBlocked =
140
- item.status === 'pending' &&
141
- item.dependencies !== undefined &&
142
- item.dependencies.length > 0 &&
143
- !item.dependencies.every((depId) => completeIds.has(depId));
144
-
145
- planLines.push(this.renderItem(item, isSelected, isBlocked, width));
146
- rowCount++;
147
- }
148
- }
149
-
150
- this.totalRows = rowCount;
151
- if (this.selectedIndex >= this.totalRows) {
152
- this.selectedIndex = Math.max(0, this.totalRows - 1);
153
- }
154
-
155
- const total = plan.items.length;
156
- const done = plan.items.filter((i) => i.status === 'complete' || i.status === 'skipped').length;
157
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
158
- const summary: PanelWorkspaceSection = {
159
- title: 'Summary',
160
- lines: [
161
- buildPanelLine(width, [
162
- [' Status ', DEFAULT_PANEL_PALETTE.label],
163
- [plan.status, this.statusColor(plan.status)],
164
- [' Progress ', DEFAULT_PANEL_PALETTE.label],
165
- [`${pct}%`, pct === 100 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.info],
166
- [' Items ', DEFAULT_PANEL_PALETTE.label],
167
- [`${done}/${total}`, DEFAULT_PANEL_PALETTE.value],
168
- ]),
169
- ],
170
- };
171
-
172
- const planSection = resolveScrollablePanelSection(width, height, {
173
- intro: plan.title,
174
- footerLines: [
175
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim]]),
176
- ],
177
- palette: DEFAULT_PANEL_PALETTE,
178
- beforeSections: [summary],
179
- section: {
180
- title: 'Execution Plan',
181
- scrollableLines: planLines,
182
- selectedIndex: selectedLineIndex,
183
- scrollOffset: this.scrollOffset,
184
- minRows: 8,
185
- },
186
- });
187
- this.scrollOffset = planSection.scrollOffset;
188
-
189
- return buildPanelWorkspace(width, height, {
190
- title: ` Plan Dashboard`,
191
- intro: plan.title,
192
- sections: [
193
- summary,
194
- planSection.section,
195
- ],
196
- footerLines: [
197
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim]]),
198
- ],
199
- palette: DEFAULT_PANEL_PALETTE,
200
- });
201
- }
202
-
203
- // --------------------------------------------------------------------------
204
- // Header — title + overall completion percentage
205
- // --------------------------------------------------------------------------
206
-
207
- private renderPhaseHeaderLine(phase: string, items: PlanItem[], width: number): Line {
208
- const done = items.filter(
209
- (i) => i.status === 'complete' || i.status === 'skipped',
210
- ).length;
211
- const total = items.length;
212
- const active = items.some((i) => i.status === 'in_progress');
213
- const failed = items.some((i) => i.status === 'failed');
214
-
215
- const phaseFg = failed ? '#ef4444' : active ? '#00ffff' : done === total ? '#22c55e' : '250';
216
-
217
- const barW = 8;
218
- const filledB = total > 0 ? Math.round((done / total) * barW) : 0;
219
- const bar = '#'.repeat(filledB) + '-'.repeat(barW - filledB);
220
-
221
- const progressText = `[${bar}] ${done}/${total}`;
222
- const phaseText = ` > ${phase}`;
223
- const spacer = Math.max(1, width - phaseText.length - progressText.length - 1);
224
- return buildPanelLine(width, [
225
- [phaseText, phaseFg],
226
- [' '.repeat(spacer), phaseFg],
227
- [progressText, phaseFg],
228
- [' ', phaseFg],
229
- ]);
230
- }
231
-
232
- // --------------------------------------------------------------------------
233
- // Individual item
234
- // --------------------------------------------------------------------------
235
-
236
- private renderItem(
237
- item: PlanItem,
238
- isSelected: boolean,
239
- isBlocked: boolean,
240
- width: number,
241
- ): Line {
242
- const icon = STATUS_ICON[item.status];
243
- const fg = isBlocked ? '238' : STATUS_FG[item.status];
244
- const dim = item.status === 'skipped' || isBlocked;
245
-
246
- // Indent blocked items to visually signal they depend on others
247
- const indent = isBlocked ? ' ' : ' ';
248
- const selectedMark = isSelected ? '▸' : ' ';
249
-
250
- let text = `${selectedMark}${indent}${icon} ${item.description}`;
251
-
252
- // Append agent ID for in-progress and complete items
253
- if (item.agentId && (item.status === 'in_progress' || item.status === 'complete')) {
254
- text += ` [${item.agentId}]`;
255
- }
256
-
257
- // Append dependency note for blocked items (first unmet dep description)
258
- if (isBlocked && item.dependencies && item.dependencies.length > 0) {
259
- text += ' (blocked)';
260
- }
261
-
262
- return buildPanelLine(width, [[text, fg, isSelected ? '#1e293b' : undefined]]);
263
- }
264
-
265
- private statusColor(status: ExecutionPlan['status']): string {
266
- return status === 'complete'
267
- ? DEFAULT_PANEL_PALETTE.good
268
- : status === 'failed'
269
- ? DEFAULT_PANEL_PALETTE.bad
270
- : status === 'active'
271
- ? DEFAULT_PANEL_PALETTE.info
272
- : DEFAULT_PANEL_PALETTE.dim;
273
- }
274
- }