@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,8 +1,37 @@
1
1
  import type { Line } from '../types/grid.ts';
2
- import { UIFactory } from '../renderer/ui-factory.ts';
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: '#94a3b8',
18
- in_progress: '#38bdf8',
19
- blocked: '#f59e0b',
20
- done: '#22c55e',
21
- failed: '#ef4444',
22
- cancelled: '#64748b',
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
- function line(text: string, width: number, style: Parameters<typeof UIFactory.stringToLine>[2] = {}): Line {
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', 'L', 'agent');
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
- const item = this.items[this.selectedIndex];
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 add <title>', summary: 'add a persistent item' },
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 text = `${status} ${item.title}${owner}${source}`;
123
- return line(text, width, {
124
- fg: selected ? '#e2e8f0' : STATUS_COLOR[item.status],
125
- bg: selected ? '#1e293b' : undefined,
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.items[this.selectedIndex];
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
- if (!force && plan.updatedAt === this.lastPlanUpdatedAt && this.items.length === plan.items.length) return;
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 Object.keys(STATUS_LABEL) as WorkPlanItemStatus[]) counts.set(status, 0);
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
- const active = this.items[this.selectedIndex];
153
- const header = [
154
- line(`Persistent Work Plan`, width, { fg: '#22d3ee', bold: true }),
155
- line(`Project: ${plan.projectRoot}`, width, { fg: '#cbd5e1' }),
156
- line(
157
- `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}`,
158
- width,
159
- { fg: '#94a3b8' },
160
- ),
161
- line(`Saved: ${this.store.filePath}`, width, { fg: '#64748b' }),
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
- header.push(line('', width));
165
- header.push(line(`Selected: ${active.id} ${statusName(active.status)} updated ${compactDate(active.updatedAt)}`, width, { fg: '#a5b4fc' }));
166
- if (active.notes) header.push(line(`Notes: ${active.notes}`, width, { fg: '#cbd5e1' }));
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
- return [
173
- 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' }),
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
  }