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
[Bug]: Markdown preview table of contents links open a new thread instead of scrolling to headings #13946
I searched existing issues and did not find a duplicate.
I included enough detail to reproduce or investigate the problem.
Area
apps/web
Steps to reproduce
Clicking a table of contents link in T3 Code’s built-in Markdown preview opens a new thread instead of jumping to the linked heading.
Steps to reproduce:
Open a Markdown document containing a linked table of contents in T3 Code.
Switch to the rendered Markdown preview.
Click a table of contents entry whose destination is a heading fragment, such as #1-operating-model.
Expected behavior
Expected behavior: The preview scrolls to the linked heading while keeping the current thread open. Heading links should stay within the Markdown preview. If a target is missing, clicking the link should preserve the current thread and document.
Actual behavior
Actual behavior: T3 Code opens a new thread instead. This makes tables of contents unusable for navigating long documents and interrupts the current thread.
Impact
Blocks work completely
Version or commit
T3 Code desktop app 0.0.43-nightly.20260926.2282
Environment
T3 Code desktop app 0.0.43-nightly.20260926.2282 on Linux (KDE Neon 24.04)
Confirmed. This is a desktop bug, and it is still present on main (the 0.0.43 nightly from 2026-09-26 is not missing a later fix). No duplicate issue turned up.
The desktop app keeps the route in the URL hash (createHashHistory in apps/web/src/main.tsx), so a thread lives at something like t3code://app/#/<environmentId>/<threadId>. The Markdown preview renders through ChatMarkdown. A table-of-contents link such as #1-operating-model becomes a real <a href="#1-operating-model">.
Clicking it replaces that hash. Chrome fires popstate for the fragment change, and TanStack Router reads the new hash as the pathname 1-operating-model. That pathname matches the chat index route /, not a heading. IndexDraftLanding in apps/web/src/routes/_chat.index.tsx then calls handleNewThread for the most recent project, which is the new thread you see.
Two details in handleMarkdownFragmentClick (apps/web/src/components/ChatMarkdown.tsx) make this reliable:
Rendered headings never get GitHub-style slug ids, so #1-operating-model has no element to scroll to. When the target is missing, the handler returns without preventDefault, and the browser follows the link.
When a target does exist, the handler still does history.pushState with nextUrl.hash set to the fragment alone. On desktop that overwrites the route hash the same way. The router’s patched pushState picks it up immediately.
On the hosted web app the route is the path, not the hash, so the same click stays on the thread. It still does not scroll, because the heading id was never generated.
A fix needs to keep same-document fragment clicks inside the preview (always preventDefault, and do not write a route-replacing hash on desktop), and to give headings stable ids that those links can find. A missing target should leave the thread and document where they are.
Activity
juliusmarminge commentedon Sep 27, 2026
Triage
Confirmed. This is a desktop bug, and it is still present on main (the 0.0.43 nightly from 2026-09-26 is not missing a later fix). No duplicate issue turned up.
The desktop app keeps the route in the URL hash (
createHashHistoryinapps/web/src/main.tsx), so a thread lives at something liket3code://app/#/<environmentId>/<threadId>. The Markdown preview renders throughChatMarkdown. A table-of-contents link such as#1-operating-modelbecomes a real<a href="#1-operating-model">.Clicking it replaces that hash. Chrome fires
popstatefor the fragment change, and TanStack Router reads the new hash as the pathname1-operating-model. That pathname matches the chat index route/, not a heading.IndexDraftLandinginapps/web/src/routes/_chat.index.tsxthen callshandleNewThreadfor the most recent project, which is the new thread you see.Two details in
handleMarkdownFragmentClick(apps/web/src/components/ChatMarkdown.tsx) make this reliable:#1-operating-modelhas no element to scroll to. When the target is missing, the handler returns withoutpreventDefault, and the browser follows the link.history.pushStatewithnextUrl.hashset to the fragment alone. On desktop that overwrites the route hash the same way. The router’s patchedpushStatepicks it up immediately.On the hosted web app the route is the path, not the hash, so the same click stays on the thread. It still does not scroll, because the heading id was never generated.
A fix needs to keep same-document fragment clicks inside the preview (always
preventDefault, and do not write a route-replacing hash on desktop), and to give headings stable ids that those links can find. A missing target should leave the thread and document where they are.