Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 4a8e8f5

Browse files
committed
wip(desktop): progress
1 parent a68bee7 commit 4a8e8f5

10 files changed

Lines changed: 784 additions & 730 deletions

File tree

packages/desktop/src/components/dialog-connect.tsx

Lines changed: 406 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { Component, createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
2+
import { useLocal } from "@/context/local"
3+
import { useLayout } from "@/context/layout"
4+
import { popularProviders, useProviders } from "@/hooks/use-providers"
5+
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
6+
import { Button } from "@opencode-ai/ui/button"
7+
import { Tag } from "@opencode-ai/ui/tag"
8+
import { Dialog } from "@opencode-ai/ui/dialog"
9+
import { List, ListRef } from "@opencode-ai/ui/list"
10+
import { iife } from "@opencode-ai/util/iife"
11+
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
12+
import { IconName } from "@opencode-ai/ui/icons/provider"
13+
14+
export const DialogModel: Component = () => {
15+
const local = useLocal()
16+
const layout = useLayout()
17+
const providers = useProviders()
18+
19+
return (
20+
<Switch>
21+
<Match when={providers.paid().length > 0}>
22+
{iife(() => {
23+
const models = createMemo(() =>
24+
local.model
25+
.list()
26+
.filter((m) => m.visible)
27+
.filter((m) =>
28+
layout.connect.state() === "complete" ? m.provider.id === layout.connect.provider() : true,
29+
),
30+
)
31+
return (
32+
<SelectDialog
33+
defaultOpen
34+
onOpenChange={(open) => {
35+
if (open) {
36+
layout.dialog.open("model")
37+
} else {
38+
layout.dialog.close("model")
39+
}
40+
}}
41+
title="Select model"
42+
placeholder="Search models"
43+
emptyMessage="No model results"
44+
key={(x) => `${x.provider.id}:${x.id}`}
45+
items={models}
46+
current={local.model.current()}
47+
filterKeys={["provider.name", "name", "id"]}
48+
sortBy={(a, b) => a.name.localeCompare(b.name)}
49+
groupBy={(x) => x.provider.name}
50+
sortGroupsBy={(a, b) => {
51+
if (a.category === "Recent" && b.category !== "Recent") return -1
52+
if (b.category === "Recent" && a.category !== "Recent") return 1
53+
const aProvider = a.items[0].provider.id
54+
const bProvider = b.items[0].provider.id
55+
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
56+
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
57+
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
58+
}}
59+
onSelect={(x) =>
60+
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
61+
recent: true,
62+
})
63+
}
64+
actions={
65+
<Button
66+
class="h-7 -my-1 text-14-medium"
67+
icon="plus-small"
68+
tabIndex={-1}
69+
onClick={() => layout.dialog.open("provider")}
70+
>
71+
Connect provider
72+
</Button>
73+
}
74+
>
75+
{(i) => (
76+
<div class="w-full flex items-center gap-x-2.5">
77+
<span>{i.name}</span>
78+
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
79+
<Tag>Free</Tag>
80+
</Show>
81+
<Show when={i.latest}>
82+
<Tag>Latest</Tag>
83+
</Show>
84+
</div>
85+
)}
86+
</SelectDialog>
87+
)
88+
})}
89+
</Match>
90+
<Match when={true}>
91+
{iife(() => {
92+
let listRef: ListRef | undefined
93+
const handleKey = (e: KeyboardEvent) => {
94+
if (e.key === "Escape") return
95+
listRef?.onKeyDown(e)
96+
}
97+
98+
onMount(() => {
99+
document.addEventListener("keydown", handleKey)
100+
onCleanup(() => {
101+
document.removeEventListener("keydown", handleKey)
102+
})
103+
})
104+
105+
return (
106+
<Dialog
107+
modal
108+
defaultOpen
109+
onOpenChange={(open) => {
110+
if (open) {
111+
layout.dialog.open("model")
112+
} else {
113+
layout.dialog.close("model")
114+
}
115+
}}
116+
>
117+
<Dialog.Header>
118+
<Dialog.Title>Select model</Dialog.Title>
119+
<Dialog.CloseButton tabIndex={-1} />
120+
</Dialog.Header>
121+
<Dialog.Body>
122+
<div class="flex flex-col gap-3 px-2.5">
123+
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
124+
<List
125+
ref={(ref) => (listRef = ref)}
126+
items={local.model.list}
127+
current={local.model.current()}
128+
key={(x) => `${x.provider.id}:${x.id}`}
129+
onSelect={(x) => {
130+
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
131+
recent: true,
132+
})
133+
layout.dialog.close("model")
134+
}}
135+
>
136+
{(i) => (
137+
<div class="w-full flex items-center gap-x-2.5">
138+
<span>{i.name}</span>
139+
<Tag>Free</Tag>
140+
<Show when={i.latest}>
141+
<Tag>Latest</Tag>
142+
</Show>
143+
</div>
144+
)}
145+
</List>
146+
<div />
147+
<div />
148+
</div>
149+
<div class="px-1.5 pb-1.5">
150+
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
151+
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
152+
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
153+
<div class="w-full">
154+
<List
155+
class="w-full"
156+
key={(x) => x?.id}
157+
items={providers.popular}
158+
activeIcon="plus-small"
159+
sortBy={(a, b) => {
160+
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
161+
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
162+
return a.name.localeCompare(b.name)
163+
}}
164+
onSelect={(x) => {
165+
if (!x) return
166+
layout.dialog.connect(x.id)
167+
}}
168+
>
169+
{(i) => (
170+
<div class="w-full flex items-center gap-x-4">
171+
<ProviderIcon
172+
data-slot="list-item-extra-icon"
173+
id={i.id as IconName}
174+
// TODO: clean this up after we update icon in models.dev
175+
classList={{
176+
"text-icon-weak-base": true,
177+
"size-4 mx-0.5": i.id === "opencode",
178+
"size-5": i.id !== "opencode",
179+
}}
180+
/>
181+
<span>{i.name}</span>
182+
<Show when={i.id === "opencode"}>
183+
<Tag>Recommended</Tag>
184+
</Show>
185+
<Show when={i.id === "anthropic"}>
186+
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
187+
</Show>
188+
</div>
189+
)}
190+
</List>
191+
<Button
192+
variant="ghost"
193+
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
194+
icon="dot-grid"
195+
onClick={() => {
196+
layout.dialog.open("provider")
197+
}}
198+
>
199+
View all providers
200+
</Button>
201+
</div>
202+
</div>
203+
</div>
204+
</div>
205+
</Dialog.Body>
206+
</Dialog>
207+
)
208+
})}
209+
</Match>
210+
</Switch>
211+
)
212+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Component, Show } from "solid-js"
2+
import { useLayout } from "@/context/layout"
3+
import { popularProviders, useProviders } from "@/hooks/use-providers"
4+
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
5+
import { Tag } from "@opencode-ai/ui/tag"
6+
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
7+
import { IconName } from "@opencode-ai/ui/icons/provider"
8+
9+
export const DialogProvider: Component = () => {
10+
const layout = useLayout()
11+
const providers = useProviders()
12+
13+
return (
14+
<SelectDialog
15+
defaultOpen
16+
title="Connect provider"
17+
placeholder="Search providers"
18+
activeIcon="plus-small"
19+
key={(x) => x?.id}
20+
items={providers.all}
21+
filterKeys={["id", "name"]}
22+
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
23+
sortBy={(a, b) => {
24+
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
25+
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
26+
return a.name.localeCompare(b.name)
27+
}}
28+
sortGroupsBy={(a, b) => {
29+
if (a.category === "Popular" && b.category !== "Popular") return -1
30+
if (b.category === "Popular" && a.category !== "Popular") return 1
31+
return 0
32+
}}
33+
onSelect={(x) => {
34+
if (!x) return
35+
layout.dialog.connect(x.id)
36+
}}
37+
onOpenChange={(open) => {
38+
if (open) {
39+
layout.dialog.open("provider")
40+
} else {
41+
layout.dialog.close("provider")
42+
}
43+
}}
44+
>
45+
{(i) => (
46+
<div class="px-1.25 w-full flex items-center gap-x-4">
47+
<ProviderIcon
48+
data-slot="list-item-extra-icon"
49+
id={i.id as IconName}
50+
// TODO: clean this up after we update icon in models.dev
51+
classList={{
52+
"text-icon-weak-base": true,
53+
"size-4 mx-0.5": i.id === "opencode",
54+
"size-5": i.id !== "opencode",
55+
}}
56+
/>
57+
<span>{i.name}</span>
58+
<Show when={i.id === "opencode"}>
59+
<Tag>Recommended</Tag>
60+
</Show>
61+
<Show when={i.id === "anthropic"}>
62+
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
63+
</Show>
64+
</div>
65+
)}
66+
</SelectDialog>
67+
)
68+
}

0 commit comments

Comments
 (0)