coxpit 3.5.0 → 3.6.0
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/README.md +1 -0
- package/package.json +1 -1
- package/src/board.ts +52 -6
- package/src/config.ts +2 -0
- package/src/orchestrator.ts +2 -0
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ Your keys and login never touch coxpit's config or database.
|
|
|
75
75
|
| `COXPIT_CODEX_BIN` | `codex` | Codex CLI command (optional second provider) |
|
|
76
76
|
| `COXPIT_CODEX_SANDBOX` | `workspace-write` | Codex sandbox policy (`danger-full-access` for full autonomy) |
|
|
77
77
|
| `COXPIT_WEBHOOK_URL` | — | POSTs `{event:"run.settled",run:{...}}` when a run finishes — wire it to Telegram, Slack, anything |
|
|
78
|
+
| `COXPIT_PUBLIC_URL` | — | if set, the webhook payload adds `url: <base>/?run=<id>` — tap it on your phone and the board opens that run |
|
|
78
79
|
|
|
79
80
|
Running on the open internet? Put it behind your own front door (Tailscale, Cloudflare Access, a reverse proxy with TLS) and keep basic auth on — it exposes shells.
|
|
80
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
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
|
@@ -4,7 +4,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
4
4
|
<html lang="en">
|
|
5
5
|
<head>
|
|
6
6
|
<meta charset="utf-8" />
|
|
7
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
8
8
|
<title>coxpit · fleet</title>
|
|
9
9
|
<link rel="stylesheet" href="/vendor/xterm.css" />
|
|
10
10
|
<style>
|
|
@@ -295,22 +295,51 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
295
295
|
#xterm{width:100%;height:100%}
|
|
296
296
|
.term-hint{font-family:var(--mono);font-size:11px;color:var(--faint)}
|
|
297
297
|
|
|
298
|
+
/* ── mobile · the pocket pass ───────────── */
|
|
299
|
+
.menu-btn{display:none;font-size:15px;padding:5px 10px}
|
|
300
|
+
.scrim{position:fixed;inset:0;top:54px;background:rgba(5,7,10,.5);z-index:29;display:none}
|
|
298
301
|
@media (max-width:860px){
|
|
299
|
-
.layout{grid-template-columns:1fr}
|
|
300
|
-
|
|
301
|
-
.
|
|
302
|
+
.layout{grid-template-columns:1fr;min-height:calc(100dvh - 55px)}
|
|
303
|
+
/* 사이드바 = 오프캔버스 드로어 — 플릿이 첫 화면, 런처는 ☰ 뒤에 */
|
|
304
|
+
.menu-btn{display:inline-flex}
|
|
305
|
+
aside{position:fixed;top:54px;bottom:0;left:0;width:min(86vw,340px);z-index:30;
|
|
306
|
+
transform:translateX(-103%);transition:transform .22s ease;overflow-y:auto;
|
|
307
|
+
-webkit-overflow-scrolling:touch;box-shadow:var(--shadow)}
|
|
308
|
+
aside.open{transform:translateX(0)}
|
|
309
|
+
.scrim.on{display:block}
|
|
310
|
+
header{padding:0 12px;gap:9px}
|
|
311
|
+
.brand .sub,.daemon-badge,.machines{display:none}
|
|
312
|
+
main{padding:14px}
|
|
313
|
+
.grid{grid-template-columns:1fr}
|
|
314
|
+
/* 모달 = 풀블리드 */
|
|
315
|
+
.overlay.open{padding:0}
|
|
316
|
+
.modal,.modal.wide{width:100%;height:100dvh;max-height:none;border:none;border-radius:0}
|
|
317
|
+
.modal-b{grid-template-columns:1fr;grid-template-rows:1fr 1fr}
|
|
302
318
|
.pane{border-right:none;border-bottom:1px solid var(--line)}
|
|
319
|
+
.modal-f{flex-wrap:wrap;padding:10px 12px}
|
|
320
|
+
#steerRow .seg{flex:0 0 108px}
|
|
321
|
+
/* iOS: 16px 미만 input 포커스 시 강제 줌 — 폰에서만 16px 로 */
|
|
322
|
+
input,textarea,select,.dd-btn{font-size:16px}
|
|
323
|
+
/* 온보딩 패널이 좁은 화면을 넘치지 않게 */
|
|
324
|
+
.empty{padding:24px 0}
|
|
325
|
+
.setup{max-width:100%;width:100%;margin:8px 0}
|
|
326
|
+
.chk .v{min-width:0}
|
|
327
|
+
.cmp{flex-direction:column;overflow-y:auto;overflow-x:hidden}
|
|
328
|
+
.cmp-col{min-width:0;border-right:none;border-bottom:1px solid var(--line);flex:0 0 auto;max-height:72vh}
|
|
303
329
|
}
|
|
330
|
+
@media (prefers-reduced-motion:reduce){aside{transition:none}}
|
|
304
331
|
</style>
|
|
305
332
|
</head>
|
|
306
333
|
<body>
|
|
307
334
|
<header>
|
|
335
|
+
<button class="btn-ghost sm menu-btn" id="menuBtn" aria-label="open launcher">☰</button>
|
|
308
336
|
<div class="brand"><span class="mark">coxpit</span><span class="sub">fleet console</span></div>
|
|
309
337
|
<span class="daemon-badge" id="daemonBadge" style="display:none"></span>
|
|
310
338
|
<div class="ws"><span class="dot" id="wsdot"></span><span id="wstext">connecting</span></div>
|
|
311
339
|
<button class="btn-ghost sm" id="bell" title="notify when a run settles">🔕</button>
|
|
312
340
|
<div class="machines" id="machines"></div>
|
|
313
341
|
</header>
|
|
342
|
+
<div class="scrim" id="scrim"></div>
|
|
314
343
|
<div class="layout">
|
|
315
344
|
<aside>
|
|
316
345
|
<div class="sect">
|
|
@@ -868,10 +897,12 @@ function notifySettleUI(ev){
|
|
|
868
897
|
if (!notifyOn) return;
|
|
869
898
|
const t = tasks.get(ev.taskId ?? (runs.get(ev.runId)||{}).taskId);
|
|
870
899
|
try {
|
|
871
|
-
new Notification('coxpit · r'+ev.runId+' '+ev.status, {
|
|
900
|
+
const n = new Notification('coxpit · r'+ev.runId+' '+ev.status, {
|
|
872
901
|
body: (t ? t.title+' — ' : '') + (ev.filesChanged??0)+' file(s) changed',
|
|
873
902
|
tag: 'coxpit-r'+ev.runId,
|
|
874
903
|
});
|
|
904
|
+
// 딥링크 — 알림 탭이 곧 그 run 의 모달
|
|
905
|
+
n.onclick = ()=>{ try{ window.focus(); }catch{} if (runs.has(ev.runId)) openModal(ev.runId); n.close(); };
|
|
875
906
|
} catch {}
|
|
876
907
|
}
|
|
877
908
|
|
|
@@ -1323,6 +1354,7 @@ $('repoForm').addEventListener('submit', async (e)=>{
|
|
|
1323
1354
|
});
|
|
1324
1355
|
$('taskForm').addEventListener('submit', async (e)=>{
|
|
1325
1356
|
e.preventDefault();
|
|
1357
|
+
setDrawer(false); // 모바일: 발사하면 드로어를 닫고 플릿을 보여준다
|
|
1326
1358
|
if (lTab === 'goal') return submitGoal();
|
|
1327
1359
|
if (lTab === 'bench') return submitBench();
|
|
1328
1360
|
const repoId = Number($('taskRepo').value);
|
|
@@ -1371,7 +1403,21 @@ let savedAgent = null;
|
|
|
1371
1403
|
try { savedAgent = localStorage.getItem('coxpit.agent'); } catch {}
|
|
1372
1404
|
if (savedAgent === 'codex') setProvider('codex', false);
|
|
1373
1405
|
|
|
1374
|
-
|
|
1406
|
+
/* ── mobile drawer ── */
|
|
1407
|
+
const asideEl = document.querySelector('aside');
|
|
1408
|
+
function setDrawer(on){ asideEl.classList.toggle('open', on); $('scrim').classList.toggle('on', on); }
|
|
1409
|
+
$('menuBtn').addEventListener('click', ()=>setDrawer(!asideEl.classList.contains('open')));
|
|
1410
|
+
$('scrim').addEventListener('click', ()=>setDrawer(false));
|
|
1411
|
+
|
|
1412
|
+
/* 딥링크 — /?run=N 이면 하이드레이션 후 그 run 모달을 연다 (웹훅 링크·알림용) */
|
|
1413
|
+
function openFromURL(){
|
|
1414
|
+
const q = new URLSearchParams(location.search).get('run');
|
|
1415
|
+
const id = Number(q);
|
|
1416
|
+
if (q && runs.has(id)){ openModal(id); }
|
|
1417
|
+
if (q) history.replaceState(null, '', location.pathname);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
hydrate().then(()=>{ connectWS(); openFromURL(); });
|
|
1375
1421
|
</script>
|
|
1376
1422
|
</body>
|
|
1377
1423
|
</html>`;
|
package/src/config.ts
CHANGED
|
@@ -65,6 +65,8 @@ export const config = {
|
|
|
65
65
|
sshKey: process.env.COXPIT_SSH_KEY ?? '',
|
|
66
66
|
// run 정착 시 POST 할 웹훅(선택) — 텔레그램 브릿지 등 사용자 연결용.
|
|
67
67
|
webhookUrl: process.env.COXPIT_WEBHOOK_URL ?? '',
|
|
68
|
+
// 웹훅 payload 의 딥링크 base(선택) — 설정 시 url: <base>/?run=<id> 포함 (폰에서 탭 → 그 run)
|
|
69
|
+
publicUrl: (process.env.COXPIT_PUBLIC_URL ?? '').replace(/\/+$/, ''),
|
|
68
70
|
agent: {
|
|
69
71
|
// 기본 드라이런(모의 에이전트). 실제 CLI 실행은 켤 때만(크레딧 소모).
|
|
70
72
|
real: process.env.COXPIT_AGENT_REAL === '1',
|
package/src/orchestrator.ts
CHANGED
|
@@ -218,6 +218,8 @@ async function notifySettle(runId: number, status: string, filesChanged: number,
|
|
|
218
218
|
body: JSON.stringify({
|
|
219
219
|
event: 'run.settled',
|
|
220
220
|
run: { id: runId, status, filesChanged, exitSummary: exitSummary.slice(0, 300), task: tr[0]?.title ?? '' },
|
|
221
|
+
// COXPIT_PUBLIC_URL 설정 시 폰에서 탭 → 보드가 그 run 모달을 딥링크로 연다
|
|
222
|
+
...(config.publicUrl ? { url: `${config.publicUrl}/?run=${runId}` } : {}),
|
|
221
223
|
}),
|
|
222
224
|
signal: AbortSignal.timeout(8000),
|
|
223
225
|
});
|