@pellux/goodvibes-tui 0.27.0 → 0.29.0
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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
+
import {
|
|
5
|
+
buildDetailBlock,
|
|
6
|
+
buildKeyboardHints,
|
|
7
|
+
buildKeyValueLine,
|
|
8
|
+
buildMeterLine,
|
|
9
|
+
buildPanelLine,
|
|
10
|
+
buildPanelListRow,
|
|
11
|
+
buildSummaryBlock,
|
|
12
|
+
DEFAULT_PANEL_PALETTE,
|
|
13
|
+
extendPalette,
|
|
14
|
+
type PanelPalette,
|
|
15
|
+
} from './polish.ts';
|
|
4
16
|
import type { WorkPlanItem, WorkPlanItemStatus, WorkPlanStore } from '../work-plans/work-plan-store.ts';
|
|
5
17
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
18
|
+
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
19
|
+
import { AgentInspectorPanel } from './agent-inspector-panel.ts';
|
|
20
|
+
import { WrfcPanel } from './wrfc-panel.ts';
|
|
21
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
22
|
+
|
|
23
|
+
type WorkPlanDraftField = 'title' | 'owner' | 'notes';
|
|
24
|
+
|
|
25
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
26
|
+
pending: '#94a3b8',
|
|
27
|
+
inProgress: '#38bdf8',
|
|
28
|
+
blocked: '#f59e0b',
|
|
29
|
+
done: '#22c55e',
|
|
30
|
+
failed: '#ef4444',
|
|
31
|
+
cancelled: '#64748b',
|
|
32
|
+
accent: '#a5b4fc',
|
|
33
|
+
selectBg: '#1e293b',
|
|
34
|
+
});
|
|
6
35
|
|
|
7
36
|
const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
|
|
8
37
|
pending: '[ ]',
|
|
@@ -14,17 +43,15 @@ const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
|
|
|
14
43
|
};
|
|
15
44
|
|
|
16
45
|
const STATUS_COLOR: Record<WorkPlanItemStatus, string> = {
|
|
17
|
-
pending:
|
|
18
|
-
in_progress:
|
|
19
|
-
blocked:
|
|
20
|
-
done:
|
|
21
|
-
failed:
|
|
22
|
-
cancelled:
|
|
46
|
+
pending: C.pending,
|
|
47
|
+
in_progress: C.inProgress,
|
|
48
|
+
blocked: C.blocked,
|
|
49
|
+
done: C.done,
|
|
50
|
+
failed: C.failed,
|
|
51
|
+
cancelled: C.cancelled,
|
|
23
52
|
};
|
|
24
53
|
|
|
25
|
-
|
|
26
|
-
return UIFactory.stringToLine(text.padEnd(width).slice(0, width), width, style);
|
|
27
|
-
}
|
|
54
|
+
const STATUS_ORDER: WorkPlanItemStatus[] = ['pending', 'in_progress', 'blocked', 'done', 'failed', 'cancelled'];
|
|
28
55
|
|
|
29
56
|
function compactDate(value: number): string {
|
|
30
57
|
return new Date(value).toISOString().replace('T', ' ').slice(0, 16);
|
|
@@ -37,9 +64,21 @@ function statusName(status: WorkPlanItemStatus): string {
|
|
|
37
64
|
export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
38
65
|
private items: readonly WorkPlanItem[] = [];
|
|
39
66
|
private lastPlanUpdatedAt = 0;
|
|
67
|
+
private lastStatus: string | null = null;
|
|
68
|
+
|
|
69
|
+
// Draft-input state for 'a' (add) / 'e' (edit): a small modal-like field
|
|
70
|
+
// editor inline in the header, following the same draft-buffer pattern as
|
|
71
|
+
// ProjectPlanningPanel's typed-answer draft (Tab cycles fields, Enter
|
|
72
|
+
// saves, Esc cancels, Backspace edits the active field).
|
|
73
|
+
private draftMode: 'add' | 'edit' | null = null;
|
|
74
|
+
private draftField: WorkPlanDraftField = 'title';
|
|
75
|
+
private draftTitle = '';
|
|
76
|
+
private draftOwner = '';
|
|
77
|
+
private draftNotes = '';
|
|
78
|
+
private draftEditingItemId: string | null = null;
|
|
40
79
|
|
|
41
80
|
constructor(private readonly store: WorkPlanStore) {
|
|
42
|
-
super('work-plan', 'Work Plan', '
|
|
81
|
+
super('work-plan', 'Work Plan', '◧', 'agent');
|
|
43
82
|
this.showSelectionGutter = true;
|
|
44
83
|
}
|
|
45
84
|
|
|
@@ -48,19 +87,27 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
48
87
|
this.refresh();
|
|
49
88
|
}
|
|
50
89
|
|
|
90
|
+
protected override getPalette(): PanelPalette {
|
|
91
|
+
return C;
|
|
92
|
+
}
|
|
93
|
+
|
|
51
94
|
render(width: number, height: number): Line[] {
|
|
52
95
|
this.refresh();
|
|
53
96
|
return this.renderList(width, height, {
|
|
54
97
|
title: 'Work Plan',
|
|
55
98
|
header: this.renderHeader(width),
|
|
56
99
|
footer: this.renderFooter(width),
|
|
57
|
-
emptyMessage: 'No work plan items yet',
|
|
58
100
|
});
|
|
59
101
|
}
|
|
60
102
|
|
|
61
103
|
handleInput(key: string): boolean {
|
|
62
104
|
if (this.lastError !== null) this.clearError();
|
|
63
|
-
|
|
105
|
+
|
|
106
|
+
if (this.draftMode) {
|
|
107
|
+
return this.handleDraftInput(key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const item = this.getSelectedItem();
|
|
64
111
|
try {
|
|
65
112
|
switch (key) {
|
|
66
113
|
case ' ':
|
|
@@ -95,6 +142,24 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
95
142
|
case 'r':
|
|
96
143
|
this.refresh(true);
|
|
97
144
|
return true;
|
|
145
|
+
case 'a':
|
|
146
|
+
this.beginAddDraft();
|
|
147
|
+
return true;
|
|
148
|
+
case 'e':
|
|
149
|
+
if (!item) return false;
|
|
150
|
+
this.beginEditDraft(item);
|
|
151
|
+
return true;
|
|
152
|
+
case 'x':
|
|
153
|
+
this.exportMarkdown();
|
|
154
|
+
return true;
|
|
155
|
+
// 'i'/'w' jump to the selected item's linked agent/WRFC chain. The
|
|
156
|
+
// actual navigation happens in handlePanelIntegrationAction (it
|
|
157
|
+
// needs the PanelManager); consuming the key here just requires the
|
|
158
|
+
// linked target to exist so the router below fires next.
|
|
159
|
+
case 'i':
|
|
160
|
+
return item?.linked?.agentId !== undefined;
|
|
161
|
+
case 'w':
|
|
162
|
+
return item?.linked?.wrfcId !== undefined;
|
|
98
163
|
default:
|
|
99
164
|
return super.handleInput(key);
|
|
100
165
|
}
|
|
@@ -104,14 +169,179 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
104
169
|
}
|
|
105
170
|
}
|
|
106
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Cross-panel jumps: 'i' opens the Inspector focused on the selected
|
|
174
|
+
* item's linked agent; 'w' opens the WRFC panel focused on its linked
|
|
175
|
+
* chain.
|
|
176
|
+
*/
|
|
177
|
+
handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
|
|
178
|
+
const item = this.getSelectedItem();
|
|
179
|
+
if (!item) return false;
|
|
180
|
+
if (key === 'i') {
|
|
181
|
+
const agentId = item.linked?.agentId;
|
|
182
|
+
if (!agentId) return false;
|
|
183
|
+
const inspector = ctx.panelManager.open('inspector');
|
|
184
|
+
if (inspector instanceof AgentInspectorPanel) {
|
|
185
|
+
inspector.inspectAgent(agentId);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (key === 'w') {
|
|
191
|
+
const wrfcId = item.linked?.wrfcId;
|
|
192
|
+
if (!wrfcId) return false;
|
|
193
|
+
const wrfc = ctx.panelManager.open('wrfc');
|
|
194
|
+
if (wrfc instanceof WrfcPanel) {
|
|
195
|
+
wrfc.selectChain(wrfcId);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// -------------------------------------------------------------------------
|
|
204
|
+
// Draft-input (add/edit)
|
|
205
|
+
// -------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
private handleDraftInput(key: string): boolean {
|
|
208
|
+
if (key === 'escape') {
|
|
209
|
+
this.cancelDraft();
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (key === 'tab') {
|
|
213
|
+
this.cycleDraftField();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (key === 'enter' || key === 'return') {
|
|
217
|
+
this.commitDraft();
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
if (isTextBackspace(key)) {
|
|
221
|
+
this.setDraftFieldValue(this.getDraftFieldValue().slice(0, -1));
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (key === 'space') {
|
|
225
|
+
this.setDraftFieldValue(`${this.getDraftFieldValue()} `);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (this.isPrintableKey(key)) {
|
|
229
|
+
this.setDraftFieldValue(`${this.getDraftFieldValue()}${key}`);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// Absorb everything else (up/down/pageup/etc.) while drafting.
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private beginAddDraft(): void {
|
|
237
|
+
this.draftMode = 'add';
|
|
238
|
+
this.draftField = 'title';
|
|
239
|
+
this.draftTitle = '';
|
|
240
|
+
this.draftOwner = '';
|
|
241
|
+
this.draftNotes = '';
|
|
242
|
+
this.draftEditingItemId = null;
|
|
243
|
+
this.lastStatus = null;
|
|
244
|
+
this.needsRender = true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private beginEditDraft(item: WorkPlanItem): void {
|
|
248
|
+
this.draftMode = 'edit';
|
|
249
|
+
this.draftField = 'title';
|
|
250
|
+
this.draftTitle = item.title;
|
|
251
|
+
this.draftOwner = item.owner ?? '';
|
|
252
|
+
this.draftNotes = item.notes ?? '';
|
|
253
|
+
this.draftEditingItemId = item.id;
|
|
254
|
+
this.lastStatus = null;
|
|
255
|
+
this.needsRender = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private cancelDraft(): void {
|
|
259
|
+
this.draftMode = null;
|
|
260
|
+
this.draftEditingItemId = null;
|
|
261
|
+
this.needsRender = true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private cycleDraftField(): void {
|
|
265
|
+
const order: WorkPlanDraftField[] = ['title', 'owner', 'notes'];
|
|
266
|
+
const next = order[(order.indexOf(this.draftField) + 1) % order.length];
|
|
267
|
+
if (next) this.draftField = next;
|
|
268
|
+
this.needsRender = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private getDraftFieldValue(): string {
|
|
272
|
+
switch (this.draftField) {
|
|
273
|
+
case 'title': return this.draftTitle;
|
|
274
|
+
case 'owner': return this.draftOwner;
|
|
275
|
+
case 'notes': return this.draftNotes;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private setDraftFieldValue(value: string): void {
|
|
280
|
+
switch (this.draftField) {
|
|
281
|
+
case 'title': this.draftTitle = value; break;
|
|
282
|
+
case 'owner': this.draftOwner = value; break;
|
|
283
|
+
case 'notes': this.draftNotes = value; break;
|
|
284
|
+
}
|
|
285
|
+
this.needsRender = true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private commitDraft(): void {
|
|
289
|
+
const title = this.draftTitle.trim();
|
|
290
|
+
if (!title) {
|
|
291
|
+
this.setError('Work plan item title is required.');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
if (this.draftMode === 'add') {
|
|
296
|
+
this.store.addItem(title, {
|
|
297
|
+
owner: this.draftOwner.trim() || undefined,
|
|
298
|
+
notes: this.draftNotes.trim() || undefined,
|
|
299
|
+
source: 'tui-panel',
|
|
300
|
+
});
|
|
301
|
+
this.lastStatus = 'Item added.';
|
|
302
|
+
} else if (this.draftMode === 'edit' && this.draftEditingItemId) {
|
|
303
|
+
this.store.updateItem(this.draftEditingItemId, {
|
|
304
|
+
title,
|
|
305
|
+
owner: this.draftOwner.trim() || null,
|
|
306
|
+
notes: this.draftNotes.trim() || null,
|
|
307
|
+
});
|
|
308
|
+
this.lastStatus = 'Item updated.';
|
|
309
|
+
}
|
|
310
|
+
this.draftMode = null;
|
|
311
|
+
this.draftEditingItemId = null;
|
|
312
|
+
this.refresh(true);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
this.setError(summarizeError(error));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private isPrintableKey(key: string): boolean {
|
|
319
|
+
return key.length === 1 && key >= ' ';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private exportMarkdown(): void {
|
|
323
|
+
try {
|
|
324
|
+
const { path } = this.store.exportMarkdown();
|
|
325
|
+
this.lastStatus = `Exported to ${path}`;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
this.setError(summarizeError(error));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
107
331
|
protected getItems(): readonly WorkPlanItem[] {
|
|
108
332
|
return this.items;
|
|
109
333
|
}
|
|
110
334
|
|
|
335
|
+
protected override getEmptyStateMessage(): string {
|
|
336
|
+
return ' No work plan items yet';
|
|
337
|
+
}
|
|
338
|
+
|
|
111
339
|
protected getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
340
|
+
// WO-160: '/work-plan add <title>' dropped — 'a' already opens an
|
|
341
|
+
// in-panel add-item draft even from this empty state (see
|
|
342
|
+
// beginAddDraft), so the printed command was a pure action substitute.
|
|
112
343
|
return [
|
|
113
|
-
{ command: '/work-plan
|
|
114
|
-
{ command: '/work-plan list', summary: 'print the current plan' },
|
|
344
|
+
{ command: '/work-plan list', summary: 'print the current plan to the shell' },
|
|
115
345
|
];
|
|
116
346
|
}
|
|
117
347
|
|
|
@@ -119,16 +349,18 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
119
349
|
const status = STATUS_LABEL[item.status];
|
|
120
350
|
const owner = item.owner ? ` @${item.owner}` : '';
|
|
121
351
|
const source = item.source ? ` (${item.source})` : '';
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
bold: selected || item.status === 'in_progress',
|
|
127
|
-
|
|
352
|
+
const reserve = status.length + owner.length + source.length + 4;
|
|
353
|
+
const title = truncateDisplay(item.title, Math.max(6, width - reserve));
|
|
354
|
+
return buildPanelListRow(width, [
|
|
355
|
+
{ text: `${status} `, fg: STATUS_COLOR[item.status], bold: item.status === 'in_progress' },
|
|
356
|
+
{ text: title, fg: selected ? C.value : C.label, bold: selected || item.status === 'in_progress' },
|
|
357
|
+
{ text: owner, fg: C.accent },
|
|
358
|
+
{ text: source, fg: C.dim },
|
|
359
|
+
], C, { selected, selectedBg: C.selectBg });
|
|
128
360
|
}
|
|
129
361
|
|
|
130
362
|
private setSelectedStatus(status: WorkPlanItemStatus): boolean {
|
|
131
|
-
const item = this.
|
|
363
|
+
const item = this.getSelectedItem();
|
|
132
364
|
if (!item) return false;
|
|
133
365
|
this.store.setItemStatus(item.id, status);
|
|
134
366
|
this.refresh();
|
|
@@ -137,7 +369,10 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
137
369
|
|
|
138
370
|
private refresh(force = false): void {
|
|
139
371
|
const plan = this.store.getActivePlan();
|
|
140
|
-
|
|
372
|
+
// updatedAt is bumped by every store mutation (addItem/updateItem/
|
|
373
|
+
// removeItem/clearCompleted), so it alone is a sufficient staleness
|
|
374
|
+
// check — no need to also compare item counts.
|
|
375
|
+
if (!force && plan.updatedAt === this.lastPlanUpdatedAt) return;
|
|
141
376
|
this.items = plan.items;
|
|
142
377
|
this.lastPlanUpdatedAt = plan.updatedAt;
|
|
143
378
|
this.clampSelection();
|
|
@@ -145,32 +380,151 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
|
|
|
145
380
|
}
|
|
146
381
|
|
|
147
382
|
private renderHeader(width: number): Line[] {
|
|
383
|
+
if (this.draftMode) return this.renderDraftForm(width);
|
|
148
384
|
const plan = this.store.getActivePlan();
|
|
149
385
|
const counts = new Map<WorkPlanItemStatus, number>();
|
|
150
|
-
for (const status of
|
|
386
|
+
for (const status of STATUS_ORDER) counts.set(status, 0);
|
|
151
387
|
for (const item of plan.items) counts.set(item.status, (counts.get(item.status) ?? 0) + 1);
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
388
|
+
|
|
389
|
+
const total = plan.items.length;
|
|
390
|
+
const done = (counts.get('done') ?? 0) + (counts.get('cancelled') ?? 0);
|
|
391
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
392
|
+
const meterWidth = Math.max(10, Math.min(26, width - 34));
|
|
393
|
+
|
|
394
|
+
const postureLines: Line[] = [
|
|
395
|
+
buildPanelLine(width, [
|
|
396
|
+
[' Project ', C.label],
|
|
397
|
+
[truncateDisplay(plan.projectRoot, Math.max(8, width - 11)), C.value],
|
|
398
|
+
]),
|
|
399
|
+
buildMeterLine(width, Math.round((pct / 100) * meterWidth), meterWidth, {
|
|
400
|
+
filled: pct === 100 ? C.done : C.inProgress,
|
|
401
|
+
empty: C.empty,
|
|
402
|
+
label: C.label,
|
|
403
|
+
}, { prefix: ' Progress ', suffix: ` ${done}/${total} (${pct}%) ` }),
|
|
404
|
+
buildKeyValueLine(width, [
|
|
405
|
+
{ label: 'pending', value: String(counts.get('pending') ?? 0), valueColor: (counts.get('pending') ?? 0) > 0 ? C.pending : C.dim },
|
|
406
|
+
{ label: 'active', value: String(counts.get('in_progress') ?? 0), valueColor: (counts.get('in_progress') ?? 0) > 0 ? C.inProgress : C.dim },
|
|
407
|
+
{ label: 'blocked', value: String(counts.get('blocked') ?? 0), valueColor: (counts.get('blocked') ?? 0) > 0 ? C.blocked : C.dim },
|
|
408
|
+
{ label: 'done', value: String(counts.get('done') ?? 0), valueColor: (counts.get('done') ?? 0) > 0 ? C.done : C.dim },
|
|
409
|
+
], C),
|
|
410
|
+
// WO-160: dropped the printed '/work-plan add <title>' guidance line —
|
|
411
|
+
// 'a' already opens an in-panel add-item draft (see beginAddDraft /
|
|
412
|
+
// handleDraftInput) and is advertised in the footer's 'a: add' hint,
|
|
413
|
+
// so the printed command was a pure action substitute.
|
|
162
414
|
];
|
|
415
|
+
|
|
416
|
+
const header: Line[] = buildSummaryBlock(width, 'Persistent Work Plan', postureLines, C);
|
|
417
|
+
|
|
418
|
+
const active = this.getSelectedItem();
|
|
163
419
|
if (active) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
420
|
+
const detailRows: Line[] = [
|
|
421
|
+
buildPanelLine(width, [
|
|
422
|
+
[' ', C.label],
|
|
423
|
+
[STATUS_LABEL[active.status], STATUS_COLOR[active.status]],
|
|
424
|
+
[' ', C.label],
|
|
425
|
+
[truncateDisplay(active.title, Math.max(8, width - 6)), C.value],
|
|
426
|
+
]),
|
|
427
|
+
buildKeyValueLine(width, [
|
|
428
|
+
{ label: 'status', value: statusName(active.status), valueColor: STATUS_COLOR[active.status] },
|
|
429
|
+
...(active.owner ? [{ label: 'owner', value: active.owner, valueColor: C.accent }] : []),
|
|
430
|
+
{ label: 'updated', value: compactDate(active.updatedAt), valueColor: C.dim },
|
|
431
|
+
], C),
|
|
432
|
+
];
|
|
433
|
+
if (active.source) {
|
|
434
|
+
detailRows.push(buildPanelLine(width, [[' source ', C.label], [active.source, C.info]]));
|
|
435
|
+
}
|
|
436
|
+
if (active.notes) {
|
|
437
|
+
detailRows.push(buildPanelLine(width, [
|
|
438
|
+
[' notes ', C.label],
|
|
439
|
+
[truncateDisplay(active.notes, Math.max(8, width - 8)), C.value],
|
|
440
|
+
]));
|
|
441
|
+
}
|
|
442
|
+
const linkedSegments = this.buildLinkedSegments(active);
|
|
443
|
+
if (linkedSegments.length > 0) {
|
|
444
|
+
detailRows.push(buildPanelLine(width, [[' linked ', C.label], ...linkedSegments]));
|
|
445
|
+
}
|
|
446
|
+
header.push(...buildDetailBlock(width, 'Selected item', detailRows, C));
|
|
167
447
|
}
|
|
168
448
|
return header;
|
|
169
449
|
}
|
|
170
450
|
|
|
451
|
+
/** Renders item.linked (agentId/wrfcId/taskId/sessionId) with their jump keys. */
|
|
452
|
+
private buildLinkedSegments(item: WorkPlanItem): Array<[string, string]> {
|
|
453
|
+
const linked = item.linked;
|
|
454
|
+
if (!linked) return [];
|
|
455
|
+
const segments: Array<[string, string]> = [];
|
|
456
|
+
if (linked.agentId) segments.push([`agent:${linked.agentId} (i) `, C.info]);
|
|
457
|
+
if (linked.wrfcId) segments.push([`wrfc:${linked.wrfcId} (w) `, C.info]);
|
|
458
|
+
if (linked.taskId) segments.push([`task:${linked.taskId} `, C.dim]);
|
|
459
|
+
if (linked.sessionId) segments.push([`session:${linked.sessionId} `, C.dim]);
|
|
460
|
+
return segments;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private renderDraftForm(width: number): Line[] {
|
|
464
|
+
const title = this.draftMode === 'add' ? 'Add Work Plan Item' : 'Edit Work Plan Item';
|
|
465
|
+
const field = (label: string, value: string, key: WorkPlanDraftField): Line => {
|
|
466
|
+
const active = key === this.draftField;
|
|
467
|
+
const text = active ? `${value}▏` : (value || '(empty)');
|
|
468
|
+
return buildPanelLine(width, [
|
|
469
|
+
[` ${label.padEnd(6)} `, C.label],
|
|
470
|
+
[truncateDisplay(text, Math.max(8, width - 10)), active ? C.value : C.dim],
|
|
471
|
+
]);
|
|
472
|
+
};
|
|
473
|
+
return buildSummaryBlock(width, title, [
|
|
474
|
+
buildPanelLine(width, [[' Tab next field Enter save Esc cancel', C.dim]]),
|
|
475
|
+
field('Title', this.draftTitle, 'title'),
|
|
476
|
+
field('Owner', this.draftOwner, 'owner'),
|
|
477
|
+
field('Notes', this.draftNotes, 'notes'),
|
|
478
|
+
], C);
|
|
479
|
+
}
|
|
480
|
+
|
|
171
481
|
private renderFooter(width: number): Line[] {
|
|
172
|
-
|
|
173
|
-
|
|
482
|
+
if (this.draftMode) {
|
|
483
|
+
return [
|
|
484
|
+
buildKeyboardHints(width, [
|
|
485
|
+
{ keys: 'Tab', label: 'next field' },
|
|
486
|
+
{ keys: 'Enter', label: 'save' },
|
|
487
|
+
{ keys: 'Esc', label: 'cancel' },
|
|
488
|
+
], C),
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
const hasItem = this.items.length > 0;
|
|
492
|
+
if (!hasItem) {
|
|
493
|
+
return [
|
|
494
|
+
buildKeyboardHints(width, [
|
|
495
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
496
|
+
{ keys: 'a', label: 'add' },
|
|
497
|
+
], C),
|
|
498
|
+
];
|
|
499
|
+
}
|
|
500
|
+
const active = this.getSelectedItem();
|
|
501
|
+
const hints: Array<{ keys: string; label: string }> = [
|
|
502
|
+
{ keys: this.items.length > 0 ? `${this.selectedIndex + 1}/${this.items.length}` : '0/0', label: 'item' },
|
|
503
|
+
{ keys: 'Enter', label: 'cycle status' },
|
|
504
|
+
{ keys: '1-6', label: 'set status' },
|
|
505
|
+
{ keys: 'a', label: 'add' },
|
|
506
|
+
{ keys: 'e', label: 'edit' },
|
|
507
|
+
{ keys: 'd', label: 'delete' },
|
|
508
|
+
{ keys: 'c', label: 'clear done' },
|
|
509
|
+
{ keys: 'x', label: 'export' },
|
|
510
|
+
{ keys: 'r', label: 'refresh' },
|
|
511
|
+
];
|
|
512
|
+
if (active?.linked?.agentId) hints.push({ keys: 'i', label: 'jump agent' });
|
|
513
|
+
if (active?.linked?.wrfcId) hints.push({ keys: 'w', label: 'jump wrfc' });
|
|
514
|
+
const lines: Line[] = [
|
|
515
|
+
buildKeyboardHints(width, hints, C),
|
|
516
|
+
buildPanelLine(width, [
|
|
517
|
+
[' 1', C.info], [' pending ', C.dim],
|
|
518
|
+
['2', C.info], [' active ', C.dim],
|
|
519
|
+
['3', C.info], [' blocked ', C.dim],
|
|
520
|
+
['4', C.info], [' done ', C.dim],
|
|
521
|
+
['5', C.info], [' failed ', C.dim],
|
|
522
|
+
['6', C.info], [' cancelled', C.dim],
|
|
523
|
+
]),
|
|
174
524
|
];
|
|
525
|
+
if (this.lastStatus) {
|
|
526
|
+
lines.push(buildPanelLine(width, [[` ${this.lastStatus}`, C.good]]));
|
|
527
|
+
}
|
|
528
|
+
return lines;
|
|
175
529
|
}
|
|
176
530
|
}
|