coxpit 5.23.1 → 5.25.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/package.json +2 -1
- package/src/cockpit.ts +195 -8
- package/src/server.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.25.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": "SEE LICENSE IN LICENSE.md",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"@fastify/websocket": "^11",
|
|
46
46
|
"@libsql/client": "^0.14",
|
|
47
47
|
"@xterm/addon-fit": "^0.10.0",
|
|
48
|
+
"@xterm/addon-search": "^0.15.0",
|
|
48
49
|
"@xterm/addon-unicode11": "^0.8.0",
|
|
49
50
|
"@xterm/addon-web-links": "^0.11.0",
|
|
50
51
|
"@xterm/xterm": "^5.5.0",
|
package/src/cockpit.ts
CHANGED
|
@@ -81,6 +81,13 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
81
81
|
|
|
82
82
|
/* ── 탭 바 + 분할 트리 페인 ── */
|
|
83
83
|
.stage{display:flex;flex-direction:column;min-width:0;background:var(--bg);position:relative}
|
|
84
|
+
/* ── 터미널 스크롤백 검색바(⌘F) ── */
|
|
85
|
+
.term-find{position:absolute;top:44px;right:14px;z-index:30;display:flex;align-items:center;gap:4px;background:var(--surface);border:1px solid var(--line-hi);border-radius:9px;padding:5px 6px;box-shadow:0 6px 20px rgba(0,0,0,.4)}
|
|
86
|
+
.term-find input{font-family:var(--mono);font-size:12px;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:5px 8px;width:200px;outline:none}
|
|
87
|
+
.term-find input:focus{border-color:var(--brand)}
|
|
88
|
+
.term-find .fc{font-family:var(--mono);font-size:10.5px;color:var(--faint);min-width:20px;text-align:center}
|
|
89
|
+
.term-find button{font-family:var(--mono);font-size:13px;color:var(--muted);background:none;border:none;cursor:pointer;padding:2px 5px;border-radius:5px}
|
|
90
|
+
.term-find button:hover{color:var(--ink);background:var(--surface2)}
|
|
84
91
|
.tabbar{display:flex;align-items:stretch;height:38px;border-bottom:1px solid var(--line);background:var(--panel);font-family:var(--mono)}
|
|
85
92
|
.tabs{flex:1;display:flex;align-items:stretch;overflow-x:auto;overflow-y:hidden}
|
|
86
93
|
.tab{display:inline-flex;align-items:center;gap:7px;padding:0 10px 0 12px;border-right:1px solid var(--line);font-size:12px;color:var(--muted);cursor:pointer;white-space:nowrap;max-width:220px;user-select:none;flex:none}
|
|
@@ -185,6 +192,19 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
185
192
|
/* ── folder picker (자유 세션 폴더 지정) ── */
|
|
186
193
|
.modal{position:fixed;inset:0;background:rgba(4,6,10,.6);display:none;align-items:center;justify-content:center;z-index:60}
|
|
187
194
|
.modal.on{display:flex}
|
|
195
|
+
/* ── ⌘K 커맨드 팔레트 ── */
|
|
196
|
+
#palette{align-items:flex-start}
|
|
197
|
+
.pal{width:min(620px,94vw);margin-top:12vh;display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--line-hi);border-radius:14px;overflow:hidden;box-shadow:0 24px 70px rgba(0,0,0,.5)}
|
|
198
|
+
.pal>input{font-family:var(--mono);font-size:14px;color:var(--ink);background:transparent;border:none;border-bottom:1px solid var(--line);padding:15px 18px;outline:none}
|
|
199
|
+
.pal-list{max-height:52vh;overflow:auto;padding:6px;scrollbar-width:none}
|
|
200
|
+
.pal-list::-webkit-scrollbar{width:0;display:none}
|
|
201
|
+
.pal-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:8px;cursor:pointer;font-family:var(--mono);font-size:12.5px;color:var(--muted)}
|
|
202
|
+
.pal-row.sel{background:var(--brand-dim);color:var(--ink)}
|
|
203
|
+
.pal-row .pk{font-size:9px;letter-spacing:.04em;color:var(--faint);border:1px solid var(--line);border-radius:999px;padding:1px 7px;flex:none;text-transform:uppercase}
|
|
204
|
+
.pal-row.sel .pk{border-color:rgba(78,201,176,.4);color:var(--brand)}
|
|
205
|
+
.pal-row .pl{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
206
|
+
.pal-row .ph{font-size:10.5px;color:var(--faint);flex:none;max-width:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
207
|
+
.pal-empty{padding:18px;text-align:center;color:var(--faint);font-family:var(--mono);font-size:12px}
|
|
188
208
|
.pick{width:min(560px,92vw);max-height:76vh;display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--line-hi);border-radius:14px;overflow:hidden;font-family:var(--mono)}
|
|
189
209
|
.pick-h{display:flex;align-items:center;gap:10px;padding:13px 15px;border-bottom:1px solid var(--line)}
|
|
190
210
|
.pick-h .t{font-size:13px;color:var(--ink);font-weight:600}
|
|
@@ -215,6 +235,11 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
215
235
|
.sec-row .shint{color:var(--faint);font-size:11px;font-family:var(--mono)}
|
|
216
236
|
.sec-row .sdel{color:var(--faint);border:none;background:none;cursor:pointer;font-size:14px;padding:0 4px}
|
|
217
237
|
.sec-row .sdel:hover{color:var(--failed)}
|
|
238
|
+
.leaf-h .pane-act{font-family:var(--mono);font-size:10px;color:var(--brand);opacity:.9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:110px}
|
|
239
|
+
.leaf-h .pane-act:empty{display:none}
|
|
240
|
+
.tnode.run .ract{margin-left:6px;font-size:10px;color:var(--brand);opacity:.85}
|
|
241
|
+
.leaf-h .zoom{color:var(--faint);border:none;background:none;cursor:pointer;font-size:12px;padding:0 2px}
|
|
242
|
+
.leaf-h .zoom:hover{color:var(--brand)}
|
|
218
243
|
.leaf-h .lock{color:var(--faint);border:none;background:none;cursor:pointer;font-size:12px;padding:0 2px}
|
|
219
244
|
.leaf-h .lock:hover{color:var(--brand)}
|
|
220
245
|
.leaf-h .sendkey{font:inherit;font-family:var(--mono);font-size:11px;color:var(--ink);background:var(--panel);border:1px solid var(--brand);border-radius:5px;padding:1px 6px;width:150px;outline:none}
|
|
@@ -259,6 +284,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
259
284
|
.rv-diff > span{display:block;min-height:1.2em}
|
|
260
285
|
.dl-file{color:var(--brand)} .dl-hunk{color:var(--open)} .dl-ctx{color:var(--muted)}
|
|
261
286
|
.dl-add{color:#7fdca0;background:rgba(88,179,104,.08)} .dl-del{color:#e58a92;background:rgba(226,91,103,.08)}
|
|
287
|
+
.rv-diff .ws{color:var(--faint);opacity:.55}
|
|
288
|
+
.rv-ws{font-family:var(--mono);font-size:11px;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:3px 8px;cursor:pointer}
|
|
289
|
+
.rv-ws:hover{color:var(--ink)} .rv-ws.on{color:var(--brand-ink);background:var(--brand);border-color:var(--brand)}
|
|
262
290
|
.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}
|
|
263
291
|
|
|
264
292
|
/* ── 모바일 대응 (드로어 트리 + 단일 터미널 + IME 입력바) ── */
|
|
@@ -358,6 +386,13 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
358
386
|
</div>
|
|
359
387
|
</div>
|
|
360
388
|
<div class="panes" id="panes"></div>
|
|
389
|
+
<div class="term-find" id="termFind" hidden>
|
|
390
|
+
<input id="findInput" placeholder="터미널에서 찾기 (⌘F)" autocomplete="off" spellcheck="false" />
|
|
391
|
+
<span class="fc" id="findCount"></span>
|
|
392
|
+
<button type="button" id="findPrev" title="이전 (⇧⏎)">↑</button>
|
|
393
|
+
<button type="button" id="findNext" title="다음 (⏎)">↓</button>
|
|
394
|
+
<button type="button" id="findClose" title="닫기 (esc)">×</button>
|
|
395
|
+
</div>
|
|
361
396
|
<div class="empty" id="empty">
|
|
362
397
|
<div class="card">
|
|
363
398
|
<div class="glyph">⌗ ⌗ ⌗</div>
|
|
@@ -421,6 +456,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
421
456
|
<input id="rvVcmd" placeholder="예: npm test — 정착 시 자동 실행" autocomplete="off" />
|
|
422
457
|
<button type="button" id="rvVsave">save</button>
|
|
423
458
|
</span>
|
|
459
|
+
<button type="button" id="rvWs" class="rv-ws" title="공백 표시(스페이스·탭)">␣ 공백</button>
|
|
424
460
|
<span id="rvHint" style="margin-left:auto;color:var(--faint)">정착하면 자동 검증 · 승자를 base 에 merge</span>
|
|
425
461
|
</div>
|
|
426
462
|
<div class="rv-cols" id="rvCols"></div>
|
|
@@ -479,12 +515,20 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
479
515
|
</div>
|
|
480
516
|
</div>
|
|
481
517
|
|
|
518
|
+
<div class="modal" id="palette">
|
|
519
|
+
<div class="pal">
|
|
520
|
+
<input id="palInput" placeholder="이동·명령 검색 (⌘K) · 세션·run·프로젝트·명령" autocomplete="off" spellcheck="false" />
|
|
521
|
+
<div class="pal-list" id="palList"></div>
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
482
525
|
<div class="toast" id="toast"></div>
|
|
483
526
|
|
|
484
527
|
<script src="/vendor/xterm.js"></script>
|
|
485
528
|
<script src="/vendor/addon-fit.js"></script>
|
|
486
529
|
<script src="/vendor/addon-unicode11.js"></script>
|
|
487
530
|
<script src="/vendor/addon-web-links.js"></script>
|
|
531
|
+
<script src="/vendor/addon-search.js"></script>
|
|
488
532
|
<script src="/vendor/marked.js"></script>
|
|
489
533
|
<script>
|
|
490
534
|
// 모바일 = 터미널 우선을 유지하되 좁은 화면에 맞춤(드로어 트리 + 단일 터미널 + IME 입력바).
|
|
@@ -496,6 +540,23 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
496
540
|
function toast(msg){ var t=$('toast'); t.textContent=msg; t.classList.add('show'); clearTimeout(toast._h); toast._h=setTimeout(function(){ t.classList.remove('show'); }, 2600); }
|
|
497
541
|
var V_GLYPH = { pass:'✓ verify', fail:'✗ verify', running:'⋯ verify', error:'! verify' };
|
|
498
542
|
function vbadge(status){ if (!status || !V_GLYPH[status]) return ''; return '<span class="vbadge '+status+'" data-role="vbadge">'+V_GLYPH[status]+'</span>'; }
|
|
543
|
+
// 라이브 상태 — run 의 최신 이벤트에서 "지금 뭐 하는지"(도구명/사고)를 뽑는다. 실행 중일 때만.
|
|
544
|
+
function latestActivity(runId){
|
|
545
|
+
var r=runById[runId]; if(!r) return '';
|
|
546
|
+
if(r.status!=='running' && r.status!=='pending') return '';
|
|
547
|
+
var evs=r.events||[];
|
|
548
|
+
for(var i=evs.length-1;i>=0;i--){
|
|
549
|
+
var e=evs[i], k=e.kind;
|
|
550
|
+
if(k==='steer') return 'steer'; if(k==='ask') return 'asking';
|
|
551
|
+
if(k!=='assistant') continue;
|
|
552
|
+
try{ var o=JSON.parse(e.payload);
|
|
553
|
+
var c=o&&o.message&&o.message.content;
|
|
554
|
+
if(c&&c.length){ for(var j=c.length-1;j>=0;j--){ if(c[j].type==='tool_use') return c[j].name||'tool'; if(c[j].type==='text'&&(c[j].text||'').trim()) return 'thinking'; } }
|
|
555
|
+
else if(o&&o.text) return 'thinking';
|
|
556
|
+
}catch(_){}
|
|
557
|
+
}
|
|
558
|
+
return r.status==='pending' ? 'starting' : 'working';
|
|
559
|
+
}
|
|
499
560
|
|
|
500
561
|
// ── fleet 상태 ──
|
|
501
562
|
var fleet = { machines:[], repos:[], tasks:[], groups:[], runs:[], providers:[] };
|
|
@@ -595,8 +656,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
595
656
|
+ '<span class="n">'+esc(t.title)+'</span></div>';
|
|
596
657
|
if (!isFold(tk)) rns.sort(function(a,b){return a.id-b.id;}).forEach(function(r){
|
|
597
658
|
var open = tabs[r.id] ? ' open' : '';
|
|
659
|
+
var act=latestActivity(r.id);
|
|
598
660
|
s += '<div class="tnode run'+open+'" data-run="'+r.id+'"><span class="st '+esc(r.status)+'"></span>'
|
|
599
|
-
+ '<span class="n">r'+r.id+' · '+esc(r.status)+'</span
|
|
661
|
+
+ '<span class="n">r'+r.id+' · '+esc(r.status)+'</span>'+(act?'<span class="ract">'+esc(act)+'</span>':'')+'</div>';
|
|
600
662
|
});
|
|
601
663
|
return s;
|
|
602
664
|
}
|
|
@@ -623,6 +685,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
623
685
|
var tabOrder = []; // 탭 바 순서(runId)
|
|
624
686
|
var layout = { leaf:true, id:'L0', tab:null }; // 분할 트리 루트
|
|
625
687
|
var focusLeaf = 'L0';
|
|
688
|
+
var zoomLeaf = null; // 페인 최대화(줌) — 설정되면 그 리프만 전체 렌더
|
|
626
689
|
var leafSeq = 1;
|
|
627
690
|
var viewerSeq = 1; // 뷰어 탭 키(문자열 'v#')
|
|
628
691
|
var MAX_LEAVES = 6;
|
|
@@ -682,6 +745,10 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
682
745
|
}
|
|
683
746
|
cb(links.length?links:undefined);
|
|
684
747
|
}}); }catch(e){}
|
|
748
|
+
// 스크롤백 검색(⌘F) — SearchAddon. t.search 에 보관, 검색바가 findNext/Previous 호출.
|
|
749
|
+
var search=null; try{ search=new window.SearchAddon.SearchAddon(); term.loadAddon(search);
|
|
750
|
+
search.onDidChangeResults(function(e){ if($('termFind') && !$('termFind').hidden && focusedRunId()===runId){ var c=$('findCount'); if(c){ var n=(e&&e.resultCount)||0; c.textContent = n ? (((e.resultIndex>=0?e.resultIndex+1:0))+'/'+n) : '0'; } } });
|
|
751
|
+
}catch(e){}
|
|
685
752
|
// 복사 배선: xterm 은 user-select:none 이라 네이티브 선택이 없다 → term.getSelection() 을 직접 클립보드로.
|
|
686
753
|
// ① 드래그 놓으면 자동 복사(select-to-copy) ② Cmd/Ctrl+C 로도 복사(선택 없으면 통과 → SIGINT).
|
|
687
754
|
var copySel=function(){ var s=''; try{ s=term.getSelection(); }catch(e){} if(!s) return false;
|
|
@@ -691,13 +758,15 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
691
758
|
host.addEventListener('mouseup', function(){ copySel(); });
|
|
692
759
|
host.addEventListener('touchend', function(){ copySel(); });
|
|
693
760
|
term.attachCustomKeyEventHandler(function(ev){
|
|
694
|
-
if(ev.type==='keydown' && (ev.metaKey||ev.ctrlKey)
|
|
695
|
-
if(term.hasSelection()){ copySel(); return false; }
|
|
761
|
+
if(ev.type==='keydown' && (ev.metaKey||ev.ctrlKey)){
|
|
762
|
+
if(ev.key==='c'||ev.key==='C'){ if(term.hasSelection()){ copySel(); return false; } }
|
|
763
|
+
// 전역 단축키(⌘K·⌘F·⌘⏎·⌘1~9)는 터미널로 보내지 않는다 — 문서 핸들러가 처리.
|
|
764
|
+
if(ev.key==='k'||ev.key==='K'||ev.key==='f'||ev.key==='F'||ev.key==='Enter'||(ev.key>='1'&&ev.key<='9')) return false;
|
|
696
765
|
}
|
|
697
766
|
return true;
|
|
698
767
|
});
|
|
699
768
|
// term.open 은 host 가 DOM 에 붙은 뒤(attachHosts) 최초 1회 — detached 에서 open 하면 렌더러가 안 뜬다.
|
|
700
|
-
var t={ runId:runId, name:tabName(runId), term:term, fit:fit, ws:null, retry:0, closing:false, host:host, ro:null, opened:false, connected:false };
|
|
769
|
+
var t={ runId:runId, name:tabName(runId), term:term, fit:fit, ws:null, retry:0, closing:false, host:host, ro:null, opened:false, connected:false, search:search };
|
|
701
770
|
term.onData(function(d){ if(t.ws&&t.ws.readyState===1) t.ws.send(JSON.stringify({t:'i',d:d})); });
|
|
702
771
|
tabs[runId]=t; tabOrder.push(runId);
|
|
703
772
|
return t; // open·connect 는 attachHosts 에서(Phase 2 순서: open → connect)
|
|
@@ -759,7 +828,10 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
759
828
|
var fp=document.createElement('iframe'); fp.src=rawUrl; // 브라우저/Electron 내장 PDF 뷰어(수동적 — sandbox 불필요)
|
|
760
829
|
body.innerHTML=''; body.appendChild(fp);
|
|
761
830
|
} else if(d.kind==='image'){
|
|
762
|
-
body.innerHTML=''; var im=document.createElement('img'); im.
|
|
831
|
+
body.innerHTML=''; var im=document.createElement('img'); im.alt=t.name;
|
|
832
|
+
// 브라우저가 못 그리는 형식(HEIC/TIFF 등)이나 로드 실패 시 조용히 빈 화면 대신 이유+원본 링크
|
|
833
|
+
im.onerror=function(){ body.innerHTML='<div class="view-msg">이 이미지를 표시할 수 없습니다 · '+fmtSize(d.size||0)+'<br><span style="color:var(--faint)">브라우저가 못 그리는 형식일 수 있어요 (예: HEIC·TIFF)</span><br><br><a href="'+rawUrl+'" target="_blank">원본 열기 / 내려받기</a></div>'; };
|
|
834
|
+
im.src=rawUrl; body.appendChild(im);
|
|
763
835
|
} else if(d.kind==='binary'){
|
|
764
836
|
body.innerHTML='<div class="view-msg">미리보기 불가 · '+fmtSize(d.size||0)+(d.note?(' · '+esc(d.note)):'')+'<br><br><a href="'+rawUrl+'" target="_blank">원본 열기 / 내려받기</a></div>';
|
|
765
837
|
} else { // text
|
|
@@ -849,15 +921,17 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
849
921
|
var leaf=document.createElement('div'); leaf.className='leaf'+(node.id===focusLeaf?' focus':''); leaf.dataset.leaf=node.id;
|
|
850
922
|
var t=node.tab!=null?tabs[node.tab]:null; var r=node.tab!=null?runById[node.tab]:null;
|
|
851
923
|
var head=document.createElement('div'); head.className='leaf-h'; head.dataset.leafhead=node.id;
|
|
924
|
+
var zoomBtn = '<button class="zoom" data-zoom="'+node.id+'" title="이 페인 최대화 (⌘⏎)">'+(zoomLeaf?'⤡':'⤢')+'</button>';
|
|
852
925
|
head.innerHTML = t
|
|
853
926
|
? (t.kind==='viewer'
|
|
854
927
|
? '<span class="st vdoc">▤</span><span class="nm">'+esc(t.name)+'</span>'
|
|
855
|
-
+ '<button class="x" title="이 페인 닫기(뷰어 유지)">×</button>'
|
|
928
|
+
+ zoomBtn + '<button class="x" title="이 페인 닫기(뷰어 유지)">×</button>'
|
|
856
929
|
: '<span class="st '+esc(r?r.status:'')+'"></span><span class="nm">'+esc(t.name)+'</span>'
|
|
930
|
+
+ '<span data-role="act" class="pane-act">'+esc(latestActivity(node.tab))+'</span>'
|
|
857
931
|
+ '<span data-role="chip" class="chip '+esc(r?r.status:'')+'">'+esc(r?r.status:'')+'</span>'
|
|
858
932
|
+ '<span data-role="vslot">'+vbadge(r&&r.verifyStatus)+'</span>'
|
|
859
933
|
+ '<button class="lock" data-lock="'+node.id+'" title="이 페인에 시크릿/비밀번호 전송(터미널에 안 찍힘)">⊟</button>'
|
|
860
|
-
+ '<button class="x" title="이 페인 닫기(탭은 유지)">×</button>')
|
|
934
|
+
+ zoomBtn + '<button class="x" title="이 페인 닫기(탭은 유지)">×</button>')
|
|
861
935
|
: '<span class="nm" style="color:var(--faint)">빈 페인</span><button class="x" title="이 페인 닫기">×</button>';
|
|
862
936
|
var body=document.createElement('div'); body.className='leaf-body'; body.dataset.leafbody=node.id;
|
|
863
937
|
if (!t){ var em=document.createElement('div'); em.className='leaf-empty'; em.textContent='탭을 여기로 드래그하거나 탭을 클릭하세요'; body.appendChild(em); }
|
|
@@ -887,7 +961,11 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
887
961
|
$('panes').style.display = n?'flex':'none';
|
|
888
962
|
renderTabs();
|
|
889
963
|
var host=$('panes'); host.innerHTML='';
|
|
890
|
-
if (n){
|
|
964
|
+
if (n){
|
|
965
|
+
var zl = zoomLeaf ? findLeaf(zoomLeaf) : null; // 줌: 그 리프만 전체 렌더(레이아웃은 메모리에 보존)
|
|
966
|
+
var root = zl ? { leaf:true, id:zl.id, tab:zl.tab } : layout;
|
|
967
|
+
host.appendChild(buildNode(root)); attachHosts();
|
|
968
|
+
}
|
|
891
969
|
updateControls();
|
|
892
970
|
if (typeof reqMode!=='undefined') setMode(reqMode);
|
|
893
971
|
requestAnimationFrame(fitAllVisible);
|
|
@@ -900,9 +978,19 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
900
978
|
updateControls();
|
|
901
979
|
if (typeof reqMode!=='undefined' && reqMode!=='new') setMode(reqMode);
|
|
902
980
|
}
|
|
981
|
+
// 페인 최대화(줌) 토글 — 포커스 페인(또는 지정 leaf)만 전체로. 다시 누르면 복원.
|
|
982
|
+
function toggleZoom(id){
|
|
983
|
+
var target = id || focusLeaf;
|
|
984
|
+
if(zoomLeaf===target){ zoomLeaf=null; }
|
|
985
|
+
else { var l=findLeaf(target); if(!l || l.tab==null) return; zoomLeaf=target; focusLeaf=target; }
|
|
986
|
+
render();
|
|
987
|
+
}
|
|
988
|
+
// 페인 순서(트리 순회) → ⌘1..9 로 N번째 포커스
|
|
989
|
+
function leafOrder(){ var out=[]; eachLeaf(layout,function(l){ out.push(l); }); return out; }
|
|
903
990
|
|
|
904
991
|
// 탭 열기 = 포커스 슬롯에 표시(강제 분할 없음). 기존 호출부(openRunPane) 호환.
|
|
905
992
|
function openTab(runId){
|
|
993
|
+
zoomLeaf=null; // 새 탭은 보여야 하므로 줌 해제
|
|
906
994
|
if(!tabs[runId]) ensureTab(runId); // 뷰어 탭은 ensureViewer 로 이미 생성됨 → 터미널로 오생성 방지
|
|
907
995
|
var l=leafOfTab(runId);
|
|
908
996
|
if (l){ setLeafFocus(l.id); return; }
|
|
@@ -915,6 +1003,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
915
1003
|
|
|
916
1004
|
function splitFocused(dir){
|
|
917
1005
|
if (!tabOrder.length) return;
|
|
1006
|
+
zoomLeaf=null;
|
|
918
1007
|
if (isMobile()){ toast('창분할은 데스크톱 전용 — 모바일은 탭으로 전환하세요'); return; }
|
|
919
1008
|
if (countLeaves()>=MAX_LEAVES){ toast('페인 최대 '+MAX_LEAVES+'개'); return; }
|
|
920
1009
|
var l=findLeaf(focusLeaf) || firstLeaf(); if(!l) return;
|
|
@@ -924,6 +1013,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
924
1013
|
focusLeaf=bId; render(); renderTree();
|
|
925
1014
|
}
|
|
926
1015
|
function closeSlot(id){
|
|
1016
|
+
zoomLeaf=null;
|
|
927
1017
|
if (countLeaves()<=1){ var only=findLeaf(id)||firstLeaf(); if(only) only.tab=null; if(only) focusLeaf=only.id; render(); renderTree(); return; }
|
|
928
1018
|
(function walk(node){
|
|
929
1019
|
if(node.leaf) return false;
|
|
@@ -935,6 +1025,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
935
1025
|
render(); renderTree();
|
|
936
1026
|
}
|
|
937
1027
|
function closeTab(runId){
|
|
1028
|
+
zoomLeaf=null;
|
|
938
1029
|
var t=tabs[runId]; if(!t) return; t.closing=true;
|
|
939
1030
|
try{ if(t.ws) t.ws.close(); }catch(e){}
|
|
940
1031
|
try{ if(t.ro) t.ro.disconnect(); }catch(e){}
|
|
@@ -953,6 +1044,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
953
1044
|
}
|
|
954
1045
|
function tileTabs(ids){
|
|
955
1046
|
ids=(ids||[]).filter(function(x){return x!=null;}); if(!ids.length) return;
|
|
1047
|
+
zoomLeaf=null;
|
|
956
1048
|
ids.forEach(ensureTab);
|
|
957
1049
|
layout=buildTiled(ids,'row'); var f=firstLeaf(); focusLeaf=f?f.id:'L0';
|
|
958
1050
|
render(); renderTree();
|
|
@@ -970,6 +1062,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
970
1062
|
var st=head.querySelector('.st'); if(st) st.className='st '+r.status;
|
|
971
1063
|
var chip=head.querySelector('[data-role=chip]'); if(chip){ chip.className='chip '+r.status; chip.textContent=r.status; }
|
|
972
1064
|
var vslot=head.querySelector('[data-role=vslot]'); if(vslot) vslot.innerHTML=vbadge(r.verifyStatus);
|
|
1065
|
+
var act=head.querySelector('[data-role=act]'); if(act) act.textContent=latestActivity(l.tab);
|
|
973
1066
|
});
|
|
974
1067
|
}
|
|
975
1068
|
|
|
@@ -1010,6 +1103,8 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1010
1103
|
|
|
1011
1104
|
// ── 페인(슬롯) 이벤트: 포커스·닫기·드롭·리사이즈 ──
|
|
1012
1105
|
$('panes').addEventListener('click', function(e){
|
|
1106
|
+
var zoomBtn=e.target.closest('[data-zoom]');
|
|
1107
|
+
if (zoomBtn){ e.stopPropagation(); toggleZoom(zoomBtn.getAttribute('data-zoom')); return; }
|
|
1013
1108
|
var lockBtn=e.target.closest('[data-lock]');
|
|
1014
1109
|
if (lockBtn){ e.stopPropagation(); startSecretSend(lockBtn.getAttribute('data-lock'), lockBtn); return; }
|
|
1015
1110
|
var leaf=e.target.closest('[data-leaf]'); if(!leaf) return;
|
|
@@ -1047,6 +1142,94 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1047
1142
|
$('menuBtn').addEventListener('click', function(){ setDrawer(!$('rail').classList.contains('open')); });
|
|
1048
1143
|
$('scrim').addEventListener('click', function(){ setDrawer(false); });
|
|
1049
1144
|
|
|
1145
|
+
// ── 터미널 스크롤백 검색(⌘F) ──
|
|
1146
|
+
function focusedTermTab(){ var rid=focusedRunId(); var t=(rid!=null)?tabs[rid]:null; return (t && t.term && t.search) ? t : null; }
|
|
1147
|
+
var FIND_DECO={ matchBackground:'#3a4a2e', activeMatchBackground:'#4ec9b0', activeMatchColorOverviewRuler:'#4ec9b0', matchOverviewRuler:'#33415580' };
|
|
1148
|
+
function runFind(dir){ var t=focusedTermTab(); var q=$('findInput').value; if(!t){ return; } if(!q){ try{ t.search.clearDecorations(); }catch(e){} var c=$('findCount'); if(c) c.textContent=''; return; }
|
|
1149
|
+
try{ if(dir<0) t.search.findPrevious(q,{decorations:FIND_DECO}); else t.search.findNext(q,{decorations:FIND_DECO}); }catch(e){} }
|
|
1150
|
+
function openFind(){ var t=focusedTermTab(); if(!t){ toast('검색할 터미널 페인을 먼저 선택하세요'); return; } $('termFind').hidden=false; var i=$('findInput'); i.focus(); i.select(); if(i.value) runFind(1); }
|
|
1151
|
+
function closeFind(){ $('termFind').hidden=true; var t=focusedTermTab(); if(t){ try{ t.search.clearDecorations(); t.term.focus(); }catch(e){} } }
|
|
1152
|
+
$('findInput').addEventListener('input', function(){ runFind(1); });
|
|
1153
|
+
$('findInput').addEventListener('keydown', function(e){ if(e.key==='Enter'){ e.preventDefault(); runFind(e.shiftKey?-1:1); } else if(e.key==='Escape'){ e.preventDefault(); closeFind(); } });
|
|
1154
|
+
$('findNext').addEventListener('click', function(){ runFind(1); });
|
|
1155
|
+
$('findPrev').addEventListener('click', function(){ runFind(-1); });
|
|
1156
|
+
$('findClose').addEventListener('click', closeFind);
|
|
1157
|
+
document.addEventListener('keydown', function(e){
|
|
1158
|
+
if((e.metaKey||e.ctrlKey) && (e.key==='f'||e.key==='F') && !e.shiftKey && !e.altKey){
|
|
1159
|
+
if(focusedTermTab()){ e.preventDefault(); openFind(); }
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
// ── ⌘K 커맨드 팔레트 — 이동(세션·run·프로젝트) + 명령 ──
|
|
1164
|
+
var palItems=[], palSel=0;
|
|
1165
|
+
function paletteItems(){
|
|
1166
|
+
var items=[
|
|
1167
|
+
{k:'cmd', label:'새 작업 세션 열기', run:openSession},
|
|
1168
|
+
{k:'cmd', label:'파일 열기 (뷰어)', run:openFilePicker},
|
|
1169
|
+
{k:'cmd', label:'터미널 검색 (⌘F)', run:openFind},
|
|
1170
|
+
{k:'cmd', label:'세로 분할', run:function(){ splitFocused('row'); }},
|
|
1171
|
+
{k:'cmd', label:'가로 분할', run:function(){ splitFocused('col'); }},
|
|
1172
|
+
{k:'cmd', label:'포커스 페인 닫기', run:function(){ if(focusLeaf) closeSlot(focusLeaf); }},
|
|
1173
|
+
{k:'cmd', label:'뷰어 / 히스토리', run:openHistory},
|
|
1174
|
+
{k:'cmd', label:'Review 열기 (비교·머지)', run:showReview},
|
|
1175
|
+
{k:'cmd', label:'시크릿 (env 주입)', run:openSecrets},
|
|
1176
|
+
{k:'cmd', label:'보드로 전환', run:function(){ location.href='/'; }}
|
|
1177
|
+
];
|
|
1178
|
+
// 세션 → 페인으로 열기
|
|
1179
|
+
var runs=[]; Object.keys(runById).forEach(function(id){ runs.push(runById[id]); });
|
|
1180
|
+
runs.sort(function(a,b){ return b.id-a.id; });
|
|
1181
|
+
runs.forEach(function(r){
|
|
1182
|
+
var task=taskById[r.taskId]; var repo=task&&repoById[task.repoId];
|
|
1183
|
+
if(repo && repo.kind==='sessions'){ items.push({k:'session', label:(task&&task.title)||('session r'+r.id), hint:'r'+r.id, run:function(){ openRunPane(r.id); }}); }
|
|
1184
|
+
});
|
|
1185
|
+
// run → 페인으로 열기 (최근 40개)
|
|
1186
|
+
var n=0; runs.forEach(function(r){
|
|
1187
|
+
var task=taskById[r.taskId]; var repo=task&&repoById[task.repoId];
|
|
1188
|
+
if(repo && repo.kind==='sessions') return; if(n>=40) return; n++;
|
|
1189
|
+
items.push({k:'run', label:'r'+r.id+' · '+(repo?repo.name:'?')+' / '+((task&&task.title)||''), hint:r.status, run:function(){ openRunPane(r.id); }});
|
|
1190
|
+
});
|
|
1191
|
+
// 프로젝트 → 요청바 대상 지정(팬아웃 준비)
|
|
1192
|
+
Object.keys(repoById).forEach(function(id){ var repo=repoById[id]; if(repo.kind==='sessions') return;
|
|
1193
|
+
items.push({k:'project', label:repo.name, hint:'팬아웃 대상 지정', run:function(){ try{ $('reqRepo').value=String(repo.id); }catch(e){} var i=$('reqInput'); if(i){ i.focus(); } }}); });
|
|
1194
|
+
return items;
|
|
1195
|
+
}
|
|
1196
|
+
function renderPalette(q){
|
|
1197
|
+
q=(q||'').trim().toLowerCase();
|
|
1198
|
+
var all=paletteItems();
|
|
1199
|
+
palItems = q ? all.filter(function(it){ return (it.label+' '+(it.hint||'')+' '+it.k).toLowerCase().indexOf(q)>=0; }) : all;
|
|
1200
|
+
palSel=0;
|
|
1201
|
+
if(!palItems.length){ $('palList').innerHTML='<div class="pal-empty">일치 없음</div>'; return; }
|
|
1202
|
+
$('palList').innerHTML = palItems.map(function(it,i){
|
|
1203
|
+
return '<div class="pal-row'+(i===0?' sel':'')+'" data-i="'+i+'"><span class="pk">'+esc(it.k)+'</span><span class="pl">'+esc(it.label)+'</span>'+(it.hint?'<span class="ph">'+esc(it.hint)+'</span>':'')+'</div>';
|
|
1204
|
+
}).join('');
|
|
1205
|
+
}
|
|
1206
|
+
function palMove(d){ if(!palItems.length) return; palSel=(palSel+d+palItems.length)%palItems.length;
|
|
1207
|
+
var rows=$('palList').querySelectorAll('.pal-row'); rows.forEach(function(el,i){ el.classList.toggle('sel', i===palSel); });
|
|
1208
|
+
var sel=rows[palSel]; if(sel) sel.scrollIntoView({block:'nearest'}); }
|
|
1209
|
+
function palRun(i){ var it=palItems[i]; if(!it) return; closePalette(); try{ it.run(); }catch(e){} }
|
|
1210
|
+
function openPalette(){ $('palette').classList.add('on'); var i=$('palInput'); i.value=''; renderPalette(''); i.focus(); }
|
|
1211
|
+
function closePalette(){ $('palette').classList.remove('on'); }
|
|
1212
|
+
$('palInput').addEventListener('input', function(){ renderPalette(this.value); });
|
|
1213
|
+
$('palInput').addEventListener('keydown', function(e){
|
|
1214
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); palMove(1); }
|
|
1215
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); palMove(-1); }
|
|
1216
|
+
else if(e.key==='Enter'){ e.preventDefault(); palRun(palSel); }
|
|
1217
|
+
else if(e.key==='Escape'){ e.preventDefault(); closePalette(); }
|
|
1218
|
+
});
|
|
1219
|
+
$('palList').addEventListener('click', function(e){ var row=e.target.closest('[data-i]'); if(row) palRun(Number(row.dataset.i)); });
|
|
1220
|
+
$('palette').addEventListener('click', function(e){ if(e.target===this) closePalette(); });
|
|
1221
|
+
document.addEventListener('keydown', function(e){
|
|
1222
|
+
if((e.metaKey||e.ctrlKey) && (e.key==='k'||e.key==='K')){ e.preventDefault(); if($('palette').classList.contains('on')) closePalette(); else openPalette(); }
|
|
1223
|
+
});
|
|
1224
|
+
// ── 페인 단축키: ⌘⏎ 최대화 토글 · ⌘1~9 N번째 페인 포커스 ──
|
|
1225
|
+
function typingInField(e){ var el=e.target; if(!el) return false; var tag=el.tagName||''; return (tag==='INPUT'||tag==='TEXTAREA') && !el.classList.contains('xterm-helper-textarea'); }
|
|
1226
|
+
document.addEventListener('keydown', function(e){
|
|
1227
|
+
if(!(e.metaKey||e.ctrlKey) || e.altKey) return;
|
|
1228
|
+
if(!tabOrder.length || typingInField(e)) return;
|
|
1229
|
+
if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); toggleZoom(focusLeaf); return; }
|
|
1230
|
+
if(e.key>='1' && e.key<='9'){ var order=leafOrder(); var idx=Number(e.key)-1; if(order[idx]){ e.preventDefault(); if(zoomLeaf){ zoomLeaf=order[idx].id; focusLeaf=order[idx].id; render(); } else setLeafFocus(order[idx].id); } }
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1050
1233
|
// ── 모바일 터미널 입력바 — 조합 완료 텍스트를 통째로 포커스 탭의 PTY 로(IME 안전) ──
|
|
1051
1234
|
function focusedWs(){ var rid=focusedRunId(); return (rid!=null && tabs[rid]) ? tabs[rid].ws : null; }
|
|
1052
1235
|
function termSendRaw(d){ var ws=focusedWs(); if(ws && ws.readyState===1) ws.send(JSON.stringify({t:'i',d:d})); }
|
|
@@ -1339,10 +1522,14 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1339
1522
|
loadCompare(rvTaskId);
|
|
1340
1523
|
}
|
|
1341
1524
|
$('rvTask').addEventListener('change', function(){ rvTaskId=Number(this.value); syncVcmd(); loadCompare(rvTaskId); });
|
|
1525
|
+
$('rvWs').addEventListener('click', function(){ rvShowWs=!rvShowWs; this.classList.toggle('on', rvShowWs); if(rvTaskId!=null) loadCompare(rvTaskId); });
|
|
1526
|
+
var rvShowWs=false;
|
|
1527
|
+
function revealWs(e){ return e.replace(/ /g,'<span class="ws">·</span>').replace(/\\t/g,'<span class="ws">→\\u2003</span>'); }
|
|
1342
1528
|
function diffHTML(text){
|
|
1343
1529
|
if (!text || !text.trim()) return '<span style="color:var(--faint)">no changes</span>';
|
|
1344
1530
|
return text.split('\\n').map(function(l){
|
|
1345
1531
|
var e = esc(l) || ' ';
|
|
1532
|
+
if (rvShowWs && l.indexOf('diff --git')!==0 && l.indexOf('@@')!==0) e = revealWs(e);
|
|
1346
1533
|
if (l.indexOf('diff --git')===0 || l.indexOf('+++')===0 || l.indexOf('---')===0) return '<span class="dl-file">'+e+'</span>';
|
|
1347
1534
|
if (l.indexOf('@@')===0) return '<span class="dl-hunk">'+e+'</span>';
|
|
1348
1535
|
if (l.charAt(0)==='+') return '<span class="dl-add">'+e+'</span>';
|
package/src/server.ts
CHANGED
|
@@ -37,6 +37,7 @@ const VENDOR: Record<string, { pkg: string; rel: string; type: string }> = {
|
|
|
37
37
|
'addon-fit.js': { pkg: '@xterm/addon-fit/package.json', rel: 'lib/addon-fit.js', type: 'text/javascript' },
|
|
38
38
|
'addon-unicode11.js': { pkg: '@xterm/addon-unicode11/package.json', rel: 'lib/addon-unicode11.js', type: 'text/javascript' },
|
|
39
39
|
'addon-web-links.js': { pkg: '@xterm/addon-web-links/package.json', rel: 'lib/addon-web-links.js', type: 'text/javascript' },
|
|
40
|
+
'addon-search.js': { pkg: '@xterm/addon-search/package.json', rel: 'lib/addon-search.js', type: 'text/javascript' },
|
|
40
41
|
'marked.js': { pkg: 'marked/package.json', rel: 'marked.min.js', type: 'text/javascript' },
|
|
41
42
|
};
|
|
42
43
|
|