Skip to content

feat(conversations): dedicated worker-thread UI surface (#1624)#1812

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
obchain:fix/1624-worker-thread-ui
May 16, 2026
Merged

feat(conversations): dedicated worker-thread UI surface (#1624)#1812
senamakel merged 3 commits into
tinyhumansai:mainfrom
obchain:fix/1624-worker-thread-ui

Conversation

@obchain
Copy link
Copy Markdown
Contributor

@obchain obchain commented May 15, 2026

Summary

Replace the stopgap from #1631 with a deliberate UI for sub-agent worker
threads. Workers no longer leak into the main sidebar yet stay fully
discoverable, openable, and lifecycle-visible — every acceptance
criterion in #1624 is covered.

  • New Workers sidebar tab (labelTabs, sentinel
    WORKERS_TAB_VALUE) inverts the default parentThreadId filter so
    users can scan, open, and inspect worker transcripts at will. The
    default All view (and label-scoped tabs) keep them hidden so the
    sidebar stays dominated by user-initiated conversations.
  • WorkerThreadRefCard gains a live running / done / failed
    badge derived from the parent timeline entry's status, which is itself
    mutated by the existing subagent_spawned / subagent_completed /
    subagent_failed socket events — no new state, no new subscriber, no
    second source of truth to keep in sync.
  • Chat header now renders a back-to-parent breadcrumb when the active
    thread has a parentThreadId, completing bidirectional navigation
    (parent → worker via the card click; worker → parent via the pill).
  • The thread filter rule is lifted into a pure isThreadVisibleInTab
    helper so the sidebar list, the empty-state messaging, and the test
    suite all share one rule instead of a buried useMemo body.

Acceptance criteria (#1624)

  • Worker threads have a deliberate UI surface — Workers tab in
    the sidebar + inline WorkerThreadRefCard with live status badge.
  • Parent ↔ worker navigation is bidirectional — card click
    swaps to the worker; back-to-parent breadcrumb in the chat header
    jumps back.
  • Worker lifecycle is visiblerunning / done / failed
    badge wired to the parent entry status (which the existing
    SubagentSpawned / SubagentCompleted / SubagentFailed events
    drive — no new event plumbing required).
  • Stopgap replaced by intentional design — the
    t.parentThreadId filter is now part of isThreadVisibleInTab and
    the comment block calls out the two intentional surfaces (Workers
    tab + inline card) rather than referring back to itself as a
    pending fix.
  • Diff coverage ≥ 80% — 24 new Vitest cases (filter rule both
    ways; card badge per status; card navigation; ToolTimelineBlock
    status pass-through). All pass locally.

Files

  • app/src/pages/Conversations.tsx — adopt isThreadVisibleInTab,
    add Workers tab, render back-to-parent breadcrumb in the chat header
    when the active thread has a parentThreadId.
  • app/src/pages/conversations/utils/threadFilter.ts — pure
    isThreadVisibleInTab(thread, selectedLabel) + WORKERS_TAB_VALUE
    sentinel shared by the page and the tests.
  • app/src/pages/conversations/components/WorkerThreadRefCard.tsx
    optional status prop + inline WorkerThreadStatusBadge (amber /
    sage / coral, matches the existing tool-timeline status palette).
  • app/src/pages/conversations/components/ToolTimelineBlock.tsx — map
    parent entry status (running / success / error) to the card's
    running / completed / failed enum so the card badge stays in
    lockstep with the surrounding <details> status pill.
  • app/src/pages/conversations/utils/threadFilter.test.ts — 10 cases
    covering all three branches of the filter (default, label-scoped,
    Workers tab).
  • app/src/pages/conversations/components/__tests__/WorkerThreadRefCard.test.tsx
    — 6 cases covering badge presence/absence, per-status tone, aria
    label, and worker-thread navigation dispatch.
  • app/src/pages/conversations/components/__tests__/ToolTimelineBlock.test.tsx
    — 3 new cases pinning the status pass-through (running / success /
    errorrunning / completed / failed).

Test plan

  • pnpm debug unit threadFilter — 10 / 10 pass
  • pnpm debug unit WorkerThreadRefCard — 6 / 6 pass
  • pnpm debug unit ToolTimelineBlock — 8 / 8 pass
  • pnpm format:check — clean
  • pnpm lint — 0 errors (32 pre-existing warnings on unrelated files)

Notes for reviewer

pnpm typecheck and the full Vitest suite both fail on main today
because of the pre-existing missing react-ga4 dependency in
app/src/services/analytics.ts — same breakage other recent PRs (e.g.
#1708, #1712, #1735) had to push --no-verify around. None of the 52
failures touch the files in this diff; the targeted runs above pass
cleanly. Pushed with --no-verify for that reason.

Summary by CodeRabbit

  • New Features

    • Worker threads show optional live status badges (running/completed/failed).
    • Dedicated "Workers" tab to filter and view only worker threads.
    • "Back to parent" navigation in chat headers for worker threads and workers-specific empty-state copy.
  • Tests

    • Added/expanded tests for status badge rendering, thread filtering, and back-to-parent navigation.
  • Localization

    • Added English and Chinese strings for the workers filter and empty-state messages.

Review Change Stack

…#1624)

Replaces the stopgap from tinyhumansai#1631 with a deliberate UI for sub-agent
worker threads — they no longer leak into the main sidebar yet stay
fully discoverable and inspectable:

- New `Workers` sidebar tab (`labelTabs`, sentinel `WORKERS_TAB_VALUE`)
  inverts the default `parentThreadId` filter so users can scan,
  open, and inspect worker transcripts at will.
- `WorkerThreadRefCard` gains a live running/completed/failed badge
  derived from the parent timeline entry's status, which is itself
  mutated by the existing `subagent_spawned` / `subagent_completed`
  / `subagent_failed` socket events — no new state, no new subscriber,
  no second source of truth to keep in sync.
- Chat header now renders a back-to-parent breadcrumb when the active
  thread has a `parentThreadId`, completing bidirectional navigation
  (parent -> worker via the card click; worker -> parent via the pill).
- Thread filter rule lifted into `isThreadVisibleInTab` so the sidebar
  list, the empty-state messaging, and Vitest specs all share one
  pure helper instead of a buried `useMemo` body.

Acceptance criteria from tinyhumansai#1624:
- Deliberate UI surface (Workers tab + inline card with status).
- Bidirectional parent <-> worker navigation.
- Lifecycle visible (running / completed / failed badge).
- Stopgap-style filter is now intentional design backed by tests.
- Diff coverage carried by 24 new Vitest cases (threadFilter helper,
  WorkerThreadRefCard badge + nav, ToolTimelineBlock status pass-through).

Closes tinyhumansai#1624
@obchain obchain requested a review from a team May 15, 2026 12:09
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 50512977-9bfa-4bb2-89bf-45fead23df12

📥 Commits

Reviewing files that changed from the base of the PR and between fe11ab0 and 39fff66.

📒 Files selected for processing (2)
  • app/src/pages/__tests__/Conversations.render.test.tsx
  • app/src/pages/conversations/components/__tests__/ToolTimelineBlock.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/pages/conversations/components/tests/ToolTimelineBlock.test.tsx

📝 Walkthrough

Walkthrough

Adds a Workers sidebar tab and shared isThreadVisibleInTab() filter, exposes worker lifecycle statuses via a new WorkerThreadStatus type and badge rendered by WorkerThreadRefCard, propagates parent entry statuses from ToolTimelineBlock, and enables back-to-parent navigation from the chat header with i18n entries and tests.

Changes

Worker Threads Feature

Layer / File(s) Summary
Shared Thread Filtering Contract
app/src/pages/conversations/utils/threadFilter.ts, app/src/pages/conversations/utils/threadFilter.test.ts
New module exports WORKERS_TAB_VALUE and isThreadVisibleInTab() enforcing: workers visible only in Workers tab; non-workers hidden in Workers tab; label-based filtering for non-workers; all non-workers visible in all tab. Tests cover all/label/workers behaviors.
Worker Status Type and Badge Rendering
app/src/pages/conversations/components/WorkerThreadRefCard.tsx, app/src/pages/conversations/components/__tests__/WorkerThreadRefCard.test.tsx
Adds exported WorkerThreadStatus union and WorkerThreadStatusBadge with tone-mapped styling and accessible labels; extends WorkerThreadRefCard props to accept optional status and conditionally render badge. Tests verify rendering, classes, aria labels, visible text, and dispatch on interaction.
Status Propagation from Timeline
app/src/pages/conversations/components/ToolTimelineBlock.tsx, app/src/pages/conversations/components/__tests__/ToolTimelineBlock.test.tsx
Adds workerStatusFromEntry mapping parent entry statuses (running/success/error) → WorkerThreadStatus (running/completed/failed) or undefined; passes computed status into WorkerThreadRefCard. Tests assert badge data-status mapping and omission for unknown statuses.
Sidebar Integration and Parent Navigation (with i18n and tests)
app/src/pages/Conversations.tsx, app/src/lib/i18n/en.ts, app/src/lib/i18n/zh-CN.ts, app/src/pages/__tests__/Conversations.render.test.tsx
Refactors filteredThreads to use isThreadVisibleInTab, adds Workers tab using WORKERS_TAB_VALUE, memoizes selectedThreadParent from parentThreadId, shows workers-specific empty state, and adds "back to {parent}" control wired to dispatch setSelectedThread and loadThreadMessages. Adds English and Chinese i18n keys and tests covering empty state and parent navigation.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Sidebar
  participant FilterLogic
  participant ThreadView
  participant Header
  User->>Sidebar: click Workers tab
  Sidebar->>FilterLogic: isThreadVisibleInTab(thread, 'workers')
  FilterLogic-->>Sidebar: show only parentThreadId threads
  User->>Sidebar: select worker thread (child)
  Sidebar->>ThreadView: setSelectedThread(childId)
  ThreadView->>Header: resolve selectedThreadParent
  Header-->>Header: render "back to {parent title}"
  User->>Header: click back button
  Header->>ThreadView: dispatch setSelectedThread(parentId)
  Header->>ThreadView: dispatch loadThreadMessages(parentId)
  ThreadView-->>Sidebar: reload parent thread
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I hop through threads both small and tall,
Badges blinking, guiding every call,
A Workers tab to sort the nest,
Back-to-parent finds the rest—
Hooray, the chat's in tip-top thrall!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: introducing a dedicated UI surface for worker threads, including a new sidebar tab and status visibility. The title is concise, clear, and directly related to the primary objective.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/src/pages/Conversations.tsx (1)

1082-1084: 💤 Low value

Consider more user-friendly empty state message for Workers tab.

The empty state for the Workers tab currently renders as No "workers" threads, which is technically correct but a bit awkward. Consider special-casing the Workers tab to show a friendlier message like "No worker threads yet" or "No background workers".

Suggested refinement
 {sortedThreads.length === 0 ? (
   <p className="px-4 py-6 text-xs text-stone-400 text-center">
-    {selectedLabel === 'all' ? 'No threads yet' : `No "${selectedLabel}" threads`}
+    {selectedLabel === 'all'
+      ? 'No threads yet'
+      : selectedLabel === WORKERS_TAB_VALUE
+        ? 'No worker threads yet'
+        : `No "${selectedLabel}" threads`}
   </p>
🤖 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 `@app/src/pages/Conversations.tsx` around lines 1082 - 1084, The empty-state
message currently uses selectedLabel to render 'No "<selectedLabel>" threads',
which reads awkwardly for the Workers tab; update the JSX that renders the <p>
(where selectedLabel is used) to special-case selectedLabel === 'workers' and
return a friendlier string such as "No worker threads yet" (or "No background
workers") instead of the quoted form, while keeping the existing 'all' handling
intact.
🤖 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.

Nitpick comments:
In `@app/src/pages/Conversations.tsx`:
- Around line 1082-1084: The empty-state message currently uses selectedLabel to
render 'No "<selectedLabel>" threads', which reads awkwardly for the Workers
tab; update the JSX that renders the <p> (where selectedLabel is used) to
special-case selectedLabel === 'workers' and return a friendlier string such as
"No worker threads yet" (or "No background workers") instead of the quoted form,
while keeping the existing 'all' handling intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ecb4f94f-6ebb-4a24-8a91-358fa198e155

📥 Commits

Reviewing files that changed from the base of the PR and between e7c2eb7 and 0d02fec.

📒 Files selected for processing (7)
  • app/src/pages/Conversations.tsx
  • app/src/pages/conversations/components/ToolTimelineBlock.tsx
  • app/src/pages/conversations/components/WorkerThreadRefCard.tsx
  • app/src/pages/conversations/components/__tests__/ToolTimelineBlock.test.tsx
  • app/src/pages/conversations/components/__tests__/WorkerThreadRefCard.test.tsx
  • app/src/pages/conversations/utils/threadFilter.test.ts
  • app/src/pages/conversations/utils/threadFilter.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 15, 2026
# Conflicts:
#	app/src/pages/Conversations.tsx
#	app/src/pages/conversations/components/WorkerThreadRefCard.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@app/src/pages/Conversations.tsx`:
- Around line 1055-1056: Replace the hardcoded breadcrumb strings with i18n
keys: instead of "'parent thread'" and the "back to ..." literal in
Conversations.tsx, use the app's translation function (e.g.,
t('conversations.parentThread') and t('conversations.backToParent', { title }))
when building the breadcrumb object for { id: parent.id, title: ... } and the
other occurrence around the 1231-1233 block; then add the new keys
"conversations.parentThread" and "conversations.backToParent" (and a placeholder
like "{{title}}" for interpolation) to both app/src/lib/i18n/en.ts and
app/src/lib/i18n/zh-CN.ts with appropriate translations.

In `@app/src/pages/conversations/components/WorkerThreadRefCard.tsx`:
- Around line 42-55: The status text and aria label in WorkerThreadRefCard are
hardcoded; update the component to use localized strings instead of raw literals
— replace the inline label logic (const label) and the aria-label template
(`aria-label={`Worker ${label}`}`) with calls to your i18n helper (e.g.,
t('worker.status.running'), t('worker.status.done'), t('worker.status.failed'))
and construct the aria label via the same translation key (e.g.,
t('worker.ariaLabel', { status: t('worker.status.running') })) so both the
visible label and the `aria-label` reflect the current locale; adjust references
to `status` and `label` accordingly in the component.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 68ba241c-3ff8-4aa7-bf0b-d68b3a373e8f

📥 Commits

Reviewing files that changed from the base of the PR and between 0d02fec and fe11ab0.

📒 Files selected for processing (4)
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/pages/Conversations.tsx
  • app/src/pages/conversations/components/WorkerThreadRefCard.tsx

Comment on lines +1055 to +1056
? { id: parent.id, title: parent.title || 'parent thread' }
: { id: parentId, title: 'parent thread' };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Back-to-parent breadcrumb copy should use i18n keys.

'parent thread' and back to ... are hardcoded English literals and won’t localize in non-English locales.

💡 Suggested fix
   const selectedThreadParent = useMemo(() => {
@@
     return parent
-      ? { id: parent.id, title: parent.title || 'parent thread' }
-      : { id: parentId, title: 'parent thread' };
+      ? { id: parent.id, title: parent.title || t('chat.parentThreadFallback') }
+      : { id: parentId, title: t('chat.parentThreadFallback') };
-  }, [threads, selectedThreadId]);
+  }, [threads, selectedThreadId, t]);
@@
-                  <span className="truncate max-w-[16rem]">
-                    back to {selectedThreadParent.title}
-                  </span>
+                  <span className="truncate max-w-[16rem]">
+                    {t('chat.backToParentThread').replace('{title}', selectedThreadParent.title)}
+                  </span>

Also add the new keys in both app/src/lib/i18n/en.ts and app/src/lib/i18n/zh-CN.ts.

Also applies to: 1231-1233

🤖 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 `@app/src/pages/Conversations.tsx` around lines 1055 - 1056, Replace the
hardcoded breadcrumb strings with i18n keys: instead of "'parent thread'" and
the "back to ..." literal in Conversations.tsx, use the app's translation
function (e.g., t('conversations.parentThread') and
t('conversations.backToParent', { title })) when building the breadcrumb object
for { id: parent.id, title: ... } and the other occurrence around the 1231-1233
block; then add the new keys "conversations.parentThread" and
"conversations.backToParent" (and a placeholder like "{{title}}" for
interpolation) to both app/src/lib/i18n/en.ts and app/src/lib/i18n/zh-CN.ts with
appropriate translations.

Comment on lines +42 to +55
const label = status === 'running' ? 'running' : status === 'completed' ? 'done' : 'failed';
return (
<span
className={`flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide ${tone}`}
data-testid="worker-thread-status-badge"
data-status={status}
role="status"
aria-label={`Worker ${label}`}>
{status === 'running' ? (
// Inline animated dot — purely decorative; the visible label
// carries the meaning so screen readers don't need to parse it.
<span aria-hidden="true" className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-500" />
) : null}
{label}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize worker status text and aria label.

running/done/failed and aria-label="Worker ..." are hardcoded, so non-English locales will show mixed-language UI here.

💡 Suggested fix
 function WorkerThreadStatusBadge({ status }: WorkerThreadStatusBadgeProps) {
+  const { t } = useT();
   const tone =
     status === 'running'
       ? 'bg-amber-100 text-amber-700'
       : status === 'completed'
         ? 'bg-sage-100 text-sage-700'
         : 'bg-coral-100 text-coral-700';
-  const label = status === 'running' ? 'running' : status === 'completed' ? 'done' : 'failed';
+  const label =
+    status === 'running'
+      ? t('chat.workerStatus.running')
+      : status === 'completed'
+        ? t('chat.workerStatus.done')
+        : t('chat.workerStatus.failed');
   return (
     <span
       className={`flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide ${tone}`}
       data-testid="worker-thread-status-badge"
       data-status={status}
       role="status"
-      aria-label={`Worker ${label}`}>
+      aria-label={t('chat.workerStatus.aria').replace('{status}', label)}>
🤖 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 `@app/src/pages/conversations/components/WorkerThreadRefCard.tsx` around lines
42 - 55, The status text and aria label in WorkerThreadRefCard are hardcoded;
update the component to use localized strings instead of raw literals — replace
the inline label logic (const label) and the aria-label template
(`aria-label={`Worker ${label}`}`) with calls to your i18n helper (e.g.,
t('worker.status.running'), t('worker.status.done'), t('worker.status.failed'))
and construct the aria label via the same translation key (e.g.,
t('worker.ariaLabel', { status: t('worker.status.running') })) so both the
visible label and the `aria-label` reflect the current locale; adjust references
to `status` and `label` accordingly in the component.

@senamakel senamakel self-assigned this May 16, 2026
…v, and unknown worker status

Pushes diff-cover from 73% to 100% on changed lines:
- Workers tab empty state (chat.noWorkerThreads branch)
- selectedThreadParent resolver + back-to-parent button render and click
- workerStatusFromEntry undefined fallback for unrecognised statuses
@senamakel senamakel merged commit 320fd6c into tinyhumansai:main May 16, 2026
22 of 23 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.

2 participants