cmux next files: chat file links open at their line, bare file names open, chat tools stay beside a chat with a new tab

LU
10h ago
+12640-9566

Summary

first piece of file viewer parity with the codex app (austin, 2026-10-21). three things a person hits in a chat: file links, file names, and where the chat's tools open.

file links open at their line. src/app.ts:42 in a reply opened the file at the top: the chip parser threw the line away (stripLine in chips/paths.ts). now the line goes chip -> open request -> LinkOpener -> file page -> editor:

  • path:42, :42:7, #L42, #L42C7, #L42-L50, :42-50, in prose, code spans, markdown links and file urls
  • the editor centers the line and highlights the line or range briefly (no fade with reduced motion); an open file reuses its tab; past the end clamps; markdown, images, folders ignore the line
  • cmux file open <path> --line 42[:7], same on mcp and the palette
  • line and column are bounded positive integers, checked on the page and again in swift; the deny list, root checks and gesture rule run first, unchanged

bare file names open the file. an agent writes next-sync.py for scripts/next-sync.py. a name with no folder is now a chip only once the host finds a file of that name in the chat's folders (one FolderNameIndex walk per folder, off the main thread, shared app-wide, at most 4 folders for 30 s, skipping hidden folders, node_modules and build output, never the home folder or above). best match = fewest folders below its root. the found file goes through the deny list and root checks like a written path; the page never learns a path. names are asked in their own link.inspect batches so a walk never holds up other chips. dotted code that isn't a file (console.log, user.name) stays code; versions and host:port (4.5:1, example.com:8080) stay text.

a chip that can't open says why (not found, may hold secrets, outside the chat's folders, couldn't open), as the app's refusal notice, 21 languages, only for a real click.

chat tools beside a chat with a new tab page. with [chat] [New Tab] in one pane the header's Files (and Changes, Preview, browser) opened in a new column and the strip scrolled away from the chat. a chat plus only new tab pages now counts as alone (ChatColumnPlacement.loneChat): the chat docks left, its new tab pages follow it into the chat column (chat stays selected; focus only if the person didn't click elsewhere meanwhile; a page they picked stays), the tool opens beside. the chat column accepts a new tab page dropped into it; a terminal or browser picked on a new tab page in the column opens in the strip.

also fixed on the way:

  • editor sticky scroll header used the transparent editor background, so scrolled code read through it; it gets --cmux-editor-sticky-background (near solid + the shared surface blur; a solid theme background or a themed sticky color wins)
  • explicit links keep their line for names without an extension or with a dot folder (src/Makefile#L42, .github/workflows/ci.yml:5); C:\repo\app.ts:42, app.ts:1.5, #section:42 are not lines; a link label that is a path no longer makes a chip inside a chip
  • FolderNameIndex.build gets an optional skippingFolders (empty by default, no change for the Files pane)

not changed: where a page's links open. after #22822 a chat's links open as a tab in the chat's pane unless Debug > "Links from pages open like Cmd-click" is on; this pr leaves that alone.

Before (file links in a chat reply) After (file links in a chat reply)
Before (header Files from [chat] [New Tab]) After (header Files from [chat] [New Tab])

Size: one feature (file line jumps, bare file names, chat tools beside a new tab page) whose Swift pieces, webview chips and tests depend on each other and were reviewed together over five rounds; it predates the PR size check, and splitting it now would re-run every review and dogfood round for no behaviour change.

Testing

each fix landed test first (red on the test commit, green on the fix): c0942f443eb, 113648e2e99, c33d95df5ed, 337acd2cced, 99ae77243b4, 01355b1a2b7, 52df604a80f, aaa2e01a49a.

  • cd webviews && bun test test/reply-path-line.test.tsx test/editor-page-line.test.ts test/editor-page.test.ts test/editor-sticky-background.test.ts: 116 pass, 0 fail on 894d7876d15. red checks: the new link/name/reset/outside tests fail against the previous sources (checked by swapping the files back).

  • scripts/web-lint.sh, bun run typecheck, scripts/cmux-next/prepush-gate.py (no new violations), verify-local.py --only swift-syntax.

  • local tagged debug build (xcode 27.1) of each round, build only under its own tag; BUILD SUCCEEDED on 954e6a9e8b0 (the head before the last base merge). the dogfood app ran 7efbdea94b3.

  • reviews: five rounds of a claude review subagent (correctness first, REVIEW.md) plus two codex reviews; every finding fixed in the commits above. findings and resolutions in a comment.

  • base merge fe539951537 + 68af67abe7c: conflicts in AgentReplySites.swift (kept both closures), LinkChips.tsx (base's new LinkHoverCard imports + this pr's), agent pane Localizable.xcstrings (key union, checked 3-way). one review subagent on the merge and the fixes: no p0/p1.

  • ci fixes from the last run (a00f2e44254): $5/$10/$20 and $HOME/notes.md made path chips (agent-pane-math red), now plain text, with a test; FilePageRestoredLineTests was missing import CmuxNextBridge (swift test, daemon tests, generated files red); both highlights get a try-it action (release-notary red).

  • after the merge: prepush-gate.py no new violations, bun run typecheck pass, web tests 105 pass 0 fail, tagged debug build (xcode 26.6) exit 0 on a00f2e44254.

  • base merge 798797446977f (3,096 base commits): conflicts in the reply links and request (base's image-aware resolve and image.load {auto} kept with this pr's line and bare-name lookup), ChatDockRules (base's takesOnlyChats plus this pr's New Tab pages), ChatColumnPlacement (DaemonCapability rename), AgentBesidePlacement (base's OutboundTab moved flag with this pr's New Tab page follow), NewTabPage+Open imports, linkStore policy, Markdown.tsx prose (base's time tokens + linkLabel). base's code-chip lineLabel is dropped: this pr's PathChip already shows the :line suffix.

  • review findings from the previous merge, fixed: a line reveal now scrolls to its column (revealPositionInCenter) and puts the page's focus in the editor, as base's Find reveal did (a64bf0f8d909f, test first in c20c3fba3d465); the agent's tool beside a lone chat is selected before the New Tab pages follow, so a click during the open no longer leaves it unselected (17358ee77ee5b).

  • red/green: bun test webviews/src/pages/editor/lineReveal.test.ts (run outside webviews/ so the dom preload is skipped): 0 pass 2 fail on c20c3fba3d465 (revealLineInCenter called, no focus), 2 pass 0 fail on a64bf0f8d909f.

  • merge follow-ups found by review and by the fleet run: a base test's onOpenFile closure arity (b8d6e959), a remote chat's code chip keeps its :line (b8d6e959), base's new FilePosition callers in TerminalHostDelegate and DiffPageService ported to FileLineLocation (5dfef3a8), the PR's tests read tabs through debugTabs after base removed openKeys (47b98c35), a scratch editor page root in the two page tests (8f403136), and the restored-tab test's route is the resolved path (7bcf633a).

  • fleet Swift (cmux-ci run … package-test-lane.sh suite Packages/macOS/CmuxNext …, aws-m4pro): on 47b98c35, 13 of 15 suites passed (FileLineLocation, FileOpenLine, ChatColumnPlacement, NewTabPagePickInPlace, AgentPaneReplyNames, AgentReplyNames, EditorRevealConfig, FindSection, FileExplorer, AgentPaneMediaNoAutoAction, ClickedLinkFocus, ChatDockLinkSplitMotion, HeaderDiffPlacement); FilePageRestoredLineTests and LinkOpenerTests then passed on 7bcf633a.

  • two more base merges: f3c8aec7 keeps base's cx-k21v (a chat on another machine never resolves or opens this Mac's paths; this PR's bare-name lookup runs only for this Mac's chats) and fixes base's new AgentPaneRemoteChatPathTests closure arity (fa0af8db); c7bbd6fd keeps base's slim inline links (#25212: no lock icon, outside-project tooltip) with this PR's line/suffix chips. test_file_open_line_localization.py runs in ci-guards and is registered (f439c4e3). Fleet Swift on fa0af8db: LinkOpener, FilePageRestoredLine, AgentPaneReplyNames, AgentPaneMediaNoAutoAction, AgentPaneRemoteChatPath, AgentReplyNames all passed. oxfmt 0.73.0 --check (printWidth 120) passes on the edited web files.

CI reds that are not this pr

  • cmux Actions / check: fresh-chief-eval.yml uses an unpinned actions/checkout@v4 (a base workflow this PR does not touch).

  • nightly-guards / Guard status, release-notary (update-with-running-agents.md), cmux-next checks: red on other fresh feat-cmux-next PRs too.

  • runner refusals (glaeda-cmux-runner-hook: refused: ... GiB free): infra, re-run by glaeda.

  • defaults-snapshot: base red (red on 21 of 22 fresh feat-cmux-next heads, per the base-red watch comment). this pr changes no default.

  • web / react-apps-check: browser tests for queue rows, the trust ask, soft edges, review threads, composer and stream jitter. this pr does not touch those files; many of the same tests are red on other prs (#23048).

Observed / Not run

  • observed (this round): the red/green bun run of lineReveal.test.ts; the fleet Swift suites above; app sources compile on the fleet at 5dfef3a8 and later.

  • not run (this round): the full Swift suite and web suites (CI runs them); the remote-chip :line fix has no test of its own (it needs the webviews DOM preload, which needs an install on this Mac); no new runtime capture, see below.

  • observed: the web tests above red then green; the builds; in the tagged app: links open at their line (cli --line 30 -> Ln 30, Col 1, --line abc/0 refused), the sticky header before/after, [chat] [New Tab] + Files keeps the chat and its page left and files right (recording), chat file links (recording).

  • not run locally (repo policy, fleet/ci only): swift tests FileLineLocationTests, FileOpenLineTests, FilePageRestoredLineTests, LinkOpenerTests, ChatColumnPlacementTests, NewTabPagePickInPlaceTests, AgentPaneReplyNamesTests, AgentReplyNamesTests; check-action-surfaces.sh. ci runs them here.

  • local build note: AppOnboardingServices+ClassicMigration.swift:200 is an ambiguous Task init on xcode 27.1 only; patched locally to build, not in this pr. ci uses xcode 26.

Changelog

Fixed: File links in agent replies open the file at the linked line, with a brief highlight, and reuse the tab if the file is already open
Added: A file name in an agent reply opens that file when it is in a subfolder of the chat's folder
Fixed: A file link in a chat that cannot open says why
Fixed: Files, Changes, Preview and browser tools opened from a chat header stay beside the chat when its tab strip also has a New Tab page, and the New Tab page stays with the chat
Fixed: The code editor's sticky scroll header no longer shows code through it

Proof

No capture: a fleet capture of 17358ee7 (job 246a1e28) failed before compiling, because the fleet's web-bundle builder has bun 1.4.2 and the base needs 1.4.4 since https://github.com/manaflow-ai/cmux/pull/25035; every fleet build of the current base fails the same way until the builder is updated. The recordings below are from the earlier head.

recordings above (lucas): chat file links before/after; header Files from [chat] [New Tab] before/after.

Checklist

  • Behavior changes have added or updated tests
  • localization: new strings in FileOpenLine.xcstrings, the control catalog and the agent pane catalog, all 21 languages; localization audit has no open rows

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

File links in agent replies open the file at the referenced line, bare file names now resolve to real files, and chat-header tools stay beside the chat when its tab strip also contains a New Tab page.

File links and bare names

  • path:42, path:42:7, path#L42, path#L42-L50, and path:42-50 carry the line through the chip, open request, and file page in prose, code spans, markdown links, and file URLs.
  • The editor centers the line and column, takes focus, and briefly highlights the line or range; the highlight stays instead of fading under reduced motion.
  • An already-open file reuses its tab and jumps; a line past the end clamps to the last line, including in large-file plain text mode; a recovered draft reveals the line again after it replaces the text.
  • Find results, the group config editor, terminal file:line links, and the diff page share the same FileLineLocation path, replacing the old FilePosition and the reveal command.
  • cmux file open, MCP, and the palette accept --line 42, 42:7, or 42-50, validated in Swift with bounds 1 to 10,000,000.
  • Bare file names (next-sync.py) become chips only for files a host finds in the chat's folders via a shared, bounded, off-main-thread name index; found files still pass the deny list and roots, and searches never walk home or above.
  • Markdown link labels that are also paths no longer create nested chips; C:\repo\app.ts:42, prices, and shell variables ($5/$10/$20, $HOME/notes.md) in prose no longer start a chip, and restored tabs keep their jump.
  • A chip that can't open says why after a real click; the sticky scroll header gets a near-solid background with the shared surface blur, or an opaque theme background when the theme sets one.

Chat placement with a New Tab page

  • A chat counts as alone when every other tab is a New Tab page, so chat-header Files, Changes, Preview, and browser tools open beside the chat instead of scrolling it out of view.
  • New Tab pages follow the chat into its chat column, which now accepts a dropped or moved new-tab page while still refusing tools, groups, and splits; terminal and browser picks from those pages open in the strip.
  • The tool tab is selected even when the person acts elsewhere during the open, and a chat on another machine never resolves or opens this Mac's paths.