diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..0524676
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,41 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(pnpm --version:*)",
+ "Bash(docker:*)",
+ "Bash(node:*)",
+ "Bash(echo:*)",
+ "Bash(pnpm install:*)",
+ "Bash(curl:*)",
+ "Bash(sudo sh:*)",
+ "Bash(sudo usermod:*)",
+ "Bash(npx convex deploy)",
+ "Bash(pnpm dev)",
+ "Bash(sudo docker compose ps:*)",
+ "Bash(python3:*)",
+ "Bash(git commit:*)",
+ "Bash(openclaw:*)",
+ "Bash(dpkg:*)",
+ "Bash(snap list:*)",
+ "Bash(apt list:*)",
+ "Bash(pnpm --filter @clawe/shared test:*)",
+ "Bash(pnpm check:*)",
+ "Bash(pnpm prettier:*)",
+ "Bash(pnpm build:*)",
+ "Bash(pnpm test:*)",
+ "Bash(npx convex run:*)",
+ "Bash(npx convex dev:*)",
+ "Bash(pnpm add:*)",
+ "Bash(pnpm run format:check:*)",
+ "Bash(git -C /home/ubuntu/clawe config user.name)",
+ "Bash(git -C /home/ubuntu/clawe config user.email)",
+ "Bash(git -C /home/ubuntu/clawe config:*)",
+ "Bash(git -C /home/ubuntu/clawe log:*)",
+ "Bash(pnpm --filter web test:*)",
+ "Bash(npx prettier:*)",
+ "Bash(pnpm --filter @clawe/cli test:*)",
+ "Bash(pnpm ls:*)",
+ "Bash(pnpm fix:*)"
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
index 5f91ca7..d7f1edd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,4 +50,7 @@ docker/openclaw/data/
.data/
# OpenClaw state directory (shared with Docker in dev)
-.openclaw/
\ No newline at end of file
+.openclaw/*
+!.openclaw/.gitkeep
+.openclaw/logs/*
+!.openclaw/logs/.gitkeep
\ No newline at end of file
diff --git a/.openclaw/.gitkeep b/.openclaw/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/apps/web/src/app/(dashboard)/board/page.tsx b/apps/web/src/app/(dashboard)/board/page.tsx
index 6a027dd..5b7ad1d 100644
--- a/apps/web/src/app/(dashboard)/board/page.tsx
+++ b/apps/web/src/app/(dashboard)/board/page.tsx
@@ -20,6 +20,7 @@ import {
import {
KanbanBoard,
type KanbanTask,
+ type KanbanSubtask,
type KanbanColumnDef,
} from "@/components/kanban";
import { LiveFeed, LiveFeedTitle } from "@/components/live-feed";
@@ -42,16 +43,21 @@ function mapPriority(priority?: string): "low" | "medium" | "high" {
// Map Convex task to Kanban task format
function mapTask(task: TaskWithAssignees): KanbanTask {
- const subtasks: KanbanTask[] =
- task.subtasks
- ?.filter((st) => !st.done)
- .map((st, i) => ({
- id: `${task._id}-${i}`,
- title: st.title,
- description: st.description,
- priority: "medium",
- subtasks: [],
- })) || [];
+ const subtasks: KanbanSubtask[] =
+ task.subtasks?.map((st, i) => ({
+ id: `${task._id}-${i}`,
+ title: st.title,
+ description: st.description,
+ done: st.done || false,
+ status: st.status ?? (st.done ? "done" : "pending"),
+ blockedReason: st.blockedReason,
+ assignee: (() => {
+ if (!st.assigneeId) return undefined;
+ const agent = task.assignees?.find((a) => a._id === st.assigneeId);
+ return agent ? `${agent.emoji || ""} ${agent.name}`.trim() : undefined;
+ })(),
+ doneAt: st.doneAt,
+ })) || [];
return {
id: task._id,
diff --git a/apps/web/src/components/kanban/_components/document-viewer-modal.tsx b/apps/web/src/components/kanban/_components/document-viewer-modal.tsx
index 1f83c94..c601243 100644
--- a/apps/web/src/components/kanban/_components/document-viewer-modal.tsx
+++ b/apps/web/src/components/kanban/_components/document-viewer-modal.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState } from "react";
+import { useEffect, useState, useMemo } from "react";
import axios from "axios";
import {
Dialog,
@@ -16,6 +16,51 @@ import type { DocumentWithCreator } from "@clawe/backend/types";
const VIEWER_HEIGHT = "h-[500px]";
+/** Very simple markdown → HTML renderer for preview mode */
+function renderMarkdown(md: string): string {
+ const html = md
+ // Escape HTML
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ // Headers
+ .replace(
+ /^#### (.+)$/gm,
+ '
$1
',
+ )
+ .replace(
+ /^### (.+)$/gm,
+ '$1
',
+ )
+ .replace(/^## (.+)$/gm, '$1
')
+ .replace(/^# (.+)$/gm, '$1
')
+ // Bold & italic
+ .replace(/\*\*(.+?)\*\*/g, "$1")
+ .replace(/\*(.+?)\*/g, "$1")
+ // Inline code
+ .replace(
+ /`([^`]+)`/g,
+ '$1',
+ )
+ // Unordered lists
+ .replace(/^- (.+)$/gm, '$1')
+ // Ordered lists
+ .replace(/^\d+\. (.+)$/gm, '$1')
+ // Horizontal rules
+ .replace(
+ /^---$/gm,
+ '
',
+ )
+ // Paragraphs (double newlines)
+ .replace(/\n\n/g, '')
+ // Single newlines within paragraphs
+ .replace(/\n/g, "
");
+
+ return `
${html}
`;
+}
+
+type ViewMode = "preview" | "raw";
+
export type DocumentViewerModalProps = {
document: DocumentWithCreator | null;
open: boolean;
@@ -29,11 +74,13 @@ export const DocumentViewerModal = ({
}: DocumentViewerModalProps) => {
const [fileContent, setFileContent] = useState(null);
const [isLoading, setIsLoading] = useState(false);
+ const [viewMode, setViewMode] = useState("preview");
useEffect(() => {
const fileUrl = document?.fileUrl;
if (!fileUrl || !open) {
setFileContent(null);
+ setViewMode("preview");
return;
}
@@ -63,9 +110,14 @@ export const DocumentViewerModal = ({
};
}, [document?.fileUrl, open]);
- if (!document) return null;
+ const content = fileContent ?? document?.content;
- const content = fileContent ?? document.content;
+ const previewHtml = useMemo(() => {
+ if (!content) return "";
+ return renderMarkdown(content);
+ }, [content]);
+
+ if (!document) return null;
return (