@steerable/agent-ui 0.6.57 → 0.6.58

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.
@@ -30,6 +30,8 @@ export function useChatList(options) {
30
30
  const [error, setError] = useState(null);
31
31
  const [selectedAgentId, setSelectedAgentId] = useState(null);
32
32
  const pageRef = useRef(1);
33
+ const hasMoreRef = useRef(false);
34
+ const loadingMoreRef = useRef(false);
33
35
  const mountedRef = useRef(true);
34
36
  useEffect(() => {
35
37
  mountedRef.current = true;
@@ -45,6 +47,7 @@ export function useChatList(options) {
45
47
  return;
46
48
  setChats(res.chats);
47
49
  setHasMore(res.hasMore);
50
+ hasMoreRef.current = res.hasMore;
48
51
  pageRef.current = 1;
49
52
  setError(null);
50
53
  }
@@ -69,16 +72,27 @@ export function useChatList(options) {
69
72
  }
70
73
  }, []);
71
74
  const loadMoreChats = useCallback(async () => {
72
- if (!hasMoreChats || isLoadingMoreChats)
75
+ if (!hasMoreRef.current || loadingMoreRef.current)
73
76
  return;
77
+ loadingMoreRef.current = true;
74
78
  setLoadingMore(true);
75
79
  try {
76
80
  const next = pageRef.current + 1;
77
81
  const res = await transportRef.current.listChats({ page: next, pageSize });
78
82
  if (!mountedRef.current)
79
83
  return;
80
- setChats((prev) => prev.concat(res.chats));
84
+ setChats((prev) => {
85
+ const ids = new Set(prev.map(transportRef.current.getChatId));
86
+ return prev.concat(res.chats.filter((chat) => {
87
+ const id = transportRef.current.getChatId(chat);
88
+ if (ids.has(id))
89
+ return false;
90
+ ids.add(id);
91
+ return true;
92
+ }));
93
+ });
81
94
  setHasMore(res.hasMore);
95
+ hasMoreRef.current = res.hasMore;
82
96
  pageRef.current = next;
83
97
  }
84
98
  catch (err) {
@@ -86,10 +100,11 @@ export function useChatList(options) {
86
100
  setError(err instanceof Error ? err : new Error(String(err)));
87
101
  }
88
102
  finally {
103
+ loadingMoreRef.current = false;
89
104
  if (mountedRef.current)
90
105
  setLoadingMore(false);
91
106
  }
92
- }, [hasMoreChats, isLoadingMoreChats, pageSize]);
107
+ }, [pageSize]);
93
108
  const createChat = useCallback(async (input) => {
94
109
  const t = transportRef.current;
95
110
  if (!t.createChat)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-ui",
3
- "version": "0.6.57",
3
+ "version": "0.6.58",
4
4
  "description": "Steerable framework Tier 4 — headless React hooks (useAgentSession, useChatStream, useToolCallStatus) and Tailwind-themed components (ChatPanel, MessageList, OrchestrationPlanCard, ToolCallRenderer, SSEStreamView) for embedding Steerable agents in any React app.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://steerableframework.com/storybook/",
@@ -57,7 +57,7 @@
57
57
  "README.md"
58
58
  ],
59
59
  "dependencies": {
60
- "@steerable/agent-protocol": "0.6.57"
60
+ "@steerable/agent-protocol": "0.6.58"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=18.0.0",
@@ -83,6 +83,22 @@ describe('useChatList', () => {
83
83
  expect(result.current.hasMoreChats).toBe(false);
84
84
  });
85
85
 
86
+ it('coalesces concurrent loadMoreChats calls for the same page', async () => {
87
+ const { transport, listChats } = makeTransport();
88
+ const { result } = renderHook(() => useChatList({ transport, pageSize: 50 }));
89
+
90
+ await waitFor(() => expect(result.current.chats.length).toBe(2));
91
+
92
+ await act(async () => {
93
+ const first = result.current.loadMoreChats();
94
+ const second = result.current.loadMoreChats();
95
+ await Promise.all([first, second]);
96
+ });
97
+
98
+ expect(listChats).toHaveBeenCalledTimes(2);
99
+ expect(result.current.chats.map((c) => c.id)).toEqual(['c1', 'c2', 'c3']);
100
+ });
101
+
86
102
  it('patchChatTitle updates without re-fetching', async () => {
87
103
  const { transport, listChats } = makeTransport();
88
104
  const { result } = renderHook(() => useChatList({ transport, pageSize: 50 }));
@@ -88,6 +88,8 @@ export function useChatList<TChat, TAgent>(
88
88
  const [error, setError] = useState<Error | null>(null);
89
89
  const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
90
90
  const pageRef = useRef(1);
91
+ const hasMoreRef = useRef(false);
92
+ const loadingMoreRef = useRef(false);
91
93
  const mountedRef = useRef(true);
92
94
 
93
95
  useEffect(() => {
@@ -104,6 +106,7 @@ export function useChatList<TChat, TAgent>(
104
106
  if (!mountedRef.current) return;
105
107
  setChats(res.chats);
106
108
  setHasMore(res.hasMore);
109
+ hasMoreRef.current = res.hasMore;
107
110
  pageRef.current = 1;
108
111
  setError(null);
109
112
  } catch (err) {
@@ -123,21 +126,34 @@ export function useChatList<TChat, TAgent>(
123
126
  }, []);
124
127
 
125
128
  const loadMoreChats = useCallback(async () => {
126
- if (!hasMoreChats || isLoadingMoreChats) return;
129
+ if (!hasMoreRef.current || loadingMoreRef.current) return;
130
+ loadingMoreRef.current = true;
127
131
  setLoadingMore(true);
128
132
  try {
129
133
  const next = pageRef.current + 1;
130
134
  const res = await transportRef.current.listChats({ page: next, pageSize });
131
135
  if (!mountedRef.current) return;
132
- setChats((prev) => prev.concat(res.chats));
136
+ setChats((prev) => {
137
+ const ids = new Set(prev.map(transportRef.current.getChatId));
138
+ return prev.concat(
139
+ res.chats.filter((chat) => {
140
+ const id = transportRef.current.getChatId(chat);
141
+ if (ids.has(id)) return false;
142
+ ids.add(id);
143
+ return true;
144
+ }),
145
+ );
146
+ });
133
147
  setHasMore(res.hasMore);
148
+ hasMoreRef.current = res.hasMore;
134
149
  pageRef.current = next;
135
150
  } catch (err) {
136
151
  if (mountedRef.current) setError(err instanceof Error ? err : new Error(String(err)));
137
152
  } finally {
153
+ loadingMoreRef.current = false;
138
154
  if (mountedRef.current) setLoadingMore(false);
139
155
  }
140
- }, [hasMoreChats, isLoadingMoreChats, pageSize]);
156
+ }, [pageSize]);
141
157
 
142
158
  const createChat = useCallback(
143
159
  async (input?: { agentId?: string; projectId?: string }) => {