diff --git a/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditorPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditorPanelWrapper.tsx
index 9013ca81..c709343a 100644
--- a/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditorPanelWrapper.tsx
+++ b/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditorPanelWrapper.tsx
@@ -1,3 +1,18 @@
+import { useWorkspace, cwdToPath } from '../../components/Workspace';
import { CodeEditorView } from './CodeEditor';
-export const CodeEditorPanelWrapper = () => ;
+/**
+ * The editor as a dashboard panel, rooted at the dashboard's folder.
+ *
+ * It ignored the workspace entirely and always opened the explorer at home, while `CodeEditorHeader` read
+ * `cwd` for itself and displayed the right folder — so a scoped dashboard showed `~/Tests/platform` in the
+ * header above a tree of the home directory. The two halves of the same panel disagreed because only one
+ * of them asked.
+ *
+ * No `urlState`: a dashboard can hold two editors and one `?file=` would drive both. Same reasoning as
+ * FileBrowserPanelWrapper, which is where this pattern came from and which had it right already.
+ */
+export const CodeEditorPanelWrapper = () => {
+ const { cwd } = useWorkspace();
+ return ;
+};
diff --git a/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx b/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx
index cbf3452d..17b0f76d 100644
--- a/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx
+++ b/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx
@@ -25,6 +25,7 @@ import {
NEW_DASH_TEMPLATE_KEY,
} from './constants';
import { useNewDashboardDraft } from './useNewDashboardDraft';
+import { cwdToPath } from '../../components/Workspace';
export const DashboardListApp = () => {
const location = useLocation();
@@ -74,7 +75,7 @@ export const DashboardListApp = () => {
setName(ws.name);
setDescription(ws.description ?? '');
setTemplateIdx(ws.templateIdx ?? 0);
- const cwdPath = !ws.cwd || ws.cwd === '~' ? '/' : ws.cwd.replace(/^~\//, '/');
+ const cwdPath = cwdToPath(ws.cwd);
setFilePath(cwdPath);
};
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx
index 56a3da55..e6e19113 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx
@@ -1,8 +1,6 @@
-import { useWorkspace } from '../../../components/Workspace';
+import { useWorkspace, cwdToPath } from '../../../components/Workspace';
import { FileBrowserApp } from './FileBrowserApp';
-const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1));
-
export const FileBrowserPanelWrapper = () => {
const { cwd, workspace } = useWorkspace();
const basePath = cwdToPath(cwd);
diff --git a/src/workspaces/officerdev/src/components/Workspace/cwd-path.test.ts b/src/workspaces/officerdev/src/components/Workspace/cwd-path.test.ts
new file mode 100644
index 00000000..848d8978
--- /dev/null
+++ b/src/workspaces/officerdev/src/components/Workspace/cwd-path.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, test } from 'bun:test';
+import { cwdToPath } from './cwd-path';
+
+// One conversion, because there were two and they disagreed — and a third was about to be written.
+
+describe('cwdToPath', () => {
+ test('the home shorthand is the root', () => {
+ expect(cwdToPath('~')).toBe('/');
+ });
+
+ test('a scoped dashboard resolves to its folder', () => {
+ expect(cwdToPath('~/Tests/platform')).toBe('/Tests/platform');
+ });
+
+ test('missing or empty is the root, not the empty string', () => {
+ // `''.slice(1)` gave `''`, which is not a path any endpoint accepts — it listed nothing rather than
+ // failing, which is the quiet kind of wrong.
+ expect(cwdToPath('')).toBe('/');
+ expect(cwdToPath(undefined)).toBe('/');
+ expect(cwdToPath(null)).toBe('/');
+ });
+
+ test('an already-absolute path is left alone', () => {
+ // `'/abs'.slice(1)` gave `'abs'` — a relative path, silently resolved against somewhere else.
+ expect(cwdToPath('/already/abs')).toBe('/already/abs');
+ });
+
+ test('spaces and punctuation are untouched — this is notation, not encoding', () => {
+ expect(cwdToPath('~/a b/c (2)')).toBe('/a b/c (2)');
+ });
+
+ test('a tilde inside a name is not a prefix and must not be rewritten', () => {
+ expect(cwdToPath('~/back~up')).toBe('/back~up');
+ });
+});
diff --git a/src/workspaces/officerdev/src/components/Workspace/cwd-path.ts b/src/workspaces/officerdev/src/components/Workspace/cwd-path.ts
new file mode 100644
index 00000000..244d840d
--- /dev/null
+++ b/src/workspaces/officerdev/src/components/Workspace/cwd-path.ts
@@ -0,0 +1,20 @@
+// A workspace's `cwd` as a path the file APIs accept.
+//
+// A dashboard is created against a directory, and every file-shaped panel in it should open there. The
+// framework stores that as a `cwd` in the shell's own notation — `~`, `~/Tests/platform` — while the file
+// endpoints take a path rooted at the home directory: `/`, `/Tests/platform`.
+//
+// This existed twice, spelled differently, and the two disagreed:
+//
+// cwd.slice(1) '' -> '' '/abs' -> 'abs' ← both wrong
+// cwd.replace(/^~\//, '/') '' -> '/' '/abs' -> '/abs' ← both right
+//
+// The second is the one kept. A third copy was about to be written for the code editor, which is what
+// prompted collapsing them: two spellings of one conversion is a bug waiting for whichever caller got the
+// short one.
+
+/** `~` and `~/x` become `/` and `/x`. An already-absolute path is returned unchanged. */
+export function cwdToPath(cwd: string | undefined | null): string {
+ if (!cwd || cwd === '~') return '/';
+ return cwd.replace(/^~\//, '/');
+}
diff --git a/src/workspaces/officerdev/src/components/Workspace/index.ts b/src/workspaces/officerdev/src/components/Workspace/index.ts
index 1d55dd7e..00286aae 100644
--- a/src/workspaces/officerdev/src/components/Workspace/index.ts
+++ b/src/workspaces/officerdev/src/components/Workspace/index.ts
@@ -33,6 +33,7 @@ export {
export type { WorkspaceIdentity } from './workspace-identity';
export { parseWorkspaceKey } from './workspace-identity';
export { WorkspaceProvider, useWorkspace } from './WorkspaceContext';
+export { cwdToPath } from './cwd-path';
export { usePanelFullscreen } from './panel-fullscreen';
export { usePanelConfig } from './usePanelConfig';
export { usePanelClose } from './usePanelClose';