mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
command bar
This commit is contained in:
parent
9d54cdac08
commit
b45ac53de7
5 changed files with 149 additions and 66 deletions
|
|
@ -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 (
|
||||
<DialogSelect
|
||||
title="Commands"
|
||||
options={[
|
||||
{
|
||||
title: "Switch model",
|
||||
value: "switch-model",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogModel />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Switch agent",
|
||||
value: "switch-agent",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogAgent />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Switch session",
|
||||
value: "switch-session",
|
||||
category: "Session",
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogSessionList />)
|
||||
},
|
||||
},
|
||||
{
|
||||
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<typeof init>
|
||||
const ctx = createContext<Context>()
|
||||
|
||||
function init() {
|
||||
const [registrations, setRegistrations] = createSignal<Accessor<DialogSelectOption[]>[]>([])
|
||||
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(() => <DialogCommand options={value.options} />)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
|
||||
}
|
||||
|
||||
function DialogCommand(props: { options: DialogSelectOption[] }) {
|
||||
return <DialogSelect title="Commands" options={props.options} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Prompt>({
|
||||
input: "",
|
||||
|
|
@ -206,6 +207,9 @@ export function Prompt(props: PromptProps) {
|
|||
<span style={{ bold: true }}>{local.model.parsed().model}</span>
|
||||
</text>
|
||||
<Switch>
|
||||
<Match when={session()?.time.compacting}>
|
||||
<text fg={Theme.textMuted}>compacting...</text>
|
||||
</Match>
|
||||
<Match when={working()}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexGrow={1}>
|
||||
<Show when={session()}>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<SyncProvider>
|
||||
<LocalProvider>
|
||||
<DialogProvider>
|
||||
<App />
|
||||
<CommandProvider>
|
||||
<App />
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</LocalProvider>
|
||||
</SyncProvider>
|
||||
|
|
@ -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(() => <DialogCommand />)
|
||||
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(() => <DialogSessionList />)
|
||||
},
|
||||
},
|
||||
{
|
||||
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(() => <DialogModel />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Switch agent",
|
||||
value: "switch-agent",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogAgent />)
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
return (
|
||||
<box width={dimensions().width} height={dimensions().height} backgroundColor={Theme.background}>
|
||||
<box flexDirection="column" flexGrow={1}>
|
||||
|
|
|
|||
|
|
@ -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<T> {
|
||||
title: string
|
||||
|
|
@ -15,15 +16,17 @@ export interface DialogSelectProps<T> {
|
|||
current?: T
|
||||
}
|
||||
|
||||
export interface DialogSelectOption<T> {
|
||||
value: T
|
||||
export interface DialogSelectOption<T = any> {
|
||||
title: string
|
||||
value: T
|
||||
description?: string
|
||||
category?: string
|
||||
onSelect?: () => void
|
||||
disabled?: boolean
|
||||
onSelect?: (ctx: DialogContext) => void
|
||||
}
|
||||
|
||||
export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
const dialog = useDialog()
|
||||
const [store, setStore] = createStore({
|
||||
selected: 0,
|
||||
filter: "",
|
||||
|
|
@ -35,6 +38,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
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<T>(props: DialogSelectProps<T>) {
|
|||
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)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue