99 lines · 3.2 KB · TypeScript
RawDownload
// Pure, dependency-free parsing/serialization for the agent Plan pane.//// The plan is stored as Markdown (Cursor-style: a `### To-dos` section with// `- [ ]` / `- [x]` checkbox lines). This module is the single source of truth// for turning that Markdown into structured todos and writing status changes// back, and is shared by both the server store and the client pane. In practice// models sometimes omit the leading list marker (`[ ] task`), so parsing accepts// both forms and status rewrites preserve the user's original shape. export type PlanTodoStatus = "pending" | "in_progress" | "completed" | "cancelled"; export type PlanTodo = {  id: string;  content: string;  status: PlanTodoStatus;  /** Index of the source checkbox line within the Markdown, for round-tripping. */  lineIndex: number;}; // Matches a Markdown checkbox item, capturing indent, optional bullet, status// mark, body. Accepted forms: `- [ ] task`, `* [ ] task`, `+ [ ] task`, and// `[ ] task`.// Accepted marks: " " pending, "x"/"X" completed, "-" cancelled,// "~"/">"/"/" in progress.const TODO_LINE = /^(\s*)(?:([-*+]\s+)?)\[([ xX~>/-])\]\s?(.*)$/; export function markToStatus(mark: string): PlanTodoStatus {  switch (mark) {    case "x":    case "X":      return "completed";    case "-":      return "cancelled";    case "~":    case ">":    case "/":      return "in_progress";    default:      return "pending";  }} export function statusToMark(status: PlanTodoStatus): string {  switch (status) {    case "completed":      return "x";    case "cancelled":      return "-";    case "in_progress":      return "/";    default:      return " ";  }} export function parsePlanTodos(markdown: string): PlanTodo[] {  const lines = markdown.split("\n");  const todos: PlanTodo[] = [];  for (let i = 0; i < lines.length; i++) {    const match = TODO_LINE.exec(lines[i]);    if (!match) continue;    todos.push({      id: `todo-${i}`,      content: match[4].trim(),      status: markToStatus(match[3]),      lineIndex: i,    });  }  return todos;} // Rewrite the checkbox mark on the given source line to reflect `status`.// Returns the original markdown unchanged if the line is not a checkbox.export function setTodoStatusAtLine(  markdown: string,  lineIndex: number,  status: PlanTodoStatus,): string {  const lines = markdown.split("\n");  if (lineIndex < 0 || lineIndex >= lines.length) return markdown;  const match = TODO_LINE.exec(lines[lineIndex]);  if (!match) return markdown;  const [, indent, bullet = "", , body] = match;  lines[lineIndex] = `${indent}${bullet}[${statusToMark(status)}] ${body}`;  return lines.join("\n");} const STATUS_CYCLE: PlanTodoStatus[] = ["pending", "in_progress", "completed", "cancelled"]; export function nextStatus(status: PlanTodoStatus): PlanTodoStatus {  const index = STATUS_CYCLE.indexOf(status);  return STATUS_CYCLE[(index + 1) % STATUS_CYCLE.length];} export function planProgress(todos: PlanTodo[]): { done: number; total: number } {  const total = todos.filter((todo) => todo.status !== "cancelled").length;  const done = todos.filter((todo) => todo.status === "completed").length;  return { done, total };}