Skip to content

feat(ui): add workspace-root navigation to directory browser - #724

Merged
pascalandr merged 10 commits into
NeuralNomadsAI:devfrom
bluelovers:pr/root-001
Sep 24, 2026
Merged

pascalandr merged 10 commits into
NeuralNomadsAI:devfrom
bluelovers:pr/root-001

Conversation

@bluelovers

Copy link
Copy Markdown
Contributor

Summary

Adds a "Workspace Root" button to the directory browser dialog that navigates the view back to the configured workspace root (FileSystemListingMetadata.rootPath).

What changed

  • New button: a Home-icon button that calls navigateTo(rootPath()) to jump back to the workspace root. It is disabled when there is no root path, when the user is already at the root, or while a folder is being created (driven by a new canGoToWorkspaceRoot memo).
  • Placement / separation: the button lives in its own grid cell to the right of the "Select folder or enter path" label, fully separated from the New Folder and Open actions. It uses a new transparent selector-button-ghost variant so it reads as a navigation tool rather than a filled action button.
  • i18n: added directoryBrowser.goToWorkspaceRoot for en plus de, es, fr, he, ja, ne, ru, tr, and zh-Hans (used for tooltip/aria-label).
  • Layout: .directory-browser-current now uses a four-column grid; the flexible middle column pushes New Folder to the far right and keeps the root button next to the label. Responsive rules stack the controls at <=640px and <=380px.

Edge cases

  • In files mode the New Folder action is hidden, but the Workspace Root button stays available so users can still jump back to the root.
  • For the Windows drives pseudo-view rootPath is empty, so the button is disabled.

Validation

  • No typecheck was run in this environment (npx is disallowed here); manual browser verification of spacing and the localized label is recommended.

🤖 Generated with CodeNomad

Add a "Workspace Root" button (Home icon) to the directory browser dialog that navigates the view back to the configured workspace root (FileSystemListingMetadata.rootPath).

Implementation:
- The button is its own grid cell placed to the right of the "Select folder or enter path" label, fully separated from the New Folder and Open actions. It uses a new transparent `selector-button-ghost` variant so it reads as a navigation tool rather than a filled action button.
- Clicking it calls navigateTo(rootPath()); it is disabled when there is no root path, when the user is already at the root, or while a folder is being created.
- A `canGoToWorkspaceRoot` memo drives the disabled state.

i18n:
- Add `directoryBrowser.goToWorkspaceRoot` in en plus de, es, fr, he, ja, ne, ru, tr and zh-Hans so the tooltip/aria-label are localized.

Layout details:
- `.directory-browser-current` now uses a four-column grid; the flexible middle column pushes New Folder to the far right and keeps the root button adjacent to the label. Responsive rules stack the controls at <=640px and <=380px.

Edge cases:
- In files mode the New Folder action is hidden; the Workspace Root button remains available so users can still jump back to the root. For the Windows drives pseudo-view rootPath is empty, so the button is disabled.

Validation:
- No typecheck run (npx disallowed in this environment); manual browser verification of spacing/divider and locale labels is recommended.
@github-actions

Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/35502137445

Artifacts expire in 7 days.
Artifacts:

  • pr-724-f0f7b5025e5bf97c400d3f45012fa910c4703fde-tauri-linux
  • pr-724-f0f7b5025e5bf97c400d3f45012fa910c4703fde-electron-macos
  • pr-724-f0f7b5025e5bf97c400d3f45012fa910c4703fde-tauri-macos-arm64
  • pr-724-f0f7b5025e5bf97c400d3f45012fa910c4703fde-tauri-windows
  • pr-724-f0f7b5025e5bf97c400d3f45012fa910c4703fde-electron-linux
  • session-pruning-plugin-macos-latest
  • pr-724-f0f7b5025e5bf97c400d3f45012fa910c4703fde-tauri-macos
  • pr-724-f0f7b5025e5bf97c400d3f45012fa910c4703fde-electron-windows
  • session-pruning-plugin-windows-latest
  • session-pruning-plugin-ubuntu-latest

