@pellux/goodvibes-tui 0.27.0 → 0.28.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 (101) hide show
  1. package/CHANGELOG.md +23 -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/eval.ts +4 -3
  9. package/src/input/handler-feed-routes.ts +61 -4
  10. package/src/input/handler-feed.ts +13 -0
  11. package/src/input/handler-picker-routes.ts +18 -13
  12. package/src/input/handler-ui-state.ts +4 -0
  13. package/src/input/keybindings.ts +5 -0
  14. package/src/input/model-picker.ts +5 -0
  15. package/src/input/panel-integration-actions.ts +10 -0
  16. package/src/main.ts +0 -1
  17. package/src/panels/agent-inspector-panel.ts +18 -13
  18. package/src/panels/agent-inspector-shared.ts +3 -3
  19. package/src/panels/agent-logs-panel.ts +22 -53
  20. package/src/panels/approval-panel.ts +146 -95
  21. package/src/panels/automation-control-panel.ts +24 -1
  22. package/src/panels/base-panel.ts +26 -2
  23. package/src/panels/builtin/development.ts +1 -1
  24. package/src/panels/cockpit-panel.ts +24 -5
  25. package/src/panels/cockpit-read-model.ts +2 -1
  26. package/src/panels/communication-panel.ts +108 -43
  27. package/src/panels/context-visualizer-panel.ts +25 -1
  28. package/src/panels/control-plane-panel.ts +27 -1
  29. package/src/panels/cost-tracker-panel.ts +82 -54
  30. package/src/panels/debug-panel.ts +51 -16
  31. package/src/panels/diff-panel.ts +59 -30
  32. package/src/panels/docs-panel.ts +19 -3
  33. package/src/panels/eval-panel.ts +130 -81
  34. package/src/panels/expandable-list-panel.ts +189 -0
  35. package/src/panels/file-explorer-panel.ts +40 -41
  36. package/src/panels/file-preview-panel.ts +9 -2
  37. package/src/panels/forensics-panel.ts +27 -13
  38. package/src/panels/git-panel.ts +58 -70
  39. package/src/panels/hooks-panel.ts +36 -2
  40. package/src/panels/incident-review-panel.ts +31 -10
  41. package/src/panels/intelligence-panel.ts +152 -71
  42. package/src/panels/knowledge-graph-panel.ts +89 -27
  43. package/src/panels/local-auth-panel.ts +17 -11
  44. package/src/panels/marketplace-panel.ts +31 -12
  45. package/src/panels/memory-panel.ts +5 -4
  46. package/src/panels/ops-control-panel.ts +69 -7
  47. package/src/panels/ops-strategy-panel.ts +61 -43
  48. package/src/panels/orchestration-panel.ts +34 -4
  49. package/src/panels/panel-list-panel.ts +33 -8
  50. package/src/panels/panel-manager.ts +23 -4
  51. package/src/panels/plan-dashboard-panel.ts +183 -66
  52. package/src/panels/plugins-panel.ts +48 -10
  53. package/src/panels/policy-panel.ts +60 -23
  54. package/src/panels/polish-core.ts +157 -0
  55. package/src/panels/polish-tables.ts +258 -0
  56. package/src/panels/polish.ts +44 -145
  57. package/src/panels/project-planning-panel.ts +22 -0
  58. package/src/panels/provider-accounts-panel.ts +55 -18
  59. package/src/panels/provider-health-panel.ts +65 -23
  60. package/src/panels/provider-stats-panel.ts +47 -22
  61. package/src/panels/qr-panel.ts +23 -11
  62. package/src/panels/remote-panel.ts +12 -5
  63. package/src/panels/routes-panel.ts +27 -5
  64. package/src/panels/sandbox-panel.ts +62 -27
  65. package/src/panels/schedule-panel.ts +40 -17
  66. package/src/panels/scrollable-list-panel.ts +124 -10
  67. package/src/panels/security-panel.ts +72 -20
  68. package/src/panels/services-panel.ts +68 -22
  69. package/src/panels/session-browser-panel.ts +29 -5
  70. package/src/panels/session-maintenance.ts +2 -2
  71. package/src/panels/settings-sync-panel.ts +30 -15
  72. package/src/panels/subscription-panel.ts +21 -3
  73. package/src/panels/symbol-outline-panel.ts +40 -47
  74. package/src/panels/system-messages-panel.ts +60 -27
  75. package/src/panels/tasks-panel.ts +34 -7
  76. package/src/panels/thinking-panel.ts +26 -25
  77. package/src/panels/token-budget-panel.ts +49 -38
  78. package/src/panels/tool-inspector-panel.ts +27 -21
  79. package/src/panels/types.ts +25 -0
  80. package/src/panels/watchers-panel.ts +25 -5
  81. package/src/panels/work-plan-panel.ts +125 -34
  82. package/src/panels/worktree-panel.ts +65 -20
  83. package/src/panels/wrfc-panel-format.ts +133 -0
  84. package/src/panels/wrfc-panel.ts +31 -127
  85. package/src/renderer/compaction-preview.ts +2 -1
  86. package/src/renderer/compositor.ts +46 -43
  87. package/src/renderer/model-picker-overlay.ts +9 -4
  88. package/src/renderer/model-workspace.ts +2 -3
  89. package/src/renderer/panel-composite.ts +7 -20
  90. package/src/renderer/panel-workspace-bar.ts +14 -8
  91. package/src/renderer/progress.ts +3 -2
  92. package/src/renderer/tab-strip.ts +148 -34
  93. package/src/renderer/ui-factory.ts +4 -6
  94. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  95. package/src/runtime/ui/model-picker/types.ts +4 -0
  96. package/src/utils/format-duration.ts +55 -0
  97. package/src/utils/format-number.ts +71 -0
  98. package/src/version.ts +1 -1
  99. package/src/panels/panel-picker.ts +0 -106
  100. package/src/renderer/panel-picker-overlay.ts +0 -202
  101. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,7 +1,8 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
