chore: remove files extension
This commit is contained in:
@@ -1,199 +0,0 @@
|
|||||||
/**
|
|
||||||
* Files Extension
|
|
||||||
*
|
|
||||||
* /files command lists all files the model has read/written/edited in the active session branch,
|
|
||||||
* coalesced by path and sorted newest first. Selecting a file opens it in VS Code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
||||||
import { Container, Key, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
|
||||||
|
|
||||||
interface FileEntry {
|
|
||||||
path: string;
|
|
||||||
operations: Set<"read" | "write" | "edit">;
|
|
||||||
lastTimestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileToolName = "read" | "write" | "edit";
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
|
||||||
pi.registerCommand("files", {
|
|
||||||
description: "Show files read/written/edited in this session",
|
|
||||||
handler: async (_args, ctx) => {
|
|
||||||
if (!ctx.hasUI) {
|
|
||||||
ctx.ui.notify("No UI available", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current branch (path from leaf to root)
|
|
||||||
const branch = ctx.sessionManager.getBranch();
|
|
||||||
|
|
||||||
// First pass: collect tool calls (id -> {path, name}) from assistant messages
|
|
||||||
const toolCalls = new Map<string, { path: string; name: FileToolName; timestamp: number }>();
|
|
||||||
|
|
||||||
for (const entry of branch) {
|
|
||||||
if (entry.type !== "message") continue;
|
|
||||||
const msg = entry.message;
|
|
||||||
|
|
||||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
||||||
for (const block of msg.content) {
|
|
||||||
if (block.type === "toolCall") {
|
|
||||||
const name = block.name;
|
|
||||||
if (name === "read" || name === "write" || name === "edit") {
|
|
||||||
const path = block.arguments?.path;
|
|
||||||
if (path && typeof path === "string") {
|
|
||||||
toolCalls.set(block.id, { path, name, timestamp: msg.timestamp });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: match tool results to get the actual execution timestamp
|
|
||||||
const fileMap = new Map<string, FileEntry>();
|
|
||||||
|
|
||||||
for (const entry of branch) {
|
|
||||||
if (entry.type !== "message") continue;
|
|
||||||
const msg = entry.message;
|
|
||||||
|
|
||||||
if (msg.role === "toolResult") {
|
|
||||||
const toolCall = toolCalls.get(msg.toolCallId);
|
|
||||||
if (!toolCall) continue;
|
|
||||||
|
|
||||||
const { path, name } = toolCall;
|
|
||||||
const timestamp = msg.timestamp;
|
|
||||||
|
|
||||||
const existing = fileMap.get(path);
|
|
||||||
if (existing) {
|
|
||||||
existing.operations.add(name);
|
|
||||||
if (timestamp > existing.lastTimestamp) {
|
|
||||||
existing.lastTimestamp = timestamp;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fileMap.set(path, {
|
|
||||||
path,
|
|
||||||
operations: new Set([name]),
|
|
||||||
lastTimestamp: timestamp,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileMap.size === 0) {
|
|
||||||
ctx.ui.notify("No files read/written/edited in this session", "info");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by most recent first
|
|
||||||
const files = Array.from(fileMap.values()).sort((a, b) => b.lastTimestamp - a.lastTimestamp);
|
|
||||||
|
|
||||||
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/;
|
|
||||||
const quoteCmdArg = (value: string) => `"${value.replace(/"/g, '""')}"`;
|
|
||||||
|
|
||||||
const openWithCode = async (path: string) => {
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(path)) {
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Refusing to open ${path}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const commandLine = `code -g ${quoteCmdArg(path)}`;
|
|
||||||
return pi.exec("cmd", ["/d", "/s", "/c", commandLine], { cwd: ctx.cwd });
|
|
||||||
}
|
|
||||||
return pi.exec("code", ["-g", path], { cwd: ctx.cwd });
|
|
||||||
};
|
|
||||||
|
|
||||||
const openSelected = async (file: FileEntry): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const openResult = await openWithCode(file.path);
|
|
||||||
if (!openResult) return;
|
|
||||||
if (openResult.code !== 0) {
|
|
||||||
const openStderr = openResult.stderr.trim();
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Failed to open ${file.path} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : ""}`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
ctx.ui.notify(`Failed to open ${file.path}: ${message}`, "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show file picker with SelectList
|
|
||||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
||||||
const container = new Container();
|
|
||||||
|
|
||||||
// Top border
|
|
||||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
||||||
|
|
||||||
// Title
|
|
||||||
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to open")), 0, 0));
|
|
||||||
|
|
||||||
// Build select items with colored operations
|
|
||||||
const items: SelectItem[] = files.map((f) => {
|
|
||||||
const ops: string[] = [];
|
|
||||||
if (f.operations.has("read")) ops.push(theme.fg("muted", "R"));
|
|
||||||
if (f.operations.has("write")) ops.push(theme.fg("success", "W"));
|
|
||||||
if (f.operations.has("edit")) ops.push(theme.fg("warning", "E"));
|
|
||||||
const opsLabel = ops.join("");
|
|
||||||
return {
|
|
||||||
value: f,
|
|
||||||
label: `${opsLabel} ${f.path}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleRows = Math.min(files.length, 15);
|
|
||||||
let currentIndex = 0;
|
|
||||||
|
|
||||||
const selectList = new SelectList(items, visibleRows, {
|
|
||||||
selectedPrefix: (t) => theme.fg("accent", t),
|
|
||||||
selectedText: (t) => t, // Keep existing colors
|
|
||||||
description: (t) => theme.fg("muted", t),
|
|
||||||
scrollInfo: (t) => theme.fg("dim", t),
|
|
||||||
noMatch: (t) => theme.fg("warning", t),
|
|
||||||
});
|
|
||||||
selectList.onSelect = (item) => {
|
|
||||||
void openSelected(item.value as FileEntry);
|
|
||||||
};
|
|
||||||
selectList.onCancel = () => done();
|
|
||||||
selectList.onSelectionChange = (item) => {
|
|
||||||
currentIndex = items.indexOf(item);
|
|
||||||
};
|
|
||||||
container.addChild(selectList);
|
|
||||||
|
|
||||||
// Help text
|
|
||||||
container.addChild(
|
|
||||||
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bottom border
|
|
||||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
||||||
|
|
||||||
return {
|
|
||||||
render: (w) => container.render(w),
|
|
||||||
invalidate: () => container.invalidate(),
|
|
||||||
handleInput: (data) => {
|
|
||||||
// Add paging with left/right
|
|
||||||
if (matchesKey(data, Key.left)) {
|
|
||||||
// Page up - clamp to 0
|
|
||||||
currentIndex = Math.max(0, currentIndex - visibleRows);
|
|
||||||
selectList.setSelectedIndex(currentIndex);
|
|
||||||
} else if (matchesKey(data, Key.right)) {
|
|
||||||
// Page down - clamp to last
|
|
||||||
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
|
||||||
selectList.setSelectedIndex(currentIndex);
|
|
||||||
} else {
|
|
||||||
selectList.handleInput(data);
|
|
||||||
}
|
|
||||||
tui.requestRender();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user