Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions docs/context-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,46 +47,60 @@ Compress conversation history using AI summarization. Replaces the conversation
### Syntax

```
/compact [-t <tokens>] [-c <message>]
/compact [-t <tokens>]
[continue message on subsequent lines]
```

### Options

- `-t <tokens>` - Maximum output tokens for the summary (default: ~2000 words)
- `-c <message>` - Continue after compaction with this user message

### Examples

**Basic compaction:**

```
/compact
```

Basic compaction with defaults.
**Limit summary size:**

```
/compact -t 5000
```

Limit summary to ~5000 tokens.
**Auto-continue with custom message:**

```
/compact -c "Continue implementing the auth system"
/compact
Continue implementing the auth system
```

Compact and automatically send a follow-up message after compaction completes.
After compaction completes, automatically sends "Continue implementing the auth system" as a follow-up message.

**Multiline continue message:**

```
/compact -t 3000 -c "Keep going"
/compact
Now let's refactor the middleware to use the new auth context.
Make sure to add tests for the error cases.
```

Combine token limit and auto-continue.
Continue messages can span multiple lines for more detailed instructions.

**Combine token limit and auto-continue:**

```
/compact -t 3000
Keep working on the feature
```

### Notes

- Uses the selected LLM to summarize conversation history
- Preserves actionable context and specific details
- **Irreversible** - original messages are replaced
- Continue message is used once per compaction (not persisted)
- Continue message is sent once after compaction completes (not persisted)

---

Expand Down
12 changes: 9 additions & 3 deletions docs/prompting-tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ passes.
## Aggressively prune context

Even though Sonnet 4.5 has up to 1M in potential context, we experience a noticeable improvement in
quality when kept <100k tokens. We suggest running `/compact -c "<what you want next>"`
often to keep context small. The `-c` flag will automatically send a follow up message post-compaction
to keep the session flowing.
quality when kept <100k tokens. We suggest running `/compact` with a continue message
often to keep context small. For example:

```
/compact
<what you want next>
```

This will automatically send a follow-up message after compaction to keep the session flowing.

## Keeping code clean

Expand Down
83 changes: 83 additions & 0 deletions src/utils/slashCommands/compact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,86 @@ it("rejects positional arguments with flags", () => {
subcommand: "Unexpected argument: extra",
});
});

describe("multiline continue messages", () => {
it("parses basic multiline continue message", () => {
const result = parseCommand("/compact\nContinue implementing the auth system");
expect(result).toEqual({
type: "compact",
maxOutputTokens: undefined,
continueMessage: "Continue implementing the auth system",
});
});

it("parses multiline with -t flag", () => {
const result = parseCommand("/compact -t 5000\nKeep working on the feature");
expect(result).toEqual({
type: "compact",
maxOutputTokens: 5000,
continueMessage: "Keep working on the feature",
});
});

it("parses multiline message with multiple lines", () => {
const result = parseCommand("/compact\nLine 1\nLine 2\nLine 3");
expect(result).toEqual({
type: "compact",
maxOutputTokens: undefined,
continueMessage: "Line 1\nLine 2\nLine 3",
});
});

it("handles empty lines in multiline message", () => {
const result = parseCommand("/compact\n\nContinue after empty line");
expect(result).toEqual({
type: "compact",
maxOutputTokens: undefined,
continueMessage: "Continue after empty line",
});
});

it("preserves whitespace in multiline content", () => {
const result = parseCommand("/compact\n Indented message\n More indented");
expect(result).toEqual({
type: "compact",
maxOutputTokens: undefined,
continueMessage: "Indented message\n More indented",
});
});

it("prioritizes -c flag over multiline content (backwards compat)", () => {
const result = parseCommand('/compact -c "Flag message"\nMultiline message');
expect(result).toEqual({
type: "compact",
maxOutputTokens: undefined,
continueMessage: "Flag message",
});
});

it("handles -c flag with multiline (flag wins)", () => {
const result = parseCommand('/compact -t 3000 -c "Keep going"\nThis should be ignored');
expect(result).toEqual({
type: "compact",
maxOutputTokens: 3000,
continueMessage: "Keep going",
});
});

it("ignores trailing newlines", () => {
const result = parseCommand("/compact\nContinue here\n\n\n");
expect(result).toEqual({
type: "compact",
maxOutputTokens: undefined,
continueMessage: "Continue here",
});
});

it("returns undefined continueMessage when only whitespace after command", () => {
const result = parseCommand("/compact\n \n \n");
expect(result).toEqual({
type: "compact",
maxOutputTokens: undefined,
continueMessage: undefined,
});
});
});
13 changes: 13 additions & 0 deletions src/utils/slashCommands/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function parseCommand(input: string): ParsedCommand {
}

// Remove leading slash and split by spaces (respecting quotes)
// Parse tokens from full input to support multi-line commands like /providers
const parts = (trimmed.substring(1).match(/(?:[^\s"]+|"[^"]*")+/g) ?? []) as string[];
if (parts.length === 0) {
return null;
Expand Down Expand Up @@ -64,11 +65,23 @@ export function parseCommand(input: string): ParsedCommand {

const cleanRemainingTokens = remainingTokens.map((token) => token.replace(/^"(.*)"$/, "$1"));

// Calculate rawInput: everything after the command key, preserving newlines
// For "/compact -t 5000\nContinue here", rawInput should be "-t 5000\nContinue here"
// For "/compact\nContinue here", rawInput should be "\nContinue here"
// We trim leading spaces on the first line only, not newlines
const commandKeyWithSlash = `/${commandKey}`;
let rawInput = trimmed.substring(commandKeyWithSlash.length);
// Only trim spaces at the start, not newlines
while (rawInput.startsWith(" ")) {
rawInput = rawInput.substring(1);
}

return targetDefinition.handler({
definition: targetDefinition,
path,
remainingTokens,
cleanRemainingTokens,
rawInput,
});
}

Expand Down
42 changes: 42 additions & 0 deletions src/utils/slashCommands/parser_multiline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Tests to ensure multiline support doesn't break other commands
*/
import { parseCommand } from "./parser";

describe("parser multiline compatibility", () => {
it("allows /providers with newlines in value", () => {
const result = parseCommand("/providers set anthropic apiKey\nsk-123");
expect(result).toEqual({
type: "providers-set",
provider: "anthropic",
keyPath: ["apiKey"],
value: "sk-123",
});
});

it("allows /providers with newlines between args", () => {
const result = parseCommand("/providers\nset\nanthropic\napiKey\nsk-456");
expect(result).toEqual({
type: "providers-set",
provider: "anthropic",
keyPath: ["apiKey"],
value: "sk-456",
});
});

it("allows /model with newlines", () => {
const result = parseCommand("/model\nopus");
expect(result).toEqual({
type: "model-set",
modelString: "anthropic:claude-opus-4-1",
});
});

it("allows /truncate with newlines", () => {
const result = parseCommand("/truncate\n50");
expect(result).toEqual({
type: "truncate",
percentage: 0.5,
});
});
});
35 changes: 25 additions & 10 deletions src/utils/slashCommands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,16 @@ const truncateCommandDefinition: SlashCommandDefinition = {
const compactCommandDefinition: SlashCommandDefinition = {
key: "compact",
description:
"Compact conversation history using AI summarization. Use -t <tokens> to set max output tokens, -c <message> to continue with custom prompt after compaction",
handler: ({ cleanRemainingTokens }): ParsedCommand => {
// Parse flags using minimist
"Compact conversation history using AI summarization. Use -t <tokens> to set max output tokens. Add continue message on lines after the command.",
handler: ({ cleanRemainingTokens, rawInput }): ParsedCommand => {
// Split rawInput into first line (for flags) and remaining lines (for multiline continue)
// rawInput format: "-t 5000\nContinue here" or "\nContinue here" (starts with newline if no flags)
const hasMultilineContent = rawInput.includes("\n");
const lines = rawInput.split("\n");
// Note: firstLine could be empty string if rawInput starts with \n (which is fine)
const remainingLines = lines.slice(1).join("\n").trim();

// Parse flags from first line using minimist
const parsed = minimist(cleanRemainingTokens, {
string: ["t", "c"],
unknown: (arg: string) => {
Expand Down Expand Up @@ -210,20 +217,28 @@ const compactCommandDefinition: SlashCommandDefinition = {
maxOutputTokens = tokens;
}

// Reject extra positional arguments
if (parsed._.length > 0) {
// Reject extra positional arguments UNLESS they're from multiline content
// (multiline content gets parsed as positional args by minimist since newlines become spaces)
if (parsed._.length > 0 && !hasMultilineContent) {
return {
type: "unknown-command",
command: "compact",
subcommand: `Unexpected argument: ${parsed._[0]}`,
};
}

// Get continue message if -c flag present
const continueMessage =
parsed.c !== undefined && typeof parsed.c === "string" && parsed.c.trim().length > 0
? parsed.c.trim()
: undefined;
// Determine continue message:
// 1. If -c flag present (backwards compat), use it
// 2. Otherwise, use multiline content (new behavior)
let continueMessage: string | undefined;

if (parsed.c !== undefined && typeof parsed.c === "string" && parsed.c.trim().length > 0) {
// -c flag takes precedence (backwards compatibility)
continueMessage = parsed.c.trim();
} else if (remainingLines.length > 0) {
// Use multiline content
continueMessage = remainingLines;
}

return { type: "compact", maxOutputTokens, continueMessage };
},
Expand Down
1 change: 1 addition & 0 deletions src/utils/slashCommands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface SlashCommandHandlerArgs {
path: readonly SlashCommandDefinition[];
remainingTokens: string[];
cleanRemainingTokens: string[];
rawInput: string; // Raw input after command name, preserving newlines
}

export type SlashCommandHandler = (input: SlashCommandHandlerArgs) => ParsedCommand;
Expand Down