mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-24 14:33:37 +00:00
feat(tui): add compact command to mini (#38251)
This commit is contained in:
parent
ca6da05d07
commit
691a7d93c8
7 changed files with 109 additions and 6 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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[] = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -410,7 +410,8 @@ test("direct command panel renders grouped command palette", async () => {
|
|||
expect(frame).toContain("Open editor")
|
||||
expect(frame).toContain("/editor")
|
||||
expect(frame).toContain("Show status")
|
||||
expect(frame).toContain("Switch model")
|
||||
expect(frame).toContain("Compact session")
|
||||
expect(frame).toContain("/compact")
|
||||
expect(frame).toContain("Skills")
|
||||
expect(frame).toContain("/skills")
|
||||
expect(frame.match(/\bAgent\b/g)?.length).toBe(1)
|
||||
|
|
|
|||
|
|
@ -80,6 +80,63 @@ describe("run runtime queue", () => {
|
|||
])
|
||||
})
|
||||
|
||||
test.each(["/compact", "/summarize"])("treats %s as a local compaction command", async (command) => {
|
||||
const ui = createFooterApiFixture()
|
||||
const seen: string[] = []
|
||||
let compacted = 0
|
||||
|
||||
const task = runPromptQueue({
|
||||
footer: ui.api,
|
||||
onCompact: async () => {
|
||||
compacted += 1
|
||||
},
|
||||
run: async (input) => {
|
||||
seen.push(input.text)
|
||||
ui.api.close()
|
||||
},
|
||||
})
|
||||
|
||||
ui.submit(command)
|
||||
ui.submit("hello")
|
||||
await task
|
||||
|
||||
expect(compacted).toBe(1)
|
||||
expect(seen).toEqual(["hello"])
|
||||
expect(ui.commits.map((item) => item.text)).toEqual(["hello"])
|
||||
})
|
||||
|
||||
test("keeps prompts submitted after an in-flight /compact behind the compaction barrier", async () => {
|
||||
const ui = createFooterApiFixture()
|
||||
const active = Promise.withResolvers<void>()
|
||||
const order: string[] = []
|
||||
|
||||
const task = runPromptQueue({
|
||||
footer: ui.api,
|
||||
onCompact: async () => {
|
||||
order.push("compact")
|
||||
},
|
||||
admit: async (prompt) => {
|
||||
order.push(`admit:${prompt.text}`)
|
||||
},
|
||||
run: async (prompt) => {
|
||||
order.push(`run:${prompt.text}`)
|
||||
if (prompt.text === "first") await active.promise
|
||||
if (prompt.text === "later") ui.api.close()
|
||||
},
|
||||
})
|
||||
|
||||
ui.submit("first")
|
||||
await Promise.resolve()
|
||||
ui.submit("/compact")
|
||||
ui.submit("later")
|
||||
await Promise.resolve()
|
||||
expect(order).toEqual(["run:first"])
|
||||
|
||||
active.resolve()
|
||||
await task
|
||||
expect(order).toEqual(["run:first", "compact", "run:later"])
|
||||
})
|
||||
|
||||
test("shell mode submits /exit as a shell command", async () => {
|
||||
const ui = createFooterApiFixture()
|
||||
const seen: RunPrompt[] = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue