@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.84",
3
+ "version": "0.19.86",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -97,7 +97,7 @@
97
97
  "@anthropic-ai/vertex-sdk": "^0.16.0",
98
98
  "@ast-grep/napi": "^0.42.0",
99
99
  "@aws/bedrock-token-generator": "^1.1.0",
100
- "@pellux/goodvibes-sdk": "0.33.19",
100
+ "@pellux/goodvibes-sdk": "0.33.21",
101
101
  "bash-language-server": "^5.6.0",
102
102
  "fuse.js": "^7.1.0",
103
103
  "graphql": "^16.13.2",
@@ -144,6 +144,7 @@ export interface CommandWorkspaceUiServices {
144
144
  bookmarkManager?: import('@pellux/goodvibes-sdk/platform/bookmarks').BookmarkManager;
145
145
  projectPlanningService?: import('@pellux/goodvibes-sdk/platform/knowledge').ProjectPlanningService;
146
146
  projectPlanningProjectId?: string;
147
+ workPlanStore?: import('../work-plans/work-plan-store.ts').WorkPlanStore;
147
148
  }
148
149
 
149
150
  export interface CommandWorkspaceServices
@@ -0,0 +1,169 @@
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 { requirePanelManager } from './runtime-services.ts';
5
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
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 openPanel(ctx: import('../command-registry.ts').CommandContext): void {
28
+ if (ctx.showPanel) {
29
+ ctx.showPanel('work-plan');
30
+ return;
31
+ }
32
+ const panelManager = requirePanelManager(ctx);
33
+ panelManager.open('work-plan');
34
+ panelManager.show();
35
+ ctx.renderRequest();
36
+ }
37
+
38
+ function formatList(store: WorkPlanStore): string {
39
+ const items = store.listItems();
40
+ if (items.length === 0) return 'Work plan is empty. Add one with /workplan add <title>.';
41
+ return [
42
+ `Work Plan (${items.length})`,
43
+ ...items.map((item) => {
44
+ const owner = item.owner ? ` @${item.owner}` : '';
45
+ return ` ${item.id} ${item.status.padEnd(11)} ${item.title}${owner}`;
46
+ }),
47
+ ].join('\n');
48
+ }
49
+
50
+ function parseAddArgs(args: string[]): { title: string; owner?: string; source?: string; notes?: string } {
51
+ const titleParts: string[] = [];
52
+ let owner: string | undefined;
53
+ let source: string | undefined;
54
+ let notes: string | undefined;
55
+ for (let i = 0; i < args.length; i++) {
56
+ const part = args[i] ?? '';
57
+ if (part === '--owner' && args[i + 1]) {
58
+ owner = args[++i];
59
+ continue;
60
+ }
61
+ if (part === '--source' && args[i + 1]) {
62
+ source = args[++i];
63
+ continue;
64
+ }
65
+ if (part === '--notes' && args[i + 1]) {
66
+ notes = args.slice(i + 1).join(' ').trim();
67
+ break;
68
+ }
69
+ titleParts.push(part);
70
+ }
71
+ return {
72
+ title: titleParts.join(' ').trim(),
73
+ ...(owner ? { owner } : {}),
74
+ ...(source ? { source } : {}),
75
+ ...(notes ? { notes } : {}),
76
+ };
77
+ }
78
+
79
+ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void {
80
+ registry.register({
81
+ name: 'workplan',
82
+ aliases: ['wp', 'todo'],
83
+ description: 'Track a persistent workspace-scoped work plan',
84
+ usage: '[panel|list|show|add <title> [--owner name] [--source label] [--notes text]|done <id>|start <id>|block <id>|fail <id>|cancel <id>|pending <id>|remove <id>|clear-done]',
85
+ argsHint: '[panel|add|list|done]',
86
+ handler(args, ctx) {
87
+ const store = getStore(ctx);
88
+ if (!store) {
89
+ ctx.print('Work plan store is not available in this runtime.');
90
+ return;
91
+ }
92
+ const subcommand = (args[0] ?? 'panel').toLowerCase();
93
+ try {
94
+ if (subcommand === 'panel' || subcommand === 'open') {
95
+ openPanel(ctx);
96
+ ctx.print('Opened work plan panel.');
97
+ return;
98
+ }
99
+ if (subcommand === 'list') {
100
+ ctx.print(formatList(store));
101
+ return;
102
+ }
103
+ if (subcommand === 'show' || subcommand === 'markdown') {
104
+ ctx.print(store.toMarkdown());
105
+ return;
106
+ }
107
+ if (subcommand === 'add') {
108
+ const parsed = parseAddArgs(args.slice(1));
109
+ if (!parsed.title) {
110
+ ctx.print('Usage: /workplan add <title> [--owner name] [--source label] [--notes text]');
111
+ return;
112
+ }
113
+ const addOptions = {
114
+ ...(parsed.owner ? { owner: parsed.owner } : {}),
115
+ source: parsed.source ?? 'manual',
116
+ ...(parsed.notes ? { notes: parsed.notes } : {}),
117
+ };
118
+ const item = store.addItem(parsed.title, addOptions);
119
+ openPanel(ctx);
120
+ ctx.print(`Added work plan item ${item.id}.`);
121
+ return;
122
+ }
123
+ if (subcommand === 'remove' || subcommand === 'delete' || subcommand === 'rm') {
124
+ const id = args[1];
125
+ if (!id) {
126
+ ctx.print(`Usage: /workplan ${subcommand} <id>`);
127
+ return;
128
+ }
129
+ const item = store.removeItem(id);
130
+ ctx.print(`Removed work plan item ${item.id}: ${item.title}`);
131
+ return;
132
+ }
133
+ if (subcommand === 'clear-done' || subcommand === 'clear-completed') {
134
+ const count = store.clearCompleted();
135
+ ctx.print(`Cleared ${count} completed/cancelled work plan item${count === 1 ? '' : 's'}.`);
136
+ return;
137
+ }
138
+ if (subcommand === 'cycle' || subcommand === 'toggle') {
139
+ const id = args[1];
140
+ if (!id) {
141
+ ctx.print(`Usage: /workplan ${subcommand} <id>`);
142
+ return;
143
+ }
144
+ const item = store.cycleItemStatus(id);
145
+ ctx.print(`Updated ${item.id}: ${item.status}.`);
146
+ return;
147
+ }
148
+ const status = STATUS_COMMANDS[subcommand];
149
+ if (status) {
150
+ const id = args[1];
151
+ if (!id) {
152
+ ctx.print(`Usage: /workplan ${subcommand} <id>`);
153
+ return;
154
+ }
155
+ const item = store.setItemStatus(id, status);
156
+ ctx.print(`Updated ${item.id}: ${item.status}.`);
157
+ return;
158
+ }
159
+ if (WORK_PLAN_STATUSES.includes(subcommand as WorkPlanItemStatus)) {
160
+ ctx.print(`Usage: /workplan ${subcommand} <id>`);
161
+ return;
162
+ }
163
+ ctx.print(`Unknown workplan subcommand: ${subcommand}`);
164
+ } catch (error) {
165
+ ctx.print(summarizeError(error));
166
+ }
167
+ },
168
+ });
169
+ }
@@ -55,6 +55,7 @@ import { registerQrcodeRuntimeCommands } from './commands/qrcode-runtime.ts';
55
55
  import { registerOnboardingRuntimeCommands } from './commands/onboarding-runtime.ts';
56
56
  import { registerTtsRuntimeCommands } from './commands/tts-runtime.ts';
57
57
  import { registerCloudflareRuntimeCommands } from './commands/cloudflare-runtime.ts';
58
+ import { registerWorkPlanRuntimeCommands } from './commands/work-plan-runtime.ts';
58
59
 
59
60
  /**
60
61
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -104,6 +105,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
104
105
  registerOnboardingRuntimeCommands(registry);
105
106
  registerTtsRuntimeCommands(registry);
106
107
  registerCloudflareRuntimeCommands(registry);
108
+ registerWorkPlanRuntimeCommands(registry);
107
109
  registerLocalRuntimeCommands(registry);
108
110
  registerSessionWorkflowCommands(registry);
109
111
  registerDiscoveryRuntimeCommands(registry);
package/src/main.ts CHANGED
@@ -16,7 +16,6 @@ import { PermissionPromptUI } from './permissions/prompt.ts';
16
16
  import { CommandRegistry } from './input/command-registry.ts';
17
17
  import type { CommandContext } from './input/command-registry.ts';
18
18
  import { renderProcessIndicator } from './renderer/process-indicator.ts';
19
- import { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
20
19
  import { registerBuiltinCommands } from './input/commands.ts';
21
20
  import { ScheduleManager } from '@pellux/goodvibes-sdk/platform/tools';
22
21
  import { InputHistory } from './input/input-history.ts';
@@ -54,6 +53,7 @@ import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './au
54
53
  import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
55
54
  import { ProjectPlanningCoordinator } from './planning/project-planning-coordinator.ts';
56
55
  import { buildCommandArgsHint } from './input/command-args-hint.ts';
56
+ import { summarizeRunningAgents } from './renderer/process-summary.ts';
57
57
 
58
58
  const ALT_SCREEN_ENTER = '\x1b[?1049h';
59
59
  const ALT_SCREEN_EXIT = '\x1b[?1049l';
@@ -483,17 +483,8 @@ async function main() {
483
483
  (a) => a.status === 'running' || a.status === 'pending',
484
484
  );
485
485
  const runtimeAgents = agentSnapshot.active;
486
- const runningAgentIds = new Set<string>();
487
- let runningAgentProgress: string | undefined;
488
- for (const agent of managerAgents) {
489
- runningAgentIds.add(agent.id);
490
- if (!runningAgentProgress && agent.progress) runningAgentProgress = agent.progress;
491
- }
492
- for (const agent of runtimeAgents) {
493
- runningAgentIds.add(agent.id);
494
- if (!runningAgentProgress && agent.latestProgress) runningAgentProgress = agent.latestProgress;
495
- }
496
- const runningAgentCount = runningAgentIds.size;
486
+ const runningAgentSummary = summarizeRunningAgents(managerAgents, runtimeAgents, ctx.services.wrfcController.listChains());
487
+ const runningAgentCount = runningAgentSummary.count;
497
488
  const runningProcessCount = processManager.list().filter((p) => !p.status.startsWith('done')).length;
498
489
  const cw = getPromptContentWidth();
499
490
  const promptInfo = input.getWrappedPromptInfo(cw);
@@ -540,7 +531,7 @@ async function main() {
540
531
  runningAgentCount,
541
532
  runningProcessCount,
542
533
  indicatorFocused: input.indicatorFocused,
543
- runningAgentProgress,
534
+ runningAgentProgress: runningAgentSummary.progress,
544
535
  composerMode: composerState.modeLabel,
545
536
  composerStatus: composerState.statusLabel,
546
537
  composerFlags: composerState.flags,
@@ -6,6 +6,7 @@ import { ToolInspectorPanel } from '../tool-inspector-panel.ts';
6
6
  import { WrfcPanel } from '../wrfc-panel.ts';
7
7
  import { SchedulePanel } from '../schedule-panel.ts';
8
8
  import { ProjectPlanningPanel } from '../project-planning-panel.ts';
9
+ import { WorkPlanPanel } from '../work-plan-panel.ts';
9
10
  import type { ResolvedBuiltinPanelDeps } from './shared.ts';
10
11
  import { requireAutomationManager, requireUiServices } from './shared.ts';
11
12
 
@@ -79,6 +80,16 @@ export function registerAgentPanels(manager: PanelManager, deps: ResolvedBuiltin
79
80
  },
80
81
  });
81
82
 
83
+ manager.registerType({
84
+ id: 'work-plan',
85
+ name: 'Work Plan',
86
+ icon: 'L',
87
+ category: 'agent',
88
+ description: 'Persistent workspace checklist for multi-step work and cross-session task tracking',
89
+ preload: true,
90
+ factory: () => new WorkPlanPanel(deps.workPlanStore),
91
+ });
92
+
82
93
  manager.registerType({
83
94
  id: 'project-planning',
84
95
  name: 'Planning',
@@ -96,6 +96,8 @@ export interface BuiltinPanelDeps {
96
96
  projectPlanningService?: ProjectPlanningService;
97
97
  /** Stable workspace project id for project:<projectId> planning spaces. */
98
98
  projectPlanningProjectId?: string;
99
+ /** TUI-owned persistent work plan store. */
100
+ workPlanStore?: import('../../work-plans/work-plan-store.ts').WorkPlanStore;
99
101
  /** Shared system-messages panel instance attached from boot so low-priority chatter stays out of conversation. */
100
102
  systemMessagesPanel?: import('../system-messages-panel.ts').SystemMessagesPanel;
101
103
  /** Explicit UI-facing runtime services for agent/process/WRFC/remote panels and modals. */
@@ -124,6 +126,7 @@ export type ResolvedBuiltinPanelDeps = Omit<
124
126
  | 'adaptivePlanner'
125
127
  | 'projectPlanningService'
126
128
  | 'projectPlanningProjectId'
129
+ | 'workPlanStore'
127
130
  | 'policyRuntimeState'
128
131
  | 'systemMessagesPanel'
129
132
  > & {
@@ -137,6 +140,7 @@ export type ResolvedBuiltinPanelDeps = Omit<
137
140
  readonly adaptivePlanner: AdaptivePlanner;
138
141
  readonly projectPlanningService: ProjectPlanningService;
139
142
  readonly projectPlanningProjectId: string;
143
+ readonly workPlanStore: import('../../work-plans/work-plan-store.ts').WorkPlanStore;
140
144
  readonly policyRuntimeState: PolicyRuntimeState;
141
145
  readonly systemMessagesPanel: import('../system-messages-panel.ts').SystemMessagesPanel;
142
146
  };
