claude-kanban 0.5.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 +550 -113
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +550 -113
- package/dist/server/index.js.map +1 -1
- package/package.json +10 -2
package/dist/bin/cli.js
CHANGED
|
@@ -533,6 +533,7 @@ var LOGS_DIR = "logs";
|
|
|
533
533
|
var TaskExecutor = class extends EventEmitter {
|
|
534
534
|
projectPath;
|
|
535
535
|
runningTask = null;
|
|
536
|
+
planningSession = null;
|
|
536
537
|
afkMode = false;
|
|
537
538
|
afkIteration = 0;
|
|
538
539
|
afkMaxIterations = 0;
|
|
@@ -658,6 +659,30 @@ ${summary}
|
|
|
658
659
|
}
|
|
659
660
|
return void 0;
|
|
660
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Check if a planning session is running
|
|
664
|
+
*/
|
|
665
|
+
isPlanning() {
|
|
666
|
+
return this.planningSession !== null;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Check if executor is busy (task or planning running)
|
|
670
|
+
*/
|
|
671
|
+
isBusy() {
|
|
672
|
+
return this.runningTask !== null || this.planningSession !== null;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Get planning session output
|
|
676
|
+
*/
|
|
677
|
+
getPlanningOutput() {
|
|
678
|
+
return this.planningSession?.output;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Get planning session goal
|
|
682
|
+
*/
|
|
683
|
+
getPlanningGoal() {
|
|
684
|
+
return this.planningSession?.goal;
|
|
685
|
+
}
|
|
661
686
|
/**
|
|
662
687
|
* Build the prompt for a task - simplified Ralph-style
|
|
663
688
|
*/
|
|
@@ -709,12 +734,225 @@ ${verifyCommands.length > 0 ? "6" : "5"}. Commit your changes with a descriptive
|
|
|
709
734
|
|
|
710
735
|
Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
711
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>`;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
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
|
+
}
|
|
712
950
|
/**
|
|
713
951
|
* Run a task
|
|
714
952
|
*/
|
|
715
953
|
async runTask(taskId) {
|
|
716
|
-
if (this.
|
|
717
|
-
throw new Error("
|
|
954
|
+
if (this.isBusy()) {
|
|
955
|
+
throw new Error("Another operation is in progress. Wait for it to complete or cancel it first.");
|
|
718
956
|
}
|
|
719
957
|
const config = getConfig(this.projectPath);
|
|
720
958
|
const task = getTaskById(this.projectPath, taskId);
|
|
@@ -949,8 +1187,8 @@ Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
|
949
1187
|
if (this.afkMode) {
|
|
950
1188
|
throw new Error("AFK mode already running");
|
|
951
1189
|
}
|
|
952
|
-
if (this.
|
|
953
|
-
throw new Error("Cannot start AFK mode while
|
|
1190
|
+
if (this.isBusy()) {
|
|
1191
|
+
throw new Error("Cannot start AFK mode while another operation is in progress");
|
|
954
1192
|
}
|
|
955
1193
|
this.afkMode = true;
|
|
956
1194
|
this.afkIteration = 0;
|
|
@@ -968,7 +1206,7 @@ Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
|
968
1206
|
this.stopAFKMode();
|
|
969
1207
|
return;
|
|
970
1208
|
}
|
|
971
|
-
if (this.
|
|
1209
|
+
if (this.isBusy()) return;
|
|
972
1210
|
const nextTask = getNextReadyTask(this.projectPath);
|
|
973
1211
|
if (!nextTask) {
|
|
974
1212
|
this.stopAFKMode();
|
|
@@ -1010,7 +1248,7 @@ Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
|
1010
1248
|
};
|
|
1011
1249
|
}
|
|
1012
1250
|
/**
|
|
1013
|
-
* Cancel running task and stop AFK mode
|
|
1251
|
+
* Cancel running task/planning and stop AFK mode
|
|
1014
1252
|
*/
|
|
1015
1253
|
cancelAll() {
|
|
1016
1254
|
if (this.runningTask) {
|
|
@@ -1020,6 +1258,13 @@ Focus only on this task. When done, output: <promise>COMPLETE</promise>`;
|
|
|
1020
1258
|
}
|
|
1021
1259
|
this.runningTask = null;
|
|
1022
1260
|
}
|
|
1261
|
+
if (this.planningSession) {
|
|
1262
|
+
try {
|
|
1263
|
+
this.planningSession.process.kill("SIGKILL");
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
this.planningSession = null;
|
|
1267
|
+
}
|
|
1023
1268
|
this.stopAFKMode();
|
|
1024
1269
|
}
|
|
1025
1270
|
};
|
|
@@ -1444,6 +1689,11 @@ async function createServer(projectPath, port) {
|
|
|
1444
1689
|
executor.on("task:failed", (data) => io.emit("task:failed", data));
|
|
1445
1690
|
executor.on("task:cancelled", (data) => io.emit("task:cancelled", data));
|
|
1446
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));
|
|
1447
1697
|
app.get("/api/tasks", (_req, res) => {
|
|
1448
1698
|
try {
|
|
1449
1699
|
const tasks = getAllTasks(projectPath);
|
|
@@ -1658,6 +1908,40 @@ async function createServer(projectPath, port) {
|
|
|
1658
1908
|
res.status(500).json({ error: String(error) });
|
|
1659
1909
|
}
|
|
1660
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
|
+
});
|
|
1661
1945
|
app.get("/api/running", (_req, res) => {
|
|
1662
1946
|
try {
|
|
1663
1947
|
const taskId = executor.getRunningTaskId();
|
|
@@ -1671,7 +1955,8 @@ async function createServer(projectPath, port) {
|
|
|
1671
1955
|
const counts = getTaskCounts(projectPath);
|
|
1672
1956
|
const running = executor.isRunning() ? 1 : 0;
|
|
1673
1957
|
const afk = executor.getAFKStatus();
|
|
1674
|
-
|
|
1958
|
+
const planning = executor.isPlanning();
|
|
1959
|
+
res.json({ counts, running, afk, planning });
|
|
1675
1960
|
} catch (error) {
|
|
1676
1961
|
res.status(500).json({ error: String(error) });
|
|
1677
1962
|
}
|
|
@@ -1698,8 +1983,11 @@ async function createServer(projectPath, port) {
|
|
|
1698
1983
|
tasks: getAllTasks(projectPath),
|
|
1699
1984
|
running: runningIds,
|
|
1700
1985
|
afk: executor.getAFKStatus(),
|
|
1701
|
-
taskLogs
|
|
1986
|
+
taskLogs,
|
|
1702
1987
|
// Include logs for running task
|
|
1988
|
+
planning: executor.isPlanning(),
|
|
1989
|
+
planningGoal: executor.getPlanningGoal() || null,
|
|
1990
|
+
planningOutput: executor.getPlanningOutput() || []
|
|
1703
1991
|
});
|
|
1704
1992
|
socket.on("get-logs", (taskId) => {
|
|
1705
1993
|
const logs = executor.getTaskLog(taskId);
|
|
@@ -1929,7 +2217,7 @@ function getClientHTML() {
|
|
|
1929
2217
|
transition: all 0.2s var(--ease-out-expo);
|
|
1930
2218
|
}
|
|
1931
2219
|
.side-panel-header {
|
|
1932
|
-
padding: 16px
|
|
2220
|
+
padding: 12px 16px;
|
|
1933
2221
|
border-bottom: 1px solid #e5e5e5;
|
|
1934
2222
|
flex-shrink: 0;
|
|
1935
2223
|
}
|
|
@@ -1947,7 +2235,8 @@ function getClientHTML() {
|
|
|
1947
2235
|
flex-shrink: 0;
|
|
1948
2236
|
}
|
|
1949
2237
|
.side-panel-tab {
|
|
1950
|
-
padding:
|
|
2238
|
+
padding: 10px 14px;
|
|
2239
|
+
font-size: 13px;
|
|
1951
2240
|
color: #737373;
|
|
1952
2241
|
cursor: pointer;
|
|
1953
2242
|
border-bottom: 2px solid transparent;
|
|
@@ -2240,6 +2529,10 @@ let state = {
|
|
|
2240
2529
|
logFullscreen: false,
|
|
2241
2530
|
closedTabs: new Set(),
|
|
2242
2531
|
sidePanel: null, // task id for side panel
|
|
2532
|
+
// Planning state
|
|
2533
|
+
planning: false,
|
|
2534
|
+
planningGoal: '',
|
|
2535
|
+
planningOutput: [],
|
|
2243
2536
|
sidePanelTab: 'logs', // 'logs' or 'details'
|
|
2244
2537
|
darkMode: localStorage.getItem('darkMode') === 'true', // Add dark mode state
|
|
2245
2538
|
};
|
|
@@ -2334,6 +2627,14 @@ socket.on('init', (data) => {
|
|
|
2334
2627
|
state.running = data.running;
|
|
2335
2628
|
state.afk = data.afk;
|
|
2336
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
|
+
|
|
2337
2638
|
// Load persisted logs for running tasks
|
|
2338
2639
|
if (data.taskLogs) {
|
|
2339
2640
|
for (const [taskId, logs] of Object.entries(data.taskLogs)) {
|
|
@@ -2438,6 +2739,48 @@ socket.on('afk:status', (status) => {
|
|
|
2438
2739
|
render();
|
|
2439
2740
|
});
|
|
2440
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
|
+
|
|
2441
2784
|
// Load templates
|
|
2442
2785
|
fetch('/api/templates').then(r => r.json()).then(data => {
|
|
2443
2786
|
state.templates = data.templates;
|
|
@@ -2517,6 +2860,18 @@ async function stopAFK() {
|
|
|
2517
2860
|
await fetch('/api/afk/stop', { method: 'POST' });
|
|
2518
2861
|
}
|
|
2519
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
|
+
|
|
2520
2875
|
// Enhanced Drag and drop
|
|
2521
2876
|
let draggedTask = null;
|
|
2522
2877
|
let draggedElement = null;
|
|
@@ -2661,6 +3016,11 @@ function openSidePanel(taskId) {
|
|
|
2661
3016
|
state.activeTab = taskId;
|
|
2662
3017
|
state.closedTabs.delete(taskId);
|
|
2663
3018
|
|
|
3019
|
+
// Auto-switch to logs tab if task is running
|
|
3020
|
+
if (state.running.includes(taskId)) {
|
|
3021
|
+
state.sidePanelTab = 'logs';
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2664
3024
|
// Request logs from server if not already loaded
|
|
2665
3025
|
if (!state.taskOutput[taskId] || state.taskOutput[taskId].length === 0) {
|
|
2666
3026
|
socket.emit('get-logs', taskId);
|
|
@@ -2965,6 +3325,71 @@ function renderModal() {
|
|
|
2965
3325
|
\`;
|
|
2966
3326
|
}
|
|
2967
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
|
+
|
|
2968
3393
|
return '';
|
|
2969
3394
|
}
|
|
2970
3395
|
|
|
@@ -2978,100 +3403,55 @@ function renderSidePanel() {
|
|
|
2978
3403
|
const output = state.taskOutput[task.id] || [];
|
|
2979
3404
|
const startTime = state.taskStartTime[task.id];
|
|
2980
3405
|
const lastExec = task.executionHistory?.[task.executionHistory.length - 1];
|
|
3406
|
+
const elapsed = isRunning && startTime ? formatElapsed(startTime) : null;
|
|
2981
3407
|
|
|
2982
3408
|
return \`
|
|
2983
3409
|
<div class="side-panel">
|
|
2984
|
-
<!-- Header -->
|
|
2985
|
-
<div class="side-panel-header">
|
|
2986
|
-
<div class="flex justify-between items-start">
|
|
2987
|
-
<div class="flex-1
|
|
2988
|
-
<h2 class="font-semibold text-canvas-900 text-
|
|
2989
|
-
<div class="flex items-center gap-2 mt-
|
|
2990
|
-
<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;">
|
|
2991
3417
|
<span class="w-1.5 h-1.5 rounded-full bg-current \${isRunning ? 'animate-pulse' : ''}"></span>
|
|
2992
3418
|
\${task.status.replace('_', ' ')}
|
|
2993
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>
|
|
2994
3422
|
</div>
|
|
2995
3423
|
</div>
|
|
2996
|
-
<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
|
+
\` : ''}
|
|
2997
3442
|
<button onclick="state.editingTask = state.tasks.find(t => t.id === '\${task.id}'); state.showModal = 'edit'; render();"
|
|
2998
|
-
class="btn btn-ghost p-
|
|
3443
|
+
class="btn btn-ghost p-1.5 text-canvas-400 hover:text-canvas-600" title="Edit">
|
|
2999
3444
|
\u270F\uFE0F
|
|
3000
3445
|
</button>
|
|
3001
|
-
<button onclick="
|
|
3002
|
-
class="btn btn-ghost p-2 text-canvas-500 hover:text-status-failed" title="Delete">
|
|
3003
|
-
\u{1F5D1}\uFE0F
|
|
3004
|
-
</button>
|
|
3005
|
-
<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">
|
|
3006
3447
|
\u2715
|
|
3007
3448
|
</button>
|
|
3008
3449
|
</div>
|
|
3009
3450
|
</div>
|
|
3010
3451
|
</div>
|
|
3011
3452
|
|
|
3012
|
-
<!--
|
|
3013
|
-
<div class="
|
|
3014
|
-
<p class="text-sm text-canvas-600 leading-relaxed">\${escapeHtml(task.description)}</p>
|
|
3015
|
-
\${task.steps && task.steps.length > 0 ? \`
|
|
3016
|
-
<div class="mt-3">
|
|
3017
|
-
<div class="text-xs font-medium text-canvas-500 mb-2">Steps:</div>
|
|
3018
|
-
<ul class="text-sm text-canvas-600 space-y-1">
|
|
3019
|
-
\${task.steps.map(s => \`<li class="flex gap-2"><span class="text-canvas-400">\u2022</span>\${escapeHtml(s)}</li>\`).join('')}
|
|
3020
|
-
</ul>
|
|
3021
|
-
</div>
|
|
3022
|
-
\` : ''}
|
|
3023
|
-
</div>
|
|
3024
|
-
|
|
3025
|
-
<!-- Task Details Grid -->
|
|
3026
|
-
<div class="details-grid">
|
|
3027
|
-
<div class="details-item">
|
|
3028
|
-
<span class="details-label">Priority</span>
|
|
3029
|
-
<span class="details-value capitalize">\${task.priority}</span>
|
|
3030
|
-
</div>
|
|
3031
|
-
<div class="details-item">
|
|
3032
|
-
<span class="details-label">Category</span>
|
|
3033
|
-
<span class="details-value">\${categoryIcons[task.category] || ''} \${task.category}</span>
|
|
3034
|
-
</div>
|
|
3035
|
-
\${startTime || lastExec ? \`
|
|
3036
|
-
<div class="details-item">
|
|
3037
|
-
<span class="details-label">Started</span>
|
|
3038
|
-
<span class="details-value">\${new Date(startTime || lastExec?.startedAt).toLocaleString()}</span>
|
|
3039
|
-
</div>
|
|
3040
|
-
\` : ''}
|
|
3041
|
-
\${lastExec?.duration ? \`
|
|
3042
|
-
<div class="details-item">
|
|
3043
|
-
<span class="details-label">Duration</span>
|
|
3044
|
-
<span class="details-value">\${Math.round(lastExec.duration / 1000)}s</span>
|
|
3045
|
-
</div>
|
|
3046
|
-
\` : ''}
|
|
3047
|
-
</div>
|
|
3048
|
-
|
|
3049
|
-
<!-- Action Buttons -->
|
|
3050
|
-
<div class="px-5 py-4 border-b border-canvas-200 flex flex-wrap gap-2 flex-shrink-0">
|
|
3051
|
-
\${task.status === 'draft' ? \`
|
|
3052
|
-
<button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-primary px-4 py-2 text-sm">
|
|
3053
|
-
\u2192 Move to Ready
|
|
3054
|
-
</button>
|
|
3055
|
-
\` : ''}
|
|
3056
|
-
\${task.status === 'ready' ? \`
|
|
3057
|
-
<button onclick="runTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3058
|
-
\u25B6 Run Task
|
|
3059
|
-
</button>
|
|
3060
|
-
\` : ''}
|
|
3061
|
-
\${task.status === 'in_progress' ? \`
|
|
3062
|
-
<button onclick="cancelTask('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
|
|
3063
|
-
\u23F9 Stop Attempt
|
|
3064
|
-
</button>
|
|
3065
|
-
\` : ''}
|
|
3066
|
-
\${task.status === 'failed' ? \`
|
|
3067
|
-
<button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3068
|
-
\u21BB Retry
|
|
3069
|
-
</button>
|
|
3070
|
-
\` : ''}
|
|
3071
|
-
</div>
|
|
3072
|
-
|
|
3073
|
-
<!-- Tabs -->
|
|
3074
|
-
<div class="side-panel-tabs">
|
|
3453
|
+
<!-- Tabs (moved up, right after header) -->
|
|
3454
|
+
<div class="side-panel-tabs" style="padding: 0 12px;">
|
|
3075
3455
|
<div class="side-panel-tab \${state.sidePanelTab === 'logs' ? 'active' : ''}" onclick="state.sidePanelTab = 'logs'; render();">
|
|
3076
3456
|
\u{1F4CB} Logs
|
|
3077
3457
|
</div>
|
|
@@ -3081,9 +3461,9 @@ function renderSidePanel() {
|
|
|
3081
3461
|
</div>
|
|
3082
3462
|
|
|
3083
3463
|
<!-- Tab Content -->
|
|
3084
|
-
<div class="side-panel-body">
|
|
3464
|
+
<div class="side-panel-body" style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
|
|
3085
3465
|
\${state.sidePanelTab === 'logs' ? \`
|
|
3086
|
-
<div class="log-container" id="side-panel-log">
|
|
3466
|
+
<div class="log-container" id="side-panel-log" style="flex: 1; overflow-y: auto;">
|
|
3087
3467
|
\${output.length > 0
|
|
3088
3468
|
? output.map((l, i) => {
|
|
3089
3469
|
const text = l.text || l;
|
|
@@ -3093,34 +3473,62 @@ function renderSidePanel() {
|
|
|
3093
3473
|
}
|
|
3094
3474
|
</div>
|
|
3095
3475
|
\` : \`
|
|
3096
|
-
<div
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
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.')}
|
|
3482
|
+
</div>
|
|
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>
|
|
3497
|
+
</div>
|
|
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>
|
|
3101
3505
|
</div>
|
|
3102
|
-
<div class="
|
|
3103
|
-
<div class="text-xs
|
|
3104
|
-
<div>\${new Date(task.updatedAt).
|
|
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>
|
|
3105
3509
|
</div>
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
<
|
|
3117
|
-
|
|
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>
|
|
3118
3524
|
</div>
|
|
3119
|
-
|
|
3120
|
-
|
|
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('')}
|
|
3121
3529
|
</div>
|
|
3122
|
-
|
|
3123
|
-
|
|
3530
|
+
</div>
|
|
3531
|
+
\` : ''}
|
|
3124
3532
|
</div>
|
|
3125
3533
|
\`}
|
|
3126
3534
|
</div>
|
|
@@ -3262,6 +3670,25 @@ function handleStartAFK() {
|
|
|
3262
3670
|
render();
|
|
3263
3671
|
}
|
|
3264
3672
|
|
|
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;
|
|
3678
|
+
}
|
|
3679
|
+
try {
|
|
3680
|
+
await startPlanning(goal);
|
|
3681
|
+
// Modal will be shown when planning:started event is received
|
|
3682
|
+
} catch (e) {
|
|
3683
|
+
showToast('Failed to start planning: ' + e.message, 'error');
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
function openPlanningModal() {
|
|
3688
|
+
state.showModal = 'plan-input';
|
|
3689
|
+
render();
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3265
3692
|
// Filter tasks based on search query
|
|
3266
3693
|
function filterTasks(tasks) {
|
|
3267
3694
|
if (!state.searchQuery) return tasks;
|
|
@@ -3300,6 +3727,12 @@ function render() {
|
|
|
3300
3727
|
</div>
|
|
3301
3728
|
</div>
|
|
3302
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>
|
|
3303
3736
|
<button onclick="state.showModal = 'new'; render();"
|
|
3304
3737
|
class="btn btn-primary px-4 py-2 text-sm">
|
|
3305
3738
|
+ Add Task
|
|
@@ -3366,6 +3799,10 @@ window.retryTask = retryTask;
|
|
|
3366
3799
|
window.generateTask = generateTask;
|
|
3367
3800
|
window.startAFK = startAFK;
|
|
3368
3801
|
window.stopAFK = stopAFK;
|
|
3802
|
+
window.startPlanning = startPlanning;
|
|
3803
|
+
window.cancelPlanning = cancelPlanning;
|
|
3804
|
+
window.handleStartPlanning = handleStartPlanning;
|
|
3805
|
+
window.openPlanningModal = openPlanningModal;
|
|
3369
3806
|
window.handleDragStart = handleDragStart;
|
|
3370
3807
|
window.handleDragEnd = handleDragEnd;
|
|
3371
3808
|
window.handleDragOver = handleDragOver;
|