summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-10 12:48:08 -0600
committerAdam <[email protected]>2025-12-10 15:17:03 -0600
commit91d743ef9a5c346fe17bb857db68dca92a6e9ba1 (patch)
treeeae37157434a8024df90ad55f08d51abc3390207
parent804ad5897f17cd5f002fbd0c124d5301205efcfb (diff)
downloadopencode-91d743ef9a5c346fe17bb857db68dca92a6e9ba1.tar.gz
opencode-91d743ef9a5c346fe17bb857db68dca92a6e9ba1.zip
wip(desktop): progress
-rw-r--r--packages/desktop/src/components/prompt-input.tsx5
-rw-r--r--packages/desktop/src/context/global-sync.tsx73
-rw-r--r--packages/desktop/src/context/layout.tsx67
-rw-r--r--packages/desktop/src/pages/layout.tsx38
-rw-r--r--packages/tauri/src-tauri/Cargo.lock34
-rw-r--r--packages/tauri/src-tauri/Cargo.toml2
-rw-r--r--packages/ui/src/components/avatar.tsx7
-rw-r--r--packages/ui/src/components/button.css18
-rw-r--r--packages/ui/src/components/select-dialog.css1
-rw-r--r--packages/ui/src/components/select-dialog.tsx6
10 files changed, 156 insertions, 95 deletions
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index 8579647da..97d27ee1e 100644
--- a/packages/desktop/src/components/prompt-input.tsx
+++ b/packages/desktop/src/components/prompt-input.tsx
@@ -483,6 +483,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<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">
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 58fc8c9cd..3e2b6bf7d 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -18,41 +18,9 @@ import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
-const PASTEL_COLORS = [
- "#FCEAFD", // pastel pink
- "#FFDFBA", // pastel peach
- "#FFFFBA", // pastel yellow
- "#BAFFC9", // pastel green
- "#EAF6FD", // pastel blue
- "#EFEAFD", // pastel lavender
- "#FEC8D8", // pastel rose
- "#D4F0F0", // pastel cyan
- "#FDF0EA", // pastel coral
- "#C1E1C1", // pastel mint
-]
-
-function pickAvailableColor(usedColors: Set<string>) {
- const available = PASTEL_COLORS.filter((c) => !usedColors.has(c))
- if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
- return available[Math.floor(Math.random() * available.length)]
-}
-
-async function ensureProjectColor(
- project: Project,
- sdk: ReturnType<typeof useGlobalSDK>,
- usedColors: Set<string>,
-): Promise<Project> {
- if (project.icon?.color) return project
- const color = pickAvailableColor(usedColors)
- usedColors.add(color)
- const updated = { ...project, icon: { ...project.icon, color } }
- sdk.client.project.update({ projectID: project.id, icon: { color } })
- return updated
-}
-
type State = {
ready: boolean
- provider: Provider[]
+ // provider: Provider[]
agent: Agent[]
project: string
config: Config
@@ -84,10 +52,12 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
projects: Project[]
+ providers: Provider[]
children: Record<string, State>
}>({
ready: false,
projects: [],
+ providers: [],
children: {},
})
@@ -100,7 +70,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
path: { state: "", config: "", worktree: "", directory: "", home: "" },
ready: false,
agent: [],
- provider: [],
+ // provider: [],
session: [],
session_status: {},
session_diff: {},
@@ -124,20 +94,17 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
if (directory === "global") {
switch (event.type) {
case "project.updated": {
- const usedColors = new Set(globalStore.projects.map((p) => p.icon?.color).filter(Boolean) as string[])
- ensureProjectColor(event.properties, sdk, usedColors).then((project) => {
- const result = Binary.search(globalStore.projects, project.id, (s) => s.id)
- if (result.found) {
- setGlobalStore("projects", result.index, reconcile(project))
- return
- }
- setGlobalStore(
- "projects",
- produce((draft) => {
- draft.splice(result.index, 0, project)
- }),
- )
- })
+ const result = Binary.search(globalStore.projects, event.properties.id, (s) => s.id)
+ if (result.found) {
+ setGlobalStore("projects", result.index, reconcile(event.properties))
+ return
+ }
+ setGlobalStore(
+ "projects",
+ produce((draft) => {
+ draft.splice(result.index, 0, event.properties)
+ }),
+ )
break
}
}
@@ -216,14 +183,16 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
Promise.all([
sdk.client.project.list().then(async (x) => {
- const filtered = x.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
- const usedColors = new Set(filtered.map((p) => p.icon?.color).filter(Boolean) as string[])
- const projects = await Promise.all(filtered.map((p) => ensureProjectColor(p, sdk, usedColors)))
setGlobalStore(
"projects",
- projects.sort((a, b) => a.id.localeCompare(b.id)),
+ 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 ?? [])
+ }),
]).then(() => setGlobalStore("ready", true))
return {
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 05a47c4eb..13c4679d6 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -4,6 +4,20 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
+import { Project } from "@opencode-ai/sdk/v2"
+
+const PASTEL_COLORS = [
+ "#FCEAFD", // pastel pink
+ "#FFDFBA", // pastel peach
+ "#FFFFBA", // pastel yellow
+ "#BAFFC9", // pastel green
+ "#EAF6FD", // pastel blue
+ "#EFEAFD", // pastel lavender
+ "#FEC8D8", // pastel rose
+ "#D4F0F0", // pastel cyan
+ "#FDF0EA", // pastel coral
+ "#C1E1C1", // pastel mint
+]
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
@@ -30,6 +44,42 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
)
+ function pickAvailableColor() {
+ const available = PASTEL_COLORS.filter((c) => !colors().has(c))
+ if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
+ return available[Math.floor(Math.random() * available.length)]
+ }
+
+ function enrich(project: { worktree: string; expanded: boolean }) {
+ const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
+ if (!metadata) return []
+ return [
+ {
+ ...project,
+ ...metadata,
+ },
+ ]
+ }
+
+ function colorize(project: Project & { expanded: boolean }) {
+ if (project.icon?.color) return project
+ const color = pickAvailableColor()
+ project.icon = { ...project.icon, color }
+ globalSdk.client.project.update({ projectID: project.id, icon: { color } })
+ return project
+ }
+
+ const enriched = createMemo(() => store.projects.flatMap(enrich))
+ const list = createMemo(() => enriched().flatMap(colorize))
+ const colors = createMemo(
+ () =>
+ new Set(
+ list()
+ .map((p) => p.icon?.color)
+ .filter(Boolean),
+ ),
+ )
+
async function loadProjectSessions(directory: string) {
const [, setStore] = globalSync.child(directory)
globalSdk.client.session.list({ directory }).then((x) => {
@@ -43,26 +93,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
onMount(() => {
Promise.all(
- store.projects.map(({ worktree }) => {
- return loadProjectSessions(worktree)
+ store.projects.map((project) => {
+ return loadProjectSessions(project.worktree)
}),
)
})
- function enrich(project: { worktree: string; expanded: boolean }) {
- const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
- if (!metadata) return []
- return [
- {
- ...project,
- ...metadata,
- },
- ]
- }
-
return {
projects: {
- list: createMemo(() => store.projects.flatMap(enrich)),
+ list,
open(directory: string) {
if (store.projects.find((x) => x.worktree === directory)) return
loadProjectSessions(directory)
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 4a17d01bd..3e0094756 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -29,6 +29,8 @@ import {
useDragDropContext,
} from "@thisbeyond/solid-dnd"
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
+import { SelectDialog } from "@opencode-ai/ui/select-dialog"
+import { Tag } from "@opencode-ai/ui/tag"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
@@ -44,11 +46,16 @@ 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())
+ })
+
function navigateToProject(directory: string | undefined) {
if (!directory) return
const lastSession = store.lastSession[directory]
@@ -550,6 +557,37 @@ 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}>
+ <SelectDialog
+ defaultOpen
+ title="Connect provider"
+ placeholder="Search providers"
+ key={(x) => x?.id}
+ items={providers()}
+ // 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
+ }
+ }
+ >
+ {(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>
</div>
)
diff --git a/packages/tauri/src-tauri/Cargo.lock b/packages/tauri/src-tauri/Cargo.lock
index 57d463355..f2e77a1e8 100644
--- a/packages/tauri/src-tauri/Cargo.lock
+++ b/packages/tauri/src-tauri/Cargo.lock
@@ -3,6 +3,23 @@
version = 4
[[package]]
+name = "OpenCode"
+version = "0.0.0"
+dependencies = [
+ "listeners",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-dialog",
+ "tauri-plugin-opener",
+ "tauri-plugin-process",
+ "tauri-plugin-shell",
+ "tauri-plugin-updater",
+ "tokio",
+]
+
+[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2501,23 +2518,6 @@ dependencies = [
]
[[package]]
-name = "opencode-desktop"
-version = "0.0.0"
-dependencies = [
- "listeners",
- "serde",
- "serde_json",
- "tauri",
- "tauri-build",
- "tauri-plugin-dialog",
- "tauri-plugin-opener",
- "tauri-plugin-process",
- "tauri-plugin-shell",
- "tauri-plugin-updater",
- "tokio",
-]
-
-[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/packages/tauri/src-tauri/Cargo.toml b/packages/tauri/src-tauri/Cargo.toml
index c6b0e409b..3d7bf654d 100644
--- a/packages/tauri/src-tauri/Cargo.toml
+++ b/packages/tauri/src-tauri/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "opencode-desktop"
+name = "OpenCode"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx
index 1ff3008ee..fb5798b08 100644
--- a/packages/ui/src/components/avatar.tsx
+++ b/packages/ui/src/components/avatar.tsx
@@ -9,22 +9,23 @@ export interface AvatarProps extends ComponentProps<"div"> {
export function Avatar(props: AvatarProps) {
const [split, rest] = splitProps(props, ["fallback", "src", "background", "size", "class", "classList", "style"])
+ const src = split.src // did this so i can zero it out to test fallback
return (
<div
{...rest}
data-component="avatar"
data-size={split.size || "normal"}
- data-has-image={split.src ? "" : undefined}
+ data-has-image={src ? "" : undefined}
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
style={{
...(typeof split.style === "object" ? split.style : {}),
- ...(!split.src && split.background ? { "--avatar-bg": split.background } : {}),
+ ...(!src && split.background ? { "--avatar-bg": split.background } : {}),
}}
>
- <Show when={split.src} fallback={split.fallback?.[0]}>
+ <Show when={src} fallback={split.fallback?.[0]}>
{(src) => <img src={src()} draggable={false} class="size-full object-cover" />}
</Show>
</div>
diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css
index 192c7b60c..f95317028 100644
--- a/packages/ui/src/components/button.css
+++ b/packages/ui/src/components/button.css
@@ -102,12 +102,20 @@
height: 24px;
padding: 0 6px;
&[data-icon] {
- padding: 0 8px 0 6px;
+ padding: 0 12px 0 4px;
}
font-size: var(--font-size-small);
line-height: var(--line-height-large);
gap: 6px;
+
+ /* text-12-medium */
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-style: normal;
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large); /* 166.667% */
+ letter-spacing: var(--letter-spacing-normal);
}
&[data-size="large"] {
@@ -115,17 +123,17 @@
padding: 0 8px;
&[data-icon] {
- padding: 0 8px 0 6px;
+ padding: 0 12px 0 8px;
}
gap: 8px;
- /* text-12-medium */
+ /* text-14-medium */
font-family: var(--font-family-sans);
- font-size: var(--font-size-small);
+ font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
- line-height: var(--line-height-large); /* 166.667% */
+ line-height: var(--line-height-large); /* 142.857% */
letter-spacing: var(--letter-spacing-normal);
}
diff --git a/packages/ui/src/components/select-dialog.css b/packages/ui/src/components/select-dialog.css
index 206eade0d..cc834f795 100644
--- a/packages/ui/src/components/select-dialog.css
+++ b/packages/ui/src/components/select-dialog.css
@@ -75,7 +75,6 @@
position: relative;
display: flex;
flex-direction: column;
- gap: 4px;
[data-slot="select-dialog-header"] {
display: flex;
diff --git a/packages/ui/src/components/select-dialog.tsx b/packages/ui/src/components/select-dialog.tsx
index 695791aad..b93993ad4 100644
--- a/packages/ui/src/components/select-dialog.tsx
+++ b/packages/ui/src/components/select-dialog.tsx
@@ -15,6 +15,7 @@ interface SelectDialogProps<T>
children: (item: T) => JSX.Element
onSelect?: (value: T | undefined) => void
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
+ actions?: JSX.Element
}
export function SelectDialog<T>(props: SelectDialogProps<T>) {
@@ -98,7 +99,8 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
<Dialog modal {...dialog} onOpenChange={handleOpenChange}>
<Dialog.Header>
<Dialog.Title>{others.title}</Dialog.Title>
- <Dialog.CloseButton ref={closeButton} tabIndex={-1} />
+ <Show when={others.actions}>{others.actions}</Show>
+ <Dialog.CloseButton ref={closeButton} tabIndex={-1} style={{ display: others.actions ? "none" : undefined }} />
</Dialog.Header>
<div data-slot="select-dialog-content">
<div data-component="select-dialog-input">
@@ -136,7 +138,7 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
fallback={
<div data-slot="select-dialog-empty-state">
<div data-slot="select-dialog-message">
- {props.emptyMessage ?? "No search results"} for{" "}
+ {props.emptyMessage ?? "No results"} for{" "}
<span data-slot="select-dialog-filter">&quot;{filter()}&quot;</span>
</div>
</div>