Merge 87651d2849 into ae614d919f
commit
8c78099cf2
|
|
@ -37,6 +37,8 @@ import { useKV } from "../../context/kv"
|
|||
import { useTextareaKeybindings } from "../textarea-keybindings"
|
||||
import { DialogSkill } from "../dialog-skill"
|
||||
|
||||
const EXIT_CONFIRM_MS = 3000
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
workspaceID?: string
|
||||
|
|
@ -95,8 +97,28 @@ export function Prompt(props: PromptProps) {
|
|||
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const [exitConfirmArmed, setExitConfirmArmed] = createSignal(false)
|
||||
const [exitPending, setExitPending] = createSignal(false)
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const hasRightContent = createMemo(() => Boolean(props.right))
|
||||
let exitConfirmTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const clearExitConfirm = () => {
|
||||
setExitConfirmArmed(false)
|
||||
if (exitConfirmTimeout) {
|
||||
clearTimeout(exitConfirmTimeout)
|
||||
exitConfirmTimeout = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const armExitConfirm = () => {
|
||||
setExitConfirmArmed(true)
|
||||
if (exitConfirmTimeout) clearTimeout(exitConfirmTimeout)
|
||||
exitConfirmTimeout = setTimeout(() => {
|
||||
setExitConfirmArmed(false)
|
||||
exitConfirmTimeout = undefined
|
||||
}, EXIT_CONFIRM_MS)
|
||||
}
|
||||
|
||||
function promptModelWarning() {
|
||||
toast.show({
|
||||
|
|
@ -429,6 +451,7 @@ export function Prompt(props: PromptProps) {
|
|||
}
|
||||
|
||||
onCleanup(() => {
|
||||
clearExitConfirm()
|
||||
props.ref?.(undefined)
|
||||
})
|
||||
|
||||
|
|
@ -919,6 +942,13 @@ export function Prompt(props: PromptProps) {
|
|||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (exitPending()) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (exitConfirmArmed() && !keybind.match("app_exit", e)) {
|
||||
clearExitConfirm()
|
||||
}
|
||||
// Check clipboard for images before terminal-handled paste runs.
|
||||
// This helps terminals that forward Ctrl+V to the app; Windows
|
||||
// Terminal 1.25+ usually handles Ctrl+V before this path.
|
||||
|
|
@ -936,6 +966,7 @@ export function Prompt(props: PromptProps) {
|
|||
// If no image, let the default paste behavior continue
|
||||
}
|
||||
if (keybind.match("input_clear", e) && store.prompt.input !== "") {
|
||||
clearExitConfirm()
|
||||
input.clear()
|
||||
input.extmarks.clear()
|
||||
setStore("prompt", {
|
||||
|
|
@ -947,9 +978,15 @@ export function Prompt(props: PromptProps) {
|
|||
}
|
||||
if (keybind.match("app_exit", e)) {
|
||||
if (store.prompt.input === "") {
|
||||
await exit()
|
||||
// Don't preventDefault - let textarea potentially handle the event
|
||||
e.preventDefault()
|
||||
if (exitConfirmArmed()) {
|
||||
clearExitConfirm()
|
||||
setExitPending(true)
|
||||
await exit()
|
||||
return
|
||||
}
|
||||
|
||||
armExitConfirm()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -1144,7 +1181,20 @@ export function Prompt(props: PromptProps) {
|
|||
/>
|
||||
</box>
|
||||
<box width="100%" flexDirection="row" justifyContent="space-between">
|
||||
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
|
||||
<Show
|
||||
when={status().type !== "idle"}
|
||||
fallback={
|
||||
<Show
|
||||
when={exitConfirmArmed()}
|
||||
fallback={props.hint ?? <text />}
|
||||
children={
|
||||
<text fg={theme.warning}>
|
||||
{keybind.print("app_exit")} <span style={{ fg: theme.textMuted }}>again to exit</span>
|
||||
</text>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
|||
await input.onBeforeExit?.()
|
||||
// Reset window title before destroying renderer
|
||||
renderer.setTerminalTitle("")
|
||||
// Disable mouse tracking synchronously before destroy.
|
||||
// renderer.destroy() may defer native cleanup when called during a
|
||||
// render frame, so we also register a process 'exit' handler as a
|
||||
// last-resort safeguard that runs right before the process terminates.
|
||||
const MOUSE_RESET =
|
||||
"\x1b[?1003l" + // disable any-event mouse tracking
|
||||
"\x1b[?1006l" + // disable SGR mouse mode
|
||||
"\x1b[?1000l" + // disable normal mouse tracking
|
||||
"\x1b[?25h" // show cursor
|
||||
process.stdout.write(MOUSE_RESET)
|
||||
renderer.destroy()
|
||||
win32FlushInputBuffer()
|
||||
if (reason) {
|
||||
|
|
@ -55,6 +65,16 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
|||
},
|
||||
)
|
||||
process.on("SIGHUP", () => exit())
|
||||
// Last-resort: if process.exit() fires before renderer cleanup finishes,
|
||||
// 'exit' event still runs synchronously and can write terminal reset sequences.
|
||||
process.on("exit", () => {
|
||||
process.stdout.write(
|
||||
"\x1b[?1003l" + // disable any-event mouse tracking
|
||||
"\x1b[?1006l" + // disable SGR mouse mode
|
||||
"\x1b[?1000l" + // disable normal mouse tracking
|
||||
"\x1b[?25h", // show cursor
|
||||
)
|
||||
})
|
||||
return exit
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
For,
|
||||
Match,
|
||||
on,
|
||||
onCleanup,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
|
|
@ -84,6 +85,8 @@ import { useTuiConfig } from "../../context/tui-config"
|
|||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
import { TuiPluginRuntime } from "../../plugin"
|
||||
|
||||
const EXIT_CONFIRM_MS = 3000
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
const context = createContext<{
|
||||
|
|
@ -217,9 +220,32 @@ export function Session() {
|
|||
const keybind = useKeybind()
|
||||
const dialog = useDialog()
|
||||
const renderer = useRenderer()
|
||||
const [exitConfirmArmed, setExitConfirmArmed] = createSignal(false)
|
||||
const [exitPending, setExitPending] = createSignal(false)
|
||||
let exitConfirmTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
// Allow exit when in child session (prompt is hidden)
|
||||
const exit = useExit()
|
||||
const clearExitConfirm = () => {
|
||||
setExitConfirmArmed(false)
|
||||
if (exitConfirmTimeout) {
|
||||
clearTimeout(exitConfirmTimeout)
|
||||
exitConfirmTimeout = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const armExitConfirm = () => {
|
||||
setExitConfirmArmed(true)
|
||||
if (exitConfirmTimeout) clearTimeout(exitConfirmTimeout)
|
||||
exitConfirmTimeout = setTimeout(() => {
|
||||
setExitConfirmArmed(false)
|
||||
exitConfirmTimeout = undefined
|
||||
}, EXIT_CONFIRM_MS)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
clearExitConfirm()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const title = Locale.truncate(session()?.title ?? "", 50)
|
||||
|
|
@ -242,8 +268,28 @@ export function Session() {
|
|||
|
||||
useKeyboard((evt) => {
|
||||
if (!session()?.parentID) return
|
||||
if (exitPending()) {
|
||||
evt.preventDefault()
|
||||
return
|
||||
}
|
||||
if (exitConfirmArmed() && !keybind.match("app_exit", evt)) {
|
||||
clearExitConfirm()
|
||||
}
|
||||
if (keybind.match("app_exit", evt)) {
|
||||
exit()
|
||||
evt.preventDefault()
|
||||
if (exitConfirmArmed()) {
|
||||
clearExitConfirm()
|
||||
setExitPending(true)
|
||||
exit()
|
||||
return
|
||||
}
|
||||
|
||||
armExitConfirm()
|
||||
toast.show({
|
||||
message: `Press ${keybind.print("app_exit")} again to exit`,
|
||||
variant: "info",
|
||||
duration: EXIT_CONFIRM_MS,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export function win32InstallCtrlCGuard() {
|
|||
// Ensure it's cleared immediately too (covers any earlier mode changes).
|
||||
later()
|
||||
|
||||
const interval = setInterval(enforce, 100)
|
||||
const interval = setInterval(enforce, 16)
|
||||
interval.unref()
|
||||
|
||||
let done = false
|
||||
|
|
|
|||
Loading…
Reference in New Issue