mixdog 0.9.3 → 0.9.5
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 +112 -38
- package/package.json +10 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/r5-orchestrated-task.json +7 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +251 -32
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +3 -4
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +25 -65
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +9 -8
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +10 -7
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +647 -2056
- package/src/output-styles/default.md +30 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +8 -6
- package/src/output-styles/simple.md +21 -7
- package/src/rules/agent/00-common.md +6 -3
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +15 -0
- package/src/rules/lead/lead-tool.md +6 -15
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +27 -318
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +247 -701
- package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
- package/src/runtime/channels/lib/event-queue.mjs +78 -13
- package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/scheduler.mjs +27 -113
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +43 -616
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +210 -1948
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/ko-morph.mjs +195 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +802 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/config.mjs +14 -4
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +119 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +205 -0
- package/src/session-runtime/quick-search-models.mjs +47 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +352 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +1 -1
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +138 -669
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +64 -14
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1059 -8110
- package/src/tui/app/app-format.mjs +213 -0
- package/src/tui/app/channel-pickers.mjs +508 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +356 -0
- package/src/tui/app/model-options.mjs +334 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +489 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +677 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +512 -0
- package/src/tui/app/use-transcript-window.mjs +607 -0
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +64 -15
- package/src/tui/components/PromptInput.jsx +33 -102
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +69 -15
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +13553 -12384
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +170 -574
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +65 -1
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -18
- package/src/workflows/sequential/WORKFLOW.md +16 -18
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +112 -7
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/channels/lib/holidays.mjs +0 -138
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* use-transcript-window.mjs — transcript row-index/window memo chain + the
|
|
3
|
+
* measured-height harvest and reading-anchor lock effects.
|
|
4
|
+
*
|
|
5
|
+
* Extracted verbatim from App.jsx: structure-signature/row-index/render-window
|
|
6
|
+
* memos, the same-frame anchor lock + capture, the per-commit Yoga height
|
|
7
|
+
* harvest, bottom-follow/anchor post-commit sync, selection layout shift and
|
|
8
|
+
* the scroll clamp. Scroll/anchor/drag refs stay App-owned (injected); this
|
|
9
|
+
* hook owns the measurement maps, measuredRowsVersion and the totalRows/
|
|
10
|
+
* preserved-delta bookkeeping refs.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import {
|
|
14
|
+
TRANSCRIPT_MEASURED_ROWS,
|
|
15
|
+
transcriptMeasuredRowsCache,
|
|
16
|
+
transcriptItemVariantKey,
|
|
17
|
+
buildTranscriptRowIndex,
|
|
18
|
+
transcriptStructureSignature,
|
|
19
|
+
transcriptRenderWindow,
|
|
20
|
+
resolveAnchorScrollOffset,
|
|
21
|
+
upperBound,
|
|
22
|
+
shiftSelectionRectY,
|
|
23
|
+
} from './transcript-window.mjs';
|
|
24
|
+
import { shouldSuppressFullyFailedToolItem } from '../transcript-tool-failures.mjs';
|
|
25
|
+
|
|
26
|
+
export function useTranscriptWindow({
|
|
27
|
+
items,
|
|
28
|
+
themeEpoch,
|
|
29
|
+
frameColumns,
|
|
30
|
+
toolOutputExpanded,
|
|
31
|
+
transcriptContentHeight,
|
|
32
|
+
transcriptBottomSlackRows,
|
|
33
|
+
transcriptGuardRows,
|
|
34
|
+
floatingPanelRows,
|
|
35
|
+
overlayHintRequested,
|
|
36
|
+
scrollOffset,
|
|
37
|
+
setScrollOffset,
|
|
38
|
+
transcriptAnchorRef,
|
|
39
|
+
transcriptAnchorDirtyRef,
|
|
40
|
+
scrollTargetRef,
|
|
41
|
+
scrollPositionRef,
|
|
42
|
+
maxScrollRowsRef,
|
|
43
|
+
transcriptGeomRef,
|
|
44
|
+
followingRef,
|
|
45
|
+
dragRef,
|
|
46
|
+
transcriptViewportRef,
|
|
47
|
+
selectionLayoutRef,
|
|
48
|
+
withSelectionClip,
|
|
49
|
+
paintSelectionRect,
|
|
50
|
+
stopSmoothScroll,
|
|
51
|
+
measuredRowsVersion,
|
|
52
|
+
setMeasuredRowsVersion,
|
|
53
|
+
}) {
|
|
54
|
+
const transcriptTotalRowsRef = useRef(0);
|
|
55
|
+
const preservedScrollDeltaRef = useRef(0);
|
|
56
|
+
// Previous frame's viewport-only geometry (content height + floating-panel
|
|
57
|
+
// reservation). A floating panel / view (picker/context/usage/text-entry)
|
|
58
|
+
// open-close changes transcriptContentHeight WITHOUT changing `items`, so the
|
|
59
|
+
// transcript window would re-slice against the new viewport height while the
|
|
60
|
+
// scroll offset still reflects the old one — the visible top row jumps, then
|
|
61
|
+
// the post-commit anchor effect snaps it back a frame later (the 2-frame
|
|
62
|
+
// re-settle). We detect that viewport-only change at render time and freeze
|
|
63
|
+
// the visible top anchor from the PREVIOUS geometry, resolving the scroll
|
|
64
|
+
// offset for THIS render so the anchored row stays put in a single frame.
|
|
65
|
+
const prevViewportGeomRef = useRef({ contentHeight: 0, floatingPanelRows: 0 });
|
|
66
|
+
// App-level measured row heights (real per-item height cache). The map of
|
|
67
|
+
// mounted item id → ink DOM element is read every commit to harvest each
|
|
68
|
+
// row's REAL Yoga height into transcriptMeasuredRowsCache. measuredRowsVersion
|
|
69
|
+
// is bumped whenever a height actually changes so the row-index/window memos
|
|
70
|
+
// recompute against the corrected heights (one-frame lag, absorbed by the
|
|
71
|
+
// overscan band).
|
|
72
|
+
const transcriptItemElsRef = useRef(new Map());
|
|
73
|
+
const transcriptMeasureRefCache = useRef(new Map());
|
|
74
|
+
// id → latest item object for this render. The callback ref reads from here so
|
|
75
|
+
// a reused (stable) callback never captures a stale item across patches.
|
|
76
|
+
const transcriptMeasureItemsRef = useRef(new Map());
|
|
77
|
+
// Stable per-item callback-ref factory: storing the element under the item id
|
|
78
|
+
// (and reading the live item object from transcriptMeasureItemsRef) avoids the
|
|
79
|
+
// ref-swap churn React would otherwise cause with an inline closure each
|
|
80
|
+
// render, while never serving a stale item: the callback resolves the current
|
|
81
|
+
// item by id at call time. The ref(null) path drops the element; the harvest
|
|
82
|
+
// reads getComputedHeight from whatever is mounted, so an unmount simply stops
|
|
83
|
+
// contributing (its last measurement stays cached on the item object).
|
|
84
|
+
const transcriptMeasureRef = useCallback((item) => {
|
|
85
|
+
if (!TRANSCRIPT_MEASURED_ROWS || !item || item.id == null) return undefined;
|
|
86
|
+
if (shouldSuppressFullyFailedToolItem(item)) {
|
|
87
|
+
transcriptMeasuredRowsCache.delete(item);
|
|
88
|
+
transcriptItemElsRef.current.delete(item.id);
|
|
89
|
+
transcriptMeasureItemsRef.current.delete(item.id);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const key = item.id;
|
|
93
|
+
transcriptMeasureItemsRef.current.set(key, item);
|
|
94
|
+
let fn = transcriptMeasureRefCache.current.get(key);
|
|
95
|
+
if (!fn) {
|
|
96
|
+
fn = (el) => {
|
|
97
|
+
if (el) {
|
|
98
|
+
transcriptItemElsRef.current.set(key, el);
|
|
99
|
+
} else {
|
|
100
|
+
transcriptItemElsRef.current.delete(key);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
transcriptMeasureRefCache.current.set(key, fn);
|
|
104
|
+
}
|
|
105
|
+
return fn;
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
// Key the heavy O(n) row-index + windowing memos on a STRUCTURE signature
|
|
109
|
+
// instead of the `items` array identity. The engine swaps `items`
|
|
110
|
+
// for a new array on every streaming flush (~8ms) while only the final
|
|
111
|
+
// assistant item's text grows; depending on array identity re-ran both memos
|
|
112
|
+
// each delta frame and visibly throttled the stream. The signature changes
|
|
113
|
+
// only when transcript structure or the streaming item's estimated height
|
|
114
|
+
// changes, so steady per-character growth keeps both memos warm.
|
|
115
|
+
//
|
|
116
|
+
// The signature itself is memoized on `items` identity (+columns/
|
|
117
|
+
// expanded) so re-renders that DO NOT touch items — drag motion, scroll,
|
|
118
|
+
// input typing — skip the O(n) signature walk entirely. During streaming the
|
|
119
|
+
// engine hands us a fresh `items` array each flush, so this memo
|
|
120
|
+
// recomputes and still tracks the streaming item's height correctly.
|
|
121
|
+
const transcriptStructureSig = useMemo(
|
|
122
|
+
() => transcriptStructureSignature(items, frameColumns, toolOutputExpanded),
|
|
123
|
+
[items, frameColumns, toolOutputExpanded],
|
|
124
|
+
);
|
|
125
|
+
const transcriptStreamingActive = (items || []).some(
|
|
126
|
+
(item) => item?.kind === 'assistant' && item?.streaming,
|
|
127
|
+
);
|
|
128
|
+
const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
|
|
129
|
+
const transcriptPinnedForStreaming = followingRef.current
|
|
130
|
+
|| scrolledUpRowsForPin <= transcriptBottomSlackRows;
|
|
131
|
+
const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > transcriptBottomSlackRows;
|
|
132
|
+
const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
|
|
133
|
+
const hasStreamingReadingAnchor = !!transcriptAnchorRef.current
|
|
134
|
+
|| transcriptAnchorDirtyRef.current;
|
|
135
|
+
const suppressMeasuredRowHeights = transcriptStreamingActive && (
|
|
136
|
+
transcriptPinnedForStreaming
|
|
137
|
+
|| (scrolledUpForStreamingMeasure && hasStreamingReadingAnchor)
|
|
138
|
+
|| (scrolledUpForStreamingMeasure && prevEstimateGeometry && !transcriptPinnedForStreaming)
|
|
139
|
+
);
|
|
140
|
+
const transcriptRowIndex = useMemo(() => buildTranscriptRowIndex(items, {
|
|
141
|
+
columns: frameColumns,
|
|
142
|
+
toolOutputExpanded,
|
|
143
|
+
suppressMeasuredRowHeights,
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: sig captures the relevant item changes; measuredRowsVersion folds in app-level measured height corrections
|
|
145
|
+
}), [transcriptStructureSig, measuredRowsVersion, suppressMeasuredRowHeights]);
|
|
146
|
+
// ── Same-frame anchor lock ───────────────────────────────────────────────
|
|
147
|
+
// While the user reads older transcript (anchor captured, not dirty), resolve
|
|
148
|
+
// the scroll offset that keeps the anchored viewport-top row fixed for THIS
|
|
149
|
+
// frame's prefix table — SYNCHRONOUSLY, before windowing/marginBottom use it.
|
|
150
|
+
// Previously this correction lived only in the post-commit layout effect, so
|
|
151
|
+
// the frame that grew the streaming tail rendered with the STALE offset first
|
|
152
|
+
// (a blank band opened at the top) and only the NEXT frame snapped it shut.
|
|
153
|
+
// Computing it here folds the new totalRows and the corrected offset into the
|
|
154
|
+
// SAME render, so the completed line fills its space with no one-frame gap.
|
|
155
|
+
// Falls back to the live scrollOffset state when there is no active anchor
|
|
156
|
+
// (bottom-follow / pinned) or it cannot be aligned (anchor item gone).
|
|
157
|
+
const hasReadingAnchor = !!transcriptAnchorRef.current && !transcriptAnchorDirtyRef.current;
|
|
158
|
+
const scrolledUpRows = Math.max(0, Number(scrollTargetRef.current) || 0);
|
|
159
|
+
// "Genuinely scrolled up" = the viewport is above the bottom slack band. The
|
|
160
|
+
// bottom-follow / pinned path owns everything at-or-below the slack; anything
|
|
161
|
+
// above it is the user reading older transcript.
|
|
162
|
+
const scrolledUp = scrolledUpRows > transcriptBottomSlackRows;
|
|
163
|
+
// A genuine reading anchor wins even if followingRef is stale-true while the
|
|
164
|
+
// user is scrolled up. The follow-arm SHOULD have been cleared when the anchor
|
|
165
|
+
// was captured, but if it lingers true we must not fall through to the stale-
|
|
166
|
+
// offset render path (that is one half of the newline-jump bug). Keep the
|
|
167
|
+
// plain !following gate for the anchor-less follow case.
|
|
168
|
+
const anchorLockActive = hasReadingAnchor && !followingRef.current && scrolledUp;
|
|
169
|
+
const targetNearBottom = followingRef.current || !scrolledUp;
|
|
170
|
+
const nearBottomWithoutAnchor = !transcriptAnchorRef.current
|
|
171
|
+
&& !transcriptAnchorDirtyRef.current
|
|
172
|
+
&& targetNearBottom;
|
|
173
|
+
let renderScrollOffset = targetNearBottom ? 0 : scrollOffset;
|
|
174
|
+
const lockViewRows = Math.max(1, Number(transcriptContentHeight) || 1);
|
|
175
|
+
const lockTotalRows = Math.max(0, Number(transcriptRowIndex?.totalRows) || 0);
|
|
176
|
+
const lockMaxRows = Math.max(0, lockTotalRows - lockViewRows);
|
|
177
|
+
const curPrefixForLock = transcriptRowIndex?.prefixRows || null;
|
|
178
|
+
if (anchorLockActive) {
|
|
179
|
+
const locked = resolveAnchorScrollOffset({
|
|
180
|
+
anchor: transcriptAnchorRef.current,
|
|
181
|
+
items: items,
|
|
182
|
+
curPrefix: curPrefixForLock,
|
|
183
|
+
totalRows: lockTotalRows,
|
|
184
|
+
viewRows: lockViewRows,
|
|
185
|
+
maxRows: lockMaxRows,
|
|
186
|
+
});
|
|
187
|
+
if (locked != null) renderScrollOffset = locked;
|
|
188
|
+
} else if (!followingRef.current && !nearBottomWithoutAnchor && scrolledUp) {
|
|
189
|
+
// ── Same-frame anchor CAPTURE for the missing/dirty-anchor case ─────────
|
|
190
|
+
// The viewport is genuinely scrolled up but there is NO usable same-frame
|
|
191
|
+
// anchor: it is missing, or dirtied by a manual scroll whose synchronous
|
|
192
|
+
// capture failed, or a stale-true follow-arm already dropped it. Without an
|
|
193
|
+
// anchor this frame would render with the STALE bottom-relative
|
|
194
|
+
// scrollOffset, so any row growth THIS frame (a streaming newline
|
|
195
|
+
// completing a line, a transcript row expanding) shifts the visible top by
|
|
196
|
+
// the delta BEFORE the post-commit rowDelta effect can capture/correct — and
|
|
197
|
+
// that effect would then capture from the ALREADY-shifted totalRows. That is
|
|
198
|
+
// the confirmed one-frame jump/jitter.
|
|
199
|
+
//
|
|
200
|
+
// Fix it at render time: capture an anchor from the PREVIOUS published
|
|
201
|
+
// geometry (transcriptGeomRef still holds the prior frame here — THIS
|
|
202
|
+
// frame's geom is published a few lines below), identifying the item id +
|
|
203
|
+
// row offset that sat at the previous visible-TOP edge, then resolve the
|
|
204
|
+
// offset against the CURRENT prefix table with the same pure helper so that
|
|
205
|
+
// exact top row stays put THIS frame. Persist the captured anchor so the
|
|
206
|
+
// post-commit effect keeps the identical anchor stable instead of
|
|
207
|
+
// re-deriving one from the already-shifted totalRows.
|
|
208
|
+
const geom = transcriptGeomRef.current || {};
|
|
209
|
+
const prevPrefix = geom.prefixRows;
|
|
210
|
+
if (Array.isArray(prevPrefix) && prevPrefix.length > 1) {
|
|
211
|
+
const prevTotal = Math.max(0, Number(geom.totalRows) || 0);
|
|
212
|
+
const prevView = Math.max(1, Number(geom.viewRows) || 1);
|
|
213
|
+
const prevOffset = Math.max(0, Number(geom.renderOffset) || 0);
|
|
214
|
+
const prevItems = geom.items || [];
|
|
215
|
+
// Bottom-relative window math (same as transcriptRenderWindow): the top
|
|
216
|
+
// edge sits `offset + viewRows` rows up from the previous total.
|
|
217
|
+
const prevTopRow = Math.max(0, Math.min(prevTotal, prevTotal - prevOffset - prevView));
|
|
218
|
+
let idx = upperBound(prevPrefix, prevTopRow) - 1;
|
|
219
|
+
if (idx < 0) idx = 0;
|
|
220
|
+
if (idx > prevPrefix.length - 2) idx = prevPrefix.length - 2;
|
|
221
|
+
const anchorItem = prevItems[idx];
|
|
222
|
+
if (anchorItem && anchorItem.id != null) {
|
|
223
|
+
const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
|
|
224
|
+
const locked = resolveAnchorScrollOffset({
|
|
225
|
+
anchor: captured,
|
|
226
|
+
items: items,
|
|
227
|
+
curPrefix: curPrefixForLock,
|
|
228
|
+
totalRows: lockTotalRows,
|
|
229
|
+
viewRows: lockViewRows,
|
|
230
|
+
maxRows: lockMaxRows,
|
|
231
|
+
});
|
|
232
|
+
if (locked != null) {
|
|
233
|
+
renderScrollOffset = locked;
|
|
234
|
+
transcriptAnchorRef.current = captured;
|
|
235
|
+
transcriptAnchorDirtyRef.current = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ── Viewport-only transition freeze (floating panel / view open-close) ─────
|
|
241
|
+
// When a picker/context/usage/text-entry panel opens or closes, only the
|
|
242
|
+
// transcript viewport height changes (transcriptContentHeight and/or the
|
|
243
|
+
// floating-panel reservation) — `items` and the row-index are unchanged. The
|
|
244
|
+
// bottom-follow / anchor-lock paths above only fire when the user is reading
|
|
245
|
+
// older transcript; a bottom-pinned transcript falls through with
|
|
246
|
+
// renderScrollOffset resolved against the NEW viewport while the on-screen
|
|
247
|
+
// top edge still reflects the OLD one, producing the visible jump that the
|
|
248
|
+
// post-commit effect then corrects one frame later. Freeze it here: when the
|
|
249
|
+
// ONLY thing that changed is the viewport (not the anchor state, not items),
|
|
250
|
+
// reconstruct the previous visible-top row from the prior published geometry
|
|
251
|
+
// and resolve the offset against the current prefix table so the same top row
|
|
252
|
+
// renders THIS frame. Bottom-pinned/following views are already stable
|
|
253
|
+
// (flex-end owns them), so restrict the freeze to the scrolled-up, no-active-
|
|
254
|
+
// lock case that the branches above did not already resolve.
|
|
255
|
+
const prevViewport = prevViewportGeomRef.current || {};
|
|
256
|
+
const viewportOnlyChanged = (Number(prevViewport.contentHeight) || 0) !== lockViewRows
|
|
257
|
+
|| (Number(prevViewport.floatingPanelRows) || 0) !== (Number(floatingPanelRows) || 0);
|
|
258
|
+
if (viewportOnlyChanged
|
|
259
|
+
&& !anchorLockActive
|
|
260
|
+
&& !followingRef.current
|
|
261
|
+
&& !nearBottomWithoutAnchor
|
|
262
|
+
&& scrolledUp) {
|
|
263
|
+
const geom = transcriptGeomRef.current || {};
|
|
264
|
+
const prevPrefix = geom.prefixRows;
|
|
265
|
+
if (Array.isArray(prevPrefix) && prevPrefix.length > 1) {
|
|
266
|
+
const prevTotal = Math.max(0, Number(geom.totalRows) || 0);
|
|
267
|
+
// Use the PREVIOUS viewport rows to reconstruct where the top edge sat on
|
|
268
|
+
// screen before the panel changed the height.
|
|
269
|
+
const prevView = Math.max(1, Number(prevViewport.contentHeight) || Number(geom.viewRows) || 1);
|
|
270
|
+
const prevOffset = Math.max(0, Number(geom.renderOffset) || 0);
|
|
271
|
+
const prevItems = geom.items || [];
|
|
272
|
+
const prevTopRow = Math.max(0, Math.min(prevTotal, prevTotal - prevOffset - prevView));
|
|
273
|
+
let idx = upperBound(prevPrefix, prevTopRow) - 1;
|
|
274
|
+
if (idx < 0) idx = 0;
|
|
275
|
+
if (idx > prevPrefix.length - 2) idx = prevPrefix.length - 2;
|
|
276
|
+
const anchorItem = prevItems[idx];
|
|
277
|
+
if (anchorItem && anchorItem.id != null) {
|
|
278
|
+
const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
|
|
279
|
+
const locked = resolveAnchorScrollOffset({
|
|
280
|
+
anchor: captured,
|
|
281
|
+
items: items,
|
|
282
|
+
curPrefix: curPrefixForLock,
|
|
283
|
+
totalRows: lockTotalRows,
|
|
284
|
+
viewRows: lockViewRows,
|
|
285
|
+
maxRows: lockMaxRows,
|
|
286
|
+
});
|
|
287
|
+
if (locked != null) {
|
|
288
|
+
renderScrollOffset = locked;
|
|
289
|
+
// Persist so the post-commit effect keeps this exact anchor stable
|
|
290
|
+
// rather than re-deriving one from the already-resized geometry.
|
|
291
|
+
transcriptAnchorRef.current = captured;
|
|
292
|
+
transcriptAnchorDirtyRef.current = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
prevViewportGeomRef.current = {
|
|
298
|
+
contentHeight: lockViewRows,
|
|
299
|
+
floatingPanelRows: Number(floatingPanelRows) || 0,
|
|
300
|
+
};
|
|
301
|
+
const transcriptWindow = useMemo(() => transcriptRenderWindow(items, {
|
|
302
|
+
scrollOffset: renderScrollOffset,
|
|
303
|
+
viewportHeight: transcriptContentHeight,
|
|
304
|
+
columns: frameColumns,
|
|
305
|
+
toolOutputExpanded,
|
|
306
|
+
rowIndex: transcriptRowIndex,
|
|
307
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: sig+scroll/viewport capture the relevant changes
|
|
308
|
+
}), [transcriptStructureSig, renderScrollOffset, transcriptContentHeight, transcriptRowIndex]);
|
|
309
|
+
maxScrollRowsRef.current = transcriptWindow.maxScrollRows;
|
|
310
|
+
// Publish this frame's geometry so a manual scroll can capture the reading
|
|
311
|
+
// anchor synchronously (see captureTranscriptAnchorAt).
|
|
312
|
+
transcriptGeomRef.current = {
|
|
313
|
+
prefixRows: transcriptRowIndex?.prefixRows || null,
|
|
314
|
+
totalRows: Math.max(0, Number(transcriptWindow.totalRows) || 0),
|
|
315
|
+
viewRows: Math.max(1, Number(transcriptContentHeight) || 1),
|
|
316
|
+
items: items || null,
|
|
317
|
+
// The offset THIS frame actually rendered with. The same-frame anchor
|
|
318
|
+
// CAPTURE for a missing/dirty anchor (above) reads this from the PREVIOUS
|
|
319
|
+
// frame to reconstruct the exact top-edge row that was on screen, so the
|
|
320
|
+
// capture matches what the user saw rather than the stale scrollOffset
|
|
321
|
+
// state. Bottom-relative, matching transcriptRenderWindow's window math.
|
|
322
|
+
renderOffset: Math.max(0, Number(renderScrollOffset) || 0),
|
|
323
|
+
suppressMeasuredRowHeights,
|
|
324
|
+
};
|
|
325
|
+
// The window memo is keyed on a structure signature that intentionally
|
|
326
|
+
// ignores per-character growth of the streaming assistant text, so its
|
|
327
|
+
// `items` slice can hold a STALE reference to the streaming item between
|
|
328
|
+
// height changes. Re-slice the live `items` over the memo's stable
|
|
329
|
+
// [startIndex, endIndex) bounds so the on-screen text is always current
|
|
330
|
+
// while the expensive indexing/windowing stays warm.
|
|
331
|
+
const transcriptVisibleItems = (items || []).slice(
|
|
332
|
+
transcriptWindow.startIndex,
|
|
333
|
+
transcriptWindow.endIndex,
|
|
334
|
+
);
|
|
335
|
+
// The bottom meta band is spinner-only, so nothing is pulled out of the
|
|
336
|
+
// transcript for it. A finished turn's done row (turndone/statusdone) renders
|
|
337
|
+
// inline in scrollback like any other item — no filtering, no double-paint.
|
|
338
|
+
const renderedTranscriptItems = transcriptVisibleItems;
|
|
339
|
+
let overlayHintAttachItemIndex = -1;
|
|
340
|
+
for (let i = renderedTranscriptItems.length - 1; i >= 0; i--) {
|
|
341
|
+
const item = renderedTranscriptItems[i];
|
|
342
|
+
if (item?.kind === 'tool' && shouldSuppressFullyFailedToolItem(item)) continue;
|
|
343
|
+
overlayHintAttachItemIndex = i;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
const transcriptTailPinned = Math.max(0, Number(transcriptWindow.effectiveScrollOffset) || 0) <= transcriptBottomSlackRows;
|
|
347
|
+
const overlayHintOnLastItem = overlayHintRequested
|
|
348
|
+
&& floatingPanelRows <= 0
|
|
349
|
+
&& transcriptWindow.bottomSpacerRows === 0
|
|
350
|
+
&& transcriptTailPinned
|
|
351
|
+
&& overlayHintAttachItemIndex >= 0;
|
|
352
|
+
const overlayHintFallbackRow = overlayHintRequested
|
|
353
|
+
&& floatingPanelRows <= 0
|
|
354
|
+
&& transcriptGuardRows > 0
|
|
355
|
+
&& !overlayHintOnLastItem;
|
|
356
|
+
// ── App-level measured height harvest ───────────────────────────────────
|
|
357
|
+
// Runs after EVERY commit (no deps): Yoga has just laid out the mounted rows,
|
|
358
|
+
// so each tracked item Box's getComputedHeight() is its REAL terminal height.
|
|
359
|
+
// Fold those into transcriptMeasuredRowsCache (validated on the same variant
|
|
360
|
+
// key the estimate caches use) and bump measuredRowsVersion only when a height
|
|
361
|
+
// actually changed — that re-runs the row-index/window memos against corrected
|
|
362
|
+
// heights, then the harvest finds nothing new and the loop settles (one frame,
|
|
363
|
+
// overscan-absorbed). Streaming
|
|
364
|
+
// assistant rows are skipped: their height churns every flush and the bottom-
|
|
365
|
+
// follow path already keeps them visually stable.
|
|
366
|
+
useLayoutEffect(() => {
|
|
367
|
+
if (!TRANSCRIPT_MEASURED_ROWS) return;
|
|
368
|
+
// Skip the per-row Yoga harvest while a drag is in progress. Edge auto-
|
|
369
|
+
// scroll commits setScrollOffset on every pointer motion, but the mounted
|
|
370
|
+
// rows' real heights do not change during a drag — only their scroll
|
|
371
|
+
// position does. Re-measuring every motion ran this O(mounted) loop (plus a
|
|
372
|
+
// variantKey check per row) for no height change, which is pure drag
|
|
373
|
+
// overhead on a tall transcript. The cached measurements stay authoritative
|
|
374
|
+
// for the row-index math; a single re-measure is forced on release below.
|
|
375
|
+
if (dragRef.current.active) return;
|
|
376
|
+
if (transcriptStreamingActive && transcriptTailPinned) return;
|
|
377
|
+
const els = transcriptItemElsRef.current;
|
|
378
|
+
if (!els || els.size === 0) return;
|
|
379
|
+
const liveItems = transcriptMeasureItemsRef.current;
|
|
380
|
+
const toolExpandedFlag = toolOutputExpanded ? 1 : 0;
|
|
381
|
+
let changed = false;
|
|
382
|
+
for (const [key, el] of els.entries()) {
|
|
383
|
+
const item = liveItems.get(key);
|
|
384
|
+
const yoga = el?.yogaNode;
|
|
385
|
+
if (!item || !yoga) continue;
|
|
386
|
+
if (shouldSuppressFullyFailedToolItem(item)) {
|
|
387
|
+
if (transcriptMeasuredRowsCache.delete(item)) changed = true;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (item.kind === 'assistant' && item.streaming) continue;
|
|
391
|
+
// Width 0 = Yoga has not laid this node out yet this frame; skip so a
|
|
392
|
+
// transient 0 never poisons the cache (guard on a real positive width).
|
|
393
|
+
if (typeof yoga.getComputedWidth === 'function' && yoga.getComputedWidth() <= 0) continue;
|
|
394
|
+
const rawMeasured = Math.round(Number(yoga.getComputedHeight?.()) || 0);
|
|
395
|
+
if (rawMeasured <= 0) {
|
|
396
|
+
if (transcriptMeasuredRowsCache.delete(item)) changed = true;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const measured = Math.max(1, rawMeasured);
|
|
400
|
+
const variantKey = transcriptItemVariantKey(item);
|
|
401
|
+
const prev = transcriptMeasuredRowsCache.get(item);
|
|
402
|
+
if (prev
|
|
403
|
+
&& prev.rows === measured
|
|
404
|
+
&& prev.columns === frameColumns
|
|
405
|
+
&& prev.toolExpanded === toolExpandedFlag
|
|
406
|
+
&& prev.variantKey === variantKey) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
transcriptMeasuredRowsCache.set(item, {
|
|
410
|
+
rows: measured,
|
|
411
|
+
columns: frameColumns,
|
|
412
|
+
toolExpanded: toolExpandedFlag,
|
|
413
|
+
variantKey,
|
|
414
|
+
});
|
|
415
|
+
changed = true;
|
|
416
|
+
}
|
|
417
|
+
if (changed) setMeasuredRowsVersion((v) => (v + 1) % 1000000);
|
|
418
|
+
// Prune the id→item / id→callback maps to the currently-mounted set so they
|
|
419
|
+
// do not grow unbounded over a long session. `els` is the live mounted set
|
|
420
|
+
// (ref(null) deletes on unmount), so anything not in it is gone.
|
|
421
|
+
if (liveItems.size > els.size) {
|
|
422
|
+
for (const key of liveItems.keys()) {
|
|
423
|
+
if (!els.has(key)) liveItems.delete(key);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const refCache = transcriptMeasureRefCache.current;
|
|
427
|
+
if (refCache.size > els.size) {
|
|
428
|
+
for (const key of refCache.keys()) {
|
|
429
|
+
if (!els.has(key)) refCache.delete(key);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
useLayoutEffect(() => {
|
|
434
|
+
const totalRows = Math.max(0, Number(transcriptWindow.totalRows) || 0);
|
|
435
|
+
const previousTotalRows = Math.max(0, Number(transcriptTotalRowsRef.current) || 0);
|
|
436
|
+
transcriptTotalRowsRef.current = totalRows;
|
|
437
|
+
const rowDelta = totalRows - previousTotalRows;
|
|
438
|
+
const curPrefix = transcriptRowIndex?.prefixRows || null;
|
|
439
|
+
if (previousTotalRows <= 0 || dragRef.current.active) return;
|
|
440
|
+
|
|
441
|
+
const currentTarget = Math.max(0, Number(scrollTargetRef.current) || 0);
|
|
442
|
+
const currentPosition = Math.max(0, Number(scrollPositionRef.current) || 0);
|
|
443
|
+
const currentOffset = Math.max(0, Number(scrollOffset) || 0);
|
|
444
|
+
const maxRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
|
|
445
|
+
const nearBottom = followingRef.current || currentTarget <= transcriptBottomSlackRows;
|
|
446
|
+
const pinnedToBottom = nearBottom;
|
|
447
|
+
// A genuine reading anchor must win over ordinary stream growth, but an
|
|
448
|
+
// explicit follow arm (prompt submit / pinned bottom) must win over stale
|
|
449
|
+
// anchor state. Manual scroll cancels followingRef before anchor capture, so
|
|
450
|
+
// deliberate reading still stays anchored while automatic bottom-follow
|
|
451
|
+
// stays armed across spinner/tool/stream height corrections.
|
|
452
|
+
const activeReadingAnchor = !!transcriptAnchorRef.current
|
|
453
|
+
&& !transcriptAnchorDirtyRef.current
|
|
454
|
+
&& !followingRef.current
|
|
455
|
+
&& !nearBottom;
|
|
456
|
+
const followOnGrowth = followingRef.current && rowDelta > 0 && !activeReadingAnchor;
|
|
457
|
+
const shouldFollowBottom = rowDelta > 0 && (followOnGrowth || pinnedToBottom);
|
|
458
|
+
if (shouldFollowBottom) {
|
|
459
|
+
// Bottom follow: while pinned to the newest output,
|
|
460
|
+
// do NOT animate row growth. The viewport is already bottom-aligned by
|
|
461
|
+
// justifyContent:flex-end; injecting a temporary positive scroll offset
|
|
462
|
+
// during streaming makes the transcript jump down/up and can clip the
|
|
463
|
+
// currently generated assistant text. Keep all scroll refs at zero so
|
|
464
|
+
// character generation stays visually stable.
|
|
465
|
+
stopSmoothScroll();
|
|
466
|
+
scrollTargetRef.current = 0;
|
|
467
|
+
scrollPositionRef.current = 0;
|
|
468
|
+
transcriptAnchorRef.current = null;
|
|
469
|
+
transcriptAnchorDirtyRef.current = false;
|
|
470
|
+
if (currentOffset !== 0) setScrollOffset(0);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Viewport-only changes, such as swapping TextEntryPanel for a picker, must
|
|
475
|
+
// not turn a bottom-pinned transcript into a reading-anchor lock.
|
|
476
|
+
if (rowDelta <= 0 && nearBottom) {
|
|
477
|
+
stopSmoothScroll();
|
|
478
|
+
scrollTargetRef.current = 0;
|
|
479
|
+
scrollPositionRef.current = 0;
|
|
480
|
+
transcriptAnchorRef.current = null;
|
|
481
|
+
transcriptAnchorDirtyRef.current = false;
|
|
482
|
+
if (currentOffset !== 0) setScrollOffset(0);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── ABSOLUTE ANCHOR LOCK ──────────────────────────────────────────────
|
|
487
|
+
// User is reading older transcript. Instead of folding the per-frame row
|
|
488
|
+
// DELTA into scrollOffset (which accumulated streaming-tail estimate jitter
|
|
489
|
+
// and could fall back to the whole-total delta on a misalignment — the
|
|
490
|
+
// "newline makes the screen jump" bug), we pin an ABSOLUTE anchor: the item
|
|
491
|
+
// id + row offset sitting at the viewport TOP edge. Every commit we look that
|
|
492
|
+
// item up in the CURRENT prefix table and re-derive scrollOffset so the
|
|
493
|
+
// anchored row stays at the same screen position. Any height change BELOW the
|
|
494
|
+
// anchor (streaming tail growth, a result landing under the reading position)
|
|
495
|
+
// only moves the bottom — the top is rock-stable. Changes ABOVE the anchor
|
|
496
|
+
// move the item's prefix start and are absorbed the same way. No deltas, no
|
|
497
|
+
// fallback, no drift.
|
|
498
|
+
const viewRows = Math.max(1, Number(transcriptContentHeight) || 1);
|
|
499
|
+
const itemList = items || [];
|
|
500
|
+
let anchor = transcriptAnchorRef.current;
|
|
501
|
+
// (Re)capture the anchor from the current viewport-top edge when missing or
|
|
502
|
+
// invalidated by a manual scroll. anchorRow = absolute row at the top edge.
|
|
503
|
+
if (!followingRef.current && (!anchor || transcriptAnchorDirtyRef.current)) {
|
|
504
|
+
if (curPrefix && curPrefix.length > 1) {
|
|
505
|
+
const anchorRow = Math.max(0, Math.min(totalRows, totalRows - currentTarget - viewRows));
|
|
506
|
+
let idx = upperBound(curPrefix, anchorRow) - 1;
|
|
507
|
+
if (idx < 0) idx = 0;
|
|
508
|
+
if (idx > itemList.length - 1) idx = itemList.length - 1;
|
|
509
|
+
const anchorItem = itemList[idx];
|
|
510
|
+
if (anchorItem && anchorItem.id != null) {
|
|
511
|
+
anchor = { id: anchorItem.id, offset: Math.max(0, anchorRow - (curPrefix[idx] || 0)) };
|
|
512
|
+
transcriptAnchorRef.current = anchor;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
transcriptAnchorDirtyRef.current = false;
|
|
516
|
+
// Just captured at the current offset; nothing to correct this frame.
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
// Resolve the anchored item's CURRENT position with the SAME pure helper the
|
|
520
|
+
// render path uses, so the post-commit state sync can never disagree with the
|
|
521
|
+
// synchronous render correction (which already fixed the screen this frame).
|
|
522
|
+
const desired = resolveAnchorScrollOffset({
|
|
523
|
+
anchor,
|
|
524
|
+
items: itemList,
|
|
525
|
+
curPrefix,
|
|
526
|
+
totalRows,
|
|
527
|
+
viewRows,
|
|
528
|
+
maxRows,
|
|
529
|
+
});
|
|
530
|
+
if (desired == null) {
|
|
531
|
+
// Anchor item gone (rare: removal/compaction). Re-capture next frame.
|
|
532
|
+
transcriptAnchorDirtyRef.current = true;
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const appliedDelta = desired - currentTarget;
|
|
536
|
+
if (appliedDelta === 0) return;
|
|
537
|
+
|
|
538
|
+
stopSmoothScroll();
|
|
539
|
+
scrollTargetRef.current = desired;
|
|
540
|
+
scrollPositionRef.current = Math.max(0, Math.min(maxRows, currentPosition + appliedDelta));
|
|
541
|
+
preservedScrollDeltaRef.current += appliedDelta;
|
|
542
|
+
setScrollOffset(Math.max(0, Math.round(desired)));
|
|
543
|
+
}, [transcriptWindow.totalRows, transcriptWindow.maxScrollRows, transcriptRowIndex, transcriptContentHeight, transcriptBottomSlackRows, scrollOffset, stopSmoothScroll]);
|
|
544
|
+
useLayoutEffect(() => {
|
|
545
|
+
if (transcriptBottomSlackRows <= 0) return;
|
|
546
|
+
if (transcriptAnchorRef.current || transcriptAnchorDirtyRef.current || followingRef.current) return;
|
|
547
|
+
const currentTarget = Math.max(0, Number(scrollTargetRef.current) || 0);
|
|
548
|
+
const currentPosition = Math.max(0, Number(scrollPositionRef.current) || 0);
|
|
549
|
+
const currentOffset = Math.max(0, Number(scrollOffset) || 0);
|
|
550
|
+
if (Math.max(currentTarget, currentPosition, currentOffset) === 0) return;
|
|
551
|
+
if (Math.max(currentTarget, currentPosition, currentOffset) > transcriptBottomSlackRows) return;
|
|
552
|
+
stopSmoothScroll();
|
|
553
|
+
scrollTargetRef.current = 0;
|
|
554
|
+
scrollPositionRef.current = 0;
|
|
555
|
+
setScrollOffset(0);
|
|
556
|
+
}, [transcriptBottomSlackRows, scrollOffset, stopSmoothScroll]);
|
|
557
|
+
useLayoutEffect(() => {
|
|
558
|
+
const top = Math.max(0, Number(transcriptViewportRef.current?.top) || 0);
|
|
559
|
+
const next = {
|
|
560
|
+
top,
|
|
561
|
+
height: Math.max(1, Number(transcriptContentHeight) || 1),
|
|
562
|
+
totalRows: Math.max(0, Number(transcriptWindow.totalRows) || 0),
|
|
563
|
+
scrollOffset: Math.max(0, Number(transcriptWindow.effectiveScrollOffset) || 0),
|
|
564
|
+
};
|
|
565
|
+
const preservedDelta = Number(preservedScrollDeltaRef.current) || 0;
|
|
566
|
+
if (preservedDelta !== 0) {
|
|
567
|
+
next.scrollOffset = Math.max(0, next.scrollOffset + preservedDelta);
|
|
568
|
+
preservedScrollDeltaRef.current = 0;
|
|
569
|
+
}
|
|
570
|
+
const previous = selectionLayoutRef.current;
|
|
571
|
+
selectionLayoutRef.current = next;
|
|
572
|
+
if (!previous || !dragRef.current.rect || dragRef.current.active) return;
|
|
573
|
+
const deltaY = (next.top - previous.top)
|
|
574
|
+
+ (next.height - previous.height)
|
|
575
|
+
- (next.totalRows - previous.totalRows)
|
|
576
|
+
+ (next.scrollOffset - previous.scrollOffset);
|
|
577
|
+
if (deltaY === 0) return;
|
|
578
|
+
const clippedRect = withSelectionClip(shiftSelectionRectY(dragRef.current.rect, deltaY));
|
|
579
|
+
dragRef.current = { ...dragRef.current, rect: clippedRect };
|
|
580
|
+
paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
|
|
581
|
+
}, [transcriptContentHeight, transcriptWindow.totalRows, transcriptWindow.effectiveScrollOffset, withSelectionClip, paintSelectionRect]);
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
if (!dragRef.current.rect) return;
|
|
584
|
+
const clippedRect = withSelectionClip(dragRef.current.rect);
|
|
585
|
+
dragRef.current = { ...dragRef.current, rect: clippedRect };
|
|
586
|
+
paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
|
|
587
|
+
}, [themeEpoch, withSelectionClip, paintSelectionRect]);
|
|
588
|
+
useEffect(() => {
|
|
589
|
+
const maxRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
|
|
590
|
+
if (scrollTargetRef.current <= maxRows && scrollPositionRef.current <= maxRows && scrollOffset <= maxRows) return;
|
|
591
|
+
stopSmoothScroll();
|
|
592
|
+
const next = Math.max(0, Math.min(maxRows, scrollTargetRef.current));
|
|
593
|
+
scrollTargetRef.current = next;
|
|
594
|
+
scrollPositionRef.current = next;
|
|
595
|
+
setScrollOffset(Math.round(next));
|
|
596
|
+
}, [transcriptWindow.maxScrollRows, scrollOffset, stopSmoothScroll]);
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
transcriptWindow,
|
|
600
|
+
renderedTranscriptItems,
|
|
601
|
+
transcriptTailPinned,
|
|
602
|
+
overlayHintAttachItemIndex,
|
|
603
|
+
overlayHintOnLastItem,
|
|
604
|
+
overlayHintFallbackRow,
|
|
605
|
+
transcriptMeasureRef,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
@@ -26,20 +26,23 @@ export function ConfirmBar({ buttons = [], focusedIndex = -1 }) {
|
|
|
26
26
|
const list = Array.isArray(buttons) ? buttons.filter(Boolean) : [];
|
|
27
27
|
if (list.length === 0) return null;
|
|
28
28
|
return (
|
|
29
|
-
<Box flexDirection="row"
|
|
29
|
+
<Box flexDirection="row" flexShrink={0} justifyContent="flex-start" alignItems="center">
|
|
30
30
|
{list.map((button, index) => {
|
|
31
31
|
const isFocused = index === focusedIndex;
|
|
32
32
|
return (
|
|
33
|
-
<
|
|
34
|
-
|
|
33
|
+
<Box
|
|
34
|
+
key={button.value ?? index}
|
|
35
|
+
marginLeft={index > 0 ? 1 : 0}
|
|
36
|
+
paddingX={1}
|
|
37
|
+
backgroundColor={isFocused ? theme.selectionBackground : undefined}
|
|
38
|
+
>
|
|
35
39
|
<Text
|
|
36
|
-
color={isFocused ? theme.selectionText : theme.
|
|
37
|
-
backgroundColor={isFocused ? theme.selectionBackground : undefined}
|
|
40
|
+
color={isFocused ? theme.selectionText : theme.subtle}
|
|
38
41
|
bold={isFocused}
|
|
39
42
|
>
|
|
40
|
-
{` ${button.label} `}
|
|
43
|
+
{`[ ${button.label} ]`}
|
|
41
44
|
</Text>
|
|
42
|
-
</
|
|
45
|
+
</Box>
|
|
43
46
|
);
|
|
44
47
|
})}
|
|
45
48
|
</Box>
|