Skip to content

Commit d62c282

Browse files
authored
🤖 Add 'Compact Here' button to Plan Results (#122)
Adds a compact button to plan tool results that allows users to start fresh from a proposed plan. When clicked, replaces conversation history with a single compacted message containing the plan title (as H1) and full plan content. The plan's markdown formatting is preserved, making it readable and useful as a conversation starting point. ## Changes - Thread `workspaceId` through MessageRenderer → ToolMessage → ProposePlanToolCall - Add "Compact Here" button to plan header (conditionally rendered when workspaceId available) - Implement `handleCompactHere()` handler that creates compacted CmuxMessage and calls `replaceChatHistory()` - Button shows "Compacting..." during operation and is disabled to prevent double-clicks ## Implementation Reuses existing `/compact` infrastructure: - `window.api.workspace.replaceChatHistory()` API - `createCmuxMessage()` factory - `compacted` metadata flag (shows 📦 badge) 4 files modified, 51 lines added. All type checks, lints, and builds pass. _Generated with `cmux`_
1 parent 01229ca commit d62c282

File tree

5 files changed

+55
-4
lines changed

5 files changed

+55
-4
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,7 @@ profile.txt
7979
src/version.ts
8080
OPENAI_FIX_SUMMARY.md
8181
docs/vercel/
82+
TESTING.md
83+
FEATURE_SUMMARY.md
84+
CODE_CHANGES.md
85+
README_COMPACT_HERE.md

src/components/AIView.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
371371

372372
return (
373373
<React.Fragment key={msg.id}>
374-
<MessageRenderer message={msg} onEditUserMessage={handleEditUserMessage} />
374+
<MessageRenderer
375+
message={msg}
376+
onEditUserMessage={handleEditUserMessage}
377+
workspaceId={workspaceId}
378+
/>
375379
{isAtCutoff && (
376380
<EditBarrier>
377381
⚠️ Messages below this line will be removed when you submit the edit

src/components/Messages/MessageRenderer.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,20 @@ interface MessageRendererProps {
1111
message: DisplayedMessage;
1212
className?: string;
1313
onEditUserMessage?: (messageId: string, content: string) => void;
14+
workspaceId?: string;
1415
}
1516

1617
// Memoized to prevent unnecessary re-renders when parent (AIView) updates
1718
export const MessageRenderer = React.memo<MessageRendererProps>(
18-
({ message, className, onEditUserMessage }) => {
19+
({ message, className, onEditUserMessage, workspaceId }) => {
1920
// Route based on message type
2021
switch (message.type) {
2122
case "user":
2223
return <UserMessage message={message} className={className} onEdit={onEditUserMessage} />;
2324
case "assistant":
2425
return <AssistantMessage message={message} className={className} />;
2526
case "tool":
26-
return <ToolMessage message={message} className={className} />;
27+
return <ToolMessage message={message} className={className} workspaceId={workspaceId} />;
2728
case "reasoning":
2829
return <ReasoningMessage message={message} className={className} />;
2930
case "stream-error":

src/components/Messages/ToolMessage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
interface ToolMessageProps {
2222
message: DisplayedMessage & { type: "tool" };
2323
className?: string;
24+
workspaceId?: string;
2425
}
2526

2627
// Type guard for bash tool
@@ -71,7 +72,7 @@ function isProposePlanTool(toolName: string, args: unknown): args is ProposePlan
7172
return toolName === "propose_plan" && typeof args === "object" && args !== null && "plan" in args;
7273
}
7374

74-
export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className }) => {
75+
export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className, workspaceId }) => {
7576
// Route to specialized components based on tool name
7677
if (isBashTool(message.toolName, message.args)) {
7778
return (
@@ -130,6 +131,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({ message, className })
130131
args={message.args}
131132
result={message.result as ProposePlanToolResult | undefined}
132133
status={message.status}
134+
workspaceId={workspaceId}
133135
/>
134136
</div>
135137
);

src/components/tools/ProposePlanToolCall.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils";
1313
import { MarkdownRenderer } from "../Messages/MarkdownRenderer";
1414
import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
15+
import { createCmuxMessage } from "@/types/message";
1516

1617
const PlanContainer = styled.div`
1718
padding: 12px;
@@ -238,16 +239,19 @@ interface ProposePlanToolCallProps {
238239
args: ProposePlanToolArgs;
239240
result?: ProposePlanToolResult;
240241
status?: ToolStatus;
242+
workspaceId?: string;
241243
}
242244

243245
export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
244246
args,
245247
result: _result,
246248
status = "pending",
249+
workspaceId,
247250
}) => {
248251
const { expanded, toggleExpanded } = useToolExpansion(true); // Expand by default
249252
const [showRaw, setShowRaw] = useState(false);
250253
const [copied, setCopied] = useState(false);
254+
const [isCompacting, setIsCompacting] = useState(false);
251255

252256
const statusDisplay = getStatusDisplay(status);
253257

@@ -261,6 +265,37 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
261265
}
262266
};
263267

268+
const handleCompactHere = async () => {
269+
if (!workspaceId || isCompacting) return;
270+
271+
setIsCompacting(true);
272+
try {
273+
// Create a compacted message with the plan content
274+
// Format: Title as H1 + plan content
275+
const compactedContent = `# ${args.title}\n\n${args.plan}`;
276+
277+
const summaryMessage = createCmuxMessage(
278+
`compact-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
279+
"assistant",
280+
compactedContent,
281+
{
282+
timestamp: Date.now(),
283+
compacted: true,
284+
}
285+
);
286+
287+
const result = await window.api.workspace.replaceChatHistory(workspaceId, summaryMessage);
288+
289+
if (!result.success) {
290+
console.error("Failed to compact:", result.error);
291+
}
292+
} catch (err) {
293+
console.error("Compact error:", err);
294+
} finally {
295+
setIsCompacting(false);
296+
}
297+
};
298+
264299
return (
265300
<ToolContainer expanded={expanded}>
266301
<ToolHeader onClick={toggleExpanded}>
@@ -278,6 +313,11 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
278313
<PlanTitle>{args.title}</PlanTitle>
279314
</PlanHeaderLeft>
280315
<PlanHeaderRight>
316+
{workspaceId && (
317+
<PlanButton onClick={() => void handleCompactHere()} disabled={isCompacting}>
318+
{isCompacting ? "Compacting..." : "📦 Compact Here"}
319+
</PlanButton>
320+
)}
281321
<PlanButton onClick={() => void handleCopy()}>
282322
{copied ? "✓ Copied" : "Copy"}
283323
</PlanButton>

0 commit comments

Comments
 (0)