@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.
- package/CHANGELOG.md +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,42 +1,40 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
4
6
|
import {
|
|
5
7
|
type ServiceConfig,
|
|
6
8
|
type ServiceInspection,
|
|
7
9
|
type ServiceConnectionTestResult,
|
|
8
10
|
} from '@pellux/goodvibes-sdk/platform/config';
|
|
11
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
12
|
import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
|
|
10
13
|
import {
|
|
11
|
-
|
|
14
|
+
buildDetailBlock,
|
|
15
|
+
buildKeyValueLine,
|
|
16
|
+
buildKeyboardHints,
|
|
12
17
|
buildPanelLine,
|
|
13
18
|
buildPanelWorkspace,
|
|
14
19
|
buildStatusPill,
|
|
15
20
|
DEFAULT_PANEL_PALETTE,
|
|
16
21
|
} from './polish.ts';
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
headerBg: '#1e293b',
|
|
22
|
-
label: '#64748b',
|
|
23
|
-
value: '#e2e8f0',
|
|
24
|
-
dim: '#475569',
|
|
25
|
-
ok: '#22c55e',
|
|
26
|
-
warn: '#eab308',
|
|
27
|
-
error: '#ef4444',
|
|
28
|
-
info: '#38bdf8',
|
|
29
|
-
selectBg: '#0f172a',
|
|
30
|
-
empty: '#334155',
|
|
31
|
-
} as const;
|
|
23
|
+
// Base chrome only — title band, state colors, and text tokens all come
|
|
24
|
+
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
25
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
32
26
|
|
|
33
27
|
interface ServicePanelEntry {
|
|
34
28
|
readonly name: string;
|
|
35
|
-
|
|
29
|
+
// null when registry.inspect(name) resolved null (e.g. a config entry that
|
|
30
|
+
// disappeared or failed to resolve between getAll() and inspect()) — the
|
|
31
|
+
// row still renders, flagged, instead of crashing the whole refresh.
|
|
32
|
+
readonly inspection: ServiceInspection | null;
|
|
36
33
|
readonly lastTest?: ServiceConnectionTestResult;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
function statusLabel(entry: ServicePanelEntry): string {
|
|
37
|
+
if (!entry.inspection) return 'INSPECT FAILED';
|
|
40
38
|
if (!entry.inspection.hasPrimaryCredential) return 'UNCONFIGURED';
|
|
41
39
|
if (entry.lastTest?.ok) return 'HEALTHY';
|
|
42
40
|
if (entry.lastTest && !entry.lastTest.ok) return 'ERROR';
|
|
@@ -45,8 +43,8 @@ function statusLabel(entry: ServicePanelEntry): string {
|
|
|
45
43
|
|
|
46
44
|
function statusColor(entry: ServicePanelEntry): string {
|
|
47
45
|
const label = statusLabel(entry);
|
|
48
|
-
if (label === 'HEALTHY') return C.
|
|
49
|
-
if (label === 'ERROR') return C.
|
|
46
|
+
if (label === 'HEALTHY') return C.good;
|
|
47
|
+
if (label === 'ERROR' || label === 'INSPECT FAILED') return C.bad;
|
|
50
48
|
if (label === 'CONFIGURED') return C.warn;
|
|
51
49
|
return C.dim;
|
|
52
50
|
}
|
|
@@ -73,18 +71,29 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
73
71
|
private readonly subscriptionManager: SubscriptionAccessQuery;
|
|
74
72
|
private entries: ServicePanelEntry[] = [];
|
|
75
73
|
private loading = false;
|
|
74
|
+
// s = open the Subscription panel via the handleInput -> handlePanelIntegrationAction
|
|
75
|
+
// bridge (handleInput has no ctx.panelManager — same pattern used elsewhere in
|
|
76
|
+
// this wave, e.g. session-browser-panel.ts's pendingOpenPanels).
|
|
77
|
+
private pendingOpenSubscription = false;
|
|
76
78
|
|
|
77
79
|
public constructor(
|
|
78
80
|
registry: ServiceInspectionQuery,
|
|
79
81
|
subscriptionManager: SubscriptionAccessQuery,
|
|
80
82
|
) {
|
|
81
|
-
super('services', 'Services', 'V', '
|
|
83
|
+
super('services', 'Services', 'V', 'providers');
|
|
82
84
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
85
|
+
this.filterEnabled = true;
|
|
86
|
+
this.filterLabel = 'Filter services';
|
|
83
87
|
this.registry = registry;
|
|
84
88
|
this.subscriptionManager = subscriptionManager;
|
|
85
89
|
void this.refresh();
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
protected override filterMatches(entry: ServicePanelEntry, q: string): boolean {
|
|
93
|
+
return entry.name.toLowerCase().includes(q)
|
|
94
|
+
|| (entry.inspection?.config.baseUrl ?? '').toLowerCase().includes(q);
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
public override onActivate(): void {
|
|
89
98
|
super.onActivate();
|
|
90
99
|
if (this.entries.length === 0 && !this.loading) {
|
|
@@ -97,7 +106,7 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
97
106
|
protected override getEmptyStateActions() {
|
|
98
107
|
return [
|
|
99
108
|
{ command: '/services auth-review', summary: 'inspect service auth posture and registry config' },
|
|
100
|
-
{ command: '
|
|
109
|
+
{ command: 's', summary: 'open the Subscription panel — review provider login state and override posture' },
|
|
101
110
|
];
|
|
102
111
|
}
|
|
103
112
|
|
|
@@ -107,60 +116,106 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
107
116
|
|
|
108
117
|
protected renderItem(entry: ServicePanelEntry, index: number, selected: boolean, width: number): Line {
|
|
109
118
|
const bg = selected ? C.selectBg : undefined;
|
|
119
|
+
const authText = entry.inspection ? authSummary(entry.inspection.config, this.subscriptionManager) : 'n/a';
|
|
120
|
+
const baseUrlText = entry.inspection ? (entry.inspection.config.baseUrl ?? '(no baseUrl)') : '(inspection failed)';
|
|
110
121
|
return buildPanelLine(width, [
|
|
111
122
|
[' ', C.label, bg],
|
|
112
123
|
[entry.name.padEnd(16), C.value, bg],
|
|
113
124
|
[` ${statusLabel(entry).padEnd(12)}`, statusColor(entry), bg],
|
|
114
|
-
[` ${
|
|
115
|
-
[` ${
|
|
125
|
+
[` ${authText.padEnd(18)}`, C.info, bg],
|
|
126
|
+
[` ${truncateDisplay(baseUrlText, Math.max(0, width - 48))}`, C.dim, bg],
|
|
116
127
|
]);
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
public handleInput(key: string): boolean {
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
if (!this.filterActive) {
|
|
132
|
+
if (key === 'r') {
|
|
133
|
+
void this.refresh();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (key === 't') {
|
|
137
|
+
void this.testSelected();
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (key === 'T') {
|
|
141
|
+
void this.testAll();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (key === 's') {
|
|
145
|
+
this.pendingOpenSubscription = true;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
127
148
|
}
|
|
128
149
|
return super.handleInput(key);
|
|
129
150
|
}
|
|
130
151
|
|
|
152
|
+
/** Drains the 's' key's pending open request via the ctx.panelManager bridge. */
|
|
153
|
+
handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
154
|
+
if (!this.pendingOpenSubscription) return false;
|
|
155
|
+
this.pendingOpenSubscription = false;
|
|
156
|
+
ctx.panelManager.open('subscription');
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
131
160
|
private async refresh(): Promise<void> {
|
|
132
161
|
this.loading = true;
|
|
133
162
|
this.markDirty();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
name
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
try {
|
|
164
|
+
const configs = this.registry.getAll();
|
|
165
|
+
const names = Object.keys(configs).sort((a, b) => a.localeCompare(b));
|
|
166
|
+
const inspections = await Promise.all(
|
|
167
|
+
names.map(async (name) => ({
|
|
168
|
+
name,
|
|
169
|
+
inspection: await this.registry.inspect(name),
|
|
170
|
+
})),
|
|
171
|
+
);
|
|
172
|
+
const previousTests = new Map(this.entries.map((entry) => [entry.name, entry.lastTest] as const));
|
|
173
|
+
this.entries = inspections.map((entry) => ({
|
|
174
|
+
...entry,
|
|
175
|
+
...(previousTests.get(entry.name) ? { lastTest: previousTests.get(entry.name) } : {}),
|
|
176
|
+
}));
|
|
177
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.entries.length - 1));
|
|
178
|
+
} catch (e) {
|
|
179
|
+
this.setError(`Refresh failed: ${summarizeError(e)}`);
|
|
180
|
+
} finally {
|
|
181
|
+
// Always clear the loading flag — a thrown getAll()/inspect() must
|
|
182
|
+
// never leave the panel stuck on the "Loading configured services..." screen.
|
|
183
|
+
this.loading = false;
|
|
184
|
+
this.markDirty();
|
|
185
|
+
}
|
|
150
186
|
}
|
|
151
187
|
|
|
152
188
|
private async testSelected(): Promise<void> {
|
|
153
|
-
|
|
189
|
+
// selectedIndex tracks the filtered view, so the acted-on entry must come
|
|
190
|
+
// from getVisibleItems() — this.entries would desync under a '/' filter.
|
|
191
|
+
const selected = this.getSelectedItem();
|
|
154
192
|
if (!selected) return;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
193
|
+
try {
|
|
194
|
+
const result = await this.registry.testConnection(selected.name);
|
|
195
|
+
this.entries = this.entries.map((entry) => (
|
|
196
|
+
entry.name === selected.name
|
|
197
|
+
? { ...entry, lastTest: result }
|
|
198
|
+
: entry
|
|
199
|
+
));
|
|
200
|
+
} catch (e) {
|
|
201
|
+
this.setError(`Test failed: ${summarizeError(e)}`);
|
|
202
|
+
}
|
|
161
203
|
this.markDirty();
|
|
162
204
|
}
|
|
163
205
|
|
|
206
|
+
/** T = test every configured service's connection, one call per row (services-runtime.ts's single-service test pattern, looped). */
|
|
207
|
+
private async testAll(): Promise<void> {
|
|
208
|
+
for (const entry of this.entries) {
|
|
209
|
+
try {
|
|
210
|
+
const result = await this.registry.testConnection(entry.name);
|
|
211
|
+
this.entries = this.entries.map((e) => (e.name === entry.name ? { ...e, lastTest: result } : e));
|
|
212
|
+
} catch (e) {
|
|
213
|
+
this.setError(`Test failed for ${entry.name}: ${summarizeError(e)}`);
|
|
214
|
+
}
|
|
215
|
+
this.markDirty();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
164
219
|
public render(width: number, height: number): Line[] {
|
|
165
220
|
this.clampSelection();
|
|
166
221
|
const intro = 'Credential posture, subscription overrides, and live connection checks for configured services.';
|
|
@@ -176,55 +231,98 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
176
231
|
return lines;
|
|
177
232
|
}
|
|
178
233
|
|
|
179
|
-
|
|
180
|
-
const
|
|
234
|
+
// Health-summary header — surface the most important counts first.
|
|
235
|
+
const counts = { healthy: 0, error: 0, configured: 0, unconfigured: 0 };
|
|
236
|
+
for (const entry of this.entries) {
|
|
237
|
+
const label = statusLabel(entry);
|
|
238
|
+
if (label === 'HEALTHY') counts.healthy++;
|
|
239
|
+
else if (label === 'ERROR') counts.error++;
|
|
240
|
+
else if (label === 'CONFIGURED') counts.configured++;
|
|
241
|
+
else counts.unconfigured++;
|
|
242
|
+
}
|
|
243
|
+
const headerLines: Line[] = [
|
|
244
|
+
buildKeyValueLine(width, [
|
|
245
|
+
{ label: 'services', value: String(this.entries.length), valueColor: this.entries.length > 0 ? C.info : C.dim },
|
|
246
|
+
{ label: 'healthy', value: String(counts.healthy), valueColor: counts.healthy > 0 ? C.good : C.dim },
|
|
247
|
+
{ label: 'errors', value: String(counts.error), valueColor: counts.error > 0 ? C.bad : C.dim },
|
|
248
|
+
{ label: 'unconfigured', value: String(counts.unconfigured), valueColor: counts.unconfigured > 0 ? C.warn : C.dim },
|
|
249
|
+
], C),
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
// Detail must describe the row the (possibly filtered) list highlights.
|
|
253
|
+
const selected = this.getSelectedItem();
|
|
254
|
+
const detailRows: Line[] = [];
|
|
181
255
|
if (selected) {
|
|
182
256
|
const inspect = selected.inspection;
|
|
183
|
-
|
|
257
|
+
detailRows.push(buildPanelLine(width, [
|
|
184
258
|
[' Service: ', C.label],
|
|
185
259
|
[selected.name, C.value],
|
|
186
260
|
[' State: ', C.label],
|
|
187
261
|
[statusLabel(selected), statusColor(selected)],
|
|
188
262
|
[' Auth: ', C.label],
|
|
189
|
-
[authSummary(inspect.config, this.subscriptionManager), C.info],
|
|
190
|
-
]));
|
|
191
|
-
detailLines.push(buildPanelLine(width, [
|
|
192
|
-
[' Primary credential: ', C.label],
|
|
193
|
-
...buildStatusPill(inspect.hasPrimaryCredential ? 'good' : 'bad', inspect.hasPrimaryCredential ? 'present' : 'missing'),
|
|
194
|
-
[' Webhook URL: ', C.label],
|
|
195
|
-
...buildStatusPill(inspect.hasWebhookUrl ? 'good' : 'info', inspect.hasWebhookUrl ? 'present' : 'missing'),
|
|
196
|
-
[' Signing secret: ', C.label],
|
|
197
|
-
...buildStatusPill(inspect.hasSigningSecret ? 'good' : 'info', inspect.hasSigningSecret ? 'present' : 'missing'),
|
|
198
|
-
[' App token: ', C.label],
|
|
199
|
-
...buildStatusPill(inspect.hasAppToken ? 'good' : 'info', inspect.hasAppToken ? 'present' : 'missing'),
|
|
263
|
+
[inspect ? authSummary(inspect.config, this.subscriptionManager) : 'n/a', C.info],
|
|
200
264
|
]));
|
|
265
|
+
if (!inspect) {
|
|
266
|
+
detailRows.push(buildPanelLine(width, [
|
|
267
|
+
[' Inspection failed — the registry could not resolve this service\'s config. Press r to retry.', C.bad],
|
|
268
|
+
]));
|
|
269
|
+
} else {
|
|
270
|
+
detailRows.push(buildPanelLine(width, [
|
|
271
|
+
[' Base URL: ', C.label],
|
|
272
|
+
[truncateDisplay(inspect.config.baseUrl ?? '(no baseUrl)', Math.max(0, width - 13)), C.dim],
|
|
273
|
+
]));
|
|
274
|
+
detailRows.push(buildPanelLine(width, [
|
|
275
|
+
[' Primary credential: ', C.label],
|
|
276
|
+
...buildStatusPill(inspect.hasPrimaryCredential ? 'good' : 'bad', inspect.hasPrimaryCredential ? 'present' : 'missing'),
|
|
277
|
+
[' Webhook URL: ', C.label],
|
|
278
|
+
...buildStatusPill(inspect.hasWebhookUrl ? 'good' : 'info', inspect.hasWebhookUrl ? 'present' : 'missing'),
|
|
279
|
+
[' Signing secret: ', C.label],
|
|
280
|
+
...buildStatusPill(inspect.hasSigningSecret ? 'good' : 'info', inspect.hasSigningSecret ? 'present' : 'missing'),
|
|
281
|
+
[' App token: ', C.label],
|
|
282
|
+
...buildStatusPill(inspect.hasAppToken ? 'good' : 'info', inspect.hasAppToken ? 'present' : 'missing'),
|
|
283
|
+
]));
|
|
284
|
+
}
|
|
201
285
|
if (selected.lastTest) {
|
|
202
|
-
|
|
286
|
+
detailRows.push(buildPanelLine(width, [
|
|
203
287
|
[' Last test: ', C.label],
|
|
204
288
|
...buildStatusPill(selected.lastTest.ok ? 'good' : 'bad', selected.lastTest.ok ? 'ok' : 'failed'),
|
|
205
289
|
[' Status: ', C.label],
|
|
206
290
|
[selected.lastTest.status != null ? String(selected.lastTest.status) : 'n/a', C.value],
|
|
207
291
|
[' URL: ', C.label],
|
|
208
|
-
[(selected.lastTest.testedUrl ?? 'n/a'
|
|
292
|
+
[truncateDisplay(selected.lastTest.testedUrl ?? 'n/a', Math.max(0, width - 34)), C.dim],
|
|
209
293
|
]));
|
|
210
294
|
if (selected.lastTest.error) {
|
|
211
|
-
|
|
295
|
+
detailRows.push(buildPanelLine(width, [
|
|
212
296
|
[' Error: ', C.label],
|
|
213
|
-
[selected.lastTest.error
|
|
297
|
+
[truncateDisplay(selected.lastTest.error, Math.max(0, width - 10)), C.bad],
|
|
214
298
|
]));
|
|
215
299
|
}
|
|
216
300
|
} else {
|
|
217
|
-
|
|
301
|
+
detailRows.push(buildPanelLine(width, [[' Not tested yet — press t to run a live connection check.', C.dim]]));
|
|
218
302
|
}
|
|
219
|
-
detailLines.push(buildPanelLine(width, [[' Services resolve credentials through hierarchy-aware secure storage, plaintext fallback policy, and project-local config.', C.dim]]));
|
|
220
303
|
}
|
|
221
304
|
|
|
305
|
+
// Context-aware hints: filter mode swaps to filter-specific keys; the test
|
|
306
|
+
// key only makes sense when there is a service to test.
|
|
307
|
+
const hints = this.filterActive
|
|
308
|
+
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
309
|
+
: [
|
|
310
|
+
{ keys: 'Up/Down', label: 'move' },
|
|
311
|
+
...(selected ? [{ keys: 't', label: 'test selected' }] : []),
|
|
312
|
+
...(this.entries.length > 0 ? [{ keys: 'T', label: 'test all' }] : []),
|
|
313
|
+
{ keys: 'r', label: 'refresh' },
|
|
314
|
+
{ keys: '/', label: 'filter' },
|
|
315
|
+
{ keys: 's', label: 'subscription panel' },
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const footer: Line[] = selected
|
|
319
|
+
? [...buildDetailBlock(width, `Service · ${selected.name}`, detailRows, C), buildKeyboardHints(width, hints, C)]
|
|
320
|
+
: [buildKeyboardHints(width, hints, C)];
|
|
321
|
+
|
|
222
322
|
return this.renderList(width, height, {
|
|
223
323
|
title: 'Service Control Room',
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
buildPanelLine(width, [[' Up/Down move t test selected service r refresh inspections', C.dim]]),
|
|
227
|
-
],
|
|
324
|
+
header: headerLines,
|
|
325
|
+
footer,
|
|
228
326
|
emptyMessage: intro,
|
|
229
327
|
});
|
|
230
328
|
}
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Line } from '../types/grid.ts';
|
|
6
6
|
import { BasePanel } from './base-panel.ts';
|
|
7
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
7
8
|
import type { SessionInfo } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
8
9
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
10
|
import type { SessionBrowserQuery } from '../runtime/ui-service-queries.ts';
|
|
10
11
|
import {
|
|
11
12
|
buildEmptyState,
|
|
13
|
+
buildKeyboardHints,
|
|
12
14
|
buildPanelLine,
|
|
13
15
|
buildSearchInputLine,
|
|
14
16
|
buildStyledPanelLine,
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
extendPalette,
|
|
19
21
|
type PanelWorkspaceSection,
|
|
20
22
|
} from './polish.ts';
|
|
21
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
23
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
22
24
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
25
|
import {
|
|
24
26
|
getPanelSearchFocusTransition,
|
|
@@ -39,6 +41,15 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
|
39
41
|
countFg: '#88bbcc',
|
|
40
42
|
});
|
|
41
43
|
|
|
44
|
+
// Splits a computed '/command arg1 arg2' string into the { name, args } shape
|
|
45
|
+
// ctx.executeCommand expects — same leading-slash-stripping approach used by
|
|
46
|
+
// remote-panel.ts for its own dispatched command.
|
|
47
|
+
function parseCommand(command: string): { name: string; args: string[] } | null {
|
|
48
|
+
const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
|
|
49
|
+
const [name, ...args] = parts;
|
|
50
|
+
return name ? { name, args } : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
function shortDate(ts: number): string {
|
|
43
54
|
const d = new Date(ts);
|
|
44
55
|
const Y = d.getFullYear();
|
|
@@ -84,7 +95,14 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
84
95
|
private confirm: ConfirmState<string> | null = null;
|
|
85
96
|
private deleteError = '';
|
|
86
97
|
private loadError = '';
|
|
98
|
+
private hasLoaded = false;
|
|
87
99
|
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
100
|
+
// x = dispatch the computed next-step command ('/remote recover <runner>'
|
|
101
|
+
// or '/session resume <name>') via the same handleInput -> handlePanelIntegrationAction
|
|
102
|
+
// bridge remote-panel.ts uses — handleInput has no ctx.executeCommand.
|
|
103
|
+
private pendingCommand: { name: string; args: string[] } | null = null;
|
|
104
|
+
// On resume, restore any panels the session had open when it was saved.
|
|
105
|
+
private pendingOpenPanels: string[] | null = null;
|
|
88
106
|
|
|
89
107
|
constructor(
|
|
90
108
|
private readonly sessionManager: SessionBrowserQuery,
|
|
@@ -99,6 +117,21 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
99
117
|
this.refreshTimerId = this.registerTimer(setInterval(() => { this._load(); }, 5000));
|
|
100
118
|
}
|
|
101
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Clears any active search/filter and moves the cursor to the session
|
|
122
|
+
* matching `sessionId` (SessionInfo.name) so a sibling panel (e.g.
|
|
123
|
+
* RoutesPanel's Enter-on-binding jump) can land directly on it. No-ops
|
|
124
|
+
* (beyond clearing the filter) if the session isn't found.
|
|
125
|
+
*/
|
|
126
|
+
focusSession(sessionId: string): void {
|
|
127
|
+
this.searching = false;
|
|
128
|
+
this.searchQuery = '';
|
|
129
|
+
this._filter();
|
|
130
|
+
const index = this.filtered.findIndex((session) => session.name === sessionId);
|
|
131
|
+
if (index >= 0) this.cursorIndex = index;
|
|
132
|
+
this.markDirty();
|
|
133
|
+
}
|
|
134
|
+
|
|
102
135
|
override onDeactivate(): void {
|
|
103
136
|
if (this.refreshTimerId !== null) { this.clearTimer(this.refreshTimerId); this.refreshTimerId = null; }
|
|
104
137
|
this.searching = false;
|
|
@@ -161,16 +194,47 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
161
194
|
case 'return': this._resume(); return true;
|
|
162
195
|
case 'd': this._promptDelete(); return true;
|
|
163
196
|
case 'r': this._load(); return true;
|
|
197
|
+
case 'x': this._dispatchNextStep(); return true;
|
|
164
198
|
default: return false;
|
|
165
199
|
}
|
|
166
200
|
}
|
|
167
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Drains `pendingCommand` (set by the 'x' key) and `pendingOpenPanels`
|
|
204
|
+
* (set by `_resume()`) via the ctx.executeCommand / PanelManager bridge.
|
|
205
|
+
* Called by the input router immediately after handleInput() consumes the
|
|
206
|
+
* same key (panel-integration-actions.ts's onPanelInputConsumed).
|
|
207
|
+
*/
|
|
208
|
+
handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
209
|
+
let consumed = false;
|
|
210
|
+
if (this.pendingCommand) {
|
|
211
|
+
const { name, args } = this.pendingCommand;
|
|
212
|
+
this.pendingCommand = null;
|
|
213
|
+
void ctx.executeCommand?.(name, args);
|
|
214
|
+
consumed = true;
|
|
215
|
+
}
|
|
216
|
+
if (this.pendingOpenPanels) {
|
|
217
|
+
const openPanels = this.pendingOpenPanels;
|
|
218
|
+
this.pendingOpenPanels = null;
|
|
219
|
+
for (const panelId of openPanels) ctx.panelManager.open(panelId);
|
|
220
|
+
consumed = true;
|
|
221
|
+
}
|
|
222
|
+
return consumed;
|
|
223
|
+
}
|
|
224
|
+
|
|
168
225
|
render(width: number, height: number): Line[] {
|
|
169
226
|
if (height <= 0 || width <= 0) return [];
|
|
170
227
|
const intro = 'Browse, search, resume, and prune saved conversations.';
|
|
171
228
|
|
|
229
|
+
// Lazily load on first render so the panel works even when rendered without
|
|
230
|
+
// onActivate(). The session list is a synchronous source, so this resolves
|
|
231
|
+
// immediately rather than flashing a loading screen.
|
|
232
|
+
if (!this.hasLoaded) this._load();
|
|
233
|
+
|
|
172
234
|
const count = this.filtered.length;
|
|
173
235
|
const total = this.sessions.length;
|
|
236
|
+
const hasSelection = this.filtered.length > 0;
|
|
237
|
+
// Search/status line — surfaces the active query or any error.
|
|
174
238
|
const searchLine = this.searching
|
|
175
239
|
? ` Search: ${this.searchQuery}_`
|
|
176
240
|
: this.loadError
|
|
@@ -178,11 +242,21 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
178
242
|
: this.deleteError
|
|
179
243
|
? ` Error: ${this.deleteError}`
|
|
180
244
|
: this.searchQuery
|
|
181
|
-
? ` Filter: ${this.searchQuery}
|
|
182
|
-
: `
|
|
245
|
+
? ` Filter: ${this.searchQuery}`
|
|
246
|
+
: ` (no filter)`;
|
|
183
247
|
const statusFg = this.loadError || this.deleteError ? DEFAULT_PANEL_PALETTE.bad : this.searching ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim;
|
|
248
|
+
// Context-aware hints: only advertise keys that work in the current state.
|
|
249
|
+
const hints: Array<{ keys: string; label: string }> = [];
|
|
250
|
+
if (this.searching) {
|
|
251
|
+
hints.push({ keys: 'type', label: 'filter' }, { keys: 'Esc/Enter', label: 'apply' });
|
|
252
|
+
} else {
|
|
253
|
+
hints.push({ keys: '/', label: this.searchQuery ? 'edit search' : 'search' });
|
|
254
|
+
if (hasSelection) hints.push({ keys: 'Enter', label: 'resume' }, { keys: 'x', label: 'run next step' }, { keys: 'd', label: 'delete' });
|
|
255
|
+
hints.push({ keys: 'r', label: 'refresh' });
|
|
256
|
+
}
|
|
184
257
|
const footerLines = [
|
|
185
258
|
buildSearchInputLine(width, '', searchLine.trimStart(), DEFAULT_PANEL_PALETTE, { active: this.searching, valueColor: statusFg }),
|
|
259
|
+
buildKeyboardHints(width, hints, DEFAULT_PANEL_PALETTE),
|
|
186
260
|
];
|
|
187
261
|
|
|
188
262
|
if (this.confirm) {
|
|
@@ -208,12 +282,15 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
208
282
|
const emptyBody = this.searchQuery
|
|
209
283
|
? 'Clear or change the current filter to surface saved conversations again.'
|
|
210
284
|
: 'Conversations are saved automatically. Once you have saved sessions, they appear here for review and resume.';
|
|
285
|
+
const emptyActions = this.searchQuery
|
|
286
|
+
? [{ command: 'Esc', summary: 'clear the filter and show all sessions' }]
|
|
287
|
+
: [{ command: '/session save', summary: 'name and persist the current conversation' }];
|
|
211
288
|
return buildPanelWorkspace(width, height, {
|
|
212
289
|
title: ` Sessions [${count}/${total}]`,
|
|
213
290
|
intro,
|
|
214
291
|
sections: [
|
|
215
292
|
{
|
|
216
|
-
lines: buildEmptyState(width, emptyTitle, emptyBody,
|
|
293
|
+
lines: buildEmptyState(width, emptyTitle, emptyBody, emptyActions, DEFAULT_PANEL_PALETTE),
|
|
217
294
|
},
|
|
218
295
|
],
|
|
219
296
|
footerLines,
|
|
@@ -263,7 +340,7 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
263
340
|
...formatReturnContextLines(selected.returnContext).map((line) =>
|
|
264
341
|
buildPanelLine(width, [[' ', DEFAULT_PANEL_PALETTE.dim], [truncateDisplay(line, Math.max(0, width - 2)), DEFAULT_PANEL_PALETTE.dim]])
|
|
265
342
|
),
|
|
266
|
-
buildPanelLine(width, [[' Next ', DEFAULT_PANEL_PALETTE.label], [selected
|
|
343
|
+
buildPanelLine(width, [[' Next ', DEFAULT_PANEL_PALETTE.label], [this._computeNextStepCommand(selected), DEFAULT_PANEL_PALETTE.dim], [' (x to run)', DEFAULT_PANEL_PALETTE.dim]]),
|
|
267
344
|
],
|
|
268
345
|
}
|
|
269
346
|
: { title: 'Selected', lines: [] };
|
|
@@ -303,7 +380,7 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
303
380
|
const bg = isCursor ? C.selectBg : '';
|
|
304
381
|
const date = shortDate(sess.timestamp);
|
|
305
382
|
const cnt = String(sess.messageCount).padStart(3) + 'm ';
|
|
306
|
-
const model = (sess.model || 'unknown'
|
|
383
|
+
const model = fitDisplay(sess.model || 'unknown', 18) + ' ';
|
|
307
384
|
const prefixLength = 1 + 16 + 1 + 4 + 19;
|
|
308
385
|
const title = truncateDisplay(sess.title || sess.name || '(untitled)', Math.max(0, width - prefixLength));
|
|
309
386
|
return buildStyledPanelLine(width, [
|
|
@@ -321,10 +398,12 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
321
398
|
this.sessions = this.sessionManager.list();
|
|
322
399
|
this._filter();
|
|
323
400
|
this.loadError = '';
|
|
401
|
+
this.hasLoaded = true;
|
|
324
402
|
this.markDirty();
|
|
325
403
|
} catch (e) {
|
|
326
404
|
logger.debug('SessionBrowserPanel._load failed', { error: summarizeError(e) });
|
|
327
405
|
this.loadError = 'Failed to load sessions';
|
|
406
|
+
this.hasLoaded = true;
|
|
328
407
|
this.markDirty();
|
|
329
408
|
}
|
|
330
409
|
}
|
|
@@ -365,9 +444,25 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
365
444
|
private _resume(): void {
|
|
366
445
|
const sess = this.filtered[this.cursorIndex];
|
|
367
446
|
if (!sess) return;
|
|
447
|
+
if (sess.returnContext?.openPanels?.length) {
|
|
448
|
+
this.pendingOpenPanels = [...sess.returnContext.openPanels];
|
|
449
|
+
}
|
|
368
450
|
this.resumeSession?.(sess.name);
|
|
369
451
|
}
|
|
370
452
|
|
|
453
|
+
/** The next-step command shown in the 'Selected' section and dispatched by 'x'. */
|
|
454
|
+
private _computeNextStepCommand(sess: SessionInfo): string {
|
|
455
|
+
const runner = sess.returnContext?.remoteRunners?.[0];
|
|
456
|
+
return runner ? `/remote recover ${runner}` : `/session resume ${sess.name}`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private _dispatchNextStep(): void {
|
|
460
|
+
const sess = this.filtered[this.cursorIndex];
|
|
461
|
+
if (!sess) return;
|
|
462
|
+
const parsed = parseCommand(this._computeNextStepCommand(sess));
|
|
463
|
+
if (parsed) this.pendingCommand = parsed;
|
|
464
|
+
}
|
|
465
|
+
|
|
371
466
|
private _promptDelete(): void {
|
|
372
467
|
const sess = this.filtered[this.cursorIndex];
|
|
373
468
|
if (!sess) return;
|