From 6f4f3e67f4b1eba1a0f0b9be0fd4f4dfd76d6c0a Mon Sep 17 00:00:00 2001 From: oratis Date: Sun, 31 May 2026 22:32:53 +0800 Subject: [PATCH] feat(cli): manual /compact slash command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/cli/src/commands.test.ts | 15 +++++++++++++++ apps/cli/src/commands.ts | 22 ++++++++++++++++++++++ docs/BEHAVIOR_PARITY.md | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/commands.test.ts b/apps/cli/src/commands.test.ts index 20d6e9b..deced6c 100644 --- a/apps/cli/src/commands.test.ts +++ b/apps/cli/src/commands.test.ts @@ -419,4 +419,19 @@ describe('inspector + export commands', () => { const out = await reg.match('/export')!.cmd.run([], makeContext({ history: [] })); expect(out.join('\n')).toMatch(/Nothing to export/); }); + + it('/compact needs a provider', async () => { + const ctx = makeContext({ + history: [{ role: 'user', content: [{ type: 'text', text: 'x' }] }], + }); + const out = await reg.match('/compact')!.cmd.run([], ctx); + expect(out.join('\n')).toMatch(/needs a provider/); + }); + + it('/compact reports nothing with empty history', async () => { + const out = await reg + .match('/compact')! + .cmd.run([], makeContext({ provider: { name: 'm', runTurn: async () => ({}) } as never })); + expect(out.join('\n')).toMatch(/Nothing to compact/); + }); }); diff --git a/apps/cli/src/commands.ts b/apps/cli/src/commands.ts index b2d6d91..b6eb0fb 100644 --- a/apps/cli/src/commands.ts +++ b/apps/cli/src/commands.ts @@ -678,6 +678,27 @@ export const ExportCommand: SlashCommand = { }, }; +export const CompactCommand: SlashCommand = { + name: '/compact', + description: 'Summarize the conversation so far to free up context.', + async run(_args, ctx) { + if (!ctx.provider) return ['(/compact needs a provider — none configured.)']; + const history = ctx.history ?? []; + if (history.length === 0) return ['Nothing to compact yet.']; + try { + const { compact } = await import('@deepcode/core'); + const result = await compact(history, { provider: ctx.provider }); + if (result.messagesRemoved === 0) { + return ['Conversation is already short enough — nothing to compact.']; + } + ctx.newHistory = result.history; + return [`✓ Compacted ${result.messagesRemoved} messages → ${result.history.length} kept.`]; + } catch (err) { + return [`(Compaction failed: ${(err as Error).message})`]; + } + }, +}; + /** Render a conversation as readable markdown (text + tool calls). */ function historyToMarkdown(history: StoredMessage[]): string { const out: string[] = ['# DeepCode conversation export', '']; @@ -732,6 +753,7 @@ export const BUILTIN_COMMANDS: SlashCommand[] = [ AgentsCommand, SkillsCommand, ExportCommand, + CompactCommand, ]; // ────────────────────────────────────────────────────────────────────────── diff --git a/docs/BEHAVIOR_PARITY.md b/docs/BEHAVIOR_PARITY.md index e7b9220..c31b0d7 100644 --- a/docs/BEHAVIOR_PARITY.md +++ b/docs/BEHAVIOR_PARITY.md @@ -26,7 +26,7 @@ Legend: `✅` matches · `🟡` matches with caveats · `🔄` deferred · `⚠ | `/add-dir` | ✓ | ✓ (records intent) | 🟡 — M3 will enforce | | `/todos` | ✓ | ✓ | ✅ — reads `/todos.json` written by TodoWrite tool | | `/plugins` | ✓ | ✓ | ✅ — lists wired plugins + contributed hook events + warnings (M5.2) | -| `/compact` | ✓ | ✓ auto-trigger | 🟡 — manual `/compact` slash command not exposed yet (auto works via agent loop) | +| `/compact` | ✓ | ✓ | ✅ — manual `/compact` + automatic threshold trigger in the agent loop | | `/btw` | ✓ | ✗ | 🔄 | | `/recap` | ✓ | ✗ | 🔄 | | `/rewind` | ✓ | ✓ | ✅ — 5 ops (code/conversation/both/summarize-from/up-to); `Esc Esc` bound |