diff options
| author | Adam <[email protected]> | 2025-12-10 12:48:08 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-10 15:17:03 -0600 |
| commit | 91d743ef9a5c346fe17bb857db68dca92a6e9ba1 (patch) | |
| tree | eae37157434a8024df90ad55f08d51abc3390207 | |
| parent | 804ad5897f17cd5f002fbd0c124d5301205efcfb (diff) | |
| download | opencode-91d743ef9a5c346fe17bb857db68dca92a6e9ba1.tar.gz opencode-91d743ef9a5c346fe17bb857db68dca92a6e9ba1.zip | |
wip(desktop): progress
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 5 | ||||
| -rw-r--r-- | packages/desktop/src/context/global-sync.tsx | 73 | ||||
| -rw-r--r-- | packages/desktop/src/context/layout.tsx | 67 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 38 | ||||
| -rw-r--r-- | packages/tauri/src-tauri/Cargo.lock | 34 | ||||
| -rw-r--r-- | packages/tauri/src-tauri/Cargo.toml | 2 | ||||
| -rw-r--r-- | packages/ui/src/components/avatar.tsx | 7 | ||||
| -rw-r--r-- | packages/ui/src/components/button.css | 18 | ||||
| -rw-r--r-- | packages/ui/src/components/select-dialog.css | 1 | ||||
| -rw-r--r-- | packages/ui/src/components/select-dialog.tsx | 6 |
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">"{filter()}"</span> </div> </div> |