+ import { basename } from 'node:path';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
- import { buildKeyValueLine, buildPanelLine, buildPanelWorkspace, DEFAULT_PANEL_PALETTE, resolvePrimaryScrollableSection, type PanelWorkspaceSection } from './polish.ts';
5
+ import { buildAlignedRow, buildKeyValueLine, buildKeyboardHints, buildPanelLine, buildPanelWorkspace, DEFAULT_PANEL_PALETTE, resolvePrimaryScrollableSection, type ColumnSpec, type PanelWorkspaceSection } from './polish.ts';
5
6
  import { summarizeWorktreeOwnership, type WorktreeRegistry, type WorktreeStatusRecord } from '@/runtime/index.ts';
6
7
 
7
8
  const C = {
@@ -22,6 +23,44 @@ function stateColor(state: WorktreeStatusRecord['state']): string {
22
23
  }
23
24
  }
24
25
 
26
+ /** Glyph that reads at a glance: ● active, ◌ paused/kept, ⊘ discard/cleanup. */
27
+ function stateGlyph(state: WorktreeStatusRecord['state']): string {
28
+ switch (state) {
29
+ case 'active': return '●';
30
+ case 'paused':
31
+ case 'kept': return '◌';
32
+ default: return '⊘';
33
+ }
34
+ }
35
+
36
+ /**
37
+ * One worktree row, aligned with display-width-aware columns so a wide-char or
38
+ * long branch name never shoves the path column out of alignment. The active
39
+ * worktree is flagged with a ● glyph and selection uses the shared marker.
40
+ */
41
+ function buildWorktreeRow(width: number, row: WorktreeStatusRecord, selected: boolean): Line {
42
+ const pathW = Math.max(8, width - (2 + 10 + 13 + 20) - 4 - 2);
43
+ const columns: ColumnSpec[] = [
44
+ { width: 2 },
45
+ { width: 10 },
46
+ { width: 13 },
47
+ { width: 20 },
48
+ { width: pathW },
49
+ ];
50
+ return buildAlignedRow(
51
+ width,
52
+ [
53
+ { text: stateGlyph(row.state), fg: stateColor(row.state) },
54
+ { text: row.kind, fg: C.info },
55
+ { text: row.state, fg: stateColor(row.state) },
56
+ { text: row.branch, fg: C.value },
57
+ { text: basename(row.path) || row.path, fg: C.dim },
58
+ ],
59
+ columns,
60
+ { selected, selectedBg: C.headerBg, marker: '▸' },
61
+ );
62
+ }
63
+
25
64
  export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
26
65
  private rows: WorktreeStatusRecord[] = [];
27
66
  private loading = false;
@@ -52,13 +91,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
52
91
  }
53
92
 
54
93
  protected renderItem(row: WorktreeStatusRecord, index: number, _selected: boolean, width: number): Line {
55
- const bg = index === this.selectedIndex ? C.headerBg : undefined;
56
- return buildPanelLine(width, [
57
- [` ${row.kind}`.padEnd(14), C.info, bg],
58
- [` ${row.state}`.padEnd(16), stateColor(row.state), bg],
59
- [` ${row.branch}`.padEnd(24), C.value, bg],
60
- [` ${row.path}`.slice(0, Math.max(0, width - 56)), C.dim, bg],
61
- ]);
94
+ return buildWorktreeRow(width, row, index === this.selectedIndex);
62
95
  }
63
96
 
64
97
  private async refresh(): Promise<void> {
@@ -80,7 +113,13 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
80
113
  if (this.loading && this.rows.length === 0) {
81
114
  sections.push({ title: 'Worktrees', lines: [buildPanelLine(width, [[' Loading worktree state...', C.info]])] });
82
115
  } else if (this.rows.length === 0) {
83
- sections.push({ title: 'Worktrees', lines: [buildPanelLine(width, [[' No git worktrees discovered for this project.', C.dim]])] });
116
+ sections.push({
117
+ title: 'Worktrees',
118
+ lines: [
119
+ buildPanelLine(width, [[' No git worktrees discovered for this project yet.', C.dim]]),
120
+ buildPanelLine(width, [[' /worktree attach <path>', C.info], [' register a worktree for orchestrator-managed lifecycle', C.dim]]),
121
+ ],
122
+ });
84
123
  } else {
85
124
  const summary = summarizeWorktreeOwnership(this.rows);
86
125
  sections.push({
@@ -142,20 +181,26 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
142
181
  };
143
182
  const resolvedWorktreesSection = resolvePrimaryScrollableSection(width, height, {
144
183
  intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
145
- footerLines: [buildPanelLine(width, [[' r refresh /worktree inspect <path> /worktree attach|pause|resume|keep|discard|cleanup ', C.dim]])],
184
+ footerLines: [buildKeyboardHints(width, [{ keys: '↑/↓', label: 'select' }, { keys: 'r', label: 'refresh' }, { keys: '/worktree inspect', label: '<path>' }, { keys: 'attach·pause·resume·keep·discard·cleanup', label: '' }], C)],
146
185
  palette: C,
147
186
  beforeSections: sections,
148
187
  section: {
149
188
  title: 'Worktrees',
150
- scrollableLines: this.rows.map((row, absolute) => {
151
- const bg = absolute === this.selectedIndex ? C.headerBg : undefined;
152
- return buildPanelLine(width, [
153
- [` ${row.kind}`.padEnd(14), C.info, bg],
154
- [` ${row.state}`.padEnd(16), stateColor(row.state), bg],
155
- [` ${row.branch}`.padEnd(24), C.value, bg],
156
- [` ${row.path}`.slice(0, Math.max(0, width - 56)), C.dim, bg],
157
- ]);
158
- }),
189
+ fixedLines: [
190
+ buildAlignedRow(width, [
191
+ { text: '', fg: C.dim },
192
+ { text: 'KIND', fg: C.label, bold: true },
193
+ { text: 'STATE', fg: C.label, bold: true },
194
+ { text: 'BRANCH', fg: C.label, bold: true },
195
+ { text: 'WORKTREE', fg: C.label, bold: true },
196
+ ], [
197
+ { width: 2 }, { width: 10 }, { width: 13 }, { width: 20 },
198
+ { width: Math.max(8, width - (2 + 10 + 13 + 20) - 4 - 2) },
199
+ ], { marker: ' ' }),
200
+ ],
201
+ scrollableLines: this.rows.map((row, absolute) =>
202
+ buildWorktreeRow(width, row, absolute === this.selectedIndex),
203
+ ),
159
204
  selectedIndex: this.selectedIndex,
160
205
  scrollOffset: this.scrollStart,
161
206
  guardRows: 1,
@@ -173,7 +218,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
173
218
  title: 'Worktree Control Room',
174
219
  intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
175
220
  sections,
176
- footerLines: [buildPanelLine(width, [[' r refresh /worktree inspect <path> /worktree attach|pause|resume|keep|discard|cleanup ', C.dim]])],
221
+ footerLines: [buildKeyboardHints(width, [{ keys: '↑/↓', label: 'select' }, { keys: 'r', label: 'refresh' }, { keys: '/worktree inspect', label: '<path>' }, { keys: 'attach·pause·resume·keep·discard·cleanup', label: '' }], C)],
177
222
  palette: C,
178
223
  });
179
224
  while (lines.length < height) lines.push(createEmptyLine(width));
@@ -0,0 +1,133 @@
1
+ import { truncateDisplay } from '../utils/terminal-width.ts';
2
+ import type { WrfcState, Constraint, ConstraintFinding } from '@pellux/goodvibes-sdk/platform/agents';
3
+ import { DEFAULT_PANEL_PALETTE, extendPalette } from './polish.ts';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Colour palette + formatting helpers for the WRFC panel.
7
+ //
8
+ // Extracted from wrfc-panel.ts to keep that module under the architecture
9
+ // line-count cap. Leaf module (only polish + terminal-width + sdk types); the
10
+ // panel re-exports the public helpers so ./wrfc-panel.ts stays their import site.
11
+ // ---------------------------------------------------------------------------
12
+ export const C = extendPalette(DEFAULT_PANEL_PALETTE, {
13
+ // WRFC state-machine colours (domain status -- no shared equivalent)
14
+ passed: '#22c55e', // green
15
+ failed: '#ef4444', // red
16
+ reviewing: '#eab308', // yellow
17
+ engineering:'#22d3ee', // cyan
18
+ fixing: '#f97316', // orange
19
+ pending: '#6b7280', // grey
20
+ gating: '#a78bfa', // violet
21
+ committing: '#38bdf8', // sky
22
+ integrating:'#818cf8', // indigo
23
+
24
+ // Issue-severity ramp (domain -- no shared equivalent)
25
+ issueCrit: '#ef4444',
26
+ issueMaj: '#f97316',
27
+ issueMin: '#eab308',
28
+ issueSug: '#6b7280',
29
+
30
+ // Selection + divider chrome with no shared equivalent
31
+ selected: '#1e40af', // selection bg
32
+ selectedFg: '#f8fafc',
33
+ border: '#334155',
34
+ });
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Helpers
38
+ // ---------------------------------------------------------------------------
39
+ const SPARKLINE_CHARS = '._-:=+*#';
40
+
41
+ export function sparkline(scores: number[], maxScore = 10): string {
42
+ if (scores.length === 0) return '';
43
+ return scores
44
+ .map(s => {
45
+ const ratio = Math.max(0, Math.min(1, s / maxScore));
46
+ const idx = Math.round(ratio * (SPARKLINE_CHARS.length - 1));
47
+ return SPARKLINE_CHARS[idx];
48
+ })
49
+ .join('');
50
+ }
51
+
52
+ export function stateColor(state: WrfcState): string {
53
+ switch (state) {
54
+ case 'passed': return C.passed;
55
+ case 'failed': return C.failed;
56
+ case 'reviewing': return C.reviewing;
57
+ case 'engineering': return C.engineering;
58
+ case 'fixing': return C.fixing;
59
+ case 'gating':
60
+ case 'awaiting_gates': return C.gating;
61
+ case 'committing': return C.committing;
62
+ case 'integrating': return C.integrating;
63
+ default: return C.pending;
64
+ }
65
+ }
66
+
67
+ export function stateLabel(state: WrfcState): string {
68
+ switch (state) {
69
+ case 'engineering': return 'ENG';
70
+ case 'reviewing': return 'REV';
71
+ case 'fixing': return 'FIX';
72
+ case 'gating': return 'GATE';
73
+ case 'awaiting_gates': return 'WAIT';
74
+ case 'committing': return 'COMMIT';
75
+ case 'integrating': return 'INTG';
76
+ case 'passed': return 'PASS';
77
+ case 'failed': return 'FAIL';
78
+ default: return 'PEND';
79
+ }
80
+ }
81
+
82
+ export function issueColor(severity: string): string {
83
+ switch (severity) {
84
+ case 'critical': return C.issueCrit;
85
+ case 'major': return C.issueMaj;
86
+ case 'minor': return C.issueMin;
87
+ default: return C.issueSug;
88
+ }
89
+ }
90
+
91
+ export function issuePrefix(severity: string): string {
92
+ switch (severity) {
93
+ case 'critical': return '[CRIT] ';
94
+ case 'major': return '[MAJR] ';
95
+ case 'minor': return '[MINR] ';
96
+ default: return '[SUGG] ';
97
+ }
98
+ }
99
+
100
+ export function truncate(s: string, max: number): string {
101
+ return truncateDisplay(s, max);
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Constraint helpers
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Returns display tag, foreground colour, and dim flag for a single constraint
110
+ * based on whether a reviewer finding exists for it.
111
+ */
112
+ export function constraintStatusMarker(
113
+ constraint: Constraint,
114
+ findings: ConstraintFinding[] | undefined,
115
+ ): { tag: string; fg: string; dim: boolean } {
116
+ const finding = findings?.find(f => f.constraintId === constraint.id);
117
+ if (!finding) {
118
+ return { tag: '[UNV]', fg: C.dim, dim: true };
119
+ }
120
+ if (finding.satisfied) {
121
+ return { tag: '[SAT]', fg: C.good, dim: false };
122
+ }
123
+ // Unsatisfied — use severity to pick colour and tag text
124
+ const sev = finding.severity ?? 'major';
125
+ let sevTag: string;
126
+ let fg: string;
127
+ switch (sev) {
128
+ case 'critical': sevTag = '[UNS CRIT]'; fg = C.issueCrit; break;
129
+ case 'minor': sevTag = '[UNS MINOR]'; fg = C.issueMin; break;
130
+ default: sevTag = '[UNS MAJOR]'; fg = C.issueMaj; break;
131
+ }
132
+ return { tag: sevTag, fg, dim: false };
133
+ }
@@ -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,
@@ -45,131 +44,21 @@ const TERMINAL_STATES: readonly WrfcState[] = ['passed', 'failed'];
45
44
  const RESUMABLE_STATES: readonly WrfcState[] = ['pending', 'reviewing', 'fixing', 'awaiting_gates'];
46
45
 
47
46
  // ---------------------------------------------------------------------------
48
- // Colour palette
47
+ // Colour palette + formatting helpers live in ./wrfc-panel-format.ts (extracted
48
+ // to keep this module under the architecture line-count cap). Re-exported here
49
+ // so ./wrfc-panel.ts remains their public import site.
49
50
  // ---------------------------------------------------------------------------
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
- }
51
+ import {
52
+ C,
53
+ sparkline,
54
+ stateColor,
55
+ stateLabel,
56
+ issueColor,
57
+ issuePrefix,
58
+ truncate,
59
+ constraintStatusMarker,
60
+ } from './wrfc-panel-format.ts';
61
+ export { sparkline, stateColor, stateLabel, truncate, constraintStatusMarker } from './wrfc-panel-format.ts';
173
62
 
174
63
  // ---------------------------------------------------------------------------
175
64
  // Panel
@@ -269,7 +158,9 @@ export class WrfcPanel extends BasePanel {
269
158
  width,
270
159
  ' No WRFC chains yet',
271
160
  'WRFC chains appear here as review/fix cycles execute. Expanded rows show scores, gates, issues, and failure detail.',
272
- [],
161
+ [
162
+ { command: '/wrfc run <task>', summary: 'start a write-review-fix-commit chain for a task' },
163
+ ],
273
164
  DEFAULT_PANEL_PALETTE,
274
165
  ),
275
166
  },
@@ -344,6 +235,10 @@ export class WrfcPanel extends BasePanel {
344
235
  ]
345
236
  : [];
346
237
 
238
+ const totalChains = this.chains.length;
239
+ const stalledCount = this.chains.filter((c) => this.isStalled(c, now)).length;
240
+ const completedChains = passedCount + failedCount;
241
+ const meterWidth = Math.max(10, Math.min(24, width - 34));
347
242
  const summarySection: PanelWorkspaceSection = {
348
243
  title: 'Summary',
349
244
  lines: [
@@ -354,7 +249,16 @@ export class WrfcPanel extends BasePanel {
354
249
  [String(passedCount), DEFAULT_PANEL_PALETTE.good],
355
250
  [' Failed ', DEFAULT_PANEL_PALETTE.label],
356
251
  [String(failedCount), failedCount > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
252
+ [' Stalled ', DEFAULT_PANEL_PALETTE.label],
253
+ [String(stalledCount), stalledCount > 0 ? C.warn : DEFAULT_PANEL_PALETTE.dim],
357
254
  ]),
255
+ buildMeterLine(
256
+ width,
257
+ totalChains > 0 ? Math.round((completedChains / totalChains) * meterWidth) : 0,
258
+ meterWidth,
259
+ { filled: failedCount > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good, empty: C.border, label: DEFAULT_PANEL_PALETTE.label },
260
+ { prefix: ' Resolved ', suffix: ` ${completedChains}/${totalChains} ` },
261
+ ),
358
262
  ],
359
263
  };
360
264
  const selectedSection: PanelWorkspaceSection = {
@@ -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
@@ -6,9 +6,15 @@ import type { SearchManager } from '../input/search.ts';
6
6
  import { allowTerminalWrite } from '../runtime/terminal-output-guard.ts';
7
7
  import { probeTermCaps, type TermColorCaps } from './term-caps.ts';
8
8
  import { DARK_THEME } from './theme.ts';
9
+ import { UI_TONES } from './ui-primitives.ts';
9
10
 
10
11
  const T = DARK_THEME;
11
12
 
13
+ // Accent / dim colors for the panel focus border. The focused pane's left
14
+ // border column is drawn in the accent tone; the unfocused pane stays dim.
15
+ const PANEL_FOCUS_ACCENT = UI_TONES.state.active; // bright blue
16
+ const PANEL_BORDER_DIM = '238';
17
+
12
18
  export interface SelectionInfo {
13
19
  isCellSelected: (col: number, absoluteRow: number) => boolean;
14
20
  scrollTop: number;
@@ -22,16 +28,18 @@ export interface SearchInfo {
22
28
  }
23
29
 
24
30
  export interface PanelCompositeData {
25
- /** Workspace-level tab bar spanning all open panels. */
31
+ /**
32
+ * The single consolidated workspace tab bar spanning all open panels across
33
+ * both panes. There are no per-pane tab bars — pane focus is shown by the
34
+ * accent border (see `topFocused`/`bottomFocused`).
35
+ */
26
36
  workspaceBar: Line;
27
- /** Top pane: tab bar */
28
- topTabBar?: Line;
29
37
  /** Top pane: panel content lines */
30
38
  topContent: Line[];
31
- /** Whether the top pane is focused (affects separator color) */
39
+ /** Whether the top pane is focused (drives the accent border) */
32
40
  topFocused: boolean;
33
- /** Bottom pane tab bar. Undefined = no bottom pane. */
34
- bottomTabBar?: Line;
41
+ /** Whether a bottom pane is present (splits the panel area). */
42
+ hasBottomPane: boolean;
35
43
  /** Bottom pane content lines. Undefined = no bottom pane. */
36
44
  bottomContent?: Line[];
37
45
  /** Whether the bottom pane is focused */
@@ -113,29 +121,31 @@ export class Compositor {
113
121
  const offset = Math.max(0, vHeight - lineCount);
114
122
 
115
123
  // --- Pre-compute panel row layout when split pane is active ---
116
- // When both top and bottom panes are visible, the panel area is split:
124
+ // A single consolidated workspace bar heads the panel area; there are no
125
+ // per-pane tab bars. When both panes are visible the layout is:
117
126
  // row 0: workspace tab bar
118
- // row 1: top tab bar
119
- // rows 2..topH+1: top content
120
- // row topH+2: horizontal separator (───)
121
- // row topH+3: bottom tab bar
122
- // rows topH+4..end: bottom content
123
- const hasBottomPane = hasPanel && panel!.bottomTabBar !== undefined;
127
+ // rows 1..topH: top content
128
+ // row topH+1: horizontal separator (───)
129
+ // rows topH+2..end: bottom content
130
+ const hasBottomPane = hasPanel && panel!.hasBottomPane;
124
131
  let topPaneHeight = 0; // number of content rows in top pane
125
- let bottomPaneHeight = 0;
126
132
  let hSepRow = -1; // viewport row of the horizontal separator
127
133
  if (hasPanel && hasBottomPane) {
128
- const panelAreaRows = Math.max(0, vHeight - 1); // subtract workspace tab bar
129
- // top: 1 (tabbar) + topContent rows; bottom: 1 (sep) + 1 (tabbar) + bottomContent
130
- const contentRows = Math.max(0, panelAreaRows - 3); // subtract top-tabbar + h-sep + bottom-tabbar
134
+ const panelAreaRows = Math.max(0, vHeight - 1); // subtract workspace bar
135
+ const contentRows = Math.max(0, panelAreaRows - 1); // subtract h-separator
131
136
  topPaneHeight = Math.max(1, Math.floor(contentRows * panel!.verticalSplitRatio));
132
- bottomPaneHeight = Math.max(1, contentRows - topPaneHeight);
133
- hSepRow = 2 + topPaneHeight; // workspace bar + top tab bar + top content rows
137
+ hSepRow = 1 + topPaneHeight; // workspace bar + top content rows
134
138
  }
135
139
 
136
- const sepFg = hasPanel && panel!.separator
137
- ? (panel!.topFocused || panel!.bottomFocused ? '244' : '238')
138
- : '238';
140
+ const panelFocused = hasPanel && (panel!.topFocused || panel!.bottomFocused);
141
+ // Per-row left-border color: the focused pane's rows get the accent tone.
142
+ const borderFgForRow = (i: number): string => {
143
+ if (!hasPanel || !panel!.separator || !panelFocused) return PANEL_BORDER_DIM;
144
+ if (!hasBottomPane) return panel!.topFocused ? PANEL_FOCUS_ACCENT : PANEL_BORDER_DIM;
145
+ if (i === 0) return PANEL_FOCUS_ACCENT; // workspace bar — panel is focused
146
+ if (i <= topPaneHeight) return panel!.topFocused ? PANEL_FOCUS_ACCENT : PANEL_BORDER_DIM;
147
+ return panel!.bottomFocused ? PANEL_FOCUS_ACCENT : PANEL_BORDER_DIM;
148
+ };
139
149
 
140
150
  viewport.forEach((line, i) => {
141
151
  const screenY = viewportStartY + i;
@@ -163,9 +173,10 @@ export class Compositor {
163
173
 
164
174
  const p = panel!;
165
175
 
166
- // Separator column (vertical bar between left and panel area)
176
+ // Separator column (vertical bar between left and panel area).
177
+ // Colored per-row so the focused pane shows a bright accent border.
167
178
  if (p.separator) {
168
- newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: sepFg }));
179
+ newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: borderFgForRow(i) }));
169
180
  }
170
181
 
171
182
  const panelStartX = sepX + 1;
@@ -195,39 +206,31 @@ export class Compositor {
195
206
  const panelLine = i === 0 ? p.workspaceBar : p.topContent[i - 1];
196
207
  drawPanelLine(panelLine);
197
208
  } else {
198
- // --- Two pane mode ---
209
+ // --- Two pane mode (single consolidated workspace bar) ---
199
210
  // Row layout (by viewport row i):
200
211
  // i = 0: workspace tab bar
201
- // i = 1: top tab bar
202
- // 2 <= i <= topPaneHeight+1: top content[i-2]
212
+ // 1 <= i <= topPaneHeight: top content[i-1]
203
213
  // i = hSepRow: horizontal separator
204
- // i = hSepRow+1: bottom tab bar
205
- // i >= hSepRow+2: bottom content[i - (hSepRow+2)]
214
+ // i >= hSepRow+1: bottom content[i - (hSepRow+1)]
206
215
  let panelLine: Line | undefined;
207
216
 
208
217
  if (i === 0) {
209
218
  panelLine = p.workspaceBar;
210
- } else if (i === 1) {
211
- panelLine = p.topTabBar;
212
- } else if (i <= topPaneHeight + 1) {
213
- panelLine = p.topContent[i - 2];
219
+ } else if (i <= topPaneHeight) {
220
+ panelLine = p.topContent[i - 1];
214
221
  } else if (i === hSepRow) {
215
- // Horizontal separator between the two panes
216
- // Render chars across the panel width
217
- const focusFg = p.bottomFocused ? '36' : '238'; // cyan if bottom pane focused
222
+ // Horizontal separator between the two panes. Accent when the bottom
223
+ // pane has focus so the divider reinforces the focus border.
224
+ const focusFg = p.bottomFocused ? PANEL_FOCUS_ACCENT : PANEL_BORDER_DIM;
218
225
  for (let x = 0; x < panelWidth; x++) {
219
226
  newBuffer.setCell(panelStartX + x, screenY, createStyledCell('─', { fg: focusFg }));
220
227
  }
221
- // Also update the separator column char to T-junction (├):
222
- // ├ connects the vertical left-separator with the horizontal pane divider,
223
- // forming a clean T-shaped joint at the split point.
228
+ // T-junction (├) joins the vertical left-border with the pane divider.
224
229
  if (p.separator) {
225
230
  newBuffer.setCell(sepX, screenY, createStyledCell('├', { fg: focusFg }));
226
231
  }
227
- } else if (i === hSepRow + 1) {
228
- panelLine = p.bottomTabBar;
229
232
  } else {
230
- panelLine = p.bottomContent?.[i - (hSepRow + 2)];
233
+ panelLine = p.bottomContent?.[i - (hSepRow + 1)];
231
234
  }
232
235
 
233
236
  if (i !== hSepRow) {
@@ -269,7 +272,7 @@ export class Compositor {
269
272
  for (let i = viewport.length; i < vHeight; i++) {
270
273
  const screenY = viewportStartY + i;
271
274
  if (screenY >= height) break;
272
- newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: sepFg }));
275
+ newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: borderFgForRow(i) }));
273
276
  }
274
277
  }
275
278