Skip to content

Commit 492d2d5

Browse files
authored
🤖 Rename 'Compact Here' to 'Start Here' and add to Assistant messages (#163)
Renames 'Compact Here' to 'Start Here' and makes it available on both Plans and final Assistant messages. ## Changes **Shared utilities (DRY):** - `src/utils/startHere.ts`: Core logic for replacing chat history - `src/hooks/useStartHere.ts`: React hook for Start Here button state - `src/constants/ui.ts`: Shared `COMPACTED_EMOJI` constant (📦) **Component updates:** - **ProposePlanToolCall**: Uses shared hook, removes ~30 lines of duplicate logic - **AssistantMessage**: Adds Start Here button to final messages - **MessageWindow**: Adds `disabled` property to `ButtonConfig` **Behavior:** - Start Here button disabled when message is already compacted - Consistent emoji (📦) across compacted badge and Start Here button - Only shows on final (non-streaming) messages **Documentation:** - Updated `docs/context-management.md` to reflect new naming and availability ## Testing ```bash make build # ✓ Passes ``` _Generated with `cmux`_
1 parent 8c2fe01 commit 492d2d5

File tree

8 files changed

+302
-66
lines changed

8 files changed

+302
-66
lines changed

‎docs/context-management.md‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@ Commands for managing conversation history length and token usage.
44

55
## Comparison
66

7-
| Approach | `/clear` | `/truncate` | `/compact` | Plan Compaction |
8-
| ------------------------ | -------- | ----------- | ---------------- | --------------- |
9-
| **Speed** | Instant | Instant | Slower (uses AI) | Instant |
10-
| **Context Preservation** | None | Temporal | Intelligent | Intelligent |
11-
| **Cost** | Free | Free | Uses API tokens | Free |
12-
| **Reversible** | No | No | No | Yes |
7+
| Approach | `/clear` | `/truncate` | `/compact` | Start Here |
8+
| ------------------------ | -------- | ----------- | ---------------- | ----------- |
9+
| **Speed** | Instant | Instant | Slower (uses AI) | Instant |
10+
| **Context Preservation** | None | Temporal | Intelligent | Intelligent |
11+
| **Cost** | Free | Free | Uses API tokens | Free |
12+
| **Reversible** | No | No | No | Yes |
1313

14-
## Plan Compaction
14+
## Start Here
1515

16-
If you've produced a plan, you can opportunistically click "Compact Here" on the plan to use it
17-
as the entire conversation history. This operation is instant as all of the LLM's work was already
18-
done when it created the plan.
16+
Start Here allows you to restart your conversation from a specific point, using that message as the entire conversation history. This is available on:
1917

20-
![Plan Compaction](./img/plan-compact.webp)
18+
- **Plans** - Click "🎯 Start Here" on any plan to use it as your conversation starting point
19+
- **Final Assistant messages** - Click "🎯 Start Here" on any completed assistant response
2120

22-
This is a form of "opportunistic compaction" and is special in that you can review the post-compact
23-
context before the old context is permanently removed.
21+
![Start Here](./img/plan-compact.webp)
22+
23+
This is a form of "opportunistic compaction" - the content is already well-structured, so the operation is instant. You can review the new starting point before the old context is permanently removed, making this the only reversible context management approach (use Cmd+Z/Ctrl+Z to undo).
2424

2525
## `/clear` - Clear All History
2626

‎src/components/Messages/AssistantMessage.tsx‎

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { MarkdownRenderer } from "./MarkdownRenderer";
55
import { TypewriterMarkdown } from "./TypewriterMarkdown";
66
import type { ButtonConfig } from "./MessageWindow";
77
import { MessageWindow } from "./MessageWindow";
8+
import { useStartHere } from "@/hooks/useStartHere";
9+
import { COMPACTED_EMOJI } from "@/constants/ui";
810

911
const RawContent = styled.pre`
1012
font-family: var(--font-monospace);
@@ -52,14 +54,29 @@ const CompactedBadge = styled.span`
5254
interface AssistantMessageProps {
5355
message: DisplayedMessage & { type: "assistant" };
5456
className?: string;
57+
workspaceId?: string;
5558
}
5659

57-
export const AssistantMessage: React.FC<AssistantMessageProps> = ({ message, className }) => {
60+
export const AssistantMessage: React.FC<AssistantMessageProps> = ({
61+
message,
62+
className,
63+
workspaceId,
64+
}) => {
5865
const [showRaw, setShowRaw] = useState(false);
5966
const [copied, setCopied] = useState(false);
6067

6168
const content = message.content;
6269
const isStreaming = message.isStreaming;
70+
const isCompacted = message.isCompacted;
71+
72+
// Use Start Here hook for final assistant messages
73+
const {
74+
openModal,
75+
buttonLabel,
76+
buttonEmoji,
77+
disabled: startHereDisabled,
78+
modal,
79+
} = useStartHere(workspaceId, content, isCompacted);
6380

6481
const handleCopy = async () => {
6582
try {
@@ -75,6 +92,18 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({ message, cla
7592
const buttons: ButtonConfig[] = isStreaming
7693
? []
7794
: [
95+
// Add Start Here button if workspaceId is available and message is not already compacted
96+
...(workspaceId && !isCompacted
97+
? [
98+
{
99+
label: buttonLabel,
100+
emoji: buttonEmoji,
101+
onClick: openModal,
102+
disabled: startHereDisabled,
103+
tooltip: "Replace all chat history with this message",
104+
},
105+
]
106+
: []),
78107
{
79108
label: copied ? "✓ Copied" : "Copy Text",
80109
onClick: () => void handleCopy(),
@@ -117,20 +146,24 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({ message, cla
117146
<LabelContainer>
118147
<span>ASSISTANT</span>
119148
{modelName && <ModelName>{modelName.toLowerCase()}</ModelName>}
120-
{isCompacted && <CompactedBadge>📦 compacted</CompactedBadge>}
149+
{isCompacted && <CompactedBadge>{COMPACTED_EMOJI} compacted</CompactedBadge>}
121150
</LabelContainer>
122151
);
123152
};
124153

125154
return (
126-
<MessageWindow
127-
label={renderLabel()}
128-
borderColor="var(--color-assistant-border)"
129-
message={message}
130-
buttons={buttons}
131-
className={className}
132-
>
133-
{renderContent()}
134-
</MessageWindow>
155+
<>
156+
<MessageWindow
157+
label={renderLabel()}
158+
borderColor="var(--color-assistant-border)"
159+
message={message}
160+
buttons={buttons}
161+
className={className}
162+
>
163+
{renderContent()}
164+
</MessageWindow>
165+
166+
{modal}
167+
</>
135168
);
136169
};

‎src/components/Messages/MessageRenderer.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export const MessageRenderer = React.memo<MessageRendererProps>(
2323
case "user":
2424
return <UserMessage message={message} className={className} onEdit={onEditUserMessage} />;
2525
case "assistant":
26-
return <AssistantMessage message={message} className={className} />;
26+
return (
27+
<AssistantMessage message={message} className={className} workspaceId={workspaceId} />
28+
);
2729
case "tool":
2830
return <ToolMessage message={message} className={className} workspaceId={workspaceId} />;
2931
case "reasoning":

‎src/components/Messages/MessageWindow.tsx‎

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import styled from "@emotion/styled";
44
import type { CmuxMessage, DisplayedMessage } from "@/types/message";
55
import { HeaderButton } from "../tools/shared/ToolPrimitives";
66
import { formatTimestamp } from "@/utils/ui/dateTime";
7+
import { TooltipWrapper, Tooltip } from "../Tooltip";
78

89
const MessageBlock = styled.div<{ borderColor: string; backgroundColor?: string }>`
910
margin-bottom: 15px;
@@ -69,6 +70,9 @@ export interface ButtonConfig {
6970
label: string;
7071
onClick: () => void;
7172
active?: boolean;
73+
disabled?: boolean;
74+
emoji?: string; // Optional emoji that shows only on hover
75+
tooltip?: string; // Optional tooltip text
7276
}
7377

7478
interface MessageWindowProps {
@@ -113,11 +117,25 @@ export const MessageWindow: React.FC<MessageWindowProps> = ({
113117
</LeftSection>
114118
<ButtonGroup>
115119
{rightLabel}
116-
{buttons.map((button, index) => (
117-
<HeaderButton key={index} active={button.active} onClick={button.onClick}>
118-
{button.label}
119-
</HeaderButton>
120-
))}
120+
{buttons.map((button, index) =>
121+
button.tooltip ? (
122+
<TooltipWrapper key={index} inline>
123+
<ButtonWithHoverEmoji
124+
button={button}
125+
active={button.active}
126+
disabled={button.disabled}
127+
/>
128+
<Tooltip align="center">{button.tooltip}</Tooltip>
129+
</TooltipWrapper>
130+
) : (
131+
<ButtonWithHoverEmoji
132+
key={index}
133+
button={button}
134+
active={button.active}
135+
disabled={button.disabled}
136+
/>
137+
)
138+
)}
121139
<HeaderButton active={showJson} onClick={() => setShowJson(!showJson)}>
122140
{showJson ? "Hide JSON" : "Show JSON"}
123141
</HeaderButton>
@@ -129,3 +147,31 @@ export const MessageWindow: React.FC<MessageWindowProps> = ({
129147
</MessageBlock>
130148
);
131149
};
150+
151+
// Button component that shows emoji only on hover
152+
interface ButtonWithHoverEmojiProps {
153+
button: ButtonConfig;
154+
active?: boolean;
155+
disabled?: boolean;
156+
}
157+
158+
const ButtonWithHoverEmoji: React.FC<ButtonWithHoverEmojiProps> = ({
159+
button,
160+
active,
161+
disabled,
162+
}) => {
163+
const [isHovered, setIsHovered] = useState(false);
164+
165+
return (
166+
<HeaderButton
167+
active={active}
168+
onClick={button.onClick}
169+
disabled={disabled}
170+
onMouseEnter={() => setIsHovered(true)}
171+
onMouseLeave={() => setIsHovered(false)}
172+
>
173+
{button.emoji && isHovered && <span style={{ marginRight: "4px" }}>{button.emoji}</span>}
174+
{button.label}
175+
</HeaderButton>
176+
);
177+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { useState, useCallback } from "react";
2+
import styled from "@emotion/styled";
3+
import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal";
4+
5+
const CenteredActions = styled(ModalActions)`
6+
justify-content: center;
7+
`;
8+
9+
interface StartHereModalProps {
10+
isOpen: boolean;
11+
onClose: () => void;
12+
onConfirm: () => void | Promise<void>;
13+
}
14+
15+
export const StartHereModal: React.FC<StartHereModalProps> = ({ isOpen, onClose, onConfirm }) => {
16+
const [isExecuting, setIsExecuting] = useState(false);
17+
18+
const handleCancel = useCallback(() => {
19+
if (!isExecuting) {
20+
onClose();
21+
}
22+
}, [isExecuting, onClose]);
23+
24+
const handleConfirm = useCallback(async () => {
25+
if (isExecuting) return;
26+
setIsExecuting(true);
27+
try {
28+
await onConfirm();
29+
onClose();
30+
} catch (error) {
31+
console.error("Start Here error:", error);
32+
setIsExecuting(false);
33+
}
34+
}, [isExecuting, onConfirm, onClose]);
35+
36+
return (
37+
<Modal
38+
isOpen={isOpen}
39+
title="Start Here"
40+
subtitle="This will replace all chat history with this message"
41+
onClose={handleCancel}
42+
isLoading={isExecuting}
43+
>
44+
<CenteredActions>
45+
<CancelButton onClick={handleCancel} disabled={isExecuting}>
46+
Cancel
47+
</CancelButton>
48+
<PrimaryButton onClick={() => void handleConfirm()} disabled={isExecuting}>
49+
{isExecuting ? "Starting..." : "OK"}
50+
</PrimaryButton>
51+
</CenteredActions>
52+
</Modal>
53+
);
54+
};

‎src/components/tools/ProposePlanToolCall.tsx‎

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ 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";
15+
import { useStartHere } from "@/hooks/useStartHere";
16+
import { TooltipWrapper, Tooltip } from "../Tooltip";
1617

1718
const PlanContainer = styled.div`
1819
padding: 12px;
@@ -251,7 +252,22 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
251252
const { expanded, toggleExpanded } = useToolExpansion(true); // Expand by default
252253
const [showRaw, setShowRaw] = useState(false);
253254
const [copied, setCopied] = useState(false);
254-
const [isCompacting, setIsCompacting] = useState(false);
255+
256+
// Format: Title as H1 + plan content for "Start Here" functionality
257+
const startHereContent = `# ${args.title}\n\n${args.plan}`;
258+
const {
259+
openModal,
260+
buttonLabel,
261+
buttonEmoji,
262+
disabled: startHereDisabled,
263+
modal,
264+
} = useStartHere(
265+
workspaceId,
266+
startHereContent,
267+
false // Plans are never already compacted
268+
);
269+
270+
const [isHovered, setIsHovered] = useState(false);
255271

256272
const statusDisplay = getStatusDisplay(status);
257273

@@ -265,37 +281,6 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
265281
}
266282
};
267283

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-
299284
return (
300285
<ToolContainer expanded={expanded}>
301286
<ToolHeader onClick={toggleExpanded}>
@@ -314,9 +299,18 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
314299
</PlanHeaderLeft>
315300
<PlanHeaderRight>
316301
{workspaceId && (
317-
<PlanButton onClick={() => void handleCompactHere()} disabled={isCompacting}>
318-
{isCompacting ? "Compacting..." : "📦 Compact Here"}
319-
</PlanButton>
302+
<TooltipWrapper inline>
303+
<PlanButton
304+
onClick={openModal}
305+
disabled={startHereDisabled}
306+
onMouseEnter={() => setIsHovered(true)}
307+
onMouseLeave={() => setIsHovered(false)}
308+
>
309+
{isHovered && <span style={{ marginRight: "4px" }}>{buttonEmoji}</span>}
310+
{buttonLabel}
311+
</PlanButton>
312+
<Tooltip align="center">Replace all chat history with this plan</Tooltip>
313+
</TooltipWrapper>
320314
)}
321315
<PlanButton onClick={() => void handleCopy()}>
322316
{copied ? "✓ Copied" : "Copy"}
@@ -345,6 +339,8 @@ export const ProposePlanToolCall: React.FC<ProposePlanToolCallProps> = ({
345339
</PlanContainer>
346340
</ToolDetails>
347341
)}
342+
343+
{modal}
348344
</ToolContainer>
349345
);
350346
};

‎src/constants/ui.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* UI-related constants shared across components
3+
*/
4+
5+
/**
6+
* Emoji used for compacted/start-here functionality throughout the app.
7+
* Used in:
8+
* - AssistantMessage compacted badge
9+
* - Start Here button (plans and assistant messages)
10+
*/
11+
export const COMPACTED_EMOJI = "📦";

0 commit comments

Comments
 (0)