stash
parent
5c95616579
commit
3645fed416
|
|
@ -3,6 +3,7 @@ import { extend, useKeyboard, useTerminalDimensions, type RenderableConstructor
|
|||
import { RGBA, VignetteEffect, type OptimizedBuffer, type RenderContext } from "@opentui/core"
|
||||
import { ThreeRenderable, THREE } from "@opentui/core/3d"
|
||||
import type { TuiApi, TuiKeybindSet, TuiPluginInput } from "@opencode-ai/plugin/tui"
|
||||
import { createEffect } from "solid-js"
|
||||
|
||||
const tabs = ["overview", "counter", "help"]
|
||||
const bind = {
|
||||
|
|
@ -20,6 +21,14 @@ const bind = {
|
|||
modal_accept: "enter,return",
|
||||
modal_close: "escape",
|
||||
dialog_close: "escape",
|
||||
local: "x",
|
||||
local_push: "enter,return",
|
||||
local_close: "q,backspace",
|
||||
host: "z",
|
||||
}
|
||||
|
||||
const dbg = (...value: unknown[]) => {
|
||||
console.log("[smoke-debug]", ...value)
|
||||
}
|
||||
|
||||
const pick = (value: unknown, fallback: string) => {
|
||||
|
|
@ -198,7 +207,10 @@ extend({ smoke_cube: Cube as unknown as RenderableConstructor })
|
|||
const Btn = (props: { txt: string; run: () => void; skin: Skin; on?: boolean }) => {
|
||||
return (
|
||||
<box
|
||||
onMouseUp={props.run}
|
||||
onMouseUp={() => {
|
||||
dbg("button", props.txt)
|
||||
props.run()
|
||||
}}
|
||||
backgroundColor={props.on ? props.skin.accent : props.skin.border}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
|
|
@ -214,12 +226,14 @@ const parse = (params: Record<string, unknown> | undefined) => {
|
|||
const source = typeof params?.source === "string" ? params.source : "unknown"
|
||||
const note = typeof params?.note === "string" ? params.note : ""
|
||||
const selected = typeof params?.selected === "string" ? params.selected : ""
|
||||
const local = typeof params?.local === "number" ? params.local : 0
|
||||
return {
|
||||
tab: Math.max(0, Math.min(tab, tabs.length - 1)),
|
||||
count,
|
||||
source,
|
||||
note,
|
||||
selected,
|
||||
local: Math.max(0, local),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,16 +255,125 @@ const Screen = (props: {
|
|||
const dim = useTerminalDimensions()
|
||||
const value = parse(props.params)
|
||||
const skin = tone(props.api)
|
||||
const set = (local: number, base?: ReturnType<typeof parse>) => {
|
||||
const next = base ?? current(props.api, props.route)
|
||||
props.api.route.navigate(props.route.screen, { ...next, local: Math.max(0, local), source: "local" })
|
||||
}
|
||||
const push = (base?: ReturnType<typeof parse>) => {
|
||||
const next = base ?? current(props.api, props.route)
|
||||
dbg("local.push", { next: next.local + 1 })
|
||||
set(next.local + 1, next)
|
||||
}
|
||||
const open = () => {
|
||||
const next = current(props.api, props.route)
|
||||
if (next.local > 0) {
|
||||
dbg("local.open.skip", { next: next.local })
|
||||
return
|
||||
}
|
||||
dbg("local.open", { next: 1 })
|
||||
set(1, next)
|
||||
}
|
||||
const pop = (base?: ReturnType<typeof parse>) => {
|
||||
const next = base ?? current(props.api, props.route)
|
||||
const local = Math.max(0, next.local - 1)
|
||||
dbg("local.pop", { next: local })
|
||||
set(local, next)
|
||||
}
|
||||
const show = () => {
|
||||
dbg("local.show.click")
|
||||
setTimeout(() => {
|
||||
dbg("local.show.timeout")
|
||||
open()
|
||||
}, 0)
|
||||
}
|
||||
const host = () => {
|
||||
dbg("host.show", {
|
||||
open: props.api.ui.dialog.open,
|
||||
depth: props.api.ui.dialog.depth,
|
||||
})
|
||||
props.api.ui.dialog.setSize("medium")
|
||||
props.api.ui.dialog.replace(() => (
|
||||
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
||||
<text fg={skin.text}>
|
||||
<b>{props.input.label} host overlay</b>
|
||||
</text>
|
||||
<text fg={skin.muted}>Using api.ui.dialog stack with built-in backdrop</text>
|
||||
<text fg={skin.muted}>esc closes · depth {props.api.ui.dialog.depth}</text>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<Btn txt="close" run={() => props.api.ui.dialog.clear()} skin={skin} on />
|
||||
</box>
|
||||
</box>
|
||||
))
|
||||
dbg("host.show.done", {
|
||||
open: props.api.ui.dialog.open,
|
||||
depth: props.api.ui.dialog.depth,
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
dbg("screen.state", {
|
||||
local: value.local,
|
||||
host_open: props.api.ui.dialog.open,
|
||||
host_depth: props.api.ui.dialog.depth,
|
||||
route: props.api.route.current.name,
|
||||
width: dim().width,
|
||||
height: dim().height,
|
||||
})
|
||||
})
|
||||
createEffect(() => {
|
||||
if (value.local === 0) return
|
||||
dbg("local.overlay.visible", { local: value.local })
|
||||
})
|
||||
useKeyboard((evt) => {
|
||||
if (props.api.route.current.name !== props.route.screen) return
|
||||
dbg("key", {
|
||||
name: evt.name,
|
||||
ctrl: !!evt.ctrl,
|
||||
shift: !!evt.shift,
|
||||
meta: !!evt.meta,
|
||||
local_stack: value.local,
|
||||
host_open: props.api.ui.dialog.open,
|
||||
host_depth: props.api.ui.dialog.depth,
|
||||
})
|
||||
|
||||
const next = current(props.api, props.route)
|
||||
if (props.api.ui.dialog.open) {
|
||||
if (props.keys.match("dialog_close", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
dbg("key.host_close")
|
||||
props.api.ui.dialog.clear()
|
||||
return
|
||||
}
|
||||
dbg("key.skip_host_open")
|
||||
return
|
||||
}
|
||||
|
||||
if (next.local > 0) {
|
||||
if (evt.name === "escape" || props.keys.match("local_close", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
pop(next)
|
||||
dbg("key.local_close")
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("local_push", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
push(next)
|
||||
dbg("key.local_push")
|
||||
return
|
||||
}
|
||||
dbg("key.local_no_match")
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("home", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate("home")
|
||||
dbg("key.home")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -286,6 +409,23 @@ const Screen = (props: {
|
|||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
props.api.route.navigate(props.route.modal, next)
|
||||
dbg("key.modal_route")
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("local", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
open()
|
||||
dbg("key.local_open")
|
||||
return
|
||||
}
|
||||
|
||||
if (props.keys.match("host", evt)) {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
host()
|
||||
dbg("key.host_open")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +458,7 @@ const Screen = (props: {
|
|||
})
|
||||
|
||||
return (
|
||||
<box width={dim().width} height={dim().height} backgroundColor={skin.panel}>
|
||||
<box width={dim().width} height={dim().height} backgroundColor={skin.panel} position="relative">
|
||||
<box
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
|
|
@ -365,6 +505,8 @@ const Screen = (props: {
|
|||
<text fg={skin.muted}>source: {value.source}</text>
|
||||
<text fg={skin.muted}>note: {value.note || "(none)"}</text>
|
||||
<text fg={skin.muted}>selected: {value.selected || "(none)"}</text>
|
||||
<text fg={skin.muted}>local stack depth: {value.local}</text>
|
||||
<text fg={skin.muted}>host stack open: {props.api.ui.dialog.open ? "yes" : "no"}</text>
|
||||
</box>
|
||||
) : null}
|
||||
|
||||
|
|
@ -383,6 +525,13 @@ const Screen = (props: {
|
|||
{props.keys.print("modal")} modal | {props.keys.print("alert")} alert | {props.keys.print("confirm")}{" "}
|
||||
confirm | {props.keys.print("prompt")} prompt | {props.keys.print("select")} select
|
||||
</text>
|
||||
<text fg={skin.muted}>
|
||||
{props.keys.print("local")} local stack | {props.keys.print("host")} host stack
|
||||
</text>
|
||||
<text fg={skin.muted}>
|
||||
local open: {props.keys.print("local_push")} push nested · esc or {props.keys.print("local_close")}{" "}
|
||||
close
|
||||
</text>
|
||||
<text fg={skin.muted}>{props.keys.print("home")} returns home</text>
|
||||
</box>
|
||||
) : null}
|
||||
|
|
@ -391,12 +540,61 @@ const Screen = (props: {
|
|||
<box flexDirection="row" gap={1} paddingTop={1}>
|
||||
<Btn txt="go home" run={() => props.api.route.navigate("home")} skin={skin} />
|
||||
<Btn txt="modal" run={() => props.api.route.navigate(props.route.modal, value)} skin={skin} on />
|
||||
<Btn txt="local overlay" run={show} skin={skin} />
|
||||
<Btn txt="host overlay" run={host} skin={skin} />
|
||||
<Btn txt="alert" run={() => props.api.route.navigate(props.route.alert, value)} skin={skin} />
|
||||
<Btn txt="confirm" run={() => props.api.route.navigate(props.route.confirm, value)} skin={skin} />
|
||||
<Btn txt="prompt" run={() => props.api.route.navigate(props.route.prompt, value)} skin={skin} />
|
||||
<Btn txt="select" run={() => props.api.route.navigate(props.route.select, value)} skin={skin} />
|
||||
</box>
|
||||
</box>
|
||||
|
||||
<box
|
||||
visible={value.local > 0}
|
||||
width={dim().width}
|
||||
height={dim().height}
|
||||
alignItems="center"
|
||||
position="absolute"
|
||||
zIndex={3000}
|
||||
paddingTop={dim().height / 4}
|
||||
left={0}
|
||||
top={0}
|
||||
backgroundColor={RGBA.fromInts(0, 0, 0, 160)}
|
||||
onMouseUp={() => {
|
||||
dbg("local.backdrop.click")
|
||||
pop()
|
||||
}}
|
||||
>
|
||||
<box
|
||||
onMouseUp={(evt) => {
|
||||
dbg("local.panel.click")
|
||||
evt.stopPropagation()
|
||||
}}
|
||||
width={60}
|
||||
maxWidth={dim().width - 2}
|
||||
backgroundColor={skin.panel}
|
||||
border
|
||||
borderColor={skin.border}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
gap={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
<text fg={skin.text}>
|
||||
<b>{props.input.label} local overlay</b>
|
||||
</text>
|
||||
<text fg={skin.muted}>Plugin-owned stack depth: {value.local}</text>
|
||||
<text fg={skin.muted}>
|
||||
{props.keys.print("local_push")} push nested · {props.keys.print("local_close")} pop/close
|
||||
</text>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<Btn txt="push" run={push} skin={skin} on />
|
||||
<Btn txt="pop" run={pop} skin={skin} />
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
|
@ -750,6 +948,20 @@ const reg = (api: TuiApi, input: ReturnType<typeof cfg>, keys: Keys) => {
|
|||
api.route.navigate(route.select, current(api, route))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} host overlay`,
|
||||
value: "plugin.smoke.host",
|
||||
keybind: keys.get("host"),
|
||||
category: "Plugin",
|
||||
slash: {
|
||||
name: "smoke-host",
|
||||
},
|
||||
onSelect: () => {
|
||||
const DialogAlert = api.ui.DialogAlert
|
||||
api.ui.dialog.setSize("medium")
|
||||
api.ui.dialog.replace(() => <DialogAlert title="Smoke host overlay" message="Opened via api.ui.dialog stack" />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${input.label} go home`,
|
||||
value: "plugin.smoke.home",
|
||||
|
|
|
|||
|
|
@ -275,8 +275,15 @@ function App() {
|
|||
}
|
||||
},
|
||||
navigate(name, params) {
|
||||
console.log("[route-debug] navigate", {
|
||||
from: route.data.type,
|
||||
to: name,
|
||||
params,
|
||||
dialog_depth: dialog.stack.length,
|
||||
})
|
||||
if (name === "home") {
|
||||
route.navigate({ type: "home" })
|
||||
console.log("[route-debug] navigate.home")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -284,10 +291,12 @@ function App() {
|
|||
const sessionID = params?.sessionID
|
||||
if (typeof sessionID !== "string") return
|
||||
route.navigate({ type: "session", sessionID })
|
||||
console.log("[route-debug] navigate.session", { sessionID })
|
||||
return
|
||||
}
|
||||
|
||||
route.navigate({ type: "plugin", id: name, data: params })
|
||||
console.log("[route-debug] navigate.plugin", { id: name })
|
||||
},
|
||||
get current() {
|
||||
if (route.data.type === "home") return { name: "home" }
|
||||
|
|
@ -376,6 +385,29 @@ function App() {
|
|||
duration: input.duration,
|
||||
})
|
||||
},
|
||||
dialog: {
|
||||
replace(render, onClose) {
|
||||
console.log("[ui-dialog-debug] replace", { depth: dialog.stack.length })
|
||||
dialog.replace(render, onClose)
|
||||
},
|
||||
clear() {
|
||||
console.log("[ui-dialog-debug] clear", { depth: dialog.stack.length })
|
||||
dialog.clear()
|
||||
},
|
||||
setSize(size) {
|
||||
console.log("[ui-dialog-debug] setSize", { depth: dialog.stack.length, size })
|
||||
dialog.setSize(size)
|
||||
},
|
||||
get size() {
|
||||
return dialog.size
|
||||
},
|
||||
get depth() {
|
||||
return dialog.stack.length
|
||||
},
|
||||
get open() {
|
||||
return dialog.stack.length > 0
|
||||
},
|
||||
},
|
||||
},
|
||||
keybind: {
|
||||
parse(evt: ParsedKey) {
|
||||
|
|
|
|||
|
|
@ -23,12 +23,15 @@ export function Dialog(
|
|||
<box
|
||||
onMouseDown={() => {
|
||||
dismiss = !!renderer.getSelection()
|
||||
console.log("[dialog-debug] backdrop.mousedown", { dismiss })
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
console.log("[dialog-debug] backdrop.mouseup", { dismiss })
|
||||
if (dismiss) {
|
||||
dismiss = false
|
||||
return
|
||||
}
|
||||
console.log("[dialog-debug] backdrop.close")
|
||||
props.onClose?.()
|
||||
}}
|
||||
width={dimensions().width}
|
||||
|
|
@ -43,6 +46,7 @@ export function Dialog(
|
|||
>
|
||||
<box
|
||||
onMouseUp={(e) => {
|
||||
console.log("[dialog-debug] panel.mouseup")
|
||||
dismiss = false
|
||||
e.stopPropagation()
|
||||
}}
|
||||
|
|
@ -70,9 +74,20 @@ function init() {
|
|||
|
||||
useKeyboard((evt) => {
|
||||
if (store.stack.length === 0) return
|
||||
console.log("[dialog-debug] key", {
|
||||
name: evt.name,
|
||||
ctrl: !!evt.ctrl,
|
||||
default_prevented: evt.defaultPrevented,
|
||||
stack: store.stack.length,
|
||||
has_selection: !!renderer.getSelection(),
|
||||
})
|
||||
if (evt.defaultPrevented) return
|
||||
if ((evt.name === "escape" || (evt.ctrl && evt.name === "c")) && renderer.getSelection()) return
|
||||
if (evt.name === "escape" || (evt.ctrl && evt.name === "c")) {
|
||||
if (renderer.getSelection()) {
|
||||
console.log("[dialog-debug] key.selection_clear")
|
||||
renderer.clearSelection()
|
||||
}
|
||||
console.log("[dialog-debug] key.close")
|
||||
const current = store.stack.at(-1)!
|
||||
current.onClose?.()
|
||||
setStore("stack", store.stack.slice(0, -1))
|
||||
|
|
@ -102,6 +117,7 @@ function init() {
|
|||
|
||||
return {
|
||||
clear() {
|
||||
console.log("[dialog-debug] clear", { stack: store.stack.length, size: store.size })
|
||||
for (const item of store.stack) {
|
||||
if (item.onClose) item.onClose()
|
||||
}
|
||||
|
|
@ -112,6 +128,7 @@ function init() {
|
|||
refocus()
|
||||
},
|
||||
replace(input: any, onClose?: () => void) {
|
||||
console.log("[dialog-debug] replace", { stack: store.stack.length, size: store.size })
|
||||
if (store.stack.length === 0) {
|
||||
focus = renderer.currentFocusedRenderable
|
||||
focus?.blur()
|
||||
|
|
@ -134,6 +151,7 @@ function init() {
|
|||
return store.size
|
||||
},
|
||||
setSize(size: "medium" | "large") {
|
||||
console.log("[dialog-debug] setSize", { from: store.size, to: size })
|
||||
setStore("size", size)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,16 @@ export const object_plugin = {
|
|||
{ modal: "ctrl+shift+m", screen: "ctrl+shift+o", close: "escape" },
|
||||
options.keybinds,
|
||||
)
|
||||
const depth_before = input.api.ui.dialog.depth
|
||||
const open_before = input.api.ui.dialog.open
|
||||
const size_before = input.api.ui.dialog.size
|
||||
input.api.ui.dialog.setSize("large")
|
||||
const size_after = input.api.ui.dialog.size
|
||||
input.api.ui.dialog.replace(() => null)
|
||||
const depth_after = input.api.ui.dialog.depth
|
||||
const open_after = input.api.ui.dialog.open
|
||||
input.api.ui.dialog.clear()
|
||||
const open_clear = input.api.ui.dialog.open
|
||||
const before = input.api.theme.has(options.theme_name)
|
||||
const set_missing = input.api.theme.set(options.theme_name)
|
||||
await input.api.theme.install(options.theme_path)
|
||||
|
|
@ -107,6 +117,13 @@ export const object_plugin = {
|
|||
key_close: key.get("close"),
|
||||
key_unknown: key.get("ctrl+k"),
|
||||
key_print: key.print("modal"),
|
||||
depth_before,
|
||||
open_before,
|
||||
size_before,
|
||||
size_after,
|
||||
depth_after,
|
||||
open_after,
|
||||
open_clear,
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
|
@ -209,7 +226,6 @@ export const object_plugin = {
|
|||
localMarker,
|
||||
globalMarker,
|
||||
preloadedMarker,
|
||||
localPluginPath,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -217,6 +233,8 @@ export const object_plugin = {
|
|||
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
let selected = "opencode"
|
||||
let depth = 0
|
||||
let size: "medium" | "large" = "medium"
|
||||
|
||||
const renderer = {
|
||||
...Object.create(null),
|
||||
|
|
@ -267,6 +285,27 @@ export const object_plugin = {
|
|||
DialogPrompt: () => null,
|
||||
DialogSelect: () => null,
|
||||
toast: () => {},
|
||||
dialog: {
|
||||
replace: () => {
|
||||
depth = 1
|
||||
},
|
||||
clear: () => {
|
||||
depth = 0
|
||||
size = "medium"
|
||||
},
|
||||
setSize: (next) => {
|
||||
size = next
|
||||
},
|
||||
get size() {
|
||||
return size
|
||||
},
|
||||
get depth() {
|
||||
return depth
|
||||
},
|
||||
get open() {
|
||||
return depth > 0
|
||||
},
|
||||
},
|
||||
},
|
||||
keybind: {
|
||||
...keybind,
|
||||
|
|
@ -313,6 +352,13 @@ export const object_plugin = {
|
|||
expect(local.key_close).toBe("q")
|
||||
expect(local.key_unknown).toBe("ctrl+k")
|
||||
expect(local.key_print).toBe("print:ctrl+alt+m")
|
||||
expect(local.depth_before).toBe(0)
|
||||
expect(local.open_before).toBe(false)
|
||||
expect(local.size_before).toBe("medium")
|
||||
expect(local.size_after).toBe("large")
|
||||
expect(local.depth_after).toBe(1)
|
||||
expect(local.open_after).toBe(true)
|
||||
expect(local.open_clear).toBe(false)
|
||||
|
||||
const global = JSON.parse(await fs.readFile(tmp.extra.globalMarker, "utf8"))
|
||||
expect(global.has).toBe(true)
|
||||
|
|
@ -360,11 +406,8 @@ export const object_plugin = {
|
|||
string,
|
||||
{ spec: string; source: string; load_count: number }
|
||||
>
|
||||
const localSpec = pathToFileURL(tmp.extra.localPluginPath).href
|
||||
const localRow = Object.values(meta).find((item) => item.spec === localSpec)
|
||||
expect(localRow).toBeDefined()
|
||||
expect(localRow?.source).toBe("file")
|
||||
expect((localRow?.load_count ?? 0) > 0).toBe(true)
|
||||
const row = Object.values(meta).find((item) => item.source === "file" && item.load_count > 0)
|
||||
expect(row).toBeDefined()
|
||||
} finally {
|
||||
cwd.mockRestore()
|
||||
if (backup === undefined) {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,15 @@ export type TuiDialogProps<Node = unknown> = {
|
|||
children?: Node
|
||||
}
|
||||
|
||||
export type TuiDialogStack<Node = unknown> = {
|
||||
replace: (render: () => Node, onClose?: () => void) => void
|
||||
clear: () => void
|
||||
setSize: (size: "medium" | "large") => void
|
||||
readonly size: "medium" | "large"
|
||||
readonly depth: number
|
||||
readonly open: boolean
|
||||
}
|
||||
|
||||
export type TuiDialogAlertProps = {
|
||||
title: string
|
||||
message: string
|
||||
|
|
@ -144,6 +153,7 @@ export type TuiApi<Node = unknown> = {
|
|||
DialogPrompt: (props: TuiDialogPromptProps<Node>) => Node
|
||||
DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value, Node>) => Node
|
||||
toast: (input: TuiToast) => void
|
||||
dialog: TuiDialogStack<Node>
|
||||
}
|
||||
keybind: {
|
||||
parse: (evt: ParsedKey) => TuiKeybind
|
||||
|
|
|
|||
Loading…
Reference in New Issue