introduce opentui keymap as sole key/cmd engine (#26053)
This commit is contained in:
@@ -49,12 +49,10 @@ import type { WebSearchTool } from "@/tool/websearch"
|
||||
import type { TaskTool } from "@/tool/task"
|
||||
import type { QuestionTool } from "@/tool/question"
|
||||
import type { SkillTool } from "@/tool/skill"
|
||||
import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useEditorContext } from "@tui/context/editor"
|
||||
import { useCommandDialog } from "@tui/component/dialog-command"
|
||||
import type { DialogContext } from "@tui/ui/dialog"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { TodoItem } from "../../component/todo-item"
|
||||
import { DialogMessage } from "./dialog-message"
|
||||
@@ -90,6 +88,8 @@ import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
|
||||
import { DialogGoUpsell } from "../../component/dialog-go-upsell"
|
||||
import { SessionRetry } from "@/session/retry"
|
||||
import { getRevertDiffFiles } from "../../util/revert-diff"
|
||||
import { useCommandPalette } from "../../context/command-palette"
|
||||
import { useBindings, useCommandShortcut } from "../../keymap"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -124,6 +124,9 @@ export function Session() {
|
||||
const event = useEvent()
|
||||
const project = useProject()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const {
|
||||
keymap: { sections },
|
||||
} = tuiConfig
|
||||
const kv = useKV()
|
||||
const { theme } = useTheme()
|
||||
const promptRef = usePromptRef()
|
||||
@@ -250,7 +253,7 @@ export function Session() {
|
||||
seeded = true
|
||||
r.set(route.prompt)
|
||||
}
|
||||
const keybind = useKeybind()
|
||||
const command = useCommandPalette()
|
||||
const dialog = useDialog()
|
||||
const renderer = useRenderer()
|
||||
|
||||
@@ -271,7 +274,6 @@ export function Session() {
|
||||
})
|
||||
})
|
||||
|
||||
// Allow exit when in child session (prompt is hidden)
|
||||
const exit = useExit()
|
||||
|
||||
createEffect(() => {
|
||||
@@ -293,13 +295,6 @@ export function Session() {
|
||||
)
|
||||
})
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (!session()?.parentID) return
|
||||
if (keybind.match("app_exit", evt)) {
|
||||
void exit()
|
||||
}
|
||||
})
|
||||
|
||||
// Helper: Find next visible message boundary in direction
|
||||
const findNextVisibleMessage = (direction: "next" | "prev"): string | null => {
|
||||
const children = scroll.getChildren()
|
||||
@@ -382,26 +377,24 @@ export function Session() {
|
||||
}
|
||||
}
|
||||
|
||||
function childSessionHandler(func: (dialog: DialogContext) => void) {
|
||||
return (dialog: DialogContext) => {
|
||||
function childSessionHandler(func: () => void) {
|
||||
return () => {
|
||||
if (!session()?.parentID || dialog.stack.length > 0) return
|
||||
func(dialog)
|
||||
func()
|
||||
}
|
||||
}
|
||||
|
||||
const command = useCommandDialog()
|
||||
command.register(() => [
|
||||
const sessionCommandList = createMemo(() => [
|
||||
{
|
||||
title: session()?.share?.url ? "Copy share link" : "Share session",
|
||||
value: "session.share",
|
||||
suggested: route.type === "session",
|
||||
keybind: "session_share",
|
||||
category: "Session",
|
||||
enabled: sync.data.config.share !== "disabled",
|
||||
slash: {
|
||||
name: "share",
|
||||
},
|
||||
onSelect: async (dialog) => {
|
||||
run: async () => {
|
||||
const copy = (url: string) =>
|
||||
Clipboard.copy(url)
|
||||
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
|
||||
@@ -434,24 +427,22 @@ export function Session() {
|
||||
{
|
||||
title: "Rename session",
|
||||
value: "session.rename",
|
||||
keybind: "session_rename",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "rename",
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
dialog.replace(() => <DialogSessionRename session={route.sessionID} />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Jump to message",
|
||||
value: "session.timeline",
|
||||
keybind: "session_timeline",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "timeline",
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
dialog.replace(() => (
|
||||
<DialogTimeline
|
||||
onMove={(messageID) => {
|
||||
@@ -469,12 +460,11 @@ export function Session() {
|
||||
{
|
||||
title: "Fork session",
|
||||
value: "session.fork",
|
||||
keybind: "session_fork",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "fork",
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
dialog.replace(() => (
|
||||
<DialogForkFromTimeline
|
||||
onMove={(messageID) => {
|
||||
@@ -492,13 +482,12 @@ export function Session() {
|
||||
{
|
||||
title: "Compact session",
|
||||
value: "session.compact",
|
||||
keybind: "session_compact",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "compact",
|
||||
aliases: ["summarize"],
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
const selectedModel = local.model.current()
|
||||
if (!selectedModel) {
|
||||
toast.show({
|
||||
@@ -519,13 +508,12 @@ export function Session() {
|
||||
{
|
||||
title: "Unshare session",
|
||||
value: "session.unshare",
|
||||
keybind: "session_unshare",
|
||||
category: "Session",
|
||||
enabled: !!session()?.share?.url,
|
||||
slash: {
|
||||
name: "unshare",
|
||||
},
|
||||
onSelect: async (dialog) => {
|
||||
run: async () => {
|
||||
await sdk.client.session
|
||||
.unshare({
|
||||
sessionID: route.sessionID,
|
||||
@@ -543,12 +531,11 @@ export function Session() {
|
||||
{
|
||||
title: "Undo previous message",
|
||||
value: "session.undo",
|
||||
keybind: "messages_undo",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "undo",
|
||||
},
|
||||
onSelect: async (dialog) => {
|
||||
run: async () => {
|
||||
const status = sync.data.session_status?.[route.sessionID]
|
||||
if (status?.type !== "idle") await sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {})
|
||||
const revert = session()?.revert?.messageID
|
||||
@@ -581,13 +568,12 @@ export function Session() {
|
||||
{
|
||||
title: "Redo",
|
||||
value: "session.redo",
|
||||
keybind: "messages_redo",
|
||||
category: "Session",
|
||||
enabled: !!session()?.revert?.messageID,
|
||||
slash: {
|
||||
name: "redo",
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
dialog.clear()
|
||||
const messageID = session()?.revert?.messageID
|
||||
if (!messageID) return
|
||||
@@ -608,9 +594,8 @@ export function Session() {
|
||||
{
|
||||
title: sidebarVisible() ? "Hide sidebar" : "Show sidebar",
|
||||
value: "session.sidebar.toggle",
|
||||
keybind: "sidebar_toggle",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
batch(() => {
|
||||
const isVisible = sidebarVisible()
|
||||
setSidebar(() => (isVisible ? "hide" : "auto"))
|
||||
@@ -622,9 +607,8 @@ export function Session() {
|
||||
{
|
||||
title: conceal() ? "Disable code concealment" : "Enable code concealment",
|
||||
value: "session.toggle.conceal",
|
||||
keybind: "messages_toggle_conceal",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
setConceal((prev) => !prev)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -637,7 +621,7 @@ export function Session() {
|
||||
name: "timestamps",
|
||||
aliases: ["toggle-timestamps"],
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
setTimestamps((prev) => (prev === "show" ? "hide" : "show"))
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -645,13 +629,12 @@ export function Session() {
|
||||
{
|
||||
title: showThinking() ? "Hide thinking" : "Show thinking",
|
||||
value: "session.toggle.thinking",
|
||||
keybind: "display_thinking",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "thinking",
|
||||
aliases: ["toggle-thinking"],
|
||||
},
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
setShowThinking((prev) => !prev)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -659,9 +642,8 @@ export function Session() {
|
||||
{
|
||||
title: showDetails() ? "Hide tool details" : "Show tool details",
|
||||
value: "session.toggle.actions",
|
||||
keybind: "tool_details",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
setShowDetails((prev) => !prev)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -669,9 +651,8 @@ export function Session() {
|
||||
{
|
||||
title: "Toggle session scrollbar",
|
||||
value: "session.toggle.scrollbar",
|
||||
keybind: "scrollbar_toggle",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
setShowScrollbar((prev) => !prev)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -680,7 +661,7 @@ export function Session() {
|
||||
title: showGenericToolOutput() ? "Hide generic tool output" : "Show generic tool output",
|
||||
value: "session.toggle.generic_tool_output",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
setShowGenericToolOutput((prev) => !prev)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -688,10 +669,9 @@ export function Session() {
|
||||
{
|
||||
title: "Page up",
|
||||
value: "session.page.up",
|
||||
keybind: "messages_page_up",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
scroll.scrollBy(-scroll.height / 2)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -699,10 +679,9 @@ export function Session() {
|
||||
{
|
||||
title: "Page down",
|
||||
value: "session.page.down",
|
||||
keybind: "messages_page_down",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
scroll.scrollBy(scroll.height / 2)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -710,10 +689,9 @@ export function Session() {
|
||||
{
|
||||
title: "Line up",
|
||||
value: "session.line.up",
|
||||
keybind: "messages_line_up",
|
||||
category: "Session",
|
||||
disabled: true,
|
||||
onSelect: (dialog) => {
|
||||
enabled: false,
|
||||
run: () => {
|
||||
scroll.scrollBy(-1)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -721,10 +699,9 @@ export function Session() {
|
||||
{
|
||||
title: "Line down",
|
||||
value: "session.line.down",
|
||||
keybind: "messages_line_down",
|
||||
category: "Session",
|
||||
disabled: true,
|
||||
onSelect: (dialog) => {
|
||||
enabled: false,
|
||||
run: () => {
|
||||
scroll.scrollBy(1)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -732,10 +709,9 @@ export function Session() {
|
||||
{
|
||||
title: "Half page up",
|
||||
value: "session.half.page.up",
|
||||
keybind: "messages_half_page_up",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
scroll.scrollBy(-scroll.height / 4)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -743,10 +719,9 @@ export function Session() {
|
||||
{
|
||||
title: "Half page down",
|
||||
value: "session.half.page.down",
|
||||
keybind: "messages_half_page_down",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
scroll.scrollBy(scroll.height / 4)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -754,10 +729,9 @@ export function Session() {
|
||||
{
|
||||
title: "First message",
|
||||
value: "session.first",
|
||||
keybind: "messages_first",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
scroll.scrollTo(0)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -765,10 +739,9 @@ export function Session() {
|
||||
{
|
||||
title: "Last message",
|
||||
value: "session.last",
|
||||
keybind: "messages_last",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
scroll.scrollTo(scroll.scrollHeight)
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -776,10 +749,9 @@ export function Session() {
|
||||
{
|
||||
title: "Jump to last user message",
|
||||
value: "session.messages_last_user",
|
||||
keybind: "messages_last_user",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
run: () => {
|
||||
const messages = sync.data.message[route.sessionID]
|
||||
if (!messages || !messages.length) return
|
||||
|
||||
@@ -808,25 +780,22 @@ export function Session() {
|
||||
{
|
||||
title: "Next message",
|
||||
value: "session.message.next",
|
||||
keybind: "messages_next",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => scrollToMessage("next", dialog),
|
||||
run: () => scrollToMessage("next", dialog),
|
||||
},
|
||||
{
|
||||
title: "Previous message",
|
||||
value: "session.message.previous",
|
||||
keybind: "messages_previous",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => scrollToMessage("prev", dialog),
|
||||
run: () => scrollToMessage("prev", dialog),
|
||||
},
|
||||
{
|
||||
title: "Copy last assistant message",
|
||||
value: "messages.copy",
|
||||
keybind: "messages_copy",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
const revertID = session()?.revert?.messageID
|
||||
const lastAssistantMessage = messages().findLast(
|
||||
(msg) => msg.role === "assistant" && (!revertID || msg.id < revertID),
|
||||
@@ -871,7 +840,7 @@ export function Session() {
|
||||
slash: {
|
||||
name: "copy",
|
||||
},
|
||||
onSelect: async (dialog) => {
|
||||
run: async () => {
|
||||
try {
|
||||
const sessionData = session()
|
||||
if (!sessionData) return
|
||||
@@ -897,12 +866,11 @@ export function Session() {
|
||||
{
|
||||
title: "Export session transcript",
|
||||
value: "session.export",
|
||||
keybind: "session_export",
|
||||
category: "Session",
|
||||
slash: {
|
||||
name: "export",
|
||||
},
|
||||
onSelect: async (dialog) => {
|
||||
run: async () => {
|
||||
try {
|
||||
const sessionData = session()
|
||||
if (!sessionData) return
|
||||
@@ -959,10 +927,9 @@ export function Session() {
|
||||
{
|
||||
title: "Go to child session",
|
||||
value: "session.child.first",
|
||||
keybind: "session_child_first",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
run: () => {
|
||||
moveFirstChild()
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -970,11 +937,10 @@ export function Session() {
|
||||
{
|
||||
title: "Go to parent session",
|
||||
value: "session.parent",
|
||||
keybind: "session_parent",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
enabled: !!session()?.parentID,
|
||||
onSelect: childSessionHandler((dialog) => {
|
||||
run: childSessionHandler(() => {
|
||||
const parentID = session()?.parentID
|
||||
if (parentID) {
|
||||
navigate({
|
||||
@@ -988,11 +954,10 @@ export function Session() {
|
||||
{
|
||||
title: "Next child session",
|
||||
value: "session.child.next",
|
||||
keybind: "session_child_cycle",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
enabled: !!session()?.parentID,
|
||||
onSelect: childSessionHandler((dialog) => {
|
||||
run: childSessionHandler(() => {
|
||||
moveChild(1)
|
||||
dialog.clear()
|
||||
}),
|
||||
@@ -1000,17 +965,36 @@ export function Session() {
|
||||
{
|
||||
title: "Previous child session",
|
||||
value: "session.child.previous",
|
||||
keybind: "session_child_cycle_reverse",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
enabled: !!session()?.parentID,
|
||||
onSelect: childSessionHandler((dialog) => {
|
||||
run: childSessionHandler(() => {
|
||||
moveChild(-1)
|
||||
dialog.clear()
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
const sessionCommands = createMemo(() =>
|
||||
sessionCommandList().map((command) => ({
|
||||
namespace: "palette",
|
||||
name: command.value,
|
||||
desc: "description" in command ? command.description : undefined,
|
||||
slashName: "slash" in command ? command.slash?.name : undefined,
|
||||
slashAliases: "slash" in command ? command.slash?.aliases : undefined,
|
||||
...command,
|
||||
})),
|
||||
)
|
||||
|
||||
useBindings(() => ({
|
||||
commands: sessionCommands(),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
enabled: command.matcher,
|
||||
bindings: sections.session,
|
||||
}))
|
||||
|
||||
const revertInfo = createMemo(() => session()?.revert)
|
||||
const revertMessageID = createMemo(() => revertInfo()?.messageID)
|
||||
|
||||
@@ -1082,7 +1066,8 @@ export function Session() {
|
||||
<Switch>
|
||||
<Match when={message.id === revert()?.messageID}>
|
||||
{(function () {
|
||||
const command = useCommandDialog()
|
||||
const command = useCommandPalette()
|
||||
const redoShortcut = useCommandShortcut("session.redo")
|
||||
const [hover, setHover] = createSignal(false)
|
||||
const dialog = useDialog()
|
||||
|
||||
@@ -1093,7 +1078,7 @@ export function Session() {
|
||||
"Are you sure you want to restore the reverted messages?",
|
||||
)
|
||||
if (confirmed) {
|
||||
command.trigger("session.redo")
|
||||
command.run("session.redo")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1116,7 +1101,7 @@ export function Session() {
|
||||
>
|
||||
<text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
|
||||
<span style={{ fg: theme.text }}>{redoShortcut()}</span> or /redo to
|
||||
restore
|
||||
</text>
|
||||
<Show when={revert()!.diffFiles?.length}>
|
||||
@@ -1370,7 +1355,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
return props.message.time.completed - user.time.created
|
||||
})
|
||||
|
||||
const keybind = useKeybind()
|
||||
const childShortcut = useCommandShortcut("session.child.first")
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1392,7 +1377,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
<Show when={props.parts.some((x) => x.type === "tool" && x.tool === "task")}>
|
||||
<box paddingTop={1} paddingLeft={3}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("session_child_first")}
|
||||
{childShortcut()}
|
||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
||||
</text>
|
||||
</box>
|
||||
|
||||
Reference in New Issue
Block a user