feat(coordination): reconciled agent-keyed inbox (de-dupe, segregate, retention) + exposé flicker fix - #171
Conversation
The periodic capture refresh repainted the full screen (dimmed backdrop + panel frame + tiles) every tick, which flickered visibly because the tmux popup does not honor synchronized-output mode 2026 — the repaint showed progressively (blank → backdrop → complete). It also repainted when nothing had changed. refreshCaptures now reports whether any capture changed, so an idle exposé does not repaint at all. The backdrop and panel frame are static (they depend only on size), so render(full=false) repaints just the opaque tiles in place; the full backdrop+frame is repainted only on initial paint, resize, tile-count change, or zoom. Navigation also repaints tiles-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add pure src/coordination-model.ts: buildCoordinationModel joins live sessions/teammates/services against the notification log and genuine threads into agent-keyed, urgency-sorted items. Each item carries reachability (live/offline/missing/none), a stale heuristic (live agent whose label moved on from an unread needs-input notice — display-only, never a write), an actionable flag, and per-agent notification rollup. Dormant until the view is wired (next phase). Also exclude notification-tagged threads from buildThreadEntries so the Coordination Threads section stops mirroring the Inbox (notifications are stored as exchange threads tagged `notification`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refreshNotificationEntries now reconciles the notification log against live agent state via buildCoordinationModel, then flattens the urgency-sorted, agent-keyed items back to a 1:1 notification list (nav/actions/detail unchanged). A parallel notificationRowMeta carries per-row reachability / stale / actionable. showCoordination builds threadEntries before the refresh so the model can annotate on first paint. The inbox now renders actionable rows first, a "handled · unreachable" divider, then the de-emphasized tail; each row tags reachability (live/offline/missing) and a stale marker (live agent that moved on from an unread needs-input notice). Header shows "(N of M)". No visual rollup yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a notification-inbox cleanup that keeps the inbox bounded without ever dropping a request the user still owes attention. New top-level `inbox` config (cleanupEnabled/retentionDays/cleanupIntervalMs/maxSize, default 14d/daily/10), a pure src/inbox-cleanup.ts (buildInboxCleanupPlan/runInboxCleanup) that archives read/handled notifications past the retention window and trims overflow beyond maxSize — evicting read-then-oldest and never a protected (unread actionable) row — and a scheduler (cleanupInbox/start/stopInboxCleanup) wired exactly like graveyard cleanup (start + immediate run on project-service launch, stop on teardown). cleanupInbox protects the unread notifications of agents the live coordination model still considers actionable, so missing/stale rows can be capped while live work is never archived. Archival uses clearNotifications (recoverable via includeCleared), consistent with the corrected invariant: read state tracks the user's relationship to the request, not a process lifecycle event. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughIntroduces a ChangesCoordination Inbox and Cleanup
tmux Incremental Render Optimization
Sequence Diagram(s)sequenceDiagram
participant SessionLaunch as runProjectService
participant PersistenceMethods as persistenceMethods
participant InboxCleanup as inbox-cleanup
participant CoordModel as buildCoordinationModel
participant Notifications as refreshNotificationEntries
SessionLaunch->>PersistenceMethods: startInboxCleanup()
PersistenceMethods->>PersistenceMethods: loadConfig → check cleanupEnabled, normalizeInterval
loop Every cleanupIntervalMs
PersistenceMethods->>CoordModel: buildCoordinationModel(notifications, sessions)
CoordModel-->>PersistenceMethods: actionable item ids (protected set)
PersistenceMethods->>InboxCleanup: buildInboxCleanupPlan(config, protectedIds)
InboxCleanup-->>PersistenceMethods: InboxCleanupPlan (aged + overflow targets)
PersistenceMethods->>InboxCleanup: runInboxCleanup(plan)
InboxCleanup-->>PersistenceMethods: InboxCleanupRunResult
PersistenceMethods->>Notifications: refreshNotificationEntries(host)
Notifications->>CoordModel: buildCoordinationModel(...)
CoordModel-->>Notifications: CoordinationModel (sorted items)
Notifications-->>PersistenceMethods: updated notificationEntries + notificationRowMeta
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/workflow.test.ts (1)
11-13: ⚡ Quick winAssert exclusion by thread tag instead of title text.
Line 73 is tied to display text, but the production filter is tag-based. Asserting on tags makes this test resilient to title-format changes.
Proposed test hardening
import { addNotification } from "./notifications.js"; +import { NOTIFICATION_TAG } from "./notifications.js"; import { buildCoordinationThreadEntries, buildWorkflowEntries, filterWorkflowEntries } from "./workflow.js"; @@ - expect(entries.some((entry) => entry.thread.title.includes("[Needs input]"))).toBe(false); + expect(entries.some((entry) => (entry.thread.tags ?? []).includes(NOTIFICATION_TAG))).toBe(false); expect(entries.some((entry) => entry.thread.kind === "task")).toBe(true);Also applies to: 73-74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/workflow.test.ts` around lines 11 - 13, The test assertions at lines 73-74 are currently checking against display text or title format, but the production filter implementation uses thread tags for filtering. Update these assertions to check the thread tags property instead of title text to ensure the test aligns with how the production code actually filters entries and to make the test resilient to future changes in title formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/coordination-model.ts`:
- Around line 18-24: The liveLabel variable is undefined because the semantic
field is never populated when DashboardSession objects are constructed. In the
resolveReachability function, liveLabel is extracted from
session.semantic?.user?.label at line 100, but since semantic is undefined, this
always results in undefined. This causes the stale detection check at line 143
to fail. Fix this by adding a fallback in the code that reads liveLabel: when
session.semantic?.user?.label is unavailable, use the top-level label field
(which exists on DashboardSession at line 18) as the fallback value. This
ensures liveLabel has a value for the stale check to work correctly, allowing
unread needs-input notifications to be properly marked stale and preventing
stale entries from being retained in cleanup operations.
In `@src/tmux/expose.ts`:
- Around line 611-615: The periodic refresh timer in the scheduleRefresh
function only calls render(false) when refreshCaptures() returns true, which
means resize events are missed if captures remain unchanged. To fix this, modify
the logic in the timer callback within scheduleRefresh to also track resize
events separately from capture changes, and call render(false) whenever either
captures have changed OR a resize has occurred, ensuring the frame and layout
are updated even when content stays identical.
---
Nitpick comments:
In `@src/workflow.test.ts`:
- Around line 11-13: The test assertions at lines 73-74 are currently checking
against display text or title format, but the production filter implementation
uses thread tags for filtering. Update these assertions to check the thread tags
property instead of title text to ensure the test aligns with how the production
code actually filters entries and to make the test resilient to future changes
in title formatting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 52ffaab3-8451-4778-ae33-df676040f4b7
📒 Files selected for processing (18)
src/config.test.tssrc/config.tssrc/coordination-model.test.tssrc/coordination-model.tssrc/inbox-cleanup.test.tssrc/inbox-cleanup.tssrc/multiplexer/coordination.tssrc/multiplexer/index.tssrc/multiplexer/notifications.test.tssrc/multiplexer/notifications.tssrc/multiplexer/persistence-methods.tssrc/multiplexer/runtime-lifecycle-methods.tssrc/multiplexer/session-launch.tssrc/notifications.tssrc/tmux/expose.tssrc/tui/screens/subscreen-renderers.tssrc/workflow.test.tssrc/workflow.ts
…ize repaint Review fixes: - Group sessionless notifications by dedupeKey (falling back to id) so repeated project-level alerts collapse into one inbox item instead of duplicate rows. - Copy the unread array before sorting for latestUnread (avoid mutating a derived array that a future refactor could alias). - Only emit the "handled · unreachable" divider once an actionable row precedes it, so an all-unreachable inbox has no leading divider. - Exposé periodic refresh now repaints on a terminal resize as well as on changed captures (no SIGWINCH handler), so an idle popup reflows on resize. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Coordination information-layer redesign (data/IA only — styling deliberately deferred), plus a follow-on exposé flicker fix.
Coordination (3 phases)
The Coordination screen rendered the raw notification + thread stores as two parallel firehoses, so it cluttered with dead "missing" agents, duplicated every needs-input notice across Inbox and Threads, and ordered by time rather than what needs you. This reconciles the persisted log against live agent state.
8d38352): new puresrc/coordination-model.tsjoins live sessions/teammates/services × notifications × threads into agent-keyed, urgency-sorted items, each carrying reachability (live/offline/missing/none), anactionableflag, and astaleheuristic (live agent whose label moved on from an unread needs-input notice — display-only, never a write). Notification-tagged threads are excluded frombuildThreadEntries, so the Threads section stops mirroring the Inbox.6a39eca):refreshNotificationEntriesdrives the Inbox from the model — actionable rows first, ahandled · unreachabledivider, then the de-emphasized tail; per-row reachability/stale tags. Kept 1:1 with notifications so nav/actions/detail are unchanged.2fd0c93): new top-levelinboxconfig (cleanupEnabled/retentionDays/cleanupIntervalMs/maxSize, default14d/daily/10), puresrc/inbox-cleanup.ts(plan/run), and a scheduler wired exactly like graveyard cleanup. Archives read+aged notifications and trims overflow beyondmaxSize— read-then-oldest, never an unread actionable row.Design invariant (deliberate): notification read/cleared state tracks the user's relationship to a request (seen / handled / aged out), not an agent process-lifecycle event — so a restart/resume never silently marks a request read.
Exposé
70db4d2— fix periodic-refresh flicker: repaint only the opaque tiles on the capture-refresh tick (the static dimmed backdrop + panel frame are painted once), and skip the repaint entirely when nothing changed. The full-screen blank-and-repaint that flickered on terminals ignoring synchronized-output mode 2026 is gone.Test plan
yarn typecheck,yarn lint, fullyarn vitest(1451 passing),yarn build— all clean.🤖 Generated with Claude Code
Summary by CodeRabbit