Skip to content
Open
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
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
92 changes: 50 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,89 @@ 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",
aliases: command.name === "compact" ? ["/summarize"] : undefined,
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 +409,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 +572,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 +589,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 @@ -374,7 +374,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 @@ -384,11 +384,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 @@ -933,11 +951,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 @@ -521,6 +521,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 @@ -997,6 +998,10 @@ export namespace Config {
.object({
auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"),
prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
command: z
.string()
.optional()
.describe("Command to use for automatic compaction when context limit is reached. Defaults to 'compact'"),
})
.optional(),
experimental: z
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 @@ -1189,6 +1189,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 @@ -1214,6 +1215,7 @@ export namespace Server {
modelID: body.modelID,
},
auto: body.auto,
prompt: body.prompt,
})
await SessionPrompt.loop(sessionID)
return c.json(true)
Expand Down
Loading