diff --git a/apps/ui/package.json b/apps/ui/package.json
index e66433fdc..5a5ac3490 100644
--- a/apps/ui/package.json
+++ b/apps/ui/package.json
@@ -102,6 +102,8 @@
"react-markdown": "10.1.0",
"react-resizable-panels": "3.0.6",
"rehype-raw": "7.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1",
"sonner": "2.0.7",
"tailwind-merge": "3.4.0",
"usehooks-ts": "3.1.1",
diff --git a/apps/ui/src/components/ui/markdown.tsx b/apps/ui/src/components/ui/markdown.tsx
index 1d4f8ef9d..ff7facbf1 100644
--- a/apps/ui/src/components/ui/markdown.tsx
+++ b/apps/ui/src/components/ui/markdown.tsx
@@ -1,13 +1,97 @@
-import ReactMarkdown from 'react-markdown';
+import ReactMarkdown, { Components } from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
+import remarkGfm from 'remark-gfm';
import { cn } from '@/lib/utils';
+import { Square, CheckSquare } from 'lucide-react';
interface MarkdownProps {
children: string;
className?: string;
}
+/**
+ * Renders a tasks code block as a proper task list with checkboxes
+ */
+function TasksBlock({ content }: { content: string }) {
+ const lines = content.split('\n');
+
+ return (
+
+ {lines.map((line, idx) => {
+ const trimmed = line.trim();
+
+ // Check for phase/section headers (## Phase 1: ...)
+ const headerMatch = trimmed.match(/^##\s+(.+)$/);
+ if (headerMatch) {
+ return (
+
+ {headerMatch[1]}
+
+ );
+ }
+
+ // Check for task items (- [ ] or - [x])
+ const taskMatch = trimmed.match(/^-\s*\[([ xX])\]\s*(.+)$/);
+ if (taskMatch) {
+ const isChecked = taskMatch[1].toLowerCase() === 'x';
+ const taskText = taskMatch[2];
+
+ return (
+
+ {isChecked ? (
+
+ ) : (
+
+ )}
+
+ {taskText}
+
+
+ );
+ }
+
+ // Empty lines
+ if (!trimmed) {
+ return
;
+ }
+
+ // Other content (render as-is)
+ return (
+
+ {trimmed}
+
+ );
+ })}
+
+ );
+}
+
+/**
+ * Custom components for ReactMarkdown
+ */
+const markdownComponents: Components = {
+ // Handle code blocks - special case for 'tasks' language
+ code({ className, children }) {
+ const match = /language-(\w+)/.exec(className || '');
+ const language = match ? match[1] : '';
+ const content = String(children).replace(/\n$/, '');
+
+ // Special handling for tasks code blocks
+ if (language === 'tasks') {
+ return ;
+ }
+
+ // Regular code (inline or block)
+ return {children};
+ },
+};
+
/**
* Reusable Markdown component for rendering markdown content
* Theme-aware styling that adapts to all predefined themes
@@ -42,10 +126,20 @@ export function Markdown({ children, className }: MarkdownProps) {
'[&_hr]:border-border [&_hr]:my-4',
// Images
'[&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-lg [&_img]:my-2 [&_img]:border [&_img]:border-border',
+ // Tables
+ '[&_table]:w-full [&_table]:border-collapse [&_table]:my-4',
+ '[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:text-foreground [&_th]:font-semibold',
+ '[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2 [&_td]:text-foreground-secondary',
className
)}
>
- {children}
+
+ {children}
+
);
}
diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx
index d49d408e0..f0e64102d 100644
--- a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx
+++ b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx
@@ -11,7 +11,7 @@ import {
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
-import { Markdown } from '@/components/ui/markdown';
+import { PlanContentViewer } from './plan-content-viewer';
import { Label } from '@/components/ui/label';
import { Feature } from '@/store/app-store';
import { Check, RefreshCw, Edit2, Eye } from 'lucide-react';
@@ -42,6 +42,10 @@ export function PlanApprovalDialog({
const [editedPlan, setEditedPlan] = useState(planContent);
const [showRejectFeedback, setShowRejectFeedback] = useState(false);
const [rejectFeedback, setRejectFeedback] = useState('');
+ const [showFullDescription, setShowFullDescription] = useState(false);
+
+ const DESCRIPTION_LIMIT = 250;
+ const TITLE_LIMIT = 50;
// Reset state when dialog opens or plan content changes
useEffect(() => {
@@ -50,6 +54,7 @@ export function PlanApprovalDialog({
setIsEditMode(false);
setShowRejectFeedback(false);
setRejectFeedback('');
+ setShowFullDescription(false);
}
}, [open, planContent]);
@@ -82,15 +87,31 @@ export function PlanApprovalDialog({