@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,37 +1,120 @@
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 { summarizeWorktreeOwnership, type WorktreeRegistry, type WorktreeStatusRecord } from '@/runtime/index.ts';
6
-
7
- const C = {
8
- ...DEFAULT_PANEL_PALETTE,
9
- dim: '#475569',
10
- info: '#38bdf8',
11
- ok: '#22c55e',
12
- warn: '#eab308',
13
- headerBg: '#1e293b',
14
- } as const;
5
+ import { buildAlignedRow, buildKeyValueLine, buildKeyboardHints, buildPanelLine, buildPanelWorkspace, DEFAULT_PANEL_PALETTE, resolvePrimaryScrollableSection, type ColumnSpec, type PanelWorkspaceSection } from './polish.ts';
6
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
7
+ import { summarizeWorktreeOwnership, type WorktreeStatusRecord } from '@/runtime/index.ts';
8
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
9
+ import type { PanelIntegrationContext } from './types.ts';
10
+
11
+ // Base chrome only — state colors and text tokens come straight from
12
+ // DEFAULT_PANEL_PALETTE (WO-002).
13
+ const C = DEFAULT_PANEL_PALETTE;
14
+
15
+ /**
16
+ * Structural shape the panel actually needs from a worktree registry. The
17
+ * real `WorktreeRegistry` (from the SDK) satisfies this directly; it has no
18
+ * `subscribe()` method today, so the panel falls back to polling when one
19
+ * isn't provided (see constructor). Kept narrow and `Like`-suffixed per the
20
+ * project's duck-typing convention (see `agent-inspector-shared.ts`).
21
+ */
22
+ export interface WorktreeRegistryLike {
23
+ list(): Promise<WorktreeStatusRecord[]>;
24
+ attach(path: string, target: { sessionId?: string; taskId?: string }): void;
25
+ setState(path: string, state: WorktreeStatusRecord['state']): void;
26
+ cleanup(path: string): Promise<void>;
27
+ /** Optional live-update hook; when present the panel prefers it over polling. */
28
+ subscribe?(listener: () => void): () => void;
29
+ }
30
+
31
+ /** What a pending destructive confirm will do once the user confirms. */
32
+ interface WorktreeConfirmSubject {
33
+ readonly path: string;
34
+ readonly action: 'discard' | 'cleanup';
35
+ }
36
+
37
+ /** Poll cadence used only when the registry has no `subscribe()` method. */
38
+ const POLL_INTERVAL_MS = 5_000;
15
39
 
16
40
  function stateColor(state: WorktreeStatusRecord['state']): string {
17
41
  switch (state) {
18
- case 'active': return C.ok;
42
+ case 'active': return C.good;
19
43
  case 'paused':
20
44
  case 'kept': return C.warn;
21
45
  default: return C.dim;
22
46
  }
23
47
  }
24
48
 
49
+ /** Glyph that reads at a glance: ● active, ◌ paused/kept, ⊘ discard/cleanup. */
50
+ function stateGlyph(state: WorktreeStatusRecord['state']): string {
51
+ switch (state) {
52
+ case 'active': return '●';
53
+ case 'paused':
54
+ case 'kept': return '◌';
55
+ default: return '⊘';
56
+ }
57
+ }
58
+
59
+ /**
60
+ * One worktree row, aligned with display-width-aware columns so a wide-char or
61
+ * long branch name never shoves the path column out of alignment. The active
62
+ * worktree is flagged with a ● glyph and selection uses the shared marker.
63
+ */
64
+ function buildWorktreeRow(width: number, row: WorktreeStatusRecord, selected: boolean): Line {
65
+ const pathW = Math.max(8, width - (2 + 10 + 13 + 20) - 4 - 2);
66
+ const columns: ColumnSpec[] = [
67
+ { width: 2 },
68
+ { width: 10 },
69
+ { width: 13 },
70
+ { width: 20 },
71
+ { width: pathW },
72
+ ];
73
+ return buildAlignedRow(
74
+ width,
75
+ [
76
+ { text: stateGlyph(row.state), fg: stateColor(row.state) },
77
+ { text: row.kind, fg: C.info },
78
+ { text: row.state, fg: stateColor(row.state) },
79
+ { text: row.branch, fg: C.value },
80
+ { text: basename(row.path) || row.path, fg: C.dim },
81
+ ],
82
+ columns,
83
+ { selected, selectedBg: C.headerBg, marker: '▸' },
84
+ );
85
+ }
86
+
87
+ const WORKTREE_HINTS = [
88
+ { keys: '↑/↓', label: 'select' },
89
+ { keys: 'p/u/k', label: 'pause/resume/keep' },
90
+ { keys: 'd/c', label: 'discard/cleanup' },
91
+ { keys: 'enter', label: 'jump to session/task' },
92
+ { keys: 'r', label: 'refresh' },
93
+ ] as const;
94
+
25
95
  export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
26
96
  private rows: WorktreeStatusRecord[] = [];
27
97
  private loading = false;
28
- private readonly worktreeRegistry: WorktreeRegistry;
98
+ private confirm: ConfirmState<WorktreeConfirmSubject> | null = null;
99
+ private pendingJump: { targetPanel: 'sessions' | 'tasks' } | null = null;
100
+ private readonly worktreeRegistry: WorktreeRegistryLike;
101
+ private readonly requestRender: () => void;
102
+ private unsubscribe: (() => void) | null = null;
29
103
 
30
- public constructor(worktreeRegistry: WorktreeRegistry) {
31
- super('worktrees', 'Worktrees', 'W', 'monitoring');
104
+ public constructor(worktreeRegistry: WorktreeRegistryLike, requestRender: () => void = () => {}) {
105
+ super('worktrees', 'Worktrees', '', 'automation-control');
32
106
  this.showSelectionGutter = true; // I5: non-color selection affordance
33
107
  this.worktreeRegistry = worktreeRegistry;
108
+ this.requestRender = requestRender;
34
109
  void this.refresh();
110
+ // Live state: prefer a registry subscription when the caller provides one
111
+ // (matches the mock shape used by contract tests); otherwise poll, so the
112
+ // real SDK WorktreeRegistry (which has no subscribe() today) still stays live.
113
+ if (typeof this.worktreeRegistry.subscribe === 'function') {
114
+ this.unsubscribe = this.worktreeRegistry.subscribe(() => { void this.refresh(); });
115
+ } else {
116
+ this.registerTimer(setInterval(() => { void this.refresh(); }, POLL_INTERVAL_MS));
117
+ }
35
118
  }
36
119
 
37
120
  public override onActivate(): void {
@@ -39,26 +122,108 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
39
122
  if (!this.loading) void this.refresh();
40
123
  }
41
124
 
42
- public handleInput(key: string): boolean {
125
+ public override onDestroy(): void {
126
+ super.onDestroy();
127
+ if (this.unsubscribe) {
128
+ this.unsubscribe();
129
+ this.unsubscribe = null;
130
+ }
131
+ }
132
+
133
+ public override handleInput(key: string): boolean {
134
+ if (this.lastError !== null) this.clearError();
135
+
136
+ const confirmResult = handleConfirmInput(this.confirm, key);
137
+ if (confirmResult === 'confirmed') {
138
+ const subject = this.confirm!.subject;
139
+ this.confirm = null;
140
+ if (subject.action === 'discard') {
141
+ this.worktreeRegistry.setState(subject.path, 'discard');
142
+ void this.refresh();
143
+ } else {
144
+ void this._cleanup(subject.path);
145
+ }
146
+ this.markDirty();
147
+ return true;
148
+ }
149
+ if (confirmResult === 'cancelled') {
150
+ this.confirm = null;
151
+ this.markDirty();
152
+ return true;
153
+ }
154
+ if (confirmResult === 'absorbed') return true;
155
+
43
156
  if (key === 'r') {
44
157
  void this.refresh();
45
158
  return true;
46
159
  }
160
+
161
+ const selected = this.getSelectedItem();
162
+
163
+ if (key === 'p' && selected && selected.state !== 'paused') {
164
+ this.worktreeRegistry.setState(selected.path, 'paused');
165
+ void this.refresh();
166
+ return true;
167
+ }
168
+ if (key === 'u' && selected && selected.state !== 'active') {
169
+ this.worktreeRegistry.setState(selected.path, 'active');
170
+ void this.refresh();
171
+ return true;
172
+ }
173
+ if (key === 'k' && selected && selected.state !== 'kept') {
174
+ this.worktreeRegistry.setState(selected.path, 'kept');
175
+ void this.refresh();
176
+ return true;
177
+ }
178
+ if (key === 'd' && selected && selected.state !== 'discard') {
179
+ this.confirm = {
180
+ subject: { path: selected.path, action: 'discard' },
181
+ label: `worktree ${basename(selected.path)} (${selected.branch})`,
182
+ verb: 'Discard',
183
+ };
184
+ this.markDirty();
185
+ return true;
186
+ }
187
+ if (key === 'c' && selected) {
188
+ this.confirm = {
189
+ subject: { path: selected.path, action: 'cleanup' },
190
+ label: `worktree ${basename(selected.path)} (${selected.branch})`,
191
+ verb: 'Clean up',
192
+ };
193
+ this.markDirty();
194
+ return true;
195
+ }
196
+ if ((key === 'enter' || key === 'return') && selected && (selected.sessionId || selected.taskId)) {
197
+ this.pendingJump = { targetPanel: selected.sessionId ? 'sessions' : 'tasks' };
198
+ return true;
199
+ }
200
+
47
201
  return super.handleInput(key);
48
202
  }
49
203
 
204
+ /**
205
+ * Enter on an attached worktree row jumps to the owning session/task panel.
206
+ * The actual `PanelManager.open()` call requires the integration context,
207
+ * which is only available here (invoked right after `handleInput` returns
208
+ * `true` for the same key) — same staged-pending-action pattern as
209
+ * `incident-review-panel.ts`.
210
+ */
211
+ public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
212
+ if (this.pendingJump) {
213
+ const jump = this.pendingJump;
214
+ this.pendingJump = null;
215
+ ctx.panelManager.open(jump.targetPanel);
216
+ return true;
217
+ }
218
+ return false;
219
+ }
220
+
50
221
  protected getItems(): readonly WorktreeStatusRecord[] {
51
222
  return this.rows;
52
223
  }
53
224
 
54
225
  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
- ]);
226
+ return buildWorktreeRow(width, row, index === this.selectedIndex);
62
227
  }
63
228
 
64
229
  private async refresh(): Promise<void> {
@@ -70,17 +235,45 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
70
235
  } finally {
71
236
  this.loading = false;
72
237
  this.markDirty();
238
+ this.requestRender();
239
+ }
240
+ }
241
+
242
+ private async _cleanup(path: string): Promise<void> {
243
+ try {
244
+ await this.worktreeRegistry.cleanup(path);
245
+ } catch (err) {
246
+ this.setError(summarizeError(err));
247
+ } finally {
248
+ await this.refresh();
73
249
  }
74
250
  }
75
251
 
76
252
  public render(width: number, height: number): Line[] {
77
253
  this.needsRender = false;
254
+
255
+ if (this.confirm) {
256
+ const lines = buildPanelWorkspace(width, height, {
257
+ title: 'Worktree Control Room',
258
+ sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
259
+ palette: C,
260
+ });
261
+ while (lines.length < height) lines.push(createEmptyLine(width));
262
+ return lines.slice(0, height);
263
+ }
264
+
78
265
  const sections: PanelWorkspaceSection[] = [];
79
266
 
80
267
  if (this.loading && this.rows.length === 0) {
81
268
  sections.push({ title: 'Worktrees', lines: [buildPanelLine(width, [[' Loading worktree state...', C.info]])] });
82
269
  } else if (this.rows.length === 0) {
83
- sections.push({ title: 'Worktrees', lines: [buildPanelLine(width, [[' No git worktrees discovered for this project.', C.dim]])] });
270
+ sections.push({
271
+ title: 'Worktrees',
272
+ lines: [
273
+ buildPanelLine(width, [[' No git worktrees discovered for this project yet.', C.dim]]),
274
+ buildPanelLine(width, [[' /worktree attach <path>', C.info], [' register a worktree for orchestrator-managed lifecycle', C.dim]]),
275
+ ],
276
+ });
84
277
  } else {
85
278
  const summary = summarizeWorktreeOwnership(this.rows);
86
279
  sections.push({
@@ -88,7 +281,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
88
281
  lines: [
89
282
  buildKeyValueLine(width, [
90
283
  { label: 'total', value: String(summary.total), valueColor: C.value },
91
- { label: 'active', value: String(summary.active), valueColor: C.ok },
284
+ { label: 'active', value: String(summary.active), valueColor: C.good },
92
285
  { label: 'paused', value: String(summary.paused), valueColor: summary.paused > 0 ? C.warn : C.dim },
93
286
  { label: 'cleanup', value: String(summary.pendingCleanup), valueColor: summary.pendingCleanup > 0 ? C.warn : C.dim },
94
287
  ], C),
@@ -100,19 +293,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
100
293
  ], C),
101
294
  ],
102
295
  });
103
- sections.push({
104
- title: 'Next Actions',
105
- lines: [
106
- buildPanelLine(width, [[
107
- summary.pendingCleanup > 0 || summary.discard > 0
108
- ? ' Review pending-cleanup and discard-marked worktrees before they drift from orchestrator ownership.'
109
- : ' Worktree ownership is healthy. Use the task and session links below for restore, merge, or cleanup review.',
110
- summary.pendingCleanup > 0 || summary.discard > 0 ? C.warn : C.dim,
111
- ]]),
112
- buildPanelLine(width, [[' /worktree task <task-id> /worktree session <session-id> /worktree inspect <path>', C.info]]),
113
- ],
114
- });
115
- const selected = this.rows[this.selectedIndex]!;
296
+ const selected = this.getSelectedItem()!;
116
297
  const detailSection: PanelWorkspaceSection = {
117
298
  title: 'Details',
118
299
  lines: [
@@ -126,36 +307,30 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
126
307
  : ' Unattached worktree detected. Review whether it should be attached, kept, discarded, or cleaned up.',
127
308
  selected.sessionId || selected.taskId ? C.info : C.warn,
128
309
  ]]),
129
- buildPanelLine(width, [[
130
- selected.state === 'paused'
131
- ? ` Next: /worktree resume ${selected.path}`
132
- : selected.state === 'discard' || selected.state === 'pending-cleanup'
133
- ? ` Next: /worktree cleanup ${selected.path}`
134
- : selected.taskId
135
- ? ` Next: /worktree task ${selected.taskId}`
136
- : selected.sessionId
137
- ? ` Next: /worktree session ${selected.sessionId}`
138
- : ` Next: /worktree inspect ${selected.path}`,
139
- C.dim,
140
- ]]),
141
310
  ],
142
311
  };
143
312
  const resolvedWorktreesSection = resolvePrimaryScrollableSection(width, height, {
144
313
  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]])],
314
+ footerLines: [buildKeyboardHints(width, WORKTREE_HINTS, C)],
146
315
  palette: C,
147
316
  beforeSections: sections,
148
317
  section: {
149
318
  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
- }),
319
+ fixedLines: [
320
+ buildAlignedRow(width, [
321
+ { text: '', fg: C.dim },
322
+ { text: 'KIND', fg: C.label, bold: true },
323
+ { text: 'STATE', fg: C.label, bold: true },
324
+ { text: 'BRANCH', fg: C.label, bold: true },
325
+ { text: 'WORKTREE', fg: C.label, bold: true },
326
+ ], [
327
+ { width: 2 }, { width: 10 }, { width: 13 }, { width: 20 },
328
+ { width: Math.max(8, width - (2 + 10 + 13 + 20) - 4 - 2) },
329
+ ], { marker: ' ' }),
330
+ ],
331
+ scrollableLines: this.rows.map((row, absolute) =>
332
+ buildWorktreeRow(width, row, absolute === this.selectedIndex),
333
+ ),
159
334
  selectedIndex: this.selectedIndex,
160
335
  scrollOffset: this.scrollStart,
161
336
  guardRows: 1,
@@ -169,11 +344,14 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
169
344
  sections.push(detailSection);
170
345
  }
171
346
 
347
+ const errorLine = this.renderErrorLine(width);
348
+ if (errorLine) sections.push({ title: 'Error', lines: [errorLine] });
349
+
172
350
  const lines = buildPanelWorkspace(width, height, {
173
351
  title: 'Worktree Control Room',
174
352
  intro: 'Orchestrator-owned worktree lifecycle, attachments, pause/resume posture, and cleanup state.',
175
353
  sections,
176
- footerLines: [buildPanelLine(width, [[' r refresh /worktree inspect <path> /worktree attach|pause|resume|keep|discard|cleanup ', C.dim]])],
354
+ footerLines: [buildKeyboardHints(width, WORKTREE_HINTS, C)],
177
355
  palette: C,
178
356
  });
179
357
  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
+ }