@pellux/goodvibes-tui 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,129 +0,0 @@
1
- export type AgentLogFilterType = 'all' | 'assistant' | 'tool' | 'error';
2
-
3
- export interface AgentLogEntry {
4
- raw: Record<string, unknown>;
5
- type: string;
6
- text: string;
7
- color: string;
8
- bold: boolean;
9
- }
10
-
11
- export const AGENT_LOG_FILTER_LABELS: Record<AgentLogFilterType, string> = {
12
- all: 'All',
13
- assistant: 'Assistant',
14
- tool: 'Tool',
15
- error: 'Error',
16
- };
17
-
18
- export const AGENT_LOG_FILTER_CYCLE: AgentLogFilterType[] = ['all', 'assistant', 'tool', 'error'];
19
-
20
- export const AGENT_LOG_COLORS = {
21
- header_bg: '235',
22
- header_fg: '250',
23
- header_accent: '#00ffff',
24
- header_label: '244',
25
- agent_selected: '#00ffff',
26
- agent_running: '#00ff87',
27
- agent_pending: '220',
28
- agent_done: '244',
29
- agent_error: '#ff5f5f',
30
- assistant: '255',
31
- tool: '#00e5ff',
32
- error: '#ff5f5f',
33
- dim: '240',
34
- paused: '220',
35
- auto_follow: '#00ff87',
36
- session_start: '238',
37
- separator: '237',
38
- filter_active: '#00ffff',
39
- filter_inactive: '244',
40
- } as const;
41
-
42
- export function parseAgentJsonl(content: string): AgentLogEntry[] {
43
- const entries: AgentLogEntry[] = [];
44
- for (const line of content.split('\n')) {
45
- const trimmed = line.trim();
46
- if (!trimmed) continue;
47
- try {
48
- const obj = JSON.parse(trimmed) as Record<string, unknown>;
49
- entries.push(toAgentLogEntry(obj));
50
- } catch {
51
- // ignore malformed lines
52
- }
53
- }
54
- return entries;
55
- }
56
-
57
- export function toAgentLogEntry(obj: Record<string, unknown>): AgentLogEntry {
58
- const type = typeof obj.type === 'string' ? obj.type : 'unknown';
59
- switch (type) {
60
- case 'meta':
61
- case 'session_start': {
62
- const agentId = String(obj.agentId ?? '');
63
- const model = String(obj.model ?? '');
64
- const provider = String(obj.provider ?? '');
65
- const ts = String(obj.timestamp ?? '').replace('T', ' ').replace(/\.\d+Z$/, '');
66
- return {
67
- raw: obj,
68
- type: 'session_start',
69
- text: `[${ts}] Session started agent=${agentId} model=${model} provider=${provider}`,
70
- color: AGENT_LOG_COLORS.session_start,
71
- bold: false,
72
- };
73
- }
74
- case 'assistant':
75
- return {
76
- raw: obj,
77
- type: 'assistant',
78
- text: String(obj.content ?? obj.text ?? ''),
79
- color: AGENT_LOG_COLORS.assistant,
80
- bold: false,
81
- };
82
- case 'tool_call': {
83
- const tool = String(obj.tool ?? obj.name ?? '');
84
- const args = obj.args ?? obj.arguments ?? {};
85
- const argsStr = typeof args === 'string' ? args : JSON.stringify(args);
86
- return {
87
- raw: obj,
88
- type: 'tool',
89
- text: `[tool] ${tool} ${argsStr.slice(0, 120)}`,
90
- color: AGENT_LOG_COLORS.tool,
91
- bold: false,
92
- };
93
- }
94
- case 'tool_result': {
95
- const tool = String(obj.tool ?? obj.name ?? '');
96
- const result = obj.result ?? obj.output ?? '';
97
- const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
98
- return {
99
- raw: obj,
100
- type: 'tool',
101
- text: `[result] ${tool} ${resultStr.slice(0, 120)}`,
102
- color: AGENT_LOG_COLORS.tool,
103
- bold: false,
104
- };
105
- }
106
- case 'error':
107
- return {
108
- raw: obj,
109
- type: 'error',
110
- text: `[error] ${String(obj.error ?? obj.message ?? obj.msg ?? JSON.stringify(obj))}`,
111
- color: AGENT_LOG_COLORS.error,
112
- bold: true,
113
- };
114
- default: {
115
- const text = typeof obj.text === 'string'
116
- ? obj.text
117
- : typeof obj.content === 'string'
118
- ? obj.content
119
- : `[${type}] ${JSON.stringify(obj).slice(0, 120)}`;
120
- return {
121
- raw: obj,
122
- type,
123
- text,
124
- color: AGENT_LOG_COLORS.dim,
125
- bold: false,
126
- };
127
- }
128
- }
129
- }
@@ -1,214 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // ContextVisualizerPanel — stacked bar showing context window composition.
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
6
- import type { Line } from '../types/grid.ts';
7
- import { BasePanel } from './base-panel.ts';
8
- import { evaluateSessionMaintenance } from '@/runtime/index.ts';
9
- import type { TurnEvent } from '@/runtime/index.ts';
10
- import type { UiEventFeed } from '../runtime/ui-events.ts';
11
- import type { UiReadModel, UiSessionSnapshot } from '../runtime/ui-read-models.ts';
12
- import type { SessionMemoryQuery } from '../runtime/ui-service-queries.ts';
13
- import {
14
- buildEmptyState,
15
- buildGuidanceLine,
16
- buildMeterLine,
17
- buildPanelLine,
18
- buildStyledPanelLine,
19
- buildPanelWorkspace,
20
- DEFAULT_PANEL_PALETTE,
21
- extendPalette,
22
- } from './polish.ts';
23
-
24
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
25
- convFg: '#cc99ff',
26
- });
27
-
28
-
29
-
30
- interface ContextSnapshot {
31
- input: number;
32
- limit: number;
33
- }
34
-
35
- function formatK(n: number): string {
36
- if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
37
- return String(n);
38
- }
39
-
40
- export class ContextVisualizerPanel extends BasePanel {
41
- private snapshot: ContextSnapshot = { input: 0, limit: 0 };
42
- private unsubs: Array<() => void> = [];
43
- private readonly sessionMemoryStore: SessionMemoryQuery;
44
-
45
- constructor(
46
- private readonly turnEvents: UiEventFeed<TurnEvent>,
47
- sessionMemoryStore: SessionMemoryQuery,
48
- private readonly configManager: Pick<ConfigManager, 'get'>,
49
- private getUsage?: () => { input: number; output: number; cacheRead: number; cacheWrite: number; model?: string },
50
- private contextLimit?: number | (() => number),
51
- private sessionReadModel?: UiReadModel<UiSessionSnapshot>,
52
- private getLastInputTokens?: () => number,
53
- ) {
54
- super('context', 'Context', 'C', 'ai');
55
- this.sessionMemoryStore = sessionMemoryStore;
56
- this._attachBus();
57
- this._refresh();
58
- }
59
-
60
- override onActivate(): void {
61
- this.needsRender = true;
62
- this._refresh();
63
- }
64
-
65
- override onDeactivate(): void {
66
- super.onDeactivate();
67
- }
68
-
69
- override onDestroy(): void {
70
- this._detachBus();
71
- }
72
-
73
- render(width: number, height: number): Line[] {
74
- return this.trackedRender(() => {
75
- if (height <= 0 || width <= 0) return [];
76
-
77
- const input = this.snapshot.input;
78
- const limit = this.snapshot.limit;
79
- const pct = limit > 0 ? Math.min(100, Math.round((input / limit) * 100)) : 0;
80
- const barWidth = Math.max(1, width - 2);
81
- const overLimit = limit > 0 && input > limit;
82
- const fg = overLimit ? C.bad : C.convFg;
83
- if (limit <= 0) {
84
- return buildPanelWorkspace(width, height, {
85
- title: ' Context Usage',
86
- intro: 'Visualize current input-token pressure against the active model context window.',
87
- sections: [
88
- {
89
- lines: buildEmptyState(
90
- width,
91
- ' Context limit unavailable',
92
- 'Select a model with a known context window and submit or complete a turn to populate live context usage.',
93
- [],
94
- DEFAULT_PANEL_PALETTE,
95
- ),
96
- },
97
- ],
98
- palette: DEFAULT_PANEL_PALETTE,
99
- });
100
- }
101
-
102
- return buildPanelWorkspace(width, height, {
103
- title: ' Context Usage',
104
- intro: 'Visualize current input-token pressure against the active model context window.',
105
- sections: [
106
- {
107
- title: 'Summary',
108
- lines: [
109
- buildPanelLine(width, [
110
- [' Input ', DEFAULT_PANEL_PALETTE.label],
111
- [formatK(input), DEFAULT_PANEL_PALETTE.value],
112
- [' Limit ', DEFAULT_PANEL_PALETTE.label],
113
- [formatK(limit), DEFAULT_PANEL_PALETTE.info],
114
- [' Fill ', DEFAULT_PANEL_PALETTE.label],
115
- [`${pct}%`, overLimit ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good],
116
- ]),
117
- ],
118
- },
119
- {
120
- title: 'Usage',
121
- lines: [
122
- this._renderBar(width, barWidth, input, limit),
123
- this._renderSegLine(width, 'Input tokens', input, pct, fg),
124
- buildPanelLine(width, [[` ${formatK(input)} / ${formatK(limit)} tokens (${pct}%) Refreshes each LLM call`, DEFAULT_PANEL_PALETTE.dim]]),
125
- ],
126
- },
127
- {
128
- title: 'Maintenance',
129
- lines: this._renderMaintenance(width),
130
- },
131
- ],
132
- palette: DEFAULT_PANEL_PALETTE,
133
- });
134
- });
135
- }
136
-
137
- private _renderBar(width: number, barWidth: number, input: number, limit: number): Line {
138
- const filled = limit > 0 ? Math.min(barWidth, Math.round((input / limit) * barWidth)) : 0;
139
- const overLimit = limit > 0 && input > limit;
140
- const barFg = overLimit ? C.bad : C.convFg;
141
- return buildMeterLine(width, filled, barWidth, {
142
- filled: barFg,
143
- empty: C.empty,
144
- label: DEFAULT_PANEL_PALETTE.dim,
145
- });
146
- }
147
-
148
- private _renderSegLine(width: number, label: string, val: number, pct: number, fg: string): Line {
149
- const labelPadded = ` ${label}`.padEnd(22);
150
- const valStr = formatK(val).padStart(7);
151
- const pctStr = `${pct}%`.padStart(5);
152
- return buildStyledPanelLine(width, [
153
- { text: labelPadded, fg: C.label },
154
- { text: valStr, fg },
155
- { text: ' ', fg: DEFAULT_PANEL_PALETTE.dim },
156
- { text: pctStr, fg },
157
- ]);
158
- }
159
-
160
- private _refresh(): void {
161
- // Resolve the context window live so the panel tracks /model switches when a
162
- // getter is supplied; a plain number stays fixed at its provided value.
163
- this.snapshot.limit =
164
- typeof this.contextLimit === 'function' ? this.contextLimit() : (this.contextLimit ?? 0);
165
- // Prefer the live per-call input occupancy (cache-inclusive, matching the
166
- // Tokens panel and the auto-compaction threshold) when a getter is wired;
167
- // otherwise fall back to the cumulative usage input.
168
- const lastInput = this.getLastInputTokens?.();
169
- if (lastInput !== undefined) {
170
- this.snapshot.input = lastInput;
171
- } else {
172
- const usage = this.getUsage?.();
173
- if (usage) {
174
- this.snapshot.input = usage.input;
175
- }
176
- }
177
- this.markDirty();
178
- }
179
-
180
- private _renderMaintenance(width: number): Line[] {
181
- const status = evaluateSessionMaintenance({
182
- configManager: this.configManager,
183
- currentTokens: this.snapshot.input,
184
- contextWindow: this.snapshot.limit,
185
- sessionMemoryCount: this.sessionMemoryStore.list().length,
186
- session: this.sessionReadModel?.getSnapshot().session,
187
- });
188
- const lines: Line[] = [
189
- buildPanelLine(width, [[` ${status.summary}`, DEFAULT_PANEL_PALETTE.label]]),
190
- ];
191
- if (status.reasons[0]) {
192
- lines.push(buildPanelLine(width, [[` ${status.reasons[0]}`, DEFAULT_PANEL_PALETTE.dim]]));
193
- }
194
- if (status.guidanceMode !== 'off' && status.nextSteps[0]) {
195
- lines.push(buildGuidanceLine(width, status.nextSteps[0], 'open the suggested maintenance action', DEFAULT_PANEL_PALETTE));
196
- }
197
- return lines;
198
- }
199
-
200
- private _attachBus(): void {
201
- if (this.unsubs.length > 0) return;
202
- this.unsubs.push(this.turnEvents.on('TURN_COMPLETED', () => {
203
- this._refresh();
204
- }));
205
- this.unsubs.push(this.turnEvents.on('TURN_SUBMITTED', () => {
206
- this._refresh();
207
- }));
208
- }
209
-
210
- private _detachBus(): void {
211
- for (const unsub of this.unsubs) unsub();
212
- this.unsubs = [];
213
- }
214
- }
@@ -1,364 +0,0 @@
1
- /**
2
- * ForensicsPanel — failure forensics TUI panel.
3
- *
4
- * Displays the most recent failure reports with auto-classified causes,
5
- * causal chains, phase timings, and jump links to related panels.
6
- *
7
- * Open via /forensics or the panel picker.
8
- */
9
- import type { Line } from '../types/grid.ts';
10
- import type { ForensicsRegistry } from '@/runtime/index.ts';
11
- import type { FailureReport, CausalChainEntry, PhaseTimingEntry } from '@/runtime/index.ts';
12
- import { ForensicsDataPanel } from '@/runtime/index.ts';
13
- import { BasePanel } from './base-panel.ts';
14
- import { createEmptyLine } from '../types/grid.ts';
15
- import {
16
- buildEmptyState,
17
- buildPanelLine,
18
- buildPanelWorkspace,
19
- resolveScrollablePanelSection,
20
- DEFAULT_PANEL_PALETTE,
21
- type PanelWorkspaceSection,
22
- } from './polish.ts';
23
-
24
- // ── Colour palette ────────────────────────────────────────────────────────────
25
- const C = {
26
- ...DEFAULT_PANEL_PALETTE,
27
- header: '#94a3b8',
28
- headerBg: '#1e293b',
29
- reportId: '#475569',
30
- timestamp: '#64748b',
31
- classification: '#f97316',
32
- classOk: '#22c55e',
33
- classCancelled: '#94a3b8',
34
- classError: '#ef4444',
35
- classWarn: '#eab308',
36
- summaryText: '#cbd5e1',
37
- label: '#64748b',
38
- value: '#e2e8f0',
39
- chainRoot: '#f97316',
40
- chainEntry: '#94a3b8',
41
- phaseOk: '#22c55e',
42
- phaseFail: '#ef4444',
43
- phasePending: '#64748b',
44
- jumpLink: '#38bdf8',
45
- separator: '#1e293b',
46
- dim: '#334155',
47
- empty: '#4b5563',
48
- selectBg: '#1e3a5f',
49
- } as const;
50
-
51
- // ── Helpers ────────────────────────────────────────────────────────────────────
52
-
53
- function fmtTime(ts: number): string {
54
- const d = new Date(ts);
55
- const hh = String(d.getHours()).padStart(2, '0');
56
- const mm = String(d.getMinutes()).padStart(2, '0');
57
- const ss = String(d.getSeconds()).padStart(2, '0');
58
- return `${hh}:${mm}:${ss}`;
59
- }
60
-
61
- function fmtDuration(ms: number | undefined): string {
62
- if (ms === undefined) return '?ms';
63
- if (ms < 1000) return `${ms}ms`;
64
- return `${(ms / 1000).toFixed(1)}s`;
65
- }
66
-
67
- function classificationColor(cls: FailureReport['classification']): string {
68
- switch (cls) {
69
- case 'cancelled': return C.classCancelled;
70
- case 'max_tokens': return C.classWarn;
71
- case 'unknown': return C.classWarn;
72
- case 'llm_error': return C.classError;
73
- case 'tool_failure': return C.classError;
74
- case 'permission_denied':return C.classError;
75
- case 'cascade_failure': return C.classError;
76
- case 'turn_timeout': return C.classError;
77
- case 'compaction_error': return C.classError;
78
- default: return C.classification;
79
- }
80
- }
81
-
82
- // ── ForensicsPanel ────────────────────────────────────────────────────────────
83
-
84
- export class ForensicsPanel extends BasePanel {
85
- private readonly _data: ForensicsDataPanel;
86
- private _unsub: (() => void) | null = null;
87
- // eslint-disable-next-line @typescript-eslint/prefer-readonly -- mutated in _renderDetail via direct assignment
88
- private _scrollOffset = 0;
89
- /** Index of the selected report in the all-reports list (newest-first). */
90
- private _selectedIndex = 0;
91
- /** View mode: 'list' shows report list; 'detail' shows a single report expanded. */
92
- private _mode: 'list' | 'detail' = 'list';
93
-
94
- public constructor(registry: ForensicsRegistry) {
95
- super('forensics', 'Forensics', 'F', 'monitoring');
96
- this._data = new ForensicsDataPanel(registry);
97
- this._unsub = this._data.subscribe(() => this.markDirty());
98
- }
99
-
100
- public override onActivate(): void {
101
- super.onActivate();
102
- this._scrollOffset = 0;
103
- this._selectedIndex = 0;
104
- this._mode = 'list';
105
- }
106
-
107
- public handleInput(key: string): boolean {
108
- if (this._mode === 'list') {
109
- if (key === 'up' || key === 'k') {
110
- this._selectedIndex = Math.max(0, this._selectedIndex - 1);
111
- this.markDirty();
112
- return true;
113
- }
114
- if (key === 'down' || key === 'j') {
115
- const count = this._data.getAll().length;
116
- this._selectedIndex = Math.min(count - 1, this._selectedIndex + 1);
117
- this.markDirty();
118
- return true;
119
- }
120
- if (key === 'return' || key === 'enter') {
121
- this._mode = 'detail';
122
- this._scrollOffset = 0;
123
- this.markDirty();
124
- return true;
125
- }
126
- } else {
127
- if (key === 'escape' || key === 'q') {
128
- this._mode = 'list';
129
- this.markDirty();
130
- return true;
131
- }
132
- if (key === 'up' || key === 'k') {
133
- this._scrollOffset = Math.max(0, this._scrollOffset - 1);
134
- this.markDirty();
135
- return true;
136
- }
137
- if (key === 'down' || key === 'j') {
138
- this._scrollOffset++;
139
- this.markDirty();
140
- return true;
141
- }
142
- }
143
- return false;
144
- }
145
-
146
- public override onDestroy(): void {
147
- if (this._unsub) {
148
- this._unsub();
149
- this._unsub = null;
150
- }
151
- this._data.dispose();
152
- }
153
-
154
- public render(width: number, height: number): Line[] {
155
- this.needsRender = false;
156
- const intro = 'Recent failure reports, causal chains, phase timings, and cross-panel jump targets for incident investigation.';
157
- const reports = this._data.getAll();
158
-
159
- if (reports.length === 0) {
160
- const workspace = buildPanelWorkspace(width, height, {
161
- title: 'Failure Forensics',
162
- intro,
163
- sections: [{
164
- lines: buildEmptyState(
165
- width,
166
- ' No failure reports. All systems nominal.',
167
- 'Failures will appear here with classification, causal chains, phase timings, and jump links as runtime incidents are captured.',
168
- [{ command: '/incident', summary: 'open the incident review workspace and forensics surfaces' }],
169
- C,
170
- ),
171
- }],
172
- palette: C,
173
- });
174
- while (workspace.length < height) workspace.push(createEmptyLine(width));
175
- return workspace;
176
- }
177
-
178
- const lines: Line[] = [];
179
- if (this._mode === 'list') {
180
- this._renderList(lines, reports, width, height, intro);
181
- } else {
182
- const report = reports[this._selectedIndex];
183
- if (report) {
184
- this._renderDetail(lines, report, width, height, intro);
185
- } else {
186
- this._mode = 'list';
187
- this._renderList(lines, reports, width, height, intro);
188
- }
189
- }
190
-
191
- while (lines.length < height) lines.push(createEmptyLine(width));
192
- return lines;
193
- }
194
-
195
- // ── List view ──────────────────────────────────────────────────────────────
196
-
197
- private _renderList(lines: Line[], reports: FailureReport[], width: number, height: number, intro: string): void {
198
- const reportRows: Line[] = [
199
- buildPanelLine(width, [[' ID TIME CLASS SUMMARY', C.label]]),
200
- ];
201
-
202
- for (let i = 0; i < reports.length; i++) {
203
- const report = reports[i]!;
204
- const isSelected = i === this._selectedIndex;
205
- const bg = isSelected ? C.selectBg : undefined;
206
-
207
- const idStr = report.id.slice(0, 8).padEnd(8, ' ');
208
- const timeStr = fmtTime(report.generatedAt);
209
- const cls = report.classification.slice(0, 20).padEnd(20, ' ');
210
- const clsColor = classificationColor(report.classification);
211
- const summaryMax = Math.max(0, width - 42);
212
- const summaryStr = report.summary.slice(0, summaryMax);
213
-
214
- const segs: Array<[string, string, string?]> = [
215
- [isSelected ? '▸' : ' ', C.jumpLink, bg],
216
- [`${idStr} `, C.reportId, bg],
217
- [`${timeStr} `, C.timestamp, bg],
218
- [`${cls} `, clsColor, bg],
219
- [summaryStr, C.summaryText, bg],
220
- ];
221
- reportRows.push(buildPanelLine(width, segs));
222
- }
223
- const reportsSection = resolveScrollablePanelSection(width, height, {
224
- intro,
225
- palette: C,
226
- section: {
227
- title: 'Reports',
228
- scrollableLines: reportRows,
229
- selectedIndex: this._selectedIndex + 1,
230
- scrollOffset: this._scrollOffset,
231
- minRows: 4,
232
- appendWindowSummary: {
233
- dimColor: C.label,
234
- formatter: () => buildPanelLine(width, [[` [${this._selectedIndex + 1}/${reports.length}] Up/Down navigate Enter expand`, C.label]]),
235
- },
236
- },
237
- });
238
- this._scrollOffset = reportsSection.scrollOffset;
239
-
240
- lines.push(...buildPanelWorkspace(width, height, {
241
- title: 'Failure Forensics',
242
- intro,
243
- sections: [reportsSection.section],
244
- palette: C,
245
- }));
246
- }
247
-
248
- // ── Detail view ────────────────────────────────────────────────────────────
249
-
250
- private _renderDetail(lines: Line[], report: FailureReport, width: number, height: number, intro: string): void {
251
- const detailLines: Line[] = [];
252
-
253
- detailLines.push(buildPanelLine(width, [
254
- [' Report: ', C.label],
255
- [report.id, C.value],
256
- [' Generated: ', C.label],
257
- [fmtTime(report.generatedAt), C.timestamp],
258
- ]));
259
- detailLines.push(buildPanelLine(width, [
260
- [' Class: ', C.label],
261
- [report.classification, classificationColor(report.classification)],
262
- ]));
263
- detailLines.push(buildPanelLine(width, [
264
- [' Summary: ', C.label],
265
- [report.summary.slice(0, width - 11), C.summaryText],
266
- ]));
267
-
268
- if (report.errorMessage) {
269
- detailLines.push(buildPanelLine(width, [
270
- [' Error: ', C.label],
271
- [report.errorMessage.slice(0, width - 11), C.classError],
272
- ]));
273
- }
274
- if (report.stopReason) {
275
- detailLines.push(buildPanelLine(width, [
276
- [' Stop: ', C.label],
277
- [report.stopReason, C.classWarn],
278
- ]));
279
- }
280
- if (report.taskId) {
281
- detailLines.push(buildPanelLine(width, [[` Task: ${report.taskId}`, C.value]]));
282
- }
283
- if (report.turnId) {
284
- detailLines.push(buildPanelLine(width, [[` Turn: ${report.turnId}`, C.value]]));
285
- }
286
-
287
- // Phase timings
288
- if (report.phaseTimings.length > 0) {
289
- detailLines.push(buildPanelLine(width, [[' Phase Timings:', C.label]]));
290
- for (const pt of report.phaseTimings) {
291
- this._renderPhase(detailLines, pt, width);
292
- }
293
- }
294
-
295
- // Causal chain
296
- if (report.causalChain.length > 0) {
297
- detailLines.push(buildPanelLine(width, [[' Causal Chain:', C.label]]));
298
- for (const entry of report.causalChain) {
299
- this._renderCausal(detailLines, entry, width);
300
- }
301
- }
302
-
303
- // Jump links
304
- if (report.jumpLinks.length > 0) {
305
- detailLines.push(buildPanelLine(width, [[' Jump Links:', C.label]]));
306
- for (const link of report.jumpLinks) {
307
- const kindTag = link.kind === 'panel' ? '[panel]' : '[cmd] ';
308
- detailLines.push(buildPanelLine(width, [
309
- [' ', C.dim],
310
- [kindTag, C.timestamp],
311
- [` ${link.label}`, C.jumpLink],
312
- [link.args ? ` (${link.args})` : '', C.dim],
313
- ]));
314
- }
315
- }
316
-
317
- detailLines.push(buildPanelLine(width, [[' Esc/q: back to list Up/Down: scroll', C.dim]]));
318
-
319
- const detailSection = resolveScrollablePanelSection(width, height, {
320
- intro,
321
- palette: C,
322
- section: {
323
- title: 'Report Detail',
324
- scrollableLines: detailLines,
325
- scrollOffset: this._scrollOffset,
326
- minRows: 1,
327
- },
328
- });
329
- this._scrollOffset = detailSection.scrollOffset;
330
- lines.push(...buildPanelWorkspace(width, height, {
331
- title: 'Failure Forensics',
332
- intro,
333
- sections: [detailSection.section],
334
- palette: C,
335
- }));
336
- }
337
-
338
- private _renderPhase(lines: Line[], pt: PhaseTimingEntry, width: number): void {
339
- const statusChar = pt.success ? '✓' : '✕';
340
- const statusColor = pt.success ? C.phaseOk : C.phaseFail;
341
- const dur = fmtDuration(pt.durationMs);
342
- const phaseLabel = pt.phase.slice(0, 14).padEnd(14, ' ');
343
- const errPart = pt.error ? ` ${pt.error.slice(0, Math.max(0, width - 32))}` : '';
344
- lines.push(buildPanelLine(width, [
345
- [' ', C.dim],
346
- [statusChar + ' ', statusColor],
347
- [phaseLabel, C.value],
348
- [dur.padStart(6, ' '), C.timestamp],
349
- [errPart, C.classError],
350
- ]));
351
- }
352
-
353
- private _renderCausal(lines: Line[], entry: CausalChainEntry, width: number): void {
354
- const prefix = entry.isRootCause ? ' * ' : ' - ';
355
- const color = entry.isRootCause ? C.chainRoot : C.chainEntry;
356
- const timeStr = fmtTime(entry.ts);
357
- const descMax = Math.max(0, width - prefix.length - 9);
358
- lines.push(buildPanelLine(width, [
359
- [prefix, color],
360
- [`${timeStr} `, C.timestamp],
361
- [entry.description.slice(0, descMax), color],
362
- ]));
363
- }
364
- }