From c6ea491000fe5f70d68c95cbe539290ff76a19a3 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:52:07 -0500 Subject: [PATCH] Improve export/import UX: rich frontmatter preview, cleaner warnings - Separate terminated agent messages from warnings into info notes (shown with subtle styling instead of amber warning banners) - Clean up warning banner styles for dark mode compatibility (use amber-500/20 borders and amber-500/5 backgrounds) - Parse YAML frontmatter in markdown files and render as structured data cards showing name, title, reportsTo, skills etc. - Apply same warning style cleanup to import page Co-Authored-By: Paperclip --- ui/src/pages/CompanyExport.tsx | 156 +++++++++++++++++++++++++++++++-- ui/src/pages/CompanyImport.tsx | 4 +- 2 files changed, 150 insertions(+), 10 deletions(-) diff --git a/ui/src/pages/CompanyExport.tsx b/ui/src/pages/CompanyExport.tsx index 93f65699..3164ac18 100644 --- a/ui/src/pages/CompanyExport.tsx +++ b/ui/src/pages/CompanyExport.tsx @@ -18,6 +18,7 @@ import { FileText, Folder, FolderOpen, + Info, Package, } from "lucide-react"; @@ -307,6 +308,106 @@ function ExportFileTree({ ); } +// ── Frontmatter helpers ─────────────────────────────────────────────── + +type FrontmatterData = Record; + +function parseFrontmatter(content: string): { data: FrontmatterData; body: string } | null { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + if (!match) return null; + + const data: FrontmatterData = {}; + const rawYaml = match[1]; + const body = match[2]; + + let currentKey: string | null = null; + let currentList: string[] | null = null; + + for (const line of rawYaml.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + // List item under current key + if (trimmed.startsWith("- ") && currentKey) { + if (!currentList) currentList = []; + currentList.push(trimmed.slice(2).trim().replace(/^["']|["']$/g, "")); + continue; + } + + // Flush previous list + if (currentKey && currentList) { + data[currentKey] = currentList; + currentList = null; + currentKey = null; + } + + const kvMatch = trimmed.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/); + if (kvMatch) { + const key = kvMatch[1]; + const val = kvMatch[2].trim().replace(/^["']|["']$/g, ""); + if (val) { + data[key] = val; + currentKey = null; + } else { + currentKey = key; + } + } + } + + // Flush trailing list + if (currentKey && currentList) { + data[currentKey] = currentList; + } + + return Object.keys(data).length > 0 ? { data, body } : null; +} + +const FRONTMATTER_FIELD_LABELS: Record = { + name: "Name", + title: "Title", + kind: "Kind", + reportsTo: "Reports to", + skills: "Skills", + status: "Status", + description: "Description", + priority: "Priority", + assignee: "Assignee", + project: "Project", + targetDate: "Target date", +}; + +function FrontmatterCard({ data }: { data: FrontmatterData }) { + return ( +
+
+ {Object.entries(data).map(([key, value]) => ( +
+
+ {FRONTMATTER_FIELD_LABELS[key] ?? key} +
+
+ {Array.isArray(value) ? ( +
+ {value.map((item) => ( + + {item} + + ))} +
+ ) : ( + {value} + )} +
+
+ ))} +
+
+ ); +} + // ── Preview pane ────────────────────────────────────────────────────── function ExportPreviewPane({ @@ -323,6 +424,7 @@ function ExportPreviewPane({ } const isMarkdown = selectedFile.endsWith(".md"); + const parsed = isMarkdown ? parseFrontmatter(content) : null; return (
@@ -330,7 +432,12 @@ function ExportPreviewPane({
{selectedFile}
- {isMarkdown ? ( + {parsed ? ( + <> + + {parsed.body.trim() && {parsed.body}} + + ) : isMarkdown ? ( {content} ) : (
@@ -408,6 +515,21 @@ export function CompanyExport() {
   const totalFiles = useMemo(() => countFiles(tree), [tree]);
   const selectedCount = checkedFiles.size;
 
+  // Separate info notes (terminated agents) from real warnings
+  const { notes, warnings } = useMemo(() => {
+    if (!exportData) return { notes: [] as string[], warnings: [] as string[] };
+    const notes: string[] = [];
+    const warnings: string[] = [];
+    for (const w of exportData.warnings) {
+      if (/terminated agent/i.test(w)) {
+        notes.push(w);
+      } else {
+        warnings.push(w);
+      }
+    }
+    return { notes, warnings };
+  }, [exportData]);
+
   function handleToggleDir(path: string) {
     setExpandedDirs((prev) => {
       const next = new Set(prev);
@@ -489,9 +611,15 @@ export function CompanyExport() {
             
               {selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
             
-            {exportData.warnings.length > 0 && (
-              
-                {exportData.warnings.length} warning{exportData.warnings.length === 1 ? "" : "s"}
+            {warnings.length > 0 && (
+              
+                {warnings.length} warning{warnings.length === 1 ? "" : "s"}
+              
+            )}
+            {notes.length > 0 && (
+              
+                
+                {notes.length} note{notes.length === 1 ? "" : "s"}
               
             )}
           
@@ -506,11 +634,23 @@ export function CompanyExport() { + {/* Notes (informational, e.g. terminated agents) */} + {notes.length > 0 && ( +
+ +
+ {notes.map((n) => ( +
{n}
+ ))} +
+
+ )} + {/* Warnings */} - {exportData.warnings.length > 0 && ( -
- {exportData.warnings.map((w) => ( -
{w}
+ {warnings.length > 0 && ( +
+ {warnings.map((w) => ( +
{w}
))}
)} diff --git a/ui/src/pages/CompanyImport.tsx b/ui/src/pages/CompanyImport.tsx index c95c82dd..b88c4051 100644 --- a/ui/src/pages/CompanyImport.tsx +++ b/ui/src/pages/CompanyImport.tsx @@ -644,9 +644,9 @@ export function CompanyImport() { {/* Warnings */} {importPreview.warnings.length > 0 && ( -
+
{importPreview.warnings.map((w) => ( -
{w}
+
{w}
))}
)}