@pellux/goodvibes-tui 0.28.0 → 1.0.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 (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -3,7 +3,6 @@ import { truncateDisplay } from '../utils/terminal-width.ts';
3
3
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
4
  import {
5
5
  buildDetailBlock,
6
- buildGuidanceLine,
7
6
  buildKeyboardHints,
8
7
  buildKeyValueLine,
9
8
  buildMeterLine,
@@ -16,6 +15,12 @@ import {
16
15
  } from './polish.ts';
17
16
  import type { WorkPlanItem, WorkPlanItemStatus, WorkPlanStore } from '../work-plans/work-plan-store.ts';
18
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';
19
24
 
20
25
  const C = extendPalette(DEFAULT_PANEL_PALETTE, {
21
26
  pending: '#94a3b8',
@@ -59,9 +64,21 @@ function statusName(status: WorkPlanItemStatus): string {
59
64
  export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
60
65
  private items: readonly WorkPlanItem[] = [];
61
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;
62
79
 
63
80
  constructor(private readonly store: WorkPlanStore) {
64
- super('work-plan', 'Work Plan', 'L', 'agent');
81
+ super('work-plan', 'Work Plan', '', 'agent');
65
82
  this.showSelectionGutter = true;
66
83
  }
67
84
 
@@ -85,7 +102,12 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
85
102
 
86
103
  handleInput(key: string): boolean {
87
104
  if (this.lastError !== null) this.clearError();
88
- const item = this.items[this.selectedIndex];
105
+
106
+ if (this.draftMode) {
107
+ return this.handleDraftInput(key);
108
+ }
109
+
110
+ const item = this.getSelectedItem();
89
111
  try {
90
112
  switch (key) {
91
113
  case ' ':
@@ -120,6 +142,24 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
120
142
  case 'r':
121
143
  this.refresh(true);
122
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;
123
163
  default:
124
164
  return super.handleInput(key);
125
165
  }
@@ -129,6 +169,165 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
129
169
  }
130
170
  }
131
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
+
132
331
  protected getItems(): readonly WorkPlanItem[] {
133
332
  return this.items;
134
333
  }
@@ -138,8 +337,10 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
138
337
  }
139
338
 
140
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.
141
343
  return [
142
- { command: '/work-plan add <title>', summary: 'add a persistent item that survives across sessions' },
143
344
  { command: '/work-plan list', summary: 'print the current plan to the shell' },
144
345
  ];
145
346
  }
@@ -159,7 +360,7 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
159
360
  }
160
361
 
161
362
  private setSelectedStatus(status: WorkPlanItemStatus): boolean {
162
- const item = this.items[this.selectedIndex];
363
+ const item = this.getSelectedItem();
163
364
  if (!item) return false;
164
365
  this.store.setItemStatus(item.id, status);
165
366
  this.refresh();
@@ -168,7 +369,10 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
168
369
 
169
370
  private refresh(force = false): void {
170
371
  const plan = this.store.getActivePlan();
171
- 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;
172
376
  this.items = plan.items;
173
377
  this.lastPlanUpdatedAt = plan.updatedAt;
174
378
  this.clampSelection();
@@ -176,6 +380,7 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
176
380
  }
177
381
 
178
382
  private renderHeader(width: number): Line[] {
383
+ if (this.draftMode) return this.renderDraftForm(width);
179
384
  const plan = this.store.getActivePlan();
180
385
  const counts = new Map<WorkPlanItemStatus, number>();
181
386
  for (const status of STATUS_ORDER) counts.set(status, 0);
@@ -202,12 +407,15 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
202
407
  { label: 'blocked', value: String(counts.get('blocked') ?? 0), valueColor: (counts.get('blocked') ?? 0) > 0 ? C.blocked : C.dim },
203
408
  { label: 'done', value: String(counts.get('done') ?? 0), valueColor: (counts.get('done') ?? 0) > 0 ? C.done : C.dim },
204
409
  ], C),
205
- buildGuidanceLine(width, '/work-plan add <title>', 'append a persistent item to the active plan', 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.
206
414
  ];
207
415
 
208
416
  const header: Line[] = buildSummaryBlock(width, 'Persistent Work Plan', postureLines, C);
209
417
 
210
- const active = this.items[this.selectedIndex];
418
+ const active = this.getSelectedItem();
211
419
  if (active) {
212
420
  const detailRows: Line[] = [
213
421
  buildPanelLine(width, [
@@ -231,29 +439,80 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
231
439
  [truncateDisplay(active.notes, Math.max(8, width - 8)), C.value],
232
440
  ]));
233
441
  }
442
+ const linkedSegments = this.buildLinkedSegments(active);
443
+ if (linkedSegments.length > 0) {
444
+ detailRows.push(buildPanelLine(width, [[' linked ', C.label], ...linkedSegments]));
445
+ }
234
446
  header.push(...buildDetailBlock(width, 'Selected item', detailRows, C));
235
447
  }
236
448
  return header;
237
449
  }
238
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
+
239
481
  private renderFooter(width: number): Line[] {
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
+ }
240
491
  const hasItem = this.items.length > 0;
241
492
  if (!hasItem) {
242
493
  return [
243
494
  buildKeyboardHints(width, [
244
495
  { keys: '↑/↓', label: 'navigate' },
496
+ { keys: 'a', label: 'add' },
245
497
  ], C),
246
498
  ];
247
499
  }
248
- return [
249
- buildKeyboardHints(width, [
250
- { keys: this.items.length > 0 ? `${this.selectedIndex + 1}/${this.items.length}` : '0/0', label: 'item' },
251
- { keys: 'Enter', label: 'cycle status' },
252
- { keys: '1-6', label: 'set status' },
253
- { keys: 'd', label: 'delete' },
254
- { keys: 'c', label: 'clear done' },
255
- { keys: 'r', label: 'refresh' },
256
- ], C),
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),
257
516
  buildPanelLine(width, [
258
517
  [' 1', C.info], [' pending ', C.dim],
259
518
  ['2', C.info], [' active ', C.dim],
@@ -263,5 +522,9 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
263
522
  ['6', C.info], [' cancelled', C.dim],
264
523
  ]),
265
524
  ];
525
+ if (this.lastStatus) {
526
+ lines.push(buildPanelLine(width, [[` ${this.lastStatus}`, C.good]]));
527
+ }
528
+ return lines;
266
529
  }
267
530
  }