Commit 385830d
authored
🤖 perf: context-efficient plan mode (#1072)
## Summary
Optimizes context usage in plan mode by:
1. Removing redundant `planContent` from `propose_plan` tool results
(plan is already visible via `file_edit_*` diffs)
2. Including the full plan in the mode transition message when switching
plan → exec
## Changes
- **`propose_plan` tool**: No longer returns `planContent` in the
result, saving context during iterative planning sessions
- **Mode transition (plan → exec)**: Now includes the full plan with
soft framing: "evaluate whether it's relevant to the user's request"
- **UI**: `ProposePlanToolCall` fetches content on-demand for the latest
plan; shows path info for historical plans without embedded content
- **Backwards compatibility**: Old chat history with `planContent` in
results still renders correctly
## Context Flow
| Phase | Before | After |
|-------|--------|-------|
| During planning | Plan in `file_edit_*` diffs + full plan in
`propose_plan` result | Plan in `file_edit_*` diffs only |
| On mode switch | Generic "mode switched" message | Full plan with
"evaluate relevance" framing |
## Testing
- Added 4 new tests for plan content in mode transitions
- All existing tests pass
---
<details>
<summary>Plan</summary>
# Plan: Context-Efficient Plan Mode
## Summary
Improve the plan mode → exec mode transition to include the approved
plan content in the model's context only when relevant, avoiding
redundant context when the plan is already visible in conversation
history or no plan was created.
## Current Behavior
### How Plan Mode Works Now
1. **System Prompt**: When in plan mode, `getPlanModeInstruction()` adds
instructions telling the model to write its plan to
`~/.mux/plans/{workspaceId}.md`
2. **propose_plan Tool**: When called, reads the plan file from disk and
returns:
```typescript
{ success: true, planPath, planContent, message: "Plan proposed. Waiting
for user approval." }
```
This content is stored in the tool result in chat history — **this is
redundant** since the plan was already written via `file_edit_*` calls.
3. **Mode Transition**: When switching from plan → exec,
`injectModeTransition()` inserts a synthetic user message:
```
[Mode switched from plan to exec. Follow exec mode instructions.
Available tools: file_read, bash, ...]
```
### Key Observation
The plan content is duplicated in multiple places:
1. **`file_edit_*` tool calls** - The actual writes/edits to the plan
file (as diffs)
2. **`propose_plan` tool result** - The full plan content (redundant!)
3. **Not included** - The mode transition message when switching to exec
For iterative planning sessions, this means the plan content appears
multiple times, wasting context:
- Each plan revision has `file_edit_*` diffs ✓ (necessary, minimal)
- Each `propose_plan` call duplicates the full content ✗ (redundant)
- Final plan isn't surfaced when switching to exec ✗ (missing)
## Problem Statement
When the user switches from plan mode to exec mode (implicitly by
changing the mode selector), the model:
1. Doesn't receive explicit confirmation that the plan was approved
2. May not realize the plan from earlier in the conversation is what it
should execute
3. Has to infer relevance from context rather than being told explicitly
## Proposed Solution
Enhance the mode transition injection to optionally include the approved
plan content when switching from plan → exec mode.
### Design Principles
1. **Only include plan when relevant** - The model should determine if
the plan applies to the current user message
2. **Avoid redundancy** - Don't include plan if it's already visible in
recent context
3. **Implicit approval** - Mux's mode switching is implicit (no explicit
approve/reject), so we frame it as "plan available for reference"
4. **Graceful fallback** - If no plan file exists, proceed without it
### Implementation
#### 1. Modify `injectModeTransition()` to accept plan content (~20 LoC)
**File**: `src/browser/utils/messages/modelMessageTransform.ts`
Add an optional `planContent` parameter that gets included when
transitioning plan → exec:
```typescript
export function injectModeTransition(
messages: MuxMessage[],
currentMode?: string,
toolNames?: string[],
planContent?: string // NEW: optional plan content for plan→exec transition
): MuxMessage[] {
// ... existing logic ...
// If transitioning from plan to exec AND plan content provided
if (lastMode === "plan" && currentMode === "exec" && planContent) {
transitionText += `
The following plan was developed in plan mode. Evaluate whether it's relevant to the user's request. If relevant, use it to guide your implementation:
<approved-plan>
${planContent}
</approved-plan>`;
}
// ... rest of function ...
}
```
#### 2. Read plan content during stream preparation (~15 LoC)
**File**: `src/node/services/aiService.ts`
Before calling `injectModeTransition`, check if we're transitioning
plan→exec and read the plan file:
```typescript
// In prepareStream(), around line 959:
let planContentForTransition: string | undefined;
if (mode === "exec") {
// Check if last assistant message was in plan mode
const lastAssistantMessage = [...filteredMessages].reverse().find(m => m.role === "assistant");
if (lastAssistantMessage?.metadata?.mode === "plan") {
// Read plan file for transition context
const planFilePath = getPlanFilePath(workspaceId);
try {
planContentForTransition = await readFileString(runtime, planFilePath);
} catch {
// No plan file, proceed without
}
}
}
const messagesWithModeContext = injectModeTransition(
messagesWithSentinel,
mode,
toolNamesForSentinel,
planContentForTransition // NEW parameter
);
```
#### 3. Update test coverage (~40 LoC)
**File**: `src/browser/utils/messages/modelMessageTransform.test.ts`
Add tests for:
- Plan content included when transitioning plan→exec with plan content
- Plan content NOT included when transitioning exec→plan
- Plan content NOT included when no plan content provided
- Plan content NOT included when staying in same mode
### Alternative Considered: Include in System Prompt
We could add the plan content to the system prompt during exec mode.
However:
- This would bust the system message cache on every plan change
- Less contextually appropriate (plans are conversation-specific, not
workspace-wide)
- Mode transition injection is already the established pattern for
mode-switching context
### Edge Cases
1. **No plan file exists**: Skip including plan content, use existing
transition message
2. **Empty plan file**: Treat same as no plan
3. **Plan from previous conversation**: If user switches modes without a
plan in current conversation, no plan content is included
4. **Very long plans**: Consider truncating after N characters with "...
(plan truncated, see ~/.mux/plans/...)"
#### 4. Remove planContent from propose_plan tool result (~5 LoC)
**File**: `src/node/services/tools/propose_plan.ts`
The tool currently returns the full plan content in the result. Since:
- The plan is already in history via `file_edit_*` tool calls (as diffs)
- The plan will be included in the mode transition message when
switching to exec
We can exclude it from the tool result to save context during iterative
planning:
```typescript
// Before:
return {
success: true as const,
planPath,
planContent, // REMOVE THIS
message: "Plan proposed. Waiting for user approval.",
};
// After:
return {
success: true as const,
planPath,
message: "Plan proposed. Waiting for user approval.",
};
```
#### 5. Update ProposePlanToolCall UI to fetch content on demand (~10
LoC)
**File**: `src/browser/components/tools/ProposePlanToolCall.tsx`
The UI component already has logic to fetch fresh content via
`getPlanContent` for the latest plan. We need to ensure it falls back to
this API call when `planContent` is not in the result:
```typescript
// The component already handles this case - when isLatest is true AND freshContent is fetched
// For historical plans (not latest), we can either:
// 1. Fetch on demand when expanded (lazy load)
// 2. Show "Plan content not available" with option to fetch
```
Since the UI already prioritizes `freshContent` from disk for the latest
plan, this mostly works. For historical plans, we should show a minimal
message indicating the plan exists at the path.
## Estimated LoC Changes
| File | Change | LoC |
|------|--------|-----|
| `modelMessageTransform.ts` | Add planContent parameter | +20 |
| `aiService.ts` | Read plan on transition | +15 |
| `propose_plan.ts` | Remove planContent from result | -3 |
| `ProposePlanToolCall.tsx` | Handle missing planContent | +10 |
| `modelMessageTransform.test.ts` | New test cases | +40 |
| **Total** | | **~82** |
## Design Decisions
1. **No truncation** - Include the full plan content. Can revisit if
context limits become an issue.
2. **No "outdated" marking** - The existing file change notification
system already handles external edits with diffs.
3. **Soft framing** - Use "developed in plan mode, evaluate relevance"
rather than "approved" since Mux's approval is implicit.
</details>
---
_Generated with [mux](https://github.com/coder/mux)_
Signed-off-by: Thomas Kosiewski <[email protected]>1 parent 92fd55f commit 385830d
File tree
6 files changed
+272
-63
lines changed- src
- browser
- components/tools
- utils/messages
- common/types
- node/services
- tools
6 files changed
+272
-63
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
38 | | - | |
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
43 | 50 | | |
44 | 51 | | |
45 | 52 | | |
| |||
173 | 180 | | |
174 | 181 | | |
175 | 182 | | |
176 | | - | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
177 | 186 | | |
178 | | - | |
179 | | - | |
180 | | - | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
181 | 197 | | |
182 | 198 | | |
183 | 199 | | |
| |||
Lines changed: 149 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
874 | 874 | | |
875 | 875 | | |
876 | 876 | | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
877 | 1026 | | |
878 | 1027 | | |
879 | 1028 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
115 | 118 | | |
116 | 119 | | |
117 | 120 | | |
| 121 | + | |
118 | 122 | | |
119 | 123 | | |
120 | 124 | | |
121 | 125 | | |
122 | 126 | | |
123 | | - | |
| 127 | + | |
| 128 | + | |
124 | 129 | | |
125 | 130 | | |
126 | 131 | | |
| |||
175 | 180 | | |
176 | 181 | | |
177 | 182 | | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
178 | 194 | | |
179 | 195 | | |
180 | 196 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
173 | | - | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
174 | 176 | | |
175 | 177 | | |
176 | 178 | | |
177 | | - | |
178 | 179 | | |
179 | 180 | | |
180 | 181 | | |
| |||
0 commit comments