claude-code-kanban 1.13.0 → 1.15.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.15.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
@@ -206,7 +206,7 @@ function loadSessionMetadata() {
206
206
  console.error('Error loading session metadata:', e);
207
207
  }
208
208
 
209
- // For team sessions with no JSONL match, try team config for project path
209
+ // For team sessions with no JSONL match, resolve from team config + parent session
210
210
  if (existsSync(TASKS_DIR)) {
211
211
  const taskDirs = readdirSync(TASKS_DIR, { withFileTypes: true })
212
212
  .filter(d => d.isDirectory());
@@ -214,11 +214,18 @@ function loadSessionMetadata() {
214
214
  if (!metadata[dir.name]) {
215
215
  const teamConfig = loadTeamConfig(dir.name);
216
216
  if (teamConfig) {
217
+ const parentMeta = teamConfig.leadSessionId ? metadata[teamConfig.leadSessionId] : null;
218
+ const leadMember = teamConfig.members?.find(m => m.agentId === teamConfig.leadAgentId) || teamConfig.members?.[0];
219
+ const project = parentMeta?.project || leadMember?.cwd || teamConfig.working_dir || null;
220
+
217
221
  metadata[dir.name] = {
218
- customTitle: null,
219
- slug: null,
220
- project: teamConfig.working_dir || null,
221
- jsonlPath: null
222
+ customTitle: parentMeta?.customTitle || null,
223
+ slug: parentMeta?.slug || null,
224
+ project,
225
+ jsonlPath: parentMeta?.jsonlPath || null,
226
+ description: parentMeta?.description || teamConfig.description || null,
227
+ gitBranch: parentMeta?.gitBranch || null,
228
+ created: parentMeta?.created || null
222
229
  };
223
230
  }
224
231
  }
@@ -532,6 +539,32 @@ app.post('/api/tasks/:sessionId/:taskId/note', async (req, res) => {
532
539
  }
533
540
  });
534
541
 
542
+ // API: Update task fields (subject, description)
543
+ app.put('/api/tasks/:sessionId/:taskId', async (req, res) => {
544
+ try {
545
+ const { sessionId, taskId } = req.params;
546
+ const { subject, description } = req.body;
547
+
548
+ const taskPath = path.join(TASKS_DIR, sessionId, `${taskId}.json`);
549
+
550
+ if (!existsSync(taskPath)) {
551
+ return res.status(404).json({ error: 'Task not found' });
552
+ }
553
+
554
+ const task = JSON.parse(await fs.readFile(taskPath, 'utf8'));
555
+
556
+ if (subject !== undefined) task.subject = subject;
557
+ if (description !== undefined) task.description = description;
558
+
559
+ await fs.writeFile(taskPath, JSON.stringify(task, null, 2));
560
+
561
+ res.json({ success: true, task });
562
+ } catch (error) {
563
+ console.error('Error updating task:', error);
564
+ res.status(500).json({ error: 'Failed to update task' });
565
+ }
566
+ });
567
+
535
568
  // API: Delete a task
536
569
  app.delete('/api/tasks/:sessionId/:taskId', async (req, res) => {
537
570
  try {