-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Problem
When background agents are running, the status line showing current activity (e.g., → Fuzu-Production-Db_query:) flashes very briefly and then disappears. This causes:
- Flickering - Status line appears for ~1 second then vanishes
- Layout jumping - Screen jumps when status line appears/disappears
- Worse with multiple agents - More agents = more jumping
Screenshots
Without status line (most of the time):
- Shows only:
◉ Research-Specialist Task "Analyze PhD user recommendations"
With status line (brief flash):
- Shows:
◉ Research-Specialist Task "Analyze PhD user recommendations" - Plus:
→ Fuzu-Production-Db_query:
Root Cause Analysis
THE BUG: Missing initial session registration causes race condition
When a task spawns a subagent:
-
TaskTool creates child session (
task.ts:169-199)- Returns
sessionIdin metadata:ctx.metadata({ metadata: { sessionId: session.id } }) - Session is persisted to storage via
Session.createNext
- Returns
-
Task component renders activity (
index.tsx:1805-1807)const childMessages = createMemo(() => props.metadata.sessionId ? (sync.data.message[props.metadata.sessionId] ?? []) : [], )
- Immediately looks up
sync.data.message[props.metadata.sessionId]
- Immediately looks up
-
Sync context message store is EMPTY for new session (
sync.tsx:228-266)sync.data.message[childSessionID]is undefined initially- Child session's messages arrive via "message.updated" events after a delay
- During this delay,
childMessages()returns[],activity()returnsnull
-
Sync.session.sync() only runs on parent session (
index.tsx:173-187)- The
createEffectin Session component callssync.session.sync(route.sessionID)for parent only - Child session is never synced, so its messages never populate the store via sync
- The
-
Result: Activity line flashes → vanishes → reappears
- T0: Task component renders with
sessionIdbut no messages → no activity line - T1 (~1 sec): First "message.updated" event arrives → activity line appears briefly
- T2 (~2 sec): State update clears
sync.data.message[childSessionId]temporarily - T3: When stable, activity line stays visible (if at all)
- T0: Task component renders with
Key Files
| File | Lines | Purpose |
|---|---|---|
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx |
1790-1888 | Task component renders subagent status |
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx |
1805-1807 | childMessages memo lookup |
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx |
1810-1838 | activity memo calculation |
packages/opencode/src/cli/cmd/tui/context/sync.tsx |
228-266 | "message.updated" event handler |
packages/opencode/src/tool/task.ts |
163-199, 225-250 | Child session creation and metadata |
Suggested Fix Approaches
Option 1: Fetch child session messages on demand
// In packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
function Task(props: ToolProps<typeof TaskTool>) {
const sync = useSync()
// NEW: Ensure child session data is loaded
createEffect(async () => {
const childSessionId = props.metadata.sessionId
if (!childSessionId) return
const hasMessages = !!sync.data.message[childSessionId]?.length
if (!hasMessages) {
await sync.session.sync(childSessionId)
}
})
// ... rest of component ...
}Option 2: Add activity placeholder state
const activity = createMemo(() => {
const msgs = childMessages()
const hasPending = props.part.state.status === "running" || props.part.state.status === "pending"
if (msgs.length === 0 && hasPending && props.metadata.sessionId) {
return "Starting..."
}
// ... existing logic ...
})Option 3: Reserve layout space
Prevent jumping by always reserving space for the status line when a task is running.
Related Issues
- [UI] Restore Subagent Activity Streaming #22 Restore Subagent Activity Streaming (may share root cause)
Priority
Medium - UX issue affecting usability when running parallel agents