import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { Type, type Static } from "typebox"; import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { constants } from "node:fs"; import path from "node:path"; import { spawn } from "node:child_process"; const LC_ORIGIN = "https://leetcode.com"; const STATE_RELATIVE_PATH = path.join(".pi", "leetcode", "state.json"); const USAGE_RELATIVE_PATH = path.join(".pi", "leetcode", "usage.json"); const DEFAULT_LIMIT = 50; type NotifyKind = "info" | "warning" | "error"; type LangSlug = "typescript" | "javascript" | "python3" | "cpp" | "java" | "golang" | "rust"; type LeetCodeCodeSnippet = { lang: string; langSlug: string; code: string }; type LeetCodeQuestion = { questionId: string; questionFrontendId: string; title: string; titleSlug: string; difficulty: string; content: string; translatedContent: string | null; exampleTestcases: string; metaData: string; codeSnippets: LeetCodeCodeSnippet[]; topicTags: Array<{ name: string; slug: string }>; }; type ProblemStatus = "started" | "edited" | "ran" | "submit-ready" | "solved"; type ProblemState = { questionId: string; title: string; titleSlug: string; difficulty: string; problemPath: string; solutionPath: string; notesPath?: string; langSlug: string; exampleTestcases: string; status?: ProblemStatus; lastAction?: string; lastRunSummary?: string; learningSummary?: string; }; type ExtensionState = { currentSlug?: string; preferredLang: LangSlug; languageSelected?: boolean; recentSlugs?: string[]; interviewMode: boolean; problems: Record; }; type UsageStats = { createdAt: string; updatedAt: string; commands: Record; actions: Record; languages: Record; difficulties: Record; }; const leetcodeFetchSchema = Type.Object({ titleSlug: Type.String({ description: "LeetCode problem title slug, for example: two-sum" }), }); type LeetcodeFetchInput = Static; const LANGUAGE_OPTIONS: Array<{ slug: LangSlug; label: string }> = [ { slug: "typescript", label: "TypeScript" }, { slug: "javascript", label: "JavaScript" }, { slug: "python3", label: "Python3" }, { slug: "cpp", label: "C++" }, { slug: "java", label: "Java" }, { slug: "golang", label: "Go" }, { slug: "rust", label: "Rust" }, ]; const DEFAULT_STATE: ExtensionState = { preferredLang: normalizeLang(process.env.LEETCODE_LANG), languageSelected: Boolean(process.env.LEETCODE_LANG), recentSlugs: [], interviewMode: false, problems: {}, }; function normalizeLang(value: string | undefined): LangSlug { if (value === "javascript" || value === "python3" || value === "cpp" || value === "java" || value === "golang" || value === "rust") return value; return "typescript"; } function extensionFor(langSlug: string): string { const map: Record = { typescript: "ts", javascript: "js", python3: "py", cpp: "cpp", java: "java", golang: "go", rust: "rs", }; return map[langSlug] ?? "txt"; } function languageLabel(langSlug: string): string { return LANGUAGE_OPTIONS.find((option) => option.slug === langSlug)?.label ?? langSlug; } function statePath(cwd: string): string { return path.join(cwd, STATE_RELATIVE_PATH); } function usagePath(cwd: string): string { return path.join(cwd, USAGE_RELATIVE_PATH); } function emptyUsageStats(): UsageStats { const now = new Date().toISOString(); return { createdAt: now, updatedAt: now, commands: {}, actions: {}, languages: {}, difficulties: {} }; } function incrementCounter(target: Record, key: string | undefined): void { if (!key) return; target[key] = (target[key] ?? 0) + 1; } async function trackUsage(cwd: string, event: { command?: string; action?: string; language?: string; difficulty?: string }): Promise { const file = usagePath(cwd); let stats = emptyUsageStats(); if (await fileExists(file)) { try { stats = { ...stats, ...JSON.parse(await readFile(file, "utf8")) } as UsageStats; stats.commands ??= {}; stats.actions ??= {}; stats.languages ??= {}; stats.difficulties ??= {}; } catch { // Keep a fresh stats file if the old one is malformed. } } incrementCounter(stats.commands, event.command); incrementCounter(stats.actions, event.action); incrementCounter(stats.languages, event.language); incrementCounter(stats.difficulties, event.difficulty); stats.updatedAt = new Date().toISOString(); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, `${JSON.stringify(stats, null, 2)}\n`, "utf8"); } function notify(ctx: ExtensionContext, message: string, level: NotifyKind = "info"): void { ctx.ui.notify(message, level); } async function fileExists(file: string): Promise { try { await access(file, constants.F_OK); return true; } catch { return false; } } async function loadState(cwd: string): Promise { const file = statePath(cwd); if (!(await fileExists(file))) return { ...DEFAULT_STATE, problems: {} }; const parsed = JSON.parse(await readFile(file, "utf8")) as Partial; return { ...DEFAULT_STATE, ...parsed, preferredLang: normalizeLang(parsed.preferredLang), recentSlugs: parsed.recentSlugs ?? [], problems: parsed.problems ?? {}, }; } async function saveState(cwd: string, state: ExtensionState): Promise { const file = statePath(cwd); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, `${JSON.stringify(state, null, 2)}\n`, "utf8"); } function publicJsonHeaders(referer = `${LC_ORIGIN}/`): HeadersInit { return { Accept: "application/json, text/plain, */*", "Content-Type": "application/json", "User-Agent": "pi-leetcode-practice/0.1.0", Referer: referer, }; } async function postJson(url: string, body: unknown, signal?: AbortSignal, referer?: string): Promise { const res = await fetch(url, { method: "POST", headers: publicJsonHeaders(referer), body: JSON.stringify(body), signal, }); const text = await res.text(); if (!res.ok) throw new Error(`LeetCode POST ${url} failed: ${res.status} ${res.statusText}\n${text}`); return JSON.parse(text) as T; } function stripHtml(html: string): string { return html .replace(/
/gi, "\n```\n")
    .replace(/<\/pre>/gi, "\n```\n")
    .replace(//gi, "`")
    .replace(/<\/code>/gi, "`")
    .replace(/
  • /gi, "\n- ") .replace(//gi, "\n") .replace(/<\/p>/gi, "\n\n") .replace(/<[^>]+>/g, "") .replace(/ /g, " ") .replace(/</g, "<") .replace(/>/g, ">") .replace(/&/g, "&") .trim(); } function problemMarkdown(question: LeetCodeQuestion): string { return `# ${question.questionFrontendId}. ${question.title}\n\n` + `- **Difficulty:** ${question.difficulty}\n` + `- **Slug:** \`${question.titleSlug}\`\n` + `- **Question ID:** ${question.questionId}\n\n` + `## Problem\n\n${stripHtml(question.content)}\n\n` + `## Example Testcases\n\n\`\`\`text\n${question.exampleTestcases}\n\`\`\`\n\n` + `## Metadata / Constraints\n\n\`\`\`json\n${question.metaData}\n\`\`\`\n\n` + `## Topics\n\n${question.topicTags.map((t) => `- ${t.name}`).join("\n") || "- (none)"}\n`; } async function fetchQuestion(titleSlug: string, signal?: AbortSignal): Promise { const payload = { operationName: "questionData", variables: { titleSlug }, query: `query questionData($titleSlug: String!) { question(titleSlug: $titleSlug) { questionId questionFrontendId title titleSlug difficulty content translatedContent exampleTestcases metaData codeSnippets { lang langSlug code } topicTags { name slug } } }`, }; const json = await postJson<{ data?: { question?: LeetCodeQuestion | null }; errors?: unknown }>(`${LC_ORIGIN}/graphql`, payload, signal); if (!json.data?.question) throw new Error(`LeetCode question not found: ${titleSlug}. ${JSON.stringify(json.errors ?? {})}`); return json.data.question; } async function listProblems(keyword: string, skip: number, limit: number, signal?: AbortSignal): Promise> { const payload = { operationName: "problemsetQuestionListV2", variables: { categorySlug: "", skip, limit: keyword ? Math.max(limit, 200) : limit, filters: { filterCombineType: "ALL" }, }, query: `query problemsetQuestionListV2($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionFilterInput) { problemsetQuestionListV2(categorySlug: $categorySlug, limit: $limit, skip: $skip, filters: $filters) { questions { questionFrontendId title titleSlug difficulty acRate } } }`, }; const json = await postJson<{ data?: { problemsetQuestionListV2?: { questions?: Array<{ title: string; titleSlug: string; difficulty: string; questionFrontendId: string; acRate: number }> } } }>(`${LC_ORIGIN}/graphql`, payload, signal); const questions = json.data?.problemsetQuestionListV2?.questions ?? []; if (!keyword) return questions; const needle = keyword.toLowerCase(); return questions.filter((q) => q.title.toLowerCase().includes(needle) || q.titleSlug.toLowerCase().includes(needle) || q.questionFrontendId === keyword).slice(0, limit); } async function activeProblem(ctx: ExtensionContext): Promise<{ state: ExtensionState; problem: ProblemState }> { const state = await loadState(ctx.cwd); if (!state.currentSlug) throw new Error("No active LeetCode problem. Run /lc-start first."); const problem = state.problems[state.currentSlug]; if (!problem) throw new Error("Active LeetCode problem state is corrupt. Run /lc-start again."); return { state, problem }; } function extractMarkdownSection(markdown: string, heading: string): string[] { const pattern = new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*$`, "im"); const match = pattern.exec(markdown); if (!match) return []; const rest = markdown.slice(match.index + match[0].length); const next = /^##\s+/m.exec(rest); const body = (next ? rest.slice(0, next.index) : rest).trim(); return body .split("\n") .map((line) => line.trim()) .filter((line) => line.startsWith("-") && !/^[-*]\s*TODO\b/i.test(line)) .slice(0, 5); } function leetcodeProblemUrl(titleSlug: string): string { return `${LC_ORIGIN}/problems/${titleSlug}/`; } async function openExternalUrl(url: string): Promise { const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open"; const args = process.platform === "win32" ? ["/c", "start", "", url] : [url]; await new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: "ignore", detached: true }); child.on("error", reject); child.on("spawn", () => { child.unref(); resolve(); }); }); } function helpMarkdown(): string { return `# Pi LeetCode Terminal Sandbox Help No LeetCode cookies are required for the default workflow. Pi fetches problem text, writes local files, and opens the LeetCode page for manual Run/Submit. Optional language default: \`\`\`bash # optional: typescript, javascript, python3, cpp, java, golang, rust export LEETCODE_LANG=typescript \`\`\` ## Typical flow \`\`\`text /lc-start two-sum # choose explain, write-a-bit, edit, debug, run, submit, or solve /lc-next # use /lc-next anytime to show the action menu again # Or browse first: /lc-list two pointers \`\`\` ## Commands - \`/lc-start [titleSlug]\` — Start a problem. With no slug, opens problem search/selection. - \`/lc-next\` — Show the “What next?” action menu again for the active problem. - \`/lc-lang\` — Choose your LeetCode solution language. - \`/lc-list [search]\` — Search/select a problem, then continue with \`/lc-start\`. - \`/lc-help\` — Show this help. That is the whole user-facing command set. Use \`/lc-next\` for explain, hint, write-a-bit, edit, debug, run, submit prep, optimize, solve, or learning-history review. ## Files - State: \`.pi/leetcode/state.json\` - Problems: \`leetcode//problem.md\` - Solutions: \`leetcode//solution.\` `; } export default function leetcodeExtension(pi: ExtensionAPI): void { pi.on("session_start", async (_event, ctx) => { const state = await loadState(ctx.cwd); if (process.env.LEETCODE_LANG) { state.preferredLang = normalizeLang(process.env.LEETCODE_LANG); state.languageSelected = true; } else if (!state.languageSelected && ctx.hasUI) { const choice = await ctx.ui.select( "Choose LeetCode language", LANGUAGE_OPTIONS.map((option) => `${option.slug} — ${option.label}`), ); if (choice) { state.preferredLang = normalizeLang(choice.split(" — ")[0]); state.languageSelected = true; notify(ctx, `LeetCode language set to ${languageLabel(state.preferredLang)}.`); } } await saveState(ctx.cwd, state); notify(ctx, "LeetCode sandbox ready. Run /lc-help for commands."); }); pi.registerTool({ name: "leetcode_fetch", label: "LeetCode Fetch", description: "Fetch LeetCode questionData via https://leetcode.com/graphql.", promptSnippet: "Fetch LeetCode constraints, difficulty, metadata, and starter code snippets.", promptGuidelines: ["Use leetcode_fetch when problem metadata or starter code is needed for a LeetCode title slug."], parameters: leetcodeFetchSchema, async execute(_toolCallId, params: LeetcodeFetchInput, signal) { const question = await fetchQuestion(params.titleSlug, signal); const summary = [ `Fetched ${question.questionFrontendId}. ${question.title} (${question.difficulty})`, `Slug: ${question.titleSlug}`, `Examples: ${question.exampleTestcases.split("\n").length} input line(s)`, `Topics: ${question.topicTags.map((t) => t.name).join(", ") || "none"}`, "Full metadata is available to the agent in tool details; not dumping JSON to the transcript.", ].join("\n"); return { content: [{ type: "text", text: summary }], details: question }; }, }); pi.registerCommand("lc-help", { description: "Show LeetCode sandbox help, auth setup, and common workflows", handler: async (_args, ctx) => { await trackUsage(ctx.cwd, { command: "lc-help" }); const text = helpMarkdown(); pi.sendMessage({ customType: "leetcode-help", content: text, display: true, details: {} }); notify(ctx, "Displayed /lc-help in the transcript."); }, }); async function chooseProblemFromSearch(args: string, ctx: ExtensionContext): Promise { const typed = args.trim(); const keyword = typed || (await ctx.ui.input("Find LeetCode problem", "Type title/slug/topic (e.g. 3sum, graph), or press Enter for top 50"))?.trim() || ""; if (!keyword) notify(ctx, "Showing top 50 LeetCode problems. Type a search next time to narrow results."); const questions = await listProblems(keyword, 0, DEFAULT_LIMIT); if (questions.length === 0) { notify(ctx, "No LeetCode problems matched.", "warning"); return; } const choice = await ctx.ui.select( keyword ? `Select LeetCode problem matching “${keyword}”` : "Select LeetCode problem — top 50", questions.map((q) => `${q.titleSlug} — ${q.questionFrontendId}. ${q.title} [${q.difficulty}] ${q.acRate.toFixed(1)}%`), ); if (!choice) return; pi.sendUserMessage(`/lc-start ${choice.split(" — ")[0]}`, { deliverAs: "followUp" }); } async function chooseLanguage(ctx: ExtensionContext, reason = "Choose LeetCode language"): Promise { const state = await loadState(ctx.cwd); const choice = await ctx.ui.select( reason, LANGUAGE_OPTIONS.map((option) => `${option.slug} — ${option.label}${option.slug === state.preferredLang ? " (current)" : ""}`), ); if (!choice) return undefined; const selected = normalizeLang(choice.split(" — ")[0]); if (selected !== state.preferredLang && Object.keys(state.problems).length > 0) { const ok = await ctx.ui.confirm( "Change language?", `Existing solution files stay as-is. New problems will use solution.${extensionFor(selected)}. Continue?`, ); if (!ok) return undefined; } state.preferredLang = selected; state.languageSelected = true; await saveState(ctx.cwd, state); await trackUsage(ctx.cwd, { command: "lc-lang", action: "language-selected", language: selected }); notify(ctx, `LeetCode language set to ${languageLabel(selected)}. New problems will use solution.${extensionFor(selected)}.`); return selected; } pi.registerCommand("lc-lang", { description: "Choose the preferred LeetCode solution language", handler: async (_args, ctx) => { await chooseLanguage(ctx); }, }); pi.registerCommand("lc-list", { description: "Search/select a LeetCode problem", handler: async (args, ctx) => { await trackUsage(ctx.cwd, { command: "lc-list", action: args.trim() ? "search-with-query" : "search-opened" }); await chooseProblemFromSearch(args, ctx); }, }); async function fetchIntoWorkspace(ctx: ExtensionContext, titleSlug: string): Promise { const state = await loadState(ctx.cwd); const question = await fetchQuestion(titleSlug); const snippet = question.codeSnippets.find((s) => s.langSlug === state.preferredLang) ?? question.codeSnippets.find((s) => s.langSlug === "typescript") ?? question.codeSnippets[0]; if (!snippet) throw new Error(`No starter code snippets available for ${titleSlug}`); const dir = path.join(ctx.cwd, "leetcode", question.titleSlug); await mkdir(dir, { recursive: true }); const problemAbs = path.join(dir, "problem.md"); const solutionAbs = path.join(dir, `solution.${extensionFor(snippet.langSlug)}`); const notesAbs = path.join(dir, "notes.md"); await writeFile(problemAbs, problemMarkdown(question), "utf8"); if (!(await fileExists(solutionAbs))) await writeFile(solutionAbs, `${snippet.code}\n`, "utf8"); if (!(await fileExists(notesAbs))) { await writeFile(notesAbs, `# ${question.questionFrontendId}. ${question.title} Notes\n\n## Highlights\n\n- TODO\n\n## Approach\n\n- TODO\n\n## Mistakes / Debugging\n\n- TODO\n\n## Edge Cases\n\n- TODO\n\n## Complexity\n\n- Time: TODO\n- Space: TODO\n`, "utf8"); } state.currentSlug = question.titleSlug; state.recentSlugs = [question.titleSlug, ...(state.recentSlugs ?? []).filter((slug) => slug !== question.titleSlug)].slice(0, 5); state.preferredLang = normalizeLang(snippet.langSlug); state.problems[question.titleSlug] = { questionId: question.questionId, title: question.title, titleSlug: question.titleSlug, difficulty: question.difficulty, problemPath: path.relative(ctx.cwd, problemAbs), solutionPath: path.relative(ctx.cwd, solutionAbs), notesPath: path.join("leetcode", question.titleSlug, "notes.md"), langSlug: snippet.langSlug, exampleTestcases: question.exampleTestcases, status: "started", lastAction: "started", }; await saveState(ctx.cwd, state); return state.problems[question.titleSlug]; } async function openEditor(ctx: ExtensionContext, solutionPath: string): Promise { const editor = process.env.EDITOR || process.env.VISUAL || "vi"; const solutionAbs = path.resolve(ctx.cwd, solutionPath); await new Promise((resolve, reject) => { const child = spawn(editor, [solutionAbs], { cwd: ctx.cwd, stdio: "inherit", shell: true }); child.on("error", reject); child.on("exit", (code, signal) => { if (code === 0) resolve(); else reject(new Error(`${editor} exited with code=${code ?? "null"} signal=${signal ?? "null"}`)); }); }); } function explainProblem(problem: ProblemState): void { pi.sendUserMessage( `Explain how to solve LeetCode ${problem.titleSlug}. Read ${problem.problemPath} and optionally inspect ${problem.solutionPath} for context. Do not edit solution code, do not run tests, and do not submit. Explain in plain text using simplified terms: (1) the core idea, (2) step-by-step algorithm, (3) how to avoid duplicates or tricky cases if relevant, (4) a small walkthrough on one example, (5) time and space complexity. Then append concise Highlights, Approach, and Complexity bullets to ${problem.notesPath ?? "notes.md"}. Avoid dumping full code unless the user explicitly asks.`, { deliverAs: "followUp" }, ); } function hintProblem(problem: ProblemState): void { pi.sendUserMessage( `Give LeetCode hints for ${problem.titleSlug}. Read ${problem.problemPath} and inspect ${problem.solutionPath} only for context. Hint level: progressive. Do not edit files, do not run tests, and do not submit. Provide at most 3 concise progressive hints, then one complexity target. Avoid revealing full code unless the user explicitly asked for a full solution.`, { deliverAs: "followUp" }, ); } function debugProblem(problem: ProblemState): void { pi.sendUserMessage( `Debug my LeetCode solution for ${problem.titleSlug}. Read ${problem.problemPath} and ${problem.solutionPath}. Symptom/failing case: no specific failing case provided. Do not edit solution code, do not run tests, and do not submit. Explain in plain text where my current approach/code is likely wrong. Be specific: (1) suspected bug or missing case, (2) why it fails, (3) minimal counterexample if possible, (4) smallest fix direction without giving a full replacement solution unless asked. Then append a concise Mistakes / Debugging entry to ${problem.notesPath ?? "notes.md"}.`, { deliverAs: "followUp" }, ); } function writeABit(problem: ProblemState): void { pi.sendUserMessage( `Write-a-bit mode for LeetCode ${problem.titleSlug}. Read ${problem.problemPath} and ${problem.solutionPath}. Make exactly one small progressive edit to ${problem.solutionPath} focused on the next smallest useful step. Do not complete the full solution unless it is already essentially complete. Prefer scaffolding, edge-case handling, data structures, helper signatures, loop skeletons, or TODO-guided partial logic. Leave at least one meaningful TODO or incomplete section for the user. Do not run tests and do not submit. After editing, report only: (1) what changed, (2) what remains, (3) the next small user step.`, { deliverAs: "followUp" }, ); } function optimizeProblem(problem: ProblemState): void { pi.sendUserMessage( `Optimize review for LeetCode ${problem.titleSlug}. Read ${problem.problemPath}, ${problem.solutionPath}, and ${problem.notesPath ?? "notes.md if present"}. Do not edit solution code, do not run tests, and do not submit. Render a terminal-friendly side-by-side comparison table with columns Current and Recommended. Include: approach summary, duplicate/tricky-case handling, complexity, and minimal patch guidance. Then append concise Highlights / Mistakes / Complexity bullets to ${problem.notesPath ?? "notes.md"}. Keep it practical and concise.`, { deliverAs: "followUp" }, ); } async function openLeetCodePage(ctx: ExtensionContext, problem: ProblemState, action: "run" | "submit"): Promise { const url = leetcodeProblemUrl(problem.titleSlug); try { await openExternalUrl(url); notify(ctx, `Opened LeetCode page for ${problem.title}.`); } catch (error) { notify(ctx, `Could not open browser automatically: ${error instanceof Error ? error.message : String(error)}`, "warning"); } pi.sendMessage({ customType: action === "run" ? "leetcode-open-run" : "leetcode-open-submit", content: `${action === "run" ? "Run" : "Submit"} manually on LeetCode.\n\nURL: ${url}\nLocal solution: ${problem.solutionPath}\nLanguage: ${languageLabel(problem.langSlug)}\n\nPi will not use the LeetCode API or browser automation for this step.`, display: true, details: { ...problem, url, action }, }); } function solveProblem(problem: ProblemState): void { pi.sendUserMessage( `Unopinionated LeetCode solve for ${problem.titleSlug}. Read ${problem.problemPath} and ${problem.solutionPath}. Replace ${problem.solutionPath} with optimal production-quality ${problem.langSlug} code, targeting O(N) when feasible and stating no tutor explanation unless required by code comments. Do not run tests, do not submit, and do not use browser automation. After writing the file, tell the user to choose Run from /lc-next to open LeetCode manually.`, { deliverAs: "followUp" }, ); } async function markProblem(ctx: ExtensionContext, problem: ProblemState, patch: Partial): Promise { const state = await loadState(ctx.cwd); const current = state.problems[problem.titleSlug] ?? problem; state.problems[problem.titleSlug] = { ...current, ...patch }; await saveState(ctx.cwd, state); } async function showLearningReview(ctx: ExtensionContext): Promise { const state = await loadState(ctx.cwd); const problems = Object.values(state.problems); if (problems.length === 0) { notify(ctx, "No learning history yet. Start a problem first.", "warning"); return; } const topicCounts = new Map(); const learned: string[] = []; const mistakes: string[] = []; const missingNotes: string[] = []; for (const problem of problems) { const notesPath = problem.notesPath ?? path.join("leetcode", problem.titleSlug, "notes.md"); if (!(await fileExists(path.join(ctx.cwd, notesPath)))) { missingNotes.push(problem.titleSlug); if (problem.learningSummary) learned.push(`- ${problem.title}: ${problem.learningSummary}`); continue; } const notes = await readFile(path.join(ctx.cwd, notesPath), "utf8"); for (const line of extractMarkdownSection(notes, "Highlights")) learned.push(`- ${problem.title}: ${line.replace(/^[-*]\s*/, "")}`); for (const line of extractMarkdownSection(notes, "Approach").slice(0, 2)) learned.push(`- ${problem.title}: ${line.replace(/^[-*]\s*/, "")}`); for (const line of extractMarkdownSection(notes, "Mistakes / Debugging")) mistakes.push(`- ${problem.title}: ${line.replace(/^[-*]\s*/, "")}`); const problemMarkdownPath = path.join(ctx.cwd, problem.problemPath); if (await fileExists(problemMarkdownPath)) { const problemText = await readFile(problemMarkdownPath, "utf8"); for (const topicLine of extractMarkdownSection(problemText, "Topics")) { const topic = topicLine.replace(/^[-*]\s*/, ""); topicCounts.set(topic, (topicCounts.get(topic) ?? 0) + 1); } } } const topics = [...topicCounts.entries()].sort((a, b) => b[1] - a[1]).map(([topic, count]) => `- ${topic}: ${count}`).join("\n") || "- No topic data found yet."; const content = [ "## Learning Review", "", `Problems tracked: ${problems.length}`, "", "### Skills practiced", topics, "", "### Key lessons", learned.slice(0, 12).join("\n") || "- No highlights yet. Use Understand, Debug, or Review from /lc-next to build notes.", "", "### Mistakes to remember", mistakes.slice(0, 8).join("\n") || "- No mistakes recorded yet.", "", "### Notes health", missingNotes.length ? `Missing notes for: ${missingNotes.join(", ")}` : "All tracked notes files are present.", ].join("\n"); pi.sendMessage({ customType: "leetcode-learning-review", content, display: true, details: { topicCounts: Object.fromEntries(topicCounts), missingNotes } }); } function confidenceFor(problem: ProblemState): string { if (problem.status === "solved" || problem.status === "submit-ready") return "high"; if (problem.status === "ran" || problem.status === "edited") return "medium"; return "low"; } function practiceDashboard(problem: ProblemState): string { return [ `## Practice Dashboard`, ``, `**${problem.questionId}. ${problem.title}** [${problem.difficulty}] ${languageLabel(problem.langSlug)}`, ``, `- Status: ${problem.status ?? "started"}`, `- Confidence: ${confidenceFor(problem)}`, `- Last action: ${problem.lastAction ?? "none"}`, `- Last run: ${problem.lastRunSummary ?? "none"}`, `- Problem: ${problem.problemPath}`, `- Solution: ${problem.solutionPath}`, `- Notes: ${problem.notesPath ?? path.join("leetcode", problem.titleSlug, "notes.md")}`, ].join("\n"); } async function chooseProblemAction(ctx: ExtensionContext, problem: ProblemState): Promise { pi.sendMessage({ customType: "leetcode-dashboard", content: practiceDashboard(problem), display: true, details: problem }); const action = await ctx.ui.select("What next?", [ "1. Understand — explain the approach", "2. Nudge — give me a hint", "3. Co-code — write a small bit", "4. Edit — open solution file", "5. Debug — find where I’m wrong", "6. Run — open LeetCode page manually", "7. Submit — open LeetCode page manually", "8. Review — optimize / compare side-by-side", "9. Solve — full solution", "10. Learning history — review past notes", ]); if (!action) return; await trackUsage(ctx.cwd, { command: "lc-next", action: action.split(" — ")[0], language: problem.langSlug, difficulty: problem.difficulty }); if (action.startsWith("1.")) { await markProblem(ctx, problem, { lastAction: "explained" }); explainProblem(problem); } else if (action.startsWith("2.")) { await markProblem(ctx, problem, { lastAction: "hinted" }); hintProblem(problem); } else if (action.startsWith("3.")) { await markProblem(ctx, problem, { lastAction: "write-a-bit" }); writeABit(problem); } else if (action.startsWith("5.")) { await markProblem(ctx, problem, { lastAction: "debugged" }); debugProblem(problem); } else if (action.startsWith("6.")) { await markProblem(ctx, problem, { status: "ran", lastAction: "run page opened", lastRunSummary: "manual LeetCode run" }); await openLeetCodePage(ctx, problem, "run"); } else if (action.startsWith("7.")) { await markProblem(ctx, problem, { status: "submit-ready", lastAction: "submit page opened" }); await openLeetCodePage(ctx, problem, "submit"); } else if (action.startsWith("8.")) { await markProblem(ctx, problem, { lastAction: "reviewed" }); optimizeProblem(problem); } else if (action.startsWith("9.")) { await markProblem(ctx, problem, { status: "solved", lastAction: "auto-solve requested" }); solveProblem(problem); } else if (action.startsWith("10.")) { await showLearningReview(ctx); } else if (action.startsWith("4.")) { await openEditor(ctx, problem.solutionPath); await markProblem(ctx, problem, { status: "edited", lastAction: "edited" }); notify(ctx, `Editor closed: ${problem.solutionPath}`); const chooseNext = await ctx.ui.confirm("Choose next action?", "Open /lc-next again to choose run, debug, or submit?"); if (chooseNext) { const { problem: updated } = await activeProblem(ctx); await chooseProblemAction(ctx, updated); } } } async function presentFetchedProblem(ctx: ExtensionContext, problem: ProblemState): Promise { const markdown = await readFile(path.join(ctx.cwd, problem.problemPath), "utf8"); pi.sendMessage({ customType: "leetcode-problem", content: `${markdown}\n\n---\n\nFiles created/updated:\n- Problem: ${problem.problemPath}\n- Solution: ${problem.solutionPath}`, display: true, details: problem, }); await chooseProblemAction(ctx, problem); } async function startProblem(args: string, ctx: ExtensionContext, commandName: string): Promise { const state = await loadState(ctx.cwd); if (!state.languageSelected && ctx.hasUI) await chooseLanguage(ctx, "Choose language before starting"); const titleSlug = args.trim(); if (!titleSlug) { const current = state.currentSlug ? state.problems[state.currentSlug] : undefined; const recent = (state.recentSlugs ?? []).filter((slug) => slug !== state.currentSlug && state.problems[slug]).slice(0, 3); const choices = [ ...(current ? [`Continue current — ${current.titleSlug}`] : []), ...recent.map((slug) => `Recent — ${slug}`), "Review learning history", "Search new problem", "Random top 50", ]; const choice = await ctx.ui.select("Start LeetCode practice", choices); if (!choice) return; if (choice.startsWith("Continue") && current) { await chooseProblemAction(ctx, current); return; } if (choice.startsWith("Recent")) { const slug = choice.split(" — ")[1]; await chooseProblemAction(ctx, state.problems[slug]); return; } if (choice.startsWith("Review")) { await showLearningReview(ctx); return; } if (choice.startsWith("Random")) { const questions = await listProblems("", 0, DEFAULT_LIMIT); const random = questions[Math.floor(Math.random() * questions.length)]; if (random) pi.sendUserMessage(`/lc-start ${random.titleSlug}`, { deliverAs: "followUp" }); return; } notify(ctx, `/${commandName} with no slug opens problem search.`); await chooseProblemFromSearch("", ctx); return; } const problem = await fetchIntoWorkspace(ctx, titleSlug); await trackUsage(ctx.cwd, { command: "lc-start", action: "problem-started", language: problem.langSlug, difficulty: problem.difficulty }); notify(ctx, `Started ${problem.title}. Active solution: ${problem.solutionPath}`); await presentFetchedProblem(ctx, problem); } pi.registerCommand("lc-start", { description: "Start a LeetCode problem, or search/select when no slug is provided", handler: async (args, ctx) => { await trackUsage(ctx.cwd, { command: "lc-start", action: args.trim() ? "start-with-slug" : "start-menu" }); await startProblem(args, ctx, "lc-start"); }, }); pi.registerCommand("lc-next", { description: "Show the What next action menu for the active LeetCode problem", handler: async (_args, ctx) => { await trackUsage(ctx.cwd, { command: "lc-next", action: "menu-opened" }); const { problem } = await activeProblem(ctx); await chooseProblemAction(ctx, problem); }, }); }