Skip to content

feat(members): open the last member on arrival, mirror it in the URL - #8546

Open
CrysisDeu wants to merge 1 commit into
mainfrom
feat/crew-members-remember-last
Open

feat(members): open the last member on arrival, mirror it in the URL#8546
CrysisDeu wants to merge 1 commit into
mainfrom
feat/crew-members-remember-last

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Crew Members page landed on an empty middle column on every visit and forgot which member was open when you navigated away and back. Zezhen's ask: 「crew mode 页面点开要打开一个 crew 的 chat,如果来回 navigate 要回到上一个打开的」.

  • Opens a member on arrival. A visit that names no member opens the remembered one, else the first row in display order — never the empty column.
  • Remembers the last member. The last member opened is stored per browser (mc-members-last-member, via safeStorage), so leaving for Sessions / Settings and coming back, or a reload, lands on the same conversation. A remembered member that was deleted or renamed falls back to the first row without an error.
  • URL mirrors the selection (/members?member=<name>): back/forward walks the conversations, and a link lands on one. The value is the exact crew name, not the slug — the slug is lossy (Oncall / oncall share one), and a slug link would misroute to the other member's thread, the exact failure this page's header comment says it exists to prevent. The URL is the single source of which member is open; clicks write the URL and one sync effect opens the thread, so click / back-forward / link / restore share one path. A link naming a member that is gone (deleted/renamed) falls back to the first row and says so in a warn-toned status above the thread, leading with the swap — "Showing alpha — 'ghost' is no longer on the roster." (pages.membersPage.member_gone; 13 catalogs + regenerated en-XA) — the user asked for someone specific, so the swap is neither silent nor muted. The remembered-member fallback stays silent: there the user named nobody.
  • Below md the page stays a two-level list→detail navigation: no ?member= IS the roster, so no auto-open there (the same rule SidePanelLayout applies to its remembered tab); a stale ?member= returns to the roster with the same notice above the list (member_gone_roster), and the header back button pops the entry the roster pushed (so the browser's own Back does not land on a duplicate roster entry) while a deep link — which has no roster entry behind it — drops the param in place.

Storage scope: one key per origin. The roster is the gateway's global crew list and localStorage is already per-gateway, so there is no frontend workspace dimension to key on.

Two new strings, pages.membersPage.member_gone and member_gone_roster, added to all 13 catalogs with en-XA regenerated; the existing pick_a_member empty state is still what the mobile roster and an empty crew list show.

Recording

Enter Crew Members → first member auto-opens → open kirocrew-research → leave to Sessions → return: kirocrew-research restored → reload: still kirocrew-research; the bottom caption reads out the live URL

recorded from cdffffd (the PR head ae4804d (rebased onto main c791f0f) differs from it only by the stale-?member= handling — the below-md return-to-roster branch and the desktop gone-member notice — which the recording's flow does not exercise) · isolated pod (kirocrew pod up) built from this branch · mode: default · real server + real roster (the pod's own crews), no fixtures · the only injected element is the bottom caption bar, which prints location.pathname + location.search so the URL is visible (the address bar is outside the viewport capture). GIF pinned to a since-dropped evidence commit d599c35b2 on this branch.

Tests

website/src/pages/members/MembersPage.test.tsx

  • resolveDefaultMember (pure): default → first row; restore → remembered; stale → first row; empty roster → undefined.
  • Page: fresh visit opens the first row (URL + memory written, no empty state); remembered member restored (one POST); stale memory → first row, no notice, memory rewritten; ?member= beats memory; ?member= naming a gone member → first row + URL rewritten + member-gone-notice (role=status, warn tone, leads with the swap) that retires when another member opens; click writes URL + memory and aria-current.
  • Below md: no auto-open (memberThread not called, URL untouched); stale ?member= → URL cleared, roster shown with member-gone-roster-notice, no POST, notice retires on tap; tap → thread + URL; header back → pops the roster-pushed entry (navigate(-1)), memory kept; from a deep link → param dropped in place, roster shown.
  • Existing cases adjusted for the page now opening a member on its own: name lookups scoped to the roster column (the name also renders in the header/drawer), thread-endpoint overrides installed before mount, and the unread-dot cases land on a different member so the flagged thread is genuinely closed.

Local: tsc -b and eslint on both files clean. Vitest left to CI per the work item's rule.

Not changed

No refactor of the page beyond splitting the sort into unfiltered/filtered (so "first member" ignores the search box) and moving the thread-open body into activate so the URL effect and the same-member re-click share it.

@CrysisDeu
CrysisDeu requested a review from a team September 4, 2026 19:52
@CrysisDeu
CrysisDeu requested a review from a team as a code owner September 4, 2026 19:52
@CrysisDeu
CrysisDeu requested a review from pepmach September 4, 2026 19:52
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of ae4804d7f8fab56b78ae3e646184dd7833cc8025 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] ae4804d

