feat(tui): add compact command to mini (#38251)

This commit is contained in:
Simon Klee 2026-07-22 07:03:46 +02:00 committed by GitHub
commit 691a7d93c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 109 additions and 6 deletions

View file

@ -374,7 +374,7 @@ export function RunCommandMenuBody(props: {
const skills = createMemo(() => (props.commands() ?? []).filter((item) => item.source === "skill"))
const activeSubagentCount = createMemo(() => props.subagents().filter((item) => item.status === "running").length)
const entries = createMemo<CommandEntry[]>(() => {
const builtins = ["editor", "new", "settings"]
const builtins = ["compact", "editor", "new", "settings"]
const session: CommandEntry[] = [
{
action: "editor",
@ -404,6 +404,14 @@ export function RunCommandMenuBody(props: {
},
]
: []),
{
action: "slash",
category: "Session",
name: "compact",
display: "Compact session",
footer: "/compact",
keywords: "compact summarize session context",
},
{
action: "slash",
category: "Session",

View file

@ -395,6 +395,12 @@ export function createPromptState(input: PromptInput): PromptState {
description: "configure Mini transcript output",
} satisfies SlashOption,
{ kind: "slash", name: "new", display: "/new", description: "start a new session" } satisfies SlashOption,
{
kind: "slash",
name: "compact",
display: "/compact",
description: "summarize the session to reduce context usage",
} satisfies SlashOption,
{ kind: "slash", name: "exit", display: "/exit", description: "close OpenCode" } satisfies SlashOption,
]
const hidden = new Set(builtins.map((item) => item.name))

View file

@ -53,6 +53,11 @@ export function isNewCommand(input: string): boolean {
return input.trim().toLowerCase() === "/new"
}
export function isCompactCommand(input: string): boolean {
const text = input.trim().toLowerCase()
return text === "/compact" || text === "/summarize"
}
export function createPromptHistory(items?: RunPrompt[]): PromptHistoryState {
const list = (items ?? []).filter((item) => item.text.trim().length > 0).map(promptCopy)
const next: RunPrompt[] = []

View file

@ -4,13 +4,13 @@
// operations drain one at a time. Ordinary prompts submitted during an active
// ordinary turn are admitted immediately to the server's durable queue.
//
// The queue also handles /exit, /quit, and /new commands, empty-prompt rejection,
// The queue also handles local session commands, empty-prompt rejection,
// and tracks per-turn wall-clock duration for the footer status line.
//
// Resolves when the footer closes and all in-flight work finishes.
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Locale } from "../util/locale"
import { isExitCommand, isNewCommand } from "./prompt.shared"
import { isCompactCommand, isExitCommand, isNewCommand } from "./prompt.shared"
import type { FooterApi, FooterEvent, RunPrompt } from "./types"
type Trace = {
@ -24,6 +24,7 @@ export type QueueInput = {
onSend?: (prompt: RunPrompt, delivery: "steer" | "queue") => void
onAdmissionError?: (prompt: RunPrompt, error: unknown) => void | Promise<void>
onNewSession?: () => void | Promise<void>
onCompact?: () => void | Promise<void>
admit: (prompt: RunPrompt, signal: AbortSignal) => Promise<void>
settle: () => Promise<void>
run: (prompt: RunPrompt, signal: AbortSignal, admitted: () => void) => Promise<void>
@ -126,6 +127,24 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
continue
}
if (prompt.mode !== "shell" && isCompactCommand(prompt.text)) {
emit(
{
type: "stream.patch",
patch: {
phase: "running",
status: "compacting session",
},
},
{
phase: "running",
status: "compacting session",
},
)
await input.onCompact?.()
continue
}
const sent =
prompt.mode === "shell"
? prompt
@ -251,7 +270,11 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
active.mode !== "shell" &&
prompt.mode !== "shell" &&
prompt.command?.source !== "skill" &&
!isNewCommand(prompt.text)
!isNewCommand(prompt.text) &&
!isCompactCommand(prompt.text) &&
!state.queue.some(
(item) => item.mode !== "shell" && (isNewCommand(item.text) || isCompactCommand(item.text)),
)
) {
const sent = { ...prompt, messageID: SessionMessage.ID.create() }
const admission = state.admission
@ -265,7 +288,7 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
}
state.queue.push(prompt)
if (prompt.mode !== "shell" && isNewCommand(prompt.text)) {
if (prompt.mode !== "shell" && (isNewCommand(prompt.text) || isCompactCommand(prompt.text))) {
drain()
return
}

View file

@ -876,6 +876,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
})
},
onAdmissionError: renderPromptError,
onCompact: async () => {
await state.sdk.session.compact({ sessionID: state.sessionID }, formRequestOptions(state.location))
},
settle: async () => {
const next = await ensureStream()
await next.handle.waitForIdle()