From bcf7a65e36af1437cda556577a275dd8531afd0d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:07:48 -0600 Subject: [PATCH] fix(app): non-git projects should be renameable --- .../src/components/dialog-edit-project.tsx | 26 ++++++--- packages/app/src/context/global-sync.tsx | 56 +++++++++++++++++++ packages/app/src/context/layout.tsx | 21 ++++++- packages/app/src/pages/layout.tsx | 9 ++- 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 7664470f7..a90cac169 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/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() diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 4964ef6e4..ec072e7be 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/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 } +type MetaCache = { + store: Store<{ value: ProjectMeta | undefined }> + setStore: SetStoreFunction<{ value: ProjectMeta | undefined }> + ready: Accessor +} + 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() + const metaCache = new Map() 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({ 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, }, } } diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 7a3556290..3c544b069 100644 --- a/packages/app/src/context/layout.tsx +++ b/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 } }) } }) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index c4fd07e57..12e11e724 100644 --- a/packages/app/src/pages/layout.tsx +++ b/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) {