Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton c478ad52a9 fix(session): show interrupt progress immediately 2026-08-12 14:48:54 -04:00
10 changed files with 268 additions and 45 deletions
@@ -402,6 +402,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
}, },
submit: { submit: {
stopping, stopping,
pending: submission.stopping,
working, working,
onSubmit: () => void submission.handleSubmit(new Event("submit")), onSubmit: () => void submission.handleSubmit(new Event("submit")),
onStop: () => void submission.abort(), onStop: () => void submission.abort(),
+48 -34
View File
@@ -261,6 +261,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
() => prompt.capture(), () => prompt.capture(),
Math.floor(Math.random() * EXAMPLES.length), Math.floor(Math.random() * EXAMPLES.length),
) )
createEffect(() => {
if (!working()) setStore("stopping", false)
})
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
const motion = (value: number) => ({ const motion = (value: number) => ({
opacity: value, opacity: value,
@@ -283,9 +286,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
.join("") .join("")
return text.trim().length === 0 && imageAttachments().length === 0 && commentCount() === 0 return text.trim().length === 0 && imageAttachments().length === 0 && commentCount() === 0
}) })
const stopping = createMemo(() => working() && blank()) const stopAction = createMemo(() => working() && blank())
const tip = () => { const tip = () => {
if (stopping()) { if (store.stopping) return <span>{language.t("prompt.action.stop")}...</span>
if (stopAction()) {
return ( return (
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>{language.t("prompt.action.stop")}</span> <span>{language.t("prompt.action.stop")}</span>
@@ -1198,36 +1202,46 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return permission.isAutoAccepting(id, sdk().directory) return permission.isAutoAccepting(id, sdk().directory)
}) })
const { abort, handleSubmit } = const {
props.submission ?? abort: requestAbort,
createPromptSubmit({ handleSubmit,
prompt, stopping: requestStopping,
info, } = props.submission ??
imageAttachments, createPromptSubmit({
commentCount, prompt,
autoAccept: () => accepting(), info,
mode: () => store.mode, imageAttachments,
working, commentCount,
editor: () => editorRef, autoAccept: () => accepting(),
queueScroll, mode: () => store.mode,
promptLength, working,
addToHistory, editor: () => editorRef,
resetHistoryNavigation: () => { queueScroll,
resetHistoryNavigation(true) promptLength,
}, addToHistory,
setMode: (mode) => setStore("mode", mode), resetHistoryNavigation: () => {
setPopover: (popover) => { resetHistoryNavigation(true)
if (!popover) return closePopover() },
setStore({ popover, slashMenu: false, slashMenuQuery: "" }) setMode: (mode) => setStore("mode", mode),
}, setPopover: (popover) => {
newSessionWorktree: () => props.newSessionWorktree, if (!popover) return closePopover()
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, setStore({ popover, slashMenu: false, slashMenuQuery: "" })
shouldQueue: props.shouldQueue, },
onQueue: props.onQueue, newSessionWorktree: () => props.newSessionWorktree,
onAbort: props.onAbort, onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
onSubmit: props.onSubmit, shouldQueue: props.shouldQueue,
model: props.controls.model.selection, onQueue: props.onQueue,
onAbort: props.onAbort,
onSubmit: props.onSubmit,
model: props.controls.model.selection,
})
const abort = () => {
if (store.stopping || requestStopping?.()) return Promise.resolve()
setStore("stopping", true)
return Promise.resolve(requestAbort()).finally(() => {
if (working()) setStore("stopping", false)
}) })
}
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") { if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") {
@@ -1579,12 +1593,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<IconButton <IconButton
data-action="prompt-submit" data-action="prompt-submit"
type="submit" type="submit"
disabled={!working() && blank()} disabled={store.stopping || (!working() && blank())}
tabIndex={store.mode === "normal" ? undefined : -1} tabIndex={store.mode === "normal" ? undefined : -1}
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"} icon={stopAction() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
variant="primary" variant="primary"
class="size-8" class="size-8"
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")} aria-label={stopAction() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/> />
</Tooltip> </Tooltip>
</div> </div>
@@ -8,6 +8,7 @@ export type PromptInputState = ReturnType<typeof usePrompt>
export type PromptInputSubmission = { export type PromptInputSubmission = {
abort: () => Promise<void> | void abort: () => Promise<void> | void
handleSubmit: (event: Event) => Promise<void> | void handleSubmit: (event: Event) => Promise<void> | void
stopping?: () => boolean
} }
export type PromptInputControls = { export type PromptInputControls = {
@@ -46,6 +46,9 @@ let selected = "/repo/worktree-a"
let variant: string | undefined let variant: string | undefined
let permissionServer = "server-a" let permissionServer = "server-a"
let createSessionGate: Promise<void> | undefined let createSessionGate: Promise<void> | undefined
let interruptGate: Promise<void> | undefined
let interruptCalls = 0
let interruptFailure = false
let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
const [promptStore, setPromptStore] = createStore<PromptStore>({ const [promptStore, setPromptStore] = createStore<PromptStore>({
@@ -121,6 +124,11 @@ const clientFor = (directory: string) => {
shell: async (input: { sessionID: string; id?: string; command: string }) => { shell: async (input: { sessionID: string; id?: string; command: string }) => {
sentShell.push(input) sentShell.push(input)
}, },
interrupt: async () => {
interruptCalls++
await interruptGate
if (interruptFailure) throw new Error("interrupt failed")
},
}, },
}, },
session: { session: {
@@ -310,11 +318,74 @@ beforeEach(() => {
variant = undefined variant = undefined
permissionServer = "server-a" permissionServer = "server-a"
createSessionGate = undefined createSessionGate = undefined
interruptGate = undefined
interruptCalls = 0
interruptFailure = false
serverSessionSyncs = 0 serverSessionSyncs = 0
for (const key of Object.keys(storedSessions)) delete storedSessions[key] for (const key of Object.keys(storedSessions)) delete storedSessions[key]
}) })
describe("prompt submit worktree selection", () => { describe("prompt submit worktree selection", () => {
test("reports stopping immediately and suppresses duplicate interrupts", async () => {
params = { id: "session-1" }
let release = () => {}
interruptGate = new Promise<void>((resolve) => {
release = resolve
})
let working = true
const submit = createPromptSubmit({
prompt,
info: () => ({ id: "session-1" }),
imageAttachments: () => [],
commentCount: () => 0,
autoAccept: () => false,
mode: () => "normal",
working: () => working,
editor: () => undefined,
queueScroll: () => undefined,
promptLength: () => 0,
addToHistory: () => undefined,
resetHistoryNavigation: () => undefined,
setMode: () => undefined,
setPopover: () => undefined,
})
const first = submit.abort()
const second = submit.abort()
expect(submit.stopping()).toBe(true)
expect(interruptCalls).toBe(1)
release()
await Promise.all([first, second])
working = false
expect(submit.stopping()).toBe(false)
})
test("clears stopping when the interrupt request fails", async () => {
params = { id: "session-1" }
interruptFailure = true
const submit = createPromptSubmit({
prompt,
info: () => ({ id: "session-1" }),
imageAttachments: () => [],
commentCount: () => 0,
autoAccept: () => false,
mode: () => "normal",
working: () => true,
editor: () => undefined,
queueScroll: () => undefined,
promptLength: () => 0,
addToHistory: () => undefined,
resetHistoryNavigation: () => undefined,
setMode: () => undefined,
setPopover: () => undefined,
})
await submit.abort()
expect(submit.stopping()).toBe(false)
})
test("reads the latest worktree accessor value per submit", async () => { test("reads the latest worktree accessor value per submit", async () => {
const submit = createPromptSubmit({ const submit = createPromptSubmit({
prompt, prompt,
@@ -4,7 +4,7 @@ import { showToast } from "@/utils/toast"
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import { Binary } from "@opencode-ai/core/util/binary" import { Binary } from "@opencode-ai/core/util/binary"
import { useNavigate, useParams, useSearchParams } from "@solidjs/router" import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
import { batch, startTransition, type Accessor } from "solid-js" import { batch, createSignal, startTransition, type Accessor } from "solid-js"
import { useTabs } from "@/context/tabs" import { useTabs } from "@/context/tabs"
import { useServerSync, type ServerSync } from "@/context/server-sync" import { useServerSync, type ServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -263,6 +263,12 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const params = useParams() const params = useParams()
const [search] = useSearchParams<{ draftId?: string }>() const [search] = useSearchParams<{ draftId?: string }>()
const tabs = useTabs() const tabs = useTabs()
const [stopping, setStopping] = createSignal(false)
const isStopping = () => {
if (input.working()) return stopping()
setStopping(false)
return false
}
const pendingKey = (sessionID: string) => ScopedKey.from(sdk().scope, sessionID) const pendingKey = (sessionID: string) => ScopedKey.from(sdk().scope, sessionID)
const errorMessage = (err: unknown) => { const errorMessage = (err: unknown) => {
@@ -276,8 +282,10 @@ export function createPromptSubmit(input: PromptSubmitInput) {
} }
const abort = async () => { const abort = async () => {
if (isStopping()) return
const sessionID = params.id const sessionID = params.id
if (!sessionID) return Promise.resolve() if (!sessionID) return Promise.resolve()
setStopping(true)
serverSync().session.set("todo", sessionID, []) serverSync().session.set("todo", sessionID, [])
@@ -289,11 +297,12 @@ export function createPromptSubmit(input: PromptSubmitInput) {
queued.abort.abort() queued.abort.abort()
queued.cleanup() queued.cleanup()
pending.delete(key) pending.delete(key)
setStopping(false)
return Promise.resolve() return Promise.resolve()
} }
return sdk() return sdk()
.api.session.interrupt({ sessionID }) .api.session.interrupt({ sessionID })
.catch(() => {}) .catch(() => setStopping(false))
} }
const restoreCommentItems = ( const restoreCommentItems = (
@@ -649,5 +658,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
return { return {
abort, abort,
handleSubmit, handleSubmit,
stopping: isStopping,
} }
} }
@@ -12,6 +12,7 @@ export type PromptInputTransientState = {
draggingType: "image" | "@mention" | null draggingType: "image" | "@mention" | null
mode: "normal" | "shell" mode: "normal" | "shell"
applyingHistory: boolean applyingHistory: boolean
stopping: boolean
} }
function resetPromptInputTransientState(setStore: SetStoreFunction<PromptInputTransientState>) { function resetPromptInputTransientState(setStore: SetStoreFunction<PromptInputTransientState>) {
@@ -24,6 +25,7 @@ function resetPromptInputTransientState(setStore: SetStoreFunction<PromptInputTr
draggingType: null, draggingType: null,
mode: "normal", mode: "normal",
applyingHistory: false, applyingHistory: false,
stopping: false,
}) })
} }
@@ -38,6 +40,7 @@ export function createPromptInputTransientState(identity: Accessor<unknown>, pla
draggingType: null, draggingType: null,
mode: "normal", mode: "normal",
applyingHistory: false, applyingHistory: false,
stopping: false,
}) })
createComputed(on(identity, () => resetPromptInputTransientState(setStore), { defer: true })) createComputed(on(identity, () => resetPromptInputTransientState(setStore), { defer: true }))
+100 -3
View File
@@ -28,7 +28,7 @@ import { EventTable } from "@opencode-ai/core/event/sql"
import { Project } from "@opencode-ai/core/project" import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql" import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Form } from "@opencode-ai/core/form" import { Form } from "@opencode-ai/core/form"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Session } from "@opencode-ai/core/session" import { Session } from "@opencode-ai/core/session"
import { Snapshot } from "@opencode-ai/core/snapshot" import { Snapshot } from "@opencode-ai/core/snapshot"
import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionEvent } from "@opencode-ai/core/session/event"
@@ -355,6 +355,18 @@ const pluginSupervisor = Layer.succeed(
flush: Effect.suspend(() => pluginFlushHook), flush: Effect.suspend(() => pluginFlushHook),
}), }),
) )
let snapshotCaptureHook: () => Effect.Effect<Snapshot.ID | undefined> = () => Effect.succeed(undefined)
let snapshotFilesHook: (input: Snapshot.CompareInput) => Effect.Effect<readonly RelativePath[], Snapshot.Error> = () =>
Effect.succeed([])
const snapshots = Layer.succeed(
Snapshot.Service,
Snapshot.Service.of({
capture: () => snapshotCaptureHook(),
files: (input) => snapshotFilesHook(input),
diff: () => Effect.succeed([]),
restore: () => Effect.void,
}),
)
const promptCatalog = Layer.mock(Catalog.Service, { const promptCatalog = Layer.mock(Catalog.Service, {
provider: { provider: {
get: () => Effect.succeed(undefined), get: () => Effect.succeed(undefined),
@@ -370,7 +382,7 @@ const promptCatalog = Layer.mock(Catalog.Service, {
}, },
}) })
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
[Snapshot.node, Snapshot.noopLayer], [Snapshot.node, snapshots],
[LayerNodePlatform.llmClient, client], [LayerNodePlatform.llmClient, client],
[SessionRunnerModel.node, models], [SessionRunnerModel.node, models],
[InstructionBuiltIns.node, systemContext], [InstructionBuiltIns.node, systemContext],
@@ -437,7 +449,7 @@ const it = testEffect(
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
[SkillInstructions.node, skillInstructions], [SkillInstructions.node, skillInstructions],
[ReferenceInstructions.node, referenceInstructions], [ReferenceInstructions.node, referenceInstructions],
[Snapshot.node, Snapshot.noopLayer], [Snapshot.node, snapshots],
[SessionExecution.node, execution], [SessionExecution.node, execution],
[Config.node, config], [Config.node, config],
[PluginSupervisor.node, pluginSupervisor], [PluginSupervisor.node, pluginSupervisor],
@@ -493,6 +505,8 @@ const setup = Effect.gen(function* () {
systemLoadHook = Effect.void systemLoadHook = Effect.void
modelResolveHook = Effect.void modelResolveHook = Effect.void
pluginFlushHook = Effect.void pluginFlushHook = Effect.void
snapshotCaptureHook = () => Effect.succeed(undefined)
snapshotFilesHook = () => Effect.succeed([])
currentModel = model currentModel = model
skillBaselines.clear() skillBaselines.clear()
toolBarrier = undefined toolBarrier = undefined
@@ -3741,6 +3755,89 @@ describe("SessionRunnerLLM", () => {
}), }),
) )
it.effect("waits for the end snapshot before interrupted settlement", () =>
Effect.gen(function* () {
const session = yield* setup
const endCaptureStarted = yield* Deferred.make<void>()
const releaseEndCapture = yield* Deferred.make<void>()
const runSettled = yield* Deferred.make<void>()
const interruptSettled = yield* Deferred.make<void>()
let captures = 0
snapshotCaptureHook = () => {
captures++
if (captures === 1) return Effect.succeed(Snapshot.ID.make("snapshot-start"))
return Deferred.succeed(endCaptureStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseEndCapture)),
Effect.as(Snapshot.ID.make("snapshot-end")),
)
}
snapshotFilesHook = () => Effect.succeed([RelativePath.make("changed.txt")])
yield* admit(session, "Interrupt during end snapshot")
const stream = yield* TestLLM.gate
const run = yield* session
.resume(sessionID)
.pipe(Effect.ensuring(Deferred.succeed(runSettled, undefined)), Effect.forkChild)
yield* stream.started
const interrupt = yield* session
.interrupt(sessionID)
.pipe(Effect.ensuring(Deferred.succeed(interruptSettled, undefined)), Effect.forkChild)
yield* Deferred.await(endCaptureStarted)
expect(yield* Deferred.isDone(interruptSettled)).toBe(false)
expect(yield* Deferred.isDone(runSettled)).toBe(false)
yield* Deferred.succeed(releaseEndCapture, undefined)
yield* Fiber.join(interrupt)
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
expect(requireAssistant(yield* session.context(sessionID))).toMatchObject({
finish: "error",
error: { type: "aborted", message: "Step interrupted" },
snapshot: {
start: "snapshot-start",
end: "snapshot-end",
files: ["changed.txt"],
},
})
}),
)
it.effect("waits for unrelated database transactions before interrupted settlement", () =>
Effect.gen(function* () {
const session = yield* setup
const { db } = yield* Database.Service
const transactionStarted = yield* Deferred.make<void>()
const releaseTransaction = yield* Deferred.make<void>()
const interruptSettled = yield* Deferred.make<void>()
yield* admit(session, "Interrupt during database contention")
const stream = yield* TestLLM.gate
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* stream.started
const transaction = yield* db
.transaction(() =>
Deferred.succeed(transactionStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseTransaction))),
)
.pipe(Effect.forkChild)
yield* Deferred.await(transactionStarted)
const interrupt = yield* session
.interrupt(sessionID)
.pipe(Effect.ensuring(Deferred.succeed(interruptSettled, undefined)), Effect.forkChild)
yield* Effect.yieldNow
expect(yield* Deferred.isDone(interruptSettled)).toBe(false)
yield* Deferred.succeed(releaseTransaction, undefined)
yield* Fiber.join(transaction)
yield* Fiber.join(interrupt)
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
expect(requireAssistant(yield* session.context(sessionID))).toMatchObject({
finish: "error",
error: { type: "aborted", message: "Step interrupted" },
})
}),
)
it.effect("durably fails blocked local tools when interrupted while awaiting settlement", () => it.effect("durably fails blocked local tools when interrupted while awaiting settlement", () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* setup const session = yield* setup
@@ -257,6 +257,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
<PromptInputV2SubmitButton <PromptInputV2SubmitButton
mode={state.mode} mode={state.mode}
stopping={view.submit.stopping()} stopping={view.submit.stopping()}
pending={view.submit.pending?.() ?? false}
disabled={!props.controller.canSubmit()} disabled={!props.controller.canSubmit()}
sendLabel={i18n.t("ui.promptInput.send")} sendLabel={i18n.t("ui.promptInput.send")}
stopLabel={i18n.t("ui.promptInput.stop")} stopLabel={i18n.t("ui.promptInput.stop")}
@@ -672,6 +673,7 @@ export function PromptInputV2Popover(props: {
export function PromptInputV2SubmitButton(props: { export function PromptInputV2SubmitButton(props: {
mode: PromptInputV2Mode mode: PromptInputV2Mode
stopping: boolean stopping: boolean
pending: boolean
disabled: boolean disabled: boolean
sendLabel: string sendLabel: string
stopLabel: string stopLabel: string
@@ -682,12 +684,12 @@ export function PromptInputV2SubmitButton(props: {
<TooltipV2 <TooltipV2
placement="top" placement="top"
inactive={!props.stopping && props.disabled} inactive={!props.stopping && props.disabled}
value={props.stopping ? props.stopLabel : props.sendLabel} value={props.pending ? `${props.stopLabel}...` : props.stopping ? props.stopLabel : props.sendLabel}
> >
<IconButton <IconButton
data-action="prompt-submit" data-action="prompt-submit"
type="button" type="button"
disabled={!props.stopping && props.disabled} disabled={props.pending || (!props.stopping && props.disabled)}
tabIndex={props.mode === "normal" ? undefined : -1} tabIndex={props.mode === "normal" ? undefined : -1}
icon={props.stopping ? "stop" : props.mode === "shell" ? "arrow-undo-down" : "arrow-up"} icon={props.stopping ? "stop" : props.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
variant="primary" variant="primary"
@@ -36,6 +36,7 @@ export type PromptInputV2ViewConfig = {
variant?: PromptInputV2SelectControl variant?: PromptInputV2SelectControl
submit: { submit: {
stopping: Accessor<boolean> stopping: Accessor<boolean>
pending?: Accessor<boolean>
working?: Accessor<boolean> working?: Accessor<boolean>
onSubmit: () => void onSubmit: () => void
onStop: () => void onStop: () => void
+27 -4
View File
@@ -167,6 +167,16 @@ export function Prompt(props: PromptProps) {
const dialog = useDialog() const dialog = useDialog()
const toast = useToast() const toast = useToast()
const status = createMemo(() => data.session.status(props.sessionID ?? "")) const status = createMemo(() => data.session.status(props.sessionID ?? ""))
const [stoppingSession, setStoppingSession] = createSignal<string>()
const stopping = createMemo(() => stoppingSession() === props.sessionID && status() === "running")
createEffect(
on(
() => [props.sessionID, status()] as const,
([, current]) => {
if (current === "idle") setStoppingSession(undefined)
},
),
)
const history = usePromptHistory() const history = usePromptHistory()
const stash = usePromptStash() const stash = usePromptStash()
const keymap = Keymap.use() const keymap = Keymap.use()
@@ -466,7 +476,7 @@ export function Prompt(props: PromptProps) {
name: "session.interrupt", name: "session.interrupt",
category: "Session", category: "Session",
palette: undefined, palette: undefined,
enabled: status() === "running", enabled: status() === "running" && !stopping(),
run: () => { run: () => {
if (auto()?.visible) return if (auto()?.visible) return
if (!input.focused) return if (!input.focused) return
@@ -484,9 +494,12 @@ export function Prompt(props: PromptProps) {
}, 5000) }, 5000)
if (store.interrupt >= 2) { if (store.interrupt >= 2) {
void client.api.session.interrupt({ setStoppingSession(props.sessionID)
sessionID: props.sessionID, void client.api.session
}) .interrupt({
sessionID: props.sessionID,
})
.catch(() => setStoppingSession(undefined))
setStore("interrupt", 0) setStore("interrupt", 0)
} }
dialog.clear() dialog.clear()
@@ -1797,6 +1810,16 @@ export function Prompt(props: PromptProps) {
<Slot path="prompt.footer.status" input={footerInput()}> <Slot path="prompt.footer.status" input={footerInput()}>
<box flexGrow={1} flexShrink={1} minWidth={0}> <box flexGrow={1} flexShrink={1} minWidth={0}>
<Switch> <Switch>
<Match when={stopping()}>
<box flexDirection="row" gap={1} flexGrow={1} justifyContent="flex-start">
<box marginLeft={1}>
<Show when={config.animations ?? true} fallback={<text fg={theme.text.subdued}>[]</text>}>
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
</Show>
</box>
<text fg={theme.text.subdued}>Stopping...</text>
</box>
</Match>
<Match when={status() === "running"}> <Match when={status() === "running"}>
<box flexDirection="row" gap={1} flexGrow={1} justifyContent="flex-start"> <box flexDirection="row" gap={1} flexGrow={1} justifyContent="flex-start">
<box marginLeft={1}> <box marginLeft={1}>