Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/agents/agent-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface CreateAdapterOptions {
sessionId: string;
worktreePath: string;
additionalDirs?: string[];
extraArgs?: string[];
}

export function createAdapter(options: CreateAdapterOptions): ClaudeAdapter {
Expand All @@ -15,6 +16,7 @@ export function createAdapter(options: CreateAdapterOptions): ClaudeAdapter {
sessionId: options.sessionId,
worktreePath: options.worktreePath,
additionalDirs: options.additionalDirs,
extraArgs: options.extraArgs,
});
default:
throw new Error(`No adapter available for agent type: ${options.agentType}`);
Expand Down
37 changes: 37 additions & 0 deletions src/main/agents/claude-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ describe("ClaudeAdapter", () => {
expect(args).not.toContain("--resume");
expect(args).not.toContain("--continue");
});

it("appends extraArgs to the argument list", () => {
const adapterWithExtra = new ClaudeAdapter({
sessionId: "test-session-1",
worktreePath: "/tmp/worktree",
extraArgs: ["--model", "claude-opus-4-5", "--max-turns", "10"],
});
const args = adapterWithExtra.buildStartArgs("Hello");
expect(args).toContain("--model");
expect(args).toContain("claude-opus-4-5");
expect(args).toContain("--max-turns");
expect(args).toContain("10");
// extraArgs should come after the standard args
const modelIndex = args.indexOf("--model");
const verboseIndex = args.indexOf("--verbose");
expect(modelIndex).toBeGreaterThan(verboseIndex);
});

it("works without extraArgs (empty by default)", () => {
const args = adapter.buildStartArgs("Hello");
expect(args).not.toContain("--model");
});
});

describe("buildResumeArgs", () => {
Expand All @@ -67,6 +89,21 @@ describe("ClaudeAdapter", () => {
const args = adapter.buildResumeArgs("Continue");
expect(args).not.toContain("--session-id");
});

it("appends extraArgs to the resume argument list", () => {
const adapterWithExtra = new ClaudeAdapter({
sessionId: "test-session-1",
worktreePath: "/tmp/worktree",
extraArgs: ["--model", "claude-opus-4-5"],
});
adapterWithExtra.setAgentSessionId("claude-session-abc");
const args = adapterWithExtra.buildResumeArgs("Continue");
expect(args).toContain("--model");
expect(args).toContain("claude-opus-4-5");
const modelIndex = args.indexOf("--model");
const resumeIndex = args.indexOf("--resume");
expect(modelIndex).toBeGreaterThan(resumeIndex);
});
});

describe("parseLine", () => {
Expand Down
5 changes: 5 additions & 0 deletions src/main/agents/claude-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,29 @@ interface ClaudeAdapterOptions {
sessionId: string;
worktreePath: string;
additionalDirs?: string[];
extraArgs?: string[];
}

export class ClaudeAdapter {
private sessionId: string;
private worktreePath: string;
private agentSessionId: string | null = null;
private additionalDirs: string[];
private extraArgs: string[];

constructor(options: ClaudeAdapterOptions) {
this.sessionId = options.sessionId;
this.worktreePath = options.worktreePath;
this.additionalDirs = options.additionalDirs ?? [];
this.extraArgs = options.extraArgs ?? [];
}

buildStartArgs(prompt: string): string[] {
const args = ["-p", prompt, "--output-format", "stream-json", "--verbose", "--session-id", this.sessionId];
for (const dir of this.additionalDirs) {
args.push("--add-dir", dir);
}
args.push(...this.extraArgs);
return args;
}

Expand All @@ -34,6 +38,7 @@ export class ClaudeAdapter {
for (const dir of this.additionalDirs) {
args.push("--add-dir", dir);
}
args.push(...this.extraArgs);
return args;
}

Expand Down
52 changes: 52 additions & 0 deletions src/main/db/connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,56 @@ describe("createDatabase", () => {
db.exec("SELECT 1 FROM messages LIMIT 1");
}).not.toThrow();
});

it("migration v5 adds binary_name and extra_args columns to sessions", () => {
const dbPath = path.join(tempDir, "migrate-v5.db");
db = createDatabase(dbPath);
// Both columns should be queryable on a fresh database
expect(() => {
db.exec("SELECT binary_name, extra_args FROM sessions LIMIT 1");
}).not.toThrow();
});

it("migration v5 does not break existing rows (columns default to null)", () => {
db = createDatabase(":memory:");
// Insert a repo first (required FK)
db.exec("INSERT INTO repos (path, name) VALUES ('/repo', 'repo')");
db.exec(
"INSERT INTO sessions (id, repo_path, worktree_path, agent_type, name, sort_order) VALUES ('s1', '/repo', '/repo', 'claude', 'test', 0)",
);
const row = db.prepare("SELECT binary_name, extra_args FROM sessions WHERE id = 's1'").get() as {
binary_name: string | null;
extra_args: string | null;
};
expect(row.binary_name).toBeNull();
expect(row.extra_args).toBeNull();
});

it("migration v6 adds preset_name column to sessions", () => {
db = createDatabase(":memory:");
expect(() => {
db.exec("SELECT preset_name FROM sessions LIMIT 1");
}).not.toThrow();
});

it("migration v7 adds env_vars column to sessions", () => {
db = createDatabase(":memory:");
expect(() => {
db.exec("SELECT env_vars FROM sessions LIMIT 1");
}).not.toThrow();
});

it("all profile columns default to null for new rows", () => {
db = createDatabase(":memory:");
db.exec("INSERT INTO repos (path, name) VALUES ('/repo', 'repo')");
db.exec(
"INSERT INTO sessions (id, repo_path, worktree_path, agent_type, name, sort_order) VALUES ('s1', '/repo', '/repo', 'claude', 'test', 0)",
);
const row = db.prepare("SELECT preset_name, env_vars FROM sessions WHERE id = 's1'").get() as {
preset_name: string | null;
env_vars: string | null;
};
expect(row.preset_name).toBeNull();
expect(row.env_vars).toBeNull();
});
});
27 changes: 16 additions & 11 deletions src/main/db/connection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Database from "better-sqlite3";

export const SCHEMA_VERSION = 4;
export const SCHEMA_VERSION = 7;

const SCHEMA = `
CREATE TABLE IF NOT EXISTS repos (
Expand Down Expand Up @@ -41,10 +41,13 @@ const SCHEMA = `
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
`;

const MIGRATIONS: Record<number, string> = {
2: "ALTER TABLE messages ADD COLUMN thinking TEXT",
3: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0",
4: "ALTER TABLE sessions ADD COLUMN branch_name TEXT",
const MIGRATIONS: Record<number, string[]> = {
2: ["ALTER TABLE messages ADD COLUMN thinking TEXT"],
3: ["ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"],
4: ["ALTER TABLE sessions ADD COLUMN branch_name TEXT"],
5: ["ALTER TABLE sessions ADD COLUMN binary_name TEXT", "ALTER TABLE sessions ADD COLUMN extra_args TEXT"],
6: ["ALTER TABLE sessions ADD COLUMN preset_name TEXT"],
7: ["ALTER TABLE sessions ADD COLUMN env_vars TEXT"],
};

export function createDatabase(dbPath: string): Database.Database {
Expand All @@ -58,12 +61,14 @@ export function createDatabase(dbPath: string): Database.Database {
// Run migrations for existing databases
const currentVersion = (db.pragma("user_version", { simple: true }) as number) ?? 0;
for (let version = currentVersion + 1; version <= SCHEMA_VERSION; version++) {
const migration = MIGRATIONS[version];
if (migration) {
try {
db.exec(migration);
} catch {
// Column may already exist if schema was created fresh
const statements = MIGRATIONS[version];
if (statements) {
for (const statement of statements) {
try {
db.exec(statement);
} catch {
// Column may already exist if schema was created fresh
}
}
}
}
Expand Down
70 changes: 70 additions & 0 deletions src/main/db/sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,76 @@ describe("createSession", () => {
});
});

describe("createSession with binaryName and extraArgs", () => {
it("stores and returns binaryName when provided", () => {
const session = createSession(db, {
repoPath: "/Users/dan/project",
worktreePath: "/Users/dan/project",
agentType: "claude",
name: "test",
binaryName: "claude-work",
});
expect(session.binaryName).toBe("claude-work");
});

it("stores and returns extraArgs when provided", () => {
const session = createSession(db, {
repoPath: "/Users/dan/project",
worktreePath: "/Users/dan/project",
agentType: "claude",
name: "test",
extraArgs: "--model claude-opus-4-5 --max-turns 10",
});
expect(session.extraArgs).toBe("--model claude-opus-4-5 --max-turns 10");
});

it("defaults binaryName and extraArgs to null when not provided", () => {
const session = createSession(db, {
repoPath: "/Users/dan/project",
worktreePath: "/Users/dan/project",
agentType: "claude",
name: "test",
});
expect(session.binaryName).toBeNull();
expect(session.extraArgs).toBeNull();
});
});

describe("createSession with profileName and envVars", () => {
it("stores and returns profileName when provided", () => {
const session = createSession(db, {
repoPath: "/Users/dan/project",
worktreePath: "/Users/dan/project",
agentType: "claude",
name: "test",
profileName: "home",
});
expect(session.profileName).toBe("home");
});

it("stores and returns envVars when provided", () => {
const session = createSession(db, {
repoPath: "/Users/dan/project",
worktreePath: "/Users/dan/project",
agentType: "claude",
name: "test",
envVars: "CLAUDE_CONFIG_DIR=/Users/dan/.claude-home\nANTHROPIC_MODEL=claude-opus-4-5",
});
expect(session.envVars).toBe("CLAUDE_CONFIG_DIR=/Users/dan/.claude-home\nANTHROPIC_MODEL=claude-opus-4-5");
});

it("defaults profileName and envVars to null when not provided", () => {
const session = createSession(db, {
repoPath: "/Users/dan/project",
worktreePath: "/Users/dan/project",
agentType: "claude",
name: "test",
});
expect(session.profileName).toBeNull();
expect(session.envVars).toBeNull();
});
});

describe("createSession with branchName", () => {
it("stores and returns branchName", () => {
const session = createSession(db, {
Expand Down
18 changes: 17 additions & 1 deletion src/main/db/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface SessionRow {
created_at: string;
last_active_at: string;
sort_order: number;
binary_name: string | null;
extra_args: string | null;
preset_name: string | null;
env_vars: string | null;
}

function rowToSession(row: SessionRow): SessionInfo {
Expand All @@ -28,6 +32,10 @@ function rowToSession(row: SessionRow): SessionInfo {
name: row.name,
createdAt: row.created_at,
lastActiveAt: row.last_active_at,
binaryName: row.binary_name,
extraArgs: row.extra_args,
profileName: row.preset_name,
envVars: row.env_vars,
};
}

Expand All @@ -37,6 +45,10 @@ interface CreateSessionParams {
branchName?: string | null;
agentType: AgentType;
name: string;
binaryName?: string | null;
extraArgs?: string | null;
profileName?: string | null;
envVars?: string | null;
}

export function createSession(db: Database.Database, params: CreateSessionParams): SessionInfo {
Expand All @@ -45,7 +57,7 @@ export function createSession(db: Database.Database, params: CreateSessionParams
db.prepare("SELECT COALESCE(MAX(sort_order), -1) as max_order FROM sessions").get() as { max_order: number }
).max_order;
db.prepare(
"INSERT INTO sessions (id, repo_path, worktree_path, branch_name, agent_type, name, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO sessions (id, repo_path, worktree_path, branch_name, agent_type, name, sort_order, binary_name, extra_args, preset_name, env_vars) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
).run(
id,
params.repoPath,
Expand All @@ -54,6 +66,10 @@ export function createSession(db: Database.Database, params: CreateSessionParams
params.agentType,
params.name,
maxOrder + 1,
params.binaryName ?? null,
params.extraArgs ?? null,
params.profileName ?? null,
params.envVars ?? null,
);
return getSession(db, id) as SessionInfo;
}
Expand Down
32 changes: 28 additions & 4 deletions src/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,17 @@ export function registerIpcHandlers(options: RegisterHandlersOptions): void {
name?: string;
baseBranch?: string;
fetchFirst?: boolean;
profileId?: string;
},
) => {
const { repoPath, agentType, branchName, name, baseBranch, fetchFirst } = options;
const { repoPath, agentType, branchName, name, baseBranch, fetchFirst, profileId } = options;
const sessionName = name || `Session ${Date.now()}`;
let worktreePath = repoPath;
let resolvedBranch: string | null = null;

const settings = readSettings(settingsPath);

if (branchName) {
const settings = readSettings(settingsPath);
worktreePath = createWorktree({
repoPath,
branchName,
Expand All @@ -139,7 +141,19 @@ export function registerIpcHandlers(options: RegisterHandlersOptions): void {
resolvedBranch = branchName;
}

return createSession(db, { repoPath, worktreePath, branchName: resolvedBranch, agentType, name: sessionName });
const profile = profileId ? settings.commandProfiles?.find((p) => p.id === profileId) : undefined;

return createSession(db, {
repoPath,
worktreePath,
branchName: resolvedBranch,
agentType,
name: sessionName,
binaryName: profile?.executable ?? null,
extraArgs: profile?.extraArgs ?? null,
profileName: profile?.name ?? null,
envVars: profile?.envVars ?? null,
});
},
);

Expand Down Expand Up @@ -201,7 +215,17 @@ export function registerIpcHandlers(options: RegisterHandlersOptions): void {
// Legacy sessions stored "used" as a boolean marker — treat those as new sessions.
const sessionRecord = getSession(db, sessionId);
const storedSessionId = sessionRecord?.agentSessionId === "used" ? null : (sessionRecord?.agentSessionId ?? null);
ptyManager.create(sessionId, agentType, worktreePath, cols, rows, storedSessionId);
ptyManager.create(
sessionId,
agentType,
worktreePath,
cols,
rows,
storedSessionId,
sessionRecord?.binaryName,
sessionRecord?.extraArgs,
sessionRecord?.envVars,
);

// Store the session ID so future launches use --resume
if (!storedSessionId) {
Expand Down
Loading
Loading