tui: convert handoff from text command to dedicated mode with slash command
This commit is contained in:
parent
9b2fd57e6e
commit
f689fc7f75
1 changed files with 65 additions and 30 deletions
|
|
@ -119,7 +119,7 @@ export function Prompt(props: PromptProps) {
|
||||||
|
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
prompt: PromptInfo
|
prompt: PromptInfo
|
||||||
mode: "normal" | "shell"
|
mode: "normal" | "shell" | "handoff"
|
||||||
extmarkToPartIndex: Map<number, number>
|
extmarkToPartIndex: Map<number, number>
|
||||||
interrupt: number
|
interrupt: number
|
||||||
placeholder: 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() {
|
async function submit() {
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
if (autocomplete?.visible) 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
|
if (!store.prompt.input) return
|
||||||
const trimmed = store.prompt.input.trim()
|
const trimmed = store.prompt.input.trim()
|
||||||
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||||
exit()
|
exit()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const selectedModel = local.model.current()
|
|
||||||
if (!selectedModel) {
|
|
||||||
promptModelWarning()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sessionID = props.sessionID
|
const sessionID = props.sessionID
|
||||||
? props.sessionID
|
? props.sessionID
|
||||||
: await (async () => {
|
: await (async () => {
|
||||||
|
|
@ -569,27 +611,6 @@ export function Prompt(props: PromptProps) {
|
||||||
command: inputText,
|
command: inputText,
|
||||||
})
|
})
|
||||||
setStore("mode", "normal")
|
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 (
|
} else if (
|
||||||
inputText.startsWith("/") &&
|
inputText.startsWith("/") &&
|
||||||
iife(() => {
|
iife(() => {
|
||||||
|
|
@ -747,6 +768,7 @@ export function Prompt(props: PromptProps) {
|
||||||
const highlight = createMemo(() => {
|
const highlight = createMemo(() => {
|
||||||
if (keybind.leader) return theme.border
|
if (keybind.leader) return theme.border
|
||||||
if (store.mode === "shell") return theme.primary
|
if (store.mode === "shell") return theme.primary
|
||||||
|
if (store.mode === "handoff") return theme.warning
|
||||||
return local.agent.color(local.agent.current().name)
|
return local.agent.color(local.agent.current().name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -818,7 +840,11 @@ export function Prompt(props: PromptProps) {
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
>
|
>
|
||||||
<textarea
|
<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}
|
textColor={keybind.leader ? theme.textMuted : theme.text}
|
||||||
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
||||||
minHeight={1}
|
minHeight={1}
|
||||||
|
|
@ -875,7 +901,7 @@ export function Prompt(props: PromptProps) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (store.mode === "shell") {
|
if (store.mode === "shell" || store.mode === "handoff") {
|
||||||
if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
|
||||||
setStore("mode", "normal")
|
setStore("mode", "normal")
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
@ -996,7 +1022,11 @@ export function Prompt(props: PromptProps) {
|
||||||
/>
|
/>
|
||||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
||||||
<text fg={highlight()}>
|
<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>
|
</text>
|
||||||
<Show when={store.mode === "normal"}>
|
<Show when={store.mode === "normal"}>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
|
|
@ -1143,6 +1173,11 @@ export function Prompt(props: PromptProps) {
|
||||||
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
||||||
</text>
|
</text>
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={store.mode === "handoff"}>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
esc <span style={{ fg: theme.textMuted }}>exit handoff mode</span>
|
||||||
|
</text>
|
||||||
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue