Repository navigation
fix(app): link mentions, folders, and spaced paths - #59937
DhivakarK-git wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
The approach looks sound, and it fits the current gui-plugins layout better than #57126, which overlaps on folders and spaced paths. I couldn't verify it in the app: the change is only visible in the desktop/web app, which this sandbox can't run. The file-link tests pass (70 in gui-plugins, 29 for the session-ui classifier), and typecheck:tsc is clean in all four changed packages. Testers should check @README.md, a top-level folder link, a nested folder link and a missing-file link on desktop and web. Before/after screenshots or a short recording from the author would help.
Public API: adds a required kind(path): Promise<"file" | "directory" | undefined> method to the Files host interface in @opencode/plugin (gui/host-apis.ts). exists keeps its file-only meaning. Plugins that call Files are unaffected. Anything that implements Files itself, such as a test double or another host, must add kind.
Two things are worth fixing, both noted inline: kind() with a trailing separator on a top-level folder, and the extra requests when a clicked file is missing.
| if (!file) return Promise.resolve(false) | ||
| if (!file) return Promise.resolve(undefined) | ||
|
|
||
| const parent = /[\\/]/.test(file) ? getDirectory(file) : undefined |
There was a problem hiding this comment.
kind() doesn't handle a trailing separator. Folder search results come back as assets/, and getDirectory("assets/") returns "/". So files.kind(any) in the click path (renderer.tsx, the findAnyFileLink branch) lists the filesystem root instead of the workspace root, and reports undefined for a top-level folder. The click still recovers, but only after a failed silent read and a second kind(direct) listing. Trimming trailing separators before computing parent (or calling kind with the trimmed path) would make the first check work.
There was a problem hiding this comment.
Fixed: kind() trims trailing separators before getDirectory, so a top-level assets/ lists the workspace root and resolves on the first check.
| return | ||
| } | ||
|
|
||
| if (index === candidates.length - 1) await openResolvedFile(attempted ?? direct, candidate.selection, false) |
There was a problem hiding this comment.
Every candidate is read silently first, so for the last candidate a missing file now costs a silent read, a kind listing, then this second non-silent read of the same path. A single-candidate link to a deleted file goes from one request to three. Reading the final candidate non-silently the first time, and checking kind only when that fails, would keep the old cost and the existing load error.
There was a problem hiding this comment.
Fixed: the final silent failure now replays the load error already stored by files.get, so a missing single-candidate link no longer reads the same path a second time. Only when no stored error exists does it fall back to the visible read.
c0b75f3 to
62897d7
Compare
There was a problem hiding this comment.
Both earlier points are fixed. kind() now trims the trailing separator, so a top-level folder resolves on the first check. The last failed try no longer reads the same file again, so a missing file costs two requests, down from three. I checked the new commit by reading the code: the file-link tests still pass (70) and tsc is clean in gui-plugins and app. It is still not verified in the desktop or web app, which this sandbox can't run. The public API impact is unchanged from the last review: kind() is added to the plugin Files interface.
One small thing about how the error toast is replayed, noted inline.
| const error = files.get(failed)?.error | ||
|
|
||
| // The silent attempt already stored the load error; replay it instead of reading the same path again. | ||
| if (error) ctx.toast.show({ variant: "error", title: ctx.t("common.requestFailed"), description: error }) |
There was a problem hiding this comment.
This replayed toast doesn't match the one setLoadError would have shown. It uses common.requestFailed instead of toast.file.loadFailed.title. It also fires when the file is not found and already open in a tab, a case setLoadError deliberately skips because the tab already shows the not-found state. Matching both would keep the old behaviour for deleted files. For example, the files model could expose a way to report the stored error, or the skip condition could be repeated here.
There was a problem hiding this comment.
Fixed: the replayed toast uses toast.file.loadFailed.title and skips when the stored state is notFound and the path is already open in a tab, matching setLoadError. A server error still shows even when the tab is open.
Try the literal file path before the @-mention reading, add Files.kind so directory links linkify and open, and let inline-code paths with spaces reach the existence check. Folder clicks reveal in the OS file manager (never launch), background links stay quiet, and web opens the file browser.
62897d7 to
c3e0067
Compare
There was a problem hiding this comment.
The replayed load error now matches the files model: it uses the same toast title and stays quiet when the not-found file already has an open tab. Threads 1 and 2 were already fixed by the previous push. I checked the new commit by reading the code: the file-link tests still pass (70) and tsc is clean in gui-plugins. It is still not verified in the desktop or web app, which this sandbox can't run. The public API impact is unchanged from the last review: kind() is added to the plugin Files interface. No new concerns.
Issue for this PR
Closes #53502
Closes #55863
Related: #57126, #51516, #50630, #51872
Type of change
What does this PR do?
File links from messages had three gaps:
@-mention href keeps the@, so opening it asked for a path literally starting with@and returned 404. The parser now returns candidates: the literal path first, then the mention reading. Literal first keeps real files and package scopes safe:@types/node/index.d.tsstill matchesnode_modules/@types/..., and@/lib/utils.tsis treated as an alias, not a mention.Files.kind(path)now reports"file","directory", or undefined from the same parent listing. Directory links linkify, and clicking one reveals it in the OS file manager instead of asking the file reader for a non-file, which returned thePath is not a file500. Revealing also avoids launching a message-supplied.appbundle; background links stay quiet. The web cannot open a folder in the OS, so it opens the file browser at the folder path instead of doing nothing.bun test src/file,cd packages/app,npm 10.2.0) stays code.Relative existence checks use one
files.search(..., { kind: "any" }); results are matched with native separators and folder trailing separators normalized, soassets/,src/components/, andsrc\components\all match. A top-levelassets/resolves on the first check, and a suffix folder resolves through the same search when clicked. A search miss no longer lists the parent.Clicks still read the file first, so ordinary links keep their old cost. Only a failed read asks
kind. Non-final mention candidates read silently so they can fall back; the last candidate replays the stored load error, so a deleted file still shows the message without reading the same path twice.This overlaps with #57126's directory/space ground; that PR is against the older
gui-extensionstree and this one uses the currentgui-pluginslayout.How did you verify your code works?
@path,@path:line,@/alias,@types, inner@, and the line selection on the mention candidate.@interaction.ts:74true,@/interaction.tsfalse,@types/node/index.d.tstrue, directories from the index including trailing and native separators,my docs/notes.md, and no parent listing on a search miss.my docs/notes.mdandC:\Program Files\app\file.txtare path candidates;bun test src/fileandcd packages/appstay code.bun test src/fileinpackages/gui-plugins: 70 pass.packages/gui-pluginssuite: 255 pass, 1 skip.packages/session-uiclassifier test: 29 pass.typecheck:tscclean inpackages/gui-plugins,packages/app,packages/plugin, andpackages/session-ui.Visual
Screenshots / recordings
No packaged desktop capture is available in this environment.
Checklist
Added by the opencode bot (AI-generated).
Related: #50630, #51872
Public API: adds the required
kind()method to the pluginFileshost interface (backwards compatible for callers; implementers must add it)