@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { type Line, createStyledCell } from '../types/grid.ts';
|
|
2
|
-
import { UIFactory } from './ui-factory.ts';
|
|
3
|
-
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
|
-
|
|
5
|
-
/** Color by file extension category. */
|
|
6
|
-
function getFileColor(name: string): string {
|
|
7
|
-
if (name.endsWith('/')) return '#00ffff'; // directory
|
|
8
|
-
const ext = name.split('.').pop()?.toLowerCase() ?? '';
|
|
9
|
-
if (['ts', 'tsx', 'js', 'jsx', 'mjs'].includes(ext)) return '#dcdcaa';
|
|
10
|
-
if (['json', 'yaml', 'yml', 'toml'].includes(ext)) return '#ce9178';
|
|
11
|
-
if (['md', 'txt', 'rst'].includes(ext)) return '252';
|
|
12
|
-
if (['sh', 'bash', 'zsh'].includes(ext)) return '#22c55e';
|
|
13
|
-
if (['css', 'scss', 'less'].includes(ext)) return '#569cd6';
|
|
14
|
-
if (['html', 'htm', 'xml', 'svg'].includes(ext)) return '#f97316';
|
|
15
|
-
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'ico'].includes(ext)) return '#a855f7';
|
|
16
|
-
if (['lock', 'env', 'gitignore'].includes(name)) return '238';
|
|
17
|
-
return '252';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface FileTreeEntry {
|
|
21
|
-
name: string;
|
|
22
|
-
isDir: boolean;
|
|
23
|
-
size?: number;
|
|
24
|
-
depth: number;
|
|
25
|
-
isLast: boolean;
|
|
26
|
-
isLastAtDepth: boolean[]; // tracks if each ancestor was last child
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* renderFileTree - Render a directory listing as a tree with colored file types.
|
|
31
|
-
* Returns Line[] for the cell-based pipeline.
|
|
32
|
-
*/
|
|
33
|
-
export function renderFileTree(
|
|
34
|
-
entries: FileTreeEntry[],
|
|
35
|
-
width: number,
|
|
36
|
-
title?: string
|
|
37
|
-
): Line[] {
|
|
38
|
-
const lines: Line[] = [];
|
|
39
|
-
|
|
40
|
-
// Optional header
|
|
41
|
-
if (title) {
|
|
42
|
-
lines.push(UIFactory.stringToLine(` [dir] ${title}`, width, { fg: '#00ffff', bold: true }));
|
|
43
|
-
lines.push(UIFactory.stringToLine(' ' + '-'.repeat(width - 2), width, { fg: '240' }));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
for (const entry of entries) {
|
|
47
|
-
const { name, isDir, size, depth, isLast, isLastAtDepth } = entry;
|
|
48
|
-
|
|
49
|
-
// Build tree prefix
|
|
50
|
-
let prefix = '';
|
|
51
|
-
for (let d = 0; d < depth; d++) {
|
|
52
|
-
if (d < depth - 1) {
|
|
53
|
-
prefix += isLastAtDepth[d] ? ' ' : '| ';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (depth > 0) {
|
|
57
|
-
prefix += isLast ? '`-- ' : '|-- ';
|
|
58
|
-
} else {
|
|
59
|
-
prefix += '';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const displayName = isDir ? name + '/' : name;
|
|
63
|
-
const fg = getFileColor(isDir ? displayName : name);
|
|
64
|
-
|
|
65
|
-
// Size info (dimmed)
|
|
66
|
-
let sizeStr = '';
|
|
67
|
-
if (size !== undefined && !isDir) {
|
|
68
|
-
if (size < 1024) sizeStr = ` ${size}B`;
|
|
69
|
-
else if (size < 1024 * 1024) sizeStr = ` ${(size / 1024).toFixed(1)}K`;
|
|
70
|
-
else sizeStr = ` ${(size / (1024 * 1024)).toFixed(1)}M`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const fullText = prefix + displayName;
|
|
74
|
-
const sizeW = getDisplayWidth(sizeStr);
|
|
75
|
-
const nameW = getDisplayWidth(fullText);
|
|
76
|
-
const maxNameW = width - sizeW - 1;
|
|
77
|
-
|
|
78
|
-
const truncated = nameW > maxNameW ? fullText.slice(0, Math.max(0, maxNameW - 3)) + '...' : fullText;
|
|
79
|
-
|
|
80
|
-
// Compose: name + padding + size
|
|
81
|
-
const paddingW = Math.max(0, width - getDisplayWidth(truncated) - sizeW);
|
|
82
|
-
const fullLine = truncated + ' '.repeat(paddingW);
|
|
83
|
-
|
|
84
|
-
// Build the line with per-character styles
|
|
85
|
-
const line = UIFactory.stringToLine(fullLine, width, { fg });
|
|
86
|
-
|
|
87
|
-
// Dim the size info
|
|
88
|
-
if (sizeStr && sizeW > 0) {
|
|
89
|
-
const sizeStartX = width - sizeW;
|
|
90
|
-
let cx = sizeStartX;
|
|
91
|
-
for (const ch of sizeStr) {
|
|
92
|
-
if (cx >= width) break;
|
|
93
|
-
const cw = getDisplayWidth(ch);
|
|
94
|
-
line[cx] = createStyledCell(ch, { fg: '240', dim: true });
|
|
95
|
-
if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
|
|
96
|
-
cx += cw;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
lines.push(line);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return lines;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* parseListDirOutput - Convert a simple list directory output string to FileTreeEntry[].
|
|
108
|
-
* Expects one path per line, relative paths with '/' for directories.
|
|
109
|
-
*/
|
|
110
|
-
export function parseListDirOutput(output: string, rootDir: string): FileTreeEntry[] {
|
|
111
|
-
const rawLines = output.trim().split('\n').filter(Boolean);
|
|
112
|
-
const entries: FileTreeEntry[] = [];
|
|
113
|
-
|
|
114
|
-
for (const line of rawLines) {
|
|
115
|
-
const trimmed = line.trim();
|
|
116
|
-
if (!trimmed) continue;
|
|
117
|
-
const isDir = trimmed.endsWith('/');
|
|
118
|
-
const name = isDir ? trimmed.slice(0, -1).split('/').pop() + '/' : trimmed.split('/').pop() ?? trimmed;
|
|
119
|
-
const depth = (trimmed.match(/\//g) ?? []).length - (isDir ? 1 : 0);
|
|
120
|
-
|
|
121
|
-
entries.push({
|
|
122
|
-
name: name || trimmed,
|
|
123
|
-
isDir,
|
|
124
|
-
depth: Math.max(0, depth),
|
|
125
|
-
isLast: false,
|
|
126
|
-
isLastAtDepth: [],
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Mark isLast for each entry (last sibling at same depth under same parent)
|
|
131
|
-
for (let i = 0; i < entries.length; i++) {
|
|
132
|
-
const nextSameOrLower = entries.slice(i + 1).findIndex(
|
|
133
|
-
e => e.depth <= entries[i].depth
|
|
134
|
-
);
|
|
135
|
-
entries[i].isLast = nextSameOrLower === 0 || nextSameOrLower === -1;
|
|
136
|
-
|
|
137
|
-
// Build isLastAtDepth[d] = true if the ancestor at depth d was the last child
|
|
138
|
-
// of its parent. This governs whether to draw '| ' or ' ' vertical bars.
|
|
139
|
-
const isLastAtDepth: boolean[] = new Array(entries[i].depth).fill(false);
|
|
140
|
-
for (let d = 0; d < entries[i].depth; d++) {
|
|
141
|
-
// Walk backwards to find the most recent ancestor at depth d
|
|
142
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
143
|
-
if (entries[j].depth === d) {
|
|
144
|
-
isLastAtDepth[d] = entries[j].isLast;
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
entries[i].isLastAtDepth = isLastAtDepth;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return entries;
|
|
153
|
-
}
|
package/src/renderer/progress.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { type Line } from '../types/grid.ts';
|
|
2
|
-
import { UIFactory } from './ui-factory.ts';
|
|
3
|
-
import { getDisplayWidth, padDisplayEnd } from '../utils/terminal-width.ts';
|
|
4
|
-
import { abbreviateCount } from '../utils/format-number.ts';
|
|
5
|
-
|
|
6
|
-
// Rich spinner frames (used by progress indicators)
|
|
7
|
-
export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
8
|
-
// Braille thinking spinner frames (used by the orchestrator thinking animation)
|
|
9
|
-
export const THINKING_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* renderSpinner - Render a spinner with label as a single Line.
|
|
13
|
-
*/
|
|
14
|
-
export function renderSpinner(
|
|
15
|
-
frame: string,
|
|
16
|
-
label: string,
|
|
17
|
-
width: number,
|
|
18
|
-
fg: string = '135'
|
|
19
|
-
): Line {
|
|
20
|
-
const text = ` ${frame} ${label}`;
|
|
21
|
-
return UIFactory.stringToLine(padDisplayEnd(text, width), width, { fg, bold: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* renderToolProgress - Render tool execution progress.
|
|
26
|
-
* E.g. "[2/5] Editing src/config.ts..."
|
|
27
|
-
*/
|
|
28
|
-
export function renderToolProgress(
|
|
29
|
-
current: number,
|
|
30
|
-
total: number,
|
|
31
|
-
label: string,
|
|
32
|
-
width: number
|
|
33
|
-
): Line[] {
|
|
34
|
-
const counter = `[${current}/${total}]`;
|
|
35
|
-
const text = ` ${counter} ${label}`;
|
|
36
|
-
return [
|
|
37
|
-
UIFactory.stringToLine(padDisplayEnd(text, width), width, { fg: '#ffcc00', bold: true }),
|
|
38
|
-
];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* renderTokenBar - Render a token usage bar for the footer.
|
|
43
|
-
* Shows used/max tokens as a visual bar + numbers.
|
|
44
|
-
*/
|
|
45
|
-
export function renderTokenBar(
|
|
46
|
-
used: number,
|
|
47
|
-
max: number,
|
|
48
|
-
width: number,
|
|
49
|
-
model: string,
|
|
50
|
-
toolCount: number
|
|
51
|
-
): Line[] {
|
|
52
|
-
const lines: Line[] = [];
|
|
53
|
-
|
|
54
|
-
// Stats row
|
|
55
|
-
const usedK = abbreviateCount(used, { noM: true });
|
|
56
|
-
const maxK = abbreviateCount(max, { noM: true });
|
|
57
|
-
const pct = max > 0 ? Math.min(100, Math.round((used / max) * 100)) : 0;
|
|
58
|
-
const toolStr = toolCount > 0 ? ` tools:${toolCount}` : '';
|
|
59
|
-
const statsText = ` ${model} in:${usedK}/${maxK} (${pct}%)${toolStr}`;
|
|
60
|
-
|
|
61
|
-
// Progress bar
|
|
62
|
-
const barLabel = ' ctx ';
|
|
63
|
-
const barLabelW = getDisplayWidth(barLabel);
|
|
64
|
-
const barW = Math.max(10, Math.floor(width * 0.3));
|
|
65
|
-
const filled = Math.round((pct / 100) * barW);
|
|
66
|
-
const empty = barW - filled;
|
|
67
|
-
|
|
68
|
-
// Color based on usage
|
|
69
|
-
const barFg = pct > 85 ? '#ef4444' : pct > 60 ? '#ffcc00' : '#22c55e';
|
|
70
|
-
|
|
71
|
-
const bar = '#'.repeat(filled) + '-'.repeat(empty);
|
|
72
|
-
const barText = barLabel + bar;
|
|
73
|
-
|
|
74
|
-
const statsW = getDisplayWidth(statsText);
|
|
75
|
-
const barTextW = getDisplayWidth(barText);
|
|
76
|
-
const spacingW = Math.max(1, width - statsW - barTextW - 2);
|
|
77
|
-
|
|
78
|
-
// Build with mixed colors
|
|
79
|
-
const line = UIFactory.stringToLine(statsText + ' '.repeat(spacingW), width, { fg: '244', dim: true });
|
|
80
|
-
|
|
81
|
-
// Overlay the bar with color
|
|
82
|
-
let barStartX = getDisplayWidth(statsText) + spacingW;
|
|
83
|
-
for (const ch of barLabel) {
|
|
84
|
-
if (barStartX >= width) break;
|
|
85
|
-
const cw = getDisplayWidth(ch);
|
|
86
|
-
line[barStartX] = { char: ch, fg: '244', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
|
|
87
|
-
if (cw === 2 && barStartX + 1 < width) line[barStartX + 1] = { ...line[barStartX], char: '' };
|
|
88
|
-
barStartX += cw;
|
|
89
|
-
}
|
|
90
|
-
for (let i = 0; i < filled && barStartX + i < width; i++) {
|
|
91
|
-
line[barStartX + i] = { char: '#', fg: barFg, bg: '', bold: false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
92
|
-
}
|
|
93
|
-
for (let i = 0; i < empty && barStartX + filled + i < width; i++) {
|
|
94
|
-
line[barStartX + filled + i] = { char: '-', fg: '238', bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
lines.push(line);
|
|
98
|
-
return lines;
|
|
99
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// buildStatusToken — always glyph + color, never color-only.
|
|
3
|
-
//
|
|
4
|
-
// Maps a semantic state to a Unicode glyph AND a palette color so that
|
|
5
|
-
// colorblind users and screen readers can distinguish states without color.
|
|
6
|
-
//
|
|
7
|
-
// Glyphs:
|
|
8
|
-
// good ✓ (CHECK MARK)
|
|
9
|
-
// warn ⚠ (WARNING SIGN)
|
|
10
|
-
// bad ✕ (MULTIPLICATION X)
|
|
11
|
-
// info ○ (WHITE CIRCLE)
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
import type { Cell } from '../types/grid.ts';
|
|
15
|
-
import { DEFAULT_PANEL_PALETTE } from '../panels/polish.ts';
|
|
16
|
-
import { type StatusState, STATE_GLYPHS } from './status-glyphs.ts';
|
|
17
|
-
|
|
18
|
-
// Re-export for downstream consumers that import from this module.
|
|
19
|
-
export type { StatusState } from './status-glyphs.ts';
|
|
20
|
-
export { STATE_GLYPHS } from './status-glyphs.ts';
|
|
21
|
-
|
|
22
|
-
const STATE_COLORS: Record<StatusState, string> = {
|
|
23
|
-
good: DEFAULT_PANEL_PALETTE.good,
|
|
24
|
-
warn: DEFAULT_PANEL_PALETTE.warn,
|
|
25
|
-
bad: DEFAULT_PANEL_PALETTE.bad,
|
|
26
|
-
info: DEFAULT_PANEL_PALETTE.info,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export interface StatusTokenOpts {
|
|
30
|
-
/** Append a numeric count after the label, e.g. "label (3)". */
|
|
31
|
-
count?: number;
|
|
32
|
-
/** Override the default glyph for this state. */
|
|
33
|
-
glyph?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Build a small sequence of styled cells: [glyph, space, label, optional count]
|
|
38
|
-
*
|
|
39
|
-
* Always prepends the glyph so colorblind users can parse the state.
|
|
40
|
-
* The color is applied to both glyph and label as a redundant cue.
|
|
41
|
-
*
|
|
42
|
-
* @param state Semantic state — controls default glyph and color.
|
|
43
|
-
* @param label Human-readable label string.
|
|
44
|
-
* @param opts Optional count suffix or glyph override.
|
|
45
|
-
* @returns Array of Cell objects ready to embed in a Line.
|
|
46
|
-
*/
|
|
47
|
-
export function buildStatusToken(
|
|
48
|
-
state: StatusState,
|
|
49
|
-
label: string,
|
|
50
|
-
opts?: StatusTokenOpts,
|
|
51
|
-
): Cell[] {
|
|
52
|
-
const glyph = opts?.glyph ?? STATE_GLYPHS[state];
|
|
53
|
-
const color = STATE_COLORS[state];
|
|
54
|
-
const suffix = opts?.count !== undefined ? ` (${opts.count})` : '';
|
|
55
|
-
const text = `${glyph} ${label}${suffix}`;
|
|
56
|
-
|
|
57
|
-
return text.split('').map((char): Cell => ({
|
|
58
|
-
char,
|
|
59
|
-
fg: color,
|
|
60
|
-
bg: '',
|
|
61
|
-
bold: false,
|
|
62
|
-
dim: false,
|
|
63
|
-
underline: false,
|
|
64
|
-
italic: false,
|
|
65
|
-
strikethrough: false,
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Panel resource diagnostics panel data provider.
|
|
3
|
-
*
|
|
4
|
-
* Polls the shared TUI-owned ComponentHealthMonitor and produces PanelResourceSnapshot
|
|
5
|
-
* values for the diagnostics panel to render.
|
|
6
|
-
*/
|
|
7
|
-
import type { ComponentHealthMonitor } from '../../perf/panel-health-monitor.ts';
|
|
8
|
-
import type {
|
|
9
|
-
ComponentResourceEntry,
|
|
10
|
-
ComponentResourceSnapshot,
|
|
11
|
-
} from '@/runtime/index.ts';
|
|
12
|
-
|
|
13
|
-
const DEFAULT_POLL_INTERVAL_MS = 500;
|
|
14
|
-
|
|
15
|
-
const HEALTH_ORDER: Record<string, number> = {
|
|
16
|
-
overloaded: 0,
|
|
17
|
-
warning: 1,
|
|
18
|
-
healthy: 2,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export class PanelResourcesPanel {
|
|
22
|
-
private readonly _pollIntervalMs: number;
|
|
23
|
-
private readonly _monitor: ComponentHealthMonitor;
|
|
24
|
-
private _current: ComponentResourceSnapshot;
|
|
25
|
-
private _timerId: ReturnType<typeof setInterval> | null = null;
|
|
26
|
-
private readonly _subscribers = new Set<() => void>();
|
|
27
|
-
|
|
28
|
-
constructor(monitor: ComponentHealthMonitor, pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS) {
|
|
29
|
-
this._monitor = monitor;
|
|
30
|
-
this._pollIntervalMs = pollIntervalMs;
|
|
31
|
-
this._current = this._buildSnapshot(Date.now());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
start(): void {
|
|
35
|
-
if (this._timerId !== null) return;
|
|
36
|
-
this._timerId = setInterval(() => {
|
|
37
|
-
this._current = this._buildSnapshot(Date.now());
|
|
38
|
-
this._notify();
|
|
39
|
-
}, this._pollIntervalMs);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
stop(): void {
|
|
43
|
-
if (this._timerId !== null) {
|
|
44
|
-
clearInterval(this._timerId);
|
|
45
|
-
this._timerId = null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
getSnapshot(): ComponentResourceSnapshot {
|
|
50
|
-
return this._current;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
refresh(now: number = Date.now()): ComponentResourceSnapshot {
|
|
54
|
-
this._current = this._buildSnapshot(now);
|
|
55
|
-
return this._current;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
subscribe(callback: () => void): () => void {
|
|
59
|
-
this._subscribers.add(callback);
|
|
60
|
-
return () => this._subscribers.delete(callback);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
dispose(): void {
|
|
64
|
-
this.stop();
|
|
65
|
-
this._subscribers.clear();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private _buildSnapshot(capturedAt: number): ComponentResourceSnapshot {
|
|
69
|
-
const healthStates = this._monitor.getAllHealth();
|
|
70
|
-
|
|
71
|
-
const panels: ComponentResourceEntry[] = healthStates.map((health) => {
|
|
72
|
-
const contract = this._monitor.getContract(health.componentId);
|
|
73
|
-
return {
|
|
74
|
-
componentId: health.componentId,
|
|
75
|
-
throttleStatus: health.throttleStatus,
|
|
76
|
-
healthStatus: health.healthStatus,
|
|
77
|
-
renderP95Ms: health.renderP95Ms,
|
|
78
|
-
maxRenderMs: contract?.maxRenderMs ?? 0,
|
|
79
|
-
rendersInWindow: health.rendersInWindow,
|
|
80
|
-
maxUpdatesPerSecond: contract?.maxUpdatesPerSecond ?? 0,
|
|
81
|
-
consecutiveViolations: health.consecutiveViolations,
|
|
82
|
-
totalSuppressed: health.totalSuppressed,
|
|
83
|
-
totalPermitted: health.totalPermitted,
|
|
84
|
-
lastRenderAt: health.lastRenderAt,
|
|
85
|
-
nextAllowedAt: health.nextAllowedAt,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
panels.sort((left, right) => {
|
|
90
|
-
const diff = (HEALTH_ORDER[left.healthStatus] ?? 2) - (HEALTH_ORDER[right.healthStatus] ?? 2);
|
|
91
|
-
return diff !== 0 ? diff : left.componentId.localeCompare(right.componentId);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const overloadedCount = panels.filter((panel) => panel.healthStatus === 'overloaded').length;
|
|
95
|
-
const warningCount = panels.filter((panel) => panel.healthStatus === 'warning').length;
|
|
96
|
-
const healthyCount = panels.filter((panel) => panel.healthStatus === 'healthy').length;
|
|
97
|
-
const totalSuppressed = panels.reduce((sum, panel) => sum + panel.totalSuppressed, 0);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
panels,
|
|
101
|
-
overloadedCount,
|
|
102
|
-
warningCount,
|
|
103
|
-
healthyCount,
|
|
104
|
-
totalSuppressed,
|
|
105
|
-
capturedAt,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private _notify(): void {
|
|
110
|
-
for (const callback of this._subscribers) {
|
|
111
|
-
try {
|
|
112
|
-
callback();
|
|
113
|
-
} catch {
|
|
114
|
-
// Subscriber failures must not take down diagnostics polling.
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Policy diagnostics panel data provider.
|
|
3
|
-
*
|
|
4
|
-
* Wraps a `PolicyRegistry` and optionally a `DivergencePanel` to expose
|
|
5
|
-
* combined policy state (current bundle, candidate, simulation status,
|
|
6
|
-
* divergence trends) for the diagnostics view.
|
|
7
|
-
*
|
|
8
|
-
* This panel is push-passive: it does not drive timers. Callers must
|
|
9
|
-
* call `recordTrendEntry()` periodically if a divergence panel is attached.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { PolicyBundleVersion, PolicyDiffResult } from '@/runtime/index.ts';
|
|
13
|
-
import { PolicyRegistry } from '@/runtime/index.ts';
|
|
14
|
-
import type { DivergenceDashboardSnapshot } from '@/runtime/index.ts';
|
|
15
|
-
import type { DivergencePanel } from '@/runtime/index.ts';
|
|
16
|
-
import type { PanelConfig } from '@/runtime/index.ts';
|
|
17
|
-
import { DEFAULT_PANEL_CONFIG } from '@/runtime/index.ts';
|
|
18
|
-
import type { PermissionAuditEntry } from '@/runtime/index.ts';
|
|
19
|
-
import type { PolicyLintFinding } from '@/runtime/index.ts';
|
|
20
|
-
import type { PolicySimulationSummary } from '@/runtime/index.ts';
|
|
21
|
-
import type { PolicyPreflightReview } from '@/runtime/index.ts';
|
|
22
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* A point-in-time snapshot of policy state for diagnostics rendering.
|
|
26
|
-
*/
|
|
27
|
-
export interface PolicyPanelSnapshot {
|
|
28
|
-
/** The currently enforced bundle, or null if no policy is active. */
|
|
29
|
-
current: PolicyBundleVersion | null;
|
|
30
|
-
/** The pending candidate bundle, or null if none loaded. */
|
|
31
|
-
candidate: PolicyBundleVersion | null;
|
|
32
|
-
/** History of previous active bundles (most recent first). */
|
|
33
|
-
history: PolicyBundleVersion[];
|
|
34
|
-
/** Diff between current and candidate, or null if unavailable. */
|
|
35
|
-
diff: PolicyDiffResult | null;
|
|
36
|
-
/** Divergence dashboard snapshot, or null if no panel attached. */
|
|
37
|
-
divergence: DivergenceDashboardSnapshot | null;
|
|
38
|
-
/** Recent permission requests and decisions for operator audit review. */
|
|
39
|
-
recentPermissionAudit: readonly PermissionAuditEntry[];
|
|
40
|
-
/** Policy lint findings for current and candidate bundles. */
|
|
41
|
-
lintFindings: readonly PolicyLintFinding[];
|
|
42
|
-
/** Concrete scenario results from the most recent policy simulation run. */
|
|
43
|
-
lastSimulationSummary: PolicySimulationSummary | null;
|
|
44
|
-
/** Most recent proactive policy preflight review. */
|
|
45
|
-
lastPreflightReview: PolicyPreflightReview | null;
|
|
46
|
-
/** ISO 8601 timestamp of when this snapshot was captured. */
|
|
47
|
-
capturedAt: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* PolicyPanel — diagnostics data provider for the policy registry.
|
|
52
|
-
*
|
|
53
|
-
* Usage:
|
|
54
|
-
* ```ts
|
|
55
|
-
* const registry = new PolicyRegistry();
|
|
56
|
-
* const panel = new PolicyPanel(registry, divergencePanel);
|
|
57
|
-
*
|
|
58
|
-
* panel.subscribe(() => {
|
|
59
|
-
* const snap = panel.getSnapshot();
|
|
60
|
-
* render(snap);
|
|
61
|
-
* });
|
|
62
|
-
*
|
|
63
|
-
* // Notify when the registry state changes:
|
|
64
|
-
* panel.notify();
|
|
65
|
-
*
|
|
66
|
-
* // On cleanup:
|
|
67
|
-
* panel.dispose();
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
export class PolicyPanel {
|
|
71
|
-
private readonly _registry: PolicyRegistry;
|
|
72
|
-
private readonly _divergencePanel: DivergencePanel | null;
|
|
73
|
-
private readonly _config: PanelConfig;
|
|
74
|
-
private readonly _recentPermissionAudit: readonly PermissionAuditEntry[];
|
|
75
|
-
private readonly _lintFindings: readonly PolicyLintFinding[];
|
|
76
|
-
private readonly _lastSimulationSummary: PolicySimulationSummary | null;
|
|
77
|
-
private readonly _lastPreflightReview: PolicyPreflightReview | null;
|
|
78
|
-
private readonly _subscribers = new Set<() => void>();
|
|
79
|
-
|
|
80
|
-
constructor(
|
|
81
|
-
registry: PolicyRegistry,
|
|
82
|
-
divergencePanel: DivergencePanel | null = null,
|
|
83
|
-
recentPermissionAudit: readonly PermissionAuditEntry[] = [],
|
|
84
|
-
lintFindings: readonly PolicyLintFinding[] = [],
|
|
85
|
-
lastSimulationSummary: PolicySimulationSummary | null = null,
|
|
86
|
-
lastPreflightReview: PolicyPreflightReview | null = null,
|
|
87
|
-
config: PanelConfig = DEFAULT_PANEL_CONFIG,
|
|
88
|
-
) {
|
|
89
|
-
this._registry = registry;
|
|
90
|
-
this._divergencePanel = divergencePanel;
|
|
91
|
-
this._recentPermissionAudit = recentPermissionAudit;
|
|
92
|
-
this._lintFindings = lintFindings;
|
|
93
|
-
this._lastSimulationSummary = lastSimulationSummary;
|
|
94
|
-
this._lastPreflightReview = lastPreflightReview;
|
|
95
|
-
this._config = config;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* recordTrendEntry — Forwards to the attached DivergencePanel if present.
|
|
100
|
-
*
|
|
101
|
-
* Call periodically (e.g. every 30 seconds) while simulation is active.
|
|
102
|
-
*/
|
|
103
|
-
public recordTrendEntry(): void {
|
|
104
|
-
this._divergencePanel?.recordTrendEntry();
|
|
105
|
-
this._notify();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* notify — Trigger a subscriber notification.
|
|
110
|
-
*
|
|
111
|
-
* Call after registry state changes (load, promote, rollback) so the
|
|
112
|
-
* diagnostics view can re-render.
|
|
113
|
-
*/
|
|
114
|
-
public notify(): void {
|
|
115
|
-
this._notify();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* getSnapshot — Returns the current combined policy + divergence snapshot.
|
|
120
|
-
*/
|
|
121
|
-
public getSnapshot(): PolicyPanelSnapshot {
|
|
122
|
-
const current = this._registry.getCurrent();
|
|
123
|
-
const candidate = this._registry.getCandidate();
|
|
124
|
-
const rawHistory = this._registry.getHistory();
|
|
125
|
-
// Most recent first, capped for display
|
|
126
|
-
const history = rawHistory
|
|
127
|
-
.slice()
|
|
128
|
-
.reverse()
|
|
129
|
-
.slice(0, this._config.bufferLimit);
|
|
130
|
-
|
|
131
|
-
const diff = this._registry.diff();
|
|
132
|
-
|
|
133
|
-
let divergence: DivergenceDashboardSnapshot | null = null;
|
|
134
|
-
if (this._divergencePanel) {
|
|
135
|
-
divergence = this._divergencePanel.getSnapshot();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
current,
|
|
140
|
-
candidate,
|
|
141
|
-
history,
|
|
142
|
-
diff,
|
|
143
|
-
divergence,
|
|
144
|
-
recentPermissionAudit: this._recentPermissionAudit.slice(0, this._config.bufferLimit),
|
|
145
|
-
lintFindings: this._lintFindings.slice(0, this._config.bufferLimit),
|
|
146
|
-
lastSimulationSummary: this._lastSimulationSummary,
|
|
147
|
-
lastPreflightReview: this._lastPreflightReview,
|
|
148
|
-
capturedAt: new Date().toISOString(),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Register a callback invoked whenever the panel state changes.
|
|
154
|
-
* @returns An unsubscribe function.
|
|
155
|
-
*/
|
|
156
|
-
public subscribe(callback: () => void): () => void {
|
|
157
|
-
this._subscribers.add(callback);
|
|
158
|
-
return () => this._subscribers.delete(callback);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Release all subscriptions.
|
|
163
|
-
*/
|
|
164
|
-
public dispose(): void {
|
|
165
|
-
this._subscribers.clear();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private _notify(): void {
|
|
169
|
-
for (const cb of this._subscribers) {
|
|
170
|
-
try {
|
|
171
|
-
cb();
|
|
172
|
-
} catch (err) {
|
|
173
|
-
logger.warn(`[PolicyPanel] subscriber error: ${err}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|