False positive or not applicable? A repository writer can comment:
/ai-review override gpt ae4804d7f8fab56b78ae3e646184dd7833cc8025: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of ae4804d7f8fab56b78ae3e646184dd7833cc8025 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

Auto-open, last-member restore, and URL mirroring remove the empty-column dead end, and the gone-member fallback says who opened instead of swapping silently.

Suggestions

  • en.json member_gone / member_gone_roster: use curly quotes “{{name}}” — the catalog's 13 existing “{{name}}” strings set that convention; these two are the only straight-quote '{{name}}' occurrences (de/ja/zh already localize their quote marks).

[UX-REVIEWED] ae4804d

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of ae4804d7f8fab56b78ae3e646184dd7833cc8025 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

URL as the single selection source with one activate path, name-not-slug identity, and an announced gone-member fallback is the right, proportionate shape.

[DESIGN-REVIEWED] ae4804d

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of ae4804d7f8fab56b78ae3e646184dd7833cc8025 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All verification is done. I've read the contract, intent, patch, the full page source, and counted consumers of every new surface. Composing the review now.

First-Principles-Verdict: CONCERNS

The quoted ask is met by items 1–2 alone; the URL layer is unrequested capability that carries the gone-member notices, 26 catalog lines, and the history-state protocol.

What this change ships

