mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-01 22:46:20 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cd634bbaf | ||
|
|
e2e82f18e2 | ||
|
|
91cdb182f0 | ||
|
|
316f7925d3 |
@@ -3,7 +3,7 @@
|
||||
- Current implementation changes belong in `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required.
|
||||
- This repository does not use Changesets. Do not add `.changeset` files; follow the existing release workflow instead.
|
||||
- The default branch in this repo is `v2`.
|
||||
- Base all new branches and worktrees on `v2`, or `origin/v2` when the local `v2` ref is unavailable. Do not base them on `dev`.
|
||||
- Default new branches and worktrees to `v2`, or `origin/v2` when the local `v2` ref is unavailable, and default pull requests to target `v2`. Use another base or target branch when the requester explicitly instructs it.
|
||||
- Local `main` ref may not exist; use `v2` or `origin/v2` for diffs.
|
||||
|
||||
## Live V2 TUI Testing
|
||||
|
||||
@@ -54,7 +54,11 @@ function managedService(options: EnsureOptions) {
|
||||
const reconnectOptions = { ...options, version: undefined }
|
||||
return {
|
||||
reconnect: () => Service.ensure(reconnectOptions),
|
||||
restart: () => Service.replace(reconnectOptions).pipe(Effect.asVoid),
|
||||
restart: () =>
|
||||
Effect.gen(function* () {
|
||||
yield* Service.stop(options)
|
||||
yield* Service.ensure(reconnectOptions)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,20 +51,10 @@ export const incumbent = Effect.fn("service.incumbent")(function* (
|
||||
// becomes discoverable. A contender is never killed merely for slow startup.
|
||||
/** Ensure a healthy, compatible local service is running. */
|
||||
export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOptions = {}) {
|
||||
return yield* ensureService(options, false)
|
||||
})
|
||||
|
||||
/** Replace the registered local service while preserving persistent terminals. */
|
||||
export const replace = Effect.fn("service.replace")(function* (options: EnsureOptions = {}) {
|
||||
return yield* ensureService(options, true)
|
||||
})
|
||||
|
||||
const ensureService = Effect.fnUntraced(function* (options: EnsureOptions, forceReplacement: boolean) {
|
||||
const timing = ensureTiming(options)
|
||||
const contenders = new Set<ServiceContender>()
|
||||
let timeouts: { readonly info: Info; readonly count: number } | undefined
|
||||
let announced = false
|
||||
let replaceCurrent = forceReplacement
|
||||
let lastSpawn = 0
|
||||
let spawnDelay = timing.spawnDelay
|
||||
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) =>
|
||||
@@ -98,7 +88,6 @@ const ensureService = Effect.fnUntraced(function* (options: EnsureOptions, force
|
||||
yield* Effect.logWarning("Background service is unresponsive; recovery cannot preserve persistent terminals")
|
||||
yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()))
|
||||
yield* terminate(info, options, timing)
|
||||
replaceCurrent = false
|
||||
timeouts = undefined
|
||||
lastSpawn = Date.now() - spawnDelay
|
||||
}
|
||||
@@ -106,29 +95,23 @@ const ensureService = Effect.fnUntraced(function* (options: EnsureOptions, force
|
||||
if (service !== undefined) {
|
||||
spawnDelay = timing.spawnDelay
|
||||
const compatible = !service.legacy && matchesVersion(service.version, options)
|
||||
if (!replaceCurrent && compatible && service.state === "ready") {
|
||||
if (compatible && service.state === "ready") {
|
||||
yield* Effect.tryPromise(() => PtyHandoff.complete(options.file ?? fallback(), service.info))
|
||||
return Option.some(service)
|
||||
}
|
||||
if (!replaceCurrent && compatible && service.state === "failed")
|
||||
if (compatible && service.state === "failed")
|
||||
return yield* Effect.fail(new Error("Background service failed to start"))
|
||||
if (!replaceCurrent && compatible) return Option.none<LocalService>()
|
||||
if (compatible) return Option.none<LocalService>()
|
||||
yield* announce("version-mismatch", service.version)
|
||||
replaceCurrent = false
|
||||
if (!service.legacy && service.state === "ready")
|
||||
yield* Effect.tryPromise(() =>
|
||||
PtyHandoff.prepare(options.file ?? fallback(), service.info, timing.requestTimeout),
|
||||
)
|
||||
else {
|
||||
if (!service.legacy)
|
||||
yield* Effect.logWarning("Background service is not ready; replacement cannot preserve persistent terminals")
|
||||
yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()))
|
||||
}
|
||||
yield* terminate(service.info, options, timing).pipe(Effect.ignore)
|
||||
if (!service.legacy && service.state !== "ready")
|
||||
yield* Effect.logWarning("Background service is not ready; replacement cannot preserve persistent terminals")
|
||||
yield* stop({
|
||||
file: options.file,
|
||||
pty: !service.legacy && service.state === "ready" ? "handoff" : "clear",
|
||||
}).pipe(Effect.ignore)
|
||||
lastSpawn = 0
|
||||
return Option.none<LocalService>()
|
||||
} else if (lastSpawn === 0 && info !== undefined) lastSpawn = Date.now()
|
||||
else if (info === undefined) replaceCurrent = false
|
||||
|
||||
const finished = [...contenders].filter(contenderFinished)
|
||||
const failure = finished.map(contenderFailure).find((error): error is Error => error !== undefined)
|
||||
@@ -158,8 +141,12 @@ const ensureService = Effect.fnUntraced(function* (options: EnsureOptions, force
|
||||
|
||||
/** Stop the registered local service. */
|
||||
export const stop = Effect.fn("service.stop")(function* (options: StopOptions = {}) {
|
||||
yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()))
|
||||
const info = yield* read(options.file)
|
||||
if (options.pty === "handoff" && info !== undefined)
|
||||
yield* Effect.tryPromise(() =>
|
||||
PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout),
|
||||
)
|
||||
else yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()))
|
||||
if (info !== undefined) yield* terminate(info, options, defaultEnsureTiming)
|
||||
})
|
||||
|
||||
@@ -307,4 +294,4 @@ const terminate = Effect.fnUntraced(function* (info: Info, options: { readonly f
|
||||
})
|
||||
|
||||
/** Effect-based local service lifecycle operations. */
|
||||
export const Service = { discover, incumbent, ensure, replace, stop, headers, Info }
|
||||
export const Service = { discover, incumbent, ensure, stop, headers, Info }
|
||||
|
||||
@@ -31,21 +31,11 @@ export async function discover(options: DiscoverOptions = {}) {
|
||||
|
||||
/** Ensure a healthy, compatible local service is running. */
|
||||
export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
return ensureService(options, false)
|
||||
}
|
||||
|
||||
/** Replace the registered local service while preserving persistent terminals. */
|
||||
export async function replace(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
return ensureService(options, true)
|
||||
}
|
||||
|
||||
async function ensureService(options: EnsureOptions, forceReplacement: boolean): Promise<Endpoint> {
|
||||
const timing = ensureTiming(options)
|
||||
const deadline = Date.now() + timing.promiseTimeout
|
||||
const contenders = new Set<ServiceContender>()
|
||||
let timeouts: { readonly info: Info; readonly count: number } | undefined
|
||||
let announced = false
|
||||
let replaceCurrent = forceReplacement
|
||||
let lastSpawn = 0
|
||||
let spawnDelay = timing.spawnDelay
|
||||
|
||||
@@ -78,7 +68,6 @@ async function ensureService(options: EnsureOptions, forceReplacement: boolean):
|
||||
console.warn("Background service is unresponsive; recovery cannot preserve persistent terminals")
|
||||
await PtyHandoff.clear(options.file ?? fallback())
|
||||
await terminate(registration.info, options, timing)
|
||||
replaceCurrent = false
|
||||
timeouts = undefined
|
||||
lastSpawn = Date.now() - spawnDelay
|
||||
}
|
||||
@@ -88,27 +77,22 @@ async function ensureService(options: EnsureOptions, forceReplacement: boolean):
|
||||
spawnDelay = timing.spawnDelay
|
||||
const service = registration.service
|
||||
const compatible = !service.legacy && matchesVersion(service.version, options)
|
||||
if (!replaceCurrent && compatible && service.state === "ready") {
|
||||
if (compatible && service.state === "ready") {
|
||||
await PtyHandoff.complete(options.file ?? fallback(), service.info)
|
||||
return service.endpoint
|
||||
}
|
||||
if (!replaceCurrent && compatible && service.state === "failed")
|
||||
throw new Error("Background service failed to start")
|
||||
if (replaceCurrent || !compatible) {
|
||||
if (compatible && service.state === "failed") throw new Error("Background service failed to start")
|
||||
if (!compatible) {
|
||||
announce("version-mismatch", service.version)
|
||||
replaceCurrent = false
|
||||
if (!service.legacy && service.state === "ready")
|
||||
await PtyHandoff.prepare(options.file ?? fallback(), service.info, timing.requestTimeout)
|
||||
else {
|
||||
if (!service.legacy)
|
||||
console.warn("Background service is not ready; replacement cannot preserve persistent terminals")
|
||||
await PtyHandoff.clear(options.file ?? fallback())
|
||||
}
|
||||
await terminate(service.info, options, timing).catch(() => undefined)
|
||||
if (!service.legacy && service.state !== "ready")
|
||||
console.warn("Background service is not ready; replacement cannot preserve persistent terminals")
|
||||
await stop({
|
||||
file: options.file,
|
||||
pty: !service.legacy && service.state === "ready" ? "handoff" : "clear",
|
||||
}).catch(() => undefined)
|
||||
lastSpawn = 0
|
||||
}
|
||||
} else {
|
||||
if (registration.info === undefined) replaceCurrent = false
|
||||
if (lastSpawn === 0 && registration.info !== undefined) lastSpawn = Date.now()
|
||||
const finished = [...contenders].filter(contenderFinished)
|
||||
const failure = finished.map(contenderFailure).find((error) => error !== undefined)
|
||||
@@ -133,8 +117,10 @@ async function ensureService(options: EnsureOptions, forceReplacement: boolean):
|
||||
|
||||
/** Stop the registered local service. */
|
||||
export async function stop(options: StopOptions = {}) {
|
||||
await PtyHandoff.clear(options.file ?? fallback())
|
||||
const info = await read(options.file)
|
||||
if (options.pty === "handoff" && info !== undefined)
|
||||
await PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout)
|
||||
else await PtyHandoff.clear(options.file ?? fallback())
|
||||
if (info !== undefined) await terminate(info, options, defaultEnsureTiming)
|
||||
}
|
||||
|
||||
@@ -266,4 +252,4 @@ function delay(milliseconds: number) {
|
||||
}
|
||||
|
||||
/** Promise-based local service lifecycle operations. */
|
||||
export const Service = { discover, ensure, replace, stop, headers }
|
||||
export const Service = { discover, ensure, stop, headers }
|
||||
|
||||
@@ -38,6 +38,8 @@ export type EnsureOptions = DiscoverOptions & {
|
||||
export type StopOptions = {
|
||||
/** Absolute registration file path. Defaults to the XDG state directory. */
|
||||
readonly file?: string
|
||||
/** How to handle persistent terminals before stopping the service. */
|
||||
readonly pty?: "clear" | "handoff"
|
||||
}
|
||||
|
||||
/** Contents of the local service registration file. */
|
||||
|
||||
@@ -1269,6 +1269,25 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
const target = sessionID
|
||||
history.append(entry)
|
||||
const prepareActionSelection = async () => {
|
||||
if (!session) {
|
||||
await data.session.sync(target)
|
||||
session = data.session.get(target)
|
||||
}
|
||||
if (session?.agent !== agent.id) await client.api.session.switchAgent({ sessionID: target, agent: agent.id })
|
||||
if (
|
||||
session?.model?.providerID === selection.providerID &&
|
||||
session.model.id === selection.modelID &&
|
||||
(session.model.variant ?? "default") === (variant ?? "default")
|
||||
)
|
||||
return
|
||||
const model = { providerID: selection.providerID, id: selection.modelID, variant }
|
||||
const cancelCommit = local.model.trackSessionCommit(target, model)
|
||||
await client.api.session.switchModel({ sessionID: target, model }).catch((error) => {
|
||||
cancelCommit()
|
||||
throw new Error(`Failed to switch model: ${errorMessage(error)}`, { cause: error })
|
||||
})
|
||||
}
|
||||
const dispatch = (send: () => Promise<unknown>) => {
|
||||
const setup = newSession
|
||||
if (setup) void setup.gate.then(send).catch(setup.recover)
|
||||
@@ -1276,11 +1295,15 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
if (currentMode === "shell") {
|
||||
move.startSubmit()
|
||||
dispatch(() => client.api.session.shell({ sessionID: target, command: inputText }))
|
||||
dispatch(async () => {
|
||||
await prepareActionSelection()
|
||||
return client.api.session.shell({ sessionID: target, command: inputText })
|
||||
})
|
||||
setStore("mode", "normal")
|
||||
} else if (slashHead && isCommand) {
|
||||
const send = () =>
|
||||
client.api.session.command({
|
||||
const send = async () => {
|
||||
await prepareActionSelection()
|
||||
return client.api.session.command({
|
||||
sessionID: target,
|
||||
command: slashHead.name,
|
||||
text: slashHead.arguments,
|
||||
@@ -1289,6 +1312,7 @@ export function Prompt(props: PromptProps) {
|
||||
skills: entry.skills?.length ? entry.skills : undefined,
|
||||
delivery,
|
||||
})
|
||||
}
|
||||
const setup = newSession
|
||||
void (setup ? setup.gate.then(send) : send()).catch((error) => {
|
||||
if (setup) return setup.recover(error)
|
||||
@@ -1297,7 +1321,10 @@ export function Prompt(props: PromptProps) {
|
||||
})
|
||||
} else if (isSkill) {
|
||||
move.startSubmit()
|
||||
dispatch(() => client.api.session.skill({ sessionID: target, skill: slashHead.name }))
|
||||
dispatch(async () => {
|
||||
await prepareActionSelection()
|
||||
return client.api.session.skill({ sessionID: target, skill: slashHead.name })
|
||||
})
|
||||
} else {
|
||||
move.startSubmit()
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { EmbeddedTerminalRenderable, type Renderable, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { EmbeddedTerminalRenderable } from "@opentui/core"
|
||||
import { createTestRenderer } from "@opentui/core/testing"
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
@@ -921,66 +921,6 @@ test("error investigations repeatedly seed editable home drafts without creating
|
||||
await setup.waitForFrame((frame) => !frame.includes("Beta initialization failed"))
|
||||
})
|
||||
|
||||
test("shows jump to latest after scrolling one line above the final message", async () => {
|
||||
const session = {
|
||||
id: "dummy",
|
||||
title: "Demo session",
|
||||
projectID: "project",
|
||||
location: { directory },
|
||||
agent: "build",
|
||||
model: { providerID: "provider", id: "model" },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
}
|
||||
const messages = Array.from({ length: 8 }, (_, index) => ({
|
||||
id: `message-${index}`,
|
||||
type: "user",
|
||||
text: index === 7 ? "Final visible message" : `Earlier message ${index}`,
|
||||
time: { created: index },
|
||||
}))
|
||||
await using setup = await createAppFixture({
|
||||
width: 80,
|
||||
height: 20,
|
||||
config: { animations: false, keybinds: { "session.line.up": "f6", "session.line.down": "f7" } },
|
||||
args: { sessionID: "dummy" },
|
||||
fetch: (url) => {
|
||||
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
||||
if (url.pathname === "/api/session/dummy") return json({ data: session })
|
||||
if (url.pathname === "/api/session/dummy/message") return json({ data: messages.toReversed(), cursor: {} })
|
||||
if (url.pathname === "/api/session/dummy/inbox") return json({ data: [] })
|
||||
if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
|
||||
},
|
||||
})
|
||||
|
||||
await setup.waitForFrame((frame) => frame.includes("Final visible message"))
|
||||
const findScrollBox = (root: Renderable): ScrollBoxRenderable | undefined =>
|
||||
root instanceof ScrollBoxRenderable && root.getRenderable("message-7")
|
||||
? root
|
||||
: root.getChildren().map(findScrollBox).find(Boolean)
|
||||
const scroll = findScrollBox(setup.renderer.root)
|
||||
expect(scroll).toBeDefined()
|
||||
if (!scroll) throw new Error("session transcript scrollbox was not found")
|
||||
const maximum = () => Math.max(0, scroll.scrollHeight - scroll.viewport.height)
|
||||
|
||||
expect(scroll.scrollTop).toBe(maximum())
|
||||
const initial = setup.captureCharFrame().split("\n")
|
||||
expect(initial.find((line) => line.includes("Jump to latest"))).toBeUndefined()
|
||||
expect(initial[initial.findIndex((line) => line.includes("Final visible message")) + 1]).toContain("┃")
|
||||
|
||||
setup.mockInput.pressKey("F6")
|
||||
const clipped = (await setup.waitForFrame((frame) => frame.includes("Jump to latest"))).split("\n")
|
||||
expect(scroll.scrollTop).toBe(maximum() - 1)
|
||||
expect(clipped.find((line) => line.includes("Jump to latest"))).toBeDefined()
|
||||
expect(clipped[clipped.findIndex((line) => line.includes("Final visible message")) + 1]).not.toContain("┃")
|
||||
|
||||
setup.mockInput.pressKey("F7")
|
||||
const restored = (await setup.waitForFrame((frame) => !frame.includes("Jump to latest"))).split("\n")
|
||||
expect(scroll.scrollTop).toBe(maximum())
|
||||
expect(restored.find((line) => line.includes("Jump to latest"))).toBeUndefined()
|
||||
expect(restored[restored.findIndex((line) => line.includes("Final visible message")) + 1]).toContain("┃")
|
||||
})
|
||||
|
||||
test("completed user shell output replaces a partial live read when the final read fails", async () => {
|
||||
await using state = await tmpdir()
|
||||
const session = {
|
||||
|
||||
@@ -233,3 +233,115 @@ test.each(["first", "second"])(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
test.each([
|
||||
{ name: "command", text: "/deploy now", endpoint: "command", change: true },
|
||||
{ name: "skill", text: "/tiger", endpoint: "skill", change: true },
|
||||
{ name: "shell", text: "echo hi", endpoint: "shell", change: true },
|
||||
{ name: "unchanged command", text: "/deploy now", endpoint: "command", change: false },
|
||||
])("prepares the prompt selection before $name submission", async ({ name, text, endpoint, change }) => {
|
||||
await using state = await tmpdir()
|
||||
const setup = await createTestRenderer({ width: 100, height: 30, useThread: false, kittyKeyboard: true })
|
||||
setup.renderer.start()
|
||||
const ready = Promise.withResolvers<void>()
|
||||
const invoked = Promise.withResolvers<void>()
|
||||
const events = createEventStream()
|
||||
const sessionID = `ses_${endpoint}_${change ? "changed" : "unchanged"}`
|
||||
const location = { directory, project: { id: "project", directory, canonical: directory } }
|
||||
const mutations: string[] = []
|
||||
const calls = createFetch(async (url, request) => {
|
||||
if (url.pathname === `/api/session/${sessionID}`)
|
||||
return json({
|
||||
data: {
|
||||
id: sessionID,
|
||||
projectID: "project",
|
||||
title: `${name} selection fixture`,
|
||||
agent: "review",
|
||||
model: { providerID: "demo", id: "current" },
|
||||
location: { directory },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
},
|
||||
})
|
||||
if (url.pathname === `/api/session/${sessionID}/message`) return json({ data: [], cursor: {} })
|
||||
if (url.pathname === `/api/session/${sessionID}/inbox` || url.pathname === `/api/session/${sessionID}/permission`)
|
||||
return json({ data: [] })
|
||||
if (url.pathname === "/api/agent")
|
||||
return json({
|
||||
location,
|
||||
data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }],
|
||||
})
|
||||
if (url.pathname === "/api/provider") return json({ location, data: [{ id: "demo", name: "Demo" }] })
|
||||
if (url.pathname === "/api/model")
|
||||
return json({
|
||||
location,
|
||||
data: ["current", "selected"].map((id) => ({
|
||||
id,
|
||||
providerID: "demo",
|
||||
name: `${id} model`,
|
||||
variants: [],
|
||||
cost: [],
|
||||
time: { released: 0 },
|
||||
})),
|
||||
})
|
||||
if (url.pathname === "/api/command")
|
||||
return json({ location, data: [{ name: "deploy", description: "Deploy", template: "" }] })
|
||||
if (url.pathname === "/api/skill")
|
||||
return json({
|
||||
location,
|
||||
data: [{ id: "tiger", name: "Tiger", description: "Tiger", slash: true, location: directory, content: "" }],
|
||||
})
|
||||
if (request.method === "POST" && url.pathname === `/api/session/${sessionID}/agent`) {
|
||||
mutations.push(`agent:${(await request.json()).agent}`)
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === `/api/session/${sessionID}/model`) {
|
||||
mutations.push(`model:${(await request.json()).model.id}`)
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === `/api/session/${sessionID}/${endpoint}`) {
|
||||
mutations.push(endpoint)
|
||||
invoked.resolve()
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
return undefined
|
||||
}, events)
|
||||
const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) })
|
||||
const { run } = await import("../src/app")
|
||||
const task = Effect.runPromise(
|
||||
run({
|
||||
app: { name: "test", version: "test", channel: "test" },
|
||||
server: { endpoint: { url: server.url.toString() } },
|
||||
config: { get: async () => ({ animations: false }), update: async () => ({}) },
|
||||
packages: { resolve: async () => undefined },
|
||||
terminalHandoff: async () => ({ renderer: setup.renderer, mode: "dark", complete: ready.resolve }),
|
||||
args: { sessionID },
|
||||
log: () => {},
|
||||
}).pipe(Effect.provide(Global.layerWith({ state: state.path })), Effect.provide(FileSystem.layerNoop({}))),
|
||||
)
|
||||
try {
|
||||
await ready.promise
|
||||
await setup.waitForFrame((frame) => frame.includes("current model"))
|
||||
if (change) {
|
||||
await setup.mockInput.typeText("/models")
|
||||
setup.mockInput.pressEnter()
|
||||
await setup.waitForFrame(
|
||||
(frame) => frame.includes("Select model") && setup.renderer.currentFocusedRenderable instanceof InputRenderable,
|
||||
)
|
||||
await setup.mockInput.typeText("selected")
|
||||
setup.mockInput.pressEnter()
|
||||
await setup.waitForFrame((frame) => frame.includes("selected model") && !frame.includes("Select model"))
|
||||
}
|
||||
if (name === "shell") setup.mockInput.typeText("!")
|
||||
await setup.mockInput.typeText(text)
|
||||
if (name === "skill") setup.mockInput.pressEscape()
|
||||
setup.mockInput.pressEnter()
|
||||
await invoked.promise
|
||||
expect(mutations).toEqual(["agent:build", ...(change ? ["model:selected"] : []), endpoint])
|
||||
} finally {
|
||||
setup.renderer.destroy()
|
||||
await task
|
||||
await server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user