Skip to content
Closed
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 drizzle/0010_add_db_target_to_tasks.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `tasks` ADD COLUMN `db_target` text;
--> statement-breakpoint
Comment on lines +1 to +2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manually created migration file

This migration file was created manually without running drizzle-kit generate. The drizzle/meta/_journal.json has no entry for 0010_add_db_target_to_tasks (it stops at idx 9 / 0009_add_ssh_support), and there is no corresponding snapshot file in drizzle/meta/.

Per the project's critical rules in CLAUDE.md: "NEVER modify drizzle/meta/ or numbered migration files — always use drizzle-kit generate". While this rule focuses on modifying existing files, the intended workflow is to always generate migrations via drizzle-kit generate after modifying schema.ts. A hand-written migration without the journal/snapshot metadata will cause Drizzle's migration runner to skip this file, meaning the db_target column will never be added to existing databases.

Please run pnpm exec drizzle-kit generate after the schema change to produce a properly tracked migration.

Context Used: Context from dashboard - CLAUDE.md (source)

1 change: 1 addition & 0 deletions src/main/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const tasks = sqliteTable(
agentId: text('agent_id'),
metadata: text('metadata'),
useWorktree: integer('use_worktree').notNull().default(1),
dbTarget: text('db_target'), // JSON string: { url?: string, name?: string, profile?: string }
archivedAt: text('archived_at'), // null = active, timestamp = archived
createdAt: text('created_at')
.notNull()
Expand Down
2 changes: 2 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
taskName: string;
projectId: string;
baseRef?: string;
dbTarget?: string | null;
}) => ipcRenderer.invoke('worktree:create', args),
worktreeList: (args: { projectPath: string }) => ipcRenderer.invoke('worktree:list', args),
worktreeRemove: (args: {
Expand Down Expand Up @@ -714,6 +715,7 @@ export interface ElectronAPI {
taskName: string;
projectId: string;
baseRef?: string;
dbTarget?: string | null;
}) => Promise<{ success: boolean; worktree?: any; error?: string }>;
worktreeList: (args: {
projectPath: string;
Expand Down
60 changes: 37 additions & 23 deletions src/main/services/DatabaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface Task {
agentId?: string | null;
metadata?: any;
useWorktree?: boolean;
dbTarget?: string | null;
archivedAt?: string | null;
createdAt: string;
updatedAt: string;
Expand Down Expand Up @@ -271,34 +272,46 @@ export class DatabaseService {
: task.metadata
? JSON.stringify(task.metadata)
: null;
const hasDbTargetField = Object.prototype.hasOwnProperty.call(task, 'dbTarget');
const dbTargetValue = hasDbTargetField ? (task.dbTarget ?? null) : undefined;
const { db } = await getDrizzleClient();
const insertValues: Record<string, any> = {
id: task.id,
projectId: task.projectId,
name: task.name,
branch: task.branch,
path: task.path,
status: task.status,
agentId: task.agentId ?? null,
metadata: metadataValue,
useWorktree: task.useWorktree !== false ? 1 : 0,
updatedAt: sql`CURRENT_TIMESTAMP`,
};
if (hasDbTargetField) {
insertValues.dbTarget = dbTargetValue;
}

const updateSet: Record<string, any> = {
projectId: task.projectId,
name: task.name,
branch: task.branch,
path: task.path,
status: task.status,
agentId: task.agentId ?? null,
metadata: metadataValue,
useWorktree: task.useWorktree !== false ? 1 : 0,
updatedAt: sql`CURRENT_TIMESTAMP`,
};
if (hasDbTargetField) {
updateSet.dbTarget = dbTargetValue;
}

await db
.insert(tasksTable)
.values({
id: task.id,
projectId: task.projectId,
name: task.name,
branch: task.branch,
path: task.path,
status: task.status,
agentId: task.agentId ?? null,
metadata: metadataValue,
useWorktree: task.useWorktree !== false ? 1 : 0,
updatedAt: sql`CURRENT_TIMESTAMP`,
})
.values(insertValues)
.onConflictDoUpdate({
target: tasksTable.id,
set: {
projectId: task.projectId,
name: task.name,
branch: task.branch,
path: task.path,
status: task.status,
agentId: task.agentId ?? null,
metadata: metadataValue,
useWorktree: task.useWorktree !== false ? 1 : 0,
updatedAt: sql`CURRENT_TIMESTAMP`,
},
set: updateSet,
});
}

Expand Down Expand Up @@ -905,6 +918,7 @@ export class DatabaseService {
? this.parseTaskMetadata(row.metadata, row.id)
: null,
useWorktree: row.useWorktree === 1,
dbTarget: row.dbTarget ?? null,
archivedAt: row.archivedAt ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down
3 changes: 3 additions & 0 deletions src/main/services/TaskLifecycleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { getTaskEnvVars } from '@shared/task/envVars';
import { log } from '../lib/logger';
import { execFile } from 'node:child_process';
import { databaseService } from './DatabaseService';

const execFileAsync = promisify(execFile);

Expand Down Expand Up @@ -107,13 +108,15 @@ class TaskLifecycleService extends EventEmitter {
): Promise<NodeJS.ProcessEnv> {
const defaultBranch = await this.resolveDefaultBranch(projectPath);
const taskName = path.basename(taskPath) || taskId;
const task = await databaseService.getTaskById(taskId);
const taskEnv = getTaskEnvVars({
taskId,
taskName,
taskPath,
projectPath,
defaultBranch,
portSeed: taskPath || taskId,
dbTarget: task?.dbTarget ?? null,
});
return { ...process.env, ...taskEnv };
}
Expand Down
17 changes: 15 additions & 2 deletions src/main/services/ptyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ const AGENT_ENV_VARS = [
'OPENAI_BASE_URL',
];

const TASK_DB_ENV_VARS = new Set([
'DATABASE_URL',
'DB_URL',
'DB_NAME',
'DATABASE_NAME',
'DB_PROFILE',
'DATABASE_PROFILE',
]);

function shouldForwardTaskEnvVar(key: string): boolean {
return key.startsWith('EMDASH_') || TASK_DB_ENV_VARS.has(key);
}

type PtyRecord = {
id: string;
proc: IPty;
Expand Down Expand Up @@ -387,7 +400,7 @@ export function startSshPty(options: {

if (env) {
for (const [key, value] of Object.entries(env)) {
if (!key.startsWith('EMDASH_')) continue;
if (!shouldForwardTaskEnvVar(key)) continue;
if (typeof value === 'string') {
useEnv[key] = value;
}
Expand Down Expand Up @@ -514,7 +527,7 @@ export function startDirectPty(options: {

if (env) {
for (const [key, value] of Object.entries(env)) {
if (!key.startsWith('EMDASH_')) continue;
if (!shouldForwardTaskEnvVar(key)) continue;
if (typeof value === 'string') {
useEnv[key] = value;
}
Expand Down
4 changes: 3 additions & 1 deletion src/main/services/worktreeIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function registerWorktreeIpc(): void {
taskName: string;
projectId: string;
baseRef?: string;
dbTarget?: string | null;
}
) => {
try {
Expand Down Expand Up @@ -103,6 +104,7 @@ export function registerWorktreeIpc(): void {
projectId: project.id,
status: 'active' as const,
createdAt: new Date().toISOString(),
dbTarget: args.dbTarget ?? null,
};
return { success: true, worktree };
}
Expand All @@ -113,7 +115,7 @@ export function registerWorktreeIpc(): void {
args.projectId,
args.baseRef
);
return { success: true, worktree };
return { success: true, worktree: { ...worktree, dbTarget: args.dbTarget ?? null } };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dbTarget not persisted to task in worktreeIpc

For the local (non-remote) worktree creation path, dbTarget is spread onto the returned worktree object as an ad-hoc property ({ ...worktree, dbTarget: args.dbTarget ?? null }). The WorktreeInfo interface returned by worktreeService.createWorktree() does not include dbTarget, so this relies on the untyped worktree?: any return type in the IPC definition.

This works at runtime because taskCreationService.ts reads worktree.dbTarget from the response, but it's fragile — if WorktreeInfo is ever typed more strictly at the IPC boundary, this will silently break. Consider either adding dbTarget to WorktreeInfo or documenting that this field is only passed through via the IPC response (not a worktree property).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

} catch (error) {
console.error('Failed to create worktree:', error);
return { success: false, error: (error as Error).message };
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ const AppContent: React.FC = () => {
linkedJiraIssue: JiraIssueSummary | null = null,
autoApprove?: boolean,
useWorktree: boolean = true,
baseRef?: string
baseRef?: string,
dbTarget?: string | null
) => {
if (!projectMgmt.selectedProject) return;
await createTask(
Expand All @@ -288,6 +289,7 @@ const AppContent: React.FC = () => {
autoApprove,
useWorktree,
baseRef,
dbTarget: dbTarget ?? null,
},
{
selectedProject: projectMgmt.selectedProject,
Expand Down
22 changes: 21 additions & 1 deletion src/renderer/components/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,23 @@ const ChatInterface: React.FC<Props> = ({
taskPath: task.path,
projectPath,
defaultBranch: defaultBranch || undefined,
dbTarget: task.dbTarget,
});
}, [task.id, task.name, task.path, projectPath, defaultBranch]);
}, [task.id, task.name, task.path, projectPath, defaultBranch, task.dbTarget]);

const dbTargetLabel = useMemo(() => {
const raw = (task.dbTarget ?? '').trim();
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
const profile = typeof parsed.profile === 'string' ? parsed.profile.trim() : '';
const name = typeof parsed.name === 'string' ? parsed.name.trim() : '';
return profile || name || 'custom';
}
} catch {}
return 'custom';
}, [task.dbTarget]);

const installedAgents = useMemo(
() =>
Expand Down Expand Up @@ -881,6 +896,11 @@ const ChatInterface: React.FC<Props> = ({
Auto-approve
</Badge>
)}
{dbTargetLabel && (
<Badge variant="outline" title="Active DB target for this task">
DB: {dbTargetLabel}
</Badge>
)}
</div>
{(() => {
if (isAgentInstalled === false) {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/MultiAgentTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ const MultiAgentTask: React.FC<Props> = ({
projectPath,
defaultBranch: defaultBranch || undefined,
portSeed: key,
dbTarget: task.dbTarget,
})
);
}
return envMap;
}, [variants, task.id, task.name, projectPath, defaultBranch]);
}, [variants, task.id, task.name, projectPath, defaultBranch, task.dbTarget]);

// Auto-scroll to bottom when this task becomes active
const { scrollToBottom } = useAutoScrollOnTaskSwitch(true, task.id);
Expand Down
25 changes: 25 additions & 0 deletions src/renderer/components/TaskAdvancedSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ interface TaskAdvancedSettingsProps {
useWorktree: boolean;
onUseWorktreeChange: (value: boolean) => void;

// DB target (app/project DB, not Emdash DB)
dbTarget: string;
onDbTargetChange: (value: string) => void;

// Auto-approve
autoApprove: boolean;
onAutoApproveChange: (value: boolean) => void;
Expand Down Expand Up @@ -62,6 +66,8 @@ export const TaskAdvancedSettings: React.FC<TaskAdvancedSettingsProps> = ({
projectPath,
useWorktree,
onUseWorktreeChange,
dbTarget,
onDbTargetChange,
autoApprove,
onAutoApproveChange,
hasAutoApproveSupport,
Expand Down Expand Up @@ -249,6 +255,25 @@ export const TaskAdvancedSettings: React.FC<TaskAdvancedSettingsProps> = ({
</div>
</div>

<div className="grid grid-cols-[128px_1fr] items-start gap-4">
<Label htmlFor="db-target" className="pt-2">
DB target
</Label>
<div className="min-w-0 flex-1">
<Textarea
id="db-target"
value={dbTarget}
onChange={(e) => onDbTargetChange(e.target.value)}
placeholder="DATABASE_URL or JSON {\"url\":\"...\",\"name\":\"...\",\"profile\":\"...\"}"
className="resize-none font-mono text-xs"
rows={2}
/>
<div className="mt-1 text-[11px] text-muted-foreground">
Passed to terminals/agents as DATABASE_URL/DB_URL and optional name/profile.
</div>
</div>
</div>

{hasAutoApproveSupport ? (
<div className="flex items-center gap-4">
<Label className="w-32 shrink-0">Auto-approve</Label>
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/components/TaskModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ interface TaskModalProps {
linkedJiraIssue?: JiraIssueSummary | null,
autoApprove?: boolean,
useWorktree?: boolean,
baseRef?: string
baseRef?: string,
dbTarget?: string | null
) => void;
projectName: string;
defaultBranch: string;
Expand Down Expand Up @@ -74,6 +75,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
const [selectedJiraIssue, setSelectedJiraIssue] = useState<JiraIssueSummary | null>(null);
const [autoApprove, setAutoApprove] = useState(false);
const [useWorktree, setUseWorktree] = useState(true);
const [dbTarget, setDbTarget] = useState('');

// Branch selection state - sync with defaultBranch unless user manually changed it
const [selectedBranch, setSelectedBranch] = useState(defaultBranch);
Expand Down Expand Up @@ -156,6 +158,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
setSelectedJiraIssue(null);
setAutoApprove(false);
setUseWorktree(true);
setDbTarget('');
userHasTypedRef.current = false;
autoNameInitializedRef.current = false;
customNameTrackedRef.current = false;
Expand Down Expand Up @@ -242,7 +245,8 @@ const TaskModal: React.FC<TaskModalProps> = ({
selectedJiraIssue,
hasAutoApproveSupport ? autoApprove : false,
useWorktree,
selectedBranch
selectedBranch,
dbTarget.trim() ? dbTarget.trim() : null
);
} catch (error) {
console.error('Failed to create task:', error);
Expand Down Expand Up @@ -317,6 +321,8 @@ const TaskModal: React.FC<TaskModalProps> = ({
projectPath={projectPath}
useWorktree={useWorktree}
onUseWorktreeChange={setUseWorktree}
dbTarget={dbTarget}
onDbTargetChange={setDbTarget}
autoApprove={autoApprove}
onAutoApproveChange={setAutoApprove}
hasAutoApproveSupport={hasAutoApproveSupport}
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/TaskTerminalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ const TaskTerminalPanelComponent: React.FC<Props> = ({
projectPath,
defaultBranch,
portSeed,
dbTarget: task.dbTarget,
});
}, [task?.id, task?.name, task?.path, projectPath, defaultBranch, portSeed]);
}, [task?.id, task?.name, task?.path, projectPath, defaultBranch, portSeed, task?.dbTarget]);

useEffect(() => {
activeTaskIdRef.current = task?.id ?? null;
Expand Down
Loading