@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.js CHANGED
@@ -46,7 +46,7 @@ __export(src_exports, {
46
46
  module.exports = __toCommonJS(src_exports);
47
47
 
48
48
  // src/components/ai-chat/index.tsx
49
- var import_react20 = require("react");
49
+ var import_react21 = require("react");
50
50
  var import_styled17 = __toESM(require("@emotion/styled"));
51
51
  var import_compass_ui4 = require("@xinghunm/compass-ui");
52
52
 
@@ -100,7 +100,10 @@ var DEFAULT_AI_CHAT_LABELS = {
100
100
  modelUnavailable: "No model available",
101
101
  skillLoading: "Loading skills...",
102
102
  skillEmpty: "No matching skills",
103
- removeSkillAriaLabel: "Remove skill"
103
+ removeSkillAriaLabel: "Remove skill",
104
+ sessionHistoryLoading: "Loading conversations...",
105
+ sessionHistoryLoadFailed: "Failed to load conversations",
106
+ sessionHistoryEmpty: "No conversations yet"
104
107
  };
105
108
 
106
109
  // src/lib/chat-session.ts
@@ -128,6 +131,20 @@ var createDraftChatSession = ({
128
131
  // src/store/chat-store.ts
129
132
  var DEFAULT_CHAT_SESSION_TITLE = "New Chat";
130
133
  var IMAGE_MESSAGE_SESSION_TITLE = "Image message";
134
+ var createHistoryMessagePaginationState = (page) => ({
135
+ previousCursor: page?.previousCursor ?? null,
136
+ hasMorePrevious: page?.hasMorePrevious ?? Boolean(page && page.previousCursor !== null),
137
+ isLoadingPrevious: false,
138
+ error: null
139
+ });
140
+ var normalizeHistoryMessages = (sessionId, messages) => messages.map((message) => ({ ...message, sessionId }));
141
+ var mergeOlderHistoryMessages = (sessionId, olderMessages, currentMessages) => {
142
+ const currentMessageIds = new Set(currentMessages.map((message) => message.id));
143
+ const uniqueOlderMessages = normalizeHistoryMessages(sessionId, olderMessages).filter(
144
+ (message) => !currentMessageIds.has(message.id)
145
+ );
146
+ return [...uniqueOlderMessages, ...currentMessages];
147
+ };
131
148
  var resolveSessionTitleFromMessage = (message) => {
132
149
  const trimmedContent = message.content.trim();
133
150
  if (trimmedContent) {
@@ -230,6 +247,9 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
230
247
  isStreamingBySession: {},
231
248
  isStoppingBySession: {},
232
249
  errorBySession: {},
250
+ sessionMessageLoadStatusBySession: {},
251
+ sessionMessageLoadErrorBySession: {},
252
+ historyMessagePaginationBySession: {},
233
253
  // ---- Session management ------------------------------------------------
234
254
  createSession: (session) => {
235
255
  const state = get();
@@ -243,6 +263,15 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
243
263
  const nextErrorBySession = { ...state.errorBySession };
244
264
  const nextIsStreamingBySession = { ...state.isStreamingBySession };
245
265
  const nextIsStoppingBySession = { ...state.isStoppingBySession };
266
+ const nextSessionMessageLoadStatusBySession = {
267
+ ...state.sessionMessageLoadStatusBySession
268
+ };
269
+ const nextSessionMessageLoadErrorBySession = {
270
+ ...state.sessionMessageLoadErrorBySession
271
+ };
272
+ const nextHistoryMessagePaginationBySession = {
273
+ ...state.historyMessagePaginationBySession
274
+ };
246
275
  const sid = session.sessionId;
247
276
  if (nextMessagesBySession[sid] === void 0)
248
277
  nextMessagesBySession[sid] = [];
@@ -252,13 +281,25 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
252
281
  nextIsStreamingBySession[sid] = false;
253
282
  if (nextIsStoppingBySession[sid] === void 0)
254
283
  nextIsStoppingBySession[sid] = false;
284
+ if (nextSessionMessageLoadStatusBySession[sid] === void 0) {
285
+ nextSessionMessageLoadStatusBySession[sid] = "loaded";
286
+ }
287
+ if (nextSessionMessageLoadErrorBySession[sid] === void 0) {
288
+ nextSessionMessageLoadErrorBySession[sid] = null;
289
+ }
290
+ if (nextHistoryMessagePaginationBySession[sid] === void 0) {
291
+ nextHistoryMessagePaginationBySession[sid] = createHistoryMessagePaginationState();
292
+ }
255
293
  set({
256
294
  sessions: nextSessions,
257
295
  activeSessionId: sid,
258
296
  messagesBySession: nextMessagesBySession,
259
297
  errorBySession: nextErrorBySession,
260
298
  isStreamingBySession: nextIsStreamingBySession,
261
- isStoppingBySession: nextIsStoppingBySession
299
+ isStoppingBySession: nextIsStoppingBySession,
300
+ sessionMessageLoadStatusBySession: nextSessionMessageLoadStatusBySession,
301
+ sessionMessageLoadErrorBySession: nextSessionMessageLoadErrorBySession,
302
+ historyMessagePaginationBySession: nextHistoryMessagePaginationBySession
262
303
  });
263
304
  },
264
305
  startNewChat: () => {
@@ -313,6 +354,27 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
313
354
  nextErrorBySession[nextSessionId] = nextErrorBySession[previousSessionId] ?? null;
314
355
  delete nextErrorBySession[previousSessionId];
315
356
  }
357
+ const nextSessionMessageLoadStatusBySession = {
358
+ ...state.sessionMessageLoadStatusBySession
359
+ };
360
+ if (previousSessionId in nextSessionMessageLoadStatusBySession) {
361
+ nextSessionMessageLoadStatusBySession[nextSessionId] = nextSessionMessageLoadStatusBySession[previousSessionId] ?? "idle";
362
+ delete nextSessionMessageLoadStatusBySession[previousSessionId];
363
+ }
364
+ const nextSessionMessageLoadErrorBySession = {
365
+ ...state.sessionMessageLoadErrorBySession
366
+ };
367
+ if (previousSessionId in nextSessionMessageLoadErrorBySession) {
368
+ nextSessionMessageLoadErrorBySession[nextSessionId] = nextSessionMessageLoadErrorBySession[previousSessionId] ?? null;
369
+ delete nextSessionMessageLoadErrorBySession[previousSessionId];
370
+ }
371
+ const nextHistoryMessagePaginationBySession = {
372
+ ...state.historyMessagePaginationBySession
373
+ };
374
+ if (previousSessionId in nextHistoryMessagePaginationBySession) {
375
+ nextHistoryMessagePaginationBySession[nextSessionId] = nextHistoryMessagePaginationBySession[previousSessionId] ?? createHistoryMessagePaginationState();
376
+ delete nextHistoryMessagePaginationBySession[previousSessionId];
377
+ }
316
378
  const nextActiveSessionId = state.activeSessionId === previousSessionId ? nextSessionId : state.activeSessionId;
317
379
  set({
318
380
  sessions: nextSessions,
@@ -321,6 +383,9 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
321
383
  isStreamingBySession: nextIsStreamingBySession,
322
384
  isStoppingBySession: nextIsStoppingBySession,
323
385
  errorBySession: nextErrorBySession,
386
+ sessionMessageLoadStatusBySession: nextSessionMessageLoadStatusBySession,
387
+ sessionMessageLoadErrorBySession: nextSessionMessageLoadErrorBySession,
388
+ historyMessagePaginationBySession: nextHistoryMessagePaginationBySession,
324
389
  activeSessionId: nextActiveSessionId
325
390
  });
326
391
  },
@@ -334,6 +399,61 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
334
399
  );
335
400
  set({ sessions: nextSessions });
336
401
  },
402
+ hydrateHistorySessions: (sessions) => {
403
+ const state = get();
404
+ const localSessions = state.sessions.filter(
405
+ (session) => isDraftChatSessionId(session.sessionId)
406
+ );
407
+ const localSessionIds = new Set(localSessions.map((session) => session.sessionId));
408
+ const nextSessions = [
409
+ ...localSessions,
410
+ ...sessions.filter((session) => !localSessionIds.has(session.sessionId)).map((session) => ({
411
+ ...session,
412
+ mode: session.mode ?? DEFAULT_CHAT_AGENT_MODE
413
+ }))
414
+ ];
415
+ const nextMessagesBySession = { ...state.messagesBySession };
416
+ const nextErrorBySession = { ...state.errorBySession };
417
+ const nextIsStreamingBySession = { ...state.isStreamingBySession };
418
+ const nextIsStoppingBySession = { ...state.isStoppingBySession };
419
+ const nextSessionMessageLoadStatusBySession = {
420
+ ...state.sessionMessageLoadStatusBySession
421
+ };
422
+ const nextSessionMessageLoadErrorBySession = {
423
+ ...state.sessionMessageLoadErrorBySession
424
+ };
425
+ const nextHistoryMessagePaginationBySession = {
426
+ ...state.historyMessagePaginationBySession
427
+ };
428
+ nextSessions.forEach((session) => {
429
+ const sid = session.sessionId;
430
+ if (nextErrorBySession[sid] === void 0)
431
+ nextErrorBySession[sid] = null;
432
+ if (nextIsStreamingBySession[sid] === void 0)
433
+ nextIsStreamingBySession[sid] = false;
434
+ if (nextIsStoppingBySession[sid] === void 0)
435
+ nextIsStoppingBySession[sid] = false;
436
+ if (nextSessionMessageLoadStatusBySession[sid] === void 0) {
437
+ nextSessionMessageLoadStatusBySession[sid] = "idle";
438
+ }
439
+ if (nextSessionMessageLoadErrorBySession[sid] === void 0) {
440
+ nextSessionMessageLoadErrorBySession[sid] = null;
441
+ }
442
+ if (nextHistoryMessagePaginationBySession[sid] === void 0) {
443
+ nextHistoryMessagePaginationBySession[sid] = createHistoryMessagePaginationState();
444
+ }
445
+ });
446
+ set({
447
+ sessions: nextSessions,
448
+ messagesBySession: nextMessagesBySession,
449
+ errorBySession: nextErrorBySession,
450
+ isStreamingBySession: nextIsStreamingBySession,
451
+ isStoppingBySession: nextIsStoppingBySession,
452
+ sessionMessageLoadStatusBySession: nextSessionMessageLoadStatusBySession,
453
+ sessionMessageLoadErrorBySession: nextSessionMessageLoadErrorBySession,
454
+ historyMessagePaginationBySession: nextHistoryMessagePaginationBySession
455
+ });
456
+ },
337
457
  // ---- Message operations ------------------------------------------------
338
458
  appendMessage: (sessionId, message) => {
339
459
  const state = get();
@@ -360,6 +480,92 @@ var createChatStore = (initialState) => (0, import_vanilla.createStore)((set, ge
360
480
  isStreamingBySession: nextIsStreamingBySession
361
481
  });
362
482
  },
483
+ hydrateHistorySessionMessages: (sessionId, messages) => {
484
+ const state = get();
485
+ set({
486
+ messagesBySession: {
487
+ ...state.messagesBySession,
488
+ [sessionId]: normalizeHistoryMessages(sessionId, messages)
489
+ },
490
+ sessionMessageLoadStatusBySession: {
491
+ ...state.sessionMessageLoadStatusBySession,
492
+ [sessionId]: "loaded"
493
+ },
494
+ sessionMessageLoadErrorBySession: {
495
+ ...state.sessionMessageLoadErrorBySession,
496
+ [sessionId]: null
497
+ },
498
+ historyMessagePaginationBySession: {
499
+ ...state.historyMessagePaginationBySession,
500
+ [sessionId]: createHistoryMessagePaginationState()
501
+ }
502
+ });
503
+ },
504
+ hydrateHistorySessionMessagesPage: (sessionId, page) => {
505
+ const state = get();
506
+ set({
507
+ messagesBySession: {
508
+ ...state.messagesBySession,
509
+ [sessionId]: normalizeHistoryMessages(sessionId, page.messages)
510
+ },
511
+ sessionMessageLoadStatusBySession: {
512
+ ...state.sessionMessageLoadStatusBySession,
513
+ [sessionId]: "loaded"
514
+ },
515
+ sessionMessageLoadErrorBySession: {
516
+ ...state.sessionMessageLoadErrorBySession,
517
+ [sessionId]: null
518
+ },
519
+ historyMessagePaginationBySession: {
520
+ ...state.historyMessagePaginationBySession,
521
+ [sessionId]: createHistoryMessagePaginationState(page)
522
+ }
523
+ });
524
+ },
525
+ prependHistorySessionMessagesPage: (sessionId, page) => {
526
+ const state = get();
527
+ set({
528
+ messagesBySession: {
529
+ ...state.messagesBySession,
530
+ [sessionId]: mergeOlderHistoryMessages(
531
+ sessionId,
532
+ page.messages,
533
+ state.messagesBySession[sessionId] ?? []
534
+ )
535
+ },
536
+ historyMessagePaginationBySession: {
537
+ ...state.historyMessagePaginationBySession,
538
+ [sessionId]: createHistoryMessagePaginationState(page)
539
+ }
540
+ });
541
+ },
542
+ setHistorySessionPreviousMessagesLoadStatus: (sessionId, isLoading, error2 = null) => {
543
+ const state = get();
544
+ const current = state.historyMessagePaginationBySession[sessionId] ?? createHistoryMessagePaginationState();
545
+ set({
546
+ historyMessagePaginationBySession: {
547
+ ...state.historyMessagePaginationBySession,
548
+ [sessionId]: {
549
+ ...current,
550
+ isLoadingPrevious: isLoading,
551
+ error: error2
552
+ }
553
+ }
554
+ });
555
+ },
556
+ setHistorySessionMessageLoadStatus: (sessionId, status, error2 = null) => {
557
+ const state = get();
558
+ set({
559
+ sessionMessageLoadStatusBySession: {
560
+ ...state.sessionMessageLoadStatusBySession,
561
+ [sessionId]: status
562
+ },
563
+ sessionMessageLoadErrorBySession: {
564
+ ...state.sessionMessageLoadErrorBySession,
565
+ [sessionId]: error2
566
+ }
567
+ });
568
+ },
363
569
  startStreamingMessage: (sessionId, message) => {
364
570
  const state = get();
365
571
  set({
@@ -807,6 +1013,10 @@ var AiChatProvider = (props) => {
807
1013
  handleQuestionnaireSubmit,
808
1014
  handleConfirmationSubmit,
809
1015
  messageRenderOrder,
1016
+ historySessionList,
1017
+ onLoadMoreSessions,
1018
+ onSelectHistorySession,
1019
+ onLoadMoreHistoryMessages,
810
1020
  enableImageAttachments = true,
811
1021
  children
812
1022
  } = props;
@@ -883,7 +1093,11 @@ var AiChatProvider = (props) => {
883
1093
  handleConfirmationSubmit,
884
1094
  messageRenderOrder,
885
1095
  transformStreamPacket: defaultTransformStreamPacket,
886
- enableImageAttachments
1096
+ enableImageAttachments,
1097
+ historySessionList,
1098
+ onLoadMoreSessions,
1099
+ onSelectHistorySession,
1100
+ onLoadMoreHistoryMessages
887
1101
  }),
888
1102
  [
889
1103
  axiosInstance,
@@ -893,8 +1107,12 @@ var AiChatProvider = (props) => {
893
1107
  enableImageAttachments,
894
1108
  handleConfirmationSubmit,
895
1109
  handleQuestionnaireSubmit,
1110
+ historySessionList,
896
1111
  labels,
897
1112
  messageRenderOrder,
1113
+ onLoadMoreSessions,
1114
+ onLoadMoreHistoryMessages,
1115
+ onSelectHistorySession,
898
1116
  renderMessageBlock,
899
1117
  sendRef,
900
1118
  retryRef,
@@ -3206,15 +3424,11 @@ var Bubble = import_styled7.default.article`
3206
3424
 
3207
3425
  &[data-role='user'] {
3208
3426
  width: auto;
3209
- max-width: min(760px, 100%);
3427
+ max-width: 100%;
3210
3428
  margin-left: auto;
3211
- padding: 14px 16px;
3212
- border-radius: 22px;
3213
- background: linear-gradient(180deg, rgba(59, 59, 63, 0.9) 0%, rgba(42, 43, 46, 0.92) 100%);
3214
- border: 1px solid rgba(255, 255, 255, 0.07);
3215
- box-shadow:
3216
- inset 0 1px 0 rgba(255, 255, 255, 0.03),
3217
- 0 12px 30px rgba(0, 0, 0, 0.18);
3429
+ padding: 8px 12px;
3430
+ background: #282825;
3431
+ border-radius: 16px;
3218
3432
  }
3219
3433
  `;
3220
3434
  var Header2 = import_styled7.default.div`
@@ -3546,6 +3760,7 @@ var HeroSubtitle = import_styled8.default.p`
3546
3760
  // src/components/chat-thread/index.tsx
3547
3761
  var import_jsx_runtime10 = require("@emotion/react/jsx-runtime");
3548
3762
  var CHAT_THREAD_PINNED_THRESHOLD_PX = 32;
3763
+ var CHAT_THREAD_LOAD_PREVIOUS_THRESHOLD_PX = 80;
3549
3764
  var isThreadPinnedToBottom = (container) => container.scrollHeight - container.clientHeight - container.scrollTop <= CHAT_THREAD_PINNED_THRESHOLD_PX;
3550
3765
  var renderChatMessage = ({
3551
3766
  message,
@@ -3619,9 +3834,13 @@ var ChatThreadView = ({
3619
3834
  historyMessages,
3620
3835
  streamingMessage,
3621
3836
  error: error2,
3837
+ isLoadingPreviousMessages = false,
3838
+ previousMessagesError,
3622
3839
  retryButtonLabel,
3623
3840
  scrollToLatestLabel,
3841
+ sessionHistoryLoadingLabel,
3624
3842
  onRetry,
3843
+ onLoadPreviousMessages,
3625
3844
  onConfirmationSubmit,
3626
3845
  onQuestionnaireSubmit,
3627
3846
  renderMessageBlock
@@ -3636,6 +3855,7 @@ var ChatThreadView = ({
3636
3855
  const latestHistoryMessage = historyMessages[historyMessages.length - 1];
3637
3856
  const latestTurnRef = (0, import_react11.useRef)(null);
3638
3857
  const reservedSpaceFrameRef = (0, import_react11.useRef)(null);
3858
+ const isLoadingPreviousRef = (0, import_react11.useRef)(false);
3639
3859
  const isPinnedRef = (0, import_react11.useRef)(true);
3640
3860
  const lastHistoryMessageIdRef = (0, import_react11.useRef)(latestHistoryMessage?.id);
3641
3861
  const lastStreamingMessageIdRef = (0, import_react11.useRef)(streamingMessage?.id);
@@ -3682,17 +3902,45 @@ var ChatThreadView = ({
3682
3902
  },
3683
3903
  [markThreadPinned, scrollToBottom]
3684
3904
  );
3905
+ const handleLoadPreviousMessages = (0, import_react11.useCallback)(async () => {
3906
+ const container = containerRef.current;
3907
+ if (!container || !onLoadPreviousMessages || isLoadingPreviousMessages) {
3908
+ return;
3909
+ }
3910
+ if (isLoadingPreviousRef.current) {
3911
+ return;
3912
+ }
3913
+ isLoadingPreviousRef.current = true;
3914
+ const previousScrollHeight = container.scrollHeight;
3915
+ const previousScrollTop = container.scrollTop;
3916
+ try {
3917
+ await onLoadPreviousMessages();
3918
+ } catch {
3919
+ return;
3920
+ } finally {
3921
+ isLoadingPreviousRef.current = false;
3922
+ }
3923
+ window.requestAnimationFrame(() => {
3924
+ const nextContainer = containerRef.current;
3925
+ if (!nextContainer)
3926
+ return;
3927
+ nextContainer.scrollTop = nextContainer.scrollHeight - previousScrollHeight + previousScrollTop;
3928
+ });
3929
+ }, [isLoadingPreviousMessages, onLoadPreviousMessages]);
3685
3930
  const handleContainerScroll = (0, import_react11.useCallback)(() => {
3686
3931
  const container = containerRef.current;
3687
3932
  if (!container)
3688
3933
  return;
3934
+ if (onLoadPreviousMessages && container.scrollTop <= CHAT_THREAD_LOAD_PREVIOUS_THRESHOLD_PX) {
3935
+ void handleLoadPreviousMessages();
3936
+ }
3689
3937
  const nextPinned = isThreadPinnedToBottom(container);
3690
3938
  isPinnedRef.current = nextPinned;
3691
3939
  setIsDetached(!nextPinned);
3692
3940
  if (nextPinned) {
3693
3941
  setPendingNewMessageCount(0);
3694
3942
  }
3695
- }, []);
3943
+ }, [handleLoadPreviousMessages, onLoadPreviousMessages]);
3696
3944
  (0, import_react11.useLayoutEffect)(() => {
3697
3945
  const nextHistoryMessageId = latestHistoryMessage?.id;
3698
3946
  if (lastHistoryMessageIdRef.current === nextHistoryMessageId) {
@@ -3820,6 +4068,8 @@ var ChatThreadView = ({
3820
4068
  }, [latestTurn, scrollToBottom]);
3821
4069
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(ThreadViewport, { children: [
3822
4070
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Container, { ref: containerRef, "data-testid": "chat-thread", onScroll: handleContainerScroll, children: [
4071
+ isLoadingPreviousMessages ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PreviousMessagesStateRow, { "data-testid": "chat-thread-loading-previous", children: sessionHistoryLoadingLabel }) : null,
4072
+ previousMessagesError ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PreviousMessagesStateRow, { "data-testid": "chat-thread-load-previous-error", children: previousMessagesError }) : null,
3823
4073
  conversationTurns.map((turn, turnIndex) => {
3824
4074
  const isLatestTurn = turnIndex === conversationTurns.length - 1;
3825
4075
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
@@ -3876,14 +4126,25 @@ var EMPTY_MESSAGES = [];
3876
4126
  var ChatThread = () => {
3877
4127
  const activeSessionId = useChatStore((s) => s.activeSessionId);
3878
4128
  const hasSessions = useChatStore((s) => s.sessions.length > 0);
3879
- const activeSessionMode = useChatStore(
3880
- (s) => s.sessions.find((x) => x.sessionId === s.activeSessionId)?.mode ?? DEFAULT_CHAT_AGENT_MODE
4129
+ const activeSession = useChatStore(
4130
+ (s) => s.sessions.find((session) => session.sessionId === s.activeSessionId)
3881
4131
  );
4132
+ const activeSessionMode = activeSession?.mode ?? DEFAULT_CHAT_AGENT_MODE;
3882
4133
  const messages = useChatStore(
3883
4134
  (s) => s.messagesBySession[s.activeSessionId ?? ""] ?? EMPTY_MESSAGES
3884
4135
  );
4136
+ const sessionMessageLoadStatus = useChatStore(
4137
+ (s) => s.sessionMessageLoadStatusBySession[s.activeSessionId ?? ""]
4138
+ );
3885
4139
  const streamingMessage = useChatStore((s) => s.streamingMessageBySession[s.activeSessionId ?? ""]);
3886
4140
  const error2 = useChatStore((s) => s.errorBySession[s.activeSessionId ?? ""]);
4141
+ const historyMessagePagination = useChatStore(
4142
+ (s) => s.historyMessagePaginationBySession[s.activeSessionId ?? ""]
4143
+ );
4144
+ const prependHistorySessionMessagesPage = useChatStore((s) => s.prependHistorySessionMessagesPage);
4145
+ const setHistorySessionPreviousMessagesLoadStatus = useChatStore(
4146
+ (s) => s.setHistorySessionPreviousMessagesLoadStatus
4147
+ );
3887
4148
  const updateQA = useChatStore((s) => s.updateQuestionnaireAnswers);
3888
4149
  const clearSessionError = useChatStore((s) => s.clearSessionError);
3889
4150
  const {
@@ -3892,6 +4153,7 @@ var ChatThread = () => {
3892
4153
  renderMessageBlock,
3893
4154
  handleQuestionnaireSubmit: customQuestionnaireSubmit,
3894
4155
  handleConfirmationSubmit: customConfirmationSubmit,
4156
+ onLoadMoreHistoryMessages,
3895
4157
  labels
3896
4158
  } = useChatContext();
3897
4159
  const handleRetry = (0, import_react11.useCallback)(() => {
@@ -3954,6 +4216,42 @@ var ChatThread = () => {
3954
4216
  },
3955
4217
  [activeSessionId, activeSessionMode, sendRef, customConfirmationSubmit]
3956
4218
  );
4219
+ const handleLoadPreviousMessages = (0, import_react11.useCallback)(async () => {
4220
+ if (!activeSession || !onLoadMoreHistoryMessages || !historyMessagePagination?.hasMorePrevious || !historyMessagePagination.previousCursor || historyMessagePagination.isLoadingPrevious) {
4221
+ return;
4222
+ }
4223
+ setHistorySessionPreviousMessagesLoadStatus(activeSession.sessionId, true);
4224
+ try {
4225
+ const page = await onLoadMoreHistoryMessages({
4226
+ session: activeSession,
4227
+ cursor: historyMessagePagination.previousCursor
4228
+ });
4229
+ if (page) {
4230
+ prependHistorySessionMessagesPage(activeSession.sessionId, page);
4231
+ return;
4232
+ }
4233
+ setHistorySessionPreviousMessagesLoadStatus(activeSession.sessionId, false);
4234
+ } catch (error3) {
4235
+ setHistorySessionPreviousMessagesLoadStatus(
4236
+ activeSession.sessionId,
4237
+ false,
4238
+ error3 instanceof Error ? error3.message : String(error3)
4239
+ );
4240
+ throw error3;
4241
+ }
4242
+ }, [
4243
+ activeSession,
4244
+ historyMessagePagination,
4245
+ onLoadMoreHistoryMessages,
4246
+ prependHistorySessionMessagesPage,
4247
+ setHistorySessionPreviousMessagesLoadStatus
4248
+ ]);
4249
+ const canLoadPreviousMessages = Boolean(
4250
+ activeSession && onLoadMoreHistoryMessages && historyMessagePagination?.hasMorePrevious && historyMessagePagination.previousCursor && !historyMessagePagination.isLoadingPrevious
4251
+ );
4252
+ if (hasSessions && sessionMessageLoadStatus === "loading" && messages.length === 0 && !streamingMessage) {
4253
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ThreadStateViewport, { "data-testid": "chat-thread-loading-state", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ThreadStateText, { children: labels.sessionHistoryLoading }) });
4254
+ }
3957
4255
  if (!hasSessions || messages.length === 0 && !streamingMessage) {
3958
4256
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatThreadEmptyState, {});
3959
4257
  }
@@ -3964,9 +4262,13 @@ var ChatThread = () => {
3964
4262
  historyMessages: messages,
3965
4263
  streamingMessage,
3966
4264
  error: error2,
4265
+ isLoadingPreviousMessages: historyMessagePagination?.isLoadingPrevious,
4266
+ previousMessagesError: historyMessagePagination?.error,
3967
4267
  retryButtonLabel: labels.retryButton,
3968
4268
  scrollToLatestLabel: labels.scrollToLatest,
4269
+ sessionHistoryLoadingLabel: labels.sessionHistoryLoading,
3969
4270
  onRetry: handleRetry,
4271
+ onLoadPreviousMessages: canLoadPreviousMessages ? handleLoadPreviousMessages : void 0,
3970
4272
  onConfirmationSubmit: handleConfirmation,
3971
4273
  onQuestionnaireSubmit: handleQuestionnaireSubmit,
3972
4274
  renderMessageBlock
@@ -3980,6 +4282,18 @@ var ThreadViewport = import_styled9.default.div`
3980
4282
  flex: 1;
3981
4283
  min-height: 0;
3982
4284
  `;
4285
+ var ThreadStateViewport = import_styled9.default.div`
4286
+ display: flex;
4287
+ flex: 1;
4288
+ min-height: 0;
4289
+ align-items: center;
4290
+ justify-content: center;
4291
+ padding: 24px;
4292
+ `;
4293
+ var ThreadStateText = import_styled9.default.div`
4294
+ color: var(--text-secondary, rgba(255, 255, 255, 0.64));
4295
+ font-size: 14px;
4296
+ `;
3983
4297
  var Container = import_styled9.default.div`
3984
4298
  display: flex;
3985
4299
  flex: 1;
@@ -3987,7 +4301,7 @@ var Container = import_styled9.default.div`
3987
4301
  gap: 18px;
3988
4302
  min-height: 0;
3989
4303
  overflow: auto;
3990
- padding: 24px 24px 88px;
4304
+ padding: 24px 16px 88px;
3991
4305
  overscroll-behavior: contain;
3992
4306
 
3993
4307
  &::-webkit-scrollbar {
@@ -4006,10 +4320,23 @@ var Container = import_styled9.default.div`
4006
4320
  var MessageSlot = import_styled9.default.div`
4007
4321
  display: flex;
4008
4322
  `;
4323
+ var PreviousMessagesStateRow = import_styled9.default.div`
4324
+ width: 100%;
4325
+ max-width: var(--chat-content-max-width, 48rem);
4326
+ margin-right: auto;
4327
+ margin-left: auto;
4328
+ color: var(--text-secondary, rgba(255, 255, 255, 0.64));
4329
+ font-size: 13px;
4330
+ text-align: center;
4331
+ `;
4009
4332
  var ConversationTurn = import_styled9.default.div`
4010
4333
  display: flex;
4011
4334
  flex-direction: column;
4012
4335
  gap: 18px;
4336
+ width: 100%;
4337
+ max-width: var(--chat-content-max-width, 48rem);
4338
+ margin-right: auto;
4339
+ margin-left: auto;
4013
4340
  `;
4014
4341
  var ErrorText = import_styled9.default.div`
4015
4342
  color: #ff7b72;
@@ -7797,7 +8124,7 @@ var PrimaryButton = (0, import_styled13.default)(import_compass_ui3.Button)`
7797
8124
  min-width: 24px;
7798
8125
  width: 24px;
7799
8126
  height: 24px;
7800
- background: ${({ $canSend }) => $canSend ? "#fcfbf8" : "rgba(255, 255, 255, 0.3)"};
8127
+ background: ${({ $canSend }) => $canSend ? "#fcfbf8" : "rgba(252,251,248,0.3);"};
7801
8128
  color: ${({ $canSend }) => $canSend ? "#5b5448" : "rgba(255, 255, 255, 0.72)"};
7802
8129
  border-radius: 12px;
7803
8130
  border: 1px solid ${({ $canSend }) => $canSend ? "rgba(198, 188, 170, 0.38)" : "transparent"};
@@ -7808,7 +8135,7 @@ var PrimaryButton = (0, import_styled13.default)(import_compass_ui3.Button)`
7808
8135
  }
7809
8136
 
7810
8137
  &:hover:not(:disabled) {
7811
- background: ${({ $canSend }) => $canSend ? "#f7f4ec" : "rgba(255, 255, 255, 0.3)"};
8138
+ background: ${({ $canSend }) => $canSend ? "#f7f4ec" : "rgba(252,251,248,0.3);"};
7812
8139
  color: ${({ $canSend }) => $canSend ? "#4f493f" : "rgba(255, 255, 255, 0.72)"};
7813
8140
  border-color: ${({ $canSend }) => $canSend ? "rgba(198, 188, 170, 0.46)" : "transparent"};
7814
8141
  }
@@ -7876,7 +8203,7 @@ var StopSpinner = import_styled13.default.span`
7876
8203
  var import_jsx_runtime16 = require("@emotion/react/jsx-runtime");
7877
8204
  var CHAT_COMPOSER_LINE_HEIGHT_PX = 20;
7878
8205
  var CHAT_COMPOSER_MAX_ROWS = 7;
7879
- var CHAT_COMPOSER_PADDING_TOP_PX = 8;
8206
+ var CHAT_COMPOSER_PADDING_TOP_PX = 12;
7880
8207
  var CHAT_COMPOSER_PADDING_BOTTOM_PX = 12;
7881
8208
  var CHAT_COMPOSER_PADDING_BLOCK_PX = CHAT_COMPOSER_PADDING_TOP_PX + CHAT_COMPOSER_PADDING_BOTTOM_PX;
7882
8209
  var CHAT_COMPOSER_MIN_ROWS = 4;
@@ -8111,6 +8438,10 @@ var ChatComposerView = ({
8111
8438
  setActiveSkillNavigation({ queryKey: "", index: 0 });
8112
8439
  };
8113
8440
  const handleKeyDown = (event) => {
8441
+ const isImeComposing = event.nativeEvent.isComposing || event.keyCode === 229;
8442
+ if (event.key === "Enter" && isImeComposing) {
8443
+ return;
8444
+ }
8114
8445
  if (skillQueryMatch) {
8115
8446
  if (event.key === "ArrowDown" && filteredSkills.length > 0) {
8116
8447
  event.preventDefault();
@@ -8388,7 +8719,7 @@ var Surface = import_styled14.default.div`
8388
8719
  'input'
8389
8720
  'footer';
8390
8721
  width: 100%;
8391
- max-width: 760px;
8722
+ max-width: var(--chat-content-max-width, 48rem);
8392
8723
  margin: 0 auto;
8393
8724
  background: var(--border-color);
8394
8725
  border-radius: 20px;
@@ -8649,6 +8980,7 @@ var SkillButton = import_styled14.default.button`
8649
8980
  `;
8650
8981
 
8651
8982
  // src/components/chat-conversation-list/index.tsx
8983
+ var import_react20 = require("react");
8652
8984
  var import_styled16 = __toESM(require("@emotion/styled"));
8653
8985
 
8654
8986
  // src/components/chat-conversation-list/components/chat-session-item.tsx
@@ -8662,6 +8994,7 @@ var ChatSessionItem = (0, import_react19.memo)(
8662
8994
  {
8663
8995
  type: "button",
8664
8996
  "data-active": isActive,
8997
+ "data-testid": `chat-session-item-${session.sessionId}`,
8665
8998
  onClick: () => onClick(session.sessionId),
8666
8999
  children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(SessionMeta, { children: [
8667
9000
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SessionTitle, { children: session.title }),
@@ -8709,14 +9042,130 @@ var ModeBadge = import_styled15.default.span`
8709
9042
  background: rgba(255, 255, 255, 0.04);
8710
9043
  `;
8711
9044
 
9045
+ // src/components/chat-conversation-list/lib/history-session-selection.ts
9046
+ var shouldLoadHistorySessionMessages = ({
9047
+ sessionId,
9048
+ messagesBySession,
9049
+ loadStatusBySession,
9050
+ isStreamingBySession
9051
+ }) => {
9052
+ if (isStreamingBySession[sessionId])
9053
+ return false;
9054
+ if (loadStatusBySession[sessionId] === "loading")
9055
+ return false;
9056
+ if (loadStatusBySession[sessionId] === "error")
9057
+ return true;
9058
+ if (loadStatusBySession[sessionId] === "loaded")
9059
+ return false;
9060
+ return messagesBySession[sessionId] === void 0;
9061
+ };
9062
+
8712
9063
  // src/components/chat-conversation-list/index.tsx
8713
9064
  var import_jsx_runtime18 = require("@emotion/react/jsx-runtime");
9065
+ var SCROLL_LOAD_MORE_THRESHOLD_PX = 80;
9066
+ var shouldLoadMoreSessions = ({
9067
+ scrollTop,
9068
+ clientHeight,
9069
+ scrollHeight,
9070
+ threshold = SCROLL_LOAD_MORE_THRESHOLD_PX
9071
+ }) => scrollHeight - scrollTop - clientHeight <= threshold;
9072
+ var isHistorySessionMessagesPage = (value) => typeof value === "object" && value !== null && Array.isArray(value.messages);
8714
9073
  var ChatConversationList = () => {
8715
- const { labels } = useChatContext();
8716
- const sessions = useChatStore((s) => s.sessions);
9074
+ const { labels, historySessionList, onLoadMoreSessions, onSelectHistorySession, store } = useChatContext();
9075
+ const localSessions = useChatStore((s) => s.sessions);
8717
9076
  const activeSessionId = useChatStore((s) => s.activeSessionId);
8718
9077
  const startNewChat = useChatStore((s) => s.startNewChat);
8719
9078
  const setActiveSession = useChatStore((s) => s.setActiveSession);
9079
+ const hydrateHistorySessions = useChatStore((s) => s.hydrateHistorySessions);
9080
+ const hydrateHistorySessionMessages = useChatStore((s) => s.hydrateHistorySessionMessages);
9081
+ const hydrateHistorySessionMessagesPage = useChatStore((s) => s.hydrateHistorySessionMessagesPage);
9082
+ const setHistorySessionMessageLoadStatus = useChatStore(
9083
+ (s) => s.setHistorySessionMessageLoadStatus
9084
+ );
9085
+ const isLoadingMoreRef = (0, import_react20.useRef)(false);
9086
+ const hasSeenLoadingMoreRef = (0, import_react20.useRef)(false);
9087
+ (0, import_react20.useEffect)(() => {
9088
+ if (!historySessionList)
9089
+ return;
9090
+ hydrateHistorySessions(historySessionList.sessions);
9091
+ }, [historySessionList, hydrateHistorySessions]);
9092
+ (0, import_react20.useEffect)(() => {
9093
+ if (historySessionList?.isLoading) {
9094
+ hasSeenLoadingMoreRef.current = true;
9095
+ return;
9096
+ }
9097
+ if (hasSeenLoadingMoreRef.current) {
9098
+ hasSeenLoadingMoreRef.current = false;
9099
+ isLoadingMoreRef.current = false;
9100
+ }
9101
+ }, [historySessionList?.isLoading]);
9102
+ (0, import_react20.useEffect)(() => {
9103
+ isLoadingMoreRef.current = false;
9104
+ hasSeenLoadingMoreRef.current = false;
9105
+ }, [historySessionList?.sessions.length, historySessionList?.hasMore]);
9106
+ const sessions = (0, import_react20.useMemo)(() => {
9107
+ if (!historySessionList) {
9108
+ return localSessions;
9109
+ }
9110
+ const localSessionIds = new Set(localSessions.map((session) => session.sessionId));
9111
+ return [
9112
+ ...localSessions,
9113
+ ...historySessionList.sessions.filter((session) => !localSessionIds.has(session.sessionId))
9114
+ ];
9115
+ }, [historySessionList, localSessions]);
9116
+ const handleSessionListScroll = (event) => {
9117
+ if (!historySessionList?.hasMore || historySessionList.isLoading || !onLoadMoreSessions || isLoadingMoreRef.current) {
9118
+ return;
9119
+ }
9120
+ const target = event.currentTarget;
9121
+ if (shouldLoadMoreSessions({
9122
+ scrollTop: target.scrollTop,
9123
+ clientHeight: target.clientHeight,
9124
+ scrollHeight: target.scrollHeight
9125
+ })) {
9126
+ isLoadingMoreRef.current = true;
9127
+ void Promise.resolve(onLoadMoreSessions()).catch(() => {
9128
+ isLoadingMoreRef.current = false;
9129
+ hasSeenLoadingMoreRef.current = false;
9130
+ });
9131
+ }
9132
+ };
9133
+ const handleSelectSession = async (sessionId) => {
9134
+ setActiveSession(sessionId);
9135
+ const session = sessions.find((item) => item.sessionId === sessionId);
9136
+ if (!session || !onSelectHistorySession) {
9137
+ return;
9138
+ }
9139
+ const state = store.getState();
9140
+ const shouldLoad = shouldLoadHistorySessionMessages({
9141
+ sessionId,
9142
+ messagesBySession: state.messagesBySession,
9143
+ loadStatusBySession: state.sessionMessageLoadStatusBySession,
9144
+ isStreamingBySession: state.isStreamingBySession
9145
+ });
9146
+ if (!shouldLoad) {
9147
+ return;
9148
+ }
9149
+ setHistorySessionMessageLoadStatus(sessionId, "loading");
9150
+ try {
9151
+ const result = await onSelectHistorySession(session);
9152
+ if (Array.isArray(result)) {
9153
+ hydrateHistorySessionMessages(sessionId, result);
9154
+ return;
9155
+ }
9156
+ if (isHistorySessionMessagesPage(result)) {
9157
+ hydrateHistorySessionMessagesPage(sessionId, result);
9158
+ return;
9159
+ }
9160
+ setHistorySessionMessageLoadStatus(sessionId, "loaded");
9161
+ } catch (error2) {
9162
+ setHistorySessionMessageLoadStatus(
9163
+ sessionId,
9164
+ "error",
9165
+ error2 instanceof Error ? error2.message : String(error2)
9166
+ );
9167
+ }
9168
+ };
8720
9169
  const modeLabels = {
8721
9170
  ask: labels.modeLabelAsk,
8722
9171
  plan: labels.modeLabelPlan,
@@ -8727,16 +9176,21 @@ var ChatConversationList = () => {
8727
9176
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Title3, { children: "Sessions" }),
8728
9177
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CreateButton, { type: "button", "data-testid": "chat-create-session", onClick: startNewChat, children: labels.newChat })
8729
9178
  ] }),
8730
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(List2, { "data-testid": "chat-session-list", children: sessions.map((session) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
8731
- ChatSessionItem,
8732
- {
8733
- session,
8734
- isActive: activeSessionId === session.sessionId,
8735
- modeLabel: modeLabels[session.mode ?? DEFAULT_CHAT_AGENT_MODE] ?? "",
8736
- onClick: setActiveSession
8737
- },
8738
- session.sessionId
8739
- )) })
9179
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(List2, { "data-testid": "chat-session-list", onScroll: handleSessionListScroll, children: [
9180
+ sessions.map((session) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
9181
+ ChatSessionItem,
9182
+ {
9183
+ session,
9184
+ isActive: activeSessionId === session.sessionId,
9185
+ modeLabel: modeLabels[session.mode ?? DEFAULT_CHAT_AGENT_MODE] ?? "",
9186
+ onClick: (sessionId) => void handleSelectSession(sessionId)
9187
+ },
9188
+ session.sessionId
9189
+ )),
9190
+ historySessionList?.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(StateRow, { "data-testid": "chat-session-history-loading", children: labels.sessionHistoryLoading }) : null,
9191
+ historySessionList?.error ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(StateRow, { "data-testid": "chat-session-history-error", children: historySessionList.error || labels.sessionHistoryLoadFailed }) : null,
9192
+ historySessionList && !historySessionList.isLoading && !historySessionList.error && sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(StateRow, { "data-testid": "chat-session-history-empty", children: labels.sessionHistoryEmpty }) : null
9193
+ ] })
8740
9194
  ] });
8741
9195
  };
8742
9196
  var Container3 = import_styled16.default.aside`
@@ -8774,6 +9228,11 @@ var List2 = import_styled16.default.div`
8774
9228
  gap: 8px;
8775
9229
  overflow: auto;
8776
9230
  `;
9231
+ var StateRow = import_styled16.default.div`
9232
+ padding: 12px;
9233
+ font-size: 13px;
9234
+ color: var(--text-secondary);
9235
+ `;
8777
9236
 
8778
9237
  // src/components/ai-chat/index.tsx
8779
9238
  var import_jsx_runtime19 = require("@emotion/react/jsx-runtime");
@@ -8835,7 +9294,7 @@ var AiChatWorkspaceContent = ({
8835
9294
  })
8836
9295
  );
8837
9296
  const shouldShowComposerOnly = showComposerOnlyBeforeFirstMessage && !showConversationList && !isConversationStarted;
8838
- (0, import_react20.useEffect)(() => {
9297
+ (0, import_react21.useEffect)(() => {
8839
9298
  onConversationStartedChange?.(isConversationStarted);
8840
9299
  }, [isConversationStarted, onConversationStartedChange]);
8841
9300
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(Root, { "data-testid": "ai-chat", children: [
@@ -8974,12 +9433,25 @@ var Root = import_styled17.default.div`
8974
9433
  overflow: hidden;
8975
9434
  `;
8976
9435
  var Workspace = import_styled17.default.section`
9436
+ --chat-layout-rem: 16px;
9437
+ --chat-content-margin: calc(var(--chat-layout-rem) * 1);
9438
+ --chat-content-max-width: calc(var(--chat-layout-rem) * 40);
9439
+
8977
9440
  flex: 1;
8978
9441
  display: flex;
8979
9442
  flex-direction: column;
8980
9443
  gap: 12px;
8981
9444
  min-height: 0;
8982
9445
  overflow: hidden;
9446
+
9447
+ @media (min-width: 640px) {
9448
+ --chat-content-margin: calc(var(--chat-layout-rem) * 1.5);
9449
+ }
9450
+
9451
+ @media (min-width: 1024px) {
9452
+ --chat-content-margin: calc(var(--chat-layout-rem) * 4);
9453
+ --chat-content-max-width: calc(var(--chat-layout-rem) * 48);
9454
+ }
8983
9455
  `;
8984
9456
  var QuickActionsRow = import_styled17.default.div`
8985
9457
  display: flex;