@@ -198,6 +202,10 @@ export function resolveBuiltinPanelDeps(deps: BuiltinPanelDeps): ResolvedBuiltin
198
202
  uiServices.planning.projectPlanningProjectId,
199
203
  'Project planning project id must be wired at bootstrap for builtin panels.',
200
204
  ),
205
+ workPlanStore: requireBuiltinPanelDep(
206
+ uiServices.planning.workPlanStore,
207
+ 'Work plan store must be wired at bootstrap for builtin panels.',
208
+ ),
201
209
  policyRuntimeState: requireBuiltinPanelDep(
202
210
  uiServices.platform.policyRuntimeState,
203
211
  'Policy runtime state must be wired at bootstrap for builtin panels.',
@@ -0,0 +1,175 @@
1
+ import type { Line } from '../types/grid.ts';
2
+ import { UIFactory } from '../renderer/ui-factory.ts';
3
+ import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
+ import type { WorkPlanItem, WorkPlanItemStatus, WorkPlanStore } from '../work-plans/work-plan-store.ts';
5
+
6
+ const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
7
+ pending: '[ ]',
8
+ in_progress: '[>]',
9
+ blocked: '[!]',
10
+ done: '[x]',
11
+ failed: '[x]',
12
+ cancelled: '[-]',
13
+ };
14
+
15
+ const STATUS_COLOR: Record<WorkPlanItemStatus, string> = {
16
+ pending: '#94a3b8',
17
+ in_progress: '#38bdf8',
18
+ blocked: '#f59e0b',
19
+ done: '#22c55e',
20
+ failed: '#ef4444',
21
+ cancelled: '#64748b',
22
+ };
23
+
24
+ function line(text: string, width: number, style: Parameters<typeof UIFactory.stringToLine>[2] = {}): Line {
25
+ return UIFactory.stringToLine(text.padEnd(width).slice(0, width), width, style);
26
+ }
27
+
28
+ function compactDate(value: number): string {
29
+ return new Date(value).toISOString().replace('T', ' ').slice(0, 16);
30
+ }
31
+
32
+ function statusName(status: WorkPlanItemStatus): string {
33
+ return status.replace(/_/g, ' ');
34
+ }
35
+
36
+ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
37
+ private items: readonly WorkPlanItem[] = [];
38
+ private lastPlanUpdatedAt = 0;
39
+
40
+ constructor(private readonly store: WorkPlanStore) {
41
+ super('work-plan', 'Work Plan', 'L', 'agent');
42
+ this.showSelectionGutter = true;
43
+ }
44
+
45
+ onActivate(): void {
46
+ super.onActivate();
47
+ this.refresh();
48
+ }
49
+
50
+ render(width: number, height: number): Line[] {
51
+ this.refresh();
52
+ return this.renderList(width, height, {
53
+ title: 'Work Plan',
54
+ header: this.renderHeader(width),
55
+ footer: this.renderFooter(width),
56
+ emptyMessage: 'No work plan items yet',
57
+ });
58
+ }
59
+
60
+ handleInput(key: string): boolean {
61
+ if (this.lastError !== null) this.clearError();
62
+ const item = this.items[this.selectedIndex];
63
+ try {
64
+ switch (key) {
65
+ case ' ':
66
+ case 'return':
67
+ case 'enter':
68
+ if (!item) return false;
69
+ this.store.cycleItemStatus(item.id);
70
+ this.refresh();
71
+ return true;
72
+ case '1':
73
+ return this.setSelectedStatus('pending');
74
+ case '2':
75
+ return this.setSelectedStatus('in_progress');
76
+ case '3':
77
+ return this.setSelectedStatus('blocked');
78
+ case '4':
79
+ return this.setSelectedStatus('done');
80
+ case '5':
81
+ return this.setSelectedStatus('failed');
82
+ case '6':
83
+ return this.setSelectedStatus('cancelled');
84
+ case 'd':
85
+ case 'delete':
86
+ if (!item) return false;
87
+ this.store.removeItem(item.id);
88
+ this.refresh();
89
+ return true;
90
+ case 'c':
91
+ this.store.clearCompleted();
92
+ this.refresh();
93
+ return true;
94
+ case 'r':
95
+ this.refresh(true);
96
+ return true;
97
+ default:
98
+ return super.handleInput(key);
99
+ }
100
+ } catch (error) {
101
+ this.setError(error instanceof Error ? error.message : String(error));
102
+ return true;
103
+ }
104
+ }
105
+
106
+ protected getItems(): readonly WorkPlanItem[] {
107
+ return this.items;
108
+ }
109
+
110
+ protected getEmptyStateActions(): Array<{ command: string; summary: string }> {
111
+ return [
112
+ { command: '/workplan add <title>', summary: 'add a persistent item' },
113
+ { command: '/workplan list', summary: 'print the current plan' },
114
+ ];
115
+ }
116
+
117
+ protected renderItem(item: WorkPlanItem, _index: number, selected: boolean, width: number): Line {
118
+ const status = STATUS_LABEL[item.status];
119
+ const owner = item.owner ? ` @${item.owner}` : '';
120
+ const source = item.source ? ` (${item.source})` : '';
121
+ const text = `${status} ${item.title}${owner}${source}`;
122
+ return line(text, width, {
123
+ fg: selected ? '#e2e8f0' : STATUS_COLOR[item.status],
124
+ bg: selected ? '#1e293b' : undefined,
125
+ bold: selected || item.status === 'in_progress',
126
+ });
127
+ }
128
+
129
+ private setSelectedStatus(status: WorkPlanItemStatus): boolean {
130
+ const item = this.items[this.selectedIndex];
131
+ if (!item) return false;
132
+ this.store.setItemStatus(item.id, status);
133
+ this.refresh();
134
+ return true;
135
+ }
136
+
137
+ private refresh(force = false): void {
138
+ const plan = this.store.getActivePlan();
139
+ if (!force && plan.updatedAt === this.lastPlanUpdatedAt && this.items.length === plan.items.length) return;
140
+ this.items = plan.items;
141
+ this.lastPlanUpdatedAt = plan.updatedAt;
142
+ this.clampSelection();
143
+ this.needsRender = true;
144
+ }
145
+
146
+ private renderHeader(width: number): Line[] {
147
+ const plan = this.store.getActivePlan();
148
+ const counts = new Map<WorkPlanItemStatus, number>();
149
+ for (const status of Object.keys(STATUS_LABEL) as WorkPlanItemStatus[]) counts.set(status, 0);
150
+ for (const item of plan.items) counts.set(item.status, (counts.get(item.status) ?? 0) + 1);
151
+ const active = this.items[this.selectedIndex];
152
+ const header = [
153
+ line(`Persistent Work Plan`, width, { fg: '#22d3ee', bold: true }),
154
+ line(`Project: ${plan.projectRoot}`, width, { fg: '#cbd5e1' }),
155
+ line(
156
+ `Items: ${plan.items.length} pending ${counts.get('pending') ?? 0} active ${counts.get('in_progress') ?? 0} blocked ${counts.get('blocked') ?? 0} done ${counts.get('done') ?? 0}`,
157
+ width,
158
+ { fg: '#94a3b8' },
159
+ ),
160
+ line(`Saved: ${this.store.filePath}`, width, { fg: '#64748b' }),
161
+ ];
162
+ if (active) {
163
+ header.push(line('', width));
164
+ header.push(line(`Selected: ${active.id} ${statusName(active.status)} updated ${compactDate(active.updatedAt)}`, width, { fg: '#a5b4fc' }));
165
+ if (active.notes) header.push(line(`Notes: ${active.notes}`, width, { fg: '#cbd5e1' }));
166
+ }
167
+ return header;
168
+ }
169
+
170
+ private renderFooter(width: number): Line[] {
171
+ return [
172
+ line('Enter/Space cycle 1 pending 2 active 3 blocked 4 done 5 failed 6 cancelled d delete c clear done r refresh', width, { fg: '#94a3b8' }),
173
+ ];
174
+ }
175
+ }