claude-kanban 0.4.0 → 0.5.1
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 +644 -738
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +638 -732
- package/dist/server/index.js.map +1 -1
- package/package.json +10 -2
package/dist/bin/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import open from "open";
|
|
7
7
|
import { existsSync as existsSync4 } from "fs";
|
|
8
|
-
import { execSync
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
9
|
import { createInterface } from "readline";
|
|
10
10
|
import { join as join6 } from "path";
|
|
11
11
|
|
|
@@ -18,9 +18,9 @@ import { fileURLToPath } from "url";
|
|
|
18
18
|
import { existsSync as existsSync3 } from "fs";
|
|
19
19
|
|
|
20
20
|
// src/server/services/executor.ts
|
|
21
|
-
import { spawn
|
|
21
|
+
import { spawn } from "child_process";
|
|
22
22
|
import { join as join4 } from "path";
|
|
23
|
-
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4
|
|
23
|
+
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4 } from "fs";
|
|
24
24
|
import { EventEmitter } from "events";
|
|
25
25
|
|
|
26
26
|
// src/server/services/project.ts
|
|
@@ -458,7 +458,6 @@ function getTaskCounts(projectPath) {
|
|
|
458
458
|
draft: 0,
|
|
459
459
|
ready: 0,
|
|
460
460
|
in_progress: 0,
|
|
461
|
-
in_review: 0,
|
|
462
461
|
completed: 0,
|
|
463
462
|
failed: 0
|
|
464
463
|
};
|
|
@@ -531,11 +530,10 @@ function getRecentProgress(projectPath, lines = 100) {
|
|
|
531
530
|
// src/server/services/executor.ts
|
|
532
531
|
var KANBAN_DIR4 = ".claude-kanban";
|
|
533
532
|
var LOGS_DIR = "logs";
|
|
534
|
-
var WORKTREES_DIR = "worktrees";
|
|
535
533
|
var TaskExecutor = class extends EventEmitter {
|
|
536
534
|
projectPath;
|
|
537
|
-
|
|
538
|
-
|
|
535
|
+
runningTask = null;
|
|
536
|
+
planningSession = null;
|
|
539
537
|
afkMode = false;
|
|
540
538
|
afkIteration = 0;
|
|
541
539
|
afkMaxIterations = 0;
|
|
@@ -641,186 +639,71 @@ ${summary}
|
|
|
641
639
|
}
|
|
642
640
|
}
|
|
643
641
|
/**
|
|
644
|
-
* Check if
|
|
642
|
+
* Check if a task is currently running
|
|
645
643
|
*/
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
execSync("git rev-parse --is-inside-work-tree", {
|
|
649
|
-
cwd: this.projectPath,
|
|
650
|
-
stdio: "pipe"
|
|
651
|
-
});
|
|
652
|
-
return true;
|
|
653
|
-
} catch {
|
|
654
|
-
return false;
|
|
655
|
-
}
|
|
644
|
+
isRunning() {
|
|
645
|
+
return this.runningTask !== null;
|
|
656
646
|
}
|
|
657
647
|
/**
|
|
658
|
-
* Get
|
|
648
|
+
* Get the currently running task ID
|
|
659
649
|
*/
|
|
660
|
-
|
|
661
|
-
return
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Get worktree path for a task
|
|
665
|
-
*/
|
|
666
|
-
getWorktreePath(taskId) {
|
|
667
|
-
return join4(this.getWorktreesDir(), taskId);
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Get branch name for a task
|
|
671
|
-
*/
|
|
672
|
-
getBranchName(taskId) {
|
|
673
|
-
return `task/${taskId}`;
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Create a git worktree for isolated task execution
|
|
677
|
-
*/
|
|
678
|
-
createWorktree(taskId) {
|
|
679
|
-
if (!this.isGitRepo()) {
|
|
680
|
-
console.log("[executor] Not a git repo, skipping worktree creation");
|
|
681
|
-
return null;
|
|
682
|
-
}
|
|
683
|
-
const worktreePath = this.getWorktreePath(taskId);
|
|
684
|
-
const branchName = this.getBranchName(taskId);
|
|
685
|
-
try {
|
|
686
|
-
const worktreesDir = this.getWorktreesDir();
|
|
687
|
-
if (!existsSync2(worktreesDir)) {
|
|
688
|
-
mkdirSync2(worktreesDir, { recursive: true });
|
|
689
|
-
}
|
|
690
|
-
if (existsSync2(worktreePath)) {
|
|
691
|
-
this.removeWorktree(taskId);
|
|
692
|
-
}
|
|
693
|
-
try {
|
|
694
|
-
execSync(`git rev-parse --verify ${branchName}`, {
|
|
695
|
-
cwd: this.projectPath,
|
|
696
|
-
stdio: "pipe"
|
|
697
|
-
});
|
|
698
|
-
execSync(`git branch -D ${branchName}`, {
|
|
699
|
-
cwd: this.projectPath,
|
|
700
|
-
stdio: "pipe"
|
|
701
|
-
});
|
|
702
|
-
} catch {
|
|
703
|
-
}
|
|
704
|
-
execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
705
|
-
cwd: this.projectPath,
|
|
706
|
-
stdio: "pipe"
|
|
707
|
-
});
|
|
708
|
-
console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
709
|
-
return { worktreePath, branchName };
|
|
710
|
-
} catch (error) {
|
|
711
|
-
console.error("[executor] Failed to create worktree:", error);
|
|
712
|
-
return null;
|
|
713
|
-
}
|
|
650
|
+
getRunningTaskId() {
|
|
651
|
+
return this.runningTask?.taskId || null;
|
|
714
652
|
}
|
|
715
653
|
/**
|
|
716
|
-
*
|
|
717
|
-
*/
|
|
718
|
-
removeWorktree(taskId) {
|
|
719
|
-
const worktreePath = this.getWorktreePath(taskId);
|
|
720
|
-
const branchName = this.getBranchName(taskId);
|
|
721
|
-
try {
|
|
722
|
-
if (existsSync2(worktreePath)) {
|
|
723
|
-
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
724
|
-
cwd: this.projectPath,
|
|
725
|
-
stdio: "pipe"
|
|
726
|
-
});
|
|
727
|
-
console.log(`[executor] Removed worktree at ${worktreePath}`);
|
|
728
|
-
}
|
|
729
|
-
} catch (error) {
|
|
730
|
-
console.error("[executor] Failed to remove worktree via git:", error);
|
|
731
|
-
try {
|
|
732
|
-
if (existsSync2(worktreePath)) {
|
|
733
|
-
rmSync(worktreePath, { recursive: true, force: true });
|
|
734
|
-
execSync("git worktree prune", {
|
|
735
|
-
cwd: this.projectPath,
|
|
736
|
-
stdio: "pipe"
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
} catch {
|
|
740
|
-
console.error("[executor] Failed to force remove worktree directory");
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
try {
|
|
744
|
-
execSync(`git branch -D ${branchName}`, {
|
|
745
|
-
cwd: this.projectPath,
|
|
746
|
-
stdio: "pipe"
|
|
747
|
-
});
|
|
748
|
-
console.log(`[executor] Deleted branch ${branchName}`);
|
|
749
|
-
} catch {
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
/**
|
|
753
|
-
* Merge worktree branch back to main branch
|
|
654
|
+
* Get running task output
|
|
754
655
|
*/
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
759
|
-
cwd: this.projectPath,
|
|
760
|
-
encoding: "utf-8"
|
|
761
|
-
}).trim();
|
|
762
|
-
execSync(`git merge ${branchName} --no-edit`, {
|
|
763
|
-
cwd: this.projectPath,
|
|
764
|
-
stdio: "pipe"
|
|
765
|
-
});
|
|
766
|
-
console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
|
|
767
|
-
return true;
|
|
768
|
-
} catch (error) {
|
|
769
|
-
console.error("[executor] Failed to merge branch:", error);
|
|
770
|
-
return false;
|
|
656
|
+
getTaskOutput(taskId) {
|
|
657
|
+
if (this.runningTask?.taskId === taskId) {
|
|
658
|
+
return this.runningTask.output;
|
|
771
659
|
}
|
|
660
|
+
return void 0;
|
|
772
661
|
}
|
|
773
662
|
/**
|
|
774
|
-
*
|
|
663
|
+
* Check if a planning session is running
|
|
775
664
|
*/
|
|
776
|
-
|
|
777
|
-
return this.
|
|
665
|
+
isPlanning() {
|
|
666
|
+
return this.planningSession !== null;
|
|
778
667
|
}
|
|
779
668
|
/**
|
|
780
|
-
* Check if
|
|
669
|
+
* Check if executor is busy (task or planning running)
|
|
781
670
|
*/
|
|
782
|
-
|
|
783
|
-
return this.
|
|
671
|
+
isBusy() {
|
|
672
|
+
return this.runningTask !== null || this.planningSession !== null;
|
|
784
673
|
}
|
|
785
674
|
/**
|
|
786
|
-
* Get
|
|
675
|
+
* Get planning session output
|
|
787
676
|
*/
|
|
788
|
-
|
|
789
|
-
return
|
|
677
|
+
getPlanningOutput() {
|
|
678
|
+
return this.planningSession?.output;
|
|
790
679
|
}
|
|
791
680
|
/**
|
|
792
|
-
* Get
|
|
681
|
+
* Get planning session goal
|
|
793
682
|
*/
|
|
794
|
-
|
|
795
|
-
return this.
|
|
683
|
+
getPlanningGoal() {
|
|
684
|
+
return this.planningSession?.goal;
|
|
796
685
|
}
|
|
797
686
|
/**
|
|
798
|
-
* Build the prompt for a
|
|
687
|
+
* Build the prompt for a task - simplified Ralph-style
|
|
799
688
|
*/
|
|
800
|
-
buildTaskPrompt(task, config
|
|
689
|
+
buildTaskPrompt(task, config) {
|
|
801
690
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
802
691
|
const prdPath = join4(kanbanDir, "prd.json");
|
|
803
692
|
const progressPath = join4(kanbanDir, "progress.txt");
|
|
804
693
|
const stepsText = task.steps.length > 0 ? `
|
|
805
694
|
Verification steps:
|
|
806
695
|
${task.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}` : "";
|
|
807
|
-
const
|
|
696
|
+
const verifyCommands = [];
|
|
808
697
|
if (config.project.typecheckCommand) {
|
|
809
|
-
|
|
698
|
+
verifyCommands.push(config.project.typecheckCommand);
|
|
810
699
|
}
|
|
811
700
|
if (config.project.testCommand) {
|
|
812
|
-
|
|
701
|
+
verifyCommands.push(config.project.testCommand);
|
|
813
702
|
}
|
|
814
|
-
const verifySection =
|
|
815
|
-
${
|
|
816
|
-
|
|
817
|
-
` : "";
|
|
818
|
-
const worktreeSection = worktreeInfo ? `## ENVIRONMENT
|
|
819
|
-
You are running in an isolated git worktree on branch "${worktreeInfo.branchName}".
|
|
820
|
-
This is a fresh checkout - dependencies (node_modules, vendor, etc.) are NOT installed.
|
|
821
|
-
Before running any build/test commands, install dependencies first (e.g., npm install, composer install, pip install).
|
|
703
|
+
const verifySection = verifyCommands.length > 0 ? `3. Run quality checks:
|
|
704
|
+
${verifyCommands.map((cmd) => ` - ${cmd}`).join("\n")}
|
|
822
705
|
|
|
823
|
-
|
|
706
|
+
4.` : "3.";
|
|
824
707
|
return `You are an AI coding agent. Complete the following task:
|
|
825
708
|
|
|
826
709
|
## TASK
|
|
@@ -831,48 +714,254 @@ Priority: ${task.priority}
|
|
|
831
714
|
${task.description}
|
|
832
715
|
${stepsText}
|
|
833
716
|
|
|
834
|
-
|
|
835
|
-
1. If dependencies are not installed, install them first.
|
|
717
|
+
## INSTRUCTIONS
|
|
836
718
|
|
|
837
|
-
|
|
719
|
+
1. Implement this task as described above.
|
|
838
720
|
|
|
839
|
-
|
|
721
|
+
2. Make sure your changes work correctly.
|
|
722
|
+
|
|
723
|
+
${verifySection} When complete, update the PRD file at ${prdPath}:
|
|
840
724
|
- Find the task with id "${task.id}"
|
|
841
725
|
- Set "passes": true
|
|
842
|
-
|
|
726
|
+
- Set "status": "completed"
|
|
843
727
|
|
|
844
|
-
${
|
|
845
|
-
- What you implemented
|
|
846
|
-
- Key decisions made
|
|
847
|
-
-
|
|
848
|
-
- Useful patterns or approaches that worked well
|
|
849
|
-
- Anything a future agent should know about this area of the codebase
|
|
728
|
+
${verifyCommands.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
|
|
729
|
+
- What you implemented
|
|
730
|
+
- Key decisions made
|
|
731
|
+
- Any gotchas or important notes for future work
|
|
850
732
|
|
|
851
|
-
${
|
|
733
|
+
${verifyCommands.length > 0 ? "6" : "5"}. Commit your changes with a descriptive message.
|
|
852
734
|
|
|
853
|
-
Focus only on this task. When
|
|
735
|
+
Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Build the prompt for a planning session
|
|
739
|
+
*/
|
|
740
|
+
buildPlanningPrompt(goal) {
|
|
741
|
+
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
742
|
+
const prdPath = join4(kanbanDir, "prd.json");
|
|
743
|
+
return `You are an AI coding planner. Your job is to analyze a codebase and break down a user's goal into concrete, actionable tasks.
|
|
744
|
+
|
|
745
|
+
## GOAL
|
|
746
|
+
${goal}
|
|
747
|
+
|
|
748
|
+
## INSTRUCTIONS
|
|
749
|
+
|
|
750
|
+
1. Explore the codebase to understand:
|
|
751
|
+
- Project structure and architecture
|
|
752
|
+
- Existing patterns and conventions
|
|
753
|
+
- Related existing code that you'll build upon
|
|
754
|
+
- What technologies and frameworks are being used
|
|
755
|
+
|
|
756
|
+
2. Break down the goal into 3-8 specific, actionable tasks that can each be completed in a single coding session.
|
|
757
|
+
|
|
758
|
+
3. For each task, determine:
|
|
759
|
+
- Clear, action-oriented title (start with a verb: Add, Create, Implement, Fix, etc.)
|
|
760
|
+
- Detailed description with implementation guidance
|
|
761
|
+
- Category: functional, ui, bug, enhancement, testing, or refactor
|
|
762
|
+
- Priority: low, medium, high, or critical
|
|
763
|
+
- 3-7 verification steps to confirm the task is complete
|
|
764
|
+
|
|
765
|
+
4. Read the current PRD file at ${prdPath}, then update it:
|
|
766
|
+
- Add your new tasks to the "tasks" array
|
|
767
|
+
- Each task must have this structure:
|
|
768
|
+
{
|
|
769
|
+
"id": "task_" + 8 random alphanumeric characters,
|
|
770
|
+
"title": "...",
|
|
771
|
+
"description": "...",
|
|
772
|
+
"category": "functional|ui|bug|enhancement|testing|refactor",
|
|
773
|
+
"priority": "low|medium|high|critical",
|
|
774
|
+
"status": "draft",
|
|
775
|
+
"steps": ["Step 1", "Step 2", ...],
|
|
776
|
+
"passes": false,
|
|
777
|
+
"createdAt": ISO timestamp,
|
|
778
|
+
"updatedAt": ISO timestamp,
|
|
779
|
+
"executionHistory": []
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
5. Commit your changes with message: "Plan: ${goal}"
|
|
783
|
+
|
|
784
|
+
IMPORTANT:
|
|
785
|
+
- Tasks should be ordered logically (dependencies first)
|
|
786
|
+
- Each task should be completable independently
|
|
787
|
+
- Be specific about implementation details
|
|
788
|
+
- Consider edge cases and error handling
|
|
789
|
+
|
|
790
|
+
When done, output: <promise>PLANNING_COMPLETE</promise>`;
|
|
854
791
|
}
|
|
855
792
|
/**
|
|
856
|
-
* Run a
|
|
793
|
+
* Run a planning session to break down a goal into tasks
|
|
794
|
+
*/
|
|
795
|
+
async runPlanningSession(goal) {
|
|
796
|
+
if (this.isBusy()) {
|
|
797
|
+
throw new Error("Another operation is in progress. Wait for it to complete or cancel it first.");
|
|
798
|
+
}
|
|
799
|
+
const config = getConfig(this.projectPath);
|
|
800
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
801
|
+
const prompt = this.buildPlanningPrompt(goal);
|
|
802
|
+
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
803
|
+
const promptFile = join4(kanbanDir, "prompt-planning.txt");
|
|
804
|
+
writeFileSync4(promptFile, prompt);
|
|
805
|
+
const args = [];
|
|
806
|
+
if (config.agent.model) {
|
|
807
|
+
args.push("--model", config.agent.model);
|
|
808
|
+
}
|
|
809
|
+
args.push("--permission-mode", config.agent.permissionMode);
|
|
810
|
+
args.push("-p");
|
|
811
|
+
args.push("--verbose");
|
|
812
|
+
args.push("--output-format", "stream-json");
|
|
813
|
+
args.push(`@${promptFile}`);
|
|
814
|
+
const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
|
|
815
|
+
const fullCommand = `${config.agent.command} ${args.join(" ")}`;
|
|
816
|
+
console.log("[executor] Planning command:", fullCommand);
|
|
817
|
+
console.log("[executor] CWD:", this.projectPath);
|
|
818
|
+
const childProcess = spawn("bash", ["-c", fullCommand], {
|
|
819
|
+
cwd: this.projectPath,
|
|
820
|
+
env: {
|
|
821
|
+
...process.env,
|
|
822
|
+
TERM: "xterm-256color",
|
|
823
|
+
FORCE_COLOR: "0",
|
|
824
|
+
NO_COLOR: "1"
|
|
825
|
+
},
|
|
826
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
827
|
+
});
|
|
828
|
+
this.planningSession = {
|
|
829
|
+
goal,
|
|
830
|
+
process: childProcess,
|
|
831
|
+
startedAt,
|
|
832
|
+
output: []
|
|
833
|
+
};
|
|
834
|
+
const logOutput = (line) => {
|
|
835
|
+
this.planningSession?.output.push(line);
|
|
836
|
+
this.emit("planning:output", { line, lineType: "stdout" });
|
|
837
|
+
};
|
|
838
|
+
this.emit("planning:started", { goal, timestamp: startedAt.toISOString() });
|
|
839
|
+
logOutput(`[claude-kanban] Starting planning session
|
|
840
|
+
`);
|
|
841
|
+
logOutput(`[claude-kanban] Goal: ${goal}
|
|
842
|
+
`);
|
|
843
|
+
logOutput(`[claude-kanban] Command: ${commandDisplay}
|
|
844
|
+
`);
|
|
845
|
+
let stdoutBuffer = "";
|
|
846
|
+
childProcess.stdout?.on("data", (data) => {
|
|
847
|
+
stdoutBuffer += data.toString();
|
|
848
|
+
const lines = stdoutBuffer.split("\n");
|
|
849
|
+
stdoutBuffer = lines.pop() || "";
|
|
850
|
+
for (const line of lines) {
|
|
851
|
+
if (!line.trim()) continue;
|
|
852
|
+
try {
|
|
853
|
+
const json = JSON.parse(line);
|
|
854
|
+
let text = "";
|
|
855
|
+
if (json.type === "assistant" && json.message?.content) {
|
|
856
|
+
for (const block of json.message.content) {
|
|
857
|
+
if (block.type === "text") {
|
|
858
|
+
text += block.text;
|
|
859
|
+
} else if (block.type === "tool_use") {
|
|
860
|
+
text += this.formatToolUse(block.name, block.input);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
} else if (json.type === "content_block_delta" && json.delta?.text) {
|
|
864
|
+
text = json.delta.text;
|
|
865
|
+
} else if (json.type === "result" && json.result) {
|
|
866
|
+
text = `
|
|
867
|
+
[Result: ${json.result}]
|
|
868
|
+
`;
|
|
869
|
+
}
|
|
870
|
+
if (text) {
|
|
871
|
+
logOutput(text);
|
|
872
|
+
}
|
|
873
|
+
} catch {
|
|
874
|
+
const cleanText = line.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
875
|
+
if (cleanText.trim()) {
|
|
876
|
+
logOutput(cleanText + "\n");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
childProcess.stderr?.on("data", (data) => {
|
|
882
|
+
const text = data.toString();
|
|
883
|
+
logOutput(`[stderr] ${text}`);
|
|
884
|
+
});
|
|
885
|
+
childProcess.on("error", (error) => {
|
|
886
|
+
console.log("[executor] Planning spawn error:", error.message);
|
|
887
|
+
this.emit("planning:output", { line: `[claude-kanban] Error: ${error.message}
|
|
888
|
+
`, lineType: "stderr" });
|
|
889
|
+
try {
|
|
890
|
+
unlinkSync(promptFile);
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
this.emit("planning:failed", { error: error.message });
|
|
894
|
+
this.planningSession = null;
|
|
895
|
+
});
|
|
896
|
+
childProcess.on("close", (code, signal) => {
|
|
897
|
+
console.log("[executor] Planning process closed with code:", code, "signal:", signal);
|
|
898
|
+
try {
|
|
899
|
+
unlinkSync(promptFile);
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
logOutput(`[claude-kanban] Planning process exited with code ${code}
|
|
903
|
+
`);
|
|
904
|
+
this.handlePlanningComplete(code);
|
|
905
|
+
});
|
|
906
|
+
const timeoutMs = 15 * 60 * 1e3;
|
|
907
|
+
setTimeout(() => {
|
|
908
|
+
if (this.planningSession) {
|
|
909
|
+
this.cancelPlanning("Planning timeout exceeded");
|
|
910
|
+
}
|
|
911
|
+
}, timeoutMs);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Handle planning session completion
|
|
915
|
+
*/
|
|
916
|
+
handlePlanningComplete(exitCode) {
|
|
917
|
+
if (!this.planningSession) return;
|
|
918
|
+
const output = this.planningSession.output.join("");
|
|
919
|
+
const isComplete = output.includes("<promise>PLANNING_COMPLETE</promise>");
|
|
920
|
+
if (isComplete || exitCode === 0) {
|
|
921
|
+
this.emit("planning:completed", { success: true });
|
|
922
|
+
} else {
|
|
923
|
+
const error = `Planning process exited with code ${exitCode}`;
|
|
924
|
+
this.emit("planning:failed", { error });
|
|
925
|
+
}
|
|
926
|
+
this.planningSession = null;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Cancel the planning session
|
|
930
|
+
*/
|
|
931
|
+
cancelPlanning(reason = "Cancelled by user") {
|
|
932
|
+
if (!this.planningSession) return false;
|
|
933
|
+
const { process: childProcess } = this.planningSession;
|
|
934
|
+
try {
|
|
935
|
+
childProcess.kill("SIGTERM");
|
|
936
|
+
setTimeout(() => {
|
|
937
|
+
try {
|
|
938
|
+
if (!childProcess.killed) {
|
|
939
|
+
childProcess.kill("SIGKILL");
|
|
940
|
+
}
|
|
941
|
+
} catch {
|
|
942
|
+
}
|
|
943
|
+
}, 2e3);
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
this.emit("planning:cancelled", { reason });
|
|
947
|
+
this.planningSession = null;
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Run a task
|
|
857
952
|
*/
|
|
858
953
|
async runTask(taskId) {
|
|
954
|
+
if (this.isBusy()) {
|
|
955
|
+
throw new Error("Another operation is in progress. Wait for it to complete or cancel it first.");
|
|
956
|
+
}
|
|
859
957
|
const config = getConfig(this.projectPath);
|
|
860
958
|
const task = getTaskById(this.projectPath, taskId);
|
|
861
959
|
if (!task) {
|
|
862
960
|
throw new Error(`Task not found: ${taskId}`);
|
|
863
961
|
}
|
|
864
|
-
if (this.isTaskRunning(taskId)) {
|
|
865
|
-
throw new Error(`Task already running: ${taskId}`);
|
|
866
|
-
}
|
|
867
|
-
const maxConcurrent = config.execution.maxConcurrent || 3;
|
|
868
|
-
if (this.getRunningCount() >= maxConcurrent) {
|
|
869
|
-
throw new Error(`Maximum concurrent tasks (${maxConcurrent}) reached`);
|
|
870
|
-
}
|
|
871
962
|
updateTask(this.projectPath, taskId, { status: "in_progress" });
|
|
872
963
|
const startedAt = /* @__PURE__ */ new Date();
|
|
873
|
-
const
|
|
874
|
-
const executionPath = worktreeInfo?.worktreePath || this.projectPath;
|
|
875
|
-
const prompt = this.buildTaskPrompt(task, config, worktreeInfo);
|
|
964
|
+
const prompt = this.buildTaskPrompt(task, config);
|
|
876
965
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
877
966
|
const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
|
|
878
967
|
writeFileSync4(promptFile, prompt);
|
|
@@ -888,48 +977,32 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
888
977
|
const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
|
|
889
978
|
const fullCommand = `${config.agent.command} ${args.join(" ")}`;
|
|
890
979
|
console.log("[executor] Command:", fullCommand);
|
|
891
|
-
console.log("[executor] CWD:",
|
|
892
|
-
if (worktreeInfo) {
|
|
893
|
-
console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
|
|
894
|
-
console.log("[executor] Branch:", worktreeInfo.branchName);
|
|
895
|
-
}
|
|
980
|
+
console.log("[executor] CWD:", this.projectPath);
|
|
896
981
|
const childProcess = spawn("bash", ["-c", fullCommand], {
|
|
897
|
-
cwd:
|
|
982
|
+
cwd: this.projectPath,
|
|
898
983
|
env: {
|
|
899
984
|
...process.env,
|
|
900
985
|
TERM: "xterm-256color",
|
|
901
986
|
FORCE_COLOR: "0",
|
|
902
|
-
// Disable colors to avoid escape codes
|
|
903
987
|
NO_COLOR: "1"
|
|
904
|
-
// Standard way to disable colors
|
|
905
988
|
},
|
|
906
989
|
stdio: ["ignore", "pipe", "pipe"]
|
|
907
|
-
// Close stdin since we don't need interactive input
|
|
908
990
|
});
|
|
909
|
-
|
|
991
|
+
this.runningTask = {
|
|
910
992
|
taskId,
|
|
911
993
|
process: childProcess,
|
|
912
994
|
startedAt,
|
|
913
|
-
output: []
|
|
914
|
-
worktreePath: worktreeInfo?.worktreePath,
|
|
915
|
-
branchName: worktreeInfo?.branchName
|
|
995
|
+
output: []
|
|
916
996
|
};
|
|
917
|
-
this.runningTasks.set(taskId, runningTask);
|
|
918
997
|
this.initLogFile(taskId);
|
|
919
998
|
const logOutput = (line) => {
|
|
920
999
|
this.appendToLog(taskId, line);
|
|
921
|
-
runningTask
|
|
1000
|
+
this.runningTask?.output.push(line);
|
|
922
1001
|
this.emit("task:output", { taskId, line, lineType: "stdout" });
|
|
923
1002
|
};
|
|
924
1003
|
this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
|
|
925
1004
|
logOutput(`[claude-kanban] Starting task: ${task.title}
|
|
926
1005
|
`);
|
|
927
|
-
if (worktreeInfo) {
|
|
928
|
-
logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
|
|
929
|
-
`);
|
|
930
|
-
logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
|
|
931
|
-
`);
|
|
932
|
-
}
|
|
933
1006
|
logOutput(`[claude-kanban] Command: ${commandDisplay}
|
|
934
1007
|
`);
|
|
935
1008
|
logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
|
|
@@ -974,9 +1047,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
974
1047
|
const text = data.toString();
|
|
975
1048
|
logOutput(`[stderr] ${text}`);
|
|
976
1049
|
});
|
|
977
|
-
childProcess.on("spawn", () => {
|
|
978
|
-
console.log("[executor] Process spawned successfully");
|
|
979
|
-
});
|
|
980
1050
|
childProcess.on("error", (error) => {
|
|
981
1051
|
console.log("[executor] Spawn error:", error.message);
|
|
982
1052
|
this.emit("task:output", { taskId, line: `[claude-kanban] Error: ${error.message}
|
|
@@ -985,9 +1055,6 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
985
1055
|
unlinkSync(promptFile);
|
|
986
1056
|
} catch {
|
|
987
1057
|
}
|
|
988
|
-
if (worktreeInfo) {
|
|
989
|
-
this.removeWorktree(taskId);
|
|
990
|
-
}
|
|
991
1058
|
updateTask(this.projectPath, taskId, { status: "failed", passes: false });
|
|
992
1059
|
const endedAt = /* @__PURE__ */ new Date();
|
|
993
1060
|
addExecutionEntry(this.projectPath, taskId, {
|
|
@@ -998,7 +1065,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
998
1065
|
error: error.message
|
|
999
1066
|
});
|
|
1000
1067
|
this.emit("task:failed", { taskId, error: error.message });
|
|
1001
|
-
this.
|
|
1068
|
+
this.runningTask = null;
|
|
1002
1069
|
});
|
|
1003
1070
|
childProcess.on("close", (code, signal) => {
|
|
1004
1071
|
console.log("[executor] Process closed with code:", code, "signal:", signal);
|
|
@@ -1012,48 +1079,41 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1012
1079
|
});
|
|
1013
1080
|
const timeoutMs = (config.execution.timeout || 30) * 60 * 1e3;
|
|
1014
1081
|
setTimeout(() => {
|
|
1015
|
-
if (this.
|
|
1016
|
-
this.cancelTask(
|
|
1082
|
+
if (this.runningTask?.taskId === taskId) {
|
|
1083
|
+
this.cancelTask("Timeout exceeded");
|
|
1017
1084
|
}
|
|
1018
1085
|
}, timeoutMs);
|
|
1019
1086
|
}
|
|
1020
1087
|
/**
|
|
1021
|
-
* Handle task completion
|
|
1088
|
+
* Handle task completion
|
|
1022
1089
|
*/
|
|
1023
1090
|
handleTaskComplete(taskId, exitCode, startedAt) {
|
|
1024
|
-
|
|
1025
|
-
if (!runningTask) return;
|
|
1091
|
+
if (!this.runningTask || this.runningTask.taskId !== taskId) return;
|
|
1026
1092
|
const endedAt = /* @__PURE__ */ new Date();
|
|
1027
1093
|
const duration = endedAt.getTime() - startedAt.getTime();
|
|
1028
|
-
const output = runningTask.output.join("");
|
|
1094
|
+
const output = this.runningTask.output.join("");
|
|
1029
1095
|
const isComplete = output.includes("<promise>COMPLETE</promise>");
|
|
1030
1096
|
const task = getTaskById(this.projectPath, taskId);
|
|
1031
1097
|
if (isComplete || exitCode === 0) {
|
|
1032
|
-
if (runningTask.worktreePath && runningTask.branchName) {
|
|
1033
|
-
this.reviewingTasks.set(taskId, {
|
|
1034
|
-
worktreePath: runningTask.worktreePath,
|
|
1035
|
-
branchName: runningTask.branchName,
|
|
1036
|
-
startedAt,
|
|
1037
|
-
endedAt
|
|
1038
|
-
});
|
|
1039
|
-
console.log(`[executor] Task ${taskId} ready for review at ${runningTask.worktreePath}`);
|
|
1040
|
-
}
|
|
1041
1098
|
updateTask(this.projectPath, taskId, {
|
|
1042
|
-
status: "
|
|
1099
|
+
status: "completed",
|
|
1043
1100
|
passes: true
|
|
1044
1101
|
});
|
|
1102
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
1103
|
+
startedAt: startedAt.toISOString(),
|
|
1104
|
+
endedAt: endedAt.toISOString(),
|
|
1105
|
+
status: "completed",
|
|
1106
|
+
duration
|
|
1107
|
+
});
|
|
1045
1108
|
logTaskExecution(this.projectPath, {
|
|
1046
1109
|
taskId,
|
|
1047
1110
|
taskTitle: task?.title || "Unknown",
|
|
1048
|
-
status: "
|
|
1111
|
+
status: "completed",
|
|
1049
1112
|
duration
|
|
1050
1113
|
});
|
|
1051
|
-
this.emit("task:completed", { taskId, duration
|
|
1114
|
+
this.emit("task:completed", { taskId, duration });
|
|
1052
1115
|
this.afkTasksCompleted++;
|
|
1053
1116
|
} else {
|
|
1054
|
-
if (runningTask.worktreePath) {
|
|
1055
|
-
this.removeWorktree(taskId);
|
|
1056
|
-
}
|
|
1057
1117
|
updateTask(this.projectPath, taskId, {
|
|
1058
1118
|
status: "failed",
|
|
1059
1119
|
passes: false
|
|
@@ -1075,184 +1135,33 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1075
1135
|
});
|
|
1076
1136
|
this.emit("task:failed", { taskId, error });
|
|
1077
1137
|
}
|
|
1078
|
-
this.
|
|
1138
|
+
this.runningTask = null;
|
|
1079
1139
|
if (this.afkMode) {
|
|
1080
1140
|
this.continueAFKMode();
|
|
1081
1141
|
}
|
|
1082
1142
|
}
|
|
1083
1143
|
/**
|
|
1084
|
-
*
|
|
1085
|
-
*/
|
|
1086
|
-
getReviewingTask(taskId) {
|
|
1087
|
-
return this.reviewingTasks.get(taskId);
|
|
1088
|
-
}
|
|
1089
|
-
/**
|
|
1090
|
-
* Merge a reviewed task into the base branch
|
|
1091
|
-
*/
|
|
1092
|
-
mergeTask(taskId) {
|
|
1093
|
-
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1094
|
-
if (!reviewInfo) {
|
|
1095
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
1096
|
-
}
|
|
1097
|
-
const merged = this.mergeWorktreeBranch(taskId);
|
|
1098
|
-
if (!merged) {
|
|
1099
|
-
return { success: false, error: "Merge failed - may have conflicts" };
|
|
1100
|
-
}
|
|
1101
|
-
this.removeWorktree(taskId);
|
|
1102
|
-
this.reviewingTasks.delete(taskId);
|
|
1103
|
-
updateTask(this.projectPath, taskId, {
|
|
1104
|
-
status: "completed"
|
|
1105
|
-
});
|
|
1106
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
1107
|
-
startedAt: reviewInfo.startedAt.toISOString(),
|
|
1108
|
-
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1109
|
-
status: "completed",
|
|
1110
|
-
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
1111
|
-
});
|
|
1112
|
-
console.log(`[executor] Task ${taskId} merged successfully`);
|
|
1113
|
-
return { success: true };
|
|
1114
|
-
}
|
|
1115
|
-
/**
|
|
1116
|
-
* Create a PR for a reviewed task
|
|
1117
|
-
*/
|
|
1118
|
-
createPR(taskId) {
|
|
1119
|
-
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1120
|
-
const task = getTaskById(this.projectPath, taskId);
|
|
1121
|
-
if (!reviewInfo) {
|
|
1122
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
1123
|
-
}
|
|
1124
|
-
try {
|
|
1125
|
-
execSync(`git push -u origin ${reviewInfo.branchName}`, {
|
|
1126
|
-
cwd: this.projectPath,
|
|
1127
|
-
stdio: "pipe"
|
|
1128
|
-
});
|
|
1129
|
-
const prTitle = task?.title || `Task ${taskId}`;
|
|
1130
|
-
const prBody = task?.description || "";
|
|
1131
|
-
const result = execSync(
|
|
1132
|
-
`gh pr create --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}" --head ${reviewInfo.branchName}`,
|
|
1133
|
-
{
|
|
1134
|
-
cwd: this.projectPath,
|
|
1135
|
-
encoding: "utf-8"
|
|
1136
|
-
}
|
|
1137
|
-
);
|
|
1138
|
-
const prUrl = result.trim();
|
|
1139
|
-
try {
|
|
1140
|
-
execSync(`git worktree remove "${reviewInfo.worktreePath}" --force`, {
|
|
1141
|
-
cwd: this.projectPath,
|
|
1142
|
-
stdio: "pipe"
|
|
1143
|
-
});
|
|
1144
|
-
} catch {
|
|
1145
|
-
}
|
|
1146
|
-
this.reviewingTasks.delete(taskId);
|
|
1147
|
-
updateTask(this.projectPath, taskId, {
|
|
1148
|
-
status: "completed"
|
|
1149
|
-
});
|
|
1150
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
1151
|
-
startedAt: reviewInfo.startedAt.toISOString(),
|
|
1152
|
-
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1153
|
-
status: "completed",
|
|
1154
|
-
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
1155
|
-
});
|
|
1156
|
-
console.log(`[executor] PR created for task ${taskId}: ${prUrl}`);
|
|
1157
|
-
return { success: true, prUrl };
|
|
1158
|
-
} catch (error) {
|
|
1159
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1160
|
-
console.error(`[executor] Failed to create PR:`, errorMsg);
|
|
1161
|
-
return { success: false, error: errorMsg };
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Discard a reviewed task - delete worktree and return to ready
|
|
1144
|
+
* Cancel the running task
|
|
1166
1145
|
*/
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
1171
|
-
}
|
|
1172
|
-
this.removeWorktree(taskId);
|
|
1173
|
-
this.reviewingTasks.delete(taskId);
|
|
1174
|
-
updateTask(this.projectPath, taskId, {
|
|
1175
|
-
status: "ready",
|
|
1176
|
-
passes: false
|
|
1177
|
-
});
|
|
1178
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
1179
|
-
startedAt: reviewInfo.startedAt.toISOString(),
|
|
1180
|
-
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1181
|
-
status: "discarded",
|
|
1182
|
-
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
1183
|
-
});
|
|
1184
|
-
console.log(`[executor] Task ${taskId} discarded, returned to ready`);
|
|
1185
|
-
return { success: true };
|
|
1186
|
-
}
|
|
1187
|
-
/**
|
|
1188
|
-
* Get diff for a task in review
|
|
1189
|
-
*/
|
|
1190
|
-
getTaskDiff(taskId) {
|
|
1191
|
-
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1192
|
-
if (!reviewInfo) {
|
|
1193
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
1194
|
-
}
|
|
1195
|
-
try {
|
|
1196
|
-
const diff = execSync(`git diff main...${reviewInfo.branchName}`, {
|
|
1197
|
-
cwd: this.projectPath,
|
|
1198
|
-
encoding: "utf-8",
|
|
1199
|
-
maxBuffer: 10 * 1024 * 1024
|
|
1200
|
-
// 10MB buffer for large diffs
|
|
1201
|
-
});
|
|
1202
|
-
return { success: true, diff };
|
|
1203
|
-
} catch (error) {
|
|
1204
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1205
|
-
return { success: false, error: errorMsg };
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Get list of changed files for a task in review
|
|
1210
|
-
*/
|
|
1211
|
-
getTaskChangedFiles(taskId) {
|
|
1212
|
-
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1213
|
-
if (!reviewInfo) {
|
|
1214
|
-
return { success: false, error: "Task not in review or no worktree found" };
|
|
1215
|
-
}
|
|
1216
|
-
try {
|
|
1217
|
-
const result = execSync(`git diff --name-only main...${reviewInfo.branchName}`, {
|
|
1218
|
-
cwd: this.projectPath,
|
|
1219
|
-
encoding: "utf-8"
|
|
1220
|
-
});
|
|
1221
|
-
const files = result.trim().split("\n").filter((f) => f);
|
|
1222
|
-
return { success: true, files };
|
|
1223
|
-
} catch (error) {
|
|
1224
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1225
|
-
return { success: false, error: errorMsg };
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
/**
|
|
1229
|
-
* Cancel a running task
|
|
1230
|
-
*/
|
|
1231
|
-
cancelTask(taskId, reason = "Cancelled by user") {
|
|
1232
|
-
const runningTask = this.runningTasks.get(taskId);
|
|
1233
|
-
if (!runningTask) return false;
|
|
1234
|
-
const startedAt = runningTask.startedAt;
|
|
1146
|
+
cancelTask(reason = "Cancelled by user") {
|
|
1147
|
+
if (!this.runningTask) return false;
|
|
1148
|
+
const { taskId, process: childProcess, startedAt } = this.runningTask;
|
|
1235
1149
|
const endedAt = /* @__PURE__ */ new Date();
|
|
1236
1150
|
const duration = endedAt.getTime() - startedAt.getTime();
|
|
1237
1151
|
const task = getTaskById(this.projectPath, taskId);
|
|
1238
1152
|
try {
|
|
1239
|
-
|
|
1153
|
+
childProcess.kill("SIGTERM");
|
|
1240
1154
|
setTimeout(() => {
|
|
1241
1155
|
try {
|
|
1242
|
-
if (!
|
|
1243
|
-
|
|
1156
|
+
if (!childProcess.killed) {
|
|
1157
|
+
childProcess.kill("SIGKILL");
|
|
1244
1158
|
}
|
|
1245
1159
|
} catch {
|
|
1246
1160
|
}
|
|
1247
1161
|
}, 2e3);
|
|
1248
1162
|
} catch {
|
|
1249
1163
|
}
|
|
1250
|
-
|
|
1251
|
-
this.removeWorktree(taskId);
|
|
1252
|
-
}
|
|
1253
|
-
updateTask(this.projectPath, taskId, {
|
|
1254
|
-
status: "ready"
|
|
1255
|
-
});
|
|
1164
|
+
updateTask(this.projectPath, taskId, { status: "ready" });
|
|
1256
1165
|
addExecutionEntry(this.projectPath, taskId, {
|
|
1257
1166
|
startedAt: startedAt.toISOString(),
|
|
1258
1167
|
endedAt: endedAt.toISOString(),
|
|
@@ -1268,48 +1177,46 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1268
1177
|
error: reason
|
|
1269
1178
|
});
|
|
1270
1179
|
this.emit("task:cancelled", { taskId });
|
|
1271
|
-
this.
|
|
1180
|
+
this.runningTask = null;
|
|
1272
1181
|
return true;
|
|
1273
1182
|
}
|
|
1274
1183
|
/**
|
|
1275
|
-
* Start AFK mode
|
|
1184
|
+
* Start AFK mode - run tasks sequentially until done
|
|
1276
1185
|
*/
|
|
1277
|
-
startAFKMode(maxIterations
|
|
1186
|
+
startAFKMode(maxIterations) {
|
|
1278
1187
|
if (this.afkMode) {
|
|
1279
1188
|
throw new Error("AFK mode already running");
|
|
1280
1189
|
}
|
|
1190
|
+
if (this.isBusy()) {
|
|
1191
|
+
throw new Error("Cannot start AFK mode while another operation is in progress");
|
|
1192
|
+
}
|
|
1281
1193
|
this.afkMode = true;
|
|
1282
1194
|
this.afkIteration = 0;
|
|
1283
1195
|
this.afkMaxIterations = maxIterations;
|
|
1284
1196
|
this.afkTasksCompleted = 0;
|
|
1285
1197
|
this.emitAFKStatus();
|
|
1286
|
-
this.continueAFKMode(
|
|
1198
|
+
this.continueAFKMode();
|
|
1287
1199
|
}
|
|
1288
1200
|
/**
|
|
1289
|
-
* Continue AFK mode - pick
|
|
1201
|
+
* Continue AFK mode - pick next task
|
|
1290
1202
|
*/
|
|
1291
|
-
continueAFKMode(
|
|
1203
|
+
continueAFKMode() {
|
|
1292
1204
|
if (!this.afkMode) return;
|
|
1293
1205
|
if (this.afkIteration >= this.afkMaxIterations) {
|
|
1294
1206
|
this.stopAFKMode();
|
|
1295
1207
|
return;
|
|
1296
1208
|
}
|
|
1297
|
-
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
if (this.getRunningCount() === 0) {
|
|
1303
|
-
this.stopAFKMode();
|
|
1304
|
-
}
|
|
1305
|
-
break;
|
|
1306
|
-
}
|
|
1307
|
-
this.afkIteration++;
|
|
1308
|
-
this.runTask(nextTask.id).catch((error) => {
|
|
1309
|
-
console.error("AFK task error:", error);
|
|
1310
|
-
});
|
|
1311
|
-
this.emitAFKStatus();
|
|
1209
|
+
if (this.isBusy()) return;
|
|
1210
|
+
const nextTask = getNextReadyTask(this.projectPath);
|
|
1211
|
+
if (!nextTask) {
|
|
1212
|
+
this.stopAFKMode();
|
|
1213
|
+
return;
|
|
1312
1214
|
}
|
|
1215
|
+
this.afkIteration++;
|
|
1216
|
+
this.runTask(nextTask.id).catch((error) => {
|
|
1217
|
+
console.error("AFK task error:", error);
|
|
1218
|
+
});
|
|
1219
|
+
this.emitAFKStatus();
|
|
1313
1220
|
}
|
|
1314
1221
|
/**
|
|
1315
1222
|
* Stop AFK mode
|
|
@@ -1341,18 +1248,22 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1341
1248
|
};
|
|
1342
1249
|
}
|
|
1343
1250
|
/**
|
|
1344
|
-
* Cancel
|
|
1251
|
+
* Cancel running task/planning and stop AFK mode
|
|
1345
1252
|
*/
|
|
1346
1253
|
cancelAll() {
|
|
1347
|
-
|
|
1254
|
+
if (this.runningTask) {
|
|
1348
1255
|
try {
|
|
1349
|
-
runningTask.process.kill("SIGKILL");
|
|
1256
|
+
this.runningTask.process.kill("SIGKILL");
|
|
1350
1257
|
} catch {
|
|
1351
1258
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1259
|
+
this.runningTask = null;
|
|
1260
|
+
}
|
|
1261
|
+
if (this.planningSession) {
|
|
1262
|
+
try {
|
|
1263
|
+
this.planningSession.process.kill("SIGKILL");
|
|
1264
|
+
} catch {
|
|
1354
1265
|
}
|
|
1355
|
-
this.
|
|
1266
|
+
this.planningSession = null;
|
|
1356
1267
|
}
|
|
1357
1268
|
this.stopAFKMode();
|
|
1358
1269
|
}
|
|
@@ -1778,6 +1689,11 @@ async function createServer(projectPath, port) {
|
|
|
1778
1689
|
executor.on("task:failed", (data) => io.emit("task:failed", data));
|
|
1779
1690
|
executor.on("task:cancelled", (data) => io.emit("task:cancelled", data));
|
|
1780
1691
|
executor.on("afk:status", (data) => io.emit("afk:status", data));
|
|
1692
|
+
executor.on("planning:started", (data) => io.emit("planning:started", data));
|
|
1693
|
+
executor.on("planning:output", (data) => io.emit("planning:output", data));
|
|
1694
|
+
executor.on("planning:completed", (data) => io.emit("planning:completed", data));
|
|
1695
|
+
executor.on("planning:failed", (data) => io.emit("planning:failed", data));
|
|
1696
|
+
executor.on("planning:cancelled", (data) => io.emit("planning:cancelled", data));
|
|
1781
1697
|
app.get("/api/tasks", (_req, res) => {
|
|
1782
1698
|
try {
|
|
1783
1699
|
const tasks = getAllTasks(projectPath);
|
|
@@ -1862,7 +1778,12 @@ async function createServer(projectPath, port) {
|
|
|
1862
1778
|
});
|
|
1863
1779
|
app.post("/api/tasks/:id/cancel", (req, res) => {
|
|
1864
1780
|
try {
|
|
1865
|
-
const
|
|
1781
|
+
const runningId = executor.getRunningTaskId();
|
|
1782
|
+
if (runningId !== req.params.id) {
|
|
1783
|
+
res.status(404).json({ error: "Task not running" });
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
const cancelled = executor.cancelTask();
|
|
1866
1787
|
if (!cancelled) {
|
|
1867
1788
|
res.status(404).json({ error: "Task not running" });
|
|
1868
1789
|
return;
|
|
@@ -1891,90 +1812,6 @@ async function createServer(projectPath, port) {
|
|
|
1891
1812
|
res.status(500).json({ error: String(error) });
|
|
1892
1813
|
}
|
|
1893
1814
|
});
|
|
1894
|
-
app.post("/api/tasks/:id/merge", (req, res) => {
|
|
1895
|
-
try {
|
|
1896
|
-
const result = executor.mergeTask(req.params.id);
|
|
1897
|
-
if (!result.success) {
|
|
1898
|
-
res.status(400).json({ error: result.error });
|
|
1899
|
-
return;
|
|
1900
|
-
}
|
|
1901
|
-
const task = getTaskById(projectPath, req.params.id);
|
|
1902
|
-
if (task) {
|
|
1903
|
-
io.emit("task:updated", task);
|
|
1904
|
-
}
|
|
1905
|
-
res.json({ success: true });
|
|
1906
|
-
} catch (error) {
|
|
1907
|
-
res.status(500).json({ error: String(error) });
|
|
1908
|
-
}
|
|
1909
|
-
});
|
|
1910
|
-
app.post("/api/tasks/:id/create-pr", (req, res) => {
|
|
1911
|
-
try {
|
|
1912
|
-
const result = executor.createPR(req.params.id);
|
|
1913
|
-
if (!result.success) {
|
|
1914
|
-
res.status(400).json({ error: result.error });
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
const task = getTaskById(projectPath, req.params.id);
|
|
1918
|
-
if (task) {
|
|
1919
|
-
io.emit("task:updated", task);
|
|
1920
|
-
}
|
|
1921
|
-
res.json({ success: true, prUrl: result.prUrl });
|
|
1922
|
-
} catch (error) {
|
|
1923
|
-
res.status(500).json({ error: String(error) });
|
|
1924
|
-
}
|
|
1925
|
-
});
|
|
1926
|
-
app.post("/api/tasks/:id/discard", (req, res) => {
|
|
1927
|
-
try {
|
|
1928
|
-
const result = executor.discardTask(req.params.id);
|
|
1929
|
-
if (!result.success) {
|
|
1930
|
-
res.status(400).json({ error: result.error });
|
|
1931
|
-
return;
|
|
1932
|
-
}
|
|
1933
|
-
const task = getTaskById(projectPath, req.params.id);
|
|
1934
|
-
if (task) {
|
|
1935
|
-
io.emit("task:updated", task);
|
|
1936
|
-
}
|
|
1937
|
-
res.json({ success: true });
|
|
1938
|
-
} catch (error) {
|
|
1939
|
-
res.status(500).json({ error: String(error) });
|
|
1940
|
-
}
|
|
1941
|
-
});
|
|
1942
|
-
app.get("/api/tasks/:id/diff", (req, res) => {
|
|
1943
|
-
try {
|
|
1944
|
-
const result = executor.getTaskDiff(req.params.id);
|
|
1945
|
-
if (!result.success) {
|
|
1946
|
-
res.status(400).json({ error: result.error });
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
|
-
res.json({ diff: result.diff });
|
|
1950
|
-
} catch (error) {
|
|
1951
|
-
res.status(500).json({ error: String(error) });
|
|
1952
|
-
}
|
|
1953
|
-
});
|
|
1954
|
-
app.get("/api/tasks/:id/changed-files", (req, res) => {
|
|
1955
|
-
try {
|
|
1956
|
-
const result = executor.getTaskChangedFiles(req.params.id);
|
|
1957
|
-
if (!result.success) {
|
|
1958
|
-
res.status(400).json({ error: result.error });
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
res.json({ files: result.files });
|
|
1962
|
-
} catch (error) {
|
|
1963
|
-
res.status(500).json({ error: String(error) });
|
|
1964
|
-
}
|
|
1965
|
-
});
|
|
1966
|
-
app.get("/api/tasks/:id/review-info", (req, res) => {
|
|
1967
|
-
try {
|
|
1968
|
-
const info = executor.getReviewingTask(req.params.id);
|
|
1969
|
-
if (!info) {
|
|
1970
|
-
res.status(404).json({ error: "Task not in review" });
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
res.json(info);
|
|
1974
|
-
} catch (error) {
|
|
1975
|
-
res.status(500).json({ error: String(error) });
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
1815
|
app.get("/api/tasks/:id/logs", (req, res) => {
|
|
1979
1816
|
try {
|
|
1980
1817
|
const logs = executor.getTaskLog(req.params.id);
|
|
@@ -2048,8 +1885,8 @@ async function createServer(projectPath, port) {
|
|
|
2048
1885
|
});
|
|
2049
1886
|
app.post("/api/afk/start", (req, res) => {
|
|
2050
1887
|
try {
|
|
2051
|
-
const { maxIterations
|
|
2052
|
-
executor.startAFKMode(maxIterations || 10
|
|
1888
|
+
const { maxIterations } = req.body;
|
|
1889
|
+
executor.startAFKMode(maxIterations || 10);
|
|
2053
1890
|
res.json({ success: true, status: executor.getAFKStatus() });
|
|
2054
1891
|
} catch (error) {
|
|
2055
1892
|
res.status(400).json({ error: String(error) });
|
|
@@ -2071,10 +1908,44 @@ async function createServer(projectPath, port) {
|
|
|
2071
1908
|
res.status(500).json({ error: String(error) });
|
|
2072
1909
|
}
|
|
2073
1910
|
});
|
|
1911
|
+
app.post("/api/plan", async (req, res) => {
|
|
1912
|
+
try {
|
|
1913
|
+
const { goal } = req.body;
|
|
1914
|
+
if (!goal) {
|
|
1915
|
+
res.status(400).json({ error: "Goal is required" });
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
await executor.runPlanningSession(goal);
|
|
1919
|
+
res.json({ success: true });
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
res.status(400).json({ error: String(error) });
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
app.post("/api/plan/cancel", (_req, res) => {
|
|
1925
|
+
try {
|
|
1926
|
+
const cancelled = executor.cancelPlanning();
|
|
1927
|
+
if (!cancelled) {
|
|
1928
|
+
res.status(404).json({ error: "No planning session running" });
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
res.json({ success: true });
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
res.status(500).json({ error: String(error) });
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
app.get("/api/plan/status", (_req, res) => {
|
|
1937
|
+
try {
|
|
1938
|
+
const planning = executor.isPlanning();
|
|
1939
|
+
const goal = executor.getPlanningGoal();
|
|
1940
|
+
res.json({ planning, goal: goal || null });
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
res.status(500).json({ error: String(error) });
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
2074
1945
|
app.get("/api/running", (_req, res) => {
|
|
2075
1946
|
try {
|
|
2076
|
-
const
|
|
2077
|
-
res.json({ running:
|
|
1947
|
+
const taskId = executor.getRunningTaskId();
|
|
1948
|
+
res.json({ running: taskId ? [taskId] : [], count: taskId ? 1 : 0 });
|
|
2078
1949
|
} catch (error) {
|
|
2079
1950
|
res.status(500).json({ error: String(error) });
|
|
2080
1951
|
}
|
|
@@ -2082,9 +1953,10 @@ async function createServer(projectPath, port) {
|
|
|
2082
1953
|
app.get("/api/stats", (_req, res) => {
|
|
2083
1954
|
try {
|
|
2084
1955
|
const counts = getTaskCounts(projectPath);
|
|
2085
|
-
const running = executor.
|
|
1956
|
+
const running = executor.isRunning() ? 1 : 0;
|
|
2086
1957
|
const afk = executor.getAFKStatus();
|
|
2087
|
-
|
|
1958
|
+
const planning = executor.isPlanning();
|
|
1959
|
+
res.json({ counts, running, afk, planning });
|
|
2088
1960
|
} catch (error) {
|
|
2089
1961
|
res.status(500).json({ error: String(error) });
|
|
2090
1962
|
}
|
|
@@ -2098,20 +1970,24 @@ async function createServer(projectPath, port) {
|
|
|
2098
1970
|
});
|
|
2099
1971
|
io.on("connection", (socket) => {
|
|
2100
1972
|
console.log("Client connected");
|
|
2101
|
-
const
|
|
1973
|
+
const runningId = executor.getRunningTaskId();
|
|
1974
|
+
const runningIds = runningId ? [runningId] : [];
|
|
2102
1975
|
const taskLogs = {};
|
|
2103
|
-
|
|
2104
|
-
const logs = executor.getTaskLog(
|
|
1976
|
+
if (runningId) {
|
|
1977
|
+
const logs = executor.getTaskLog(runningId);
|
|
2105
1978
|
if (logs) {
|
|
2106
|
-
taskLogs[
|
|
1979
|
+
taskLogs[runningId] = logs;
|
|
2107
1980
|
}
|
|
2108
1981
|
}
|
|
2109
1982
|
socket.emit("init", {
|
|
2110
1983
|
tasks: getAllTasks(projectPath),
|
|
2111
1984
|
running: runningIds,
|
|
2112
1985
|
afk: executor.getAFKStatus(),
|
|
2113
|
-
taskLogs
|
|
2114
|
-
// Include logs for running
|
|
1986
|
+
taskLogs,
|
|
1987
|
+
// Include logs for running task
|
|
1988
|
+
planning: executor.isPlanning(),
|
|
1989
|
+
planningGoal: executor.getPlanningGoal() || null,
|
|
1990
|
+
planningOutput: executor.getPlanningOutput() || []
|
|
2115
1991
|
});
|
|
2116
1992
|
socket.on("get-logs", (taskId) => {
|
|
2117
1993
|
const logs = executor.getTaskLog(taskId);
|
|
@@ -2284,7 +2160,6 @@ function getClientHTML() {
|
|
|
2284
2160
|
.status-dot-draft { background: #a3a3a3; }
|
|
2285
2161
|
.status-dot-ready { background: #3b82f6; }
|
|
2286
2162
|
.status-dot-in_progress { background: #f97316; }
|
|
2287
|
-
.status-dot-in_review { background: #8b5cf6; }
|
|
2288
2163
|
.status-dot-completed { background: #22c55e; }
|
|
2289
2164
|
.status-dot-failed { background: #ef4444; }
|
|
2290
2165
|
|
|
@@ -2342,7 +2217,7 @@ function getClientHTML() {
|
|
|
2342
2217
|
transition: all 0.2s var(--ease-out-expo);
|
|
2343
2218
|
}
|
|
2344
2219
|
.side-panel-header {
|
|
2345
|
-
padding: 16px
|
|
2220
|
+
padding: 12px 16px;
|
|
2346
2221
|
border-bottom: 1px solid #e5e5e5;
|
|
2347
2222
|
flex-shrink: 0;
|
|
2348
2223
|
}
|
|
@@ -2360,7 +2235,8 @@ function getClientHTML() {
|
|
|
2360
2235
|
flex-shrink: 0;
|
|
2361
2236
|
}
|
|
2362
2237
|
.side-panel-tab {
|
|
2363
|
-
padding:
|
|
2238
|
+
padding: 10px 14px;
|
|
2239
|
+
font-size: 13px;
|
|
2364
2240
|
color: #737373;
|
|
2365
2241
|
cursor: pointer;
|
|
2366
2242
|
border-bottom: 2px solid transparent;
|
|
@@ -2494,7 +2370,6 @@ function getClientHTML() {
|
|
|
2494
2370
|
.status-badge-draft { background: #f5f5f5; color: #737373; }
|
|
2495
2371
|
.status-badge-ready { background: #eff6ff; color: #3b82f6; }
|
|
2496
2372
|
.status-badge-in_progress { background: #fff7ed; color: #f97316; }
|
|
2497
|
-
.status-badge-in_review { background: #f5f3ff; color: #8b5cf6; }
|
|
2498
2373
|
.status-badge-completed { background: #f0fdf4; color: #22c55e; }
|
|
2499
2374
|
.status-badge-failed { background: #fef2f2; color: #ef4444; }
|
|
2500
2375
|
|
|
@@ -2654,6 +2529,10 @@ let state = {
|
|
|
2654
2529
|
logFullscreen: false,
|
|
2655
2530
|
closedTabs: new Set(),
|
|
2656
2531
|
sidePanel: null, // task id for side panel
|
|
2532
|
+
// Planning state
|
|
2533
|
+
planning: false,
|
|
2534
|
+
planningGoal: '',
|
|
2535
|
+
planningOutput: [],
|
|
2657
2536
|
sidePanelTab: 'logs', // 'logs' or 'details'
|
|
2658
2537
|
darkMode: localStorage.getItem('darkMode') === 'true', // Add dark mode state
|
|
2659
2538
|
};
|
|
@@ -2748,6 +2627,14 @@ socket.on('init', (data) => {
|
|
|
2748
2627
|
state.running = data.running;
|
|
2749
2628
|
state.afk = data.afk;
|
|
2750
2629
|
|
|
2630
|
+
// Planning state
|
|
2631
|
+
state.planning = data.planning || false;
|
|
2632
|
+
state.planningGoal = data.planningGoal || '';
|
|
2633
|
+
state.planningOutput = (data.planningOutput || []).map(text => ({
|
|
2634
|
+
text: text,
|
|
2635
|
+
timestamp: new Date().toISOString()
|
|
2636
|
+
}));
|
|
2637
|
+
|
|
2751
2638
|
// Load persisted logs for running tasks
|
|
2752
2639
|
if (data.taskLogs) {
|
|
2753
2640
|
for (const [taskId, logs] of Object.entries(data.taskLogs)) {
|
|
@@ -2820,16 +2707,14 @@ socket.on('task:output', ({ taskId, line }) => {
|
|
|
2820
2707
|
}
|
|
2821
2708
|
});
|
|
2822
2709
|
|
|
2823
|
-
socket.on('task:completed', ({ taskId
|
|
2710
|
+
socket.on('task:completed', ({ taskId }) => {
|
|
2824
2711
|
state.running = state.running.filter(id => id !== taskId);
|
|
2825
2712
|
const task = state.tasks.find(t => t.id === taskId);
|
|
2826
2713
|
if (task) {
|
|
2827
|
-
|
|
2828
|
-
task.status = status || 'in_review';
|
|
2714
|
+
task.status = 'completed';
|
|
2829
2715
|
task.passes = true;
|
|
2830
2716
|
}
|
|
2831
|
-
|
|
2832
|
-
showToast('Task ' + statusMsg + ': ' + (task?.title || taskId), 'success');
|
|
2717
|
+
showToast('Task completed: ' + (task?.title || taskId), 'success');
|
|
2833
2718
|
render();
|
|
2834
2719
|
});
|
|
2835
2720
|
|
|
@@ -2854,6 +2739,48 @@ socket.on('afk:status', (status) => {
|
|
|
2854
2739
|
render();
|
|
2855
2740
|
});
|
|
2856
2741
|
|
|
2742
|
+
// Planning socket handlers
|
|
2743
|
+
socket.on('planning:started', ({ goal, timestamp }) => {
|
|
2744
|
+
state.planning = true;
|
|
2745
|
+
state.planningGoal = goal;
|
|
2746
|
+
state.planningOutput = [];
|
|
2747
|
+
state.showModal = 'planning';
|
|
2748
|
+
showToast('Planning started: ' + goal.substring(0, 50) + (goal.length > 50 ? '...' : ''), 'info');
|
|
2749
|
+
render();
|
|
2750
|
+
});
|
|
2751
|
+
|
|
2752
|
+
socket.on('planning:output', ({ line }) => {
|
|
2753
|
+
state.planningOutput.push({
|
|
2754
|
+
text: line,
|
|
2755
|
+
timestamp: new Date().toISOString()
|
|
2756
|
+
});
|
|
2757
|
+
render();
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2760
|
+
socket.on('planning:completed', () => {
|
|
2761
|
+
state.planning = false;
|
|
2762
|
+
showToast('Planning complete! New tasks added to board.', 'success');
|
|
2763
|
+
// Refresh tasks from server
|
|
2764
|
+
fetch('/api/tasks').then(r => r.json()).then(data => {
|
|
2765
|
+
state.tasks = data.tasks;
|
|
2766
|
+
state.showModal = null;
|
|
2767
|
+
render();
|
|
2768
|
+
});
|
|
2769
|
+
});
|
|
2770
|
+
|
|
2771
|
+
socket.on('planning:failed', ({ error }) => {
|
|
2772
|
+
state.planning = false;
|
|
2773
|
+
showToast('Planning failed: ' + error, 'error');
|
|
2774
|
+
render();
|
|
2775
|
+
});
|
|
2776
|
+
|
|
2777
|
+
socket.on('planning:cancelled', () => {
|
|
2778
|
+
state.planning = false;
|
|
2779
|
+
state.showModal = null;
|
|
2780
|
+
showToast('Planning cancelled', 'warning');
|
|
2781
|
+
render();
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2857
2784
|
// Load templates
|
|
2858
2785
|
fetch('/api/templates').then(r => r.json()).then(data => {
|
|
2859
2786
|
state.templates = data.templates;
|
|
@@ -2903,53 +2830,6 @@ async function retryTask(id) {
|
|
|
2903
2830
|
});
|
|
2904
2831
|
}
|
|
2905
2832
|
|
|
2906
|
-
async function mergeTask(id) {
|
|
2907
|
-
const res = await fetch('/api/tasks/' + id + '/merge', { method: 'POST' });
|
|
2908
|
-
const data = await res.json();
|
|
2909
|
-
if (!res.ok) {
|
|
2910
|
-
throw new Error(data.error || 'Merge failed');
|
|
2911
|
-
}
|
|
2912
|
-
return data;
|
|
2913
|
-
}
|
|
2914
|
-
|
|
2915
|
-
async function createPR(id) {
|
|
2916
|
-
const res = await fetch('/api/tasks/' + id + '/create-pr', { method: 'POST' });
|
|
2917
|
-
const data = await res.json();
|
|
2918
|
-
if (!res.ok) {
|
|
2919
|
-
throw new Error(data.error || 'PR creation failed');
|
|
2920
|
-
}
|
|
2921
|
-
return data;
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
async function discardTask(id) {
|
|
2925
|
-
const res = await fetch('/api/tasks/' + id + '/discard', { method: 'POST' });
|
|
2926
|
-
const data = await res.json();
|
|
2927
|
-
if (!res.ok) {
|
|
2928
|
-
throw new Error(data.error || 'Discard failed');
|
|
2929
|
-
}
|
|
2930
|
-
return data;
|
|
2931
|
-
}
|
|
2932
|
-
|
|
2933
|
-
async function getReviewInfo(id) {
|
|
2934
|
-
const res = await fetch('/api/tasks/' + id + '/review-info');
|
|
2935
|
-
const data = await res.json();
|
|
2936
|
-
if (!res.ok) {
|
|
2937
|
-
throw new Error(data.error || 'Failed to get review info');
|
|
2938
|
-
}
|
|
2939
|
-
return data;
|
|
2940
|
-
}
|
|
2941
|
-
|
|
2942
|
-
async function openInVSCode(id) {
|
|
2943
|
-
try {
|
|
2944
|
-
const info = await getReviewInfo(id);
|
|
2945
|
-
// Open VS Code at the worktree path
|
|
2946
|
-
window.open('vscode://file/' + encodeURIComponent(info.worktreePath), '_blank');
|
|
2947
|
-
showToast('Opening in VS Code...', 'info');
|
|
2948
|
-
} catch (e) {
|
|
2949
|
-
showToast('Failed to open in VS Code: ' + e.message, 'error');
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
2833
|
async function generateTask(prompt) {
|
|
2954
2834
|
state.aiGenerating = true;
|
|
2955
2835
|
render();
|
|
@@ -2968,11 +2848,11 @@ async function generateTask(prompt) {
|
|
|
2968
2848
|
}
|
|
2969
2849
|
}
|
|
2970
2850
|
|
|
2971
|
-
async function startAFK(maxIterations
|
|
2851
|
+
async function startAFK(maxIterations) {
|
|
2972
2852
|
await fetch('/api/afk/start', {
|
|
2973
2853
|
method: 'POST',
|
|
2974
2854
|
headers: { 'Content-Type': 'application/json' },
|
|
2975
|
-
body: JSON.stringify({ maxIterations
|
|
2855
|
+
body: JSON.stringify({ maxIterations })
|
|
2976
2856
|
});
|
|
2977
2857
|
}
|
|
2978
2858
|
|
|
@@ -2980,6 +2860,18 @@ async function stopAFK() {
|
|
|
2980
2860
|
await fetch('/api/afk/stop', { method: 'POST' });
|
|
2981
2861
|
}
|
|
2982
2862
|
|
|
2863
|
+
async function startPlanning(goal) {
|
|
2864
|
+
await fetch('/api/plan', {
|
|
2865
|
+
method: 'POST',
|
|
2866
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2867
|
+
body: JSON.stringify({ goal })
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
async function cancelPlanning() {
|
|
2872
|
+
await fetch('/api/plan/cancel', { method: 'POST' });
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2983
2875
|
// Enhanced Drag and drop
|
|
2984
2876
|
let draggedTask = null;
|
|
2985
2877
|
let draggedElement = null;
|
|
@@ -3124,6 +3016,11 @@ function openSidePanel(taskId) {
|
|
|
3124
3016
|
state.activeTab = taskId;
|
|
3125
3017
|
state.closedTabs.delete(taskId);
|
|
3126
3018
|
|
|
3019
|
+
// Auto-switch to logs tab if task is running
|
|
3020
|
+
if (state.running.includes(taskId)) {
|
|
3021
|
+
state.sidePanelTab = 'logs';
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3127
3024
|
// Request logs from server if not already loaded
|
|
3128
3025
|
if (!state.taskOutput[taskId] || state.taskOutput[taskId].length === 0) {
|
|
3129
3026
|
socket.emit('get-logs', taskId);
|
|
@@ -3153,7 +3050,6 @@ function renderColumn(status, title, tasks) {
|
|
|
3153
3050
|
draft: 'To Do',
|
|
3154
3051
|
ready: 'Ready',
|
|
3155
3052
|
in_progress: 'In Progress',
|
|
3156
|
-
in_review: 'In Review',
|
|
3157
3053
|
completed: 'Done',
|
|
3158
3054
|
failed: 'Failed'
|
|
3159
3055
|
};
|
|
@@ -3406,21 +3302,13 @@ function renderModal() {
|
|
|
3406
3302
|
<button onclick="state.showModal = null; render();" class="btn btn-ghost p-1.5 text-canvas-500 hover:text-canvas-700">\u2715</button>
|
|
3407
3303
|
</div>
|
|
3408
3304
|
<div class="p-6">
|
|
3409
|
-
<p class="text-sm text-canvas-600 mb-5">Run the agent in a loop, automatically picking up tasks from the "Ready" column until complete.</p>
|
|
3305
|
+
<p class="text-sm text-canvas-600 mb-5">Run the agent in a loop, automatically picking up tasks from the "Ready" column one at a time until complete.</p>
|
|
3410
3306
|
<div class="space-y-4">
|
|
3411
3307
|
<div>
|
|
3412
3308
|
<label class="block text-sm font-medium text-canvas-700 mb-2">Maximum Iterations</label>
|
|
3413
3309
|
<input type="number" id="afk-iterations" value="10" min="1" max="100"
|
|
3414
3310
|
class="input w-full">
|
|
3415
3311
|
</div>
|
|
3416
|
-
<div>
|
|
3417
|
-
<label class="block text-sm font-medium text-canvas-700 mb-2">Concurrent Tasks</label>
|
|
3418
|
-
<select id="afk-concurrent" class="input w-full">
|
|
3419
|
-
<option value="1">1 (Sequential)</option>
|
|
3420
|
-
<option value="2">2</option>
|
|
3421
|
-
<option value="3">3 (Max)</option>
|
|
3422
|
-
</select>
|
|
3423
|
-
</div>
|
|
3424
3312
|
<div class="bg-status-running/10 border border-status-running/20 rounded-lg p-3">
|
|
3425
3313
|
<p class="text-xs text-status-running">\u26A0\uFE0F You can close this tab - the agent will continue running. Check back later or watch the terminal output.</p>
|
|
3426
3314
|
</div>
|
|
@@ -3437,6 +3325,71 @@ function renderModal() {
|
|
|
3437
3325
|
\`;
|
|
3438
3326
|
}
|
|
3439
3327
|
|
|
3328
|
+
// Planning input modal
|
|
3329
|
+
if (state.showModal === 'plan-input') {
|
|
3330
|
+
return \`
|
|
3331
|
+
<div class="modal-backdrop fixed inset-0 flex items-center justify-center z-50" onclick="if(event.target === event.currentTarget) { state.showModal = null; render(); }">
|
|
3332
|
+
<div class="modal-content card rounded-xl w-full max-w-lg mx-4">
|
|
3333
|
+
<div class="px-6 py-4 border-b border-white/5 flex justify-between items-center">
|
|
3334
|
+
<h3 class="font-display font-semibold text-canvas-800 text-lg">\u{1F3AF} AI Task Planner</h3>
|
|
3335
|
+
<button onclick="state.showModal = null; render();" class="btn btn-ghost p-1.5 text-canvas-500 hover:text-canvas-700">\u2715</button>
|
|
3336
|
+
</div>
|
|
3337
|
+
<div class="p-6">
|
|
3338
|
+
<p class="text-sm text-canvas-600 mb-4">Describe your goal and the AI will analyze the codebase and break it down into concrete tasks.</p>
|
|
3339
|
+
<div class="space-y-4">
|
|
3340
|
+
<div>
|
|
3341
|
+
<label class="block text-sm font-medium text-canvas-700 mb-2">What do you want to build?</label>
|
|
3342
|
+
<textarea id="planning-goal" rows="4" placeholder="e.g., Add user authentication with JWT tokens, login/logout functionality, and protected routes..."
|
|
3343
|
+
class="input w-full resize-none"></textarea>
|
|
3344
|
+
</div>
|
|
3345
|
+
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
3346
|
+
<p class="text-xs text-blue-700">\u{1F4A1} Be specific about what you want. The AI will explore your codebase and create 3-8 tasks with implementation guidance.</p>
|
|
3347
|
+
</div>
|
|
3348
|
+
</div>
|
|
3349
|
+
<div class="flex justify-end gap-3 mt-6">
|
|
3350
|
+
<button onclick="state.showModal = null; render();"
|
|
3351
|
+
class="btn btn-ghost px-4 py-2.5">Cancel</button>
|
|
3352
|
+
<button onclick="handleStartPlanning()"
|
|
3353
|
+
class="btn px-5 py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium">\u{1F680} Start Planning</button>
|
|
3354
|
+
</div>
|
|
3355
|
+
</div>
|
|
3356
|
+
</div>
|
|
3357
|
+
</div>
|
|
3358
|
+
\`;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
// Planning progress modal
|
|
3362
|
+
if (state.showModal === 'planning') {
|
|
3363
|
+
const outputHtml = state.planningOutput.length > 0
|
|
3364
|
+
? state.planningOutput.map(l => \`<div class="log-line">\${highlightLog(l.text || l)}</div>\`).join('')
|
|
3365
|
+
: '<div class="text-canvas-400 text-sm">Analyzing codebase and generating tasks...</div>';
|
|
3366
|
+
|
|
3367
|
+
return \`
|
|
3368
|
+
<div class="modal-backdrop fixed inset-0 flex items-center justify-center z-50">
|
|
3369
|
+
<div class="modal-content card rounded-xl w-full max-w-3xl mx-4 max-h-[80vh] flex flex-col">
|
|
3370
|
+
<div class="px-6 py-4 border-b border-white/5 flex justify-between items-center flex-shrink-0">
|
|
3371
|
+
<div class="flex items-center gap-3">
|
|
3372
|
+
<span class="text-xl animate-pulse">\u{1F3AF}</span>
|
|
3373
|
+
<div>
|
|
3374
|
+
<h3 class="font-display font-semibold text-canvas-800 text-lg">Planning in Progress</h3>
|
|
3375
|
+
<p class="text-xs text-canvas-500 truncate max-w-[400px]">\${escapeHtml(state.planningGoal)}</p>
|
|
3376
|
+
</div>
|
|
3377
|
+
</div>
|
|
3378
|
+
<button onclick="if(confirm('Cancel planning?')) cancelPlanning();"
|
|
3379
|
+
class="btn btn-ghost px-3 py-1.5 text-sm text-status-failed hover:bg-status-failed/10">
|
|
3380
|
+
\u23F9 Cancel
|
|
3381
|
+
</button>
|
|
3382
|
+
</div>
|
|
3383
|
+
<div class="flex-1 overflow-hidden p-4">
|
|
3384
|
+
<div class="log-container h-full overflow-y-auto" id="planning-log">
|
|
3385
|
+
\${outputHtml}
|
|
3386
|
+
</div>
|
|
3387
|
+
</div>
|
|
3388
|
+
</div>
|
|
3389
|
+
</div>
|
|
3390
|
+
\`;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3440
3393
|
return '';
|
|
3441
3394
|
}
|
|
3442
3395
|
|
|
@@ -3450,114 +3403,55 @@ function renderSidePanel() {
|
|
|
3450
3403
|
const output = state.taskOutput[task.id] || [];
|
|
3451
3404
|
const startTime = state.taskStartTime[task.id];
|
|
3452
3405
|
const lastExec = task.executionHistory?.[task.executionHistory.length - 1];
|
|
3406
|
+
const elapsed = isRunning && startTime ? formatElapsed(startTime) : null;
|
|
3453
3407
|
|
|
3454
3408
|
return \`
|
|
3455
3409
|
<div class="side-panel">
|
|
3456
|
-
<!-- Header -->
|
|
3457
|
-
<div class="side-panel-header">
|
|
3458
|
-
<div class="flex justify-between items-start">
|
|
3459
|
-
<div class="flex-1
|
|
3460
|
-
<h2 class="font-semibold text-canvas-900 text-
|
|
3461
|
-
<div class="flex items-center gap-2 mt-
|
|
3462
|
-
<span class="status-badge status-badge-\${task.status}">
|
|
3410
|
+
<!-- Compact Header -->
|
|
3411
|
+
<div class="side-panel-header" style="padding: 12px 16px;">
|
|
3412
|
+
<div class="flex justify-between items-start gap-2">
|
|
3413
|
+
<div class="flex-1 min-w-0">
|
|
3414
|
+
<h2 class="font-semibold text-canvas-900 text-base leading-tight truncate" title="\${escapeHtml(task.title)}">\${escapeHtml(task.title)}</h2>
|
|
3415
|
+
<div class="flex items-center gap-2 mt-1.5 flex-wrap">
|
|
3416
|
+
<span class="status-badge status-badge-\${task.status}" style="font-size: 11px; padding: 2px 8px;">
|
|
3463
3417
|
<span class="w-1.5 h-1.5 rounded-full bg-current \${isRunning ? 'animate-pulse' : ''}"></span>
|
|
3464
3418
|
\${task.status.replace('_', ' ')}
|
|
3465
3419
|
</span>
|
|
3420
|
+
\${elapsed ? \`<span class="text-xs text-canvas-500">\u23F1 \${elapsed}</span>\` : ''}
|
|
3421
|
+
<span class="text-xs text-canvas-400">\${task.priority} \xB7 \${task.category}</span>
|
|
3466
3422
|
</div>
|
|
3467
3423
|
</div>
|
|
3468
|
-
<div class="flex items-center gap-
|
|
3424
|
+
<div class="flex items-center gap-0.5 flex-shrink-0">
|
|
3425
|
+
\${task.status === 'in_progress' ? \`
|
|
3426
|
+
<button onclick="cancelTask('\${task.id}')" class="btn btn-ghost p-1.5 text-status-failed hover:bg-status-failed/10" title="Stop">
|
|
3427
|
+
\u23F9
|
|
3428
|
+
</button>
|
|
3429
|
+
\` : task.status === 'ready' ? \`
|
|
3430
|
+
<button onclick="runTask('\${task.id}')" class="btn btn-ghost p-1.5 text-status-success hover:bg-status-success/10" title="Run">
|
|
3431
|
+
\u25B6
|
|
3432
|
+
</button>
|
|
3433
|
+
\` : task.status === 'draft' ? \`
|
|
3434
|
+
<button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-ghost p-1.5 text-blue-500 hover:bg-blue-50" title="Move to Ready">
|
|
3435
|
+
\u2192
|
|
3436
|
+
</button>
|
|
3437
|
+
\` : task.status === 'failed' ? \`
|
|
3438
|
+
<button onclick="retryTask('\${task.id}')" class="btn btn-ghost p-1.5 text-canvas-500 hover:bg-canvas-100" title="Retry">
|
|
3439
|
+
\u21BB
|
|
3440
|
+
</button>
|
|
3441
|
+
\` : ''}
|
|
3469
3442
|
<button onclick="state.editingTask = state.tasks.find(t => t.id === '\${task.id}'); state.showModal = 'edit'; render();"
|
|
3470
|
-
class="btn btn-ghost p-
|
|
3443
|
+
class="btn btn-ghost p-1.5 text-canvas-400 hover:text-canvas-600" title="Edit">
|
|
3471
3444
|
\u270F\uFE0F
|
|
3472
3445
|
</button>
|
|
3473
|
-
<button onclick="
|
|
3474
|
-
class="btn btn-ghost p-2 text-canvas-500 hover:text-status-failed" title="Delete">
|
|
3475
|
-
\u{1F5D1}\uFE0F
|
|
3476
|
-
</button>
|
|
3477
|
-
<button onclick="closeSidePanel()" class="btn btn-ghost p-2 text-canvas-500 hover:text-canvas-700" title="Close">
|
|
3446
|
+
<button onclick="closeSidePanel()" class="btn btn-ghost p-1.5 text-canvas-400 hover:text-canvas-600" title="Close">
|
|
3478
3447
|
\u2715
|
|
3479
3448
|
</button>
|
|
3480
3449
|
</div>
|
|
3481
3450
|
</div>
|
|
3482
3451
|
</div>
|
|
3483
3452
|
|
|
3484
|
-
<!--
|
|
3485
|
-
<div class="
|
|
3486
|
-
<p class="text-sm text-canvas-600 leading-relaxed">\${escapeHtml(task.description)}</p>
|
|
3487
|
-
\${task.steps && task.steps.length > 0 ? \`
|
|
3488
|
-
<div class="mt-3">
|
|
3489
|
-
<div class="text-xs font-medium text-canvas-500 mb-2">Steps:</div>
|
|
3490
|
-
<ul class="text-sm text-canvas-600 space-y-1">
|
|
3491
|
-
\${task.steps.map(s => \`<li class="flex gap-2"><span class="text-canvas-400">\u2022</span>\${escapeHtml(s)}</li>\`).join('')}
|
|
3492
|
-
</ul>
|
|
3493
|
-
</div>
|
|
3494
|
-
\` : ''}
|
|
3495
|
-
</div>
|
|
3496
|
-
|
|
3497
|
-
<!-- Task Details Grid -->
|
|
3498
|
-
<div class="details-grid">
|
|
3499
|
-
<div class="details-item">
|
|
3500
|
-
<span class="details-label">Priority</span>
|
|
3501
|
-
<span class="details-value capitalize">\${task.priority}</span>
|
|
3502
|
-
</div>
|
|
3503
|
-
<div class="details-item">
|
|
3504
|
-
<span class="details-label">Category</span>
|
|
3505
|
-
<span class="details-value">\${categoryIcons[task.category] || ''} \${task.category}</span>
|
|
3506
|
-
</div>
|
|
3507
|
-
\${startTime || lastExec ? \`
|
|
3508
|
-
<div class="details-item">
|
|
3509
|
-
<span class="details-label">Started</span>
|
|
3510
|
-
<span class="details-value">\${new Date(startTime || lastExec?.startedAt).toLocaleString()}</span>
|
|
3511
|
-
</div>
|
|
3512
|
-
\` : ''}
|
|
3513
|
-
\${lastExec?.duration ? \`
|
|
3514
|
-
<div class="details-item">
|
|
3515
|
-
<span class="details-label">Duration</span>
|
|
3516
|
-
<span class="details-value">\${Math.round(lastExec.duration / 1000)}s</span>
|
|
3517
|
-
</div>
|
|
3518
|
-
\` : ''}
|
|
3519
|
-
</div>
|
|
3520
|
-
|
|
3521
|
-
<!-- Action Buttons -->
|
|
3522
|
-
<div class="px-5 py-4 border-b border-canvas-200 flex flex-wrap gap-2 flex-shrink-0">
|
|
3523
|
-
\${task.status === 'draft' ? \`
|
|
3524
|
-
<button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-primary px-4 py-2 text-sm">
|
|
3525
|
-
\u2192 Move to Ready
|
|
3526
|
-
</button>
|
|
3527
|
-
\` : ''}
|
|
3528
|
-
\${task.status === 'ready' ? \`
|
|
3529
|
-
<button onclick="runTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3530
|
-
\u25B6 Run Task
|
|
3531
|
-
</button>
|
|
3532
|
-
\` : ''}
|
|
3533
|
-
\${task.status === 'in_progress' ? \`
|
|
3534
|
-
<button onclick="cancelTask('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
|
|
3535
|
-
\u23F9 Stop Attempt
|
|
3536
|
-
</button>
|
|
3537
|
-
\` : ''}
|
|
3538
|
-
\${task.status === 'in_review' ? \`
|
|
3539
|
-
<button onclick="handleMerge('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3540
|
-
\u2713 Merge
|
|
3541
|
-
</button>
|
|
3542
|
-
<button onclick="handleCreatePR('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
|
|
3543
|
-
\u21E1 Create PR
|
|
3544
|
-
</button>
|
|
3545
|
-
<button onclick="openInVSCode('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
|
|
3546
|
-
\u{1F4C2} Open in VS Code
|
|
3547
|
-
</button>
|
|
3548
|
-
<button onclick="handleDiscard('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
|
|
3549
|
-
\u2715 Discard
|
|
3550
|
-
</button>
|
|
3551
|
-
\` : ''}
|
|
3552
|
-
\${task.status === 'failed' ? \`
|
|
3553
|
-
<button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3554
|
-
\u21BB Retry
|
|
3555
|
-
</button>
|
|
3556
|
-
\` : ''}
|
|
3557
|
-
</div>
|
|
3558
|
-
|
|
3559
|
-
<!-- Tabs -->
|
|
3560
|
-
<div class="side-panel-tabs">
|
|
3453
|
+
<!-- Tabs (moved up, right after header) -->
|
|
3454
|
+
<div class="side-panel-tabs" style="padding: 0 12px;">
|
|
3561
3455
|
<div class="side-panel-tab \${state.sidePanelTab === 'logs' ? 'active' : ''}" onclick="state.sidePanelTab = 'logs'; render();">
|
|
3562
3456
|
\u{1F4CB} Logs
|
|
3563
3457
|
</div>
|
|
@@ -3567,9 +3461,9 @@ function renderSidePanel() {
|
|
|
3567
3461
|
</div>
|
|
3568
3462
|
|
|
3569
3463
|
<!-- Tab Content -->
|
|
3570
|
-
<div class="side-panel-body">
|
|
3464
|
+
<div class="side-panel-body" style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
|
|
3571
3465
|
\${state.sidePanelTab === 'logs' ? \`
|
|
3572
|
-
<div class="log-container" id="side-panel-log">
|
|
3466
|
+
<div class="log-container" id="side-panel-log" style="flex: 1; overflow-y: auto;">
|
|
3573
3467
|
\${output.length > 0
|
|
3574
3468
|
? output.map((l, i) => {
|
|
3575
3469
|
const text = l.text || l;
|
|
@@ -3579,34 +3473,62 @@ function renderSidePanel() {
|
|
|
3579
3473
|
}
|
|
3580
3474
|
</div>
|
|
3581
3475
|
\` : \`
|
|
3582
|
-
<div
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3476
|
+
<div style="flex: 1; overflow-y: auto; padding: 16px;">
|
|
3477
|
+
<!-- Description -->
|
|
3478
|
+
<div class="mb-5">
|
|
3479
|
+
<div class="text-xs font-semibold text-canvas-500 uppercase tracking-wide mb-2">Description</div>
|
|
3480
|
+
<div class="text-sm text-canvas-700 leading-relaxed bg-canvas-50 rounded-lg p-3 border border-canvas-200">
|
|
3481
|
+
\${escapeHtml(task.description || 'No description provided.')}
|
|
3587
3482
|
</div>
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3483
|
+
</div>
|
|
3484
|
+
|
|
3485
|
+
<!-- Steps -->
|
|
3486
|
+
\${task.steps && task.steps.length > 0 ? \`
|
|
3487
|
+
<div class="mb-5">
|
|
3488
|
+
<div class="text-xs font-semibold text-canvas-500 uppercase tracking-wide mb-2">Steps (\${task.steps.length})</div>
|
|
3489
|
+
<div class="bg-canvas-50 rounded-lg border border-canvas-200 divide-y divide-canvas-200">
|
|
3490
|
+
\${task.steps.map((step, i) => \`
|
|
3491
|
+
<div class="flex gap-3 p-3 text-sm">
|
|
3492
|
+
<span class="flex-shrink-0 w-5 h-5 rounded-full bg-canvas-200 text-canvas-500 flex items-center justify-center text-xs font-medium">\${i + 1}</span>
|
|
3493
|
+
<span class="text-canvas-700">\${escapeHtml(step)}</span>
|
|
3494
|
+
</div>
|
|
3495
|
+
\`).join('')}
|
|
3496
|
+
</div>
|
|
3591
3497
|
</div>
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3498
|
+
\` : ''}
|
|
3499
|
+
|
|
3500
|
+
<!-- Metadata -->
|
|
3501
|
+
<div class="mb-5 grid grid-cols-2 gap-3">
|
|
3502
|
+
<div class="bg-canvas-50 rounded-lg p-3 border border-canvas-200">
|
|
3503
|
+
<div class="text-xs text-canvas-500 mb-1">Created</div>
|
|
3504
|
+
<div class="text-sm text-canvas-700">\${new Date(task.createdAt).toLocaleDateString()}</div>
|
|
3505
|
+
</div>
|
|
3506
|
+
<div class="bg-canvas-50 rounded-lg p-3 border border-canvas-200">
|
|
3507
|
+
<div class="text-xs text-canvas-500 mb-1">Updated</div>
|
|
3508
|
+
<div class="text-sm text-canvas-700">\${new Date(task.updatedAt).toLocaleDateString()}</div>
|
|
3509
|
+
</div>
|
|
3510
|
+
</div>
|
|
3511
|
+
|
|
3512
|
+
<!-- Execution History -->
|
|
3513
|
+
\${task.executionHistory && task.executionHistory.length > 0 ? \`
|
|
3514
|
+
<div>
|
|
3515
|
+
<div class="text-xs font-semibold text-canvas-500 uppercase tracking-wide mb-2">Execution History</div>
|
|
3516
|
+
<div class="space-y-2">
|
|
3517
|
+
\${task.executionHistory.slice(-5).reverse().map(exec => \`
|
|
3518
|
+
<div class="bg-canvas-50 rounded-lg p-3 border border-canvas-200">
|
|
3519
|
+
<div class="flex justify-between items-center">
|
|
3520
|
+
<span class="text-sm font-medium \${exec.status === 'completed' ? 'text-status-success' : 'text-status-failed'}">
|
|
3521
|
+
\${exec.status === 'completed' ? '\u2713' : '\u2717'} \${exec.status}
|
|
3522
|
+
</span>
|
|
3523
|
+
<span class="text-xs text-canvas-500">\${Math.round(exec.duration / 1000)}s</span>
|
|
3604
3524
|
</div>
|
|
3605
|
-
|
|
3606
|
-
|
|
3525
|
+
<div class="text-xs text-canvas-500 mt-1">\${new Date(exec.startedAt).toLocaleString()}</div>
|
|
3526
|
+
\${exec.error ? \`<div class="text-xs text-status-failed mt-2 bg-status-failed/5 rounded p-2">\${escapeHtml(exec.error)}</div>\` : ''}
|
|
3527
|
+
</div>
|
|
3528
|
+
\`).join('')}
|
|
3607
3529
|
</div>
|
|
3608
|
-
|
|
3609
|
-
|
|
3530
|
+
</div>
|
|
3531
|
+
\` : ''}
|
|
3610
3532
|
</div>
|
|
3611
3533
|
\`}
|
|
3612
3534
|
</div>
|
|
@@ -3743,45 +3665,28 @@ function applyTemplate(templateId) {
|
|
|
3743
3665
|
|
|
3744
3666
|
function handleStartAFK() {
|
|
3745
3667
|
const iterations = parseInt(document.getElementById('afk-iterations').value) || 10;
|
|
3746
|
-
|
|
3747
|
-
startAFK(iterations, concurrent);
|
|
3668
|
+
startAFK(iterations);
|
|
3748
3669
|
state.showModal = null;
|
|
3749
3670
|
render();
|
|
3750
3671
|
}
|
|
3751
3672
|
|
|
3752
|
-
async function
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
closeSidePanel();
|
|
3758
|
-
} catch (e) {
|
|
3759
|
-
showToast('Merge failed: ' + e.message, 'error');
|
|
3673
|
+
async function handleStartPlanning() {
|
|
3674
|
+
const goal = document.getElementById('planning-goal').value;
|
|
3675
|
+
if (!goal.trim()) {
|
|
3676
|
+
showToast('Please enter a goal', 'warning');
|
|
3677
|
+
return;
|
|
3760
3678
|
}
|
|
3761
|
-
}
|
|
3762
|
-
|
|
3763
|
-
async function handleCreatePR(taskId) {
|
|
3764
3679
|
try {
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
if (result.prUrl) {
|
|
3768
|
-
window.open(result.prUrl, '_blank');
|
|
3769
|
-
}
|
|
3770
|
-
closeSidePanel();
|
|
3680
|
+
await startPlanning(goal);
|
|
3681
|
+
// Modal will be shown when planning:started event is received
|
|
3771
3682
|
} catch (e) {
|
|
3772
|
-
showToast('
|
|
3683
|
+
showToast('Failed to start planning: ' + e.message, 'error');
|
|
3773
3684
|
}
|
|
3774
3685
|
}
|
|
3775
3686
|
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
await discardTask(taskId);
|
|
3780
|
-
showToast('Task discarded, returned to Ready', 'warning');
|
|
3781
|
-
closeSidePanel();
|
|
3782
|
-
} catch (e) {
|
|
3783
|
-
showToast('Discard failed: ' + e.message, 'error');
|
|
3784
|
-
}
|
|
3687
|
+
function openPlanningModal() {
|
|
3688
|
+
state.showModal = 'plan-input';
|
|
3689
|
+
render();
|
|
3785
3690
|
}
|
|
3786
3691
|
|
|
3787
3692
|
// Filter tasks based on search query
|
|
@@ -3822,6 +3727,12 @@ function render() {
|
|
|
3822
3727
|
</div>
|
|
3823
3728
|
</div>
|
|
3824
3729
|
<div class="flex items-center gap-2">
|
|
3730
|
+
<button onclick="openPlanningModal();"
|
|
3731
|
+
class="btn px-4 py-2 text-sm bg-blue-500 hover:bg-blue-600 text-white \${state.planning ? 'opacity-50 cursor-not-allowed' : ''}"
|
|
3732
|
+
\${state.planning ? 'disabled' : ''}
|
|
3733
|
+
title="AI Task Planner">
|
|
3734
|
+
\u{1F3AF} \${state.planning ? 'Planning...' : 'Plan'}
|
|
3735
|
+
</button>
|
|
3825
3736
|
<button onclick="state.showModal = 'new'; render();"
|
|
3826
3737
|
class="btn btn-primary px-4 py-2 text-sm">
|
|
3827
3738
|
+ Add Task
|
|
@@ -3845,7 +3756,6 @@ function render() {
|
|
|
3845
3756
|
\${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
|
|
3846
3757
|
\${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
|
|
3847
3758
|
\${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
|
|
3848
|
-
\${renderColumn('in_review', 'In Review', filterTasks(state.tasks))}
|
|
3849
3759
|
\${renderColumn('completed', 'Done', filterTasks(state.tasks))}
|
|
3850
3760
|
\${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
|
|
3851
3761
|
</div>
|
|
@@ -3889,6 +3799,10 @@ window.retryTask = retryTask;
|
|
|
3889
3799
|
window.generateTask = generateTask;
|
|
3890
3800
|
window.startAFK = startAFK;
|
|
3891
3801
|
window.stopAFK = stopAFK;
|
|
3802
|
+
window.startPlanning = startPlanning;
|
|
3803
|
+
window.cancelPlanning = cancelPlanning;
|
|
3804
|
+
window.handleStartPlanning = handleStartPlanning;
|
|
3805
|
+
window.openPlanningModal = openPlanningModal;
|
|
3892
3806
|
window.handleDragStart = handleDragStart;
|
|
3893
3807
|
window.handleDragEnd = handleDragEnd;
|
|
3894
3808
|
window.handleDragOver = handleDragOver;
|
|
@@ -3910,14 +3824,6 @@ window.closeSidePanel = closeSidePanel;
|
|
|
3910
3824
|
window.showTaskMenu = showTaskMenu;
|
|
3911
3825
|
window.filterTasks = filterTasks;
|
|
3912
3826
|
window.scrollSidePanelLog = scrollSidePanelLog;
|
|
3913
|
-
window.mergeTask = mergeTask;
|
|
3914
|
-
window.createPR = createPR;
|
|
3915
|
-
window.discardTask = discardTask;
|
|
3916
|
-
window.handleMerge = handleMerge;
|
|
3917
|
-
window.handleCreatePR = handleCreatePR;
|
|
3918
|
-
window.handleDiscard = handleDiscard;
|
|
3919
|
-
window.openInVSCode = openInVSCode;
|
|
3920
|
-
window.getReviewInfo = getReviewInfo;
|
|
3921
3827
|
|
|
3922
3828
|
// Keyboard shortcuts
|
|
3923
3829
|
document.addEventListener('keydown', (e) => {
|
|
@@ -3997,7 +3903,7 @@ function isGitRepo(dir) {
|
|
|
3997
3903
|
}
|
|
3998
3904
|
function hasGitRemote(dir) {
|
|
3999
3905
|
try {
|
|
4000
|
-
const result =
|
|
3906
|
+
const result = execSync("git remote -v", { cwd: dir, stdio: "pipe" }).toString();
|
|
4001
3907
|
return result.trim().length > 0;
|
|
4002
3908
|
} catch {
|
|
4003
3909
|
return false;
|
|
@@ -4005,7 +3911,7 @@ function hasGitRemote(dir) {
|
|
|
4005
3911
|
}
|
|
4006
3912
|
function detectRemoteType(dir) {
|
|
4007
3913
|
try {
|
|
4008
|
-
const result =
|
|
3914
|
+
const result = execSync("git remote get-url origin", { cwd: dir, stdio: "pipe" }).toString().toLowerCase();
|
|
4009
3915
|
if (result.includes("github.com")) return "github";
|
|
4010
3916
|
if (result.includes("gitlab.com") || result.includes("gitlab")) return "gitlab";
|
|
4011
3917
|
if (result.includes("bitbucket.org") || result.includes("bitbucket")) return "bitbucket";
|
|
@@ -4016,10 +3922,10 @@ function detectRemoteType(dir) {
|
|
|
4016
3922
|
}
|
|
4017
3923
|
}
|
|
4018
3924
|
function initializeGit(dir) {
|
|
4019
|
-
|
|
3925
|
+
execSync("git init", { cwd: dir, stdio: "pipe" });
|
|
4020
3926
|
try {
|
|
4021
|
-
|
|
4022
|
-
|
|
3927
|
+
execSync("git add -A", { cwd: dir, stdio: "pipe" });
|
|
3928
|
+
execSync('git commit -m "Initial commit"', { cwd: dir, stdio: "pipe" });
|
|
4023
3929
|
} catch {
|
|
4024
3930
|
}
|
|
4025
3931
|
}
|