// @generated by scripts/build-runtime.mjs; do not edit. // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader. // src/todo-widget.ts import { StringEnum } from "@earendil-works/pi-ai"; import { buildSessionContext } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; // src/settings.ts import { constants } from "node:fs"; import { open } from "node:fs/promises"; import { join } from "node:path"; import { getAgentDir } from "@earendil-works/pi-coding-agent"; var TODO_SETTINGS_FILE = "pi-todo.json"; var MAX_TODO_SETTINGS_BYTES = 64 * 1024; var TODO_DISPLAY_MODES = ["adaptive", "expanded", "collapsed"]; var DEFAULT_TODO_SETTINGS = Object.freeze({ widget: Object.freeze({ enabled: true, displayMode: "adaptive", showCompleted: true, maxVisibleItems: null, showProgress: true }) }); function todoSettingsPath() { return join(getAgentDir(), TODO_SETTINGS_FILE); } function normalizeTodoSettings(value) { if (!isRecord(value)) return void 0; const widgetValue = Object.hasOwn(value, "widget") ? value.widget : void 0; if (widgetValue !== void 0 && !isRecord(widgetValue)) return void 0; const widget = widgetValue ?? {}; const enabled = booleanSetting(widget, "enabled", DEFAULT_TODO_SETTINGS.widget.enabled); const showCompleted = booleanSetting(widget, "showCompleted", DEFAULT_TODO_SETTINGS.widget.showCompleted); const showProgress = booleanSetting(widget, "showProgress", DEFAULT_TODO_SETTINGS.widget.showProgress); if (enabled === void 0 || showCompleted === void 0 || showProgress === void 0) { return void 0; } const displayMode = Object.hasOwn(widget, "displayMode") ? widget.displayMode : DEFAULT_TODO_SETTINGS.widget.displayMode; if (!TODO_DISPLAY_MODES.includes(displayMode)) return void 0; const maxVisibleItems = Object.hasOwn(widget, "maxVisibleItems") ? widget.maxVisibleItems : DEFAULT_TODO_SETTINGS.widget.maxVisibleItems; if (maxVisibleItems !== null && (typeof maxVisibleItems !== "number" || !Number.isSafeInteger(maxVisibleItems) || maxVisibleItems < 1 || maxVisibleItems > 50)) { return void 0; } return { widget: { enabled, displayMode, showCompleted, maxVisibleItems, showProgress } }; } async function loadTodoSettings(path = todoSettingsPath(), signal) { throwIfAborted(signal); let handle; try { handle = await open(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)); } catch (error) { throwIfAborted(signal); if (isNodeError(error) && error.code === "ENOENT") { return { kind: "missing", path, settings: cloneDefaultSettings() }; } return { kind: "invalid", path, settings: cloneDefaultSettings(), issue: safeReadIssue(error) }; } try { const stats = await handle.stat(); throwIfAborted(signal); if (!stats.isFile()) return invalidResult(path, "settings path is not a regular file"); if (stats.size > MAX_TODO_SETTINGS_BYTES) { return invalidResult(path, `settings file exceeds ${MAX_TODO_SETTINGS_BYTES} bytes`); } const buffer = Buffer.alloc(MAX_TODO_SETTINGS_BYTES + 1); let offset = 0; while (offset < buffer.byteLength) { throwIfAborted(signal); const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset); throwIfAborted(signal); if (bytesRead === 0) break; offset += bytesRead; } if (offset > MAX_TODO_SETTINGS_BYTES) { return invalidResult(path, `settings file exceeds ${MAX_TODO_SETTINGS_BYTES} bytes`); } let text; try { text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, offset)); } catch { return invalidResult(path, "settings file is not valid UTF-8"); } let parsed; try { parsed = JSON.parse(text); } catch { return invalidResult(path, "invalid JSON"); } const settings = normalizeTodoSettings(parsed); return settings ? { kind: "loaded", path, settings } : invalidResult(path, "invalid settings shape or values"); } catch (error) { throwIfAborted(signal); return invalidResult(path, safeReadIssue(error)); } finally { await handle.close(); } } function booleanSetting(record, key, fallback) { const value = Object.hasOwn(record, key) ? record[key] : fallback; return typeof value === "boolean" ? value : void 0; } function cloneDefaultSettings() { return { widget: { ...DEFAULT_TODO_SETTINGS.widget } }; } function invalidResult(path, issue) { return { kind: "invalid", path, settings: cloneDefaultSettings(), issue }; } function throwIfAborted(signal) { signal?.throwIfAborted(); } function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function isNodeError(error) { return error instanceof Error && "code" in error; } function safeReadIssue(error) { if (isNodeError(error) && error.code === "ELOOP") return "symbolic links are not accepted"; return error instanceof Error ? error.message : String(error); } // src/widget-renderer.ts import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui"; import { EditorStatusWidget } from "@narumitw/pi-tui-kit/editor-status-widget"; import { sanitizeTerminalDocument } from "@narumitw/pi-tui-kit/terminal-document"; function renderTodoWidget(todos, theme, width, options = {}) { const renderWidth = Math.max(0, width); const settings = options.settings ?? DEFAULT_TODO_SETTINGS.widget; const header = renderHeader(todos, theme, renderWidth, settings.showProgress); const rendered = todos.map((todo) => ({ todo, lines: renderTodo(todo, theme, renderWidth) })); let lines; switch (settings.displayMode) { case "expanded": lines = renderExpanded(header, rendered, settings, theme, renderWidth); break; case "collapsed": lines = renderCollapsed(header, rendered, settings, theme, renderWidth, widgetRowBudget(options.terminalRows)); break; case "adaptive": { const expanded = renderExpanded(header, rendered, settings, theme, renderWidth); const rowBudget = widgetRowBudget(options.terminalRows); const eligibleItems = settings.showCompleted ? rendered.length : rendered.filter(({ todo }) => todo.status !== "completed").length; const itemCapHidesWork = settings.maxVisibleItems !== null && eligibleItems > settings.maxVisibleItems; lines = expanded.length <= rowBudget && !itemCapHidesWork ? expanded : renderCollapsed(header, rendered, settings, theme, renderWidth, rowBudget); break; } } return lines.map((line) => truncateToWidth(line, renderWidth, "")); } function renderCompletionSummary(total, theme, width) { return new EditorStatusWidget({ theme, renderBody: () => [theme.fg("success", `\u2713 ${total}/${total} tasks completed`)] }).render(width); } function sanitizeTodoText(value) { return sanitizeTerminalDocument(value).replace(/\s+/gu, " ").trim(); } function widgetRowBudget(terminalRows) { const rows = typeof terminalRows === "number" && Number.isFinite(terminalRows) ? Math.max(0, Math.floor(terminalRows)) : 36; return Math.max(4, Math.min(12, Math.floor(rows / 3))); } function renderHeader(todos, theme, width, showProgress) { const completed = todos.filter((todo) => todo.status === "completed").length; return new EditorStatusWidget({ theme, renderBody: () => [theme.fg("muted", showProgress ? `Todo \xB7 ${completed}/${todos.length} complete` : "Todo")] }).render(width); } function renderTodo(todo, theme, width) { const step = sanitizeTodoText(todo.step) || "(text hidden after sanitization)"; let prefix; let styledText; switch (todo.status) { case "completed": prefix = theme.fg("success", "\u2713 "); styledText = theme.fg("muted", theme.strikethrough(step)); break; case "in_progress": prefix = theme.fg("accent", "\u25B6 "); styledText = theme.fg("accent", theme.bold(step)); break; case "blocked": { prefix = theme.fg("warning", "\u26A0 "); const reason = sanitizeTodoText(todo.reason ?? "") || "(reason hidden after sanitization)"; styledText = `${theme.fg("warning", step)}${reason ? theme.fg("muted", ` \u2014 ${reason}`) : ""}`; break; } case "pending": prefix = theme.fg("dim", "\u25CB "); styledText = theme.fg("text", step); break; } if (width <= 2) return [prefix]; const wrapped = wrapTextWithAnsi(styledText, width - 2); return wrapped.map((line, index) => `${index === 0 ? prefix : " "}${line}`); } function renderExpanded(header, rendered, settings, theme, width) { const candidates = settings.showCompleted ? rendered : rendered.filter(({ todo }) => todo.status !== "completed"); const visible = candidates.slice(0, settings.maxVisibleItems ?? candidates.length); const hidden = candidates.length - visible.length; const lines = [...header, ...visible.flatMap((item) => item.lines)]; if (hidden > 0) lines.push(theme.fg("dim", `\u2026 ${hidden} more`)); return lines.map((line) => truncateToWidth(line, width, "")); } function renderCollapsed(header, rendered, settings, theme, width, rowBudget) { const prioritized = [ ...rendered.filter(({ todo }) => todo.status === "in_progress"), ...rendered.filter(({ todo }) => todo.status === "blocked"), ...rendered.filter(({ todo }) => todo.status === "pending") ]; const itemLimit = Math.min(settings.maxVisibleItems ?? prioritized.length, prioritized.length); const completed = rendered.filter(({ todo }) => todo.status === "completed").length; const bodyBudget = Math.max(0, rowBudget - header.length); const selected = []; let selectedItems = 0; let clippedItem = false; for (const item of prioritized.slice(0, itemLimit)) { const remainingItems = prioritized.length - selectedItems - 1; const needsFooter = remainingItems > 0 || itemLimit < prioritized.length || settings.showCompleted && completed > 0; const available = bodyBudget - selected.length - (needsFooter ? 1 : 0); if (available <= 0) break; if (item.lines.length <= available) { selected.push(...item.lines); selectedItems += 1; continue; } if (selectedItems === 0) { selected.push(...clipLines(item.lines, available, width)); selectedItems += 1; clippedItem = true; } break; } const hidden = prioritized.length - selectedItems; const footerParts = []; if (settings.showCompleted && completed > 0) footerParts.push(`\u2713 ${completed} completed`); if (hidden > 0) footerParts.push(`\u2026 ${hidden} more`); if (clippedItem) footerParts.push("item truncated"); if (footerParts.length > 0 && selected.length < bodyBudget) { selected.push(theme.fg("dim", footerParts.join(" \xB7 "))); } return [...header, ...selected].slice(0, rowBudget).map((line) => truncateToWidth(line, width, "")); } function clipLines(lines, count, width) { const clipped = lines.slice(0, count); const last = clipped.at(-1); if (last !== void 0) clipped[clipped.length - 1] = truncateToWidth(`${last}\u2026`, width, "\u2026"); return clipped; } // src/todo-widget.ts var TOOL_NAME = "update_todo_list"; var WIDGET_KEY = "todo"; var TODO_CONTEXT_MESSAGE_TYPE = "todo-list-status"; var TODO_CONTEXT_VERSION = 3; var TODO_DETAILS_VERSION = 3; var TODO_RESTORED_BOUNDARY_ENTRY_TYPE = "todo-restored-context-boundary"; var MAX_TODOS = 50; var MAX_TODO_STEP_LENGTH = 300; var MAX_TODO_REASON_LENGTH = 200; var COMPLETION_SUMMARY_MS = 3e3; var TODO_MIGRATION_NOTICE = "pi-todo is moving to pi-progress. Confirm availability with `npm view @narumitw/pi-progress version`. Persistent user settings: run `pi remove npm:@narumitw/pi-todo`; then `pi install npm:@narumitw/pi-progress`. Persistent project settings: run `pi remove npm:@narumitw/pi-todo -l`; then `pi install npm:@narumitw/pi-progress -l`. Temporary or local `-e` load: switch its source instead; do not run `pi remove`. Restart Pi; do not load both packages."; var TODO_RESTORED_BOUNDARY_VERSION = 1; var PREVIOUS_TODO_CONTEXT_VERSION = 2; var PREVIOUS_TODO_DETAILS_VERSION = 2; var LEGACY_TODO_CONTEXT_VERSION = 1; var LEGACY_TODO_DETAILS_VERSION = 1; var WIDGET_OPTIONS = { placement: "aboveEditor" }; var LEGACY_TOOL_NAME = "todo_widget"; var TODO_STATUSES = ["pending", "in_progress", "completed", "blocked"]; var PREVIOUS_TODO_STATUSES = ["pending", "in_progress", "completed"]; var RESUBMIT_GUIDANCE = "Fix the input and resubmit the complete todos array."; var TodoParameters = Type.Object({ todos: Type.Array( Type.Object({ step: Type.String({ minLength: 1, maxLength: MAX_TODO_STEP_LENGTH, description: "A concise, action-oriented step" }), status: StringEnum(TODO_STATUSES, { description: "The step's current status" }), reason: Type.Optional( Type.String({ minLength: 1, maxLength: MAX_TODO_REASON_LENGTH, description: "Required only for blocked todos; explain what must unblock the step" }) ) }), { maxItems: MAX_TODOS, description: "The complete current todo list; send an empty array to clear it" } ) }); function todoWidgetExtension(pi, dependencies = {}) { const readSettings = dependencies.loadSettings ?? loadTodoSettings; const scheduleTimeout = dependencies.setTimeout ?? globalThis.setTimeout; const cancelTimeout = dependencies.clearTimeout ?? globalThis.clearTimeout; let activeSession; let todos = []; let settings = cloneDefaultSettings2(); let restoredBoundary; let settingsController = new AbortController(); let generation = 0; let completionTimer; let completionToken = 0; let completionSummaryHidden = false; const ownsSession = (ctx) => ctx.sessionManager === activeSession; const cancelCompletionSummary = () => { completionToken += 1; if (completionTimer !== void 0) cancelTimeout(completionTimer); completionTimer = void 0; }; const publish = (ctx) => { if (!ownsSession(ctx) || ctx.mode !== "tui") return; if (!settings.widget.enabled || todos.length === 0 || completionSummaryHidden) { ctx.ui.setWidget(WIDGET_KEY, void 0); return; } const snapshot = cloneTodos(todos); const widgetSettings = { ...settings.widget }; ctx.ui.setWidget( WIDGET_KEY, (tui, theme) => ({ render: (width) => renderTodoWidget(snapshot, theme, width, { settings: widgetSettings, terminalRows: tui.terminal.rows }), invalidate: () => { } }), WIDGET_OPTIONS ); }; const publishCompletionSummary = (ctx) => { cancelCompletionSummary(); completionSummaryHidden = false; if (!ownsSession(ctx) || ctx.mode !== "tui" || !settings.widget.enabled) { publish(ctx); return; } const total = todos.length; ctx.ui.setWidget( WIDGET_KEY, (_tui, theme) => ({ render: (width) => renderCompletionSummary(total, theme, width), invalidate: () => { } }), WIDGET_OPTIONS ); const ownerSession = activeSession; const token = completionToken; completionTimer = scheduleTimeout(() => { if (completionToken !== token || activeSession !== ownerSession || ctx.sessionManager !== ownerSession || !settings.widget.enabled || !allTodosCompleted(todos)) { return; } completionTimer = void 0; completionSummaryHidden = true; ctx.ui.setWidget(WIDGET_KEY, void 0); }, COMPLETION_SUMMARY_MS); }; pi.registerTool({ name: TOOL_NAME, label: "Todo List", description: "Replace the current session todo list with the complete supplied todos. Call update_todo_list whenever actual step state changes; keep at most one todo in_progress, require a reason for each blocked todo, and send an empty todos array to clear it.", promptSnippet: "Maintain the complete session todo list as multi-step work progresses", promptGuidelines: [ "Use update_todo_list to track work with multiple meaningful steps; skip it for simple, single-step tasks.", "Use update_todo_list to keep the list aligned with actual work: mark a step in_progress before starting it, mark it completed as soon as it finishes, and revise the list before continuing when the plan changes.", "Use blocked with a concise reason only when progress depends on an external action or condition; blocked does not mean completed.", "Before a progress report or final response, call update_todo_list to reconcile every todo with actual work; do not report completion while the list is stale.", "On every update_todo_list call, send the complete current todos array, keep at most one todo in_progress, and send an empty array when no tracked work remains." ], parameters: TodoParameters, prepareArguments: validateTodoArguments, async execute(_toolCallId, params, signal, _onUpdate, ctx) { signal?.throwIfAborted(); if (!ownsSession(ctx)) { throw new Error("Cannot update the todo list because the session changed."); } const nextTodos = validateTodoArguments(params).todos; const wasComplete = allTodosCompleted(todos); cancelCompletionSummary(); completionSummaryHidden = false; todos = cloneTodos(nextTodos); const becameComplete = todos.length > 0 && allTodosCompleted(todos) && !wasComplete; if (becameComplete) publishCompletionSummary(ctx); else publish(ctx); const details = { version: TODO_DETAILS_VERSION, todos: cloneTodos(todos) }; if (todos.length === 0) { return { content: [{ type: "text", text: "Todo list cleared." }], details }; } const completed = todos.filter((todo) => todo.status === "completed").length; const inProgress = todos.some((todo) => todo.status === "in_progress"); const blocked = todos.filter((todo) => todo.status === "blocked").length; const suffixes = [...inProgress ? ["1 in progress"] : [], ...blocked > 0 ? [`${blocked} blocked`] : []]; return { content: [ { type: "text", text: `Todo list updated: ${completed} of ${todos.length} complete${suffixes.length > 0 ? `; ${suffixes.join("; ")}` : ""}.` } ], details }; } }); const restoreBranchState = (ctx) => { const branch = ctx.sessionManager.getBranch(); todos = reconstructTodos(branch); restoredBoundary = reconstructRestoredTodoBoundary(branch); }; pi.on("session_start", async (_event, ctx) => { settingsController.abort(); settingsController = new AbortController(); const ownerController = settingsController; generation += 1; const ownerGeneration = generation; activeSession = ctx.sessionManager; cancelCompletionSummary(); completionSummaryHidden = false; settings = cloneDefaultSettings2(); restoreBranchState(ctx); if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_KEY, void 0); let loaded; try { loaded = await readSettings(void 0, ownerController.signal); } catch (error) { if (ownerController.signal.aborted || ownerGeneration !== generation || !ownsSession(ctx)) { return; } loaded = { kind: "invalid", path: "pi-todo.json", settings: cloneDefaultSettings2(), issue: error instanceof Error ? error.message : String(error) }; } if (ownerController.signal.aborted || ownerGeneration !== generation || !ownsSession(ctx)) { return; } settings = cloneSettings(loaded.settings); if (ctx.hasUI) ctx.ui.notify(TODO_MIGRATION_NOTICE, "warning"); if (loaded.kind === "invalid" && ctx.hasUI) { ctx.ui.notify( sanitizeTodoText(`Invalid pi-todo settings at ${loaded.path}; using defaults. ${loaded.issue}`), "warning" ); } publish(ctx); }); pi.on("context", (event, ctx) => { if (!ownsSession(ctx)) return; const summaryEpoch = leadingSummaryEpoch(event.messages); if (restoredBoundary?.summaryEpoch !== summaryEpoch) restoredBoundary = void 0; const messages = reconcileTodoContext(event.messages, todos, restoredBoundary?.content); if (restoredBoundary === void 0 && summaryEpoch) { const boundaryMessage = messages[leadingSummaryBoundary(messages)]; if (isTodoContextMessage(boundaryMessage)) { restoredBoundary = { summaryEpoch, content: boundaryMessage.content }; pi.appendEntry(TODO_RESTORED_BOUNDARY_ENTRY_TYPE, { version: TODO_RESTORED_BOUNDARY_VERSION, ...restoredBoundary }); } } if (messages !== event.messages) return { messages }; }); pi.on("session_tree", (_event, ctx) => { if (!ownsSession(ctx)) return; cancelCompletionSummary(); completionSummaryHidden = false; restoreBranchState(ctx); publish(ctx); }); pi.on("session_shutdown", (_event, ctx) => { if (!ownsSession(ctx)) return; settingsController.abort(); generation += 1; cancelCompletionSummary(); if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_KEY, void 0); todos = []; settings = cloneDefaultSettings2(); restoredBoundary = void 0; completionSummaryHidden = false; activeSession = void 0; }); } function reconcileTodoContext(messages, todos, restoredBoundaryContent) { const existing = messages.filter(isTodoContextMessage); const withoutExisting = messages.filter((message) => !isTodoContextMessage(message)); const summaryBoundary = leadingSummaryBoundary(withoutExisting); const currentContent = todos.length > 0 && !hasModelVisibleTodoState(withoutExisting, todos) ? todoContextContent(todos) : void 0; const content = hasLeadingSummary(withoutExisting, summaryBoundary) ? restoredBoundaryContent ?? currentContent : void 0; if (content !== void 0 && existing.length === 1 && messages[summaryBoundary] === existing[0] && existing[0]?.content === content && hasTodoContextVersion(existing[0])) { return messages; } if (existing.length === 0 && content === void 0) return messages; if (content === void 0) return withoutExisting; return [ ...withoutExisting.slice(0, summaryBoundary), { role: "custom", customType: TODO_CONTEXT_MESSAGE_TYPE, content, display: false, details: { version: TODO_CONTEXT_VERSION }, timestamp: 0 }, ...withoutExisting.slice(summaryBoundary) ]; } function validateTodoArguments(value) { if (!isRecord2(value)) rejectTodos("input must be an object with a todos array."); if (!Array.isArray(value.todos)) rejectTodos("todos must be an array."); if (value.todos.length > MAX_TODOS) { rejectTodos(`todos contains ${value.todos.length} items; the maximum is ${MAX_TODOS}.`); } const todos = []; const inProgressIndices = []; for (const [index, entry] of value.todos.entries()) { const item = index + 1; if (!isRecord2(entry)) rejectTodos(`item ${item} must be an object.`); if (typeof entry.step !== "string") rejectTodos(`item ${item} step must be a string.`); if (entry.step.trim().length === 0) { rejectTodos(`item ${item} step must contain non-whitespace text.`); } if (!hasMaxGraphemeLength(entry.step, MAX_TODO_STEP_LENGTH)) { rejectTodos(`item ${item} step exceeds ${MAX_TODO_STEP_LENGTH} characters.`); } if (!TODO_STATUSES.includes(entry.status)) { rejectTodos(`item ${item} status must be pending, in_progress, completed, or blocked.`); } const status = entry.status; if (status === "in_progress") inProgressIndices.push(item); if (status === "blocked") { if (typeof entry.reason !== "string" || entry.reason.trim().length === 0) { rejectTodos(`item ${item} is blocked and requires a non-whitespace reason.`); } if (!hasMaxGraphemeLength(entry.reason, MAX_TODO_REASON_LENGTH)) { rejectTodos(`item ${item} reason exceeds ${MAX_TODO_REASON_LENGTH} characters.`); } todos.push({ step: entry.step, status, reason: entry.reason }); continue; } if (Object.hasOwn(entry, "reason")) { rejectTodos(`item ${item} may include reason only when status is blocked.`); } todos.push({ step: entry.step, status }); } if (inProgressIndices.length > 1) { rejectTodos(`items ${inProgressIndices.join(" and ")} are in_progress; keep at most one in_progress item.`); } return { todos }; } function rejectTodos(message) { throw new Error(`Todo list rejected: ${message} ${RESUBMIT_GUIDANCE}`); } function todoContextContent(todos) { return `${todoContextPrefix(TODO_CONTEXT_VERSION)}${JSON.stringify({ todos })}`; } function reconstructRestoredTodoBoundary(entries) { const summaryEpoch = leadingSummaryEpoch(buildSessionContext([...entries]).messages); if (!summaryEpoch) return void 0; for (let index = entries.length - 1; index >= 0; index -= 1) { const entry = entries[index]; if (entry?.type !== "custom" || entry.customType !== TODO_RESTORED_BOUNDARY_ENTRY_TYPE) { continue; } if (!isRestoredTodoBoundaryData(entry.data, summaryEpoch)) continue; return { summaryEpoch, content: entry.data.content }; } return void 0; } function isRestoredTodoBoundaryData(value, summaryEpoch) { if (!isRecord2(value)) return false; if (value.version !== TODO_RESTORED_BOUNDARY_VERSION || value.summaryEpoch !== summaryEpoch || typeof value.content !== "string") { return false; } return isCanonicalTodoContextContent(value.content); } function isCanonicalTodoContextContent(content) { const currentPrefix = todoContextPrefix(TODO_CONTEXT_VERSION); if (content.startsWith(currentPrefix)) { try { const restoredTodos = currentTodosFromToolArguments(JSON.parse(content.slice(currentPrefix.length))); return restoredTodos !== void 0 && restoredTodos.length > 0 && todoContextContent(restoredTodos) === content; } catch { return false; } } const previousPrefix = todoContextPrefix(PREVIOUS_TODO_CONTEXT_VERSION); if (content.startsWith(previousPrefix)) { try { const restoredTodos = previousTodoDataFromToolArguments(JSON.parse(content.slice(previousPrefix.length))); return restoredTodos !== void 0 && restoredTodos.length > 0 && previousTodoContextContent(restoredTodos) === content; } catch { return false; } } const legacyPrefix = todoContextPrefix(LEGACY_TODO_CONTEXT_VERSION); if (!content.startsWith(legacyPrefix)) return false; try { const restoredItems = JSON.parse(content.slice(legacyPrefix.length)); return isLegacyTodoItems(restoredItems) && restoredItems.length > 0 && legacyTodoContextContent(restoredItems) === content; } catch { return false; } } function todoContextPrefix(version) { return `[PI TODO STATUS v${version}] Current todo list as JSON data: `; } function previousTodoContextContent(todos) { const canonicalTodos = todos.map((todo) => ({ step: todo.step, status: todo.status })); return `${todoContextPrefix(PREVIOUS_TODO_CONTEXT_VERSION)}${JSON.stringify({ todos: canonicalTodos })}`; } function legacyTodoContextContent(items) { const canonicalItems = items.map((item) => ({ text: item.text, status: item.status })); return `${todoContextPrefix(LEGACY_TODO_CONTEXT_VERSION)}${JSON.stringify(canonicalItems)}`; } function hasModelVisibleTodoState(messages, todos) { const currentResults = /* @__PURE__ */ new Map(); for (const message of messages) { if (message.role !== "toolResult" || message.isError || message.toolName !== TOOL_NAME && message.toolName !== LEGACY_TOOL_NAME) { continue; } const resultTodos = todosFromDetails(message.details); if (resultTodos !== void 0 && todosEqual(resultTodos, todos)) { currentResults.set(message.toolCallId, message.toolName); } } if (currentResults.size === 0) return false; return messages.some( (message) => message.role === "assistant" && message.content.some((content) => { if (content.type !== "toolCall" || currentResults.get(content.id) !== content.name) { return false; } const argumentTodos = todosFromToolArguments(content.arguments); return argumentTodos !== void 0 && todosEqual(argumentTodos, todos); }) ); } function todosFromToolArguments(value) { return currentTodosFromToolArguments(value) ?? previousTodosFromToolArguments(value); } function currentTodosFromToolArguments(value) { if (!isRecord2(value)) return void 0; if (isTodos(value.todos)) return cloneTodos(value.todos); if (isLegacyTodoItems(value.items)) return migrateLegacyItems(value.items); return void 0; } function previousTodosFromToolArguments(value) { const previous = previousTodoDataFromToolArguments(value); return previous ? migratePreviousTodos(previous) : void 0; } function previousTodoDataFromToolArguments(value) { if (!isRecord2(value) || !isPreviousTodos(value.todos)) return void 0; return value.todos.map((todo) => ({ step: todo.step, status: todo.status })); } function isTodoContextMessage(message) { return message.role === "custom" && message.customType === TODO_CONTEXT_MESSAGE_TYPE; } function hasTodoContextVersion(message) { return isRecord2(message.details) && message.details.version === TODO_CONTEXT_VERSION; } function leadingSummaryEpoch(messages) { const summaryStart = messages[0]?.role === "system" ? 1 : 0; const boundary = leadingSummaryBoundary(messages); return boundary === summaryStart ? void 0 : JSON.stringify(messages.slice(summaryStart, boundary)); } function leadingSummaryBoundary(messages) { let index = messages[0]?.role === "system" ? 1 : 0; while (index < messages.length) { const role = messages[index]?.role; if (role !== "compactionSummary" && role !== "branchSummary") break; index += 1; } return index; } function hasLeadingSummary(messages, boundary) { const summaryStart = messages[0]?.role === "system" ? 1 : 0; return boundary > summaryStart; } function reconstructTodos(entries) { let restored = []; for (const entry of entries) { if (entry.type !== "message") continue; const message = entry.message; if (message.role !== "toolResult" || message.isError || message.toolName !== TOOL_NAME && message.toolName !== LEGACY_TOOL_NAME) { continue; } const resultTodos = todosFromDetails(message.details); if (resultTodos !== void 0) restored = resultTodos; } return restored; } function todosFromDetails(value) { if (!isRecord2(value)) return void 0; if (value.version === TODO_DETAILS_VERSION && isTodos(value.todos)) { return cloneTodos(value.todos); } if (value.version === PREVIOUS_TODO_DETAILS_VERSION && isPreviousTodos(value.todos)) { return migratePreviousTodos(value.todos); } if (value.version === LEGACY_TODO_DETAILS_VERSION && isLegacyTodoItems(value.items)) { return migrateLegacyItems(value.items); } return void 0; } function isTodos(value) { if (!Array.isArray(value) || value.length > MAX_TODOS) return false; let inProgressCount = 0; for (const entry of value) { if (!isRecord2(entry)) return false; if (typeof entry.step !== "string" || entry.step.trim().length === 0 || !hasMaxGraphemeLength(entry.step, MAX_TODO_STEP_LENGTH) || !TODO_STATUSES.includes(entry.status)) { return false; } if (entry.status === "in_progress") inProgressCount += 1; if (entry.status === "blocked") { if (typeof entry.reason !== "string" || entry.reason.trim().length === 0 || !hasMaxGraphemeLength(entry.reason, MAX_TODO_REASON_LENGTH)) { return false; } } else if (Object.hasOwn(entry, "reason")) { return false; } } return inProgressCount <= 1; } function isPreviousTodos(value) { return hasPreviousTodoShape(value, "step"); } function isLegacyTodoItems(value) { return hasPreviousTodoShape(value, "text"); } function hasPreviousTodoShape(value, stepProperty) { if (!Array.isArray(value) || value.length > MAX_TODOS) return false; let inProgressCount = 0; for (const entry of value) { if (!isRecord2(entry) || Object.hasOwn(entry, "reason")) return false; const step = entry[stepProperty]; if (typeof step !== "string" || step.trim().length === 0 || !hasMaxGraphemeLength(step, MAX_TODO_STEP_LENGTH) || !PREVIOUS_TODO_STATUSES.includes(entry.status)) { return false; } if (entry.status === "in_progress") inProgressCount += 1; } return inProgressCount <= 1; } function todosEqual(left, right) { return left.length === right.length && left.every( (todo, index) => todo.step === right[index]?.step && todo.status === right[index]?.status && todo.reason === right[index]?.reason ); } function migratePreviousTodos(todos) { return todos.map((todo) => ({ step: todo.step, status: todo.status })); } function migrateLegacyItems(items) { return items.map((item) => ({ step: item.text, status: item.status })); } function cloneTodos(todos) { return todos.map((todo) => ({ step: todo.step, status: todo.status, ...todo.reason === void 0 ? {} : { reason: todo.reason } })); } function allTodosCompleted(value) { return value.length > 0 && value.every((todo) => todo.status === "completed"); } function hasMaxGraphemeLength(value, maximum) { let count = 0; let index = 0; while (index < value.length) { index = nextGraphemeClusterIndex(value, index); count += 1; if (count > maximum) return false; } return true; } function nextGraphemeClusterIndex(value, clusterStart) { const start = value.codePointAt(clusterStart) ?? 0; let clusterEnd = clusterStart + codePointLength(start); clusterEnd = consumeGraphemeModifiers(value, clusterEnd); while (clusterEnd < value.length - 1 && value.codePointAt(clusterEnd) === 8205) { const next = value.codePointAt(clusterEnd + 1) ?? 0; clusterEnd += 1 + codePointLength(next); clusterEnd = consumeGraphemeModifiers(value, clusterEnd); } if (isBetween(start, 127462, 127487) && clusterEnd < value.length && isBetween(value.codePointAt(clusterEnd) ?? 0, 127462, 127487)) { clusterEnd += codePointLength(value.codePointAt(clusterEnd) ?? 0); } return clusterEnd; } function consumeGraphemeModifiers(value, start) { let index = start; while (index < value.length) { const point = value.codePointAt(index) ?? 0; if (!isCombiningMark(point) && !isBetween(point, 65024, 65039)) break; index += codePointLength(point); } return index; } function isCombiningMark(value) { return isBetween(value, 768, 879) || isBetween(value, 6832, 6911) || isBetween(value, 7616, 7679) || isBetween(value, 65056, 65071); } function codePointLength(value) { return value > 65535 ? 2 : 1; } function isBetween(value, minimum, maximum) { return value >= minimum && value <= maximum; } function isRecord2(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function cloneDefaultSettings2() { return { widget: { ...DEFAULT_TODO_SETTINGS.widget } }; } function cloneSettings(value) { return { widget: { ...value.widget } }; } export { todoWidgetExtension as default }; //# sourceMappingURL=index.ts.map