basalt-ui 1.27.0 → 1.28.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 +23 -23
- package/CHANGELOG.md +59 -0
- package/MIGRATING.md +613 -20
- package/README.md +33 -3
- package/agent/rules/basalt-batteries.md +25 -10
- package/agent/rules/basalt-controls.md +56 -31
- package/agent/rules/basalt-mantine.md +17 -6
- package/agent/rules/basalt-tokens.md +1 -1
- package/configs/oxlint-plugin.js +1056 -28
- package/configs/oxlint-plugin.test.ts +1066 -0
- package/configs/oxlint.json +7 -0
- 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-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/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 +8 -5
- package/dist/agent-chat/thread-feed-row.d.ts +2 -1
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed-row.js +92 -92
- package/dist/agent-chat/thread-feed.d.ts +3 -2
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +8 -3
- 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/charts/hooks/useBandPlot.d.ts.map +1 -1
- package/dist/charts/hooks/useBandPlot.js +15 -8
- package/dist/charts/index.d.ts +8 -7
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +45 -5
- 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 +12 -6
- 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 +6 -5
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +36 -8
- 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 +12 -8
- 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 +11 -2
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +15 -5
- 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 +27 -2
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +37 -17
- 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 +45 -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 +77 -4
- 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 +92 -2
- package/dist/charts/utils/format.d.ts.map +1 -1
- package/dist/charts/utils/format.js +64 -1
- 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/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -2
- 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 +10 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +3 -1
- package/dist/commands/overlays-mount.d.ts +13 -6
- package/dist/commands/overlays-mount.d.ts.map +1 -1
- package/dist/commands/overlays-mount.js +23 -14
- package/dist/common/errors.d.ts +34 -0
- package/dist/common/errors.d.ts.map +1 -0
- package/dist/common/errors.js +36 -0
- package/dist/common/index.d.ts +15 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +29 -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/connectivity/connectivity-indicator.d.ts +3 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +3 -1
- package/dist/connectivity/connectivity-provider.d.ts.map +1 -1
- package/dist/connectivity/connectivity-provider.js +4 -1
- package/dist/connectivity/index.d.ts +1 -0
- package/dist/connectivity/index.d.ts.map +1 -1
- package/dist/connectivity/types.d.ts +2 -2
- package/dist/connectivity/types.d.ts.map +1 -1
- package/dist/connectivity/use-connectivity.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/content/markdown.js +2 -0
- 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 +49 -6
- 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 +5 -1
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/empty-state.d.ts +10 -3
- package/dist/dashboard/empty-state.d.ts.map +1 -1
- package/dist/dashboard/empty-state.js +37 -6
- package/dist/dashboard/index.d.ts +6 -4
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +7 -1
- package/dist/dashboard/query-state.d.ts +48 -27
- package/dist/dashboard/query-state.d.ts.map +1 -1
- package/dist/dashboard/query-state.js +111 -57
- 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 +74 -25
- 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 +55 -42
- 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/data/data-table.d.ts +151 -12
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +384 -138
- package/dist/data/data-table.module.css +64 -0
- package/dist/data/data-table.module.css.d.ts +11 -0
- package/dist/data/index.d.ts +2 -2
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +24 -2
- 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/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 +1 -1
- package/dist/data/virtual.d.ts.map +1 -1
- 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 +46 -6
- package/dist/forms/field.d.ts.map +1 -1
- package/dist/forms/field.js +10 -5
- 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 -3
- 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.map +1 -1
- package/dist/guard/index.js +26 -6
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -4
- package/dist/nav/types.d.ts +15 -6
- package/dist/nav/types.d.ts.map +1 -1
- 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 +6 -2
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +5 -1
- 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/overlay.d.ts.map +1 -1
- package/dist/notifications/overlay.js +2 -0
- package/dist/notifications/store.d.ts +7 -0
- package/dist/notifications/store.d.ts.map +1 -1
- package/dist/provider/index.d.ts +37 -5
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +66 -28
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/lab-theme.js +2 -1
- package/dist/query/devtools.d.ts.map +1 -1
- package/dist/query/devtools.js +1 -0
- package/dist/router-tanstack/field.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/router-tanstack/use-nav.js +1 -1
- package/dist/section/index.d.ts +1 -1
- package/dist/section/index.d.ts.map +1 -1
- package/dist/section/section.d.ts +36 -2
- package/dist/section/section.d.ts.map +1 -1
- package/dist/section/section.js +49 -57
- package/dist/section/section.module.css +11 -0
- 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 +4 -2
- 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 +107 -0
- package/dist/shell/app-main.module.css.d.ts +11 -0
- package/dist/shell/app-mobile-nav.d.ts +3 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +29 -15
- 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 +9 -3
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +55 -109
- package/dist/shell/app-sidebar.module.css +53 -96
- package/dist/shell/app-sidebar.module.css.d.ts +0 -5
- package/dist/shell/index.d.ts +14 -6
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +47 -12
- 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.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/shell/page-aside.d.ts +43 -0
- package/dist/shell/page-aside.d.ts.map +1 -0
- package/dist/shell/page-aside.js +202 -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/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/state/fields.d.ts +23 -3
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +13 -2
- 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/surfaces.d.ts +12 -11
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +42 -12
- 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-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/theme-toggle/index.d.ts +3 -2
- package/dist/theme-toggle/index.d.ts.map +1 -1
- package/dist/theme-toggle/index.js +8 -2
- package/dist/tokens/index.d.ts +42 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +35 -6
- package/dist/tokens/palette.d.ts +45 -25
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +31 -14
- package/dist/tokens.css +4 -5
- package/dist/utils/is-dev.d.ts +3 -0
- package/dist/utils/is-dev.d.ts.map +1 -0
- package/dist/utils/is-dev.js +6 -0
- package/dist/widget-header/delta-badge.d.ts +7 -1
- package/dist/widget-header/delta-badge.d.ts.map +1 -1
- package/dist/widget-header/delta-badge.js +8 -1
- package/dist/widget-header/index.d.ts +2 -2
- package/dist/widget-header/index.d.ts.map +1 -1
- package/dist/widget-header/index.js +3 -1
- package/dist/widget-header/widget-header.d.ts +69 -4
- package/dist/widget-header/widget-header.d.ts.map +1 -1
- package/dist/widget-header/widget-header.js +42 -27
- package/dist/widget-header/widget-header.module.css +72 -7
- package/llms.txt +6 -6
- package/package.json +49 -25
- 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-chat/composer.tsx +124 -103
- package/src/agent-chat/relative-time.ts +2 -1
- package/src/agent-chat/thread-detail-panel.tsx +9 -5
- package/src/agent-chat/thread-feed-row.tsx +17 -9
- package/src/agent-chat/thread-feed.tsx +9 -3
- 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/charts/hooks/useBandPlot.ts +26 -8
- package/src/charts/index.ts +39 -4
- package/src/charts/kinds/BandStrip.tsx +36 -14
- package/src/charts/kinds/Bars.tsx +67 -16
- package/src/charts/kinds/Donut.tsx +30 -3
- package/src/charts/kinds/DualPanel.tsx +56 -10
- 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 +71 -31
- package/src/charts/layout/auto-margin.ts +134 -21
- package/src/charts/layout/log-ticks.ts +90 -0
- package/src/charts/primitives/Axes.tsx +33 -5
- package/src/charts/primitives/CartesianChart.tsx +194 -41
- package/src/charts/primitives/ChartCard.tsx +66 -25
- package/src/charts/primitives/ChartFrame.tsx +107 -26
- package/src/charts/primitives/ChartLegend.tsx +43 -13
- package/src/charts/primitives/ChartPending.tsx +185 -7
- 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 +163 -2
- package/src/charts/utils/ticks.ts +149 -4
- package/src/cli/index.ts +8 -3
- 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 +12 -0
- package/src/commands/overlays-mount.tsx +60 -26
- package/src/common/errors.ts +73 -0
- package/src/common/index.ts +21 -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/connectivity/connectivity-indicator.tsx +6 -1
- package/src/connectivity/connectivity-provider.tsx +4 -1
- package/src/connectivity/index.ts +1 -0
- package/src/connectivity/types.ts +5 -2
- package/src/connectivity/use-connectivity.ts +3 -2
- 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 +2 -0
- 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 +89 -9
- 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 +5 -1
- package/src/dashboard/empty-state.tsx +72 -25
- package/src/dashboard/index.ts +13 -1
- package/src/dashboard/query-state.tsx +154 -84
- package/src/dashboard/settings-section.tsx +87 -31
- 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 +185 -127
- 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/data/data-table.module.css +64 -0
- package/src/data/data-table.module.css.d.ts +11 -0
- package/src/data/data-table.tsx +919 -346
- package/src/data/index.ts +30 -2
- package/src/data/query-branch.ts +53 -0
- package/src/data/table.ts +5 -0
- package/src/data/virtual-list.tsx +189 -55
- package/src/data/virtual.ts +5 -1
- package/src/forms/FormErrorSummary.tsx +13 -6
- package/src/forms/create-form.ts +2 -2
- package/src/forms/field.ts +60 -11
- 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 +38 -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 +73 -18
- package/src/index.ts +74 -10
- package/src/nav/types.ts +15 -6
- 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 +12 -1
- package/src/notifications/mount-guard.ts +41 -0
- package/src/notifications/notify.tsx +122 -3
- package/src/notifications/overlay.tsx +2 -0
- package/src/notifications/store.ts +7 -0
- package/src/provider/index.tsx +101 -13
- package/src/provider/lab-theme.ts +2 -1
- package/src/query/devtools.tsx +1 -0
- package/src/router-tanstack/field.ts +1 -0
- package/src/router-tanstack/search-store.ts +3 -10
- package/src/router-tanstack/use-nav.ts +18 -13
- package/src/section/index.ts +1 -1
- package/src/section/section.module.css +11 -0
- package/src/section/section.tsx +163 -96
- 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 +19 -9
- package/src/shell/app-header.module.css +10 -5
- package/src/shell/app-main.module.css +107 -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 +43 -11
- package/src/shell/app-sidebar-account.tsx +29 -6
- package/src/shell/app-sidebar.module.css +53 -96
- package/src/shell/app-sidebar.module.css.d.ts +0 -5
- package/src/shell/app-sidebar.tsx +40 -113
- package/src/shell/index.tsx +156 -65
- package/src/shell/isomorphic-layout-effect.ts +14 -0
- package/src/shell/mobile-nav-model.ts +66 -10
- package/src/shell/nav-count-badge.tsx +6 -1
- 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 +377 -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/sidebar-search.module.css +1 -1
- package/src/shell/sidebar-search.tsx +13 -4
- package/src/state/fields.ts +43 -10
- package/src/state/persisted-or-local.ts +50 -0
- package/src/state.ts +0 -9
- package/src/surfaces.ts +42 -12
- package/src/theme/ctl-theme.tsx +10 -0
- package/src/theme/index.ts +129 -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-lab/boot.ts +140 -0
- package/src/theme-lab/index.tsx +21 -112
- package/src/theme-toggle/index.tsx +10 -3
- package/src/tokens/index.ts +50 -6
- package/src/tokens/palette.ts +58 -30
- package/src/utils/is-dev.ts +4 -0
- package/src/widget-header/delta-badge.tsx +25 -5
- package/src/widget-header/index.ts +10 -2
- package/src/widget-header/widget-header.module.css +72 -7
- package/src/widget-header/widget-header.tsx +150 -71
- 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/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/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/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/error-message.test.d.ts +0 -2
- package/dist/query/error-message.test.d.ts.map +0 -1
- 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.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/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/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.test.d.ts +0 -2
- package/dist/widget-header/delta-badge.test.d.ts.map +0 -1
- 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.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/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/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/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/router-tanstack/multi-search-param-store.test.ts +0 -124
- 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-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/section.test.tsx +0 -204
- 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/no-mantine.test.ts +0 -33
- package/src/widget-header/widget-header.test.tsx +0 -187
|
@@ -1,1120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useAgentThreadRuns — concurrency, busy-thread no-op, finalize ordering, failure status, retry,
|
|
3
|
-
* and (as of B2) the stop()/stopAll() lifecycle (partial-turn preservation, the idle no-op
|
|
4
|
-
* guard, and the resolveOutcome-race fix).
|
|
5
|
-
*
|
|
6
|
-
* Scope: the multi-thread run manager AS THE PUBLIC API EXISTS TODAY. No mergePart/part ids/
|
|
7
|
-
* offsets, no seven-state ToolCallPart, no ResumableAgentTransport/isResumable/idempotentReplay
|
|
8
|
-
* beyond the plain optional `resume` already on AgentTransport, no ThreadsStoreAdapter — later
|
|
9
|
-
* releases, not exercised here.
|
|
10
|
-
*
|
|
11
|
-
* The mount-reconcile/resume path (StartPart handling, orphaned-thread resumption on mount) has
|
|
12
|
-
* its own file, `use-agent-thread-runs.resume.test.tsx`; the F3 wedge has its own file,
|
|
13
|
-
* `use-agent-thread-runs.wedge.test.tsx`.
|
|
14
|
-
*/
|
|
15
|
-
import { describe, expect, test } from 'bun:test'
|
|
16
|
-
import { act, renderHook, waitFor } from '@testing-library/react'
|
|
17
|
-
import { useAgentThreadRuns } from './use-agent-thread-runs'
|
|
18
|
-
import type { AgentThread, ThreadsStore } from './thread'
|
|
19
|
-
import type { AgentTransport } from './transport'
|
|
20
|
-
import type { AgentPart, AgentPartDraft } from './parts'
|
|
21
|
-
import type { AgentOutcome, OutcomeResolver } from './outcome'
|
|
22
|
-
|
|
23
|
-
// ── test-only ThreadsStore double — see use-agent-thread-runs.wedge.test.tsx for the rationale ──
|
|
24
|
-
function createTestThreadsStore(
|
|
25
|
-
opts: {
|
|
26
|
-
readonly initial?: AgentThread<AgentPart>[]
|
|
27
|
-
readonly onCall?: (method: string) => void
|
|
28
|
-
} = {},
|
|
29
|
-
): ThreadsStore<AgentPart> {
|
|
30
|
-
let threads = opts.initial ?? []
|
|
31
|
-
let activeId: string | null = null
|
|
32
|
-
const onCall = opts.onCall ?? ((): void => {})
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
get threads() {
|
|
36
|
-
return threads
|
|
37
|
-
},
|
|
38
|
-
get activeId() {
|
|
39
|
-
return activeId
|
|
40
|
-
},
|
|
41
|
-
select(id) {
|
|
42
|
-
activeId = id
|
|
43
|
-
},
|
|
44
|
-
create(createOpts) {
|
|
45
|
-
const id = crypto.randomUUID()
|
|
46
|
-
const now = Date.now()
|
|
47
|
-
const thread: AgentThread<AgentPart> = {
|
|
48
|
-
id,
|
|
49
|
-
messages: [],
|
|
50
|
-
outcome: null,
|
|
51
|
-
status: 'pending',
|
|
52
|
-
read: false,
|
|
53
|
-
createdAt: now,
|
|
54
|
-
updatedAt: now,
|
|
55
|
-
...(createOpts?.meta !== undefined ? { meta: createOpts.meta } : {}),
|
|
56
|
-
}
|
|
57
|
-
threads = [thread, ...threads]
|
|
58
|
-
return id
|
|
59
|
-
},
|
|
60
|
-
appendMessage(id, message) {
|
|
61
|
-
onCall('appendMessage')
|
|
62
|
-
threads = threads.map((thread) =>
|
|
63
|
-
thread.id === id
|
|
64
|
-
? { ...thread, messages: [...thread.messages, message], updatedAt: Date.now() }
|
|
65
|
-
: thread,
|
|
66
|
-
)
|
|
67
|
-
},
|
|
68
|
-
setOutcome(id, outcome) {
|
|
69
|
-
onCall('setOutcome')
|
|
70
|
-
threads = threads.map((thread) =>
|
|
71
|
-
thread.id === id ? { ...thread, outcome, updatedAt: Date.now() } : thread,
|
|
72
|
-
)
|
|
73
|
-
},
|
|
74
|
-
setStatus(id, status) {
|
|
75
|
-
onCall('setStatus')
|
|
76
|
-
threads = threads.map((thread) =>
|
|
77
|
-
thread.id === id ? { ...thread, status, updatedAt: Date.now() } : thread,
|
|
78
|
-
)
|
|
79
|
-
},
|
|
80
|
-
setResumeToken(id, token) {
|
|
81
|
-
onCall('setResumeToken')
|
|
82
|
-
threads = threads.map((thread) => {
|
|
83
|
-
if (thread.id !== id) return thread
|
|
84
|
-
const { resumeToken: _resumeToken, ...rest } = thread
|
|
85
|
-
return {
|
|
86
|
-
...rest,
|
|
87
|
-
...(token !== undefined ? { resumeToken: token } : {}),
|
|
88
|
-
updatedAt: Date.now(),
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
},
|
|
92
|
-
markRead(id) {
|
|
93
|
-
threads = threads.map((thread) => (thread.id === id ? { ...thread, read: true } : thread))
|
|
94
|
-
},
|
|
95
|
-
remove(id) {
|
|
96
|
-
threads = threads.filter((thread) => thread.id !== id)
|
|
97
|
-
if (activeId === id) activeId = null
|
|
98
|
-
},
|
|
99
|
-
clear() {
|
|
100
|
-
threads = []
|
|
101
|
-
activeId = null
|
|
102
|
-
},
|
|
103
|
-
// Always-hydrated, never-erroring — mirrors the localStorage-backed store's real values
|
|
104
|
-
// (see ThreadsStore.hydrated/.error doc comments); this double is a synchronous in-memory
|
|
105
|
-
// stand-in with no async load path to fail.
|
|
106
|
-
hydrated: true,
|
|
107
|
-
error: undefined,
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
|
|
112
|
-
let resolve!: (value: T) => void
|
|
113
|
-
const promise = new Promise<T>((r) => {
|
|
114
|
-
resolve = r
|
|
115
|
-
})
|
|
116
|
-
return { promise, resolve }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const resolveOutcome: OutcomeResolver<AgentPart> = (): AgentOutcome => ({
|
|
120
|
-
title: 'title',
|
|
121
|
-
summary: 'summary',
|
|
122
|
-
status: 'done',
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
describe('useAgentThreadRuns — concurrency, no-op guard, finalize order, failure, retry', () => {
|
|
126
|
-
test('two threads stream fully independently', async () => {
|
|
127
|
-
const store = createTestThreadsStore()
|
|
128
|
-
const threadA = store.create()
|
|
129
|
-
const threadB = store.create()
|
|
130
|
-
|
|
131
|
-
const gates = new Map<string, ReturnType<typeof deferred<void>>>()
|
|
132
|
-
gates.set('input-a', deferred<void>())
|
|
133
|
-
gates.set('input-b', deferred<void>())
|
|
134
|
-
|
|
135
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
136
|
-
async *stream(input) {
|
|
137
|
-
await gates.get(input)?.promise
|
|
138
|
-
yield { id: 'p1', type: 'text', text: `done:${input}` }
|
|
139
|
-
},
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
143
|
-
|
|
144
|
-
act(() => {
|
|
145
|
-
result.current.start(threadA, 'input-a')
|
|
146
|
-
result.current.start(threadB, 'input-b')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
expect(result.current.runs.get(threadA)?.status).toBe('streaming')
|
|
150
|
-
expect(result.current.runs.get(threadB)?.status).toBe('streaming')
|
|
151
|
-
|
|
152
|
-
// Resolve A only — B must stay untouched. Resolving a promise is not itself a React update,
|
|
153
|
-
// so this stays outside act(); waitFor (imported from @testing-library/react) is act-wrapped
|
|
154
|
-
// internally and polls until the resulting state settles.
|
|
155
|
-
gates.get('input-a')?.resolve(undefined)
|
|
156
|
-
|
|
157
|
-
await waitFor(() => {
|
|
158
|
-
expect(store.threads.find((t) => t.id === threadA)?.status).toBe('done')
|
|
159
|
-
})
|
|
160
|
-
expect(store.threads.find((t) => t.id === threadB)?.status).toBe('streaming')
|
|
161
|
-
expect(result.current.runs.has(threadB)).toBe(true)
|
|
162
|
-
|
|
163
|
-
// Resolve B — it settles independently, unaffected by A having already finished.
|
|
164
|
-
gates.get('input-b')?.resolve(undefined)
|
|
165
|
-
|
|
166
|
-
await waitFor(() => {
|
|
167
|
-
expect(store.threads.find((t) => t.id === threadB)?.status).toBe('done')
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
test('start() on a busy thread is a no-op and does not overwrite the cached input', async () => {
|
|
172
|
-
const store = createTestThreadsStore()
|
|
173
|
-
const threadId = store.create()
|
|
174
|
-
|
|
175
|
-
const calls: string[] = []
|
|
176
|
-
const gate = deferred<void>()
|
|
177
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
178
|
-
async *stream(input) {
|
|
179
|
-
calls.push(input)
|
|
180
|
-
await gate.promise
|
|
181
|
-
yield { id: 'p1', type: 'text', text: 'ok' }
|
|
182
|
-
},
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
186
|
-
|
|
187
|
-
act(() => {
|
|
188
|
-
result.current.start(threadId, 'first')
|
|
189
|
-
})
|
|
190
|
-
expect(calls).toEqual(['first'])
|
|
191
|
-
|
|
192
|
-
// Busy: the SAME thread already has a controller. This must no-op — no second stream() call,
|
|
193
|
-
// and (critically) must NOT clobber the cached input retry() will replay.
|
|
194
|
-
act(() => {
|
|
195
|
-
result.current.start(threadId, 'second')
|
|
196
|
-
})
|
|
197
|
-
expect(calls).toEqual(['first'])
|
|
198
|
-
|
|
199
|
-
// Let the first (and only) turn finish, then retry() — it must replay 'first', never 'second'.
|
|
200
|
-
gate.resolve(undefined)
|
|
201
|
-
await waitFor(() => {
|
|
202
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
act(() => {
|
|
206
|
-
result.current.retry(threadId)
|
|
207
|
-
})
|
|
208
|
-
expect(calls).toEqual(['first', 'first'])
|
|
209
|
-
|
|
210
|
-
// Let the replayed run settle too, so no pending async work leaks past this test's act
|
|
211
|
-
// boundary into the next one.
|
|
212
|
-
await waitFor(() => {
|
|
213
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
214
|
-
})
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
test('finalize order on success: appendMessage → resolveOutcome → setOutcome → setStatus → setResumeToken', async () => {
|
|
218
|
-
const order: string[] = []
|
|
219
|
-
const store = createTestThreadsStore({ onCall: (method) => order.push(method) })
|
|
220
|
-
const threadId = store.create()
|
|
221
|
-
|
|
222
|
-
const gate = deferred<void>()
|
|
223
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
224
|
-
async *stream() {
|
|
225
|
-
await gate.promise
|
|
226
|
-
yield { id: 'p1', type: 'text', text: 'hi' }
|
|
227
|
-
},
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const orderedResolveOutcome: OutcomeResolver<AgentPart> = async (): Promise<AgentOutcome> => {
|
|
231
|
-
order.push('resolveOutcome')
|
|
232
|
-
return { title: 'title', summary: 'summary', status: 'done' }
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const { result } = renderHook(() =>
|
|
236
|
-
useAgentThreadRuns({ transport, store, resolveOutcome: orderedResolveOutcome }),
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
act(() => {
|
|
240
|
-
result.current.start(threadId, 'hi')
|
|
241
|
-
})
|
|
242
|
-
// start() itself already logged 'appendMessage' (user message) + 'setStatus' ('streaming') —
|
|
243
|
-
// baseline everything from here on.
|
|
244
|
-
const baseline = order.length
|
|
245
|
-
|
|
246
|
-
gate.resolve(undefined)
|
|
247
|
-
await waitFor(() => {
|
|
248
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
expect(order.slice(baseline)).toEqual([
|
|
252
|
-
'appendMessage',
|
|
253
|
-
'resolveOutcome',
|
|
254
|
-
'setOutcome',
|
|
255
|
-
'setStatus',
|
|
256
|
-
'setResumeToken',
|
|
257
|
-
])
|
|
258
|
-
|
|
259
|
-
// The controller/runs entry is torn down as the final step of the same finalize — proven
|
|
260
|
-
// indirectly: the run entry is gone, and a subsequent start() is accepted immediately (the
|
|
261
|
-
// busy guard would block it if the controller were still registered).
|
|
262
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
test('a thrown (non-abort) error sets the onFailureStatus for start() — "error"', async () => {
|
|
266
|
-
const store = createTestThreadsStore()
|
|
267
|
-
const threadId = store.create()
|
|
268
|
-
|
|
269
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
270
|
-
async *stream() {
|
|
271
|
-
throw new Error('boom')
|
|
272
|
-
// eslint-disable-next-line no-unreachable
|
|
273
|
-
yield { id: 'p1', type: 'text', text: 'unreachable' }
|
|
274
|
-
},
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
278
|
-
|
|
279
|
-
act(() => {
|
|
280
|
-
result.current.start(threadId, 'hi')
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
await waitFor(() => {
|
|
284
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('error')
|
|
285
|
-
})
|
|
286
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
test('retry() is a no-op when nothing has been cached for that thread', () => {
|
|
290
|
-
// No store.create() here on purpose: a freshly-created 'pending' thread is itself swept by
|
|
291
|
-
// the mount-reconcile effect (it treats any 'pending'/'streaming' thread with no live
|
|
292
|
-
// controller as orphaned — see use-agent-thread-runs.resume.test.tsx), which would confound
|
|
293
|
-
// this assertion. Using an id with NO entry in the store at all isolates retry()'s own
|
|
294
|
-
// "nothing cached" guard from that unrelated mount-time behavior.
|
|
295
|
-
const store = createTestThreadsStore()
|
|
296
|
-
const threadId = crypto.randomUUID()
|
|
297
|
-
|
|
298
|
-
const calls: string[] = []
|
|
299
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
300
|
-
async *stream(input) {
|
|
301
|
-
calls.push(input)
|
|
302
|
-
yield { id: 'p1', type: 'text', text: 'x' }
|
|
303
|
-
},
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
307
|
-
|
|
308
|
-
act(() => {
|
|
309
|
-
result.current.retry(threadId)
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
expect(calls).toEqual([])
|
|
313
|
-
expect(store.threads).toEqual([])
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
test('retry() re-invokes start() with the cached input and produces a genuine second run', async () => {
|
|
317
|
-
const store = createTestThreadsStore()
|
|
318
|
-
const threadId = store.create()
|
|
319
|
-
|
|
320
|
-
const calls: string[] = []
|
|
321
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
322
|
-
async *stream(input) {
|
|
323
|
-
calls.push(input)
|
|
324
|
-
yield { id: 'p1', type: 'text', text: `done:${input}` }
|
|
325
|
-
},
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
329
|
-
|
|
330
|
-
act(() => {
|
|
331
|
-
result.current.start(threadId, 'original input')
|
|
332
|
-
})
|
|
333
|
-
await waitFor(() => {
|
|
334
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
335
|
-
})
|
|
336
|
-
expect(calls).toEqual(['original input'])
|
|
337
|
-
|
|
338
|
-
act(() => {
|
|
339
|
-
result.current.retry(threadId)
|
|
340
|
-
})
|
|
341
|
-
// The positive path this test exists to pin: retry() actually calls start() again with the
|
|
342
|
-
// SAME cached input — not a no-op, and not a clobbered/different input.
|
|
343
|
-
expect(calls).toEqual(['original input', 'original input'])
|
|
344
|
-
// A genuine second run is now live — the run entry is back and streaming.
|
|
345
|
-
expect(result.current.runs.get(threadId)?.status).toBe('streaming')
|
|
346
|
-
|
|
347
|
-
await waitFor(() => {
|
|
348
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
349
|
-
})
|
|
350
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
351
|
-
// Two full turns landed in history: user+assistant, twice.
|
|
352
|
-
expect(store.threads.find((t) => t.id === threadId)?.messages).toHaveLength(4)
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
// Pins the fix for a race in consumeAndFinalize's success path: it awaits resolveOutcome and
|
|
356
|
-
// then writes setOutcome/setStatus, and now RE-CHECKS the supersede/abort guards after that
|
|
357
|
-
// await (not just before it) before performing those writes. Without the recheck, a slow
|
|
358
|
-
// resolveOutcome racing a stop() call would clobber stop()'s synchronous 'done' with whatever
|
|
359
|
-
// the outcome resolved to once it finally settled.
|
|
360
|
-
test('stop() during a slow resolveOutcome must not be clobbered by the outcome settling later', async () => {
|
|
361
|
-
const store = createTestThreadsStore()
|
|
362
|
-
const threadId = store.create()
|
|
363
|
-
|
|
364
|
-
let releaseOutcome: (() => void) | undefined
|
|
365
|
-
const slowResolveOutcome: OutcomeResolver<AgentPart> = () =>
|
|
366
|
-
new Promise<AgentOutcome>((resolve) => {
|
|
367
|
-
releaseOutcome = () => resolve({ title: 't', summary: 's', status: 'attention' })
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
371
|
-
async *stream() {
|
|
372
|
-
yield { id: 'p1', type: 'text', text: 'hi' }
|
|
373
|
-
},
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const { result } = renderHook(() =>
|
|
377
|
-
useAgentThreadRuns({ transport, store, resolveOutcome: slowResolveOutcome }),
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
act(() => {
|
|
381
|
-
result.current.start(threadId, 'hi')
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
// Wait until the assistant message has landed (proving resolveOutcome has been called and is
|
|
385
|
-
// now the pending await).
|
|
386
|
-
await waitFor(() => {
|
|
387
|
-
expect(store.threads.find((t) => t.id === threadId)?.messages.length).toBe(2)
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
act(() => {
|
|
391
|
-
result.current.stop(threadId)
|
|
392
|
-
})
|
|
393
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
394
|
-
|
|
395
|
-
releaseOutcome?.()
|
|
396
|
-
|
|
397
|
-
await waitFor(() => {
|
|
398
|
-
// The late resolveOutcome settling must not resurrect the thread past the 'done' stop()
|
|
399
|
-
// already committed.
|
|
400
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
401
|
-
})
|
|
402
|
-
})
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
describe('useAgentThreadRuns — stop()/stopAll() lifecycle', () => {
|
|
406
|
-
test('stop() mid-stream persists the accumulated parts as a finish: "stopped" assistant message', async () => {
|
|
407
|
-
const store = createTestThreadsStore()
|
|
408
|
-
const threadId = store.create()
|
|
409
|
-
|
|
410
|
-
const gate = deferred<void>()
|
|
411
|
-
// Yields id-less drafts on purpose — the assertion below exists specifically to prove
|
|
412
|
-
// withPartIds stamps the missing id as `${threadId}#0`, so the cast documents that the type
|
|
413
|
-
// says AgentPart (identified) while this transport deliberately produces what the real
|
|
414
|
-
// contract is AgentPartDraft (a transport is always permitted to omit id).
|
|
415
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
416
|
-
async *stream() {
|
|
417
|
-
yield { type: 'text', text: 'partial' } as AgentPartDraft as AgentPart
|
|
418
|
-
await gate.promise
|
|
419
|
-
yield { type: 'text', text: 'never arrives' } as AgentPartDraft as AgentPart
|
|
420
|
-
},
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
424
|
-
|
|
425
|
-
act(() => {
|
|
426
|
-
result.current.start(threadId, 'hi')
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
await waitFor(() => {
|
|
430
|
-
expect(result.current.runs.get(threadId)?.parts).toHaveLength(1)
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
act(() => {
|
|
434
|
-
result.current.stop(threadId)
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
const thread = store.threads.find((t) => t.id === threadId)
|
|
438
|
-
expect(thread?.status).toBe('done')
|
|
439
|
-
expect(thread?.messages).toHaveLength(2)
|
|
440
|
-
const assistantMessage = thread?.messages[1]
|
|
441
|
-
expect(assistantMessage?.role).toBe('assistant')
|
|
442
|
-
expect(assistantMessage?.finish).toBe('stopped')
|
|
443
|
-
// withPartIds stamps the missing id as `${threadId}#0` — the first (and only) id-less part
|
|
444
|
-
// this run's generator yielded.
|
|
445
|
-
expect(assistantMessage?.parts).toEqual([
|
|
446
|
-
{ id: `${threadId}#0`, type: 'text', text: 'partial' },
|
|
447
|
-
])
|
|
448
|
-
// The run entry is torn down synchronously — no lingering 'streaming' indicator.
|
|
449
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
450
|
-
|
|
451
|
-
// Release the still-suspended (aborted, guard-caught) generator so it doesn't leak into the
|
|
452
|
-
// next test.
|
|
453
|
-
gate.resolve(undefined)
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
test('stop() does not double-append when the stream already fully arrived before stop() was called', async () => {
|
|
457
|
-
const store = createTestThreadsStore()
|
|
458
|
-
const threadId = store.create()
|
|
459
|
-
|
|
460
|
-
let releaseOutcome: (() => void) | undefined
|
|
461
|
-
const slowResolveOutcome: OutcomeResolver<AgentPart> = () =>
|
|
462
|
-
new Promise<AgentOutcome>((resolve) => {
|
|
463
|
-
releaseOutcome = () => resolve({ title: 't', summary: 's', status: 'done' })
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
467
|
-
async *stream() {
|
|
468
|
-
yield { id: 'p1', type: 'text', text: 'hi' }
|
|
469
|
-
},
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const { result } = renderHook(() =>
|
|
473
|
-
useAgentThreadRuns({ transport, store, resolveOutcome: slowResolveOutcome }),
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
act(() => {
|
|
477
|
-
result.current.start(threadId, 'hi')
|
|
478
|
-
})
|
|
479
|
-
|
|
480
|
-
// Wait until consumeAndFinalize's own appendMessage has already landed (it happens BEFORE
|
|
481
|
-
// its await on resolveOutcome) — this is the narrow window stop() can race without ever
|
|
482
|
-
// being able to observe "not yet appended" (see finalizeStop's doc comment).
|
|
483
|
-
await waitFor(() => {
|
|
484
|
-
expect(store.threads.find((t) => t.id === threadId)?.messages.length).toBe(2)
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
act(() => {
|
|
488
|
-
result.current.stop(threadId)
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
// Still exactly 2 messages (user + the ORIGINAL 'complete' assistant message) — stop() must
|
|
492
|
-
// recognize the terminal message already landed and skip its own append rather than
|
|
493
|
-
// duplicating it as a second 'stopped' message.
|
|
494
|
-
const thread = store.threads.find((t) => t.id === threadId)
|
|
495
|
-
expect(thread?.messages).toHaveLength(2)
|
|
496
|
-
expect(thread?.messages[1]?.finish).toBe('complete')
|
|
497
|
-
|
|
498
|
-
releaseOutcome?.()
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
test('stop() is a true no-op when threadId has no in-flight run', () => {
|
|
502
|
-
const store = createTestThreadsStore()
|
|
503
|
-
// No store.create() here on purpose — mirrors the "retry() is a no-op" test above: isolates
|
|
504
|
-
// stop()'s own "nothing in flight" guard from the mount-reconcile effect's unrelated handling
|
|
505
|
-
// of a freshly-created 'pending' thread.
|
|
506
|
-
const threadId = crypto.randomUUID()
|
|
507
|
-
|
|
508
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
509
|
-
async *stream() {
|
|
510
|
-
yield { id: 'p1', type: 'text', text: 'x' }
|
|
511
|
-
},
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
515
|
-
|
|
516
|
-
act(() => {
|
|
517
|
-
result.current.stop(threadId)
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
expect(store.threads).toEqual([])
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
// Pins the fix for the reference-equality hole: runsRef mirrors `runs` only as of the last
|
|
524
|
-
// FLUSHED render (assigned during render, `:~355`), so a stop() landing inside the narrow
|
|
525
|
-
// window between consumeAndFinalize's post-loop appendMessage and its `await resolveOutcome`
|
|
526
|
-
// resolving could observe a STALE parts array (one delta behind) via runsRef — defeating the
|
|
527
|
-
// old `lastMessage.parts === parts` guard and double-appending. This test lands stop()
|
|
528
|
-
// deterministically inside that exact window (fired synchronously from inside the injected
|
|
529
|
-
// resolveOutcome, itself called synchronously right after appendMessage, before any of the
|
|
530
|
-
// hook's pending state updates can flush — flushing needs a macrotask, and nothing here crosses
|
|
531
|
-
// one) rather than relying on timing luck, so it is a reliable regression guard for the fixed
|
|
532
|
-
// marker-based mechanism, not just a one-off repro.
|
|
533
|
-
test("stop() fired synchronously from inside resolveOutcome (before the final delta's render flushes) must not double-append", async () => {
|
|
534
|
-
const store = createTestThreadsStore()
|
|
535
|
-
const threadId = store.create()
|
|
536
|
-
|
|
537
|
-
const gate = deferred<void>()
|
|
538
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
539
|
-
async *stream() {
|
|
540
|
-
// Distinct ids: mergePart addresses by id, and two parts sharing the (undefined) id would
|
|
541
|
-
// splice into ONE entry instead of accumulating two — this test needs the SECOND delta to
|
|
542
|
-
// produce a genuinely NEW array entry (and therefore a new `parts` array reference) to
|
|
543
|
-
// exercise the race at all.
|
|
544
|
-
yield { id: 'p1', type: 'text', text: 'first' }
|
|
545
|
-
await gate.promise
|
|
546
|
-
yield { id: 'p2', type: 'text', text: 'second' }
|
|
547
|
-
},
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const hookRef: { stop?: (id: string) => void } = {}
|
|
551
|
-
let resolveCalls = 0
|
|
552
|
-
const raceResolveOutcome: OutcomeResolver<AgentPart> = async () => {
|
|
553
|
-
resolveCalls += 1
|
|
554
|
-
// Fire stop() from inside the FIRST resolveOutcome call — synchronously, before this
|
|
555
|
-
// function even returns its promise. Everything from the second delta's setRuns dispatch to
|
|
556
|
-
// this point runs purely through microtasks (async-generator machinery + this direct call),
|
|
557
|
-
// so no macrotask has elapsed and React's scheduler cannot have flushed that dispatch into
|
|
558
|
-
// runsRef yet — this reliably lands stop() inside the race window.
|
|
559
|
-
if (resolveCalls === 1) {
|
|
560
|
-
hookRef.stop?.(threadId)
|
|
561
|
-
}
|
|
562
|
-
return { title: 't', summary: 's', status: 'done' }
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const { result } = renderHook(() =>
|
|
566
|
-
useAgentThreadRuns({ transport, store, resolveOutcome: raceResolveOutcome }),
|
|
567
|
-
)
|
|
568
|
-
hookRef.stop = result.current.stop
|
|
569
|
-
|
|
570
|
-
act(() => {
|
|
571
|
-
result.current.start(threadId, 'hi')
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
// Let the FIRST delta's render flush — runsRef now mirrors a non-empty parts array
|
|
575
|
-
// (['first']), which is what makes the eventual mismatch observable rather than vacuously
|
|
576
|
-
// guarded by parts.length === 0.
|
|
577
|
-
await waitFor(() => {
|
|
578
|
-
expect(result.current.runs.get(threadId)?.parts).toHaveLength(1)
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
// Resolve the gate outside act() on purpose: this must not synchronously flush React state —
|
|
582
|
-
// the whole point is to let the second delta, the loop exit, and appendMessage run purely as
|
|
583
|
-
// microtask continuations, with stop() firing from inside resolveOutcome before any render for
|
|
584
|
-
// the second delta occurs.
|
|
585
|
-
gate.resolve(undefined)
|
|
586
|
-
|
|
587
|
-
await waitFor(() => {
|
|
588
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
const thread = store.threads.find((t) => t.id === threadId)
|
|
592
|
-
const assistantMessages = thread?.messages.filter((m) => m.role === 'assistant') ?? []
|
|
593
|
-
expect(assistantMessages).toHaveLength(1)
|
|
594
|
-
// The surviving message must be the ORIGINAL 'complete' one with the full (['first','second'])
|
|
595
|
-
// content — not a truncated 'stopped' message built from the stale ['first']-only parts.
|
|
596
|
-
expect(assistantMessages[0]?.finish).toBe('complete')
|
|
597
|
-
expect(assistantMessages[0]?.parts).toEqual([
|
|
598
|
-
{ id: 'p1', type: 'text', text: 'first' },
|
|
599
|
-
{ id: 'p2', type: 'text', text: 'second' },
|
|
600
|
-
])
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
// Pins the flip side of the marker fix: the marker must be cleared once a run concludes, or a
|
|
604
|
-
// stale entry from an EARLIER finished run on the same thread would wrongly suppress a
|
|
605
|
-
// legitimate append on a LATER run — a worse bug than the double-append this mechanism exists to
|
|
606
|
-
// prevent.
|
|
607
|
-
test('a stale marker from an earlier finished run must not suppress a legitimate later stop() append', async () => {
|
|
608
|
-
const store = createTestThreadsStore()
|
|
609
|
-
const threadId = store.create()
|
|
610
|
-
|
|
611
|
-
const inputs: string[] = []
|
|
612
|
-
let gate = deferred<void>()
|
|
613
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
614
|
-
async *stream(input) {
|
|
615
|
-
inputs.push(input)
|
|
616
|
-
// Distinct ids: two id-less parts sharing the (undefined) id would silently splice into
|
|
617
|
-
// ONE entry (the F1 bug) — turn 1's own "did these two deltas stay separate?" assertion
|
|
618
|
-
// below is invisible otherwise, since it only checked assistant MESSAGE count, not parts.
|
|
619
|
-
yield { id: `${input}-partial`, type: 'text', text: `${input}-partial` }
|
|
620
|
-
await gate.promise
|
|
621
|
-
yield { id: `${input}-final`, type: 'text', text: `${input}-final` }
|
|
622
|
-
},
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
626
|
-
|
|
627
|
-
// Turn 1: runs to full completion (no stop()) so consumeAndFinalize's own success path sets
|
|
628
|
-
// and then clears the marker for this thread.
|
|
629
|
-
act(() => {
|
|
630
|
-
result.current.start(threadId, 'first-turn')
|
|
631
|
-
})
|
|
632
|
-
gate.resolve(undefined)
|
|
633
|
-
await waitFor(() => {
|
|
634
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
635
|
-
})
|
|
636
|
-
const turn1Assistant = store.threads
|
|
637
|
-
.find((t) => t.id === threadId)
|
|
638
|
-
?.messages.filter((m) => m.role === 'assistant')
|
|
639
|
-
expect(turn1Assistant).toHaveLength(1)
|
|
640
|
-
// The two deltas landed as two SEPARATE parts, not spliced into one.
|
|
641
|
-
expect(turn1Assistant?.[0]?.parts).toEqual([
|
|
642
|
-
{ id: 'first-turn-partial', type: 'text', text: 'first-turn-partial' },
|
|
643
|
-
{ id: 'first-turn-final', type: 'text', text: 'first-turn-final' },
|
|
644
|
-
])
|
|
645
|
-
|
|
646
|
-
// Turn 2: a genuinely new run on the SAME thread, stopped mid-stream. If a stale marker from
|
|
647
|
-
// turn 1 leaked, this stop() would be wrongly treated as "already persisted" and silently drop
|
|
648
|
-
// the partial content instead of appending it.
|
|
649
|
-
gate = deferred<void>()
|
|
650
|
-
act(() => {
|
|
651
|
-
result.current.retry(threadId)
|
|
652
|
-
})
|
|
653
|
-
expect(inputs).toEqual(['first-turn', 'first-turn'])
|
|
654
|
-
|
|
655
|
-
await waitFor(() => {
|
|
656
|
-
expect(result.current.runs.get(threadId)?.parts).toHaveLength(1)
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
act(() => {
|
|
660
|
-
result.current.stop(threadId)
|
|
661
|
-
})
|
|
662
|
-
|
|
663
|
-
const thread = store.threads.find((t) => t.id === threadId)
|
|
664
|
-
const assistantMessages = thread?.messages.filter((m) => m.role === 'assistant') ?? []
|
|
665
|
-
expect(assistantMessages).toHaveLength(2)
|
|
666
|
-
expect(assistantMessages[1]?.finish).toBe('stopped')
|
|
667
|
-
expect(assistantMessages[1]?.parts).toEqual([
|
|
668
|
-
{ id: 'first-turn-partial', type: 'text', text: 'first-turn-partial' },
|
|
669
|
-
])
|
|
670
|
-
|
|
671
|
-
gate.resolve(undefined)
|
|
672
|
-
})
|
|
673
|
-
|
|
674
|
-
test('stopAll() resolves every aborted thread instead of stranding them in "streaming"', async () => {
|
|
675
|
-
const store = createTestThreadsStore()
|
|
676
|
-
const threadA = store.create()
|
|
677
|
-
const threadB = store.create()
|
|
678
|
-
|
|
679
|
-
const gateA = deferred<void>()
|
|
680
|
-
const gateB = deferred<void>()
|
|
681
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
682
|
-
async *stream(input) {
|
|
683
|
-
if (input === 'a') {
|
|
684
|
-
yield { id: 'p1', type: 'text', text: 'partial-a' }
|
|
685
|
-
await gateA.promise
|
|
686
|
-
} else {
|
|
687
|
-
yield { id: 'p1', type: 'text', text: 'partial-b' }
|
|
688
|
-
await gateB.promise
|
|
689
|
-
}
|
|
690
|
-
},
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
694
|
-
|
|
695
|
-
act(() => {
|
|
696
|
-
result.current.start(threadA, 'a')
|
|
697
|
-
result.current.start(threadB, 'b')
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
await waitFor(() => {
|
|
701
|
-
expect(result.current.runs.get(threadA)?.parts).toHaveLength(1)
|
|
702
|
-
expect(result.current.runs.get(threadB)?.parts).toHaveLength(1)
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
act(() => {
|
|
706
|
-
result.current.stopAll()
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
expect(store.threads.find((t) => t.id === threadA)?.status).toBe('done')
|
|
710
|
-
expect(store.threads.find((t) => t.id === threadB)?.status).toBe('done')
|
|
711
|
-
expect(result.current.runs.size).toBe(0)
|
|
712
|
-
|
|
713
|
-
// Release the still-suspended (aborted, guard-caught) generators so they don't leak into the
|
|
714
|
-
// next test.
|
|
715
|
-
gateA.resolve(undefined)
|
|
716
|
-
gateB.resolve(undefined)
|
|
717
|
-
})
|
|
718
|
-
|
|
719
|
-
// F4 regression: stop()'s no-op guard used to read runsRef, which only mirrors `runs` as of the
|
|
720
|
-
// last FLUSHED RENDER. start() then stop() called in the SAME synchronous act() batch — e.g.
|
|
721
|
-
// `onClick={() => { start(id, text); stop(id) }}` — never lets a render land in between, so
|
|
722
|
-
// runsRef was still empty when stop() ran and it wrongly no-op'd: the controller was never
|
|
723
|
-
// aborted, and a transport that only resolves on its AbortSignal firing would strand the thread
|
|
724
|
-
// in 'streaming' forever (this is exactly what `stopAll()`'s own regression test above cannot
|
|
725
|
-
// see — it deliberately `waitFor`s a render to flush before calling stopAll()).
|
|
726
|
-
test('F4 regression: start() then stop() in the same synchronous tick must actually abort, not strand the thread in "streaming"', async () => {
|
|
727
|
-
const store = createTestThreadsStore()
|
|
728
|
-
const threadId = store.create()
|
|
729
|
-
|
|
730
|
-
// Hangs until its AbortSignal fires — never resolves on its own. If stop()'s no-op guard skips
|
|
731
|
-
// the abort, this generator never settles and the thread is stuck 'streaming' forever.
|
|
732
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
733
|
-
// oxlint-disable-next-line require-yield -- hangs until aborted; never has anything to yield
|
|
734
|
-
async *stream(_input, signal) {
|
|
735
|
-
await new Promise((resolve) => {
|
|
736
|
-
signal?.addEventListener('abort', () => resolve(undefined))
|
|
737
|
-
})
|
|
738
|
-
},
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
742
|
-
|
|
743
|
-
act(() => {
|
|
744
|
-
result.current.start(threadId, 'hi')
|
|
745
|
-
result.current.stop(threadId)
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
await waitFor(() => {
|
|
749
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
750
|
-
})
|
|
751
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
752
|
-
})
|
|
753
|
-
})
|
|
754
|
-
|
|
755
|
-
describe('useAgentThreadRuns — F1 regression: id-less drafts within one run', () => {
|
|
756
|
-
test('drafts of different types do not destroy each other — mergePart no longer sees undefined === undefined', async () => {
|
|
757
|
-
const store = createTestThreadsStore()
|
|
758
|
-
const threadId = store.create()
|
|
759
|
-
|
|
760
|
-
// Yields id-less drafts on purpose — the whole point of this regression is that TWO drafts
|
|
761
|
-
// (of different types, so a real bug wouldn't be "same id" but "no id at all") must not
|
|
762
|
-
// collide via `undefined === undefined`. The cast documents that the type says AgentPart
|
|
763
|
-
// (identified) while a transport is always permitted to omit id (AgentPartDraft).
|
|
764
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
765
|
-
async *stream() {
|
|
766
|
-
yield { type: 'text', text: 'Hi' } as AgentPartDraft as AgentPart
|
|
767
|
-
yield {
|
|
768
|
-
type: 'tool',
|
|
769
|
-
toolCallId: 'c1',
|
|
770
|
-
toolName: 'search',
|
|
771
|
-
state: 'input-available',
|
|
772
|
-
input: { q: 'x' },
|
|
773
|
-
} as AgentPartDraft as AgentPart
|
|
774
|
-
yield { type: 'text', text: 'Bye' } as AgentPartDraft as AgentPart
|
|
775
|
-
},
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
779
|
-
|
|
780
|
-
act(() => {
|
|
781
|
-
result.current.start(threadId, 'go')
|
|
782
|
-
})
|
|
783
|
-
|
|
784
|
-
await waitFor(() => {
|
|
785
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('done')
|
|
786
|
-
})
|
|
787
|
-
|
|
788
|
-
const assistantMessage = store.threads
|
|
789
|
-
.find((t) => t.id === threadId)
|
|
790
|
-
?.messages.find((m) => m.role === 'assistant')
|
|
791
|
-
expect(assistantMessage?.parts).toHaveLength(3)
|
|
792
|
-
expect(assistantMessage?.parts.map((part) => part.type)).toEqual(['text', 'tool', 'text'])
|
|
793
|
-
})
|
|
794
|
-
})
|
|
795
|
-
|
|
796
|
-
describe('useAgentThreadRuns — message-id minting under degraded crypto', () => {
|
|
797
|
-
test('start() mints distinct message ids via crypto.getRandomValues when randomUUID is unavailable', async () => {
|
|
798
|
-
const store = createTestThreadsStore()
|
|
799
|
-
const threadA = store.create()
|
|
800
|
-
const threadB = store.create()
|
|
801
|
-
|
|
802
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
803
|
-
async *stream(input) {
|
|
804
|
-
yield { id: 'p1', type: 'text', text: `done:${input}` }
|
|
805
|
-
},
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
const originalCrypto = globalThis.crypto
|
|
809
|
-
Object.defineProperty(globalThis, 'crypto', {
|
|
810
|
-
value: { getRandomValues: originalCrypto.getRandomValues.bind(originalCrypto) },
|
|
811
|
-
configurable: true,
|
|
812
|
-
})
|
|
813
|
-
try {
|
|
814
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
815
|
-
|
|
816
|
-
act(() => {
|
|
817
|
-
result.current.start(threadA, 'input-a')
|
|
818
|
-
result.current.start(threadB, 'input-b')
|
|
819
|
-
})
|
|
820
|
-
|
|
821
|
-
await waitFor(() => {
|
|
822
|
-
expect(store.threads.find((t) => t.id === threadA)?.status).toBe('done')
|
|
823
|
-
expect(store.threads.find((t) => t.id === threadB)?.status).toBe('done')
|
|
824
|
-
})
|
|
825
|
-
|
|
826
|
-
const allMessageIds = store.threads.flatMap((t) => t.messages.map((m) => m.id))
|
|
827
|
-
expect(allMessageIds.length).toBeGreaterThan(0)
|
|
828
|
-
expect(new Set(allMessageIds).size).toBe(allMessageIds.length)
|
|
829
|
-
} finally {
|
|
830
|
-
Object.defineProperty(globalThis, 'crypto', { value: originalCrypto, configurable: true })
|
|
831
|
-
}
|
|
832
|
-
})
|
|
833
|
-
|
|
834
|
-
test('start() THROWS on a host with no usable crypto at all, rather than silently minting a colliding message id', () => {
|
|
835
|
-
const store = createTestThreadsStore()
|
|
836
|
-
const threadId = store.create()
|
|
837
|
-
|
|
838
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
839
|
-
async *stream() {
|
|
840
|
-
yield { id: 'p1', type: 'text', text: 'unreachable' }
|
|
841
|
-
},
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
845
|
-
|
|
846
|
-
const originalCrypto = globalThis.crypto
|
|
847
|
-
Object.defineProperty(globalThis, 'crypto', { value: undefined, configurable: true })
|
|
848
|
-
try {
|
|
849
|
-
expect(() => {
|
|
850
|
-
act(() => {
|
|
851
|
-
result.current.start(threadId, 'hi')
|
|
852
|
-
})
|
|
853
|
-
}).toThrow(/idempotency key/)
|
|
854
|
-
} finally {
|
|
855
|
-
Object.defineProperty(globalThis, 'crypto', { value: originalCrypto, configurable: true })
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// The deliberate divergence documented on mintMessageId: throwing means the write never
|
|
859
|
-
// happened at all (no half-appended message, no orphaned run entry) rather than silently
|
|
860
|
-
// dropping content behind a colliding id.
|
|
861
|
-
expect(store.threads.find((t) => t.id === threadId)?.messages).toHaveLength(0)
|
|
862
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
863
|
-
})
|
|
864
|
-
})
|
|
865
|
-
|
|
866
|
-
// A ThreadsStore double that delegates everything to a real test store EXCEPT appendMessage,
|
|
867
|
-
// which throws for messages matching `shouldThrow` — lets a test simulate consumer-adapter code
|
|
868
|
-
// (appendMessage) throwing without touching the shared createTestThreadsStore factory used by
|
|
869
|
-
// every other test in this file.
|
|
870
|
-
function wrapStoreWithThrowingAppend(
|
|
871
|
-
store: ThreadsStore<AgentPart>,
|
|
872
|
-
shouldThrow: (message: AgentThread<AgentPart>['messages'][number]) => boolean,
|
|
873
|
-
): ThreadsStore<AgentPart> {
|
|
874
|
-
return {
|
|
875
|
-
get threads() {
|
|
876
|
-
return store.threads
|
|
877
|
-
},
|
|
878
|
-
get activeId() {
|
|
879
|
-
return store.activeId
|
|
880
|
-
},
|
|
881
|
-
select: store.select,
|
|
882
|
-
create: store.create,
|
|
883
|
-
appendMessage(id, message) {
|
|
884
|
-
if (shouldThrow(message)) {
|
|
885
|
-
throw new Error('consumer appendMessage boom')
|
|
886
|
-
}
|
|
887
|
-
store.appendMessage(id, message)
|
|
888
|
-
},
|
|
889
|
-
setOutcome: store.setOutcome,
|
|
890
|
-
setStatus: store.setStatus,
|
|
891
|
-
setResumeToken: store.setResumeToken,
|
|
892
|
-
markRead: store.markRead,
|
|
893
|
-
remove: store.remove,
|
|
894
|
-
clear: store.clear,
|
|
895
|
-
hydrated: store.hydrated,
|
|
896
|
-
error: store.error,
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/** As above, but for the settle half of `finalizeStop` — a consumer store whose `setStatus` throws
|
|
901
|
-
* (a remote-backed adapter rejecting a thread removed mid-stream is the realistic shape). Narrowed
|
|
902
|
-
* by `shouldThrow` because the hook ALSO calls `setStatus` from its mount-time orphan sweep, which
|
|
903
|
-
* is a different call site with a different (non-wedge) failure mode. */
|
|
904
|
-
function wrapStoreWithThrowingSetStatus(
|
|
905
|
-
store: ThreadsStore<AgentPart>,
|
|
906
|
-
shouldThrow: (status: AgentThread<AgentPart>['status']) => boolean,
|
|
907
|
-
): ThreadsStore<AgentPart> {
|
|
908
|
-
return {
|
|
909
|
-
get threads() {
|
|
910
|
-
return store.threads
|
|
911
|
-
},
|
|
912
|
-
get activeId() {
|
|
913
|
-
return store.activeId
|
|
914
|
-
},
|
|
915
|
-
select: store.select,
|
|
916
|
-
create: store.create,
|
|
917
|
-
appendMessage: store.appendMessage,
|
|
918
|
-
setOutcome: store.setOutcome,
|
|
919
|
-
setStatus(id, status) {
|
|
920
|
-
if (shouldThrow(status)) throw new Error('consumer setStatus boom')
|
|
921
|
-
store.setStatus(id, status)
|
|
922
|
-
},
|
|
923
|
-
setResumeToken: store.setResumeToken,
|
|
924
|
-
markRead: store.markRead,
|
|
925
|
-
remove: store.remove,
|
|
926
|
-
clear: store.clear,
|
|
927
|
-
hydrated: store.hydrated,
|
|
928
|
-
error: store.error,
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
describe('useAgentThreadRuns — finalizeStop must reach a terminal state no matter what', () => {
|
|
933
|
-
test('stop() on a host with no usable crypto still reaches a terminal status and clears the run entry (was: wedged at "streaming" forever)', async () => {
|
|
934
|
-
const store = createTestThreadsStore()
|
|
935
|
-
const threadId = store.create()
|
|
936
|
-
|
|
937
|
-
const gate = deferred<void>()
|
|
938
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
939
|
-
async *stream() {
|
|
940
|
-
yield { id: 'p1', type: 'text', text: 'partial' }
|
|
941
|
-
await gate.promise
|
|
942
|
-
yield { id: 'p2', type: 'text', text: 'never arrives' }
|
|
943
|
-
},
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
947
|
-
|
|
948
|
-
act(() => {
|
|
949
|
-
result.current.start(threadId, 'hi')
|
|
950
|
-
})
|
|
951
|
-
|
|
952
|
-
await waitFor(() => {
|
|
953
|
-
expect(result.current.runs.get(threadId)?.parts).toHaveLength(1)
|
|
954
|
-
})
|
|
955
|
-
|
|
956
|
-
const originalCrypto = globalThis.crypto
|
|
957
|
-
Object.defineProperty(globalThis, 'crypto', { value: undefined, configurable: true })
|
|
958
|
-
try {
|
|
959
|
-
act(() => {
|
|
960
|
-
result.current.stop(threadId)
|
|
961
|
-
})
|
|
962
|
-
|
|
963
|
-
const thread = store.threads.find((t) => t.id === threadId)
|
|
964
|
-
// Before the fix: mintMessageId's throw escaped finalizeStop entirely, so setStatus/setRuns
|
|
965
|
-
// never ran and this stayed 'streaming' forever.
|
|
966
|
-
expect(thread?.status).toBe('error')
|
|
967
|
-
// The stopped message never minted an id, so appendMessage was never even attempted —
|
|
968
|
-
// only the original user message is present.
|
|
969
|
-
expect(thread?.messages).toHaveLength(1)
|
|
970
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
971
|
-
|
|
972
|
-
// A second stop() must not be the user's only recourse — and it isn't: controllersRef
|
|
973
|
-
// already had this threadId deleted before finalizeStop ever ran, so this is an ordinary
|
|
974
|
-
// no-op, not a retry of the failed append.
|
|
975
|
-
act(() => {
|
|
976
|
-
result.current.stop(threadId)
|
|
977
|
-
})
|
|
978
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('error')
|
|
979
|
-
} finally {
|
|
980
|
-
Object.defineProperty(globalThis, 'crypto', { value: originalCrypto, configurable: true })
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
gate.resolve(undefined)
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
test("stop() reaches a terminal status and clears the run entry even when the consumer's appendMessage throws (was: wedged at 'streaming' forever)", async () => {
|
|
987
|
-
const baseStore = createTestThreadsStore()
|
|
988
|
-
const threadId = baseStore.create()
|
|
989
|
-
const store = wrapStoreWithThrowingAppend(
|
|
990
|
-
baseStore,
|
|
991
|
-
(message) => message.role === 'assistant' && message.finish === 'stopped',
|
|
992
|
-
)
|
|
993
|
-
|
|
994
|
-
const gate = deferred<void>()
|
|
995
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
996
|
-
async *stream() {
|
|
997
|
-
yield { id: 'p1', type: 'text', text: 'partial' }
|
|
998
|
-
await gate.promise
|
|
999
|
-
yield { id: 'p2', type: 'text', text: 'never arrives' }
|
|
1000
|
-
},
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
1004
|
-
|
|
1005
|
-
act(() => {
|
|
1006
|
-
result.current.start(threadId, 'hi')
|
|
1007
|
-
})
|
|
1008
|
-
|
|
1009
|
-
await waitFor(() => {
|
|
1010
|
-
expect(result.current.runs.get(threadId)?.parts).toHaveLength(1)
|
|
1011
|
-
})
|
|
1012
|
-
|
|
1013
|
-
act(() => {
|
|
1014
|
-
result.current.stop(threadId)
|
|
1015
|
-
})
|
|
1016
|
-
|
|
1017
|
-
const thread = store.threads.find((t) => t.id === threadId)
|
|
1018
|
-
expect(thread?.status).toBe('error')
|
|
1019
|
-
// Only the user message — the consumer's appendMessage rejected the stopped message.
|
|
1020
|
-
expect(thread?.messages).toHaveLength(1)
|
|
1021
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
1022
|
-
|
|
1023
|
-
// A second stop() remains a true no-op — not the user's only recourse.
|
|
1024
|
-
act(() => {
|
|
1025
|
-
result.current.stop(threadId)
|
|
1026
|
-
})
|
|
1027
|
-
expect(store.threads.find((t) => t.id === threadId)?.status).toBe('error')
|
|
1028
|
-
|
|
1029
|
-
gate.resolve(undefined)
|
|
1030
|
-
})
|
|
1031
|
-
|
|
1032
|
-
test("stop() clears the hook's own run entry even when the consumer's setStatus throws (the second half of the same wedge)", async () => {
|
|
1033
|
-
const baseStore = createTestThreadsStore()
|
|
1034
|
-
const threadId = baseStore.create()
|
|
1035
|
-
const store = wrapStoreWithThrowingSetStatus(baseStore, (status) => status === 'done')
|
|
1036
|
-
|
|
1037
|
-
const gate = deferred<void>()
|
|
1038
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
1039
|
-
async *stream() {
|
|
1040
|
-
yield { id: 'p1', type: 'text', text: 'partial' }
|
|
1041
|
-
await gate.promise
|
|
1042
|
-
yield { id: 'p2', type: 'text', text: 'never arrives' }
|
|
1043
|
-
},
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
1047
|
-
|
|
1048
|
-
act(() => {
|
|
1049
|
-
result.current.start(threadId, 'hi')
|
|
1050
|
-
})
|
|
1051
|
-
|
|
1052
|
-
await waitFor(() => {
|
|
1053
|
-
expect(result.current.runs.get(threadId)?.parts).toHaveLength(1)
|
|
1054
|
-
})
|
|
1055
|
-
|
|
1056
|
-
act(() => {
|
|
1057
|
-
result.current.stop(threadId)
|
|
1058
|
-
})
|
|
1059
|
-
|
|
1060
|
-
// The store's own status is whatever the throwing adapter left it as — unrecoverable from
|
|
1061
|
-
// here. What IS this hook's to guarantee is that `runs` no longer reports an in-flight turn:
|
|
1062
|
-
// that entry is the hook's own state and nothing later can ever clear it (stop() already
|
|
1063
|
-
// deleted the thread from controllersRef), so a throw above the teardown stranded it forever.
|
|
1064
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
1065
|
-
// The partial content still landed — the append runs before the settle.
|
|
1066
|
-
expect(store.threads.find((t) => t.id === threadId)?.messages).toHaveLength(2)
|
|
1067
|
-
|
|
1068
|
-
act(() => {
|
|
1069
|
-
result.current.stop(threadId)
|
|
1070
|
-
})
|
|
1071
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
1072
|
-
|
|
1073
|
-
gate.resolve(undefined)
|
|
1074
|
-
})
|
|
1075
|
-
|
|
1076
|
-
test('stop() still succeeds via crypto.getRandomValues when randomUUID is unavailable (rung 2 — the rung that matters in practice)', async () => {
|
|
1077
|
-
const store = createTestThreadsStore()
|
|
1078
|
-
const threadId = store.create()
|
|
1079
|
-
|
|
1080
|
-
const gate = deferred<void>()
|
|
1081
|
-
const transport: AgentTransport<AgentPart, string> = {
|
|
1082
|
-
async *stream() {
|
|
1083
|
-
yield { id: 'p1', type: 'text', text: 'partial' }
|
|
1084
|
-
await gate.promise
|
|
1085
|
-
yield { id: 'p2', type: 'text', text: 'never arrives' }
|
|
1086
|
-
},
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const originalCrypto = globalThis.crypto
|
|
1090
|
-
Object.defineProperty(globalThis, 'crypto', {
|
|
1091
|
-
value: { getRandomValues: originalCrypto.getRandomValues.bind(originalCrypto) },
|
|
1092
|
-
configurable: true,
|
|
1093
|
-
})
|
|
1094
|
-
try {
|
|
1095
|
-
const { result } = renderHook(() => useAgentThreadRuns({ transport, store, resolveOutcome }))
|
|
1096
|
-
|
|
1097
|
-
act(() => {
|
|
1098
|
-
result.current.start(threadId, 'hi')
|
|
1099
|
-
})
|
|
1100
|
-
|
|
1101
|
-
await waitFor(() => {
|
|
1102
|
-
expect(result.current.runs.get(threadId)?.parts).toHaveLength(1)
|
|
1103
|
-
})
|
|
1104
|
-
|
|
1105
|
-
act(() => {
|
|
1106
|
-
result.current.stop(threadId)
|
|
1107
|
-
})
|
|
1108
|
-
|
|
1109
|
-
const thread = store.threads.find((t) => t.id === threadId)
|
|
1110
|
-
expect(thread?.status).toBe('done')
|
|
1111
|
-
expect(thread?.messages).toHaveLength(2)
|
|
1112
|
-
expect(thread?.messages[1]?.finish).toBe('stopped')
|
|
1113
|
-
expect(result.current.runs.has(threadId)).toBe(false)
|
|
1114
|
-
} finally {
|
|
1115
|
-
Object.defineProperty(globalThis, 'crypto', { value: originalCrypto, configurable: true })
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
gate.resolve(undefined)
|
|
1119
|
-
})
|
|
1120
|
-
})
|