mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 10:16:03 +00:00
fix(tui): restore compaction model marker
This commit is contained in:
@@ -117,17 +117,6 @@ export type SessionMessageCompactionRunning = {
|
||||
recent: string
|
||||
}
|
||||
|
||||
export type SessionMessageCompactionCompleted = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
time: { created: number }
|
||||
status: "completed"
|
||||
reason: "auto" | "manual"
|
||||
summary: string
|
||||
recent: string
|
||||
}
|
||||
|
||||
export type InstructionEntryKey = string
|
||||
|
||||
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||
@@ -507,6 +496,18 @@ export type SessionMessageModelSelected = {
|
||||
previous?: ModelRef
|
||||
}
|
||||
|
||||
export type SessionMessageCompactionCompleted = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
time: { created: number }
|
||||
status: "completed"
|
||||
reason: "auto" | "manual"
|
||||
model?: ModelRef
|
||||
summary: string
|
||||
recent: string
|
||||
}
|
||||
|
||||
export type CommandInfo = {
|
||||
name: string
|
||||
template: string
|
||||
@@ -749,7 +750,7 @@ export type SessionCompactionEnded = {
|
||||
type: "session.compaction.ended"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string }
|
||||
data: { sessionID: string; reason: "auto" | "manual"; model?: ModelRef; text: string; recent: string }
|
||||
}
|
||||
|
||||
export type SessionRevertCleared = {
|
||||
|
||||
@@ -69,6 +69,7 @@ export type AutoInput = {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
readonly model: Model
|
||||
readonly ref: SessionMessage.Assistant["model"]
|
||||
}
|
||||
|
||||
export type ManualInput = {
|
||||
@@ -80,6 +81,7 @@ export type ManualInput = {
|
||||
type Plan = {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly model: Model
|
||||
readonly ref: SessionMessage.Assistant["model"]
|
||||
readonly reason: SessionMessage.Compaction["reason"]
|
||||
readonly prompt: string
|
||||
readonly recent: string
|
||||
@@ -291,6 +293,7 @@ const make = (dependencies: Dependencies) => {
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
||||
sessionID: plan.sessionID,
|
||||
reason: plan.reason,
|
||||
model: plan.ref,
|
||||
text: summary,
|
||||
recent: plan.recent,
|
||||
})
|
||||
@@ -302,6 +305,7 @@ const make = (dependencies: Dependencies) => {
|
||||
return yield* execute({
|
||||
sessionID: input.sessionID,
|
||||
model: input.model,
|
||||
ref: input.ref,
|
||||
reason: "auto",
|
||||
...content,
|
||||
})
|
||||
@@ -350,6 +354,7 @@ const make = (dependencies: Dependencies) => {
|
||||
return yield* execute({
|
||||
sessionID: input.session.id,
|
||||
model: resolved.model,
|
||||
ref: resolved.ref,
|
||||
reason: "manual",
|
||||
inputID: input.inputID,
|
||||
...content,
|
||||
|
||||
@@ -470,6 +470,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
...current,
|
||||
status: "completed",
|
||||
reason: event.data.reason,
|
||||
model: event.data.model,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
})
|
||||
@@ -482,6 +483,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
status: "completed",
|
||||
metadata: event.metadata,
|
||||
reason: event.data.reason,
|
||||
model: event.data.model,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
time: { created: event.created },
|
||||
|
||||
@@ -187,7 +187,7 @@ const layer = Layer.effect(
|
||||
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
|
||||
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
|
||||
const context = history.entries.map((entry) => entry.message)
|
||||
const compactionInput = { sessionID: session.id, messages: context, model }
|
||||
const compactionInput = { sessionID: session.id, messages: context, model, ref: resolved.ref }
|
||||
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
|
||||
const compacted = yield* compaction.compact(compactionInput)
|
||||
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
||||
@@ -331,8 +331,8 @@ const layer = Layer.effect(
|
||||
recoverOverflow &&
|
||||
!publisher.hasRetryEvidence() &&
|
||||
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
|
||||
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model }))).status ===
|
||||
"completed"
|
||||
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model, ref: resolved.ref })))
|
||||
.status === "completed"
|
||||
)
|
||||
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
|
||||
|
||||
|
||||
@@ -154,7 +154,13 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
expect(requests[0]?.generation).toBeUndefined()
|
||||
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
|
||||
expect(yield* store.context(sessionID)).toMatchObject([
|
||||
{ type: "compaction", reason: "manual", summary: "manual summary", recent: "" },
|
||||
{
|
||||
type: "compaction",
|
||||
reason: "manual",
|
||||
model: { id: "summary-model", providerID: "test" },
|
||||
summary: "manual summary",
|
||||
recent: "",
|
||||
},
|
||||
])
|
||||
expect(
|
||||
yield* db
|
||||
|
||||
@@ -488,6 +488,7 @@ export namespace Compaction {
|
||||
schema: {
|
||||
...Base,
|
||||
reason: Started.data.fields.reason,
|
||||
model: Model.Ref.pipe(optional),
|
||||
text: Schema.String,
|
||||
recent: Schema.String,
|
||||
},
|
||||
|
||||
@@ -222,6 +222,7 @@ export const CompactionCompleted = Schema.Struct({
|
||||
...CompactionBase,
|
||||
status: Schema.tag("completed"),
|
||||
reason: Schema.Literals(["auto", "manual"]),
|
||||
model: Model.Ref.pipe(optional),
|
||||
summary: Schema.String,
|
||||
recent: Schema.String,
|
||||
}).annotate({ identifier: "Session.Message.Compaction.Completed" })
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { SessionMessage } from "../src/session-message.js"
|
||||
|
||||
describe("session message", () => {
|
||||
test("decodes completed compactions recorded before model attribution", () => {
|
||||
const encoded = {
|
||||
type: "compaction" as const,
|
||||
id: "msg_compaction",
|
||||
time: { created: 0 },
|
||||
status: "completed" as const,
|
||||
reason: "manual" as const,
|
||||
summary: "summary",
|
||||
recent: "",
|
||||
}
|
||||
const message = Schema.decodeUnknownSync(SessionMessage.CompactionCompleted)(encoded)
|
||||
|
||||
expect(message.model).toBeUndefined()
|
||||
expect(Schema.encodeSync(SessionMessage.CompactionCompleted)(message)).toEqual(encoded)
|
||||
})
|
||||
})
|
||||
@@ -732,6 +732,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
Object.assign(current, {
|
||||
status: "completed",
|
||||
reason: event.data.reason,
|
||||
model: event.data.model,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
})
|
||||
@@ -742,6 +743,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
reason: event.data.reason,
|
||||
model: event.data.model,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
time: { created: event.created },
|
||||
|
||||
@@ -73,7 +73,7 @@ import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keyma
|
||||
import { usePathFormatter } from "../../context/path-format"
|
||||
import { LocationProvider } from "../../context/location"
|
||||
import { createSessionRows, resolvePart, type PartRef, type SessionRow } from "./rows"
|
||||
import { switchLabel } from "../../util/model"
|
||||
import { compactionMarker, switchLabel } from "../../util/model"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -1306,10 +1306,14 @@ function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { typ
|
||||
|
||||
function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type: "compaction" }> }) {
|
||||
const ctx = use()
|
||||
const local = useLocal()
|
||||
const { theme, syntax } = useTheme()
|
||||
const status = () => props.message.status
|
||||
const text = () => (props.message.status === "failed" ? props.message.error.message : props.message.summary)
|
||||
const content = createMemo(() => text().trim())
|
||||
const marker = createMemo(() =>
|
||||
compactionMarker(props.message.status === "completed" ? props.message.model : undefined, ctx.models()),
|
||||
)
|
||||
const color = () => (status() === "failed" ? theme.error : theme.textMuted)
|
||||
return (
|
||||
<box>
|
||||
@@ -1344,6 +1348,16 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={marker()}>
|
||||
{(value) => (
|
||||
<box paddingLeft={3} marginTop={1}>
|
||||
<text>
|
||||
<span style={{ fg: local.agent.color(value().agent) }}>{Locale.titlecase(value().agent)}</span>
|
||||
<span style={{ fg: theme.textMuted }}> · {value().model}</span>
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,18 @@ export function formatRef(model: { providerID: string; id: string; variant?: str
|
||||
return [model.providerID, model.id, model.variant].filter((value) => value !== undefined).join("/")
|
||||
}
|
||||
|
||||
export function compactionMarker(
|
||||
model: { providerID: string; id: string; variant?: string } | undefined,
|
||||
models?: readonly { providerID: string; id: string; name: string }[],
|
||||
) {
|
||||
if (!model) return
|
||||
return {
|
||||
agent: "compaction",
|
||||
model:
|
||||
models?.find((item) => item.providerID === model.providerID && item.id === model.id)?.name ?? formatRef(model),
|
||||
}
|
||||
}
|
||||
|
||||
export function switchLabel(
|
||||
model: { providerID: string; id: string; variant?: string },
|
||||
models?: readonly { providerID: string; id: string; name: string }[],
|
||||
|
||||
@@ -1126,7 +1126,13 @@ test("tracks session status from active sessions and execution events", async ()
|
||||
created: 0,
|
||||
type: "session.compaction.ended",
|
||||
durable: durable("session-live", 5),
|
||||
data: { sessionID: "session-live", reason: "auto", text: "Live summary", recent: "recent" },
|
||||
data: {
|
||||
sessionID: "session-live",
|
||||
reason: "auto",
|
||||
model: { providerID: "anthropic", id: "claude-sonnet" },
|
||||
text: "Live summary",
|
||||
recent: "recent",
|
||||
},
|
||||
})
|
||||
await wait(() => {
|
||||
const message = data.session.message.get("session-live", "msg_compaction_started")
|
||||
@@ -1135,6 +1141,7 @@ test("tracks session status from active sessions and execution events", async ()
|
||||
expect(data.session.message.get("session-live", "msg_compaction_started")).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
model: { providerID: "anthropic", id: "claude-sonnet" },
|
||||
summary: "Live summary",
|
||||
})
|
||||
expect(rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")).toBe(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { formatRef, parse, switchLabel } from "../../src/util/model"
|
||||
import { compactionMarker, formatRef, parse, switchLabel } from "../../src/util/model"
|
||||
|
||||
describe("util.model", () => {
|
||||
test("splits provider from a nested model identifier", () => {
|
||||
@@ -12,6 +12,20 @@ describe("util.model", () => {
|
||||
expect(formatRef({ providerID: "anthropic", id: "sonnet" })).toBe("anthropic/sonnet")
|
||||
})
|
||||
|
||||
test("labels completed compactions with their actual model", () => {
|
||||
const models = [{ providerID: "anthropic", id: "sonnet", name: "Claude Sonnet" }]
|
||||
|
||||
expect(compactionMarker({ providerID: "anthropic", id: "sonnet" }, models)).toEqual({
|
||||
agent: "compaction",
|
||||
model: "Claude Sonnet",
|
||||
})
|
||||
expect(compactionMarker({ providerID: "removed", id: "gone", variant: "high" }, models)).toEqual({
|
||||
agent: "compaction",
|
||||
model: "removed/gone/high",
|
||||
})
|
||||
expect(compactionMarker(undefined, models)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("includes the selected variant in model switch notices", () => {
|
||||
expect(switchLabel({ providerID: "anthropic", id: "sonnet", variant: "thinking" })).toBe(
|
||||
"Switched model to anthropic/sonnet/thinking",
|
||||
|
||||
Reference in New Issue
Block a user