summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/components/dialog-edit-project.tsx4
-rw-r--r--packages/app/src/context/layout.tsx62
-rw-r--r--packages/app/src/pages/layout.tsx2
-rw-r--r--packages/opencode/src/project/project.ts4
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts1
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts2
6 files changed, 51 insertions, 24 deletions
diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx
index 091f00702..7acb766f8 100644
--- a/packages/app/src/components/dialog-edit-project.tsx
+++ b/packages/app/src/components/dialog-edit-project.tsx
@@ -22,7 +22,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
const [store, setStore] = createStore({
name: defaultName(),
color: props.project.icon?.color || "pink",
- iconUrl: props.project.icon?.url || "",
+ iconUrl: props.project.icon?.override || "",
saving: false,
})
@@ -74,7 +74,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
await globalSDK.client.project.update({
projectID: props.project.id,
name,
- icon: { color: store.color, url: store.iconUrl },
+ icon: { color: store.color, override: store.iconUrl },
})
setStore("saving", false)
dialog.close()
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx
index a8a8ce1e9..d7d09aa39 100644
--- a/packages/app/src/context/layout.tsx
+++ b/packages/app/src/context/layout.tsx
@@ -208,10 +208,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
})
- const usedColors = new Set<AvatarColorKey>()
+ const [colors, setColors] = createStore<Record<string, AvatarColorKey>>({})
- function pickAvailableColor(): AvatarColorKey {
- const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c))
+ function pickAvailableColor(used: Set<string>): AvatarColorKey {
+ const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c))
if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)]
return available[Math.floor(Math.random() * available.length)]
}
@@ -222,24 +222,15 @@ 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 [
- {
- ...(metadata ?? {}),
- ...project,
- icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
+ return {
+ ...(metadata ?? {}),
+ ...project,
+ icon: {
+ url: metadata?.icon?.url,
+ override: metadata?.icon?.override,
+ color: metadata?.icon?.color,
},
- ]
- }
-
- function colorize(project: LocalProject) {
- if (project.icon?.color) return project
- const color = pickAvailableColor()
- usedColors.add(color)
- project.icon = { ...project.icon, color }
- if (project.id) {
- globalSdk.client.project.update({ projectID: project.id, icon: { color } })
}
- return project
}
const roots = createMemo(() => {
@@ -277,8 +268,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
})
- const enriched = createMemo(() => server.projects.list().flatMap(enrich))
- const list = createMemo(() => enriched().flatMap(colorize))
+ const enriched = createMemo(() => server.projects.list().map(enrich))
+ const list = createMemo(() => {
+ const projects = enriched()
+ return projects.map((project) => {
+ const color = project.icon?.color ?? colors[project.worktree]
+ if (!color) return project
+ const icon = project.icon ? { ...project.icon, color } : { color }
+ return { ...project, icon }
+ })
+ })
+
+ createEffect(() => {
+ const projects = enriched()
+ if (projects.length === 0) return
+
+ const used = new Set<string>()
+ for (const project of projects) {
+ const color = project.icon?.color ?? colors[project.worktree]
+ if (color) used.add(color)
+ }
+
+ for (const project of projects) {
+ if (project.icon?.color) continue
+ if (colors[project.worktree]) continue
+ const color = pickAvailableColor(used)
+ used.add(color)
+ setColors(project.worktree, color)
+ if (!project.id) continue
+ void globalSdk.client.project.update({ projectID: project.id, icon: { color } })
+ }
+ })
onMount(() => {
Promise.all(
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index a8f9b162f..9daac949e 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -1284,7 +1284,7 @@ export default function Layout(props: ParentProps) {
<div class="size-full rounded overflow-clip">
<Avatar
fallback={name()}
- src={props.project.id === opencode ? "https://opencode.ai/favicon-v2.svg" : props.project.icon?.url}
+ src={props.project.id === opencode ? "https://opencode.ai/favicon-v2.svg" : props.project.icon?.override}
{...getAvatarColors(props.project.icon?.color)}
class="size-full rounded"
style={
diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts
index 2cec78623..40ebb21ea 100644
--- a/packages/opencode/src/project/project.ts
+++ b/packages/opencode/src/project/project.ts
@@ -25,6 +25,7 @@ export namespace Project {
icon: z
.object({
url: z.string().optional(),
+ override: z.string().optional(),
color: z.string().optional(),
})
.optional(),
@@ -190,6 +191,7 @@ export namespace Project {
if (!existing.sandboxes) existing.sandboxes = []
if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing)
+
const result: Info = {
...existing,
worktree,
@@ -213,6 +215,7 @@ export namespace Project {
export async function discover(input: Info) {
if (input.vcs !== "git") return
+ if (input.icon?.override) return
if (input.icon?.url) return
const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}")
const matches = await Array.fromAsync(
@@ -293,6 +296,7 @@ export namespace Project {
...draft.icon,
}
if (input.icon.url !== undefined) draft.icon.url = input.icon.url
+ if (input.icon.override !== undefined) draft.icon.override = input.icon.override || undefined
if (input.icon.color !== undefined) draft.icon.color = input.icon.color
}
draft.time.updated = Date.now()
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 59b7f0696..706d0f9c2 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -302,6 +302,7 @@ export class Project extends HeyApiClient {
name?: string
icon?: {
url?: string
+ override?: string
color?: string
}
},
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 75540f907..b7e72fbad 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -25,6 +25,7 @@ export type Project = {
name?: string
icon?: {
url?: string
+ override?: string
color?: string
}
time: {
@@ -2229,6 +2230,7 @@ export type ProjectUpdateData = {
name?: string
icon?: {
url?: string
+ override?: string
color?: string
}
}