@townco/ui 0.1.45 → 0.1.46

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.
@@ -52,6 +52,12 @@ export declare function useChatMessages(client: AcpClient | null, startSession:
52
52
  outputTokens?: number | undefined;
53
53
  totalTokens?: number | undefined;
54
54
  } | undefined;
55
+ _meta?: {
56
+ truncationWarning?: string | undefined;
57
+ compactionAction?: "compacted" | "truncated" | undefined;
58
+ originalTokens?: number | undefined;
59
+ finalTokens?: number | undefined;
60
+ } | undefined;
55
61
  }[] | undefined;
56
62
  tokenUsage?: {
57
63
  inputTokens?: number | undefined;
@@ -14,7 +14,7 @@ export function useChatMessages(client, startSession) {
14
14
  const addMessage = useChatStore((state) => state.addMessage);
15
15
  const updateMessage = useChatStore((state) => state.updateMessage);
16
16
  const setError = useChatStore((state) => state.setError);
17
- const setLatestContextInputTokens = useChatStore((state) => state.setLatestContextInputTokens);
17
+ const setLatestContextSize = useChatStore((state) => state.setLatestContextSize);
18
18
  /**
19
19
  * Send a message to the agent
20
20
  */
@@ -79,14 +79,24 @@ export function useChatMessages(client, startSession) {
79
79
  let accumulatedContent = "";
80
80
  let streamCompleted = false;
81
81
  for await (const chunk of messageStream) {
82
- // Update context input tokens if provided
83
- if ("contextInputTokens" in chunk &&
84
- chunk.contextInputTokens != null) {
85
- const tokens = chunk.contextInputTokens;
86
- logger.debug("Received contextInputTokens from backend", {
87
- contextInputTokens: tokens,
82
+ // Update context size if provided (check both _meta.context_size and direct context_size)
83
+ const chunkMeta = chunk._meta;
84
+ const contextSizeData = chunkMeta?.context_size || chunk.context_size;
85
+ if (contextSizeData != null) {
86
+ const contextSize = contextSizeData;
87
+ logger.info("✅ Received context_size from backend", {
88
+ context_size: contextSize,
89
+ totalEstimated: contextSize.totalEstimated,
90
+ source: chunkMeta?.context_size ? "_meta" : "direct",
91
+ });
92
+ setLatestContextSize(contextSize);
93
+ }
94
+ else {
95
+ logger.debug("Chunk does not have context_size", {
96
+ hasContextSize: "context_size" in chunk,
97
+ hasMeta: "_meta" in chunk,
98
+ metaKeys: chunkMeta ? Object.keys(chunkMeta) : null,
88
99
  });
89
- setLatestContextInputTokens(tokens);
90
100
  }
91
101
  if (chunk.tokenUsage) {
92
102
  logger.debug("Received tokenUsage from backend", {
@@ -150,6 +160,7 @@ export function useChatMessages(client, startSession) {
150
160
  setIsStreaming,
151
161
  setStreamingStartTime,
152
162
  setError,
163
+ setLatestContextSize,
153
164
  ]);
154
165
  return {
155
166
  messages,
@@ -14,11 +14,24 @@ export function useChatSession(client, initialSessionId) {
14
14
  const clearMessages = useChatStore((state) => state.clearMessages);
15
15
  const resetTokens = useChatStore((state) => state.resetTokens);
16
16
  const addMessage = useChatStore((state) => state.addMessage);
17
+ const setLatestContextSize = useChatStore((state) => state.setLatestContextSize);
17
18
  // Subscribe to session updates to handle replayed messages
18
19
  useEffect(() => {
19
20
  if (!client)
20
21
  return;
21
22
  const unsubscribe = client.onSessionUpdate((update) => {
23
+ // Extract context size from update metadata if available
24
+ const updateMeta = update._meta;
25
+ const contextSizeData = updateMeta?.context_size || update.context_size;
26
+ if (contextSizeData != null) {
27
+ const contextSize = contextSizeData;
28
+ logger.info("✅ Received context_size from session update", {
29
+ context_size: contextSize,
30
+ totalEstimated: contextSize.totalEstimated,
31
+ isReplay: updateMeta?.isReplay,
32
+ });
33
+ setLatestContextSize(contextSize);
34
+ }
22
35
  // Handle replayed messages during session loading
23
36
  if (update.message) {
24
37
  logger.debug("Session update with message", {
@@ -64,7 +77,7 @@ export function useChatSession(client, initialSessionId) {
64
77
  }
65
78
  });
66
79
  return unsubscribe;
67
- }, [client, addMessage]);
80
+ }, [client, addMessage, setLatestContextSize]);
68
81
  /**
69
82
  * Connect to the agent (without creating a session)
70
83
  */
@@ -99,12 +112,15 @@ export function useChatSession(client, initialSessionId) {
99
112
  try {
100
113
  setConnectionStatus("connecting");
101
114
  setError(null);
115
+ // Clear existing UI state before loading
116
+ clearMessages();
117
+ resetTokens(); // Clears latestContextSize temporarily
102
118
  // Try to load session (this will also connect and replay messages)
103
119
  const id = await client.loadSession(sessionIdToLoad);
104
120
  setSessionId(id);
105
121
  setConnectionStatus("connected");
106
- // Don't clear messages - they will be populated by session replay
107
- resetTokens();
122
+ // Messages and context size will be restored from backend during replay
123
+ // Backend sends context_size after replay completes (see adapter.ts:369-401)
108
124
  logger.info("Session loaded successfully", { sessionId: id });
109
125
  }
110
126
  catch (error) {
@@ -50,6 +50,12 @@ export declare function useToolCalls(client: AcpClient | null): {
50
50
  outputTokens?: number | undefined;
51
51
  totalTokens?: number | undefined;
52
52
  } | undefined;
53
+ _meta?: {
54
+ truncationWarning?: string | undefined;
55
+ compactionAction?: "compacted" | "truncated" | undefined;
56
+ originalTokens?: number | undefined;
57
+ finalTokens?: number | undefined;
58
+ } | undefined;
53
59
  }[]>;
54
60
  getToolCallsForSession: (sessionId: string) => ToolCall[];
55
61
  };
@@ -74,6 +74,15 @@ export declare const DisplayMessage: z.ZodObject<{
74
74
  outputTokens: z.ZodOptional<z.ZodNumber>;
75
75
  totalTokens: z.ZodOptional<z.ZodNumber>;
76
76
  }, z.core.$strip>>;
77
+ _meta: z.ZodOptional<z.ZodObject<{
78
+ truncationWarning: z.ZodOptional<z.ZodString>;
79
+ compactionAction: z.ZodOptional<z.ZodEnum<{
80
+ compacted: "compacted";
81
+ truncated: "truncated";
82
+ }>>;
83
+ originalTokens: z.ZodOptional<z.ZodNumber>;
84
+ finalTokens: z.ZodOptional<z.ZodNumber>;
85
+ }, z.core.$strip>>;
77
86
  }, z.core.$strip>>>;
78
87
  tokenUsage: z.ZodOptional<z.ZodObject<{
79
88
  inputTokens: z.ZodOptional<z.ZodNumber>;
@@ -172,6 +181,15 @@ export declare const ChatSessionState: z.ZodObject<{
172
181
  outputTokens: z.ZodOptional<z.ZodNumber>;
173
182
  totalTokens: z.ZodOptional<z.ZodNumber>;
174
183
  }, z.core.$strip>>;
184
+ _meta: z.ZodOptional<z.ZodObject<{
185
+ truncationWarning: z.ZodOptional<z.ZodString>;
186
+ compactionAction: z.ZodOptional<z.ZodEnum<{
187
+ compacted: "compacted";
188
+ truncated: "truncated";
189
+ }>>;
190
+ originalTokens: z.ZodOptional<z.ZodNumber>;
191
+ finalTokens: z.ZodOptional<z.ZodNumber>;
192
+ }, z.core.$strip>>;
175
193
  }, z.core.$strip>>>;
176
194
  tokenUsage: z.ZodOptional<z.ZodObject<{
177
195
  inputTokens: z.ZodOptional<z.ZodNumber>;
@@ -125,6 +125,15 @@ export declare const ToolCallSchema: z.ZodObject<{
125
125
  outputTokens: z.ZodOptional<z.ZodNumber>;
126
126
  totalTokens: z.ZodOptional<z.ZodNumber>;
127
127
  }, z.core.$strip>>;
128
+ _meta: z.ZodOptional<z.ZodObject<{
129
+ truncationWarning: z.ZodOptional<z.ZodString>;
130
+ compactionAction: z.ZodOptional<z.ZodEnum<{
131
+ compacted: "compacted";
132
+ truncated: "truncated";
133
+ }>>;
134
+ originalTokens: z.ZodOptional<z.ZodNumber>;
135
+ finalTokens: z.ZodOptional<z.ZodNumber>;
136
+ }, z.core.$strip>>;
128
137
  }, z.core.$strip>;
129
138
  export type ToolCall = z.infer<typeof ToolCallSchema>;
130
139
  /**
@@ -101,6 +101,15 @@ export const ToolCallSchema = z.object({
101
101
  completedAt: z.number().optional(),
102
102
  /** Token usage metadata for this tool call */
103
103
  tokenUsage: TokenUsageSchema.optional(),
104
+ /** Internal metadata (e.g., truncation warnings) */
105
+ _meta: z
106
+ .object({
107
+ truncationWarning: z.string().optional(),
108
+ compactionAction: z.enum(["compacted", "truncated"]).optional(),
109
+ originalTokens: z.number().optional(),
110
+ finalTokens: z.number().optional(),
111
+ })
112
+ .optional(),
104
113
  });
105
114
  /**
106
115
  * Partial update for an existing tool call
@@ -1,6 +1,18 @@
1
1
  import { type LogEntry } from "@townco/core";
2
2
  import type { ConnectionStatus, DisplayMessage, InputState } from "../schemas/index.js";
3
3
  import type { ToolCall, ToolCallUpdate } from "../schemas/tool-call.js";
4
+ /**
5
+ * Context size breakdown from agent
6
+ */
7
+ export interface ContextSize {
8
+ systemPromptTokens: number;
9
+ userMessagesTokens: number;
10
+ assistantMessagesTokens: number;
11
+ toolInputTokens: number;
12
+ toolResultsTokens: number;
13
+ totalEstimated: number;
14
+ llmReportedInputTokens?: number;
15
+ }
4
16
  /**
5
17
  * Chat store state
6
18
  */
@@ -22,7 +34,7 @@ export interface ChatStore {
22
34
  outputTokens: number;
23
35
  totalTokens: number;
24
36
  };
25
- latestContextInputTokens: number | null;
37
+ latestContextSize: ContextSize | null;
26
38
  currentModel: string | null;
27
39
  tokenDisplayMode: "context" | "input" | "output";
28
40
  logs: LogEntry[];
@@ -50,7 +62,7 @@ export interface ChatStore {
50
62
  outputTokens?: number;
51
63
  totalTokens?: number;
52
64
  }) => void;
53
- setLatestContextInputTokens: (tokens: number | null) => void;
65
+ setLatestContextSize: (contextSize: ContextSize | null) => void;
54
66
  setCurrentModel: (model: string) => void;
55
67
  resetTokens: () => void;
56
68
  cycleTokenDisplayMode: () => void;
@@ -24,7 +24,7 @@ export const useChatStore = create((set) => ({
24
24
  outputTokens: 0,
25
25
  totalTokens: 0,
26
26
  },
27
- latestContextInputTokens: null,
27
+ latestContextSize: null,
28
28
  currentModel: "claude-sonnet-4-5-20250929", // Default model, TODO: get from server
29
29
  tokenDisplayMode: "context", // Default to showing context (both billed and current)
30
30
  logs: [],
@@ -279,7 +279,7 @@ export const useChatStore = create((set) => ({
279
279
  (state.currentContext.outputTokens + (tokenUsage.outputTokens ?? 0)),
280
280
  },
281
281
  })),
282
- setLatestContextInputTokens: (tokens) => set({ latestContextInputTokens: tokens }),
282
+ setLatestContextSize: (contextSize) => set({ latestContextSize: contextSize }),
283
283
  setCurrentModel: (model) => set({ currentModel: model }),
284
284
  resetTokens: () => set({
285
285
  totalBilled: {
@@ -292,7 +292,7 @@ export const useChatStore = create((set) => ({
292
292
  outputTokens: 0,
293
293
  totalTokens: 0,
294
294
  },
295
- latestContextInputTokens: null,
295
+ latestContextSize: null,
296
296
  }),
297
297
  cycleTokenDisplayMode: () => set((state) => {
298
298
  const modes = [
@@ -42,6 +42,11 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
42
42
  const error = useChatStore((state) => state.error);
43
43
  const currentModel = useChatStore((state) => state.currentModel);
44
44
  const [agentName, setAgentName] = useState("Agent");
45
+ const [agentDescription, setAgentDescription] = useState("This agent can help you with your tasks. Start a conversation by typing a message below.");
46
+ const [suggestedPrompts, setSuggestedPrompts] = useState([
47
+ "Search the web for the latest news on top tech company earnings, produce a summary for each company, and then a macro trend analysis of the tech industry. Use your todo list",
48
+ "What can you help me with?",
49
+ ]);
45
50
  const [isLargeScreen, setIsLargeScreen] = useState(typeof window !== "undefined" ? window.innerWidth >= 1024 : true);
46
51
  const [placeholder, setPlaceholder] = useState("Type a message or / for commands...");
47
52
  // Log connection status changes
@@ -51,15 +56,32 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
51
56
  logger.error("Connection error occurred", { error });
52
57
  }
53
58
  }, [connectionStatus, error]);
54
- // Get agent name from session metadata
59
+ // Get agent name from session metadata or client info
55
60
  useEffect(() => {
56
- if (client && sessionId) {
61
+ if (client) {
62
+ // Try to get from current session first
57
63
  const session = client.getCurrentSession();
58
64
  if (session?.metadata?.agentName) {
59
65
  setAgentName(session.metadata.agentName);
66
+ // We don't currently have description in session metadata, but we could add it
67
+ }
68
+ // Fallback to client info (if connected)
69
+ const agentInfo = client.getAgentInfo();
70
+ // Prefer displayName (human-readable) over name (programmatic)
71
+ if (agentInfo.displayName) {
72
+ setAgentName(agentInfo.displayName);
73
+ }
74
+ else if (agentInfo.name) {
75
+ setAgentName(agentInfo.name);
76
+ }
77
+ if (agentInfo.description) {
78
+ setAgentDescription(agentInfo.description);
79
+ }
80
+ if (agentInfo.suggestedPrompts && agentInfo.suggestedPrompts.length > 0) {
81
+ setSuggestedPrompts(agentInfo.suggestedPrompts);
60
82
  }
61
83
  }
62
- }, [client, sessionId]);
84
+ }, [client, sessionId, connectionStatus]);
63
85
  // Monitor screen size changes and update isLargeScreen state
64
86
  useEffect(() => {
65
87
  const mediaQuery = window.matchMedia("(min-width: 1024px)");
@@ -123,8 +145,8 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
123
145
  favicon: "https://www.google.com/s2/favicons?domain=theverge.com&sz=32",
124
146
  },
125
147
  ];
126
- // Get the latest context input tokens from the session context
127
- const latestContextInputTokens = useChatStore((state) => state.latestContextInputTokens);
148
+ // Get the latest context size from the session context
149
+ const latestContextSize = useChatStore((state) => state.latestContextSize);
128
150
  // Command menu items for chat input
129
151
  const commandMenuItems = [
130
152
  {
@@ -168,12 +190,7 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
168
190
  },
169
191
  },
170
192
  ];
171
- return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsxs(ChatLayout.Main, { children: [_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0 }), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-paragraph-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-paragraph-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-paragraph-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { children: messages.length === 0 ? (_jsx(OpenFilesButton, { children: ({ openFiles }) => (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: "This agent can help you with your tasks. Start a conversation by typing a message below.", suggestedPrompts: [
172
- "Search the web for the latest news on top tech company earnings, produce a summary for each company, and then a macro trend analysis of the tech industry. Use your todo list",
173
- "Explain how this works",
174
- "Create a new feature",
175
- "Review my changes",
176
- ], onPromptClick: (prompt) => {
193
+ return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsxs(ChatLayout.Main, { children: [_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0 }), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-paragraph-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-paragraph-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-paragraph-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { children: messages.length === 0 ? (_jsx(OpenFilesButton, { children: ({ openFiles }) => (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: agentDescription, suggestedPrompts: suggestedPrompts, onPromptClick: (prompt) => {
177
194
  sendMessage(prompt);
178
195
  setPlaceholder("Type a message or / for commands...");
179
196
  logger.info("Prompt clicked", { prompt });
@@ -196,5 +213,5 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
196
213
  previousMessage?.role === "assistant" ? "mt-2" : "mt-6";
197
214
  }
198
215
  return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
199
- }) })) }), _jsx(ChatLayout.Footer, { children: _jsxs(ChatInputRoot, { client: client, startSession: startSession, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), _jsx(ChatInputField, { placeholder: placeholder, autoFocus: true }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, {}), latestContextInputTokens != null && (_jsx(ContextUsageButton, { percentage: calculateTokenPercentage(latestContextInputTokens, currentModel ?? undefined), tokens: latestContextInputTokens, formattedPercentage: formatTokenPercentage(latestContextInputTokens, currentModel ?? undefined) }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })] })] })] }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsx(AsideTabs, {}) }))] }));
216
+ }) })) }), _jsx(ChatLayout.Footer, { children: _jsxs(ChatInputRoot, { client: client, startSession: startSession, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), _jsx(ChatInputField, { placeholder: placeholder, autoFocus: true }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, {}), latestContextSize != null && (_jsx(ContextUsageButton, { contextSize: latestContextSize, modelContextWindow: currentModel?.includes("claude") ? 200000 : 128000 }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })] })] })] }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsx(AsideTabs, {}) }))] }));
200
217
  }
