basalt-ui 1.11.0 → 1.13.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 +21 -21
- package/CHANGELOG.md +32 -0
- package/README.md +63 -51
- package/agent/rules/basalt-agent.md +14 -0
- package/agent/rules/basalt-content.md +16 -3
- package/configs/oxlint-plugin.js +7 -3
- package/configs/oxlint-plugin.test.ts +34 -5
- package/configs/oxlint.json +4 -4
- package/dist/agent/adapter.d.ts +150 -0
- package/dist/agent/adapter.d.ts.map +1 -0
- package/dist/agent/adapter.js +482 -0
- package/dist/agent/adapter.test.d.ts +2 -0
- package/dist/agent/adapter.test.d.ts.map +1 -0
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +6 -2
- package/dist/agent/ai-sdk-transport.test.d.ts +2 -0
- package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
- package/dist/agent/coalesce.d.ts +20 -4
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +76 -8
- package/dist/agent/coalesce.test.d.ts +2 -0
- package/dist/agent/coalesce.test.d.ts.map +1 -0
- package/dist/agent/foreign.test.d.ts +2 -0
- package/dist/agent/foreign.test.d.ts.map +1 -0
- package/dist/agent/id.d.ts +78 -0
- package/dist/agent/id.d.ts.map +1 -1
- package/dist/agent/id.js +30 -0
- package/dist/agent/id.test.d.ts +2 -0
- package/dist/agent/id.test.d.ts.map +1 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +3 -0
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +19 -7
- package/dist/agent/merge.test.d.ts +2 -0
- package/dist/agent/merge.test.d.ts.map +1 -0
- package/dist/agent/part-list.d.ts +49 -34
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +42 -6
- package/dist/agent/part-list.test.d.ts +2 -0
- package/dist/agent/part-list.test.d.ts.map +1 -0
- package/dist/agent/parts.d.ts +46 -10
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +16 -6
- package/dist/agent/parts.test.d.ts +2 -0
- package/dist/agent/parts.test.d.ts.map +1 -0
- package/dist/agent/thread.d.ts +18 -0
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/thread.js +8 -2
- package/dist/agent/thread.test.d.ts +2 -0
- package/dist/agent/thread.test.d.ts.map +1 -0
- package/dist/agent/transport.test.d.ts +2 -0
- package/dist/agent/transport.test.d.ts.map +1 -0
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +2 -2
- package/dist/agent/use-agent-stream.test.d.ts +2 -0
- package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.d.ts +24 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.js +55 -15
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
- package/dist/agent-chat/composer.d.ts +130 -8
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +189 -23
- package/dist/agent-chat/composer.test.d.ts +2 -0
- package/dist/agent-chat/composer.test.d.ts.map +1 -0
- package/dist/agent-chat/index.d.ts +5 -1
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/message-affordances.d.ts +59 -0
- package/dist/agent-chat/message-affordances.d.ts.map +1 -0
- package/dist/agent-chat/message-affordances.js +7 -0
- package/dist/agent-chat/relative-time.d.ts +27 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -0
- package/dist/agent-chat/relative-time.js +27 -0
- package/dist/agent-chat/relative-time.test.d.ts +2 -0
- package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- package/dist/agent-chat/thread-feed-row.d.ts +65 -0
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed-row.js +156 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed.d.ts +34 -5
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +52 -19
- package/dist/agent-chat/thread-feed.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-message.d.ts +183 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +329 -37
- package/dist/agent-chat/thread-message.test.d.ts +2 -0
- package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
- package/dist/agent-chat/thread-outcome-card.js +1 -16
- package/dist/agent-chat/thread-workspace.d.ts +14 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +30 -4
- package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
- package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
- package/dist/agent-chat/tool-chip.test.d.ts +2 -0
- package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.d.ts +123 -0
- package/dist/agent-chat/virtualize.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.js +9 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
- package/dist/charts/utils/domain.test.d.ts +2 -0
- package/dist/charts/utils/domain.test.d.ts.map +1 -0
- package/dist/cli/check-theme.test.d.ts +2 -0
- package/dist/cli/check-theme.test.d.ts.map +1 -0
- package/dist/cli/help.test.d.ts +2 -0
- package/dist/cli/help.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/init-sync.test.d.ts +2 -0
- package/dist/cli/init-sync.test.d.ts.map +1 -0
- package/dist/cli/placement-engine.test.d.ts +2 -0
- package/dist/cli/placement-engine.test.d.ts.map +1 -0
- package/dist/cli/tarball-parity.test.d.ts +2 -0
- package/dist/cli/tarball-parity.test.d.ts.map +1 -0
- package/dist/cli/template-format.test.d.ts +2 -0
- package/dist/cli/template-format.test.d.ts.map +1 -0
- package/dist/cli/template-ledger.test.d.ts +2 -0
- package/dist/cli/template-ledger.test.d.ts.map +1 -0
- package/dist/cli/tokens-css.test.d.ts +2 -0
- package/dist/cli/tokens-css.test.d.ts.map +1 -0
- package/dist/content/article-actions.test.d.ts +2 -0
- package/dist/content/article-actions.test.d.ts.map +1 -0
- package/dist/content/article-model.test.d.ts +2 -0
- package/dist/content/article-model.test.d.ts.map +1 -0
- package/dist/content/block-split.test.d.ts +2 -0
- package/dist/content/block-split.test.d.ts.map +1 -0
- package/dist/content/fence-block.d.ts +37 -10
- package/dist/content/fence-block.d.ts.map +1 -1
- package/dist/content/fence-block.js +80 -14
- package/dist/content/heading-components.d.ts.map +1 -1
- package/dist/content/heading-components.js +7 -4
- package/dist/content/heading-components.test.d.ts +2 -0
- package/dist/content/heading-components.test.d.ts.map +1 -0
- package/dist/content/index.d.ts +6 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +5 -1
- package/dist/content/markdown.d.ts +101 -14
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +141 -28
- package/dist/content/markdown.test.d.ts +2 -0
- package/dist/content/markdown.test.d.ts.map +1 -0
- package/dist/content/prose.module.css.test.d.ts +2 -0
- package/dist/content/prose.module.css.test.d.ts.map +1 -0
- package/dist/content/sanitize.d.ts +170 -0
- package/dist/content/sanitize.d.ts.map +1 -0
- package/dist/content/sanitize.js +178 -0
- package/dist/content/sanitize.test.d.ts +2 -0
- package/dist/content/sanitize.test.d.ts.map +1 -0
- package/dist/content/slug.test.d.ts +2 -0
- package/dist/content/slug.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.test.d.ts +2 -0
- package/dist/dashboard/stat-card.test.d.ts.map +1 -0
- package/dist/data/index.d.ts +5 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual.d.ts +2 -1
- package/dist/data/virtual.d.ts.map +1 -1
- package/dist/guard/check-source.test.d.ts +2 -0
- package/dist/guard/check-source.test.d.ts.map +1 -0
- package/dist/guard/guard-hook.test.d.ts +2 -0
- package/dist/guard/guard-hook.test.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/provider/index.test.d.ts +2 -0
- package/dist/provider/index.test.d.ts.map +1 -0
- package/dist/provider/lab-theme.test.d.ts +2 -0
- package/dist/provider/lab-theme.test.d.ts.map +1 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
- package/dist/styles.floor.test.d.ts +2 -0
- package/dist/styles.floor.test.d.ts.map +1 -0
- package/dist/surfaces.d.ts +5 -5
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +14 -4
- package/dist/theme/border-coverage.test.d.ts +18 -0
- package/dist/theme/border-coverage.test.d.ts.map +1 -0
- package/dist/theme/contrast.test.d.ts +2 -0
- package/dist/theme/contrast.test.d.ts.map +1 -0
- package/dist/theme/density-option.test.d.ts +2 -0
- package/dist/theme/density-option.test.d.ts.map +1 -0
- package/dist/theme/density-relations.test.d.ts +2 -0
- package/dist/theme/density-relations.test.d.ts.map +1 -0
- package/dist/theme/derive-theme.test.d.ts +2 -0
- package/dist/theme/derive-theme.test.d.ts.map +1 -0
- package/dist/theme/fonts-option.test.d.ts +2 -0
- package/dist/theme/fonts-option.test.d.ts.map +1 -0
- package/dist/theme/radius-option.test.d.ts +2 -0
- package/dist/theme/radius-option.test.d.ts.map +1 -0
- package/dist/theme/radius.test.d.ts +2 -0
- package/dist/theme/radius.test.d.ts.map +1 -0
- package/dist/theme/shadow-surfaces.test.d.ts +26 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
- package/dist/theme/spacing.test.d.ts +2 -0
- package/dist/theme/spacing.test.d.ts.map +1 -0
- package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
- package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-controls.test.d.ts +2 -0
- package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-state.test.d.ts +2 -0
- package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
- package/dist/theme-lab/theme-lab.test.d.ts +2 -0
- package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
- package/dist/tokens/build-fonts-css.test.d.ts +2 -0
- package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
- package/dist/tokens/build-radius-css.test.d.ts +2 -0
- package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
- package/dist/tokens/density.test.d.ts +2 -0
- package/dist/tokens/density.test.d.ts.map +1 -0
- package/dist/tokens/derive-radius.test.d.ts +2 -0
- package/dist/tokens/derive-radius.test.d.ts.map +1 -0
- package/dist/tokens/derive.test.d.ts +2 -0
- package/dist/tokens/derive.test.d.ts.map +1 -0
- package/dist/tokens/palette.test.d.ts +2 -0
- package/dist/tokens/palette.test.d.ts.map +1 -0
- package/dist/vite-color-mix.test.d.ts +2 -0
- package/dist/vite-color-mix.test.d.ts.map +1 -0
- package/dist/vite.test.d.ts +2 -0
- package/dist/vite.test.d.ts.map +1 -0
- package/llms.txt +9 -9
- package/package.json +7 -2
- package/src/agent/adapter.test.ts +1640 -0
- package/src/agent/adapter.ts +945 -0
- package/src/agent/ai-sdk-transport.test.ts +117 -26
- package/src/agent/ai-sdk-transport.ts +8 -2
- package/src/agent/coalesce.test.ts +202 -9
- package/src/agent/coalesce.ts +126 -15
- package/src/agent/foreign.test.ts +5 -1
- package/src/agent/id.test.ts +101 -3
- package/src/agent/id.ts +118 -0
- package/src/agent/index.ts +8 -0
- package/src/agent/merge.test.ts +87 -0
- package/src/agent/merge.ts +57 -10
- package/src/agent/part-list.test.tsx +31 -1
- package/src/agent/part-list.tsx +77 -29
- package/src/agent/parts.test.ts +121 -4
- package/src/agent/parts.ts +78 -17
- package/src/agent/thread.test.ts +75 -2
- package/src/agent/thread.ts +25 -1
- package/src/agent/use-agent-stream.test.tsx +31 -5
- package/src/agent/use-agent-stream.ts +6 -3
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +5 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +16 -11
- package/src/agent/use-agent-thread-runs.test.tsx +356 -17
- package/src/agent/use-agent-thread-runs.ts +159 -23
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +93 -7
- package/src/agent-chat/composer.test.tsx +924 -0
- package/src/agent-chat/composer.tsx +521 -52
- package/src/agent-chat/index.ts +13 -1
- package/src/agent-chat/message-affordances.ts +61 -0
- package/src/agent-chat/relative-time.test.ts +95 -0
- package/src/agent-chat/relative-time.ts +55 -0
- package/src/agent-chat/thread-detail-panel.tsx +3 -1
- package/src/agent-chat/thread-feed-row.test.tsx +510 -0
- package/src/agent-chat/thread-feed-row.tsx +274 -0
- package/src/agent-chat/thread-feed.test.tsx +241 -0
- package/src/agent-chat/thread-feed.tsx +119 -18
- package/src/agent-chat/thread-message.test.tsx +1078 -4
- package/src/agent-chat/thread-message.tsx +795 -42
- package/src/agent-chat/thread-outcome-card.tsx +1 -25
- package/src/agent-chat/thread-workspace.test.tsx +169 -0
- package/src/agent-chat/thread-workspace.tsx +70 -6
- package/src/agent-chat/virtualize.ts +132 -0
- package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/template-format.test.ts +2 -2
- package/src/content/fence-block.tsx +242 -15
- package/src/content/heading-components.test.tsx +65 -0
- package/src/content/heading-components.tsx +26 -5
- package/src/content/index.ts +19 -3
- package/src/content/markdown.test.tsx +603 -0
- package/src/content/markdown.tsx +355 -35
- package/src/content/sanitize.test.ts +659 -0
- package/src/content/sanitize.ts +510 -0
- package/src/dashboard/stat-card.test.tsx +6 -1
- package/src/data/index.ts +5 -1
- package/src/data/virtual-list.tsx +2 -1
- package/src/data/virtual.ts +2 -1
- package/src/index.ts +18 -1
- package/src/surfaces.ts +13 -3
- package/src/theme/contrast.test.ts +1 -4
- package/src/theme/density-relations.test.ts +4 -2
- package/src/theme/fonts-option.test.ts +7 -1
- package/src/theme/shadow-surfaces.test.ts +21 -6
- package/src/theme/use-basalt-spacing.test.tsx +2 -1
|
@@ -17,7 +17,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'
|
|
|
17
17
|
import { useAgentThreadRuns } from './use-agent-thread-runs'
|
|
18
18
|
import type { AgentThread, ThreadsStore } from './thread'
|
|
19
19
|
import type { AgentTransport } from './transport'
|
|
20
|
-
import type { AgentPart } from './parts'
|
|
20
|
+
import type { AgentPart, AgentPartDraft } from './parts'
|
|
21
21
|
import type { AgentOutcome, OutcomeResolver } from './outcome'
|
|
22
22
|
|
|
23
23
|
// ── test-only ThreadsStore double — see use-agent-thread-runs.wedge.test.tsx for the rationale ──
|
|
@@ -100,6 +100,11 @@ function createTestThreadsStore(
|
|
|
100
100
|
threads = []
|
|
101
101
|
activeId = null
|
|
102
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,
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -130,7 +135,7 @@ describe('useAgentThreadRuns — concurrency, no-op guard, finalize order, failu
|
|
|
130
135
|
const transport: AgentTransport<AgentPart, string> = {
|
|
131
136
|
async *stream(input) {
|
|
132
137
|
await gates.get(input)?.promise
|
|
133
|
-
yield { type: 'text', text: `done:${input}` }
|
|
138
|
+
yield { id: 'p1', type: 'text', text: `done:${input}` }
|
|
134
139
|
},
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -173,7 +178,7 @@ describe('useAgentThreadRuns — concurrency, no-op guard, finalize order, failu
|
|
|
173
178
|
async *stream(input) {
|
|
174
179
|
calls.push(input)
|
|
175
180
|
await gate.promise
|
|
176
|
-
yield { type: 'text', text: 'ok' }
|
|
181
|
+
yield { id: 'p1', type: 'text', text: 'ok' }
|
|
177
182
|
},
|
|
178
183
|
}
|
|
179
184
|
|
|
@@ -218,7 +223,7 @@ describe('useAgentThreadRuns — concurrency, no-op guard, finalize order, failu
|
|
|
218
223
|
const transport: AgentTransport<AgentPart, string> = {
|
|
219
224
|
async *stream() {
|
|
220
225
|
await gate.promise
|
|
221
|
-
yield { type: 'text', text: 'hi' }
|
|
226
|
+
yield { id: 'p1', type: 'text', text: 'hi' }
|
|
222
227
|
},
|
|
223
228
|
}
|
|
224
229
|
|
|
@@ -265,7 +270,7 @@ describe('useAgentThreadRuns — concurrency, no-op guard, finalize order, failu
|
|
|
265
270
|
async *stream() {
|
|
266
271
|
throw new Error('boom')
|
|
267
272
|
// eslint-disable-next-line no-unreachable
|
|
268
|
-
yield { type: 'text', text: 'unreachable' }
|
|
273
|
+
yield { id: 'p1', type: 'text', text: 'unreachable' }
|
|
269
274
|
},
|
|
270
275
|
}
|
|
271
276
|
|
|
@@ -294,7 +299,7 @@ describe('useAgentThreadRuns — concurrency, no-op guard, finalize order, failu
|
|
|
294
299
|
const transport: AgentTransport<AgentPart, string> = {
|
|
295
300
|
async *stream(input) {
|
|
296
301
|
calls.push(input)
|
|
297
|
-
yield { type: 'text', text: 'x' }
|
|
302
|
+
yield { id: 'p1', type: 'text', text: 'x' }
|
|
298
303
|
},
|
|
299
304
|
}
|
|
300
305
|
|
|
@@ -316,7 +321,7 @@ describe('useAgentThreadRuns — concurrency, no-op guard, finalize order, failu
|
|
|
316
321
|
const transport: AgentTransport<AgentPart, string> = {
|
|
317
322
|
async *stream(input) {
|
|
318
323
|
calls.push(input)
|
|
319
|
-
yield { type: 'text', text: `done:${input}` }
|
|
324
|
+
yield { id: 'p1', type: 'text', text: `done:${input}` }
|
|
320
325
|
},
|
|
321
326
|
}
|
|
322
327
|
|
|
@@ -364,7 +369,7 @@ describe('useAgentThreadRuns — concurrency, no-op guard, finalize order, failu
|
|
|
364
369
|
|
|
365
370
|
const transport: AgentTransport<AgentPart, string> = {
|
|
366
371
|
async *stream() {
|
|
367
|
-
yield { type: 'text', text: 'hi' }
|
|
372
|
+
yield { id: 'p1', type: 'text', text: 'hi' }
|
|
368
373
|
},
|
|
369
374
|
}
|
|
370
375
|
|
|
@@ -403,11 +408,15 @@ describe('useAgentThreadRuns — stop()/stopAll() lifecycle', () => {
|
|
|
403
408
|
const threadId = store.create()
|
|
404
409
|
|
|
405
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).
|
|
406
415
|
const transport: AgentTransport<AgentPart, string> = {
|
|
407
416
|
async *stream() {
|
|
408
|
-
yield { type: 'text', text: 'partial' }
|
|
417
|
+
yield { type: 'text', text: 'partial' } as AgentPartDraft as AgentPart
|
|
409
418
|
await gate.promise
|
|
410
|
-
yield { type: 'text', text: 'never arrives' }
|
|
419
|
+
yield { type: 'text', text: 'never arrives' } as AgentPartDraft as AgentPart
|
|
411
420
|
},
|
|
412
421
|
}
|
|
413
422
|
|
|
@@ -456,7 +465,7 @@ describe('useAgentThreadRuns — stop()/stopAll() lifecycle', () => {
|
|
|
456
465
|
|
|
457
466
|
const transport: AgentTransport<AgentPart, string> = {
|
|
458
467
|
async *stream() {
|
|
459
|
-
yield { type: 'text', text: 'hi' }
|
|
468
|
+
yield { id: 'p1', type: 'text', text: 'hi' }
|
|
460
469
|
},
|
|
461
470
|
}
|
|
462
471
|
|
|
@@ -498,7 +507,7 @@ describe('useAgentThreadRuns — stop()/stopAll() lifecycle', () => {
|
|
|
498
507
|
|
|
499
508
|
const transport: AgentTransport<AgentPart, string> = {
|
|
500
509
|
async *stream() {
|
|
501
|
-
yield { type: 'text', text: 'x' }
|
|
510
|
+
yield { id: 'p1', type: 'text', text: 'x' }
|
|
502
511
|
},
|
|
503
512
|
}
|
|
504
513
|
|
|
@@ -672,10 +681,10 @@ describe('useAgentThreadRuns — stop()/stopAll() lifecycle', () => {
|
|
|
672
681
|
const transport: AgentTransport<AgentPart, string> = {
|
|
673
682
|
async *stream(input) {
|
|
674
683
|
if (input === 'a') {
|
|
675
|
-
yield { type: 'text', text: 'partial-a' }
|
|
684
|
+
yield { id: 'p1', type: 'text', text: 'partial-a' }
|
|
676
685
|
await gateA.promise
|
|
677
686
|
} else {
|
|
678
|
-
yield { type: 'text', text: 'partial-b' }
|
|
687
|
+
yield { id: 'p1', type: 'text', text: 'partial-b' }
|
|
679
688
|
await gateB.promise
|
|
680
689
|
}
|
|
681
690
|
},
|
|
@@ -748,17 +757,21 @@ describe('useAgentThreadRuns — F1 regression: id-less drafts within one run',
|
|
|
748
757
|
const store = createTestThreadsStore()
|
|
749
758
|
const threadId = store.create()
|
|
750
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).
|
|
751
764
|
const transport: AgentTransport<AgentPart, string> = {
|
|
752
765
|
async *stream() {
|
|
753
|
-
yield { type: 'text', text: 'Hi' }
|
|
766
|
+
yield { type: 'text', text: 'Hi' } as AgentPartDraft as AgentPart
|
|
754
767
|
yield {
|
|
755
768
|
type: 'tool',
|
|
756
769
|
toolCallId: 'c1',
|
|
757
770
|
toolName: 'search',
|
|
758
771
|
state: 'input-available',
|
|
759
772
|
input: { q: 'x' },
|
|
760
|
-
}
|
|
761
|
-
yield { type: 'text', text: 'Bye' }
|
|
773
|
+
} as AgentPartDraft as AgentPart
|
|
774
|
+
yield { type: 'text', text: 'Bye' } as AgentPartDraft as AgentPart
|
|
762
775
|
},
|
|
763
776
|
}
|
|
764
777
|
|
|
@@ -779,3 +792,329 @@ describe('useAgentThreadRuns — F1 regression: id-less drafts within one run',
|
|
|
779
792
|
expect(assistantMessage?.parts.map((part) => part.type)).toEqual(['text', 'tool', 'text'])
|
|
780
793
|
})
|
|
781
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
|
+
})
|