Skip to content

Commit bcf7a65

Browse files
committed
fix(app): non-git projects should be renameable
1 parent 7c80ac0 commit bcf7a65

4 files changed

Lines changed: 102 additions & 10 deletions

File tree

packages/app/src/components/dialog-edit-project.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Icon } from "@opencode-ai/ui/icon"
66
import { createMemo, createSignal, For, Show } from "solid-js"
77
import { createStore } from "solid-js/store"
88
import { useGlobalSDK } from "@/context/global-sdk"
9+
import { useGlobalSync } from "@/context/global-sync"
910
import { type LocalProject, getAvatarColors } from "@/context/layout"
1011
import { getFilename } from "@opencode-ai/util/path"
1112
import { Avatar } from "@opencode-ai/ui/avatar"
@@ -16,6 +17,7 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a
1617
export function DialogEditProject(props: { project: LocalProject }) {
1718
const dialog = useDialog()
1819
const globalSDK = useGlobalSDK()
20+
const globalSync = useGlobalSync()
1921
const language = useLanguage()
2022

2123
const folderName = createMemo(() => getFilename(props.project.worktree))
@@ -71,17 +73,27 @@ export function DialogEditProject(props: { project: LocalProject }) {
7173
async function handleSubmit(e: SubmitEvent) {
7274
e.preventDefault()
7375

74-
if (!props.project.id) return
75-
7676
setStore("saving", true)
7777
const name = store.name.trim() === folderName() ? "" : store.name.trim()
7878
const start = store.startup.trim()
79-
await globalSDK.client.project.update({
80-
projectID: props.project.id,
81-
directory: props.project.worktree,
79+
80+
if (props.project.id && props.project.id !== "global") {
81+
await globalSDK.client.project.update({
82+
projectID: props.project.id,
83+
directory: props.project.worktree,
84+
name,
85+
icon: { color: store.color, override: store.iconUrl },
86+
commands: { start },
87+
})
88+
setStore("saving", false)
89+
dialog.close()
90+
return
91+
}
92+
93+
globalSync.project.meta(props.project.worktree, {
8294
name,
83-
icon: { color: store.color, override: store.iconUrl },
84-
commands: { start },
95+
icon: { color: store.color, override: store.iconUrl || undefined },
96+
commands: { start: start || undefined },
8597
})
8698
setStore("saving", false)
8799
dialog.close()

packages/app/src/context/global-sync.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,23 @@ import { usePlatform } from "./platform"
4444
import { useLanguage } from "@/context/language"
4545
import { Persist, persisted } from "@/utils/persist"
4646

47+
type ProjectMeta = {
48+
name?: string
49+
icon?: {
50+
override?: string
51+
color?: string
52+
}
53+
commands?: {
54+
start?: string
55+
}
56+
}
57+
4758
type State = {
4859
status: "loading" | "partial" | "complete"
4960
agent: Agent[]
5061
command: Command[]
5162
project: string
63+
projectMeta: ProjectMeta | undefined
5264
provider: ProviderListResponse
5365
config: Config
5466
path: Path
@@ -89,6 +101,12 @@ type VcsCache = {
89101
ready: Accessor<boolean>
90102
}
91103

104+
type MetaCache = {
105+
store: Store<{ value: ProjectMeta | undefined }>
106+
setStore: SetStoreFunction<{ value: ProjectMeta | undefined }>
107+
ready: Accessor<boolean>
108+
}
109+
92110
type ChildOptions = {
93111
bootstrap?: boolean
94112
}
@@ -100,6 +118,7 @@ function createGlobalSync() {
100118
const owner = getOwner()
101119
if (!owner) throw new Error("GlobalSync must be created within owner")
102120
const vcsCache = new Map<string, VcsCache>()
121+
const metaCache = new Map<string, MetaCache>()
103122
const [globalStore, setGlobalStore] = createStore<{
104123
ready: boolean
105124
error?: InitError
@@ -149,9 +168,19 @@ function createGlobalSync() {
149168
if (!cache) throw new Error("Failed to create persisted cache")
150169
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
151170

171+
const meta = runWithOwner(owner, () =>
172+
persisted(
173+
Persist.workspace(directory, "project", ["project.v1"]),
174+
createStore({ value: undefined as ProjectMeta | undefined }),
175+
),
176+
)
177+
if (!meta) throw new Error("Failed to create persisted project metadata")
178+
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
179+
152180
const init = () => {
153181
children[directory] = createStore<State>({
154182
project: "",
183+
projectMeta: meta[0].value,
155184
provider: { all: [], connected: [], default: {} },
156185
config: {},
157186
path: { state: "", config: "", worktree: "", directory: "", home: "" },
@@ -253,6 +282,8 @@ function createGlobalSync() {
253282
const [store, setStore] = ensureChild(directory)
254283
const cache = vcsCache.get(directory)
255284
if (!cache) return
285+
const meta = metaCache.get(directory)
286+
if (!meta) return
256287
const sdk = createOpencodeClient({
257288
baseUrl: globalSDK.url,
258289
fetch: platform.fetch,
@@ -269,6 +300,13 @@ function createGlobalSync() {
269300
setStore("vcs", (value) => value ?? cached)
270301
})
271302

303+
createEffect(() => {
304+
if (!meta.ready()) return
305+
const cached = meta.store.value
306+
if (!cached) return
307+
setStore("projectMeta", (value) => value ?? cached)
308+
})
309+
272310
const blockingRequests = {
273311
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
274312
provider: () =>
@@ -725,6 +763,23 @@ function createGlobalSync() {
725763
bootstrap()
726764
})
727765

766+
function projectMeta(directory: string, patch: ProjectMeta) {
767+
const [store, setStore] = ensureChild(directory)
768+
const cached = metaCache.get(directory)
769+
if (!cached) return
770+
const previous = store.projectMeta ?? {}
771+
const icon = patch.icon ? { ...(previous.icon ?? {}), ...patch.icon } : previous.icon
772+
const commands = patch.commands ? { ...(previous.commands ?? {}), ...patch.commands } : previous.commands
773+
const next = {
774+
...previous,
775+
...patch,
776+
icon,
777+
commands,
778+
}
779+
cached.setStore("value", next)
780+
setStore("projectMeta", next)
781+
}
782+
728783
return {
729784
data: globalStore,
730785
set: setGlobalStore,
@@ -746,6 +801,7 @@ function createGlobalSync() {
746801
},
747802
project: {
748803
loadSessions,
804+
meta: projectMeta,
749805
},
750806
}
751807
}

packages/app/src/context/layout.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
222222
const metadata = projectID
223223
? globalSync.data.project.find((x) => x.id === projectID)
224224
: globalSync.data.project.find((x) => x.worktree === project.worktree)
225-
return {
225+
226+
const base = {
226227
...(metadata ?? {}),
227228
...project,
228229
icon: {
@@ -231,6 +232,20 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
231232
color: metadata?.icon?.color,
232233
},
233234
}
235+
236+
if (projectID !== "global") return base
237+
238+
const local = childStore.projectMeta
239+
return {
240+
...base,
241+
name: local?.name,
242+
commands: local?.commands,
243+
icon: {
244+
url: base.icon?.url,
245+
override: local?.icon?.override,
246+
color: local?.icon?.color,
247+
},
248+
}
234249
}
235250

236251
const roots = createMemo(() => {
@@ -296,6 +311,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
296311
used.add(color)
297312
setColors(project.worktree, color)
298313
if (!project.id) continue
314+
if (project.id === "global") {
315+
globalSync.project.meta(project.worktree, { icon: { color } })
316+
continue
317+
}
299318
void globalSdk.client.project.update({ projectID: project.id, directory: project.worktree, icon: { color } })
300319
}
301320
})

packages/app/src/pages/layout.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,11 +1018,16 @@ export default function Layout(props: ParentProps) {
10181018
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
10191019

10201020
async function renameProject(project: LocalProject, next: string) {
1021-
if (!project.id) return
10221021
const current = displayName(project)
10231022
if (next === current) return
10241023
const name = next === getFilename(project.worktree) ? "" : next
1025-
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
1024+
1025+
if (project.id && project.id !== "global") {
1026+
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
1027+
return
1028+
}
1029+
1030+
globalSync.project.meta(project.worktree, { name })
10261031
}
10271032

10281033
async function renameSession(session: Session, next: string) {

0 commit comments

Comments
 (0)