@townco/ui 0.1.58 → 0.1.65

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.
@@ -15,6 +15,10 @@ export function useChatMessages(client, startSession) {
15
15
  const updateMessage = useChatStore((state) => state.updateMessage);
16
16
  const setError = useChatStore((state) => state.setError);
17
17
  const setLatestContextSize = useChatStore((state) => state.setLatestContextSize);
18
+ const addToolCall = useChatStore((state) => state.addToolCall);
19
+ const updateToolCall = useChatStore((state) => state.updateToolCall);
20
+ const addToolCallToCurrentMessage = useChatStore((state) => state.addToolCallToCurrentMessage);
21
+ const updateToolCallInCurrentMessage = useChatStore((state) => state.updateToolCallInCurrentMessage);
18
22
  /**
19
23
  * Send a message to the agent
20
24
  */
@@ -94,54 +98,74 @@ export function useChatMessages(client, startSession) {
94
98
  let accumulatedContent = "";
95
99
  let streamCompleted = false;
96
100
  for await (const chunk of messageStream) {
97
- // Update context size if provided (check both _meta.context_size and direct context_size)
98
- const chunkMeta = chunk._meta;
99
- const contextSizeData = chunkMeta?.context_size || chunk.context_size;
100
- if (contextSizeData != null) {
101
- const contextSize = contextSizeData;
102
- logger.info("✅ Received context_size from backend", {
103
- context_size: contextSize,
104
- totalEstimated: contextSize.totalEstimated,
105
- source: chunkMeta?.context_size ? "_meta" : "direct",
106
- });
107
- setLatestContextSize(contextSize);
108
- }
109
- else {
110
- logger.debug("Chunk does not have context_size", {
111
- hasContextSize: "context_size" in chunk,
112
- hasMeta: "_meta" in chunk,
113
- metaKeys: chunkMeta ? Object.keys(chunkMeta) : null,
114
- });
115
- }
116
- if (chunk.tokenUsage) {
117
- logger.debug("Received tokenUsage from backend", {
118
- tokenUsage: chunk.tokenUsage,
119
- });
120
- }
121
- if (chunk.isComplete) {
122
- // Update final message
123
- updateMessage(assistantMessageId, {
124
- content: accumulatedContent,
125
- isStreaming: false,
126
- streamingStartTime: undefined, // Clear streaming start time
127
- ...(chunk.tokenUsage ? { tokenUsage: chunk.tokenUsage } : {}),
128
- });
129
- setIsStreaming(false);
130
- setStreamingStartTime(null); // Clear global streaming start time
131
- streamCompleted = true;
132
- break;
133
- }
134
- else {
135
- // Update streaming content
136
- if (chunk.contentDelta.type === "text") {
137
- accumulatedContent += chunk.contentDelta.text;
101
+ // Handle different chunk types using discriminated union
102
+ if (chunk.type === "content") {
103
+ // Content chunk - text streaming
104
+ // Update context size if provided (check both _meta.context_size and direct context_size)
105
+ const chunkMeta = chunk._meta;
106
+ const contextSizeData = chunkMeta?.context_size || chunk.context_size;
107
+ if (contextSizeData != null) {
108
+ const contextSize = contextSizeData;
109
+ logger.info("✅ Received context_size from backend", {
110
+ context_size: contextSize,
111
+ totalEstimated: contextSize.totalEstimated,
112
+ source: chunkMeta?.context_size ? "_meta" : "direct",
113
+ });
114
+ setLatestContextSize(contextSize);
115
+ }
116
+ else {
117
+ logger.debug("Chunk does not have context_size", {
118
+ hasContextSize: "context_size" in chunk,
119
+ hasMeta: "_meta" in chunk,
120
+ metaKeys: chunkMeta ? Object.keys(chunkMeta) : null,
121
+ });
122
+ }
123
+ if (chunk.tokenUsage) {
124
+ logger.debug("Received tokenUsage from backend", {
125
+ tokenUsage: chunk.tokenUsage,
126
+ });
127
+ }
128
+ if (chunk.isComplete) {
129
+ // Update final message
138
130
  updateMessage(assistantMessageId, {
139
131
  content: accumulatedContent,
132
+ isStreaming: false,
133
+ streamingStartTime: undefined, // Clear streaming start time
140
134
  ...(chunk.tokenUsage ? { tokenUsage: chunk.tokenUsage } : {}),
141
135
  });
142
- // Small delay to allow Ink to render between chunks (~60fps)
143
- await new Promise((resolve) => setTimeout(resolve, 16));
136
+ setIsStreaming(false);
137
+ setStreamingStartTime(null); // Clear global streaming start time
138
+ streamCompleted = true;
139
+ break;
144
140
  }
141
+ else {
142
+ // Update streaming content
143
+ if (chunk.contentDelta.type === "text") {
144
+ accumulatedContent += chunk.contentDelta.text;
145
+ updateMessage(assistantMessageId, {
146
+ content: accumulatedContent,
147
+ ...(chunk.tokenUsage ? { tokenUsage: chunk.tokenUsage } : {}),
148
+ });
149
+ // Small delay to allow Ink to render between chunks (~60fps)
150
+ await new Promise((resolve) => setTimeout(resolve, 16));
151
+ }
152
+ }
153
+ }
154
+ else if (chunk.type === "tool_call") {
155
+ // Tool call chunk - tool invocation
156
+ logger.debug("Received tool_call chunk", { chunk });
157
+ // Add to session-level tool calls (for sidebar)
158
+ addToolCall(activeSessionId, chunk.toolCall);
159
+ // Also add to current assistant message (for inline display)
160
+ addToolCallToCurrentMessage(chunk.toolCall);
161
+ }
162
+ else if (chunk.type === "tool_call_update") {
163
+ // Tool call update chunk - tool results
164
+ logger.debug("Received tool_call_update chunk", { chunk });
165
+ // Update session-level tool calls (for sidebar)
166
+ updateToolCall(activeSessionId, chunk.toolCallUpdate);
167
+ // Also update in current assistant message (for inline display)
168
+ updateToolCallInCurrentMessage(chunk.toolCallUpdate);
145
169
  }
146
170
  }
147
171
  // Ensure streaming state is cleared even if no explicit isComplete was received
@@ -176,6 +200,10 @@ export function useChatMessages(client, startSession) {
176
200
  setStreamingStartTime,
177
201
  setError,
178
202
  setLatestContextSize,
203
+ addToolCall,
204
+ updateToolCall,
205
+ addToolCallToCurrentMessage,
206
+ updateToolCallInCurrentMessage,
179
207
  ]);
180
208
  return {
181
209
  messages,
@@ -36,8 +36,6 @@ export interface ChatLayoutMessagesProps extends React.HTMLAttributes<HTMLDivEle
36
36
  onScrollChange?: (isAtBottom: boolean) => void;
37
37
  /** Whether to show scroll to bottom button */
38
38
  showScrollToBottom?: boolean;
39
- /** Whether to scroll to bottom on initial mount (default: true) */
40
- initialScrollToBottom?: boolean;
41
39
  }
42
40
  declare const ChatLayoutMessages: React.ForwardRefExoticComponent<ChatLayoutMessagesProps & React.RefAttributes<HTMLDivElement>>;
43
41
  export interface ChatLayoutFooterProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -38,190 +38,135 @@ const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, childr
38
38
  return (_jsxs("div", { ref: ref, className: cn("relative flex flex-1 flex-col overflow-hidden", className), ...props, children: [children, showToaster && _jsx(Toaster, {})] }));
39
39
  });
40
40
  ChatLayoutBody.displayName = "ChatLayout.Body";
41
- const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, initialScrollToBottom = true, ...props }, ref) => {
42
- const [showScrollButton, setShowScrollButton] = React.useState(false);
41
+ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, ...props }, ref) => {
43
42
  const scrollContainerRef = React.useRef(null);
44
- const wasAtBottomRef = React.useRef(true); // Track if user was at bottom before content update
45
- const isAutoScrollingRef = React.useRef(false); // Track if we're programmatically scrolling
46
- const hasInitialScrolledRef = React.useRef(false); // Track if initial scroll has happened
43
+ const [isAtBottom, setIsAtBottom] = React.useState(true);
44
+ const isAtBottomRef = React.useRef(true);
45
+ const isUserScrollingRef = React.useRef(false);
46
+ const lastScrollHeightRef = React.useRef(0);
47
+ const lastMutationTimeRef = React.useRef(0);
48
+ const isSmoothScrollingRef = React.useRef(false); // Protect smooth scrolls from interruption
49
+ // Keep ref in sync with state
50
+ React.useEffect(() => {
51
+ isAtBottomRef.current = isAtBottom;
52
+ }, [isAtBottom]);
47
53
  // Merge refs
48
54
  React.useImperativeHandle(ref, () => scrollContainerRef.current);
49
- // Check if user is at bottom of scroll
50
- const checkScrollPosition = React.useCallback(() => {
55
+ // Check if at bottom
56
+ const checkIfAtBottom = React.useCallback(() => {
51
57
  const container = scrollContainerRef.current;
52
58
  if (!container)
53
- return false;
59
+ return true;
54
60
  const { scrollTop, scrollHeight, clientHeight } = container;
55
- const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
56
- const isAtBottom = distanceFromBottom < 100; // 100px threshold
57
- setShowScrollButton(!isAtBottom && showScrollToBottom);
58
- onScrollChange?.(isAtBottom);
59
- return isAtBottom;
60
- }, [onScrollChange, showScrollToBottom]);
61
- // Handle scroll events
62
- const handleScroll = React.useCallback(() => {
63
- // If this is a programmatic scroll, don't update wasAtBottomRef
64
- if (isAutoScrollingRef.current) {
65
- return;
66
- }
67
- // This is a user-initiated scroll, update the position
68
- const isAtBottom = checkScrollPosition();
69
- wasAtBottomRef.current = isAtBottom;
70
- }, [checkScrollPosition]);
71
- // Scroll to bottom function
72
- const scrollToBottom = React.useCallback((smooth = true) => {
61
+ return scrollTop + clientHeight >= scrollHeight - 100;
62
+ }, []);
63
+ // Scroll to bottom with protection for smooth scrolls
64
+ const scrollToBottom = React.useCallback((behavior = "smooth") => {
73
65
  const container = scrollContainerRef.current;
74
66
  if (!container)
75
67
  return;
76
- // Mark that we're about to programmatically scroll
77
- isAutoScrollingRef.current = true;
78
- wasAtBottomRef.current = true; // Set immediately for instant scrolls
68
+ // If we're doing a smooth scroll, don't interrupt with instant
69
+ if (isSmoothScrollingRef.current && behavior === "instant") {
70
+ return;
71
+ }
72
+ if (behavior === "smooth") {
73
+ isSmoothScrollingRef.current = true;
74
+ // Clear the smooth scroll protection after animation completes
75
+ setTimeout(() => {
76
+ isSmoothScrollingRef.current = false;
77
+ }, 500);
78
+ }
79
79
  container.scrollTo({
80
80
  top: container.scrollHeight,
81
- behavior: smooth ? "smooth" : "auto",
81
+ behavior,
82
82
  });
83
- // Clear the flag after scroll completes
84
- // For instant scrolling, clear immediately; for smooth, wait
85
- setTimeout(() => {
86
- isAutoScrollingRef.current = false;
87
- }, smooth ? 300 : 0);
88
83
  }, []);
89
- // Auto-scroll when content changes if user was at bottom
90
- React.useEffect(() => {
91
- const container = scrollContainerRef.current;
92
- if (!container)
93
- return;
94
- // If user was at the bottom, scroll to new content
95
- if (wasAtBottomRef.current && !isAutoScrollingRef.current) {
96
- // Use requestAnimationFrame to ensure DOM has updated
97
- requestAnimationFrame(() => {
98
- scrollToBottom(false); // Use instant scroll for streaming to avoid jarring smooth animations
99
- });
100
- }
101
- // Update scroll position state (but don't change wasAtBottomRef if we're auto-scrolling)
102
- if (!isAutoScrollingRef.current) {
103
- checkScrollPosition();
104
- }
105
- }, [children, scrollToBottom, checkScrollPosition]);
106
- // Track last scroll height to detect when content stops loading
107
- const lastScrollHeightRef = React.useRef(0);
108
- const scrollStableCountRef = React.useRef(0);
109
- // Scroll to bottom on initial mount and during session loading
110
- // Keep scrolling until content stabilizes (no more changes)
84
+ // Handle user scroll events
111
85
  React.useEffect(() => {
112
- if (!initialScrollToBottom)
113
- return;
114
86
  const container = scrollContainerRef.current;
115
87
  if (!container)
116
88
  return;
117
- const scrollToBottomInstant = () => {
118
- container.scrollTop = container.scrollHeight;
119
- wasAtBottomRef.current = true;
89
+ let scrollTimeout;
90
+ const handleScroll = () => {
91
+ // Mark as user scrolling
92
+ isUserScrollingRef.current = true;
93
+ clearTimeout(scrollTimeout);
94
+ // Update isAtBottom state
95
+ const atBottom = checkIfAtBottom();
96
+ setIsAtBottom(atBottom);
97
+ isAtBottomRef.current = atBottom;
98
+ onScrollChange?.(atBottom);
99
+ // Reset user scrolling flag after scroll ends
100
+ scrollTimeout = setTimeout(() => {
101
+ isUserScrollingRef.current = false;
102
+ }, 150);
120
103
  };
121
- // Check if content has stabilized (scrollHeight hasn't changed)
122
- const currentHeight = container.scrollHeight;
123
- if (currentHeight === lastScrollHeightRef.current) {
124
- scrollStableCountRef.current++;
125
- }
126
- else {
127
- scrollStableCountRef.current = 0;
128
- lastScrollHeightRef.current = currentHeight;
129
- }
130
- // If content is still loading (height changing) or we haven't scrolled yet,
131
- // keep auto-scrolling. Stop after content is stable for a few renders.
132
- if (scrollStableCountRef.current < 3) {
133
- isAutoScrollingRef.current = true;
134
- scrollToBottomInstant();
135
- hasInitialScrolledRef.current = true;
136
- }
137
- else {
138
- // Content is stable, stop auto-scrolling
139
- isAutoScrollingRef.current = false;
140
- }
141
- }, [initialScrollToBottom, children]);
142
- // Also use a timer-based approach as backup for session replay
143
- // which may not trigger children changes
144
- React.useEffect(() => {
145
- if (!initialScrollToBottom)
146
- return;
147
- const container = scrollContainerRef.current;
148
- if (!container)
149
- return;
150
- // Keep scrolling to bottom for the first 2 seconds of session load
151
- // to catch async message replay
152
- let cancelled = false;
153
- const scrollInterval = setInterval(() => {
154
- if (cancelled)
155
- return;
156
- if (container.scrollHeight > container.clientHeight) {
157
- isAutoScrollingRef.current = true;
158
- container.scrollTop = container.scrollHeight;
159
- wasAtBottomRef.current = true;
160
- hasInitialScrolledRef.current = true;
161
- }
162
- }, 100);
163
- // Stop after 2 seconds
164
- const timeout = setTimeout(() => {
165
- clearInterval(scrollInterval);
166
- isAutoScrollingRef.current = false;
167
- }, 2000);
104
+ container.addEventListener("scroll", handleScroll, { passive: true });
168
105
  return () => {
169
- cancelled = true;
170
- clearInterval(scrollInterval);
171
- clearTimeout(timeout);
106
+ container.removeEventListener("scroll", handleScroll);
107
+ clearTimeout(scrollTimeout);
172
108
  };
173
- }, [initialScrollToBottom]); // Only run once on mount
174
- // Check scroll position on mount
109
+ }, [checkIfAtBottom, onScrollChange]);
110
+ // Auto-scroll when content changes using MutationObserver + ResizeObserver
175
111
  React.useEffect(() => {
176
- if (!isAutoScrollingRef.current) {
177
- const isAtBottom = checkScrollPosition();
178
- wasAtBottomRef.current = isAtBottom;
179
- }
180
- }, [checkScrollPosition]);
181
- // Detect user interaction with scroll area (wheel, touch) - IMMEDIATELY break auto-scroll
182
- const handleUserInteraction = React.useCallback(() => {
183
- // Immediately mark that user is interacting
184
- isAutoScrollingRef.current = false;
185
- // For wheel/touch events, temporarily break auto-scroll
186
- // The actual scroll event will update wasAtBottomRef properly
187
- // This prevents the race condition where content updates before scroll completes
188
112
  const container = scrollContainerRef.current;
189
113
  if (!container)
190
114
  return;
191
- // Check current position BEFORE the scroll happens
192
- const { scrollTop, scrollHeight, clientHeight } = container;
193
- const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
194
- // If user is not currently at the bottom, definitely break auto-scroll
195
- if (distanceFromBottom >= 100) {
196
- wasAtBottomRef.current = false;
197
- }
198
- // If they are at bottom, the scroll event will determine if they stay there
199
- }, []);
200
- // Handle keyboard navigation
201
- const handleKeyDown = React.useCallback((e) => {
202
- // If user presses arrow keys, page up/down, home/end - they're scrolling
203
- const scrollKeys = [
204
- "ArrowUp",
205
- "ArrowDown",
206
- "PageUp",
207
- "PageDown",
208
- "Home",
209
- "End",
210
- ];
211
- if (scrollKeys.includes(e.key)) {
212
- isAutoScrollingRef.current = false;
213
- // Check position on next frame after the scroll happens
115
+ const scrollIfNeeded = () => {
116
+ // Only auto-scroll if user was at bottom and isn't actively scrolling
117
+ if (!isAtBottomRef.current || isUserScrollingRef.current) {
118
+ return;
119
+ }
120
+ const now = Date.now();
121
+ const timeSinceLastMutation = now - lastMutationTimeRef.current;
122
+ const currentScrollHeight = container.scrollHeight;
123
+ const heightDelta = currentScrollHeight - lastScrollHeightRef.current;
124
+ lastMutationTimeRef.current = now;
125
+ lastScrollHeightRef.current = currentScrollHeight;
126
+ // Detect if this is likely a new message (large height increase + time gap)
127
+ // vs streaming (small incremental changes)
128
+ const isLikelyNewMessage = heightDelta > 80 && timeSinceLastMutation > 150;
214
129
  requestAnimationFrame(() => {
215
- const container = scrollContainerRef.current;
216
- if (!container)
217
- return;
218
- const { scrollTop, scrollHeight, clientHeight } = container;
219
- const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
220
- wasAtBottomRef.current = distanceFromBottom < 100;
130
+ if (isLikelyNewMessage) {
131
+ scrollToBottom("smooth");
132
+ }
133
+ else {
134
+ scrollToBottom("instant");
135
+ }
136
+ setIsAtBottom(true);
137
+ isAtBottomRef.current = true;
221
138
  });
139
+ };
140
+ // Watch for DOM changes (new messages, content updates)
141
+ const mutationObserver = new MutationObserver(scrollIfNeeded);
142
+ mutationObserver.observe(container, {
143
+ childList: true,
144
+ subtree: true,
145
+ characterData: true,
146
+ });
147
+ // Watch for size changes (images loading, content expanding)
148
+ const resizeObserver = new ResizeObserver(scrollIfNeeded);
149
+ resizeObserver.observe(container);
150
+ // Also observe direct children for size changes
151
+ for (const child of Array.from(container.children)) {
152
+ resizeObserver.observe(child);
222
153
  }
223
- }, []);
224
- return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), onScroll: handleScroll, onWheel: handleUserInteraction, onTouchStart: handleUserInteraction, onKeyDown: handleKeyDown, tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: () => scrollToBottom(true), className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
154
+ // Initialize scroll height
155
+ lastScrollHeightRef.current = container.scrollHeight;
156
+ return () => {
157
+ mutationObserver.disconnect();
158
+ resizeObserver.disconnect();
159
+ };
160
+ }, [scrollToBottom]);
161
+ // Button click handler - smooth scroll and re-engage auto-scroll
162
+ const handleScrollToBottomClick = React.useCallback(() => {
163
+ setIsAtBottom(true);
164
+ isAtBottomRef.current = true;
165
+ scrollToBottom("smooth");
166
+ }, [scrollToBottom]);
167
+ return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollToBottom && (_jsx("button", { type: "button", onClick: handleScrollToBottomClick, className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", isAtBottom
168
+ ? "pointer-events-none scale-0 opacity-0"
169
+ : "pointer-events-auto scale-100 opacity-100"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
225
170
  });
226
171
  ChatLayoutMessages.displayName = "ChatLayout.Messages";
227
172
  const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  /**
3
3
  * Response component inspired by shadcn.io/ai
4
- * Streaming-optimized markdown renderer for AI-generated content
4
+ * Streaming-optimized markdown renderer using streamdown for incremental parsing
5
5
  */
6
6
  export interface ResponseProps extends React.HTMLAttributes<HTMLDivElement> {
7
7
  /** The markdown content to render */
@@ -13,4 +13,4 @@ export interface ResponseProps extends React.HTMLAttributes<HTMLDivElement> {
13
13
  /** Custom empty state message */
14
14
  emptyMessage?: string;
15
15
  }
16
- export declare const Response: React.ForwardRefExoticComponent<ResponseProps & React.RefAttributes<HTMLDivElement>>;
16
+ export declare const Response: React.NamedExoticComponent<ResponseProps & React.RefAttributes<HTMLDivElement>>;
@@ -1,9 +1,9 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import * as React from "react";
3
- import ReactMarkdown from "react-markdown";
4
- import remarkGfm from "remark-gfm";
4
+ import { Streamdown } from "streamdown";
5
5
  import { cn } from "../lib/utils.js";
6
- export const Response = React.forwardRef(({ content, isStreaming = false, showEmpty = true, emptyMessage = "", className, ...props }, ref) => {
6
+ export const Response = React.memo(React.forwardRef(({ content, isStreaming = false, showEmpty = true, emptyMessage = "", className, ...props }, ref) => {
7
7
  // Show empty state during streaming if no content yet
8
8
  if (!content && isStreaming && showEmpty) {
9
9
  return (_jsx("div", { ref: ref, className: cn("opacity-70 italic text-paragraph-sm", className), ...props, children: emptyMessage }));
@@ -11,89 +11,6 @@ export const Response = React.forwardRef(({ content, isStreaming = false, showEm
11
11
  if (!content) {
12
12
  return null;
13
13
  }
14
- const components = {
15
- // Table styling
16
- table: ({ node, ...props }) => (_jsx("div", { className: "overflow-x-auto my-4", children: _jsx("table", { className: "min-w-full border-collapse border border-border rounded-md", ...props }) })),
17
- thead: ({ node, ...props }) => (_jsx("thead", { className: "bg-card border-b border-border", ...props })),
18
- tbody: ({ node, ...props }) => _jsx("tbody", { ...props }),
19
- tr: ({ node, ...props }) => (_jsx("tr", { className: "border-b border-border hover:bg-card transition-colors", ...props })),
20
- th: ({ node, ...props }) => (_jsx("th", { className: "px-4 py-2 text-left font-semibold text-foreground border-r border-border last:border-r-0", ...props })),
21
- td: ({ node, ...props }) => (_jsx("td", { className: "px-4 py-2 text-foreground border-r border-border last:border-r-0", ...props })),
22
- // Task list styling
23
- input: ({ node, checked, ...props }) => {
24
- if (props.type === "checkbox") {
25
- return (_jsx("input", { type: "checkbox", checked: checked || false, disabled: true, readOnly: true, className: "mr-2 w-4 h-4 accent-[primary] cursor-not-allowed", ...props }));
26
- }
27
- return _jsx("input", { ...props });
28
- },
29
- // Code block styling with enhanced shadows
30
- code: ({ node, ...props }) => {
31
- const inline = !props.className?.includes("language-");
32
- if (inline) {
33
- return (_jsx("code", { className: "px-1.5 py-0.5 bg-card border border-border rounded text-code text-foreground", ...props }));
34
- }
35
- return (_jsx("code", { className: "block p-4 bg-card border border-border rounded-md overflow-x-auto text-code text-foreground shadow-sm", ...props }));
36
- },
37
- pre: ({ node, ...props }) => (_jsx("pre", { className: "my-4 rounded-lg", ...props })),
38
- // Heading styling with improved hierarchy
39
- h1: ({ node, ...props }) => (_jsx("h1", { className: "text-heading-3 mt-6 mb-4 text-foreground border-b border-border pb-2", ...props })),
40
- h2: ({ node, ...props }) => (_jsx("h2", { className: "text-subheading mt-5 mb-3 text-foreground border-b border-border/50 pb-1.5", ...props })),
41
- h3: ({ node, ...props }) => (_jsx("h3", { className: "text-subheading mt-4 mb-2 text-foreground", ...props })),
42
- h4: ({ node, ...props }) => (_jsx("h4", { className: "text-paragraph-sm font-semibold mt-3 mb-2 text-foreground", ...props })),
43
- // List styling
44
- ul: ({ node, ...props }) => {
45
- // Check if this is a task list by looking for checkbox inputs in children
46
- const isTaskList = node?.children?.some((child) => typeof child === "object" &&
47
- child !== null &&
48
- "type" in child &&
49
- child.type === "element" &&
50
- "tagName" in child &&
51
- child.tagName === "li" &&
52
- "children" in child &&
53
- Array.isArray(child.children) &&
54
- child.children.some((grandChild) => typeof grandChild === "object" &&
55
- grandChild !== null &&
56
- "type" in grandChild &&
57
- grandChild.type === "element" &&
58
- "tagName" in grandChild &&
59
- grandChild.tagName === "input" &&
60
- "properties" in grandChild &&
61
- typeof grandChild.properties === "object" &&
62
- grandChild.properties !== null &&
63
- "type" in grandChild.properties &&
64
- grandChild.properties.type === "checkbox"));
65
- return (_jsx("ul", { className: cn("my-2 space-y-1 text-foreground", isTaskList
66
- ? "list-none space-y-2"
67
- : "list-disc list-outside pl-4"), ...props }));
68
- },
69
- ol: ({ node, ...props }) => (_jsx("ol", { className: "list-decimal list-outside pl-4 my-2 space-y-1 text-foreground", ...props })),
70
- // List item styling
71
- li: ({ node, ...props }) => {
72
- // Check if this li contains a checkbox (task list item)
73
- const isTaskListItem = node?.children?.some((child) => typeof child === "object" &&
74
- child !== null &&
75
- "type" in child &&
76
- child.type === "element" &&
77
- "tagName" in child &&
78
- child.tagName === "input" &&
79
- "properties" in child &&
80
- typeof child.properties === "object" &&
81
- child.properties !== null &&
82
- "type" in child.properties &&
83
- child.properties.type === "checkbox");
84
- return (_jsx("li", { className: cn(isTaskListItem ? "flex items-start gap-2" : ""), ...props }));
85
- },
86
- // Link styling with hover effect
87
- a: ({ node, ...props }) => (_jsx("a", { className: "text-primary hover:underline decoration-2 underline-offset-2 transition-all", target: "_blank", rel: "noopener noreferrer", ...props })),
88
- // Paragraph styling
89
- p: ({ node, ...props }) => (_jsx("p", { className: "mb-2 text-foreground leading-relaxed", ...props })),
90
- // Blockquote styling with enhanced visual
91
- blockquote: ({ node, ...props }) => (_jsx("blockquote", { className: "border-l-4 border-[primary] pl-4 italic my-4 text-foreground bg-card py-2 rounded-r-md shadow-sm", ...props })),
92
- // Horizontal rule
93
- hr: ({ node, ...props }) => (_jsx("hr", { className: "my-6 border-t border-border opacity-50", ...props })),
94
- // Image styling
95
- img: ({ node, ...props }) => (_jsx("img", { className: "max-w-full h-auto rounded-md border border-border my-4", loading: "lazy", ...props })),
96
- };
97
- return (_jsx("div", { ref: ref, className: cn("markdown-content prose prose-sm max-w-none dark:prose-invert", className), ...props, children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: components, children: content }) }));
98
- });
14
+ return (_jsx("div", { ref: ref, ...props, children: _jsx(Streamdown, { className: cn("size-full", "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0", "[&_code]:whitespace-pre-wrap [&_code]:break-words", "[&_pre]:max-w-full [&_pre]:overflow-x-auto", className), children: content }) }));
15
+ }), (prevProps, nextProps) => prevProps.content === nextProps.content);
99
16
  Response.displayName = "Response";
@@ -178,9 +178,10 @@ export declare const Message: z.ZodObject<{
178
178
  }, z.core.$strip>;
179
179
  export type Message = z.infer<typeof Message>;
180
180
  /**
181
- * Streaming message chunk
181
+ * Streaming message chunk - content delta
182
182
  */
183
- export declare const MessageChunk: z.ZodObject<{
183
+ export declare const ContentChunk: z.ZodObject<{
184
+ type: z.ZodLiteral<"content">;
184
185
  id: z.ZodString;
185
186
  role: z.ZodEnum<{
186
187
  user: "user";
@@ -241,4 +242,100 @@ export declare const MessageChunk: z.ZodObject<{
241
242
  }, z.core.$strip>>;
242
243
  }, z.core.$strip>>;
243
244
  }, z.core.$strip>;
245
+ export type ContentChunk = z.infer<typeof ContentChunk>;
246
+ /**
247
+ * Tool call event chunk
248
+ */
249
+ export declare const ToolCallChunk: z.ZodObject<{
250
+ type: z.ZodLiteral<"tool_call">;
251
+ id: z.ZodString;
252
+ toolCall: z.ZodAny;
253
+ messageId: z.ZodOptional<z.ZodString>;
254
+ }, z.core.$strip>;
255
+ export type ToolCallChunk = z.infer<typeof ToolCallChunk>;
256
+ /**
257
+ * Tool call update chunk
258
+ */
259
+ export declare const ToolCallUpdateChunk: z.ZodObject<{
260
+ type: z.ZodLiteral<"tool_call_update">;
261
+ id: z.ZodString;
262
+ toolCallUpdate: z.ZodAny;
263
+ messageId: z.ZodOptional<z.ZodString>;
264
+ }, z.core.$strip>;
265
+ export type ToolCallUpdateChunk = z.infer<typeof ToolCallUpdateChunk>;
266
+ /**
267
+ * Message chunk - discriminated union of all chunk types
268
+ */
269
+ export declare const MessageChunk: z.ZodDiscriminatedUnion<[z.ZodObject<{
270
+ type: z.ZodLiteral<"content">;
271
+ id: z.ZodString;
272
+ role: z.ZodEnum<{
273
+ user: "user";
274
+ assistant: "assistant";
275
+ system: "system";
276
+ tool: "tool";
277
+ }>;
278
+ contentDelta: z.ZodDiscriminatedUnion<[z.ZodObject<{
279
+ type: z.ZodLiteral<"text">;
280
+ text: z.ZodString;
281
+ }, z.core.$strip>, z.ZodObject<{
282
+ type: z.ZodLiteral<"image">;
283
+ url: z.ZodOptional<z.ZodString>;
284
+ source: z.ZodOptional<z.ZodObject<{
285
+ type: z.ZodLiteral<"base64">;
286
+ media_type: z.ZodEnum<{
287
+ "image/jpeg": "image/jpeg";
288
+ "image/png": "image/png";
289
+ "image/gif": "image/gif";
290
+ "image/webp": "image/webp";
291
+ }>;
292
+ data: z.ZodString;
293
+ }, z.core.$strip>>;
294
+ }, z.core.$strip>, z.ZodObject<{
295
+ type: z.ZodLiteral<"file">;
296
+ name: z.ZodString;
297
+ path: z.ZodOptional<z.ZodString>;
298
+ url: z.ZodOptional<z.ZodString>;
299
+ mimeType: z.ZodString;
300
+ size: z.ZodOptional<z.ZodNumber>;
301
+ }, z.core.$strip>, z.ZodObject<{
302
+ type: z.ZodLiteral<"tool_call">;
303
+ id: z.ZodString;
304
+ name: z.ZodString;
305
+ arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
306
+ }, z.core.$strip>, z.ZodObject<{
307
+ type: z.ZodLiteral<"tool_result">;
308
+ callId: z.ZodString;
309
+ result: z.ZodUnknown;
310
+ error: z.ZodOptional<z.ZodString>;
311
+ }, z.core.$strip>], "type">;
312
+ isComplete: z.ZodBoolean;
313
+ tokenUsage: z.ZodOptional<z.ZodObject<{
314
+ inputTokens: z.ZodOptional<z.ZodNumber>;
315
+ outputTokens: z.ZodOptional<z.ZodNumber>;
316
+ totalTokens: z.ZodOptional<z.ZodNumber>;
317
+ }, z.core.$strip>>;
318
+ contextInputTokens: z.ZodOptional<z.ZodNumber>;
319
+ _meta: z.ZodOptional<z.ZodObject<{
320
+ context_size: z.ZodOptional<z.ZodObject<{
321
+ systemPromptTokens: z.ZodNumber;
322
+ userMessagesTokens: z.ZodNumber;
323
+ assistantMessagesTokens: z.ZodNumber;
324
+ toolInputTokens: z.ZodNumber;
325
+ toolResultsTokens: z.ZodNumber;
326
+ totalEstimated: z.ZodNumber;
327
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
328
+ }, z.core.$strip>>;
329
+ }, z.core.$strip>>;
330
+ }, z.core.$strip>, z.ZodObject<{
331
+ type: z.ZodLiteral<"tool_call">;
332
+ id: z.ZodString;
333
+ toolCall: z.ZodAny;
334
+ messageId: z.ZodOptional<z.ZodString>;
335
+ }, z.core.$strip>, z.ZodObject<{
336
+ type: z.ZodLiteral<"tool_call_update">;
337
+ id: z.ZodString;
338
+ toolCallUpdate: z.ZodAny;
339
+ messageId: z.ZodOptional<z.ZodString>;
340
+ }, z.core.$strip>], "type">;
244
341
  export type MessageChunk = z.infer<typeof MessageChunk>;
@@ -97,9 +97,10 @@ export const Message = z.object({
97
97
  metadata: z.record(z.string(), z.unknown()).optional(),
98
98
  });
99
99
  /**
100
- * Streaming message chunk
100
+ * Streaming message chunk - content delta
101
101
  */
102
- export const MessageChunk = z.object({
102
+ export const ContentChunk = z.object({
103
+ type: z.literal("content"),
103
104
  id: z.string(),
104
105
  role: MessageRole,
105
106
  contentDelta: Content,
@@ -128,3 +129,29 @@ export const MessageChunk = z.object({
128
129
  })
129
130
  .optional(),
130
131
  });
132
+ /**
133
+ * Tool call event chunk
134
+ */
135
+ export const ToolCallChunk = z.object({
136
+ type: z.literal("tool_call"),
137
+ id: z.string(),
138
+ toolCall: z.any(), // Will be typed properly by importing ToolCall
139
+ messageId: z.string().optional(),
140
+ });
141
+ /**
142
+ * Tool call update chunk
143
+ */
144
+ export const ToolCallUpdateChunk = z.object({
145
+ type: z.literal("tool_call_update"),
146
+ id: z.string(),
147
+ toolCallUpdate: z.any(), // Will be typed properly by importing ToolCallUpdate
148
+ messageId: z.string().optional(),
149
+ });
150
+ /**
151
+ * Message chunk - discriminated union of all chunk types
152
+ */
153
+ export const MessageChunk = z.discriminatedUnion("type", [
154
+ ContentChunk,
155
+ ToolCallChunk,
156
+ ToolCallUpdateChunk,
157
+ ]);
@@ -346,6 +346,7 @@ export class HttpTransport {
346
346
  const resolver = this.chunkResolvers.shift();
347
347
  if (resolver) {
348
348
  resolver({
349
+ type: "content",
349
350
  id: this.currentSessionId || "unknown",
350
351
  role: "assistant",
351
352
  contentDelta: { type: "text", text: "" },
@@ -354,6 +355,7 @@ export class HttpTransport {
354
355
  }
355
356
  else {
356
357
  this.messageQueue.push({
358
+ type: "content",
357
359
  id: this.currentSessionId || "unknown",
358
360
  role: "assistant",
359
361
  contentDelta: { type: "text", text: "" },
@@ -379,7 +381,7 @@ export class HttpTransport {
379
381
  const chunk = this.messageQueue.shift();
380
382
  if (chunk) {
381
383
  yield chunk;
382
- if (chunk.isComplete) {
384
+ if (chunk.type === "content" && chunk.isComplete) {
383
385
  return;
384
386
  }
385
387
  }
@@ -389,7 +391,7 @@ export class HttpTransport {
389
391
  const chunk = await new Promise((resolve) => {
390
392
  this.chunkResolvers.push(resolve);
391
393
  });
392
- if (chunk.isComplete) {
394
+ if (chunk.type === "content" && chunk.isComplete) {
393
395
  yield chunk;
394
396
  return;
395
397
  }
@@ -407,6 +409,7 @@ export class HttpTransport {
407
409
  }
408
410
  // Mark the stream as complete
409
411
  yield {
412
+ type: "content",
410
413
  id: this.currentSessionId || "unknown",
411
414
  role: "assistant",
412
415
  contentDelta: { type: "text", text: "" },
@@ -760,7 +763,20 @@ export class HttpTransport {
760
763
  toolCall: toolCall,
761
764
  messageId,
762
765
  };
763
- this.notifySessionUpdate(sessionUpdate);
766
+ // Queue tool call as a chunk for ordered processing
767
+ const toolCallChunk = {
768
+ type: "tool_call",
769
+ id: sessionId,
770
+ toolCall: toolCall,
771
+ messageId,
772
+ };
773
+ const resolver = this.chunkResolvers.shift();
774
+ if (resolver) {
775
+ resolver(toolCallChunk);
776
+ }
777
+ else {
778
+ this.messageQueue.push(toolCallChunk);
779
+ }
764
780
  }
765
781
  else if (update?.sessionUpdate === "tool_call_update") {
766
782
  // Extract messageId and metadata from _meta
@@ -882,10 +898,23 @@ export class HttpTransport {
882
898
  toolCallUpdate: toolCallUpdate,
883
899
  messageId,
884
900
  };
885
- logger.debug("Notifying tool_call_update session update", {
901
+ // Queue tool call update as a chunk for ordered processing
902
+ const toolCallUpdateChunk = {
903
+ type: "tool_call_update",
904
+ id: sessionId,
905
+ toolCallUpdate: toolCallUpdate,
906
+ messageId,
907
+ };
908
+ const resolver = this.chunkResolvers.shift();
909
+ if (resolver) {
910
+ resolver(toolCallUpdateChunk);
911
+ }
912
+ else {
913
+ this.messageQueue.push(toolCallUpdateChunk);
914
+ }
915
+ logger.debug("Queued tool_call_update chunk", {
886
916
  sessionUpdate,
887
917
  });
888
- this.notifySessionUpdate(sessionUpdate);
889
918
  }
890
919
  else if (update &&
891
920
  "sessionUpdate" in update &&
@@ -966,10 +995,23 @@ export class HttpTransport {
966
995
  toolCallUpdate: toolOutput,
967
996
  messageId,
968
997
  };
969
- logger.debug("Notifying tool_output as tool_call_update", {
998
+ // Queue tool output as a chunk for ordered processing
999
+ const toolCallUpdateChunk = {
1000
+ type: "tool_call_update",
1001
+ id: sessionId,
1002
+ toolCallUpdate: toolOutput,
1003
+ messageId,
1004
+ };
1005
+ const resolver = this.chunkResolvers.shift();
1006
+ if (resolver) {
1007
+ resolver(toolCallUpdateChunk);
1008
+ }
1009
+ else {
1010
+ this.messageQueue.push(toolCallUpdateChunk);
1011
+ }
1012
+ logger.debug("Queued tool_output as tool_call_update chunk", {
970
1013
  sessionUpdate,
971
1014
  });
972
- this.notifySessionUpdate(sessionUpdate);
973
1015
  }
974
1016
  else if (update?.sessionUpdate === "agent_message_chunk") {
975
1017
  // Check if this is a replay (not live streaming)
@@ -1015,6 +1057,7 @@ export class HttpTransport {
1015
1057
  let chunk = null;
1016
1058
  if (contentObj.type === "text" && typeof contentObj.text === "string") {
1017
1059
  chunk = {
1060
+ type: "content",
1018
1061
  id: params.sessionId,
1019
1062
  role: "assistant",
1020
1063
  contentDelta: { type: "text", text: contentObj.text },
@@ -186,7 +186,19 @@ export class StdioTransport {
186
186
  status: "active",
187
187
  toolCall: toolCall,
188
188
  };
189
- self.notifySessionUpdate(sessionUpdate);
189
+ // Queue tool call as a chunk for ordered processing
190
+ const toolCallChunk = {
191
+ type: "tool_call",
192
+ id: sessionId,
193
+ toolCall: toolCall,
194
+ };
195
+ const resolver = self.chunkResolvers.shift();
196
+ if (resolver) {
197
+ resolver(toolCallChunk);
198
+ }
199
+ else {
200
+ self.messageQueue.push(toolCallChunk);
201
+ }
190
202
  }
191
203
  else if (update?.sessionUpdate === "tool_call_update") {
192
204
  // Tool call update notification
@@ -259,7 +271,19 @@ export class StdioTransport {
259
271
  status: "active",
260
272
  toolCallUpdate: toolCallUpdate,
261
273
  };
262
- self.notifySessionUpdate(sessionUpdate);
274
+ // Queue tool call update as a chunk for ordered processing
275
+ const toolCallUpdateChunk = {
276
+ type: "tool_call_update",
277
+ id: sessionId,
278
+ toolCallUpdate: toolCallUpdate,
279
+ };
280
+ const resolver = self.chunkResolvers.shift();
281
+ if (resolver) {
282
+ resolver(toolCallUpdateChunk);
283
+ }
284
+ else {
285
+ self.messageQueue.push(toolCallUpdateChunk);
286
+ }
263
287
  }
264
288
  else if (update?.sessionUpdate === "agent_message_chunk") {
265
289
  // Handle agent message chunks
@@ -291,6 +315,7 @@ export class StdioTransport {
291
315
  if (contentObj.type === "text" &&
292
316
  typeof contentObj.text === "string") {
293
317
  chunk = {
318
+ type: "content",
294
319
  id: params.sessionId,
295
320
  role: "assistant",
296
321
  contentDelta: { type: "text", text: contentObj.text },
@@ -485,6 +510,7 @@ export class StdioTransport {
485
510
  const resolver = this.chunkResolvers.shift();
486
511
  if (resolver) {
487
512
  resolver({
513
+ type: "content",
488
514
  id: this.currentSessionId || "unknown",
489
515
  role: "assistant",
490
516
  contentDelta: { type: "text", text: "" },
@@ -507,6 +533,9 @@ export class StdioTransport {
507
533
  const chunk = this.messageQueue.shift();
508
534
  if (chunk) {
509
535
  yield chunk;
536
+ if (chunk.type === "content" && chunk.isComplete) {
537
+ return;
538
+ }
510
539
  }
511
540
  }
512
541
  else {
@@ -514,7 +543,7 @@ export class StdioTransport {
514
543
  const chunk = await new Promise((resolve) => {
515
544
  this.chunkResolvers.push(resolve);
516
545
  });
517
- if (chunk.isComplete) {
546
+ if (chunk.type === "content" && chunk.isComplete) {
518
547
  yield chunk;
519
548
  return;
520
549
  }
@@ -532,6 +561,7 @@ export class StdioTransport {
532
561
  }
533
562
  // Mark the stream as complete
534
563
  yield {
564
+ type: "content",
535
565
  id: this.currentSessionId || "unknown",
536
566
  role: "assistant",
537
567
  contentDelta: { type: "text", text: "" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.58",
3
+ "version": "0.1.65",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -42,13 +42,14 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@agentclientprotocol/sdk": "^0.5.1",
45
- "@townco/core": "0.0.36",
46
45
  "@radix-ui/react-dialog": "^1.1.15",
47
46
  "@radix-ui/react-dropdown-menu": "^2.1.16",
48
47
  "@radix-ui/react-label": "^2.1.8",
49
48
  "@radix-ui/react-select": "^2.2.6",
50
49
  "@radix-ui/react-slot": "^1.2.4",
51
50
  "@radix-ui/react-tabs": "^1.1.13",
51
+ "@radix-ui/react-tooltip": "^1.2.8",
52
+ "@townco/core": "0.0.43",
52
53
  "@uiw/react-json-view": "^2.0.0-alpha.39",
53
54
  "bun": "^1.3.1",
54
55
  "class-variance-authority": "^0.7.1",
@@ -56,16 +57,16 @@
56
57
  "lucide-react": "^0.552.0",
57
58
  "react-markdown": "^10.1.0",
58
59
  "react-resizable-panels": "^3.0.6",
59
- "@radix-ui/react-tooltip": "^1.2.8",
60
60
  "remark-gfm": "^4.0.1",
61
61
  "sonner": "^2.0.7",
62
+ "streamdown": "^1.6.9",
62
63
  "tailwind-merge": "^3.3.1",
63
64
  "zod": "^4.1.12",
64
65
  "zustand": "^5.0.8"
65
66
  },
66
67
  "devDependencies": {
67
68
  "@tailwindcss/postcss": "^4.1.17",
68
- "@townco/tsconfig": "0.1.55",
69
+ "@townco/tsconfig": "0.1.62",
69
70
  "@types/node": "^24.10.0",
70
71
  "@types/react": "^19.2.2",
71
72
  "ink": "^6.4.0",
@@ -2,6 +2,9 @@
2
2
 
3
3
  @source "../**/*.{ts,tsx}";
4
4
 
5
+ /* Include utility classes used by streamdown for markdown rendering */
6
+ @source '../node_modules/streamdown/dist/index.js';
7
+
5
8
  @theme {
6
9
  /* Semantic Color Tokens */
7
10
  --color-background: var(--background);
@@ -206,6 +209,27 @@
206
209
  }
207
210
  }
208
211
 
212
+ /* Streamdown code block dark mode styling */
213
+ /* Ensure Shiki dark theme variables are applied in dark mode */
214
+ .dark [data-streamdown="code-block"] {
215
+ background-color: var(--shiki-dark-bg, #24292e);
216
+ }
217
+
218
+ .dark [data-streamdown="code-block-body"] {
219
+ background-color: var(--shiki-dark-bg, #24292e) !important;
220
+ color: var(--shiki-dark, #e1e4e8) !important;
221
+ }
222
+
223
+ .dark [data-streamdown="code-block-header"] {
224
+ background-color: color-mix(in srgb, var(--shiki-dark-bg, #24292e) 80%, transparent);
225
+ }
226
+
227
+ /* Ensure inline code also has proper dark mode styling */
228
+ .dark [data-streamdown="inline-code"] {
229
+ background-color: var(--muted);
230
+ color: var(--foreground);
231
+ }
232
+
209
233
  @layer utilities {
210
234
  /* Typography utilities following design system */
211
235
  .text-heading-1 {