/**
 * This tool stores todo items as files under <todo-dir> (defaults to
 * <CONFIG_DIR_NAME>/todos, or the path in <APP_NAME>_TODO_PATH).  Each todo is
 * a standalone markdown file named <id>.md and an optional <id>.lock file is
 * used while a session is editing it.
 *
 * File format in <CONFIG_DIR_NAME>/todos:
 * - The file starts with a JSON object (not YAML) containing the front matter:
 *   { id, title, tags, status, created_at, assigned_to_session }
 * - After the JSON block comes optional markdown body text separated by a blank line.
 * - Example:
 *   {
 *     "id": "deadbeef",
 *     "title": "Add tests",
 *     "tags": ["qa"],
 *     "status": "open",
 *     "created_at": "2026-01-25T17:00:00.000Z",
 *     "assigned_to_session": "session.json"
 *   }
 *
 *   Notes about the work go here.
 */
import { StringEnum } from "@earendil-works/pi-ai";
import { Text } from "@earendil-works/pi-tui";
import crypto from "node:crypto";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { Type } from "typebox";
import { APP_NAME, CONFIG_DIR_NAME, getEnvValue } from "../../config.js";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
const TODO_DIR_NAME = `${CONFIG_DIR_NAME}/todos`;
const TODO_PATH_ENV = `${APP_NAME.toUpperCase()}_TODO_PATH`;
const TODO_ID_PREFIX = "TODO-";
const TODO_ID_PATTERN = /^[a-f0-9]{8}$/i;
const LOCK_TTL_MS = 30 * 60 * 1000;
const TodoParams = Type.Object({
    action: StringEnum([
        "list",
        "list-all",
        "get",
        "create",
        "update",
        "append",
        "delete",
        "claim",
        "release",
    ]),
    id: Type.Optional(Type.String({ description: "Todo id (TODO-<hex> or raw hex filename)" })),
    title: Type.Optional(Type.String({ description: "Short summary shown in lists" })),
    status: Type.Optional(Type.String({ description: "Todo status" })),
    tags: Type.Optional(Type.Array(Type.String({ description: "Todo tag" }))),
    body: Type.Optional(Type.String({
        description: "Long-form details (markdown). Update replaces; append adds.",
    })),
    force: Type.Optional(Type.Boolean({ description: "Override another session's assignment" })),
});
function formatTodoId(id) {
    return `${TODO_ID_PREFIX}${id}`;
}
function normalizeTodoId(id) {
    let trimmed = id.trim();
    if (trimmed.startsWith("#")) {
        trimmed = trimmed.slice(1);
    }
    if (trimmed.toUpperCase().startsWith(TODO_ID_PREFIX)) {
        trimmed = trimmed.slice(TODO_ID_PREFIX.length);
    }
    return trimmed;
}
function validateTodoId(id) {
    const normalized = normalizeTodoId(id);
    if (!normalized || !TODO_ID_PATTERN.test(normalized)) {
        return { error: "Invalid todo id. Expected TODO-<hex>." };
    }
    return { id: normalized.toLowerCase() };
}
function displayTodoId(id) {
    return formatTodoId(normalizeTodoId(id));
}
function isTodoClosed(status) {
    return ["closed", "done"].includes(status.toLowerCase());
}
function clearAssignmentIfClosed(todo) {
    if (isTodoClosed(getTodoStatus(todo))) {
        todo.assigned_to_session = undefined;
    }
}
function sortTodos(todos) {
    return [...todos].sort((a, b) => {
        const aClosed = isTodoClosed(a.status);
        const bClosed = isTodoClosed(b.status);
        if (aClosed !== bClosed)
            return aClosed ? 1 : -1;
        const aAssigned = !aClosed && Boolean(a.assigned_to_session);
        const bAssigned = !bClosed && Boolean(b.assigned_to_session);
        if (aAssigned !== bAssigned)
            return aAssigned ? -1 : 1;
        return (a.created_at || "").localeCompare(b.created_at || "");
    });
}
function getTodosDir(cwd) {
    const overridePath = getEnvValue(TODO_PATH_ENV);
    if (overridePath && overridePath.trim()) {
        return path.resolve(cwd, overridePath.trim());
    }
    return path.resolve(cwd, TODO_DIR_NAME);
}
function getTodosDirLabel(cwd) {
    const overridePath = getEnvValue(TODO_PATH_ENV);
    if (overridePath && overridePath.trim()) {
        return path.resolve(cwd, overridePath.trim());
    }
    return TODO_DIR_NAME;
}
function getTodoPath(todosDir, id) {
    return path.join(todosDir, `${id}.md`);
}
function getLockPath(todosDir, id) {
    return path.join(todosDir, `${id}.lock`);
}
function parseFrontMatter(text, idFallback) {
    const data = {
        id: idFallback,
        title: "",
        tags: [],
        status: "open",
        created_at: "",
        assigned_to_session: undefined,
    };
    const trimmed = text.trim();
    if (!trimmed)
        return data;
    try {
        const parsed = JSON.parse(trimmed);
        if (!parsed || typeof parsed !== "object")
            return data;
        if (typeof parsed.id === "string" && parsed.id)
            data.id = parsed.id;
        if (typeof parsed.title === "string")
            data.title = parsed.title;
        if (typeof parsed.status === "string" && parsed.status)
            data.status = parsed.status;
        if (typeof parsed.created_at === "string")
            data.created_at = parsed.created_at;
        if (typeof parsed.assigned_to_session === "string" &&
            parsed.assigned_to_session.trim()) {
            data.assigned_to_session = parsed.assigned_to_session;
        }
        if (Array.isArray(parsed.tags)) {
            data.tags = parsed.tags.filter((tag) => typeof tag === "string");
        }
    }
    catch {
        return data;
    }
    return data;
}
function findJsonObjectEnd(content) {
    let depth = 0;
    let inString = false;
    let escaped = false;
    for (let i = 0; i < content.length; i += 1) {
        const char = content[i];
        if (inString) {
            if (escaped) {
                escaped = false;
                continue;
            }
            if (char === "\\") {
                escaped = true;
                continue;
            }
            if (char === '"') {
                inString = false;
            }
            continue;
        }
        if (char === '"') {
            inString = true;
            continue;
        }
        if (char === "{") {
            depth += 1;
            continue;
        }
        if (char === "}") {
            depth -= 1;
            if (depth === 0)
                return i;
        }
    }
    return -1;
}
function splitFrontMatter(content) {
    if (!content.startsWith("{")) {
        return { frontMatter: "", body: content };
    }
    const endIndex = findJsonObjectEnd(content);
    if (endIndex === -1) {
        return { frontMatter: "", body: content };
    }
    const frontMatter = content.slice(0, endIndex + 1);
    const body = content.slice(endIndex + 1).replace(/^\r?\n+/, "");
    return { frontMatter, body };
}
function parseTodoContent(content, idFallback) {
    const { frontMatter, body } = splitFrontMatter(content);
    const parsed = parseFrontMatter(frontMatter, idFallback);
    return {
        id: idFallback,
        title: parsed.title,
        tags: parsed.tags ?? [],
        status: parsed.status,
        created_at: parsed.created_at,
        assigned_to_session: parsed.assigned_to_session,
        body: body ?? "",
    };
}
function serializeTodo(todo) {
    const frontMatter = JSON.stringify({
        id: todo.id,
        title: todo.title,
        tags: todo.tags ?? [],
        status: todo.status,
        created_at: todo.created_at,
        assigned_to_session: todo.assigned_to_session || undefined,
    }, null, 2);
    const body = todo.body ?? "";
    const trimmedBody = body.replace(/^\n+/, "").replace(/\s+$/, "");
    if (!trimmedBody)
        return `${frontMatter}\n`;
    return `${frontMatter}\n\n${trimmedBody}\n`;
}
async function ensureTodosDir(todosDir) {
    await fs.mkdir(todosDir, { recursive: true });
}
async function readTodoFile(filePath, idFallback) {
    const content = await fs.readFile(filePath, "utf8");
    return parseTodoContent(content, idFallback);
}
async function writeTodoFile(filePath, todo) {
    await fs.writeFile(filePath, serializeTodo(todo), "utf8");
}
async function generateTodoId(todosDir) {
    for (let attempt = 0; attempt < 10; attempt += 1) {
        const id = crypto.randomBytes(4).toString("hex");
        const todoPath = getTodoPath(todosDir, id);
        if (!existsSync(todoPath))
            return id;
    }
    throw new Error("Failed to generate unique todo id");
}
async function readLockInfo(lockPath) {
    try {
        const raw = await fs.readFile(lockPath, "utf8");
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
async function acquireLock(todosDir, id, ctx) {
    const lockPath = getLockPath(todosDir, id);
    const now = Date.now();
    const session = ctx.sessionManager.getSessionFile();
    for (let attempt = 0; attempt < 2; attempt += 1) {
        try {
            const handle = await fs.open(lockPath, "wx");
            const info = {
                id,
                pid: process.pid,
                session,
                created_at: new Date(now).toISOString(),
            };
            await handle.writeFile(JSON.stringify(info, null, 2), "utf8");
            await handle.close();
            return async () => {
                try {
                    await fs.unlink(lockPath);
                }
                catch {
                    // ignore
                }
            };
        }
        catch (error) {
            const fsError = error;
            if (fsError.code !== "EEXIST") {
                return {
                    error: `Failed to acquire lock: ${fsError.message ?? "unknown error"}`,
                };
            }
            const stats = await fs.stat(lockPath).catch(() => null);
            const lockAge = stats ? now - stats.mtimeMs : LOCK_TTL_MS + 1;
            if (lockAge <= LOCK_TTL_MS) {
                const info = await readLockInfo(lockPath);
                const owner = info?.session ? ` (session ${info.session})` : "";
                return {
                    error: `Todo ${displayTodoId(id)} is locked${owner}. Try again later.`,
                };
            }
            if (!ctx.hasUI) {
                return {
                    error: `Todo ${displayTodoId(id)} lock is stale; rerun in interactive mode to steal it.`,
                };
            }
            const ok = await ctx.ui.confirm("Todo locked", `Todo ${displayTodoId(id)} appears locked. Steal the lock?`);
            if (!ok) {
                return { error: `Todo ${displayTodoId(id)} remains locked.` };
            }
            await fs.unlink(lockPath).catch(() => undefined);
        }
    }
    return { error: `Failed to acquire lock for todo ${displayTodoId(id)}.` };
}
async function withTodoLock(todosDir, id, ctx, fn) {
    const lock = await acquireLock(todosDir, id, ctx);
    if (typeof lock === "object" && "error" in lock)
        return lock;
    try {
        return await fn();
    }
    finally {
        await lock();
    }
}
async function listTodos(todosDir) {
    let entries = [];
    try {
        entries = await fs.readdir(todosDir);
    }
    catch {
        return [];
    }
    const todos = [];
    for (const entry of entries) {
        if (!entry.endsWith(".md"))
            continue;
        const id = entry.slice(0, -3);
        const filePath = path.join(todosDir, entry);
        try {
            const content = await fs.readFile(filePath, "utf8");
            const { frontMatter } = splitFrontMatter(content);
            const parsed = parseFrontMatter(frontMatter, id);
            todos.push({
                id,
                title: parsed.title,
                tags: parsed.tags ?? [],
                status: parsed.status,
                created_at: parsed.created_at,
                assigned_to_session: parsed.assigned_to_session,
            });
        }
        catch {
            // ignore unreadable todo
        }
    }
    return sortTodos(todos);
}
function getTodoTitle(todo) {
    return todo.title || "(untitled)";
}
function getTodoStatus(todo) {
    return todo.status || "open";
}
function renderAssignmentSuffix(theme, todo, currentSessionId) {
    if (!todo.assigned_to_session)
        return "";
    const isCurrent = todo.assigned_to_session === currentSessionId;
    const color = isCurrent ? "success" : "dim";
    const suffix = isCurrent ? ", current" : "";
    return theme.fg(color, ` (assigned: ${todo.assigned_to_session}${suffix})`);
}
function splitTodosByAssignment(todos) {
    const assignedTodos = [];
    const openTodos = [];
    const closedTodos = [];
    for (const todo of todos) {
        if (isTodoClosed(getTodoStatus(todo))) {
            closedTodos.push(todo);
            continue;
        }
        if (todo.assigned_to_session) {
            assignedTodos.push(todo);
        }
        else {
            openTodos.push(todo);
        }
    }
    return { assignedTodos, openTodos, closedTodos };
}
function serializeTodoForAgent(todo) {
    const payload = { ...todo, id: formatTodoId(todo.id) };
    return JSON.stringify(payload, null, 2);
}
function serializeTodoListForAgent(todos) {
    const { assignedTodos, openTodos, closedTodos } = splitTodosByAssignment(todos);
    const mapTodo = (todo) => ({
        ...todo,
        id: formatTodoId(todo.id),
    });
    return JSON.stringify({
        assigned: assignedTodos.map(mapTodo),
        open: openTodos.map(mapTodo),
        closed: closedTodos.map(mapTodo),
    }, null, 2);
}
function renderTodoHeading(theme, todo, currentSessionId) {
    const closed = isTodoClosed(getTodoStatus(todo));
    const titleColor = closed ? "dim" : "text";
    const tagText = todo.tags.length ? theme.fg("dim", ` [${todo.tags.join(", ")}]`) : "";
    const assignmentText = renderAssignmentSuffix(theme, todo, currentSessionId);
    return (theme.fg("accent", formatTodoId(todo.id)) +
        " " +
        theme.fg(titleColor, getTodoTitle(todo)) +
        tagText +
        assignmentText);
}
function renderTodoList(theme, todos, expanded, currentSessionId) {
    if (!todos.length)
        return theme.fg("dim", "No todos");
    const { assignedTodos, openTodos, closedTodos } = splitTodosByAssignment(todos);
    const lines = [];
    const pushSection = (label, sectionTodos) => {
        lines.push(theme.fg("muted", `${label} (${sectionTodos.length})`));
        if (!sectionTodos.length) {
            lines.push(theme.fg("dim", "  none"));
            return;
        }
        const maxItems = expanded ? sectionTodos.length : Math.min(sectionTodos.length, 3);
        for (let i = 0; i < maxItems; i++) {
            lines.push(`  ${renderTodoHeading(theme, sectionTodos[i], currentSessionId)}`);
        }
        if (!expanded && sectionTodos.length > maxItems) {
            lines.push(theme.fg("dim", `  ... ${sectionTodos.length - maxItems} more`));
        }
    };
    const sections = [
        { label: "Assigned todos", todos: assignedTodos },
        { label: "Open todos", todos: openTodos },
        { label: "Closed todos", todos: closedTodos },
    ];
    sections.forEach((section, index) => {
        if (index > 0)
            lines.push("");
        pushSection(section.label, section.todos);
    });
    return lines.join("\n");
}
function renderTodoDetail(theme, todo, expanded) {
    const summary = renderTodoHeading(theme, todo);
    if (!expanded)
        return summary;
    const tags = todo.tags.length ? todo.tags.join(", ") : "none";
    const createdAt = todo.created_at || "unknown";
    const bodyText = todo.body?.trim() ? todo.body.trim() : "No details yet.";
    const bodyLines = bodyText.split("\n");
    const lines = [
        summary,
        theme.fg("muted", `Status: ${getTodoStatus(todo)}`),
        theme.fg("muted", `Tags: ${tags}`),
        theme.fg("muted", `Created: ${createdAt}`),
        "",
        theme.fg("muted", "Body:"),
        ...bodyLines.map((line) => theme.fg("text", `  ${line}`)),
    ];
    return lines.join("\n");
}
function appendExpandHint(theme, text) {
    return `${text}\n${theme.fg("dim", `(${keyHint("app.tools.expand", "Expand")})`)}`;
}
async function ensureTodoExists(filePath, id) {
    if (!existsSync(filePath))
        return null;
    return readTodoFile(filePath, id);
}
async function appendTodoBody(filePath, todo, text) {
    const spacer = todo.body.trim().length ? "\n\n" : "";
    todo.body = `${todo.body.replace(/\s+$/, "")}${spacer}${text.trim()}\n`;
    await writeTodoFile(filePath, todo);
    return todo;
}
async function claimTodoAssignment(todosDir, id, ctx, force = false) {
    const validated = validateTodoId(id);
    if ("error" in validated) {
        return { error: validated.error };
    }
    const normalizedId = validated.id;
    const filePath = getTodoPath(todosDir, normalizedId);
    if (!existsSync(filePath)) {
        return { error: `Todo ${displayTodoId(id)} not found` };
    }
    const sessionId = ctx.sessionManager.getSessionId();
    const result = await withTodoLock(todosDir, normalizedId, ctx, async () => {
        const existing = await ensureTodoExists(filePath, normalizedId);
        if (!existing)
            return { error: `Todo ${displayTodoId(id)} not found` };
        if (isTodoClosed(existing.status)) {
            return { error: `Todo ${displayTodoId(id)} is closed` };
        }
        const assigned = existing.assigned_to_session;
        if (assigned && assigned !== sessionId && !force) {
            return {
                error: `Todo ${displayTodoId(id)} is already assigned to session ${assigned}. Use force to override.`,
            };
        }
        if (assigned !== sessionId) {
            existing.assigned_to_session = sessionId;
            await writeTodoFile(filePath, existing);
        }
        return existing;
    });
    if (typeof result === "object" && "error" in result) {
        return { error: result.error };
    }
    return result;
}
async function releaseTodoAssignment(todosDir, id, ctx, force = false) {
    const validated = validateTodoId(id);
    if ("error" in validated) {
        return { error: validated.error };
    }
    const normalizedId = validated.id;
    const filePath = getTodoPath(todosDir, normalizedId);
    if (!existsSync(filePath)) {
        return { error: `Todo ${displayTodoId(id)} not found` };
    }
    const sessionId = ctx.sessionManager.getSessionId();
    const result = await withTodoLock(todosDir, normalizedId, ctx, async () => {
        const existing = await ensureTodoExists(filePath, normalizedId);
        if (!existing)
            return { error: `Todo ${displayTodoId(id)} not found` };
        const assigned = existing.assigned_to_session;
        if (!assigned) {
            return existing;
        }
        if (assigned !== sessionId && !force) {
            return {
                error: `Todo ${displayTodoId(id)} is assigned to session ${assigned}. Use force to release.`,
            };
        }
        existing.assigned_to_session = undefined;
        await writeTodoFile(filePath, existing);
        return existing;
    });
    if (typeof result === "object" && "error" in result) {
        return { error: result.error };
    }
    return result;
}
async function deleteTodo(todosDir, id, ctx) {
    const validated = validateTodoId(id);
    if ("error" in validated) {
        return { error: validated.error };
    }
    const normalizedId = validated.id;
    const filePath = getTodoPath(todosDir, normalizedId);
    if (!existsSync(filePath)) {
        return { error: `Todo ${displayTodoId(id)} not found` };
    }
    const result = await withTodoLock(todosDir, normalizedId, ctx, async () => {
        const existing = await ensureTodoExists(filePath, normalizedId);
        if (!existing)
            return { error: `Todo ${displayTodoId(id)} not found` };
        await fs.unlink(filePath);
        return existing;
    });
    if (typeof result === "object" && "error" in result) {
        return { error: result.error };
    }
    return result;
}
export function createTodoToolDefinition(cwd = process.cwd()) {
    const todosDirLabel = getTodosDirLabel(cwd);
    return {
        name: "todo",
        label: "Todo",
        description: `Manage file-based todos in ${todosDirLabel} (list, list-all, get, create, update, append, delete, claim, release). ` +
            "Title is the short summary; body is long-form markdown notes (update replaces, append adds). " +
            "Todo ids are shown as TODO-<hex>; id parameters accept TODO-<hex> or the raw hex filename. " +
            "Claim tasks before working on them to avoid conflicts, and close them when complete.",
        parameters: TodoParams,
        async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
            const todosDir = getTodosDir(ctx.cwd);
            const action = params.action;
            switch (action) {
                case "list": {
                    const todos = await listTodos(todosDir);
                    const { assignedTodos, openTodos } = splitTodosByAssignment(todos);
                    const listedTodos = [...assignedTodos, ...openTodos];
                    const currentSessionId = ctx.sessionManager.getSessionId();
                    return {
                        content: [{ type: "text", text: serializeTodoListForAgent(listedTodos) }],
                        details: { action: "list", todos: listedTodos, currentSessionId },
                    };
                }
                case "list-all": {
                    const todos = await listTodos(todosDir);
                    const currentSessionId = ctx.sessionManager.getSessionId();
                    return {
                        content: [{ type: "text", text: serializeTodoListForAgent(todos) }],
                        details: { action: "list-all", todos, currentSessionId },
                    };
                }
                case "get": {
                    if (!params.id) {
                        return {
                            content: [{ type: "text", text: "Error: id required" }],
                            details: { action: "get", error: "id required" },
                        };
                    }
                    const validated = validateTodoId(params.id);
                    if ("error" in validated) {
                        return {
                            content: [{ type: "text", text: validated.error }],
                            details: { action: "get", error: validated.error },
                        };
                    }
                    const normalizedId = validated.id;
                    const displayId = formatTodoId(normalizedId);
                    const filePath = getTodoPath(todosDir, normalizedId);
                    const todo = await ensureTodoExists(filePath, normalizedId);
                    if (!todo) {
                        return {
                            content: [{ type: "text", text: `Todo ${displayId} not found` }],
                            details: { action: "get", error: "not found" },
                        };
                    }
                    return {
                        content: [{ type: "text", text: serializeTodoForAgent(todo) }],
                        details: { action: "get", todo },
                    };
                }
                case "create": {
                    if (!params.title) {
                        return {
                            content: [{ type: "text", text: "Error: title required" }],
                            details: { action: "create", error: "title required" },
                        };
                    }
                    await ensureTodosDir(todosDir);
                    const id = await generateTodoId(todosDir);
                    const filePath = getTodoPath(todosDir, id);
                    const todo = {
                        id,
                        title: params.title,
                        tags: params.tags ?? [],
                        status: params.status ?? "open",
                        created_at: new Date().toISOString(),
                        body: params.body ?? "",
                    };
                    const result = await withTodoLock(todosDir, id, ctx, async () => {
                        await writeTodoFile(filePath, todo);
                        return todo;
                    });
                    if (typeof result === "object" && "error" in result) {
                        return {
                            content: [{ type: "text", text: result.error }],
                            details: { action: "create", error: result.error },
                        };
                    }
                    return {
                        content: [{ type: "text", text: serializeTodoForAgent(todo) }],
                        details: { action: "create", todo },
                    };
                }
                case "update": {
                    if (!params.id) {
                        return {
                            content: [{ type: "text", text: "Error: id required" }],
                            details: { action: "update", error: "id required" },
                        };
                    }
                    const validated = validateTodoId(params.id);
                    if ("error" in validated) {
                        return {
                            content: [{ type: "text", text: validated.error }],
                            details: { action: "update", error: validated.error },
                        };
                    }
                    const normalizedId = validated.id;
                    const displayId = formatTodoId(normalizedId);
                    const filePath = getTodoPath(todosDir, normalizedId);
                    if (!existsSync(filePath)) {
                        return {
                            content: [{ type: "text", text: `Todo ${displayId} not found` }],
                            details: { action: "update", error: "not found" },
                        };
                    }
                    const result = await withTodoLock(todosDir, normalizedId, ctx, async () => {
                        const existing = await ensureTodoExists(filePath, normalizedId);
                        if (!existing)
                            return { error: `Todo ${displayId} not found` };
                        existing.id = normalizedId;
                        if (params.title !== undefined)
                            existing.title = params.title;
                        if (params.status !== undefined)
                            existing.status = params.status;
                        if (params.tags !== undefined)
                            existing.tags = params.tags;
                        if (params.body !== undefined)
                            existing.body = params.body;
                        if (!existing.created_at)
                            existing.created_at = new Date().toISOString();
                        clearAssignmentIfClosed(existing);
                        await writeTodoFile(filePath, existing);
                        return existing;
                    });
                    if (typeof result === "object" && "error" in result) {
                        return {
                            content: [{ type: "text", text: result.error }],
                            details: { action: "update", error: result.error },
                        };
                    }
                    const updatedTodo = result;
                    return {
                        content: [{ type: "text", text: serializeTodoForAgent(updatedTodo) }],
                        details: { action: "update", todo: updatedTodo },
                    };
                }
                case "append": {
                    if (!params.id) {
                        return {
                            content: [{ type: "text", text: "Error: id required" }],
                            details: { action: "append", error: "id required" },
                        };
                    }
                    const validated = validateTodoId(params.id);
                    if ("error" in validated) {
                        return {
                            content: [{ type: "text", text: validated.error }],
                            details: { action: "append", error: validated.error },
                        };
                    }
                    const normalizedId = validated.id;
                    const displayId = formatTodoId(normalizedId);
                    const filePath = getTodoPath(todosDir, normalizedId);
                    if (!existsSync(filePath)) {
                        return {
                            content: [{ type: "text", text: `Todo ${displayId} not found` }],
                            details: { action: "append", error: "not found" },
                        };
                    }
                    const result = await withTodoLock(todosDir, normalizedId, ctx, async () => {
                        const existing = await ensureTodoExists(filePath, normalizedId);
                        if (!existing)
                            return { error: `Todo ${displayId} not found` };
                        if (!params.body || !params.body.trim()) {
                            return existing;
                        }
                        const updated = await appendTodoBody(filePath, existing, params.body);
                        return updated;
                    });
                    if (typeof result === "object" && "error" in result) {
                        return {
                            content: [{ type: "text", text: result.error }],
                            details: { action: "append", error: result.error },
                        };
                    }
                    const updatedTodo = result;
                    return {
                        content: [{ type: "text", text: serializeTodoForAgent(updatedTodo) }],
                        details: { action: "append", todo: updatedTodo },
                    };
                }
                case "claim": {
                    if (!params.id) {
                        return {
                            content: [{ type: "text", text: "Error: id required" }],
                            details: { action: "claim", error: "id required" },
                        };
                    }
                    const result = await claimTodoAssignment(todosDir, params.id, ctx, Boolean(params.force));
                    if (typeof result === "object" && "error" in result) {
                        return {
                            content: [{ type: "text", text: result.error }],
                            details: { action: "claim", error: result.error },
                        };
                    }
                    const updatedTodo = result;
                    return {
                        content: [{ type: "text", text: serializeTodoForAgent(updatedTodo) }],
                        details: { action: "claim", todo: updatedTodo },
                    };
                }
                case "release": {
                    if (!params.id) {
                        return {
                            content: [{ type: "text", text: "Error: id required" }],
                            details: { action: "release", error: "id required" },
                        };
                    }
                    const result = await releaseTodoAssignment(todosDir, params.id, ctx, Boolean(params.force));
                    if (typeof result === "object" && "error" in result) {
                        return {
                            content: [{ type: "text", text: result.error }],
                            details: { action: "release", error: result.error },
                        };
                    }
                    const updatedTodo = result;
                    return {
                        content: [{ type: "text", text: serializeTodoForAgent(updatedTodo) }],
                        details: { action: "release", todo: updatedTodo },
                    };
                }
                case "delete": {
                    if (!params.id) {
                        return {
                            content: [{ type: "text", text: "Error: id required" }],
                            details: { action: "delete", error: "id required" },
                        };
                    }
                    const validated = validateTodoId(params.id);
                    if ("error" in validated) {
                        return {
                            content: [{ type: "text", text: validated.error }],
                            details: { action: "delete", error: validated.error },
                        };
                    }
                    const result = await deleteTodo(todosDir, validated.id, ctx);
                    if (typeof result === "object" && "error" in result) {
                        return {
                            content: [{ type: "text", text: result.error }],
                            details: { action: "delete", error: result.error },
                        };
                    }
                    return {
                        content: [
                            {
                                type: "text",
                                text: serializeTodoForAgent(result),
                            },
                        ],
                        details: { action: "delete", todo: result },
                    };
                }
            }
        },
        renderCall(args, theme) {
            const action = typeof args.action === "string" ? args.action : "";
            const id = typeof args.id === "string" ? args.id : "";
            const normalizedId = id ? normalizeTodoId(id) : "";
            const title = typeof args.title === "string" ? args.title : "";
            let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", action);
            if (normalizedId) {
                text += " " + theme.fg("accent", formatTodoId(normalizedId));
            }
            if (title) {
                text += " " + theme.fg("dim", `"${title}"`);
            }
            return new Text(text, 0, 0);
        },
        renderResult(result, { expanded, isPartial }, theme) {
            const details = result.details;
            if (isPartial) {
                return new Text(theme.fg("warning", "Processing..."), 0, 0);
            }
            if (!details) {
                const text = result.content[0];
                return new Text(text?.type === "text" ? text.text : "", 0, 0);
            }
            if (details.error) {
                return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
            }
            if (details.action === "list" || details.action === "list-all") {
                let text = renderTodoList(theme, details.todos, expanded, details.currentSessionId);
                if (!expanded) {
                    const { closedTodos } = splitTodosByAssignment(details.todos);
                    if (closedTodos.length) {
                        text = appendExpandHint(theme, text);
                    }
                }
                return new Text(text, 0, 0);
            }
            if (!("todo" in details) || !details.todo) {
                const text = result.content[0];
                return new Text(text?.type === "text" ? text.text : "", 0, 0);
            }
            let text = renderTodoDetail(theme, details.todo, expanded);
            const actionLabel = details.action === "create"
                ? "Created"
                : details.action === "update"
                    ? "Updated"
                    : details.action === "append"
                        ? "Appended to"
                        : details.action === "delete"
                            ? "Deleted"
                            : details.action === "claim"
                                ? "Claimed"
                                : details.action === "release"
                                    ? "Released"
                                    : null;
            if (actionLabel) {
                const lines = text.split("\n");
                lines[0] = theme.fg("success", "✓ ") + theme.fg("muted", `${actionLabel} `) + lines[0];
                text = lines.join("\n");
            }
            if (!expanded) {
                text = appendExpandHint(theme, text);
            }
            return new Text(text, 0, 0);
        },
    };
}
//# sourceMappingURL=todos.js.map