Skip to content

Build stored heading and message links from the pad path, not the address bar #331

Description

@HMarzban

Parent

#328. Related: #174

What to build

A person opens a Filter view, or has a heading chat open, and inserts a heading link with Mod-k. The stored link copies the whole address bar. So the pad reads it as a Filter link or a chat link, not a heading link. TOC "Copy link", the Bookmark and Notification "Copy link" buttons, and the chat message "Copy Link" action have the same fault. After this change, every link that a person stores or copies starts from the pad path and carries only its own keys.

Acceptance criteria

  • In a Filter view (/<pad>/<term>), a heading link picked with Mod-k stores <origin>/<pad>?h=…&id=…, with no Filter segment and no mode key.
  • With ?chatroom= in the address bar, the same pick stores a heading link with no chatroom or msg_id key. Its preview shows the heading title over "In this document · Heading", not "Message in chat" or "Chat".
  • A link built while History is open (#history…) carries no hash.
  • In a Filter view, four copy actions give the same clean shape. They are TOC "Copy link", Bookmark "Copy link", Notification "Copy link" and chat message "Copy Link".
  • A TOC click in a Filter view still keeps the Filter path and ?chatroom= in the address bar, as today.
  • The chat breadcrumb still writes the address bar from the current URL, as today.
  • A unit test pins the Filter, ?chatroom= and hash cases. It fails when the new builder reads the whole address bar (prove it by sabotage).

Blocked by

None — can start now.

Agent brief

Type: AFK — an agent can finish this alone.

Category: bug

Current behavior:

  • buildBookmarkHref and buildHeadingHref start from new URL(window.location.href) (apps/webapp/src/utils/link-helpers.ts:59-72). They keep the Filter path segments, the other query keys and the hash.
  • classifyInternalDocumentLink tests the history hash first, then Filter segments, then chatroom, then id (apps/webapp/src/components/TipTap/hyperlinkPopovers/internalDocumentLink.ts:38-60). So a heading link built in a Filter view classifies as filter. One built with ?chatroom= classifies as chat.
  • Three callers store or copy through these builders. Mod-k pick applyPicked (TipTap/hyperlinkPopovers/hooks/useHyperlinkEditorForm.ts:119) also serves the Slash "Link to a section" row. TOC copyLink (components/toc/hooks/tocActions.tsx:98) is the desktop TOC right-click "Copy link". Bookmark handleCopyUrl is at components/bookmarkPanel/components/BookmarkItem.tsx:103-108.
  • Notification handleCopyUrl builds the same shape by hand from location.href (components/notificationPanel/components/NotificationItem.tsx:119-127).
  • Chat message getMessageUrl takes the pad slug from location.pathname.split('/').pop() (components/chatroom/components/MessageCard/hooks/useCopyMessageLinkHandler.ts:13). In a Filter view that is the last Filter term, not the pad.

Desired behavior:

  • One stored-link builder pair in apps/webapp/src/utils/link-helpers.ts. Both start from window.location.origin + '/' + padSlugOf(window.location.pathname). padSlugOf is in apps/webapp/src/utils/filterRoute.ts:8.
  • The heading builder sets only h and id. The message builder sets only chatroom and msg_id. Neither sets a hash.
  • The five stored or copied paths above use the new pair. Keep the Notification guard that returns when message_id or channel_id is missing. Keep the getMessageUrl fallback to workspaceId when channel_id is empty.
  • buildBookmarkHref has no address-bar caller, so it may change in place. Fix its JSDoc, which says it mirrors handleCopyUrl.
  • Trap: do not change buildHeadingHref in place. Add a new heading builder beside it. navigateToHeading writes the address bar with buildHeadingHref (components/toc/utils/navigateToHeading.ts:23). An in-place change drops the Filter and ?chatroom= on every TOC click.
  • These callers stay on buildHeadingHref: navigateToHeading, and TocItemBody titleHref (components/toc/TocItemBody.tsx:155). resolveHeadingBreadcrumbs also stays (components/chatroom/utils/buildHeadingPath.ts:20). Breadcrumb.tsx:46 pushes its url into the address bar.

Where to start: link-helpers.ts (buildHeadingHref, buildBookmarkHref), filterRoute.ts (padSlugOf), then the five callers. Line numbers are hints as of 2026-09-28; the agent searches by symbol.

Rules that apply:

  • AGENTS.md §Test Policy, case (b): this bug shipped. The same section requires a browser check for URL and bookmark or notification panel changes, light and dark, on iOS Safari and Android Chrome.
  • apps/webapp/CLAUDE.md §TOC And Heading Actions, §Document Filters, and §Bookmark And Notification Panels.
  • Tests: add cases to the existing apps/webapp/src/utils/link-helpers.test.ts (jsdom URL https://app.test/doc/abc). Use window.history.replaceState to set /doc/abc/term, /doc/abc?chatroom=x&msg_id=y, and /doc/abc#history. Prove it by sabotage: point the new builder at window.location.href, and the new cases must fail. Old links stay as stored; add no backfill.

Verify:

  • cd apps/webapp && bun run test -- --testPathPatterns link-helpers
  • cd apps/webapp && bun run typecheck, then bun run lint at the root.
  • Browser, desktop, light and dark: open a pad with two headings. Apply a Filter. Insert a heading link with Mod-k. Hover it. The preview shows the heading title over "In this document · Heading", not "Filtered view". Open a heading chat and repeat. The preview still shows "Heading", not "Message in chat".
  • Browser: in a Filter view, use TOC right-click "Copy link" and chat message "Copy Link". Paste each into the address bar of a new tab. The pad opens with no Filter.
  • Browser: click a TOC row in a Filter view. The address bar keeps the Filter path.
  • Phone: use iOS Safari and Android Chrome, or a mobile user agent. A narrow window is not enough. In a Filter view, long-press a chat message and use "Copy Link". The link has no Filter segment.

Out of scope

Activity

  1. added
    bugSomething isn't working
    EditorTiptap & Prosemirror
    on Sep 28, 2026
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

    EditorTiptap & ProsemirrorbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions