@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.
- package/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/main.ts +0 -1
- package/src/panels/agent-inspector-panel.ts +18 -13
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +22 -53
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/development.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +25 -1
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +82 -54
- package/src/panels/debug-panel.ts +51 -16
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +19 -3
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +40 -41
- package/src/panels/file-preview-panel.ts +9 -2
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +58 -70
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +31 -12
- package/src/panels/memory-panel.ts +5 -4
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +22 -0
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +65 -23
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +40 -17
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +29 -5
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +34 -7
- package/src/panels/thinking-panel.ts +26 -25
- package/src/panels/token-budget-panel.ts +49 -38
- package/src/panels/tool-inspector-panel.ts +27 -21
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +125 -34
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +31 -127
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/version.ts +1 -1
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- 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
|
-
|
|
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({
|
|
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: [
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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: [
|
|
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
|
+
}
|
package/src/panels/wrfc-panel.ts
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
/**
|
|
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 (
|
|
39
|
+
/** Whether the top pane is focused (drives the accent border) */
|
|
32
40
|
topFocused: boolean;
|
|
33
|
-
/**
|
|
34
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
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
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
211
|
-
panelLine = p.
|
|
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
|
-
//
|
|
217
|
-
const focusFg = p.bottomFocused ?
|
|
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
|
-
//
|
|
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 +
|
|
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:
|
|
275
|
+
newBuffer.setCell(sepX, screenY, createStyledCell('│', { fg: borderFgForRow(i) }));
|
|
273
276
|
}
|
|
274
277
|
}
|
|
275
278
|
|