From 8d3eac2347c525039e96a540c29d6bb9cc26cc8f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Dec 2025 16:14:32 -0500 Subject: fix type --- packages/desktop/src/context/global-sync.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/desktop/src/context') diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx index 890801611..58fc8c9cd 100644 --- a/packages/desktop/src/context/global-sync.tsx +++ b/packages/desktop/src/context/global-sync.tsx @@ -97,7 +97,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple setGlobalStore("children", directory, { project: "", config: {}, - path: { state: "", config: "", worktree: "", directory: "" }, + path: { state: "", config: "", worktree: "", directory: "", home: "" }, ready: false, agent: [], provider: [], -- cgit v1.2.3 From 67a95c3cc8de5dc8e71f8af8efbaca4e7efdb4d6 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:05:27 -0600 Subject: wip(desktop): progress --- packages/desktop/src/context/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/desktop/src/context') diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index b7d1fabb5..05a47c4eb 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -66,7 +66,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( open(directory: string) { if (store.projects.find((x) => x.worktree === directory)) return loadProjectSessions(directory) - setStore("projects", (x) => [...x, { worktree: directory, expanded: true }]) + setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x]) }, close(directory: string) { setStore("projects", (x) => x.filter((x) => x.worktree !== directory)) -- cgit v1.2.3 From f20d6e855556693e33cddd51c837263c8846694d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 07:27:30 -0600 Subject: wip(desktop): progress --- packages/desktop/src/components/prompt-input.tsx | 3 +++ packages/desktop/src/context/local.tsx | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) (limited to 'packages/desktop/src/context') diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index fbb643e58..bbd638e44 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -489,6 +489,9 @@ export const PromptInput: Component = (props) => { Free + + Latest + )} diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 8223a36b9..58a65b0de 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -25,6 +25,7 @@ export type View = LocalFile["view"] export type LocalModel = Omit & { provider: Provider + latest?: boolean } export type ModelKey = { providerID: string; modelID: string } @@ -114,7 +115,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const list = createMemo(() => - sync.data.provider.flatMap((p) => Object.values(p.models).map((m) => ({ ...m, provider: p }) as LocalModel)), + 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, + ), + ), ) const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID) -- cgit v1.2.3 From 91d743ef9a5c346fe17bb857db68dca92a6e9ba1 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:48:08 -0600 Subject: wip(desktop): progress --- packages/desktop/src/components/prompt-input.tsx | 5 ++ packages/desktop/src/context/global-sync.tsx | 73 +++++++----------------- packages/desktop/src/context/layout.tsx | 67 +++++++++++++++++----- packages/desktop/src/pages/layout.tsx | 38 ++++++++++++ packages/tauri/src-tauri/Cargo.lock | 34 +++++------ packages/tauri/src-tauri/Cargo.toml | 2 +- packages/ui/src/components/avatar.tsx | 7 ++- packages/ui/src/components/button.css | 18 ++++-- packages/ui/src/components/select-dialog.css | 1 - packages/ui/src/components/select-dialog.tsx | 6 +- 10 files changed, 156 insertions(+), 95 deletions(-) (limited to 'packages/desktop/src/context') 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 = (props) => { } + actions={ + + } > {(i) => (
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) { - 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, - usedColors: Set, -): Promise { - 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 }>({ 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) {
{props.children}
+ + 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) => ( +
+ {i.name} + + Free + + + Latest + +
+ )} +
+
) 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 @@ -2,6 +2,23 @@ # It is not intended for manual editing. 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" @@ -2500,23 +2517,6 @@ dependencies = [ "pathdiff", ] -[[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" 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 (
- + {(src) => }
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 children: (item: T) => JSX.Element onSelect?: (value: T | undefined) => void onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void + actions?: JSX.Element } export function SelectDialog(props: SelectDialogProps) { @@ -98,7 +99,8 @@ export function SelectDialog(props: SelectDialogProps) { {others.title} - + {others.actions} +
@@ -136,7 +138,7 @@ export function SelectDialog(props: SelectDialogProps) { fallback={
- {props.emptyMessage ?? "No search results"} for{" "} + {props.emptyMessage ?? "No results"} for{" "} "{filter()}"
-- cgit v1.2.3 From 190fa4c87aa2b3f954a419f716add1fc29e4011e Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:48:08 -0600 Subject: wip(desktop): progress --- packages/desktop/src/components/prompt-input.tsx | 111 ++++++++++++--------- packages/desktop/src/context/global-sync.tsx | 26 ++--- packages/desktop/src/context/layout.tsx | 20 +++- packages/desktop/src/context/local.tsx | 38 +++---- packages/desktop/src/context/session.tsx | 2 +- packages/desktop/src/context/sync.tsx | 6 +- packages/desktop/src/pages/home.tsx | 4 +- packages/desktop/src/pages/layout.tsx | 90 +++++++++++------ packages/enterprise/src/routes/share/[shareID].tsx | 2 +- packages/ui/src/components/provider-icon.tsx | 6 +- packages/ui/src/components/select-dialog.css | 12 ++- packages/ui/src/components/select-dialog.tsx | 10 +- 12 files changed, 201 insertions(+), 126 deletions(-) (limited to 'packages/desktop/src/context') diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 97d27ee1e..985dbae8e 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -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 = (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 = (props) => { class="capitalize" variant="ghost" /> - `${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={ - - } - actions={ - - } - > - {(i) => ( -
- {i.name} - - Free - - - Latest - -
- )} -
+ + + { + 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={ + + } + > + {(i) => ( +
+ {i.name} + + Free + + + Latest + +
+ )} +
+
{ + const sdk = useGlobalSDK() const [globalStore, setGlobalStore] = createStore<{ ready: boolean - projects: Project[] - providers: Provider[] + project: Project[] + provider: ProviderListResponse children: Record }>({ 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)) diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index 13c4679d6..1de8550cb 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -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) + } + }, + }, } }, }) diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 58a65b0de..74d3ac364 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -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(() => { diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index 31004811b..db2b3af7c 100644 --- a/packages/desktop/src/context/session.tsx +++ b/packages/desktop/src/context/session.tsx @@ -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] ?? []) : [])) diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx index 85986c327..1a11cd599 100644 --- a/packages/desktop/src/context/sync.tsx +++ b/packages/desktop/src/context/sync.tsx @@ -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: { diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx index 4aac241e1..205ffd815 100644 --- a/packages/desktop/src/pages/home.tsx +++ b/packages/desktop/src/pages/home.tsx @@ -38,7 +38,7 @@ export default function Home() {
- 0}> + 0}>
Recent projects
@@ -50,7 +50,7 @@ export default function Home() {
    (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) .slice(0, 5)} > diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 3e0094756..2ea6c4ba0 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -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) {
- +
Getting started
@@ -493,7 +502,7 @@ export default function Layout(props: ParentProps) {
{props.children}
- + 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) => ( -
+
+ {i.name} - - Free + + Recommended - - Latest + +
Connect with Claude Pro/Max or API key
)} diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 15a36b2ff..1c593ca87 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -212,7 +212,7 @@ export default function () {
v{info().version}
- +
{model()?.name ?? modelID()}
diff --git a/packages/ui/src/components/provider-icon.tsx b/packages/ui/src/components/provider-icon.tsx index 924dcd25c..d653765a5 100644 --- a/packages/ui/src/components/provider-icon.tsx +++ b/packages/ui/src/components/provider-icon.tsx @@ -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 = (props) => { - const [local, rest] = splitProps(props, ["name", "class", "classList"]) + const [local, rest] = splitProps(props, ["id", "class", "classList"]) return ( = (props) => { [local.class ?? ""]: !!local.class, }} > - + ) } diff --git a/packages/ui/src/components/select-dialog.css b/packages/ui/src/components/select-dialog.css index cc834f795..f5687ad8e 100644 --- a/packages/ui/src/components/select-dialog.css +++ b/packages/ui/src/components/select-dialog.css @@ -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); + } } } } diff --git a/packages/ui/src/components/select-dialog.tsx b/packages/ui/src/components/select-dialog.tsx index b93993ad4..86f723225 100644 --- a/packages/ui/src/components/select-dialog.tsx +++ b/packages/ui/src/components/select-dialog.tsx @@ -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 onSelect?: (value: T | undefined) => void onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void actions?: JSX.Element + activeIcon?: IconProps["name"] } export function SelectDialog(props: SelectDialogProps) { @@ -165,7 +166,12 @@ export function SelectDialog(props: SelectDialogProps) { }} > {others.children(item)} - + + + + + {(icon) => } + )} -- cgit v1.2.3 From 56540f83125d8ec3fd6f26ac7edca7471c2aca3f Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:30:53 -0600 Subject: wip(desktop): progress --- packages/desktop/src/components/prompt-input.tsx | 6 ++++++ packages/desktop/src/context/layout.tsx | 12 +++--------- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'packages/desktop/src/context') diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 0672dfc85..22f2c1642 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -621,6 +621,12 @@ export const PromptInput: Component = (props) => {
)} +
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index 1de8550cb..5530ad28f 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -48,9 +48,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( open: undefined as undefined | "provider" | "model", }, }) + const usedColors = new Set() function pickAvailableColor() { - const available = PASTEL_COLORS.filter((c) => !colors().has(c)) + 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)] } @@ -69,6 +70,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( function colorize(project: Project & { expanded: boolean }) { if (project.icon?.color) return project const color = pickAvailableColor() + usedColors.add(color) project.icon = { ...project.icon, color } globalSdk.client.project.update({ projectID: project.id, icon: { color } }) return project @@ -76,14 +78,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( 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) -- cgit v1.2.3