Skip to content

fix(tui): progress percentage stuck at 0% — agentId mismatch between TOOL_INVOKED and primary agent #503

@JeremyDev87

Description

@JeremyDev87

Bug Description

The FocusedAgent panel progress bar never advances from 0%. The [0%] display and empty progress bar remain static throughout the entire session regardless of tool activity.

Root Cause

AgentId mismatch between the primary agent registration and TOOL_INVOKED events.

How agents are registered

In response-event-extractor.ts, extractFromParseMode() creates the primary agent:

// agentId = "primary:solution-architect"
events.push({
  event: TUI_EVENTS.AGENT_ACTIVATED,
  payload: {
    agentId: `primary:${delegateName}`,  // ← e.g. "primary:solution-architect"
    name: delegateName,
    role: 'primary',
    isPrimary: true,
  },
});

This agent is stored in state.agents Map with key "primary:solution-architect".

How TOOL_INVOKED fires

In tui-interceptor.ts, every tool call emits TOOL_INVOKED with agentId from parseAgentFromToolName():

// parse-agent.ts returns agentId like "search_rules", "context", "plan-mode"
this.eventBus.emit(TUI_EVENTS.TOOL_INVOKED, {
  toolName,
  agentId: agentInfo?.agentId ?? null,  // ← e.g. "search_rules", "context", null
  timestamp: Date.now(),
});

How progress should increment

In use-dashboard-state.ts, the TOOL_INVOKED handler:

case 'TOOL_INVOKED': {
  const invokedAgentId = action.payload.agentId;
  if (invokedAgentId) {
    const agent = state.agents.get(invokedAgentId);  // ← LOOKUP FAILS
    // "search_rules" !== "primary:solution-architect"
    if (agent && agent.status === 'running') {
      agents.set(invokedAgentId, {
        ...agent,
        progress: Math.min(95, agent.progress + 5),  // ← NEVER REACHED
      });
    }
  }
}

The lookup state.agents.get("search_rules") always returns undefined because the primary agent is stored under key "primary:solution-architect". Progress never increments.

Proposed Fix

Add a fallback to focusedAgentId when direct agentId lookup fails:

use-dashboard-state.ts — TOOL_INVOKED handler

case 'TOOL_INVOKED': {
  const entry: EventLogEntry = { /* ... unchanged ... */ };
  const base = state.eventLog.length >= EVENT_LOG_MAX
    ? state.eventLog.slice(1) : state.eventLog;

  // Progress increment logic with fallback
  const invokedAgentId = action.payload.agentId;
  let agents = state.agents;

  // 1st: Try exact agentId match
  let targetAgent = invokedAgentId
    ? state.agents.get(invokedAgentId) ?? null
    : null;

  // 2nd: Fallback to focused agent when no exact match
  if (!targetAgent && state.focusedAgentId) {
    targetAgent = state.agents.get(state.focusedAgentId) ?? null;
  }

  if (targetAgent && targetAgent.status === 'running') {
    agents = cloneAgents(state.agents);
    agents.set(targetAgent.id, {
      ...targetAgent,
      progress: Math.min(95, targetAgent.progress + 5),
    });
  }

  const toolCall: ToolCallRecord = { /* ... unchanged ... */ };
  const toolCallsBase = state.toolCalls.length >= TOOL_CALLS_MAX
    ? state.toolCalls.slice(1) : state.toolCalls;

  return {
    ...state,
    agents,
    eventLog: [...base, entry],
    toolCalls: [...toolCallsBase, toolCall],
  };
}

Why this works

  • focusedAgentId is always set to the primary agent after AGENT_ACTIVATED (via selectFocusedAgent())
  • The primary agent is always 'running' during the session
  • Each tool call increments progress by 5 (capped at 95)
  • When AGENT_DEACTIVATED fires with reason !== 'error', progress jumps to 100

Alternative considered: Map tool-level IDs to primary agent

This was rejected because it would require maintaining a mapping between short-lived tool-agent IDs (from parseAgentFromToolName) and the primary agent ID. The fallback approach is simpler and covers the common case.

Files to Modify

File Change
apps/mcp-server/src/tui/hooks/use-dashboard-state.ts Add focusedAgentId fallback in TOOL_INVOKED
apps/mcp-server/src/tui/hooks/use-dashboard-state.spec.ts Add test for fallback progress increment

Test Plan

yarn workspace codingbuddy test -- --testPathPattern="use-dashboard-state"
  1. Fallback progress: Dispatch AGENT_ACTIVATED (primary, id="primary:arch"), then TOOL_INVOKED (agentId="search_rules") → primary agent progress should be 5 (not 0)
  2. Exact match still works: Dispatch AGENT_ACTIVATED (id="search_rules"), then TOOL_INVOKED (agentId="search_rules") → exact match agent progress = 5
  3. Cap at 95: Dispatch 20× TOOL_INVOKED → progress capped at 95 (not 100)
  4. No fallback when no focused agent: focusedAgentId=null + unmatched TOOL_INVOKED → no crash, agents unchanged
  5. Only running agents: Primary agent with status='done' → progress not incremented

Acceptance Criteria

  • Progress bar advances with each tool call during active session
  • Progress increments by 5 per tool call, capped at 95
  • Reaches 100 only on AGENT_DEACTIVATED (completed)
  • Exact agentId match takes priority over fallback
  • No regression on existing TOOL_INVOKED behavior
  • All tests pass

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpriority:mustMust Have - 반드시 필요, 없으면 릴리즈 불가tuiTUI Agent Monitor

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions