perf(app): don't remount directory layout
This commit is contained in:
@@ -32,8 +32,8 @@ export function DialogSelectFile() {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
const view = createMemo(() => layout.view(sessionKey()))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||||
const [grouped, setGrouped] = createSignal(false)
|
const [grouped, setGrouped] = createSignal(false)
|
||||||
const common = [
|
const common = [
|
||||||
|
|||||||
@@ -167,8 +167,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
const view = createMemo(() => layout.view(sessionKey()))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
|
||||||
const recent = createMemo(() => {
|
const recent = createMemo(() => {
|
||||||
const all = tabs().all()
|
const all = tabs().all()
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
|
|
||||||
const variant = createMemo(() => props.variant ?? "button")
|
const variant = createMemo(() => props.variant ?? "button")
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
const view = createMemo(() => layout.view(sessionKey()))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||||
|
|
||||||
const cost = createMemo(() => {
|
const cost = createMemo(() => {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function SessionHeader() {
|
|||||||
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
||||||
const showReview = createMemo(() => !!currentSession())
|
const showReview = createMemo(() => !!currentSession())
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const view = createMemo(() => layout.view(sessionKey()))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
share: false,
|
share: false,
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
|
const scope = createMemo(() => sdk.directory)
|
||||||
|
|
||||||
const directory = createMemo(() => sync.data.path.directory)
|
const directory = createMemo(() => sync.data.path.directory)
|
||||||
|
|
||||||
function normalize(input: string) {
|
function normalize(input: string) {
|
||||||
@@ -234,6 +236,12 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
file: {},
|
file: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
scope()
|
||||||
|
inflight.clear()
|
||||||
|
setStore("file", {})
|
||||||
|
})
|
||||||
|
|
||||||
const viewCache = new Map<string, ViewCacheEntry>()
|
const viewCache = new Map<string, ViewCacheEntry>()
|
||||||
|
|
||||||
const disposeViews = () => {
|
const disposeViews = () => {
|
||||||
@@ -284,12 +292,16 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
const path = normalize(input)
|
const path = normalize(input)
|
||||||
if (!path) return Promise.resolve()
|
if (!path) return Promise.resolve()
|
||||||
|
|
||||||
|
const directory = scope()
|
||||||
|
const key = `${directory}\n${path}`
|
||||||
|
const client = sdk.client
|
||||||
|
|
||||||
ensure(path)
|
ensure(path)
|
||||||
|
|
||||||
const current = store.file[path]
|
const current = store.file[path]
|
||||||
if (!options?.force && current?.loaded) return Promise.resolve()
|
if (!options?.force && current?.loaded) return Promise.resolve()
|
||||||
|
|
||||||
const pending = inflight.get(path)
|
const pending = inflight.get(key)
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
setStore(
|
setStore(
|
||||||
@@ -301,9 +313,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const promise = sdk.client.file
|
const promise = client.file
|
||||||
.read({ path })
|
.read({ path })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
|
if (scope() !== directory) return
|
||||||
setStore(
|
setStore(
|
||||||
"file",
|
"file",
|
||||||
path,
|
path,
|
||||||
@@ -315,6 +328,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
if (scope() !== directory) return
|
||||||
setStore(
|
setStore(
|
||||||
"file",
|
"file",
|
||||||
path,
|
path,
|
||||||
@@ -330,10 +344,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
inflight.delete(path)
|
inflight.delete(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
inflight.set(path, promise)
|
inflight.set(key, promise)
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
import { batch, createEffect, createMemo, on, onCleanup, onMount, type Accessor } from "solid-js"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useGlobalSync } from "./global-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
@@ -432,10 +432,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
setStore("mobileSidebar", "opened", (x) => !x)
|
setStore("mobileSidebar", "opened", (x) => !x)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
view(sessionKey: string) {
|
view(sessionKey: string | Accessor<string>) {
|
||||||
touch(sessionKey)
|
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||||
scroll.seed(sessionKey)
|
|
||||||
const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} })
|
touch(key())
|
||||||
|
scroll.seed(key())
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
key,
|
||||||
|
(value) => {
|
||||||
|
touch(value)
|
||||||
|
scroll.seed(value)
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
|
||||||
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
|
||||||
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
|
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
|
||||||
|
|
||||||
@@ -465,10 +479,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
scroll(tab: string) {
|
scroll(tab: string) {
|
||||||
return scroll.scroll(sessionKey, tab)
|
return scroll.scroll(key(), tab)
|
||||||
},
|
},
|
||||||
setScroll(tab: string, pos: SessionScroll) {
|
setScroll(tab: string, pos: SessionScroll) {
|
||||||
scroll.setScroll(sessionKey, tab, pos)
|
scroll.setScroll(key(), tab, pos)
|
||||||
},
|
},
|
||||||
terminal: {
|
terminal: {
|
||||||
opened: terminalOpened,
|
opened: terminalOpened,
|
||||||
@@ -497,9 +511,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
review: {
|
review: {
|
||||||
open: createMemo(() => s().reviewOpen),
|
open: createMemo(() => s().reviewOpen),
|
||||||
setOpen(open: string[]) {
|
setOpen(open: string[]) {
|
||||||
const current = store.sessionView[sessionKey]
|
const session = key()
|
||||||
|
const current = store.sessionView[session]
|
||||||
if (!current) {
|
if (!current) {
|
||||||
setStore("sessionView", sessionKey, {
|
setStore("sessionView", session, {
|
||||||
scroll: {},
|
scroll: {},
|
||||||
reviewOpen: open,
|
reviewOpen: open,
|
||||||
})
|
})
|
||||||
@@ -507,93 +522,111 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (same(current.reviewOpen, open)) return
|
if (same(current.reviewOpen, open)) return
|
||||||
setStore("sessionView", sessionKey, "reviewOpen", open)
|
setStore("sessionView", session, "reviewOpen", open)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tabs(sessionKey: string) {
|
tabs(sessionKey: string | Accessor<string>) {
|
||||||
touch(sessionKey)
|
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
|
||||||
const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
|
|
||||||
|
touch(key())
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
key,
|
||||||
|
(value) => {
|
||||||
|
touch(value)
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
|
||||||
return {
|
return {
|
||||||
tabs,
|
tabs,
|
||||||
active: createMemo(() => tabs().active),
|
active: createMemo(() => tabs().active),
|
||||||
all: createMemo(() => tabs().all),
|
all: createMemo(() => tabs().all),
|
||||||
setActive(tab: string | undefined) {
|
setActive(tab: string | undefined) {
|
||||||
if (!store.sessionTabs[sessionKey]) {
|
const session = key()
|
||||||
setStore("sessionTabs", sessionKey, { all: [], active: tab })
|
if (!store.sessionTabs[session]) {
|
||||||
|
setStore("sessionTabs", session, { all: [], active: tab })
|
||||||
} else {
|
} else {
|
||||||
setStore("sessionTabs", sessionKey, "active", tab)
|
setStore("sessionTabs", session, "active", tab)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setAll(all: string[]) {
|
setAll(all: string[]) {
|
||||||
if (!store.sessionTabs[sessionKey]) {
|
const session = key()
|
||||||
setStore("sessionTabs", sessionKey, { all, active: undefined })
|
if (!store.sessionTabs[session]) {
|
||||||
|
setStore("sessionTabs", session, { all, active: undefined })
|
||||||
} else {
|
} else {
|
||||||
setStore("sessionTabs", sessionKey, "all", all)
|
setStore("sessionTabs", session, "all", all)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async open(tab: string) {
|
async open(tab: string) {
|
||||||
const current = store.sessionTabs[sessionKey] ?? { all: [] }
|
const session = key()
|
||||||
|
const current = store.sessionTabs[session] ?? { all: [] }
|
||||||
|
|
||||||
if (tab === "review") {
|
if (tab === "review") {
|
||||||
if (!store.sessionTabs[sessionKey]) {
|
if (!store.sessionTabs[session]) {
|
||||||
setStore("sessionTabs", sessionKey, { all: [], active: tab })
|
setStore("sessionTabs", session, { all: [], active: tab })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("sessionTabs", sessionKey, "active", tab)
|
setStore("sessionTabs", session, "active", tab)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab === "context") {
|
if (tab === "context") {
|
||||||
const all = [tab, ...current.all.filter((x) => x !== tab)]
|
const all = [tab, ...current.all.filter((x) => x !== tab)]
|
||||||
if (!store.sessionTabs[sessionKey]) {
|
if (!store.sessionTabs[session]) {
|
||||||
setStore("sessionTabs", sessionKey, { all, active: tab })
|
setStore("sessionTabs", session, { all, active: tab })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("sessionTabs", sessionKey, "all", all)
|
setStore("sessionTabs", session, "all", all)
|
||||||
setStore("sessionTabs", sessionKey, "active", tab)
|
setStore("sessionTabs", session, "active", tab)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!current.all.includes(tab)) {
|
if (!current.all.includes(tab)) {
|
||||||
if (!store.sessionTabs[sessionKey]) {
|
if (!store.sessionTabs[session]) {
|
||||||
setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
|
setStore("sessionTabs", session, { all: [tab], active: tab })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
|
setStore("sessionTabs", session, "all", [...current.all, tab])
|
||||||
setStore("sessionTabs", sessionKey, "active", tab)
|
setStore("sessionTabs", session, "active", tab)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!store.sessionTabs[sessionKey]) {
|
if (!store.sessionTabs[session]) {
|
||||||
setStore("sessionTabs", sessionKey, { all: current.all, active: tab })
|
setStore("sessionTabs", session, { all: current.all, active: tab })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("sessionTabs", sessionKey, "active", tab)
|
setStore("sessionTabs", session, "active", tab)
|
||||||
},
|
},
|
||||||
close(tab: string) {
|
close(tab: string) {
|
||||||
const current = store.sessionTabs[sessionKey]
|
const session = key()
|
||||||
|
const current = store.sessionTabs[session]
|
||||||
if (!current) return
|
if (!current) return
|
||||||
|
|
||||||
const all = current.all.filter((x) => x !== tab)
|
const all = current.all.filter((x) => x !== tab)
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("sessionTabs", sessionKey, "all", all)
|
setStore("sessionTabs", session, "all", all)
|
||||||
if (current.active !== tab) return
|
if (current.active !== tab) return
|
||||||
|
|
||||||
const index = current.all.findIndex((f) => f === tab)
|
const index = current.all.findIndex((f) => f === tab)
|
||||||
const next = all[index - 1] ?? all[0]
|
const next = all[index - 1] ?? all[0]
|
||||||
setStore("sessionTabs", sessionKey, "active", next)
|
setStore("sessionTabs", session, "active", next)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
move(tab: string, to: number) {
|
move(tab: string, to: number) {
|
||||||
const current = store.sessionTabs[sessionKey]
|
const session = key()
|
||||||
|
const current = store.sessionTabs[session]
|
||||||
if (!current) return
|
if (!current) return
|
||||||
const index = current.all.findIndex((f) => f === tab)
|
const index = current.all.findIndex((f) => f === tab)
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
setStore(
|
setStore(
|
||||||
"sessionTabs",
|
"sessionTabs",
|
||||||
sessionKey,
|
session,
|
||||||
"all",
|
"all",
|
||||||
produce((opened) => {
|
produce((opened) => {
|
||||||
opened.splice(to, 0, opened.splice(index, 1)[0])
|
opened.splice(to, 0, opened.splice(index, 1)[0])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { batch, createMemo, onCleanup } from "solid-js"
|
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||||
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
@@ -338,6 +338,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
|
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const scope = createMemo(() => sdk.directory)
|
||||||
|
createEffect(() => {
|
||||||
|
scope()
|
||||||
|
setStore("node", {})
|
||||||
|
})
|
||||||
|
|
||||||
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
|
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
|
||||||
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
|
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
|
||||||
|
|
||||||
@@ -394,10 +400,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
|
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
|
||||||
|
|
||||||
const load = async (path: string) => {
|
const load = async (path: string) => {
|
||||||
|
const directory = scope()
|
||||||
|
const client = sdk.client
|
||||||
const relativePath = relative(path)
|
const relativePath = relative(path)
|
||||||
await sdk.client.file
|
await client.file
|
||||||
.read({ path: relativePath })
|
.read({ path: relativePath })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
|
if (scope() !== directory) return
|
||||||
if (!store.node[relativePath]) return
|
if (!store.node[relativePath]) return
|
||||||
setStore(
|
setStore(
|
||||||
"node",
|
"node",
|
||||||
@@ -409,6 +418,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
if (scope() !== directory) return
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: language.t("toast.file.loadFailed.title"),
|
title: language.t("toast.file.loadFailed.title"),
|
||||||
@@ -453,9 +463,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const list = async (path: string) => {
|
const list = async (path: string) => {
|
||||||
return sdk.client.file
|
const directory = scope()
|
||||||
|
const client = sdk.client
|
||||||
|
return client.file
|
||||||
.list({ path: path + "/" })
|
.list({ path: path + "/" })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
|
if (scope() !== directory) return
|
||||||
setStore(
|
setStore(
|
||||||
"node",
|
"node",
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||||
import { onCleanup } from "solid-js"
|
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { usePlatform } from "./platform"
|
import { usePlatform } from "./platform"
|
||||||
|
|
||||||
@@ -10,22 +10,39 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||||||
init: (props: { directory: string }) => {
|
init: (props: { directory: string }) => {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const sdk = createOpencodeClient({
|
|
||||||
baseUrl: globalSDK.url,
|
const directory = createMemo(() => props.directory)
|
||||||
fetch: platform.fetch,
|
const client = createMemo(() =>
|
||||||
directory: props.directory,
|
createOpencodeClient({
|
||||||
throwOnError: true,
|
baseUrl: globalSDK.url,
|
||||||
})
|
fetch: platform.fetch,
|
||||||
|
directory: directory(),
|
||||||
|
throwOnError: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const emitter = createGlobalEmitter<{
|
const emitter = createGlobalEmitter<{
|
||||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const unsub = globalSDK.event.on(props.directory, (event) => {
|
createEffect(() => {
|
||||||
emitter.emit(event.type, event)
|
const unsub = globalSDK.event.on(directory(), (event) => {
|
||||||
|
emitter.emit(event.type, event)
|
||||||
|
})
|
||||||
|
onCleanup(unsub)
|
||||||
})
|
})
|
||||||
onCleanup(unsub)
|
|
||||||
|
|
||||||
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
|
return {
|
||||||
|
get directory() {
|
||||||
|
return directory()
|
||||||
|
},
|
||||||
|
get client() {
|
||||||
|
return client()
|
||||||
|
},
|
||||||
|
event: emitter,
|
||||||
|
get url() {
|
||||||
|
return globalSDK.url
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,13 +7,20 @@ import { useGlobalSync } from "./global-sync"
|
|||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
|
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
||||||
|
|
||||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||||
name: "Sync",
|
name: "Sync",
|
||||||
init: () => {
|
init: () => {
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const [store, setStore] = globalSync.child(sdk.directory)
|
|
||||||
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
type Child = ReturnType<(typeof globalSync)["child"]>
|
||||||
|
type Store = Child[0]
|
||||||
|
type Setter = Child[1]
|
||||||
|
|
||||||
|
const current = createMemo(() => globalSync.child(sdk.directory))
|
||||||
|
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||||
const chunk = 400
|
const chunk = 400
|
||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
const inflightDiff = new Map<string, Promise<void>>()
|
const inflightDiff = new Map<string, Promise<void>>()
|
||||||
@@ -25,6 +32,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const getSession = (sessionID: string) => {
|
const getSession = (sessionID: string) => {
|
||||||
|
const store = current()[0]
|
||||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||||
if (match.found) return store.session[match.index]
|
if (match.found) return store.session[match.index]
|
||||||
return undefined
|
return undefined
|
||||||
@@ -35,22 +43,30 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
return Math.ceil(count / chunk) * chunk
|
return Math.ceil(count / chunk) * chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateMessages = (sessionID: string) => {
|
const hydrateMessages = (directory: string, store: Store, sessionID: string) => {
|
||||||
if (meta.limit[sessionID] !== undefined) return
|
const key = keyFor(directory, sessionID)
|
||||||
|
if (meta.limit[key] !== undefined) return
|
||||||
|
|
||||||
const messages = store.message[sessionID]
|
const messages = store.message[sessionID]
|
||||||
if (!messages) return
|
if (!messages) return
|
||||||
|
|
||||||
const limit = limitFor(messages.length)
|
const limit = limitFor(messages.length)
|
||||||
setMeta("limit", sessionID, limit)
|
setMeta("limit", key, limit)
|
||||||
setMeta("complete", sessionID, messages.length < limit)
|
setMeta("complete", key, messages.length < limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadMessages = async (sessionID: string, limit: number) => {
|
const loadMessages = async (input: {
|
||||||
if (meta.loading[sessionID]) return
|
directory: string
|
||||||
|
client: typeof sdk.client
|
||||||
|
setStore: Setter
|
||||||
|
sessionID: string
|
||||||
|
limit: number
|
||||||
|
}) => {
|
||||||
|
const key = keyFor(input.directory, input.sessionID)
|
||||||
|
if (meta.loading[key]) return
|
||||||
|
|
||||||
setMeta("loading", sessionID, true)
|
setMeta("loading", key, true)
|
||||||
await retry(() => sdk.client.session.messages({ sessionID, limit }))
|
await retry(() => input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }))
|
||||||
.then((messages) => {
|
.then((messages) => {
|
||||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||||
const next = items
|
const next = items
|
||||||
@@ -60,10 +76,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
.sort((a, b) => a.id.localeCompare(b.id))
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("message", sessionID, reconcile(next, { key: "id" }))
|
input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
|
||||||
|
|
||||||
for (const message of items) {
|
for (const message of items) {
|
||||||
setStore(
|
input.setStore(
|
||||||
"part",
|
"part",
|
||||||
message.info.id,
|
message.info.id,
|
||||||
reconcile(
|
reconcile(
|
||||||
@@ -76,25 +92,32 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setMeta("limit", sessionID, limit)
|
setMeta("limit", key, input.limit)
|
||||||
setMeta("complete", sessionID, next.length < limit)
|
setMeta("complete", key, next.length < input.limit)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setMeta("loading", sessionID, false)
|
setMeta("loading", key, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const set: (...args: Parameters<Setter>) => ReturnType<Setter> = (...args) => {
|
||||||
|
return current()[1](...args)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: store,
|
get data() {
|
||||||
set: setStore,
|
return current()[0]
|
||||||
|
},
|
||||||
|
set,
|
||||||
get status() {
|
get status() {
|
||||||
return store.status
|
return current()[0].status
|
||||||
},
|
},
|
||||||
get ready() {
|
get ready() {
|
||||||
return store.status !== "loading"
|
return current()[0].status !== "loading"
|
||||||
},
|
},
|
||||||
get project() {
|
get project() {
|
||||||
|
const store = current()[0]
|
||||||
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
|
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
|
||||||
if (match.found) return globalSync.data.project[match.index]
|
if (match.found) return globalSync.data.project[match.index]
|
||||||
return undefined
|
return undefined
|
||||||
@@ -116,7 +139,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
model: input.model,
|
model: input.model,
|
||||||
}
|
}
|
||||||
setStore(
|
current()[1](
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const messages = draft.message[input.sessionID]
|
const messages = draft.message[input.sessionID]
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
@@ -133,20 +156,28 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
async sync(sessionID: string) {
|
async sync(sessionID: string) {
|
||||||
const hasSession = getSession(sessionID) !== undefined
|
const directory = sdk.directory
|
||||||
hydrateMessages(sessionID)
|
const client = sdk.client
|
||||||
|
const [store, setStore] = globalSync.child(directory)
|
||||||
|
const hasSession = (() => {
|
||||||
|
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||||
|
return match.found
|
||||||
|
})()
|
||||||
|
|
||||||
|
hydrateMessages(directory, store, sessionID)
|
||||||
|
|
||||||
const hasMessages = store.message[sessionID] !== undefined
|
const hasMessages = store.message[sessionID] !== undefined
|
||||||
if (hasSession && hasMessages) return
|
if (hasSession && hasMessages) return
|
||||||
|
|
||||||
const pending = inflight.get(sessionID)
|
const key = keyFor(directory, sessionID)
|
||||||
|
const pending = inflight.get(key)
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
const limit = meta.limit[sessionID] ?? chunk
|
const limit = meta.limit[key] ?? chunk
|
||||||
|
|
||||||
const sessionReq = hasSession
|
const sessionReq = hasSession
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
: retry(() => sdk.client.session.get({ sessionID })).then((session) => {
|
: retry(() => client.session.get({ sessionID })).then((session) => {
|
||||||
const data = session.data
|
const data = session.data
|
||||||
if (!data) return
|
if (!data) return
|
||||||
setStore(
|
setStore(
|
||||||
@@ -162,72 +193,104 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const messagesReq = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit)
|
const messagesReq = hasMessages
|
||||||
|
? Promise.resolve()
|
||||||
|
: loadMessages({
|
||||||
|
directory,
|
||||||
|
client,
|
||||||
|
setStore,
|
||||||
|
sessionID,
|
||||||
|
limit,
|
||||||
|
})
|
||||||
|
|
||||||
const promise = Promise.all([sessionReq, messagesReq])
|
const promise = Promise.all([sessionReq, messagesReq])
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
inflight.delete(sessionID)
|
inflight.delete(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
inflight.set(sessionID, promise)
|
inflight.set(key, promise)
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
async diff(sessionID: string) {
|
async diff(sessionID: string) {
|
||||||
|
const directory = sdk.directory
|
||||||
|
const client = sdk.client
|
||||||
|
const [store, setStore] = globalSync.child(directory)
|
||||||
if (store.session_diff[sessionID] !== undefined) return
|
if (store.session_diff[sessionID] !== undefined) return
|
||||||
|
|
||||||
const pending = inflightDiff.get(sessionID)
|
const key = keyFor(directory, sessionID)
|
||||||
|
const pending = inflightDiff.get(key)
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
const promise = retry(() => sdk.client.session.diff({ sessionID }))
|
const promise = retry(() => client.session.diff({ sessionID }))
|
||||||
.then((diff) => {
|
.then((diff) => {
|
||||||
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
inflightDiff.delete(sessionID)
|
inflightDiff.delete(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
inflightDiff.set(sessionID, promise)
|
inflightDiff.set(key, promise)
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
async todo(sessionID: string) {
|
async todo(sessionID: string) {
|
||||||
|
const directory = sdk.directory
|
||||||
|
const client = sdk.client
|
||||||
|
const [store, setStore] = globalSync.child(directory)
|
||||||
if (store.todo[sessionID] !== undefined) return
|
if (store.todo[sessionID] !== undefined) return
|
||||||
|
|
||||||
const pending = inflightTodo.get(sessionID)
|
const key = keyFor(directory, sessionID)
|
||||||
|
const pending = inflightTodo.get(key)
|
||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
const promise = retry(() => sdk.client.session.todo({ sessionID }))
|
const promise = retry(() => client.session.todo({ sessionID }))
|
||||||
.then((todo) => {
|
.then((todo) => {
|
||||||
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
|
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
inflightTodo.delete(sessionID)
|
inflightTodo.delete(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
inflightTodo.set(sessionID, promise)
|
inflightTodo.set(key, promise)
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
more(sessionID: string) {
|
more(sessionID: string) {
|
||||||
|
const store = current()[0]
|
||||||
|
const key = keyFor(sdk.directory, sessionID)
|
||||||
if (store.message[sessionID] === undefined) return false
|
if (store.message[sessionID] === undefined) return false
|
||||||
if (meta.limit[sessionID] === undefined) return false
|
if (meta.limit[key] === undefined) return false
|
||||||
if (meta.complete[sessionID]) return false
|
if (meta.complete[key]) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
loading(sessionID: string) {
|
loading(sessionID: string) {
|
||||||
return meta.loading[sessionID] ?? false
|
const key = keyFor(sdk.directory, sessionID)
|
||||||
|
return meta.loading[key] ?? false
|
||||||
},
|
},
|
||||||
async loadMore(sessionID: string, count = chunk) {
|
async loadMore(sessionID: string, count = chunk) {
|
||||||
if (meta.loading[sessionID]) return
|
const directory = sdk.directory
|
||||||
if (meta.complete[sessionID]) return
|
const client = sdk.client
|
||||||
|
const [, setStore] = globalSync.child(directory)
|
||||||
|
const key = keyFor(directory, sessionID)
|
||||||
|
if (meta.loading[key]) return
|
||||||
|
if (meta.complete[key]) return
|
||||||
|
|
||||||
const current = meta.limit[sessionID] ?? chunk
|
const currentLimit = meta.limit[key] ?? chunk
|
||||||
await loadMessages(sessionID, current + count)
|
await loadMessages({
|
||||||
|
directory,
|
||||||
|
client,
|
||||||
|
setStore,
|
||||||
|
sessionID,
|
||||||
|
limit: currentLimit + count,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fetch: async (count = 10) => {
|
fetch: async (count = 10) => {
|
||||||
|
const directory = sdk.directory
|
||||||
|
const client = sdk.client
|
||||||
|
const [store, setStore] = globalSync.child(directory)
|
||||||
setStore("limit", (x) => x + count)
|
setStore("limit", (x) => x + count)
|
||||||
await sdk.client.session.list().then((x) => {
|
await client.session.list().then((x) => {
|
||||||
const sessions = (x.data ?? [])
|
const sessions = (x.data ?? [])
|
||||||
.filter((s) => !!s?.id)
|
.filter((s) => !!s?.id)
|
||||||
.slice()
|
.slice()
|
||||||
@@ -236,9 +299,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
more: createMemo(() => store.session.length >= store.limit),
|
more: createMemo(() => current()[0].session.length >= current()[0].limit),
|
||||||
archive: async (sessionID: string) => {
|
archive: async (sessionID: string) => {
|
||||||
await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
|
const directory = sdk.directory
|
||||||
|
const client = sdk.client
|
||||||
|
const [, setStore] = globalSync.child(directory)
|
||||||
|
await client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
||||||
@@ -249,7 +315,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
absolute,
|
absolute,
|
||||||
get directory() {
|
get directory() {
|
||||||
return store.path.directory
|
return current()[0].path.directory
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
return base64Decode(params.dir!)
|
return base64Decode(params.dir!)
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Show when={params.dir} keyed>
|
<Show when={params.dir}>
|
||||||
<SDKProvider directory={directory()}>
|
<SDKProvider directory={directory()}>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
{iife(() => {
|
{iife(() => {
|
||||||
|
|||||||
@@ -199,8 +199,8 @@ export default function Page() {
|
|||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const [pendingMessage, setPendingMessage] = createSignal<string | undefined>(undefined)
|
const [pendingMessage, setPendingMessage] = createSignal<string | undefined>(undefined)
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
const view = createMemo(() => layout.view(sessionKey()))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
createEffect(
|
createEffect(
|
||||||
|
|||||||
Reference in New Issue
Block a user