claude-kanban 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +450 -64
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +377 -64
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync as existsSync3 } from "fs";
|
|
|
9
9
|
// src/server/services/executor.ts
|
|
10
10
|
import { spawn, execSync } from "child_process";
|
|
11
11
|
import { join as join4 } from "path";
|
|
12
|
-
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync
|
|
12
|
+
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
14
|
|
|
15
15
|
// src/server/services/project.ts
|
|
@@ -134,6 +134,7 @@ function getTaskCounts(projectPath) {
|
|
|
134
134
|
draft: 0,
|
|
135
135
|
ready: 0,
|
|
136
136
|
in_progress: 0,
|
|
137
|
+
in_review: 0,
|
|
137
138
|
completed: 0,
|
|
138
139
|
failed: 0
|
|
139
140
|
};
|
|
@@ -210,6 +211,7 @@ var WORKTREES_DIR = "worktrees";
|
|
|
210
211
|
var TaskExecutor = class extends EventEmitter {
|
|
211
212
|
projectPath;
|
|
212
213
|
runningTasks = /* @__PURE__ */ new Map();
|
|
214
|
+
reviewingTasks = /* @__PURE__ */ new Map();
|
|
213
215
|
afkMode = false;
|
|
214
216
|
afkIteration = 0;
|
|
215
217
|
afkMaxIterations = 0;
|
|
@@ -379,7 +381,6 @@ ${summary}
|
|
|
379
381
|
cwd: this.projectPath,
|
|
380
382
|
stdio: "pipe"
|
|
381
383
|
});
|
|
382
|
-
this.symlinkDependencies(worktreePath);
|
|
383
384
|
console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
384
385
|
return { worktreePath, branchName };
|
|
385
386
|
} catch (error) {
|
|
@@ -387,38 +388,6 @@ ${summary}
|
|
|
387
388
|
return null;
|
|
388
389
|
}
|
|
389
390
|
}
|
|
390
|
-
/**
|
|
391
|
-
* Symlink dependency directories from main project to worktree
|
|
392
|
-
*/
|
|
393
|
-
symlinkDependencies(worktreePath) {
|
|
394
|
-
const depDirs = [
|
|
395
|
-
"node_modules",
|
|
396
|
-
".pnpm",
|
|
397
|
-
// pnpm store
|
|
398
|
-
".yarn",
|
|
399
|
-
// yarn cache
|
|
400
|
-
"vendor",
|
|
401
|
-
// PHP/Ruby deps
|
|
402
|
-
"__pycache__",
|
|
403
|
-
// Python cache
|
|
404
|
-
".venv",
|
|
405
|
-
// Python virtual env
|
|
406
|
-
"venv"
|
|
407
|
-
// Python virtual env
|
|
408
|
-
];
|
|
409
|
-
for (const dir of depDirs) {
|
|
410
|
-
const sourcePath = join4(this.projectPath, dir);
|
|
411
|
-
const targetPath = join4(worktreePath, dir);
|
|
412
|
-
if (existsSync2(sourcePath) && !existsSync2(targetPath)) {
|
|
413
|
-
try {
|
|
414
|
-
symlinkSync(sourcePath, targetPath, "junction");
|
|
415
|
-
console.log(`[executor] Symlinked ${dir} to worktree`);
|
|
416
|
-
} catch (error) {
|
|
417
|
-
console.log(`[executor] Could not symlink ${dir}:`, error);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
391
|
/**
|
|
423
392
|
* Remove a git worktree
|
|
424
393
|
*/
|
|
@@ -504,7 +473,7 @@ ${summary}
|
|
|
504
473
|
/**
|
|
505
474
|
* Build the prompt for a specific task
|
|
506
475
|
*/
|
|
507
|
-
buildTaskPrompt(task, config) {
|
|
476
|
+
buildTaskPrompt(task, config, worktreeInfo) {
|
|
508
477
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
509
478
|
const prdPath = join4(kanbanDir, "prd.json");
|
|
510
479
|
const progressPath = join4(kanbanDir, "progress.txt");
|
|
@@ -518,9 +487,15 @@ ${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
|
|
|
518
487
|
if (config.project.testCommand) {
|
|
519
488
|
verifySteps.push(`Run tests: ${config.project.testCommand}`);
|
|
520
489
|
}
|
|
521
|
-
const verifySection = verifySteps.length > 0 ? `
|
|
490
|
+
const verifySection = verifySteps.length > 0 ? `3. Verify your work:
|
|
522
491
|
${verifySteps.map((s) => ` - ${s}`).join("\n")}
|
|
523
492
|
|
|
493
|
+
` : "";
|
|
494
|
+
const worktreeSection = worktreeInfo ? `## ENVIRONMENT
|
|
495
|
+
You are running in an isolated git worktree on branch "${worktreeInfo.branchName}".
|
|
496
|
+
This is a fresh checkout - dependencies (node_modules, vendor, etc.) are NOT installed.
|
|
497
|
+
Before running any build/test commands, install dependencies first (e.g., npm install, composer install, pip install).
|
|
498
|
+
|
|
524
499
|
` : "";
|
|
525
500
|
return `You are an AI coding agent. Complete the following task:
|
|
526
501
|
|
|
@@ -532,22 +507,24 @@ Priority: ${task.priority}
|
|
|
532
507
|
${task.description}
|
|
533
508
|
${stepsText}
|
|
534
509
|
|
|
535
|
-
## INSTRUCTIONS
|
|
536
|
-
1.
|
|
510
|
+
${worktreeSection}## INSTRUCTIONS
|
|
511
|
+
1. If dependencies are not installed, install them first.
|
|
512
|
+
|
|
513
|
+
2. Implement this task as described above.
|
|
537
514
|
|
|
538
|
-
${verifySection}${verifySteps.length > 0 ? "
|
|
515
|
+
${verifySection}${verifySteps.length > 0 ? "4" : "3"}. When complete, update the task in ${prdPath}:
|
|
539
516
|
- Find the task with id "${task.id}"
|
|
540
517
|
- Set "passes": true
|
|
541
|
-
|
|
518
|
+
(Note: Do NOT change the status - the system will move it to review)
|
|
542
519
|
|
|
543
|
-
${verifySteps.length > 0 ? "
|
|
520
|
+
${verifySteps.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
|
|
544
521
|
- What you implemented and files changed
|
|
545
522
|
- Key decisions made and why
|
|
546
523
|
- Gotchas, edge cases, or tricky parts discovered
|
|
547
524
|
- Useful patterns or approaches that worked well
|
|
548
525
|
- Anything a future agent should know about this area of the codebase
|
|
549
526
|
|
|
550
|
-
${verifySteps.length > 0 ? "
|
|
527
|
+
${verifySteps.length > 0 ? "6" : "5"}. Make a git commit with a descriptive message.
|
|
551
528
|
|
|
552
529
|
Focus only on this task. When successfully complete, output: <promise>COMPLETE</promise>`;
|
|
553
530
|
}
|
|
@@ -571,7 +548,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
571
548
|
const startedAt = /* @__PURE__ */ new Date();
|
|
572
549
|
const worktreeInfo = this.createWorktree(taskId);
|
|
573
550
|
const executionPath = worktreeInfo?.worktreePath || this.projectPath;
|
|
574
|
-
const prompt = this.buildTaskPrompt(task, config);
|
|
551
|
+
const prompt = this.buildTaskPrompt(task, config, worktreeInfo);
|
|
575
552
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
576
553
|
const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
|
|
577
554
|
writeFileSync4(promptFile, prompt);
|
|
@@ -717,7 +694,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
717
694
|
}, timeoutMs);
|
|
718
695
|
}
|
|
719
696
|
/**
|
|
720
|
-
* Handle task completion
|
|
697
|
+
* Handle task completion - moves to in_review, keeps worktree alive
|
|
721
698
|
*/
|
|
722
699
|
handleTaskComplete(taskId, exitCode, startedAt) {
|
|
723
700
|
const runningTask = this.runningTasks.get(taskId);
|
|
@@ -729,31 +706,25 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
729
706
|
const task = getTaskById(this.projectPath, taskId);
|
|
730
707
|
if (isComplete || exitCode === 0) {
|
|
731
708
|
if (runningTask.worktreePath && runningTask.branchName) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
709
|
+
this.reviewingTasks.set(taskId, {
|
|
710
|
+
worktreePath: runningTask.worktreePath,
|
|
711
|
+
branchName: runningTask.branchName,
|
|
712
|
+
startedAt,
|
|
713
|
+
endedAt
|
|
714
|
+
});
|
|
715
|
+
console.log(`[executor] Task ${taskId} ready for review at ${runningTask.worktreePath}`);
|
|
739
716
|
}
|
|
740
717
|
updateTask(this.projectPath, taskId, {
|
|
741
|
-
status: "
|
|
718
|
+
status: "in_review",
|
|
742
719
|
passes: true
|
|
743
720
|
});
|
|
744
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
745
|
-
startedAt: startedAt.toISOString(),
|
|
746
|
-
endedAt: endedAt.toISOString(),
|
|
747
|
-
status: "completed",
|
|
748
|
-
duration
|
|
749
|
-
});
|
|
750
721
|
logTaskExecution(this.projectPath, {
|
|
751
722
|
taskId,
|
|
752
723
|
taskTitle: task?.title || "Unknown",
|
|
753
|
-
status: "
|
|
724
|
+
status: "in_review",
|
|
754
725
|
duration
|
|
755
726
|
});
|
|
756
|
-
this.emit("task:completed", { taskId, duration });
|
|
727
|
+
this.emit("task:completed", { taskId, duration, status: "in_review" });
|
|
757
728
|
this.afkTasksCompleted++;
|
|
758
729
|
} else {
|
|
759
730
|
if (runningTask.worktreePath) {
|
|
@@ -785,6 +756,151 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
785
756
|
this.continueAFKMode();
|
|
786
757
|
}
|
|
787
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Get info about a task in review
|
|
761
|
+
*/
|
|
762
|
+
getReviewingTask(taskId) {
|
|
763
|
+
return this.reviewingTasks.get(taskId);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Merge a reviewed task into the base branch
|
|
767
|
+
*/
|
|
768
|
+
mergeTask(taskId) {
|
|
769
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
770
|
+
if (!reviewInfo) {
|
|
771
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
772
|
+
}
|
|
773
|
+
const merged = this.mergeWorktreeBranch(taskId);
|
|
774
|
+
if (!merged) {
|
|
775
|
+
return { success: false, error: "Merge failed - may have conflicts" };
|
|
776
|
+
}
|
|
777
|
+
this.removeWorktree(taskId);
|
|
778
|
+
this.reviewingTasks.delete(taskId);
|
|
779
|
+
updateTask(this.projectPath, taskId, {
|
|
780
|
+
status: "completed"
|
|
781
|
+
});
|
|
782
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
783
|
+
startedAt: reviewInfo.startedAt.toISOString(),
|
|
784
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
785
|
+
status: "completed",
|
|
786
|
+
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
787
|
+
});
|
|
788
|
+
console.log(`[executor] Task ${taskId} merged successfully`);
|
|
789
|
+
return { success: true };
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Create a PR for a reviewed task
|
|
793
|
+
*/
|
|
794
|
+
createPR(taskId) {
|
|
795
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
796
|
+
const task = getTaskById(this.projectPath, taskId);
|
|
797
|
+
if (!reviewInfo) {
|
|
798
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
execSync(`git push -u origin ${reviewInfo.branchName}`, {
|
|
802
|
+
cwd: this.projectPath,
|
|
803
|
+
stdio: "pipe"
|
|
804
|
+
});
|
|
805
|
+
const prTitle = task?.title || `Task ${taskId}`;
|
|
806
|
+
const prBody = task?.description || "";
|
|
807
|
+
const result = execSync(
|
|
808
|
+
`gh pr create --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}" --head ${reviewInfo.branchName}`,
|
|
809
|
+
{
|
|
810
|
+
cwd: this.projectPath,
|
|
811
|
+
encoding: "utf-8"
|
|
812
|
+
}
|
|
813
|
+
);
|
|
814
|
+
const prUrl = result.trim();
|
|
815
|
+
try {
|
|
816
|
+
execSync(`git worktree remove "${reviewInfo.worktreePath}" --force`, {
|
|
817
|
+
cwd: this.projectPath,
|
|
818
|
+
stdio: "pipe"
|
|
819
|
+
});
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
this.reviewingTasks.delete(taskId);
|
|
823
|
+
updateTask(this.projectPath, taskId, {
|
|
824
|
+
status: "completed"
|
|
825
|
+
});
|
|
826
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
827
|
+
startedAt: reviewInfo.startedAt.toISOString(),
|
|
828
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
829
|
+
status: "completed",
|
|
830
|
+
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
831
|
+
});
|
|
832
|
+
console.log(`[executor] PR created for task ${taskId}: ${prUrl}`);
|
|
833
|
+
return { success: true, prUrl };
|
|
834
|
+
} catch (error) {
|
|
835
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
836
|
+
console.error(`[executor] Failed to create PR:`, errorMsg);
|
|
837
|
+
return { success: false, error: errorMsg };
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Discard a reviewed task - delete worktree and return to ready
|
|
842
|
+
*/
|
|
843
|
+
discardTask(taskId) {
|
|
844
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
845
|
+
if (!reviewInfo) {
|
|
846
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
847
|
+
}
|
|
848
|
+
this.removeWorktree(taskId);
|
|
849
|
+
this.reviewingTasks.delete(taskId);
|
|
850
|
+
updateTask(this.projectPath, taskId, {
|
|
851
|
+
status: "ready",
|
|
852
|
+
passes: false
|
|
853
|
+
});
|
|
854
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
855
|
+
startedAt: reviewInfo.startedAt.toISOString(),
|
|
856
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
857
|
+
status: "discarded",
|
|
858
|
+
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
859
|
+
});
|
|
860
|
+
console.log(`[executor] Task ${taskId} discarded, returned to ready`);
|
|
861
|
+
return { success: true };
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Get diff for a task in review
|
|
865
|
+
*/
|
|
866
|
+
getTaskDiff(taskId) {
|
|
867
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
868
|
+
if (!reviewInfo) {
|
|
869
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
870
|
+
}
|
|
871
|
+
try {
|
|
872
|
+
const diff = execSync(`git diff main...${reviewInfo.branchName}`, {
|
|
873
|
+
cwd: this.projectPath,
|
|
874
|
+
encoding: "utf-8",
|
|
875
|
+
maxBuffer: 10 * 1024 * 1024
|
|
876
|
+
// 10MB buffer for large diffs
|
|
877
|
+
});
|
|
878
|
+
return { success: true, diff };
|
|
879
|
+
} catch (error) {
|
|
880
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
881
|
+
return { success: false, error: errorMsg };
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Get list of changed files for a task in review
|
|
886
|
+
*/
|
|
887
|
+
getTaskChangedFiles(taskId) {
|
|
888
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
889
|
+
if (!reviewInfo) {
|
|
890
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const result = execSync(`git diff --name-only main...${reviewInfo.branchName}`, {
|
|
894
|
+
cwd: this.projectPath,
|
|
895
|
+
encoding: "utf-8"
|
|
896
|
+
});
|
|
897
|
+
const files = result.trim().split("\n").filter((f) => f);
|
|
898
|
+
return { success: true, files };
|
|
899
|
+
} catch (error) {
|
|
900
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
901
|
+
return { success: false, error: errorMsg };
|
|
902
|
+
}
|
|
903
|
+
}
|
|
788
904
|
/**
|
|
789
905
|
* Cancel a running task
|
|
790
906
|
*/
|
|
@@ -1451,6 +1567,90 @@ async function createServer(projectPath, port) {
|
|
|
1451
1567
|
res.status(500).json({ error: String(error) });
|
|
1452
1568
|
}
|
|
1453
1569
|
});
|
|
1570
|
+
app.post("/api/tasks/:id/merge", (req, res) => {
|
|
1571
|
+
try {
|
|
1572
|
+
const result = executor.mergeTask(req.params.id);
|
|
1573
|
+
if (!result.success) {
|
|
1574
|
+
res.status(400).json({ error: result.error });
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const task = getTaskById(projectPath, req.params.id);
|
|
1578
|
+
if (task) {
|
|
1579
|
+
io.emit("task:updated", task);
|
|
1580
|
+
}
|
|
1581
|
+
res.json({ success: true });
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
res.status(500).json({ error: String(error) });
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
app.post("/api/tasks/:id/create-pr", (req, res) => {
|
|
1587
|
+
try {
|
|
1588
|
+
const result = executor.createPR(req.params.id);
|
|
1589
|
+
if (!result.success) {
|
|
1590
|
+
res.status(400).json({ error: result.error });
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const task = getTaskById(projectPath, req.params.id);
|
|
1594
|
+
if (task) {
|
|
1595
|
+
io.emit("task:updated", task);
|
|
1596
|
+
}
|
|
1597
|
+
res.json({ success: true, prUrl: result.prUrl });
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
res.status(500).json({ error: String(error) });
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
app.post("/api/tasks/:id/discard", (req, res) => {
|
|
1603
|
+
try {
|
|
1604
|
+
const result = executor.discardTask(req.params.id);
|
|
1605
|
+
if (!result.success) {
|
|
1606
|
+
res.status(400).json({ error: result.error });
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const task = getTaskById(projectPath, req.params.id);
|
|
1610
|
+
if (task) {
|
|
1611
|
+
io.emit("task:updated", task);
|
|
1612
|
+
}
|
|
1613
|
+
res.json({ success: true });
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
res.status(500).json({ error: String(error) });
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
app.get("/api/tasks/:id/diff", (req, res) => {
|
|
1619
|
+
try {
|
|
1620
|
+
const result = executor.getTaskDiff(req.params.id);
|
|
1621
|
+
if (!result.success) {
|
|
1622
|
+
res.status(400).json({ error: result.error });
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
res.json({ diff: result.diff });
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
res.status(500).json({ error: String(error) });
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
app.get("/api/tasks/:id/changed-files", (req, res) => {
|
|
1631
|
+
try {
|
|
1632
|
+
const result = executor.getTaskChangedFiles(req.params.id);
|
|
1633
|
+
if (!result.success) {
|
|
1634
|
+
res.status(400).json({ error: result.error });
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
res.json({ files: result.files });
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
res.status(500).json({ error: String(error) });
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
app.get("/api/tasks/:id/review-info", (req, res) => {
|
|
1643
|
+
try {
|
|
1644
|
+
const info = executor.getReviewingTask(req.params.id);
|
|
1645
|
+
if (!info) {
|
|
1646
|
+
res.status(404).json({ error: "Task not in review" });
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
res.json(info);
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
res.status(500).json({ error: String(error) });
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1454
1654
|
app.get("/api/tasks/:id/logs", (req, res) => {
|
|
1455
1655
|
try {
|
|
1456
1656
|
const logs = executor.getTaskLog(req.params.id);
|
|
@@ -1760,6 +1960,7 @@ function getClientHTML() {
|
|
|
1760
1960
|
.status-dot-draft { background: #a3a3a3; }
|
|
1761
1961
|
.status-dot-ready { background: #3b82f6; }
|
|
1762
1962
|
.status-dot-in_progress { background: #f97316; }
|
|
1963
|
+
.status-dot-in_review { background: #8b5cf6; }
|
|
1763
1964
|
.status-dot-completed { background: #22c55e; }
|
|
1764
1965
|
.status-dot-failed { background: #ef4444; }
|
|
1765
1966
|
|
|
@@ -1969,6 +2170,7 @@ function getClientHTML() {
|
|
|
1969
2170
|
.status-badge-draft { background: #f5f5f5; color: #737373; }
|
|
1970
2171
|
.status-badge-ready { background: #eff6ff; color: #3b82f6; }
|
|
1971
2172
|
.status-badge-in_progress { background: #fff7ed; color: #f97316; }
|
|
2173
|
+
.status-badge-in_review { background: #f5f3ff; color: #8b5cf6; }
|
|
1972
2174
|
.status-badge-completed { background: #f0fdf4; color: #22c55e; }
|
|
1973
2175
|
.status-badge-failed { background: #fef2f2; color: #ef4444; }
|
|
1974
2176
|
|
|
@@ -2294,11 +2496,16 @@ socket.on('task:output', ({ taskId, line }) => {
|
|
|
2294
2496
|
}
|
|
2295
2497
|
});
|
|
2296
2498
|
|
|
2297
|
-
socket.on('task:completed', ({ taskId }) => {
|
|
2499
|
+
socket.on('task:completed', ({ taskId, status }) => {
|
|
2298
2500
|
state.running = state.running.filter(id => id !== taskId);
|
|
2299
2501
|
const task = state.tasks.find(t => t.id === taskId);
|
|
2300
|
-
if (task) {
|
|
2301
|
-
|
|
2502
|
+
if (task) {
|
|
2503
|
+
// Use status from server (could be 'in_review' or 'completed')
|
|
2504
|
+
task.status = status || 'in_review';
|
|
2505
|
+
task.passes = true;
|
|
2506
|
+
}
|
|
2507
|
+
const statusMsg = (status === 'in_review') ? 'ready for review' : 'completed';
|
|
2508
|
+
showToast('Task ' + statusMsg + ': ' + (task?.title || taskId), 'success');
|
|
2302
2509
|
render();
|
|
2303
2510
|
});
|
|
2304
2511
|
|
|
@@ -2372,6 +2579,53 @@ async function retryTask(id) {
|
|
|
2372
2579
|
});
|
|
2373
2580
|
}
|
|
2374
2581
|
|
|
2582
|
+
async function mergeTask(id) {
|
|
2583
|
+
const res = await fetch('/api/tasks/' + id + '/merge', { method: 'POST' });
|
|
2584
|
+
const data = await res.json();
|
|
2585
|
+
if (!res.ok) {
|
|
2586
|
+
throw new Error(data.error || 'Merge failed');
|
|
2587
|
+
}
|
|
2588
|
+
return data;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
async function createPR(id) {
|
|
2592
|
+
const res = await fetch('/api/tasks/' + id + '/create-pr', { method: 'POST' });
|
|
2593
|
+
const data = await res.json();
|
|
2594
|
+
if (!res.ok) {
|
|
2595
|
+
throw new Error(data.error || 'PR creation failed');
|
|
2596
|
+
}
|
|
2597
|
+
return data;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
async function discardTask(id) {
|
|
2601
|
+
const res = await fetch('/api/tasks/' + id + '/discard', { method: 'POST' });
|
|
2602
|
+
const data = await res.json();
|
|
2603
|
+
if (!res.ok) {
|
|
2604
|
+
throw new Error(data.error || 'Discard failed');
|
|
2605
|
+
}
|
|
2606
|
+
return data;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
async function getReviewInfo(id) {
|
|
2610
|
+
const res = await fetch('/api/tasks/' + id + '/review-info');
|
|
2611
|
+
const data = await res.json();
|
|
2612
|
+
if (!res.ok) {
|
|
2613
|
+
throw new Error(data.error || 'Failed to get review info');
|
|
2614
|
+
}
|
|
2615
|
+
return data;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
async function openInVSCode(id) {
|
|
2619
|
+
try {
|
|
2620
|
+
const info = await getReviewInfo(id);
|
|
2621
|
+
// Open VS Code at the worktree path
|
|
2622
|
+
window.open('vscode://file/' + encodeURIComponent(info.worktreePath), '_blank');
|
|
2623
|
+
showToast('Opening in VS Code...', 'info');
|
|
2624
|
+
} catch (e) {
|
|
2625
|
+
showToast('Failed to open in VS Code: ' + e.message, 'error');
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2375
2629
|
async function generateTask(prompt) {
|
|
2376
2630
|
state.aiGenerating = true;
|
|
2377
2631
|
render();
|
|
@@ -2575,6 +2829,7 @@ function renderColumn(status, title, tasks) {
|
|
|
2575
2829
|
draft: 'To Do',
|
|
2576
2830
|
ready: 'Ready',
|
|
2577
2831
|
in_progress: 'In Progress',
|
|
2832
|
+
in_review: 'In Review',
|
|
2578
2833
|
completed: 'Done',
|
|
2579
2834
|
failed: 'Failed'
|
|
2580
2835
|
};
|
|
@@ -2940,7 +3195,7 @@ function renderSidePanel() {
|
|
|
2940
3195
|
</div>
|
|
2941
3196
|
|
|
2942
3197
|
<!-- Action Buttons -->
|
|
2943
|
-
<div class="px-5 py-4 border-b border-canvas-200 flex gap-2 flex-shrink-0">
|
|
3198
|
+
<div class="px-5 py-4 border-b border-canvas-200 flex flex-wrap gap-2 flex-shrink-0">
|
|
2944
3199
|
\${task.status === 'draft' ? \`
|
|
2945
3200
|
<button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-primary px-4 py-2 text-sm">
|
|
2946
3201
|
\u2192 Move to Ready
|
|
@@ -2956,6 +3211,20 @@ function renderSidePanel() {
|
|
|
2956
3211
|
\u23F9 Stop Attempt
|
|
2957
3212
|
</button>
|
|
2958
3213
|
\` : ''}
|
|
3214
|
+
\${task.status === 'in_review' ? \`
|
|
3215
|
+
<button onclick="handleMerge('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3216
|
+
\u2713 Merge
|
|
3217
|
+
</button>
|
|
3218
|
+
<button onclick="handleCreatePR('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
|
|
3219
|
+
\u21E1 Create PR
|
|
3220
|
+
</button>
|
|
3221
|
+
<button onclick="openInVSCode('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
|
|
3222
|
+
\u{1F4C2} Open in VS Code
|
|
3223
|
+
</button>
|
|
3224
|
+
<button onclick="handleDiscard('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
|
|
3225
|
+
\u2715 Discard
|
|
3226
|
+
</button>
|
|
3227
|
+
\` : ''}
|
|
2959
3228
|
\${task.status === 'failed' ? \`
|
|
2960
3229
|
<button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
2961
3230
|
\u21BB Retry
|
|
@@ -3156,6 +3425,41 @@ function handleStartAFK() {
|
|
|
3156
3425
|
render();
|
|
3157
3426
|
}
|
|
3158
3427
|
|
|
3428
|
+
async function handleMerge(taskId) {
|
|
3429
|
+
if (!confirm('Merge this task into the main branch?')) return;
|
|
3430
|
+
try {
|
|
3431
|
+
await mergeTask(taskId);
|
|
3432
|
+
showToast('Task merged successfully!', 'success');
|
|
3433
|
+
closeSidePanel();
|
|
3434
|
+
} catch (e) {
|
|
3435
|
+
showToast('Merge failed: ' + e.message, 'error');
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
async function handleCreatePR(taskId) {
|
|
3440
|
+
try {
|
|
3441
|
+
const result = await createPR(taskId);
|
|
3442
|
+
showToast('PR created successfully!', 'success');
|
|
3443
|
+
if (result.prUrl) {
|
|
3444
|
+
window.open(result.prUrl, '_blank');
|
|
3445
|
+
}
|
|
3446
|
+
closeSidePanel();
|
|
3447
|
+
} catch (e) {
|
|
3448
|
+
showToast('PR creation failed: ' + e.message, 'error');
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
async function handleDiscard(taskId) {
|
|
3453
|
+
if (!confirm('Discard this work? The task will return to Ready status.')) return;
|
|
3454
|
+
try {
|
|
3455
|
+
await discardTask(taskId);
|
|
3456
|
+
showToast('Task discarded, returned to Ready', 'warning');
|
|
3457
|
+
closeSidePanel();
|
|
3458
|
+
} catch (e) {
|
|
3459
|
+
showToast('Discard failed: ' + e.message, 'error');
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3159
3463
|
// Filter tasks based on search query
|
|
3160
3464
|
function filterTasks(tasks) {
|
|
3161
3465
|
if (!state.searchQuery) return tasks;
|
|
@@ -3217,6 +3521,7 @@ function render() {
|
|
|
3217
3521
|
\${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
|
|
3218
3522
|
\${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
|
|
3219
3523
|
\${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
|
|
3524
|
+
\${renderColumn('in_review', 'In Review', filterTasks(state.tasks))}
|
|
3220
3525
|
\${renderColumn('completed', 'Done', filterTasks(state.tasks))}
|
|
3221
3526
|
\${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
|
|
3222
3527
|
</div>
|
|
@@ -3281,6 +3586,14 @@ window.closeSidePanel = closeSidePanel;
|
|
|
3281
3586
|
window.showTaskMenu = showTaskMenu;
|
|
3282
3587
|
window.filterTasks = filterTasks;
|
|
3283
3588
|
window.scrollSidePanelLog = scrollSidePanelLog;
|
|
3589
|
+
window.mergeTask = mergeTask;
|
|
3590
|
+
window.createPR = createPR;
|
|
3591
|
+
window.discardTask = discardTask;
|
|
3592
|
+
window.handleMerge = handleMerge;
|
|
3593
|
+
window.handleCreatePR = handleCreatePR;
|
|
3594
|
+
window.handleDiscard = handleDiscard;
|
|
3595
|
+
window.openInVSCode = openInVSCode;
|
|
3596
|
+
window.getReviewInfo = getReviewInfo;
|
|
3284
3597
|
|
|
3285
3598
|
// Keyboard shortcuts
|
|
3286
3599
|
document.addEventListener('keydown', (e) => {
|