[a1e2bfb4] Fix 26 verified UI bugs across the panel dashboard (#175)

* [41219301] Fix 26 verified UI bugs in the panel dashboard (#174)

* [115788ef] API/state bugs batch 1 — PATCH fix, WebSocket reconnect, agent roster, sessions export, timestamp (#173)

* fix(orchestrator): launch agent MCP servers with uv run --no-sync

Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.

* fix(agent): launch agent uv-run subprocesses with --no-sync

Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.

- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync

* [115788ef] fix(api): use PATCH not PUT in tasksApi.update(), remove WS double-increment, fix staleTime/roster id, remove sessions groupsApi dup, add < 1h ago label

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [db2c341f] UI/visual bugs batch 1 — priority labels, QA columns, DnD prompt, CEO dialog, dark mode (#172)

* fix(orchestrator): launch agent MCP servers with uv run --no-sync

Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.

* fix(agent): launch agent uv-run subprocesses with --no-sync

Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.

- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync

* [db2c341f] fix(ui): priority badges, QA columns, DnD dialog, CEO label, dark mode

- priority-indicator.tsx: update labels P0→P0-Highest etc, add text-xs to all color strings, fix className operator precedence bug
- task-table.tsx: match priority label format and add text-xs to badge className
- kanban-column.tsx: show QA Pass/Fail buttons in VERIFYING column alongside AWAITING_QA
- kanban-board.tsx: intercept DnD drops onto NEEDS_REVISION/AWAITING_DOCUMENTATION to show notes dialog when showQaActions is true
- task-action-dialogs.tsx: change CeoApproveDialog Label from 'Approval notes' to 'Notes required'; fix all Cancel buttons to call handleOpenChange(false) so state is cleared on dismiss
- create-task-dialog.tsx: reset form when dialog is closed without submitting
- active-blockers-panel.tsx: add dark:border-red-900 dark:bg-red-950 dark:hover:bg-red-900 to blocker items
- notifications/page.tsx: add dark: Tailwind variants for NORMAL, HIGH, URGENT priority badge colors

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [123e2ec2] Fix remaining 15 UI bugs — revision pass after CEO rejection (#178)

* [cdb9b22a] fix(ui): task-header BACKLOG/NEEDS_REVISION actions, P0 priority label, chat-composer safe clear, and inline-edit double-mutation guards (#176)

- task-header.tsx: add BACKLOG ('Activate Task') and NEEDS_REVISION ('Start Revision') cases to getAvailableActions() switch so the Actions dropdown is never empty for those statuses
- draft-proposal-card.tsx: PRIORITY_LABELS[0] changed from 'Urgent' to 'Highest' to match backend contract
- chat-composer.tsx: move setValue('') inside try-block after onSend resolves; a failed send now preserves the textarea text
- acceptance-criteria.tsx: onMouseDown={(e)=>e.preventDefault()} on inline-edit save button to prevent onBlur+onClick double API mutation
- tab-dependencies.tsx: same onMouseDown guard on parent-task inline-edit save button
- tab-plan.tsx: onMouseDown guards on all inline-edit/add save buttons (ApproachSection, SubTasks, TechConsiderations, Risks, OpenQuestions)

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [01852d69] fix(dashboard): wire real agent status, refetch all 4 queries, error indicator, Coming Soon tooltip on search, sentinel div auto-scroll in message-list and mentor-chat, and New Report / Generate Report button mutations in reports-panel and auditor-dashboard — all 9 files fixed (#177)

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [3f35f502] feat(tasks): add case activate and case start-revision to handleAction switch in task detail page (#179) (#180)

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* fix(tasks): route Start Revision through the operator status override

The new "Start Revision" action on a NEEDS_REVISION task called
lifecycle.start (POST /tasks/{id}/start), which is assignee-only — so an
operator/CEO clicking it from the task detail page got a 403 ("Only the
assigned agent can start this task") instead of a transition.

Route it through useUpdateTask (PATCH /tasks/{id} with status=in_progress)
instead. The backend treats status as an audited admin override applied via
admin_set_status and gated on elevated (ASSIGN) permissions — the same
god-mode path the kanban board uses for operator status changes — so the
operator can nudge a needs_revision task back into progress for its assignee
to rework. Mirrors the existing kanban updateTask.mutateAsync shape.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
This commit is contained in:
Renzo F
2026-06-16 06:57:00 +02:00
committed by GitHub
co-authored by Renn F Frontend Developer 1 Frontend Developer 2
parent de1336c74d
commit a89d3cc885
29 changed files with 201 additions and 84 deletions
@@ -45,9 +45,9 @@ const typeIcons: Record<NotificationType, React.ReactNode> = {
};
const priorityColors: Record<NotificationPriority, string> = {
[NotificationPriority.NORMAL]: "bg-gray-100 text-gray-700",
[NotificationPriority.HIGH]: "bg-orange-100 text-orange-700",
[NotificationPriority.URGENT]: "bg-red-100 text-red-700",
[NotificationPriority.NORMAL]: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300",
[NotificationPriority.HIGH]: "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300",
[NotificationPriority.URGENT]: "bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300",
};
interface NotificationCardProps {
@@ -2,7 +2,7 @@
import { use, useState } from "react";
import axios from "axios";
import { useTask, useTaskLifecycle } from "@/hooks/use-tasks";
import { useTask, useTaskLifecycle, useUpdateTask } from "@/hooks/use-tasks";
import { useProject } from "@/hooks/use-projects";
import { useCreateBranch, useCreatePR, useMergePR } from "@/hooks/use-git";
import { Team, TaskStatus } from "@/types";
@@ -35,6 +35,7 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
const { data: task, isLoading, error, refetch } = useTask(taskId);
const { data: project } = useProject(task?.project_id ?? "");
const lifecycle = useTaskLifecycle();
const updateTask = useUpdateTask();
const createBranch = useCreateBranch();
const createPR = useCreatePR();
const mergePR = useMergePR();
@@ -106,6 +107,20 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
await lifecycle.reopen.mutateAsync(task.id);
toast.success("Task reopened");
break;
case "activate":
await lifecycle.activate.mutateAsync(task.id);
toast.success("Task activated");
break;
case "start-revision":
// Operator override: /start is assignee-only, so route the
// needs_revision -> in_progress nudge through the audited admin
// status path (PATCH /tasks/{id}), which privileged roles may use.
await updateTask.mutateAsync({
taskId: task.id,
updates: { status: TaskStatus.IN_PROGRESS },
});
toast.success("Revision started");
break;
// Git workflow actions
case "docs-complete":
setDocsCompleteDialogOpen(true);
@@ -4,6 +4,7 @@ import {
useAuditorDashboard,
useAuditorFlags,
useAuditorReports,
useCreateAuditorReport,
} from "@/hooks/use-dashboard";
import { LiveFeedsPanel } from "./live-feeds-panel";
import { QualityMetricsPanel } from "./quality-metrics-panel";
@@ -11,6 +12,7 @@ import { FlaggedItemsPanel } from "./flagged-items-panel";
import { ReportsPanel } from "./reports-panel";
import { Button } from "@/components/ui/button";
import { RefreshCw, FileText } from "lucide-react";
import { toast } from "sonner";
export function AuditorDashboard() {
const {
@@ -20,11 +22,27 @@ export function AuditorDashboard() {
} = useAuditorDashboard();
const { data: flags, isLoading: loadingFlags } = useAuditorFlags();
const { data: reports, isLoading: loadingReports } = useAuditorReports();
const createReport = useCreateAuditorReport();
const handleRefresh = () => {
refetch();
};
const handleGenerateReport = () => {
createReport.mutate(
{
report_type: "audit_summary",
title: `Audit Summary — ${new Date().toLocaleDateString()}`,
summary: "Automatically generated audit summary report.",
sections: [],
},
{
onSuccess: () => toast.success("Audit report generated successfully"),
onError: () => toast.error("Failed to generate audit report"),
}
);
};
return (
<div className="space-y-6">
{/* Header */}
@@ -40,7 +58,7 @@ export function AuditorDashboard() {
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
<Button>
<Button onClick={handleGenerateReport} disabled={createReport.isPending}>
<FileText className="h-4 w-4 mr-2" />
Generate Report
</Button>
+23 -2
View File
@@ -1,7 +1,7 @@
"use client";
import { AuditorReport } from "@/types";
import { useSendAuditorReport } from "@/hooks/use-dashboard";
import { useSendAuditorReport, useCreateAuditorReport } from "@/hooks/use-dashboard";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -26,6 +26,27 @@ function formatDate(timestamp: string): string {
export function ReportsPanel({ reports, isLoading, onCreateReport }: ReportsPanelProps) {
const sendReport = useSendAuditorReport();
const createReport = useCreateAuditorReport();
const handleNewReport = () => {
if (onCreateReport) {
onCreateReport();
return;
}
// No parent handler provided — create a draft report directly
createReport.mutate(
{
report_type: "summary",
title: `New Report — ${new Date().toLocaleDateString()}`,
summary: "Draft report created from the Reports panel.",
sections: [],
},
{
onSuccess: () => toast.success("Draft report created"),
onError: () => toast.error("Failed to create report"),
}
);
};
const handleSend = async (reportId: string) => {
try {
@@ -44,7 +65,7 @@ export function ReportsPanel({ reports, isLoading, onCreateReport }: ReportsPane
<FileText className="h-5 w-5" />
Reports
</CardTitle>
<Button size="sm" onClick={onCreateReport}>
<Button size="sm" onClick={handleNewReport} disabled={createReport.isPending}>
<Plus className="h-4 w-4 mr-1" />
New Report
</Button>
@@ -1,5 +1,6 @@
"use client";
import { useRef, useEffect } from "react";
import { Message } from "@/types";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
@@ -12,6 +13,13 @@ interface MessageListProps {
}
export function MessageList({ messages, isLoading }: MessageListProps) {
const bottomRef = useRef<HTMLDivElement>(null);
// Auto-scroll to the newest message using a sentinel div + scrollIntoView
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
if (isLoading) {
return (
<div className="space-y-4 p-4">
@@ -44,6 +52,8 @@ export function MessageList({ messages, isLoading }: MessageListProps) {
{messages.map((message) => (
<MessageItem key={message.id} message={message} />
))}
{/* Sentinel div — scrollIntoView targets this to keep the newest message visible */}
<div ref={bottomRef} />
</div>
</ScrollArea>
);
@@ -64,7 +64,7 @@ export function ActiveBlockersPanel({ tasks, isLoading }: ActiveBlockersPanelPro
<div className="space-y-3">
{blockedTasks.map((task) => (
<Link key={task.id} href={"/tasks/" + task.id}>
<div className="flex items-start gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 transition-colors">
<div className="flex items-start gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 dark:border-red-900 dark:bg-red-950 dark:hover:bg-red-900 transition-colors">
<span className="text-lg">\uD83D\uDD34</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
@@ -12,17 +12,22 @@ import { CeoApprovalQueue } from "./ceo-approval-queue";
import type { Activity } from "./activity-item";
import { Button } from "@/components/ui/button";
import { UsageOverviewPanel } from "./usage-overview-panel";
import { RefreshCw, Settings } from "lucide-react";
import { RefreshCw, Settings, AlertCircle } from "lucide-react";
import Link from "next/link";
export function CommandCenter() {
const { data: overview, isLoading: loadingOverview, refetch: refetchOverview } = useCeoOverview();
const { data: flags, isLoading: loadingFlags } = useAuditorFlags({ resolved: false });
const { data: tasks, isLoading: loadingTasks } = useTasks();
const { data: activity, isLoading: loadingActivity } = useRecentActivity(24);
const { data: overview, isLoading: loadingOverview, isError: errorOverview, refetch: refetchOverview } = useCeoOverview();
const { data: flags, isLoading: loadingFlags, isError: errorFlags, refetch: refetchFlags } = useAuditorFlags({ resolved: false });
const { data: tasks, isLoading: loadingTasks, isError: errorTasks, refetch: refetchTasks } = useTasks();
const { data: activity, isLoading: loadingActivity, isError: errorActivity, refetch: refetchActivity } = useRecentActivity(24);
const hasError = errorOverview || errorFlags || errorTasks || errorActivity;
const handleRefresh = () => {
refetchOverview();
refetchFlags();
refetchTasks();
refetchActivity();
};
return (
@@ -48,6 +53,14 @@ export function CommandCenter() {
</div>
</div>
{/* Error indicator */}
{hasError && (
<div className="flex items-center gap-2 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
<AlertCircle className="h-4 w-4 shrink-0" />
Some data failed to load. Click Refresh to try again.
</div>
)}
{/* Team Health */}
<section>
<h2 className="text-lg font-semibold mb-4">Team Health</h2>
@@ -105,6 +105,19 @@ export function KanbanBoard({
return;
}
// When QA actions are active, dragging to QA transition columns must go
// through the notes/audit dialog — do NOT fire the mutation directly.
if (showQaActions) {
if (newStatus === TaskStatus.NEEDS_REVISION) {
setPendingNotesAction({ kind: "fail-qa", taskId });
return;
}
if (newStatus === TaskStatus.AWAITING_DOCUMENTATION) {
setPendingNotesAction({ kind: "pass-qa", taskId });
return;
}
}
try {
await updateTask.mutateAsync({
taskId,
@@ -64,7 +64,7 @@ export function KanbanColumn({
key={task.id}
task={task}
onAction={onAction}
showQaActions={showQaActions && status === TaskStatus.AWAITING_QA}
showQaActions={showQaActions && (status === TaskStatus.AWAITING_QA || status === TaskStatus.VERIFYING)}
/>
))}
</div>
@@ -7,23 +7,23 @@ interface PriorityIndicatorProps {
}
const priorityColors: Record<number, string> = {
0: "bg-red-200 text-red-700",
1: "bg-orange-200 text-orange-700",
2: "bg-blue-200 text-blue-700",
3: "bg-gray-200 text-gray-700",
0: "bg-red-200 text-red-700 text-xs",
1: "bg-orange-200 text-orange-700 text-xs",
2: "bg-blue-200 text-blue-700 text-xs",
3: "bg-gray-200 text-gray-700 text-xs",
};
const priorityLabels: Record<number, string> = {
0: "P0",
1: "P1",
2: "P2",
3: "P3",
0: "P0 - Highest",
1: "P1 - High",
2: "P2 - Medium",
3: "P3 - Low",
};
export function PriorityIndicator({ priority }: PriorityIndicatorProps) {
return (
<Badge className={priorityColors[priority] ?? priorityColors[2] + " text-xs"}>
{priorityLabels[priority] ?? "P2"}
<Badge className={priorityColors[priority] ?? priorityColors[2]}>
{priorityLabels[priority] ?? "P2 - Medium"}
</Badge>
);
}
@@ -40,14 +40,12 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
const [input, setInput] = useState("");
const [conversationId, setConversationId] = useState<string | null>(null);
const [expandedSources, setExpandedSources] = useState<number | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const bottomRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
// Auto-scroll to bottom when new messages arrive
// Auto-scroll to the newest message using a sentinel div + scrollIntoView
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isLoading]);
const handleSubmit = async (question?: string) => {
@@ -176,7 +174,7 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
</div>
{/* Messages */}
<ScrollArea className="flex-1 pr-4" ref={scrollRef}>
<ScrollArea className="flex-1 pr-4">
<div className="space-y-4">
{messages.map((msg, idx) => (
<div key={idx}>
@@ -278,6 +276,9 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
<span className="text-sm">Mentor is thinking...</span>
</div>
)}
{/* Sentinel div — scrollIntoView targets this to keep the newest message visible */}
<div ref={bottomRef} />
</div>
</ScrollArea>
+22 -8
View File
@@ -13,6 +13,12 @@ import {
import { NotificationBell } from "@/components/notifications/notification-bell";
import { ConnectionStatus } from "./connection-status";
import { MobileSidebar } from "./mobile-sidebar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export function Header() {
const { setTheme } = useTheme();
@@ -23,14 +29,22 @@ export function Header() {
<div className="flex items-center gap-4 flex-1 max-w-md">
{/* Mobile nav trigger — only shown below md, where the sidebar is hidden */}
<MobileSidebar />
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="search"
placeholder="Search tasks, agents..."
className="pl-10"
/>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="search"
placeholder="Search tasks, agents..."
className="pl-10"
disabled={true}
/>
</div>
</TooltipTrigger>
<TooltipContent>Coming Soon</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{/* Actions */}
@@ -26,9 +26,14 @@ export function ChatComposer({
const handleSend = async () => {
const text = value.trim();
if (!text || isSending || disabled) return;
setValue("");
await onSend(text);
// Refocus after send
try {
await onSend(text);
// Clear only after a successful send — a failed send leaves the original text intact.
setValue("");
} catch {
// onSend threw; leave the text so the user can retry.
}
// Refocus after send (success or failure)
textareaRef.current?.focus();
};
@@ -18,7 +18,7 @@ interface DraftProposalCardProps {
// 0 is the highest priority, 3 the lowest — matches the backend contract.
const PRIORITY_LABELS: Record<number, string> = {
0: "Urgent",
0: "Highest",
1: "High",
2: "Medium",
3: "Low",
+2 -1
View File
@@ -16,7 +16,8 @@ export function Providers({ children }: { children: React.ReactNode }) {
staleTime: 5 * 60 * 1000,
// Keep unused data in cache for 30 minutes
gcTime: 30 * 60 * 1000,
// Don't refetch on window focus by default
// Intentionally disabled: refetching on window focus causes excessive
// API calls when the user alt-tabs back to the panel.
refetchOnWindowFocus: false,
// Don't refetch on reconnect by default
refetchOnReconnect: false,
@@ -181,7 +181,7 @@ export function CreateTaskDialog() {
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={open} onOpenChange={(newOpen) => { setOpen(newOpen); if (!newOpen) resetForm(); }}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
@@ -235,6 +235,7 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
<Button
size="sm"
onClick={handleSaveEdit}
onMouseDown={(e) => e.preventDefault()}
className="h-7 w-7 p-0"
>
<Check className="h-4 w-4" />
@@ -17,6 +17,7 @@ function formatTime(timestamp: string): string {
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) return "< 1h ago";
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString("en-US", {
@@ -327,6 +327,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
<Button
size="sm"
onClick={handleParentSave}
onMouseDown={(e) => e.preventDefault()}
disabled={updateTask.isPending}
className="h-7 w-7 p-0"
>
@@ -121,7 +121,7 @@ function ApproachSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</TabsList>
</Tabs>
<Button size="sm" variant="ghost" onClick={handleCancel}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleSave} disabled={updateTask.isPending}>
<Button size="sm" onClick={handleSave} onMouseDown={(e) => e.preventDefault()} disabled={updateTask.isPending}>
<Check className="h-4 w-4 mr-1" />Save
</Button>
</div>
@@ -333,7 +333,7 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<Button size="sm" variant="ghost" onClick={() => { setNewTitle(""); setIsAdding(false); }} className="h-7 w-7 p-0">
<X className="h-4 w-4" />
</Button>
<Button size="sm" onClick={handleAdd} disabled={!newTitle.trim()} className="h-7 w-7 p-0">
<Button size="sm" onClick={handleAdd} onMouseDown={(e) => e.preventDefault()} disabled={!newTitle.trim()} className="h-7 w-7 p-0">
<Check className="h-4 w-4" />
</Button>
</div>
@@ -489,7 +489,7 @@ function TechConsiderationsSection({ task, plan }: { task: Task; plan: TaskPlan
<Button size="sm" variant="ghost" onClick={() => { setNewItem(""); setIsAdding(false); }} className="h-7 w-7 p-0">
<X className="h-4 w-4" />
</Button>
<Button size="sm" onClick={handleAdd} disabled={!newItem.trim()} className="h-7 w-7 p-0">
<Button size="sm" onClick={handleAdd} onMouseDown={(e) => e.preventDefault()} disabled={!newItem.trim()} className="h-7 w-7 p-0">
<Check className="h-4 w-4" />
</Button>
</li>
@@ -620,7 +620,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</Select>
<div className="flex-1" />
<Button size="sm" variant="ghost" onClick={() => setEditingIdx(null)}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleEdit}><Check className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleEdit} onMouseDown={(e) => e.preventDefault()}><Check className="h-4 w-4" /></Button>
</div>
</div>
) : (
@@ -682,7 +682,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</Select>
<div className="flex-1" />
<Button size="sm" variant="ghost" onClick={() => { setNewDesc(""); setNewMit(""); setIsAdding(false); }}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleAdd} disabled={!newDesc.trim()}><Check className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleAdd} onMouseDown={(e) => e.preventDefault()} disabled={!newDesc.trim()}><Check className="h-4 w-4" /></Button>
</div>
</div>
)}
@@ -801,7 +801,7 @@ function OpenQuestionsSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<div className="flex items-center gap-2">
<div className="flex-1" />
<Button size="sm" variant="ghost" onClick={() => setEditingIdx(null)}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleEdit}><Check className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleEdit} onMouseDown={(e) => e.preventDefault()}><Check className="h-4 w-4" /></Button>
</div>
</div>
) : (
@@ -860,7 +860,7 @@ function OpenQuestionsSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<div className="flex items-center gap-2">
<div className="flex-1" />
<Button size="sm" variant="ghost" onClick={() => { setNewQuestion(""); setIsAdding(false); }}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleAdd} disabled={!newQuestion.trim()}><Check className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleAdd} onMouseDown={(e) => e.preventDefault()} disabled={!newQuestion.trim()}><Check className="h-4 w-4" /></Button>
</div>
</div>
)}
@@ -71,7 +71,7 @@ export function EscalateToCeoDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={!reason.trim() || isPending}>
@@ -133,7 +133,7 @@ export function CeoRejectDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button
@@ -232,7 +232,7 @@ export function CeoApproveDialog({
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="ceo-approve-notes">Approval notes</Label>
<Label htmlFor="ceo-approve-notes">Notes required</Label>
<Textarea
id="ceo-approve-notes"
value={notes}
@@ -246,7 +246,7 @@ export function CeoApproveDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={tooShort || isPending}>
@@ -325,7 +325,7 @@ export function RequiredNotesDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button
@@ -398,7 +398,7 @@ export function CreateBranchDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={isPending}>
@@ -480,7 +480,7 @@ export function CreatePRDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={!title.trim() || isPending}>
@@ -297,6 +297,12 @@ export function TaskHeader({ task, onAction }: TaskHeaderProps) {
case TaskStatus.CANCELLED:
actions.push({ label: "Reopen Task", action: "reopen", icon: <Play className="h-4 w-4 mr-2" /> });
break;
case TaskStatus.BACKLOG:
actions.push({ label: "Activate Task", action: "activate", icon: <Play className="h-4 w-4 mr-2" /> });
break;
case TaskStatus.NEEDS_REVISION:
actions.push({ label: "Start Revision", action: "start-revision", icon: <Play className="h-4 w-4 mr-2" /> });
break;
}
// Cancel is always available for non-terminal states
+6 -6
View File
@@ -47,10 +47,10 @@ const priorityColors: Record<number, string> = {
};
const priorityLabels: Record<number, string> = {
0: "P0",
1: "P1",
2: "P2",
3: "P3",
0: "P0 - Highest",
1: "P1 - High",
2: "P2 - Medium",
3: "P3 - Low",
};
// Sorting types - exported for parent components
@@ -514,8 +514,8 @@ export function TaskTable({
{task.team.replace(/_/g, " ")}
</TableCell>
<TableCell className="whitespace-nowrap">
<Badge className={priorityColors[task.priority] ?? priorityColors[2]}>
{priorityLabels[task.priority] ?? "P2"}
<Badge className={(priorityColors[task.priority] ?? priorityColors[2]) + " text-xs"}>
{priorityLabels[task.priority] ?? "P2 - Medium"}
</Badge>
</TableCell>
<TableCell className="whitespace-nowrap">
+2 -2
View File
@@ -27,7 +27,7 @@ const AGENT_ROSTER: Agent[] = [
// UX/UI Cell
{ id: "15", agent_id: "ux-dev-1", name: "UX/UI Dev 1", role: "developer" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
{ id: "16", agent_id: "ux-dev-2", name: "UX/UI Dev 2", role: "developer" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
{ id: "16", agent_id: "ux-qa", name: "UX/UI QA", role: "qa" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
{ id: "19", agent_id: "ux-qa", name: "UX/UI QA", role: "qa" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
{ id: "17", agent_id: "ux-pm", name: "UX/UI PM", role: "cell_pm" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
{ id: "18", agent_id: "ux-doc", name: "UX/UI Documenter", role: "documenter" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
];
@@ -77,7 +77,7 @@ export function useAgents() {
};
});
},
staleTime: Infinity, // Static data, only updates when orchestrator updates
staleTime: 5 * 60 * 1000, // 5 min — allows roster to refresh when orchestrator status changes
enabled: true,
});
}
+10 -3
View File
@@ -61,17 +61,24 @@ export function useMetrics() {
return useQuery({
queryKey: dashboardKeys.metrics(),
queryFn: async (): Promise<MetricsSummary> => {
// Fetch all metrics in parallel
const [velocity, blockers, communication] = await Promise.all([
// Fetch all metrics in parallel, including real agent status
const [velocity, blockers, communication, agentStatus] = await Promise.all([
dashboardApi.getVelocityMetrics(),
dashboardApi.getBlockerMetrics(),
dashboardApi.getCommunicationMetrics(),
dashboardApi.getAgentStatus(),
]);
return {
velocity,
blockers,
communication,
agents: { total_agents: 0, running: 0, idle: 0, waiting: 0, errors: 0 },
agents: {
total_agents: agentStatus?.total_agents ?? 0,
running: agentStatus?.by_state?.running ?? 0,
idle: agentStatus?.by_state?.idle ?? 0,
waiting: agentStatus?.waiting_count ?? 0,
errors: 0,
},
};
},
refetchInterval: 60000,
+2 -1
View File
@@ -27,7 +27,8 @@ export const getBoardAgents = (agents: AgentDefinition[] | undefined | null) =>
(a.team === Team.BOARD ||
a.role === AgentRole.HEAD_MARKETING ||
a.role === AgentRole.AUDITOR ||
a.role === AgentRole.PRODUCT_OWNER)
a.role === AgentRole.PRODUCT_OWNER ||
a.role === AgentRole.MAIN_PM)
);
export const getMainPm = (agents: AgentDefinition[] | undefined | null) =>
-11
View File
@@ -169,14 +169,3 @@ export const sessionsApi = {
return data;
},
};
export const groupsApi = {
// Get groups for a channel
listByChannel: async (channelId: string): Promise<unknown[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<unknown[]>("/channels/" + channelId + "/groups");
return data;
},
};
+1 -1
View File
@@ -139,7 +139,7 @@ export const tasksApi = {
mockTasks[idx] = { ...mockTasks[idx], ...updates, updated_at: new Date().toISOString() };
return mockTasks[idx];
}
const { data } = await api.put<Task>("/tasks/" + taskId, updates);
const { data } = await api.patch<Task>("/tasks/" + taskId, updates);
return data;
},
+2 -2
View File
@@ -104,8 +104,8 @@ export class WebSocketConnection {
this.ws.onerror = () => {
// WebSocket errors are expected when backend is offline
// Don't log - the onclose handler will manage reconnection
// Increment attempts on error to prevent infinite loops
this.reconnectAttempts++;
// Do NOT increment reconnectAttempts here; scheduleReconnect() is the
// sole place that advances the counter to avoid double-counting.
};
} catch {
// Connection failed - backend likely offline