Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/app/src/context/global.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createEffect, createMemo, createRoot } from "solid-js"
import { createStore } from "solid-js/store"
import { createServerProjects, RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server"
import {
createServerProjects,
RECENTLY_CLOSED_DISPLAY_LIMIT,
ServerConnection,
useServer,
visibleProjectEntries,
} from "./server"
import { pathKey } from "@/utils/path-key"
import { useServerHealth } from "@/utils/server-health"
import { createServerSdkContext } from "./server-sdk"
Expand Down Expand Up @@ -127,7 +133,9 @@ function createServerCtx(
return base
}

const projectsList = createMemo(() => projects.list().map(enrich))
const projectsList = createMemo(() =>
visibleProjectEntries(projects.list(), sync.data.project, projects.recentlyClosed()).map(enrich),
)
const recentlyClosedList = createMemo(() => {
const known = new Set(sync.data.project.map((project) => pathKey(project.worktree)))
return projects
Expand Down
31 changes: 31 additions & 0 deletions packages/app/src/context/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
nextServerAfterRemoval,
resolveServerList,
ServerConnection,
visibleProjectEntries,
} from "./server"
import { ServerScope } from "@/utils/server-scope"

Expand Down Expand Up @@ -243,3 +244,33 @@ describe("migrateCanonicalLocalServerState", () => {
})
})
})

describe("visibleProjectEntries", () => {
test("shows server projects until the first bookmark exists", () => {
expect(visibleProjectEntries([], [{ worktree: "/repo/a" }, { worktree: "/repo/b" }])).toEqual([
{ worktree: "/repo/a", expanded: false },
{ worktree: "/repo/b", expanded: false },
])
})

test("keeps server projects after the first bookmark appears", () => {
const bookmarked = [{ worktree: "/repo/a", expanded: true }]
expect(visibleProjectEntries(bookmarked, [{ worktree: "/repo/a" }, { worktree: "/repo/b" }])).toEqual([
{ worktree: "/repo/a", expanded: true },
{ worktree: "/repo/b", expanded: false },
])
})

test("deduplicates server projects already bookmarked by normalized path", () => {
const bookmarked = [{ worktree: "/repo/a/", expanded: true }]
expect(visibleProjectEntries(bookmarked, [{ worktree: "/repo/a" }])).toEqual([
{ worktree: "/repo/a/", expanded: true },
])
})

test("hides server projects the user closed", () => {
expect(visibleProjectEntries([], [{ worktree: "/repo/a" }, { worktree: "/repo/b" }], ["/repo/a"])).toEqual([
{ worktree: "/repo/b", expanded: false },
])
})
})
20 changes: 20 additions & 0 deletions packages/app/src/context/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,26 @@ export function createServerProjects<T extends ServerProjectState>(input: {
}
}

// Bookmarks are authoritative and keep their order. Server projects stay discoverable alongside
// them so a fresh web client (which never seeds bookmarks) still shows the `/project` list, and so
// bookmarks added later do not evict the remaining server projects. `hidden` is the per-server
// recently-closed set: server projects the user explicitly closed stay out until reopened.
export function visibleProjectEntries(
bookmarked: ReadonlyArray<StoredProject>,
server: ReadonlyArray<{ worktree: string }>,
hidden: ReadonlyArray<string> = [],
): StoredProject[] {
const seen = new Set(bookmarked.map((project) => pathKey(project.worktree)))
const hiddenKeys = new Set(hidden.map((project) => pathKey(project)))
const discovered = server.flatMap((project) => {
const key = pathKey(project.worktree)
if (hiddenKeys.has(key) || seen.has(key)) return []
seen.add(key)
return [{ worktree: project.worktree, expanded: false }]
})
return [...bookmarked, ...discovered]
}

export function resolveServerList(input: {
props?: Array<ServerConnection.Any>
stored: StoredServer[]
Expand Down
Loading