Skip to content

RFC: should a link to another docs.plus pad open in the same window? #359

Description

@HMarzban

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 /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

Activity

  1. HMarzban commented on Sep 28, 2026

    @HMarzban
    CollaboratorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions