agent-tempo 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +213 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/icon-dark-32.png +0 -0
- package/assets/icon-dark-64.png +0 -0
- package/assets/icon-dark.svg +9 -0
- package/assets/icon.svg +9 -0
- package/assets/logo-dark.svg +11 -0
- package/assets/logo-light.svg +11 -0
- package/dashboard/README.md +91 -0
- package/dashboard/dist/assets/index-CB78ToNE.css +2 -0
- package/dashboard/dist/assets/index-_5jV0Znu.js +62 -0
- package/dashboard/dist/assets/index-_5jV0Znu.js.map +1 -0
- package/dashboard/dist/index.html +21 -0
- package/dashboard/package.json +47 -0
- package/dist/activities/hard-terminate.d.ts +32 -0
- package/dist/activities/hard-terminate.js +460 -0
- package/dist/activities/maestro.d.ts +72 -0
- package/dist/activities/maestro.js +254 -0
- package/dist/activities/outbox.d.ts +188 -0
- package/dist/activities/outbox.js +849 -0
- package/dist/activities/resolve.d.ts +64 -0
- package/dist/activities/resolve.js +129 -0
- package/dist/activities/schedule-fire.d.ts +36 -0
- package/dist/activities/schedule-fire.js +147 -0
- package/dist/adapters/base.d.ts +426 -0
- package/dist/adapters/base.js +1270 -0
- package/dist/adapters/claude-api/adapter.d.ts +168 -0
- package/dist/adapters/claude-api/adapter.js +797 -0
- package/dist/adapters/claude-api/api-error.d.ts +96 -0
- package/dist/adapters/claude-api/api-error.js +191 -0
- package/dist/adapters/claude-api/index.d.ts +16 -0
- package/dist/adapters/claude-api/index.js +21 -0
- package/dist/adapters/claude-api/mcp-bridge.d.ts +50 -0
- package/dist/adapters/claude-api/mcp-bridge.js +157 -0
- package/dist/adapters/claude-code/adapter.d.ts +133 -0
- package/dist/adapters/claude-code/adapter.js +274 -0
- package/dist/adapters/claude-code/index.d.ts +15 -0
- package/dist/adapters/claude-code/index.js +20 -0
- package/dist/adapters/claude-code-headless/adapter.d.ts +131 -0
- package/dist/adapters/claude-code-headless/adapter.js +710 -0
- package/dist/adapters/claude-code-headless/error-mapper.d.ts +107 -0
- package/dist/adapters/claude-code-headless/error-mapper.js +281 -0
- package/dist/adapters/claude-code-headless/index.d.ts +17 -0
- package/dist/adapters/claude-code-headless/index.js +26 -0
- package/dist/adapters/claude-code-headless/pre-flight.d.ts +51 -0
- package/dist/adapters/claude-code-headless/pre-flight.js +207 -0
- package/dist/adapters/claude-code-headless/prompt.d.ts +93 -0
- package/dist/adapters/claude-code-headless/prompt.js +79 -0
- package/dist/adapters/claude-code-headless/stream-json.d.ts +242 -0
- package/dist/adapters/claude-code-headless/stream-json.js +208 -0
- package/dist/adapters/claude-code-headless/types.d.ts +28 -0
- package/dist/adapters/claude-code-headless/types.js +36 -0
- package/dist/adapters/copilot/adapter.d.ts +100 -0
- package/dist/adapters/copilot/adapter.js +730 -0
- package/dist/adapters/copilot/index.d.ts +15 -0
- package/dist/adapters/copilot/index.js +20 -0
- package/dist/adapters/index.d.ts +42 -0
- package/dist/adapters/index.js +115 -0
- package/dist/adapters/opencode/adapter.d.ts +82 -0
- package/dist/adapters/opencode/adapter.js +710 -0
- package/dist/adapters/opencode/config.d.ts +90 -0
- package/dist/adapters/opencode/config.js +137 -0
- package/dist/adapters/opencode/helpers.d.ts +40 -0
- package/dist/adapters/opencode/helpers.js +144 -0
- package/dist/adapters/opencode/index.d.ts +12 -0
- package/dist/adapters/opencode/index.js +17 -0
- package/dist/adapters/opencode/server-bridge.d.ts +124 -0
- package/dist/adapters/opencode/server-bridge.js +216 -0
- package/dist/adapters/sdk/base.d.ts +95 -0
- package/dist/adapters/sdk/base.js +134 -0
- package/dist/adapters/sdk/system-prompt.d.ts +64 -0
- package/dist/adapters/sdk/system-prompt.js +78 -0
- package/dist/adapters/terminal-error.d.ts +27 -0
- package/dist/adapters/terminal-error.js +39 -0
- package/dist/channel.d.ts +3 -0
- package/dist/channel.js +48 -0
- package/dist/cli/commands.d.ts +245 -0
- package/dist/cli/commands.js +2438 -0
- package/dist/cli/config-command.d.ts +8 -0
- package/dist/cli/config-command.js +254 -0
- package/dist/cli/daemon-command.d.ts +57 -0
- package/dist/cli/daemon-command.js +493 -0
- package/dist/cli/daemon.d.ts +217 -0
- package/dist/cli/daemon.js +632 -0
- package/dist/cli/dashboard-command.d.ts +20 -0
- package/dist/cli/dashboard-command.js +241 -0
- package/dist/cli/dev-banner.d.ts +107 -0
- package/dist/cli/dev-banner.js +190 -0
- package/dist/cli/dev-mode-bootstrap.d.ts +29 -0
- package/dist/cli/dev-mode-bootstrap.js +36 -0
- package/dist/cli/dev-verbs.d.ts +43 -0
- package/dist/cli/dev-verbs.js +254 -0
- package/dist/cli/help-text.d.ts +1 -0
- package/dist/cli/help-text.js +158 -0
- package/dist/cli/legacy-migration.d.ts +35 -0
- package/dist/cli/legacy-migration.js +335 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +63 -0
- package/dist/cli/output.d.ts +12 -0
- package/dist/cli/output.js +37 -0
- package/dist/cli/preflight.d.ts +9 -0
- package/dist/cli/preflight.js +96 -0
- package/dist/cli/removed-verbs.d.ts +9 -0
- package/dist/cli/removed-verbs.js +78 -0
- package/dist/cli/sa-preflight.d.ts +99 -0
- package/dist/cli/sa-preflight.js +183 -0
- package/dist/cli/scenarios-command.d.ts +6 -0
- package/dist/cli/scenarios-command.js +167 -0
- package/dist/cli/startup.d.ts +112 -0
- package/dist/cli/startup.js +641 -0
- package/dist/cli/upgrade-command.d.ts +5 -0
- package/dist/cli/upgrade-command.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +680 -0
- package/dist/client/core.d.ts +33 -0
- package/dist/client/core.js +1260 -0
- package/dist/client/ensure-conductor-spawned.d.ts +35 -0
- package/dist/client/ensure-conductor-spawned.js +48 -0
- package/dist/client/index.d.ts +32 -0
- package/dist/client/index.js +22 -0
- package/dist/client/interface.d.ts +461 -0
- package/dist/client/interface.js +2 -0
- package/dist/client/subscribe.d.ts +108 -0
- package/dist/client/subscribe.js +598 -0
- package/dist/client/with-spawn.d.ts +27 -0
- package/dist/client/with-spawn.js +87 -0
- package/dist/config.d.ts +323 -0
- package/dist/config.js +593 -0
- package/dist/connection.d.ts +7 -0
- package/dist/connection.js +46 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.js +74 -0
- package/dist/copilot-bridge.d.ts +22 -0
- package/dist/copilot-bridge.js +565 -0
- package/dist/daemon-adapter-versions.d.ts +52 -0
- package/dist/daemon-adapter-versions.js +170 -0
- package/dist/daemon.d.ts +275 -0
- package/dist/daemon.js +989 -0
- package/dist/ensemble/agent-types.d.ts +23 -0
- package/dist/ensemble/agent-types.js +132 -0
- package/dist/ensemble/loader.d.ts +14 -0
- package/dist/ensemble/loader.js +140 -0
- package/dist/ensemble/saver.d.ts +49 -0
- package/dist/ensemble/saver.js +201 -0
- package/dist/ensemble/schema.d.ts +71 -0
- package/dist/ensemble/schema.js +3 -0
- package/dist/git-info.d.ts +4 -0
- package/dist/git-info.js +29 -0
- package/dist/http/aggregate.d.ts +319 -0
- package/dist/http/aggregate.js +684 -0
- package/dist/http/auth.d.ts +67 -0
- package/dist/http/auth.js +177 -0
- package/dist/http/body.d.ts +71 -0
- package/dist/http/body.js +121 -0
- package/dist/http/catalog.d.ts +67 -0
- package/dist/http/catalog.js +209 -0
- package/dist/http/cors.d.ts +42 -0
- package/dist/http/cors.js +111 -0
- package/dist/http/dashboard-pair.d.ts +94 -0
- package/dist/http/dashboard-pair.js +148 -0
- package/dist/http/dashboard.d.ts +20 -0
- package/dist/http/dashboard.js +160 -0
- package/dist/http/event-bus.d.ts +217 -0
- package/dist/http/event-bus.js +365 -0
- package/dist/http/event-id.d.ts +77 -0
- package/dist/http/event-id.js +117 -0
- package/dist/http/event-types.d.ts +348 -0
- package/dist/http/event-types.js +36 -0
- package/dist/http/fixtures/chat-stress.d.ts +8 -0
- package/dist/http/fixtures/chat-stress.js +63 -0
- package/dist/http/fixtures/conductor-leaving.d.ts +8 -0
- package/dist/http/fixtures/conductor-leaving.js +80 -0
- package/dist/http/fixtures/constants.d.ts +10 -0
- package/dist/http/fixtures/constants.js +13 -0
- package/dist/http/fixtures/eight-player-broadcast.d.ts +10 -0
- package/dist/http/fixtures/eight-player-broadcast.js +81 -0
- package/dist/http/fixtures/empty-ensemble.d.ts +6 -0
- package/dist/http/fixtures/empty-ensemble.js +26 -0
- package/dist/http/fixtures/index.d.ts +55 -0
- package/dist/http/fixtures/index.js +110 -0
- package/dist/http/fixtures/single-conductor.d.ts +7 -0
- package/dist/http/fixtures/single-conductor.js +46 -0
- package/dist/http/fixtures/sse-reconnect.d.ts +8 -0
- package/dist/http/fixtures/sse-reconnect.js +77 -0
- package/dist/http/index.d.ts +21 -0
- package/dist/http/index.js +61 -0
- package/dist/http/port-file.d.ts +22 -0
- package/dist/http/port-file.js +132 -0
- package/dist/http/responses.d.ts +27 -0
- package/dist/http/responses.js +40 -0
- package/dist/http/ring-buffer.d.ts +41 -0
- package/dist/http/ring-buffer.js +80 -0
- package/dist/http/server.d.ts +122 -0
- package/dist/http/server.js +459 -0
- package/dist/http/snapshot.d.ts +85 -0
- package/dist/http/snapshot.js +180 -0
- package/dist/http/sse-handler.d.ts +87 -0
- package/dist/http/sse-handler.js +294 -0
- package/dist/http/writes.d.ts +55 -0
- package/dist/http/writes.js +240 -0
- package/dist/palette/index.d.ts +138 -0
- package/dist/palette/index.js +221 -0
- package/dist/reconcile/orphans.d.ts +255 -0
- package/dist/reconcile/orphans.js +340 -0
- package/dist/scripts/258-spotcheck.js +303 -0
- package/dist/scripts/check-components-css-sync.js +199 -0
- package/dist/scripts/run-shard.js +121 -0
- package/dist/scripts/verify-daemon-isolation-guard.js +128 -0
- package/dist/server-tools.d.ts +87 -0
- package/dist/server-tools.js +146 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +366 -0
- package/dist/spawn.d.ts +296 -0
- package/dist/spawn.js +747 -0
- package/dist/tools/agent-types.d.ts +2 -0
- package/dist/tools/agent-types.js +21 -0
- package/dist/tools/attachment-info.d.ts +4 -0
- package/dist/tools/attachment-info.js +48 -0
- package/dist/tools/broadcast.d.ts +4 -0
- package/dist/tools/broadcast.js +76 -0
- package/dist/tools/cancel-stage.d.ts +3 -0
- package/dist/tools/cancel-stage.js +20 -0
- package/dist/tools/clear-state.d.ts +3 -0
- package/dist/tools/clear-state.js +37 -0
- package/dist/tools/coat-check-evict.d.ts +4 -0
- package/dist/tools/coat-check-evict.js +43 -0
- package/dist/tools/coat-check-get.d.ts +4 -0
- package/dist/tools/coat-check-get.js +56 -0
- package/dist/tools/coat-check-list.d.ts +4 -0
- package/dist/tools/coat-check-list.js +60 -0
- package/dist/tools/coat-check-put.d.ts +4 -0
- package/dist/tools/coat-check-put.js +53 -0
- package/dist/tools/cue.d.ts +44 -0
- package/dist/tools/cue.js +201 -0
- package/dist/tools/destroy.d.ts +4 -0
- package/dist/tools/destroy.js +188 -0
- package/dist/tools/detach.d.ts +4 -0
- package/dist/tools/detach.js +45 -0
- package/dist/tools/encore.d.ts +4 -0
- package/dist/tools/encore.js +31 -0
- package/dist/tools/ensemble.d.ts +32 -0
- package/dist/tools/ensemble.js +198 -0
- package/dist/tools/evaluate-gate.d.ts +3 -0
- package/dist/tools/evaluate-gate.js +32 -0
- package/dist/tools/fetch-state.d.ts +13 -0
- package/dist/tools/fetch-state.js +78 -0
- package/dist/tools/gates.d.ts +3 -0
- package/dist/tools/gates.js +41 -0
- package/dist/tools/helpers.d.ts +21 -0
- package/dist/tools/helpers.js +25 -0
- package/dist/tools/hosts.d.ts +4 -0
- package/dist/tools/hosts.js +40 -0
- package/dist/tools/listen.d.ts +3 -0
- package/dist/tools/listen.js +22 -0
- package/dist/tools/load-lineup.d.ts +5 -0
- package/dist/tools/load-lineup.js +381 -0
- package/dist/tools/migrate.d.ts +4 -0
- package/dist/tools/migrate.js +60 -0
- package/dist/tools/pause-ensemble.d.ts +4 -0
- package/dist/tools/pause-ensemble.js +58 -0
- package/dist/tools/pause.d.ts +4 -0
- package/dist/tools/pause.js +36 -0
- package/dist/tools/play.d.ts +4 -0
- package/dist/tools/play.js +57 -0
- package/dist/tools/quality-gate.d.ts +3 -0
- package/dist/tools/quality-gate.js +26 -0
- package/dist/tools/recall.d.ts +3 -0
- package/dist/tools/recall.js +32 -0
- package/dist/tools/recruit.d.ts +38 -0
- package/dist/tools/recruit.js +447 -0
- package/dist/tools/release.d.ts +4 -0
- package/dist/tools/release.js +98 -0
- package/dist/tools/report.d.ts +3 -0
- package/dist/tools/report.js +29 -0
- package/dist/tools/resolve.d.ts +1 -0
- package/dist/tools/resolve.js +7 -0
- package/dist/tools/restart.d.ts +35 -0
- package/dist/tools/restart.js +131 -0
- package/dist/tools/restore.d.ts +4 -0
- package/dist/tools/restore.js +107 -0
- package/dist/tools/resume-ensemble.d.ts +4 -0
- package/dist/tools/resume-ensemble.js +79 -0
- package/dist/tools/save-lineup.d.ts +4 -0
- package/dist/tools/save-lineup.js +36 -0
- package/dist/tools/save-state.d.ts +3 -0
- package/dist/tools/save-state.js +57 -0
- package/dist/tools/schedule.d.ts +4 -0
- package/dist/tools/schedule.js +152 -0
- package/dist/tools/schedules.d.ts +4 -0
- package/dist/tools/schedules.js +54 -0
- package/dist/tools/set-ensemble-description.d.ts +4 -0
- package/dist/tools/set-ensemble-description.js +37 -0
- package/dist/tools/set-name.d.ts +4 -0
- package/dist/tools/set-name.js +45 -0
- package/dist/tools/set-part.d.ts +3 -0
- package/dist/tools/set-part.js +20 -0
- package/dist/tools/shutdown.d.ts +4 -0
- package/dist/tools/shutdown.js +54 -0
- package/dist/tools/stage.d.ts +3 -0
- package/dist/tools/stage.js +28 -0
- package/dist/tools/stages.d.ts +3 -0
- package/dist/tools/stages.js +35 -0
- package/dist/tools/stop.d.ts +4 -0
- package/dist/tools/stop.js +29 -0
- package/dist/tools/unschedule.d.ts +4 -0
- package/dist/tools/unschedule.js +35 -0
- package/dist/tools/who-am-i.d.ts +3 -0
- package/dist/tools/who-am-i.js +34 -0
- package/dist/tools/worktree.d.ts +4 -0
- package/dist/tools/worktree.js +181 -0
- package/dist/tui/App.d.ts +85 -0
- package/dist/tui/App.js +1791 -0
- package/dist/tui/bootstrap-types.d.ts +46 -0
- package/dist/tui/bootstrap-types.js +7 -0
- package/dist/tui/client.d.ts +6 -0
- package/dist/tui/client.js +9 -0
- package/dist/tui/commands.d.ts +71 -0
- package/dist/tui/commands.js +1375 -0
- package/dist/tui/components/ActivityLog.d.ts +16 -0
- package/dist/tui/components/ActivityLog.js +36 -0
- package/dist/tui/components/ChatView.d.ts +35 -0
- package/dist/tui/components/ChatView.js +54 -0
- package/dist/tui/components/CommandOverlay.d.ts +15 -0
- package/dist/tui/components/CommandOverlay.js +34 -0
- package/dist/tui/components/CommandPalette.d.ts +21 -0
- package/dist/tui/components/CommandPalette.js +67 -0
- package/dist/tui/components/ConductorChat.d.ts +16 -0
- package/dist/tui/components/ConductorChat.js +32 -0
- package/dist/tui/components/ConversationStream.d.ts +114 -0
- package/dist/tui/components/ConversationStream.js +307 -0
- package/dist/tui/components/CreateEnsembleWizard.d.ts +19 -0
- package/dist/tui/components/CreateEnsembleWizard.js +223 -0
- package/dist/tui/components/DestroyConfirmModal.d.ts +17 -0
- package/dist/tui/components/DestroyConfirmModal.js +62 -0
- package/dist/tui/components/EnsembleListView.d.ts +14 -0
- package/dist/tui/components/EnsembleListView.js +32 -0
- package/dist/tui/components/EnsemblePanel.d.ts +12 -0
- package/dist/tui/components/EnsemblePanel.js +40 -0
- package/dist/tui/components/ErrorView.d.ts +31 -0
- package/dist/tui/components/ErrorView.js +129 -0
- package/dist/tui/components/HomeView.d.ts +54 -0
- package/dist/tui/components/HomeView.js +306 -0
- package/dist/tui/components/InputBar.d.ts +13 -0
- package/dist/tui/components/InputBar.js +58 -0
- package/dist/tui/components/LoadLineupModal.d.ts +18 -0
- package/dist/tui/components/LoadLineupModal.js +79 -0
- package/dist/tui/components/MainView.d.ts +21 -0
- package/dist/tui/components/MainView.js +107 -0
- package/dist/tui/components/NewEnsembleModal.d.ts +9 -0
- package/dist/tui/components/NewEnsembleModal.js +73 -0
- package/dist/tui/components/Picker.d.ts +23 -0
- package/dist/tui/components/Picker.js +70 -0
- package/dist/tui/components/PlayerDetailView.d.ts +26 -0
- package/dist/tui/components/PlayerDetailView.js +118 -0
- package/dist/tui/components/PromptArea.d.ts +50 -0
- package/dist/tui/components/PromptArea.js +303 -0
- package/dist/tui/components/RecruitWizard.d.ts +17 -0
- package/dist/tui/components/RecruitWizard.js +221 -0
- package/dist/tui/components/RestoreConfirmModal.d.ts +18 -0
- package/dist/tui/components/RestoreConfirmModal.js +71 -0
- package/dist/tui/components/ScheduleOverlay.d.ts +13 -0
- package/dist/tui/components/ScheduleOverlay.js +113 -0
- package/dist/tui/components/ScheduleWizard.d.ts +19 -0
- package/dist/tui/components/ScheduleWizard.js +259 -0
- package/dist/tui/components/Splash.d.ts +23 -0
- package/dist/tui/components/Splash.js +221 -0
- package/dist/tui/components/StatusBar.d.ts +48 -0
- package/dist/tui/components/StatusBar.js +128 -0
- package/dist/tui/components/StatusOverlay.d.ts +15 -0
- package/dist/tui/components/StatusOverlay.js +76 -0
- package/dist/tui/components/TitleBar.d.ts +10 -0
- package/dist/tui/components/TitleBar.js +21 -0
- package/dist/tui/components/TopBar.d.ts +12 -0
- package/dist/tui/components/TopBar.js +15 -0
- package/dist/tui/core-api.d.ts +26 -0
- package/dist/tui/core-api.js +67 -0
- package/dist/tui/hooks/useEnsembleDiscovery.d.ts +3 -0
- package/dist/tui/hooks/useEnsembleDiscovery.js +30 -0
- package/dist/tui/hooks/useMaestroPoller.d.ts +3 -0
- package/dist/tui/hooks/useMaestroPoller.js +36 -0
- package/dist/tui/hooks/useSendCommand.d.ts +7 -0
- package/dist/tui/hooks/useSendCommand.js +29 -0
- package/dist/tui/index.d.ts +15 -0
- package/dist/tui/index.js +156 -0
- package/dist/tui/ink-context.d.ts +18 -0
- package/dist/tui/ink-context.js +59 -0
- package/dist/tui/ink-loader.d.ts +26 -0
- package/dist/tui/ink-loader.js +42 -0
- package/dist/tui/removed-commands.d.ts +9 -0
- package/dist/tui/removed-commands.js +22 -0
- package/dist/tui/sse-handler.d.ts +52 -0
- package/dist/tui/sse-handler.js +157 -0
- package/dist/tui/store.d.ts +598 -0
- package/dist/tui/store.js +753 -0
- package/dist/tui/utils/format.d.ts +56 -0
- package/dist/tui/utils/format.js +155 -0
- package/dist/tui/utils/fullscreen.d.ts +23 -0
- package/dist/tui/utils/fullscreen.js +71 -0
- package/dist/tui/utils/history.d.ts +10 -0
- package/dist/tui/utils/history.js +85 -0
- package/dist/tui/utils/platform.d.ts +45 -0
- package/dist/tui/utils/platform.js +258 -0
- package/dist/tui/utils/theme.d.ts +21 -0
- package/dist/tui/utils/theme.js +24 -0
- package/dist/types.d.ts +1020 -0
- package/dist/types.js +39 -0
- package/dist/utils/attachment-format.d.ts +22 -0
- package/dist/utils/attachment-format.js +32 -0
- package/dist/utils/default-part.d.ts +43 -0
- package/dist/utils/default-part.js +104 -0
- package/dist/utils/duration.d.ts +30 -0
- package/dist/utils/duration.js +69 -0
- package/dist/utils/ensemble-ops.d.ts +61 -0
- package/dist/utils/ensemble-ops.js +77 -0
- package/dist/utils/format-hosts.d.ts +21 -0
- package/dist/utils/format-hosts.js +73 -0
- package/dist/utils/hosts.d.ts +113 -0
- package/dist/utils/hosts.js +265 -0
- package/dist/utils/parent-death-watchdog.d.ts +1 -0
- package/dist/utils/parent-death-watchdog.js +47 -0
- package/dist/utils/query-timeout.d.ts +103 -0
- package/dist/utils/query-timeout.js +113 -0
- package/dist/utils/recall-format.d.ts +78 -0
- package/dist/utils/recall-format.js +105 -0
- package/dist/utils/restore-format.d.ts +49 -0
- package/dist/utils/restore-format.js +91 -0
- package/dist/utils/safe-path.d.ts +10 -0
- package/dist/utils/safe-path.js +43 -0
- package/dist/utils/sdk-probe.d.ts +9 -0
- package/dist/utils/sdk-probe.js +45 -0
- package/dist/utils/search-attributes.d.ts +76 -0
- package/dist/utils/search-attributes.js +86 -0
- package/dist/utils/validation.d.ts +113 -0
- package/dist/utils/validation.js +163 -0
- package/dist/utils/visibility-deadline.d.ts +186 -0
- package/dist/utils/visibility-deadline.js +158 -0
- package/dist/utils/worktree.d.ts +103 -0
- package/dist/utils/worktree.js +327 -0
- package/dist/worker.d.ts +14 -0
- package/dist/worker.js +146 -0
- package/dist/workflows/attachment-math.d.ts +56 -0
- package/dist/workflows/attachment-math.js +47 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.js +11 -0
- package/dist/workflows/maestro-signals.d.ts +217 -0
- package/dist/workflows/maestro-signals.js +155 -0
- package/dist/workflows/maestro.d.ts +3 -0
- package/dist/workflows/maestro.js +812 -0
- package/dist/workflows/scheduler-signals.d.ts +10 -0
- package/dist/workflows/scheduler-signals.js +14 -0
- package/dist/workflows/scheduler.d.ts +17 -0
- package/dist/workflows/scheduler.js +143 -0
- package/dist/workflows/session.d.ts +2 -0
- package/dist/workflows/session.js +1638 -0
- package/dist/workflows/signals.d.ts +297 -0
- package/dist/workflows/signals.js +239 -0
- package/examples/agents/tempo-composer.md +56 -0
- package/examples/agents/tempo-conductor.md +117 -0
- package/examples/agents/tempo-critic.md +73 -0
- package/examples/agents/tempo-improv.md +74 -0
- package/examples/agents/tempo-liner.md +75 -0
- package/examples/agents/tempo-roadie.md +61 -0
- package/examples/agents/tempo-soloist.md +71 -0
- package/examples/agents/tempo-tuner.md +94 -0
- package/examples/ensembles/tempo-big-band.yaml +146 -0
- package/examples/ensembles/tempo-dev-team.yaml +58 -0
- package/examples/ensembles/tempo-headless-jam.yaml +77 -0
- package/examples/ensembles/tempo-jam-session.yaml +41 -0
- package/examples/ensembles/tempo-mock-jam.yaml +79 -0
- package/examples/ensembles/tempo-review-squad.yaml +32 -0
- package/package.json +172 -0
- package/packaging/launchd/com.agent.tempo.plist +46 -0
- package/packaging/systemd/agent-tempo.service +32 -0
- package/packaging/windows/install-task.ps1 +71 -0
- package/scenarios/conductor-recruit-mock.yaml +33 -0
- package/scenarios/echo-roundtrip.yaml +15 -0
- package/scenarios/multi-player-handoff.yaml +38 -0
- package/scenarios/recruit-cascade.yaml +38 -0
- package/scenarios/two-player-conversation.yaml +33 -0
- package/workflow-bundle.js +14146 -0
package/dist/tui/App.js
ADDED
|
@@ -0,0 +1,1791 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.App = App;
|
|
37
|
+
exports.stripLeadingIcon = stripLeadingIcon;
|
|
38
|
+
exports.isHomeView = isHomeView;
|
|
39
|
+
exports.pinnedConfirmationLines = pinnedConfirmationLines;
|
|
40
|
+
exports.countPinnedConfirmationLines = countPinnedConfirmationLines;
|
|
41
|
+
exports.pinnedTipLine = pinnedTipLine;
|
|
42
|
+
exports.countPinnedTipLines = countPinnedTipLines;
|
|
43
|
+
/**
|
|
44
|
+
* Root TUI application component — chat-focused shell with slash commands.
|
|
45
|
+
*
|
|
46
|
+
* Layout (top to bottom):
|
|
47
|
+
* - TitleBar (pinned)
|
|
48
|
+
* - Divider
|
|
49
|
+
* - Static scroll-up history
|
|
50
|
+
* - Live content area (splash, main, chat, error)
|
|
51
|
+
* - Divider
|
|
52
|
+
* - PromptArea (pinned)
|
|
53
|
+
*/
|
|
54
|
+
const react_1 = __importStar(require("react"));
|
|
55
|
+
const os_1 = require("os");
|
|
56
|
+
const ink_context_1 = require("./ink-context");
|
|
57
|
+
const store_1 = require("./store");
|
|
58
|
+
/**
|
|
59
|
+
* Track terminal rows so the root Box height stays < stdout.rows.
|
|
60
|
+
* Prevents Ink's fullscreen bypass (clearTerminal + full rewrite).
|
|
61
|
+
*/
|
|
62
|
+
function useTerminalRows() {
|
|
63
|
+
const [rows, setRows] = (0, react_1.useState)(process.stdout.rows || 24);
|
|
64
|
+
(0, react_1.useEffect)(() => {
|
|
65
|
+
const onResize = () => setRows(process.stdout.rows || 24);
|
|
66
|
+
process.stdout.on('resize', onResize);
|
|
67
|
+
return () => { process.stdout.off('resize', onResize); };
|
|
68
|
+
}, []);
|
|
69
|
+
return rows;
|
|
70
|
+
}
|
|
71
|
+
const Splash_1 = require("./components/Splash");
|
|
72
|
+
const ChatView_1 = require("./components/ChatView");
|
|
73
|
+
const ErrorView_1 = require("./components/ErrorView");
|
|
74
|
+
const RecruitWizard_1 = require("./components/RecruitWizard");
|
|
75
|
+
const PromptArea_1 = require("./components/PromptArea");
|
|
76
|
+
const StatusBar_1 = require("./components/StatusBar");
|
|
77
|
+
const ScheduleWizard_1 = require("./components/ScheduleWizard");
|
|
78
|
+
const CreateEnsembleWizard_1 = require("./components/CreateEnsembleWizard");
|
|
79
|
+
const HomeView_1 = require("./components/HomeView");
|
|
80
|
+
const NewEnsembleModal_1 = require("./components/NewEnsembleModal");
|
|
81
|
+
const LoadLineupModal_1 = require("./components/LoadLineupModal");
|
|
82
|
+
const RestoreConfirmModal_1 = require("./components/RestoreConfirmModal");
|
|
83
|
+
const DestroyConfirmModal_1 = require("./components/DestroyConfirmModal");
|
|
84
|
+
const CommandPalette_1 = require("./components/CommandPalette");
|
|
85
|
+
const StatusOverlay_1 = require("./components/StatusOverlay");
|
|
86
|
+
const ConversationStream_1 = require("./components/ConversationStream");
|
|
87
|
+
const PlayerDetailView_1 = require("./components/PlayerDetailView");
|
|
88
|
+
const Picker_1 = require("./components/Picker");
|
|
89
|
+
const commands_1 = require("./commands");
|
|
90
|
+
const removed_commands_1 = require("./removed-commands");
|
|
91
|
+
const theme_1 = require("./utils/theme");
|
|
92
|
+
const format_1 = require("./utils/format");
|
|
93
|
+
const platform_1 = require("./utils/platform");
|
|
94
|
+
const format_2 = require("./utils/format");
|
|
95
|
+
const history_1 = require("./utils/history");
|
|
96
|
+
const sse_handler_1 = require("./sse-handler");
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
98
|
+
const packageVersion = require('../../package.json').version;
|
|
99
|
+
let staticIdCounter = 0;
|
|
100
|
+
function nextStaticId() {
|
|
101
|
+
return `static-${++staticIdCounter}`;
|
|
102
|
+
}
|
|
103
|
+
/** Color for static item text. */
|
|
104
|
+
function staticItemColor(item) {
|
|
105
|
+
switch (item.type) {
|
|
106
|
+
case 'error': return theme_1.THEME.error;
|
|
107
|
+
case 'message': return theme_1.THEME.accent;
|
|
108
|
+
case 'splash-done': return theme_1.THEME.success;
|
|
109
|
+
case 'info': return theme_1.THEME.textMuted;
|
|
110
|
+
case 'command-output': return theme_1.THEME.text;
|
|
111
|
+
default: return theme_1.THEME.text;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function App({ api, ensemble, defaultAgent }) {
|
|
115
|
+
const { Box, Text, useApp, useInput } = (0, ink_context_1.useInk)();
|
|
116
|
+
const [state, dispatch] = (0, react_1.useReducer)(store_1.tuiReducer, (0, store_1.initialState)(ensemble));
|
|
117
|
+
const { exit } = useApp();
|
|
118
|
+
const termRows = useTerminalRows();
|
|
119
|
+
// ── Persistent command history ──
|
|
120
|
+
const [cmdHistory] = react_1.default.useState(() => (0, history_1.loadHistory)());
|
|
121
|
+
// ── Prompt ref (uncontrolled — input lives in PromptArea, not parent state) ──
|
|
122
|
+
const promptRef = react_1.default.useRef(null);
|
|
123
|
+
// Input value ref for palette filtering (no dispatch per keystroke)
|
|
124
|
+
const inputValueRef = react_1.default.useRef('');
|
|
125
|
+
// ── Refs for values read by useInput/useCallback (avoids stale closures + excess re-renders) ──
|
|
126
|
+
const lastSeenMsgRef = react_1.default.useRef(state.lastSeenMessageId);
|
|
127
|
+
const lastSeenMaestroRef = react_1.default.useRef(undefined);
|
|
128
|
+
const stateRef = react_1.default.useRef(state);
|
|
129
|
+
stateRef.current = state; // Always current on every render
|
|
130
|
+
// Track which messages have been committed to Static (overflow from live area)
|
|
131
|
+
const overflowCommittedRef = react_1.default.useRef(new Set());
|
|
132
|
+
// Overflow data computed during render, committed to Static via useEffect
|
|
133
|
+
const overflowRef = react_1.default.useRef(null);
|
|
134
|
+
// Callback for picker selection — set before showing picker, called on Enter
|
|
135
|
+
const pickerCallbackRef = react_1.default.useRef(null);
|
|
136
|
+
// Picker items ref — synced from pickerItems memo so useInput reads sorted items
|
|
137
|
+
const pickerItemsRef = react_1.default.useRef([]);
|
|
138
|
+
// Reset stale refs when switching ensembles + add separator
|
|
139
|
+
const prevEnsembleRef = react_1.default.useRef(state.activeEnsemble);
|
|
140
|
+
(0, react_1.useEffect)(() => {
|
|
141
|
+
lastSeenMsgRef.current = undefined;
|
|
142
|
+
lastSeenMaestroRef.current = undefined;
|
|
143
|
+
overflowCommittedRef.current.clear();
|
|
144
|
+
// Add separator when switching between ensembles (not on initial load)
|
|
145
|
+
if (state.activeEnsemble && prevEnsembleRef.current !== state.activeEnsemble && prevEnsembleRef.current !== undefined) {
|
|
146
|
+
dispatch({
|
|
147
|
+
type: 'COMMIT_STATIC',
|
|
148
|
+
item: {
|
|
149
|
+
id: nextStaticId(),
|
|
150
|
+
type: 'info',
|
|
151
|
+
content: `\u2500\u2500 Switched to ensemble: ${state.activeEnsemble} \u2500\u2500`,
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
prevEnsembleRef.current = state.activeEnsemble;
|
|
157
|
+
}, [state.activeEnsemble]);
|
|
158
|
+
// Commit overflow messages to Static scrollback after render (not during render)
|
|
159
|
+
(0, react_1.useEffect)(() => {
|
|
160
|
+
const data = overflowRef.current;
|
|
161
|
+
if (!data)
|
|
162
|
+
return;
|
|
163
|
+
const { formatted, startIdx } = data;
|
|
164
|
+
const overflow = formatted.slice(0, startIdx);
|
|
165
|
+
for (const msg of overflow) {
|
|
166
|
+
const key = `${msg.direction}:${msg.time}:${msg.body.slice(0, 60)}`;
|
|
167
|
+
if (!overflowCommittedRef.current.has(key)) {
|
|
168
|
+
overflowCommittedRef.current.add(key);
|
|
169
|
+
// Blank separator
|
|
170
|
+
dispatch({ type: 'COMMIT_STATIC', item: { id: nextStaticId(), type: 'info', content: '', timestamp: Date.now() } });
|
|
171
|
+
// Cap third-party messages to 4 lines; direct messages uncapped
|
|
172
|
+
const lines = msg.body.split('\n');
|
|
173
|
+
const lineCap = msg.thirdParty ? 4 : lines.length;
|
|
174
|
+
let body = lines.slice(0, lineCap).join('\n');
|
|
175
|
+
if (lines.length > lineCap) {
|
|
176
|
+
body += `\n\u2026 (${lines.length - lineCap} more lines)`;
|
|
177
|
+
}
|
|
178
|
+
// Commit entire body as a single message item — static renderer word-wraps correctly
|
|
179
|
+
dispatch({ type: 'COMMIT_STATIC', item: {
|
|
180
|
+
id: nextStaticId(), type: 'message', content: body, timestamp: Date.now(),
|
|
181
|
+
msgDirection: msg.direction, msgSender: msg.sender, msgTime: msg.time,
|
|
182
|
+
msgThirdParty: msg.thirdParty, msgRouteLabel: msg.routeLabel,
|
|
183
|
+
} });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
const handleHistoryUpdate = (0, react_1.useCallback)((entries) => {
|
|
188
|
+
(0, history_1.saveHistory)(entries);
|
|
189
|
+
}, []);
|
|
190
|
+
// ── Global keybindings (uses stateRef to avoid recreating on every poll) ──
|
|
191
|
+
useInput((0, react_1.useCallback)((input, key) => {
|
|
192
|
+
const s = stateRef.current;
|
|
193
|
+
if (key.ctrl && input === 'c') {
|
|
194
|
+
exit();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Scrollback navigation (Page Up/Down, Home/End)
|
|
198
|
+
// Scroll keys removed — terminal native scrollback via <Static> handles this
|
|
199
|
+
// Status overlay — Escape dismisses, ↑↓ scrolls
|
|
200
|
+
if (s.statusOverlay) {
|
|
201
|
+
if (key.escape) {
|
|
202
|
+
dispatch({ type: 'HIDE_STATUS' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (key.upArrow) {
|
|
206
|
+
dispatch({ type: 'STATUS_SCROLL_UP' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (key.downArrow) {
|
|
210
|
+
dispatch({ type: 'STATUS_SCROLL_DOWN' });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Interactive overlay — Escape dismisses, ↑↓ selects, action keys per type
|
|
216
|
+
if (s.overlay) {
|
|
217
|
+
if (key.escape) {
|
|
218
|
+
dispatch({ type: 'HIDE_OVERLAY' });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (key.upArrow) {
|
|
222
|
+
dispatch({ type: 'OVERLAY_SELECT', direction: 'up' });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (key.downArrow) {
|
|
226
|
+
dispatch({ type: 'OVERLAY_SELECT', direction: 'down' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// Schedule overlay action keys
|
|
230
|
+
if (s.overlay.type === 'schedules') {
|
|
231
|
+
if (input === 'n' || input === 'N') {
|
|
232
|
+
dispatch({ type: 'HIDE_OVERLAY' });
|
|
233
|
+
dispatch({ type: 'ENTER_SCHEDULE_WIZARD' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Gates/stages — Enter shows detail for selected item
|
|
238
|
+
if ((s.overlay.type === 'gates' || s.overlay.type === 'stages') && key.return) {
|
|
239
|
+
const selected = s.overlay.items[s.overlay.selectedIndex];
|
|
240
|
+
if (selected) {
|
|
241
|
+
const detail = selected.sublabel
|
|
242
|
+
? `\n ${selected.label}\n\n ${selected.sublabel.split(' ').join('\n ')}`
|
|
243
|
+
: `\n ${selected.label}\n\n No details available.`;
|
|
244
|
+
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title: selected.label.slice(0, 40), content: detail });
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
return; // Swallow all other input while overlay is active
|
|
249
|
+
}
|
|
250
|
+
// Player detail view — Escape goes back, ↑↓ scrolls messages
|
|
251
|
+
if (s.view === 'player') {
|
|
252
|
+
if (key.escape) {
|
|
253
|
+
dispatch({ type: 'NAVIGATE_ENSEMBLE', ensemble: s.activeEnsemble });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (key.upArrow) {
|
|
257
|
+
dispatch({ type: 'PLAYER_SCROLL_UP' });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (key.downArrow) {
|
|
261
|
+
dispatch({ type: 'PLAYER_SCROLL_DOWN' });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Picker overlay navigation
|
|
267
|
+
if (s.pickerVisible) {
|
|
268
|
+
if (key.escape) {
|
|
269
|
+
dispatch({ type: 'HIDE_PICKER' });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (key.upArrow) {
|
|
273
|
+
dispatch({ type: 'PICKER_UP' });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (key.downArrow) {
|
|
277
|
+
dispatch({ type: 'PICKER_DOWN' });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (key.return) {
|
|
281
|
+
const cb = pickerCallbackRef.current;
|
|
282
|
+
if (s.pickerType === 'players') {
|
|
283
|
+
const item = pickerItemsRef.current[s.pickerIndex];
|
|
284
|
+
if (item) {
|
|
285
|
+
dispatch({ type: 'HIDE_PICKER' });
|
|
286
|
+
if (cb) {
|
|
287
|
+
cb(item.id);
|
|
288
|
+
pickerCallbackRef.current = null;
|
|
289
|
+
}
|
|
290
|
+
else if (s.pickerIntent === 'navigate') {
|
|
291
|
+
// Navigate to player detail view
|
|
292
|
+
dispatch({ type: 'NAVIGATE_PLAYER', playerId: item.id });
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Default: navigate to player detail view
|
|
296
|
+
dispatch({ type: 'NAVIGATE_PLAYER', playerId: item.id });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else if (s.pickerType === 'ensembles') {
|
|
301
|
+
const ensItem = pickerItemsRef.current[s.pickerIndex];
|
|
302
|
+
if (ensItem) {
|
|
303
|
+
dispatch({ type: 'HIDE_PICKER' });
|
|
304
|
+
if (ensItem.id === '__create__') {
|
|
305
|
+
// Launch the create-ensemble wizard
|
|
306
|
+
dispatch({ type: 'ENTER_CREATE_ENSEMBLE' });
|
|
307
|
+
}
|
|
308
|
+
else if (cb) {
|
|
309
|
+
cb(ensItem.id);
|
|
310
|
+
pickerCallbackRef.current = null;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
dispatch({ type: 'NAVIGATE_ENSEMBLE', ensemble: ensItem.id });
|
|
314
|
+
dispatch({
|
|
315
|
+
type: 'COMMIT_STATIC',
|
|
316
|
+
item: { id: nextStaticId(), type: 'info', content: `Switched to ensemble: ${ensItem.id}`, timestamp: Date.now() },
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Destroy confirmation mode (PR-H: was `/stop`; now `/destroy` and routed
|
|
326
|
+
// through TempoClient.destroy() — the V2 outbox path — instead of the
|
|
327
|
+
// legacy raw-Temporal `terminatePlayer` shim.
|
|
328
|
+
if (s.confirmingStop) {
|
|
329
|
+
if (input === 'y' || input === 'Y') {
|
|
330
|
+
const target = s.confirmingStop;
|
|
331
|
+
const reason = s.confirmingStopReason;
|
|
332
|
+
dispatch({ type: 'CANCEL_STOP' });
|
|
333
|
+
(async () => {
|
|
334
|
+
try {
|
|
335
|
+
const ensembles = await api.discoverEnsembles();
|
|
336
|
+
for (const ens of ensembles) {
|
|
337
|
+
try {
|
|
338
|
+
await api.destroy(ens.name, target, reason);
|
|
339
|
+
// #306: command-result summary as a bottom-pinned notification
|
|
340
|
+
// so the user actually sees the confirmation when chat is busy.
|
|
341
|
+
(0, commands_1.commitNotification)(dispatch, 'info', `\u2716 Destroyed ${target}${reason ? ` (${reason})` : ''}.`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Try next ensemble
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `\u2717 Player "${target}" not found in any ensemble.`);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `\u2717 Failed to destroy ${target}: ${err}`);
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
354
|
+
}
|
|
355
|
+
else if (input === 'n' || input === 'N' || key.escape) {
|
|
356
|
+
dispatch({ type: 'CANCEL_STOP' });
|
|
357
|
+
dispatch({
|
|
358
|
+
type: 'COMMIT_STATIC',
|
|
359
|
+
item: { id: nextStaticId(), type: 'info', content: 'Destroy cancelled.', timestamp: Date.now() },
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Disband confirmation mode
|
|
365
|
+
if (s.confirmingDisband) {
|
|
366
|
+
if (input === 'y' || input === 'Y') {
|
|
367
|
+
const ensemble = s.confirmingDisband;
|
|
368
|
+
dispatch({ type: 'CANCEL_DISBAND' });
|
|
369
|
+
(async () => {
|
|
370
|
+
try {
|
|
371
|
+
const { terminated } = await api.disbandEnsemble(ensemble);
|
|
372
|
+
dispatch({
|
|
373
|
+
type: 'COMMIT_STATIC',
|
|
374
|
+
item: { id: nextStaticId(), type: 'info', content: `\u2714 Disbanded ensemble "${ensemble}" — terminated ${terminated} workflow${terminated !== 1 ? 's' : ''}.`, timestamp: Date.now() },
|
|
375
|
+
});
|
|
376
|
+
// Navigate back to home view
|
|
377
|
+
dispatch({ type: 'NAVIGATE_HOME' });
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `\u2717 Failed to disband "${ensemble}": ${err}`);
|
|
381
|
+
}
|
|
382
|
+
})();
|
|
383
|
+
}
|
|
384
|
+
else if (input === 'n' || input === 'N' || key.escape) {
|
|
385
|
+
dispatch({ type: 'CANCEL_DISBAND' });
|
|
386
|
+
dispatch({
|
|
387
|
+
type: 'COMMIT_STATIC',
|
|
388
|
+
item: { id: nextStaticId(), type: 'info', content: 'Disband cancelled.', timestamp: Date.now() },
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// Lineup confirmation mode
|
|
394
|
+
if (s.confirmingLineup) {
|
|
395
|
+
if (input === 'y' || input === 'Y') {
|
|
396
|
+
const { path: lineupPath } = s.confirmingLineup;
|
|
397
|
+
const activeEns = s.activeEnsemble;
|
|
398
|
+
dispatch({ type: 'CANCEL_LINEUP' });
|
|
399
|
+
if (!activeEns) {
|
|
400
|
+
(0, commands_1.commitNotification)(dispatch, 'error', 'No active ensemble.');
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
(async () => {
|
|
404
|
+
try {
|
|
405
|
+
await api.sendCommand(activeEns, `/load_lineup ${lineupPath}`, 'maestro');
|
|
406
|
+
dispatch({
|
|
407
|
+
type: 'COMMIT_STATIC',
|
|
408
|
+
item: { id: nextStaticId(), type: 'info', content: `\u2714 Lineup load requested: ${lineupPath}`, timestamp: Date.now() },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `\u2717 Failed to load lineup: ${err}`);
|
|
413
|
+
}
|
|
414
|
+
})();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (input === 'n' || input === 'N' || key.escape) {
|
|
418
|
+
dispatch({ type: 'CANCEL_LINEUP' });
|
|
419
|
+
dispatch({
|
|
420
|
+
type: 'COMMIT_STATIC',
|
|
421
|
+
item: { id: nextStaticId(), type: 'info', content: 'Lineup load cancelled.', timestamp: Date.now() },
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
// #306: Esc dismisses the oldest live notification when no other
|
|
427
|
+
// Esc-consumer is active (overlays, pickers, confirmations all return
|
|
428
|
+
// early above). Filter-expired-first is handled in the reducer so this
|
|
429
|
+
// always acts on what the user is actually looking at. No-op when the
|
|
430
|
+
// stack is empty, so other Esc fallthroughs — like clearing a typed
|
|
431
|
+
// command — aren't disturbed.
|
|
432
|
+
if (key.escape && s.notifications.some(n => n.expiresAt > Date.now())) {
|
|
433
|
+
dispatch({ type: 'DISMISS_OLDEST_NOTIFICATION' });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
}, [exit, api])); // Stable deps only — reads stateRef.current for everything else
|
|
437
|
+
// ── Derived: conductor player id ──
|
|
438
|
+
// #358: single source of truth — derive the active conductor's playerId from
|
|
439
|
+
// the `players` array rather than caching it in a separate state field that
|
|
440
|
+
// only the snapshot path updated. Incremental SSE events (`player.added`,
|
|
441
|
+
// `player.removed`) update `players` directly, so the badge stays accurate
|
|
442
|
+
// between snapshots. `undefined` when no conductor is in the ensemble.
|
|
443
|
+
const conductorPlayerId = (0, react_1.useMemo)(() => state.players.find(p => p.isConductor)?.playerId, [state.players]);
|
|
444
|
+
// ── Context string for title bar ──
|
|
445
|
+
const contextString = (0, react_1.useMemo)(() => {
|
|
446
|
+
if (state.phase === 'splash')
|
|
447
|
+
return 'Starting up...';
|
|
448
|
+
if (state.phase === 'error')
|
|
449
|
+
return 'Error';
|
|
450
|
+
if (state.chatTarget) {
|
|
451
|
+
const isConductor = state.chatTarget === conductorPlayerId;
|
|
452
|
+
const player = state.players.find(p => p.playerId === state.chatTarget);
|
|
453
|
+
const status = (0, format_1.phaseToLabel)(player?.phase);
|
|
454
|
+
const icon = isConductor ? '\u2605' : '\u2022';
|
|
455
|
+
return `${icon} ${state.chatTarget} \u00b7 ${status}${state.activeEnsemble ? ` \u00b7 ${state.activeEnsemble}` : ''}`;
|
|
456
|
+
}
|
|
457
|
+
if (state.activeEnsemble) {
|
|
458
|
+
// Headline count excludes the maestro session (TUI's own dashboard
|
|
459
|
+
// attachment). The full list with the maestro is still available in
|
|
460
|
+
// `/players` and the status overlay.
|
|
461
|
+
const count = (0, format_1.filterRealPlayers)(state.players).length;
|
|
462
|
+
const conductorInfo = conductorPlayerId ? '' : ' \u00b7 No conductor';
|
|
463
|
+
return `${state.activeEnsemble} \u00b7 ${count} player${count !== 1 ? 's' : ''}${conductorInfo} \u00b7 Connected`;
|
|
464
|
+
}
|
|
465
|
+
const count = state.ensembles?.length ?? 0;
|
|
466
|
+
return count > 0 ? `${count} ensemble${count !== 1 ? 's' : ''} \u00b7 Connected` : 'Discovering ensembles...';
|
|
467
|
+
}, [state.phase, state.chatTarget, state.activeEnsemble, state.players, state.ensembles, conductorPlayerId]);
|
|
468
|
+
// ── Hint text for prompt area ──
|
|
469
|
+
const promptHints = (0, react_1.useMemo)(() => {
|
|
470
|
+
if (state.confirmingStop) {
|
|
471
|
+
return `Destroy ${state.confirmingStop}? This will terminally end their session workflow. [y/N]`;
|
|
472
|
+
}
|
|
473
|
+
if (state.confirmingDisband) {
|
|
474
|
+
return `Disband ensemble "${state.confirmingDisband}"? All sessions will be terminated. [y/N]`;
|
|
475
|
+
}
|
|
476
|
+
if (state.confirmingLineup) {
|
|
477
|
+
return `${state.confirmingLineup.summary} [y/N]`;
|
|
478
|
+
}
|
|
479
|
+
if (state.phase === 'recruit') {
|
|
480
|
+
return 'Follow the prompts above. Esc to cancel.';
|
|
481
|
+
}
|
|
482
|
+
if (state.chatTarget) {
|
|
483
|
+
return `Chatting with ${state.chatTarget}. /back to return.`;
|
|
484
|
+
}
|
|
485
|
+
if (state.activeEnsemble) {
|
|
486
|
+
return 'Type a message, or @player to message directly. /players to list.';
|
|
487
|
+
}
|
|
488
|
+
return '/help /quit';
|
|
489
|
+
}, [state.phase, state.chatTarget, state.confirmingStop, state.confirmingDisband, state.activeEnsemble]);
|
|
490
|
+
// ── Completion data for prompt ──
|
|
491
|
+
const commandNamesList = (0, react_1.useMemo)(() => (0, commands_1.getCommandNames)(), []);
|
|
492
|
+
const playerNamesList = (0, react_1.useMemo)(() => state.players.map(p => p.playerId), [state.players]);
|
|
493
|
+
// ── Picker items ──
|
|
494
|
+
const pickerItems = (0, react_1.useMemo)(() => {
|
|
495
|
+
if (!state.pickerVisible)
|
|
496
|
+
return [];
|
|
497
|
+
if (state.pickerType === 'players') {
|
|
498
|
+
// Apply status filter if set.
|
|
499
|
+
const filtered = state.pickerStatusFilter
|
|
500
|
+
? state.players.filter(p => p.phase === state.pickerStatusFilter)
|
|
501
|
+
: state.players;
|
|
502
|
+
// Sort by type for grouping, conductor first
|
|
503
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
504
|
+
if (a.isConductor !== b.isConductor)
|
|
505
|
+
return a.isConductor ? -1 : 1;
|
|
506
|
+
const typeA = a.playerType || a.agentType || '';
|
|
507
|
+
const typeB = b.playerType || b.agentType || '';
|
|
508
|
+
return typeA.localeCompare(typeB) || a.playerId.localeCompare(b.playerId);
|
|
509
|
+
});
|
|
510
|
+
// Resolve icons once for the whole map (not per-item) per
|
|
511
|
+
// docs/tui-performance.md — `statusIcons()` allocates a small object.
|
|
512
|
+
const icons = (0, platform_1.statusIcons)((0, platform_1.supportsUnicode)());
|
|
513
|
+
return sorted.map(p => ({
|
|
514
|
+
id: p.playerId,
|
|
515
|
+
label: p.playerId,
|
|
516
|
+
detail: `[${(0, format_1.phaseToLabel)(p.phase)}]`,
|
|
517
|
+
meta: p.part || undefined,
|
|
518
|
+
icon: p.isConductor ? '\u2605' : icons[(0, format_1.phaseToIconName)(p.phase)],
|
|
519
|
+
color: (0, format_1.phaseToColor)(p.phase),
|
|
520
|
+
current: p.playerId === state.chatTarget,
|
|
521
|
+
group: p.playerType || p.agentType || 'unknown',
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
if (state.pickerType === 'ensembles') {
|
|
525
|
+
const items = (state.ensembles ?? []).map(ens => ({
|
|
526
|
+
id: ens.name,
|
|
527
|
+
label: ens.name,
|
|
528
|
+
detail: `${ens.playerCount} player${ens.playerCount !== 1 ? 's' : ''}`,
|
|
529
|
+
meta: ens.hasConductor ? '\u2605 conductor' : undefined,
|
|
530
|
+
current: ens.name === state.activeEnsemble,
|
|
531
|
+
}));
|
|
532
|
+
// Add "Create new ensemble" option at the bottom
|
|
533
|
+
items.push({
|
|
534
|
+
id: '__create__',
|
|
535
|
+
label: '+ Create new ensemble',
|
|
536
|
+
detail: 'launch wizard',
|
|
537
|
+
icon: '\u2795',
|
|
538
|
+
color: theme_1.THEME.accent,
|
|
539
|
+
});
|
|
540
|
+
return items;
|
|
541
|
+
}
|
|
542
|
+
return [];
|
|
543
|
+
}, [state.pickerVisible, state.pickerType, state.pickerStatusFilter, state.players, state.ensembles, state.chatTarget, state.activeEnsemble]);
|
|
544
|
+
pickerItemsRef.current = pickerItems;
|
|
545
|
+
// ── Command palette ──
|
|
546
|
+
const allPaletteCommands = (0, react_1.useMemo)(() => (0, commands_1.getCommandNames)().map(name => ({
|
|
547
|
+
name,
|
|
548
|
+
usage: commands_1.COMMANDS[name].usage,
|
|
549
|
+
description: commands_1.COMMANDS[name].description,
|
|
550
|
+
})), []);
|
|
551
|
+
// Palette filter state — updated via onInputChange ref callback (no dispatch per keystroke).
|
|
552
|
+
// Stores the full PaletteContext (mode + partial + replacePrefix) so the palette can
|
|
553
|
+
// show player names for `/restart <partial>`-style player-arg inputs, not just bare
|
|
554
|
+
// `/cmd` and `@name` inputs.
|
|
555
|
+
const [paletteCtx, setPaletteCtx] = (0, react_1.useState)(null);
|
|
556
|
+
const handleInputChange = (0, react_1.useCallback)((value) => {
|
|
557
|
+
inputValueRef.current = value;
|
|
558
|
+
const next = (0, commands_1.classifyPaletteInput)(value);
|
|
559
|
+
// Reference-equal no-op avoidance: only dispatch when mode/partial actually changed.
|
|
560
|
+
setPaletteCtx(prev => {
|
|
561
|
+
if (prev === next)
|
|
562
|
+
return prev;
|
|
563
|
+
if (prev && next && prev.mode === next.mode && prev.partial === next.partial && prev.replacePrefix === next.replacePrefix) {
|
|
564
|
+
return prev;
|
|
565
|
+
}
|
|
566
|
+
return next;
|
|
567
|
+
});
|
|
568
|
+
}, []);
|
|
569
|
+
// Player commands and subcommand map imported from commands.ts
|
|
570
|
+
const filteredPaletteCommands = (0, react_1.useMemo)(() => {
|
|
571
|
+
if (!state.paletteVisible || !paletteCtx)
|
|
572
|
+
return [];
|
|
573
|
+
if (paletteCtx.mode === 'player' || paletteCtx.mode === 'player-arg') {
|
|
574
|
+
return (0, commands_1.filterPlayerNames)(playerNamesList, paletteCtx.partial)
|
|
575
|
+
.map(n => ({ name: n, usage: `${paletteCtx.replacePrefix}${n}`, description: '' }));
|
|
576
|
+
}
|
|
577
|
+
// command mode
|
|
578
|
+
if (!paletteCtx.partial)
|
|
579
|
+
return allPaletteCommands;
|
|
580
|
+
return allPaletteCommands.filter(c => c.name.startsWith(paletteCtx.partial));
|
|
581
|
+
}, [state.paletteVisible, paletteCtx, allPaletteCommands, playerNamesList]);
|
|
582
|
+
// Clamp palette index
|
|
583
|
+
const clampedPaletteIndex = Math.min(state.paletteIndex, Math.max(0, filteredPaletteCommands.length - 1));
|
|
584
|
+
const handlePaletteToggle = (0, react_1.useCallback)((visible) => {
|
|
585
|
+
dispatch(visible ? { type: 'SHOW_PALETTE' } : { type: 'HIDE_PALETTE' });
|
|
586
|
+
}, []);
|
|
587
|
+
const handlePaletteUp = (0, react_1.useCallback)(() => {
|
|
588
|
+
dispatch({ type: 'PALETTE_UP' });
|
|
589
|
+
}, []);
|
|
590
|
+
const handlePaletteDown = (0, react_1.useCallback)(() => {
|
|
591
|
+
if (state.paletteIndex < filteredPaletteCommands.length - 1) {
|
|
592
|
+
dispatch({ type: 'PALETTE_DOWN' });
|
|
593
|
+
}
|
|
594
|
+
}, [state.paletteIndex, filteredPaletteCommands.length]);
|
|
595
|
+
const handlePaletteSelect = (0, react_1.useCallback)(() => {
|
|
596
|
+
if (filteredPaletteCommands.length > 0 && paletteCtx) {
|
|
597
|
+
const selected = filteredPaletteCommands[clampedPaletteIndex];
|
|
598
|
+
// replacePrefix already carries the right leading characters:
|
|
599
|
+
// command mode → '/' → `${/}recruit `
|
|
600
|
+
// player mode → '@' → `${@}conductor `
|
|
601
|
+
// player-arg mode → '/restart ' → `${/restart }conductor `
|
|
602
|
+
const value = `${paletteCtx.replacePrefix}${selected.name} `;
|
|
603
|
+
promptRef.current?.setValue(value);
|
|
604
|
+
inputValueRef.current = value;
|
|
605
|
+
dispatch({ type: 'HIDE_PALETTE' });
|
|
606
|
+
}
|
|
607
|
+
}, [filteredPaletteCommands, clampedPaletteIndex, paletteCtx]);
|
|
608
|
+
// ── Command submission handler ──
|
|
609
|
+
const handleSubmit = (0, react_1.useCallback)(async (input) => {
|
|
610
|
+
const trimmed = input.trim();
|
|
611
|
+
if (!trimmed)
|
|
612
|
+
return;
|
|
613
|
+
const s = stateRef.current;
|
|
614
|
+
// PromptArea clears itself on Enter (uncontrolled). Just clear our ref + palette.
|
|
615
|
+
inputValueRef.current = '';
|
|
616
|
+
if (s.paletteVisible)
|
|
617
|
+
dispatch({ type: 'HIDE_PALETTE' });
|
|
618
|
+
const parsed = (0, commands_1.parseCommand)(trimmed);
|
|
619
|
+
if (parsed) {
|
|
620
|
+
// Slash command
|
|
621
|
+
if (parsed.name === 'quit' || parsed.name === 'exit') {
|
|
622
|
+
exit();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (parsed.name === 'back') {
|
|
626
|
+
// Return to maestro view (exit any player chat)
|
|
627
|
+
if (s.chatTarget) {
|
|
628
|
+
dispatch({ type: 'EXIT_CHAT' });
|
|
629
|
+
dispatch({
|
|
630
|
+
type: 'COMMIT_STATIC',
|
|
631
|
+
item: { id: nextStaticId(), type: 'info', content: `\u2500\u2500 returned to maestro view \u2500\u2500`, timestamp: Date.now() },
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
else if (s.activeEnsemble) {
|
|
635
|
+
dispatch({ type: 'NAVIGATE_HOME' });
|
|
636
|
+
dispatch({
|
|
637
|
+
type: 'COMMIT_STATIC',
|
|
638
|
+
item: { id: nextStaticId(), type: 'info', content: `\u2500\u2500 returned to home view \u2500\u2500`, timestamp: Date.now() },
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (parsed.name === 'help') {
|
|
644
|
+
if (parsed.args.length > 0) {
|
|
645
|
+
const cmdName = parsed.args[0].replace(/^\//, '');
|
|
646
|
+
const cmd = commands_1.COMMANDS[cmdName];
|
|
647
|
+
if (cmd) {
|
|
648
|
+
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title: `Help \u00B7 /${cmdName}`, content: `\n ${cmd.description}\n\n Usage: ${cmd.usage}` });
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title: 'Help', content: `\n Unknown command: "${cmdName}"` });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title: 'Help', content: (0, commands_1.formatHelpSummary)() });
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
// Commands that open player picker when no args provided
|
|
660
|
+
const PICKER_COMMANDS = {
|
|
661
|
+
stop: (id) => {
|
|
662
|
+
dispatch({ type: 'COMMIT_STATIC', item: { id: nextStaticId(), type: 'info', content: `Stopping ${id}...`, timestamp: Date.now() } });
|
|
663
|
+
const cmd = commands_1.COMMANDS['stop'];
|
|
664
|
+
if (cmd?.handler)
|
|
665
|
+
cmd.handler([id], dispatch, api, { activeEnsemble: stateRef.current.activeEnsemble, defaultAgent });
|
|
666
|
+
},
|
|
667
|
+
players: (id) => {
|
|
668
|
+
dispatch({ type: 'NAVIGATE_PLAYER', playerId: id });
|
|
669
|
+
},
|
|
670
|
+
player: (id) => {
|
|
671
|
+
dispatch({ type: 'NAVIGATE_PLAYER', playerId: id });
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
if (PICKER_COMMANDS[parsed.name] && parsed.args.length === 0) {
|
|
675
|
+
pickerCallbackRef.current = PICKER_COMMANDS[parsed.name];
|
|
676
|
+
dispatch({ type: 'SHOW_PICKER', pickerType: 'players' });
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
// Alias: /player → /players
|
|
680
|
+
if (parsed.name === 'player') {
|
|
681
|
+
parsed.name = 'players';
|
|
682
|
+
}
|
|
683
|
+
if (!(0, commands_1.isValidCommand)(parsed.name)) {
|
|
684
|
+
const migrationHint = (0, removed_commands_1.removedSlashCommandHelp)(parsed.name);
|
|
685
|
+
(0, commands_1.commitNotification)(dispatch, 'error', migrationHint ?? `Unknown command: /${parsed.name}. Type /help for available commands.`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// Command exists but handler not yet implemented
|
|
689
|
+
const cmd = commands_1.COMMANDS[parsed.name];
|
|
690
|
+
if (!cmd.handler) {
|
|
691
|
+
dispatch({
|
|
692
|
+
type: 'COMMIT_STATIC',
|
|
693
|
+
item: {
|
|
694
|
+
id: nextStaticId(),
|
|
695
|
+
type: 'info',
|
|
696
|
+
content: `/${parsed.name}: coming soon. Usage: ${cmd.usage}`,
|
|
697
|
+
timestamp: Date.now(),
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
// Execute handler
|
|
703
|
+
try {
|
|
704
|
+
const ctx = { activeEnsemble: s.activeEnsemble, defaultAgent };
|
|
705
|
+
await cmd.handler(parsed.args, dispatch, api, ctx);
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `Error running /${parsed.name}: ${err}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
else if (s.activeEnsemble) {
|
|
712
|
+
// Bare text → route via @player or to conductor
|
|
713
|
+
const atMatch = trimmed.match(/^@(\S+)\s+(.+)$/s);
|
|
714
|
+
try {
|
|
715
|
+
if (atMatch) {
|
|
716
|
+
// @player message → send directly to that player
|
|
717
|
+
const [, targetPlayer, message] = atMatch;
|
|
718
|
+
dispatch({ type: 'APPEND_SENT_MESSAGE', to: targetPlayer, text: `@${targetPlayer} ${message}` });
|
|
719
|
+
api.sendAsMaestro(s.activeEnsemble, targetPlayer, message).catch(err => (0, commands_1.commitNotification)(dispatch, 'error', `\u2717 Failed to deliver to @${targetPlayer}: ${err}`));
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
// No @prefix → send to conductor.
|
|
723
|
+
// #358: derive from `players` (single source of truth) instead of
|
|
724
|
+
// a separate cached field. `hasConductor` is the snapshot-derived
|
|
725
|
+
// flag; we fall back to the legacy `'conductor'` literal when no
|
|
726
|
+
// playerId is yet known so a freshly-loaded ensemble with the
|
|
727
|
+
// hasConductor flag still routes correctly.
|
|
728
|
+
const conductorPid = s.players.find(p => p.isConductor)?.playerId;
|
|
729
|
+
if (!conductorPid && !s.hasConductor) {
|
|
730
|
+
// No conductor — show error
|
|
731
|
+
(0, commands_1.commitNotification)(dispatch, 'error', 'No conductor. Use @player to message directly, or /recruit a conductor.');
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const conductorTarget = conductorPid || 'conductor';
|
|
735
|
+
dispatch({ type: 'APPEND_SENT_MESSAGE', to: conductorTarget, text: trimmed });
|
|
736
|
+
api.sendCommand(s.activeEnsemble, trimmed, 'maestro').catch(err => (0, commands_1.commitNotification)(dispatch, 'error', `\u2717 Failed to deliver: ${err}`));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `\u2717 Error: ${err}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// Bare text in main mode — hint to use commands
|
|
745
|
+
dispatch({
|
|
746
|
+
type: 'COMMIT_STATIC',
|
|
747
|
+
item: {
|
|
748
|
+
id: nextStaticId(),
|
|
749
|
+
type: 'info',
|
|
750
|
+
content: 'Use /commands to interact. Type /help for available commands.',
|
|
751
|
+
timestamp: Date.now(),
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}, [api, exit]); // Reads stateRef.current for chatTarget/activeEnsemble
|
|
756
|
+
// ── Lightweight startup: check connectivity + ensure maestro session ──
|
|
757
|
+
// Phase starts at 'main' — polling handles all data discovery.
|
|
758
|
+
(0, react_1.useEffect)(() => {
|
|
759
|
+
let cancelled = false;
|
|
760
|
+
(async () => {
|
|
761
|
+
try {
|
|
762
|
+
// Check Temporal connectivity
|
|
763
|
+
const connected = await api.isConnected();
|
|
764
|
+
if (cancelled)
|
|
765
|
+
return;
|
|
766
|
+
if (!connected) {
|
|
767
|
+
dispatch({ type: 'SET_PHASE', phase: 'error', error: 'Cannot connect to Temporal. Run `agent-tempo up` first.' });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
// Mark splash as connected (updates splash UI)
|
|
771
|
+
dispatch({ type: 'SET_SPLASH_CONNECTED' });
|
|
772
|
+
// Ensure maestro session (best effort)
|
|
773
|
+
const ens = stateRef.current.activeEnsemble;
|
|
774
|
+
if (ens) {
|
|
775
|
+
try {
|
|
776
|
+
await api.ensureMaestroSession(ens);
|
|
777
|
+
}
|
|
778
|
+
catch (err) {
|
|
779
|
+
console.error('[tui] Failed to create maestro session:', err);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
if (!cancelled) {
|
|
785
|
+
dispatch({ type: 'SET_PHASE', phase: 'error', error: String(err) });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
})();
|
|
789
|
+
return () => { cancelled = true; };
|
|
790
|
+
}, [api]);
|
|
791
|
+
// ── Ensure maestro session exists when ensemble changes ──
|
|
792
|
+
(0, react_1.useEffect)(() => {
|
|
793
|
+
if (!state.activeEnsemble)
|
|
794
|
+
return;
|
|
795
|
+
api.ensureMaestroSession(state.activeEnsemble).catch(err => console.error('[tui] maestro session:', err));
|
|
796
|
+
}, [state.activeEnsemble, api]);
|
|
797
|
+
// ── #94/#95 PR-4a: data acquisition is split across three effects ──
|
|
798
|
+
//
|
|
799
|
+
// Previously a single 2 s `setInterval` fanned out 5 RPCs/tick (players,
|
|
800
|
+
// schedules, chat, paused, held) and conditionally drilled into a
|
|
801
|
+
// selected player. After PR-3 the per-ensemble surface is exposed via
|
|
802
|
+
// SSE, so we:
|
|
803
|
+
// 1. Keep a 2 s poll for the home view's ensemble list (no per-
|
|
804
|
+
// ensemble surface there; SSE wouldn't help).
|
|
805
|
+
// 2. Subscribe to the daemon's SSE event stream for the active
|
|
806
|
+
// ensemble so player/chat/flags/schedule updates land in
|
|
807
|
+
// sub-second latency rather than waiting for a poll tick.
|
|
808
|
+
// 3. Keep a 2 s poll for the player drill-in view — per
|
|
809
|
+
// docs/SSE-PROTOCOL.md §11 the per-player + per-message
|
|
810
|
+
// endpoints are intentionally Temporal-direct.
|
|
811
|
+
// PR-4b will replace the rendering primitives (chat scrollback +
|
|
812
|
+
// player list); this PR deliberately leaves layout untouched so a
|
|
813
|
+
// streaming regression is bisectable independent of scroll changes.
|
|
814
|
+
// Effect 1: home-view ensembles list polling.
|
|
815
|
+
(0, react_1.useEffect)(() => {
|
|
816
|
+
if (state.phase !== 'splash' && state.phase !== 'main' && state.phase !== 'chat')
|
|
817
|
+
return;
|
|
818
|
+
if (state.activeEnsemble)
|
|
819
|
+
return;
|
|
820
|
+
let cancelled = false;
|
|
821
|
+
const tick = async () => {
|
|
822
|
+
try {
|
|
823
|
+
const ensembles = await api.discoverEnsembles();
|
|
824
|
+
if (cancelled)
|
|
825
|
+
return;
|
|
826
|
+
// Intentionally no auto-select: HomeView is an explicit picker
|
|
827
|
+
// (Online / Paused / Offline, arrow keys + Enter). Auto-selecting
|
|
828
|
+
// on the poller was bouncing users back into a just-shut-down
|
|
829
|
+
// ensemble after `/shutdown`, `/back`, or `/disband`.
|
|
830
|
+
dispatch({ type: 'REFRESH_ENSEMBLES', ensembles });
|
|
831
|
+
}
|
|
832
|
+
catch (err) {
|
|
833
|
+
console.error('[tui:home-poll] error:', err);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
void tick();
|
|
837
|
+
const interval = setInterval(tick, 2000);
|
|
838
|
+
return () => {
|
|
839
|
+
cancelled = true;
|
|
840
|
+
clearInterval(interval);
|
|
841
|
+
};
|
|
842
|
+
}, [state.phase, state.activeEnsemble, api]);
|
|
843
|
+
// Effect 2: active-ensemble SSE subscription.
|
|
844
|
+
(0, react_1.useEffect)(() => {
|
|
845
|
+
if (state.phase !== 'splash' && state.phase !== 'main' && state.phase !== 'chat')
|
|
846
|
+
return;
|
|
847
|
+
if (!state.activeEnsemble)
|
|
848
|
+
return;
|
|
849
|
+
const ensemble = state.activeEnsemble;
|
|
850
|
+
const controller = new AbortController();
|
|
851
|
+
void (async () => {
|
|
852
|
+
try {
|
|
853
|
+
for await (const event of api.subscribe(ensemble, { signal: controller.signal })) {
|
|
854
|
+
await (0, sse_handler_1.handleSseEvent)(event, dispatch, ensemble, api);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
// AbortError on teardown is expected — only log unexpected failures.
|
|
859
|
+
if (controller.signal.aborted)
|
|
860
|
+
return;
|
|
861
|
+
console.error('[tui:subscribe] error:', err);
|
|
862
|
+
}
|
|
863
|
+
})();
|
|
864
|
+
return () => controller.abort();
|
|
865
|
+
}, [state.phase, state.activeEnsemble, api]);
|
|
866
|
+
// Effect 3: player drill-in polling (per spec §11 — Temporal-direct).
|
|
867
|
+
(0, react_1.useEffect)(() => {
|
|
868
|
+
if (state.view !== 'player')
|
|
869
|
+
return;
|
|
870
|
+
if (!state.activeEnsemble || !state.activePlayer)
|
|
871
|
+
return;
|
|
872
|
+
const ensemble = state.activeEnsemble;
|
|
873
|
+
const playerId = state.activePlayer;
|
|
874
|
+
let cancelled = false;
|
|
875
|
+
const tick = async () => {
|
|
876
|
+
try {
|
|
877
|
+
const [metadata, messages] = await Promise.all([
|
|
878
|
+
api.getPlayerMetadata(ensemble, playerId),
|
|
879
|
+
api.getPlayerMessages(ensemble, playerId),
|
|
880
|
+
]);
|
|
881
|
+
if (cancelled)
|
|
882
|
+
return;
|
|
883
|
+
dispatch({ type: 'REFRESH_PLAYER_DATA', metadata, messages });
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
// Best-effort — player may have been terminated mid-poll.
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
void tick();
|
|
890
|
+
const interval = setInterval(tick, 2000);
|
|
891
|
+
return () => {
|
|
892
|
+
cancelled = true;
|
|
893
|
+
clearInterval(interval);
|
|
894
|
+
};
|
|
895
|
+
}, [state.view, state.activeEnsemble, state.activePlayer, api]);
|
|
896
|
+
// ── Recruit wizard callbacks (must be before early return — Rules of Hooks) ──
|
|
897
|
+
const handleRecruitAnswer = (0, react_1.useCallback)((answer) => {
|
|
898
|
+
dispatch({ type: 'RECRUIT_NEXT_STEP', answer });
|
|
899
|
+
}, []);
|
|
900
|
+
const handleRecruitBack = (0, react_1.useCallback)(() => {
|
|
901
|
+
dispatch({ type: 'RECRUIT_PREV_STEP' });
|
|
902
|
+
}, []);
|
|
903
|
+
const handleRecruitConfirm = (0, react_1.useCallback)(async () => {
|
|
904
|
+
if (!state.recruitState)
|
|
905
|
+
return;
|
|
906
|
+
const activeEns = state.activeEnsemble;
|
|
907
|
+
if (!activeEns) {
|
|
908
|
+
dispatch({ type: 'RECRUIT_DONE', error: 'No active ensemble. Start one with: agent-tempo up <name>' });
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
dispatch({ type: 'RECRUIT_SUBMIT' });
|
|
912
|
+
const a = state.recruitState.answers;
|
|
913
|
+
try {
|
|
914
|
+
// #306: Direct TempoClient path — submit the recruit entry on the
|
|
915
|
+
// TUI's own maestro session instead of round-tripping through the
|
|
916
|
+
// conductor's Claude Code session. Works when no conductor is present
|
|
917
|
+
// (the wizard's original use-case) and eliminates the 2-5s LLM hop.
|
|
918
|
+
await api.recruit(activeEns, {
|
|
919
|
+
name: a.name,
|
|
920
|
+
workDir: a.workDir,
|
|
921
|
+
agent: a.agent,
|
|
922
|
+
...(a.playerType ? { playerType: a.playerType } : {}),
|
|
923
|
+
...(a.host && a.host !== 'localhost' ? { host: a.host } : {}),
|
|
924
|
+
...(a.initialMessage ? { initialMessage: a.initialMessage } : {}),
|
|
925
|
+
});
|
|
926
|
+
dispatch({ type: 'RECRUIT_DONE' });
|
|
927
|
+
dispatch({
|
|
928
|
+
type: 'COMMIT_STATIC',
|
|
929
|
+
item: {
|
|
930
|
+
id: nextStaticId(),
|
|
931
|
+
type: 'info',
|
|
932
|
+
content: `\u2714 Recruit requested: ${a.name} (${a.agent}${a.playerType ? ', type: ' + a.playerType : ''})`,
|
|
933
|
+
timestamp: Date.now(),
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
dispatch({ type: 'RECRUIT_DONE', error: String(err) });
|
|
939
|
+
}
|
|
940
|
+
}, [state.recruitState, state.activeEnsemble, api]);
|
|
941
|
+
const handleRecruitCancel = (0, react_1.useCallback)(() => {
|
|
942
|
+
dispatch({ type: 'EXIT_RECRUIT' });
|
|
943
|
+
dispatch({
|
|
944
|
+
type: 'COMMIT_STATIC',
|
|
945
|
+
item: { id: nextStaticId(), type: 'info', content: 'Recruit cancelled.', timestamp: Date.now() },
|
|
946
|
+
});
|
|
947
|
+
}, []);
|
|
948
|
+
const handleRecruitDone = (0, react_1.useCallback)(() => {
|
|
949
|
+
dispatch({ type: 'EXIT_RECRUIT' });
|
|
950
|
+
}, []);
|
|
951
|
+
// ── Schedule wizard callbacks ──
|
|
952
|
+
const handleScheduleAnswer = (0, react_1.useCallback)((answer) => {
|
|
953
|
+
dispatch({ type: 'SCHEDULE_NEXT_STEP', answer });
|
|
954
|
+
}, []);
|
|
955
|
+
const handleScheduleBack = (0, react_1.useCallback)(() => {
|
|
956
|
+
dispatch({ type: 'SCHEDULE_PREV_STEP' });
|
|
957
|
+
}, []);
|
|
958
|
+
const handleScheduleConfirm = (0, react_1.useCallback)(async () => {
|
|
959
|
+
if (!state.scheduleWizard)
|
|
960
|
+
return;
|
|
961
|
+
const activeEns = state.activeEnsemble;
|
|
962
|
+
if (!activeEns) {
|
|
963
|
+
dispatch({ type: 'SCHEDULE_DONE', error: 'No active ensemble.' });
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
dispatch({ type: 'SCHEDULE_SUBMIT' });
|
|
967
|
+
const a = state.scheduleWizard.answers;
|
|
968
|
+
try {
|
|
969
|
+
const parts = [`/schedule ${a.name} --to ${a.target}`];
|
|
970
|
+
if (a.schedType === 'delay')
|
|
971
|
+
parts.push(`--delay ${a.timing}`);
|
|
972
|
+
else if (a.schedType === 'at')
|
|
973
|
+
parts.push(`--at ${a.timing}`);
|
|
974
|
+
else if (a.schedType === 'every')
|
|
975
|
+
parts.push(`--every ${a.timing}`);
|
|
976
|
+
else if (a.schedType === 'cron') {
|
|
977
|
+
parts.push(`--cron "${a.timing}"`);
|
|
978
|
+
if (a.timezone)
|
|
979
|
+
parts.push(`--timezone ${a.timezone}`);
|
|
980
|
+
}
|
|
981
|
+
parts.push(a.message);
|
|
982
|
+
await api.sendCommand(activeEns, parts.join(' '), 'maestro');
|
|
983
|
+
dispatch({ type: 'SCHEDULE_DONE' });
|
|
984
|
+
dispatch({
|
|
985
|
+
type: 'COMMIT_STATIC',
|
|
986
|
+
item: { id: nextStaticId(), type: 'info', content: `\u2714 Schedule "${a.name}" creation requested.`, timestamp: Date.now() },
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
catch (err) {
|
|
990
|
+
dispatch({ type: 'SCHEDULE_DONE', error: String(err) });
|
|
991
|
+
}
|
|
992
|
+
}, [state.scheduleWizard, state.activeEnsemble, api]);
|
|
993
|
+
const handleScheduleCancel = (0, react_1.useCallback)(() => {
|
|
994
|
+
dispatch({ type: 'EXIT_SCHEDULE_WIZARD' });
|
|
995
|
+
dispatch({
|
|
996
|
+
type: 'COMMIT_STATIC',
|
|
997
|
+
item: { id: nextStaticId(), type: 'info', content: 'Schedule creation cancelled.', timestamp: Date.now() },
|
|
998
|
+
});
|
|
999
|
+
}, []);
|
|
1000
|
+
const handleScheduleDone = (0, react_1.useCallback)(() => {
|
|
1001
|
+
dispatch({ type: 'EXIT_SCHEDULE_WIZARD' });
|
|
1002
|
+
}, []);
|
|
1003
|
+
// ── Create ensemble wizard callbacks ──
|
|
1004
|
+
const handleCreateEnsAnswer = (0, react_1.useCallback)((answer) => {
|
|
1005
|
+
dispatch({ type: 'CREATE_ENSEMBLE_NEXT_STEP', answer });
|
|
1006
|
+
}, []);
|
|
1007
|
+
const handleCreateEnsBack = (0, react_1.useCallback)(() => {
|
|
1008
|
+
dispatch({ type: 'CREATE_ENSEMBLE_PREV_STEP' });
|
|
1009
|
+
}, []);
|
|
1010
|
+
const handleCreateEnsConfirm = (0, react_1.useCallback)(async () => {
|
|
1011
|
+
const wizState = stateRef.current.createEnsembleState;
|
|
1012
|
+
if (!wizState)
|
|
1013
|
+
return;
|
|
1014
|
+
dispatch({ type: 'CREATE_ENSEMBLE_SUBMIT' });
|
|
1015
|
+
const { name, workDir, lineup } = wizState.answers;
|
|
1016
|
+
try {
|
|
1017
|
+
await api.createEnsemble({ ensemble: name, workDir, ...(lineup ? { lineup } : {}) });
|
|
1018
|
+
dispatch({
|
|
1019
|
+
type: 'COMMIT_STATIC',
|
|
1020
|
+
item: { id: nextStaticId(), type: 'info', content: `\u2714 Ensemble "${name}" created.`, timestamp: Date.now() },
|
|
1021
|
+
});
|
|
1022
|
+
dispatch({ type: 'CREATE_ENSEMBLE_DONE', ensemble: name });
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
dispatch({ type: 'CREATE_ENSEMBLE_DONE', error: err instanceof Error ? err.message : String(err) });
|
|
1026
|
+
}
|
|
1027
|
+
}, [api]);
|
|
1028
|
+
const handleCreateEnsCancel = (0, react_1.useCallback)(() => {
|
|
1029
|
+
dispatch({ type: 'EXIT_CREATE_ENSEMBLE' });
|
|
1030
|
+
}, []);
|
|
1031
|
+
const handleCreateEnsDone = (0, react_1.useCallback)(() => {
|
|
1032
|
+
dispatch({ type: 'EXIT_CREATE_ENSEMBLE' });
|
|
1033
|
+
}, []);
|
|
1034
|
+
// ── Home view ─────────────────────────────────────────────────────────
|
|
1035
|
+
const [cwdGitRoot] = (0, react_1.useState)(() => {
|
|
1036
|
+
const { getGitInfo } = require('../git-info');
|
|
1037
|
+
return getGitInfo(process.cwd()).gitRoot ?? null;
|
|
1038
|
+
});
|
|
1039
|
+
const bootstrapInitial = (0, react_1.useMemo)(() => ({
|
|
1040
|
+
ensembles: state.ensembles ?? [],
|
|
1041
|
+
cwdGitRoot,
|
|
1042
|
+
badges: { orphanCount: 0 },
|
|
1043
|
+
}), [state.ensembles, cwdGitRoot]);
|
|
1044
|
+
const handleHomeEnter = (0, react_1.useCallback)((name) => {
|
|
1045
|
+
dispatch({ type: 'NAVIGATE_ENSEMBLE', ensemble: name });
|
|
1046
|
+
}, []);
|
|
1047
|
+
const handleHomeQuit = (0, react_1.useCallback)(() => {
|
|
1048
|
+
exit();
|
|
1049
|
+
}, [exit]);
|
|
1050
|
+
const handleHomeOpenNew = (0, react_1.useCallback)(() => {
|
|
1051
|
+
// #306: Use the full CreateEnsembleWizard (same as Splash's `+ Create new
|
|
1052
|
+
// ensemble` row) so the user gets the multi-step name → dir → lineup
|
|
1053
|
+
// flow instead of the bare single-prompt NewEnsembleModal.
|
|
1054
|
+
dispatch({ type: 'ENTER_CREATE_ENSEMBLE' });
|
|
1055
|
+
}, []);
|
|
1056
|
+
const handleHomeOpenLineup = (0, react_1.useCallback)(() => {
|
|
1057
|
+
dispatch({ type: 'OPEN_HOME_MODAL', modal: { type: 'lineup' } });
|
|
1058
|
+
}, []);
|
|
1059
|
+
const handleHomeOpenRestore = (0, react_1.useCallback)((ensembleName) => {
|
|
1060
|
+
const match = state.ensembles?.find((e) => e.name === ensembleName);
|
|
1061
|
+
dispatch({
|
|
1062
|
+
type: 'OPEN_HOME_MODAL',
|
|
1063
|
+
modal: {
|
|
1064
|
+
type: 'restore',
|
|
1065
|
+
ensemble: ensembleName,
|
|
1066
|
+
playerCount: Math.max(0, (match?.playerCount ?? 1) - (match?.hasConductor ? 1 : 0)),
|
|
1067
|
+
conductor: match?.hasConductor ? 'conductor' : undefined,
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
}, [state.ensembles]);
|
|
1071
|
+
const handleHomeModalClose = (0, react_1.useCallback)(() => {
|
|
1072
|
+
dispatch({ type: 'CLOSE_HOME_MODAL' });
|
|
1073
|
+
}, []);
|
|
1074
|
+
const handleHomeNewSubmit = (0, react_1.useCallback)(async (name) => {
|
|
1075
|
+
dispatch({ type: 'SET_HOME_MODAL_STATUS', submitting: true });
|
|
1076
|
+
try {
|
|
1077
|
+
await api.createEnsemble({ ensemble: name });
|
|
1078
|
+
dispatch({ type: 'CLOSE_HOME_MODAL' });
|
|
1079
|
+
dispatch({ type: 'NAVIGATE_ENSEMBLE', ensemble: name });
|
|
1080
|
+
}
|
|
1081
|
+
catch (err) {
|
|
1082
|
+
dispatch({
|
|
1083
|
+
type: 'SET_HOME_MODAL_STATUS',
|
|
1084
|
+
submitting: false,
|
|
1085
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}, [api]);
|
|
1089
|
+
const handleHomeLineupSubmit = (0, react_1.useCallback)(async (args) => {
|
|
1090
|
+
dispatch({ type: 'SET_HOME_MODAL_STATUS', submitting: true });
|
|
1091
|
+
try {
|
|
1092
|
+
await api.createEnsemble({ ensemble: args.ensemble, lineup: args.lineupPath });
|
|
1093
|
+
dispatch({ type: 'CLOSE_HOME_MODAL' });
|
|
1094
|
+
dispatch({ type: 'NAVIGATE_ENSEMBLE', ensemble: args.ensemble });
|
|
1095
|
+
}
|
|
1096
|
+
catch (err) {
|
|
1097
|
+
dispatch({
|
|
1098
|
+
type: 'SET_HOME_MODAL_STATUS',
|
|
1099
|
+
submitting: false,
|
|
1100
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}, [api]);
|
|
1104
|
+
// /destroy <ensemble> typed-name confirmation handlers (#291)
|
|
1105
|
+
const handleEnsembleDestroyInput = (0, react_1.useCallback)((next) => {
|
|
1106
|
+
dispatch({ type: 'ENSEMBLE_DESTROY_INPUT', input: next });
|
|
1107
|
+
}, []);
|
|
1108
|
+
const handleEnsembleDestroyCancel = (0, react_1.useCallback)(() => {
|
|
1109
|
+
dispatch({ type: 'CANCEL_ENSEMBLE_DESTROY' });
|
|
1110
|
+
dispatch({
|
|
1111
|
+
type: 'COMMIT_STATIC',
|
|
1112
|
+
item: { id: nextStaticId(), type: 'info', content: 'Destroy cancelled.', timestamp: Date.now() },
|
|
1113
|
+
});
|
|
1114
|
+
}, []);
|
|
1115
|
+
const handleEnsembleDestroySubmit = (0, react_1.useCallback)(async () => {
|
|
1116
|
+
const pending = stateRef.current.confirmingEnsembleDestroy;
|
|
1117
|
+
if (!pending)
|
|
1118
|
+
return;
|
|
1119
|
+
if (pending.input !== pending.ensemble) {
|
|
1120
|
+
dispatch({ type: 'ENSEMBLE_DESTROY_MISMATCH' });
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
dispatch({ type: 'ENSEMBLE_DESTROY_SUBMIT_BUSY' });
|
|
1124
|
+
const target = pending.ensemble;
|
|
1125
|
+
try {
|
|
1126
|
+
const summary = await api.destroy(target);
|
|
1127
|
+
dispatch({ type: 'CANCEL_ENSEMBLE_DESTROY' });
|
|
1128
|
+
if (summary && 'details' in summary) {
|
|
1129
|
+
// #306: aggregate ensemble-destroy summary surfaces as a bottom-pinned
|
|
1130
|
+
// notification so it's still visible after we navigate the user home.
|
|
1131
|
+
(0, commands_1.commitNotification)(dispatch, summary.failed > 0 ? 'error' : 'info', `\u2714 Destroyed "${target}" \u2014 ${summary.destroyed} destroyed, ${summary.terminated} terminated, ${summary.failed} failed.`);
|
|
1132
|
+
}
|
|
1133
|
+
dispatch({ type: 'NAVIGATE_HOME' });
|
|
1134
|
+
}
|
|
1135
|
+
catch (err) {
|
|
1136
|
+
dispatch({ type: 'CANCEL_ENSEMBLE_DESTROY' });
|
|
1137
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `\u2717 Destroy failed for "${target}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1138
|
+
}
|
|
1139
|
+
}, [api]);
|
|
1140
|
+
const handleHomeRestoreConfirm = (0, react_1.useCallback)(async () => {
|
|
1141
|
+
const modal = stateRef.current.homeModal;
|
|
1142
|
+
if (!modal || modal.type !== 'restore')
|
|
1143
|
+
return;
|
|
1144
|
+
const target = modal.ensemble;
|
|
1145
|
+
dispatch({ type: 'SET_HOME_MODAL_STATUS', submitting: true });
|
|
1146
|
+
try {
|
|
1147
|
+
const summary = await api.restore(target);
|
|
1148
|
+
dispatch({ type: 'CLOSE_HOME_MODAL' });
|
|
1149
|
+
if (summary.failed > 0) {
|
|
1150
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `Restore partial: ${summary.reattached} queued, ${summary.failed} failed, ${summary.skipped} skipped.`);
|
|
1151
|
+
}
|
|
1152
|
+
// Mirror the `/restore` slash two-op: ensure a conductor terminal is
|
|
1153
|
+
// live so the home-view restore path never strands the user on a
|
|
1154
|
+
// reattached-but-conductor-less ensemble.
|
|
1155
|
+
const { ensureConductorSpawned } = await Promise.resolve().then(() => __importStar(require('../client/ensure-conductor-spawned')));
|
|
1156
|
+
const conductorOutcome = await ensureConductorSpawned(target, api);
|
|
1157
|
+
if (!conductorOutcome.spawned && conductorOutcome.reason === 'spawnFailed') {
|
|
1158
|
+
(0, commands_1.commitNotification)(dispatch, 'error', `Conductor spawn failed for "${target}": ${conductorOutcome.error}`);
|
|
1159
|
+
}
|
|
1160
|
+
dispatch({ type: 'NAVIGATE_ENSEMBLE', ensemble: target });
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
dispatch({
|
|
1164
|
+
type: 'SET_HOME_MODAL_STATUS',
|
|
1165
|
+
submitting: false,
|
|
1166
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
}, [api]);
|
|
1170
|
+
// ── Memoize chat messages (must be before early return — Rules of Hooks) ──
|
|
1171
|
+
const memoizedChatData = (0, react_1.useMemo)(() => {
|
|
1172
|
+
if (!state.chatTarget)
|
|
1173
|
+
return null;
|
|
1174
|
+
const isConductorChat = state.chatTarget === conductorPlayerId;
|
|
1175
|
+
let chatMessages;
|
|
1176
|
+
if (isConductorChat) {
|
|
1177
|
+
const fromHistory = state.conductorHistory.map(entry => {
|
|
1178
|
+
if (entry.type === 'command') {
|
|
1179
|
+
const cmd = entry.data;
|
|
1180
|
+
return { direction: 'sent', from: cmd.source || 'maestro', text: cmd.text, timestamp: entry.timestamp || cmd.timestamp };
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
const report = entry.data;
|
|
1184
|
+
return { direction: 'received', from: report.playerId, text: `[${report.type}] ${report.text}`, timestamp: entry.timestamp || report.timestamp };
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
const fromSent = state.sentMessages
|
|
1188
|
+
.filter(m => m.to === state.chatTarget)
|
|
1189
|
+
.map(m => ({ direction: 'sent', from: 'maestro', text: m.text, timestamp: m.timestamp }));
|
|
1190
|
+
const seen = new Set();
|
|
1191
|
+
chatMessages = [...fromHistory, ...fromSent]
|
|
1192
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
1193
|
+
.filter(m => { const k = `${m.direction}:${m.timestamp}:${m.text.slice(0, 50)}`; if (seen.has(k))
|
|
1194
|
+
return false; seen.add(k); return true; });
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
const fromRelay = state.messages
|
|
1198
|
+
.filter(m => m.from === state.chatTarget || m.to === state.chatTarget)
|
|
1199
|
+
.map(m => ({ direction: m.to === state.chatTarget ? 'sent' : 'received', from: m.from, text: m.text, timestamp: m.timestamp }));
|
|
1200
|
+
const fromSent = state.sentMessages
|
|
1201
|
+
.filter(m => m.to === state.chatTarget)
|
|
1202
|
+
.map(m => ({ direction: 'sent', from: 'maestro', text: m.text, timestamp: m.timestamp }));
|
|
1203
|
+
const seen = new Set();
|
|
1204
|
+
chatMessages = [...fromRelay, ...fromSent]
|
|
1205
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
1206
|
+
.filter(m => { const k = `${m.direction}:${m.timestamp}:${m.text.slice(0, 50)}`; if (seen.has(k))
|
|
1207
|
+
return false; seen.add(k); return true; });
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
messages: chatMessages,
|
|
1211
|
+
received: chatMessages.filter(m => m.direction === 'received').length,
|
|
1212
|
+
sent: chatMessages.filter(m => m.direction === 'sent').length,
|
|
1213
|
+
isConductor: isConductorChat,
|
|
1214
|
+
};
|
|
1215
|
+
}, [state.chatTarget, conductorPlayerId, state.conductorHistory, state.messages, state.sentMessages]);
|
|
1216
|
+
// Note: relay messages are committed to staticItems directly in the poll loop.
|
|
1217
|
+
// Conductor history messages are committed when entering conductor chat mode.
|
|
1218
|
+
// ── Render ──
|
|
1219
|
+
// Divider — thin horizontal rule
|
|
1220
|
+
const dividerWidth = Math.max(20, (process.stdout.columns || 80) - 4);
|
|
1221
|
+
const dividerLine = '\u2500'.repeat(dividerWidth);
|
|
1222
|
+
// Splash → create ensemble handler: launch the create-ensemble wizard
|
|
1223
|
+
const handleSplashCreate = (0, react_1.useCallback)(() => {
|
|
1224
|
+
dispatch({ type: 'ENTER_CREATE_ENSEMBLE' });
|
|
1225
|
+
}, []);
|
|
1226
|
+
// Splash → main transition handler
|
|
1227
|
+
const handleSplashContinue = (0, react_1.useCallback)((selectedEnsemble) => {
|
|
1228
|
+
if (selectedEnsemble) {
|
|
1229
|
+
dispatch({ type: 'NAVIGATE_ENSEMBLE', ensemble: selectedEnsemble });
|
|
1230
|
+
dispatch({
|
|
1231
|
+
type: 'COMMIT_STATIC',
|
|
1232
|
+
item: { id: nextStaticId(), type: 'info', content: `\u2714 Connected to ensemble: ${selectedEnsemble}`, timestamp: Date.now() },
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
dispatch({ type: 'SET_PHASE', phase: 'main' });
|
|
1236
|
+
}, []);
|
|
1237
|
+
function renderLiveContent() {
|
|
1238
|
+
// Terminal size warning (non-blocking)
|
|
1239
|
+
const termCols = process.stdout.columns || 80;
|
|
1240
|
+
if (termCols < 60 || termRows < 15) {
|
|
1241
|
+
return react_1.default.createElement(Text, { color: theme_1.THEME.warning }, `\n \u26A0 Terminal too small (${termCols}\u00D7${termRows}). Resize to at least 60\u00D715 for best experience.`);
|
|
1242
|
+
}
|
|
1243
|
+
// Splash screen — shown on startup when no ensemble is specified
|
|
1244
|
+
if (state.phase === 'splash') {
|
|
1245
|
+
return react_1.default.createElement(Splash_1.Splash, {
|
|
1246
|
+
status: state.splashStatus,
|
|
1247
|
+
version: packageVersion,
|
|
1248
|
+
connected: state.splashConnected,
|
|
1249
|
+
ensembles: (state.ensembles ?? undefined),
|
|
1250
|
+
onContinue: handleSplashContinue,
|
|
1251
|
+
onCreateEnsemble: handleSplashCreate,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
if (state.phase === 'error') {
|
|
1255
|
+
return react_1.default.createElement(ErrorView_1.ErrorView, {
|
|
1256
|
+
version: packageVersion,
|
|
1257
|
+
checks: [
|
|
1258
|
+
{ label: `Cannot reach Temporal`, passed: false, detail: state.error },
|
|
1259
|
+
],
|
|
1260
|
+
errorDetail: state.error,
|
|
1261
|
+
onQuit: () => { process.exitCode = 1; exit(); },
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
// Picker takes over full content area
|
|
1265
|
+
if (state.pickerVisible) {
|
|
1266
|
+
const pickerTitle = state.pickerType === 'ensembles'
|
|
1267
|
+
? 'Select Ensemble'
|
|
1268
|
+
: 'Select Player';
|
|
1269
|
+
return react_1.default.createElement(Picker_1.Picker, {
|
|
1270
|
+
title: pickerTitle,
|
|
1271
|
+
items: pickerItems,
|
|
1272
|
+
selectedIndex: state.pickerIndex,
|
|
1273
|
+
hint: '\u2191\u2193 navigate, Enter select, Esc dismiss',
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (state.phase === 'recruit' && state.recruitState) {
|
|
1277
|
+
return react_1.default.createElement(RecruitWizard_1.RecruitWizard, {
|
|
1278
|
+
state: state.recruitState,
|
|
1279
|
+
onAnswer: handleRecruitAnswer,
|
|
1280
|
+
onBack: handleRecruitBack,
|
|
1281
|
+
onConfirm: handleRecruitConfirm,
|
|
1282
|
+
onCancel: handleRecruitCancel,
|
|
1283
|
+
onDone: handleRecruitDone,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
if (state.phase === 'schedule-create' && state.scheduleWizard) {
|
|
1287
|
+
return react_1.default.createElement(ScheduleWizard_1.ScheduleWizard, {
|
|
1288
|
+
state: state.scheduleWizard,
|
|
1289
|
+
onAnswer: handleScheduleAnswer,
|
|
1290
|
+
onBack: handleScheduleBack,
|
|
1291
|
+
onConfirm: handleScheduleConfirm,
|
|
1292
|
+
onCancel: handleScheduleCancel,
|
|
1293
|
+
onDone: handleScheduleDone,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
if (state.phase === 'create-ensemble' && state.createEnsembleState) {
|
|
1297
|
+
return react_1.default.createElement(CreateEnsembleWizard_1.CreateEnsembleWizard, {
|
|
1298
|
+
state: state.createEnsembleState,
|
|
1299
|
+
onAnswer: handleCreateEnsAnswer,
|
|
1300
|
+
onBack: handleCreateEnsBack,
|
|
1301
|
+
onConfirm: handleCreateEnsConfirm,
|
|
1302
|
+
onCancel: handleCreateEnsCancel,
|
|
1303
|
+
onDone: handleCreateEnsDone,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
// Home-view modals (#290). Render in place of HomeView so the modal
|
|
1307
|
+
// owns keyboard focus — HomeView's own useInput short-circuits when a
|
|
1308
|
+
// modal is up (matches the wizard pattern).
|
|
1309
|
+
if (state.homeModal?.type === 'new') {
|
|
1310
|
+
return react_1.default.createElement(NewEnsembleModal_1.NewEnsembleModal, {
|
|
1311
|
+
onSubmit: handleHomeNewSubmit,
|
|
1312
|
+
onCancel: handleHomeModalClose,
|
|
1313
|
+
submitting: state.homeModalSubmitting,
|
|
1314
|
+
error: state.homeModalError,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
if (state.homeModal?.type === 'lineup') {
|
|
1318
|
+
return react_1.default.createElement(LoadLineupModal_1.LoadLineupModal, {
|
|
1319
|
+
onSubmit: handleHomeLineupSubmit,
|
|
1320
|
+
onCancel: handleHomeModalClose,
|
|
1321
|
+
submitting: state.homeModalSubmitting,
|
|
1322
|
+
error: state.homeModalError,
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
if (state.homeModal?.type === 'restore') {
|
|
1326
|
+
return react_1.default.createElement(RestoreConfirmModal_1.RestoreConfirmModal, {
|
|
1327
|
+
ensemble: state.homeModal.ensemble,
|
|
1328
|
+
playerCount: state.homeModal.playerCount,
|
|
1329
|
+
conductorName: state.homeModal.conductor,
|
|
1330
|
+
onConfirm: handleHomeRestoreConfirm,
|
|
1331
|
+
onCancel: handleHomeModalClose,
|
|
1332
|
+
submitting: state.homeModalSubmitting,
|
|
1333
|
+
error: state.homeModalError,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
// /destroy <ensemble> typed-name confirmation modal (#291)
|
|
1337
|
+
if (state.confirmingEnsembleDestroy) {
|
|
1338
|
+
return react_1.default.createElement(DestroyConfirmModal_1.DestroyConfirmModal, {
|
|
1339
|
+
ensemble: state.confirmingEnsembleDestroy.ensemble,
|
|
1340
|
+
input: state.confirmingEnsembleDestroy.input,
|
|
1341
|
+
error: state.confirmingEnsembleDestroy.error,
|
|
1342
|
+
submitting: state.confirmingEnsembleDestroy.submitting,
|
|
1343
|
+
onInput: handleEnsembleDestroyInput,
|
|
1344
|
+
onSubmit: handleEnsembleDestroySubmit,
|
|
1345
|
+
onCancel: handleEnsembleDestroyCancel,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
// Home view (#290) — renders when on the 'home' navigation and the
|
|
1349
|
+
// app is past the splash connection check. The view pre-renders from
|
|
1350
|
+
// the current `ensembles` snapshot and refreshes itself on a timer.
|
|
1351
|
+
if (state.phase === 'main' && state.view === 'home' && state.splashConnected) {
|
|
1352
|
+
return react_1.default.createElement(HomeView_1.HomeView, {
|
|
1353
|
+
initial: bootstrapInitial,
|
|
1354
|
+
client: api,
|
|
1355
|
+
onEnterEnsemble: handleHomeEnter,
|
|
1356
|
+
onCreateEnsemble: handleHomeOpenNew,
|
|
1357
|
+
onLoadLineup: handleHomeOpenLineup,
|
|
1358
|
+
onRestoreEnsemble: handleHomeOpenRestore,
|
|
1359
|
+
onQuit: handleHomeQuit,
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
if (state.chatTarget && memoizedChatData) {
|
|
1363
|
+
const targetPlayer = state.players.find(p => p.playerId === state.chatTarget);
|
|
1364
|
+
return react_1.default.createElement(ChatView_1.ChatView, {
|
|
1365
|
+
targetPlayer: state.chatTarget,
|
|
1366
|
+
targetPart: targetPlayer?.part,
|
|
1367
|
+
targetBranch: targetPlayer?.gitBranch,
|
|
1368
|
+
targetStatus: targetPlayer?.phase,
|
|
1369
|
+
targetHost: targetPlayer?.hostname,
|
|
1370
|
+
localHost: (0, os_1.hostname)(),
|
|
1371
|
+
isConductor: memoizedChatData.isConductor,
|
|
1372
|
+
receivedCount: memoizedChatData.received,
|
|
1373
|
+
sentCount: memoizedChatData.sent,
|
|
1374
|
+
messages: memoizedChatData.messages,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
// Status overlay — card layout with scrolling
|
|
1378
|
+
if (state.statusOverlay && state.activeEnsemble) {
|
|
1379
|
+
return react_1.default.createElement(StatusOverlay_1.StatusOverlay, {
|
|
1380
|
+
players: state.players,
|
|
1381
|
+
ensemble: state.activeEnsemble,
|
|
1382
|
+
scrollOffset: state.statusScrollOffset,
|
|
1383
|
+
contentHeight,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
// Unified overlay — all overlay types rendered here
|
|
1387
|
+
if (state.overlay) {
|
|
1388
|
+
const ov = state.overlay;
|
|
1389
|
+
const children = [];
|
|
1390
|
+
children.push(react_1.default.createElement(Text, { key: 'ov-title', bold: true, color: theme_1.THEME.accent }, ` ${ov.title}`));
|
|
1391
|
+
if (ov.items.length === 0) {
|
|
1392
|
+
children.push('\n\n');
|
|
1393
|
+
children.push(react_1.default.createElement(Text, { key: 'ov-empty', color: theme_1.THEME.dim }, ' No items.'));
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
for (let i = 0; i < ov.items.length; i++) {
|
|
1397
|
+
const item = ov.items[i];
|
|
1398
|
+
const selected = i === ov.selectedIndex;
|
|
1399
|
+
const prefix = selected ? ' \u276F ' : ' ';
|
|
1400
|
+
children.push('\n\n');
|
|
1401
|
+
children.push(react_1.default.createElement(Text, { key: `ov-${i}`, color: selected ? theme_1.THEME.text : theme_1.THEME.dim, bold: selected }, `${prefix}${item.label}`));
|
|
1402
|
+
if (item.sublabel) {
|
|
1403
|
+
children.push('\n');
|
|
1404
|
+
children.push(react_1.default.createElement(Text, { key: `ovs-${i}`, color: theme_1.THEME.dim }, ` ${item.sublabel}`));
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
// Pad to fill contentHeight
|
|
1409
|
+
const usedLines = 1 + ov.items.reduce((n, item) => n + 2 + (item.sublabel ? 1 : 0), 0) + 2;
|
|
1410
|
+
const padLines = Math.max(0, contentHeight - usedLines);
|
|
1411
|
+
if (padLines > 0)
|
|
1412
|
+
children.push('\n'.repeat(padLines));
|
|
1413
|
+
children.push('\n');
|
|
1414
|
+
children.push(react_1.default.createElement(Text, { key: 'ov-hint', color: theme_1.THEME.dim }, ` ${ov.hint}`));
|
|
1415
|
+
return react_1.default.createElement(Text, null, ...children);
|
|
1416
|
+
}
|
|
1417
|
+
// Player detail view — shows player metadata + message history
|
|
1418
|
+
if (state.view === 'player' && state.activePlayer && state.activeEnsemble) {
|
|
1419
|
+
const player = state.players.find(p => p.playerId === state.activePlayer) || null;
|
|
1420
|
+
return react_1.default.createElement(PlayerDetailView_1.PlayerDetailView, {
|
|
1421
|
+
playerId: state.activePlayer,
|
|
1422
|
+
ensemble: state.activeEnsemble,
|
|
1423
|
+
player,
|
|
1424
|
+
metadata: state.playerMetadata,
|
|
1425
|
+
messages: state.playerMessages,
|
|
1426
|
+
scrollOffset: state.playerScrollOffset,
|
|
1427
|
+
localHost: (0, os_1.hostname)(),
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
// Main view — conversation stream (like Claude Code)
|
|
1431
|
+
if (state.activeEnsemble) {
|
|
1432
|
+
// Show loading state until first poll completes
|
|
1433
|
+
if (state.conversation === null) {
|
|
1434
|
+
return react_1.default.createElement(Text, { color: theme_1.THEME.dim }, '\n \u27F3 Loading messages...');
|
|
1435
|
+
}
|
|
1436
|
+
return react_1.default.createElement(ConversationStream_1.ConversationStream, {
|
|
1437
|
+
conversation: state.conversation,
|
|
1438
|
+
sentMessages: state.sentMessages,
|
|
1439
|
+
contentHeight,
|
|
1440
|
+
overflowRef,
|
|
1441
|
+
conductorPlayerId,
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
// No active ensemble — show ensemble list, loading state, or help
|
|
1445
|
+
// Still loading ensembles
|
|
1446
|
+
if (state.ensembles === null) {
|
|
1447
|
+
if (ensemble) {
|
|
1448
|
+
return react_1.default.createElement(Text, { color: theme_1.THEME.dim }, `\n \u27F3 Connecting to ${ensemble}...`);
|
|
1449
|
+
}
|
|
1450
|
+
return react_1.default.createElement(Text, { color: theme_1.THEME.dim }, '\n \u27F3 Discovering ensembles...');
|
|
1451
|
+
}
|
|
1452
|
+
if (state.ensembles.length > 0) {
|
|
1453
|
+
const ensLines = [
|
|
1454
|
+
react_1.default.createElement(Text, { key: 'eh', bold: true, color: theme_1.THEME.text }, `${state.ensembles.length} ensemble${state.ensembles.length !== 1 ? 's' : ''} running:`),
|
|
1455
|
+
];
|
|
1456
|
+
for (const ens of state.ensembles) {
|
|
1457
|
+
ensLines.push('\n');
|
|
1458
|
+
ensLines.push(react_1.default.createElement(Text, { key: ens.name, color: theme_1.THEME.textMuted }, ` ${ens.name} (${ens.playerCount} player${ens.playerCount !== 1 ? 's' : ''})${ens.hasConductor ? ' \u2605' : ''}`));
|
|
1459
|
+
}
|
|
1460
|
+
ensLines.push('\n\n');
|
|
1461
|
+
ensLines.push(react_1.default.createElement(Text, { key: 'hint', color: theme_1.THEME.dim }, ' Type /ensemble <name> to connect, or /ensemble to browse'));
|
|
1462
|
+
return react_1.default.createElement(Text, null, ...ensLines);
|
|
1463
|
+
}
|
|
1464
|
+
// No ensembles running (single Text, 1 Yoga node)
|
|
1465
|
+
return react_1.default.createElement(Text, null, '\n', react_1.default.createElement(Text, { bold: true, color: theme_1.THEME.accent }, ' Getting Started'), '\n', '\n', react_1.default.createElement(Text, { color: theme_1.THEME.text }, ' No ensembles are running.'), '\n', '\n', react_1.default.createElement(Text, { color: theme_1.THEME.text }, ' Create an ensemble:'), '\n', react_1.default.createElement(Text, { color: theme_1.THEME.accent }, ' /ensemble → + Create new ensemble'), '\n', '\n', react_1.default.createElement(Text, { color: theme_1.THEME.text }, ' Or load a lineup:'), '\n', react_1.default.createElement(Text, { color: theme_1.THEME.accent }, ' /lineup load <file.yml>'), '\n', '\n', react_1.default.createElement(Text, { color: theme_1.THEME.dim }, ' The TUI will auto-detect ensembles as they start.'), '\n', react_1.default.createElement(Text, { color: theme_1.THEME.dim }, ' Type /help for all available commands.'));
|
|
1466
|
+
}
|
|
1467
|
+
// ── Notification expiry tick (#306) ──
|
|
1468
|
+
// A single interval keeps the notifications stack fresh — when any
|
|
1469
|
+
// notification exists, bump the tick counter every 500ms so the render
|
|
1470
|
+
// pass re-evaluates `expiresAt > Date.now()` and drops expired entries
|
|
1471
|
+
// from view. Auto-stops (cleared to 0) when the stack empties, avoiding
|
|
1472
|
+
// a background timer when there's nothing to watch. Cheap — one integer
|
|
1473
|
+
// diff per tick, and only while notifications are live.
|
|
1474
|
+
(0, react_1.useEffect)(() => {
|
|
1475
|
+
if (state.notifications.length === 0)
|
|
1476
|
+
return undefined;
|
|
1477
|
+
const id = setInterval(() => {
|
|
1478
|
+
dispatch({ type: 'NOTIFICATION_TICK' });
|
|
1479
|
+
}, 500);
|
|
1480
|
+
return () => clearInterval(id);
|
|
1481
|
+
}, [state.notifications.length]);
|
|
1482
|
+
// ── Static items — rendered once to stdout, become native terminal scrollback ──
|
|
1483
|
+
const { Static } = (0, ink_context_1.useInk)();
|
|
1484
|
+
// Layout: header (2 lines) + content (variable) + footer (dynamic)
|
|
1485
|
+
// Content height is calculated to guarantee footer is always visible.
|
|
1486
|
+
// When command palette is visible, footer grows to accommodate palette items.
|
|
1487
|
+
const paletteLines = (state.paletteVisible && filteredPaletteCommands.length > 0)
|
|
1488
|
+
? Math.min(filteredPaletteCommands.length, 6) + (filteredPaletteCommands.length > 6 ? 2 : 0) // items + scroll indicators
|
|
1489
|
+
: 0;
|
|
1490
|
+
// #306: Notifications stack lives below the palette. Each live notification
|
|
1491
|
+
// takes one line; when the stack is empty, this contributes zero to the
|
|
1492
|
+
// footer so the main content area gets the full terminal height back.
|
|
1493
|
+
const now = Date.now();
|
|
1494
|
+
const notificationLines = state.notifications.filter(n => n.expiresAt > now).length;
|
|
1495
|
+
// Pinned confirmation lines — persist exactly as long as the state does,
|
|
1496
|
+
// no TTL. Each active confirmation state contributes one line above the
|
|
1497
|
+
// notifications stack. Keeps the y/N prompt anchored below the input so
|
|
1498
|
+
// it can't scroll away under new messages.
|
|
1499
|
+
const confirmationLines = countPinnedConfirmationLines(state);
|
|
1500
|
+
// #306 follow-up: Pinned paused/held tip — 1 row when an ensemble is
|
|
1501
|
+
// paused or held (or both), 0 otherwise. Same accounting pattern as
|
|
1502
|
+
// confirmationLines so the live content area reclaims the row when the
|
|
1503
|
+
// tip auto-clears on state change.
|
|
1504
|
+
const tipLines = countPinnedTipLines(state);
|
|
1505
|
+
// #306: Hide the chat prompt on the home view. Home is a wizard/picker
|
|
1506
|
+
// (arrow keys + Enter), not a chat target — there is no ensemble to talk
|
|
1507
|
+
// to, and a visible input box double-fires Enter (HomeView's own useInput
|
|
1508
|
+
// selects the row, PromptArea's useInput submits the empty buffer). The
|
|
1509
|
+
// Splash phase already follows this pattern; home now mirrors it. When
|
|
1510
|
+
// hidden we drop 2 lines from FOOTER_LINES (PromptArea row + the second
|
|
1511
|
+
// divider) so the live content area reclaims that space.
|
|
1512
|
+
const hidePrompt = isHomeView(state);
|
|
1513
|
+
const promptFooterLines = hidePrompt ? 0 : 2; // PromptArea + bottom divider
|
|
1514
|
+
const FOOTER_LINES = 2 + promptFooterLines + paletteLines + confirmationLines + tipLines + notificationLines; // StatusBar + divider + (PromptArea + bottom divider when shown) + palette + pinned confirmations + paused/held tip + notifications
|
|
1515
|
+
const contentHeight = Math.max(3, termRows - 1 - FOOTER_LINES);
|
|
1516
|
+
// Splash phase — full screen, no chrome (title/status/prompt hidden)
|
|
1517
|
+
if (state.phase === 'splash') {
|
|
1518
|
+
return react_1.default.createElement(Box, { flexDirection: 'column', height: termRows - 1, overflow: 'hidden' }, renderLiveContent());
|
|
1519
|
+
}
|
|
1520
|
+
// Root layout: <Static> items above, then live area constrained to terminal height
|
|
1521
|
+
return react_1.default.createElement(react_1.default.Fragment, null,
|
|
1522
|
+
// Static items — rendered once to stdout, become native terminal scrollback
|
|
1523
|
+
react_1.default.createElement(Static, { items: state.staticItems, children: (item) => {
|
|
1524
|
+
// Rich rendering for messages — header + indented body (matches live ConversationStream)
|
|
1525
|
+
if (item.type === 'message' && item.msgDirection) {
|
|
1526
|
+
const cols = process.stdout.columns || 80;
|
|
1527
|
+
const bodyWidth = Math.max(20, cols - 4);
|
|
1528
|
+
const wrapped = (0, format_2.wordWrap)(item.content, bodyWidth);
|
|
1529
|
+
if (item.msgDirection === 'out') {
|
|
1530
|
+
// Outbound: ♩ first line, then indented continuation (matches live)
|
|
1531
|
+
const firstLine = wrapped[0] || '';
|
|
1532
|
+
const pad = ' '.repeat(Math.max(0, cols - 2 - 3 - firstLine.length));
|
|
1533
|
+
const contLines = wrapped.slice(1).map(l => ` ${l}`.padEnd(cols - 2)).join('\n');
|
|
1534
|
+
const children = [
|
|
1535
|
+
react_1.default.createElement(Text, { backgroundColor: theme_1.THEME.inputBg, color: theme_1.THEME.accent, bold: true }, ' \u2669 '),
|
|
1536
|
+
react_1.default.createElement(Text, { backgroundColor: theme_1.THEME.inputBg, color: theme_1.THEME.text }, firstLine),
|
|
1537
|
+
react_1.default.createElement(Text, { backgroundColor: theme_1.THEME.inputBg, color: theme_1.THEME.dim }, pad),
|
|
1538
|
+
];
|
|
1539
|
+
if (contLines) {
|
|
1540
|
+
children.push('\n');
|
|
1541
|
+
children.push(react_1.default.createElement(Text, { backgroundColor: theme_1.THEME.inputBg, color: theme_1.THEME.text }, contLines));
|
|
1542
|
+
}
|
|
1543
|
+
return react_1.default.createElement(Text, { key: item.id }, ...children);
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
// Inbound: header + 3-space indent body
|
|
1547
|
+
const isThirdParty = item.msgThirdParty;
|
|
1548
|
+
const headerLabel = item.msgRouteLabel || item.msgSender || '';
|
|
1549
|
+
const headerPrefix = isThirdParty ? ' ' : ' \u2190 ';
|
|
1550
|
+
const headerColor = isThirdParty ? theme_1.THEME.dim : theme_1.THEME.accent;
|
|
1551
|
+
const bodyColor = isThirdParty ? theme_1.THEME.textMuted : theme_1.THEME.text;
|
|
1552
|
+
const bodyLines = wrapped.map(l => ` ${l}`).join('\n');
|
|
1553
|
+
return react_1.default.createElement(Text, { key: item.id }, react_1.default.createElement(Text, { color: theme_1.THEME.dim }, headerPrefix), react_1.default.createElement(Text, { color: headerColor }, headerLabel), react_1.default.createElement(Text, { color: theme_1.THEME.dim }, ` ${item.msgTime || ''}`), '\n', react_1.default.createElement(Text, { color: bodyColor }, bodyLines));
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
// Fallback for non-message items
|
|
1557
|
+
return react_1.default.createElement(Text, { key: item.id, color: staticItemColor(item) }, ` ${item.content}`);
|
|
1558
|
+
} }),
|
|
1559
|
+
// Live area — height constrained to termRows-1
|
|
1560
|
+
react_1.default.createElement(Box, { flexDirection: 'column', height: termRows - 1, overflow: 'hidden' },
|
|
1561
|
+
// Content area — full height above footer
|
|
1562
|
+
react_1.default.createElement(Box, { flexDirection: 'column', height: contentHeight, overflow: 'hidden' },
|
|
1563
|
+
// Live content area
|
|
1564
|
+
renderLiveContent()),
|
|
1565
|
+
// ── Footer (fixed height, always visible) ──
|
|
1566
|
+
// Status bar (1 Text node)
|
|
1567
|
+
react_1.default.createElement(StatusBar_1.StatusBar, {
|
|
1568
|
+
ensemble: state.activeEnsemble,
|
|
1569
|
+
players: state.players,
|
|
1570
|
+
playersLoaded: state.playersLoaded,
|
|
1571
|
+
scheduleCount: state.schedules.length,
|
|
1572
|
+
connected: true,
|
|
1573
|
+
ensemblePaused: state.ensemblePaused,
|
|
1574
|
+
ensembleHeld: state.ensembleHeld,
|
|
1575
|
+
}),
|
|
1576
|
+
// Bottom divider (1 Text node, no Box wrapper)
|
|
1577
|
+
react_1.default.createElement(Text, { color: theme_1.THEME.border }, ` ${dividerLine} `),
|
|
1578
|
+
// Prompt area + bottom divider — hidden on the home view (#306).
|
|
1579
|
+
// Home is a picker, not a chat target; the input would either eat keys
|
|
1580
|
+
// or double-fire Enter against HomeView's own useInput. Mirrors the
|
|
1581
|
+
// Splash phase, which renders no prompt at all.
|
|
1582
|
+
hidePrompt
|
|
1583
|
+
? null
|
|
1584
|
+
: react_1.default.createElement(PromptArea_1.PromptArea, {
|
|
1585
|
+
hints: promptHints,
|
|
1586
|
+
onSubmit: handleSubmit,
|
|
1587
|
+
disabled: (state.phase !== 'main' && state.phase !== 'chat') || !!state.confirmingStop || !!state.confirmingDisband || !!state.confirmingEnsembleDestroy || !!state.confirmingLineup || state.pickerVisible || state.statusOverlay || !!state.overlay,
|
|
1588
|
+
commandNames: commandNamesList,
|
|
1589
|
+
playerNames: playerNamesList,
|
|
1590
|
+
initialHistory: cmdHistory,
|
|
1591
|
+
onHistoryUpdate: handleHistoryUpdate,
|
|
1592
|
+
onInputChange: handleInputChange,
|
|
1593
|
+
paletteVisible: state.paletteVisible,
|
|
1594
|
+
onPaletteToggle: handlePaletteToggle,
|
|
1595
|
+
onPaletteUp: handlePaletteUp,
|
|
1596
|
+
onPaletteDown: handlePaletteDown,
|
|
1597
|
+
onPaletteSelect: handlePaletteSelect,
|
|
1598
|
+
inputRef: promptRef,
|
|
1599
|
+
}),
|
|
1600
|
+
// Bottom divider (1 Text node) — also hidden when the prompt is hidden
|
|
1601
|
+
// so the footer accounting in FOOTER_LINES stays consistent.
|
|
1602
|
+
hidePrompt
|
|
1603
|
+
? null
|
|
1604
|
+
: react_1.default.createElement(Text, { color: theme_1.THEME.border }, ` ${dividerLine} `),
|
|
1605
|
+
// Command palette (1 Text node when visible)
|
|
1606
|
+
state.paletteVisible && filteredPaletteCommands.length > 0
|
|
1607
|
+
? react_1.default.createElement(CommandPalette_1.CommandPalette, {
|
|
1608
|
+
commands: filteredPaletteCommands,
|
|
1609
|
+
selectedIndex: clampedPaletteIndex,
|
|
1610
|
+
// Display prefix mirrors what the user's input would become on select.
|
|
1611
|
+
prefix: paletteCtx?.replacePrefix ?? '/',
|
|
1612
|
+
})
|
|
1613
|
+
: null,
|
|
1614
|
+
// Pinned confirmation prompts — sit directly below the prompt area,
|
|
1615
|
+
// above the notifications stack. Unlike notifications, these have no
|
|
1616
|
+
// TTL and persist exactly as long as the corresponding `confirming*`
|
|
1617
|
+
// state field is set. Keeps the y/N (or typed-name) prompt visible
|
|
1618
|
+
// even when new chat messages are flooding the scroll-up history.
|
|
1619
|
+
renderPinnedConfirmations(state, Box, Text),
|
|
1620
|
+
// #306 follow-up: paused/held informational tip. Dim color so it
|
|
1621
|
+
// sits behind the warning-yellow confirmations and red-error
|
|
1622
|
+
// notifications visually. Auto-clears on state change.
|
|
1623
|
+
renderPinnedTip(state, Box, Text),
|
|
1624
|
+
// #306: Bottom-pinned notifications — errors/warnings stay visible below
|
|
1625
|
+
// the prompt until they TTL out (8s for errors, 5s otherwise) or the user
|
|
1626
|
+
// hits Esc. Filters by `expiresAt` every render; the notificationTick
|
|
1627
|
+
// counter forces periodic re-renders while entries are live.
|
|
1628
|
+
renderNotifications(state.notifications, Box, Text))); // closes Fragment
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* #306: Render the bottom-pinned notification stack. Kept as a free function
|
|
1632
|
+
* (not a component) so the caller composes Ink primitives directly — the
|
|
1633
|
+
* App's render tree is already heavy with createElement calls, and a
|
|
1634
|
+
* dedicated component for 10 lines of JSX would add a layout boundary for
|
|
1635
|
+
* no gain.
|
|
1636
|
+
*/
|
|
1637
|
+
function renderNotifications(notifications, Box, Text) {
|
|
1638
|
+
const now = Date.now();
|
|
1639
|
+
const live = notifications.filter(n => n.expiresAt > now);
|
|
1640
|
+
if (live.length === 0)
|
|
1641
|
+
return null;
|
|
1642
|
+
return react_1.default.createElement(Box, { flexDirection: 'column', paddingLeft: 1, paddingRight: 1 }, ...live.map(n => {
|
|
1643
|
+
const icon = n.kind === 'error' ? '✗'
|
|
1644
|
+
: n.kind === 'warn' ? '⚠'
|
|
1645
|
+
: 'ⓘ';
|
|
1646
|
+
const color = n.kind === 'error' ? theme_1.THEME.error
|
|
1647
|
+
: n.kind === 'warn' ? theme_1.THEME.warning
|
|
1648
|
+
: theme_1.THEME.accent;
|
|
1649
|
+
return react_1.default.createElement(Text, { key: `notif-${n.id}`, color }, `${icon} ${stripLeadingIcon(n.content)}`);
|
|
1650
|
+
}));
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* #306: Strip a leading kind-icon (`✗ `, `⚠ `, `ⓘ `) from notification
|
|
1654
|
+
* content. Defensive: many call sites historically prepended the icon into
|
|
1655
|
+
* the message string, and the renderer also prepends a kind-based icon —
|
|
1656
|
+
* without this normalization the user sees the icon twice (e.g.
|
|
1657
|
+
* `✗ ✗ Cannot destroy the conductor …`). Exported for unit testing.
|
|
1658
|
+
*/
|
|
1659
|
+
function stripLeadingIcon(content) {
|
|
1660
|
+
return content.replace(/^[✗⚠ⓘ]\s+/u, '');
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* #306: True when the TUI is on the home picker view — `phase === 'main'`
|
|
1664
|
+
* AND `view === 'home'`. The chat input has no target on this view (the
|
|
1665
|
+
* ensemble has not been entered yet), and HomeView owns Enter to navigate
|
|
1666
|
+
* its own row list. Render guard for PromptArea + the second divider so
|
|
1667
|
+
* the input cannot double-fire alongside HomeView's own `useInput`.
|
|
1668
|
+
*
|
|
1669
|
+
* Pure function, exported so tests can pin the guard's logic without
|
|
1670
|
+
* standing up an Ink render. Mirrors the splash-phase bypass pattern.
|
|
1671
|
+
*/
|
|
1672
|
+
function isHomeView(state) {
|
|
1673
|
+
return state.phase === 'main' && state.view === 'home';
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* #306: Build the pinned-confirmation line(s) for the current state. Returns
|
|
1677
|
+
* an array (possibly empty) of `{ key, text }` entries — one per active
|
|
1678
|
+
* `confirming*` state field. Rendered above the notifications stack by
|
|
1679
|
+
* `renderPinnedConfirmations` and sized by `countPinnedConfirmationLines`
|
|
1680
|
+
* so `FOOTER_LINES` reserves terminal rows correctly.
|
|
1681
|
+
*
|
|
1682
|
+
* Pure function, exported for unit testing — no Ink imports, no dispatch.
|
|
1683
|
+
* The render helper below is the only caller that wraps these in Text nodes.
|
|
1684
|
+
*/
|
|
1685
|
+
function pinnedConfirmationLines(state) {
|
|
1686
|
+
const out = [];
|
|
1687
|
+
if (state.confirmingStop) {
|
|
1688
|
+
const reason = state.confirmingStopReason ? ` Reason: ${state.confirmingStopReason}.` : '';
|
|
1689
|
+
out.push({
|
|
1690
|
+
key: 'confirm-stop',
|
|
1691
|
+
text: `⚠ Destroy ${state.confirmingStop}? Press y to confirm, n to cancel.${reason}`,
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
if (state.confirmingDisband) {
|
|
1695
|
+
out.push({
|
|
1696
|
+
key: 'confirm-disband',
|
|
1697
|
+
text: `⚠ Disband ensemble "${state.confirmingDisband}"? All sessions will be terminated. Press y to confirm, n to cancel.`,
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
if (state.confirmingEnsembleDestroy) {
|
|
1701
|
+
// Typed-name gate — the detailed input UX lives in the full-screen
|
|
1702
|
+
// modal; this pinned reminder mirrors the modal's question so the
|
|
1703
|
+
// bottom of the screen still answers "what am I being asked?" at a
|
|
1704
|
+
// glance. Shown even while the modal is up for layout consistency
|
|
1705
|
+
// with the other confirmation states.
|
|
1706
|
+
out.push({
|
|
1707
|
+
key: 'confirm-ensemble-destroy',
|
|
1708
|
+
text: `⚠ Destroy ensemble "${state.confirmingEnsembleDestroy.ensemble}"? Type the ensemble name to confirm, Esc to cancel.`,
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
if (state.confirmingLineup) {
|
|
1712
|
+
out.push({
|
|
1713
|
+
key: 'confirm-lineup',
|
|
1714
|
+
text: `⚠ ${state.confirmingLineup.summary}? Press y to confirm, n to cancel.`,
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
return out;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* #306: Count of pinned confirmation lines — one per active `confirming*`
|
|
1721
|
+
* state field. Consumed by the `FOOTER_LINES` reservation so the live
|
|
1722
|
+
* content area shrinks when a confirmation is active, keeping the pinned
|
|
1723
|
+
* prompt on-screen.
|
|
1724
|
+
*/
|
|
1725
|
+
function countPinnedConfirmationLines(state) {
|
|
1726
|
+
return pinnedConfirmationLines(state).length;
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* #306 follow-up: Build the pinned tip line for the current paused/held
|
|
1730
|
+
* state. Returns `null` when neither flag is set — no tip should render.
|
|
1731
|
+
*
|
|
1732
|
+
* The tip appears below the input prompt in a dim color (informational,
|
|
1733
|
+
* not an error or warning) and tells the user which slash commands they
|
|
1734
|
+
* need to fully resume the ensemble. `/load_lineup` flips both flags;
|
|
1735
|
+
* `/play` clears only paused; `/go` clears only held — without this
|
|
1736
|
+
* tip users would unpause an ensemble and stare at frozen players.
|
|
1737
|
+
*
|
|
1738
|
+
* Pure function, exported for unit testing — no Ink imports.
|
|
1739
|
+
*/
|
|
1740
|
+
function pinnedTipLine(state) {
|
|
1741
|
+
// Hide tips on the home view — there's no ensemble context to act on,
|
|
1742
|
+
// and the prompt itself is hidden there too (see `hidePrompt` in App).
|
|
1743
|
+
if (!state.activeEnsemble)
|
|
1744
|
+
return null;
|
|
1745
|
+
if (state.ensemblePaused && state.ensembleHeld) {
|
|
1746
|
+
return {
|
|
1747
|
+
key: 'tip-paused-held',
|
|
1748
|
+
text: 'Tip: Type /play to unpause + /go to release held players.',
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
if (state.ensemblePaused) {
|
|
1752
|
+
return { key: 'tip-paused', text: 'Tip: Type /play to resume.' };
|
|
1753
|
+
}
|
|
1754
|
+
if (state.ensembleHeld) {
|
|
1755
|
+
return { key: 'tip-held', text: 'Tip: Type /go to release held players.' };
|
|
1756
|
+
}
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Count of pinned tip lines (0 or 1). Mirrors
|
|
1761
|
+
* {@link countPinnedConfirmationLines} so `FOOTER_LINES` can reserve a
|
|
1762
|
+
* row for the tip without re-evaluating the state shape twice.
|
|
1763
|
+
*/
|
|
1764
|
+
function countPinnedTipLines(state) {
|
|
1765
|
+
return pinnedTipLine(state) ? 1 : 0;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* #306: Render the pinned confirmation prompts as Ink Text nodes. Kept
|
|
1769
|
+
* free-function (mirroring `renderNotifications`) because the App's root
|
|
1770
|
+
* render tree is already `createElement`-heavy and a dedicated component
|
|
1771
|
+
* would add a layout boundary for zero gain. Uses THEME.warning so the
|
|
1772
|
+
* user's eye is drawn away from the chat scroll-up area.
|
|
1773
|
+
*/
|
|
1774
|
+
function renderPinnedConfirmations(state, Box, Text) {
|
|
1775
|
+
const lines = pinnedConfirmationLines(state);
|
|
1776
|
+
if (lines.length === 0)
|
|
1777
|
+
return null;
|
|
1778
|
+
return react_1.default.createElement(Box, { flexDirection: 'column', paddingLeft: 1, paddingRight: 1 }, ...lines.map(line => react_1.default.createElement(Text, { key: line.key, color: theme_1.THEME.warning, bold: true }, line.text)));
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* #306 follow-up: Render the pinned paused/held tip below the input. Dim
|
|
1782
|
+
* (THEME.dim) so it reads as informational and doesn't compete visually
|
|
1783
|
+
* with the yellow confirmation prompts above or red notifications below.
|
|
1784
|
+
* Auto-clears when the state changes — no user dismissal needed.
|
|
1785
|
+
*/
|
|
1786
|
+
function renderPinnedTip(state, Box, Text) {
|
|
1787
|
+
const tip = pinnedTipLine(state);
|
|
1788
|
+
if (!tip)
|
|
1789
|
+
return null;
|
|
1790
|
+
return react_1.default.createElement(Box, { flexDirection: 'column', paddingLeft: 1, paddingRight: 1 }, react_1.default.createElement(Text, { key: tip.key, color: theme_1.THEME.dim }, tip.text));
|
|
1791
|
+
}
|