@townco/ui 0.1.44 → 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.
Files changed (34) hide show
  1. package/dist/core/hooks/use-chat-messages.d.ts +6 -0
  2. package/dist/core/hooks/use-chat-messages.js +22 -1
  3. package/dist/core/hooks/use-chat-session.js +19 -3
  4. package/dist/core/hooks/use-tool-calls.d.ts +6 -0
  5. package/dist/core/schemas/chat.d.ts +18 -0
  6. package/dist/core/schemas/tool-call.d.ts +9 -0
  7. package/dist/core/schemas/tool-call.js +9 -0
  8. package/dist/core/store/chat-store.d.ts +14 -0
  9. package/dist/core/store/chat-store.js +3 -0
  10. package/dist/gui/components/Button.d.ts +1 -1
  11. package/dist/gui/components/ChatView.js +29 -15
  12. package/dist/gui/components/ContextUsageButton.d.ts +11 -3
  13. package/dist/gui/components/ContextUsageButton.js +22 -3
  14. package/dist/gui/components/MessageContent.d.ts +5 -0
  15. package/dist/gui/components/MessageContent.js +2 -55
  16. package/dist/gui/components/ToolCall.js +1 -1
  17. package/dist/sdk/client/acp-client.d.ts +12 -0
  18. package/dist/sdk/client/acp-client.js +19 -0
  19. package/dist/sdk/schemas/message.d.ts +14 -2
  20. package/dist/sdk/schemas/message.js +16 -0
  21. package/dist/sdk/schemas/session.d.ts +19 -6
  22. package/dist/sdk/schemas/session.js +1 -0
  23. package/dist/sdk/transports/http.d.ts +8 -0
  24. package/dist/sdk/transports/http.js +79 -8
  25. package/dist/sdk/transports/stdio.d.ts +8 -0
  26. package/dist/sdk/transports/stdio.js +28 -0
  27. package/dist/sdk/transports/types.d.ts +12 -0
  28. package/dist/tui/components/ToolCall.js +1 -1
  29. package/package.json +3 -3
  30. package/src/styles/global.css +15 -0
  31. package/dist/core/lib/logger.d.ts +0 -59
  32. package/dist/core/lib/logger.js +0 -191
  33. package/dist/tui/components/LogsPanel.d.ts +0 -5
  34. package/dist/tui/components/LogsPanel.js +0 -29
@@ -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,6 +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 setLatestContextSize = useChatStore((state) => state.setLatestContextSize);
17
18
  /**
18
19
  * Send a message to the agent
19
20
  */
@@ -78,8 +79,27 @@ export function useChatMessages(client, startSession) {
78
79
  let accumulatedContent = "";
79
80
  let streamCompleted = false;
80
81
  for await (const chunk of messageStream) {
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,
99
+ });
100
+ }
81
101
  if (chunk.tokenUsage) {
82
- logger.debug("chunk.tokenUsage", {
102
+ logger.debug("Received tokenUsage from backend", {
83
103
  tokenUsage: chunk.tokenUsage,
84
104
  });
85
105
  }
@@ -140,6 +160,7 @@ export function useChatMessages(client, startSession) {
140
160
  setIsStreaming,
141
161
  setStreamingStartTime,
142
162
  setError,
163
+ setLatestContextSize,
143
164
  ]);
144
165
  return {
145
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,6 +34,7 @@ export interface ChatStore {
22
34
  outputTokens: number;
23
35
  totalTokens: number;
24
36
  };
37
+ latestContextSize: ContextSize | null;
25
38
  currentModel: string | null;
26
39
  tokenDisplayMode: "context" | "input" | "output";
27
40
  logs: LogEntry[];
@@ -49,6 +62,7 @@ export interface ChatStore {
49
62
  outputTokens?: number;
50
63
  totalTokens?: number;
51
64
  }) => void;
65
+ setLatestContextSize: (contextSize: ContextSize | null) => void;
52
66
  setCurrentModel: (model: string) => void;
53
67
  resetTokens: () => void;
54
68
  cycleTokenDisplayMode: () => void;
@@ -24,6 +24,7 @@ export const useChatStore = create((set) => ({
24
24
  outputTokens: 0,
25
25
  totalTokens: 0,
26
26
  },
27
+ latestContextSize: null,
27
28
  currentModel: "claude-sonnet-4-5-20250929", // Default model, TODO: get from server
28
29
  tokenDisplayMode: "context", // Default to showing context (both billed and current)
29
30
  logs: [],
@@ -278,6 +279,7 @@ export const useChatStore = create((set) => ({
278
279
  (state.currentContext.outputTokens + (tokenUsage.outputTokens ?? 0)),
279
280
  },
280
281
  })),
282
+ setLatestContextSize: (contextSize) => set({ latestContextSize: contextSize }),
281
283
  setCurrentModel: (model) => set({ currentModel: model }),
282
284
  resetTokens: () => set({
283
285
  totalBilled: {
@@ -290,6 +292,7 @@ export const useChatStore = create((set) => ({
290
292
  outputTokens: 0,
291
293
  totalTokens: 0,
292
294
  },
295
+ latestContextSize: null,
293
296
  }),
294
297
  cycleTokenDisplayMode: () => set((state) => {
295
298
  const modes = [
@@ -2,7 +2,7 @@ import { type VariantProps } from "class-variance-authority";
2
2
  import * as React from "react";
3
3
  declare const buttonVariants: (props?: ({
4
4
  variant?: "default" | "link" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
5
- size?: "default" | "sm" | "lg" | "icon" | null | undefined;
5
+ size?: "default" | "icon" | "sm" | "lg" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
8
8
  asChild?: boolean;
@@ -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,11 +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 token usage from the most recent assistant message
127
- const latestTokenUsage = messages
128
- .slice()
129
- .reverse()
130
- .find((msg) => msg.role === "assistant" && msg.tokenUsage)?.tokenUsage;
148
+ // Get the latest context size from the session context
149
+ const latestContextSize = useChatStore((state) => state.latestContextSize);
131
150
  // Command menu items for chat input
132
151
  const commandMenuItems = [
133
152
  {
@@ -171,12 +190,7 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
171
190
  },
172
191
  },
173
192
  ];
174
- 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: [
175
- "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",
176
- "Explain how this works",
177
- "Create a new feature",
178
- "Review my changes",
179
- ], 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) => {
180
194
  sendMessage(prompt);
181
195
  setPlaceholder("Type a message or / for commands...");
182
196
  logger.info("Prompt clicked", { prompt });
@@ -199,5 +213,5 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
199
213
  previousMessage?.role === "assistant" ? "mt-2" : "mt-6";
200
214
  }
201
215
  return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
202
- }) })) }), _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, {}), latestTokenUsage && (_jsx(ContextUsageButton, { percentage: calculateTokenPercentage(latestTokenUsage.totalTokens ?? 0, currentModel ?? undefined), tokens: latestTokenUsage.totalTokens ?? 0, formattedPercentage: formatTokenPercentage(latestTokenUsage.totalTokens ?? 0, 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, {}) }))] }));
203
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
  */