wip(desktop): progress

This commit is contained in:
Adam 2025-12-09 11:08:55 -06:00
parent 6b2ac20abc
commit 5442adb517
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
10 changed files with 145 additions and 73 deletions

View file

@ -74,7 +74,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
session_status: {},
session_diff: {},
todo: {},
limit: 10,
limit: 5,
message: {},
part: {},
node: [],

View file

@ -1,14 +1,18 @@
import { createStore } from "solid-js/store"
import { createMemo } from "solid-js"
import { createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
projects: [] as { directory: string; expanded: boolean; lastSession?: string }[],
projects: [] as { directory: string; expanded: boolean }[],
sidebar: {
opened: false,
width: 280,
@ -26,11 +30,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
)
async function loadProjectSessions(directory: string) {
const [, setStore] = globalSync.child(directory)
globalSdk.client.session.list({ directory }).then((x) => {
const sessions = (x.data ?? [])
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, 5)
setStore("session", sessions)
})
}
onMount(() => {
Promise.all(
store.projects.map(({ directory }) => {
return loadProjectSessions(directory)
}),
)
})
return {
projects: {
list: createMemo(() => store.projects),
open(directory: string) {
if (store.projects.find((x) => x.directory === directory)) return
loadProjectSessions(directory)
setStore("projects", (x) => [...x, { directory, expanded: true }])
},
close(directory: string) {
@ -42,12 +66,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
collapse(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
},
lastSession(directory: string) {
return store.projects.find((x) => x.directory === directory)?.lastSession
},
setLastSession(directory: string, session: string | undefined) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, lastSession: session } : x)))
},
},
sidebar: {
opened: createMemo(() => store.sidebar.opened),

View file

@ -335,7 +335,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return {
node: async (path: string) => {
if (!store.node[path]) {
if (!store.node[path] || store.node[path].loaded === false) {
await init(path)
}
return store.node[path]

View file

@ -1,9 +1,9 @@
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createEffect, createMemo } from "solid-js"
import { batch, createEffect, createMemo, onMount } from "solid-js"
import { useSync } from "./sync"
import { makePersisted } from "@solid-primitives/storage"
import { TextSelection } from "./local"
import { TextSelection, useLocal } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
@ -25,6 +25,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const sdk = useSDK()
const params = useParams()
const sync = useSync()
const local = useLocal()
const name = createMemo(
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v2`,
)
@ -55,6 +56,14 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
},
)
onMount(() => {
store.tabs.all.forEach((tab) => {
if (tab.startsWith("file://")) {
local.file.open(tab.replace("file://", ""))
}
})
})
createEffect(() => {
if (!params.id) return
sync.session.sync(params.id)

View file

@ -1,32 +1,31 @@
import { createMemo, type ParentProps } from "solid-js"
import { createMemo, Show, type ParentProps } from "solid-js"
import { useParams } from "@solidjs/router"
import { SDKProvider } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
import { iife } from "@opencode-ai/util/iife"
export default function Layout(props: ParentProps) {
const params = useParams()
const sync = useGlobalSync()
const directory = createMemo(() => {
const decoded = base64Decode(params.dir!)
return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
return base64Decode(params.dir!)
})
return (
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {
const sync = useSync()
return (
<DataProvider data={sync.data} directory={directory()}>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
})}
</SyncProvider>
</SDKProvider>
<Show when={params.dir} keyed>
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {
const sync = useSync()
return (
<DataProvider data={sync.data} directory={directory()}>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
})}
</SyncProvider>
</SDKProvider>
</Show>
)
}

View file

@ -1,22 +1,38 @@
import { useGlobalSync } from "@/context/global-sync"
import { For, Match, Switch } from "solid-js"
import { For, Match, Show, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Logo } from "@opencode-ai/ui/logo"
import { useLayout } from "@/context/layout"
import { useNavigate } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/util/encode"
import { Icon } from "@opencode-ai/ui/icon"
import { usePlatform } from "@/context/platform"
export default function Home() {
const navigate = useNavigate()
const sync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const navigate = useNavigate()
function openProject(directory: string) {
layout.projects.open(directory)
navigate(`/${base64Encode(directory)}`)
}
async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory)
}
} else if (result) {
openProject(result)
}
}
return (
<div class="mx-auto mt-55">
<Logo class="w-xl opacity-12" />
@ -25,9 +41,11 @@ export default function Home() {
<div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">Recent projects</div>
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3">
Open project
</Button>
<Show when={platform.openDirectoryPickerDialog}>
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
Open project
</Button>
</Show>
</div>
<ol class="flex flex-col gap-2">
<For each={sync.data.projects.slice(0, 5)}>
@ -54,7 +72,11 @@ export default function Home() {
<div class="text-12-regular text-text-weak">Get started by opening a local project</div>
</div>
<div />
<Button class="px-3">Open project</Button>
<Show when={platform.openDirectoryPickerDialog}>
<Button class="px-3" onClick={chooseProject}>
Open project
</Button>
</Show>
</div>
</Match>
</Switch>

View file

@ -17,19 +17,27 @@ import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { createStore } from "solid-js/store"
export default function Layout(props: ParentProps) {
const navigate = useNavigate()
const [store, setStore] = createStore({
lastSession: {} as { [directory: string]: string },
})
const params = useParams()
const globalSync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const navigate = useNavigate()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
function navigateToProject(directory: string | undefined) {
if (!directory) return
navigate(`/${base64Encode(directory)}`)
const lastSession = store.lastSession[directory]
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
}
function navigateToSession(session: Session | undefined) {
@ -37,19 +45,36 @@ export default function Layout(props: ParentProps) {
navigate(`/${params.dir}/session/${session?.id}`)
}
function openProject(directory: string, navigate = true) {
layout.projects.open(directory)
if (navigate) navigateToProject(directory)
}
function closeProject(directory: string) {
layout.projects.close(directory)
navigate("/")
}
const handleOpenProject = async () => {
// layout.projects.open(dir.)
async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory, false)
}
navigateToProject(result[0])
} else if (result) {
openProject(result)
}
}
// createEffect(() => {
// if (!params.dir) return
// layout.projects.setLastSession(base64Decode(params.dir), params.id)
// })
createEffect(() => {
if (!params.dir || !params.id) return
const directory = base64Decode(params.dir)
setStore("lastSession", directory, params.id)
})
return (
<div class="relative h-screen flex flex-col">
@ -89,7 +114,7 @@ export default function Layout(props: ParentProps) {
<Select
options={sessions()}
current={currentSession()}
placeholder="Select session"
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
@ -97,9 +122,11 @@ export default function Layout(props: ParentProps) {
variant="ghost"
/>
</div>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
<Show when={currentSession()}>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Show>
</div>
<div class="flex items-center gap-4">
<Tooltip
@ -155,7 +182,7 @@ export default function Layout(props: ParentProps) {
onCollapse={layout.sidebar.close}
/>
</Show>
<div class="grow flex flex-col items-start self-stretch gap-4 p-2 min-h-0">
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
<Button
variant="ghost"
@ -187,7 +214,7 @@ export default function Layout(props: ParentProps) {
</Show>
</Button>
</Tooltip>
<div class="size-full min-w-8 flex flex-col gap-2 grow min-h-0 overflow-y-auto no-scrollbar">
<div class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar">
<For each={layout.projects.list()}>
{(project) => {
const [store] = globalSync.child(project.directory)
@ -196,7 +223,7 @@ export default function Layout(props: ParentProps) {
return (
<Switch>
<Match when={layout.sidebar.opened()}>
<Collapsible variant="ghost" defaultOpen class="gap-2">
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0">
<Button
as={"div"}
variant="ghost"
@ -232,7 +259,7 @@ export default function Layout(props: ParentProps) {
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<Tooltip placement="bottom" value="New session">
<Tooltip placement="top" value="New session">
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
</Tooltip>
</div>
@ -300,11 +327,11 @@ export default function Layout(props: ParentProps) {
<Match when={true}>
<Tooltip placement="right" value={project.directory}>
<Button
as={A}
href={`${slug()}/session`}
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none"
data-selected={project.directory === currentDirectory()}
onClick={() => navigateToProject(project.directory)}
>
<div class="size-6 shrink-0 inset-0">
<Avatar fallback={name()} background="var(--surface-info-base)" class="size-full" />
@ -319,18 +346,19 @@ export default function Layout(props: ParentProps) {
</div>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
disabled
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={handleOpenProject}
>
<Show when={layout.sidebar.opened()}>Open project</Show>
</Button>
</Tooltip>
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={chooseProject}
>
<Show when={layout.sidebar.opened()}>Open project</Show>
</Button>
</Tooltip>
</Show>
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
<Button
disabled

View file

@ -220,7 +220,6 @@ export default function Page() {
onTabClose: (tab: string) => void
}): JSX.Element => {
const sortable = createSortable(props.tab)
const [file] = createResource(
() => props.tab,
async (tab) => {
@ -230,7 +229,6 @@ export default function Page() {
return undefined
},
)
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
@ -576,8 +574,7 @@ export default function Page() {
onOpenChange={(open) => setStore("fileSelectOpen", open)}
onSelect={(x) => {
if (x) {
local.file.open(x)
return session.layout.openTab("file://" + x)
return local.file.open(x).then(() => session.layout.openTab("file://" + x))
}
return undefined
}}

View file

@ -54,6 +54,9 @@
opacity: 0.7;
cursor: not-allowed;
}
&[data-selected="true"]:not(:disabled) {
background-color: var(--surface-raised-base-hover);
}
}
&[data-variant="secondary"] {

View file

@ -1,7 +1,6 @@
import { AssistantMessage } from "@opencode-ai/sdk"
import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { createEffect, createMemo, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js"
@ -31,9 +30,6 @@ export function SessionTurn(
) {
const data = useData()
const diffComponent = useDiffComponent()
const match = Binary.search(data.store.session, props.sessionID, (s) => s.id)
if (!match.found) throw new Error(`Session ${props.sessionID} not found`)
const sanitizer = createMemo(() => (data.directory ? new RegExp(`${data.directory}/`, "g") : undefined))
const messages = createMemo(() => (props.sessionID ? (data.store.message[props.sessionID] ?? []) : []))
const userMessages = createMemo(() =>