Commit 9bfc002dba00
Changed files (1)
dots
pi
agent
extensions
org-todos
dots/pi/agent/extensions/org-todos/index.ts
@@ -30,13 +30,13 @@
* - org-ql and pi-org-todos.el loaded
*/
-import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
import { execSync } from "node:child_process";
import { homedir } from "node:os";
import { join } from "node:path";
import * as chrono from "chrono-node";
-import { Container, Text } from "@mariozechner/pi-tui";
+import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
// Configuration
const DEFAULT_ORG_FILE = join(homedir(), "desktop/org/todos.org");
@@ -1061,18 +1061,73 @@ export default function (pi: ExtensionAPI) {
},
});
- // Register /inbox-refile command - Refile inbox item with interactive menu
+ // Helper: show a SelectList and return the chosen value (or null on cancel)
+ async function showSelectMenu(ctx: ExtensionContext, title: string, items: SelectItem[]): Promise<string | null> {
+ return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
+ const container = new Container();
+ container.addChild(new DynamicBorder((str: string) => theme.fg("accent", str)));
+ container.addChild(new Text(theme.fg("accent", theme.bold(title))));
+
+ const selectList = new SelectList(items, Math.min(items.length, 15), {
+ selectedPrefix: (text: string) => theme.fg("accent", text),
+ selectedText: (text: string) => theme.fg("accent", text),
+ description: (text: string) => theme.fg("muted", text),
+ scrollInfo: (text: string) => theme.fg("dim", text),
+ noMatch: (text: string) => theme.fg("warning", text),
+ });
+
+ selectList.onSelect = (item: SelectItem) => done(item.value);
+ selectList.onCancel = () => done(null);
+
+ container.addChild(selectList);
+ container.addChild(new Text(theme.fg("dim", "Type to filter · enter to confirm · esc to cancel")));
+ container.addChild(new DynamicBorder((str: string) => theme.fg("accent", str)));
+
+ return {
+ render(width: number) { return container.render(width); },
+ invalidate() { container.invalidate(); },
+ handleInput(data: string) { selectList.handleInput(data); tui.requestRender(); },
+ };
+ });
+ }
+
+ // Register /inbox-refile command - Refile inbox item with interactive selectors
pi.registerCommand("inbox-refile", {
- description: "Refile inbox item to a section. Usage: /inbox-refile <heading>",
+ description: "Refile inbox item to a section (interactive)",
handler: async (args, ctx) => {
- if (!args?.trim()) {
- ctx.ui.notify("Usage: /inbox-refile <heading>", "error");
+ // Step 1: Get inbox items
+ const inboxResult = execEmacs(`(pi/org-todo-inbox-all)`);
+
+ if (!inboxResult.success || !inboxResult.data || inboxResult.data.length === 0) {
+ ctx.ui.notify("Inbox is empty!", "info");
return;
}
- const heading = args.trim();
+ // Step 2: Select inbox item (skip if heading provided as argument)
+ let heading = (args || "").trim();
- // Get available refile targets
+ if (!heading) {
+ const inboxItems: SelectItem[] = inboxResult.data.map((item: any) => {
+ const prefix = item.todo ? `[${item.todo}] ` : "";
+ const label = `${prefix}${item.heading}`;
+ // Strip org link markup for cleaner display
+ const cleanLabel = label.replace(/\[\[([^\]]*)\]\[([^\]]*)\]\]/g, "$2");
+ return {
+ value: item.heading,
+ label: cleanLabel,
+ description: item.todo ? undefined : "link/note",
+ };
+ });
+
+ const selected = await showSelectMenu(ctx, "Select inbox item to refile", inboxItems);
+ if (!selected) {
+ ctx.ui.notify("Refile cancelled", "info");
+ return;
+ }
+ heading = selected;
+ }
+
+ // Step 3: Get refile targets and select section
const targetsResult = execEmacs("(pi/org-todo-get-refile-targets)");
if (!targetsResult.success || !targetsResult.data) {
@@ -1080,34 +1135,35 @@ export default function (pi: ExtensionAPI) {
return;
}
- const sections = targetsResult.data.map((t: any) => t.section);
+ const sectionItems: SelectItem[] = targetsResult.data.map((t: any) => ({
+ value: t.section,
+ label: t.section,
+ }));
- // Display menu for selection
- const lines: string[] = [];
- lines.push(`## 📋 Refile: "${heading}"`);
- lines.push("");
- lines.push("**Available sections:**");
- lines.push("");
- sections.forEach((section: string, i: number) => {
- lines.push(`${i + 1}. **${section}**`);
- });
- lines.push("");
- lines.push("To refile to a section, I'll call the org_todo tool with:");
- lines.push("```json");
- lines.push(`{`);
- lines.push(` "action": "refile",`);
- lines.push(` "heading": "${heading}",`);
- lines.push(` "section": "<choose from list above>"`);
- lines.push(`}`);
- lines.push("```");
- lines.push("");
- lines.push("*Which section would you like to refile to?*");
+ // Clean heading for display
+ const displayHeading = heading.replace(/\[\[([^\]]*)\]\[([^\]]*)\]\]/g, "$2").slice(0, 60);
+ const section = await showSelectMenu(ctx, `Refile "${displayHeading}" to:`, sectionItems);
+ if (!section) {
+ ctx.ui.notify("Refile cancelled", "info");
+ return;
+ }
+
+ // Step 4: Perform refile
+ const refileResult = execEmacs(`(pi/org-todo-refile "${heading.replace(/"/g, '\\"')}" "${section.replace(/"/g, '\\"')}")`);
+
+ if (!refileResult.success) {
+ ctx.ui.notify(`Refile failed: ${refileResult.error}`, "error");
+ return;
+ }
pi.sendMessage({
customType: "org-todos",
- content: lines.join("\n"),
+ content: `## ✅ Refiled\n\n**${displayHeading}** → *${section}*`,
display: true,
});
+
+ // Update inbox count
+ updateInboxStatus(ctx);
},
});