Skip to content

Commit 698d0b0

Browse files
authored
🤖 refactor: remove /truncate slash command (#3332)
## Summary Removes the `/truncate <percentage>` slash command from every user-facing surface (browser registry, ACP, mobile, palette ghost-text hints, docs, placeholder tips). The underlying `workspace.truncateHistory` IPC, the `onTruncateHistory` callback chain, and the **Truncate History to 25/50/75%** command-palette entries remain in place — they back `/clear` and the palette and aren't reachable via the removed slash command. ## Background `/truncate` was a free-form, percentage-based destructive command sitting next to `/clear`, `/compact`, and the palette entries. Its UX was awkward (the user had to know what percentage they wanted, then re-confirm in the destructive modal), it didn't work with the OpenAI Responses API at all, and the palette already exposes fixed 25/50/75% truncation as discrete actions for users who want bounded history pruning. The slash command itself was redundant. ## Implementation Removed the command at every layer that referenced it explicitly: - **Browser slash command system** (`registry.ts`, `types.ts`, `parser_multiline.test.ts`) — dropped `truncateCommandDefinition`, the `{ type: "truncate"; percentage: number }` union member, and the parser test. - **Command dispatch** (`chatCommands.ts`) — removed the `case "truncate":` switch arm and `handleTruncateCommand`. - **ChatInput destructive-confirmation modal** — simplified `pendingDestructiveCommand` from a discriminated `{ type, percentage }` object to a plain `boolean`. `/clear` is now the only path through the modal, so the title/description/confirm label are hard-coded. - **ACP integration** (`acp/slashCommands.ts`, `acp/agent.ts`, `tests/ipc/acp.slashCommands.test.ts`) — removed `TRUNCATE_COMMAND_NAME`, the server command definition, the `kind: "truncate"` union member, `parseTruncateCommand`, the agent dispatch case, and the corresponding test cases. - **Shared constants** (`constants/slashCommands.ts`, `common/constants/slashCommandHints.ts`) — removed `"truncate"` from the workspace-only key/type sets and dropped the `truncate` hint. - **Mobile** (`mobile/src/utils/slashCommandRunner.ts`) — removed the `case "truncate":` branch and renamed the now-clear-only `handleTruncate` helper to `handleClear`, dropping the dead `percentage < 1` branch. - **Discovery surfaces** — removed the `/truncate 50` placeholder tip from the carousel and the e2e scenario that exercised the command; the review e2e now uses `/clear`. - **Docs** (`docs/workspaces/compaction/manual.mdx`, `index.mdx`) — removed the "Truncate (Simple Truncation)" section and the table row in the approaches table; regenerated `builtInSkillContent.generated.ts` via `bun scripts/gen_builtin_skills.ts`. Stale references to `/truncate` in code comments (`aiService.ts`, `acp/agent.ts`) were also updated. ## Validation - `make typecheck`, `make lint`, `make fmt-check`, `make static-check` — green. - Targeted unit tests: `tests/ipc/acp.slashCommands.test.ts`, `src/browser/utils/slashCommands/parser_multiline.test.ts`, `src/browser/utils/chatCommands.test.ts`, `make test-mobile` — all passing. - `make test` — 21 pre-existing `GitStatusStore` failures reproduce identically on `main` without these changes; unrelated to this PR. ## Risks Low. Three call sites that look similar to `/truncate` are intentionally preserved: 1. `workspace.truncateHistory` IPC — still used by `/clear` (calls with `percentage: 1.0`) and the palette **Truncate History to N%** actions. 2. `onTruncateHistory` callback drilled through `ChatPane` → `ChatInput` — still used by `/clear`. 3. The destructive-confirmation modal is now `/clear`-only; the previously dynamic title/description are hard-coded. The only behavioral change visible to users is that typing `/truncate ...` now falls through to the "Unknown command" path instead of executing. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `max` • Cost: `$8.53`_ <!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=8.53 -->
1 parent b88fd12 commit 698d0b0

18 files changed

Lines changed: 37 additions & 357 deletions

File tree

docs/workspaces/compaction/index.mdx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ As conversations grow, they consume more of the model's context window. Compacti
77

88
## Approaches
99

10-
| Approach | Speed | Context Preservation | Cost | Reversible |
11-
| ----------------------------------------------------------------------- | ---------------- | -------------------- | --------------- | ---------- |
12-
| [Start Here](/workspaces/compaction/manual#start-here) | Instant | Intelligent | Free | Yes |
13-
| [`/compact`](/workspaces/compaction/manual#compact-ai-summarization) | Slower (uses AI) | Intelligent | Uses API tokens | No |
14-
| [`/clear`](/workspaces/compaction/manual#clear-all-history) | Instant | None | Free | No |
15-
| [`/truncate`](/workspaces/compaction/manual#truncate-simple-truncation) | Instant | Temporal | Free | No |
16-
| [Auto-Compaction](/workspaces/compaction/automatic) | Automatic | Intelligent | Uses API tokens | No |
10+
| Approach | Speed | Context Preservation | Cost | Reversible |
11+
| -------------------------------------------------------------------- | ---------------- | -------------------- | --------------- | ---------- |
12+
| [Start Here](/workspaces/compaction/manual#start-here) | Instant | Intelligent | Free | Yes |
13+
| [`/compact`](/workspaces/compaction/manual#compact-ai-summarization) | Slower (uses AI) | Intelligent | Uses API tokens | No |
14+
| [`/clear`](/workspaces/compaction/manual#clear-all-history) | Instant | None | Free | No |
15+
| [Auto-Compaction](/workspaces/compaction/automatic) | Automatic | Intelligent | Uses API tokens | No |
1716

1817
## When to compact
1918

docs/workspaces/compaction/manual.mdx

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -109,52 +109,3 @@ Remove all messages from conversation history.
109109
- Instant deletion of all messages
110110
- **Irreversible** — all history is permanently removed
111111
- Use when you want to start a completely new conversation
112-
113-
---
114-
115-
## Truncate (Simple Truncation)
116-
117-
Remove a percentage of messages from conversation history (from the oldest first).
118-
119-
### Syntax
120-
121-
```
122-
/truncate <percentage>
123-
```
124-
125-
### Parameters
126-
127-
- `percentage` (required) — Percentage of messages to remove (0-100)
128-
129-
### Examples
130-
131-
```
132-
/truncate 50
133-
```
134-
135-
Remove oldest 50% of messages.
136-
137-
### Notes
138-
139-
- Simple deletion, no AI involved
140-
- Removes messages from oldest to newest
141-
- About as fast as `/clear`
142-
- `/truncate 100` is equivalent to `/clear`
143-
- **Irreversible** — messages are permanently removed
144-
145-
### OpenAI Responses API Limitation
146-
147-
<Warning>
148-
`/truncate` does not work with OpenAI models due to the Responses API architecture:
149-
</Warning>
150-
151-
- OpenAI's Responses API stores conversation state server-side
152-
- Manual message deletion via `/truncate` doesn't affect the server-side state
153-
- Instead, OpenAI models use **automatic truncation** (`truncation: "auto"`)
154-
- When context exceeds the limit, the API automatically drops messages from the middle of the conversation
155-
156-
**Workarounds for OpenAI:**
157-
158-
- Use `/clear` to start a fresh conversation
159-
- Use `/compact` to intelligently summarize and reduce context
160-
- Rely on automatic truncation (enabled by default)

mobile/src/utils/slashCommandRunner.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export async function executeSlashCommand(
3434

3535
switch (parsed.type) {
3636
case "clear":
37-
return handleTruncate(ctx, 1);
37+
return handleClear(ctx);
3838
case "compact":
3939
return handleCompaction(ctx, parsed);
4040
case "model-set":
@@ -57,8 +57,6 @@ export async function executeSlashCommand(
5757
return true;
5858
case "new":
5959
return handleNew(ctx, parsed);
60-
case "truncate":
61-
return handleTruncate(ctx, parsed.percentage);
6260
case "idle-compaction":
6361
return handleIdleCompaction(ctx, parsed.hours);
6462
case "plan-show":
@@ -84,23 +82,18 @@ function ensureWorkspaceId(ctx: SlashCommandRunnerContext): string {
8482
return ctx.workspaceId;
8583
}
8684

87-
async function handleTruncate(
88-
ctx: SlashCommandRunnerContext,
89-
percentage: number
90-
): Promise<boolean> {
85+
async function handleClear(ctx: SlashCommandRunnerContext): Promise<boolean> {
9186
try {
9287
const workspaceId = ensureWorkspaceId(ctx);
93-
const result = await ctx.client.workspace.truncateHistory({ workspaceId, percentage });
88+
// /clear maps to a 100% truncate at the backend.
89+
const result = await ctx.client.workspace.truncateHistory({ workspaceId, percentage: 1 });
9490
if (!result.success) {
95-
ctx.showError("History", result.error ?? "Failed to truncate history");
91+
ctx.showError("History", result.error ?? "Failed to clear history");
9692
return true;
9793
}
9894
ctx.onClearTimeline();
9995
ctx.onCancelEdit();
100-
ctx.showInfo(
101-
"History",
102-
percentage >= 1 ? "Cleared conversation" : `Truncated to ${(percentage * 100).toFixed(0)}%`
103-
);
96+
ctx.showInfo("History", "Cleared conversation");
10497
return true;
10598
} catch (error) {
10699
ctx.showError("History", getErrorMessage(error));

src/browser/features/ChatInput/index.tsx

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -364,11 +364,8 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
364364
const [commandSuggestions, setCommandSuggestions] = useState<SlashSuggestion[]>([]);
365365
const [agentSkillDescriptors, setAgentSkillDescriptors] = useState<AgentSkillDescriptor[]>([]);
366366
const [toast, setToast] = useState<Toast | null>(null);
367-
// State for destructive command confirmation modal
368-
const [pendingDestructiveCommand, setPendingDestructiveCommand] = useState<{
369-
type: "clear" | "truncate";
370-
percentage?: number;
371-
} | null>(null);
367+
// State for destructive command confirmation modal (currently only /clear).
368+
const [pendingDestructiveCommand, setPendingDestructiveCommand] = useState(false);
372369
const pushToast = useCallback(
373370
(nextToast: Omit<Toast, "id" | "type"> & { type: Toast["type"] | "info" }) => {
374371
// Keep a dedicated "info" intent for callsites while rendering with the shared non-error toast style.
@@ -1925,13 +1922,9 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
19251922
return false;
19261923
}
19271924

1928-
const isDestructive =
1929-
(parsed.type === "clear" && parsed.mode === "hard") || parsed.type === "truncate";
1925+
const isDestructive = parsed.type === "clear" && parsed.mode === "hard";
19301926
if (isDestructive && variant === "workspace" && !options?.skipConfirmation) {
1931-
setPendingDestructiveCommand({
1932-
type: parsed.type,
1933-
percentage: parsed.type === "truncate" ? parsed.percentage : undefined,
1934-
});
1927+
setPendingDestructiveCommand(true);
19351928
return true;
19361929
}
19371930

@@ -2006,24 +1999,18 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
20061999
return true;
20072000
};
20082001

2009-
// Handle destructive command confirmation
2002+
// Handle destructive command confirmation (currently only /clear).
20102003
const handleDestructiveCommandConfirm = async () => {
20112004
if (!pendingDestructiveCommand || variant !== "workspace") return;
20122005

2013-
const parsedCommand: ParsedCommand =
2014-
pendingDestructiveCommand.type === "clear"
2015-
? { type: "clear", mode: "hard" }
2016-
: {
2017-
type: "truncate",
2018-
percentage: pendingDestructiveCommand.percentage ?? 0,
2019-
};
2006+
const parsedCommand: ParsedCommand = { type: "clear", mode: "hard" };
20202007

2021-
setPendingDestructiveCommand(null);
2008+
setPendingDestructiveCommand(false);
20222009
await executeParsedCommand(parsedCommand, input, { skipConfirmation: true });
20232010
};
20242011

20252012
const handleDestructiveCommandCancel = useCallback(() => {
2026-
setPendingDestructiveCommand(null);
2013+
setPendingDestructiveCommand(false);
20272014
}, []);
20282015

20292016
// Handle drag over to allow drop
@@ -3128,21 +3115,13 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
31283115
</div>
31293116
</div>
31303117

3131-
{/* Confirmation modal for destructive commands */}
3118+
{/* Confirmation modal for destructive commands (currently only /clear). */}
31323119
<ConfirmationModal
3133-
isOpen={pendingDestructiveCommand !== null}
3134-
title={
3135-
pendingDestructiveCommand?.type === "clear"
3136-
? "Clear Chat History?"
3137-
: `Truncate ${Math.round((pendingDestructiveCommand?.percentage ?? 0) * 100)}% of Chat History?`
3138-
}
3139-
description={
3140-
pendingDestructiveCommand?.type === "clear"
3141-
? "This will remove all messages from the conversation."
3142-
: `This will remove approximately ${Math.round((pendingDestructiveCommand?.percentage ?? 0) * 100)}% of the oldest messages.`
3143-
}
3120+
isOpen={pendingDestructiveCommand}
3121+
title="Clear Chat History?"
3122+
description="This will remove all messages from the conversation."
31443123
warning="This action cannot be undone."
3145-
confirmLabel={pendingDestructiveCommand?.type === "clear" ? "Clear" : "Truncate"}
3124+
confirmLabel="Clear"
31463125
onConfirm={handleDestructiveCommandConfirm}
31473126
onCancel={handleDestructiveCommandCancel}
31483127
/>

src/browser/features/ChatInput/placeholderTips.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ export const PLACEHOLDER_TIPS: readonly string[] = [
5050
"Try /clear --soft to reset context while keeping the chat visible",
5151
"Try /new <start> to start a fresh workspace from the trunk branch",
5252
"Try /vim to toggle vim keybindings in the chat input",
53-
"Try /truncate 50 to drop the oldest half of the conversation",
5453
];
5554

5655
/**

src/browser/utils/chatCommands.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,6 @@ export async function processSlashCommand(
508508
switch (parsed.type) {
509509
case "clear":
510510
return handleClearCommand(parsed, context);
511-
case "truncate":
512-
return handleTruncateCommand(parsed, context);
513511
case "compact":
514512
// handleCompactCommand expects workspaceId in context
515513
if (!context.workspaceId) throw new Error("Workspace ID required");
@@ -982,37 +980,6 @@ async function handleClearCommand(
982980
}
983981
}
984982

985-
async function handleTruncateCommand(
986-
parsed: Extract<ParsedCommand, { type: "truncate" }>,
987-
context: SlashCommandContext
988-
): Promise<CommandHandlerResult> {
989-
const { setInput, onTruncateHistory, resetInputHeight, setToast } = context;
990-
991-
setInput("");
992-
resetInputHeight();
993-
994-
if (!onTruncateHistory) return { clearInput: true, toastShown: false };
995-
996-
try {
997-
await onTruncateHistory(parsed.percentage);
998-
setToast({
999-
id: Date.now().toString(),
1000-
type: "success",
1001-
message: `Chat history truncated by ${Math.round(parsed.percentage * 100)}%`,
1002-
});
1003-
return { clearInput: true, toastShown: true };
1004-
} catch (error) {
1005-
const normalized = error instanceof Error ? error : new Error("Failed to truncate history");
1006-
console.error("Failed to truncate history:", normalized);
1007-
setToast({
1008-
id: Date.now().toString(),
1009-
type: "error",
1010-
message: normalized.message,
1011-
});
1012-
return { clearInput: false, toastShown: true };
1013-
}
1014-
}
1015-
1016983
async function handleForkCommand(
1017984
parsed: Extract<ParsedCommand, { type: "fork" }>,
1018985
context: SlashCommandContext

src/browser/utils/slashCommands/parser_multiline.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,4 @@ describe("parser multiline compatibility", () => {
2222
modelString: KNOWN_MODELS.OPUS.id,
2323
});
2424
});
25-
26-
it("allows /truncate with newlines", () => {
27-
const result = parseCommand("/truncate\n50");
28-
expect(result).toEqual({
29-
type: "truncate",
30-
percentage: 0.5,
31-
});
32-
});
3325
});

src/browser/utils/slashCommands/registry.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -114,48 +114,6 @@ const clearCommandDefinition: SlashCommandDefinition = {
114114
},
115115
};
116116

117-
const TRUNCATE_USAGE = `/truncate ${SLASH_COMMAND_HINTS.truncate} (percentage to remove)`;
118-
119-
const truncateCommandDefinition: SlashCommandDefinition = {
120-
key: "truncate",
121-
description: "Truncate conversation history by percentage (0-100)",
122-
inputHint: SLASH_COMMAND_HINTS.truncate,
123-
handler: ({ cleanRemainingTokens }): ParsedCommand => {
124-
if (cleanRemainingTokens.length === 0) {
125-
return {
126-
type: "command-missing-args",
127-
command: "truncate",
128-
usage: TRUNCATE_USAGE,
129-
};
130-
}
131-
132-
if (cleanRemainingTokens.length > 1) {
133-
return {
134-
type: "command-invalid-args",
135-
command: "truncate",
136-
input: cleanRemainingTokens.join(" "),
137-
usage: TRUNCATE_USAGE,
138-
};
139-
}
140-
141-
// Parse percentage (0-100)
142-
const pctStr = cleanRemainingTokens[0];
143-
const pct = parseFloat(pctStr);
144-
145-
if (isNaN(pct) || pct < 0 || pct > 100) {
146-
return {
147-
type: "command-invalid-args",
148-
command: "truncate",
149-
input: pctStr,
150-
usage: TRUNCATE_USAGE,
151-
};
152-
}
153-
154-
// Convert to 0.0-1.0
155-
return { type: "truncate", percentage: pct / 100 };
156-
},
157-
};
158-
159117
const compactCommandDefinition: SlashCommandDefinition = {
160118
key: "compact",
161119
description:
@@ -708,7 +666,6 @@ const debugLlmRequestCommandDefinition: SlashCommandDefinition = {
708666

709667
export const SLASH_COMMAND_DEFINITIONS: readonly SlashCommandDefinition[] = [
710668
clearCommandDefinition,
711-
truncateCommandDefinition,
712669
compactCommandDefinition,
713670
modelCommandDefinition,
714671
planCommandDefinition,

src/browser/utils/slashCommands/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export type ParsedCommand =
2525
}
2626
| { type: "model-help" }
2727
| { type: "clear"; mode: "hard" | "soft" }
28-
| { type: "truncate"; percentage: number }
2928
| { type: "compact"; maxOutputTokens?: number; continueMessage?: string; model?: string }
3029
| { type: "fork"; startMessage?: string }
3130
| { type: "new"; startMessage?: string }

src/common/constants/slashCommandHints.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* Keyed by command name (without the leading "/").
55
*/
66
export const SLASH_COMMAND_HINTS = {
7-
truncate: "<0-100>",
87
compact: "[-t <tokens>] [-m <model>] [continue message]",
98
model: "<abbreviation|full-id>",
109
fork: "[start message]",

0 commit comments

Comments
 (0)