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

Commit 4246cdb

Browse files
committed
wip(desktop): progress
1 parent 7ade6d3 commit 4246cdb

20 files changed

Lines changed: 726 additions & 479 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export const DialogConnect: Component<{ provider: string }> = (props) => {
117117
title: `${provider().name} connected`,
118118
description: `${provider().name} models are now available to use.`,
119119
})
120-
dialog.replace(() => <DialogModel connectedProvider={props.provider} />)
120+
dialog.replace(() => <DialogModel provider={props.provider} />)
121121
}, 500)
122122
}
123123

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Component } from "solid-js"
2+
import { useLocal } from "@/context/local"
3+
import { Dialog } from "@opencode-ai/ui/dialog"
4+
import { List } from "@opencode-ai/ui/list"
5+
import { FileIcon } from "@opencode-ai/ui/file-icon"
6+
import { getDirectory, getFilename } from "@opencode-ai/util/path"
7+
8+
export const DialogFileSelect: Component<{
9+
onOpenChange?: (open: boolean) => void
10+
onSelect?: (path: string) => void
11+
}> = (props) => {
12+
const local = useLocal()
13+
let closeButton!: HTMLButtonElement
14+
15+
return (
16+
<Dialog modal defaultOpen onOpenChange={props.onOpenChange}>
17+
<Dialog.Header>
18+
<Dialog.Title>Select file</Dialog.Title>
19+
<Dialog.CloseButton ref={closeButton} tabIndex={-1} />
20+
</Dialog.Header>
21+
<Dialog.Body>
22+
<List
23+
class="px-2.5"
24+
search={{ placeholder: "Search files", autofocus: true }}
25+
emptyMessage="No files found"
26+
items={local.file.searchFiles}
27+
key={(x) => x}
28+
onSelect={(x) => {
29+
if (x) {
30+
props.onSelect?.(x)
31+
}
32+
closeButton.click()
33+
}}
34+
>
35+
{(i) => (
36+
<div class="w-full flex items-center justify-between rounded-md">
37+
<div class="flex items-center gap-x-2 grow min-w-0">
38+
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
39+
<div class="flex items-center text-14-regular">
40+
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
41+
{getDirectory(i)}
42+
</span>
43+
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
44+
</div>
45+
</div>
46+
</div>
47+
)}
48+
</List>
49+
</Dialog.Body>
50+
</Dialog>
51+
)
52+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Component } from "solid-js"
2+
import { useLocal } from "@/context/local"
3+
import { useDialog } from "@/context/dialog"
4+
import { popularProviders } from "@/hooks/use-providers"
5+
import { Dialog } from "@opencode-ai/ui/dialog"
6+
import { List } from "@opencode-ai/ui/list"
7+
import { Switch } from "@opencode-ai/ui/switch"
8+
9+
export const DialogManageModels: Component = () => {
10+
const local = useLocal()
11+
const dialog = useDialog()
12+
13+
return (
14+
<Dialog
15+
modal
16+
defaultOpen
17+
onOpenChange={(open) => {
18+
if (!open) {
19+
dialog.clear()
20+
}
21+
}}
22+
>
23+
<Dialog.Header>
24+
<Dialog.Title>Manage models</Dialog.Title>
25+
<Dialog.CloseButton tabIndex={-1} />
26+
</Dialog.Header>
27+
<Dialog.Description>Customize which models appear in the model selector.</Dialog.Description>
28+
<Dialog.Body>
29+
<List
30+
class="px-2.5"
31+
search={{ placeholder: "Search models", autofocus: true }}
32+
emptyMessage="No model results"
33+
key={(x) => `${x?.provider?.id}:${x?.id}`}
34+
items={local.model.list()}
35+
filterKeys={["provider.name", "name", "id"]}
36+
sortBy={(a, b) => a.name.localeCompare(b.name)}
37+
groupBy={(x) => x.provider.name}
38+
sortGroupsBy={(a, b) => {
39+
const aProvider = a.items[0].provider.id
40+
const bProvider = b.items[0].provider.id
41+
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
42+
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
43+
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
44+
}}
45+
onSelect={(x) => {
46+
if (!x) return
47+
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !x.visible)
48+
}}
49+
>
50+
{(i) => (
51+
<div class="w-full flex items-center justify-between gap-x-2.5">
52+
<span>{i.name}</span>
53+
<Switch
54+
checked={!!i.visible}
55+
onChange={(checked) => {
56+
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
57+
}}
58+
/>
59+
</div>
60+
)}
61+
</List>
62+
</Dialog.Body>
63+
</Dialog>
64+
)
65+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Component, onCleanup, onMount, Show } from "solid-js"
2+
import { useLocal } from "@/context/local"
3+
import { useDialog } from "@/context/dialog"
4+
import { popularProviders, useProviders } from "@/hooks/use-providers"
5+
import { Button } from "@opencode-ai/ui/button"
6+
import { Tag } from "@opencode-ai/ui/tag"
7+
import { Dialog } from "@opencode-ai/ui/dialog"
8+
import { List, ListRef } from "@opencode-ai/ui/list"
9+
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
10+
import { IconName } from "@opencode-ai/ui/icons/provider"
11+
import { DialogSelectProvider } from "./dialog-select-provider"
12+
import { DialogConnect } from "./dialog-connect"
13+
14+
export const DialogModelUnpaid: Component = () => {
15+
const local = useLocal()
16+
const dialog = useDialog()
17+
const providers = useProviders()
18+
19+
let listRef: ListRef | undefined
20+
const handleKey = (e: KeyboardEvent) => {
21+
if (e.key === "Escape") return
22+
listRef?.onKeyDown(e)
23+
}
24+
25+
onMount(() => {
26+
document.addEventListener("keydown", handleKey)
27+
onCleanup(() => {
28+
document.removeEventListener("keydown", handleKey)
29+
})
30+
})
31+
32+
return (
33+
<Dialog
34+
modal
35+
defaultOpen
36+
onOpenChange={(open) => {
37+
if (!open) {
38+
dialog.clear()
39+
}
40+
}}
41+
>
42+
<Dialog.Header>
43+
<Dialog.Title>Select model</Dialog.Title>
44+
<Dialog.CloseButton tabIndex={-1} />
45+
</Dialog.Header>
46+
<Dialog.Body>
47+
<div class="flex flex-col gap-3 px-2.5">
48+
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
49+
<List
50+
ref={(ref) => (listRef = ref)}
51+
items={local.model.list}
52+
current={local.model.current()}
53+
key={(x) => `${x.provider.id}:${x.id}`}
54+
onSelect={(x) => {
55+
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
56+
recent: true,
57+
})
58+
dialog.clear()
59+
}}
60+
>
61+
{(i) => (
62+
<div class="w-full flex items-center gap-x-2.5">
63+
<span>{i.name}</span>
64+
<Tag>Free</Tag>
65+
<Show when={i.latest}>
66+
<Tag>Latest</Tag>
67+
</Show>
68+
</div>
69+
)}
70+
</List>
71+
<div />
72+
<div />
73+
</div>
74+
<div class="px-1.5 pb-1.5">
75+
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
76+
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
77+
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
78+
<div class="w-full">
79+
<List
80+
class="w-full"
81+
key={(x) => x?.id}
82+
items={providers.popular}
83+
activeIcon="plus-small"
84+
sortBy={(a, b) => {
85+
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
86+
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
87+
return a.name.localeCompare(b.name)
88+
}}
89+
onSelect={(x) => {
90+
if (!x) return
91+
dialog.replace(() => <DialogConnect provider={x.id} />)
92+
}}
93+
>
94+
{(i) => (
95+
<div class="w-full flex items-center gap-x-4">
96+
<ProviderIcon
97+
data-slot="list-item-extra-icon"
98+
id={i.id as IconName}
99+
// TODO: clean this up after we update icon in models.dev
100+
classList={{
101+
"text-icon-weak-base": true,
102+
"size-4 mx-0.5": i.id === "opencode",
103+
"size-5": i.id !== "opencode",
104+
}}
105+
/>
106+
<span>{i.name}</span>
107+
<Show when={i.id === "opencode"}>
108+
<Tag>Recommended</Tag>
109+
</Show>
110+
<Show when={i.id === "anthropic"}>
111+
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
112+
</Show>
113+
</div>
114+
)}
115+
</List>
116+
<Button
117+
variant="ghost"
118+
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
119+
icon="dot-grid"
120+
onClick={() => {
121+
dialog.replace(() => <DialogSelectProvider />)
122+
}}
123+
>
124+
View all providers
125+
</Button>
126+
</div>
127+
</div>
128+
</div>
129+
</div>
130+
</Dialog.Body>
131+
</Dialog>
132+
)
133+
}

0 commit comments

Comments
 (0)