Intent: land on the conversation you last had open on the Members page instead of an empty column — an ADDITION (feat) closing a reported UX gap.

  1. Arriving opens your last member, else the first row — justified (the quoted ask)
  2. The opened member is remembered across visits and reloads — justified (the ask)
  3. The open member rides the URL; back/forward and links work — declared; inherited provenance, zero in-code consumers
  4. A link to a deleted member opens the first row and says so — justified given item 3 (documented misroute invariant)
  5. Phones still land on the roster, no auto-open — justified (SidePanelLayout's documented remembered-tab rule)
  6. Phones show a second notice for a stale link — zero option costs almost nothing
  7. Phone back button now pops history when you came from the roster — declared; derived from item 3's pushes
  8. Search-box text no longer changes which member a fresh visit opens — justified, declared
  9. Two strings added to all 13 catalogs — declared
  10. resolveDefaultMember exported — zero non-test consumers

Watch

  • Item 3's stated harm is capability, not a defect: the ask (「来回 navigate 要回到上一个打开的」) is fully met by localStorage memory; "back/forward walks the conversations, and a link lands on one" names no failing person. Grepped /members?: 0 code sites link with the param. It also carries most of the permanent surface — the gone state, fromRoster, both strings — and a shipped URL param cannot be quietly withdrawn. Its one derived benefit is the phone back gesture no longer exiting the page mid-thread (also unreported). A human should confirm this capability is wanted, not just this fix.

Subtractions

  • Drop member_gone_roster (13 catalog lines), the roster-notice branch, and the shown: '' sentinel in MembersPage.tsx: below md the stale-link fallback opens nobody, so there is no misroute to announce — the roster itself shows the name is absent.
  • Drop the export on resolveDefaultMember and its pure describe block: the page tests already pin default/restore/stale, and the export's non-test consumer count is 0 (grepped resolveDefaultMember; one same-file caller).

[FIRST-PRINCIPLES-REVIEWED] ae4804d

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed ae4804d7f8fab56b78ae3e646184dd7833cc8025 — this comment is updated in place on each push.

Review details

I've analyzed the single candidate and traced the URL-sync effect, the activate/openMember paths, and the unread-drain effect against the full MembersPage.tsx.

Candidate 1 (auto-open drains unread badge): The drain effect at lines 383–385 fires because activatememberThreadsetSlots makes activeSlot truthy for the auto-opened member. But this is not an observable wrong outcome: the resolved member's thread is genuinely mounted and displayed on screen (line 751–760), and the code's own documented invariant is "Mounting a member thread IS reading it" (line 371). Clearing the unread signal for a thread the user is now looking at is the intended "opening is reading" semantics of the feature this PR ships, not a defect. The candidate cannot establish (c) an observable wrong outcome — it explicitly concedes it "could not confirm whether the product intends auto-open to count as reading," which is a "might," not a defect. Below the 80 bar; dropped.

No new groundable defect emerged from falsification: the effect is a stable fixed point (a valid urlMember === activeName short-circuits activate, gone-notice writes use functional updates and settle), memberThread is posted exactly once per arrival, and the mobile branch correctly suppresses auto-open.

No findings.

[OPUS-REVIEWED] ae4804d

Verdict parsed from the review's SHA-scoped output markers for commit ae4804d7f8fab56b78ae3e646184dd7833cc8025.

False positive or not applicable? A repository writer can comment:
/ai-review override fable ae4804d7f8fab56b78ae3e646184dd7833cc8025: <one-sentence reason>

@CrysisDeu
CrysisDeu force-pushed the feat/crew-members-remember-last branch from cdffffd to 4777a72 Compare September 4, 2026 21:00
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/crew-members-remember-last branch from 4777a72 to e891871 Compare September 4, 2026 21:39
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX Review disposition — fixed in 4777a7207 (carried in e891871ce)

  • Same stale link on mobile skips the roster. Below md, a ?member= naming a member that is gone is now rewritten to /members (the roster) with no thread opened, instead of falling to the first row. Test: a stale ?member= returns to the roster instead of opening a different member.

With urlMember set but unmatched, the effect falls past the else if (isMobile) branch … bouncing a phone user straight into a different member's thread.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX Review disposition — fixed in e891871ce

  • Stale deep link bounces desktop users into another member's thread with no notice. The URL-named gone-member fallback now renders a one-line status above the mounted thread — 'ghost' is no longer on the roster — showing alpha. (pages.membersPage.member_gone, 13 catalogs + en-XA) — and retires it once a different member opens. The remembered-member fallback stays silent, as the review asked: there the user named nobody. Test: a URL naming a member that is gone falls back to the first row and SAYS so.

falling back silently should stay reserved for the remembered member, where the user named nothing.

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 4, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX Review disposition — accepted-and-deferred#8570

render the notice at default text weight (or text-warning), keeping role="status".

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX Review disposition — accepted-and-deferred#8570

show the same member_gone-style status line above the roster on that branch.

@CrysisDeu
CrysisDeu force-pushed the feat/crew-members-remember-last branch from e891871 to 0d139ea Compare September 4, 2026 22:21
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/crew-members-remember-last branch from 0d139ea to d11cc70 Compare September 4, 2026 23:41
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 4, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/crew-members-remember-last branch from d11cc70 to ab6615e Compare September 5, 2026 00:14
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX Review disposition — fixed in ab6615e47 (supersedes the earlier deferral to #8570)

  • Swap notice is decision-critical but visually muted. The thread-side notice now renders text-[13px] text-warn (body size, warn tone) instead of text-xs text-muted, keeps role="status", and leads with the swap: "Showing alpha — 'ghost' is no longer on the roster." Test pins the tone and the leading clause.

render the notice at default text weight (or text-warning), keeping role="status".

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX Review disposition — fixed in ab6615e47 (supersedes the earlier deferral to #8570)

  • Below md the same stale link is answered with silence. The below-md branch now records the gone member and the roster shows "'ghost' is no longer on the roster." above the list (member_gone_roster, 13 catalogs + en-XA, role="status", same warn tone), retired when the user taps a member. Test: a stale ?member= returns to the roster and says where the member went.

show the same member_gone-style status line above the roster on that branch.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

UX Review disposition — fixed in ab6615e47 (the round-2 suggestion)

  • member-back uses replace, so after the UI back the browser's own Back lands on an identical roster entry. Roster taps now push with state.fromRoster; the header back pops that entry (navigate(-1)) so history keeps one roster entry, and falls back to the in-place param drop only for a deep link, which has no roster entry behind it. Two tests pin both paths.

popping instead of replacing would keep one roster entry in history.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 5, 2026
@bolichen97
bolichen97 enabled auto-merge (squash) September 5, 2026 06:47
chenmingwei23
chenmingwei23 previously approved these changes Sep 5, 2026
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 5, 2026
@CrysisDeu
CrysisDeu force-pushed the feat/crew-members-remember-last branch from ab6615e to f44861a Compare September 5, 2026 07:06
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Sep 5, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Direction is right and the implementation is solid: no history loop (all fallback URL writes use replace), exact-name lookup so a stale link can never open the wrong member's thread, both gone-member notices in all 13 catalogs, and CI is green on this head. Two behaviours I want changed before approving:

  1. Every member click pushes a history entry. Ten clicks in the roster means ten Back presses to leave /members. The Sessions sidebar does not behave this way when switching conversations. Please replace when switching between members and only push when entering a thread from the roster (or from a deep link), so Back leaves the page after one press.
  2. A dead link overwrites the remembered member. activate() writes LAST_MEMBER_KEY on every open including the fallback, so following one stale ?member=ghost link permanently replaces the user's remembered member with the first row. Skip the memory write on the fallback path; the test 'a URL naming a member wins...' should be adjusted accordingly.

Low, non-blocking: the back/forward claim rests on a useNavigate spy (navigateSpy(-1)), not a driven history test -- one MemoryRouter test walking two members and pressing Back would pin the behaviour above. Will re-approve as soon as those two land.

@CrysisDeu
CrysisDeu force-pushed the feat/crew-members-remember-last branch from f44861a to 83915af Compare September 5, 2026 07:37
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 5, 2026
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 5, 2026
The Crew Members page landed on an empty column every visit and forgot
which member was open when the user navigated away and back. Now:

- A visit that names no member opens the remembered one, else the first
  row in display order — never the empty column.
- The last member opened is remembered per browser (mc-members-last-member,
  via safeStorage), so leaving for Sessions/Settings and returning, or a
  reload, lands on the same conversation. A remembered member that was
  deleted or renamed falls back to the first row without an error.
- The open member rides the URL (?member=<name>): back/forward walks the
  conversations and a link lands on one. The value is the exact crew name,
  not the slug — the slug is lossy and would misroute Oncall to oncall.
  A link naming a member that is gone takes the same fallback and says so
  in a warn-toned status above the thread, leading with the swap
  ("Showing X — 'Y' is no longer on the roster."; 13 catalogs + en-XA):
  the user asked for someone specific, and a silently mounted other thread
  is the misroute this page exists to prevent.
- Below md the page stays a two-level list->detail navigation: no
  ?member= IS the roster, so no auto-open there (same rule as
  SidePanelLayout's remembered tab). A stale ?member= returns to the
  roster with the same notice above the list. The header back pops the
  entry the roster pushed (no duplicate roster entry in history) and
  drops the param in place for a deep link, which has no roster behind it.

Tests cover default, restore, stale fallback (memory: silent; URL: with
notice on both sides of md, retired on the next open), URL-over-memory,
click writes URL+memory, and the below-md back semantics.
@CrysisDeu
CrysisDeu force-pushed the feat/crew-members-remember-last branch from 83915af to ae4804d Compare September 5, 2026 12:31
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: passed Eligible automated validation passed for the current revision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants