@sybilion/uilib 1.3.88 → 1.3.90

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 (31) hide show
  1. package/dist/esm/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.js +29 -21
  2. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +4 -1
  3. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +17 -10
  4. package/dist/esm/contexts/chat-context.js +180 -205
  5. package/dist/esm/contexts/chatPersistence.js +6 -18
  6. package/dist/esm/contexts/chatSessionStorage.js +245 -0
  7. package/dist/esm/lib/dashboard-spec/jsonDashboardFence.js +80 -0
  8. package/dist/esm/types/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.d.ts +1 -5
  9. package/dist/esm/types/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.test.d.ts +1 -0
  10. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +6 -1
  11. package/dist/esm/types/src/contexts/chat-context.d.ts +6 -0
  12. package/dist/esm/types/src/contexts/chatPersistence.d.ts +4 -1
  13. package/dist/esm/types/src/contexts/chatSessionStorage.d.ts +32 -0
  14. package/dist/esm/types/src/contexts/chatSessionStorage.test.d.ts +1 -0
  15. package/dist/esm/types/src/lib/dashboard-spec/jsonDashboardFence.d.ts +9 -0
  16. package/dist/esm/types/src/lib/dashboard-spec/stripJsonDashboardFences.d.ts +1 -2
  17. package/dist/esm/types/src/lib/dashboard-spec/stripJsonDashboardFences.test.d.ts +1 -0
  18. package/package.json +1 -1
  19. package/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.test.tsx +25 -0
  20. package/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.tsx +35 -26
  21. package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +7 -1
  22. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +21 -8
  23. package/src/contexts/chat-context.tsx +253 -220
  24. package/src/contexts/chatPersistence.test.ts +11 -0
  25. package/src/contexts/chatPersistence.ts +22 -6
  26. package/src/contexts/chatSessionStorage.test.ts +125 -0
  27. package/src/contexts/chatSessionStorage.ts +321 -0
  28. package/src/lib/dashboard-spec/jsonDashboardFence.ts +98 -0
  29. package/src/lib/dashboard-spec/stripJsonDashboardFences.test.ts +84 -0
  30. package/src/lib/dashboard-spec/stripJsonDashboardFences.ts +5 -6
  31. package/dist/esm/lib/dashboard-spec/stripJsonDashboardFences.js +0 -7
@@ -2,84 +2,42 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { createContext, useState, useCallback, useEffect, useContext, useMemo } from 'react';
3
3
  import { MessageRole } from '../components/ui/Chat/Chat.types.js';
4
4
  import { normalizeUserTextFileAttachments } from '../components/ui/Chat/buildChatSendMessagePayload.js';
5
- import { stripJsonDashboardFences } from '../lib/dashboard-spec/stripJsonDashboardFences.js';
6
- import { LS } from '@homecode/ui';
7
- import { persistChatsToLS, safeLsSet } from './chatPersistence.js';
5
+ import { normalizeJsonDashboardChatText, stripJsonDashboardFences } from '../lib/dashboard-spec/jsonDashboardFence.js';
6
+ import { isEphemeralChatScope } from './chatPersistence.js';
7
+ import { loadChatsFromSessionStorage, loadScopeIndex, persistScopeIndex, linkSessionToScope, loadSession, persistSession, addScopeIdToRegistry, removeSessionFromScope, countSessionReferences, deleteSessionBlob, renameSessionId } from './chatSessionStorage.js';
8
8
 
9
- const CHATS_PREFIX = 'chats-';
10
- const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
11
9
  const ChatContext = createContext(undefined);
12
10
  /** Stable composite key; avoids collisions if `scopeId` contains `:`. */
13
11
  function outboundPendingKey(scopeId, chatSessionId) {
14
12
  return `${scopeId}\0${chatSessionId}`;
15
13
  }
16
- function getCurrentChatIdKey(scopeId) {
17
- return `chat-current-id-${scopeId}`;
18
- }
19
- function getChatsKey(scopeId) {
20
- return `${CHATS_PREFIX}${scopeId}`;
21
- }
22
- function scopeIdsForUser(userId, all) {
23
- const prefix = `${userId}-`;
24
- const out = new Set();
25
- for (const id of all) {
26
- if (id.startsWith(prefix))
27
- out.add(id);
28
- }
29
- return out;
30
- }
31
- function discoverScopeIdsFromLS() {
32
- const scopeIds = new Set();
33
- const registryRaw = LS.get(CHAT_SCOPE_IDS_REGISTRY_KEY);
34
- const registry = Array.isArray(registryRaw) ? registryRaw : [];
35
- registry.forEach((id) => {
36
- if (typeof id === 'string' && id)
37
- scopeIds.add(id);
38
- });
39
- try {
40
- for (let i = 0; i < window.localStorage.length; i++) {
41
- const key = window.localStorage.key(i);
42
- if (key?.startsWith(CHATS_PREFIX)) {
43
- const scopeId = key.slice(CHATS_PREFIX.length);
44
- if (scopeId)
45
- scopeIds.add(scopeId);
46
- }
14
+ function applySessionUpdate(prev, sessionId, updater) {
15
+ let updatedChat = null;
16
+ const next = {};
17
+ for (const [scopeId, scopeChats] of Object.entries(prev)) {
18
+ const idx = scopeChats.findIndex(chat => chat.session_id === sessionId);
19
+ if (idx === -1) {
20
+ next[scopeId] = scopeChats;
21
+ continue;
47
22
  }
23
+ const chat = updater(scopeChats[idx]);
24
+ updatedChat = chat;
25
+ const scopeNext = [...scopeChats];
26
+ scopeNext[idx] = chat;
27
+ next[scopeId] = scopeNext;
48
28
  }
49
- catch {
50
- // ignore
51
- }
52
- return scopeIds;
53
- }
54
- function loadChatsFromLS(userId) {
55
- if (typeof window === 'undefined' || userId === null) {
56
- return { chats: {}, currentChatId: {} };
57
- }
58
- const chats = {};
59
- const currentChatId = {};
60
- const scopeIds = scopeIdsForUser(userId, discoverScopeIdsFromLS());
61
- for (const scopeId of scopeIds) {
62
- const chatsKey = getChatsKey(scopeId);
63
- const storedRaw = LS.get(chatsKey);
64
- const stored = Array.isArray(storedRaw) ? storedRaw : undefined;
65
- if (stored?.length) {
66
- chats[scopeId] = stored;
67
- const ck = getCurrentChatIdKey(scopeId);
68
- const savedCurrentIdRaw = LS.get(ck);
69
- const savedCurrentId = typeof savedCurrentIdRaw === 'string' && savedCurrentIdRaw !== ''
70
- ? savedCurrentIdRaw
71
- : undefined;
72
- currentChatId[scopeId] = savedCurrentId ?? stored[0]?.session_id ?? null;
73
- }
74
- }
75
- return { chats, currentChatId };
29
+ if (updatedChat)
30
+ persistSession(updatedChat);
31
+ return next;
76
32
  }
77
- function addScopeIdToRegistry(scopeId) {
78
- const raw = LS.get(CHAT_SCOPE_IDS_REGISTRY_KEY);
79
- const registry = Array.isArray(raw) ? [...raw] : [];
80
- if (!registry.includes(scopeId)) {
81
- safeLsSet(CHAT_SCOPE_IDS_REGISTRY_KEY, [...registry, scopeId]);
33
+ function remapSessionIdInMemory(prev, oldSessionId, newSessionId) {
34
+ const next = {};
35
+ for (const [scopeId, scopeChats] of Object.entries(prev)) {
36
+ next[scopeId] = scopeChats.map(chat => chat.session_id === oldSessionId
37
+ ? { ...chat, session_id: newSessionId }
38
+ : chat);
82
39
  }
40
+ return next;
83
41
  }
84
42
  /** Shallow-clone messages for seeding another session; drops in-progress rows. */
85
43
  function cloneMessagesForNewSession(messages) {
@@ -87,6 +45,7 @@ function cloneMessagesForNewSession(messages) {
87
45
  .filter(message => !message.inProgress)
88
46
  .map(message => ({
89
47
  ...message,
48
+ text: stripJsonDashboardFences(message.text),
90
49
  ...(message.meta ? { meta: { ...message.meta } } : {}),
91
50
  ...(message.userTextFileAttachments
92
51
  ? {
@@ -97,14 +56,14 @@ function cloneMessagesForNewSession(messages) {
97
56
  }
98
57
  function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessageFn, }) {
99
58
  const [chats, setChats] = useState(() => {
100
- if (userSwitchKey === null)
59
+ if (userSwitchKey === null || typeof window === 'undefined')
101
60
  return {};
102
- return loadChatsFromLS(userSwitchKey).chats;
61
+ return loadChatsFromSessionStorage(userSwitchKey).chats;
103
62
  });
104
63
  const [currentChatId, setCurrentChatIdState] = useState(() => {
105
- if (userSwitchKey === null)
64
+ if (userSwitchKey === null || typeof window === 'undefined')
106
65
  return {};
107
- return loadChatsFromLS(userSwitchKey).currentChatId;
66
+ return loadChatsFromSessionStorage(userSwitchKey).currentChatId;
108
67
  });
109
68
  const [outboundPendingByKey, setOutboundPendingByKey] = useState({});
110
69
  const [panelBusyCount, setPanelBusyCount] = useState(0);
@@ -140,6 +99,54 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
140
99
  return null;
141
100
  return v;
142
101
  }, [currentChatId]);
102
+ const setCurrentChatId = useCallback((currScopeId, sessionId) => {
103
+ if (!sessionId)
104
+ return;
105
+ setCurrentChatIdState(prev => ({ ...prev, [currScopeId]: sessionId }));
106
+ const index = loadScopeIndex(currScopeId);
107
+ if (index) {
108
+ persistScopeIndex(currScopeId, {
109
+ ...index,
110
+ currentSessionId: sessionId,
111
+ });
112
+ }
113
+ }, []);
114
+ const linkSessionToScope$1 = useCallback((scopeId, sessionId, options) => {
115
+ if (userSwitchKey === null)
116
+ return;
117
+ linkSessionToScope(scopeId, sessionId, options);
118
+ setChats(prev => {
119
+ const sessionFromLs = loadSession(sessionId);
120
+ let sessionFromMemory = null;
121
+ for (const scopeChats of Object.values(prev)) {
122
+ const found = scopeChats.find(chat => chat.session_id === sessionId);
123
+ if (found &&
124
+ (!sessionFromMemory ||
125
+ found.messages.length > sessionFromMemory.messages.length)) {
126
+ sessionFromMemory = found;
127
+ }
128
+ }
129
+ const session = sessionFromMemory &&
130
+ (!sessionFromLs ||
131
+ sessionFromMemory.messages.length >= sessionFromLs.messages.length)
132
+ ? sessionFromMemory
133
+ : sessionFromLs;
134
+ if (!session)
135
+ return prev;
136
+ if (session === sessionFromMemory) {
137
+ persistSession(sessionFromMemory);
138
+ }
139
+ const scopeChats = prev[scopeId] ?? [];
140
+ const exists = scopeChats.some(chat => chat.session_id === sessionId);
141
+ const updatedChats = exists
142
+ ? scopeChats.map(chat => chat.session_id === sessionId ? session : chat)
143
+ : [session, ...scopeChats];
144
+ return { ...prev, [scopeId]: updatedChats };
145
+ });
146
+ if (options?.makeCurrent !== false) {
147
+ setCurrentChatIdState(prev => ({ ...prev, [scopeId]: sessionId }));
148
+ }
149
+ }, [userSwitchKey]);
143
150
  const newChat = useCallback((scopeId, options) => {
144
151
  if (userSwitchKey === null)
145
152
  return undefined;
@@ -148,63 +155,65 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
148
155
  const seededMessages = options?.seedMessages && options.seedMessages.length > 0
149
156
  ? cloneMessagesForNewSession(options.seedMessages)
150
157
  : [];
151
- const newChat = {
158
+ const created = {
152
159
  session_id: sessionId,
153
160
  name: '',
154
161
  messages: seededMessages,
155
162
  };
163
+ persistSession(created);
164
+ linkSessionToScope(scopeId, sessionId, { makeCurrent: true });
156
165
  setChats(prev => {
157
- const currentChats = prev[scopeId] ?? [];
158
- const updatedChats = [newChat, ...currentChats];
159
- persistChatsToLS(scopeId, updatedChats);
160
- return {
161
- ...prev,
162
- [scopeId]: updatedChats,
163
- };
166
+ const scopeChats = prev[scopeId] ?? [];
167
+ const isEphemeral = isEphemeralChatScope(scopeId);
168
+ if (isEphemeral) {
169
+ for (const old of scopeChats) {
170
+ if (old.session_id !== sessionId) {
171
+ removeSessionFromScope(scopeId, old.session_id);
172
+ if (countSessionReferences(userSwitchKey, old.session_id) === 0) {
173
+ deleteSessionBlob(old.session_id);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ const updatedChats = isEphemeral ? [created] : [created, ...scopeChats];
179
+ return { ...prev, [scopeId]: updatedChats };
164
180
  });
165
181
  setCurrentChatIdState(prev => ({ ...prev, [scopeId]: sessionId }));
166
- safeLsSet(getCurrentChatIdKey(scopeId), sessionId);
167
182
  return sessionId;
168
183
  }, [userSwitchKey]);
169
- const setCurrentChatId = useCallback((currScopeId, sessionId) => {
170
- if (!sessionId)
171
- return;
172
- setCurrentChatIdState(prev => ({ ...prev, [currScopeId]: sessionId }));
173
- safeLsSet(getCurrentChatIdKey(currScopeId), sessionId);
174
- }, []);
175
184
  const deleteChat = useCallback((scopeId, sessionId) => {
176
- const currentKey = getCurrentChatIdKey(scopeId);
185
+ const scopeChats = chats[scopeId] ?? [];
186
+ const deletedIndex = scopeChats.findIndex(chat => chat.session_id === sessionId);
187
+ if (deletedIndex === -1)
188
+ return;
189
+ removeSessionFromScope(scopeId, sessionId);
190
+ if (userSwitchKey != null &&
191
+ countSessionReferences(userSwitchKey, sessionId) === 0) {
192
+ deleteSessionBlob(sessionId);
193
+ }
177
194
  setChats(prev => {
178
- const scopeChats = prev[scopeId] ?? [];
179
- const deletedIndex = scopeChats.findIndex(c => c.session_id === sessionId);
180
- if (deletedIndex === -1) {
181
- return prev;
182
- }
183
- const updatedChats = scopeChats.filter(c => c.session_id !== sessionId);
184
- persistChatsToLS(scopeId, updatedChats);
185
- setCurrentChatIdState(prevCurr => {
186
- if (prevCurr[scopeId] !== sessionId) {
187
- return prevCurr;
188
- }
189
- const nextId = deletedIndex > 0
190
- ? scopeChats[deletedIndex - 1].session_id
191
- : (updatedChats[0]?.session_id ?? null);
192
- if (nextId) {
193
- safeLsSet(currentKey, nextId);
194
- }
195
- else {
196
- LS.remove(currentKey);
197
- }
198
- return { ...prevCurr, [scopeId]: nextId };
199
- });
200
- return { ...prev, [scopeId]: updatedChats };
195
+ const currentScopeChats = prev[scopeId] ?? [];
196
+ return {
197
+ ...prev,
198
+ [scopeId]: currentScopeChats.filter(chat => chat.session_id !== sessionId),
199
+ };
201
200
  });
202
- }, []);
201
+ setCurrentChatIdState(prevCurr => {
202
+ if (prevCurr[scopeId] !== sessionId)
203
+ return prevCurr;
204
+ const index = loadScopeIndex(scopeId);
205
+ const nextId = index?.currentSessionId ??
206
+ (deletedIndex > 0 ? scopeChats[deletedIndex - 1].session_id : null);
207
+ return { ...prevCurr, [scopeId]: nextId };
208
+ });
209
+ }, [chats, userSwitchKey]);
203
210
  const addMessage = useCallback((scopeId, chatId, role, text, options) => {
204
211
  if (userSwitchKey === null)
205
212
  return undefined;
206
213
  addScopeIdToRegistry(scopeId);
207
- const storedText = stripJsonDashboardFences(text);
214
+ const storedText = role === MessageRole.ASSISTANT
215
+ ? normalizeJsonDashboardChatText(text)
216
+ : stripJsonDashboardFences(text);
208
217
  const attachments = role === MessageRole.USER
209
218
  ? options?.userTextFileAttachments
210
219
  : undefined;
@@ -219,73 +228,52 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
219
228
  ...(options?.inProgress ? { inProgress: true } : {}),
220
229
  ...(options?.meta ? { meta: { ...options.meta } } : {}),
221
230
  };
222
- setChats(prev => {
223
- const scopeChats = prev[scopeId] ?? [];
224
- const updatedChats = scopeChats.map(chat => {
225
- if (chat.session_id === chatId) {
226
- return { ...chat, messages: [...chat.messages, newMessage] };
227
- }
228
- return chat;
229
- });
230
- persistChatsToLS(scopeId, updatedChats);
231
- return { ...prev, [scopeId]: updatedChats };
232
- });
231
+ setChats(prev => applySessionUpdate(prev, chatId, chat => ({
232
+ ...chat,
233
+ messages: [...chat.messages, newMessage],
234
+ })));
233
235
  return newMessage.id;
234
236
  }, [userSwitchKey]);
235
237
  const removeMessageById = useCallback((scopeId, chatId, messageId) => {
236
238
  if (userSwitchKey === null)
237
239
  return;
238
- setChats(prev => {
239
- const scopeChats = prev[scopeId] ?? [];
240
- const updatedChats = scopeChats.map(chat => {
241
- if (chat.session_id !== chatId)
242
- return chat;
243
- return {
244
- ...chat,
245
- messages: chat.messages.filter(m => m.id !== messageId),
246
- };
247
- });
248
- persistChatsToLS(scopeId, updatedChats);
249
- return { ...prev, [scopeId]: updatedChats };
250
- });
240
+ setChats(prev => applySessionUpdate(prev, chatId, chat => ({
241
+ ...chat,
242
+ messages: chat.messages.filter(message => message.id !== messageId),
243
+ })));
251
244
  }, [userSwitchKey]);
252
245
  const updateMessageById = useCallback((scopeId, chatId, messageId, patch) => {
253
246
  if (userSwitchKey === null)
254
247
  return;
255
- setChats(prev => {
256
- const scopeChats = prev[scopeId] ?? [];
257
- const updatedChats = scopeChats.map(chat => {
258
- if (chat.session_id !== chatId)
259
- return chat;
260
- return {
261
- ...chat,
262
- messages: chat.messages.map(message => {
263
- if (message.id !== messageId)
264
- return message;
265
- const next = { ...message };
266
- if (patch.role != null) {
267
- next.role = patch.role;
268
- }
269
- if (patch.text != null) {
270
- next.text = stripJsonDashboardFences(patch.text);
271
- }
272
- if (patch.inProgress === true) {
273
- next.inProgress = true;
274
- }
275
- else if (patch.inProgress === false ||
276
- (patch.role != null && patch.role !== MessageRole.SYSTEM)) {
277
- delete next.inProgress;
278
- }
279
- if (patch.meta != null) {
280
- next.meta = { ...next.meta, ...patch.meta };
281
- }
282
- return next;
283
- }),
284
- };
285
- });
286
- persistChatsToLS(scopeId, updatedChats);
287
- return { ...prev, [scopeId]: updatedChats };
288
- });
248
+ setChats(prev => applySessionUpdate(prev, chatId, chat => ({
249
+ ...chat,
250
+ messages: chat.messages.map(message => {
251
+ if (message.id !== messageId)
252
+ return message;
253
+ const next = { ...message };
254
+ if (patch.role != null) {
255
+ next.role = patch.role;
256
+ }
257
+ if (patch.text != null) {
258
+ const targetRole = patch.role ?? message.role;
259
+ next.text =
260
+ targetRole === MessageRole.ASSISTANT
261
+ ? normalizeJsonDashboardChatText(patch.text)
262
+ : stripJsonDashboardFences(patch.text);
263
+ }
264
+ if (patch.inProgress === true) {
265
+ next.inProgress = true;
266
+ }
267
+ else if (patch.inProgress === false ||
268
+ (patch.role != null && patch.role !== MessageRole.SYSTEM)) {
269
+ delete next.inProgress;
270
+ }
271
+ if (patch.meta != null) {
272
+ next.meta = { ...next.meta, ...patch.meta };
273
+ }
274
+ return next;
275
+ }),
276
+ })));
289
277
  }, [userSwitchKey]);
290
278
  const setChatMessages = useCallback((scopeId, chatId, messages) => {
291
279
  if (userSwitchKey === null)
@@ -293,35 +281,23 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
293
281
  addScopeIdToRegistry(scopeId);
294
282
  const cloned = messages.map(message => ({
295
283
  ...message,
284
+ text: message.role === MessageRole.ASSISTANT
285
+ ? message.text
286
+ : stripJsonDashboardFences(message.text),
296
287
  ...(message.meta ? { meta: { ...message.meta } } : {}),
297
288
  }));
298
- setChats(prev => {
299
- const scopeChats = prev[scopeId] ?? [];
300
- const updatedChats = scopeChats.map(chat => {
301
- if (chat.session_id !== chatId)
302
- return chat;
303
- return { ...chat, messages: cloned };
304
- });
305
- persistChatsToLS(scopeId, updatedChats);
306
- return { ...prev, [scopeId]: updatedChats };
307
- });
289
+ setChats(prev => applySessionUpdate(prev, chatId, chat => ({
290
+ ...chat,
291
+ messages: cloned,
292
+ })));
308
293
  }, [userSwitchKey]);
309
294
  const updateChatMeta = useCallback((scopeId, chatId, patch) => {
310
295
  if (userSwitchKey === null)
311
296
  return;
312
- setChats(prev => {
313
- const scopeChats = prev[scopeId] ?? [];
314
- const updatedChats = scopeChats.map(chat => {
315
- if (chat.session_id !== chatId)
316
- return chat;
317
- return {
318
- ...chat,
319
- meta: { ...chat.meta, ...patch },
320
- };
321
- });
322
- persistChatsToLS(scopeId, updatedChats);
323
- return { ...prev, [scopeId]: updatedChats };
324
- });
297
+ setChats(prev => applySessionUpdate(prev, chatId, chat => ({
298
+ ...chat,
299
+ meta: { ...chat.meta, ...patch },
300
+ })));
325
301
  }, [userSwitchKey]);
326
302
  const sendMessage = useCallback(async (scopeId, message, chatId) => {
327
303
  const targetChatId = chatId ?? getCurrentChatId(scopeId);
@@ -350,15 +326,11 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
350
326
  data.session_id && data.session_id !== pendingChatSessionId
351
327
  ? data.session_id
352
328
  : pendingChatSessionId;
353
- if (data.session_id && data.session_id !== pendingChatSessionId) {
354
- setChats(prev => {
355
- const scopeChats = prev[scopeId] ?? [];
356
- const updatedChats = scopeChats.map(chat => chat.session_id === pendingChatSessionId
357
- ? { ...chat, session_id: data.session_id }
358
- : chat);
359
- persistChatsToLS(scopeId, updatedChats);
360
- return { ...prev, [scopeId]: updatedChats };
361
- });
329
+ if (data.session_id &&
330
+ data.session_id !== pendingChatSessionId &&
331
+ userSwitchKey != null) {
332
+ renameSessionId(userSwitchKey, pendingChatSessionId, data.session_id);
333
+ setChats(prev => remapSessionIdInMemory(prev, pendingChatSessionId, data.session_id));
362
334
  setCurrentChatId(scopeId, data.session_id);
363
335
  }
364
336
  if (progressMessageId) {
@@ -389,6 +361,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
389
361
  getCurrentChatId,
390
362
  sendChatMessageFn,
391
363
  setCurrentChatId,
364
+ userSwitchKey,
392
365
  ]);
393
366
  useEffect(() => {
394
367
  if (userSwitchKey === null) {
@@ -398,7 +371,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
398
371
  setPanelBusyCount(0);
399
372
  return;
400
373
  }
401
- const loaded = loadChatsFromLS(userSwitchKey);
374
+ const loaded = loadChatsFromSessionStorage(userSwitchKey);
402
375
  const updatedCurrentChatId = { ...loaded.currentChatId };
403
376
  for (const [scopeId, scopeChats] of Object.entries(loaded.chats)) {
404
377
  if (scopeChats.length > 0 &&
@@ -412,6 +385,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
412
385
  }, [userSwitchKey]);
413
386
  return (jsx(ChatContext.Provider, { value: {
414
387
  newChat,
388
+ linkSessionToScope: linkSessionToScope$1,
415
389
  setCurrentChatId,
416
390
  addMessage,
417
391
  removeMessageById,
@@ -478,7 +452,7 @@ function useSyncChatPanelBusy(isLoading) {
478
452
  }, [isLoading, acquirePanelBusy, releasePanelBusy]);
479
453
  }
480
454
  function useChatsForScopeId(scopeId) {
481
- const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, updateMessageById, updateChatMeta, setChatMessages, sendMessage, deleteChat, } = useChats();
455
+ const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, linkSessionToScope, addMessage, removeMessageById, updateMessageById, updateChatMeta, setChatMessages, sendMessage, deleteChat, } = useChats();
482
456
  const chats = getChatsForScopeId(scopeId);
483
457
  const currentChatId = getCurrentChatId(scopeId);
484
458
  const currentChat = useChat(scopeId, currentChatId ?? undefined);
@@ -489,6 +463,7 @@ function useChatsForScopeId(scopeId) {
489
463
  currentChatId,
490
464
  isOutboundPending,
491
465
  setCurrentChatId: (targetId) => setCurrentChatId(scopeId, targetId),
466
+ linkSessionToScope: (sessionId, options) => linkSessionToScope(scopeId, sessionId, options),
492
467
  newChat: (options) => newChat(scopeId, options),
493
468
  addMessage: (chatId, role, text, options) => addMessage(scopeId, chatId, role, text, options),
494
469
  removeMessageById: (chatId, messageId) => removeMessageById(scopeId, chatId, messageId),
@@ -1,4 +1,5 @@
1
- import { stripJsonDashboardFences } from '../lib/dashboard-spec/stripJsonDashboardFences.js';
1
+ import { MessageRole } from '../components/ui/Chat/Chat.types.js';
2
+ import { stripJsonDashboardFences } from '../lib/dashboard-spec/jsonDashboardFence.js';
2
3
  import { LS } from '@homecode/ui';
3
4
 
4
5
  /** Ephemeral scopes (e.g. /reports/new draft) — keep only the latest session in LS. */
@@ -21,7 +22,9 @@ function stripAttachmentForPersistence(attachment) {
21
22
  };
22
23
  }
23
24
  function stripMessageForPersistence(message) {
24
- const text = stripJsonDashboardFences(message.text);
25
+ const text = message.role === MessageRole.ASSISTANT
26
+ ? message.text
27
+ : stripJsonDashboardFences(message.text);
25
28
  const next = { ...message, text };
26
29
  if (message.inProgress) {
27
30
  delete next.inProgress;
@@ -39,21 +42,6 @@ function stripChatsForPersistence(chats) {
39
42
  .map(stripMessageForPersistence),
40
43
  }));
41
44
  }
42
- function chatsForScopePersistence(scopeId, chats) {
43
- const scopeChats = isEphemeralChatScope(scopeId) ? chats.slice(0, 1) : chats;
44
- return stripChatsForPersistence(scopeChats);
45
- }
46
- function persistChatsToLS(scopeId, chats) {
47
- const chatsKey = `chats-${scopeId}`;
48
- const payload = chatsForScopePersistence(scopeId, chats);
49
- try {
50
- LS.set(chatsKey, payload);
51
- }
52
- catch (error) {
53
- if (!isQuotaExceededError(error))
54
- throw error;
55
- }
56
- }
57
45
  function safeLsSet(key, value) {
58
46
  try {
59
47
  LS.set(key, value);
@@ -64,4 +52,4 @@ function safeLsSet(key, value) {
64
52
  }
65
53
  }
66
54
 
67
- export { persistChatsToLS, safeLsSet, stripChatsForPersistence, stripMessageForPersistence };
55
+ export { isEphemeralChatScope, safeLsSet, stripChatsForPersistence, stripMessageForPersistence };