fix(cases): custom-folder create landing fixes (#535)

- Route test hygiene: each test works in its own mkdtemp folder, every
  deletion goes through safeRmHomeTree, and the suite refuses to start
  outside test/setup.ts's temp HOME, so a raw `npx vitest` can no longer
  delete a real ~/projects or the live linked-cases registry.
- Path policy: the symlink-resolved target is also judged against the
  resolved home, data dir and system roots (home reached through a link,
  macOS /etc -> /private/etc); test expectations are realpath-safe.
- Refuse a target equal to or inside the caller's or the shared cases
  directory, pointing at plain Create New (it would list twice, and
  deleting the local copy removes files).
- The registry re-read comment no longer claims to prevent the
  lost-update race; documented as narrowing it, like /api/cases/link.
- UI: the success toast names the folder the server created, the
  "under ~/codeman-cases" blurb and name hint change while a custom
  folder is ticked, a "/" parent previews and sends /<name> instead of
  an empty path, and the new labels have zh-CN entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-05 19:51:59 +02:00
1 parent 566365e127
commit 192a5994e0
9 files changed
+240 -28

No files matched your search

+17
View File
@@ -788,6 +788,22 @@
'现有项目文件夹的绝对路径,例如 /home/you/my-project',
'Letters, numbers, hyphens, underscores only. Created in ~/codeman-cases/':
'仅允许字母、数字、连字符和下划线;将在 ~/codeman-cases/ 中创建。',
'Letters, numbers, hyphens, underscores only. Created inside the parent folder below.':
'仅允许字母、数字、连字符和下划线;将在下方的父文件夹中创建。',
'A fresh workspace under ~/codeman-cases, scaffolded with its own CLAUDE.md.':
'在 ~/codeman-cases 下新建工作区,并生成独立的 CLAUDE.md。',
'A fresh workspace in a folder you choose, scaffolded with its own CLAUDE.md.':
'在你选择的文件夹中新建工作区,并生成独立的 CLAUDE.md。',
'Create in a custom folder': '在自定义文件夹中创建',
'📁 Create in a custom folder': '📁 在自定义文件夹中创建',
'By default a new case is created under ~/codeman-cases. Choose another folder and the case is created there instead; it is listed like any other case.':
'新案例默认创建在 ~/codeman-cases 下。选择其他文件夹后,案例会改为创建在那里,并像其他案例一样列出。',
'Parent Folder': '父文件夹',
'Pick the folder the new case folder should be created inside.': '选择要在其中创建新案例文件夹的文件夹。',
'Choose the folder to create the case in': '选择要在其中创建案例的文件夹',
'Not available for a Docker case': 'Docker 案例不可用',
'Not available with a custom folder': '使用自定义文件夹时不可用',
'Browse…': '浏览…',
'Docker exports': 'Docker 导出',
'No exports yet. Export a docker case from its tab.': '暂无导出;请从 Docker 案例标签页导出。',
'Runs inside an isolated container. Multiple sessions can share the same container.':
@@ -952,6 +968,7 @@
[/^Update available: v(.+)$/, (_m, version) => `有可用更新:v${version}`],
[/^Selected: (.+)$/, (_m, value) => `已选择:${value}`],
[/^Failed to (.+)$/, (_m, action) => `操作失败:${action}`],
[/^Will create: (.+)$/, (_m, path) => `将创建:${path}`],
// Group names are user text: they pass through untranslated.
[/^Move to "(.+)"$/, (_m, group) => `移到“${group}”`],
[
+2 -2
View File
@@ -3023,11 +3023,11 @@
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 8v8M8 12h8"/></svg>
<h2>Create New</h2>
</div>
<p class="set-section-blurb">A fresh workspace under ~/codeman-cases, scaffolded with its own CLAUDE.md.</p>
<p class="set-section-blurb" id="newCaseBlurb">A fresh workspace under ~/codeman-cases, scaffolded with its own CLAUDE.md.</p>
<div class="form-row">
<label>Case Name</label>
<input type="text" id="newCaseName" placeholder="my-project" pattern="[a-zA-Z0-9_-]+" autocomplete="off" autocapitalize="off" spellcheck="false" oninput="app.updateNewCasePathPreview()">
<span class="form-hint">Letters, numbers, hyphens, underscores only. Created in ~/codeman-cases/</span>
<span class="form-hint" id="newCaseNameHint">Letters, numbers, hyphens, underscores only. Created in ~/codeman-cases/</span>
</div>
<div class="form-row">
<label>Description (optional)</label>
+21 -3
View File
@@ -3356,6 +3356,19 @@ Object.assign(CodemanApp.prototype, {
const row = document.getElementById('newCaseCustomPathRow');
if (!custom || !row) return;
row.style.display = custom.checked ? '' : 'none';
// The "under ~/codeman-cases" wording is wrong while a custom folder is picked.
const blurb = document.getElementById('newCaseBlurb');
if (blurb) {
blurb.textContent = custom.checked
? 'A fresh workspace in a folder you choose, scaffolded with its own CLAUDE.md.'
: 'A fresh workspace under ~/codeman-cases, scaffolded with its own CLAUDE.md.';
}
const nameHint = document.getElementById('newCaseNameHint');
if (nameHint) {
nameHint.textContent = custom.checked
? 'Letters, numbers, hyphens, underscores only. Created inside the parent folder below.'
: 'Letters, numbers, hyphens, underscores only. Created in ~/codeman-cases/';
}
custom.disabled = !!docker?.checked;
custom.title = docker?.checked ? 'Not available for a Docker case' : '';
if (docker) {
@@ -3367,9 +3380,12 @@ Object.assign(CodemanApp.prototype, {
/** The folder the case would be created in: the parent field plus the case name. */
_newCaseTargetPath() {
const parent = (document.getElementById('newCasePath')?.value || '').trim().replace(/\/+$/, '');
const rawParent = (document.getElementById('newCasePath')?.value || '').trim();
const name = (document.getElementById('newCaseName')?.value || '').trim();
return parent && name ? `${parent}/${name}` : '';
if (!rawParent || !name) return '';
// Trailing slashes off, but `/` stays the root rather than becoming an empty path.
const parent = rawParent.replace(/\/+$/, '');
return `${parent}/${name}`;
},
updateNewCasePathPreview() {
@@ -3443,7 +3459,9 @@ Object.assign(CodemanApp.prototype, {
// Start a session INSIDE the container (routes through quick-start).
await this.runClaude();
} else {
this.showToast(customFolder ? `Case "${name}" created in ${payload.path}` : `Case "${name}" created`, 'success');
// The server's path is the folder actually created (~ expanded, symlinks resolved).
const createdIn = data.data?.case?.path || payload.path;
this.showToast(customFolder ? `Case "${name}" created in ${createdIn}` : `Case "${name}" created`, 'success');
}
} else {
this.showToast(data.error || 'Failed to create case', 'error');