@pellux/goodvibes-tui 0.28.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 +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- 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-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/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- 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/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 +203 -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 +309 -128
- 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/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 +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- 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/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- 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/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- 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 -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/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -5,6 +5,7 @@ import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
|
5
5
|
import {
|
|
6
6
|
buildDetailBlock,
|
|
7
7
|
buildGuidanceLine,
|
|
8
|
+
buildKeyboardHints,
|
|
8
9
|
buildPanelListRow,
|
|
9
10
|
buildPanelLine,
|
|
10
11
|
buildSummaryBlock,
|
|
@@ -17,14 +18,11 @@ import type { LocalAuthInspectionQuery } from '../runtime/ui-service-queries.ts'
|
|
|
17
18
|
import type { KeyName } from './types.ts';
|
|
18
19
|
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
19
20
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
21
|
+
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
warn: '#eab308',
|
|
25
|
-
error: '#ef4444',
|
|
26
|
-
selectBg: '#1e293b',
|
|
27
|
-
} as const;
|
|
23
|
+
// Base chrome only — state colors and text tokens come straight from
|
|
24
|
+
// DEFAULT_PANEL_PALETTE (WO-002).
|
|
25
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
28
26
|
|
|
29
27
|
function formatRoles(roles: readonly string[]): string {
|
|
30
28
|
return roles.length > 0 ? roles.join(', ') : '(none)';
|
|
@@ -42,16 +40,47 @@ interface MaskedEntryState {
|
|
|
42
40
|
buffer: string;
|
|
43
41
|
}
|
|
44
42
|
|
|
43
|
+
/** Mutation surface the panel needs for p/a/d/b — a subset of UserAuthManager. */
|
|
44
|
+
type LocalAuthMutations = Pick<UserAuthManager, 'addUser' | 'deleteUser' | 'rotatePassword' | 'clearBootstrapCredentialFile'>;
|
|
45
|
+
|
|
46
|
+
function hasLocalAuthMutations(value: LocalAuthInspectionQuery): value is LocalAuthInspectionQuery & LocalAuthMutations {
|
|
47
|
+
const candidate = value as Partial<LocalAuthMutations>;
|
|
48
|
+
return typeof candidate.addUser === 'function'
|
|
49
|
+
&& typeof candidate.deleteUser === 'function'
|
|
50
|
+
&& typeof candidate.rotatePassword === 'function'
|
|
51
|
+
&& typeof candidate.clearBootstrapCredentialFile === 'function';
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
46
55
|
private readonly authManager: LocalAuthInspectionQuery;
|
|
47
56
|
private maskedState: MaskedEntryState | null = null;
|
|
57
|
+
/** Pending delete-user confirmation — project-standard ConfirmState contract. */
|
|
58
|
+
private deleteConfirm: ConfirmState<string> | null = null;
|
|
59
|
+
/** Draft username buffer for the 'a' add-user flow's first step (username,
|
|
60
|
+
* then openMaskedEntry for the password). Not masked — usernames aren't secret. */
|
|
61
|
+
private usernameEntry: string | null = null;
|
|
62
|
+
/** Cached inspect() snapshot for the current render pass — see render(). */
|
|
63
|
+
private cachedSnapshot: LocalAuthSnapshot | null = null;
|
|
48
64
|
|
|
49
65
|
public constructor(authManager: LocalAuthInspectionQuery) {
|
|
50
|
-
super('local-auth', 'Local Auth', 'U', '
|
|
66
|
+
super('local-auth', 'Local Auth', 'U', 'security-policy');
|
|
51
67
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
52
68
|
this.authManager = authManager;
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The full mutation surface (addUser/deleteUser/rotatePassword/
|
|
73
|
+
* clearBootstrapCredentialFile), when the constructor's authManager
|
|
74
|
+
* happens to be the real UserAuthManager rather than the narrower
|
|
75
|
+
* read-only LocalAuthInspectionQuery some callers (and tests) supply.
|
|
76
|
+
* Real production wiring (builtin/operations.ts) always passes the full
|
|
77
|
+
* manager; test doubles that implement only inspect() correctly get null
|
|
78
|
+
* here, and the p/a/d/b keys surface a clear error instead of throwing.
|
|
79
|
+
*/
|
|
80
|
+
private get mutations(): LocalAuthMutations | null {
|
|
81
|
+
return hasLocalAuthMutations(this.authManager) ? this.authManager : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
/**
|
|
56
85
|
* Activate masked password-entry mode for the given operation.
|
|
57
86
|
* The panel's handleInput will capture keystrokes into a private buffer;
|
|
@@ -68,13 +97,143 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
68
97
|
}
|
|
69
98
|
|
|
70
99
|
public override handleInput(key: KeyName): boolean {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
// Masked entry takes priority when active — it must capture every
|
|
101
|
+
// keystroke (including letters that would otherwise be p/a/d/b actions)
|
|
102
|
+
// as part of the password being typed.
|
|
103
|
+
if (this.maskedState !== null) {
|
|
104
|
+
return this.handleMaskedEntryInput(key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.usernameEntry !== null) {
|
|
108
|
+
return this.handleUsernameEntryInput(key);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.deleteConfirm !== null) {
|
|
112
|
+
return this.handleDeleteConfirmInput(key);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const selected = this.getSelectedItem();
|
|
116
|
+
|
|
117
|
+
if (key === 'p') {
|
|
118
|
+
if (!selected) return false;
|
|
119
|
+
const mutations = this.mutations;
|
|
120
|
+
if (!mutations) {
|
|
121
|
+
this.setError('Local auth mutations are not available in this session.');
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
this.openMaskedEntry('rotate-password', selected.username, mutations as unknown as UserAuthManager);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (key === 'a') {
|
|
129
|
+
const mutations = this.mutations;
|
|
130
|
+
if (!mutations) {
|
|
131
|
+
this.setError('Local auth mutations are not available in this session.');
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
this.usernameEntry = '';
|
|
135
|
+
this.invalidate();
|
|
136
|
+
return true;
|
|
74
137
|
}
|
|
75
138
|
|
|
139
|
+
if (key === 'd') {
|
|
140
|
+
if (!selected) return false;
|
|
141
|
+
this.deleteConfirm = { subject: selected.username, label: selected.username };
|
|
142
|
+
this.invalidate();
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (key === 'b') {
|
|
147
|
+
const snapshot = this.cachedSnapshot ?? this.authManager.inspect();
|
|
148
|
+
if (!snapshot.bootstrapCredentialPresent) return false;
|
|
149
|
+
const mutations = this.mutations;
|
|
150
|
+
if (!mutations) {
|
|
151
|
+
this.setError('Local auth mutations are not available in this session.');
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const cleared = mutations.clearBootstrapCredentialFile();
|
|
155
|
+
if (cleared) {
|
|
156
|
+
this.cachedSnapshot = null; // force a fresh inspect() on the next render
|
|
157
|
+
} else {
|
|
158
|
+
this.setError('No bootstrap credential file was present.');
|
|
159
|
+
}
|
|
160
|
+
this.invalidate();
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Delegate scroll/selection to ScrollableListPanel for everything else.
|
|
165
|
+
return super.handleInput?.(key) ?? false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private handleDeleteConfirmInput(key: KeyName): boolean {
|
|
169
|
+
const result = handleConfirmInput(this.deleteConfirm, key);
|
|
170
|
+
if (result === 'confirmed') {
|
|
171
|
+
const username = this.deleteConfirm!.subject;
|
|
172
|
+
this.deleteConfirm = null;
|
|
173
|
+
const mutations = this.mutations;
|
|
174
|
+
if (mutations) {
|
|
175
|
+
try {
|
|
176
|
+
const deleted = mutations.deleteUser(username);
|
|
177
|
+
if (deleted) this.cachedSnapshot = null; // force a fresh inspect() on the next render
|
|
178
|
+
else this.setError(`Unknown local auth user: ${username}`);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
this.setError(summarizeError(error));
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
this.setError('Local auth mutations are not available in this session.');
|
|
184
|
+
}
|
|
185
|
+
this.invalidate();
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (result === 'cancelled') {
|
|
189
|
+
this.deleteConfirm = null;
|
|
190
|
+
this.invalidate();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
// 'absorbed' (or defensively, 'inactive' — deleteConfirm is non-null here so this never fires)
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private handleUsernameEntryInput(key: KeyName): boolean {
|
|
198
|
+
if (key === 'escape') {
|
|
199
|
+
this.usernameEntry = null;
|
|
200
|
+
this.invalidate();
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
if (key === 'enter' || key === 'return') {
|
|
204
|
+
const username = (this.usernameEntry ?? '').trim();
|
|
205
|
+
this.usernameEntry = null;
|
|
206
|
+
if (username.length === 0) {
|
|
207
|
+
this.invalidate();
|
|
208
|
+
return true; // no-op on empty username, mirroring the masked-entry empty-buffer no-op
|
|
209
|
+
}
|
|
210
|
+
const mutations = this.mutations;
|
|
211
|
+
if (mutations) {
|
|
212
|
+
this.openMaskedEntry('add-user', username, mutations as unknown as UserAuthManager);
|
|
213
|
+
} else {
|
|
214
|
+
this.setError('Local auth mutations are not available in this session.');
|
|
215
|
+
this.invalidate();
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (isTextBackspace(key)) {
|
|
220
|
+
if (this.usernameEntry && this.usernameEntry.length > 0) {
|
|
221
|
+
this.usernameEntry = this.usernameEntry.slice(0, -1);
|
|
222
|
+
this.invalidate();
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (key.length === 1) {
|
|
227
|
+
this.usernameEntry = (this.usernameEntry ?? '') + key;
|
|
228
|
+
this.invalidate();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return true; // absorb everything else while username entry is active
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private handleMaskedEntryInput(key: KeyName): boolean {
|
|
76
235
|
// Masked entry is active — capture all keystrokes.
|
|
77
|
-
const state = this.maskedState
|
|
236
|
+
const state = this.maskedState!;
|
|
78
237
|
|
|
79
238
|
if (key === 'escape') {
|
|
80
239
|
// Cancel: discard buffer, exit masked mode without persisting.
|
|
@@ -132,7 +291,10 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
132
291
|
}
|
|
133
292
|
|
|
134
293
|
protected getItems(): readonly LocalAuthUser[] {
|
|
135
|
-
|
|
294
|
+
// Reads the render-pass cache populated by render() below (single
|
|
295
|
+
// inspect() per render). Falls back to a fresh inspect() only when
|
|
296
|
+
// called before the first render (e.g. directly from a test).
|
|
297
|
+
return (this.cachedSnapshot ?? this.authManager.inspect()).users;
|
|
136
298
|
}
|
|
137
299
|
|
|
138
300
|
protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
|
|
@@ -147,19 +309,26 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
147
309
|
}
|
|
148
310
|
|
|
149
311
|
public render(width: number, height: number): Line[] {
|
|
150
|
-
// When masked entry is active, render a dedicated
|
|
151
|
-
// normal panel content. No plaintext password
|
|
312
|
+
// When masked entry or username entry is active, render a dedicated
|
|
313
|
+
// prompt instead of the normal panel content. No plaintext password
|
|
314
|
+
// appears anywhere in output.
|
|
152
315
|
if (this.maskedState !== null) {
|
|
153
316
|
return this.renderMaskedPrompt(width, height);
|
|
154
317
|
}
|
|
318
|
+
if (this.usernameEntry !== null) {
|
|
319
|
+
return this.renderUsernameEntryPrompt(width, height);
|
|
320
|
+
}
|
|
155
321
|
|
|
156
322
|
const intro = 'Manage local daemon and HTTP-listener auth users, bootstrap state, and active sessions.';
|
|
157
|
-
|
|
323
|
+
// Single inspect() call for this whole render pass — getItems() (called
|
|
324
|
+
// both directly below and again inside renderList()'s internal
|
|
325
|
+
// getVisibleItems()) reads from this cache instead of re-inspecting.
|
|
326
|
+
this.cachedSnapshot = this.authManager.inspect();
|
|
327
|
+
const snapshot = this.cachedSnapshot;
|
|
158
328
|
const users = this.getItems();
|
|
159
329
|
|
|
160
330
|
const issueMessages: string[] = [];
|
|
161
|
-
if (snapshot.bootstrapCredentialPresent) issueMessages.push('Bootstrap credential file still exists
|
|
162
|
-
if (snapshot.userCount <= 1) issueMessages.push('Only one local auth user is configured.');
|
|
331
|
+
if (snapshot.bootstrapCredentialPresent) issueMessages.push('Bootstrap credential file still exists — press b to clear it after password rotation.');
|
|
163
332
|
if (snapshot.sessionCount === 0) issueMessages.push('No active local auth sessions are currently tracked.');
|
|
164
333
|
|
|
165
334
|
const headerLines: Line[] = [
|
|
@@ -177,7 +346,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
177
346
|
...(issueMessages.length > 0
|
|
178
347
|
? issueMessages.map((issue) => buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.warn]]))
|
|
179
348
|
: [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
|
|
180
|
-
buildGuidanceLine(width, '/
|
|
349
|
+
buildGuidanceLine(width, 'p / a / d', 'rotate password, add user, or delete the selected user via masked in-panel entry', C),
|
|
181
350
|
], C),
|
|
182
351
|
];
|
|
183
352
|
|
|
@@ -186,22 +355,25 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
186
355
|
title: 'Local Auth Control Room',
|
|
187
356
|
intro,
|
|
188
357
|
sections: [{ lines: headerLines }],
|
|
358
|
+
footerLines: [this.buildHintsLine(width, null)],
|
|
189
359
|
palette: C,
|
|
190
360
|
});
|
|
191
361
|
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
192
|
-
return workspace;
|
|
362
|
+
return workspace.slice(0, height);
|
|
193
363
|
}
|
|
194
364
|
|
|
195
365
|
this.clampSelection();
|
|
196
|
-
const selected =
|
|
366
|
+
const selected = this.getSelectedItem();
|
|
197
367
|
|
|
198
368
|
const footerLines: Line[] = [];
|
|
199
369
|
if (selected) {
|
|
370
|
+
const deleteActive = this.deleteConfirm?.subject === selected.username;
|
|
200
371
|
footerLines.push(
|
|
201
372
|
...buildDetailBlock(width, 'Selected user', [
|
|
202
373
|
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [truncateDisplay(formatRoles(selected.roles), Math.max(0, width - 23)), C.info]]),
|
|
203
|
-
|
|
204
|
-
|
|
374
|
+
deleteActive
|
|
375
|
+
? buildPanelLine(width, [[` Delete ${selected.username}? Press y or Enter to confirm, n or Esc to cancel.`, C.warn]])
|
|
376
|
+
: buildPanelLine(width, [[' p: rotate password a: add user d: delete user', C.dim]]),
|
|
205
377
|
], C),
|
|
206
378
|
);
|
|
207
379
|
}
|
|
@@ -216,19 +388,59 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
216
388
|
])),
|
|
217
389
|
);
|
|
218
390
|
}
|
|
219
|
-
footerLines.push(buildPanelLine(width, [[' /auth local review /auth local add-user <user> /auth local rotate-password <user> (omit password for masked entry) ', C.dim]]));
|
|
220
391
|
|
|
221
392
|
return this.renderList(width, height, {
|
|
222
393
|
title: 'Local Auth Control Room',
|
|
223
394
|
header: headerLines,
|
|
224
395
|
footer: footerLines,
|
|
225
|
-
hints:
|
|
226
|
-
{ keys: '↑/↓', label: 'select user' },
|
|
227
|
-
{ keys: '/auth local rotate-password', label: 'masked entry' },
|
|
228
|
-
],
|
|
396
|
+
hints: this.buildHints(selected ?? null, snapshot.bootstrapCredentialPresent),
|
|
229
397
|
});
|
|
230
398
|
}
|
|
231
399
|
|
|
400
|
+
/** Footer keyboard hints, adapted to the current state — confirm-pending
|
|
401
|
+
* vs. normal browsing, and whether there's a bootstrap credential to clear. */
|
|
402
|
+
private buildHints(
|
|
403
|
+
selected: LocalAuthUser | null,
|
|
404
|
+
bootstrapCredentialPresent: boolean,
|
|
405
|
+
): ReadonlyArray<{ keys: string; label: string }> {
|
|
406
|
+
if (this.deleteConfirm) {
|
|
407
|
+
return [
|
|
408
|
+
{ keys: 'y/Enter', label: 'confirm delete' },
|
|
409
|
+
{ keys: 'n/Esc', label: 'cancel' },
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
const hints: Array<{ keys: string; label: string }> = [{ keys: '↑/↓', label: 'select user' }];
|
|
413
|
+
if (selected) {
|
|
414
|
+
hints.push({ keys: 'p', label: 'rotate password' }, { keys: 'd', label: 'delete user' });
|
|
415
|
+
}
|
|
416
|
+
hints.push({ keys: 'a', label: 'add user' });
|
|
417
|
+
if (bootstrapCredentialPresent) hints.push({ keys: 'b', label: 'clear bootstrap credential' });
|
|
418
|
+
return hints;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private buildHintsLine(width: number, selected: LocalAuthUser | null): Line {
|
|
422
|
+
return buildKeyboardHints(width, this.buildHints(selected, false), C);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private renderUsernameEntryPrompt(width: number, height: number): Line[] {
|
|
426
|
+
const draft = this.usernameEntry ?? '';
|
|
427
|
+
const promptLines: Line[] = [
|
|
428
|
+
buildPanelLine(width, [[' Add local auth user — enter a username, then set a masked password.', C.value]]),
|
|
429
|
+
buildPanelLine(width, [['', C.label]]),
|
|
430
|
+
buildPanelLine(width, [[' Username ', C.label], [`${draft}█`, C.info]]),
|
|
431
|
+
buildPanelLine(width, [['', C.label]]),
|
|
432
|
+
buildPanelLine(width, [[' [Enter] Continue to password entry [Esc] Cancel [Backspace] Delete char', C.dim]]),
|
|
433
|
+
];
|
|
434
|
+
const workspace = buildPanelWorkspace(width, height, {
|
|
435
|
+
title: 'Local Auth — Add User',
|
|
436
|
+
intro: 'Type the new username and press Enter to continue to masked password entry.',
|
|
437
|
+
sections: [{ lines: promptLines }],
|
|
438
|
+
palette: C,
|
|
439
|
+
});
|
|
440
|
+
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
441
|
+
return workspace.slice(0, height);
|
|
442
|
+
}
|
|
443
|
+
|
|
232
444
|
private renderMaskedPrompt(width: number, height: number): Line[] {
|
|
233
445
|
const state = this.maskedState!;
|
|
234
446
|
const actionLabel = state.kind === 'add-user' ? 'Add user' : 'Rotate password';
|