@portablecore/chat 0.2.16 → 0.3.1

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 (40) hide show
  1. package/dist/chat-interface-core.d.ts +5 -0
  2. package/dist/chat-interface-core.d.ts.map +1 -1
  3. package/dist/chat-interface-core.js +83 -15
  4. package/dist/chat-interface-core.js.map +1 -1
  5. package/dist/components/density-control.d.ts +14 -0
  6. package/dist/components/density-control.d.ts.map +1 -0
  7. package/dist/components/density-control.js +18 -0
  8. package/dist/components/density-control.js.map +1 -0
  9. package/dist/components/focus-mode-control.d.ts +3 -10
  10. package/dist/components/focus-mode-control.d.ts.map +1 -1
  11. package/dist/components/focus-mode-control.js +5 -10
  12. package/dist/components/focus-mode-control.js.map +1 -1
  13. package/dist/components/message-bubble.d.ts.map +1 -1
  14. package/dist/components/message-bubble.js +2 -2
  15. package/dist/components/message-bubble.js.map +1 -1
  16. package/dist/components/message-list.d.ts +7 -1
  17. package/dist/components/message-list.d.ts.map +1 -1
  18. package/dist/components/message-list.js +25 -16
  19. package/dist/components/message-list.js.map +1 -1
  20. package/dist/hooks/use-chat-scroll.d.ts +6 -0
  21. package/dist/hooks/use-chat-scroll.d.ts.map +1 -1
  22. package/dist/hooks/use-chat-scroll.js +48 -14
  23. package/dist/hooks/use-chat-scroll.js.map +1 -1
  24. package/dist/hooks/use-content-density.d.ts +35 -0
  25. package/dist/hooks/use-content-density.d.ts.map +1 -0
  26. package/dist/hooks/use-content-density.js +96 -0
  27. package/dist/hooks/use-content-density.js.map +1 -0
  28. package/dist/hooks/use-focus-mode.d.ts +4 -26
  29. package/dist/hooks/use-focus-mode.d.ts.map +1 -1
  30. package/dist/hooks/use-focus-mode.js +5 -75
  31. package/dist/hooks/use-focus-mode.js.map +1 -1
  32. package/dist/hooks/use-virtual-messages.d.ts +14 -1
  33. package/dist/hooks/use-virtual-messages.d.ts.map +1 -1
  34. package/dist/hooks/use-virtual-messages.js +111 -27
  35. package/dist/hooks/use-virtual-messages.js.map +1 -1
  36. package/dist/index.d.ts +8 -5
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +13 -6
  39. package/dist/index.js.map +1 -1
  40. package/package.json +2 -2
@@ -10,22 +10,29 @@ exports.useChatScroll = useChatScroll;
10
10
  * - A new message arrives (smooth, if user was near bottom)
11
11
  * - Streaming content grows (RAF-gated, if user was near bottom)
12
12
  *
13
- * Respects user intent: if user scrolled up, don't pull them down.
14
- * Includes a programmatic scroll guard so auto-scrolls don't
15
- * accidentally trip the "user scrolled away" detection.
13
+ * Scroll guards (ported from legacy ChatInterface):
14
+ * - 600ms user scroll debounce for mobile momentum scrolling
15
+ * - 200ms programmatic scroll guard to prevent auto-scroll events
16
+ * from being misinterpreted as user scrolls
17
+ * - userTookOverDuringStreaming flag that persists until streaming ends,
18
+ * preventing the RAF loop from fighting user intent
16
19
  *
17
20
  * Supports an optional virtualizer ref for large message lists.
18
21
  * The ref is read at call time (not a dependency) to avoid circular hooks.
19
22
  */
20
23
  const react_1 = require("react");
21
24
  const NEAR_BOTTOM_THRESHOLD = 100;
22
- const PROGRAMMATIC_SCROLL_GUARD_MS = 150;
25
+ const PROGRAMMATIC_SCROLL_GUARD_MS = 200;
26
+ const USER_SCROLL_DEBOUNCE_MS = 600;
23
27
  function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef, }) {
24
28
  const scrollRef = (0, react_1.useRef)(null);
25
29
  const wasNearBottom = (0, react_1.useRef)(true);
26
30
  const prevThreadIdRef = (0, react_1.useRef)(undefined);
27
- const lastProgrammaticScroll = (0, react_1.useRef)(0);
28
31
  const rafRef = (0, react_1.useRef)(null);
32
+ const isUserScrolling = (0, react_1.useRef)(false);
33
+ const userScrollTimeout = (0, react_1.useRef)(null);
34
+ const lastProgrammaticScroll = (0, react_1.useRef)(0);
35
+ const userTookOverDuringStreaming = (0, react_1.useRef)(false);
29
36
  const isNearBottom = (0, react_1.useCallback)(() => {
30
37
  const el = scrollRef.current;
31
38
  if (!el)
@@ -48,26 +55,44 @@ function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef,
48
55
  el.scrollTo({ top: el.scrollHeight, behavior });
49
56
  }
50
57
  },
51
- // virtualizerRef is a stable ref, not a dependency
52
58
  // eslint-disable-next-line react-hooks/exhaustive-deps
53
59
  [messages.length]);
54
- // Track whether user is near bottom (with programmatic scroll guard)
60
+ // Unified scroll listener: tracks near-bottom state + user scroll guards
55
61
  (0, react_1.useEffect)(() => {
56
62
  const el = scrollRef.current;
57
63
  if (!el)
58
64
  return;
59
- const onScroll = () => {
65
+ const handleScroll = () => {
60
66
  const elapsed = Date.now() - lastProgrammaticScroll.current;
61
67
  if (elapsed < PROGRAMMATIC_SCROLL_GUARD_MS)
62
68
  return;
63
69
  wasNearBottom.current = isNearBottom();
70
+ isUserScrolling.current = true;
71
+ if (streaming) {
72
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_THRESHOLD;
73
+ if (!nearBottom) {
74
+ userTookOverDuringStreaming.current = true;
75
+ }
76
+ }
77
+ if (userScrollTimeout.current) {
78
+ clearTimeout(userScrollTimeout.current);
79
+ }
80
+ userScrollTimeout.current = setTimeout(() => {
81
+ isUserScrolling.current = false;
82
+ }, USER_SCROLL_DEBOUNCE_MS);
64
83
  };
65
- el.addEventListener("scroll", onScroll, { passive: true });
66
- return () => el.removeEventListener("scroll", onScroll);
67
- }, [isNearBottom]);
84
+ el.addEventListener("scroll", handleScroll, { passive: true });
85
+ return () => el.removeEventListener("scroll", handleScroll);
86
+ }, [isNearBottom, streaming]);
87
+ // Reset userTookOver when streaming ends
88
+ (0, react_1.useEffect)(() => {
89
+ if (!streaming) {
90
+ userTookOverDuringStreaming.current = false;
91
+ }
92
+ }, [streaming]);
68
93
  // Auto-scroll on new messages
69
94
  (0, react_1.useEffect)(() => {
70
- if (wasNearBottom.current) {
95
+ if (wasNearBottom.current && !isUserScrolling.current) {
71
96
  scrollToBottom("smooth");
72
97
  }
73
98
  }, [messages.length, scrollToBottom]);
@@ -81,7 +106,9 @@ function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef,
81
106
  return;
82
107
  }
83
108
  const tick = () => {
84
- if (wasNearBottom.current) {
109
+ if (wasNearBottom.current &&
110
+ !userTookOverDuringStreaming.current &&
111
+ !isUserScrolling.current) {
85
112
  scrollToBottom("smooth");
86
113
  }
87
114
  rafRef.current = requestAnimationFrame(tick);
@@ -123,6 +150,13 @@ function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef,
123
150
  }
124
151
  });
125
152
  }, [selectedThreadId, scrollToBottom]);
126
- return { scrollRef, scrollToBottom, isNearBottom };
153
+ return {
154
+ scrollRef,
155
+ scrollToBottom,
156
+ isNearBottom,
157
+ isUserScrolling,
158
+ userTookOverDuringStreaming,
159
+ lastProgrammaticScroll,
160
+ };
127
161
  }
128
162
  //# sourceMappingURL=use-chat-scroll.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat-scroll.js","sourceRoot":"","sources":["../../src/hooks/use-chat-scroll.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-chat-scroll.js","sourceRoot":"","sources":["../../src/hooks/use-chat-scroll.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { LiteMessage, MessageViewMode } from "@portablecore/types/chat";
2
+ interface UseContentDensityOptions {
3
+ messages: LiteMessage[];
4
+ initialViewMode?: MessageViewMode;
5
+ currentUserId: string;
6
+ onViewModeChange?: (mode: MessageViewMode) => void;
7
+ /** How many recent messages stay full in Smart mode (default 10) */
8
+ recentMessageCount?: number;
9
+ }
10
+ export interface ContentDensityState {
11
+ viewMode: MessageViewMode;
12
+ setViewMode: (mode: MessageViewMode) => void;
13
+ /** True when the message should render shortContent instead of content */
14
+ shouldShowShort: (messageId: string) => boolean;
15
+ /** Toggle a single message between expanded/collapsed */
16
+ toggleExpanded: (messageId: string, currentlyShowingFull: boolean) => void;
17
+ }
18
+ /**
19
+ * Auto-determine the default view mode based on chat characteristics.
20
+ * Group chats and long chats default to Smart; short chats default to Full.
21
+ */
22
+ export declare function getDefaultViewMode(chatLength: number, chatType?: string): MessageViewMode;
23
+ /**
24
+ * Manages Content Density state: view mode, per-message expansion overrides,
25
+ * and the core logic for deciding whether to show short or full content.
26
+ *
27
+ * Mode behaviors:
28
+ * - "full": all messages expanded
29
+ * - "brief": all messages with shortContent are compressed
30
+ * - "smart": recency-weighted auto-selection. Recent N messages stay full,
31
+ * older messages that the current user prompted stay full, all others brief.
32
+ */
33
+ export declare function useContentDensity({ messages, initialViewMode, currentUserId, onViewModeChange, recentMessageCount, }: UseContentDensityOptions): ContentDensityState;
34
+ export {};
35
+ //# sourceMappingURL=use-content-density.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-content-density.d.ts","sourceRoot":"","sources":["../../src/hooks/use-content-density.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAE5E,UAAU,wBAAwB;IAChC,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IAClD,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,eAAe,CAAA;IACzB,WAAW,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IAC5C,0EAA0E;IAC1E,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/C,yDAAyD;IACzD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,KAAK,IAAI,CAAA;CAC3E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,eAAe,CAIjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,eAAyB,EACzB,aAAa,EACb,gBAAgB,EAChB,kBAAuB,GACxB,EAAE,wBAAwB,GAAG,mBAAmB,CAkFhD"}
@@ -0,0 +1,96 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getDefaultViewMode = getDefaultViewMode;
5
+ exports.useContentDensity = useContentDensity;
6
+ const react_1 = require("react");
7
+ /**
8
+ * Auto-determine the default view mode based on chat characteristics.
9
+ * Group chats and long chats default to Smart; short chats default to Full.
10
+ */
11
+ function getDefaultViewMode(chatLength, chatType) {
12
+ if (chatType === "group")
13
+ return "smart";
14
+ if (chatLength > 50)
15
+ return "smart";
16
+ return "full";
17
+ }
18
+ /**
19
+ * Manages Content Density state: view mode, per-message expansion overrides,
20
+ * and the core logic for deciding whether to show short or full content.
21
+ *
22
+ * Mode behaviors:
23
+ * - "full": all messages expanded
24
+ * - "brief": all messages with shortContent are compressed
25
+ * - "smart": recency-weighted auto-selection. Recent N messages stay full,
26
+ * older messages that the current user prompted stay full, all others brief.
27
+ */
28
+ function useContentDensity({ messages, initialViewMode = "smart", currentUserId, onViewModeChange, recentMessageCount = 10, }) {
29
+ const [viewMode, setViewModeInternal] = (0, react_1.useState)(initialViewMode);
30
+ const [overrides, setOverrides] = (0, react_1.useState)(() => new Map());
31
+ const initialViewModeRef = (0, react_1.useRef)(initialViewMode);
32
+ const setViewMode = (0, react_1.useCallback)((mode) => {
33
+ setViewModeInternal(mode);
34
+ setOverrides(new Map());
35
+ onViewModeChange?.(mode);
36
+ }, [onViewModeChange]);
37
+ (0, react_1.useEffect)(() => {
38
+ if (viewMode !== initialViewModeRef.current) {
39
+ onViewModeChange?.(viewMode);
40
+ }
41
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
42
+ const messageIndex = (0, react_1.useMemo)(() => {
43
+ const map = new Map();
44
+ for (let i = 0; i < messages.length; i++) {
45
+ map.set(messages[i].id, i);
46
+ }
47
+ return map;
48
+ }, [messages]);
49
+ const shouldShowShort = (0, react_1.useCallback)((messageId) => {
50
+ const idx = messageIndex.get(messageId);
51
+ if (idx === undefined)
52
+ return false;
53
+ const msg = messages[idx];
54
+ if (!msg || msg.role !== "assistant" || !msg.shortContent)
55
+ return false;
56
+ const override = overrides.get(messageId);
57
+ if (override === "expanded")
58
+ return false;
59
+ if (override === "collapsed")
60
+ return true;
61
+ if (viewMode === "full")
62
+ return false;
63
+ if (viewMode === "brief")
64
+ return true;
65
+ // Smart mode: recency + authorship
66
+ if (viewMode === "smart") {
67
+ const totalMessages = messages.length;
68
+ const isRecent = totalMessages - idx <= recentMessageCount;
69
+ if (isRecent)
70
+ return false;
71
+ // Check if the current user prompted this response
72
+ for (let i = idx - 1; i >= 0; i--) {
73
+ const prevMsg = messages[i];
74
+ if (prevMsg && prevMsg.role === "user") {
75
+ const promptingUserId = prevMsg.senderUserId || currentUserId;
76
+ return promptingUserId !== currentUserId;
77
+ }
78
+ }
79
+ }
80
+ return false;
81
+ }, [messages, messageIndex, overrides, viewMode, currentUserId, recentMessageCount]);
82
+ const toggleExpanded = (0, react_1.useCallback)((messageId, currentlyShowingFull) => {
83
+ setOverrides((prev) => {
84
+ const next = new Map(prev);
85
+ next.set(messageId, currentlyShowingFull ? "collapsed" : "expanded");
86
+ return next;
87
+ });
88
+ }, []);
89
+ return {
90
+ viewMode,
91
+ setViewMode,
92
+ shouldShowShort,
93
+ toggleExpanded,
94
+ };
95
+ }
96
+ //# sourceMappingURL=use-content-density.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-content-density.js","sourceRoot":"","sources":["../../src/hooks/use-content-density.ts"],"names":[],"mappings":"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"}
@@ -1,29 +1,7 @@
1
- import type { LiteMessage, MessageViewMode } from "@portablecore/types/chat";
2
- interface UseFocusModeOptions {
3
- messages: LiteMessage[];
4
- enabled: boolean;
5
- initialViewMode?: MessageViewMode;
6
- currentUserId: string;
7
- onViewModeChange?: (mode: MessageViewMode) => void;
8
- }
9
- export interface FocusModeState {
10
- viewMode: MessageViewMode;
11
- setViewMode: (mode: MessageViewMode) => void;
12
- /** True when the message should render shortContent instead of content */
13
- shouldShowShort: (messageId: string) => boolean;
14
- /** Toggle a single message between expanded/collapsed */
15
- toggleExpanded: (messageId: string, currentlyShowingFull: boolean) => void;
16
- enabled: boolean;
17
- }
18
1
  /**
19
- * Manages focus mode state: view mode, per-message expansion overrides,
20
- * and the core logic for deciding whether to show short or full content.
21
- *
22
- * Mirrors the legacy ChatInterface focus mode exactly:
23
- * - "long": all messages expanded
24
- * - "short": all messages with shortContent are compressed
25
- * - "focus": messages you prompted are expanded, others' compressed
2
+ * @deprecated Use useContentDensity from ./use-content-density.ts instead.
3
+ * This file re-exports the new hook for backward compatibility.
26
4
  */
27
- export declare function useFocusMode({ messages, enabled, initialViewMode, currentUserId, onViewModeChange, }: UseFocusModeOptions): FocusModeState;
28
- export {};
5
+ export { useContentDensity as useFocusMode } from "./use-content-density.js";
6
+ export type { ContentDensityState as FocusModeState } from "./use-content-density.js";
29
7
  //# sourceMappingURL=use-focus-mode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-focus-mode.d.ts","sourceRoot":"","sources":["../../src/hooks/use-focus-mode.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAE5E,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;CACnD;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,CAAA;IACzB,WAAW,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IAC5C,0EAA0E;IAC1E,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/C,yDAAyD;IACzD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,KAAK,IAAI,CAAA;IAC1E,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,OAAO,EACP,eAAyB,EACzB,aAAa,EACb,gBAAgB,GACjB,EAAE,mBAAmB,GAAG,cAAc,CAkFtC"}
1
+ {"version":3,"file":"use-focus-mode.d.ts","sourceRoot":"","sources":["../../src/hooks/use-focus-mode.ts"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,IAAI,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5E,YAAY,EAAE,mBAAmB,IAAI,cAAc,EAAE,MAAM,0BAA0B,CAAA"}
@@ -1,81 +1,11 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.useFocusMode = useFocusMode;
5
- const react_1 = require("react");
4
+ exports.useFocusMode = void 0;
6
5
  /**
7
- * Manages focus mode state: view mode, per-message expansion overrides,
8
- * and the core logic for deciding whether to show short or full content.
9
- *
10
- * Mirrors the legacy ChatInterface focus mode exactly:
11
- * - "long": all messages expanded
12
- * - "short": all messages with shortContent are compressed
13
- * - "focus": messages you prompted are expanded, others' compressed
6
+ * @deprecated Use useContentDensity from ./use-content-density.ts instead.
7
+ * This file re-exports the new hook for backward compatibility.
14
8
  */
15
- function useFocusMode({ messages, enabled, initialViewMode = "focus", currentUserId, onViewModeChange, }) {
16
- const [viewMode, setViewModeInternal] = (0, react_1.useState)(enabled ? initialViewMode : "long");
17
- const [overrides, setOverrides] = (0, react_1.useState)(() => new Map());
18
- const initialViewModeRef = (0, react_1.useRef)(initialViewMode);
19
- const setViewMode = (0, react_1.useCallback)((mode) => {
20
- setViewModeInternal(mode);
21
- setOverrides(new Map());
22
- onViewModeChange?.(mode);
23
- }, [onViewModeChange]);
24
- (0, react_1.useEffect)(() => {
25
- if (viewMode !== initialViewModeRef.current) {
26
- onViewModeChange?.(viewMode);
27
- }
28
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
29
- const messageIndex = (0, react_1.useMemo)(() => {
30
- const map = new Map();
31
- for (let i = 0; i < messages.length; i++) {
32
- map.set(messages[i].id, i);
33
- }
34
- return map;
35
- }, [messages]);
36
- const shouldShowShort = (0, react_1.useCallback)((messageId) => {
37
- if (!enabled)
38
- return false;
39
- const idx = messageIndex.get(messageId);
40
- if (idx === undefined)
41
- return false;
42
- const msg = messages[idx];
43
- if (!msg || msg.role !== "assistant" || !msg.shortContent)
44
- return false;
45
- const override = overrides.get(messageId);
46
- if (override === "expanded")
47
- return false;
48
- if (override === "collapsed")
49
- return true;
50
- if (viewMode === "long")
51
- return false;
52
- if (viewMode === "short")
53
- return true;
54
- // Focus mode: expand if current user prompted this response
55
- if (viewMode === "focus") {
56
- for (let i = idx - 1; i >= 0; i--) {
57
- const prevMsg = messages[i];
58
- if (prevMsg && prevMsg.role === "user") {
59
- const promptingUserId = prevMsg.senderUserId || currentUserId;
60
- return promptingUserId !== currentUserId;
61
- }
62
- }
63
- }
64
- return false;
65
- }, [enabled, messages, messageIndex, overrides, viewMode, currentUserId]);
66
- const toggleExpanded = (0, react_1.useCallback)((messageId, currentlyShowingFull) => {
67
- setOverrides((prev) => {
68
- const next = new Map(prev);
69
- next.set(messageId, currentlyShowingFull ? "collapsed" : "expanded");
70
- return next;
71
- });
72
- }, []);
73
- return {
74
- viewMode,
75
- setViewMode,
76
- shouldShowShort,
77
- toggleExpanded,
78
- enabled,
79
- };
80
- }
9
+ var use_content_density_js_1 = require("./use-content-density.js");
10
+ Object.defineProperty(exports, "useFocusMode", { enumerable: true, get: function () { return use_content_density_js_1.useContentDensity; } });
81
11
  //# sourceMappingURL=use-focus-mode.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-focus-mode.js","sourceRoot":"","sources":["../../src/hooks/use-focus-mode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-focus-mode.js","sourceRoot":"","sources":["../../src/hooks/use-focus-mode.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;;;;AAEZ;;;GAGG;AAEH,mEAA4E;AAAnE,sHAAA,iBAAiB,OAAgB"}
@@ -1,12 +1,21 @@
1
1
  import { type Virtualizer } from "@tanstack/react-virtual";
2
2
  import type { RenderableMessage } from "../types.js";
3
3
  export declare const VIRTUALIZATION_THRESHOLD = 100;
4
+ export declare const PRE_MEASURE_THRESHOLD = 100;
5
+ export type MessageHeights = number | {
6
+ short: number;
7
+ long: number;
8
+ };
4
9
  interface UseVirtualMessagesOptions {
5
10
  renderableMessages: RenderableMessage[];
6
11
  scrollRef: React.RefObject<HTMLDivElement>;
7
12
  /** Increment to force re-measurement (focus mode toggle, thread switch) */
8
13
  measureVersion?: number;
9
14
  enabled?: boolean;
15
+ /** Pre-measured heights from hidden containers (optional) */
16
+ preMeasuredHeights?: Map<string, MessageHeights>;
17
+ /** Returns true if message is showing full content (for short/long estimation) */
18
+ getMessageIsExpanded?: (messageId: string, index: number) => boolean;
10
19
  }
11
20
  export type VirtualState = {
12
21
  active: false;
@@ -15,7 +24,11 @@ export type VirtualState = {
15
24
  virtualizer: Virtualizer<HTMLDivElement, Element>;
16
25
  virtualItems: ReturnType<Virtualizer<HTMLDivElement, Element>["getVirtualItems"]>;
17
26
  totalSize: number;
27
+ /** True once layout has stabilized after initial render */
28
+ isSettled: boolean;
29
+ /** True during mode transition (virtualizer count briefly set to 0) */
30
+ isResetting: boolean;
18
31
  };
19
- export declare function useVirtualMessages({ renderableMessages, scrollRef, measureVersion, enabled, }: UseVirtualMessagesOptions): VirtualState;
32
+ export declare function useVirtualMessages({ renderableMessages, scrollRef, measureVersion, enabled, preMeasuredHeights, getMessageIsExpanded, }: UseVirtualMessagesOptions): VirtualState;
20
33
  export {};
21
34
  //# sourceMappingURL=use-virtual-messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-virtual-messages.d.ts","sourceRoot":"","sources":["../../src/hooks/use-virtual-messages.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAE3C,UAAU,yBAAyB;IACjC,kBAAkB,EAAE,iBAAiB,EAAE,CAAA;IACvC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IAC1C,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IACE,MAAM,EAAE,IAAI,CAAA;IACZ,WAAW,EAAE,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,CAAA;IACjD,YAAY,EAAE,UAAU,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAA;IACjF,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAyBL,wBAAgB,kBAAkB,CAAC,EACjC,kBAAkB,EAClB,SAAS,EACT,cAAkB,EAClB,OAAc,GACf,EAAE,yBAAyB,GAAG,YAAY,CA8C1C"}
1
+ {"version":3,"file":"use-virtual-messages.d.ts","sourceRoot":"","sources":["../../src/hooks/use-virtual-messages.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAC3C,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,UAAU,yBAAyB;IACjC,kBAAkB,EAAE,iBAAiB,EAAE,CAAA;IACvC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IAC1C,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,6DAA6D;IAC7D,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAChD,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;CACrE;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IACE,MAAM,EAAE,IAAI,CAAA;IACZ,WAAW,EAAE,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,CAAA;IACjD,YAAY,EAAE,UAAU,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAA;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAA;IAClB,uEAAuE;IACvE,WAAW,EAAE,OAAO,CAAA;CACrB,CAAA;AAuCL,wBAAgB,kBAAkB,CAAC,EACjC,kBAAkB,EAClB,SAAS,EACT,cAAkB,EAClB,OAAc,EACd,kBAAkB,EAClB,oBAAoB,GACrB,EAAE,yBAAyB,GAAG,YAAY,CAkH1C"}
@@ -1,57 +1,139 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.VIRTUALIZATION_THRESHOLD = void 0;
4
+ exports.PRE_MEASURE_THRESHOLD = exports.VIRTUALIZATION_THRESHOLD = void 0;
5
5
  exports.useVirtualMessages = useVirtualMessages;
6
+ /**
7
+ * useVirtualMessages — Battle-tested virtualizer for large chat histories.
8
+ *
9
+ * Ported from the legacy ChatInterface's tanstack/react-virtual integration,
10
+ * which included workarounds for variable-height messages, mode transitions,
11
+ * and mobile scrolling. Gated behind config.features.jsVirtualization.
12
+ *
13
+ * Key patterns ported from legacy:
14
+ * - Overscan 100 (pre-render many items above/below viewport)
15
+ * - Content-length-based height estimation with short/long awareness
16
+ * - Mode version in getItemKey for cache invalidation on view mode changes
17
+ * - isResetting state (count-to-0) for mode transitions
18
+ * - Settlement polling (50ms, 4-stable, 40-max) with 2.5s fallback
19
+ * - Pre-measured heights map integration for accurate initial positioning
20
+ */
6
21
  const react_1 = require("react");
7
22
  const react_virtual_1 = require("@tanstack/react-virtual");
8
23
  exports.VIRTUALIZATION_THRESHOLD = 100;
9
- function estimateMessageHeight(rm) {
10
- const content = rm.message.content || "";
11
- const len = content.length;
12
- const charsPerLine = 80;
13
- const lineHeight = 24;
14
- let base;
15
- if (rm.message.role === "user") {
16
- base = 40;
17
- }
18
- else {
19
- base = 60;
24
+ exports.PRE_MEASURE_THRESHOLD = 100;
25
+ function estimateMessageHeight(rm, preMeasuredHeights, isExpanded) {
26
+ const msg = rm.message;
27
+ const PADDING = 24;
28
+ if (preMeasuredHeights) {
29
+ const cached = preMeasuredHeights.get(msg.id);
30
+ if (cached) {
31
+ if (typeof cached === "object") {
32
+ return isExpanded ? cached.long : cached.short;
33
+ }
34
+ return cached;
35
+ }
20
36
  }
21
- if (rm.showDateSeparator) {
37
+ let base = 0;
38
+ if (rm.showDateSeparator)
22
39
  base += 48;
40
+ if (msg.role === "user") {
41
+ const hasDocuments = msg.metadata?.hasDocuments;
42
+ return base + (hasDocuments ? 150 : 80) + PADDING;
23
43
  }
24
- const lines = Math.ceil(len / charsPerLine);
25
- const estimated = base + lines * lineHeight;
26
- return Math.max(60, Math.min(estimated, 800));
44
+ const relevantContent = !isExpanded && msg.shortContent ? msg.shortContent : msg.content;
45
+ const contentLength = relevantContent?.length || 0;
46
+ if (contentLength < 200)
47
+ return base + 100 + PADDING;
48
+ if (contentLength < 500)
49
+ return base + 150 + PADDING;
50
+ if (contentLength < 1000)
51
+ return base + 250 + PADDING;
52
+ if (contentLength < 2000)
53
+ return base + 400 + PADDING;
54
+ return base + 600 + PADDING;
27
55
  }
28
- function useVirtualMessages({ renderableMessages, scrollRef, measureVersion = 0, enabled = true, }) {
56
+ function useVirtualMessages({ renderableMessages, scrollRef, measureVersion = 0, enabled = true, preMeasuredHeights, getMessageIsExpanded, }) {
29
57
  const count = renderableMessages.length;
30
58
  const shouldVirtualize = enabled && count >= exports.VIRTUALIZATION_THRESHOLD;
31
59
  const messagesRef = (0, react_1.useRef)(renderableMessages);
32
60
  messagesRef.current = renderableMessages;
61
+ const preMeasuredRef = (0, react_1.useRef)(preMeasuredHeights);
62
+ preMeasuredRef.current = preMeasuredHeights;
63
+ const getExpandedRef = (0, react_1.useRef)(getMessageIsExpanded);
64
+ getExpandedRef.current = getMessageIsExpanded;
65
+ const [modeVersion, setModeVersion] = (0, react_1.useState)(0);
66
+ const [isResetting, setIsResetting] = (0, react_1.useState)(false);
67
+ const [isSettled, setIsSettled] = (0, react_1.useState)(!shouldVirtualize);
68
+ const prevMeasureVersion = (0, react_1.useRef)(measureVersion);
69
+ (0, react_1.useEffect)(() => {
70
+ if (prevMeasureVersion.current !== measureVersion && shouldVirtualize) {
71
+ prevMeasureVersion.current = measureVersion;
72
+ setIsResetting(true);
73
+ setIsSettled(false);
74
+ setModeVersion((v) => v + 1);
75
+ requestAnimationFrame(() => {
76
+ setIsResetting(false);
77
+ });
78
+ }
79
+ }, [measureVersion, shouldVirtualize]);
33
80
  const virtualizer = (0, react_virtual_1.useVirtualizer)({
34
- count: shouldVirtualize ? count : 0,
81
+ count: isResetting ? 0 : shouldVirtualize ? count : 0,
35
82
  getScrollElement: () => scrollRef.current,
36
83
  estimateSize: (0, react_1.useCallback)((index) => {
37
84
  const rm = messagesRef.current[index];
38
85
  if (!rm)
39
86
  return 100;
40
- return estimateMessageHeight(rm);
87
+ const isExpanded = getExpandedRef.current?.(rm.message.id, index) ?? true;
88
+ return estimateMessageHeight(rm, preMeasuredRef.current, isExpanded);
41
89
  }, []),
42
90
  getItemKey: (0, react_1.useCallback)((index) => {
43
91
  const rm = messagesRef.current[index];
44
- return rm?.message.id ?? index;
45
- }, []),
46
- overscan: 30,
92
+ return `${rm?.message.id ?? index}-v${modeVersion}`;
93
+ }, [modeVersion]),
94
+ overscan: 100,
47
95
  });
48
- const prevMeasureVersion = (0, react_1.useRef)(measureVersion);
96
+ // Settlement polling: watch for scrollHeight stability after initial render.
97
+ // 50ms polling, need 4 consecutive stable checks (~200ms), give up after 40 (~2s).
49
98
  (0, react_1.useEffect)(() => {
50
- if (prevMeasureVersion.current !== measureVersion && shouldVirtualize) {
51
- prevMeasureVersion.current = measureVersion;
52
- virtualizer.measure();
99
+ if (!shouldVirtualize || isResetting || isSettled)
100
+ return;
101
+ const container = scrollRef.current;
102
+ if (!container) {
103
+ setIsSettled(true);
104
+ return;
53
105
  }
54
- }, [measureVersion, shouldVirtualize, virtualizer]);
106
+ let lastScrollHeight = container.scrollHeight;
107
+ let stableCount = 0;
108
+ const STABLE_THRESHOLD = 4;
109
+ const MAX_CHECKS = 40;
110
+ let checkCount = 0;
111
+ let pollTimer;
112
+ const checkSettlement = () => {
113
+ checkCount++;
114
+ const currentScrollHeight = container.scrollHeight;
115
+ if (currentScrollHeight !== lastScrollHeight) {
116
+ lastScrollHeight = currentScrollHeight;
117
+ stableCount = 0;
118
+ }
119
+ else {
120
+ stableCount++;
121
+ }
122
+ if (stableCount >= STABLE_THRESHOLD || checkCount >= MAX_CHECKS) {
123
+ setIsSettled(true);
124
+ return;
125
+ }
126
+ pollTimer = setTimeout(checkSettlement, 50);
127
+ };
128
+ pollTimer = setTimeout(checkSettlement, 50);
129
+ const fallbackTimer = setTimeout(() => {
130
+ setIsSettled(true);
131
+ }, 2500);
132
+ return () => {
133
+ clearTimeout(pollTimer);
134
+ clearTimeout(fallbackTimer);
135
+ };
136
+ }, [shouldVirtualize, isResetting, isSettled, scrollRef]);
55
137
  if (!shouldVirtualize) {
56
138
  return { active: false };
57
139
  }
@@ -60,6 +142,8 @@ function useVirtualMessages({ renderableMessages, scrollRef, measureVersion = 0,
60
142
  virtualizer,
61
143
  virtualItems: virtualizer.getVirtualItems(),
62
144
  totalSize: virtualizer.getTotalSize(),
145
+ isSettled,
146
+ isResetting,
63
147
  };
64
148
  }
65
149
  //# sourceMappingURL=use-virtual-messages.js.map