Compare commits

..

3 Commits

Author SHA1 Message Date
Aiden Cline d1aebc9f85 fix(tui): use active model for compaction 2026-08-10 21:05:20 +00:00
Simon Klee 283258e95b feat(tui): add clipboard image previews and transcript rendering (#41603)
Use the OpenTUI clipboard service for image input, show image previews in
the prompt, and render transcript images with interactive previews.

Note this includes upgrade of opentui to 0.5.1 +
anomalyco/opentui#1271
2026-08-10 22:51:39 +02:00
opencode-agent[bot] d7a7256bb6 test: stabilize Windows CI timing (#41600)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-08-10 15:25:55 -05:00
6 changed files with 128 additions and 22 deletions
@@ -11,7 +11,9 @@ import { createAcpFixture, expectOk, initialize, newSession, selectConfigOption
describe("acp lifecycle subprocess", () => {
test("stdin EOF exits cleanly", async () => {
await using fixture = await createAcpFixture()
expect(await fixture.spawn().close()).toBe(0)
const acp = fixture.spawn()
await initialize(acp)
expect(await acp.close()).toBe(0)
}, 60_000)
test("close capability and close request", async () => {
+1
View File
@@ -465,6 +465,7 @@ Use native v2 fields.`,
},
}),
)
yield* Effect.yieldNow
yield* Effect.promise(() => fs.writeFile(path.join(directory, "reviewer.md"), "Review once"))
yield* configTest.emitChange({ type: "create", path: path.join(directory, "reviewer.md") })
@@ -185,6 +185,7 @@ Review files`,
},
}),
)
yield* Effect.yieldNow
yield* Effect.promise(() => fs.writeFile(path.join(directory, "review.md"), "Review once"))
yield* configTest.emitChange({ type: "create", path: path.join(directory, "review.md") })
yield* configTest.emitChange({ type: "update", path: path.join(directory, "review.md") })
+24 -21
View File
@@ -286,27 +286,30 @@ describe("ShellTool", () => {
),
)
it.live("permissions compound commands separately", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) => {
reset()
return withSession(tmp.path, (registry) =>
executeTool(registry, call({ command: "printf one && printf two" }, "call-compound")),
).pipe(
Effect.andThen(
Effect.sync(() => {
expect(assertions).toHaveLength(1)
expect(assertions[0]).toMatchObject({
resources: ["printf one", "printf two"],
save: ["printf *", "printf *"],
})
}),
),
)
},
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
),
it.live(
"permissions compound commands separately",
() =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) => {
reset()
return withSession(tmp.path, (registry) =>
executeTool(registry, call({ command: "printf one && printf two" }, "call-compound")),
).pipe(
Effect.andThen(
Effect.sync(() => {
expect(assertions).toHaveLength(1)
expect(assertions[0]).toMatchObject({
resources: ["printf one", "printf two"],
save: ["printf *", "printf *"],
})
}),
),
)
},
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
),
{ timeout: 15_000 },
)
it.live(
+13
View File
@@ -928,6 +928,19 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
},
onAdmissionError: renderPromptError,
onCompact: async () => {
await state.switching?.catch(() => {})
if (state.model)
await state.sdk.session.switchModel(
{
sessionID: state.sessionID,
model: {
providerID: state.model.providerID,
id: state.model.modelID,
variant: state.activeVariant,
},
},
formRequestOptions(state.location),
)
await state.sdk.session.compact({ sessionID: state.sessionID }, formRequestOptions(state.location))
},
settle: async () => {
+86
View File
@@ -164,6 +164,92 @@ describe("run interactive runtime", () => {
await task
})
test("switches to the active model and variant before compacting", async () => {
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
const ui = createFooterApiFixture()
const api = ui.api
let lifecycle!: LifecycleInput
const calls: string[] = []
const model = catalogModel({
id: "selected",
providerID: "test",
name: "Selected Model",
variants: ["high"],
})
stubCatalogLists(sdk, {
providers: [catalogProvider("test", "Test Provider")],
models: [model],
})
const switched = spyOn(sdk.session, "switchModel").mockImplementation((input) => {
calls.push("switch")
expect(input).toEqual({
sessionID: "ses_root",
model: { providerID: "test", id: "selected", variant: "high" },
})
return ok(undefined)
})
const compacted = spyOn(sdk.session, "compact").mockImplementation(() => {
calls.push("compact")
api.close()
return ok({}) as never
})
const task = runInteractiveDeferredMode(
{
host: host(),
sdk,
directory: "/tmp",
target: async () => ({
sessionID: "ses_root",
location: { directory: "/tmp", project: { id: "pro-1", directory: "/tmp", canonical: "/tmp" } },
agent: "build",
model: undefined,
variant: undefined,
resume: false,
}),
agent: "build",
model: undefined,
variant: undefined,
files: [],
},
{
createRuntimeLifecycle: async (input) => {
lifecycle = input
return {
footer: api,
onResize: () => () => {},
refreshTheme: () => {},
setTitle: () => {},
resetForReplay: () => Promise.resolve(),
close: () => Promise.resolve(),
}
},
streamTransport: Promise.resolve({
createSessionTransport: async () => ({
runPromptTurn: async () => {},
admitPromptTurn: async () => {},
waitForIdle: async () => {},
interruptActiveTurn: async () => {},
selectSubagent: () => {},
replayOnResize: async () => false,
close: async () => {},
}),
formatUnknownError: (error: unknown) => String(error),
}),
},
)
await ui.promptReady
await lifecycle.onModelSelect?.({ providerID: "test", modelID: "selected" })
await lifecycle.onVariantSelect?.("high")
expect(ui.submit("/compact")).toBe(true)
await task
expect(switched).toHaveBeenCalledTimes(1)
expect(compacted).toHaveBeenCalledTimes(1)
expect(calls).toEqual(["switch", "compact"])
})
test("routes form responses to their owners with global location and local settlement", async () => {
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
const api = footer()