feat: add server link sharing
This commit is contained in:
parent
ec8eea7cbe
commit
7698a5e6ac
25 changed files with 1586 additions and 1194 deletions
|
|
@ -45,6 +45,7 @@ import { useConnected } from "./component/use-connected"
|
|||
import { DialogMcp } from "./component/dialog-mcp"
|
||||
import { DialogStatus } from "./component/dialog-status"
|
||||
import { DialogDebug } from "./component/dialog-debug"
|
||||
import { DialogLink, type DialogLinkCredentials } from "./component/dialog-link"
|
||||
import { DialogThemeList } from "./component/dialog-theme-list"
|
||||
import { DialogHelp } from "./ui/dialog-help"
|
||||
import { DialogAgent } from "./component/dialog-agent"
|
||||
|
|
@ -121,6 +122,7 @@ const appBindingCommands = [
|
|||
"provider.connect",
|
||||
"console.org.switch",
|
||||
"opencode.status",
|
||||
"server.link",
|
||||
"opencode.debug",
|
||||
"theme.switch",
|
||||
"theme.switch_mode",
|
||||
|
|
@ -146,6 +148,7 @@ export type TuiInput = {
|
|||
api: OpenCodeClient
|
||||
discover?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>
|
||||
reload?: () => Promise<void>
|
||||
link?: DialogLinkCredentials
|
||||
args: Args
|
||||
config: TuiConfig.Resolved
|
||||
onSnapshot?: () => Promise<string[]>
|
||||
|
|
@ -335,6 +338,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||
<App
|
||||
onSnapshot={input.onSnapshot}
|
||||
pluginHost={input.pluginHost}
|
||||
link={input.link}
|
||||
/>
|
||||
</LocationProvider>
|
||||
</EditorContextProvider>
|
||||
|
|
@ -380,7 +384,11 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||
})
|
||||
})
|
||||
|
||||
function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPluginHost }) {
|
||||
function App(props: {
|
||||
onSnapshot?: () => Promise<string[]>
|
||||
pluginHost: TuiPluginHost
|
||||
link?: DialogLinkCredentials
|
||||
}) {
|
||||
const log = useLog({ component: "app" })
|
||||
const startup = useTuiStartup()
|
||||
const tuiConfig = useTuiConfig()
|
||||
|
|
@ -813,6 +821,15 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
|||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
name: "server.link",
|
||||
title: "Show server connection information",
|
||||
slashName: "link",
|
||||
run: () => {
|
||||
dialog.replace(() => <DialogLink credentials={props.link} />)
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
...(sdk.reload
|
||||
? [
|
||||
{
|
||||
|
|
|
|||
126
packages/tui/src/component/dialog-link.tsx
Normal file
126
packages/tui/src/component/dialog-link.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { TextAttributes } from "@opentui/core"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { createMemo, createResource, createSignal, For, Show } from "solid-js"
|
||||
import { renderUnicodeCompact } from "uqr"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { errorMessage } from "../util/error"
|
||||
|
||||
export type DialogLinkCredentials = {
|
||||
readonly username: string
|
||||
readonly password: string
|
||||
}
|
||||
|
||||
export function DialogLink(props: { credentials?: DialogLinkCredentials }) {
|
||||
const sdk = useSDK()
|
||||
const dialog = useDialog()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme } = useTheme()
|
||||
const [loadError, setLoadError] = createSignal<unknown>()
|
||||
const [showPassword, setShowPassword] = createSignal(false)
|
||||
const [passwordHover, setPasswordHover] = createSignal(false)
|
||||
|
||||
dialog.setSize("large")
|
||||
dialog.setCentered(true)
|
||||
|
||||
const [server] = createResource(() =>
|
||||
sdk.client.v2.server
|
||||
.get({ throwOnError: true })
|
||||
.then((result) => result.data)
|
||||
.catch((error) => {
|
||||
setLoadError(error)
|
||||
return undefined
|
||||
}),
|
||||
)
|
||||
const info = createMemo(() => {
|
||||
const current = server()
|
||||
if (!current) return
|
||||
return {
|
||||
urls: current.urls,
|
||||
username: props.credentials?.username ?? "opencode",
|
||||
password: props.credentials?.password ?? "",
|
||||
}
|
||||
})
|
||||
const horizontal = createMemo(() => dimensions().width >= 96)
|
||||
const content = () => {
|
||||
const value = info()
|
||||
if (!value) return
|
||||
return (
|
||||
<box
|
||||
flexDirection={horizontal() ? "row" : "column"}
|
||||
alignItems={horizontal() ? "flex-start" : "center"}
|
||||
gap={2}
|
||||
>
|
||||
<box width={horizontal() ? 29 : "100%"} flexShrink={0} gap={1}>
|
||||
<box>
|
||||
<text fg={theme.textMuted}>URLs</text>
|
||||
<For each={value.urls}>{(url) => <text fg={theme.text}>{url}</text>}</For>
|
||||
</box>
|
||||
<box>
|
||||
<text fg={theme.textMuted}>Username</text>
|
||||
<text fg={theme.text}>{value.username}</text>
|
||||
</box>
|
||||
<box>
|
||||
<text fg={theme.textMuted}>Password</text>
|
||||
<text
|
||||
fg={passwordHover() ? theme.text : theme.textMuted}
|
||||
wrapMode="word"
|
||||
onMouseOver={() => setPasswordHover(true)}
|
||||
onMouseOut={() => setPasswordHover(false)}
|
||||
onMouseUp={() => setShowPassword((current) => !current)}
|
||||
>
|
||||
{showPassword() ? value.password : "************"}
|
||||
</text>
|
||||
</box>
|
||||
<Show
|
||||
when={value.urls.some((url) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname))}
|
||||
>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
Run `opencode service set hostname 0.0.0.0` to access the service remotely.
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
<box
|
||||
width={horizontal() ? undefined : "100%"}
|
||||
flexGrow={horizontal() ? 1 : 0}
|
||||
flexShrink={0}
|
||||
alignItems={horizontal() ? "flex-end" : "center"}
|
||||
>
|
||||
<text fg={theme.text}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
Link
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<Show when={loadError()}>
|
||||
{(error) => <text fg={theme.error}>{errorMessage(error())}</text>}
|
||||
</Show>
|
||||
<Show when={info()} fallback={<text fg={theme.textMuted}>Loading server information...</text>}>
|
||||
<Show
|
||||
when={dimensions().height >= 36}
|
||||
fallback={
|
||||
<scrollbox
|
||||
height={Math.max(8, dimensions().height - Math.floor(dimensions().height / 4) - 6)}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
>
|
||||
{content()}
|
||||
</scrollbox>
|
||||
}
|
||||
>
|
||||
{content()}
|
||||
</Show>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import { useClipboard } from "../context/clipboard"
|
|||
export function Dialog(
|
||||
props: ParentProps<{
|
||||
size?: "medium" | "large" | "xlarge"
|
||||
centered?: boolean
|
||||
onClose: () => void
|
||||
}>,
|
||||
) {
|
||||
|
|
@ -40,9 +41,10 @@ export function Dialog(
|
|||
width={dimensions().width}
|
||||
height={dimensions().height}
|
||||
alignItems="center"
|
||||
justifyContent={props.centered ? "center" : undefined}
|
||||
position="absolute"
|
||||
zIndex={3000}
|
||||
paddingTop={dimensions().height / 4}
|
||||
paddingTop={props.centered ? 0 : dimensions().height / 4}
|
||||
left={0}
|
||||
top={0}
|
||||
backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
|
||||
|
|
@ -73,6 +75,7 @@ function init() {
|
|||
onClose?: () => void
|
||||
}[],
|
||||
size: "medium" as "medium" | "large" | "xlarge",
|
||||
centered: false,
|
||||
})
|
||||
|
||||
const renderer = useRenderer()
|
||||
|
|
@ -143,6 +146,7 @@ function init() {
|
|||
}
|
||||
batch(() => {
|
||||
setStore("size", "medium")
|
||||
setStore("centered", false)
|
||||
setStore("stack", [])
|
||||
})
|
||||
refocus()
|
||||
|
|
@ -156,6 +160,7 @@ function init() {
|
|||
if (item.onClose) item.onClose()
|
||||
}
|
||||
setStore("size", "medium")
|
||||
setStore("centered", false)
|
||||
setStore("stack", [
|
||||
{
|
||||
element: input,
|
||||
|
|
@ -169,9 +174,15 @@ function init() {
|
|||
get size() {
|
||||
return store.size
|
||||
},
|
||||
get centered() {
|
||||
return store.centered
|
||||
},
|
||||
setSize(size: "medium" | "large" | "xlarge") {
|
||||
setStore("size", size)
|
||||
},
|
||||
setCentered(centered: boolean) {
|
||||
setStore("centered", centered)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +224,7 @@ export function DialogProvider(props: ParentProps) {
|
|||
onMouseUp={!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ? copySelection : undefined}
|
||||
>
|
||||
<Show when={value.stack.length}>
|
||||
<Dialog onClose={() => value.clear()} size={value.size}>
|
||||
<Dialog onClose={() => value.clear()} size={value.size} centered={value.centered}>
|
||||
{value.stack.at(-1)!.element}
|
||||
</Dialog>
|
||||
</Show>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue