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}.",