coxpit 5.0.1 → 5.0.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/board.ts +49 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coxpit",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
4
4
  "description": "Self-hosted cockpit for running a fleet of AI coding agents across your own machines — parallel worktree runs, live board, compare & merge, web terminal, design capture.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/board.ts CHANGED
@@ -58,7 +58,10 @@ export const BOARD_HTML = /* html */ `<!doctype html>
58
58
  .mdot.on{background:var(--s-done)}
59
59
 
60
60
  /* ── layout ─────────────────────────────── */
61
- .layout{display:grid;grid-template-columns:318px 1fr;min-height:calc(100vh - 55px)}
61
+ /* fixed viewport height so the rail stays put and each column scrolls on its own
62
+ (the board grew tall with many cards and dragged the rail's + New below the fold) */
63
+ .layout{display:grid;grid-template-columns:318px 1fr;height:calc(100vh - 55px);overflow:hidden}
64
+ .layout > aside,.layout > main{min-height:0}
62
65
  aside{border-right:1px solid var(--line);padding:20px 18px;background:var(--surface);
63
66
  display:flex;flex-direction:column;gap:26px}
64
67
  main{padding:20px;overflow:auto}
@@ -138,7 +141,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
138
141
  /* rail repo-row actions — icon-ghost, never white: transparent bg · muted icon · hover ink + surface */
139
142
  #repoList .repo .rmbtn{background:transparent;border:1px solid transparent;color:var(--muted);cursor:pointer;
140
143
  padding:3px;display:inline-flex;border-radius:6px;transition:color .15s,background .15s,border-color .15s}
141
- #repoList .repo .rmbtn:hover{color:var(--ink);background:var(--surface);border-color:var(--line-hi)}
144
+ #repoList .repo .rmbtn:hover{color:var(--ink);background:var(--surface2)}
142
145
  #repoList .repo .rmbtn .ic{width:13px;height:13px;color:inherit}
143
146
  .navi{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:8px;color:var(--muted);
144
147
  cursor:pointer;font-size:13px;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
@@ -182,9 +185,24 @@ export const BOARD_HTML = /* html */ `<!doctype html>
182
185
  .sheet-h .sp{flex:1}.sheet-h .x{color:var(--muted);cursor:pointer;background:none;border:none;padding:2px}
183
186
  .sheet #taskForm{display:flex;flex-direction:column;gap:13px;min-height:0}
184
187
  .sheet-body{display:flex;flex-direction:column;gap:8px;overflow-y:auto;min-height:0;max-height:60vh;padding:1px}
185
- .sheet-f{display:flex;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--line)}
186
- .sheet-f #modeSeg{flex:1}
187
- .sheet-f #runFleetBtn{flex:0 0 auto}
188
+ .sheet-f{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding-top:12px;border-top:1px solid var(--line)}
189
+ .sheet-f #modeSeg{flex:0 0 auto}
190
+ .sheet-f #taskCountStep{flex:0 0 auto;margin-left:auto}
191
+ .sheet-f #runFleetBtn{flex:1 1 100%;justify-content:center}
192
+ /* agent-mode switch — compact toggle (dry ⇄ real), never a native checkbox/white box */
193
+ .realtog{display:inline-flex;align-items:center;gap:8px;background:#0e1118;border:1px solid var(--line);
194
+ border-radius:999px;padding:5px 13px 5px 6px;cursor:pointer;font-family:var(--sans);font-size:12px;
195
+ font-weight:600;color:var(--muted);white-space:nowrap;transition:color .15s,border-color .15s}
196
+ .realtog:hover{border-color:var(--brand)}
197
+ .realtog .realtog-knob{width:30px;height:17px;border-radius:999px;background:var(--surface2);
198
+ position:relative;flex:none;transition:background .18s}
199
+ .realtog .realtog-knob::after{content:"";position:absolute;top:2px;left:2px;width:13px;height:13px;
200
+ border-radius:50%;background:var(--faint);transition:transform .18s,background .18s}
201
+ .realtog .realtog-hint{font-family:var(--mono);font-size:10px;font-weight:400;color:var(--faint)}
202
+ .realtog[aria-checked="true"]{color:var(--brand);border-color:rgba(78,201,176,.4)}
203
+ .realtog[aria-checked="true"] .realtog-knob{background:var(--brand-dim)}
204
+ .realtog[aria-checked="true"] .realtog-knob::after{transform:translateX(13px);background:var(--brand)}
205
+ .realtog:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
188
206
  /* progressive Options reveal (rarely-used Task fields) */
189
207
  .opt{border:1px solid var(--line);border-radius:var(--r-ctl);background:#0e1118;overflow:hidden}
190
208
  .opt-h{width:100%;display:flex;align-items:center;gap:8px;background:transparent;border:none;cursor:pointer;
@@ -221,7 +239,8 @@ export const BOARD_HTML = /* html */ `<!doctype html>
221
239
  .dd-btn:hover{border-color:var(--line-hi)}
222
240
  .dd.open .dd-btn{border-color:rgba(78,201,176,.55)}
223
241
  .dd-lbl{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
224
- .dd-car{color:var(--faint);font-size:10px;transition:transform .15s}
242
+ .dd-car{color:var(--faint);display:inline-flex;align-items:center;transition:transform .15s}
243
+ .dd-car .ic{width:13px;height:13px}
225
244
  .dd.open .dd-car{transform:rotate(180deg)}
226
245
  .dd-panel{position:absolute;top:calc(100% + 5px);left:0;right:0;z-index:40;background:var(--surface2);
227
246
  border:1px solid var(--line-hi);border-radius:9px;box-shadow:var(--shadow);max-height:230px;
@@ -249,7 +268,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
249
268
  .seg{display:flex;gap:3px;padding:3px;border:1px solid var(--line);border-radius:var(--r-ctl);background:#0e1118}
250
269
  /* slim single-row option — matches the type/provider seg height; the risky hint rides inline, subtle */
251
270
  .seg-opt{flex:1;display:inline-flex;flex-direction:row;align-items:center;justify-content:center;gap:5px;
252
- padding:6px 8px;border:none;min-width:0;
271
+ padding:6px 8px;border:none;min-width:0;white-space:nowrap;line-height:1.2;
253
272
  background:transparent;color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;
254
273
  border-radius:calc(var(--r-ctl) - 3px);transition:background .15s,color .15s}
255
274
  .seg-opt:hover{color:var(--ink)}
@@ -710,7 +729,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
710
729
  .menu-btn{display:none;font-size:15px;padding:5px 10px}
711
730
  .scrim{position:fixed;inset:0;top:54px;background:rgba(5,7,10,.5);z-index:29;display:none}
712
731
  @media (max-width:860px){
713
- .layout{grid-template-columns:1fr;min-height:calc(100dvh - 55px)}
732
+ .layout{grid-template-columns:1fr;height:calc(100dvh - 55px)}
714
733
  /* 사이드바 = 오프캔버스 드로어 — 플릿이 첫 화면, 런처는 ☰ 뒤에 */
715
734
  .menu-btn{display:inline-flex}
716
735
  aside{position:fixed;top:54px;bottom:0;left:0;width:min(86vw,340px);max-width:100vw;z-index:30;
@@ -880,10 +899,11 @@ ${ICON_SPRITE}
880
899
  </div>
881
900
  </div>
882
901
  <div class="sheet-f" id="sheetFooter">
883
- <div class="seg" id="modeSeg" role="group" aria-label="agent mode">
884
- <button type="button" class="seg-opt" data-real="0">Dry run</button>
885
- <button type="button" class="seg-opt" data-real="1">Real agent<span class="seg-hint">spends credits</span></button>
886
- </div>
902
+ <button type="button" id="modeSeg" class="realtog" role="switch" aria-checked="false" title="Dry run rehearses the full pipeline with zero credits. Turn on to run the real agent CLI.">
903
+ <span class="realtog-knob"></span>
904
+ <span class="realtog-lbl" id="modeRealLbl">Dry run</span>
905
+ <span class="realtog-hint" id="modeRealHint">rehearsal</span>
906
+ </button>
887
907
  <input type="checkbox" id="taskReal" hidden />
888
908
  <div class="stepper" id="taskCountStep" title="agents — 1 for a job, N to explore variants">
889
909
  <button type="button" class="stp" id="cntDown" aria-label="fewer agents"><svg class="ic"><use href="#i-minus"/></svg></button>
@@ -1199,7 +1219,7 @@ function dressSelect(id){
1199
1219
  const sel = $(id); if(!sel || sel.dataset.dd) return;
1200
1220
  sel.dataset.dd = '1';
1201
1221
  const dd = document.createElement('div'); dd.className = 'dd';
1202
- dd.innerHTML = '<button type="button" class="dd-btn"><span class="dd-lbl"></span><span class="dd-car">▾</span></button><div class="dd-panel"></div>';
1222
+ dd.innerHTML = '<button type="button" class="dd-btn"><span class="dd-lbl"></span><span class="dd-car"><svg class="ic"><use href="#i-chevron-down"/></svg></span></button><div class="dd-panel"></div>';
1203
1223
  sel.parentNode.insertBefore(dd, sel);
1204
1224
  sel.style.display = 'none';
1205
1225
  dd.querySelector('.dd-btn').addEventListener('click', (e)=>{
@@ -1760,19 +1780,19 @@ function renderRail(){
1760
1780
  const cnt = railCounts();
1761
1781
  let html = '';
1762
1782
  if (repos.length){
1763
- html += '<button type="button" class="repo'+(selectedRepo==null?' on':'')+'" data-repo="all">'
1764
- + ic('layers')+'<span class="nm">All repositories</span></button>';
1783
+ html += '<div class="repo'+(selectedRepo==null?' on':'')+'" role="button" tabindex="0" data-repo="all">'
1784
+ + ic('layers')+'<span class="nm">All repositories</span></div>';
1765
1785
  for (const r of repos){
1766
1786
  const c = cnt.get(r.id) || { active:0, attn:false };
1767
1787
  const on = selectedRepo===r.id;
1768
- html += '<button type="button" class="repo'+(on?' on':'')+'" data-repo="'+r.id+'" title="'+escA(r.path||r.name)+'">'
1788
+ html += '<div class="repo'+(on?' on':'')+'" role="button" tabindex="0" data-repo="'+r.id+'" title="'+escA(r.path||r.name)+'">'
1769
1789
  + ic('folder')+'<span class="nm">'+esc(r.name)+'</span>'
1770
1790
  + (c.attn ? '<span class="attn" title="a run needs a hand"></span>' : '')
1771
1791
  + (c.active>0 ? '<span class="cnt">'+c.active+'</span>' : '')
1772
1792
  + '<span class="rowmenu">'
1773
1793
  + '<button type="button" class="rmbtn" data-rbranch="'+r.id+'" title="base branch">'+ic('branch')+'</button>'
1774
1794
  + '<button type="button" class="rmbtn" data-rremove="'+r.id+'" title="remove">'+ic('x')+'</button>'
1775
- + '</span></button>';
1795
+ + '</span></div>';
1776
1796
  }
1777
1797
  } else {
1778
1798
  // empty state — 첫 repo 추가 CTA
@@ -1800,6 +1820,12 @@ $('repoList').addEventListener('click', (e)=>{
1800
1820
  const row = e.target.closest('.repo[data-repo]'); if(!row) return;
1801
1821
  setScope(row.dataset.repo==='all' ? null : Number(row.dataset.repo));
1802
1822
  });
1823
+ $('repoList').addEventListener('keydown', (e)=>{
1824
+ if (e.key!=='Enter' && e.key!==' ') return;
1825
+ const row = e.target.closest('.repo[data-repo]'); if(!row) return;
1826
+ e.preventDefault();
1827
+ setScope(row.dataset.repo==='all' ? null : Number(row.dataset.repo));
1828
+ });
1803
1829
  $('captures').addEventListener('click', async (e)=>{
1804
1830
  const b = e.target.closest('button[data-delcap]'); if(!b) return;
1805
1831
  await fetch('/api/design/'+b.dataset.delcap,{method:'DELETE'});
@@ -3030,7 +3056,7 @@ function setV(tab){
3030
3056
  $('panelBench').hidden = tab!=='bench';
3031
3057
  $('panelBench').style.display = tab==='bench' ? 'flex' : 'none';
3032
3058
  // footer adapts: Task=Dry/Real·count·Run fleet · Goal=Dry/Real·Plan & run · Workbench=Open workbench only
3033
- $('modeSeg').style.display = tab==='bench' ? 'none' : 'flex'; // workbench 는 에이전트 없음
3059
+ $('modeSeg').style.display = tab==='bench' ? 'none' : 'inline-flex'; // workbench 는 에이전트 없음
3034
3060
  $('taskCountStep').style.display = tab==='task' ? 'flex' : 'none'; // count 는 Task 만(플래너·워크벤치는 1)
3035
3061
  $('runFleetBtn').innerHTML = ic(L_ICON[tab]) + ' ' + L_LABEL[tab];
3036
3062
  }
@@ -3174,18 +3200,15 @@ $('cntDown').addEventListener('click', ()=>stepCount(-1));
3174
3200
  $('cntUp').addEventListener('click', ()=>stepCount(1));
3175
3201
  $('taskCount').addEventListener('change', ()=>stepCount(0)); // clamp manual typing
3176
3202
 
3177
- /* ── agent mode segmented control (mirrors hidden #taskReal) ── */
3178
- const segOpts = Array.from(document.querySelectorAll('#modeSeg .seg-opt'));
3203
+ /* ── agent mode switch (compact toggle mirroring hidden #taskReal) ── */
3179
3204
  function setMode(real, persist){
3180
3205
  $('taskReal').checked = real;
3181
- for (const b of segOpts){
3182
- const on = (b.dataset.real === '1') === real;
3183
- b.classList.toggle('on', on);
3184
- b.setAttribute('aria-pressed', on ? 'true' : 'false');
3185
- }
3206
+ $('modeSeg').setAttribute('aria-checked', real ? 'true' : 'false');
3207
+ $('modeRealLbl').textContent = real ? 'Real agent' : 'Dry run';
3208
+ $('modeRealHint').textContent = real ? 'spends credits' : 'rehearsal';
3186
3209
  if (persist) try { localStorage.setItem('coxpit.real', real ? '1' : '0'); } catch {}
3187
3210
  }
3188
- for (const b of segOpts) b.addEventListener('click', ()=>setMode(b.dataset.real === '1', true));
3211
+ $('modeSeg').addEventListener('click', ()=>setMode(!$('taskReal').checked, true));
3189
3212
  let savedMode = null;
3190
3213
  try { savedMode = localStorage.getItem('coxpit.real'); } catch {}
3191
3214
  setMode(savedMode === '1', false);