Skip to content

(MOT-4357) feat(queue): queues and dead-letters page as injectable console UI - #738

Open
rohitg00 wants to merge 3 commits into
mainfrom
feat/queue-console-ui
Open

(MOT-4357) feat(queue): queues and dead-letters page as injectable console UI#738
rohitg00 wants to merge 3 commits into
mainfrom
feat/queue-console-ui

Conversation

@rohitg00

@rohitg00 rohitg00 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

Screenshot 2026-08-07 at 15 40 58

The queue worker ships its own console page at #/ext/queues through the shared iii-console-ui crate: topics, delivery policies, subscribers, live movement, publish, and the dead-letter queue with redrive/discard. Every function the page drives already existed on the worker — this is the missing surface, not new plumbing.

The page

List — a stats table, not a name list: depth, delivered, failed, dead, subscribers per topic with alert coloring; fifo topics carry their ordering key inline (fifo · session_id). The page auto-selects on open — dead-lettered topic first, else the busiest — so it never opens empty.

Overview — what a topic IS:

  • delivery policy from the worker's queue_configs: standard vs fifo with its message-group field (harness-turn is fifo per session_id — the agent turn queue, now visible), concurrency, retry budget and backoff, timeout, redeliver-on-engine-restart (drop renders as a warning)
  • live subscribers from durable:subscriber registrations, each with its own retry config and condition

Activity — movement from the trace store: publishes onto the topic (attributed via the recorded payload) and deliveries into each subscriber, with duration and age.

Publish — behind a confirm; it goes through the real queue, so every subscriber receives it and retry/DLQ rules apply.

Dead letters — grouped by error first (forty identical failures are one fact), then paged instances with payload, retries, failure time, size; per-message redrive and discard, redrive-all behind a confirm.

Honest gating — redis keeps no DLQ and the tab says so instead of rendering an empty table; a builtin store that is not file-backed shows a volatile store header warning, because queued jobs die with a worker restart.

Live, no polling

A stream subscription on iii:devtools:all-spans reloads whenever a queue function or a subscriber of the selected topic executes anywhere on the bus — a publish from chat, a redrive from the CLI, a consumer failing — debounced; a refresh control covers the rest.

Verification

End to end against a running engine: a deliberately failing durable:subscriber dead-lettered real publishes with max_retries honored; redrive sent a message back through the pipeline where the consumer failed it again (failed count ticked up — the real path, not a UI trick); discard removed it with engine::queue::dlq_topics agreeing; publish from the page moved the dead-letter badge live off the traffic tick. Both themes. cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test (4 UI asset tests pin the page id and driven function ids), biome, tsc + esbuild green.

Wire notes for review: engine::queue::list_topics and dlq_topics return bare arrays, not envelopes; the write functions live under iii::queue::* / iii::durable::*.

Linear: MOT-4357.

Summary by CodeRabbit

  • New Features
    • Added a queue console UI for browsing topics, policies, subscribers, statistics, and recent activity.
    • Added topic filtering, detail views, JSON message publishing, and live refresh support.
    • Added dead-letter queue management, including message inspection, redrive, discard, pagination, and bulk actions.
    • Integrated the queue console into worker startup with responsive and dark-theme styling.

…nsole UI

The queue worker ships its own console page at #/ext/queues, through the
shared iii-console-ui crate — queues are queue-worker data, so the page
appears when the worker connects and leaves with it. Every function the
page drives already existed (engine::queue::* reads, iii::durable::publish,
iii::queue::redrive / redrive_message / discard_message); this is the
missing surface, not new plumbing.

The list is a stats table, not a name list: depth, delivered, failed, dead
and subscriber counts per topic with alert coloring, fifo topics carrying
their ordering key inline. The page auto-selects on open — the topic with
dead letters first, else the busiest — so it never opens empty.

Topic detail leads with what a topic IS:

