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> = {
|
||||
[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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user