claude-controller 0.1.0 → 0.1.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/package.json +7 -3
- package/web/static/app.js +42 -14
- package/web/static/styles.css +1 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-controller",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Claude Code headless daemon controller — FIFO-based async task dispatch, Git Worktree isolation, auto-checkpointing, and a web dashboard",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "choiwon",
|
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
},
|
|
57
57
|
"repository": {
|
|
58
58
|
"type": "git",
|
|
59
|
-
"url": ""
|
|
60
|
-
}
|
|
59
|
+
"url": "git+https://github.com/k984530/controller.git"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/k984530/controller/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/k984530/controller#readme"
|
|
61
65
|
}
|
package/web/static/app.js
CHANGED
|
@@ -132,7 +132,8 @@ function _renderSessionItem(s) {
|
|
|
132
132
|
const costLabel = s.cost_usd != null ? '$' + Number(s.cost_usd).toFixed(4) : '';
|
|
133
133
|
const timeLabel = s.timestamp ? s.timestamp.replace(/^\d{4}-/, '') : '';
|
|
134
134
|
return '<div class="session-item" data-sid="' + escapeHtml(s.session_id)
|
|
135
|
-
+ '" data-prompt="' + escapeHtml(s.prompt || '')
|
|
135
|
+
+ '" data-prompt="' + escapeHtml(s.prompt || '')
|
|
136
|
+
+ '" data-cwd="' + escapeHtml(s.cwd || '') + '">'
|
|
136
137
|
+ '<div class="session-item-row">'
|
|
137
138
|
+ '<span class="session-item-status ' + escapeHtml(statusClass) + '"></span>'
|
|
138
139
|
+ '<span class="session-item-id">' + escapeHtml(s.session_id.slice(0, 8)) + '</span>'
|
|
@@ -306,11 +307,16 @@ function _toggleProjectFilter() {
|
|
|
306
307
|
_filterSessions(searchInput ? searchInput.value : '');
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
function _selectSession(sid, prompt) {
|
|
310
|
+
function _selectSession(sid, prompt, cwd) {
|
|
310
311
|
_contextSessionId = sid;
|
|
311
312
|
_contextSessionPrompt = prompt;
|
|
312
313
|
_updateContextUI();
|
|
313
314
|
_closeSessionPicker();
|
|
315
|
+
// 세션의 cwd로 디렉터리 + 최근 프로젝트 칩 동기화
|
|
316
|
+
if (cwd) {
|
|
317
|
+
addRecentDir(cwd);
|
|
318
|
+
selectRecentDir(cwd);
|
|
319
|
+
}
|
|
314
320
|
showToast(t('msg_session_select') + ': ' + sid.slice(0, 8) + '...');
|
|
315
321
|
}
|
|
316
322
|
|
|
@@ -321,7 +327,7 @@ function _closeSessionPicker() {
|
|
|
321
327
|
document.addEventListener('click', function(e) {
|
|
322
328
|
var item = e.target.closest('.session-item');
|
|
323
329
|
if (item && item.dataset.sid) {
|
|
324
|
-
_selectSession(item.dataset.sid, item.dataset.prompt || '');
|
|
330
|
+
_selectSession(item.dataset.sid, item.dataset.prompt || '', item.dataset.cwd || '');
|
|
325
331
|
return;
|
|
326
332
|
}
|
|
327
333
|
var picker = document.getElementById('sessionPicker');
|
|
@@ -533,6 +539,8 @@ async function handleFiles(files) {
|
|
|
533
539
|
|
|
534
540
|
try {
|
|
535
541
|
const data = await uploadFile(file);
|
|
542
|
+
// 업로드 중 사용자가 첨부를 제거했거나 폼이 초기화된 경우 무시
|
|
543
|
+
if (!attachments[tempIdx]) continue;
|
|
536
544
|
attachments[tempIdx].serverPath = data.path;
|
|
537
545
|
attachments[tempIdx].filename = data.filename || file.name;
|
|
538
546
|
thumb.classList.remove('uploading');
|
|
@@ -541,8 +549,8 @@ async function handleFiles(files) {
|
|
|
541
549
|
insertAtCursor(ta, `@image${tempIdx}`);
|
|
542
550
|
} catch (err) {
|
|
543
551
|
showToast(`${t('msg_upload_failed')}: ${escapeHtml(file.name)} — ${err.message}`, 'error');
|
|
544
|
-
attachments[tempIdx] = null;
|
|
545
|
-
thumb.remove();
|
|
552
|
+
if (attachments[tempIdx]) attachments[tempIdx] = null;
|
|
553
|
+
if (thumb.parentNode) thumb.remove();
|
|
546
554
|
updateAttachBadge();
|
|
547
555
|
}
|
|
548
556
|
}
|
|
@@ -615,15 +623,16 @@ function statusBadgeHtml(status) {
|
|
|
615
623
|
return `<span class="badge ${cls[s] || 'badge-pending'}">${labels[s] || s}</span>`;
|
|
616
624
|
}
|
|
617
625
|
|
|
618
|
-
function jobActionsHtml(id, status, sessionId) {
|
|
626
|
+
function jobActionsHtml(id, status, sessionId, cwd) {
|
|
619
627
|
const isRunning = status === 'running';
|
|
628
|
+
const escapedCwd = escapeHtml(cwd || '');
|
|
620
629
|
let btns = '';
|
|
621
630
|
if (!isRunning) {
|
|
622
631
|
btns += `<button class="btn-retry-job" onclick="event.stopPropagation(); retryJob('${escapeHtml(id)}')" title="같은 프롬프트로 다시 실행"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button>`;
|
|
623
632
|
}
|
|
624
633
|
if (sessionId) {
|
|
625
634
|
btns += `<button class="btn-continue-job" onclick="event.stopPropagation(); openFollowUp('${escapeHtml(id)}')" title="세션 이어서 명령 (resume)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg></button>`;
|
|
626
|
-
btns += `<button class="btn-fork-job" onclick="event.stopPropagation(); quickForkSession('${escapeHtml(sessionId)}')" title="이 세션에서 분기 (fork)" style="color:var(--yellow);"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M6 9v3c0 3.3 2.7 6 6 6h3"/></svg></button>`;
|
|
635
|
+
btns += `<button class="btn-fork-job" onclick="event.stopPropagation(); quickForkSession('${escapeHtml(sessionId)}', '${escapedCwd}')" title="이 세션에서 분기 (fork)" style="color:var(--yellow);"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M6 9v3c0 3.3 2.7 6 6 6h3"/></svg></button>`;
|
|
627
636
|
}
|
|
628
637
|
if (!isRunning) {
|
|
629
638
|
btns += `<button class="btn-delete-job" onclick="event.stopPropagation(); deleteJob('${escapeHtml(id)}')" title="작업 제거"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>`;
|
|
@@ -633,22 +642,32 @@ function jobActionsHtml(id, status, sessionId) {
|
|
|
633
642
|
}
|
|
634
643
|
|
|
635
644
|
// 작업 목록에서 직접 fork 세션 선택
|
|
636
|
-
function quickForkSession(sessionId) {
|
|
645
|
+
function quickForkSession(sessionId, cwd) {
|
|
637
646
|
_contextMode = 'fork';
|
|
638
647
|
_contextSessionId = sessionId;
|
|
639
648
|
_contextSessionPrompt = null;
|
|
640
649
|
_updateContextUI();
|
|
650
|
+
// 세션의 cwd로 디렉터리 + 최근 프로젝트 칩 동기화
|
|
651
|
+
if (cwd) {
|
|
652
|
+
addRecentDir(cwd);
|
|
653
|
+
selectRecentDir(cwd);
|
|
654
|
+
}
|
|
641
655
|
showToast(t('msg_fork_mode') + ' (' + sessionId.slice(0, 8) + '...). ' + t('msg_fork_input'));
|
|
642
656
|
document.getElementById('promptInput').focus();
|
|
643
657
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
644
658
|
}
|
|
645
659
|
|
|
646
660
|
// 작업 목록의 세션 ID 클릭 → resume 모드로 전환
|
|
647
|
-
function resumeFromJob(sessionId, promptHint) {
|
|
661
|
+
function resumeFromJob(sessionId, promptHint, cwd) {
|
|
648
662
|
_contextMode = 'resume';
|
|
649
663
|
_contextSessionId = sessionId;
|
|
650
664
|
_contextSessionPrompt = promptHint || null;
|
|
651
665
|
_updateContextUI();
|
|
666
|
+
// 세션의 cwd로 디렉터리 + 최근 프로젝트 칩 동기화
|
|
667
|
+
if (cwd) {
|
|
668
|
+
addRecentDir(cwd);
|
|
669
|
+
selectRecentDir(cwd);
|
|
670
|
+
}
|
|
652
671
|
showToast('Resume 모드: ' + sessionId.slice(0, 8) + '... 세션에 이어서 전송합니다.');
|
|
653
672
|
document.getElementById('promptInput').focus();
|
|
654
673
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
@@ -851,10 +870,10 @@ function renderJobs(jobs) {
|
|
|
851
870
|
if (job.session_id) {
|
|
852
871
|
cells[4].className = 'job-session clickable';
|
|
853
872
|
cells[4].title = job.session_id;
|
|
854
|
-
cells[4].setAttribute('onclick', `event.stopPropagation(); resumeFromJob('${escapeHtml(job.session_id)}', '${escapeHtml(truncate(job.prompt, 40))}')`);
|
|
873
|
+
cells[4].setAttribute('onclick', `event.stopPropagation(); resumeFromJob('${escapeHtml(job.session_id)}', '${escapeHtml(truncate(job.prompt, 40))}', '${escapeHtml(job.cwd || '')}')`);
|
|
855
874
|
}
|
|
856
875
|
}
|
|
857
|
-
const newActions = jobActionsHtml(id, job.status, job.session_id);
|
|
876
|
+
const newActions = jobActionsHtml(id, job.status, job.session_id, job.cwd);
|
|
858
877
|
if (cells[6].innerHTML !== newActions) {
|
|
859
878
|
cells[6].innerHTML = newActions;
|
|
860
879
|
}
|
|
@@ -871,9 +890,9 @@ function renderJobs(jobs) {
|
|
|
871
890
|
<td>${statusBadgeHtml(job.status)}</td>
|
|
872
891
|
<td class="prompt-cell" title="${escapeHtml(job.prompt)}">${renderPromptHtml(job.prompt)}</td>
|
|
873
892
|
<td class="job-cwd" title="${escapeHtml(job.cwd || '')}">${escapeHtml(formatCwd(job.cwd))}</td>
|
|
874
|
-
<td class="job-session${job.session_id ? ' clickable' : ''}" title="${escapeHtml(job.session_id || '')}" ${job.session_id ? `onclick="event.stopPropagation(); resumeFromJob('${escapeHtml(job.session_id)}', '${escapeHtml(truncate(job.prompt, 40))}')"` : ''}>${job.session_id ? escapeHtml(job.session_id.slice(0, 8)) : (job.status === 'running' ? '<span style="color:var(--text-muted);font-size:0.7rem;">—</span>' : '-')}</td>
|
|
893
|
+
<td class="job-session${job.session_id ? ' clickable' : ''}" title="${escapeHtml(job.session_id || '')}" ${job.session_id ? `onclick="event.stopPropagation(); resumeFromJob('${escapeHtml(job.session_id)}', '${escapeHtml(truncate(job.prompt, 40))}', '${escapeHtml(job.cwd || '')}')"` : ''}>${job.session_id ? escapeHtml(job.session_id.slice(0, 8)) : (job.status === 'running' ? '<span style="color:var(--text-muted);font-size:0.7rem;">—</span>' : '-')}</td>
|
|
875
894
|
<td class="job-time">${formatTime(job.created || job.created_at)}</td>
|
|
876
|
-
<td>${jobActionsHtml(id, job.status, job.session_id)}</td>`;
|
|
895
|
+
<td>${jobActionsHtml(id, job.status, job.session_id, job.cwd)}</td>`;
|
|
877
896
|
tbody.appendChild(tr);
|
|
878
897
|
} else {
|
|
879
898
|
delete existingRows[id];
|
|
@@ -1132,7 +1151,8 @@ function renderStreamEvents(jobId) {
|
|
|
1132
1151
|
cells[4].textContent = evt.session_id.slice(0, 8);
|
|
1133
1152
|
cells[4].className = 'job-session clickable';
|
|
1134
1153
|
cells[4].title = evt.session_id;
|
|
1135
|
-
|
|
1154
|
+
const evtCwd = panel ? (panel.dataset.cwd || '') : '';
|
|
1155
|
+
cells[4].setAttribute('onclick', `event.stopPropagation(); resumeFromJob('${escapeHtml(evt.session_id)}', '', '${escapeHtml(evtCwd)}')`);
|
|
1136
1156
|
}
|
|
1137
1157
|
}
|
|
1138
1158
|
}
|
|
@@ -1390,6 +1410,12 @@ function renderRecentDirs() {
|
|
|
1390
1410
|
}
|
|
1391
1411
|
|
|
1392
1412
|
function selectRecentDir(path) {
|
|
1413
|
+
const current = document.getElementById('cwdInput').value;
|
|
1414
|
+
// 같은 칩을 다시 클릭하면 선택 해제
|
|
1415
|
+
if (current === path) {
|
|
1416
|
+
clearDirSelection();
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1393
1419
|
document.getElementById('cwdInput').value = path;
|
|
1394
1420
|
updateCwdBadge(path);
|
|
1395
1421
|
const text = document.getElementById('dirPickerText');
|
|
@@ -1453,6 +1479,7 @@ async function browseTo(path) {
|
|
|
1453
1479
|
document.getElementById('dirPickerText').textContent = data.current;
|
|
1454
1480
|
document.getElementById('dirPickerDisplay').classList.add('has-value');
|
|
1455
1481
|
document.getElementById('dirPickerClear').classList.add('visible');
|
|
1482
|
+
renderRecentDirs();
|
|
1456
1483
|
|
|
1457
1484
|
renderBreadcrumb(data.current, breadcrumb);
|
|
1458
1485
|
|
|
@@ -1503,6 +1530,7 @@ function selectCurrentDir() {
|
|
|
1503
1530
|
|
|
1504
1531
|
addRecentDir(dirBrowserCurrentPath);
|
|
1505
1532
|
closeDirBrowser();
|
|
1533
|
+
renderRecentDirs();
|
|
1506
1534
|
}
|
|
1507
1535
|
|
|
1508
1536
|
function clearDirSelection() {
|
package/web/static/styles.css
CHANGED
|
@@ -48,8 +48,6 @@ body {
|
|
|
48
48
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
49
49
|
::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
|
|
50
50
|
|
|
51
|
-
/* ── Header ── */
|
|
52
|
-
|
|
53
51
|
/* ── Buttons ── */
|
|
54
52
|
button {
|
|
55
53
|
font-family: var(--font);
|
|
@@ -201,6 +199,7 @@ button {
|
|
|
201
199
|
|
|
202
200
|
.session-picker {
|
|
203
201
|
display: none;
|
|
202
|
+
flex-direction: column;
|
|
204
203
|
position: absolute;
|
|
205
204
|
top: 100%;
|
|
206
205
|
right: 0;
|
|
@@ -213,8 +212,6 @@ button {
|
|
|
213
212
|
border-radius: var(--radius-lg);
|
|
214
213
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
215
214
|
z-index: 200;
|
|
216
|
-
display: none;
|
|
217
|
-
flex-direction: column;
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
.session-picker.open { display: flex; }
|
|
@@ -442,16 +439,11 @@ button {
|
|
|
442
439
|
/* ── Layout ── */
|
|
443
440
|
.layout {
|
|
444
441
|
min-height: 100vh;
|
|
445
|
-
width: 100%;
|
|
446
|
-
max-width: 100vw;
|
|
447
442
|
}
|
|
448
443
|
|
|
449
444
|
.main {
|
|
450
|
-
flex: 1;
|
|
451
445
|
padding: 8px 0;
|
|
452
446
|
overflow-x: hidden;
|
|
453
|
-
width: 100%;
|
|
454
|
-
max-width: 100vw;
|
|
455
447
|
}
|
|
456
448
|
|
|
457
449
|
/* ── CWD badge ── */
|
|
@@ -1168,12 +1160,6 @@ thead tr { cursor: default; }
|
|
|
1168
1160
|
color: var(--yellow);
|
|
1169
1161
|
}
|
|
1170
1162
|
|
|
1171
|
-
.prompt-cell {
|
|
1172
|
-
overflow: hidden;
|
|
1173
|
-
text-overflow: ellipsis;
|
|
1174
|
-
white-space: nowrap;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
1163
|
.job-id {
|
|
1178
1164
|
font-family: var(--font-mono);
|
|
1179
1165
|
font-size: 0.78rem;
|
|
@@ -1184,10 +1170,6 @@ thead tr { cursor: default; }
|
|
|
1184
1170
|
font-family: var(--font-mono);
|
|
1185
1171
|
font-size: 0.75rem;
|
|
1186
1172
|
color: var(--text-muted);
|
|
1187
|
-
max-width: 100%;
|
|
1188
|
-
overflow: hidden;
|
|
1189
|
-
text-overflow: ellipsis;
|
|
1190
|
-
white-space: nowrap;
|
|
1191
1173
|
}
|
|
1192
1174
|
|
|
1193
1175
|
.job-session {
|
|
@@ -1671,7 +1653,6 @@ thead tr { cursor: default; }
|
|
|
1671
1653
|
}
|
|
1672
1654
|
|
|
1673
1655
|
@media (max-width: 900px) {
|
|
1674
|
-
.main { padding: 8px 0; }
|
|
1675
1656
|
th, td { padding: 10px 10px; font-size: 0.78rem; }
|
|
1676
1657
|
}
|
|
1677
1658
|
|
|
@@ -1679,7 +1660,6 @@ thead tr { cursor: default; }
|
|
|
1679
1660
|
.main { padding: 4px 0; }
|
|
1680
1661
|
.card-body { padding: 14px; }
|
|
1681
1662
|
th, td { padding: 8px 6px; font-size: 0.74rem; }
|
|
1682
|
-
.job-cwd { max-width: 100%; }
|
|
1683
1663
|
.stream-content { max-height: 300px; }
|
|
1684
1664
|
}
|
|
1685
1665
|
|