lib/project-file-history.js
Rama main · réplica de lectura del repositorio original.
var path = require("path");
var execFileSync = require("child_process").execFileSync;
var wrapSpawnAsUser = require("./os-users").wrapSpawnAsUser;
function attachFileHistory(ctx, access) {
var cwd = ctx.cwd;
var sm = ctx.sm;
var sendTo = ctx.sendTo;
function runGit(ws, args, options) {
options = Object.assign({}, options, { stdio: ["ignore", "pipe", "pipe"] });
var identity = access.osIdentity(ws);
if (identity) options = Object.assign({}, options, { uid: identity.uid, gid: identity.gid });
var command = wrapSpawnAsUser("git", args, options);
return execFileSync(command.command, command.args, command.options);
}
function handleFileHistory(ws, msg) {
function reply(message) { sendTo(ws, Object.assign({ instanceId: msg.instanceId, requestId: msg.requestId }, message)); }
if (!/^fs_(file_history|git_diff|file_at)$/.test(msg.type)) return false;
if (typeof msg.path !== "string" || !msg.path || path.isAbsolute(msg.path) ||
path.relative(cwd, path.resolve(cwd, msg.path)).split(path.sep).indexOf("..") !== -1 ||
(msg.hash && !/^[a-fA-F0-9]{4,64}$/.test(msg.hash)) ||
(msg.hash2 && !/^[a-fA-F0-9]{4,64}$/.test(msg.hash2))) {
reply({ type: msg.type + "_result", path: msg.path, error: "Invalid file history request" });
return true;
}
// --- File edit history ---
if (msg.type === "fs_file_history") {
var histPath = msg.path;
if (!histPath) {
reply({ type: "fs_file_history_result", path: histPath, entries: [] });
return true;
}
var absHistPath = path.resolve(cwd, histPath);
var entries = [];
// Collect session edits
sm.sessions.forEach(function (session) {
if (!access.canReadSession(ws, session)) return;
var sessionLocalId = session.localId;
var sessionTitle = session.title || "Untitled";
var histLen = session.history.length || 1;
for (var hi = 0; hi < session.history.length; hi++) {
var entry = session.history[hi];
if (entry.type !== "tool_executing") continue;
if (entry.name !== "Edit" && entry.name !== "Write") continue;
if (!entry.input || !entry.input.file_path) continue;
if (entry.input.file_path !== absHistPath) continue;
// Find parent assistant UUID + message snippet by scanning backwards
var assistantUuid = null;
var uuidIndex = -1;
for (var hj = hi - 1; hj >= 0; hj--) {
if (session.history[hj].type === "message_uuid" && session.history[hj].messageType === "assistant") {
assistantUuid = session.history[hj].uuid;
uuidIndex = hj;
break;
}
}
// Find user prompt by scanning backwards from the assistant uuid
var messageSnippet = "";
var searchFrom = uuidIndex >= 0 ? uuidIndex : hi;
for (var hk = searchFrom - 1; hk >= 0; hk--) {
if (session.history[hk].type === "user_message" && session.history[hk].text) {
messageSnippet = session.history[hk].text.trim().substring(0, 100);
break;
}
}
// Collect Claude's explanation: scan backwards from tool_executing
// to find the nearest delta text block (skipping tool_start).
// If no delta found immediately before this tool, scan past
// intervening tool blocks to find the last delta text within
// the same assistant turn.
var assistantSnippet = "";
var deltaChunks = [];
for (var hd = hi - 1; hd >= 0; hd--) {
var hEntry = session.history[hd];
if (hEntry.type === "tool_start") continue;
if (hEntry.type === "delta" && hEntry.text) {
deltaChunks.unshift(hEntry.text);
} else {
break;
}
}
if (deltaChunks.length === 0) {
// No delta immediately before; scan past tool blocks
// to find the nearest preceding delta in the same turn
for (var hd2 = hi - 1; hd2 >= 0; hd2--) {
var hEntry2 = session.history[hd2];
if (hEntry2.type === "tool_start" || hEntry2.type === "tool_executing" || hEntry2.type === "tool_result") continue;
if (hEntry2.type === "delta" && hEntry2.text) {
// Found a delta before an earlier tool in the same turn.
// Collect this contiguous block of deltas.
for (var hd3 = hd2; hd3 >= 0; hd3--) {
var hEntry3 = session.history[hd3];
if (hEntry3.type === "tool_start") continue;
if (hEntry3.type === "delta" && hEntry3.text) {
deltaChunks.unshift(hEntry3.text);
} else {
break;
}
}
break;
} else {
// Hit message_uuid, user_message, etc. Stop.
break;
}
}
}
assistantSnippet = deltaChunks.join("").trim().substring(0, 150);
// Approximate timestamp: interpolate between session creation and last activity
var tStart = session.createdAt || 0;
var tEnd = session.lastActivity || tStart;
var ts = tStart + Math.floor((hi / histLen) * (tEnd - tStart));
var editRecord = {
source: "session",
timestamp: ts,
sessionLocalId: sessionLocalId,
sessionTitle: sessionTitle,
assistantUuid: assistantUuid,
toolId: entry.id,
messageSnippet: messageSnippet,
assistantSnippet: assistantSnippet,
toolName: entry.name,
};
if (entry.name === "Edit") {
editRecord.old_string = entry.input.old_string || "";
editRecord.new_string = entry.input.new_string || "";
} else {
editRecord.isFullWrite = true;
if (typeof entry.input.content === "string") editRecord.content = entry.input.content;
}
entries.push(editRecord);
}
});
// Collect git commits
try {
var gitLog = runGit(ws,
["log", "--format=%H|%at|%an|%s", "--follow", "--", histPath],
{ cwd: cwd, encoding: "utf8", timeout: 5000 }
);
var gitLines = gitLog.trim().split("\n");
for (var gi = 0; gi < gitLines.length; gi++) {
if (!gitLines[gi]) continue;
var parts = gitLines[gi].split("|");
if (parts.length < 4) continue;
entries.push({
source: "git",
hash: parts[0],
timestamp: parseInt(parts[1], 10) * 1000,
author: parts[2],
message: parts.slice(3).join("|"),
});
}
} catch (e) {
// Not a git repo or file not tracked, that is fine
}
// Sort by timestamp descending (newest first)
entries.sort(function (a, b) { return b.timestamp - a.timestamp; });
reply({ type: "fs_file_history_result", path: histPath, entries: entries });
return true;
}
// --- Git diff for file history ---
if (msg.type === "fs_git_diff") {
var diffPath = msg.path;
var hash = msg.hash;
var hash2 = msg.hash2 || null;
if (!diffPath || !hash) {
reply({ type: "fs_git_diff_result", hash: hash, path: diffPath, diff: "", error: "Missing params" });
return true;
}
try {
var diff;
if (hash2) {
diff = runGit(ws, ["diff", hash, hash2, "--", diffPath],
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
} else {
diff = runGit(ws, ["show", hash, "--format=", "--", diffPath],
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
}
reply({ type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: diff || "" });
} catch (e) {
reply({ type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: "", error: e.message });
}
return true;
}
// --- File content at a git commit ---
if (msg.type === "fs_file_at") {
var atPath = msg.path;
var atHash = msg.hash;
if (!atPath || !atHash) {
reply({ type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: "Missing params" });
return true;
}
try {
// Convert to repo-relative path (git show requires hash:relative/path)
var atAbsPath = path.resolve(cwd, atPath);
var atRelPath = path.relative(cwd, atAbsPath);
var content = runGit(ws, ["show", atHash + ":" + atRelPath],
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
reply({ type: "fs_file_at_result", hash: atHash, path: atPath, content: content });
} catch (e) {
reply({ type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: e.message });
}
return true;
}
return false;
}
return { handleFileHistory: handleFileHistory };
}
module.exports = { attachFileHistory: attachFileHistory };