tui: convert handoff from text command to dedicated mode with slash command

pull/12755/head
Dax Raad 2026-02-08 18:44:35 -05:00
parent 9b2fd57e6e
commit f689fc7f75
1 changed files with 65 additions and 30 deletions

View File

@ -119,7 +119,7 @@ export function Prompt(props: PromptProps) {
const [store, setStore] = createStore<{
prompt: PromptInfo
mode: "normal" | "shell"
mode: "normal" | "shell" | "handoff"
extmarkToPartIndex: Map<number, number>
interrupt: number
placeholder: number
@ -338,6 +338,20 @@ export function Prompt(props: PromptProps) {
))
},
},
{
title: "Handoff",
value: "prompt.handoff",
disabled: props.sessionID === undefined,
category: "Prompt",
slash: {
name: "handoff",
},
onSelect: () => {
input.clear()
setStore("mode", "handoff")
setStore("prompt", { input: "", parts: [] })
},
},
]
})
@ -515,17 +529,45 @@ export function Prompt(props: PromptProps) {
async function submit() {
if (props.disabled) return
if (autocomplete?.visible) return
const selectedModel = local.model.current()
if (!selectedModel) {
promptModelWarning()
return
}
if (store.mode === "handoff") {
const result = await sdk.client.session.handoff({
sessionID: props.sessionID!,
goal: store.prompt.input,
model: {
providerID: selectedModel.providerID,
modelID: selectedModel.modelID,
},
})
if (result.data) {
route.navigate({
type: "home",
initialPrompt: {
input: result.data.text,
parts:
result.data.files.map((file) => ({
type: "file",
url: file,
filename: file,
mime: "text/plain",
})) ?? [],
},
})
}
return
}
if (!store.prompt.input) return
const trimmed = store.prompt.input.trim()
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
exit()
return
}
const selectedModel = local.model.current()
if (!selectedModel) {
promptModelWarning()
return
}
const sessionID = props.sessionID
? props.sessionID
: await (async () => {
@ -569,27 +611,6 @@ export function Prompt(props: PromptProps) {
command: inputText,
})
setStore("mode", "normal")
} else if (inputText.startsWith("/handoff ")) {
// Handle handoff command specially - call endpoint and replace prompt
const goal = inputText.slice(9).trim() // Remove "/handoff " prefix
if (goal) {
const result = await sdk.client.session.handoff({
sessionID,
goal,
model: {
providerID: selectedModel.providerID,
modelID: selectedModel.modelID,
},
})
if (result.data) {
// Replace prompt with the handoff text
const handoffText = result.data.text
input.setText(handoffText)
setStore("prompt", { input: handoffText, parts: [] })
// Don't submit yet - let user review and submit manually
return
}
}
} else if (
inputText.startsWith("/") &&
iife(() => {
@ -747,6 +768,7 @@ export function Prompt(props: PromptProps) {
const highlight = createMemo(() => {
if (keybind.leader) return theme.border
if (store.mode === "shell") return theme.primary
if (store.mode === "handoff") return theme.warning
return local.agent.color(local.agent.current().name)
})
@ -818,7 +840,11 @@ export function Prompt(props: PromptProps) {
flexGrow={1}
>
<textarea
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
placeholder={iife(() => {
if (store.mode === "handoff") return "Goal for the new session"
if (props.sessionID) return undefined
return `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`
})}
textColor={keybind.leader ? theme.textMuted : theme.text}
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
minHeight={1}
@ -875,7 +901,7 @@ export function Prompt(props: PromptProps) {
e.preventDefault()
return
}
if (store.mode === "shell") {
if (store.mode === "shell" || store.mode === "handoff") {
if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
setStore("mode", "normal")
e.preventDefault()
@ -996,7 +1022,11 @@ export function Prompt(props: PromptProps) {
/>
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
<text fg={highlight()}>
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
<Switch>
<Match when={store.mode === "normal"}>{Locale.titlecase(local.agent.current().name)}</Match>
<Match when={store.mode === "shell"}>Shell</Match>
<Match when={store.mode === "handoff"}>Handoff</Match>
</Switch>
</text>
<Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}>
@ -1143,6 +1173,11 @@ export function Prompt(props: PromptProps) {
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
</text>
</Match>
<Match when={store.mode === "handoff"}>
<text fg={theme.text}>
esc <span style={{ fg: theme.textMuted }}>exit handoff mode</span>
</text>
</Match>
</Switch>
</box>
</Show>