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 CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "cc-agent-ui",
3
- "version": "0.2.0",
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: grid;
244
- grid-template-columns: repeat(var(--cols, 3), 460px);
245
- gap: 14px;
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, lineCount }
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
- // Use actual DOM layout positions (accurate regardless of grid index math)
820
- const cardX = card.offsetLeft;
821
- const cardY = card.offsetTop;
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 - (cardX + cardW/2) * scale;
826
- oy = vh/2 - (cardY + cardH/2) * scale;
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
- // ── Handle snapshot (initial load) ────────────────────────────────────────
871
- function handleSnapshot(data) {
872
- // Sort: running first, then pending, then recent done/failed
873
- const sorted = [...data.jobs].sort((a, b) => {
874
- const order = { running:0, cloning:1, pending_approval:2, failed:3, done:4, cancelled:5 };
875
- return (order[a.status]??9) - (order[b.status]??9) ||
876
- new Date(b.startedAt||0) - new Date(a.startedAt||0);
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
- for (const job of sorted) {
880
- addJob(job, job.lines || []);
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; // already exists
988
+ if (jobs[job.id]) return;
891
989
  jobCounter++;
892
990
  const { card, logEl } = makeCard(job, jobCounter);
893
- inner.appendChild(card);
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
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  /**
2
3
  * cc-agent-ui server — plugged into Redis directly.
3
4
  *