fix(file-picker): default the case picker to Codeman Cases, not Home

The "Link Existing" case picker opens with an empty path and no
sessionId, so the browse endpoint's fallback root picked whichever
root happened to be first in the list — which was always `Home`.

On the native default that's harmless (~/codeman-cases nests inside
Home anyway), but a Docker deployment binds CODEMAN_APPDATA_PATH
(Home) and CODEMAN_CASES_PATH at unrelated host paths, so the picker
opened somewhere with no cases in sight. Worse: if CODEMAN_CASES_PATH
is ever changed after cases already exist, the old cases directory
lingers, still reachable, under Home — indistinguishable at a glance
from the real one under the new Codeman Cases root.

Prefer the Codeman Cases root in the fallback chain, ahead of the
generic roots[0].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
This commit is contained in:
Devvyn
2026-09-05 17:14:05 +08:00
co-authored by Claude Sonnet 5
parent 6f7add7ce4
commit 06febfa032
2 changed files with 29 additions and 1 deletions
+14 -1
View File
@@ -536,8 +536,21 @@ async function resolveFilesystemPickerPath(
throwFilesystemPickerError(403, ApiErrorCode.INVALID_INPUT, 'No filesystem browse roots are available');
}
// With no explicit path (the "Link Existing" case picker, which passes no
// sessionId and an empty initialPath until the user has typed something),
// land on the shared cases root rather than falling through to whichever
// root happens to be first. `Home` is only nested under `Codeman Cases` on
// the native default (~/codeman-cases); a Docker deployment binds them at
// unrelated host paths (CODEMAN_APPDATA_PATH vs CODEMAN_CASES_PATH), so a
// Home-first fallback opened the picker somewhere with no cases in sight —
// and, worse, made an OLD case folder left behind by a since-changed
// CODEMAN_CASES_PATH look like a normal thing to stumble across while
// browsing for one to link.
const fallbackRoot =
roots.find((root) => root.label === 'Current Folder') ?? roots.find((root) => root.path === '/mnt/d') ?? roots[0];
roots.find((root) => root.label === 'Current Folder') ??
roots.find((root) => root.label === 'Codeman Cases') ??
roots.find((root) => root.path === '/mnt/d') ??
roots[0];
const candidatePath = resolve(requestedPath ?? fallbackRoot.path);
let resolvedPath: string;
+15
View File
@@ -10,6 +10,7 @@ import { Readable } from 'node:stream';
import { createRouteTestHarness, type RouteTestHarness } from './_route-test-utils.js';
import { registerFileRoutes } from '../../src/web/routes/file-routes.js';
import { ApiErrorCode } from '../../src/types.js';
import { CASES_DIR } from '../../src/web/route-helpers.js';
// Mock fs/promises for file operations
vi.mock('node:fs/promises', () => ({
@@ -114,6 +115,20 @@ describe('file-routes', () => {
]);
});
it('defaults to the Codeman Cases root, not Home, when linking a case with no path chosen yet', async () => {
// The "Link Existing" case picker opens with an empty path and no
// sessionId. `Home` and `Codeman Cases` are unrelated bind mounts under
// Docker, so falling back to whichever root happened to be listed first
// could open the picker somewhere with no cases in it at all — and, worse,
// make a stale directory from a since-changed CODEMAN_CASES_PATH look like
// a normal thing to stumble across while browsing for one to link.
const res = await harness.app.inject({ method: 'GET', url: '/api/filesystem/browse' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.data.path).toBe(CASES_DIR);
});
it('rejects paths outside the configured roots', async () => {
const res = await harness.app.inject({
method: 'GET',