cc-agent-ui 0.2.3 → 0.2.5
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 +214 -20
- package/server.js +67 -8
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -296,26 +296,23 @@ body {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/* status colors */
|
|
299
|
-
|
|
299
|
+
|
|
300
300
|
.tcard.done { border-color: #1e2d1e; box-shadow: 0 0 16px var(--green-glow); }
|
|
301
301
|
.tcard.failed { border-color: #2d1e20; box-shadow: 0 0 16px var(--red-glow); }
|
|
302
302
|
.tcard.pending_approval { border-color: #2d2a1e; box-shadow: 0 0 16px var(--yellow-glow); }
|
|
303
303
|
.tcard.cloning { border-color: #1e2233; box-shadow: 0 0 16px rgba(97,175,239,0.1); }
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
opacity: 0.4;
|
|
312
|
-
animation: scanline 3s linear infinite;
|
|
313
|
-
pointer-events: none;
|
|
314
|
-
z-index: 5;
|
|
305
|
+
@keyframes border-race {
|
|
306
|
+
0% { box-shadow: 0 -1px 4px 0px #60a5fa, inset 0 0 0 1px #1e3a8a, 0 0 10px rgba(59,130,246,0.12); }
|
|
307
|
+
25% { box-shadow: 2px 0 4px 0px #60a5fa, inset 0 0 0 1px #1e3a8a, 0 0 10px rgba(59,130,246,0.12); }
|
|
308
|
+
50% { box-shadow: 0 2px 4px 0px #60a5fa, inset 0 0 0 1px #1e3a8a, 0 0 10px rgba(59,130,246,0.12); }
|
|
309
|
+
75% { box-shadow: -2px 0 4px 0px #60a5fa, inset 0 0 0 1px #1e3a8a, 0 0 10px rgba(59,130,246,0.12); }
|
|
310
|
+
100% { box-shadow: 0 -1px 4px 0px #60a5fa, inset 0 0 0 1px #1e3a8a, 0 0 10px rgba(59,130,246,0.12); }
|
|
315
311
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
312
|
+
|
|
313
|
+
.tcard.running {
|
|
314
|
+
border-color: #1e3a8a;
|
|
315
|
+
animation: border-race 2s linear infinite;
|
|
319
316
|
}
|
|
320
317
|
|
|
321
318
|
/* ── Card Header ── */
|
|
@@ -390,6 +387,7 @@ body {
|
|
|
390
387
|
}
|
|
391
388
|
.card-repo-icon { color: var(--dimmer); }
|
|
392
389
|
.card-branch { color: var(--purple); margin-left: auto; flex-shrink:0; }
|
|
390
|
+
.card-docker-badge { color: #38bdf8; font-size:9px; background: rgba(56,189,248,.12); border:1px solid rgba(56,189,248,.3); border-radius:3px; padding:1px 5px; flex-shrink:0; }
|
|
393
391
|
|
|
394
392
|
/* ── Terminal body ── */
|
|
395
393
|
.card-term {
|
|
@@ -409,11 +407,11 @@ body {
|
|
|
409
407
|
|
|
410
408
|
/* Output line coloring */
|
|
411
409
|
.tl { white-space: pre-wrap; word-break: break-all; }
|
|
412
|
-
.tl-sys { color:
|
|
410
|
+
.tl-sys { color: #7a8099; } /* [cc-agent] meta lines */
|
|
413
411
|
.tl-ok { color: var(--green); } /* success */
|
|
414
412
|
.tl-err { color: var(--red); } /* errors */
|
|
415
413
|
.tl-warn { color: var(--yellow); } /* warnings */
|
|
416
|
-
.tl-tool { color: var(--blue); }
|
|
414
|
+
.tl-tool { color: var(--blue); opacity: 0.8; font-size: 10px; } /* tool calls */
|
|
417
415
|
.tl-file { color: var(--orange); } /* file operations */
|
|
418
416
|
.tl-head { color: var(--cyan); font-weight:600; } /* bold headings */
|
|
419
417
|
.tl-dim { color: var(--dim); } /* faded */
|
|
@@ -649,6 +647,49 @@ body {
|
|
|
649
647
|
text-decoration-color: var(--orange);
|
|
650
648
|
}
|
|
651
649
|
|
|
650
|
+
/* ── Message bar (job panel) ── */
|
|
651
|
+
#jp-msg-bar {
|
|
652
|
+
border-top: 1px solid var(--border);
|
|
653
|
+
padding: 8px 14px;
|
|
654
|
+
display: flex;
|
|
655
|
+
gap: 8px;
|
|
656
|
+
flex-shrink: 0;
|
|
657
|
+
background: rgba(0,0,0,0.15);
|
|
658
|
+
}
|
|
659
|
+
#jp-msg-bar.hidden { display: none; }
|
|
660
|
+
#jp-msg-input {
|
|
661
|
+
flex: 1;
|
|
662
|
+
background: rgba(0,0,0,0.4);
|
|
663
|
+
border: 1px solid var(--border-hi);
|
|
664
|
+
border-radius: 4px;
|
|
665
|
+
color: var(--text);
|
|
666
|
+
font-family: var(--font);
|
|
667
|
+
font-size: 11px;
|
|
668
|
+
padding: 5px 10px;
|
|
669
|
+
outline: none;
|
|
670
|
+
transition: border-color 0.15s;
|
|
671
|
+
}
|
|
672
|
+
#jp-msg-input::placeholder { color: var(--dim); }
|
|
673
|
+
#jp-msg-input:focus { border-color: var(--cyan); }
|
|
674
|
+
#jp-msg-send {
|
|
675
|
+
font-family: var(--font); font-size: 10px;
|
|
676
|
+
padding: 5px 14px; border-radius: 4px; cursor: pointer;
|
|
677
|
+
border: 1px solid rgba(86,182,194,0.4);
|
|
678
|
+
background: transparent; color: var(--cyan); font-weight: 600;
|
|
679
|
+
transition: all 0.15s; white-space: nowrap;
|
|
680
|
+
}
|
|
681
|
+
#jp-msg-send:hover { background: rgba(86,182,194,0.12); border-color: var(--cyan); }
|
|
682
|
+
#jp-msg-send:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
683
|
+
|
|
684
|
+
/* ── Working indicator on empty running terminal ── */
|
|
685
|
+
.term-working {
|
|
686
|
+
padding: 12px 0 4px;
|
|
687
|
+
display: flex; align-items: center; gap: 8px;
|
|
688
|
+
color: var(--dim); font-size: 10px;
|
|
689
|
+
}
|
|
690
|
+
.term-spinner { animation: spin 1.2s linear infinite; display: inline-block; }
|
|
691
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
692
|
+
|
|
652
693
|
/* ── Empty state ── */
|
|
653
694
|
#empty-state {
|
|
654
695
|
position: absolute;
|
|
@@ -662,10 +703,41 @@ body {
|
|
|
662
703
|
#empty-state .e-icon { font-size: 48px; color: var(--dimmer); margin-bottom: 4px; }
|
|
663
704
|
#empty-state .e-title { font-size: 16px; font-weight:700; color: var(--dim); }
|
|
664
705
|
#empty-state .e-sub { font-size: 11px; color: var(--dimmer); }
|
|
706
|
+
/* ── Loading overlay ──────────────────────────────────────────────────── */
|
|
707
|
+
#loading-overlay {
|
|
708
|
+
position: fixed;
|
|
709
|
+
inset: 0;
|
|
710
|
+
background: var(--bg);
|
|
711
|
+
display: flex;
|
|
712
|
+
flex-direction: column;
|
|
713
|
+
align-items: center;
|
|
714
|
+
justify-content: center;
|
|
715
|
+
gap: 16px;
|
|
716
|
+
z-index: 9999;
|
|
717
|
+
}
|
|
718
|
+
#loading-spinner {
|
|
719
|
+
font-size: 24px;
|
|
720
|
+
color: var(--cyan);
|
|
721
|
+
text-shadow: 0 0 12px var(--cyan);
|
|
722
|
+
font-family: var(--font);
|
|
723
|
+
line-height: 1;
|
|
724
|
+
}
|
|
725
|
+
#loading-text {
|
|
726
|
+
font-size: 12px;
|
|
727
|
+
color: var(--dim);
|
|
728
|
+
letter-spacing: 0.08em;
|
|
729
|
+
font-family: var(--font);
|
|
730
|
+
}
|
|
665
731
|
</style>
|
|
666
732
|
</head>
|
|
667
733
|
<body>
|
|
668
734
|
|
|
735
|
+
<!-- Loading overlay -->
|
|
736
|
+
<div id="loading-overlay">
|
|
737
|
+
<div id="loading-spinner">⣾</div>
|
|
738
|
+
<div id="loading-text">Loading agents…</div>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
669
741
|
<!-- Top bar -->
|
|
670
742
|
<div id="topbar">
|
|
671
743
|
<div id="logo">
|
|
@@ -723,6 +795,10 @@ body {
|
|
|
723
795
|
<div id="jp-task"></div>
|
|
724
796
|
<div id="jp-actions"></div>
|
|
725
797
|
<div id="jp-log"></div>
|
|
798
|
+
<div id="jp-msg-bar" class="hidden">
|
|
799
|
+
<input id="jp-msg-input" type="text" placeholder="Send message to Claude…" autocomplete="off">
|
|
800
|
+
<button id="jp-msg-send" onclick="jpSendMessage()">↵ Send</button>
|
|
801
|
+
</div>
|
|
726
802
|
</div>
|
|
727
803
|
|
|
728
804
|
<!-- File Browser Panel -->
|
|
@@ -820,29 +896,51 @@ viewport.addEventListener('touchmove', e => {
|
|
|
820
896
|
}, { passive: false });
|
|
821
897
|
|
|
822
898
|
// ── Log line colorizer ─────────────────────────────────────────────────────
|
|
899
|
+
const TOOL_ICONS = {
|
|
900
|
+
Read:'📖', Edit:'✏️', Write:'💾', Bash:'$', Glob:'🔍', Grep:'🔎',
|
|
901
|
+
WebFetch:'🌐', WebSearch:'🌐', TodoWrite:'✅', Agent:'🤖',
|
|
902
|
+
MultiEdit:'✏️', NotebookEdit:'📓', Dispatch:'⚡',
|
|
903
|
+
};
|
|
904
|
+
|
|
823
905
|
function classifyLine(raw) {
|
|
824
906
|
const s = raw.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
825
907
|
if (!s) return null;
|
|
908
|
+
if (s.startsWith('[tool] ')) return 'tl tl-tool';
|
|
909
|
+
if (s.startsWith('[you] ')) return 'tl tl-head';
|
|
826
910
|
if (s.startsWith('[cc-agent]')) return 'tl tl-sys';
|
|
827
911
|
if (/✓|✅|PASS|passed|merged|pushed|success/i.test(s)) return 'tl tl-ok';
|
|
828
912
|
if (/✗|❌|error|Error|FAIL|failed|exception/i.test(s)) return 'tl tl-err';
|
|
829
913
|
if (/⚠|warn|Warning/i.test(s)) return 'tl tl-warn';
|
|
830
|
-
if (/^(Read|Edit|Write|Bash|Glob|Grep|WebFetch|TodoWrite|Agent)\b/.test(s)) return 'tl tl-tool';
|
|
914
|
+
if (/^(Read|Edit|Write|Bash|Glob|Grep|WebFetch|WebSearch|TodoWrite|Agent)\b/.test(s)) return 'tl tl-tool';
|
|
831
915
|
if (/\.(ts|js|tsx|jsx|py|go|rs|md|json|yaml|sh)\b/.test(s)) return 'tl tl-file';
|
|
832
916
|
if (/^#{1,3}\s|^\*\*/.test(s)) return 'tl tl-head';
|
|
833
917
|
if (/^[\-–—>·]/.test(s)) return 'tl tl-dim';
|
|
834
918
|
return 'tl';
|
|
835
919
|
}
|
|
836
920
|
|
|
921
|
+
function formatLine(raw) {
|
|
922
|
+
const s = raw.replace(/\x1b\[[0-9;]*m/g, '');
|
|
923
|
+
// Enrich [tool] lines with icon
|
|
924
|
+
const toolMatch = s.match(/^\[tool\] (\w+)(.*)/);
|
|
925
|
+
if (toolMatch) {
|
|
926
|
+
const icon = TOOL_ICONS[toolMatch[1]] || '⚙';
|
|
927
|
+
return `${icon} ${toolMatch[1]}${toolMatch[2]}`;
|
|
928
|
+
}
|
|
929
|
+
return s;
|
|
930
|
+
}
|
|
931
|
+
|
|
837
932
|
function appendLines(logEl, lines, isNew) {
|
|
933
|
+
// Remove working indicator if present
|
|
934
|
+
const working = logEl.querySelector('.term-working');
|
|
935
|
+
if (working && lines.some(l => l.trim())) working.remove();
|
|
936
|
+
|
|
838
937
|
const frag = document.createDocumentFragment();
|
|
839
938
|
for (const raw of lines) {
|
|
840
939
|
const cls = classifyLine(raw);
|
|
841
940
|
if (!cls) continue;
|
|
842
941
|
const d = document.createElement('div');
|
|
843
942
|
d.className = isNew ? cls + ' tl-new' : cls;
|
|
844
|
-
const clean = raw
|
|
845
|
-
// Linkify file paths
|
|
943
|
+
const clean = formatLine(raw);
|
|
846
944
|
d.innerHTML = linkifyPaths(clean);
|
|
847
945
|
if (isNew) setTimeout(() => d.classList.remove('tl-new'), 1500);
|
|
848
946
|
frag.appendChild(d);
|
|
@@ -852,6 +950,18 @@ function appendLines(logEl, lines, isNew) {
|
|
|
852
950
|
logEl.scrollTop = logEl.scrollHeight;
|
|
853
951
|
}
|
|
854
952
|
|
|
953
|
+
function setWorkingIndicator(logEl, show) {
|
|
954
|
+
const existing = logEl.querySelector('.term-working');
|
|
955
|
+
if (show && !existing && logEl.children.length === 0) {
|
|
956
|
+
const d = document.createElement('div');
|
|
957
|
+
d.className = 'term-working';
|
|
958
|
+
d.innerHTML = '<span class="term-spinner">⟳</span> Claude is working…';
|
|
959
|
+
logEl.appendChild(d);
|
|
960
|
+
} else if (!show && existing) {
|
|
961
|
+
existing.remove();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
855
965
|
// ── Card factory ───────────────────────────────────────────────────────────
|
|
856
966
|
function shortRepo(url) {
|
|
857
967
|
if (!url) return 'local';
|
|
@@ -902,6 +1012,7 @@ function makeCard(job, n) {
|
|
|
902
1012
|
<span class="card-repo-icon">⎇</span>
|
|
903
1013
|
<span>${escHtml(repoStr)}</span>
|
|
904
1014
|
${job.branch ? `<span class="card-branch">${escHtml(job.branch)}</span>` : ''}
|
|
1015
|
+
${job.dockerIsolation ? `<span class="card-docker-badge" title="Running in Docker container">🐳 isolated</span>` : ''}
|
|
905
1016
|
</div>
|
|
906
1017
|
<div class="card-term"></div>
|
|
907
1018
|
<div class="card-foot">
|
|
@@ -926,6 +1037,7 @@ function makeSidebarItem(job) {
|
|
|
926
1037
|
const item = document.createElement('div');
|
|
927
1038
|
item.className = `job-item`;
|
|
928
1039
|
item.id = `si-${job.id}`;
|
|
1040
|
+
item.dataset.startedAt = job.startedAt || '';
|
|
929
1041
|
const status = job.status || 'unknown';
|
|
930
1042
|
|
|
931
1043
|
item.innerHTML = `
|
|
@@ -1089,6 +1201,7 @@ function handleSnapshot(data) {
|
|
|
1089
1201
|
updateCounts();
|
|
1090
1202
|
applyFilter();
|
|
1091
1203
|
if (Object.keys(jobs).length > 0) emptyState.style.display = 'none';
|
|
1204
|
+
if (typeof hideLoader === 'function') hideLoader();
|
|
1092
1205
|
}
|
|
1093
1206
|
|
|
1094
1207
|
function addJob(job, lines) {
|
|
@@ -1099,9 +1212,15 @@ function addJob(job, lines) {
|
|
|
1099
1212
|
getOrCreateCol(key);
|
|
1100
1213
|
insertCardInColumn(key, card, job);
|
|
1101
1214
|
const sidebarItem = makeSidebarItem(job);
|
|
1102
|
-
|
|
1215
|
+
const jobTime = new Date(job.startedAt || 0).getTime();
|
|
1216
|
+
const after = Array.from(jobList.children).find(el =>
|
|
1217
|
+
new Date(el.dataset.startedAt || 0).getTime() < jobTime
|
|
1218
|
+
);
|
|
1219
|
+
if (after) jobList.insertBefore(sidebarItem, after);
|
|
1220
|
+
else jobList.appendChild(sidebarItem);
|
|
1103
1221
|
jobs[job.id] = { job: { ...job }, card, logEl, n: jobCounter };
|
|
1104
1222
|
if (lines.length) appendLines(logEl, lines, false);
|
|
1223
|
+
else if (job.status === 'running' || job.status === 'cloning') setWorkingIndicator(logEl, true);
|
|
1105
1224
|
updateColHeader(key);
|
|
1106
1225
|
wireCardClick(card, job.id);
|
|
1107
1226
|
}
|
|
@@ -1124,6 +1243,10 @@ function handleJobUpdate(data) {
|
|
|
1124
1243
|
entry.job = { ...entry.job, ...job };
|
|
1125
1244
|
const status = job.status;
|
|
1126
1245
|
|
|
1246
|
+
// Working indicator
|
|
1247
|
+
if (status === 'running' || status === 'cloning') setWorkingIndicator(entry.logEl, true);
|
|
1248
|
+
else setWorkingIndicator(entry.logEl, false);
|
|
1249
|
+
|
|
1127
1250
|
// Update card class
|
|
1128
1251
|
entry.card.className = `tcard ${status}`;
|
|
1129
1252
|
|
|
@@ -1154,6 +1277,9 @@ function handleJobUpdate(data) {
|
|
|
1154
1277
|
jpMeta.querySelector('.jp-badge').className = `jp-badge badge-${status}`;
|
|
1155
1278
|
jpMeta.querySelector('.jp-badge').textContent = status.replace('_', ' ');
|
|
1156
1279
|
renderActions(status);
|
|
1280
|
+
const msgBar = $('jp-msg-bar');
|
|
1281
|
+
if (status === 'running') msgBar.classList.remove('hidden');
|
|
1282
|
+
else msgBar.classList.add('hidden');
|
|
1157
1283
|
}
|
|
1158
1284
|
|
|
1159
1285
|
updateColHeader(repoKey(job));
|
|
@@ -1216,6 +1342,38 @@ function jpClose() {
|
|
|
1216
1342
|
jpCurrentId = null;
|
|
1217
1343
|
}
|
|
1218
1344
|
|
|
1345
|
+
async function jpSendMessage() {
|
|
1346
|
+
const input = $('jp-msg-input');
|
|
1347
|
+
const btn = $('jp-msg-send');
|
|
1348
|
+
const text = input.value.trim();
|
|
1349
|
+
if (!text || !jpCurrentId) return;
|
|
1350
|
+
input.value = '';
|
|
1351
|
+
btn.disabled = true;
|
|
1352
|
+
try {
|
|
1353
|
+
const r = await fetch('/api/job/action', {
|
|
1354
|
+
method: 'POST',
|
|
1355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1356
|
+
body: JSON.stringify({ id: jpCurrentId, action: 'message', message: text }),
|
|
1357
|
+
});
|
|
1358
|
+
await r.json();
|
|
1359
|
+
} catch (e) {
|
|
1360
|
+
const d = document.createElement('div');
|
|
1361
|
+
d.className = 'tl tl-err';
|
|
1362
|
+
d.textContent = `[ui] Error: ${e.message}`;
|
|
1363
|
+
jpLog.appendChild(d);
|
|
1364
|
+
} finally {
|
|
1365
|
+
btn.disabled = false;
|
|
1366
|
+
input.focus();
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Enter key submits message
|
|
1371
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1372
|
+
$('jp-msg-input').addEventListener('keydown', e => {
|
|
1373
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); jpSendMessage(); }
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1219
1377
|
async function jpOpen(id) {
|
|
1220
1378
|
const entry = jobs[id];
|
|
1221
1379
|
if (!entry) return;
|
|
@@ -1233,6 +1391,7 @@ async function jpOpen(id) {
|
|
|
1233
1391
|
<span class="jp-badge badge-${status}">${status.replace('_',' ')}</span>
|
|
1234
1392
|
<span class="jp-meta-row">repo <span>${escHtml(shortRepo(job.repoUrl) || 'local')}</span>
|
|
1235
1393
|
${job.branch ? ` · branch <span>${escHtml(job.branch)}</span>` : ''}
|
|
1394
|
+
${job.dockerIsolation ? ` · <span style="color:#38bdf8">🐳 docker isolated</span>` : ''}
|
|
1236
1395
|
</span>
|
|
1237
1396
|
<span class="jp-meta-row">id <span>${escHtml(job.id || id)}</span></span>
|
|
1238
1397
|
<span class="jp-meta-row">started <span>${job.startedAt ? new Date(job.startedAt).toLocaleString() : '—'}</span></span>
|
|
@@ -1241,6 +1400,15 @@ async function jpOpen(id) {
|
|
|
1241
1400
|
// Actions
|
|
1242
1401
|
renderActions(status);
|
|
1243
1402
|
|
|
1403
|
+
// Show/hide message bar
|
|
1404
|
+
const msgBar = $('jp-msg-bar');
|
|
1405
|
+
if (status === 'running') {
|
|
1406
|
+
msgBar.classList.remove('hidden');
|
|
1407
|
+
setTimeout(() => $('jp-msg-input').focus(), 50);
|
|
1408
|
+
} else {
|
|
1409
|
+
msgBar.classList.add('hidden');
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1244
1412
|
// Load full log
|
|
1245
1413
|
jpLog.innerHTML = '<div style="color:var(--dim);padding:4px">loading…</div>';
|
|
1246
1414
|
try {
|
|
@@ -1447,6 +1615,32 @@ setInterval(() => {
|
|
|
1447
1615
|
}
|
|
1448
1616
|
}, 30000);
|
|
1449
1617
|
|
|
1618
|
+
// ── Loading overlay ────────────────────────────────────────────────────────
|
|
1619
|
+
(function() {
|
|
1620
|
+
const overlay = document.getElementById('loading-overlay');
|
|
1621
|
+
const spinnerEl = document.getElementById('loading-spinner');
|
|
1622
|
+
const textEl = document.getElementById('loading-text');
|
|
1623
|
+
const frames = ['⣾','⣽','⣻','⢿','⡿','⣟','⣯','⣷'];
|
|
1624
|
+
let frame = 0;
|
|
1625
|
+
const spinInterval = setInterval(() => {
|
|
1626
|
+
spinnerEl.textContent = frames[frame++ % frames.length];
|
|
1627
|
+
}, 100);
|
|
1628
|
+
|
|
1629
|
+
let snapshotReceived = false;
|
|
1630
|
+
const fallbackTimer = setTimeout(() => {
|
|
1631
|
+
if (!snapshotReceived) {
|
|
1632
|
+
textEl.textContent = 'Reconnecting…';
|
|
1633
|
+
}
|
|
1634
|
+
}, 8000);
|
|
1635
|
+
|
|
1636
|
+
window.hideLoader = function() {
|
|
1637
|
+
snapshotReceived = true;
|
|
1638
|
+
clearInterval(spinInterval);
|
|
1639
|
+
clearTimeout(fallbackTimer);
|
|
1640
|
+
overlay.style.display = 'none';
|
|
1641
|
+
};
|
|
1642
|
+
})();
|
|
1643
|
+
|
|
1450
1644
|
connect();
|
|
1451
1645
|
</script>
|
|
1452
1646
|
</body>
|
package/server.js
CHANGED
|
@@ -220,11 +220,12 @@ const server = http.createServer((req, res) => {
|
|
|
220
220
|
if (!job) { res.writeHead(404); res.end('job not found'); return; }
|
|
221
221
|
|
|
222
222
|
if (action === 'approve') {
|
|
223
|
-
//
|
|
223
|
+
// Mark approved in Redis — cc-agent MCP must be called separately to actually start it
|
|
224
|
+
// (cc-agent's approval is in-memory; this sets a flag for reference and for GitHub issue polling)
|
|
224
225
|
const updated = { ...job, approvedAt: new Date().toISOString(), approved: true };
|
|
225
226
|
await redis.set(`cca:job:${id}`, JSON.stringify(updated));
|
|
226
|
-
|
|
227
|
-
|
|
227
|
+
await redis.rPush(`cca:job:${id}:output`, '[cc-agent-ui] Approved by UI — use MCP approve_job to start');
|
|
228
|
+
broadcast({ type: 'job_output', id, lines: ['[cc-agent-ui] Approved by UI — use MCP approve_job to start'] });
|
|
228
229
|
} else if (action === 'cancel') {
|
|
229
230
|
const updated = { ...job, status: 'cancelled', cancelledAt: new Date().toISOString() };
|
|
230
231
|
await redis.set(`cca:job:${id}`, JSON.stringify(updated));
|
|
@@ -234,8 +235,16 @@ const server = http.createServer((req, res) => {
|
|
|
234
235
|
await redis.set(`cca:job:${id}`, JSON.stringify(updated));
|
|
235
236
|
broadcast({ type: 'job_update', job: updated });
|
|
236
237
|
} else if (action === 'message') {
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
if (message) {
|
|
239
|
+
// Queue for cc-agent to pick up (future: cc-agent polls cca:job:{id}:input)
|
|
240
|
+
await redis.rPush(`cca:job:${id}:input`, message);
|
|
241
|
+
// Echo to output so it's visible in terminal immediately
|
|
242
|
+
const line = `[you] ${message}`;
|
|
243
|
+
const newLen = await redis.rPush(`cca:job:${id}:output`, line);
|
|
244
|
+
// Advance the output length tracker so the poller doesn't re-broadcast this line
|
|
245
|
+
outputLengths[id] = newLen;
|
|
246
|
+
broadcast({ type: 'job_output', id, lines: [line] });
|
|
247
|
+
}
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -264,6 +273,25 @@ wss.on('connection', async ws => {
|
|
|
264
273
|
ws.on('close', () => { clients.delete(ws); });
|
|
265
274
|
});
|
|
266
275
|
|
|
276
|
+
// ── Tool call synthesis from recentTools diff ──────────────────────────────
|
|
277
|
+
const toolTrack = {}; // id → last recentTools array
|
|
278
|
+
|
|
279
|
+
function diffTools(prevArr, currArr) {
|
|
280
|
+
if (!currArr?.length) return [];
|
|
281
|
+
if (!prevArr?.length) return currArr.slice(-3); // first snapshot: emit up to 3
|
|
282
|
+
if (JSON.stringify(prevArr) === JSON.stringify(currArr)) return [];
|
|
283
|
+
// Find how many NEW items appeared at the tail of currArr relative to prevArr.
|
|
284
|
+
// Strategy: find the longest suffix of prevArr that matches a prefix of the new tail.
|
|
285
|
+
for (let overlap = Math.min(prevArr.length, currArr.length); overlap >= 0; overlap--) {
|
|
286
|
+
const prevSuffix = prevArr.slice(prevArr.length - overlap);
|
|
287
|
+
const currPrefix = currArr.slice(0, overlap);
|
|
288
|
+
if (JSON.stringify(prevSuffix) === JSON.stringify(currPrefix)) {
|
|
289
|
+
return currArr.slice(overlap); // these are genuinely new
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return currArr.slice(-Math.min(3, currArr.length)); // fallback: last 3
|
|
293
|
+
}
|
|
294
|
+
|
|
267
295
|
// ── Polling: job status changes ────────────────────────────────────────────
|
|
268
296
|
setInterval(async () => {
|
|
269
297
|
try {
|
|
@@ -277,6 +305,7 @@ setInterval(async () => {
|
|
|
277
305
|
if (!prev) {
|
|
278
306
|
// New job
|
|
279
307
|
jobCache[job.id] = job;
|
|
308
|
+
toolTrack[job.id] = job.recentTools || [];
|
|
280
309
|
const lines = await getOutputTail(job.id, 50);
|
|
281
310
|
broadcast({ type: 'job_new', job: { ...job, lines } });
|
|
282
311
|
} else if (prev.status !== job.status) {
|
|
@@ -285,6 +314,22 @@ setInterval(async () => {
|
|
|
285
314
|
} else {
|
|
286
315
|
jobCache[job.id] = { ...prev, ...job };
|
|
287
316
|
}
|
|
317
|
+
// Detect new tool calls via recentTools diff
|
|
318
|
+
const activeStatuses = new Set(['running', 'cloning']);
|
|
319
|
+
if (activeStatuses.has(job.status)) {
|
|
320
|
+
const prevTools = toolTrack[job.id] || [];
|
|
321
|
+
const currTools = job.recentTools || [];
|
|
322
|
+
const newTools = diffTools(prevTools, currTools);
|
|
323
|
+
toolTrack[job.id] = currTools;
|
|
324
|
+
if (newTools.length) {
|
|
325
|
+
const lines = newTools.map(t => `[tool] ${t}`);
|
|
326
|
+
broadcast({ type: 'job_output', id: job.id, lines });
|
|
327
|
+
// Also write to Redis output so it persists
|
|
328
|
+
const pipeline = redis.multi();
|
|
329
|
+
for (const l of lines) pipeline.rPush(`cca:job:${job.id}:output`, l);
|
|
330
|
+
pipeline.exec().catch(() => {});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
288
333
|
}
|
|
289
334
|
}
|
|
290
335
|
} catch (e) {
|
|
@@ -292,11 +337,21 @@ setInterval(async () => {
|
|
|
292
337
|
}
|
|
293
338
|
}, 2500);
|
|
294
339
|
|
|
295
|
-
// ── Polling: output for active jobs
|
|
340
|
+
// ── Polling: output for active + recently-finished jobs ───────────────────
|
|
341
|
+
const recentlyFinished = new Map(); // id → finishedTimestamp
|
|
296
342
|
setInterval(async () => {
|
|
343
|
+
const now = Date.now();
|
|
297
344
|
const activeStatuses = new Set(['running', 'cloning', 'pending_approval']);
|
|
298
|
-
|
|
299
|
-
|
|
345
|
+
// Include recently finished jobs for 15s to catch tail output
|
|
346
|
+
const toPoll = Object.values(jobCache).filter(j =>
|
|
347
|
+
activeStatuses.has(j.status) ||
|
|
348
|
+
(recentlyFinished.has(j.id) && now - recentlyFinished.get(j.id) < 15000)
|
|
349
|
+
);
|
|
350
|
+
for (const job of toPoll) {
|
|
351
|
+
// Track when jobs finish
|
|
352
|
+
if (!activeStatuses.has(job.status) && !recentlyFinished.has(job.id)) {
|
|
353
|
+
recentlyFinished.set(job.id, now);
|
|
354
|
+
}
|
|
300
355
|
try {
|
|
301
356
|
const lines = await pollNewOutput(job.id);
|
|
302
357
|
if (lines.length > 0) {
|
|
@@ -304,6 +359,10 @@ setInterval(async () => {
|
|
|
304
359
|
}
|
|
305
360
|
} catch {}
|
|
306
361
|
}
|
|
362
|
+
// Clean up old entries
|
|
363
|
+
for (const [id, ts] of recentlyFinished) {
|
|
364
|
+
if (now - ts > 30000) recentlyFinished.delete(id);
|
|
365
|
+
}
|
|
307
366
|
}, 900);
|
|
308
367
|
|
|
309
368
|
// ── Start ──────────────────────────────────────────────────────────────────
|