Skip to content

Commit e8aa7f2

Browse files
authored
fix: Fix context window size and redesign usage indicator (#1328)
## Problem Closes #1323 Context window size was underreported by the SDK (returning 200K for 1M-context models), causing inaccurate usage percentages and premature compaction warnings. ## Changes 1. Use gateway-reported context window as floor, only adopt SDK value when larger 2. Persist lastContextWindowSize across prompt() calls so it survives turn boundaries 3. Replace text-only percentage indicator with circular progress ring in session footer 4. Emit live usage_update during streaming (on assistant messages, not just on result) 5. Show context percentage in compact boundary markers 6. Remove old getDefaultContextWindow hardcoded lookup in favor of gateway model data ## How did you test this? Manually
1 parent 4fc651b commit e8aa7f2

15 files changed

Lines changed: 264 additions & 112 deletions

File tree

apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx

Lines changed: 0 additions & 41 deletions
This file was deleted.

apps/code/src/renderer/features/message-editor/components/EditorToolbar.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { ModelSelector } from "@features/sessions/components/ModelSelector";
22
import { Flex } from "@radix-ui/themes";
33
import type { FileAttachment, MentionChip } from "../utils/content";
44
import { AttachmentMenu } from "./AttachmentMenu";
5-
import { ContextUsageIndicator } from "./ContextUsageIndicator";
65

76
interface EditorToolbarProps {
87
disabled?: boolean;
@@ -43,7 +42,6 @@ export function EditorToolbar({
4342
{!hideSelectors && (
4443
<ModelSelector taskId={taskId} adapter={adapter} disabled={disabled} />
4544
)}
46-
<ContextUsageIndicator taskId={taskId} />
4745
</Flex>
4846
);
4947
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Tooltip } from "@components/ui/Tooltip";
2+
import type { ContextUsage } from "@features/sessions/hooks/useContextUsage";
3+
import { Flex, Text } from "@radix-ui/themes";
4+
5+
function formatTokensCompact(tokens: number): string {
6+
if (tokens >= 1_000_000) {
7+
return `${(tokens / 1_000_000).toFixed(1)}M`;
8+
}
9+
return `${Math.round(tokens / 1000)}K`;
10+
}
11+
12+
function formatTokensFull(tokens: number): string {
13+
return tokens.toLocaleString();
14+
}
15+
16+
function getUsageColor(percentage: number): string {
17+
if (percentage >= 90) return "var(--red-9)";
18+
if (percentage >= 75) return "var(--orange-9)";
19+
if (percentage >= 50) return "var(--amber-9)";
20+
return "var(--green-9)";
21+
}
22+
23+
const CIRCLE_SIZE = 20;
24+
const STROKE_WIDTH = 2.5;
25+
const RADIUS = (CIRCLE_SIZE - STROKE_WIDTH) / 2;
26+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
27+
28+
interface ContextUsageIndicatorProps {
29+
usage: ContextUsage | null;
30+
}
31+
32+
export function ContextUsageIndicator({ usage }: ContextUsageIndicatorProps) {
33+
if (!usage) return null;
34+
35+
const { used, size, percentage } = usage;
36+
const strokeDashoffset = CIRCUMFERENCE - (percentage / 100) * CIRCUMFERENCE;
37+
const color = getUsageColor(percentage);
38+
39+
return (
40+
<Tooltip
41+
content={`${formatTokensFull(used)} / ${formatTokensFull(size)} tokens (${percentage}%)`}
42+
side="top"
43+
>
44+
<Flex align="center" gap="1" className="cursor-default select-none">
45+
<svg
46+
width={CIRCLE_SIZE}
47+
height={CIRCLE_SIZE}
48+
className="-rotate-90 shrink-0"
49+
role="img"
50+
aria-label={`Context usage: ${percentage}%`}
51+
>
52+
<circle
53+
cx={CIRCLE_SIZE / 2}
54+
cy={CIRCLE_SIZE / 2}
55+
r={RADIUS}
56+
fill="none"
57+
stroke="var(--gray-5)"
58+
strokeWidth={STROKE_WIDTH}
59+
/>
60+
<circle
61+
cx={CIRCLE_SIZE / 2}
62+
cy={CIRCLE_SIZE / 2}
63+
r={RADIUS}
64+
fill="none"
65+
stroke={color}
66+
strokeWidth={STROKE_WIDTH}
67+
strokeDasharray={CIRCUMFERENCE}
68+
strokeDashoffset={strokeDashoffset}
69+
strokeLinecap="round"
70+
/>
71+
</svg>
72+
<Text size="1" className="text-gray-10 tabular-nums">
73+
{formatTokensCompact(used)}/{formatTokensCompact(size)}
74+
</Text>
75+
</Flex>
76+
</Tooltip>
77+
);
78+
}

apps/code/src/renderer/features/sessions/components/ConversationView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useContextUsage } from "@features/sessions/hooks/useContextUsage";
12
import {
23
sessionStoreSetters,
34
useOptimisticItemsForTask,
@@ -52,6 +53,7 @@ export function ConversationView({
5253
const agentLogsEnabled = useFeatureFlag("posthog-code-background-agent-logs");
5354
const debugLogsCloudRuns = useSettingsStore((s) => s.debugLogsCloudRuns);
5455
const showDebugLogs = agentLogsEnabled && debugLogsCloudRuns;
56+
const contextUsage = useContextUsage(events);
5557

5658
const {
5759
items: conversationItems,
@@ -229,6 +231,7 @@ export function ConversationView({
229231
hasPendingPermission={pendingPermissionsCount > 0}
230232
pausedDurationMs={pausedDurationMs}
231233
isCompacting={isCompacting}
234+
usage={contextUsage}
232235
/>
233236
</div>
234237
}

apps/code/src/renderer/features/sessions/components/SessionFooter.tsx

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { ContextUsage } from "@features/sessions/hooks/useContextUsage";
12
import { Pause } from "@phosphor-icons/react";
23
import { Box, Flex, Text } from "@radix-ui/themes";
34

5+
import { ContextUsageIndicator } from "./ContextUsageIndicator";
46
import { formatDuration, GeneratingIndicator } from "./GeneratingIndicator";
57

68
interface SessionFooterProps {
@@ -12,6 +14,7 @@ interface SessionFooterProps {
1214
hasPendingPermission?: boolean;
1315
pausedDurationMs?: number;
1416
isCompacting?: boolean;
17+
usage?: ContextUsage | null;
1518
}
1619

1720
export function SessionFooter({
@@ -23,37 +26,43 @@ export function SessionFooter({
2326
hasPendingPermission = false,
2427
pausedDurationMs,
2528
isCompacting = false,
29+
usage,
2630
}: SessionFooterProps) {
2731
if (isPromptPending && !isCompacting) {
28-
// Show static "waiting" state when permission is pending
2932
if (hasPendingPermission) {
3033
return (
3134
<Box className="pt-3 pb-1">
32-
<Flex
33-
align="center"
34-
gap="2"
35-
className="select-none text-gray-10"
36-
style={{ userSelect: "none", WebkitUserSelect: "none" }}
37-
>
38-
<Pause size={14} weight="fill" />
39-
<Text size="1">Awaiting permission...</Text>
35+
<Flex align="center" justify="between" gap="2">
36+
<Flex
37+
align="center"
38+
gap="2"
39+
className="select-none text-gray-10"
40+
style={{ userSelect: "none", WebkitUserSelect: "none" }}
41+
>
42+
<Pause size={14} weight="fill" />
43+
<Text size="1">Awaiting permission...</Text>
44+
</Flex>
45+
<ContextUsageIndicator usage={usage ?? null} />
4046
</Flex>
4147
</Box>
4248
);
4349
}
4450

4551
return (
4652
<Box className="pt-3 pb-1">
47-
<Flex align="center" gap="2">
48-
<GeneratingIndicator
49-
startedAt={promptStartedAt}
50-
pausedDurationMs={pausedDurationMs}
51-
/>
52-
{queuedCount > 0 && (
53-
<Text size="1" color="gray">
54-
({queuedCount} queued)
55-
</Text>
56-
)}
53+
<Flex align="center" justify="between" gap="2">
54+
<Flex align="center" gap="2">
55+
<GeneratingIndicator
56+
startedAt={promptStartedAt}
57+
pausedDurationMs={pausedDurationMs}
58+
/>
59+
{queuedCount > 0 && (
60+
<Text size="1" color="gray">
61+
({queuedCount} queued)
62+
</Text>
63+
)}
64+
</Flex>
65+
<ContextUsageIndicator usage={usage ?? null} />
5766
</Flex>
5867
</Box>
5968
);
@@ -69,13 +78,26 @@ export function SessionFooter({
6978
) {
7079
return (
7180
<Box className="pb-1">
72-
<Text
73-
size="1"
74-
color="gray"
75-
style={{ fontVariantNumeric: "tabular-nums" }}
76-
>
77-
Generated in {formatDuration(lastGenerationDuration)}
78-
</Text>
81+
<Flex align="center" justify="between" gap="2">
82+
<Text
83+
size="1"
84+
color="gray"
85+
style={{ fontVariantNumeric: "tabular-nums" }}
86+
>
87+
Generated in {formatDuration(lastGenerationDuration)}
88+
</Text>
89+
<ContextUsageIndicator usage={usage ?? null} />
90+
</Flex>
91+
</Box>
92+
);
93+
}
94+
95+
if (usage) {
96+
return (
97+
<Box className="pb-1">
98+
<Flex justify="end">
99+
<ContextUsageIndicator usage={usage} />
100+
</Flex>
79101
</Box>
80102
);
81103
}

apps/code/src/renderer/features/sessions/components/buildConversationItems.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,14 @@ function handleNotification(
344344
const params = msg.params as {
345345
trigger: "manual" | "auto";
346346
preTokens: number;
347+
contextSize?: number;
347348
};
348349
markCompactingStatusComplete(b);
349350
pushItem(b, {
350351
sessionUpdate: "compact_boundary",
351352
trigger: params.trigger,
352353
preTokens: params.preTokens,
354+
contextSize: params.contextSize,
353355
});
354356
return;
355357
}
@@ -549,6 +551,7 @@ function processSessionUpdate(b: ItemBuilder, update: SessionUpdate) {
549551
case "plan":
550552
case "available_commands_update":
551553
case "config_option_update":
554+
case "usage_update":
552555
break;
553556

554557
default: {

apps/code/src/renderer/features/sessions/components/session-update/CompactBoundaryView.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import { Badge, Box, Flex, Text } from "@radix-ui/themes";
44
interface CompactBoundaryViewProps {
55
trigger: "manual" | "auto";
66
preTokens: number;
7+
contextSize?: number;
78
}
89

910
export function CompactBoundaryView({
1011
trigger,
1112
preTokens,
13+
contextSize,
1214
}: CompactBoundaryViewProps) {
1315
const tokensK = Math.round(preTokens / 1000);
16+
const percent =
17+
contextSize && contextSize > 0
18+
? Math.round((preTokens / contextSize) * 100)
19+
: null;
1420

1521
return (
1622
<Box className="my-1 border-blue-6 border-l-2 py-1 pl-3 dark:border-blue-8">
@@ -27,7 +33,9 @@ export function CompactBoundaryView({
2733
{trigger}
2834
</Badge>
2935
<Text size="1" className="text-gray-9">
30-
(~{tokensK}K tokens summarized)
36+
{percent !== null
37+
? `(${percent}% of context · ~${tokensK}K tokens summarized)`
38+
: `(~${tokensK}K tokens summarized)`}
3139
</Text>
3240
</Flex>
3341
</Box>

apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type RenderItem =
2323
sessionUpdate: "compact_boundary";
2424
trigger: "manual" | "auto";
2525
preTokens: number;
26+
contextSize?: number;
2627
}
2728
| {
2829
sessionUpdate: "status";
@@ -101,6 +102,7 @@ export const SessionUpdateView = memo(function SessionUpdateView({
101102
<CompactBoundaryView
102103
trigger={item.trigger}
103104
preTokens={item.preTokens}
105+
contextSize={item.contextSize}
104106
/>
105107
);
106108
case "status":

0 commit comments

Comments
 (0)