Compare commits

..
3 Commits
19 changed files with 69 additions and 178 deletions
+1
View File
@@ -63,6 +63,7 @@ const layer = Layer.effect(
if (rule?.resource === "*" && rule.effect === "deny") continue
registrations.set(name, registration)
}
if (registrations.size === 0) return {}
const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action))
if (executeRule?.resource === "*" && executeRule.effect === "deny") return {}
return {
+13 -17
View File
@@ -6,9 +6,7 @@ import { Instructions } from "../instructions/index"
import { CodeModeCatalog } from "./catalog"
// prettier-ignore
const prompt = (hasMoreTools: boolean) => `The Code Mode tool catalog below is ${hasMoreTools ? "partial" : "complete"}.
Inside Code Mode, \`tools\` contains only the tools shown below${hasMoreTools ? " or returned by `search`" : ""}; surrounding top-level agent tools are not available and must not be called from the code.${hasMoreTools ? `
const prompt = (hasMoreTools: boolean) => `The Code Mode tool catalog below is ${hasMoreTools ? "partial" : "complete"}.${hasMoreTools ? `
## Search
@@ -19,8 +17,7 @@ Use \`search\` to discover exact paths and signatures for additional tools:
## Available tools`
export function render(catalog: CodeModeCatalog.Summary) {
if (catalog.total === 0)
return "No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools."
if (catalog.total === 0) return "No tools are currently available."
const tools = catalog.namespaces.flatMap((namespace) => {
const count = namespace.count === 1 ? "1 tool" : `${namespace.count} tools`
@@ -39,13 +36,12 @@ ${tools.join("\n")}`
}
export function update(previous: CodeModeCatalog.Summary, current: CodeModeCatalog.Summary) {
const replacement = `The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.
const full = `The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.
${render(current)}`
if (current.total === 0) return replacement
const previousComplete = previous.shown === previous.total
const currentComplete = current.shown === current.total
if (previousComplete !== currentComplete) return replacement
if (previousComplete !== currentComplete) return full
const diff = Instructions.diffByKey(
previous.namespaces.flatMap((namespace) => namespace.entries),
@@ -56,7 +52,7 @@ ${render(current)}`
const entriesChanged = diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0
if (!currentComplete) {
if (entriesChanged) return replacement
if (entriesChanged) return full
const namespaces = Instructions.diffByKey(
previous.namespaces,
current.namespaces,
@@ -64,7 +60,7 @@ ${render(current)}`
(before, after) => before.count !== after.count,
)
const changed = namespaces.added.length > 0 || namespaces.removed.length > 0 || namespaces.changed.length > 0
if (!changed) return replacement
if (!changed) return full
const parts = ["The Code Mode tool catalog has changed."]
if (namespaces.added.length > 0) {
@@ -89,11 +85,11 @@ ${render(current)}`
)
}
const delta = parts.join("\n\n")
if (delta.length < replacement.length) return delta
return replacement
if (delta.length < full.length) return delta
return full
}
if (!entriesChanged) return replacement
if (!entriesChanged) return full
const parts = ["The Code Mode tool catalog has changed."]
if (diff.added.length > 0) {
parts.push(
@@ -119,19 +115,19 @@ ${render(current)}`
)
}
const delta = parts.join("\n\n")
if (delta.length < replacement.length) return delta
return replacement
if (delta.length < full.length) return delta
return full
}
const key = Instructions.Key.make("core/codemode")
const codec = Schema.toCodecJson(CodeModeCatalog.Summary)
export const make = (entries?: ReadonlyArray<CodeModeCatalog.Entry>): Instructions.Instructions => {
const catalog = entries === undefined ? Instructions.removed : CodeModeCatalog.summarize(entries)
const catalog = CodeModeCatalog.summarize(entries ?? [])
return Instructions.make({
key,
codec,
read: Effect.succeed(catalog),
read: Effect.succeed(catalog.total === 0 ? Instructions.removed : catalog),
render: {
initial: render,
changed: update,
+2 -1
View File
@@ -36,7 +36,8 @@ type CollectedFiles = {
const description = [
"Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.",
"Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.",
'Call Code Mode tools through `tools` using only exact paths and signatures from the current catalog or `search`. Do not infer or normalize tool names; preserve bracket notation such as `tools.<namespace>["tool-name"](input)`.',
"Within `{ code }`, the only callable tools are those explicitly listed in the Code Mode catalog instructions or returned by `search`. Inside `{ code }`, ignore tools shown outside the Code Mode catalog. They are not available in the Code Mode runtime.",
'Call tools through `tools` using only exact paths and signatures from the catalog. Do not infer or normalize tool names; preserve bracket notation such as `tools.<namespace>["tool-name"](input)`.',
"Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.",
"Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.",
].join("\n")
+5 -10
View File
@@ -67,9 +67,7 @@ describe("CodeModeInstructions.render", () => {
expect(instructions).toContain(` - ${lookup.signature} // Look up an order by ID`)
expect(instructions).not.toContain("## Search")
expect(instructions).toContain("The Code Mode tool catalog below is complete.")
expect(instructions).toContain(
"`tools` contains only the tools shown below; surrounding top-level agent tools are not available and must not be called from the code.",
)
expect(instructions).not.toContain("surrounding top-level agent tools")
})
test("adds search guidance when the catalog exceeds the budget", () => {
@@ -78,9 +76,7 @@ describe("CodeModeInstructions.render", () => {
expect(partial).toContain("- orders (1 tool, none shown)")
expect(partial).toContain("## Search")
expect(partial).toContain("The Code Mode tool catalog below is partial.")
expect(partial).toContain(
"`tools` contains only the tools shown below or returned by `search`; surrounding top-level agent tools are not available and must not be called from the code.",
)
expect(partial).not.toContain("surrounding top-level agent tools")
expect(partial).toContain("- search(input: {")
expect(partial).toContain(" limit?: number,\n offset?: number,")
expect(partial).not.toContain("tools.orders.lookup(input:")
@@ -117,9 +113,7 @@ describe("CodeModeInstructions.render", () => {
})
test("renders only the no-tools notice for an empty catalog", () => {
expect(render([])).toBe(
"No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.",
)
expect(render([])).toBe("No tools are currently available.")
})
})
@@ -129,7 +123,8 @@ describe("CodeModeInstructions.update", () => {
test("renders additions, changes, and removals as a compact semantic delta", () => {
const changed = { ...echo, signature: "tools.notes.echo(input: {\n text: string,\n}): Promise<string>" }
const added = entry("notes.list", "List notes")
const text = update([echo, lookup], [changed, added])
const unchanged = Array.from({ length: 5 }, (_, index) => entry(`stable.tool${index}`, `Stable ${index}`))
const text = update([echo, lookup, ...unchanged], [changed, added, ...unchanged])
expect(text).toContain("The Code Mode tool catalog has changed.")
expect(text).toContain(`New tools are available in addition to those previously listed:\n - ${added.signature}`)
expect(text).toContain(
@@ -21,25 +21,6 @@ const lookup: CodeModeCatalog.Entry = {
}
describe("CodeModeInstructions", () => {
it.effect("instructs the model not to call execute while the catalog is empty", () =>
Effect.gen(function* () {
const initialized = yield* readInitial(CodeModeInstructions.make([]))
expect(initialized.text).toBe(
"No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.",
)
const added = yield* readUpdate(CodeModeInstructions.make([echo]), initialized)
expect(added.text).toContain("New tools are available in addition to those previously listed:")
expect(added.text).toContain(echo.signature)
expect(yield* readUpdate(CodeModeInstructions.make([]), { values: added.values })).toMatchObject({
text:
"The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.\n\n" +
"No Code Mode tools are currently available. Do not call `execute` until a later system update announces available tools.",
})
}),
)
it.effect("renders the initial catalog, semantic deltas, and removal", () =>
Effect.gen(function* () {
const initialized = yield* readInitial(CodeModeInstructions.make([echo]))
-16
View File
@@ -32,22 +32,6 @@ export function waitForTool(
})
}
export function waitForCodeModeTool(
registry: ToolRegistry.Interface,
path: string,
remaining = 1000,
): Effect.Effect<ToolRegistry.ToolSet, Error> {
return Effect.gen(function* () {
const toolSet = yield* registry.snapshot()
if (toolSet.codeModeCatalog?.some((tool) => tool.path === path)) return toolSet
if (remaining === 0) {
return yield* Effect.fail(new Error(`Timed out waiting for Code Mode tool: ${path}`))
}
yield* Effect.promise(() => Bun.sleep(1))
return yield* waitForCodeModeTool(registry, path, remaining - 1)
})
}
/**
* Registers a core tool plugin's tools against the real registry without booting the
* full plugin host. Only the tool domain is live; focused tool tests exercise
+8 -14
View File
@@ -33,7 +33,7 @@ import { Image } from "@opencode-ai/core/image"
import { testEffect } from "./lib/effect"
import { imagePassthrough } from "./lib/image"
import { location } from "./fixture/location"
import { executeTool, toolDefinitions, toolIdentity, waitForCodeModeTool, waitForTool } from "./lib/tool"
import { executeTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool"
let assertion: Deferred.Deferred<PermissionV2.AssertInput> | undefined
let decision: Effect.Effect<void, PermissionV2.Error> = Effect.void
@@ -802,16 +802,10 @@ test("serializes concurrent MCP lifecycle operations", async () => {
it.effect("advertises MCP output schemas to Code Mode", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
const toolSet = yield* waitForCodeModeTool(registry, "demo.search")
const execute = toolSet.definitions.find((tool) => tool.name === "execute")
yield* waitForTool(registry, "execute")
const definitions = yield* toolDefinitions(registry)
const execute = definitions.find((tool) => tool.name === "execute")
expect(toolSet.definitions.map((tool) => tool.name)).toEqual([
"direct_fail",
"direct_lookup",
"direct_media",
"execute",
])
expect(toolSet.codeModeCatalog?.find((tool) => tool.path === "demo.search")?.signature).toContain("ok: boolean")
expect(execute?.description).not.toContain("tools.demo.search")
}),
)
@@ -879,9 +873,9 @@ it.effect("waits for permission before calling an MCP tool", () =>
const permission = yield* Deferred.make<void>()
decision = Deferred.await(permission)
const registry = yield* ToolRegistry.Service
const toolSet = yield* waitForCodeModeTool(registry, "demo.search")
yield* waitForTool(registry, "execute")
const fiber = yield* toolSet.execute({
const fiber = yield* executeTool(registry, {
sessionID: SessionV2.ID.make("ses_mcp_permission"),
...toolIdentity,
call: {
@@ -918,9 +912,9 @@ it.effect("does not call MCP when permission is blocked", () =>
assertion = yield* Deferred.make<PermissionV2.AssertInput>()
decision = Effect.fail(new PermissionV2.BlockedError({ rules: [], permission: "demo_search", resources: ["*"] }))
const registry = yield* ToolRegistry.Service
const toolSet = yield* waitForCodeModeTool(registry, "demo.search")
yield* waitForTool(registry, "execute")
const execution = yield* toolSet.execute({
const execution = yield* executeTool(registry, {
sessionID: SessionV2.ID.make("ses_mcp_blocked"),
...toolIdentity,
call: {
@@ -186,7 +186,6 @@ describe("SessionRunnerLLM recorded", () => {
yield* agents.transform((draft) =>
draft.update(AgentV2.ID.make("build"), (agent) => {
agent.mode = "primary"
agent.permissions.push({ action: "execute", resource: "*", effect: "deny" })
}),
)
const pluginHost = host({
@@ -88,7 +88,7 @@ describe("ToolRegistry", () => {
expect(error).toBeInstanceOf(Tool.RegistrationError)
expect(error.message).toBe('Invalid tool namespace: "slack..admin"')
expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"])
expect((yield* service.snapshot()).definitions).toEqual([])
}),
)
@@ -102,7 +102,7 @@ describe("ToolRegistry", () => {
.register({ "echo.tool": make(), echo_tool: make() }, { codemode: false })
.pipe(Effect.flip)
expect(collision.message).toBe("Duplicate normalized tool name: echo_tool")
expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"])
expect((yield* service.snapshot()).definitions).toEqual([])
}),
)
@@ -117,7 +117,7 @@ describe("ToolRegistry", () => {
.pipe(Effect.flip)
expect(error).toBeInstanceOf(Tool.RegistrationError)
expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"])
expect((yield* service.snapshot()).definitions).toEqual([])
}),
)
@@ -148,20 +148,6 @@ describe("ToolRegistry", () => {
}),
)
it.effect("keeps execute available without Code Mode tools unless explicitly denied", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
const available = yield* service.snapshot()
expect(available.definitions.map((tool) => tool.name)).toEqual(["execute"])
expect(available.codeModeCatalog).toEqual([])
const denied = yield* service.snapshot([{ action: "execute", resource: "*", effect: "deny" }])
expect(denied.definitions).toEqual([])
expect(denied.codeModeCatalog).toBeUndefined()
}),
)
it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
@@ -170,12 +156,7 @@ describe("ToolRegistry", () => {
const names = (permissions: PermissionV2.Ruleset) =>
toolDefinitions(service, permissions).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name)))
expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual([
"bash",
"edit",
"write",
"execute",
])
expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual(["bash", "edit", "write"])
expect(
yield* names([
{ action: "*", resource: "*", effect: "deny" },
@@ -188,11 +169,7 @@ describe("ToolRegistry", () => {
{ action: "*", resource: "*", effect: "deny" },
]),
).toEqual([])
expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual([
"bash",
"question",
"execute",
])
expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["bash", "question"])
}),
)
@@ -205,7 +182,7 @@ describe("ToolRegistry", () => {
expect(
(yield* toolDefinitions(service, [{ action: "edit", resource: "*", effect: "deny" }])).map((tool) => tool.name),
).toEqual(["first", "execute"])
).toEqual(["first"])
}),
)
@@ -214,9 +191,9 @@ describe("ToolRegistry", () => {
const service = yield* ToolRegistry.Service
const scope = yield* Scope.make()
yield* service.register({ echo: make() }, { codemode: false }).pipe(Scope.provide(scope))
expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo", "execute"])
expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo"])
yield* Scope.close(scope, Exit.void)
expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["execute"])
expect(yield* toolDefinitions(service)).toEqual([])
}),
)
@@ -236,9 +213,9 @@ describe("ToolRegistry", () => {
yield* Deferred.await(registered)
yield* Fiber.interrupt(fiber)
expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo", "execute"])
expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo"])
yield* Scope.close(scope, Exit.void)
expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["execute"])
expect(yield* toolDefinitions(service)).toEqual([])
}),
)
-40
View File
@@ -880,46 +880,6 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("advertises execute and durable guidance for an empty Code Mode catalog", () =>
Effect.gen(function* () {
const session = yield* setup
const empty = {
catalog: [],
tool: Tool.make({
description: "Execute Code Mode",
input: Schema.Struct({ code: Schema.String }),
output: Schema.String,
execute: () => Effect.succeed({ output: "unused" }),
}),
}
codeModeMaterializations = [empty, empty, {}]
yield* admit(session, "Continue without Code Mode tools")
response = reply.stop()
yield* session.resume(sessionID)
yield* admit(session, "Still no Code Mode tools")
yield* session.resume(sessionID)
yield* admit(session, "Code Mode denied")
yield* session.resume(sessionID)
expect(requests).toHaveLength(3)
expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"])
expect(requests[0]?.system.some((part) => part.text.includes("Do not call `execute`"))).toBe(true)
expect(requests[1]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"])
expect(requests[1]?.messages.filter((message) => message.role === "system")).toEqual([])
expect(requests[2]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"])
expect(
requests[2]?.messages.some(
(message) =>
message.role === "system" &&
message.content.some(
(part) => part.type === "text" && part.text.includes("Code Mode tools are no longer available"),
),
),
).toBe(true)
}),
)
it.effect("applies session context hooks without exposing unavailable tools", () =>
Effect.gen(function* () {
const session = yield* setup
+4 -6
View File
@@ -137,12 +137,10 @@ describe("EditTool", () => {
Effect.andThen(
withTool(tmp.path, (registry) =>
Effect.gen(function* () {
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["edit", "execute"])
expect(
(yield* toolDefinitions(registry, [{ action: "edit", resource: "*", effect: "deny" }])).map(
(tool) => tool.name,
),
).toEqual(["execute"])
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["edit"])
expect(yield* toolDefinitions(registry, [{ action: "edit", resource: "*", effect: "deny" }])).toEqual(
[],
)
const settled = yield* executeTool(
registry,
call({ path: "hello.txt", oldString: "before", newString: "after" }),
+2 -1
View File
@@ -19,7 +19,8 @@ test("execute describes invariant Code Mode behavior", () => {
[
"Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.",
"Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.",
'Call Code Mode tools through `tools` using only exact paths and signatures from the current catalog or `search`. Do not infer or normalize tool names; preserve bracket notation such as `tools.<namespace>["tool-name"](input)`.',
"Within `{ code }`, the only callable tools are those explicitly listed in the Code Mode catalog instructions or returned by `search`. Inside `{ code }`, ignore tools shown outside the Code Mode catalog. They are not available in the Code Mode runtime.",
'Call tools through `tools` using only exact paths and signatures from the catalog. Do not infer or normalize tool names; preserve bracket notation such as `tools.<namespace>["tool-name"](input)`.',
"Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.",
"Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.",
].join("\n"),
+1 -1
View File
@@ -181,7 +181,7 @@ describe("PatchTool", () => {
Effect.andThen(
withTool(tmp.path, (registry) =>
Effect.gen(function* () {
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["patch", "execute"])
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["patch"])
const settled = yield* executeTool(
registry,
call(
+2 -6
View File
@@ -97,11 +97,7 @@ describe("QuestionTool", () => {
deny = true
const registry = yield* ToolRegistry.Service
expect(
(yield* toolDefinitions(registry, [{ action: "question", resource: "*", effect: "deny" }])).map(
(tool) => tool.name,
),
).toEqual(["execute"])
expect(yield* toolDefinitions(registry, [{ action: "question", resource: "*", effect: "deny" }])).toEqual([])
expect(
yield* executeTool(registry, {
sessionID,
@@ -146,7 +142,7 @@ describe("QuestionTool", () => {
},
]
expect((yield* toolDefinitions(registry)).map((definition) => definition.name)).toEqual(["question", "execute"])
expect((yield* toolDefinitions(registry)).map((definition) => definition.name)).toEqual(["question"])
expect(
yield* executeTool(registry, {
sessionID,
+2 -6
View File
@@ -197,12 +197,8 @@ describe("ReadTool", () => {
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["read", "execute"])
expect(
(yield* toolDefinitions(registry, [{ action: "read", resource: "*", effect: "deny" }])).map(
(tool) => tool.name,
),
).toEqual(["execute"])
expect(yield* toolDefinitions(registry)).toMatchObject([{ name: "read" }])
expect(yield* toolDefinitions(registry, [{ action: "read", resource: "*", effect: "deny" }])).toEqual([])
const execution = yield* executeTool(registry, {
sessionID,
...toolIdentity,
+1 -1
View File
@@ -92,7 +92,7 @@ describe("WebFetchTool registration", () => {
const registry = yield* ToolRegistry.Service
const url = "http://example.com/public"
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch", "execute"])
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch"])
expect(yield* executeTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({
status: "completed",
output: { url, contentType: "text/plain", format: "text", output: "hello" },
+1 -1
View File
@@ -154,7 +154,7 @@ describe("WebSearchTool registration", () => {
config = { provider: "exa", enableExa: false, enableParallel: false }
const registry = yield* ToolRegistry.Service
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["websearch", "execute"])
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["websearch"])
expect(
yield* executeTool(registry, {
sessionID,
+1 -1
View File
@@ -118,7 +118,7 @@ describe("WriteTool", () => {
reset()
return withTool(tmp.path, (registry) =>
Effect.gen(function* () {
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["write", "execute"])
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["write"])
const settled = yield* executeTool(registry, call({ path: "src/new.txt", content: "created" }))
expect(settled).toEqual({
status: "completed",
+16 -4
View File
@@ -1,8 +1,9 @@
import { RGBA } from "@opentui/core"
import { useTheme } from "../context/theme"
import { Spinner } from "./spinner"
export function Reconnecting() {
const { themeV2 } = useTheme()
const { themeV2 } = useTheme().contextual("elevated")
return (
<box
@@ -12,12 +13,23 @@ export function Reconnecting() {
right={0}
bottom={0}
left={0}
backgroundColor={themeV2.background.default}
backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
alignItems="center"
justifyContent="center"
>
<box width={62} maxWidth="90%" flexDirection="column" alignItems="center" gap={1}>
<Spinner color={themeV2.text.subdued}>Waiting for background service...</Spinner>
<box
width={48}
maxWidth="90%"
flexDirection="column"
backgroundColor={themeV2.background.default}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
gap={1}
>
<Spinner color={themeV2.text.default}>Restarting service...</Spinner>
<text fg={themeV2.text.subdued}>Your session will resume automatically.</text>
</box>
</box>
)