@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- 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 +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- 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 +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,41 +1,28 @@
|
|
|
1
1
|
import { promises as fsPromises } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, sep } from 'node:path';
|
|
3
3
|
import type { Line } from '../types/grid.ts';
|
|
4
4
|
import { createEmptyLine } from '../types/grid.ts';
|
|
5
5
|
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
6
6
|
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
8
|
+
import { FilePreviewPanel } from './file-preview-panel.ts';
|
|
9
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
8
10
|
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
9
|
-
import type
|
|
11
|
+
import { listInstalledEcosystemEntries, type EcosystemCatalogPathOptions, type ShellPathService } from '@/runtime/index.ts';
|
|
10
12
|
import {
|
|
11
13
|
buildPanelLine,
|
|
12
14
|
buildPanelWorkspace,
|
|
13
15
|
DEFAULT_PANEL_PALETTE,
|
|
16
|
+
extendPalette,
|
|
14
17
|
} from './polish.ts';
|
|
15
|
-
import {
|
|
16
|
-
getPanelSearchFocusTransition,
|
|
17
|
-
isPanelSearchCancel,
|
|
18
|
-
} from './search-focus.ts';
|
|
19
18
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
label: '#64748b',
|
|
28
|
-
value: '#e2e8f0',
|
|
29
|
-
dim: '#64748b',
|
|
30
|
-
empty: '#334155',
|
|
31
|
-
selectedFg: '#e2e8f0',
|
|
32
|
-
selectedBg: '#1e3a5f',
|
|
33
|
-
project: '#38bdf8',
|
|
34
|
-
global: '#a78bfa',
|
|
35
|
-
hint: '#475569',
|
|
36
|
-
path: '#94a3b8',
|
|
37
|
-
selectBg: '#1e3a5f',
|
|
38
|
-
} as const;
|
|
20
|
+
// Domain accents only; base chrome (header/headerBg/label/value/dim/empty/
|
|
21
|
+
// selectBg) comes from DEFAULT_PANEL_PALETTE.
|
|
22
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
23
|
+
project: '#38bdf8', // project-local skill origin
|
|
24
|
+
global: '#a78bfa', // global skill origin
|
|
25
|
+
} as const);
|
|
39
26
|
|
|
40
27
|
export type SkillOrigin = 'project-local' | 'global' | 'custom';
|
|
41
28
|
|
|
@@ -47,11 +34,20 @@ export interface SkillRecord {
|
|
|
47
34
|
dependencies: string[];
|
|
48
35
|
includes: string[];
|
|
49
36
|
frontmatter: Record<string, string>;
|
|
37
|
+
/**
|
|
38
|
+
* Set when this skill's file lives under an installed ecosystem-marketplace
|
|
39
|
+
* receipt's targetPath — the receipt's provenance summary (e.g. curated
|
|
40
|
+
* source / signature info), so marketplace-installed skills read as such
|
|
41
|
+
* instead of looking identical to hand-authored ones.
|
|
42
|
+
*/
|
|
43
|
+
marketplaceProvenance?: string;
|
|
50
44
|
}
|
|
51
45
|
|
|
52
46
|
export interface SkillsPanelOptions {
|
|
53
47
|
shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
|
|
54
48
|
componentHealthMonitor?: ComponentHealthMonitor;
|
|
49
|
+
/** When provided, installed skill entries are tagged with marketplace provenance. */
|
|
50
|
+
ecosystemPaths?: EcosystemCatalogPathOptions;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
function parseFrontmatter(content: string): Record<string, string> {
|
|
@@ -133,7 +129,31 @@ async function scanSkillDirectory(root: string, origin: SkillOrigin): Promise<Sk
|
|
|
133
129
|
return records;
|
|
134
130
|
}
|
|
135
131
|
|
|
136
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Tags records whose file lives under an installed ecosystem-marketplace
|
|
134
|
+
* receipt's targetPath with that receipt's provenance summary. Best-effort:
|
|
135
|
+
* a lookup failure (e.g. no ecosystem catalogs configured) leaves records
|
|
136
|
+
* untagged rather than failing skill discovery.
|
|
137
|
+
*/
|
|
138
|
+
function applyMarketplaceProvenance(records: SkillRecord[], ecosystemPaths: EcosystemCatalogPathOptions): SkillRecord[] {
|
|
139
|
+
let receipts: ReturnType<typeof listInstalledEcosystemEntries>;
|
|
140
|
+
try {
|
|
141
|
+
receipts = listInstalledEcosystemEntries('skill', ecosystemPaths);
|
|
142
|
+
} catch {
|
|
143
|
+
return records;
|
|
144
|
+
}
|
|
145
|
+
if (receipts.length === 0) return records;
|
|
146
|
+
return records.map((record) => {
|
|
147
|
+
const receipt = receipts.find((candidate) => record.path === candidate.targetPath || record.path.startsWith(`${candidate.targetPath}${sep}`));
|
|
148
|
+
if (!receipt) return record;
|
|
149
|
+
return { ...record, marketplaceProvenance: receipt.provenanceSummary };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function discoverSkills(
|
|
154
|
+
shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>,
|
|
155
|
+
ecosystemPaths?: EcosystemCatalogPathOptions,
|
|
156
|
+
): Promise<SkillRecord[]> {
|
|
137
157
|
const cwd = shellPaths.workingDirectory;
|
|
138
158
|
const homeDir = shellPaths.homeDirectory;
|
|
139
159
|
const seen = new Set<string>();
|
|
@@ -147,7 +167,9 @@ export async function discoverSkills(shellPaths: Pick<ShellPathService, 'working
|
|
|
147
167
|
}
|
|
148
168
|
}
|
|
149
169
|
|
|
150
|
-
|
|
170
|
+
const tagged = ecosystemPaths ? applyMarketplaceProvenance(records, ecosystemPaths) : records;
|
|
171
|
+
|
|
172
|
+
return tagged.sort((a, b) => {
|
|
151
173
|
const originRank = a.origin === b.origin
|
|
152
174
|
? 0
|
|
153
175
|
: a.origin === 'project-local'
|
|
@@ -227,27 +249,34 @@ function originColor(origin: SkillOrigin): string {
|
|
|
227
249
|
}
|
|
228
250
|
}
|
|
229
251
|
|
|
230
|
-
export class SkillsPanel extends
|
|
252
|
+
export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
|
|
231
253
|
private readonly shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
|
|
232
|
-
|
|
233
|
-
private filterFocused = false;
|
|
254
|
+
private readonly ecosystemPaths: EcosystemCatalogPathOptions | undefined;
|
|
234
255
|
private cached: SkillRecord[] | null = null;
|
|
235
256
|
private cacheDirty = true;
|
|
236
257
|
// I1: confirm state for destructive delete
|
|
237
258
|
private confirm: ConfirmState | null = null;
|
|
238
259
|
private readyPromise: Promise<void> | null = null;
|
|
260
|
+
// Staged pending action consumed by handlePanelIntegrationAction (same
|
|
261
|
+
// pattern as diff-panel.ts's pendingOpenPreview): Enter marks the intent
|
|
262
|
+
// here, the actual PanelManager/preview wiring happens once the
|
|
263
|
+
// integration context is available.
|
|
264
|
+
private pendingOpenPreview = false;
|
|
239
265
|
|
|
240
266
|
public constructor(options: SkillsPanelOptions) {
|
|
241
|
-
super('skills', 'Skills', '
|
|
267
|
+
super('skills', 'Skills', '▩', 'automation-control', options.componentHealthMonitor);
|
|
242
268
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
269
|
+
this.filterEnabled = true; // WO-153: converged modal '/' filter
|
|
270
|
+
this.filterLabel = 'Filter';
|
|
243
271
|
this.shellPaths = options.shellPaths;
|
|
272
|
+
this.ecosystemPaths = options.ecosystemPaths;
|
|
244
273
|
}
|
|
245
274
|
|
|
246
275
|
// -------------------------------------------------------------------------
|
|
247
|
-
//
|
|
276
|
+
// ScrollableListPanel implementation
|
|
248
277
|
// -------------------------------------------------------------------------
|
|
249
278
|
|
|
250
|
-
protected
|
|
279
|
+
protected getItems(): readonly SkillRecord[] {
|
|
251
280
|
return this.cached ?? [];
|
|
252
281
|
}
|
|
253
282
|
|
|
@@ -255,9 +284,9 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
|
255
284
|
const p = (async () => {
|
|
256
285
|
try {
|
|
257
286
|
await this.withLoading('Scanning skills\u2026', async () => {
|
|
258
|
-
this.cached = await discoverSkills(this.shellPaths);
|
|
287
|
+
this.cached = await discoverSkills(this.shellPaths, this.ecosystemPaths);
|
|
259
288
|
this.cacheDirty = false;
|
|
260
|
-
this.
|
|
289
|
+
this.markDirty();
|
|
261
290
|
});
|
|
262
291
|
} catch (err) {
|
|
263
292
|
this.setError(summarizeError(err));
|
|
@@ -273,14 +302,31 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
|
273
302
|
return this.readyPromise ?? Promise.resolve();
|
|
274
303
|
}
|
|
275
304
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
305
|
+
/**
|
|
306
|
+
* REAL delete: removes the confirmed skill's markdown file from disk, then
|
|
307
|
+
* rescans so the list reflects the change immediately. Replaces the old
|
|
308
|
+
* setError('Delete via shell: rm …') signpost, which never actually
|
|
309
|
+
* deleted anything.
|
|
310
|
+
*/
|
|
311
|
+
private async _deleteSkill(path: string): Promise<void> {
|
|
312
|
+
try {
|
|
313
|
+
await fsPromises.rm(path);
|
|
314
|
+
this.cacheDirty = true;
|
|
315
|
+
await this._loadSkillsAsync();
|
|
316
|
+
} catch (err) {
|
|
317
|
+
this.setError(summarizeError(err));
|
|
318
|
+
this.markDirty();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** `q` arrives already trimmed + lower-cased from ScrollableListPanel.getVisibleItems(). */
|
|
323
|
+
protected override filterMatches(skill: SkillRecord, q: string): boolean {
|
|
279
324
|
const haystack = [
|
|
280
325
|
skill.name,
|
|
281
326
|
skill.description,
|
|
282
327
|
skill.path,
|
|
283
328
|
skill.origin,
|
|
329
|
+
skill.marketplaceProvenance ?? '',
|
|
284
330
|
skill.dependencies.join(' '),
|
|
285
331
|
skill.includes.join(' '),
|
|
286
332
|
].join(' ').toLowerCase();
|
|
@@ -290,17 +336,22 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
|
290
336
|
protected renderItem(skill: SkillRecord, index: number, selected: boolean, width: number): Line {
|
|
291
337
|
const bg = selected ? C.selectBg : undefined;
|
|
292
338
|
const dot = skill.origin === 'project-local' ? '\u25c6' : '\u2022';
|
|
339
|
+
// Marketplace-installed skills get a badge glyph; unbadged column stays a
|
|
340
|
+
// single blank cell so rows stay aligned either way.
|
|
341
|
+
const marketplaceBadge = skill.marketplaceProvenance ? '\u2605' : ' ';
|
|
293
342
|
const desc = skill.description || 'No description provided.';
|
|
294
|
-
const descWidth = Math.max(1, width -
|
|
343
|
+
const descWidth = Math.max(1, width - 6 - skill.name.length - 6);
|
|
295
344
|
const descLines = wordWrap(desc, descWidth);
|
|
296
345
|
return buildPanelLine(width, [
|
|
297
|
-
[selected ? '\u25b8' : ' ', C.
|
|
346
|
+
[selected ? '\u25b8' : ' ', C.value, bg],
|
|
298
347
|
[' ', C.dim, bg],
|
|
299
348
|
[dot, originColor(skill.origin), bg],
|
|
300
349
|
[' ', C.dim, bg],
|
|
301
|
-
[
|
|
350
|
+
[marketplaceBadge, C.info, bg],
|
|
351
|
+
[' ', C.dim, bg],
|
|
352
|
+
[skill.name, selected ? C.value : C.value, bg],
|
|
302
353
|
[' ', C.dim, bg],
|
|
303
|
-
[descLines[0] ?? '', selected ? C.
|
|
354
|
+
[descLines[0] ?? '', selected ? C.value : C.dim, bg],
|
|
304
355
|
]);
|
|
305
356
|
}
|
|
306
357
|
|
|
@@ -315,24 +366,66 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
|
315
366
|
|
|
316
367
|
public override onActivate(): void {
|
|
317
368
|
super.onActivate();
|
|
318
|
-
this.
|
|
319
|
-
this.
|
|
320
|
-
this.filterFocused = false;
|
|
369
|
+
this.filterQuery = '';
|
|
370
|
+
this.filterActive = false;
|
|
321
371
|
this.cacheDirty = true;
|
|
322
372
|
void this._loadSkillsAsync();
|
|
323
373
|
}
|
|
324
374
|
|
|
325
375
|
public override onDestroy(): void {}
|
|
326
376
|
|
|
377
|
+
protected override onSelect(_skill: SkillRecord): void {
|
|
378
|
+
// Enter opens the skill's markdown source in the preview panel — see
|
|
379
|
+
// handlePanelIntegrationAction for the actual PanelManager wiring
|
|
380
|
+
// (needs the integration context, not available here). Selection itself
|
|
381
|
+
// is read back from getVisibleItems() in that hook. onSelect is only
|
|
382
|
+
// invoked by ScrollableListPanel's navigation handler when the filter is
|
|
383
|
+
// not active, so no manual guard is needed here.
|
|
384
|
+
this.pendingOpenPreview = true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Cross-panel integration hook — Enter opens the selected skill's markdown
|
|
389
|
+
* source in the preview panel via the same open/focus bridge DiffPanel
|
|
390
|
+
* uses (src/input/panel-integration-actions.ts), without this panel
|
|
391
|
+
* needing to know about PanelManager pane/focus mechanics beyond what ctx
|
|
392
|
+
* exposes.
|
|
393
|
+
*/
|
|
394
|
+
public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
395
|
+
if (!this.pendingOpenPreview) return false;
|
|
396
|
+
this.pendingOpenPreview = false;
|
|
397
|
+
const skill = this.getSelectedItem();
|
|
398
|
+
if (!skill) return false;
|
|
399
|
+
|
|
400
|
+
const pm = ctx.panelManager;
|
|
401
|
+
let previewPanel = pm.getPanel('preview');
|
|
402
|
+
if (previewPanel instanceof FilePreviewPanel) {
|
|
403
|
+
const pane = pm.getPaneOf('preview');
|
|
404
|
+
pm.activateById('preview');
|
|
405
|
+
if (pane) pm.focusPane(pane);
|
|
406
|
+
} else {
|
|
407
|
+
const targetPane: 'top' | 'bottom' = pm.isBottomPaneVisible()
|
|
408
|
+
? (pm.getFocusedPane() === 'top' ? 'bottom' : 'top')
|
|
409
|
+
: 'bottom';
|
|
410
|
+
const opened = pm.open('preview', targetPane);
|
|
411
|
+
pm.show();
|
|
412
|
+
pm.focusPane(targetPane);
|
|
413
|
+
previewPanel = opened instanceof FilePreviewPanel ? opened : null;
|
|
414
|
+
}
|
|
415
|
+
if (previewPanel instanceof FilePreviewPanel) {
|
|
416
|
+
previewPanel.openFile(skill.path);
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
327
422
|
public handleInput(key: string): boolean {
|
|
328
423
|
// I1: y/n confirmation dialog for delete
|
|
329
424
|
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
330
425
|
if (confirmResult === 'confirmed') {
|
|
331
426
|
const toDelete = this.confirm!.subject;
|
|
332
427
|
this.confirm = null;
|
|
333
|
-
|
|
334
|
-
// Surface an error directing the user to remove the file manually.
|
|
335
|
-
this.setError(`Delete via shell: rm "${toDelete}"`);
|
|
428
|
+
void this._deleteSkill(toDelete);
|
|
336
429
|
this.markDirty();
|
|
337
430
|
return true;
|
|
338
431
|
}
|
|
@@ -343,38 +436,11 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
|
343
436
|
}
|
|
344
437
|
if (confirmResult === 'absorbed') return true;
|
|
345
438
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
if (this.
|
|
350
|
-
const
|
|
351
|
-
if (transition === 'focus-list') {
|
|
352
|
-
this.filterFocused = false;
|
|
353
|
-
this.markDirty();
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
// Escape: also blur filter focus (clear + return to list navigation)
|
|
357
|
-
if (isPanelSearchCancel(key)) {
|
|
358
|
-
this.filterFocused = false;
|
|
359
|
-
// Delegate to super to clear the query. If the query is empty, super
|
|
360
|
-
// returns false and escape propagates to the panel dismissal handler —
|
|
361
|
-
// this is the intentional double-escape UX (blur filter, then close).
|
|
362
|
-
return super.handleInput(key);
|
|
363
|
-
}
|
|
364
|
-
// Delegate backspace/printable to SearchableListPanel.handleInput
|
|
365
|
-
return super.handleInput(key);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
|
|
369
|
-
if (transition === 'focus-search') {
|
|
370
|
-
this.filterFocused = true;
|
|
371
|
-
this.markDirty();
|
|
372
|
-
return true;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// I1: 'd' prompts delete confirmation
|
|
376
|
-
if (key === 'd') {
|
|
377
|
-
const skill = items[this.selectedIndex];
|
|
439
|
+
// I1: 'd' prompts delete confirmation — only outside filter mode, so 'd'
|
|
440
|
+
// remains typeable into the filter query while it is active (WO-153:
|
|
441
|
+
// converged modal '/' filter coexists with single-letter action keys).
|
|
442
|
+
if (!this.filterActive && key === 'd') {
|
|
443
|
+
const skill = this.getSelectedItem();
|
|
378
444
|
if (skill) {
|
|
379
445
|
this.confirm = { subject: skill.path, label: skill.name };
|
|
380
446
|
this.markDirty();
|
|
@@ -382,7 +448,8 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
|
382
448
|
return true;
|
|
383
449
|
}
|
|
384
450
|
|
|
385
|
-
// Navigation +
|
|
451
|
+
// Navigation + filter: delegate to ScrollableListPanel ('/' enters
|
|
452
|
+
// filter, typing narrows, Esc clears, up/down/g/G/page/enter navigate).
|
|
386
453
|
return super.handleInput(key);
|
|
387
454
|
}
|
|
388
455
|
|
|
@@ -402,27 +469,28 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
|
402
469
|
return lines.slice(0, height);
|
|
403
470
|
}
|
|
404
471
|
|
|
405
|
-
// Build filter input line (provided by SearchableListPanel base)
|
|
406
|
-
const filterLine = this.buildFilterInputLine(width, 'Filter', this.filterFocused);
|
|
407
|
-
|
|
408
472
|
// Build detail footer for the currently selected skill
|
|
409
|
-
const
|
|
410
|
-
const selected = items[this.selectedIndex];
|
|
473
|
+
const selected = this.getSelectedItem();
|
|
411
474
|
const detailLines: Line[] = [];
|
|
412
475
|
if (selected) {
|
|
413
476
|
detailLines.push(
|
|
414
477
|
buildPanelLine(width, [[' Selected: ', C.label], [selected.name, C.value], [' [', C.dim], [originLabel(selected.origin), originColor(selected.origin)], [']', C.dim]]),
|
|
415
|
-
buildPanelLine(width, [[' Path: ', C.label], [truncatePathDisplay(selected.path, Math.max(1, width - 8)), C.
|
|
478
|
+
buildPanelLine(width, [[' Path: ', C.label], [truncatePathDisplay(selected.path, Math.max(1, width - 8)), C.label]]),
|
|
416
479
|
buildPanelLine(width, [[' Desc: ', C.label], [selected.description || 'No description provided.', C.value]]),
|
|
417
480
|
buildPanelLine(width, [[' Depends: ', C.label], [selected.dependencies.length > 0 ? selected.dependencies.join(', ') : 'none', C.dim]]),
|
|
418
481
|
buildPanelLine(width, [[' Includes: ', C.label], [selected.includes.length > 0 ? selected.includes.join(', ') : 'none', C.dim]]),
|
|
482
|
+
buildPanelLine(width, [
|
|
483
|
+
[' Provenance: ', C.label],
|
|
484
|
+
[selected.marketplaceProvenance ?? 'not installed via marketplace', selected.marketplaceProvenance ? C.info : C.dim],
|
|
485
|
+
]),
|
|
419
486
|
);
|
|
420
487
|
}
|
|
421
|
-
detailLines.push(buildPanelLine(width, [[' Up/Down navigate /
|
|
488
|
+
detailLines.push(buildPanelLine(width, [[' Up/Down navigate / filter Esc clear Backspace edit', C.dim]]));
|
|
489
|
+
detailLines.push(buildPanelLine(width, [[' Enter open in preview d delete (confirm)', C.dim]]));
|
|
422
490
|
|
|
491
|
+
// Filter input line is auto-injected by renderList() (filterEnabled=true).
|
|
423
492
|
const lines = this.renderList(width, height, {
|
|
424
493
|
title: 'Skills - discover project-local and global skill packs',
|
|
425
|
-
header: [filterLine],
|
|
426
494
|
footer: detailLines,
|
|
427
495
|
});
|
|
428
496
|
return lines;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
3
|
import { fitDisplay } from '../utils/terminal-width.ts';
|
|
4
|
+
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
4
5
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
|
-
import type { KeyName } from './types.ts';
|
|
6
|
+
import type { KeyName, PanelIntegrationContext } from './types.ts';
|
|
6
7
|
import type { ProviderSubscription, PendingSubscriptionLogin } from '@pellux/goodvibes-sdk/platform/config';
|
|
7
8
|
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
8
9
|
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
9
10
|
import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
|
|
10
11
|
import {
|
|
11
12
|
buildEmptyState,
|
|
12
|
-
buildGuidanceLine,
|
|
13
13
|
buildKeyboardHints,
|
|
14
14
|
buildKeyValueLine,
|
|
15
15
|
buildPanelListRow,
|
|
@@ -19,17 +19,9 @@ import {
|
|
|
19
19
|
DEFAULT_PANEL_PALETTE,
|
|
20
20
|
} from './polish.ts';
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
headerBg: '#1e293b',
|
|
26
|
-
dim: '#64748b',
|
|
27
|
-
info: '#38bdf8',
|
|
28
|
-
good: '#22c55e',
|
|
29
|
-
warn: '#f59e0b',
|
|
30
|
-
bad: '#ef4444',
|
|
31
|
-
selectedBg: '#0f172a',
|
|
32
|
-
} as const;
|
|
22
|
+
// Base chrome only — title band, state colors, and text tokens all come
|
|
23
|
+
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
24
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
33
25
|
|
|
34
26
|
interface SubscriptionRow {
|
|
35
27
|
readonly provider: string;
|
|
@@ -57,18 +49,38 @@ function statusColor(status: ReturnType<typeof statusOf>): string {
|
|
|
57
49
|
}
|
|
58
50
|
}
|
|
59
51
|
|
|
52
|
+
// Highlight window for "expiring soon" — mirrors the provider-console
|
|
53
|
+
// convention of flagging tokens inside their last day of validity so the
|
|
54
|
+
// operator can refresh/re-login before the subscription actually lapses.
|
|
55
|
+
const EXPIRING_SOON_MS = 24 * 60 * 60 * 1000;
|
|
56
|
+
|
|
57
|
+
function isExpiringSoon(subscription: ProviderSubscription | null): boolean {
|
|
58
|
+
if (!subscription?.expiresAt) return false;
|
|
59
|
+
const remaining = subscription.expiresAt - Date.now();
|
|
60
|
+
return remaining > 0 && remaining <= EXPIRING_SOON_MS;
|
|
61
|
+
}
|
|
62
|
+
|
|
60
63
|
export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
61
64
|
private readonly serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
|
|
62
65
|
private readonly subscriptionManager: SubscriptionAccessQuery;
|
|
63
66
|
private rows: SubscriptionRow[] = [];
|
|
64
67
|
/** Pending logout confirmation — uses project-standard ConfirmState contract. */
|
|
65
68
|
private confirm: ConfirmState<string> | null = null;
|
|
69
|
+
// Set by handleInput (Enter on an 'available' row) and consumed on the very
|
|
70
|
+
// next handlePanelIntegrationAction dispatch of that same key — handleInput
|
|
71
|
+
// has no ctx.executeCommand, so the actual login-start dispatch happens via
|
|
72
|
+
// the bridge (remote-panel.ts / token-budget-panel.ts pattern).
|
|
73
|
+
private pendingCommand: { name: string; args: string[] } | null = null;
|
|
74
|
+
// Background posture refresh while the panel is visible, so a login/logout
|
|
75
|
+
// that completes elsewhere (e.g. the browser OAuth handshake finishing)
|
|
76
|
+
// shows up without requiring an explicit 'r'.
|
|
77
|
+
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
66
78
|
|
|
67
79
|
public constructor(
|
|
68
80
|
serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>,
|
|
69
81
|
subscriptionManager: SubscriptionAccessQuery,
|
|
70
82
|
) {
|
|
71
|
-
super('subscription', 'Subscriptions', 'B', '
|
|
83
|
+
super('subscription', 'Subscriptions', 'B', 'providers');
|
|
72
84
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
73
85
|
this.serviceRegistry = serviceRegistry;
|
|
74
86
|
this.subscriptionManager = subscriptionManager;
|
|
@@ -77,6 +89,28 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
77
89
|
public override onActivate(): void {
|
|
78
90
|
super.onActivate();
|
|
79
91
|
this.refresh();
|
|
92
|
+
if (this.refreshTimer === null) {
|
|
93
|
+
this.refreshTimer = this.registerTimer(setInterval(() => {
|
|
94
|
+
this.refresh();
|
|
95
|
+
this.markDirty();
|
|
96
|
+
}, 5_000));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public override onDeactivate(): void {
|
|
101
|
+
if (this.refreshTimer !== null) {
|
|
102
|
+
this.clearTimer(this.refreshTimer);
|
|
103
|
+
this.refreshTimer = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Consumes the login-start command queued by handleInput's Enter-on-'available' branch. */
|
|
108
|
+
public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
109
|
+
if (!this.pendingCommand) return false;
|
|
110
|
+
const { name, args } = this.pendingCommand;
|
|
111
|
+
this.pendingCommand = null;
|
|
112
|
+
void ctx.executeCommand?.(name, args);
|
|
113
|
+
return true;
|
|
80
114
|
}
|
|
81
115
|
|
|
82
116
|
protected override getPalette() { return C; }
|
|
@@ -95,12 +129,17 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
95
129
|
|
|
96
130
|
protected renderItem(row: SubscriptionRow, index: number, selected: boolean, width: number): Line {
|
|
97
131
|
const status = statusOf(row);
|
|
132
|
+
// Expiring-soon highlight: the STATUS text stays 'ACTIVE' (the row is
|
|
133
|
+
// still active), but it's colored as a warning and gets a trailing
|
|
134
|
+
// marker so an about-to-lapse token stands out from the list.
|
|
135
|
+
const expiringSoon = isExpiringSoon(row.subscription);
|
|
98
136
|
return buildPanelListRow(width, [
|
|
99
137
|
{ text: fitDisplay(row.provider, 16), fg: C.value },
|
|
100
|
-
{ text: ` ${status.toUpperCase().padEnd(12)}`, fg: statusColor(status) },
|
|
138
|
+
{ text: ` ${status.toUpperCase().padEnd(12)}`, fg: expiringSoon ? C.warn : statusColor(status) },
|
|
101
139
|
{ text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
|
|
102
140
|
{ text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
|
|
103
|
-
|
|
141
|
+
...(expiringSoon ? [{ text: ' expiring soon', fg: C.warn }] : []),
|
|
142
|
+
], C, { selected, selectedBg: C.selectBg });
|
|
104
143
|
}
|
|
105
144
|
|
|
106
145
|
public handleInput(key: KeyName): boolean {
|
|
@@ -122,7 +161,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
122
161
|
if (confirmResult === 'absorbed') return true;
|
|
123
162
|
|
|
124
163
|
if (this.rows.length === 0) return false;
|
|
125
|
-
const selected = this.
|
|
164
|
+
const selected = this.getSelectedItem() ?? null;
|
|
126
165
|
if (key === 'up' || key === 'k') {
|
|
127
166
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
128
167
|
this.confirm = null;
|
|
@@ -136,10 +175,18 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
136
175
|
return true;
|
|
137
176
|
}
|
|
138
177
|
if (key === 'enter' || key === 'return') {
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
178
|
+
if (selected?.subscription) {
|
|
179
|
+
this.confirm = { subject: selected.provider, label: selected.provider };
|
|
180
|
+
this.markDirty();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
// Enter on an 'available' row starts the real login flow via the
|
|
184
|
+
// bridge instead of leaving it as a printed signpost.
|
|
185
|
+
if (selected && statusOf(selected) === 'available') {
|
|
186
|
+
this.pendingCommand = { name: 'subscription', args: ['login', selected.provider, 'start'] };
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
143
190
|
}
|
|
144
191
|
if (key === 'r') {
|
|
145
192
|
this.refresh();
|
|
@@ -187,17 +234,21 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
187
234
|
{ keys: 'n/Esc', label: 'cancel' },
|
|
188
235
|
], C);
|
|
189
236
|
}
|
|
190
|
-
const selected = this.
|
|
237
|
+
const selected = this.getSelectedItem();
|
|
191
238
|
const hints: Array<{ keys: string; label: string }> = [{ keys: 'Up/Down', label: 'select' }];
|
|
192
239
|
if (selected?.subscription) hints.push({ keys: 'Enter', label: 'sign out' });
|
|
193
|
-
else if (selected?.hasOAuthConfig) hints.push({ keys: '
|
|
240
|
+
else if (selected?.hasOAuthConfig) hints.push({ keys: 'Enter', label: 'sign in' });
|
|
194
241
|
hints.push({ keys: 'r', label: 'refresh' });
|
|
195
242
|
return buildKeyboardHints(width, hints, C);
|
|
196
243
|
}
|
|
197
244
|
|
|
245
|
+
// WO-139: refresh() moved out of render() (hooks-panel.ts:109 pattern) — it
|
|
246
|
+
// now runs on onActivate(), on 'r', and on the 5s background timer above,
|
|
247
|
+
// instead of rebuilding the row set (service registry scan + builtin
|
|
248
|
+
// provider list + subscription-manager reads) on every single frame.
|
|
198
249
|
public render(width: number, height: number): Line[] {
|
|
199
|
-
this.refresh();
|
|
200
250
|
this.clampSelection();
|
|
251
|
+
const selected = this.getSelectedItem();
|
|
201
252
|
const intro = 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.';
|
|
202
253
|
|
|
203
254
|
const activeCount = this.rows.filter((row) => row.subscription).length;
|
|
@@ -210,10 +261,9 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
210
261
|
{ label: 'providers', value: String(this.rows.length), valueColor: C.value },
|
|
211
262
|
], C),
|
|
212
263
|
buildKeyValueLine(width, [
|
|
213
|
-
{ label: 'selected', value: (
|
|
214
|
-
{ label: 'status', value:
|
|
264
|
+
{ label: 'selected', value: (selected?.provider ?? 'none'), valueColor: selected ? C.value : C.dim },
|
|
265
|
+
{ label: 'status', value: selected ? statusOf(selected) : 'n/a', valueColor: selected ? statusColor(statusOf(selected)) : C.dim },
|
|
215
266
|
], C),
|
|
216
|
-
buildGuidanceLine(width, '/subscription login <provider> start', 'start or repair browser login for the selected provider route', C),
|
|
217
267
|
];
|
|
218
268
|
|
|
219
269
|
// Empty state: render posture + base empty state
|
|
@@ -231,7 +281,6 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
231
281
|
intro,
|
|
232
282
|
sections: [{ lines: [...summaryLines, ...emptyLines] }],
|
|
233
283
|
footerLines: [
|
|
234
|
-
buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
|
|
235
284
|
this.buildFooterHint(width),
|
|
236
285
|
],
|
|
237
286
|
palette: C,
|
|
@@ -240,7 +289,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
240
289
|
return workspace.slice(0, height);
|
|
241
290
|
}
|
|
242
291
|
|
|
243
|
-
const selectedRow =
|
|
292
|
+
const selectedRow = selected;
|
|
244
293
|
const detailRows: Line[] = [];
|
|
245
294
|
if (selectedRow) {
|
|
246
295
|
detailRows.push(buildKeyValueLine(width, [
|
|
@@ -249,12 +298,13 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
249
298
|
{ label: 'oauth config', value: selectedRow.hasOAuthConfig ? 'present' : 'missing', valueColor: selectedRow.hasOAuthConfig ? C.good : C.bad },
|
|
250
299
|
], C));
|
|
251
300
|
if (selectedRow.subscription) {
|
|
301
|
+
const expiringSoon = isExpiringSoon(selectedRow.subscription);
|
|
252
302
|
const expires = selectedRow.subscription.expiresAt
|
|
253
303
|
? new Date(selectedRow.subscription.expiresAt).toISOString()
|
|
254
304
|
: 'n/a';
|
|
255
305
|
detailRows.push(buildKeyValueLine(width, [
|
|
256
306
|
{ label: 'token type', value: selectedRow.subscription.tokenType, valueColor: C.info },
|
|
257
|
-
{ label: 'expires', value: expires, valueColor: C.dim },
|
|
307
|
+
{ label: 'expires', value: expiringSoon ? `${expires} (expiring soon)` : expires, valueColor: expiringSoon ? C.warn : C.dim },
|
|
258
308
|
], C));
|
|
259
309
|
detailRows.push(buildPanelLine(width, [[
|
|
260
310
|
` ${selectedRow.subscription.overrideAmbientApiKeys
|
|
@@ -262,13 +312,17 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
262
312
|
: 'Stored for subscription-backed flows. Ambient API-key resolution remains unchanged.'}`,
|
|
263
313
|
C.dim,
|
|
264
314
|
]]));
|
|
315
|
+
if (expiringSoon) {
|
|
316
|
+
detailRows.push(buildPanelLine(width, [[' Token expires within 24h — sign out and re-login (Enter) to refresh it before it lapses.', C.warn]]));
|
|
317
|
+
}
|
|
265
318
|
if (this.confirm?.subject === selectedRow.provider) {
|
|
266
319
|
detailRows.push(buildPanelLine(width, [[` Sign out ${selectedRow.provider}? Press y or Enter to confirm, n or Esc to cancel.`, C.warn]]));
|
|
267
320
|
}
|
|
268
321
|
} else if (selectedRow.pending) {
|
|
269
|
-
|
|
322
|
+
const age = formatElapsed(Date.now() - selectedRow.pending.createdAt);
|
|
323
|
+
detailRows.push(buildPanelLine(width, [[` Login is pending (started ${age} ago). Finish with /subscription login <provider> finish <code>.`, C.warn]]));
|
|
270
324
|
} else if (selectedRow.hasOAuthConfig) {
|
|
271
|
-
detailRows.push(buildPanelLine(width, [[' Ready for login.
|
|
325
|
+
detailRows.push(buildPanelLine(width, [[' Ready for login. Press Enter to start it here.', C.dim]]));
|
|
272
326
|
} else {
|
|
273
327
|
detailRows.push(buildPanelLine(width, [[' Add a provider-specific OAuth config or enable a built-in subscription provider to use subscription login.', C.bad]]));
|
|
274
328
|
}
|
|
@@ -281,7 +335,6 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
281
335
|
header: headerLines,
|
|
282
336
|
footer: [
|
|
283
337
|
...detailRows,
|
|
284
|
-
buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
|
|
285
338
|
this.buildFooterHint(width),
|
|
286
339
|
],
|
|
287
340
|
});
|