tui: convert handoff from text command to dedicated mode with slash command
parent
9b2fd57e6e
commit
f689fc7f75
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue