@townco/ui 0.1.115 → 0.1.117

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.
@@ -158,11 +158,36 @@ export declare function useChatMessages(client: AcpClient | null, startSession:
158
158
  };
159
159
  })[] | undefined;
160
160
  isStreaming?: boolean | undefined;
161
+ context_size?: {
162
+ systemPromptTokens: number;
163
+ toolOverheadTokens?: number | undefined;
164
+ mcpOverheadTokens?: number | undefined;
165
+ userMessagesTokens: number;
166
+ assistantMessagesTokens: number;
167
+ toolInputTokens: number;
168
+ toolResultsTokens: number;
169
+ totalEstimated: number;
170
+ llmReportedInputTokens?: number | undefined;
171
+ modelContextWindow?: number | undefined;
172
+ } | undefined;
161
173
  _meta?: {
174
+ [x: string]: unknown;
162
175
  semanticName?: string | undefined;
163
176
  agentDefinitionName?: string | undefined;
164
177
  currentActivity?: string | undefined;
165
178
  statusGenerating?: boolean | undefined;
179
+ context_size?: {
180
+ systemPromptTokens: number;
181
+ toolOverheadTokens?: number | undefined;
182
+ mcpOverheadTokens?: number | undefined;
183
+ userMessagesTokens: number;
184
+ assistantMessagesTokens: number;
185
+ toolInputTokens: number;
186
+ toolResultsTokens: number;
187
+ totalEstimated: number;
188
+ llmReportedInputTokens?: number | undefined;
189
+ modelContextWindow?: number | undefined;
190
+ } | undefined;
166
191
  } | undefined;
167
192
  }[] | undefined;
168
193
  subagentStreaming?: boolean | undefined;
@@ -155,11 +155,36 @@ export declare function useToolCalls(client: AcpClient | null): {
155
155
  };
156
156
  })[] | undefined;
157
157
  isStreaming?: boolean | undefined;
158
+ context_size?: {
159
+ systemPromptTokens: number;
160
+ toolOverheadTokens?: number | undefined;
161
+ mcpOverheadTokens?: number | undefined;
162
+ userMessagesTokens: number;
163
+ assistantMessagesTokens: number;
164
+ toolInputTokens: number;
165
+ toolResultsTokens: number;
166
+ totalEstimated: number;
167
+ llmReportedInputTokens?: number | undefined;
168
+ modelContextWindow?: number | undefined;
169
+ } | undefined;
158
170
  _meta?: {
171
+ [x: string]: unknown;
159
172
  semanticName?: string | undefined;
160
173
  agentDefinitionName?: string | undefined;
161
174
  currentActivity?: string | undefined;
162
175
  statusGenerating?: boolean | undefined;
176
+ context_size?: {
177
+ systemPromptTokens: number;
178
+ toolOverheadTokens?: number | undefined;
179
+ mcpOverheadTokens?: number | undefined;
180
+ userMessagesTokens: number;
181
+ assistantMessagesTokens: number;
182
+ toolInputTokens: number;
183
+ toolResultsTokens: number;
184
+ totalEstimated: number;
185
+ llmReportedInputTokens?: number | undefined;
186
+ modelContextWindow?: number | undefined;
187
+ } | undefined;
163
188
  } | undefined;
164
189
  }[] | undefined;
165
190
  subagentStreaming?: boolean | undefined;
@@ -312,11 +337,36 @@ export declare function useToolCalls(client: AcpClient | null): {
312
337
  };
313
338
  })[] | undefined;
314
339
  isStreaming?: boolean | undefined;
340
+ context_size?: {
341
+ systemPromptTokens: number;
342
+ toolOverheadTokens?: number | undefined;
343
+ mcpOverheadTokens?: number | undefined;
344
+ userMessagesTokens: number;
345
+ assistantMessagesTokens: number;
346
+ toolInputTokens: number;
347
+ toolResultsTokens: number;
348
+ totalEstimated: number;
349
+ llmReportedInputTokens?: number | undefined;
350
+ modelContextWindow?: number | undefined;
351
+ } | undefined;
315
352
  _meta?: {
353
+ [x: string]: unknown;
316
354
  semanticName?: string | undefined;
317
355
  agentDefinitionName?: string | undefined;
318
356
  currentActivity?: string | undefined;
319
357
  statusGenerating?: boolean | undefined;
358
+ context_size?: {
359
+ systemPromptTokens: number;
360
+ toolOverheadTokens?: number | undefined;
361
+ mcpOverheadTokens?: number | undefined;
362
+ userMessagesTokens: number;
363
+ assistantMessagesTokens: number;
364
+ toolInputTokens: number;
365
+ toolResultsTokens: number;
366
+ totalEstimated: number;
367
+ llmReportedInputTokens?: number | undefined;
368
+ modelContextWindow?: number | undefined;
369
+ } | undefined;
320
370
  } | undefined;
321
371
  }[] | undefined;
322
372
  subagentStreaming?: boolean | undefined;
@@ -237,12 +237,36 @@ export declare const DisplayMessage: z.ZodObject<{
237
237
  }, z.core.$strip>;
238
238
  }, z.core.$strip>], "type">>>;
239
239
  isStreaming: z.ZodOptional<z.ZodBoolean>;
240
+ context_size: z.ZodOptional<z.ZodObject<{
241
+ systemPromptTokens: z.ZodNumber;
242
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
243
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
244
+ userMessagesTokens: z.ZodNumber;
245
+ assistantMessagesTokens: z.ZodNumber;
246
+ toolInputTokens: z.ZodNumber;
247
+ toolResultsTokens: z.ZodNumber;
248
+ totalEstimated: z.ZodNumber;
249
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
250
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
251
+ }, z.core.$strip>>;
240
252
  _meta: z.ZodOptional<z.ZodObject<{
241
253
  semanticName: z.ZodOptional<z.ZodString>;
242
254
  agentDefinitionName: z.ZodOptional<z.ZodString>;
243
255
  currentActivity: z.ZodOptional<z.ZodString>;
244
256
  statusGenerating: z.ZodOptional<z.ZodBoolean>;
245
- }, z.core.$strip>>;
257
+ context_size: z.ZodOptional<z.ZodObject<{
258
+ systemPromptTokens: z.ZodNumber;
259
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
260
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
261
+ userMessagesTokens: z.ZodNumber;
262
+ assistantMessagesTokens: z.ZodNumber;
263
+ toolInputTokens: z.ZodNumber;
264
+ toolResultsTokens: z.ZodNumber;
265
+ totalEstimated: z.ZodNumber;
266
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
267
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
268
+ }, z.core.$strip>>;
269
+ }, z.core.$loose>>;
246
270
  }, z.core.$strip>>>;
247
271
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
248
272
  subagentCompleted: z.ZodOptional<z.ZodBoolean>;
@@ -508,12 +532,36 @@ export declare const ChatSessionState: z.ZodObject<{
508
532
  }, z.core.$strip>;
509
533
  }, z.core.$strip>], "type">>>;
510
534
  isStreaming: z.ZodOptional<z.ZodBoolean>;
535
+ context_size: z.ZodOptional<z.ZodObject<{
536
+ systemPromptTokens: z.ZodNumber;
537
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
538
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
539
+ userMessagesTokens: z.ZodNumber;
540
+ assistantMessagesTokens: z.ZodNumber;
541
+ toolInputTokens: z.ZodNumber;
542
+ toolResultsTokens: z.ZodNumber;
543
+ totalEstimated: z.ZodNumber;
544
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
545
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
546
+ }, z.core.$strip>>;
511
547
  _meta: z.ZodOptional<z.ZodObject<{
512
548
  semanticName: z.ZodOptional<z.ZodString>;
513
549
  agentDefinitionName: z.ZodOptional<z.ZodString>;
514
550
  currentActivity: z.ZodOptional<z.ZodString>;
515
551
  statusGenerating: z.ZodOptional<z.ZodBoolean>;
516
- }, z.core.$strip>>;
552
+ context_size: z.ZodOptional<z.ZodObject<{
553
+ systemPromptTokens: z.ZodNumber;
554
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
555
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
556
+ userMessagesTokens: z.ZodNumber;
557
+ assistantMessagesTokens: z.ZodNumber;
558
+ toolInputTokens: z.ZodNumber;
559
+ toolResultsTokens: z.ZodNumber;
560
+ totalEstimated: z.ZodNumber;
561
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
562
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
563
+ }, z.core.$strip>>;
564
+ }, z.core.$loose>>;
517
565
  }, z.core.$strip>>>;
518
566
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
519
567
  subagentCompleted: z.ZodOptional<z.ZodBoolean>;
@@ -1,4 +1,20 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Context size breakdown (as provided by agent metadata).
4
+ * Stored on message metadata as `_meta.context_size` (or sometimes `context_size`).
5
+ */
6
+ export declare const ContextSizeSchema: z.ZodObject<{
7
+ systemPromptTokens: z.ZodNumber;
8
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
9
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
10
+ userMessagesTokens: z.ZodNumber;
11
+ assistantMessagesTokens: z.ZodNumber;
12
+ toolInputTokens: z.ZodNumber;
13
+ toolResultsTokens: z.ZodNumber;
14
+ totalEstimated: z.ZodNumber;
15
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
16
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
17
+ }, z.core.$strip>;
2
18
  /**
3
19
  * Tool call status lifecycle
4
20
  */
@@ -262,12 +278,36 @@ export declare const SubagentMessageSchema: z.ZodObject<{
262
278
  }, z.core.$strip>;
263
279
  }, z.core.$strip>], "type">>>;
264
280
  isStreaming: z.ZodOptional<z.ZodBoolean>;
281
+ context_size: z.ZodOptional<z.ZodObject<{
282
+ systemPromptTokens: z.ZodNumber;
283
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
284
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
285
+ userMessagesTokens: z.ZodNumber;
286
+ assistantMessagesTokens: z.ZodNumber;
287
+ toolInputTokens: z.ZodNumber;
288
+ toolResultsTokens: z.ZodNumber;
289
+ totalEstimated: z.ZodNumber;
290
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
291
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
292
+ }, z.core.$strip>>;
265
293
  _meta: z.ZodOptional<z.ZodObject<{
266
294
  semanticName: z.ZodOptional<z.ZodString>;
267
295
  agentDefinitionName: z.ZodOptional<z.ZodString>;
268
296
  currentActivity: z.ZodOptional<z.ZodString>;
269
297
  statusGenerating: z.ZodOptional<z.ZodBoolean>;
270
- }, z.core.$strip>>;
298
+ context_size: z.ZodOptional<z.ZodObject<{
299
+ systemPromptTokens: z.ZodNumber;
300
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
301
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
302
+ userMessagesTokens: z.ZodNumber;
303
+ assistantMessagesTokens: z.ZodNumber;
304
+ toolInputTokens: z.ZodNumber;
305
+ toolResultsTokens: z.ZodNumber;
306
+ totalEstimated: z.ZodNumber;
307
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
308
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
309
+ }, z.core.$strip>>;
310
+ }, z.core.$loose>>;
271
311
  }, z.core.$strip>;
272
312
  export type SubagentMessage = z.infer<typeof SubagentMessageSchema>;
273
313
  /**
@@ -449,12 +489,36 @@ export declare const ToolCallSchema: z.ZodObject<{
449
489
  }, z.core.$strip>;
450
490
  }, z.core.$strip>], "type">>>;
451
491
  isStreaming: z.ZodOptional<z.ZodBoolean>;
492
+ context_size: z.ZodOptional<z.ZodObject<{
493
+ systemPromptTokens: z.ZodNumber;
494
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
495
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
496
+ userMessagesTokens: z.ZodNumber;
497
+ assistantMessagesTokens: z.ZodNumber;
498
+ toolInputTokens: z.ZodNumber;
499
+ toolResultsTokens: z.ZodNumber;
500
+ totalEstimated: z.ZodNumber;
501
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
502
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
503
+ }, z.core.$strip>>;
452
504
  _meta: z.ZodOptional<z.ZodObject<{
453
505
  semanticName: z.ZodOptional<z.ZodString>;
454
506
  agentDefinitionName: z.ZodOptional<z.ZodString>;
455
507
  currentActivity: z.ZodOptional<z.ZodString>;
456
508
  statusGenerating: z.ZodOptional<z.ZodBoolean>;
457
- }, z.core.$strip>>;
509
+ context_size: z.ZodOptional<z.ZodObject<{
510
+ systemPromptTokens: z.ZodNumber;
511
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
512
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
513
+ userMessagesTokens: z.ZodNumber;
514
+ assistantMessagesTokens: z.ZodNumber;
515
+ toolInputTokens: z.ZodNumber;
516
+ toolResultsTokens: z.ZodNumber;
517
+ totalEstimated: z.ZodNumber;
518
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
519
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
520
+ }, z.core.$strip>>;
521
+ }, z.core.$loose>>;
458
522
  }, z.core.$strip>>>;
459
523
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
460
524
  subagentCompleted: z.ZodOptional<z.ZodBoolean>;
@@ -608,12 +672,36 @@ export declare const ToolCallUpdateSchema: z.ZodObject<{
608
672
  }, z.core.$strip>;
609
673
  }, z.core.$strip>], "type">>>;
610
674
  isStreaming: z.ZodOptional<z.ZodBoolean>;
675
+ context_size: z.ZodOptional<z.ZodObject<{
676
+ systemPromptTokens: z.ZodNumber;
677
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
678
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
679
+ userMessagesTokens: z.ZodNumber;
680
+ assistantMessagesTokens: z.ZodNumber;
681
+ toolInputTokens: z.ZodNumber;
682
+ toolResultsTokens: z.ZodNumber;
683
+ totalEstimated: z.ZodNumber;
684
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
685
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
686
+ }, z.core.$strip>>;
611
687
  _meta: z.ZodOptional<z.ZodObject<{
612
688
  semanticName: z.ZodOptional<z.ZodString>;
613
689
  agentDefinitionName: z.ZodOptional<z.ZodString>;
614
690
  currentActivity: z.ZodOptional<z.ZodString>;
615
691
  statusGenerating: z.ZodOptional<z.ZodBoolean>;
616
- }, z.core.$strip>>;
692
+ context_size: z.ZodOptional<z.ZodObject<{
693
+ systemPromptTokens: z.ZodNumber;
694
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
695
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
696
+ userMessagesTokens: z.ZodNumber;
697
+ assistantMessagesTokens: z.ZodNumber;
698
+ toolInputTokens: z.ZodNumber;
699
+ toolResultsTokens: z.ZodNumber;
700
+ totalEstimated: z.ZodNumber;
701
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
702
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
703
+ }, z.core.$strip>>;
704
+ }, z.core.$loose>>;
617
705
  }, z.core.$strip>>>;
618
706
  subagentCompleted: z.ZodOptional<z.ZodBoolean>;
619
707
  _meta: z.ZodOptional<z.ZodObject<{
@@ -1,4 +1,20 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Context size breakdown (as provided by agent metadata).
4
+ * Stored on message metadata as `_meta.context_size` (or sometimes `context_size`).
5
+ */
6
+ export const ContextSizeSchema = z.object({
7
+ systemPromptTokens: z.number(),
8
+ toolOverheadTokens: z.number().optional(), // Optional for backward compatibility
9
+ mcpOverheadTokens: z.number().optional(), // Optional for backward compatibility
10
+ userMessagesTokens: z.number(),
11
+ assistantMessagesTokens: z.number(),
12
+ toolInputTokens: z.number(),
13
+ toolResultsTokens: z.number(),
14
+ totalEstimated: z.number(),
15
+ llmReportedInputTokens: z.number().optional(),
16
+ modelContextWindow: z.number().optional(), // Model's max context window
17
+ });
2
18
  /**
3
19
  * Tool call status lifecycle
4
20
  */
@@ -117,13 +133,19 @@ export const SubagentMessageSchema = z.object({
117
133
  /** Interleaved content blocks in arrival order */
118
134
  contentBlocks: z.array(SubagentContentBlockSchema).optional(),
119
135
  isStreaming: z.boolean().optional(),
136
+ /** Context size metadata (may be present at top-level in some payloads) */
137
+ context_size: ContextSizeSchema.optional(),
120
138
  _meta: z
121
139
  .object({
122
140
  semanticName: z.string().optional(),
123
141
  agentDefinitionName: z.string().optional(),
124
142
  currentActivity: z.string().optional(),
125
143
  statusGenerating: z.boolean().optional(),
144
+ /** Context size metadata (preferred location) */
145
+ context_size: ContextSizeSchema.optional(),
126
146
  })
147
+ // Preserve forward-compatible metadata fields
148
+ .passthrough()
127
149
  .optional(),
128
150
  });
129
151
  /**
@@ -330,30 +330,7 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
330
330
  logger.info("Prompt clicked", { prompt });
331
331
  }, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles, onOpenSettings: openSettings, toolsAndMcpsCount: agentTools.length +
332
332
  agentMcps.length +
333
- agentSubagents.length }) })) })) : null) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
334
- // Calculate dynamic spacing based on message sequence
335
- const isFirst = index === 0;
336
- const previousMessage = isFirst
337
- ? null
338
- : messages[index - 1];
339
- let spacingClass = "mt-2";
340
- if (isFirst) {
341
- // First message needs more top margin if it's an assistant initial message
342
- spacingClass =
343
- message.role === "assistant" ? "mt-8" : "mt-2";
344
- }
345
- else if (message.role === "user") {
346
- // User message usually starts a new turn
347
- spacingClass =
348
- previousMessage?.role === "user" ? "mt-4" : "mt-4";
349
- }
350
- else if (message.role === "assistant") {
351
- // Assistant message is usually a response
352
- spacingClass =
353
- previousMessage?.role === "assistant"
354
- ? "mt-2"
355
- : "mt-6";
356
- }
333
+ agentSubagents.length }) })) })) : null) : (_jsx("div", { className: "flex flex-col px-4 py-4", children: messages.map((message, index) => {
357
334
  // Check if any message is streaming
358
335
  const anyMessageStreaming = messages.some((m) => m.isStreaming);
359
336
  // Calculate which user message number this is (1-indexed for display, 0-indexed for API)
@@ -366,7 +343,7 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
366
343
  // Check if this message should be dimmed (comes after editing message)
367
344
  const shouldDim = editingMessageIndex !== null &&
368
345
  index > editingMessageIndex;
369
- return (_jsx(Message, { message: message, className: cn(spacingClass, "group", shouldDim && "opacity-50"), isLastMessage: index === messages.length - 1, children: _jsx("div", { className: "flex flex-col w-full", children: message.role === "user" ? (_jsx(EditableUserMessage, { message: message, messageIndex: userMessageIndex, isStreaming: anyMessageStreaming, onEditAndResend: editAndResend, sticky: true, onEditingChange: (isEditing) => {
346
+ return (_jsx(Message, { message: message, className: cn("group", shouldDim && "opacity-50"), isLastMessage: index === messages.length - 1, children: _jsx("div", { className: "flex flex-col w-full min-w-0", children: message.role === "user" ? (_jsx(EditableUserMessage, { message: message, messageIndex: userMessageIndex, isStreaming: anyMessageStreaming, onEditAndResend: editAndResend, sticky: true, onEditingChange: (isEditing) => {
370
347
  setEditingMessageIndex(isEditing ? index : null);
371
348
  } })) : (_jsxs(_Fragment, { children: [
372
349
  _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }), _jsx(MessageActions, { message: message, isStreaming: message.isStreaming, onSendMessage: sendMessage, isLastAssistantMessage: index ===
@@ -1,16 +1,20 @@
1
1
  import * as React from "react";
2
- export interface ContextSize {
3
- systemPromptTokens: number;
4
- toolOverheadTokens?: number;
5
- mcpOverheadTokens?: number;
6
- userMessagesTokens: number;
7
- assistantMessagesTokens: number;
8
- toolInputTokens: number;
9
- toolResultsTokens: number;
10
- totalEstimated: number;
11
- llmReportedInputTokens?: number;
12
- modelContextWindow?: number;
13
- }
2
+ import { type ContextSize } from "../../core/store/chat-store.js";
14
3
  export interface ContextUsageButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
+ /**
5
+ * Optional explicit context size to render.
6
+ * If omitted, the component uses the latest context size from the chat store.
7
+ */
8
+ contextSize?: ContextSize | null;
15
9
  }
10
+ /**
11
+ * Inline (non-button) context usage visualization. Useful inside other clickable
12
+ * elements (e.g. subagent headers) to avoid nesting <button> inside <button>.
13
+ */
14
+ export declare function ContextUsageIndicator({ contextSize, className, size }: {
15
+ contextSize?: ContextSize | null;
16
+ className?: string;
17
+ /** Pixel size of the indicator (width/height) */
18
+ size?: number;
19
+ }): import("react/jsx-runtime").JSX.Element | null;
16
20
  export declare const ContextUsageButton: React.ForwardRefExoticComponent<ContextUsageButtonProps & React.RefAttributes<HTMLButtonElement>>;
@@ -6,13 +6,7 @@ import { Button } from "./Button.js";
6
6
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./Tooltip.js";
7
7
  // Default context window for backward compatibility (should not be used in practice)
8
8
  const DEFAULT_MODEL_CONTEXT_WINDOW = 200000;
9
- export const ContextUsageButton = React.forwardRef(({ className, ...props }, ref) => {
10
- const latestContextSize = useChatStore((state) => state.latestContextSize);
11
- // Don't render if no context size available
12
- if (latestContextSize == null) {
13
- return null;
14
- }
15
- const contextSize = latestContextSize;
9
+ function getContextUsageStats(contextSize) {
16
10
  // Use max of estimated and LLM-reported tokens (LLM reported as fallback if higher)
17
11
  const actualTokens = Math.max(contextSize.totalEstimated, contextSize.llmReportedInputTokens ?? 0);
18
12
  // Use model context window from backend, or default for backward compatibility
@@ -36,53 +30,99 @@ export const ContextUsageButton = React.forwardRef(({ className, ...props }, ref
36
30
  return "0.0%";
37
31
  return `${((tokens / actualTokens) * 100).toFixed(1)}%`;
38
32
  };
39
- // SVG parameters
33
+ // SVG parameters (defaults; can be overridden by callers)
40
34
  const size = 16;
41
35
  const strokeWidth = 2;
42
36
  const radius = (size - strokeWidth) / 2;
43
37
  const center = size / 2;
44
38
  const circumference = 2 * Math.PI * radius;
45
39
  const offset = circumference - (clampedPercentage / 100) * circumference;
40
+ return {
41
+ actualTokens,
42
+ formattedPercentage,
43
+ percentage,
44
+ clampedPercentage,
45
+ colorClass,
46
+ calculatePercentage,
47
+ size,
48
+ strokeWidth,
49
+ radius,
50
+ center,
51
+ circumference,
52
+ offset,
53
+ };
54
+ }
55
+ function ContextUsageTooltipContent({ contextSize, }) {
56
+ const { actualTokens, formattedPercentage, colorClass, calculatePercentage } = getContextUsageStats(contextSize);
57
+ return (_jsx(TooltipContent, { side: "top", align: "center", className: "p-3", children: _jsxs("div", { className: "space-y-2 font-mono text-xs", children: [
58
+ _jsxs("div", { className: "space-y-1", children: [
59
+ _jsxs("div", { className: "flex justify-between gap-6", children: [
60
+ _jsx("span", { className: "text-muted-foreground", children: "System Prompt:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
61
+ _jsx("span", { children: contextSize.systemPromptTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.systemPromptTokens), ")"] })
62
+ ] })
63
+ ] }), contextSize.toolOverheadTokens !== undefined &&
64
+ contextSize.toolOverheadTokens > 0 && (_jsxs("div", { className: "flex justify-between gap-6", children: [
65
+ _jsx("span", { className: "text-muted-foreground", children: "Tools Definition:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
66
+ _jsx("span", { children: contextSize.toolOverheadTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolOverheadTokens), ")"] })
67
+ ] })
68
+ ] })), contextSize.mcpOverheadTokens !== undefined &&
69
+ contextSize.mcpOverheadTokens > 0 && (_jsxs("div", { className: "flex justify-between gap-6", children: [
70
+ _jsx("span", { className: "text-muted-foreground", children: "MCPs Definition:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
71
+ _jsx("span", { children: contextSize.mcpOverheadTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.mcpOverheadTokens), ")"] })
72
+ ] })
73
+ ] })), _jsxs("div", { className: "flex justify-between gap-6", children: [
74
+ _jsx("span", { className: "text-muted-foreground", children: "User Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
75
+ _jsx("span", { children: contextSize.userMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.userMessagesTokens), ")"] })
76
+ ] })
77
+ ] }), _jsxs("div", { className: "flex justify-between gap-6", children: [
78
+ _jsx("span", { className: "text-muted-foreground", children: "Assistant Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
79
+ _jsx("span", { children: contextSize.assistantMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.assistantMessagesTokens), ")"] })
80
+ ] })
81
+ ] }), _jsxs("div", { className: "flex justify-between gap-6", children: [
82
+ _jsx("span", { className: "text-muted-foreground", children: "Tool Inputs:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
83
+ _jsx("span", { children: contextSize.toolInputTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolInputTokens), ")"] })
84
+ ] })
85
+ ] }), _jsxs("div", { className: "flex justify-between gap-6", children: [
86
+ _jsx("span", { className: "text-muted-foreground", children: "Tool Results:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
87
+ _jsx("span", { children: contextSize.toolResultsTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolResultsTokens), ")"] })
88
+ ] })
89
+ ] })
90
+ ] }), _jsx("div", { className: "border-t border-border pt-2", children: _jsxs("div", { className: cn("flex justify-end gap-2 font-semibold", colorClass), children: [
91
+ _jsx("span", { children: actualTokens.toLocaleString() }), _jsxs("span", { children: ["(", formattedPercentage, ")"] })
92
+ ] }) })
93
+ ] }) }));
94
+ }
95
+ /**
96
+ * Inline (non-button) context usage visualization. Useful inside other clickable
97
+ * elements (e.g. subagent headers) to avoid nesting <button> inside <button>.
98
+ */
99
+ export function ContextUsageIndicator({ contextSize, className, size = 12, }) {
100
+ if (contextSize == null)
101
+ return null;
102
+ const { colorClass, clampedPercentage } = getContextUsageStats(contextSize);
103
+ const strokeWidth = 2;
104
+ const radius = (size - strokeWidth) / 2;
105
+ const center = size / 2;
106
+ const circumference = 2 * Math.PI * radius;
107
+ const offset = circumference - (clampedPercentage / 100) * circumference;
108
+ return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [
109
+ _jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: cn("inline-flex items-center justify-center rounded-full cursor-default", colorClass, className), role: "img", "aria-label": "Context usage indicator", children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "transform -rotate-90", children: [
110
+ _jsx("title", { children: "Context usage indicator" }), _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" })
111
+ ] }) }) }), _jsx(ContextUsageTooltipContent, { contextSize: contextSize })
112
+ ] }) }));
113
+ }
114
+ export const ContextUsageButton = React.forwardRef(({ className, contextSize: explicitContextSize, ...props }, ref) => {
115
+ const latestContextSize = useChatStore((state) => state.latestContextSize);
116
+ const contextSize = explicitContextSize ?? latestContextSize;
117
+ // Don't render if no context size available
118
+ if (contextSize == null) {
119
+ return null;
120
+ }
121
+ const { colorClass, size, center, radius, strokeWidth, circumference, offset, } = getContextUsageStats(contextSize);
46
122
  return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [
47
123
  _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: [
48
124
  _jsx("title", { children: "Context usage indicator" }), _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" })
49
- ] }) }) }), _jsx(TooltipContent, { side: "top", align: "center", className: "p-3", children: _jsxs("div", { className: "space-y-2 font-mono text-xs", children: [
50
- _jsxs("div", { className: "space-y-1", children: [
51
- _jsxs("div", { className: "flex justify-between gap-6", children: [
52
- _jsx("span", { className: "text-muted-foreground", children: "System Prompt:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
53
- _jsx("span", { children: contextSize.systemPromptTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.systemPromptTokens), ")"] })
54
- ] })
55
- ] }), contextSize.toolOverheadTokens !== undefined &&
56
- contextSize.toolOverheadTokens > 0 && (_jsxs("div", { className: "flex justify-between gap-6", children: [
57
- _jsx("span", { className: "text-muted-foreground", children: "Tools Definition:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
58
- _jsx("span", { children: contextSize.toolOverheadTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolOverheadTokens), ")"] })
59
- ] })
60
- ] })), contextSize.mcpOverheadTokens !== undefined &&
61
- contextSize.mcpOverheadTokens > 0 && (_jsxs("div", { className: "flex justify-between gap-6", children: [
62
- _jsx("span", { className: "text-muted-foreground", children: "MCPs Definition:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
63
- _jsx("span", { children: contextSize.mcpOverheadTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.mcpOverheadTokens), ")"] })
64
- ] })
65
- ] })), _jsxs("div", { className: "flex justify-between gap-6", children: [
66
- _jsx("span", { className: "text-muted-foreground", children: "User Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
67
- _jsx("span", { children: contextSize.userMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.userMessagesTokens), ")"] })
68
- ] })
69
- ] }), _jsxs("div", { className: "flex justify-between gap-6", children: [
70
- _jsx("span", { className: "text-muted-foreground", children: "Assistant Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
71
- _jsx("span", { children: contextSize.assistantMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.assistantMessagesTokens), ")"] })
72
- ] })
73
- ] }), _jsxs("div", { className: "flex justify-between gap-6", children: [
74
- _jsx("span", { className: "text-muted-foreground", children: "Tool Inputs:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
75
- _jsx("span", { children: contextSize.toolInputTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolInputTokens), ")"] })
76
- ] })
77
- ] }), _jsxs("div", { className: "flex justify-between gap-6", children: [
78
- _jsx("span", { className: "text-muted-foreground", children: "Tool Results:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [
79
- _jsx("span", { children: contextSize.toolResultsTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolResultsTokens), ")"] })
80
- ] })
81
- ] })
82
- ] }), _jsx("div", { className: "border-t border-border pt-2", children: _jsxs("div", { className: cn("flex justify-end gap-2 font-semibold", colorClass), children: [
83
- _jsx("span", { children: actualTokens.toLocaleString() }), _jsxs("span", { children: ["(", formattedPercentage, ")"] })
84
- ] }) })
85
- ] }) })
125
+ ] }) }) }), _jsx(ContextUsageTooltipContent, { contextSize: contextSize })
86
126
  ] }) }));
87
127
  });
88
128
  ContextUsageButton.displayName = "ContextUsageButton";
@@ -114,7 +114,7 @@ function PureEditableUserMessage({ message, messageIndex, isStreaming, onEditAnd
114
114
  }
115
115
  : undefined, role: sticky && !isEditing ? "button" : undefined, tabIndex: sticky && !isEditing ? 0 : undefined, children: _jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: message.images.map((image, imageIndex) => (_jsx("img", { src: `data:${image.mimeType};base64,${image.data}`, alt: `Attachment ${imageIndex + 1}`, className: cn("max-w-[200px] max-h-[200px] rounded-lg object-cover", isEditing && "opacity-50") }, `image-${image.mimeType}-${image.data.slice(0, 20)}`))) })), message.content && (
116
116
  // biome-ignore lint/a11y/useSemanticElements: contentEditable div preserves whitespace formatting better than textarea
117
- _jsx("div", { ref: contentEditableRef, role: "textbox", tabIndex: isEditing ? 0 : -1, contentEditable: isEditing, onKeyDown: isEditing ? handleKeyDown : undefined, suppressContentEditableWarning: true, className: cn("whitespace-pre-wrap text-foreground leading-relaxed outline-none", isEditing && "cursor-text", !isEditing && "cursor-default"), children: message.content }))] }) }), !isStreaming && message.content && (_jsx(Actions, { className: cn("mt-2 transition-opacity justify-end", isEditing
117
+ _jsx("div", { ref: contentEditableRef, role: "textbox", tabIndex: isEditing ? 0 : -1, contentEditable: isEditing, onKeyDown: isEditing ? handleKeyDown : undefined, suppressContentEditableWarning: true, className: cn("whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-foreground leading-relaxed outline-none", isEditing && "cursor-text", !isEditing && "cursor-default"), children: message.content }))] }) }), !isStreaming && message.content && (_jsx(Actions, { className: cn("mt-2 transition-opacity justify-end", isEditing
118
118
  ? "opacity-100"
119
119
  : "opacity-0 group-hover/user-message:opacity-100"), children: isEditing ? (_jsxs(_Fragment, { children: [
120
120
  _jsx(Button, { variant: "ghost", size: "sm", onClick: handleCancelEdit, className: "h-7 px-2 text-xs text-muted-foreground hover:text-foreground", children: "Cancel" }), _jsx(Button, { size: "sm", onClick: handleSaveAndResend, className: "h-7 px-3 text-xs", children: "Send" })
@@ -6,10 +6,10 @@ import { cn } from "../lib/utils.js";
6
6
  * Message wrapper component inspired by shadcn.io/ai
7
7
  * Provides role-based layout and styling for chat messages
8
8
  */
9
- const messageVariants = cva("flex animate-fadeIn", {
9
+ const messageVariants = cva("flex min-w-0 animate-fadeIn", {
10
10
  variants: {
11
11
  role: {
12
- user: "max-w-[80%] self-end ml-auto mr-2",
12
+ user: "max-w-[80%] self-end ml-auto",
13
13
  assistant: "self-start mr-auto",
14
14
  system: "self-start mr-auto max-w-full",
15
15
  },
@@ -63,7 +63,7 @@ function PureMessageActions({ message, isStreaming, onRedo, onSendMessage, isLas
63
63
  toast.info("Export not available");
64
64
  }
65
65
  };
66
- return (_jsxs(Actions, { className: cn("mt-2 mb-10", visibilityClass), children: [
66
+ return (_jsxs(Actions, { className: cn(visibilityClass), children: [
67
67
  _jsx(Action, { onClick: handleCopy, tooltip: isCopied ? "Copied!" : "Copy", children: isCopied ? _jsx(Check, { className: "size-4" }) : _jsx(Copy, { className: "size-4" }) }), _jsx(Action, { onClick: handleRedo, tooltip: "Redo", children: _jsx(RotateCcw, { className: "size-4" }) }), _jsxs(DropdownMenu, { children: [
68
68
  _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [
69
69
  _jsx(TooltipTrigger, { asChild: true, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { className: cn("relative size-7 p-1.5 text-muted-foreground hover:text-foreground"), size: "sm", type: "button", variant: "ghost", children: [