@pellux/goodvibes-agent 0.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/.goodvibes/GOODVIBES.md +35 -0
- package/.goodvibes/agents/reviewer.md +48 -0
- package/.goodvibes/skills/add-provider/SKILL.md +199 -0
- package/CHANGELOG.md +25 -0
- package/README.md +74 -0
- package/bin/goodvibes-agent.ts +2 -0
- package/docs/README.md +23 -0
- package/docs/deployment-and-services.md +57 -0
- package/docs/getting-started.md +53 -0
- package/docs/release-and-publishing.md +46 -0
- package/package.json +134 -0
- package/scripts/check-bun.sh +20 -0
- package/src/audio/player.ts +156 -0
- package/src/audio/spoken-turn-controller.ts +203 -0
- package/src/audio/spoken-turn-model-routing.ts +117 -0
- package/src/audio/spoken-turn-wiring.ts +44 -0
- package/src/audio/text-chunker.ts +110 -0
- package/src/cli/bundle-command.ts +227 -0
- package/src/cli/completion.ts +90 -0
- package/src/cli/config-overrides.ts +159 -0
- package/src/cli/endpoints.ts +63 -0
- package/src/cli/entrypoint.ts +172 -0
- package/src/cli/help.ts +299 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/management-commands.ts +426 -0
- package/src/cli/management.ts +744 -0
- package/src/cli/network-posture.ts +46 -0
- package/src/cli/package-verification.ts +123 -0
- package/src/cli/parser.ts +369 -0
- package/src/cli/provider-auth-routes.ts +22 -0
- package/src/cli/provider-classification.ts +107 -0
- package/src/cli/redaction.ts +105 -0
- package/src/cli/service-command.ts +26 -0
- package/src/cli/service-posture.ts +482 -0
- package/src/cli/status.ts +383 -0
- package/src/cli/surface-command.ts +247 -0
- package/src/cli/tui-startup.ts +32 -0
- package/src/cli/types.ts +69 -0
- package/src/cli-flags.ts +21 -0
- package/src/config/goodvibes-home-audit.ts +465 -0
- package/src/config/index.ts +57 -0
- package/src/config/provider-model.ts +23 -0
- package/src/config/secret-config.ts +119 -0
- package/src/config/secrets.ts +71 -0
- package/src/config/surface.ts +1 -0
- package/src/core/composer-state.ts +61 -0
- package/src/core/conversation-rendering.ts +359 -0
- package/src/core/conversation.ts +551 -0
- package/src/core/history.ts +45 -0
- package/src/core/orchestrator.ts +7 -0
- package/src/core/system-message-router.ts +171 -0
- package/src/daemon/cli.ts +55 -0
- package/src/daemon/safe-serve.ts +61 -0
- package/src/input/agent-workspace.ts +428 -0
- package/src/input/autocomplete.ts +96 -0
- package/src/input/bookmark-modal.ts +115 -0
- package/src/input/command-args-hint.ts +36 -0
- package/src/input/command-registry.ts +329 -0
- package/src/input/commands/agent-externalized-tui.ts +73 -0
- package/src/input/commands/agent-workspace-runtime.ts +17 -0
- package/src/input/commands/branch-runtime.ts +72 -0
- package/src/input/commands/cloudflare-runtime.ts +370 -0
- package/src/input/commands/config.ts +18 -0
- package/src/input/commands/control-room-runtime.ts +255 -0
- package/src/input/commands/conversation-runtime.ts +207 -0
- package/src/input/commands/discovery-runtime.ts +52 -0
- package/src/input/commands/eval.ts +204 -0
- package/src/input/commands/experience-runtime.ts +278 -0
- package/src/input/commands/guidance-runtime.ts +106 -0
- package/src/input/commands/health-runtime.ts +434 -0
- package/src/input/commands/hooks-runtime.ts +148 -0
- package/src/input/commands/incident-runtime.ts +95 -0
- package/src/input/commands/integration-runtime.ts +394 -0
- package/src/input/commands/intelligence-runtime.ts +223 -0
- package/src/input/commands/knowledge.ts +531 -0
- package/src/input/commands/local-auth-runtime.ts +105 -0
- package/src/input/commands/local-provider-runtime.ts +170 -0
- package/src/input/commands/local-runtime.ts +392 -0
- package/src/input/commands/local-setup-review.ts +199 -0
- package/src/input/commands/local-setup-transfer.ts +135 -0
- package/src/input/commands/local-setup.ts +282 -0
- package/src/input/commands/managed-runtime.ts +209 -0
- package/src/input/commands/marketplace-runtime.ts +290 -0
- package/src/input/commands/mcp-runtime.ts +432 -0
- package/src/input/commands/memory-product-runtime.ts +111 -0
- package/src/input/commands/memory.ts +151 -0
- package/src/input/commands/notify-runtime.ts +83 -0
- package/src/input/commands/onboarding-runtime.ts +14 -0
- package/src/input/commands/operator-panel-runtime.ts +146 -0
- package/src/input/commands/operator-runtime.ts +392 -0
- package/src/input/commands/planning-runtime.ts +205 -0
- package/src/input/commands/platform-access-runtime.ts +422 -0
- package/src/input/commands/platform-services-runtime.ts +246 -0
- package/src/input/commands/policy-dispatch.ts +339 -0
- package/src/input/commands/policy.ts +17 -0
- package/src/input/commands/product-runtime.ts +351 -0
- package/src/input/commands/profile-sync-runtime.ts +99 -0
- package/src/input/commands/provider-accounts-runtime.ts +113 -0
- package/src/input/commands/provider.ts +363 -0
- package/src/input/commands/qrcode-runtime.ts +20 -0
- package/src/input/commands/quit-shared.ts +162 -0
- package/src/input/commands/recall-bundle.ts +132 -0
- package/src/input/commands/recall-capture.ts +152 -0
- package/src/input/commands/recall-query.ts +229 -0
- package/src/input/commands/recall-review.ts +98 -0
- package/src/input/commands/recall-shared.ts +22 -0
- package/src/input/commands/remote-runtime-pool.ts +106 -0
- package/src/input/commands/remote-runtime-setup.ts +199 -0
- package/src/input/commands/remote-runtime.ts +431 -0
- package/src/input/commands/replay-runtime.ts +18 -0
- package/src/input/commands/runtime-services.ts +291 -0
- package/src/input/commands/schedule-runtime.ts +91 -0
- package/src/input/commands/services-runtime.ts +209 -0
- package/src/input/commands/session-content.ts +408 -0
- package/src/input/commands/session-workflow.ts +464 -0
- package/src/input/commands/session.ts +375 -0
- package/src/input/commands/settings-sync-runtime.ts +174 -0
- package/src/input/commands/share-runtime.ts +119 -0
- package/src/input/commands/shell-core.ts +307 -0
- package/src/input/commands/skills-runtime.ts +221 -0
- package/src/input/commands/subscription-runtime.ts +434 -0
- package/src/input/commands/tasks-runtime.ts +230 -0
- package/src/input/commands/teamwork-runtime.ts +339 -0
- package/src/input/commands/teleport-runtime.ts +57 -0
- package/src/input/commands/tts-runtime.ts +29 -0
- package/src/input/commands/work-plan-runtime.ts +169 -0
- package/src/input/commands.ts +131 -0
- package/src/input/feed-context-factory.ts +254 -0
- package/src/input/file-picker.ts +192 -0
- package/src/input/handler-command-route.ts +180 -0
- package/src/input/handler-content-actions.ts +497 -0
- package/src/input/handler-feed-routes.ts +648 -0
- package/src/input/handler-feed.ts +452 -0
- package/src/input/handler-interactions.ts +281 -0
- package/src/input/handler-modal-routes.ts +418 -0
- package/src/input/handler-modal-stack.ts +263 -0
- package/src/input/handler-modal-token-routes.ts +329 -0
- package/src/input/handler-onboarding-cloudflare.ts +391 -0
- package/src/input/handler-onboarding.ts +620 -0
- package/src/input/handler-picker-routes.ts +472 -0
- package/src/input/handler-prompt-buffer.ts +320 -0
- package/src/input/handler-shortcuts.ts +213 -0
- package/src/input/handler-ui-state.ts +372 -0
- package/src/input/handler.ts +729 -0
- package/src/input/input-history.ts +297 -0
- package/src/input/keybindings.ts +292 -0
- package/src/input/mcp-workspace.ts +554 -0
- package/src/input/model-picker-provider-filter.ts +28 -0
- package/src/input/model-picker-types.ts +137 -0
- package/src/input/model-picker.ts +797 -0
- package/src/input/onboarding/handler-onboarding-routes.ts +125 -0
- package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +277 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +158 -0
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +130 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +762 -0
- package/src/input/onboarding/onboarding-wizard-helpers.ts +167 -0
- package/src/input/onboarding/onboarding-wizard-rules.ts +256 -0
- package/src/input/onboarding/onboarding-wizard-state.ts +365 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +798 -0
- package/src/input/onboarding/onboarding-wizard-types.ts +195 -0
- package/src/input/onboarding/onboarding-wizard.ts +711 -0
- package/src/input/panel-integration-actions.ts +78 -0
- package/src/input/profile-picker-modal.ts +222 -0
- package/src/input/search.ts +100 -0
- package/src/input/selection-modal.ts +163 -0
- package/src/input/selection.ts +135 -0
- package/src/input/session-picker-modal.ts +136 -0
- package/src/input/settings-modal-behavior.ts +37 -0
- package/src/input/settings-modal-secrets.ts +41 -0
- package/src/input/settings-modal-subscriptions.ts +95 -0
- package/src/input/settings-modal-types.ts +91 -0
- package/src/input/settings-modal.ts +793 -0
- package/src/input/submission-intent.ts +17 -0
- package/src/input/submission-router.ts +59 -0
- package/src/input/tts-settings-actions.ts +100 -0
- package/src/main.ts +792 -0
- package/src/mcp/runtime-reload.ts +81 -0
- package/src/panels/agent-inspector-panel.ts +521 -0
- package/src/panels/agent-inspector-shared.ts +94 -0
- package/src/panels/agent-logs-panel.ts +559 -0
- package/src/panels/agent-logs-shared.ts +129 -0
- package/src/panels/approval-panel.ts +150 -0
- package/src/panels/automation-control-panel.ts +212 -0
- package/src/panels/base-panel.ts +254 -0
- package/src/panels/builtin/agent.ts +117 -0
- package/src/panels/builtin/development.ts +31 -0
- package/src/panels/builtin/knowledge.ts +26 -0
- package/src/panels/builtin/operations.ts +349 -0
- package/src/panels/builtin/session.ts +129 -0
- package/src/panels/builtin/shared.ts +274 -0
- package/src/panels/builtin-panels.ts +23 -0
- package/src/panels/cockpit-panel.ts +183 -0
- package/src/panels/communication-panel.ts +153 -0
- package/src/panels/confirm-state.ts +61 -0
- package/src/panels/context-visualizer-panel.ts +204 -0
- package/src/panels/control-plane-panel.ts +211 -0
- package/src/panels/cost-tracker-panel.ts +444 -0
- package/src/panels/debug-panel.ts +432 -0
- package/src/panels/diff-panel.ts +520 -0
- package/src/panels/docs-panel.ts +283 -0
- package/src/panels/eval-panel.ts +399 -0
- package/src/panels/file-explorer-panel.ts +584 -0
- package/src/panels/file-preview-panel.ts +434 -0
- package/src/panels/forensics-panel.ts +364 -0
- package/src/panels/git-panel.ts +638 -0
- package/src/panels/hooks-panel.ts +239 -0
- package/src/panels/incident-review-panel.ts +197 -0
- package/src/panels/index.ts +46 -0
- package/src/panels/intelligence-panel.ts +176 -0
- package/src/panels/knowledge-panel.ts +345 -0
- package/src/panels/local-auth-panel.ts +130 -0
- package/src/panels/marketplace-panel.ts +212 -0
- package/src/panels/memory-panel.ts +225 -0
- package/src/panels/ops-control-panel.ts +150 -0
- package/src/panels/ops-strategy-panel.ts +235 -0
- package/src/panels/orchestration-panel.ts +273 -0
- package/src/panels/panel-list-panel.ts +509 -0
- package/src/panels/panel-manager.ts +570 -0
- package/src/panels/panel-picker.ts +106 -0
- package/src/panels/plan-dashboard-panel.ts +274 -0
- package/src/panels/plugins-panel.ts +178 -0
- package/src/panels/policy-panel.ts +308 -0
- package/src/panels/polish.ts +717 -0
- package/src/panels/project-planning-panel.ts +711 -0
- package/src/panels/provider-account-snapshot.ts +259 -0
- package/src/panels/provider-accounts-panel.ts +218 -0
- package/src/panels/provider-health-domains.ts +215 -0
- package/src/panels/provider-health-panel.ts +727 -0
- package/src/panels/provider-health-tracker.ts +115 -0
- package/src/panels/provider-stats-panel.ts +366 -0
- package/src/panels/qr-panel.ts +182 -0
- package/src/panels/remote-panel.ts +449 -0
- package/src/panels/routes-panel.ts +178 -0
- package/src/panels/sandbox-panel.ts +283 -0
- package/src/panels/schedule-panel.ts +329 -0
- package/src/panels/scrollable-list-panel.ts +491 -0
- package/src/panels/search-focus.ts +32 -0
- package/src/panels/security-panel.ts +295 -0
- package/src/panels/services-panel.ts +231 -0
- package/src/panels/session-browser-panel.ts +400 -0
- package/src/panels/session-maintenance.ts +125 -0
- package/src/panels/settings-sync-panel.ts +120 -0
- package/src/panels/skills-panel.ts +431 -0
- package/src/panels/subscription-panel.ts +263 -0
- package/src/panels/symbol-outline-panel.ts +486 -0
- package/src/panels/system-messages-panel.ts +230 -0
- package/src/panels/tasks-panel.ts +399 -0
- package/src/panels/thinking-panel.ts +304 -0
- package/src/panels/token-budget-panel.ts +475 -0
- package/src/panels/tool-inspector-panel.ts +429 -0
- package/src/panels/types.ts +54 -0
- package/src/panels/watchers-panel.ts +193 -0
- package/src/panels/work-plan-panel.ts +175 -0
- package/src/panels/worktree-panel.ts +182 -0
- package/src/panels/wrfc-panel.ts +609 -0
- package/src/permissions/prompt.ts +165 -0
- package/src/planning/project-planning-coordinator.ts +543 -0
- package/src/plugins/loader.ts +15 -0
- package/src/renderer/agent-detail-modal.ts +331 -0
- package/src/renderer/agent-workspace.ts +238 -0
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/autocomplete-overlay.ts +154 -0
- package/src/renderer/block-actions.ts +76 -0
- package/src/renderer/bookmark-modal.ts +101 -0
- package/src/renderer/bottom-bar.ts +58 -0
- package/src/renderer/buffer.ts +113 -0
- package/src/renderer/code-block.ts +373 -0
- package/src/renderer/compositor.ts +283 -0
- package/src/renderer/context-inspector.ts +219 -0
- package/src/renderer/conversation-layout.ts +67 -0
- package/src/renderer/conversation-overlays.ts +140 -0
- package/src/renderer/conversation-surface.ts +260 -0
- package/src/renderer/diff-view.ts +132 -0
- package/src/renderer/diff.ts +130 -0
- package/src/renderer/file-picker-overlay.ts +101 -0
- package/src/renderer/file-tree.ts +153 -0
- package/src/renderer/fullscreen-primitives.ts +130 -0
- package/src/renderer/fullscreen-workspace.ts +199 -0
- package/src/renderer/git-status.ts +89 -0
- package/src/renderer/help-overlay.ts +267 -0
- package/src/renderer/history-search-overlay.ts +73 -0
- package/src/renderer/layout-engine.ts +97 -0
- package/src/renderer/layout.ts +32 -0
- package/src/renderer/live-tail-modal.ts +156 -0
- package/src/renderer/markdown.ts +635 -0
- package/src/renderer/mcp-workspace.ts +237 -0
- package/src/renderer/modal-factory.ts +467 -0
- package/src/renderer/modal-utils.ts +24 -0
- package/src/renderer/model-picker-overlay.ts +473 -0
- package/src/renderer/model-workspace.ts +488 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +615 -0
- package/src/renderer/overlay-box.ts +146 -0
- package/src/renderer/overlay-viewport.ts +104 -0
- package/src/renderer/panel-composite.ts +158 -0
- package/src/renderer/panel-picker-overlay.ts +202 -0
- package/src/renderer/panel-tab-bar.ts +69 -0
- package/src/renderer/panel-workspace-bar.ts +42 -0
- package/src/renderer/process-indicator.ts +96 -0
- package/src/renderer/process-modal.ts +656 -0
- package/src/renderer/process-summary.ts +67 -0
- package/src/renderer/profile-picker-modal.ts +129 -0
- package/src/renderer/progress.ts +98 -0
- package/src/renderer/qr-renderer.ts +120 -0
- package/src/renderer/search-overlay.ts +54 -0
- package/src/renderer/selection-modal-overlay.ts +214 -0
- package/src/renderer/semantic-diff.ts +369 -0
- package/src/renderer/session-picker-modal.ts +127 -0
- package/src/renderer/settings-modal-helpers.ts +193 -0
- package/src/renderer/settings-modal.ts +537 -0
- package/src/renderer/shell-surface.ts +88 -0
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +67 -0
- package/src/renderer/surface-layout.ts +101 -0
- package/src/renderer/syntax-highlighter.ts +542 -0
- package/src/renderer/system-message.ts +83 -0
- package/src/renderer/tab-strip.ts +108 -0
- package/src/renderer/text-layout.ts +31 -0
- package/src/renderer/thinking.ts +17 -0
- package/src/renderer/tool-call.ts +234 -0
- package/src/renderer/ui-factory.ts +524 -0
- package/src/renderer/ui-primitives.ts +96 -0
- package/src/runtime/bootstrap-command-context.ts +278 -0
- package/src/runtime/bootstrap-command-parts.ts +386 -0
- package/src/runtime/bootstrap-core.ts +540 -0
- package/src/runtime/bootstrap-hook-bridge.ts +112 -0
- package/src/runtime/bootstrap-shell.ts +283 -0
- package/src/runtime/bootstrap.ts +575 -0
- package/src/runtime/cloudflare-control-plane.ts +349 -0
- package/src/runtime/context.ts +142 -0
- package/src/runtime/diagnostics/panels/index.ts +24 -0
- package/src/runtime/diagnostics/panels/ops.ts +156 -0
- package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
- package/src/runtime/diagnostics/panels/policy.ts +177 -0
- package/src/runtime/index.ts +662 -0
- package/src/runtime/onboarding/apply.ts +642 -0
- package/src/runtime/onboarding/derivation.ts +534 -0
- package/src/runtime/onboarding/index.ts +7 -0
- package/src/runtime/onboarding/markers.ts +148 -0
- package/src/runtime/onboarding/snapshot.ts +406 -0
- package/src/runtime/onboarding/state.ts +141 -0
- package/src/runtime/onboarding/types.ts +404 -0
- package/src/runtime/onboarding/verify.ts +171 -0
- package/src/runtime/operator-token-cleanup.ts +27 -0
- package/src/runtime/perf/panel-contracts.ts +32 -0
- package/src/runtime/perf/panel-health-monitor.ts +18 -0
- package/src/runtime/sandbox-public-gaps.ts +358 -0
- package/src/runtime/services.ts +670 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +15 -0
- package/src/runtime/store/domains/index.ts +222 -0
- package/src/runtime/store/domains/panels.ts +117 -0
- package/src/runtime/store/domains/ui-perf.ts +103 -0
- package/src/runtime/store/index.ts +305 -0
- package/src/runtime/store/selectors/index.ts +359 -0
- package/src/runtime/store/state.ts +145 -0
- package/src/runtime/surface-feature-flags.ts +65 -0
- package/src/runtime/terminal-output-guard.ts +228 -0
- package/src/runtime/ui/index.ts +39 -0
- package/src/runtime/ui/model-picker/data-provider.ts +182 -0
- package/src/runtime/ui/model-picker/health-enrichment.ts +228 -0
- package/src/runtime/ui/model-picker/index.ts +59 -0
- package/src/runtime/ui/model-picker/types.ts +149 -0
- package/src/runtime/ui/provider-health/data-provider.ts +244 -0
- package/src/runtime/ui/provider-health/fallback-visualizer.ts +71 -0
- package/src/runtime/ui/provider-health/index.ts +46 -0
- package/src/runtime/ui/provider-health/types.ts +146 -0
- package/src/runtime/ui-events.ts +1 -0
- package/src/runtime/ui-read-model-helpers.ts +1 -0
- package/src/runtime/ui-read-models-observability-maintenance.ts +1 -0
- package/src/runtime/ui-read-models-observability-options.ts +1 -0
- package/src/runtime/ui-read-models-observability-remote.ts +1 -0
- package/src/runtime/ui-read-models-observability-security.ts +1 -0
- package/src/runtime/ui-read-models-observability-system.ts +1 -0
- package/src/runtime/ui-read-models-observability.ts +1 -0
- package/src/runtime/ui-read-models.ts +61 -0
- package/src/runtime/ui-service-queries.ts +1 -0
- package/src/runtime/ui-services.ts +190 -0
- package/src/scripts/process-messages.ts +42 -0
- package/src/shell/blocking-input.ts +98 -0
- package/src/shell/service-settings-sync.ts +273 -0
- package/src/shell/ui-openers.ts +352 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/wrfc-agent-guard.ts +49 -0
- package/src/types/grid.ts +48 -0
- package/src/types/sql-js.d.ts +15 -0
- package/src/utils/clipboard.ts +22 -0
- package/src/utils/splash-lines.ts +46 -0
- package/src/utils/terminal-width.ts +185 -0
- package/src/verification/live-verifier.ts +430 -0
- package/src/verification/verification-ledger.ts +242 -0
- package/src/version.ts +17 -0
- package/src/widget/index.ts +2 -0
- package/src/widget/types.ts +9 -0
- package/src/widget/widget.ts +8 -0
- package/src/work-plans/work-plan-store.ts +374 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
|
+
import {
|
|
4
|
+
buildGuidanceLine,
|
|
5
|
+
buildKeyValueLine,
|
|
6
|
+
buildPanelLine,
|
|
7
|
+
DEFAULT_PANEL_PALETTE,
|
|
8
|
+
} from './polish.ts';
|
|
9
|
+
import type { PolicyRuntimeState } from '@/runtime/index.ts';
|
|
10
|
+
import { buildPermissionRuleSuggestions } from '@/runtime/index.ts';
|
|
11
|
+
|
|
12
|
+
const C = {
|
|
13
|
+
...DEFAULT_PANEL_PALETTE,
|
|
14
|
+
headerBg: '#111827',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
const APPROVAL_ROWS = [
|
|
18
|
+
['shell', 'why prompted: side effects, destructive ops, secret exposure, escalation', 'review via /security and /policy preflight'],
|
|
19
|
+
['file', 'why prompted: config mutation, notebook edits, secret-bearing paths', 'review via /approval review file'],
|
|
20
|
+
['network', 'why prompted: external hosts, fetch scope, egress policy', 'review via /approval review network'],
|
|
21
|
+
['delegate', 'why prompted: recursive agents, spawn ceilings, write-set inheritance', 'review via /approval review delegate'],
|
|
22
|
+
['mcp', 'why prompted: trust escalation, host scope, path scope, coherence mismatch', 'review via /mcp trust and /security'],
|
|
23
|
+
['remote', 'why prompted: runner trust, remote write scope, artifact requirements', 'review via /remote and delegated TUI sandbox context'],
|
|
24
|
+
['hook', 'why prompted: deny/mutate authority, blocking behavior, runner provenance', 'review via /hooks and /security'],
|
|
25
|
+
['plugin', 'why prompted: install/update lifecycle, provenance, capability grants', 'review via /marketplace and /security'],
|
|
26
|
+
['sandbox', 'why prompted: WSL/VM isolation changes alter host risk posture', 'delegate sandbox changes to GoodVibes TUI'],
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
type ApprovalRow = (typeof APPROVAL_ROWS)[number];
|
|
30
|
+
|
|
31
|
+
export class ApprovalPanel extends ScrollableListPanel<ApprovalRow> {
|
|
32
|
+
private readonly policyRuntimeState: Pick<PolicyRuntimeState, 'getSnapshot'>;
|
|
33
|
+
|
|
34
|
+
public constructor(policyRuntimeState: Pick<PolicyRuntimeState, 'getSnapshot'>) {
|
|
35
|
+
super('approval', 'Approval', 'A', 'monitoring');
|
|
36
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
37
|
+
this.policyRuntimeState = policyRuntimeState;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected override getPalette() { return C; }
|
|
41
|
+
protected override getEmptyStateMessage() { return ' No approval lanes defined.'; }
|
|
42
|
+
|
|
43
|
+
protected getItems(): readonly ApprovalRow[] {
|
|
44
|
+
return APPROVAL_ROWS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected renderItem(row: ApprovalRow, index: number, selected: boolean, width: number): Line {
|
|
48
|
+
const bg = selected ? C.selectBg : undefined;
|
|
49
|
+
return buildPanelLine(width, [
|
|
50
|
+
[' ', C.label],
|
|
51
|
+
[row[0].padEnd(10), C.info, bg],
|
|
52
|
+
[row[1].slice(0, Math.max(0, width - 18)), C.value, bg],
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public handleInput(key: string): boolean {
|
|
57
|
+
if (key === 'home') {
|
|
58
|
+
this.selectedIndex = 0;
|
|
59
|
+
this.markDirty();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (key === 'end') {
|
|
63
|
+
this.selectedIndex = APPROVAL_ROWS.length - 1;
|
|
64
|
+
this.markDirty();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (key === 'enter' || key === 'return') {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return super.handleInput(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public getSelectedCommand(): string | null {
|
|
74
|
+
const selected = APPROVAL_ROWS[this.selectedIndex] ?? null;
|
|
75
|
+
return selected ? selected[2].replace('review via ', '').trim() : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public render(width: number, height: number): Line[] {
|
|
79
|
+
this.clampSelection();
|
|
80
|
+
const policySnapshot = this.policyRuntimeState.getSnapshot();
|
|
81
|
+
const approvalCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === true).length;
|
|
82
|
+
const denialCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === false).length;
|
|
83
|
+
const pendingCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === undefined).length;
|
|
84
|
+
|
|
85
|
+
const selected = APPROVAL_ROWS[this.selectedIndex] ?? null;
|
|
86
|
+
const detailLines: Line[] = [];
|
|
87
|
+
if (selected) {
|
|
88
|
+
detailLines.push(buildPanelLine(width, [[' Selected Lane', C.label]]));
|
|
89
|
+
detailLines.push(buildKeyValueLine(width, [
|
|
90
|
+
{ label: 'lane', value: selected[0], valueColor: C.info },
|
|
91
|
+
{ label: 'next review', value: selected[2], valueColor: C.dim },
|
|
92
|
+
], C));
|
|
93
|
+
detailLines.push(buildPanelLine(width, [[` ${selected[1]}`, C.value]]));
|
|
94
|
+
detailLines.push(buildGuidanceLine(width, selected[2].replace('review via ', ''), `open the ${selected[0]} review path`, C));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const recentAuditLines: Line[] = [];
|
|
98
|
+
for (const entry of policySnapshot.recentPermissionAudit.slice(0, 5)) {
|
|
99
|
+
const decision = entry.approved === undefined ? 'pending' : entry.approved ? 'approved' : 'denied';
|
|
100
|
+
const decisionColor = entry.approved === undefined ? C.info : entry.approved ? C.good : C.bad;
|
|
101
|
+
recentAuditLines.push(buildPanelLine(width, [
|
|
102
|
+
[` ${decision.padEnd(8)}`, decisionColor],
|
|
103
|
+
[`${entry.tool}`.padEnd(14), C.label],
|
|
104
|
+
[entry.summary.slice(0, Math.max(0, width - 28)), C.value],
|
|
105
|
+
]));
|
|
106
|
+
if (entry.reasons[0]) {
|
|
107
|
+
recentAuditLines.push(buildPanelLine(width, [[` ${entry.reasons[0]}`, C.dim]]));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (recentAuditLines.length === 0) {
|
|
111
|
+
recentAuditLines.push(buildPanelLine(width, [[` No recent approval pressure. Live requests and decisions will appear here.`, C.dim]]));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ruleSuggestionLines: Line[] = [];
|
|
115
|
+
for (const suggestion of buildPermissionRuleSuggestions(policySnapshot.recentPermissionAudit).slice(0, 3)) {
|
|
116
|
+
ruleSuggestionLines.push(buildPanelLine(width, [[` ${suggestion.summary}`, C.info]]));
|
|
117
|
+
ruleSuggestionLines.push(buildGuidanceLine(width, suggestion.command, suggestion.reason, C));
|
|
118
|
+
}
|
|
119
|
+
if (ruleSuggestionLines.length === 0) {
|
|
120
|
+
ruleSuggestionLines.push(buildPanelLine(width, [[` No repeated denials currently suggest a durable rule.`, C.dim]]));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const headerLines: Line[] = [
|
|
124
|
+
buildPanelLine(width, [[' Approval posture', C.label]]),
|
|
125
|
+
buildKeyValueLine(width, [
|
|
126
|
+
{ label: 'why prompted', value: 'risk summary', valueColor: C.value },
|
|
127
|
+
{ label: 'what-if', value: '/policy simulate + preflight', valueColor: C.info },
|
|
128
|
+
{ label: 'operator', value: '/security + /cockpit', valueColor: C.good },
|
|
129
|
+
], C),
|
|
130
|
+
buildPanelLine(width, [
|
|
131
|
+
[' \u2713 ', C.good],
|
|
132
|
+
[`approvals (${approvalCount}) `, C.good],
|
|
133
|
+
['\u2715 ', C.bad],
|
|
134
|
+
[`denials (${denialCount}) `, C.bad],
|
|
135
|
+
['\u25cb ', C.info],
|
|
136
|
+
[`pending (${pendingCount})`, C.info],
|
|
137
|
+
]),
|
|
138
|
+
buildGuidanceLine(width, '/approval review shell', 'inspect the highest-risk approval lane and refine scoped review posture', C),
|
|
139
|
+
...detailLines,
|
|
140
|
+
...recentAuditLines,
|
|
141
|
+
...ruleSuggestionLines,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
return this.renderList(width, height, {
|
|
145
|
+
title: 'Approval Control Room',
|
|
146
|
+
header: headerLines,
|
|
147
|
+
footer: [buildPanelLine(width, [[` Up/Down move Home/End jump selected lane opens the next command path`, C.dim]])],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
+
import type { UiAutomationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
5
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
|
+
import {
|
|
7
|
+
buildEmptyState,
|
|
8
|
+
buildGuidanceLine,
|
|
9
|
+
buildKeyValueLine,
|
|
10
|
+
buildPanelLine,
|
|
11
|
+
buildPanelWorkspace,
|
|
12
|
+
DEFAULT_PANEL_PALETTE,
|
|
13
|
+
type PanelPalette,
|
|
14
|
+
} from './polish.ts';
|
|
15
|
+
|
|
16
|
+
const C = {
|
|
17
|
+
...DEFAULT_PANEL_PALETTE,
|
|
18
|
+
header: '#94a3b8',
|
|
19
|
+
headerBg: '#1e293b',
|
|
20
|
+
ok: '#22c55e',
|
|
21
|
+
warn: '#eab308',
|
|
22
|
+
error: '#ef4444',
|
|
23
|
+
info: '#38bdf8',
|
|
24
|
+
selectBg: '#0f172a',
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
function formatTime(value?: number): string {
|
|
28
|
+
if (!value) return 'n/a';
|
|
29
|
+
return new Date(value).toLocaleString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runStatusColor(status: string): string {
|
|
33
|
+
if (status === 'completed') return C.ok;
|
|
34
|
+
if (status === 'failed' || status === 'dead_lettered') return C.error;
|
|
35
|
+
if (status === 'cancelled') return C.warn;
|
|
36
|
+
return C.info;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type AutomationRun = UiAutomationSnapshot['runs'][number];
|
|
40
|
+
type AutomationJob = UiAutomationSnapshot['jobs'][number];
|
|
41
|
+
|
|
42
|
+
export class AutomationControlPanel extends ScrollableListPanel<AutomationRun> {
|
|
43
|
+
private readonly readModel?: UiReadModel<UiAutomationSnapshot>;
|
|
44
|
+
private readonly unsub: (() => void) | null;
|
|
45
|
+
|
|
46
|
+
public constructor(readModel?: UiReadModel<UiAutomationSnapshot>) {
|
|
47
|
+
super('automation', 'Automation', 'M', 'monitoring');
|
|
48
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
49
|
+
this.readModel = readModel;
|
|
50
|
+
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public override onDestroy(): void {
|
|
54
|
+
this.unsub?.();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected override getPalette(): PanelPalette {
|
|
58
|
+
return C;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private getJobs(): readonly AutomationJob[] {
|
|
62
|
+
if (!this.readModel) return [];
|
|
63
|
+
return this.readModel.getSnapshot().jobs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected getItems(): readonly AutomationRun[] {
|
|
67
|
+
if (!this.readModel) return [];
|
|
68
|
+
return this.readModel.getSnapshot().runs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected renderItem(run: AutomationRun, _index: number, selected: boolean, width: number): Line {
|
|
72
|
+
const bg = selected ? C.selectBg : undefined;
|
|
73
|
+
const jobs = this.getJobs();
|
|
74
|
+
const name = jobs.find((job) => job.id === run.jobId)?.name ?? run.jobId;
|
|
75
|
+
return buildPanelLine(width, [
|
|
76
|
+
[' ', C.label, bg],
|
|
77
|
+
[run.status.padEnd(11), runStatusColor(run.status), bg],
|
|
78
|
+
[` ${truncateDisplay(name, 22).padEnd(22)}`, C.value, bg],
|
|
79
|
+
[` ${truncateDisplay(run.target.kind, 12).padEnd(12)}`, C.info, bg],
|
|
80
|
+
[` ${truncateDisplay(formatTime(run.queuedAt), Math.max(0, width - 49))}`, C.dim, bg],
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
protected override getEmptyStateMessage(): string {
|
|
85
|
+
return ' No automation activity recorded.';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
89
|
+
return [
|
|
90
|
+
{ command: '/schedule list', summary: 'inspect jobs and run history without mutating schedules' },
|
|
91
|
+
{ command: '/automation jobs', summary: 'review daemon-owned automation jobs from the Agent CLI' },
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public render(width: number, height: number): Line[] {
|
|
96
|
+
const intro = 'Automation jobs, active runs, deliveries, and failure posture across the shared control plane.';
|
|
97
|
+
|
|
98
|
+
if (!this.readModel) {
|
|
99
|
+
const workspace = buildPanelWorkspace(width, height, {
|
|
100
|
+
title: 'Automation Control',
|
|
101
|
+
intro,
|
|
102
|
+
sections: [{
|
|
103
|
+
lines: buildEmptyState(
|
|
104
|
+
width,
|
|
105
|
+
' Runtime store not wired.',
|
|
106
|
+
'This panel needs the shared runtime store to inspect automation jobs, runs, and deliveries.',
|
|
107
|
+
[{ command: '/schedule list', summary: 'review automation from the shell while the runtime wiring is restored' }],
|
|
108
|
+
C,
|
|
109
|
+
),
|
|
110
|
+
}],
|
|
111
|
+
palette: C,
|
|
112
|
+
});
|
|
113
|
+
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
114
|
+
return workspace;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const snapshot = this.readModel.getSnapshot();
|
|
118
|
+
const jobs = [...snapshot.jobs];
|
|
119
|
+
const runs = this.getItems();
|
|
120
|
+
|
|
121
|
+
const headerLines: Line[] = [
|
|
122
|
+
buildKeyValueLine(width, [
|
|
123
|
+
{ label: 'jobs', value: String(snapshot.totalJobs), valueColor: snapshot.totalJobs > 0 ? C.info : C.dim },
|
|
124
|
+
{ label: 'runs', value: String(snapshot.totalRuns), valueColor: snapshot.totalRuns > 0 ? C.value : C.dim },
|
|
125
|
+
{ label: 'active', value: String(snapshot.activeRunIds.length), valueColor: snapshot.activeRunIds.length > 0 ? C.warn : C.dim },
|
|
126
|
+
{ label: 'failed', value: String(snapshot.totalFailed), valueColor: snapshot.totalFailed > 0 ? C.error : C.dim },
|
|
127
|
+
], C),
|
|
128
|
+
buildKeyValueLine(width, [
|
|
129
|
+
{ label: 'deliveries ok', value: String(snapshot.deliveryTotals.succeeded), valueColor: snapshot.deliveryTotals.succeeded > 0 ? C.ok : C.dim },
|
|
130
|
+
{ label: 'delivery fail', value: String(snapshot.deliveryTotals.failed), valueColor: snapshot.deliveryTotals.failed > 0 ? C.error : C.dim },
|
|
131
|
+
{ label: 'dead letters', value: String(snapshot.deliveryTotals.deadLettered), valueColor: snapshot.deliveryTotals.deadLettered > 0 ? C.warn : C.dim },
|
|
132
|
+
{ label: 'sources', value: String(snapshot.sourceCount), valueColor: snapshot.sourceCount > 0 ? C.info : C.dim },
|
|
133
|
+
], C),
|
|
134
|
+
buildGuidanceLine(width, '/schedule list', 'read-only in Agent; schedule mutation/run controls remain blocked here', C),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
if (jobs.length === 0 && runs.length === 0) {
|
|
138
|
+
return this.renderList(width, height, {
|
|
139
|
+
title: 'Automation Control',
|
|
140
|
+
header: headerLines,
|
|
141
|
+
emptyMessage: ' No automation activity recorded.',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.clampSelection();
|
|
146
|
+
const selectedRun = runs[this.selectedIndex];
|
|
147
|
+
const jobName = selectedRun ? (jobs.find((job) => job.id === selectedRun.jobId)?.name ?? selectedRun.jobId) : 'n/a';
|
|
148
|
+
|
|
149
|
+
const footerLines: Line[] = [];
|
|
150
|
+
if (selectedRun) {
|
|
151
|
+
footerLines.push(
|
|
152
|
+
buildPanelLine(width, [
|
|
153
|
+
[' Run: ', C.label],
|
|
154
|
+
[selectedRun.id, C.value],
|
|
155
|
+
[' Status: ', C.label],
|
|
156
|
+
[selectedRun.status, runStatusColor(selectedRun.status)],
|
|
157
|
+
]),
|
|
158
|
+
buildPanelLine(width, [
|
|
159
|
+
[' Job: ', C.label],
|
|
160
|
+
[jobName, C.value],
|
|
161
|
+
[' Agent: ', C.label],
|
|
162
|
+
[selectedRun.agentId ?? 'n/a', C.info],
|
|
163
|
+
]),
|
|
164
|
+
buildPanelLine(width, [
|
|
165
|
+
[' Queue: ', C.label],
|
|
166
|
+
[formatTime(selectedRun.queuedAt), C.dim],
|
|
167
|
+
[' End: ', C.label],
|
|
168
|
+
[formatTime(selectedRun.endedAt), C.dim],
|
|
169
|
+
]),
|
|
170
|
+
buildPanelLine(width, [
|
|
171
|
+
[' Trigger: ', C.label],
|
|
172
|
+
[selectedRun.triggeredBy.kind, C.info],
|
|
173
|
+
[' Target: ', C.label],
|
|
174
|
+
[selectedRun.target.kind, C.value],
|
|
175
|
+
]),
|
|
176
|
+
buildPanelLine(width, [
|
|
177
|
+
[' Deliveries: ', C.label],
|
|
178
|
+
[String(selectedRun.deliveryIds.length), selectedRun.deliveryIds.length > 0 ? C.info : C.dim],
|
|
179
|
+
[' Route: ', C.label],
|
|
180
|
+
[selectedRun.routeId ?? 'n/a', C.dim],
|
|
181
|
+
]),
|
|
182
|
+
);
|
|
183
|
+
if (selectedRun.error) {
|
|
184
|
+
footerLines.push(buildPanelLine(width, [
|
|
185
|
+
[' Error: ', C.label],
|
|
186
|
+
[truncateDisplay(selectedRun.error, Math.max(0, width - 10)), C.error],
|
|
187
|
+
]));
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
footerLines.push(buildPanelLine(width, [[' No run selected.', C.dim]]));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Jobs quick view
|
|
194
|
+
if (jobs.length > 0) {
|
|
195
|
+
footerLines.push(
|
|
196
|
+
...jobs.slice(0, 6).map((job) => buildPanelLine(width, [
|
|
197
|
+
[' ', C.label],
|
|
198
|
+
[job.enabled ? 'ENABLED ' : 'PAUSED ', job.enabled ? C.ok : C.warn],
|
|
199
|
+
[truncateDisplay(job.name, 24).padEnd(24), C.value],
|
|
200
|
+
[` next ${truncateDisplay(formatTime(job.nextRunAt), Math.max(0, width - 43))}`, C.dim],
|
|
201
|
+
])),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
footerLines.push(buildPanelLine(width, [[' Up/Down move through runs', C.dim]]));
|
|
205
|
+
|
|
206
|
+
return this.renderList(width, height, {
|
|
207
|
+
title: 'Automation Control',
|
|
208
|
+
header: headerLines,
|
|
209
|
+
footer: footerLines,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import type { Panel, PanelCategory } from './types.ts';
|
|
3
|
+
import type { ComponentResourceContract, ComponentHealthState } from '../runtime/perf/panel-contracts.ts';
|
|
4
|
+
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
5
|
+
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
6
|
+
import { SPINNER_FRAMES } from '../renderer/progress.ts';
|
|
7
|
+
|
|
8
|
+
export abstract class BasePanel implements Panel {
|
|
9
|
+
public needsRender = true;
|
|
10
|
+
public isTransient = false;
|
|
11
|
+
public isPinned = false;
|
|
12
|
+
protected readonly componentHealthMonitor?: ComponentHealthMonitor;
|
|
13
|
+
|
|
14
|
+
// -------------------------------------------------------------------------
|
|
15
|
+
// Timer registry
|
|
16
|
+
// -------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** All timers registered via registerTimer(). Cleared automatically on onDestroy(). */
|
|
19
|
+
private readonly _timers: Set<ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>> = new Set();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register a timer id (from setInterval or setTimeout) so it is
|
|
23
|
+
* automatically cleared when the panel is destroyed. Returns the id
|
|
24
|
+
* unchanged so the call can be chained inline:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* this.registerTimer(setInterval(() => this.refresh(), 5_000));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
protected registerTimer<T extends ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>>(id: T): T {
|
|
31
|
+
this._timers.add(id);
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clear a specific timer and remove it from the registry.
|
|
37
|
+
* Safe to call with an id that was never registered or already cleared.
|
|
38
|
+
*/
|
|
39
|
+
protected clearTimer(id: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>): void {
|
|
40
|
+
clearInterval(id as ReturnType<typeof setInterval>);
|
|
41
|
+
this._timers.delete(id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// -------------------------------------------------------------------------
|
|
45
|
+
// I2: Error surface slot
|
|
46
|
+
// -------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Last error message to surface in the panel footer.
|
|
50
|
+
* Auto-cleared on the next keystroke by `ScrollableListPanel.handleInput()` (and any
|
|
51
|
+
* subclass that calls `super.handleInput()` or manually calls `this.clearError()` at
|
|
52
|
+
* the start of its handler). BasePanel itself does NOT auto-clear — only subclasses
|
|
53
|
+
* that opt into the contract do.
|
|
54
|
+
*/
|
|
55
|
+
protected lastError: string | null = null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set a transient error message. Triggers a re-render.
|
|
59
|
+
* The error will be auto-cleared on the next keystroke if the panel extends
|
|
60
|
+
* `ScrollableListPanel` (which calls `clearError()` at the top of `handleInput()`).
|
|
61
|
+
*/
|
|
62
|
+
protected setError(msg: string): void {
|
|
63
|
+
this.lastError = msg;
|
|
64
|
+
this.needsRender = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Clear the current error. */
|
|
68
|
+
protected clearError(): void {
|
|
69
|
+
this.lastError = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build a single error Line for display above the hints footer.
|
|
74
|
+
* Returns null when there is no active error.
|
|
75
|
+
*
|
|
76
|
+
* Color: bold red foreground (palette-consistent: #ef4444).
|
|
77
|
+
*/
|
|
78
|
+
protected renderErrorLine(width: number): Line | null {
|
|
79
|
+
if (!this.lastError) return null;
|
|
80
|
+
return UIFactory.stringToLine(
|
|
81
|
+
` ✕ ${this.lastError}`.padEnd(width).slice(0, width),
|
|
82
|
+
width,
|
|
83
|
+
{ fg: '#ef4444', bold: true },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------------------------------
|
|
88
|
+
// I3: Loading spinner slot
|
|
89
|
+
// -------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/** Tracks the loading label for the spinner (undefined = no spinner active). */
|
|
92
|
+
protected loadingState: 'idle' | 'loading' | 'error' = 'idle';
|
|
93
|
+
private _loadingLabel = '';
|
|
94
|
+
|
|
95
|
+
/** Begin loading. Triggers a re-render. */
|
|
96
|
+
protected startLoading(label = 'Loading...'): void {
|
|
97
|
+
this.loadingState = 'loading';
|
|
98
|
+
this._loadingLabel = label;
|
|
99
|
+
this.needsRender = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** End loading (returns to idle). Triggers a re-render. */
|
|
103
|
+
protected stopLoading(): void {
|
|
104
|
+
this.loadingState = 'idle';
|
|
105
|
+
this._loadingLabel = '';
|
|
106
|
+
this.needsRender = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run an async operation with the panel's loading spinner visible.
|
|
111
|
+
* The spinner is always cleared on completion, whether the operation succeeds or throws
|
|
112
|
+
* (uses try/finally). Rethrows any error so callers can handle it or forward to setError.
|
|
113
|
+
*
|
|
114
|
+
* @param label Optional label shown next to the spinner.
|
|
115
|
+
* @param fn The async work to run.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* try {
|
|
120
|
+
* await this.withLoading('Loading diff…', () => this.fetchDiff());
|
|
121
|
+
* } catch (err) {
|
|
122
|
+
* this.setError(summarizeError(err));
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
protected async withLoading<T>(label: string | undefined, fn: () => Promise<T>): Promise<T> {
|
|
127
|
+
this.startLoading(label);
|
|
128
|
+
try {
|
|
129
|
+
return await fn();
|
|
130
|
+
} finally {
|
|
131
|
+
this.stopLoading();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build a spinner Line for the loading state.
|
|
137
|
+
* Returns null when loadingState is not 'loading'.
|
|
138
|
+
*
|
|
139
|
+
* @param width Panel width in columns.
|
|
140
|
+
* @param frame Current animation frame index (caller increments each render).
|
|
141
|
+
*/
|
|
142
|
+
protected renderLoadingLine(width: number, frame = 0): Line | null {
|
|
143
|
+
if (this.loadingState !== 'loading') return null;
|
|
144
|
+
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0]!;
|
|
145
|
+
const text = ` ${spinner} ${this._loadingLabel}`;
|
|
146
|
+
return UIFactory.stringToLine(text.padEnd(width).slice(0, width), width, { fg: '135', bold: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Optional resource contract for this panel.
|
|
151
|
+
* Override in subclasses to declare a custom contract; leave undefined
|
|
152
|
+
* to use the category default enforced by ComponentHealthMonitor.
|
|
153
|
+
*/
|
|
154
|
+
public resourceContract: Readonly<ComponentResourceContract> | undefined = undefined;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Live health state populated by ComponentHealthMonitor.
|
|
158
|
+
* Read-only from outside the monitor.
|
|
159
|
+
*/
|
|
160
|
+
public get healthState(): Readonly<ComponentHealthState> | undefined {
|
|
161
|
+
return this.componentHealthMonitor?.getHealth(this.id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
constructor(
|
|
165
|
+
public readonly id: string,
|
|
166
|
+
public readonly name: string,
|
|
167
|
+
public readonly icon: string,
|
|
168
|
+
public readonly category: PanelCategory,
|
|
169
|
+
componentHealthMonitor?: ComponentHealthMonitor,
|
|
170
|
+
) {
|
|
171
|
+
this.componentHealthMonitor = componentHealthMonitor;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onActivate(): void { this.needsRender = true; }
|
|
175
|
+
onDeactivate(): void {}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Called when the panel is permanently removed. Subclasses should call
|
|
179
|
+
* `super.onDestroy()` to ensure all registered timers are cleared.
|
|
180
|
+
*/
|
|
181
|
+
onDestroy(): void {
|
|
182
|
+
for (const id of this._timers) {
|
|
183
|
+
clearInterval(id as ReturnType<typeof setInterval>);
|
|
184
|
+
}
|
|
185
|
+
this._timers.clear();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
abstract render(width: number, height: number): Line[];
|
|
189
|
+
|
|
190
|
+
/** R2: Mark this panel dirty — it will be re-rendered on the next compositor frame. */
|
|
191
|
+
public invalidate(): void { this.needsRender = true; }
|
|
192
|
+
|
|
193
|
+
/** R2: Called by the compositor after a successful render to clear the dirty flag. */
|
|
194
|
+
public markRendered(): void { this.needsRender = false; }
|
|
195
|
+
|
|
196
|
+
protected markDirty(): void { this.needsRender = true; }
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check whether the panel is currently permitted to render.
|
|
200
|
+
*
|
|
201
|
+
* Consults the shared ComponentHealthMonitor. Returns true if not registered
|
|
202
|
+
* (unthrottled) or if the monitor permits a render at this moment.
|
|
203
|
+
*
|
|
204
|
+
* Call this inside render() or before invoking render() to skip
|
|
205
|
+
* expensive work when throttled:
|
|
206
|
+
*
|
|
207
|
+
* ```ts
|
|
208
|
+
* render(width, height): Line[] {
|
|
209
|
+
* if (!this.canRenderNow()) return this._lastLines ?? [];
|
|
210
|
+
* // ... expensive render ...
|
|
211
|
+
* }
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
protected canRenderNow(now: number = Date.now()): boolean {
|
|
215
|
+
return this.componentHealthMonitor?.canRender(this.id, now) ?? true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Report the duration of a completed render to the health monitor.
|
|
220
|
+
* Call this at the end of render() after measuring wall-clock cost.
|
|
221
|
+
*/
|
|
222
|
+
protected reportRenderDuration(durationMs: number, now: number = Date.now()): void {
|
|
223
|
+
this.componentHealthMonitor?.recordRender(this.id, durationMs, now);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Cache of the most recent lines produced by trackedRender. */
|
|
227
|
+
private _lastTrackedLines: Line[] = [];
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Wrap a render body with canRenderNow throttle check, wall-clock timing,
|
|
231
|
+
* and automatic reportRenderDuration.
|
|
232
|
+
*
|
|
233
|
+
* When throttled, returns the previously cached lines (stale but correctly
|
|
234
|
+
* sized) rather than empty lines, avoiding a flicker on every skipped frame.
|
|
235
|
+
*
|
|
236
|
+
* Usage:
|
|
237
|
+
* ```ts
|
|
238
|
+
* render(width: number, height: number): Line[] {
|
|
239
|
+
* return this.trackedRender(() => {
|
|
240
|
+
* // expensive render logic
|
|
241
|
+
* return lines;
|
|
242
|
+
* });
|
|
243
|
+
* }
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
protected trackedRender(fn: () => Line[]): Line[] {
|
|
247
|
+
if (!this.canRenderNow()) return this._lastTrackedLines;
|
|
248
|
+
const start = Date.now();
|
|
249
|
+
const lines = fn();
|
|
250
|
+
this.reportRenderDuration(Date.now() - start);
|
|
251
|
+
this._lastTrackedLines = lines;
|
|
252
|
+
return lines;
|
|
253
|
+
}
|
|
254
|
+
}
|