Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0ee7d14
feat: add support for custom compact commands
maxeonyx Dec 7, 2025
b2353cb
refactor: implement minimal custom compact commands via markdown files
maxeonyx Dec 7, 2025
de9ba91
fix: address code review feedback
maxeonyx Dec 8, 2025
288a2da
fix: restore REVIEW command that was accidentally removed
maxeonyx Dec 8, 2025
84c9845
fix: restore config.ts from dev and re-apply minimal compact field ch…
maxeonyx Dec 8, 2025
486353c
refactor: make prompt required for all compaction, auto-compaction in…
maxeonyx Dec 8, 2025
711bdb6
Revert unrelated noise
maxeonyx Dec 8, 2025
86c7b11
Add "auto_compact_command" option
maxeonyx Dec 8, 2025
69bcc27
Merge origin/dev into feature/compact-handover-mode
maxeonyx Dec 8, 2025
25d2861
docs: add configurable compaction documentation
maxeonyx Dec 8, 2025
b1dedd2
fix: make CompactionPart prompt required
maxeonyx Dec 8, 2025
771f079
docs: combine compact option and compaction sections
maxeonyx Dec 8, 2025
4567292
docs: remove compact example from built-in section
maxeonyx Dec 8, 2025
b952000
docs: restructure compact section for clarity
maxeonyx Dec 8, 2025
fbbf502
docs: clarify auto-compaction default behavior
maxeonyx Dec 8, 2025
e55872c
docs: remove redundant point about command templates
maxeonyx Dec 8, 2025
239d6a7
docs: remove auto_compact_command config example
maxeonyx Dec 8, 2025
7860d92
docs: fix command file path to use global location
maxeonyx Dec 8, 2025
4b88c8f
docs: revert to .opencode/command/ path
maxeonyx Dec 8, 2025
637208c
refactor: add logging for auto-compaction errors
maxeonyx Dec 8, 2025
2df7447
Merge origin/dev into feature/compact-handover-mode
maxeonyx Dec 12, 2025
ec204d7
refactor: cleanup dialog command structure and handlers
maxeonyx Dec 12, 2025
4b56ad7
Merge tag 'v1.0.150' (stable dev state) into feature/compact-handover…
maxeonyx Dec 12, 2025
e829d2f
fix: TUI bugfixes for compact command handling
maxeonyx Dec 16, 2025
2367a9f
Merge origin/dev into feature/compact-handover-mode
maxeonyx Dec 16, 2025
6df3686
fix: pass prompt to summarize endpoint and regenerate SDK
maxeonyx Dec 16, 2025
c75383f
Merge v1.0.166 into feature/compact-handover-mode
maxeonyx Dec 17, 2025
7687689
Merge v1.1.8 into feature/compact-handover-mode
maxeonyx Jan 9, 2026
e64f6ca
fix: resolve type errors after merge with v1.1.8
maxeonyx Jan 9, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ function init() {
})

const result = {
trigger(name: string, source?: "prompt") {
trigger(name: string, source?: "prompt", data?: any) {
for (const option of options()) {
if (option.value === name) {
option.onSelect?.(dialog, source)
option.onSelect?.(dialog, source, data)
return
}
}
Expand Down
91 changes: 49 additions & 42 deletions packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function Autocomplete(props: {
}) {
const sdk = useSDK()
const sync = useSync()
const command = useCommandDialog()
const dialog = useCommandDialog()
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
const frecency = useFrecency()
Expand Down Expand Up @@ -317,81 +317,88 @@ export function Autocomplete(props: {
const results: AutocompleteOption[] = []
const s = session()
for (const command of sync.data.command) {
results.push({
display: "/" + command.name + (command.mcp ? " (MCP)" : ""),
description: command.description,
onSelect: () => {
const newText = "/" + command.name + " "
const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col)
props.input().insertText(newText)
props.input().cursorOffset = Bun.stringWidth(newText)
},
})
if (command.compact && s) {
results.push({
display: "/" + command.name + (command.mcp ? " (MCP)" : ""),
description: command.description ?? "compact the session",
onSelect: () => {
dialog.trigger("session.compact", "prompt", {
commandName: command.name,
template: command.template,
})
},
})
} else if (!command.compact) {
results.push({
display: "/" + command.name + (command.mcp ? " (MCP)" : ""),
description: command.description,
onSelect: () => {
const newText = "/" + command.name + " "
const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col)
props.input().insertText(newText)
props.input().cursorOffset = Bun.stringWidth(newText)
},
})
}
}
if (s) {
results.push(
{
display: "/undo",
description: "undo the last message",
onSelect: () => {
command.trigger("session.undo")
dialog.trigger("session.undo")
},
},
{
display: "/redo",
description: "redo the last message",
onSelect: () => command.trigger("session.redo"),
},
{
display: "/compact",
aliases: ["/summarize"],
description: "compact the session",
onSelect: () => command.trigger("session.compact"),
onSelect: () => dialog.trigger("session.redo"),
},
{
display: "/unshare",
disabled: !s.share,
description: "unshare a session",
onSelect: () => command.trigger("session.unshare"),
onSelect: () => dialog.trigger("session.unshare"),
},
{
display: "/rename",
description: "rename session",
onSelect: () => command.trigger("session.rename"),
onSelect: () => dialog.trigger("session.rename"),
},
{
display: "/copy",
description: "copy session transcript to clipboard",
onSelect: () => command.trigger("session.copy"),
onSelect: () => dialog.trigger("session.copy"),
},
{
display: "/export",
description: "export session transcript to file",
onSelect: () => command.trigger("session.export"),
onSelect: () => dialog.trigger("session.export"),
},
{
display: "/timeline",
description: "jump to message",
onSelect: () => command.trigger("session.timeline"),
onSelect: () => dialog.trigger("session.timeline"),
},
{
display: "/fork",
description: "fork from message",
onSelect: () => command.trigger("session.fork"),
onSelect: () => dialog.trigger("session.fork"),
},
{
display: "/thinking",
description: "toggle thinking visibility",
onSelect: () => command.trigger("session.toggle.thinking"),
onSelect: () => dialog.trigger("session.toggle.thinking"),
},
)
if (sync.data.config.share !== "disabled") {
results.push({
display: "/share",
disabled: !!s.share?.url,
description: "share a session",
onSelect: () => command.trigger("session.share"),
onSelect: () => dialog.trigger("session.share"),
})
}
}
Expand All @@ -401,64 +408,64 @@ export function Autocomplete(props: {
display: "/new",
aliases: ["/clear"],
description: "create a new session",
onSelect: () => command.trigger("session.new"),
onSelect: () => dialog.trigger("session.new"),
},
{
display: "/models",
description: "list models",
onSelect: () => command.trigger("model.list"),
onSelect: () => dialog.trigger("model.list"),
},
{
display: "/agents",
description: "list agents",
onSelect: () => command.trigger("agent.list"),
onSelect: () => dialog.trigger("agent.list"),
},
{
display: "/session",
aliases: ["/resume", "/continue"],
description: "list sessions",
onSelect: () => command.trigger("session.list"),
onSelect: () => dialog.trigger("session.list"),
},
{
display: "/status",
description: "show status",
onSelect: () => command.trigger("opencode.status"),
onSelect: () => dialog.trigger("opencode.status"),
},
{
display: "/mcp",
description: "toggle MCPs",
onSelect: () => command.trigger("mcp.list"),
onSelect: () => dialog.trigger("mcp.list"),
},
{
display: "/theme",
description: "toggle theme",
onSelect: () => command.trigger("theme.switch"),
onSelect: () => dialog.trigger("theme.switch"),
},
{
display: "/editor",
description: "open editor",
onSelect: () => command.trigger("prompt.editor", "prompt"),
onSelect: () => dialog.trigger("prompt.editor", "prompt"),
},
{
display: "/connect",
description: "connect to a provider",
onSelect: () => command.trigger("provider.connect"),
onSelect: () => dialog.trigger("provider.connect"),
},
{
display: "/help",
description: "show help",
onSelect: () => command.trigger("help.show"),
onSelect: () => dialog.trigger("help.show"),
},
{
display: "/commands",
description: "show all commands",
onSelect: () => command.show(),
onSelect: () => dialog.show(),
},
{
display: "/exit",
aliases: ["/quit", "/q"],
description: "exit the app",
onSelect: () => command.trigger("app.exit"),
onSelect: () => dialog.trigger("app.exit"),
},
)
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
Expand Down Expand Up @@ -564,7 +571,7 @@ export function Autocomplete(props: {
}

function show(mode: "@" | "/") {
command.keybinds(false)
dialog.keybinds(false)
setStore({
visible: mode,
index: props.input().cursorOffset,
Expand All @@ -581,7 +588,7 @@ export function Autocomplete(props: {
draft.input = props.input().plainText
})
}
command.keybinds(true)
dialog.keybinds(true)
setStore("visible", false)
}

Expand Down
28 changes: 23 additions & 5 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ export function Session() {
value: "session.compact",
keybind: "session_compact",
category: "Session",
onSelect: (dialog) => {
onSelect: async (dialog, trigger, data) => {
const selectedModel = local.model.current()
if (!selectedModel) {
toast.show({
Expand All @@ -366,11 +366,29 @@ export function Session() {
})
return
}
sdk.client.session.summarize({
// If no template provided (e.g., via keybind), use the default /compact command
const prompt = data?.template ?? sync.data.command.find((c) => c.name === "compact")?.template
if (!prompt) {
toast.show({
variant: "warning",
message: "No compact command configured",
duration: 3000,
})
return
}
const result = await sdk.client.session.summarize({
sessionID: route.sessionID,
modelID: selectedModel.modelID,
providerID: selectedModel.providerID,
modelID: selectedModel.modelID,
prompt,
})
if (result.error) {
toast.show({
variant: "error",
message: "Summarize failed: " + JSON.stringify(result.error),
duration: 5000,
})
}
dialog.clear()
},
},
Expand Down Expand Up @@ -915,11 +933,11 @@ export function Session() {
{(function () {
const command = useCommandDialog()
const [hover, setHover] = createSignal(false)
const dialog = useDialog()
const modal = useDialog()

const handleUnrevert = async () => {
const confirmed = await DialogConfirm.show(
dialog,
modal,
"Confirm Redo",
"Are you sure you want to restore the reverted messages?",
)
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface DialogSelectOption<T = any> {
disabled?: boolean
bg?: RGBA
gutter?: JSX.Element
onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
onSelect?: (ctx: DialogContext, trigger?: "prompt", data?: any) => void
}

export type DialogSelectRef<T> = {
Expand Down
11 changes: 11 additions & 0 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Instance } from "../project/instance"
import { Identifier } from "../id/id"
import PROMPT_INITIALIZE from "./template/initialize.txt"
import PROMPT_REVIEW from "./template/review.txt"
import PROMPT_COMPACT from "./template/compact.txt"
import { MCP } from "../mcp"

export namespace Command {
Expand All @@ -31,6 +32,7 @@ export namespace Command {
// https://zod.dev/v4/changelog?id=zfunction
template: z.promise(z.string()).or(z.string()),
subtask: z.boolean().optional(),
compact: z.boolean().optional(),
hints: z.array(z.string()),
})
.meta({
Expand All @@ -53,6 +55,7 @@ export namespace Command {
export const Default = {
INIT: "init",
REVIEW: "review",
COMPACT: "compact",
} as const

const state = Instance.state(async () => {
Expand All @@ -76,6 +79,13 @@ export namespace Command {
subtask: true,
hints: hints(PROMPT_REVIEW),
},
[Default.COMPACT]: {
name: Default.COMPACT,
description: "summarize session for compaction",
template: PROMPT_COMPACT,
compact: true,
hints: hints(PROMPT_COMPACT),
},
}

for (const [name, command] of Object.entries(cfg.command ?? {})) {
Expand All @@ -88,6 +98,7 @@ export namespace Command {
return command.template
},
subtask: command.subtask,
compact: command.compact,
hints: hints(command.template),
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/command/template/compact.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Provide a detailed prompt for continuing our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next considering new session will not have access to our conversation.
5 changes: 5 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ export namespace Config {
agent: z.string().optional(),
model: z.string().optional(),
subtask: z.boolean().optional(),
compact: z.boolean().optional(),
})
export type Command = z.infer<typeof Command>

Expand Down Expand Up @@ -843,6 +844,10 @@ export namespace Config {
.string()
.optional()
.describe("Custom username to display in conversations instead of system username"),
auto_compact_command: z
.string()
.optional()
.describe("Command to use for automatic compaction when context limit is reached. Defaults to 'compact'"),
mode: z
.object({
build: Agent.optional(),
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,7 @@ export namespace Server {
z.object({
providerID: z.string(),
modelID: z.string(),
prompt: z.string().describe("Prompt to use for compaction"),
auto: z.boolean().optional().default(false),
}),
),
Expand All @@ -1203,6 +1204,7 @@ export namespace Server {
modelID: body.modelID,
},
auto: body.auto,
prompt: body.prompt,
})
await SessionPrompt.loop(sessionID)
return c.json(true)
Expand Down
Loading