@xinghunm/ai-chat 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/ai-chat/index.tsx
2
- import { useEffect as useEffect10 } from "react";
2
+ import { useEffect as useEffect11 } from "react";
3
3
  import styled17 from "@emotion/styled";
4
4
  import { ConfigProvider } from "@xinghunm/compass-ui";
5
5
 
@@ -53,7 +53,10 @@ var DEFAULT_AI_CHAT_LABELS = {
53
53
  modelUnavailable: "No model available",
54
54
  skillLoading: "Loading skills...",
55
55
  skillEmpty: "No matching skills",
56
- removeSkillAriaLabel: "Remove skill"
56
+ removeSkillAriaLabel: "Remove skill",
57
+ sessionHistoryLoading: "Loading conversations...",
58
+ sessionHistoryLoadFailed: "Failed to load conversations",
59
+ sessionHistoryEmpty: "No conversations yet"
57
60
  };
58
61
 
59
62
  // src/lib/chat-session.ts
@@ -81,6 +84,20 @@ var createDraftChatSession = ({
81
84
  // src/store/chat-store.ts
82
85
  var DEFAULT_CHAT_SESSION_TITLE = "New Chat";
83
86
  var IMAGE_MESSAGE_SESSION_TITLE = "Image message";
87
+ var createHistoryMessagePaginationState = (page) => ({
88
+ previousCursor: page?.previousCursor ?? null,
89
+ hasMorePrevious: page?.hasMorePrevious ?? Boolean(page && page.previousCursor !== null),
90
+ isLoadingPrevious: false,
91
+ error: null
92
+ });
93
+ var normalizeHistoryMessages = (sessionId, messages) => messages.map((message) => ({ ...message, sessionId }));
94
+ var mergeOlderHistoryMessages = (sessionId, olderMessages, currentMessages) => {
95
+ const currentMessageIds = new Set(currentMessages.map((message) => message.id));
96
+ const uniqueOlderMessages = normalizeHistoryMessages(sessionId, olderMessages).filter(
97
+ (message) => !currentMessageIds.has(message.id)
98
+ );
99
+ return [...uniqueOlderMessages, ...currentMessages];
100
+ };
84
101
  var resolveSessionTitleFromMessage = (message) => {
85
102
  const trimmedContent = message.content.trim();
86
103
  if (trimmedContent) {
@@ -183,6 +200,9 @@ var createChatStore = (initialState) => createStore((set, get) => ({
183
200
  isStreamingBySession: {},
184
201
  isStoppingBySession: {},
185
202
  errorBySession: {},
203
+ sessionMessageLoadStatusBySession: {},
204
+ sessionMessageLoadErrorBySession: {},
205
+ historyMessagePaginationBySession: {},
186
206
  // ---- Session management ------------------------------------------------
187
207
  createSession: (session) => {
188
208
  const state = get();
@@ -196,6 +216,15 @@ var createChatStore = (initialState) => createStore((set, get) => ({
196
216
  const nextErrorBySession = { ...state.errorBySession };
197
217
  const nextIsStreamingBySession = { ...state.isStreamingBySession };
198
218
  const nextIsStoppingBySession = { ...state.isStoppingBySession };
219
+ const nextSessionMessageLoadStatusBySession = {
220
+ ...state.sessionMessageLoadStatusBySession
221
+ };
222
+ const nextSessionMessageLoadErrorBySession = {
223
+ ...state.sessionMessageLoadErrorBySession
224
+ };
225
+ const nextHistoryMessagePaginationBySession = {
226
+ ...state.historyMessagePaginationBySession
227
+ };
199
228
  const sid = session.sessionId;
200
229
  if (nextMessagesBySession[sid] === void 0)
201
230
  nextMessagesBySession[sid] = [];
@@ -205,13 +234,25 @@ var createChatStore = (initialState) => createStore((set, get) => ({
205
234
  nextIsStreamingBySession[sid] = false;
206
235
  if (nextIsStoppingBySession[sid] === void 0)
207
236
  nextIsStoppingBySession[sid] = false;
237
+ if (nextSessionMessageLoadStatusBySession[sid] === void 0) {
238
+ nextSessionMessageLoadStatusBySession[sid] = "loaded";
239
+ }
240
+ if (nextSessionMessageLoadErrorBySession[sid] === void 0) {
241
+ nextSessionMessageLoadErrorBySession[sid] = null;
242
+ }
243
+ if (nextHistoryMessagePaginationBySession[sid] === void 0) {
244
+ nextHistoryMessagePaginationBySession[sid] = createHistoryMessagePaginationState();
245
+ }
208
246
  set({
209
247
  sessions: nextSessions,
210
248
  activeSessionId: sid,
211
249
  messagesBySession: nextMessagesBySession,
212
250
  errorBySession: nextErrorBySession,
213
251
  isStreamingBySession: nextIsStreamingBySession,
214
- isStoppingBySession: nextIsStoppingBySession
252
+ isStoppingBySession: nextIsStoppingBySession,
253
+ sessionMessageLoadStatusBySession: nextSessionMessageLoadStatusBySession,
254
+ sessionMessageLoadErrorBySession: nextSessionMessageLoadErrorBySession,
255
+ historyMessagePaginationBySession: nextHistoryMessagePaginationBySession
215
256
  });
216
257
  },
217
258
  startNewChat: () => {
@@ -266,6 +307,27 @@ var createChatStore = (initialState) => createStore((set, get) => ({
266
307
  nextErrorBySession[nextSessionId] = nextErrorBySession[previousSessionId] ?? null;
267
308
  delete nextErrorBySession[previousSessionId];
268
309
  }
310
+ const nextSessionMessageLoadStatusBySession = {
311
+ ...state.sessionMessageLoadStatusBySession
312
+ };
313
+ if (previousSessionId in nextSessionMessageLoadStatusBySession) {
314
+ nextSessionMessageLoadStatusBySession[nextSessionId] = nextSessionMessageLoadStatusBySession[previousSessionId] ?? "idle";
315
+ delete nextSessionMessageLoadStatusBySession[previousSessionId];
316
+ }
317
+ const nextSessionMessageLoadErrorBySession = {
318
+ ...state.sessionMessageLoadErrorBySession
319
+ };
320
+ if (previousSessionId in nextSessionMessageLoadErrorBySession) {
321
+ nextSessionMessageLoadErrorBySession[nextSessionId] = nextSessionMessageLoadErrorBySession[previousSessionId] ?? null;
322
+ delete nextSessionMessageLoadErrorBySession[previousSessionId];
323
+ }
324
+ const nextHistoryMessagePaginationBySession = {
325
+ ...state.historyMessagePaginationBySession
326
+ };
327
+ if (previousSessionId in nextHistoryMessagePaginationBySession) {
328
+ nextHistoryMessagePaginationBySession[nextSessionId] = nextHistoryMessagePaginationBySession[previousSessionId] ?? createHistoryMessagePaginationState();
329
+ delete nextHistoryMessagePaginationBySession[previousSessionId];
330
+ }
269
331
  const nextActiveSessionId = state.activeSessionId === previousSessionId ? nextSessionId : state.activeSessionId;
270
332
  set({
271
333
  sessions: nextSessions,
@@ -274,6 +336,9 @@ var createChatStore = (initialState) => createStore((set, get) => ({
274
336
  isStreamingBySession: nextIsStreamingBySession,
275
337
  isStoppingBySession: nextIsStoppingBySession,
276
338
  errorBySession: nextErrorBySession,
339
+ sessionMessageLoadStatusBySession: nextSessionMessageLoadStatusBySession,
340
+ sessionMessageLoadErrorBySession: nextSessionMessageLoadErrorBySession,
341
+ historyMessagePaginationBySession: nextHistoryMessagePaginationBySession,
277
342
  activeSessionId: nextActiveSessionId
278
343
  });
279
344
  },
@@ -287,6 +352,61 @@ var createChatStore = (initialState) => createStore((set, get) => ({
287
352
  );
288
353
  set({ sessions: nextSessions });
289
354
  },
355
+ hydrateHistorySessions: (sessions) => {
356
+ const state = get();
357
+ const localSessions = state.sessions.filter(
358
+ (session) => isDraftChatSessionId(session.sessionId)
359
+ );
360
+ const localSessionIds = new Set(localSessions.map((session) => session.sessionId));
361
+ const nextSessions = [
362
+ ...localSessions,
363
+ ...sessions.filter((session) => !localSessionIds.has(session.sessionId)).map((session) => ({
364
+ ...session,
365
+ mode: session.mode ?? DEFAULT_CHAT_AGENT_MODE
366
+ }))
367
+ ];
368
+ const nextMessagesBySession = { ...state.messagesBySession };
369
+ const nextErrorBySession = { ...state.errorBySession };
370
+ const nextIsStreamingBySession = { ...state.isStreamingBySession };
371
+ const nextIsStoppingBySession = { ...state.isStoppingBySession };
372
+ const nextSessionMessageLoadStatusBySession = {
373
+ ...state.sessionMessageLoadStatusBySession
374
+ };
375
+ const nextSessionMessageLoadErrorBySession = {
376
+ ...state.sessionMessageLoadErrorBySession
377
+ };
378
+ const nextHistoryMessagePaginationBySession = {
379
+ ...state.historyMessagePaginationBySession
380
+ };
381
+ nextSessions.forEach((session) => {
382
+ const sid = session.sessionId;
383
+ if (nextErrorBySession[sid] === void 0)
384
+ nextErrorBySession[sid] = null;
385
+ if (nextIsStreamingBySession[sid] === void 0)
386
+ nextIsStreamingBySession[sid] = false;
387
+ if (nextIsStoppingBySession[sid] === void 0)
388
+ nextIsStoppingBySession[sid] = false;
389
+ if (nextSessionMessageLoadStatusBySession[sid] === void 0) {
390
+ nextSessionMessageLoadStatusBySession[sid] = "idle";
391
+ }
392
+ if (nextSessionMessageLoadErrorBySession[sid] === void 0) {
393
+ nextSessionMessageLoadErrorBySession[sid] = null;
394
+ }
395
+ if (nextHistoryMessagePaginationBySession[sid] === void 0) {
396
+ nextHistoryMessagePaginationBySession[sid] = createHistoryMessagePaginationState();
397
+ }
398
+ });
399
+ set({
400
+ sessions: nextSessions,
401
+ messagesBySession: nextMessagesBySession,
402
+ errorBySession: nextErrorBySession,
403
+ isStreamingBySession: nextIsStreamingBySession,
404
+ isStoppingBySession: nextIsStoppingBySession,
405
+ sessionMessageLoadStatusBySession: nextSessionMessageLoadStatusBySession,
406
+ sessionMessageLoadErrorBySession: nextSessionMessageLoadErrorBySession,
407
+ historyMessagePaginationBySession: nextHistoryMessagePaginationBySession
408
+ });
409
+ },
290
410
  // ---- Message operations ------------------------------------------------
291
411
  appendMessage: (sessionId, message) => {
292
412
  const state = get();
@@ -313,6 +433,92 @@ var createChatStore = (initialState) => createStore((set, get) => ({
313
433
  isStreamingBySession: nextIsStreamingBySession
314
434
  });
315
435
  },
436
+ hydrateHistorySessionMessages: (sessionId, messages) => {
437
+ const state = get();
438
+ set({
439
+ messagesBySession: {
440
+ ...state.messagesBySession,
441
+ [sessionId]: normalizeHistoryMessages(sessionId, messages)
442
+ },
443
+ sessionMessageLoadStatusBySession: {
444
+ ...state.sessionMessageLoadStatusBySession,
445
+ [sessionId]: "loaded"
446
+ },
447
+ sessionMessageLoadErrorBySession: {
448
+ ...state.sessionMessageLoadErrorBySession,
449
+ [sessionId]: null
450
+ },
451
+ historyMessagePaginationBySession: {
452
+ ...state.historyMessagePaginationBySession,
453
+ [sessionId]: createHistoryMessagePaginationState()
454
+ }
455
+ });
456
+ },
457
+ hydrateHistorySessionMessagesPage: (sessionId, page) => {
458
+ const state = get();
459
+ set({
460
+ messagesBySession: {
461
+ ...state.messagesBySession,
462
+ [sessionId]: normalizeHistoryMessages(sessionId, page.messages)
463
+ },
464
+ sessionMessageLoadStatusBySession: {
465
+ ...state.sessionMessageLoadStatusBySession,
466
+ [sessionId]: "loaded"
467
+ },
468
+ sessionMessageLoadErrorBySession: {
469
+ ...state.sessionMessageLoadErrorBySession,
470
+ [sessionId]: null
471
+ },
472
+ historyMessagePaginationBySession: {
473
+ ...state.historyMessagePaginationBySession,
474
+ [sessionId]: createHistoryMessagePaginationState(page)
475
+ }
476
+ });
477
+ },
478
+ prependHistorySessionMessagesPage: (sessionId, page) => {
479
+ const state = get();
480
+ set({
481
+ messagesBySession: {
482
+ ...state.messagesBySession,
483
+ [sessionId]: mergeOlderHistoryMessages(
484
+ sessionId,
485
+ page.messages,
486
+ state.messagesBySession[sessionId] ?? []
487
+ )
488
+ },
489
+ historyMessagePaginationBySession: {
490
+ ...state.historyMessagePaginationBySession,
491
+ [sessionId]: createHistoryMessagePaginationState(page)
492
+ }
493
+ });
494
+ },
495
+ setHistorySessionPreviousMessagesLoadStatus: (sessionId, isLoading, error2 = null) => {
496
+ const state = get();
497
+ const current = state.historyMessagePaginationBySession[sessionId] ?? createHistoryMessagePaginationState();
498
+ set({
499
+ historyMessagePaginationBySession: {
500
+ ...state.historyMessagePaginationBySession,
501
+ [sessionId]: {
502
+ ...current,
503
+ isLoadingPrevious: isLoading,
504
+ error: error2
505
+ }
506
+ }
507
+ });
508
+ },
509
+ setHistorySessionMessageLoadStatus: (sessionId, status, error2 = null) => {
510
+ const state = get();
511
+ set({
512
+ sessionMessageLoadStatusBySession: {
513
+ ...state.sessionMessageLoadStatusBySession,
514
+ [sessionId]: status
515
+ },
516
+ sessionMessageLoadErrorBySession: {
517
+ ...state.sessionMessageLoadErrorBySession,
518
+ [sessionId]: error2
519
+ }
520
+ });
521
+ },
316
522
  startStreamingMessage: (sessionId, message) => {
317
523
  const state = get();
318
524
  set({
@@ -760,6 +966,10 @@ var AiChatProvider = (props) => {
760
966
  handleQuestionnaireSubmit,
761
967
  handleConfirmationSubmit,
762
968
  messageRenderOrder,
969
+ historySessionList,
970
+ onLoadMoreSessions,
971
+ onSelectHistorySession,
972
+ onLoadMoreHistoryMessages,
763
973
  enableImageAttachments = true,
764
974
  children
765
975
  } = props;
@@ -836,7 +1046,11 @@ var AiChatProvider = (props) => {
836
1046
  handleConfirmationSubmit,
837
1047
  messageRenderOrder,
838
1048
  transformStreamPacket: defaultTransformStreamPacket,
839
- enableImageAttachments
1049
+ enableImageAttachments,
1050
+ historySessionList,
1051
+ onLoadMoreSessions,
1052
+ onSelectHistorySession,
1053
+ onLoadMoreHistoryMessages
840
1054
  }),
841
1055
  [
842
1056
  axiosInstance,
@@ -846,8 +1060,12 @@ var AiChatProvider = (props) => {
846
1060
  enableImageAttachments,
847
1061
  handleConfirmationSubmit,
848
1062
  handleQuestionnaireSubmit,
1063
+ historySessionList,
849
1064
  labels,
850
1065
  messageRenderOrder,
1066
+ onLoadMoreSessions,
1067
+ onLoadMoreHistoryMessages,
1068
+ onSelectHistorySession,
851
1069
  renderMessageBlock,
852
1070
  sendRef,
853
1071
  retryRef,
@@ -3163,15 +3381,11 @@ var Bubble = styled7.article`
3163
3381
 
3164
3382
  &[data-role='user'] {
3165
3383
  width: auto;
3166
- max-width: min(760px, 100%);
3384
+ max-width: 100%;
3167
3385
  margin-left: auto;
3168
- padding: 14px 16px;
3169
- border-radius: 22px;
3170
- background: linear-gradient(180deg, rgba(59, 59, 63, 0.9) 0%, rgba(42, 43, 46, 0.92) 100%);
3171
- border: 1px solid rgba(255, 255, 255, 0.07);
3172
- box-shadow:
3173
- inset 0 1px 0 rgba(255, 255, 255, 0.03),
3174
- 0 12px 30px rgba(0, 0, 0, 0.18);
3386
+ padding: 8px 12px;
3387
+ background: #282825;
3388
+ border-radius: 16px;
3175
3389
  }
3176
3390
  `;
3177
3391
  var Header2 = styled7.div`
@@ -3503,6 +3717,7 @@ var HeroSubtitle = styled8.p`
3503
3717
  // src/components/chat-thread/index.tsx
3504
3718
  import { jsx as jsx10, jsxs as jsxs7 } from "@emotion/react/jsx-runtime";
3505
3719
  var CHAT_THREAD_PINNED_THRESHOLD_PX = 32;
3720
+ var CHAT_THREAD_LOAD_PREVIOUS_THRESHOLD_PX = 80;
3506
3721
  var isThreadPinnedToBottom = (container) => container.scrollHeight - container.clientHeight - container.scrollTop <= CHAT_THREAD_PINNED_THRESHOLD_PX;
3507
3722
  var renderChatMessage = ({
3508
3723
  message,
@@ -3576,9 +3791,13 @@ var ChatThreadView = ({
3576
3791
  historyMessages,
3577
3792
  streamingMessage,
3578
3793
  error: error2,
3794
+ isLoadingPreviousMessages = false,
3795
+ previousMessagesError,
3579
3796
  retryButtonLabel,
3580
3797
  scrollToLatestLabel,
3798
+ sessionHistoryLoadingLabel,
3581
3799
  onRetry,
3800
+ onLoadPreviousMessages,
3582
3801
  onConfirmationSubmit,
3583
3802
  onQuestionnaireSubmit,
3584
3803
  renderMessageBlock
@@ -3593,6 +3812,7 @@ var ChatThreadView = ({
3593
3812
  const latestHistoryMessage = historyMessages[historyMessages.length - 1];
3594
3813
  const latestTurnRef = useRef5(null);
3595
3814
  const reservedSpaceFrameRef = useRef5(null);
3815
+ const isLoadingPreviousRef = useRef5(false);
3596
3816
  const isPinnedRef = useRef5(true);
3597
3817
  const lastHistoryMessageIdRef = useRef5(latestHistoryMessage?.id);
3598
3818
  const lastStreamingMessageIdRef = useRef5(streamingMessage?.id);
@@ -3639,17 +3859,45 @@ var ChatThreadView = ({
3639
3859
  },
3640
3860
  [markThreadPinned, scrollToBottom]
3641
3861
  );
3862
+ const handleLoadPreviousMessages = useCallback3(async () => {
3863
+ const container = containerRef.current;
3864
+ if (!container || !onLoadPreviousMessages || isLoadingPreviousMessages) {
3865
+ return;
3866
+ }
3867
+ if (isLoadingPreviousRef.current) {
3868
+ return;
3869
+ }
3870
+ isLoadingPreviousRef.current = true;
3871
+ const previousScrollHeight = container.scrollHeight;
3872
+ const previousScrollTop = container.scrollTop;
3873
+ try {
3874
+ await onLoadPreviousMessages();
3875
+ } catch {
3876
+ return;
3877
+ } finally {
3878
+ isLoadingPreviousRef.current = false;
3879
+ }
3880
+ window.requestAnimationFrame(() => {
3881
+ const nextContainer = containerRef.current;
3882
+ if (!nextContainer)
3883
+ return;
3884
+ nextContainer.scrollTop = nextContainer.scrollHeight - previousScrollHeight + previousScrollTop;
3885
+ });
3886
+ }, [isLoadingPreviousMessages, onLoadPreviousMessages]);
3642
3887
  const handleContainerScroll = useCallback3(() => {
3643
3888
  const container = containerRef.current;
3644
3889
  if (!container)
3645
3890
  return;
3891
+ if (onLoadPreviousMessages && container.scrollTop <= CHAT_THREAD_LOAD_PREVIOUS_THRESHOLD_PX) {
3892
+ void handleLoadPreviousMessages();
3893
+ }
3646
3894
  const nextPinned = isThreadPinnedToBottom(container);
3647
3895
  isPinnedRef.current = nextPinned;
3648
3896
  setIsDetached(!nextPinned);
3649
3897
  if (nextPinned) {
3650
3898
  setPendingNewMessageCount(0);
3651
3899
  }
3652
- }, []);
3900
+ }, [handleLoadPreviousMessages, onLoadPreviousMessages]);
3653
3901
  useLayoutEffect3(() => {
3654
3902
  const nextHistoryMessageId = latestHistoryMessage?.id;
3655
3903
  if (lastHistoryMessageIdRef.current === nextHistoryMessageId) {
@@ -3777,6 +4025,8 @@ var ChatThreadView = ({
3777
4025
  }, [latestTurn, scrollToBottom]);
3778
4026
  return /* @__PURE__ */ jsxs7(ThreadViewport, { children: [
3779
4027
  /* @__PURE__ */ jsxs7(Container, { ref: containerRef, "data-testid": "chat-thread", onScroll: handleContainerScroll, children: [
4028
+ isLoadingPreviousMessages ? /* @__PURE__ */ jsx10(PreviousMessagesStateRow, { "data-testid": "chat-thread-loading-previous", children: sessionHistoryLoadingLabel }) : null,
4029
+ previousMessagesError ? /* @__PURE__ */ jsx10(PreviousMessagesStateRow, { "data-testid": "chat-thread-load-previous-error", children: previousMessagesError }) : null,
3780
4030
  conversationTurns.map((turn, turnIndex) => {
3781
4031
  const isLatestTurn = turnIndex === conversationTurns.length - 1;
3782
4032
  return /* @__PURE__ */ jsxs7(
@@ -3833,14 +4083,25 @@ var EMPTY_MESSAGES = [];
3833
4083
  var ChatThread = () => {
3834
4084
  const activeSessionId = useChatStore((s) => s.activeSessionId);
3835
4085
  const hasSessions = useChatStore((s) => s.sessions.length > 0);
3836
- const activeSessionMode = useChatStore(
3837
- (s) => s.sessions.find((x) => x.sessionId === s.activeSessionId)?.mode ?? DEFAULT_CHAT_AGENT_MODE
4086
+ const activeSession = useChatStore(
4087
+ (s) => s.sessions.find((session) => session.sessionId === s.activeSessionId)
3838
4088
  );
4089
+ const activeSessionMode = activeSession?.mode ?? DEFAULT_CHAT_AGENT_MODE;
3839
4090
  const messages = useChatStore(
3840
4091
  (s) => s.messagesBySession[s.activeSessionId ?? ""] ?? EMPTY_MESSAGES
3841
4092
  );
4093
+ const sessionMessageLoadStatus = useChatStore(
4094
+ (s) => s.sessionMessageLoadStatusBySession[s.activeSessionId ?? ""]
4095
+ );
3842
4096
  const streamingMessage = useChatStore((s) => s.streamingMessageBySession[s.activeSessionId ?? ""]);
3843
4097
  const error2 = useChatStore((s) => s.errorBySession[s.activeSessionId ?? ""]);
4098
+ const historyMessagePagination = useChatStore(
4099
+ (s) => s.historyMessagePaginationBySession[s.activeSessionId ?? ""]
4100
+ );
4101
+ const prependHistorySessionMessagesPage = useChatStore((s) => s.prependHistorySessionMessagesPage);
4102
+ const setHistorySessionPreviousMessagesLoadStatus = useChatStore(
4103
+ (s) => s.setHistorySessionPreviousMessagesLoadStatus
4104
+ );
3844
4105
  const updateQA = useChatStore((s) => s.updateQuestionnaireAnswers);
3845
4106
  const clearSessionError = useChatStore((s) => s.clearSessionError);
3846
4107
  const {
@@ -3849,6 +4110,7 @@ var ChatThread = () => {
3849
4110
  renderMessageBlock,
3850
4111
  handleQuestionnaireSubmit: customQuestionnaireSubmit,
3851
4112
  handleConfirmationSubmit: customConfirmationSubmit,
4113
+ onLoadMoreHistoryMessages,
3852
4114
  labels
3853
4115
  } = useChatContext();
3854
4116
  const handleRetry = useCallback3(() => {
@@ -3911,6 +4173,42 @@ var ChatThread = () => {
3911
4173
  },
3912
4174
  [activeSessionId, activeSessionMode, sendRef, customConfirmationSubmit]
3913
4175
  );
4176
+ const handleLoadPreviousMessages = useCallback3(async () => {
4177
+ if (!activeSession || !onLoadMoreHistoryMessages || !historyMessagePagination?.hasMorePrevious || !historyMessagePagination.previousCursor || historyMessagePagination.isLoadingPrevious) {
4178
+ return;
4179
+ }
4180
+ setHistorySessionPreviousMessagesLoadStatus(activeSession.sessionId, true);
4181
+ try {
4182
+ const page = await onLoadMoreHistoryMessages({
4183
+ session: activeSession,
4184
+ cursor: historyMessagePagination.previousCursor
4185
+ });
4186
+ if (page) {
4187
+ prependHistorySessionMessagesPage(activeSession.sessionId, page);
4188
+ return;
4189
+ }
4190
+ setHistorySessionPreviousMessagesLoadStatus(activeSession.sessionId, false);
4191
+ } catch (error3) {
4192
+ setHistorySessionPreviousMessagesLoadStatus(
4193
+ activeSession.sessionId,
4194
+ false,
4195
+ error3 instanceof Error ? error3.message : String(error3)
4196
+ );
4197
+ throw error3;
4198
+ }
4199
+ }, [
4200
+ activeSession,
4201
+ historyMessagePagination,
4202
+ onLoadMoreHistoryMessages,
4203
+ prependHistorySessionMessagesPage,
4204
+ setHistorySessionPreviousMessagesLoadStatus
4205
+ ]);
4206
+ const canLoadPreviousMessages = Boolean(
4207
+ activeSession && onLoadMoreHistoryMessages && historyMessagePagination?.hasMorePrevious && historyMessagePagination.previousCursor && !historyMessagePagination.isLoadingPrevious
4208
+ );
4209
+ if (hasSessions && sessionMessageLoadStatus === "loading" && messages.length === 0 && !streamingMessage) {
4210
+ return /* @__PURE__ */ jsx10(ThreadStateViewport, { "data-testid": "chat-thread-loading-state", children: /* @__PURE__ */ jsx10(ThreadStateText, { children: labels.sessionHistoryLoading }) });
4211
+ }
3914
4212
  if (!hasSessions || messages.length === 0 && !streamingMessage) {
3915
4213
  return /* @__PURE__ */ jsx10(ChatThreadEmptyState, {});
3916
4214
  }
@@ -3921,9 +4219,13 @@ var ChatThread = () => {
3921
4219
  historyMessages: messages,
3922
4220
  streamingMessage,
3923
4221
  error: error2,
4222
+ isLoadingPreviousMessages: historyMessagePagination?.isLoadingPrevious,
4223
+ previousMessagesError: historyMessagePagination?.error,
3924
4224
  retryButtonLabel: labels.retryButton,
3925
4225
  scrollToLatestLabel: labels.scrollToLatest,
4226
+ sessionHistoryLoadingLabel: labels.sessionHistoryLoading,
3926
4227
  onRetry: handleRetry,
4228
+ onLoadPreviousMessages: canLoadPreviousMessages ? handleLoadPreviousMessages : void 0,
3927
4229
  onConfirmationSubmit: handleConfirmation,
3928
4230
  onQuestionnaireSubmit: handleQuestionnaireSubmit,
3929
4231
  renderMessageBlock
@@ -3937,6 +4239,18 @@ var ThreadViewport = styled9.div`
3937
4239
  flex: 1;
3938
4240
  min-height: 0;
3939
4241
  `;
4242
+ var ThreadStateViewport = styled9.div`
4243
+ display: flex;
4244
+ flex: 1;
4245
+ min-height: 0;
4246
+ align-items: center;
4247
+ justify-content: center;
4248
+ padding: 24px;
4249
+ `;
4250
+ var ThreadStateText = styled9.div`
4251
+ color: var(--text-secondary, rgba(255, 255, 255, 0.64));
4252
+ font-size: 14px;
4253
+ `;
3940
4254
  var Container = styled9.div`
3941
4255
  display: flex;
3942
4256
  flex: 1;
@@ -3944,7 +4258,7 @@ var Container = styled9.div`
3944
4258
  gap: 18px;
3945
4259
  min-height: 0;
3946
4260
  overflow: auto;
3947
- padding: 24px 24px 88px;
4261
+ padding: 24px 16px 88px;
3948
4262
  overscroll-behavior: contain;
3949
4263
 
3950
4264
  &::-webkit-scrollbar {
@@ -3963,10 +4277,23 @@ var Container = styled9.div`
3963
4277
  var MessageSlot = styled9.div`
3964
4278
  display: flex;
3965
4279
  `;
4280
+ var PreviousMessagesStateRow = styled9.div`
4281
+ width: 100%;
4282
+ max-width: var(--chat-content-max-width, 48rem);
4283
+ margin-right: auto;
4284
+ margin-left: auto;
4285
+ color: var(--text-secondary, rgba(255, 255, 255, 0.64));
4286
+ font-size: 13px;
4287
+ text-align: center;
4288
+ `;
3966
4289
  var ConversationTurn = styled9.div`
3967
4290
  display: flex;
3968
4291
  flex-direction: column;
3969
4292
  gap: 18px;
4293
+ width: 100%;
4294
+ max-width: var(--chat-content-max-width, 48rem);
4295
+ margin-right: auto;
4296
+ margin-left: auto;
3970
4297
  `;
3971
4298
  var ErrorText = styled9.div`
3972
4299
  color: #ff7b72;
@@ -7754,7 +8081,7 @@ var PrimaryButton = styled13(Button)`
7754
8081
  min-width: 24px;
7755
8082
  width: 24px;
7756
8083
  height: 24px;
7757
- background: ${({ $canSend }) => $canSend ? "#fcfbf8" : "rgba(255, 255, 255, 0.3)"};
8084
+ background: ${({ $canSend }) => $canSend ? "#fcfbf8" : "rgba(252,251,248,0.3);"};
7758
8085
  color: ${({ $canSend }) => $canSend ? "#5b5448" : "rgba(255, 255, 255, 0.72)"};
7759
8086
  border-radius: 12px;
7760
8087
  border: 1px solid ${({ $canSend }) => $canSend ? "rgba(198, 188, 170, 0.38)" : "transparent"};
@@ -7765,7 +8092,7 @@ var PrimaryButton = styled13(Button)`
7765
8092
  }
7766
8093
 
7767
8094
  &:hover:not(:disabled) {
7768
- background: ${({ $canSend }) => $canSend ? "#f7f4ec" : "rgba(255, 255, 255, 0.3)"};
8095
+ background: ${({ $canSend }) => $canSend ? "#f7f4ec" : "rgba(252,251,248,0.3);"};
7769
8096
  color: ${({ $canSend }) => $canSend ? "#4f493f" : "rgba(255, 255, 255, 0.72)"};
7770
8097
  border-color: ${({ $canSend }) => $canSend ? "rgba(198, 188, 170, 0.46)" : "transparent"};
7771
8098
  }
@@ -7833,7 +8160,7 @@ var StopSpinner = styled13.span`
7833
8160
  import { jsx as jsx16, jsxs as jsxs11 } from "@emotion/react/jsx-runtime";
7834
8161
  var CHAT_COMPOSER_LINE_HEIGHT_PX = 20;
7835
8162
  var CHAT_COMPOSER_MAX_ROWS = 7;
7836
- var CHAT_COMPOSER_PADDING_TOP_PX = 8;
8163
+ var CHAT_COMPOSER_PADDING_TOP_PX = 12;
7837
8164
  var CHAT_COMPOSER_PADDING_BOTTOM_PX = 12;
7838
8165
  var CHAT_COMPOSER_PADDING_BLOCK_PX = CHAT_COMPOSER_PADDING_TOP_PX + CHAT_COMPOSER_PADDING_BOTTOM_PX;
7839
8166
  var CHAT_COMPOSER_MIN_ROWS = 4;
@@ -8068,6 +8395,10 @@ var ChatComposerView = ({
8068
8395
  setActiveSkillNavigation({ queryKey: "", index: 0 });
8069
8396
  };
8070
8397
  const handleKeyDown = (event) => {
8398
+ const isImeComposing = event.nativeEvent.isComposing || event.keyCode === 229;
8399
+ if (event.key === "Enter" && isImeComposing) {
8400
+ return;
8401
+ }
8071
8402
  if (skillQueryMatch) {
8072
8403
  if (event.key === "ArrowDown" && filteredSkills.length > 0) {
8073
8404
  event.preventDefault();
@@ -8345,7 +8676,7 @@ var Surface = styled14.div`
8345
8676
  'input'
8346
8677
  'footer';
8347
8678
  width: 100%;
8348
- max-width: 760px;
8679
+ max-width: var(--chat-content-max-width, 48rem);
8349
8680
  margin: 0 auto;
8350
8681
  background: var(--border-color);
8351
8682
  border-radius: 20px;
@@ -8606,6 +8937,7 @@ var SkillButton = styled14.button`
8606
8937
  `;
8607
8938
 
8608
8939
  // src/components/chat-conversation-list/index.tsx
8940
+ import { useEffect as useEffect10, useMemo as useMemo7, useRef as useRef12 } from "react";
8609
8941
  import styled16 from "@emotion/styled";
8610
8942
 
8611
8943
  // src/components/chat-conversation-list/components/chat-session-item.tsx
@@ -8619,6 +8951,7 @@ var ChatSessionItem = memo2(
8619
8951
  {
8620
8952
  type: "button",
8621
8953
  "data-active": isActive,
8954
+ "data-testid": `chat-session-item-${session.sessionId}`,
8622
8955
  onClick: () => onClick(session.sessionId),
8623
8956
  children: /* @__PURE__ */ jsxs12(SessionMeta, { children: [
8624
8957
  /* @__PURE__ */ jsx17(SessionTitle, { children: session.title }),
@@ -8666,14 +8999,130 @@ var ModeBadge = styled15.span`
8666
8999
  background: rgba(255, 255, 255, 0.04);
8667
9000
  `;
8668
9001
 
9002
+ // src/components/chat-conversation-list/lib/history-session-selection.ts
9003
+ var shouldLoadHistorySessionMessages = ({
9004
+ sessionId,
9005
+ messagesBySession,
9006
+ loadStatusBySession,
9007
+ isStreamingBySession
9008
+ }) => {
9009
+ if (isStreamingBySession[sessionId])
9010
+ return false;
9011
+ if (loadStatusBySession[sessionId] === "loading")
9012
+ return false;
9013
+ if (loadStatusBySession[sessionId] === "error")
9014
+ return true;
9015
+ if (loadStatusBySession[sessionId] === "loaded")
9016
+ return false;
9017
+ return messagesBySession[sessionId] === void 0;
9018
+ };
9019
+
8669
9020
  // src/components/chat-conversation-list/index.tsx
8670
9021
  import { jsx as jsx18, jsxs as jsxs13 } from "@emotion/react/jsx-runtime";
9022
+ var SCROLL_LOAD_MORE_THRESHOLD_PX = 80;
9023
+ var shouldLoadMoreSessions = ({
9024
+ scrollTop,
9025
+ clientHeight,
9026
+ scrollHeight,
9027
+ threshold = SCROLL_LOAD_MORE_THRESHOLD_PX
9028
+ }) => scrollHeight - scrollTop - clientHeight <= threshold;
9029
+ var isHistorySessionMessagesPage = (value) => typeof value === "object" && value !== null && Array.isArray(value.messages);
8671
9030
  var ChatConversationList = () => {
8672
- const { labels } = useChatContext();
8673
- const sessions = useChatStore((s) => s.sessions);
9031
+ const { labels, historySessionList, onLoadMoreSessions, onSelectHistorySession, store } = useChatContext();
9032
+ const localSessions = useChatStore((s) => s.sessions);
8674
9033
  const activeSessionId = useChatStore((s) => s.activeSessionId);
8675
9034
  const startNewChat = useChatStore((s) => s.startNewChat);
8676
9035
  const setActiveSession = useChatStore((s) => s.setActiveSession);
9036
+ const hydrateHistorySessions = useChatStore((s) => s.hydrateHistorySessions);
9037
+ const hydrateHistorySessionMessages = useChatStore((s) => s.hydrateHistorySessionMessages);
9038
+ const hydrateHistorySessionMessagesPage = useChatStore((s) => s.hydrateHistorySessionMessagesPage);
9039
+ const setHistorySessionMessageLoadStatus = useChatStore(
9040
+ (s) => s.setHistorySessionMessageLoadStatus
9041
+ );
9042
+ const isLoadingMoreRef = useRef12(false);
9043
+ const hasSeenLoadingMoreRef = useRef12(false);
9044
+ useEffect10(() => {
9045
+ if (!historySessionList)
9046
+ return;
9047
+ hydrateHistorySessions(historySessionList.sessions);
9048
+ }, [historySessionList, hydrateHistorySessions]);
9049
+ useEffect10(() => {
9050
+ if (historySessionList?.isLoading) {
9051
+ hasSeenLoadingMoreRef.current = true;
9052
+ return;
9053
+ }
9054
+ if (hasSeenLoadingMoreRef.current) {
9055
+ hasSeenLoadingMoreRef.current = false;
9056
+ isLoadingMoreRef.current = false;
9057
+ }
9058
+ }, [historySessionList?.isLoading]);
9059
+ useEffect10(() => {
9060
+ isLoadingMoreRef.current = false;
9061
+ hasSeenLoadingMoreRef.current = false;
9062
+ }, [historySessionList?.sessions.length, historySessionList?.hasMore]);
9063
+ const sessions = useMemo7(() => {
9064
+ if (!historySessionList) {
9065
+ return localSessions;
9066
+ }
9067
+ const localSessionIds = new Set(localSessions.map((session) => session.sessionId));
9068
+ return [
9069
+ ...localSessions,
9070
+ ...historySessionList.sessions.filter((session) => !localSessionIds.has(session.sessionId))
9071
+ ];
9072
+ }, [historySessionList, localSessions]);
9073
+ const handleSessionListScroll = (event) => {
9074
+ if (!historySessionList?.hasMore || historySessionList.isLoading || !onLoadMoreSessions || isLoadingMoreRef.current) {
9075
+ return;
9076
+ }
9077
+ const target = event.currentTarget;
9078
+ if (shouldLoadMoreSessions({
9079
+ scrollTop: target.scrollTop,
9080
+ clientHeight: target.clientHeight,
9081
+ scrollHeight: target.scrollHeight
9082
+ })) {
9083
+ isLoadingMoreRef.current = true;
9084
+ void Promise.resolve(onLoadMoreSessions()).catch(() => {
9085
+ isLoadingMoreRef.current = false;
9086
+ hasSeenLoadingMoreRef.current = false;
9087
+ });
9088
+ }
9089
+ };
9090
+ const handleSelectSession = async (sessionId) => {
9091
+ setActiveSession(sessionId);
9092
+ const session = sessions.find((item) => item.sessionId === sessionId);
9093
+ if (!session || !onSelectHistorySession) {
9094
+ return;
9095
+ }
9096
+ const state = store.getState();
9097
+ const shouldLoad = shouldLoadHistorySessionMessages({
9098
+ sessionId,
9099
+ messagesBySession: state.messagesBySession,
9100
+ loadStatusBySession: state.sessionMessageLoadStatusBySession,
9101
+ isStreamingBySession: state.isStreamingBySession
9102
+ });
9103
+ if (!shouldLoad) {
9104
+ return;
9105
+ }
9106
+ setHistorySessionMessageLoadStatus(sessionId, "loading");
9107
+ try {
9108
+ const result = await onSelectHistorySession(session);
9109
+ if (Array.isArray(result)) {
9110
+ hydrateHistorySessionMessages(sessionId, result);
9111
+ return;
9112
+ }
9113
+ if (isHistorySessionMessagesPage(result)) {
9114
+ hydrateHistorySessionMessagesPage(sessionId, result);
9115
+ return;
9116
+ }
9117
+ setHistorySessionMessageLoadStatus(sessionId, "loaded");
9118
+ } catch (error2) {
9119
+ setHistorySessionMessageLoadStatus(
9120
+ sessionId,
9121
+ "error",
9122
+ error2 instanceof Error ? error2.message : String(error2)
9123
+ );
9124
+ }
9125
+ };
8677
9126
  const modeLabels = {
8678
9127
  ask: labels.modeLabelAsk,
8679
9128
  plan: labels.modeLabelPlan,
@@ -8684,16 +9133,21 @@ var ChatConversationList = () => {
8684
9133
  /* @__PURE__ */ jsx18(Title3, { children: "Sessions" }),
8685
9134
  /* @__PURE__ */ jsx18(CreateButton, { type: "button", "data-testid": "chat-create-session", onClick: startNewChat, children: labels.newChat })
8686
9135
  ] }),
8687
- /* @__PURE__ */ jsx18(List2, { "data-testid": "chat-session-list", children: sessions.map((session) => /* @__PURE__ */ jsx18(
8688
- ChatSessionItem,
8689
- {
8690
- session,
8691
- isActive: activeSessionId === session.sessionId,
8692
- modeLabel: modeLabels[session.mode ?? DEFAULT_CHAT_AGENT_MODE] ?? "",
8693
- onClick: setActiveSession
8694
- },
8695
- session.sessionId
8696
- )) })
9136
+ /* @__PURE__ */ jsxs13(List2, { "data-testid": "chat-session-list", onScroll: handleSessionListScroll, children: [
9137
+ sessions.map((session) => /* @__PURE__ */ jsx18(
9138
+ ChatSessionItem,
9139
+ {
9140
+ session,
9141
+ isActive: activeSessionId === session.sessionId,
9142
+ modeLabel: modeLabels[session.mode ?? DEFAULT_CHAT_AGENT_MODE] ?? "",
9143
+ onClick: (sessionId) => void handleSelectSession(sessionId)
9144
+ },
9145
+ session.sessionId
9146
+ )),
9147
+ historySessionList?.isLoading ? /* @__PURE__ */ jsx18(StateRow, { "data-testid": "chat-session-history-loading", children: labels.sessionHistoryLoading }) : null,
9148
+ historySessionList?.error ? /* @__PURE__ */ jsx18(StateRow, { "data-testid": "chat-session-history-error", children: historySessionList.error || labels.sessionHistoryLoadFailed }) : null,
9149
+ historySessionList && !historySessionList.isLoading && !historySessionList.error && sessions.length === 0 ? /* @__PURE__ */ jsx18(StateRow, { "data-testid": "chat-session-history-empty", children: labels.sessionHistoryEmpty }) : null
9150
+ ] })
8697
9151
  ] });
8698
9152
  };
8699
9153
  var Container3 = styled16.aside`
@@ -8731,6 +9185,11 @@ var List2 = styled16.div`
8731
9185
  gap: 8px;
8732
9186
  overflow: auto;
8733
9187
  `;
9188
+ var StateRow = styled16.div`
9189
+ padding: 12px;
9190
+ font-size: 13px;
9191
+ color: var(--text-secondary);
9192
+ `;
8734
9193
 
8735
9194
  // src/components/ai-chat/index.tsx
8736
9195
  import { Fragment as Fragment6, jsx as jsx19, jsxs as jsxs14 } from "@emotion/react/jsx-runtime";
@@ -8792,7 +9251,7 @@ var AiChatWorkspaceContent = ({
8792
9251
  })
8793
9252
  );
8794
9253
  const shouldShowComposerOnly = showComposerOnlyBeforeFirstMessage && !showConversationList && !isConversationStarted;
8795
- useEffect10(() => {
9254
+ useEffect11(() => {
8796
9255
  onConversationStartedChange?.(isConversationStarted);
8797
9256
  }, [isConversationStarted, onConversationStartedChange]);
8798
9257
  return /* @__PURE__ */ jsxs14(Root, { "data-testid": "ai-chat", children: [
@@ -8931,12 +9390,25 @@ var Root = styled17.div`
8931
9390
  overflow: hidden;
8932
9391
  `;
8933
9392
  var Workspace = styled17.section`
9393
+ --chat-layout-rem: 16px;
9394
+ --chat-content-margin: calc(var(--chat-layout-rem) * 1);
9395
+ --chat-content-max-width: calc(var(--chat-layout-rem) * 40);
9396
+
8934
9397
  flex: 1;
8935
9398
  display: flex;
8936
9399
  flex-direction: column;
8937
9400
  gap: 12px;
8938
9401
  min-height: 0;
8939
9402
  overflow: hidden;
9403
+
9404
+ @media (min-width: 640px) {
9405
+ --chat-content-margin: calc(var(--chat-layout-rem) * 1.5);
9406
+ }
9407
+
9408
+ @media (min-width: 1024px) {
9409
+ --chat-content-margin: calc(var(--chat-layout-rem) * 4);
9410
+ --chat-content-max-width: calc(var(--chat-layout-rem) * 48);
9411
+ }
8940
9412
  `;
8941
9413
  var QuickActionsRow = styled17.div`
8942
9414
  display: flex;