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 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.isRunning()) {
717
- throw new Error("A task is already running. Wait for it to complete or cancel it first.");
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.isRunning()) {
953
- throw new Error("Cannot start AFK mode while a task is running");
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.isRunning()) return;
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
- res.json({ counts, running, afk });
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 20px;
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: 12px 16px;
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 pr-4">
2988
- <h2 class="font-semibold text-canvas-900 text-lg leading-tight">\${escapeHtml(task.title)}</h2>
2989
- <div class="flex items-center gap-2 mt-2">
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-1">
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-2 text-canvas-500 hover:text-canvas-700" title="Edit">
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="if(confirm('Delete this task?')) { deleteTask('\${task.id}'); closeSidePanel(); }"
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
- <!-- Description -->
3013
- <div class="px-5 py-4 border-b border-canvas-200 flex-shrink-0">
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 class="p-5">
3097
- <div class="text-sm text-canvas-600">
3098
- <div class="mb-4">
3099
- <div class="text-xs font-medium text-canvas-500 mb-1">Created</div>
3100
- <div>\${new Date(task.createdAt).toLocaleString()}</div>
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="mb-4">
3103
- <div class="text-xs font-medium text-canvas-500 mb-1">Last Updated</div>
3104
- <div>\${new Date(task.updatedAt).toLocaleString()}</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>
3105
3509
  </div>
3106
- \${task.executionHistory && task.executionHistory.length > 0 ? \`
3107
- <div>
3108
- <div class="text-xs font-medium text-canvas-500 mb-2">Execution History</div>
3109
- <div class="space-y-2">
3110
- \${task.executionHistory.slice(-5).reverse().map(exec => \`
3111
- <div class="bg-canvas-100 rounded p-2 text-xs">
3112
- <div class="flex justify-between">
3113
- <span class="\${exec.status === 'completed' ? 'text-status-success' : 'text-status-failed'}">\${exec.status}</span>
3114
- <span class="text-canvas-500">\${Math.round(exec.duration / 1000)}s</span>
3115
- </div>
3116
- <div class="text-canvas-500 mt-1">\${new Date(exec.startedAt).toLocaleString()}</div>
3117
- \${exec.error ? \`<div class="text-status-failed mt-1">\${escapeHtml(exec.error)}</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>
3118
3524
  </div>
3119
- \`).join('')}
3120
- </div>
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
- </div>
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;