641 lines
26 KiB
TypeScript
641 lines
26 KiB
TypeScript
/**
|
|
* EasyMode — All-in-one beginner-friendly Pi extension
|
|
*
|
|
* Built-in features:
|
|
* 🎯 Goal Keeper — set a focus so the AI stays on track
|
|
* 🛡️ Safety Guard — catches dangerous commands before they run
|
|
* 📊 Smart Footer — model, context bar, cost, git branch, tool tally
|
|
* 🤖 Agent Presets — switch AI personality with /agent
|
|
* 🟢 /menu — visual command picker (no memorization needed)
|
|
* 🛑 /stop — abort the current AI action
|
|
* 💡 /explain — re-explain the last action in plain English
|
|
* 🔖 /bookmark — save + list important moments
|
|
* ↩️ /undo — how to revert the last file change
|
|
* 📋 /status — full session dashboard
|
|
* 📖 /help — quick reference
|
|
* 🔄 /quickstart — re-run the welcome wizard
|
|
*
|
|
* Usage: pi -e extensions/easymode.ts
|
|
*
|
|
* Or with theme cycling:
|
|
* pi -e extensions/easymode.ts -e extensions/theme-cycler.ts
|
|
*/
|
|
|
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
import { basename } from "node:path";
|
|
import { applyExtensionDefaults } from "./themeMap.ts";
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Agent Presets
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
interface AgentPreset {
|
|
name: string;
|
|
emoji: string;
|
|
description: string;
|
|
systemPrompt: string;
|
|
}
|
|
|
|
const AGENT_PRESETS: AgentPreset[] = [
|
|
{
|
|
name: "Default",
|
|
emoji: "🤖",
|
|
description: "Standard coding assistant — no restrictions",
|
|
systemPrompt: "",
|
|
},
|
|
{
|
|
name: "Explain Like I'm 5",
|
|
emoji: "👶",
|
|
description: "Explains everything in the simplest terms possible",
|
|
systemPrompt: `You are a patient teacher. The user is a beginner.
|
|
- Explain EVERY concept in simple terms before using it
|
|
- Use analogies and real-world comparisons
|
|
- Show the "before" and "after" of every change
|
|
- Never assume the user knows technical jargon — define it first
|
|
- After each code change, add a brief "What just happened?" summary
|
|
- If a command could break something, warn the user FIRST`,
|
|
},
|
|
{
|
|
name: "Guided Builder",
|
|
emoji: "🏗️",
|
|
description: "Step-by-step building with confirmations at each stage",
|
|
systemPrompt: `You are a guided builder. Help the user build things step-by-step.
|
|
- Break every task into numbered steps and show the plan first
|
|
- Ask for confirmation before each major step
|
|
- After each change, briefly explain what you did and why
|
|
- If there are multiple approaches, list 2-3 options with pros/cons
|
|
- Always suggest the safest option as the default
|
|
- Show file paths and explain the project structure as you go`,
|
|
},
|
|
{
|
|
name: "Code Reviewer",
|
|
emoji: "🔍",
|
|
description: "Reviews code and suggests improvements without changing anything",
|
|
systemPrompt: `You are a code reviewer. Review, don't modify.
|
|
- DO NOT write or edit any files unless explicitly asked
|
|
- Point out bugs, security issues, and improvement opportunities
|
|
- Rate code quality on a 1-5 scale with specific reasons
|
|
- Suggest improvements as clear before/after examples
|
|
- Focus on: readability, security, performance, maintainability
|
|
- Be encouraging — highlight what's done well too`,
|
|
},
|
|
{
|
|
name: "Debug Detective",
|
|
emoji: "🕵️",
|
|
description: "Focused on finding and fixing bugs",
|
|
systemPrompt: `You are a debug detective. Help find and fix bugs systematically.
|
|
- Always ask to see error messages and logs first
|
|
- Explain your debugging thought process step by step
|
|
- List possible causes ranked by likelihood
|
|
- Test hypotheses one at a time
|
|
- After fixing, explain what caused the bug and how to prevent it
|
|
- Suggest adding error handling or tests to prevent recurrence`,
|
|
},
|
|
{
|
|
name: "Safe Mode",
|
|
emoji: "🔒",
|
|
description: "Extra cautious — confirms every file modification",
|
|
systemPrompt: `You are in Safe Mode. Maximum caution.
|
|
- ALWAYS show the exact changes you plan to make BEFORE making them
|
|
- Ask for explicit "yes" confirmation before ANY file write, edit, or bash command
|
|
- Never run destructive commands (rm, drop, truncate, etc.) without double-confirming
|
|
- Create backups before modifying existing files when possible
|
|
- After each change, verify the change was correct by reading the file back
|
|
- If something goes wrong, immediately suggest how to undo it`,
|
|
},
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Dangerous Command Patterns
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
const DANGEROUS_PATTERNS: { pattern: RegExp; reason: string }[] = [
|
|
{ pattern: /rm\s+(-[rRf]+\s+|.*\s+-[rRf]+)/, reason: "Recursive/forced delete detected" },
|
|
{ pattern: /rm\s+-rf\s+[\/~]/, reason: "Deleting from root or home directory!" },
|
|
{ pattern: /DROP\s+(TABLE|DATABASE|SCHEMA)/i, reason: "SQL DROP command detected" },
|
|
{ pattern: /TRUNCATE\s+TABLE/i, reason: "SQL TRUNCATE command detected" },
|
|
{ pattern: /DELETE\s+FROM\s+\w+\s*(;|$)/i, reason: "DELETE without WHERE clause" },
|
|
{ pattern: />\s*\/dev\/sd[a-z]/, reason: "Writing directly to disk device" },
|
|
{ pattern: /mkfs\./, reason: "Filesystem format command detected" },
|
|
{ pattern: /dd\s+if=/, reason: "dd command — raw disk copy" },
|
|
{ pattern: /chmod\s+-R\s+777/, reason: "Recursive world-writable permissions" },
|
|
{ pattern: /:(){ :\|:& };:/, reason: "Fork bomb detected!" },
|
|
{ pattern: /curl.*\|\s*(ba)?sh/, reason: "Piping remote script to shell" },
|
|
{ pattern: /wget.*\|\s*(ba)?sh/, reason: "Piping remote script to shell" },
|
|
{ pattern: /git\s+push\s+.*--force/, reason: "Force push — may overwrite remote history" },
|
|
{ pattern: /git\s+reset\s+--hard/, reason: "Hard reset — may lose uncommitted changes" },
|
|
{ pattern: /npm\s+publish/, reason: "Publishing to npm registry" },
|
|
{ pattern: /docker\s+system\s+prune/, reason: "Docker system prune — removes all unused data" },
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Extension
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
let purpose: string | undefined;
|
|
let activePreset: AgentPreset = AGENT_PRESETS[1]; // Default to "Explain Like I'm 5" for beginners
|
|
let activeCtx: ExtensionContext | undefined;
|
|
const toolCounts: Record<string, number> = {};
|
|
const bookmarks: { timestamp: string; note: string }[] = [];
|
|
let lastFileChange: { tool: string; path: string } | null = null;
|
|
let toolRunsSinceLastTip = 0;
|
|
|
|
// ── Tool tracking + contextual tips ───────────────────────────────
|
|
pi.on("tool_execution_end", async (event) => {
|
|
toolCounts[event.toolName] = (toolCounts[event.toolName] || 0) + 1;
|
|
|
|
// Show a helpful hint every 5 tool runs
|
|
toolRunsSinceLastTip++;
|
|
if (activeCtx && toolRunsSinceLastTip >= 5) {
|
|
toolRunsSinceLastTip = 0;
|
|
const tips = [
|
|
"💡 Confused? Type /explain and the AI will break down what just happened.",
|
|
"💡 Want to save your progress? Type /bookmark to mark this moment.",
|
|
"💡 Type /menu to see everything you can do.",
|
|
"💡 Things going wrong? Type /stop to pause the AI immediately.",
|
|
"💡 Type /undo to see how to reverse the last file change.",
|
|
"💡 Type /status to see a summary of your whole session.",
|
|
];
|
|
const tip = tips[Math.floor(Math.random() * tips.length)];
|
|
activeCtx.ui.notify(tip, "info");
|
|
}
|
|
});
|
|
|
|
// ── Track file changes for /undo ──────────────────────────────────
|
|
pi.on("tool_call", async (event, _ctx) => {
|
|
if (isToolCallEventType("write", event) || isToolCallEventType("edit", event)) {
|
|
lastFileChange = { tool: event.toolName, path: event.input.path };
|
|
}
|
|
return { block: false };
|
|
});
|
|
|
|
// ── Welcome Wizard ────────────────────────────────────────────────
|
|
async function runWelcomeWizard(ctx: ExtensionContext) {
|
|
// Step 1: Welcome
|
|
ctx.ui.notify(
|
|
"👋 Welcome to EasyMode!\n\n" +
|
|
"This is your AI coding assistant. You type what you want\n" +
|
|
"in plain English, and it writes the code for you.\n\n" +
|
|
"Let's get you set up in 30 seconds...",
|
|
"info"
|
|
);
|
|
|
|
// Step 2: Pick a personality
|
|
const personalityOptions = AGENT_PRESETS.map((p) => `${p.emoji} ${p.name} — ${p.description}`);
|
|
const personalityChoice = await ctx.ui.select(
|
|
"Step 1 of 2: How should the AI talk to you?",
|
|
personalityOptions
|
|
);
|
|
|
|
if (personalityChoice !== undefined) {
|
|
const idx = personalityOptions.indexOf(personalityChoice);
|
|
activePreset = AGENT_PRESETS[idx];
|
|
ctx.ui.setStatus("easymode", `${activePreset.emoji} ${activePreset.name}`);
|
|
ctx.ui.notify(`Great choice! ${activePreset.emoji} ${activePreset.name} activated.`, "info");
|
|
} else {
|
|
ctx.ui.notify(`Using default: ${activePreset.emoji} ${activePreset.name}`, "info");
|
|
}
|
|
|
|
// Step 3: Set a goal
|
|
const answer = await ctx.ui.input(
|
|
"Step 2 of 2: What do you want to do today?",
|
|
"Examples: 'Make a website', 'Fix the bug on line 42', 'Explain this code to me'"
|
|
);
|
|
|
|
if (answer && answer.trim()) {
|
|
purpose = answer.trim();
|
|
ctx.ui.notify(`🎯 Goal set: ${purpose}`, "info");
|
|
setPurposeWidget(ctx);
|
|
} else {
|
|
ctx.ui.notify("No problem! You can set a goal anytime by typing /goal", "info");
|
|
}
|
|
|
|
// Step 4: Quick orientation
|
|
ctx.ui.notify(
|
|
"✅ You're all set! Here's what you need to know:\n\n" +
|
|
" 📝 Just type what you want in plain English\n" +
|
|
" 🟢 /menu — see all available commands\n" +
|
|
" 🛑 /stop — stop the AI if it's doing something wrong\n" +
|
|
" ❓ /explain — ask the AI to explain what it just did\n\n" +
|
|
"⚠️ The AI will ask for your OK before doing anything dangerous.\n\n" +
|
|
"Go ahead — type your first message! 🚀",
|
|
"info"
|
|
);
|
|
}
|
|
|
|
// ── Session start ─────────────────────────────────────────────────
|
|
pi.on("session_start", async (_event, ctx) => {
|
|
applyExtensionDefaults(import.meta.url, ctx);
|
|
activeCtx = ctx;
|
|
|
|
// Fire-and-forget — don't block TUI startup
|
|
void runWelcomeWizard(ctx);
|
|
|
|
// Footer
|
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
|
|
|
return {
|
|
dispose: unsub,
|
|
invalidate() {},
|
|
render(width: number): string[] {
|
|
// Accumulate tokens + cost
|
|
let tokIn = 0, tokOut = 0, cost = 0;
|
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
const m = entry.message as AssistantMessage;
|
|
tokIn += m.usage.input;
|
|
tokOut += m.usage.output;
|
|
cost += m.usage.cost.total;
|
|
}
|
|
}
|
|
|
|
const fmt = (n: number) => n < 1000 ? `${n}` : `${(n / 1000).toFixed(1)}k`;
|
|
const model = ctx.model?.id || "no-model";
|
|
const dir = basename(ctx.cwd);
|
|
const branch = footerData.getGitBranch();
|
|
|
|
// Context bar
|
|
const usage = ctx.getContextUsage();
|
|
const pct = usage?.percent ?? 0;
|
|
const filled = Math.round(pct / 10);
|
|
const bar = "█".repeat(filled) + "░".repeat(10 - filled);
|
|
|
|
// Line 1: model + context + agent + cost
|
|
const l1Left =
|
|
theme.fg("dim", ` ${model} `) +
|
|
(pct < 70 ? theme.fg("success", bar) : pct < 90 ? theme.fg("warning", bar) : theme.fg("error", bar)) +
|
|
theme.fg("dim", ` ${Math.round(pct)}% `) +
|
|
theme.fg("accent", `${activePreset.emoji} ${activePreset.name}`);
|
|
|
|
const l1Right =
|
|
theme.fg("success", fmt(tokIn)) +
|
|
theme.fg("dim", "↓ ") +
|
|
theme.fg("accent", fmt(tokOut)) +
|
|
theme.fg("dim", "↑ ") +
|
|
theme.fg("warning", `$${cost.toFixed(4)}`) +
|
|
theme.fg("dim", " ");
|
|
|
|
const pad1 = " ".repeat(Math.max(1, width - visibleWidth(l1Left) - visibleWidth(l1Right)));
|
|
const line1 = truncateToWidth(l1Left + pad1 + l1Right, width, "");
|
|
|
|
// Line 2: cwd + branch + tools
|
|
const l2Left =
|
|
theme.fg("dim", ` ${dir}`) +
|
|
(branch
|
|
? theme.fg("dim", " ") + theme.fg("warning", "(") + theme.fg("success", branch) + theme.fg("warning", ")")
|
|
: "");
|
|
|
|
const entries = Object.entries(toolCounts);
|
|
const l2Right = entries.length === 0
|
|
? theme.fg("dim", "ready ")
|
|
: entries
|
|
.map(([name, count]) => theme.fg("accent", name) + theme.fg("dim", ":") + theme.fg("success", `${count}`))
|
|
.join(theme.fg("dim", " ")) + " ";
|
|
|
|
const pad2 = " ".repeat(Math.max(1, width - visibleWidth(l2Left) - visibleWidth(l2Right)));
|
|
const line2 = truncateToWidth(l2Left + pad2 + l2Right, width, "");
|
|
|
|
return [line1, line2];
|
|
},
|
|
};
|
|
});
|
|
|
|
ctx.ui.setStatus("easymode", `${activePreset.emoji} ${activePreset.name}`);
|
|
});
|
|
|
|
pi.on("session_switch", async (_event, ctx) => { activeCtx = ctx; });
|
|
|
|
// ── Safety guard ──────────────────────────────────────────────────
|
|
pi.on("tool_call", async (event, ctx) => {
|
|
if (isToolCallEventType("bash", event)) {
|
|
const cmd = event.input.command;
|
|
for (const { pattern, reason } of DANGEROUS_PATTERNS) {
|
|
if (pattern.test(cmd)) {
|
|
const confirmed = await ctx.ui.confirm(
|
|
"⚠️ Safety Warning",
|
|
`${reason}\n\nCommand: ${cmd}\n\nAre you sure you want to run this?`,
|
|
{ timeout: 30000 }
|
|
);
|
|
if (!confirmed) {
|
|
ctx.ui.notify(`🛡️ Blocked: ${reason}`, "warning");
|
|
ctx.abort();
|
|
return {
|
|
block: true,
|
|
reason: `🛑 BLOCKED by Safety Guard: ${reason}\n\nThe user chose not to run this command. Ask them what they'd like to do instead.`,
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return { block: false };
|
|
});
|
|
|
|
// ── System prompt injection ───────────────────────────────────────
|
|
pi.on("before_agent_start", async (event) => {
|
|
let extra = "";
|
|
if (activePreset.systemPrompt) {
|
|
extra += `\n\n<agent-preset name="${activePreset.name}">\n${activePreset.systemPrompt}\n</agent-preset>`;
|
|
}
|
|
if (purpose) {
|
|
extra += `\n\n<user-goal>\nThe user's goal for this session: ${purpose}\nKeep this goal in mind. If the conversation drifts, gently guide back.\n</user-goal>`;
|
|
}
|
|
if (!extra) return;
|
|
return { systemPrompt: event.systemPrompt + extra };
|
|
});
|
|
|
|
// ── Helper ────────────────────────────────────────────────────────
|
|
function setPurposeWidget(ctx: ExtensionContext) {
|
|
ctx.ui.setWidget("purpose", () => ({
|
|
render(width: number): string[] {
|
|
const label = " ✅ Working on: ";
|
|
const content = purpose!;
|
|
const line = truncateToWidth(label + content, width - 2, "…");
|
|
return [
|
|
" ".repeat(width),
|
|
line + " ".repeat(Math.max(0, width - visibleWidth(line))),
|
|
" ".repeat(width),
|
|
];
|
|
},
|
|
invalidate() {},
|
|
}));
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// Commands
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
pi.registerCommand("help", {
|
|
description: "Show all EasyMode commands",
|
|
handler: async (_args, ctx) => {
|
|
ctx.ui.notify(
|
|
"📖 EasyMode Commands:\n\n" +
|
|
" /menu — 🟢 START HERE — pick a command from a list\n" +
|
|
" /help — You're looking at it!\n" +
|
|
" /agent — Switch AI personality (beginner, builder, reviewer...)\n" +
|
|
" /goal — Set or change your session goal\n" +
|
|
" /stop — Immediately stop the current AI action\n" +
|
|
" /explain — Ask the AI to explain what it just did\n" +
|
|
" /bookmark — Save the current moment with a note\n" +
|
|
" /bookmarks — See all saved bookmarks\n" +
|
|
" /undo — Show how to undo the last file change\n" +
|
|
" /status — Show session overview (goal, agent, stats)\n" +
|
|
" /quickstart — Re-run the setup wizard\n" +
|
|
"\n💡 Tips:\n" +
|
|
" • Just type what you want in plain English — no special syntax needed\n" +
|
|
" • The bottom bar shows your context usage and cost\n" +
|
|
" • ⚠️ Dangerous commands always ask for your OK first",
|
|
"info"
|
|
);
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("menu", {
|
|
description: "Pick a command from a visual menu",
|
|
handler: async (_args, ctx) => {
|
|
const menuItems = [
|
|
"🎯 Set or change my goal",
|
|
"🤖 Switch AI personality",
|
|
"💡 Explain what just happened",
|
|
"🔖 Save a bookmark",
|
|
"📋 See all my bookmarks",
|
|
"↩️ Undo the last file change",
|
|
"📊 Show session status",
|
|
"🛑 Stop the AI",
|
|
"🔄 Re-run setup wizard",
|
|
"📖 Show all commands (help)",
|
|
];
|
|
const choice = await ctx.ui.select("What would you like to do?", menuItems);
|
|
if (choice === undefined) return;
|
|
|
|
const idx = menuItems.indexOf(choice);
|
|
switch (idx) {
|
|
case 0: { // Goal
|
|
const answer = await ctx.ui.input("🎯 What's your goal?", purpose || "e.g. Fix the login bug...");
|
|
if (answer && answer.trim()) {
|
|
purpose = answer.trim();
|
|
ctx.ui.notify(`🎯 Goal updated: ${purpose}`, "info");
|
|
setPurposeWidget(ctx);
|
|
}
|
|
return;
|
|
}
|
|
case 1: { // Agent
|
|
const options = AGENT_PRESETS.map((p) => `${p.emoji} ${p.name} — ${p.description}`);
|
|
const agentChoice = await ctx.ui.select("Select AI Personality", options);
|
|
if (agentChoice !== undefined) {
|
|
const agentIdx = options.indexOf(agentChoice);
|
|
activePreset = AGENT_PRESETS[agentIdx];
|
|
ctx.ui.setStatus("easymode", `${activePreset.emoji} ${activePreset.name}`);
|
|
ctx.ui.notify(`Switched to: ${activePreset.emoji} ${activePreset.name}\n${activePreset.description}`, "info");
|
|
}
|
|
return;
|
|
}
|
|
case 2: // Explain
|
|
pi.sendUserMessage("Please explain what you just did in simple terms that a beginner would understand. Use bullet points and avoid jargon. If you wrote code, explain what each part does.");
|
|
return;
|
|
case 3: { // Bookmark
|
|
const note = await ctx.ui.input("🔖 Bookmark note", "e.g. Got login working, Before refactor...");
|
|
if (note && note.trim()) {
|
|
const ts = new Date().toLocaleTimeString();
|
|
bookmarks.push({ timestamp: ts, note: note.trim() });
|
|
pi.appendEntry("easymode-bookmarks", { timestamp: ts, note: note.trim() });
|
|
ctx.ui.notify(`🔖 Saved: ${note.trim()} (${ts})`, "info");
|
|
}
|
|
return;
|
|
}
|
|
case 4: { // Bookmarks
|
|
if (bookmarks.length === 0) return ctx.ui.notify("No bookmarks yet. Use /bookmark to save one.", "info");
|
|
const list = bookmarks.map((b, i) => ` ${i + 1}. [${b.timestamp}] ${b.note}`).join("\n");
|
|
return ctx.ui.notify(`🔖 Bookmarks:\n\n${list}`, "info");
|
|
}
|
|
case 5: { // Undo
|
|
if (!lastFileChange) {
|
|
return ctx.ui.notify("No file changes tracked yet.\n\n💡 You can also tell the AI: \"Please undo the last change\"", "info");
|
|
}
|
|
return ctx.ui.notify(
|
|
`↩️ Last change: ${lastFileChange.tool} → ${lastFileChange.path}\n\n` +
|
|
'Tell the AI: "Please revert the last change"\n' +
|
|
`Or run: git checkout -- ${lastFileChange.path}`,
|
|
"info"
|
|
);
|
|
}
|
|
case 6: { // Status
|
|
const usage = ctx.getContextUsage();
|
|
const pct = usage?.percent ?? 0;
|
|
let tokIn = 0, tokOut = 0, cost = 0;
|
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
const m = entry.message as AssistantMessage;
|
|
tokIn += m.usage.input;
|
|
tokOut += m.usage.output;
|
|
cost += m.usage.cost.total;
|
|
}
|
|
}
|
|
return ctx.ui.notify(
|
|
`📋 Session Status\n\n` +
|
|
` 🎯 Goal: ${purpose || "(not set)"}\n` +
|
|
` 🤖 Agent: ${activePreset.emoji} ${activePreset.name}\n` +
|
|
` 📊 Context: ${Math.round(pct)}% used\n` +
|
|
` 💰 Cost: $${cost.toFixed(4)}`,
|
|
"info"
|
|
);
|
|
}
|
|
case 7: // Stop
|
|
ctx.abort();
|
|
return ctx.ui.notify("🛑 Stopped.", "warning");
|
|
case 8: // Quickstart
|
|
await runWelcomeWizard(ctx);
|
|
return;
|
|
case 9: // Help
|
|
return ctx.ui.notify(
|
|
"📖 All commands start with / (slash):\n\n" +
|
|
" /menu /goal /agent /explain /bookmark\n" +
|
|
" /bookmarks /undo /status /stop /quickstart /help\n\n" +
|
|
"Or just type what you want in plain English!",
|
|
"info"
|
|
);
|
|
}
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("quickstart", {
|
|
description: "Re-run the welcome wizard",
|
|
handler: async (_args, ctx) => {
|
|
await runWelcomeWizard(ctx);
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("agent", {
|
|
description: "Switch AI personality preset",
|
|
handler: async (_args, ctx) => {
|
|
const options = AGENT_PRESETS.map((p) => `${p.emoji} ${p.name} — ${p.description}`);
|
|
const choice = await ctx.ui.select("Select AI Personality", options);
|
|
if (choice === undefined) return;
|
|
const idx = options.indexOf(choice);
|
|
activePreset = AGENT_PRESETS[idx];
|
|
ctx.ui.setStatus("easymode", `${activePreset.emoji} ${activePreset.name}`);
|
|
ctx.ui.notify(`Switched to: ${activePreset.emoji} ${activePreset.name}\n${activePreset.description}`, "info");
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("goal", {
|
|
description: "Set or change your session goal",
|
|
handler: async (_args, ctx) => {
|
|
const answer = await ctx.ui.input("🎯 What's your goal?", purpose || "e.g. Fix the login bug...");
|
|
if (answer && answer.trim()) {
|
|
purpose = answer.trim();
|
|
ctx.ui.notify(`🎯 Goal updated: ${purpose}`, "info");
|
|
setPurposeWidget(ctx);
|
|
}
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("stop", {
|
|
description: "Immediately stop the current AI action",
|
|
handler: async (args, ctx) => {
|
|
const reason = (args || "").trim();
|
|
ctx.abort();
|
|
ctx.ui.notify(reason ? `🛑 Stopped: ${reason}` : "🛑 Stopped.", "warning");
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("explain", {
|
|
description: "Ask the AI to explain its last action in simple terms",
|
|
handler: async (_args, _ctx) => {
|
|
pi.sendUserMessage(
|
|
"Please explain what you just did in simple terms that a beginner would understand. " +
|
|
"Use bullet points and avoid jargon. If you wrote code, explain what each part does."
|
|
);
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("bookmark", {
|
|
description: "Save a bookmark with a note",
|
|
handler: async (_args, ctx) => {
|
|
const note = await ctx.ui.input("🔖 Bookmark note", "e.g. Got login working, Before refactor...");
|
|
if (note && note.trim()) {
|
|
const ts = new Date().toLocaleTimeString();
|
|
bookmarks.push({ timestamp: ts, note: note.trim() });
|
|
pi.appendEntry("easymode-bookmarks", { timestamp: ts, note: note.trim() });
|
|
ctx.ui.notify(`🔖 Saved: ${note.trim()} (${ts})`, "info");
|
|
}
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("bookmarks", {
|
|
description: "Show all saved bookmarks",
|
|
handler: async (_args, ctx) => {
|
|
if (bookmarks.length === 0) {
|
|
ctx.ui.notify("No bookmarks yet. Use /bookmark to save one.", "info");
|
|
return;
|
|
}
|
|
const list = bookmarks.map((b, i) => ` ${i + 1}. [${b.timestamp}] ${b.note}`).join("\n");
|
|
ctx.ui.notify(`🔖 Bookmarks:\n\n${list}`, "info");
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("undo", {
|
|
description: "Show how to undo the last file change",
|
|
handler: async (_args, ctx) => {
|
|
if (!lastFileChange) {
|
|
ctx.ui.notify(
|
|
"No file changes tracked yet.\n\n" +
|
|
"💡 General undo:\n" +
|
|
" git checkout -- <file> revert to last commit\n" +
|
|
" git diff see what changed\n" +
|
|
" git stash save & revert everything",
|
|
"info"
|
|
);
|
|
return;
|
|
}
|
|
ctx.ui.notify(
|
|
`↩️ Last change: ${lastFileChange.tool} → ${lastFileChange.path}\n\n` +
|
|
"To undo:\n" +
|
|
` git checkout -- ${lastFileChange.path}\n\n` +
|
|
"See diff:\n" +
|
|
` git diff ${lastFileChange.path}\n\n` +
|
|
'Or tell the AI: "Please revert the last change"',
|
|
"info"
|
|
);
|
|
},
|
|
});
|
|
|
|
pi.registerCommand("status", {
|
|
description: "Show session overview",
|
|
handler: async (_args, ctx) => {
|
|
const usage = ctx.getContextUsage();
|
|
const pct = usage?.percent ?? 0;
|
|
let tokIn = 0, tokOut = 0, cost = 0;
|
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
const m = entry.message as AssistantMessage;
|
|
tokIn += m.usage.input;
|
|
tokOut += m.usage.output;
|
|
cost += m.usage.cost.total;
|
|
}
|
|
}
|
|
const entries = Object.entries(toolCounts);
|
|
const toolSummary = entries.length === 0
|
|
? " None yet"
|
|
: entries.map(([n, c]) => ` ${n}: ${c}`).join("\n");
|
|
|
|
ctx.ui.notify(
|
|
`📋 Session Status\n\n` +
|
|
` 🎯 Goal: ${purpose || "(not set — use /goal)"}\n` +
|
|
` 🤖 Agent: ${activePreset.emoji} ${activePreset.name}\n` +
|
|
` 📊 Context: ${Math.round(pct)}% used\n` +
|
|
` 💰 Cost: $${cost.toFixed(4)}\n` +
|
|
` 📨 Tokens: ${tokIn} in / ${tokOut} out\n` +
|
|
` 🔖 Bookmarks: ${bookmarks.length}\n` +
|
|
`\n🔧 Tools Used:\n${toolSummary}`,
|
|
"info"
|
|
);
|
|
},
|
|
});
|
|
}
|