basalt-ui 1.12.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 +17 -0
- package/README.md +38 -35
- package/agent/rules/basalt-agent.md +14 -0
- 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 +5 -1
- package/dist/agent/adapter.d.ts.map +1 -1
- package/dist/agent/adapter.js +22 -13
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +6 -2
- package/dist/agent/id.d.ts +67 -19
- package/dist/agent/id.d.ts.map +1 -1
- package/dist/agent/id.js +21 -7
- package/dist/agent/thread.d.ts +4 -0
- package/dist/agent/thread.d.ts.map +1 -1
- 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-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.js +55 -15
- package/dist/agent-chat/index.d.ts +4 -0
- 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-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 +315 -34
- 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 +29 -3
- 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/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/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/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/surfaces.d.ts +2 -2
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +3 -2
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/llms.txt +6 -6
- package/package.json +2 -2
- package/src/agent/adapter.test.ts +237 -0
- package/src/agent/adapter.ts +72 -14
- package/src/agent/ai-sdk-transport.test.ts +64 -0
- package/src/agent/ai-sdk-transport.ts +8 -2
- package/src/agent/id.test.ts +95 -1
- package/src/agent/id.ts +98 -28
- package/src/agent/thread.ts +4 -0
- package/src/agent/use-agent-stream.test.tsx +22 -0
- package/src/agent/use-agent-stream.ts +6 -3
- package/src/agent/use-agent-thread-runs.test.tsx +326 -0
- package/src/agent/use-agent-thread-runs.ts +159 -23
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +80 -0
- package/src/agent-chat/index.ts +12 -0
- 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-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 +937 -3
- package/src/agent-chat/thread-message.tsx +766 -38
- 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 +69 -5
- package/src/agent-chat/virtualize.ts +132 -0
- package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
- 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 +5 -0
- package/src/surfaces.ts +2 -1
- package/src/theme/shadow-surfaces.test.ts +9 -0
|
@@ -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":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { AgentThread, ForeignPart, PartRenderer, PartRenderers, StreamStatus, TranscriptPart } from '../agent/index.js';
|
|
3
|
+
import type { ComposerProps, ComposerSubmit } from './composer.js';
|
|
4
|
+
import type { MessageAffordances } from './message-affordances.js';
|
|
5
|
+
import type { VirtualizeProps } from './virtualize.js';
|
|
6
|
+
type ThreadFeedRowBase = {
|
|
7
|
+
/**
|
|
8
|
+
* The thread this row projects. Typed against `AgentThread<TranscriptPart>` — not the plain,
|
|
9
|
+
* `AgentPart`-defaulted `AgentThread` `ThreadFeed` uses, and not a type parameter. A consumer
|
|
10
|
+
* with a registered `ForeignPart` union passes its own threads straight through with no generic
|
|
11
|
+
* argument and no cast, because `TranscriptPart` is the widest part type and arrays are
|
|
12
|
+
* structurally covariant: `AgentPart[]` and `ConsumerPart[]` both widen into `TranscriptPart[]`.
|
|
13
|
+
* Widening the row is what lets both `ThreadFeed` (non-generic, `AgentThread<AgentPart>`) and a
|
|
14
|
+
* part-registry consumer feed the same component.
|
|
15
|
+
*/
|
|
16
|
+
readonly thread: AgentThread<TranscriptPart>;
|
|
17
|
+
/** Whether this row's transcript+composer body is currently visible. */
|
|
18
|
+
readonly expanded: boolean;
|
|
19
|
+
/** Called with the thread's id when the header is clicked/activated. */
|
|
20
|
+
readonly onToggle: (id: string) => void;
|
|
21
|
+
/** The live (in-flight) assistant turn's parts, when a run is streaming for this thread. */
|
|
22
|
+
readonly liveParts?: readonly TranscriptPart[];
|
|
23
|
+
/** The live run's status — drives the in-progress indicator on the live block. */
|
|
24
|
+
readonly liveStatus?: StreamStatus;
|
|
25
|
+
/** Consumer renderers keyed by part.type, forwarded to `ThreadTranscript`. */
|
|
26
|
+
readonly renderers?: PartRenderers;
|
|
27
|
+
/** Forwarded to `ThreadTranscript` — see its own prop doc. */
|
|
28
|
+
readonly fallbackRenderer?: PartRenderer<ForeignPart>;
|
|
29
|
+
/**
|
|
30
|
+
* Per-message hover-row contract (timestamp/copy/regenerate/actions), forwarded verbatim to the
|
|
31
|
+
* row's `ThreadTranscript`. Shared with it via `message-affordances.ts` so the two cannot drift
|
|
32
|
+
* on what an unset field means.
|
|
33
|
+
*/
|
|
34
|
+
readonly affordances?: MessageAffordances;
|
|
35
|
+
/** Forwarded to `ThreadTranscript` — suppresses role label + chrome on same-speaker runs.
|
|
36
|
+
* @default true */
|
|
37
|
+
readonly groupConsecutive?: boolean;
|
|
38
|
+
/** Called with the composer's submit payload. */
|
|
39
|
+
readonly onSend: (payload: ComposerSubmit) => void;
|
|
40
|
+
/** Shown as the composer's Stop action while `liveStatus === 'streaming'`. */
|
|
41
|
+
readonly onStop?: () => void;
|
|
42
|
+
/** Forwarded to the row's `Composer`, minus the three props this component always wires itself
|
|
43
|
+
* (`onSubmit`, `onStop`, and `streaming` — the last derived from `liveStatus` so a consumer can't
|
|
44
|
+
* pass one that disagrees with the row's own live status). */
|
|
45
|
+
readonly composerProps?: Omit<ComposerProps, 'onSubmit' | 'onStop' | 'streaming'>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* `virtualize`/`height` are forwarded straight to the row's `ThreadTranscript`, carrying the same
|
|
49
|
+
* union guard: an inline row holding a very long thread can window it, and doing so REQUIRES a
|
|
50
|
+
* `height` (the transcript then owns a fixed-height scroll node inside the expanded body, instead
|
|
51
|
+
* of the body growing to the thread's full length). Omit both and the row's transcript is
|
|
52
|
+
* content-sized, as before.
|
|
53
|
+
*/
|
|
54
|
+
export type ThreadFeedRowProps = ThreadFeedRowBase & VirtualizeProps;
|
|
55
|
+
/**
|
|
56
|
+
* One inline-expanding thread row: a header (title + relative timestamp) that toggles a lazily
|
|
57
|
+
* mounted, kept-mounted transcript + composer body. See the module doc for the mount lifecycle
|
|
58
|
+
* invariant this component exists to guarantee.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* <ThreadFeedRow thread={thread} expanded={expanded} onToggle={onToggle} onSend={onSend} />
|
|
62
|
+
*/
|
|
63
|
+
export declare function ThreadFeedRow(props: ThreadFeedRowProps): JSX.Element;
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=thread-feed-row.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-feed-row.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-feed-row.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,UAAU,CAAA;AAEjB,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAI/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AA6BnD,KAAK,iBAAiB,GAAG;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,cAAc,CAAC,CAAA;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;IAC9C,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,YAAY,CAAA;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,8DAA8D;IAC9D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,CAAA;IACrD;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC;uBACmB;IACnB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;IACnC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAA;IAClD,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B;;kEAE8D;IAC9D,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC,CAAA;CAClF,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG,eAAe,CAAA;AASpE;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,GAAG,CAAC,OAAO,CA8HpE"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Group, Stack, Text, UnstyledButton } from "@mantine/core";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { VX } from "../tokens/index.js";
|
|
5
|
+
import { Composer } from "./composer.js";
|
|
6
|
+
import { formatRelativeTime } from "./relative-time.js";
|
|
7
|
+
import { ThreadTranscript } from "./thread-message.js";
|
|
8
|
+
import { resolveVirtualize } from "./virtualize.js";
|
|
9
|
+
function ChevronGlyph({ expanded }) {
|
|
10
|
+
return /* @__PURE__ */ jsx(
|
|
11
|
+
"svg",
|
|
12
|
+
{
|
|
13
|
+
width: 14,
|
|
14
|
+
height: 14,
|
|
15
|
+
viewBox: "0 0 24 24",
|
|
16
|
+
fill: "none",
|
|
17
|
+
"aria-hidden": true,
|
|
18
|
+
style: {
|
|
19
|
+
transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
|
|
20
|
+
transition: "transform 120ms ease",
|
|
21
|
+
flexShrink: 0
|
|
22
|
+
},
|
|
23
|
+
children: /* @__PURE__ */ jsx(
|
|
24
|
+
"path",
|
|
25
|
+
{
|
|
26
|
+
d: "M9 6l6 6l-6 6",
|
|
27
|
+
stroke: "currentColor",
|
|
28
|
+
strokeWidth: 2,
|
|
29
|
+
strokeLinecap: "round",
|
|
30
|
+
strokeLinejoin: "round"
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
function rowTitle(thread) {
|
|
37
|
+
return thread.outcome?.title ?? "Untitled thread";
|
|
38
|
+
}
|
|
39
|
+
function ThreadFeedRow(props) {
|
|
40
|
+
const {
|
|
41
|
+
thread,
|
|
42
|
+
expanded,
|
|
43
|
+
onToggle,
|
|
44
|
+
liveParts,
|
|
45
|
+
liveStatus,
|
|
46
|
+
renderers,
|
|
47
|
+
fallbackRenderer,
|
|
48
|
+
affordances,
|
|
49
|
+
groupConsecutive,
|
|
50
|
+
onSend,
|
|
51
|
+
onStop,
|
|
52
|
+
composerProps
|
|
53
|
+
} = props;
|
|
54
|
+
const virtualized = resolveVirtualize(props);
|
|
55
|
+
const virtualizeProps = virtualized === null ? {} : { virtualize: virtualized.options, height: virtualized.height };
|
|
56
|
+
const [hasOpened, setHasOpened] = useState(expanded);
|
|
57
|
+
if (expanded && !hasOpened) {
|
|
58
|
+
setHasOpened(true);
|
|
59
|
+
}
|
|
60
|
+
const summary = thread.outcome?.summary ?? "";
|
|
61
|
+
const streaming = liveStatus === "streaming";
|
|
62
|
+
return /* @__PURE__ */ jsxs(
|
|
63
|
+
Box,
|
|
64
|
+
{
|
|
65
|
+
style: {
|
|
66
|
+
borderRadius: VX.radiusCard,
|
|
67
|
+
boxShadow: VX.shadowCard,
|
|
68
|
+
backgroundColor: VX.surface.panel,
|
|
69
|
+
overflow: "hidden"
|
|
70
|
+
},
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
UnstyledButton,
|
|
74
|
+
{
|
|
75
|
+
onClick: () => onToggle(thread.id),
|
|
76
|
+
w: "100%",
|
|
77
|
+
"aria-expanded": expanded,
|
|
78
|
+
style: {
|
|
79
|
+
display: "block",
|
|
80
|
+
padding: "var(--mantine-spacing-xs) var(--mantine-spacing-sm)"
|
|
81
|
+
},
|
|
82
|
+
children: /* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", wrap: "nowrap", align: "center", children: [
|
|
83
|
+
/* @__PURE__ */ jsxs(Stack, { gap: 2, style: { flex: 1, minWidth: 0 }, children: [
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
Text,
|
|
86
|
+
{
|
|
87
|
+
fw: 550,
|
|
88
|
+
lineClamp: 1,
|
|
89
|
+
style: {
|
|
90
|
+
fontFamily: "var(--basalt-font-head)",
|
|
91
|
+
fontSize: VX.text.md,
|
|
92
|
+
fontStretch: "88%"
|
|
93
|
+
},
|
|
94
|
+
children: rowTitle(thread)
|
|
95
|
+
}
|
|
96
|
+
),
|
|
97
|
+
summary.length > 0 && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", lineClamp: 1, children: summary })
|
|
98
|
+
] }),
|
|
99
|
+
/* @__PURE__ */ jsx(
|
|
100
|
+
Text,
|
|
101
|
+
{
|
|
102
|
+
style: {
|
|
103
|
+
fontFamily: "var(--basalt-font-mono)",
|
|
104
|
+
fontSize: VX.text.micro,
|
|
105
|
+
color: VX.faint,
|
|
106
|
+
flexShrink: 0
|
|
107
|
+
},
|
|
108
|
+
children: formatRelativeTime(thread.updatedAt)
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
/* @__PURE__ */ jsx(ChevronGlyph, { expanded })
|
|
112
|
+
] })
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
hasOpened && /* @__PURE__ */ jsx(
|
|
116
|
+
Box,
|
|
117
|
+
{
|
|
118
|
+
"data-testid": "thread-feed-row-body",
|
|
119
|
+
style: {
|
|
120
|
+
display: expanded ? "block" : "none",
|
|
121
|
+
padding: "var(--mantine-spacing-sm)",
|
|
122
|
+
paddingTop: 0
|
|
123
|
+
},
|
|
124
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
ThreadTranscript,
|
|
127
|
+
{
|
|
128
|
+
messages: thread.messages,
|
|
129
|
+
...liveParts !== void 0 ? { liveParts } : {},
|
|
130
|
+
...liveStatus !== void 0 ? { liveStatus } : {},
|
|
131
|
+
...renderers !== void 0 ? { renderers } : {},
|
|
132
|
+
...fallbackRenderer !== void 0 ? { fallbackRenderer } : {},
|
|
133
|
+
...affordances !== void 0 ? { affordances } : {},
|
|
134
|
+
...groupConsecutive !== void 0 ? { groupConsecutive } : {},
|
|
135
|
+
...virtualizeProps
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
/* @__PURE__ */ jsx(
|
|
139
|
+
Composer,
|
|
140
|
+
{
|
|
141
|
+
onSubmit: onSend,
|
|
142
|
+
streaming,
|
|
143
|
+
...onStop !== void 0 ? { onStop } : {},
|
|
144
|
+
...composerProps
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
] })
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
export {
|
|
155
|
+
ThreadFeedRow
|
|
156
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-feed-row.test.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-feed-row.test.tsx"],"names":[],"mappings":""}
|
|
@@ -1,18 +1,47 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
1
|
+
import type { JSX, ReactNode } from 'react';
|
|
2
2
|
import type { AgentThread } from '../agent/index.js';
|
|
3
|
+
import type { ComposerSubmit } from './composer.js';
|
|
3
4
|
export type ThreadFeedProps = {
|
|
4
5
|
readonly threads: AgentThread[];
|
|
5
6
|
/** The currently open thread id, or null when none is selected. */
|
|
6
7
|
readonly activeId: string | null;
|
|
7
|
-
/** Called with a thread's id when its row is selected.
|
|
8
|
+
/** Called with a thread's id when its row is selected. Never called to report a collapse — see
|
|
9
|
+
* `variant`'s doc for how the built-in `'inline'` row's expand/collapse is decoupled from this. */
|
|
8
10
|
readonly onSelect: (id: string) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Row shape: `'outcome'` (default) renders today's `ThreadOutcomeCard` inbox row, unchanged.
|
|
13
|
+
* `'inline'` renders `ThreadFeedRow`, the Slack-style inline-expanding row, wired to this feed's
|
|
14
|
+
* own `activeId` for which row is open (so at most one row is open at a time, mirroring the
|
|
15
|
+
* inbox's single-selection model) — with its OWN collapse tracking layered on top, not a raw
|
|
16
|
+
* pass-through of `onSelect` as `onToggle`. `onSelect` is documented as a selection event, and
|
|
17
|
+
* collapsing a row that remains the active thread isn't one; wiring `onToggle={onSelect}`
|
|
18
|
+
* directly (the previous shape) meant a plain `onSelect={setActiveId}` consumer could never
|
|
19
|
+
* collapse a row, since re-selecting the same id is a no-op state update. Collapsing here is
|
|
20
|
+
* local, UI-only state, reset whenever `activeId` changes to point at a DIFFERENT thread. Since
|
|
21
|
+
* this reset is a `useEffect` keyed on `activeId`, re-driving it to the SAME id from outside
|
|
22
|
+
* this component is a no-op React dep comparison, so it does not clear the override.
|
|
23
|
+
*/
|
|
24
|
+
readonly variant?: 'outcome' | 'inline';
|
|
25
|
+
/**
|
|
26
|
+
* Wires the built-in `'inline'` row's composer to a real send channel — called with the thread
|
|
27
|
+
* and the composer's submit payload. Omit it and the row's composer renders disabled (visibly
|
|
28
|
+
* inert) instead of a live control that silently discards whatever is typed into it; a consumer
|
|
29
|
+
* that needs the composer live must supply this (or `renderRow`, for full control over the row).
|
|
30
|
+
*/
|
|
31
|
+
readonly onSend?: (thread: AgentThread, payload: ComposerSubmit) => void;
|
|
32
|
+
/** Overrides row rendering entirely, for BOTH variants — when supplied, this is called for every
|
|
33
|
+
* thread instead of either built-in row. Gives full control over live-run wiring
|
|
34
|
+
* (`onStop`/`liveParts`/`liveStatus`) the built-in `'inline'` row doesn't expose; see `variant`
|
|
35
|
+
* and `onSend`. */
|
|
36
|
+
readonly renderRow?: (thread: AgentThread) => ReactNode;
|
|
9
37
|
};
|
|
10
38
|
/**
|
|
11
|
-
* A scrollable feed of thread rows, animated on add/remove/reorder.
|
|
12
|
-
* `
|
|
39
|
+
* A scrollable feed of thread rows, animated on add/remove/reorder. Row content comes from
|
|
40
|
+
* `renderRow` when supplied, else from the built-in `variant` row (`ThreadOutcomeCard` or
|
|
41
|
+
* `ThreadFeedRow`) — see `ThreadFeedProps` for the full contract.
|
|
13
42
|
*
|
|
14
43
|
* @example
|
|
15
44
|
* <ThreadFeed threads={threads} activeId={activeId} onSelect={(id) => select(id)} />
|
|
16
45
|
*/
|
|
17
|
-
export declare function ThreadFeed({ threads, activeId, onSelect }: ThreadFeedProps): JSX.Element;
|
|
46
|
+
export declare function ThreadFeed({ threads, activeId, onSelect, variant, onSend, renderRow, }: ThreadFeedProps): JSX.Element;
|
|
18
47
|
//# sourceMappingURL=thread-feed.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread-feed.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-feed.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"thread-feed.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-feed.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAShD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,CAAA;IAC/B,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC;uGACmG;IACnG,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,KAAK,IAAI,CAAA;IACxE;;;uBAGmB;IACnB,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,SAAS,CAAA;CACxD,CAAA;AAgCD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,OAAmB,EACnB,MAAM,EACN,SAAS,GACV,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CAkE/B"}
|
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { ScrollArea, Stack } from "@mantine/core";
|
|
2
|
+
import { Box, ScrollArea, Stack } from "@mantine/core";
|
|
3
3
|
import { useReducedMotion } from "@mantine/hooks";
|
|
4
4
|
import { AnimatePresence, motion } from "motion/react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
5
6
|
import { MOTION_SPRING } from "../motion/index.js";
|
|
7
|
+
import { ThreadFeedRow } from "./thread-feed-row.js";
|
|
6
8
|
import { ThreadOutcomeCard } from "./thread-outcome-card.js";
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
function noopSend() {
|
|
10
|
+
}
|
|
11
|
+
function defaultRow(thread, variant, activeId, collapsedId, onToggle, onSelect, onSend) {
|
|
12
|
+
if (variant === "inline") {
|
|
13
|
+
const expanded = thread.id === activeId && thread.id !== collapsedId;
|
|
14
|
+
return /* @__PURE__ */ jsx(
|
|
15
|
+
ThreadFeedRow,
|
|
12
16
|
{
|
|
13
17
|
thread,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
expanded,
|
|
19
|
+
onToggle,
|
|
20
|
+
onSend: onSend !== void 0 ? (payload) => onSend(thread, payload) : noopSend,
|
|
21
|
+
...onSend === void 0 ? { composerProps: { disabled: true } } : {}
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return /* @__PURE__ */ jsx(
|
|
26
|
+
ThreadOutcomeCard,
|
|
27
|
+
{
|
|
28
|
+
thread,
|
|
29
|
+
selected: thread.id === activeId,
|
|
30
|
+
onSelect: () => onSelect(thread.id)
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function ThreadFeed({
|
|
35
|
+
threads,
|
|
36
|
+
activeId,
|
|
37
|
+
onSelect,
|
|
38
|
+
variant = "outcome",
|
|
39
|
+
onSend,
|
|
40
|
+
renderRow
|
|
41
|
+
}) {
|
|
42
|
+
const reduceMotion = useReducedMotion();
|
|
43
|
+
const [collapsedId, setCollapsedId] = useState(null);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setCollapsedId(null);
|
|
46
|
+
}, [activeId]);
|
|
47
|
+
function handleInlineToggle(id) {
|
|
48
|
+
const isOpen = id === activeId && id !== collapsedId;
|
|
49
|
+
if (isOpen) {
|
|
50
|
+
setCollapsedId(id);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setCollapsedId(null);
|
|
54
|
+
onSelect(id);
|
|
55
|
+
}
|
|
56
|
+
const rowFor = (thread) => renderRow !== void 0 ? renderRow(thread) : defaultRow(thread, variant, activeId, collapsedId, handleInlineToggle, onSelect, onSend);
|
|
57
|
+
if (reduceMotion) {
|
|
58
|
+
return /* @__PURE__ */ jsx(ScrollArea, { style: { height: "100%" }, children: /* @__PURE__ */ jsx(Stack, { gap: "sm", p: "sm", children: threads.map((thread) => /* @__PURE__ */ jsx(Box, { children: rowFor(thread) }, thread.id)) }) });
|
|
19
59
|
}
|
|
20
60
|
return /* @__PURE__ */ jsx(ScrollArea, { style: { height: "100%" }, children: /* @__PURE__ */ jsx(Stack, { gap: "sm", p: "sm", children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "popLayout", initial: false, children: threads.map((thread) => /* @__PURE__ */ jsx(
|
|
21
61
|
motion.div,
|
|
@@ -25,14 +65,7 @@ function ThreadFeed({ threads, activeId, onSelect }) {
|
|
|
25
65
|
animate: { opacity: 1, y: 0 },
|
|
26
66
|
exit: { opacity: 0, scale: 0.97 },
|
|
27
67
|
transition: MOTION_SPRING,
|
|
28
|
-
children:
|
|
29
|
-
ThreadOutcomeCard,
|
|
30
|
-
{
|
|
31
|
-
thread,
|
|
32
|
-
selected: thread.id === activeId,
|
|
33
|
-
onSelect: () => onSelect(thread.id)
|
|
34
|
-
}
|
|
35
|
-
)
|
|
68
|
+
children: rowFor(thread)
|
|
36
69
|
},
|
|
37
70
|
thread.id
|
|
38
71
|
)) }) }) });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-feed.test.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-feed.test.tsx"],"names":[],"mappings":""}
|