@pellux/goodvibes-tui 0.18.19 → 0.18.23
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 +170 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-rendering.ts +20 -6
- package/src/input/commands/session.ts +0 -1
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed-routes.ts +10 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +121 -119
- package/src/input/keybindings.ts +30 -0
- package/src/panels/approval-panel.ts +54 -74
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/base-panel.ts +71 -0
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/confirm-state.ts +61 -0
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/git-panel.ts +9 -0
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/knowledge-panel.ts +63 -14
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/marketplace-panel.ts +19 -12
- package/src/panels/mcp-panel.ts +108 -155
- package/src/panels/ops-control-panel.ts +50 -85
- package/src/panels/panel-manager.ts +22 -2
- package/src/panels/plugins-panel.ts +36 -60
- package/src/panels/routes-panel.ts +89 -141
- package/src/panels/scrollable-list-panel.ts +71 -16
- package/src/panels/security-panel.ts +101 -137
- package/src/panels/services-panel.ts +58 -102
- package/src/panels/settings-sync-panel.ts +76 -122
- package/src/panels/skills-panel.ts +44 -0
- package/src/panels/subscription-panel.ts +69 -80
- package/src/panels/tasks-panel.ts +129 -179
- package/src/panels/watchers-panel.ts +88 -137
- package/src/renderer/buffer.ts +11 -0
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +37 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/renderer/status-token.ts +71 -0
- package/src/version.ts +1 -1
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import {
|
|
3
|
-
import { BasePanel } from './base-panel.ts';
|
|
2
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
3
|
import {
|
|
5
4
|
buildDetailBlock,
|
|
6
5
|
buildGuidanceLine,
|
|
7
6
|
buildPanelListRow,
|
|
8
7
|
buildPanelLine,
|
|
9
|
-
buildPanelWorkspace,
|
|
10
8
|
buildSummaryBlock,
|
|
11
9
|
DEFAULT_PANEL_PALETTE,
|
|
12
|
-
|
|
13
|
-
type PanelWorkspaceSection,
|
|
10
|
+
type PanelPalette,
|
|
14
11
|
} from './polish.ts';
|
|
15
12
|
import { getSettingsControlPlaneSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/settings/control-plane';
|
|
16
13
|
import type { ConfigManager } from '../config/index.ts';
|
|
@@ -24,141 +21,98 @@ const C = {
|
|
|
24
21
|
error: '#ef4444',
|
|
25
22
|
} as const;
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
private selectedIndex = 0;
|
|
29
|
-
private scrollOffset = 0;
|
|
24
|
+
type ResolvedEntry = ReturnType<typeof getSettingsControlPlaneSnapshot>['resolvedEntries'][number];
|
|
30
25
|
|
|
26
|
+
export class SettingsSyncPanel extends ScrollableListPanel<ResolvedEntry> {
|
|
31
27
|
public constructor(private readonly configManager: ConfigManager) {
|
|
32
28
|
super('settings-sync', 'Settings Sync', 'S', 'monitoring');
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
31
|
+
protected override getPalette(): PanelPalette {
|
|
32
|
+
return C;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected getItems(): readonly ResolvedEntry[] {
|
|
36
|
+
return getSettingsControlPlaneSnapshot(this.configManager).resolvedEntries;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected renderItem(entry: ResolvedEntry, _index: number, selected: boolean, width: number): Line {
|
|
40
|
+
return buildPanelListRow(width, [
|
|
41
|
+
{ text: entry.key.padEnd(32), fg: C.value },
|
|
42
|
+
{ text: ` ${entry.effectiveSource}`.padEnd(11), fg: entry.effectiveSource === 'managed' ? C.warn : entry.effectiveSource === 'synced' ? C.ok : entry.effectiveSource === 'local' ? C.info : C.dim },
|
|
43
|
+
{ text: `${String(entry.effectiveValue)}`.slice(0, Math.max(0, width - 47)), fg: entry.locked ? C.warn : C.dim },
|
|
44
|
+
], C, { selected });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected override getEmptyStateMessage(): string {
|
|
48
|
+
return ' No resolved settings entries.';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
public render(width: number, height: number): Line[] {
|
|
52
|
-
this.needsRender = false;
|
|
53
52
|
const snapshot = getSettingsControlPlaneSnapshot(this.configManager);
|
|
54
|
-
|
|
55
|
-
this.selectedIndex = safeSelectedIndex;
|
|
53
|
+
|
|
56
54
|
const postureLines: Line[] = [
|
|
57
55
|
buildPanelLine(width, [[' resolved keys ', C.label], [String(snapshot.resolvedEntries.length), C.value], [' conflicts ', C.label], [String(snapshot.conflicts.length), snapshot.conflicts.length > 0 ? C.error : C.good], [' failures ', C.label], [String(snapshot.recentFailures.length), snapshot.recentFailures.length > 0 ? C.warn : C.good]]),
|
|
58
56
|
buildPanelLine(width, [[' managed locks ', C.label], [String(snapshot.managedLockCount), snapshot.managedLockCount > 0 ? C.warn : C.dim], [' staged bundle ', C.label], [snapshot.stagedManagedBundle ? snapshot.stagedManagedBundle.profileName : 'none', snapshot.stagedManagedBundle ? C.info : C.dim]]),
|
|
59
57
|
buildGuidanceLine(width, '/settingssync conflicts', 'review conflicting synced values before they silently shape effective configuration', C),
|
|
60
58
|
buildGuidanceLine(width, '/managed review', 'inspect staged managed changes, risk posture, and rollback records', C),
|
|
61
59
|
];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
title: 'Failures',
|
|
97
|
-
lines: snapshot.recentFailures.length > 0
|
|
98
|
-
? snapshot.recentFailures.map((failure) => buildPanelLine(width, [[` ${failure.surface}`.padEnd(10), C.error], [` ${failure.message}`.slice(0, Math.max(0, width - 12)), C.dim]]))
|
|
99
|
-
: [buildPanelLine(width, [[' No recent sync or managed-setting failures.', C.dim]])],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
title: 'Conflicts',
|
|
103
|
-
lines: snapshot.conflicts.length > 0
|
|
104
|
-
? snapshot.conflicts.map((conflict) => buildPanelLine(width, [[` ${conflict.key}`.padEnd(30), C.value], [` ${conflict.source}`.padEnd(10), C.warn], [` resolve: /settingssync resolve ${conflict.key} local|synced`.slice(0, Math.max(0, width - 42)), C.dim]]))
|
|
105
|
-
: [buildPanelLine(width, [[' No settings conflicts detected.', C.dim]])],
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
title: 'Rollback History',
|
|
109
|
-
lines: snapshot.rollbackHistory.length > 0
|
|
110
|
-
? snapshot.rollbackHistory.map((entry) => buildPanelLine(width, [[` ${entry.token}`.padEnd(18), C.info], [` ${entry.profileName}`.padEnd(18), C.value], [` restored=${String(entry.restoredKeys.length).padEnd(4)}`, C.warn], [` ${new Date(entry.appliedAt).toLocaleString()}`.slice(0, Math.max(0, width - 46)), C.dim]]))
|
|
111
|
-
: [buildPanelLine(width, [[' No managed rollback records yet.', C.dim]])],
|
|
112
|
-
},
|
|
60
|
+
|
|
61
|
+
const headerLines: Line[] = [
|
|
62
|
+
...buildSummaryBlock(width, 'Settings posture', postureLines, C),
|
|
63
|
+
buildPanelLine(width, [[' local typed config ', C.label], [String(snapshot.liveKeyCount), C.value], [' saved profiles ', C.label], [String(snapshot.profileCount), C.info], [' managed locks ', C.label], [String(snapshot.managedLockCount), snapshot.managedLockCount > 0 ? C.warn : C.dim]]),
|
|
64
|
+
buildPanelLine(width, [[' effective local ', C.label], [String(snapshot.resolvedCounts.local), C.info], [' synced ', C.label], [String(snapshot.resolvedCounts.synced), snapshot.resolvedCounts.synced > 0 ? C.ok : C.dim], [' managed ', C.label], [String(snapshot.resolvedCounts.managed), snapshot.resolvedCounts.managed > 0 ? C.warn : C.dim]]),
|
|
65
|
+
buildPanelLine(width, [[' last sync ', C.label], [snapshot.lastSync ? `${snapshot.lastSync.surface}/${snapshot.lastSync.direction}` : 'none', snapshot.lastSync ? C.ok : C.dim], [' when ', C.label], [snapshot.lastSync ? new Date(snapshot.lastSync.timestamp).toLocaleString() : 'n/a', C.dim]]),
|
|
66
|
+
// Staged Bundle
|
|
67
|
+
...(snapshot.stagedManagedBundle
|
|
68
|
+
? [
|
|
69
|
+
buildPanelLine(width, [[' profile ', C.label], [snapshot.stagedManagedBundle.profileName, C.value], [' risk ', C.label], [snapshot.stagedManagedBundle.risk, snapshot.stagedManagedBundle.risk === 'high' ? C.error : snapshot.stagedManagedBundle.risk === 'medium' ? C.warn : C.ok], [' changes ', C.label], [String(snapshot.stagedManagedBundle.changeCount), C.info]]),
|
|
70
|
+
buildPanelLine(width, [[' path ', C.label], [snapshot.stagedManagedBundle.path.slice(0, Math.max(0, width - 9)), C.dim]]),
|
|
71
|
+
]
|
|
72
|
+
: [buildPanelLine(width, [[' No staged managed settings bundle.', C.dim]])]),
|
|
73
|
+
// Recent Events
|
|
74
|
+
...(snapshot.recentEvents.length > 0
|
|
75
|
+
? snapshot.recentEvents.map((event) => buildPanelLine(width, [[` ${event.surface}/${event.direction}`.padEnd(18), C.info], [` ${event.detail}`.slice(0, Math.max(0, width - 20)), C.dim]]))
|
|
76
|
+
: [buildPanelLine(width, [[' No sync or managed-setting events recorded yet.', C.dim]])]),
|
|
77
|
+
// Managed Locks
|
|
78
|
+
...(snapshot.managedLocks.length > 0
|
|
79
|
+
? snapshot.managedLocks.slice(0, 10).map((lock) => buildPanelLine(width, [[` ${lock.key}`.padEnd(30), C.value], [` source=${lock.source}`.padEnd(24), C.info], [` ${lock.reason}`.slice(0, Math.max(0, width - 56)), C.dim]]))
|
|
80
|
+
: [buildPanelLine(width, [[' No managed locks are currently active.', C.dim]])]),
|
|
81
|
+
// Failures
|
|
82
|
+
...(snapshot.recentFailures.length > 0
|
|
83
|
+
? snapshot.recentFailures.map((failure) => buildPanelLine(width, [[` ${failure.surface}`.padEnd(10), C.error], [` ${failure.message}`.slice(0, Math.max(0, width - 12)), C.dim]]))
|
|
84
|
+
: [buildPanelLine(width, [[' No recent sync or managed-setting failures.', C.dim]])]),
|
|
85
|
+
// Conflicts
|
|
86
|
+
...(snapshot.conflicts.length > 0
|
|
87
|
+
? snapshot.conflicts.map((conflict) => buildPanelLine(width, [[` ${conflict.key}`.padEnd(30), C.value], [` ${conflict.source}`.padEnd(10), C.warn], [` resolve: /settingssync resolve ${conflict.key} local|synced`.slice(0, Math.max(0, width - 42)), C.dim]]))
|
|
88
|
+
: [buildPanelLine(width, [[' No settings conflicts detected.', C.dim]])]),
|
|
89
|
+
// Rollback History
|
|
90
|
+
...(snapshot.rollbackHistory.length > 0
|
|
91
|
+
? snapshot.rollbackHistory.map((entry) => buildPanelLine(width, [[` ${entry.token}`.padEnd(18), C.info], [` ${entry.profileName}`.padEnd(18), C.value], [` restored=${String(entry.restoredKeys.length).padEnd(4)}`, C.warn], [` ${new Date(entry.appliedAt).toLocaleString()}`.slice(0, Math.max(0, width - 46)), C.dim]]))
|
|
92
|
+
: [buildPanelLine(width, [[' No managed rollback records yet.', C.dim]])]),
|
|
113
93
|
];
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
beforeSections: prefixSections,
|
|
131
|
-
section: {
|
|
132
|
-
title: 'Resolved Entries',
|
|
133
|
-
scrollableLines: snapshot.resolvedEntries.map((entry, absolute) => {
|
|
134
|
-
return buildPanelListRow(width, [
|
|
135
|
-
{ text: entry.key.padEnd(32), fg: C.value },
|
|
136
|
-
{ text: ` ${entry.effectiveSource}`.padEnd(11), fg: entry.effectiveSource === 'managed' ? C.warn : entry.effectiveSource === 'synced' ? C.ok : entry.effectiveSource === 'local' ? C.info : C.dim },
|
|
137
|
-
{ text: `${String(entry.effectiveValue)}`.slice(0, Math.max(0, width - 47)), fg: entry.locked ? C.warn : C.dim },
|
|
138
|
-
], C, { selected: absolute === this.selectedIndex });
|
|
139
|
-
}),
|
|
140
|
-
selectedIndex: this.selectedIndex,
|
|
141
|
-
scrollOffset: this.scrollOffset,
|
|
142
|
-
guardRows: 1,
|
|
143
|
-
minRows: 4,
|
|
144
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
145
|
-
},
|
|
146
|
-
afterSections: selectedSections,
|
|
147
|
-
});
|
|
148
|
-
this.scrollOffset = resolvedEntriesSection.scrollOffset;
|
|
149
|
-
const sections: PanelWorkspaceSection[] = [
|
|
150
|
-
...prefixSections,
|
|
151
|
-
resolvedEntriesSection.section,
|
|
152
|
-
...selectedSections,
|
|
94
|
+
|
|
95
|
+
this.clampSelection();
|
|
96
|
+
const selectedEntry = snapshot.resolvedEntries[this.selectedIndex];
|
|
97
|
+
const footerLines: Line[] = [
|
|
98
|
+
...(selectedEntry
|
|
99
|
+
? buildDetailBlock(width, 'Selected setting', [
|
|
100
|
+
buildPanelLine(width, [[' key ', C.label], [selectedEntry.key, C.value], [' category ', C.label], [selectedEntry.category, C.info]]),
|
|
101
|
+
buildPanelLine(width, [[' effective ', C.label], [selectedEntry.effectiveSource, selectedEntry.effectiveSource === 'managed' ? C.warn : selectedEntry.effectiveSource === 'synced' ? C.ok : selectedEntry.effectiveSource === 'local' ? C.info : C.dim], [' locked ', C.label], [selectedEntry.locked ? 'yes' : 'no', selectedEntry.locked ? C.warn : C.dim], [' conflict ', C.label], [selectedEntry.conflict ? 'yes' : 'no', selectedEntry.conflict ? C.error : C.good]]),
|
|
102
|
+
buildPanelLine(width, [[' source ', C.label], [(selectedEntry.sourceLabel ?? 'local/default').slice(0, Math.max(0, width - 10)), C.dim]]),
|
|
103
|
+
buildPanelLine(width, [[' overrides ', C.label], [(selectedEntry.overriddenSources.length > 0 ? selectedEntry.overriddenSources.join(', ') : 'none').slice(0, Math.max(0, width - 13)), C.dim]]),
|
|
104
|
+
buildPanelLine(width, [[' local ', C.label], [String(selectedEntry.localValue).slice(0, Math.max(0, width - 9)), C.dim]]),
|
|
105
|
+
buildPanelLine(width, [[' synced ', C.label], [String(selectedEntry.syncedValue ?? '(unset)').slice(0, Math.max(0, width - 10)), C.ok]]),
|
|
106
|
+
buildPanelLine(width, [[' managed ', C.label], [String(selectedEntry.managedValue ?? '(unset)').slice(0, Math.max(0, width - 11)), C.warn]]),
|
|
107
|
+
], C)
|
|
108
|
+
: []),
|
|
109
|
+
buildPanelLine(width, [[' ↑/↓ browse /settingssync show <key> /settingssync resolve <key> <local|synced> /managed apply-staged [key...] ', C.dim]]),
|
|
153
110
|
];
|
|
154
|
-
|
|
111
|
+
|
|
112
|
+
return this.renderList(width, height, {
|
|
155
113
|
title: 'Settings Sync',
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
footerLines: [buildPanelLine(width, [[' ↑/↓ browse /settingssync show <key> /settingssync resolve <key> <local|synced> /managed apply-staged [key...] ', C.dim]])],
|
|
159
|
-
palette: C,
|
|
114
|
+
header: headerLines,
|
|
115
|
+
footer: footerLines,
|
|
160
116
|
});
|
|
161
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
162
|
-
return lines.slice(0, height);
|
|
163
117
|
}
|
|
164
118
|
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { Line } from '../types/grid.ts';
|
|
4
4
|
import { createEmptyLine } from '../types/grid.ts';
|
|
5
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
5
6
|
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
7
|
import { BasePanel } from './base-panel.ts';
|
|
7
8
|
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
@@ -241,6 +242,8 @@ export class SkillsPanel extends BasePanel {
|
|
|
241
242
|
private scrollOffset = 0;
|
|
242
243
|
private cached: SkillRecord[] | null = null;
|
|
243
244
|
private cacheDirty = true;
|
|
245
|
+
// I1: confirm state for destructive delete
|
|
246
|
+
private confirm: ConfirmState | null = null;
|
|
244
247
|
|
|
245
248
|
public constructor(options: SkillsPanelOptions) {
|
|
246
249
|
super('skills', 'Skills', 'K', 'monitoring', options.componentHealthMonitor);
|
|
@@ -259,6 +262,24 @@ export class SkillsPanel extends BasePanel {
|
|
|
259
262
|
public override onDestroy(): void {}
|
|
260
263
|
|
|
261
264
|
public handleInput(key: string): boolean {
|
|
265
|
+
// I1: y/n confirmation dialog for delete
|
|
266
|
+
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
267
|
+
if (confirmResult === 'confirmed') {
|
|
268
|
+
const toDelete = this.confirm!.subject;
|
|
269
|
+
this.confirm = null;
|
|
270
|
+
// Skills are read from the filesystem — deletion requires a shell command.
|
|
271
|
+
// Surface an error directing the user to remove the file manually.
|
|
272
|
+
this.setError(`Delete via shell: rm "${toDelete}"`);
|
|
273
|
+
this.markDirty();
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
if (confirmResult === 'cancelled') {
|
|
277
|
+
this.confirm = null;
|
|
278
|
+
this.markDirty();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (confirmResult === 'absorbed') return true;
|
|
282
|
+
|
|
262
283
|
const records = this._filteredSkills();
|
|
263
284
|
if (this.filterFocused) {
|
|
264
285
|
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: records.length });
|
|
@@ -329,6 +350,15 @@ export class SkillsPanel extends BasePanel {
|
|
|
329
350
|
this.markDirty();
|
|
330
351
|
return true;
|
|
331
352
|
}
|
|
353
|
+
// I1: 'd' prompts delete confirmation
|
|
354
|
+
if (key === 'd') {
|
|
355
|
+
const skill = records[this.selectedIndex];
|
|
356
|
+
if (skill) {
|
|
357
|
+
this.confirm = { subject: skill.path, label: skill.name };
|
|
358
|
+
this.markDirty();
|
|
359
|
+
}
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
332
362
|
if (isPanelSearchBackspace(key)) {
|
|
333
363
|
if (this.query.length === 0) return false;
|
|
334
364
|
this.query = this.query.slice(0, -1);
|
|
@@ -355,6 +385,20 @@ export class SkillsPanel extends BasePanel {
|
|
|
355
385
|
|
|
356
386
|
const start = Date.now();
|
|
357
387
|
this.needsRender = false;
|
|
388
|
+
|
|
389
|
+
// I1: show confirm dialog in place of normal content
|
|
390
|
+
if (this.confirm) {
|
|
391
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
392
|
+
title: 'Skills - confirm action',
|
|
393
|
+
intro: '',
|
|
394
|
+
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
395
|
+
palette: C,
|
|
396
|
+
});
|
|
397
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
398
|
+
this.reportRenderDuration(Date.now() - start);
|
|
399
|
+
return lines.slice(0, height);
|
|
400
|
+
}
|
|
401
|
+
|
|
358
402
|
const intro = 'Discover project-local and global skill packs, filter by name or description, and inspect path, dependencies, and includes.';
|
|
359
403
|
const skills = this._filteredSkills();
|
|
360
404
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
4
|
import type { ProviderSubscription, PendingSubscriptionLogin } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
|
|
5
5
|
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
|
|
6
6
|
import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
|
|
7
7
|
import {
|
|
8
|
-
buildDetailBlock,
|
|
9
8
|
buildEmptyState,
|
|
10
9
|
buildGuidanceLine,
|
|
11
10
|
buildKeyValueLine,
|
|
@@ -14,8 +13,6 @@ import {
|
|
|
14
13
|
buildSummaryBlock,
|
|
15
14
|
buildPanelWorkspace,
|
|
16
15
|
DEFAULT_PANEL_PALETTE,
|
|
17
|
-
resolvePrimaryScrollableSection,
|
|
18
|
-
type PanelWorkspaceSection,
|
|
19
16
|
} from './polish.ts';
|
|
20
17
|
|
|
21
18
|
const C = {
|
|
@@ -56,12 +53,10 @@ function statusColor(status: ReturnType<typeof statusOf>): string {
|
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
export class SubscriptionPanel extends
|
|
56
|
+
export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
60
57
|
private readonly serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
|
|
61
58
|
private readonly subscriptionManager: SubscriptionAccessQuery;
|
|
62
59
|
private rows: SubscriptionRow[] = [];
|
|
63
|
-
private selectedIndex = 0;
|
|
64
|
-
private scrollOffset = 0;
|
|
65
60
|
private logoutConfirmationTarget: string | null = null;
|
|
66
61
|
|
|
67
62
|
public constructor(
|
|
@@ -78,6 +73,30 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
78
73
|
this.refresh();
|
|
79
74
|
}
|
|
80
75
|
|
|
76
|
+
protected override getPalette() { return C; }
|
|
77
|
+
protected override getEmptyStateMessage() { return ' No provider subscriptions are active yet.'; }
|
|
78
|
+
protected override getEmptyStateActions() {
|
|
79
|
+
return [
|
|
80
|
+
{ command: '/subscription login openai start', summary: 'start the first-class OpenAI subscription flow' },
|
|
81
|
+
{ command: '/login provider <name> start', summary: 'use the front-door auth surface for supported providers' },
|
|
82
|
+
{ command: '/services auth-review', summary: 'inspect configured service auth posture and stored secrets' },
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected getItems(): readonly SubscriptionRow[] {
|
|
87
|
+
return this.rows;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected renderItem(row: SubscriptionRow, index: number, selected: boolean, width: number): Line {
|
|
91
|
+
const status = statusOf(row);
|
|
92
|
+
return buildPanelListRow(width, [
|
|
93
|
+
{ text: row.provider.padEnd(16).slice(0, 16), fg: C.value },
|
|
94
|
+
{ text: ` ${status.toUpperCase().padEnd(12)}`, fg: statusColor(status) },
|
|
95
|
+
{ text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
|
|
96
|
+
{ text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
|
|
97
|
+
], C, { selected, selectedBg: C.selectedBg });
|
|
98
|
+
}
|
|
99
|
+
|
|
81
100
|
public handleInput(key: string): boolean {
|
|
82
101
|
if (this.rows.length === 0) return false;
|
|
83
102
|
const selected = this.rows[this.selectedIndex] ?? null;
|
|
@@ -95,7 +114,7 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
95
114
|
}
|
|
96
115
|
if (key === 'enter' || key === 'x') {
|
|
97
116
|
if (!selected?.subscription) return false;
|
|
98
|
-
if (this.logoutConfirmationTarget !== selected.provider) {
|
|
117
|
+
if (this.logoutConfirmationTarget === null || this.logoutConfirmationTarget !== selected.provider) {
|
|
99
118
|
this.logoutConfirmationTarget = selected.provider;
|
|
100
119
|
this.markDirty();
|
|
101
120
|
return true;
|
|
@@ -106,6 +125,11 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
106
125
|
this.markDirty();
|
|
107
126
|
return true;
|
|
108
127
|
}
|
|
128
|
+
if ((key === 'n' || key === 'escape') && this.logoutConfirmationTarget) {
|
|
129
|
+
this.logoutConfirmationTarget = null;
|
|
130
|
+
this.markDirty();
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
109
133
|
if (key === 'r') {
|
|
110
134
|
this.refresh();
|
|
111
135
|
this.logoutConfirmationTarget = null;
|
|
@@ -145,8 +169,9 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
145
169
|
}
|
|
146
170
|
|
|
147
171
|
public render(width: number, height: number): Line[] {
|
|
148
|
-
this.needsRender = false;
|
|
149
172
|
this.refresh();
|
|
173
|
+
this.clampSelection();
|
|
174
|
+
const intro = 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.';
|
|
150
175
|
|
|
151
176
|
const activeCount = this.rows.filter((row) => row.subscription).length;
|
|
152
177
|
const pendingCount = this.rows.filter((row) => row.pending).length;
|
|
@@ -163,111 +188,75 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
163
188
|
], C),
|
|
164
189
|
buildGuidanceLine(width, '/subscription login <provider> start', 'start or repair browser login for the selected provider route', C),
|
|
165
190
|
];
|
|
166
|
-
const footerLines = [
|
|
167
|
-
buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
|
|
168
|
-
buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
|
|
169
|
-
] as const;
|
|
170
191
|
|
|
192
|
+
// Empty state: render posture + base empty state
|
|
171
193
|
if (this.rows.length === 0) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
lines.push(...buildEmptyState(
|
|
194
|
+
const summaryLines = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
|
|
195
|
+
const emptyLines = buildEmptyState(
|
|
175
196
|
width,
|
|
176
|
-
|
|
197
|
+
this.getEmptyStateMessage(),
|
|
177
198
|
'Built-in OAuth-capable providers and configured service providers will appear here once available for browser login or session import.',
|
|
178
|
-
|
|
179
|
-
{ command: '/subscription login openai start', summary: 'start the first-class OpenAI subscription flow' },
|
|
180
|
-
{ command: '/login provider <name> start', summary: 'use the front-door auth surface for supported providers' },
|
|
181
|
-
{ command: '/services auth-review', summary: 'inspect configured service auth posture and stored secrets' },
|
|
182
|
-
],
|
|
199
|
+
this.getEmptyStateActions(),
|
|
183
200
|
C,
|
|
184
|
-
)
|
|
201
|
+
);
|
|
185
202
|
const workspace = buildPanelWorkspace(width, height, {
|
|
186
203
|
title: 'Provider Subscriptions',
|
|
187
|
-
intro
|
|
188
|
-
sections: [{ lines
|
|
189
|
-
footerLines
|
|
204
|
+
intro,
|
|
205
|
+
sections: [{ lines: [...summaryLines, ...emptyLines] }],
|
|
206
|
+
footerLines: [
|
|
207
|
+
buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
|
|
208
|
+
buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
|
|
209
|
+
],
|
|
190
210
|
palette: C,
|
|
191
211
|
});
|
|
192
212
|
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
193
213
|
return workspace.slice(0, height);
|
|
194
214
|
}
|
|
195
215
|
|
|
196
|
-
const
|
|
216
|
+
const selectedRow = this.rows[this.selectedIndex];
|
|
197
217
|
const detailRows: Line[] = [];
|
|
198
|
-
if (
|
|
218
|
+
if (selectedRow) {
|
|
199
219
|
detailRows.push(buildKeyValueLine(width, [
|
|
200
|
-
{ label: 'provider', value:
|
|
201
|
-
{ label: 'status', value: statusOf(
|
|
202
|
-
{ label: 'oauth config', value:
|
|
220
|
+
{ label: 'provider', value: selectedRow.provider, valueColor: C.value },
|
|
221
|
+
{ label: 'status', value: statusOf(selectedRow), valueColor: statusColor(statusOf(selectedRow)) },
|
|
222
|
+
{ label: 'oauth config', value: selectedRow.hasOAuthConfig ? 'present' : 'missing', valueColor: selectedRow.hasOAuthConfig ? C.good : C.bad },
|
|
203
223
|
], C));
|
|
204
|
-
if (
|
|
205
|
-
const expires =
|
|
206
|
-
? new Date(
|
|
224
|
+
if (selectedRow.subscription) {
|
|
225
|
+
const expires = selectedRow.subscription.expiresAt
|
|
226
|
+
? new Date(selectedRow.subscription.expiresAt).toISOString()
|
|
207
227
|
: 'n/a';
|
|
208
228
|
detailRows.push(buildKeyValueLine(width, [
|
|
209
|
-
{ label: 'token type', value:
|
|
229
|
+
{ label: 'token type', value: selectedRow.subscription.tokenType, valueColor: C.info },
|
|
210
230
|
{ label: 'expires', value: expires, valueColor: C.dim },
|
|
211
231
|
], C));
|
|
212
232
|
detailRows.push(buildPanelLine(width, [[
|
|
213
|
-
` ${
|
|
233
|
+
` ${selectedRow.subscription.overrideAmbientApiKeys
|
|
214
234
|
? 'Provider subscription overrides ambient API-key resolution for this provider.'
|
|
215
235
|
: 'Stored for subscription-backed flows. Ambient API-key resolution remains unchanged.'}`,
|
|
216
236
|
C.dim,
|
|
217
237
|
]]));
|
|
218
|
-
if (this.logoutConfirmationTarget ===
|
|
219
|
-
detailRows.push(buildPanelLine(width, [[` Press Enter or X again to sign out ${
|
|
238
|
+
if (this.logoutConfirmationTarget === selectedRow.provider) {
|
|
239
|
+
detailRows.push(buildPanelLine(width, [[` Press Enter or X again to sign out ${selectedRow.provider}.`, C.warn]]));
|
|
220
240
|
}
|
|
221
|
-
} else if (
|
|
241
|
+
} else if (selectedRow.pending) {
|
|
222
242
|
detailRows.push(buildPanelLine(width, [[' Login is pending. Finish with /subscription login <provider> finish <code>.', C.warn]]));
|
|
223
|
-
} else if (
|
|
243
|
+
} else if (selectedRow.hasOAuthConfig) {
|
|
224
244
|
detailRows.push(buildPanelLine(width, [[' Ready for login. Start with /subscription login <provider> start.', C.dim]]));
|
|
225
245
|
} else {
|
|
226
246
|
detailRows.push(buildPanelLine(width, [[' Add a provider-specific OAuth config or enable a built-in subscription provider to use subscription login.', C.bad]]));
|
|
227
247
|
}
|
|
228
248
|
}
|
|
229
|
-
const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Subscription posture', postureLines, C) };
|
|
230
|
-
const detailSection: PanelWorkspaceSection = { lines: buildDetailBlock(width, 'Selected provider', detailRows, C) };
|
|
231
|
-
const rawProviderLines: Line[] = this.rows.map((row, absolute) => {
|
|
232
|
-
const status = statusOf(row);
|
|
233
|
-
return buildPanelListRow(width, [
|
|
234
|
-
{ text: row.provider.padEnd(16).slice(0, 16), fg: C.value },
|
|
235
|
-
{ text: ` ${status.toUpperCase().padEnd(12)}`, fg: statusColor(status) },
|
|
236
|
-
{ text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
|
|
237
|
-
{ text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
|
|
238
|
-
], C, { selected: absolute === this.selectedIndex, selectedBg: C.selectedBg });
|
|
239
|
-
});
|
|
240
|
-
const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
|
|
241
|
-
intro: 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.',
|
|
242
|
-
footerLines,
|
|
243
|
-
palette: C,
|
|
244
|
-
beforeSections: [postureSection],
|
|
245
|
-
section: {
|
|
246
|
-
title: 'Providers',
|
|
247
|
-
scrollableLines: rawProviderLines,
|
|
248
|
-
selectedIndex: this.selectedIndex,
|
|
249
|
-
scrollOffset: this.scrollOffset,
|
|
250
|
-
guardRows: 1,
|
|
251
|
-
minRows: 4,
|
|
252
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
253
|
-
},
|
|
254
|
-
afterSections: [detailSection],
|
|
255
|
-
});
|
|
256
|
-
this.scrollOffset = resolvedProvidersSection.scrollOffset;
|
|
257
249
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
detailSection,
|
|
262
|
-
];
|
|
263
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
250
|
+
const headerLines: Line[] = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
|
|
251
|
+
|
|
252
|
+
return this.renderList(width, height, {
|
|
264
253
|
title: 'Provider Subscriptions',
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
254
|
+
header: headerLines,
|
|
255
|
+
footer: [
|
|
256
|
+
...detailRows,
|
|
257
|
+
buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
|
|
258
|
+
buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
|
|
259
|
+
],
|
|
269
260
|
});
|
|
270
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
271
|
-
return lines.slice(0, height);
|
|
272
261
|
}
|
|
273
262
|
}
|