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
|
@@ -1,18 +1,140 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
1
|
+
import type { ClipboardEvent, JSX, ReactNode, Ref } from 'react';
|
|
2
|
+
export type ComposerAttachment = {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly mediaType: string;
|
|
6
|
+
readonly size: number;
|
|
7
|
+
readonly url: string;
|
|
8
|
+
};
|
|
9
|
+
export type ComposerSubmit = {
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly attachments: readonly ComposerAttachment[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Imperative escape hatch for slot content that must write into the composer from OUTSIDE the
|
|
15
|
+
* render tree — a voice recorder streaming transcript chunks, a suggestion chip inserting at the
|
|
16
|
+
* cursor. Obtained via `ref`. Kept to exactly the three primitives that cover both real use cases
|
|
17
|
+
* (every method here is API surface fixed for a major, so this is deliberately not more):
|
|
18
|
+
*
|
|
19
|
+
* - `insertText` — the one write primitive. Inserts at the CURRENT SELECTION, replacing it if
|
|
20
|
+
* non-collapsed, so a mid-speech voice chunk lands where the caret is and a suggestion never
|
|
21
|
+
* clobbers a selection the user made on purpose. See its own doc for what "current selection"
|
|
22
|
+
* means when the textarea isn't focused.
|
|
23
|
+
* - `setValue` — a whole-value replace (loading a canned reply, discarding the draft), for when
|
|
24
|
+
* there is no meaningful selection to insert around.
|
|
25
|
+
* - `focus` — hands focus back to the textarea (e.g. once a recorder finishes).
|
|
26
|
+
*
|
|
27
|
+
* All three write through the SAME draft store `onChange` does — same `draftKey` persistence,
|
|
28
|
+
* same subscriber notification, same value the existing submit/clear/restore logic already sees.
|
|
29
|
+
* There is no second source of truth.
|
|
30
|
+
*
|
|
31
|
+
* A slot control that inserts at the caret must not steal focus first. A plain `<button>` in
|
|
32
|
+
* `leftSection`/`rightSection` blurs the textarea on mousedown, which collapses the selection before
|
|
33
|
+
* `onClick` runs — so every insert silently lands at the end and the at-caret path is unreachable by
|
|
34
|
+
* mouse. Suppress it on the control, not here:
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <ActionIcon onMouseDown={(e) => e.preventDefault()} onClick={record}>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export type ComposerHandle = {
|
|
41
|
+
/**
|
|
42
|
+
* Inserts `text` at the textarea's current selection, replacing it if non-collapsed, then moves
|
|
43
|
+
* the caret to just after the inserted text.
|
|
44
|
+
*
|
|
45
|
+
* "Current selection" only exists while the textarea IS the focused element — a `<textarea>`'s
|
|
46
|
+
* `selectionStart`/`selectionEnd` are not a reliable caret once focus has moved elsewhere, so:
|
|
47
|
+
* - **focused, collapsed caret**: inserts at that caret position.
|
|
48
|
+
* - **focused, with a range selected**: replaces the selected range (never inserted alongside
|
|
49
|
+
* it — that would silently keep text the user was in the middle of deleting).
|
|
50
|
+
* - **not focused, including never focused**: appends to the end of the current value. This is
|
|
51
|
+
* the safe default for a voice recorder streaming chunks before the user has clicked into
|
|
52
|
+
* the box at all.
|
|
53
|
+
*/
|
|
54
|
+
readonly insertText: (text: string) => void;
|
|
55
|
+
/** Replaces the whole draft value and moves the caret to the end. */
|
|
56
|
+
readonly setValue: (value: string) => void;
|
|
57
|
+
/** Focuses the textarea. */
|
|
58
|
+
readonly focus: () => void;
|
|
59
|
+
};
|
|
2
60
|
export type ComposerProps = {
|
|
3
|
-
/**
|
|
4
|
-
readonly
|
|
5
|
-
/**
|
|
6
|
-
|
|
61
|
+
/** Imperative write/focus handle — see `ComposerHandle`. */
|
|
62
|
+
readonly ref?: Ref<ComposerHandle>;
|
|
63
|
+
/**
|
|
64
|
+
* Called with the trimmed text plus the pending attachments. May return `void` (fire-and-forget,
|
|
65
|
+
* validated only synchronously) or a `Promise<void>` (its outcome is awaited, its timing is not).
|
|
66
|
+
*
|
|
67
|
+
* The draft and attachments clear OPTIMISTICALLY, immediately, before this settles — a chat
|
|
68
|
+
* composer has to feel instant, so Composer never sits on the typed text for a round trip.
|
|
69
|
+
* Rejecting restores them instead:
|
|
70
|
+
* - THROW synchronously to refuse a send outright (bad input, offline, …) — the clear never
|
|
71
|
+
* runs, exactly as before this prop went async.
|
|
72
|
+
* - Return a promise that REJECTS to report a failure discovered later (a network error) — the
|
|
73
|
+
* text and attachments are put back, UNLESS something has since made that unsafe:
|
|
74
|
+
* - the user already typed something new into the (now-empty) box — their new text wins;
|
|
75
|
+
* the failed one is not restored (surface a retry via the failed message instead);
|
|
76
|
+
* - a LATER submit has already gone through this same draft — that one's own optimistic
|
|
77
|
+
* clear (whether it went on to succeed or fail) wins over restoring this older one;
|
|
78
|
+
* - the attachments are no longer empty (the caller already put something else there), or
|
|
79
|
+
* `draftKey` has since changed (this Composer now points at a different thread/consumer
|
|
80
|
+
* destination) — either way the attachments are left as they are rather than risking a
|
|
81
|
+
* leak into whatever is current now.
|
|
82
|
+
* A resolved promise clears and stays cleared, same as a `void` return.
|
|
83
|
+
*/
|
|
84
|
+
readonly onSubmit: (payload: ComposerSubmit) => void | Promise<void>;
|
|
7
85
|
readonly placeholder?: string;
|
|
8
86
|
/** Autofocuses the textarea on mount. */
|
|
9
87
|
readonly autoFocus?: boolean;
|
|
88
|
+
/** Autosize ceiling for the textarea. Default 6. */
|
|
89
|
+
readonly maxRows?: number;
|
|
90
|
+
/** Hard-disable, independent of streaming (e.g. offline). */
|
|
91
|
+
readonly disabled?: boolean;
|
|
92
|
+
/** A run is in flight on this thread. */
|
|
93
|
+
readonly streaming?: boolean;
|
|
94
|
+
/** Default false — preserves the historical `disabled={streaming}` behaviour. */
|
|
95
|
+
readonly allowSubmitWhileStreaming?: boolean;
|
|
96
|
+
/** When given, the send action becomes a Stop action while `streaming`. */
|
|
97
|
+
readonly onStop?: () => void;
|
|
98
|
+
/**
|
|
99
|
+
* Opaque slot rendered before the textarea (a voice recorder, a mode switch, …). Slot content
|
|
100
|
+
* that needs to WRITE into the composer — append a transcript, insert a suggestion — does so
|
|
101
|
+
* through the `ref` handle (`ComposerHandle`), not a callback prop here; see the file's
|
|
102
|
+
* `@example`.
|
|
103
|
+
*/
|
|
104
|
+
readonly leftSection?: ReactNode;
|
|
105
|
+
/** Opaque slot rendered between the textarea and the send action (an attach button, …). */
|
|
106
|
+
readonly rightSection?: ReactNode;
|
|
107
|
+
/**
|
|
108
|
+
* Pending attachments, owned by the caller. Composer only forwards them on submit and asks for a
|
|
109
|
+
* reset afterwards — rendering previews is the caller's job, via `leftSection`/`rightSection`.
|
|
110
|
+
*/
|
|
111
|
+
readonly attachments?: readonly ComposerAttachment[];
|
|
112
|
+
/**
|
|
113
|
+
* Called with `[]` optimistically the instant a submit goes out WITH pending attachments — a
|
|
114
|
+
* submit that had none to begin with skips the call rather than reporting a no-op change — and,
|
|
115
|
+
* subject to the restoration guards documented on `onSubmit`, with the original attachments
|
|
116
|
+
* again if that submit later rejects.
|
|
117
|
+
*/
|
|
118
|
+
readonly onAttachmentsChange?: (next: readonly ComposerAttachment[]) => void;
|
|
119
|
+
readonly onPaste?: (event: ClipboardEvent<HTMLTextAreaElement>) => void;
|
|
120
|
+
/**
|
|
121
|
+
* Persists the unsent draft under `basalt:composer-draft:<key>`; switching keys swaps drafts, and
|
|
122
|
+
* the entry is removed (not blanked) on a successful submit.
|
|
123
|
+
*
|
|
124
|
+
* "Successful" means `onSubmit` returned (or resolved) without throwing or rejecting — a send
|
|
125
|
+
* that fails, synchronously or later over the network, restores this persisted draft instead of
|
|
126
|
+
* leaving it erased (subject to the restoration guards documented on `onSubmit`).
|
|
127
|
+
*/
|
|
128
|
+
readonly draftKey?: string;
|
|
129
|
+
/** Replaces the whole Enter/Shift+Enter hint row. Pass `null` to drop it entirely. */
|
|
130
|
+
readonly hint?: ReactNode;
|
|
10
131
|
};
|
|
11
132
|
/**
|
|
12
|
-
* An autosize Textarea + send ActionIcon. Enter submits, Shift+Enter
|
|
133
|
+
* An autosize Textarea + send ActionIcon, with slots either side. Enter submits, Shift+Enter
|
|
134
|
+
* inserts a newline, and `draftKey` survives an unmount.
|
|
13
135
|
*
|
|
14
136
|
* @example
|
|
15
|
-
* <Composer onSubmit={onSend}
|
|
137
|
+
* <Composer onSubmit={({ text }) => onSend(text)} streaming={streaming} onStop={stop} />
|
|
16
138
|
*/
|
|
17
|
-
export declare function Composer({
|
|
139
|
+
export declare function Composer({ ref, onSubmit, placeholder, autoFocus, maxRows, disabled, streaming, allowSubmitWhileStreaming, onStop, leftSection, rightSection, attachments, onAttachmentsChange, onPaste, draftKey, hint, }: ComposerProps): JSX.Element;
|
|
18
140
|
//# sourceMappingURL=composer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/agent-chat/composer.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/agent-chat/composer.tsx"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAAE,cAAc,EAAkB,GAAG,EAAiB,SAAS,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAmJ/F,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,SAAS,kBAAkB,EAAE,CAAA;CACpD,CAAA;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAC3B,CAAA;AAWD,MAAM,MAAM,aAAa,GAAG;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAA;IAClC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,yCAAyC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,oDAAoD;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,yCAAyC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,iFAAiF;IACjF,QAAQ,CAAC,yBAAyB,CAAC,EAAE,OAAO,CAAA;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,CAAA;IAChC,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,CAAA;IACjC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACpD;;;;;OAKG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,kBAAkB,EAAE,KAAK,IAAI,CAAA;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,mBAAmB,CAAC,KAAK,IAAI,CAAA;IACvE;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAC1B,CAAA;AAsBD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,QAAQ,EACR,WAAW,EACX,SAAS,EACT,OAAW,EACX,QAAgB,EAChB,SAAiB,EACjB,yBAAiC,EACjC,MAAM,EACN,WAAW,EACX,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,IAAI,GACL,EAAE,aAAa,GAAG,GAAG,CAAC,OAAO,CAgO7B"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ActionIcon, Group, Kbd, Stack, Text, Textarea } from "@mantine/core";
|
|
3
|
-
import {
|
|
3
|
+
import { useImperativeHandle, useLayoutEffect, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
import { readPersistedValue } from "../state.js";
|
|
4
5
|
import { VX } from "../tokens/index.js";
|
|
5
6
|
function SendGlyph() {
|
|
6
7
|
return /* @__PURE__ */ jsx("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ jsx(
|
|
@@ -14,21 +15,178 @@ function SendGlyph() {
|
|
|
14
15
|
}
|
|
15
16
|
) });
|
|
16
17
|
}
|
|
18
|
+
function StopGlyph() {
|
|
19
|
+
return /* @__PURE__ */ jsx("svg", { width: 16, height: 16, viewBox: "0 0 24 24", "aria-hidden": true, children: /* @__PURE__ */ jsx("rect", { x: 7, y: 7, width: 10, height: 10, rx: 2, fill: "currentColor" }) });
|
|
20
|
+
}
|
|
21
|
+
const DRAFT_VERSION = 1;
|
|
22
|
+
function createDraftStore(key) {
|
|
23
|
+
const storageKey = key === null ? null : `basalt:composer-draft:${key}`;
|
|
24
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
25
|
+
let cache = key === null ? "" : null;
|
|
26
|
+
let storageHandler = null;
|
|
27
|
+
let submissionId = 0;
|
|
28
|
+
const notify = () => {
|
|
29
|
+
for (const cb of listeners) cb();
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
subscribe: (cb) => {
|
|
33
|
+
listeners.add(cb);
|
|
34
|
+
if (storageKey !== null && storageHandler === null) {
|
|
35
|
+
storageHandler = (e) => {
|
|
36
|
+
if (e.key !== storageKey) return;
|
|
37
|
+
cache = null;
|
|
38
|
+
notify();
|
|
39
|
+
};
|
|
40
|
+
window.addEventListener("storage", storageHandler);
|
|
41
|
+
}
|
|
42
|
+
return () => {
|
|
43
|
+
listeners.delete(cb);
|
|
44
|
+
if (listeners.size === 0 && storageHandler !== null) {
|
|
45
|
+
window.removeEventListener("storage", storageHandler);
|
|
46
|
+
storageHandler = null;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
get: () => {
|
|
51
|
+
if (cache === null) {
|
|
52
|
+
const persisted = key === null ? null : readPersistedValue(`composer-draft:${key}`, DRAFT_VERSION);
|
|
53
|
+
cache = typeof persisted === "string" ? persisted : "";
|
|
54
|
+
}
|
|
55
|
+
return cache;
|
|
56
|
+
},
|
|
57
|
+
set: (next) => {
|
|
58
|
+
cache = next;
|
|
59
|
+
if (storageKey !== null) {
|
|
60
|
+
try {
|
|
61
|
+
if (next.length === 0) window.localStorage.removeItem(storageKey);
|
|
62
|
+
else
|
|
63
|
+
window.localStorage.setItem(
|
|
64
|
+
storageKey,
|
|
65
|
+
JSON.stringify({ v: DRAFT_VERSION, value: next })
|
|
66
|
+
);
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
notify();
|
|
71
|
+
},
|
|
72
|
+
bumpSubmissionId: () => {
|
|
73
|
+
submissionId += 1;
|
|
74
|
+
return submissionId;
|
|
75
|
+
},
|
|
76
|
+
currentSubmissionId: () => submissionId
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const draftStores = /* @__PURE__ */ new Map();
|
|
80
|
+
function persistedDraftStore(key) {
|
|
81
|
+
const existing = draftStores.get(key);
|
|
82
|
+
if (existing !== void 0) return existing;
|
|
83
|
+
const created = createDraftStore(key);
|
|
84
|
+
draftStores.set(key, created);
|
|
85
|
+
return created;
|
|
86
|
+
}
|
|
87
|
+
const isServer = typeof window === "undefined";
|
|
88
|
+
const noopSubscribe = (_cb) => () => {
|
|
89
|
+
};
|
|
90
|
+
const emptyDraft = () => "";
|
|
91
|
+
const NO_ATTACHMENTS = [];
|
|
92
|
+
function liveSelection(el) {
|
|
93
|
+
if (document.activeElement !== el) return null;
|
|
94
|
+
const { selectionStart, selectionEnd } = el;
|
|
95
|
+
if (selectionStart === null || selectionEnd === null) return null;
|
|
96
|
+
return { start: selectionStart, end: selectionEnd };
|
|
97
|
+
}
|
|
98
|
+
function DefaultHint() {
|
|
99
|
+
return /* @__PURE__ */ jsxs(Group, { gap: 4, children: [
|
|
100
|
+
/* @__PURE__ */ jsx(Kbd, { size: "xs", children: "Enter" }),
|
|
101
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { fontFamily: "var(--basalt-font-mono)" }, children: "to send" }),
|
|
102
|
+
/* @__PURE__ */ jsx(Kbd, { size: "xs", children: "Shift" }),
|
|
103
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { fontFamily: "var(--basalt-font-mono)" }, children: "+" }),
|
|
104
|
+
/* @__PURE__ */ jsx(Kbd, { size: "xs", children: "Enter" }),
|
|
105
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { fontFamily: "var(--basalt-font-mono)" }, children: "for a new line" })
|
|
106
|
+
] });
|
|
107
|
+
}
|
|
17
108
|
function Composer({
|
|
109
|
+
ref,
|
|
18
110
|
onSubmit,
|
|
19
|
-
disabled = false,
|
|
20
111
|
placeholder,
|
|
21
|
-
autoFocus
|
|
112
|
+
autoFocus,
|
|
113
|
+
maxRows = 6,
|
|
114
|
+
disabled = false,
|
|
115
|
+
streaming = false,
|
|
116
|
+
allowSubmitWhileStreaming = false,
|
|
117
|
+
onStop,
|
|
118
|
+
leftSection,
|
|
119
|
+
rightSection,
|
|
120
|
+
attachments,
|
|
121
|
+
onAttachmentsChange,
|
|
122
|
+
onPaste,
|
|
123
|
+
draftKey,
|
|
124
|
+
hint
|
|
22
125
|
}) {
|
|
23
|
-
const
|
|
126
|
+
const memoryStore = useRef(null);
|
|
127
|
+
memoryStore.current ??= createDraftStore(null);
|
|
128
|
+
const store = draftKey === void 0 ? memoryStore.current : persistedDraftStore(draftKey);
|
|
129
|
+
const value = useSyncExternalStore(
|
|
130
|
+
isServer ? noopSubscribe : store.subscribe,
|
|
131
|
+
isServer ? emptyDraft : store.get,
|
|
132
|
+
emptyDraft
|
|
133
|
+
);
|
|
134
|
+
const inputDisabled = disabled || streaming && !allowSubmitWhileStreaming;
|
|
135
|
+
const showStop = streaming && onStop !== void 0;
|
|
136
|
+
const showSend = !showStop || allowSubmitWhileStreaming;
|
|
137
|
+
const pending = attachments ?? NO_ATTACHMENTS;
|
|
138
|
+
const trimmed = value.trim();
|
|
139
|
+
const hasPayload = trimmed.length > 0 || pending.length > 0;
|
|
140
|
+
const attachmentsRef = useRef(pending);
|
|
141
|
+
attachmentsRef.current = pending;
|
|
142
|
+
const draftKeyRef = useRef(draftKey);
|
|
143
|
+
draftKeyRef.current = draftKey;
|
|
144
|
+
const textareaRef = useRef(null);
|
|
145
|
+
const pendingCaretRef = useRef(null);
|
|
146
|
+
useLayoutEffect(() => {
|
|
147
|
+
const caret = pendingCaretRef.current;
|
|
148
|
+
if (caret === null) return;
|
|
149
|
+
pendingCaretRef.current = null;
|
|
150
|
+
textareaRef.current?.setSelectionRange(caret, caret);
|
|
151
|
+
}, [value]);
|
|
152
|
+
const insertText = (text) => {
|
|
153
|
+
const current = store.get();
|
|
154
|
+
const selection = textareaRef.current === null ? null : liveSelection(textareaRef.current);
|
|
155
|
+
const start = selection?.start ?? current.length;
|
|
156
|
+
const end = selection?.end ?? current.length;
|
|
157
|
+
const next = current.slice(0, start) + text + current.slice(end);
|
|
158
|
+
store.set(next);
|
|
159
|
+
pendingCaretRef.current = next === value ? null : start + text.length;
|
|
160
|
+
};
|
|
161
|
+
const setValue = (next) => {
|
|
162
|
+
store.set(next);
|
|
163
|
+
pendingCaretRef.current = next === value ? null : next.length;
|
|
164
|
+
};
|
|
165
|
+
const focusTextarea = () => {
|
|
166
|
+
textareaRef.current?.focus();
|
|
167
|
+
};
|
|
168
|
+
useImperativeHandle(ref, () => ({ insertText, setValue, focus: focusTextarea }));
|
|
24
169
|
const submit = () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
170
|
+
if (inputDisabled || !hasPayload) return;
|
|
171
|
+
const submittedText = trimmed;
|
|
172
|
+
const submittedAttachments = pending;
|
|
173
|
+
const submittedDraftKey = draftKey;
|
|
174
|
+
const mySubmissionId = store.bumpSubmissionId();
|
|
175
|
+
const result = onSubmit({ text: submittedText, attachments: submittedAttachments });
|
|
176
|
+
store.set("");
|
|
177
|
+
if (submittedAttachments.length > 0) onAttachmentsChange?.([]);
|
|
178
|
+
void Promise.resolve(result).catch(() => {
|
|
179
|
+
if (store.currentSubmissionId() === mySubmissionId && store.get() === "") {
|
|
180
|
+
store.set(submittedText);
|
|
181
|
+
}
|
|
182
|
+
if (submittedAttachments.length > 0 && attachmentsRef.current.length === 0 && draftKeyRef.current === submittedDraftKey) {
|
|
183
|
+
onAttachmentsChange?.(submittedAttachments);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
29
186
|
};
|
|
30
187
|
const handleKeyDown = (event) => {
|
|
31
188
|
if (event.key !== "Enter" || event.shiftKey) return;
|
|
189
|
+
if (event.nativeEvent.isComposing) return;
|
|
32
190
|
event.preventDefault();
|
|
33
191
|
submit();
|
|
34
192
|
};
|
|
@@ -36,10 +194,10 @@ function Composer({
|
|
|
36
194
|
flex: 1,
|
|
37
195
|
autosize: true,
|
|
38
196
|
minRows: 1,
|
|
39
|
-
maxRows
|
|
197
|
+
maxRows,
|
|
40
198
|
value,
|
|
41
|
-
disabled,
|
|
42
|
-
onChange: (event) =>
|
|
199
|
+
disabled: inputDisabled,
|
|
200
|
+
onChange: (event) => store.set(event.currentTarget.value),
|
|
43
201
|
onKeyDown: handleKeyDown,
|
|
44
202
|
// Composer input surface (docs/DESIGN-SPEC.md §5): panel + control radius (6px, VX.radiusCtrl);
|
|
45
203
|
// the ring lives in the shadow, so the input carries no separate border. Depth is deliberately
|
|
@@ -56,30 +214,38 @@ function Composer({
|
|
|
56
214
|
};
|
|
57
215
|
if (placeholder !== void 0) textareaProps.placeholder = placeholder;
|
|
58
216
|
if (autoFocus !== void 0) textareaProps.autoFocus = autoFocus;
|
|
217
|
+
if (onPaste !== void 0) textareaProps.onPaste = onPaste;
|
|
59
218
|
return /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
|
|
60
219
|
/* @__PURE__ */ jsxs(Group, { gap: "xs", align: "flex-end", wrap: "nowrap", children: [
|
|
61
|
-
|
|
62
|
-
/* @__PURE__ */ jsx(
|
|
220
|
+
leftSection,
|
|
221
|
+
/* @__PURE__ */ jsx(Textarea, { ...textareaProps, ref: textareaRef }),
|
|
222
|
+
rightSection,
|
|
223
|
+
showStop ? /* @__PURE__ */ jsx(
|
|
224
|
+
ActionIcon,
|
|
225
|
+
{
|
|
226
|
+
size: 42,
|
|
227
|
+
radius: "md",
|
|
228
|
+
variant: "filled",
|
|
229
|
+
color: "red",
|
|
230
|
+
onClick: onStop,
|
|
231
|
+
"aria-label": "Stop generating",
|
|
232
|
+
children: /* @__PURE__ */ jsx(StopGlyph, {})
|
|
233
|
+
}
|
|
234
|
+
) : null,
|
|
235
|
+
showSend ? /* @__PURE__ */ jsx(
|
|
63
236
|
ActionIcon,
|
|
64
237
|
{
|
|
65
238
|
size: 42,
|
|
66
239
|
radius: "md",
|
|
67
240
|
variant: "filled",
|
|
68
241
|
onClick: submit,
|
|
69
|
-
disabled:
|
|
242
|
+
disabled: inputDisabled || !hasPayload,
|
|
70
243
|
"aria-label": "Send message",
|
|
71
244
|
children: /* @__PURE__ */ jsx(SendGlyph, {})
|
|
72
245
|
}
|
|
73
|
-
)
|
|
246
|
+
) : null
|
|
74
247
|
] }),
|
|
75
|
-
/* @__PURE__ */
|
|
76
|
-
/* @__PURE__ */ jsx(Kbd, { size: "xs", children: "Enter" }),
|
|
77
|
-
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { fontFamily: "var(--basalt-font-mono)" }, children: "to send" }),
|
|
78
|
-
/* @__PURE__ */ jsx(Kbd, { size: "xs", children: "Shift" }),
|
|
79
|
-
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { fontFamily: "var(--basalt-font-mono)" }, children: "+" }),
|
|
80
|
-
/* @__PURE__ */ jsx(Kbd, { size: "xs", children: "Enter" }),
|
|
81
|
-
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", style: { fontFamily: "var(--basalt-font-mono)" }, children: "for a new line" })
|
|
82
|
-
] })
|
|
248
|
+
hint === void 0 ? /* @__PURE__ */ jsx(DefaultHint, {}) : hint
|
|
83
249
|
] });
|
|
84
250
|
}
|
|
85
251
|
export {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.test.d.ts","sourceRoot":"","sources":["../../src/agent-chat/composer.test.tsx"],"names":[],"mappings":""}
|
|
@@ -14,12 +14,16 @@ export { ThreadWorkspace } from './thread-workspace.js';
|
|
|
14
14
|
export type { ThreadWorkspaceProps } from './thread-workspace.js';
|
|
15
15
|
export { ThreadFeed } from './thread-feed.js';
|
|
16
16
|
export type { ThreadFeedProps } from './thread-feed.js';
|
|
17
|
+
export { ThreadFeedRow } from './thread-feed-row.js';
|
|
18
|
+
export type { ThreadFeedRowProps } from './thread-feed-row.js';
|
|
19
|
+
export type { MessageAffordances } from './message-affordances.js';
|
|
20
|
+
export type { VirtualizeOptions, VirtualizeProps } from './virtualize.js';
|
|
17
21
|
export { ThreadOutcomeCard } from './thread-outcome-card.js';
|
|
18
22
|
export type { ThreadOutcomeCardProps } from './thread-outcome-card.js';
|
|
19
23
|
export { ThreadDetailPanel } from './thread-detail-panel.js';
|
|
20
24
|
export type { ThreadDetailPanelProps } from './thread-detail-panel.js';
|
|
21
25
|
export { Composer } from './composer.js';
|
|
22
|
-
export type { ComposerProps } from './composer.js';
|
|
26
|
+
export type { ComposerAttachment, ComposerHandle, ComposerProps, ComposerSubmit } from './composer.js';
|
|
23
27
|
export { threadPartRenderers, ThreadTranscript } from './thread-message.js';
|
|
24
28
|
export type { ThreadTranscriptProps } from './thread-message.js';
|
|
25
29
|
export { ToolChip } from './tool-chip.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG9D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG9D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAO3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAGtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGnG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAG7D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/agent-chat/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ThreadWorkspace } from "./thread-workspace.js";
|
|
2
2
|
import { ThreadFeed } from "./thread-feed.js";
|
|
3
|
+
import { ThreadFeedRow } from "./thread-feed-row.js";
|
|
3
4
|
import { ThreadOutcomeCard } from "./thread-outcome-card.js";
|
|
4
5
|
import { ThreadDetailPanel } from "./thread-detail-panel.js";
|
|
5
6
|
import { Composer } from "./composer.js";
|
|
@@ -9,6 +10,7 @@ export {
|
|
|
9
10
|
Composer,
|
|
10
11
|
ThreadDetailPanel,
|
|
11
12
|
ThreadFeed,
|
|
13
|
+
ThreadFeedRow,
|
|
12
14
|
ThreadOutcomeCard,
|
|
13
15
|
ThreadTranscript,
|
|
14
16
|
ThreadWorkspace,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageAffordances — the per-message hover-row/action contract shared by `ThreadTranscript` and
|
|
3
|
+
* a virtualized `ThreadFeedRow` (both render `MessageBlock`, so the affordance surface lives here
|
|
4
|
+
* rather than being redeclared on each consumer).
|
|
5
|
+
*
|
|
6
|
+
* Type-only module — the rendering (the actual hover row, copy button, regenerate button) lives in
|
|
7
|
+
* `thread-message.tsx`. `MessageAffordances` itself IS public (re-exported, type-only, from the
|
|
8
|
+
* agent-chat barrel and the root): it is named in `ThreadTranscriptProps.affordances` and
|
|
9
|
+
* `ThreadFeedRowProps.affordances`, so a consumer has to be able to name it. `DEFAULT_AFFORDANCES`
|
|
10
|
+
* stays internal — resolving unset fields is the components' job, not the consumer's.
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
import type { ChatMessage, TranscriptPart } from '../agent/index.js';
|
|
14
|
+
export type MessageAffordances = {
|
|
15
|
+
/**
|
|
16
|
+
* How the per-message timestamp renders.
|
|
17
|
+
* @default 'relative'
|
|
18
|
+
*/
|
|
19
|
+
readonly timestamp?: 'relative' | 'absolute' | 'none';
|
|
20
|
+
/**
|
|
21
|
+
* Whether a copy action is offered on the hover row. Copies the message's COALESCED text (the
|
|
22
|
+
* same merged/de-duplicated text `coalesceParts` produces for display), not the raw `parts`
|
|
23
|
+
* array — a message can accumulate multiple adjacent/by-id text parts while streaming, and
|
|
24
|
+
* copying the raw parts would either duplicate text or copy fragments the user never saw
|
|
25
|
+
* assembled.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
readonly copy?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Regenerate action, shown on the LAST ASSISTANT message only (never on earlier turns, never on
|
|
31
|
+
* user messages). Receives that message's `id`. `useAgentThreadRuns`'s `retry` is
|
|
32
|
+
* THREAD-keyed (`retry: (threadId: string) => void`), replaying the last user input for the
|
|
33
|
+
* whole thread — the consumer bridges this messageId-keyed callback to that thread-keyed retry;
|
|
34
|
+
* the framework only ever hands back the messageId, it does not know about threads.
|
|
35
|
+
*/
|
|
36
|
+
readonly onRegenerate?: (messageId: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Extra actions appended to the hover row, after the built-in copy/regenerate actions.
|
|
39
|
+
*
|
|
40
|
+
* This is a RENDER prop, not an event handler — it is called during render, once per message,
|
|
41
|
+
* and its return value is rendered. That means it is treated differently from `onRegenerate`:
|
|
42
|
+
* `onRegenerate` is identity-stabilized internally (a fresh inline literal every render costs
|
|
43
|
+
* nothing), while `actions` is compared by REFERENCE, because its reference is the only signal
|
|
44
|
+
* that its output may have changed. A fresh inline `actions={() => …}` literal therefore
|
|
45
|
+
* re-renders every message block on every render of the transcript. That is correct — an action
|
|
46
|
+
* whose output depends on consumer state (a pin/star toggle) must be allowed to update — but on
|
|
47
|
+
* a long thread it is worth avoiding: wrap `actions` in the consumer's own `useCallback`, keyed
|
|
48
|
+
* on whatever state it actually reads.
|
|
49
|
+
*/
|
|
50
|
+
readonly actions?: (ctx: {
|
|
51
|
+
readonly message: ChatMessage<TranscriptPart>;
|
|
52
|
+
}) => ReactNode;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolved defaults for the optional {@link MessageAffordances} fields — one place, so
|
|
56
|
+
* `ThreadTranscript` and a virtualized `ThreadFeedRow` cannot drift on what "unset" means.
|
|
57
|
+
*/
|
|
58
|
+
export declare const DEFAULT_AFFORDANCES: Required<Pick<MessageAffordances, 'timestamp' | 'copy'>>;
|
|
59
|
+
//# sourceMappingURL=message-affordances.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-affordances.d.ts","sourceRoot":"","sources":["../../src/agent-chat/message-affordances.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAE3D,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,CAAA;IACrD;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACnD;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,cAAc,CAAC,CAAA;KAAE,KAAK,SAAS,CAAA;CACzF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,MAAM,CAAC,CAGxF,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* formatRelativeTime — dependency-free relative-time formatting (no date-fns).
|
|
3
|
+
*
|
|
4
|
+
* Shared between `ThreadOutcomeCard` (inbox row timestamps) and `ThreadTranscript` (per-message
|
|
5
|
+
* timestamps). 'en'-hardcoded for now — see the module-level note below for the deferred locale
|
|
6
|
+
* seam.
|
|
7
|
+
*
|
|
8
|
+
* Internal to `agent-chat/` — not part of the public barrel.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Formats an epoch-ms timestamp as a short relative string ("3 hours ago", "just now").
|
|
12
|
+
*
|
|
13
|
+
* A non-finite `timestamp` — `NaN`, `±Infinity`, or a non-number value that slipped past the
|
|
14
|
+
* `ChatMessage.createdAt: number` type at runtime (an ISO string or `undefined`/`null` from a
|
|
15
|
+
* hand-rolled `ThreadsStoreAdapter`, or from JSON-deserialized `localStorage` state) renders as an
|
|
16
|
+
* empty string rather than reaching `Intl.RelativeTimeFormat.format`, which throws a `RangeError`
|
|
17
|
+
* on any non-finite input. This is render-path code — one bad `createdAt` on one message must not
|
|
18
|
+
* blank the whole transcript (the standing rule `spliceText`/`coalesceParts` already hold) — so it
|
|
19
|
+
* degrades instead of throwing, and warns in dev, matching `mergePart`'s clamp-and-warn precedent
|
|
20
|
+
* (`clampOffset` in `../agent/merge.ts`). The warning is NOT deduplicated, also matching that
|
|
21
|
+
* precedent — but note the call sites differ: `clampOffset` runs once per wire event, whereas this
|
|
22
|
+
* runs during render, once per message, so a single bad `createdAt` in a long transcript warns
|
|
23
|
+
* once per message per render. That is loud on purpose (a `createdAt` that isn't a number is an
|
|
24
|
+
* adapter bug worth fixing, not worth muting), and it costs nothing in production.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatRelativeTime(timestamp: number): string;
|
|
27
|
+
//# sourceMappingURL=relative-time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relative-time.d.ts","sourceRoot":"","sources":["../../src/agent-chat/relative-time.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAc5D"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const RELATIVE_TIME_FORMAT = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
2
|
+
const RELATIVE_TIME_UNITS = [
|
|
3
|
+
{ unit: "year", ms: 31536e6 },
|
|
4
|
+
{ unit: "month", ms: 2628e6 },
|
|
5
|
+
{ unit: "week", ms: 6048e5 },
|
|
6
|
+
{ unit: "day", ms: 864e5 },
|
|
7
|
+
{ unit: "hour", ms: 36e5 },
|
|
8
|
+
{ unit: "minute", ms: 6e4 }
|
|
9
|
+
];
|
|
10
|
+
function formatRelativeTime(timestamp) {
|
|
11
|
+
if (!Number.isFinite(timestamp)) {
|
|
12
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
13
|
+
console.warn(
|
|
14
|
+
`[basalt] formatRelativeTime: non-finite timestamp ${String(timestamp)} \u2014 rendering empty string`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
const diffMs = timestamp - Date.now();
|
|
20
|
+
const absMs = Math.abs(diffMs);
|
|
21
|
+
if (absMs < 6e4) return "just now";
|
|
22
|
+
const unit = RELATIVE_TIME_UNITS.find(({ ms }) => absMs >= ms) ?? RELATIVE_TIME_UNITS.at(-1);
|
|
23
|
+
return RELATIVE_TIME_FORMAT.format(Math.round(diffMs / unit.ms), unit.unit);
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
formatRelativeTime
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relative-time.test.d.ts","sourceRoot":"","sources":["../../src/agent-chat/relative-time.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread-detail-panel.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-detail-panel.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAiDpE,MAAM,MAAM,sBAAsB,GAAG;IACnC,uFAAuF;IACvF,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IACnC,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,CAAA;IAChC,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAA;IACjC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,SAAS,EACT,SAAS,EACT,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,GACR,EAAE,sBAAsB,GAAG,GAAG,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"thread-detail-panel.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-detail-panel.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAiDpE,MAAM,MAAM,sBAAsB,GAAG;IACnC,uFAAuF;IACvF,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IACnC,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,CAAA;IAChC,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAA;IACjC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,SAAS,EACT,SAAS,EACT,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,GACR,EAAE,sBAAsB,GAAG,GAAG,CAAC,OAAO,CAyFtC"}
|
|
@@ -90,7 +90,7 @@ function ThreadDetailPanel({
|
|
|
90
90
|
) }),
|
|
91
91
|
/* @__PURE__ */ jsx(Divider, { color: "var(--vx-divider)" }),
|
|
92
92
|
/* @__PURE__ */ jsx(Box, { p: "sm", children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
|
|
93
|
-
/* @__PURE__ */ jsx(Composer, { onSubmit: onSend,
|
|
93
|
+
/* @__PURE__ */ jsx(Composer, { onSubmit: ({ text }) => onSend(text), streaming }),
|
|
94
94
|
streaming && /* @__PURE__ */ jsx(Button, { size: "compact-sm", variant: "subtle", color: "red", onClick: onStop, children: "Stop" })
|
|
95
95
|
] }) })
|
|
96
96
|
] });
|