coxpit 5.13.0 → 5.14.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 +1 -1
- package/src/board.ts +3 -1
- package/src/cockpit.ts +103 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.2",
|
|
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": "SEE LICENSE IN LICENSE.md",
|
package/src/board.ts
CHANGED
|
@@ -845,7 +845,9 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
845
845
|
.fab{display:inline-flex}
|
|
846
846
|
#newBtn,.newnote{display:none}
|
|
847
847
|
header{padding:0 12px;gap:9px}
|
|
848
|
-
.brand .sub,.daemon-badge,.machines
|
|
848
|
+
.brand .sub,.daemon-badge,.machines{display:none}
|
|
849
|
+
/* cockpit(터미널) 링크는 모바일에서도 보이게 — 터미널 워크스페이스로 가는 유일한 입구 */
|
|
850
|
+
.cockpit-link{display:inline-flex;margin-left:auto}
|
|
849
851
|
main{padding:14px}
|
|
850
852
|
.grid{grid-template-columns:1fr}
|
|
851
853
|
/* 모달 = 풀블리드 */
|
package/src/cockpit.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
5
5
|
<html lang="en">
|
|
6
6
|
<head>
|
|
7
7
|
<meta charset="utf-8" />
|
|
8
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
|
|
9
9
|
<title>coxpit · cockpit</title>
|
|
10
10
|
<link rel="icon" href="/brand/favicon.ico" sizes="any" />
|
|
11
11
|
<link rel="icon" type="image/png" sizes="32x32" href="/brand/favicon-32.png" />
|
|
@@ -23,9 +23,13 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
23
23
|
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,Roboto,sans-serif;
|
|
24
24
|
}
|
|
25
25
|
*{box-sizing:border-box}
|
|
26
|
-
|
|
26
|
+
/* 모바일 앱처럼 잠금 — 러버밴드·핀치줌 차단(뷰포트 meta user-scalable=no 와 함께).
|
|
27
|
+
body overflow:hidden 이라 앱셸은 스크롤 안 하고, 안쪽 컨테이너(터미널·트리)만 스크롤.
|
|
28
|
+
position:fixed 는 iOS 소프트키보드가 입력바를 가려 회피 — overscroll-behavior:none 으로 충분. */
|
|
29
|
+
html,body{height:100%;overscroll-behavior:none}
|
|
27
30
|
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px;line-height:1.5;
|
|
28
|
-
-webkit-font-smoothing:antialiased;overflow:hidden}
|
|
31
|
+
-webkit-font-smoothing:antialiased;overflow:hidden;overscroll-behavior:none;touch-action:manipulation}
|
|
32
|
+
.term-host .xterm-viewport{overscroll-behavior:contain} /* 터미널 스크롤이 페이지로 안 번지게 */
|
|
29
33
|
button{font-family:var(--sans)}
|
|
30
34
|
:focus-visible{outline:2px solid rgba(78,201,176,.5);outline-offset:1px;border-radius:4px}
|
|
31
35
|
|
|
@@ -49,7 +53,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
49
53
|
.toggle{font-size:12px;color:var(--muted);text-decoration:none;border:1px solid var(--line);border-radius:7px;padding:5px 11px;background:none;cursor:pointer;font-family:var(--mono)}
|
|
50
54
|
.toggle:hover{color:var(--ink);border-color:var(--line-hi)}
|
|
51
55
|
|
|
52
|
-
.layout{display:grid;grid-template-columns:270px 1fr;height:calc(
|
|
56
|
+
.layout{display:grid;grid-template-columns:270px 1fr;height:calc(100dvh - 46px)}
|
|
53
57
|
.layout > *{min-height:0;min-width:0}
|
|
54
58
|
|
|
55
59
|
/* ── workspace tree ── */
|
|
@@ -229,26 +233,70 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
229
233
|
.dl-file{color:var(--brand)} .dl-hunk{color:var(--open)} .dl-ctx{color:var(--muted)}
|
|
230
234
|
.dl-add{color:#7fdca0;background:rgba(88,179,104,.08)} .dl-del{color:#e58a92;background:rgba(226,91,103,.08)}
|
|
231
235
|
.rv-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--faint);font-family:var(--mono);font-size:13px;text-align:center;padding:24px}
|
|
236
|
+
|
|
237
|
+
/* ── 모바일 대응 (드로어 트리 + 단일 터미널 + IME 입력바) ── */
|
|
238
|
+
.menu-btn{display:none;font-size:15px;color:var(--muted);background:none;border:1px solid var(--line);border-radius:7px;padding:5px 9px;cursor:pointer;font-family:var(--mono)}
|
|
239
|
+
.menu-btn:hover{color:var(--ink);border-color:var(--line-hi)}
|
|
240
|
+
.scrim{display:none;position:fixed;inset:46px 0 0 0;background:rgba(5,7,10,.5);z-index:39}
|
|
241
|
+
.scrim.on{display:block}
|
|
242
|
+
/* 모바일/터치 터미널 입력바 — 소프트키보드 IME 자모분리 방지: 조합 완료 텍스트를 통째로 PTY 로.
|
|
243
|
+
2행 구성(스크롤되는 키 줄 + 입력 줄)이라 방향키·조합키가 많아도 안 잘린다. */
|
|
244
|
+
.term-ibar{display:none;flex-direction:column;gap:6px;padding:7px 9px;border-top:1px solid var(--line);background:var(--surface2);padding-bottom:calc(7px + env(safe-area-inset-bottom))}
|
|
245
|
+
.tkeys{display:flex;gap:6px;overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:2px}
|
|
246
|
+
.tinput{display:flex;gap:6px;align-items:center}
|
|
247
|
+
.tinput input{flex:1;min-width:0;font-family:var(--mono);font-size:16px;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:9px 10px}
|
|
248
|
+
.tinput input:focus{outline:none;border-color:var(--brand)}
|
|
249
|
+
.tkey{font-family:var(--mono);font-size:13px;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:7px;padding:8px 11px;cursor:pointer;flex:0 0 auto;min-width:42px}
|
|
250
|
+
.tkey:active{color:var(--ink);border-color:var(--brand);background:var(--brand-dim)}
|
|
251
|
+
.tsend{font-family:var(--mono);font-size:13px;font-weight:600;color:var(--brand-ink);background:var(--brand);border:none;border-radius:8px;padding:9px 14px;cursor:pointer;flex:0 0 auto}
|
|
252
|
+
/* 터치 기기(아이패드 포함, 화면폭 무관): 입력바 노출 + 요청바/분할 숨김(하단 클러터·잘림 방지) */
|
|
253
|
+
body.touch .term-ibar{display:flex}
|
|
254
|
+
body.touch .reqbar{display:none}
|
|
255
|
+
body.touch #splitRow, body.touch #splitCol{display:none}
|
|
256
|
+
@media (max-width:860px){
|
|
257
|
+
/* 모바일 = 아이콘만(텍스트 라벨 숨김) + 폰트 최소화 */
|
|
258
|
+
.b-txt{display:none}
|
|
259
|
+
body{font-size:12px}
|
|
260
|
+
header{gap:6px;padding:0 8px;height:44px}
|
|
261
|
+
.layout{height:calc(100dvh - 44px)}
|
|
262
|
+
.scrim{inset:44px 0 0 0}
|
|
263
|
+
.brand img{height:19px} .brand .wm{font-size:15px;margin-left:0}
|
|
264
|
+
.mach{display:none}
|
|
265
|
+
.vtabs{gap:2px} .vtab{padding:6px 8px;gap:0;font-size:12px}
|
|
266
|
+
.toggle{padding:5px 8px;font-size:12px}
|
|
267
|
+
.menu-btn{display:inline-flex;font-size:14px;padding:4px 8px}
|
|
268
|
+
.layout{grid-template-columns:1fr}
|
|
269
|
+
.rail{position:fixed;top:44px;bottom:0;left:0;width:84%;max-width:300px;z-index:40;background:var(--panel);transform:translateX(-100%);transition:transform .18s ease;border-right:1px solid var(--line-hi);box-shadow:2px 0 16px rgba(0,0,0,.4);font-size:11.5px}
|
|
270
|
+
.rail.open{transform:translateX(0)}
|
|
271
|
+
.tabbar{height:34px} .tab{max-width:56vw;font-size:11px;padding:0 8px}
|
|
272
|
+
.tc-btn{font-size:11px;padding:3px 7px}
|
|
273
|
+
.leaf-h{height:24px;font-size:10.5px}
|
|
274
|
+
.term-ibar{display:flex}
|
|
275
|
+
.tkey{font-size:12px;padding:7px 9px;min-width:38px}
|
|
276
|
+
.tinput input{font-size:15px;padding:8px 10px}
|
|
277
|
+
}
|
|
232
278
|
</style>
|
|
233
279
|
</head>
|
|
234
280
|
<body>
|
|
235
281
|
<header>
|
|
282
|
+
<button type="button" class="menu-btn" id="menuBtn" title="워크스페이스 트리">☰</button>
|
|
236
283
|
<a class="brand" href="/"><img src="/brand/mark.png" alt="" /><span class="wm">coxpit</span></a>
|
|
237
284
|
<span class="mach"><span class="dot"></span><span id="mach">local</span></span>
|
|
238
285
|
<div class="vtabs">
|
|
239
|
-
<button type="button" class="vtab on" id="vtTerm"><span class="g">⌗</span>Terminal</button>
|
|
240
|
-
<button type="button" class="vtab" id="vtReview"><span class="g">⧉</span>Review</button>
|
|
241
|
-
<button type="button" class="vtab" disabled title="Docs = 보드"><span class="g">▤</span>Docs</button>
|
|
286
|
+
<button type="button" class="vtab on" id="vtTerm"><span class="g">⌗</span><span class="b-txt">Terminal</span></button>
|
|
287
|
+
<button type="button" class="vtab" id="vtReview"><span class="g">⧉</span><span class="b-txt">Review</span></button>
|
|
288
|
+
<button type="button" class="vtab" disabled title="Docs = 보드"><span class="g">▤</span><span class="b-txt">Docs</span></button>
|
|
242
289
|
</div>
|
|
243
290
|
<div class="right">
|
|
244
|
-
<span class="ws" id="ws"><span class="dot"></span><span id="wstext">connecting</span></span>
|
|
245
|
-
<button type="button" class="toggle" id="secretsBtn" title="시크릿(API 키) 관리 — 세션에 env 로 주입"
|
|
246
|
-
<a class="toggle" href="/" title="보드(모니터) 뷰로"
|
|
291
|
+
<span class="ws" id="ws"><span class="dot"></span><span id="wstext" class="b-txt">connecting</span></span>
|
|
292
|
+
<button type="button" class="toggle" id="secretsBtn" title="시크릿(API 키) 관리 — 세션에 env 로 주입">🔑<span class="b-txt"> Secrets</span></button>
|
|
293
|
+
<a class="toggle" href="/" title="보드(모니터) 뷰로">←<span class="b-txt"> Board</span></a>
|
|
247
294
|
</div>
|
|
248
295
|
</header>
|
|
249
296
|
|
|
297
|
+
<div class="scrim" id="scrim"></div>
|
|
250
298
|
<div class="layout">
|
|
251
|
-
<aside class="rail">
|
|
299
|
+
<aside class="rail" id="rail">
|
|
252
300
|
<div class="lbl"><span>Workspace</span><span id="machName" style="color:var(--faint)">local</span></div>
|
|
253
301
|
<div id="tree"></div>
|
|
254
302
|
<div class="railfoot">클릭한 run·세션은 <b>탭</b>으로 열립니다 · split 으로 페인을 나란히 배치</div>
|
|
@@ -260,8 +308,8 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
260
308
|
<div class="tabctl">
|
|
261
309
|
<button class="tc-btn" id="splitRow" title="세로 분할 — 포커스 페인을 좌우로" disabled>▐ split</button>
|
|
262
310
|
<button class="tc-btn" id="splitCol" title="가로 분할 — 포커스 페인을 상하로" disabled>▬ split</button>
|
|
263
|
-
<button class="tc-btn session" id="sessionBtn" title="자유 세션(폴더 지정 터미널) 열기"
|
|
264
|
-
<button class="tc-btn" id="closeBtn" title="포커스 페인 닫기(탭은 유지)" disabled
|
|
311
|
+
<button class="tc-btn session" id="sessionBtn" title="자유 세션(폴더 지정 터미널) 열기">+<span class="b-txt"> Session</span></button>
|
|
312
|
+
<button class="tc-btn" id="closeBtn" title="포커스 페인 닫기(탭은 유지)" disabled>×<span class="b-txt"> pane</span></button>
|
|
265
313
|
</div>
|
|
266
314
|
</div>
|
|
267
315
|
<div class="panes" id="panes"></div>
|
|
@@ -274,6 +322,25 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
274
322
|
<div class="hint">세션 = <b>지정한 폴더</b>의 tmux 셸(특정 프로젝트에 소속되지 않음). 그 안에서 <code>claude</code> 를 띄워 “이 프로젝트 구현해줘” 처럼 직접 지시할 수 있습니다.</div>
|
|
275
323
|
</div>
|
|
276
324
|
</div>
|
|
325
|
+
<div class="term-ibar" id="termIbar">
|
|
326
|
+
<div class="tkeys">
|
|
327
|
+
<button type="button" class="tkey" data-k="esc" title="Esc">esc</button>
|
|
328
|
+
<button type="button" class="tkey" data-k="tab" title="Tab">tab</button>
|
|
329
|
+
<button type="button" class="tkey" data-k="enter" title="Enter">⏎</button>
|
|
330
|
+
<button type="button" class="tkey" data-k="left" title="←">←</button>
|
|
331
|
+
<button type="button" class="tkey" data-k="up" title="↑">↑</button>
|
|
332
|
+
<button type="button" class="tkey" data-k="down" title="↓">↓</button>
|
|
333
|
+
<button type="button" class="tkey" data-k="right" title="→">→</button>
|
|
334
|
+
<button type="button" class="tkey" data-k="cc" title="Ctrl-C">^C</button>
|
|
335
|
+
<button type="button" class="tkey" data-k="cd" title="Ctrl-D">^D</button>
|
|
336
|
+
<button type="button" class="tkey" data-k="cr" title="Ctrl-R (검색)">^R</button>
|
|
337
|
+
<button type="button" class="tkey" data-k="cu" title="Ctrl-U (줄 지우기)">^U</button>
|
|
338
|
+
</div>
|
|
339
|
+
<div class="tinput">
|
|
340
|
+
<input id="termInput" placeholder="입력 → 한글 OK · Enter 전송" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" />
|
|
341
|
+
<button type="button" class="tsend" id="termSend">전송</button>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
277
344
|
<div class="reqbar">
|
|
278
345
|
<div class="modes">
|
|
279
346
|
<button type="button" class="mode on" data-mode="new" title="새 태스크를 만들고 N개 에이전트로 팬아웃">⌗ New</button>
|
|
@@ -342,8 +409,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
342
409
|
<script src="/vendor/addon-fit.js"></script>
|
|
343
410
|
<script src="/vendor/addon-unicode11.js"></script>
|
|
344
411
|
<script>
|
|
345
|
-
//
|
|
346
|
-
|
|
412
|
+
// 모바일 = 터미널 우선을 유지하되 좁은 화면에 맞춤(드로어 트리 + 단일 터미널 + IME 입력바).
|
|
413
|
+
// (이전엔 보드로 리다이렉트했지만, 이제 cockpit 을 모바일 대응)
|
|
414
|
+
function isMobile(){ return window.matchMedia('(max-width:860px),(pointer:coarse)').matches; }
|
|
347
415
|
var esc = function(s){ return String(s==null?'':s).replace(/[&<>"]/g, function(c){ return ({'&':'&','<':'<','>':'>','"':'"'})[c]; }); };
|
|
348
416
|
var $ = function(id){ return document.getElementById(id); };
|
|
349
417
|
|
|
@@ -453,7 +521,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
453
521
|
$('tree').addEventListener('click', function(e){
|
|
454
522
|
if (e.target.closest('[data-newsession]')){ openSession(); return; }
|
|
455
523
|
var run = e.target.closest('[data-run]');
|
|
456
|
-
if (run){ openRunPane(+run.dataset.run); return; }
|
|
524
|
+
if (run){ openRunPane(+run.dataset.run); if (isMobile()) setDrawer(false); return; }
|
|
457
525
|
var fn = e.target.closest('[data-fold]');
|
|
458
526
|
if (fn){ var k = fn.dataset.fold; fold[k] = !isFold(k); renderTree(); }
|
|
459
527
|
});
|
|
@@ -498,7 +566,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
498
566
|
var host=document.createElement('div'); host.className='term-host';
|
|
499
567
|
var term=new window.Terminal({
|
|
500
568
|
fontFamily: "ui-monospace, 'SF Mono', Menlo, Monaco, 'Apple SD Gothic Neo', 'Noto Sans KR', monospace",
|
|
501
|
-
fontSize: 12, cursorBlink: true, allowProposedApi: true, scrollback: 4000,
|
|
569
|
+
fontSize: isMobile() ? 11 : 12, cursorBlink: true, allowProposedApi: true, scrollback: 4000,
|
|
502
570
|
theme: { background:'#0b0d12', foreground:'#dee4ec', cursor:'#4ec9b0', selectionBackground:'rgba(78,201,176,.25)', black:'#1c212c', brightBlack:'#5c6675' },
|
|
503
571
|
});
|
|
504
572
|
var fit=new window.FitAddon.FitAddon(); term.loadAddon(fit);
|
|
@@ -614,6 +682,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
614
682
|
|
|
615
683
|
function splitFocused(dir){
|
|
616
684
|
if (!tabOrder.length) return;
|
|
685
|
+
if (isMobile()){ toast('창분할은 데스크톱 전용 — 모바일은 탭으로 전환하세요'); return; }
|
|
617
686
|
if (countLeaves()>=MAX_LEAVES){ toast('페인 최대 '+MAX_LEAVES+'개'); return; }
|
|
618
687
|
var l=findLeaf(focusLeaf) || firstLeaf(); if(!l) return;
|
|
619
688
|
var keep=l.tab; var aId=l.id, bId=newLeafId();
|
|
@@ -740,6 +809,23 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
740
809
|
$('splitCol').addEventListener('click', function(){ splitFocused('col'); });
|
|
741
810
|
$('closeBtn').addEventListener('click', function(){ if(focusLeaf) closeSlot(focusLeaf); });
|
|
742
811
|
|
|
812
|
+
// ── 모바일: 워크스페이스 드로어 ──
|
|
813
|
+
function setDrawer(open){ $('rail').classList.toggle('open', open); $('scrim').classList.toggle('on', open); }
|
|
814
|
+
$('menuBtn').addEventListener('click', function(){ setDrawer(!$('rail').classList.contains('open')); });
|
|
815
|
+
$('scrim').addEventListener('click', function(){ setDrawer(false); });
|
|
816
|
+
|
|
817
|
+
// ── 모바일 터미널 입력바 — 조합 완료 텍스트를 통째로 포커스 탭의 PTY 로(IME 안전) ──
|
|
818
|
+
function focusedWs(){ var rid=focusedRunId(); return (rid!=null && tabs[rid]) ? tabs[rid].ws : null; }
|
|
819
|
+
function termSendRaw(d){ var ws=focusedWs(); if(ws && ws.readyState===1) ws.send(JSON.stringify({t:'i',d:d})); }
|
|
820
|
+
function termSendLine(){ var inp=$('termInput'); var v=inp.value; if(!v){ termSendRaw('\\r'); return; } termSendRaw(v+'\\r'); inp.value=''; inp.focus(); }
|
|
821
|
+
$('termSend').addEventListener('click', termSendLine);
|
|
822
|
+
$('termInput').addEventListener('keydown', function(e){ if(e.isComposing) return; if(e.key==='Enter'){ e.preventDefault(); termSendLine(); } });
|
|
823
|
+
var TKEYS={ esc:'\\x1b', tab:'\\t', enter:'\\r', cc:'\\x03', cd:'\\x04', cr:'\\x12', cu:'\\x15',
|
|
824
|
+
up:'\\x1b[A', down:'\\x1b[B', right:'\\x1b[C', left:'\\x1b[D' };
|
|
825
|
+
Array.prototype.forEach.call(document.querySelectorAll('#termIbar .tkey'), function(b){ b.addEventListener('click', function(){ var k=TKEYS[b.dataset.k]; if(k){ termSendRaw(k); var inp=$('termInput'); if(inp) inp.focus(); } }); });
|
|
826
|
+
// 터치 기기(아이패드 포함) 판별 → body.touch (화면폭 무관하게 입력바 노출)
|
|
827
|
+
if (window.matchMedia('(pointer:coarse)').matches || (navigator.maxTouchPoints||0) > 0) document.body.classList.add('touch');
|
|
828
|
+
|
|
743
829
|
// ── 자유 세션 — 폴더를 지정해 tmux 셸(프로젝트 비소속). ──
|
|
744
830
|
var pickPathCur = '';
|
|
745
831
|
function machineSlug(){ return (fleet.machines && fleet.machines[0] && fleet.machines[0].slug) || 'local'; }
|