@portablecore/chat 0.2.15 → 0.3.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.
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 +96 -9
  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 +16 -2
  17. package/dist/components/message-list.d.ts.map +1 -1
  18. package/dist/components/message-list.js +44 -6
  19. package/dist/components/message-list.js.map +1 -1
  20. package/dist/hooks/use-chat-scroll.d.ts +10 -1
  21. package/dist/hooks/use-chat-scroll.d.ts.map +1 -1
  22. package/dist/hooks/use-chat-scroll.js +99 -32
  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 +34 -0
  33. package/dist/hooks/use-virtual-messages.d.ts.map +1 -0
  34. package/dist/hooks/use-virtual-messages.js +149 -0
  35. package/dist/hooks/use-virtual-messages.js.map +1 -0
  36. package/dist/index.d.ts +8 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +17 -6
  39. package/dist/index.js.map +1 -1
  40. package/package.json +20 -19
@@ -1,15 +1,24 @@
1
1
  import type { LiteMessage } from "@portablecore/types/chat";
2
+ import type { Virtualizer } from "@tanstack/react-virtual";
2
3
  interface UseChatScrollOptions {
3
4
  messages: LiteMessage[];
4
5
  streaming: boolean;
5
6
  /** When provided, drives scroll-on-thread-change: top for a thread, bottom for all */
6
7
  selectedThreadId?: string | null;
8
+ /** Mutable ref to the virtualizer; read at scroll time to avoid hook ordering issues */
9
+ virtualizerRef?: React.RefObject<Virtualizer<HTMLDivElement, Element> | null>;
7
10
  }
8
11
  interface UseChatScrollReturn {
9
12
  scrollRef: React.RefObject<HTMLDivElement>;
10
13
  scrollToBottom: (behavior?: ScrollBehavior) => void;
11
14
  isNearBottom: () => boolean;
15
+ /** True when user is actively scrolling (600ms debounce for mobile momentum) */
16
+ isUserScrolling: React.MutableRefObject<boolean>;
17
+ /** True when user scrolled away during streaming; resets when streaming ends */
18
+ userTookOverDuringStreaming: React.MutableRefObject<boolean>;
19
+ /** Timestamp of the last programmatic scroll (for guard checks) */
20
+ lastProgrammaticScroll: React.MutableRefObject<number>;
12
21
  }
13
- export declare function useChatScroll({ messages, streaming, selectedThreadId, }: UseChatScrollOptions): UseChatScrollReturn;
22
+ export declare function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef, }: UseChatScrollOptions): UseChatScrollReturn;
14
23
  export {};
15
24
  //# sourceMappingURL=use-chat-scroll.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat-scroll.d.ts","sourceRoot":"","sources":["../../src/hooks/use-chat-scroll.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE3D,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,SAAS,EAAE,OAAO,CAAA;IAClB,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED,UAAU,mBAAmB;IAC3B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IAC1C,cAAc,EAAE,CAAC,QAAQ,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACnD,YAAY,EAAE,MAAM,OAAO,CAAA;CAC5B;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,SAAS,EACT,gBAAgB,GACjB,EAAE,oBAAoB,GAAG,mBAAmB,CAkF5C"}
1
+ {"version":3,"file":"use-chat-scroll.d.ts","sourceRoot":"","sources":["../../src/hooks/use-chat-scroll.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE1D,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,SAAS,EAAE,OAAO,CAAA;IAClB,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,wFAAwF;IACxF,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;CAC9E;AAED,UAAU,mBAAmB;IAC3B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IAC1C,cAAc,EAAE,CAAC,QAAQ,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACnD,YAAY,EAAE,MAAM,OAAO,CAAA;IAC3B,gFAAgF;IAChF,eAAe,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAChD,gFAAgF;IAChF,2BAA2B,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC5D,mEAAmE;IACnE,sBAAsB,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAA;CACvD;AAMD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,cAAc,GACf,EAAE,oBAAoB,GAAG,mBAAmB,CAwJ5C"}
@@ -3,64 +3,123 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.useChatScroll = useChatScroll;
5
5
  /**
6
- * useChatScroll — Simple, reliable scroll management for chat.
6
+ * useChatScroll — Reliable scroll management for chat.
7
7
  *
8
8
  * Auto-scrolls to bottom when:
9
9
  * - Component mounts (instant)
10
10
  * - A new message arrives (smooth, if user was near bottom)
11
- * - Streaming content grows (smooth, if user was near bottom)
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.
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
14
19
  *
15
- * The spacer system from legacy ChatInterface is intentionally NOT
16
- * included here. It depends on the virtualizer (item 9) and will
17
- * be ported together with virtualization.
20
+ * Supports an optional virtualizer ref for large message lists.
21
+ * The ref is read at call time (not a dependency) to avoid circular hooks.
18
22
  */
19
23
  const react_1 = require("react");
20
- function useChatScroll({ messages, streaming, selectedThreadId, }) {
24
+ const NEAR_BOTTOM_THRESHOLD = 100;
25
+ const PROGRAMMATIC_SCROLL_GUARD_MS = 200;
26
+ const USER_SCROLL_DEBOUNCE_MS = 600;
27
+ function useChatScroll({ messages, streaming, selectedThreadId, virtualizerRef, }) {
21
28
  const scrollRef = (0, react_1.useRef)(null);
22
29
  const wasNearBottom = (0, react_1.useRef)(true);
23
30
  const prevThreadIdRef = (0, react_1.useRef)(undefined);
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);
24
36
  const isNearBottom = (0, react_1.useCallback)(() => {
25
37
  const el = scrollRef.current;
26
38
  if (!el)
27
39
  return true;
28
- const threshold = 100;
29
- return el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
40
+ return el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_THRESHOLD;
30
41
  }, []);
31
42
  const scrollToBottom = (0, react_1.useCallback)((behavior = "smooth") => {
32
- const el = scrollRef.current;
33
- if (!el)
34
- return;
35
- el.scrollTo({ top: el.scrollHeight, behavior });
36
- }, []);
37
- // Track whether user is near bottom
43
+ lastProgrammaticScroll.current = Date.now();
44
+ const v = virtualizerRef?.current;
45
+ if (v && messages.length > 0) {
46
+ v.scrollToIndex(messages.length - 1, {
47
+ align: "end",
48
+ behavior,
49
+ });
50
+ }
51
+ else {
52
+ const el = scrollRef.current;
53
+ if (!el)
54
+ return;
55
+ el.scrollTo({ top: el.scrollHeight, behavior });
56
+ }
57
+ },
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ [messages.length]);
60
+ // Unified scroll listener: tracks near-bottom state + user scroll guards
38
61
  (0, react_1.useEffect)(() => {
39
62
  const el = scrollRef.current;
40
63
  if (!el)
41
64
  return;
42
- const onScroll = () => {
65
+ const handleScroll = () => {
66
+ const elapsed = Date.now() - lastProgrammaticScroll.current;
67
+ if (elapsed < PROGRAMMATIC_SCROLL_GUARD_MS)
68
+ return;
43
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);
44
83
  };
45
- el.addEventListener("scroll", onScroll, { passive: true });
46
- return () => el.removeEventListener("scroll", onScroll);
47
- }, [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]);
48
93
  // Auto-scroll on new messages
49
94
  (0, react_1.useEffect)(() => {
50
- if (wasNearBottom.current) {
95
+ if (wasNearBottom.current && !isUserScrolling.current) {
51
96
  scrollToBottom("smooth");
52
97
  }
53
98
  }, [messages.length, scrollToBottom]);
54
- // Auto-scroll during streaming if near bottom
99
+ // RAF-gated auto-scroll during streaming
55
100
  (0, react_1.useEffect)(() => {
56
- if (!streaming)
101
+ if (!streaming) {
102
+ if (rafRef.current != null) {
103
+ cancelAnimationFrame(rafRef.current);
104
+ rafRef.current = null;
105
+ }
57
106
  return;
58
- const interval = setInterval(() => {
59
- if (wasNearBottom.current) {
107
+ }
108
+ const tick = () => {
109
+ if (wasNearBottom.current &&
110
+ !userTookOverDuringStreaming.current &&
111
+ !isUserScrolling.current) {
60
112
  scrollToBottom("smooth");
61
113
  }
62
- }, 200);
63
- return () => clearInterval(interval);
114
+ rafRef.current = requestAnimationFrame(tick);
115
+ };
116
+ rafRef.current = requestAnimationFrame(tick);
117
+ return () => {
118
+ if (rafRef.current != null) {
119
+ cancelAnimationFrame(rafRef.current);
120
+ rafRef.current = null;
121
+ }
122
+ };
64
123
  }, [streaming, scrollToBottom]);
65
124
  // Instant scroll on mount
66
125
  (0, react_1.useEffect)(() => {
@@ -79,17 +138,25 @@ function useChatScroll({ messages, streaming, selectedThreadId, }) {
79
138
  return;
80
139
  prevThreadIdRef.current = newId;
81
140
  requestAnimationFrame(() => {
82
- const el = scrollRef.current;
83
- if (!el)
84
- return;
85
141
  if (newId) {
142
+ const el = scrollRef.current;
143
+ if (!el)
144
+ return;
145
+ lastProgrammaticScroll.current = Date.now();
86
146
  el.scrollTo({ top: 0, behavior: "instant" });
87
147
  }
88
148
  else {
89
- el.scrollTo({ top: el.scrollHeight, behavior: "instant" });
149
+ scrollToBottom("instant");
90
150
  }
91
151
  });
92
- }, [selectedThreadId]);
93
- return { scrollRef, scrollToBottom, isNearBottom };
152
+ }, [selectedThreadId, scrollToBottom]);
153
+ return {
154
+ scrollRef,
155
+ scrollToBottom,
156
+ isNearBottom,
157
+ isUserScrolling,
158
+ userTookOverDuringStreaming,
159
+ lastProgrammaticScroll,
160
+ };
94
161
  }
95
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":"AAAA,YAAY,CAAA;;;AA2BZ,gDAOC;AAYD,8CAwFC;AApID,iCAAyE;AAqBzE;;;GAGG;AACH,SAAgB,kBAAkB,CAChC,UAAkB,EAClB,QAAiB;IAEjB,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,OAAO,CAAA;IACxC,IAAI,UAAU,GAAG,EAAE;QAAE,OAAO,OAAO,CAAA;IACnC,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,eAAe,GAAG,OAAO,EACzB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,GAAG,EAAE,GACE;IACzB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,CAAC,GAAG,IAAA,gBAAQ,EAAkB,eAAe,CAAC,CAAA;IAClF,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,IAAA,gBAAQ,EACxC,GAAG,EAAE,CAAC,IAAI,GAAG,EAAE,CAChB,CAAA;IACD,MAAM,kBAAkB,GAAG,IAAA,cAAM,EAAC,eAAe,CAAC,CAAA;IAElD,MAAM,WAAW,GAAG,IAAA,mBAAW,EAC7B,CAAC,IAAqB,EAAE,EAAE;QACxB,mBAAmB,CAAC,IAAI,CAAC,CAAA;QACzB,YAAY,CAAC,IAAI,GAAG,EAAE,CAAC,CAAA;QACvB,gBAAgB,EAAE,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC,EACD,CAAC,gBAAgB,CAAC,CACnB,CAAA;IAED,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,IAAI,QAAQ,KAAK,kBAAkB,CAAC,OAAO,EAAE,CAAC;YAC5C,gBAAgB,EAAE,CAAC,QAAQ,CAAC,CAAA;QAC9B,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAA,CAAC,kDAAkD;IAEzD,MAAM,YAAY,GAAG,IAAA,eAAO,EAAC,GAAG,EAAE;QAChC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAA;QACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACzC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;QAC7B,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEd,MAAM,eAAe,GAAG,IAAA,mBAAW,EACjC,CAAC,SAAiB,EAAW,EAAE;QAC7B,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACvC,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QACnC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;QACzB,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,GAAG,CAAC,YAAY;YAAE,OAAO,KAAK,CAAA;QAEvE,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACzC,IAAI,QAAQ,KAAK,UAAU;YAAE,OAAO,KAAK,CAAA;QACzC,IAAI,QAAQ,KAAK,WAAW;YAAE,OAAO,IAAI,CAAA;QAEzC,IAAI,QAAQ,KAAK,MAAM;YAAE,OAAO,KAAK,CAAA;QACrC,IAAI,QAAQ,KAAK,OAAO;YAAE,OAAO,IAAI,CAAA;QAErC,mCAAmC;QACnC,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YACzB,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAA;YACrC,MAAM,QAAQ,GAAG,aAAa,GAAG,GAAG,IAAI,kBAAkB,CAAA;YAC1D,IAAI,QAAQ;gBAAE,OAAO,KAAK,CAAA;YAE1B,mDAAmD;YACnD,KAAK,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAClC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;gBAC3B,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBACvC,MAAM,eAAe,GAAG,OAAO,CAAC,YAAY,IAAI,aAAa,CAAA;oBAC7D,OAAO,eAAe,KAAK,aAAa,CAAA;gBAC1C,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAA;IACd,CAAC,EACD,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,kBAAkB,CAAC,CACjF,CAAA;IAED,MAAM,cAAc,GAAG,IAAA,mBAAW,EAChC,CAAC,SAAiB,EAAE,oBAA6B,EAAE,EAAE;QACnD,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE;YACpB,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;YAC1B,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;YACpE,OAAO,IAAI,CAAA;QACb,CAAC,CAAC,CAAA;IACJ,CAAC,EACD,EAAE,CACH,CAAA;IAED,OAAO;QACL,QAAQ;QACR,WAAW;QACX,eAAe;QACf,cAAc;KACf,CAAA;AACH,CAAC"}
@@ -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"}
@@ -0,0 +1,34 @@
1
+ import { type Virtualizer } from "@tanstack/react-virtual";
2
+ import type { RenderableMessage } from "../types.js";
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
+ };
9
+ interface UseVirtualMessagesOptions {
10
+ renderableMessages: RenderableMessage[];
11
+ scrollRef: React.RefObject<HTMLDivElement>;
12
+ /** Increment to force re-measurement (focus mode toggle, thread switch) */
13
+ measureVersion?: number;
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;
19
+ }
20
+ export type VirtualState = {
21
+ active: false;
22
+ } | {
23
+ active: true;
24
+ virtualizer: Virtualizer<HTMLDivElement, Element>;
25
+ virtualItems: ReturnType<Virtualizer<HTMLDivElement, Element>["getVirtualItems"]>;
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;
31
+ };
32
+ export declare function useVirtualMessages({ renderableMessages, scrollRef, measureVersion, enabled, preMeasuredHeights, getMessageIsExpanded, }: UseVirtualMessagesOptions): VirtualState;
33
+ export {};
34
+ //# sourceMappingURL=use-virtual-messages.d.ts.map
@@ -0,0 +1 @@
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"}