214 lines
6.9 KiB
TypeScript
214 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import { memo, useEffect, useMemo, useState, useCallback } from "react";
|
|
import { IconX, IconChevronDown, IconPlayerPlay, IconPlus } from "@tabler/icons-react";
|
|
import { TabsContent } from "@kandev/ui/tabs";
|
|
import { Button } from "@kandev/ui/button";
|
|
import { SessionPanel, SessionPanelContent } from "@kandev/ui/pannel-session";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@kandev/ui/dropdown-menu";
|
|
import { useAppStore } from "@/components/state-provider";
|
|
import { useLayoutStore } from "@/lib/state/layout-store";
|
|
// closeDocument kept for mobile/tablet backward compat
|
|
import { getWebSocketClient } from "@/lib/ws/connection";
|
|
import { useTask } from "@/hooks/use-task";
|
|
import { SessionTabs, type SessionTab } from "@/components/session-tabs";
|
|
import { TaskPlanPanel } from "../task-plan-panel";
|
|
import { TaskCreateDialog } from "@/components/task-create-dialog";
|
|
import type { ActiveDocument } from "@/lib/state/slices/ui/types";
|
|
|
|
type DocumentPanelProps = {
|
|
sessionId: string | null;
|
|
};
|
|
|
|
export const DocumentPanel = memo(function DocumentPanel({ sessionId }: DocumentPanelProps) {
|
|
const activeDocument = useAppStore((state) =>
|
|
sessionId ? (state.documentPanel.activeDocumentBySessionId[sessionId] ?? null) : null,
|
|
);
|
|
const setActiveDocument = useAppStore((state) => state.setActiveDocument);
|
|
const closeDocument = useLayoutStore((state) => state.closeDocument);
|
|
|
|
const handleClose = () => {
|
|
if (!sessionId) return;
|
|
closeDocument(sessionId);
|
|
setActiveDocument(sessionId, null);
|
|
};
|
|
|
|
// Auto-close document layout when no document is active (e.g. stale localStorage)
|
|
useEffect(() => {
|
|
if (!activeDocument && sessionId) {
|
|
closeDocument(sessionId);
|
|
}
|
|
}, [activeDocument, sessionId, closeDocument]);
|
|
|
|
const tabs: SessionTab[] = useMemo(() => {
|
|
if (!activeDocument) return [];
|
|
if (activeDocument.type === "plan") {
|
|
return [{ id: "plan", label: "Plan" }];
|
|
}
|
|
return [{ id: "file", label: activeDocument.name }];
|
|
}, [activeDocument]);
|
|
|
|
const activeTab = activeDocument?.type === "plan" ? "plan" : "file";
|
|
|
|
if (!activeDocument) {
|
|
return null;
|
|
}
|
|
|
|
const isPlan = activeDocument.type === "plan";
|
|
|
|
const headerActions = (
|
|
<div className="flex items-center gap-0.5">
|
|
{isPlan && <ImplementPlanDropdown sessionId={sessionId} onClose={handleClose} />}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon-sm" className="cursor-pointer" onClick={handleClose}>
|
|
<IconX className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Close document panel</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<SessionPanel borderSide="right" margin="right">
|
|
<SessionTabs
|
|
tabs={tabs}
|
|
activeTab={activeTab}
|
|
onTabChange={() => {}}
|
|
className="flex-1 min-h-0 flex flex-col gap-2"
|
|
rightContent={headerActions}
|
|
>
|
|
<TabsContent value="plan" className="flex-1 min-h-0">
|
|
<DocumentPlanContent />
|
|
</TabsContent>
|
|
<TabsContent value="file" className="flex-1 min-h-0">
|
|
<DocumentFileContent doc={activeDocument} />
|
|
</TabsContent>
|
|
</SessionTabs>
|
|
</SessionPanel>
|
|
);
|
|
});
|
|
|
|
/** Dropdown button for implementing the plan */
|
|
const ImplementPlanDropdown = memo(function ImplementPlanDropdown({
|
|
sessionId,
|
|
onClose,
|
|
}: {
|
|
sessionId: string | null;
|
|
onClose: () => void;
|
|
}) {
|
|
const taskId = useAppStore((state) => state.tasks.activeTaskId);
|
|
const task = useTask(taskId);
|
|
const plan = useAppStore((state) => (taskId ? (state.taskPlans.byTaskId[taskId] ?? null) : null));
|
|
const [showNewSessionDialog, setShowNewSessionDialog] = useState(false);
|
|
|
|
const handleImplement = useCallback(async () => {
|
|
if (!taskId || !sessionId || !plan?.content) return;
|
|
const client = getWebSocketClient();
|
|
if (!client) return;
|
|
|
|
// Close plan mode first
|
|
onClose();
|
|
|
|
// Send the plan as an implementation prompt (not in plan mode)
|
|
try {
|
|
await client.request(
|
|
"message.add",
|
|
{
|
|
task_id: taskId,
|
|
session_id: sessionId,
|
|
content: `Implement the following plan:\n\n${plan.content}`,
|
|
},
|
|
10000,
|
|
);
|
|
} catch (err) {
|
|
console.error("Failed to send implement plan message:", err);
|
|
}
|
|
}, [taskId, sessionId, plan, onClose]);
|
|
|
|
const handleImplementNewSession = useCallback(() => {
|
|
// Close plan mode and open the new session dialog
|
|
onClose();
|
|
setShowNewSessionDialog(true);
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 gap-1 px-2 cursor-pointer text-xs hover:bg-muted/40"
|
|
onClick={handleImplement}
|
|
disabled={!plan?.content}
|
|
>
|
|
<IconPlayerPlay className="h-3 w-3" />
|
|
Implement
|
|
</Button>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-5 p-0 cursor-pointer hover:bg-muted/40"
|
|
disabled={!plan?.content}
|
|
>
|
|
<IconChevronDown className="h-3 w-3" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-64">
|
|
<DropdownMenuItem onClick={handleImplementNewSession} className="cursor-pointer">
|
|
<IconPlus className="h-4 w-4 mr-2 shrink-0 self-start mt-0.5" />
|
|
<div>
|
|
<div>Implement in a new session</div>
|
|
<div className="text-[11px] text-muted-foreground font-normal">
|
|
Starts fresh with a clean context window
|
|
</div>
|
|
</div>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
<TaskCreateDialog
|
|
open={showNewSessionDialog}
|
|
onOpenChange={setShowNewSessionDialog}
|
|
mode="session"
|
|
workspaceId={null}
|
|
workflowId={null}
|
|
defaultStepId={null}
|
|
steps={[]}
|
|
taskId={taskId}
|
|
initialValues={{
|
|
title: task?.title ?? "",
|
|
description: plan?.content ? `Implement the following plan:\n\n${plan.content}` : "",
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
});
|
|
|
|
const DocumentPlanContent = memo(function DocumentPlanContent() {
|
|
const activeTaskId = useAppStore((state) => state.tasks.activeTaskId);
|
|
return <TaskPlanPanel taskId={activeTaskId} visible />;
|
|
});
|
|
|
|
const DocumentFileContent = memo(function DocumentFileContent({ doc }: { doc: ActiveDocument }) {
|
|
if (doc.type === "plan") return null;
|
|
|
|
return (
|
|
<SessionPanelContent>
|
|
<div className="flex h-full items-center justify-center text-muted-foreground text-sm">
|
|
File viewer: {doc.path}
|
|
</div>
|
|
</SessionPanelContent>
|
|
);
|
|
});
|