Skip to content

fix(workspace): a chat or agent URL you cannot open shows no composer (#3140) - #3156

Merged
vybe merged 2 commits into
devfrom
fix/3140-unavailable-chat-no-composer
Oct 1, 2026
Merged

vybe merged 2 commits into
devfrom
fix/3140-unavailable-chat-no-composer

Conversation

@dolho

@dolho dolho commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • /workspace/c/<a chat that isn't yours> used to show an empty chat of one of your own agents, with a live composer, under that id. It now shows "This chat isn't available" with Back to your chats. No conversation is mounted and no history is read.
  • /workspace/a/<an agent not shared with you> used to show "You don't have access" next to "Start a conversation below.", a live composer, and a "Try again" that couldn't help. It now shows the existing "You don't have access to …" stage alone, with Back to your chats.
  • No "Try again" on a refusal. On a 403/404 the agent band and the details panel no longer offer a retry that would get the same answer. A 5xx keeps its retry.

How

  • Chat URL, decided up front. The shell's thread list is the viewer's whole set: the server applies no LIMIT. So when the list loaded cleanly, an id it lacks isn't the viewer's. The shell excludes a brand-new chat it has just adopted, to avoid a race. activeAgent stays null, so the conversation never mounts. This applies on deep links (bootstrap) and on in-app navigation (the route watcher).
  • Chat URL, fallback. If the list failed, the conversation still tries. A 404 on its history read emits thread-missing. The shell accepts it only while the URL still names that id, so a late 404 can't blank the chat the person moved to.
  • Agent URL. The /workspace/a/:name watcher applies the same roster rule as ?agent=: a name missing from a cleanly loaded roster gets the "unreachable" stage instead of landOnAgent. A roster error keeps its own copy.
  • usePortalAgentPage exposes denied (403/404). The band and the details panel bind retryable / show-retry to it.
  • The stages reuse the existing stage classes and tokens: no new colours or primitives (raw-colour ratchet unchanged).

Changes

  • src/frontend/src/views/Portal.vue: unavailableChatId / chatUnavailable, the new stage, the roster check on the agent route, and a way back from both stages
  • src/frontend/src/components/portal/PortalConversation.vue: emits thread-missing on a history 404
  • src/frontend/src/composables/usePortalAgentPage.js: denied
  • src/frontend/src/components/portal/PortalAgentBand.vue, PortalAgentDetails.vue: no retry when denied
  • Tests (all mounted, Safety-critical UI logic keeps landing in the one tier with no executable coverage — and the stated reason is false #2918):
    • portalUnavailableTargets.mount.spec.js
    • portalAgentBandDenied.mount.spec.js
    • portalThreadMissing.mount.spec.js

Test Plan

  • 14 new mounted tests pass. They cover:
    • both URLs: the stage, no composer, and the way back;
    • your own chat and agent still open;
    • in-app navigation to an unknown id;
    • the failed-list fallback, and a late 404 ignored;
    • 403/404 → denied, while 503 is not denied;
    • the band shows no retry when refused and keeps it for other failures;
    • the conversation emits on a 404 and not on a 5xx.
  • Mutation: reverting each source file turns its tests red. Portal.vue: 4; the composable: 3; the band: 2; the conversation: 1.
  • Full frontend suite: 247 files, 4,512 tests, including the raw-colour, loading-gate and source-text checks. check-design-tokens: OK. npm run build: OK.
  • In the browser, this branch's Vite against the local backend, light and dark:
    • both URLs render their stage with no composer and no page errors;
    • "Back to your chats" returns to the Workspace;
    • an own agent (/workspace/a/<own>) still lands in its chat, and reloading that chat URL still shows the composer.
  • Not re-walked as an external (magic-link) client; the shell logic is the same for both principals.

Fixes #3140

🤖 Generated with Claude Code

…#3140)

Two Workspace URLs offered a live composer for something the URL did not name:

- /workspace/c/<an id that is not yours>: the shell fell back to the first
  roster agent, so the page showed an empty chat of one of your own agents
  under someone else's chat id.
- /workspace/a/<an agent not shared with you>: landOnAgent opened a fresh chat
  with no roster check. The band then said "You don't have access" above
  "Start a conversation below." and a live composer, with a "Try again" that
  cannot help.

The chat case is decided in two places. The shell's thread list is the
viewer's whole set (no server-side LIMIT), so when it loaded cleanly an id it
lacks, and that the shell is not holding as a just-adopted new chat, renders
"This chat isn't available" with a way back. The conversation is never mounted
and its history is never read. When the list failed, the conversation still
tries, and a 404 on its history read emits `thread-missing`. The shell takes
that only while the URL still names the id, so a late answer cannot blank the
chat the person moved to.

An off-roster /workspace/a/<name> now uses the existing "You don't have
access" stage (the ?agent= rule) and gains a way back. The roster must have
loaded cleanly first, so a roster error keeps its own copy.

usePortalAgentPage exposes `denied` for a 403/404, and the band and the details
panel withhold "Try again" for it, since a retry gets the same answer. A 5xx
keeps its retry.

Tests (mounted, #2918), 14 in all:
- portalUnavailableTargets.mount.spec.js: the shell, both URLs, own chat and
  agent still open, in-app navigation, the failed-list fallback, a late 404, and
  the composable's denied flag.
- portalAgentBandDenied.mount.spec.js: no retry when refused.
- portalThreadMissing.mount.spec.js: a 404 emits, a 5xx does not.

Reverting each source file turns its tests red: Portal.vue 4, the composable
3, the band 2, the conversation 1. Full suite 4,512 passed; the design-token
check and the build pass. Looked at on a branch build against the local stack,
in light and dark: both states render with no composer and no page errors, the
way back works, and an own agent and chat still open with their composer.

Fixes #3140

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dolho dolho added the ui PR touches the frontend UI — triggers Playwright e2e tests label Oct 1, 2026
@dolho

dolho commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

/review report: fix/3140-unavailable-chat-no-composer → dev

Files changed: 8 (+409/-9). Scope: CLEAN. Plan completion: 2/2:

  • neither URL renders a composer, and both offer a way back;
  • mounted tests exist for both.

Execution coverage

changed symbol executed by live consumer verdict
Portal.vue chatUnavailable / bootstrap + watcher rule portalUnavailableTargets.mount.spec.js (shell mounted) the stage branch ✅
Portal.vue off-roster /workspace/a/:name same the unreachableAgent stage ✅
PortalConversation → thread-missing on a history 404 portalThreadMissing.mount.spec.js Portal.vue onThreadMissing ✅
usePortalAgentPage.denied portalUnavailableTargets (composable), portalAgentBandDenied.mount.spec.js PortalAgentBand, PortalAgentDetails ✅ for the band; see I1

Fix mutations: reverting each file turns its tests red. Portal.vue: 4; the composable: 3; the band: 2; the conversation: 1.

Live check (local dev stack, 2026-10-01, dev + this PR, light and dark)

  • /workspace/c/<foreign id>: "This chat isn't available", no composer.
  • /workspace/a/no-such-agent: "You don't have access to …", no composer, no retry.
  • 0 page errors.
  • An own agent and chat still open with the composer.

Critical findings

None.

Informational

  • [I1] Test gap: PortalAgentDetails retry bindings aren't mounted (confidence 7/10). PortalAgentDetails.vue: :retryable="!pageDenied" and :show-retry="!pageDenied". They mirror the band's, which is mutation-proven. A mount of the details panel with a denied composable would close it.
  • [I2] Depends on dev's threadsLoaded. Cherry-picked onto a branch older than dev, the shell throws a ReferenceError and the fix doesn't apply. On dev it's correct. Note this for anyone backporting.

Clean categories

  • Auth: UI only; the server already refuses these reads.
  • Race: a just-adopted new chat is excluded via pendingSession, and a late 404 for a chat already left is ignored (both tested).
  • Frontend: existing stage classes and tokens; no v-html.

Summary: Critical 0 · Informational 2 · Scope clean.

…vailable' (#3140)

The sessionId watcher set unavailableChatId when the trusted list lacked the
id, but its 'known' branch never cleared it, so a chat created in another tab
after this one loaded its list stayed behind the unavailable screen until the
user navigated away. Clear it when the id turns up. Mechanical, per the
merge-train note on the PR; new mount test fails without the line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vybe

vybe commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

merge-train: on this train. I pushed one mechanical commit to your branch (0af06f2c4).

Why: the sessionId watcher's known branch never cleared unavailableChatId. A chat created in another tab after this tab loaded its list stayed behind "This chat isn't available" until the user navigated away.

What changed:

  • That branch now clears the unavailableChatId verdict.
  • New mount test in portalUnavailableTargets.mount.spec.js. It fails without the line and passes with it.

Not fixed, follow-ups:

  • The PortalAgentDetails pageDenied retry change isn't executed by any test. Reverting it leaves the suite green.
  • _fetchSessionsFanout swallows a per-agent failure while setting sessionsFailed=false. During deploy skew that can mark a real chat unavailable.

Note: #3168 conflicts with this PR in PortalConversation.vue, so it rides the next train on top of this one.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: batch validated on train #3172

@vybe
vybe merged commit 12cb5f9 into dev Oct 1, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants