Agent-JAE/default-extensions/pair-programming.ts
jae a1b6e22c01 feat: add 16 default extensions for Agent JAE
New extensions shipping with Agent JAE:
- jae-rules: Per-project .jae/rules.md injection
- cost-tracker: Token/cost tracking with budget limits
- project-dna: Auto-analyze project fingerprint
- teach-mode: Pedagogical explanation mode
- bookmarks: File bookmark management
- replay: Session recording and asciicast export
- checkpoints: Git-based undo/time-travel
- pr-review: Autonomous PR diff review
- auto-docs: Auto-generate README and docs
- screenshot-context: Vision analysis for screenshots
- deploy: One-command deploy (Docker/Vercel/rsync/static)
- pair-programming: Real-time file watching with feedback
- dashboard: Terminal HUD with live stats
- swarm: Multi-agent parallel task execution
- skill-marketplace: Search/install/publish skills
- widget-api-demo: TUI widget API demonstration
2026-03-23 20:14:41 +01:00

112 lines
3.4 KiB
TypeScript

import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
import { watch, type FSWatcher, readFileSync } from "node:fs";
import { join, relative, extname } from "node:path";
export default function (pi: ExtensionAPI) {
let watcher: FSWatcher | null = null;
let pairMode = false;
let watchedFiles = new Set<string>();
let lastChangeTime = 0;
const DEBOUNCE_MS = 2000;
const CODE_EXTENSIONS = new Set([
".ts", ".tsx", ".js", ".jsx", ".py", ".rs", ".go",
".java", ".c", ".cpp", ".h", ".css", ".scss",
".html", ".vue", ".svelte", ".rb", ".php",
".json", ".yaml", ".yml", ".toml", ".md",
]);
const IGNORE_PATTERNS = [
"node_modules", ".git", "dist", "build", ".jae",
".next", ".cache", "__pycache__", "target",
];
function shouldWatch(filepath: string): boolean {
if (IGNORE_PATTERNS.some((p) => filepath.includes(p))) return false;
return CODE_EXTENSIONS.has(extname(filepath));
}
function startWatching(cwd: string, ctx: any): void {
if (watcher) {
watcher.close();
}
try {
watcher = watch(cwd, { recursive: true }, (eventType, filename) => {
if (!filename || !pairMode) return;
const filepath = join(cwd, filename);
if (!shouldWatch(filepath)) return;
const now = Date.now();
if (now - lastChangeTime < DEBOUNCE_MS) return;
lastChangeTime = now;
const relPath = relative(cwd, filepath);
watchedFiles.add(relPath);
try {
const content = readFileSync(filepath, "utf-8");
const lines = content.split("\n").length;
ctx.ui.notify(
`\u{1F4DD} File changed: ${relPath} (${lines} lines, ${eventType})\nJAE is watching in pair mode.`,
"info",
);
} catch {
ctx.ui.notify(`\u{1F4DD} File ${eventType}: ${relPath}`, "info");
}
});
ctx.ui.notify(
`\u{1F91D} Pair programming mode started!\nWatching: ${cwd}\nJAE will observe your changes and provide feedback.`,
"info",
);
} catch (err) {
ctx.ui.notify(`Failed to start file watcher: ${err}`, "error");
}
}
function stopWatching(ctx: any): void {
if (watcher) {
watcher.close();
watcher = null;
}
const filesCount = watchedFiles.size;
watchedFiles.clear();
ctx.ui.notify(
`\u{1F91D} Pair programming mode stopped. ${filesCount} files were modified during session.`,
"info",
);
}
pi.on("session_end", async (_event, _ctx) => {
if (watcher) {
watcher.close();
watcher = null;
pairMode = false;
}
});
pi.on("before_agent_start", async (_event, _ctx) => {
if (pairMode && watchedFiles.size > 0) {
const filesList = Array.from(watchedFiles).slice(0, 10).join(", ");
return {
systemPrompt: `\n\n# Pair Programming Mode Active\nRecently modified files: ${filesList}\nProvide constructive feedback on changes. Suggest improvements, catch bugs, and explain patterns.\n`,
};
}
});
pi.registerCommand("pair", {
description: "Toggle pair programming mode - JAE watches and comments on your file changes",
handler: async (_args, ctx) => {
pairMode = !pairMode;
if (pairMode) {
startWatching(ctx.cwd, ctx);
ctx.ui.setFooter("pair", "\u{1F91D} Pair Mode");
} else {
stopWatching(ctx);
ctx.ui.setFooter("pair", undefined);
}
},
});
}