add filechanges extension from amosblomqvist/pi-config
This commit is contained in:
594
extensions/filechanges/index.ts
Normal file
594
extensions/filechanges/index.ts
Normal file
@@ -0,0 +1,594 @@
|
||||
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
DynamicBorder,
|
||||
getMarkdownTheme,
|
||||
isEditToolResult,
|
||||
isToolCallEventType,
|
||||
isWriteToolResult,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import type { SelectItem } from "@mariozechner/pi-tui";
|
||||
import { Container, Key, Markdown, SelectList, Text, matchesKey } from "@mariozechner/pi-tui";
|
||||
import { createTwoFilesPatch } from "diff";
|
||||
import { readFile, writeFile, rm, mkdir } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
|
||||
// Custom session entry types
|
||||
// New name: filechanges
|
||||
const ENTRY_BASELINE = "filechanges:baseline";
|
||||
const ENTRY_CLEAR = "filechanges:clear";
|
||||
const ENTRY_UNTRACK = "filechanges:untrack";
|
||||
|
||||
type Baseline = {
|
||||
path: string; // normalized path relative to ctx.cwd where possible
|
||||
absPath: string;
|
||||
originalContent: string | null; // null => file did not exist (created)
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
type TrackedFile = {
|
||||
path: string;
|
||||
absPath: string;
|
||||
displayPath: string;
|
||||
originalContent: string | null;
|
||||
currentContent: string;
|
||||
diff: string;
|
||||
added: number;
|
||||
removed: number;
|
||||
kind: "new" | "edited";
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
type PendingSnapshot = {
|
||||
path: string;
|
||||
absPath: string;
|
||||
before: string | null;
|
||||
};
|
||||
|
||||
function stripAtPrefix(p: string): string {
|
||||
return p.startsWith("@") ? p.slice(1) : p;
|
||||
}
|
||||
|
||||
function normalizeToolPath(cwd: string, raw: string): { absPath: string; relPath: string } {
|
||||
const cleaned = stripAtPrefix(raw);
|
||||
const absPath = resolve(cwd, cleaned);
|
||||
// Use relative path for storage/UI when possible. If it escapes cwd, keep the cleaned input.
|
||||
const rel = relative(cwd, absPath);
|
||||
const relPath = rel && !rel.startsWith("..") && rel !== "" ? rel : cleaned;
|
||||
return { absPath, relPath };
|
||||
}
|
||||
|
||||
async function readTextOrNull(absPath: string): Promise<string | null> {
|
||||
try {
|
||||
return await readFile(absPath, "utf-8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function countDiffLines(unifiedDiff: string): { added: number; removed: number } {
|
||||
let added = 0;
|
||||
let removed = 0;
|
||||
for (const line of unifiedDiff.split("\n")) {
|
||||
if (line.startsWith("+++ ") || line.startsWith("--- ") || line.startsWith("@@")) continue;
|
||||
if (line.startsWith("+")) added++;
|
||||
else if (line.startsWith("-")) removed++;
|
||||
}
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
function formatAddedRemovedPlain(added: number, removed: number): string {
|
||||
return `(+${added}/-${removed})`;
|
||||
}
|
||||
|
||||
|
||||
function styleAddedRemovedForList(theme: any, text: string): string {
|
||||
// File rows use "+x/-y" as description; other rows use normal sentences.
|
||||
const m = text.match(/^\+(\d+)\/\-(\d+)$/);
|
||||
if (!m) return theme.fg("muted", text);
|
||||
const added = Number(m[1]);
|
||||
const removed = Number(m[2]);
|
||||
|
||||
const plus = added === 0 ? theme.fg("text", `+${added}`) : theme.fg("success", `+${added}`);
|
||||
const minus = removed === 0 ? theme.fg("text", `-${removed}`) : theme.fg("error", `-${removed}`);
|
||||
return plus + theme.fg("text", "/") + minus;
|
||||
}
|
||||
|
||||
function formatStatus(tracked: Map<string, TrackedFile>, theme?: any): string | undefined {
|
||||
if (tracked.size === 0) return undefined;
|
||||
let edited = 0;
|
||||
let created = 0;
|
||||
for (const t of tracked.values()) {
|
||||
if (t.kind === "new") created++;
|
||||
else edited++;
|
||||
}
|
||||
if (!theme) {
|
||||
return `Δ ${edited} + ${created}`;
|
||||
}
|
||||
return theme.fg("muted", `Δ ${edited} + ${created}`);
|
||||
}
|
||||
|
||||
function buildWidgetLines(tracked: Map<string, TrackedFile>, theme?: any): string[] | undefined {
|
||||
if (tracked.size === 0) return undefined;
|
||||
const items = [...tracked.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
const max = 8;
|
||||
const lines: string[] = [];
|
||||
|
||||
// Separator between chat history and this widget (widget renders above the editor).
|
||||
//const sep = "─".repeat(60);
|
||||
//lines.push(theme ? theme.fg("borderMuted", sep) : sep);
|
||||
|
||||
for (const t of items.slice(0, max)) {
|
||||
const tag = t.kind === "new" ? "+" : "Δ";
|
||||
|
||||
if (!theme) {
|
||||
lines.push(`${tag} ${t.displayPath} ${formatAddedRemovedPlain(t.added, t.removed)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const prefix = theme.fg("muted", `${tag} `) + theme.fg("muted", `${t.displayPath} `);
|
||||
let counts: string;
|
||||
const plus = t.added === 0 ? theme.fg("text", `+${t.added}`) : theme.fg("success", `+${t.added}`);
|
||||
const minus = t.removed === 0 ? theme.fg("text", `-${t.removed}`) : theme.fg("error", `-${t.removed}`);
|
||||
counts = theme.fg("text", "(") + plus + theme.fg("text", "/") + minus + theme.fg("text", ")");
|
||||
|
||||
lines.push(prefix + counts);
|
||||
}
|
||||
if (items.length > max) {
|
||||
lines.push(theme ? theme.fg("dim", `…and ${items.length - max} more`) : `…and ${items.length - max} more`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function patchFromBaseline(displayPath: string, original: string | null, current: string): string {
|
||||
return createTwoFilesPatch(
|
||||
displayPath,
|
||||
displayPath,
|
||||
original ?? "",
|
||||
current,
|
||||
"",
|
||||
"",
|
||||
{ context: 3 }
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureParentDir(absPath: string): Promise<void> {
|
||||
await mkdir(dirname(absPath), { recursive: true });
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
// In-memory state (reconstructed on session_start from custom entries)
|
||||
const baselines = new Map<string, Baseline>(); // key: relPath
|
||||
const tracked = new Map<string, TrackedFile>(); // key: relPath
|
||||
|
||||
// Per-tool-call snapshot, only committed on successful tool_result
|
||||
const pendingByToolCallId = new Map<string, PendingSnapshot>();
|
||||
|
||||
function updateUi(ctx: any) {
|
||||
if (!ctx?.hasUI) return;
|
||||
|
||||
ctx.ui.setStatus("filechanges", formatStatus(tracked, ctx.ui.theme));
|
||||
ctx.ui.setWidget("filechanges", buildWidgetLines(tracked, ctx.ui.theme));
|
||||
}
|
||||
|
||||
async function recomputeTrackedFile(ctx: any, relPath: string) {
|
||||
const baseline = baselines.get(relPath);
|
||||
if (!baseline) return;
|
||||
|
||||
const current = await readTextOrNull(baseline.absPath);
|
||||
if (baseline.originalContent === null) {
|
||||
// file was created
|
||||
if (current === null) {
|
||||
tracked.delete(relPath);
|
||||
return;
|
||||
}
|
||||
const displayPath = baseline.path;
|
||||
const diff = patchFromBaseline(displayPath, null, current);
|
||||
const { added, removed } = countDiffLines(diff);
|
||||
tracked.set(relPath, {
|
||||
path: baseline.path,
|
||||
absPath: baseline.absPath,
|
||||
displayPath,
|
||||
originalContent: null,
|
||||
currentContent: current,
|
||||
diff,
|
||||
added,
|
||||
removed,
|
||||
kind: "new",
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// file existed before
|
||||
if (current === null) {
|
||||
// Deleted outside of tracked tools (or manually). Still track as edited; diff will show removal.
|
||||
const displayPath = baseline.path;
|
||||
const diff = patchFromBaseline(displayPath, baseline.originalContent, "");
|
||||
const { added, removed } = countDiffLines(diff);
|
||||
tracked.set(relPath, {
|
||||
path: baseline.path,
|
||||
absPath: baseline.absPath,
|
||||
displayPath,
|
||||
originalContent: baseline.originalContent,
|
||||
currentContent: "",
|
||||
diff,
|
||||
added,
|
||||
removed,
|
||||
kind: "edited",
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (current === baseline.originalContent) {
|
||||
// back to original; untrack
|
||||
tracked.delete(relPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const displayPath = baseline.path;
|
||||
const diff = patchFromBaseline(displayPath, baseline.originalContent, current);
|
||||
const { added, removed } = countDiffLines(diff);
|
||||
tracked.set(relPath, {
|
||||
path: baseline.path,
|
||||
absPath: baseline.absPath,
|
||||
displayPath,
|
||||
originalContent: baseline.originalContent,
|
||||
currentContent: current,
|
||||
diff,
|
||||
added,
|
||||
removed,
|
||||
kind: "edited",
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async function clearLog(ctx: ExtensionCommandContext, reason: "accept" | "decline") {
|
||||
baselines.clear();
|
||||
tracked.clear();
|
||||
pendingByToolCallId.clear();
|
||||
pi.appendEntry(ENTRY_CLEAR, { timestamp: Date.now(), reason });
|
||||
updateUi(ctx);
|
||||
}
|
||||
|
||||
async function declineAll(ctx: ExtensionCommandContext) {
|
||||
await ctx.waitForIdle();
|
||||
|
||||
if (tracked.size === 0) {
|
||||
if (ctx.hasUI) ctx.ui.notify("filechanges: nothing to decline.", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const force = (ctx as any).args?.includes("force") ?? false;
|
||||
if (ctx.hasUI && !force) {
|
||||
const ok = await ctx.ui.confirm(
|
||||
"Decline pi changes?",
|
||||
"This will revert ALL currently logged pi changes (overwrite files / delete created files)."
|
||||
);
|
||||
if (!ok) return;
|
||||
} else if (!ctx.hasUI && !force) {
|
||||
throw new Error("Decline requires confirmation. Run: /filechanges-decline force");
|
||||
}
|
||||
|
||||
const items = [...tracked.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
let reverted = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
try {
|
||||
if (item.originalContent === null) {
|
||||
// created file
|
||||
await rm(item.absPath, { force: true });
|
||||
} else {
|
||||
await ensureParentDir(item.absPath);
|
||||
await writeFile(item.absPath, item.originalContent, "utf-8");
|
||||
}
|
||||
reverted++;
|
||||
} catch (e: any) {
|
||||
errors.push(`${item.displayPath}: ${e?.message ?? String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
await clearLog(ctx, "decline");
|
||||
|
||||
if (ctx.hasUI) {
|
||||
if (errors.length === 0) {
|
||||
ctx.ui.notify(`filechanges: declined changes for ${reverted} file(s).`, "success");
|
||||
} else {
|
||||
ctx.ui.notify(
|
||||
`filechanges: declined with ${errors.length} error(s). Run /filechanges to inspect; see console for details.`,
|
||||
"warning"
|
||||
);
|
||||
console.warn("[filechanges] decline errors:\n" + errors.join("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function acceptAll(ctx: ExtensionCommandContext) {
|
||||
await ctx.waitForIdle();
|
||||
|
||||
if (tracked.size === 0) {
|
||||
if (ctx.hasUI) ctx.ui.notify("filechanges: nothing to accept.", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const force = (ctx as any).args?.includes("force") ?? false;
|
||||
if (ctx.hasUI && !force) {
|
||||
const ok = await ctx.ui.confirm(
|
||||
"Accept pi changes?",
|
||||
"This will keep current files as-is and clear the modification log."
|
||||
);
|
||||
if (!ok) return;
|
||||
} else if (!ctx.hasUI && !force) {
|
||||
throw new Error("Accept requires confirmation. Run: /filechanges-accept force");
|
||||
}
|
||||
|
||||
const count = tracked.size;
|
||||
await clearLog(ctx, "accept");
|
||||
if (ctx.hasUI) ctx.ui.notify(`filechanges: accepted changes for ${count} file(s).`, "success");
|
||||
}
|
||||
|
||||
function parseCommandArgs(args: string | undefined): string[] {
|
||||
if (!args) return [];
|
||||
return args
|
||||
.split(/\s+/g)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// Commands
|
||||
pi.registerCommand("filechanges", {
|
||||
description: "Show files changed by pi and inspect diffs",
|
||||
handler: async (_args, ctx) => {
|
||||
// Provide args to helpers (a bit hacky but keeps code compact)
|
||||
(ctx as any).args = parseCommandArgs(_args);
|
||||
|
||||
await ctx.waitForIdle();
|
||||
updateUi(ctx);
|
||||
|
||||
if (!ctx.hasUI) {
|
||||
const items = [...tracked.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
if (items.length === 0) {
|
||||
console.log("filechanges: no pi-made modifications recorded.");
|
||||
return;
|
||||
}
|
||||
// Non-interactive: just print a summary to stdout
|
||||
const lines = buildWidgetLines(tracked) ?? [];
|
||||
console.log(lines.join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Interactive loop: ESC in diff view returns to the modification log.
|
||||
while (true) {
|
||||
await ctx.waitForIdle();
|
||||
updateUi(ctx);
|
||||
|
||||
const items = [...tracked.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
if (items.length === 0) {
|
||||
ctx.ui.notify("filechanges: no pi-made modifications recorded.", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectItems: SelectItem[] = [
|
||||
{ value: "__accept__", label: "Accept changes (clear log)", description: "Keep current files" },
|
||||
{ value: "__decline__", label: "Undo changes (revert)", description: "Restore original contents" },
|
||||
{ value: "__sep__", label: "────────", description: "" },
|
||||
...items.map((t) => ({
|
||||
value: t.path,
|
||||
label: `${t.kind === "new" ? "+" : "Δ"} ${t.displayPath}`,
|
||||
description: `+${t.added}/-${t.removed}`,
|
||||
})),
|
||||
];
|
||||
|
||||
const picked = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
container.addChild(new Text(theme.fg("accent", theme.bold("File changes")), 1, 0));
|
||||
|
||||
const list = new SelectList(selectItems, Math.min(14, selectItems.length), {
|
||||
selectedPrefix: (t) => theme.fg("accent", t),
|
||||
selectedText: (t) => theme.fg("accent", t),
|
||||
description: (t) => styleAddedRemovedForList(theme, t),
|
||||
scrollInfo: (t) => theme.fg("dim", t),
|
||||
noMatch: (t) => theme.fg("warning", t),
|
||||
});
|
||||
|
||||
list.onSelect = (item) => {
|
||||
if (item.value === "__sep__") return;
|
||||
done(item.value);
|
||||
};
|
||||
list.onCancel = () => done(null);
|
||||
container.addChild(list);
|
||||
|
||||
container.addChild(
|
||||
new Text(theme.fg("dim", "↑↓ navigate • enter select • esc close"), 1, 0)
|
||||
);
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
list.handleInput(data);
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
}, { overlay: true });
|
||||
|
||||
if (!picked) return;
|
||||
if (picked === "__accept__") {
|
||||
await acceptAll(ctx);
|
||||
return;
|
||||
}
|
||||
if (picked === "__decline__") {
|
||||
await declineAll(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
const t = tracked.get(picked);
|
||||
if (!t) {
|
||||
ctx.ui.notify("filechanges: entry not found (maybe log was cleared).", "warning");
|
||||
continue;
|
||||
}
|
||||
|
||||
const md = "```diff\n" + (t.diff.trimEnd() || "(no diff)") + "\n```";
|
||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
container.addChild(new Text(theme.fg("accent", theme.bold(t.displayPath)), 1, 0));
|
||||
container.addChild(new Markdown(md, 1, 0, getMarkdownTheme()));
|
||||
container.addChild(new Text(theme.fg("dim", "esc to go back"), 1, 0));
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) done();
|
||||
else tui.requestRender();
|
||||
},
|
||||
};
|
||||
}, { overlay: true });
|
||||
|
||||
// After closing diff, loop back to the modification log.
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("filechanges-accept", {
|
||||
description: "Accept pi-made changes (keeps files, clears log)",
|
||||
handler: async (args, ctx) => {
|
||||
(ctx as any).args = parseCommandArgs(args);
|
||||
await acceptAll(ctx);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("filechanges-decline", {
|
||||
description: "Decline pi-made changes (reverts files, clears log)",
|
||||
handler: async (args, ctx) => {
|
||||
(ctx as any).args = parseCommandArgs(args);
|
||||
await declineAll(ctx);
|
||||
},
|
||||
});
|
||||
|
||||
async function rebuildFromSession(ctx: any): Promise<void> {
|
||||
baselines.clear();
|
||||
tracked.clear();
|
||||
pendingByToolCallId.clear();
|
||||
|
||||
// Replay custom entries on current branch
|
||||
for (const entry of ctx.sessionManager.getBranch()) {
|
||||
if (entry.type !== "custom") continue;
|
||||
|
||||
if (entry.customType === ENTRY_CLEAR) {
|
||||
baselines.clear();
|
||||
tracked.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.customType === ENTRY_BASELINE) {
|
||||
const data = entry.data as any;
|
||||
if (!data?.path) continue;
|
||||
const { absPath, relPath } = normalizeToolPath(ctx.cwd, data.path);
|
||||
baselines.set(relPath, {
|
||||
path: relPath,
|
||||
absPath,
|
||||
originalContent: typeof data.originalContent === "string" ? data.originalContent : null,
|
||||
createdAt: typeof data.timestamp === "number" ? data.timestamp : Date.now(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.customType === ENTRY_UNTRACK) {
|
||||
const data = entry.data as any;
|
||||
if (!data?.path) continue;
|
||||
const { relPath } = normalizeToolPath(ctx.cwd, data.path);
|
||||
baselines.delete(relPath);
|
||||
tracked.delete(relPath);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute current diffs
|
||||
for (const relPath of baselines.keys()) {
|
||||
await recomputeTrackedFile(ctx, relPath);
|
||||
}
|
||||
|
||||
updateUi(ctx);
|
||||
}
|
||||
|
||||
// Rebuild state on any session/branch navigation events
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
await rebuildFromSession(ctx);
|
||||
});
|
||||
|
||||
pi.on("session_switch", async (_event, ctx) => {
|
||||
await rebuildFromSession(ctx);
|
||||
});
|
||||
|
||||
pi.on("session_tree", async (_event, ctx) => {
|
||||
await rebuildFromSession(ctx);
|
||||
});
|
||||
|
||||
pi.on("session_fork", async (_event, ctx) => {
|
||||
await rebuildFromSession(ctx);
|
||||
});
|
||||
|
||||
// Capture before snapshots for edit/write
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (isToolCallEventType("edit", event) || isToolCallEventType("write", event)) {
|
||||
const { absPath, relPath } = normalizeToolPath(ctx.cwd, event.input.path);
|
||||
const before = await readTextOrNull(absPath);
|
||||
pendingByToolCallId.set(event.toolCallId, { path: relPath, absPath, before });
|
||||
}
|
||||
});
|
||||
|
||||
// Commit on successful results
|
||||
pi.on("tool_result", async (event, ctx) => {
|
||||
if (event.isError) {
|
||||
pendingByToolCallId.delete(event.toolCallId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEditToolResult(event) && !isWriteToolResult(event)) return;
|
||||
|
||||
const pending = pendingByToolCallId.get(event.toolCallId);
|
||||
pendingByToolCallId.delete(event.toolCallId);
|
||||
if (!pending) return;
|
||||
|
||||
// If no baseline exists yet for this file, create one now from the successful call's snapshot.
|
||||
if (!baselines.has(pending.path)) {
|
||||
baselines.set(pending.path, {
|
||||
path: pending.path,
|
||||
absPath: pending.absPath,
|
||||
originalContent: pending.before,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
pi.appendEntry(ENTRY_BASELINE, {
|
||||
path: pending.path,
|
||||
originalContent: pending.before,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Recompute cumulative diff against baseline
|
||||
await recomputeTrackedFile(ctx, pending.path);
|
||||
|
||||
// If file is back to baseline, untrack + persist
|
||||
const baseline = baselines.get(pending.path);
|
||||
const current = await readTextOrNull(pending.absPath);
|
||||
if (baseline) {
|
||||
const backToOriginal =
|
||||
(baseline.originalContent !== null && current === baseline.originalContent) ||
|
||||
(baseline.originalContent === null && current === null);
|
||||
|
||||
if (backToOriginal) {
|
||||
baselines.delete(pending.path);
|
||||
tracked.delete(pending.path);
|
||||
pi.appendEntry(ENTRY_UNTRACK, { path: pending.path, timestamp: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
updateUi(ctx);
|
||||
});
|
||||
}
|
||||
24
extensions/filechanges/package-lock.json
generated
Normal file
24
extensions/filechanges/package-lock.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "filechanges",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "filechanges",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"diff": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
extensions/filechanges/package.json
Normal file
9
extensions/filechanges/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "filechanges",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"diff": "^7.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user