mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 19:55:11 +00:00
fix(session): restore compact slash arguments
This commit is contained in:
parent
f5d20c580b
commit
fbce48f597
8 changed files with 160 additions and 10 deletions
|
|
@ -18,6 +18,7 @@ const optimistic: Array<{
|
|||
const optimisticSeeded: boolean[] = []
|
||||
const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
|
||||
const promoted: Array<{ directory: string; sessionID: string }> = []
|
||||
const sentSummaries: Array<{ sessionID: string; instructions?: string }> = []
|
||||
const sentShell: string[] = []
|
||||
const syncedDirectories: string[] = []
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ let params: { id?: string } = {}
|
|||
let selected = "/repo/worktree-a"
|
||||
let variant: string | undefined
|
||||
|
||||
const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
||||
let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
||||
|
||||
const clientFor = (directory: string) => {
|
||||
createdClients.push(directory)
|
||||
|
|
@ -47,6 +48,10 @@ const clientFor = (directory: string) => {
|
|||
prompt: async () => ({ data: undefined }),
|
||||
promptAsync: async () => ({ data: undefined }),
|
||||
command: async () => ({ data: undefined }),
|
||||
summarize: async (input: { sessionID: string; instructions?: string }) => {
|
||||
sentSummaries.push({ sessionID: input.sessionID, instructions: input.instructions })
|
||||
return { data: undefined }
|
||||
},
|
||||
abort: async () => ({ data: undefined }),
|
||||
},
|
||||
worktree: {
|
||||
|
|
@ -208,6 +213,8 @@ beforeEach(() => {
|
|||
optimistic.length = 0
|
||||
optimisticSeeded.length = 0
|
||||
promoted.length = 0
|
||||
promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
||||
sentSummaries.length = 0
|
||||
params = {}
|
||||
sentShell.length = 0
|
||||
syncedDirectories.length = 0
|
||||
|
|
@ -342,4 +349,31 @@ describe("prompt submit worktree selection", () => {
|
|||
expect(storedSessions["/repo/worktree-a"]).toEqual([{ id: "session-1", title: "New session 1" }])
|
||||
expect(optimisticSeeded).toEqual([true])
|
||||
})
|
||||
|
||||
test("submits typed compact slash arguments as summarize instructions", async () => {
|
||||
params = { id: "session-1" }
|
||||
promptValue = [{ type: "text", content: "/compact keep unresolved TODOs", start: 0, end: 30 }]
|
||||
|
||||
const submit = createPromptSubmit({
|
||||
info: () => ({ id: "session-1" }),
|
||||
imageAttachments: () => [],
|
||||
commentCount: () => 0,
|
||||
autoAccept: () => false,
|
||||
mode: () => "normal",
|
||||
working: () => false,
|
||||
editor: () => undefined,
|
||||
queueScroll: () => undefined,
|
||||
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||
addToHistory: () => undefined,
|
||||
resetHistoryNavigation: () => undefined,
|
||||
setMode: () => undefined,
|
||||
setPopover: () => undefined,
|
||||
onSubmit: () => undefined,
|
||||
})
|
||||
|
||||
await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event)
|
||||
|
||||
expect(sentSummaries).toEqual([{ sessionID: "session-1", instructions: "keep unresolved TODOs" }])
|
||||
expect(optimistic).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -50,6 +50,21 @@ const draftText = (prompt: Prompt) => prompt.map((part) => ("content" in part ?
|
|||
|
||||
const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttachmentPart => part.type === "image")
|
||||
|
||||
const slashCommand = (text: string) => {
|
||||
if (!text.startsWith("/")) return undefined
|
||||
return text.split("\n")[0].split(" ")[0].slice(1)
|
||||
}
|
||||
|
||||
const slashArguments = (text: string) => {
|
||||
const firstLineEnd = text.indexOf("\n")
|
||||
const firstLine = firstLineEnd === -1 ? text : text.slice(0, firstLineEnd)
|
||||
const [, ...firstLineArgs] = firstLine.split(" ")
|
||||
const restOfInput = firstLineEnd === -1 ? "" : text.slice(firstLineEnd + 1)
|
||||
return firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
|
||||
}
|
||||
|
||||
const isCompactSlash = (command: string | undefined) => command === "compact" || command === "summarize"
|
||||
|
||||
export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
const text = draftText(input.draft.prompt)
|
||||
const images = draftImages(input.draft.prompt)
|
||||
|
|
@ -71,8 +86,27 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
|||
return true
|
||||
}
|
||||
|
||||
const [head, ...tail] = text.split(" ")
|
||||
const cmd = head?.startsWith("/") ? head.slice(1) : undefined
|
||||
const cmd = slashCommand(text)
|
||||
if (isCompactSlash(cmd)) {
|
||||
setBusy()
|
||||
try {
|
||||
if (!(await wait())) {
|
||||
setIdle()
|
||||
return false
|
||||
}
|
||||
|
||||
const instructions = slashArguments(text).trim()
|
||||
const payload = instructions
|
||||
? { sessionID: input.draft.sessionID, ...input.draft.model, instructions }
|
||||
: { sessionID: input.draft.sessionID, ...input.draft.model }
|
||||
await input.client.session.summarize(payload)
|
||||
return true
|
||||
} catch (err) {
|
||||
setIdle()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd && input.sync.data.command.find((item) => item.name === cmd)) {
|
||||
setBusy()
|
||||
try {
|
||||
|
|
@ -84,7 +118,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
|||
await input.client.session.command({
|
||||
sessionID: input.draft.sessionID,
|
||||
command: cmd,
|
||||
arguments: tail.join(" "),
|
||||
arguments: slashArguments(text),
|
||||
agent: input.draft.agent,
|
||||
model: `${input.draft.model.providerID}/${input.draft.model.modelID}`,
|
||||
variant: input.draft.variant,
|
||||
|
|
@ -453,8 +487,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
}
|
||||
|
||||
if (text.startsWith("/")) {
|
||||
const [cmdName, ...args] = text.split(" ")
|
||||
const commandName = cmdName.slice(1)
|
||||
const commandName = slashCommand(text) ?? ""
|
||||
const customCommand = sync.data.command.find((c) => c.name === commandName)
|
||||
if (customCommand) {
|
||||
clearInput()
|
||||
|
|
@ -462,7 +495,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
.command({
|
||||
sessionID: session.id,
|
||||
command: commandName,
|
||||
arguments: args.join(" "),
|
||||
arguments: slashArguments(text),
|
||||
agent,
|
||||
model: `${model.providerID}/${model.modelID}`,
|
||||
variant,
|
||||
|
|
@ -483,6 +516,22 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isCompactSlash(commandName)) {
|
||||
clearInput()
|
||||
const instructions = slashArguments(text).trim()
|
||||
const payload = instructions
|
||||
? { sessionID: session.id, ...model, instructions }
|
||||
: { sessionID: session.id, ...model }
|
||||
client.session.summarize(payload).catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
restoreInput()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
|
||||
|
|
|
|||
|
|
@ -1143,6 +1143,21 @@ export function Prompt(props: PromptProps) {
|
|||
command: inputText,
|
||||
})
|
||||
setStore("mode", "normal")
|
||||
} else if (isCompactSlash(inputText)) {
|
||||
const instructions = slashArguments(inputText).trim()
|
||||
const payload = instructions
|
||||
? {
|
||||
sessionID,
|
||||
modelID: selectedModel.modelID,
|
||||
providerID: selectedModel.providerID,
|
||||
instructions,
|
||||
}
|
||||
: {
|
||||
sessionID,
|
||||
modelID: selectedModel.modelID,
|
||||
providerID: selectedModel.providerID,
|
||||
}
|
||||
void sdk.client.session.summarize(payload)
|
||||
} else if (
|
||||
inputText.startsWith("/") &&
|
||||
iife(() => {
|
||||
|
|
@ -1220,6 +1235,19 @@ export function Prompt(props: PromptProps) {
|
|||
input.clear()
|
||||
return true
|
||||
}
|
||||
|
||||
function isCompactSlash(text: string) {
|
||||
const command = text.split("\n")[0].split(" ")[0].slice(1)
|
||||
return text.startsWith("/") && (command === "compact" || command === "summarize")
|
||||
}
|
||||
|
||||
function slashArguments(text: string) {
|
||||
const firstLineEnd = text.indexOf("\n")
|
||||
const firstLine = firstLineEnd === -1 ? text : text.slice(0, firstLineEnd)
|
||||
const [, ...firstLineArgs] = firstLine.split(" ")
|
||||
const restOfInput = firstLineEnd === -1 ? "" : text.slice(firstLineEnd + 1)
|
||||
return firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
|
||||
}
|
||||
const exit = useExit()
|
||||
|
||||
function pasteText(text: string, virtualText: string) {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export const SummarizePayload = Schema.Struct({
|
|||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
auto: Schema.optional(Schema.Boolean),
|
||||
instructions: Schema.optional(Schema.String),
|
||||
})
|
||||
export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"]))
|
||||
export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInput.fields, ["sessionID"]))
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|||
modelID: ctx.payload.modelID,
|
||||
},
|
||||
auto: ctx.payload.auto ?? false,
|
||||
instructions: ctx.payload.instructions,
|
||||
})
|
||||
yield* promptSvc.loop({ sessionID: ctx.params.sessionID })
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -133,6 +133,12 @@ function buildPrompt(input: { previousSummary?: string; context: string[] }) {
|
|||
return [anchor, SUMMARY_TEMPLATE, ...input.context].join("\n\n")
|
||||
}
|
||||
|
||||
function withInstructions(prompt: string, instructions: string | undefined) {
|
||||
const trimmed = instructions?.trim()
|
||||
if (!trimmed) return prompt
|
||||
return [prompt, "Additional user instructions for this compaction:", trimmed].join("\n\n")
|
||||
}
|
||||
|
||||
function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) {
|
||||
return (
|
||||
input.cfg.compaction?.preserve_recent_tokens ??
|
||||
|
|
@ -202,6 +208,7 @@ export interface Interface {
|
|||
model: { providerID: ProviderID; modelID: ModelID }
|
||||
auto: boolean
|
||||
overflow?: boolean
|
||||
instructions?: string
|
||||
}) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
|
|
@ -400,7 +407,10 @@ export const layer = Layer.effect(
|
|||
{ sessionID: input.sessionID },
|
||||
{ context: [], prompt: undefined },
|
||||
)
|
||||
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
|
||||
const nextPrompt = withInstructions(
|
||||
compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context }),
|
||||
compactionPart?.instructions,
|
||||
)
|
||||
const msgs = structuredClone(selected.head)
|
||||
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
||||
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
|
||||
|
|
@ -587,6 +597,7 @@ export const layer = Layer.effect(
|
|||
model: { providerID: ProviderID; modelID: ModelID }
|
||||
auto: boolean
|
||||
overflow?: boolean
|
||||
instructions?: string
|
||||
}) {
|
||||
const msg = yield* session.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
|
|
@ -603,6 +614,7 @@ export const layer = Layer.effect(
|
|||
type: "compaction",
|
||||
auto: input.auto,
|
||||
overflow: input.overflow,
|
||||
instructions: input.instructions,
|
||||
})
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Compaction.Started, {
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ export const CompactionPart = Schema.Struct({
|
|||
type: Schema.Literal("compaction"),
|
||||
auto: Schema.Boolean,
|
||||
overflow: Schema.optional(Schema.Boolean),
|
||||
instructions: Schema.optional(Schema.String),
|
||||
tail_start_id: Schema.optional(MessageID),
|
||||
}).annotate({ identifier: "CompactionPart" })
|
||||
export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
|
||||
|
|
|
|||
|
|
@ -287,8 +287,8 @@ function compactionProcessLayer(options?: CompactionProcessOptions) {
|
|||
)
|
||||
}
|
||||
|
||||
function createSummaryCompaction(sessionID: SessionID) {
|
||||
return SessionCompaction.use.create({ sessionID, agent: "build", model: ref, auto: false })
|
||||
function createSummaryCompaction(sessionID: SessionID, instructions?: string) {
|
||||
return SessionCompaction.use.create({ sessionID, agent: "build", model: ref, auto: false, instructions })
|
||||
}
|
||||
|
||||
function readCompactionPart(sessionID: SessionID) {
|
||||
|
|
@ -959,6 +959,30 @@ describe("session.compaction.process", () => {
|
|||
}).pipe(withCompaction({ config: cfg({ tail_turns: 2, preserve_recent_tokens: 100 }) })),
|
||||
)
|
||||
|
||||
itCompaction.instance(
|
||||
"appends stored instructions to compaction prompt",
|
||||
() => {
|
||||
const stub = llm()
|
||||
let captured = ""
|
||||
stub.push(reply("summary", (input) => (captured = JSON.stringify(input.messages))))
|
||||
return Effect.gen(function* () {
|
||||
const ssn = yield* SessionNs.Service
|
||||
const session = yield* ssn.create({})
|
||||
yield* createUserMessage(session.id, "first")
|
||||
yield* createSummaryCompaction(session.id, "focus on unresolved TODOs")
|
||||
|
||||
const msgs = yield* ssn.messages({ sessionID: session.id })
|
||||
const parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false })
|
||||
|
||||
expect(captured).toContain("Additional user instructions for this compaction")
|
||||
expect(captured).toContain("focus on unresolved TODOs")
|
||||
}).pipe(withCompaction({ llm: stub.layer }))
|
||||
},
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
itCompaction.instance(
|
||||
"falls back to full summary when even one recent turn exceeds preserve token budget",
|
||||
() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue