mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-28 14:11:49 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad9d97a92e | |||
| 3b0d8f0e6f | |||
| 4d59b059ee | |||
| b8c548b51b | |||
| 29494bbf71 |
@@ -77,6 +77,7 @@ export function DevToolsBar() {
|
||||
const runtime = createMemo(() => runtimeStatus(frontendSamples()))
|
||||
const timing = () => config.data.debug?.timing ?? false
|
||||
const turnTokens = () => config.data.debug?.turn_tokens ?? false
|
||||
const verboseTurnTokens = () => turnTokens() === "verbose"
|
||||
|
||||
const offEscape = keymap.intercept(
|
||||
"key",
|
||||
@@ -380,6 +381,16 @@ export function DevToolsBar() {
|
||||
>
|
||||
{turnTokens() ? "[x]" : "[ ]"} Turn token usage
|
||||
</Action>
|
||||
<Action
|
||||
onClick={() =>
|
||||
void config.update((draft) => {
|
||||
draft.debug = { ...draft.debug, turn_tokens: verboseTurnTokens() ? true : "verbose" }
|
||||
})
|
||||
}
|
||||
hoverBackground
|
||||
>
|
||||
{verboseTurnTokens() ? "[x]" : "[ ]"} Turn token usage (verbose)
|
||||
</Action>
|
||||
</box>
|
||||
<For each={groups()}>
|
||||
{(group) => (
|
||||
|
||||
@@ -254,8 +254,8 @@ export const settings: Setting[] = [
|
||||
category: "Debug",
|
||||
path: ["debug", "turn_tokens"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
values: [false, true, "verbose"],
|
||||
labels: ["off", "on", "verbose"],
|
||||
keywords: ["tokens", "usage", "debug"],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -7,16 +7,20 @@ import { DialogVariant } from "./dialog-variant"
|
||||
import * as fuzzysort from "fuzzysort"
|
||||
import { useConnected } from "./use-connected"
|
||||
import { useData } from "../context/data"
|
||||
import { useLocation } from "../context/location"
|
||||
|
||||
export function DialogModel(props: { providerID?: string }) {
|
||||
const local = useLocal()
|
||||
const data = useData()
|
||||
const location = useLocation()
|
||||
const dialog = useDialog()
|
||||
const [query, setQuery] = createSignal("")
|
||||
|
||||
const connected = useConnected()
|
||||
const providers = createMemo(() => new Map((data.location.provider.list() ?? []).map((item) => [item.id, item])))
|
||||
const models = createMemo(() => data.location.model.list() ?? [])
|
||||
const providers = createMemo(
|
||||
() => new Map((data.location.provider.list(location.ref) ?? []).map((item) => [item.id, item])),
|
||||
)
|
||||
const models = createMemo(() => data.location.model.list(location.ref) ?? [])
|
||||
|
||||
const showExtra = createMemo(() => connected() && !props.providerID)
|
||||
|
||||
|
||||
@@ -360,7 +360,8 @@ export function Prompt(props: PromptProps) {
|
||||
),
|
||||
)
|
||||
|
||||
// Initialize agent/model/variant from the durable V2 Session state.
|
||||
// Initialize the agent from the durable V2 Session state. Model hydration is
|
||||
// reactive in LocalProvider so it can wait for the location catalog.
|
||||
let syncedSessionID: string | undefined
|
||||
createEffect(() => {
|
||||
const sessionID = props.sessionID
|
||||
@@ -369,10 +370,6 @@ export function Prompt(props: PromptProps) {
|
||||
if (!session) return
|
||||
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
||||
if (agent && !args.agent) local.agent.set(agent.id)
|
||||
if (session.model) {
|
||||
local.model.set({ providerID: session.model.providerID, modelID: session.model.id })
|
||||
local.model.variant.set(session.model.variant)
|
||||
}
|
||||
syncedSessionID = sessionID
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { useData } from "../context/data"
|
||||
import { useLocation } from "../context/location"
|
||||
import { hasConnectedProvider } from "../util/connected-provider"
|
||||
|
||||
export function useConnected() {
|
||||
const data = useData()
|
||||
return createMemo(() => hasConnectedProvider(data.location.integration.list() ?? []))
|
||||
const location = useLocation()
|
||||
return createMemo(() => hasConnectedProvider(data.location.integration.list(location.ref) ?? []))
|
||||
}
|
||||
|
||||
@@ -159,7 +159,9 @@ export const Info = Schema.Struct({
|
||||
Schema.Struct({
|
||||
devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools debug bar" }),
|
||||
timing: Schema.optional(Schema.Boolean).annotate({ description: "Show time-to-first-draw diagnostics" }),
|
||||
turn_tokens: Schema.optional(Schema.Boolean).annotate({ description: "Show per-turn token usage diagnostics" }),
|
||||
turn_tokens: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("verbose")])).annotate({
|
||||
description: "Show per-turn token usage diagnostics, optionally with tool call inputs",
|
||||
}),
|
||||
}),
|
||||
).annotate({ description: "Debugging settings" }),
|
||||
animations: Schema.optional(Schema.Boolean).annotate({ description: "Enable interface animations" }),
|
||||
|
||||
@@ -761,6 +761,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
})
|
||||
break
|
||||
case "session.compaction.ended":
|
||||
setStore(
|
||||
"session",
|
||||
"pending",
|
||||
event.data.sessionID,
|
||||
(store.session.pending[event.data.sessionID] ?? []).filter((item) => item.type !== "compaction"),
|
||||
)
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running")
|
||||
const current = draft[position]
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useToast } from "../ui/toast"
|
||||
import { useRoute } from "./route"
|
||||
import { useData } from "./data"
|
||||
import { usePermission } from "./permission"
|
||||
import { useLocation } from "./location"
|
||||
|
||||
export function parseModel(model: string) {
|
||||
const [providerID, ...rest] = model.split("/")
|
||||
@@ -56,11 +57,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const args = useArgs()
|
||||
const event = useEvent()
|
||||
const permission = usePermission()
|
||||
const location = useLocation()
|
||||
|
||||
const models = () => data.location.model.list(location.ref)
|
||||
|
||||
function isModelValid(model: ModelPreferenceModel) {
|
||||
return !!data.location.model
|
||||
.list()
|
||||
?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
|
||||
return !!models()?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
|
||||
}
|
||||
|
||||
function getFirstValidModel(...modelFns: (() => ModelPreferenceModel | undefined)[]) {
|
||||
@@ -73,9 +75,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
function createAgent() {
|
||||
const agents = createMemo(() =>
|
||||
(data.location.agent.list() ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
|
||||
(data.location.agent.list(location.ref) ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
|
||||
)
|
||||
const visibleAgents = createMemo(() =>
|
||||
(data.location.agent.list(location.ref) ?? []).filter((agent) => !agent.hidden),
|
||||
)
|
||||
const visibleAgents = createMemo(() => (data.location.agent.list() ?? []).filter((agent) => !agent.hidden))
|
||||
const [agentStore, setAgentStore] = createStore({
|
||||
current: undefined as string | undefined,
|
||||
})
|
||||
@@ -190,7 +194,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
const model = data.location.model.list()?.[0]
|
||||
const model = models()?.[0]
|
||||
if (!model) return undefined
|
||||
return {
|
||||
providerID: model.providerID,
|
||||
@@ -209,6 +213,33 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
|
||||
let syncedSessionModel: string | undefined
|
||||
createEffect(() => {
|
||||
if (route.data.type !== "session") {
|
||||
syncedSessionModel = undefined
|
||||
return
|
||||
}
|
||||
const session = data.session.get(route.data.sessionID)
|
||||
const selected = session?.model
|
||||
const a = agent.current()
|
||||
if (!selected || !a) return
|
||||
if (args.model) {
|
||||
const requested = parseModel(args.model)
|
||||
if (isModelValid(requested)) return
|
||||
}
|
||||
const model = { providerID: selected.providerID, modelID: selected.id }
|
||||
if (!isModelValid(model)) return
|
||||
const fingerprint = [session.id, a.id, selected.providerID, selected.id, selected.variant ?? "default"].join(
|
||||
":",
|
||||
)
|
||||
if (fingerprint === syncedSessionModel) return
|
||||
syncedSessionModel = fingerprint
|
||||
batch(() => {
|
||||
setModelStore("model", a.id, model)
|
||||
setModelStore("variant", modelPreferenceKey(model), normalizeModelVariant(selected.variant))
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
current: currentModel,
|
||||
get ready() {
|
||||
@@ -229,10 +260,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
reasoning: false,
|
||||
}
|
||||
}
|
||||
const provider = data.location.provider.list()?.find((item) => item.id === value.providerID)
|
||||
const info = data.location.model
|
||||
.list()
|
||||
?.find((item) => item.providerID === value.providerID && item.id === value.modelID)
|
||||
const provider = data.location.provider.list(location.ref)?.find((item) => item.id === value.providerID)
|
||||
const info = models()?.find((item) => item.providerID === value.providerID && item.id === value.modelID)
|
||||
return {
|
||||
provider: provider?.name ?? value.providerID,
|
||||
model: info?.name ?? value.modelID,
|
||||
@@ -340,9 +369,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
list() {
|
||||
const m = currentModel()
|
||||
if (!m) return []
|
||||
const info = data.location.model
|
||||
.list()
|
||||
?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
|
||||
const info = models()?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
|
||||
return info?.variants?.map((variant) => variant.id) ?? []
|
||||
},
|
||||
set(value: string | undefined) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useClient } from "./client"
|
||||
import { useData } from "./data"
|
||||
|
||||
const context = createContext<{
|
||||
readonly ref: LocationRef | undefined
|
||||
readonly current: LocationGetOutput | undefined
|
||||
set: (location?: LocationRef) => void
|
||||
}>()
|
||||
@@ -34,6 +35,9 @@ export function LocationProvider(props: ParentProps) {
|
||||
return (
|
||||
<context.Provider
|
||||
value={{
|
||||
get ref() {
|
||||
return ref()
|
||||
},
|
||||
get current() {
|
||||
return current()
|
||||
},
|
||||
|
||||
@@ -1104,6 +1104,7 @@ function TurnTokenUsage(props: {
|
||||
}) {
|
||||
const config = useConfig()
|
||||
const { themeV2 } = useTheme()
|
||||
const verbose = () => config.data.debug?.turn_tokens === "verbose"
|
||||
const steps = createMemo(() => {
|
||||
let previousCache = props.previousCache
|
||||
return props.messageIDs.flatMap((messageID) => {
|
||||
@@ -1123,6 +1124,7 @@ function TurnTokenUsage(props: {
|
||||
return [
|
||||
{
|
||||
finish: message.finish === "tool-calls" ? "tool-call" : (message.finish ?? "unknown"),
|
||||
tools: verbose() ? message.content.filter((part) => part.type === "tool") : [],
|
||||
newTokens,
|
||||
cached: message.tokens.cache.read,
|
||||
total,
|
||||
@@ -1138,7 +1140,7 @@ function TurnTokenUsage(props: {
|
||||
total: Math.max("Total".length, ...steps().map((item) => item.total.toLocaleString().length)),
|
||||
}))
|
||||
return (
|
||||
<Show when={config.data.debug?.turn_tokens === true && steps().length > 0}>
|
||||
<Show when={Boolean(config.data.debug?.turn_tokens) && steps().length > 0}>
|
||||
<box paddingLeft={3} flexDirection="column">
|
||||
<box flexDirection="row">
|
||||
<text width={INLINE_TOOL_ICON_WIDTH} fg={themeV2.text.subdued}>
|
||||
@@ -1161,7 +1163,7 @@ function TurnTokenUsage(props: {
|
||||
<For each={steps()}>
|
||||
{(item) => (
|
||||
<box paddingLeft={INLINE_TOOL_ICON_WIDTH} flexDirection="column">
|
||||
<text fg={themeV2.text.subdued}>
|
||||
<text fg={verbose() && item.finish === "tool-call" ? undefined : themeV2.text.subdued}>
|
||||
{item.finish.padEnd(columns().step + 2)}
|
||||
<span style={{ attributes: TextAttributes.BOLD }}>
|
||||
{item.newTokens.toLocaleString().padStart(columns().newTokens)}
|
||||
@@ -1171,6 +1173,7 @@ function TurnTokenUsage(props: {
|
||||
{" "}
|
||||
{item.total.toLocaleString().padStart(columns().total)}
|
||||
</text>
|
||||
<TurnTokenToolCalls tools={item.tools} />
|
||||
<Show when={item.reuseDrop !== undefined}>
|
||||
<text fg={themeV2.text.feedback.warning.default}>
|
||||
! Likely cache bust: {item.reuseDrop?.toLocaleString()} fewer cached tokens than the previous step
|
||||
@@ -1184,6 +1187,54 @@ function TurnTokenUsage(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function TurnTokenToolCalls(props: { tools: SessionMessageAssistantTool[] }) {
|
||||
const { themeV2 } = useTheme()
|
||||
const nameWidth = () => Math.max(0, ...props.tools.map((tool) => tool.name.length)) + 2
|
||||
return (
|
||||
<Show when={props.tools.length > 0}>
|
||||
<box paddingLeft={2} flexDirection="column">
|
||||
<For each={props.tools}>
|
||||
{(tool) => (
|
||||
<box flexDirection="row">
|
||||
<text
|
||||
width={nameWidth()}
|
||||
flexShrink={0}
|
||||
fg={themeV2.text.subdued}
|
||||
attributes={TextAttributes.BOLD}
|
||||
>
|
||||
{tool.name}
|
||||
</text>
|
||||
<text
|
||||
fg={themeV2.text.subdued}
|
||||
attributes={TextAttributes.DIM}
|
||||
wrapMode="word"
|
||||
flexGrow={1}
|
||||
minWidth={0}
|
||||
>
|
||||
{turnTokenToolSummary(tool)}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function turnTokenToolSummary(tool: SessionMessageAssistantTool) {
|
||||
const data = tool.state.input
|
||||
if (typeof data === "string") return data
|
||||
const primaryKey = ["command", "id", "pattern", "url", "query", "path", "description", "code"].find(
|
||||
(key) => key in data,
|
||||
)
|
||||
const input = Object.entries(data).filter(([, value]) =>
|
||||
["string", "number", "boolean"].includes(typeof value),
|
||||
)
|
||||
const primary = input.find(([key]) => key === primaryKey)?.[1]
|
||||
const details = input.filter(([key]) => key !== primaryKey).map(([key, value]) => `${key}: ${String(value)}`)
|
||||
return [primary === undefined ? "" : String(primary), ...details].filter(Boolean).join(" ")
|
||||
}
|
||||
|
||||
function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
|
||||
const { themeV2 } = useTheme()
|
||||
const shortcut = Keymap.useShortcut("session.background")
|
||||
|
||||
@@ -41,7 +41,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||
const config = useConfig()
|
||||
const [rows, setRows] = createStore<SessionRow[]>([])
|
||||
const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID
|
||||
const turnTokens = () => config.data.debug?.turn_tokens === true
|
||||
const turnTokens = () => Boolean(config.data.debug?.turn_tokens)
|
||||
|
||||
function reduce() {
|
||||
const messages = data.session.message.list(sessionID())
|
||||
|
||||
@@ -138,46 +138,87 @@ test("session lifecycle updates the terminal title and prints the epilogue after
|
||||
}
|
||||
})
|
||||
|
||||
test("session startup prompt is submitted exactly once", async () => {
|
||||
test("session startup waits for catalogs and follows durable model changes", async () => {
|
||||
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||
const core = await import("@opentui/core")
|
||||
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
|
||||
const events = createEventStream()
|
||||
const cwd = process.cwd()
|
||||
const location = { directory: cwd, project: { id: "project", directory: cwd } }
|
||||
const sessionDirectory = `${cwd}/session`
|
||||
const location = (directory: string) => ({ directory, project: { id: "project", directory } })
|
||||
const session = {
|
||||
id: "dummy",
|
||||
title: "Demo session",
|
||||
projectID: "project",
|
||||
location: { directory: cwd },
|
||||
location: { directory: sessionDirectory },
|
||||
agent: "build",
|
||||
model: { providerID: "provider", id: "model" },
|
||||
model: { providerID: "provider", id: "model", variant: "high" },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
}
|
||||
const bodies: unknown[] = []
|
||||
const modelSwitches: unknown[] = []
|
||||
const locationRequested = Promise.withResolvers<void>()
|
||||
const targetLocation = Promise.withResolvers<void>()
|
||||
const catalogRequested = Promise.withResolvers<void>()
|
||||
const catalog = Promise.withResolvers<void>()
|
||||
const promptSubmitted = Promise.withResolvers<void>()
|
||||
const secondPromptSubmitted = Promise.withResolvers<void>()
|
||||
const calls = createFetch(async (url, request) => {
|
||||
if (url.pathname === "/api/location") return json(location)
|
||||
const directory = url.searchParams.get("location[directory]") ?? cwd
|
||||
if (url.pathname === "/api/location") {
|
||||
if (directory === sessionDirectory) {
|
||||
locationRequested.resolve()
|
||||
await targetLocation.promise
|
||||
}
|
||||
return json(location(directory))
|
||||
}
|
||||
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: [], cursor: {} })
|
||||
if (url.pathname === "/api/session/dummy/message/msg_model_gamma")
|
||||
return json({
|
||||
data: {
|
||||
id: "msg_model_gamma",
|
||||
type: "model-switched",
|
||||
previous: session.model,
|
||||
model: { providerID: "provider", id: "gamma", variant: "medium" },
|
||||
time: { created: 1 },
|
||||
},
|
||||
})
|
||||
if (url.pathname === "/api/session/dummy/pending") return json({ data: [] })
|
||||
if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
|
||||
if (url.pathname === "/api/agent")
|
||||
return json({
|
||||
location,
|
||||
location: location(directory),
|
||||
data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }],
|
||||
})
|
||||
if (url.pathname === "/api/model")
|
||||
if (url.pathname === "/api/model") {
|
||||
if (directory !== sessionDirectory)
|
||||
return json({
|
||||
location: location(directory),
|
||||
data: [{ id: "fallback", providerID: "provider", name: "Fallback", variants: [] }],
|
||||
})
|
||||
catalogRequested.resolve()
|
||||
await catalog.promise
|
||||
return json({
|
||||
location,
|
||||
data: [{ id: "model", providerID: "provider", name: "Model", variants: [] }],
|
||||
location: location(directory),
|
||||
data: [
|
||||
{ id: "model", providerID: "provider", name: "Model", variants: [{ id: "high" }] },
|
||||
{ id: "fallback", providerID: "provider", name: "Fallback", variants: [] },
|
||||
{ id: "gamma", providerID: "provider", name: "Gamma", variants: [{ id: "medium" }] },
|
||||
],
|
||||
})
|
||||
}
|
||||
if (url.pathname === "/api/session/dummy/model") {
|
||||
modelSwitches.push(await request.json())
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
if (url.pathname === "/api/session/dummy/prompt") {
|
||||
bodies.push(await request.json())
|
||||
promptSubmitted.resolve()
|
||||
if (bodies.length === 2) secondPromptSubmitted.resolve()
|
||||
return json({ data: {} })
|
||||
}
|
||||
}, events)
|
||||
@@ -196,6 +237,22 @@ test("session startup prompt is submitted exactly once", async () => {
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),
|
||||
)
|
||||
|
||||
await locationRequested.promise
|
||||
await Promise.race([
|
||||
promptSubmitted.promise.then(() => {
|
||||
throw new Error("startup prompt was submitted before its location loaded")
|
||||
}),
|
||||
Bun.sleep(100),
|
||||
])
|
||||
targetLocation.resolve()
|
||||
await catalogRequested.promise
|
||||
await Promise.race([
|
||||
promptSubmitted.promise.then(() => {
|
||||
throw new Error("startup prompt was submitted before its model catalog loaded")
|
||||
}),
|
||||
Bun.sleep(100),
|
||||
])
|
||||
catalog.resolve()
|
||||
await Promise.race([
|
||||
promptSubmitted.promise,
|
||||
Bun.sleep(2000).then(() => {
|
||||
@@ -203,12 +260,38 @@ test("session startup prompt is submitted exactly once", async () => {
|
||||
}),
|
||||
])
|
||||
await Bun.sleep(20)
|
||||
setup.renderer.destroy()
|
||||
await task
|
||||
|
||||
expect(bodies).toHaveLength(1)
|
||||
expect(bodies[0]).toMatchObject({ text: "RESUME_READY" })
|
||||
expect(modelSwitches).toEqual([])
|
||||
|
||||
events.emit({
|
||||
id: "evt_model_gamma",
|
||||
created: 1,
|
||||
type: "session.model.selected",
|
||||
durable: { aggregateID: session.id, seq: 1, version: 1 },
|
||||
location: session.location,
|
||||
data: {
|
||||
sessionID: session.id,
|
||||
model: { providerID: "provider", id: "gamma", variant: "medium" },
|
||||
},
|
||||
})
|
||||
await Bun.sleep(20)
|
||||
await setup.mockInput.typeText("SECOND")
|
||||
setup.mockInput.pressEnter()
|
||||
await Promise.race([
|
||||
secondPromptSubmitted.promise,
|
||||
Bun.sleep(2000).then(() => {
|
||||
throw new Error("second prompt was not submitted")
|
||||
}),
|
||||
])
|
||||
expect(modelSwitches).toEqual([])
|
||||
await Bun.sleep(20)
|
||||
setup.renderer.destroy()
|
||||
await task
|
||||
} finally {
|
||||
targetLocation.resolve()
|
||||
catalog.resolve()
|
||||
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
|
||||
await server.stop()
|
||||
mock.restore()
|
||||
|
||||
Reference in New Issue
Block a user