@pellux/goodvibes-tui 2.0.21 → 2.1.1
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 +65 -1
- package/README.md +30 -29
- package/docs/README.md +8 -5
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +28 -19
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-reference.ts +2 -2
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/live-verifier.ts +0 -10
- package/src/version.ts +1 -1
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- package/src/verification/verification-ledger.ts +0 -707
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import { createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
|
|
3
|
-
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
|
-
import {
|
|
6
|
-
buildDetailBlock,
|
|
7
|
-
buildGuidanceLine,
|
|
8
|
-
buildKeyboardHints,
|
|
9
|
-
buildPanelListRow,
|
|
10
|
-
buildPanelLine,
|
|
11
|
-
buildSummaryBlock,
|
|
12
|
-
buildPanelWorkspace,
|
|
13
|
-
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
type PanelPalette,
|
|
15
|
-
} from './polish.ts';
|
|
16
|
-
import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
17
|
-
import type { LocalAuthInspectionQuery } from '@/runtime/index.ts';
|
|
18
|
-
import type { KeyName } from './types.ts';
|
|
19
|
-
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
20
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
21
|
-
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
22
|
-
|
|
23
|
-
// Base chrome only, state colors and text tokens come straight from
|
|
24
|
-
// DEFAULT_PANEL_PALETTE.
|
|
25
|
-
const C = DEFAULT_PANEL_PALETTE;
|
|
26
|
-
|
|
27
|
-
function formatRoles(roles: readonly string[]): string {
|
|
28
|
-
return roles.length > 0 ? roles.join(', ') : '(none)';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type LocalAuthUser = LocalAuthSnapshot['users'][number];
|
|
32
|
-
|
|
33
|
-
/** Action kind for the masked password entry mode. */
|
|
34
|
-
export type MaskedEntryKind = 'add-user' | 'rotate-password';
|
|
35
|
-
|
|
36
|
-
interface MaskedEntryState {
|
|
37
|
-
readonly kind: MaskedEntryKind;
|
|
38
|
-
readonly username: string;
|
|
39
|
-
readonly auth: UserAuthManager;
|
|
40
|
-
buffer: string;
|
|
41
|
-
}
|
|
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
|
-
|
|
54
|
-
export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
55
|
-
private readonly authManager: LocalAuthInspectionQuery;
|
|
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;
|
|
64
|
-
|
|
65
|
-
public constructor(authManager: LocalAuthInspectionQuery) {
|
|
66
|
-
super('local-auth', 'Local Auth', 'U', 'security-policy');
|
|
67
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
68
|
-
this.authManager = authManager;
|
|
69
|
-
}
|
|
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
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Activate masked password-entry mode for the given operation.
|
|
86
|
-
* The panel's handleInput will capture keystrokes into a private buffer;
|
|
87
|
-
* no plaintext is ever recorded in input history, transcript, or logs.
|
|
88
|
-
*/
|
|
89
|
-
public openMaskedEntry(kind: MaskedEntryKind, username: string, auth: UserAuthManager): void {
|
|
90
|
-
this.maskedState = { kind, username, auth, buffer: '' };
|
|
91
|
-
this.invalidate();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Returns true when the panel is in masked-entry mode. */
|
|
95
|
-
public get isMaskedEntryActive(): boolean {
|
|
96
|
-
return this.maskedState !== null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Masked password/username entry wants every character of a burst (paste,
|
|
101
|
-
* or fast typing landing in one input.feed() call) delivered one at a
|
|
102
|
-
* time, same as it always has, see the interface doc on
|
|
103
|
-
* `Panel.isCapturingTextBurst`.
|
|
104
|
-
*/
|
|
105
|
-
public override isCapturingTextBurst(): boolean {
|
|
106
|
-
return this.isMaskedEntryActive || this.usernameEntry !== null || super.isCapturingTextBurst();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
public override handleInput(key: KeyName): boolean {
|
|
110
|
-
// Masked entry takes priority when active, it must capture every
|
|
111
|
-
// keystroke (including letters that would otherwise be p/a/d/b actions)
|
|
112
|
-
// as part of the password being typed.
|
|
113
|
-
if (this.maskedState !== null) {
|
|
114
|
-
return this.handleMaskedEntryInput(key);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (this.usernameEntry !== null) {
|
|
118
|
-
return this.handleUsernameEntryInput(key);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (this.deleteConfirm !== null) {
|
|
122
|
-
return this.handleDeleteConfirmInput(key);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const selected = this.getSelectedItem();
|
|
126
|
-
|
|
127
|
-
if (key === 'p') {
|
|
128
|
-
if (!selected) return false;
|
|
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.openMaskedEntry('rotate-password', selected.username, mutations as unknown as UserAuthManager);
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (key === 'a') {
|
|
139
|
-
const mutations = this.mutations;
|
|
140
|
-
if (!mutations) {
|
|
141
|
-
this.setError('Local auth mutations are not available in this session.');
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
this.usernameEntry = '';
|
|
145
|
-
this.invalidate();
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (key === 'd') {
|
|
150
|
-
if (!selected) return false;
|
|
151
|
-
this.deleteConfirm = { subject: selected.username, label: selected.username };
|
|
152
|
-
this.invalidate();
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (key === 'b') {
|
|
157
|
-
const snapshot = this.cachedSnapshot ?? this.authManager.inspect();
|
|
158
|
-
if (!snapshot.bootstrapCredentialPresent) return false;
|
|
159
|
-
const mutations = this.mutations;
|
|
160
|
-
if (!mutations) {
|
|
161
|
-
this.setError('Local auth mutations are not available in this session.');
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
const cleared = mutations.clearBootstrapCredentialFile();
|
|
165
|
-
if (cleared) {
|
|
166
|
-
this.cachedSnapshot = null; // force a fresh inspect() on the next render
|
|
167
|
-
} else {
|
|
168
|
-
this.setError('No bootstrap credential file was present.');
|
|
169
|
-
}
|
|
170
|
-
this.invalidate();
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Delegate scroll/selection to ScrollableListPanel for everything else.
|
|
175
|
-
return super.handleInput?.(key) ?? false;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private handleDeleteConfirmInput(key: KeyName): boolean {
|
|
179
|
-
const result = handleConfirmInput(this.deleteConfirm, key);
|
|
180
|
-
if (result === 'confirmed') {
|
|
181
|
-
const username = this.deleteConfirm!.subject;
|
|
182
|
-
this.deleteConfirm = null;
|
|
183
|
-
const mutations = this.mutations;
|
|
184
|
-
if (mutations) {
|
|
185
|
-
try {
|
|
186
|
-
const deleted = mutations.deleteUser(username);
|
|
187
|
-
if (deleted) this.cachedSnapshot = null; // force a fresh inspect() on the next render
|
|
188
|
-
else this.setError(`Unknown local auth user: ${username}`);
|
|
189
|
-
} catch (error) {
|
|
190
|
-
this.setError(summarizeError(error));
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
this.setError('Local auth mutations are not available in this session.');
|
|
194
|
-
}
|
|
195
|
-
this.invalidate();
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
if (result === 'cancelled') {
|
|
199
|
-
this.deleteConfirm = null;
|
|
200
|
-
this.invalidate();
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
// 'absorbed' (or defensively, 'inactive', deleteConfirm is non-null here so this never fires)
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
private handleUsernameEntryInput(key: KeyName): boolean {
|
|
208
|
-
if (key === 'escape') {
|
|
209
|
-
this.usernameEntry = null;
|
|
210
|
-
this.invalidate();
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
if (key === 'enter' || key === 'return') {
|
|
214
|
-
const username = (this.usernameEntry ?? '').trim();
|
|
215
|
-
this.usernameEntry = null;
|
|
216
|
-
if (username.length === 0) {
|
|
217
|
-
this.invalidate();
|
|
218
|
-
return true; // no-op on empty username, mirroring the masked-entry empty-buffer no-op
|
|
219
|
-
}
|
|
220
|
-
const mutations = this.mutations;
|
|
221
|
-
if (mutations) {
|
|
222
|
-
this.openMaskedEntry('add-user', username, mutations as unknown as UserAuthManager);
|
|
223
|
-
} else {
|
|
224
|
-
this.setError('Local auth mutations are not available in this session.');
|
|
225
|
-
this.invalidate();
|
|
226
|
-
}
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
if (isTextBackspace(key)) {
|
|
230
|
-
if (this.usernameEntry && this.usernameEntry.length > 0) {
|
|
231
|
-
this.usernameEntry = this.usernameEntry.slice(0, -1);
|
|
232
|
-
this.invalidate();
|
|
233
|
-
}
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
if (key.length === 1) {
|
|
237
|
-
this.usernameEntry = (this.usernameEntry ?? '') + key;
|
|
238
|
-
this.invalidate();
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
return true; // absorb everything else while username entry is active
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private handleMaskedEntryInput(key: KeyName): boolean {
|
|
245
|
-
// Masked entry is active, capture all keystrokes.
|
|
246
|
-
const state = this.maskedState!;
|
|
247
|
-
|
|
248
|
-
if (key === 'escape') {
|
|
249
|
-
// Cancel: discard buffer, exit masked mode without persisting.
|
|
250
|
-
this.maskedState = null;
|
|
251
|
-
this.invalidate();
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (key === 'enter' || key === 'return') {
|
|
256
|
-
// Submit: call the auth API if a non-empty password was entered.
|
|
257
|
-
if (state.buffer.length === 0) {
|
|
258
|
-
return true; // no-op, require at least one character
|
|
259
|
-
}
|
|
260
|
-
const password = state.buffer;
|
|
261
|
-
const { kind, username, auth } = state;
|
|
262
|
-
// Clear the mutable state *before* the auth call so the secret
|
|
263
|
-
// never lingers in the buffer after an exception.
|
|
264
|
-
this.maskedState = null;
|
|
265
|
-
try {
|
|
266
|
-
if (kind === 'add-user') {
|
|
267
|
-
auth.addUser(username, password, ['admin']);
|
|
268
|
-
} else {
|
|
269
|
-
auth.rotatePassword(username, password);
|
|
270
|
-
}
|
|
271
|
-
} catch (_error) {
|
|
272
|
-
// Surface errors via the panel's error facility rather than logging.
|
|
273
|
-
this.setError(summarizeError(_error));
|
|
274
|
-
}
|
|
275
|
-
this.invalidate();
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (isTextBackspace(key)) {
|
|
280
|
-
// Remove last character (Ink 6.8.0: raw-stdin raw 'backspace' only).
|
|
281
|
-
if (state.buffer.length > 0) {
|
|
282
|
-
state.buffer = state.buffer.slice(0, -1);
|
|
283
|
-
this.invalidate();
|
|
284
|
-
}
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Single printable character: append to buffer.
|
|
289
|
-
if (key.length === 1) {
|
|
290
|
-
state.buffer += key;
|
|
291
|
-
this.invalidate();
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// All other named keys are consumed (ignored) while masked mode is active.
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
protected override getPalette(): PanelPalette {
|
|
300
|
-
return C;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
protected getItems(): readonly LocalAuthUser[] {
|
|
304
|
-
// Reads the render-pass cache populated by render() below (single
|
|
305
|
-
// inspect() per render). Falls back to a fresh inspect() only when
|
|
306
|
-
// called before the first render (e.g. directly from a test).
|
|
307
|
-
return (this.cachedSnapshot ?? this.authManager.inspect()).users;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
|
|
311
|
-
return buildPanelListRow(width, [
|
|
312
|
-
{ text: fitDisplay(user.username, 20), fg: C.value },
|
|
313
|
-
{ text: truncateDisplay(` roles=${formatRoles(user.roles)}`, Math.max(0, width - 24)), fg: C.info },
|
|
314
|
-
], C, { selected });
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
protected override getEmptyStateMessage(): string {
|
|
318
|
-
return ' No local auth users configured.';
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
public render(width: number, height: number): Line[] {
|
|
322
|
-
// When masked entry or username entry is active, render a dedicated
|
|
323
|
-
// prompt instead of the normal panel content. No plaintext password
|
|
324
|
-
// appears anywhere in output.
|
|
325
|
-
if (this.maskedState !== null) {
|
|
326
|
-
return this.renderMaskedPrompt(width, height);
|
|
327
|
-
}
|
|
328
|
-
if (this.usernameEntry !== null) {
|
|
329
|
-
return this.renderUsernameEntryPrompt(width, height);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const intro = 'Manage local daemon and HTTP-listener auth users, bootstrap state, and active sessions.';
|
|
333
|
-
// Single inspect() call for this whole render pass, getItems() (called
|
|
334
|
-
// both directly below and again inside renderList()'s internal
|
|
335
|
-
// getVisibleItems()) reads from this cache instead of re-inspecting.
|
|
336
|
-
this.cachedSnapshot = this.authManager.inspect();
|
|
337
|
-
const snapshot = this.cachedSnapshot;
|
|
338
|
-
const users = this.getItems();
|
|
339
|
-
|
|
340
|
-
const issueMessages: string[] = [];
|
|
341
|
-
if (snapshot.bootstrapCredentialPresent) issueMessages.push('Bootstrap credential file still exists; press b to clear it after password rotation.');
|
|
342
|
-
if (snapshot.sessionCount === 0) issueMessages.push('No active local auth sessions are currently tracked.');
|
|
343
|
-
|
|
344
|
-
const headerLines: Line[] = [
|
|
345
|
-
...buildSummaryBlock(width, 'Local auth posture', [
|
|
346
|
-
buildPanelLine(width, [
|
|
347
|
-
[' users ', C.label],
|
|
348
|
-
[String(snapshot.userCount), C.value],
|
|
349
|
-
[' sessions ', C.label],
|
|
350
|
-
[String(snapshot.sessionCount), snapshot.sessionCount > 0 ? C.info : C.dim],
|
|
351
|
-
[' bootstrap ', C.label],
|
|
352
|
-
[snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
|
|
353
|
-
]),
|
|
354
|
-
buildPanelLine(width, [[' user store ', C.label], [truncateDisplay(snapshot.userStorePath, Math.max(0, width - 13)), C.dim]]),
|
|
355
|
-
buildPanelLine(width, [[' bootstrap file ', C.label], [truncateDisplay(snapshot.bootstrapCredentialPath, Math.max(0, width - 18)), C.dim]]),
|
|
356
|
-
...(issueMessages.length > 0
|
|
357
|
-
? issueMessages.map((issue) => buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.warn]]))
|
|
358
|
-
: [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
|
|
359
|
-
buildGuidanceLine(width, 'p / a / d', 'rotate password, add user, or delete the selected user via masked in-panel entry', C),
|
|
360
|
-
], C),
|
|
361
|
-
];
|
|
362
|
-
|
|
363
|
-
if (users.length === 0) {
|
|
364
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
365
|
-
title: 'Local Auth Control Room',
|
|
366
|
-
intro,
|
|
367
|
-
sections: [{ lines: headerLines }],
|
|
368
|
-
footerLines: [this.buildHintsLine(width, null)],
|
|
369
|
-
palette: C,
|
|
370
|
-
});
|
|
371
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
372
|
-
return workspace.slice(0, height);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
this.clampSelection();
|
|
376
|
-
const selected = this.getSelectedItem();
|
|
377
|
-
|
|
378
|
-
const footerLines: Line[] = [];
|
|
379
|
-
if (selected) {
|
|
380
|
-
const deleteActive = this.deleteConfirm?.subject === selected.username;
|
|
381
|
-
footerLines.push(
|
|
382
|
-
...buildDetailBlock(width, 'Selected user', [
|
|
383
|
-
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [truncateDisplay(formatRoles(selected.roles), Math.max(0, width - 23)), C.info]]),
|
|
384
|
-
deleteActive
|
|
385
|
-
? buildPanelLine(width, [[` Delete ${selected.username}? Press y or Enter to confirm, n or Esc to cancel.`, C.warn]])
|
|
386
|
-
: buildPanelLine(width, [[' p: rotate password a: add user d: delete user', C.dim]]),
|
|
387
|
-
], C),
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (snapshot.sessions.length > 0) {
|
|
392
|
-
footerLines.push(buildPanelLine(width, [[` Active sessions (${snapshot.sessions.length})`, C.label]]));
|
|
393
|
-
footerLines.push(
|
|
394
|
-
...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
|
|
395
|
-
[' ', C.label],
|
|
396
|
-
[fitDisplay(session.username, 18), C.value],
|
|
397
|
-
[truncateDisplay(` expires ${new Date(session.expiresAt).toLocaleString()}`, Math.max(0, width - 20)), C.dim],
|
|
398
|
-
])),
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return this.renderList(width, height, {
|
|
403
|
-
title: 'Local Auth Control Room',
|
|
404
|
-
header: headerLines,
|
|
405
|
-
footer: footerLines,
|
|
406
|
-
hints: this.buildHints(selected ?? null, snapshot.bootstrapCredentialPresent),
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Footer keyboard hints, adapted to the current state, confirm-pending
|
|
411
|
-
* vs. normal browsing, and whether there's a bootstrap credential to clear. */
|
|
412
|
-
private buildHints(
|
|
413
|
-
selected: LocalAuthUser | null,
|
|
414
|
-
bootstrapCredentialPresent: boolean,
|
|
415
|
-
): ReadonlyArray<{ keys: string; label: string }> {
|
|
416
|
-
if (this.deleteConfirm) {
|
|
417
|
-
return [
|
|
418
|
-
{ keys: 'y/Enter', label: 'confirm delete' },
|
|
419
|
-
{ keys: 'n/Esc', label: 'cancel' },
|
|
420
|
-
];
|
|
421
|
-
}
|
|
422
|
-
const hints: Array<{ keys: string; label: string }> = [{ keys: '↑/↓', label: 'select user' }];
|
|
423
|
-
if (selected) {
|
|
424
|
-
hints.push({ keys: 'p', label: 'rotate password' }, { keys: 'd', label: 'delete user' });
|
|
425
|
-
}
|
|
426
|
-
hints.push({ keys: 'a', label: 'add user' });
|
|
427
|
-
if (bootstrapCredentialPresent) hints.push({ keys: 'b', label: 'clear bootstrap credential' });
|
|
428
|
-
return hints;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
private buildHintsLine(width: number, selected: LocalAuthUser | null): Line {
|
|
432
|
-
return buildKeyboardHints(width, this.buildHints(selected, false), C);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
private renderUsernameEntryPrompt(width: number, height: number): Line[] {
|
|
436
|
-
const draft = this.usernameEntry ?? '';
|
|
437
|
-
const promptLines: Line[] = [
|
|
438
|
-
buildPanelLine(width, [[' Add local auth user: enter a username, then set a masked password.', C.value]]),
|
|
439
|
-
buildPanelLine(width, [['', C.label]]),
|
|
440
|
-
buildPanelLine(width, [[' Username ', C.label], [`${draft}█`, C.info]]),
|
|
441
|
-
buildPanelLine(width, [['', C.label]]),
|
|
442
|
-
buildPanelLine(width, [[' [Enter] Continue to password entry [Esc] Cancel [Backspace] Delete char', C.dim]]),
|
|
443
|
-
];
|
|
444
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
445
|
-
title: 'Local Auth: Add User',
|
|
446
|
-
intro: 'Type the new username and press Enter to continue to masked password entry.',
|
|
447
|
-
sections: [{ lines: promptLines }],
|
|
448
|
-
palette: C,
|
|
449
|
-
});
|
|
450
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
451
|
-
return workspace.slice(0, height);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private renderMaskedPrompt(width: number, height: number): Line[] {
|
|
455
|
-
const state = this.maskedState!;
|
|
456
|
-
const actionLabel = state.kind === 'add-user' ? 'Add user' : 'Rotate password';
|
|
457
|
-
const dots = '•'.repeat(Math.min(32, state.buffer.length));
|
|
458
|
-
const cursor = '█'; // block cursor
|
|
459
|
-
const maskedDisplay = state.buffer.length > 0 ? `${dots}${cursor}` : cursor;
|
|
460
|
-
|
|
461
|
-
const promptLines: Line[] = [
|
|
462
|
-
buildPanelLine(width, [[` ${actionLabel}: ${state.username}`, C.value]]),
|
|
463
|
-
buildPanelLine(width, [['', C.label]]),
|
|
464
|
-
buildPanelLine(width, [[' Password ', C.label], [truncateDisplay(maskedDisplay, Math.max(0, width - 12)), C.info]]),
|
|
465
|
-
buildPanelLine(width, [['', C.label]]),
|
|
466
|
-
buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
|
|
467
|
-
];
|
|
468
|
-
|
|
469
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
470
|
-
title: 'Local Auth: Password Entry',
|
|
471
|
-
intro: `Type a password for ${state.username}. The value is never echoed in plaintext or stored in history.`,
|
|
472
|
-
sections: [{ lines: promptLines }],
|
|
473
|
-
palette: C,
|
|
474
|
-
});
|
|
475
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
476
|
-
return workspace;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PanelNotificationFeed, the read-model behind the `panel_only` notification
|
|
3
|
-
* target.
|
|
4
|
-
*
|
|
5
|
-
* The SDK's NotificationRouter (`@/runtime/index.ts` -> platform runtime `ui`
|
|
6
|
-
* barrel) can route a notification's `RoutingDecision.target` to
|
|
7
|
-
* `'panel_only'`, meaning: don't surface this in the conversation or the
|
|
8
|
-
* status bar, but it must still reach a real, visible place. Before this
|
|
9
|
-
* file, nothing in this repo consumed that target outside of tests, a
|
|
10
|
-
* panel-routed notification (including every burst- or batch-collapsed one)
|
|
11
|
-
* had nowhere to go. This feed is that place: a caller holding a routed
|
|
12
|
-
* `Notification` + `RoutingDecision` pair calls `record()`, and
|
|
13
|
-
* `notifications-panel.ts` renders whatever has accumulated.
|
|
14
|
-
*
|
|
15
|
-
* Collapsed groups (reasonCode `burst_collapsed` / `batch_window_collapsed`)
|
|
16
|
-
* are tracked by their `batchKey` and accumulate an honest running count
|
|
17
|
-
* instead of rendering one row per suppressed duplicate.
|
|
18
|
-
*
|
|
19
|
-
* Production wiring, something in the running app actually calling
|
|
20
|
-
* `record()` with live notifications, rides the SDK round that turns on
|
|
21
|
-
* adaptive notification suppression by default; this feed and its panel are
|
|
22
|
-
* the render target that work lands on, built ahead of it so the target is
|
|
23
|
-
* never missing when that switch flips.
|
|
24
|
-
*/
|
|
25
|
-
import type { Notification, RoutingDecision } from '@/runtime/index.ts';
|
|
26
|
-
|
|
27
|
-
export interface PanelFeedEntry {
|
|
28
|
-
readonly key: string;
|
|
29
|
-
readonly domain: string;
|
|
30
|
-
readonly level: Notification['level'];
|
|
31
|
-
readonly title: string;
|
|
32
|
-
readonly body: string | undefined;
|
|
33
|
-
/** Unix ms of the most recent notification folded into this entry. */
|
|
34
|
-
readonly timestamp: number;
|
|
35
|
-
readonly reasonCode: RoutingDecision['reasonCode'];
|
|
36
|
-
/** How many notifications this entry represents. 1 for a standalone item; >1 for a collapsed group. Always the true count, never estimated. */
|
|
37
|
-
readonly collapsedCount: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const MAX_ENTRIES = 200;
|
|
41
|
-
|
|
42
|
-
/** Reason codes whose notifications fold into one running-count entry per batch key, rather than one entry per occurrence. */
|
|
43
|
-
const COLLAPSING_REASON_CODES: ReadonlySet<RoutingDecision['reasonCode']> = new Set([
|
|
44
|
-
'burst_collapsed',
|
|
45
|
-
'batch_window_collapsed',
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
export class PanelNotificationFeed {
|
|
49
|
-
private readonly entries = new Map<string, PanelFeedEntry>();
|
|
50
|
-
/** Insertion order of `entries` keys (oldest first), for bounded eviction. */
|
|
51
|
-
private order: string[] = [];
|
|
52
|
-
private readonly listeners = new Set<() => void>();
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Record a routed notification. Only notifications actually targeted at
|
|
56
|
-
* `panel_only` are kept, a caller passing anything else is a mistake (the
|
|
57
|
-
* router sent it elsewhere), so it's dropped rather than shown in the
|
|
58
|
-
* wrong place.
|
|
59
|
-
*/
|
|
60
|
-
record(notification: Notification, decision: RoutingDecision): void {
|
|
61
|
-
if (decision.target !== 'panel_only') return;
|
|
62
|
-
|
|
63
|
-
const collapsing = COLLAPSING_REASON_CODES.has(decision.reasonCode) && Boolean(decision.batchKey);
|
|
64
|
-
const key = collapsing ? `group:${decision.batchKey}` : `single:${notification.id}`;
|
|
65
|
-
const previousCount = collapsing ? (this.entries.get(key)?.collapsedCount ?? 0) : 0;
|
|
66
|
-
|
|
67
|
-
const entry: PanelFeedEntry = {
|
|
68
|
-
key,
|
|
69
|
-
domain: notification.domain,
|
|
70
|
-
level: notification.level,
|
|
71
|
-
title: notification.title,
|
|
72
|
-
body: notification.body,
|
|
73
|
-
timestamp: notification.timestamp,
|
|
74
|
-
reasonCode: decision.reasonCode,
|
|
75
|
-
collapsedCount: previousCount + 1,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
if (!this.entries.has(key)) {
|
|
79
|
-
this.order.push(key);
|
|
80
|
-
if (this.order.length > MAX_ENTRIES) {
|
|
81
|
-
const evicted = this.order.shift();
|
|
82
|
-
if (evicted !== undefined) this.entries.delete(evicted);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
this.entries.set(key, entry);
|
|
86
|
-
this.emitChange();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Every entry, most recently updated first. */
|
|
90
|
-
list(): readonly PanelFeedEntry[] {
|
|
91
|
-
const items: PanelFeedEntry[] = [];
|
|
92
|
-
for (const key of this.order) {
|
|
93
|
-
const entry = this.entries.get(key);
|
|
94
|
-
if (entry) items.push(entry);
|
|
95
|
-
}
|
|
96
|
-
return items.reverse();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
clear(): void {
|
|
100
|
-
this.entries.clear();
|
|
101
|
-
this.order = [];
|
|
102
|
-
this.emitChange();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Subscribe to feed changes (e.g. to mark a panel dirty). Returns an unsubscribe function. */
|
|
106
|
-
subscribe(listener: () => void): () => void {
|
|
107
|
-
this.listeners.add(listener);
|
|
108
|
-
return () => this.listeners.delete(listener);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private emitChange(): void {
|
|
112
|
-
for (const listener of this.listeners) listener();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let sharedFeed: PanelNotificationFeed | null = null;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The process-wide `panel_only` feed. Lazily created so the running app has
|
|
120
|
-
* exactly one feed that every future caller and the NotificationsPanel agree
|
|
121
|
-
* on, while tests construct their own isolated `PanelNotificationFeed`
|
|
122
|
-
* instance instead of reaching for this one.
|
|
123
|
-
*/
|
|
124
|
-
export function getSharedNotificationFeed(): PanelNotificationFeed {
|
|
125
|
-
if (!sharedFeed) sharedFeed = new PanelNotificationFeed();
|
|
126
|
-
return sharedFeed;
|
|
127
|
-
}
|