cc-agent-ui 0.2.0 → 0.2.2
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 +4 -1
- package/public/index.html +127 -33
- package/server.js +1 -0
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-agent-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Live canvas UI for cc-agent jobs — infinite canvas, streaming output, file browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/Gonzih/cc-agent-ui.git"
|
|
9
9
|
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"cc-agent-ui": "./server.js"
|
|
12
|
+
},
|
|
10
13
|
"scripts": {
|
|
11
14
|
"start": "node server.js"
|
|
12
15
|
},
|
package/public/index.html
CHANGED
|
@@ -240,13 +240,48 @@ body {
|
|
|
240
240
|
position: absolute;
|
|
241
241
|
top: 0; left: 0;
|
|
242
242
|
transform-origin: 0 0;
|
|
243
|
-
display:
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
display: flex;
|
|
244
|
+
flex-direction: row;
|
|
245
|
+
align-items: flex-start;
|
|
246
|
+
gap: 18px;
|
|
246
247
|
padding: 24px;
|
|
247
248
|
width: max-content;
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/* ── Repo Column ──────────────────────────────────────────────────────── */
|
|
252
|
+
.repo-col {
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
gap: 10px;
|
|
256
|
+
width: 460px;
|
|
257
|
+
flex-shrink: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.col-header {
|
|
261
|
+
padding: 8px 12px;
|
|
262
|
+
background: rgba(0,0,0,0.35);
|
|
263
|
+
border: 1px solid var(--border);
|
|
264
|
+
border-radius: 5px;
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
gap: 8px;
|
|
268
|
+
flex-shrink: 0;
|
|
269
|
+
}
|
|
270
|
+
.col-repo-icon { color: var(--dimmer); font-size: 12px; }
|
|
271
|
+
.col-repo-name {
|
|
272
|
+
font-size: 11px; font-weight: 600; color: var(--blue);
|
|
273
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
274
|
+
flex: 1;
|
|
275
|
+
}
|
|
276
|
+
.col-counts { font-size: 9px; color: var(--dim); flex-shrink: 0; }
|
|
277
|
+
.col-counts.live { color: var(--cyan); }
|
|
278
|
+
|
|
279
|
+
.col-cards {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
gap: 10px;
|
|
283
|
+
}
|
|
284
|
+
|
|
250
285
|
/* ── Terminal Card ────────────────────────────────────────────────────── */
|
|
251
286
|
.tcard {
|
|
252
287
|
width: 460px;
|
|
@@ -595,7 +630,8 @@ body {
|
|
|
595
630
|
|
|
596
631
|
<script>
|
|
597
632
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
598
|
-
const jobs = {}; // id → { job, card, logEl,
|
|
633
|
+
const jobs = {}; // id → { job, card, logEl, n }
|
|
634
|
+
const cols = {}; // repoKey → { el, cardsEl }
|
|
599
635
|
let filterMode = 'all';
|
|
600
636
|
let jobCounter = 0;
|
|
601
637
|
let scale = 1, ox = 40, oy = 40;
|
|
@@ -816,14 +852,14 @@ function focusCard(id) {
|
|
|
816
852
|
applyFilter();
|
|
817
853
|
}
|
|
818
854
|
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
855
|
+
// Walk offsetParent chain up to inner for accurate canvas coordinates
|
|
856
|
+
let cx = 0, cy = 0, el = card;
|
|
857
|
+
while (el && el !== inner) { cx += el.offsetLeft; cy += el.offsetTop; el = el.offsetParent; }
|
|
822
858
|
const cardW = card.offsetWidth || 460;
|
|
823
859
|
const cardH = card.offsetHeight || 370;
|
|
824
860
|
const vw = viewport.clientWidth, vh = viewport.clientHeight;
|
|
825
|
-
ox = vw/2 - (
|
|
826
|
-
oy = vh/2 - (
|
|
861
|
+
ox = vw/2 - (cx + cardW/2) * scale;
|
|
862
|
+
oy = vh/2 - (cy + cardH/2) * scale;
|
|
827
863
|
applyTransform();
|
|
828
864
|
|
|
829
865
|
// Scroll terminal to bottom
|
|
@@ -856,6 +892,11 @@ function applyFilter() {
|
|
|
856
892
|
if (item) item.style.display = show ? '' : 'none';
|
|
857
893
|
if (card) card.style.display = show ? '' : 'none';
|
|
858
894
|
}
|
|
895
|
+
// Hide columns with no visible cards
|
|
896
|
+
for (const [key, col] of Object.entries(cols)) {
|
|
897
|
+
const hasVisible = [...col.cardsEl.children].some(c => c.style.display !== 'none');
|
|
898
|
+
col.el.style.display = hasVisible ? '' : 'none';
|
|
899
|
+
}
|
|
859
900
|
}
|
|
860
901
|
|
|
861
902
|
document.querySelectorAll('.fbtn').forEach(btn => {
|
|
@@ -867,34 +908,94 @@ document.querySelectorAll('.fbtn').forEach(btn => {
|
|
|
867
908
|
});
|
|
868
909
|
});
|
|
869
910
|
|
|
870
|
-
// ──
|
|
871
|
-
function
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
911
|
+
// ── Column helpers ─────────────────────────────────────────────────────────
|
|
912
|
+
function repoKey(job) {
|
|
913
|
+
return shortRepo(job.repoUrl) || 'local';
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function getOrCreateCol(key) {
|
|
917
|
+
if (cols[key]) return cols[key];
|
|
918
|
+
const el = document.createElement('div');
|
|
919
|
+
el.className = 'repo-col';
|
|
920
|
+
el.id = `col-${key.replace(/[^a-z0-9]/gi, '-')}`;
|
|
921
|
+
el.innerHTML = `
|
|
922
|
+
<div class="col-header">
|
|
923
|
+
<span class="col-repo-icon">⎇</span>
|
|
924
|
+
<span class="col-repo-name" title="${escHtml(key)}">${escHtml(key)}</span>
|
|
925
|
+
<span class="col-counts"></span>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="col-cards"></div>
|
|
928
|
+
`;
|
|
929
|
+
cols[key] = { el, cardsEl: el.querySelector('.col-cards') };
|
|
930
|
+
inner.appendChild(el);
|
|
931
|
+
return cols[key];
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function insertCardInColumn(key, card, job) {
|
|
935
|
+
const { cardsEl } = cols[key];
|
|
936
|
+
const t = new Date(job.startedAt || 0).getTime();
|
|
937
|
+
for (const sib of cardsEl.children) {
|
|
938
|
+
const sibJob = jobs[sib.id.replace('card-', '')]?.job;
|
|
939
|
+
if (t >= new Date(sibJob?.startedAt || 0).getTime()) {
|
|
940
|
+
cardsEl.insertBefore(card, sib);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
cardsEl.appendChild(card);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function updateColHeader(key) {
|
|
948
|
+
const col = cols[key];
|
|
949
|
+
if (!col) return;
|
|
950
|
+
const colJobs = Object.values(jobs).filter(e => repoKey(e.job) === key);
|
|
951
|
+
const running = colJobs.filter(e => ['running','cloning','pending_approval'].includes(e.job.status)).length;
|
|
952
|
+
const el = col.el.querySelector('.col-counts');
|
|
953
|
+
if (!el) return;
|
|
954
|
+
if (running > 0) {
|
|
955
|
+
el.textContent = `${running} live · ${colJobs.length}`;
|
|
956
|
+
el.className = 'col-counts live';
|
|
957
|
+
} else {
|
|
958
|
+
el.textContent = `${colJobs.length}`;
|
|
959
|
+
el.className = 'col-counts';
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function sortColumns() {
|
|
964
|
+
// Order columns by their most recent job's startedAt, descending
|
|
965
|
+
const keys = Object.keys(cols).sort((a, b) => {
|
|
966
|
+
const latest = key => Math.max(0, ...Object.values(jobs)
|
|
967
|
+
.filter(e => repoKey(e.job) === key)
|
|
968
|
+
.map(e => new Date(e.job.startedAt || 0).getTime()));
|
|
969
|
+
return latest(b) - latest(a);
|
|
877
970
|
});
|
|
971
|
+
for (const key of keys) inner.appendChild(cols[key].el);
|
|
972
|
+
}
|
|
878
973
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
974
|
+
// ── Handle snapshot (initial load) ────────────────────────────────────────
|
|
975
|
+
function handleSnapshot(data) {
|
|
976
|
+
// Sort purely by time descending — columns handle repo grouping visually
|
|
977
|
+
const sorted = [...data.jobs].sort((a, b) =>
|
|
978
|
+
new Date(b.startedAt||0) - new Date(a.startedAt||0)
|
|
979
|
+
);
|
|
980
|
+
for (const job of sorted) addJob(job, job.lines || []);
|
|
981
|
+
sortColumns();
|
|
882
982
|
updateCounts();
|
|
883
983
|
applyFilter();
|
|
884
|
-
if (Object.keys(jobs).length > 0)
|
|
885
|
-
emptyState.style.display = 'none';
|
|
886
|
-
}
|
|
984
|
+
if (Object.keys(jobs).length > 0) emptyState.style.display = 'none';
|
|
887
985
|
}
|
|
888
986
|
|
|
889
987
|
function addJob(job, lines) {
|
|
890
|
-
if (jobs[job.id]) return;
|
|
988
|
+
if (jobs[job.id]) return;
|
|
891
989
|
jobCounter++;
|
|
892
990
|
const { card, logEl } = makeCard(job, jobCounter);
|
|
893
|
-
|
|
991
|
+
const key = repoKey(job);
|
|
992
|
+
getOrCreateCol(key);
|
|
993
|
+
insertCardInColumn(key, card, job);
|
|
894
994
|
const sidebarItem = makeSidebarItem(job);
|
|
895
995
|
jobList.prepend(sidebarItem);
|
|
896
996
|
jobs[job.id] = { job: { ...job }, card, logEl, n: jobCounter };
|
|
897
997
|
if (lines.length) appendLines(logEl, lines, false);
|
|
998
|
+
updateColHeader(key);
|
|
898
999
|
}
|
|
899
1000
|
|
|
900
1001
|
// ── Handle job update ──────────────────────────────────────────────────────
|
|
@@ -940,6 +1041,7 @@ function handleJobUpdate(data) {
|
|
|
940
1041
|
const dot = $(`si-${job.id}`)?.querySelector('.ji-status');
|
|
941
1042
|
if (dot) { dot.className = `ji-status ${status}`; }
|
|
942
1043
|
|
|
1044
|
+
updateColHeader(repoKey(job));
|
|
943
1045
|
updateCounts();
|
|
944
1046
|
applyFilter();
|
|
945
1047
|
}
|
|
@@ -947,6 +1049,7 @@ function handleJobUpdate(data) {
|
|
|
947
1049
|
// ── Handle job_new ─────────────────────────────────────────────────────────
|
|
948
1050
|
function handleJobNew(data) {
|
|
949
1051
|
addJob(data.job, data.job.lines || []);
|
|
1052
|
+
sortColumns();
|
|
950
1053
|
emptyState.style.display = 'none';
|
|
951
1054
|
updateCounts();
|
|
952
1055
|
applyFilter();
|
|
@@ -1117,15 +1220,6 @@ setInterval(() => {
|
|
|
1117
1220
|
}
|
|
1118
1221
|
}, 30000);
|
|
1119
1222
|
|
|
1120
|
-
// Auto-layout columns based on viewport width
|
|
1121
|
-
function updateCols() {
|
|
1122
|
-
const vw = viewport.clientWidth;
|
|
1123
|
-
const cols = Math.max(1, Math.floor((vw - 40) / 474));
|
|
1124
|
-
inner.style.setProperty('--cols', cols);
|
|
1125
|
-
}
|
|
1126
|
-
updateCols();
|
|
1127
|
-
new ResizeObserver(updateCols).observe(viewport);
|
|
1128
|
-
|
|
1129
1223
|
connect();
|
|
1130
1224
|
</script>
|
|
1131
1225
|
</body>
|
package/server.js
CHANGED