Ctrl K
ComunidadGuías…

lib/project-file-history.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 };