- the delivery policy from the worker's queue_configs: standard vs fifo
  with its message-group field (harness-turn is fifo per session_id — the
  agent turn queue, finally visible), concurrency, retry budget and
  backoff, timeout, and redeliver-on-engine-restart, with drop rendered as
  a warning
- live subscribers from durable:subscriber registrations, each with its
  own retry config and condition
- an activity tab reading the trace store: publishes onto the topic
  (attributed via the recorded payload) and deliveries into each
  subscriber, with duration and age
- publish, behind a confirm — it goes through the real queue, so
  subscribers receive it and retry/DLQ rules apply
- dead letters: grouped by error first (forty identical failures are one
  fact), then paged instances with payload, retries, failure time and
  size, per-message redrive and discard, redrive-all behind a confirm
- adapter gating: redis keeps no DLQ and the tab says so; a builtin store
  that is not file-backed shows a volatile-store warning in the header,
  because queued jobs die with a worker restart

Live without polling: a stream subscription on the all-spans feed reloads
whenever a queue function or a subscriber of the selected topic executes
anywhere on the bus, debounced; a refresh control covers the rest.

Verified against a running engine end to end: a deliberately failing
subscriber dead-lettered real publishes with max_retries honored; redrive
sent a message back through the pipeline where it failed again honestly;
discard removed it with the engine's dlq_topics agreeing; publish from the
page updated the dead-letter count live off the traffic tick.
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 7, 2026 2:06pm
workers-tech-spec Ready Ready Preview Aug 7, 2026 2:06pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The queue worker now includes a console UI for topic inspection, publishing, activity monitoring, and dead-letter message management. The UI is bundled by esbuild, embedded into the Rust worker, and registered during worker startup.

Changes

Queue console UI

Layer / File(s) Summary
Queue data and event API
queue/ui/src/page/api.ts
Adds typed APIs for queue topics, statistics, subscribers, configuration, activity, publishing, and DLQ operations.
Queue page workflows
queue/ui/page.tsx, queue/ui/src/page/index.tsx, queue/ui/styles.css
Adds queue browsing, topic details, policy and subscriber views, activity monitoring, JSON publishing, and DLQ redrive or discard controls with scoped styling.
Build, embedding, and worker registration
pnpm-workspace.yaml, queue/ui/*, queue/build.rs, queue/Cargo.toml, queue/src/*
Adds the UI package and build configuration, rebuild checks, Rust asset embedding, the iii-console-ui dependency, and boot-time registration.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • iii-hq/workers#580 — Uses the same worker-specific injectable console UI and boot-time registration pattern.
  • iii-hq/workers#601 — Uses related frontend bundle discovery and build.rs asset integration.
  • iii-hq/workers#610 — Adds another worker-specific console UI with matching workspace and Rust integration.

Suggested reviewers: andersonleal

Sequence Diagram(s)

sequenceDiagram
  participant ConsoleHost
  participant QueuesPage
  participant QueueApi
  participant QueueWorker
  ConsoleHost->>QueuesPage: register queues page
  QueuesPage->>QueueApi: load queue data
  QueueApi->>QueueWorker: invoke queue functions and trace APIs
  QueueWorker-->>QueueApi: return queue data and activity
  QueueApi-->>QueuesPage: provide normalized models
  QueuesPage->>QueueApi: publish or manage DLQ messages
  QueueApi->>QueueWorker: execute queue operation
  QueueWorker-->>QueuesPage: return operation result
Loading

Poem

A rabbit hops through queues so wide,
With topics lined from side to side.
DLQ crumbs redrive, messages flow,
Fresh bundles help the console glow.
The worker wakes, the UI takes flight—
Queue trails sparkle in the night. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the addition of an injectable queue console UI with queues and dead-letter support.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/queue-console-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 56 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

Seven findings from review, each verified against the code first.

Activity feed: delivery traces were fetched with no topic filter, so a
subscriber bound to several topics showed every topic's deliveries in
each one's feed. Delivery inputs are the bare message data — the
envelope is gone — so the filter is lenient: a span is kept unless its
payload carries a `topic` field naming a DIFFERENT topic (publishes
keep their exact envelope filter). The per-subscriber trace reads now
run through the same 4-wide pool statsForAll uses instead of all at
once, and one unreadable subscriber no longer blanks the feed.

Live reload: the traffic handler JSON.stringify'd every stream frame to
substring-match it, and its trailing debounce reset on every matching
frame — continuous traffic postponed the reload forever, exactly when
the page matters most. Frames now have their span names read directly
off the envelope, and the debounce carries a 2.5s ceiling.

Selection: the load fallback re-selected the first topic on EVERY
reload, overriding a deliberately cleared selection on the next traffic
tick. The dead-letters-else-first pick now happens once, on the first
populated load.

Smaller: activity rows key on the index too, so same-instant events
from one function render distinctly; subtree_older_than checks the
directory's own mtime, catching deletions the per-entry walk missed;
the build script declares rerun-if-env-changed for SKIP_UI_BUILD and
PNPM alongside the file directives.
…nnected consumers

On engine 0.22.1 `list_topics.subscriber_count` (and topic_stats
`consumer_count`) read 0 for idle durable subscribers, so every topic
showed subs 0 while the detail panel — which counts durable:subscriber
REGISTRATIONS — showed the same topic with live subscribers.
Registrations are what an operator means by "does anything consume this
topic": the table now overlays a per-topic registration count (one
registered-triggers read per load, shared with the detail panel's
fetch) and shows whichever of the two figures is higher, so a future
engine that reports consumers again is not undercut.

The other zeros after a restart are honest: the builtin store was
running in memory, and the VOLATILE STORE badge exists for exactly that
conversation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (10)
queue/ui/styles.css (1)

125-132: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

These selectors match nothing.

queue/ui/src/page/index.tsx renders no .meta or .dead element inside .queue-ui-row. The row contains .c-name, .name, .fifo, and .c-n only. .meta appears under .queue-ui-dead-head, which Line 241 already styles. Remove both rules.

🤖 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 `@queue/ui/styles.css` around lines 125 - 132, Remove the unused .meta and
.dead rules from the queue row styles in the stylesheet;
queue/ui/src/page/index.tsx renders neither class within .queue-ui-row, and
.meta is already styled by the existing queue-ui-dead-head rule.
queue/ui/src/page/index.tsx (7)

444-447: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

defaultValue applies only on the first mount.

TopicDetail has no key at the call site (Line 342), so it does not remount when the operator selects another topic. Tabs keeps the tab that was already open and ignores the new deadCount > 0 ? 'dead' : 'overview' value. A topic with dead letters therefore does not open on the dead-letter tab after the first selection. If the intent is to re-apply the rule per topic, add key={topic} to TopicDetail.

🤖 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 `@queue/ui/src/page/index.tsx` around lines 444 - 447, Add key={topic} to the
TopicDetail component usage so it remounts when the selected topic changes,
allowing Tabs defaultValue to reapply the deadCount-based initial tab for each
topic.

870-872: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Disable the redrive all trigger while an action runs.

Every other action button checks busyId !== null. This button does not. The operator can open the confirm step while a per-message redrive is in flight.

♻️ Proposed change
-          <Button size="sm" onClick={() => setConfirmingAll(true)}>
+          <Button
+            size="sm"
+            disabled={busyId !== null}
+            onClick={() => setConfirmingAll(true)}
+          >
             redrive all
           </Button>
🤖 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 `@queue/ui/src/page/index.tsx` around lines 870 - 872, Update the “redrive all”
Button near setConfirmingAll to be disabled whenever busyId is not null,
matching the other action buttons and preventing the confirmation flow from
opening during an active per-message redrive.

619-633: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Subscriber array identity churn re-runs the activity read on every detail reload.

load depends on subscribers. TopicDetail calls setSubscribers(subs) on every loadDetail run, and subscribersFor always builds a new array. The identity therefore changes even when the subscriber set is unchanged. Each change recreates load and re-fires useEffect(load), which issues one engine::traces::list call per subscriber plus the publish read. loadDetail itself runs on every debounced traffic burst, so activity reads multiply.

Depend on a content-derived key instead, or skip the state update in TopicDetail when the subscriber list is unchanged.

♻️ Proposed change
+  const subsKey = useMemo(
+    () => subscribers.map((s) => s.functionId).join('\u0000'),
+    [subscribers],
+  )
+  const subsRef = useRef(subscribers)
+  subsRef.current = subscribers
   const load = useCallback(() => {
-    recentActivity(host, topic, subscribers).then(
+    recentActivity(host, topic, subsRef.current).then(
       (rows) => {
         setEvents(rows)
         setError(null)
       },
       (err: unknown) => setError(errorMessage(err)),
     )
-  }, [host, topic, subscribers])
+  }, [host, topic, subsKey])
🤖 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 `@queue/ui/src/page/index.tsx` around lines 619 - 633, Prevent subscriber array
identity churn from recreating the activity loader: update the load callback and
related memoization in TopicDetail to depend on a stable content-derived
subscriber key, or avoid calling setSubscribers when the list is unchanged.
Preserve subscriber-driven watched IDs while ensuring repeated loadDetail runs
do not re-fire recentActivity unnecessarily.

191-219: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Concurrent load calls can commit stale results.

load runs on mount, on the refresh button, and on every debounced traffic burst. Nothing cancels an in-flight run. If an earlier run resolves after a later run, it overwrites topics, dlq, setup, and statsByTopic with older data. The await statsForAll(...) on Line 213 widens the window, because that step issues one request per topic before it commits.

Add a sequence guard.

♻️ Proposed change
   const pickedRef = useRef(false)
+  const loadSeq = useRef(0)
 
   const load = useCallback(() => {
+    const seq = ++loadSeq.current
     Promise.all([
       listTopics(host),
       dlqTopics(host),
       queueSetup(host),
       subscriberCounts(host).catch(() => new Map<string, number>()),
     ]).then(
       async ([topicRows, dlqRows, setupValue, counts]) => {
+        if (seq !== loadSeq.current) return
         setTopics(topicRows)

Apply the same check again after the statsForAll await, before setStatsByTopic.

🤖 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 `@queue/ui/src/page/index.tsx` around lines 191 - 219, Update the load callback
to use a monotonically increasing request sequence and capture the current
sequence for each invocation. Before committing topics, DLQ, setup, counts, or
error results, ignore the result when its sequence is no longer current; apply
the same guard after the await statsForAll call and before setStatsByTopic so
stale statistics cannot overwrite newer data.

566-567: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

functionId may not be unique per topic.

subscribersFor returns one entry per registration. If the same function registers twice for the topic, two entries share sub.functionId and React reports duplicate keys. Include the index or the worker name in the key.

♻️ Proposed change
-        subscribers.map((sub) => (
-          <div key={sub.functionId} className="queue-ui-subscriber">
+        subscribers.map((sub, i) => (
+          <div key={`${sub.functionId}-${sub.worker}-${i}`} className="queue-ui-subscriber">
🤖 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 `@queue/ui/src/page/index.tsx` around lines 566 - 567, Update the subscriber
list rendering in the subscribers.map callback to use a key that is unique for
each registration, rather than relying solely on sub.functionId. Include the map
index or another available unique worker/registration identifier while
preserving the existing subscriber content and rendering behavior.

852-860: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report the redrive-all count.

redriveAll returns the number of messages that went back onto the main queue, but act discards it and shows a fixed label. Show the count so the operator can confirm the effect.

♻️ Proposed change
               onClick={() =>
-                act('redrove all messages', () => redriveAll(host, topic), null)
+                act(
+                  (n) => `redrove ${n} messages`,
+                  () => redriveAll(host, topic),
+                  null,
+                )
               }

Change act to accept a label builder that receives the resolved value.

🤖 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 `@queue/ui/src/page/index.tsx` around lines 852 - 860, Update the redrive-all
action around redriveAll and act so the resolved count is preserved and
displayed in the button or action feedback instead of using a fixed label.
Change act to accept a label builder receiving redriveAll’s returned count, and
format the message to report how many messages were returned to the main queue.

107-163: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Four components subscribe to the same span stream independently.

QueuesPage (Line 219), TopicDetail (Line 402), ActivityPanel (Line 633), and DlqPanel (Line 786) each call useQueueTraffic. Each call registers its own iii:devtools:all-spans stream trigger and runs its own debounce timer. A single burst of queue traffic therefore triggers four independent reload chains, and the QueuesPage chain alone issues listTopics, dlqTopics, queueSetup, subscriberCounts, plus one topic_stats call per topic.

Consider one shared subscription at the page level that fans out to registered listeners through a context.

🤖 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 `@queue/ui/src/page/index.tsx` around lines 107 - 163, Refactor useQueueTraffic
into a page-level shared subscription that registers one iii:devtools:all-spans
trigger and one debounced reload chain, then expose listener registration
through context for QueuesPage, TopicDetail, ActivityPanel, and DlqPanel. Update
each component to consume the shared context instead of independently invoking
useQueueTraffic, preserving their existing traffic filtering and reload
behavior.
queue/ui/src/page/api.ts (2)

380-400: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Make lenient matching order independent.

In lenient mode matches can flip back to false after an exact hit. If a span carries more than one iii.invocation.input event, or more than one iii.payload.json attribute, and a later payload names a different topic, the span is dropped even though an earlier payload matched topicFilter. Stop the scan once an exact match is found.

♻️ Proposed change
       const eventsAttr = Array.isArray(span.events) ? span.events : []
       let matches = lenient
+      let exact = false
       for (const entry of eventsAttr) {
         if (!isRecord(entry) || entry.name !== 'iii.invocation.input') continue
         for (const attr of Array.isArray(entry.attributes)
           ? entry.attributes
           : []) {
           if (!Array.isArray(attr) || attr[0] !== 'iii.payload.json') continue
           try {
             const payload = JSON.parse(String(attr[1]))
             if (!isRecord(payload)) continue
             if (payload.topic === topicFilter) {
               matches = true
+              exact = true
             } else if (lenient && typeof payload.topic === 'string') {
               matches = false
             }
           } catch {
             // Unparseable payload: not a match.
           }
+          if (exact) break
         }
+        if (exact) break
       }
🤖 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 `@queue/ui/src/page/api.ts` around lines 380 - 400, Update the matching loops
around the `matches` variable so an exact `payload.topic === topicFilter` match
is terminal: stop scanning further attributes and events once found, and prevent
later non-matching payloads from resetting `matches` to false. Preserve lenient
fallback behavior when no exact match exists.

224-226: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider surfacing a failed configuration read.

The catch treats every failure as "worker absent". A transport error then leaves adapter: 'builtin' and no storeMethod, which makes index.tsx render the "volatile store" warning badge for an adapter that was never read. Returning an optional unknown: true flag would let the page suppress the badge when the configuration is unknown.

🤖 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 `@queue/ui/src/page/api.ts` around lines 224 - 226, Update the
configuration-read catch handling in the surrounding API function to distinguish
an absent configuration worker from a failed read, returning an optional
unknown: true flag for transport or other read errors while preserving builtin
defaults. Update the consuming logic in index.tsx to suppress the volatile-store
warning badge whenever the configuration is marked unknown.
🤖 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 `@queue/ui/src/page/api.ts`:
- Around line 429-436: Update the publishRead request to pass the topic filter
directly to engine::traces::list using the activity-read filter supported by the
engine, while retaining the existing name, limit, and include_internal options.
Keep spanEvents focused on transforming the already-filtered results rather than
relying on client-side topic filtering.

In `@queue/ui/src/page/index.tsx`:
- Around line 444-447: In queue/ui/src/page/index.tsx, add key={selected} to the
TopicDetail rendered around lines 342 so the detail subtree remounts whenever
the topic changes; this root fix covers both affected sites: the Tabs
defaultValue at lines 444-447 and DlqPanel state around lines 776-786 require no
direct changes.
- Around line 300-310: Update the topic row button in the selection handler
around selected and topic.name to include aria-pressed={selected ===
topic.name}, preserving the existing data-selected styling and toggle behavior
so assistive technology can detect the active topic.

In `@queue/ui/styles.css`:
- Around line 73-89: Add explicit :focus-visible styles for the queue row button
selector .queue-ui-row and the dead-head selector .queue-ui-dead-head, using a
clearly visible focus indicator while preserving their existing layout and hover
styling.

---

Nitpick comments:
In `@queue/ui/src/page/api.ts`:
- Around line 380-400: Update the matching loops around the `matches` variable
so an exact `payload.topic === topicFilter` match is terminal: stop scanning
further attributes and events once found, and prevent later non-matching
payloads from resetting `matches` to false. Preserve lenient fallback behavior
when no exact match exists.
- Around line 224-226: Update the configuration-read catch handling in the
surrounding API function to distinguish an absent configuration worker from a
failed read, returning an optional unknown: true flag for transport or other
read errors while preserving builtin defaults. Update the consuming logic in
index.tsx to suppress the volatile-store warning badge whenever the
configuration is marked unknown.

In `@queue/ui/src/page/index.tsx`:
- Around line 444-447: Add key={topic} to the TopicDetail component usage so it
remounts when the selected topic changes, allowing Tabs defaultValue to reapply
the deadCount-based initial tab for each topic.
- Around line 870-872: Update the “redrive all” Button near setConfirmingAll to
be disabled whenever busyId is not null, matching the other action buttons and
preventing the confirmation flow from opening during an active per-message
redrive.
- Around line 619-633: Prevent subscriber array identity churn from recreating
the activity loader: update the load callback and related memoization in
TopicDetail to depend on a stable content-derived subscriber key, or avoid
calling setSubscribers when the list is unchanged. Preserve subscriber-driven
watched IDs while ensuring repeated loadDetail runs do not re-fire
recentActivity unnecessarily.
- Around line 191-219: Update the load callback to use a monotonically
increasing request sequence and capture the current sequence for each
invocation. Before committing topics, DLQ, setup, counts, or error results,
ignore the result when its sequence is no longer current; apply the same guard
after the await statsForAll call and before setStatsByTopic so stale statistics
cannot overwrite newer data.
- Around line 566-567: Update the subscriber list rendering in the
subscribers.map callback to use a key that is unique for each registration,
rather than relying solely on sub.functionId. Include the map index or another
available unique worker/registration identifier while preserving the existing
subscriber content and rendering behavior.
- Around line 852-860: Update the redrive-all action around redriveAll and act
so the resolved count is preserved and displayed in the button or action
feedback instead of using a fixed label. Change act to accept a label builder
receiving redriveAll’s returned count, and format the message to report how many
messages were returned to the main queue.
- Around line 107-163: Refactor useQueueTraffic into a page-level shared
subscription that registers one iii:devtools:all-spans trigger and one debounced
reload chain, then expose listener registration through context for QueuesPage,
TopicDetail, ActivityPanel, and DlqPanel. Update each component to consume the
shared context instead of independently invoking useQueueTraffic, preserving
their existing traffic filtering and reload behavior.

In `@queue/ui/styles.css`:
- Around line 125-132: Remove the unused .meta and .dead rules from the queue
row styles in the stylesheet; queue/ui/src/page/index.tsx renders neither class
within .queue-ui-row, and .meta is already styled by the existing
queue-ui-dead-head rule.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 87718ad0-6466-45ff-8306-d04132a47ce1

📥 Commits

Reviewing files that changed from the base of the PR and between abfadf4 and 89e53e2.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • queue/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • pnpm-workspace.yaml
  • queue/Cargo.toml
  • queue/build.rs
  • queue/src/boot.rs
  • queue/src/lib.rs
  • queue/src/ui.rs
  • queue/ui/build.mjs
  • queue/ui/package.json
  • queue/ui/page.tsx
  • queue/ui/src/page/api.ts
  • queue/ui/src/page/index.tsx
  • queue/ui/styles.css
  • queue/ui/tsconfig.json

Comment thread queue/ui/src/page/api.ts
Comment on lines +429 to +436
const publishRead = host.iii
.trigger('engine::traces::list', {
name: 'execute iii::durable::publish',
limit: 60,
include_internal: true,
})
.then((out) => spanEvents(out, 'publish', topic))
.catch((): QueueEvent[] => [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check which filter parameters engine::traces::list accepts.
rg -n -C 10 'traces::list' --iglob '*.rs'

Repository: iii-hq/workers

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching api.ts:"
fd -a 'api\.ts$' . || true

echo
echo "Git tree files under queue/ui/src/page:"
git ls-files queue/ui/src/page 2>/dev/null || true

echo
echo "Search trace API identifiers across tracked files:"
rg -n -C 6 'engine::traces::list|traces::list|spanEvents|ii::traces|traces' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true

echo
echo "Find host.iii definitions/usages:"
rg -n -C 4 '\.iii|Host|host|IHost|h\.iii|host\.iii' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true

Repository: iii-hq/workers

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Exact tracked references to traces::list (case sensitive):"
rg -n 'traces::list' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true

echo
echo "Exact tracked references to engine::traces::list (case sensitive):"
rg -n 'engine::traces::list' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true

echo
echo "Exact tracked references to spanEvents:"
rg -n 'spanEvents' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . || true

echo
echo "Files matching api.ts:"
fd -a 'api\.ts$' . | sed -n '1,20p'

echo
echo "Search only by file paths matching api.ts and inspect relevant files if any:"
while IFS= read -r file; do
  echo "--- $file ---"
  wc -l "$file"
  rg -n -C 8 'traces::list|spanEvents|limit: 60|limit: 40|topic' "$file" || true
done < <(fd 'api\.ts$' .)

Repository: iii-hq/workers

Length of output: 4457


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "queue/ui/src/page/api.ts: spanEvents and recentActivity sections:"
sed -n '340,470p' queue/ui/src/page/api.ts

echo
echo "queue/ui/src/page/index.tsx traces read/readError sections:"
sed -n '580,650p' queue/ui/src/page/index.tsx

echo
echo "TracesV2 harness canned engine::traces::list support:"
sed -n '45,85p' console/web/src/pages/TracesV2/stories/harness.tsx

echo
echo "TracesV2 api filters:"
sed -n '120,150p' console/web/src/pages/TracesV2/api/traces.ts

Repository: iii-hq/workers

Length of output: 8637


Move the topic filter into the traces request.

engine::traces::list returns the newest spans for the given name; this call does not filter spans by topic, then spanEvents(out, 'publish', topic) filters client-side. A busy engine can return spans from other topics before this one, so low-traffic topics can show no publishes. Add a topic filter if the engine supports it for this activity read.

🤖 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 `@queue/ui/src/page/api.ts` around lines 429 - 436, Update the publishRead
request to pass the topic filter directly to engine::traces::list using the
activity-read filter supported by the engine, while retaining the existing name,
limit, and include_internal options. Keep spanEvents focused on transforming the
already-filtered results rather than relying on client-side topic filtering.

Comment on lines +300 to +310
<button
key={topic.name}
type="button"
className="queue-ui-row"
data-selected={selected === topic.name}
onClick={() =>
setSelected((prev) =>
prev === topic.name ? null : topic.name,
)
}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Expose the row selection state to assistive technology.

The row button conveys selection only through data-selected, which styles the row but is not announced. Add aria-pressed={selected === topic.name} so screen reader users can tell which topic is open.

♻️ Proposed change
                   type="button"
                   className="queue-ui-row"
                   data-selected={selected === topic.name}
+                  aria-pressed={selected === topic.name}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
key={topic.name}
type="button"
className="queue-ui-row"
data-selected={selected === topic.name}
onClick={() =>
setSelected((prev) =>
prev === topic.name ? null : topic.name,
)
}
>
<button
key={topic.name}
type="button"
className="queue-ui-row"
data-selected={selected === topic.name}
aria-pressed={selected === topic.name}
onClick={() =>
setSelected((prev) =>
prev === topic.name ? null : topic.name,
)
}
>
🤖 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 `@queue/ui/src/page/index.tsx` around lines 300 - 310, Update the topic row
button in the selection handler around selected and topic.name to include
aria-pressed={selected === topic.name}, preserving the existing data-selected
styling and toggle behavior so assistive technology can detect the active topic.

Comment on lines +444 to +447
<Tabs
defaultValue={deadCount > 0 ? 'dead' : 'overview'}
className="queue-ui-tabs"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Per-topic child state persists across topic switches in queue/ui/src/page/index.tsx. QueuesPage renders TopicDetail at Line 342 without a key. React reuses the same component instances when selected changes, so uncontrolled child state carries over from the previous topic. Adding key={selected} to TopicDetail resets the whole detail subtree per topic and fixes both sites.

  • queue/ui/src/page/index.tsx#L444-L447: the Tabs defaultValue rule deadCount > 0 ? 'dead' : 'overview' never re-applies, so a topic with dead letters does not open on the dead-letter tab after the first selection.
  • queue/ui/src/page/index.tsx#L776-L786: DlqPanel keeps its previous page, so a topic with fewer dead letters renders an empty panel with only pagination controls, because the empty-state branch on Line 827 requires page === 0.
📍 Affects 1 file
  • queue/ui/src/page/index.tsx#L444-L447 (this comment)
  • queue/ui/src/page/index.tsx#L776-L786
🤖 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 `@queue/ui/src/page/index.tsx` around lines 444 - 447, In
queue/ui/src/page/index.tsx, add key={selected} to the TopicDetail rendered
around lines 342 so the detail subtree remounts whenever the topic changes; this
root fix covers both affected sites: the Tabs defaultValue at lines 444-447 and
DlqPanel state around lines 776-786 require no direct changes.

Comment thread queue/ui/styles.css
Comment on lines +73 to +89
[data-iii-ui="queue"] .queue-ui-thead,
[data-iii-ui="queue"] .queue-ui-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 64px 76px 64px 56px 52px;
align-items: baseline;
gap: 8px;
width: 100%;
padding: 8px 10px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--color-ink);
font: inherit;
font-size: 13px;
text-align: left;
cursor: pointer;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a visible focus style for the row button.

.queue-ui-row is a <button> with border: 0 and a transparent background. Only :hover is styled. Keyboard users get whatever the console default outline provides, and the same applies to .queue-ui-dead-head at Line 216. Add an explicit :focus-visible rule so keyboard navigation stays visible.

♻️ Proposed change
 [data-iii-ui="queue"] .queue-ui-row:hover {
   background: var(--color-surface-hover, rgba(0, 0, 0, 0.08));
 }
+[data-iii-ui="queue"] .queue-ui-row:focus-visible,
+[data-iii-ui="queue"] .queue-ui-dead-head:focus-visible {
+  outline: 2px solid var(--color-rule-focus, var(--color-accent));
+  outline-offset: -2px;
+}
🤖 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 `@queue/ui/styles.css` around lines 73 - 89, Add explicit :focus-visible styles
for the queue row button selector .queue-ui-row and the dead-head selector
.queue-ui-dead-head, using a clearly visible focus indicator while preserving
their existing layout and hover styling.

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