Skip to content
Merged
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
14 changes: 10 additions & 4 deletions packages/app/src/pages/layout/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,13 @@ describe("layout workspace helpers", () => {
})

describe("sessionListDirectories", () => {
test("uses opened projects when present", () => {
test("unions opened and server projects, opened first", () => {
const opened = [{ worktree: "/a", sandboxes: ["/a-sbx"] }, { worktree: "/b" }]
const server = [{ worktree: "/server", sandboxes: ["/server-sbx"] }]
expect(sessionListDirectories(opened, server)).toEqual(["/a", "/a-sbx", "/b"])
expect(sessionListDirectories(opened, server)).toEqual(["/a", "/a-sbx", "/b", "/server", "/server-sbx"])
})

test("falls back to server projects when nothing is opened (fresh client, amicode#288)", () => {
test("returns only server projects when nothing is opened (fresh client, amicode#288)", () => {
const server = [
{ worktree: "/", sandboxes: ["/staging"] },
{ worktree: "/amicode" },
Expand All @@ -336,7 +336,13 @@ describe("layout workspace helpers", () => {
expect(sessionListDirectories([], server)).toEqual(["/", "/staging", "/amicode", "/opencode"])
})

test("fallback dedupes and drops empty entries", () => {
test("dedupes across opened and server projects", () => {
const opened = [{ worktree: "/a", sandboxes: ["/shared"] }]
const server = [{ worktree: "/a" }, { worktree: "/b", sandboxes: ["/shared"] }]
expect(sessionListDirectories(opened, server)).toEqual(["/a", "/shared", "/b"])
})

test("dedupes and drops empty entries from server-only path", () => {
const server = [
{ worktree: "/a", sandboxes: ["/a"] },
{ worktree: "/a" },
Expand Down
31 changes: 20 additions & 11 deletions packages/app/src/pages/layout/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,33 @@ export const displayName = (project: { name?: string; worktree: string }) =>

/**
* Directories whose sessions the list surfaces (Sessions dropdown, home list)
* aggregate over: the user's opened projects, falling back to the server's
* registered projects when nothing has been opened yet (fresh client, empty
* persisted registry — amicode#288). Session listing only; this must NOT feed
* the project switcher, or closing a server-registered project becomes
* impossible (the fallback would re-add it).
* — always the union of opened projects (first) and server-registered projects
* (deduplicated, appended). Every historical project the server knows about
* contributes sessions, not just the ones currently open. Session listing only;
* this must NOT feed the project switcher, or closing a server-registered
* project becomes impossible (amicode#839, replacing the fallback-only behavior
* from amicode#288).
*/
export function sessionListDirectories(
opened: { worktree: string; sandboxes?: string[] }[],
serverProjects: { worktree: string; sandboxes?: string[] }[],
): string[] {
const dirs = opened.flatMap((p) => [p.worktree, ...(p.sandboxes ?? [])])
if (dirs.length > 0) return dirs
const seen = new Set<string>()
return serverProjects.flatMap((p) => [p.worktree, ...(p.sandboxes ?? [])]).filter((d) => {
if (!d || seen.has(d)) return false
const result: string[] = []
const add = (d: string) => {
if (!d || seen.has(d)) return
seen.add(d)
return true
})
result.push(d)
}
for (const p of opened) {
add(p.worktree)
for (const s of p.sandboxes ?? []) add(s)
}
for (const p of serverProjects) {
add(p.worktree)
for (const s of p.sandboxes ?? []) add(s)
}
return result
}

export function toggleHomeProjectSelection(
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,11 @@ function ResolvedTargetSessionRoute() {
})

// Notify the extension which project this session is bound to so the
// sidebar highlight tracks the active tab. autoExpand=false: session
// navigation should only change the highlight, never toggle folder state.
// sidebar highlight tracks the active tab. mode="expand": opening a session
// expands its project folder if collapsed, without collapsing others.
createEffect(() => {
const dir = directory()
if (dir) notifyProjectSelected(dir, false)
if (dir) notifyProjectSelected(dir, "expand")
})

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function createPromptProjectControls() {
if (pathKey(worktree) === pathKey(sdk().directory)) {
const fallback = hiddenProjectWorktree()
if (search.draftId && fallback) {
notifyProjectSelected(fallback, true)
notifyProjectSelected(fallback, "reset")
tabs.updateDraft(search.draftId, { directory: fallback })
}
return
Expand Down
18 changes: 9 additions & 9 deletions packages/app/src/utils/amicode-workspace-projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,34 @@ describe("amicode workspace-projects — notifyProjectSelected (#663)", () => {
notifyProjectSelected("/Users/jj/harmoniqs")
expect(calls).toHaveLength(1)
const [msg, origin] = calls[0] as [Record<string, unknown>, string]
expect(msg).toEqual({ source: "amicode", kind: "project-selected", path: "/Users/jj/harmoniqs", autoExpand: true })
expect(msg).toEqual({ source: "amicode", kind: "project-selected", path: "/Users/jj/harmoniqs", mode: "reset" })
expect(origin).toBe("*")
} finally {
window.parent.postMessage = orig
}
})

test("explicit selection defaults to autoExpand=true", () => {
test("explicit selection defaults to mode 'reset'", () => {
const calls: unknown[] = []
const orig = window.parent.postMessage
window.parent.postMessage = (...args: unknown[]) => { calls.push(args) }
try {
notifyProjectSelected("/projects/foo")
const [msg] = calls[0] as [Record<string, unknown>]
expect(msg.autoExpand).toBe(true)
expect(msg.mode).toBe("reset")
} finally {
window.parent.postMessage = orig
}
})

test("session navigation passes autoExpand=false", () => {
test("session navigation passes mode 'expand'", () => {
const calls: unknown[] = []
const orig = window.parent.postMessage
window.parent.postMessage = (...args: unknown[]) => { calls.push(args) }
try {
notifyProjectSelected("/projects/foo", false)
notifyProjectSelected("/projects/foo", "expand")
const [msg] = calls[0] as [Record<string, unknown>]
expect(msg.autoExpand).toBe(false)
expect(msg.mode).toBe("expand")
} finally {
window.parent.postMessage = orig
}
Expand Down Expand Up @@ -85,11 +85,11 @@ describe("session-composer-controls — toggle deselect (#673)", () => {
expect(toggleBranch).toMatch(/notifyProjectSelected/)
})

test("deselect notification passes autoExpand true so the folder collapses", () => {
test("deselect notification passes mode 'reset' so the sidebar collapses the old folder", () => {
const selectFn = ctrlSrc.slice(ctrlSrc.indexOf("const selectProject"))
// The toggle branch's notifyProjectSelected call (before the main one)
const toggleBranch = selectFn.slice(0, selectFn.indexOf("notifyProjectSelected(worktree)"))
// Must NOT pass false — needs true (or default) so the sidebar collapses the old folder
expect(toggleBranch).not.toMatch(/notifyProjectSelected\([^)]*,\s*false/)
// Must NOT pass "none" — needs "reset" (or default) so the sidebar collapses the old folder
expect(toggleBranch).not.toMatch(/notifyProjectSelected\([^)]*,\s*["']none["']/)
})
})
14 changes: 9 additions & 5 deletions packages/app/src/utils/amicode-workspace-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ export function requestAddWorkspaceProject(): void {
}

/** Notify the extension host that the user selected a project in the dropdown.
* The extension forwards this to the sidebar (collapse others, expand selected).
* autoExpand=true (default) for explicit dropdown clicks; false for session
* navigation (highlight only, don't toggle folder state). */
export function notifyProjectSelected(worktree: string, autoExpand = true): void {
window.parent.postMessage({ source: "amicode", kind: "project-selected", path: worktree, autoExpand }, "*")
* The extension forwards this to the sidebar with a three-valued mode:
* - "reset" (default) — expand selected, collapse others (project selector)
* - "expand" — expand selected if collapsed, leave others alone (session open / tab switch)
* - "none" — highlight only, no folder state changes (orphan fallback, replay)
*/
export type ActiveProjectMode = "none" | "expand" | "reset"

export function notifyProjectSelected(worktree: string, mode: ActiveProjectMode = "reset"): void {
window.parent.postMessage({ source: "amicode", kind: "project-selected", path: worktree, mode }, "*")
}
Loading