Skip to content

Commit 3df4aed

Browse files
committed
feat: integrate console task execution and enhance execution flow
- Add Console Store and components for structured activity logging - Improve task execution handlers with better dependency and cycle detection - Enhance auto-execution logic for dependent tasks - Update local agent session and terminal services - Refactor task repository and history tracking
1 parent b7c1e66 commit 3df4aed

29 files changed

Lines changed: 3328 additions & 255 deletions

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
engine-strict=true
22

3+

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- **Smart Operators**: Reusable AI personas with drag-and-drop assignment
1515
- **Project Memory System**: Automatic context management with Curator
1616
- **MCP Server Integration**: Filesystem, Shell, Git, HTTP Fetch, and third-party tools
17-
- **Local Agent Support**: Antigravity and Claude-Code integrations
17+
- **Local Agent Support**: Claude-Code integrations
1818
- **Marketplace**: Share and discover workflows, operators, and templates
1919
- **Four Task Types**: AI tasks, Script tasks (JavaScript), Input tasks, Output tasks
2020
- **Webhooks**: Task completion notifications

electron/main/database/repositories/task-history-repository.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ export class TaskHistoryRepository {
3939
return result[0]!;
4040
}
4141

42+
async findByProject(projectId: number, limit?: number): Promise<TaskHistory[]> {
43+
let query = db
44+
.select()
45+
.from(taskHistory)
46+
.where(eq(taskHistory.taskProjectId, projectId))
47+
.orderBy(desc(taskHistory.createdAt), desc(taskHistory.id));
48+
49+
if (limit) {
50+
query = query.limit(limit) as typeof query;
51+
}
52+
53+
return await query;
54+
}
55+
4256
/**
4357
* Find all history entries for a task
4458
*/

electron/main/database/repositories/task-repository.ts

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,25 @@ export class TaskRepository {
7979
const [result] = await db
8080
.select()
8181
.from(tasks)
82-
.where(
83-
and(
84-
eq(tasks.projectId, projectId),
85-
eq(tasks.projectSequence, projectSequence),
86-
isNull(tasks.deletedAt)
87-
)
88-
)
82+
.where(and(eq(tasks.projectId, projectId), eq(tasks.projectSequence, projectSequence)))
8983
.limit(1);
9084

85+
if (result?.executionResult?.content) {
86+
const safeResult = {
87+
...result,
88+
executionResult: { ...result.executionResult, content: '(truncated)' },
89+
};
90+
console.log(
91+
`[TaskRepo] findByKey(${projectId}, ${projectSequence}) raw result:`,
92+
safeResult
93+
);
94+
} else {
95+
console.log(
96+
`[TaskRepo] findByKey(${projectId}, ${projectSequence}) raw result:`,
97+
result
98+
);
99+
}
100+
91101
return result;
92102
}
93103

@@ -244,14 +254,110 @@ export class TaskRepository {
244254
/**
245255
* Soft delete task by composite key
246256
*/
257+
/**
258+
* Soft delete task by composite key and clean up dependencies
259+
*/
247260
async deleteByKey(projectId: number, projectSequence: number): Promise<void> {
248-
await db
249-
.update(tasks)
250-
.set({
251-
deletedAt: new Date(),
252-
updatedAt: new Date(),
253-
})
254-
.where(and(eq(tasks.projectId, projectId), eq(tasks.projectSequence, projectSequence)));
261+
// 1. Find tasks that depend on this task
262+
const dependentTasks = await db
263+
.select()
264+
.from(tasks)
265+
.where(
266+
and(
267+
eq(tasks.projectId, projectId),
268+
isNull(tasks.deletedAt)
269+
// We can't easily filter JSON in SQLite efficiently for all cases,
270+
// so we'll fetch project tasks and filter in memory or rely on broader fetch.
271+
// Given findByProject usage pattern, fetching all active tasks for project is safe/standard here.
272+
)
273+
);
274+
275+
const tasksToUpdate_TriggerConfig: Task[] = [];
276+
const tasksToUpdate_Dependencies: Task[] = [];
277+
278+
for (const task of dependentTasks) {
279+
// Check triggerConfig
280+
if (
281+
task.triggerConfig &&
282+
task.triggerConfig.dependsOn &&
283+
Array.isArray(task.triggerConfig.dependsOn.taskIds)
284+
) {
285+
if (task.triggerConfig.dependsOn.taskIds.includes(projectSequence)) {
286+
tasksToUpdate_TriggerConfig.push(task);
287+
}
288+
}
289+
290+
// Check dependencies (legacy or simple array)
291+
if (Array.isArray(task.dependencies) && task.dependencies.includes(projectSequence)) {
292+
tasksToUpdate_Dependencies.push(task);
293+
}
294+
}
295+
296+
// 2. Update dependent tasks
297+
await db.transaction(async (tx) => {
298+
// Update triggerConfigs
299+
for (const task of tasksToUpdate_TriggerConfig) {
300+
if (!task.triggerConfig?.dependsOn?.taskIds) continue;
301+
302+
const newTaskIds = task.triggerConfig.dependsOn.taskIds.filter(
303+
(id) => id !== projectSequence
304+
);
305+
const newTriggerConfig = {
306+
...task.triggerConfig,
307+
dependsOn: {
308+
...task.triggerConfig.dependsOn,
309+
taskIds: newTaskIds,
310+
},
311+
};
312+
313+
// If no dependencies left, remove dependsOn entirely?
314+
// Or keep empty array? Keeping empty array might be safer or user might want to add more.
315+
// User request says "remove from dependency".
316+
// If taskIds becomes empty, the task might auto-trigger or never trigger depending on logic.
317+
// Usually empty dependency means "no dependency".
318+
319+
await tx
320+
.update(tasks)
321+
.set({
322+
triggerConfig: newTriggerConfig,
323+
updatedAt: new Date(),
324+
})
325+
.where(
326+
and(
327+
eq(tasks.projectId, projectId),
328+
eq(tasks.projectSequence, task.projectSequence)
329+
)
330+
);
331+
}
332+
333+
// Update dependencies column
334+
for (const task of tasksToUpdate_Dependencies) {
335+
const newDeps = (task.dependencies || []).filter((id) => id !== projectSequence);
336+
await tx
337+
.update(tasks)
338+
.set({
339+
dependencies: newDeps,
340+
updatedAt: new Date(),
341+
})
342+
.where(
343+
and(
344+
eq(tasks.projectId, projectId),
345+
eq(tasks.projectSequence, task.projectSequence)
346+
)
347+
);
348+
}
349+
350+
// 3. Perform the soft delete
351+
await tx
352+
.update(tasks)
353+
.set({
354+
deletedAt: new Date(),
355+
updatedAt: new Date(),
356+
})
357+
.where(
358+
and(eq(tasks.projectId, projectId), eq(tasks.projectSequence, projectSequence))
359+
);
360+
});
255361
}
256362

257363
/**

electron/main/index.ts

Lines changed: 133 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,9 @@ async function registerIpcHandlers(): Promise<void> {
256256
}
257257

258258
// Append to description if sensible
259+
// MOVED TO projectGuidelines
259260
if (context.guidelines) {
260-
data.description =
261-
(data.description || '') +
262-
`\n\n[Imported Guidelines]\n${context.guidelines}`;
261+
(data as any).projectGuidelines = context.guidelines;
263262
}
264263
}
265264
} catch (scanErr) {
@@ -466,14 +465,9 @@ async function registerIpcHandlers(): Promise<void> {
466465
};
467466
}
468467

469-
// Append guidelines to description if present
468+
// Update projectGuidelines if present
470469
if (context.guidelines) {
471-
// Check if guidelines are not already in description to avoid duplication
472-
if (!project.description?.includes('[Imported Guidelines]')) {
473-
updateData.description =
474-
(project.description || '') +
475-
`\n\n[Imported Guidelines]\n${context.guidelines}`;
476-
}
470+
updateData.projectGuidelines = context.guidelines;
477471
}
478472

479473
if (Object.keys(updateData).length > 0) {
@@ -510,6 +504,135 @@ async function registerIpcHandlers(): Promise<void> {
510504
}
511505
});
512506

507+
// Detect existing agent context for project recovery
508+
ipcMain.handle('projects:detect-context', async (_event, projectId: number) => {
509+
try {
510+
const project = await projectRepo.findById(projectId);
511+
if (!project || !project.baseDevFolder) {
512+
return { hasContext: false };
513+
}
514+
515+
const folder = project.baseDevFolder;
516+
const fs = await import('fs/promises');
517+
const path = await import('path');
518+
519+
// Helper to check if path exists
520+
const exists = async (p: string) => {
521+
try {
522+
await fs.access(p);
523+
return true;
524+
} catch {
525+
return false;
526+
}
527+
};
528+
529+
// Helper to read file safely
530+
const readFileSafe = async (p: string): Promise<string | null> => {
531+
try {
532+
return await fs.readFile(p, 'utf-8');
533+
} catch {
534+
return null;
535+
}
536+
};
537+
538+
// Detect various context files
539+
const hasClaudeMd = await exists(path.join(folder, 'CLAUDE.md'));
540+
const hasGeminiDir = await exists(path.join(folder, '.gemini'));
541+
const hasCodexDir = await exists(path.join(folder, '.codex'));
542+
const hasGit = await exists(path.join(folder, '.git'));
543+
const hasGeminiMd = await exists(path.join(folder, 'GEMINI.md'));
544+
const hasAgentsMd = await exists(path.join(folder, 'AGENTS.md'));
545+
const hasTaskMd = await exists(path.join(folder, 'task.md'));
546+
const hasPlanMd = await exists(path.join(folder, 'implementation_plan.md'));
547+
const hasReadme = await exists(path.join(folder, 'README.md'));
548+
549+
// Read Claude CLAUDE.md content
550+
const claudeMdContent = hasClaudeMd
551+
? await readFileSafe(path.join(folder, 'CLAUDE.md'))
552+
: null;
553+
554+
// Read Gemini GEMINI.md content
555+
const geminiMdContent = hasGeminiMd
556+
? await readFileSafe(path.join(folder, 'GEMINI.md'))
557+
: null;
558+
559+
// Read Codex AGENTS.md content (OpenAI Codex uses AGENTS.md)
560+
const agentsMdContent = hasAgentsMd
561+
? await readFileSafe(path.join(folder, 'AGENTS.md'))
562+
: null;
563+
564+
// Read task.md for Gemini CLI progress context
565+
const taskMdContent = hasTaskMd
566+
? await readFileSafe(path.join(folder, 'task.md'))
567+
: null;
568+
569+
// Read implementation_plan.md for Gemini CLI progress
570+
const planMdContent = hasPlanMd
571+
? await readFileSafe(path.join(folder, 'implementation_plan.md'))
572+
: null;
573+
574+
// Check .gemini/task.md as alternative location
575+
let geminiTaskMdContent: string | null = null;
576+
if (hasGeminiDir) {
577+
geminiTaskMdContent = await readFileSafe(path.join(folder, '.gemini', 'task.md'));
578+
}
579+
580+
// Get recent git commits if git exists
581+
let recentCommits: string[] = [];
582+
if (hasGit) {
583+
try {
584+
const { exec } = await import('child_process');
585+
const { promisify } = await import('util');
586+
const execAsync = promisify(exec);
587+
const { stdout } = await execAsync('git log --oneline -n 5', { cwd: folder });
588+
recentCommits = stdout
589+
.trim()
590+
.split('\n')
591+
.filter((l: string) => l);
592+
} catch (e) {
593+
console.warn('Could not get git history:', e);
594+
}
595+
}
596+
597+
const hasContext =
598+
hasClaudeMd ||
599+
hasGeminiDir ||
600+
hasCodexDir ||
601+
hasGeminiMd ||
602+
hasAgentsMd ||
603+
hasTaskMd ||
604+
hasPlanMd;
605+
606+
return {
607+
hasContext,
608+
// Detection flags
609+
hasClaudeMd,
610+
hasGeminiDir,
611+
hasCodexDir,
612+
hasGit,
613+
hasGeminiMd,
614+
hasAgentsMd,
615+
hasTaskMd,
616+
hasPlanMd,
617+
hasReadme,
618+
// Content
619+
claudeMdContent,
620+
geminiMdContent,
621+
agentsMdContent,
622+
taskMdContent,
623+
planMdContent,
624+
geminiTaskMdContent,
625+
recentCommits,
626+
};
627+
} catch (error) {
628+
console.error('Error detecting context:', error);
629+
return {
630+
hasContext: false,
631+
error: error instanceof Error ? error.message : String(error),
632+
};
633+
}
634+
});
635+
513636
// ========================================
514637
// Task IPC Handlers
515638
// ========================================

electron/main/ipc/task-execution-handlers.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,10 @@ async function processInputSubmission(
873873
);
874874
}
875875

876+
// IMPORTANT: Clean up activeExecutions BEFORE checking dependents
877+
// Without this, dependents see this task as "Active: true" and skip execution
878+
activeExecutions.delete(getTaskKey(projectId, sequence));
879+
876880
// Trigger dependents
877881
await checkAndExecuteDependentTasks(projectId, sequence, task as Task, options);
878882

@@ -3821,6 +3825,22 @@ export function registerTaskExecutionHandlers(_mainWindow: BrowserWindow | null)
38213825
}
38223826
);
38233827

3828+
/**
3829+
* Get recent execution history for a project
3830+
*/
3831+
ipcMain.handle('taskExecution:getRecent', async (_, projectId: number, limit: number = 50) => {
3832+
try {
3833+
const history = await taskHistoryRepository.findByProject(projectId, limit);
3834+
return { success: true, history };
3835+
} catch (error) {
3836+
console.error('[TaskExecution] Failed to fetch recent history:', error);
3837+
return {
3838+
success: false,
3839+
error: error instanceof Error ? error.message : String(error),
3840+
};
3841+
}
3842+
});
3843+
38243844
console.log('Task execution IPC handlers registered');
38253845

38263846
// ========================================

0 commit comments

Comments
 (0)