mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-13 15:03:43 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c478ad52a9 |
@@ -402,6 +402,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
|
||||
},
|
||||
submit: {
|
||||
stopping,
|
||||
pending: submission.stopping,
|
||||
working,
|
||||
onSubmit: () => void submission.handleSubmit(new Event("submit")),
|
||||
onStop: () => void submission.abort(),
|
||||
|
||||
@@ -261,6 +261,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
() => prompt.capture(),
|
||||
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 motion = (value: number) => ({
|
||||
opacity: value,
|
||||
@@ -283,9 +286,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
.join("")
|
||||
return text.trim().length === 0 && imageAttachments().length === 0 && commentCount() === 0
|
||||
})
|
||||
const stopping = createMemo(() => working() && blank())
|
||||
const stopAction = createMemo(() => working() && blank())
|
||||
const tip = () => {
|
||||
if (stopping()) {
|
||||
if (store.stopping) return <span>{language.t("prompt.action.stop")}...</span>
|
||||
if (stopAction()) {
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{language.t("prompt.action.stop")}</span>
|
||||
@@ -1198,36 +1202,46 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return permission.isAutoAccepting(id, sdk().directory)
|
||||
})
|
||||
|
||||
const { abort, handleSubmit } =
|
||||
props.submission ??
|
||||
createPromptSubmit({
|
||||
prompt,
|
||||
info,
|
||||
imageAttachments,
|
||||
commentCount,
|
||||
autoAccept: () => accepting(),
|
||||
mode: () => store.mode,
|
||||
working,
|
||||
editor: () => editorRef,
|
||||
queueScroll,
|
||||
promptLength,
|
||||
addToHistory,
|
||||
resetHistoryNavigation: () => {
|
||||
resetHistoryNavigation(true)
|
||||
},
|
||||
setMode: (mode) => setStore("mode", mode),
|
||||
setPopover: (popover) => {
|
||||
if (!popover) return closePopover()
|
||||
setStore({ popover, slashMenu: false, slashMenuQuery: "" })
|
||||
},
|
||||
newSessionWorktree: () => props.newSessionWorktree,
|
||||
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
|
||||
shouldQueue: props.shouldQueue,
|
||||
onQueue: props.onQueue,
|
||||
onAbort: props.onAbort,
|
||||
onSubmit: props.onSubmit,
|
||||
model: props.controls.model.selection,
|
||||
const {
|
||||
abort: requestAbort,
|
||||
handleSubmit,
|
||||
stopping: requestStopping,
|
||||
} = props.submission ??
|
||||
createPromptSubmit({
|
||||
prompt,
|
||||
info,
|
||||
imageAttachments,
|
||||
commentCount,
|
||||
autoAccept: () => accepting(),
|
||||
mode: () => store.mode,
|
||||
working,
|
||||
editor: () => editorRef,
|
||||
queueScroll,
|
||||
promptLength,
|
||||
addToHistory,
|
||||
resetHistoryNavigation: () => {
|
||||
resetHistoryNavigation(true)
|
||||
},
|
||||
setMode: (mode) => setStore("mode", mode),
|
||||
setPopover: (popover) => {
|
||||
if (!popover) return closePopover()
|
||||
setStore({ popover, slashMenu: false, slashMenuQuery: "" })
|
||||
},
|
||||
newSessionWorktree: () => props.newSessionWorktree,
|
||||
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
|
||||
shouldQueue: props.shouldQueue,
|
||||
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) => {
|
||||
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") {
|
||||
@@ -1579,12 +1593,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<IconButton
|
||||
data-action="prompt-submit"
|
||||
type="submit"
|
||||
disabled={!working() && blank()}
|
||||
disabled={store.stopping || (!working() && blank())}
|
||||
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"
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ export type PromptInputState = ReturnType<typeof usePrompt>
|
||||
export type PromptInputSubmission = {
|
||||
abort: () => Promise<void> | void
|
||||
handleSubmit: (event: Event) => Promise<void> | void
|
||||
stopping?: () => boolean
|
||||
}
|
||||
|
||||
export type PromptInputControls = {
|
||||
|
||||
@@ -46,6 +46,9 @@ let selected = "/repo/worktree-a"
|
||||
let variant: string | undefined
|
||||
let permissionServer = "server-a"
|
||||
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 }]
|
||||
const [promptStore, setPromptStore] = createStore<PromptStore>({
|
||||
@@ -121,6 +124,11 @@ const clientFor = (directory: string) => {
|
||||
shell: async (input: { sessionID: string; id?: string; command: string }) => {
|
||||
sentShell.push(input)
|
||||
},
|
||||
interrupt: async () => {
|
||||
interruptCalls++
|
||||
await interruptGate
|
||||
if (interruptFailure) throw new Error("interrupt failed")
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
@@ -310,11 +318,74 @@ beforeEach(() => {
|
||||
variant = undefined
|
||||
permissionServer = "server-a"
|
||||
createSessionGate = undefined
|
||||
interruptGate = undefined
|
||||
interruptCalls = 0
|
||||
interruptFailure = false
|
||||
serverSessionSyncs = 0
|
||||
for (const key of Object.keys(storedSessions)) delete storedSessions[key]
|
||||
})
|
||||
|
||||
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 () => {
|
||||
const submit = createPromptSubmit({
|
||||
prompt,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { showToast } from "@/utils/toast"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
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 { useServerSync, type ServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -263,6 +263,12 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const params = useParams()
|
||||
const [search] = useSearchParams<{ draftId?: string }>()
|
||||
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 errorMessage = (err: unknown) => {
|
||||
@@ -276,8 +282,10 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
}
|
||||
|
||||
const abort = async () => {
|
||||
if (isStopping()) return
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return Promise.resolve()
|
||||
setStopping(true)
|
||||
|
||||
serverSync().session.set("todo", sessionID, [])
|
||||
|
||||
@@ -289,11 +297,12 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
queued.abort.abort()
|
||||
queued.cleanup()
|
||||
pending.delete(key)
|
||||
setStopping(false)
|
||||
return Promise.resolve()
|
||||
}
|
||||
return sdk()
|
||||
.api.session.interrupt({ sessionID })
|
||||
.catch(() => {})
|
||||
.catch(() => setStopping(false))
|
||||
}
|
||||
|
||||
const restoreCommentItems = (
|
||||
@@ -649,5 +658,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
return {
|
||||
abort,
|
||||
handleSubmit,
|
||||
stopping: isStopping,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export type PromptInputTransientState = {
|
||||
draggingType: "image" | "@mention" | null
|
||||
mode: "normal" | "shell"
|
||||
applyingHistory: boolean
|
||||
stopping: boolean
|
||||
}
|
||||
|
||||
function resetPromptInputTransientState(setStore: SetStoreFunction<PromptInputTransientState>) {
|
||||
@@ -24,6 +25,7 @@ function resetPromptInputTransientState(setStore: SetStoreFunction<PromptInputTr
|
||||
draggingType: null,
|
||||
mode: "normal",
|
||||
applyingHistory: false,
|
||||
stopping: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,6 +40,7 @@ export function createPromptInputTransientState(identity: Accessor<unknown>, pla
|
||||
draggingType: null,
|
||||
mode: "normal",
|
||||
applyingHistory: false,
|
||||
stopping: false,
|
||||
})
|
||||
|
||||
createComputed(on(identity, () => resetPromptInputTransientState(setStore), { defer: true }))
|
||||
|
||||
@@ -28,7 +28,7 @@ import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
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 { Snapshot } from "@opencode-ai/core/snapshot"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
@@ -355,6 +355,18 @@ const pluginSupervisor = Layer.succeed(
|
||||
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, {
|
||||
provider: {
|
||||
get: () => Effect.succeed(undefined),
|
||||
@@ -370,7 +382,7 @@ const promptCatalog = Layer.mock(Catalog.Service, {
|
||||
},
|
||||
})
|
||||
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
||||
[Snapshot.node, Snapshot.noopLayer],
|
||||
[Snapshot.node, snapshots],
|
||||
[LayerNodePlatform.llmClient, client],
|
||||
[SessionRunnerModel.node, models],
|
||||
[InstructionBuiltIns.node, systemContext],
|
||||
@@ -437,7 +449,7 @@ const it = testEffect(
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
[SkillInstructions.node, skillInstructions],
|
||||
[ReferenceInstructions.node, referenceInstructions],
|
||||
[Snapshot.node, Snapshot.noopLayer],
|
||||
[Snapshot.node, snapshots],
|
||||
[SessionExecution.node, execution],
|
||||
[Config.node, config],
|
||||
[PluginSupervisor.node, pluginSupervisor],
|
||||
@@ -493,6 +505,8 @@ const setup = Effect.gen(function* () {
|
||||
systemLoadHook = Effect.void
|
||||
modelResolveHook = Effect.void
|
||||
pluginFlushHook = Effect.void
|
||||
snapshotCaptureHook = () => Effect.succeed(undefined)
|
||||
snapshotFilesHook = () => Effect.succeed([])
|
||||
currentModel = model
|
||||
skillBaselines.clear()
|
||||
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", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
|
||||
@@ -257,6 +257,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
||||
<PromptInputV2SubmitButton
|
||||
mode={state.mode}
|
||||
stopping={view.submit.stopping()}
|
||||
pending={view.submit.pending?.() ?? false}
|
||||
disabled={!props.controller.canSubmit()}
|
||||
sendLabel={i18n.t("ui.promptInput.send")}
|
||||
stopLabel={i18n.t("ui.promptInput.stop")}
|
||||
@@ -672,6 +673,7 @@ export function PromptInputV2Popover(props: {
|
||||
export function PromptInputV2SubmitButton(props: {
|
||||
mode: PromptInputV2Mode
|
||||
stopping: boolean
|
||||
pending: boolean
|
||||
disabled: boolean
|
||||
sendLabel: string
|
||||
stopLabel: string
|
||||
@@ -682,12 +684,12 @@ export function PromptInputV2SubmitButton(props: {
|
||||
<TooltipV2
|
||||
placement="top"
|
||||
inactive={!props.stopping && props.disabled}
|
||||
value={props.stopping ? props.stopLabel : props.sendLabel}
|
||||
value={props.pending ? `${props.stopLabel}...` : props.stopping ? props.stopLabel : props.sendLabel}
|
||||
>
|
||||
<IconButton
|
||||
data-action="prompt-submit"
|
||||
type="button"
|
||||
disabled={!props.stopping && props.disabled}
|
||||
disabled={props.pending || (!props.stopping && props.disabled)}
|
||||
tabIndex={props.mode === "normal" ? undefined : -1}
|
||||
icon={props.stopping ? "stop" : props.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
|
||||
variant="primary"
|
||||
|
||||
@@ -36,6 +36,7 @@ export type PromptInputV2ViewConfig = {
|
||||
variant?: PromptInputV2SelectControl
|
||||
submit: {
|
||||
stopping: Accessor<boolean>
|
||||
pending?: Accessor<boolean>
|
||||
working?: Accessor<boolean>
|
||||
onSubmit: () => void
|
||||
onStop: () => void
|
||||
|
||||
@@ -167,6 +167,16 @@ export function Prompt(props: PromptProps) {
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
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 stash = usePromptStash()
|
||||
const keymap = Keymap.use()
|
||||
@@ -466,7 +476,7 @@ export function Prompt(props: PromptProps) {
|
||||
name: "session.interrupt",
|
||||
category: "Session",
|
||||
palette: undefined,
|
||||
enabled: status() === "running",
|
||||
enabled: status() === "running" && !stopping(),
|
||||
run: () => {
|
||||
if (auto()?.visible) return
|
||||
if (!input.focused) return
|
||||
@@ -484,9 +494,12 @@ export function Prompt(props: PromptProps) {
|
||||
}, 5000)
|
||||
|
||||
if (store.interrupt >= 2) {
|
||||
void client.api.session.interrupt({
|
||||
sessionID: props.sessionID,
|
||||
})
|
||||
setStoppingSession(props.sessionID)
|
||||
void client.api.session
|
||||
.interrupt({
|
||||
sessionID: props.sessionID,
|
||||
})
|
||||
.catch(() => setStoppingSession(undefined))
|
||||
setStore("interrupt", 0)
|
||||
}
|
||||
dialog.clear()
|
||||
@@ -1797,6 +1810,16 @@ export function Prompt(props: PromptProps) {
|
||||
<Slot path="prompt.footer.status" input={footerInput()}>
|
||||
<box flexGrow={1} flexShrink={1} minWidth={0}>
|
||||
<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"}>
|
||||
<box flexDirection="row" gap={1} flexGrow={1} justifyContent="flex-start">
|
||||
<box marginLeft={1}>
|
||||
|
||||
Reference in New Issue
Block a user