Skip to content

feat(desktop): edit project folders — move/redirect from UI - #61406

Closed
Clement-ccy wants to merge 4 commits into
NousResearch:mainfrom
Clement-ccy:feat/desktop-edit-project-folders
Closed

feat(desktop): edit project folders — move/redirect from UI#61406
Clement-ccy wants to merge 4 commits into
NousResearch:mainfrom
Clement-ccy:feat/desktop-edit-project-folders

Conversation

@Clement-ccy

@Clement-ccy Clement-ccy commented Jul 9, 2026

Copy link
Copy Markdown

Summary

Closes the missing half of the project folder lifecycle. The desktop already supports add folder and remove folder, but had no way to redirect an existing folder to a new absolute path after it was renamed or moved on disk — users had to remove and re-add, losing is_primary / label / added_at and the project's primary_path pointer in the process.

This PR adds the edit/move capability end-to-end across every layer, plus a remove button in the edit dialog that closes the last UX gap (the desktop had no frontend wiring for projects.remove_folder even though the backend, RPC, and CLI all shipped it).

What's new

Layer Addition
hermes_cli/projects_db.py move_folder(conn, project_id, old_path, new_path) — rewrites a folder's path in place, preserves is_primary / label / added_at, retargets projects.primary_path when the moved folder was primary, rejects same-project collisions.
tui_gateway/server.py projects.move_folder JSON-RPC method (registered alongside add_folder / remove_folder / set_primary). Bad-arg and unknown-project map to the existing 5063 / 5062 codes.
hermes_cli/projects_cmd.py hermes project move-folder <slug> <old> <new> CLI subcommand, dispatched via the existing _with_project decorator so collisions / unknown paths return rc=2.
apps/desktop/src/store/projects.ts moveProjectFolder + removeProjectFolder store actions. Both Apollo-style optimistic: retarget the cached $projects / $projectTree atoms in place, roll the snapshot back on failure. removeProjectFolder is brand-new — the desktop frontend had no path to drop a folder while keeping the project.
apps/desktop/.../project-dialog.tsx New edit-folders mode: shows the project's existing folders, lets the user (a) pick a new path per row (move/redirect) or (b) toggle a trash button per row (remove). Staged moves + removals commit sequentially in one Save. Same-row move + remove is blocked at the UI level (the pick + revert buttons hide when a row is marked removed). Detects same-project collisions client-side and flags them in red.
apps/desktop/.../projects/project-menu.tsx New Edit folders… menu item (disabled for projects with zero folders / no primary path; auto projects are excluded by the existing !project.isAuto branch).
i18n 14 new strings × 4 locales (en / ja / zh / zh-hant).

The dialog also disables Save when no edits AND no removals were staged, so an empty Save closes immediately without a no-op RPC.

Tests

  • 8 new tests/hermes_cli/test_projects_db.py cases — primary-retarget, label preservation, non-primary move, idempotent same-path, collision, missing old path, missing project, normalization.
  • 3 new tests/tui_gateway/test_projects_rpc.py cases — RPC roundtrip with normalized path in response, structured 5063 on collision, structured 5062 on unknown project.
  • 3 new tests/hermes_cli/test_projects_cli.py cases — move-folder CLI roundtrip, bad-arg rc=2 on unknown old path, bad-arg rc=2 on collision.
  • 1 new apps/desktop/src/store/projects.test.ts case — missing projects.move_folder marks the backend stale, surfaces the existing friendly error, and rolls back the optimistic path update.

The 14 new Python tests use tmp_path so they're cross-platform. The 5 pre-existing POSIX-only failures in test_projects_db.py are unrelated to this PR and were failing on Windows before any of my changes — git stash && pytest reproduces them on a clean tree. No new tests for removeProjectFolder because the wire contract (projects.remove_folder) was already covered by tests/hermes_cli and tests/tui_gateway from the original first-class-projects PR (#49037).

Locally on Windows: baseline 5 failed / 29 passed → with this PR 5 failed / 43 passed (14 new passing tests, 0 new failures).

Commits

  1. feat(desktop): edit project folders — move/redirect from UI — the move/redirect half.
  2. feat(desktop): add remove folder to edit-folders dialog — the remove toggle, store wiring, and combined Save flow that ships both move + remove in one submit.
  3. fix(desktop): remove duplicate project locale keys — removes duplicate object keys reported by review.
  4. fix(desktop): handle stale folder move backends — adds stale-backend translation, optimistic rollback coverage, and lint cleanup.

User-visible behavior

Right-click an explicit project → Edit folders… → dialog lists each existing folder with its current path. Per row:

  • Click the folder icon to pick a new path (move/redirect, original path shows line-through + new path underneath).
  • Click the trash icon to mark the row for removal (row goes dim with a "Will be removed" badge; click the now-discard icon to undo).
  • The Save button ("Folders updated") lights up when any row is staged.

Hit Folders updated → staged removals run first, then staged moves, sequentially through the corresponding JSON-RPC methods. The sidebar tree retargets, primary_path updates where needed, and the dialog closes.

Related

Review follow-up

Addressed the automated review feedback in two atomic commits:

  • Removed duplicate menuEditFolders keys from ja / zh / zh-hant.
  • Converted a missing projects.move_folder RPC into the existing stale-backend state/message and added rollback coverage.

Verified with Desktop typecheck, targeted ESLint, and projects.test.ts (14/14 passing).

Adds the missing 'edit folders' half of the project folder lifecycle:
the desktop previously supported add-folder and remove-folder but had no
way to redirect a folder to a new absolute path after it was renamed or
moved on disk, so users had to remove and re-add (losing is_primary /
label / added_at and the project's primary_path pointer).

Layers (all additive — no schema migration required):

- hermes_cli/projects_db.move_folder: rewrite a project's folder path
  in place, preserving is_primary / label / added_at. Updates
  projects.primary_path when the moved folder was the primary, and
  rejects same-project collisions with a clear ValueError.
- tui_gateway: projects.move_folder JSON-RPC method, registered
  alongside add_folder/remove_folder/set_primary. Bad args and unknown
  projects surface as the existing 5061/5062/5063 codes.
- hermes_cli/projects_cmd: `hermes project move-folder <slug> <old> <new>`
  for CLI parity with add-folder/remove-folder.
- apps/desktop: a new 'edit-folders' mode on the project dialog
  (stages several moves in one commit) + a kebab menu item + an
  optimistic moveProjectFolder store action that retargets primary_path
  in the cached / atoms and rolls back on failure.
- i18n: 11 new strings per locale (en/ja/zh/zh-hant).

Tests: 8 new projects_db tests + 3 new RPC tests + 3 new CLI tests
covering primary-retarget, label preservation, non-primary move,
idempotent same-path, collision, unknown old path, missing project,
path normalization. Pre-existing 5 POSIX-only failures on Windows
are unrelated (predate this PR).

Refs the user-facing gap noted in NousResearch#53004 (folder→session flow has no
post-hoc correction affordance) and the related wish in NousResearch#42525.
@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have labels Jul 9, 2026
The edit-folders dialog introduced in the previous commit only supported
moving a folder to a new path. After using it, the only way to drop a
folder from a project was the legacy sidebar-rebuild flow: remove it
from outside the app and let a project-tree refresh re-classify it.
That left no place to remove a folder while keeping the project — and
the backend + RPC + CLI for projects.remove_folder already existed but
the desktop never wired it up.

Wires the missing frontend half:

- store: removeProjectFolder(id, path) — Apollo-style optimistic cache
  update (drop the row, clear primary_path when the removed folder was
  primary, null the tree node's path on the removed primary). The
  backend repoints primary to the next-oldest folder when the primary
  is removed; we don't try to predict it client-side and let the
  reconcile pass pick up the server's choice.
- dialog: FolderEdit.removed flag + toggleRemoveFolderEdit handler.
  Each row gets a trash/keep toggle button (icon flips between
  `trash` and `discard`; row goes opacity-50 with a 'Will be removed'
  badge when toggled). When a row is removed, the pick + revert buttons
  hide so the user can't simultaneously move + remove the same folder.
- submit() now runs removals first, then moves, sequentially — each
  RPC awaits the previous so the optimistic cache stays consistent.
  Save is disabled when every row is unchanged AND none are removed.

Backend, RPC, and CLI for projects.remove_folder are unchanged — they
already shipped with first-class projects (NousResearch#49037). The 3 new i18n
strings ship across en/ja/zh/zh-hant.

No backend test changes: existing tests/hermes_cli and tests/tui_gateway
for remove_folder already cover the wire contract.

@teknium1 teknium1 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.

Thanks for covering the missing folder-redirect path across the DB, RPC, CLI, and desktop layers. The premise is still valid on current main: hermes_cli/projects_db.py:462-535 has add/remove but no in-place move, and apps/desktop/src/app/chat/sidebar/projects/project-menu.tsx:157-160 only exposes Add folder.

Problems

  • apps/desktop/src/i18n/ja.ts:1524 duplicates menuEditFolders in one object literal; the same duplicate is in apps/desktop/src/i18n/zh.ts:1724 and apps/desktop/src/i18n/zh-hant.ts:1477. Remove the duplicate entries; TypeScript rejects duplicate object keys.
  • apps/desktop/src/store/projects.ts:609 directly calls the new RPC. Current stale-backend handling only establishes the generic project capability from projects.list / projects.tree (apps/desktop/src/store/projects.ts:237-244), while remote gateways are supported (website/docs/reference/cli-commands.md:1441). A backend predating only projects.move_folder will let the dialog open and then fail generically on Save. Match createProject's missing-method handling at apps/desktop/src/store/projects.ts:458-460 and add coverage.

Suggested changes

  • Remove the three duplicate locale keys and run desktop typecheck.
  • Convert a missing projects.move_folder RPC into the existing stale-backend state/message, with a regression test.

Current main has no changes on these paths since the PR base 411d59976410bc6eabcd4d59b6f688de3e879f05, so salvage should otherwise be mechanical. This is an automated hermes-sweeper review.

Comment thread apps/desktop/src/i18n/ja.ts Outdated
Comment thread apps/desktop/src/store/projects.ts Outdated
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 11, 2026
@Clement-ccy
Clement-ccy deleted the feat/desktop-edit-project-folders branch July 25, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants