[chore] panel: prettier reformat across the codebase

Apply `pnpm format` (prettier 3.8.5, 80-col / double-quote / semi /
trailing-comma-all) to the 223 pre-existing panel files that predated the
prettier infra added in cb5365a4. Pure formatting — no semantic changes:
multi-line arrays/objects collapsed where they fit, trailing newlines added
(.prettierrc.json), import grouping unchanged.

Verified: `pnpm format:check` clean, `pnpm lint` clean, `pnpm typecheck`
clean, `pnpm test` 113/113 pass (7 files).
This commit is contained in:
Renn F
2026-06-27 01:12:05 +02:00
parent 204e1525ee
commit 517bee7d28
223 changed files with 5721 additions and 2505 deletions
@@ -2,12 +2,32 @@
import { useParams, useRouter } from "next/navigation";
import { formatDistanceToNow } from "date-fns";
import { useAgentStatus, useStopAgent, useSpawnAgent, useAgentDefinition } from "@/hooks/use-agents";
import {
useAgentStatus,
useStopAgent,
useSpawnAgent,
useAgentDefinition,
} from "@/hooks/use-agents";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { ArrowLeft, Play, Square, AlertTriangle, Clock, RefreshCw, User, Users } from "lucide-react";
import {
ArrowLeft,
Play,
Square,
AlertTriangle,
Clock,
RefreshCw,
User,
Users,
} from "lucide-react";
import { toast } from "sonner";
import {
AgentStatusCards,
@@ -50,13 +70,19 @@ export default function AgentDetailPage() {
// Get display values from definition or fallback
const displayName = definition?.name || agentId;
const roleLabel = definition?.role ? ROLE_LABELS[definition.role] || definition.role : null;
const teamLabel = definition?.team ? TEAM_LABELS[definition.team] || definition.team : null;
const roleLabel = definition?.role
? ROLE_LABELS[definition.role] || definition.role
: null;
const teamLabel = definition?.team
? TEAM_LABELS[definition.team] || definition.team
: null;
const handleStop = async (graceful: boolean) => {
try {
await stopAgent.mutateAsync({ agentId, graceful });
toast.success(graceful ? "Agent stopping gracefully" : "Agent force stopped");
toast.success(
graceful ? "Agent stopping gracefully" : "Agent force stopped",
);
} catch {
toast.error("Failed to stop agent");
}
@@ -97,7 +123,9 @@ export default function AgentDetailPage() {
);
}
const isActive = agent && ["running", "ready", "starting", "waiting_long"].includes(agent.state);
const isActive =
agent &&
["running", "ready", "starting", "waiting_long"].includes(agent.state);
const isWaiting = agent?.state === "waiting_long";
return (
@@ -111,7 +139,9 @@ export default function AgentDetailPage() {
</Button>
<div>
<div className="flex items-center gap-3">
<h1 className="text-3xl font-bold tracking-tight">{displayName}</h1>
<h1 className="text-3xl font-bold tracking-tight">
{displayName}
</h1>
{roleLabel && (
<Badge variant="secondary" className="gap-1">
<User className="h-3 w-3" />
@@ -199,7 +229,9 @@ export default function AgentDetailPage() {
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-lg font-semibold text-red-600">{agent.error_count} error(s)</p>
<p className="text-lg font-semibold text-red-600">
{agent.error_count} error(s)
</p>
</CardContent>
</Card>
)}
@@ -213,20 +245,23 @@ export default function AgentDetailPage() {
Agent Waiting for Input
</CardTitle>
<CardDescription>
This agent is blocked and waiting for human input or external resolution.
This agent is blocked and waiting for human input or external
resolution.
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Use the &quot;Resolve Wait&quot; button above to provide the information or decision
the agent needs to continue execution.
Use the &quot;Resolve Wait&quot; button above to provide the
information or decision the agent needs to continue execution.
</p>
</CardContent>
</Card>
)}
{/* Agent Stream Viewer */}
{isActive && <AgentStreamViewer agentId={agentId} agentName={displayName} />}
{isActive && (
<AgentStreamViewer agentId={agentId} agentName={displayName} />
)}
</>
) : null}
</div>
+8 -6
View File
@@ -32,11 +32,11 @@ export default function AgentsPage() {
const { data: usageRows } = useAgentUsage();
// Check if it's a connection error (backend not running)
const isOffline = error && (
error.message?.includes("Network Error") ||
error.message?.includes("ECONNREFUSED") ||
(error as { code?: string })?.code === "ERR_NETWORK"
);
const isOffline =
error &&
(error.message?.includes("Network Error") ||
error.message?.includes("ECONNREFUSED") ||
(error as { code?: string })?.code === "ERR_NETWORK");
// Convert agents array to a record keyed by agent_id for easy lookup
const agentStatuses = useMemo(() => {
@@ -86,7 +86,9 @@ export default function AgentsPage() {
<OrchestratorStatusCards status={status} isLoading={isLoading} />
{/* Waiting Agents Alert */}
{waitingAgents && <WaitingAgentsAlert waitingAgents={waitingAgents} />}
{waitingAgents && (
<WaitingAgentsAlert waitingAgents={waitingAgents} />
)}
</>
)}
+2 -2
View File
@@ -42,8 +42,8 @@ function BusinessPageContent() {
<div>
<h1 className="text-3xl font-bold tracking-tight">Business</h1>
<p className="text-muted-foreground">
Company goals, your chief-of-staff Secretary, and Board pitches all in
one place.
Company goals, your chief-of-staff Secretary, and Board pitches all
in one place.
</p>
</div>
@@ -37,9 +37,10 @@ function SessionDetailContent() {
const groupId = searchParams.get("group");
// Build back URL preserving context
const backUrl = channelId && groupId
? `/communications?channel=${channelId}&group=${groupId}`
: "/communications";
const backUrl =
channelId && groupId
? `/communications?channel=${channelId}&group=${groupId}`
: "/communications";
const queryClient = useQueryClient();
const scrollRef = useRef<HTMLDivElement>(null);
@@ -52,12 +53,20 @@ function SessionDetailContent() {
// is what stops the panel from accumulating a 404 storm across every dead
// session it has opened. `refetchMessages` (the manual Refresh button) stays
// available for live sessions.
const { data: session, isLoading: loadingSession, refetch: refetchSession } = useSession(sessionId);
const { data: messagesData, isLoading: loadingMessages, refetch: refetchMessages } = useSessionMessages(sessionId);
const {
data: session,
isLoading: loadingSession,
refetch: refetchSession,
} = useSession(sessionId);
const {
data: messagesData,
isLoading: loadingMessages,
refetch: refetchMessages,
} = useSessionMessages(sessionId);
// Sort messages chronologically (oldest first for chat UI)
const messages = [...(messagesData?.items || [])].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
);
// Track if we've done the initial scroll
@@ -73,11 +82,19 @@ function SessionDetailContent() {
// Send message mutation
const sendMessage = useMutation({
mutationFn: async ({ content, type }: { content: string; type: string }) => {
mutationFn: async ({
content,
type,
}: {
content: string;
type: string;
}) => {
return messagesApi.send(sessionId, content, type);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["messages", "list", sessionId] });
queryClient.invalidateQueries({
queryKey: ["messages", "list", sessionId],
});
toast.success("Message sent");
},
onError: (error: Error) => {
@@ -95,7 +112,8 @@ function SessionDetailContent() {
};
// Get primary task
const primaryTask = session?.task_links?.find(t => t.is_primary) || session?.task_links?.[0];
const primaryTask =
session?.task_links?.find((t) => t.is_primary) || session?.task_links?.[0];
if (loadingSession) {
return (
@@ -122,7 +140,8 @@ function SessionDetailContent() {
<MessageSquare className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<h3 className="text-lg font-medium mb-2">Session Not Found</h3>
<p className="text-sm text-muted-foreground">
The session you&apos;re looking for doesn&apos;t exist or has been deleted.
The session you&apos;re looking for doesn&apos;t exist or has
been deleted.
</p>
</div>
</CardContent>
@@ -162,7 +181,9 @@ function SessionDetailContent() {
<Card className="mb-4 shrink-0">
<CardContent className="py-3">
<div className="flex items-center gap-4 flex-wrap">
<Badge variant={session.status === "active" ? "default" : "secondary"}>
<Badge
variant={session.status === "active" ? "default" : "secondary"}
>
{session.status}
</Badge>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
@@ -175,7 +196,8 @@ function SessionDetailContent() {
</div>
{session.closed_at && (
<div className="flex items-center gap-1 text-sm text-muted-foreground">
Closed: {format(new Date(session.closed_at), "MMM d, yyyy h:mm a")}
Closed:{" "}
{format(new Date(session.closed_at), "MMM d, yyyy h:mm a")}
</div>
)}
@@ -190,7 +212,8 @@ function SessionDetailContent() {
href={`/tasks/${primaryTask.task_id}`}
className="text-sm text-primary hover:underline"
>
{primaryTask.task_title || `Task ${primaryTask.task_id.slice(0, 8)}`}
{primaryTask.task_title ||
`Task ${primaryTask.task_id.slice(0, 8)}`}
</Link>
)}
{session.task_links.length > 1 && (
@@ -232,7 +255,9 @@ function SessionDetailContent() {
<div className="text-center py-12 text-muted-foreground">
<MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No messages in this session</p>
<p className="text-sm">Use the composer below to start the conversation</p>
<p className="text-sm">
Use the composer below to start the conversation
</p>
</div>
) : (
<div className="space-y-3">
@@ -37,12 +37,17 @@ interface ChannelListProps {
isLoading: boolean;
}
function ChannelList({ channels, selectedId, onSelect, isLoading }: ChannelListProps) {
function ChannelList({
channels,
selectedId,
onSelect,
isLoading,
}: ChannelListProps) {
const cellChannels = channels.filter((c) => c.type === "cell");
const crossCellChannels = channels.filter((c) => c.type === "cross_cell");
const managementChannels = channels.filter((c) => c.type === "management");
const otherChannels = channels.filter(
(c) => !["cell", "cross_cell", "management"].includes(c.type)
(c) => !["cell", "cross_cell", "management"].includes(c.type),
);
const renderGroup = (title: string, items: Channel[]) => {
@@ -208,9 +213,10 @@ function SessionList({ channelId, groupId }: SessionListProps) {
<div className="font-medium text-sm truncate">
{session.task_links?.length > 0 ? (
<>
{session.task_links.find(l => l.is_primary)?.task_title ||
session.task_links[0]?.task_title ||
`Task ${session.task_links[0]?.task_id.slice(0, 8)}`}
{session.task_links.find((l) => l.is_primary)
?.task_title ||
session.task_links[0]?.task_title ||
`Task ${session.task_links[0]?.task_id.slice(0, 8)}`}
</>
) : (
`Session ${session.id.slice(0, 8)}`
@@ -222,7 +228,9 @@ function SessionList({ channelId, groupId }: SessionListProps) {
</div>
<div className="flex flex-col items-end gap-1 shrink-0">
<Badge
variant={session.status === "active" ? "default" : "secondary"}
variant={
session.status === "active" ? "default" : "secondary"
}
className="text-xs"
>
{session.status}
@@ -243,7 +251,13 @@ function SessionList({ channelId, groupId }: SessionListProps) {
// Empty State Components
// =============================================================================
function EmptyPanel({ icon: Icon, message }: { icon: typeof MessageSquare; message: string }) {
function EmptyPanel({
icon: Icon,
message,
}: {
icon: typeof MessageSquare;
message: string;
}) {
return (
<div className="h-full flex items-center justify-center text-muted-foreground">
<div className="text-center p-4">
@@ -267,10 +281,10 @@ function CommunicationsPageContent() {
const { data: channels, isLoading, error, refetch } = useChannels();
const isOffline = error && (
error.message?.includes("Network Error") ||
(error as { code?: string })?.code === "ERR_NETWORK"
);
const isOffline =
error &&
(error.message?.includes("Network Error") ||
(error as { code?: string })?.code === "ERR_NETWORK");
const updateParams = useCallback(
(updates: Record<string, string | null>) => {
@@ -285,18 +299,24 @@ function CommunicationsPageContent() {
const query = params.toString();
router.push(query ? `/communications?${query}` : "/communications");
},
[router, searchParams]
[router, searchParams],
);
const handleSelectChannel = useCallback((id: string) => {
updateParams({ channel: id, group: null });
}, [updateParams]);
const handleSelectChannel = useCallback(
(id: string) => {
updateParams({ channel: id, group: null });
},
[updateParams],
);
const handleSelectGroup = useCallback((id: string) => {
updateParams({ group: id });
}, [updateParams]);
const handleSelectGroup = useCallback(
(id: string) => {
updateParams({ group: id });
},
[updateParams],
);
const selectedChannel = channels?.find(c => c.id === channelId);
const selectedChannel = channels?.find((c) => c.id === channelId);
return (
<div className="flex flex-col lg:h-[calc(100vh-7rem)]">
@@ -347,7 +367,10 @@ function CommunicationsPageContent() {
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Groups</span>
{selectedChannel && (
<Badge variant="outline" className="ml-auto text-xs font-normal">
<Badge
variant="outline"
className="ml-auto text-xs font-normal"
>
{selectedChannel.name}
</Badge>
)}
@@ -379,7 +402,11 @@ function CommunicationsPageContent() {
) : (
<EmptyPanel
icon={MessageSquare}
message={channelId ? "Select a group" : "Select a channel and group"}
message={
channelId
? "Select a group"
: "Select a channel and group"
}
/>
)}
</div>
@@ -394,33 +421,35 @@ function CommunicationsPageContent() {
// Wrap in Suspense for useSearchParams
export default function CommunicationsPage() {
return (
<Suspense fallback={
<div className="flex flex-col lg:h-[calc(100vh-7rem)]">
<div className="flex items-center justify-between mb-4">
<div>
<Skeleton className="h-9 w-48 mb-2" />
<Skeleton className="h-5 w-64" />
<Suspense
fallback={
<div className="flex flex-col lg:h-[calc(100vh-7rem)]">
<div className="flex items-center justify-between mb-4">
<div>
<Skeleton className="h-9 w-48 mb-2" />
<Skeleton className="h-5 w-64" />
</div>
</div>
<div className="grid grid-cols-12 gap-4 lg:gap-6">
<Card className="col-span-12 lg:col-span-3">
<CardContent className="p-3 space-y-2">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-8 w-full" />
))}
</CardContent>
</Card>
<Card className="col-span-12 lg:col-span-3">
<CardContent className="p-3 space-y-2">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</CardContent>
</Card>
<Card className="col-span-12 lg:col-span-6" />
</div>
</div>
<div className="grid grid-cols-12 gap-4 lg:gap-6">
<Card className="col-span-12 lg:col-span-3">
<CardContent className="p-3 space-y-2">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-8 w-full" />
))}
</CardContent>
</Card>
<Card className="col-span-12 lg:col-span-3">
<CardContent className="p-3 space-y-2">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</CardContent>
</Card>
<Card className="col-span-12 lg:col-span-6" />
</div>
</div>
}>
}
>
<CommunicationsPageContent />
</Suspense>
);
@@ -144,7 +144,10 @@ export default function JournalEntryPage({ params }: JournalEntryPageProps) {
<span>Related Task</span>
</div>
<Link href={`/tasks/${entry.task_id}`}>
<Badge variant="outline" className="hover:bg-muted cursor-pointer">
<Badge
variant="outline"
className="hover:bg-muted cursor-pointer"
>
Task #{entry.task_id.slice(0, 8)}
</Badge>
</Link>
+71 -54
View File
@@ -89,37 +89,52 @@ function JournalsPageContent() {
}, [selectedAgentId, agentSearch, urlType, taskFilter]);
// Update URL params
const updateParams = useCallback((updates: Record<string, string | null>) => {
const params = new URLSearchParams(searchParams.toString());
Object.entries(updates).forEach(([key, value]) => {
if (value) {
params.set(key, value);
} else {
params.delete(key);
const updateParams = useCallback(
(updates: Record<string, string | null>) => {
const params = new URLSearchParams(searchParams.toString());
Object.entries(updates).forEach(([key, value]) => {
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
});
const query = params.toString();
router.push(query ? `/journals?${query}` : "/journals");
},
[router, searchParams],
);
const handleSelectAgent = useCallback(
(agentId: string | null) => {
// Only reset filters when changing to a different agent
if (agentId !== selectedAgentId) {
updateParams({ agent: agentId, type: null, task: null });
}
});
const query = params.toString();
router.push(query ? `/journals?${query}` : "/journals");
}, [router, searchParams]);
},
[updateParams, selectedAgentId],
);
const handleSelectAgent = useCallback((agentId: string | null) => {
// Only reset filters when changing to a different agent
if (agentId !== selectedAgentId) {
updateParams({ agent: agentId, type: null, task: null });
}
}, [updateParams, selectedAgentId]);
const handleAgentSearch = useCallback(
(value: string) => {
updateParams({ q: value || null });
},
[updateParams],
);
const handleAgentSearch = useCallback((value: string) => {
updateParams({ q: value || null });
}, [updateParams]);
const handleTypeChange = useCallback(
(value: JournalEntryType | "all") => {
updateParams({ type: value === "all" ? null : value });
},
[updateParams],
);
const handleTypeChange = useCallback((value: JournalEntryType | "all") => {
updateParams({ type: value === "all" ? null : value });
}, [updateParams]);
const handleTaskChange = useCallback((value: string | null) => {
updateParams({ task: value });
}, [updateParams]);
const handleTaskChange = useCallback(
(value: string | null) => {
updateParams({ task: value });
},
[updateParams],
);
// Filter agents by search
const filteredAgents = (agents ?? []).filter((agent) => {
@@ -211,35 +226,37 @@ function JournalsPageContent() {
// Wrap in Suspense for useSearchParams
export default function JournalsPage() {
return (
<Suspense fallback={
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-48 mb-2" />
<Skeleton className="h-5 w-64" />
<Suspense
fallback={
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-48 mb-2" />
<Skeleton className="h-5 w-64" />
</div>
</div>
<div className="grid grid-cols-12 gap-6">
<div className="col-span-12 lg:col-span-3">
<Card>
<CardContent className="p-3 space-y-2">
<Skeleton className="h-10 w-full" />
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</CardContent>
</Card>
</div>
<div className="col-span-12 lg:col-span-9">
<Card>
<CardContent className="p-6">
<Skeleton className="h-64 w-full" />
</CardContent>
</Card>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-6">
<div className="col-span-12 lg:col-span-3">
<Card>
<CardContent className="p-3 space-y-2">
<Skeleton className="h-10 w-full" />
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</CardContent>
</Card>
</div>
<div className="col-span-12 lg:col-span-9">
<Card>
<CardContent className="p-6">
<Skeleton className="h-64 w-full" />
</CardContent>
</Card>
</div>
</div>
</div>
}>
}
>
<JournalsPageContent />
</Suspense>
);
+17 -10
View File
@@ -2,7 +2,12 @@
import { Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { DevKanban, QaKanban, PrReviewKanban, PmKanban } from "@/components/kanban";
import {
DevKanban,
QaKanban,
PrReviewKanban,
PmKanban,
} from "@/components/kanban";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton";
import { Code, TestTube, GitPullRequest, ClipboardList } from "lucide-react";
@@ -66,16 +71,18 @@ function KanbanPageContent() {
// Wrap in Suspense for useSearchParams
export default function KanbanPage() {
return (
<Suspense fallback={
<div className="space-y-6">
<Skeleton className="h-10 w-72" />
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-96 w-full" />
))}
<Suspense
fallback={
<div className="space-y-6">
<Skeleton className="h-10 w-72" />
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-96 w-full" />
))}
</div>
</div>
</div>
}>
}
>
<KanbanPageContent />
</Suspense>
);
+142 -60
View File
@@ -42,7 +42,10 @@ import {
Coins,
Sparkles,
} from "lucide-react";
import type { UsageProjection as UP, CacheEfficiencyResponse as CER } from "@/types";
import type {
UsageProjection as UP,
CacheEfficiencyResponse as CER,
} from "@/types";
// ─── Humanized number formatting ─────────────────────────────────────────────
@@ -71,7 +74,14 @@ interface MetricCardProps {
trendValue?: string;
}
function MetricCard({ title, value, subtitle, icon, trend, trendValue }: MetricCardProps) {
function MetricCard({
title,
value,
subtitle,
icon,
trend,
trendValue,
}: MetricCardProps) {
const displayValue = typeof value === "number" ? humanizeCount(value) : value;
return (
<Card>
@@ -87,10 +97,19 @@ function MetricCard({ title, value, subtitle, icon, trend, trendValue }: MetricC
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
)}
{trend && trendValue && (
<div className={"flex items-center gap-1 mt-2 text-xs " +
(trend === "up" ? "text-green-600" : trend === "down" ? "text-red-600" : "text-gray-500")
}>
<TrendingUp className={"h-3 w-3 " + (trend === "down" ? "rotate-180" : "")} />
<div
className={
"flex items-center gap-1 mt-2 text-xs " +
(trend === "up"
? "text-green-600"
: trend === "down"
? "text-red-600"
: "text-gray-500")
}
>
<TrendingUp
className={"h-3 w-3 " + (trend === "down" ? "rotate-180" : "")}
/>
{trendValue}
</div>
)}
@@ -106,8 +125,13 @@ interface TeamHealthCardProps {
completedToday: number;
}
function TeamHealthCard({ team, activeTasks, blockedTasks, completedToday }: TeamHealthCardProps) {
const healthScore = Math.max(0, 100 - (blockedTasks * 20));
function TeamHealthCard({
team,
activeTasks,
blockedTasks,
completedToday,
}: TeamHealthCardProps) {
const healthScore = Math.max(0, 100 - blockedTasks * 20);
return (
<Card>
@@ -144,12 +168,16 @@ function TeamHealthCard({ team, activeTasks, blockedTasks, completedToday }: Tea
function PerformanceTabContent() {
const { data: tasks, error: tasksError, refetch: refetchTasks } = useTasks();
const { data: status, error: statusError, refetch: refetchStatus } = useOrchestratorStatus();
const {
data: status,
error: statusError,
refetch: refetchStatus,
} = useOrchestratorStatus();
const isOffline = (tasksError || statusError) && (
tasksError?.message?.includes("Network Error") ||
statusError?.message?.includes("Network Error")
);
const isOffline =
(tasksError || statusError) &&
(tasksError?.message?.includes("Network Error") ||
statusError?.message?.includes("Network Error"));
const refetch = () => {
refetchTasks();
@@ -176,17 +204,35 @@ function PerformanceTabContent() {
}).length;
// Task status counts
const pending = taskList.filter((t) => t.status === TaskStatus.PENDING).length;
const inProgress = taskList.filter((t) => t.status === TaskStatus.IN_PROGRESS).length;
const blocked = taskList.filter((t) => t.status === TaskStatus.BLOCKED).length;
const awaitingQa = taskList.filter((t) => t.status === TaskStatus.AWAITING_QA).length;
const completed = taskList.filter((t) => t.status === TaskStatus.COMPLETED).length;
const pending = taskList.filter(
(t) => t.status === TaskStatus.PENDING,
).length;
const inProgress = taskList.filter(
(t) => t.status === TaskStatus.IN_PROGRESS,
).length;
const blocked = taskList.filter(
(t) => t.status === TaskStatus.BLOCKED,
).length;
const awaitingQa = taskList.filter(
(t) => t.status === TaskStatus.AWAITING_QA,
).length;
const completed = taskList.filter(
(t) => t.status === TaskStatus.COMPLETED,
).length;
// Agent counts
const runningAgents = status?.by_state?.running || agentList.filter((a) => a.state === "running").length;
const idleAgents = status?.by_state?.idle || agentList.filter((a) => a.state === "idle" || a.state === "stopped").length;
const waitingAgents = status?.waiting_count || agentList.filter((a) => a.state === "waiting_long").length;
const errorAgents = status?.by_state?.error || agentList.filter((a) => a.state === "error").length;
const runningAgents =
status?.by_state?.running ||
agentList.filter((a) => a.state === "running").length;
const idleAgents =
status?.by_state?.idle ||
agentList.filter((a) => a.state === "idle" || a.state === "stopped").length;
const waitingAgents =
status?.waiting_count ||
agentList.filter((a) => a.state === "waiting_long").length;
const errorAgents =
status?.by_state?.error ||
agentList.filter((a) => a.state === "error").length;
// Team metrics
const teamMetrics = Object.values(Team).map((team) => {
@@ -194,9 +240,10 @@ function PerformanceTabContent() {
return {
team,
activeTasks: teamTasks.filter((t) =>
[TaskStatus.IN_PROGRESS, TaskStatus.CLAIMED].includes(t.status)
[TaskStatus.IN_PROGRESS, TaskStatus.CLAIMED].includes(t.status),
).length,
blockedTasks: teamTasks.filter((t) => t.status === TaskStatus.BLOCKED).length,
blockedTasks: teamTasks.filter((t) => t.status === TaskStatus.BLOCKED)
.length,
completedToday: teamTasks.filter((t) => {
if (!t.completed_at) return false;
const c = new Date(t.completed_at);
@@ -242,7 +289,11 @@ function PerformanceTabContent() {
/>
<MetricCard
title="Completion Rate"
value={taskList.length > 0 ? Math.round((completed / taskList.length) * 100) + "%" : "0%"}
value={
taskList.length > 0
? Math.round((completed / taskList.length) * 100) + "%"
: "0%"
}
subtitle="Of all tasks"
icon={<Activity className="h-4 w-4 text-purple-500" />}
/>
@@ -341,7 +392,8 @@ function TokenUsageCostsSection() {
const { data: sessions, isLoading: loadingSessions } = useUsageSessions(100);
const { data: modelUsage, isLoading: loadingModels } = useModelUsage("24h");
const { data: projection, isLoading: loadingProj } = useUsageProjection();
const { data: cacheStats, isLoading: loadingCache } = useCacheEfficiency("24h");
const { data: cacheStats, isLoading: loadingCache } =
useCacheEfficiency("24h");
const trendUp = (summary?.trend_pct ?? 0) >= 0;
@@ -369,7 +421,11 @@ function TokenUsageCostsSection() {
/>
<SummaryCard
title="Trend vs Prior"
value={summary ? (trendUp ? "+" : "") + summary.trend_pct.toFixed(1) + "%" : undefined}
value={
summary
? (trendUp ? "+" : "") + summary.trend_pct.toFixed(1) + "%"
: undefined
}
icon={
trendUp ? (
<TrendingUp className="h-4 w-4 text-red-500" />
@@ -387,7 +443,11 @@ function TokenUsageCostsSection() {
/>
<SummaryCard
title="Cache Saved"
value={cacheStats ? "$" + cacheStats.cost_saved_by_cache_usd.toFixed(4) : undefined}
value={
cacheStats
? "$" + cacheStats.cost_saved_by_cache_usd.toFixed(4)
: undefined
}
icon={<Sparkles className="h-4 w-4 text-purple-500" />}
isLoading={loadingCache}
/>
@@ -429,11 +489,19 @@ interface SummaryCardProps {
isLoading: boolean;
}
function SummaryCard({ title, value, icon, trend, isLoading }: SummaryCardProps) {
function SummaryCard({
title,
value,
icon,
trend,
isLoading,
}: SummaryCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
{icon}
</CardHeader>
<CardContent>
@@ -479,7 +547,9 @@ function ProjectionCard({ projection, isLoading }: ProjectionCardProps) {
) : (
<div>
<div className="text-3xl font-bold">
{projection != null ? "$" + projection.projected_monthly_cost_usd.toFixed(2) : "—"}
{projection != null
? "$" + projection.projected_monthly_cost_usd.toFixed(2)
: "—"}
</div>
<p className="text-xs text-muted-foreground mt-1">
Based on {projection?.basis_days ?? 7}-day rolling average ($
@@ -497,7 +567,10 @@ interface CacheEfficiencyCardProps {
isLoading: boolean;
}
function CacheEfficiencyCard({ cacheStats, isLoading }: CacheEfficiencyCardProps) {
function CacheEfficiencyCard({
cacheStats,
isLoading,
}: CacheEfficiencyCardProps) {
const pct = cacheStats ? cacheStats.cache_hit_rate * 100 : 0;
return (
@@ -515,8 +588,9 @@ function CacheEfficiencyCard({ cacheStats, isLoading }: CacheEfficiencyCardProps
<div>
<div className="text-3xl font-bold">{pct.toFixed(1)}%</div>
<p className="text-xs text-muted-foreground mt-1">
{cacheStats ? fmtTokens(cacheStats.tokens_cache_read) : "—"} cache reads ·
saved ${cacheStats?.cost_saved_by_cache_usd.toFixed(4) ?? "—"}
{cacheStats ? fmtTokens(cacheStats.tokens_cache_read) : "—"} cache
reads · saved $
{cacheStats?.cost_saved_by_cache_usd.toFixed(4) ?? "—"}
</p>
<Progress value={pct} className="mt-2" />
</div>
@@ -530,7 +604,11 @@ function CacheEfficiencyCard({ cacheStats, isLoading }: CacheEfficiencyCardProps
type MetricsTab = "performance" | "token-usage" | "delivery";
const VALID_METRICS_TABS: MetricsTab[] = ["performance", "token-usage", "delivery"];
const VALID_METRICS_TABS: MetricsTab[] = [
"performance",
"token-usage",
"delivery",
];
function isValidMetricsTab(value: string | null): value is MetricsTab {
return VALID_METRICS_TABS.includes(value as MetricsTab);
@@ -544,7 +622,9 @@ function MetricsPageContent() {
// Read ?tab= from URL, default to "performance"
const rawTab = searchParams.get("tab");
const activeTab: MetricsTab = isValidMetricsTab(rawTab) ? rawTab : "performance";
const activeTab: MetricsTab = isValidMetricsTab(rawTab)
? rawTab
: "performance";
function handleTabChange(value: string) {
const params = new URLSearchParams(searchParams.toString());
@@ -590,32 +670,34 @@ function MetricsPageContent() {
// Wrap in Suspense for useSearchParams
export default function MetricsPage() {
return (
<Suspense fallback={
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-32 mb-2" />
<Skeleton className="h-5 w-72" />
<Suspense
fallback={
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-32 mb-2" />
<Skeleton className="h-5 w-72" />
</div>
</div>
<div className="flex gap-2">
<Skeleton className="h-9 w-28" />
<Skeleton className="h-9 w-28" />
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16" />
</CardContent>
</Card>
))}
</div>
</div>
<div className="flex gap-2">
<Skeleton className="h-9 w-28" />
<Skeleton className="h-9 w-28" />
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16" />
</CardContent>
</Card>
))}
</div>
</div>
}>
}
>
<MetricsPageContent />
</Suspense>
);
@@ -34,21 +34,38 @@ import { toast } from "sonner";
import { Markdown } from "@/components/ui/markdown";
const typeIcons: Record<NotificationType, React.ReactNode> = {
[NotificationType.TASK_ASSIGNMENT]: <ListTodo className="h-4 w-4 text-green-500" />,
[NotificationType.PRIORITY_CHANGE]: <ArrowUpCircle className="h-4 w-4 text-orange-500" />,
[NotificationType.BLOCKER_ESCALATION]: <AlertTriangle className="h-4 w-4 text-red-500" />,
[NotificationType.REVIEW_REQUEST]: <Check className="h-4 w-4 text-purple-500" />,
[NotificationType.DOCUMENTATION_REQUEST]: <Info className="h-4 w-4 text-blue-500" />,
[NotificationType.ALERT]: <AlertTriangle className="h-4 w-4 text-yellow-500" />,
[NotificationType.TASK_ASSIGNMENT]: (
<ListTodo className="h-4 w-4 text-green-500" />
),
[NotificationType.PRIORITY_CHANGE]: (
<ArrowUpCircle className="h-4 w-4 text-orange-500" />
),
[NotificationType.BLOCKER_ESCALATION]: (
<AlertTriangle className="h-4 w-4 text-red-500" />
),
[NotificationType.REVIEW_REQUEST]: (
<Check className="h-4 w-4 text-purple-500" />
),
[NotificationType.DOCUMENTATION_REQUEST]: (
<Info className="h-4 w-4 text-blue-500" />
),
[NotificationType.ALERT]: (
<AlertTriangle className="h-4 w-4 text-yellow-500" />
),
[NotificationType.BROADCAST]: <Bell className="h-4 w-4 text-gray-500" />,
[NotificationType.KNOWLEDGE_SHARE]: <BookOpen className="h-4 w-4 text-cyan-500" />,
[NotificationType.KNOWLEDGE_SHARE]: (
<BookOpen className="h-4 w-4 text-cyan-500" />
),
[NotificationType.MENTION]: <AtSign className="h-4 w-4 text-indigo-500" />,
};
const priorityColors: Record<NotificationPriority, string> = {
[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",
[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 {
@@ -57,23 +74,37 @@ interface NotificationCardProps {
onAcknowledge: () => void;
}
function NotificationCard({ notification, onMarkRead, onAcknowledge }: NotificationCardProps) {
function NotificationCard({
notification,
onMarkRead,
onAcknowledge,
}: NotificationCardProps) {
return (
<Card className={notification.is_read ? "opacity-70" : "border-l-4 border-l-primary"}>
<Card
className={
notification.is_read ? "opacity-70" : "border-l-4 border-l-primary"
}
>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className="mt-1">{typeIcons[notification.type]}</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium">{notification.subject}</span>
<Badge className={priorityColors[notification.priority] + " text-xs"}>
<Badge
className={priorityColors[notification.priority] + " text-xs"}
>
{notification.priority}
</Badge>
{!notification.is_read && (
<Badge variant="secondary" className="text-xs">New</Badge>
<Badge variant="secondary" className="text-xs">
New
</Badge>
)}
{notification.requires_ack && !notification.is_acknowledged && (
<Badge variant="destructive" className="text-xs">Needs Ack</Badge>
<Badge variant="destructive" className="text-xs">
Needs Ack
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground mt-1">
@@ -129,19 +160,21 @@ function NotificationsPageContent() {
}
const { data, isLoading, error, refetch } = useNotifications(
activeTab === "unread" ? { unread_only: true } :
activeTab === "pending" ? { pending_ack_only: true } :
undefined
activeTab === "unread"
? { unread_only: true }
: activeTab === "pending"
? { pending_ack_only: true }
: undefined,
);
const markRead = useMarkNotificationRead();
const acknowledge = useAcknowledgeNotification();
const markAllRead = useMarkAllNotificationsRead();
const isOffline = error && (
error.message?.includes("Network Error") ||
(error as { code?: string })?.code === "ERR_NETWORK"
);
const isOffline =
error &&
(error.message?.includes("Network Error") ||
(error as { code?: string })?.code === "ERR_NETWORK");
const handleMarkRead = async (id: string) => {
try {
@@ -213,7 +246,9 @@ function NotificationsPageContent() {
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">{data.unread_count}</div>
<div className="text-2xl font-bold text-blue-600">
{data.unread_count}
</div>
</CardContent>
</Card>
<Card>
@@ -224,7 +259,9 @@ function NotificationsPageContent() {
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{data.pending_ack_count}</div>
<div className="text-2xl font-bold text-red-600">
{data.pending_ack_count}
</div>
</CardContent>
</Card>
</div>
@@ -244,7 +281,10 @@ function NotificationsPageContent() {
Unread {data && data.unread_count > 0 && `(${data.unread_count})`}
</TabsTrigger>
<TabsTrigger value="pending">
Pending {data && data.pending_ack_count > 0 && `(${data.pending_ack_count})`}
Pending{" "}
{data &&
data.pending_ack_count > 0 &&
`(${data.pending_ack_count})`}
</TabsTrigger>
</TabsList>
@@ -284,41 +324,43 @@ function NotificationsPageContent() {
// Wrap in Suspense for useSearchParams
export default function NotificationsPage() {
return (
<Suspense fallback={
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-48 mb-2" />
<Skeleton className="h-5 w-72" />
<Suspense
fallback={
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-48 mb-2" />
<Skeleton className="h-5 w-72" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-36" />
<Skeleton className="h-9 w-24" />
</div>
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-36" />
<Skeleton className="h-9 w-24" />
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-16" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-12" />
</CardContent>
</Card>
))}
</div>
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<Card key={i}>
<CardContent className="p-4">
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
))}
</div>
</div>
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-16" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-12" />
</CardContent>
</Card>
))}
</div>
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<Card key={i}>
<CardContent className="p-4">
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
))}
</div>
</div>
}>
}
>
<NotificationsPageContent />
</Suspense>
);
+24 -9
View File
@@ -5,7 +5,11 @@ import { useSearchParams, useRouter } from "next/navigation";
import { useProjects } from "@/hooks/use-projects";
import { Team } from "@/types";
import { OfflineState } from "@/components/ui/offline-state";
import { CreateProjectDialog, ProjectFilters, ProjectTable } from "@/components/projects";
import {
CreateProjectDialog,
ProjectFilters,
ProjectTable,
} from "@/components/projects";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { RefreshCw } from "lucide-react";
@@ -19,7 +23,7 @@ function ProjectsPageContent() {
const cellFilterParam = searchParams.get("cell");
const cellFilter = useMemo(
() => (cellFilterParam?.split(",").filter(Boolean) as Team[]) || [],
[cellFilterParam]
[cellFilterParam],
);
const showInactive = searchParams.get("inactive") === "true";
@@ -37,32 +41,37 @@ function ProjectsPageContent() {
const query = params.toString();
router.push(query ? `/projects?${query}` : "/projects");
},
[router, searchParams]
[router, searchParams],
);
const handleSearchChange = useCallback(
(value: string) => {
updateParams({ q: value || null });
},
[updateParams]
[updateParams],
);
const handleCellChange = useCallback(
(value: Team[]) => {
updateParams({ cell: value.length > 0 ? value.join(",") : null });
},
[updateParams]
[updateParams],
);
const handleShowInactiveChange = useCallback(
(value: boolean) => {
updateParams({ inactive: value ? "true" : null });
},
[updateParams]
[updateParams],
);
// Fetch projects
const { data: projects, isLoading, error, refetch } = useProjects({
const {
data: projects,
isLoading,
error,
refetch,
} = useProjects({
active_only: !showInactive,
});
@@ -72,12 +81,18 @@ function ProjectsPageContent() {
return projects.filter((project) => {
// Search filter
if (searchQuery && !project.name.toLowerCase().includes(searchQuery.toLowerCase())) {
if (
searchQuery &&
!project.name.toLowerCase().includes(searchQuery.toLowerCase())
) {
return false;
}
// Cell filter (if any selected, project must match one of them)
if (cellFilter.length > 0 && !cellFilter.includes(project.assigned_cell)) {
if (
cellFilter.length > 0 &&
!cellFilter.includes(project.assigned_cell)
) {
return false;
}
+23 -18
View File
@@ -3,7 +3,13 @@
import { useState } from "react";
import { useTheme } from "next-themes";
import { useUIStore } from "@/store";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -16,14 +22,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import {
Settings,
Palette,
Bell,
Server,
User,
Save,
} from "lucide-react";
import { Settings, Palette, Bell, Server, User, Save } from "lucide-react";
import { toast } from "sonner";
import { API_URL, WS_URL } from "@/lib/constants";
import { TranscriptRetentionCard } from "@/components/settings/transcript-retention-card";
@@ -69,11 +68,15 @@ export default function SettingsPage() {
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<div className="h-16 w-16 rounded-full bg-primary flex items-center justify-center">
<span className="text-primary-foreground font-bold text-2xl">CEO</span>
<span className="text-primary-foreground font-bold text-2xl">
CEO
</span>
</div>
<div>
<p className="font-semibold text-lg">Renzo</p>
<p className="text-sm text-muted-foreground">Chief Executive Officer</p>
<p className="text-sm text-muted-foreground">
Chief Executive Officer
</p>
<p className="text-xs text-muted-foreground mt-1">
Agent ID: 00000000-0000-0000-0000-000000000001
</p>
@@ -89,7 +92,9 @@ export default function SettingsPage() {
<Palette className="h-5 w-5" />
Appearance
</CardTitle>
<CardDescription>Customize the look and feel of the panel</CardDescription>
<CardDescription>
Customize the look and feel of the panel
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
@@ -143,10 +148,7 @@ export default function SettingsPage() {
Automatically refresh data periodically
</p>
</div>
<Switch
checked={autoRefresh}
onCheckedChange={setAutoRefresh}
/>
<Switch checked={autoRefresh} onCheckedChange={setAutoRefresh} />
</div>
<Separator />
<div className="flex items-center justify-between">
@@ -224,7 +226,9 @@ export default function SettingsPage() {
<Settings className="h-5 w-5" />
Connection Info
</CardTitle>
<CardDescription>Backend API configuration (read-only)</CardDescription>
<CardDescription>
Backend API configuration (read-only)
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
@@ -236,7 +240,8 @@ export default function SettingsPage() {
<Input value={WS_URL} readOnly className="bg-muted" />
</div>
<p className="text-xs text-muted-foreground">
These values are configured via environment variables (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WS_URL)
These values are configured via environment variables
(NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WS_URL)
</p>
</CardContent>
</Card>
@@ -6,7 +6,11 @@ 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";
import { TaskHeader, TaskMetadata, TaskTabs } from "@/components/tasks/task-detail";
import {
TaskHeader,
TaskMetadata,
TaskTabs,
} from "@/components/tasks/task-detail";
import { ApproveAndStartButton } from "@/components/tasks/approve-and-start-button";
import {
EscalateToCeoDialog,
@@ -42,7 +46,8 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
// Dialog states
const [escalateDialogOpen, setEscalateDialogOpen] = useState(false);
const [approveAndMergeDialogOpen, setApproveAndMergeDialogOpen] = useState(false);
const [approveAndMergeDialogOpen, setApproveAndMergeDialogOpen] =
useState(false);
const [approveDialogOpen, setApproveDialogOpen] = useState(false);
const [rejectDialogOpen, setRejectDialogOpen] = useState(false);
const [branchDialogOpen, setBranchDialogOpen] = useState(false);
@@ -51,7 +56,8 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
const [passQaDialogOpen, setPassQaDialogOpen] = useState(false);
const [failQaDialogOpen, setFailQaDialogOpen] = useState(false);
const [docsCompleteDialogOpen, setDocsCompleteDialogOpen] = useState(false);
const [submitPmReviewDialogOpen, setSubmitPmReviewDialogOpen] = useState(false);
const [submitPmReviewDialogOpen, setSubmitPmReviewDialogOpen] =
useState(false);
const [completeDialogOpen, setCompleteDialogOpen] = useState(false);
const handleAction = async (action: string) => {
@@ -141,7 +147,10 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
setEscalateDialogOpen(true);
return; // Don't refetch yet, dialog will handle it
case "request-changes":
await lifecycle.failQa.mutateAsync({ taskId: task.id, qaNotes: "Changes requested by PM" });
await lifecycle.failQa.mutateAsync({
taskId: task.id,
qaNotes: "Changes requested by PM",
});
toast.success("Changes requested");
break;
case "create-branch":
@@ -221,11 +230,21 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
refetch();
} catch (err) {
if (axios.isAxiosError(err)) {
const detail = (err.response?.data as { detail?: string } | undefined)?.detail ?? "";
const detail =
(err.response?.data as { detail?: string } | undefined)?.detail ?? "";
if (typeof detail === "string" && detail.startsWith("NO_PR")) {
toast.error("No PR found for this task. Create a pull request before merging.");
} else if (typeof detail === "string" && detail.startsWith("Merge failed")) {
toast.error("Merge failed: " + (detail.slice("Merge failed".length).replace(/^[: ]+/, "") || "the merge could not be completed"));
toast.error(
"No PR found for this task. Create a pull request before merging.",
);
} else if (
typeof detail === "string" &&
detail.startsWith("Merge failed")
) {
toast.error(
"Merge failed: " +
(detail.slice("Merge failed".length).replace(/^[: ]+/, "") ||
"the merge could not be completed"),
);
} else {
toast.error("Failed to approve and merge task");
}
@@ -333,7 +352,12 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
await createBranch.mutateAsync({
project_slug: project.slug,
task_id: task.id,
branch_type: branchType as "feature" | "bug" | "chore" | "docs" | "hotfix",
branch_type: branchType as
| "feature"
| "bug"
| "chore"
| "docs"
| "hotfix",
agent_id: "ceo", // CEO is creating the branch from the panel
});
toast.success("Branch created successfully");
@@ -403,7 +427,8 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
<AlertTriangle className="h-16 w-16 mx-auto mb-4 text-destructive" />
<h2 className="text-xl font-semibold mb-2">Task Not Found</h2>
<p className="text-muted-foreground mb-6">
{error?.message ?? "The task you're looking for doesn't exist or has been deleted."}
{error?.message ??
"The task you're looking for doesn't exist or has been deleted."}
</p>
<div className="flex justify-center gap-4">
<Button variant="outline" onClick={() => refetch()}>
@@ -5,7 +5,10 @@ import { useSearchParams, useRouter } from "next/navigation";
import { useWorkSessions } from "@/hooks/use-work-sessions";
import { WorkSessionStatus } from "@/types";
import { OfflineState } from "@/components/ui/offline-state";
import { WorkSessionTable, WorkSessionFilters } from "@/components/work-sessions";
import {
WorkSessionTable,
WorkSessionFilters,
} from "@/components/work-sessions";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { RefreshCw } from "lucide-react";
@@ -18,8 +21,9 @@ function WorkSessionsPageContent() {
const searchQuery = searchParams.get("q") || "";
const statusParam = searchParams.get("status");
const statusFilter = useMemo(
() => (statusParam?.split(",").filter(Boolean) as WorkSessionStatus[]) || [],
[statusParam]
() =>
(statusParam?.split(",").filter(Boolean) as WorkSessionStatus[]) || [],
[statusParam],
);
// Update URL params
@@ -36,21 +40,21 @@ function WorkSessionsPageContent() {
const query = params.toString();
router.push(query ? `/work-sessions?${query}` : "/work-sessions");
},
[router, searchParams]
[router, searchParams],
);
const handleSearchChange = useCallback(
(value: string) => {
updateParams({ q: value || null });
},
[updateParams]
[updateParams],
);
const handleStatusChange = useCallback(
(value: WorkSessionStatus[]) => {
updateParams({ status: value.length > 0 ? value.join(",") : null });
},
[updateParams]
[updateParams],
);
// Fetch work sessions