@pellux/goodvibes-tui 0.26.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 (148) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +4 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/context-usage.ts +53 -0
  8. package/src/core/conversation-rendering.ts +2 -2
  9. package/src/core/conversation.ts +53 -8
  10. package/src/core/session-recovery.ts +26 -18
  11. package/src/core/system-message-router.ts +42 -26
  12. package/src/core/turn-event-wiring.ts +13 -16
  13. package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
  14. package/src/daemon/handlers/inbox/index.ts +2 -1
  15. package/src/daemon/handlers/inbox/poller.ts +2 -1
  16. package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
  17. package/src/daemon/handlers/inbox/providers/email.ts +2 -1
  18. package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
  19. package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
  20. package/src/daemon/handlers/triage/integration.ts +2 -1
  21. package/src/daemon/handlers/triage/pipeline.ts +2 -1
  22. package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
  23. package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
  24. package/src/export/gist-uploader.ts +3 -1
  25. package/src/input/command-registry.ts +1 -0
  26. package/src/input/commands/cloudflare-runtime.ts +2 -1
  27. package/src/input/commands/eval.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +2 -4
  29. package/src/input/commands/health-runtime.ts +13 -7
  30. package/src/input/commands/knowledge.ts +2 -1
  31. package/src/input/commands/runtime-services.ts +40 -10
  32. package/src/input/handler-command-route.ts +1 -1
  33. package/src/input/handler-feed-routes.ts +61 -4
  34. package/src/input/handler-feed.ts +13 -0
  35. package/src/input/handler-interactions.ts +2 -1
  36. package/src/input/handler-modal-routes.ts +2 -1
  37. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  38. package/src/input/handler-onboarding.ts +8 -8
  39. package/src/input/handler-picker-routes.ts +18 -13
  40. package/src/input/handler-ui-state.ts +5 -1
  41. package/src/input/keybindings.ts +5 -0
  42. package/src/input/model-picker.ts +5 -0
  43. package/src/input/panel-integration-actions.ts +10 -0
  44. package/src/input/tts-settings-actions.ts +2 -1
  45. package/src/main.ts +52 -60
  46. package/src/panels/agent-inspector-panel.ts +90 -15
  47. package/src/panels/agent-inspector-shared.ts +3 -3
  48. package/src/panels/agent-logs-panel.ts +149 -72
  49. package/src/panels/approval-panel.ts +146 -95
  50. package/src/panels/automation-control-panel.ts +24 -1
  51. package/src/panels/base-panel.ts +28 -3
  52. package/src/panels/builtin/agent.ts +3 -1
  53. package/src/panels/builtin/development.ts +2 -1
  54. package/src/panels/builtin/session.ts +1 -1
  55. package/src/panels/cockpit-panel.ts +24 -5
  56. package/src/panels/cockpit-read-model.ts +2 -1
  57. package/src/panels/communication-panel.ts +108 -43
  58. package/src/panels/context-visualizer-panel.ts +49 -15
  59. package/src/panels/control-plane-panel.ts +27 -1
  60. package/src/panels/cost-tracker-panel.ts +87 -65
  61. package/src/panels/debug-panel.ts +61 -37
  62. package/src/panels/diff-panel.ts +59 -30
  63. package/src/panels/docs-panel.ts +30 -24
  64. package/src/panels/eval-panel.ts +130 -81
  65. package/src/panels/expandable-list-panel.ts +189 -0
  66. package/src/panels/file-explorer-panel.ts +42 -42
  67. package/src/panels/file-preview-panel.ts +11 -3
  68. package/src/panels/forensics-panel.ts +27 -13
  69. package/src/panels/git-panel.ts +65 -84
  70. package/src/panels/hooks-panel.ts +36 -2
  71. package/src/panels/incident-review-panel.ts +31 -10
  72. package/src/panels/intelligence-panel.ts +152 -71
  73. package/src/panels/knowledge-graph-panel.ts +89 -27
  74. package/src/panels/local-auth-panel.ts +17 -11
  75. package/src/panels/marketplace-panel.ts +33 -13
  76. package/src/panels/memory-panel.ts +7 -5
  77. package/src/panels/ops-control-panel.ts +69 -7
  78. package/src/panels/ops-strategy-panel.ts +61 -43
  79. package/src/panels/orchestration-panel.ts +34 -4
  80. package/src/panels/panel-list-panel.ts +33 -8
  81. package/src/panels/panel-manager.ts +23 -4
  82. package/src/panels/plan-dashboard-panel.ts +183 -66
  83. package/src/panels/plugins-panel.ts +48 -10
  84. package/src/panels/policy-panel.ts +60 -23
  85. package/src/panels/polish-core.ts +157 -0
  86. package/src/panels/polish-tables.ts +258 -0
  87. package/src/panels/polish.ts +44 -145
  88. package/src/panels/project-planning-panel.ts +27 -4
  89. package/src/panels/provider-accounts-panel.ts +55 -18
  90. package/src/panels/provider-health-panel.ts +118 -87
  91. package/src/panels/provider-stats-panel.ts +47 -22
  92. package/src/panels/qr-panel.ts +23 -11
  93. package/src/panels/remote-panel.ts +12 -5
  94. package/src/panels/routes-panel.ts +27 -5
  95. package/src/panels/sandbox-panel.ts +62 -27
  96. package/src/panels/schedule-panel.ts +44 -24
  97. package/src/panels/scrollable-list-panel.ts +124 -10
  98. package/src/panels/security-panel.ts +72 -20
  99. package/src/panels/services-panel.ts +68 -22
  100. package/src/panels/session-browser-panel.ts +42 -26
  101. package/src/panels/session-maintenance.ts +2 -2
  102. package/src/panels/settings-sync-panel.ts +30 -15
  103. package/src/panels/skills-panel.ts +2 -1
  104. package/src/panels/subscription-panel.ts +21 -3
  105. package/src/panels/symbol-outline-panel.ts +40 -47
  106. package/src/panels/system-messages-panel.ts +60 -27
  107. package/src/panels/tasks-panel.ts +36 -14
  108. package/src/panels/thinking-panel.ts +32 -36
  109. package/src/panels/token-budget-panel.ts +80 -53
  110. package/src/panels/tool-inspector-panel.ts +37 -39
  111. package/src/panels/types.ts +25 -0
  112. package/src/panels/watchers-panel.ts +25 -5
  113. package/src/panels/work-plan-panel.ts +127 -35
  114. package/src/panels/worktree-panel.ts +65 -20
  115. package/src/panels/wrfc-panel-format.ts +133 -0
  116. package/src/panels/wrfc-panel.ts +53 -147
  117. package/src/renderer/agent-detail-modal.ts +2 -2
  118. package/src/renderer/compaction-preview.ts +2 -1
  119. package/src/renderer/compositor.ts +46 -43
  120. package/src/renderer/modal-utils.ts +0 -10
  121. package/src/renderer/model-picker-overlay.ts +9 -4
  122. package/src/renderer/model-workspace.ts +2 -3
  123. package/src/renderer/panel-composite.ts +7 -20
  124. package/src/renderer/panel-workspace-bar.ts +14 -8
  125. package/src/renderer/process-modal.ts +2 -2
  126. package/src/renderer/progress.ts +3 -2
  127. package/src/renderer/tab-strip.ts +148 -34
  128. package/src/renderer/ui-factory.ts +4 -6
  129. package/src/runtime/bootstrap-command-context.ts +3 -0
  130. package/src/runtime/bootstrap-command-parts.ts +3 -1
  131. package/src/runtime/bootstrap-core.ts +31 -31
  132. package/src/runtime/bootstrap-shell.ts +1 -0
  133. package/src/runtime/onboarding/apply.ts +4 -3
  134. package/src/runtime/onboarding/snapshot.ts +4 -3
  135. package/src/runtime/process-lifecycle.ts +195 -0
  136. package/src/runtime/services.ts +4 -1
  137. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  138. package/src/runtime/ui/model-picker/types.ts +4 -0
  139. package/src/runtime/wrfc-persistence.ts +20 -5
  140. package/src/shell/ui-openers.ts +3 -2
  141. package/src/utils/format-duration.ts +55 -0
  142. package/src/utils/format-number.ts +71 -0
  143. package/src/verification/live-verifier.ts +4 -3
  144. package/src/version.ts +1 -1
  145. package/src/core/context-auto-compact.ts +0 -110
  146. package/src/panels/panel-picker.ts +0 -106
  147. package/src/renderer/panel-picker-overlay.ts +0 -202
  148. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,7 +1,32 @@
1
1
  import type { Line } from '../types/grid.ts';
2
- import { UIFactory } from '../renderer/ui-factory.ts';
2
+ import { truncateDisplay } from '../utils/terminal-width.ts';
3
3
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
+ import {
5
+ buildDetailBlock,
6
+ buildGuidanceLine,
7
+ buildKeyboardHints,
8
+ buildKeyValueLine,
9
+ buildMeterLine,
10
+ buildPanelLine,
11
+ buildPanelListRow,
12
+ buildSummaryBlock,
13
+ DEFAULT_PANEL_PALETTE,
14
+ extendPalette,
15
+ type PanelPalette,
16
+ } from './polish.ts';
4
17
  import type { WorkPlanItem, WorkPlanItemStatus, WorkPlanStore } from '../work-plans/work-plan-store.ts';
18
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
19
+
20
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
21
+ pending: '#94a3b8',
22
+ inProgress: '#38bdf8',
23
+ blocked: '#f59e0b',
24
+ done: '#22c55e',
25
+ failed: '#ef4444',
26
+ cancelled: '#64748b',
27
+ accent: '#a5b4fc',
28
+ selectBg: '#1e293b',
29
+ });
5
30
 
6
31
  const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
7
32
  pending: '[ ]',
@@ -13,17 +38,15 @@ const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
13
38
  };
14
39
 
15
40
  const STATUS_COLOR: Record<WorkPlanItemStatus, string> = {
16
- pending: '#94a3b8',
17
- in_progress: '#38bdf8',
18
- blocked: '#f59e0b',
19
- done: '#22c55e',
20
- failed: '#ef4444',
21
- cancelled: '#64748b',
41
+ pending: C.pending,
42
+ in_progress: C.inProgress,
43
+ blocked: C.blocked,
44
+ done: C.done,
45
+ failed: C.failed,
46
+ cancelled: C.cancelled,
22
47
  };
23
48
 
24
- function line(text: string, width: number, style: Parameters<typeof UIFactory.stringToLine>[2] = {}): Line {
25
- return UIFactory.stringToLine(text.padEnd(width).slice(0, width), width, style);
26
- }
49
+ const STATUS_ORDER: WorkPlanItemStatus[] = ['pending', 'in_progress', 'blocked', 'done', 'failed', 'cancelled'];
27
50
 
28
51
  function compactDate(value: number): string {
29
52
  return new Date(value).toISOString().replace('T', ' ').slice(0, 16);
@@ -47,13 +70,16 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
47
70
  this.refresh();
48
71
  }
49
72
 
73
+ protected override getPalette(): PanelPalette {
74
+ return C;
75
+ }
76
+
50
77
  render(width: number, height: number): Line[] {
51
78
  this.refresh();
52
79
  return this.renderList(width, height, {
53
80
  title: 'Work Plan',
54
81
  header: this.renderHeader(width),
55
82
  footer: this.renderFooter(width),
56
- emptyMessage: 'No work plan items yet',
57
83
  });
58
84
  }
59
85
 
@@ -98,7 +124,7 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
98
124
  return super.handleInput(key);
99
125
  }
100
126
  } catch (error) {
101
- this.setError(error instanceof Error ? error.message : String(error));
127
+ this.setError(summarizeError(error));
102
128
  return true;
103
129
  }
104
130
  }
@@ -107,10 +133,14 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
107
133
  return this.items;
108
134
  }
109
135
 
136
+ protected override getEmptyStateMessage(): string {
137
+ return ' No work plan items yet';
138
+ }
139
+
110
140
  protected getEmptyStateActions(): Array<{ command: string; summary: string }> {
111
141
  return [
112
- { command: '/work-plan add <title>', summary: 'add a persistent item' },
113
- { command: '/work-plan list', summary: 'print the current plan' },
142
+ { command: '/work-plan add <title>', summary: 'add a persistent item that survives across sessions' },
143
+ { command: '/work-plan list', summary: 'print the current plan to the shell' },
114
144
  ];
115
145
  }
116
146
 
@@ -118,12 +148,14 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
118
148
  const status = STATUS_LABEL[item.status];
119
149
  const owner = item.owner ? ` @${item.owner}` : '';
120
150
  const source = item.source ? ` (${item.source})` : '';
121
- const text = `${status} ${item.title}${owner}${source}`;
122
- return line(text, width, {
123
- fg: selected ? '#e2e8f0' : STATUS_COLOR[item.status],
124
- bg: selected ? '#1e293b' : undefined,
125
- bold: selected || item.status === 'in_progress',
126
- });
151
+ const reserve = status.length + owner.length + source.length + 4;
152
+ const title = truncateDisplay(item.title, Math.max(6, width - reserve));
153
+ return buildPanelListRow(width, [
154
+ { text: `${status} `, fg: STATUS_COLOR[item.status], bold: item.status === 'in_progress' },
155
+ { text: title, fg: selected ? C.value : C.label, bold: selected || item.status === 'in_progress' },
156
+ { text: owner, fg: C.accent },
157
+ { text: source, fg: C.dim },
158
+ ], C, { selected, selectedBg: C.selectBg });
127
159
  }
128
160
 
129
161
  private setSelectedStatus(status: WorkPlanItemStatus): boolean {
@@ -146,30 +178,90 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
146
178
  private renderHeader(width: number): Line[] {
147
179
  const plan = this.store.getActivePlan();
148
180
  const counts = new Map<WorkPlanItemStatus, number>();
149
- for (const status of Object.keys(STATUS_LABEL) as WorkPlanItemStatus[]) counts.set(status, 0);
181
+ for (const status of STATUS_ORDER) counts.set(status, 0);
150
182
  for (const item of plan.items) counts.set(item.status, (counts.get(item.status) ?? 0) + 1);
151
- const active = this.items[this.selectedIndex];
152
- const header = [
153
- line(`Persistent Work Plan`, width, { fg: '#22d3ee', bold: true }),
154
- line(`Project: ${plan.projectRoot}`, width, { fg: '#cbd5e1' }),
155
- line(
156
- `Items: ${plan.items.length} pending ${counts.get('pending') ?? 0} active ${counts.get('in_progress') ?? 0} blocked ${counts.get('blocked') ?? 0} done ${counts.get('done') ?? 0}`,
157
- width,
158
- { fg: '#94a3b8' },
159
- ),
160
- line(`Saved: ${this.store.filePath}`, width, { fg: '#64748b' }),
183
+
184
+ const total = plan.items.length;
185
+ const done = (counts.get('done') ?? 0) + (counts.get('cancelled') ?? 0);
186
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
187
+ const meterWidth = Math.max(10, Math.min(26, width - 34));
188
+
189
+ const postureLines: Line[] = [
190
+ buildPanelLine(width, [
191
+ [' Project ', C.label],
192
+ [truncateDisplay(plan.projectRoot, Math.max(8, width - 11)), C.value],
193
+ ]),
194
+ buildMeterLine(width, Math.round((pct / 100) * meterWidth), meterWidth, {
195
+ filled: pct === 100 ? C.done : C.inProgress,
196
+ empty: C.empty,
197
+ label: C.label,
198
+ }, { prefix: ' Progress ', suffix: ` ${done}/${total} (${pct}%) ` }),
199
+ buildKeyValueLine(width, [
200
+ { label: 'pending', value: String(counts.get('pending') ?? 0), valueColor: (counts.get('pending') ?? 0) > 0 ? C.pending : C.dim },
201
+ { label: 'active', value: String(counts.get('in_progress') ?? 0), valueColor: (counts.get('in_progress') ?? 0) > 0 ? C.inProgress : C.dim },
202
+ { label: 'blocked', value: String(counts.get('blocked') ?? 0), valueColor: (counts.get('blocked') ?? 0) > 0 ? C.blocked : C.dim },
203
+ { label: 'done', value: String(counts.get('done') ?? 0), valueColor: (counts.get('done') ?? 0) > 0 ? C.done : C.dim },
204
+ ], C),
205
+ buildGuidanceLine(width, '/work-plan add <title>', 'append a persistent item to the active plan', C),
161
206
  ];
207
+
208
+ const header: Line[] = buildSummaryBlock(width, 'Persistent Work Plan', postureLines, C);
209
+
210
+ const active = this.items[this.selectedIndex];
162
211
  if (active) {
163
- header.push(line('', width));
164
- header.push(line(`Selected: ${active.id} ${statusName(active.status)} updated ${compactDate(active.updatedAt)}`, width, { fg: '#a5b4fc' }));
165
- if (active.notes) header.push(line(`Notes: ${active.notes}`, width, { fg: '#cbd5e1' }));
212
+ const detailRows: Line[] = [
213
+ buildPanelLine(width, [
214
+ [' ', C.label],
215
+ [STATUS_LABEL[active.status], STATUS_COLOR[active.status]],
216
+ [' ', C.label],
217
+ [truncateDisplay(active.title, Math.max(8, width - 6)), C.value],
218
+ ]),
219
+ buildKeyValueLine(width, [
220
+ { label: 'status', value: statusName(active.status), valueColor: STATUS_COLOR[active.status] },
221
+ ...(active.owner ? [{ label: 'owner', value: active.owner, valueColor: C.accent }] : []),
222
+ { label: 'updated', value: compactDate(active.updatedAt), valueColor: C.dim },
223
+ ], C),
224
+ ];
225
+ if (active.source) {
226
+ detailRows.push(buildPanelLine(width, [[' source ', C.label], [active.source, C.info]]));
227
+ }
228
+ if (active.notes) {
229
+ detailRows.push(buildPanelLine(width, [
230
+ [' notes ', C.label],
231
+ [truncateDisplay(active.notes, Math.max(8, width - 8)), C.value],
232
+ ]));
233
+ }
234
+ header.push(...buildDetailBlock(width, 'Selected item', detailRows, C));
166
235
  }
167
236
  return header;
168
237
  }
169
238
 
170
239
  private renderFooter(width: number): Line[] {
240
+ const hasItem = this.items.length > 0;
241
+ if (!hasItem) {
242
+ return [
243
+ buildKeyboardHints(width, [
244
+ { keys: '↑/↓', label: 'navigate' },
245
+ ], C),
246
+ ];
247
+ }
171
248
  return [
172
- line('Enter/Space cycle 1 pending 2 active 3 blocked 4 done 5 failed 6 cancelled d delete c clear done r refresh', width, { fg: '#94a3b8' }),
249
+ buildKeyboardHints(width, [
250
+ { keys: this.items.length > 0 ? `${this.selectedIndex + 1}/${this.items.length}` : '0/0', label: 'item' },
251
+ { keys: 'Enter', label: 'cycle status' },
252
+ { keys: '1-6', label: 'set status' },
253
+ { keys: 'd', label: 'delete' },
254
+ { keys: 'c', label: 'clear done' },
255
+ { keys: 'r', label: 'refresh' },
256
+ ], C),
257
+ buildPanelLine(width, [
258
+ [' 1', C.info], [' pending ', C.dim],
259
+ ['2', C.info], [' active ', C.dim],
260
+ ['3', C.info], [' blocked ', C.dim],
261
+ ['4', C.info], [' done ', C.dim],
262
+ ['5', C.info], [' failed ', C.dim],
263
+ ['6', C.info], [' cancelled', C.dim],
264
+ ]),
173
265
  ];
174
266
  }
175
267
  }
@@ -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
+ }