mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
wip(desktop): progress
This commit is contained in:
parent
91d743ef9a
commit
190fa4c87a
12 changed files with 201 additions and 126 deletions
|
|
@ -16,6 +16,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useLayout } from "@/context/layout"
|
||||
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
|
|
@ -56,6 +57,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
const sync = useSync()
|
||||
const local = useLocal()
|
||||
const session = useSession()
|
||||
const layout = useLayout()
|
||||
let editorRef!: HTMLDivElement
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
|
|
@ -453,54 +455,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
class="capitalize"
|
||||
variant="ghost"
|
||||
/>
|
||||
<SelectDialog
|
||||
title="Select model"
|
||||
placeholder="Search models"
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
items={local.model.list()}
|
||||
current={local.model.current()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
sortGroupsBy={(a, b) => {
|
||||
const order = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
|
||||
if (a.category === "Recent" && b.category !== "Recent") return -1
|
||||
if (b.category === "Recent" && a.category !== "Recent") return 1
|
||||
const aProvider = a.items[0].provider.id
|
||||
const bProvider = b.items[0].provider.id
|
||||
if (order.includes(aProvider) && !order.includes(bProvider)) return -1
|
||||
if (!order.includes(aProvider) && order.includes(bProvider)) return 1
|
||||
return order.indexOf(aProvider) - order.indexOf(bProvider)
|
||||
}}
|
||||
onSelect={(x) =>
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
|
||||
}
|
||||
trigger={
|
||||
<Button as="div" variant="ghost">
|
||||
{local.model.current()?.name ?? "Select model"}
|
||||
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</Button>
|
||||
}
|
||||
actions={
|
||||
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1}>
|
||||
Connect provider
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<Show when={!i.cost || i.cost?.input === 0}>
|
||||
<Tag>Free</Tag>
|
||||
</Show>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</SelectDialog>
|
||||
<Button as="div" variant="ghost" onClick={() => layout.dialog.open("model")}>
|
||||
{local.model.current()?.name ?? "Select model"}
|
||||
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</Button>
|
||||
<Show when={layout.dialog.opened() === "model"}>
|
||||
<SelectDialog
|
||||
defaultOpen
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
layout.dialog.open("model")
|
||||
} else {
|
||||
layout.dialog.close("model")
|
||||
}
|
||||
}}
|
||||
title="Select model"
|
||||
placeholder="Search models"
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
items={local.model.list()}
|
||||
current={local.model.current()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
sortGroupsBy={(a, b) => {
|
||||
const order = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
|
||||
if (a.category === "Recent" && b.category !== "Recent") return -1
|
||||
if (b.category === "Recent" && a.category !== "Recent") return 1
|
||||
const aProvider = a.items[0].provider.id
|
||||
const bProvider = b.items[0].provider.id
|
||||
if (order.includes(aProvider) && !order.includes(bProvider)) return -1
|
||||
if (!order.includes(aProvider) && order.includes(bProvider)) return 1
|
||||
return order.indexOf(aProvider) - order.indexOf(bProvider)
|
||||
}}
|
||||
onSelect={(x) =>
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
|
||||
}
|
||||
actions={
|
||||
<Button
|
||||
class="h-7 -my-1 text-14-medium"
|
||||
icon="plus-small"
|
||||
tabIndex={-1}
|
||||
onClick={() => layout.dialog.open("provider")}
|
||||
>
|
||||
Connect provider
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<Show when={!i.cost || i.cost?.input === 0}>
|
||||
<Tag>Free</Tag>
|
||||
</Show>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</SelectDialog>
|
||||
</Show>
|
||||
</div>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type {
|
||||
Message,
|
||||
Agent,
|
||||
Provider,
|
||||
Session,
|
||||
Part,
|
||||
Config,
|
||||
|
|
@ -12,6 +11,7 @@ import type {
|
|||
FileDiff,
|
||||
Todo,
|
||||
SessionStatus,
|
||||
ProviderListResponse,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
|
|
@ -20,9 +20,9 @@ import { useGlobalSDK } from "./global-sdk"
|
|||
|
||||
type State = {
|
||||
ready: boolean
|
||||
// provider: Provider[]
|
||||
agent: Agent[]
|
||||
project: string
|
||||
provider: ProviderListResponse
|
||||
config: Config
|
||||
path: Path
|
||||
session: Session[]
|
||||
|
|
@ -49,15 +49,16 @@ type State = {
|
|||
export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
|
||||
name: "GlobalSync",
|
||||
init: () => {
|
||||
const sdk = useGlobalSDK()
|
||||
const [globalStore, setGlobalStore] = createStore<{
|
||||
ready: boolean
|
||||
projects: Project[]
|
||||
providers: Provider[]
|
||||
project: Project[]
|
||||
provider: ProviderListResponse
|
||||
children: Record<string, State>
|
||||
}>({
|
||||
ready: false,
|
||||
projects: [],
|
||||
providers: [],
|
||||
project: [],
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
children: {},
|
||||
})
|
||||
|
||||
|
|
@ -66,11 +67,11 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
if (!children[directory]) {
|
||||
setGlobalStore("children", directory, {
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
ready: false,
|
||||
agent: [],
|
||||
// provider: [],
|
||||
session: [],
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
|
|
@ -86,7 +87,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
return children[directory]
|
||||
}
|
||||
|
||||
const sdk = useGlobalSDK()
|
||||
sdk.event.listen((e) => {
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
|
|
@ -94,13 +94,13 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
if (directory === "global") {
|
||||
switch (event.type) {
|
||||
case "project.updated": {
|
||||
const result = Binary.search(globalStore.projects, event.properties.id, (s) => s.id)
|
||||
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
setGlobalStore("projects", result.index, reconcile(event.properties))
|
||||
setGlobalStore("project", result.index, reconcile(event.properties))
|
||||
return
|
||||
}
|
||||
setGlobalStore(
|
||||
"projects",
|
||||
"project",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties)
|
||||
}),
|
||||
|
|
@ -184,14 +184,14 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
Promise.all([
|
||||
sdk.client.project.list().then(async (x) => {
|
||||
setGlobalStore(
|
||||
"projects",
|
||||
"project",
|
||||
x
|
||||
.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
)
|
||||
}),
|
||||
sdk.client.provider.list().then((x) => {
|
||||
setGlobalStore("providers", x.data ?? [])
|
||||
setGlobalStore("provider", x.data ?? {})
|
||||
}),
|
||||
]).then(() => setGlobalStore("ready", true))
|
||||
|
||||
|
|
|
|||
|
|
@ -40,9 +40,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
},
|
||||
}),
|
||||
{
|
||||
name: "default-layout.v6",
|
||||
name: "default-layout.v7",
|
||||
},
|
||||
)
|
||||
const [ephemeral, setEphemeral] = createStore({
|
||||
dialog: {
|
||||
open: undefined as undefined | "provider" | "model",
|
||||
},
|
||||
})
|
||||
|
||||
function pickAvailableColor() {
|
||||
const available = PASTEL_COLORS.filter((c) => !colors().has(c))
|
||||
|
|
@ -51,7 +56,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
}
|
||||
|
||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||
const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
|
||||
const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
if (!metadata) return []
|
||||
return [
|
||||
{
|
||||
|
|
@ -168,6 +173,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||
setStore("review", "state", "tab")
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
opened: createMemo(() => ephemeral.dialog?.open),
|
||||
open(dialog: "provider" | "model") {
|
||||
setEphemeral("dialog", "open", dialog)
|
||||
},
|
||||
close(dialog: "provider" | "model") {
|
||||
if (ephemeral.dialog?.open === dialog) {
|
||||
setEphemeral("dialog", "open", undefined)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
const sync = useSync()
|
||||
|
||||
function isModelValid(model: ModelKey) {
|
||||
const provider = sync.data.provider.find((x) => x.id === model.providerID)
|
||||
return !!provider?.models[model.modelID]
|
||||
const provider = sync.data.provider?.all.find((x) => x.id === model.providerID)
|
||||
return !!provider?.models[model.modelID] && sync.data.provider?.connected.includes(model.providerID)
|
||||
}
|
||||
|
||||
function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
|
||||
|
|
@ -115,17 +115,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
})
|
||||
|
||||
const list = createMemo(() =>
|
||||
sync.data.provider.flatMap((p) =>
|
||||
Object.values(p.models).map(
|
||||
(m) =>
|
||||
({
|
||||
...m,
|
||||
name: m.name.replace("(latest)", "").trim(),
|
||||
provider: p,
|
||||
latest: m.name.includes("(latest)"),
|
||||
}) as LocalModel,
|
||||
sync.data.provider.all
|
||||
.filter((p) => sync.data.provider.connected.includes(p.id))
|
||||
.flatMap((p) =>
|
||||
Object.values(p.models).map((m) => ({
|
||||
...m,
|
||||
name: m.name.replace("(latest)", "").trim(),
|
||||
provider: p,
|
||||
latest: m.name.includes("(latest)"),
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
|
||||
|
||||
|
|
@ -145,12 +144,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||
return item
|
||||
}
|
||||
}
|
||||
const provider = sync.data.provider[0]
|
||||
const model = Object.values(provider.models)[0]
|
||||
return {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
|
||||
for (const p of sync.data.provider.connected) {
|
||||
if (p in sync.data.provider.default) {
|
||||
return {
|
||||
providerID: p,
|
||||
modelID: sync.data.provider.default[p],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No default model found")
|
||||
})
|
||||
|
||||
const currentModel = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||
() => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
|
||||
)
|
||||
const model = createMemo(() =>
|
||||
last() ? sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
|
||||
last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
|
||||
)
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
|
||||
const load = {
|
||||
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
|
||||
provider: () => sdk.client.provider.list().then((x) => setStore("provider", x.data!)),
|
||||
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
|
||||
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
session: () =>
|
||||
|
|
@ -42,8 +42,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
return store.ready
|
||||
},
|
||||
get project() {
|
||||
const match = Binary.search(globalSync.data.projects, store.project, (p) => p.id)
|
||||
if (match.found) return globalSync.data.projects[match.index]
|
||||
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
|
||||
if (match.found) return globalSync.data.project[match.index]
|
||||
return undefined
|
||||
},
|
||||
session: {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default function Home() {
|
|||
<div class="mx-auto mt-55">
|
||||
<Logo class="w-xl opacity-12" />
|
||||
<Switch>
|
||||
<Match when={sync.data.projects.length > 0}>
|
||||
<Match when={sync.data.project.length > 0}>
|
||||
<div class="mt-20 w-full flex flex-col gap-4">
|
||||
<div class="flex gap-2 items-center justify-between pl-3">
|
||||
<div class="text-14-medium text-text-strong">Recent projects</div>
|
||||
|
|
@ -50,7 +50,7 @@ export default function Home() {
|
|||
</div>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<For
|
||||
each={sync.data.projects
|
||||
each={sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice(0, 5)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Avatar } from "@opencode-ai/ui/avatar"
|
|||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
|
|
@ -31,6 +32,9 @@ import {
|
|||
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
||||
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
|
||||
const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore] = createStore({
|
||||
|
|
@ -46,15 +50,18 @@ export default function Layout(props: ParentProps) {
|
|||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const providers = createMemo(() => globalSync.data.providers)
|
||||
const hasProviders = createMemo(() => {
|
||||
const [projectStore] = globalSync.child(currentDirectory())
|
||||
return projectStore.provider.filter((p) => p.id !== "opencode").length > 0
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
console.log(providers())
|
||||
const providers = createMemo(() => {
|
||||
if (currentDirectory()) {
|
||||
const [projectStore] = globalSync.child(currentDirectory())
|
||||
return projectStore.provider
|
||||
}
|
||||
return globalSync.data.provider
|
||||
})
|
||||
const connectedProviders = createMemo(() =>
|
||||
providers().all.filter(
|
||||
(p) => providers().connected.includes(p.id) && Object.values(p.models).find((m) => m.cost?.input),
|
||||
),
|
||||
)
|
||||
|
||||
function navigateToProject(directory: string | undefined) {
|
||||
if (!directory) return
|
||||
|
|
@ -93,7 +100,9 @@ export default function Layout(props: ParentProps) {
|
|||
}
|
||||
}
|
||||
|
||||
async function connectProvider() {}
|
||||
async function connectProvider() {
|
||||
layout.dialog.open("provider")
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!params.dir || !params.id) return
|
||||
|
|
@ -484,7 +493,7 @@ export default function Layout(props: ParentProps) {
|
|||
</div>
|
||||
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
||||
<Switch>
|
||||
<Match when={!hasProviders() && layout.sidebar.opened()}>
|
||||
<Match when={!connectedProviders().length && layout.sidebar.opened()}>
|
||||
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
|
||||
<div class="p-3 flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Getting started</div>
|
||||
|
|
@ -493,7 +502,7 @@ export default function Layout(props: ParentProps) {
|
|||
</div>
|
||||
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-[7px]"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
onClick={connectProvider}
|
||||
|
|
@ -506,7 +515,7 @@ export default function Layout(props: ParentProps) {
|
|||
<Match when={true}>
|
||||
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
|
|
@ -520,7 +529,7 @@ export default function Layout(props: ParentProps) {
|
|||
<Show when={platform.openDirectoryPickerDialog}>
|
||||
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="folder-add-left"
|
||||
|
|
@ -533,7 +542,7 @@ export default function Layout(props: ParentProps) {
|
|||
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
disabled
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="settings-gear"
|
||||
|
|
@ -546,7 +555,7 @@ export default function Layout(props: ParentProps) {
|
|||
as={"a"}
|
||||
href="https://opencode.ai/desktop-feedback"
|
||||
target="_blank"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="bubble-5"
|
||||
|
|
@ -557,32 +566,53 @@ export default function Layout(props: ParentProps) {
|
|||
</div>
|
||||
</div>
|
||||
<main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
|
||||
<Show when={true}>
|
||||
<Show when={layout.dialog.opened() === "provider"}>
|
||||
<SelectDialog
|
||||
defaultOpen
|
||||
title="Connect provider"
|
||||
placeholder="Search providers"
|
||||
activeIcon="plus-small"
|
||||
key={(x) => x?.id}
|
||||
items={providers()}
|
||||
items={providers().all}
|
||||
// current={local.model.current()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
|
||||
// groupBy={(x) => x.provider.name}
|
||||
onSelect={(x) =>
|
||||
// local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
|
||||
{
|
||||
return
|
||||
filterKeys={["id", "name"]}
|
||||
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
|
||||
sortBy={(a, b) => {
|
||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||
return a.name.localeCompare(b.name)
|
||||
}}
|
||||
sortGroupsBy={(a, b) => {
|
||||
if (a.category === "Popular" && b.category !== "Popular") return -1
|
||||
if (b.category === "Popular" && a.category !== "Popular") return 1
|
||||
return 0
|
||||
}}
|
||||
// onSelect={(x) => }
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
layout.dialog.open("provider")
|
||||
} else {
|
||||
layout.dialog.close("provider")
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2.5">
|
||||
<div class="px-1.25 w-full flex items-center gap-x-4">
|
||||
<ProviderIcon
|
||||
id={i.id as IconName}
|
||||
// TODO: clean this up after we update icon in models.dev
|
||||
classList={{
|
||||
"text-icon-weak-base": true,
|
||||
"size-4 mx-0.5": i.id === "opencode",
|
||||
"size-5": i.id !== "opencode",
|
||||
}}
|
||||
/>
|
||||
<span>{i.name}</span>
|
||||
<Show when={!i.cost || i.cost?.input === 0}>
|
||||
<Tag>Free</Tag>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
</Show>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ export default function () {
|
|||
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ProviderIcon name={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
|
||||
<ProviderIcon id={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import sprite from "./provider-icons/sprite.svg"
|
|||
import type { IconName } from "./provider-icons/types"
|
||||
|
||||
export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
|
||||
name: IconName
|
||||
id: IconName
|
||||
}
|
||||
|
||||
export const ProviderIcon: Component<ProviderIconProps> = (props) => {
|
||||
const [local, rest] = splitProps(props, ["name", "class", "classList"])
|
||||
const [local, rest] = splitProps(props, ["id", "class", "classList"])
|
||||
return (
|
||||
<svg
|
||||
data-component="provider-icon"
|
||||
|
|
@ -18,7 +18,7 @@ export const ProviderIcon: Component<ProviderIconProps> = (props) => {
|
|||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
<use href={`${sprite}#${local.name}`} />
|
||||
<use href={`${sprite}#${local.id}`} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
display: flex;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
padding: 4px 10px 4px 6px;
|
||||
padding: 4px 10px 4px 16px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-self: stretch;
|
||||
|
|
@ -121,6 +121,9 @@
|
|||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
[data-slot="select-dialog-item-selected-icon"] {
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
[data-slot="select-dialog-item-active-icon"] {
|
||||
display: none;
|
||||
color: var(--icon-strong-base);
|
||||
}
|
||||
|
|
@ -128,12 +131,13 @@
|
|||
&[data-active="true"] {
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface-raised-base-hover);
|
||||
}
|
||||
&[data-selected="true"] {
|
||||
[data-slot="select-dialog-item-selected-icon"] {
|
||||
[data-slot="select-dialog-item-active-icon"] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
background: var(--surface-raised-base-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createEffect, Show, For, type JSX, splitProps, createSignal } from "sol
|
|||
import { createStore } from "solid-js/store"
|
||||
import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { Dialog, DialogProps } from "./dialog"
|
||||
import { Icon } from "./icon"
|
||||
import { Icon, IconProps } from "./icon"
|
||||
import { Input } from "./input"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ interface SelectDialogProps<T>
|
|||
onSelect?: (value: T | undefined) => void
|
||||
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
|
||||
actions?: JSX.Element
|
||||
activeIcon?: IconProps["name"]
|
||||
}
|
||||
|
||||
export function SelectDialog<T>(props: SelectDialogProps<T>) {
|
||||
|
|
@ -165,7 +166,12 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
|
|||
}}
|
||||
>
|
||||
{others.children(item)}
|
||||
<Icon data-slot="select-dialog-item-selected-icon" name="check-small" />
|
||||
<Show when={item === others.current}>
|
||||
<Icon data-slot="select-dialog-item-selected-icon" name="check-small" />
|
||||
</Show>
|
||||
<Show when={others.activeIcon}>
|
||||
{(icon) => <Icon data-slot="select-dialog-item-active-icon" name={icon()} />}
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue