@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,656 @@
|
|
|
1
|
+
import { type Line } from '../types/grid.ts';
|
|
2
|
+
import { ModalFactory } from './modal-factory.ts';
|
|
3
|
+
import { formatDuration } from './modal-utils.ts';
|
|
4
|
+
import type { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
|
|
5
|
+
import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
6
|
+
import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
|
|
7
|
+
import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
|
|
8
|
+
import { getVisibleWindow } from './surface-layout.ts';
|
|
9
|
+
|
|
10
|
+
// ─── ProcessEntry ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface ProcessEntry {
|
|
13
|
+
/** Unique process identifier */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Display label (agent task or exec command) */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Tree prefix for child processes, e.g. "└─ " under a WRFC owner. */
|
|
18
|
+
treePrefix?: string;
|
|
19
|
+
/** Process type */
|
|
20
|
+
type: 'agent' | 'exec';
|
|
21
|
+
/** Current status string */
|
|
22
|
+
status: string;
|
|
23
|
+
/** Elapsed milliseconds since start */
|
|
24
|
+
elapsedMs: number;
|
|
25
|
+
/** Live streaming snippet for tracked delegated sessions (last ~60 chars of current turn output). */
|
|
26
|
+
streamSnippet?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Maximum characters from agent task / exec command stored in ProcessEntry.label. */
|
|
32
|
+
const MAX_LABEL_LENGTH = 80;
|
|
33
|
+
/** Border and margin width subtracted from terminal width to get modal content width. */
|
|
34
|
+
const MODAL_BORDER_WIDTH = 8;
|
|
35
|
+
|
|
36
|
+
const WRFC_ROLE_ORDER: Record<string, number> = {
|
|
37
|
+
owner: 0,
|
|
38
|
+
engineer: 1,
|
|
39
|
+
reviewer: 2,
|
|
40
|
+
fixer: 3,
|
|
41
|
+
verifier: 4,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface ProcessModalDeps {
|
|
45
|
+
readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'cancel'>;
|
|
46
|
+
readonly processManager: Pick<ProcessManager, 'list' | 'getStatus' | 'stop'>;
|
|
47
|
+
readonly wrfcController: Pick<WrfcController, 'getChain'> & Partial<Pick<WrfcController, 'listChains'>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type WrfcChainLike = {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly state: string;
|
|
53
|
+
readonly task: string;
|
|
54
|
+
readonly ownerAgentId: string;
|
|
55
|
+
readonly engineerAgentId?: string;
|
|
56
|
+
readonly reviewerAgentId?: string;
|
|
57
|
+
readonly fixerAgentId?: string;
|
|
58
|
+
readonly allAgentIds?: readonly string[];
|
|
59
|
+
readonly constraints?: readonly unknown[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Build a display label for an agent based on its task and template. */
|
|
63
|
+
function buildAgentLabel(rec: AgentRecord, deps: ProcessModalDeps): string {
|
|
64
|
+
const task = rec.task;
|
|
65
|
+
|
|
66
|
+
// Look up the original task from the WRFC chain if available
|
|
67
|
+
const originalTask = getChainTask(rec.wrfcId, deps);
|
|
68
|
+
|
|
69
|
+
if (rec.wrfcRole === 'owner') {
|
|
70
|
+
const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
|
|
71
|
+
return `[WRFC owner] ${desc}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (rec.wrfcRole === 'engineer') {
|
|
75
|
+
const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 11);
|
|
76
|
+
return `[Engineer] ${desc}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (rec.wrfcRole === 'verifier') {
|
|
80
|
+
const desc = truncateFirst(originalTask ?? task, MAX_LABEL_LENGTH - 13);
|
|
81
|
+
return `[Verifier] ${desc}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// WRFC Review agent
|
|
85
|
+
if (task.startsWith('WRFC Review Request')) {
|
|
86
|
+
const thresholdMatch = task.match(/threshold is (\d+(?:\.\d+)?)/);
|
|
87
|
+
const threshold = thresholdMatch ? thresholdMatch[1] : '9.9';
|
|
88
|
+
const desc = truncateFirst(originalTask ?? 'review in progress', 50);
|
|
89
|
+
return `[Review] ${desc} (target: ${threshold}/10)`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// WRFC Fix agent
|
|
93
|
+
if (task.startsWith('WRFC Fix Request')) {
|
|
94
|
+
const scoreMatch = task.match(/Review score:\s*(\d+(?:\.\d+)?)\/(\d+)\s*\(threshold:\s*(\d+(?:\.\d+)?)/);
|
|
95
|
+
const fromScore = scoreMatch ? scoreMatch[1] : '?';
|
|
96
|
+
const toScore = scoreMatch ? scoreMatch[3] : '?';
|
|
97
|
+
const attemptMatch = task.match(/Fix attempt:\s*(\d+)/);
|
|
98
|
+
const attempt = attemptMatch ? attemptMatch[1] : '?';
|
|
99
|
+
const desc = truncateFirst(originalTask ?? 'fix in progress', 45);
|
|
100
|
+
// Show constraint count when the chain has constraints to target (SDK 0.23.0)
|
|
101
|
+
const chain = rec.wrfcId ? safeGetChain(rec.wrfcId, deps) : null;
|
|
102
|
+
const constraintCount = chain && (chain.constraints?.length ?? 0) > 0 ? chain.constraints?.length ?? 0 : 0;
|
|
103
|
+
const constraintSuffix = constraintCount > 0 ? ` [${constraintCount}c]` : '';
|
|
104
|
+
return `[Fix #${attempt}] ${desc} (${fromScore} \u2192 ${toScore}/10)${constraintSuffix}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Regular agent — show template and truncated first line
|
|
108
|
+
const templateLabels: Record<string, string> = {
|
|
109
|
+
engineer: 'Engineer', reviewer: 'Reviewer', tester: 'Tester',
|
|
110
|
+
researcher: 'Researcher', general: 'Agent',
|
|
111
|
+
};
|
|
112
|
+
const tag = templateLabels[rec.template] ?? 'Agent';
|
|
113
|
+
const maxDesc = MAX_LABEL_LENGTH - tag.length - 3;
|
|
114
|
+
return `[${tag}] ${truncateFirst(task, maxDesc)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isActiveAgent(rec: AgentRecord): boolean {
|
|
118
|
+
return rec.status !== 'completed' && rec.status !== 'failed' && rec.status !== 'cancelled';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isActiveWrfcState(state: string): boolean {
|
|
122
|
+
return state !== 'passed' && state !== 'failed';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getStreamSnippet(rec: AgentRecord): string | undefined {
|
|
126
|
+
if (!rec.streamingContent) return undefined;
|
|
127
|
+
const raw = rec.streamingContent.replace(/\n/g, ' ').trim();
|
|
128
|
+
return raw.length > 60 ? '...' + raw.slice(-57) : raw;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compareAgents(a: AgentRecord, b: AgentRecord): number {
|
|
132
|
+
const roleDelta = (WRFC_ROLE_ORDER[a.wrfcRole ?? ''] ?? 50) - (WRFC_ROLE_ORDER[b.wrfcRole ?? ''] ?? 50);
|
|
133
|
+
if (roleDelta !== 0) return roleDelta;
|
|
134
|
+
return a.startedAt - b.startedAt || a.id.localeCompare(b.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildAgentEntry(
|
|
138
|
+
rec: AgentRecord,
|
|
139
|
+
deps: ProcessModalDeps,
|
|
140
|
+
now: number,
|
|
141
|
+
treePrefix = '',
|
|
142
|
+
): ProcessEntry {
|
|
143
|
+
return {
|
|
144
|
+
id: rec.id,
|
|
145
|
+
label: buildAgentLabel(rec, deps),
|
|
146
|
+
treePrefix,
|
|
147
|
+
type: 'agent',
|
|
148
|
+
status: rec.status,
|
|
149
|
+
elapsedMs: now - rec.startedAt,
|
|
150
|
+
streamSnippet: getStreamSnippet(rec),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function appendAgentSubtree(
|
|
155
|
+
result: ProcessEntry[],
|
|
156
|
+
rec: AgentRecord,
|
|
157
|
+
childrenByParent: Map<string, AgentRecord[]>,
|
|
158
|
+
deps: ProcessModalDeps,
|
|
159
|
+
now: number,
|
|
160
|
+
prefix: string,
|
|
161
|
+
connector: string,
|
|
162
|
+
visited: Set<string>,
|
|
163
|
+
): void {
|
|
164
|
+
if (visited.has(rec.id)) return;
|
|
165
|
+
visited.add(rec.id);
|
|
166
|
+
result.push(buildAgentEntry(rec, deps, now, `${prefix}${connector}`));
|
|
167
|
+
|
|
168
|
+
const children = (childrenByParent.get(rec.id) ?? []).slice().sort(compareAgents);
|
|
169
|
+
const descendantPrefix = connector === '├─ ' ? '│ ' : connector === '└─ ' ? ' ' : '';
|
|
170
|
+
children.forEach((child, index) => {
|
|
171
|
+
const last = index === children.length - 1;
|
|
172
|
+
appendAgentSubtree(
|
|
173
|
+
result,
|
|
174
|
+
child,
|
|
175
|
+
childrenByParent,
|
|
176
|
+
deps,
|
|
177
|
+
now,
|
|
178
|
+
`${prefix}${descendantPrefix}`,
|
|
179
|
+
last ? '└─ ' : '├─ ',
|
|
180
|
+
visited,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function appendAgentGroupEntries(
|
|
186
|
+
result: ProcessEntry[],
|
|
187
|
+
records: AgentRecord[],
|
|
188
|
+
deps: ProcessModalDeps,
|
|
189
|
+
now: number,
|
|
190
|
+
): void {
|
|
191
|
+
const group = records.slice().sort(compareAgents);
|
|
192
|
+
const byId = new Map(group.map((rec) => [rec.id, rec]));
|
|
193
|
+
const childrenByParent = new Map<string, AgentRecord[]>();
|
|
194
|
+
|
|
195
|
+
for (const rec of group) {
|
|
196
|
+
if (!rec.parentAgentId || !byId.has(rec.parentAgentId)) continue;
|
|
197
|
+
const children = childrenByParent.get(rec.parentAgentId) ?? [];
|
|
198
|
+
children.push(rec);
|
|
199
|
+
childrenByParent.set(rec.parentAgentId, children);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const chain = group[0]?.wrfcId ? safeGetChain(group[0].wrfcId, deps) : null;
|
|
203
|
+
const owner = group.find((rec) => rec.id === chain?.ownerAgentId)
|
|
204
|
+
?? group.find((rec) => rec.wrfcRole === 'owner');
|
|
205
|
+
const roots = owner
|
|
206
|
+
? [owner]
|
|
207
|
+
: group.filter((rec) => !rec.parentAgentId || !byId.has(rec.parentAgentId));
|
|
208
|
+
const visited = new Set<string>();
|
|
209
|
+
|
|
210
|
+
roots.forEach((root, index) => {
|
|
211
|
+
const connector = owner || roots.length === 1 ? '' : (index === roots.length - 1 ? '└─ ' : '├─ ');
|
|
212
|
+
appendAgentSubtree(result, root, childrenByParent, deps, now, '', connector, visited);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const leftovers = group.filter((rec) => !visited.has(rec.id));
|
|
216
|
+
leftovers.forEach((rec, index) => {
|
|
217
|
+
appendAgentSubtree(
|
|
218
|
+
result,
|
|
219
|
+
rec,
|
|
220
|
+
childrenByParent,
|
|
221
|
+
deps,
|
|
222
|
+
now,
|
|
223
|
+
'',
|
|
224
|
+
index === leftovers.length - 1 ? '└─ ' : '├─ ',
|
|
225
|
+
visited,
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildAgentEntries(
|
|
231
|
+
agents: AgentRecord[],
|
|
232
|
+
deps: ProcessModalDeps,
|
|
233
|
+
now: number,
|
|
234
|
+
getGroupOrder?: (key: string) => number | undefined,
|
|
235
|
+
ensureGroupOrder?: (key: string) => number,
|
|
236
|
+
): ProcessEntry[] {
|
|
237
|
+
const result: ProcessEntry[] = [];
|
|
238
|
+
const displayAgents = prepareAgentRecordsForDisplay(agents, deps);
|
|
239
|
+
const activeById = new Map(displayAgents.map((agent) => [agent.id, agent]));
|
|
240
|
+
const groups = new Map<string, AgentRecord[]>();
|
|
241
|
+
|
|
242
|
+
for (const agent of displayAgents) {
|
|
243
|
+
const groupKey = getAgentGroupKey(agent, activeById);
|
|
244
|
+
const group = groups.get(groupKey) ?? [];
|
|
245
|
+
group.push(agent);
|
|
246
|
+
groups.set(groupKey, group);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const sortedGroups = Array.from(groups.entries()).sort(([aKey, a], [bKey, b]) => {
|
|
250
|
+
const aOrder = getGroupOrder?.(aKey);
|
|
251
|
+
const bOrder = getGroupOrder?.(bKey);
|
|
252
|
+
if (aOrder !== undefined || bOrder !== undefined) {
|
|
253
|
+
if (aOrder === undefined) return 1;
|
|
254
|
+
if (bOrder === undefined) return -1;
|
|
255
|
+
return aOrder - bOrder;
|
|
256
|
+
}
|
|
257
|
+
const aStarted = Math.min(...a.map((rec) => rec.startedAt));
|
|
258
|
+
const bStarted = Math.min(...b.map((rec) => rec.startedAt));
|
|
259
|
+
return aStarted - bStarted || aKey.localeCompare(bKey);
|
|
260
|
+
});
|
|
261
|
+
for (const [key, group] of sortedGroups) {
|
|
262
|
+
ensureGroupOrder?.(key);
|
|
263
|
+
appendAgentGroupEntries(result, group, deps, now);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function prepareAgentRecordsForDisplay(agents: AgentRecord[], deps: ProcessModalDeps): AgentRecord[] {
|
|
270
|
+
const chains = listWrfcChains(deps);
|
|
271
|
+
const agentById = new Map(agents.map((agent) => [agent.id, agent]));
|
|
272
|
+
const normalizedById = new Map<string, AgentRecord>();
|
|
273
|
+
|
|
274
|
+
for (const agent of agents) {
|
|
275
|
+
if (!isActiveAgent(agent)) continue;
|
|
276
|
+
normalizedById.set(agent.id, normalizeWrfcAgentRecord(agent, chains));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A WRFC owner is the durable root of the chain. Keep it visible until the
|
|
280
|
+
// chain itself is terminal, even if the underlying owner agent has already
|
|
281
|
+
// emitted a completed phase event before reviewer/fixer/gate work finishes.
|
|
282
|
+
for (const chain of chains) {
|
|
283
|
+
if (!isActiveWrfcState(chain.state)) continue;
|
|
284
|
+
const owner = agentById.get(chain.ownerAgentId);
|
|
285
|
+
if (!owner || normalizedById.has(owner.id)) continue;
|
|
286
|
+
const chainHasActiveMember = agents.some((agent) =>
|
|
287
|
+
agent.id !== owner.id
|
|
288
|
+
&& isActiveAgent(agent)
|
|
289
|
+
&& isAgentInChain(agent, chain)
|
|
290
|
+
);
|
|
291
|
+
if (!chainHasActiveMember) continue;
|
|
292
|
+
normalizedById.set(owner.id, normalizeWrfcAgentRecord({
|
|
293
|
+
...owner,
|
|
294
|
+
status: 'running',
|
|
295
|
+
completedAt: undefined,
|
|
296
|
+
progress: owner.progress ?? `WRFC chain ${chain.state}`,
|
|
297
|
+
}, chains));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const normalized = Array.from(normalizedById.values());
|
|
301
|
+
return inferDuplicateWrfcOwnerRows(normalized);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeWrfcAgentRecord(agent: AgentRecord, chains: WrfcChainLike[]): AgentRecord {
|
|
305
|
+
const chain = findChainForAgent(agent, chains);
|
|
306
|
+
if (!chain) return agent;
|
|
307
|
+
|
|
308
|
+
const role = inferWrfcRole(agent, chain);
|
|
309
|
+
const parentAgentId = role && role !== 'owner'
|
|
310
|
+
? agent.parentAgentId ?? chain.ownerAgentId
|
|
311
|
+
: agent.parentAgentId;
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
...agent,
|
|
315
|
+
wrfcId: agent.wrfcId ?? chain.id,
|
|
316
|
+
wrfcRole: agent.wrfcRole ?? role,
|
|
317
|
+
parentAgentId,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function inferDuplicateWrfcOwnerRows(agents: AgentRecord[]): AgentRecord[] {
|
|
322
|
+
const byTask = new Map<string, AgentRecord[]>();
|
|
323
|
+
for (const agent of agents) {
|
|
324
|
+
if (agent.wrfcId || agent.wrfcRole || agent.parentAgentId) continue;
|
|
325
|
+
if (agent.reviewMode !== 'wrfc') continue;
|
|
326
|
+
const key = agent.task.trim();
|
|
327
|
+
if (!key) continue;
|
|
328
|
+
const group = byTask.get(key) ?? [];
|
|
329
|
+
group.push(agent);
|
|
330
|
+
byTask.set(key, group);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const inferredIds = new Set<string>();
|
|
334
|
+
const inferred = new Map<string, AgentRecord>();
|
|
335
|
+
for (const [task, group] of byTask) {
|
|
336
|
+
if (group.length < 2) continue;
|
|
337
|
+
const sorted = group.slice().sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id));
|
|
338
|
+
const owner = sorted[0]!;
|
|
339
|
+
const syntheticWrfcId = `inferred:${owner.id}`;
|
|
340
|
+
inferred.set(owner.id, {
|
|
341
|
+
...owner,
|
|
342
|
+
wrfcId: syntheticWrfcId,
|
|
343
|
+
wrfcRole: 'owner',
|
|
344
|
+
});
|
|
345
|
+
inferredIds.add(owner.id);
|
|
346
|
+
for (const child of sorted.slice(1)) {
|
|
347
|
+
inferred.set(child.id, {
|
|
348
|
+
...child,
|
|
349
|
+
wrfcId: syntheticWrfcId,
|
|
350
|
+
wrfcRole: child.template === 'reviewer' ? 'reviewer' : 'engineer',
|
|
351
|
+
parentAgentId: owner.id,
|
|
352
|
+
});
|
|
353
|
+
inferredIds.add(child.id);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Avoid accidentally grouping unrelated long-running WRFC roots that just
|
|
357
|
+
// happen to share an empty or generic task after this exact duplicate group.
|
|
358
|
+
byTask.delete(task);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (inferredIds.size === 0) return agents;
|
|
362
|
+
return agents.map((agent) => inferred.get(agent.id) ?? agent);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function listWrfcChains(deps: ProcessModalDeps): WrfcChainLike[] {
|
|
366
|
+
const controller = deps.wrfcController as ProcessModalDeps['wrfcController'] & {
|
|
367
|
+
listChains?: () => unknown;
|
|
368
|
+
};
|
|
369
|
+
if (typeof controller.listChains !== 'function') return [];
|
|
370
|
+
try {
|
|
371
|
+
const value = controller.listChains();
|
|
372
|
+
return Array.isArray(value) ? value.filter(isWrfcChainLike) : [];
|
|
373
|
+
} catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isWrfcChainLike(value: unknown): value is WrfcChainLike {
|
|
379
|
+
if (!value || typeof value !== 'object') return false;
|
|
380
|
+
const record = value as Record<string, unknown>;
|
|
381
|
+
return typeof record.id === 'string'
|
|
382
|
+
&& typeof record.state === 'string'
|
|
383
|
+
&& typeof record.task === 'string'
|
|
384
|
+
&& typeof record.ownerAgentId === 'string';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function findChainForAgent(agent: AgentRecord, chains: WrfcChainLike[]): WrfcChainLike | null {
|
|
388
|
+
if (agent.wrfcId) {
|
|
389
|
+
const direct = chains.find((chain) => chain.id === agent.wrfcId);
|
|
390
|
+
if (direct) return direct;
|
|
391
|
+
}
|
|
392
|
+
return chains.find((chain) => isAgentInChain(agent, chain)) ?? null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isAgentInChain(agent: AgentRecord, chain: WrfcChainLike): boolean {
|
|
396
|
+
return chain.ownerAgentId === agent.id
|
|
397
|
+
|| chain.engineerAgentId === agent.id
|
|
398
|
+
|| chain.reviewerAgentId === agent.id
|
|
399
|
+
|| chain.fixerAgentId === agent.id
|
|
400
|
+
|| (chain.allAgentIds?.includes(agent.id) ?? false)
|
|
401
|
+
|| agent.wrfcId === chain.id;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function inferWrfcRole(agent: AgentRecord, chain: WrfcChainLike): AgentRecord['wrfcRole'] {
|
|
405
|
+
if (agent.wrfcRole) return agent.wrfcRole;
|
|
406
|
+
if (chain.ownerAgentId === agent.id) return 'owner';
|
|
407
|
+
if (chain.engineerAgentId === agent.id) return 'engineer';
|
|
408
|
+
if (chain.reviewerAgentId === agent.id) return 'reviewer';
|
|
409
|
+
if (chain.fixerAgentId === agent.id) return 'fixer';
|
|
410
|
+
if (agent.template === 'reviewer') return 'reviewer';
|
|
411
|
+
return 'engineer';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function getAgentGroupKey(agent: AgentRecord, activeById: Map<string, AgentRecord>): string {
|
|
415
|
+
if (agent.wrfcId) return `wrfc:${agent.wrfcId}`;
|
|
416
|
+
|
|
417
|
+
const seen = new Set<string>();
|
|
418
|
+
let root = agent;
|
|
419
|
+
while (root.parentAgentId && activeById.has(root.parentAgentId) && !seen.has(root.parentAgentId)) {
|
|
420
|
+
seen.add(root.id);
|
|
421
|
+
root = activeById.get(root.parentAgentId)!;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If the active root is an orphaned child, keep it anchored to its missing parent id
|
|
425
|
+
// so it does not jump to a new group when the parent exits before its children.
|
|
426
|
+
return `root:${root.parentAgentId ?? root.id}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function safeGetChain(wrfcId: string, deps: Pick<ProcessModalDeps, 'wrfcController'>): WrfcChainLike | null {
|
|
430
|
+
try {
|
|
431
|
+
const chain = deps.wrfcController.getChain(wrfcId);
|
|
432
|
+
return isWrfcChainLike(chain) ? chain : null;
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Get the original task description from a WRFC chain. */
|
|
439
|
+
function getChainTask(wrfcId: string | undefined, deps: Pick<ProcessModalDeps, 'wrfcController'>): string | null {
|
|
440
|
+
if (!wrfcId) return null;
|
|
441
|
+
return safeGetChain(wrfcId, deps)?.task ?? null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Truncate to first line, capped at max chars. */
|
|
445
|
+
function truncateFirst(text: string, max: number): string {
|
|
446
|
+
const line = text.split('\n')[0].trim();
|
|
447
|
+
return line.length > max ? line.slice(0, Math.max(0, max - 3)) + '...' : line;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Truncate a command string to first line, capped at MAX_LABEL_LENGTH. */
|
|
451
|
+
function truncateCmd(text: string): string {
|
|
452
|
+
const firstLine = text.split('\n')[0].trim();
|
|
453
|
+
if (firstLine.length > MAX_LABEL_LENGTH) return firstLine.slice(0, MAX_LABEL_LENGTH - 3) + '...';
|
|
454
|
+
return firstLine;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ─── ProcessModalState ────────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* ProcessModal — manages the state for the background-process list modal.
|
|
461
|
+
*
|
|
462
|
+
* Holds the list of ProcessEntry items, selected index, and active flag.
|
|
463
|
+
* Rendering is done by renderProcessModal().
|
|
464
|
+
*/
|
|
465
|
+
export class ProcessModal {
|
|
466
|
+
public active = false;
|
|
467
|
+
public selectedIndex = 0;
|
|
468
|
+
public entries: ProcessEntry[] = [];
|
|
469
|
+
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
470
|
+
private onRefresh: (() => void) | null = null;
|
|
471
|
+
private groupOrder = new Map<string, number>();
|
|
472
|
+
private nextGroupOrder = 0;
|
|
473
|
+
|
|
474
|
+
constructor(private readonly deps: ProcessModalDeps) {}
|
|
475
|
+
|
|
476
|
+
/** Set a callback to trigger re-render on timer tick. */
|
|
477
|
+
setOnRefresh(fn: () => void): void {
|
|
478
|
+
this.onRefresh = fn;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
open(): void {
|
|
482
|
+
this.refresh();
|
|
483
|
+
this.active = true;
|
|
484
|
+
this.selectedIndex = 0;
|
|
485
|
+
if (this.refreshTimer) clearInterval(this.refreshTimer);
|
|
486
|
+
this.refreshTimer = setInterval(() => {
|
|
487
|
+
this.refresh();
|
|
488
|
+
this.onRefresh?.();
|
|
489
|
+
}, 1000);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
close(): void {
|
|
493
|
+
this.active = false;
|
|
494
|
+
if (this.refreshTimer) {
|
|
495
|
+
clearInterval(this.refreshTimer);
|
|
496
|
+
this.refreshTimer = null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Rebuild entries from the currently owned runtime services. */
|
|
501
|
+
refresh(): void {
|
|
502
|
+
const manager = this.deps.agentManager;
|
|
503
|
+
if (typeof manager?.list !== 'function') return; // Guard against test mock pollution
|
|
504
|
+
const now = Date.now();
|
|
505
|
+
const result: ProcessEntry[] = [];
|
|
506
|
+
|
|
507
|
+
// Agents — only show active (pending/running), grouped by stable parent/child hierarchy.
|
|
508
|
+
result.push(...buildAgentEntries(
|
|
509
|
+
manager.list(),
|
|
510
|
+
this.deps,
|
|
511
|
+
now,
|
|
512
|
+
(key) => this.groupOrder.get(key),
|
|
513
|
+
(key) => this.ensureGroupOrder(key),
|
|
514
|
+
));
|
|
515
|
+
|
|
516
|
+
// Background exec processes — only show running
|
|
517
|
+
const pm = this.deps.processManager;
|
|
518
|
+
for (const p of pm.list()) {
|
|
519
|
+
if (p.status.startsWith('done')) continue;
|
|
520
|
+
const startTime = pm.getStatus(p.id)?.startTime ?? now;
|
|
521
|
+
result.push({
|
|
522
|
+
id: p.id,
|
|
523
|
+
label: truncateCmd(p.cmd),
|
|
524
|
+
type: 'exec',
|
|
525
|
+
status: p.status,
|
|
526
|
+
elapsedMs: now - startTime,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.entries = result;
|
|
531
|
+
|
|
532
|
+
// Keep selection in-bounds
|
|
533
|
+
if (this.selectedIndex >= this.entries.length) {
|
|
534
|
+
this.selectedIndex = Math.max(0, this.entries.length - 1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private ensureGroupOrder(key: string): number {
|
|
539
|
+
const existing = this.groupOrder.get(key);
|
|
540
|
+
if (existing !== undefined) return existing;
|
|
541
|
+
const next = this.nextGroupOrder++;
|
|
542
|
+
this.groupOrder.set(key, next);
|
|
543
|
+
return next;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
moveUp(): void {
|
|
547
|
+
if (this.entries.length === 0) return;
|
|
548
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.entries.length) % this.entries.length;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
moveDown(): void {
|
|
552
|
+
if (this.entries.length === 0) return;
|
|
553
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.entries.length;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
getSelected(): ProcessEntry | undefined {
|
|
557
|
+
return this.entries[this.selectedIndex];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Kill the selected process.
|
|
562
|
+
* Returns true if a process was killed, false otherwise.
|
|
563
|
+
*/
|
|
564
|
+
killSelected(): boolean {
|
|
565
|
+
const entry = this.getSelected();
|
|
566
|
+
if (!entry) return false;
|
|
567
|
+
|
|
568
|
+
if (entry.type === 'exec') {
|
|
569
|
+
return this.deps.processManager.stop(entry.id);
|
|
570
|
+
} else {
|
|
571
|
+
return this.deps.agentManager.cancel(entry.id);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ─── renderProcessModal ───────────────────────────────────────────────────────
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Render the process list modal as Line[] for overlay in the viewport.
|
|
580
|
+
*
|
|
581
|
+
* @param modal ProcessModal state
|
|
582
|
+
* @param width Terminal width
|
|
583
|
+
*/
|
|
584
|
+
export function renderProcessModal(modal: ProcessModal, width: number, viewportHeight = 24): Line[] {
|
|
585
|
+
modal.refresh();
|
|
586
|
+
|
|
587
|
+
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
588
|
+
margin: 2,
|
|
589
|
+
maxWidth: Math.max(24, width - 4),
|
|
590
|
+
chromeRows: 4,
|
|
591
|
+
minContentRows: 5,
|
|
592
|
+
maxContentRows: 9,
|
|
593
|
+
});
|
|
594
|
+
const boxMargin = metrics.margin;
|
|
595
|
+
const boxW = metrics.boxWidth;
|
|
596
|
+
const maxVisibleRows = metrics.contentRows;
|
|
597
|
+
const targetContentRows = getStableOverlayContentRows(metrics.contentRows, 7);
|
|
598
|
+
|
|
599
|
+
if (modal.entries.length === 0) {
|
|
600
|
+
return ModalFactory.createModal({
|
|
601
|
+
title: 'Background Processes',
|
|
602
|
+
width: boxW,
|
|
603
|
+
margin: boxMargin,
|
|
604
|
+
targetContentRows,
|
|
605
|
+
sections: [
|
|
606
|
+
{ type: 'text', content: 'No background processes running.' },
|
|
607
|
+
],
|
|
608
|
+
hints: ['[Esc] Close'],
|
|
609
|
+
}, width);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const maxLabelW = Math.max(10, boxW - MODAL_BORDER_WIDTH);
|
|
613
|
+
const window = getVisibleWindow(modal.entries.length, modal.selectedIndex, maxVisibleRows);
|
|
614
|
+
const visibleEntries = modal.entries.slice(window.start, window.end);
|
|
615
|
+
|
|
616
|
+
const items = visibleEntries.map((e, i) => {
|
|
617
|
+
const absoluteIndex = window.start + i;
|
|
618
|
+
const statusIcon = {
|
|
619
|
+
running: '●',
|
|
620
|
+
pending: '•',
|
|
621
|
+
completed: '✓',
|
|
622
|
+
failed: '✗',
|
|
623
|
+
cancelled: '–',
|
|
624
|
+
}[e.status] ?? '•';
|
|
625
|
+
const typeTag = e.type === 'agent' ? '[agent]' : '[exec]';
|
|
626
|
+
const dur = formatDuration(e.elapsedMs);
|
|
627
|
+
const statusStr = e.streamSnippet ? `streaming ${dur}` : `${e.status} ${dur}`;
|
|
628
|
+
const suffix = ` ${statusStr}`;
|
|
629
|
+
const treePrefix = e.treePrefix ?? '';
|
|
630
|
+
const maxDescW = maxLabelW - typeTag.length - treePrefix.length - suffix.length - 4; // icon + spaces
|
|
631
|
+
const desc = e.label.length > maxDescW ? e.label.slice(0, Math.max(0, maxDescW - 3)) + '...' : e.label;
|
|
632
|
+
const label = `${statusIcon} ${typeTag} ${treePrefix}${desc}${suffix}`;
|
|
633
|
+
return {
|
|
634
|
+
label,
|
|
635
|
+
selected: absoluteIndex === modal.selectedIndex,
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
const sections: import('./modal-factory.ts').ModalSection[] = [
|
|
639
|
+
{ type: 'list', items },
|
|
640
|
+
];
|
|
641
|
+
if (modal.entries.length > maxVisibleRows) {
|
|
642
|
+
sections.push({ type: 'separator' });
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return ModalFactory.createModal({
|
|
646
|
+
title: 'Background Processes',
|
|
647
|
+
width: boxW,
|
|
648
|
+
margin: boxMargin,
|
|
649
|
+
targetContentRows,
|
|
650
|
+
sections,
|
|
651
|
+
helpers: modal.entries.length > maxVisibleRows
|
|
652
|
+
? [{ content: `[${window.start + 1}-${window.end} of ${modal.entries.length}]` }]
|
|
653
|
+
: undefined,
|
|
654
|
+
hints: ['[Up/Down] Navigate', '[Enter] Peek output', '[k] Kill', '[Esc] Close'],
|
|
655
|
+
}, width);
|
|
656
|
+
}
|