chore: Refactor git context gathering for session and init [LET-7906] (#1379)
This commit is contained in:
@@ -230,7 +230,7 @@ import { parsePatchOperations } from "./helpers/formatArgsDisplay";
|
||||
import {
|
||||
buildInitMessage,
|
||||
fireAutoInit,
|
||||
gatherGitContext,
|
||||
gatherInitGitContext,
|
||||
} from "./helpers/initCommand";
|
||||
import {
|
||||
getReflectionSettings,
|
||||
@@ -9531,7 +9531,7 @@ export default function App({
|
||||
true,
|
||||
);
|
||||
|
||||
const gitContext = gatherGitContext();
|
||||
const { context: gitContext } = gatherInitGitContext();
|
||||
const memoryDir = settingsManager.isMemfsEnabled(agentId)
|
||||
? getMemoryFilesystemRoot(agentId)
|
||||
: undefined;
|
||||
|
||||
117
src/cli/helpers/gitContext.ts
Normal file
117
src/cli/helpers/gitContext.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
export interface GitContextSnapshot {
|
||||
isGitRepo: boolean;
|
||||
branch: string | null;
|
||||
status: string | null;
|
||||
recentCommits: string | null;
|
||||
gitUser: string | null;
|
||||
}
|
||||
|
||||
export interface GatherGitContextOptions {
|
||||
cwd?: string;
|
||||
recentCommitLimit?: number;
|
||||
/**
|
||||
* Git log format string passed to `git log --format=...`.
|
||||
* If omitted, uses `git log --oneline`.
|
||||
*/
|
||||
recentCommitFormat?: string;
|
||||
statusLineLimit?: number;
|
||||
}
|
||||
|
||||
function runGit(args: string[], cwd: string): string | null {
|
||||
try {
|
||||
return execFileSync("git", args, {
|
||||
cwd,
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function truncateLines(value: string, maxLines: number): string {
|
||||
const lines = value.split("\n");
|
||||
if (lines.length <= maxLines) {
|
||||
return value;
|
||||
}
|
||||
return (
|
||||
lines.slice(0, maxLines).join("\n") +
|
||||
`\n... and ${lines.length - maxLines} more changes`
|
||||
);
|
||||
}
|
||||
|
||||
function formatGitUser(
|
||||
name: string | null,
|
||||
email: string | null,
|
||||
): string | null {
|
||||
if (!name && !email) {
|
||||
return null;
|
||||
}
|
||||
if (name && email) {
|
||||
return `${name} <${email}>`;
|
||||
}
|
||||
return name || email;
|
||||
}
|
||||
|
||||
export function gatherGitContextSnapshot(
|
||||
options: GatherGitContextOptions = {},
|
||||
): GitContextSnapshot {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const recentCommitLimit = options.recentCommitLimit ?? 3;
|
||||
|
||||
if (!runGit(["rev-parse", "--git-dir"], cwd)) {
|
||||
return {
|
||||
isGitRepo: false,
|
||||
branch: null,
|
||||
status: null,
|
||||
recentCommits: null,
|
||||
gitUser: null,
|
||||
};
|
||||
}
|
||||
|
||||
const branch = runGit(["branch", "--show-current"], cwd);
|
||||
|
||||
const fullStatus = runGit(["status", "--short"], cwd);
|
||||
const status =
|
||||
typeof fullStatus === "string" && options.statusLineLimit
|
||||
? truncateLines(fullStatus, options.statusLineLimit)
|
||||
: fullStatus;
|
||||
|
||||
const recentCommits = options.recentCommitFormat
|
||||
? runGit(
|
||||
[
|
||||
"log",
|
||||
`--format=${options.recentCommitFormat}`,
|
||||
"-n",
|
||||
String(recentCommitLimit),
|
||||
],
|
||||
cwd,
|
||||
)
|
||||
: runGit(["log", "--oneline", "-n", String(recentCommitLimit)], cwd);
|
||||
|
||||
const userConfig = runGit(
|
||||
["config", "--get-regexp", "^user\\.(name|email)$"],
|
||||
cwd,
|
||||
);
|
||||
let userName: string | null = null;
|
||||
let userEmail: string | null = null;
|
||||
if (userConfig) {
|
||||
for (const line of userConfig.split("\n")) {
|
||||
if (line.startsWith("user.name "))
|
||||
userName = line.slice("user.name ".length);
|
||||
else if (line.startsWith("user.email "))
|
||||
userEmail = line.slice("user.email ".length);
|
||||
}
|
||||
}
|
||||
const gitUser = formatGitUser(userName, userEmail);
|
||||
|
||||
return {
|
||||
isGitRepo: true,
|
||||
branch,
|
||||
status,
|
||||
recentCommits,
|
||||
gitUser,
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../../agent/memoryFilesystem";
|
||||
import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants";
|
||||
import { settingsManager } from "../../settings-manager";
|
||||
import { gatherGitContextSnapshot } from "./gitContext";
|
||||
import { getSnapshot as getSubagentSnapshot } from "./subagentState";
|
||||
|
||||
// ── Guard ──────────────────────────────────────────────────
|
||||
@@ -29,69 +30,38 @@ export function hasActiveInitSubagent(): boolean {
|
||||
|
||||
// ── Git context ────────────────────────────────────────────
|
||||
|
||||
export function gatherGitContext(): string {
|
||||
export function gatherInitGitContext(): { context: string; identity: string } {
|
||||
try {
|
||||
const cwd = process.cwd();
|
||||
const git = gatherGitContextSnapshot({
|
||||
recentCommitLimit: 10,
|
||||
});
|
||||
if (!git.isGitRepo) {
|
||||
return {
|
||||
context: "(not a git repository)",
|
||||
identity: "",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" });
|
||||
|
||||
const branch = execSync("git branch --show-current", {
|
||||
cwd,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
const mainBranch = execSync(
|
||||
"git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo 'main'",
|
||||
{ cwd, encoding: "utf-8", shell: "/bin/bash" },
|
||||
).trim();
|
||||
const status = execSync("git status --short", {
|
||||
cwd,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
const recentCommits = execSync(
|
||||
"git log --oneline -10 2>/dev/null || echo 'No commits yet'",
|
||||
{ cwd, encoding: "utf-8" },
|
||||
).trim();
|
||||
|
||||
return `
|
||||
- branch: ${branch}
|
||||
- main: ${mainBranch}
|
||||
- status: ${status || "(clean)"}
|
||||
return {
|
||||
context: `
|
||||
- branch: ${git.branch ?? "(unknown)"}
|
||||
- status: ${git.status || "(clean)"}
|
||||
|
||||
Recent commits:
|
||||
${recentCommits}
|
||||
`;
|
||||
} catch {
|
||||
return "(not a git repository)";
|
||||
}
|
||||
${git.recentCommits || "No commits yet"}
|
||||
`,
|
||||
identity: git.gitUser ?? "",
|
||||
};
|
||||
} catch {
|
||||
// execSync import failed (shouldn't happen with static import, but be safe)
|
||||
return "";
|
||||
return {
|
||||
context: "",
|
||||
identity: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shallow init (background subagent) ───────────────────
|
||||
|
||||
/** Gather git identity for the local user. */
|
||||
function gatherGitIdentity(): string {
|
||||
const cwd = process.cwd();
|
||||
try {
|
||||
const userName = execSync("git config user.name 2>/dev/null || true", {
|
||||
cwd,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
const userEmail = execSync("git config user.email 2>/dev/null || true", {
|
||||
cwd,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
if (userName || userEmail) return `${userName} <${userEmail}>`;
|
||||
return "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** Read existing memory files from the local filesystem. */
|
||||
function gatherExistingMemory(agentId: string): string {
|
||||
const systemDir = getMemorySystemDir(agentId);
|
||||
@@ -258,8 +228,7 @@ export async function fireAutoInit(
|
||||
if (hasActiveInitSubagent()) return false;
|
||||
if (!settingsManager.isMemfsEnabled(agentId)) return false;
|
||||
|
||||
const gitContext = gatherGitContext();
|
||||
const gitIdentity = gatherGitIdentity();
|
||||
const gitDetails = gatherInitGitContext();
|
||||
const existingMemory = gatherExistingMemory(agentId);
|
||||
const dirListing = gatherDirListing();
|
||||
|
||||
@@ -267,8 +236,8 @@ export async function fireAutoInit(
|
||||
agentId,
|
||||
workingDirectory: process.cwd(),
|
||||
memoryDir: getMemoryFilesystemRoot(agentId),
|
||||
gitContext,
|
||||
gitIdentity,
|
||||
gitContext: gitDetails.context,
|
||||
gitIdentity: gitDetails.identity,
|
||||
existingMemory,
|
||||
dirListing,
|
||||
});
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// Generates session context system reminder for the first message of each CLI session
|
||||
// Contains device/environment information only. Agent metadata is in agentMetadata.ts.
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { platform } from "node:os";
|
||||
import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants";
|
||||
import { getVersion } from "../../version";
|
||||
import { gatherGitContextSnapshot } from "./gitContext";
|
||||
|
||||
/**
|
||||
* Get the current local time in a human-readable format
|
||||
@@ -40,17 +40,6 @@ export function getDeviceType(): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely execute a git command, returning null on failure
|
||||
*/
|
||||
function safeGitExec(command: string, cwd: string): string | null {
|
||||
try {
|
||||
return execSync(command, { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather git information if in a git repository
|
||||
* Returns truncated commits (3) and status (20 lines)
|
||||
@@ -61,41 +50,25 @@ function getGitInfo(): {
|
||||
branch?: string;
|
||||
recentCommits?: string;
|
||||
status?: string;
|
||||
gitUser?: string;
|
||||
} {
|
||||
const cwd = process.cwd();
|
||||
const git = gatherGitContextSnapshot({
|
||||
recentCommitLimit: 3,
|
||||
recentCommitFormat: "%h %s (%an)",
|
||||
statusLineLimit: 20,
|
||||
});
|
||||
|
||||
try {
|
||||
// Check if we're in a git repo
|
||||
execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" });
|
||||
|
||||
// Get current branch (with fallback)
|
||||
const branch = safeGitExec("git branch --show-current", cwd) ?? "(unknown)";
|
||||
|
||||
// Get recent commits (3 commits with author, with fallback)
|
||||
const recentCommits =
|
||||
safeGitExec('git log --format="%h %s (%an)" -3', cwd) ??
|
||||
"(failed to get commits)";
|
||||
|
||||
// Get git status (truncate to 20 lines, with fallback)
|
||||
const fullStatus =
|
||||
safeGitExec("git status --short", cwd) ?? "(failed to get status)";
|
||||
const statusLines = fullStatus.split("\n");
|
||||
let status = fullStatus;
|
||||
if (statusLines.length > 20) {
|
||||
status =
|
||||
statusLines.slice(0, 20).join("\n") +
|
||||
`\n... and ${statusLines.length - 20} more files`;
|
||||
}
|
||||
|
||||
return {
|
||||
isGitRepo: true,
|
||||
branch,
|
||||
recentCommits,
|
||||
status: status || "(clean working tree)",
|
||||
};
|
||||
} catch {
|
||||
if (!git.isGitRepo) {
|
||||
return { isGitRepo: false };
|
||||
}
|
||||
|
||||
return {
|
||||
isGitRepo: true,
|
||||
branch: git.branch ?? "(unknown)",
|
||||
recentCommits: git.recentCommits ?? "(failed to get commits)",
|
||||
status: git.status || "(clean working tree)",
|
||||
gitUser: git.gitUser ?? "(not configured)",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,6 +119,7 @@ The user has just initiated a new connection via the [Letta Code CLI client](htt
|
||||
// Add git info if available
|
||||
if (gitInfo.isGitRepo) {
|
||||
context += `- **Git repository**: Yes (branch: ${gitInfo.branch})
|
||||
- **Git user**: ${gitInfo.gitUser}
|
||||
|
||||
### Recent Commits
|
||||
\`\`\`
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("init wiring", () => {
|
||||
const helperSource = readSource("../../cli/helpers/initCommand.ts");
|
||||
|
||||
expect(helperSource).toContain("export function hasActiveInitSubagent(");
|
||||
expect(helperSource).toContain("export function gatherGitContext()");
|
||||
expect(helperSource).toContain("export function gatherInitGitContext()");
|
||||
expect(helperSource).toContain("export function buildShallowInitPrompt(");
|
||||
expect(helperSource).toContain("export function buildInitMessage(");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user