summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-22 18:07:48 -0600
committerAdam <[email protected]>2026-01-22 18:07:57 -0600
commitbcf7a65e36af1437cda556577a275dd8531afd0d (patch)
treea370d10879d11c8efcf161f1b73526a464c40ea8
parent7c80ac072bd1b42e4ea962ad47b049606b015ebb (diff)
downloadopencode-bcf7a65e36af1437cda556577a275dd8531afd0d.tar.gz
opencode-bcf7a65e36af1437cda556577a275dd8531afd0d.zip
fix(app): non-git projects should be renameable
-rw-r--r--packages/app/src/components/dialog-edit-project.tsx26
-rw-r--r--packages/app/src/context/global-sync.tsx56
-rw-r--r--packages/app/src/context/layout.tsx21
-rw-r--r--packages/app/src/pages/layout.tsx9
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<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,
},
}
}
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) {