Skip to content

feat(web): board cards show where a thread came from - #390

Merged
patroza merged 3 commits into
fork/devfrom
feat/board-identity-mark
Aug 10, 2026
Merged

feat(web): board cards show where a thread came from#390
patroza merged 3 commits into
fork/devfrom
feat/board-identity-mark

Conversation

@omegent-app

@omegent-app omegent-app Bot commented Aug 10, 2026

Copy link
Copy Markdown

Board cards now show where a thread came from.

Both sidebars and the mobile board already mark a thread's source and participants with
ThreadIdentityMark. The web board did not — so a card gave no way to tell a Discord- or
Jira-started thread from a local one, or to see who else is on it, without opening the thread. This
was a web/mobile parity gap rather than a new idea, so the placement is copied rather than invented:
the mark trails the card title, exactly where Sidebar.tsx, LegacySidebar.tsx and
BoardScreen.tsx put it.

Layout

The title was line-clamp-2 w-full. It becomes the flexible child of a new row
(flex min-w-0 items-start gap-1.5) with the mark as a shrink-0 sibling, so the two-line clamp and
truncation are unchanged and a long title cannot push the mark out of the card.

A thread with no source costs nothing: SourceChannelGlyph returns null for an empty channel,
so the row renders exactly as before — no reserved space, no height change. That is asserted, not
assumed.

Adversarial review

Both reviewers called the change sound. One real bug between them, plus test-strength findings — all
fixed in 0bb062b5f.

Finding Disposition
gpt-5.6-sol, MEDIUMBoardCardDragOverlay is aria-hidden and its test asserts it holds no focusable controls, but ParticipantStack always renders tabIndex={0}. Dragging a thread with participants put a tab stop inside a clone assistive technology cannot see Fixed. ParticipantStack / ThreadIdentityMark take an interactive flag and the card passes its existing rendering.interactive through. The existing overlay test passed only because its fixture had no participants — a test agreeing with itself. The new participant-bearing overlay case fails without the fix; I verified that by reverting the prop rather than assuming
both, MEDIUM/LOW — the tests asserted the literal Δ character, which would go quiet on any glyph redesign and says nothing about non-Discord channels; no participant-path or overlay coverage Fixed. They now assert the component's own contract (source-channel-glyph / participant-stack testids and the aria-label), plus a participant-only fallback case and the overlay case
both, LOW — the call site restated the participant-channel fallback that ThreadIdentityMark already applies Fixed in 1d8f0eb50
grok-4.5, MEDIUM — tabbing to the stack focuses something that does not open the thread on Enter Not changed. Pre-existing ParticipantStack behaviour, identical in both sidebars; the board only makes it denser. Worth fixing in the component, not smuggled into this change
both, LOW — each participant-bearing card subscribes to the identity-claim atom, and a board shows more cards than a sidebar shows rows Accepted. O(visible cards), no per-card network, and claim maps change rarely. Channel-only threads add no subscription at all
grok-4.5, LOW — the surface guard only proves the token appears in the file True of every guard in that file; it is an anti-stack-drop net, not a behaviour test. The BoardCard tests carry the behaviour

Both independently confirmed what I most wanted checked: the two-line clamp and truncation still
hold, a card with no source is byte-identical to before (the glyph renders null, so the flex
gap reserves nothing), the effective channel matches the sidebar's derivation, memo is not broken,
and the fields really are populated in production — traced from useThreadShells() through
OrchestrationThreadShell to the persistence query.

Verification

  • Typecheck clean across 18 packages; vp build of the web app passes.
  • 2,368 tests pass, the only failure being the pre-existing CodexTextGeneration launch-args one
    that reproduces on unmerged fork/dev.
  • Five new BoardCard tests: the Discord glyph appears for a thread with a source, and is absent
    for one without — the pair is what makes the first assertion mean something.
  • Confirmed the data is really there rather than test-only: OrchestrationThreadShell carries
    originSource and participantSummaries, and it is the same useThreadShells() source the
    sidebar already renders the mark from.
  • forkSurfaceExistence now guards the board card alongside the sidebars it already checked.

Note

The web unit suite has a pre-existing intermittent failure in
browserHistoryStore.test.ts (~1 run in 3). It is not from this branch: I reproduced it on
clean fork/dev and again on f067b34a1, before any of my recent work. Unrelated to the board,
and worth its own look.

Co-authored by @patroza

opened by Patrick Roza in chat thread Discord · Discord · T3

omegent-app Bot and others added 3 commits August 10, 2026 07:36
Both sidebars and the mobile board mark a thread's source and participants with
ThreadIdentityMark; the web board did not, so a card gave no way to tell a
Discord- or Jira-started thread from a local one, or to see who else is on it,
without opening the thread.

The mark now trails the card title, matching where the sidebars and the mobile
board put it. The title keeps its two-line clamp by becoming the flexible child
of that row rather than spanning it, so nothing about the existing layout
moves when a thread has no source — the glyph renders nothing at all in that
case.

The fork's surface guard now covers the board card too, next to the sidebars it
already checks.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
ThreadIdentityMark already falls back to the first participant's channel when
no origin channel is given, so restating it at the call site was a second copy
of the same rule that could drift.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
BoardCardDragOverlay is aria-hidden and its test asserts it holds no focusable
controls, but ParticipantStack always renders tabIndex={0}. Dragging a thread
that has participants therefore put a tab stop inside a clone that assistive
technology cannot see. The existing overlay test passed only because its
fixture had no participants — the exact shape of a test that agrees with itself.

ParticipantStack and ThreadIdentityMark take an `interactive` flag, and the
board card passes its existing rendering.interactive through, so the mark is
inert in the clone and unchanged on a real card.

The tests now assert the component's own contract (source-channel-glyph and
participant-stack testids, and the aria-label) rather than the Δ character,
which would have gone quiet on any glyph redesign and said nothing about other
channels. Added the participant-only fallback case and a participant-bearing
overlay case; the latter fails without this fix, which is the point.

Found by the gpt-5.6-sol adversarial review.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
@patroza
patroza merged commit 4f3b2a5 into fork/dev Aug 10, 2026
5 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