@@ -1,7 +1,15 @@
1
1
  import * as React from "react";
2
+ export interface ContextSize {
3
+ systemPromptTokens: number;
4
+ userMessagesTokens: number;
5
+ assistantMessagesTokens: number;
6
+ toolInputTokens: number;
7
+ toolResultsTokens: number;
8
+ totalEstimated: number;
9
+ llmReportedInputTokens?: number;
10
+ }
2
11
  export interface ContextUsageButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
3
- percentage: number;
4
- tokens: number;
5
- formattedPercentage: string;
12
+ contextSize: ContextSize;
13
+ modelContextWindow: number;
6
14
  }
7
15
  export declare const ContextUsageButton: React.ForwardRefExoticComponent<ContextUsageButtonProps & React.RefAttributes<HTMLButtonElement>>;
@@ -3,9 +3,28 @@ import * as React from "react";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { Button } from "./Button.js";
5
5
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./Tooltip.js";
6
- export const ContextUsageButton = React.forwardRef(({ percentage, tokens, formattedPercentage, className, ...props }, ref) => {
7
- // Clamp percentage between 0 and 100
6
+ export const ContextUsageButton = React.forwardRef(({ contextSize, modelContextWindow, className, ...props }, ref) => {
7
+ // Use max of estimated and LLM-reported tokens (LLM reported as fallback if higher)
8
+ const actualTokens = Math.max(contextSize.totalEstimated, contextSize.llmReportedInputTokens ?? 0);
9
+ const percentage = (actualTokens / modelContextWindow) * 100;
10
+ const formattedPercentage = `${percentage.toFixed(1)}%`;
11
+ // Clamp percentage between 0 and 100 for display
8
12
  const clampedPercentage = Math.min(100, Math.max(0, percentage));
13
+ // Determine color based on percentage thresholds
14
+ const getColorClass = (pct) => {
15
+ if (pct < 50)
16
+ return "text-foreground"; // Normal text color
17
+ if (pct < 75)
18
+ return "text-yellow-500"; // Yellow
19
+ return "text-red-500"; // Red
20
+ };
21
+ const colorClass = getColorClass(percentage);
22
+ // Calculate percentage contribution for each element
23
+ const calculatePercentage = (tokens) => {
24
+ if (actualTokens === 0)
25
+ return "0.0%";
26
+ return `${((tokens / actualTokens) * 100).toFixed(1)}%`;
27
+ };
9
28
  // SVG parameters
10
29
  const size = 16;
11
30
  const strokeWidth = 2;
@@ -13,6 +32,6 @@ export const ContextUsageButton = React.forwardRef(({ percentage, tokens, format
13
32
  const center = size / 2;
14
33
  const circumference = 2 * Math.PI * radius;
15
34
  const offset = circumference - (clampedPercentage / 100) * circumference;
16
- return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full cursor-default", className), ...props, children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "transform -rotate-90", children: [_jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", className: "opacity-20" }), _jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", className: "transition-all duration-300 ease-in-out" })] }) }) }), _jsx(TooltipContent, { side: "top", align: "center", children: _jsxs("p", { children: ["Context: ", formattedPercentage, " (", tokens.toLocaleString(), " tokens)"] }) })] }) }));
35
+ return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full cursor-default", colorClass, className), ...props, children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "transform -rotate-90", children: [_jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", className: "opacity-20" }), _jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", className: "transition-all duration-300 ease-in-out" })] }) }) }), _jsx(TooltipContent, { side: "top", align: "center", className: "p-3", children: _jsxs("div", { className: "space-y-2 font-mono text-xs", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "System Prompt:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.systemPromptTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.systemPromptTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "User Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.userMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.userMessagesTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Assistant Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.assistantMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.assistantMessagesTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tool Inputs:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolInputTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolInputTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tool Results:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolResultsTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolResultsTokens), ")"] })] })] })] }), _jsx("div", { className: "border-t border-border pt-2", children: _jsxs("div", { className: cn("flex justify-end gap-2 font-semibold", colorClass), children: [_jsx("span", { children: actualTokens.toLocaleString() }), _jsxs("span", { children: ["(", formattedPercentage, ")"] })] }) })] }) })] }) }));
17
36
  });
18
37
  ContextUsageButton.displayName = "ContextUsageButton";
@@ -1,6 +1,11 @@
1
1
  import { type VariantProps } from "class-variance-authority";
2
2
  import * as React from "react";
3
3
  import type { DisplayMessage } from "./MessageList.js";
4
+ /**
5
+ * MessageContent component inspired by shadcn.io/ai
6
+ * Provides the content container with role-based styling
7
+ * Handles automatic rendering of thinking, waiting states, and content
8
+ */
4
9
  declare const messageContentVariants: (props?: ({
5
10
  role?: "user" | "assistant" | "system" | null | undefined;
6
11
  variant?: "default" | "outline" | "ghost" | null | undefined;
@@ -1,9 +1,7 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cva } from "class-variance-authority";
3
- import { Loader2Icon } from "lucide-react";
4
3
  import * as React from "react";
5
4
  import { useChatStore } from "../../core/store/chat-store.js";
6
- import { formatTokenPercentage } from "../../core/utils/model-context.js";
7
5
  import { cn } from "../lib/utils.js";
8
6
  import { Reasoning } from "./Reasoning.js";
9
7
  import { Response } from "./Response.js";
@@ -13,57 +11,6 @@ import { ToolCall } from "./ToolCall.js";
13
11
  * Provides the content container with role-based styling
14
12
  * Handles automatic rendering of thinking, waiting states, and content
15
13
  */
16
- // Synonyms of "thinking" in multiple languages
17
- const THINKING_WORDS = [
18
- "Thinking",
19
- "Pensando",
20
- "Pensant",
21
- "Denkend",
22
- "Pensando",
23
- "考えている",
24
- "생각 중",
25
- "思考中",
26
- "Размышляя",
27
- "Düşünüyor",
28
- "Myślący",
29
- "Tänkande",
30
- "Pensando",
31
- "Ajatellen",
32
- "Σκεπτόμενος",
33
- "חושב",
34
- "सोच रहा है",
35
- "Berpikir",
36
- ];
37
- const DOT_ANIMATIONS = ["...", "·..", ".·.", "..·", ".·.", "·.."];
38
- function WaitingElapsedTime({ startTime }) {
39
- const [elapsed, setElapsed] = React.useState(0);
40
- const [thinkingWord, setThinkingWord] = React.useState(() => THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)]);
41
- const [dotIndex, setDotIndex] = React.useState(0);
42
- React.useEffect(() => {
43
- const interval = setInterval(() => {
44
- const now = Date.now();
45
- const elapsedMs = now - startTime;
46
- setElapsed(elapsedMs);
47
- }, 100);
48
- return () => clearInterval(interval);
49
- }, [startTime]);
50
- React.useEffect(() => {
51
- const wordInterval = setInterval(() => {
52
- const randomIndex = Math.floor(Math.random() * THINKING_WORDS.length);
53
- setThinkingWord(THINKING_WORDS[randomIndex]);
54
- }, 1500);
55
- return () => clearInterval(wordInterval);
56
- }, []);
57
- React.useEffect(() => {
58
- const dotInterval = setInterval(() => {
59
- setDotIndex((prev) => (prev + 1) % DOT_ANIMATIONS.length);
60
- }, 100);
61
- return () => clearInterval(dotInterval);
62
- }, []);
63
- const seconds = (elapsed / 1000).toFixed(1);
64
- const animatedDots = DOT_ANIMATIONS[dotIndex];
65
- return (_jsxs("span", { className: "text-muted-foreground text-paragraph-sm", children: [thinkingWord, animatedDots, " ", seconds, "s"] }));
66
- }
67
14
  const messageContentVariants = cva("w-full rounded-2xl text-[var(--font-size)] font-[var(--font-family)] leading-relaxed break-words transition-colors", {
68
15
  variants: {
69
16
  role: {
@@ -100,7 +47,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
100
47
  const hasThinking = !!thinking;
101
48
  // Check if waiting (streaming but no content yet)
102
49
  const isWaiting = message.isStreaming && !message.content && message.role === "assistant";
103
- content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true })), isWaiting && streamingStartTime && (_jsxs("div", { className: "flex items-center gap-2 opacity-50", children: [_jsx(Loader2Icon, { className: "size-4 animate-spin text-muted-foreground" }), _jsx(WaitingElapsedTime, { startTime: streamingStartTime })] })), message.role === "assistant" ? ((() => {
50
+ content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true })), isWaiting && streamingStartTime && (_jsx("div", { className: "flex items-center gap-2", children: _jsx("div", { className: "size-2.5 rounded-full bg-foreground animate-pulse-scale" }) })), message.role === "assistant" ? ((() => {
104
51
  // Sort tool calls by content position
105
52
  const sortedToolCalls = (message.toolCalls || [])
106
53
  .slice()
@@ -137,5 +137,5 @@ export function ToolCall({ toolCall }) {
137
137
  return (_jsxs("div", { className: "bg-neutral-900 text-neutral-100 p-2 rounded text-[11px] font-mono", children: ["Terminal: ", block.terminalId] }, getBlockKey()));
138
138
  }
139
139
  return null;
140
- }), toolCall.error && (_jsxs("div", { className: "text-destructive font-mono text-[11px] mt-2", children: ["Error: ", toolCall.error] }))] })] })) : null, (toolCall.tokenUsage || toolCall.startedAt) && (_jsxs("div", { className: "p-2 bg-muted/50 border-t border-border flex flex-wrap gap-4 text-[10px] text-muted-foreground font-sans", children: [toolCall.tokenUsage && (_jsxs("div", { className: "flex gap-3", children: [toolCall.tokenUsage.inputTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Input:" }), toolCall.tokenUsage.inputTokens.toLocaleString()] })), toolCall.tokenUsage.outputTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Output:" }), toolCall.tokenUsage.outputTokens.toLocaleString()] })), toolCall.tokenUsage.totalTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Total:" }), toolCall.tokenUsage.totalTokens.toLocaleString()] }))] })), toolCall.startedAt && (_jsxs("div", { className: "flex gap-3 ml-auto", children: [_jsxs("span", { children: ["Started: ", new Date(toolCall.startedAt).toLocaleTimeString()] }), toolCall.completedAt && (_jsxs("span", { children: ["Completed:", " ", new Date(toolCall.completedAt).toLocaleTimeString(), " (", Math.round((toolCall.completedAt - toolCall.startedAt) / 1000), "s)"] }))] }))] }))] }))] }));
140
+ }), toolCall.error && (_jsxs("div", { className: "text-destructive font-mono text-[11px] mt-2", children: ["Error: ", toolCall.error] }))] })] })) : null, toolCall._meta?.truncationWarning && (_jsxs("div", { className: "mx-3 mt-3 mb-0 flex items-center gap-2 rounded-md bg-yellow-50 dark:bg-yellow-950/20 px-3 py-2 text-[11px] text-yellow-800 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-900", children: [_jsx("span", { className: "text-yellow-600 dark:text-yellow-500", children: "\u26A0\uFE0F" }), _jsx("span", { children: toolCall._meta.truncationWarning })] })), (toolCall.tokenUsage || toolCall.startedAt) && (_jsxs("div", { className: "p-2 bg-muted/50 border-t border-border flex flex-wrap gap-4 text-[10px] text-muted-foreground font-sans", children: [toolCall.tokenUsage && (_jsxs("div", { className: "flex gap-3", children: [toolCall.tokenUsage.inputTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Input:" }), toolCall.tokenUsage.inputTokens.toLocaleString()] })), toolCall.tokenUsage.outputTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Output:" }), toolCall.tokenUsage.outputTokens.toLocaleString()] })), toolCall.tokenUsage.totalTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Total:" }), toolCall.tokenUsage.totalTokens.toLocaleString()] }))] })), toolCall.startedAt && (_jsxs("div", { className: "flex gap-3 ml-auto", children: [_jsxs("span", { children: ["Started: ", new Date(toolCall.startedAt).toLocaleTimeString()] }), toolCall.completedAt && (_jsxs("span", { children: ["Completed:", " ", new Date(toolCall.completedAt).toLocaleTimeString(), " (", Math.round((toolCall.completedAt - toolCall.startedAt) / 1000), "s)"] }))] }))] }))] }))] }));
141
141
  }
@@ -76,6 +76,18 @@ export declare class AcpClient {
76
76
  * Subscribe to errors
77
77
  */
78
78
  onError(handler: (error: Error) => void): () => void;
79
+ /**
80
+ * Get agent information
81
+ * - displayName: Human-readable name for UI (preferred)
82
+ * - name: Programmatic name (fallback if displayName not set)
83
+ */
84
+ getAgentInfo(): {
85
+ name?: string;
86
+ displayName?: string;
87
+ version?: string;
88
+ description?: string;
89
+ suggestedPrompts?: string[];
90
+ };
79
91
  /**
80
92
  * Create transport based on explicit configuration
81
93
  */
@@ -57,6 +57,7 @@ export class AcpClient {
57
57
  if (transportSessionId) {
58
58
  // Use the session ID from the transport
59
59
  const now = new Date().toISOString();
60
+ const agentName = this.transport.getAgentInfo?.()?.name;
60
61
  const session = {
61
62
  id: transportSessionId,
62
63
  status: "connected",
@@ -66,6 +67,7 @@ export class AcpClient {
66
67
  messages: [],
67
68
  metadata: {
68
69
  startedAt: now,
70
+ agentName,
69
71
  },
70
72
  };
71
73
  this.sessions.set(transportSessionId, session);
@@ -75,6 +77,7 @@ export class AcpClient {
75
77
  // Fallback: generate session ID (for transports that don't auto-create sessions)
76
78
  const sessionId = this.generateSessionId();
77
79
  const now = new Date().toISOString();
80
+ const agentName = this.transport.getAgentInfo?.()?.name;
78
81
  const session = {
79
82
  id: sessionId,
80
83
  status: "connecting",
@@ -84,6 +87,7 @@ export class AcpClient {
84
87
  messages: [],
85
88
  metadata: {
86
89
  startedAt: now,
90
+ agentName,
87
91
  },
88
92
  };
89
93
  this.sessions.set(sessionId, session);
@@ -116,6 +120,13 @@ export class AcpClient {
116
120
  this.currentSessionId = sessionId;
117
121
  // Load session from transport (will replay messages via session updates)
118
122
  await this.transport.loadSession(sessionId);
123
+ // Update metadata with agent name if available
124
+ if (this.transport.getAgentInfo) {
125
+ const agentInfo = this.transport.getAgentInfo();
126
+ if (agentInfo.name && session.metadata) {
127
+ session.metadata.agentName = agentInfo.name;
128
+ }
129
+ }
119
130
  // Update session status
120
131
  this.updateSessionStatus(sessionId, "connected");
121
132
  return sessionId;
@@ -200,6 +211,14 @@ export class AcpClient {
200
211
  this.errorHandlers.delete(handler);
201
212
  };
202
213
  }
214
+ /**
215
+ * Get agent information
216
+ * - displayName: Human-readable name for UI (preferred)
217
+ * - name: Programmatic name (fallback if displayName not set)
218
+ */
219
+ getAgentInfo() {
220
+ return this.transport.getAgentInfo?.() || {};
221
+ }
203
222
  /**
204
223
  * Create transport based on explicit configuration
205
224
  */
@@ -13,9 +13,9 @@ export type MessageRole = z.infer<typeof MessageRole>;
13
13
  * Content type for messages
14
14
  */
15
15
  export declare const ContentType: z.ZodEnum<{
16
- file: "file";
17
16
  text: "text";
18
17
  image: "image";
18
+ file: "file";
19
19
  tool_call: "tool_call";
20
20
  tool_result: "tool_result";
21
21
  }>;
@@ -25,9 +25,9 @@ export type ContentType = z.infer<typeof ContentType>;
25
25
  */
26
26
  export declare const BaseContent: z.ZodObject<{
27
27
  type: z.ZodEnum<{
28
- file: "file";
29
28
  text: "text";
30
29
  image: "image";
30
+ file: "file";
31
31
  tool_call: "tool_call";
32
32
  tool_result: "tool_result";
33
33
  }>;
@@ -197,5 +197,16 @@ export declare const MessageChunk: z.ZodObject<{
197
197
  totalTokens: z.ZodOptional<z.ZodNumber>;
198
198
  }, z.core.$strip>>;
199
199
  contextInputTokens: z.ZodOptional<z.ZodNumber>;
200
+ _meta: z.ZodOptional<z.ZodObject<{
201
+ context_size: z.ZodOptional<z.ZodObject<{
202
+ systemPromptTokens: z.ZodNumber;
203
+ userMessagesTokens: z.ZodNumber;
204
+ assistantMessagesTokens: z.ZodNumber;
205
+ toolInputTokens: z.ZodNumber;
206
+ toolResultsTokens: z.ZodNumber;
207
+ totalEstimated: z.ZodNumber;
208
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
209
+ }, z.core.$strip>>;
210
+ }, z.core.$strip>>;
200
211
  }, z.core.$strip>;
201
212
  export type MessageChunk = z.infer<typeof MessageChunk>;
@@ -100,4 +100,19 @@ export const MessageChunk = z.object({
100
100
  })
101
101
  .optional(),
102
102
  contextInputTokens: z.number().optional(),
103
+ _meta: z
104
+ .object({
105
+ context_size: z
106
+ .object({
107
+ systemPromptTokens: z.number(),
108
+ userMessagesTokens: z.number(),
109
+ assistantMessagesTokens: z.number(),
110
+ toolInputTokens: z.number(),
111
+ toolResultsTokens: z.number(),
112
+ totalEstimated: z.number(),
113
+ llmReportedInputTokens: z.number().optional(),
114
+ })
115
+ .optional(),
116
+ })
117
+ .optional(),
103
118
  });
@@ -3,12 +3,12 @@ import { z } from "zod";
3
3
  * Session status
4
4
  */
5
5
  export declare const SessionStatus: z.ZodEnum<{
6
- error: "error";
7
6
  idle: "idle";
8
7
  connecting: "connecting";
9
8
  connected: "connected";
10
9
  active: "active";
11
10
  streaming: "streaming";
11
+ error: "error";
12
12
  disconnected: "disconnected";
13
13
  }>;
14
14
  export type SessionStatus = z.infer<typeof SessionStatus>;
@@ -40,12 +40,12 @@ export type SessionMetadata = z.infer<typeof SessionMetadata>;
40
40
  export declare const Session: z.ZodObject<{
41
41
  id: z.ZodString;
42
42
  status: z.ZodEnum<{
43
- error: "error";
44
43
  idle: "idle";
45
44
  connecting: "connecting";
46
45
  connected: "connected";
47
46
  active: "active";
48
47
  streaming: "streaming";
48
+ error: "error";
49
49
  disconnected: "disconnected";
50
50
  }>;
51
51
  config: z.ZodObject<{
@@ -108,12 +108,12 @@ export type Session = z.infer<typeof Session>;
108
108
  export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
109
109
  sessionId: z.ZodString;
110
110
  status: z.ZodOptional<z.ZodEnum<{
111
- error: "error";
112
111
  idle: "idle";
113
112
  connecting: "connecting";
114
113
  connected: "connected";
115
114
  active: "active";
116
115
  streaming: "streaming";
116
+ error: "error";
117
117
  disconnected: "disconnected";
118
118
  }>>;
119
119
  message: z.ZodOptional<z.ZodObject<{
@@ -154,6 +154,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
154
154
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
155
155
  }, z.core.$strip>>;
156
156
  error: z.ZodOptional<z.ZodString>;
157
+ _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
157
158
  type: z.ZodLiteral<"tool_call">;
158
159
  toolCall: z.ZodObject<{
159
160
  id: z.ZodString;
@@ -212,17 +213,26 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
212
213
  outputTokens: z.ZodOptional<z.ZodNumber>;
213
214
  totalTokens: z.ZodOptional<z.ZodNumber>;
214
215
  }, z.core.$strip>>;
216
+ _meta: z.ZodOptional<z.ZodObject<{
217
+ truncationWarning: z.ZodOptional<z.ZodString>;
218
+ compactionAction: z.ZodOptional<z.ZodEnum<{
219
+ compacted: "compacted";
220
+ truncated: "truncated";
221
+ }>>;
222
+ originalTokens: z.ZodOptional<z.ZodNumber>;
223
+ finalTokens: z.ZodOptional<z.ZodNumber>;
224
+ }, z.core.$strip>>;
215
225
  }, z.core.$strip>;
216
226
  messageId: z.ZodOptional<z.ZodString>;
217
227
  }, z.core.$strip>, z.ZodObject<{
218
228
  sessionId: z.ZodString;
219
229
  status: z.ZodOptional<z.ZodEnum<{
220
- error: "error";
221
230
  idle: "idle";
222
231
  connecting: "connecting";
223
232
  connected: "connected";
224
233
  active: "active";
225
234
  streaming: "streaming";
235
+ error: "error";
226
236
  disconnected: "disconnected";
227
237
  }>>;
228
238
  message: z.ZodOptional<z.ZodObject<{
@@ -263,6 +273,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
263
273
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
264
274
  }, z.core.$strip>>;
265
275
  error: z.ZodOptional<z.ZodString>;
276
+ _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
266
277
  type: z.ZodLiteral<"tool_call_update">;
267
278
  toolCallUpdate: z.ZodObject<{
268
279
  id: z.ZodString;
@@ -308,12 +319,12 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
308
319
  }, z.core.$strip>, z.ZodObject<{
309
320
  sessionId: z.ZodString;
310
321
  status: z.ZodOptional<z.ZodEnum<{
311
- error: "error";
312
322
  idle: "idle";
313
323
  connecting: "connecting";
314
324
  connected: "connected";
315
325
  active: "active";
316
326
  streaming: "streaming";
327
+ error: "error";
317
328
  disconnected: "disconnected";
318
329
  }>>;
319
330
  message: z.ZodOptional<z.ZodObject<{
@@ -354,6 +365,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
354
365
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
355
366
  }, z.core.$strip>>;
356
367
  error: z.ZodOptional<z.ZodString>;
368
+ _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
357
369
  type: z.ZodLiteral<"tool_output">;
358
370
  toolOutput: z.ZodObject<{
359
371
  id: z.ZodString;
@@ -364,12 +376,12 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
364
376
  }, z.core.$strip>, z.ZodObject<{
365
377
  sessionId: z.ZodString;
366
378
  status: z.ZodOptional<z.ZodEnum<{
367
- error: "error";
368
379
  idle: "idle";
369
380
  connecting: "connecting";
370
381
  connected: "connected";
371
382
  active: "active";
372
383
  streaming: "streaming";
384
+ error: "error";
373
385
  disconnected: "disconnected";
374
386
  }>>;
375
387
  message: z.ZodOptional<z.ZodObject<{
@@ -410,6 +422,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
410
422
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
411
423
  }, z.core.$strip>>;
412
424
  error: z.ZodOptional<z.ZodString>;
425
+ _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
413
426
  type: z.ZodOptional<z.ZodLiteral<"generic">>;
414
427
  }, z.core.$strip>]>;
415
428
  export type SessionUpdate = z.infer<typeof SessionUpdate>;
@@ -52,6 +52,7 @@ const BaseSessionUpdate = z.object({
52
52
  status: SessionStatus.optional(),
53
53
  message: Message.optional(),
54
54
  error: z.string().optional(),
55
+ _meta: z.record(z.string(), z.unknown()).optional(),
55
56
  });
56
57
  /**
57
58
  * Session update with tool call (sessionUpdate: "tool_call")
@@ -21,6 +21,7 @@ export declare class HttpTransport implements Transport {
21
21
  private options;
22
22
  private isReceivingMessages;
23
23
  private isInReplayMode;
24
+ private agentInfo?;
24
25
  constructor(options: HttpTransportOptions);
25
26
  connect(): Promise<void>;
26
27
  /**
@@ -34,6 +35,13 @@ export declare class HttpTransport implements Transport {
34
35
  isConnected(): boolean;
35
36
  onSessionUpdate(callback: (update: SessionUpdate) => void): () => void;
36
37
  onError(callback: (error: Error) => void): () => void;
38
+ getAgentInfo(): {
39
+ name?: string;
40
+ displayName?: string;
41
+ version?: string;
42
+ description?: string;
43
+ suggestedPrompts?: string[];
44
+ };
37
45
  /**
38
46
  * Send an ACP RPC request to the server
39
47
  */
@@ -22,6 +22,7 @@ export class HttpTransport {
22
22
  options;
23
23
  isReceivingMessages = false;
24
24
  isInReplayMode = false; // True during session replay, ignores non-replay streaming
25
+ agentInfo;
25
26
  constructor(options) {
26
27
  // Ensure baseUrl doesn't end with a slash
27
28
  this.options = { ...options, baseUrl: options.baseUrl.replace(/\/$/, "") };
@@ -43,6 +44,30 @@ export class HttpTransport {
43
44
  },
44
45
  };
45
46
  const initResponse = await this.sendRpcRequest("initialize", initRequest);
47
+ if (initResponse.agentInfo) {
48
+ // Read description and suggestedPrompts from _meta extension point (ACP protocol extension)
49
+ const description = initResponse._meta &&
50
+ typeof initResponse._meta === "object" &&
51
+ "agentDescription" in initResponse._meta
52
+ ? String(initResponse._meta.agentDescription)
53
+ : undefined;
54
+ const suggestedPrompts = initResponse._meta &&
55
+ typeof initResponse._meta === "object" &&
56
+ "suggestedPrompts" in initResponse._meta &&
57
+ Array.isArray(initResponse._meta.suggestedPrompts)
58
+ ? initResponse._meta.suggestedPrompts
59
+ : undefined;
60
+ this.agentInfo = {
61
+ name: initResponse.agentInfo.name,
62
+ // title is the ACP field for human-readable display name
63
+ ...(initResponse.agentInfo.title
64
+ ? { displayName: initResponse.agentInfo.title }
65
+ : {}),
66
+ version: initResponse.agentInfo.version,
67
+ ...(description ? { description } : {}),
68
+ ...(suggestedPrompts ? { suggestedPrompts } : {}),
69
+ };
70
+ }
46
71
  logger.info("ACP connection initialized", { initResponse });
47
72
  // Step 2: Create a new session
48
73
  const sessionRequest = {
@@ -89,6 +114,30 @@ export class HttpTransport {
89
114
  };
90
115
  logger.info("Loading session - initializing connection", { sessionId });
91
116
  const initResponse = await this.sendRpcRequest("initialize", initRequest);
117
+ if (initResponse.agentInfo) {
118
+ // Read description and suggestedPrompts from _meta extension point (ACP protocol extension)
119
+ const description = initResponse._meta &&
120
+ typeof initResponse._meta === "object" &&
121
+ "agentDescription" in initResponse._meta
122
+ ? String(initResponse._meta.agentDescription)
123
+ : undefined;
124
+ const suggestedPrompts = initResponse._meta &&
125
+ typeof initResponse._meta === "object" &&
126
+ "suggestedPrompts" in initResponse._meta &&
127
+ Array.isArray(initResponse._meta.suggestedPrompts)
128
+ ? initResponse._meta.suggestedPrompts
129
+ : undefined;
130
+ this.agentInfo = {
131
+ name: initResponse.agentInfo.name,
132
+ // title is the ACP field for human-readable display name
133
+ ...(initResponse.agentInfo.title
134
+ ? { displayName: initResponse.agentInfo.title }
135
+ : {}),
136
+ version: initResponse.agentInfo.version,
137
+ ...(description ? { description } : {}),
138
+ ...(suggestedPrompts ? { suggestedPrompts } : {}),
139
+ };
140
+ }
92
141
  // Check if loadSession is supported
93
142
  if (!initResponse.agentCapabilities?.loadSession) {
94
143
  logger.error("Agent does not support loading sessions", {
@@ -287,6 +336,9 @@ export class HttpTransport {
287
336
  this.errorCallbacks.delete(callback);
288
337
  };
289
338
  }
339
+ getAgentInfo() {
340
+ return this.agentInfo || {};
341
+ }
290
342
  /**
291
343
  * Send an ACP RPC request to the server
292
344
  */
@@ -759,12 +811,6 @@ export class HttpTransport {
759
811
  if (this.isInReplayMode && !isReplay) {
760
812
  return;
761
813
  }
762
- // Handle agent message chunks
763
- const sessionUpdate = {
764
- type: "generic",
765
- sessionId,
766
- status: "active",
767
- };
768
814
  // Extract token usage from _meta if present
769
815
  const tokenUsage = update._meta &&
770
816
  typeof update._meta === "object" &&
@@ -778,6 +824,19 @@ export class HttpTransport {
778
824
  typeof update._meta.contextInputTokens === "number"
779
825
  ? update._meta.contextInputTokens
780
826
  : undefined;
827
+ // Extract context_size from _meta if present
828
+ const context_size = update._meta &&
829
+ typeof update._meta === "object" &&
830
+ "context_size" in update._meta
831
+ ? update._meta.context_size
832
+ : undefined;
833
+ // Handle agent message chunks
834
+ const sessionUpdate = {
835
+ type: "generic",
836
+ sessionId,
837
+ status: "active",
838
+ _meta: update._meta,
839
+ };
781
840
  // Queue message chunks if present (but skip during replay)
782
841
  // For agent_message_chunk, content is an object, not an array
783
842
  const content = update.content;
@@ -791,6 +850,7 @@ export class HttpTransport {
791
850
  contentDelta: { type: "text", text: contentObj.text },
792
851
  tokenUsage,
793
852
  contextInputTokens,
853
+ _meta: context_size ? { context_size } : undefined,
794
854
  isComplete: false,
795
855
  };
796
856
  }
@@ -832,8 +892,11 @@ export class HttpTransport {
832
892
  this.notifySessionUpdate(messageSessionUpdate);
833
893
  }
834
894
  }
835
- // Only send generic session update during live streaming (not replay)
836
- if (!isReplay) {
895
+ // Send session update for:
896
+ // 1. Live streaming (not replay)
897
+ // 2. Replay messages with context_size (need to restore context on session load)
898
+ const hasContextSize = sessionUpdate._meta && "context_size" in sessionUpdate._meta;
899
+ if (!isReplay || hasContextSize) {
837
900
  this.notifySessionUpdate(sessionUpdate);
838
901
  }
839
902
  }
@@ -15,6 +15,7 @@ export declare class StdioTransport implements Transport {
15
15
  private currentSessionId;
16
16
  private chunkResolvers;
17
17
  private streamComplete;
18
+ private agentInfo?;
18
19
  private originalConsole;
19
20
  constructor(options: StdioTransportOptions);
20
21
  connect(): Promise<void>;
@@ -24,6 +25,13 @@ export declare class StdioTransport implements Transport {
24
25
  isConnected(): boolean;
25
26
  onSessionUpdate(callback: (update: SessionUpdate) => void): () => void;
26
27
  onError(callback: (error: Error) => void): () => void;
28
+ getAgentInfo(): {
29
+ name?: string;
30
+ displayName?: string;
31
+ version?: string;
32
+ description?: string;
33
+ suggestedPrompts?: string[];
34
+ };
27
35
  private notifySessionUpdate;
28
36
  private notifyError;
29
37
  }
@@ -18,6 +18,7 @@ export class StdioTransport {
18
18
  currentSessionId = null;
19
19
  chunkResolvers = [];
20
20
  streamComplete = false;
21
+ agentInfo;
21
22
  originalConsole = null;
22
23
  constructor(options) {
23
24
  this.options = options;
@@ -353,6 +354,30 @@ export class StdioTransport {
353
354
  },
354
355
  },
355
356
  });
357
+ if (initResponse.agentInfo) {
358
+ // Read description and suggestedPrompts from _meta extension point (ACP protocol extension)
359
+ const description = initResponse._meta &&
360
+ typeof initResponse._meta === "object" &&
361
+ "agentDescription" in initResponse._meta
362
+ ? String(initResponse._meta.agentDescription)
363
+ : undefined;
364
+ const suggestedPrompts = initResponse._meta &&
365
+ typeof initResponse._meta === "object" &&
366
+ "suggestedPrompts" in initResponse._meta &&
367
+ Array.isArray(initResponse._meta.suggestedPrompts)
368
+ ? initResponse._meta.suggestedPrompts
369
+ : undefined;
370
+ this.agentInfo = {
371
+ name: initResponse.agentInfo.name,
372
+ // title is the ACP field for human-readable display name
373
+ ...(initResponse.agentInfo.title
374
+ ? { displayName: initResponse.agentInfo.title }
375
+ : {}),
376
+ version: initResponse.agentInfo.version,
377
+ ...(description ? { description } : {}),
378
+ ...(suggestedPrompts ? { suggestedPrompts } : {}),
379
+ };
380
+ }
356
381
  logger.info("ACP connection initialized", { initResponse });
357
382
  this.connected = true;
358
383
  }
@@ -511,6 +536,9 @@ export class StdioTransport {
511
536
  this.errorCallbacks.delete(callback);
512
537
  };
513
538
  }
539
+ getAgentInfo() {
540
+ return this.agentInfo || {};
541
+ }
514
542
  notifySessionUpdate(update) {
515
543
  for (const callback of this.sessionUpdateCallbacks) {
516
544
  try {
@@ -36,6 +36,18 @@ export interface Transport {
36
36
  * Subscribe to errors
37
37
  */
38
38
  onError(callback: (error: Error) => void): () => void;
39
+ /**
40
+ * Get agent information
41
+ * - displayName: Human-readable name for UI (from ACP title field)
42
+ * - name: Programmatic name (fallback if displayName not set)
43
+ */
44
+ getAgentInfo?(): {
45
+ name?: string;
46
+ displayName?: string;
47
+ version?: string;
48
+ description?: string;
49
+ suggestedPrompts?: string[];
50
+ };
39
51
  }
40
52
  /**
41
53
  * Stdio transport options
@@ -37,5 +37,5 @@ export function ToolCall({ toolCall }) {
37
37
  ? ((toolCall.tokenUsage.inputTokens / 1_000_000) * 3 +
38
38
  (toolCall.tokenUsage.outputTokens / 1_000_000) * 15).toFixed(4)
39
39
  : null;
40
- return (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: statusColors[toolCall.status], children: statusIndicators[toolCall.status] }), _jsx(Text, { color: "cyan", bold: true, children: "[TOOL]" }), _jsx(Text, { children: toolCall.title }), startTime && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| ", startTime] })), duration && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| ", duration, "s"] })), cost && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| $", cost] }))] }));
40
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: statusColors[toolCall.status], children: statusIndicators[toolCall.status] }), _jsx(Text, { color: "cyan", bold: true, children: "[TOOL]" }), _jsx(Text, { children: toolCall.title }), startTime && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| ", startTime] })), duration && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| ", duration, "s"] })), cost && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| $", cost] }))] }), toolCall._meta?.truncationWarning && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", bold: true, children: ["\u26A0\uFE0F ", toolCall._meta.truncationWarning] }) }))] }));
41
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@agentclientprotocol/sdk": "^0.5.1",
43
- "@townco/core": "0.0.23",
43
+ "@townco/core": "0.0.24",
44
44
  "@radix-ui/react-dialog": "^1.1.15",
45
45
  "@radix-ui/react-dropdown-menu": "^2.1.16",
46
46
  "@radix-ui/react-label": "^2.1.8",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@tailwindcss/postcss": "^4.1.17",
66
- "@townco/tsconfig": "0.1.42",
66
+ "@townco/tsconfig": "0.1.43",
67
67
  "@types/node": "^24.10.0",
68
68
  "@types/react": "^19.2.2",
69
69
  "ink": "^6.4.0",
@@ -378,3 +378,18 @@
378
378
  transform: translateX(0);
379
379
  }
380
380
  }
381
+
382
+ @keyframes pulse-scale {
383
+ 0%, 100% {
384
+ transform: scale(1);
385
+ background-color: var(--color-neutral-500);
386
+ }
387
+ 50% {
388
+ transform: scale(.9);
389
+ background-color: var(--color-neutral-500);
390
+ }
391
+ }
392
+
393
+ .animate-pulse-scale {
394
+ animation: pulse-scale 1s ease-in-out infinite;
395
+ }