mixdog 0.9.3 → 0.9.4
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/package.json +7 -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/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-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 +215 -29
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- 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-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +23 -63
- 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 +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- 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 +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -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 +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -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/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 +182 -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 +251 -397
- 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/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 +32 -41
- 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 +12 -292
- package/src/runtime/channels/index.mjs +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- 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/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 +312 -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 +36 -570
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- 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/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 +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -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/launcher-control.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 +84 -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 +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +319 -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/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 +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- 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 +945 -8094
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -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 +324 -0
- package/src/tui/app/model-options.mjs +330 -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 +490 -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 +671 -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 +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +23 -101
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- 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 +19652 -18630
- 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 +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- 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 +67 -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 +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- 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/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,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* use-transcript-scroll.mjs — transcript scroll + ink-grid selection engine.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from App.jsx: smooth scroll animation, reading-anchor
|
|
5
|
+
* capture, selection painting/throttling/clipping, word-line span extension,
|
|
6
|
+
* keyboard selection focus movement and the wheel/edge-drag coalescer.
|
|
7
|
+
* Scroll/drag state refs stay App-owned and are injected; this hook owns only
|
|
8
|
+
* its internal timers (animation interval, selection paint throttle, text
|
|
9
|
+
* capture defer, scroll coalescer).
|
|
10
|
+
*/
|
|
11
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
12
|
+
import { theme } from '../theme.mjs';
|
|
13
|
+
import {
|
|
14
|
+
SELECTION_PAINT_INTERVAL_MS,
|
|
15
|
+
SCROLL_COALESCE_MS,
|
|
16
|
+
selectionRectsEqual,
|
|
17
|
+
shiftSelectionRectY,
|
|
18
|
+
comparePoints,
|
|
19
|
+
upperBound,
|
|
20
|
+
} from './transcript-window.mjs';
|
|
21
|
+
|
|
22
|
+
export function useTranscriptScroll({
|
|
23
|
+
store,
|
|
24
|
+
frameColumns,
|
|
25
|
+
statuslineBandRows,
|
|
26
|
+
setScrollOffset,
|
|
27
|
+
scrollPositionRef,
|
|
28
|
+
scrollTargetRef,
|
|
29
|
+
maxScrollRowsRef,
|
|
30
|
+
transcriptBottomSlackRowsRef,
|
|
31
|
+
followingRef,
|
|
32
|
+
transcriptAnchorRef,
|
|
33
|
+
transcriptAnchorDirtyRef,
|
|
34
|
+
transcriptGeomRef,
|
|
35
|
+
dragRef,
|
|
36
|
+
frameRowsRef,
|
|
37
|
+
transcriptViewportRef,
|
|
38
|
+
selectionLayoutRef,
|
|
39
|
+
selectionTextRef,
|
|
40
|
+
}) {
|
|
41
|
+
const scrollAnimationRef = useRef(null);
|
|
42
|
+
const selectionPaintRef = useRef({ t: 0, rect: null, pending: null, timer: null });
|
|
43
|
+
// Coalescer for edge-drag auto-scroll + wheel scroll deltas (see
|
|
44
|
+
// SCROLL_COALESCE_MS). Both call sites accumulate into pendingRows.
|
|
45
|
+
const scrollCoalesceRef = useRef({ pendingRows: 0, timer: null });
|
|
46
|
+
const selectionTextTimerRef = useRef(null);
|
|
47
|
+
|
|
48
|
+
const stopSmoothScroll = useCallback(() => {
|
|
49
|
+
if (!scrollAnimationRef.current) return;
|
|
50
|
+
clearInterval(scrollAnimationRef.current);
|
|
51
|
+
scrollAnimationRef.current = null;
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const startSmoothScroll = useCallback(() => {
|
|
55
|
+
if (scrollAnimationRef.current) return;
|
|
56
|
+
scrollAnimationRef.current = setInterval(() => {
|
|
57
|
+
const current = scrollPositionRef.current;
|
|
58
|
+
const target = scrollTargetRef.current;
|
|
59
|
+
const next = current + (target - current) * 0.32;
|
|
60
|
+
if (Math.abs(target - next) < 0.12) {
|
|
61
|
+
scrollPositionRef.current = target;
|
|
62
|
+
setScrollOffset(Math.max(0, Math.round(target)));
|
|
63
|
+
followingRef.current = false;
|
|
64
|
+
stopSmoothScroll();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
scrollPositionRef.current = Math.max(0, next);
|
|
68
|
+
setScrollOffset(Math.max(0, Math.round(scrollPositionRef.current)));
|
|
69
|
+
}, 16);
|
|
70
|
+
scrollAnimationRef.current.unref?.();
|
|
71
|
+
}, [stopSmoothScroll]);
|
|
72
|
+
|
|
73
|
+
const cancelTranscriptFollow = useCallback(() => {
|
|
74
|
+
followingRef.current = false;
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const resetTranscriptScroll = useCallback(() => {
|
|
78
|
+
cancelTranscriptFollow();
|
|
79
|
+
stopSmoothScroll();
|
|
80
|
+
scrollPositionRef.current = 0;
|
|
81
|
+
scrollTargetRef.current = 0;
|
|
82
|
+
transcriptAnchorRef.current = null;
|
|
83
|
+
transcriptAnchorDirtyRef.current = false;
|
|
84
|
+
setScrollOffset(0);
|
|
85
|
+
}, [stopSmoothScroll, cancelTranscriptFollow]);
|
|
86
|
+
|
|
87
|
+
const armTranscriptFollow = useCallback(() => {
|
|
88
|
+
// Do not mutate scrollOffset here. During prompt submit the transcript rows
|
|
89
|
+
// have not necessarily been committed yet; resetting immediately makes a
|
|
90
|
+
// long transcript jump to the bottom, then jump again when the new row is
|
|
91
|
+
// appended. Keep the current viewport stable and let the row-delta effect
|
|
92
|
+
// perform the single bottom-follow when the transcript actually grows.
|
|
93
|
+
transcriptAnchorRef.current = null;
|
|
94
|
+
transcriptAnchorDirtyRef.current = false;
|
|
95
|
+
followingRef.current = true;
|
|
96
|
+
stopSmoothScroll();
|
|
97
|
+
}, [stopSmoothScroll]);
|
|
98
|
+
|
|
99
|
+
const rememberSelectionTextSoon = useCallback(() => {
|
|
100
|
+
if (selectionTextTimerRef.current) return;
|
|
101
|
+
selectionTextTimerRef.current = setTimeout(() => {
|
|
102
|
+
selectionTextTimerRef.current = null;
|
|
103
|
+
const text = store.getRenderSelectionText?.();
|
|
104
|
+
if (text && text.trim()) selectionTextRef.current = text;
|
|
105
|
+
}, 0);
|
|
106
|
+
}, [store]);
|
|
107
|
+
|
|
108
|
+
const selectionClip = useCallback(() => {
|
|
109
|
+
// The status-bar grid selection lives in the bottom statusline band, not the
|
|
110
|
+
// transcript viewport — clip there so the highlight cannot spill into the
|
|
111
|
+
// prompt/transcript rows. Everything else (transcript, word-select) keeps the
|
|
112
|
+
// transcript-viewport clip.
|
|
113
|
+
if (dragRef.current.region === 'status') {
|
|
114
|
+
const rows = Math.max(1, Number(frameRowsRef.current) || 24);
|
|
115
|
+
const top = Math.max(0, rows - statuslineBandRows);
|
|
116
|
+
return { y1: top, y2: Math.max(top, rows - 1) };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
y1: Math.max(0, Number(transcriptViewportRef.current?.top) || 0),
|
|
120
|
+
y2: Math.max(0, Number(transcriptViewportRef.current?.bottom) || 0),
|
|
121
|
+
};
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
const withSelectionClip = useCallback((rect, options = {}) => {
|
|
125
|
+
if (!rect) return null;
|
|
126
|
+
const clip = selectionClip();
|
|
127
|
+
const clipped = {
|
|
128
|
+
...rect,
|
|
129
|
+
clipY1: clip.y1,
|
|
130
|
+
clipY2: Math.max(clip.y1, clip.y2),
|
|
131
|
+
selectionForeground: theme.selectionHighlightText || theme.selectionText,
|
|
132
|
+
selectionBackground: theme.selectionHighlightBackground || theme.selectionBackground,
|
|
133
|
+
};
|
|
134
|
+
if (options.captureText === false) clipped.captureText = false;
|
|
135
|
+
return clipped;
|
|
136
|
+
}, [selectionClip]);
|
|
137
|
+
|
|
138
|
+
const paintSelectionRect = useCallback((clippedRect, { rememberText = true, immediate = false } = {}) => {
|
|
139
|
+
const nextRect = clippedRect || null;
|
|
140
|
+
const state = selectionPaintRef.current;
|
|
141
|
+
if (selectionRectsEqual(state.rect, nextRect)) {
|
|
142
|
+
const needsCapture = nextRect && rememberText && nextRect.captureText !== false;
|
|
143
|
+
if (!immediate && !needsCapture) return false;
|
|
144
|
+
if (immediate || needsCapture) {
|
|
145
|
+
store.setRenderSelection?.(nextRect, { immediate: true });
|
|
146
|
+
}
|
|
147
|
+
if (needsCapture) rememberSelectionTextSoon();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
state.rect = nextRect;
|
|
151
|
+
state.t = Date.now();
|
|
152
|
+
store.setRenderSelection?.(nextRect, immediate ? { immediate: true } : undefined);
|
|
153
|
+
if (nextRect && rememberText && nextRect.captureText !== false) rememberSelectionTextSoon();
|
|
154
|
+
return true;
|
|
155
|
+
}, [store, rememberSelectionTextSoon]);
|
|
156
|
+
|
|
157
|
+
const applySelectionRect = useCallback((rect) => {
|
|
158
|
+
const clippedRect = withSelectionClip(rect);
|
|
159
|
+
dragRef.current.rect = clippedRect || null;
|
|
160
|
+
if (!clippedRect) selectionTextRef.current = '';
|
|
161
|
+
const state = selectionPaintRef.current;
|
|
162
|
+
if (state.timer) {
|
|
163
|
+
clearTimeout(state.timer);
|
|
164
|
+
state.timer = null;
|
|
165
|
+
state.pending = null;
|
|
166
|
+
}
|
|
167
|
+
paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
|
|
168
|
+
}, [paintSelectionRect, withSelectionClip]);
|
|
169
|
+
|
|
170
|
+
const applySelectionRectThrottled = useCallback((rect) => {
|
|
171
|
+
const clippedRect = withSelectionClip(rect, { captureText: false });
|
|
172
|
+
if (selectionRectsEqual(dragRef.current.rect, clippedRect)) return;
|
|
173
|
+
dragRef.current.rect = clippedRect || null;
|
|
174
|
+
const state = selectionPaintRef.current;
|
|
175
|
+
if (selectionRectsEqual(state.rect, clippedRect)) return;
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const elapsed = now - state.t;
|
|
178
|
+
if (elapsed >= SELECTION_PAINT_INTERVAL_MS) {
|
|
179
|
+
if (state.timer) {
|
|
180
|
+
clearTimeout(state.timer);
|
|
181
|
+
state.timer = null;
|
|
182
|
+
state.pending = null;
|
|
183
|
+
}
|
|
184
|
+
paintSelectionRect(clippedRect, { rememberText: false });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
state.pending = clippedRect || null;
|
|
188
|
+
if (!state.timer) {
|
|
189
|
+
state.timer = setTimeout(() => {
|
|
190
|
+
const current = selectionPaintRef.current;
|
|
191
|
+
const pending = current.pending;
|
|
192
|
+
current.timer = null;
|
|
193
|
+
current.pending = null;
|
|
194
|
+
paintSelectionRect(pending, { rememberText: false });
|
|
195
|
+
}, Math.max(1, SELECTION_PAINT_INTERVAL_MS - elapsed));
|
|
196
|
+
state.timer.unref?.();
|
|
197
|
+
}
|
|
198
|
+
}, [paintSelectionRect, withSelectionClip]);
|
|
199
|
+
|
|
200
|
+
const selectionPointAtCurrentScroll = useCallback((point, pointScroll = 0) => {
|
|
201
|
+
if (!point) return null;
|
|
202
|
+
return {
|
|
203
|
+
x: point.x,
|
|
204
|
+
y: point.y + (Number(scrollTargetRef.current) || 0) - (Number(pointScroll) || 0),
|
|
205
|
+
};
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
// Port of selection.ts extendSelection onto the linear-rect model, hoisted to
|
|
209
|
+
// component scope so BOTH the mouse handler (motion/release) AND the
|
|
210
|
+
// auto-scroll path (scrollTranscriptRows) can rebuild a span-aware rect. Grows
|
|
211
|
+
// a word/line multi-click selection from its anchor span to the word/line under
|
|
212
|
+
// the cursor: target ends before the span → extend backward (span.hi→targetLo);
|
|
213
|
+
// target starts after → extend forward (span.lo→targetHi); overlapping → the
|
|
214
|
+
// span. The moving end snaps to the word (getWordRectAt) or line (getLineRectAt)
|
|
215
|
+
// at the cursor; a miss (blank/gutter) falls back to the raw cell. spanScroll
|
|
216
|
+
// re-anchors the span to the current transcript scroll (status never scrolls) so
|
|
217
|
+
// the original word/line tracks the content while dragging/auto-scrolling.
|
|
218
|
+
const buildSpanRect = useCallback((span, x, y, region, spanScroll = 0) => {
|
|
219
|
+
const conv = (pt) => (region === 'status' ? pt : selectionPointAtCurrentScroll(pt, spanScroll));
|
|
220
|
+
const spanLo = conv(span.lo);
|
|
221
|
+
const spanHi = conv(span.hi);
|
|
222
|
+
let mLo;
|
|
223
|
+
let mHi;
|
|
224
|
+
if (span.kind === 'word') {
|
|
225
|
+
const wr = store.getWordRectAt?.(x, y);
|
|
226
|
+
if (wr) { mLo = { x: wr.x1, y: wr.y1 }; mHi = { x: wr.x2, y: wr.y2 }; }
|
|
227
|
+
else { mLo = { x, y }; mHi = { x, y }; }
|
|
228
|
+
} else {
|
|
229
|
+
const lr = store.getLineRectAt?.(y);
|
|
230
|
+
if (lr) { mLo = { x: lr.x1, y: lr.y1 }; mHi = { x: lr.x2, y: lr.y2 }; }
|
|
231
|
+
else { mLo = { x: 0, y }; mHi = { x, y }; }
|
|
232
|
+
}
|
|
233
|
+
const rect = (a, b) => ({ mode: 'linear', x1: a.x, y1: a.y, x2: b.x, y2: b.y });
|
|
234
|
+
if (comparePoints(mHi, spanLo) < 0) return rect(spanHi, mLo);
|
|
235
|
+
if (comparePoints(mLo, spanHi) > 0) return rect(spanLo, mHi);
|
|
236
|
+
return rect(spanLo, spanHi);
|
|
237
|
+
}, [store, selectionPointAtCurrentScroll]);
|
|
238
|
+
|
|
239
|
+
const transcriptViewportRows = useCallback(() => {
|
|
240
|
+
const top = Math.max(0, Number(transcriptViewportRef.current?.top) || 0);
|
|
241
|
+
const bottom = Math.max(top, Number(transcriptViewportRef.current?.bottom) || top);
|
|
242
|
+
return { top, bottom };
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
245
|
+
const statusBandRows = useCallback(() => {
|
|
246
|
+
const rows = Math.max(1, Number(frameRowsRef.current) || 24);
|
|
247
|
+
const top = Math.max(0, rows - statuslineBandRows);
|
|
248
|
+
return { top, bottom: Math.max(top, rows - 1) };
|
|
249
|
+
}, []);
|
|
250
|
+
|
|
251
|
+
const selectionMaxColAtRow = useCallback((row) => {
|
|
252
|
+
const lr = store.getLineRectAt?.(row);
|
|
253
|
+
if (lr != null && Number.isFinite(lr.x2)) return Math.max(0, lr.x2);
|
|
254
|
+
return Math.max(0, frameColumns - 1);
|
|
255
|
+
}, [store, frameColumns]);
|
|
256
|
+
|
|
257
|
+
// Synchronous predicate: is a transcript/status ink-grid selection live? Used
|
|
258
|
+
// both by App (to consume Shift+Arrow even when focus clamps at an edge) and
|
|
259
|
+
// by PromptInput (via prop) to gate its own Shift+Arrow at event time — a flag
|
|
260
|
+
// set inside App's parent handler would be one event stale (parent handler
|
|
261
|
+
// runs AFTER the child prompt handler for the same key).
|
|
262
|
+
const gridSelectionActiveRef = useRef(() => {
|
|
263
|
+
const drag = dragRef.current;
|
|
264
|
+
if (!drag || drag.active) return false;
|
|
265
|
+
if (drag.region !== 'transcript' && drag.region !== 'status') return false;
|
|
266
|
+
const rect = drag.rect;
|
|
267
|
+
return Boolean(rect) && !(rect.x1 === rect.x2 && rect.y1 === rect.y2);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
useEffect(() => () => {
|
|
271
|
+
const paintState = selectionPaintRef.current;
|
|
272
|
+
if (paintState.timer) clearTimeout(paintState.timer);
|
|
273
|
+
paintState.timer = null;
|
|
274
|
+
paintState.pending = null;
|
|
275
|
+
if (selectionTextTimerRef.current) clearTimeout(selectionTextTimerRef.current);
|
|
276
|
+
selectionTextTimerRef.current = null;
|
|
277
|
+
const coalesceState = scrollCoalesceRef.current;
|
|
278
|
+
if (coalesceState.timer) clearTimeout(coalesceState.timer);
|
|
279
|
+
coalesceState.timer = null;
|
|
280
|
+
coalesceState.pendingRows = 0;
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
const scrollTranscriptRows = useCallback((deltaRows, options = {}) => {
|
|
284
|
+
const maxTarget = Math.max(0, Number(maxScrollRowsRef.current) || 0);
|
|
285
|
+
const target = Math.max(0, Math.min(maxTarget, scrollTargetRef.current + deltaRows));
|
|
286
|
+
const appliedDelta = target - scrollTargetRef.current;
|
|
287
|
+
// Any manual wheel/keyboard scroll takes precedence over an in-flight
|
|
288
|
+
// transcript follow: drop the glide so the user's intent wins.
|
|
289
|
+
if (appliedDelta !== 0) cancelTranscriptFollow();
|
|
290
|
+
scrollTargetRef.current = target;
|
|
291
|
+
// A manual scroll moves the reading position. Capture the new reading anchor
|
|
292
|
+
// SYNCHRONOUSLY from the latest published geometry so the very next render
|
|
293
|
+
// already locks to it — no one-frame "dirty" window where concurrent
|
|
294
|
+
// streaming growth could lurch the view. At/over the bottom, drop the anchor
|
|
295
|
+
// so the bottom-follow path owns the viewport again.
|
|
296
|
+
if (appliedDelta !== 0) {
|
|
297
|
+
if (target <= Math.max(0, Number(transcriptBottomSlackRowsRef.current) || 0)) {
|
|
298
|
+
transcriptAnchorRef.current = null;
|
|
299
|
+
transcriptAnchorDirtyRef.current = false;
|
|
300
|
+
} else {
|
|
301
|
+
const geom = transcriptGeomRef.current || {};
|
|
302
|
+
const prefixRows = geom.prefixRows;
|
|
303
|
+
if (Array.isArray(prefixRows) && prefixRows.length > 1) {
|
|
304
|
+
const gTotal = Math.max(0, Number(geom.totalRows) || 0);
|
|
305
|
+
const gView = Math.max(1, Number(geom.viewRows) || 1);
|
|
306
|
+
const anchorRow = Math.max(0, Math.min(gTotal, gTotal - target - gView));
|
|
307
|
+
let idx = upperBound(prefixRows, anchorRow) - 1;
|
|
308
|
+
if (idx < 0) idx = 0;
|
|
309
|
+
if (idx > prefixRows.length - 2) idx = prefixRows.length - 2;
|
|
310
|
+
const items = geom.items || [];
|
|
311
|
+
const anchorItem = items[idx];
|
|
312
|
+
if (anchorItem && anchorItem.id != null) {
|
|
313
|
+
transcriptAnchorRef.current = { id: anchorItem.id, offset: Math.max(0, anchorRow - (prefixRows[idx] || 0)) };
|
|
314
|
+
transcriptAnchorDirtyRef.current = false;
|
|
315
|
+
} else {
|
|
316
|
+
transcriptAnchorDirtyRef.current = true;
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
transcriptAnchorDirtyRef.current = true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (appliedDelta !== 0 && selectionLayoutRef.current) {
|
|
324
|
+
selectionLayoutRef.current = { ...selectionLayoutRef.current, scrollOffset: target };
|
|
325
|
+
}
|
|
326
|
+
if (appliedDelta !== 0 && dragRef.current.rect) {
|
|
327
|
+
let rect;
|
|
328
|
+
if (dragRef.current.active) {
|
|
329
|
+
const { anchor, anchorScroll, last, anchorSpan, region } = dragRef.current;
|
|
330
|
+
if (anchorSpan && last) {
|
|
331
|
+
// Word/line multi-click drag that reached the edge: keep extending by
|
|
332
|
+
// whole words/lines from the span to the word/line at the current cell,
|
|
333
|
+
// NOT collapsing to a char {anchor->last} rect. Mirrors the motion path.
|
|
334
|
+
rect = buildSpanRect(anchorSpan, last.x, last.y, region, anchorScroll);
|
|
335
|
+
} else {
|
|
336
|
+
const currentAnchor = selectionPointAtCurrentScroll(anchor, anchorScroll);
|
|
337
|
+
rect = currentAnchor && last ? { mode: 'linear', x1: currentAnchor.x, y1: currentAnchor.y, x2: last.x, y2: last.y } : null;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
rect = shiftSelectionRectY(dragRef.current.rect, appliedDelta);
|
|
341
|
+
}
|
|
342
|
+
const clippedRect = withSelectionClip(rect);
|
|
343
|
+
dragRef.current = { ...dragRef.current, rect: clippedRect };
|
|
344
|
+
paintSelectionRect(clippedRect, { rememberText: !dragRef.current.active });
|
|
345
|
+
}
|
|
346
|
+
if (options.smooth) {
|
|
347
|
+
startSmoothScroll();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
stopSmoothScroll();
|
|
351
|
+
scrollPositionRef.current = target;
|
|
352
|
+
setScrollOffset(Math.round(target));
|
|
353
|
+
}, [startSmoothScroll, stopSmoothScroll, paintSelectionRect, selectionPointAtCurrentScroll, withSelectionClip, cancelTranscriptFollow, buildSpanRect]);
|
|
354
|
+
|
|
355
|
+
// Leading-edge coalescer for edge-drag auto-scroll + wheel deltas: the first
|
|
356
|
+
// delta after an idle period flushes immediately (single wheel ticks/short
|
|
357
|
+
// drags stay responsive), while a flood of deltas within SCROLL_COALESCE_MS
|
|
358
|
+
// accumulates into one scrollTranscriptRows call per tick instead of one per
|
|
359
|
+
// mousemove/wheel event. Both call sites below route through this instead of
|
|
360
|
+
// calling scrollTranscriptRows directly.
|
|
361
|
+
const queueScrollCoalesced = useCallback((deltaRows) => {
|
|
362
|
+
const state = scrollCoalesceRef.current;
|
|
363
|
+
state.pendingRows += deltaRows;
|
|
364
|
+
if (state.timer) return;
|
|
365
|
+
const rows = state.pendingRows;
|
|
366
|
+
state.pendingRows = 0;
|
|
367
|
+
scrollTranscriptRows(rows);
|
|
368
|
+
state.timer = setTimeout(() => {
|
|
369
|
+
state.timer = null;
|
|
370
|
+
if (state.pendingRows !== 0) {
|
|
371
|
+
const remaining = state.pendingRows;
|
|
372
|
+
state.pendingRows = 0;
|
|
373
|
+
scrollTranscriptRows(remaining);
|
|
374
|
+
}
|
|
375
|
+
}, SCROLL_COALESCE_MS);
|
|
376
|
+
state.timer.unref?.();
|
|
377
|
+
}, [scrollTranscriptRows]);
|
|
378
|
+
|
|
379
|
+
// NOTE: declared AFTER scrollTranscriptRows — it appears in the deps array
|
|
380
|
+
// below, and useCallback deps are evaluated at render time, so referencing
|
|
381
|
+
// it before its const initializer would throw a TDZ ReferenceError.
|
|
382
|
+
const moveSelectionFocus = useCallback((move) => {
|
|
383
|
+
const drag = dragRef.current;
|
|
384
|
+
if (drag.active) return false;
|
|
385
|
+
const region = drag.region;
|
|
386
|
+
if (region !== 'transcript' && region !== 'status') return false;
|
|
387
|
+
const rect = drag.rect;
|
|
388
|
+
if (!rect) return false;
|
|
389
|
+
if (rect.x1 === rect.x2 && rect.y1 === rect.y2) return false;
|
|
390
|
+
|
|
391
|
+
let anchor = { x: rect.x1, y: rect.y1 };
|
|
392
|
+
let col = rect.x2;
|
|
393
|
+
let row = rect.y2;
|
|
394
|
+
const beforeCol = col;
|
|
395
|
+
const beforeRow = row;
|
|
396
|
+
// Set when Shift+Up/Down hits the viewport edge and we scroll the
|
|
397
|
+
// transcript to reveal a new row rather than moving within the current
|
|
398
|
+
// viewport (mirrors the mouse edge-drag auto-scroll at the drag-motion
|
|
399
|
+
// handler). In that case row/col numerically equal their "before" values
|
|
400
|
+
// (both clamp to the same viewport edge index) even though the
|
|
401
|
+
// underlying content changed, so the generic before/after guard below
|
|
402
|
+
// must not treat it as a no-op.
|
|
403
|
+
let scrolledEdge = false;
|
|
404
|
+
|
|
405
|
+
const { top, bottom } = region === 'status' ? statusBandRows() : transcriptViewportRows();
|
|
406
|
+
|
|
407
|
+
switch (move) {
|
|
408
|
+
case 'left':
|
|
409
|
+
if (col > 0) col -= 1;
|
|
410
|
+
else if (row > top) {
|
|
411
|
+
row -= 1;
|
|
412
|
+
col = selectionMaxColAtRow(row);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
case 'right': {
|
|
416
|
+
const maxCol = selectionMaxColAtRow(row);
|
|
417
|
+
if (col < maxCol) col += 1;
|
|
418
|
+
else if (row < bottom) {
|
|
419
|
+
row += 1;
|
|
420
|
+
col = 0;
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case 'up':
|
|
425
|
+
if (row > top) {
|
|
426
|
+
row -= 1;
|
|
427
|
+
} else if (region === 'transcript') {
|
|
428
|
+
// Already at the top visible row: scroll the transcript up by one
|
|
429
|
+
// row (same direction as the mouse edge-drag auto-scroll at the
|
|
430
|
+
// top edge, App.jsx ~3060) instead of clamping the selection in
|
|
431
|
+
// place, then extend the focus onto the newly revealed top row.
|
|
432
|
+
const beforeTarget = scrollTargetRef.current;
|
|
433
|
+
scrollTranscriptRows(1);
|
|
434
|
+
if (scrollTargetRef.current !== beforeTarget) {
|
|
435
|
+
scrolledEdge = true;
|
|
436
|
+
// scrollTranscriptRows REPLACES dragRef.current with a shifted
|
|
437
|
+
// copy — re-read it; the local `drag` binding is stale here.
|
|
438
|
+
const shiftedRect = dragRef.current.rect;
|
|
439
|
+
if (shiftedRect) anchor = { x: shiftedRect.x1, y: shiftedRect.y1 };
|
|
440
|
+
row = top;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
break;
|
|
444
|
+
case 'down':
|
|
445
|
+
if (row < bottom) {
|
|
446
|
+
row += 1;
|
|
447
|
+
} else if (region === 'transcript') {
|
|
448
|
+
// Already at the bottom visible row: scroll down by one row (mirrors
|
|
449
|
+
// the mouse edge-drag auto-scroll at the bottom edge, App.jsx
|
|
450
|
+
// ~3062) instead of clamping, then extend the focus onto the newly
|
|
451
|
+
// revealed bottom row.
|
|
452
|
+
const beforeTarget = scrollTargetRef.current;
|
|
453
|
+
scrollTranscriptRows(-1);
|
|
454
|
+
if (scrollTargetRef.current !== beforeTarget) {
|
|
455
|
+
scrolledEdge = true;
|
|
456
|
+
// Re-read post-scroll dragRef.current (see 'up' case).
|
|
457
|
+
const shiftedRect = dragRef.current.rect;
|
|
458
|
+
if (shiftedRect) anchor = { x: shiftedRect.x1, y: shiftedRect.y1 };
|
|
459
|
+
row = bottom;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
case 'lineStart':
|
|
464
|
+
col = 0;
|
|
465
|
+
break;
|
|
466
|
+
case 'lineEnd':
|
|
467
|
+
col = selectionMaxColAtRow(row);
|
|
468
|
+
break;
|
|
469
|
+
default:
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
row = Math.max(top, Math.min(bottom, row));
|
|
474
|
+
col = Math.max(0, Math.min(selectionMaxColAtRow(row), col));
|
|
475
|
+
|
|
476
|
+
if (!scrolledEdge && col === beforeCol && row === beforeRow) return false;
|
|
477
|
+
|
|
478
|
+
// After an edge scroll dragRef.current is a NEW object; mutate the live
|
|
479
|
+
// one, not the stale entry binding.
|
|
480
|
+
const dragNow = dragRef.current;
|
|
481
|
+
if (dragNow.anchorSpan) dragNow.anchorSpan = null;
|
|
482
|
+
|
|
483
|
+
const focus = { x: col, y: row };
|
|
484
|
+
applySelectionRect({
|
|
485
|
+
mode: 'linear',
|
|
486
|
+
x1: anchor.x,
|
|
487
|
+
y1: anchor.y,
|
|
488
|
+
x2: focus.x,
|
|
489
|
+
y2: focus.y,
|
|
490
|
+
});
|
|
491
|
+
dragNow.last = { x: focus.x, y: focus.y };
|
|
492
|
+
return true;
|
|
493
|
+
}, [applySelectionRect, statusBandRows, transcriptViewportRows, selectionMaxColAtRow, scrollTranscriptRows]);
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
stopSmoothScroll,
|
|
497
|
+
resetTranscriptScroll,
|
|
498
|
+
armTranscriptFollow,
|
|
499
|
+
withSelectionClip,
|
|
500
|
+
paintSelectionRect,
|
|
501
|
+
applySelectionRect,
|
|
502
|
+
applySelectionRectThrottled,
|
|
503
|
+
selectionPointAtCurrentScroll,
|
|
504
|
+
buildSpanRect,
|
|
505
|
+
gridSelectionActiveRef,
|
|
506
|
+
scrollTranscriptRows,
|
|
507
|
+
queueScrollCoalesced,
|
|
508
|
+
moveSelectionFocus,
|
|
509
|
+
};
|
|
510
|
+
}
|