Repository navigation
fix(tui): show where a Markdown link goes, and link local files with their URL - #318
Conversation
…their URL With hyperlinks on, a Markdown link that is not a web or mail link was drawn as its text only: [Open image](file:///C:/Temp/example.png) showed "Open image", with no hyperlink and no URL. Such a link now shows its URL after its text, as it already did with hyperlinks off. A link to an absolute file: URL on this computer (file:///... or file://localhost/...) is a hyperlink, text and printed URL together, as Pi already links the paths it prints; other hosts, shares, file: without //, encoded separators or control characters, format characters and URLs over 2048 characters are not. Other schemes and relative paths stay without a hyperlink. The HTML export shows the URL of a link it does not link to after its text. closes #297
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
OpenSec security review
✅ No security findings in this change.
The change makes the TUI and the HTML export show a Markdown link's destination in parentheses when the page/TUI does not link to it (fixing #297), and additionally turns file: URLs of local absolute paths into OSC 8 terminal hyperlinks via the new localFileLinkTarget() gate. Reviewed the new escaping in the export viewer, the destination gate for traversal/host/encoded-control bypasses, OSC 8 sequence-injection safety, the click path through TuiAltScreen and openBrowser, and the new tests; the only candidate (widened file: click surface) did not survive verification as a regression introduced by this change.
Notes (uncertain or pre-existing, not counted)
- packages/tui/src/components/markdown.ts:2306-2311 widens the click surface that #167 removed (docs/releases/1.0.4.md:53-54, open-browser.ts:49-54): in the non-default tuiMode "regular" (TuiMainScreen, no mouse reporting, no click handler) a plain click on a file:///…/x.command or .app link is resolved by the terminal with the OS handler, bypassing openBrowser's reveal-only policy; needs an existing executable at a known absolute path and a user click, and the same capability pre-exists for tool-path links, so it is a note rather than a finding.
- Hyperlinks-on mode now prints the raw href of non-web links (markdown.ts:2305) where it previously printed nothing; token.href may still contain Cf characters (e.g. U+202E) that markdown's sanitizeTerminalText leaves in place, allowing visual reversal of the displayed destination. Pre-existing for hyperlinks-off and moot given the model can write arbitrary text, so noted only.
- The export viewer's new text path uses the unsanitized token.href rather than sanitizeMarkdownUrl()'s control-stripped value (template.js:1759-1760); HTML-escaped, so harmless, but control characters can land in the page text.
- No dependency, manifest, Dockerfile or CI changes in this diff.
1 candidate rejected by verification
- Markdown links to local file: paths become clickable OSC 8 hyperlinks, re-enabling one-click open of a local file with the OS handler: The click-to-open primitive already ships at head for model-chosen absolute paths via linkPath (packages/coding-agent/src/core/tools/render-utils.ts:23-28), and the change prints the destination next to the label, so a click is a deliberate activation of a shown target (user-decision/social-engineering class excluded by SECURITY.md); default tuiMode fullscreen still routes plain clicks through the hardened openBrowser reveal-instead-of-open path.
Range 55d0c5c129fb..ea4d3fe1e9f7, 387 changed lines. Review by OpenSec Code Review.
Brings #318 (Markdown links show where they go; local file links are hyperlinks) and #317 (release downloads retry, the Windows update goes past locked leftovers, npm installs check the registry, `compile --off` keeps its record on Ctrl-C, updating no longer compiles extensions). No conflicts: #318's link rendering is beside the HTML comment filter in markdown.ts, and the `pi-bolt auth login` hint in package-manager-cli.ts keeps COMMAND_NAME.
…nges Changed: Markdown links show where they go and local file links open (#297, #318); updating no longer compiles extensions (#317); npm installs check the npm registry for updates (#303, #317). Fixed: downloads try again after a dropped connection; a Windows update no longer stops at a locked leftover file (#316); `pi-bolt compile --off` keeps its record when stopped (#317).
Problem
With hyperlinks on (
terminal.hyperlinks, or a terminal Pi detects as supporting OSC 8), a Markdown link that is not a web or mail link is drawn as its text only: no hyperlink, and no URL.1.2.0 draws
Open image. Ctrl+click does nothing, and where the link goes is not shown anywhere. The same happens to a relative path ([main](src/main.ts)) or an app scheme. With hyperlinks off, the URL was already printed after the text.Change
A link's URL is never dropped. With hyperlinks on:
[Open image](file:///C:/Temp/example.png)Open image (file:///C:/Temp/example.png)file:///C:/Temp/example.png[Open local](file://localhost/C:/Temp/example.png)Open local (file://localhost/C:/Temp/example.png)file:///C:/Temp/example.png[x.png](file:///tmp/x.png)x.png (file:///tmp/x.png)file:///tmp/x.png[GitHub](https://github.com)GitHubhttps://github.com(as before)[Settings](x-apple.systempreferences:...)Settings (x-apple.systempreferences:...)[share](file://server/share/x.png)share (file://server/share/x.png)[main](src/main.ts)main (src/main.ts)The URL is printed after the text in the link URL colour, as it already is with hyperlinks off (unchanged). A link whose text is its URL (
<file:///tmp/x.png>) is drawn once. The same lines are drawn in the regular and the fullscreen screen; in fullscreen a click on a file link goes toopenBrowser, which shows the file in Finder or the file manager (on Windows it opens images and text files and selects anything else in Explorer), and opens nothing on another host.Schemes
#167 draws only web and mail links as hyperlinks because a click on anything else can run a program (
openon an.appor.command,ShellExecuteon a.bat), and because a link's text can name something other than its target. That stays as it is for relative paths, app schemes and files on other hosts: no hyperlink. Their URL is now printed, so the user sees it; a terminal may detect a printed URL and make it clickable itself, as it does for any URL in the output, but Pi does not.An absolute local
file:URL is a hyperlink, text and printed URL together:file:///URL clickable, so this adds no target that was not one click away.file://hyperlinks with their path printed: images it cannot show (imageFallbackinterminal-image.ts) and the paths in tool calls (linkPath), which the model writes too.What counts as local (
localFileLinkTarget()inpackages/tui/src/utils.ts):file:///...orfile://localhost/...(any case), parsed withURL, which also percent-encodes spaces and other characters for the hyperlink. Not a hyperlink: any other host (file://server/share,file://127.0.0.1/), a share written as a path (file:////server/share,file:///\\server\share,file://localhost//server),file:without//(file:C:/x,file:/tmp/x), encoded separators (%2f,%5c), percent-encoded control characters (%00-%1f,%7f,%c2%80-%c2%9f), raw control or format characters (such as a right-to-left override, which can make the printed URL read as another), and URLs longer than 2048 characters (VTE ignores ones over 2083 bytes). Those are drawn with their URL and no hyperlink.HTML export
The export already links only
http,https,mailto,telandftpURLs and showed the text alone for anything else. It now shows the URL after the text, escaped, as text (markdown-link-url, in the theme's link URL colour), and still links nothing else.Streaming
A link renders from its token and the render settings (which include the capabilities), so a block reused while a reply streams in has the lines a fresh render gives.
markdown-streaming-blocks.test.tsstreams a document of file, web and other links in paragraphs, a list and a table, one character at a time at three widths, with hyperlinks on and off, and compares every render with a fresh one.Tests
packages/tui/test/markdown-link-destinations.test.ts(new): the reporter's cases (labelledfile:///C:/...,file://localhost/C:/..., POSIXfile:///tmp/x.png,https:,x-apple...:,javascript:,file://server/...) with hyperlinks on and off: the text drawn and the hyperlink under the text and under the URL; the same inTuiMainScreen(what is on the screen and the hyperlinks written) and inTuiAltScreen(a click on the text and on the URL opens the file or web link, and nothing for the others); a long file URL wrapped over several lines; a URL too long to link;localFileLinkTargetaccepted and rejected cases.packages/tui/test/terminal-text-sanitize.test.ts: other links have no hyperlink and show their URL.packages/tui/test/markdown-streaming-blocks.test.ts: the streaming case above.packages/coding-agent/test/export-html-session-fields.test.ts: the export shows the URL of a link it does not link to, escaped.The hyperlinks-on cases, the export case and the changed sanitize test fail on
pi-boltbefore this change; the hyperlinks-off cases pass on both (unchanged).Checks (darwin-arm64)
npm run checkis clean.markdown-link-destinations,terminal-text-sanitize,markdown,markdown-streaming-blocks,tui-alt-screen,tui-render,terminal-imagepass. coding-agent:terminal-escape-display,open-browser,export-html-*,tool-execution-componentpass../test.sh: tui, ai, agent and the other packages pass. Failures elsewhere, none in code this touches:packages/env("Build the daemon first"),packages/chord(4 timeouts, and 3 tests inupgrades,plugins/buildandworkersthat also fail on their own; chord does not import pi-tui or the coding agent), andcoding-agent/test/theme-picker.test.ts(a file-watch wait that passes when run again).bench/e2e_screen.py, this build against the 1.2.0 release build: the same screen at 120 and 80 columns (985 and 1,121 lines); its document has web links only.terminal.hyperlinks: true, only the link lines differ, for example at 120 columns:The lines after them move down by the lines the URLs add; nothing else differs.
Not tested: Windows Terminal itself (only its handling of an OSC 8
file:///link as described in the issue), nor how terminals treatfile://localhost(the hyperlink usesfile:///).closes #297