mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[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:
co-authored by
Renn F
Frontend Developer 1
Frontend Developer 2
parent
de1336c74d
commit
a89d3cc885
@@ -45,9 +45,9 @@ const typeIcons: Record<NotificationType, React.ReactNode> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const priorityColors: Record<NotificationPriority, string> = {
|
const priorityColors: Record<NotificationPriority, string> = {
|
||||||
[NotificationPriority.NORMAL]: "bg-gray-100 text-gray-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",
|
[NotificationPriority.HIGH]: "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300",
|
||||||
[NotificationPriority.URGENT]: "bg-red-100 text-red-700",
|
[NotificationPriority.URGENT]: "bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NotificationCardProps {
|
interface NotificationCardProps {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { use, useState } from "react";
|
import { use, useState } from "react";
|
||||||
import axios from "axios";
|
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 { useProject } from "@/hooks/use-projects";
|
||||||
import { useCreateBranch, useCreatePR, useMergePR } from "@/hooks/use-git";
|
import { useCreateBranch, useCreatePR, useMergePR } from "@/hooks/use-git";
|
||||||
import { Team, TaskStatus } from "@/types";
|
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: task, isLoading, error, refetch } = useTask(taskId);
|
||||||
const { data: project } = useProject(task?.project_id ?? "");
|
const { data: project } = useProject(task?.project_id ?? "");
|
||||||
const lifecycle = useTaskLifecycle();
|
const lifecycle = useTaskLifecycle();
|
||||||
|
const updateTask = useUpdateTask();
|
||||||
const createBranch = useCreateBranch();
|
const createBranch = useCreateBranch();
|
||||||
const createPR = useCreatePR();
|
const createPR = useCreatePR();
|
||||||
const mergePR = useMergePR();
|
const mergePR = useMergePR();
|
||||||
@@ -106,6 +107,20 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
|
|||||||
await lifecycle.reopen.mutateAsync(task.id);
|
await lifecycle.reopen.mutateAsync(task.id);
|
||||||
toast.success("Task reopened");
|
toast.success("Task reopened");
|
||||||
break;
|
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
|
// Git workflow actions
|
||||||
case "docs-complete":
|
case "docs-complete":
|
||||||
setDocsCompleteDialogOpen(true);
|
setDocsCompleteDialogOpen(true);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
useAuditorDashboard,
|
useAuditorDashboard,
|
||||||
useAuditorFlags,
|
useAuditorFlags,
|
||||||
useAuditorReports,
|
useAuditorReports,
|
||||||
|
useCreateAuditorReport,
|
||||||
} from "@/hooks/use-dashboard";
|
} from "@/hooks/use-dashboard";
|
||||||
import { LiveFeedsPanel } from "./live-feeds-panel";
|
import { LiveFeedsPanel } from "./live-feeds-panel";
|
||||||
import { QualityMetricsPanel } from "./quality-metrics-panel";
|
import { QualityMetricsPanel } from "./quality-metrics-panel";
|
||||||
@@ -11,6 +12,7 @@ import { FlaggedItemsPanel } from "./flagged-items-panel";
|
|||||||
import { ReportsPanel } from "./reports-panel";
|
import { ReportsPanel } from "./reports-panel";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw, FileText } from "lucide-react";
|
import { RefreshCw, FileText } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function AuditorDashboard() {
|
export function AuditorDashboard() {
|
||||||
const {
|
const {
|
||||||
@@ -20,11 +22,27 @@ export function AuditorDashboard() {
|
|||||||
} = useAuditorDashboard();
|
} = useAuditorDashboard();
|
||||||
const { data: flags, isLoading: loadingFlags } = useAuditorFlags();
|
const { data: flags, isLoading: loadingFlags } = useAuditorFlags();
|
||||||
const { data: reports, isLoading: loadingReports } = useAuditorReports();
|
const { data: reports, isLoading: loadingReports } = useAuditorReports();
|
||||||
|
const createReport = useCreateAuditorReport();
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
refetch();
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -40,7 +58,7 @@ export function AuditorDashboard() {
|
|||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<Button>
|
<Button onClick={handleGenerateReport} disabled={createReport.isPending}>
|
||||||
<FileText className="h-4 w-4 mr-2" />
|
<FileText className="h-4 w-4 mr-2" />
|
||||||
Generate Report
|
Generate Report
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AuditorReport } from "@/types";
|
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -26,6 +26,27 @@ function formatDate(timestamp: string): string {
|
|||||||
|
|
||||||
export function ReportsPanel({ reports, isLoading, onCreateReport }: ReportsPanelProps) {
|
export function ReportsPanel({ reports, isLoading, onCreateReport }: ReportsPanelProps) {
|
||||||
const sendReport = useSendAuditorReport();
|
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) => {
|
const handleSend = async (reportId: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -44,7 +65,7 @@ export function ReportsPanel({ reports, isLoading, onCreateReport }: ReportsPane
|
|||||||
<FileText className="h-5 w-5" />
|
<FileText className="h-5 w-5" />
|
||||||
Reports
|
Reports
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button size="sm" onClick={onCreateReport}>
|
<Button size="sm" onClick={handleNewReport} disabled={createReport.isPending}>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
New Report
|
New Report
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, useEffect } from "react";
|
||||||
import { Message } from "@/types";
|
import { Message } from "@/types";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@@ -12,6 +13,13 @@ interface MessageListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MessageList({ messages, isLoading }: 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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
@@ -44,6 +52,8 @@ export function MessageList({ messages, isLoading }: MessageListProps) {
|
|||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<MessageItem key={message.id} message={message} />
|
<MessageItem key={message.id} message={message} />
|
||||||
))}
|
))}
|
||||||
|
{/* Sentinel div — scrollIntoView targets this to keep the newest message visible */}
|
||||||
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function ActiveBlockersPanel({ tasks, isLoading }: ActiveBlockersPanelPro
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{blockedTasks.map((task) => (
|
{blockedTasks.map((task) => (
|
||||||
<Link key={task.id} href={"/tasks/" + task.id}>
|
<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>
|
<span className="text-lg">\uD83D\uDD34</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<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 type { Activity } from "./activity-item";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { UsageOverviewPanel } from "./usage-overview-panel";
|
import { UsageOverviewPanel } from "./usage-overview-panel";
|
||||||
import { RefreshCw, Settings } from "lucide-react";
|
import { RefreshCw, Settings, AlertCircle } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export function CommandCenter() {
|
export function CommandCenter() {
|
||||||
const { data: overview, isLoading: loadingOverview, refetch: refetchOverview } = useCeoOverview();
|
const { data: overview, isLoading: loadingOverview, isError: errorOverview, refetch: refetchOverview } = useCeoOverview();
|
||||||
const { data: flags, isLoading: loadingFlags } = useAuditorFlags({ resolved: false });
|
const { data: flags, isLoading: loadingFlags, isError: errorFlags, refetch: refetchFlags } = useAuditorFlags({ resolved: false });
|
||||||
const { data: tasks, isLoading: loadingTasks } = useTasks();
|
const { data: tasks, isLoading: loadingTasks, isError: errorTasks, refetch: refetchTasks } = useTasks();
|
||||||
const { data: activity, isLoading: loadingActivity } = useRecentActivity(24);
|
const { data: activity, isLoading: loadingActivity, isError: errorActivity, refetch: refetchActivity } = useRecentActivity(24);
|
||||||
|
|
||||||
|
const hasError = errorOverview || errorFlags || errorTasks || errorActivity;
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
refetchOverview();
|
refetchOverview();
|
||||||
|
refetchFlags();
|
||||||
|
refetchTasks();
|
||||||
|
refetchActivity();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,6 +53,14 @@ export function CommandCenter() {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Team Health */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-lg font-semibold mb-4">Team Health</h2>
|
<h2 className="text-lg font-semibold mb-4">Team Health</h2>
|
||||||
|
|||||||
@@ -105,6 +105,19 @@ export function KanbanBoard({
|
|||||||
return;
|
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 {
|
try {
|
||||||
await updateTask.mutateAsync({
|
await updateTask.mutateAsync({
|
||||||
taskId,
|
taskId,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function KanbanColumn({
|
|||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
onAction={onAction}
|
onAction={onAction}
|
||||||
showQaActions={showQaActions && status === TaskStatus.AWAITING_QA}
|
showQaActions={showQaActions && (status === TaskStatus.AWAITING_QA || status === TaskStatus.VERIFYING)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,23 +7,23 @@ interface PriorityIndicatorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const priorityColors: Record<number, string> = {
|
const priorityColors: Record<number, string> = {
|
||||||
0: "bg-red-200 text-red-700",
|
0: "bg-red-200 text-red-700 text-xs",
|
||||||
1: "bg-orange-200 text-orange-700",
|
1: "bg-orange-200 text-orange-700 text-xs",
|
||||||
2: "bg-blue-200 text-blue-700",
|
2: "bg-blue-200 text-blue-700 text-xs",
|
||||||
3: "bg-gray-200 text-gray-700",
|
3: "bg-gray-200 text-gray-700 text-xs",
|
||||||
};
|
};
|
||||||
|
|
||||||
const priorityLabels: Record<number, string> = {
|
const priorityLabels: Record<number, string> = {
|
||||||
0: "P0",
|
0: "P0 - Highest",
|
||||||
1: "P1",
|
1: "P1 - High",
|
||||||
2: "P2",
|
2: "P2 - Medium",
|
||||||
3: "P3",
|
3: "P3 - Low",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PriorityIndicator({ priority }: PriorityIndicatorProps) {
|
export function PriorityIndicator({ priority }: PriorityIndicatorProps) {
|
||||||
return (
|
return (
|
||||||
<Badge className={priorityColors[priority] ?? priorityColors[2] + " text-xs"}>
|
<Badge className={priorityColors[priority] ?? priorityColors[2]}>
|
||||||
{priorityLabels[priority] ?? "P2"}
|
{priorityLabels[priority] ?? "P2 - Medium"}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,14 +40,12 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
|||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||||
const [expandedSources, setExpandedSources] = useState<number | null>(null);
|
const [expandedSources, setExpandedSources] = useState<number | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(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(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current) {
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [messages, isLoading]);
|
}, [messages, isLoading]);
|
||||||
|
|
||||||
const handleSubmit = async (question?: string) => {
|
const handleSubmit = async (question?: string) => {
|
||||||
@@ -176,7 +174,7 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<ScrollArea className="flex-1 pr-4" ref={scrollRef}>
|
<ScrollArea className="flex-1 pr-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{messages.map((msg, idx) => (
|
{messages.map((msg, idx) => (
|
||||||
<div key={idx}>
|
<div key={idx}>
|
||||||
@@ -278,6 +276,9 @@ export function MentorChat({ onAsk, isLoading }: MentorChatProps) {
|
|||||||
<span className="text-sm">Mentor is thinking...</span>
|
<span className="text-sm">Mentor is thinking...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Sentinel div — scrollIntoView targets this to keep the newest message visible */}
|
||||||
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import {
|
|||||||
import { NotificationBell } from "@/components/notifications/notification-bell";
|
import { NotificationBell } from "@/components/notifications/notification-bell";
|
||||||
import { ConnectionStatus } from "./connection-status";
|
import { ConnectionStatus } from "./connection-status";
|
||||||
import { MobileSidebar } from "./mobile-sidebar";
|
import { MobileSidebar } from "./mobile-sidebar";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
@@ -23,14 +29,22 @@ export function Header() {
|
|||||||
<div className="flex items-center gap-4 flex-1 max-w-md">
|
<div className="flex items-center gap-4 flex-1 max-w-md">
|
||||||
{/* Mobile nav trigger — only shown below md, where the sidebar is hidden */}
|
{/* Mobile nav trigger — only shown below md, where the sidebar is hidden */}
|
||||||
<MobileSidebar />
|
<MobileSidebar />
|
||||||
<div className="relative w-full">
|
<TooltipProvider>
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
<Tooltip>
|
||||||
<Input
|
<TooltipTrigger asChild>
|
||||||
type="search"
|
<div className="relative w-full">
|
||||||
placeholder="Search tasks, agents..."
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
className="pl-10"
|
<Input
|
||||||
/>
|
type="search"
|
||||||
</div>
|
placeholder="Search tasks, agents..."
|
||||||
|
className="pl-10"
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Coming Soon</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|||||||
@@ -26,9 +26,14 @@ export function ChatComposer({
|
|||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
const text = value.trim();
|
const text = value.trim();
|
||||||
if (!text || isSending || disabled) return;
|
if (!text || isSending || disabled) return;
|
||||||
setValue("");
|
try {
|
||||||
await onSend(text);
|
await onSend(text);
|
||||||
// Refocus after send
|
// 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();
|
textareaRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface DraftProposalCardProps {
|
|||||||
|
|
||||||
// 0 is the highest priority, 3 the lowest — matches the backend contract.
|
// 0 is the highest priority, 3 the lowest — matches the backend contract.
|
||||||
const PRIORITY_LABELS: Record<number, string> = {
|
const PRIORITY_LABELS: Record<number, string> = {
|
||||||
0: "Urgent",
|
0: "Highest",
|
||||||
1: "High",
|
1: "High",
|
||||||
2: "Medium",
|
2: "Medium",
|
||||||
3: "Low",
|
3: "Low",
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
// Keep unused data in cache for 30 minutes
|
// Keep unused data in cache for 30 minutes
|
||||||
gcTime: 30 * 60 * 1000,
|
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,
|
refetchOnWindowFocus: false,
|
||||||
// Don't refetch on reconnect by default
|
// Don't refetch on reconnect by default
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export function CreateTaskDialog() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={(newOpen) => { setOpen(newOpen); if (!newOpen) resetForm(); }}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>
|
<Button>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSaveEdit}
|
onClick={handleSaveEdit}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className="h-7 w-7 p-0"
|
className="h-7 w-7 p-0"
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function formatTime(timestamp: string): string {
|
|||||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
const diffDays = Math.floor(diffHours / 24);
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
|
||||||
|
if (diffHours < 1) return "< 1h ago";
|
||||||
if (diffHours < 24) return `${diffHours}h ago`;
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
if (diffDays < 7) return `${diffDays}d ago`;
|
if (diffDays < 7) return `${diffDays}d ago`;
|
||||||
return date.toLocaleDateString("en-US", {
|
return date.toLocaleDateString("en-US", {
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleParentSave}
|
onClick={handleParentSave}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
disabled={updateTask.isPending}
|
disabled={updateTask.isPending}
|
||||||
className="h-7 w-7 p-0"
|
className="h-7 w-7 p-0"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function ApproachSection({ task, plan }: { task: Task; plan: TaskPlan }) {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Button size="sm" variant="ghost" onClick={handleCancel}><X className="h-4 w-4" /></Button>
|
<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
|
<Check className="h-4 w-4 mr-1" />Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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">
|
<Button size="sm" variant="ghost" onClick={() => { setNewTitle(""); setIsAdding(false); }} className="h-7 w-7 p-0">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</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" />
|
<Check className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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">
|
<Button size="sm" variant="ghost" onClick={() => { setNewItem(""); setIsAdding(false); }} className="h-7 w-7 p-0">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</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" />
|
<Check className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
@@ -620,7 +620,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
|
|||||||
</Select>
|
</Select>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<Button size="sm" variant="ghost" onClick={() => setEditingIdx(null)}><X className="h-4 w-4" /></Button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -682,7 +682,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
|
|||||||
</Select>
|
</Select>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<Button size="sm" variant="ghost" onClick={() => { setNewDesc(""); setNewMit(""); setIsAdding(false); }}><X className="h-4 w-4" /></Button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -801,7 +801,7 @@ function OpenQuestionsSection({ task, plan }: { task: Task; plan: TaskPlan }) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<Button size="sm" variant="ghost" onClick={() => setEditingIdx(null)}><X className="h-4 w-4" /></Button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -860,7 +860,7 @@ function OpenQuestionsSection({ task, plan }: { task: Task; plan: TaskPlan }) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<Button size="sm" variant="ghost" onClick={() => { setNewQuestion(""); setIsAdding(false); }}><X className="h-4 w-4" /></Button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export function EscalateToCeoDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirm} disabled={!reason.trim() || isPending}>
|
<Button onClick={handleConfirm} disabled={!reason.trim() || isPending}>
|
||||||
@@ -133,7 +133,7 @@ export function CeoRejectDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -232,7 +232,7 @@ export function CeoApproveDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="ceo-approve-notes">Approval notes</Label>
|
<Label htmlFor="ceo-approve-notes">Notes required</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="ceo-approve-notes"
|
id="ceo-approve-notes"
|
||||||
value={notes}
|
value={notes}
|
||||||
@@ -246,7 +246,7 @@ export function CeoApproveDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirm} disabled={tooShort || isPending}>
|
<Button onClick={handleConfirm} disabled={tooShort || isPending}>
|
||||||
@@ -325,7 +325,7 @@ export function RequiredNotesDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -398,7 +398,7 @@ export function CreateBranchDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirm} disabled={isPending}>
|
<Button onClick={handleConfirm} disabled={isPending}>
|
||||||
@@ -480,7 +480,7 @@ export function CreatePRDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirm} disabled={!title.trim() || isPending}>
|
<Button onClick={handleConfirm} disabled={!title.trim() || isPending}>
|
||||||
|
|||||||
@@ -297,6 +297,12 @@ export function TaskHeader({ task, onAction }: TaskHeaderProps) {
|
|||||||
case TaskStatus.CANCELLED:
|
case TaskStatus.CANCELLED:
|
||||||
actions.push({ label: "Reopen Task", action: "reopen", icon: <Play className="h-4 w-4 mr-2" /> });
|
actions.push({ label: "Reopen Task", action: "reopen", icon: <Play className="h-4 w-4 mr-2" /> });
|
||||||
break;
|
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
|
// Cancel is always available for non-terminal states
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ const priorityColors: Record<number, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const priorityLabels: Record<number, string> = {
|
const priorityLabels: Record<number, string> = {
|
||||||
0: "P0",
|
0: "P0 - Highest",
|
||||||
1: "P1",
|
1: "P1 - High",
|
||||||
2: "P2",
|
2: "P2 - Medium",
|
||||||
3: "P3",
|
3: "P3 - Low",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sorting types - exported for parent components
|
// Sorting types - exported for parent components
|
||||||
@@ -514,8 +514,8 @@ export function TaskTable({
|
|||||||
{task.team.replace(/_/g, " ")}
|
{task.team.replace(/_/g, " ")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="whitespace-nowrap">
|
<TableCell className="whitespace-nowrap">
|
||||||
<Badge className={priorityColors[task.priority] ?? priorityColors[2]}>
|
<Badge className={(priorityColors[task.priority] ?? priorityColors[2]) + " text-xs"}>
|
||||||
{priorityLabels[task.priority] ?? "P2"}
|
{priorityLabels[task.priority] ?? "P2 - Medium"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="whitespace-nowrap">
|
<TableCell className="whitespace-nowrap">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const AGENT_ROSTER: Agent[] = [
|
|||||||
// UX/UI Cell
|
// 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: "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-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: "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 },
|
{ 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,
|
enabled: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,17 +61,24 @@ export function useMetrics() {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: dashboardKeys.metrics(),
|
queryKey: dashboardKeys.metrics(),
|
||||||
queryFn: async (): Promise<MetricsSummary> => {
|
queryFn: async (): Promise<MetricsSummary> => {
|
||||||
// Fetch all metrics in parallel
|
// Fetch all metrics in parallel, including real agent status
|
||||||
const [velocity, blockers, communication] = await Promise.all([
|
const [velocity, blockers, communication, agentStatus] = await Promise.all([
|
||||||
dashboardApi.getVelocityMetrics(),
|
dashboardApi.getVelocityMetrics(),
|
||||||
dashboardApi.getBlockerMetrics(),
|
dashboardApi.getBlockerMetrics(),
|
||||||
dashboardApi.getCommunicationMetrics(),
|
dashboardApi.getCommunicationMetrics(),
|
||||||
|
dashboardApi.getAgentStatus(),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
velocity,
|
velocity,
|
||||||
blockers,
|
blockers,
|
||||||
communication,
|
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,
|
refetchInterval: 60000,
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export const getBoardAgents = (agents: AgentDefinition[] | undefined | null) =>
|
|||||||
(a.team === Team.BOARD ||
|
(a.team === Team.BOARD ||
|
||||||
a.role === AgentRole.HEAD_MARKETING ||
|
a.role === AgentRole.HEAD_MARKETING ||
|
||||||
a.role === AgentRole.AUDITOR ||
|
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) =>
|
export const getMainPm = (agents: AgentDefinition[] | undefined | null) =>
|
||||||
|
|||||||
@@ -169,14 +169,3 @@ export const sessionsApi = {
|
|||||||
return data;
|
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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export const tasksApi = {
|
|||||||
mockTasks[idx] = { ...mockTasks[idx], ...updates, updated_at: new Date().toISOString() };
|
mockTasks[idx] = { ...mockTasks[idx], ...updates, updated_at: new Date().toISOString() };
|
||||||
return mockTasks[idx];
|
return mockTasks[idx];
|
||||||
}
|
}
|
||||||
const { data } = await api.put<Task>("/tasks/" + taskId, updates);
|
const { data } = await api.patch<Task>("/tasks/" + taskId, updates);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ export class WebSocketConnection {
|
|||||||
this.ws.onerror = () => {
|
this.ws.onerror = () => {
|
||||||
// WebSocket errors are expected when backend is offline
|
// WebSocket errors are expected when backend is offline
|
||||||
// Don't log - the onclose handler will manage reconnection
|
// Don't log - the onclose handler will manage reconnection
|
||||||
// Increment attempts on error to prevent infinite loops
|
// Do NOT increment reconnectAttempts here; scheduleReconnect() is the
|
||||||
this.reconnectAttempts++;
|
// sole place that advances the counter to avoid double-counting.
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// Connection failed - backend likely offline
|
// Connection failed - backend likely offline
|
||||||
|
|||||||
Reference in New Issue
Block a user