Skip to content

fix: multi-repo file links open the file in its own repo - #307

Merged
LoganRupe merged 6 commits into
LoganRupe:multi-repo-workspacesfrom
dotrahat:fix/multi-repo-absolute-file-paths
Sep 25, 2026
Merged

LoganRupe merged 6 commits into
LoganRupe:multi-repo-workspacesfrom
dotrahat:fix/multi-repo-absolute-file-paths

Conversation

@dotrahat

@dotrahat dotrahat commented Sep 25, 2026 •

Copy link
Copy Markdown

In a multi-repo project whose workspace root is a parent folder, agents quote file paths the way git prints them, relative to the repo the command ran in. The chat resolved those paths against the workspace root, so a chip like .graft/config.json failed with "Failed to read workspace file '.graft/config.json' in '/home/cvl'". The same relative path can also exist in several repos, so the client cannot pick the right one by itself. Chips with an absolute path into a repo other than the workspace root also opened the wrong place.

This replaces #303, which cannot be reopened because the fork it came from was deleted.

Fix

Agent instructions. A session whose project spans more than one repo root now gets a rule to write every file and directory path in inline code as a full absolute path, each time it is mentioned, including paths copied from git or grep output. The rule explains why (the client opens a relative path against the workspace root), gives one example, and lists the ways replies slipped in testing: a path shortened on a later mention, file lists copied from git output, submodule paths or ~/name, and bare file names. Code blocks and text meant to be pasted elsewhere are exempt. The block is 265 words. Shorter versions failed in testing, as described under Testing. All six adapters pass the multi-repo flag (Claude, Codex, Cursor, Grok, OpenCode, Antigravity).

Chat file chips (ChatMarkdown.tsx)

  • Inline-code links and the two fallback resolver calls now get the repo roots.
  • The chip now passes its owning repo root to the file panel. The resolver already found the root, but the chip dropped it, so the panel read the repo-relative path against the workspace root. The diff panel already passed it, which is why the same file opened there.
  • A path with an owning root skips the workspace-wide basename lookup, both when it opens in the panel and for "Reveal in file manager". Before, a single-name path such as .graft or a repo's top-level README.md could open or reveal a same-named entry in another repo.

Folder links (FileBrowserPanel.tsx). The file tree keys multi-repo rows under the repo label but received only the repo-relative path, so it never found the folder to reveal. It now maps the selection onto its repo key.

Breadcrumbs (FileBreadcrumbs.tsx). A file in a repo other than the workspace root read <project-name> > .graft > config.json. It now reads <project-name> > <repo> > .graft > config.json. Crumbs from the repo down list and open files within that repo. The repo crumb uses the same label as the file tree, so two repos with the same folder name get distinct crumbs.

Root folder links (FilePreviewPanel.tsx, FileBrowserPanel.tsx). A link to the root folder of a repo outside the workspace root, or to the workspace root itself, failed with "Failed to read workspace file". resolveOwningRoot only matches paths strictly inside a root, so the root itself arrived as a host path, and the panel keeps the read error for a host folder. The panel now opens a host path that is the workspace root or one of the project's repo roots as a folder. The tree selects that repo's top-level node, or shows the whole workspace for the workspace root. Repo roots inside the workspace root already worked, because their workspace-relative path equals the tree label. The workspace-root case failed in single-repo projects too.

Testing

  • vp test run: RuntimeInstructions, ClaudeAdapter, CodexSessionRuntime, markdown-links, ChatMarkdown, and components/files tests pass.
  • The full suite (vp run -r test) passes except for 24 server failures in GitManager, GitVcsDriver, and GitVcsDriverCore tests. The same 24 fail on the base commit 1d386b99a. They come from the host's git 2.34.1, not from this change.
  • Web and server typechecks have no errors.
  • Manual test on a Linux AppImage with a five-repo project: file chips, folder chips, and breadcrumbs open the file or folder in the correct repo. A four-repo .code-workspace with one repo outside the workspace root: the pills for that repo's root folder and for the workspace root open them in the tree.
  • Prompt, tested with Claude Opus 5.5 on the same questions:
    • A one-sentence rule: paths were shortened in the closing bullets, and in a second run almost none were absolute.
    • A ~175-word rule: file lists from git stash show stayed relative.
    • The final rule: over five turns in a five-repo project, 46 path mentions were absolute pills, including stash file lists and closing summaries. Two were relative, both short back-references to a file named earlier. A few were bare names without a path, such as .gitignore and submodule names.

Screenshots

Before: the chip fails to read .graft/config.json from the workspace root, and the breadcrumbs leave out the repo.

Before: the file chip fails with Failed to read workspace file

After: the chip links to the absolute path, the file opens from its repo, the breadcrumbs name the repo, and the tree reveals the file.

Image

Root folder links (video): in a four-repo .code-workspace with one repo outside the workspace root, the workspace-root pill and the outside repo's pill open in the tree, and files open from their own repos. Before this change, both root pills failed with "Failed to read workspace file".

video-3.webm

Fixes #302

Model: Claude Opus 5.5, harness: Claude Code.

dotrahat and others added 2 commits September 25, 2026 14:19
In a multi-repo project whose workspace root is a parent folder, agents
quote paths the way git prints them, relative to the repo they ran in.
The chat resolved those paths against the workspace root, so a chip like
`.graft/config.json` failed with "Failed to read workspace file". The
same relative path can also exist in several repos.

Sessions that span more than one repo root now get a mandatory rule to
write every path as an absolute path, built from a
`<base directory>/<relative path>` template, with no shortened repeat
mentions and a check before sending. All six adapters pass the flag.

Chat chips now resolve against the repo roots, including the two
fallback resolver calls, and pass the owning root to the file panel. A
path with an owning root skips the workspace-wide basename lookup. The
file tree maps a chat selection onto its repo key, so folder links in a
non-anchor repo are revealed.

Fixes LoganRupe#302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file in a repo other than the workspace root showed
"<project-name> > .graft > config.json", hiding which repo it came from,
and the crumb menus listed the workspace root instead of the repo.
Breadcrumbs now read "<project-name> > <repo> > path", and crumbs from
the repo down browse and open files within that repo.

Refs LoganRupe#302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L labels Sep 25, 2026
@LoganRupe

Copy link
Copy Markdown
Owner

Thanks @dotrahat, this is a really nice fix, and the before/after screenshots made it easy to follow. The chip fix is exactly right: passing fileRoot through, and skipping the basename lookup when a root is known. Keeping the breadcrumb commit in this PR is fine by me, no need to split it.

Two things before I merge:

1. Could we trim the multi-repo prompt block? It's about 440 words. Cursor and Antigravity append runtime instructions to every turn, and OpenCode resends them as system each turn, so that's around 600 extra tokens per turn. I'm also a bit worried about the scope. "Every path in every part of the response", plus the final check, could lead agents to rewrite relative paths inside code blocks (import "./foo", cd src, config snippets) that people paste.

Something closer to this would work for me:

This workspace contains more than one git repository, and the same relative path can exist in several of them. When you refer to a file or directory in your response, write its full absolute path, every time you mention it, including paths copied from git or grep output. This does not apply to code blocks or text meant to be pasted elsewhere.

A generic example path would be better than .graft/config.json too. I get that Claude shortened paths in your testing. If the shorter version still slips, I'm happy to add one sentence back.

2. "Reveal in file manager" has the same basename issue. revealMarkdownFileInFileManager in ChatMarkdown.tsx still does the workspace basename lookup when the chip has an owning root. So revealing a bare README.md in one repo can reveal another repo's copy. Skipping the lookup when fileLinkMeta.fileRoot is set should cover it, same as you did for openFileInPanel.

One small nit, no need to block on it: the repo crumb uses the raw folder basename, while the file tree uses buildRootLabels. So two roots with the same folder name would show the same crumb.

Thanks again for picking this up!

@dotrahat

dotrahat commented Sep 25, 2026 •

Copy link
Copy Markdown
Author

@LoganRupe Yes, I was worried about token usage too. I reduced prompt size and it started slipping again. I'm progressively increasing and testing to see what works best while ensuring prompt does not get too big. In start of its responses, it does work fine but at the end of response sometimes it slips back to relative.

Noted your concerns on scope and included in prompt.
Thanks for the instruct on file manager same basename issue.

Working on it.

dotrahat and others added 4 commits September 25, 2026 18:24
The multi-repo instruction block is shorter and exempts code blocks and
text meant to be pasted elsewhere. It asks for every file and directory
path in inline code as an absolute path, explains why, gives one example,
and lists the ways replies slipped in testing. "Reveal in file manager"
skips the workspace basename lookup when a chip has an owning repo root,
like opening in the panel does.

Refs LoganRupe#302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two repos with the same folder name showed the same crumb. The crumb now
uses the file tree's root labels, which grow by parent folders until they
differ.

Refs LoganRupe#302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A link to the root folder of a repo that lives outside the workspace root
failed with "Failed to read workspace file". The resolver only matches
paths strictly inside a root, so the root itself arrived as a host path,
and the panel keeps the read error for a host folder. The panel now opens
a host path that is one of the project's repo roots as a folder, and the
file tree selects that repo's top-level node.

Refs LoganRupe#302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A link to the workspace root itself failed with "Failed to read workspace
file", in single-repo and multi-repo projects alike. It arrived as a host
path, like a repo root outside the workspace root did. The panel now opens
the workspace root as a folder too, and the file tree shows the whole
workspace with nothing selected.

Refs LoganRupe#302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dotrahat

Copy link
Copy Markdown
Author

@LoganRupe This is ready for another look. I've addressed all three points and pushed the changes, plus a fix for a related bug in two separate commits.

1. Prompt block. I started with your wording and a generic example path. In a multi-repo project it didn't hold up with Claude Opus 5.5:

  • Your wording as written: the agent wrote absolute paths in a table, then shortened them to .graft/config.json in the closing bullets.
  • Plus one sentence against shortening: worse. Almost nothing was absolute (~/<repo>, test-another/work.txt, .graft/config.json, a bare .gitignore).
  • A ~175-word version: file lists from git stash show stayed relative in two runs.
  • A version that said the client turns paths into links: the agent wrote bare absolute paths without backticks, so no pills at all.

The block is now 265 words, about 350 tokens against the ~600 before. It keeps your scope: code blocks and text meant to be pasted elsewhere are exempt, and the "MANDATORY" wording is gone. It follows Anthropic's Opus 5.5 prompting guidance: it explains why (the client opens a relative path against the workspace root), gives one example in <example> tags, and lists the specific ways replies slipped. I tested it in a five-repo project over five turns. 46 path mentions were absolute pills, including stash file lists and closing summaries. Two were relative, both short back-references such as "a minute before test-another/work.txt". A few were bare names without a path: .gitignore and submodule names. If you'd still like it shorter, tell me which part you'd drop and I'll rerun the same test.

2. Reveal in file manager. Fixed. It now skips the basename lookup when the chip has an owning root, the same as openFileInPanel.

3. Repo crumb label. I moved buildRootLabels and labelForRoot into filePath.ts. The breadcrumb now uses the file tree's label, so two repos with the same folder name get different crumbs.

4. Related bug: links to a root folder. I tested with a .code-workspace whose fourth folder is outside the workspace root. A pill for that repo's root folder failed with "Failed to read workspace file '/outlier-repo' in ''", and a pill for the workspace root itself failed the same way. Files inside the repo opened fine.

  • Cause: resolveOwningRoot only matches paths strictly inside a root. So the root folder itself fell back to a host path, and the panel keeps the read error for a host path that is a folder. Repo roots inside the workspace root only worked because their workspace-relative path happens to equal the tree label.
  • Fix (last two commits): the panel opens a host path that equals the workspace root or a repo root as a folder. The tree selects that repo's top-level node, or shows the whole workspace for the workspace root. The workspace-root case failed in single-repo projects too.
  • What it doesn't cover:
    • The breadcrumbs for that folder show the full host path.
    • A repo root nested deeper in the workspace, whose tree label differs from its path, isn't selected in the tree.
  • Alternative: fixing it in the resolver ("this path is the root of repo X") would be cleaner, but the server rejects an empty or . path inside a root, so that needs a server change.

It isn't strictly #302, so if you'd rather review it separately, I'll move both commits to their own PR. Same if you'd prefer the resolver and server approach.

@LoganRupe LoganRupe left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks @dotrahat, this looks great. Your test notes on the prompt settled it for me: my shorter wording clearly didn't hold up, and 265 words with code blocks exempt is a fair trade. The reveal fix and the shared root labels are exactly what I was after.

The root-folder fix is a nice catch too, especially since the workspace-root case was broken in single-repo projects as well. Keeping it in this PR is fine, and the two gaps you listed can wait until someone hits them.

Approving and merging. Thanks for sticking with this one!

@LoganRupe
LoganRupe merged commit 44b00df into LoganRupe:multi-repo-workspaces Sep 25, 2026
5 checks passed
LoganRupe pushed a commit that referenced this pull request Sep 26, 2026
In a multi-repo project whose workspace root is a parent folder, chat file chips resolved repo-relative paths against the workspace root and failed to open, or opened a same-named file in another repo.

- Multi-repo sessions get runtime instructions to write absolute file paths; every provider adapter passes the flag.
- Chat file chips pass their owning repo root to the file panel, and skip the workspace-wide basename lookup when a root is known, for both open and reveal.
- The file tree reveals files and repo roots under their repo label; breadcrumbs name the owning repo using the tree's labels.
- A link to the workspace root or a repo root opens as a folder.

Fixes #302
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants