claude-code-kanban 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -936,6 +936,34 @@
936
936
  font-family: var(--serif);
937
937
  font-size: 22px;
938
938
  line-height: 1.4;
939
+ cursor: pointer;
940
+ padding: 2px 4px;
941
+ margin: -2px -4px;
942
+ border-radius: 4px;
943
+ border: 1px solid transparent;
944
+ transition: border-color 0.15s ease;
945
+ }
946
+
947
+ .detail-title:hover {
948
+ border-color: var(--border);
949
+ }
950
+
951
+ .detail-title-input {
952
+ font-family: var(--serif);
953
+ font-size: 22px;
954
+ line-height: 1.4;
955
+ width: 100%;
956
+ padding: 2px 4px;
957
+ margin: -2px -4px;
958
+ background: var(--bg-elevated);
959
+ border: 1px solid var(--accent);
960
+ border-radius: 4px;
961
+ color: var(--text-primary);
962
+ box-shadow: 0 0 0 2px var(--accent-dim);
963
+ }
964
+
965
+ .detail-title-input:focus {
966
+ outline: none;
939
967
  }
940
968
 
941
969
  .detail-status {
@@ -1011,6 +1039,79 @@
1011
1039
  font-size: 14px;
1012
1040
  line-height: 1.7;
1013
1041
  color: var(--text-secondary);
1042
+ cursor: pointer;
1043
+ padding: 4px 6px;
1044
+ margin: -4px -6px;
1045
+ border-radius: 4px;
1046
+ border: 1px solid transparent;
1047
+ transition: border-color 0.15s ease;
1048
+ }
1049
+
1050
+ .detail-desc:hover {
1051
+ border-color: var(--border);
1052
+ }
1053
+
1054
+ .detail-desc-textarea {
1055
+ width: 100%;
1056
+ min-height: 120px;
1057
+ padding: 8px 10px;
1058
+ margin: -4px -6px;
1059
+ background: var(--bg-elevated);
1060
+ border: 1px solid var(--accent);
1061
+ border-radius: 4px;
1062
+ color: var(--text-primary);
1063
+ font-family: var(--mono);
1064
+ font-size: 13px;
1065
+ line-height: 1.6;
1066
+ resize: vertical;
1067
+ box-shadow: 0 0 0 2px var(--accent-dim);
1068
+ }
1069
+
1070
+ .detail-desc-textarea:focus {
1071
+ outline: none;
1072
+ }
1073
+
1074
+ .edit-actions {
1075
+ display: flex;
1076
+ gap: 8px;
1077
+ justify-content: flex-end;
1078
+ margin-top: 8px;
1079
+ }
1080
+
1081
+ .edit-actions button {
1082
+ padding: 6px 14px;
1083
+ border: none;
1084
+ border-radius: 4px;
1085
+ font-family: var(--mono);
1086
+ font-size: 11px;
1087
+ font-weight: 500;
1088
+ cursor: pointer;
1089
+ transition: all 0.15s ease;
1090
+ }
1091
+
1092
+ .edit-save {
1093
+ background: var(--accent);
1094
+ color: white;
1095
+ }
1096
+
1097
+ .edit-save:hover {
1098
+ filter: brightness(1.1);
1099
+ }
1100
+
1101
+ .edit-cancel {
1102
+ background: var(--bg-elevated);
1103
+ color: var(--text-secondary);
1104
+ border: 1px solid var(--border) !important;
1105
+ }
1106
+
1107
+ .edit-cancel:hover {
1108
+ color: var(--text-primary);
1109
+ }
1110
+
1111
+ .detail-deps {
1112
+ font-size: 14px;
1113
+ line-height: 1.7;
1114
+ color: var(--text-secondary);
1014
1115
  }
1015
1116
 
1016
1117
  .detail-desc pre {
@@ -2806,7 +2907,7 @@
2806
2907
 
2807
2908
  <div class="detail-section">
2808
2909
  <div class="detail-label">Blocked By</div>
2809
- <div class="detail-desc">
2910
+ <div class="detail-deps">
2810
2911
  ${task.blockedBy && task.blockedBy.length > 0
2811
2912
  ? `<div class="detail-box blocked"><strong>Blocked by:</strong> ${task.blockedBy.map(id => '#' + id).join(', ')}</div>`
2812
2913
  : '<em style="color: var(--text-muted); font-size: 13px;">No dependencies</em>'}
@@ -2815,7 +2916,7 @@
2815
2916
 
2816
2917
  <div class="detail-section">
2817
2918
  <div class="detail-label">Blocks</div>
2818
- <div class="detail-desc">
2919
+ <div class="detail-deps">
2819
2920
  ${task.blocks && task.blocks.length > 0
2820
2921
  ? `<div class="detail-box blocks"><strong>Blocks:</strong> ${task.blocks.map(id => '#' + id).join(', ')}</div>`
2821
2922
  : '<em style="color: var(--text-muted); font-size: 13px;">No tasks blocked</em>'}
@@ -2835,6 +2936,108 @@
2835
2936
  const deleteBtn = document.getElementById('delete-task-btn');
2836
2937
  deleteBtn.style.display = '';
2837
2938
  deleteBtn.onclick = () => deleteTask(task.id, actualSessionId);
2939
+
2940
+ // Setup inline editing
2941
+ const titleEl = detailContent.querySelector('.detail-title');
2942
+ if (titleEl) {
2943
+ titleEl.onclick = () => editTitle(titleEl, task, actualSessionId);
2944
+ }
2945
+
2946
+ const descEl = detailContent.querySelector('.detail-desc');
2947
+ if (descEl) {
2948
+ descEl.onclick = () => editDescription(descEl, task, actualSessionId);
2949
+ }
2950
+ }
2951
+
2952
+ function editTitle(titleEl, task, sessionId) {
2953
+ if (titleEl.querySelector('input')) return;
2954
+ const input = document.createElement('input');
2955
+ input.type = 'text';
2956
+ input.className = 'detail-title-input';
2957
+ input.value = task.subject;
2958
+
2959
+ titleEl.replaceWith(input);
2960
+ input.focus();
2961
+ input.select();
2962
+
2963
+ const save = async () => {
2964
+ const val = input.value.trim();
2965
+ if (val && val !== task.subject) {
2966
+ await saveTaskField(task.id, sessionId, 'subject', val);
2967
+ } else {
2968
+ showTaskDetail(task.id, sessionId);
2969
+ }
2970
+ };
2971
+
2972
+ input.onkeydown = (e) => {
2973
+ if (e.key === 'Enter') { e.preventDefault(); save(); }
2974
+ if (e.key === 'Escape') showTaskDetail(task.id, sessionId);
2975
+ };
2976
+ input.onblur = () => save();
2977
+ }
2978
+
2979
+ function editDescription(descEl, task, sessionId) {
2980
+ if (descEl.querySelector('textarea')) return;
2981
+ const wrapper = document.createElement('div');
2982
+ const textarea = document.createElement('textarea');
2983
+ textarea.className = 'detail-desc-textarea';
2984
+ textarea.value = task.description || '';
2985
+ textarea.rows = Math.max(5, (task.description || '').split('\n').length + 2);
2986
+
2987
+ const actions = document.createElement('div');
2988
+ actions.className = 'edit-actions';
2989
+
2990
+ const saveBtn = document.createElement('button');
2991
+ saveBtn.className = 'edit-save';
2992
+ saveBtn.textContent = 'Save';
2993
+
2994
+ const cancelBtn = document.createElement('button');
2995
+ cancelBtn.className = 'edit-cancel';
2996
+ cancelBtn.textContent = 'Cancel';
2997
+
2998
+ actions.append(cancelBtn, saveBtn);
2999
+ wrapper.append(textarea, actions);
3000
+ descEl.replaceWith(wrapper);
3001
+ textarea.focus();
3002
+
3003
+ const save = async () => {
3004
+ const val = textarea.value;
3005
+ if (val !== (task.description || '')) {
3006
+ await saveTaskField(task.id, sessionId, 'description', val);
3007
+ } else {
3008
+ showTaskDetail(task.id, sessionId);
3009
+ }
3010
+ };
3011
+
3012
+ saveBtn.onclick = save;
3013
+ cancelBtn.onclick = () => showTaskDetail(task.id, sessionId);
3014
+ textarea.onkeydown = (e) => {
3015
+ if (e.key === 'Escape') showTaskDetail(task.id, sessionId);
3016
+ };
3017
+ }
3018
+
3019
+ async function saveTaskField(taskId, sessionId, field, value) {
3020
+ try {
3021
+ const res = await fetch(`/api/tasks/${sessionId}/${taskId}`, {
3022
+ method: 'PUT',
3023
+ headers: { 'Content-Type': 'application/json' },
3024
+ body: JSON.stringify({ [field]: value })
3025
+ });
3026
+
3027
+ if (res.ok) {
3028
+ lastCurrentTasksHash = null;
3029
+ if (viewMode === 'all') {
3030
+ const tasksRes = await fetch('/api/tasks/all');
3031
+ currentTasks = await tasksRes.json();
3032
+ renderKanban();
3033
+ } else {
3034
+ await fetchTasks(sessionId);
3035
+ }
3036
+ showTaskDetail(taskId, sessionId);
3037
+ }
3038
+ } catch (error) {
3039
+ console.error('Failed to update task:', error);
3040
+ }
2838
3041
  }
2839
3042
 
2840
3043
  async function addNote(event, taskId, sessionId) {
package/server.js CHANGED
@@ -532,6 +532,32 @@ app.post('/api/tasks/:sessionId/:taskId/note', async (req, res) => {
532
532
  }
533
533
  });
534
534
 
535
+ // API: Update task fields (subject, description)
536
+ app.put('/api/tasks/:sessionId/:taskId', async (req, res) => {
537
+ try {
538
+ const { sessionId, taskId } = req.params;
539
+ const { subject, description } = req.body;
540
+
541
+ const taskPath = path.join(TASKS_DIR, sessionId, `${taskId}.json`);
542
+
543
+ if (!existsSync(taskPath)) {
544
+ return res.status(404).json({ error: 'Task not found' });
545
+ }
546
+
547
+ const task = JSON.parse(await fs.readFile(taskPath, 'utf8'));
548
+
549
+ if (subject !== undefined) task.subject = subject;
550
+ if (description !== undefined) task.description = description;
551
+
552
+ await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
553
+
554
+ res.json({ success: true, task });
555
+ } catch (error) {
556
+ console.error('Error updating task:', error);
557
+ res.status(500).json({ error: 'Failed to update task' });
558
+ }
559
+ });
560
+
535
561
  // API: Delete a task
536
562
  app.delete('/api/tasks/:sessionId/:taskId', async (req, res) => {
537
563
  try {