feat(web): address destinations, cards and panel views by path - #6667
Merged
Conversation
Navigation was thread-based: /$org/$taskId with a ?main= param selecting the
main panel, so the sidebar's destinations were overlays on someone's thread and
opening one from a coding agent forced a hand-back to the Super Agent, minting a
thread to do it. Destinations become paths:
/$org/home /$org/chat/{-$project} /$org/tasks/{-$project}
/$org/reports /$org/library
Unconditional — #6642 removed the nav_v2 flag and made first-class navigation
the only UI, so there is no gate and no alternate chrome.
The project segment goes only where swapping it changes what you see: chat (it
is the old ?virtualmcpid=, moved into the path) and tasks. Reports is one per
org and the Library has no project axis, so neither takes one.
Governing rule, documented in router.tsx: path = which page, search = how that
page is laid out. main/sidepanel/thread are declared once on the pathless
agent-shell parent; per-agent views (preview/code/content/git/settings) stay in
?main= because main is panel visibility, and main=0 means closed, which no path
segment can express.
/$org/$taskId stays mounted forever — two of its URLs are in already-delivered
email — and translates to the new shape via a pure (pathname, search) function
with no thread lookup.
Panel behaviour:
- the chat panel starts collapsed on a destination that declares a default main
view, and open on /chat, where chat is the subject
- ?sidepanel is a boolean; the legacy "chat"/0 values still parse, since URLs
outlive a rename and a throwing validateSearch on the pathless parent would
take down every route beneath it
- retainSearchParams carries it across navigation, so opening the chat once
keeps it open
Also fixes, all found by running it:
- panel toggles fabricated a thread id via crypto.randomUUID which reached the
SSE subscriber (a retry loop, 24-570 stream 404s per destination visit) and
reported fabricated thread_ids to PostHog. Absence is now a type.
- "new chat" read only ?virtualmcpid=, so on /chat/<project> it created the
thread on the Super Agent and wrote a stale param that then overrode the path
- ProjectSwitcher mounted two useSuspenseQuery dialogs inside a portal whose
target node is provider state, remount-looping /chat and /tasks
- Stripe billing return URLs pointed at /{org}/members, which matches no route
and minted a thread row named "members" on every checkout return
- choose-editor was not a reserved org slug despite being a root route
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Destinations open the chat collapsed, and `/$org` resolves to one — so the landing page lost its composer and four e2e specs that wait on the chat input timed out. `/$org` is a resolver outside the agent shell that owns the layout search, and its <Navigate> forwarded a fixed list that omitted `sidepanel`, so `/$org?sidepanel=true` silently dropped it. Declare the param on the route and forward it, which makes that URL the way to land with the chat open. The four specs (chat-input-draft, hosted-tier-selector) exercise the composer, not the panel default, so they ask for it explicitly rather than relying on a default that no longer holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three shapes the URL could not express before.
A card gets its own address: /$org/tasks/{-$taskKey}, taking the slot the
UI-unreachable {-$project} held. The segment is the human key — DECO-01, or a
synced card's own OS-333 — because a uuid is not a thing anyone pastes into
Slack. `findTaskByKeyOrId` already resolved a key or a raw id, so a
non-canonical spelling rewrites itself to the shareable form. /$org/t/KEY stays
(delivered digest email) as a redirect that needs no fetch.
The main panel's view becomes a path segment: /$org/agents/{-$project}/{-$panel}.
`main` conflated two things — which view shows, and whether the panel is open at
all (the `0` sentinel) — which is why a segment could not express "closed".
Split them: the path names the view, and ?mainpanel mirrors ?sidepanel as a
boolean. The six tab ids that carry a payload keep it in search, since they
encode their own `/` and `:`.
The destination is /agents, not /chat: it is the agent's workspace, and the
sidebar rows that link to it are agents. No alias — both /chat emitters were
introduced on this branch and never shipped, so no delivered URL holds it.
?main= is accepted forever regardless: two server-side emitters put
main=app:<conn>:<tool> into mail that is already in inboxes. Those emitters now
mint the path form, so only delivered links use the legacy shape.
Also: org-level destinations are Super-Agent-only again. Opening a coding
agent's thread from the chats menu on /home, /reports or /library used to keep
you there and write ?virtualmcpid=<that agent>, which then beat the path segment
and scoped the whole destination to a project. A thread now goes where its agent
lives, and only the legacy /$org/$taskId route reads that search key.
STUDIO_API_TARGET points the dev server's /api proxy at a remote Studio, for
exercising the UI against real data. It rewrites Origin because trustedOrigins
is derived from the server's own base URL; never grant that to a deployed one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tlgimenes
force-pushed
the
claude/polymorphic-singing-seal
branch
from
August 28, 2026 05:12
f38895e to
23ee6f2
Compare
decocms Bot
pushed a commit
that referenced
this pull request
Aug 28, 2026
PR: #6667 feat(web): address destinations, cards and panel views by path Bump type: minor - decocms (apps/api/package.json): 4.289.2 -> 4.290.0 - @decocms/native (apps/native/package.json): 4.289.2 -> 4.290.0 - @decocms/e2e (packages/e2e/package.json): 1.55.0 -> 1.56.0 - @decocms/shared (packages/shared/package.json): 0.65.1 -> 0.66.0 Deploy-Scope: both
guitavano
added a commit
that referenced
this pull request
Aug 28, 2026
…p) (#6680) After #6667 started addressing destinations by path, a repo-backed agent editor could open with no thread in the URL (`?thread=` absent — a deep link, choose-editor, or any navigation that didn't carry it, since the shell deliberately doesn't retain `thread`). The branch is a thread field, so a threadless editor stranded the branch picker on "Select branch…" with selecting and "New" both silently no-op (setCurrentTaskBranch is gated on an active thread), and the adopt/auto-fresh effects never fired. AgentInsetProvider now guarantees a thread for repo-backed agents: when the URL names none it mints one into `?thread=` before mounting, reusing the user's idle empty "New chat" for that agent when one exists — exactly what useNavigateToAgent does (findReusableNewChat ?? fresh id). This restores the pre-#6667 behavior where the client minted the thread id and the ensure fallback created the row on landing. Scoped to repo-backed agents; the Super Agent keeps its lazy threadless composer. Complements #6639: with a thread guaranteed, adopt-branch mints a fresh (unmaterialized, non-stale) branch and the auto-fresh-stale check evaluates correctly instead of sitting dead on a threadless entry. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Navigation was thread-based:
/$org/$taskIdwith a?main=param selecting the main panel. Destinations were overlays on someone's thread, so opening one from a coding agent forced a hand-back to the Super Agent, minting a thread to do it.Now every page, card and panel view has an address:
/$org/home/$org/agents/{-$project}/{-$panel}/$org/tasks/{-$taskKey}/$org/tasks/DECO-01opens that card/$org/reports/$org/library/$org/$taskIdUnconditional — #6642 removed
nav_v2and made first-class navigation the only UI.The rule
mainused to conflate two things: which view shows, and whether the panel is open at all (the0sentinel). That conflation — not the path — is why a segment couldn't express "closed". Split them: the path names the view, and?mainpanelmirrors?sidepanelas a boolean. The six tab ids carrying a payload (app:<conn>:<tool>,code:<path>, …) keep it in search, since they encode their own/and:.main/sidepanel/threadare declared once on the pathless agent-shell parent.Cards
The segment is the human key —
DECO-01, or a synced card's ownOS-333— because a uuid is not a thing anyone pastes into Slack. A non-canonical spelling (deco-1, a raw id) rewrites itself to the shareable form./$org/t/KEYstays for delivered digest email, reduced to a redirect needing no fetch.Legacy URLs never break
/$org/$taskIdtranslates via a pure(pathname, search)function with no thread lookup.?main=is accepted forever: two server-side emitters putmain=app:<conn>:<tool>into mail already in inboxes. Those emitters now mint the path form, so only delivered links use the old shape.Bugs fixed, all found by running it
crypto.randomUUID()that reached the SSE subscriber — a retry loop, 24–570 stream 404s per destination visit — and reported fabricatedthread_ids to PostHog.?virtualmcpid=that overrode the path segment./home,/tasks,/reports,/libraryare Super-Agent-only again.ProjectSwitcherremount-looped/chatand/tasks(twouseSuspenseQuerydialogs in a portal whose target node is provider state)./{org}/members, which matched no route — it fell through and minted a thread row namedmemberson every checkout return./$org?sidepanel=truesilently dropped the param, so the URL that opens the chat did nothing.choose-editorwas not a reserved org slug despite being a root route.Also
STUDIO_API_TARGET=https://studio.decocms.com bun run --cwd=apps/web devpoints the dev server's/apiproxy at a remote Studio, for exercising the UI against real data. It rewritesOriginbecausetrustedOriginsderives from the server's own base URL — safe only because the proxy is unreachable off the machine.Testing
fmt,check(14 workspaces),lint(0 errors),knipclean.bun run test→ 7895 pass / 0 fail. Browser-verified against a real org: every destination, both legacy translations, the card URL round-trip, and the panel defaults.Known gaps
/$org/chatdoesn't 404 — the legacy catch-all swallows any unknown single segment. Pre-existing, but the retired URL now fails silently.?virtualmcpid=stays visible on org-level URLs. Inert; deliberately not stripped rather than add a third canonicalisation.use-destination-route.test.tswas deleted with the project switcher (built, then removed as premature).DESTINATION_ROUTEanduseLeafRoutePathhave no unit coverage./$orgshows Overview with no composer — the uniform collapse rule; starting a chat means opening the panel first.🤖 Generated with Claude Code