Skip to content

Navigation Menu

Sign in
Sign up

fix(sidebar): resolve DM usernames on first paint - #167

Open
agustif wants to merge 2 commits into
gammons:main from
agustif:fix/dm-sidebar-names
Open

fix(sidebar): resolve DM usernames on first paint #167
agustif wants to merge 2 commits into
gammons:main from
agustif:fix/dm-sidebar-names

Conversation

@agustif

@agustif agustif commented Aug 31, 2026

Copy link
Copy Markdown

Why

DM sidebar rows were often blank (or a raw user ID). users.conversations IM objects sometimes omit user, so buildChannelItem had nothing to name and the unresolved-DM sweep had nothing to fetch. Resolutions also failed to survive a workspace switch because only the active sidebar was patched.

What

  • Overlay IM counterparties from client.userBoot ims[] onto conversations that lack user (fillIMUsers).
  • Batch-resolve display names from edge before the first sidebar paint (seedDMDisplayNames).
  • Persist resolved names onto wctx.Channels / finder items (patchWorkspaceDM).
  • DMNameResolvedMsg carries TeamID so a late resolution from an inactive workspace cannot rename the active sidebar.
  • UserResolvedMsg also renames matching sidebar DMs.

Test plan

  • go test ./cmd/slk/ ./internal/ui/ ./internal/config/
  • Connect a workspace with DMs whose conversations payload omits user; first paint should show display names, not blank rows.
  • Switch away and back; resolved names should still be there.

Merge after #166 (stack 2/4). This branch contains PR1 + this commit. GitHub will show only this commit once #166 is merged.

agustif added 2 commits August 31, 2026 16:24
saveWorkspaceVersionTS used to look only for [workspaces.<tomlKey>].
When tomlKey fell back to the raw team ID it appended a second block
next to the slug-keyed one, and Load then refused to start ("both
reference team_id").
Merge leftover team-ID-keyed prefs into the slug block, drop the
duplicate key, and write subsequent saves into the existing slug
section (or a matching team_id field).
users.conversations IM objects sometimes omit `user`, so the sidebar
row was blank and the unresolved-DM sweep had nothing to fetch.
Overlay counterparties from userBoot's ims[], seed display names from
edge before the first paint, and patch wctx.Channels so a workspace
switch keeps the resolved name.
DMNameResolvedMsg now carries TeamID so a late resolution from an
inactive workspace cannot rename the active sidebar.

gammons commented Sep 3, 2026

Copy link
Copy Markdown
Owner

There's a correct, valuable fix in here and I want it — but it's bundled with three other changes, one of which introduces a data race.

What's right: fillIMUsers. The bug is real. cmd/slk/channelitem.go:45-51 sets displayName = ch.Name for an IM (always empty), then falls back to ch.User — and if ch.User is empty the row renders blank. Worse, main.go:2554-2559 then queues UnresolvedDM{UserID: ""}, which resolveDMNames can never resolve, so it's a permanently blank row. boot.IM does carry UserID json:"user" (internal/slack/boot/boot.go:132-134), so overlaying it is the correct root-cause fix. That plus the empty-ID guards and the edgeUserDisplayName extraction is about 90 lines and I'd merge it today.

The DMNameResolvedMsg.TeamID guard is also a real fixreducer_workspace.go:92-105 on main patches the active sidebar unconditionally by ChannelID, so a late resolution from workspace B can rename workspace A's row. Good catch. But it's a different bug from "first paint" and belongs in its own commit.

Blocker 1: patchWorkspaceDM introduces a data race. It writes wctx.Channels[i].Name / .Type from the go resolveDMNames(...) goroutine (main.go:2152). wctx.Channels is a plain slice with no synchronization — the WorkspaceContext doc block at main.go:133-143 and main.go:189-200 spells out exactly which fields needed sync.Map/atomic.Pointer and why, and Channels is deliberately not one of them. It's read on the UI goroutine at main.go:1906 and main.go:1375, and mutated on the WS-reader goroutine at main.go:4485 — where there's an explicit comment about avoiding this very race (main.go:4209).

Confirmed under -race:

WARNING: DATA RACE
Write at 0x00c000266510 by goroutine 12:
 cmd/slk.patchWorkspaceDM() main.go:2935
 cmd/slk.resolveDMNames() main.go:2989
Previous read at 0x00c000266510 by goroutine 13: [wctx.Channels reader]

CI is green only because your tests call resolveDMNames synchronously. Route it through p.Send and patch on the UI goroutine (like SectionsRefreshedMsg does), or put wctx.Channels behind an accessor + mutex and document it in that struct's concurrency block.

Blocker 2: seedDMDisplayNames blocks first paint on network I/O. It calls ResolveNow(ids) synchronously on the connect goroutine, before buildChannelItem and before WorkspaceReadyMsg. That's ceil(unknown_peers / 80) sequential HTTP round-trips (usersInfoBatchSize = 80, internal/slack/edge/cache.go:45) on context.Background() with no timeout, per workspace. So on a cold cache the entire workspace — sidebar, channels, everything — waits, and if edge hangs the workspace never appears at all.

That directly reverses a decision documented at main.go:2499-2521, a 23-line comment explaining that the users.list sweep was deliberately deleted and that "a name slk has never seen renders as its user ID for the moment before resolveUser answers" is the accepted trade-off. go resolveDMNames(...) at main.go:2151-2157 already does this asynchronously.

Credit where due: the warm path costs zero requests, since db.ListUsers seeds wctx.UserNames at main.go:2319-2331 and you skip IDs already present. The cost is entirely on cold boot — which is slk's most latency-visible moment.

fillIMUsers alone already gets ~90% of the user-visible win here: a correct user ID instead of a blank, with the existing async sweep naming it a moment later. Showing an ID for 200ms is the trade we already chose.

What I'd like:

  1. Fix the race — don't mutate wctx.Channels/wctx.FinderItems off the UI goroutine. Add a -race test running resolveDMNames concurrently with a reader.
  2. Drop seedDMDisplayNames from this PR. If you want it, it needs its own PR with a measured cold-boot number, made non-blocking, and ResolveNow needs a bounded context.WithTimeout regardless.
  3. Split into three commits: (a) fillIMUsers + empty-ID guards + edgeUserDisplayName, (b) the TeamID cross-workspace guard, (c) workspace-switch persistence. (a) and (b) I'll merge immediately.
  4. Drop the UserResolvedMsg sidebar-rename block (reducer_workspace.go:131-156). DMNameResolvedMsg already covers it, so both now fire SetChannels for the same DM — and SetChannels (app.go:1901-1926) rebuilds the picker slices and re-pushes names to every window model. The comment you replaced at reducer_workspace.go:115-118 documented that separation deliberately.

Your tests are good — TestFillIMUsers_CopiesCounterpartyFromBoot and TestDMNameResolvedMsg_IgnoresOtherWorkspace both pin real behavior. What's missing is any concurrent exercise of patchWorkspaceDM (which is why the race shipped green) and any assertion about how many network calls happen before first paint.

Split it up and the first two land fast.

@gammons gammons added the changes requested Blocking issues found in review label Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

changes requested Blocking issues found in review

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

2 participants

AltStyle によって変換されたページ (->オリジナル) /