@pellux/goodvibes-tui 2.0.21 → 2.1.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 +58 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +16 -6
- 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-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/verification-ledger.ts +18 -16
- 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/{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,454 +0,0 @@
|
|
|
1
|
-
import { promises as fsPromises } from 'node:fs';
|
|
2
|
-
import { join, sep } from 'node:path';
|
|
3
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
4
|
-
import { createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
|
|
5
|
-
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
6
|
-
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
7
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
8
|
-
import type { ComponentHealthMonitor } from '@pellux/goodvibes-sdk/platform/runtime/observability';
|
|
9
|
-
import { listInstalledEcosystemEntries, type EcosystemCatalogPathOptions, type ShellPathService } from '@/runtime/index.ts';
|
|
10
|
-
import {
|
|
11
|
-
buildPanelLine,
|
|
12
|
-
buildPanelWorkspace,
|
|
13
|
-
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
extendPalette,
|
|
15
|
-
} from './polish.ts';
|
|
16
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
|
-
|
|
18
|
-
// Domain accents only; base chrome (header/headerBg/label/value/dim/empty/
|
|
19
|
-
// selectBg) comes from DEFAULT_PANEL_PALETTE.
|
|
20
|
-
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
21
|
-
project: '#38bdf8', // project-local skill origin
|
|
22
|
-
global: '#a78bfa', // global skill origin
|
|
23
|
-
} as const);
|
|
24
|
-
|
|
25
|
-
export type SkillOrigin = 'project-local' | 'global' | 'custom';
|
|
26
|
-
|
|
27
|
-
export interface SkillRecord {
|
|
28
|
-
name: string;
|
|
29
|
-
description: string;
|
|
30
|
-
path: string;
|
|
31
|
-
origin: SkillOrigin;
|
|
32
|
-
dependencies: string[];
|
|
33
|
-
includes: string[];
|
|
34
|
-
frontmatter: Record<string, string>;
|
|
35
|
-
/**
|
|
36
|
-
* Set when this skill's file lives under an installed ecosystem-marketplace
|
|
37
|
-
* receipt's targetPath, the receipt's provenance summary (e.g. curated
|
|
38
|
-
* source / signature info), so marketplace-installed skills read as such
|
|
39
|
-
* instead of looking identical to hand-authored ones.
|
|
40
|
-
*/
|
|
41
|
-
marketplaceProvenance?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SkillsPanelOptions {
|
|
45
|
-
shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
|
|
46
|
-
componentHealthMonitor?: ComponentHealthMonitor;
|
|
47
|
-
/** When provided, installed skill entries are tagged with marketplace provenance. */
|
|
48
|
-
ecosystemPaths?: EcosystemCatalogPathOptions;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function parseFrontmatter(content: string): Record<string, string> {
|
|
52
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
53
|
-
if (!match) return {};
|
|
54
|
-
const result: Record<string, string> = {};
|
|
55
|
-
for (const line of match[1].split('\n')) {
|
|
56
|
-
const [key, ...rest] = line.split(':');
|
|
57
|
-
if (key && rest.length > 0) {
|
|
58
|
-
result[key.trim()] = rest.join(':').trim();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function getSkillDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: SkillOrigin }> {
|
|
65
|
-
return [
|
|
66
|
-
{ root: join(cwd, '.goodvibes', 'skills'), origin: 'project-local' },
|
|
67
|
-
{ root: join(cwd, '.goodvibes', 'tui', 'skills'), origin: 'project-local' },
|
|
68
|
-
{ root: join(homeDir, '.goodvibes', 'skills'), origin: 'global' },
|
|
69
|
-
{ root: join(homeDir, '.goodvibes', 'tui', 'skills'), origin: 'global' },
|
|
70
|
-
];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function readSkillFile(path: string, origin: SkillOrigin): Promise<SkillRecord | null> {
|
|
74
|
-
let content = '';
|
|
75
|
-
try {
|
|
76
|
-
content = await fsPromises.readFile(path, 'utf-8');
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const frontmatter = parseFrontmatter(content);
|
|
82
|
-
const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
83
|
-
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/, '') ?? 'skill';
|
|
84
|
-
const description = frontmatter.description ?? frontmatter.summary ?? '';
|
|
85
|
-
const dependencies = frontmatter.depends_on
|
|
86
|
-
? frontmatter.depends_on.split(',').map((item) => item.trim()).filter(Boolean)
|
|
87
|
-
: [];
|
|
88
|
-
const includes: string[] = [];
|
|
89
|
-
const includeRegex = /^@([\w/-]+)/gm;
|
|
90
|
-
let match: RegExpExecArray | null;
|
|
91
|
-
while ((match = includeRegex.exec(body)) !== null) {
|
|
92
|
-
includes.push(match[1]);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
name,
|
|
97
|
-
description,
|
|
98
|
-
path,
|
|
99
|
-
origin,
|
|
100
|
-
dependencies,
|
|
101
|
-
includes,
|
|
102
|
-
frontmatter,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function scanSkillDirectory(root: string, origin: SkillOrigin): Promise<SkillRecord[]> {
|
|
107
|
-
let entries: string[] = [];
|
|
108
|
-
try {
|
|
109
|
-
entries = await fsPromises.readdir(root);
|
|
110
|
-
} catch {
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const records: SkillRecord[] = [];
|
|
115
|
-
for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
|
|
116
|
-
if (entry.endsWith('.md')) {
|
|
117
|
-
const record = await readSkillFile(join(root, entry), origin);
|
|
118
|
-
if (record) records.push(record);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const markerPath = join(root, entry, 'SKILL.md');
|
|
123
|
-
const record = await readSkillFile(markerPath, origin);
|
|
124
|
-
if (record) records.push(record);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return records;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Tags records whose file lives under an installed ecosystem-marketplace
|
|
132
|
-
* receipt's targetPath with that receipt's provenance summary. Best-effort:
|
|
133
|
-
* a lookup failure (e.g. no ecosystem catalogs configured) leaves records
|
|
134
|
-
* untagged rather than failing skill discovery.
|
|
135
|
-
*/
|
|
136
|
-
function applyMarketplaceProvenance(records: SkillRecord[], ecosystemPaths: EcosystemCatalogPathOptions): SkillRecord[] {
|
|
137
|
-
let receipts: ReturnType<typeof listInstalledEcosystemEntries>;
|
|
138
|
-
try {
|
|
139
|
-
receipts = listInstalledEcosystemEntries('skill', ecosystemPaths);
|
|
140
|
-
} catch {
|
|
141
|
-
return records;
|
|
142
|
-
}
|
|
143
|
-
if (receipts.length === 0) return records;
|
|
144
|
-
return records.map((record) => {
|
|
145
|
-
const receipt = receipts.find((candidate) => record.path === candidate.targetPath || record.path.startsWith(`${candidate.targetPath}${sep}`));
|
|
146
|
-
if (!receipt) return record;
|
|
147
|
-
return { ...record, marketplaceProvenance: receipt.provenanceSummary };
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function discoverSkills(
|
|
152
|
-
shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>,
|
|
153
|
-
ecosystemPaths?: EcosystemCatalogPathOptions,
|
|
154
|
-
): Promise<SkillRecord[]> {
|
|
155
|
-
const cwd = shellPaths.workingDirectory;
|
|
156
|
-
const homeDir = shellPaths.homeDirectory;
|
|
157
|
-
const seen = new Set<string>();
|
|
158
|
-
const records: SkillRecord[] = [];
|
|
159
|
-
|
|
160
|
-
for (const { root, origin } of getSkillDirectories(cwd, homeDir)) {
|
|
161
|
-
for (const record of await scanSkillDirectory(root, origin)) {
|
|
162
|
-
if (seen.has(record.name.toLowerCase())) continue;
|
|
163
|
-
seen.add(record.name.toLowerCase());
|
|
164
|
-
records.push(record);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const tagged = ecosystemPaths ? applyMarketplaceProvenance(records, ecosystemPaths) : records;
|
|
169
|
-
|
|
170
|
-
return tagged.sort((a, b) => {
|
|
171
|
-
const originRank = a.origin === b.origin
|
|
172
|
-
? 0
|
|
173
|
-
: a.origin === 'project-local'
|
|
174
|
-
? -1
|
|
175
|
-
: 1;
|
|
176
|
-
return originRank || a.name.localeCompare(b.name);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function wordWrap(text: string, maxWidth: number): string[] {
|
|
181
|
-
if (maxWidth <= 0) return [''];
|
|
182
|
-
const words = text.split(/\s+/).filter(Boolean);
|
|
183
|
-
if (words.length === 0) return [''];
|
|
184
|
-
const lines: string[] = [];
|
|
185
|
-
let line = '';
|
|
186
|
-
for (const word of words) {
|
|
187
|
-
if (!line) {
|
|
188
|
-
line = word;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (line.length + 1 + word.length <= maxWidth) {
|
|
192
|
-
line += ` ${word}`;
|
|
193
|
-
} else {
|
|
194
|
-
lines.push(line);
|
|
195
|
-
line = word;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (line) lines.push(line);
|
|
199
|
-
return lines.length > 0 ? lines : [''];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function truncatePathDisplay(path: string, width: number): string {
|
|
203
|
-
if (width <= 0) return '';
|
|
204
|
-
if (getDisplayWidth(path) <= width) return path;
|
|
205
|
-
|
|
206
|
-
const ellipsis = '…';
|
|
207
|
-
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
208
|
-
if (ellipsisWidth >= width) return truncateDisplay(path, width);
|
|
209
|
-
|
|
210
|
-
const available = width - ellipsisWidth;
|
|
211
|
-
const prefixBudget = Math.max(1, Math.floor(available * 0.35));
|
|
212
|
-
const suffixBudget = Math.max(1, available - prefixBudget);
|
|
213
|
-
const prefix = truncateDisplay(path, prefixBudget, '');
|
|
214
|
-
|
|
215
|
-
let suffix = '';
|
|
216
|
-
let suffixWidth = 0;
|
|
217
|
-
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
218
|
-
const char = path[index]!;
|
|
219
|
-
const charWidth = getDisplayWidth(char);
|
|
220
|
-
if (suffixWidth + charWidth > suffixBudget) break;
|
|
221
|
-
suffix = char + suffix;
|
|
222
|
-
suffixWidth += charWidth;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return `${prefix}${ellipsis}${suffix}`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function originLabel(origin: SkillOrigin): string {
|
|
229
|
-
switch (origin) {
|
|
230
|
-
case 'project-local':
|
|
231
|
-
return 'project';
|
|
232
|
-
case 'global':
|
|
233
|
-
return 'global';
|
|
234
|
-
case 'custom':
|
|
235
|
-
return 'custom';
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function originColor(origin: SkillOrigin): string {
|
|
240
|
-
switch (origin) {
|
|
241
|
-
case 'project-local':
|
|
242
|
-
return C.project;
|
|
243
|
-
case 'global':
|
|
244
|
-
return C.global;
|
|
245
|
-
case 'custom':
|
|
246
|
-
return C.dim;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
|
|
251
|
-
private readonly shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
|
|
252
|
-
private readonly ecosystemPaths: EcosystemCatalogPathOptions | undefined;
|
|
253
|
-
private cached: SkillRecord[] | null = null;
|
|
254
|
-
private cacheDirty = true;
|
|
255
|
-
// I1: confirm state for destructive delete
|
|
256
|
-
private confirm: ConfirmState | null = null;
|
|
257
|
-
private readyPromise: Promise<void> | null = null;
|
|
258
|
-
|
|
259
|
-
public constructor(options: SkillsPanelOptions) {
|
|
260
|
-
super('skills', 'Skills', '▩', 'automation-control', options.componentHealthMonitor);
|
|
261
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
262
|
-
this.filterEnabled = true; // converged modal '/' filter
|
|
263
|
-
this.filterLabel = 'Filter';
|
|
264
|
-
this.shellPaths = options.shellPaths;
|
|
265
|
-
this.ecosystemPaths = options.ecosystemPaths;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// -------------------------------------------------------------------------
|
|
269
|
-
// ScrollableListPanel implementation
|
|
270
|
-
// -------------------------------------------------------------------------
|
|
271
|
-
|
|
272
|
-
protected getItems(): readonly SkillRecord[] {
|
|
273
|
-
return this.cached ?? [];
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private _loadSkillsAsync(): Promise<void> {
|
|
277
|
-
const p = (async () => {
|
|
278
|
-
try {
|
|
279
|
-
await this.withLoading('Scanning skills\u2026', async () => {
|
|
280
|
-
this.cached = await discoverSkills(this.shellPaths, this.ecosystemPaths);
|
|
281
|
-
this.cacheDirty = false;
|
|
282
|
-
this.markDirty();
|
|
283
|
-
});
|
|
284
|
-
} catch (err) {
|
|
285
|
-
this.setError(summarizeError(err));
|
|
286
|
-
}
|
|
287
|
-
this.markDirty();
|
|
288
|
-
})();
|
|
289
|
-
this.readyPromise = p;
|
|
290
|
-
return p;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Resolves when the current load cycle has settled. */
|
|
294
|
-
public awaitReady(): Promise<void> {
|
|
295
|
-
return this.readyPromise ?? Promise.resolve();
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* REAL delete: removes the confirmed skill's markdown file from disk, then
|
|
300
|
-
* rescans so the list reflects the change immediately. Replaces the old
|
|
301
|
-
* setError('Delete via shell: rm …') signpost, which never actually
|
|
302
|
-
* deleted anything.
|
|
303
|
-
*/
|
|
304
|
-
private async _deleteSkill(path: string): Promise<void> {
|
|
305
|
-
try {
|
|
306
|
-
await fsPromises.rm(path);
|
|
307
|
-
this.cacheDirty = true;
|
|
308
|
-
await this._loadSkillsAsync();
|
|
309
|
-
} catch (err) {
|
|
310
|
-
this.setError(summarizeError(err));
|
|
311
|
-
this.markDirty();
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/** `q` arrives already trimmed + lower-cased from ScrollableListPanel.getVisibleItems(). */
|
|
316
|
-
protected override filterMatches(skill: SkillRecord, q: string): boolean {
|
|
317
|
-
const haystack = [
|
|
318
|
-
skill.name,
|
|
319
|
-
skill.description,
|
|
320
|
-
skill.path,
|
|
321
|
-
skill.origin,
|
|
322
|
-
skill.marketplaceProvenance ?? '',
|
|
323
|
-
skill.dependencies.join(' '),
|
|
324
|
-
skill.includes.join(' '),
|
|
325
|
-
].join(' ').toLowerCase();
|
|
326
|
-
return haystack.includes(q);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
protected renderItem(skill: SkillRecord, index: number, selected: boolean, width: number): Line {
|
|
330
|
-
const bg = selected ? C.selectBg : undefined;
|
|
331
|
-
const dot = skill.origin === 'project-local' ? '\u25c6' : '\u2022';
|
|
332
|
-
// Marketplace-installed skills get a badge glyph; unbadged column stays a
|
|
333
|
-
// single blank cell so rows stay aligned either way.
|
|
334
|
-
const marketplaceBadge = skill.marketplaceProvenance ? '\u2605' : ' ';
|
|
335
|
-
const desc = skill.description || 'No description provided.';
|
|
336
|
-
const descWidth = Math.max(1, width - 6 - skill.name.length - 6);
|
|
337
|
-
const descLines = wordWrap(desc, descWidth);
|
|
338
|
-
return buildPanelLine(width, [
|
|
339
|
-
[selected ? '\u25b8' : ' ', C.value, bg],
|
|
340
|
-
[' ', C.dim, bg],
|
|
341
|
-
[dot, originColor(skill.origin), bg],
|
|
342
|
-
[' ', C.dim, bg],
|
|
343
|
-
[marketplaceBadge, C.info, bg],
|
|
344
|
-
[' ', C.dim, bg],
|
|
345
|
-
[skill.name, selected ? C.value : C.value, bg],
|
|
346
|
-
[' ', C.dim, bg],
|
|
347
|
-
[descLines[0] ?? '', selected ? C.value : C.dim, bg],
|
|
348
|
-
]);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
protected override getPalette() { return C; }
|
|
352
|
-
protected override getEmptyStateMessage() { return ' No skills discovered.'; }
|
|
353
|
-
protected override getEmptyStateActions() {
|
|
354
|
-
return [
|
|
355
|
-
{ command: '.goodvibes/skills', summary: 'place skill .md files here (project-local) or ~/.goodvibes/skills (global)' },
|
|
356
|
-
{ command: '/registry search skills', summary: 'inspect the same skill directories from the shell' },
|
|
357
|
-
];
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
public override onActivate(): void {
|
|
361
|
-
super.onActivate();
|
|
362
|
-
this.filterQuery = '';
|
|
363
|
-
this.filterActive = false;
|
|
364
|
-
this.cacheDirty = true;
|
|
365
|
-
void this._loadSkillsAsync();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
public override onDestroy(): void {}
|
|
369
|
-
|
|
370
|
-
// (the purge): Enter used to open the skill's markdown source in the
|
|
371
|
-
// preview panel via a staged pendingOpenPreview flag + handlePanelIntegrationAction
|
|
372
|
-
// (same pattern diff-panel.ts used). 'preview' is DELETE-disposition with no
|
|
373
|
-
// successor surface (no file-picker-overlay preview to repoint to either,
|
|
374
|
-
// verified), so that cross-panel jump was removed rather than repointed;
|
|
375
|
-
// Enter is currently a no-op on this list (browse-only until migrates
|
|
376
|
-
// Skills to a modal).
|
|
377
|
-
|
|
378
|
-
public handleInput(key: string): boolean {
|
|
379
|
-
// I1: y/n confirmation dialog for delete
|
|
380
|
-
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
381
|
-
if (confirmResult === 'confirmed') {
|
|
382
|
-
const toDelete = this.confirm!.subject;
|
|
383
|
-
this.confirm = null;
|
|
384
|
-
void this._deleteSkill(toDelete);
|
|
385
|
-
this.markDirty();
|
|
386
|
-
return true;
|
|
387
|
-
}
|
|
388
|
-
if (confirmResult === 'cancelled') {
|
|
389
|
-
this.confirm = null;
|
|
390
|
-
this.markDirty();
|
|
391
|
-
return true;
|
|
392
|
-
}
|
|
393
|
-
if (confirmResult === 'absorbed') return true;
|
|
394
|
-
|
|
395
|
-
// I1: 'd' prompts delete confirmation, only outside filter mode, so 'd'
|
|
396
|
-
// remains typeable into the filter query while it is active (// converged modal '/' filter coexists with single-letter action keys).
|
|
397
|
-
if (!this.filterActive && key === 'd') {
|
|
398
|
-
const skill = this.getSelectedItem();
|
|
399
|
-
if (skill) {
|
|
400
|
-
this.confirm = { subject: skill.path, label: skill.name };
|
|
401
|
-
this.markDirty();
|
|
402
|
-
}
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Navigation + filter: delegate to ScrollableListPanel ('/' enters
|
|
407
|
-
// filter, typing narrows, Esc clears, up/down/g/G/page/enter navigate).
|
|
408
|
-
return super.handleInput(key);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
public render(width: number, height: number): Line[] {
|
|
412
|
-
return this.trackedRender(() => {
|
|
413
|
-
this.needsRender = false;
|
|
414
|
-
|
|
415
|
-
// I1: show confirm dialog in place of normal content
|
|
416
|
-
if (this.confirm) {
|
|
417
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
418
|
-
title: 'Skills - confirm action',
|
|
419
|
-
intro: '',
|
|
420
|
-
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
421
|
-
palette: C,
|
|
422
|
-
});
|
|
423
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
424
|
-
return lines.slice(0, height);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Build detail footer for the currently selected skill
|
|
428
|
-
const selected = this.getSelectedItem();
|
|
429
|
-
const detailLines: Line[] = [];
|
|
430
|
-
if (selected) {
|
|
431
|
-
detailLines.push(
|
|
432
|
-
buildPanelLine(width, [[' Selected: ', C.label], [selected.name, C.value], [' [', C.dim], [originLabel(selected.origin), originColor(selected.origin)], [']', C.dim]]),
|
|
433
|
-
buildPanelLine(width, [[' Path: ', C.label], [truncatePathDisplay(selected.path, Math.max(1, width - 8)), C.label]]),
|
|
434
|
-
buildPanelLine(width, [[' Desc: ', C.label], [selected.description || 'No description provided.', C.value]]),
|
|
435
|
-
buildPanelLine(width, [[' Depends: ', C.label], [selected.dependencies.length > 0 ? selected.dependencies.join(', ') : 'none', C.dim]]),
|
|
436
|
-
buildPanelLine(width, [[' Includes: ', C.label], [selected.includes.length > 0 ? selected.includes.join(', ') : 'none', C.dim]]),
|
|
437
|
-
buildPanelLine(width, [
|
|
438
|
-
[' Provenance: ', C.label],
|
|
439
|
-
[selected.marketplaceProvenance ?? 'not installed via marketplace', selected.marketplaceProvenance ? C.info : C.dim],
|
|
440
|
-
]),
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
detailLines.push(buildPanelLine(width, [[' Up/Down navigate / filter Esc clear Backspace edit', C.dim]]));
|
|
444
|
-
detailLines.push(buildPanelLine(width, [[' Enter open in preview d delete (confirm)', C.dim]]));
|
|
445
|
-
|
|
446
|
-
// Filter input line is auto-injected by renderList() (filterEnabled=true).
|
|
447
|
-
const lines = this.renderList(width, height, {
|
|
448
|
-
title: 'Skills - discover project-local and global skill packs',
|
|
449
|
-
footer: detailLines,
|
|
450
|
-
});
|
|
451
|
-
return lines;
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|