@adea-ai/ui 0.140.0 → 0.142.0
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/dist/components/composites/appearance-editor/appearance-editor.js +2 -2
- package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -1
- package/dist/components/composites/catalog-browser/catalog-browser-group.js +1 -1
- package/dist/components/composites/chain-of-thought/chain-of-thought.d.ts +56 -0
- package/dist/components/composites/chain-of-thought/chain-of-thought.d.ts.map +1 -0
- package/dist/components/composites/chain-of-thought/chain-of-thought.js +117 -0
- package/dist/components/composites/chain-of-thought/chain-of-thought.js.map +1 -0
- package/dist/components/composites/chain-of-thought/index.d.ts +2 -0
- package/dist/components/composites/chain-of-thought/index.d.ts.map +1 -0
- package/dist/components/composites/chain-of-thought/index.js +2 -0
- package/dist/components/composites/chat/chat.d.ts +67 -0
- package/dist/components/composites/chat/chat.d.ts.map +1 -0
- package/dist/components/composites/chat/chat.js +179 -0
- package/dist/components/composites/chat/chat.js.map +1 -0
- package/dist/components/composites/chat/index.d.ts +2 -0
- package/dist/components/composites/chat/index.d.ts.map +1 -0
- package/dist/components/composites/chat/index.js +2 -0
- package/dist/components/composites/checkpoint/checkpoint-model.d.ts +34 -0
- package/dist/components/composites/checkpoint/checkpoint-model.d.ts.map +1 -0
- package/dist/components/composites/checkpoint/checkpoint-model.js +48 -0
- package/dist/components/composites/checkpoint/checkpoint-model.js.map +1 -0
- package/dist/components/composites/checkpoint/checkpoint.d.ts +48 -0
- package/dist/components/composites/checkpoint/checkpoint.d.ts.map +1 -0
- package/dist/components/composites/checkpoint/checkpoint.js +160 -0
- package/dist/components/composites/checkpoint/checkpoint.js.map +1 -0
- package/dist/components/composites/checkpoint/index.d.ts +3 -0
- package/dist/components/composites/checkpoint/index.d.ts.map +1 -0
- package/dist/components/composites/checkpoint/index.js +3 -0
- package/dist/components/composites/confirmation/confirmation-model.d.ts +45 -0
- package/dist/components/composites/confirmation/confirmation-model.d.ts.map +1 -0
- package/dist/components/composites/confirmation/confirmation-model.js +51 -0
- package/dist/components/composites/confirmation/confirmation-model.js.map +1 -0
- package/dist/components/composites/confirmation/confirmation.d.ts +72 -0
- package/dist/components/composites/confirmation/confirmation.d.ts.map +1 -0
- package/dist/components/composites/confirmation/confirmation.js +300 -0
- package/dist/components/composites/confirmation/confirmation.js.map +1 -0
- package/dist/components/composites/confirmation/index.d.ts +3 -0
- package/dist/components/composites/confirmation/index.d.ts.map +1 -0
- package/dist/components/composites/confirmation/index.js +3 -0
- package/dist/components/composites/context/context-model.d.ts +69 -0
- package/dist/components/composites/context/context-model.d.ts.map +1 -0
- package/dist/components/composites/context/context-model.js +122 -0
- package/dist/components/composites/context/context-model.js.map +1 -0
- package/dist/components/composites/context/context.d.ts +38 -0
- package/dist/components/composites/context/context.d.ts.map +1 -0
- package/dist/components/composites/context/context.js +234 -0
- package/dist/components/composites/context/context.js.map +1 -0
- package/dist/components/composites/context/index.d.ts +3 -0
- package/dist/components/composites/context/index.d.ts.map +1 -0
- package/dist/components/composites/context/index.js +3 -0
- package/dist/components/composites/file-tree/build-file-tree.d.ts +138 -0
- package/dist/components/composites/file-tree/build-file-tree.d.ts.map +1 -0
- package/dist/components/composites/file-tree/build-file-tree.js +325 -0
- package/dist/components/composites/file-tree/build-file-tree.js.map +1 -0
- package/dist/components/composites/file-tree/file-tree.d.ts +74 -0
- package/dist/components/composites/file-tree/file-tree.d.ts.map +1 -0
- package/dist/components/composites/file-tree/file-tree.js +389 -0
- package/dist/components/composites/file-tree/file-tree.js.map +1 -0
- package/dist/components/composites/file-tree/index.d.ts +3 -0
- package/dist/components/composites/file-tree/index.d.ts.map +1 -0
- package/dist/components/composites/file-tree/index.js +3 -0
- package/dist/components/composites/inline-citation/citation.d.ts +37 -0
- package/dist/components/composites/inline-citation/citation.d.ts.map +1 -0
- package/dist/components/composites/inline-citation/citation.js +52 -0
- package/dist/components/composites/inline-citation/citation.js.map +1 -0
- package/dist/components/composites/inline-citation/index.d.ts +3 -0
- package/dist/components/composites/inline-citation/index.d.ts.map +1 -0
- package/dist/components/composites/inline-citation/index.js +3 -0
- package/dist/components/composites/inline-citation/inline-citation.d.ts +64 -0
- package/dist/components/composites/inline-citation/inline-citation.d.ts.map +1 -0
- package/dist/components/composites/inline-citation/inline-citation.js +272 -0
- package/dist/components/composites/inline-citation/inline-citation.js.map +1 -0
- package/dist/components/composites/message-scroller/index.d.ts +3 -0
- package/dist/components/composites/message-scroller/index.d.ts.map +1 -0
- package/dist/components/composites/message-scroller/index.js +3 -0
- package/dist/components/composites/message-scroller/message-scroller.d.ts +55 -0
- package/dist/components/composites/message-scroller/message-scroller.d.ts.map +1 -0
- package/dist/components/composites/message-scroller/message-scroller.js +201 -0
- package/dist/components/composites/message-scroller/message-scroller.js.map +1 -0
- package/dist/components/composites/message-scroller/scroll-anchor.d.ts +66 -0
- package/dist/components/composites/message-scroller/scroll-anchor.d.ts.map +1 -0
- package/dist/components/composites/message-scroller/scroll-anchor.js +122 -0
- package/dist/components/composites/message-scroller/scroll-anchor.js.map +1 -0
- package/dist/components/composites/model-selector/index.d.ts +3 -0
- package/dist/components/composites/model-selector/index.d.ts.map +1 -0
- package/dist/components/composites/model-selector/index.js +3 -0
- package/dist/components/composites/model-selector/model-selector.d.ts +55 -0
- package/dist/components/composites/model-selector/model-selector.d.ts.map +1 -0
- package/dist/components/composites/model-selector/model-selector.js +490 -0
- package/dist/components/composites/model-selector/model-selector.js.map +1 -0
- package/dist/components/composites/model-selector/models.d.ts +51 -0
- package/dist/components/composites/model-selector/models.d.ts.map +1 -0
- package/dist/components/composites/model-selector/models.js +71 -0
- package/dist/components/composites/model-selector/models.js.map +1 -0
- package/dist/components/composites/plan/index.d.ts +2 -0
- package/dist/components/composites/plan/index.d.ts.map +1 -0
- package/dist/components/composites/plan/index.js +2 -0
- package/dist/components/composites/plan/plan.d.ts +61 -0
- package/dist/components/composites/plan/plan.d.ts.map +1 -0
- package/dist/components/composites/plan/plan.js +130 -0
- package/dist/components/composites/plan/plan.js.map +1 -0
- package/dist/components/composites/queue/index.d.ts +2 -0
- package/dist/components/composites/queue/index.d.ts.map +1 -0
- package/dist/components/composites/queue/index.js +2 -0
- package/dist/components/composites/queue/queue.d.ts +77 -0
- package/dist/components/composites/queue/queue.d.ts.map +1 -0
- package/dist/components/composites/queue/queue.js +271 -0
- package/dist/components/composites/queue/queue.js.map +1 -0
- package/dist/components/composites/reasoning/index.d.ts +2 -0
- package/dist/components/composites/reasoning/index.d.ts.map +1 -0
- package/dist/components/composites/reasoning/index.js +2 -0
- package/dist/components/composites/reasoning/reasoning.d.ts +62 -0
- package/dist/components/composites/reasoning/reasoning.d.ts.map +1 -0
- package/dist/components/composites/reasoning/reasoning.js +156 -0
- package/dist/components/composites/reasoning/reasoning.js.map +1 -0
- package/dist/components/composites/sources/index.d.ts +3 -0
- package/dist/components/composites/sources/index.d.ts.map +1 -0
- package/dist/components/composites/sources/index.js +3 -0
- package/dist/components/composites/sources/source.d.ts +46 -0
- package/dist/components/composites/sources/source.d.ts.map +1 -0
- package/dist/components/composites/sources/source.js +74 -0
- package/dist/components/composites/sources/source.js.map +1 -0
- package/dist/components/composites/sources/sources.d.ts +45 -0
- package/dist/components/composites/sources/sources.d.ts.map +1 -0
- package/dist/components/composites/sources/sources.js +175 -0
- package/dist/components/composites/sources/sources.js.map +1 -0
- package/dist/components/composites/stack-trace/index.d.ts +3 -0
- package/dist/components/composites/stack-trace/index.d.ts.map +1 -0
- package/dist/components/composites/stack-trace/index.js +3 -0
- package/dist/components/composites/stack-trace/parse-stack-trace.d.ts +118 -0
- package/dist/components/composites/stack-trace/parse-stack-trace.d.ts.map +1 -0
- package/dist/components/composites/stack-trace/parse-stack-trace.js +376 -0
- package/dist/components/composites/stack-trace/parse-stack-trace.js.map +1 -0
- package/dist/components/composites/stack-trace/stack-trace.d.ts +63 -0
- package/dist/components/composites/stack-trace/stack-trace.d.ts.map +1 -0
- package/dist/components/composites/stack-trace/stack-trace.js +491 -0
- package/dist/components/composites/stack-trace/stack-trace.js.map +1 -0
- package/dist/components/composites/subagent/index.d.ts +2 -0
- package/dist/components/composites/subagent/index.d.ts.map +1 -0
- package/dist/components/composites/subagent/index.js +2 -0
- package/dist/components/composites/subagent/subagent.d.ts +55 -0
- package/dist/components/composites/subagent/subagent.d.ts.map +1 -0
- package/dist/components/composites/subagent/subagent.js +138 -0
- package/dist/components/composites/subagent/subagent.js.map +1 -0
- package/dist/components/composites/suggestion/index.d.ts +2 -0
- package/dist/components/composites/suggestion/index.d.ts.map +1 -0
- package/dist/components/composites/suggestion/index.js +2 -0
- package/dist/components/composites/suggestion/suggestion.d.ts +37 -0
- package/dist/components/composites/suggestion/suggestion.d.ts.map +1 -0
- package/dist/components/composites/suggestion/suggestion.js +100 -0
- package/dist/components/composites/suggestion/suggestion.js.map +1 -0
- package/dist/components/composites/task/index.d.ts +2 -0
- package/dist/components/composites/task/index.d.ts.map +1 -0
- package/dist/components/composites/task/index.js +2 -0
- package/dist/components/composites/task/task.d.ts +96 -0
- package/dist/components/composites/task/task.d.ts.map +1 -0
- package/dist/components/composites/task/task.js +311 -0
- package/dist/components/composites/task/task.js.map +1 -0
- package/dist/components/composites/terminal/ansi.d.ts +101 -0
- package/dist/components/composites/terminal/ansi.d.ts.map +1 -0
- package/dist/components/composites/terminal/ansi.js +544 -0
- package/dist/components/composites/terminal/ansi.js.map +1 -0
- package/dist/components/composites/terminal/index.d.ts +4 -0
- package/dist/components/composites/terminal/index.d.ts.map +1 -0
- package/dist/components/composites/terminal/index.js +4 -0
- package/dist/components/composites/terminal/terminal-model.d.ts +63 -0
- package/dist/components/composites/terminal/terminal-model.d.ts.map +1 -0
- package/dist/components/composites/terminal/terminal-model.js +69 -0
- package/dist/components/composites/terminal/terminal-model.js.map +1 -0
- package/dist/components/composites/terminal/terminal.d.ts +88 -0
- package/dist/components/composites/terminal/terminal.d.ts.map +1 -0
- package/dist/components/composites/terminal/terminal.js +500 -0
- package/dist/components/composites/terminal/terminal.js.map +1 -0
- package/dist/components/composites/test-results/index.d.ts +3 -0
- package/dist/components/composites/test-results/index.d.ts.map +1 -0
- package/dist/components/composites/test-results/index.js +3 -0
- package/dist/components/composites/test-results/test-results-model.d.ts +154 -0
- package/dist/components/composites/test-results/test-results-model.d.ts.map +1 -0
- package/dist/components/composites/test-results/test-results-model.js +252 -0
- package/dist/components/composites/test-results/test-results-model.js.map +1 -0
- package/dist/components/composites/test-results/test-results.d.ts +60 -0
- package/dist/components/composites/test-results/test-results.d.ts.map +1 -0
- package/dist/components/composites/test-results/test-results.js +631 -0
- package/dist/components/composites/test-results/test-results.js.map +1 -0
- package/dist/components/composites/tool/format-tool-value.d.ts +37 -0
- package/dist/components/composites/tool/format-tool-value.d.ts.map +1 -0
- package/dist/components/composites/tool/format-tool-value.js +135 -0
- package/dist/components/composites/tool/format-tool-value.js.map +1 -0
- package/dist/components/composites/tool/index.d.ts +3 -0
- package/dist/components/composites/tool/index.d.ts.map +1 -0
- package/dist/components/composites/tool/index.js +3 -0
- package/dist/components/composites/tool/tool.d.ts +69 -0
- package/dist/components/composites/tool/tool.d.ts.map +1 -0
- package/dist/components/composites/tool/tool.js +214 -0
- package/dist/components/composites/tool/tool.js.map +1 -0
- package/dist/components/composites/update-dialog/update-dialog.js +1 -1
- package/dist/components/conversation/conversation-surface.d.ts +7 -0
- package/dist/components/conversation/conversation-surface.d.ts.map +1 -1
- package/dist/components/conversation/conversation-surface.js +2 -0
- package/dist/components/conversation/conversation-surface.js.map +1 -1
- package/dist/components/conversation/scroll-follow-core.d.ts +24 -0
- package/dist/components/conversation/scroll-follow-core.d.ts.map +1 -1
- package/dist/components/conversation/scroll-follow-core.js +23 -1
- package/dist/components/conversation/scroll-follow-core.js.map +1 -1
- package/dist/components/conversation/scroll-follow.d.ts +3 -0
- package/dist/components/conversation/scroll-follow.d.ts.map +1 -1
- package/dist/components/conversation/scroll-follow.js +80 -1
- package/dist/components/conversation/scroll-follow.js.map +1 -1
- package/dist/components/ui/attachment/attachment.d.ts +58 -0
- package/dist/components/ui/attachment/attachment.d.ts.map +1 -0
- package/dist/components/ui/attachment/attachment.js +279 -0
- package/dist/components/ui/attachment/attachment.js.map +1 -0
- package/dist/components/ui/attachment/index.d.ts +2 -0
- package/dist/components/ui/attachment/index.d.ts.map +1 -0
- package/dist/components/ui/attachment/index.js +2 -0
- package/dist/components/ui/badge/badge.d.ts +1 -1
- package/dist/components/ui/bubble/bubble.d.ts +33 -0
- package/dist/components/ui/bubble/bubble.d.ts.map +1 -0
- package/dist/components/ui/bubble/bubble.js +95 -0
- package/dist/components/ui/bubble/bubble.js.map +1 -0
- package/dist/components/ui/bubble/index.d.ts +2 -0
- package/dist/components/ui/bubble/index.d.ts.map +1 -0
- package/dist/components/ui/bubble/index.js +2 -0
- package/dist/components/ui/button/button.d.ts +1 -1
- package/dist/components/ui/code-block/code-block.d.ts +56 -3
- package/dist/components/ui/code-block/code-block.d.ts.map +1 -1
- package/dist/components/ui/code-block/code-block.js +231 -50
- package/dist/components/ui/code-block/code-block.js.map +1 -1
- package/dist/components/ui/code-block/index.d.ts +1 -1
- package/dist/components/ui/code-block/index.d.ts.map +1 -1
- package/dist/components/ui/code-block/index.js +2 -2
- package/dist/components/ui/field/number-field.d.ts +21 -4
- package/dist/components/ui/field/number-field.d.ts.map +1 -1
- package/dist/components/ui/field/number-field.js +22 -11
- package/dist/components/ui/field/number-field.js.map +1 -1
- package/dist/components/ui/marker/index.d.ts +2 -0
- package/dist/components/ui/marker/index.d.ts.map +1 -0
- package/dist/components/ui/marker/index.js +2 -0
- package/dist/components/ui/marker/marker.d.ts +35 -0
- package/dist/components/ui/marker/marker.d.ts.map +1 -0
- package/dist/components/ui/marker/marker.js +123 -0
- package/dist/components/ui/marker/marker.js.map +1 -0
- package/dist/components/ui/message/cite-ranges.d.ts +42 -0
- package/dist/components/ui/message/cite-ranges.d.ts.map +1 -0
- package/dist/components/ui/message/cite-ranges.js +105 -0
- package/dist/components/ui/message/cite-ranges.js.map +1 -0
- package/dist/components/ui/message/index.d.ts +4 -0
- package/dist/components/ui/message/index.d.ts.map +1 -0
- package/dist/components/ui/message/index.js +4 -0
- package/dist/components/ui/message/message.d.ts +95 -0
- package/dist/components/ui/message/message.d.ts.map +1 -0
- package/dist/components/ui/message/message.js +443 -0
- package/dist/components/ui/message/message.js.map +1 -0
- package/dist/components/ui/message/parse-message-text.d.ts +93 -0
- package/dist/components/ui/message/parse-message-text.d.ts.map +1 -0
- package/dist/components/ui/message/parse-message-text.js +365 -0
- package/dist/components/ui/message/parse-message-text.js.map +1 -0
- package/dist/components/ui/questionnaire/index.d.ts +2 -0
- package/dist/components/ui/questionnaire/index.d.ts.map +1 -0
- package/dist/components/ui/questionnaire/index.js +2 -0
- package/dist/components/ui/questionnaire/questionnaire.d.ts +83 -0
- package/dist/components/ui/questionnaire/questionnaire.d.ts.map +1 -0
- package/dist/components/ui/questionnaire/questionnaire.js +470 -0
- package/dist/components/ui/questionnaire/questionnaire.js.map +1 -0
- package/dist/components/ui/status-chip/status-chip.d.ts +1 -1
- package/dist/components/ui/switch/switch.js +11 -11
- package/dist/components/ui/switch/switch.js.map +1 -1
- package/dist/components/ui/trace/index.d.ts +2 -0
- package/dist/components/ui/trace/index.d.ts.map +1 -0
- package/dist/components/ui/trace/index.js +2 -0
- package/dist/components/ui/trace/trace.d.ts +72 -0
- package/dist/components/ui/trace/trace.d.ts.map +1 -0
- package/dist/components/ui/trace/trace.js +340 -0
- package/dist/components/ui/trace/trace.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -5
- package/dist/r/attachment.json +25 -0
- package/dist/r/bubble.json +22 -0
- package/dist/r/chain-of-thought.json +26 -0
- package/dist/r/chat.json +24 -0
- package/dist/r/checkpoint.json +35 -0
- package/dist/r/confirmation.json +34 -0
- package/dist/r/context.json +33 -0
- package/dist/r/file-tree.json +32 -0
- package/dist/r/inline-citation.json +35 -0
- package/dist/r/marker.json +22 -0
- package/dist/r/message-scroller.json +31 -0
- package/dist/r/message.json +33 -0
- package/dist/r/model-selector.json +34 -0
- package/dist/r/plan.json +26 -0
- package/dist/r/questionnaire.json +28 -0
- package/dist/r/queue.json +26 -0
- package/dist/r/reasoning.json +26 -0
- package/dist/r/registry.json +811 -52
- package/dist/r/sources.json +32 -0
- package/dist/r/src/components/composites/chain-of-thought/chain-of-thought.tsx +141 -0
- package/dist/r/src/components/composites/chain-of-thought/index.ts +11 -0
- package/dist/r/src/components/composites/chat/chat.tsx +190 -0
- package/dist/r/src/components/composites/chat/index.ts +11 -0
- package/dist/r/src/components/composites/checkpoint/checkpoint-model.ts +71 -0
- package/dist/r/src/components/composites/checkpoint/checkpoint.tsx +214 -0
- package/dist/r/src/components/composites/checkpoint/index.ts +8 -0
- package/dist/r/src/components/composites/confirmation/confirmation-model.ts +83 -0
- package/dist/r/src/components/composites/confirmation/confirmation.tsx +311 -0
- package/dist/r/src/components/composites/confirmation/index.ts +9 -0
- package/dist/r/src/components/composites/context/context-model.ts +168 -0
- package/dist/r/src/components/composites/context/context.tsx +187 -0
- package/dist/r/src/components/composites/context/index.ts +18 -0
- package/dist/r/src/components/composites/file-tree/build-file-tree.ts +456 -0
- package/dist/r/src/components/composites/file-tree/file-tree.tsx +405 -0
- package/dist/r/src/components/composites/file-tree/index.ts +25 -0
- package/dist/r/src/components/composites/inline-citation/citation.ts +78 -0
- package/dist/r/src/components/composites/inline-citation/index.ts +9 -0
- package/dist/r/src/components/composites/inline-citation/inline-citation.tsx +285 -0
- package/dist/r/src/components/composites/message-scroller/index.ts +2 -0
- package/dist/r/src/components/composites/message-scroller/message-scroller.tsx +301 -0
- package/dist/r/src/components/composites/message-scroller/scroll-anchor.ts +170 -0
- package/dist/r/src/components/composites/model-selector/index.ts +14 -0
- package/dist/r/src/components/composites/model-selector/model-selector.tsx +420 -0
- package/dist/r/src/components/composites/model-selector/models.ts +103 -0
- package/dist/r/src/components/composites/plan/index.ts +1 -0
- package/dist/r/src/components/composites/plan/plan.tsx +155 -0
- package/dist/r/src/components/composites/queue/index.ts +10 -0
- package/dist/r/src/components/composites/queue/queue.tsx +264 -0
- package/dist/r/src/components/composites/reasoning/index.ts +1 -0
- package/dist/r/src/components/composites/reasoning/reasoning.tsx +176 -0
- package/dist/r/src/components/composites/sources/index.ts +11 -0
- package/dist/r/src/components/composites/sources/source.ts +116 -0
- package/dist/r/src/components/composites/sources/sources.tsx +163 -0
- package/dist/r/src/components/composites/stack-trace/index.ts +19 -0
- package/dist/r/src/components/composites/stack-trace/parse-stack-trace.ts +507 -0
- package/dist/r/src/components/composites/stack-trace/stack-trace.tsx +395 -0
- package/dist/r/src/components/composites/subagent/index.ts +1 -0
- package/dist/r/src/components/composites/subagent/subagent.tsx +159 -0
- package/dist/r/src/components/composites/suggestion/index.ts +1 -0
- package/dist/r/src/components/composites/suggestion/suggestion.tsx +140 -0
- package/dist/r/src/components/composites/task/index.ts +14 -0
- package/dist/r/src/components/composites/task/task.tsx +317 -0
- package/dist/r/src/components/composites/terminal/ansi.ts +674 -0
- package/dist/r/src/components/composites/terminal/index.ts +20 -0
- package/dist/r/src/components/composites/terminal/terminal-model.ts +111 -0
- package/dist/r/src/components/composites/terminal/terminal.tsx +574 -0
- package/dist/r/src/components/composites/test-results/index.ts +20 -0
- package/dist/r/src/components/composites/test-results/test-results-model.ts +368 -0
- package/dist/r/src/components/composites/test-results/test-results.tsx +586 -0
- package/dist/r/src/components/composites/tool/format-tool-value.ts +221 -0
- package/dist/r/src/components/composites/tool/index.ts +2 -0
- package/dist/r/src/components/composites/tool/tool.tsx +228 -0
- package/dist/r/src/components/conversation/conversation-surface.tsx +9 -0
- package/dist/r/src/components/conversation/scroll-follow-core.ts +31 -0
- package/dist/r/src/components/conversation/scroll-follow.ts +92 -0
- package/dist/r/src/components/ui/attachment/attachment.tsx +282 -0
- package/dist/r/src/components/ui/attachment/index.ts +10 -0
- package/dist/r/src/components/ui/bubble/bubble.tsx +86 -0
- package/dist/r/src/components/ui/bubble/index.ts +1 -0
- package/dist/r/src/components/ui/code-block/code-block.tsx +256 -26
- package/dist/r/src/components/ui/code-block/index.ts +9 -1
- package/dist/r/src/components/ui/field/number-field.tsx +51 -14
- package/dist/r/src/components/ui/marker/index.ts +1 -0
- package/dist/r/src/components/ui/marker/marker.tsx +90 -0
- package/dist/r/src/components/ui/message/cite-ranges.ts +144 -0
- package/dist/r/src/components/ui/message/index.ts +23 -0
- package/dist/r/src/components/ui/message/message.tsx +434 -0
- package/dist/r/src/components/ui/message/parse-message-text.ts +450 -0
- package/dist/r/src/components/ui/questionnaire/index.ts +10 -0
- package/dist/r/src/components/ui/questionnaire/questionnaire.tsx +446 -0
- package/dist/r/src/components/ui/trace/index.ts +9 -0
- package/dist/r/src/components/ui/trace/trace.tsx +334 -0
- package/dist/r/src/styles/base.css +34 -0
- package/dist/r/stack-trace.json +32 -0
- package/dist/r/subagent.json +27 -0
- package/dist/r/suggestion.json +24 -0
- package/dist/r/task.json +28 -0
- package/dist/r/terminal.json +35 -0
- package/dist/r/test-results.json +33 -0
- package/dist/r/tool.json +31 -0
- package/dist/r/trace.json +25 -0
- package/package.json +6 -3
- package/registry.json +811 -52
- package/src/components/composites/chain-of-thought/chain-of-thought.tsx +141 -0
- package/src/components/composites/chain-of-thought/index.ts +11 -0
- package/src/components/composites/chat/chat.tsx +190 -0
- package/src/components/composites/chat/index.ts +11 -0
- package/src/components/composites/checkpoint/checkpoint-model.ts +71 -0
- package/src/components/composites/checkpoint/checkpoint.tsx +214 -0
- package/src/components/composites/checkpoint/index.ts +8 -0
- package/src/components/composites/confirmation/confirmation-model.ts +83 -0
- package/src/components/composites/confirmation/confirmation.tsx +311 -0
- package/src/components/composites/confirmation/index.ts +9 -0
- package/src/components/composites/context/context-model.ts +168 -0
- package/src/components/composites/context/context.tsx +187 -0
- package/src/components/composites/context/index.ts +18 -0
- package/src/components/composites/file-tree/build-file-tree.ts +456 -0
- package/src/components/composites/file-tree/file-tree.tsx +405 -0
- package/src/components/composites/file-tree/index.ts +25 -0
- package/src/components/composites/inline-citation/citation.ts +78 -0
- package/src/components/composites/inline-citation/index.ts +9 -0
- package/src/components/composites/inline-citation/inline-citation.tsx +285 -0
- package/src/components/composites/message-scroller/index.ts +2 -0
- package/src/components/composites/message-scroller/message-scroller.tsx +301 -0
- package/src/components/composites/message-scroller/scroll-anchor.ts +170 -0
- package/src/components/composites/model-selector/index.ts +14 -0
- package/src/components/composites/model-selector/model-selector.tsx +420 -0
- package/src/components/composites/model-selector/models.ts +103 -0
- package/src/components/composites/plan/index.ts +1 -0
- package/src/components/composites/plan/plan.tsx +155 -0
- package/src/components/composites/queue/index.ts +10 -0
- package/src/components/composites/queue/queue.tsx +264 -0
- package/src/components/composites/reasoning/index.ts +1 -0
- package/src/components/composites/reasoning/reasoning.tsx +176 -0
- package/src/components/composites/sources/index.ts +11 -0
- package/src/components/composites/sources/source.ts +116 -0
- package/src/components/composites/sources/sources.tsx +163 -0
- package/src/components/composites/stack-trace/index.ts +19 -0
- package/src/components/composites/stack-trace/parse-stack-trace.ts +507 -0
- package/src/components/composites/stack-trace/stack-trace.tsx +395 -0
- package/src/components/composites/subagent/index.ts +1 -0
- package/src/components/composites/subagent/subagent.tsx +159 -0
- package/src/components/composites/suggestion/index.ts +1 -0
- package/src/components/composites/suggestion/suggestion.tsx +140 -0
- package/src/components/composites/task/index.ts +14 -0
- package/src/components/composites/task/task.tsx +317 -0
- package/src/components/composites/terminal/ansi.ts +674 -0
- package/src/components/composites/terminal/index.ts +20 -0
- package/src/components/composites/terminal/terminal-model.ts +111 -0
- package/src/components/composites/terminal/terminal.tsx +574 -0
- package/src/components/composites/test-results/index.ts +20 -0
- package/src/components/composites/test-results/test-results-model.ts +368 -0
- package/src/components/composites/test-results/test-results.tsx +586 -0
- package/src/components/composites/tool/format-tool-value.ts +221 -0
- package/src/components/composites/tool/index.ts +2 -0
- package/src/components/composites/tool/tool.tsx +228 -0
- package/src/components/conversation/conversation-surface.tsx +9 -0
- package/src/components/conversation/scroll-follow-core.ts +31 -0
- package/src/components/conversation/scroll-follow.ts +92 -0
- package/src/components/ui/attachment/attachment.tsx +282 -0
- package/src/components/ui/attachment/index.ts +10 -0
- package/src/components/ui/bubble/bubble.tsx +86 -0
- package/src/components/ui/bubble/index.ts +1 -0
- package/src/components/ui/code-block/code-block.tsx +256 -26
- package/src/components/ui/code-block/index.ts +9 -1
- package/src/components/ui/field/number-field.tsx +51 -14
- package/src/components/ui/marker/index.ts +1 -0
- package/src/components/ui/marker/marker.tsx +90 -0
- package/src/components/ui/message/cite-ranges.ts +144 -0
- package/src/components/ui/message/index.ts +23 -0
- package/src/components/ui/message/message.tsx +434 -0
- package/src/components/ui/message/parse-message-text.ts +450 -0
- package/src/components/ui/questionnaire/index.ts +10 -0
- package/src/components/ui/questionnaire/questionnaire.tsx +446 -0
- package/src/components/ui/trace/index.ts +9 -0
- package/src/components/ui/trace/trace.tsx +334 -0
- package/src/index.ts +26 -0
- package/src/styles/base.css +34 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from "../../../lib/utils.js";
|
|
2
|
+
import { Button } from "../../ui/button/button.js";
|
|
2
3
|
import { Badge } from "../../ui/badge/badge.js";
|
|
3
4
|
import { Heading } from "../../ui/typography/typography.js";
|
|
4
|
-
import { Button } from "../../ui/button/button.js";
|
|
5
5
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, createDialogFocusRestoration } from "../../ui/dialog/dialog.js";
|
|
6
6
|
import { ActionButton } from "../action-button/action-button.js";
|
|
7
7
|
import { Progress } from "../../ui/progress/progress.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentProps, JSX } from 'solid-js';
|
|
2
|
+
import type { ScrollFollowHold } from './scroll-follow';
|
|
2
3
|
/**
|
|
3
4
|
* ConversationSurface.
|
|
4
5
|
*
|
|
@@ -40,6 +41,12 @@ export type ConversationSurfaceProps = ComponentProps<'div'> & {
|
|
|
40
41
|
* frame around the surface is `ConversationPane`'s job.
|
|
41
42
|
*/
|
|
42
43
|
gutter?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Receives the surface's `hold`, once, when it is created. A host that moves content
|
|
46
|
+
* under a reader who is following (a history load that lands after the conversation
|
|
47
|
+
* changed) opens a hold around it, so the move is not read as the reader scrolling away.
|
|
48
|
+
*/
|
|
49
|
+
onFollowHold?: (hold: ScrollFollowHold) => void;
|
|
43
50
|
};
|
|
44
51
|
export declare function ConversationSurface(props: ConversationSurfaceProps): JSX.Element;
|
|
45
52
|
//# sourceMappingURL=conversation-surface.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-surface.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/conversation-surface.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"conversation-surface.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/conversation-surface.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAKnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAEvF,MAAM,MAAM,wBAAwB,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG;IAC7D,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sFAAsF;IACtF,sBAAsB,CAAC,EAAE,2BAA2B,CAAA;IACpD,kFAAkF;IAClF,uBAAuB,CAAC,EAAE,CAAC,QAAQ,EAAE,2BAA2B,KAAK,IAAI,CAAA;IACzE,iEAAiE;IACjE,KAAK,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACnB,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,eA8ElE"}
|
|
@@ -19,6 +19,7 @@ function ConversationSurface(props) {
|
|
|
19
19
|
"empty",
|
|
20
20
|
"header",
|
|
21
21
|
"gutter",
|
|
22
|
+
"onFollowHold",
|
|
22
23
|
"children",
|
|
23
24
|
"onScroll",
|
|
24
25
|
"ref",
|
|
@@ -31,6 +32,7 @@ function ConversationSurface(props) {
|
|
|
31
32
|
initialPosition: () => local.initialReadingPosition,
|
|
32
33
|
onPositionChange: (position) => local.onReadingPositionChange?.(position)
|
|
33
34
|
});
|
|
35
|
+
local.onFollowHold?.(follow.hold);
|
|
34
36
|
const onScroll = (event) => {
|
|
35
37
|
follow.onScroll();
|
|
36
38
|
const handler = local.onScroll;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-surface.js","names":["ArrowDown","ComponentProps","JSX","Show","splitProps","cn","Button","createScrollFollow","ConversationReadingPosition","Readonly","top","following","ConversationSurfaceProps","threshold","follow","resetKey","initialReadingPosition","onReadingPositionChange","position","empty","Element","header","gutter","ConversationSurface","props","local","rest","enabled","initialPosition","onPositionChange","onScroll","EventHandlerUnion","HTMLDivElement","Event","event","handler","_el$","_tmpl$2","_el$2","firstChild","_el$3","addEventListener","_$use","element","bindScroller","ref","_$spread","_$mergeProps","tabindex","class","_ref$","bindContent","_$insert","_$createComponent","when","children","fallback","_el$5","_tmpl$3","atBottom","_el$4","_tmpl$","type","size","variant","onClick","jump","_$effect","_$className","_$mergeProps","_$use","_$createComponent","_$className"],"sources":["../../../src/components/conversation/conversation-surface.tsx"],"sourcesContent":["import { ArrowDown } from 'lucide-solid'\nimport type { ComponentProps, JSX } from 'solid-js'\nimport { Show, splitProps } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport { Button } from '../ui/button/button'\nimport { createScrollFollow } from './scroll-follow'\n\n/**\n * ConversationSurface.\n *\n * The scrolling transcript. Its job is small and it is the difference between a\n * transcript that stays readable as it grows and one that fights the reader:\n *\n * - **Stick to the bottom.** New messages scroll into view only when the reader is\n * already at the bottom. Someone reading back through history must not be yanked\n * to the end by an arriving message, and that is the single most common way a\n * chat surface is made unusable.\n * - **Offer a way back.** Once the reader is not at the bottom, a control appears\n * that returns them. Pixel distance cannot prove an unread message count.\n * - **`overscroll-contain`.** The transcript scrolls to its end and stops, rather\n * than dragging the pane behind it — which in a desktop shell means the whole\n * window moves.\n */\nexport type ConversationReadingPosition = Readonly<{ top: number; following: boolean }>\n\nexport type ConversationSurfaceProps = ComponentProps<'div'> & {\n /** How far from the bottom still counts as \"at the bottom\", in px. */\n threshold?: number\n /** Disable automatic positioning while the host owns scroll restoration. */\n follow?: boolean\n /** Optional conversation identity: changing it re-arms follow at the bottom. */\n resetKey?: string\n /** Host snapshot consumed on mount/identity reset, never on ordinary prop updates. */\n initialReadingPosition?: ConversationReadingPosition\n /** Actual follow intent and native offset; storage/identity remain host-owned. */\n onReadingPositionChange?: (position: ConversationReadingPosition) => void\n /** Rendered above the transcript when there is nothing in it. */\n empty?: JSX.Element\n /** A header that scrolls with the transcript, e.g. a day divider or a banner. */\n header?: JSX.Element\n /**\n * The shared page gutter: reading padding inside the scroller plus a stable\n * gutter, so the scrollbar never shifts the text when it appears. The page\n * frame around the surface is `ConversationPane`'s job.\n */\n gutter?: boolean\n}\n\nexport function ConversationSurface(props: ConversationSurfaceProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'threshold',\n 'follow',\n 'resetKey',\n 'initialReadingPosition',\n 'onReadingPositionChange',\n 'empty',\n 'header',\n 'gutter',\n 'children',\n 'onScroll',\n 'ref',\n 'tabindex',\n ])\n // Kobalte provides the control's semantics; transcript follow is a native\n // geometry contract. KiroCrew's plain-scroller controller handles streaming,\n // content collapse and pane resizing without treating them as user consent.\n const follow = createScrollFollow({\n enabled: () => local.follow !== false,\n resetKey: () => local.resetKey,\n threshold: () => local.threshold ?? 80,\n initialPosition: () => local.initialReadingPosition,\n onPositionChange: (position) => local.onReadingPositionChange?.(position),\n })\n const onScroll: JSX.EventHandlerUnion<HTMLDivElement, Event> = (event) => {\n follow.onScroll()\n const handler = local.onScroll\n if (typeof handler === 'function') handler(event)\n else if (handler) handler[0](handler[1], event)\n }\n\n return (\n <div\n data-slot=\"conversation-surface\"\n class={cn('relative flex min-h-0 flex-1 flex-col', local.class)}\n >\n <div\n ref={(element) => {\n follow.bindScroller(element)\n if (typeof local.ref === 'function') local.ref(element)\n }}\n onScroll={onScroll}\n tabindex={local.tabindex ?? 0}\n class={cn(\n 'min-h-0 flex-1 overflow-y-auto overscroll-contain',\n local.gutter &&\n 'px-[clamp(0.8rem,3vw,2.5rem)] pt-[1.35rem] pb-[1.1rem] [scrollbar-gutter:stable] max-[30rem]:px-2'\n )}\n {...rest}\n >\n <div ref={follow.bindContent} data-slot=\"conversation-content\">\n <Show when={local.header}>{local.header}</Show>\n <Show when={local.children} fallback={<div class=\"p-4\">{local.empty}</div>}>\n {local.children}\n </Show>\n </div>\n </div>\n\n <Show when={!follow.atBottom()}>\n <div class=\"pointer-events-none absolute inset-x-0 bottom-3 flex justify-center\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n class=\"pointer-events-auto gap-1.5 shadow-md\"\n onClick={follow.jump}\n >\n <ArrowDown />\n Jump to latest\n </Button>\n </div>\n </Show>\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"conversation-surface.js","names":["ArrowDown","ComponentProps","JSX","Show","splitProps","cn","Button","createScrollFollow","ScrollFollowHold","ConversationReadingPosition","Readonly","top","following","ConversationSurfaceProps","threshold","follow","resetKey","initialReadingPosition","onReadingPositionChange","position","empty","Element","header","gutter","onFollowHold","hold","ConversationSurface","props","local","rest","enabled","initialPosition","onPositionChange","onScroll","EventHandlerUnion","HTMLDivElement","Event","event","handler","_el$","_tmpl$2","_el$2","firstChild","_el$3","addEventListener","_$use","element","bindScroller","ref","_$spread","_$mergeProps","tabindex","class","_ref$","bindContent","_$insert","_$createComponent","when","children","fallback","_el$5","_tmpl$3","atBottom","_el$4","_tmpl$","type","size","variant","onClick","jump","_$effect","_$className","_$mergeProps","_$use","_$createComponent","_$className"],"sources":["../../../src/components/conversation/conversation-surface.tsx"],"sourcesContent":["import { ArrowDown } from 'lucide-solid'\nimport type { ComponentProps, JSX } from 'solid-js'\nimport { Show, splitProps } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport { Button } from '../ui/button/button'\nimport { createScrollFollow } from './scroll-follow'\nimport type { ScrollFollowHold } from './scroll-follow'\n\n/**\n * ConversationSurface.\n *\n * The scrolling transcript. Its job is small and it is the difference between a\n * transcript that stays readable as it grows and one that fights the reader:\n *\n * - **Stick to the bottom.** New messages scroll into view only when the reader is\n * already at the bottom. Someone reading back through history must not be yanked\n * to the end by an arriving message, and that is the single most common way a\n * chat surface is made unusable.\n * - **Offer a way back.** Once the reader is not at the bottom, a control appears\n * that returns them. Pixel distance cannot prove an unread message count.\n * - **`overscroll-contain`.** The transcript scrolls to its end and stops, rather\n * than dragging the pane behind it — which in a desktop shell means the whole\n * window moves.\n */\nexport type ConversationReadingPosition = Readonly<{ top: number; following: boolean }>\n\nexport type ConversationSurfaceProps = ComponentProps<'div'> & {\n /** How far from the bottom still counts as \"at the bottom\", in px. */\n threshold?: number\n /** Disable automatic positioning while the host owns scroll restoration. */\n follow?: boolean\n /** Optional conversation identity: changing it re-arms follow at the bottom. */\n resetKey?: string\n /** Host snapshot consumed on mount/identity reset, never on ordinary prop updates. */\n initialReadingPosition?: ConversationReadingPosition\n /** Actual follow intent and native offset; storage/identity remain host-owned. */\n onReadingPositionChange?: (position: ConversationReadingPosition) => void\n /** Rendered above the transcript when there is nothing in it. */\n empty?: JSX.Element\n /** A header that scrolls with the transcript, e.g. a day divider or a banner. */\n header?: JSX.Element\n /**\n * The shared page gutter: reading padding inside the scroller plus a stable\n * gutter, so the scrollbar never shifts the text when it appears. The page\n * frame around the surface is `ConversationPane`'s job.\n */\n gutter?: boolean\n /**\n * Receives the surface's `hold`, once, when it is created. A host that moves content\n * under a reader who is following (a history load that lands after the conversation\n * changed) opens a hold around it, so the move is not read as the reader scrolling away.\n */\n onFollowHold?: (hold: ScrollFollowHold) => void\n}\n\nexport function ConversationSurface(props: ConversationSurfaceProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'threshold',\n 'follow',\n 'resetKey',\n 'initialReadingPosition',\n 'onReadingPositionChange',\n 'empty',\n 'header',\n 'gutter',\n 'onFollowHold',\n 'children',\n 'onScroll',\n 'ref',\n 'tabindex',\n ])\n // Kobalte provides the control's semantics; transcript follow is a native\n // geometry contract. KiroCrew's plain-scroller controller handles streaming,\n // content collapse and pane resizing without treating them as user consent.\n const follow = createScrollFollow({\n enabled: () => local.follow !== false,\n resetKey: () => local.resetKey,\n threshold: () => local.threshold ?? 80,\n initialPosition: () => local.initialReadingPosition,\n onPositionChange: (position) => local.onReadingPositionChange?.(position),\n })\n local.onFollowHold?.(follow.hold)\n const onScroll: JSX.EventHandlerUnion<HTMLDivElement, Event> = (event) => {\n follow.onScroll()\n const handler = local.onScroll\n if (typeof handler === 'function') handler(event)\n else if (handler) handler[0](handler[1], event)\n }\n\n return (\n <div\n data-slot=\"conversation-surface\"\n class={cn('relative flex min-h-0 flex-1 flex-col', local.class)}\n >\n <div\n ref={(element) => {\n follow.bindScroller(element)\n if (typeof local.ref === 'function') local.ref(element)\n }}\n onScroll={onScroll}\n tabindex={local.tabindex ?? 0}\n class={cn(\n 'min-h-0 flex-1 overflow-y-auto overscroll-contain',\n local.gutter &&\n 'px-[clamp(0.8rem,3vw,2.5rem)] pt-[1.35rem] pb-[1.1rem] [scrollbar-gutter:stable] max-[30rem]:px-2'\n )}\n {...rest}\n >\n <div ref={follow.bindContent} data-slot=\"conversation-content\">\n <Show when={local.header}>{local.header}</Show>\n <Show when={local.children} fallback={<div class=\"p-4\">{local.empty}</div>}>\n {local.children}\n </Show>\n </div>\n </div>\n\n <Show when={!follow.atBottom()}>\n <div class=\"pointer-events-none absolute inset-x-0 bottom-3 flex justify-center\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n class=\"pointer-events-auto gap-1.5 shadow-md\"\n onClick={follow.jump}\n >\n <ArrowDown />\n Jump to latest\n </Button>\n </div>\n </Show>\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;AAuDA,SAAgB0B,oBAAoBC,OAAiC;CACnE,MAAM,CAACC,OAAOC,QAAQzB,WAAWuB,OAAO;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAAU,CACX;CAID,MAAMZ,SAASR,mBAAmB;EAChCuB,eAAeF,MAAMb,WAAW;EAChCC,gBAAgBY,MAAMZ;EACtBF,iBAAiBc,MAAMd,aAAa;EACpCiB,uBAAuBH,MAAMX;EAC7Be,mBAAmBb,aAAaS,MAAMV,0BAA0BC,QAAQ;CAC1E,CAAC;CACDS,MAAMJ,eAAeT,OAAOU,IAAI;CAChC,MAAMQ,YAA0DI,UAAU;EACxEtB,OAAOkB,SAAS;EAChB,MAAMK,UAAUV,MAAMK;EACtB,IAAI,OAAOK,YAAY,YAAYA,QAAQD,KAAK;OAC3C,IAAIC,SAASA,QAAQ,EAAE,CAACA,QAAQ,IAAID,KAAK;CAChD;CAEA,cAAA;EAAA,IAAAE,OAAAC,QAAA,GAAAC,QAAAF,KAAAG,YAAAC,QAAAF,MAAAC;EAAAD,MAAAG,iBAAA,UAUgBX,QAAQ;EAAAY,KAJZC,YAAY;GAChB/B,OAAOgC,aAAaD,OAAO;GAC3B,IAAI,OAAOlB,MAAMoB,QAAQ,YAAYpB,MAAMoB,IAAIF,OAAO;EACxD,GAACL,KAAA;EAAAQ,OAAAR,OAAAS,WAAA;GAAA,IAEDC,WAAQ;IAAA,OAAEvB,MAAMuB,YAAY;GAAC;GAAA,KAAA,WAAA;IAAA,OACtB9C,GACL,qDACAuB,MAAML,UACJ,mGACJ;GAAC;EAAA,GACGM,IAAI,GAAA,OAAA,IAAA;EAAA,IAAAwB,QAEEtC,OAAOuC;EAAW,OAAAD,UAAA,aAAAR,IAAAQ,OAAAV,KAAA,IAAlB5B,OAAOuC,cAAWX;EAAAY,OAAAZ,OAAAa,gBACzBrD,MAAI;GAAA,IAACsD,OAAI;IAAA,OAAE7B,MAAMN;GAAM;GAAA,IAAAoC,WAAA;IAAA,OAAG9B,MAAMN;GAAM;EAAA,CAAA,GAAA,IAAA;EAAAiC,OAAAZ,OAAAa,gBACtCrD,MAAI;GAAA,IAACsD,OAAI;IAAA,OAAE7B,MAAM8B;GAAQ;GAAA,IAAEC,WAAQ;IAAA,cAAA;KAAA,IAAAC,QAAAC,QAAA;KAAAN,OAAAK,aAAoBhC,MAAMR,KAAK;KAAA,OAAAwC;IAAA,EAAA,CAAA;GAAA;GAAA,IAAAF,WAAA;IAAA,OAChE9B,MAAM8B;GAAQ;EAAA,CAAA,GAAA,IAAA;EAAAH,OAAAhB,MAAAiB,gBAKpBrD,MAAI;GAAA,IAACsD,OAAI;IAAA,OAAE,CAAC1C,OAAO+C,SAAS;GAAC;GAAA,IAAAJ,WAAA;IAAA,IAAAK,QAAAC,OAAA;IAAAT,OAAAQ,OAAAP,gBAEzBlD,QAAM;KACL2D,MAAI;KACJC,MAAI;KACJC,SAAO;KAAA,SAAA;KAAA,IAEPC,UAAO;MAAA,OAAErD,OAAOsD;KAAI;KAAA,IAAAX,WAAA;MAAA,OAAA,CAAAF,gBAEnBxD,WAAS,CAAA,CAAA,GAAA,gBAAA;KAAA;IAAA,CAAA,CAAA;IAAA,OAAA+D;GAAA;EAAA,CAAA,GAAA,IAAA;EAAAO,aAAAC,UAAAhC,MAjCTlC,GAAG,yCAAyCuB,MAAMwB,KAAK,CAAC,CAAA;EAAA,OAAAb;CAAA,EAAA,CAAA;AAwCrE"}
|
|
@@ -90,6 +90,30 @@ export declare function resolveUserScrollStick(args: {
|
|
|
90
90
|
* distinguishable, and this is the delta that tells them apart. */
|
|
91
91
|
viewportGrowth?: number;
|
|
92
92
|
}): boolean;
|
|
93
|
+
/** What the reader did to the scroller, as far as telling a scroll-up from anything else. */
|
|
94
|
+
export type ReaderInput = {
|
|
95
|
+
kind: 'wheel';
|
|
96
|
+
deltaY: number;
|
|
97
|
+
} | {
|
|
98
|
+
kind: 'key';
|
|
99
|
+
key: string;
|
|
100
|
+
shiftKey: boolean;
|
|
101
|
+
} | {
|
|
102
|
+
kind: 'touchmove';
|
|
103
|
+
} | {
|
|
104
|
+
kind: 'pointerdown';
|
|
105
|
+
onScroller: boolean;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Is this the reader moving the view up, right now?
|
|
109
|
+
*
|
|
110
|
+
* A held position (see `createScrollFollow().hold`) is the caller's until the reader takes it,
|
|
111
|
+
* and only an input that can scroll the view up takes it: a wheel notch up, an up key, a
|
|
112
|
+
* touch drag, or a press on the scroller itself (its scrollbar or gutter). A click or a
|
|
113
|
+
* selection inside the content, a letter, a wheel notch down or a down key do not move the
|
|
114
|
+
* view away from the end, so they leave it where it is.
|
|
115
|
+
*/
|
|
116
|
+
export declare function isScrollUpIntent(input: ReaderInput): boolean;
|
|
93
117
|
/** Result of an automatic (RO / append) pin evaluation. */
|
|
94
118
|
export interface AutoPinResult {
|
|
95
119
|
/** Whether to write `el.scrollTop = target` now. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-follow-core.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/scroll-follow-core.ts"],"names":[],"mappings":"AAqBA,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAED,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAErD;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAE3D;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAE5E;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,MAA4B,GACpC,OAAO,CAET;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,KAAK,EAAE,OAAO,CAAA;IACd,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,UAAU,CAAA;IAChB;;;;;;;;;;;wEAWoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CA4BV;AAED,2DAA2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,GAAG,EAAE,OAAO,CAAA;IACZ,0EAA0E;IAC1E,KAAK,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,KAAK,EAAE,OAAO,CAAA;IACd,IAAI,EAAE,UAAU,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;;;;;;4CAUwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;;;;;;;mDAY+C;IAC/C,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;;;;;;;;;;;;;;kFAiB8E;IAC9E,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,GAAG,aAAa,CAsDhB"}
|
|
1
|
+
{"version":3,"file":"scroll-follow-core.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/scroll-follow-core.ts"],"names":[],"mappings":"AAqBA,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAED,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAErD;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAE3D;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAE5E;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,MAA4B,GACpC,OAAO,CAET;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,KAAK,EAAE,OAAO,CAAA;IACd,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,UAAU,CAAA;IAChB;;;;;;;;;;;wEAWoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CA4BV;AAED,6FAA6F;AAC7F,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAA;AAIhD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAW5D;AAED,2DAA2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,GAAG,EAAE,OAAO,CAAA;IACZ,0EAA0E;IAC1E,KAAK,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,KAAK,EAAE,OAAO,CAAA;IACd,IAAI,EAAE,UAAU,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;;;;;;4CAUwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;;;;;;;mDAY+C;IAC/C,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;;;;;;;;;;;;;;kFAiB8E;IAC9E,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,GAAG,aAAa,CAsDhB"}
|
|
@@ -67,6 +67,28 @@ function resolveUserScrollStick(args) {
|
|
|
67
67
|
if (scrollTop > prevScrollTop + .5 && dist <= 16) return true;
|
|
68
68
|
return stick;
|
|
69
69
|
}
|
|
70
|
+
var SCROLL_UP_KEYS = /* @__PURE__ */ new Set([
|
|
71
|
+
"ArrowUp",
|
|
72
|
+
"PageUp",
|
|
73
|
+
"Home"
|
|
74
|
+
]);
|
|
75
|
+
/**
|
|
76
|
+
* Is this the reader moving the view up, right now?
|
|
77
|
+
*
|
|
78
|
+
* A held position (see `createScrollFollow().hold`) is the caller's until the reader takes it,
|
|
79
|
+
* and only an input that can scroll the view up takes it: a wheel notch up, an up key, a
|
|
80
|
+
* touch drag, or a press on the scroller itself (its scrollbar or gutter). A click or a
|
|
81
|
+
* selection inside the content, a letter, a wheel notch down or a down key do not move the
|
|
82
|
+
* view away from the end, so they leave it where it is.
|
|
83
|
+
*/
|
|
84
|
+
function isScrollUpIntent(input) {
|
|
85
|
+
switch (input.kind) {
|
|
86
|
+
case "wheel": return input.deltaY < 0;
|
|
87
|
+
case "key": return SCROLL_UP_KEYS.has(input.key) || input.key === " " && input.shiftKey;
|
|
88
|
+
case "touchmove": return true;
|
|
89
|
+
case "pointerdown": return input.onScroller;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
70
92
|
/**
|
|
71
93
|
* Decide an automatic pin at the moment content changed (RO callback / append
|
|
72
94
|
* layout effect / its follow-up rAF), reading LIVE geometry.
|
|
@@ -134,6 +156,6 @@ function evaluateAutoPin(args) {
|
|
|
134
156
|
};
|
|
135
157
|
}
|
|
136
158
|
//#endregion
|
|
137
|
-
export { atBottomEpsilon, bottomTarget, computeAtBottom, distanceFromBottom, evaluateAutoPin, isSelfScroll, resolveUserScrollStick };
|
|
159
|
+
export { atBottomEpsilon, bottomTarget, computeAtBottom, distanceFromBottom, evaluateAutoPin, isScrollUpIntent, isSelfScroll, resolveUserScrollStick };
|
|
138
160
|
|
|
139
161
|
//# sourceMappingURL=scroll-follow-core.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-follow-core.js","names":[],"sources":["../../../src/components/conversation/scroll-follow-core.ts"],"sourcesContent":["/*\n * Substantially translated from KiroCrew website/src/hooks/virtualizer/FollowController.ts\n * (plain-scroller decision units), pinned at\n * 283e136c0f902e965a535a7c9548c57c7504fed0.\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * This product includes software developed at Amazon.com, Inc.\n * (https://www.amazon.com/).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Pure plain-scroller decisions retained from FollowController; virtual row\n * measurement, persisted anchor resolution and restore settling are excluded.\n */\n/** Default distance (px) from the bottom within which `isAtBottom` is true. */\nexport const DEFAULT_BOTTOM_THRESHOLD = 100\n\n/**\n * Tolerance (px) for treating a scroll position as \"the same\" as a value we\n * wrote programmatically. Covers sub-pixel rounding and 1px momentum overshoot.\n * Must stay small so a deliberate user scroll of even a few px is still seen as\n * a user scroll.\n */\nexport const SELF_SCROLL_EPSILON = 2\n\n/**\n * \"At the bottom\" tolerance (px) for deciding whether an auto-pin still has\n * work to do. A flat 0.5 is UNDER one device pixel at fractional device-pixel\n * ratios (0.67 CSS px at 150% zoom, 0.8 at 125%): the scroller's resting\n * maximum scrollTop lands on a fractional value, so `|scrollTop - target|`\n * stays just above 0.5 even when the viewport is visually pinned to the\n * bottom — making the pin re-fire on every ResizeObserver tick. Scaling the\n * epsilon to the device pixel (never below 1 CSS px) absorbs that fractional\n * resting error. `devicePixelRatio` is read defensively so a jsdom / SSR\n * environment that leaves it undefined falls back to 1 (→ 1.5px).\n */\nexport function atBottomEpsilon(): number {\n const dpr =\n typeof window !== 'undefined' &&\n typeof window.devicePixelRatio === 'number' &&\n window.devicePixelRatio > 0\n ? window.devicePixelRatio\n : 1\n return Math.max(1, 1 / dpr + 0.5)\n}\n\n/** Live scroll geometry snapshot read from the scroller element. */\nexport interface ScrollGeom {\n scrollTop: number\n scrollHeight: number\n clientHeight: number\n}\n\n/** scrollTop that places the viewport exactly at the bottom (never negative). */\nexport function bottomTarget(geom: ScrollGeom): number {\n return Math.max(0, geom.scrollHeight - geom.clientHeight)\n}\n\n/** Pixels between the current scroll position and the bottom. */\nexport function distanceFromBottom(geom: ScrollGeom): number {\n return geom.scrollHeight - geom.scrollTop - geom.clientHeight\n}\n\n/** Whether the scroller is within `threshold` px of the bottom. */\nexport function computeAtBottom(geom: ScrollGeom, threshold: number): boolean {\n return distanceFromBottom(geom) <= threshold\n}\n\n/**\n * Recognise a `scroll` event caused by our own programmatic write rather than\n * by the user. `lastWriteTop < 0` means \"we have not written this session\", so\n * any scroll is treated as the user's.\n */\nexport function isSelfScroll(\n scrollTop: number,\n lastWriteTop: number,\n epsilon: number = SELF_SCROLL_EPSILON\n): boolean {\n return lastWriteTop >= 0 && Math.abs(scrollTop - lastWriteTop) <= epsilon\n}\n\n/**\n * Distance (px) from the true bottom within which a user scroll RE-ENGAGES\n * follow. Deliberately much tighter than DEFAULT_BOTTOM_THRESHOLD: that 100px\n * band drives the jump-to-bottom pill's visibility, and reusing it for follow\n * meant a deliberate 3-99px scroll-up kept `stick` armed — the next content\n * change then yanked the reader back to the bottom. Re-engaging only when the\n * user has returned essentially to the bottom keeps \"scrolled up to read\"\n * positions belonging to the user.\n */\nexport const FOLLOW_REENGAGE_PX = 16\n\n/**\n * Direction-aware `stick` decision for a *user-initiated* scroll (self-scrolls\n * filtered out by the caller via `isSelfScroll`):\n *\n * 1. At the true bottom (within the DPR-aware epsilon) → follow. This also\n * absorbs the layout engine's clamp: a mid-stream content SHRINK drops\n * scrollTop (which reads as an upward move) but lands exactly at the new\n * bottom — releasing there froze streaming follow for the rest of the\n * turn.\n * 2. Any other upward move → release, regardless of distance from the\n * bottom. The scroll position now belongs to the user; only returning to\n * the bottom (3) re-engages.\n * 3. A genuine DOWNWARD move that arrives within FOLLOW_REENGAGE_PX of the\n * bottom → re-engage. A neutral event inside the band does NOT: that is\n * how content collapsing under a still reader re-armed follow.\n * 4. Otherwise (downward/neutral, still away from the bottom) → keep the\n * previous state.\n *\n * `prevScrollTop < 0` means \"no prior observation this session\". Direction is\n * unknowable then, so the decision is position-only and CONSERVATIVE: follow\n * only within the re-engage band. Keeping a stale `stick` on an unattributable\n * away-from-bottom scroll is how a reader gets yanked.\n */\nexport function resolveUserScrollStick(args: {\n stick: boolean\n followOutput: boolean\n scrollTop: number\n prevScrollTop: number\n geom: ScrollGeom\n /** Change in the scroller's own height since the previous scroll event.\n *\n * Positive = the viewport GREW (the composer shrank under a deletion, the\n * keyboard closed). That growth lowers the maximum scrollTop, so the engine\n * clamps any reader parked closer to the bottom than the growth — with no\n * application write anywhere. The clamp then arrives here as an ordinary\n * scroll event sitting at distance ~0, which rule 1 below used to read as\n * \"the reader came back to the bottom\" and re-arm follow for someone who\n * never touched the scroller. The next turn to start then took them to the\n * end. Rule 1 exists to absorb a CONTENT-shrink clamp mid-stream, and content\n * shrink moves `scrollHeight`, not `clientHeight` — so the two are\n * distinguishable, and this is the delta that tells them apart. */\n viewportGrowth?: number\n}): boolean {\n const { stick, followOutput, scrollTop, prevScrollTop, geom } = args\n if (!followOutput) return false\n const dist = distanceFromBottom(geom)\n // A viewport growth large enough to explain the reader's arrival at the bottom\n // is the engine's clamp, not the reader. Leave `stick` exactly as it was.\n // A native clamp only ever LOWERS scrollTop, so a downward move concurrent with\n // the growth is the user's own and must still re-engage follow. Without the\n // direction term a reader who deliberately scrolls down while the keyboard\n // closes is refused their re-engagement.\n const clampedByViewport =\n (args.viewportGrowth ?? 0) > atBottomEpsilon() && scrollTop <= prevScrollTop + atBottomEpsilon()\n if (dist <= atBottomEpsilon()) return clampedByViewport ? stick : true\n if (prevScrollTop < 0) return dist <= FOLLOW_REENGAGE_PX\n if (scrollTop < prevScrollTop - 0.5) return false\n // Re-engagement requires a genuine DOWNWARD move, not merely a non-upward\n // event that finds the reader inside the band. A neutral event (identical\n // scrollTop -- the tail of an iOS momentum run, or any scroll fired while the\n // reader is at rest) used to satisfy this, so a reader sitting mid-transcript\n // could be re-armed by CONTENT rather than by their own hand: when rows\n // outside the window reprice smaller than their estimates, the remaining\n // content collapses under them and the bottom band arrives at the reader\n // instead of the reader arriving at it. Follow re-engaged, and the next pin\n // took them to the end -- reported as scrolling along and suddenly landing at\n // the bottom. Distance alone cannot tell those apart; the direction of the\n // reader's own move can.\n if (scrollTop > prevScrollTop + 0.5 && dist <= FOLLOW_REENGAGE_PX) return true\n return stick\n}\n\n/** Result of an automatic (RO / append) pin evaluation. */\nexport interface AutoPinResult {\n /** Whether to write `el.scrollTop = target` now. */\n pin: boolean\n /** Next value for `stick` (released to false if the user scrolled up). */\n stick: boolean\n /** The bottom scrollTop the caller should write when `pin` is true. */\n target: number\n}\n\n/**\n * Decide an automatic pin at the moment content changed (RO callback / append\n * layout effect / its follow-up rAF), reading LIVE geometry.\n *\n * - Not sticking → never pin.\n * - Sticking but the user has scrolled up since our last write\n * (`scrollTop < lastWriteTop - epsilon`) → release stick, don't pin.\n * This is the synchronous, race-proof guard.\n * - Otherwise → pin to the bottom (only actually move if not already there).\n *\n * `lastWriteTop < 0` disables the scroll-up guard (used right after a slot\n * switch, before we have written anything this session).\n *\n * `viewportShrink` (px, default 0) is how much the SCROLLER'S OWN BOX has\n * shrunk since that reference was recorded — chrome mounting below the\n * transcript (a queue band, an attachment strip, a tip card), often\n * spring-animated over several frames. Our own shrink inflates\n * `distanceFromBottom` with no user input, so without this allowance the\n * distance guard reads it as \"meaningfully away from the bottom\". Paired with\n * a content SHRINK in the same commit window — a tail-row remount clamping\n * scrollTop below `lastWriteTop` — that produced a full user-scroll-up\n * signature out of two of our own layout changes: follow released mid\n * animation and the content settled a card-height low. Judging the distance\n * against the box we were last a bottom FOR keeps the guard measuring the\n * user's move rather than our own. Only the shrink's own pixels are forgiven,\n * so a genuine drag inside the same tick still releases.\n */\nexport function evaluateAutoPin(args: {\n stick: boolean\n geom: ScrollGeom\n lastWriteTop: number\n epsilon?: number\n viewportShrink?: number\n /** Is a turn actually producing output right now?\n *\n * Follow means \"keep me at the end of a LIVE turn\". With nothing running there\n * is no output to follow, so a reader sitting above the bottom is not\n * following — and an automatic pin there is a yank with no cause, reported\n * from a phone as the transcript springing back after scrolling up about a\n * hundred pixels with nothing streaming.\n *\n * Defaults to `true` = assume a run is live, which keeps the behaviour of a\n * caller that has no run signal to give (the app-SDK chat surface). The chat\n * transcript passes the real thing. */\n runActive?: boolean\n /** Is an anchor restore currently OWNING the scroll position?\n *\n * A restore places the reader at an absolute offset and then re-lands it as\n * measurements arrive. An automatic pin during that window is a second owner\n * writing the same scroller, and the two fight: captured on a phone as\n * `WRITE autopin 3091->4245` answered by `WRITE settle 4245->3091`, twice in\n * 120ms, 1,154px each way. The settle won those rounds, but only because its\n * budget had not expired yet -- which is why the same switch sometimes landed\n * at the bottom and sometimes did not.\n *\n * Released rather than merely skipped, for the reason the idle branch below\n * gives: skipping leaves follow armed, so the next growth yanks the reader\n * from wherever the restore just put them. */\n restoreGate?: boolean\n /** Has hardware input -- wheel / touch / pointer / a scrolling key -- reached\n * the scroller since we last placed the reader at the bottom?\n *\n * False means the reader has done nothing, so a gap that opened while they\n * rest on our last write was opened by content -- a row settling from its\n * estimate, a code-block stand-in swapping for the highlighted block, the\n * top spacer repricing -- and is a gap WE owe them, not one they chose.\n *\n * The idle rule below cannot tell those apart from distance alone, and it\n * errs toward release, which was invisible wherever the browser's native\n * scroll anchoring quietly carried the reader through the growth. WebKit has\n * no scroll anchoring at all, so on an iPhone every entry into an idle\n * session paid the whole post-pin reprice as a displacement and then had\n * follow released on top of it: the transcript opened a viewport or more\n * above the end with nothing streaming to bring it back.\n *\n * Defaults to `true` = assume the reader may have moved, which is the\n * release-leaning legacy behaviour for a caller that has no input signal. */\n readerMovedSinceWrite?: boolean\n}): AutoPinResult {\n const { stick, geom, lastWriteTop } = args\n const epsilon = args.epsilon ?? SELF_SCROLL_EPSILON\n const viewportShrink = Math.max(0, args.viewportShrink ?? 0)\n const runActive = args.runActive ?? true\n const readerMovedSinceWrite = args.readerMovedSinceWrite ?? true\n const target = bottomTarget(geom)\n if (args.restoreGate) return { pin: false, stick: false, target }\n if (!stick) return { pin: false, stick: false, target }\n // The reader is resting exactly where we last put them and has given no input\n // since, so any gap is content settling under them -- carry them back, live\n // turn or not. Both conditions are load-bearing. Position alone would read our\n // own write as consent for a reader who wheeled up and happened to stop on it;\n // input alone would drag back a programmatic reveal -- a search hit, a pinned\n // prompt, find-in-page -- whose scroll event has not dispatched yet when a\n // height commit lands, since none of those touch the scroller's input\n // listeners. A reveal moves scrollTop off our write; a reprice does not.\n const restingOnOurWrite = lastWriteTop >= 0 && Math.abs(geom.scrollTop - lastWriteTop) <= epsilon\n if (!readerMovedSinceWrite && restingOnOurWrite) {\n return { pin: distanceFromBottom(geom) > atBottomEpsilon(), stick: true, target }\n }\n // Idle: release rather than merely skip the pin. Skipping would leave follow\n // armed, so the next turn to start would yank this reader to the bottom from\n // wherever they had settled — the same defect one event later.\n //\n // But distance alone cannot say WHO opened that gap, and the two causes want\n // opposite answers: a reader who scrolled up should be released, while a\n // reader the CONTENT moved away from should be carried back.\n if (!runActive && distanceFromBottom(geom) > atBottomEpsilon()) {\n // Released. The question this branch cannot answer from geometry -- did the\n // reader open this gap, or did the content -- is answered ABOVE by\n // `readerMovedSinceWrite`: reaching here means input or an unexplained\n // scroll has been seen since our last positioning, so a reader who is now\n // above the bottom while nothing runs is one who left it. Reading our own\n // last write as consent would be an automatic action authorizing itself;\n // the only evidence that the reader never moved is the absence of input,\n // and that is what the branch above requires.\n return { pin: false, stick: false, target }\n }\n // Release only on a genuine user scroll-UP: scrollTop dropped below our last\n // write AND we are now meaningfully away from the bottom. A pure content\n // SHRINK mid-stream (a partial markdown line re-parsing, a code fence opening\n // and reclassifying the block) clamps scrollTop below lastWriteTop too, but\n // leaves us still AT the new bottom (distance ~0). Without the distance guard\n // that shrink looked like a scroll-up and froze streaming follow — once\n // released, nothing re-armed stick for the rest of the response.\n if (\n lastWriteTop >= 0 &&\n geom.scrollTop < lastWriteTop - epsilon &&\n distanceFromBottom(geom) - viewportShrink > epsilon\n ) {\n return { pin: false, stick: false, target }\n }\n return { pin: Math.abs(geom.scrollTop - target) > atBottomEpsilon(), stick: true, target }\n}\n"],"mappings":";;;;;;;;;;;AA2CA,SAAgB,kBAA0B;CACxC,MAAM,MACJ,OAAO,WAAW,eAClB,OAAO,OAAO,qBAAqB,YACnC,OAAO,mBAAmB,IACtB,OAAO,mBACP;CACN,OAAO,KAAK,IAAI,GAAG,IAAI,MAAM,EAAG;AAClC;;AAUA,SAAgB,aAAa,MAA0B;CACrD,OAAO,KAAK,IAAI,GAAG,KAAK,eAAe,KAAK,YAAY;AAC1D;;AAGA,SAAgB,mBAAmB,MAA0B;CAC3D,OAAO,KAAK,eAAe,KAAK,YAAY,KAAK;AACnD;;AAGA,SAAgB,gBAAgB,MAAkB,WAA4B;CAC5E,OAAO,mBAAmB,IAAI,KAAK;AACrC;;;;;;AAOA,SAAgB,aACd,WACA,cACA,UAAA,GACS;CACT,OAAO,gBAAgB,KAAK,KAAK,IAAI,YAAY,YAAY,KAAK;AACpE;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,uBAAuB,MAmB3B;CACV,MAAM,EAAE,OAAO,cAAc,WAAW,eAAe,SAAS;CAChE,IAAI,CAAC,cAAc,OAAO;CAC1B,MAAM,OAAO,mBAAmB,IAAI;CAOpC,MAAM,qBACH,KAAK,kBAAkB,KAAK,gBAAgB,KAAK,aAAa,gBAAgB,gBAAgB;CACjG,IAAI,QAAQ,gBAAgB,GAAG,OAAO,oBAAoB,QAAQ;CAClE,IAAI,gBAAgB,GAAG,OAAO,QAAA;CAC9B,IAAI,YAAY,gBAAgB,IAAK,OAAO;CAY5C,IAAI,YAAY,gBAAgB,MAAO,QAAA,IAA4B,OAAO;CAC1E,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,gBAAgB,MAmDd;CAChB,MAAM,EAAE,OAAO,MAAM,iBAAiB;CACtC,MAAM,UAAU,KAAK,WAAA;CACrB,MAAM,iBAAiB,KAAK,IAAI,GAAG,KAAK,kBAAkB,CAAC;CAC3D,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,wBAAwB,KAAK,yBAAyB;CAC5D,MAAM,SAAS,aAAa,IAAI;CAChC,IAAI,KAAK,aAAa,OAAO;EAAE,KAAK;EAAO,OAAO;EAAO;CAAO;CAChE,IAAI,CAAC,OAAO,OAAO;EAAE,KAAK;EAAO,OAAO;EAAO;CAAO;CAStD,MAAM,oBAAoB,gBAAgB,KAAK,KAAK,IAAI,KAAK,YAAY,YAAY,KAAK;CAC1F,IAAI,CAAC,yBAAyB,mBAC5B,OAAO;EAAE,KAAK,mBAAmB,IAAI,IAAI,gBAAgB;EAAG,OAAO;EAAM;CAAO;CASlF,IAAI,CAAC,aAAa,mBAAmB,IAAI,IAAI,gBAAgB,GAS3D,OAAO;EAAE,KAAK;EAAO,OAAO;EAAO;CAAO;CAS5C,IACE,gBAAgB,KAChB,KAAK,YAAY,eAAe,WAChC,mBAAmB,IAAI,IAAI,iBAAiB,SAE5C,OAAO;EAAE,KAAK;EAAO,OAAO;EAAO;CAAO;CAE5C,OAAO;EAAE,KAAK,KAAK,IAAI,KAAK,YAAY,MAAM,IAAI,gBAAgB;EAAG,OAAO;EAAM;CAAO;AAC3F"}
|
|
1
|
+
{"version":3,"file":"scroll-follow-core.js","names":[],"sources":["../../../src/components/conversation/scroll-follow-core.ts"],"sourcesContent":["/*\n * Substantially translated from KiroCrew website/src/hooks/virtualizer/FollowController.ts\n * (plain-scroller decision units), pinned at\n * 283e136c0f902e965a535a7c9548c57c7504fed0.\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * This product includes software developed at Amazon.com, Inc.\n * (https://www.amazon.com/).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Pure plain-scroller decisions retained from FollowController; virtual row\n * measurement, persisted anchor resolution and restore settling are excluded.\n */\n/** Default distance (px) from the bottom within which `isAtBottom` is true. */\nexport const DEFAULT_BOTTOM_THRESHOLD = 100\n\n/**\n * Tolerance (px) for treating a scroll position as \"the same\" as a value we\n * wrote programmatically. Covers sub-pixel rounding and 1px momentum overshoot.\n * Must stay small so a deliberate user scroll of even a few px is still seen as\n * a user scroll.\n */\nexport const SELF_SCROLL_EPSILON = 2\n\n/**\n * \"At the bottom\" tolerance (px) for deciding whether an auto-pin still has\n * work to do. A flat 0.5 is UNDER one device pixel at fractional device-pixel\n * ratios (0.67 CSS px at 150% zoom, 0.8 at 125%): the scroller's resting\n * maximum scrollTop lands on a fractional value, so `|scrollTop - target|`\n * stays just above 0.5 even when the viewport is visually pinned to the\n * bottom — making the pin re-fire on every ResizeObserver tick. Scaling the\n * epsilon to the device pixel (never below 1 CSS px) absorbs that fractional\n * resting error. `devicePixelRatio` is read defensively so a jsdom / SSR\n * environment that leaves it undefined falls back to 1 (→ 1.5px).\n */\nexport function atBottomEpsilon(): number {\n const dpr =\n typeof window !== 'undefined' &&\n typeof window.devicePixelRatio === 'number' &&\n window.devicePixelRatio > 0\n ? window.devicePixelRatio\n : 1\n return Math.max(1, 1 / dpr + 0.5)\n}\n\n/** Live scroll geometry snapshot read from the scroller element. */\nexport interface ScrollGeom {\n scrollTop: number\n scrollHeight: number\n clientHeight: number\n}\n\n/** scrollTop that places the viewport exactly at the bottom (never negative). */\nexport function bottomTarget(geom: ScrollGeom): number {\n return Math.max(0, geom.scrollHeight - geom.clientHeight)\n}\n\n/** Pixels between the current scroll position and the bottom. */\nexport function distanceFromBottom(geom: ScrollGeom): number {\n return geom.scrollHeight - geom.scrollTop - geom.clientHeight\n}\n\n/** Whether the scroller is within `threshold` px of the bottom. */\nexport function computeAtBottom(geom: ScrollGeom, threshold: number): boolean {\n return distanceFromBottom(geom) <= threshold\n}\n\n/**\n * Recognise a `scroll` event caused by our own programmatic write rather than\n * by the user. `lastWriteTop < 0` means \"we have not written this session\", so\n * any scroll is treated as the user's.\n */\nexport function isSelfScroll(\n scrollTop: number,\n lastWriteTop: number,\n epsilon: number = SELF_SCROLL_EPSILON\n): boolean {\n return lastWriteTop >= 0 && Math.abs(scrollTop - lastWriteTop) <= epsilon\n}\n\n/**\n * Distance (px) from the true bottom within which a user scroll RE-ENGAGES\n * follow. Deliberately much tighter than DEFAULT_BOTTOM_THRESHOLD: that 100px\n * band drives the jump-to-bottom pill's visibility, and reusing it for follow\n * meant a deliberate 3-99px scroll-up kept `stick` armed — the next content\n * change then yanked the reader back to the bottom. Re-engaging only when the\n * user has returned essentially to the bottom keeps \"scrolled up to read\"\n * positions belonging to the user.\n */\nexport const FOLLOW_REENGAGE_PX = 16\n\n/**\n * Direction-aware `stick` decision for a *user-initiated* scroll (self-scrolls\n * filtered out by the caller via `isSelfScroll`):\n *\n * 1. At the true bottom (within the DPR-aware epsilon) → follow. This also\n * absorbs the layout engine's clamp: a mid-stream content SHRINK drops\n * scrollTop (which reads as an upward move) but lands exactly at the new\n * bottom — releasing there froze streaming follow for the rest of the\n * turn.\n * 2. Any other upward move → release, regardless of distance from the\n * bottom. The scroll position now belongs to the user; only returning to\n * the bottom (3) re-engages.\n * 3. A genuine DOWNWARD move that arrives within FOLLOW_REENGAGE_PX of the\n * bottom → re-engage. A neutral event inside the band does NOT: that is\n * how content collapsing under a still reader re-armed follow.\n * 4. Otherwise (downward/neutral, still away from the bottom) → keep the\n * previous state.\n *\n * `prevScrollTop < 0` means \"no prior observation this session\". Direction is\n * unknowable then, so the decision is position-only and CONSERVATIVE: follow\n * only within the re-engage band. Keeping a stale `stick` on an unattributable\n * away-from-bottom scroll is how a reader gets yanked.\n */\nexport function resolveUserScrollStick(args: {\n stick: boolean\n followOutput: boolean\n scrollTop: number\n prevScrollTop: number\n geom: ScrollGeom\n /** Change in the scroller's own height since the previous scroll event.\n *\n * Positive = the viewport GREW (the composer shrank under a deletion, the\n * keyboard closed). That growth lowers the maximum scrollTop, so the engine\n * clamps any reader parked closer to the bottom than the growth — with no\n * application write anywhere. The clamp then arrives here as an ordinary\n * scroll event sitting at distance ~0, which rule 1 below used to read as\n * \"the reader came back to the bottom\" and re-arm follow for someone who\n * never touched the scroller. The next turn to start then took them to the\n * end. Rule 1 exists to absorb a CONTENT-shrink clamp mid-stream, and content\n * shrink moves `scrollHeight`, not `clientHeight` — so the two are\n * distinguishable, and this is the delta that tells them apart. */\n viewportGrowth?: number\n}): boolean {\n const { stick, followOutput, scrollTop, prevScrollTop, geom } = args\n if (!followOutput) return false\n const dist = distanceFromBottom(geom)\n // A viewport growth large enough to explain the reader's arrival at the bottom\n // is the engine's clamp, not the reader. Leave `stick` exactly as it was.\n // A native clamp only ever LOWERS scrollTop, so a downward move concurrent with\n // the growth is the user's own and must still re-engage follow. Without the\n // direction term a reader who deliberately scrolls down while the keyboard\n // closes is refused their re-engagement.\n const clampedByViewport =\n (args.viewportGrowth ?? 0) > atBottomEpsilon() && scrollTop <= prevScrollTop + atBottomEpsilon()\n if (dist <= atBottomEpsilon()) return clampedByViewport ? stick : true\n if (prevScrollTop < 0) return dist <= FOLLOW_REENGAGE_PX\n if (scrollTop < prevScrollTop - 0.5) return false\n // Re-engagement requires a genuine DOWNWARD move, not merely a non-upward\n // event that finds the reader inside the band. A neutral event (identical\n // scrollTop -- the tail of an iOS momentum run, or any scroll fired while the\n // reader is at rest) used to satisfy this, so a reader sitting mid-transcript\n // could be re-armed by CONTENT rather than by their own hand: when rows\n // outside the window reprice smaller than their estimates, the remaining\n // content collapses under them and the bottom band arrives at the reader\n // instead of the reader arriving at it. Follow re-engaged, and the next pin\n // took them to the end -- reported as scrolling along and suddenly landing at\n // the bottom. Distance alone cannot tell those apart; the direction of the\n // reader's own move can.\n if (scrollTop > prevScrollTop + 0.5 && dist <= FOLLOW_REENGAGE_PX) return true\n return stick\n}\n\n/** What the reader did to the scroller, as far as telling a scroll-up from anything else. */\nexport type ReaderInput =\n | { kind: 'wheel'; deltaY: number }\n | { kind: 'key'; key: string; shiftKey: boolean }\n | { kind: 'touchmove' }\n | { kind: 'pointerdown'; onScroller: boolean }\n\nconst SCROLL_UP_KEYS = new Set(['ArrowUp', 'PageUp', 'Home'])\n\n/**\n * Is this the reader moving the view up, right now?\n *\n * A held position (see `createScrollFollow().hold`) is the caller's until the reader takes it,\n * and only an input that can scroll the view up takes it: a wheel notch up, an up key, a\n * touch drag, or a press on the scroller itself (its scrollbar or gutter). A click or a\n * selection inside the content, a letter, a wheel notch down or a down key do not move the\n * view away from the end, so they leave it where it is.\n */\nexport function isScrollUpIntent(input: ReaderInput): boolean {\n switch (input.kind) {\n case 'wheel':\n return input.deltaY < 0\n case 'key':\n return SCROLL_UP_KEYS.has(input.key) || (input.key === ' ' && input.shiftKey)\n case 'touchmove':\n return true\n case 'pointerdown':\n return input.onScroller\n }\n}\n\n/** Result of an automatic (RO / append) pin evaluation. */\nexport interface AutoPinResult {\n /** Whether to write `el.scrollTop = target` now. */\n pin: boolean\n /** Next value for `stick` (released to false if the user scrolled up). */\n stick: boolean\n /** The bottom scrollTop the caller should write when `pin` is true. */\n target: number\n}\n\n/**\n * Decide an automatic pin at the moment content changed (RO callback / append\n * layout effect / its follow-up rAF), reading LIVE geometry.\n *\n * - Not sticking → never pin.\n * - Sticking but the user has scrolled up since our last write\n * (`scrollTop < lastWriteTop - epsilon`) → release stick, don't pin.\n * This is the synchronous, race-proof guard.\n * - Otherwise → pin to the bottom (only actually move if not already there).\n *\n * `lastWriteTop < 0` disables the scroll-up guard (used right after a slot\n * switch, before we have written anything this session).\n *\n * `viewportShrink` (px, default 0) is how much the SCROLLER'S OWN BOX has\n * shrunk since that reference was recorded — chrome mounting below the\n * transcript (a queue band, an attachment strip, a tip card), often\n * spring-animated over several frames. Our own shrink inflates\n * `distanceFromBottom` with no user input, so without this allowance the\n * distance guard reads it as \"meaningfully away from the bottom\". Paired with\n * a content SHRINK in the same commit window — a tail-row remount clamping\n * scrollTop below `lastWriteTop` — that produced a full user-scroll-up\n * signature out of two of our own layout changes: follow released mid\n * animation and the content settled a card-height low. Judging the distance\n * against the box we were last a bottom FOR keeps the guard measuring the\n * user's move rather than our own. Only the shrink's own pixels are forgiven,\n * so a genuine drag inside the same tick still releases.\n */\nexport function evaluateAutoPin(args: {\n stick: boolean\n geom: ScrollGeom\n lastWriteTop: number\n epsilon?: number\n viewportShrink?: number\n /** Is a turn actually producing output right now?\n *\n * Follow means \"keep me at the end of a LIVE turn\". With nothing running there\n * is no output to follow, so a reader sitting above the bottom is not\n * following — and an automatic pin there is a yank with no cause, reported\n * from a phone as the transcript springing back after scrolling up about a\n * hundred pixels with nothing streaming.\n *\n * Defaults to `true` = assume a run is live, which keeps the behaviour of a\n * caller that has no run signal to give (the app-SDK chat surface). The chat\n * transcript passes the real thing. */\n runActive?: boolean\n /** Is an anchor restore currently OWNING the scroll position?\n *\n * A restore places the reader at an absolute offset and then re-lands it as\n * measurements arrive. An automatic pin during that window is a second owner\n * writing the same scroller, and the two fight: captured on a phone as\n * `WRITE autopin 3091->4245` answered by `WRITE settle 4245->3091`, twice in\n * 120ms, 1,154px each way. The settle won those rounds, but only because its\n * budget had not expired yet -- which is why the same switch sometimes landed\n * at the bottom and sometimes did not.\n *\n * Released rather than merely skipped, for the reason the idle branch below\n * gives: skipping leaves follow armed, so the next growth yanks the reader\n * from wherever the restore just put them. */\n restoreGate?: boolean\n /** Has hardware input -- wheel / touch / pointer / a scrolling key -- reached\n * the scroller since we last placed the reader at the bottom?\n *\n * False means the reader has done nothing, so a gap that opened while they\n * rest on our last write was opened by content -- a row settling from its\n * estimate, a code-block stand-in swapping for the highlighted block, the\n * top spacer repricing -- and is a gap WE owe them, not one they chose.\n *\n * The idle rule below cannot tell those apart from distance alone, and it\n * errs toward release, which was invisible wherever the browser's native\n * scroll anchoring quietly carried the reader through the growth. WebKit has\n * no scroll anchoring at all, so on an iPhone every entry into an idle\n * session paid the whole post-pin reprice as a displacement and then had\n * follow released on top of it: the transcript opened a viewport or more\n * above the end with nothing streaming to bring it back.\n *\n * Defaults to `true` = assume the reader may have moved, which is the\n * release-leaning legacy behaviour for a caller that has no input signal. */\n readerMovedSinceWrite?: boolean\n}): AutoPinResult {\n const { stick, geom, lastWriteTop } = args\n const epsilon = args.epsilon ?? SELF_SCROLL_EPSILON\n const viewportShrink = Math.max(0, args.viewportShrink ?? 0)\n const runActive = args.runActive ?? true\n const readerMovedSinceWrite = args.readerMovedSinceWrite ?? true\n const target = bottomTarget(geom)\n if (args.restoreGate) return { pin: false, stick: false, target }\n if (!stick) return { pin: false, stick: false, target }\n // The reader is resting exactly where we last put them and has given no input\n // since, so any gap is content settling under them -- carry them back, live\n // turn or not. Both conditions are load-bearing. Position alone would read our\n // own write as consent for a reader who wheeled up and happened to stop on it;\n // input alone would drag back a programmatic reveal -- a search hit, a pinned\n // prompt, find-in-page -- whose scroll event has not dispatched yet when a\n // height commit lands, since none of those touch the scroller's input\n // listeners. A reveal moves scrollTop off our write; a reprice does not.\n const restingOnOurWrite = lastWriteTop >= 0 && Math.abs(geom.scrollTop - lastWriteTop) <= epsilon\n if (!readerMovedSinceWrite && restingOnOurWrite) {\n return { pin: distanceFromBottom(geom) > atBottomEpsilon(), stick: true, target }\n }\n // Idle: release rather than merely skip the pin. Skipping would leave follow\n // armed, so the next turn to start would yank this reader to the bottom from\n // wherever they had settled — the same defect one event later.\n //\n // But distance alone cannot say WHO opened that gap, and the two causes want\n // opposite answers: a reader who scrolled up should be released, while a\n // reader the CONTENT moved away from should be carried back.\n if (!runActive && distanceFromBottom(geom) > atBottomEpsilon()) {\n // Released. The question this branch cannot answer from geometry -- did the\n // reader open this gap, or did the content -- is answered ABOVE by\n // `readerMovedSinceWrite`: reaching here means input or an unexplained\n // scroll has been seen since our last positioning, so a reader who is now\n // above the bottom while nothing runs is one who left it. Reading our own\n // last write as consent would be an automatic action authorizing itself;\n // the only evidence that the reader never moved is the absence of input,\n // and that is what the branch above requires.\n return { pin: false, stick: false, target }\n }\n // Release only on a genuine user scroll-UP: scrollTop dropped below our last\n // write AND we are now meaningfully away from the bottom. A pure content\n // SHRINK mid-stream (a partial markdown line re-parsing, a code fence opening\n // and reclassifying the block) clamps scrollTop below lastWriteTop too, but\n // leaves us still AT the new bottom (distance ~0). Without the distance guard\n // that shrink looked like a scroll-up and froze streaming follow — once\n // released, nothing re-armed stick for the rest of the response.\n if (\n lastWriteTop >= 0 &&\n geom.scrollTop < lastWriteTop - epsilon &&\n distanceFromBottom(geom) - viewportShrink > epsilon\n ) {\n return { pin: false, stick: false, target }\n }\n return { pin: Math.abs(geom.scrollTop - target) > atBottomEpsilon(), stick: true, target }\n}\n"],"mappings":";;;;;;;;;;;AA2CA,SAAgB,kBAA0B;CACxC,MAAM,MACJ,OAAO,WAAW,eAClB,OAAO,OAAO,qBAAqB,YACnC,OAAO,mBAAmB,IACtB,OAAO,mBACP;CACN,OAAO,KAAK,IAAI,GAAG,IAAI,MAAM,EAAG;AAClC;;AAUA,SAAgB,aAAa,MAA0B;CACrD,OAAO,KAAK,IAAI,GAAG,KAAK,eAAe,KAAK,YAAY;AAC1D;;AAGA,SAAgB,mBAAmB,MAA0B;CAC3D,OAAO,KAAK,eAAe,KAAK,YAAY,KAAK;AACnD;;AAGA,SAAgB,gBAAgB,MAAkB,WAA4B;CAC5E,OAAO,mBAAmB,IAAI,KAAK;AACrC;;;;;;AAOA,SAAgB,aACd,WACA,cACA,UAAA,GACS;CACT,OAAO,gBAAgB,KAAK,KAAK,IAAI,YAAY,YAAY,KAAK;AACpE;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,uBAAuB,MAmB3B;CACV,MAAM,EAAE,OAAO,cAAc,WAAW,eAAe,SAAS;CAChE,IAAI,CAAC,cAAc,OAAO;CAC1B,MAAM,OAAO,mBAAmB,IAAI;CAOpC,MAAM,qBACH,KAAK,kBAAkB,KAAK,gBAAgB,KAAK,aAAa,gBAAgB,gBAAgB;CACjG,IAAI,QAAQ,gBAAgB,GAAG,OAAO,oBAAoB,QAAQ;CAClE,IAAI,gBAAgB,GAAG,OAAO,QAAA;CAC9B,IAAI,YAAY,gBAAgB,IAAK,OAAO;CAY5C,IAAI,YAAY,gBAAgB,MAAO,QAAA,IAA4B,OAAO;CAC1E,OAAO;AACT;AASA,IAAM,iCAAiB,IAAI,IAAI;CAAC;CAAW;CAAU;AAAM,CAAC;;;;;;;;;;AAW5D,SAAgB,iBAAiB,OAA6B;CAC5D,QAAQ,MAAM,MAAd;EACE,KAAK,SACH,OAAO,MAAM,SAAS;EACxB,KAAK,OACH,OAAO,eAAe,IAAI,MAAM,GAAG,KAAM,MAAM,QAAQ,OAAO,MAAM;EACtE,KAAK,aACH,OAAO;EACT,KAAK,eACH,OAAO,MAAM;CACjB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,gBAAgB,MAmDd;CAChB,MAAM,EAAE,OAAO,MAAM,iBAAiB;CACtC,MAAM,UAAU,KAAK,WAAA;CACrB,MAAM,iBAAiB,KAAK,IAAI,GAAG,KAAK,kBAAkB,CAAC;CAC3D,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,wBAAwB,KAAK,yBAAyB;CAC5D,MAAM,SAAS,aAAa,IAAI;CAChC,IAAI,KAAK,aAAa,OAAO;EAAE,KAAK;EAAO,OAAO;EAAO;CAAO;CAChE,IAAI,CAAC,OAAO,OAAO;EAAE,KAAK;EAAO,OAAO;EAAO;CAAO;CAStD,MAAM,oBAAoB,gBAAgB,KAAK,KAAK,IAAI,KAAK,YAAY,YAAY,KAAK;CAC1F,IAAI,CAAC,yBAAyB,mBAC5B,OAAO;EAAE,KAAK,mBAAmB,IAAI,IAAI,gBAAgB;EAAG,OAAO;EAAM;CAAO;CASlF,IAAI,CAAC,aAAa,mBAAmB,IAAI,IAAI,gBAAgB,GAS3D,OAAO;EAAE,KAAK;EAAO,OAAO;EAAO;CAAO;CAS5C,IACE,gBAAgB,KAChB,KAAK,YAAY,eAAe,WAChC,mBAAmB,IAAI,IAAI,iBAAiB,SAE5C,OAAO;EAAE,KAAK;EAAO,OAAO;EAAO;CAAO;CAE5C,OAAO;EAAE,KAAK,KAAK,IAAI,KAAK,YAAY,MAAM,IAAI,gBAAgB;EAAG,OAAO;EAAM;CAAO;AAC3F"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ConversationReadingPosition } from './conversation-surface';
|
|
2
|
+
/** Opens a hold on the follow position (see `createScrollFollow`); the function it returns closes it. */
|
|
3
|
+
export type ScrollFollowHold = () => () => void;
|
|
2
4
|
/** Internal plain-scroller binding; host-owned restoration can disable follow. */
|
|
3
5
|
export declare function createScrollFollow(options: {
|
|
4
6
|
enabled: () => boolean;
|
|
@@ -10,6 +12,7 @@ export declare function createScrollFollow(options: {
|
|
|
10
12
|
atBottom: import("solid-js").Accessor<boolean>;
|
|
11
13
|
onScroll: () => void;
|
|
12
14
|
jump: () => void;
|
|
15
|
+
hold: () => () => void;
|
|
13
16
|
bindScroller: (element: HTMLDivElement) => void;
|
|
14
17
|
bindContent: (element: HTMLDivElement) => void;
|
|
15
18
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-follow.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/scroll-follow.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"scroll-follow.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/scroll-follow.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAA;AAqBzE,yGAAyG;AACzG,MAAM,MAAM,gBAAgB,GAAG,MAAM,MAAM,IAAI,CAAA;AAE/C,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,OAAO,EAAE,MAAM,OAAO,CAAA;IACtB,QAAQ,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAClC,SAAS,EAAE,MAAM,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,2BAA2B,GAAG,SAAS,CAAA;IAC/D,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,2BAA2B,KAAK,IAAI,CAAA;CACnE;;;;;4BAqS2B,cAAc;2BAKf,cAAc;EAIxC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bottomTarget, computeAtBottom, evaluateAutoPin, isSelfScroll, resolveUserScrollStick } from "./scroll-follow-core.js";
|
|
1
|
+
import { atBottomEpsilon, bottomTarget, computeAtBottom, distanceFromBottom, evaluateAutoPin, isScrollUpIntent, isSelfScroll, resolveUserScrollStick } from "./scroll-follow-core.js";
|
|
2
2
|
import { createEffect, createSignal, onCleanup, untrack } from "solid-js";
|
|
3
3
|
//#region src/components/conversation/scroll-follow.ts
|
|
4
4
|
var geometry = (element) => ({
|
|
@@ -20,6 +20,19 @@ function createScrollFollow(options) {
|
|
|
20
20
|
let pendingRestore;
|
|
21
21
|
let pendingRestoreUserIntent = false;
|
|
22
22
|
let removeRestoreIntentListeners = noop;
|
|
23
|
+
let holds = 0;
|
|
24
|
+
let holdFollowing = false;
|
|
25
|
+
let holdIntent = false;
|
|
26
|
+
let holdTop = -1;
|
|
27
|
+
let holdGeneration = 0;
|
|
28
|
+
const holding = () => holds > 0 && holdFollowing && !holdIntent;
|
|
29
|
+
const holdsPosition = (element) => {
|
|
30
|
+
if (!holding()) return false;
|
|
31
|
+
const maxTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
|
32
|
+
if (Math.abs(element.scrollTop - Math.min(holdTop, maxTop)) <= 2) return true;
|
|
33
|
+
holdIntent = true;
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
23
36
|
const reportPosition = () => {
|
|
24
37
|
if (scroller) untrack(() => options.onPositionChange?.({
|
|
25
38
|
top: scroller.scrollTop,
|
|
@@ -33,8 +46,22 @@ function createScrollFollow(options) {
|
|
|
33
46
|
lastWriteClientHeight = element.clientHeight;
|
|
34
47
|
previousTop = element.scrollTop;
|
|
35
48
|
};
|
|
49
|
+
const pinHeld = (element) => {
|
|
50
|
+
const geom = geometry(element);
|
|
51
|
+
stick = true;
|
|
52
|
+
if (distanceFromBottom(geom) > atBottomEpsilon()) writePin(element, bottomTarget(geom));
|
|
53
|
+
else {
|
|
54
|
+
lastWriteTop = element.scrollTop;
|
|
55
|
+
lastWriteClientHeight = element.clientHeight;
|
|
56
|
+
previousTop = element.scrollTop;
|
|
57
|
+
}
|
|
58
|
+
holdTop = element.scrollTop;
|
|
59
|
+
setAtBottom(computeAtBottom(geometry(element), options.threshold()));
|
|
60
|
+
reportPosition();
|
|
61
|
+
};
|
|
36
62
|
const pinAuto = () => {
|
|
37
63
|
if (!scroller || !options.enabled() || pendingRestore) return;
|
|
64
|
+
if (holdsPosition(scroller)) return pinHeld(scroller);
|
|
38
65
|
const geom = geometry(scroller);
|
|
39
66
|
const result = evaluateAutoPin({
|
|
40
67
|
stick,
|
|
@@ -68,6 +95,7 @@ function createScrollFollow(options) {
|
|
|
68
95
|
reportPosition();
|
|
69
96
|
return;
|
|
70
97
|
}
|
|
98
|
+
if (holdsPosition(scroller)) return pinHeld(scroller);
|
|
71
99
|
const geom = geometry(scroller);
|
|
72
100
|
setAtBottom(computeAtBottom(geom, options.threshold()));
|
|
73
101
|
if (!isSelfScroll(geom.scrollTop, lastWriteTop)) {
|
|
@@ -96,6 +124,46 @@ function createScrollFollow(options) {
|
|
|
96
124
|
reportPosition();
|
|
97
125
|
scroller.focus({ preventScroll: true });
|
|
98
126
|
};
|
|
127
|
+
const hold = () => {
|
|
128
|
+
if (holds === 0) {
|
|
129
|
+
holdFollowing = options.enabled() && stick && scroller !== void 0;
|
|
130
|
+
holdIntent = false;
|
|
131
|
+
holdTop = scroller ? scroller.scrollTop : -1;
|
|
132
|
+
}
|
|
133
|
+
holds += 1;
|
|
134
|
+
const generation = holdGeneration;
|
|
135
|
+
let open = true;
|
|
136
|
+
return () => {
|
|
137
|
+
if (!open || generation !== holdGeneration) return;
|
|
138
|
+
open = false;
|
|
139
|
+
const last = holds === 1;
|
|
140
|
+
const resume = last && scroller !== void 0 && holdsPosition(scroller);
|
|
141
|
+
holds -= 1;
|
|
142
|
+
if (!last) return;
|
|
143
|
+
holdFollowing = false;
|
|
144
|
+
holdIntent = false;
|
|
145
|
+
if (resume && scroller && options.enabled() && !pendingRestore) pinHeld(scroller);
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const noteHoldInput = (event) => {
|
|
149
|
+
if (holds === 0 || holdIntent) return;
|
|
150
|
+
let input;
|
|
151
|
+
if (event instanceof WheelEvent) input = {
|
|
152
|
+
kind: "wheel",
|
|
153
|
+
deltaY: event.deltaY
|
|
154
|
+
};
|
|
155
|
+
else if (event instanceof KeyboardEvent) input = {
|
|
156
|
+
kind: "key",
|
|
157
|
+
key: event.key,
|
|
158
|
+
shiftKey: event.shiftKey
|
|
159
|
+
};
|
|
160
|
+
else if (event.type === "touchmove") input = { kind: "touchmove" };
|
|
161
|
+
else if (event.type === "pointerdown") input = {
|
|
162
|
+
kind: "pointerdown",
|
|
163
|
+
onScroller: event.target === scroller
|
|
164
|
+
};
|
|
165
|
+
if (input && isScrollUpIntent(input)) holdIntent = true;
|
|
166
|
+
};
|
|
99
167
|
createEffect(() => {
|
|
100
168
|
options.resetKey();
|
|
101
169
|
const enabled = options.enabled();
|
|
@@ -114,6 +182,10 @@ function createScrollFollow(options) {
|
|
|
114
182
|
observer?.disconnect();
|
|
115
183
|
});
|
|
116
184
|
stick = true;
|
|
185
|
+
holdGeneration += 1;
|
|
186
|
+
holds = 0;
|
|
187
|
+
holdFollowing = false;
|
|
188
|
+
holdIntent = false;
|
|
117
189
|
lastWriteTop = -1;
|
|
118
190
|
lastWriteClientHeight = -1;
|
|
119
191
|
previousTop = -1;
|
|
@@ -190,8 +262,15 @@ function createScrollFollow(options) {
|
|
|
190
262
|
atBottom,
|
|
191
263
|
onScroll,
|
|
192
264
|
jump,
|
|
265
|
+
hold,
|
|
193
266
|
bindScroller: (element) => {
|
|
194
267
|
scroller = element;
|
|
268
|
+
for (const type of [
|
|
269
|
+
"wheel",
|
|
270
|
+
"touchmove",
|
|
271
|
+
"pointerdown",
|
|
272
|
+
"keydown"
|
|
273
|
+
]) element.addEventListener(type, noteHoldInput, { passive: true });
|
|
195
274
|
},
|
|
196
275
|
bindContent: (element) => {
|
|
197
276
|
content = element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-follow.js","names":[],"sources":["../../../src/components/conversation/scroll-follow.ts"],"sourcesContent":["/*\n * Substantially translated from KiroCrew website/src/app-sdk/useChatScrollFollow.ts,\n * pinned at 283e136c0f902e965a535a7c9548c57c7504fed0.\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * This product includes software developed at Amazon.com, Inc. (https://www.amazon.com/).\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy at http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software distributed\n * under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\n * CONDITIONS OF ANY KIND, either express or implied. See the License for the\n * specific language governing permissions and limitations under the License.\n * Solid owner effects replace React refs/effects; native geometry and the\n * content/viewport observer contract are retained. No session store is copied.\n */\nimport { createEffect, createSignal, onCleanup, untrack } from 'solid-js'\nimport type { ConversationReadingPosition } from './conversation-surface'\nimport {\n bottomTarget,\n computeAtBottom,\n evaluateAutoPin,\n isSelfScroll,\n resolveUserScrollStick,\n} from './scroll-follow-core'\n\nconst geometry = (element: HTMLDivElement) => ({\n scrollTop: element.scrollTop,\n scrollHeight: element.scrollHeight,\n clientHeight: element.clientHeight,\n})\nconst noop = () => {}\n\n/** Internal plain-scroller binding; host-owned restoration can disable follow. */\nexport function createScrollFollow(options: {\n enabled: () => boolean\n resetKey: () => string | undefined\n threshold: () => number\n initialPosition?: () => ConversationReadingPosition | undefined\n onPositionChange?: (position: ConversationReadingPosition) => void\n}) {\n const [atBottom, setAtBottom] = createSignal(true)\n let scroller: HTMLDivElement | undefined\n let content: HTMLDivElement | undefined\n let stick = true\n let lastWriteTop = -1\n let lastWriteClientHeight = -1\n let previousTop = -1\n let lastScrollClientHeight = 0\n let pendingRestore: { frame: number; fromTop: number; target: number } | undefined\n let pendingRestoreUserIntent = false\n let removeRestoreIntentListeners = noop\n const reportPosition = () => {\n if (scroller)\n untrack(() =>\n options.onPositionChange?.({\n top: scroller!.scrollTop,\n following: options.enabled() && stick,\n })\n )\n }\n onCleanup(reportPosition)\n\n const writePin = (element: HTMLDivElement, target: number) => {\n // Instant writes keep the self-scroll reference synchronized. A smooth\n // animation would produce intermediate positions that resemble user input.\n element.scrollTop = target\n lastWriteTop = element.scrollTop\n lastWriteClientHeight = element.clientHeight\n previousTop = element.scrollTop\n }\n const pinAuto = () => {\n if (!scroller || !options.enabled() || pendingRestore) return\n const geom = geometry(scroller)\n const result = evaluateAutoPin({\n stick,\n geom,\n lastWriteTop,\n viewportShrink: lastWriteClientHeight >= 0 ? lastWriteClientHeight - geom.clientHeight : 0,\n })\n stick = result.stick\n if (result.pin) writePin(scroller, result.target)\n else if (result.stick) {\n lastWriteTop = result.target\n lastWriteClientHeight = geom.clientHeight\n }\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n reportPosition()\n }\n const onScroll = () => {\n if (!scroller) return\n if (pendingRestore) {\n // A reset can clamp the outgoing offset while the incoming rows are\n // shorter. That notification is layout, not reader intent, and must not\n // cancel the snapshot waiting for the new rows to commit.\n const maxTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight)\n const clampedFromTop = Math.min(pendingRestore.fromTop, maxTop)\n if (\n pendingRestore.fromTop - clampedFromTop > 1 &&\n Math.abs(scroller.scrollTop - clampedFromTop) <= 1\n )\n return\n // An unexplained offset change can also be a browser clamp. Only actual\n // user scroll intent followed by a non-clamp movement cancels restoration.\n if (!pendingRestoreUserIntent) return\n cancelAnimationFrame(pendingRestore.frame)\n pendingRestore = undefined\n pendingRestoreUserIntent = false\n removeRestoreIntentListeners()\n removeRestoreIntentListeners = noop\n }\n if (!options.enabled()) {\n reportPosition()\n return\n }\n const geom = geometry(scroller)\n setAtBottom(computeAtBottom(geom, options.threshold()))\n if (!isSelfScroll(geom.scrollTop, lastWriteTop)) {\n stick = resolveUserScrollStick({\n stick,\n followOutput: true,\n scrollTop: geom.scrollTop,\n prevScrollTop: previousTop,\n geom,\n viewportGrowth: lastScrollClientHeight > 0 ? geom.clientHeight - lastScrollClientHeight : 0,\n })\n if (!stick) {\n lastWriteTop = -1\n lastWriteClientHeight = -1\n }\n }\n previousTop = geom.scrollTop\n // This baseline belongs only to scroll events, not observer callbacks.\n // Advancing it during resize would erase the viewport-clamp evidence.\n lastScrollClientHeight = geom.clientHeight\n reportPosition()\n }\n const jump = () => {\n if (!scroller || !options.enabled()) return\n stick = true\n writePin(scroller, bottomTarget(geometry(scroller)))\n setAtBottom(true)\n reportPosition()\n // The jump control disappears at the bottom. Return its keyboard focus to\n // the transcript instead of leaving the reader on a detached button.\n scroller.focus({ preventScroll: true })\n }\n\n createEffect(() => {\n options.resetKey()\n const enabled = options.enabled()\n // Capture before any reporting callback can replace the host snapshot.\n const candidate = untrack(() => options.initialPosition?.())\n const initial =\n candidate && Number.isFinite(candidate.top) && candidate.top >= 0 ? candidate : undefined\n let scheduledRestore: typeof pendingRestore\n let observer: ResizeObserver | undefined\n onCleanup(() => {\n if (scheduledRestore) {\n cancelAnimationFrame(scheduledRestore.frame)\n if (pendingRestore === scheduledRestore) pendingRestore = undefined\n }\n pendingRestoreUserIntent = false\n removeRestoreIntentListeners()\n removeRestoreIntentListeners = noop\n observer?.disconnect()\n })\n stick = true\n lastWriteTop = -1\n lastWriteClientHeight = -1\n previousTop = -1\n lastScrollClientHeight = 0\n setAtBottom(true)\n if (!enabled || !scroller) return\n // Snapshot updates must not become another positioning owner. Consume the\n // host's snapshot only at entry/reset, then native reader intent owns follow.\n if (initial) {\n stick = enabled && initial.following\n }\n\n const observe = () => {\n if (!enabled || typeof ResizeObserver === 'undefined') return\n observer = new ResizeObserver(pinAuto)\n observer.observe(scroller!)\n if (content) observer.observe(content)\n }\n\n const watchRestoreIntent = () => {\n const element = scroller\n if (!element) return\n const markIntent = (event: Event) => {\n if (!pendingRestore) return\n if (event.type === 'pointerdown' && event.target !== element) return\n if (event.type === 'keydown') {\n if (event.target !== element) return\n const key = (event as KeyboardEvent).key\n if (!['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End', ' '].includes(key))\n return\n }\n pendingRestoreUserIntent = true\n }\n element.addEventListener('wheel', markIntent, { passive: true })\n element.addEventListener('pointerdown', markIntent)\n element.addEventListener('keydown', markIntent)\n removeRestoreIntentListeners = () => {\n element.removeEventListener('wheel', markIntent)\n element.removeEventListener('pointerdown', markIntent)\n element.removeEventListener('keydown', markIntent)\n }\n }\n\n if (initial && !stick) {\n // The identity effect can run before Solid has inserted the new rows. A\n // synchronous write is clamped by the outgoing short transcript, so defer\n // restoration until the browser has committed the new content and layout.\n const restore = {\n frame: 0,\n fromTop: scroller.scrollTop,\n target: initial.top,\n }\n scheduledRestore = restore\n pendingRestore = restore\n pendingRestoreUserIntent = false\n watchRestoreIntent()\n // Keep resize observation alive during the pending frame. Its callback is\n // guarded above, and remains active if user input cancels restoration.\n observe()\n restore.frame = requestAnimationFrame(() => {\n if (pendingRestore !== restore || !scroller) return\n pendingRestore = undefined\n pendingRestoreUserIntent = false\n removeRestoreIntentListeners()\n removeRestoreIntentListeners = noop\n writePin(scroller, restore.target)\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n reportPosition()\n })\n return\n }\n\n if (!initial && enabled) writePin(scroller, bottomTarget(geometry(scroller)))\n else if (initial && stick) writePin(scroller, bottomTarget(geometry(scroller)))\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n reportPosition()\n observe()\n })\n\n return {\n atBottom,\n onScroll,\n jump,\n bindScroller: (element: HTMLDivElement) => {\n scroller = element\n },\n bindContent: (element: HTMLDivElement) => {\n content = element\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scroll-follow.js","names":[],"sources":["../../../src/components/conversation/scroll-follow.ts"],"sourcesContent":["/*\n * Substantially translated from KiroCrew website/src/app-sdk/useChatScrollFollow.ts,\n * pinned at 283e136c0f902e965a535a7c9548c57c7504fed0.\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * This product includes software developed at Amazon.com, Inc. (https://www.amazon.com/).\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy at http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software distributed\n * under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\n * CONDITIONS OF ANY KIND, either express or implied. See the License for the\n * specific language governing permissions and limitations under the License.\n * Solid owner effects replace React refs/effects; native geometry and the\n * content/viewport observer contract are retained. No session store is copied.\n */\nimport { createEffect, createSignal, onCleanup, untrack } from 'solid-js'\nimport type { ConversationReadingPosition } from './conversation-surface'\nimport {\n atBottomEpsilon,\n bottomTarget,\n computeAtBottom,\n distanceFromBottom,\n evaluateAutoPin,\n isScrollUpIntent,\n isSelfScroll,\n resolveUserScrollStick,\n SELF_SCROLL_EPSILON,\n type ReaderInput,\n} from './scroll-follow-core'\n\nconst geometry = (element: HTMLDivElement) => ({\n scrollTop: element.scrollTop,\n scrollHeight: element.scrollHeight,\n clientHeight: element.clientHeight,\n})\nconst noop = () => {}\n\n/** Opens a hold on the follow position (see `createScrollFollow`); the function it returns closes it. */\nexport type ScrollFollowHold = () => () => void\n\n/** Internal plain-scroller binding; host-owned restoration can disable follow. */\nexport function createScrollFollow(options: {\n enabled: () => boolean\n resetKey: () => string | undefined\n threshold: () => number\n initialPosition?: () => ConversationReadingPosition | undefined\n onPositionChange?: (position: ConversationReadingPosition) => void\n}) {\n const [atBottom, setAtBottom] = createSignal(true)\n let scroller: HTMLDivElement | undefined\n let content: HTMLDivElement | undefined\n let stick = true\n let lastWriteTop = -1\n let lastWriteClientHeight = -1\n let previousTop = -1\n let lastScrollClientHeight = 0\n let pendingRestore: { frame: number; fromTop: number; target: number } | undefined\n let pendingRestoreUserIntent = false\n let removeRestoreIntentListeners = noop\n // A hold is the caller saying it is positioning the view (see `hold`): while one is\n // open and the reader was following, the content's moves are not read as the reader's.\n let holds = 0\n let holdFollowing = false\n let holdIntent = false\n let holdTop = -1\n // Bumped on every identity change, so a hold opened for the previous conversation can\n // neither pin the next one nor close over its state.\n let holdGeneration = 0\n const holding = () => holds > 0 && holdFollowing && !holdIntent\n // Is the view still where the hold left it? The content can move the offset only by\n // clamping it to the end it has now; anything else moved it from outside (a reveal, a\n // search jump), and that is the caller's to keep, so the hold gives way.\n const holdsPosition = (element: HTMLDivElement) => {\n if (!holding()) return false\n const maxTop = Math.max(0, element.scrollHeight - element.clientHeight)\n if (Math.abs(element.scrollTop - Math.min(holdTop, maxTop)) <= SELF_SCROLL_EPSILON) return true\n holdIntent = true\n return false\n }\n const reportPosition = () => {\n if (scroller)\n untrack(() =>\n options.onPositionChange?.({\n top: scroller!.scrollTop,\n following: options.enabled() && stick,\n })\n )\n }\n onCleanup(reportPosition)\n\n const writePin = (element: HTMLDivElement, target: number) => {\n // Instant writes keep the self-scroll reference synchronized. A smooth\n // animation would produce intermediate positions that resemble user input.\n element.scrollTop = target\n lastWriteTop = element.scrollTop\n lastWriteClientHeight = element.clientHeight\n previousTop = element.scrollTop\n }\n const pinHeld = (element: HTMLDivElement) => {\n const geom = geometry(element)\n stick = true\n if (distanceFromBottom(geom) > atBottomEpsilon()) writePin(element, bottomTarget(geom))\n // An end the engine already clamped to is accepted as it stands, and is the reference\n // from here on: the write before it is further down than the content now reaches.\n else {\n lastWriteTop = element.scrollTop\n lastWriteClientHeight = element.clientHeight\n previousTop = element.scrollTop\n }\n holdTop = element.scrollTop\n setAtBottom(computeAtBottom(geometry(element), options.threshold()))\n reportPosition()\n }\n const pinAuto = () => {\n if (!scroller || !options.enabled() || pendingRestore) return\n if (holdsPosition(scroller)) return pinHeld(scroller)\n const geom = geometry(scroller)\n const result = evaluateAutoPin({\n stick,\n geom,\n lastWriteTop,\n viewportShrink: lastWriteClientHeight >= 0 ? lastWriteClientHeight - geom.clientHeight : 0,\n })\n stick = result.stick\n if (result.pin) writePin(scroller, result.target)\n else if (result.stick) {\n lastWriteTop = result.target\n lastWriteClientHeight = geom.clientHeight\n }\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n reportPosition()\n }\n const onScroll = () => {\n if (!scroller) return\n if (pendingRestore) {\n // A reset can clamp the outgoing offset while the incoming rows are\n // shorter. That notification is layout, not reader intent, and must not\n // cancel the snapshot waiting for the new rows to commit.\n const maxTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight)\n const clampedFromTop = Math.min(pendingRestore.fromTop, maxTop)\n if (\n pendingRestore.fromTop - clampedFromTop > 1 &&\n Math.abs(scroller.scrollTop - clampedFromTop) <= 1\n )\n return\n // An unexplained offset change can also be a browser clamp. Only actual\n // user scroll intent followed by a non-clamp movement cancels restoration.\n if (!pendingRestoreUserIntent) return\n cancelAnimationFrame(pendingRestore.frame)\n pendingRestore = undefined\n pendingRestoreUserIntent = false\n removeRestoreIntentListeners()\n removeRestoreIntentListeners = noop\n }\n if (!options.enabled()) {\n reportPosition()\n return\n }\n if (holdsPosition(scroller)) return pinHeld(scroller)\n const geom = geometry(scroller)\n setAtBottom(computeAtBottom(geom, options.threshold()))\n if (!isSelfScroll(geom.scrollTop, lastWriteTop)) {\n stick = resolveUserScrollStick({\n stick,\n followOutput: true,\n scrollTop: geom.scrollTop,\n prevScrollTop: previousTop,\n geom,\n viewportGrowth: lastScrollClientHeight > 0 ? geom.clientHeight - lastScrollClientHeight : 0,\n })\n if (!stick) {\n lastWriteTop = -1\n lastWriteClientHeight = -1\n }\n }\n previousTop = geom.scrollTop\n // This baseline belongs only to scroll events, not observer callbacks.\n // Advancing it during resize would erase the viewport-clamp evidence.\n lastScrollClientHeight = geom.clientHeight\n reportPosition()\n }\n const jump = () => {\n if (!scroller || !options.enabled()) return\n stick = true\n writePin(scroller, bottomTarget(geometry(scroller)))\n setAtBottom(true)\n reportPosition()\n // The jump control disappears at the bottom. Return its keyboard focus to\n // the transcript instead of leaving the reader on a detached button.\n scroller.focus({ preventScroll: true })\n }\n\n // Open while the caller moves content under a reader who is following, so a clamp or an\n // insert it causes is not mistaken for the reader leaving the end. It gives way to the\n // reader's own scroll-up (`isScrollUpIntent`) and to anything else that moves the view\n // from outside (a reveal, a search jump); closing it puts a reader who is still following\n // back at the end. A change of conversation invalidates it, so the next conversation's\n // saved position is its own. A reveal or search jump outside a hold is never affected.\n const hold = () => {\n if (holds === 0) {\n holdFollowing = options.enabled() && stick && scroller !== undefined\n holdIntent = false\n holdTop = scroller ? scroller.scrollTop : -1\n }\n holds += 1\n const generation = holdGeneration\n let open = true\n return () => {\n if (!open || generation !== holdGeneration) return\n open = false\n // Asked while this hold still counts: `holdsPosition` is false once none is open.\n const last = holds === 1\n const resume = last && scroller !== undefined && holdsPosition(scroller)\n holds -= 1\n if (!last) return\n holdFollowing = false\n holdIntent = false\n if (resume && scroller && options.enabled() && !pendingRestore) pinHeld(scroller)\n }\n }\n const noteHoldInput = (event: Event) => {\n if (holds === 0 || holdIntent) return\n let input: ReaderInput | undefined\n if (event instanceof WheelEvent) input = { kind: 'wheel', deltaY: event.deltaY }\n else if (event instanceof KeyboardEvent)\n input = { kind: 'key', key: event.key, shiftKey: event.shiftKey }\n else if (event.type === 'touchmove') input = { kind: 'touchmove' }\n else if (event.type === 'pointerdown')\n input = { kind: 'pointerdown', onScroller: event.target === scroller }\n if (input && isScrollUpIntent(input)) holdIntent = true\n }\n\n createEffect(() => {\n options.resetKey()\n const enabled = options.enabled()\n // Capture before any reporting callback can replace the host snapshot.\n const candidate = untrack(() => options.initialPosition?.())\n const initial =\n candidate && Number.isFinite(candidate.top) && candidate.top >= 0 ? candidate : undefined\n let scheduledRestore: typeof pendingRestore\n let observer: ResizeObserver | undefined\n onCleanup(() => {\n if (scheduledRestore) {\n cancelAnimationFrame(scheduledRestore.frame)\n if (pendingRestore === scheduledRestore) pendingRestore = undefined\n }\n pendingRestoreUserIntent = false\n removeRestoreIntentListeners()\n removeRestoreIntentListeners = noop\n observer?.disconnect()\n })\n stick = true\n holdGeneration += 1\n holds = 0\n holdFollowing = false\n holdIntent = false\n lastWriteTop = -1\n lastWriteClientHeight = -1\n previousTop = -1\n lastScrollClientHeight = 0\n setAtBottom(true)\n if (!enabled || !scroller) return\n // Snapshot updates must not become another positioning owner. Consume the\n // host's snapshot only at entry/reset, then native reader intent owns follow.\n if (initial) {\n stick = enabled && initial.following\n }\n\n const observe = () => {\n if (!enabled || typeof ResizeObserver === 'undefined') return\n observer = new ResizeObserver(pinAuto)\n observer.observe(scroller!)\n if (content) observer.observe(content)\n }\n\n const watchRestoreIntent = () => {\n const element = scroller\n if (!element) return\n const markIntent = (event: Event) => {\n if (!pendingRestore) return\n if (event.type === 'pointerdown' && event.target !== element) return\n if (event.type === 'keydown') {\n if (event.target !== element) return\n const key = (event as KeyboardEvent).key\n if (!['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End', ' '].includes(key))\n return\n }\n pendingRestoreUserIntent = true\n }\n element.addEventListener('wheel', markIntent, { passive: true })\n element.addEventListener('pointerdown', markIntent)\n element.addEventListener('keydown', markIntent)\n removeRestoreIntentListeners = () => {\n element.removeEventListener('wheel', markIntent)\n element.removeEventListener('pointerdown', markIntent)\n element.removeEventListener('keydown', markIntent)\n }\n }\n\n if (initial && !stick) {\n // The identity effect can run before Solid has inserted the new rows. A\n // synchronous write is clamped by the outgoing short transcript, so defer\n // restoration until the browser has committed the new content and layout.\n const restore = {\n frame: 0,\n fromTop: scroller.scrollTop,\n target: initial.top,\n }\n scheduledRestore = restore\n pendingRestore = restore\n pendingRestoreUserIntent = false\n watchRestoreIntent()\n // Keep resize observation alive during the pending frame. Its callback is\n // guarded above, and remains active if user input cancels restoration.\n observe()\n restore.frame = requestAnimationFrame(() => {\n if (pendingRestore !== restore || !scroller) return\n pendingRestore = undefined\n pendingRestoreUserIntent = false\n removeRestoreIntentListeners()\n removeRestoreIntentListeners = noop\n writePin(scroller, restore.target)\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n reportPosition()\n })\n return\n }\n\n if (!initial && enabled) writePin(scroller, bottomTarget(geometry(scroller)))\n else if (initial && stick) writePin(scroller, bottomTarget(geometry(scroller)))\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n reportPosition()\n observe()\n })\n\n return {\n atBottom,\n onScroll,\n jump,\n hold,\n bindScroller: (element: HTMLDivElement) => {\n scroller = element\n for (const type of ['wheel', 'touchmove', 'pointerdown', 'keydown'])\n element.addEventListener(type, noteHoldInput, { passive: true })\n },\n bindContent: (element: HTMLDivElement) => {\n content = element\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type ComponentProps, type JSX } from 'solid-js';
|
|
2
|
+
/**
|
|
3
|
+
* Attachment.
|
|
4
|
+
*
|
|
5
|
+
* A file that travels with a message or sits in the composer. Three
|
|
6
|
+
* presentations of one thing: a `chip` (a line, for the composer and for a list
|
|
7
|
+
* of many), a `card` (name, type and size, for a message's attachments), and a
|
|
8
|
+
* `tile` (a preview image with the name beneath, for pictures).
|
|
9
|
+
*
|
|
10
|
+
* ## It only offers what works
|
|
11
|
+
*
|
|
12
|
+
* An attachment is a *claim that a file exists and can be handled*. This
|
|
13
|
+
* component therefore draws an action only when the caller supplied the
|
|
14
|
+
* handler: no `onOpen`/`href` and the name is plain text, not a dead button; no
|
|
15
|
+
* `onRemove` and there is no ✕. A host whose attachments cannot be reopened (a
|
|
16
|
+
* name the runtime echoed back, a file that was never stored) renders honest,
|
|
17
|
+
* static chips instead of controls that do nothing.
|
|
18
|
+
*
|
|
19
|
+
* ## Untrusted metadata
|
|
20
|
+
*
|
|
21
|
+
* Name, type and size come from whoever sent the file. They are rendered as
|
|
22
|
+
* text, the name is never used as a path or URL, and a preview is shown only
|
|
23
|
+
* for `blob:`, `https:`/`http:` or a `data:image/*` raster source (no SVG, which
|
|
24
|
+
* can carry script when opened on its own).
|
|
25
|
+
*
|
|
26
|
+
* `status` states the truth about the file's lifecycle in words (`uploading`,
|
|
27
|
+
* `error`, `unavailable`), not by colour alone.
|
|
28
|
+
*/
|
|
29
|
+
export type AttachmentKind = 'image' | 'document' | 'code' | 'text' | 'archive' | 'other';
|
|
30
|
+
export type AttachmentStatus = 'ready' | 'uploading' | 'error' | 'unavailable';
|
|
31
|
+
export type AttachmentVariant = 'chip' | 'card' | 'tile';
|
|
32
|
+
/** `1536` → `1.5 KB`. Returns nothing for a value that is not a finite size. */
|
|
33
|
+
export declare function formatAttachmentSize(bytes: number | undefined): string | undefined;
|
|
34
|
+
/** The only sources a preview may use. */
|
|
35
|
+
export declare function safePreviewSrc(value: string | undefined): string | undefined;
|
|
36
|
+
export type AttachmentProps = Omit<ComponentProps<'div'>, 'children'> & {
|
|
37
|
+
name: string;
|
|
38
|
+
kind?: AttachmentKind;
|
|
39
|
+
/** Bytes. */
|
|
40
|
+
size?: number;
|
|
41
|
+
variant?: AttachmentVariant;
|
|
42
|
+
status?: AttachmentStatus;
|
|
43
|
+
/** 0–100 while `uploading`; omit for indeterminate. */
|
|
44
|
+
progress?: number;
|
|
45
|
+
/** Why it failed or is unavailable, in words. */
|
|
46
|
+
statusLabel?: string;
|
|
47
|
+
previewSrc?: string;
|
|
48
|
+
/** Open the file in the host. Omit and the name is not a control. */
|
|
49
|
+
onOpen?: () => void;
|
|
50
|
+
/** Open the file by address. Omit and the name is not a link. */
|
|
51
|
+
href?: string;
|
|
52
|
+
/** Detach it. Omit and there is no remove control. */
|
|
53
|
+
onRemove?: () => void;
|
|
54
|
+
removeLabel?: string;
|
|
55
|
+
};
|
|
56
|
+
export declare function Attachment(props: AttachmentProps): JSX.Element;
|
|
57
|
+
export declare function AttachmentList(props: ComponentProps<'ul'>): JSX.Element;
|
|
58
|
+
//# sourceMappingURL=attachment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/attachment/attachment.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAoB,KAAK,cAAc,EAAE,KAAK,GAAG,EAAE,MAAM,UAAU,CAAA;AAI1E;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AACzF,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,GAAG,aAAa,CAAA;AAC9E,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAcxD,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAalF;AAED,0CAA0C;AAC1C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAU5E;AAED,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,GAAG;IACtE,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,aAAa;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,iBAAiB,CAAA;IAC3B,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,eAgKhD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,eAYzD"}
|