diff --git a/apps/web/src/app/(dashboard)/board/_components/agents-panel/agents-panel-item.tsx b/apps/web/src/app/(dashboard)/board/_components/agents-panel/agents-panel-item.tsx
index 9e07b5c..8b3fb28 100644
--- a/apps/web/src/app/(dashboard)/board/_components/agents-panel/agents-panel-item.tsx
+++ b/apps/web/src/app/(dashboard)/board/_components/agents-panel/agents-panel-item.tsx
@@ -6,6 +6,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@clawe/ui/components/tooltip";
+import type { Agent } from "@clawe/backend/types";
type AgentStatus = "idle" | "active" | "blocked";
@@ -68,14 +69,7 @@ const getAvatarColor = (name: string) => {
};
export type AgentsPanelItemProps = {
- agent: {
- _id: string;
- name: string;
- emoji?: string;
- role: string;
- status: string;
- lastSeen?: number;
- };
+ agent: Agent;
collapsed?: boolean;
selected?: boolean;
onToggle?: () => void;
diff --git a/apps/web/src/app/(dashboard)/board/_components/agents-panel/agents-panel-list.tsx b/apps/web/src/app/(dashboard)/board/_components/agents-panel/agents-panel-list.tsx
index 9bdd743..b77c113 100644
--- a/apps/web/src/app/(dashboard)/board/_components/agents-panel/agents-panel-list.tsx
+++ b/apps/web/src/app/(dashboard)/board/_components/agents-panel/agents-panel-list.tsx
@@ -2,17 +2,9 @@
import { ScrollArea } from "@clawe/ui/components/scroll-area";
import { cn } from "@clawe/ui/lib/utils";
+import type { Agent } from "@clawe/backend/types";
import { AgentsPanelItem } from "./agents-panel-item";
-type Agent = {
- _id: string;
- name: string;
- emoji?: string;
- role: string;
- status: string;
- lastSeen?: number;
-};
-
export type AgentsPanelListProps = {
agents: Agent[];
collapsed?: boolean;
diff --git a/apps/web/src/app/(dashboard)/board/page.tsx b/apps/web/src/app/(dashboard)/board/page.tsx
index 86622f2..29a6ae6 100644
--- a/apps/web/src/app/(dashboard)/board/page.tsx
+++ b/apps/web/src/app/(dashboard)/board/page.tsx
@@ -3,6 +3,7 @@
import { useState } from "react";
import { useQuery } from "convex/react";
import { api } from "@clawe/backend";
+import type { TaskWithAssignees } from "@clawe/backend/types";
import { Bell } from "lucide-react";
import { Button } from "@clawe/ui/components/button";
import {
@@ -38,17 +39,8 @@ function mapPriority(priority?: string): "low" | "medium" | "high" {
}
}
-type ConvexTask = {
- _id: string;
- title: string;
- description?: string;
- priority?: string;
- assignees?: { _id: string; name: string; emoji?: string }[];
- subtasks?: { title: string; description?: string; done: boolean }[];
-};
-
// Map Convex task to Kanban task format
-function mapTask(task: ConvexTask): KanbanTask {
+function mapTask(task: TaskWithAssignees): KanbanTask {
const subtasks: KanbanTask[] =
task.subtasks
?.filter((st) => !st.done)
@@ -69,6 +61,7 @@ function mapTask(task: ConvexTask): KanbanTask {
? `${task.assignees[0].emoji || ""} ${task.assignees[0].name}`.trim()
: undefined,
subtasks,
+ documentCount: task.documentCount,
};
}
diff --git a/apps/web/src/components/chat/chat-message.tsx b/apps/web/src/components/chat/chat-message.tsx
index 0b43445..c37eba1 100644
--- a/apps/web/src/components/chat/chat-message.tsx
+++ b/apps/web/src/components/chat/chat-message.tsx
@@ -31,13 +31,13 @@ export const ChatMessage = ({ message, className }: ChatMessageProps) => {
if (isContext) {
return (
-
+
Context
-
+
{
{children}
),
- code: ({ children, ...props }) => (
- {
+ const isInline = !codeClassName;
+ return isInline ? (
+
+ {children}
+
+ ) : (
+
+ {children}
+
+ );
+ },
+ pre: ({ children, ...props }) => (
+
{children}
-
+
),
}}
>
@@ -108,22 +123,24 @@ export const ChatMessage = ({ message, className }: ChatMessageProps) => {
{/* Message Content */}
{isUser ? (
-
{message.content}
+
+ {message.content}
+
) : (
-
+
+
+
+ Loading messages...
+
+
+ );
+ }
+
+ if (!hasMessages && !isStreaming) {
return
;
}
diff --git a/apps/web/src/components/kanban/_components/document-viewer-modal.tsx b/apps/web/src/components/kanban/_components/document-viewer-modal.tsx
new file mode 100644
index 0000000..1f83c94
--- /dev/null
+++ b/apps/web/src/components/kanban/_components/document-viewer-modal.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import axios from "axios";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@clawe/ui/components/dialog";
+import { Button } from "@clawe/ui/components/button";
+import { Download } from "lucide-react";
+import { Spinner } from "@clawe/ui/components/spinner";
+import { cn } from "@clawe/ui/lib/utils";
+import type { DocumentWithCreator } from "@clawe/backend/types";
+
+const VIEWER_HEIGHT = "h-[500px]";
+
+export type DocumentViewerModalProps = {
+ document: DocumentWithCreator | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+};
+
+export const DocumentViewerModal = ({
+ document,
+ open,
+ onOpenChange,
+}: DocumentViewerModalProps) => {
+ const [fileContent, setFileContent] = useState
(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ const fileUrl = document?.fileUrl;
+ if (!fileUrl || !open) {
+ setFileContent(null);
+ return;
+ }
+
+ const controller = new AbortController();
+
+ const fetchContent = async () => {
+ setIsLoading(true);
+ try {
+ const response = await axios.get(fileUrl, {
+ responseType: "text",
+ signal: controller.signal,
+ });
+ setFileContent(response.data);
+ } catch (error) {
+ if (!axios.isCancel(error)) {
+ setFileContent(null);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ void fetchContent();
+
+ return () => {
+ controller.abort();
+ };
+ }, [document?.fileUrl, open]);
+
+ if (!document) return null;
+
+ const content = fileContent ?? document.content;
+
+ return (
+
+ );
+};
diff --git a/apps/web/src/components/kanban/_components/documents-section.tsx b/apps/web/src/components/kanban/_components/documents-section.tsx
new file mode 100644
index 0000000..7d2b03e
--- /dev/null
+++ b/apps/web/src/components/kanban/_components/documents-section.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { useQuery } from "convex/react";
+import { api } from "@clawe/backend";
+import type { Id } from "@clawe/backend/dataModel";
+import type { DocumentWithCreator } from "@clawe/backend/types";
+import { FileText, Download, Eye } from "lucide-react";
+import { Button } from "@clawe/ui/components/button";
+
+export type DocumentsSectionProps = {
+ taskId: string;
+ onViewDocument: (doc: DocumentWithCreator) => void;
+};
+
+export const DocumentsSection = ({
+ taskId,
+ onViewDocument,
+}: DocumentsSectionProps) => {
+ const documents = useQuery(api.documents.getForTask, {
+ taskId: taskId as Id<"tasks">,
+ });
+
+ // Filter to only show deliverables
+ const deliverables = documents?.filter((d) => d.type === "deliverable") ?? [];
+
+ if (deliverables.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ Documents ({deliverables.length})
+
+
+ {deliverables.map((doc) => (
+ -
+
+
+ {doc.title}
+
+
+ {doc.fileUrl && (
+ <>
+
+
+ >
+ )}
+
+
+ ))}
+
+
+ );
+};
diff --git a/apps/web/src/components/kanban/kanban-card.tsx b/apps/web/src/components/kanban/kanban-card.tsx
index 1fbd188..a9bc63b 100644
--- a/apps/web/src/components/kanban/kanban-card.tsx
+++ b/apps/web/src/components/kanban/kanban-card.tsx
@@ -1,7 +1,13 @@
"use client";
import { useState } from "react";
-import { ChevronDown, ChevronRight, AlignLeft, User } from "lucide-react";
+import {
+ ChevronDown,
+ ChevronRight,
+ AlignLeft,
+ User,
+ FileText,
+} from "lucide-react";
import { cn } from "@clawe/ui/lib/utils";
import {
Popover,
@@ -120,21 +126,35 @@ export const KanbanCard = ({
)}
- {/* Subtask toggle */}
- {hasSubtasks && !isSubtask && (
-
- )}
+ {/* Subtask toggle and document badge */}
+ {(hasSubtasks || (task.documentCount && task.documentCount > 0)) &&
+ !isSubtask && (
+
+ {hasSubtasks && (
+
+ )}
+
+ {task.documentCount && task.documentCount > 0 && (
+
+
+ {task.documentCount} document
+ {task.documentCount !== 1 && "s"}
+
+ )}
+
+ )}
{/* Expanded subtasks */}
diff --git a/apps/web/src/components/kanban/task-detail-modal.tsx b/apps/web/src/components/kanban/task-detail-modal.tsx
index ea820b0..fd70e5b 100644
--- a/apps/web/src/components/kanban/task-detail-modal.tsx
+++ b/apps/web/src/components/kanban/task-detail-modal.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useState } from "react";
import {
Dialog,
DialogContent,
@@ -9,6 +10,9 @@ import {
import { cn } from "@clawe/ui/lib/utils";
import { Circle } from "lucide-react";
import type { KanbanTask } from "./types";
+import type { DocumentWithCreator } from "@clawe/backend/types";
+import { DocumentsSection } from "./_components/documents-section";
+import { DocumentViewerModal } from "./_components/document-viewer-modal";
const priorityConfig: Record<
KanbanTask["priority"],
@@ -41,63 +45,80 @@ export const TaskDetailModal = ({
open,
onOpenChange,
}: TaskDetailModalProps) => {
+ const [selectedDocument, setSelectedDocument] =
+ useState
(null);
+
if (!task) return null;
const priority = priorityConfig[task.priority];
const hasSubtasks = task.subtasks.length > 0;
return (
-