Skip to content

fix(v2): a phone first login lands in the workspace thread - #1844

Merged
lilyshen0722 merged 2 commits into
mainfrom
fix/task-144-phone-first-run-workspace
Sep 23, 2026
Merged

lilyshen0722 merged 2 commits into
mainfrom
fix/task-144-phone-first-run-workspace

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Closes TASK-144. Implements ux-lead's ruling on the row.

Cause

V2Layout's auto-select effect returned early on phone — the #1578 direction-C rule, "the phone never redirects: /v2 is the pods list page there." So at 390 a first login stopped on the pods list, with the workspace thread (Scout's welcome, the composer) one tap away.

Change

-    if (selectionMode !== 'auto' || paramPodId || loading || phone || createFromConnectors) return;
+    if (selectionMode !== 'auto' || paramPodId || loading || createFromConnectors) return;
+    // The phone keeps `/v2` as the list for anyone who has opened a pod here.
+    if (phone && readLastPodId()) return;

The list is the right landing for a phone that has a pod to return to, and that is untouched. It is the wrong landing for a device that has never opened one — every first login — so that one case runs the same resolution as desktop (own invite-only workspace, else pods[0]) with replace.

Why it cannot loop: the redirect lands on the pod route, which mounts the param shell, and that instance's paramPodId effect writes LAST_POD_KEY. The next /v2 — the thread's back arrow — finds the key set and stays on the list. replace means browser-back does not re-resolve the landing either. No new screen, no toast, no banner, no change to where the first-run hero renders.

Proof

Mutation-proven in three directions, because the rule is two-sided and one of its halves is a router verb:

mutation result
baseline 18/18 green
phone early-return restored (old guard) both new cases red
device check dropped (phone always redirects) pre-existing #1578 phone case red + the new first case (its second half: a later /v2 must stay on the list)
replace → push the back-step case red, only it
restored 18/18 green

Ruling case (1) — LAST_POD_KEY set, /v2 renders the list and does not navigate — was already pinned by the pre-existing phone test and stays green. Suite neighbours that mount the shell (v2-layout-invariants, V2InviteDmGate, V2FirstRunHero): 4 suites, 146/146.

Limit

jsdom cannot observe replace directly, so that case pins the property it exists for: one step back does not return to /v2. The assertion goes through the router's own history rather than inspecting the navigate call.

Not verified here: the 390 rendering gate — that is ux-lead's, and this seat has no browser.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

UX-GATE: PASS @ e310850 — TASK-144, gated at 390 + 1440 on a production build of this head served with the 1799 fixture plus an own invite-only workspace pod listed LAST (so the landing proves the ownWorkspace branch, not pods[0]). Measured: (1) 390, LAST_POD_KEY unset → /v2 replaces to /v2/pods/pod-workspace, composer present, key written = pod-workspace; browser back has nothing to step into (replace confirmed at the history level). (2) Same device, /v2 again by full reload and by the thread's Back-to-pods arrow → the pods list (.v2-shell--list), and it stays across a further reload. (3) 390 with the key set (live pod, and a pod no longer listed) → list, no navigate. (4) 1440 unset → /v2/pods/pod-workspace as before. Non-blocking, pre-existing and out of this diff: the first-run hero card now overlays the workspace thread at 390 on first login (composer visible beneath, Skip for now present) where it used to sit on the list — same component, same rendering rule, fine by the ruling; the thread back arrow measures 28×28 at 390 against the 44 phone act rule; header reads '1 agents'. Note the ruling named the jsdom phone mock at (max-width: 760px) — the test's matchMedia stub matches that string exactly, so a query change would go red rather than silently pass.

@samxu01
samxu01 force-pushed the fix/task-144-phone-first-run-workspace branch from e310850 to f196bbd Compare September 23, 2026 09:49

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

GATE: PASS at f196bbd2 — measured. BASE 18/18, RESTORED 18/18, tree clean after every mutation. Anchor count 1 on each.

mutation on V2Layout.tsx result
M1 if (phone && readLastPodId()) return; → if (phone) return; (the old behaviour) 2 red — both new tests
M2 → if (false) return; (phone always redirects) 2 red — including the pre-existing "on a phone /v2 is the pods list page"
M3 drop { replace: true } 1 red — the back-bounce test
M4 remove writeLastPodId(paramPodId) 2 red — including the new first-login test

The guard is pinned in both directions, which is what this change needed: M1 catches losing the new behaviour, and M2 catches over-applying it and stealing the list from a returning phone user. A one-sided test would have allowed the second.

M4 is the one I want to call out. Your comment argues the redirect is self-limiting because landing on the thread sets LAST_POD_KEY via the paramPodId effect. That is a claim about a different effect than the one you changed, so it could easily have been prose. It is not: deleting that write reds the new first-login test. The "and the list is where you come back to" half of the test name is doing real work.

No gap found. Nothing further from me.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

UX-GATE: PASS @ f196bbd — re-pin after the rebase onto a2fc1a0 (one backend test file). Patch-id over frontend/src/v2/components is 155780b6, identical to e310850; range-diff shows the commit unchanged. The 390 + 1440 capture and the PASS carry.

At 390 a new account stopped on the pods list: the auto-select effect returns
early on `phone` (the #1578 direction-C rule, "the phone never redirects — /v2
is the pods list page there"), so the workspace thread with Scout's welcome and
the composer was one tap away and easy to miss. 18 of 29 recent signups never
sent a first message.

The list is the right landing for a phone that has a pod to return to, so that
stays. It is the wrong landing for a device that has never opened one, which is
every first login — there the list has nothing to return to. So the phone runs
the same resolution (own invite-only workspace, else pods[0]) for that one
case, with `replace`.

The redirect is self-limiting: it lands on the param shell, whose paramPodId
effect writes LAST_POD_KEY, so the next /v2 finds the key set and stays on the
list, and `replace` means browser-back does not re-resolve it either. No new
screen, no toast, no banner; the first-run hero keeps rendering where it does.

Two jsdom cases on V2Layout with the phone media query mocked (both named in
ux-lead's ruling), plus the pre-existing "phone /v2 is the pods list page" case
for the device that does have history. Mutation-proven three ways: restoring
the early return reds both new cases, ignoring the device reds the pre-existing
one, and dropping `replace` reds the back-step case.
@samxu01
samxu01 force-pushed the fix/task-144-phone-first-run-workspace branch from f196bbd to 3f4f09b Compare September 23, 2026 23:17

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

UX-GATE: PASS @ 3f4f09b — re-pin after rebase onto 38b6ea0: patch-id 155780b6 over frontend/src/v2/components unchanged.

@lilyshen0722
lilyshen0722 merged commit 4a0661d into main Sep 23, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant