Skip to content

Commit a85f2d2

Browse files
committed
🤖 feat: improve auto-compact UI spacing and interactivity
- Move AutoCompactionSettings inside context usage section in CostsTab for better visual grouping - Make CompactionWarning clickable to insert /compact command - Add prependText method to ChatInputAPI for prepending text - Change warning text from 'Context left until' to 'Auto-Compact in X% usage' - Add Storybook coverage for auto-compaction warning state _Generated with mux_
1 parent 27c9cd0 commit a85f2d2

File tree

7 files changed

+208
-8
lines changed

7 files changed

+208
-8
lines changed

src/browser/App.stories.tsx

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,3 +1487,170 @@ These tables should render cleanly without any disruptive copy or download actio
14871487
return <AppWithTableMocks />;
14881488
},
14891489
};
1490+
1491+
/**
1492+
* Story showing the auto-compaction warning when context usage is approaching the threshold.
1493+
* The warning appears above the chat input when usage is >= 60% (threshold 70% minus 10% warning advance).
1494+
* claude-sonnet-4-5 has max_input_tokens: 200,000, so we set usage to ~130,000 tokens (65%) to trigger warning.
1495+
*/
1496+
export const AutoCompactionWarning: Story = {
1497+
render: () => {
1498+
const workspaceId = "ws-high-usage";
1499+
1500+
const projects = new Map<string, ProjectConfig>([
1501+
[
1502+
"/home/user/projects/my-app",
1503+
{
1504+
workspaces: [
1505+
{ path: "/home/user/.mux/src/my-app/feature", id: workspaceId, name: "main" },
1506+
],
1507+
},
1508+
],
1509+
]);
1510+
1511+
const workspaces: FrontendWorkspaceMetadata[] = [
1512+
{
1513+
id: workspaceId,
1514+
name: "main",
1515+
projectPath: "/home/user/projects/my-app",
1516+
projectName: "my-app",
1517+
namedWorkspacePath: "/home/user/.mux/src/my-app/feature",
1518+
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
1519+
createdAt: new Date(NOW - 3600000).toISOString(),
1520+
},
1521+
];
1522+
1523+
const AppWithHighUsage: React.FC = () => {
1524+
const initialized = useRef(false);
1525+
if (!initialized.current) {
1526+
// Enable auto-compaction for this workspace
1527+
localStorage.setItem(`autoCompaction:${workspaceId}:enabled`, "true");
1528+
localStorage.setItem(`autoCompaction:${workspaceId}:threshold`, "70");
1529+
1530+
setupMockAPI({
1531+
projects,
1532+
workspaces,
1533+
apiOverrides: {
1534+
tokenizer: {
1535+
countTokens: () => Promise.resolve(100),
1536+
countTokensBatch: (_model, texts) => Promise.resolve(texts.map(() => 100)),
1537+
calculateStats: () =>
1538+
Promise.resolve({
1539+
consumers: [],
1540+
totalTokens: 0,
1541+
model: "claude-sonnet-4-5",
1542+
tokenizerName: "claude",
1543+
usageHistory: [],
1544+
}),
1545+
},
1546+
providers: {
1547+
setProviderConfig: () => Promise.resolve({ success: true, data: undefined }),
1548+
setModels: () => Promise.resolve({ success: true, data: undefined }),
1549+
getConfig: () =>
1550+
Promise.resolve(
1551+
{} as Record<string, { apiKeySet: boolean; baseUrl?: string; models?: string[] }>
1552+
),
1553+
list: () => Promise.resolve(["anthropic"]),
1554+
},
1555+
workspace: {
1556+
create: (projectPath: string, branchName: string) =>
1557+
Promise.resolve({
1558+
success: true,
1559+
metadata: {
1560+
id: Math.random().toString(36).substring(2, 12),
1561+
name: branchName,
1562+
projectPath,
1563+
projectName: projectPath.split("/").pop() ?? "project",
1564+
namedWorkspacePath: `/mock/workspace/${branchName}`,
1565+
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
1566+
},
1567+
}),
1568+
list: () => Promise.resolve(workspaces),
1569+
rename: (wsId: string) =>
1570+
Promise.resolve({ success: true, data: { newWorkspaceId: wsId } }),
1571+
remove: () => Promise.resolve({ success: true }),
1572+
fork: () => Promise.resolve({ success: false, error: "Not implemented in mock" }),
1573+
openTerminal: () => Promise.resolve(undefined),
1574+
onChat: (wsId, callback) => {
1575+
if (wsId === workspaceId) {
1576+
setTimeout(() => {
1577+
// User message
1578+
callback({
1579+
id: "msg-1",
1580+
role: "user",
1581+
parts: [{ type: "text", text: "Help me with this large codebase" }],
1582+
metadata: {
1583+
historySequence: 1,
1584+
timestamp: STABLE_TIMESTAMP - 60000,
1585+
},
1586+
});
1587+
1588+
// Assistant message with HIGH usage to trigger compaction warning
1589+
// 130,000 tokens = 65% of 200,000 max, which is above 60% warning threshold
1590+
callback({
1591+
id: "msg-2",
1592+
role: "assistant",
1593+
parts: [
1594+
{
1595+
type: "text",
1596+
text: "I've analyzed the codebase. The context window is getting full - notice the compaction warning below!",
1597+
},
1598+
],
1599+
metadata: {
1600+
historySequence: 2,
1601+
timestamp: STABLE_TIMESTAMP,
1602+
model: "claude-sonnet-4-5",
1603+
usage: {
1604+
inputTokens: 125000, // High input to trigger warning
1605+
outputTokens: 5000,
1606+
totalTokens: 130000,
1607+
},
1608+
duration: 5000,
1609+
},
1610+
});
1611+
1612+
callback({ type: "caught-up" });
1613+
}, 100);
1614+
}
1615+
return () => undefined;
1616+
},
1617+
onMetadata: () => () => undefined,
1618+
activity: {
1619+
list: () => Promise.resolve({}),
1620+
subscribe: () => () => undefined,
1621+
},
1622+
sendMessage: () => Promise.resolve({ success: true, data: undefined }),
1623+
resumeStream: () => Promise.resolve({ success: true, data: undefined }),
1624+
interruptStream: () => Promise.resolve({ success: true, data: undefined }),
1625+
clearQueue: () => Promise.resolve({ success: true, data: undefined }),
1626+
truncateHistory: () => Promise.resolve({ success: true, data: undefined }),
1627+
replaceChatHistory: () => Promise.resolve({ success: true, data: undefined }),
1628+
getInfo: () => Promise.resolve(null),
1629+
executeBash: () =>
1630+
Promise.resolve({
1631+
success: true,
1632+
data: { success: true, output: "", exitCode: 0, wall_duration_ms: 0 },
1633+
}),
1634+
},
1635+
},
1636+
});
1637+
1638+
localStorage.setItem(
1639+
"selectedWorkspace",
1640+
JSON.stringify({
1641+
workspaceId: workspaceId,
1642+
projectPath: "/home/user/projects/my-app",
1643+
projectName: "my-app",
1644+
namedWorkspacePath: "/home/user/.mux/src/my-app/feature",
1645+
})
1646+
);
1647+
1648+
initialized.current = true;
1649+
}
1650+
1651+
return <AppLoader />;
1652+
};
1653+
1654+
return <AppWithHighUsage />;
1655+
},
1656+
};

src/browser/components/AIView.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
217217
chatInputAPI.current?.appendText(note);
218218
}, []);
219219

220+
// Handler for manual compaction from CompactionWarning click
221+
const handleCompactClick = useCallback(() => {
222+
chatInputAPI.current?.prependText("/compact\n");
223+
}, []);
224+
220225
// Thinking level state from context
221226
const { thinkingLevel: currentWorkspaceThinking, setThinkingLevel } = useThinking();
222227

@@ -573,6 +578,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
573578
<CompactionWarning
574579
usagePercentage={autoCompactionResult.usagePercentage}
575580
thresholdPercentage={autoCompactionResult.thresholdPercentage}
581+
onCompactClick={handleCompactClick}
576582
/>
577583
)}
578584
<ChatInput

src/browser/components/ChatInput/index.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,15 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
260260
[setInput]
261261
);
262262

263+
// Method to prepend text to input (used by manual compact trigger)
264+
const prependText = useCallback(
265+
(text: string) => {
266+
setInput((prev) => text + prev);
267+
focusMessageInput();
268+
},
269+
[focusMessageInput, setInput]
270+
);
271+
263272
// Method to restore images to input (used by queued message edit)
264273
const restoreImages = useCallback((images: ImagePart[]) => {
265274
const attachments: ImageAttachment[] = images.map((img, index) => ({
@@ -277,10 +286,19 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
277286
focus: focusMessageInput,
278287
restoreText,
279288
appendText,
289+
prependText,
280290
restoreImages,
281291
});
282292
}
283-
}, [props.onReady, focusMessageInput, restoreText, appendText, restoreImages, props]);
293+
}, [
294+
props.onReady,
295+
focusMessageInput,
296+
restoreText,
297+
appendText,
298+
prependText,
299+
restoreImages,
300+
props,
301+
]);
284302

285303
useEffect(() => {
286304
const handleGlobalKeyDown = (event: KeyboardEvent) => {

src/browser/components/ChatInput/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface ChatInputAPI {
66
focus: () => void;
77
restoreText: (text: string) => void;
88
appendText: (text: string) => void;
9+
prependText: (text: string) => void;
910
restoreImages: (images: ImagePart[]) => void;
1011
}
1112

src/browser/components/CompactionWarning.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React from "react";
44
* Warning banner shown when context usage is approaching the compaction threshold.
55
*
66
* Displays progressive warnings:
7-
* - Below threshold: "Context left until Auto-Compact: X% remaining" (where X = threshold - current)
7+
* - Below threshold: "Auto-Compact in X% usage" (where X = threshold - current)
88
* - At/above threshold: "Approaching context limit. Next message will trigger auto-compaction."
99
*
1010
* Displayed above ChatInput when:
@@ -13,10 +13,12 @@ import React from "react";
1313
*
1414
* @param usagePercentage - Current token usage as percentage (0-100)
1515
* @param thresholdPercentage - Auto-compaction trigger threshold (0-100, default 70)
16+
* @param onCompactClick - Callback when user clicks to trigger manual compaction
1617
*/
1718
export const CompactionWarning: React.FC<{
1819
usagePercentage: number;
1920
thresholdPercentage: number;
21+
onCompactClick?: () => void;
2022
}> = (props) => {
2123
// At threshold or above, next message will trigger compaction
2224
const willCompactNext = props.usagePercentage >= props.thresholdPercentage;
@@ -30,11 +32,18 @@ export const CompactionWarning: React.FC<{
3032
);
3133
}
3234

33-
// Countdown warning below threshold - subtle grey text, right-aligned
35+
// Countdown warning below threshold - subtle grey text, right-aligned, clickable
3436
const remaining = props.thresholdPercentage - props.usagePercentage;
3537
return (
36-
<div className="text-muted mx-4 mt-2 mb-1 text-right text-[10px]">
37-
Context left until Auto-Compact: {Math.round(remaining)}%
38+
<div className="mx-4 mt-2 mb-1 text-right text-[10px]">
39+
<button
40+
type="button"
41+
onClick={props.onCompactClick}
42+
className="text-muted cursor-pointer hover:underline"
43+
title="Click to insert /compact command"
44+
>
45+
Auto-Compact in {Math.round(remaining)}% usage
46+
</button>
3847
</div>
3948
);
4049
};

src/browser/components/RightSidebar/AutoCompactionSettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const AutoCompactionSettings: React.FC<AutoCompactionSettingsProps> = ({
2121
);
2222

2323
return (
24-
<div data-testid="auto-compaction-settings" className="mb-6">
24+
<div data-testid="auto-compaction-settings" className="mt-1">
2525
<div className="flex items-baseline justify-between">
2626
{/* Left side: checkbox + label + tooltip */}
2727
<div className="flex items-baseline gap-1">

src/browser/components/RightSidebar/CostsTab.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,14 @@ const CostsTabComponent: React.FC<CostsTabProps> = ({ workspaceId }) => {
226226
Unknown model limits - showing relative usage only
227227
</div>
228228
)}
229+
<AutoCompactionSettings workspaceId={workspaceId} />
229230
</>
230231
);
231232
})()}
232233
</div>
233234
</div>
234235
)}
235236

236-
{hasUsageData && <AutoCompactionSettings workspaceId={workspaceId} />}
237-
238237
{hasUsageData && (
239238
<div data-testid="cost-section" className="mb-6">
240239
<div className="flex flex-col gap-3">

0 commit comments

Comments
 (0)