@pellux/goodvibes-tui 0.18.20 → 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 +120 -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.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 -82
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/local-auth-panel.ts +76 -93
- 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 +45 -14
- 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/subscription-panel.ts +63 -86
- 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/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
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import {
|
|
4
|
-
import { handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
4
|
import type { ProviderSubscription, PendingSubscriptionLogin } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
|
|
6
5
|
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
|
|
7
6
|
import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
|
|
8
7
|
import {
|
|
9
|
-
buildDetailBlock,
|
|
10
8
|
buildEmptyState,
|
|
11
9
|
buildGuidanceLine,
|
|
12
10
|
buildKeyValueLine,
|
|
@@ -15,8 +13,6 @@ import {
|
|
|
15
13
|
buildSummaryBlock,
|
|
16
14
|
buildPanelWorkspace,
|
|
17
15
|
DEFAULT_PANEL_PALETTE,
|
|
18
|
-
resolvePrimaryScrollableSection,
|
|
19
|
-
type PanelWorkspaceSection,
|
|
20
16
|
} from './polish.ts';
|
|
21
17
|
|
|
22
18
|
const C = {
|
|
@@ -57,12 +53,10 @@ function statusColor(status: ReturnType<typeof statusOf>): string {
|
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
export class SubscriptionPanel extends
|
|
56
|
+
export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
61
57
|
private readonly serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
|
|
62
58
|
private readonly subscriptionManager: SubscriptionAccessQuery;
|
|
63
59
|
private rows: SubscriptionRow[] = [];
|
|
64
|
-
private selectedIndex = 0;
|
|
65
|
-
private scrollOffset = 0;
|
|
66
60
|
private logoutConfirmationTarget: string | null = null;
|
|
67
61
|
|
|
68
62
|
public constructor(
|
|
@@ -79,6 +73,30 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
79
73
|
this.refresh();
|
|
80
74
|
}
|
|
81
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
|
+
|
|
82
100
|
public handleInput(key: string): boolean {
|
|
83
101
|
if (this.rows.length === 0) return false;
|
|
84
102
|
const selected = this.rows[this.selectedIndex] ?? null;
|
|
@@ -96,11 +114,6 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
96
114
|
}
|
|
97
115
|
if (key === 'enter' || key === 'x') {
|
|
98
116
|
if (!selected?.subscription) return false;
|
|
99
|
-
// I1: use confirm helper — first press sets target, second (y) executes
|
|
100
|
-
const confirmResult = handleConfirmInput(
|
|
101
|
-
this.logoutConfirmationTarget ? { subject: this.logoutConfirmationTarget, label: this.logoutConfirmationTarget } : null,
|
|
102
|
-
key,
|
|
103
|
-
);
|
|
104
117
|
if (this.logoutConfirmationTarget === null || this.logoutConfirmationTarget !== selected.provider) {
|
|
105
118
|
this.logoutConfirmationTarget = selected.provider;
|
|
106
119
|
this.markDirty();
|
|
@@ -112,7 +125,6 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
112
125
|
this.markDirty();
|
|
113
126
|
return true;
|
|
114
127
|
}
|
|
115
|
-
// Allow n/Esc to cancel pending logout confirm
|
|
116
128
|
if ((key === 'n' || key === 'escape') && this.logoutConfirmationTarget) {
|
|
117
129
|
this.logoutConfirmationTarget = null;
|
|
118
130
|
this.markDirty();
|
|
@@ -157,8 +169,9 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
157
169
|
}
|
|
158
170
|
|
|
159
171
|
public render(width: number, height: number): Line[] {
|
|
160
|
-
this.needsRender = false;
|
|
161
172
|
this.refresh();
|
|
173
|
+
this.clampSelection();
|
|
174
|
+
const intro = 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.';
|
|
162
175
|
|
|
163
176
|
const activeCount = this.rows.filter((row) => row.subscription).length;
|
|
164
177
|
const pendingCount = this.rows.filter((row) => row.pending).length;
|
|
@@ -175,111 +188,75 @@ export class SubscriptionPanel extends BasePanel {
|
|
|
175
188
|
], C),
|
|
176
189
|
buildGuidanceLine(width, '/subscription login <provider> start', 'start or repair browser login for the selected provider route', C),
|
|
177
190
|
];
|
|
178
|
-
const footerLines = [
|
|
179
|
-
buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
|
|
180
|
-
buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
|
|
181
|
-
] as const;
|
|
182
191
|
|
|
192
|
+
// Empty state: render posture + base empty state
|
|
183
193
|
if (this.rows.length === 0) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
lines.push(...buildEmptyState(
|
|
194
|
+
const summaryLines = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
|
|
195
|
+
const emptyLines = buildEmptyState(
|
|
187
196
|
width,
|
|
188
|
-
|
|
197
|
+
this.getEmptyStateMessage(),
|
|
189
198
|
'Built-in OAuth-capable providers and configured service providers will appear here once available for browser login or session import.',
|
|
190
|
-
|
|
191
|
-
{ command: '/subscription login openai start', summary: 'start the first-class OpenAI subscription flow' },
|
|
192
|
-
{ command: '/login provider <name> start', summary: 'use the front-door auth surface for supported providers' },
|
|
193
|
-
{ command: '/services auth-review', summary: 'inspect configured service auth posture and stored secrets' },
|
|
194
|
-
],
|
|
199
|
+
this.getEmptyStateActions(),
|
|
195
200
|
C,
|
|
196
|
-
)
|
|
201
|
+
);
|
|
197
202
|
const workspace = buildPanelWorkspace(width, height, {
|
|
198
203
|
title: 'Provider Subscriptions',
|
|
199
|
-
intro
|
|
200
|
-
sections: [{ lines
|
|
201
|
-
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
|
+
],
|
|
202
210
|
palette: C,
|
|
203
211
|
});
|
|
204
212
|
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
205
213
|
return workspace.slice(0, height);
|
|
206
214
|
}
|
|
207
215
|
|
|
208
|
-
const
|
|
216
|
+
const selectedRow = this.rows[this.selectedIndex];
|
|
209
217
|
const detailRows: Line[] = [];
|
|
210
|
-
if (
|
|
218
|
+
if (selectedRow) {
|
|
211
219
|
detailRows.push(buildKeyValueLine(width, [
|
|
212
|
-
{ label: 'provider', value:
|
|
213
|
-
{ label: 'status', value: statusOf(
|
|
214
|
-
{ 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 },
|
|
215
223
|
], C));
|
|
216
|
-
if (
|
|
217
|
-
const expires =
|
|
218
|
-
? new Date(
|
|
224
|
+
if (selectedRow.subscription) {
|
|
225
|
+
const expires = selectedRow.subscription.expiresAt
|
|
226
|
+
? new Date(selectedRow.subscription.expiresAt).toISOString()
|
|
219
227
|
: 'n/a';
|
|
220
228
|
detailRows.push(buildKeyValueLine(width, [
|
|
221
|
-
{ label: 'token type', value:
|
|
229
|
+
{ label: 'token type', value: selectedRow.subscription.tokenType, valueColor: C.info },
|
|
222
230
|
{ label: 'expires', value: expires, valueColor: C.dim },
|
|
223
231
|
], C));
|
|
224
232
|
detailRows.push(buildPanelLine(width, [[
|
|
225
|
-
` ${
|
|
233
|
+
` ${selectedRow.subscription.overrideAmbientApiKeys
|
|
226
234
|
? 'Provider subscription overrides ambient API-key resolution for this provider.'
|
|
227
235
|
: 'Stored for subscription-backed flows. Ambient API-key resolution remains unchanged.'}`,
|
|
228
236
|
C.dim,
|
|
229
237
|
]]));
|
|
230
|
-
if (this.logoutConfirmationTarget ===
|
|
231
|
-
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]]));
|
|
232
240
|
}
|
|
233
|
-
} else if (
|
|
241
|
+
} else if (selectedRow.pending) {
|
|
234
242
|
detailRows.push(buildPanelLine(width, [[' Login is pending. Finish with /subscription login <provider> finish <code>.', C.warn]]));
|
|
235
|
-
} else if (
|
|
243
|
+
} else if (selectedRow.hasOAuthConfig) {
|
|
236
244
|
detailRows.push(buildPanelLine(width, [[' Ready for login. Start with /subscription login <provider> start.', C.dim]]));
|
|
237
245
|
} else {
|
|
238
246
|
detailRows.push(buildPanelLine(width, [[' Add a provider-specific OAuth config or enable a built-in subscription provider to use subscription login.', C.bad]]));
|
|
239
247
|
}
|
|
240
248
|
}
|
|
241
|
-
const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Subscription posture', postureLines, C) };
|
|
242
|
-
const detailSection: PanelWorkspaceSection = { lines: buildDetailBlock(width, 'Selected provider', detailRows, C) };
|
|
243
|
-
const rawProviderLines: Line[] = this.rows.map((row, absolute) => {
|
|
244
|
-
const status = statusOf(row);
|
|
245
|
-
return buildPanelListRow(width, [
|
|
246
|
-
{ text: row.provider.padEnd(16).slice(0, 16), fg: C.value },
|
|
247
|
-
{ text: ` ${status.toUpperCase().padEnd(12)}`, fg: statusColor(status) },
|
|
248
|
-
{ text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
|
|
249
|
-
{ text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
|
|
250
|
-
], C, { selected: absolute === this.selectedIndex, selectedBg: C.selectedBg });
|
|
251
|
-
});
|
|
252
|
-
const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
|
|
253
|
-
intro: 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.',
|
|
254
|
-
footerLines,
|
|
255
|
-
palette: C,
|
|
256
|
-
beforeSections: [postureSection],
|
|
257
|
-
section: {
|
|
258
|
-
title: 'Providers',
|
|
259
|
-
scrollableLines: rawProviderLines,
|
|
260
|
-
selectedIndex: this.selectedIndex,
|
|
261
|
-
scrollOffset: this.scrollOffset,
|
|
262
|
-
guardRows: 1,
|
|
263
|
-
minRows: 4,
|
|
264
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
265
|
-
},
|
|
266
|
-
afterSections: [detailSection],
|
|
267
|
-
});
|
|
268
|
-
this.scrollOffset = resolvedProvidersSection.scrollOffset;
|
|
269
249
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
detailSection,
|
|
274
|
-
];
|
|
275
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
250
|
+
const headerLines: Line[] = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
|
|
251
|
+
|
|
252
|
+
return this.renderList(width, height, {
|
|
276
253
|
title: 'Provider Subscriptions',
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
],
|
|
281
260
|
});
|
|
282
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
283
|
-
return lines.slice(0, height);
|
|
284
261
|
}
|
|
285
262
|
}
|