@townco/ui 0.1.115 → 0.1.116

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
  /**
@@ -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";
@@ -0,0 +1,23 @@
1
+ export interface SubagentStreamProps {
2
+ /** Sub-agent HTTP port */
3
+ port: number;
4
+ /** Sub-agent session ID */
5
+ sessionId: string;
6
+ /** Optional host (defaults to localhost) */
7
+ host?: string;
8
+ /** Parent tool call status - use this to determine if sub-agent is running */
9
+ parentStatus?: "pending" | "in_progress" | "completed" | "failed";
10
+ /** Sub-agent name (for display) */
11
+ agentName?: string | undefined;
12
+ /** Query sent to the sub-agent */
13
+ query?: string | undefined;
14
+ }
15
+ /**
16
+ * SubagentStream component - displays streaming content from a sub-agent.
17
+ *
18
+ * This component:
19
+ * - Connects directly to the sub-agent's SSE endpoint
20
+ * - Displays streaming text and tool calls
21
+ * - Renders in a collapsible section (collapsed by default)
22
+ */
23
+ export declare function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }: SubagentStreamProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,98 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown, CircleDot, Loader2 } from "lucide-react";
3
+ import React, { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
5
+ const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
6
+ /**
7
+ * SubagentStream component - displays streaming content from a sub-agent.
8
+ *
9
+ * This component:
10
+ * - Connects directly to the sub-agent's SSE endpoint
11
+ * - Displays streaming text and tool calls
12
+ * - Renders in a collapsible section (collapsed by default)
13
+ */
14
+ export function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }) {
15
+ const [isExpanded, setIsExpanded] = useState(false); // Start collapsed for parallel ops
16
+ const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
17
+ const [isNearBottom, setIsNearBottom] = useState(true);
18
+ const thinkingContainerRef = useRef(null);
19
+ const { messages, isStreaming: hookIsStreaming, error } = useSubagentStream({
20
+ port,
21
+ sessionId,
22
+ ...(host !== undefined ? { host } : {}),
23
+ });
24
+ // Use parent status as primary indicator, fall back to hook's streaming state
25
+ // Parent is "in_progress" means sub-agent is definitely still running
26
+ const isRunning = parentStatus === "in_progress" || parentStatus === "pending" || hookIsStreaming;
27
+ // Get the current/latest message
28
+ const currentMessage = messages[messages.length - 1];
29
+ const hasContent = currentMessage &&
30
+ (currentMessage.content ||
31
+ (currentMessage.toolCalls && currentMessage.toolCalls.length > 0));
32
+ // Auto-collapse Thinking when completed (so Output is the primary view)
33
+ const prevIsRunningRef = useRef(isRunning);
34
+ useEffect(() => {
35
+ if (prevIsRunningRef.current && !isRunning) {
36
+ // Just completed - collapse thinking to show output
37
+ setIsThinkingExpanded(false);
38
+ }
39
+ prevIsRunningRef.current = isRunning;
40
+ }, [isRunning]);
41
+ // Check if user is near bottom of scroll area
42
+ const checkScrollPosition = useCallback(() => {
43
+ const container = thinkingContainerRef.current;
44
+ if (!container)
45
+ return;
46
+ const { scrollTop, scrollHeight, clientHeight } = container;
47
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
48
+ setIsNearBottom(distanceFromBottom < SCROLL_THRESHOLD);
49
+ }, []);
50
+ // Scroll to bottom
51
+ const scrollToBottom = useCallback(() => {
52
+ const container = thinkingContainerRef.current;
53
+ if (!container)
54
+ return;
55
+ container.scrollTop = container.scrollHeight;
56
+ }, []);
57
+ // Auto-scroll when content changes and user is near bottom
58
+ useEffect(() => {
59
+ if (isNearBottom && (isRunning || hasContent)) {
60
+ scrollToBottom();
61
+ }
62
+ }, [currentMessage?.content, currentMessage?.toolCalls, isNearBottom, isRunning, hasContent, scrollToBottom]);
63
+ // Set up scroll listener
64
+ useEffect(() => {
65
+ const container = thinkingContainerRef.current;
66
+ if (!container)
67
+ return;
68
+ const handleScroll = () => checkScrollPosition();
69
+ container.addEventListener("scroll", handleScroll, { passive: true });
70
+ checkScrollPosition(); // Check initial position
71
+ return () => container.removeEventListener("scroll", handleScroll);
72
+ }, [checkScrollPosition, isThinkingExpanded, isExpanded]);
73
+ // Get last line of streaming content for preview
74
+ const lastLine = currentMessage?.content
75
+ ? currentMessage.content.split("\n").filter(Boolean).pop() || ""
76
+ : "";
77
+ const previewText = lastLine.length > 100 ? `${lastLine.slice(0, 100)}...` : lastLine;
78
+ return (_jsxs("div", { children: [!isExpanded && (_jsx("button", { type: "button", onClick: () => setIsExpanded(true), className: "w-full max-w-md text-left cursor-pointer bg-transparent border-none p-0", children: previewText ? (_jsx("p", { className: `text-paragraph-sm text-muted-foreground truncate ${isRunning ? "animate-pulse" : ""}`, children: previewText })) : isRunning ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 italic animate-pulse", children: "Waiting for response..." })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Thinking" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [error && (_jsxs("div", { className: "px-2 py-2 text-[11px] text-destructive", children: ["Error: ", error] })), !error && !hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.toolCalls &&
79
+ currentMessage.toolCalls.length > 0 && (_jsx("div", { className: "space-y-1", children: currentMessage.toolCalls.map((tc) => (_jsx(SubagentToolCallItem, { toolCall: tc }, tc.id))) })), currentMessage.content && (_jsxs("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: [currentMessage.content, currentMessage.isStreaming && (_jsx("span", { className: "inline-block w-1.5 h-3 bg-primary/70 ml-0.5 animate-pulse" }))] }))] }))] }))] }), !isRunning && currentMessage?.content && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2", children: currentMessage.content })] }))] }))] }));
80
+ }
81
+ /**
82
+ * Simple tool call display for sub-agent tool calls
83
+ */
84
+ function SubagentToolCallItem({ toolCall }) {
85
+ const statusIcon = {
86
+ pending: "...",
87
+ in_progress: "",
88
+ completed: "",
89
+ failed: "",
90
+ }[toolCall.status];
91
+ const statusColor = {
92
+ pending: "text-muted-foreground",
93
+ in_progress: "text-blue-500",
94
+ completed: "text-green-500",
95
+ failed: "text-destructive",
96
+ }[toolCall.status];
97
+ return (_jsxs("div", { className: "flex items-center gap-2 text-[10px] text-muted-foreground", children: [_jsx("span", { className: statusColor, children: statusIcon }), _jsx("span", { className: "font-medium", children: toolCall.prettyName || toolCall.title }), toolCall.status === "in_progress" && (_jsx(Loader2, { className: "h-2.5 w-2.5 animate-spin" }))] }));
98
+ }
@@ -44,7 +44,15 @@ export function ToolCall({ toolCall }) {
44
44
  const { resolvedTheme } = useTheme();
45
45
  // Detect TodoWrite tool and subagent
46
46
  const isTodoWrite = toolCall.title === "todo_write";
47
- const isSubagentCall = !!(toolCall.subagentPort && toolCall.subagentSessionId);
47
+ // A subagent call can be detected by:
48
+ // - Live: has port and sessionId (but no stored messages yet)
49
+ // - Replay: has stored subagentMessages
50
+ const hasLiveSubagent = !!(toolCall.subagentPort && toolCall.subagentSessionId);
51
+ const hasStoredSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
52
+ const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
53
+ // Use replay mode if we have stored messages - they should take precedence
54
+ // over trying to connect to SSE (which won't work for replayed sessions)
55
+ const isReplaySubagent = hasStoredSubagent;
48
56
  // Safely access ChatLayout context - will be undefined if not within ChatLayout
49
57
  const layoutContext = React.useContext(ChatLayout.Context);
50
58
  // Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
@@ -139,7 +147,7 @@ export function ToolCall({ toolCall }) {
139
147
  if (isPreliminary) {
140
148
  return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
141
149
  }
142
- return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: iconColorClass, title: statusTooltip, children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isSubagentCall && (_jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
150
+ return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: iconColorClass, title: statusTooltip, children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isSubagentCall && (_jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: toolCall.subagentMessages, isReplay: isReplaySubagent }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
143
151
  Object.keys(toolCall.rawInput).length > 0 &&
144
152
  !toolCall.subagentPort && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-foreground", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) })] })), toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Files" }), _jsx("ul", { className: "space-y-1", children: toolCall.locations.map((loc) => (_jsxs("li", { className: "font-mono text-[11px] text-foreground bg-muted px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null &&
145
153
  loc.line !== undefined &&
@@ -5,6 +5,7 @@ import React, { useEffect, useRef, useState } from "react";
5
5
  import { getGroupDisplayState, getToolCallDisplayState, getToolCallStateVerbiage, isPreliminaryToolCall, } from "../../core/utils/tool-call-state.js";
6
6
  import { generateSmartSummary } from "../../core/utils/tool-summary.js";
7
7
  import * as ChatLayout from "./ChatLayout.js";
8
+ import { ContextUsageIndicator } from "./ContextUsageButton.js";
8
9
  import { SubAgentDetails } from "./SubAgentDetails.js";
9
10
  import { useTheme } from "./ThemeProvider.js";
10
11
  import { TodoSubline } from "./TodoSubline.js";
@@ -141,6 +142,20 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
141
142
  // Detect subagent calls (subagents now run in-process, messages in subagentMessages)
142
143
  const isSubagentCall = !!(singleToolCall?.subagentMessages &&
143
144
  singleToolCall.subagentMessages.length > 0);
145
+ // Extract latest subagent context size (if provided by backend on subagent messages)
146
+ const subagentContextSize = React.useMemo(() => {
147
+ if (!singleToolCall?.subagentMessages)
148
+ return null;
149
+ const messages = singleToolCall.subagentMessages;
150
+ for (let i = messages.length - 1; i >= 0; i--) {
151
+ const msg = messages[i];
152
+ const cs = msg?._meta?.context_size ?? msg?.context_size;
153
+ if (cs != null)
154
+ return cs;
155
+ }
156
+ return null;
157
+ }, [singleToolCall?.subagentMessages]);
158
+ const subagentHeaderContextSize = subagentContextSize;
144
159
  // State for subagent expansion
145
160
  const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
146
161
  // Safely access ChatLayout context
@@ -253,7 +268,7 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
253
268
  return (_jsxs("div", { className: "flex flex-col my-4", children: [
254
269
  _jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit rounded-md px-1 -mx-1", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [
255
270
  _jsxs("div", { className: "flex items-center gap-1.5", children: [
256
- _jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), !isGrouped &&
271
+ _jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), isSubagentCall && (_jsx(ContextUsageIndicator, { contextSize: subagentHeaderContextSize, size: 12, className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors" })), !isGrouped &&
257
272
  singleToolCall?.startedAt &&
258
273
  displayState === "executing" && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt, isRunning: !singleToolCall.subagentCompleted })), isGrouped &&
259
274
  displayState === "executing" &&
@@ -333,6 +348,18 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
333
348
  const [isExpanded, setIsExpanded] = useState(false);
334
349
  // Detect subagent calls (subagents now run in-process, messages in subagentMessages)
335
350
  const isSubagentCall = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
351
+ // Extract latest subagent context size (if provided by backend on subagent messages)
352
+ const subagentContextSize = React.useMemo(() => {
353
+ const messages = toolCall.subagentMessages ?? [];
354
+ for (let i = messages.length - 1; i >= 0; i--) {
355
+ const msg = messages[i];
356
+ const cs = msg?._meta?.context_size ?? msg?.context_size;
357
+ if (cs != null)
358
+ return cs;
359
+ }
360
+ return null;
361
+ }, [toolCall.subagentMessages]);
362
+ const subagentHeaderContextSize = subagentContextSize;
336
363
  // Detect compaction for this individual tool call
337
364
  const hasCompaction = !!((hookNotification?.status === "completed" &&
338
365
  hookNotification.metadata?.action &&
@@ -351,7 +378,7 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
351
378
  _jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [
352
379
  _jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.subagentMessages?.[0]?._meta?.semanticName ||
353
380
  toolCall.rawInput?.agentName ||
354
- "Subagent" }), toolCall.subagentMessages?.[0]?._meta?.semanticName && (_jsxs("span", { className: "text-muted-foreground text-[10px] ml-1", children: ["(", toolCall.rawInput?.agentName || "subagent", ")"] })), isRunning && toolCall.startedAt && (_jsx(RunningDuration, { startTime: toolCall.startedAt })), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
381
+ "Subagent" }), toolCall.subagentMessages?.[0]?._meta?.semanticName && (_jsxs("span", { className: "text-muted-foreground text-[10px] ml-1", children: ["(", toolCall.rawInput?.agentName || "subagent", ")"] })), _jsx(ContextUsageIndicator, { contextSize: subagentHeaderContextSize, size: 12, className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors" }), isRunning && toolCall.startedAt && (_jsx(RunningDuration, { startTime: toolCall.startedAt })), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
355
382
  _jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: isTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: (() => {
356
383
  const meta = toolCall._meta;
357
384
  const percentage = meta?.originalTokens && meta?.finalTokens
@@ -5,6 +5,8 @@ import { cn } from "../lib/utils.js";
5
5
  const TooltipProvider = TooltipPrimitive.Provider;
6
6
  const Tooltip = TooltipPrimitive.Root;
7
7
  const TooltipTrigger = TooltipPrimitive.Trigger;
8
- const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn("z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className), ...props })));
8
+ const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(TooltipPrimitive.Portal, { children: _jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn(
9
+ // High z-index so tooltips render above overlays/modals in the chat UI.
10
+ "z-[9999] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className), ...props }) })));
9
11
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
10
12
  export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
@@ -348,12 +348,36 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
348
348
  }, z.core.$strip>;
349
349
  }, z.core.$strip>], "type">>>;
350
350
  isStreaming: z.ZodOptional<z.ZodBoolean>;
351
+ context_size: z.ZodOptional<z.ZodObject<{
352
+ systemPromptTokens: z.ZodNumber;
353
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
354
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
355
+ userMessagesTokens: z.ZodNumber;
356
+ assistantMessagesTokens: z.ZodNumber;
357
+ toolInputTokens: z.ZodNumber;
358
+ toolResultsTokens: z.ZodNumber;
359
+ totalEstimated: z.ZodNumber;
360
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
361
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
362
+ }, z.core.$strip>>;
351
363
  _meta: z.ZodOptional<z.ZodObject<{
352
364
  semanticName: z.ZodOptional<z.ZodString>;
353
365
  agentDefinitionName: z.ZodOptional<z.ZodString>;
354
366
  currentActivity: z.ZodOptional<z.ZodString>;
355
367
  statusGenerating: z.ZodOptional<z.ZodBoolean>;
356
- }, z.core.$strip>>;
368
+ context_size: z.ZodOptional<z.ZodObject<{
369
+ systemPromptTokens: z.ZodNumber;
370
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
371
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
372
+ userMessagesTokens: z.ZodNumber;
373
+ assistantMessagesTokens: z.ZodNumber;
374
+ toolInputTokens: z.ZodNumber;
375
+ toolResultsTokens: z.ZodNumber;
376
+ totalEstimated: z.ZodNumber;
377
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
378
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
379
+ }, z.core.$strip>>;
380
+ }, z.core.$loose>>;
357
381
  }, z.core.$strip>>>;
358
382
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
359
383
  subagentCompleted: z.ZodOptional<z.ZodBoolean>;
@@ -563,12 +587,36 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
563
587
  }, z.core.$strip>;
564
588
  }, z.core.$strip>], "type">>>;
565
589
  isStreaming: z.ZodOptional<z.ZodBoolean>;
590
+ context_size: z.ZodOptional<z.ZodObject<{
591
+ systemPromptTokens: z.ZodNumber;
592
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
593
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
594
+ userMessagesTokens: z.ZodNumber;
595
+ assistantMessagesTokens: z.ZodNumber;
596
+ toolInputTokens: z.ZodNumber;
597
+ toolResultsTokens: z.ZodNumber;
598
+ totalEstimated: z.ZodNumber;
599
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
600
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
601
+ }, z.core.$strip>>;
566
602
  _meta: z.ZodOptional<z.ZodObject<{
567
603
  semanticName: z.ZodOptional<z.ZodString>;
568
604
  agentDefinitionName: z.ZodOptional<z.ZodString>;
569
605
  currentActivity: z.ZodOptional<z.ZodString>;
570
606
  statusGenerating: z.ZodOptional<z.ZodBoolean>;
571
- }, z.core.$strip>>;
607
+ context_size: z.ZodOptional<z.ZodObject<{
608
+ systemPromptTokens: z.ZodNumber;
609
+ toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
610
+ mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
611
+ userMessagesTokens: z.ZodNumber;
612
+ assistantMessagesTokens: z.ZodNumber;
613
+ toolInputTokens: z.ZodNumber;
614
+ toolResultsTokens: z.ZodNumber;
615
+ totalEstimated: z.ZodNumber;
616
+ llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
617
+ modelContextWindow: z.ZodOptional<z.ZodNumber>;
618
+ }, z.core.$strip>>;
619
+ }, z.core.$loose>>;
572
620
  }, z.core.$strip>>>;
573
621
  subagentCompleted: z.ZodOptional<z.ZodBoolean>;
574
622
  _meta: z.ZodOptional<z.ZodObject<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.115",
3
+ "version": "0.1.116",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -49,7 +49,7 @@
49
49
  "@radix-ui/react-slot": "^1.2.4",
50
50
  "@radix-ui/react-tabs": "^1.1.13",
51
51
  "@radix-ui/react-tooltip": "^1.2.8",
52
- "@townco/core": "0.0.93",
52
+ "@townco/core": "0.0.94",
53
53
  "@types/mdast": "^4.0.4",
54
54
  "@uiw/react-json-view": "^2.0.0-alpha.39",
55
55
  "class-variance-authority": "^0.7.1",
@@ -67,7 +67,7 @@
67
67
  "zustand": "^5.0.8"
68
68
  },
69
69
  "devDependencies": {
70
- "@townco/tsconfig": "0.1.112",
70
+ "@townco/tsconfig": "0.1.113",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "@types/unist": "^3.0.3",