componentsfix(mail): a link in a chat thread is a link LinkedIn and WhatsApp send their words as plain text, so a pasted URL rendered as characters. Slack linkified only its own <url|label> tokens — which its composer emits and a bot, an unfurl field or pasted text do not. Bare URLs are now linkified in both, before the mrkdwn formatting pass so the parser cannot read a URL's underscores as italics. The splitter is lifted out of the calendar's LinkifiedEventText into lib/linkify. Links are painted with the action ramp, not primary: primary is near-black on light and near-white on dark, the colour of the words around them. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 18, 2026, 9:38 AM1 defecthooksfix(inbox): give every channel chat an artifact, and an address Clicking a LinkedIn row left the URL untouched and the panel beside it empty. One cause: openChannelChat set a display artifact only for Slack. The artifact is not just the address a URL is projected from — it is what artifactToContext reads to pick the surface for the chat column — so LinkedIn and WhatsApp opened with the mail surface beside them and the counterpart card had no slot to render into. All three channels now set one, and LayoutUrlSync's Slack block is extracted into a hook called three times: ?slack=, ?linkedin=, ?whatsapp=. One copy of the ref handshake instead of three. This makes a LinkedIn chat take the single display slot and close on the back button, exactly as Slack always has. Three tests pinned the old asymmetry and now pin the new symmetry. Presentation, from looking at real profiles: - the unlinked "No conversation linked" block is suppressed here; for a DM that will never be a deal it is a permanent no above the only thing this surface has to say - the role shown as current no longer repeats as the first past role - Past is a timeline, with consecutive roles at one company grouped, so a promotion reads as one job rather than three - posts are a carousel, fitting all five in one row's height Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 8, 2026, 4:26 PMlibfix(unibox): a chat id the feed forgot still isn't a Gmail thread `splitHotkeyTargets` decided what a selection id was by asking the loaded feed, and read a lookup MISS as "email". But selection and hover state outlive a feed replacement, so a miss is an ordinary event — and on one, a `li:`/`wa:`/`slack:` id went to the Gmail driver, which is precisely the modify-a-thread-that-does-not-exist this module exists to prevent. The prefix is structural (`services/inbox/feed-core.ts` mints it) and an email row's id is a bare Gmail thread id, which is hex and cannot hold a colon — so the prefix answers the question without the feed. An id that is a chat but resolves to nothing is now dropped from both halves: there is no row to act on, and no driver that could act on it. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 4:40 PMstorefeat(tables): sort by a column, and LinkedIn/WhatsApp output cells Two additions to table documents. SORTING. `schema.sort` orders the rows the way the column's TYPE orders — currency numerically, a date by the instant (in the column's own `format`), a select by its DECLARED option order, `_status` by the fan-out lifecycle. One comparator in `table-sort.ts`, shared by the grid and by `read`, so a sorted read and the sorted table cannot disagree. The sort is a VIEW: `tableRows` keeps document order, so `_id` addressing, the ordinal forms and every row's CRDT identity are untouched. Two consequences fall out of that — the row drag handle is withdrawn while sorted, and `read` reports rather than silently adopts a declared sort, because an ordinal resolves against document order on every backend. `table-sort.ts` also absorbs the tolerant cell readers, which existed twice (the Excel writer's and the grid's) and had already drifted on the checkbox vocabulary. LINKEDIN / WHATSAPP OUTPUT CELLS. Both kinds were declared but their payload fell into `GenericCellOutput`, which holds the words and nothing that says who they are for. Real payloads now, addressable by an existing chat OR by a person with no chat yet — which is the lead-list case the column is for. `canSendCellOutput` becomes `sendableCellOutput`, which RESOLVES a cell into a payload whose fields are required, removing three non-null assertions from the send site. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 8:17 PMtypes.tsfeat(inbox): retire the server-merged feed, and make one email read serve both lists (design: inbox-triage phases 8 + 0) Finishes Phase 8 and lands Phase 0. RETIRED — `inbox.listItems` and `assembleInboxFeed`, with `fetchEmailItems`, `inboxCompiledQuery`, the folder→query fallbacks and `resolveEmailLabelTerms`. feed.ts drops 875→500 lines and no longer reads mail at all. Its 4 test files are RETARGETED, not deleted: scope onto `participatingChannels` and the three sources' SQL, and the real-DB files onto the live `inbox.listChannelItems` route. TWO PAGING BUGS the server-merged feed was hiding, both found by retargeting those tests onto a single source — three others had been covering for each one: * `mergeAndPage` truncated a single-source feed at page two: the sources resume at `<= cursorSortedAt`, so every page re-read its boundary row and spent the `limit + 1` that proves "more remains". It now takes a REQUIRED `sourcesSaturated` — required so a new caller cannot silently reintroduce it. * The sources ordered by `last_message_at` alone while the pager tiebreaks by id, so a LIMIT landing mid-tie-band took an arbitrary subset and the skipped rows sorted ABOVE the minted cursor — gone for good. All six source queries now order `(last_message_at DESC, <itemId> COLLATE "C" DESC)`; the collation is load-bearing, since the default one folds case and ignores punctuation and would disagree with `compareItemsDesc`. Measured: three rows vanished from a paged walk of a real feed. PHASE 0 — one email read, one route resolver: * The `headChanged` latch is per-surface. Phase 8 gave `mail.listThreads` a second caller, so a single per-connection flag meant whichever surface read first consumed the signal. `markPendingHeadChange` fans out to every surface, because the reconcile dedup key carries none and the second reader routinely joins the first's in-flight run. * `use-route-inbox.ts` resolves the active inbox from the URL for BOTH lists; the persisted `settings.activeInboxId` fallback is gone. Closes the important/other and system-folder divergences as well. * `drafts.create` awaits `surfaceDraftInInbox`, and the label join row is born with its ordering key instead of being backfilled by a second statement. compose-feed becomes self-verifying now that its diff target is gone: ordered, unique, stable-across-paging, and watermark-respecting, checked over real paging rounds and each probed against a deliberately broken history. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 9:56 PM