@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
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.
|
|
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
|
+
}
|
package/src/input/commands.ts
CHANGED
|
@@ -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
|
|
487
|
-
|
|
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
|
+
}
|