@pellux/goodvibes-tui 0.18.13 → 0.18.18

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 (71) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +3 -2
  5. package/src/daemon/cli.ts +82 -6
  6. package/src/input/command-registry.ts +2 -0
  7. package/src/input/commands/control-room-runtime.ts +1 -1
  8. package/src/input/commands/health-runtime.ts +1 -1
  9. package/src/input/commands/local-setup-review.ts +1 -1
  10. package/src/input/commands/platform-access-runtime.ts +1 -1
  11. package/src/input/commands/qrcode-runtime.ts +20 -0
  12. package/src/input/commands/subscription-runtime.ts +1 -1
  13. package/src/input/commands.ts +2 -0
  14. package/src/input/handler-feed.ts +6 -0
  15. package/src/input/handler-modal-routes.ts +19 -2
  16. package/src/input/handler-modal-token-routes.ts +3 -0
  17. package/src/input/handler-picker-routes.ts +4 -2
  18. package/src/input/model-picker.ts +11 -0
  19. package/src/input/settings-modal.ts +31 -3
  20. package/src/panels/agent-logs-panel.ts +23 -24
  21. package/src/panels/base-panel.ts +6 -0
  22. package/src/panels/builtin/session.ts +66 -0
  23. package/src/panels/builtin/shared.ts +1 -1
  24. package/src/panels/provider-account-snapshot.ts +1 -1
  25. package/src/panels/provider-accounts-panel.ts +23 -27
  26. package/src/panels/qr-panel.ts +182 -0
  27. package/src/panels/scrollable-list-panel.ts +407 -0
  28. package/src/panels/services-panel.ts +1 -1
  29. package/src/panels/subscription-panel.ts +1 -1
  30. package/src/panels/types.ts +6 -0
  31. package/src/panels/worktree-panel.ts +20 -19
  32. package/src/renderer/buffer.ts +19 -0
  33. package/src/renderer/compositor.ts +19 -6
  34. package/src/renderer/panel-composite.ts +24 -3
  35. package/src/renderer/qr-renderer.ts +117 -0
  36. package/src/renderer/settings-modal-helpers.ts +122 -0
  37. package/src/renderer/settings-modal.ts +147 -111
  38. package/src/runtime/bootstrap-command-context.ts +1 -1
  39. package/src/runtime/bootstrap-command-parts.ts +31 -15
  40. package/src/runtime/bootstrap-core.ts +23 -1
  41. package/src/runtime/bootstrap.ts +6 -1
  42. package/src/runtime/diagnostics/panels/index.ts +5 -5
  43. package/src/runtime/services.ts +1 -1
  44. package/src/runtime/store/domains/domain-read-matrix.ts +0 -2
  45. package/src/runtime/ui-events.ts +1 -46
  46. package/src/runtime/ui-read-model-helpers.ts +1 -32
  47. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -81
  48. package/src/runtime/ui-read-models-observability-options.ts +1 -5
  49. package/src/runtime/ui-read-models-observability-remote.ts +1 -73
  50. package/src/runtime/ui-read-models-observability-security.ts +1 -172
  51. package/src/runtime/ui-read-models-observability-system.ts +1 -217
  52. package/src/runtime/ui-read-models-observability.ts +1 -59
  53. package/src/runtime/ui-service-queries.ts +1 -114
  54. package/src/version.ts +1 -1
  55. package/src/config/service-registry.ts +0 -1
  56. package/src/config/subscription-providers.ts +0 -1
  57. package/src/runtime/diagnostics/actions.ts +0 -776
  58. package/src/runtime/diagnostics/index.ts +0 -99
  59. package/src/runtime/diagnostics/panels/agents.ts +0 -252
  60. package/src/runtime/diagnostics/panels/events.ts +0 -188
  61. package/src/runtime/diagnostics/panels/health.ts +0 -242
  62. package/src/runtime/diagnostics/panels/tasks.ts +0 -251
  63. package/src/runtime/diagnostics/panels/tool-calls.ts +0 -267
  64. package/src/runtime/diagnostics/provider.ts +0 -262
  65. package/src/runtime/store/domains/conversation.ts +0 -1
  66. package/src/runtime/store/domains/permissions.ts +0 -1
  67. package/src/runtime/store/helpers/reducers/conversation.ts +0 -1
  68. package/src/runtime/store/helpers/reducers/lifecycle.ts +0 -1
  69. package/src/runtime/store/helpers/reducers/shared.ts +0 -60
  70. package/src/runtime/store/helpers/reducers/sync.ts +0 -555
  71. package/src/runtime/store/helpers/reducers.ts +0 -30
@@ -1,242 +0,0 @@
1
- /**
2
- * Health diagnostic panel data provider.
3
- *
4
- * Subscribes to the RuntimeHealthAggregator and produces HealthDashboardData
5
- * snapshots for the health dashboard diagnostics panel.
6
- *
7
- * Implements the health visualization layer for diagnostics.
8
- * SLO status rows are included when an SloCollector is attached.
9
- * Remediation actions are included when a CascadeTimer is attached.
10
- */
11
- import type { RuntimeHealthAggregator } from '@pellux/goodvibes-sdk/platform/runtime/health/aggregator';
12
- import type { CompositeHealth, HealthDomain, HealthStatus } from '@pellux/goodvibes-sdk/platform/runtime/health/types';
13
- import type { HealthDashboardData, DomainHealthSummary, SloRow, SloGateStatus, RemediationAction } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/types';
14
- import type { SloCollector } from '@pellux/goodvibes-sdk/platform/runtime/perf/slo-collector';
15
- import type { CascadeTimer } from '@pellux/goodvibes-sdk/platform/runtime/health/cascade-timing';
16
- import { SLO_METRICS } from '@pellux/goodvibes-sdk/platform/runtime/perf/slo-collector';
17
- import { DEFAULT_BUDGETS } from '@pellux/goodvibes-sdk/platform/runtime/perf/budgets';
18
-
19
- /**
20
- * Human-readable names for playbooks, keyed by playbook ID.
21
- * Used to populate RemediationAction.playbookName in the health dashboard.
22
- */
23
- const PLAYBOOK_NAMES: ReadonlyMap<string, string> = new Map([
24
- ['stuck-turn', 'Stuck Turn / Task'],
25
- ['reconnect-failure', 'Reconnect Failure'],
26
- ['permission-deadlock', 'Permission Deadlock'],
27
- ['plugin-degradation', 'Plugin Degradation'],
28
- ['export-recovery', 'Export Recovery'],
29
- ['session-unrecoverable', 'Session Unrecoverable'],
30
- ['compaction-failure', 'Compaction Failure'],
31
- ]);
32
-
33
- /**
34
- * HealthPanel — diagnostic data provider for runtime health telemetry.
35
- *
36
- * Subscribes to health aggregator updates and maintains a current
37
- * HealthDashboardData snapshot for the panel to render.
38
- */
39
- /** Warn threshold: 20% above the SLO target triggers a 'warn' status. */
40
- const SLO_WARN_FACTOR = 1.2;
41
-
42
- /** SLO budget metadata needed for row construction, keyed by metric name. */
43
- const SLO_BUDGET_META = new Map(
44
- DEFAULT_BUDGETS
45
- .filter((b) => b.metric.startsWith('slo.'))
46
- .map((b) => [b.metric, { name: b.name, targetMs: b.threshold }])
47
- );
48
-
49
- export class HealthPanel {
50
- private readonly _aggregator: RuntimeHealthAggregator;
51
- private readonly _sloCollector: SloCollector | null;
52
- private readonly _cascadeTimer: CascadeTimer | null;
53
- private _current: HealthDashboardData;
54
- /** Registered change notification callbacks. */
55
- private readonly _subscribers = new Set<() => void>();
56
- /** Unsubscribe function from the aggregator. */
57
- private _unsub: (() => void) | null = null;
58
-
59
- /**
60
- * @param aggregator - The runtime health aggregator to subscribe to.
61
- * @param sloCollector - Optional SLO collector for SLO status rows.
62
- * When provided, SLO rows are included in every dashboard snapshot.
63
- * @param cascadeTimer - Optional CascadeTimer for remediation action rows.
64
- * When provided, active failed domains are evaluated and remediation
65
- * playbook IDs are surfaced in every dashboard snapshot.
66
- */
67
- constructor(
68
- aggregator: RuntimeHealthAggregator,
69
- sloCollector: SloCollector | null = null,
70
- cascadeTimer: CascadeTimer | null = null,
71
- ) {
72
- this._aggregator = aggregator;
73
- this._sloCollector = sloCollector;
74
- this._cascadeTimer = cascadeTimer;
75
- // Capture the initial snapshot before subscribing
76
- this._current = this._buildDashboard(aggregator.getCompositeHealth());
77
- this._unsub = aggregator.subscribe((health) => {
78
- this._current = this._buildDashboard(health);
79
- this._notify();
80
- });
81
- }
82
-
83
- /**
84
- * Build a HealthDashboardData snapshot from a CompositeHealth record.
85
- */
86
- private _buildDashboard(composite: CompositeHealth): HealthDashboardData {
87
- const domains: DomainHealthSummary[] = [];
88
- for (const [, dh] of composite.domains) {
89
- domains.push({
90
- domain: dh.domain,
91
- status: dh.status,
92
- lastTransitionAt: dh.lastTransitionAt,
93
- degradedCapabilities: dh.degradedCapabilities ?? [],
94
- failureReason: dh.failureReason,
95
- recoveryAttempts: dh.recoveryAttempts,
96
- });
97
- }
98
- // Sort: failed first, then degraded, then healthy, alphabetically within tier
99
- domains.sort((a, b) => {
100
- const order = { failed: 0, degraded: 1, healthy: 2, unknown: 3 };
101
- const diff = (order[a.status] ?? 3) - (order[b.status] ?? 3);
102
- return diff !== 0 ? diff : a.domain.localeCompare(b.domain);
103
- });
104
- return {
105
- overall: composite.overall,
106
- domains,
107
- degradedDomains: composite.degradedDomains,
108
- failedDomains: composite.failedDomains,
109
- lastUpdatedAt: composite.lastUpdatedAt,
110
- sloRows: this._buildSloRows(),
111
- remediationActions: this._buildRemediationActions(composite),
112
- };
113
- }
114
-
115
- /**
116
- * Build remediation action rows by evaluating cascade rules for all
117
- * currently-failed domains using the CascadeTimer.
118
- *
119
- * Returns an empty array when no CascadeTimer is attached or when
120
- * no domains are in the failed state.
121
- */
122
- private _buildRemediationActions(composite: CompositeHealth): readonly RemediationAction[] {
123
- if (this._cascadeTimer === null || composite.failedDomains.length === 0) {
124
- return [];
125
- }
126
-
127
- const actions: RemediationAction[] = [];
128
- const seen = new Set<string>(); // deduplicate by playbookId+ruleId
129
-
130
- for (const domain of composite.failedDomains) {
131
- const { cascades } = this._cascadeTimer.evaluate(
132
- domain,
133
- 'failed',
134
- );
135
-
136
- for (const cascade of cascades) {
137
- for (const playbookId of cascade.remediationPlaybookIds) {
138
- const key = `${playbookId}:${cascade.ruleId}`;
139
- if (seen.has(key)) continue;
140
- seen.add(key);
141
- actions.push({
142
- playbookId,
143
- playbookName: PLAYBOOK_NAMES.get(playbookId) ?? playbookId,
144
- ruleId: cascade.ruleId,
145
- sourceDomain: cascade.source,
146
- severity: cascade.severity ?? 'low',
147
- });
148
- }
149
- }
150
- }
151
-
152
- // Sort by severity: critical first, then high, medium, low
153
- const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
154
- actions.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
155
-
156
- return actions;
157
- }
158
-
159
- /**
160
- * Build SLO status rows from the current SloCollector snapshot.
161
- * Returns an empty array when no SloCollector is attached.
162
- */
163
- private _buildSloRows(): SloRow[] {
164
- if (this._sloCollector === null) return [];
165
-
166
- const metrics = this._sloCollector.getMetrics();
167
- const counts = this._sloCollector.getSampleCounts();
168
-
169
- const SLO_ORDER = [
170
- SLO_METRICS.TURN_START,
171
- SLO_METRICS.CANCEL,
172
- SLO_METRICS.RECONNECT_RECOVERY,
173
- SLO_METRICS.PERMISSION_DECISION,
174
- ] as const;
175
-
176
- return SLO_ORDER.map((metricKey): SloRow => {
177
- const metric = metrics.find((m) => m.name === metricKey);
178
- const meta = SLO_BUDGET_META.get(metricKey);
179
- const p95Ms = metric?.value ?? 0;
180
- const targetMs = meta?.targetMs ?? 0;
181
- const sampleCount = counts[metricKey] ?? 0;
182
-
183
- let status: SloGateStatus;
184
- if (sampleCount === 0) {
185
- status = 'no_data';
186
- } else if (p95Ms > targetMs) {
187
- status = 'violated';
188
- } else if (p95Ms > targetMs / SLO_WARN_FACTOR) {
189
- status = 'warn';
190
- } else {
191
- status = 'ok';
192
- }
193
-
194
- return {
195
- metric: metricKey,
196
- name: meta?.name ?? metricKey,
197
- p95Ms,
198
- targetMs,
199
- sampleCount,
200
- status,
201
- };
202
- });
203
- }
204
-
205
- /**
206
- * Return the current health dashboard snapshot.
207
- * This is updated synchronously when the aggregator fires.
208
- */
209
- public getSnapshot(): HealthDashboardData {
210
- return this._current;
211
- }
212
-
213
- /**
214
- * Register a callback invoked whenever health data changes.
215
- * @returns An unsubscribe function.
216
- */
217
- public subscribe(callback: () => void): () => void {
218
- this._subscribers.add(callback);
219
- return () => this._subscribers.delete(callback);
220
- }
221
-
222
- /**
223
- * Release the aggregator subscription.
224
- */
225
- public dispose(): void {
226
- if (this._unsub) {
227
- this._unsub();
228
- this._unsub = null;
229
- }
230
- this._subscribers.clear();
231
- }
232
-
233
- private _notify(): void {
234
- for (const cb of this._subscribers) {
235
- try {
236
- cb();
237
- } catch {
238
- // Non-fatal: subscriber errors must not crash the provider
239
- }
240
- }
241
- }
242
- }
@@ -1,251 +0,0 @@
1
- /**
2
- * Tasks diagnostic panel data provider.
3
- *
4
- * Subscribes to task lifecycle events via the RuntimeEventBus and maintains
5
- * a bounded buffer of TaskEntry records. Provides filtered snapshots
6
- * for the tasks diagnostics panel.
7
- *
8
- * Covers all task kinds: exec, agent, acp, scheduler, daemon, mcp, plugin, integration.
9
- */
10
- import type { RuntimeEventBus, EnvelopeListener } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
11
- import type { AnyRuntimeEvent } from '@pellux/goodvibes-sdk/platform/runtime/events/domain-map';
12
- import type { RuntimeEventEnvelope } from '@pellux/goodvibes-sdk/platform/runtime/events/envelope';
13
- import {
14
- type TaskEntry,
15
- type DiagnosticFilter,
16
- type PanelConfig,
17
- DEFAULT_PANEL_CONFIG,
18
- applyFilter,
19
- appendBounded,
20
- } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/types';
21
-
22
- /** Task state as tracked internally while a task is in progress. */
23
- type MutableTaskState = TaskEntry['state'];
24
-
25
- /** Internal mutable task record used while the task is in progress. */
26
- interface MutableTaskRecord {
27
- taskId: string;
28
- agentId?: string;
29
- description: string;
30
- priority: number;
31
- state: MutableTaskState;
32
- createdAt: number;
33
- completedAt?: number;
34
- durationMs?: number;
35
- progress?: number;
36
- progressMessage?: string;
37
- blockReason?: string;
38
- error?: string;
39
- traceId: string;
40
- sessionId: string;
41
- }
42
-
43
- /**
44
- * TasksPanel — diagnostic data provider for runtime task telemetry.
45
- *
46
- * Active tasks are tracked in a live map; terminal tasks are moved
47
- * to the history buffer for filtering and display.
48
- */
49
- export class TasksPanel {
50
- private readonly _config: PanelConfig;
51
- private readonly _eventBus: RuntimeEventBus;
52
- /** Active tasks keyed by taskId. */
53
- private readonly _active = new Map<string, MutableTaskRecord>();
54
- /** Completed task history (oldest first). */
55
- private readonly _history: TaskEntry[] = [];
56
- /** Registered change notification callbacks. */
57
- private readonly _subscribers = new Set<() => void>();
58
- /** Unsubscribe function from the event bus. */
59
- private _unsub: (() => void) | null = null;
60
-
61
- constructor(eventBus: RuntimeEventBus, config: PanelConfig = DEFAULT_PANEL_CONFIG) {
62
- this._eventBus = eventBus;
63
- this._config = config;
64
- this._start();
65
- }
66
-
67
- private _start(): void {
68
- const handler: EnvelopeListener<AnyRuntimeEvent> = (
69
- envelope: RuntimeEventEnvelope<AnyRuntimeEvent['type'], AnyRuntimeEvent>
70
- ) => {
71
- this._handleEnvelope(envelope);
72
- };
73
- this._unsub = this._eventBus.onDomain('tasks', handler as EnvelopeListener);
74
- }
75
-
76
- private _handleEnvelope(
77
- envelope: RuntimeEventEnvelope<AnyRuntimeEvent['type'], AnyRuntimeEvent>
78
- ): void {
79
- const p = envelope.payload;
80
- if (!('type' in p)) return;
81
- const type = (p as { type: string }).type;
82
- const traceId = envelope.traceId;
83
- const sessionId = envelope.sessionId;
84
-
85
- switch (type) {
86
- case 'TASK_CREATED': {
87
- const evt = p as { type: 'TASK_CREATED'; taskId: string; agentId?: string; description: string; priority: number };
88
- this._active.set(evt.taskId, {
89
- taskId: evt.taskId,
90
- agentId: evt.agentId,
91
- description: evt.description,
92
- priority: evt.priority,
93
- state: 'created',
94
- createdAt: envelope.ts,
95
- traceId,
96
- sessionId,
97
- });
98
- this._notify();
99
- break;
100
- }
101
- case 'TASK_STARTED': {
102
- const evt = p as { type: 'TASK_STARTED'; taskId: string };
103
- const record = this._active.get(evt.taskId);
104
- if (record) {
105
- record.state = 'running';
106
- this._notify();
107
- }
108
- break;
109
- }
110
- case 'TASK_BLOCKED': {
111
- const evt = p as { type: 'TASK_BLOCKED'; taskId: string; reason: string };
112
- const record = this._active.get(evt.taskId);
113
- if (record) {
114
- record.state = 'blocked';
115
- record.blockReason = evt.reason;
116
- this._notify();
117
- }
118
- break;
119
- }
120
- case 'TASK_PROGRESS': {
121
- const evt = p as { type: 'TASK_PROGRESS'; taskId: string; progress: number; message?: string };
122
- const record = this._active.get(evt.taskId);
123
- if (record) {
124
- record.state = 'progressing';
125
- record.progress = evt.progress;
126
- record.progressMessage = evt.message;
127
- this._notify();
128
- }
129
- break;
130
- }
131
- case 'TASK_COMPLETED': {
132
- const evt = p as { type: 'TASK_COMPLETED'; taskId: string; durationMs: number };
133
- const record = this._active.get(evt.taskId);
134
- if (record) {
135
- record.state = 'completed';
136
- record.completedAt = envelope.ts;
137
- record.durationMs = evt.durationMs;
138
- this._finalize(record);
139
- }
140
- break;
141
- }
142
- case 'TASK_FAILED': {
143
- const evt = p as { type: 'TASK_FAILED'; taskId: string; error: string; durationMs: number };
144
- const record = this._active.get(evt.taskId);
145
- if (record) {
146
- record.state = 'failed';
147
- record.completedAt = envelope.ts;
148
- record.durationMs = evt.durationMs;
149
- record.error = evt.error;
150
- this._finalize(record);
151
- }
152
- break;
153
- }
154
- case 'TASK_CANCELLED': {
155
- const evt = p as { type: 'TASK_CANCELLED'; taskId: string; reason?: string };
156
- const record = this._active.get(evt.taskId);
157
- if (record) {
158
- record.state = 'cancelled';
159
- record.completedAt = envelope.ts;
160
- record.error = evt.reason;
161
- this._finalize(record);
162
- }
163
- break;
164
- }
165
- default:
166
- break;
167
- }
168
- }
169
-
170
- private _finalize(record: MutableTaskRecord): void {
171
- this._active.delete(record.taskId);
172
- const entry: TaskEntry = {
173
- taskId: record.taskId,
174
- agentId: record.agentId,
175
- description: record.description,
176
- priority: record.priority,
177
- state: record.state,
178
- createdAt: record.createdAt,
179
- completedAt: record.completedAt,
180
- durationMs: record.durationMs,
181
- progress: record.progress,
182
- progressMessage: record.progressMessage,
183
- blockReason: record.blockReason,
184
- error: record.error,
185
- traceId: record.traceId,
186
- sessionId: record.sessionId,
187
- };
188
- appendBounded(this._history, entry, this._config.bufferLimit);
189
- this._notify();
190
- }
191
-
192
- /**
193
- * Return a filtered snapshot combining active tasks and completed history.
194
- * Ordered most-recent first.
195
- *
196
- * @param filter - Optional filter to restrict entries.
197
- */
198
- public getSnapshot(filter?: DiagnosticFilter): TaskEntry[] {
199
- const activeEntries: TaskEntry[] = [];
200
- for (const record of this._active.values()) {
201
- activeEntries.push({
202
- taskId: record.taskId,
203
- agentId: record.agentId,
204
- description: record.description,
205
- priority: record.priority,
206
- state: record.state,
207
- createdAt: record.createdAt,
208
- completedAt: record.completedAt,
209
- durationMs: record.durationMs,
210
- progress: record.progress,
211
- progressMessage: record.progressMessage,
212
- blockReason: record.blockReason,
213
- error: record.error,
214
- traceId: record.traceId,
215
- sessionId: record.sessionId,
216
- });
217
- }
218
- const combined: TaskEntry[] = [...this._history, ...activeEntries];
219
- return applyFilter(combined, filter, (e) => e.createdAt);
220
- }
221
-
222
- /**
223
- * Register a callback invoked whenever the data changes.
224
- * @returns An unsubscribe function.
225
- */
226
- public subscribe(callback: () => void): () => void {
227
- this._subscribers.add(callback);
228
- return () => this._subscribers.delete(callback);
229
- }
230
-
231
- /**
232
- * Release all event bus subscriptions and clear internal state.
233
- */
234
- public dispose(): void {
235
- if (this._unsub) {
236
- this._unsub();
237
- this._unsub = null;
238
- }
239
- this._subscribers.clear();
240
- }
241
-
242
- private _notify(): void {
243
- for (const cb of this._subscribers) {
244
- try {
245
- cb();
246
- } catch {
247
- // Non-fatal: subscriber errors must not crash the provider
248
- }
249
- }
250
- }
251
- }