mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
wip(desktop): progress
This commit is contained in:
parent
6b2ac20abc
commit
5442adb517
10 changed files with 145 additions and 73 deletions
|
|
@ -74,7 +74,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||
session_status: {},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
limit: 10,
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
node: [],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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"] {
|
||||
|
|
|
|||
|
|
@ -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(() =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue