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
package/src/agent/thread.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* createThreadsStore — ring-buffer caps (maxThreads/maxMessagesPerThread), newest-first ordering,
|
|
3
|
-
* setResumeToken's delete-the-key-on-undefined semantics,
|
|
3
|
+
* setResumeToken's delete-the-key-on-undefined semantics, same-tick action accumulation, and the
|
|
4
|
+
* constant hydrated/error pair the async sibling store (./adapter) actually varies.
|
|
4
5
|
*
|
|
5
6
|
* Uses the REAL localStorage-backed store (happy-dom provides `window.localStorage`), so every
|
|
6
7
|
* test uses its own unique `key` — the same convention as
|
|
@@ -13,7 +14,12 @@ import { createThreadsStore } from './thread'
|
|
|
13
14
|
import type { ChatMessage } from './history'
|
|
14
15
|
|
|
15
16
|
function makeMessage(id: string): ChatMessage {
|
|
16
|
-
return {
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
role: 'user',
|
|
20
|
+
parts: [{ id: `${id}-p0`, type: 'text', text: id }],
|
|
21
|
+
createdAt: Date.now(),
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
describe('createThreadsStore', () => {
|
|
@@ -144,4 +150,71 @@ describe('createThreadsStore', () => {
|
|
|
144
150
|
expect(thread?.status).toBe('streaming')
|
|
145
151
|
expect(thread?.resumeToken).toBe('tok-accum')
|
|
146
152
|
})
|
|
153
|
+
|
|
154
|
+
test('hydrated is always true and error always undefined — localStorage resolves synchronously', () => {
|
|
155
|
+
const useThreads = createThreadsStore({ key: 'thread-hydrated-a', version: 1 })
|
|
156
|
+
const { result } = renderHook(() => useThreads())
|
|
157
|
+
|
|
158
|
+
// Pinned because these two exist for the async adapter store (./adapter); the localStorage
|
|
159
|
+
// store must fill them with the "nothing to wait for, nothing to report" constants on the
|
|
160
|
+
// very first render, before any action has run.
|
|
161
|
+
expect(result.current.hydrated).toBe(true)
|
|
162
|
+
expect(result.current.error).toBeUndefined()
|
|
163
|
+
|
|
164
|
+
act(() => {
|
|
165
|
+
result.current.create()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(result.current.hydrated).toBe(true)
|
|
169
|
+
expect(result.current.error).toBeUndefined()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('create() mints distinct ids via crypto.getRandomValues when randomUUID is unavailable', () => {
|
|
173
|
+
const originalCrypto = globalThis.crypto
|
|
174
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
175
|
+
value: { getRandomValues: originalCrypto.getRandomValues.bind(originalCrypto) },
|
|
176
|
+
configurable: true,
|
|
177
|
+
})
|
|
178
|
+
try {
|
|
179
|
+
const useThreads = createThreadsStore({ key: 'thread-crypto-a', version: 1 })
|
|
180
|
+
const { result } = renderHook(() => useThreads())
|
|
181
|
+
|
|
182
|
+
let first = ''
|
|
183
|
+
let second = ''
|
|
184
|
+
act(() => {
|
|
185
|
+
first = result.current.create()
|
|
186
|
+
second = result.current.create()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
expect(first).not.toBe('')
|
|
190
|
+
expect(second).not.toBe('')
|
|
191
|
+
expect(first).not.toBe(second)
|
|
192
|
+
} finally {
|
|
193
|
+
// Restore the real crypto so no later test in the process is affected.
|
|
194
|
+
Object.defineProperty(globalThis, 'crypto', { value: originalCrypto, configurable: true })
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('create() still mints distinct ids with no usable crypto at all (randomUUID and getRandomValues both absent)', () => {
|
|
199
|
+
const originalCrypto = globalThis.crypto
|
|
200
|
+
Object.defineProperty(globalThis, 'crypto', { value: undefined, configurable: true })
|
|
201
|
+
try {
|
|
202
|
+
const useThreads = createThreadsStore({ key: 'thread-crypto-b', version: 1 })
|
|
203
|
+
const { result } = renderHook(() => useThreads())
|
|
204
|
+
|
|
205
|
+
let first = ''
|
|
206
|
+
let second = ''
|
|
207
|
+
act(() => {
|
|
208
|
+
first = result.current.create()
|
|
209
|
+
second = result.current.create()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(first).not.toBe('')
|
|
213
|
+
expect(second).not.toBe('')
|
|
214
|
+
expect(first).not.toBe(second)
|
|
215
|
+
} finally {
|
|
216
|
+
// Restore the real crypto so no later test in the process is affected.
|
|
217
|
+
Object.defineProperty(globalThis, 'crypto', { value: originalCrypto, configurable: true })
|
|
218
|
+
}
|
|
219
|
+
})
|
|
147
220
|
})
|
package/src/agent/thread.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { useCallback, useRef } from 'react'
|
|
25
25
|
import { createPersistedState } from '../state'
|
|
26
26
|
import type { ChatMessage } from './history'
|
|
27
|
+
import { mintThreadId } from './id'
|
|
27
28
|
import type { AgentOutcome } from './outcome'
|
|
28
29
|
import type { AgentPart } from './parts'
|
|
29
30
|
|
|
@@ -138,6 +139,24 @@ export type ThreadsStore<TPart = AgentPart> = {
|
|
|
138
139
|
readonly remove: (id: string) => void
|
|
139
140
|
/** Remove all threads and clear the active selection. */
|
|
140
141
|
readonly clear: () => void
|
|
142
|
+
/**
|
|
143
|
+
* Whether `threads` reflects the backing store yet. Always `true` for the localStorage store
|
|
144
|
+
* (createPersistedState resolves synchronously); meaningful only for an async, server-backed
|
|
145
|
+
* store built with `createAdapterThreadsStore` (./adapter), where it stays `false` until the
|
|
146
|
+
* first `listThreads` SUCCEEDS. Pair it with `error` — `!hydrated && error !== undefined` is a
|
|
147
|
+
* failed load, `!hydrated && error === undefined` is still loading.
|
|
148
|
+
*
|
|
149
|
+
* Gating an empty state on this is the CALLER's job when hand-assembling `ThreadFeed` /
|
|
150
|
+
* `ThreadDetailPanel`; the shipped `ThreadWorkspace` already does it internally, so an
|
|
151
|
+
* async store never flashes "no threads yet" through it before it actually knows.
|
|
152
|
+
*/
|
|
153
|
+
readonly hydrated: boolean
|
|
154
|
+
/**
|
|
155
|
+
* The most recent persistence failure, or `undefined`. Always `undefined` for the localStorage
|
|
156
|
+
* store, which has no failure the caller can act on. An async store surfaces the rejected
|
|
157
|
+
* load/write here after rolling its optimistic edit back.
|
|
158
|
+
*/
|
|
159
|
+
readonly error: unknown
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
// ── createThreadsStore ────────────────────────────────────────────────────────
|
|
@@ -198,7 +217,7 @@ export function createThreadsStore<TPart = AgentPart>(
|
|
|
198
217
|
|
|
199
218
|
const create = useCallback(
|
|
200
219
|
(createOpts?: { readonly meta?: Record<string, unknown> }): string => {
|
|
201
|
-
const id =
|
|
220
|
+
const id = mintThreadId()
|
|
202
221
|
const now = Date.now()
|
|
203
222
|
const thread: AgentThread<TPart> = {
|
|
204
223
|
id,
|
|
@@ -307,6 +326,11 @@ export function createThreadsStore<TPart = AgentPart>(
|
|
|
307
326
|
markRead,
|
|
308
327
|
remove,
|
|
309
328
|
clear,
|
|
329
|
+
// localStorage resolves synchronously and swallows its own quota errors, so there is never
|
|
330
|
+
// a pending load to await nor a failure a caller could act on. See ./adapter for the async
|
|
331
|
+
// store where these two carry real information.
|
|
332
|
+
hydrated: true,
|
|
333
|
+
error: undefined,
|
|
310
334
|
}
|
|
311
335
|
}
|
|
312
336
|
}
|
|
@@ -11,7 +11,7 @@ import { StrictMode, useEffect } from 'react'
|
|
|
11
11
|
import { act, render, renderHook, waitFor } from '@testing-library/react'
|
|
12
12
|
import { useAgentStream } from './use-agent-stream'
|
|
13
13
|
import type { AgentTransport } from './transport'
|
|
14
|
-
import type { AgentPart } from './parts'
|
|
14
|
+
import type { AgentPart, AgentPartDraft } from './parts'
|
|
15
15
|
|
|
16
16
|
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
|
|
17
17
|
let resolve!: (value: T) => void
|
|
@@ -96,17 +96,21 @@ describe('useAgentStream', () => {
|
|
|
96
96
|
})
|
|
97
97
|
|
|
98
98
|
test('F1 regression: id-less drafts of different types within one send() do not destroy each other', async () => {
|
|
99
|
+
// Yields id-less drafts on purpose — the whole point of this regression is that TWO drafts
|
|
100
|
+
// (of different types, so a real bug wouldn't be "same id" but "no id at all") must not
|
|
101
|
+
// collide via `undefined === undefined`. The cast documents that the type says AgentPart
|
|
102
|
+
// (identified) while a transport is always permitted to omit id (AgentPartDraft).
|
|
99
103
|
const transport: AgentTransport<AgentPart, string> = {
|
|
100
104
|
async *stream() {
|
|
101
|
-
yield { type: 'text', text: 'Hi' }
|
|
105
|
+
yield { type: 'text', text: 'Hi' } as AgentPartDraft as AgentPart
|
|
102
106
|
yield {
|
|
103
107
|
type: 'tool',
|
|
104
108
|
toolCallId: 'c1',
|
|
105
109
|
toolName: 'search',
|
|
106
110
|
state: 'input-available',
|
|
107
111
|
input: { q: 'x' },
|
|
108
|
-
}
|
|
109
|
-
yield { type: 'text', text: 'Bye' }
|
|
112
|
+
} as AgentPartDraft as AgentPart
|
|
113
|
+
yield { type: 'text', text: 'Bye' } as AgentPartDraft as AgentPart
|
|
110
114
|
},
|
|
111
115
|
}
|
|
112
116
|
|
|
@@ -131,7 +135,7 @@ describe('useAgentStream', () => {
|
|
|
131
135
|
async *stream() {
|
|
132
136
|
throw boom
|
|
133
137
|
// eslint-disable-next-line no-unreachable
|
|
134
|
-
yield { type: 'text', text: 'unreachable' }
|
|
138
|
+
yield { id: 'unreachable', type: 'text', text: 'unreachable' }
|
|
135
139
|
},
|
|
136
140
|
}
|
|
137
141
|
|
|
@@ -258,4 +262,26 @@ describe('useAgentStream', () => {
|
|
|
258
262
|
// Only the SECOND (surviving) call's part made it into final state — the first is discarded.
|
|
259
263
|
expect(capturedParts).toEqual([{ id: 'call-2', type: 'text', text: 'call-2' }])
|
|
260
264
|
})
|
|
265
|
+
|
|
266
|
+
test('send() still streams without throwing on a host with no usable crypto at all — its runId is a low-collision-cost mint (mintThreadId), never the message-id one', async () => {
|
|
267
|
+
const originalCrypto = globalThis.crypto
|
|
268
|
+
Object.defineProperty(globalThis, 'crypto', { value: undefined, configurable: true })
|
|
269
|
+
try {
|
|
270
|
+
const transport: AgentTransport<AgentPart, string> = {
|
|
271
|
+
async *stream() {
|
|
272
|
+
yield { type: 'text', text: 'a' } as AgentPartDraft as AgentPart
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
const { result } = renderHook(() => useAgentStream({ transport }))
|
|
276
|
+
|
|
277
|
+
await act(async () => {
|
|
278
|
+
await result.current.send('hi')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
expect(result.current.status).toBe('done')
|
|
282
|
+
expect(result.current.parts).toHaveLength(1)
|
|
283
|
+
} finally {
|
|
284
|
+
Object.defineProperty(globalThis, 'crypto', { value: originalCrypto, configurable: true })
|
|
285
|
+
}
|
|
286
|
+
})
|
|
261
287
|
})
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* }
|
|
28
28
|
*/
|
|
29
29
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
30
|
-
import { withPartIds } from './id'
|
|
30
|
+
import { mintThreadId, withPartIds } from './id'
|
|
31
31
|
import { isStartPart } from './parts'
|
|
32
32
|
import { mergePart } from './merge'
|
|
33
33
|
import type { PartLike } from './merge'
|
|
@@ -107,8 +107,11 @@ export function useAgentStream<TPart extends PartLike = AgentPart, TInput = stri
|
|
|
107
107
|
// fully resets `parts` anyway, so a fresh id per call is enough to keep two id-less drafts
|
|
108
108
|
// ARRIVING WITHIN THE SAME SEND from colliding on `undefined === undefined` in mergePart (the
|
|
109
109
|
// F1 bug — text→tool→text destroying each other). Not a replay mechanism — this hook has no
|
|
110
|
-
// resume() path — see withPartIds' own doc for why it can't be one anyway.
|
|
111
|
-
|
|
110
|
+
// resume() path — see withPartIds' own doc for why it can't be one anyway. Low collision
|
|
111
|
+
// cost (this hook persists nothing — `parts` is discarded on the next send()/unmount), so
|
|
112
|
+
// `mintThreadId` — never `mintMessageId` — is the right rung-3 behavior here: this must not
|
|
113
|
+
// throw on a host with no usable crypto at all.
|
|
114
|
+
const runId = mintThreadId()
|
|
112
115
|
|
|
113
116
|
try {
|
|
114
117
|
for await (const part of withPartIds(runId, transport.stream(input, controller.signal))) {
|
|
@@ -86,6 +86,11 @@ function createTestThreadsStore(): ThreadsStore<TranscriptPart> {
|
|
|
86
86
|
threads = []
|
|
87
87
|
activeId = null
|
|
88
88
|
},
|
|
89
|
+
// Always-hydrated, never-erroring — mirrors the localStorage-backed store's real values
|
|
90
|
+
// (see ThreadsStore.hydrated/.error doc comments); this double is a synchronous in-memory
|
|
91
|
+
// stand-in with no async load path to fail.
|
|
92
|
+
hydrated: true,
|
|
93
|
+
error: undefined,
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
@@ -16,7 +16,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
16
16
|
import { renderHook, waitFor } from '@testing-library/react'
|
|
17
17
|
import { useAgentThreadRuns } from './use-agent-thread-runs'
|
|
18
18
|
import type { AgentThread, ThreadsStore } from './thread'
|
|
19
|
-
import type { AgentTransport } from './transport'
|
|
19
|
+
import type { AgentTransport, ResumableAgentTransport } from './transport'
|
|
20
20
|
import type { AgentPart } from './parts'
|
|
21
21
|
import type { ChatMessage } from './history'
|
|
22
22
|
import type { AgentOutcome } from './outcome'
|
|
@@ -90,6 +90,11 @@ function createTestThreadsStore(initial: AgentThread<AgentPart>[]): ThreadsStore
|
|
|
90
90
|
threads = []
|
|
91
91
|
activeId = null
|
|
92
92
|
},
|
|
93
|
+
// Always-hydrated, never-erroring — mirrors the localStorage-backed store's real values
|
|
94
|
+
// (see ThreadsStore.hydrated/.error doc comments); this double is a synchronous in-memory
|
|
95
|
+
// stand-in with no async load path to fail.
|
|
96
|
+
hydrated: true,
|
|
97
|
+
error: undefined,
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
|
@@ -100,7 +105,7 @@ function makeStreamingThread(
|
|
|
100
105
|
const userMessage: ChatMessage<AgentPart> = {
|
|
101
106
|
id: crypto.randomUUID(),
|
|
102
107
|
role: 'user',
|
|
103
|
-
parts: [{ type: 'text', text: 'hello' }],
|
|
108
|
+
parts: [{ id: 'seed-user-part', type: 'text', text: 'hello' }],
|
|
104
109
|
createdAt: now,
|
|
105
110
|
}
|
|
106
111
|
return {
|
|
@@ -142,7 +147,7 @@ describe('useAgentThreadRuns — mount-time reconcile', () => {
|
|
|
142
147
|
const userMessage: ChatMessage<AgentPart> = {
|
|
143
148
|
id: crypto.randomUUID(),
|
|
144
149
|
role: 'user',
|
|
145
|
-
parts: [{ type: 'text', text: 'hello' }],
|
|
150
|
+
parts: [{ id: 'seed-user-part', type: 'text', text: 'hello' }],
|
|
146
151
|
createdAt: now,
|
|
147
152
|
}
|
|
148
153
|
const thread: AgentThread<AgentPart> = {
|
|
@@ -157,11 +162,11 @@ describe('useAgentThreadRuns — mount-time reconcile', () => {
|
|
|
157
162
|
const store = createTestThreadsStore([thread])
|
|
158
163
|
|
|
159
164
|
let resumeCalls = 0
|
|
160
|
-
const transport = {
|
|
165
|
+
const transport: ResumableAgentTransport<AgentPart, string> = {
|
|
161
166
|
async *stream() {},
|
|
162
167
|
async *resume() {
|
|
163
168
|
resumeCalls++
|
|
164
|
-
yield { type: 'text', text: 'resumed' }
|
|
169
|
+
yield { id: 'p1', type: 'text', text: 'resumed' }
|
|
165
170
|
},
|
|
166
171
|
idempotentReplay: true as const,
|
|
167
172
|
}
|
|
@@ -179,11 +184,11 @@ describe('useAgentThreadRuns — mount-time reconcile', () => {
|
|
|
179
184
|
const store = createTestThreadsStore([thread])
|
|
180
185
|
|
|
181
186
|
let resumeCalls = 0
|
|
182
|
-
const transport = {
|
|
187
|
+
const transport: ResumableAgentTransport<AgentPart, string> = {
|
|
183
188
|
async *stream() {},
|
|
184
189
|
async *resume() {
|
|
185
190
|
resumeCalls++
|
|
186
|
-
yield { type: 'text', text: 'resumed' }
|
|
191
|
+
yield { id: 'p1', type: 'text', text: 'resumed' }
|
|
187
192
|
},
|
|
188
193
|
idempotentReplay: true as const,
|
|
189
194
|
}
|
|
@@ -205,7 +210,7 @@ describe('useAgentThreadRuns — mount-time reconcile', () => {
|
|
|
205
210
|
async *stream() {},
|
|
206
211
|
async *resume() {
|
|
207
212
|
resumeCalls++
|
|
208
|
-
yield { type: 'text', text: 'resumed' }
|
|
213
|
+
yield { id: 'p1', type: 'text', text: 'resumed' }
|
|
209
214
|
},
|
|
210
215
|
// deliberately NO idempotentReplay: true
|
|
211
216
|
}
|
|
@@ -223,11 +228,11 @@ describe('useAgentThreadRuns — mount-time reconcile', () => {
|
|
|
223
228
|
const store = createTestThreadsStore([thread])
|
|
224
229
|
|
|
225
230
|
const resumeArgs: string[] = []
|
|
226
|
-
const transport = {
|
|
231
|
+
const transport: ResumableAgentTransport<AgentPart, string> = {
|
|
227
232
|
async *stream() {},
|
|
228
|
-
async *resume(token
|
|
233
|
+
async *resume(token) {
|
|
229
234
|
resumeArgs.push(token)
|
|
230
|
-
yield { type: 'text', text: 'resumed' }
|
|
235
|
+
yield { id: 'p1', type: 'text', text: 'resumed' }
|
|
231
236
|
},
|
|
232
237
|
idempotentReplay: true as const,
|
|
233
238
|
}
|