diff --git a/client/src/components/AppShell.tsx b/client/src/components/AppShell.tsx
index a73dded..8529066 100644
--- a/client/src/components/AppShell.tsx
+++ b/client/src/components/AppShell.tsx
@@ -25,11 +25,17 @@ import { api } from '@/lib/api'
import { cn } from '@/lib/utils'
import { useAuth } from '@/store/auth'
-/** Polls the count of held actions awaiting review, so the nav can flag work is waiting. */
+/** Event other pages fire after dispatching a run or deciding a review, so the badge updates at once. */
+export const REVIEWS_CHANGED = 'teamup:reviews-changed'
+
+/** Tracks the count of held actions awaiting review: polls, and refetches on focus / a change event. */
function usePendingReviewCount(organizationId: string | null): number {
const [count, setCount] = useState(0)
useEffect(() => {
- if (!organizationId) return
+ if (!organizationId) {
+ setCount(0)
+ return
+ }
let cancelled = false
const tick = async () => {
try {
@@ -40,10 +46,17 @@ function usePendingReviewCount(organizationId: string | null): number {
}
}
void tick()
- const id = setInterval(tick, 12000)
+ const id = setInterval(tick, 6000)
+ const refetch = () => void tick()
+ window.addEventListener(REVIEWS_CHANGED, refetch)
+ window.addEventListener('focus', refetch)
+ document.addEventListener('visibilitychange', refetch)
return () => {
cancelled = true
clearInterval(id)
+ window.removeEventListener(REVIEWS_CHANGED, refetch)
+ window.removeEventListener('focus', refetch)
+ document.removeEventListener('visibilitychange', refetch)
}
}, [organizationId])
return count
diff --git a/client/src/pages/BoardPage.tsx b/client/src/pages/BoardPage.tsx
index 39dab93..769ea8c 100644
--- a/client/src/pages/BoardPage.tsx
+++ b/client/src/pages/BoardPage.tsx
@@ -10,7 +10,7 @@ import {
} from '@dnd-kit/core'
import { Bot, Plus, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
-import { AppShell } from '@/components/AppShell'
+import { AppShell, REVIEWS_CHANGED } from '@/components/AppShell'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
@@ -514,8 +514,13 @@ function TaskDrawer({
onClick={() =>
act(
() => api.post('/api/assembler/runs', { seatId, workItemId: task.id }),
- 'Dispatched — the proposal will land in the review inbox.',
- )
+ 'Dispatched — it will appear in the review inbox shortly.',
+ ).then(() => {
+ // The proposal lands ~a few seconds later; nudge the review badge to refetch.
+ window.dispatchEvent(new Event(REVIEWS_CHANGED))
+ setTimeout(() => window.dispatchEvent(new Event(REVIEWS_CHANGED)), 4000)
+ setTimeout(() => window.dispatchEvent(new Event(REVIEWS_CHANGED)), 9000)
+ })
}
>
diff --git a/client/src/pages/ReviewsPage.tsx b/client/src/pages/ReviewsPage.tsx
index e90d31a..03c5c46 100644
--- a/client/src/pages/ReviewsPage.tsx
+++ b/client/src/pages/ReviewsPage.tsx
@@ -8,6 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton'
import { Textarea } from '@/components/ui/textarea'
+import { REVIEWS_CHANGED } from '@/components/AppShell'
import { AgentFace } from '@/components/AgentFace'
import { MarkdownEditor } from '@/components/MarkdownEditor'
import { api } from '@/lib/api'
@@ -169,6 +170,7 @@ function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: str
toast.info('Sent back to the agent')
}
onDecided(item.id)
+ window.dispatchEvent(new Event(REVIEWS_CHANGED))
} catch (err) {
toast.error((err as Error).message)
} finally {
diff --git a/src/Modules/TeamUp.Modules.Assembler/Runtime/AgentRunExecutor.cs b/src/Modules/TeamUp.Modules.Assembler/Runtime/AgentRunExecutor.cs
index 24bd757..fd3d520 100644
--- a/src/Modules/TeamUp.Modules.Assembler/Runtime/AgentRunExecutor.cs
+++ b/src/Modules/TeamUp.Modules.Assembler/Runtime/AgentRunExecutor.cs
@@ -94,12 +94,20 @@ internal sealed class AgentRunExecutor(
run.Complete(output, assembled.PrimaryAction, assembled.PrimaryActionRisk, result, completion.LatencyMs, clock.GetUtcNow());
await db.SaveChangesAsync(cancellationToken);
+ // Only a spec / story-breakdown agent proposes child stories. Other actions (code, design,
+ // tests) produce an artifact, not a backlog — so don't mistake a numbered list in their
+ // output (e.g. a list of file names) for child tasks.
+ var primarySkill = context.SkillKeys.Count > 0 ? context.SkillKeys[0] : null;
+ var childTitles = primarySkill is "spec-writing" or "story-breakdown"
+ ? OutputParser.ExtractChildTitles(output)
+ : Array.Empty();
+
// Hand the parsed action to the gate: autonomy vs risk → execute now or hold in review.
var gate = await actionGate.EvaluateAsync(
new AgentActionProposal(
run.Id, run.SeatId, context.AgentId, run.WorkItemId, context.TeamId, context.OrganizationId,
context.Autonomy, assembled.PrimaryAction, assembled.PrimaryActionRisk,
- context.TaskTitle, output, OutputParser.ExtractChildTitles(output), assembled.Trace),
+ context.TaskTitle, output, childTitles, assembled.Trace),
cancellationToken);
logger.LogInformation(
"Run {RunId}: {Action} ({Risk}) → {Outcome}.",