@pellux/goodvibes-tui 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
5
|
import {
|
|
5
6
|
buildDetailBlock,
|
|
6
7
|
buildGuidanceLine,
|
|
8
|
+
buildKeyboardHints,
|
|
7
9
|
buildPanelListRow,
|
|
8
10
|
buildPanelLine,
|
|
9
11
|
buildSummaryBlock,
|
|
@@ -16,14 +18,11 @@ import type { LocalAuthInspectionQuery } from '../runtime/ui-service-queries.ts'
|
|
|
16
18
|
import type { KeyName } from './types.ts';
|
|
17
19
|
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
18
20
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
21
|
+
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
warn: '#eab308',
|
|
24
|
-
error: '#ef4444',
|
|
25
|
-
selectBg: '#1e293b',
|
|
26
|
-
} 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;
|
|
27
26
|
|
|
28
27
|
function formatRoles(roles: readonly string[]): string {
|
|
29
28
|
return roles.length > 0 ? roles.join(', ') : '(none)';
|
|
@@ -41,16 +40,47 @@ interface MaskedEntryState {
|
|
|
41
40
|
buffer: string;
|
|
42
41
|
}
|
|
43
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
|
+
|
|
44
54
|
export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
45
55
|
private readonly authManager: LocalAuthInspectionQuery;
|
|
46
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;
|
|
47
64
|
|
|
48
65
|
public constructor(authManager: LocalAuthInspectionQuery) {
|
|
49
|
-
super('local-auth', 'Local Auth', 'U', '
|
|
66
|
+
super('local-auth', 'Local Auth', 'U', 'security-policy');
|
|
50
67
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
51
68
|
this.authManager = authManager;
|
|
52
69
|
}
|
|
53
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
|
+
|
|
54
84
|
/**
|
|
55
85
|
* Activate masked password-entry mode for the given operation.
|
|
56
86
|
* The panel's handleInput will capture keystrokes into a private buffer;
|
|
@@ -67,13 +97,143 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
public override handleInput(key: KeyName): boolean {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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;
|
|
73
137
|
}
|
|
74
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 {
|
|
75
235
|
// Masked entry is active — capture all keystrokes.
|
|
76
|
-
const state = this.maskedState
|
|
236
|
+
const state = this.maskedState!;
|
|
77
237
|
|
|
78
238
|
if (key === 'escape') {
|
|
79
239
|
// Cancel: discard buffer, exit masked mode without persisting.
|
|
@@ -131,13 +291,16 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
131
291
|
}
|
|
132
292
|
|
|
133
293
|
protected getItems(): readonly LocalAuthUser[] {
|
|
134
|
-
|
|
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;
|
|
135
298
|
}
|
|
136
299
|
|
|
137
300
|
protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
|
|
138
301
|
return buildPanelListRow(width, [
|
|
139
|
-
{ text: user.username
|
|
140
|
-
{ text: ` roles=${formatRoles(user.roles)}
|
|
302
|
+
{ text: fitDisplay(user.username, 20), fg: C.value },
|
|
303
|
+
{ text: truncateDisplay(` roles=${formatRoles(user.roles)}`, Math.max(0, width - 24)), fg: C.info },
|
|
141
304
|
], C, { selected });
|
|
142
305
|
}
|
|
143
306
|
|
|
@@ -146,19 +309,26 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
146
309
|
}
|
|
147
310
|
|
|
148
311
|
public render(width: number, height: number): Line[] {
|
|
149
|
-
// When masked entry is active, render a dedicated
|
|
150
|
-
// 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.
|
|
151
315
|
if (this.maskedState !== null) {
|
|
152
316
|
return this.renderMaskedPrompt(width, height);
|
|
153
317
|
}
|
|
318
|
+
if (this.usernameEntry !== null) {
|
|
319
|
+
return this.renderUsernameEntryPrompt(width, height);
|
|
320
|
+
}
|
|
154
321
|
|
|
155
322
|
const intro = 'Manage local daemon and HTTP-listener auth users, bootstrap state, and active sessions.';
|
|
156
|
-
|
|
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;
|
|
157
328
|
const users = this.getItems();
|
|
158
329
|
|
|
159
330
|
const issueMessages: string[] = [];
|
|
160
|
-
if (snapshot.bootstrapCredentialPresent) issueMessages.push('Bootstrap credential file still exists
|
|
161
|
-
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.');
|
|
162
332
|
if (snapshot.sessionCount === 0) issueMessages.push('No active local auth sessions are currently tracked.');
|
|
163
333
|
|
|
164
334
|
const headerLines: Line[] = [
|
|
@@ -171,12 +341,12 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
171
341
|
[' bootstrap ', C.label],
|
|
172
342
|
[snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
|
|
173
343
|
]),
|
|
174
|
-
buildPanelLine(width, [[' user store ', C.label], [snapshot.userStorePath
|
|
175
|
-
buildPanelLine(width, [[' bootstrap file ', C.label], [snapshot.bootstrapCredentialPath
|
|
344
|
+
buildPanelLine(width, [[' user store ', C.label], [truncateDisplay(snapshot.userStorePath, Math.max(0, width - 13)), C.dim]]),
|
|
345
|
+
buildPanelLine(width, [[' bootstrap file ', C.label], [truncateDisplay(snapshot.bootstrapCredentialPath, Math.max(0, width - 18)), C.dim]]),
|
|
176
346
|
...(issueMessages.length > 0
|
|
177
|
-
? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}
|
|
347
|
+
? issueMessages.map((issue) => buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.warn]]))
|
|
178
348
|
: [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
|
|
179
|
-
buildGuidanceLine(width, '/
|
|
349
|
+
buildGuidanceLine(width, 'p / a / d', 'rotate password, add user, or delete the selected user via masked in-panel entry', C),
|
|
180
350
|
], C),
|
|
181
351
|
];
|
|
182
352
|
|
|
@@ -185,44 +355,92 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
185
355
|
title: 'Local Auth Control Room',
|
|
186
356
|
intro,
|
|
187
357
|
sections: [{ lines: headerLines }],
|
|
358
|
+
footerLines: [this.buildHintsLine(width, null)],
|
|
188
359
|
palette: C,
|
|
189
360
|
});
|
|
190
361
|
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
191
|
-
return workspace;
|
|
362
|
+
return workspace.slice(0, height);
|
|
192
363
|
}
|
|
193
364
|
|
|
194
365
|
this.clampSelection();
|
|
195
|
-
const selected =
|
|
366
|
+
const selected = this.getSelectedItem();
|
|
196
367
|
|
|
197
368
|
const footerLines: Line[] = [];
|
|
198
369
|
if (selected) {
|
|
370
|
+
const deleteActive = this.deleteConfirm?.subject === selected.username;
|
|
199
371
|
footerLines.push(
|
|
200
372
|
...buildDetailBlock(width, 'Selected user', [
|
|
201
|
-
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles)
|
|
202
|
-
|
|
203
|
-
|
|
373
|
+
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [truncateDisplay(formatRoles(selected.roles), Math.max(0, width - 23)), C.info]]),
|
|
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]]),
|
|
204
377
|
], C),
|
|
205
378
|
);
|
|
206
379
|
}
|
|
207
380
|
|
|
208
381
|
if (snapshot.sessions.length > 0) {
|
|
382
|
+
footerLines.push(buildPanelLine(width, [[` Active sessions (${snapshot.sessions.length})`, C.label]]));
|
|
209
383
|
footerLines.push(
|
|
210
384
|
...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
|
|
211
385
|
[' ', C.label],
|
|
212
|
-
[session.username
|
|
213
|
-
[` expires ${new Date(session.expiresAt).toLocaleString()}
|
|
386
|
+
[fitDisplay(session.username, 18), C.value],
|
|
387
|
+
[truncateDisplay(` expires ${new Date(session.expiresAt).toLocaleString()}`, Math.max(0, width - 20)), C.dim],
|
|
214
388
|
])),
|
|
215
389
|
);
|
|
216
390
|
}
|
|
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]]));
|
|
218
391
|
|
|
219
392
|
return this.renderList(width, height, {
|
|
220
393
|
title: 'Local Auth Control Room',
|
|
221
394
|
header: headerLines,
|
|
222
395
|
footer: footerLines,
|
|
396
|
+
hints: this.buildHints(selected ?? null, snapshot.bootstrapCredentialPresent),
|
|
223
397
|
});
|
|
224
398
|
}
|
|
225
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
|
+
|
|
226
444
|
private renderMaskedPrompt(width: number, height: number): Line[] {
|
|
227
445
|
const state = this.maskedState!;
|
|
228
446
|
const actionLabel = state.kind === 'add-user' ? 'Add user' : 'Rotate password';
|
|
@@ -233,7 +451,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
233
451
|
const promptLines: Line[] = [
|
|
234
452
|
buildPanelLine(width, [[` ${actionLabel}: ${state.username}`, C.value]]),
|
|
235
453
|
buildPanelLine(width, [['', C.label]]),
|
|
236
|
-
buildPanelLine(width, [[' Password ', C.label], [maskedDisplay
|
|
454
|
+
buildPanelLine(width, [[' Password ', C.label], [truncateDisplay(maskedDisplay, Math.max(0, width - 12)), C.info]]),
|
|
237
455
|
buildPanelLine(width, [['', C.label]]),
|
|
238
456
|
buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
|
|
239
457
|
];
|