Skip to content

feat: display background task count in status bar#421

Merged
tiann merged 3 commits intotiann:mainfrom
mrrnb:feat/background-task-indicator
Apr 9, 2026
Merged

feat: display background task count in status bar#421
tiann merged 3 commits intotiann:mainfrom
mrrnb:feat/background-task-indicator

Conversation

@mrrnb
Copy link
Copy Markdown
Contributor

@mrrnb mrrnb commented Apr 9, 2026

When Claude Code runs background tasks (run_in_background), the web UI now shows "N background task(s) running" with a pulsing blue indicator in the status bar instead of just "online".

Detection: scans message content for "Command running in background" (task start) and "" (task completion). Count is tracked in-memory per session and resets on session end.

When Claude Code runs background tasks (run_in_background), the web UI
now shows "N background task(s) running" with a pulsing blue indicator
in the status bar instead of just "online".

Detection: scans message content for "Command running in background"
(task start) and "<task-notification>" (task completion). Count is
tracked in-memory per session and resets on session end.
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Review mode: initial

Findings

  • [Major] Background task detection miscounts when users mention the sentinel text literally. extractBackgroundTaskDelta() stringifies the entire inbound message and counts <task-notification> / Command running in background with ID: anywhere in the payload, but the repo already distinguishes injected task notifications from normal user text (cli/src/api/apiSession.test.ts:81). Because this runs on every message before updating the session cache, a prompt like How do I use the <task-notification> tag? will decrement backgroundTaskCount and can hide still-running tasks. Evidence: hub/src/sync/backgroundTasks.ts:14, hub/src/socket/handlers/cli/sessionHandlers.ts:114, cli/src/api/apiSession.test.ts:81.

Summary

  • Review mode: initial
  • 1 finding. The counter logic needs role/shape-aware parsing instead of whole-message string scanning.

Testing

  • Not run (automation)

HAPI Bot

* - Completed: <task-notification> user messages (sidechain prompts)
*/
export function extractBackgroundTaskDelta(messageContent: unknown): { started: number; completed: number } | null {
const str = stringify(messageContent)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MAJOR] This counts sentinel strings anywhere in the JSON/stringified message, so normal text can change the counter too. The repo already treats How do I use the <task-notification> tag? as an external user message in cli/src/api/apiSession.test.ts:81, but this implementation would still decrement backgroundTaskCount for that prompt.

Suggested fix:

const record = unwrapRoleWrappedRecordEnvelope(messageContent)
if (!record) return null

let started = 0
let completed = 0

if (record.role === 'user' && Array.isArray(record.content)) {
    for (const block of record.content) {
        if (isObject(block)
            && block.type === 'tool_result'
            && typeof block.content === 'string'
            && block.content.includes('Command running in background with ID:')) {
            started += 1
        }
    }
}

if (record.role === 'agent' && isObject(record.content) && record.content.type === 'output') {
    const data = isObject(record.content.data) ? record.content.data : null
    const message = data && isObject(data.message) ? data.message : null
    const text = typeof message?.content === 'string' ? message.content.trimStart() : null

    if (data?.type === 'user' && text?.startsWith('<task-notification>')) {
        completed += 1
    }
}

return started || completed ? { started, completed } : null

Replace naive string scanning with structured message parsing:
- Task starts: only match tool_result blocks in agent-role output
- Task completions: only match user-role messages starting with
  <task-notification> (system-injected), not user text mentioning
  the tag literally

Addresses review feedback on false-positive risk.
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Review mode: follow-up after new commits

Findings

  • [Major] Background-task completions never decrement the counter. The CLI wraps injected Claude records, including <task-notification>, as outer role: 'agent' / content.type: 'output', so extractBackgroundTaskDelta() never enters the new record.role === 'user' completion branch and backgroundTaskCount only grows until session end. Evidence: hub/src/sync/backgroundTasks.ts:17, cli/src/api/apiSession.ts:378, web/src/chat/normalize.test.ts:110.
    Suggested fix:
    const record = unwrapRoleWrappedRecordEnvelope(messageContent)
    if (!record || record.role !== 'agent') return null
    if (!isObject(record.content) || record.content.type !== 'output') return null
    
    const data = isObject(record.content.data) ? record.content.data : null
    const started = countTaskStarts(record.content)
    const completed = data?.type === 'user' ? countTaskCompletions(data) : 0

Summary

  • Review mode: follow-up after new commits
  • 1 finding. The follow-up fixed the literal-text false positive, but the completion parser still does not match the agent-wrapped Claude message shape used in this repo.

Testing

  • Not run (automation; bun unavailable in this runner). Missing coverage for extractBackgroundTaskDelta() on tool-result start and <task-notification> completion envelopes.

HAPI Bot

if (!record) return null

const started = record.role === 'agent' ? countTaskStarts(record.content) : 0
const completed = record.role === 'user' ? countTaskCompletions(record.content) : 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MAJOR] This follow-up still misses every background-task completion. sendClaudeSessionMessage() wraps injected Claude records as outer role: 'agent' + content.type: 'output' (cli/src/api/apiSession.ts:378), and the existing normalization tests cover <task-notification> in that same shape (web/src/chat/normalize.test.ts:110). Because this branch only decrements when the outer role is 'user', the counter can only increase and the status bar stays stuck on "N background task(s) running" until session end.

Suggested fix:

const record = unwrapRoleWrappedRecordEnvelope(messageContent)
if (!record || record.role !== 'agent') return null
if (!isObject(record.content) || record.content.type !== 'output') return null

const data = isObject(record.content.data) ? record.content.data : null
const started = countTaskStarts(record.content)
const completed = data?.type === 'user' ? countTaskCompletions(data) : 0

CLI wraps <task-notification> as { role: 'agent', content: { type: 'output',
data: { type: 'user', message: { content: '<task-notification>...' } } } }.
Check data.type === 'user' inside the agent output envelope to correctly
detect completions and decrement backgroundTaskCount.
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Minor] The new background-task status is hardcoded in English, so it bypasses the existing i18n path and regresses non-English sessions. The other connection-state labels already use translation keys, and the locale tables only define those translated variants today. Evidence web/src/components/AssistantChat/StatusBar.tsx:90, web/src/lib/locales/en.ts:288, web/src/lib/locales/zh-CN.ts:290.
    Suggested fix:
    text: t('misc.backgroundTasksRunning', {
        count: backgroundTaskCount,
        s: backgroundTaskCount > 1 ? 's' : ''
    })

Summary

  • Review mode: follow-up after new commits
  • 1 minor finding. The background-task parser now matches the agent-wrapped Claude shape, but the new status label regresses localization in the web UI.

Testing

  • Not run (review automation). Missing targeted coverage for the new background-task status path and locale keys.

HAPI Bot


if (backgroundTaskCount > 0) {
return {
text: `${backgroundTaskCount} background task${backgroundTaskCount > 1 ? 's' : ''} running`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MINOR] This new status text is hardcoded in English, while the rest of the connection-state labels already go through useTranslation(). In zh-CN, users will now see an English-only status as soon as a background task is running.

Suggested fix:

text: t('misc.backgroundTasksRunning', {
    count: backgroundTaskCount,
    s: backgroundTaskCount > 1 ? 's' : ''
})

Add the matching misc.backgroundTasksRunning entry in web/src/lib/locales/en.ts and web/src/lib/locales/zh-CN.ts.

@mrrnb
Copy link
Copy Markdown
Contributor Author

mrrnb commented Apr 9, 2026

Re: i18n finding — intentionally kept in English to match Claude Code CLI's native status line, which displays "N background task(s) running" regardless of locale. Localizing this would actually diverge from the CLI behavior and break user expectations for those familiar with the terminal experience.

@tiann tiann merged commit 0e1b653 into tiann:main Apr 9, 2026
2 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