basalt-ui 1.12.0 → 1.14.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.
Files changed (123) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +38 -35
  4. package/agent/rules/basalt-agent.md +14 -0
  5. package/configs/oxlint-plugin.js +7 -3
  6. package/configs/oxlint-plugin.test.ts +34 -5
  7. package/configs/oxlint.json +4 -4
  8. package/dist/agent/adapter.d.ts +5 -1
  9. package/dist/agent/adapter.d.ts.map +1 -1
  10. package/dist/agent/adapter.js +22 -13
  11. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  12. package/dist/agent/ai-sdk-transport.js +6 -2
  13. package/dist/agent/id.d.ts +67 -19
  14. package/dist/agent/id.d.ts.map +1 -1
  15. package/dist/agent/id.js +21 -7
  16. package/dist/agent/thread.d.ts +4 -0
  17. package/dist/agent/thread.d.ts.map +1 -1
  18. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  19. package/dist/agent/use-agent-stream.js +2 -2
  20. package/dist/agent/use-agent-thread-runs.d.ts +24 -3
  21. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  22. package/dist/agent/use-agent-thread-runs.js +55 -15
  23. package/dist/agent-chat/index.d.ts +4 -0
  24. package/dist/agent-chat/index.d.ts.map +1 -1
  25. package/dist/agent-chat/index.js +2 -0
  26. package/dist/agent-chat/message-affordances.d.ts +59 -0
  27. package/dist/agent-chat/message-affordances.d.ts.map +1 -0
  28. package/dist/agent-chat/message-affordances.js +7 -0
  29. package/dist/agent-chat/relative-time.d.ts +27 -0
  30. package/dist/agent-chat/relative-time.d.ts.map +1 -0
  31. package/dist/agent-chat/relative-time.js +27 -0
  32. package/dist/agent-chat/relative-time.test.d.ts +2 -0
  33. package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
  34. package/dist/agent-chat/thread-feed-row.d.ts +65 -0
  35. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
  36. package/dist/agent-chat/thread-feed-row.js +156 -0
  37. package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
  38. package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
  39. package/dist/agent-chat/thread-feed.d.ts +34 -5
  40. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  41. package/dist/agent-chat/thread-feed.js +52 -19
  42. package/dist/agent-chat/thread-feed.test.d.ts +2 -0
  43. package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
  44. package/dist/agent-chat/thread-message.d.ts +183 -2
  45. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  46. package/dist/agent-chat/thread-message.js +315 -34
  47. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  48. package/dist/agent-chat/thread-outcome-card.js +1 -16
  49. package/dist/agent-chat/thread-workspace.d.ts +14 -2
  50. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  51. package/dist/agent-chat/thread-workspace.js +29 -3
  52. package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
  53. package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
  54. package/dist/agent-chat/virtualize.d.ts +123 -0
  55. package/dist/agent-chat/virtualize.d.ts.map +1 -0
  56. package/dist/agent-chat/virtualize.js +9 -0
  57. package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
  58. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
  59. package/dist/data/index.d.ts +5 -1
  60. package/dist/data/index.d.ts.map +1 -1
  61. package/dist/data/virtual-list.d.ts.map +1 -1
  62. package/dist/data/virtual.d.ts +2 -1
  63. package/dist/data/virtual.d.ts.map +1 -1
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -0
  67. package/dist/shell/app-sidebar.d.ts +19 -1
  68. package/dist/shell/app-sidebar.d.ts.map +1 -1
  69. package/dist/shell/app-sidebar.js +30 -26
  70. package/dist/shell/app-sidebar.module.css +11 -0
  71. package/dist/shell/app-sidebar.module.css.d.ts +1 -0
  72. package/dist/shell/app-sidebar.test.d.ts +2 -0
  73. package/dist/shell/app-sidebar.test.d.ts.map +1 -0
  74. package/dist/shell/index.d.ts +10 -1
  75. package/dist/shell/index.d.ts.map +1 -1
  76. package/dist/shell/index.js +2 -0
  77. package/dist/shell/index.test.d.ts +2 -0
  78. package/dist/shell/index.test.d.ts.map +1 -0
  79. package/dist/surfaces.d.ts +2 -2
  80. package/dist/surfaces.d.ts.map +1 -1
  81. package/dist/surfaces.js +3 -2
  82. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  83. package/llms.txt +6 -6
  84. package/package.json +2 -2
  85. package/src/agent/adapter.test.ts +237 -0
  86. package/src/agent/adapter.ts +72 -14
  87. package/src/agent/ai-sdk-transport.test.ts +64 -0
  88. package/src/agent/ai-sdk-transport.ts +8 -2
  89. package/src/agent/id.test.ts +95 -1
  90. package/src/agent/id.ts +98 -28
  91. package/src/agent/thread.ts +4 -0
  92. package/src/agent/use-agent-stream.test.tsx +22 -0
  93. package/src/agent/use-agent-stream.ts +6 -3
  94. package/src/agent/use-agent-thread-runs.test.tsx +326 -0
  95. package/src/agent/use-agent-thread-runs.ts +159 -23
  96. package/src/agent/use-agent-thread-runs.wedge.test.tsx +80 -0
  97. package/src/agent-chat/index.ts +12 -0
  98. package/src/agent-chat/message-affordances.ts +61 -0
  99. package/src/agent-chat/relative-time.test.ts +95 -0
  100. package/src/agent-chat/relative-time.ts +55 -0
  101. package/src/agent-chat/thread-feed-row.test.tsx +510 -0
  102. package/src/agent-chat/thread-feed-row.tsx +274 -0
  103. package/src/agent-chat/thread-feed.test.tsx +241 -0
  104. package/src/agent-chat/thread-feed.tsx +119 -18
  105. package/src/agent-chat/thread-message.test.tsx +937 -3
  106. package/src/agent-chat/thread-message.tsx +766 -38
  107. package/src/agent-chat/thread-outcome-card.tsx +1 -25
  108. package/src/agent-chat/thread-workspace.test.tsx +169 -0
  109. package/src/agent-chat/thread-workspace.tsx +69 -5
  110. package/src/agent-chat/virtualize.ts +132 -0
  111. package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
  112. package/src/data/index.ts +5 -1
  113. package/src/data/virtual-list.tsx +2 -1
  114. package/src/data/virtual.ts +2 -1
  115. package/src/index.ts +5 -0
  116. package/src/shell/app-sidebar.module.css +11 -0
  117. package/src/shell/app-sidebar.module.css.d.ts +1 -0
  118. package/src/shell/app-sidebar.test.tsx +98 -0
  119. package/src/shell/app-sidebar.tsx +22 -0
  120. package/src/shell/index.test.tsx +35 -0
  121. package/src/shell/index.tsx +11 -0
  122. package/src/surfaces.ts +2 -1
  123. 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,7 @@
1
+ const DEFAULT_AFFORDANCES = {
2
+ timestamp: "relative",
3
+ copy: true
4
+ };
5
+ export {
6
+ DEFAULT_AFFORDANCES
7
+ };
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=relative-time.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=thread-feed-row.test.d.ts.map
@@ -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. Each row renders via
12
- * `ThreadOutcomeCard`; selection highlighting is derived from `activeId`.
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":"AAgBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAI3C,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,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CA4CxF"}
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 ThreadFeed({ threads, activeId, onSelect }) {
8
- const reduceMotion = useReducedMotion();
9
- if (reduceMotion) {
10
- return /* @__PURE__ */ jsx(ScrollArea, { style: { height: "100%" }, children: /* @__PURE__ */ jsx(Stack, { gap: "sm", p: "sm", children: threads.map((thread) => /* @__PURE__ */ jsx(
11
- ThreadOutcomeCard,
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
- selected: thread.id === activeId,
15
- onSelect: () => onSelect(thread.id)
16
- },
17
- thread.id
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: /* @__PURE__ */ jsx(
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=thread-feed.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread-feed.test.d.ts","sourceRoot":"","sources":["../../src/agent-chat/thread-feed.test.tsx"],"names":[],"mappings":""}