Skip to content

Commit 02a60ef

Browse files
oratisclaude
andauthored
feat(cli): manual /compact slash command (#128)
Auto-compaction already runs in the agent loop at the context threshold; this exposes a manual `/compact` that summarizes the conversation on demand (reusing core's compact()). Sets ctx.newHistory so the REPL swaps in the compacted history, mirroring /rewind summarize. No-ops cleanly when there's no provider or the conversation is already short. Tests: +2 (no-provider guard, empty-history). BEHAVIOR_PARITY /compact → ✅. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b10cb34 commit 02a60ef

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

apps/cli/src/commands.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,4 +419,19 @@ describe('inspector + export commands', () => {
419419
const out = await reg.match('/export')!.cmd.run([], makeContext({ history: [] }));
420420
expect(out.join('\n')).toMatch(/Nothing to export/);
421421
});
422+
423+
it('/compact needs a provider', async () => {
424+
const ctx = makeContext({
425+
history: [{ role: 'user', content: [{ type: 'text', text: 'x' }] }],
426+
});
427+
const out = await reg.match('/compact')!.cmd.run([], ctx);
428+
expect(out.join('\n')).toMatch(/needs a provider/);
429+
});
430+
431+
it('/compact reports nothing with empty history', async () => {
432+
const out = await reg
433+
.match('/compact')!
434+
.cmd.run([], makeContext({ provider: { name: 'm', runTurn: async () => ({}) } as never }));
435+
expect(out.join('\n')).toMatch(/Nothing to compact/);
436+
});
422437
});

apps/cli/src/commands.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,27 @@ export const ExportCommand: SlashCommand = {
678678
},
679679
};
680680

681+
export const CompactCommand: SlashCommand = {
682+
name: '/compact',
683+
description: 'Summarize the conversation so far to free up context.',
684+
async run(_args, ctx) {
685+
if (!ctx.provider) return ['(/compact needs a provider — none configured.)'];
686+
const history = ctx.history ?? [];
687+
if (history.length === 0) return ['Nothing to compact yet.'];
688+
try {
689+
const { compact } = await import('@deepcode/core');
690+
const result = await compact(history, { provider: ctx.provider });
691+
if (result.messagesRemoved === 0) {
692+
return ['Conversation is already short enough — nothing to compact.'];
693+
}
694+
ctx.newHistory = result.history;
695+
return [`✓ Compacted ${result.messagesRemoved} messages → ${result.history.length} kept.`];
696+
} catch (err) {
697+
return [`(Compaction failed: ${(err as Error).message})`];
698+
}
699+
},
700+
};
701+
681702
/** Render a conversation as readable markdown (text + tool calls). */
682703
function historyToMarkdown(history: StoredMessage[]): string {
683704
const out: string[] = ['# DeepCode conversation export', ''];
@@ -732,6 +753,7 @@ export const BUILTIN_COMMANDS: SlashCommand[] = [
732753
AgentsCommand,
733754
SkillsCommand,
734755
ExportCommand,
756+
CompactCommand,
735757
];
736758

737759
// ──────────────────────────────────────────────────────────────────────────

docs/BEHAVIOR_PARITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Legend: `✅` matches · `🟡` matches with caveats · `🔄` deferred · `⚠
2626
| `/add-dir` || ✓ (records intent) | 🟡 — M3 will enforce |
2727
| `/todos` ||| ✅ — reads `<sessionDir>/todos.json` written by TodoWrite tool |
2828
| `/plugins` ||| ✅ — lists wired plugins + contributed hook events + warnings (M5.2) |
29-
| `/compact` ||auto-trigger | 🟡 — manual `/compact` slash command not exposed yet (auto works via agent loop) |
29+
| `/compact` || | — manual `/compact` + automatic threshold trigger in the agent loop |
3030
| `/btw` ||| 🔄 |
3131
| `/recap` ||| 🔄 |
3232
| `/rewind` ||| ✅ — 5 ops (code/conversation/both/summarize-from/up-to); `Esc Esc` bound |

0 commit comments

Comments
 (0)