@assistant-ui/react-ink 0.0.15 → 0.0.16

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/README.md CHANGED
@@ -1,74 +1,72 @@
1
- # @assistant-ui/react-ink
1
+ # `@assistant-ui/react-ink`
2
2
 
3
- React Ink (terminal UI) bindings for [assistant-ui](https://www.assistant-ui.com/).
3
+ [![npm version](https://img.shields.io/npm/v/@assistant-ui/react-ink)](https://www.npmjs.com/package/@assistant-ui/react-ink)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@assistant-ui/react-ink)](https://www.npmjs.com/package/@assistant-ui/react-ink)
5
+ [![GitHub stars](https://img.shields.io/github/stars/assistant-ui/assistant-ui)](https://github.com/assistant-ui/assistant-ui)
6
+ ![License](https://img.shields.io/npm/l/@assistant-ui/react-ink)
4
7
 
5
- Build AI chat interfaces for the terminal using [Ink](https://github.com/vadimdemedes/ink) React for CLIs powered by the same runtime as assistant-ui.
8
+ [Ink](https://github.com/vadimdemedes/ink) bindings for assistant-ui. Composable, unstyled terminal primitives that share the same runtime, adapters, and tools as `@assistant-ui/react`, so you can ship a CLI chat UI without rewriting your backend code.
6
9
 
7
10
  ## Installation
8
11
 
9
- ```sh
12
+ Requires React 19 and ink 6 or newer.
13
+
14
+ ```bash
10
15
  npm install @assistant-ui/react-ink ink react
11
16
  ```
12
17
 
13
- ## Quick Start
18
+ For markdown rendering with syntax highlighting, also install [`@assistant-ui/react-ink-markdown`](https://www.npmjs.com/package/@assistant-ui/react-ink-markdown).
19
+
20
+ ## Usage
14
21
 
15
22
  ```tsx
16
- import { render } from "ink";
17
- import { Box, Text } from "ink";
23
+ import { render, Box, Text } from "ink";
18
24
  import {
19
- AssistantProvider,
25
+ AssistantRuntimeProvider,
20
26
  useLocalRuntime,
21
- ThreadRoot,
22
- ThreadMessages,
23
- ComposerInput,
27
+ ThreadPrimitive,
28
+ ComposerPrimitive,
29
+ useAuiState,
24
30
  type ChatModelAdapter,
25
31
  } from "@assistant-ui/react-ink";
26
32
 
27
- const myAdapter: ChatModelAdapter = {
33
+ const adapter: ChatModelAdapter = {
28
34
  async *run({ messages }) {
29
- // your AI backend here
30
35
  yield { content: [{ type: "text", text: "Hello from AI!" }] };
31
36
  },
32
37
  };
33
38
 
34
- function App() {
35
- const runtime = useLocalRuntime(myAdapter);
39
+ const Message = () => {
40
+ const message = useAuiState((s) => s.message);
41
+ const text = message.content.find((p) => p.type === "text")?.text ?? "";
42
+ return (
43
+ <Box marginBottom={1}>
44
+ <Text color={message.role === "user" ? "green" : "blue"}>
45
+ {message.role === "user" ? "You: " : "AI: "}
46
+ </Text>
47
+ <Text>{text}</Text>
48
+ </Box>
49
+ );
50
+ };
36
51
 
52
+ const App = () => {
53
+ const runtime = useLocalRuntime(adapter);
37
54
  return (
38
- <AssistantProvider runtime={runtime}>
39
- <ThreadRoot>
40
- <ThreadMessages
41
- renderMessage={({ message }) => (
42
- <Box marginBottom={1}>
43
- <Text>
44
- {message.content
45
- .filter((p) => p.type === "text")
46
- .map((p) => p.text)
47
- .join("")}
48
- </Text>
49
- </Box>
50
- )}
51
- />
52
- <ComposerInput submitOnEnter placeholder="Message..." autoFocus />
53
- </ThreadRoot>
54
- </AssistantProvider>
55
+ <AssistantRuntimeProvider runtime={runtime}>
56
+ <ThreadPrimitive.Root>
57
+ <ThreadPrimitive.Messages>{() => <Message />}</ThreadPrimitive.Messages>
58
+ <Box borderStyle="round" paddingX={1}>
59
+ <Text>{"> "}</Text>
60
+ <ComposerPrimitive.Input submitOnEnter placeholder="Message..." autoFocus />
61
+ </Box>
62
+ </ThreadPrimitive.Root>
63
+ </AssistantRuntimeProvider>
55
64
  );
56
- }
65
+ };
57
66
 
58
67
  render(<App />);
59
68
  ```
60
69
 
61
- ## Features
62
-
63
- - Composable, unstyled primitives (Thread, Composer, Message, ActionBar, etc.)
64
- - Streaming responses with real-time updates
65
- - Tool call support with built-in ToolFallback component
66
- - Diff rendering with DiffPrimitive components and DiffView for unified diffs and file comparisons in the terminal
67
- - Message branching and editing
68
- - Multi-thread support with thread list management
69
- - Markdown rendering via `@assistant-ui/react-ink-markdown`
70
- - Same runtime as `@assistant-ui/react` — share adapters, tools, and backend code
71
-
72
70
  ## Documentation
73
71
 
74
72
  - [Getting Started](https://www.assistant-ui.com/docs/ink)
@@ -76,12 +74,7 @@ render(<App />);
76
74
  - [Primitives](https://www.assistant-ui.com/docs/ink/primitives)
77
75
  - [Hooks](https://www.assistant-ui.com/docs/ink/hooks)
78
76
 
79
- ## Related Packages
80
-
81
- - [`@assistant-ui/react-ink-markdown`](https://www.npmjs.com/package/@assistant-ui/react-ink-markdown) — Terminal markdown rendering with syntax highlighting
82
- - [`@assistant-ui/react`](https://www.npmjs.com/package/@assistant-ui/react) — Web (React) bindings
83
- - [`@assistant-ui/react-native`](https://www.npmjs.com/package/@assistant-ui/react-native) — React Native bindings
84
-
85
- ## License
77
+ ## For other platforms
86
78
 
87
- MIT
79
+ - Web: [`@assistant-ui/react`](https://www.npmjs.com/package/@assistant-ui/react)
80
+ - React Native: [`@assistant-ui/react-native`](https://www.npmjs.com/package/@assistant-ui/react-native)
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference types="@assistant-ui/core/react" />
2
- export type { ThreadMessage, ThreadUserMessage, ThreadAssistantMessage, ThreadSystemMessage, MessageStatus, MessageRole, ThreadMessageLike, AppendMessage, RunConfig, TextMessagePart, ReasoningMessagePart, SourceMessagePart, ToolCallMessagePart, ImageMessagePart, FileMessagePart, DataMessagePart, Unstable_AudioMessagePart, ThreadUserMessagePart, ThreadAssistantMessagePart, AssistantRuntime, ThreadRuntime, MessageRuntime, ThreadComposerRuntime, EditComposerRuntime, ComposerRuntime, ThreadListRuntime, ThreadListItemRuntime, ChatModelAdapter, ChatModelRunOptions, ChatModelRunResult, RuntimeCapabilities, Attachment, PendingAttachment, CompleteAttachment, CreateAttachment, AttachmentRuntime, AttachmentAdapter, ThreadHistoryAdapter, FeedbackAdapter, RealtimeVoiceAdapter, VoiceSessionControls, VoiceSessionHelpers, SuggestionAdapter, Unsubscribe, } from "@assistant-ui/core";
2
+ export type { ThreadMessage, ThreadUserMessage, ThreadAssistantMessage, ThreadSystemMessage, MessageStatus, MessageRole, ThreadMessageLike, AppendMessage, RunConfig, TextMessagePart, ReasoningMessagePart, SourceMessagePart, ToolCallMessagePart, ToolModelContentPart, ImageMessagePart, FileMessagePart, DataMessagePart, Unstable_AudioMessagePart, ThreadUserMessagePart, ThreadAssistantMessagePart, AssistantRuntime, ThreadRuntime, MessageRuntime, ThreadComposerRuntime, EditComposerRuntime, ComposerRuntime, ThreadListRuntime, ThreadListItemRuntime, ChatModelAdapter, ChatModelRunOptions, ChatModelRunResult, RuntimeCapabilities, Attachment, PendingAttachment, CompleteAttachment, CreateAttachment, AttachmentRuntime, AttachmentAdapter, ThreadHistoryAdapter, FeedbackAdapter, RealtimeVoiceAdapter, VoiceSessionControls, VoiceSessionHelpers, SuggestionAdapter, Unsubscribe, } from "@assistant-ui/core";
3
3
  export type { RemoteThreadListAdapter, RemoteThreadListOptions, } from "@assistant-ui/core";
4
4
  export { InMemoryThreadListAdapter } from "@assistant-ui/core";
5
5
  export { createVoiceSession } from "@assistant-ui/core";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kDAAkD;AA8DlD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,qCAAqC;AACrC,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAY5B,6BAA6B;AAC7B,OAAO,EACL,MAAM,EACN,WAAW,EACX,WAAW,EACX,WAAW,EACX,KAAK,GAQN,MAAM,qBAAqB,CAAC;AAE7B,oBAAoB;AACpB,OAAO,EAAE,wBAAwB,EAAE,sCAAmC;AACtE,OAAO,EACL,sBAAsB,EACtB,kBAAkB,GAEnB,MAAM,0BAA0B,CAAC;AAElC,UAAU;AACV,OAAO,EACL,eAAe,GAEhB,sCAAmC;AACpC,OAAO,EAAE,0BAA0B,EAAE,iDAA8C;AAEnF,aAAa;AACb,OAAO,KAAK,eAAe,+BAA4B;AACvD,OAAO,KAAK,iBAAiB,iCAA8B;AAC3D,OAAO,KAAK,gBAAgB,gCAA6B;AACzD,OAAO,KAAK,mBAAmB,mCAAgC;AAC/D,OAAO,KAAK,kBAAkB,kCAA+B;AAC7D,OAAO,KAAK,qBAAqB,qCAAkC;AACnE,OAAO,KAAK,mBAAmB,mCAAgC;AAC/D,OAAO,KAAK,uBAAuB,uCAAoC;AACvE,OAAO,KAAK,uBAAuB,uCAAoC;AACvE,OAAO,KAAK,mBAAmB,mCAAgC;AAC/D,OAAO,KAAK,iBAAiB,iCAA8B;AAC3D,OAAO,KAAK,cAAc,oCAA2B;AACrD,OAAO,KAAK,aAAa,6BAA0B;AACnD,OAAO,EAAE,QAAQ,EAAsB,sCAAmC;AAE1E,6CAA6C;AAC7C,OAAO,EACL,6BAA6B,EAC7B,+BAA+B,EAC/B,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,iCAAiC,EACjC,yBAAyB,GAC1B,MAAM,0BAA0B,CAAC;AAElC,iCAAiC;AACjC,OAAO,EACL,iBAAiB,EAEjB,mBAAmB,EAEnB,mBAAmB,EAEnB,gBAAgB,EAEhB,kBAAkB,EAElB,kBAAkB,EAElB,wBAAwB,EACxB,mBAAmB,EAEnB,eAAe,EAGf,KAAK,EACL,aAAa,EACb,aAAa,EACb,wBAAwB,EAExB,oBAAoB,EACpB,iBAAiB,GAElB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAyB,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAO1D,iBAAiB;AACjB,OAAO,EAAE,YAAY,IAAI,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AA0BhE,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kDAAkD;AA+DlD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,qCAAqC;AACrC,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAY5B,6BAA6B;AAC7B,OAAO,EACL,MAAM,EACN,WAAW,EACX,WAAW,EACX,WAAW,EACX,KAAK,GAQN,MAAM,qBAAqB,CAAC;AAE7B,oBAAoB;AACpB,OAAO,EAAE,wBAAwB,EAAE,sCAAmC;AACtE,OAAO,EACL,sBAAsB,EACtB,kBAAkB,GAEnB,MAAM,0BAA0B,CAAC;AAElC,UAAU;AACV,OAAO,EACL,eAAe,GAEhB,sCAAmC;AACpC,OAAO,EAAE,0BAA0B,EAAE,iDAA8C;AAEnF,aAAa;AACb,OAAO,KAAK,eAAe,+BAA4B;AACvD,OAAO,KAAK,iBAAiB,iCAA8B;AAC3D,OAAO,KAAK,gBAAgB,gCAA6B;AACzD,OAAO,KAAK,mBAAmB,mCAAgC;AAC/D,OAAO,KAAK,kBAAkB,kCAA+B;AAC7D,OAAO,KAAK,qBAAqB,qCAAkC;AACnE,OAAO,KAAK,mBAAmB,mCAAgC;AAC/D,OAAO,KAAK,uBAAuB,uCAAoC;AACvE,OAAO,KAAK,uBAAuB,uCAAoC;AACvE,OAAO,KAAK,mBAAmB,mCAAgC;AAC/D,OAAO,KAAK,iBAAiB,iCAA8B;AAC3D,OAAO,KAAK,cAAc,oCAA2B;AACrD,OAAO,KAAK,aAAa,6BAA0B;AACnD,OAAO,EAAE,QAAQ,EAAsB,sCAAmC;AAE1E,6CAA6C;AAC7C,OAAO,EACL,6BAA6B,EAC7B,+BAA+B,EAC/B,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,iCAAiC,EACjC,yBAAyB,GAC1B,MAAM,0BAA0B,CAAC;AAElC,iCAAiC;AACjC,OAAO,EACL,iBAAiB,EAEjB,mBAAmB,EAEnB,mBAAmB,EAEnB,gBAAgB,EAEhB,kBAAkB,EAElB,kBAAkB,EAElB,wBAAwB,EACxB,mBAAmB,EAEnB,eAAe,EAGf,KAAK,EACL,aAAa,EACb,aAAa,EACb,wBAAwB,EAExB,oBAAoB,EACpB,iBAAiB,GAElB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAyB,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAO1D,iBAAiB;AACjB,OAAO,EAAE,YAAY,IAAI,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AA0BhE,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ThreadMessage } from "@assistant-ui/core";
3
+ type MemoMessageProps = {
4
+ index: number;
5
+ render: (value: {
6
+ message: ThreadMessage;
7
+ }) => ReactNode;
8
+ };
9
+ export declare const MemoMessage: import("react").MemoExoticComponent<{
10
+ ({ index, render }: MemoMessageProps): import("react/jsx-runtime").JSX.Element;
11
+ displayName: string;
12
+ }>;
13
+ export {};
14
+ //# sourceMappingURL=MemoMessage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MemoMessage.d.ts","sourceRoot":"","sources":["../../../src/primitives/internal/MemoMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAQ,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE,KAAK,SAAS,CAAC;CAC1D,CAAC;AAsBF,eAAO,MAAM,WAAW;wBApBoB,gBAAgB;;EAwB3D,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { memo } from "react";
3
+ import { RenderChildrenWithAccessor } from "@assistant-ui/store";
4
+ import { MessageByIndexProvider } from "@assistant-ui/core/react";
5
+ const MemoMessageImpl = ({ index, render }) => {
6
+ return (_jsx(MessageByIndexProvider, { index: index, children: _jsx(RenderChildrenWithAccessor, { getItemState: (aui) => aui.thread().message({ index }).getState(), children: (getItem) => render({
7
+ get message() {
8
+ return getItem();
9
+ },
10
+ }) }) }));
11
+ };
12
+ MemoMessageImpl.displayName = "ThreadPrimitive.Messages.MemoItem";
13
+ export const MemoMessage = memo(MemoMessageImpl, (prev, next) => prev.index === next.index && Object.is(prev.render, next.render));
14
+ //# sourceMappingURL=MemoMessage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MemoMessage.js","sourceRoot":"","sources":["../../../src/primitives/internal/MemoMessage.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAkB,IAAI,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAOlE,MAAM,eAAe,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAoB,EAAE,EAAE;IAC9D,OAAO,CACL,KAAC,sBAAsB,IAAC,KAAK,EAAE,KAAK,YAClC,KAAC,0BAA0B,IACzB,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE,YAEhE,CAAC,OAAO,EAAE,EAAE,CACX,MAAM,CAAC;gBACL,IAAI,OAAO;oBACT,OAAO,OAAO,EAAE,CAAC;gBACnB,CAAC;aACF,CAAC,GAEuB,GACN,CAC1B,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,CAAC,WAAW,GAAG,mCAAmC,CAAC;AAElE,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAC7B,eAAe,EACf,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CACb,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CACnE,CAAC"}
@@ -19,15 +19,30 @@ type MessageComponents = {
19
19
  AssistantMessage: ComponentType;
20
20
  SystemMessage?: ComponentType | undefined;
21
21
  };
22
- export type ThreadMessagesProps = {
22
+ /**
23
+ * Live render region keeps the last `windowSize + windowOverscan` messages;
24
+ * older messages graduate through Ink's `<Static>` into terminal scrollback
25
+ * and stop repainting. Defaults to no windowing.
26
+ *
27
+ * Per-message memoization only engages when the render callback is
28
+ * referentially stable. The `components` API handles stability internally;
29
+ * with the children render-fn API, hoist or memoize the function.
30
+ */
31
+ type WindowingProps = {
32
+ /** Recent messages kept live. Unset renders all dynamically. Negative clamped to 0. */
33
+ windowSize?: number | undefined;
34
+ /** Extra live messages above the window to absorb boundary churn. Defaults to 4. Negative clamped to 0. */
35
+ windowOverscan?: number | undefined;
36
+ };
37
+ export type ThreadMessagesProps = ({
23
38
  components: MessageComponents;
24
39
  children?: never;
25
- } | {
40
+ } & WindowingProps) | ({
26
41
  children: (value: {
27
42
  message: ThreadMessage;
28
43
  }) => ReactNode;
29
44
  components?: never;
30
- };
45
+ } & WindowingProps);
31
46
  export declare const ThreadMessages: FC<ThreadMessagesProps>;
32
47
  export {};
33
48
  //# sourceMappingURL=ThreadMessages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadMessages.d.ts","sourceRoot":"","sources":["../../../src/primitives/thread/ThreadMessages.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,EAAE,EAAE,KAAK,SAAS,EAAW,MAAM,OAAO,CAAC;AAE7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,KAAK,iBAAiB,GAClB;IACE,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACzC,gBAAgB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC7C,qBAAqB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAClD,kBAAkB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC/C,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACxC,gBAAgB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC7C,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAC3C,GACD;IACE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACzC,gBAAgB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC7C,qBAAqB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAClD,kBAAkB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC/C,WAAW,EAAE,aAAa,CAAC;IAC3B,gBAAgB,EAAE,aAAa,CAAC;IAChC,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAC3C,CAAC;AAEN,MAAM,MAAM,mBAAmB,GAC3B;IACE,UAAU,EAAE,iBAAiB,CAAC;IAC9B,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB,GACD;IACE,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE,KAAK,SAAS,CAAC;IAC3D,UAAU,CAAC,EAAE,KAAK,CAAC;CACpB,CAAC;AA4FN,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAkBlD,CAAC"}
1
+ {"version":3,"file":"ThreadMessages.d.ts","sourceRoot":"","sources":["../../../src/primitives/thread/ThreadMessages.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,EAAE,EACP,KAAK,SAAS,EAGf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,KAAK,iBAAiB,GAClB;IACE,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACzC,gBAAgB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC7C,qBAAqB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAClD,kBAAkB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC/C,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACxC,gBAAgB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC7C,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAC3C,GACD;IACE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACzC,gBAAgB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC7C,qBAAqB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAClD,kBAAkB,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC/C,WAAW,EAAE,aAAa,CAAC;IAC3B,gBAAgB,EAAE,aAAa,CAAC;IAChC,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAC3C,CAAC;AAEN;;;;;;;;GAQG;AACH,KAAK,cAAc,GAAG;IACpB,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,2GAA2G;IAC3G,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC3B,CAAC;IACC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB,GAAG,cAAc,CAAC,GACnB,CAAC;IACC,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE,KAAK,SAAS,CAAC;IAC3D,UAAU,CAAC,EAAE,KAAK,CAAC;CACpB,GAAG,cAAc,CAAC,CAAC;AA8GxB,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAqElD,CAAC"}
@@ -1,8 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useMemo } from "react";
3
- import { Box } from "ink";
4
- import { RenderChildrenWithAccessor, useAuiState } from "@assistant-ui/store";
5
- import { MessageByIndexProvider } from "@assistant-ui/core/react";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, } from "react";
3
+ import { Box, Static } from "ink";
4
+ import { useAuiState } from "@assistant-ui/store";
5
+ import { MemoMessage } from "../internal/MemoMessage.js";
6
6
  const DEFAULT_SYSTEM_MESSAGE = () => null;
7
7
  const getComponent = (components, role, isEditing) => {
8
8
  switch (role) {
@@ -51,22 +51,64 @@ const ThreadMessageComponent = ({ components, }) => {
51
51
  const Component = getComponent(components, role, isEditing);
52
52
  return _jsx(Component, {});
53
53
  };
54
- const ThreadMessagesInner = ({ children }) => {
54
+ const ThreadMessagesInner = ({ children, windowSize, windowOverscan = 4 }) => {
55
55
  const messagesLength = useAuiState((s) => s.thread.messages.length);
56
- return useMemo(() => {
56
+ const tailStart = windowSize !== undefined
57
+ ? Math.max(0, messagesLength -
58
+ Math.max(0, windowSize) -
59
+ Math.max(0, windowOverscan))
60
+ : 0;
61
+ const prefixIndices = useMemo(() => Array.from({ length: tailStart }, (_, i) => i), [tailStart]);
62
+ const tail = useMemo(() => {
57
63
  if (messagesLength === 0)
58
64
  return null;
59
- return Array.from({ length: messagesLength }, (_, index) => (_jsx(MessageByIndexProvider, { index: index, children: _jsx(RenderChildrenWithAccessor, { getItemState: (aui) => aui.thread().message({ index }).getState(), children: (getItem) => children({
60
- get message() {
61
- return getItem();
62
- },
63
- }) }) }, index)));
64
- }, [messagesLength, children]);
65
+ const items = [];
66
+ for (let index = tailStart; index < messagesLength; index++) {
67
+ items.push(_jsx(MemoMessage, { index: index, render: children }, index));
68
+ }
69
+ return items;
70
+ }, [messagesLength, tailStart, children]);
71
+ if (tailStart === 0)
72
+ return tail;
73
+ return (_jsxs(_Fragment, { children: [_jsx(Static, { items: prefixIndices, children: (index) => _jsx(MemoMessage, { index: index, render: children }, index) }), tail] }));
65
74
  };
66
- export const ThreadMessages = ({ components, children, }) => {
75
+ export const ThreadMessages = ({ components, children, windowSize, windowOverscan, }) => {
76
+ const Message = components?.Message;
77
+ const EditComposer = components?.EditComposer;
78
+ const UserEditComposer = components?.UserEditComposer;
79
+ const AssistantEditComposer = components?.AssistantEditComposer;
80
+ const SystemEditComposer = components?.SystemEditComposer;
81
+ const UserMessage = components?.UserMessage;
82
+ const AssistantMessage = components?.AssistantMessage;
83
+ const SystemMessage = components?.SystemMessage;
84
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `components` excluded so inline literals do not bust the memo; per-field deps cover real changes.
85
+ const stableComponents = useMemo(() => {
86
+ if (!components)
87
+ return undefined;
88
+ return {
89
+ Message,
90
+ EditComposer,
91
+ UserEditComposer,
92
+ AssistantEditComposer,
93
+ SystemEditComposer,
94
+ UserMessage,
95
+ AssistantMessage,
96
+ SystemMessage,
97
+ };
98
+ }, [
99
+ Message,
100
+ EditComposer,
101
+ UserEditComposer,
102
+ AssistantEditComposer,
103
+ SystemEditComposer,
104
+ UserMessage,
105
+ AssistantMessage,
106
+ SystemMessage,
107
+ ]);
108
+ const renderFromComponents = useCallback(() => stableComponents ? (_jsx(ThreadMessageComponent, { components: stableComponents })) : null, [stableComponents]);
67
109
  if (components) {
68
- return (_jsx(Box, { flexDirection: "column", children: _jsx(ThreadMessagesInner, { children: () => _jsx(ThreadMessageComponent, { components: components }) }) }));
110
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(ThreadMessagesInner, { windowSize: windowSize, windowOverscan: windowOverscan, children: renderFromComponents }) }));
69
111
  }
70
- return (_jsx(Box, { flexDirection: "column", children: _jsx(ThreadMessagesInner, { children: children }) }));
112
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(ThreadMessagesInner, { windowSize: windowSize, windowOverscan: windowOverscan, children: children }) }));
71
113
  };
72
114
  //# sourceMappingURL=ThreadMessages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadMessages.js","sourceRoot":"","sources":["../../../src/primitives/thread/ThreadMessages.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ThreadMessages.js","sourceRoot":"","sources":["../../../src/primitives/thread/ThreadMessages.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-ink",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "React Ink (terminal UI) bindings for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -32,10 +32,10 @@
32
32
  ],
33
33
  "sideEffects": false,
34
34
  "dependencies": {
35
- "@assistant-ui/core": "^0.2.0",
35
+ "@assistant-ui/core": "^0.2.1",
36
36
  "@assistant-ui/store": "^0.2.10",
37
37
  "@assistant-ui/tap": "^0.5.11",
38
- "assistant-stream": "^0.3.13",
38
+ "assistant-stream": "^0.3.14",
39
39
  "diff": "^9.0.0",
40
40
  "ink-spinner": "^5.0.0",
41
41
  "parse-diff": "^0.12.0",
@@ -56,12 +56,12 @@
56
56
  "ink": "^7.0.2",
57
57
  "ink-testing-library": "^4.0.0",
58
58
  "react": "^19.2.5",
59
+ "tsx": "^4.21.0",
59
60
  "vitest": "^4.1.5",
60
61
  "@assistant-ui/x-buildutils": "0.0.7"
61
62
  },
62
63
  "publishConfig": {
63
- "access": "public",
64
- "provenance": true
64
+ "access": "public"
65
65
  },
66
66
  "homepage": "https://www.assistant-ui.com/",
67
67
  "repository": {
@@ -75,6 +75,7 @@
75
75
  "scripts": {
76
76
  "build": "aui-build",
77
77
  "test": "vitest run",
78
- "test:watch": "vitest"
78
+ "test:watch": "vitest",
79
+ "benchmark": "tsx benchmarks/run.ts"
79
80
  }
80
81
  }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export type {
17
17
  ReasoningMessagePart,
18
18
  SourceMessagePart,
19
19
  ToolCallMessagePart,
20
+ ToolModelContentPart,
20
21
  ImageMessagePart,
21
22
  FileMessagePart,
22
23
  DataMessagePart,
@@ -0,0 +1,35 @@
1
+ import { type ReactNode, memo } from "react";
2
+ import type { ThreadMessage } from "@assistant-ui/core";
3
+ import { RenderChildrenWithAccessor } from "@assistant-ui/store";
4
+ import { MessageByIndexProvider } from "@assistant-ui/core/react";
5
+
6
+ type MemoMessageProps = {
7
+ index: number;
8
+ render: (value: { message: ThreadMessage }) => ReactNode;
9
+ };
10
+
11
+ const MemoMessageImpl = ({ index, render }: MemoMessageProps) => {
12
+ return (
13
+ <MessageByIndexProvider index={index}>
14
+ <RenderChildrenWithAccessor
15
+ getItemState={(aui) => aui.thread().message({ index }).getState()}
16
+ >
17
+ {(getItem) =>
18
+ render({
19
+ get message() {
20
+ return getItem();
21
+ },
22
+ })
23
+ }
24
+ </RenderChildrenWithAccessor>
25
+ </MessageByIndexProvider>
26
+ );
27
+ };
28
+
29
+ MemoMessageImpl.displayName = "ThreadPrimitive.Messages.MemoItem";
30
+
31
+ export const MemoMessage = memo(
32
+ MemoMessageImpl,
33
+ (prev, next) =>
34
+ prev.index === next.index && Object.is(prev.render, next.render),
35
+ );
@@ -0,0 +1,220 @@
1
+ import type { ReactNode } from "react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { cleanup, render } from "ink-testing-library";
4
+ import { Text } from "ink";
5
+
6
+ const hoisted = vi.hoisted(() => ({
7
+ state: { messagesLength: 0 },
8
+ capturedIndices: [] as number[],
9
+ staticItemCounts: [] as number[],
10
+ tailItemCounts: [] as number[],
11
+ }));
12
+
13
+ vi.mock("ink", async (importOriginal) => {
14
+ const actual = await importOriginal<typeof import("ink")>();
15
+ return {
16
+ ...actual,
17
+ Static: <T,>({
18
+ items,
19
+ children,
20
+ }: {
21
+ items: readonly T[];
22
+ children: (item: T, index: number) => ReactNode;
23
+ }) => {
24
+ hoisted.staticItemCounts.push(items.length);
25
+ return <>{items.map((item, i) => children(item, i))}</>;
26
+ },
27
+ };
28
+ });
29
+
30
+ vi.mock("@assistant-ui/store", async (importOriginal) => {
31
+ const actual = await importOriginal<typeof import("@assistant-ui/store")>();
32
+ return {
33
+ ...actual,
34
+ useAuiState: (selector: (s: unknown) => unknown) => {
35
+ const state = {
36
+ thread: { messages: { length: hoisted.state.messagesLength } },
37
+ message: {
38
+ role: "user" as const,
39
+ composer: { isEditing: false },
40
+ parts: [],
41
+ },
42
+ };
43
+ return selector(state);
44
+ },
45
+ RenderChildrenWithAccessor: ({
46
+ children,
47
+ }: {
48
+ children: (getItem: () => unknown) => ReactNode;
49
+ }) => <>{children(() => ({}))}</>,
50
+ };
51
+ });
52
+
53
+ vi.mock("@assistant-ui/core/react", async (importOriginal) => {
54
+ const actual =
55
+ await importOriginal<typeof import("@assistant-ui/core/react")>();
56
+ return {
57
+ ...actual,
58
+ MessageByIndexProvider: ({
59
+ index,
60
+ children,
61
+ }: {
62
+ index: number;
63
+ children: ReactNode;
64
+ }) => {
65
+ hoisted.capturedIndices.push(index);
66
+ return <>{children}</>;
67
+ },
68
+ };
69
+ });
70
+
71
+ import { ThreadPrimitive } from "../../index";
72
+
73
+ beforeEach(() => {
74
+ hoisted.state.messagesLength = 0;
75
+ hoisted.capturedIndices = [];
76
+ hoisted.staticItemCounts = [];
77
+ hoisted.tailItemCounts = [];
78
+ });
79
+
80
+ afterEach(() => {
81
+ cleanup();
82
+ });
83
+
84
+ describe("ThreadPrimitive.Messages", () => {
85
+ it("renders every message when windowSize is undefined", () => {
86
+ hoisted.state.messagesLength = 5;
87
+
88
+ render(
89
+ <ThreadPrimitive.Messages>
90
+ {() => <Text>m</Text>}
91
+ </ThreadPrimitive.Messages>,
92
+ );
93
+
94
+ expect(hoisted.capturedIndices).toEqual([0, 1, 2, 3, 4]);
95
+ expect(hoisted.staticItemCounts).toEqual([]);
96
+ });
97
+
98
+ it("renders nothing when there are no messages", () => {
99
+ hoisted.state.messagesLength = 0;
100
+
101
+ render(
102
+ <ThreadPrimitive.Messages>
103
+ {() => <Text>m</Text>}
104
+ </ThreadPrimitive.Messages>,
105
+ );
106
+
107
+ expect(hoisted.capturedIndices).toEqual([]);
108
+ expect(hoisted.staticItemCounts).toEqual([]);
109
+ });
110
+
111
+ it("keeps every message live when total fits within windowSize + windowOverscan", () => {
112
+ hoisted.state.messagesLength = 5;
113
+
114
+ render(
115
+ <ThreadPrimitive.Messages windowSize={3} windowOverscan={4}>
116
+ {() => <Text>m</Text>}
117
+ </ThreadPrimitive.Messages>,
118
+ );
119
+
120
+ expect(hoisted.capturedIndices).toEqual([0, 1, 2, 3, 4]);
121
+ expect(hoisted.staticItemCounts).toEqual([]);
122
+ });
123
+
124
+ it("splits long threads into a Static prefix plus live tail covering windowSize + windowOverscan", () => {
125
+ hoisted.state.messagesLength = 20;
126
+
127
+ render(
128
+ <ThreadPrimitive.Messages windowSize={3} windowOverscan={4}>
129
+ {() => <Text>m</Text>}
130
+ </ThreadPrimitive.Messages>,
131
+ );
132
+
133
+ expect(hoisted.staticItemCounts.at(-1)).toBe(13);
134
+ expect(hoisted.capturedIndices).toHaveLength(20);
135
+ expect(hoisted.capturedIndices.slice(0, 13)).toEqual(
136
+ Array.from({ length: 13 }, (_, i) => i),
137
+ );
138
+ expect(hoisted.capturedIndices.slice(13)).toEqual([
139
+ 13, 14, 15, 16, 17, 18, 19,
140
+ ]);
141
+ });
142
+
143
+ it("defaults windowOverscan to 4", () => {
144
+ hoisted.state.messagesLength = 7;
145
+
146
+ render(
147
+ <ThreadPrimitive.Messages windowSize={2}>
148
+ {() => <Text>m</Text>}
149
+ </ThreadPrimitive.Messages>,
150
+ );
151
+
152
+ expect(hoisted.staticItemCounts.at(-1)).toBe(1);
153
+ expect(hoisted.capturedIndices).toHaveLength(7);
154
+ });
155
+
156
+ it("clamps negative windowSize and windowOverscan to 0", () => {
157
+ hoisted.state.messagesLength = 6;
158
+
159
+ render(
160
+ <ThreadPrimitive.Messages windowSize={-5} windowOverscan={-2}>
161
+ {() => <Text>m</Text>}
162
+ </ThreadPrimitive.Messages>,
163
+ );
164
+
165
+ expect(hoisted.staticItemCounts.at(-1)).toBe(6);
166
+ expect(hoisted.capturedIndices).toHaveLength(6);
167
+ });
168
+
169
+ it("with windowSize=0 keeps only windowOverscan messages live", () => {
170
+ hoisted.state.messagesLength = 10;
171
+
172
+ render(
173
+ <ThreadPrimitive.Messages windowSize={0} windowOverscan={4}>
174
+ {() => <Text>m</Text>}
175
+ </ThreadPrimitive.Messages>,
176
+ );
177
+
178
+ expect(hoisted.staticItemCounts.at(-1)).toBe(6);
179
+ expect(hoisted.capturedIndices).toHaveLength(10);
180
+ });
181
+
182
+ it("does not re-render messages on parent rerender with a stable render fn", () => {
183
+ hoisted.state.messagesLength = 3;
184
+ const stableRender = () => <Text>m</Text>;
185
+
186
+ const instance = render(
187
+ <ThreadPrimitive.Messages>{stableRender}</ThreadPrimitive.Messages>,
188
+ );
189
+ expect(hoisted.capturedIndices).toEqual([0, 1, 2]);
190
+
191
+ instance.rerender(
192
+ <ThreadPrimitive.Messages>{stableRender}</ThreadPrimitive.Messages>,
193
+ );
194
+ expect(hoisted.capturedIndices).toEqual([0, 1, 2]);
195
+ });
196
+
197
+ it("supports the components API alongside windowing", () => {
198
+ hoisted.state.messagesLength = 4;
199
+ const Message = () => <Text>m</Text>;
200
+
201
+ render(
202
+ <ThreadPrimitive.Messages windowSize={2} components={{ Message }} />,
203
+ );
204
+
205
+ expect(hoisted.capturedIndices).toEqual([0, 1, 2, 3]);
206
+ });
207
+
208
+ it("does not re-render messages when an inline components literal is passed", () => {
209
+ hoisted.state.messagesLength = 3;
210
+ const Message = () => <Text>m</Text>;
211
+
212
+ const App = () => <ThreadPrimitive.Messages components={{ Message }} />;
213
+
214
+ const instance = render(<App />);
215
+ expect(hoisted.capturedIndices).toEqual([0, 1, 2]);
216
+
217
+ instance.rerender(<App />);
218
+ expect(hoisted.capturedIndices).toEqual([0, 1, 2]);
219
+ });
220
+ });
@@ -1,8 +1,14 @@
1
- import { type ComponentType, type FC, type ReactNode, useMemo } from "react";
2
- import { Box } from "ink";
1
+ import {
2
+ type ComponentType,
3
+ type FC,
4
+ type ReactNode,
5
+ useCallback,
6
+ useMemo,
7
+ } from "react";
8
+ import { Box, Static } from "ink";
3
9
  import type { ThreadMessage } from "@assistant-ui/core";
4
- import { RenderChildrenWithAccessor, useAuiState } from "@assistant-ui/store";
5
- import { MessageByIndexProvider } from "@assistant-ui/core/react";
10
+ import { useAuiState } from "@assistant-ui/store";
11
+ import { MemoMessage } from "../internal/MemoMessage";
6
12
 
7
13
  type MessageComponents =
8
14
  | {
@@ -26,15 +32,31 @@ type MessageComponents =
26
32
  SystemMessage?: ComponentType | undefined;
27
33
  };
28
34
 
35
+ /**
36
+ * Live render region keeps the last `windowSize + windowOverscan` messages;
37
+ * older messages graduate through Ink's `<Static>` into terminal scrollback
38
+ * and stop repainting. Defaults to no windowing.
39
+ *
40
+ * Per-message memoization only engages when the render callback is
41
+ * referentially stable. The `components` API handles stability internally;
42
+ * with the children render-fn API, hoist or memoize the function.
43
+ */
44
+ type WindowingProps = {
45
+ /** Recent messages kept live. Unset renders all dynamically. Negative clamped to 0. */
46
+ windowSize?: number | undefined;
47
+ /** Extra live messages above the window to absorb boundary churn. Defaults to 4. Negative clamped to 0. */
48
+ windowOverscan?: number | undefined;
49
+ };
50
+
29
51
  export type ThreadMessagesProps =
30
- | {
52
+ | ({
31
53
  components: MessageComponents;
32
54
  children?: never;
33
- }
34
- | {
55
+ } & WindowingProps)
56
+ | ({
35
57
  children: (value: { message: ThreadMessage }) => ReactNode;
36
58
  components?: never;
37
- };
59
+ } & WindowingProps);
38
60
 
39
61
  const DEFAULT_SYSTEM_MESSAGE = () => null;
40
62
 
@@ -103,45 +125,114 @@ const ThreadMessageComponent: FC<{ components: MessageComponents }> = ({
103
125
 
104
126
  const ThreadMessagesInner: FC<{
105
127
  children: (value: { message: ThreadMessage }) => ReactNode;
106
- }> = ({ children }) => {
128
+ windowSize?: number | undefined;
129
+ windowOverscan?: number | undefined;
130
+ }> = ({ children, windowSize, windowOverscan = 4 }) => {
107
131
  const messagesLength = useAuiState((s) => s.thread.messages.length);
108
132
 
109
- return useMemo(() => {
133
+ const tailStart =
134
+ windowSize !== undefined
135
+ ? Math.max(
136
+ 0,
137
+ messagesLength -
138
+ Math.max(0, windowSize) -
139
+ Math.max(0, windowOverscan),
140
+ )
141
+ : 0;
142
+
143
+ const prefixIndices = useMemo(
144
+ () => Array.from({ length: tailStart }, (_, i) => i),
145
+ [tailStart],
146
+ );
147
+
148
+ const tail = useMemo(() => {
110
149
  if (messagesLength === 0) return null;
111
- return Array.from({ length: messagesLength }, (_, index) => (
112
- <MessageByIndexProvider key={index} index={index}>
113
- <RenderChildrenWithAccessor
114
- getItemState={(aui) => aui.thread().message({ index }).getState()}
115
- >
116
- {(getItem) =>
117
- children({
118
- get message() {
119
- return getItem();
120
- },
121
- })
122
- }
123
- </RenderChildrenWithAccessor>
124
- </MessageByIndexProvider>
125
- ));
126
- }, [messagesLength, children]);
150
+ const items: ReactNode[] = [];
151
+ for (let index = tailStart; index < messagesLength; index++) {
152
+ items.push(<MemoMessage key={index} index={index} render={children} />);
153
+ }
154
+ return items;
155
+ }, [messagesLength, tailStart, children]);
156
+
157
+ if (tailStart === 0) return tail;
158
+
159
+ return (
160
+ <>
161
+ <Static items={prefixIndices}>
162
+ {(index) => <MemoMessage key={index} index={index} render={children} />}
163
+ </Static>
164
+ {tail}
165
+ </>
166
+ );
127
167
  };
128
168
 
129
169
  export const ThreadMessages: FC<ThreadMessagesProps> = ({
130
170
  components,
131
171
  children,
172
+ windowSize,
173
+ windowOverscan,
132
174
  }) => {
175
+ const Message = components?.Message;
176
+ const EditComposer = components?.EditComposer;
177
+ const UserEditComposer = components?.UserEditComposer;
178
+ const AssistantEditComposer = components?.AssistantEditComposer;
179
+ const SystemEditComposer = components?.SystemEditComposer;
180
+ const UserMessage = components?.UserMessage;
181
+ const AssistantMessage = components?.AssistantMessage;
182
+ const SystemMessage = components?.SystemMessage;
183
+
184
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `components` excluded so inline literals do not bust the memo; per-field deps cover real changes.
185
+ const stableComponents = useMemo<MessageComponents | undefined>(() => {
186
+ if (!components) return undefined;
187
+ return {
188
+ Message,
189
+ EditComposer,
190
+ UserEditComposer,
191
+ AssistantEditComposer,
192
+ SystemEditComposer,
193
+ UserMessage,
194
+ AssistantMessage,
195
+ SystemMessage,
196
+ } as MessageComponents;
197
+ }, [
198
+ Message,
199
+ EditComposer,
200
+ UserEditComposer,
201
+ AssistantEditComposer,
202
+ SystemEditComposer,
203
+ UserMessage,
204
+ AssistantMessage,
205
+ SystemMessage,
206
+ ]);
207
+
208
+ const renderFromComponents = useCallback(
209
+ () =>
210
+ stableComponents ? (
211
+ <ThreadMessageComponent components={stableComponents} />
212
+ ) : null,
213
+ [stableComponents],
214
+ );
215
+
133
216
  if (components) {
134
217
  return (
135
218
  <Box flexDirection="column">
136
- <ThreadMessagesInner>
137
- {() => <ThreadMessageComponent components={components} />}
219
+ <ThreadMessagesInner
220
+ windowSize={windowSize}
221
+ windowOverscan={windowOverscan}
222
+ >
223
+ {renderFromComponents}
138
224
  </ThreadMessagesInner>
139
225
  </Box>
140
226
  );
141
227
  }
142
228
  return (
143
229
  <Box flexDirection="column">
144
- <ThreadMessagesInner>{children}</ThreadMessagesInner>
230
+ <ThreadMessagesInner
231
+ windowSize={windowSize}
232
+ windowOverscan={windowOverscan}
233
+ >
234
+ {children}
235
+ </ThreadMessagesInner>
145
236
  </Box>
146
237
  );
147
238
  };