@pascalandr
pascalandr self-requested a review September 20, 2026 11:44

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convenience is understandable, but the destination needs an explicit product contract before this shared control is added.

FileSystemListingMetadata.rootPath is the server browsing/authorization origin, not necessarily the dialog's starting folder or the active workspace. Electron and Tauri launch the server with --unrestricted-root and no explicit --workspace-root, so this value defaults to the server process working directory. The shared dialog is used for workspace selection, clone destinations, OpenCode binaries, and file attachments, each with different initialPath semantics. A Home icon labelled “Workspace Root” therefore has no stable meaning across callers.

Please first choose the intended destination and model it explicitly: capture the canonical initial location for “back to start”, pass an active workspace target from callers, use homePath for filesystem home, or clearly expose the configured browsing root. The editable path field does not provide history. CodeNomad currently persists recent opened project folders, but has no generic directory history or favourites; those should not be conflated. A per-dialog start shortcut or back/forward history can stay local, while persistent favourites deserve a separate scoped design.

There are also two confirmed implementation regressions in the inline comments. Please add a real browser regression covering directory and file modes at desktop and narrow widths, plus the edited-path navigation case.

type="button"
class="selector-button selector-button-ghost directory-browser-go-root"
disabled={!canGoToWorkspaceRoot()}
onClick={() => void navigateTo(rootPath())}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bypasses handleNavigateTo(), which deliberately clears pathInputDirty before changing directories. Reproduction: navigate below the root, edit the path field without submitting it, then click this button. The listing moves to the root but the field keeps the stale edited path; the next Open or New Folder action operates on that stale target. Route this action through the shared navigation handler (or otherwise clear/synchronize the edit state) and cover the sequence with a regression test.

flex-shrink: 0;
}

.directory-browser-open-path {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grid templates still reserve an open area, but this rule no longer assigns grid-area: open. Chromium therefore auto-places Open into the desktop spacer on the label row; below 640px it occupies only half the intended row, and in files mode it lands beside the root button. Restore the explicit area assignment. Please cover both dialog modes and responsive layouts so this cannot regress silently.

@github-actions

Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/35512118379

Artifacts expire in 7 days.
Artifacts:

  • pr-724-71e90f1d1231e7893cbb99d9807f98bd33774d4c-tauri-windows
  • pr-724-71e90f1d1231e7893cbb99d9807f98bd33774d4c-electron-windows
  • pr-724-71e90f1d1231e7893cbb99d9807f98bd33774d4c-tauri-macos-arm64
  • pr-724-71e90f1d1231e7893cbb99d9807f98bd33774d4c-electron-macos
  • session-pruning-plugin-macos-latest
  • pr-724-71e90f1d1231e7893cbb99d9807f98bd33774d4c-electron-linux
  • pr-724-71e90f1d1231e7893cbb99d9807f98bd33774d4c-tauri-linux
  • session-pruning-plugin-ubuntu-latest
  • session-pruning-plugin-windows-latest
  • pr-724-71e90f1d1231e7893cbb99d9807f98bd33774d4c-tauri-macos

Address the directory-browser review on PR NeuralNomadsAI#724.

Replace the single "Workspace Root" button with conditional, de-duplicated navigation shortcuts driven by FileSystemListingMetadata:
- workspace-root shortcut only when scope is "restricted" (rootPath is the configured browsing root),
- user-home shortcut only when scope is "unrestricted" (uses homePath),
- initial-path shortcut when initialPath is provided.
Identical absolute targets collapse into a single button.

Fix Regression A: shortcut clicks clear the edited-path state (setPathInputDirty(false)) before navigating, so a stale manually-edited path field no longer leaks into the next Open / New Folder action.

Fix Regression B: restore grid-area: open on .directory-browser-open-path so the Open button is explicitly placed instead of being auto-placed into the label-row spacer at desktop widths and below 640px (and beside the root button in files mode).

Add a browser regression test (directory-browser.test.ts + fixture) covering restricted/unrestricted scopes, dedupe, files mode, narrow widths, and the edited-path navigation case. Add i18n keys directoryBrowser.goToHome and directoryBrowser.goToInitial across all 10 locales.
Rename the workspace-root shortcut to a "Default Start Directory" shortcut (i18n key directoryBrowser.goToRoot) so the label reflects the resolved root semantics (CLI_WORKSPACE_ROOT -> --root -> cwd) instead of a parameter name.

Show the start-directory shortcut whenever rootPath is present in BOTH restricted and unrestricted modes. This matches issue NeuralNomadsAI#370, where CLI_WORKSPACE_ROOT is the intended default path even under --unrestricted-root, so the server's resolved root is a valid jump target regardless of scope.

Keep the home shortcut limited to unrestricted scope (its target is outside the authorized root in restricted mode) and dedupe it against rootPath. The initial-path shortcut is unchanged. All three shortcuts are still de-duplicated by their canonical absolute path.

Update i18n keys/labels across all 10 locales and adjust the browser regression test: unrestricted scenarios now yield root + home + initial (3 shortcuts), and the home shortcut is selected by index.

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The destination contract is now much clearer, and the two production regressions from the previous review are corrected: shortcut navigation clears the edited-path state and grid-area: open is restored.

This HEAD is still not merge-ready. The new browser suite never mounts the dialog and all five tests fail in required CI; after fixing the fixture, two assertions are themselves guaranteed to fail. There is also a user-visible mismatch in how a relative initial path is resolved in unrestricted mode, plus a broken shortcut after initial-path fallback. Please address the inline findings and rerun the browser suite. No merge performed.

let navigations: string[] = []
const [open, setOpen] = createSignal(true)

render(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DirectoryBrowserDialog calls useI18n() immediately, so rendering it without I18nProvider throws before the dialog mounts; I18nProvider in turn requires ConfigProvider. This is why all five new tests time out waiting for .directory-browser-current-path in required CI instead of exercising any assertions. Wrap this fixture in the same ConfigProvider + I18nProvider stack used by the other real-component fixtures (and preferably surface pageerror so fixture startup failures fail fast).

}
const initial = props.initialPath?.trim()
if (initial) {
const target = isAbsolutePathLike(initial) ? initial : resolveAbsolutePath(meta.rootPath, initial)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not preserve the server's initialPath semantics. initialize() sends a relative initial path unchanged, and unrestricted FileSystemBrowser.resolveUnrestrictedPath() resolves it against homePath; this line instead rewrites it under rootPath. With root /srv/start, home /home/user, and initial projects, the dialog opens /home/user/projects but Initial Path jumps to /srv/start/projects. The button also remains after an invalid/missing initial path made initialize() fall back to the default root, leaving a permanently failing shortcut. Capture the canonical absolute path only from a successful initial navigation and use that for the target/deduplication; omit the shortcut after fallback.

// initialPath equals rootPath, so the workspace + initial shortcuts collapse into one.
await openFixture(page, scenario, "initialPath=/ws&mode=directories")
assert.equal(await shortcutCount(page), 1)
await page.locator(".directory-browser-shortcut").first().click()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the fixture mounts, this click still cannot succeed: the initial path is /ws, the sole deduplicated shortcut also targets /ws, and atTarget() therefore disables the button. Playwright will wait for it to become enabled and time out. Assert that the deduplicated button is disabled here, or initialize below the root before testing navigation.

assert.equal(await page.locator(".directory-browser-open-path").count(), 1)
// Open button should span the full row at this width, not half.
const box = await page.locator(".directory-browser-open-path").boundingBox()
assert.ok(box !== null && box.width >= 300, `open button width ${box?.width} should fill the row`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a 360 px viewport the modal is at most 90vw and the body has 48 px horizontal padding, so the Open button cannot be 300 px wide; this assertion will fail after the fixture is repaired. Compare its width/edges with the grid content box instead of a hard-coded viewport threshold. Also, 360 px exercises the separate <=380px template, not the 381–640px two-column breakpoint where the original half-row/file-mode regression occurred; please cover that breakpoint in both directory and file modes as requested.

…tics

Address the latest PR NeuralNomadsAI#724 review.

Fixture:
- Wrap DirectoryBrowserDialog in ConfigProvider + I18nProvider + ThemeProvider (the dialog calls useI18n() at mount, so without the providers it threw and every test timed out). Surface pageerror so fixture startup failures fail fast.

Component:
- Capture the Initial Path shortcut target from a *successful* initial navigation via getAbsolutePathFromMetadata, instead of re-resolving props.initialPath under rootPath. The server resolves relative initial paths against homePath in unrestricted mode, so the old code opened one location but the button jumped to another. Now the target equals exactly where the dialog opened.
- Omit the Initial Path shortcut when the initial path is rejected and initialize() falls back to the default root, so no broken/permanently-disabled shortcut remains.

Tests:
- Fix the dedupe test: assert the collapsed shortcut is disabled (it targets the current path) instead of clicking a disabled button.
- Replace the hard-coded 300px width assertion with a comparison against the grid content box, and cover the 381-640px two-column breakpoint in both directory and file modes.
- Assert no pageerror in every test.

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixture now mounts and capturing the canonical result of the initial navigation is the right direction. This HEAD is still not merge-ready: the required browser job fails both new responsive assertions, the captured initial shortcut can survive a later fallback when the mounted dialog is reopened, and the relative-path contract being fixed is not covered by the fixture. Please address the inline findings and rerun the focused browser suite.

const [loadingPaths, setLoadingPaths] = createSignal<Set<string>>(new Set())
const [currentPathKey, setCurrentPathKey] = createSignal<string | null>(null)
const [currentMetadata, setCurrentMetadata] = createSignal<FileSystemListingMetadata | null>(null)
const [initialAbsolutePath, setInitialAbsolutePath] = createSignal("")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new per-opening value is not cleared by resetState(). The dialog component remains mounted while open is false, so a successful first opening can leak its captured target into a later opening whose initialPath is absent or rejected. After fallback to the default root, the old Initial Path shortcut is shown and points back to the stale/failing location, contrary to the comment in initialize(). Clear the signal before each initialization and cover successful open → close → fallback/reopen.

const openBox = await page.locator(".directory-browser-open-path").boundingBox()
assert.ok(bodyBox && openBox, "layout boxes must be measurable")
// Open spans the full content row at the 381-640px two-column breakpoint.
assert.ok(openBox.width >= bodyBox.width - 16, `open width ${openBox.width} should fill content width ${bodyBox.width}`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still compares against the body border box rather than the grid content box. .directory-browser-body includes horizontal padding, so required CI on this exact HEAD measures the button at 512px and the body at 536px; this assertion and its file-mode copy at line 203 both fail. Compare the button with .directory-browser-current (preferably matching left/right edges with a small subpixel tolerance) instead.

parentPath: scenario.parentPath,
rootPath: scenario.rootPath,
homePath: scenario.homePath,
displayPath: currentPath,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixture forces every response to pathKind: "absolute" and echoes the requested path, while every test supplies an absolute initialPath. Consequently the regression motivating the production change—an unrestricted relative initial path being canonicalized under homePath rather than rootPath—would not be caught here. Add a scenario such as initialPath=projects whose fake server returns /home/projects, navigate away, then assert that the Initial Path shortcut returns to that canonical target.

…h contract

Address the third PR NeuralNomadsAI#724 review round.

Component:
- Clear the initialAbsolutePath signal in resetState() so a successful first opening no longer leaks its captured target into a later reopen whose initialPath is absent or rejected (the mounted dialog stays alive while open is false).

Fixture:
- Make initialPath a signal and expose setInitialPath/open/close so tests can drive close -> change initialPath -> reopen on the same mounted component.
- Mirror server canonicalization in the fake server: a relative initial path resolves under homePath in unrestricted mode and under rootPath in restricted mode, instead of always echoing the absolute value.

Tests:
- Add a relative-initial-path test (scope=unrestricted, initialPath=projects) asserting the dialog opens at /home/user/projects and the Initial Path shortcut returns there after navigating away, covering the regression that motivated the production change.
- Add a close/reopen leak test: open with /ws/start (2 shortcuts), close, drop initialPath, reopen -> only the start-directory shortcut remains (1), proving resetState clears the captured target.
- Fix both two-column (600px) responsive assertions to compare the Open button's left/right edges against the .directory-browser-current grid box (within 2px) instead of the padded .directory-browser-body, in both directory and file modes.
pascalandr
pascalandr previously approved these changes Sep 23, 2026

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the updated HEAD 77c3d2a6296c7ad66448ed8916b9c99ff82ab8ea. The outstanding findings from my previous review are addressed; no remaining blocking finding in the directory-browser changes.

Confirmed:

  • resetState() clears the captured canonical initial target before each opening, preventing the old shortcut from surviving a subsequent opening without an initial path.
  • Both responsive tests now compare the Open button's left/right edges against .directory-browser-current, at the actual two-column breakpoint and in both dialog modes.
  • The relative-path fixture now models unrestricted resolution under homePath, and the regression navigates away and back to the canonical initial location.
  • The prior fixes remain in place: provider-backed fixture, edited-path synchronization, explicit Open grid area, and distinct/deduplicated shortcut destinations.

Validation on this exact HEAD in an isolated worktree:

  • UI typecheck: passed.
  • node --import tsx --test tests/browser/directory-browser.test.ts: 8 passed, 0 failed.

CI/integration follow-up before merge: run 35870907908 is still red and did not reach the browser suite. Its current workflow invokes files absent from this older branch (view-menu.test.ts, runtime-support.ts, test-session-aside-native.mjs, and test-opencode-install-native.mjs). Tauri Windows additionally reports two cross-host client-state election failures (178 passed, 2 failed), outside this PR's diff. Please synchronize with current dev and rerun required CI; this code approval is not a claim that the global checks pass. No merge performed.

@pascalandr pascalandr left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--

Replace the three persistent root/home/initial icons with an address field that reveals a named return destination, Home when unrestricted, and the parent when useful. Filter already loaded child directories only after typing, preserving direct path entry while keeping Open and New Folder visible in narrow layouts. Share the address presentation across every caller of the directory browser and localize its labels.

Fence late filesystem responses across dialog reopenings so an older visit cannot replace the new location or populate its cache. Cover canonical relative starts, failed starts, keyboard and Escape behavior, stale responses, and file/directory layouts at both responsive breakpoints with real browser fixtures.

Validation: 11 directory-browser Playwright scenarios, UI typecheck, UI build, and staged diff check passed. Reviewed desktop and narrow rendered captures.
Keep Parent folder in the address destinations and remove the redundant Up one level row from the file and directory lists. This keeps return navigation available while avoiding two controls for the same destination in the same dialog.

Add browser coverage for both dialog modes so the parent remains reachable from the address and cannot reappear as a duplicate folder row. Include real English browser captures of contextual destinations and typed child-folder suggestions to illustrate the PR interaction to its author.

Validation: all 13 focused directory-browser browser scenarios and UI typecheck passed; reviewed both screenshot captures and the staged diff.
@pascalandr

pascalandr commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

@bluelovers I pushed a follow-up to this PR (55e25b8a) after revisiting the UX. Thank you for working through the earlier review rounds. The implementation was technically sound, I was not convinced that three permanent icons for the server root, Home, and the initial path were easy to understand across this dialog's different uses.

Here are actual browser captures of the updated shared directory browser (English locale, deterministic example paths). These show the interaction I described above:

Focus after moving away from the starting folder — “Back to start” and Home appear under the path field; the normal folder list is below it.

Directory browser with contextual Back to start and Home destinations

Typing /srv/start/pic — matching pictures is offered for keyboard or mouse selection, alongside the available location shortcuts.

Path field suggesting the matching pictures folder

I noticed the old “Up one level” list row repeated the Parent folder destination in the address. That duplication is fixed in cfd7a05a: parent navigation stays in the address, and the redundant row is gone in both folder and file modes (13 focused browser scenarios pass). Does the resulting UI and interaction work for you?

@pascalandr
pascalandr merged commit b13e800 into NeuralNomadsAI:dev Sep 24, 2026
11 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants