From b45ac53de7ac55fe092d9c7497ce9f6d73413d7d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 22 Sep 2025 23:55:35 -0400 Subject: [PATCH] command bar --- .../cli/cmd/tui/component/dialog-command.tsx | 112 +++++++++--------- .../src/cli/cmd/tui/component/prompt.tsx | 4 + packages/opencode/src/cli/cmd/tui/session.tsx | 33 ++++++ packages/opencode/src/cli/cmd/tui/tui.tsx | 52 ++++++-- .../src/cli/cmd/tui/ui/dialog-select.tsx | 14 ++- 5 files changed, 149 insertions(+), 66 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx index 85f88b45e..196a550ca 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx @@ -1,61 +1,65 @@ import { useDialog } from "../ui/dialog" import { DialogModel } from "./dialog-model" -import { DialogSelect } from "../ui/dialog-select" +import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select" import { useRoute } from "../context/route" import { DialogSessionList } from "./dialog-session-list" import { DialogAgent } from "./dialog-agent" +import { + createContext, + createMemo, + createSignal, + onCleanup, + useContext, + type Accessor, + type ParentProps, +} from "solid-js" +import { useKeyboard } from "@opentui/solid" -export function DialogCommand() { - const dialog = useDialog() - const route = useRoute() - return ( - { - dialog.replace(() => ) - }, - }, - { - title: "Switch agent", - value: "switch-agent", - category: "Agent", - onSelect: () => { - dialog.replace(() => ) - }, - }, - { - title: "Switch session", - value: "switch-session", - category: "Session", - onSelect: () => { - dialog.replace(() => ) - }, - }, - { - title: "New session", - value: "new-session", - category: "Session", - onSelect: () => { - route.navigate({ - type: "home", - }) - dialog.clear() - }, - }, - { - title: "Share session", - value: "share-session", - category: "Session", - onSelect: () => { - console.log("share session") - }, - }, - ]} - /> - ) +type Context = ReturnType +const ctx = createContext() + +function init() { + const [registrations, setRegistrations] = createSignal[]>([]) + const options = createMemo(() => { + return registrations().flatMap((x) => x()) + }) + + return { + register(cb: () => DialogSelectOption[]) { + const results = createMemo(cb) + setRegistrations((x) => [...x, results]) + onCleanup(() => { + setRegistrations((x) => x.filter((x) => x !== results)) + }) + }, + get options() { + return options() + }, + } +} + +export function useCommandDialog() { + const value = useContext(ctx) + if (!value) { + throw new Error("useCommandDialog must be used within a CommandProvider") + } + return value +} + +export function CommandProvider(props: ParentProps) { + const value = init() + const dialog = useDialog() + + useKeyboard((evt) => { + if (evt.name === "k" && evt.ctrl) { + dialog.replace(() => ) + return + } + }) + + return {props.children} +} + +function DialogCommand(props: { options: DialogSelectOption[] }) { + return } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt.tsx index e41c0788a..6f41df2ad 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt.tsx @@ -34,6 +34,7 @@ export function Prompt(props: PromptProps) { const sdk = useSDK() const route = useRoute() const sync = useSync() + const session = createMemo(() => (props.sessionID ? sync.session.get(props.sessionID) : undefined)) const [store, setStore] = createStore({ input: "", @@ -206,6 +207,9 @@ export function Prompt(props: PromptProps) { {local.model.parsed().model} + + compacting... + diff --git a/packages/opencode/src/cli/cmd/tui/session.tsx b/packages/opencode/src/cli/cmd/tui/session.tsx index 6ea7f2003..ca61243f2 100644 --- a/packages/opencode/src/cli/cmd/tui/session.tsx +++ b/packages/opencode/src/cli/cmd/tui/session.tsx @@ -25,10 +25,12 @@ import type { WebFetchTool } from "../../../tool/webfetch" import type { TaskTool } from "../../../tool/task" import { useKeyboard, type BoxProps, type JSX } from "@opentui/solid" import { useSDK } from "./context/sdk" +import { useCommandDialog } from "./component/dialog-command" export function Session() { const route = useRouteData("session") const sync = useSync() + const command = useCommandDialog() const session = createMemo(() => sync.session.get(route.sessionID)!) const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const todo = createMemo(() => sync.data.todo[route.sessionID] ?? []) @@ -48,6 +50,37 @@ export function Session() { }) }) + command.register(() => [ + { + title: "Share session", + value: "session.share", + disabled: !session().share?.url, + category: "Session", + onSelect: (ctx) => { + sdk.session.share({ + path: { + id: route.sessionID, + }, + }) + ctx.clear() + }, + }, + { + title: "Unshare session", + value: "session.unshare", + disabled: !!session().share?.url, + category: "Session", + onSelect: (ctx) => { + sdk.session.unshare({ + path: { + id: route.sessionID, + }, + }) + ctx.clear() + }, + }, + ]) + return ( diff --git a/packages/opencode/src/cli/cmd/tui/tui.tsx b/packages/opencode/src/cli/cmd/tui/tui.tsx index 10655f3b8..054422fe7 100644 --- a/packages/opencode/src/cli/cmd/tui/tui.tsx +++ b/packages/opencode/src/cli/cmd/tui/tui.tsx @@ -12,10 +12,12 @@ import { SDKProvider } from "./context/sdk" import { SyncProvider } from "./context/sync" import { LocalProvider, useLocal } from "./context/local" import { DialogModel } from "./component/dialog-model" -import { DialogCommand } from "./component/dialog-command" import { Session } from "./session" import { Instance } from "../../../project/instance" import { EventLoop } from "../../../util/eventloop" +import { CommandProvider, useCommandDialog } from "./component/dialog-command" +import { DialogAgent } from "./component/dialog-agent" +import { DialogSessionList } from "./component/dialog-session-list" export const TuiCommand = cmd({ command: "$0 [project]", @@ -79,7 +81,9 @@ export const TuiCommand = cmd({ - + + + @@ -103,6 +107,7 @@ function App() { const renderer = useRenderer() const dialog = useDialog() const local = useLocal() + const command = useCommandDialog() useKeyboard(async (evt) => { if (evt.name === "tab") { @@ -110,11 +115,6 @@ function App() { return } - if (evt.ctrl && evt.name === "k") { - dialog.replace(() => ) - return - } - if (evt.meta && evt.name === "t") { renderer.toggleDebugOverlay() return @@ -134,6 +134,44 @@ function App() { console.log(JSON.stringify(route.data)) }) + command.register(() => [ + { + title: "Switch session", + value: "switch-session", + category: "Session", + onSelect: () => { + dialog.replace(() => ) + }, + }, + { + title: "New session", + value: "new-session", + category: "Session", + onSelect: () => { + route.navigate({ + type: "home", + }) + dialog.clear() + }, + }, + { + title: "Switch model", + value: "switch-model", + category: "Agent", + onSelect: () => { + dialog.replace(() => ) + }, + }, + { + title: "Switch agent", + value: "switch-agent", + category: "Agent", + onSelect: () => { + dialog.replace(() => ) + }, + }, + ]) + return ( diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index bf4b5381b..7cc76f9af 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -1,11 +1,12 @@ import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core" import { Theme } from "../context/theme" -import { entries, flatMap, groupBy, pipe } from "remeda" +import { entries, filter, flatMap, groupBy, pipe } from "remeda" import { batch, createEffect, createMemo, For, Show } from "solid-js" import { createStore } from "solid-js/store" import { useKeyboard } from "@opentui/solid" import * as fuzzysort from "fuzzysort" import { isDeepEqual } from "remeda" +import { useDialog, type DialogContext } from "./dialog" export interface DialogSelectProps { title: string @@ -15,15 +16,17 @@ export interface DialogSelectProps { current?: T } -export interface DialogSelectOption { - value: T +export interface DialogSelectOption { title: string + value: T description?: string category?: string - onSelect?: () => void + disabled?: boolean + onSelect?: (ctx: DialogContext) => void } export function DialogSelect(props: DialogSelectProps) { + const dialog = useDialog() const [store, setStore] = createStore({ selected: 0, filter: "", @@ -35,6 +38,7 @@ export function DialogSelect(props: DialogSelectProps) { const needle = store.filter.toLowerCase() const result = pipe( props.options, + filter((x) => x.disabled !== false), (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)), groupBy((x) => x.category ?? ""), // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))), @@ -84,7 +88,7 @@ export function DialogSelect(props: DialogSelectProps) { if (evt.name === "down") move(1) if (evt.name === "return") { const option = selected() - if (option.onSelect) option.onSelect() + if (option.onSelect) option.onSelect(dialog) props.onSelect?.(option) } })