@pellux/goodvibes-tui 0.20.3 → 0.22.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.
Files changed (142) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +23 -2
  3. package/docs/foundation-artifacts/operator-contract.json +78 -1
  4. package/package.json +4 -2
  5. package/src/audio/spoken-turn-controller.ts +31 -1
  6. package/src/audio/spoken-turn-wiring.ts +26 -4
  7. package/src/cli/bundle-command.ts +1 -1
  8. package/src/cli/completions/generate.ts +658 -0
  9. package/src/cli/config-overrides.ts +68 -0
  10. package/src/cli/entrypoint.ts +6 -0
  11. package/src/cli/help.ts +4 -2
  12. package/src/cli/management-commands.ts +1 -1
  13. package/src/cli/management.ts +1 -8
  14. package/src/cli/parser.ts +31 -18
  15. package/src/cli/service-command.ts +1 -1
  16. package/src/cli/surface-command.ts +1 -1
  17. package/src/cli/tui-startup.ts +72 -10
  18. package/src/cli/types.ts +14 -3
  19. package/src/cli-flags.ts +1 -0
  20. package/src/config/atomic-write.ts +70 -0
  21. package/src/config/goodvibes-home-audit.ts +2 -0
  22. package/src/config/read-versioned.ts +115 -0
  23. package/src/core/context-auto-compact.ts +77 -0
  24. package/src/core/conversation-rendering.ts +49 -15
  25. package/src/core/conversation.ts +101 -16
  26. package/src/core/format-user-error.ts +192 -0
  27. package/src/core/stream-event-wiring.ts +144 -0
  28. package/src/core/stream-stall-watchdog.ts +103 -0
  29. package/src/core/system-message-router.ts +5 -1
  30. package/src/core/turn-event-wiring.ts +124 -0
  31. package/src/daemon/cli.ts +5 -0
  32. package/src/export/cost-utils.ts +71 -0
  33. package/src/export/gist-uploader.ts +136 -0
  34. package/src/input/command-registry.ts +32 -1
  35. package/src/input/commands/control-room-runtime.ts +10 -10
  36. package/src/input/commands/experience-runtime.ts +5 -4
  37. package/src/input/commands/knowledge.ts +1 -1
  38. package/src/input/commands/local-auth-runtime.ts +27 -5
  39. package/src/input/commands/local-setup.ts +4 -6
  40. package/src/input/commands/memory-product-runtime.ts +8 -6
  41. package/src/input/commands/operator-panel-runtime.ts +1 -1
  42. package/src/input/commands/operator-runtime.ts +3 -10
  43. package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
  44. package/src/input/commands/provider.ts +57 -3
  45. package/src/input/commands/recall-review.ts +26 -2
  46. package/src/input/commands/services-runtime.ts +2 -2
  47. package/src/input/commands/session-workflow.ts +8 -16
  48. package/src/input/commands/session.ts +70 -20
  49. package/src/input/commands/share-runtime.ts +99 -12
  50. package/src/input/commands/tts-runtime.ts +30 -4
  51. package/src/input/commands.ts +2 -4
  52. package/src/input/delete-key-policy.ts +46 -0
  53. package/src/input/feed-context-factory.ts +2 -0
  54. package/src/input/handler-feed.ts +3 -0
  55. package/src/input/handler-interactions.ts +2 -15
  56. package/src/input/handler-modal-routes.ts +128 -12
  57. package/src/input/handler-modal-token-routes.ts +22 -5
  58. package/src/input/handler-onboarding-cloudflare.ts +1 -1
  59. package/src/input/handler-onboarding.ts +73 -69
  60. package/src/input/handler-types.ts +163 -0
  61. package/src/input/handler.ts +6 -2
  62. package/src/input/input-history.ts +76 -6
  63. package/src/input/model-picker-filter.ts +265 -0
  64. package/src/input/model-picker-items.ts +208 -0
  65. package/src/input/model-picker.ts +92 -325
  66. package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
  67. package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
  68. package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
  69. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
  70. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
  71. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
  72. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
  73. package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
  74. package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
  75. package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
  76. package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
  77. package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
  78. package/src/input/onboarding/onboarding-wizard.ts +3 -3
  79. package/src/input/settings-modal-behavior.ts +5 -0
  80. package/src/input/settings-modal-data.ts +378 -0
  81. package/src/input/settings-modal-mutations.ts +157 -0
  82. package/src/input/settings-modal-reset.ts +154 -0
  83. package/src/input/settings-modal.ts +236 -232
  84. package/src/main.ts +93 -85
  85. package/src/panels/agent-inspector-panel.ts +120 -18
  86. package/src/panels/agent-inspector-shared.ts +29 -0
  87. package/src/panels/builtin/agent.ts +4 -1
  88. package/src/panels/builtin/development.ts +5 -1
  89. package/src/panels/builtin/knowledge.ts +14 -13
  90. package/src/panels/builtin/operations.ts +22 -1
  91. package/src/panels/builtin/shared.ts +7 -0
  92. package/src/panels/cockpit-panel.ts +123 -3
  93. package/src/panels/cockpit-read-model.ts +232 -0
  94. package/src/panels/confirm-state.ts +27 -12
  95. package/src/panels/cost-tracker-panel.ts +23 -67
  96. package/src/panels/eval-panel.ts +10 -9
  97. package/src/panels/index.ts +1 -1
  98. package/src/panels/knowledge-graph-panel.ts +84 -0
  99. package/src/panels/local-auth-panel.ts +124 -4
  100. package/src/panels/memory-panel.ts +370 -40
  101. package/src/panels/project-planning-panel.ts +42 -4
  102. package/src/panels/search-focus.ts +11 -5
  103. package/src/panels/session-maintenance.ts +66 -15
  104. package/src/panels/subscription-panel.ts +33 -25
  105. package/src/panels/types.ts +28 -1
  106. package/src/panels/wrfc-panel.ts +224 -41
  107. package/src/renderer/agent-detail-modal.ts +118 -13
  108. package/src/renderer/code-block.ts +10 -2
  109. package/src/renderer/compositor.ts +18 -4
  110. package/src/renderer/context-inspector.ts +1 -5
  111. package/src/renderer/context-status-hint.ts +54 -0
  112. package/src/renderer/diff.ts +94 -21
  113. package/src/renderer/markdown.ts +29 -13
  114. package/src/renderer/settings-modal-helpers.ts +1 -1
  115. package/src/renderer/settings-modal.ts +90 -10
  116. package/src/renderer/shell-surface.ts +10 -0
  117. package/src/renderer/syntax-highlighter.ts +10 -3
  118. package/src/renderer/term-caps.ts +318 -0
  119. package/src/renderer/theme.ts +158 -0
  120. package/src/renderer/tool-call.ts +12 -2
  121. package/src/renderer/ui-factory.ts +50 -6
  122. package/src/runtime/bootstrap-command-context.ts +1 -0
  123. package/src/runtime/bootstrap-command-parts.ts +18 -0
  124. package/src/runtime/bootstrap-core.ts +145 -13
  125. package/src/runtime/bootstrap-shell.ts +11 -0
  126. package/src/runtime/bootstrap.ts +9 -0
  127. package/src/runtime/onboarding/apply.ts +4 -6
  128. package/src/runtime/onboarding/index.ts +1 -0
  129. package/src/runtime/onboarding/markers.ts +42 -49
  130. package/src/runtime/onboarding/progress.ts +148 -0
  131. package/src/runtime/onboarding/state.ts +133 -55
  132. package/src/runtime/onboarding/types.ts +20 -0
  133. package/src/runtime/services.ts +27 -1
  134. package/src/runtime/wrfc-persistence.ts +237 -0
  135. package/src/shell/blocking-input.ts +20 -5
  136. package/src/tools/wrfc-agent-guard.ts +64 -3
  137. package/src/utils/format-elapsed.ts +30 -0
  138. package/src/utils/terminal-width.ts +45 -0
  139. package/src/version.ts +1 -1
  140. package/src/work-plans/work-plan-store.ts +4 -6
  141. package/src/panels/knowledge-panel.ts +0 -345
  142. package/src/planning/project-planning-coordinator.ts +0 -543
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { BasePanel } from './base-panel.ts';
9
9
  import type { Line } from '../types/grid.ts';
10
+ import type { KeyName } from './types.ts';
10
11
  import { createEmptyLine } from '../types/grid.ts';
11
12
  import {
12
13
  buildEmptyState,
@@ -136,21 +137,21 @@ export class EvalPanel extends BasePanel {
136
137
  this._unsub = null;
137
138
  }
138
139
 
139
- public handleInput(key: string): boolean {
140
+ public handleInput(key: KeyName): boolean {
140
141
  const suites = this._registry.getSuiteResults();
141
142
 
142
143
  if (this._mode === 'list') {
143
- if (key === 'ArrowUp' || key === 'k') {
144
+ if (key === 'up' || key === 'k') {
144
145
  this._selectedSuiteIdx = Math.max(0, this._selectedSuiteIdx - 1);
145
146
  this.markDirty();
146
147
  return true;
147
148
  }
148
- if (key === 'ArrowDown' || key === 'j') {
149
+ if (key === 'down' || key === 'j') {
149
150
  this._selectedSuiteIdx = Math.min(suites.length - 1, this._selectedSuiteIdx + 1);
150
151
  this.markDirty();
151
152
  return true;
152
153
  }
153
- if ((key === 'Enter' || key === 'Return' || key === 'l') && suites.length > 0) {
154
+ if ((key === 'enter' || key === 'return' || key === 'l') && suites.length > 0) {
154
155
  this._mode = 'detail';
155
156
  this._selectedScenarioIdx = 0;
156
157
  this._scrollOffset = 0;
@@ -161,12 +162,12 @@ export class EvalPanel extends BasePanel {
161
162
  }
162
163
 
163
164
  // detail mode
164
- if (key === 'Escape' || key === 'q' || key === 'h') {
165
+ if (key === 'escape' || key === 'q' || key === 'h') {
165
166
  this._mode = 'list';
166
167
  this.markDirty();
167
168
  return true;
168
169
  }
169
- if (key === 'ArrowUp' || key === 'k') {
170
+ if (key === 'up' || key === 'k') {
170
171
  const suite = suites[this._selectedSuiteIdx];
171
172
  if (suite) {
172
173
  this._selectedScenarioIdx = Math.max(0, this._selectedScenarioIdx - 1);
@@ -175,7 +176,7 @@ export class EvalPanel extends BasePanel {
175
176
  }
176
177
  return true;
177
178
  }
178
- if (key === 'ArrowDown' || key === 'j') {
179
+ if (key === 'down' || key === 'j') {
179
180
  const suite = suites[this._selectedSuiteIdx];
180
181
  if (suite) {
181
182
  this._selectedScenarioIdx = Math.min(
@@ -187,12 +188,12 @@ export class EvalPanel extends BasePanel {
187
188
  }
188
189
  return true;
189
190
  }
190
- if (key === 'PageUp') {
191
+ if (key === 'pageup') {
191
192
  this._scrollOffset = Math.max(0, this._scrollOffset - 5);
192
193
  this.markDirty();
193
194
  return true;
194
195
  }
195
- if (key === 'PageDown') {
196
+ if (key === 'pagedown') {
196
197
  this._scrollOffset += 5;
197
198
  this.markDirty();
198
199
  return true;
@@ -40,7 +40,7 @@ export { SecurityPanel } from './security-panel.ts';
40
40
  export { MarketplacePanel } from './marketplace-panel.ts';
41
41
  export { SandboxPanel } from './sandbox-panel.ts';
42
42
  export { ApprovalPanel } from './approval-panel.ts';
43
- export { KnowledgePanel } from './knowledge-panel.ts';
43
+ export { KnowledgeGraphPanel } from './knowledge-graph-panel.ts';
44
44
  export { SystemMessagesPanel } from './system-messages-panel.ts';
45
45
  export { PanelListPanel } from './panel-list-panel.ts';
46
46
  export type { SystemMessageEntry, SystemMessagePriority } from './system-messages-panel.ts';
@@ -0,0 +1,84 @@
1
+ /**
2
+ * KnowledgeGraphPanel — SDK knowledge graph front-door.
3
+ *
4
+ * TASK-040: The 'knowledge' panel id is repointed here (the SDK graph), fixing
5
+ * the naming inversion where the former panel named 'Knowledge' was actually
6
+ * rendering memory records.
7
+ *
8
+ * This panel is a thin information surface that explains the graph's capabilities
9
+ * and routes the user to the /knowledge command suite for ingest/RAG operations.
10
+ * The full graph UI is command-driven (/knowledge ask, ingest-url, list, search…).
11
+ */
12
+
13
+ import type { Line } from '../types/grid.ts';
14
+ import { BasePanel } from './base-panel.ts';
15
+ import {
16
+ buildBodyText,
17
+ buildGuidanceLine,
18
+ buildPanelLine,
19
+ buildPanelWorkspace,
20
+ DEFAULT_PANEL_PALETTE,
21
+ } from './polish.ts';
22
+
23
+ const C = {
24
+ ...DEFAULT_PANEL_PALETTE,
25
+ header: '#94a3b8',
26
+ headerBg: '#1e293b',
27
+ } as const;
28
+
29
+ export class KnowledgeGraphPanel extends BasePanel {
30
+ constructor() {
31
+ super('knowledge', 'Knowledge', 'K', 'agent');
32
+ }
33
+
34
+ handleInput(_key: string): boolean {
35
+ return false;
36
+ }
37
+
38
+ render(width: number, height: number): Line[] {
39
+ const sections = [
40
+ {
41
+ title: 'SDK Knowledge Graph',
42
+ lines: [
43
+ ...buildBodyText(
44
+ width,
45
+ 'The knowledge graph stores ingested URLs, bookmarks, and structured facts as nodes and edges. ' +
46
+ 'Use /knowledge commands to ingest sources, search the graph, and build task-context packets.',
47
+ C,
48
+ C.value,
49
+ ),
50
+ buildPanelLine(width, [['', C.dim]]),
51
+ buildGuidanceLine(width, '/knowledge status', 'check the graph status and source counts', C),
52
+ buildGuidanceLine(width, '/knowledge ask <query>', 'ask a question against the ingested knowledge', C),
53
+ buildGuidanceLine(width, '/knowledge ingest-url <url>', 'ingest a URL as a knowledge source', C),
54
+ buildGuidanceLine(width, '/knowledge list', 'list ingested sources or graph nodes', C),
55
+ buildGuidanceLine(width, '/knowledge search <query>', 'search the graph for nodes and sources', C),
56
+ buildGuidanceLine(width, '/knowledge packet <task>', 'build a compact prompt packet for a task', C),
57
+ ],
58
+ },
59
+ {
60
+ title: 'Project Memory',
61
+ lines: [
62
+ ...buildBodyText(
63
+ width,
64
+ 'For durable decisions, risks, runbooks, incidents, and architecture records, use the Memory panel ' +
65
+ 'or the /recall command surface. Durable memory is a sub-namespace of the knowledge graph.',
66
+ C,
67
+ C.dim,
68
+ ),
69
+ buildPanelLine(width, [['', C.dim]]),
70
+ buildGuidanceLine(width, '/recall add <class> <summary>', 'capture a new memory record', C),
71
+ buildGuidanceLine(width, '/recall queue', 'show the operator review queue', C),
72
+ buildGuidanceLine(width, '/project-memory (pmem)', 'project-memory alias for /recall front-door', C),
73
+ ],
74
+ },
75
+ ];
76
+
77
+ return buildPanelWorkspace(width, height, {
78
+ title: 'Knowledge Graph',
79
+ intro: 'Ingested sources, graph nodes, and the durable memory bridge.',
80
+ sections,
81
+ palette: C,
82
+ });
83
+ }
84
+ }
@@ -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> <password>', 'rotate bootstrap/default credentials and revoke older sessions as needed', C),
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} <password>`.slice(0, Math.max(0, width)), C.dim]]),
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 /auth local revoke-session ', C.dim]]));
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
  }