mixdog 0.9.95 → 0.9.97
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +9 -6
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +31 -33
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +10 -3
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -69,6 +69,22 @@ export const SELECTION_PAINT_INTERVAL_MS = positiveIntEnv('MIXDOG_TUI_SELECTION_
|
|
|
69
69
|
export const SCROLL_COALESCE_MS = positiveIntEnv('MIXDOG_TUI_SCROLL_COALESCE_MS', 16);
|
|
70
70
|
export const PROMPT_HISTORY_LIMIT = 50;
|
|
71
71
|
|
|
72
|
+
// Accumulate same-direction wheel/edge-drag deltas, but never retain movement
|
|
73
|
+
// from the opposite direction. Without this reset a quick wheel reversal could
|
|
74
|
+
// apply an older queued delta one frame later and visibly roll the transcript
|
|
75
|
+
// back against the user's latest gesture. Returns true when the direction
|
|
76
|
+
// changed so the caller can cancel its old coalescing timer and flush now.
|
|
77
|
+
export function accumulateDirectionalScrollDelta(state, deltaRows) {
|
|
78
|
+
const delta = Number(deltaRows) || 0;
|
|
79
|
+
if (!delta) return false;
|
|
80
|
+
const direction = Math.sign(delta);
|
|
81
|
+
const reversed = Boolean(state.direction && state.direction !== direction);
|
|
82
|
+
if (reversed) state.pendingRows = 0;
|
|
83
|
+
state.direction = direction;
|
|
84
|
+
state.pendingRows += delta;
|
|
85
|
+
return reversed;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
// Parse a boolean env var that DEFAULTS ON. Any of 0/false/off/no (case-
|
|
73
89
|
// insensitive, trimmed) disables it; everything else (including unset) leaves it
|
|
74
90
|
// on. Used as the kill switch for the app-level measured-height feature below.
|
|
@@ -647,6 +663,28 @@ export function transcriptRowAt(values, index) {
|
|
|
647
663
|
: (Number(values[index]) || 0);
|
|
648
664
|
}
|
|
649
665
|
|
|
666
|
+
/** True when a transcript items swap destroyed the reader's position and the
|
|
667
|
+
* viewport must return to the live tail with follow re-armed.
|
|
668
|
+
*
|
|
669
|
+
* A bulk swap (session load/clear, mid-turn COMPACTION) drops the history the
|
|
670
|
+
* reader was anchored to. Compaction pushes its status row in the SAME commit
|
|
671
|
+
* that trims the head, so the item count can stay level or even grow: a
|
|
672
|
+
* count-drop test alone left auto-scroll released with an anchor lock on a
|
|
673
|
+
* deleted row, and every new row piled up below the fold for the rest of the
|
|
674
|
+
* session (user: 컴팩션된 이후로 자동 스크롤이 풀린다). Live appends never
|
|
675
|
+
* touch index 0, so a changed HEAD id is the bulk-swap signal. */
|
|
676
|
+
export function transcriptSwapReturnsToTail({
|
|
677
|
+
count = 0,
|
|
678
|
+
previousCount = 0,
|
|
679
|
+
firstId = null,
|
|
680
|
+
previousFirstId = null,
|
|
681
|
+
} = {}) {
|
|
682
|
+
if (previousCount === 0) return true;
|
|
683
|
+
if (count < previousCount) return true;
|
|
684
|
+
if (count === 0) return false;
|
|
685
|
+
return firstId != null && previousFirstId != null && firstId !== previousFirstId;
|
|
686
|
+
}
|
|
687
|
+
|
|
650
688
|
function appendTranscriptRow(prefix, tailValue) {
|
|
651
689
|
return Object.freeze({
|
|
652
690
|
length: prefix.length + 1,
|
|
@@ -146,13 +146,11 @@ export function usePromptDraftFlow({
|
|
|
146
146
|
}, [slashCommands, slashIndex]);
|
|
147
147
|
|
|
148
148
|
const cancelSlashPalette = useCallback((value = '') => {
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
setSlashDismissedFor('');
|
|
153
|
-
|
|
154
|
-
setPromptDraftOverride({ id: Date.now(), value: '' });
|
|
155
|
-
}, []);
|
|
149
|
+
// Claude Code's autocomplete:dismiss closes suggestions without changing
|
|
150
|
+
// the draft. Remember this exact value so the palette does not immediately
|
|
151
|
+
// reopen; the next edit clears the marker in onPromptDraftChange.
|
|
152
|
+
setSlashDismissedFor(String(value ?? ''));
|
|
153
|
+
}, [setSlashDismissedFor]);
|
|
156
154
|
|
|
157
155
|
return {
|
|
158
156
|
onPromptDraftChange,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* byte-identical to the original inline hooks. Import-level helpers (paste
|
|
10
10
|
* attachments) are imported directly here.
|
|
11
11
|
*/
|
|
12
|
-
import { useCallback } from 'react';
|
|
12
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
13
13
|
import {
|
|
14
14
|
readClipboardImageAttachment,
|
|
15
15
|
readClipboardText,
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
shouldFoldPastedText,
|
|
21
21
|
} from '../paste-attachments.mjs';
|
|
22
22
|
import { promptHistoryKey } from '../prompt-history-store.mjs';
|
|
23
|
+
import { promptInterruptRestoreText } from '../components/prompt-input/interrupt-policy.mjs';
|
|
24
|
+
import { PROMPT_ESCAPE_CLEAR_WINDOW_MS } from '../components/prompt-input/escape-policy.mjs';
|
|
23
25
|
|
|
24
26
|
export function usePromptHandlers({
|
|
25
27
|
store,
|
|
@@ -44,6 +46,7 @@ export function usePromptHandlers({
|
|
|
44
46
|
recentPromptHistory,
|
|
45
47
|
resetPromptHistoryNav,
|
|
46
48
|
restoreQueuedToPrompt,
|
|
49
|
+
openMessageSelector,
|
|
47
50
|
usagePanel,
|
|
48
51
|
closeUsagePanel,
|
|
49
52
|
contextPanel,
|
|
@@ -55,6 +58,8 @@ export function usePromptHandlers({
|
|
|
55
58
|
clearPastedTextsSnapshot,
|
|
56
59
|
registerPastedText,
|
|
57
60
|
}) {
|
|
61
|
+
const interruptGenerationRef = useRef(0);
|
|
62
|
+
const pendingInterruptRestoreRef = useRef(null);
|
|
58
63
|
const handlePromptPaste = useCallback((text, meta = {}) => {
|
|
59
64
|
const source = String(meta?.source || 'paste');
|
|
60
65
|
const value = String(text ?? '');
|
|
@@ -197,41 +202,101 @@ export function usePromptHandlers({
|
|
|
197
202
|
|
|
198
203
|
// ESC / Up handling (prompt input):
|
|
199
204
|
// - prompt-local overlays such as the slash palette close first.
|
|
200
|
-
// -
|
|
201
|
-
// - non-empty
|
|
202
|
-
//
|
|
203
|
-
//
|
|
205
|
+
// - active work is cancelled before queue/draft handling.
|
|
206
|
+
// - idle non-empty text uses Claude Code's "Esc again to clear" guard.
|
|
207
|
+
// - idle empty input restores queued editable messages, and a double press
|
|
208
|
+
// opens the message selector (jump back to a previous prompt).
|
|
204
209
|
const handlePromptEscape = useCallback((text = '', meta = {}) => {
|
|
205
210
|
if (usagePanel) { closeUsagePanel(); return true; }
|
|
206
211
|
if (contextPanel) { setContextPanel(null); return true; }
|
|
207
212
|
|
|
213
|
+
if (meta.phase === 'clear-arm') {
|
|
214
|
+
showPromptHint('Esc again to clear', 'plain', PROMPT_ESCAPE_CLEAR_WINDOW_MS);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (meta.phase === 'select-arm') {
|
|
218
|
+
showPromptHint('Esc again to pick a message', 'plain', PROMPT_ESCAPE_CLEAR_WINDOW_MS);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if (meta.phase === 'select') {
|
|
222
|
+
clearPromptHint();
|
|
223
|
+
// Queue first (Claude Code order): a stale queue projection must not let
|
|
224
|
+
// the selector shadow an editable follow-up that is still waiting.
|
|
225
|
+
if (restoreQueuedToPrompt({ restoreDraft: true, showHint: false, currentText: text })) return true;
|
|
226
|
+
return openMessageSelector?.() === true;
|
|
227
|
+
}
|
|
208
228
|
if (meta.phase === 'clear') {
|
|
229
|
+
try {
|
|
230
|
+
const remembered = store.rememberPromptHistory?.(text);
|
|
231
|
+
if (remembered?.catch) void remembered.catch(() => {});
|
|
232
|
+
} catch { /* best-effort history parity */ }
|
|
209
233
|
clearPastedImagesSnapshot();
|
|
234
|
+
clearPastedTextsSnapshot();
|
|
210
235
|
clearPromptHint();
|
|
211
236
|
return false;
|
|
212
237
|
}
|
|
213
238
|
if (meta.phase === 'empty') {
|
|
214
239
|
return restoreQueuedToPrompt({ restoreDraft: true, showHint: false, currentText: text });
|
|
215
240
|
}
|
|
216
|
-
// Idle + empty +
|
|
217
|
-
// opens message selector, but we don't have that feature yet).
|
|
241
|
+
// Idle + empty + no transcript to jump back into: nothing to do.
|
|
218
242
|
return false;
|
|
219
|
-
}, [contextPanel, usagePanel, closeUsagePanel, restoreQueuedToPrompt, clearPromptHint, clearPastedImagesSnapshot, store]);
|
|
243
|
+
}, [contextPanel, usagePanel, closeUsagePanel, restoreQueuedToPrompt, openMessageSelector, showPromptHint, clearPromptHint, clearPastedImagesSnapshot, clearPastedTextsSnapshot, store]);
|
|
220
244
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
if (result?.
|
|
227
|
-
if (result?.
|
|
228
|
-
const restoreText = String(result?.restoreText || '').trim();
|
|
229
|
-
if (!restoreText) return undefined;
|
|
230
|
-
const existingText = String(currentText || '').trim();
|
|
231
|
-
const nextText = [restoreText, existingText].filter(Boolean).join('\n');
|
|
245
|
+
const commitAsyncInterruptRestore = useCallback((pending) => {
|
|
246
|
+
if (!pending || pending.generation !== interruptGenerationRef.current) return true;
|
|
247
|
+
if (store.getState?.().busy) return false;
|
|
248
|
+
const restoreText = promptInterruptRestoreText(pending.result, promptValueRef.current);
|
|
249
|
+
if (!restoreText) return true;
|
|
250
|
+
if (pending.result?.pastedImages) installPastedImages(pending.result.pastedImages, { merge: true });
|
|
251
|
+
if (pending.result?.pastedTexts) installPastedTexts(pending.result.pastedTexts, { merge: true });
|
|
232
252
|
clearPromptHint();
|
|
233
|
-
|
|
234
|
-
|
|
253
|
+
syncPromptLayoutRows(restoreText);
|
|
254
|
+
setPromptDraftOverride({ id: Date.now(), value: restoreText });
|
|
255
|
+
return true;
|
|
256
|
+
}, [store, promptValueRef, installPastedImages, installPastedTexts, clearPromptHint, syncPromptLayoutRows, setPromptDraftOverride]);
|
|
257
|
+
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
const pending = pendingInterruptRestoreRef.current;
|
|
260
|
+
if (!pending) return;
|
|
261
|
+
if (commitAsyncInterruptRestore(pending)) pendingInterruptRestoreRef.current = null;
|
|
262
|
+
}, [state.busy, commitAsyncInterruptRestore]);
|
|
263
|
+
|
|
264
|
+
const handlePromptInterrupt = useCallback((currentText = '') => {
|
|
265
|
+
const generation = ++interruptGenerationRef.current;
|
|
266
|
+
pendingInterruptRestoreRef.current = null;
|
|
267
|
+
const applyResult = (result, draftText, asyncResult = false) => {
|
|
268
|
+
if (generation !== interruptGenerationRef.current || result?.aborted === false) return undefined;
|
|
269
|
+
if (result?.discardPastedImages) clearPastedImagesSnapshot(result.discardPastedImages);
|
|
270
|
+
if (result?.discardPastedTexts) clearPastedTextsSnapshot(result.discardPastedTexts);
|
|
271
|
+
if (asyncResult) {
|
|
272
|
+
const pending = { generation, result };
|
|
273
|
+
if (!commitAsyncInterruptRestore(pending)) pendingInterruptRestoreRef.current = pending;
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const restoreText = promptInterruptRestoreText(result, draftText);
|
|
277
|
+
if (!restoreText) return undefined;
|
|
278
|
+
if (result?.pastedImages) installPastedImages(result.pastedImages, { merge: true });
|
|
279
|
+
if (result?.pastedTexts) installPastedTexts(result.pastedTexts, { merge: true });
|
|
280
|
+
clearPromptHint();
|
|
281
|
+
return restoreText;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
let result;
|
|
285
|
+
try {
|
|
286
|
+
const options = { restorePrompt: String(currentText ?? '') === '' };
|
|
287
|
+
result = typeof store.abortAsync === 'function' ? store.abortAsync(options) : store.abort?.(options);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
store.pushNotice?.(`interrupt failed: ${error?.message || error}`, 'error');
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (result && typeof result.then === 'function') {
|
|
293
|
+
void Promise.resolve(result)
|
|
294
|
+
.then((resolved) => applyResult(resolved, promptValueRef.current, true))
|
|
295
|
+
.catch((error) => store.pushNotice?.(`interrupt failed: ${error?.message || error}`, 'error'));
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
return applyResult(result, currentText, false);
|
|
299
|
+
}, [store, commitAsyncInterruptRestore, clearPromptHint, installPastedImages, installPastedTexts, clearPastedImagesSnapshot, clearPastedTextsSnapshot]);
|
|
235
300
|
|
|
236
301
|
return {
|
|
237
302
|
handlePromptPaste,
|
|
@@ -12,17 +12,18 @@ export function usePromptHint() {
|
|
|
12
12
|
const promptHintTimerRef = useRef(null);
|
|
13
13
|
const promptHintActiveRef = useRef(false);
|
|
14
14
|
|
|
15
|
-
const showPromptHint = useCallback((text, tone = 'info') => {
|
|
15
|
+
const showPromptHint = useCallback((text, tone = 'info', dismissMs = PROMPT_HINT_DISMISS_MS) => {
|
|
16
16
|
if (promptHintTimerRef.current) clearTimeout(promptHintTimerRef.current);
|
|
17
17
|
promptHintActiveRef.current = true;
|
|
18
18
|
setPromptHint(String(text || ''));
|
|
19
19
|
setPromptHintTone(tone);
|
|
20
|
+
const timeoutMs = Number(dismissMs) > 0 ? Number(dismissMs) : PROMPT_HINT_DISMISS_MS;
|
|
20
21
|
promptHintTimerRef.current = setTimeout(() => {
|
|
21
22
|
promptHintTimerRef.current = null;
|
|
22
23
|
promptHintActiveRef.current = false;
|
|
23
24
|
setPromptHint('');
|
|
24
25
|
setPromptHintTone('info');
|
|
25
|
-
},
|
|
26
|
+
}, timeoutMs);
|
|
26
27
|
}, []);
|
|
27
28
|
|
|
28
29
|
// Same band and timer; only the resting tone differs for copy feedback.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { useCallback, useMemo } from 'react';
|
|
6
6
|
import { PROMPT_HISTORY_LIMIT } from './transcript-window.mjs';
|
|
7
7
|
import { promptHistoryKey } from './app-format.mjs';
|
|
8
|
+
import { mergeQueuedRestoreText } from '../components/prompt-input/restore-policy.mjs';
|
|
8
9
|
|
|
9
10
|
export function usePromptQueueHistory({
|
|
10
11
|
store,
|
|
@@ -60,8 +61,12 @@ export function usePromptQueueHistory({
|
|
|
60
61
|
if (restoreDraft) {
|
|
61
62
|
if (restored.pastedImages) installPastedImages(restored.pastedImages, { merge: true });
|
|
62
63
|
if (restored.pastedTexts) installPastedTexts(restored.pastedTexts, { merge: true });
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
const restoredText = mergeQueuedRestoreText(
|
|
65
|
+
restored.text,
|
|
66
|
+
promptValueRef.current ?? currentText,
|
|
67
|
+
);
|
|
68
|
+
syncPromptLayoutRows(restoredText);
|
|
69
|
+
setPromptDraftOverride({ id: Date.now(), value: restoredText });
|
|
65
70
|
}
|
|
66
71
|
if (showHint) {
|
|
67
72
|
showPromptHint(`restored ${restored.count} queued message${restored.count === 1 ? '' : 's'}`, 'info');
|
|
@@ -70,7 +75,9 @@ export function usePromptQueueHistory({
|
|
|
70
75
|
}
|
|
71
76
|
return true;
|
|
72
77
|
};
|
|
73
|
-
|
|
78
|
+
// Ask the session service for the queued portion only. The live draft remains
|
|
79
|
+
// renderer-owned and is merged in apply(), after a daemon promise settles.
|
|
80
|
+
const restored = store.restoreQueued?.('');
|
|
74
81
|
// A daemon-backed store answers this as an ASYNC remote call, so the
|
|
75
82
|
// payload (and with it the queued text) only exists a tick later. Reading
|
|
76
83
|
// `.count`/`.text` off the promise dropped the popped entries on the floor —
|
|
@@ -13,13 +13,14 @@ import { theme } from '../theme.mjs';
|
|
|
13
13
|
import {
|
|
14
14
|
SELECTION_PAINT_INTERVAL_MS,
|
|
15
15
|
SCROLL_COALESCE_MS,
|
|
16
|
+
accumulateDirectionalScrollDelta,
|
|
16
17
|
selectionRectsEqual,
|
|
17
18
|
shiftSelectionRectY,
|
|
18
19
|
comparePoints,
|
|
19
20
|
upperBound,
|
|
20
21
|
transcriptRowAt,
|
|
21
22
|
} from './transcript-window.mjs';
|
|
22
|
-
import { yieldToRenderer } from '../
|
|
23
|
+
import { yieldToRenderer } from '../session/render-timing.mjs';
|
|
23
24
|
|
|
24
25
|
export function useTranscriptScroll({
|
|
25
26
|
store,
|
|
@@ -45,7 +46,7 @@ export function useTranscriptScroll({
|
|
|
45
46
|
const selectionPaintRef = useRef({ t: 0, rect: null, pending: null, timer: null });
|
|
46
47
|
// Coalescer for edge-drag auto-scroll + wheel scroll deltas (see
|
|
47
48
|
// SCROLL_COALESCE_MS). Both call sites accumulate into pendingRows.
|
|
48
|
-
const scrollCoalesceRef = useRef({ pendingRows: 0, timer: null });
|
|
49
|
+
const scrollCoalesceRef = useRef({ pendingRows: 0, direction: 0, timer: null });
|
|
49
50
|
const selectionTextCaptureRef = useRef(0);
|
|
50
51
|
// Stitch buffer: accumulates harvested transcript selection rows across scroll
|
|
51
52
|
// positions so Ctrl+C copies the FULL drag even after it auto-scrolled past the
|
|
@@ -446,6 +447,7 @@ export function useTranscriptScroll({
|
|
|
446
447
|
if (coalesceState.timer) clearTimeout(coalesceState.timer);
|
|
447
448
|
coalesceState.timer = null;
|
|
448
449
|
coalesceState.pendingRows = 0;
|
|
450
|
+
coalesceState.direction = 0;
|
|
449
451
|
}, []);
|
|
450
452
|
|
|
451
453
|
const scrollTranscriptRows = useCallback((deltaRows, options = {}) => {
|
|
@@ -653,13 +655,18 @@ export function useTranscriptScroll({
|
|
|
653
655
|
// calling scrollTranscriptRows directly.
|
|
654
656
|
const queueScrollCoalesced = useCallback((deltaRows) => {
|
|
655
657
|
const state = scrollCoalesceRef.current;
|
|
656
|
-
state
|
|
658
|
+
const reversed = accumulateDirectionalScrollDelta(state, deltaRows);
|
|
659
|
+
if (reversed && state.timer) {
|
|
660
|
+
clearTimeout(state.timer);
|
|
661
|
+
state.timer = null;
|
|
662
|
+
}
|
|
657
663
|
if (state.timer) return;
|
|
658
664
|
const rows = state.pendingRows;
|
|
659
665
|
state.pendingRows = 0;
|
|
660
666
|
scrollTranscriptRows(rows);
|
|
661
667
|
state.timer = setTimeout(() => {
|
|
662
668
|
state.timer = null;
|
|
669
|
+
state.direction = 0;
|
|
663
670
|
if (state.pendingRows !== 0) {
|
|
664
671
|
const remaining = state.pendingRows;
|
|
665
672
|
state.pendingRows = 0;
|
|
@@ -207,8 +207,6 @@ export function useTranscriptWindow({
|
|
|
207
207
|
const streamingTailItem = streamingTail?.kind === 'assistant' && streamingTail.streaming
|
|
208
208
|
? streamingTail
|
|
209
209
|
: null;
|
|
210
|
-
const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
|
|
211
|
-
const transcriptPinnedForStreaming = followingRef.current || scrolledUpRowsForPin === 0;
|
|
212
210
|
const tailRows = streamingTailItem
|
|
213
211
|
? estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)
|
|
214
212
|
: 0;
|
|
@@ -226,17 +224,12 @@ export function useTranscriptWindow({
|
|
|
226
224
|
revision,
|
|
227
225
|
streamingTailItem,
|
|
228
226
|
);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
const suppressMeasuredRowHeights = bottomPinnedForMeasure
|
|
236
|
-
|| (transcriptStreamingActive && (
|
|
237
|
-
(scrolledUpForStreamingMeasure && hasStreamingReadingAnchor)
|
|
238
|
-
|| (scrolledUpForStreamingMeasure && prevEstimateGeometry && !transcriptPinnedForStreaming)
|
|
239
|
-
));
|
|
227
|
+
// The live assistant already has one deterministic geometry authority in
|
|
228
|
+
// estimateTranscriptItemRowsCached. Settled rows always keep their measured
|
|
229
|
+
// geometry, whether following, reading, or resizing around a popup/picker.
|
|
230
|
+
// Switching the entire prefix back to estimates on the first upward wheel
|
|
231
|
+
// changed the row table during that gesture and caused rollback/jitter.
|
|
232
|
+
const suppressMeasuredRowHeights = false;
|
|
240
233
|
// Incremental builder: on a streaming flush where only the trailing assistant
|
|
241
234
|
// item's text grew, it recomputes just the tail row and appends to a cached
|
|
242
235
|
// settled-prefix row-index (O(1) prefix) instead of re-walking all N items.
|
|
@@ -680,9 +673,14 @@ export function useTranscriptWindow({
|
|
|
680
673
|
// off-screen. Ref: render-node-to-output's
|
|
681
674
|
// `atBottom = sticky || (grew && scrollTop >= prevMaxScroll)`, which
|
|
682
675
|
// likewise flips a user-broken sticky flag back on when the position says
|
|
683
|
-
// bottom.
|
|
684
|
-
// `grew
|
|
685
|
-
|
|
676
|
+
// bottom. Gated on `rowDelta >= 0` in exact parity with the reference's
|
|
677
|
+
// `grew = scrollHeight >= prevScrollHeight`: only a SHRINK is excluded
|
|
678
|
+
// (it can put the offset at 0 as an artifact). Demanding real growth was
|
|
679
|
+
// stricter than the reference and left the equal-height commits (a text
|
|
680
|
+
// replaced in place, a spinner/mode swap) falling through to the anchor
|
|
681
|
+
// capture below — which then locked the viewport at the tail with follow
|
|
682
|
+
// off, and every later row piled up below the fold.
|
|
683
|
+
if (!followingRef.current && rowDelta >= 0 && currentTarget === 0) {
|
|
686
684
|
followingRef.current = true;
|
|
687
685
|
}
|
|
688
686
|
// Follow ownership is explicit. target=0 can also mean the first manual
|
|
@@ -774,7 +772,31 @@ export function useTranscriptWindow({
|
|
|
774
772
|
maxRows,
|
|
775
773
|
});
|
|
776
774
|
if (desired == null) {
|
|
777
|
-
// Anchor item gone (
|
|
775
|
+
// Anchor item gone (removal/compaction). On GROWTH this is a transient
|
|
776
|
+
// identity gap — re-capture next frame. On a SHRINK the anchored rows
|
|
777
|
+
// were deleted: the reading position no longer exists, and leaving the
|
|
778
|
+
// lock in place pinned the target above zero forever, so the positional
|
|
779
|
+
// follow restore above (which needs target === 0) could never fire and
|
|
780
|
+
// auto-scroll stayed released for the rest of the session
|
|
781
|
+
// (user: 컴팩션된 이후로 자동 스크롤이 풀린다).
|
|
782
|
+
// A compaction commit can also NET-GROW the frame it lands in (it pushes
|
|
783
|
+
// its status row and keeps the streaming tail growing while it trims the
|
|
784
|
+
// head), so the row delta alone missed exactly the case it was written
|
|
785
|
+
// for. A bottom-relative target that no longer fits the transcript
|
|
786
|
+
// (currentTarget > maxRows) is the same deletion signal without the sign
|
|
787
|
+
// assumption: a transient identity gap during growth still fits, and the
|
|
788
|
+
// clamp effect below would otherwise silently park the target with follow
|
|
789
|
+
// left off.
|
|
790
|
+
if (rowDelta < 0 || currentTarget > maxRows) {
|
|
791
|
+
stopSmoothScroll();
|
|
792
|
+
transcriptAnchorRef.current = null;
|
|
793
|
+
transcriptAnchorDirtyRef.current = false;
|
|
794
|
+
scrollTargetRef.current = 0;
|
|
795
|
+
scrollPositionRef.current = 0;
|
|
796
|
+
followingRef.current = true;
|
|
797
|
+
if (currentOffset !== 0) setScrollOffset(0);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
778
800
|
transcriptAnchorDirtyRef.current = true;
|
|
779
801
|
return;
|
|
780
802
|
}
|
|
@@ -837,6 +859,17 @@ export function useTranscriptWindow({
|
|
|
837
859
|
}, [themeEpoch, withSelectionClip, paintSelectionRect]);
|
|
838
860
|
useEffect(() => {
|
|
839
861
|
const maxRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
|
|
862
|
+
// Opencode parity (createAutoScroll: `if (!canScroll(el)) userScrolled =
|
|
863
|
+
// false`, applied both on scroll and on the content resize; our desktop
|
|
864
|
+
// hook mirrors it in handleScroll/handleAutoScroll): a transcript that no
|
|
865
|
+
// longer OVERFLOWS holds no reading position, so a compaction or clear
|
|
866
|
+
// that shrinks it inside the viewport re-arms follow instead of leaving
|
|
867
|
+
// auto-scroll released with nothing left to scroll.
|
|
868
|
+
if (maxRows === 0 && !followingRef.current) {
|
|
869
|
+
transcriptAnchorRef.current = null;
|
|
870
|
+
transcriptAnchorDirtyRef.current = false;
|
|
871
|
+
followingRef.current = true;
|
|
872
|
+
}
|
|
840
873
|
if (scrollTargetRef.current <= maxRows && scrollPositionRef.current <= maxRows && scrollOffset <= maxRows) return;
|
|
841
874
|
stopSmoothScroll();
|
|
842
875
|
const next = Math.max(0, Math.min(maxRows, scrollTargetRef.current));
|