Commit 6e9acde29620

Vincent Demeester <vincent@sbr.pm>
2026-02-06 10:44:03
feat(org-todos): display todos as conversation messages
Changed /todos and /todo-search commands to use pi.sendMessage() instead of TUI widgets. Output now appears as markdown messages in the conversation, rendered like LLM responses. Added formatTodoMarkdown() for cleaner markdown formatting with bold state badges, code tags, and emoji date indicators.
1 parent 508bd01
Changed files (1)
dots
pi
agent
extensions
org-todos
dots/pi/agent/extensions/org-todos/index.ts
@@ -119,6 +119,47 @@ function formatTodo(todo: any): string {
   return parts.join(" ");
 }
 
+/**
+ * Format TODO item for markdown display
+ */
+function formatTodoMarkdown(todo: any): string {
+  const parts: string[] = [];
+  
+  // State badge
+  const state = todo.todo || "TODO";
+  parts.push(`**[${state}]**`);
+  
+  // Priority
+  if (todo.priority) {
+    parts.push(`\`#${todo.priority}\``);
+  }
+  
+  // Heading
+  parts.push(todo.heading);
+  
+  // Tags
+  if (todo.tags && todo.tags.length > 0) {
+    const tagStr = todo.tags.map((t: string) => `\`${t}\``).join(" ");
+    parts.push(tagStr);
+  }
+  
+  // Scheduled/Deadline on new line
+  const dates: string[] = [];
+  if (todo.scheduled) {
+    dates.push(`๐Ÿ“… ${todo.scheduled}`);
+  }
+  if (todo.deadline) {
+    dates.push(`โฐ ${todo.deadline}`);
+  }
+  
+  let result = parts.join(" ");
+  if (dates.length > 0) {
+    result += ` *(${dates.join(", ")})*`;
+  }
+  
+  return result;
+}
+
 export default function (pi: ExtensionAPI) {
   // Register the org_todo tool
   pi.registerTool({
@@ -350,12 +391,11 @@ export default function (pi: ExtensionAPI) {
     },
   });
 
+
   // Register /todos command
   pi.registerCommand("todos", {
     description: "Show today's tasks (scheduled + overdue + NEXT)",
     handler: async (args, ctx) => {
-      const theme = ctx.ui.theme;
-      
       // Fetch scheduled, overdue, and NEXT items
       const scheduled = execEmacs("(pi/org-todo-scheduled)");
       const overdue = execEmacs("(pi/org-todo-overdue)");
@@ -366,61 +406,58 @@ export default function (pi: ExtensionAPI) {
         return;
       }
       
+      // Build markdown content
       const lines: string[] = [];
       
-      // Header
-      lines.push(theme.bold("๐Ÿ“‹ Today's Tasks"));
-      lines.push(theme.fg("dim", "โ”€".repeat(50)));
+      lines.push("## ๐Ÿ“‹ Today's Tasks");
+      lines.push("");
       
       // Overdue section
       if (overdue.success && overdue.data && overdue.data.length > 0) {
+        lines.push(`### โš ๏ธ Overdue (${overdue.data.length})`);
         lines.push("");
-        lines.push(theme.fg("error", `โš ๏ธ  Overdue (${overdue.data.length})`));
-        for (const todo of overdue.data.slice(0, 5)) {
-          lines.push(`  ${theme.fg("error", "โ€ข")} ${formatTodo(todo)}`);
-        }
-        if (overdue.data.length > 5) {
-          lines.push(theme.fg("dim", `  ... and ${overdue.data.length - 5} more`));
+        for (const todo of overdue.data) {
+          lines.push(`- ${formatTodoMarkdown(todo)}`);
         }
+        lines.push("");
       }
       
       // Scheduled section
       if (scheduled.success && scheduled.data && scheduled.data.length > 0) {
+        lines.push(`### ๐Ÿ“… Scheduled Today (${scheduled.data.length})`);
         lines.push("");
-        lines.push(theme.fg("accent", `๐Ÿ“… Scheduled Today (${scheduled.data.length})`));
-        for (const todo of scheduled.data.slice(0, 5)) {
-          lines.push(`  ${theme.fg("accent", "โ€ข")} ${formatTodo(todo)}`);
-        }
-        if (scheduled.data.length > 5) {
-          lines.push(theme.fg("dim", `  ... and ${scheduled.data.length - 5} more`));
+        for (const todo of scheduled.data) {
+          lines.push(`- ${formatTodoMarkdown(todo)}`);
         }
+        lines.push("");
       }
       
       // NEXT section
       if (next.success && next.data && next.data.length > 0) {
+        lines.push(`### โžก๏ธ Next Actions (${next.data.length})`);
         lines.push("");
-        lines.push(theme.fg("success", `โžก๏ธ  Next Actions (${next.data.length})`));
-        for (const todo of next.data.slice(0, 5)) {
-          lines.push(`  ${theme.fg("success", "โ€ข")} ${formatTodo(todo)}`);
-        }
-        if (next.data.length > 5) {
-          lines.push(theme.fg("dim", `  ... and ${next.data.length - 5} more`));
+        for (const todo of next.data) {
+          lines.push(`- ${formatTodoMarkdown(todo)}`);
         }
+        lines.push("");
       }
       
       // Empty state
-      if (lines.length === 2) {
-        lines.push("");
-        lines.push(theme.fg("dim", "No tasks for today. ๐ŸŽ‰"));
+      const hasContent = 
+        (overdue.success && overdue.data?.length > 0) ||
+        (scheduled.success && scheduled.data?.length > 0) ||
+        (next.success && next.data?.length > 0);
+        
+      if (!hasContent) {
+        lines.push("*No tasks for today.* ๐ŸŽ‰");
       }
       
-      // Show as widget
-      ctx.ui.setWidget("todos", lines);
-      
-      // Auto-dismiss after 15 seconds
-      setTimeout(() => {
-        ctx.ui.setWidget("todos", undefined);
-      }, 15000);
+      // Send as a message that appears in the conversation
+      pi.sendMessage({
+        customType: "org-todos",
+        content: lines.join("\n"),
+        display: true,
+      });
     },
   });
 
@@ -435,7 +472,6 @@ export default function (pi: ExtensionAPI) {
         return;
       }
       
-      const theme = ctx.ui.theme;
       const result = execEmacs(`(pi/org-todo-search "${query.replace(/"/g, '\\"')}" nil t)`);
       
       if (!result.success) {
@@ -448,24 +484,24 @@ export default function (pi: ExtensionAPI) {
         return;
       }
       
+      // Build markdown content
       const lines: string[] = [];
-      lines.push(theme.bold(`๐Ÿ” Search: "${query}" (${result.data.length} results)`));
-      lines.push(theme.fg("dim", "โ”€".repeat(50)));
+      lines.push(`## ๐Ÿ” Search: "${query}"`);
+      lines.push("");
+      lines.push(`*${result.data.length} result(s)*`);
+      lines.push("");
       
-      for (const todo of result.data.slice(0, 10)) {
-        const matchedIn = todo.matched_in === "heading" ? "" : theme.fg("dim", " (in content)");
-        lines.push(`  ${theme.fg("accent", "โ€ข")} ${formatTodo(todo)}${matchedIn}`);
+      for (const todo of result.data) {
+        const matchedIn = todo.matched_in === "heading" ? "" : " *(matched in content)*";
+        lines.push(`- ${formatTodoMarkdown(todo)}${matchedIn}`);
       }
       
-      if (result.data.length > 10) {
-        lines.push(theme.fg("dim", `  ... and ${result.data.length - 10} more`));
-      }
-      
-      ctx.ui.setWidget("todo-search", lines);
-      
-      setTimeout(() => {
-        ctx.ui.setWidget("todo-search", undefined);
-      }, 20000);
+      // Send as a message that appears in the conversation
+      pi.sendMessage({
+        customType: "org-todos-search",
+        content: lines.join("\n"),
+        display: true,
+      });
     },
   });
 }