Repository navigation
RFC: should a link to another docs.plus pad open in the same window? #359
Description
Parent
#328. Related: #249, #261, #174
What to build
A person pastes a link to another docs.plus pad into a pad or a chat message. A click opens that pad in a new tab or window. In the installed app, that can mean a second pad window. The PWA rule says "Open pads with router.push, never _blank, so launch_handler: navigate-existing holds." It is not clear whether that rule covers links inside content. This issue asks the maintainer to rule, then records the build that follows.
Acceptance criteria
- The maintainer answers the question below in a comment on this issue.
- If yes: a plain click on a same-origin link to another pad opens it in the same window with
router.push. This holds in the pad and in chat. - If yes: a Mod-click or middle-click still opens a new tab.
- If yes: links to other sites still open in a new tab, as today.
- If yes: links to
/privacy,/terms,/oauth/consent,/c/<id>and/receivedo not route as pads. - If yes:
extensions/CLAUDE.md§Composer Link Dialog And Internal Links no longer says other-pad links stay external. It names the new branch. - If no:
apps/webapp/CLAUDE.md§Landing Page Shell And PWA says therouter.pushrule covers app navigation only. Links inside content are exempt.
Blocked by
None — can start now.
Agent brief
Type: HITL — the maintainer decides: does the PWA rule "open pads with router.push" cover a pasted link to another pad? The answer goes in a comment on this issue. An agent then builds the "yes" or "no" branch above.
Category: ruling
Current behavior:
navigateHrefruns a link inside the current pad in place. Anything else opens withwindow.open(href, '_blank', SAFE_WINDOW_FEATURES)(apps/webapp/src/components/TipTap/hyperlinkPopovers/hrefEventHandler.ts:16-30).classifyInternalDocumentLinkreturnsnullfor a link to another pad, because the first path segment must equal the current pad slug (TipTap/hyperlinkPopovers/internalDocumentLink.ts:31-33). So another pad counts as an outside link.- Callers: the pad preview popover title (
previewShared.ts:106), the preview actions (linkPreviewActions.tsx:50) and chat message links (chatroom/components/MessageCard/components/MessageContent/components/HTMLBody.tsx:13). hrefEventHandlerignores modifier keys and always callspreventDefault(hrefEventHandler.ts:32-37).navigateHreftakes no event today.- The preview title is an
<a target="_blank">, so a middle-click opens a new tab natively (previewShared.ts:99-106). extensions/CLAUDE.md§Composer Link Dialog And Internal Links says "Cross-document docs.plus links stay external".- The manifest sets
launch_handler(apps/webapp/public/manifest.json:22). Do not add a second pad window #261 is a standing "do not add a second pad window" card. - For the ruling, yes gives: one window, one socket per pad, and the same behavior as a Documents row Open.
- For the ruling, no gives: a person reading a pad can keep it open while they check the linked pad.
- Either way, a click inside the same pad stays in place.
Desired behavior: set by the ruling.
Where to start: Yes branch: navigateHref in hrefEventHandler.ts. After classifyInternalDocumentLink returns null, match the same origin and a first path segment that is a pad slug. Skip reserved routes with isReservedSlug (apps/webapp/src/utils/reservedSlugs.ts:17). That list lacks four page routes: c, oauth, privacy and terms (apps/webapp/src/pages/). Add them in the same change. navigateHref is not a hook, so use the Router singleton from next/router, as receiveSharedFile.ts does. Copy the Documents row Open: router.push(`/${doc.slug}`) (DocumentListRow.tsx:84). Add an optional event argument to navigateHref. hrefEventHandler and openLinkFromClick in HTMLBody.tsx pass it. With metaKey or ctrlKey set, keep window.open. The phone preview sheet button in linkPreviewActions.tsx has no event and routes in place. Line numbers are hints as of 2026-09-28; the agent searches by symbol.
Rules that apply: apps/webapp/CLAUDE.md §Landing Page Shell And PWA (PWA path guards). extensions/CLAUDE.md §Composer Link Dialog And Internal Links. AGENTS.md §Test Policy, case (c): the new branch parses a URL and branches on origin, reserved slug and modifier keys. Add one case to the existing hrefEventHandler.test.ts: a same-origin other-pad link routes in place, and /privacy does not. Prove it by sabotage: remove the new branch, and the case must fail. Prose edits follow .cursor/skills/tech-writer/SKILL.md §Simplified English (house standard).
Verify:
Yes branch:
cd apps/webapp && bun run test -- --testPathPatterns hrefEventHandlercd apps/webapp && bun run typecheck, thenbun run lintat the root.- Browser, desktop, light and dark: in pad A, paste a link to pad B. Click it in the preview: pad B opens in the same tab. Mod-click and middle-click: a new tab. Paste a link to
/privacy: it opens in a new tab. Repeat from a chat message. - Installed app on desktop Chrome and Android Chrome: the click stays in the one app window.
- Phone: repeat on iOS Safari and Android Chrome. The preview sheet Open button routes in place.
No branch:
- Read the edited bullet in
apps/webapp/CLAUDE.md§Landing Page Shell And PWA. Runbun run formatat the root.
Out of scope
- A new
padlink kind, a title for other pads, or an@pad list. These were considered and cut. - The kind icon on links inside the pad — Richer internal-link chips, and a kind icon beside the pad link #174.
- A second pad window — Do not add a second pad window #261.
Activity
HMarzban commentedon Sep 28, 2026
One piece of the "yes" branch landed early. It is committed locally on
mainand goes up with the next push.1bb284ac5(fix(webapp): reserve the privacy, terms, oauth and c slugs) addsprivacy,terms,oauthandctoapps/webapp/src/utils/reservedSlugs.ts. SoisReservedSlugalready covers the four page routes this issue names.create_documenttool has the backend twin of that list (ROUTE_SLUGSinapps/hocuspocus.server/src/modules/mcp/infra/documentStore.ts, commit0dacee392). A document titled "Privacy" gets the slugprivacy-document, so it never collides with the page route. Each list names the other, so they stay in step.The ruling itself is still open, and nothing about same-window navigation changed. Whichever branch you choose, the reserved-slug step in "Where to start" is already done.