Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Fixes
- `lib/opencode-projects.ts`: align `ProjectFolder.name` for OpenCode projects with the URL-slug encoding every other CLI uses (`encodeFolderName(worktree)`, e.g. `-home-nivedit-dev-purge`), replacing the previous `proj.name`/`basename(worktree)` fallback chain that produced bare names like `dev-purge`. The `[name]` route handler resolves OpenCode sessions via `encodeFolderName(p.worktree) === name` in `getOpenCodeSessionsByEncodedName`, and the Projects-list link in `app/components/project-list.tsx:330` is built from `folder.name` — so an OpenCode-only project at `/home/nivedit/dev-purge` was emitting `/project/dev-purge`, which 404'd, while the session-row URL built elsewhere via `encodeCwdForUrl(cwd)` correctly used `-home-nivedit-dev-purge` and worked. Side benefit: `mergeProjectFolders` (`lib/projects.ts:107`) keys by `f.name`, so OpenCode projects now merge with their Claude / Codex / Copilot / Cursor / Pi / Gemini siblings into a single row with multiple CLI badges instead of appearing as two separate rows. Display via `decodeFolderName(folder.name)` in the Projects table now shows the full path (`/home/nivedit/dev-purge`) instead of the prior `dev/purge`, matching every other CLI. Three test assertions in `__tests__/lib/opencode-projects.test.ts` updated to pin the encoded form (`-repo`, `-other`); existing `getOpenCodeSessionsByEncodedName` tests already asserted the correct contract and didn't need changes (#336).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## 0.0.10-beta.9 — 2026-05-09

### Features
Expand Down
7 changes: 4 additions & 3 deletions __tests__/lib/opencode-projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ describe("getOpenCodeProjects", () => {
const projects = await getOpenCodeProjects();
expect(projects).toHaveLength(2);
// Newest first — p1 has time_updated=200, p2 has 50.
expect(projects[0].name).toBe("repo"); // basename(/repo) — name was null
// `name` is the URL slug = encodeFolderName(worktree) — matches every other CLI.
expect(projects[0].name).toBe("-repo");
expect(projects[0].path).toBe("/repo");
expect(projects[0].cli).toEqual(["opencode"]);
expect(projects[0].lastModified.getTime()).toBe(200);
expect(projects[1].name).toBe("Other Project");
expect(projects[1].name).toBe("-other");
expect(projects[1].lastModified.getTime()).toBe(50);
});

Expand All @@ -86,7 +87,7 @@ describe("getOpenCodeProjects", () => {
]);
const projects = await getOpenCodeProjects();
expect(projects).toHaveLength(1);
expect(projects[0].name).toBe("Empty Project");
expect(projects[0].name).toBe("-repo");
expect(projects[0].lastModified.getTime()).toBe(10);
});

Expand Down
16 changes: 9 additions & 7 deletions lib/opencode-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
* https://opencode.ai/docs/plugins/ (plugin model context)
*/
import { execFileSync } from "node:child_process";
import { basename } from "node:path";
import { encodeFolderName } from "./paths";
import type { ProjectFolder, SessionFile } from "./projects";
import { runtimeCache } from "./runtime-cache";
Expand Down Expand Up @@ -91,10 +90,11 @@ function readProjectRows(): OpenCodeProjectRow[] | null {

/**
* Group sessions by `project_id` and produce one ProjectFolder per project.
* The folder name comes from `project.name` when set, else `basename(worktree)`,
* else the project_id (last-resort). `lastModified` is the max session
* `time_updated` for that project (or the project's own time_updated if no
* sessions exist yet).
* The folder name is `encodeFolderName(worktree)` (matches every other CLI's
* URL-slug encoding so the dashboard's `/project/[name]` route resolves), or
* the project_id when no worktree is recorded. `lastModified` is the max
* session `time_updated` for that project (or the project's own time_updated
* if no sessions exist yet).
*/
export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {
const sessions = readSessionRows();
Expand Down Expand Up @@ -122,13 +122,15 @@ export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {

// Emit one ProjectFolder per project that has at least one session OR a
// project row (covers projects opencode knows about but hasn't run yet).
// `name` is the dashboard's URL slug — must be `encodeFolderName(cwd)` to
// match every other CLI (and the resolver in `getOpenCodeSessionsByEncodedName`).
const seen = new Set<string>();
const out: ProjectFolder[] = [];
for (const [projectId, group] of groups) {
seen.add(projectId);
const proj = projectMap.get(projectId);
const worktree = proj?.worktree ?? group.rows[0]?.directory ?? null;
const name = proj?.name?.trim() || (worktree ? basename(worktree) : projectId);
const name = worktree ? encodeFolderName(worktree) : projectId;
const path = worktree ?? "";
const lastModified = new Date(Math.max(group.latest, proj?.time_updated ?? 0));
out.push({
Expand All @@ -143,7 +145,7 @@ export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {
for (const p of projects ?? []) {
if (seen.has(p.id)) continue;
const worktree = p.worktree ?? "";
const name = p.name?.trim() || (worktree ? basename(worktree) : p.id);
const name = worktree ? encodeFolderName(worktree) : p.id;
const lastModified = new Date(p.time_updated);
out.push({
name,
Expand Down