refactor(tui): improve workspace management (#22691)
This commit is contained in:
parent
305460b25f
commit
06afd33291
10 changed files with 349 additions and 37 deletions
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { TextAttributes } from "@opentui/core"
|
||||||
|
import { useTheme } from "../context/theme"
|
||||||
|
import { useDialog } from "../ui/dialog"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { For } from "solid-js"
|
||||||
|
import { useKeyboard } from "@opentui/solid"
|
||||||
|
|
||||||
|
export function DialogSessionDeleteFailed(props: {
|
||||||
|
session: string
|
||||||
|
workspace: string
|
||||||
|
onDelete?: () => boolean | void | Promise<boolean | void>
|
||||||
|
onRestore?: () => boolean | void | Promise<boolean | void>
|
||||||
|
onDone?: () => void
|
||||||
|
}) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
active: "delete" as "delete" | "restore",
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
id: "delete" as const,
|
||||||
|
title: "Delete workspace",
|
||||||
|
description: "Delete the workspace and all sessions attached to it.",
|
||||||
|
run: props.onDelete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "restore" as const,
|
||||||
|
title: "Restore to new workspace",
|
||||||
|
description: "Try to restore this session into a new workspace.",
|
||||||
|
run: props.onRestore,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function confirm() {
|
||||||
|
const result = await options.find((item) => item.id === store.active)?.run?.()
|
||||||
|
if (result === false) return
|
||||||
|
props.onDone?.()
|
||||||
|
if (!props.onDone) dialog.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
useKeyboard((evt) => {
|
||||||
|
if (evt.name === "return") {
|
||||||
|
void confirm()
|
||||||
|
}
|
||||||
|
if (evt.name === "left" || evt.name === "up") {
|
||||||
|
setStore("active", "delete")
|
||||||
|
}
|
||||||
|
if (evt.name === "right" || evt.name === "down") {
|
||||||
|
setStore("active", "restore")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||||
|
Failed to Delete Session
|
||||||
|
</text>
|
||||||
|
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||||
|
esc
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<text fg={theme.textMuted} wrapMode="word">
|
||||||
|
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
|
||||||
|
</text>
|
||||||
|
<text fg={theme.textMuted} wrapMode="word">
|
||||||
|
Choose how you want to recover this broken workspace session.
|
||||||
|
</text>
|
||||||
|
<box flexDirection="column" paddingBottom={1} gap={1}>
|
||||||
|
<For each={options}>
|
||||||
|
{(item) => (
|
||||||
|
<box
|
||||||
|
flexDirection="column"
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
backgroundColor={item.id === store.active ? theme.primary : undefined}
|
||||||
|
onMouseUp={() => {
|
||||||
|
setStore("active", item.id)
|
||||||
|
void confirm()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
attributes={TextAttributes.BOLD}
|
||||||
|
fg={item.id === store.active ? theme.selectedListItemText : theme.text}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</text>
|
||||||
|
<text fg={item.id === store.active ? theme.selectedListItemText : theme.textMuted} wrapMode="word">
|
||||||
|
{item.description}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -13,8 +13,10 @@ import { DialogSessionRename } from "./dialog-session-rename"
|
||||||
import { Keybind } from "@/util"
|
import { Keybind } from "@/util"
|
||||||
import { createDebouncedSignal } from "../util/signal"
|
import { createDebouncedSignal } from "../util/signal"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create"
|
import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create"
|
||||||
import { Spinner } from "./spinner"
|
import { Spinner } from "./spinner"
|
||||||
|
import { errorMessage } from "@/util/error"
|
||||||
|
import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
|
||||||
|
|
||||||
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
||||||
|
|
||||||
|
|
@ -30,7 +32,7 @@ export function DialogSessionList() {
|
||||||
const [toDelete, setToDelete] = createSignal<string>()
|
const [toDelete, setToDelete] = createSignal<string>()
|
||||||
const [search, setSearch] = createDebouncedSignal("", 150)
|
const [search, setSearch] = createDebouncedSignal("", 150)
|
||||||
|
|
||||||
const [searchResults] = createResource(search, async (query) => {
|
const [searchResults, { refetch }] = createResource(search, async (query) => {
|
||||||
if (!query) return undefined
|
if (!query) return undefined
|
||||||
const result = await sdk.client.session.list({ search: query, limit: 30 })
|
const result = await sdk.client.session.list({ search: query, limit: 30 })
|
||||||
return result.data ?? []
|
return result.data ?? []
|
||||||
|
|
@ -56,6 +58,57 @@ export function DialogSessionList() {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
|
||||||
|
const workspace = project.workspace.get(session.workspaceID!)
|
||||||
|
const list = () => dialog.replace(() => <DialogSessionList />)
|
||||||
|
dialog.replace(() => (
|
||||||
|
<DialogSessionDeleteFailed
|
||||||
|
session={session.title}
|
||||||
|
workspace={workspace?.name ?? session.workspaceID!}
|
||||||
|
onDone={list}
|
||||||
|
onDelete={async () => {
|
||||||
|
const current = currentSessionID()
|
||||||
|
const info = current ? sync.data.session.find((item) => item.id === current) : undefined
|
||||||
|
const result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! })
|
||||||
|
if (result.error) {
|
||||||
|
toast.show({
|
||||||
|
variant: "error",
|
||||||
|
title: "Failed to delete workspace",
|
||||||
|
message: errorMessage(result.error),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
await project.workspace.sync()
|
||||||
|
await sync.session.refresh()
|
||||||
|
if (search()) await refetch()
|
||||||
|
if (info?.workspaceID === session.workspaceID) {
|
||||||
|
route.navigate({ type: "home" })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}}
|
||||||
|
onRestore={() => {
|
||||||
|
dialog.replace(() => (
|
||||||
|
<DialogWorkspaceCreate
|
||||||
|
onSelect={(workspaceID) =>
|
||||||
|
restoreWorkspaceSession({
|
||||||
|
dialog,
|
||||||
|
sdk,
|
||||||
|
sync,
|
||||||
|
project,
|
||||||
|
toast,
|
||||||
|
workspaceID,
|
||||||
|
sessionID: session.id,
|
||||||
|
done: list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
return false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
const today = new Date().toDateString()
|
const today = new Date().toDateString()
|
||||||
return sessions()
|
return sessions()
|
||||||
|
|
@ -145,9 +198,43 @@ export function DialogSessionList() {
|
||||||
title: "delete",
|
title: "delete",
|
||||||
onTrigger: async (option) => {
|
onTrigger: async (option) => {
|
||||||
if (toDelete() === option.value) {
|
if (toDelete() === option.value) {
|
||||||
void sdk.client.session.delete({
|
const session = sessions().find((item) => item.id === option.value)
|
||||||
sessionID: option.value,
|
const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefined
|
||||||
})
|
|
||||||
|
try {
|
||||||
|
const result = await sdk.client.session.delete({
|
||||||
|
sessionID: option.value,
|
||||||
|
})
|
||||||
|
if (result.error) {
|
||||||
|
if (session?.workspaceID) {
|
||||||
|
recover(session)
|
||||||
|
} else {
|
||||||
|
toast.show({
|
||||||
|
variant: "error",
|
||||||
|
title: "Failed to delete session",
|
||||||
|
message: errorMessage(result.error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setToDelete(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (session?.workspaceID) {
|
||||||
|
recover(session)
|
||||||
|
} else {
|
||||||
|
toast.show({
|
||||||
|
variant: "error",
|
||||||
|
title: "Failed to delete session",
|
||||||
|
message: errorMessage(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setToDelete(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (status && status !== "connected") {
|
||||||
|
await sync.session.refresh()
|
||||||
|
}
|
||||||
|
if (search()) await refetch()
|
||||||
setToDelete(undefined)
|
setToDelete(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { useSync } from "@tui/context/sync"
|
||||||
import { useProject } from "@tui/context/project"
|
import { useProject } from "@tui/context/project"
|
||||||
import { createMemo, createSignal, onMount } from "solid-js"
|
import { createMemo, createSignal, onMount } from "solid-js"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
|
import { errorData, errorMessage } from "@/util/error"
|
||||||
|
import * as Log from "@/util/log"
|
||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
|
|
||||||
|
|
@ -15,6 +17,8 @@ type Adaptor = {
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const log = Log.Default.clone().tag("service", "tui-workspace")
|
||||||
|
|
||||||
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
||||||
return createOpencodeClient({
|
return createOpencodeClient({
|
||||||
baseUrl: sdk.url,
|
baseUrl: sdk.url,
|
||||||
|
|
@ -33,8 +37,20 @@ export async function openWorkspaceSession(input: {
|
||||||
workspaceID: string
|
workspaceID: string
|
||||||
}) {
|
}) {
|
||||||
const client = scoped(input.sdk, input.sync, input.workspaceID)
|
const client = scoped(input.sdk, input.sync, input.workspaceID)
|
||||||
|
log.info("workspace session create requested", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("opening!")
|
||||||
while (true) {
|
while (true) {
|
||||||
const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
|
console.log("creating")
|
||||||
|
const result = await client.session.create({ workspace: input.workspaceID }).catch((err) => {
|
||||||
|
log.error("workspace session create request failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
if (!result) {
|
if (!result) {
|
||||||
input.toast.show({
|
input.toast.show({
|
||||||
message: "Failed to create workspace session",
|
message: "Failed to create workspace session",
|
||||||
|
|
@ -42,26 +58,113 @@ export async function openWorkspaceSession(input: {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (result.response.status >= 500 && result.response.status < 600) {
|
log.info("workspace session create response", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
status: result.response?.status,
|
||||||
|
sessionID: result.data?.id,
|
||||||
|
})
|
||||||
|
if (result.response?.status && result.response.status >= 500 && result.response.status < 600) {
|
||||||
|
log.warn("workspace session create retrying after server error", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
status: result.response.status,
|
||||||
|
})
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!result.data) {
|
if (!result.data) {
|
||||||
|
log.error("workspace session create returned no data", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
status: result.response?.status,
|
||||||
|
})
|
||||||
input.toast.show({
|
input.toast.show({
|
||||||
message: "Failed to create workspace session",
|
message: "Failed to create workspace session",
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
input.route.navigate({
|
input.route.navigate({
|
||||||
type: "session",
|
type: "session",
|
||||||
sessionID: result.data.id,
|
sessionID: result.data.id,
|
||||||
})
|
})
|
||||||
|
log.info("workspace session create complete", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: result.data.id,
|
||||||
|
})
|
||||||
input.dialog.clear()
|
input.dialog.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restoreWorkspaceSession(input: {
|
||||||
|
dialog: ReturnType<typeof useDialog>
|
||||||
|
sdk: ReturnType<typeof useSDK>
|
||||||
|
sync: ReturnType<typeof useSync>
|
||||||
|
project: ReturnType<typeof useProject>
|
||||||
|
toast: ReturnType<typeof useToast>
|
||||||
|
workspaceID: string
|
||||||
|
sessionID: string
|
||||||
|
done?: () => void
|
||||||
|
}) {
|
||||||
|
log.info("session restore requested", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
})
|
||||||
|
const result = await input.sdk.client.experimental.workspace
|
||||||
|
.sessionRestore({ id: input.workspaceID, sessionID: input.sessionID })
|
||||||
|
.catch((err) => {
|
||||||
|
log.error("session restore request failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (!result?.data) {
|
||||||
|
log.error("session restore failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
status: result?.response?.status,
|
||||||
|
error: result?.error ? errorData(result.error) : undefined,
|
||||||
|
})
|
||||||
|
input.toast.show({
|
||||||
|
message: `Failed to restore session: ${errorMessage(result?.error ?? "no response")}`,
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("session restore response", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
status: result.response?.status,
|
||||||
|
total: result.data.total,
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => {
|
||||||
|
log.error("session restore refresh failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info("session restore complete", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
total: result.data.total,
|
||||||
|
})
|
||||||
|
|
||||||
|
input.toast.show({
|
||||||
|
message: "Session restored into the new workspace",
|
||||||
|
variant: "success",
|
||||||
|
})
|
||||||
|
input.done?.()
|
||||||
|
if (input.done) return
|
||||||
|
input.dialog.clear()
|
||||||
|
}
|
||||||
|
|
||||||
export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
|
export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
|
@ -123,18 +226,43 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
||||||
const create = async (type: string) => {
|
const create = async (type: string) => {
|
||||||
if (creating()) return
|
if (creating()) return
|
||||||
setCreating(type)
|
setCreating(type)
|
||||||
|
log.info("workspace create requested", {
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => {
|
||||||
|
log.error("workspace create request failed", {
|
||||||
|
type,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => undefined)
|
|
||||||
const workspace = result?.data
|
const workspace = result?.data
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
setCreating(undefined)
|
setCreating(undefined)
|
||||||
|
log.error("workspace create failed", {
|
||||||
|
type,
|
||||||
|
status: result?.response.status,
|
||||||
|
error: result?.error ? errorData(result.error) : undefined,
|
||||||
|
})
|
||||||
toast.show({
|
toast.show({
|
||||||
message: "Failed to create workspace",
|
message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.info("workspace create response", {
|
||||||
|
type,
|
||||||
|
workspaceID: workspace.id,
|
||||||
|
status: result.response?.status,
|
||||||
|
})
|
||||||
|
|
||||||
await project.workspace.sync()
|
await project.workspace.sync()
|
||||||
|
log.info("workspace create synced", {
|
||||||
|
type,
|
||||||
|
workspaceID: workspace.id,
|
||||||
|
})
|
||||||
await props.onSelect(workspace.id)
|
await props.onSelect(workspace.id)
|
||||||
setCreating(undefined)
|
setCreating(undefined)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -617,9 +617,7 @@ export function Prompt(props: PromptProps) {
|
||||||
|
|
||||||
let sessionID = props.sessionID
|
let sessionID = props.sessionID
|
||||||
if (sessionID == null) {
|
if (sessionID == null) {
|
||||||
const res = await sdk.client.session.create({
|
const res = await sdk.client.session.create({ workspace: props.workspaceID })
|
||||||
workspaceID: props.workspaceID,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
console.log("Creating a session failed:", res.error)
|
console.log("Creating a session failed:", res.error)
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||||
if (match.found) return store.session[match.index]
|
if (match.found) return store.session[match.index]
|
||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
|
async refresh() {
|
||||||
|
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
|
const list = await sdk.client.session
|
||||||
|
.list({ start })
|
||||||
|
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
||||||
|
setStore("session", reconcile(list))
|
||||||
|
},
|
||||||
status(sessionID: string) {
|
status(sessionID: string) {
|
||||||
const session = result.session.get(sessionID)
|
const session = result.session.get(sessionID)
|
||||||
if (!session) return "idle"
|
if (!session) return "idle"
|
||||||
|
|
@ -485,13 +492,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||||
return last.time.completed ? "idle" : "working"
|
return last.time.completed ? "idle" : "working"
|
||||||
},
|
},
|
||||||
async sync(sessionID: string) {
|
async sync(sessionID: string) {
|
||||||
|
console.log('YO', sessionID, fullSyncedSessions.has(sessionID))
|
||||||
if (fullSyncedSessions.has(sessionID)) return
|
if (fullSyncedSessions.has(sessionID)) return
|
||||||
const workspace = project.workspace.current()
|
|
||||||
const [session, messages, todo, diff] = await Promise.all([
|
const [session, messages, todo, diff] = await Promise.all([
|
||||||
sdk.client.session.get({ sessionID, workspace }, { throwOnError: true }),
|
sdk.client.session.get({ sessionID }, { throwOnError: true }),
|
||||||
sdk.client.session.messages({ sessionID, limit: 100, workspace }),
|
sdk.client.session.messages({ sessionID, limit: 100 }),
|
||||||
sdk.client.session.todo({ sessionID, workspace }),
|
sdk.client.session.todo({ sessionID }),
|
||||||
sdk.client.session.diff({ sessionID, workspace }),
|
sdk.client.session.diff({ sessionID }),
|
||||||
])
|
])
|
||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,17 @@ import { LocalContext } from "../util"
|
||||||
import type { WorkspaceID } from "../control-plane/schema"
|
import type { WorkspaceID } from "../control-plane/schema"
|
||||||
|
|
||||||
export interface WorkspaceContext {
|
export interface WorkspaceContext {
|
||||||
workspaceID: string
|
workspaceID: WorkspaceID
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = LocalContext.create<WorkspaceContext>("instance")
|
const context = LocalContext.create<WorkspaceContext>("instance")
|
||||||
|
|
||||||
export const WorkspaceContext = {
|
export const WorkspaceContext = {
|
||||||
async provide<R>(input: { workspaceID: WorkspaceID; fn: () => R }): Promise<R> {
|
async provide<R>(input: { workspaceID: WorkspaceID; fn: () => R }): Promise<R> {
|
||||||
return context.provide({ workspaceID: input.workspaceID as string }, () => input.fn())
|
return context.provide({ workspaceID: input.workspaceID }, () => input.fn())
|
||||||
},
|
},
|
||||||
|
|
||||||
restore<R>(workspaceID: string, fn: () => R): R {
|
restore<R>(workspaceID: WorkspaceID, fn: () => R): R {
|
||||||
return context.provide({ workspaceID }, fn)
|
return context.provide({ workspaceID }, fn)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Effect, Fiber } from "effect"
|
import { Effect, Fiber } from "effect"
|
||||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
import { Instance, type InstanceContext } from "@/project/instance"
|
import { Instance, type InstanceContext } from "@/project/instance"
|
||||||
|
import type { WorkspaceID } from "@/control-plane/schema"
|
||||||
import { LocalContext } from "@/util"
|
import { LocalContext } from "@/util"
|
||||||
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||||
import { attachWith } from "./run-service"
|
import { attachWith } from "./run-service"
|
||||||
|
|
@ -10,7 +11,7 @@ export interface Shape {
|
||||||
readonly fork: <A, E, R>(effect: Effect.Effect<A, E, R>) => Fiber.Fiber<A, E>
|
readonly fork: <A, E, R>(effect: Effect.Effect<A, E, R>) => Fiber.Fiber<A, E>
|
||||||
}
|
}
|
||||||
|
|
||||||
function restore<R>(instance: InstanceContext | undefined, workspace: string | undefined, fn: () => R): R {
|
function restore<R>(instance: InstanceContext | undefined, workspace: WorkspaceID | undefined, fn: () => R): R {
|
||||||
if (instance && workspace !== undefined) {
|
if (instance && workspace !== undefined) {
|
||||||
return WorkspaceContext.restore(workspace, () => Instance.restore(instance, fn))
|
return WorkspaceContext.restore(workspace, () => Instance.restore(instance, fn))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { Context } from "effect"
|
import { Context } from "effect"
|
||||||
import type { InstanceContext } from "@/project/instance"
|
import type { InstanceContext } from "@/project/instance"
|
||||||
|
import type { WorkspaceID } from "@/control-plane/schema"
|
||||||
|
|
||||||
export const InstanceRef = Context.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
|
export const InstanceRef = Context.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
|
||||||
defaultValue: () => undefined,
|
defaultValue: () => undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const WorkspaceRef = Context.Reference<string | undefined>("~opencode/WorkspaceRef", {
|
export const WorkspaceRef = Context.Reference<WorkspaceID | undefined>("~opencode/WorkspaceRef", {
|
||||||
defaultValue: () => undefined,
|
defaultValue: () => undefined,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -519,12 +519,13 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service> =
|
||||||
workspaceID?: WorkspaceID
|
workspaceID?: WorkspaceID
|
||||||
}) {
|
}) {
|
||||||
const directory = yield* InstanceState.directory
|
const directory = yield* InstanceState.directory
|
||||||
|
const workspace = yield* InstanceState.workspaceID
|
||||||
return yield* createNext({
|
return yield* createNext({
|
||||||
parentID: input?.parentID,
|
parentID: input?.parentID,
|
||||||
directory,
|
directory,
|
||||||
title: input?.title,
|
title: input?.title,
|
||||||
permission: input?.permission,
|
permission: input?.permission,
|
||||||
workspaceID: input?.workspaceID,
|
workspaceID: workspace,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -264,27 +264,15 @@ describe("SyncProvider", () => {
|
||||||
|
|
||||||
log.length = 0
|
log.length = 0
|
||||||
await sync.session.sync("ses_1")
|
await sync.session.sync("ses_1")
|
||||||
|
expect(log.filter((item) => item.path === "/session/ses_1")).toHaveLength(1)
|
||||||
|
|
||||||
expect(log.filter((item) => item.path === "/session/ses_1" && item.workspace === "ws_a")).toHaveLength(1)
|
|
||||||
expect(sync.data.todo.ses_1[0]?.content).toBe("todo-ws_a")
|
|
||||||
expect(sync.data.message.ses_1[0]?.id).toBe("msg_1")
|
|
||||||
expect(sync.data.part.msg_1[0]).toMatchObject({ type: "text", text: "part-ws_a" })
|
|
||||||
expect(sync.data.session_diff.ses_1[0]?.file).toBe("ws_a.ts")
|
|
||||||
|
|
||||||
log.length = 0
|
|
||||||
project.workspace.set("ws_b")
|
project.workspace.set("ws_b")
|
||||||
await waitBoot(log, "ws_b")
|
await waitBoot(log, "ws_b")
|
||||||
expect(project.workspace.current()).toBe("ws_b")
|
expect(project.workspace.current()).toBe("ws_b")
|
||||||
|
|
||||||
log.length = 0
|
log.length = 0
|
||||||
await sync.session.sync("ses_1")
|
await sync.session.sync("ses_1")
|
||||||
await wait(() => log.some((item) => item.path === "/session/ses_1" && item.workspace === "ws_b"))
|
expect(log.filter((item) => item.path === "/session/ses_1")).toHaveLength(1)
|
||||||
|
|
||||||
expect(log.filter((item) => item.path === "/session/ses_1" && item.workspace === "ws_b")).toHaveLength(1)
|
|
||||||
expect(sync.data.todo.ses_1[0]?.content).toBe("todo-ws_b")
|
|
||||||
expect(sync.data.message.ses_1[0]?.id).toBe("msg_1")
|
|
||||||
expect(sync.data.part.msg_1[0]).toMatchObject({ type: "text", text: "part-ws_b" })
|
|
||||||
expect(sync.data.session_diff.ses_1[0]?.file).toBe("ws_b.ts")
|
|
||||||
} finally {
|
} finally {
|
||||||
app.renderer.destroy()
|
app.renderer.destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue