@pellux/goodvibes-tui 0.20.3 → 0.21.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 +27 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +662 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +14 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +11 -10
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +77 -8
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +21 -0
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -11,8 +11,11 @@ import {
|
|
|
11
11
|
DEFAULT_PANEL_PALETTE,
|
|
12
12
|
type PanelPalette,
|
|
13
13
|
} from './polish.ts';
|
|
14
|
-
import type { LocalAuthSnapshot } from '@pellux/goodvibes-sdk/platform/security';
|
|
14
|
+
import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
15
15
|
import type { LocalAuthInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
16
|
+
import type { KeyName } from './types.ts';
|
|
17
|
+
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
18
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
16
19
|
|
|
17
20
|
const C = {
|
|
18
21
|
...DEFAULT_PANEL_PALETTE,
|
|
@@ -28,8 +31,19 @@ function formatRoles(roles: readonly string[]): string {
|
|
|
28
31
|
|
|
29
32
|
type LocalAuthUser = LocalAuthSnapshot['users'][number];
|
|
30
33
|
|
|
34
|
+
/** Action kind for the masked password entry mode. */
|
|
35
|
+
export type MaskedEntryKind = 'add-user' | 'rotate-password';
|
|
36
|
+
|
|
37
|
+
interface MaskedEntryState {
|
|
38
|
+
readonly kind: MaskedEntryKind;
|
|
39
|
+
readonly username: string;
|
|
40
|
+
readonly auth: UserAuthManager;
|
|
41
|
+
buffer: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
32
45
|
private readonly authManager: LocalAuthInspectionQuery;
|
|
46
|
+
private maskedState: MaskedEntryState | null = null;
|
|
33
47
|
|
|
34
48
|
public constructor(authManager: LocalAuthInspectionQuery) {
|
|
35
49
|
super('local-auth', 'Local Auth', 'U', 'monitoring');
|
|
@@ -37,6 +51,81 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
37
51
|
this.authManager = authManager;
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Activate masked password-entry mode for the given operation.
|
|
56
|
+
* The panel's handleInput will capture keystrokes into a private buffer;
|
|
57
|
+
* no plaintext is ever recorded in input history, transcript, or logs.
|
|
58
|
+
*/
|
|
59
|
+
public openMaskedEntry(kind: MaskedEntryKind, username: string, auth: UserAuthManager): void {
|
|
60
|
+
this.maskedState = { kind, username, auth, buffer: '' };
|
|
61
|
+
this.invalidate();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Returns true when the panel is in masked-entry mode. */
|
|
65
|
+
public get isMaskedEntryActive(): boolean {
|
|
66
|
+
return this.maskedState !== null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public override handleInput(key: KeyName): boolean {
|
|
70
|
+
if (this.maskedState === null) {
|
|
71
|
+
// Delegate scroll/selection to ScrollableListPanel when not in masked mode.
|
|
72
|
+
return super.handleInput?.(key) ?? false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Masked entry is active — capture all keystrokes.
|
|
76
|
+
const state = this.maskedState;
|
|
77
|
+
|
|
78
|
+
if (key === 'escape') {
|
|
79
|
+
// Cancel: discard buffer, exit masked mode without persisting.
|
|
80
|
+
this.maskedState = null;
|
|
81
|
+
this.invalidate();
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (key === 'enter' || key === 'return') {
|
|
86
|
+
// Submit: call the auth API if a non-empty password was entered.
|
|
87
|
+
if (state.buffer.length === 0) {
|
|
88
|
+
return true; // no-op — require at least one character
|
|
89
|
+
}
|
|
90
|
+
const password = state.buffer;
|
|
91
|
+
const { kind, username, auth } = state;
|
|
92
|
+
// Clear the mutable state *before* the auth call so the secret
|
|
93
|
+
// never lingers in the buffer after an exception.
|
|
94
|
+
this.maskedState = null;
|
|
95
|
+
try {
|
|
96
|
+
if (kind === 'add-user') {
|
|
97
|
+
auth.addUser(username, password, ['admin']);
|
|
98
|
+
} else {
|
|
99
|
+
auth.rotatePassword(username, password);
|
|
100
|
+
}
|
|
101
|
+
} catch (_error) {
|
|
102
|
+
// Surface errors via the panel's error facility rather than logging.
|
|
103
|
+
this.setError(summarizeError(_error));
|
|
104
|
+
}
|
|
105
|
+
this.invalidate();
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isTextBackspace(key)) {
|
|
110
|
+
// Remove last character (Ink 6.8.0: raw-stdin raw 'backspace' only).
|
|
111
|
+
if (state.buffer.length > 0) {
|
|
112
|
+
state.buffer = state.buffer.slice(0, -1);
|
|
113
|
+
this.invalidate();
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Single printable character: append to buffer.
|
|
119
|
+
if (key.length === 1) {
|
|
120
|
+
state.buffer += key;
|
|
121
|
+
this.invalidate();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// All other named keys are consumed (ignored) while masked mode is active.
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
40
129
|
protected override getPalette(): PanelPalette {
|
|
41
130
|
return C;
|
|
42
131
|
}
|
|
@@ -57,6 +146,12 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
57
146
|
}
|
|
58
147
|
|
|
59
148
|
public render(width: number, height: number): Line[] {
|
|
149
|
+
// When masked entry is active, render a dedicated prompt instead of the
|
|
150
|
+
// normal panel content. No plaintext password appears anywhere in output.
|
|
151
|
+
if (this.maskedState !== null) {
|
|
152
|
+
return this.renderMaskedPrompt(width, height);
|
|
153
|
+
}
|
|
154
|
+
|
|
60
155
|
const intro = 'Manage local daemon and HTTP-listener auth users, bootstrap state, and active sessions.';
|
|
61
156
|
const snapshot = this.authManager.inspect();
|
|
62
157
|
const users = this.getItems();
|
|
@@ -81,7 +176,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
81
176
|
...(issueMessages.length > 0
|
|
82
177
|
? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}`.slice(0, Math.max(0, width)), C.warn]]))
|
|
83
178
|
: [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
|
|
84
|
-
buildGuidanceLine(width, '/auth local rotate-password <user>
|
|
179
|
+
buildGuidanceLine(width, '/auth local rotate-password <user>', 'open masked password entry for the selected user (no plaintext in history)', C),
|
|
85
180
|
], C),
|
|
86
181
|
];
|
|
87
182
|
|
|
@@ -104,7 +199,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
104
199
|
footerLines.push(
|
|
105
200
|
...buildDetailBlock(width, 'Selected user', [
|
|
106
201
|
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
|
|
107
|
-
buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username}
|
|
202
|
+
buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
|
|
108
203
|
buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
|
|
109
204
|
], C),
|
|
110
205
|
);
|
|
@@ -119,7 +214,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
119
214
|
])),
|
|
120
215
|
);
|
|
121
216
|
}
|
|
122
|
-
footerLines.push(buildPanelLine(width, [[' /auth local review /auth local add-user /auth local rotate-password
|
|
217
|
+
footerLines.push(buildPanelLine(width, [[' /auth local review /auth local add-user <user> /auth local rotate-password <user> (omit password for masked entry) ', C.dim]]));
|
|
123
218
|
|
|
124
219
|
return this.renderList(width, height, {
|
|
125
220
|
title: 'Local Auth Control Room',
|
|
@@ -127,4 +222,29 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
127
222
|
footer: footerLines,
|
|
128
223
|
});
|
|
129
224
|
}
|
|
225
|
+
|
|
226
|
+
private renderMaskedPrompt(width: number, height: number): Line[] {
|
|
227
|
+
const state = this.maskedState!;
|
|
228
|
+
const actionLabel = state.kind === 'add-user' ? 'Add user' : 'Rotate password';
|
|
229
|
+
const dots = '•'.repeat(Math.min(32, state.buffer.length));
|
|
230
|
+
const cursor = '█'; // block cursor
|
|
231
|
+
const maskedDisplay = state.buffer.length > 0 ? `${dots}${cursor}` : cursor;
|
|
232
|
+
|
|
233
|
+
const promptLines: Line[] = [
|
|
234
|
+
buildPanelLine(width, [[` ${actionLabel}: ${state.username}`, C.value]]),
|
|
235
|
+
buildPanelLine(width, [['', C.label]]),
|
|
236
|
+
buildPanelLine(width, [[' Password ', C.label], [maskedDisplay.slice(0, Math.max(0, width - 12)), C.info]]),
|
|
237
|
+
buildPanelLine(width, [['', C.label]]),
|
|
238
|
+
buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const workspace = buildPanelWorkspace(width, height, {
|
|
242
|
+
title: 'Local Auth — Password Entry',
|
|
243
|
+
intro: `Type a password for ${state.username}. The value is never echoed in plaintext or stored in history.`,
|
|
244
|
+
sections: [{ lines: promptLines }],
|
|
245
|
+
palette: C,
|
|
246
|
+
});
|
|
247
|
+
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
248
|
+
return workspace;
|
|
249
|
+
}
|
|
130
250
|
}
|
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
} from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
10
10
|
import type { Line } from '../types/grid.ts';
|
|
11
11
|
import { BasePanel } from './base-panel.ts';
|
|
12
|
+
import { handleConfirmInput, renderConfirmLines, type ConfirmState } from './confirm-state.ts';
|
|
13
|
+
import { isTextBackspace, isTextForwardDelete } from '../input/delete-key-policy.ts';
|
|
12
14
|
import {
|
|
13
15
|
buildBodyText,
|
|
14
16
|
buildEmptyState,
|
|
@@ -69,6 +71,8 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
69
71
|
private scrollOffset = 0;
|
|
70
72
|
private selectedActionIndex = 0;
|
|
71
73
|
private draftAnswer = '';
|
|
74
|
+
// Pending confirmation for Delete (clear draft). Null when inactive.
|
|
75
|
+
private clearDraftConfirm: ConfirmState<'clear-draft'> | null = null;
|
|
72
76
|
|
|
73
77
|
public constructor(options: ProjectPlanningPanelOptions) {
|
|
74
78
|
super('project-planning', 'Planning', 'P', 'agent');
|
|
@@ -86,6 +90,26 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
86
90
|
|
|
87
91
|
public handleInput(key: string): boolean {
|
|
88
92
|
if (this.lastError !== null) this.clearError();
|
|
93
|
+
|
|
94
|
+
// ConfirmState gate: Delete (clear draft) requires y/n confirmation.
|
|
95
|
+
// handleConfirmInput absorbs all keys while a confirmation is pending.
|
|
96
|
+
const confirmResult = handleConfirmInput(this.clearDraftConfirm, key);
|
|
97
|
+
if (confirmResult === 'confirmed') {
|
|
98
|
+
this.draftAnswer = '';
|
|
99
|
+
this.clearDraftConfirm = null;
|
|
100
|
+
this.markDirty();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (confirmResult === 'cancelled') {
|
|
104
|
+
this.clearDraftConfirm = null;
|
|
105
|
+
this.markDirty();
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (confirmResult === 'absorbed') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// confirmResult === 'inactive': proceed with normal dispatch.
|
|
112
|
+
|
|
89
113
|
const question = this.getCurrentQuestion();
|
|
90
114
|
if (question) {
|
|
91
115
|
const actions = this.getAnswerActions(question);
|
|
@@ -104,13 +128,15 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
104
128
|
this.submitSelectedAction(question, actions);
|
|
105
129
|
return true;
|
|
106
130
|
}
|
|
107
|
-
if (key
|
|
131
|
+
if (isTextBackspace(key)) {
|
|
108
132
|
this.draftAnswer = this.draftAnswer.slice(0, -1);
|
|
109
133
|
this.markDirty();
|
|
110
134
|
return true;
|
|
111
135
|
}
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
// 'delete' opens the clear-draft confirmation gate (per delete-key policy).
|
|
137
|
+
// The draft is not wiped until the user confirms with y/Enter.
|
|
138
|
+
if (isTextForwardDelete(key)) {
|
|
139
|
+
this.clearDraftConfirm = { subject: 'clear-draft', label: 'draft answer' };
|
|
114
140
|
this.markDirty();
|
|
115
141
|
return true;
|
|
116
142
|
}
|
|
@@ -244,8 +270,10 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
244
270
|
[' choose answer ', C.dim],
|
|
245
271
|
['type', C.info],
|
|
246
272
|
[' draft ', C.dim],
|
|
247
|
-
['Backspace
|
|
273
|
+
['Backspace', C.info],
|
|
248
274
|
[' edit ', C.dim],
|
|
275
|
+
['Del', C.info],
|
|
276
|
+
[' clear draft ', C.dim],
|
|
249
277
|
['Enter', C.info],
|
|
250
278
|
[' submit Esc prompt focus Ctrl+X close panel', C.dim],
|
|
251
279
|
]),
|
|
@@ -284,6 +312,16 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
284
312
|
private buildQuestionSection(width: number, question: ProjectPlanningQuestion): RenderedPlanningSection {
|
|
285
313
|
const actions = this.getAnswerActions(question);
|
|
286
314
|
this.selectedActionIndex = this.clampActionIndex(actions.length);
|
|
315
|
+
// When a clear-draft confirmation is pending, show the confirm prompt
|
|
316
|
+
// inline above the draft line instead of the normal content.
|
|
317
|
+
if (this.clearDraftConfirm) {
|
|
318
|
+
const confirmLines = renderConfirmLines(width, this.clearDraftConfirm);
|
|
319
|
+
const lines: Line[] = [
|
|
320
|
+
...buildBodyText(width, question.prompt, C, C.planning),
|
|
321
|
+
...confirmLines,
|
|
322
|
+
];
|
|
323
|
+
return { title: 'Answer Current Question', lines, selectedLineIndex: undefined };
|
|
324
|
+
}
|
|
287
325
|
const lines: Line[] = [
|
|
288
326
|
...buildBodyText(width, question.prompt, C, C.planning),
|
|
289
327
|
];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
2
|
+
|
|
1
3
|
export type PanelSearchFocusTransition = 'focus-search' | 'focus-list' | null;
|
|
2
4
|
|
|
3
5
|
export function getPanelSearchFocusTransition(
|
|
@@ -6,25 +8,29 @@ export function getPanelSearchFocusTransition(
|
|
|
6
8
|
): PanelSearchFocusTransition {
|
|
7
9
|
const focusKeys = options.focusKeys ?? ['/'];
|
|
8
10
|
if (focusKeys.includes(key)) return 'focus-search';
|
|
9
|
-
if (
|
|
11
|
+
if (key === 'up' && options.selectedIndex <= 0) {
|
|
10
12
|
return 'focus-search';
|
|
11
13
|
}
|
|
12
|
-
if (
|
|
14
|
+
if (key === 'down' && options.itemCount > 0) {
|
|
13
15
|
return 'focus-list';
|
|
14
16
|
}
|
|
15
17
|
return null;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
// Panel search filters are end-anchored (no moveable cursor).
|
|
21
|
+
// Per the delete-key policy (src/input/delete-key-policy.ts):
|
|
22
|
+
// 'backspace' removes the last character.
|
|
23
|
+
// 'delete' is a no-op — there is no cursor, so forward-delete is meaningless.
|
|
18
24
|
export function isPanelSearchBackspace(key: string): boolean {
|
|
19
|
-
return key
|
|
25
|
+
return isTextBackspace(key);
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export function isPanelSearchCancel(key: string): boolean {
|
|
23
|
-
return key === 'escape'
|
|
29
|
+
return key === 'escape';
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export function isPanelSearchCommit(key: string): boolean {
|
|
27
|
-
return key === 'return' || key === 'enter'
|
|
33
|
+
return key === 'return' || key === 'enter';
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export function isPanelSearchPrintable(key: string): boolean {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
+
import type { KeyName } from './types.ts';
|
|
4
5
|
import type { ProviderSubscription, PendingSubscriptionLogin } from '@pellux/goodvibes-sdk/platform/config';
|
|
5
6
|
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
7
|
+
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
6
8
|
import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
|
|
7
9
|
import {
|
|
8
10
|
buildEmptyState,
|
|
@@ -57,7 +59,8 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
57
59
|
private readonly serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
|
|
58
60
|
private readonly subscriptionManager: SubscriptionAccessQuery;
|
|
59
61
|
private rows: SubscriptionRow[] = [];
|
|
60
|
-
|
|
62
|
+
/** Pending logout confirmation — uses project-standard ConfirmState contract. */
|
|
63
|
+
private confirm: ConfirmState<string> | null = null;
|
|
61
64
|
|
|
62
65
|
public constructor(
|
|
63
66
|
serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>,
|
|
@@ -98,42 +101,47 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
98
101
|
], C, { selected, selectedBg: C.selectedBg });
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
public handleInput(key:
|
|
104
|
+
public handleInput(key: KeyName): boolean {
|
|
105
|
+
// Project-standard confirm contract: Enter/y confirm; n/Esc cancel; other absorbed.
|
|
106
|
+
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
107
|
+
if (confirmResult === 'confirmed') {
|
|
108
|
+
const provider = this.confirm!.subject;
|
|
109
|
+
this.confirm = null;
|
|
110
|
+
this.subscriptionManager.logout(provider);
|
|
111
|
+
this.refresh();
|
|
112
|
+
this.markDirty();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (confirmResult === 'cancelled') {
|
|
116
|
+
this.confirm = null;
|
|
117
|
+
this.markDirty();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (confirmResult === 'absorbed') return true;
|
|
121
|
+
|
|
102
122
|
if (this.rows.length === 0) return false;
|
|
103
123
|
const selected = this.rows[this.selectedIndex] ?? null;
|
|
104
|
-
if (key === '
|
|
124
|
+
if (key === 'up' || key === 'k') {
|
|
105
125
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
106
|
-
this.
|
|
126
|
+
this.confirm = null;
|
|
107
127
|
this.markDirty();
|
|
108
128
|
return true;
|
|
109
129
|
}
|
|
110
|
-
if (key === '
|
|
130
|
+
if (key === 'down' || key === 'j') {
|
|
111
131
|
this.selectedIndex = Math.min(this.rows.length - 1, this.selectedIndex + 1);
|
|
112
|
-
this.
|
|
132
|
+
this.confirm = null;
|
|
113
133
|
this.markDirty();
|
|
114
134
|
return true;
|
|
115
135
|
}
|
|
116
|
-
if (key === 'enter' || key === '
|
|
136
|
+
if (key === 'enter' || key === 'return') {
|
|
117
137
|
if (!selected?.subscription) return false;
|
|
118
|
-
|
|
119
|
-
this.logoutConfirmationTarget = selected.provider;
|
|
120
|
-
this.markDirty();
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
this.subscriptionManager.logout(selected.provider);
|
|
124
|
-
this.logoutConfirmationTarget = null;
|
|
125
|
-
this.refresh();
|
|
126
|
-
this.markDirty();
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
if ((key === 'n' || key === 'escape') && this.logoutConfirmationTarget) {
|
|
130
|
-
this.logoutConfirmationTarget = null;
|
|
138
|
+
this.confirm = { subject: selected.provider, label: selected.provider };
|
|
131
139
|
this.markDirty();
|
|
132
140
|
return true;
|
|
133
141
|
}
|
|
134
142
|
if (key === 'r') {
|
|
135
143
|
this.refresh();
|
|
136
|
-
this.
|
|
144
|
+
this.confirm = null;
|
|
137
145
|
this.markDirty();
|
|
138
146
|
return true;
|
|
139
147
|
}
|
|
@@ -206,7 +214,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
206
214
|
sections: [{ lines: [...summaryLines, ...emptyLines] }],
|
|
207
215
|
footerLines: [
|
|
208
216
|
buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
|
|
209
|
-
buildPanelLine(width, [[' Up/Down move Enter
|
|
217
|
+
buildPanelLine(width, [[' Up/Down move Enter sign out selected provider y/Esc confirm/cancel r refresh', C.dim]]),
|
|
210
218
|
],
|
|
211
219
|
palette: C,
|
|
212
220
|
});
|
|
@@ -236,8 +244,8 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
236
244
|
: 'Stored for subscription-backed flows. Ambient API-key resolution remains unchanged.'}`,
|
|
237
245
|
C.dim,
|
|
238
246
|
]]));
|
|
239
|
-
if (this.
|
|
240
|
-
detailRows.push(buildPanelLine(width, [[` Press
|
|
247
|
+
if (this.confirm?.subject === selectedRow.provider) {
|
|
248
|
+
detailRows.push(buildPanelLine(width, [[` Sign out ${selectedRow.provider}? Press y or Enter to confirm, n or Esc to cancel.`, C.warn]]));
|
|
241
249
|
}
|
|
242
250
|
} else if (selectedRow.pending) {
|
|
243
251
|
detailRows.push(buildPanelLine(width, [[' Login is pending. Finish with /subscription login <provider> finish <code>.', C.warn]]));
|
|
@@ -256,7 +264,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
256
264
|
footer: [
|
|
257
265
|
...detailRows,
|
|
258
266
|
buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
|
|
259
|
-
buildPanelLine(width, [[' Up/Down move Enter
|
|
267
|
+
buildPanelLine(width, [[' Up/Down move Enter sign out selected provider y/Esc confirm/cancel r refresh', C.dim]]),
|
|
260
268
|
],
|
|
261
269
|
});
|
|
262
270
|
}
|
package/src/panels/types.ts
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import type { ComponentResourceContract, ComponentHealthState } from '../runtime/perf/panel-contracts.ts';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Named logical key identifiers emitted by the input tokenizer.
|
|
6
|
+
* These are the ONLY key names that will appear in `handleInput` calls;
|
|
7
|
+
* the tokenizer never emits DOM/browser-style names like 'ArrowUp' or 'Enter'.
|
|
8
|
+
*
|
|
9
|
+
* Printable single-character input is passed through as-is; the `string & {}`
|
|
10
|
+
* fallback preserves that handling while making named-key completions discoverable
|
|
11
|
+
* in editors and keeping non-null single-character values compatible.
|
|
12
|
+
*/
|
|
13
|
+
export type NamedKey =
|
|
14
|
+
| 'up' | 'down' | 'left' | 'right'
|
|
15
|
+
| 'home' | 'end' | 'pageup' | 'pagedown'
|
|
16
|
+
| 'insert' | 'delete' | 'backspace'
|
|
17
|
+
| 'enter' | 'return' | 'escape' | 'space' | 'tab'
|
|
18
|
+
| 'f1' | 'f2' | 'f3' | 'f4' | 'f5' | 'f6'
|
|
19
|
+
| 'f7' | 'f8' | 'f9' | 'f10' | 'f11' | 'f12';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The full key type accepted by `Panel.handleInput`.
|
|
23
|
+
*
|
|
24
|
+
* Named keys (arrow keys, modifiers, function keys) are represented by
|
|
25
|
+
* lowercase `NamedKey` members. Single printable characters are passed
|
|
26
|
+
* verbatim via the `string & {}` escape hatch so panels can handle 'j', 'k',
|
|
27
|
+
* 'r', etc. without losing type safety on the named members.
|
|
28
|
+
*/
|
|
29
|
+
export type KeyName = NamedKey | (string & {});
|
|
30
|
+
|
|
4
31
|
export type PanelCategory = 'development' | 'agent' | 'monitoring' | 'session' | 'ai';
|
|
5
32
|
|
|
6
33
|
export interface Panel {
|
|
@@ -35,7 +62,7 @@ export interface Panel {
|
|
|
35
62
|
healthState?: Readonly<ComponentHealthState>;
|
|
36
63
|
|
|
37
64
|
// Input (optional)
|
|
38
|
-
handleInput?(key:
|
|
65
|
+
handleInput?(key: KeyName): boolean;
|
|
39
66
|
|
|
40
67
|
// Scroll input (optional)
|
|
41
68
|
// Positive delta scrolls down; negative delta scrolls up.
|