Skip to content

Commit 0ee7d14

Browse files
committed
feat: add support for custom compact commands
- Add 'compact' metadata field to Command schema - Allow users to create custom compact-type commands in .opencode/command/ - Add experimental.autoCompactCommand config to choose which command for auto-compaction - Default /compact command uses SessionCompaction.DEFAULT_PROMPT - UI detects compact commands and triggers with custom template - Users can override /compact by creating their own compact.md command This enables users to customize the compaction prompt for their specific needs, such as creating a /handover command with detailed context for team handoffs.
1 parent 6667856 commit 0ee7d14

File tree

12 files changed

+771
-725
lines changed

12 files changed

+771
-725
lines changed

COMPACT_COMMANDS_ANALYSIS.md

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Custom Compact Commands: Analysis & Requirements
2+
3+
## Core Requirements
4+
5+
1. **Shift "/compact" to be implemented in terms of a generic custom "compact"-type command capability, similar to "/init"**
6+
- Users should be able to create custom compact commands just like they create custom agents
7+
- The mechanism should follow the same pattern as `/init` with custom agents
8+
9+
2. **Users should be able to define such commands in opencode config as markdown files**
10+
- Commands defined in `.opencode/command/*.md`
11+
- Metadata-driven configuration (frontmatter)
12+
- Template-based prompt customization
13+
14+
3. **Example: How to implement "/handover"**
15+
- Create `.opencode/command/handover.md`
16+
- Use metadata to mark it as a compact-type command
17+
- Define custom template for handover-specific summaries
18+
- Write this example to globally-ignored directory `config.ignore/command/handover.md` for testing.
19+
20+
4. **Remove all other custom compact prompt changes**
21+
- Remove deprecated `compactPrompt` config field
22+
- Clean up any legacy compact customization mechanisms
23+
- Consolidate to a single, clear pattern
24+
25+
5. **Refactor until minimal possible changes**
26+
- Reuse existing patterns and infrastructure
27+
- Avoid special-case logic
28+
- Keep changes localized and clean
29+
30+
## How `/init` Works (Reference Pattern)
31+
32+
1. `/init` is defined as a regular command with a template
33+
2. When user types `/init`, autocomplete shows it as an option
34+
3. User can add custom text after it (e.g., `/init Explain the codebase`)
35+
4. When submitted, the full text is sent as a normal message
36+
5. The command's `agent` and `model` fields are used during execution
37+
6. No special API calls - it follows the standard message flow
38+
7. The agent processes the message according to its instructions
39+
40+
## Key Architectural Insight
41+
42+
The `/init` pattern works because:
43+
44+
- Commands are just text templates that get inserted into the prompt
45+
- The `agent` field determines which agent processes the message
46+
- No special branching or API calls in autocomplete
47+
- Clean separation: command definition → text insertion → message handling
48+
49+
## Challenges Encountered
50+
51+
### Challenge 1: Initial Implementation Took Wrong Approach
52+
53+
**What Happened:**
54+
55+
- Implemented compact commands with special `compact: boolean` metadata field
56+
- Added special-case logic in autocomplete to detect compact commands
57+
- Made compact commands call compaction API directly, bypassing normal flow
58+
- Created dual mechanisms: global `compactPrompt` config + per-command templates
59+
60+
**Why This Was Wrong:**
61+
62+
- Doesn't follow `/init` pattern (direct API call vs text insertion)
63+
- Can't accept arguments like `/compact Focus on database changes`
64+
- Special-case logic breaks architectural consistency
65+
- Agent and model fields are ignored
66+
- More complex than the pattern it's supposed to mirror
67+
68+
### Challenge 2: TUI Crash on Startup
69+
70+
**What Happened:**
71+
72+
- Original error: `undefined is not an object (evaluating 'x.data.providers')`
73+
- Error occurred in `packages/opencode/src/cli/cmd/tui/context/sync.tsx:148`
74+
- App crashed immediately on startup before even testing compact commands
75+
76+
**Root Cause (from subagent analysis):**
77+
78+
- Race condition during initialization
79+
- Event listener calling `setStore()` while component is being cleaned up
80+
- SolidJS reactive graph being torn down during async operations
81+
- Abort signal shared between event stream and all API calls
82+
- When event stream aborts, all in-flight API calls fail
83+
84+
**Attempted Fixes Led to Cascading Issues:**
85+
86+
1. Added safe fallbacks (`x.data?.providers ?? []`)
87+
2. This made agents array empty
88+
3. App tried to access `agents()[0].name` → crash
89+
4. Fixed that, then `local.agent.current().name` → crash
90+
5. Fixed that, but agents being empty breaks fundamental app functionality
91+
92+
**The Deeper Problem:**
93+
94+
- The app fundamentally requires agents to function
95+
- Safe fallbacks that return empty arrays just move the crash elsewhere
96+
- The real issue is WHY the API calls are failing in the first place
97+
- Went down a rabbit hole of fixing symptoms instead of root cause
98+
99+
### Challenge 3: Too Many Changes, Lost Focus
100+
101+
**What Happened:**
102+
103+
- Started with compact command feature
104+
- Hit unrelated TUI crash
105+
- Made many changes trying to fix the crash
106+
- Now have changes across multiple files:
107+
- `sync.tsx` - extensive error handling
108+
- `sdk.tsx` - exposed abort signal
109+
- `local.tsx` - safe agent access
110+
- Plus all the compact command changes
111+
- Lost ability to isolate which changes are for which problem
112+
113+
**The Right Approach Should Have Been:**
114+
115+
1. Test compact command implementation on a working branch first
116+
2. If crash occurs, isolate it as a separate issue
117+
3. Fix crash in isolation, verify fix works
118+
4. Then return to feature implementation
119+
5. Keep changes minimal and focused
120+
121+
## Key Files Involved in Compact Commands
122+
123+
### Schema/Config Files
124+
125+
- `packages/opencode/src/config/config.ts` - Command schema with `compact` field
126+
- `packages/opencode/src/command/index.ts` - Command.Info type and default commands
127+
128+
### Execution Files
129+
130+
- `packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx` - Special handling for compact commands
131+
- `packages/opencode/src/session/system.ts` - Compact prompt customization
132+
- `packages/opencode/src/session/compaction.ts` - Where compaction actually happens
133+
134+
### SDK
135+
136+
- `packages/sdk/js/src/gen/types.gen.ts` - Generated types including Command and Config
137+
138+
### Example
139+
140+
- `config.ignore/command/handover.md` - Example custom compact command
141+
142+
## Key Files Involved in TUI Crash
143+
144+
### Context Providers
145+
146+
- `packages/opencode/src/cli/cmd/tui/context/sync.tsx` - Main initialization and state sync
147+
- `packages/opencode/src/cli/cmd/tui/context/sdk.tsx` - SDK client wrapper with abort signal
148+
- `packages/opencode/src/cli/cmd/tui/context/local.tsx` - Local state including current agent
149+
150+
### Components That Use Agents
151+
152+
- `packages/opencode/src/cli/cmd/tui/app.tsx` - Main app, displays current agent
153+
- `packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx` - Agent selection dialog
154+
- `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx` - Prompt component
155+
156+
## Questions That Need Answers
157+
158+
1. **Why is `/config/providers` returning undefined in the first place?**
159+
- Is there a configuration issue?
160+
- Is the server not starting properly?
161+
- Is there a timing issue?
162+
163+
2. **Should compact commands really call the API directly or should they follow `/init` pattern?**
164+
- Direct API call: simpler but inconsistent with `/init`
165+
- Text insertion: consistent but requires message routing logic
166+
167+
3. **Where should message routing happen if we follow the `/init` pattern?**
168+
- At message send time?
169+
- In the session handler?
170+
- Via a special compact agent?
171+
172+
4. **What is the minimal set of changes needed?**
173+
- Can we reuse more existing infrastructure?
174+
- What can be deleted vs added?
175+
176+
## Recommended Next Steps
177+
178+
1. **Reset branch to clean state**
179+
- Start fresh from `dev` branch
180+
- Clear separation between feature work and bug fixes
181+
182+
2. **First: Verify `/config/providers` works on dev branch**
183+
- Test that app starts cleanly
184+
- Understand why it might fail
185+
- Fix any startup issues in isolation
186+
187+
3. **Second: Design the right compact command approach**
188+
- Decide: direct API call vs `/init` pattern
189+
- Document the architecture before coding
190+
- Identify exactly which files need changes
191+
192+
4. **Third: Implement incrementally**
193+
- One change at a time
194+
- Test after each change
195+
- Keep diff minimal
196+
197+
5. **Fourth: Test thoroughly**
198+
- Verify `/compact` still works
199+
- Test custom compact commands
200+
- Ensure no regressions
201+
202+
## Success Criteria
203+
204+
- [ ] User can create `.opencode/command/handover.md` with custom compact template
205+
- [ ] `/handover` command works like `/compact` but with custom template
206+
- [ ] Implementation follows same pattern as `/init` with custom agents
207+
- [ ] No deprecated `compactPrompt` config field
208+
- [ ] Minimal code changes (additions are localized, deletions exceed additions)
209+
- [ ] No special-case logic in autocomplete
210+
- [ ] App starts without crashing
211+
- [ ] All existing compact functionality preserved
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[11:11:38] [ERROR] Error: undefined is not an object (evaluating 'x.data.providers')
2+
Error: undefined is not an object (evaluating 'x.data.providers')
3+
at <anonymous> (/home/mclarke/opencode/packages/opencode/src/cli/cmd/tui/context/sync.tsx:148:81)
4+
at processTicksAndRejections (native)
5+
6+
[11:11:38] [ERROR] Error: undefined is not an object (evaluating 'x.data.providers')
7+
Error: undefined is not an object (evaluating 'x.data.providers')
8+
at <anonymous> (/home/mclarke/opencode/packages/opencode/src/cli/cmd/tui/context/sync.tsx:148:81)
9+
at processTicksAndRejections (native)

packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ function init() {
5555
})
5656

5757
const result = {
58-
trigger(name: string, source?: "prompt") {
58+
trigger(name: string, data?: any) {
5959
for (const option of options()) {
6060
if (option.value === name) {
61-
option.onSelect?.(dialog, source)
61+
option.onSelect?.(dialog, undefined, data)
6262
return
6363
}
6464
}

packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useCommandDialog } from "@tui/component/dialog-command"
1111
import { useTerminalDimensions } from "@opentui/solid"
1212
import { Locale } from "@/util/locale"
1313
import type { PromptInfo } from "./history"
14+
import { SessionCompaction } from "../../../../../session/compaction"
1415

1516
export type AutocompleteRef = {
1617
onInput: (value: string) => void
@@ -209,20 +210,34 @@ export function Autocomplete(props: {
209210
const commands = createMemo((): AutocompleteOption[] => {
210211
const results: AutocompleteOption[] = []
211212
const s = session()
212-
for (const command of sync.data.command) {
213-
results.push({
214-
display: "/" + command.name,
215-
description: command.description,
216-
onSelect: () => {
217-
const newText = "/" + command.name + " "
218-
const cursor = props.input().logicalCursor
219-
props.input().deleteRange(0, 0, cursor.row, cursor.col)
220-
props.input().insertText(newText)
221-
props.input().cursorOffset = Bun.stringWidth(newText)
222-
},
223-
})
213+
for (const cmd of sync.data.command) {
214+
if (cmd.compact) {
215+
results.push({
216+
display: "/" + cmd.name,
217+
description: cmd.description ?? "compact the session",
218+
onSelect: () => {
219+
command.trigger("session.compact", {
220+
commandName: cmd.name,
221+
template: cmd.template,
222+
})
223+
},
224+
})
225+
} else {
226+
results.push({
227+
display: "/" + cmd.name,
228+
description: cmd.description,
229+
onSelect: () => {
230+
const newText = "/" + cmd.name + " "
231+
const cursor = props.input().logicalCursor
232+
props.input().deleteRange(0, 0, cursor.row, cursor.col)
233+
props.input().insertText(newText)
234+
props.input().cursorOffset = Bun.stringWidth(newText)
235+
},
236+
})
237+
}
224238
}
225239
if (s) {
240+
const hasCompactCommand = sync.data.command.some((c) => c.name === "compact")
226241
results.push(
227242
{
228243
display: "/undo",
@@ -236,12 +251,20 @@ export function Autocomplete(props: {
236251
description: "redo the last message",
237252
onSelect: () => command.trigger("session.redo"),
238253
},
239-
{
240-
display: "/compact",
241-
aliases: ["/summarize"],
242-
description: "compact the session",
243-
onSelect: () => command.trigger("session.compact"),
244-
},
254+
...(!hasCompactCommand
255+
? [
256+
{
257+
display: "/compact",
258+
aliases: ["/summarize"],
259+
description: "compact the session",
260+
onSelect: () =>
261+
command.trigger("session.compact", {
262+
commandName: "compact",
263+
template: SessionCompaction.DEFAULT_PROMPT,
264+
}),
265+
},
266+
]
267+
: []),
245268
{
246269
display: "/unshare",
247270
disabled: !s.share,

0 commit comments

Comments
 (0)