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 +1 -1
- package/public/index.html +205 -2
- package/server.js +38 -5
package/package.json
CHANGED
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-
|
|
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-
|
|
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,
|
|
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
|
|
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 {
|