Skip to content

feat(web): address destinations, cards and panel views by path - #6667

Merged
tlgimenes merged 3 commits into
mainfrom
claude/polymorphic-singing-seal
Aug 28, 2026
Merged

feat(web): address destinations, cards and panel views by path#6667
tlgimenes merged 3 commits into
mainfrom
claude/polymorphic-singing-seal

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What

Navigation was thread-based: /$org/$taskId with 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 Overview
/$org/agents/{-$project}/{-$panel} an agent's workspace; the view is a path segment
/$org/tasks/{-$taskKey} the board, and /$org/tasks/DECO-01 opens that card
/$org/reports /$org/library org-wide
/$org/$taskId legacy, mounted forever, translated

Unconditional — #6642 removed nav_v2 and made first-class navigation the only UI.

The rule

Path = which page. Search = how that page is laid out.

main used to conflate two things: which view shows, and whether the panel is open at all (the 0 sentinel). That conflation — not the path — is why a segment couldn't express "closed". Split them: the path names the view, and ?mainpanel mirrors ?sidepanel as 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/thread are declared once on the pathless agent-shell parent.

Cards

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. A non-canonical spelling (deco-1, a raw id) rewrites itself to the shareable form. /$org/t/KEY stays for delivered digest email, reduced to a redirect needing no fetch.

Legacy URLs never break

/$org/$taskId translates via a pure (pathname, search) function with no thread lookup. ?main= is accepted forever: two server-side emitters put main=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

  • Panel toggles fabricated a thread id via crypto.randomUUID() that reached the SSE subscriber — a retry loop, 24–570 stream 404s per destination visit — and reported fabricated thread_ids to PostHog.
  • "New chat" on a project created the thread on the Super Agent, then wrote a stale ?virtualmcpid= that overrode the path segment.
  • Org-level destinations could be scoped to a project the same way; /home, /tasks, /reports, /library are Super-Agent-only again.
  • ProjectSwitcher remount-looped /chat and /tasks (two useSuspenseQuery dialogs in a portal whose target node is provider state).
  • Stripe return URLs pointed at /{org}/members, which matched no route — it fell through and minted a thread row named members on every checkout return.
  • /$org?sidepanel=true silently dropped the param, so the URL that opens the chat did nothing.
  • choose-editor was not a reserved org slug despite being a root route.

Also

STUDIO_API_TARGET=https://studio.decocms.com bun run --cwd=apps/web dev points the dev server's /api proxy at a remote Studio, for exercising the UI against real data. It rewrites Origin because trustedOrigins derives 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), knip clean. bun run test7895 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/chat doesn't 404 — the legacy catch-all swallows any unknown single segment. Pre-existing, but the retired URL now fails silently.
  • A pasted ?virtualmcpid= stays visible on org-level URLs. Inert; deliberately not stripped rather than add a third canonicalisation.
  • use-destination-route.test.ts was deleted with the project switcher (built, then removed as premature). DESTINATION_ROUTE and useLeafRoutePath have no unit coverage.
  • Cold entry to /$org shows Overview with no composer — the uniform collapse rule; starting a chat means opening the panel first.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the claude PR authored by a coding agent label Aug 28, 2026
tlgimenes and others added 3 commits August 28, 2026 02:11
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
tlgimenes force-pushed the claude/polymorphic-singing-seal branch from f38895e to 23ee6f2 Compare August 28, 2026 05:12
@tlgimenes tlgimenes changed the title feat(web): promote navigation destinations to real routes feat(web): address destinations, cards and panel views by path Aug 28, 2026
@tlgimenes
tlgimenes merged commit d2a702b into main Aug 28, 2026
34 checks passed
@tlgimenes
tlgimenes deleted the claude/polymorphic-singing-seal branch August 28, 2026 12:15
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>
decocms Bot pushed a commit that referenced this pull request Aug 28, 2026
PR: #6680 fix(web): always run a repo-backed editor on a thread (#6667 follow-up)
Bump type: patch

- decocms (apps/api/package.json): 4.291.3 -> 4.291.4
- @decocms/native (apps/native/package.json): 4.291.3 -> 4.291.4

Deploy-Scope: web
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude PR authored by a coding agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant