@zergai/ztc 0.4.0-beta.1 → 0.5.0-beta.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/README.md +398 -45
- package/RUST_THIRD_PARTY_LICENSES.txt +1080 -822
- package/THIRD_PARTY_NOTICES.md +273 -12
- package/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-audio-darwin-arm64 +0 -0
- package/bin/ztc-capture-darwin-arm64 +0 -0
- package/bin/ztc-computer-darwin-arm64 +0 -0
- package/bin/ztc-llm-darwin-arm64 +0 -0
- package/bin/ztc-tts-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/ztc-zlicky-overlay-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/App.js +1206 -209
- package/dist/agent/agent.d.ts +74 -8
- package/dist/agent/agent.js +1073 -184
- package/dist/agent/backends/account_rotation.d.ts +4 -0
- package/dist/agent/backends/account_rotation.js +8 -2
- package/dist/agent/backends/anthropic.d.ts +13 -0
- package/dist/agent/backends/anthropic.js +169 -28
- package/dist/agent/backends/claude_cli.js +18 -8
- package/dist/agent/backends/codex_app_server.d.ts +1 -0
- package/dist/agent/backends/codex_app_server.js +55 -3
- package/dist/agent/backends/codex_auth.d.ts +1 -0
- package/dist/agent/backends/codex_auth.js +67 -13
- package/dist/agent/backends/codex_buffered_recovery.d.ts +27 -0
- package/dist/agent/backends/codex_buffered_recovery.js +293 -0
- package/dist/agent/backends/gemini.js +3 -1
- package/dist/agent/backends/http_error.d.ts +26 -0
- package/dist/agent/backends/http_error.js +221 -0
- package/dist/agent/backends/index.d.ts +9 -2
- package/dist/agent/backends/index.js +22 -2
- package/dist/agent/backends/openai.d.ts +0 -6
- package/dist/agent/backends/openai.js +5 -2
- package/dist/agent/backends/openai_compatible.d.ts +5 -4
- package/dist/agent/backends/openai_compatible.js +26 -19
- package/dist/agent/backends/openai_responses.d.ts +8 -3
- package/dist/agent/backends/openai_responses.js +77 -33
- package/dist/agent/backends/provider_ids.d.ts +1 -0
- package/dist/agent/backends/provider_ids.js +4 -0
- package/dist/agent/backends/request_metrics.js +12 -2
- package/dist/agent/backends/types.d.ts +109 -6
- package/dist/agent/backends/types.js +5 -1
- package/dist/agent/backends/zrouter_stream.d.ts +6 -1
- package/dist/agent/backends/zrouter_stream.js +160 -42
- package/dist/agent/background_registry.d.ts +2 -0
- package/dist/agent/background_registry.js +1 -0
- package/dist/agent/claim_observation.d.ts +6 -0
- package/dist/agent/claim_observation.js +54 -0
- package/dist/agent/commands/accounts.js +46 -177
- package/dist/agent/commands/attention.d.ts +2 -0
- package/dist/agent/commands/attention.js +67 -0
- package/dist/agent/commands/auth.js +18 -2
- package/dist/agent/commands/board.d.ts +2 -0
- package/dist/agent/commands/board.js +169 -0
- package/dist/agent/commands/clipboard.js +50 -1
- package/dist/agent/commands/compact.js +21 -13
- package/dist/agent/commands/config.js +4 -1
- package/dist/agent/commands/context.js +6 -1
- package/dist/agent/commands/desktop.d.ts +2 -0
- package/dist/agent/commands/desktop.js +29 -0
- package/dist/agent/commands/effort.js +14 -6
- package/dist/agent/commands/environment.d.ts +3 -0
- package/dist/agent/commands/environment.js +109 -0
- package/dist/agent/commands/evidence.d.ts +2 -0
- package/dist/agent/commands/evidence.js +9 -0
- package/dist/agent/commands/fork.js +4 -3
- package/dist/agent/commands/goal.d.ts +33 -1
- package/dist/agent/commands/goal.js +174 -14
- package/dist/agent/commands/goal_exit.d.ts +36 -0
- package/dist/agent/commands/goal_exit.js +120 -0
- package/dist/agent/commands/help.js +3 -1
- package/dist/agent/commands/history.js +3 -1
- package/dist/agent/commands/inbox.d.ts +2 -0
- package/dist/agent/commands/inbox.js +148 -0
- package/dist/agent/commands/index.js +16 -1
- package/dist/agent/commands/input_mode.js +8 -6
- package/dist/agent/commands/job.d.ts +2 -0
- package/dist/agent/commands/job.js +93 -0
- package/dist/agent/commands/keybindings.js +4 -1
- package/dist/agent/commands/label.d.ts +2 -0
- package/dist/agent/commands/label.js +77 -0
- package/dist/agent/commands/layout.d.ts +2 -0
- package/dist/agent/commands/layout.js +20 -0
- package/dist/agent/commands/mirror.js +16 -1
- package/dist/agent/commands/model.js +12 -2
- package/dist/agent/commands/permissions.d.ts +3 -1
- package/dist/agent/commands/permissions.js +79 -89
- package/dist/agent/commands/plan.js +5 -15
- package/dist/agent/commands/profile.js +21 -2
- package/dist/agent/commands/provider_selection.js +20 -1
- package/dist/agent/commands/rename.js +1 -1
- package/dist/agent/commands/resources.d.ts +2 -0
- package/dist/agent/commands/resources.js +67 -0
- package/dist/agent/commands/rewind.d.ts +3 -0
- package/dist/agent/commands/rewind.js +16 -0
- package/dist/agent/commands/runbooks.d.ts +2 -0
- package/dist/agent/commands/runbooks.js +73 -0
- package/dist/agent/commands/runbooks_lazy.d.ts +2 -0
- package/dist/agent/commands/runbooks_lazy.js +8 -0
- package/dist/agent/commands/sessions.d.ts +1 -1
- package/dist/agent/commands/sessions.js +33 -5
- package/dist/agent/commands/settings.js +185 -39
- package/dist/agent/commands/setup.d.ts +2 -0
- package/dist/agent/commands/setup.js +78 -0
- package/dist/agent/commands/share.d.ts +2 -0
- package/dist/agent/commands/share.js +64 -0
- package/dist/agent/commands/status.js +14 -2
- package/dist/agent/commands/types.d.ts +99 -6
- package/dist/agent/commands/update.js +8 -0
- package/dist/agent/commands/usage.d.ts +2 -0
- package/dist/agent/commands/usage.js +8 -0
- package/dist/agent/factory.d.ts +11 -0
- package/dist/agent/factory.js +113 -88
- package/dist/agent/loop_policy.d.ts +6 -3
- package/dist/agent/loop_policy.js +12 -3
- package/dist/agent/measurement_guidance.d.ts +2 -0
- package/dist/agent/measurement_guidance.js +5 -0
- package/dist/agent/request_encoder.d.ts +8 -1
- package/dist/agent/request_encoder.js +95 -23
- package/dist/agent/runtime/benchmark_comparison.d.ts +30 -0
- package/dist/agent/runtime/benchmark_comparison.js +94 -0
- package/dist/agent/runtime/benchmark_contract.d.ts +48 -0
- package/dist/agent/runtime/benchmark_contract.js +90 -0
- package/dist/agent/runtime/checked_commands.d.ts +32 -0
- package/dist/agent/runtime/checked_commands.js +119 -0
- package/dist/agent/runtime/command_execution_policy.d.ts +26 -0
- package/dist/agent/runtime/command_execution_policy.js +91 -0
- package/dist/agent/runtime/command_jobs.d.ts +36 -18
- package/dist/agent/runtime/command_jobs.js +151 -13
- package/dist/agent/runtime/command_panes.d.ts +59 -0
- package/dist/agent/runtime/command_panes.js +398 -0
- package/dist/agent/runtime/command_wait.d.ts +19 -0
- package/dist/agent/runtime/command_wait.js +167 -0
- package/dist/agent/runtime/cpu_benchmark.d.ts +30 -0
- package/dist/agent/runtime/cpu_benchmark.js +126 -0
- package/dist/agent/runtime/delegated_evidence.d.ts +26 -0
- package/dist/agent/runtime/delegated_evidence.js +80 -0
- package/dist/agent/runtime/evidence.d.ts +127 -0
- package/dist/agent/runtime/evidence.js +468 -0
- package/dist/agent/runtime/execution_context.d.ts +7 -0
- package/dist/agent/runtime/execution_context.js +39 -3
- package/dist/agent/runtime/json_verifier_recipe.d.ts +8 -0
- package/dist/agent/runtime/json_verifier_recipe.js +66 -0
- package/dist/agent/runtime/measurements.d.ts +110 -0
- package/dist/agent/runtime/measurements.js +317 -0
- package/dist/agent/runtime/policy.d.ts +4 -0
- package/dist/agent/runtime/policy.js +31 -20
- package/dist/agent/runtime/provider_native_evidence.d.ts +20 -0
- package/dist/agent/runtime/provider_native_evidence.js +86 -0
- package/dist/agent/runtime/recovery.d.ts +2 -0
- package/dist/agent/runtime/recovery.js +45 -2
- package/dist/agent/runtime/remote_broker_transport.d.ts +10 -0
- package/dist/agent/runtime/remote_broker_transport.js +84 -0
- package/dist/agent/runtime/remote_deletion_receipts.d.ts +3 -0
- package/dist/agent/runtime/remote_deletion_receipts.js +43 -0
- package/dist/agent/runtime/remote_journal_replay.d.ts +13 -0
- package/dist/agent/runtime/remote_journal_replay.js +233 -0
- package/dist/agent/runtime/remote_mutation_journal.d.ts +25 -0
- package/dist/agent/runtime/remote_mutation_journal.js +208 -0
- package/dist/agent/runtime/remote_resource_endpoint.d.ts +7 -0
- package/dist/agent/runtime/remote_resource_endpoint.js +6 -0
- package/dist/agent/runtime/remote_resource_errors.d.ts +5 -0
- package/dist/agent/runtime/remote_resource_errors.js +9 -0
- package/dist/agent/runtime/remote_resource_script.d.ts +2 -0
- package/dist/agent/runtime/remote_resource_script.js +1009 -0
- package/dist/agent/runtime/remote_resources.d.ts +29 -0
- package/dist/agent/runtime/remote_resources.js +160 -0
- package/dist/agent/runtime/source_verification.d.ts +12 -0
- package/dist/agent/runtime/source_verification.js +210 -0
- package/dist/agent/runtime/source_verification_contract.d.ts +75 -0
- package/dist/agent/runtime/source_verification_contract.js +203 -0
- package/dist/agent/runtime/tool_checkpoints.d.ts +11 -0
- package/dist/agent/runtime/tool_checkpoints.js +26 -0
- package/dist/agent/runtime/tool_effects.d.ts +5 -0
- package/dist/agent/runtime/tool_progress.d.ts +12 -0
- package/dist/agent/runtime/tool_progress.js +76 -0
- package/dist/agent/runtime/tool_runtime.d.ts +16 -1
- package/dist/agent/runtime/tool_runtime.js +204 -10
- package/dist/agent/runtime/tracing.d.ts +1 -1
- package/dist/agent/runtime/verifier_capture.d.ts +9 -0
- package/dist/agent/runtime/verifier_capture.js +100 -0
- package/dist/agent/runtime/verifier_contract.d.ts +24 -0
- package/dist/agent/runtime/verifier_contract.js +41 -0
- package/dist/agent/subagent.d.ts +20 -10
- package/dist/agent/subagent.js +266 -29
- package/dist/agent/tool_response_directory.d.ts +24 -0
- package/dist/agent/tool_response_directory.js +61 -0
- package/dist/agent/tool_response_source.d.ts +8 -0
- package/dist/agent/tool_response_source.js +49 -0
- package/dist/agent/tool_response_store.d.ts +75 -4
- package/dist/agent/tool_response_store.js +583 -64
- package/dist/agent/tools/ask_user.d.ts +3 -1
- package/dist/agent/tools/ask_user.js +30 -11
- package/dist/agent/tools/background.js +1 -0
- package/dist/agent/tools/benchmark_cpu.d.ts +2 -0
- package/dist/agent/tools/benchmark_cpu.js +102 -0
- package/dist/agent/tools/compare_benchmarks.d.ts +2 -0
- package/dist/agent/tools/compare_benchmarks.js +53 -0
- package/dist/agent/tools/deferred.js +1 -0
- package/dist/agent/tools/delegate.js +18 -2
- package/dist/agent/tools/helper_requirements.d.ts +7 -0
- package/dist/agent/tools/helper_requirements.js +19 -0
- package/dist/agent/tools/index.d.ts +1 -0
- package/dist/agent/tools/index.js +3 -0
- package/dist/agent/tools/measurement_report.d.ts +22 -0
- package/dist/agent/tools/measurement_report.js +190 -0
- package/dist/agent/tools/pane.d.ts +0 -7
- package/dist/agent/tools/pane.js +5 -4
- package/dist/agent/tools/project_context.d.ts +3 -0
- package/dist/agent/tools/project_context.js +45 -0
- package/dist/agent/tools/recall_tool_response.d.ts +3 -5
- package/dist/agent/tools/recall_tool_response.js +19 -29
- package/dist/agent/tools/registry.d.ts +1 -0
- package/dist/agent/tools/registry.js +23 -0
- package/dist/agent/tools/related_sessions.d.ts +2 -0
- package/dist/agent/tools/related_sessions.js +156 -0
- package/dist/agent/tools/routine.js +13 -3
- package/dist/agent/tools/session_evidence.d.ts +3 -0
- package/dist/agent/tools/session_evidence.js +72 -0
- package/dist/agent/tools/shell.d.ts +4 -3
- package/dist/agent/tools/shell.js +174 -47
- package/dist/agent/tools/tool_search.js +3 -2
- package/dist/agent/tools/types.d.ts +28 -0
- package/dist/agent/tools/verify_artifact.d.ts +2 -0
- package/dist/agent/tools/verify_artifact.js +60 -0
- package/dist/agent/tools/workflow.js +1 -0
- package/dist/app/activity.d.ts +6 -1
- package/dist/app/activity.js +81 -12
- package/dist/app/agent_runner.d.ts +6 -1
- package/dist/app/agent_runner.js +34 -19
- package/dist/app/command_context.js +106 -10
- package/dist/app/controller.d.ts +15 -4
- package/dist/app/controller.js +29 -3
- package/dist/app/escape_stop.d.ts +11 -0
- package/dist/app/escape_stop.js +15 -0
- package/dist/app/liveness.d.ts +6 -4
- package/dist/app/liveness.js +13 -3
- package/dist/app/model_catalog_cache.d.ts +5 -0
- package/dist/app/model_catalog_cache.js +138 -0
- package/dist/app/ordered_transcript.js +43 -4
- package/dist/app/prompt_turn.d.ts +14 -0
- package/dist/app/prompt_turn.js +53 -0
- package/dist/app/recap.d.ts +3 -0
- package/dist/app/recap.js +9 -3
- package/dist/app/restart.d.ts +3 -2
- package/dist/app/restart.js +22 -7
- package/dist/app/rewind.d.ts +3 -3
- package/dist/app/rewind.js +3 -3
- package/dist/app/selection_completion.d.ts +17 -2
- package/dist/app/selection_completion.js +84 -9
- package/dist/app/session_manager.d.ts +38 -6
- package/dist/app/session_manager.js +263 -60
- package/dist/app/session_messages.d.ts +3 -0
- package/dist/app/session_messages.js +60 -0
- package/dist/app/task_scope_router.d.ts +20 -0
- package/dist/app/task_scope_router.js +173 -0
- package/dist/app/transcript_window.d.ts +2 -0
- package/dist/app/transcript_window.js +4 -0
- package/dist/app/workspace_admission_details.d.ts +3 -0
- package/dist/app/workspace_admission_details.js +26 -0
- package/dist/app/workspace_layout_controller.d.ts +11 -0
- package/dist/app/workspace_layout_controller.js +88 -0
- package/dist/app/workspace_monitor.d.ts +1 -0
- package/dist/app/workspace_monitor.js +216 -0
- package/dist/app/workspace_presets.d.ts +29 -0
- package/dist/app/workspace_presets.js +192 -0
- package/dist/attention/queue.d.ts +37 -0
- package/dist/attention/queue.js +183 -0
- package/dist/bench/rf_shared.d.ts +2 -0
- package/dist/board/binding.d.ts +42 -0
- package/dist/board/binding.js +38 -0
- package/dist/board/client.d.ts +42 -0
- package/dist/board/client.js +60 -0
- package/dist/cli.js +1 -1
- package/dist/cli_args.js +19 -17
- package/dist/cli_main.js +110 -13
- package/dist/cli_ratatui.d.ts +183 -16
- package/dist/cli_ratatui.js +3989 -1255
- package/dist/components/ActivityLine.d.ts +3 -2
- package/dist/components/ActivityLine.js +2 -2
- package/dist/components/InputArea.d.ts +19 -1
- package/dist/components/InputArea.js +264 -190
- package/dist/components/MessageList.d.ts +2 -0
- package/dist/components/MessageList.js +3 -1
- package/dist/components/SingleMessage.d.ts +2 -0
- package/dist/components/SingleMessage.js +4 -2
- package/dist/components/StatusBar.d.ts +4 -2
- package/dist/components/StatusBar.js +3 -1
- package/dist/conductor_branch.js +3 -0
- package/dist/config/api_retry.d.ts +6 -1
- package/dist/config/api_retry.js +19 -1
- package/dist/config/defaults.d.ts +2 -1
- package/dist/config/defaults.js +9 -2
- package/dist/config/message_display.d.ts +1 -0
- package/dist/config/message_display.js +17 -2
- package/dist/config/permissions.d.ts +21 -0
- package/dist/config/permissions.js +89 -0
- package/dist/config/resolved.d.ts +2 -2
- package/dist/config/types.d.ts +32 -5
- package/dist/config.d.ts +38 -4
- package/dist/config.js +290 -42
- package/dist/context/project_intelligence.d.ts +44 -0
- package/dist/context/project_intelligence.js +246 -0
- package/dist/desktop/cli_launch.d.ts +8 -0
- package/dist/desktop/cli_launch.js +6 -0
- package/dist/desktop/cli_shim.d.ts +8 -0
- package/dist/desktop/cli_shim.js +120 -0
- package/dist/desktop/runtime.d.ts +16 -0
- package/dist/desktop/runtime.js +161 -0
- package/dist/desktop/worker_launch.d.ts +7 -0
- package/dist/desktop/worker_launch.js +7 -0
- package/dist/diagnostics/cargo_mutants_selection.d.ts +27 -0
- package/dist/diagnostics/cargo_mutants_selection.js +48 -0
- package/dist/diagnostics/native_terminal_benchmark.d.ts +58 -0
- package/dist/diagnostics/native_terminal_benchmark.js +1247 -0
- package/dist/diagnostics/session_evidence_package.d.ts +40 -0
- package/dist/diagnostics/session_evidence_package.js +234 -0
- package/dist/diagnostics/session_export.d.ts +93 -0
- package/dist/diagnostics/session_export.js +634 -0
- package/dist/emulation/tool_adapters.js +48 -18
- package/dist/emulation/ztc_addendum.d.ts +1 -0
- package/dist/emulation/ztc_addendum.js +13 -12
- package/dist/environment/service_runner.d.ts +1 -0
- package/dist/environment/service_runner.js +68 -0
- package/dist/environment/store.d.ts +120 -0
- package/dist/environment/store.js +387 -0
- package/dist/events.d.ts +40 -1
- package/dist/events.js +10 -17
- package/dist/goal_contract.d.ts +25 -0
- package/dist/goal_contract.js +44 -0
- package/dist/headless.d.ts +31 -0
- package/dist/headless.js +578 -70
- package/dist/headless_cli.js +8 -0
- package/dist/headless_cli_args.d.ts +1 -0
- package/dist/headless_cli_args.js +1 -0
- package/dist/intake/cli.d.ts +1 -0
- package/dist/intake/cli.js +33 -0
- package/dist/intake/format.d.ts +2 -0
- package/dist/intake/format.js +14 -0
- package/dist/intake/notifications.d.ts +16 -0
- package/dist/intake/notifications.js +132 -0
- package/dist/intake/store.d.ts +56 -0
- package/dist/intake/store.js +238 -0
- package/dist/library.d.ts +8 -0
- package/dist/library.js +19 -10
- package/dist/mcp/client.d.ts +1 -0
- package/dist/mcp/client.js +20 -0
- package/dist/modes/builtin/plan.yaml +2 -2
- package/dist/modes/handlers.js +1 -1
- package/dist/onboarding/first_run.d.ts +3 -0
- package/dist/onboarding/first_run.js +33 -25
- package/dist/onboarding/setup.d.ts +32 -0
- package/dist/onboarding/setup.js +133 -0
- package/dist/onboarding/setup_agent_health.d.ts +17 -0
- package/dist/onboarding/setup_agent_health.js +79 -0
- package/dist/onboarding/setup_environment.d.ts +66 -0
- package/dist/onboarding/setup_environment.js +219 -0
- package/dist/onboarding/setup_workspace.d.ts +17 -0
- package/dist/onboarding/setup_workspace.js +129 -0
- package/dist/optimize/adapters/index.d.ts +4 -1
- package/dist/optimize/adapters/index.js +7 -0
- package/dist/optimize/adapters/kernelbench.js +14 -5
- package/dist/optimize/adapters/popcorn.js +3 -2
- package/dist/optimize/adapters/profile_benchmark.js +13 -7
- package/dist/optimize/adapters/rfbench.js +25 -14
- package/dist/optimize/adapters/types.d.ts +3 -0
- package/dist/optimize/adapters/zergpod.js +5 -2
- package/dist/optimize/baseline.js +10 -21
- package/dist/optimize/branch.js +9 -12
- package/dist/optimize/combine.js +2 -3
- package/dist/optimize/executor.js +34 -48
- package/dist/optimize/measurements.d.ts +55 -0
- package/dist/optimize/measurements.js +173 -0
- package/dist/optimize/results.d.ts +1 -0
- package/dist/optimize/results.js +11 -33
- package/dist/optimize/tge.js +2 -1
- package/dist/panes/initial_input.d.ts +19 -0
- package/dist/panes/initial_input.js +206 -0
- package/dist/panes/labels.d.ts +16 -0
- package/dist/panes/labels.js +42 -0
- package/dist/panes/model_catalog.d.ts +3 -3
- package/dist/panes/model_catalog.js +28 -4
- package/dist/panes/pane_registry.js +6 -2
- package/dist/panes/types.d.ts +3 -0
- package/dist/panes/ztc_launch.js +19 -0
- package/dist/plugin/optimization.d.ts +2 -0
- package/dist/plugin/optimization.js +1 -0
- package/dist/plugin/runtime.d.ts +2 -0
- package/dist/plugin/simulation.d.ts +6 -1
- package/dist/plugin/simulation_contract.d.ts +836 -1
- package/dist/plugin/simulation_documents.d.ts +22 -0
- package/dist/plugin/simulation_documents.js +1 -0
- package/dist/plugin/simulation_interventions.d.ts +103 -0
- package/dist/plugin/simulation_interventions.js +1 -0
- package/dist/plugin/simulation_living.d.ts +1 -0
- package/dist/plugin/simulation_living.js +3 -0
- package/dist/plugin/simulation_meetings.d.ts +121 -0
- package/dist/plugin/simulation_meetings.js +1 -0
- package/dist/plugin/simulation_story_review.d.ts +111 -0
- package/dist/plugin/simulation_story_review.js +1 -0
- package/dist/plugin/simulation_storyteller.d.ts +91 -0
- package/dist/plugin/simulation_storyteller.js +1 -0
- package/dist/plugin/simulation_structured.d.ts +2 -0
- package/dist/plugin/simulation_structured.js +2 -0
- package/dist/plugin/simulation_toolkit.d.ts +21 -0
- package/dist/plugin/simulation_toolkit.js +24 -0
- package/dist/prompts/tool_delegate_task.j2 +1 -1
- package/dist/remote-broker/Dockerfile +10 -0
- package/dist/remote-broker/archive.py +1227 -0
- package/dist/remote-broker/cutover.py +761 -0
- package/dist/remote-broker/requirements.txt +5 -0
- package/dist/remote-broker/resources.json +6 -0
- package/dist/remote-broker/resources.write-staging.example.json +18 -0
- package/dist/remote-broker/retained.py +686 -0
- package/dist/remote-broker/retained_cleanup.py +550 -0
- package/dist/remote-broker/server.py +648 -0
- package/dist/remote-broker/worker.py +1007 -0
- package/dist/router_auth.d.ts +31 -0
- package/dist/router_auth.js +155 -9
- package/dist/runbooks/catalog.d.ts +3 -0
- package/dist/runbooks/catalog.js +26 -0
- package/dist/runbooks/library.d.ts +11 -0
- package/dist/runbooks/library.js +95 -0
- package/dist/runbooks/package.d.ts +43 -0
- package/dist/runbooks/package.js +102 -0
- package/dist/session.d.ts +65 -13
- package/dist/session.js +548 -43
- package/dist/session_handoff.d.ts +25 -0
- package/dist/session_handoff.js +322 -0
- package/dist/session_handoff_contract.d.ts +89 -0
- package/dist/session_handoff_contract.js +179 -0
- package/dist/session_media.d.ts +41 -0
- package/dist/session_media.js +357 -0
- package/dist/session_ownership.d.ts +16 -0
- package/dist/session_ownership.js +221 -0
- package/dist/session_public.d.ts +10 -2
- package/dist/session_public.js +6 -1
- package/dist/simulation/dynamic_history.d.ts +42 -0
- package/dist/simulation/dynamic_history.js +173 -0
- package/dist/simulation/engines/swarm/pane_manager.js +7 -6
- package/dist/simulation/inference.d.ts +3 -1
- package/dist/simulation/inference.js +19 -1
- package/dist/simulation/intervention_generation.d.ts +27 -0
- package/dist/simulation/intervention_generation.js +246 -0
- package/dist/simulation/intervention_material.d.ts +4 -0
- package/dist/simulation/intervention_material.js +81 -0
- package/dist/simulation/living/context.d.ts +3130 -0
- package/dist/simulation/living/context.js +944 -0
- package/dist/simulation/living/definer.d.ts +12 -0
- package/dist/simulation/living/definer.js +223 -0
- package/dist/simulation/living/definition.d.ts +257 -0
- package/dist/simulation/living/definition.js +341 -0
- package/dist/simulation/living/frame.d.ts +259 -0
- package/dist/simulation/living/frame.js +223 -0
- package/dist/simulation/living/identifiers.d.ts +2 -0
- package/dist/simulation/living/identifiers.js +12 -0
- package/dist/simulation/living/index.d.ts +15 -0
- package/dist/simulation/living/index.js +15 -0
- package/dist/simulation/living/inference.d.ts +219 -0
- package/dist/simulation/living/inference.js +576 -0
- package/dist/simulation/living/mechanics.d.ts +54 -0
- package/dist/simulation/living/mechanics.js +588 -0
- package/dist/simulation/living/model-input.d.ts +25 -0
- package/dist/simulation/living/model-input.js +215 -0
- package/dist/simulation/living/open-world.d.ts +257 -0
- package/dist/simulation/living/open-world.js +102 -0
- package/dist/simulation/living/projection.d.ts +7 -0
- package/dist/simulation/living/projection.js +31 -0
- package/dist/simulation/living/recall.d.ts +17 -0
- package/dist/simulation/living/recall.js +25 -0
- package/dist/simulation/living/reduce.d.ts +15 -0
- package/dist/simulation/living/reduce.js +1853 -0
- package/dist/simulation/living/replay.d.ts +6 -0
- package/dist/simulation/living/replay.js +22 -0
- package/dist/simulation/living/runtime.d.ts +140 -0
- package/dist/simulation/living/runtime.js +3020 -0
- package/dist/simulation/living/shared-kinds.d.ts +24 -0
- package/dist/simulation/living/shared-kinds.js +96 -0
- package/dist/simulation/living/types.d.ts +2785 -0
- package/dist/simulation/living/types.js +669 -0
- package/dist/simulation/llm.d.ts +3 -1
- package/dist/simulation/llm.js +90 -30
- package/dist/simulation/memory/embedding.d.ts +1 -1
- package/dist/simulation/memory/embedding.js +9 -3
- package/dist/simulation/memory/filesystem.d.ts +2 -0
- package/dist/simulation/memory/filesystem.js +5 -1
- package/dist/simulation/memory/filesystem_file_tree.d.ts +4 -1
- package/dist/simulation/memory/filesystem_file_tree.js +41 -10
- package/dist/simulation/memory/filesystem_invariants.js +1 -2
- package/dist/simulation/memory/filesystem_types.d.ts +1 -0
- package/dist/simulation/memory/memory.d.ts +2 -0
- package/dist/simulation/memory/memory.js +5 -1
- package/dist/simulation/orchestration/intent.js +0 -2
- package/dist/simulation/orchestration/lifecycle.js +0 -2
- package/dist/simulation/plugin_host.d.ts +25 -1
- package/dist/simulation/plugin_host.js +334 -17
- package/dist/simulation/session_documents.d.ts +20 -0
- package/dist/simulation/session_documents.js +62 -0
- package/dist/simulation/session_input_validation.d.ts +43 -0
- package/dist/simulation/session_input_validation.js +364 -0
- package/dist/simulation/session_inputs.d.ts +32 -0
- package/dist/simulation/session_inputs.js +154 -0
- package/dist/simulation/telemetry.d.ts +11 -14
- package/dist/simulation/telemetry.js +177 -54
- package/dist/simulation/toolkit/bounded_turns.d.ts +13 -0
- package/dist/simulation/toolkit/bounded_turns.js +80 -0
- package/dist/simulation/toolkit/dynamic_agents.d.ts +25 -0
- package/dist/simulation/toolkit/dynamic_agents.js +218 -0
- package/dist/simulation/toolkit/dynamic_digest.d.ts +1 -0
- package/dist/simulation/toolkit/dynamic_digest.js +8 -0
- package/dist/simulation/toolkit/dynamic_documents.d.ts +7 -0
- package/dist/simulation/toolkit/dynamic_documents.js +71 -0
- package/dist/simulation/toolkit/dynamic_driver.d.ts +8 -0
- package/dist/simulation/toolkit/dynamic_driver.js +269 -0
- package/dist/simulation/toolkit/dynamic_game.d.ts +53 -0
- package/dist/simulation/toolkit/dynamic_game.js +805 -0
- package/dist/simulation/toolkit/dynamic_knowledge.d.ts +15 -0
- package/dist/simulation/toolkit/dynamic_knowledge.js +91 -0
- package/dist/simulation/toolkit/dynamic_mechanics.d.ts +22 -0
- package/dist/simulation/toolkit/dynamic_mechanics.js +910 -0
- package/dist/simulation/toolkit/dynamic_meetings.d.ts +239 -0
- package/dist/simulation/toolkit/dynamic_meetings.js +350 -0
- package/dist/simulation/toolkit/dynamic_progress.d.ts +131 -0
- package/dist/simulation/toolkit/dynamic_progress.js +214 -0
- package/dist/simulation/toolkit/dynamic_projection.d.ts +9 -0
- package/dist/simulation/toolkit/dynamic_projection.js +219 -0
- package/dist/simulation/toolkit/dynamic_runtime.d.ts +412 -0
- package/dist/simulation/toolkit/dynamic_runtime.js +6536 -0
- package/dist/simulation/toolkit/dynamic_state.d.ts +111 -0
- package/dist/simulation/toolkit/dynamic_state.js +1967 -0
- package/dist/simulation/toolkit/dynamic_story_review.d.ts +508 -0
- package/dist/simulation/toolkit/dynamic_story_review.js +333 -0
- package/dist/simulation/toolkit/dynamic_storyteller.d.ts +77 -0
- package/dist/simulation/toolkit/dynamic_storyteller.js +114 -0
- package/dist/simulation/toolkit/dynamic_storyteller_runtime.d.ts +26 -0
- package/dist/simulation/toolkit/dynamic_storyteller_runtime.js +126 -0
- package/dist/simulation/toolkit/dynamic_world.d.ts +22 -0
- package/dist/simulation/toolkit/dynamic_world.js +377 -0
- package/dist/simulation/toolkit/structured_output.d.ts +5 -0
- package/dist/simulation/toolkit/structured_output.js +321 -0
- package/dist/simulation/types/world.d.ts +2 -0
- package/dist/simulation/world/artifact_store.js +1 -1
- package/dist/simulation/world/memory_manager.d.ts +5 -0
- package/dist/simulation/world/memory_manager.js +17 -2
- package/dist/simulation/world/state.d.ts +3 -2
- package/dist/simulation/world/state.js +15 -5
- package/dist/skills/bundled/measurement-validation/SKILL.md +144 -0
- package/dist/terminal_attach.d.ts +1 -0
- package/dist/terminal_attach.js +113 -0
- package/dist/tool_progress.d.ts +3 -0
- package/dist/tool_progress.js +27 -0
- package/dist/types.d.ts +145 -10
- package/dist/types.js +3 -0
- package/dist/ui/core/input.d.ts +3 -0
- package/dist/ui/core/input_segments.d.ts +18 -1
- package/dist/ui/core/input_segments.js +131 -47
- package/dist/ui/core/input_state.d.ts +12 -0
- package/dist/ui/core/layout_yoga.d.ts +1 -1
- package/dist/ui/core/turn_metadata.d.ts +5 -0
- package/dist/ui/core/turn_metadata.js +22 -0
- package/dist/ui/ink/render.d.ts +2 -1
- package/dist/ui/ink/render.js +6 -5
- package/dist/ui/ratatui/headless_pane_manager.d.ts +14 -1
- package/dist/ui/ratatui/headless_pane_manager.js +293 -104
- package/dist/ui/ratatui/index.d.ts +1 -0
- package/dist/ui/ratatui/input.d.ts +4 -0
- package/dist/ui/ratatui/input.js +15 -0
- package/dist/ui/ratatui/ipc_path.d.ts +2 -0
- package/dist/ui/ratatui/ipc_path.js +29 -1
- package/dist/ui/ratatui/mux_persistence.d.ts +3 -1
- package/dist/ui/ratatui/mux_persistence.js +139 -1
- package/dist/ui/ratatui/pane_relay.d.ts +1 -1
- package/dist/ui/ratatui/pane_relay.js +55 -27
- package/dist/ui/ratatui/process.d.ts +17 -1
- package/dist/ui/ratatui/process.js +80 -7
- package/dist/ui/ratatui/protocol.d.ts +236 -1
- package/dist/ui/ratatui/protocol.js +90 -0
- package/dist/ui/scrollback.d.ts +2 -1
- package/dist/ui/scrollback.js +9 -4
- package/dist/ui/stream_typewriter.d.ts +8 -3
- package/dist/ui/stream_typewriter.js +30 -1
- package/dist/ui/streaming_scrollback.d.ts +1 -0
- package/dist/ui/streaming_scrollback.js +1 -1
- package/dist/ui/transcript_render_cache.js +10 -0
- package/dist/ui/views/activity_line.d.ts +5 -3
- package/dist/ui/views/activity_line.js +65 -7
- package/dist/ui/views/app.d.ts +6 -3
- package/dist/ui/views/app.js +17 -6
- package/dist/ui/views/completion_menu.js +10 -5
- package/dist/ui/views/input_area.d.ts +7 -3
- package/dist/ui/views/input_area.js +15 -9
- package/dist/ui/views/message_list.d.ts +4 -1
- package/dist/ui/views/message_list.js +89 -19
- package/dist/ui/views/queue_panel.d.ts +3 -1
- package/dist/ui/views/queue_panel.js +9 -2
- package/dist/ui/views/status_bar.d.ts +4 -2
- package/dist/ui/views/status_bar.js +28 -13
- package/dist/ui/views/theme.d.ts +2 -0
- package/dist/ui/views/theme.js +2 -0
- package/dist/ui/widgets/color_panel.js +18 -0
- package/dist/ui/widgets/form.js +8 -0
- package/dist/ui/widgets/log_view.js +7 -0
- package/dist/ui/widgets/navigator_panel.js +8 -0
- package/dist/ui/widgets/prompt_viewer.js +2 -0
- package/dist/ui/widgets/questionnaire.d.ts +2 -0
- package/dist/ui/widgets/questionnaire.js +79 -16
- package/dist/ui/widgets/resume_panel.d.ts +2 -0
- package/dist/ui/widgets/resume_panel.js +26 -15
- package/dist/ui/widgets/rewind_panel.d.ts +2 -2
- package/dist/ui/widgets/rewind_panel.js +4 -2
- package/dist/ui/widgets/select.js +16 -27
- package/dist/ui/widgets/settings_panel.js +16 -0
- package/dist/ui/widgets/status_line_panel.d.ts +1 -0
- package/dist/ui/widgets/status_line_panel.js +30 -4
- package/dist/ui/widgets/text_input.d.ts +2 -0
- package/dist/ui/widgets/text_input.js +15 -1
- package/dist/ui/widgets/types.d.ts +2 -0
- package/dist/usage_projection.d.ts +24 -0
- package/dist/usage_projection.js +130 -0
- package/dist/usage_snapshot.d.ts +36 -0
- package/dist/usage_snapshot.js +131 -0
- package/dist/utils/assistant_message.d.ts +2 -0
- package/dist/utils/assistant_message.js +5 -0
- package/dist/utils/atomic_write.d.ts +6 -2
- package/dist/utils/atomic_write.js +29 -4
- package/dist/utils/checked_shell.d.ts +25 -0
- package/dist/utils/checked_shell.js +107 -0
- package/dist/utils/compaction.d.ts +4 -1
- package/dist/utils/compaction.js +31 -7
- package/dist/utils/context.d.ts +4 -4
- package/dist/utils/context.js +14 -12
- package/dist/utils/deepseek.d.ts +2 -2
- package/dist/utils/deepseek.js +13 -5
- package/dist/utils/ephemeral.js +4 -0
- package/dist/utils/ephemeral_tools.d.ts +18 -0
- package/dist/utils/ephemeral_tools.js +97 -0
- package/dist/utils/jev_client.d.ts +4 -0
- package/dist/utils/jev_client.js +15 -0
- package/dist/utils/model_cache.d.ts +15 -0
- package/dist/utils/model_cache.js +143 -0
- package/dist/utils/models.d.ts +4 -2
- package/dist/utils/models.js +12 -16
- package/dist/utils/reasoning.d.ts +8 -1
- package/dist/utils/reasoning.js +31 -11
- package/dist/utils/router_catalog.d.ts +49 -0
- package/dist/utils/router_catalog.js +271 -0
- package/dist/utils/sandbox.d.ts +9 -0
- package/dist/utils/sandbox.js +118 -15
- package/dist/utils/shell.d.ts +7 -0
- package/dist/utils/shell.js +98 -34
- package/dist/utils/subprocess_env.d.ts +3 -0
- package/dist/utils/subprocess_env.js +27 -1
- package/dist/web/accounts_mirror.d.ts +3 -1
- package/dist/web/accounts_mirror.js +13 -9
- package/dist/web/child_publication.d.ts +17 -0
- package/dist/web/child_publication.js +45 -0
- package/dist/web/mirror_authenticator.d.ts +31 -11
- package/dist/web/mirror_authenticator.js +67 -17
- package/dist/web/mirror_client.d.ts +31 -4
- package/dist/web/mirror_client.js +189 -23
- package/dist/web/mirror_stream.d.ts +84 -22
- package/dist/web/mirror_stream.js +676 -95
- package/dist/web/observer_query_redaction.d.ts +6 -0
- package/dist/web/observer_query_redaction.js +58 -0
- package/dist/web/pane_registry.d.ts +2 -0
- package/dist/web/pane_registry.js +86 -22
- package/dist/web/tool_image.d.ts +6 -0
- package/dist/web/tool_image.js +17 -0
- package/dist/web/view_plugin.d.ts +14 -0
- package/dist/web/view_plugin.js +23 -1
- package/dist/worker/executor.js +31 -12
- package/dist/worker/index.d.ts +1 -0
- package/dist/worker/toolsets.js +3 -1
- package/dist/worker/types.d.ts +12 -1
- package/dist/workflows/registry.js +1 -0
- package/dist/workflows/registry_handle.d.ts +2 -0
- package/dist/workflows/runtime.d.ts +2 -0
- package/dist/workflows/runtime.js +1 -0
- package/dist/workspace_agent.d.ts +7 -0
- package/dist/workspace_agent.js +36 -0
- package/dist/workspace_collision.d.ts +76 -0
- package/dist/workspace_collision.js +688 -0
- package/dist/workspace_layout.d.ts +18 -0
- package/dist/workspace_layout.js +121 -0
- package/dist/workspace_runtime.d.ts +44 -0
- package/dist/workspace_runtime.js +82 -0
- package/dist/workspace_session_context.d.ts +62 -0
- package/dist/workspace_session_context.js +160 -0
- package/dist/zlm-embed.mjs +32518 -9615
- package/dist/zlm.js +1 -0
- package/node_modules/@zerg/model-contracts/LICENSE.md +375 -0
- package/node_modules/@zerg/model-contracts/README.md +90 -0
- package/node_modules/@zerg/model-contracts/index.d.ts +20 -0
- package/node_modules/@zerg/model-contracts/index.js +223 -0
- package/node_modules/@zerg/model-contracts/package.json +8 -0
- package/node_modules/glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/glob/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
- package/package.json +45 -24
- package/packages/model-contracts/LICENSE.md +375 -0
- package/packages/model-contracts/README.md +90 -0
- package/packages/model-contracts/index.d.ts +20 -0
- package/packages/model-contracts/index.js +223 -0
- package/packages/model-contracts/package.json +8 -0
- package/remote-broker/Dockerfile +10 -0
- package/remote-broker/README.md +308 -0
- package/remote-broker/archive.py +1227 -0
- package/remote-broker/cutover.py +761 -0
- package/remote-broker/requirements.txt +5 -0
- package/remote-broker/resources.example.json +6 -0
- package/remote-broker/resources.write-staging.example.json +18 -0
- package/remote-broker/retained.py +686 -0
- package/remote-broker/retained_cleanup.py +550 -0
- package/remote-broker/server.py +648 -0
- package/scripts/build-remote-broker.mjs +13 -0
- package/scripts/ensure-local-packages.mjs +6 -0
- package/scripts/node-pty-helpers.mjs +103 -0
package/dist/App.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PromptTurn } from './app/prompt_turn.js';
|
|
2
3
|
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
|
3
4
|
import { Box, Text, useApp, useInput, Static } from 'ink';
|
|
4
5
|
import { Header, MessageList, SingleMessage, InputArea, StatusBar, FullScreen, ActivityLine, useScreenSize, FormWidget, QueuePanel } from './components/index.js';
|
|
5
6
|
import { AgentError } from './agent/index.js';
|
|
6
7
|
import { commands } from './agent/commands/index.js';
|
|
8
|
+
import { goalManager } from './agent/commands/goal.js';
|
|
9
|
+
import { listCommandJobs, subscribeCommandJobs } from './agent/runtime/command_jobs.js';
|
|
10
|
+
import { normalizeInputMode } from './types.js';
|
|
7
11
|
import { DEFAULT_TOAST_MS, MAX_QUEUE } from './app/controller.js';
|
|
8
12
|
import { getQueuePreview } from './ui/views/queue_panel.js';
|
|
9
13
|
import { estimateInputLines } from './ui/views/input_area.js';
|
|
10
14
|
import { configStore } from './config.js';
|
|
15
|
+
import { applyAccountOp } from './web/accounts_mirror.js';
|
|
16
|
+
import { createUsageSnapshot, mergeUsageSnapshots, recordUsageReceipt } from './usage_snapshot.js';
|
|
11
17
|
import { resolveMessageDisplaySettings } from './config/message_display.js';
|
|
18
|
+
import { initialStreamTypewriterState, planStreamTypewriterSample, STREAM_TYPEWRITER_DEFAULTS } from './ui/stream_typewriter.js';
|
|
12
19
|
import { createInputBus, isControlKey } from './ui/core/input.js';
|
|
13
20
|
import { loadEmulationProfiles, getEmulationProfile } from './emulation/catalog.js';
|
|
14
21
|
import { getTraceStyle } from './emulation/trace_style.js';
|
|
@@ -21,6 +28,20 @@ import { formatToolStart, formatToolError, buildToolOutputMessage } from './util
|
|
|
21
28
|
import { autoActivateSkills, buildSkillPrompt } from './skills/index.js';
|
|
22
29
|
import { getSkills } from './skills/registry.js';
|
|
23
30
|
import { createAgentFromConfig } from './agent/factory.js';
|
|
31
|
+
import { hasWorkspaceCollisionRisk, LocalWorkspaceCoordinator, inspectWorkspaceSessions, prepareWorkspaceIsolation, WorkspaceIsolationError } from './workspace_collision.js';
|
|
32
|
+
import { generateSessionId, saveSession } from './session.js';
|
|
33
|
+
import { prepareSessionMessages } from './app/session_messages.js';
|
|
34
|
+
import { captureSessionDiagnosticSnapshot } from './diagnostics/session_export.js';
|
|
35
|
+
import { runtimePluginRegistry } from './plugin/runtime_registry.js';
|
|
36
|
+
import { classifyBusyInput } from './app/task_scope_router.js';
|
|
37
|
+
import { formatWorkspaceAdmissionDetails } from './app/workspace_admission_details.js';
|
|
38
|
+
import { reconcileOrderedTranscript } from './app/ordered_transcript.js';
|
|
39
|
+
import { advanceEscapeStop, isEscapeStopArmed, ESCAPE_STOP_WINDOW_MS } from './app/escape_stop.js';
|
|
40
|
+
import { SelectWidget } from './ui/widgets/select.js';
|
|
41
|
+
import { QuestionnaireWidget } from './ui/widgets/questionnaire.js';
|
|
42
|
+
import { setAskUserCallback, clearAskUserCallback, resetAskUserTurnFlag } from './agent/tools/ask_user.js';
|
|
43
|
+
import { formatAskUserResponse } from './app/ask_user_response.js';
|
|
44
|
+
import { InkNode } from './ui/ink/render.js';
|
|
24
45
|
import { getModeConfig, getNextMode, } from './app/modes.js';
|
|
25
46
|
import { getModeRegistry } from './modes/index.js';
|
|
26
47
|
import { isMeaningfulUserInput } from './app/input_validation.js';
|
|
@@ -35,10 +56,11 @@ import { isNativeDictationAvailable, onDictationEvent } from './utils/dictation_
|
|
|
35
56
|
import { getContextStatus, contextStatusFromUsed } from './utils/context.js';
|
|
36
57
|
import { compactMessages, localSummarizer } from './utils/compaction.js';
|
|
37
58
|
import { speak, optionsFromConfig, isProviderAvailable } from './utils/tts.js';
|
|
38
|
-
import { applyAgentEventToState, buildActivityDisplayState, getAgentActivityPlaceholder } from './app/activity.js';
|
|
59
|
+
import { applyAgentEventToState, buildActivityDisplayState, formatApiRetryNotice, getAgentActivityPlaceholder } from './app/activity.js';
|
|
39
60
|
import { buildCredentialSetupMessage, buildNoCredentialsWelcome, buildReadyWelcome, createFirstRunLaunchMessage, isFirstRunWelcomeMessage, } from './onboarding/first_run.js';
|
|
40
61
|
// --- Utilities ---
|
|
41
62
|
const generateId = () => Math.random().toString(36).slice(2, 11);
|
|
63
|
+
const ESCAPE_CONFIRMATION_HINT = 'Esc again to stop';
|
|
42
64
|
/**
|
|
43
65
|
* Render a VU meter from dB level
|
|
44
66
|
* @param db Audio level in dB (-60 to 0)
|
|
@@ -116,7 +138,7 @@ function getWelcomeMessage(runtime) {
|
|
|
116
138
|
};
|
|
117
139
|
}
|
|
118
140
|
// --- Create agent if possible ---
|
|
119
|
-
function createAgent(mode) {
|
|
141
|
+
function createAgent(mode, skillPrompt, workspace) {
|
|
120
142
|
// Can create agent if we have an API key OR are authenticated
|
|
121
143
|
const hasKey = configStore.hasApiKey();
|
|
122
144
|
const isAuthenticated = configStore.isAuthenticated();
|
|
@@ -132,7 +154,9 @@ function createAgent(mode) {
|
|
|
132
154
|
openaiCompatibleBaseUrl: configStore.getOpenAICompatibleBaseUrl(),
|
|
133
155
|
emulationId: configStore.getEmulationId(),
|
|
134
156
|
mode,
|
|
157
|
+
skillPrompt,
|
|
135
158
|
allowSelfPlanTransition: false,
|
|
159
|
+
...workspace,
|
|
136
160
|
});
|
|
137
161
|
}
|
|
138
162
|
// --- Main App ---
|
|
@@ -146,30 +170,111 @@ export const App = () => {
|
|
|
146
170
|
const messageDisplay = resolveMessageDisplaySettings(configStore.get());
|
|
147
171
|
const shellCwdRef = React.useRef(process.cwd());
|
|
148
172
|
// State
|
|
149
|
-
const [messages,
|
|
173
|
+
const [messages, updateMessages] = useState([getWelcomeMessage()]);
|
|
150
174
|
const messagesRef = useRef(messages);
|
|
151
|
-
const
|
|
175
|
+
const readingStatesRef = useRef(new Map());
|
|
176
|
+
const bypassedReadingIdsRef = useRef(new Set());
|
|
177
|
+
const [readingTick, refreshReadingDisplay] = useState(0);
|
|
178
|
+
const [, refreshActivityClock] = useState(0);
|
|
179
|
+
const flushReadingDisplay = useCallback(() => {
|
|
180
|
+
for (const id of readingStatesRef.current.keys())
|
|
181
|
+
bypassedReadingIdsRef.current.add(id);
|
|
182
|
+
readingStatesRef.current.clear();
|
|
183
|
+
refreshReadingDisplay(tick => tick + 1);
|
|
184
|
+
}, []);
|
|
185
|
+
const setMessages = useCallback((next) => {
|
|
186
|
+
const value = typeof next === 'function' ? next(messagesRef.current) : next;
|
|
187
|
+
messagesRef.current = value;
|
|
188
|
+
updateMessages(value);
|
|
189
|
+
}, []);
|
|
190
|
+
const [agentState, updateAgentState] = useState({ status: 'idle' });
|
|
191
|
+
const setAgentState = useCallback((next) => {
|
|
192
|
+
updateAgentState(previous => {
|
|
193
|
+
const value = typeof next === 'function' ? next(previous) : next;
|
|
194
|
+
return { ...value, evidence: value.evidence ?? previous.evidence,
|
|
195
|
+
workspaceCollision: value.workspaceCollision ?? previous.workspaceCollision };
|
|
196
|
+
});
|
|
197
|
+
}, []);
|
|
152
198
|
// Latest agentState for closures that must not capture a stale snapshot (the
|
|
153
199
|
// command context getters below read the real per-call context count live).
|
|
154
200
|
const agentStateRef = useRef(agentState);
|
|
155
201
|
agentStateRef.current = agentState;
|
|
156
|
-
const [sessionId] = useState(
|
|
202
|
+
const [sessionId, setSessionId] = useState(generateSessionId);
|
|
203
|
+
const sessionIdRef = useRef(sessionId);
|
|
204
|
+
sessionIdRef.current = sessionId;
|
|
205
|
+
const readingSessionRef = useRef(sessionId);
|
|
206
|
+
const [sessionName, setSessionName] = useState();
|
|
157
207
|
const [agentMode, setAgentMode] = useState('normal');
|
|
158
|
-
const [agent, setAgent] = useState(
|
|
208
|
+
const [agent, setAgent] = useState(null);
|
|
209
|
+
const diagnosticAgentRef = useRef(null);
|
|
210
|
+
diagnosticAgentRef.current = agent;
|
|
211
|
+
const retainedUsageRef = useRef({ sessionId, snapshot: createUsageSnapshot() });
|
|
212
|
+
const [routingUsageRevision, setRoutingUsageRevision] = useState(0);
|
|
213
|
+
const getUsageSnapshot = useCallback(() => {
|
|
214
|
+
if (retainedUsageRef.current.sessionId !== sessionId)
|
|
215
|
+
retainedUsageRef.current = { sessionId, snapshot: createUsageSnapshot() };
|
|
216
|
+
const current = diagnosticAgentRef.current;
|
|
217
|
+
if (current?.getUsageSessionId() === sessionId)
|
|
218
|
+
retainedUsageRef.current.snapshot = mergeUsageSnapshots(retainedUsageRef.current.snapshot, current.getUsageSnapshot());
|
|
219
|
+
return structuredClone(retainedUsageRef.current.snapshot);
|
|
220
|
+
}, [sessionId]);
|
|
221
|
+
const usageSaveQueueRef = useRef(Promise.resolve());
|
|
222
|
+
const lastUsageSaveRef = useRef('');
|
|
223
|
+
const retiredAgentsRef = useRef(new WeakSet());
|
|
159
224
|
const [expandToolOutputs, setExpandToolOutputs] = useState(false);
|
|
160
225
|
const [skills, setSkills] = useState([]);
|
|
161
|
-
const [inputMode, setInputMode] = useState('
|
|
226
|
+
const [inputMode, setInputMode] = useState('steer');
|
|
162
227
|
const [queue, setQueue] = useState([]);
|
|
163
228
|
const queueRef = useRef(queue);
|
|
164
|
-
|
|
229
|
+
const queuePausedRef = useRef(false);
|
|
165
230
|
const flushingRef = useRef(false);
|
|
231
|
+
const [flushing, setFlushing] = useState(false);
|
|
166
232
|
const prevStatusRef = useRef('idle');
|
|
167
233
|
const activeRunIdRef = useRef(null);
|
|
168
234
|
const runCounterRef = useRef(0);
|
|
169
235
|
const abortControllerRef = useRef(null);
|
|
236
|
+
const runtimeMutationRef = useRef(false);
|
|
237
|
+
const accountReloadPendingRef = useRef(false);
|
|
238
|
+
const [escapeStopArm, setEscapeStopArm] = useState(null);
|
|
239
|
+
const escapeStopArmRef = useRef(null);
|
|
240
|
+
const escapeStopTimerRef = useRef(null);
|
|
170
241
|
const lastRunDurationRef = useRef(null);
|
|
171
242
|
const pendingPromptRef = useRef(null);
|
|
172
243
|
const formCallbacksRef = useRef(new Map());
|
|
244
|
+
const questionnaireWidgetRef = useRef(null);
|
|
245
|
+
const questionnaireInputReadyRef = useRef(false);
|
|
246
|
+
const questionnaireEventOwnedRef = useRef(false);
|
|
247
|
+
const agentQuestionRef = useRef(null);
|
|
248
|
+
const [questionnaireVersion, renderQuestionnaireWidget] = useState(0);
|
|
249
|
+
const scopeWidgetRef = useRef(null);
|
|
250
|
+
const [, renderScopeWidget] = useState(0);
|
|
251
|
+
const routingEpochRef = useRef(0);
|
|
252
|
+
const routingTailRef = useRef(Promise.resolve());
|
|
253
|
+
const routingControllersRef = useRef(new Map());
|
|
254
|
+
const routingDraftsRef = useRef(new Map());
|
|
255
|
+
const routingChoiceCancelRef = useRef(null);
|
|
256
|
+
const workspaceWidgetRef = useRef(null);
|
|
257
|
+
const [, renderWorkspaceWidget] = useState(0);
|
|
258
|
+
const workspaceChoiceCancelRef = useRef(null);
|
|
259
|
+
const workspaceAdmissionTailRef = useRef(Promise.resolve());
|
|
260
|
+
const workspacePausedRef = useRef(false);
|
|
261
|
+
const pausedWorkspaceInputRef = useRef(null);
|
|
262
|
+
const pausedWorkspaceOperationRef = useRef(null);
|
|
263
|
+
const isolatedWorkspaceResumeRef = useRef(null);
|
|
264
|
+
const workspaceIsolationRunningRef = useRef(false);
|
|
265
|
+
const [workspaceResumeVersion, setWorkspaceResumeVersion] = useState(0);
|
|
266
|
+
const workspaceSubmitRef = useRef(async () => { throw new Error('Workspace input is not initialized.'); });
|
|
267
|
+
const workspaceClosedRef = useRef(false);
|
|
268
|
+
const workspaceCoordinator = useMemo(() => new LocalWorkspaceCoordinator({
|
|
269
|
+
sessionId, getCwd: () => shellCwdRef.current, kind: 'interactive',
|
|
270
|
+
getWork: () => ({ sessionId: sessionIdRef.current, messages: messagesRef.current,
|
|
271
|
+
goal: goalManager.get(), jobs: listCommandJobs(sessionIdRef.current) }),
|
|
272
|
+
onSnapshot: snapshot => {
|
|
273
|
+
if (!workspaceClosedRef.current && snapshot.current.sessionId === sessionIdRef.current)
|
|
274
|
+
setAgentState(previous => ({ ...previous, workspaceCollision: snapshot }));
|
|
275
|
+
},
|
|
276
|
+
}), [sessionId, setAgentState]);
|
|
277
|
+
const workspaceReadyRef = useRef(null);
|
|
173
278
|
React.useEffect(() => {
|
|
174
279
|
let active = true;
|
|
175
280
|
let runtimeTimer;
|
|
@@ -184,7 +289,9 @@ export const App = () => {
|
|
|
184
289
|
return prev;
|
|
185
290
|
});
|
|
186
291
|
};
|
|
187
|
-
|
|
292
|
+
// Mount reuses settings already loaded by the host. A forced disk reload
|
|
293
|
+
// can overwrite a newer interactive selection while its save is pending.
|
|
294
|
+
configStore.load(false).then(() => {
|
|
188
295
|
if (!active)
|
|
189
296
|
return;
|
|
190
297
|
refreshUntouchedWelcome();
|
|
@@ -211,10 +318,27 @@ export const App = () => {
|
|
|
211
318
|
history: [],
|
|
212
319
|
historyIdx: -1
|
|
213
320
|
});
|
|
321
|
+
const [clearDraftVersion, setClearDraftVersion] = useState(0);
|
|
214
322
|
const lastRequestRef = useRef(null);
|
|
323
|
+
const promptTurnRef = useRef(null);
|
|
324
|
+
const [, setPromptClock] = useState(0);
|
|
215
325
|
const [retryAvailable, setRetryAvailable] = useState(false);
|
|
216
326
|
const [toast, setToast] = useState(null);
|
|
217
327
|
const toastTimerRef = useRef(null);
|
|
328
|
+
const resetEscapeStop = useCallback(() => {
|
|
329
|
+
if (escapeStopTimerRef.current)
|
|
330
|
+
clearTimeout(escapeStopTimerRef.current);
|
|
331
|
+
escapeStopTimerRef.current = null;
|
|
332
|
+
escapeStopArmRef.current = null;
|
|
333
|
+
setEscapeStopArm(null);
|
|
334
|
+
setToast(current => current === ESCAPE_CONFIRMATION_HINT ? null : current);
|
|
335
|
+
}, []);
|
|
336
|
+
React.useEffect(() => () => {
|
|
337
|
+
if (escapeStopTimerRef.current)
|
|
338
|
+
clearTimeout(escapeStopTimerRef.current);
|
|
339
|
+
if (toastTimerRef.current)
|
|
340
|
+
clearTimeout(toastTimerRef.current);
|
|
341
|
+
}, []);
|
|
218
342
|
const [isRecording, setIsRecording] = useState(false);
|
|
219
343
|
const [recordingDevice, setRecordingDevice] = useState(null);
|
|
220
344
|
const [isTranscribing, setIsTranscribing] = useState(false);
|
|
@@ -237,8 +361,8 @@ export const App = () => {
|
|
|
237
361
|
// the provider-reported window (Codex) as the denominator. Only before the
|
|
238
362
|
// first API response (no real count yet) do we fall back to the char/4 estimate.
|
|
239
363
|
const contextStatus = useMemo(() => (agentState.contextTokens && agentState.contextTokens > 0)
|
|
240
|
-
? contextStatusFromUsed(agentState.contextTokens, currentModel, agentState.contextWindow)
|
|
241
|
-
: getContextStatus(messages, currentModel), [agentState.contextTokens, agentState.contextWindow, messages, currentModel]);
|
|
364
|
+
? contextStatusFromUsed(agentState.contextTokens, currentModel, agentState.contextWindow ?? configStore.getModelContextWindow(currentModel))
|
|
365
|
+
: getContextStatus(messages, currentModel, configStore.getModelContextWindow(currentModel)), [agentState.contextTokens, agentState.contextWindow, messages, currentModel]);
|
|
242
366
|
const spinnerVerbs = configStore.get().spinnerVerbs || DEFAULT_SPINNER_VERBS;
|
|
243
367
|
const spinnerVerbsKey = spinnerVerbs.join('|');
|
|
244
368
|
const queuePreview = useMemo(() => getQueuePreview(queue), [queue]);
|
|
@@ -246,10 +370,8 @@ export const App = () => {
|
|
|
246
370
|
renderCount.current += 1;
|
|
247
371
|
debugLog(`App render #${renderCount.current} (messages=${messages.length}, state=${agentState.status})`);
|
|
248
372
|
});
|
|
249
|
-
React.useEffect(() => {
|
|
250
|
-
messagesRef.current = messages;
|
|
251
|
-
}, [messages]);
|
|
252
373
|
const [debug, setDebug] = useState(false);
|
|
374
|
+
const [transcriptGeneration, setTranscriptGeneration] = useState(0);
|
|
253
375
|
const scrollback = process.env.ZTC_SCROLLBACK === '1' || process.env.ZTC_ALT_SCREEN !== '1';
|
|
254
376
|
// Spinner label effect - picks a random verb when agent starts working
|
|
255
377
|
React.useEffect(() => {
|
|
@@ -274,7 +396,7 @@ export const App = () => {
|
|
|
274
396
|
return;
|
|
275
397
|
}
|
|
276
398
|
setSpinnerLabel(label);
|
|
277
|
-
}, [agentState.status, agentState.currentTool, spinnerVerbsKey]);
|
|
399
|
+
}, [agentState.status, agentState.currentTool, agentState.requestRetry, agentState.waiting, spinnerVerbsKey]);
|
|
278
400
|
// Spinner frame animation effect
|
|
279
401
|
React.useEffect(() => {
|
|
280
402
|
const isActive = agentState.status === 'thinking' || agentState.status === 'streaming' || agentState.status === 'tool_use';
|
|
@@ -299,6 +421,14 @@ export const App = () => {
|
|
|
299
421
|
clearInterval(interval);
|
|
300
422
|
};
|
|
301
423
|
}, [agentState.status, scrollback]);
|
|
424
|
+
// Scrollback keeps a static spinner, but elapsed time must advance even when
|
|
425
|
+
// the provider or tool emits nothing. Idle/unmounted apps own no clock timer.
|
|
426
|
+
React.useEffect(() => {
|
|
427
|
+
if (!scrollback || !['thinking', 'streaming', 'tool_use'].includes(agentState.status))
|
|
428
|
+
return;
|
|
429
|
+
const timer = setInterval(() => refreshActivityClock(tick => tick + 1), 1_000);
|
|
430
|
+
return () => clearInterval(timer);
|
|
431
|
+
}, [agentState.status, scrollback]);
|
|
302
432
|
const headerHeight = scrollback ? 0 : 4; // border (2) + content (1) + margin (1)
|
|
303
433
|
const statusHeight = 1;
|
|
304
434
|
const suggestionLines = 4;
|
|
@@ -323,10 +453,6 @@ export const App = () => {
|
|
|
323
453
|
}, [columns, estimateBadgePreviewLines, inputSnapshot, rows]);
|
|
324
454
|
// Calculate content height (total - header - input - status)
|
|
325
455
|
const contentHeight = useMemo(() => scrollback ? undefined : Math.max(rows - (headerHeight + inputHeight + statusHeight), 5), [rows, scrollback, inputHeight]);
|
|
326
|
-
// Reload agent when config changes
|
|
327
|
-
const reloadAgent = useCallback(() => {
|
|
328
|
-
setAgent(createAgent());
|
|
329
|
-
}, []);
|
|
330
456
|
React.useEffect(() => {
|
|
331
457
|
let active = true;
|
|
332
458
|
getSkills()
|
|
@@ -339,27 +465,66 @@ export const App = () => {
|
|
|
339
465
|
return () => { active = false; };
|
|
340
466
|
}, []);
|
|
341
467
|
// Keyboard shortcuts
|
|
468
|
+
const ownsLocalForm = formCallbacksRef.current.size > 0;
|
|
342
469
|
useInput((input, key) => {
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
470
|
+
if (!key.escape)
|
|
471
|
+
resetEscapeStop();
|
|
472
|
+
if (workspaceWidgetRef.current?.isActive()) {
|
|
473
|
+
if (key.escape)
|
|
474
|
+
resetEscapeStop();
|
|
475
|
+
workspaceWidgetRef.current.handleInput(input, key);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (questionnaireWidgetRef.current?.isActive()) {
|
|
479
|
+
// Every subscriber to this same key must respect its widget owner,
|
|
480
|
+
// even when answering closes the widget before the composer sees it.
|
|
481
|
+
questionnaireEventOwnedRef.current = true;
|
|
482
|
+
queueMicrotask(() => { questionnaireEventOwnedRef.current = false; });
|
|
483
|
+
resetEscapeStop();
|
|
484
|
+
if (questionnaireInputReadyRef.current)
|
|
485
|
+
questionnaireWidgetRef.current.handleInput(input, key);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (scopeWidgetRef.current?.isActive()) {
|
|
489
|
+
if (key.escape)
|
|
490
|
+
resetEscapeStop();
|
|
491
|
+
scopeWidgetRef.current.handleInput(input, key);
|
|
492
|
+
return;
|
|
346
493
|
}
|
|
347
494
|
if (key.escape) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (hasActiveRun) {
|
|
353
|
-
cancelGeneration();
|
|
495
|
+
if (ownsLocalForm) {
|
|
496
|
+
resetEscapeStop();
|
|
497
|
+
// The focused FormWidget consumes this same key locally.
|
|
498
|
+
return;
|
|
354
499
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
500
|
+
if (pendingPromptRef.current) {
|
|
501
|
+
resetEscapeStop();
|
|
502
|
+
retireRouting();
|
|
503
|
+
pendingPromptRef.current = null;
|
|
504
|
+
setClearDraftVersion(version => version + 1);
|
|
505
|
+
addMessage({ role: 'system', content: 'Cancelled.' });
|
|
506
|
+
return;
|
|
359
507
|
}
|
|
360
|
-
|
|
508
|
+
const next = advanceEscapeStop(escapeStopArmRef.current, abortControllerRef.current, Date.now());
|
|
509
|
+
resetEscapeStop();
|
|
510
|
+
if (next.confirmed) {
|
|
511
|
+
queuePausedRef.current = true;
|
|
361
512
|
cancelGeneration();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (next.arm) {
|
|
516
|
+
const arm = next.arm;
|
|
517
|
+
escapeStopArmRef.current = arm;
|
|
518
|
+
setEscapeStopArm(arm);
|
|
519
|
+
// The policy includes the exact 2000ms boundary.
|
|
520
|
+
escapeStopTimerRef.current = setTimeout(() => {
|
|
521
|
+
if (escapeStopArmRef.current !== arm)
|
|
522
|
+
return;
|
|
523
|
+
resetEscapeStop();
|
|
524
|
+
}, ESCAPE_STOP_WINDOW_MS + 1);
|
|
525
|
+
showToastRef.current?.(ESCAPE_CONFIRMATION_HINT, ESCAPE_STOP_WINDOW_MS);
|
|
362
526
|
}
|
|
527
|
+
return;
|
|
363
528
|
}
|
|
364
529
|
if (isControlKey(input, key, 'l')) {
|
|
365
530
|
setMessages([{
|
|
@@ -377,50 +542,542 @@ export const App = () => {
|
|
|
377
542
|
const addMessage = useCallback((msg) => {
|
|
378
543
|
setMessages(prev => [...prev, {
|
|
379
544
|
...msg,
|
|
545
|
+
...(msg.role === 'assistant' && !msg.turn && promptTurnRef.current ? { turn: promptTurnRef.current.snapshot() } : {}),
|
|
380
546
|
id: generateId(),
|
|
381
547
|
timestamp: new Date()
|
|
382
548
|
}]);
|
|
383
549
|
}, []);
|
|
384
550
|
const showToastRef = useRef(null);
|
|
551
|
+
const questionnaireWidget = useMemo(() => new QuestionnaireWidget({
|
|
552
|
+
render: () => renderQuestionnaireWidget(version => version + 1),
|
|
553
|
+
addSystemMessage: content => addMessage({ role: 'system', content }),
|
|
554
|
+
setToast: (content, durationMs) => {
|
|
555
|
+
if (content)
|
|
556
|
+
showToastRef.current?.(content, durationMs);
|
|
557
|
+
else
|
|
558
|
+
setToast(null);
|
|
559
|
+
},
|
|
560
|
+
writeClipboard,
|
|
561
|
+
}), [addMessage]);
|
|
562
|
+
questionnaireWidgetRef.current = questionnaireWidget;
|
|
563
|
+
React.useEffect(() => () => {
|
|
564
|
+
questionnaireInputReadyRef.current = false;
|
|
565
|
+
questionnaireWidget.cancel();
|
|
566
|
+
}, [questionnaireWidget]);
|
|
567
|
+
const retireAgentQuestion = useCallback(() => {
|
|
568
|
+
if (!agentQuestionRef.current)
|
|
569
|
+
return;
|
|
570
|
+
agentQuestionRef.current = null;
|
|
571
|
+
questionnaireInputReadyRef.current = false;
|
|
572
|
+
questionnaireWidget.cancel();
|
|
573
|
+
renderQuestionnaireWidget(version => version + 1);
|
|
574
|
+
}, [questionnaireWidget]);
|
|
575
|
+
React.useEffect(() => {
|
|
576
|
+
setAskUserCallback(questions => {
|
|
577
|
+
const runId = activeRunIdRef.current;
|
|
578
|
+
const owner = lastRequestRef.current?.agent;
|
|
579
|
+
if (!runId || !owner || workspaceClosedRef.current || !abortControllerRef.current
|
|
580
|
+
|| abortControllerRef.current.signal.aborted || retiredAgentsRef.current.has(owner))
|
|
581
|
+
return 'Question input has no active owning agent turn.';
|
|
582
|
+
if (pendingPromptRef.current || formCallbacksRef.current.size > 0 || questionnaireWidget.isActive())
|
|
583
|
+
return 'Another interactive input owns this session.';
|
|
584
|
+
const pending = agentQuestionRef.current;
|
|
585
|
+
if (pending && (pending.runId !== runId || pending.agent !== owner))
|
|
586
|
+
return 'Another agent turn owns the pending question.';
|
|
587
|
+
if (pending)
|
|
588
|
+
pending.questions.push(...structuredClone(questions));
|
|
589
|
+
else
|
|
590
|
+
agentQuestionRef.current = { runId, sessionId: sessionIdRef.current, agent: owner,
|
|
591
|
+
questions: structuredClone(questions), shown: false };
|
|
592
|
+
return null;
|
|
593
|
+
});
|
|
594
|
+
return () => {
|
|
595
|
+
clearAskUserCallback();
|
|
596
|
+
agentQuestionRef.current = null;
|
|
597
|
+
questionnaireInputReadyRef.current = false;
|
|
598
|
+
questionnaireWidget.cancel();
|
|
599
|
+
};
|
|
600
|
+
}, [questionnaireWidget]);
|
|
601
|
+
React.useEffect(() => { retireAgentQuestion(); }, [agent, sessionId, retireAgentQuestion]);
|
|
602
|
+
const presentAgentQuestion = useCallback((runId) => {
|
|
603
|
+
const owner = agentQuestionRef.current;
|
|
604
|
+
if (!owner || owner.runId !== runId || owner.shown)
|
|
605
|
+
return;
|
|
606
|
+
if (workspaceClosedRef.current || owner.sessionId !== sessionIdRef.current
|
|
607
|
+
|| owner.agent !== diagnosticAgentRef.current || retiredAgentsRef.current.has(owner.agent)) {
|
|
608
|
+
retireAgentQuestion();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
owner.shown = true;
|
|
612
|
+
questionnaireInputReadyRef.current = false;
|
|
613
|
+
questionnaireWidget.show({
|
|
614
|
+
title: owner.questions.length === 1 ? 'Question' : 'Questions',
|
|
615
|
+
questions: owner.questions.map((question, index) => ({ name: `q${index}`,
|
|
616
|
+
header: owner.questions.length > 1 ? `Q${index + 1}` : undefined,
|
|
617
|
+
question: question.question, type: 'select', required: true,
|
|
618
|
+
options: question.options.map(option => ({ value: option.label, ...option })),
|
|
619
|
+
})),
|
|
620
|
+
allowOther: owner.questions[0]?.allow_other ?? true,
|
|
621
|
+
allowOptionNotes: true,
|
|
622
|
+
callback: async (answers) => {
|
|
623
|
+
if (agentQuestionRef.current !== owner || workspaceClosedRef.current
|
|
624
|
+
|| owner.sessionId !== sessionIdRef.current || owner.agent !== diagnosticAgentRef.current
|
|
625
|
+
|| retiredAgentsRef.current.has(owner.agent))
|
|
626
|
+
return;
|
|
627
|
+
agentQuestionRef.current = null;
|
|
628
|
+
questionnaireInputReadyRef.current = false;
|
|
629
|
+
await workspaceSubmitRef.current(answers === null ? '__SKIP__' : formatAskUserResponse(owner.questions, answers), undefined, undefined, true);
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
queueMicrotask(() => { if (agentQuestionRef.current === owner)
|
|
633
|
+
questionnaireInputReadyRef.current = true; });
|
|
634
|
+
}, [questionnaireWidget, retireAgentQuestion]);
|
|
635
|
+
const scopeWidget = useMemo(() => new SelectWidget({
|
|
636
|
+
render: () => renderScopeWidget(version => version + 1),
|
|
637
|
+
addSystemMessage: content => addMessage({ role: 'system', content }),
|
|
638
|
+
setToast: content => { if (content)
|
|
639
|
+
showToastRef.current?.(content); },
|
|
640
|
+
writeClipboard,
|
|
641
|
+
}), [addMessage]);
|
|
642
|
+
scopeWidgetRef.current = scopeWidget;
|
|
643
|
+
const retireRouting = useCallback((restore = true) => {
|
|
644
|
+
resetEscapeStop();
|
|
645
|
+
routingEpochRef.current += 1;
|
|
646
|
+
for (const controller of routingControllersRef.current.values())
|
|
647
|
+
controller.abort();
|
|
648
|
+
routingControllersRef.current.clear();
|
|
649
|
+
if (restore)
|
|
650
|
+
for (const draft of routingDraftsRef.current.values())
|
|
651
|
+
draft?.restore();
|
|
652
|
+
routingDraftsRef.current.clear();
|
|
653
|
+
routingChoiceCancelRef.current?.();
|
|
654
|
+
routingChoiceCancelRef.current = null;
|
|
655
|
+
scopeWidget.cancel();
|
|
656
|
+
routingTailRef.current = Promise.resolve();
|
|
657
|
+
renderScopeWidget(version => version + 1);
|
|
658
|
+
}, [resetEscapeStop, scopeWidget]);
|
|
659
|
+
React.useEffect(() => () => { retireRouting(false); }, [retireRouting]);
|
|
385
660
|
/** Returns the post-enqueue size, or null if the queue was full. */
|
|
386
|
-
const enqueueMessage = useCallback((text, delivery = 'end-of-turn') => {
|
|
661
|
+
const enqueueMessage = useCallback((text, delivery = 'end-of-turn', submitted) => {
|
|
387
662
|
const current = queueRef.current;
|
|
388
|
-
|
|
663
|
+
const reserved = routingDraftsRef.current.size
|
|
664
|
+
- (submitted && routingDraftsRef.current.has(submitted.id) ? 1 : 0);
|
|
665
|
+
if (current.length + reserved >= MAX_QUEUE) {
|
|
389
666
|
showToastRef.current?.(`Queue full (${MAX_QUEUE}). Cancel or wait.`);
|
|
390
667
|
return null;
|
|
391
668
|
}
|
|
392
|
-
const entry = {
|
|
669
|
+
const entry = submitted ?? {
|
|
393
670
|
id: generateId(),
|
|
394
671
|
text,
|
|
395
672
|
queuedAt: new Date(),
|
|
396
673
|
delivery,
|
|
397
674
|
};
|
|
398
675
|
const nextSize = current.length + 1;
|
|
399
|
-
|
|
676
|
+
queueRef.current = [...current, entry];
|
|
677
|
+
setQueue(queueRef.current);
|
|
400
678
|
return nextSize;
|
|
401
679
|
}, []);
|
|
402
680
|
const dequeueMessage = useCallback(() => {
|
|
403
681
|
const current = queueRef.current;
|
|
404
682
|
if (current.length === 0)
|
|
405
683
|
return undefined;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
setQueue(
|
|
684
|
+
// A steering correction that missed its tool boundary gets the next
|
|
685
|
+
// request before Tab's explicit after-turn work.
|
|
686
|
+
const index = Math.max(0, current.findIndex(entry => entry.delivery === 'subturn'));
|
|
687
|
+
const entry = current[index];
|
|
688
|
+
queueRef.current = current.filter(candidate => candidate.id !== entry.id);
|
|
689
|
+
setQueue(queueRef.current);
|
|
690
|
+
return entry;
|
|
412
691
|
}, []);
|
|
413
692
|
const cancelGeneration = useCallback(() => {
|
|
693
|
+
retireAgentQuestion();
|
|
694
|
+
resetEscapeStop();
|
|
695
|
+
flushReadingDisplay();
|
|
414
696
|
if (abortControllerRef.current) {
|
|
697
|
+
retireRouting();
|
|
698
|
+
if (lastRequestRef.current)
|
|
699
|
+
retiredAgentsRef.current.add(lastRequestRef.current.agent);
|
|
415
700
|
activeRunIdRef.current = null;
|
|
416
701
|
abortControllerRef.current.abort();
|
|
417
702
|
abortControllerRef.current = null;
|
|
703
|
+
streamingMessageId.current = null;
|
|
704
|
+
streamedResponse.current = false;
|
|
705
|
+
setMessages(previous => previous.map(message => message.isStreaming
|
|
706
|
+
? { ...message, isStreaming: false } : message));
|
|
418
707
|
setAgentState({ status: 'idle' });
|
|
419
|
-
|
|
708
|
+
showToastRef.current?.('Stopped by you');
|
|
420
709
|
return true;
|
|
421
710
|
}
|
|
422
711
|
return false;
|
|
423
|
-
}, [
|
|
712
|
+
}, [resetEscapeStop, retireRouting, flushReadingDisplay, retireAgentQuestion]);
|
|
713
|
+
const workspaceWidget = useMemo(() => new SelectWidget({
|
|
714
|
+
render: () => renderWorkspaceWidget(version => version + 1),
|
|
715
|
+
addSystemMessage: content => addMessage({ role: 'system', content }),
|
|
716
|
+
setToast: content => { if (content)
|
|
717
|
+
showToastRef.current?.(content); },
|
|
718
|
+
writeClipboard,
|
|
719
|
+
}), [addMessage]);
|
|
720
|
+
workspaceWidgetRef.current = workspaceWidget;
|
|
721
|
+
React.useEffect(() => {
|
|
722
|
+
workspaceClosedRef.current = false;
|
|
723
|
+
workspaceReadyRef.current = workspaceCoordinator.start();
|
|
724
|
+
const unsubscribeJobs = subscribeCommandJobs(sessionId, () => {
|
|
725
|
+
if (!workspaceClosedRef.current && sessionIdRef.current === sessionId)
|
|
726
|
+
void workspaceCoordinator.refresh().catch(error => debugLog(`[workspace.jobs] ${String(error)}`));
|
|
727
|
+
});
|
|
728
|
+
void workspaceReadyRef.current.catch(error => {
|
|
729
|
+
if (!workspaceClosedRef.current)
|
|
730
|
+
addMessage({ role: 'system', content: `Workspace discovery unavailable: ${error instanceof Error ? error.message : String(error)}` });
|
|
731
|
+
});
|
|
732
|
+
return () => {
|
|
733
|
+
workspaceClosedRef.current = true;
|
|
734
|
+
unsubscribeJobs();
|
|
735
|
+
workspaceChoiceCancelRef.current?.();
|
|
736
|
+
pausedWorkspaceOperationRef.current?.cancel();
|
|
737
|
+
pausedWorkspaceOperationRef.current = null;
|
|
738
|
+
workspaceWidget.cancel();
|
|
739
|
+
void workspaceCoordinator.close().catch(error => debugLog(`[workspace.close] ${String(error)}`));
|
|
740
|
+
};
|
|
741
|
+
}, [sessionId, workspaceCoordinator, workspaceWidget, addMessage]);
|
|
742
|
+
const refreshWorkspace = useCallback(async () => {
|
|
743
|
+
if (workspaceReadyRef.current)
|
|
744
|
+
await workspaceReadyRef.current;
|
|
745
|
+
return workspaceCoordinator.refresh();
|
|
746
|
+
}, [workspaceCoordinator]);
|
|
747
|
+
const readWorkspace = useCallback(async (cwd) => {
|
|
748
|
+
const snapshot = await refreshWorkspace();
|
|
749
|
+
return cwd === undefined ? snapshot : inspectWorkspaceSessions(cwd, sessionId);
|
|
750
|
+
}, [refreshWorkspace, sessionId]);
|
|
751
|
+
const admitWorkspace = useCallback((signal, cwd) => {
|
|
752
|
+
const admission = workspaceAdmissionTailRef.current.then(async () => {
|
|
753
|
+
let snapshot = await readWorkspace(cwd);
|
|
754
|
+
if (workspacePausedRef.current || workspaceClosedRef.current || signal?.aborted)
|
|
755
|
+
return { allowed: false, snapshot };
|
|
756
|
+
while (hasWorkspaceCollisionRisk(snapshot) && !workspaceCoordinator.isAcknowledged(snapshot)) {
|
|
757
|
+
resetEscapeStop();
|
|
758
|
+
const choice = await new Promise(resolve => {
|
|
759
|
+
let settled = false;
|
|
760
|
+
const finish = (value) => {
|
|
761
|
+
if (settled)
|
|
762
|
+
return;
|
|
763
|
+
settled = true;
|
|
764
|
+
signal?.removeEventListener('abort', abort);
|
|
765
|
+
workspaceChoiceCancelRef.current = null;
|
|
766
|
+
workspaceWidget.cancel();
|
|
767
|
+
renderWorkspaceWidget(version => version + 1);
|
|
768
|
+
resolve(value);
|
|
769
|
+
};
|
|
770
|
+
const abort = () => finish(null);
|
|
771
|
+
workspaceChoiceCancelRef.current = () => finish(null);
|
|
772
|
+
if (signal?.aborted || workspaceClosedRef.current) {
|
|
773
|
+
finish(null);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
777
|
+
workspaceWidget.show({ title: 'Workspace overlap',
|
|
778
|
+
question: formatWorkspaceAdmissionDetails(snapshot),
|
|
779
|
+
type: 'select', allowOther: false,
|
|
780
|
+
options: [{ value: 'continue', label: 'Continue here' },
|
|
781
|
+
{ value: 'isolate', label: 'Isolate this session' }, { value: 'pause', label: 'Pause this session' }],
|
|
782
|
+
callback: value => finish(typeof value === 'string' ? value : null),
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
if (signal?.aborted || workspaceClosedRef.current)
|
|
786
|
+
return { allowed: false, snapshot };
|
|
787
|
+
if (choice === 'continue') {
|
|
788
|
+
const checked = await readWorkspace(cwd);
|
|
789
|
+
if (checked.fingerprint !== snapshot.fingerprint) {
|
|
790
|
+
snapshot = checked;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
workspaceCoordinator.acknowledge(checked);
|
|
794
|
+
return { allowed: true, snapshot: checked };
|
|
795
|
+
}
|
|
796
|
+
workspacePausedRef.current = true;
|
|
797
|
+
queuePausedRef.current = true;
|
|
798
|
+
const paused = await workspaceCoordinator.setActivity('paused');
|
|
799
|
+
if (cwd === undefined)
|
|
800
|
+
snapshot = paused;
|
|
801
|
+
return { allowed: false, snapshot, isolate: choice === 'isolate' };
|
|
802
|
+
}
|
|
803
|
+
return { allowed: true, snapshot };
|
|
804
|
+
});
|
|
805
|
+
workspaceAdmissionTailRef.current = admission.then(() => { }, () => { });
|
|
806
|
+
return admission;
|
|
807
|
+
}, [readWorkspace, workspaceCoordinator, workspaceWidget, resetEscapeStop, addMessage]);
|
|
808
|
+
const workspaceFactoryOptions = useCallback(() => ({
|
|
809
|
+
cwd: shellCwdRef.current, sessionId,
|
|
810
|
+
toolEffectObserver: async (event) => {
|
|
811
|
+
if (event.effect === 'read')
|
|
812
|
+
return;
|
|
813
|
+
const admission = await admitWorkspace(event.signal, event.cwd);
|
|
814
|
+
if (!admission.allowed) {
|
|
815
|
+
if (workspaceClosedRef.current || event.signal?.aborted)
|
|
816
|
+
throw new AgentError('Workspace execution cancelled before this managed effect.', 'CANCELLED');
|
|
817
|
+
if (pausedWorkspaceOperationRef.current)
|
|
818
|
+
throw new AgentError('Workspace paused with an earlier operation retained.', 'CANCELLED');
|
|
819
|
+
const owner = abortControllerRef.current?.signal === event.signal ? abortControllerRef.current : null;
|
|
820
|
+
await new Promise((resolve, reject) => {
|
|
821
|
+
let settled = false;
|
|
822
|
+
let resuming = false;
|
|
823
|
+
const finish = (error) => {
|
|
824
|
+
if (settled)
|
|
825
|
+
return;
|
|
826
|
+
settled = true;
|
|
827
|
+
event.signal?.removeEventListener('abort', abort);
|
|
828
|
+
if (pausedWorkspaceOperationRef.current === operation)
|
|
829
|
+
pausedWorkspaceOperationRef.current = null;
|
|
830
|
+
if (error)
|
|
831
|
+
reject(error);
|
|
832
|
+
else
|
|
833
|
+
resolve();
|
|
834
|
+
};
|
|
835
|
+
const abort = () => finish(new AgentError('Workspace execution cancelled before this managed effect.', 'CANCELLED'));
|
|
836
|
+
const operation = {
|
|
837
|
+
kind: 'tool', cwd: event.cwd ?? shellCwdRef.current,
|
|
838
|
+
fingerprint: admission.snapshot.fingerprint,
|
|
839
|
+
resume: async () => {
|
|
840
|
+
if (settled || resuming)
|
|
841
|
+
return;
|
|
842
|
+
resuming = true;
|
|
843
|
+
try {
|
|
844
|
+
const checked = await admitWorkspace(event.signal, event.cwd);
|
|
845
|
+
if (workspaceClosedRef.current || event.signal?.aborted) {
|
|
846
|
+
abort();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (!checked.allowed) {
|
|
850
|
+
operation.fingerprint = checked.snapshot.fingerprint;
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
await workspaceCoordinator.setActivity('running');
|
|
854
|
+
if (workspaceClosedRef.current || event.signal?.aborted) {
|
|
855
|
+
abort();
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
finish();
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
if (!settled) {
|
|
862
|
+
workspacePausedRef.current = true;
|
|
863
|
+
queuePausedRef.current = true;
|
|
864
|
+
}
|
|
865
|
+
throw error;
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
resuming = false;
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
cancel: () => { owner?.abort(); abort(); },
|
|
872
|
+
};
|
|
873
|
+
pausedWorkspaceOperationRef.current = operation;
|
|
874
|
+
event.signal?.addEventListener('abort', abort, { once: true });
|
|
875
|
+
if (event.signal?.aborted || workspaceClosedRef.current) {
|
|
876
|
+
abort();
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
addMessage({ role: 'system', content: 'Workspace session paused. The managed operation is retained; use /sessions continue.' });
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
onLocalFileChange: async (paths, cwd) => { await workspaceCoordinator.recordObservedPaths(paths, cwd); },
|
|
884
|
+
}), [admitWorkspace, addMessage, sessionId, workspaceCoordinator]);
|
|
885
|
+
const createBoundAgent = useCallback((mode, skillPrompt) => {
|
|
886
|
+
const next = createAgent(mode, skillPrompt, workspaceFactoryOptions());
|
|
887
|
+
const previous = diagnosticAgentRef.current;
|
|
888
|
+
if (next) {
|
|
889
|
+
next.restoreUsageSnapshot(getUsageSnapshot());
|
|
890
|
+
if (previous?.getDiagnosticSessionId() === next.getDiagnosticSessionId()) {
|
|
891
|
+
next.restoreDiagnosticSnapshot(captureSessionDiagnosticSnapshot({
|
|
892
|
+
messages: messagesRef.current, diagnostics: previous.getDiagnosticSnapshot(),
|
|
893
|
+
}));
|
|
894
|
+
}
|
|
895
|
+
next.recordDiagnosticEvent('runtime', { renderer: 'ink', rendererVersion: null, nativeCapabilities: null });
|
|
896
|
+
next.recordDiagnosticEvent('mode', { mode: mode ?? 'normal', source: 'agent_construction' });
|
|
897
|
+
diagnosticAgentRef.current = next;
|
|
898
|
+
}
|
|
899
|
+
return next;
|
|
900
|
+
}, [workspaceFactoryOptions, getUsageSnapshot]);
|
|
901
|
+
// Every reconstruction retains workspace admission and successful-file observation.
|
|
902
|
+
const reloadAgent = useCallback(() => {
|
|
903
|
+
retireAgentQuestion();
|
|
904
|
+
const previous = diagnosticAgentRef.current;
|
|
905
|
+
const next = createBoundAgent();
|
|
906
|
+
diagnosticAgentRef.current = next;
|
|
907
|
+
if (previous && previous !== next) {
|
|
908
|
+
retiredAgentsRef.current.add(previous);
|
|
909
|
+
previous.dispose();
|
|
910
|
+
}
|
|
911
|
+
if (lastRequestRef.current && next)
|
|
912
|
+
lastRequestRef.current = { ...lastRequestRef.current, agent: next };
|
|
913
|
+
setAgent(next);
|
|
914
|
+
}, [createBoundAgent, retireAgentQuestion]);
|
|
915
|
+
const applyAccountOperation = useCallback(async (body) => {
|
|
916
|
+
if (runtimeMutationRef.current || abortControllerRef.current
|
|
917
|
+
|| (agentStateRef.current.status !== 'idle' && agentStateRef.current.status !== 'error'))
|
|
918
|
+
return { ok: false, blocked: true, error: 'An agent turn or tool is active; wait before switching accounts.' };
|
|
919
|
+
runtimeMutationRef.current = true;
|
|
920
|
+
let operationOpen = true;
|
|
921
|
+
try {
|
|
922
|
+
return await applyAccountOp(body, configStore, () => {
|
|
923
|
+
if (!operationOpen && (abortControllerRef.current || runtimeMutationRef.current)) {
|
|
924
|
+
accountReloadPendingRef.current = true;
|
|
925
|
+
addMessage({ role: 'system', content: 'Account sign-in completed; reload will apply after this turn finishes.' });
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
reloadAgent();
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
finally {
|
|
932
|
+
operationOpen = false;
|
|
933
|
+
runtimeMutationRef.current = false;
|
|
934
|
+
}
|
|
935
|
+
}, [reloadAgent, addMessage]);
|
|
936
|
+
React.useEffect(() => { setAgent(createBoundAgent()); }, [createBoundAgent]);
|
|
937
|
+
React.useEffect(() => {
|
|
938
|
+
if ((agentState.status !== 'idle' && agentState.status !== 'error') || abortControllerRef.current)
|
|
939
|
+
return;
|
|
940
|
+
if (accountReloadPendingRef.current && !runtimeMutationRef.current) {
|
|
941
|
+
accountReloadPendingRef.current = false;
|
|
942
|
+
reloadAgent();
|
|
943
|
+
}
|
|
944
|
+
const usage = getUsageSnapshot();
|
|
945
|
+
if (!usage.receipts.length && !usage.omittedReceipts)
|
|
946
|
+
return;
|
|
947
|
+
const signature = JSON.stringify([sessionId, usage]);
|
|
948
|
+
if (signature === lastUsageSaveRef.current)
|
|
949
|
+
return;
|
|
950
|
+
const messages = structuredClone(messagesRef.current);
|
|
951
|
+
const runtime = { mode: agentMode, usage, evidence: agent?.getEvidenceSnapshot(),
|
|
952
|
+
diagnostics: captureSessionDiagnosticSnapshot({ messages, diagnostics: agent?.getDiagnosticSnapshot() }) };
|
|
953
|
+
const cwd = shellCwdRef.current;
|
|
954
|
+
usageSaveQueueRef.current = usageSaveQueueRef.current.then(async () => {
|
|
955
|
+
await saveSession(sessionId, cwd, prepareSessionMessages(messages), configStore.getModel(), configStore.getProvider(), undefined, undefined, runtime);
|
|
956
|
+
lastUsageSaveRef.current = signature;
|
|
957
|
+
}).catch(error => { addMessage({ role: 'system', content: `Usage checkpoint could not be saved: ${String(error)}` }); });
|
|
958
|
+
}, [agentState.status, agent, agentMode, sessionId, getUsageSnapshot, reloadAgent, addMessage, routingUsageRevision]);
|
|
959
|
+
const isolateWorkspace = useCallback(async () => {
|
|
960
|
+
if (workspaceIsolationRunningRef.current || workspaceClosedRef.current)
|
|
961
|
+
return false;
|
|
962
|
+
if (pausedWorkspaceOperationRef.current) {
|
|
963
|
+
addMessage({ role: 'system', content: 'The paused operation belongs to the current execution context. Use /sessions continue to finish it, then isolate a fresh task; the original operation is retained.' });
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
if (configStore.get().remoteResources !== undefined || process.env.ZTC_HOST_RUNTIME !== undefined
|
|
967
|
+
|| runtimePluginRegistry.resolveHostRuntime({ mode: agentMode })) {
|
|
968
|
+
addMessage({ role: 'system', content: 'This host or guarded remote session owns its execution context. Local isolation cannot transfer its conversation or remote lease. Your input is retained; use /sessions continue here or open a separate local session.' });
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
workspaceIsolationRunningRef.current = true;
|
|
972
|
+
workspacePausedRef.current = true;
|
|
973
|
+
queuePausedRef.current = true;
|
|
974
|
+
const oldSessionId = sessionId;
|
|
975
|
+
let retainedTarget = null;
|
|
976
|
+
try {
|
|
977
|
+
cancelGeneration();
|
|
978
|
+
retireRouting();
|
|
979
|
+
const snapshot = await workspaceCoordinator.setActivity('paused');
|
|
980
|
+
await saveSession(oldSessionId, shellCwdRef.current, prepareSessionMessages(structuredClone(messagesRef.current)), configStore.getModel(), configStore.getProvider(), undefined, undefined, { mode: agentMode, usage: getUsageSnapshot(), evidence: agent?.getEvidenceSnapshot(), diagnostics: captureSessionDiagnosticSnapshot({ messages: messagesRef.current, diagnostics: agent?.getDiagnosticSnapshot() }) });
|
|
981
|
+
const isolated = await prepareWorkspaceIsolation(snapshot);
|
|
982
|
+
retainedTarget = isolated.cwd;
|
|
983
|
+
const nextSessionId = generateSessionId();
|
|
984
|
+
await saveSession(nextSessionId, isolated.cwd, [], configStore.getModel(), configStore.getProvider(), undefined, undefined, { mode: agentMode });
|
|
985
|
+
if (workspaceClosedRef.current || sessionIdRef.current !== oldSessionId)
|
|
986
|
+
return false;
|
|
987
|
+
const pending = pausedWorkspaceInputRef.current;
|
|
988
|
+
isolatedWorkspaceResumeRef.current = { sessionId: nextSessionId,
|
|
989
|
+
...(pending ? { text: pending.text, queuedEntry: pending.queuedEntry, askUserAnswer: pending.askUserAnswer } : {}) };
|
|
990
|
+
if (agent) {
|
|
991
|
+
retiredAgentsRef.current.add(agent);
|
|
992
|
+
agent.dispose();
|
|
993
|
+
}
|
|
994
|
+
lastRequestRef.current = null;
|
|
995
|
+
activeSkillIds.current = [];
|
|
996
|
+
toolStartTimes.current.clear();
|
|
997
|
+
streamingMessageId.current = null;
|
|
998
|
+
streamedResponse.current = false;
|
|
999
|
+
shellCwdRef.current = isolated.cwd;
|
|
1000
|
+
sessionIdRef.current = nextSessionId;
|
|
1001
|
+
setAgent(null);
|
|
1002
|
+
setMessages([]);
|
|
1003
|
+
updateAgentState({ status: 'idle' });
|
|
1004
|
+
setClearDraftVersion(version => version + 1);
|
|
1005
|
+
setSessionId(nextSessionId);
|
|
1006
|
+
addMessage({ role: 'system', content: `Workspace isolated: ${isolated.cwd}\nSession: ${nextSessionId}\nPrevious session saved: ${oldSessionId}. Source edits and index preserved.` });
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
catch (error) {
|
|
1010
|
+
if (error instanceof WorkspaceIsolationError)
|
|
1011
|
+
retainedTarget = error.retainedTarget;
|
|
1012
|
+
if (!workspaceClosedRef.current)
|
|
1013
|
+
addMessage({ role: 'system', content: `Workspace isolation failed: ${error instanceof Error ? error.message : String(error)}${retainedTarget ? `\nRetained target: ${retainedTarget}` : ''}\nYour current session, input and queued work are retained; use /sessions continue.` });
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
finally {
|
|
1017
|
+
workspaceIsolationRunningRef.current = false;
|
|
1018
|
+
}
|
|
1019
|
+
}, [addMessage, agent, agentMode, cancelGeneration, retireRouting, sessionId, setMessages, workspaceCoordinator]);
|
|
1020
|
+
React.useEffect(() => {
|
|
1021
|
+
const pending = isolatedWorkspaceResumeRef.current;
|
|
1022
|
+
if (!pending || pending.sessionId !== sessionId)
|
|
1023
|
+
return;
|
|
1024
|
+
// This effect runs after the fresh coordinator and Agent bindings are installed.
|
|
1025
|
+
// Keep the queue paused until the new session owns the submitted input.
|
|
1026
|
+
void refreshWorkspace().then(async () => {
|
|
1027
|
+
if (workspaceClosedRef.current || sessionIdRef.current !== pending.sessionId)
|
|
1028
|
+
return;
|
|
1029
|
+
isolatedWorkspaceResumeRef.current = null;
|
|
1030
|
+
pausedWorkspaceInputRef.current = null;
|
|
1031
|
+
workspacePausedRef.current = false;
|
|
1032
|
+
queuePausedRef.current = false;
|
|
1033
|
+
if (pending.text !== undefined) {
|
|
1034
|
+
flushingRef.current = true;
|
|
1035
|
+
setFlushing(true);
|
|
1036
|
+
try {
|
|
1037
|
+
await workspaceSubmitRef.current(pending.text, undefined, pending.queuedEntry, pending.askUserAnswer);
|
|
1038
|
+
}
|
|
1039
|
+
finally {
|
|
1040
|
+
flushingRef.current = false;
|
|
1041
|
+
setFlushing(false);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
setWorkspaceResumeVersion(version => version + 1);
|
|
1045
|
+
}).catch(error => {
|
|
1046
|
+
if (!workspaceClosedRef.current)
|
|
1047
|
+
addMessage({ role: 'system', content: `The isolated session could not resume: ${error instanceof Error ? error.message : String(error)}. Use /sessions continue.` });
|
|
1048
|
+
});
|
|
1049
|
+
}, [sessionId, refreshWorkspace, addMessage]);
|
|
1050
|
+
const continueWorkspace = useCallback(async () => {
|
|
1051
|
+
const pending = pausedWorkspaceInputRef.current;
|
|
1052
|
+
const operation = pausedWorkspaceOperationRef.current;
|
|
1053
|
+
const snapshot = await readWorkspace(operation?.cwd);
|
|
1054
|
+
if ((operation?.fingerprint ?? pending?.fingerprint) === snapshot.fingerprint)
|
|
1055
|
+
workspaceCoordinator.acknowledge(snapshot);
|
|
1056
|
+
workspacePausedRef.current = false;
|
|
1057
|
+
queuePausedRef.current = false;
|
|
1058
|
+
await workspaceCoordinator.setActivity('idle');
|
|
1059
|
+
if (pending || operation) {
|
|
1060
|
+
pausedWorkspaceInputRef.current = null;
|
|
1061
|
+
// A suspended tool stays reachable for cancellation until its owner resolves.
|
|
1062
|
+
if (operation?.kind !== 'tool')
|
|
1063
|
+
pausedWorkspaceOperationRef.current = null;
|
|
1064
|
+
flushingRef.current = true;
|
|
1065
|
+
setFlushing(true);
|
|
1066
|
+
try {
|
|
1067
|
+
if (operation?.kind === 'tool')
|
|
1068
|
+
await operation.resume();
|
|
1069
|
+
if (pending)
|
|
1070
|
+
await workspaceSubmitRef.current(pending.text, undefined, pending.queuedEntry, pending.askUserAnswer);
|
|
1071
|
+
if (operation && operation.kind !== 'tool')
|
|
1072
|
+
await operation.resume();
|
|
1073
|
+
}
|
|
1074
|
+
finally {
|
|
1075
|
+
flushingRef.current = false;
|
|
1076
|
+
setFlushing(false);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
setWorkspaceResumeVersion(version => version + 1);
|
|
1080
|
+
}, [readWorkspace, workspaceCoordinator]);
|
|
424
1081
|
React.useEffect(() => {
|
|
425
1082
|
if (process.env.ZTC_DISABLE_UPDATE === '1')
|
|
426
1083
|
return;
|
|
@@ -495,8 +1152,13 @@ export const App = () => {
|
|
|
495
1152
|
});
|
|
496
1153
|
}, [contextStatus, agentState.status, messages, currentModel, addMessage]);
|
|
497
1154
|
const clearMessages = useCallback(() => {
|
|
1155
|
+
retireAgentQuestion();
|
|
1156
|
+
resetEscapeStop();
|
|
1157
|
+
retireRouting();
|
|
1158
|
+
flushReadingDisplay();
|
|
498
1159
|
setMessages([]);
|
|
499
|
-
|
|
1160
|
+
setTranscriptGeneration(generation => generation + 1);
|
|
1161
|
+
}, [resetEscapeStop, retireRouting, setMessages, flushReadingDisplay, retireAgentQuestion]);
|
|
500
1162
|
const getMessages = useCallback(() => messages, [messages]);
|
|
501
1163
|
const shellController = useMemo(() => ({
|
|
502
1164
|
getCwd: () => shellCwdRef.current,
|
|
@@ -507,156 +1169,245 @@ export const App = () => {
|
|
|
507
1169
|
if (agent) {
|
|
508
1170
|
agent.setCwd(next);
|
|
509
1171
|
}
|
|
1172
|
+
await refreshWorkspace();
|
|
510
1173
|
return next;
|
|
511
1174
|
},
|
|
512
|
-
run:
|
|
513
|
-
|
|
1175
|
+
run: (command) => {
|
|
1176
|
+
const cwd = shellCwdRef.current;
|
|
1177
|
+
return new Promise((resolve, reject) => {
|
|
1178
|
+
const execute = async () => {
|
|
1179
|
+
const admission = await admitWorkspace(undefined, cwd);
|
|
1180
|
+
if (workspaceClosedRef.current)
|
|
1181
|
+
throw new Error('Workspace command cancelled when the session closed.');
|
|
1182
|
+
if (!admission.allowed) {
|
|
1183
|
+
if (pausedWorkspaceOperationRef.current)
|
|
1184
|
+
throw new Error('Workspace paused with an earlier operation retained. Continue that operation, then resubmit this command.');
|
|
1185
|
+
pausedWorkspaceOperationRef.current = { kind: 'shell', cwd,
|
|
1186
|
+
fingerprint: admission.snapshot.fingerprint, resume: execute,
|
|
1187
|
+
cancel: () => reject(new Error('Workspace command cancelled when the session closed.')) };
|
|
1188
|
+
addMessage({ role: 'system', content: 'Workspace session paused. The shell command is retained; use /sessions continue.' });
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
resolve(await runShellCommand(command, cwd));
|
|
1192
|
+
};
|
|
1193
|
+
void execute().catch(reject);
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}), [agent, refreshWorkspace, admitWorkspace, addMessage]);
|
|
514
1197
|
const isRetryableError = useCallback((message) => {
|
|
515
1198
|
const lower = message.toLowerCase();
|
|
516
1199
|
return lower.includes('overloaded') || lower.includes('529') || lower.includes('429') || lower.includes('rate limit');
|
|
517
1200
|
}, []);
|
|
518
|
-
const runWithRetry = useCallback(async (requestMessages, runAgent, isManual = false, isActive, signal) => {
|
|
1201
|
+
const runWithRetry = useCallback(async (requestMessages, runAgent, isManual = false, isActive, signal, takeSteering, promptTurn = promptTurnRef.current) => {
|
|
519
1202
|
const maxRetries = 3;
|
|
520
1203
|
let attempt = 0;
|
|
521
1204
|
setRetryAvailable(false);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (signal?.aborted)
|
|
527
|
-
return;
|
|
528
|
-
try {
|
|
529
|
-
streamedResponse.current = false;
|
|
530
|
-
const priorIds = new Set(requestMessages.map(message => message.id));
|
|
531
|
-
const stopCheckpoints = runAgent.on(event => {
|
|
532
|
-
if (event.type !== 'history_checkpoint')
|
|
533
|
-
return;
|
|
534
|
-
const ordered = event.messages;
|
|
535
|
-
setMessages(prev => [...prev.filter(message => priorIds.has(message.id)), ...ordered,
|
|
536
|
-
...prev.filter(message => !priorIds.has(message.id) && (message.role === 'system' || message.role === 'tool'))]);
|
|
537
|
-
});
|
|
538
|
-
let result;
|
|
539
|
-
try {
|
|
540
|
-
result = await runAgent.run(requestMessages, signal);
|
|
541
|
-
}
|
|
542
|
-
finally {
|
|
543
|
-
stopCheckpoints();
|
|
544
|
-
}
|
|
1205
|
+
promptTurn?.start();
|
|
1206
|
+
try {
|
|
1207
|
+
while (attempt < maxRetries) {
|
|
1208
|
+
attempt += 1;
|
|
545
1209
|
if (isActive && !isActive())
|
|
546
1210
|
return;
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1211
|
+
if (signal?.aborted)
|
|
1212
|
+
return;
|
|
1213
|
+
try {
|
|
1214
|
+
streamedResponse.current = false;
|
|
1215
|
+
const iterations = [];
|
|
1216
|
+
const toolIterations = new Map();
|
|
1217
|
+
const stopCheckpoints = runAgent.on(event => {
|
|
1218
|
+
if (isActive && !isActive())
|
|
1219
|
+
return;
|
|
1220
|
+
if (event.type === 'thinking_start')
|
|
1221
|
+
iterations.push({ start: messagesRef.current.length });
|
|
1222
|
+
if (event.type === 'tool_start') {
|
|
1223
|
+
const iteration = iterations.length - 1;
|
|
1224
|
+
if (iteration >= 0) {
|
|
1225
|
+
// The live listener has just painted this tool's start trace.
|
|
1226
|
+
iterations[iteration].toolStart ??= messagesRef.current.length - 1;
|
|
1227
|
+
toolIterations.set(event.toolCallId, iteration);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (event.type !== 'history_checkpoint')
|
|
1231
|
+
return;
|
|
1232
|
+
setMessages(live => reconcileOrderedTranscript({ live, ordered: event.messages, iterations, toolIterations }));
|
|
1233
|
+
});
|
|
1234
|
+
let result;
|
|
1235
|
+
try {
|
|
1236
|
+
result = await runAgent.run(requestMessages, signal, takeSteering, undefined, undefined, promptTurn?.observe);
|
|
1237
|
+
}
|
|
1238
|
+
finally {
|
|
1239
|
+
stopCheckpoints();
|
|
1240
|
+
}
|
|
1241
|
+
if (isActive && !isActive())
|
|
1242
|
+
return;
|
|
1243
|
+
if (!streamedResponse.current) {
|
|
1244
|
+
// Only add assistant message if there's actual text content.
|
|
1245
|
+
// Tool calls are already displayed via tool_start/tool_end events.
|
|
1246
|
+
// When the model returns empty content after tool execution, don't show empty message.
|
|
1247
|
+
const hasContent = result.content && result.content.trim().length > 0;
|
|
1248
|
+
if (hasContent) {
|
|
1249
|
+
addMessage({
|
|
1250
|
+
role: 'assistant',
|
|
1251
|
+
content: result.content,
|
|
1252
|
+
...(result.nativeContent ? { nativeContent: result.nativeContent, nativeModel: result.nativeModel, nativeContextId: result.nativeContextId } : {}),
|
|
1253
|
+
toolCalls: result.turnMessages?.length ? undefined : result.toolCalls.length > 0 ? result.toolCalls : undefined,
|
|
1254
|
+
// Keep the provider's reasoning trace alongside usage: reasoning
|
|
1255
|
+
// models expect the assistant message replayed with its
|
|
1256
|
+
// reasoning_content, and dropping it here strips every later turn.
|
|
1257
|
+
metadata: (result.usage || result.reasoning)
|
|
1258
|
+
? {
|
|
1259
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
1260
|
+
...(result.reasoning ? { reasoningContent: result.reasoning } : {}),
|
|
1261
|
+
}
|
|
1262
|
+
: undefined
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
else if (result.reasoning || result.nativeContent) {
|
|
1267
|
+
// A streamed turn already put its assistant message in state, so the
|
|
1268
|
+
// reasoning trace has to be attached after the fact. Without this,
|
|
1269
|
+
// only buffered replies keep it and streamed turns — the common case —
|
|
1270
|
+
// replay a stripped history.
|
|
1271
|
+
setMessages(prev => {
|
|
1272
|
+
const lastIdx = prev.reduce((last, m, i) => (isPersistableAssistantMessage(m) ? i : last), -1);
|
|
1273
|
+
if (lastIdx < 0)
|
|
1274
|
+
return prev;
|
|
1275
|
+
return prev.map((m, i) => i === lastIdx
|
|
1276
|
+
? { ...m, ...(result.nativeContent ? { nativeContent: result.nativeContent, nativeModel: result.nativeModel, nativeContextId: result.nativeContextId } : {}),
|
|
1277
|
+
metadata: { ...m.metadata, ...(result.reasoning ? { reasoningContent: result.reasoning } : {}) } }
|
|
1278
|
+
: m);
|
|
566
1279
|
});
|
|
567
1280
|
}
|
|
1281
|
+
// Idle publication can synchronously flush queued work. Retire the
|
|
1282
|
+
// completed owner's confirmation before publishing that transition.
|
|
1283
|
+
if (abortControllerRef.current?.signal === signal) {
|
|
1284
|
+
abortControllerRef.current = null;
|
|
1285
|
+
resetEscapeStop();
|
|
1286
|
+
}
|
|
1287
|
+
setAgentState(s => ({
|
|
1288
|
+
status: 'idle',
|
|
1289
|
+
tokensUsed: result.usage?.totalTokens || 0,
|
|
1290
|
+
// Carry the PER-CALL context the last token_usage event set; do NOT use
|
|
1291
|
+
// result.usage.inputTokens — it is the cumulative cross-iteration sum
|
|
1292
|
+
// (mergeUsage) and would inflate the bar. (currentTool/error/startedAt
|
|
1293
|
+
// are intentionally dropped on idle, as before.)
|
|
1294
|
+
contextTokens: s.contextTokens,
|
|
1295
|
+
contextWindow: s.contextWindow,
|
|
1296
|
+
cacheInputTokens: s.cacheInputTokens,
|
|
1297
|
+
cachedInputTokens: s.cachedInputTokens,
|
|
1298
|
+
tokensEstimated: s.tokensEstimated,
|
|
1299
|
+
authError: false
|
|
1300
|
+
}));
|
|
1301
|
+
return;
|
|
568
1302
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1303
|
+
catch (err) {
|
|
1304
|
+
const errorMsg = err.message || 'Agent error';
|
|
1305
|
+
if ((err instanceof AgentError && err.code === 'CANCELLED') || signal?.aborted)
|
|
1306
|
+
return;
|
|
1307
|
+
if (isActive && !isActive())
|
|
1308
|
+
return;
|
|
1309
|
+
if (!isRetryableError(errorMsg) || attempt >= maxRetries) {
|
|
1310
|
+
throw err;
|
|
1311
|
+
}
|
|
1312
|
+
const delayMs = 500 * Math.pow(2, attempt - 1);
|
|
1313
|
+
addMessage({
|
|
1314
|
+
role: 'system',
|
|
1315
|
+
content: `Overloaded. Retrying in ${Math.round(delayMs / 100) / 10}s... (attempt ${attempt + 1}/${maxRetries})`
|
|
581
1316
|
});
|
|
1317
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
582
1318
|
}
|
|
583
|
-
setAgentState(s => ({
|
|
584
|
-
status: 'idle',
|
|
585
|
-
tokensUsed: result.usage?.totalTokens || 0,
|
|
586
|
-
// Carry the PER-CALL context the last token_usage event set; do NOT use
|
|
587
|
-
// result.usage.inputTokens — it is the cumulative cross-iteration sum
|
|
588
|
-
// (mergeUsage) and would inflate the bar. (currentTool/error/startedAt
|
|
589
|
-
// are intentionally dropped on idle, as before.)
|
|
590
|
-
contextTokens: s.contextTokens,
|
|
591
|
-
contextWindow: s.contextWindow,
|
|
592
|
-
cacheInputTokens: s.cacheInputTokens,
|
|
593
|
-
cachedInputTokens: s.cachedInputTokens,
|
|
594
|
-
tokensEstimated: s.tokensEstimated,
|
|
595
|
-
authError: false
|
|
596
|
-
}));
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
catch (err) {
|
|
600
|
-
const errorMsg = err.message || 'Agent error';
|
|
601
|
-
if ((err instanceof AgentError && err.code === 'CANCELLED') || signal?.aborted)
|
|
602
|
-
return;
|
|
603
|
-
if (isActive && !isActive())
|
|
604
|
-
return;
|
|
605
|
-
if (!isRetryableError(errorMsg) || attempt >= maxRetries) {
|
|
606
|
-
throw err;
|
|
607
|
-
}
|
|
608
|
-
const delayMs = 500 * Math.pow(2, attempt - 1);
|
|
609
|
-
addMessage({
|
|
610
|
-
role: 'system',
|
|
611
|
-
content: `Overloaded. Retrying in ${Math.round(delayMs / 100) / 10}s... (attempt ${attempt + 1}/${maxRetries})`
|
|
612
|
-
});
|
|
613
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
614
1319
|
}
|
|
615
1320
|
}
|
|
616
|
-
|
|
1321
|
+
finally {
|
|
1322
|
+
promptTurn?.finish();
|
|
1323
|
+
}
|
|
1324
|
+
}, [addMessage, isRetryableError, resetEscapeStop, setAgentState]);
|
|
617
1325
|
const retryLast = useCallback(() => {
|
|
618
1326
|
const last = lastRequestRef.current;
|
|
619
1327
|
if (!last) {
|
|
620
1328
|
addMessage({ role: 'system', content: 'No previous request to retry.' });
|
|
621
1329
|
return;
|
|
622
1330
|
}
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1331
|
+
const replay = async () => {
|
|
1332
|
+
const cwd = last.agent.getCwd();
|
|
1333
|
+
const admission = await admitWorkspace(undefined, cwd);
|
|
1334
|
+
if (workspaceClosedRef.current)
|
|
1335
|
+
return;
|
|
1336
|
+
if (!admission.allowed) {
|
|
1337
|
+
if (!pausedWorkspaceOperationRef.current)
|
|
1338
|
+
pausedWorkspaceOperationRef.current = {
|
|
1339
|
+
kind: 'retry', cwd, fingerprint: admission.snapshot.fingerprint, resume: replay, cancel: () => { },
|
|
1340
|
+
};
|
|
1341
|
+
addMessage({ role: 'system', content: 'Workspace session paused. The original retry request is retained; use /sessions continue.' });
|
|
631
1342
|
return;
|
|
632
|
-
addMessage({ role: 'system', content: `Error: ${message}` });
|
|
633
|
-
if (isRetryableError(message)) {
|
|
634
|
-
addMessage({ role: 'system', content: 'Retries exhausted. Use /retry to try again.' });
|
|
635
|
-
setRetryAvailable(true);
|
|
636
1343
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1344
|
+
const runId = `${Date.now()}_${runCounterRef.current++}`;
|
|
1345
|
+
flushReadingDisplay();
|
|
1346
|
+
activeRunIdRef.current = runId;
|
|
1347
|
+
resetAskUserTurnFlag();
|
|
1348
|
+
const isActive = () => activeRunIdRef.current === runId;
|
|
1349
|
+
const retryController = new AbortController();
|
|
1350
|
+
resetEscapeStop();
|
|
1351
|
+
queuePausedRef.current = false;
|
|
1352
|
+
abortControllerRef.current = retryController;
|
|
1353
|
+
await workspaceCoordinator.setActivity('running');
|
|
1354
|
+
try {
|
|
1355
|
+
await runWithRetry(last.messages, last.agent, true, isActive, retryController.signal);
|
|
1356
|
+
}
|
|
1357
|
+
catch (err) {
|
|
1358
|
+
const message = err.message || 'Agent error';
|
|
1359
|
+
if (!isActive())
|
|
1360
|
+
return;
|
|
1361
|
+
addMessage({ role: 'system', content: `Error: ${message}` });
|
|
1362
|
+
if (isRetryableError(message)) {
|
|
1363
|
+
addMessage({ role: 'system', content: 'Retries exhausted. Use /retry to try again.' });
|
|
1364
|
+
setRetryAvailable(true);
|
|
1365
|
+
}
|
|
1366
|
+
setAgentState({ status: 'error', error: message });
|
|
1367
|
+
}
|
|
1368
|
+
finally {
|
|
1369
|
+
if (abortControllerRef.current === retryController) {
|
|
1370
|
+
abortControllerRef.current = null;
|
|
1371
|
+
resetEscapeStop();
|
|
1372
|
+
}
|
|
1373
|
+
if (isActive() && !workspaceClosedRef.current && !workspacePausedRef.current)
|
|
1374
|
+
await workspaceCoordinator.setActivity('idle');
|
|
1375
|
+
if (isActive()) {
|
|
1376
|
+
activeRunIdRef.current = null;
|
|
1377
|
+
presentAgentQuestion(runId);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
void replay().catch(error => {
|
|
1382
|
+
if (!workspaceClosedRef.current)
|
|
1383
|
+
addMessage({ role: 'system', content: `Unable to retry: ${error instanceof Error ? error.message : String(error)}` });
|
|
641
1384
|
});
|
|
642
|
-
}, [addMessage, isRetryableError, runWithRetry, setAgentState]);
|
|
1385
|
+
}, [addMessage, isRetryableError, resetEscapeStop, runWithRetry, setAgentState, admitWorkspace, workspaceCoordinator, flushReadingDisplay, presentAgentQuestion]);
|
|
643
1386
|
// Prompt controller for interactive commands
|
|
644
1387
|
const promptController = useMemo(() => ({
|
|
645
|
-
prompt: (message, callback) => {
|
|
646
|
-
|
|
1388
|
+
prompt: (message, callback, options) => {
|
|
1389
|
+
retireRouting();
|
|
1390
|
+
pendingPromptRef.current = { message, callback, isPassword: false, allowEmptySubmit: options?.allowEmptySubmit };
|
|
647
1391
|
addMessage({ role: 'system', content: message });
|
|
648
1392
|
},
|
|
649
1393
|
promptPassword: (message, callback) => {
|
|
1394
|
+
retireRouting();
|
|
650
1395
|
pendingPromptRef.current = { message, callback, isPassword: true };
|
|
651
1396
|
addMessage({ role: 'system', content: message });
|
|
652
1397
|
},
|
|
653
1398
|
hasPending: () => pendingPromptRef.current !== null,
|
|
654
1399
|
getPending: () => pendingPromptRef.current,
|
|
655
|
-
clearPending: () => {
|
|
656
|
-
|
|
1400
|
+
clearPending: () => {
|
|
1401
|
+
const passwordDismissed = pendingPromptRef.current?.isPassword === true;
|
|
1402
|
+
pendingPromptRef.current = null;
|
|
1403
|
+
if (passwordDismissed)
|
|
1404
|
+
setClearDraftVersion(version => version + 1);
|
|
1405
|
+
},
|
|
1406
|
+
}), [addMessage, retireRouting]);
|
|
657
1407
|
// Form controller for inline form widgets
|
|
658
1408
|
const formController = useMemo(() => ({
|
|
659
1409
|
showForm: (options, callback) => {
|
|
1410
|
+
retireRouting();
|
|
660
1411
|
const formId = generateId();
|
|
661
1412
|
formCallbacksRef.current.set(formId, callback);
|
|
662
1413
|
setMessages(prev => [...prev, {
|
|
@@ -676,7 +1427,7 @@ export const App = () => {
|
|
|
676
1427
|
cancelForm: (formId) => {
|
|
677
1428
|
formCallbacksRef.current.delete(formId);
|
|
678
1429
|
}
|
|
679
|
-
}), []);
|
|
1430
|
+
}), [retireRouting, setMessages]);
|
|
680
1431
|
// Handle form submission from FormWidget
|
|
681
1432
|
const handleFormSubmit = useCallback(async (formId, values) => {
|
|
682
1433
|
const callback = formCallbacksRef.current.get(formId);
|
|
@@ -700,19 +1451,44 @@ export const App = () => {
|
|
|
700
1451
|
const appContext = useMemo(() => ({
|
|
701
1452
|
addMessage,
|
|
702
1453
|
clearMessages,
|
|
1454
|
+
getEvidenceSnapshot: () => agent?.getEvidenceSnapshot(),
|
|
1455
|
+
getDiagnostics: () => {
|
|
1456
|
+
const snapshot = captureSessionDiagnosticSnapshot({ messages: messagesRef.current, diagnostics: agent?.getDiagnosticSnapshot() });
|
|
1457
|
+
agent?.restoreDiagnosticSnapshot(snapshot);
|
|
1458
|
+
return snapshot;
|
|
1459
|
+
},
|
|
1460
|
+
clearTransientState: () => {
|
|
1461
|
+
agent?.clearPinnedTask();
|
|
1462
|
+
if (agent) {
|
|
1463
|
+
agent.toolResponseStore.evidence.clear();
|
|
1464
|
+
const snapshot = agent.getEvidenceSnapshot();
|
|
1465
|
+
setAgentState(previous => applyAgentEventToState(previous, { type: 'evidence', sessionId: previous.evidence?.sessionId ?? sessionId, snapshot }));
|
|
1466
|
+
}
|
|
1467
|
+
},
|
|
703
1468
|
getMessages,
|
|
704
1469
|
setMessages,
|
|
705
1470
|
setAgentState,
|
|
706
1471
|
reloadAgent,
|
|
1472
|
+
applyAccountOperation,
|
|
1473
|
+
getUsageSnapshot,
|
|
707
1474
|
setDebug,
|
|
708
1475
|
retry: retryLast,
|
|
709
1476
|
exit,
|
|
1477
|
+
getSessionId: () => sessionId,
|
|
1478
|
+
getSessionName: () => sessionName,
|
|
1479
|
+
setSessionName,
|
|
1480
|
+
setPaneLabels: async (labels) => { setSessionName(labels.manual_title); },
|
|
710
1481
|
config: configStore,
|
|
711
1482
|
emulation: {
|
|
712
1483
|
list: loadEmulationProfiles,
|
|
713
1484
|
get: getEmulationProfile
|
|
714
1485
|
},
|
|
715
1486
|
shell: shellController,
|
|
1487
|
+
workspace: {
|
|
1488
|
+
getSnapshot: refreshWorkspace,
|
|
1489
|
+
continue: continueWorkspace,
|
|
1490
|
+
isolate: async () => { await isolateWorkspace(); },
|
|
1491
|
+
},
|
|
716
1492
|
clipboard: {
|
|
717
1493
|
writeText: writeClipboard
|
|
718
1494
|
},
|
|
@@ -745,11 +1521,21 @@ export const App = () => {
|
|
|
745
1521
|
cancelSelect: () => { },
|
|
746
1522
|
},
|
|
747
1523
|
questionnaire: {
|
|
748
|
-
showQuestionnaire: () => {
|
|
749
|
-
|
|
1524
|
+
showQuestionnaire: (options, callback) => {
|
|
1525
|
+
retireRouting();
|
|
1526
|
+
// Ink subscribers share the current key dispatch; the opening Enter belongs to the composer.
|
|
1527
|
+
questionnaireInputReadyRef.current = false;
|
|
1528
|
+
questionnaireWidget.show({ ...options, callback });
|
|
1529
|
+
queueMicrotask(() => { questionnaireInputReadyRef.current = true; });
|
|
1530
|
+
},
|
|
1531
|
+
cancelQuestionnaire: () => {
|
|
1532
|
+
questionnaireInputReadyRef.current = false;
|
|
1533
|
+
questionnaireWidget.cancel();
|
|
1534
|
+
renderQuestionnaireWidget(version => version + 1);
|
|
1535
|
+
},
|
|
750
1536
|
},
|
|
751
1537
|
getInputMode: () => inputMode,
|
|
752
|
-
setInputMode: (mode) => setInputMode(mode),
|
|
1538
|
+
setInputMode: (mode) => setInputMode(normalizeInputMode(mode)),
|
|
753
1539
|
triggerAgentTurn: () => { },
|
|
754
1540
|
// Authoritative context getters for /context + /compact: the real per-call
|
|
755
1541
|
// provider count + window (read live via the ref so commands never see a
|
|
@@ -763,10 +1549,15 @@ export const App = () => {
|
|
|
763
1549
|
cacheInputTokens: undefined, cachedInputTokens: undefined,
|
|
764
1550
|
})),
|
|
765
1551
|
model: configStore.get().model
|
|
766
|
-
}), [addMessage, clearMessages, getMessages, setMessages, reloadAgent, exit, shellController, retryLast, inputMode, promptController, formController]);
|
|
1552
|
+
}), [addMessage, clearMessages, getMessages, setMessages, reloadAgent, exit, shellController, retryLast, inputMode, promptController, formController, questionnaireWidget, retireRouting, agent, sessionId, sessionName, setAgentState, refreshWorkspace, continueWorkspace, isolateWorkspace]);
|
|
767
1553
|
// Handle commands
|
|
768
1554
|
const handleCommand = useCallback((cmd, args) => {
|
|
769
1555
|
const normalizedCommand = cmd.toLowerCase();
|
|
1556
|
+
if (normalizedCommand === 'model' && (runtimeMutationRef.current || abortControllerRef.current
|
|
1557
|
+
|| (agentStateRef.current.status !== 'idle' && agentStateRef.current.status !== 'error'))) {
|
|
1558
|
+
addMessage({ role: 'system', content: 'Cannot switch model while an active agent turn or account change is running.' });
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
770
1561
|
let resolvedPlanMode = false;
|
|
771
1562
|
if (normalizedCommand === 'mode' && args[0]) {
|
|
772
1563
|
try {
|
|
@@ -804,13 +1595,93 @@ export const App = () => {
|
|
|
804
1595
|
});
|
|
805
1596
|
}
|
|
806
1597
|
}, [appContext, addMessage]);
|
|
1598
|
+
const routeBusyInput = useCallback((text, draft) => {
|
|
1599
|
+
if (routingDraftsRef.current.size + queueRef.current.length >= MAX_QUEUE) {
|
|
1600
|
+
showToastRef.current?.(`Queue full (${MAX_QUEUE}). Cancel or wait.`);
|
|
1601
|
+
// InputArea clears after this callback, so restoration waits for that
|
|
1602
|
+
// submission's empty-composer revision.
|
|
1603
|
+
queueMicrotask(() => draft?.restore());
|
|
1604
|
+
return Promise.resolve();
|
|
1605
|
+
}
|
|
1606
|
+
const submitted = { id: generateId(), text, queuedAt: new Date(), delivery: 'subturn' };
|
|
1607
|
+
const epoch = routingEpochRef.current;
|
|
1608
|
+
const controller = new AbortController();
|
|
1609
|
+
const config = structuredClone(configStore.get());
|
|
1610
|
+
const backendOptions = { config, provider: configStore.getProvider(), model: configStore.getModel(),
|
|
1611
|
+
openaiCompatibleBaseUrl: configStore.getOpenAICompatibleBaseUrl(), emulationId: configStore.getEmulationId(),
|
|
1612
|
+
mode: agentMode, cwd: shellCwdRef.current, sessionId, allowSelfPlanTransition: false };
|
|
1613
|
+
const currentTask = lastRequestRef.current?.messages.filter(message => message.role === 'user'
|
|
1614
|
+
&& !message.metadata?.toolResult).at(-1)?.content ?? text;
|
|
1615
|
+
getUsageSnapshot();
|
|
1616
|
+
const routingUsage = retainedUsageRef.current;
|
|
1617
|
+
routingControllersRef.current.set(submitted.id, controller);
|
|
1618
|
+
routingDraftsRef.current.set(submitted.id, draft);
|
|
1619
|
+
const owns = () => routingEpochRef.current === epoch && !controller.signal.aborted;
|
|
1620
|
+
// Serial decisions preserve submission FIFO even when a parent completes
|
|
1621
|
+
// or the first decision needs an explicit choice.
|
|
1622
|
+
const routed = routingTailRef.current.then(async () => {
|
|
1623
|
+
if (!owns())
|
|
1624
|
+
return;
|
|
1625
|
+
const decision = await classifyBusyInput({ backendOptions, currentTask, inputText: text,
|
|
1626
|
+
hosted: false, signal: controller.signal, onUsage: receipt => {
|
|
1627
|
+
routingUsage.snapshot = recordUsageReceipt(routingUsage.snapshot, receipt);
|
|
1628
|
+
if (sessionIdRef.current === receipt.sessionId)
|
|
1629
|
+
setRoutingUsageRevision(revision => revision + 1);
|
|
1630
|
+
} });
|
|
1631
|
+
if (!owns())
|
|
1632
|
+
return;
|
|
1633
|
+
if (pendingPromptRef.current || formCallbacksRef.current.size > 0 || questionnaireWidget.isActive()) {
|
|
1634
|
+
draft?.restore();
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (decision.route === 'divergent') {
|
|
1638
|
+
const choice = await new Promise(resolve => {
|
|
1639
|
+
routingChoiceCancelRef.current = () => resolve(null);
|
|
1640
|
+
scopeWidget.show({ title: 'Separate task', question: 'This input appears to start different work.', type: 'select',
|
|
1641
|
+
options: [{ value: 'pane', label: 'New pane' }, { value: 'session', label: 'New session' },
|
|
1642
|
+
{ value: 'steer', label: 'Steer anyway' }, { value: 'cancel', label: 'Cancel' }],
|
|
1643
|
+
callback: value => { routingChoiceCancelRef.current = null; resolve(typeof value === 'string' ? value : null); } });
|
|
1644
|
+
});
|
|
1645
|
+
if (!owns())
|
|
1646
|
+
return;
|
|
1647
|
+
if (choice !== 'steer') {
|
|
1648
|
+
if (choice === 'pane' || choice === 'session')
|
|
1649
|
+
addMessage({ role: 'system', content: `The Ink renderer cannot open a ${choice === 'pane' ? 'new pane' : 'new session'}. Use the Ratatui renderer (run ./scripts/build-rust.sh), or start a separate ZTC terminal. Your unsent draft is retained.` });
|
|
1650
|
+
const restored = draft?.restore();
|
|
1651
|
+
if (draft && !restored)
|
|
1652
|
+
addMessage({ role: 'system', content: 'Your newer composer draft was kept. The earlier separate-task input was not sent.' });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
if (enqueueMessage(text, 'subturn', submitted) === null)
|
|
1657
|
+
draft?.restore();
|
|
1658
|
+
else
|
|
1659
|
+
showToastRef.current?.('Steering current task · Sending after the current tool finishes');
|
|
1660
|
+
}).catch(error => {
|
|
1661
|
+
if (!owns())
|
|
1662
|
+
return;
|
|
1663
|
+
draft?.restore();
|
|
1664
|
+
addMessage({ role: 'system', content: `Unable to route this input: ${error instanceof Error ? error.message : String(error)}` });
|
|
1665
|
+
}).finally(() => {
|
|
1666
|
+
routingControllersRef.current.delete(submitted.id);
|
|
1667
|
+
routingDraftsRef.current.delete(submitted.id);
|
|
1668
|
+
});
|
|
1669
|
+
routingTailRef.current = routed;
|
|
1670
|
+
return routed;
|
|
1671
|
+
}, [addMessage, agentMode, enqueueMessage, scopeWidget, questionnaireWidget, getUsageSnapshot, sessionId]);
|
|
807
1672
|
// Handle message submission
|
|
808
|
-
const handleSubmit = useCallback(async (text) => {
|
|
1673
|
+
const handleSubmit = useCallback(async (text, draft, queuedEntry, askUserAnswer = false) => {
|
|
809
1674
|
// Reject empty / whitespace-only prompts before any downstream work.
|
|
810
1675
|
// Interactive pending prompts (password, etc.) may legitimately want
|
|
811
1676
|
// blank input, so bypass this check when one is pending.
|
|
812
1677
|
if (!pendingPromptRef.current && !isMeaningfulUserInput(text))
|
|
813
1678
|
return;
|
|
1679
|
+
if (runtimeMutationRef.current && !pendingPromptRef.current) {
|
|
1680
|
+
const position = enqueueMessage(text, 'end-of-turn');
|
|
1681
|
+
addMessage({ role: 'system', content: position === null ? 'Account change in progress; the queue is full. Retry when it finishes.'
|
|
1682
|
+
: 'Account change in progress; input retained in the queue.' });
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
814
1685
|
// Check for pending prompts first (interactive command flow)
|
|
815
1686
|
const pending = pendingPromptRef.current;
|
|
816
1687
|
if (pending) {
|
|
@@ -831,7 +1702,7 @@ export const App = () => {
|
|
|
831
1702
|
}
|
|
832
1703
|
return;
|
|
833
1704
|
}
|
|
834
|
-
if (text.startsWith('!')) {
|
|
1705
|
+
if (!askUserAnswer && text.startsWith('!')) {
|
|
835
1706
|
const commandText = text.slice(1).trim();
|
|
836
1707
|
if (commandText) {
|
|
837
1708
|
// Intercept 'cd' commands to use the proper /cd handler that persists
|
|
@@ -855,22 +1726,48 @@ export const App = () => {
|
|
|
855
1726
|
});
|
|
856
1727
|
return;
|
|
857
1728
|
}
|
|
858
|
-
const
|
|
1729
|
+
const admission = await admitWorkspace();
|
|
1730
|
+
if (!admission.allowed) {
|
|
1731
|
+
if (!workspaceClosedRef.current) {
|
|
1732
|
+
if (!pausedWorkspaceInputRef.current)
|
|
1733
|
+
pausedWorkspaceInputRef.current = {
|
|
1734
|
+
text, draft, queuedEntry, askUserAnswer, fingerprint: admission.snapshot.fingerprint,
|
|
1735
|
+
};
|
|
1736
|
+
if (admission.isolate && await isolateWorkspace())
|
|
1737
|
+
return;
|
|
1738
|
+
draft?.restore();
|
|
1739
|
+
addMessage({ role: 'system', content: 'Workspace session paused. Input and queued work retained; use /sessions continue.' });
|
|
1740
|
+
}
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
const busy = abortControllerRef.current !== null
|
|
1744
|
+
|| (agentStateRef.current.status !== 'idle' && agentStateRef.current.status !== 'error');
|
|
1745
|
+
flushReadingDisplay();
|
|
1746
|
+
if ((busy || routingDraftsRef.current.size > 0) && inputMode === 'steer' && !queuedEntry) {
|
|
1747
|
+
await routeBusyInput(text, draft);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
859
1750
|
if (busy) {
|
|
860
|
-
if (inputMode === '
|
|
861
|
-
const nextSize = enqueueMessage(text);
|
|
1751
|
+
if (inputMode === 'steer') {
|
|
1752
|
+
const nextSize = enqueueMessage(text, 'subturn');
|
|
862
1753
|
if (nextSize !== null) {
|
|
863
|
-
|
|
864
|
-
addMessage({ role: 'system', content: `Queued (${nextSize})` });
|
|
1754
|
+
showToastRef.current?.('Steering current task · Sending after the current tool finishes');
|
|
865
1755
|
}
|
|
866
1756
|
return;
|
|
867
1757
|
}
|
|
1758
|
+
// Retire the prior event owner before aborting it, so late output and
|
|
1759
|
+
// checkpoints cannot overwrite the replacement turn.
|
|
1760
|
+
cancelGeneration();
|
|
868
1761
|
addMessage({
|
|
869
1762
|
role: 'system',
|
|
870
1763
|
content: 'Interrupting current response...'
|
|
871
1764
|
});
|
|
872
1765
|
}
|
|
873
|
-
|
|
1766
|
+
// Agent events do not carry a run owner. An aborted HTTP body can finish
|
|
1767
|
+
// after its run promise settles, so a replacement must not subscribe to
|
|
1768
|
+
// that retired Agent's emitter.
|
|
1769
|
+
const liveAgent = diagnosticAgentRef.current;
|
|
1770
|
+
let currentAgent = liveAgent && !retiredAgentsRef.current.has(liveAgent) ? liveAgent : null;
|
|
874
1771
|
if (skills.length > 0) {
|
|
875
1772
|
const activated = autoActivateSkills(text, skills);
|
|
876
1773
|
const ids = activated.map(skill => skill.id).sort();
|
|
@@ -880,15 +1777,7 @@ export const App = () => {
|
|
|
880
1777
|
activeSkillIds.current = ids;
|
|
881
1778
|
const prompt = buildSkillPrompt(activated);
|
|
882
1779
|
// Let the resolver apply config-key precedence (no env-first getApiKey()).
|
|
883
|
-
const nextAgent =
|
|
884
|
-
config: configStore.get(),
|
|
885
|
-
provider: configStore.getProvider(),
|
|
886
|
-
model: configStore.getModel(),
|
|
887
|
-
openaiCompatibleBaseUrl: configStore.getOpenAICompatibleBaseUrl(),
|
|
888
|
-
emulationId: configStore.getEmulationId(),
|
|
889
|
-
skillPrompt: prompt,
|
|
890
|
-
allowSelfPlanTransition: false,
|
|
891
|
-
});
|
|
1780
|
+
const nextAgent = createBoundAgent(agentMode, prompt);
|
|
892
1781
|
if (nextAgent) {
|
|
893
1782
|
setAgent(nextAgent);
|
|
894
1783
|
currentAgent = nextAgent;
|
|
@@ -902,7 +1791,7 @@ export const App = () => {
|
|
|
902
1791
|
}
|
|
903
1792
|
}
|
|
904
1793
|
if (!currentAgent) {
|
|
905
|
-
currentAgent =
|
|
1794
|
+
currentAgent = createBoundAgent(agentMode, buildSkillPrompt(skills.filter(skill => activeSkillIds.current.includes(skill.id))));
|
|
906
1795
|
setAgent(currentAgent);
|
|
907
1796
|
}
|
|
908
1797
|
if (!currentAgent) {
|
|
@@ -913,18 +1802,28 @@ export const App = () => {
|
|
|
913
1802
|
return;
|
|
914
1803
|
}
|
|
915
1804
|
const userMsg = {
|
|
916
|
-
id: generateId(),
|
|
1805
|
+
id: queuedEntry?.id ?? generateId(),
|
|
917
1806
|
role: 'user',
|
|
918
1807
|
content: text,
|
|
919
|
-
timestamp: new Date()
|
|
1808
|
+
timestamp: queuedEntry?.queuedAt ?? new Date(),
|
|
1809
|
+
...(askUserAnswer ? { metadata: { isAskUserAnswer: true } } : {}),
|
|
920
1810
|
};
|
|
921
|
-
setMessages(prev => [...prev, userMsg]);
|
|
922
1811
|
const requestMessages = [...messagesRef.current, userMsg];
|
|
923
|
-
|
|
1812
|
+
setMessages(prev => [...prev, userMsg]);
|
|
1813
|
+
const promptTurn = new PromptTurn(userMsg.id, () => {
|
|
1814
|
+
const snapshot = promptTurn.snapshot();
|
|
1815
|
+
setMessages(prev => prev.map(message => message.turn?.id === snapshot.id ? { ...message, turn: snapshot } : message));
|
|
1816
|
+
});
|
|
1817
|
+
promptTurnRef.current = promptTurn;
|
|
1818
|
+
lastRequestRef.current = { messages: requestMessages, agent: currentAgent, turn: promptTurn };
|
|
924
1819
|
const runId = `${Date.now()}_${runCounterRef.current++}`;
|
|
925
1820
|
activeRunIdRef.current = runId;
|
|
1821
|
+
resetAskUserTurnFlag();
|
|
926
1822
|
const controller = new AbortController();
|
|
1823
|
+
resetEscapeStop();
|
|
1824
|
+
queuePausedRef.current = false;
|
|
927
1825
|
abortControllerRef.current = controller;
|
|
1826
|
+
await workspaceCoordinator.setActivity('running');
|
|
928
1827
|
const isActive = () => activeRunIdRef.current === runId;
|
|
929
1828
|
const runStartedAt = Date.now();
|
|
930
1829
|
streamingMessageId.current = null;
|
|
@@ -933,6 +1832,12 @@ export const App = () => {
|
|
|
933
1832
|
if (!isActive())
|
|
934
1833
|
return;
|
|
935
1834
|
switch (event.type) {
|
|
1835
|
+
case 'evidence':
|
|
1836
|
+
setAgentState(previous => applyAgentEventToState(previous, event));
|
|
1837
|
+
break;
|
|
1838
|
+
case 'command_wait':
|
|
1839
|
+
case 'command_wait_end':
|
|
1840
|
+
case 'tool_progress':
|
|
936
1841
|
case 'thinking_start':
|
|
937
1842
|
setAgentState(s => applyAgentEventToState(s, event));
|
|
938
1843
|
break;
|
|
@@ -954,13 +1859,17 @@ export const App = () => {
|
|
|
954
1859
|
setAgentState(s => applyAgentEventToState(s, event));
|
|
955
1860
|
if (!streamingMessageId.current) {
|
|
956
1861
|
const id = generateId();
|
|
1862
|
+
// Register the owned stream before React can batch its completion
|
|
1863
|
+
// into the same paint. Historical settled messages are never typed.
|
|
1864
|
+
readingStatesRef.current.set(id, { ...initialStreamTypewriterState(), messageId: id });
|
|
957
1865
|
streamingMessageId.current = id;
|
|
958
1866
|
setMessages(prev => [...prev, {
|
|
959
1867
|
id,
|
|
960
1868
|
role: 'assistant',
|
|
961
1869
|
content: '',
|
|
962
1870
|
timestamp: new Date(),
|
|
963
|
-
isStreaming: true
|
|
1871
|
+
isStreaming: true,
|
|
1872
|
+
turn: promptTurn.snapshot(),
|
|
964
1873
|
}]);
|
|
965
1874
|
}
|
|
966
1875
|
break;
|
|
@@ -983,6 +1892,7 @@ export const App = () => {
|
|
|
983
1892
|
});
|
|
984
1893
|
break;
|
|
985
1894
|
case 'stream_end':
|
|
1895
|
+
setAgentState(s => applyAgentEventToState(s, event));
|
|
986
1896
|
if (streamingMessageId.current) {
|
|
987
1897
|
// Get the streaming message to speak it
|
|
988
1898
|
const streamingMsgId = streamingMessageId.current;
|
|
@@ -1007,6 +1917,7 @@ export const App = () => {
|
|
|
1007
1917
|
streamingMessageId.current = null;
|
|
1008
1918
|
break;
|
|
1009
1919
|
case 'tool_end': {
|
|
1920
|
+
setAgentState(s => applyAgentEventToState(s, event));
|
|
1010
1921
|
const key = event.tool;
|
|
1011
1922
|
const bucket = toolStartTimes.current.get(key) || [];
|
|
1012
1923
|
const started = bucket.shift();
|
|
@@ -1024,6 +1935,7 @@ export const App = () => {
|
|
|
1024
1935
|
break;
|
|
1025
1936
|
}
|
|
1026
1937
|
case 'tool_error': {
|
|
1938
|
+
setAgentState(s => applyAgentEventToState(s, event));
|
|
1027
1939
|
addMessage({
|
|
1028
1940
|
role: 'tool',
|
|
1029
1941
|
content: formatToolError(event.tool, event.error, configStore.getEmulationId())
|
|
@@ -1047,9 +1959,10 @@ export const App = () => {
|
|
|
1047
1959
|
}));
|
|
1048
1960
|
break;
|
|
1049
1961
|
case 'api_retry':
|
|
1962
|
+
setAgentState(s => applyAgentEventToState(s, event));
|
|
1050
1963
|
addMessage({
|
|
1051
1964
|
role: 'system',
|
|
1052
|
-
content:
|
|
1965
|
+
content: formatApiRetryNotice(event)
|
|
1053
1966
|
});
|
|
1054
1967
|
break;
|
|
1055
1968
|
case 'error':
|
|
@@ -1058,7 +1971,17 @@ export const App = () => {
|
|
|
1058
1971
|
}
|
|
1059
1972
|
});
|
|
1060
1973
|
try {
|
|
1061
|
-
await runWithRetry(requestMessages, currentAgent, false, isActive, controller.signal)
|
|
1974
|
+
await runWithRetry(requestMessages, currentAgent, false, isActive, controller.signal, () => {
|
|
1975
|
+
if (!isActive())
|
|
1976
|
+
return [];
|
|
1977
|
+
const pending = queueRef.current.filter(entry => entry.delivery === 'subturn');
|
|
1978
|
+
queueRef.current = queueRef.current.filter(entry => entry.delivery !== 'subturn');
|
|
1979
|
+
setQueue(queueRef.current);
|
|
1980
|
+
// These messages become visible through the real history checkpoint
|
|
1981
|
+
// when the Agent admits them after the completed tool result.
|
|
1982
|
+
return pending.map(entry => ({ id: entry.id, role: 'user', content: entry.text,
|
|
1983
|
+
timestamp: entry.queuedAt, attachments: entry.attachments, metadata: { steering: true } }));
|
|
1984
|
+
}, promptTurn);
|
|
1062
1985
|
}
|
|
1063
1986
|
catch (err) {
|
|
1064
1987
|
if ((err instanceof AgentError && err.code === 'CANCELLED') || controller.signal.aborted)
|
|
@@ -1098,6 +2021,7 @@ export const App = () => {
|
|
|
1098
2021
|
activeRunIdRef.current = null;
|
|
1099
2022
|
abortControllerRef.current = null;
|
|
1100
2023
|
lastRunDurationRef.current = Date.now() - runStartedAt;
|
|
2024
|
+
resetEscapeStop();
|
|
1101
2025
|
if (getTraceStyle(configStore.getEmulationId()) === 'codex') {
|
|
1102
2026
|
const seconds = Math.max(0, Math.round((lastRunDurationRef.current || 0) / 1000));
|
|
1103
2027
|
addMessage({
|
|
@@ -1106,6 +2030,9 @@ export const App = () => {
|
|
|
1106
2030
|
});
|
|
1107
2031
|
}
|
|
1108
2032
|
cleanup();
|
|
2033
|
+
if (!workspaceClosedRef.current && !workspacePausedRef.current)
|
|
2034
|
+
await workspaceCoordinator.setActivity('idle');
|
|
2035
|
+
presentAgentQuestion(runId);
|
|
1109
2036
|
// Queue flushing is handled by the flush-on-idle effect below; shifting
|
|
1110
2037
|
// here would strand the queue on cancel/error paths.
|
|
1111
2038
|
}
|
|
@@ -1113,7 +2040,8 @@ export const App = () => {
|
|
|
1113
2040
|
cleanup();
|
|
1114
2041
|
}
|
|
1115
2042
|
}
|
|
1116
|
-
}, [agent, agentState.status, addMessage, handleCommand, enqueueMessage, inputMode, isRetryableError, runWithRetry]);
|
|
2043
|
+
}, [agent, agentMode, skills, agentState.status, addMessage, handleCommand, enqueueMessage, inputMode, isRetryableError, runWithRetry, cancelGeneration, resetEscapeStop, routeBusyInput, admitWorkspace, workspaceFactoryOptions, createBoundAgent, workspaceCoordinator, isolateWorkspace, flushReadingDisplay, presentAgentQuestion]);
|
|
2044
|
+
workspaceSubmitRef.current = handleSubmit;
|
|
1117
2045
|
// Flush-on-idle: one dequeue per idle-edge, uniform across cancel/error.
|
|
1118
2046
|
// Latch is set BEFORE dequeue because setQueue's notify is synchronous —
|
|
1119
2047
|
// otherwise this effect would re-enter itself and dequeue twice.
|
|
@@ -1125,35 +2053,41 @@ export const App = () => {
|
|
|
1125
2053
|
// must still be sent when the run is configured to interrupt on Enter.
|
|
1126
2054
|
if (agentState.status !== 'idle')
|
|
1127
2055
|
return;
|
|
2056
|
+
if (abortControllerRef.current !== null)
|
|
2057
|
+
return;
|
|
2058
|
+
if (queuePausedRef.current || workspacePausedRef.current || agentQuestionRef.current || pendingPromptRef.current || formCallbacksRef.current.size > 0 || questionnaireWidget.isActive() || scopeWidget.isActive() || workspaceWidget.isActive())
|
|
2059
|
+
return;
|
|
1128
2060
|
if (prevStatus === 'idle' && queue.length === 0)
|
|
1129
2061
|
return;
|
|
1130
2062
|
if (queue.length === 0)
|
|
1131
2063
|
return;
|
|
1132
2064
|
if (flushingRef.current)
|
|
1133
2065
|
return;
|
|
1134
|
-
// End-of-turn entries only; a `subturn` entry still waiting here is started
|
|
1135
|
-
// by its own fallback path, so taking it here would send it twice.
|
|
1136
|
-
const ready = queueRef.current.find(entry => entry.delivery !== 'subturn');
|
|
1137
|
-
if (!ready)
|
|
1138
|
-
return;
|
|
1139
2066
|
flushingRef.current = true;
|
|
2067
|
+
setFlushing(true);
|
|
1140
2068
|
const next = dequeueMessage();
|
|
1141
2069
|
if (!next) {
|
|
1142
2070
|
flushingRef.current = false;
|
|
2071
|
+
setFlushing(false);
|
|
1143
2072
|
return;
|
|
1144
2073
|
}
|
|
1145
|
-
void handleSubmit(next.text).finally(() => {
|
|
2074
|
+
void handleSubmit(next.text, undefined, next).finally(() => {
|
|
1146
2075
|
flushingRef.current = false;
|
|
2076
|
+
setFlushing(false);
|
|
1147
2077
|
});
|
|
1148
|
-
}, [agentState.status, queue.length, dequeueMessage, handleSubmit]);
|
|
1149
|
-
const showToast = useCallback((message) => {
|
|
2078
|
+
}, [agentState.status, queue.length, flushing, dequeueMessage, handleSubmit, scopeWidget, workspaceWidget, questionnaireWidget, questionnaireVersion, workspaceResumeVersion]);
|
|
2079
|
+
const showToast = useCallback((message, durationMs = DEFAULT_TOAST_MS) => {
|
|
1150
2080
|
setToast(message);
|
|
1151
2081
|
if (toastTimerRef.current) {
|
|
1152
2082
|
clearTimeout(toastTimerRef.current);
|
|
1153
2083
|
}
|
|
1154
|
-
|
|
2084
|
+
const timer = setTimeout(() => {
|
|
2085
|
+
if (toastTimerRef.current !== timer)
|
|
2086
|
+
return;
|
|
2087
|
+
toastTimerRef.current = null;
|
|
1155
2088
|
setToast(null);
|
|
1156
|
-
},
|
|
2089
|
+
}, durationMs);
|
|
2090
|
+
toastTimerRef.current = timer;
|
|
1157
2091
|
}, []);
|
|
1158
2092
|
// Wire showToast into the ref so enqueue (which is declared before showToast)
|
|
1159
2093
|
// can surface "Queue full" notices without creating a dependency cycle.
|
|
@@ -1170,7 +2104,7 @@ export const App = () => {
|
|
|
1170
2104
|
if (skippedPlan)
|
|
1171
2105
|
next = getNextMode(next);
|
|
1172
2106
|
setAgentMode(next);
|
|
1173
|
-
setAgent(
|
|
2107
|
+
setAgent(createBoundAgent(next));
|
|
1174
2108
|
const label = getModeConfig(next).label;
|
|
1175
2109
|
// The status line's mode field already reads out the active mode, so this
|
|
1176
2110
|
// fallback only speaks up when it had to skip Plan.
|
|
@@ -1181,7 +2115,7 @@ export const App = () => {
|
|
|
1181
2115
|
catch (error) {
|
|
1182
2116
|
showToast(error instanceof Error ? error.message : String(error));
|
|
1183
2117
|
}
|
|
1184
|
-
}, [agentMode, showToast]);
|
|
2118
|
+
}, [agentMode, showToast, createBoundAgent]);
|
|
1185
2119
|
const handleDictationStateChange = useCallback((state) => {
|
|
1186
2120
|
setIsRecording(state === 'recording');
|
|
1187
2121
|
setIsTranscribing(state === 'transcribing');
|
|
@@ -1225,29 +2159,92 @@ export const App = () => {
|
|
|
1225
2159
|
});
|
|
1226
2160
|
return cleanup;
|
|
1227
2161
|
}, [useNativeDictation, showToast]);
|
|
2162
|
+
// Reveal is a display projection; canonical messages and provider/session
|
|
2163
|
+
// history always retain the full received body and its actual completion.
|
|
2164
|
+
const { displayMessages, readingPending } = useMemo(() => {
|
|
2165
|
+
if (readingSessionRef.current !== sessionId) {
|
|
2166
|
+
readingStatesRef.current.clear();
|
|
2167
|
+
bypassedReadingIdsRef.current.clear();
|
|
2168
|
+
readingSessionRef.current = sessionId;
|
|
2169
|
+
}
|
|
2170
|
+
const ids = new Set(messages.map(message => message.id));
|
|
2171
|
+
for (const id of readingStatesRef.current.keys())
|
|
2172
|
+
if (!ids.has(id))
|
|
2173
|
+
readingStatesRef.current.delete(id);
|
|
2174
|
+
for (const id of bypassedReadingIdsRef.current)
|
|
2175
|
+
if (!ids.has(id))
|
|
2176
|
+
bypassedReadingIdsRef.current.delete(id);
|
|
2177
|
+
let pending = false;
|
|
2178
|
+
const shown = messages.map(message => {
|
|
2179
|
+
const eligible = message.role === 'assistant' && message.metadata?.reasoning !== true
|
|
2180
|
+
&& message.metadata?.toolResult !== true && !message.metadata?.toolOutput
|
|
2181
|
+
&& !message.toolCalls?.length && !message.widget;
|
|
2182
|
+
const previous = readingStatesRef.current.get(message.id);
|
|
2183
|
+
if (!eligible || bypassedReadingIdsRef.current.has(message.id)
|
|
2184
|
+
|| (!message.isStreaming && !previous))
|
|
2185
|
+
return message;
|
|
2186
|
+
const plan = planStreamTypewriterSample(previous ?? initialStreamTypewriterState(), message, {
|
|
2187
|
+
enabled: messageDisplay.smoothStreaming || messageDisplay.proseReadingRateWordsPerSecond !== null,
|
|
2188
|
+
now: Date.now(), proseReadingRateWordsPerSecond: messageDisplay.proseReadingRateWordsPerSecond,
|
|
2189
|
+
});
|
|
2190
|
+
readingStatesRef.current.set(message.id, plan.nextState);
|
|
2191
|
+
pending ||= plan.pending;
|
|
2192
|
+
return plan.content === message.content && !plan.pending ? message
|
|
2193
|
+
: { ...message, content: plan.content, isStreaming: message.isStreaming || plan.pending };
|
|
2194
|
+
});
|
|
2195
|
+
return { displayMessages: shown, readingPending: pending };
|
|
2196
|
+
}, [messages, sessionId, messageDisplay.smoothStreaming, messageDisplay.proseReadingRateWordsPerSecond, readingTick]);
|
|
2197
|
+
React.useEffect(() => {
|
|
2198
|
+
if (!readingPending)
|
|
2199
|
+
return;
|
|
2200
|
+
const timer = setTimeout(() => refreshReadingDisplay(tick => tick + 1), STREAM_TYPEWRITER_DEFAULTS.tickMs);
|
|
2201
|
+
return () => clearTimeout(timer);
|
|
2202
|
+
}, [readingPending, readingTick, messages, messageDisplay.smoothStreaming, messageDisplay.proseReadingRateWordsPerSecond]);
|
|
1228
2203
|
// Compute messages for Static (completed) vs live (streaming/interactive)
|
|
1229
|
-
// Static
|
|
1230
|
-
|
|
2204
|
+
// Static renders only items past its retained index. Explicit clear starts a
|
|
2205
|
+
// new component generation so the shorter transcript is rendered from zero.
|
|
2206
|
+
const { staticMessages, liveTurnMessages, formMessages } = useMemo(() => {
|
|
1231
2207
|
// Form widget messages are always computed (need interactivity)
|
|
1232
2208
|
const forms = messages.filter(m => m.widget?.widgetType === 'form');
|
|
1233
2209
|
if (!scrollback) {
|
|
1234
|
-
return { staticMessages: [],
|
|
2210
|
+
return { staticMessages: [], liveTurnMessages: [], formMessages: forms };
|
|
1235
2211
|
}
|
|
1236
2212
|
// Completed messages without widgets go to Static (rendered once, kept in scrollback)
|
|
1237
|
-
const completed =
|
|
2213
|
+
const completed = displayMessages.filter(m => !m.isStreaming
|
|
2214
|
+
&& !(messageDisplay.showTurnMetadata && m.turn && m.turn.finishedAt === undefined)
|
|
1238
2215
|
&& !m.widget
|
|
1239
2216
|
&& (messageDisplay.showThinking || m.metadata?.reasoning !== true));
|
|
1240
2217
|
// Streaming message stays in live section (can update)
|
|
1241
|
-
const streaming =
|
|
1242
|
-
&& (messageDisplay.showThinking || m.metadata?.reasoning !== true))
|
|
2218
|
+
const streaming = displayMessages.filter(m => (m.isStreaming || (messageDisplay.showTurnMetadata && m.turn && m.turn.finishedAt === undefined))
|
|
2219
|
+
&& (messageDisplay.showThinking || m.metadata?.reasoning !== true));
|
|
1243
2220
|
return {
|
|
1244
2221
|
staticMessages: completed,
|
|
1245
|
-
|
|
2222
|
+
liveTurnMessages: streaming,
|
|
1246
2223
|
formMessages: forms
|
|
1247
2224
|
};
|
|
1248
|
-
}, [messages, scrollback, messageDisplay.showThinking]);
|
|
1249
|
-
|
|
2225
|
+
}, [messages, displayMessages, scrollback, messageDisplay.showThinking, messageDisplay.showTurnMetadata]);
|
|
2226
|
+
React.useEffect(() => {
|
|
2227
|
+
if (!messageDisplay.showTurnMetadata || !messages.some(message => message.turn && message.turn.finishedAt === undefined))
|
|
2228
|
+
return;
|
|
2229
|
+
const timer = setInterval(() => setPromptClock(clock => clock + 1), 1000);
|
|
2230
|
+
return () => clearInterval(timer);
|
|
2231
|
+
}, [messages, messageDisplay.showTurnMetadata]);
|
|
2232
|
+
return (_jsxs(FullScreen, { debug: debug, scrollback: scrollback, children: [!scrollback && _jsx(Header, { version: getVersion(), debug: debug, renderer: "ink" }), scrollback ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticMessages, children: (msg) => (_jsx(Box, { flexDirection: "column", children: _jsx(SingleMessage, { message: msg, expandToolOutputs: expandToolOutputs, cols: columns, showTurnMetadata: messageDisplay.showTurnMetadata }) }, msg.id)) }, transcriptGeneration), liveTurnMessages.map(message => _jsx(SingleMessage, { message: message, expandToolOutputs: expandToolOutputs, cols: columns, showTurnMetadata: messageDisplay.showTurnMetadata }, message.id))] })) : (_jsx(MessageList, { messages: displayMessages, height: contentHeight, debug: debug, expandToolOutputs: expandToolOutputs, scrollback: scrollback, showThinking: messageDisplay.showThinking, showTurnMetadata: messageDisplay.showTurnMetadata, cols: columns })), _jsx(ActivityLine, { state: agentState, spinnerLabel: spinnerLabel, spinnerFrame: spinnerFrame, inputMode: inputMode, scrollback: scrollback, cols: columns }), _jsx(QueuePanel, { entries: queuePreview, total: queue.length, cols: columns }), isRecording && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "red", children: "\u25CF Recording" }), recordingDevice && (_jsxs(Text, { color: "gray", children: [" [", recordingDevice, "]"] })), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: renderVUMeter(audioLevel) }), _jsx(Text, { color: "gray", children: " \u2014 Ctrl+R to stop" })] }), partialTranscript && (_jsx(Box, { children: _jsxs(Text, { color: "gray", italic: true, children: ["\"", partialTranscript, "\""] }) }))] })), isTranscribing && !useNativeDictation && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "yellow", children: "\u280B Transcribing..." }) })), scopeWidget.isActive() && _jsx(Box, { paddingX: 1, children: _jsx(Text, { children: scopeWidget.buildContent(columns) }) }), workspaceWidget.isActive() && _jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { children: workspaceWidget.buildContent(columns) }) }), questionnaireWidget.isActive() && _jsx(InkNode, { node: questionnaireWidget.buildSurface(columns, rows).view }), formMessages.map((msg) => (msg.widget && (_jsx(FormWidget, { title: msg.widget.title, fields: msg.widget.fields, formId: msg.widget.formId, submitLabel: msg.widget.submitLabel, onSubmit: handleFormSubmit, onCancel: handleFormCancel, isActive: formCallbacksRef.current.has(msg.widget.formId) }, msg.id)))), _jsx(InputArea, { onCancelRun: () => {
|
|
2233
|
+
const routing = routingControllersRef.current.size > 0;
|
|
2234
|
+
retireRouting();
|
|
2235
|
+
return cancelGeneration() || routing;
|
|
2236
|
+
}, isRunning: agentState.status !== 'idle' && agentState.status !== 'error', mode: agentMode, onSubmit: handleSubmit, onQueue: (text) => {
|
|
2237
|
+
const size = enqueueMessage(text, 'end-of-turn');
|
|
2238
|
+
if (size === null)
|
|
2239
|
+
return false;
|
|
2240
|
+
showToast(`Queued (${size})`);
|
|
2241
|
+
return true;
|
|
2242
|
+
}, onCommand: handleCommand, commands: commands, onStateChange: setInputSnapshot, onToast: showToast, onDictationStateChange: handleDictationStateChange, cols: columns, inputBus: inputBus, inputBlocked: () => questionnaireWidget.isActive() || questionnaireEventOwnedRef.current, clearDraftVersion: clearDraftVersion, disabled: scopeWidget.isActive() || workspaceWidget.isActive() || questionnaireWidget.isActive() || formCallbacksRef.current.size > 0, debug: debug, cwd: shellCwdRef.current, placeholder: (!configStore.hasApiKey() && !configStore.isAuthenticated())
|
|
1250
2243
|
? 'Use /config key or /auth login to get started'
|
|
1251
|
-
: getAgentActivityPlaceholder(agentState) ?? 'Type a message or /help for commands...', spinnerLabel: scrollback ? spinnerLabel : null, spinnerFrame: scrollback ? spinnerFrame : null, passwordMode: pendingPromptRef.current?.isPassword ?? false, onCycleMode: onCycleMode }), _jsx(StatusBar, { state: agentState, sessionId: sessionId, version: getVersion(), connectionStatus: (configStore.hasApiKey() || configStore.isAuthenticated()) ? 'connected' : 'disconnected', contextLength: contextLength, contextEstimated: contextEstimated, contextStatus: contextStatus, provider: provider, model: model, emulationId: emulationId, effort: configStore.getCodexEffort(), inputMode: inputMode, toast: toast,
|
|
2244
|
+
: getAgentActivityPlaceholder(agentState) ?? 'Type a message or /help for commands...', spinnerLabel: scrollback ? spinnerLabel : null, spinnerFrame: scrollback ? spinnerFrame : null, passwordMode: pendingPromptRef.current?.isPassword ?? false, promptOwner: pendingPromptRef.current || undefined, allowEmptySubmit: pendingPromptRef.current?.allowEmptySubmit ?? false, onCycleMode: onCycleMode }), _jsx(StatusBar, { state: agentState, sessionId: sessionId, sessionName: sessionName, version: getVersion(), connectionStatus: (configStore.hasApiKey() || configStore.isAuthenticated()) ? 'connected' : 'disconnected', contextLength: contextLength, contextEstimated: contextEstimated, contextStatus: contextStatus, provider: provider, model: model, emulationId: emulationId, effort: configStore.getCodexEffort(), inputMode: inputMode, toast: toast, escapeHint: scopeWidget.isActive() || workspaceWidget.isActive() || questionnaireWidget.isActive() || formCallbacksRef.current.size > 0 || pendingPromptRef.current
|
|
2245
|
+
? 'Esc close'
|
|
2246
|
+
: abortControllerRef.current
|
|
2247
|
+
? isEscapeStopArmed(escapeStopArm, abortControllerRef.current, Date.now()) ? ESCAPE_CONFIRMATION_HINT : 'Esc twice to stop'
|
|
2248
|
+
: queue.length > 0 ? 'Esc keeps queue · /rewind' : 'Esc keeps draft · /rewind', debug: debug, mode: agentMode, cols: columns, statusLine: configStore.get().statusLine })] }));
|
|
1252
2249
|
};
|
|
1253
2250
|
export default App;
|