You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
RFC: should a link to another docs.plus pad open in the same window? #359
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 /receive do 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 the router.push rule 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:
navigateHref runs a link inside the current pad in place. Anything else opens with window.open(href, '_blank', SAFE_WINDOW_FEATURES) (apps/webapp/src/components/TipTap/hyperlinkPopovers/hrefEventHandler.ts:16-30).
classifyInternalDocumentLink returns null for 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).
hrefEventHandler ignores modifier keys and always calls preventDefault (hrefEventHandler.ts:32-37). navigateHref takes 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 hrefEventHandler
cd apps/webapp && bun run typecheck, then bun run lint at 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. Run bun run format at the root.
Out of scope
A new pad link kind, a title for other pads, or an @ pad list. These were considered and cut.
One piece of the "yes" branch landed early. It is committed locally on main and goes up with the next push.
1bb284ac5 (fix(webapp): reserve the privacy, terms, oauth and c slugs) adds privacy, terms, oauth and c to apps/webapp/src/utils/reservedSlugs.ts. So isReservedSlug already covers the four page routes this issue names.
The MCP create_document tool has the backend twin of that list (ROUTE_SLUGS in apps/hocuspocus.server/src/modules/mcp/infra/documentStore.ts, commit 0dacee392). A document titled "Privacy" gets the slug privacy-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.
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, solaunch_handler: navigate-existingholds." 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
router.push. This holds in the pad and in chat./privacy,/terms,/oauth/consent,/c/<id>and/receivedo not route as pads.extensions/CLAUDE.md§Composer Link Dialog And Internal Links no longer says other-pad links stay external. It names the new branch.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.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.<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".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.Desired behavior: set by the ruling.
Where to start: Yes branch:
navigateHrefinhrefEventHandler.ts. AfterclassifyInternalDocumentLinkreturnsnull, match the same origin and a first path segment that is a pad slug. Skip reserved routes withisReservedSlug(apps/webapp/src/utils/reservedSlugs.ts:17). That list lacks four page routes:c,oauth,privacyandterms(apps/webapp/src/pages/). Add them in the same change.navigateHrefis not a hook, so use theRoutersingleton fromnext/router, asreceiveSharedFile.tsdoes. Copy the Documents row Open:router.push(`/${doc.slug}`)(DocumentListRow.tsx:84). Add an optional event argument tonavigateHref.hrefEventHandlerandopenLinkFromClickinHTMLBody.tsxpass it. WithmetaKeyorctrlKeyset, keepwindow.open. The phone preview sheet button inlinkPreviewActions.tsxhas 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/privacydoes 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./privacy: it opens in a new tab. Repeat from a chat message.No branch:
apps/webapp/CLAUDE.md§Landing Page Shell And PWA. Runbun run formatat the root.Out of scope
padlink kind, a title for other pads, or an@pad list. These were considered and cut.