Просмотр исходного кода

fix(app): non-git projects should be renameable

Adam 1 месяц назад
Родитель
Сommit
bcf7a65e36

+ 19 - 7
packages/app/src/components/dialog-edit-project.tsx

@@ -6,6 +6,7 @@ import { Icon } from "@opencode-ai/ui/icon"
 import { createMemo, createSignal, For, Show } from "solid-js"
 import { createStore } from "solid-js/store"
 import { useGlobalSDK } from "@/context/global-sdk"
+import { useGlobalSync } from "@/context/global-sync"
 import { type LocalProject, getAvatarColors } from "@/context/layout"
 import { getFilename } from "@opencode-ai/util/path"
 import { Avatar } from "@opencode-ai/ui/avatar"
@@ -16,6 +17,7 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a
 export function DialogEditProject(props: { project: LocalProject }) {
   const dialog = useDialog()
   const globalSDK = useGlobalSDK()
+  const globalSync = useGlobalSync()
   const language = useLanguage()
 
   const folderName = createMemo(() => getFilename(props.project.worktree))
@@ -71,17 +73,27 @@ export function DialogEditProject(props: { project: LocalProject }) {
   async function handleSubmit(e: SubmitEvent) {
     e.preventDefault()
 
-    if (!props.project.id) return
-
     setStore("saving", true)
     const name = store.name.trim() === folderName() ? "" : store.name.trim()
     const start = store.startup.trim()
-    await globalSDK.client.project.update({
-      projectID: props.project.id,
-      directory: props.project.worktree,
+
+    if (props.project.id && props.project.id !== "global") {
+      await globalSDK.client.project.update({
+        projectID: props.project.id,
+        directory: props.project.worktree,
+        name,
+        icon: { color: store.color, override: store.iconUrl },
+        commands: { start },
+      })
+      setStore("saving", false)
+      dialog.close()
+      return
+    }
+
+    globalSync.project.meta(props.project.worktree, {
       name,
-      icon: { color: store.color, override: store.iconUrl },
-      commands: { start },
+      icon: { color: store.color, override: store.iconUrl || undefined },
+      commands: { start: start || undefined },
     })
     setStore("saving", false)
     dialog.close()

+ 56 - 0
packages/app/src/context/global-sync.tsx

@@ -44,11 +44,23 @@ import { usePlatform } from "./platform"
 import { useLanguage } from "@/context/language"
 import { Persist, persisted } from "@/utils/persist"
 
+type ProjectMeta = {
+  name?: string
+  icon?: {
+    override?: string
+    color?: string
+  }
+  commands?: {
+    start?: string
+  }
+}
+
 type State = {
   status: "loading" | "partial" | "complete"
   agent: Agent[]
   command: Command[]
   project: string
+  projectMeta: ProjectMeta | undefined
   provider: ProviderListResponse
   config: Config
   path: Path
@@ -89,6 +101,12 @@ type VcsCache = {
   ready: Accessor<boolean>
 }
 
+type MetaCache = {
+  store: Store<{ value: ProjectMeta | undefined }>
+  setStore: SetStoreFunction<{ value: ProjectMeta | undefined }>
+  ready: Accessor<boolean>
+}
+
 type ChildOptions = {
   bootstrap?: boolean
 }
@@ -100,6 +118,7 @@ function createGlobalSync() {
   const owner = getOwner()
   if (!owner) throw new Error("GlobalSync must be created within owner")
   const vcsCache = new Map<string, VcsCache>()
+  const metaCache = new Map<string, MetaCache>()
   const [globalStore, setGlobalStore] = createStore<{
     ready: boolean
     error?: InitError
@@ -149,9 +168,19 @@ function createGlobalSync() {
       if (!cache) throw new Error("Failed to create persisted cache")
       vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
 
+      const meta = runWithOwner(owner, () =>
+        persisted(
+          Persist.workspace(directory, "project", ["project.v1"]),
+          createStore({ value: undefined as ProjectMeta | undefined }),
+        ),
+      )
+      if (!meta) throw new Error("Failed to create persisted project metadata")
+      metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
+
       const init = () => {
         children[directory] = createStore<State>({
           project: "",
+          projectMeta: meta[0].value,
           provider: { all: [], connected: [], default: {} },
           config: {},
           path: { state: "", config: "", worktree: "", directory: "", home: "" },
@@ -253,6 +282,8 @@ function createGlobalSync() {
       const [store, setStore] = ensureChild(directory)
       const cache = vcsCache.get(directory)
       if (!cache) return
+      const meta = metaCache.get(directory)
+      if (!meta) return
       const sdk = createOpencodeClient({
         baseUrl: globalSDK.url,
         fetch: platform.fetch,
@@ -269,6 +300,13 @@ function createGlobalSync() {
         setStore("vcs", (value) => value ?? cached)
       })
 
+      createEffect(() => {
+        if (!meta.ready()) return
+        const cached = meta.store.value
+        if (!cached) return
+        setStore("projectMeta", (value) => value ?? cached)
+      })
+
       const blockingRequests = {
         project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
         provider: () =>
@@ -725,6 +763,23 @@ function createGlobalSync() {
     bootstrap()
   })
 
+  function projectMeta(directory: string, patch: ProjectMeta) {
+    const [store, setStore] = ensureChild(directory)
+    const cached = metaCache.get(directory)
+    if (!cached) return
+    const previous = store.projectMeta ?? {}
+    const icon = patch.icon ? { ...(previous.icon ?? {}), ...patch.icon } : previous.icon
+    const commands = patch.commands ? { ...(previous.commands ?? {}), ...patch.commands } : previous.commands
+    const next = {
+      ...previous,
+      ...patch,
+      icon,
+      commands,
+    }
+    cached.setStore("value", next)
+    setStore("projectMeta", next)
+  }
+
   return {
     data: globalStore,
     set: setGlobalStore,
@@ -746,6 +801,7 @@ function createGlobalSync() {
     },
     project: {
       loadSessions,
+      meta: projectMeta,
     },
   }
 }

+ 20 - 1
packages/app/src/context/layout.tsx

@@ -222,7 +222,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       const metadata = projectID
         ? globalSync.data.project.find((x) => x.id === projectID)
         : globalSync.data.project.find((x) => x.worktree === project.worktree)
-      return {
+
+      const base = {
         ...(metadata ?? {}),
         ...project,
         icon: {
@@ -231,6 +232,20 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           color: metadata?.icon?.color,
         },
       }
+
+      if (projectID !== "global") return base
+
+      const local = childStore.projectMeta
+      return {
+        ...base,
+        name: local?.name,
+        commands: local?.commands,
+        icon: {
+          url: base.icon?.url,
+          override: local?.icon?.override,
+          color: local?.icon?.color,
+        },
+      }
     }
 
     const roots = createMemo(() => {
@@ -296,6 +311,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         used.add(color)
         setColors(project.worktree, color)
         if (!project.id) continue
+        if (project.id === "global") {
+          globalSync.project.meta(project.worktree, { icon: { color } })
+          continue
+        }
         void globalSdk.client.project.update({ projectID: project.id, directory: project.worktree, icon: { color } })
       }
     })

+ 7 - 2
packages/app/src/pages/layout.tsx

@@ -1018,11 +1018,16 @@ export default function Layout(props: ParentProps) {
   const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
 
   async function renameProject(project: LocalProject, next: string) {
-    if (!project.id) return
     const current = displayName(project)
     if (next === current) return
     const name = next === getFilename(project.worktree) ? "" : next
-    await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
+
+    if (project.id && project.id !== "global") {
+      await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
+      return
+    }
+
+    globalSync.project.meta(project.worktree, { name })
   }
 
   async function renameSession(session: Session, next: string) {