@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,16 +1,15 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import type { WrfcChain, WrfcState, QualityGateResult } from '@pellux/goodvibes-sdk/platform/agents';
3
- import type { Constraint, ConstraintFinding } from '@pellux/goodvibes-sdk/platform/agents';
4
3
  import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
5
4
  import { BasePanel } from './base-panel.ts';
6
5
  import type { WorkflowEvent } from '@/runtime/index.ts';
7
6
  import type { UiEventFeed } from '../runtime/ui-events.ts';
8
7
  import {
8
+ buildMeterLine,
9
9
  buildPanelLine,
10
10
  buildPanelWorkspace,
11
11
  resolveScrollablePanelSection,
12
12
  DEFAULT_PANEL_PALETTE,
13
- extendPalette,
14
13
  type PanelWorkspaceSection,
15
14
  buildSelectablePanelLine,
16
15
  buildStyledPanelLine,
@@ -22,6 +21,9 @@ import {
22
21
  handleConfirmInput,
23
22
  } from './confirm-state.ts';
24
23
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
24
+ import { formatDuration } from '../utils/format-duration.ts';
25
+ import { AgentInspectorPanel } from './agent-inspector-panel.ts';
26
+ import type { PanelIntegrationContext } from './types.ts';
25
27
 
26
28
  // ---------------------------------------------------------------------------
27
29
  // Constants
@@ -45,131 +47,21 @@ const TERMINAL_STATES: readonly WrfcState[] = ['passed', 'failed'];
45
47
  const RESUMABLE_STATES: readonly WrfcState[] = ['pending', 'reviewing', 'fixing', 'awaiting_gates'];
46
48
 
47
49
  // ---------------------------------------------------------------------------
48
- // Colour palette
50
+ // Colour palette + formatting helpers live in ./wrfc-panel-format.ts (extracted
51
+ // to keep this module under the architecture line-count cap). Re-exported here
52
+ // so ./wrfc-panel.ts remains their public import site.
49
53
  // ---------------------------------------------------------------------------
50
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
51
- // WRFC state-machine colours (domain status -- no shared equivalent)
52
- passed: '#22c55e', // green
53
- failed: '#ef4444', // red
54
- reviewing: '#eab308', // yellow
55
- engineering:'#22d3ee', // cyan
56
- fixing: '#f97316', // orange
57
- pending: '#6b7280', // grey
58
- gating: '#a78bfa', // violet
59
- committing: '#38bdf8', // sky
60
- integrating:'#818cf8', // indigo
61
-
62
- // Issue-severity ramp (domain -- no shared equivalent)
63
- issueCrit: '#ef4444',
64
- issueMaj: '#f97316',
65
- issueMin: '#eab308',
66
- issueSug: '#6b7280',
67
-
68
- // Selection + divider chrome with no shared equivalent
69
- selected: '#1e40af', // selection bg
70
- selectedFg: '#f8fafc',
71
- border: '#334155',
72
- });
73
-
74
- // ---------------------------------------------------------------------------
75
- // Helpers
76
- // ---------------------------------------------------------------------------
77
- const SPARKLINE_CHARS = '._-:=+*#';
78
-
79
- export function sparkline(scores: number[], maxScore = 10): string {
80
- if (scores.length === 0) return '';
81
- return scores
82
- .map(s => {
83
- const ratio = Math.max(0, Math.min(1, s / maxScore));
84
- const idx = Math.round(ratio * (SPARKLINE_CHARS.length - 1));
85
- return SPARKLINE_CHARS[idx];
86
- })
87
- .join('');
88
- }
89
-
90
- export function stateColor(state: WrfcState): string {
91
- switch (state) {
92
- case 'passed': return C.passed;
93
- case 'failed': return C.failed;
94
- case 'reviewing': return C.reviewing;
95
- case 'engineering': return C.engineering;
96
- case 'fixing': return C.fixing;
97
- case 'gating':
98
- case 'awaiting_gates': return C.gating;
99
- case 'committing': return C.committing;
100
- case 'integrating': return C.integrating;
101
- default: return C.pending;
102
- }
103
- }
104
-
105
- export function stateLabel(state: WrfcState): string {
106
- switch (state) {
107
- case 'engineering': return 'ENG';
108
- case 'reviewing': return 'REV';
109
- case 'fixing': return 'FIX';
110
- case 'gating': return 'GATE';
111
- case 'awaiting_gates': return 'WAIT';
112
- case 'committing': return 'COMMIT';
113
- case 'integrating': return 'INTG';
114
- case 'passed': return 'PASS';
115
- case 'failed': return 'FAIL';
116
- default: return 'PEND';
117
- }
118
- }
119
-
120
- function issueColor(severity: string): string {
121
- switch (severity) {
122
- case 'critical': return C.issueCrit;
123
- case 'major': return C.issueMaj;
124
- case 'minor': return C.issueMin;
125
- default: return C.issueSug;
126
- }
127
- }
128
-
129
- function issuePrefix(severity: string): string {
130
- switch (severity) {
131
- case 'critical': return '[CRIT] ';
132
- case 'major': return '[MAJR] ';
133
- case 'minor': return '[MINR] ';
134
- default: return '[SUGG] ';
135
- }
136
- }
137
-
138
- export function truncate(s: string, max: number): string {
139
- if (s.length <= max) return s;
140
- return s.slice(0, max - 3) + '...';
141
- }
142
-
143
- // ---------------------------------------------------------------------------
144
- // Constraint helpers
145
- // ---------------------------------------------------------------------------
146
-
147
- /**
148
- * Returns display tag, foreground colour, and dim flag for a single constraint
149
- * based on whether a reviewer finding exists for it.
150
- */
151
- export function constraintStatusMarker(
152
- constraint: Constraint,
153
- findings: ConstraintFinding[] | undefined,
154
- ): { tag: string; fg: string; dim: boolean } {
155
- const finding = findings?.find(f => f.constraintId === constraint.id);
156
- if (!finding) {
157
- return { tag: '[UNV]', fg: C.dim, dim: true };
158
- }
159
- if (finding.satisfied) {
160
- return { tag: '[SAT]', fg: C.good, dim: false };
161
- }
162
- // Unsatisfied — use severity to pick colour and tag text
163
- const sev = finding.severity ?? 'major';
164
- let sevTag: string;
165
- let fg: string;
166
- switch (sev) {
167
- case 'critical': sevTag = '[UNS CRIT]'; fg = C.issueCrit; break;
168
- case 'minor': sevTag = '[UNS MINOR]'; fg = C.issueMin; break;
169
- default: sevTag = '[UNS MAJOR]'; fg = C.issueMaj; break;
170
- }
171
- return { tag: sevTag, fg, dim: false };
172
- }
54
+ import {
55
+ C,
56
+ sparkline,
57
+ stateColor,
58
+ stateLabel,
59
+ issueColor,
60
+ issuePrefix,
61
+ truncate,
62
+ constraintStatusMarker,
63
+ } from './wrfc-panel-format.ts';
64
+ export { sparkline, stateColor, stateLabel, truncate, constraintStatusMarker } from './wrfc-panel-format.ts';
173
65
 
174
66
  // ---------------------------------------------------------------------------
175
67
  // Panel
@@ -184,6 +76,17 @@ export class WrfcPanel extends BasePanel {
184
76
  private chains: WrfcChain[] = [];
185
77
  private selectedIndex = 0;
186
78
  private scrollOffset = 0;
79
+
80
+ /**
81
+ * The chain under the cursor. This panel owns its own selection state
82
+ * (`selectedIndex` navigates `this.chains` directly), so every selected-row
83
+ * read routes through this one accessor — indexing `this.chains` by the
84
+ * cursor directly is banned by the no-raw-selectedindex-read architecture rule.
85
+ */
86
+ private selectedChain(): WrfcChain | undefined {
87
+ return this.chains.at(this.selectedIndex);
88
+ }
89
+
187
90
  private expandedChainIds = new Set<string>();
188
91
  private unsubscribers: Array<() => void> = [];
189
92
  /** Last event timestamp per chain id, for stall detection. */
@@ -248,10 +151,64 @@ export class WrfcPanel extends BasePanel {
248
151
  case 'enter': this.toggleExpanded(); return true;
249
152
  case 'c': this.beginCancelConfirm(); return true;
250
153
  case 'r': this.doResume(); return true;
154
+ // 'a' jumps to the selected chain's owner agent in the Inspector. The
155
+ // actual navigation happens in handlePanelIntegrationAction (it needs
156
+ // the PanelManager); consuming the key here just requires a selected
157
+ // chain to exist so the router below fires next.
158
+ case 'a': return this.selectedChain() !== undefined;
251
159
  default: return false;
252
160
  }
253
161
  }
254
162
 
163
+ /**
164
+ * Cross-panel jump: 'a' on a selected chain opens the Inspector focused on
165
+ * that chain's owner agent (inspectAgent, reverse of the forward jump
166
+ * agent-detail-modal.ts makes from an agent record's wrfcId to its chain).
167
+ */
168
+ handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
169
+ if (key !== 'a') return false;
170
+ const chain = this.selectedChain();
171
+ if (!chain) return false;
172
+ const inspector = ctx.panelManager.open('inspector');
173
+ if (inspector instanceof AgentInspectorPanel) {
174
+ inspector.inspectAgent(chain.ownerAgentId);
175
+ return true;
176
+ }
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Mouse-wheel scroll: pan the chain list viewport without moving the
182
+ * selection cursor (BasePanel's default forwards wheel deltas into
183
+ * handleInput('up'/'down'), which would also move the selected row).
184
+ * scrollOffset is a hint consumed by resolveScrollablePanelSection's
185
+ * tracked window on the next render, which clamps it into range and keeps
186
+ * the selected row inside the guard band — so an over-large delta here is
187
+ * always safe.
188
+ */
189
+ override handleScroll(deltaRows: number): boolean {
190
+ if (this.confirmCancel) return false;
191
+ const rows = Math.trunc(deltaRows);
192
+ if (rows === 0) return false;
193
+ const next = Math.max(0, this.scrollOffset + rows);
194
+ if (next === this.scrollOffset) return false;
195
+ this.scrollOffset = next;
196
+ this.markDirty();
197
+ return true;
198
+ }
199
+
200
+ /**
201
+ * Cross-panel jump target: select the chain by id and expand it. Used by
202
+ * WorkPlanPanel's 'w' jump key when an item links a wrfcId.
203
+ */
204
+ selectChain(chainId: string): void {
205
+ const index = this.chains.findIndex((chain) => chain.id === chainId);
206
+ if (index < 0) return;
207
+ this.selectedIndex = index;
208
+ this.expandedChainIds.add(chainId);
209
+ this.markDirty();
210
+ }
211
+
255
212
  // -------------------------------------------------------------------------
256
213
  // Render
257
214
  // -------------------------------------------------------------------------
@@ -269,7 +226,10 @@ export class WrfcPanel extends BasePanel {
269
226
  width,
270
227
  ' No WRFC chains yet',
271
228
  'WRFC chains appear here as review/fix cycles execute. Expanded rows show scores, gates, issues, and failure detail.',
272
- [],
229
+ [
230
+ { command: '/teamwork create-mode local-engineer <task>', summary: 'run an engineer task through the write-review-fix-commit loop (reviewMode: wrfc)' },
231
+ { command: '/teamwork modes', summary: 'list every teamwork mode and see which ones use reviewMode wrfc' },
232
+ ],
273
233
  DEFAULT_PANEL_PALETTE,
274
234
  ),
275
235
  },
@@ -303,21 +263,25 @@ export class WrfcPanel extends BasePanel {
303
263
  const stalled = this.isStalled(chain, now);
304
264
 
305
265
  if (isSelected) selectedLineIndex = chainLines.length;
306
- chainLines.push(...this.renderChainRow(chain, width, isSelected, isExpanded, rowBg, rowFg, stalled));
266
+ chainLines.push(...this.renderChainRow(chain, width, isSelected, isExpanded, rowBg, rowFg, stalled, now));
307
267
 
308
268
  if (isExpanded) {
309
269
  chainLines.push(...this.renderChainDetail(chain, width, 12));
310
270
  }
311
271
  }
312
272
 
313
- const selectedChain = this.chains[this.selectedIndex];
273
+ const selectedChain = this.selectedChain();
314
274
  const selectedLines: Line[] = selectedChain
315
275
  ? [
316
276
  buildPanelLine(width, [
317
277
  [' State ', DEFAULT_PANEL_PALETTE.label],
318
278
  [stateLabel(selectedChain.state), stateColor(selectedChain.state)],
319
- [' Task ', DEFAULT_PANEL_PALETTE.label],
320
- [truncate(selectedChain.task, Math.max(8, width - 24)), DEFAULT_PANEL_PALETTE.value],
279
+ [TERMINAL_STATES.includes(selectedChain.state) ? ' Duration ' : ' Elapsed ', DEFAULT_PANEL_PALETTE.label],
280
+ [this.durationLabel(selectedChain, now), DEFAULT_PANEL_PALETTE.info],
281
+ ]),
282
+ buildPanelLine(width, [
283
+ [' Task ', DEFAULT_PANEL_PALETTE.label],
284
+ [truncate(selectedChain.task, Math.max(8, width - 8)), DEFAULT_PANEL_PALETTE.value],
321
285
  ]),
322
286
  buildPanelLine(width, [
323
287
  [' Reviews ', DEFAULT_PANEL_PALETTE.label],
@@ -344,6 +308,10 @@ export class WrfcPanel extends BasePanel {
344
308
  ]
345
309
  : [];
346
310
 
311
+ const totalChains = this.chains.length;
312
+ const stalledCount = this.chains.filter((c) => this.isStalled(c, now)).length;
313
+ const completedChains = passedCount + failedCount;
314
+ const meterWidth = Math.max(10, Math.min(24, width - 34));
347
315
  const summarySection: PanelWorkspaceSection = {
348
316
  title: 'Summary',
349
317
  lines: [
@@ -354,7 +322,16 @@ export class WrfcPanel extends BasePanel {
354
322
  [String(passedCount), DEFAULT_PANEL_PALETTE.good],
355
323
  [' Failed ', DEFAULT_PANEL_PALETTE.label],
356
324
  [String(failedCount), failedCount > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
325
+ [' Stalled ', DEFAULT_PANEL_PALETTE.label],
326
+ [String(stalledCount), stalledCount > 0 ? C.warn : DEFAULT_PANEL_PALETTE.dim],
357
327
  ]),
328
+ buildMeterLine(
329
+ width,
330
+ totalChains > 0 ? Math.round((completedChains / totalChains) * meterWidth) : 0,
331
+ meterWidth,
332
+ { filled: failedCount > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good, empty: C.border, label: DEFAULT_PANEL_PALETTE.label },
333
+ { prefix: ' Resolved ', suffix: ` ${completedChains}/${totalChains} ` },
334
+ ),
358
335
  ],
359
336
  };
360
337
  const selectedSection: PanelWorkspaceSection = {
@@ -379,7 +356,7 @@ export class WrfcPanel extends BasePanel {
379
356
  } : null;
380
357
 
381
358
  // Footer: show resume-disabled reason for the selected chain.
382
- const selectedForFooter = this.chains[this.selectedIndex];
359
+ const selectedForFooter = this.selectedChain();
383
360
  const resumeReason = selectedForFooter ? this.resumeDisabledReason(selectedForFooter) : null;
384
361
  const footerLines: Line[] = [
385
362
  buildPanelLine(width, [
@@ -388,6 +365,7 @@ export class WrfcPanel extends BasePanel {
388
365
  [' c', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
389
366
  [' r', resumeReason ? DEFAULT_PANEL_PALETTE.dim : DEFAULT_PANEL_PALETTE.info],
390
367
  [resumeReason ? ` resume (${resumeReason})` : ' resume', DEFAULT_PANEL_PALETTE.dim],
368
+ [' a', DEFAULT_PANEL_PALETTE.info], [' jump to agent', DEFAULT_PANEL_PALETTE.dim],
391
369
  ]),
392
370
  ];
393
371
 
@@ -449,6 +427,7 @@ export class WrfcPanel extends BasePanel {
449
427
  bg: string,
450
428
  fg: string,
451
429
  stalled = false,
430
+ now: number = Date.now(),
452
431
  ): Line[] {
453
432
  const stateCol = stalled ? C.warn : stateColor(chain.state);
454
433
  const stateTag = stalled
@@ -463,6 +442,7 @@ export class WrfcPanel extends BasePanel {
463
442
  ? ` ${chain.reviewScores[chain.reviewScores.length - 1].toFixed(1)}/10`
464
443
  : '';
465
444
  const stalledBadge = stalled ? ' [STALLED]' : '';
445
+ const durationTag = ` ${this.durationLabel(chain, now)}`;
466
446
  // Constraint badge: c:sat/total — only when constraints exist
467
447
  let constraintBadge = '';
468
448
  if (chain.constraints.length > 0) {
@@ -471,7 +451,7 @@ export class WrfcPanel extends BasePanel {
471
451
  const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
472
452
  constraintBadge = ` c:${satisfied}/${total}`;
473
453
  }
474
- const rightInfo = `${stalledBadge}${latestScore}${fixes}${cycles}${constraintBadge} `;
454
+ const rightInfo = `${stalledBadge}${durationTag}${latestScore}${fixes}${cycles}${constraintBadge} `;
475
455
 
476
456
  // Compute how much space the task text can use, then check if rightInfo fits.
477
457
  // If the terminal is narrow and rightInfo would overflow, omit it entirely
@@ -529,6 +509,11 @@ export class WrfcPanel extends BasePanel {
529
509
  }
530
510
 
531
511
  private renderChainDetail(chain: WrfcChain, width: number, maxLines: number): Line[] {
512
+ // Build the FULL detail content first (no per-section maxLines guards
513
+ // below), then apply one cap at the end so a truncation always ends with
514
+ // a visible "+N more" indicator instead of silently dropping content —
515
+ // the constraint sub-list keeps its own independent (+N more) cap since
516
+ // that is documented, unrelated badge/marker behavior (PRESERVE).
532
517
  const lines: Line[] = [];
533
518
  const indent = ' ';
534
519
 
@@ -555,13 +540,12 @@ export class WrfcPanel extends BasePanel {
555
540
  }
556
541
 
557
542
  // Constraints section (between Cycles and Gates)
558
- if (chain.constraints.length > 0 && lines.length < maxLines) {
543
+ if (chain.constraints.length > 0) {
559
544
  lines.push(buildStyledPanelLine(width, [{ text: `${indent}Constraints`, fg: C.label }]));
560
545
  const MAX_CONSTRAINTS = 10;
561
546
  const findings = chain.reviewerReport?.constraintFindings;
562
547
  const displayed = chain.constraints.slice(0, MAX_CONSTRAINTS);
563
548
  for (const constraint of displayed) {
564
- if (lines.length >= maxLines) break;
565
549
  const marker = constraintStatusMarker(constraint, findings);
566
550
  const statusTag = marker.tag;
567
551
  const rowPrefix = `${indent} ${statusTag} `;
@@ -575,7 +559,7 @@ export class WrfcPanel extends BasePanel {
575
559
  ]));
576
560
  }
577
561
  const remaining = chain.constraints.length - MAX_CONSTRAINTS;
578
- if (remaining > 0 && lines.length < maxLines) {
562
+ if (remaining > 0) {
579
563
  lines.push(buildStyledPanelLine(width, [
580
564
  { text: `${indent} (+${remaining} more)`, fg: C.dim, dim: true },
581
565
  ]));
@@ -586,7 +570,6 @@ export class WrfcPanel extends BasePanel {
586
570
  if (chain.gateResults && chain.gateResults.length > 0) {
587
571
  lines.push(buildStyledPanelLine(width, [{ text: `${indent}Gates`, fg: C.label }]));
588
572
  for (const gate of chain.gateResults) {
589
- if (lines.length >= maxLines) break;
590
573
  lines.push(this.renderGateResult(gate, width, indent + ' '));
591
574
  }
592
575
  }
@@ -599,10 +582,9 @@ export class WrfcPanel extends BasePanel {
599
582
  }
600
583
 
601
584
  // Synthetic issues injected by controller (continuity violations)
602
- if (chain.syntheticIssues && chain.syntheticIssues.length > 0 && lines.length < maxLines) {
585
+ if (chain.syntheticIssues && chain.syntheticIssues.length > 0) {
603
586
  lines.push(buildStyledPanelLine(width, [{ text: `${indent}Controller flags`, fg: C.issueCrit, bold: true }]));
604
587
  for (const synthetic of chain.syntheticIssues) {
605
- if (lines.length >= maxLines) break;
606
588
  const prefix = `${indent} [CRIT] `;
607
589
  const descMax = Math.max(8, width - prefix.length);
608
590
  const desc = truncate(synthetic.description, descMax);
@@ -615,10 +597,9 @@ export class WrfcPanel extends BasePanel {
615
597
 
616
598
  // Issues from reviewer
617
599
  const issues = chain.reviewerReport?.issues ?? [];
618
- if (issues.length > 0 && lines.length < maxLines) {
600
+ if (issues.length > 0) {
619
601
  lines.push(buildStyledPanelLine(width, [{ text: `${indent}Issues`, fg: C.label }]));
620
602
  for (const issue of issues) {
621
- if (lines.length >= maxLines) break;
622
603
  const prefix = `${indent} ${issuePrefix(issue.severity)}`;
623
604
  const descMax = width - prefix.length;
624
605
  const desc = truncate(issue.description, Math.max(8, descMax));
@@ -630,7 +611,7 @@ export class WrfcPanel extends BasePanel {
630
611
  }
631
612
 
632
613
  // Error
633
- if (chain.error && lines.length < maxLines) {
614
+ if (chain.error) {
634
615
  const errPrefix = `${indent}Error `;
635
616
  lines.push(buildStyledPanelLine(width, [
636
617
  { text: errPrefix, fg: C.failed, bold: true },
@@ -639,11 +620,15 @@ export class WrfcPanel extends BasePanel {
639
620
  }
640
621
 
641
622
  // Divider after expanded section
642
- if (lines.length < maxLines) {
643
- lines.push(buildStyledPanelLine(width, [{ text: ' ' + '-'.repeat(Math.max(0, width - 4)) + ' ', fg: C.border, dim: true }]));
644
- }
645
-
646
- return lines.slice(0, maxLines);
623
+ lines.push(buildStyledPanelLine(width, [{ text: ' ' + '-'.repeat(Math.max(0, width - 4)) + ' ', fg: C.border, dim: true }]));
624
+
625
+ if (lines.length <= maxLines) return lines;
626
+ const visible = lines.slice(0, Math.max(0, maxLines - 1));
627
+ const hidden = lines.length - visible.length;
628
+ visible.push(buildStyledPanelLine(width, [
629
+ { text: `${indent}+${hidden} more (press a to inspect the owner agent for full detail)`, fg: C.dim, dim: true },
630
+ ]));
631
+ return visible;
647
632
  }
648
633
 
649
634
  private renderGateResult(gate: QualityGateResult, width: number, indent: string): Line {
@@ -671,7 +656,7 @@ export class WrfcPanel extends BasePanel {
671
656
  }
672
657
 
673
658
  private toggleExpanded(): void {
674
- const chain = this.chains[this.selectedIndex];
659
+ const chain = this.selectedChain();
675
660
  if (!chain) return;
676
661
  if (this.expandedChainIds.has(chain.id)) {
677
662
  this.expandedChainIds.delete(chain.id);
@@ -754,7 +739,7 @@ export class WrfcPanel extends BasePanel {
754
739
 
755
740
  /** Initiate cancel-confirm flow for the selected chain (noop if terminal). */
756
741
  private beginCancelConfirm(): void {
757
- const chain = this.chains[this.selectedIndex];
742
+ const chain = this.selectedChain();
758
743
  if (!chain || TERMINAL_STATES.includes(chain.state)) return;
759
744
  this.confirmCancel = {
760
745
  subject: chain.id,
@@ -769,7 +754,7 @@ export class WrfcPanel extends BasePanel {
769
754
  * Emits a visible noop reason when the chain is not resumable.
770
755
  */
771
756
  private doResume(): void {
772
- const chain = this.chains[this.selectedIndex];
757
+ const chain = this.selectedChain();
773
758
  if (!chain) return;
774
759
  if (!RESUMABLE_STATES.includes(chain.state)) return;
775
760
  this.deps.controller.resumeChain(chain.id);
@@ -791,4 +776,15 @@ export class WrfcPanel extends BasePanel {
791
776
  const last = this.lastEventAt.get(chain.id) ?? chain.createdAt;
792
777
  return (now - last) >= STALL_THRESHOLD_MS;
793
778
  }
779
+
780
+ /**
781
+ * Elapsed time since createdAt for an active chain, or total duration from
782
+ * createdAt to completedAt for a terminal one. createdAt/completedAt are
783
+ * already consumed for chain sort order (syncFromController) — this just
784
+ * surfaces the same fields as a human-readable label.
785
+ */
786
+ private durationLabel(chain: WrfcChain, now: number): string {
787
+ const end = chain.completedAt ?? now;
788
+ return formatDuration(Math.max(0, end - chain.createdAt));
789
+ }
794
790
  }
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core';
20
+ import { computeContextUsage } from '../core/context-usage.ts';
20
21
  import type { CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
21
22
  import type { ProviderMessage } from '@pellux/goodvibes-sdk/platform/providers';
22
23
 
@@ -58,7 +59,7 @@ export function buildCompactionPreview(opts: CompactionPreviewOptions): string {
58
59
  const tokensAfterEstimate = COMPACTION_OUTPUT_CEILING_ESTIMATE;
59
60
 
60
61
  const contextStr = contextWindow > 0
61
- ? ` (${Math.round((tokensBefore / contextWindow) * 100)}% of ${fmtN(contextWindow)} context window)`
62
+ ? ` (${Math.round(computeContextUsage(tokensBefore, contextWindow).rawRatio * 100)}% of ${fmtN(contextWindow)} context window)`
62
63
  : '';
63
64
 
64
65
  const pinStr = pinnedMemoryCount > 0