mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
Tui onboarding (#4569)
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763464769,
|
||||
"narHash": "sha256-AJHrsT7VoeQzErpBRlLJM1SODcaayp0joAoEA35yiwM=",
|
||||
"lastModified": 1763618868,
|
||||
"narHash": "sha256-v5afmLjn/uyD9EQuPBn7nZuaZVV9r+JerayK/4wvdWA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6f374686605df381de8541c072038472a5ea2e2d",
|
||||
"rev": "a8d610af3f1a5fb71e23e08434d8d61a466fc942",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -2,10 +2,11 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch } from "solid-js"
|
||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show } from "solid-js"
|
||||
import { Installation } from "@/installation"
|
||||
import { Global } from "@/global"
|
||||
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
||||
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||
import { SyncProvider, useSync } from "@tui/context/sync"
|
||||
import { LocalProvider, useLocal } from "@tui/context/local"
|
||||
@@ -293,6 +294,14 @@ function App() {
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Connect provider",
|
||||
value: "provider.connect",
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogProviderList />)
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
|
||||
value: "theme.switch_mode",
|
||||
@@ -451,16 +460,18 @@ function App() {
|
||||
<text fg={theme.textMuted}>{process.cwd().replace(Global.Path.home, "~")}</text>
|
||||
</box>
|
||||
</box>
|
||||
<box flexDirection="row" flexShrink={0}>
|
||||
<text fg={theme.textMuted} paddingRight={1}>
|
||||
tab
|
||||
</text>
|
||||
<text fg={local.agent.color(local.agent.current().name)}>{""}</text>
|
||||
<text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
|
||||
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
||||
<span> AGENT </span>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={false}>
|
||||
<box flexDirection="row" flexShrink={0}>
|
||||
<text fg={theme.textMuted} paddingRight={1}>
|
||||
tab
|
||||
</text>
|
||||
<text fg={local.agent.color(local.agent.current().name)}>{""}</text>
|
||||
<text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
|
||||
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
||||
<span> AGENT </span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
export const EmptyBorder = {
|
||||
topLeft: "",
|
||||
bottomLeft: "",
|
||||
vertical: "",
|
||||
topRight: "",
|
||||
bottomRight: "",
|
||||
horizontal: " ",
|
||||
bottomT: "",
|
||||
topT: "",
|
||||
cross: "",
|
||||
leftT: "",
|
||||
rightT: "",
|
||||
}
|
||||
|
||||
export const SplitBorder = {
|
||||
border: ["left" as const, "right" as const],
|
||||
customBorderChars: {
|
||||
topLeft: "",
|
||||
bottomLeft: "",
|
||||
...EmptyBorder,
|
||||
vertical: "┃",
|
||||
topRight: "",
|
||||
bottomRight: "",
|
||||
horizontal: "",
|
||||
bottomT: "",
|
||||
topT: "",
|
||||
cross: "",
|
||||
leftT: "",
|
||||
rightT: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,10 +5,20 @@ import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy } from "remeda
|
||||
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
||||
|
||||
function Free() {
|
||||
const { theme } = useTheme()
|
||||
return <span style={{ fg: theme.secondary }}>Free</span>
|
||||
return <span style={{ fg: theme.text }}>Free</span>
|
||||
}
|
||||
const PROVIDER_PRIORITY: Record<string, number> = {
|
||||
opencode: 0,
|
||||
anthropic: 1,
|
||||
"github-copilot": 2,
|
||||
openai: 3,
|
||||
google: 4,
|
||||
openrouter: 5,
|
||||
vercel: 6,
|
||||
}
|
||||
|
||||
export function DialogModel() {
|
||||
@@ -17,9 +27,16 @@ export function DialogModel() {
|
||||
const dialog = useDialog()
|
||||
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
|
||||
|
||||
const connected = createMemo(() =>
|
||||
sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
|
||||
)
|
||||
|
||||
const showRecent = createMemo(() => !ref()?.filter && local.model.recent().length > 0 && connected())
|
||||
const providers = createDialogProviderOptions()
|
||||
|
||||
const options = createMemo(() => {
|
||||
return [
|
||||
...(!ref()?.filter
|
||||
...(showRecent()
|
||||
? local.model.recent().flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)!
|
||||
if (!provider) return []
|
||||
@@ -35,7 +52,17 @@ export function DialogModel() {
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Recent",
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -56,28 +83,56 @@ export function DialogModel() {
|
||||
modelID: model,
|
||||
},
|
||||
title: info.name ?? model,
|
||||
description: provider.name,
|
||||
category: provider.name,
|
||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
|
||||
description: connected() ? provider.name : undefined,
|
||||
category: connected() ? provider.name : undefined,
|
||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect() {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
})),
|
||||
filter((x) => Boolean(ref()?.filter) || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
|
||||
filter((x) => !showRecent() || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
|
||||
sortBy((x) => x.title),
|
||||
),
|
||||
),
|
||||
),
|
||||
...(!connected()
|
||||
? pipe(
|
||||
providers(),
|
||||
map((option) => {
|
||||
return {
|
||||
...option,
|
||||
category: "Popular providers",
|
||||
}
|
||||
}),
|
||||
filter((x) => PROVIDER_PRIORITY[x.value] !== undefined),
|
||||
sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99),
|
||||
)
|
||||
: []),
|
||||
]
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
keybind={[
|
||||
{
|
||||
keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
|
||||
title: connected() ? "Connect provider" : "More providers",
|
||||
onTrigger() {
|
||||
dialog.replace(() => <DialogProvider />)
|
||||
},
|
||||
},
|
||||
]}
|
||||
ref={setRef}
|
||||
title="Select model"
|
||||
current={local.model.current()}
|
||||
options={options()}
|
||||
onSelect={(option) => {
|
||||
dialog.clear()
|
||||
local.model.set(option.value, { recent: true })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
223
packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
Normal file
223
packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import { createMemo, createSignal, onMount, Show } from "solid-js"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { map, pipe, sortBy } from "remeda"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { DialogPrompt } from "../ui/dialog-prompt"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk"
|
||||
import { DialogModel } from "./dialog-model"
|
||||
|
||||
const PROVIDER_PRIORITY: Record<string, number> = {
|
||||
opencode: 0,
|
||||
anthropic: 1,
|
||||
"github-copilot": 2,
|
||||
openai: 3,
|
||||
google: 4,
|
||||
openrouter: 5,
|
||||
vercel: 6,
|
||||
}
|
||||
|
||||
export function createDialogProviderOptions() {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const options = createMemo(() => {
|
||||
return pipe(
|
||||
sync.data.provider_next.all,
|
||||
map((provider) => ({
|
||||
title: provider.name,
|
||||
value: provider.id,
|
||||
footer: {
|
||||
opencode: "Recommended",
|
||||
anthropic: "Claude Max or API key",
|
||||
}[provider.id],
|
||||
async onSelect() {
|
||||
const methods = sync.data.provider_auth[provider.id] ?? [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
},
|
||||
]
|
||||
let index: number | null = 0
|
||||
if (methods.length > 1) {
|
||||
index = await new Promise<number | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogSelect
|
||||
title="Select auth method"
|
||||
options={methods.map((x, index) => ({
|
||||
title: x.label,
|
||||
value: index,
|
||||
}))}
|
||||
onSelect={(option) => resolve(option.value)}
|
||||
/>
|
||||
),
|
||||
() => resolve(null),
|
||||
)
|
||||
})
|
||||
}
|
||||
if (index == null) return
|
||||
const method = methods[index]
|
||||
if (method.type === "oauth") {
|
||||
const result = await sdk.client.provider.oauth.authorize({
|
||||
path: {
|
||||
id: provider.id,
|
||||
},
|
||||
body: {
|
||||
method: index,
|
||||
},
|
||||
})
|
||||
if (result.data?.method === "code") {
|
||||
dialog.replace(() => (
|
||||
<CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
||||
))
|
||||
}
|
||||
if (result.data?.method === "auto") {
|
||||
dialog.replace(() => (
|
||||
<AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
||||
))
|
||||
}
|
||||
}
|
||||
if (method.type === "api") {
|
||||
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
||||
}
|
||||
},
|
||||
})),
|
||||
sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99),
|
||||
)
|
||||
})
|
||||
return options
|
||||
}
|
||||
|
||||
export function DialogProvider() {
|
||||
const options = createDialogProviderOptions()
|
||||
return <DialogSelect title="Connect a provider" options={options()} />
|
||||
}
|
||||
|
||||
interface AutoMethodProps {
|
||||
index: number
|
||||
providerID: string
|
||||
title: string
|
||||
authorization: ProviderAuthAuthorization
|
||||
}
|
||||
function AutoMethod(props: AutoMethodProps) {
|
||||
const { theme } = useTheme()
|
||||
const sdk = useSDK()
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
|
||||
onMount(async () => {
|
||||
const result = await sdk.client.provider.oauth.callback({
|
||||
path: {
|
||||
id: props.providerID,
|
||||
},
|
||||
body: {
|
||||
method: props.index,
|
||||
},
|
||||
})
|
||||
if (result.error) {
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
await sdk.client.instance.dispose()
|
||||
await sync.bootstrap()
|
||||
dialog.replace(() => <DialogModel />)
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<box gap={1}>
|
||||
<text fg={theme.primary}>{props.authorization.url}</text>
|
||||
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted}>Waiting for authorization...</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
interface CodeMethodProps {
|
||||
index: number
|
||||
title: string
|
||||
providerID: string
|
||||
authorization: ProviderAuthAuthorization
|
||||
}
|
||||
function CodeMethod(props: CodeMethodProps) {
|
||||
const { theme } = useTheme()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const [error, setError] = createSignal(false)
|
||||
|
||||
return (
|
||||
<DialogPrompt
|
||||
title={props.title}
|
||||
placeholder="Authorization code"
|
||||
onConfirm={async (value) => {
|
||||
const { error } = await sdk.client.provider.oauth.callback({
|
||||
path: {
|
||||
id: props.providerID,
|
||||
},
|
||||
body: {
|
||||
method: props.index,
|
||||
code: value,
|
||||
},
|
||||
})
|
||||
if (!error) {
|
||||
await sdk.client.instance.dispose()
|
||||
await sync.bootstrap()
|
||||
dialog.replace(() => <DialogModel />)
|
||||
return
|
||||
}
|
||||
setError(true)
|
||||
}}
|
||||
description={() => (
|
||||
<box gap={1}>
|
||||
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||
<text fg={theme.primary}>{props.authorization.url}</text>
|
||||
<Show when={error()}>
|
||||
<text fg={theme.error}>Invalid code</text>
|
||||
</Show>
|
||||
</box>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface ApiMethodProps {
|
||||
providerID: string
|
||||
title: string
|
||||
}
|
||||
function ApiMethod(props: ApiMethodProps) {
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
|
||||
return (
|
||||
<DialogPrompt
|
||||
title={props.title}
|
||||
placeholder="API key"
|
||||
onConfirm={async (value) => {
|
||||
if (!value) return
|
||||
sdk.client.auth.set({
|
||||
path: {
|
||||
id: props.providerID,
|
||||
},
|
||||
body: {
|
||||
type: "api",
|
||||
key: value,
|
||||
},
|
||||
})
|
||||
await sdk.client.instance.dispose()
|
||||
await sync.bootstrap()
|
||||
dialog.replace(() => <DialogModel />)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,8 @@
|
||||
import {
|
||||
TextAttributes,
|
||||
BoxRenderable,
|
||||
TextareaRenderable,
|
||||
MouseEvent,
|
||||
PasteEvent,
|
||||
t,
|
||||
dim,
|
||||
fg,
|
||||
type KeyBinding,
|
||||
} from "@opentui/core"
|
||||
import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js"
|
||||
import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg, type KeyBinding } from "@opentui/core"
|
||||
import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { SplitBorder } from "@tui/component/border"
|
||||
import { EmptyBorder } from "@tui/component/border"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
@@ -29,6 +19,8 @@ import { Clipboard } from "../../util/clipboard"
|
||||
import type { FilePart } from "@opencode-ai/sdk"
|
||||
import { TuiEvent } from "../../event"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { Shimmer } from "../../ui/shimmer"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
@@ -57,7 +49,7 @@ export function Prompt(props: PromptProps) {
|
||||
const sdk = useSDK()
|
||||
const route = useRoute()
|
||||
const sync = useSync()
|
||||
const status = createMemo(() => (props.sessionID ? sync.session.status(props.sessionID) : "idle"))
|
||||
const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" })
|
||||
const history = usePromptHistory()
|
||||
const command = useCommandDialog()
|
||||
const renderer = useRenderer()
|
||||
@@ -222,12 +214,17 @@ export function Prompt(props: PromptProps) {
|
||||
title: "Interrupt session",
|
||||
value: "session.interrupt",
|
||||
keybind: "session_interrupt",
|
||||
disabled: status() !== "working",
|
||||
disabled: status().type === "idle",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
if (!props.sessionID) return
|
||||
if (autocomplete.visible) return
|
||||
if (!input.focused) return
|
||||
// TODO: this should be its own command
|
||||
if (store.mode === "shell") {
|
||||
setStore("mode", "normal")
|
||||
return
|
||||
}
|
||||
if (!props.sessionID) return
|
||||
|
||||
setStore("interrupt", store.interrupt + 1)
|
||||
|
||||
@@ -542,6 +539,16 @@ export function Prompt(props: PromptProps) {
|
||||
return
|
||||
}
|
||||
|
||||
const highlight = createMemo(() => {
|
||||
if (keybind.leader) return theme.border
|
||||
if (store.mode === "shell") return theme.primary
|
||||
return local.agent.color(local.agent.current().name)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
renderer.setCursorColor(highlight())
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
@@ -566,17 +573,22 @@ export function Prompt(props: PromptProps) {
|
||||
/>
|
||||
<box ref={(r) => (anchor = r)}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
{...SplitBorder}
|
||||
borderColor={keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border}
|
||||
justifyContent="space-evenly"
|
||||
border={["left"]}
|
||||
borderColor={highlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
vertical: "┃",
|
||||
bottomLeft: "╹",
|
||||
}}
|
||||
>
|
||||
<box backgroundColor={theme.backgroundElement} width={3} height="100%" alignItems="center" paddingTop={1}>
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.primary}>
|
||||
{store.mode === "normal" ? ">" : "!"}
|
||||
</text>
|
||||
</box>
|
||||
<box paddingTop={1} paddingBottom={1} backgroundColor={theme.backgroundElement} flexGrow={1}>
|
||||
<box
|
||||
paddingLeft={2}
|
||||
paddingRight={1}
|
||||
paddingTop={1}
|
||||
flexShrink={0}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
flexGrow={1}
|
||||
>
|
||||
<textarea
|
||||
placeholder={
|
||||
props.showPlaceholder
|
||||
@@ -751,35 +763,114 @@ export function Prompt(props: PromptProps) {
|
||||
cursorColor={theme.primary}
|
||||
syntaxStyle={syntax()}
|
||||
/>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
||||
<text fg={highlight()}>
|
||||
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
|
||||
</text>
|
||||
<Show when={store.mode === "normal"}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
||||
<text flexShrink={0} fg={theme.text}>
|
||||
{local.model.parsed().model}
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
<box backgroundColor={theme.backgroundElement} width={1} justifyContent="center" alignItems="center"></box>
|
||||
</box>
|
||||
<box
|
||||
height={1}
|
||||
border={["left"]}
|
||||
borderColor={highlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
vertical: "╹",
|
||||
}}
|
||||
>
|
||||
<box
|
||||
height={1}
|
||||
border={["bottom"]}
|
||||
borderColor={theme.backgroundElement}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
horizontal: "▀",
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text flexShrink={0} wrapMode="none" fg={theme.text}>
|
||||
<span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "}
|
||||
<span style={{ bold: true }}>{local.model.parsed().model}</span>
|
||||
</text>
|
||||
<Switch>
|
||||
<Match when={status() === "compacting"}>
|
||||
<text fg={theme.textMuted}>compacting...</text>
|
||||
</Match>
|
||||
<Match when={status() === "working"}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||
esc{" "}
|
||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
||||
</span>
|
||||
</text>
|
||||
<Show when={status().type !== "idle"} fallback={<text />}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
flexGrow={1}
|
||||
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||
>
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<Shimmer text="Working" color={theme.text} />
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
{(() => {
|
||||
const retry = createMemo(() => {
|
||||
const s = status()
|
||||
if (s.type !== "retry") return
|
||||
return s
|
||||
})
|
||||
const message = createMemo(() => {
|
||||
const r = retry()
|
||||
if (!r) return
|
||||
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
|
||||
return "gemini 3 way too hot right now"
|
||||
if (r.message.length > 50) return r.message.slice(0, 50) + "..."
|
||||
return r.message
|
||||
})
|
||||
const [seconds, setSeconds] = createSignal(0)
|
||||
onMount(() => {
|
||||
const timer = setInterval(() => {
|
||||
const next = retry()?.next
|
||||
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
||||
}, 1000)
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
})
|
||||
return (
|
||||
<Show when={retry()}>
|
||||
<text fg={theme.error}>
|
||||
{message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""}
|
||||
attempt #{retry()!.attempt}]
|
||||
</text>
|
||||
</Show>
|
||||
)
|
||||
})()}
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={props.hint}>{props.hint!}</Match>
|
||||
<Match when={true}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||
esc{" "}
|
||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
||||
</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={status().type !== "retry"}>
|
||||
<box gap={2} flexDirection="row">
|
||||
<Switch>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>switch agent</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={store.mode === "shell"}>
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
</>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createSimpleContext } from "./helper"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { useArgs } from "./args"
|
||||
import { RGBA } from "@opentui/core"
|
||||
|
||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
name: "Local",
|
||||
@@ -91,7 +92,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
},
|
||||
color(name: string) {
|
||||
const agent = agents().find((x) => x.name === name)
|
||||
if (agent?.color) return agent.color
|
||||
if (agent?.color) return RGBA.fromHex(agent.color)
|
||||
const index = agents().findIndex((x) => x.name === name)
|
||||
return colors()[index % colors().length]
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
|
||||
sdk.event.subscribe().then(async (events) => {
|
||||
for await (const event of events.stream) {
|
||||
console.log("event", event.type)
|
||||
emitter.emit(event.type, event)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
McpStatus,
|
||||
FormatterStatus,
|
||||
SessionStatus,
|
||||
ProviderListResponse,
|
||||
ProviderAuthMethod,
|
||||
} from "@opencode-ai/sdk"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
@@ -28,6 +30,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
status: "loading" | "partial" | "complete"
|
||||
provider: Provider[]
|
||||
provider_default: Record<string, string>
|
||||
provider_next: ProviderListResponse
|
||||
provider_auth: Record<string, ProviderAuthMethod[]>
|
||||
agent: Agent[]
|
||||
command: Command[]
|
||||
permission: {
|
||||
@@ -56,6 +60,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
}
|
||||
formatter: FormatterStatus[]
|
||||
}>({
|
||||
provider_next: {
|
||||
all: [],
|
||||
default: {},
|
||||
connected: [],
|
||||
},
|
||||
provider_auth: {},
|
||||
config: {},
|
||||
status: "loading",
|
||||
agent: [],
|
||||
@@ -232,20 +242,25 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
|
||||
const exit = useExit()
|
||||
|
||||
onMount(() => {
|
||||
async function bootstrap() {
|
||||
// blocking
|
||||
Promise.all([
|
||||
await Promise.all([
|
||||
sdk.client.config.providers({ throwOnError: true }).then((x) => {
|
||||
batch(() => {
|
||||
setStore("provider", x.data!.providers)
|
||||
setStore("provider_default", x.data!.default)
|
||||
})
|
||||
}),
|
||||
sdk.client.provider.list({ throwOnError: true }).then((x) => {
|
||||
batch(() => {
|
||||
setStore("provider_next", x.data!)
|
||||
})
|
||||
}),
|
||||
sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
|
||||
sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
|
||||
])
|
||||
.then(() => {
|
||||
setStore("status", "partial")
|
||||
if (store.status !== "complete") setStore("status", "partial")
|
||||
// non-blocking
|
||||
Promise.all([
|
||||
sdk.client.session.list().then((x) =>
|
||||
@@ -259,6 +274,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
|
||||
sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})),
|
||||
]).then(() => {
|
||||
setStore("status", "complete")
|
||||
})
|
||||
@@ -266,6 +282,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.catch(async (e) => {
|
||||
await exit(e)
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
bootstrap()
|
||||
})
|
||||
|
||||
const result = {
|
||||
@@ -320,6 +340,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
console.log("synced in " + (Date.now() - now), sessionID)
|
||||
},
|
||||
},
|
||||
bootstrap,
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
@@ -601,11 +601,9 @@ export function Session() {
|
||||
}
|
||||
|
||||
// Prompt for optional filename
|
||||
const customFilename = await DialogPrompt.show(
|
||||
dialog,
|
||||
"Export filename",
|
||||
`session-${sessionData.id.slice(0, 8)}.md`,
|
||||
)
|
||||
const customFilename = await DialogPrompt.show(dialog, "Export filename", {
|
||||
value: `session-${sessionData.id.slice(0, 8)}.md`,
|
||||
})
|
||||
|
||||
// Cancel if user pressed escape
|
||||
if (customFilename === null) return
|
||||
@@ -904,52 +902,55 @@ function UserMessage(props: {
|
||||
<Show when={text()}>
|
||||
<box
|
||||
id={props.message.id}
|
||||
onMouseOver={() => {
|
||||
setHover(true)
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setHover(false)
|
||||
}}
|
||||
onMouseUp={props.onMouseUp}
|
||||
border={["left"]}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
marginTop={props.index === 0 ? 0 : 1}
|
||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={color()}
|
||||
flexShrink={0}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
marginTop={props.index === 0 ? 0 : 1}
|
||||
>
|
||||
<text fg={theme.text}>{text()?.text}</text>
|
||||
<Show when={files().length}>
|
||||
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
|
||||
<For each={files()}>
|
||||
{(file) => {
|
||||
const bg = createMemo(() => {
|
||||
if (file.mime.startsWith("image/")) return theme.accent
|
||||
if (file.mime === "application/pdf") return theme.primary
|
||||
return theme.secondary
|
||||
})
|
||||
return (
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
|
||||
</text>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
{sync.data.config.username ?? "You"}{" "}
|
||||
<Show
|
||||
when={queued()}
|
||||
fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
|
||||
>
|
||||
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
|
||||
<box
|
||||
onMouseOver={() => {
|
||||
setHover(true)
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setHover(false)
|
||||
}}
|
||||
onMouseUp={props.onMouseUp}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={1}
|
||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||
flexShrink={0}
|
||||
>
|
||||
<text fg={theme.text}>{text()?.text}</text>
|
||||
<Show when={files().length}>
|
||||
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
|
||||
<For each={files()}>
|
||||
{(file) => {
|
||||
const bg = createMemo(() => {
|
||||
if (file.mime.startsWith("image/")) return theme.accent
|
||||
if (file.mime === "application/pdf") return theme.primary
|
||||
return theme.secondary
|
||||
})
|
||||
return (
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
|
||||
</text>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
</text>
|
||||
<text fg={theme.textMuted}>
|
||||
{sync.data.config.username ?? "You"}{" "}
|
||||
<Show
|
||||
when={queued()}
|
||||
fallback={<span style={{ fg: theme.textMuted }}>{Locale.time(props.message.time.created)}</span>}
|
||||
>
|
||||
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={compaction()}>
|
||||
@@ -1007,7 +1008,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
</box>
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={props.last && status().type !== "idle"}>
|
||||
<Match when={props.last && status().type !== "idle" && false}>
|
||||
<box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
|
||||
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
|
||||
<Shimmer text={props.message.modelID} color={theme.text} />
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog, type DialogContext } from "./dialog"
|
||||
import { onMount } from "solid-js"
|
||||
import { onMount, type JSX } from "solid-js"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
|
||||
export type DialogPromptProps = {
|
||||
title: string
|
||||
description?: () => JSX.Element
|
||||
placeholder?: string
|
||||
value?: string
|
||||
onConfirm?: (value: string) => void
|
||||
onCancel?: () => void
|
||||
@@ -19,12 +21,11 @@ export function DialogPrompt(props: DialogPromptProps) {
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "return") {
|
||||
props.onConfirm?.(textarea.plainText)
|
||||
dialog.clear()
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("large")
|
||||
dialog.setSize("medium")
|
||||
setTimeout(() => {
|
||||
textarea.focus()
|
||||
}, 1)
|
||||
@@ -37,35 +38,36 @@ export function DialogPrompt(props: DialogPromptProps) {
|
||||
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<box>
|
||||
<box gap={1}>
|
||||
{props.description}
|
||||
<textarea
|
||||
onSubmit={() => {
|
||||
props.onConfirm?.(textarea.plainText)
|
||||
dialog.clear()
|
||||
}}
|
||||
height={3}
|
||||
keyBindings={[{ name: "return", action: "submit" }]}
|
||||
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||
initialValue={props.value}
|
||||
placeholder="Enter text"
|
||||
placeholder={props.placeholder ?? "Enter text"}
|
||||
/>
|
||||
</box>
|
||||
<box paddingBottom={1}>
|
||||
<text fg={theme.textMuted}>Press enter to confirm, esc to cancel</text>
|
||||
<box paddingBottom={1} gap={1} flexDirection="row">
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>submit</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>cancel</span>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
DialogPrompt.show = (dialog: DialogContext, title: string, value?: string) => {
|
||||
DialogPrompt.show = (dialog: DialogContext, title: string, options?: Omit<DialogPromptProps, "title">) => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogPrompt
|
||||
title={title}
|
||||
value={value}
|
||||
onConfirm={(value) => resolve(value)}
|
||||
onCancel={() => resolve(null)}
|
||||
/>
|
||||
<DialogPrompt title={title} {...options} onConfirm={(value) => resolve(value)} onCancel={() => resolve(null)} />
|
||||
),
|
||||
() => resolve(null),
|
||||
)
|
||||
|
||||
@@ -157,7 +157,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
|
||||
return (
|
||||
<box gap={1}>
|
||||
<box paddingLeft={3} paddingRight={2}>
|
||||
<box paddingLeft={4} paddingRight={4}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
{props.title}
|
||||
@@ -184,8 +184,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
</box>
|
||||
</box>
|
||||
<scrollbox
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
||||
maxHeight={height()}
|
||||
@@ -194,7 +194,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
{([category, options], index) => (
|
||||
<>
|
||||
<Show when={category}>
|
||||
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={1}>
|
||||
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
|
||||
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
|
||||
{category}
|
||||
</text>
|
||||
@@ -203,6 +203,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
<For each={options}>
|
||||
{(option) => {
|
||||
const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
|
||||
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
||||
return (
|
||||
<box
|
||||
id={JSON.stringify(option.value)}
|
||||
@@ -217,8 +218,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
moveTo(index)
|
||||
}}
|
||||
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
paddingLeft={current() ? 1 : 3}
|
||||
paddingRight={3}
|
||||
gap={1}
|
||||
>
|
||||
<Option
|
||||
@@ -226,7 +227,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
footer={option.footer}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={active()}
|
||||
current={isDeepEqual(option.value, props.current)}
|
||||
current={current()}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
@@ -236,12 +237,14 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
<box paddingRight={2} paddingLeft={3} flexDirection="row" paddingBottom={1} gap={1}>
|
||||
<box paddingRight={2} paddingLeft={4} flexDirection="row" paddingBottom={1} gap={1}>
|
||||
<For each={props.keybind ?? []}>
|
||||
{(item) => (
|
||||
<text>
|
||||
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>{Keybind.toString(item.keybind)}</span>
|
||||
<span style={{ fg: theme.textMuted }}> {item.title}</span>
|
||||
<span style={{ fg: theme.text }}>
|
||||
<b>{item.title}</b>{" "}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}>{Keybind.toString(item.keybind)}</span>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
@@ -268,7 +271,7 @@ function Option(props: {
|
||||
fg={props.active ? theme.background : props.current ? theme.primary : theme.text}
|
||||
marginRight={0.5}
|
||||
>
|
||||
●
|
||||
◆
|
||||
</text>
|
||||
</Show>
|
||||
<text
|
||||
@@ -277,6 +280,7 @@ function Option(props: {
|
||||
attributes={props.active ? TextAttributes.BOLD : undefined}
|
||||
overflow="hidden"
|
||||
wrapMode="none"
|
||||
paddingLeft={3}
|
||||
>
|
||||
{Locale.truncate(props.title, 62)}
|
||||
<span style={{ fg: props.active ? theme.background : theme.textMuted }}> {props.description}</span>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { batch, createContext, Show, useContext, type JSX, type ParentProps } fr
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { Renderable, RGBA } from "@opentui/core"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { useToast } from "./toast"
|
||||
|
||||
export function Dialog(
|
||||
props: ParentProps<{
|
||||
@@ -12,10 +14,12 @@ export function Dialog(
|
||||
) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme } = useTheme()
|
||||
const renderer = useRenderer()
|
||||
|
||||
return (
|
||||
<box
|
||||
onMouseUp={async () => {
|
||||
if (renderer.getSelection()) return
|
||||
props.onClose?.()
|
||||
}}
|
||||
width={dimensions().width}
|
||||
@@ -29,6 +33,7 @@ export function Dialog(
|
||||
>
|
||||
<box
|
||||
onMouseUp={async (e) => {
|
||||
if (renderer.getSelection()) return
|
||||
e.stopPropagation()
|
||||
}}
|
||||
width={props.size === "large" ? 80 : 60}
|
||||
@@ -124,10 +129,28 @@ const ctx = createContext<DialogContext>()
|
||||
|
||||
export function DialogProvider(props: ParentProps) {
|
||||
const value = init()
|
||||
const renderer = useRenderer()
|
||||
const toast = useToast()
|
||||
return (
|
||||
<ctx.Provider value={value}>
|
||||
{props.children}
|
||||
<box position="absolute">
|
||||
<box
|
||||
position="absolute"
|
||||
onMouseUp={async () => {
|
||||
const text = renderer.getSelection()?.getSelectedText()
|
||||
if (text && text.length > 0) {
|
||||
const base64 = Buffer.from(text).toString("base64")
|
||||
const osc52 = `\x1b]52;c;${base64}\x07`
|
||||
const finalOsc52 = process.env["TMUX"] ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52
|
||||
/* @ts-expect-error */
|
||||
renderer.writeOut(finalOsc52)
|
||||
await Clipboard.copy(text)
|
||||
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||
.catch(toast.error)
|
||||
renderer.clearSelection()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Show when={value.stack.length}>
|
||||
<Dialog onClose={() => value.clear()} size={value.size}>
|
||||
{value.stack.at(-1)!.element}
|
||||
|
||||
@@ -51,6 +51,7 @@ export const Instance = {
|
||||
async dispose() {
|
||||
Log.Default.info("disposing instance", { directory: Instance.directory })
|
||||
await State.dispose(Instance.directory)
|
||||
cache.delete(Instance.directory)
|
||||
},
|
||||
async disposeAll() {
|
||||
Log.Default.info("disposing all instances")
|
||||
|
||||
@@ -57,7 +57,7 @@ export namespace State {
|
||||
|
||||
tasks.push(task)
|
||||
}
|
||||
entries.delete(key)
|
||||
entries.clear()
|
||||
await Promise.all(tasks)
|
||||
disposalFinished = true
|
||||
log.info("state disposal completed", { key })
|
||||
|
||||
143
packages/opencode/src/provider/auth.ts
Normal file
143
packages/opencode/src/provider/auth.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Plugin } from "../plugin"
|
||||
import { map, filter, pipe, fromEntries, mapValues } from "remeda"
|
||||
import z from "zod"
|
||||
import { fn } from "@/util/fn"
|
||||
import type { AuthOuathResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { NamedError } from "@/util/error"
|
||||
import { Auth } from "@/auth"
|
||||
|
||||
export namespace ProviderAuth {
|
||||
const state = Instance.state(async () => {
|
||||
const methods = pipe(
|
||||
await Plugin.list(),
|
||||
filter((x) => x.auth?.provider !== undefined),
|
||||
map((x) => [x.auth!.provider, x.auth!] as const),
|
||||
fromEntries(),
|
||||
)
|
||||
return { methods, pending: {} as Record<string, AuthOuathResult> }
|
||||
})
|
||||
|
||||
export const Method = z
|
||||
.object({
|
||||
type: z.union([z.literal("oauth"), z.literal("api")]),
|
||||
label: z.string(),
|
||||
})
|
||||
.meta({
|
||||
ref: "ProviderAuthMethod",
|
||||
})
|
||||
export type Method = z.infer<typeof Method>
|
||||
|
||||
export async function methods() {
|
||||
const s = await state().then((x) => x.methods)
|
||||
return mapValues(s, (x) =>
|
||||
x.methods.map(
|
||||
(y): Method => ({
|
||||
type: y.type,
|
||||
label: y.label,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const Authorization = z
|
||||
.object({
|
||||
url: z.string(),
|
||||
method: z.union([z.literal("auto"), z.literal("code")]),
|
||||
instructions: z.string(),
|
||||
})
|
||||
.meta({
|
||||
ref: "ProviderAuthAuthorization",
|
||||
})
|
||||
export type Authorization = z.infer<typeof Authorization>
|
||||
|
||||
export const authorize = fn(
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
method: z.number(),
|
||||
}),
|
||||
async (input): Promise<Authorization | undefined> => {
|
||||
const auth = await state().then((s) => s.methods[input.providerID])
|
||||
const method = auth.methods[input.method]
|
||||
if (method.type === "oauth") {
|
||||
const result = await method.authorize()
|
||||
await state().then((s) => (s.pending[input.providerID] = result))
|
||||
return {
|
||||
url: result.url,
|
||||
method: result.method,
|
||||
instructions: result.instructions,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const callback = fn(
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
method: z.number(),
|
||||
code: z.string().optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
const match = await state().then((s) => s.pending[input.providerID])
|
||||
if (!match) throw new OauthMissing({ providerID: input.providerID })
|
||||
let result
|
||||
|
||||
if (match.method === "code") {
|
||||
if (!input.code) throw new OauthCodeMissing({ providerID: input.providerID })
|
||||
result = await match.callback(input.code)
|
||||
}
|
||||
|
||||
if (match.method === "auto") {
|
||||
result = await match.callback()
|
||||
}
|
||||
|
||||
if (result?.type === "success") {
|
||||
if ("key" in result) {
|
||||
await Auth.set(input.providerID, {
|
||||
type: "api",
|
||||
key: result.key,
|
||||
})
|
||||
}
|
||||
if ("refresh" in result) {
|
||||
await Auth.set(input.providerID, {
|
||||
type: "oauth",
|
||||
access: result.access,
|
||||
refresh: result.refresh,
|
||||
expires: result.expires,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
throw new OauthCallbackFailed({})
|
||||
},
|
||||
)
|
||||
|
||||
export const api = fn(
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
key: z.string(),
|
||||
}),
|
||||
async (input) => {
|
||||
await Auth.set(input.providerID, {
|
||||
type: "api",
|
||||
key: input.key,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export const OauthMissing = NamedError.create(
|
||||
"ProviderAuthOauthMissing",
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
}),
|
||||
)
|
||||
export const OauthCodeMissing = NamedError.create(
|
||||
"ProviderAuthOauthCodeMissing",
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import path from "path"
|
||||
import { Config } from "../config/config"
|
||||
import { mergeDeep, sortBy } from "remeda"
|
||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||
@@ -10,7 +9,6 @@ import { ModelsDev } from "./models"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Auth } from "../auth"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Global } from "../global"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ export namespace ProviderTransform {
|
||||
result["promptCacheKey"] = sessionID
|
||||
}
|
||||
|
||||
if (providerID === "google") {
|
||||
if (providerID === "google" || (providerID === "opencode" && modelID.includes("gemini-3"))) {
|
||||
result["thinkingConfig"] = {
|
||||
includeThoughts: true,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Instance } from "../project/instance"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Auth } from "../auth"
|
||||
import { Command } from "../command"
|
||||
import { ProviderAuth } from "../provider/auth"
|
||||
import { Global } from "../global"
|
||||
import { ProjectRoute } from "./project"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
@@ -306,6 +307,27 @@ export namespace Server {
|
||||
)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/instance/dispose",
|
||||
describeRoute({
|
||||
description: "Dispose the current instance",
|
||||
operationId: "instance.dispose",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Instance disposed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
await Instance.dispose()
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/path",
|
||||
describeRoute({
|
||||
@@ -1163,6 +1185,138 @@ export namespace Server {
|
||||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/provider",
|
||||
describeRoute({
|
||||
description: "List all providers",
|
||||
operationId: "provider.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of providers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
all: ModelsDev.Provider.array(),
|
||||
default: z.record(z.string(), z.string()),
|
||||
connected: z.array(z.string()),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const providers = await ModelsDev.get()
|
||||
const connected = await Provider.list().then((x) => Object.keys(x))
|
||||
return c.json({
|
||||
all: Object.values(providers),
|
||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||
connected,
|
||||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/provider/auth",
|
||||
describeRoute({
|
||||
description: "Get provider authentication methods",
|
||||
operationId: "provider.auth",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Provider auth methods",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await ProviderAuth.methods())
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/provider/:id/oauth/authorize",
|
||||
describeRoute({
|
||||
description: "Authorize a provider using OAuth",
|
||||
operationId: "provider.oauth.authorize",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Authorization URL and method",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ProviderAuth.Authorization.optional()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().meta({ description: "Provider ID" }),
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
method: z.number().meta({ description: "Auth method index" }),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const id = c.req.valid("param").id
|
||||
const { method } = c.req.valid("json")
|
||||
const result = await ProviderAuth.authorize({
|
||||
providerID: id,
|
||||
method,
|
||||
})
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/provider/:id/oauth/callback",
|
||||
describeRoute({
|
||||
description: "Handle OAuth callback for a provider",
|
||||
operationId: "provider.oauth.callback",
|
||||
responses: {
|
||||
200: {
|
||||
description: "OAuth callback processed successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().meta({ description: "Provider ID" }),
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
method: z.number().meta({ description: "Auth method index" }),
|
||||
code: z.string().optional().meta({ description: "OAuth authorization code" }),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const id = c.req.valid("param").id
|
||||
const { method, code } = c.req.valid("json")
|
||||
await ProviderAuth.callback({
|
||||
providerID: id,
|
||||
method,
|
||||
code,
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/find",
|
||||
describeRoute({
|
||||
|
||||
@@ -49,6 +49,9 @@ import { SessionProcessor } from "./processor"
|
||||
import { TaskTool } from "@/tool/task"
|
||||
import { SessionStatus } from "./status"
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
|
||||
export namespace SessionPrompt {
|
||||
const log = Log.create({ service: "session.prompt" })
|
||||
export const OUTPUT_TOKEN_MAX = 32_000
|
||||
@@ -239,6 +242,7 @@ export namespace SessionPrompt {
|
||||
|
||||
let step = 0
|
||||
while (true) {
|
||||
SessionStatus.set(sessionID, { type: "busy" })
|
||||
log.info("loop", { step, sessionID })
|
||||
if (abort.aborted) break
|
||||
let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
|
||||
@@ -26,120 +26,122 @@ export type PluginInput = {
|
||||
|
||||
export type Plugin = (input: PluginInput) => Promise<Hooks>
|
||||
|
||||
export type AuthHook = {
|
||||
provider: string
|
||||
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
|
||||
methods: (
|
||||
| {
|
||||
type: "oauth"
|
||||
label: string
|
||||
prompts?: Array<
|
||||
| {
|
||||
type: "text"
|
||||
key: string
|
||||
message: string
|
||||
placeholder?: string
|
||||
validate?: (value: string) => string | undefined
|
||||
condition?: (inputs: Record<string, string>) => boolean
|
||||
}
|
||||
| {
|
||||
type: "select"
|
||||
key: string
|
||||
message: string
|
||||
options: Array<{
|
||||
label: string
|
||||
value: string
|
||||
hint?: string
|
||||
}>
|
||||
condition?: (inputs: Record<string, string>) => boolean
|
||||
}
|
||||
>
|
||||
authorize(inputs?: Record<string, string>): Promise<AuthOuathResult>
|
||||
}
|
||||
| {
|
||||
type: "api"
|
||||
label: string
|
||||
prompts?: Array<
|
||||
| {
|
||||
type: "text"
|
||||
key: string
|
||||
message: string
|
||||
placeholder?: string
|
||||
validate?: (value: string) => string | undefined
|
||||
condition?: (inputs: Record<string, string>) => boolean
|
||||
}
|
||||
| {
|
||||
type: "select"
|
||||
key: string
|
||||
message: string
|
||||
options: Array<{
|
||||
label: string
|
||||
value: string
|
||||
hint?: string
|
||||
}>
|
||||
condition?: (inputs: Record<string, string>) => boolean
|
||||
}
|
||||
>
|
||||
authorize?(inputs?: Record<string, string>): Promise<
|
||||
| {
|
||||
type: "success"
|
||||
key: string
|
||||
provider?: string
|
||||
}
|
||||
| {
|
||||
type: "failed"
|
||||
}
|
||||
>
|
||||
}
|
||||
)[]
|
||||
}
|
||||
|
||||
export type AuthOuathResult = { url: string; instructions: string } & (
|
||||
| {
|
||||
method: "auto"
|
||||
callback(): Promise<
|
||||
| ({
|
||||
type: "success"
|
||||
provider?: string
|
||||
} & (
|
||||
| {
|
||||
refresh: string
|
||||
access: string
|
||||
expires: number
|
||||
}
|
||||
| { key: string }
|
||||
))
|
||||
| {
|
||||
type: "failed"
|
||||
}
|
||||
>
|
||||
}
|
||||
| {
|
||||
method: "code"
|
||||
callback(code: string): Promise<
|
||||
| ({
|
||||
type: "success"
|
||||
provider?: string
|
||||
} & (
|
||||
| {
|
||||
refresh: string
|
||||
access: string
|
||||
expires: number
|
||||
}
|
||||
| { key: string }
|
||||
))
|
||||
| {
|
||||
type: "failed"
|
||||
}
|
||||
>
|
||||
}
|
||||
)
|
||||
|
||||
export interface Hooks {
|
||||
event?: (input: { event: Event }) => Promise<void>
|
||||
config?: (input: Config) => Promise<void>
|
||||
tool?: {
|
||||
[key: string]: ToolDefinition
|
||||
}
|
||||
auth?: {
|
||||
provider: string
|
||||
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
|
||||
methods: (
|
||||
| {
|
||||
type: "oauth"
|
||||
label: string
|
||||
prompts?: Array<
|
||||
| {
|
||||
type: "text"
|
||||
key: string
|
||||
message: string
|
||||
placeholder?: string
|
||||
validate?: (value: string) => string | undefined
|
||||
condition?: (inputs: Record<string, string>) => boolean
|
||||
}
|
||||
| {
|
||||
type: "select"
|
||||
key: string
|
||||
message: string
|
||||
options: Array<{
|
||||
label: string
|
||||
value: string
|
||||
hint?: string
|
||||
}>
|
||||
condition?: (inputs: Record<string, string>) => boolean
|
||||
}
|
||||
>
|
||||
authorize(inputs?: Record<string, string>): Promise<
|
||||
{ url: string; instructions: string } & (
|
||||
| {
|
||||
method: "auto"
|
||||
callback(): Promise<
|
||||
| ({
|
||||
type: "success"
|
||||
provider?: string
|
||||
} & (
|
||||
| {
|
||||
refresh: string
|
||||
access: string
|
||||
expires: number
|
||||
}
|
||||
| { key: string }
|
||||
))
|
||||
| {
|
||||
type: "failed"
|
||||
}
|
||||
>
|
||||
}
|
||||
| {
|
||||
method: "code"
|
||||
callback(code: string): Promise<
|
||||
| ({
|
||||
type: "success"
|
||||
provider?: string
|
||||
} & (
|
||||
| {
|
||||
refresh: string
|
||||
access: string
|
||||
expires: number
|
||||
}
|
||||
| { key: string }
|
||||
))
|
||||
| {
|
||||
type: "failed"
|
||||
}
|
||||
>
|
||||
}
|
||||
)
|
||||
>
|
||||
}
|
||||
| {
|
||||
type: "api"
|
||||
label: string
|
||||
prompts?: Array<
|
||||
| {
|
||||
type: "text"
|
||||
key: string
|
||||
message: string
|
||||
placeholder?: string
|
||||
validate?: (value: string) => string | undefined
|
||||
condition?: (inputs: Record<string, string>) => boolean
|
||||
}
|
||||
| {
|
||||
type: "select"
|
||||
key: string
|
||||
message: string
|
||||
options: Array<{
|
||||
label: string
|
||||
value: string
|
||||
hint?: string
|
||||
}>
|
||||
condition?: (inputs: Record<string, string>) => boolean
|
||||
}
|
||||
>
|
||||
authorize?(inputs?: Record<string, string>): Promise<
|
||||
| {
|
||||
type: "success"
|
||||
key: string
|
||||
provider?: string
|
||||
}
|
||||
| {
|
||||
type: "failed"
|
||||
}
|
||||
>
|
||||
}
|
||||
)[]
|
||||
}
|
||||
auth?: AuthHook
|
||||
/**
|
||||
* Called when a new message is received
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
ToolListData,
|
||||
ToolListResponses,
|
||||
ToolListErrors,
|
||||
InstanceDisposeData,
|
||||
InstanceDisposeResponses,
|
||||
PathGetData,
|
||||
PathGetResponses,
|
||||
SessionListData,
|
||||
@@ -92,6 +94,16 @@ import type {
|
||||
CommandListResponses,
|
||||
ConfigProvidersData,
|
||||
ConfigProvidersResponses,
|
||||
ProviderListData,
|
||||
ProviderListResponses,
|
||||
ProviderAuthData,
|
||||
ProviderAuthResponses,
|
||||
ProviderOauthAuthorizeData,
|
||||
ProviderOauthAuthorizeResponses,
|
||||
ProviderOauthAuthorizeErrors,
|
||||
ProviderOauthCallbackData,
|
||||
ProviderOauthCallbackResponses,
|
||||
ProviderOauthCallbackErrors,
|
||||
FindTextData,
|
||||
FindTextResponses,
|
||||
FindFilesData,
|
||||
@@ -272,6 +284,18 @@ class Tool extends _HeyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
class Instance extends _HeyApiClient {
|
||||
/**
|
||||
* Dispose the current instance
|
||||
*/
|
||||
public dispose<ThrowOnError extends boolean = false>(options?: Options<InstanceDisposeData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).post<InstanceDisposeResponses, unknown, ThrowOnError>({
|
||||
url: "/instance/dispose",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Path extends _HeyApiClient {
|
||||
/**
|
||||
* Get the current path
|
||||
@@ -554,6 +578,67 @@ class Command extends _HeyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
class Oauth extends _HeyApiClient {
|
||||
/**
|
||||
* Authorize a provider using OAuth
|
||||
*/
|
||||
public authorize<ThrowOnError extends boolean = false>(options: Options<ProviderOauthAuthorizeData, ThrowOnError>) {
|
||||
return (options.client ?? this._client).post<
|
||||
ProviderOauthAuthorizeResponses,
|
||||
ProviderOauthAuthorizeErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/provider/{id}/oauth/authorize",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OAuth callback for a provider
|
||||
*/
|
||||
public callback<ThrowOnError extends boolean = false>(options: Options<ProviderOauthCallbackData, ThrowOnError>) {
|
||||
return (options.client ?? this._client).post<
|
||||
ProviderOauthCallbackResponses,
|
||||
ProviderOauthCallbackErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/provider/{id}/oauth/callback",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Provider extends _HeyApiClient {
|
||||
/**
|
||||
* List all providers
|
||||
*/
|
||||
public list<ThrowOnError extends boolean = false>(options?: Options<ProviderListData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).get<ProviderListResponses, unknown, ThrowOnError>({
|
||||
url: "/provider",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider authentication methods
|
||||
*/
|
||||
public auth<ThrowOnError extends boolean = false>(options?: Options<ProviderAuthData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).get<ProviderAuthResponses, unknown, ThrowOnError>({
|
||||
url: "/provider/auth",
|
||||
...options,
|
||||
})
|
||||
}
|
||||
oauth = new Oauth({ client: this._client })
|
||||
}
|
||||
|
||||
class Find extends _HeyApiClient {
|
||||
/**
|
||||
* Find text in files
|
||||
@@ -891,9 +976,11 @@ export class OpencodeClient extends _HeyApiClient {
|
||||
project = new Project({ client: this._client })
|
||||
config = new Config({ client: this._client })
|
||||
tool = new Tool({ client: this._client })
|
||||
instance = new Instance({ client: this._client })
|
||||
path = new Path({ client: this._client })
|
||||
session = new Session({ client: this._client })
|
||||
command = new Command({ client: this._client })
|
||||
provider = new Provider({ client: this._client })
|
||||
find = new Find({ client: this._client })
|
||||
file = new File({ client: this._client })
|
||||
app = new App({ client: this._client })
|
||||
|
||||
@@ -1333,6 +1333,17 @@ export type Provider = {
|
||||
}
|
||||
}
|
||||
|
||||
export type ProviderAuthMethod = {
|
||||
type: "oauth" | "api"
|
||||
label: string
|
||||
}
|
||||
|
||||
export type ProviderAuthAuthorization = {
|
||||
url: string
|
||||
method: "auto" | "code"
|
||||
instructions: string
|
||||
}
|
||||
|
||||
export type Symbol = {
|
||||
name: string
|
||||
kind: number
|
||||
@@ -1611,6 +1622,24 @@ export type ToolListResponses = {
|
||||
|
||||
export type ToolListResponse = ToolListResponses[keyof ToolListResponses]
|
||||
|
||||
export type InstanceDisposeData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/instance/dispose"
|
||||
}
|
||||
|
||||
export type InstanceDisposeResponses = {
|
||||
/**
|
||||
* Instance disposed
|
||||
*/
|
||||
200: boolean
|
||||
}
|
||||
|
||||
export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses]
|
||||
|
||||
export type PathGetData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -2484,6 +2513,128 @@ export type ConfigProvidersResponses = {
|
||||
|
||||
export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses]
|
||||
|
||||
export type ProviderListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/provider"
|
||||
}
|
||||
|
||||
export type ProviderListResponses = {
|
||||
/**
|
||||
* List of providers
|
||||
*/
|
||||
200: {
|
||||
all: Array<Provider>
|
||||
default: {
|
||||
[key: string]: string
|
||||
}
|
||||
connected: Array<string>
|
||||
}
|
||||
}
|
||||
|
||||
export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses]
|
||||
|
||||
export type ProviderAuthData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/provider/auth"
|
||||
}
|
||||
|
||||
export type ProviderAuthResponses = {
|
||||
/**
|
||||
* Provider auth methods
|
||||
*/
|
||||
200: {
|
||||
[key: string]: Array<ProviderAuthMethod>
|
||||
}
|
||||
}
|
||||
|
||||
export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses]
|
||||
|
||||
export type ProviderOauthAuthorizeData = {
|
||||
body?: {
|
||||
/**
|
||||
* Auth method index
|
||||
*/
|
||||
method: number
|
||||
}
|
||||
path: {
|
||||
/**
|
||||
* Provider ID
|
||||
*/
|
||||
id: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/provider/{id}/oauth/authorize"
|
||||
}
|
||||
|
||||
export type ProviderOauthAuthorizeErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors]
|
||||
|
||||
export type ProviderOauthAuthorizeResponses = {
|
||||
/**
|
||||
* Authorization URL and method
|
||||
*/
|
||||
200: ProviderAuthAuthorization
|
||||
}
|
||||
|
||||
export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses]
|
||||
|
||||
export type ProviderOauthCallbackData = {
|
||||
body?: {
|
||||
/**
|
||||
* Auth method index
|
||||
*/
|
||||
method: number
|
||||
/**
|
||||
* OAuth authorization code
|
||||
*/
|
||||
code?: string
|
||||
}
|
||||
path: {
|
||||
/**
|
||||
* Provider ID
|
||||
*/
|
||||
id: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
}
|
||||
url: "/provider/{id}/oauth/callback"
|
||||
}
|
||||
|
||||
export type ProviderOauthCallbackErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors]
|
||||
|
||||
export type ProviderOauthCallbackResponses = {
|
||||
/**
|
||||
* OAuth callback processed successfully
|
||||
*/
|
||||
200: boolean
|
||||
}
|
||||
|
||||
export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses]
|
||||
|
||||
export type FindTextData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
Reference in New Issue
Block a user