coxpit 5.23.1 → 5.26.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 +267 -15
- 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.26.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
|
@@ -54,6 +54,11 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
54
54
|
.toggle:hover{color:var(--ink);border-color:var(--line-hi)}
|
|
55
55
|
|
|
56
56
|
.layout{display:grid;grid-template-columns:270px 1fr;height:calc(100dvh - 46px)}
|
|
57
|
+
/* 포커스 모드 — 트리·요청바 숨기고 페인만(⌘.) */
|
|
58
|
+
.layout.focusmode{grid-template-columns:1fr}
|
|
59
|
+
.layout.focusmode .rail{display:none}
|
|
60
|
+
.layout.focusmode .reqbar{display:none}
|
|
61
|
+
.tc-btn.on{color:var(--brand-ink);background:var(--brand);border-color:var(--brand)}
|
|
57
62
|
.layout > *{min-height:0;min-width:0}
|
|
58
63
|
|
|
59
64
|
/* ── workspace tree ── */
|
|
@@ -81,6 +86,15 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
81
86
|
|
|
82
87
|
/* ── 탭 바 + 분할 트리 페인 ── */
|
|
83
88
|
.stage{display:flex;flex-direction:column;min-width:0;background:var(--bg);position:relative}
|
|
89
|
+
/* ── 터미널 스크롤백 검색바(⌘F) ── */
|
|
90
|
+
.term-find{position:absolute;top:44px;right:14px;z-index:30;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)}
|
|
91
|
+
.term-find[hidden]{display:none} /* hidden 속성 존중 — 아래 규칙이 UA 를 이기지 않게 */
|
|
92
|
+
.term-find:not([hidden]){display:flex}
|
|
93
|
+
.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}
|
|
94
|
+
.term-find input:focus{border-color:var(--brand)}
|
|
95
|
+
.term-find .fc{font-family:var(--mono);font-size:10.5px;color:var(--faint);min-width:20px;text-align:center}
|
|
96
|
+
.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}
|
|
97
|
+
.term-find button:hover{color:var(--ink);background:var(--surface2)}
|
|
84
98
|
.tabbar{display:flex;align-items:stretch;height:38px;border-bottom:1px solid var(--line);background:var(--panel);font-family:var(--mono)}
|
|
85
99
|
.tabs{flex:1;display:flex;align-items:stretch;overflow-x:auto;overflow-y:hidden}
|
|
86
100
|
.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 +199,19 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
185
199
|
/* ── folder picker (자유 세션 폴더 지정) ── */
|
|
186
200
|
.modal{position:fixed;inset:0;background:rgba(4,6,10,.6);display:none;align-items:center;justify-content:center;z-index:60}
|
|
187
201
|
.modal.on{display:flex}
|
|
202
|
+
/* ── ⌘K 커맨드 팔레트 ── */
|
|
203
|
+
#palette{align-items:flex-start}
|
|
204
|
+
.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)}
|
|
205
|
+
.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}
|
|
206
|
+
.pal-list{max-height:52vh;overflow:auto;padding:6px;scrollbar-width:none}
|
|
207
|
+
.pal-list::-webkit-scrollbar{width:0;display:none}
|
|
208
|
+
.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)}
|
|
209
|
+
.pal-row.sel{background:var(--brand-dim);color:var(--ink)}
|
|
210
|
+
.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}
|
|
211
|
+
.pal-row.sel .pk{border-color:rgba(78,201,176,.4);color:var(--brand)}
|
|
212
|
+
.pal-row .pl{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
213
|
+
.pal-row .ph{font-size:10.5px;color:var(--faint);flex:none;max-width:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
214
|
+
.pal-empty{padding:18px;text-align:center;color:var(--faint);font-family:var(--mono);font-size:12px}
|
|
188
215
|
.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
216
|
.pick-h{display:flex;align-items:center;gap:10px;padding:13px 15px;border-bottom:1px solid var(--line)}
|
|
190
217
|
.pick-h .t{font-size:13px;color:var(--ink);font-weight:600}
|
|
@@ -215,6 +242,16 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
215
242
|
.sec-row .shint{color:var(--faint);font-size:11px;font-family:var(--mono)}
|
|
216
243
|
.sec-row .sdel{color:var(--faint);border:none;background:none;cursor:pointer;font-size:14px;padding:0 4px}
|
|
217
244
|
.sec-row .sdel:hover{color:var(--failed)}
|
|
245
|
+
.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}
|
|
246
|
+
.leaf-h .pane-act:empty{display:none}
|
|
247
|
+
.tnode.run .ract{margin-left:6px;font-size:10px;color:var(--brand);opacity:.85}
|
|
248
|
+
.leaf-h .zoom{color:var(--faint);border:none;background:none;cursor:pointer;font-size:12px;padding:0 2px}
|
|
249
|
+
.leaf-h .zoom:hover{color:var(--brand)}
|
|
250
|
+
.leaf-h .pact{color:var(--faint);border:none;background:none;cursor:pointer;font-size:12px;padding:0 2px}
|
|
251
|
+
.leaf-h .pact:hover{color:var(--brand)}
|
|
252
|
+
.leaf-h .bsel{display:none;color:var(--faint);border:none;background:none;cursor:pointer;font-size:11px;padding:0 2px}
|
|
253
|
+
body.bcastmode .leaf-h .bsel{display:inline-block} /* 브로드캐스트 모드에서만 대상 선택 토글 노출 */
|
|
254
|
+
.leaf-h .bsel.on{color:var(--open)}
|
|
218
255
|
.leaf-h .lock{color:var(--faint);border:none;background:none;cursor:pointer;font-size:12px;padding:0 2px}
|
|
219
256
|
.leaf-h .lock:hover{color:var(--brand)}
|
|
220
257
|
.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 +296,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
259
296
|
.rv-diff > span{display:block;min-height:1.2em}
|
|
260
297
|
.dl-file{color:var(--brand)} .dl-hunk{color:var(--open)} .dl-ctx{color:var(--muted)}
|
|
261
298
|
.dl-add{color:#7fdca0;background:rgba(88,179,104,.08)} .dl-del{color:#e58a92;background:rgba(226,91,103,.08)}
|
|
299
|
+
.rv-diff .ws{color:var(--faint);opacity:.55}
|
|
300
|
+
.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}
|
|
301
|
+
.rv-ws:hover{color:var(--ink)} .rv-ws.on{color:var(--brand-ink);background:var(--brand);border-color:var(--brand)}
|
|
262
302
|
.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
303
|
|
|
264
304
|
/* ── 모바일 대응 (드로어 트리 + 단일 터미널 + IME 입력바) ── */
|
|
@@ -339,7 +379,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
339
379
|
</header>
|
|
340
380
|
|
|
341
381
|
<div class="scrim" id="scrim"></div>
|
|
342
|
-
<div class="layout">
|
|
382
|
+
<div class="layout" id="layout">
|
|
343
383
|
<aside class="rail" id="rail">
|
|
344
384
|
<div class="lbl"><span>Workspace</span><span id="machName" style="color:var(--faint)">local</span></div>
|
|
345
385
|
<div id="tree"></div>
|
|
@@ -354,10 +394,18 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
354
394
|
<button class="tc-btn" id="splitCol" title="가로 분할 — 포커스 페인을 상하로" disabled>▬ split</button>
|
|
355
395
|
<button class="tc-btn session" id="sessionBtn" title="자유 세션(폴더 지정 터미널) 열기">+<span class="b-txt"> Session</span></button>
|
|
356
396
|
<button class="tc-btn" id="fileBtn" title="파일 보기 — md·html·pdf·이미지·텍스트 뷰어(터미널 옆 페인)">▤<span class="b-txt"> File</span></button>
|
|
397
|
+
<button class="tc-btn" id="focusBtn" title="포커스 모드 — 트리·요청바 숨기고 페인만 (⌘.)">◱<span class="b-txt"> Focus</span></button>
|
|
357
398
|
<button class="tc-btn" id="closeBtn" title="포커스 페인 닫기(탭은 유지)" disabled>×<span class="b-txt"> pane</span></button>
|
|
358
399
|
</div>
|
|
359
400
|
</div>
|
|
360
401
|
<div class="panes" id="panes"></div>
|
|
402
|
+
<div class="term-find" id="termFind" hidden>
|
|
403
|
+
<input id="findInput" placeholder="터미널에서 찾기 (⌘F)" autocomplete="off" spellcheck="false" />
|
|
404
|
+
<span class="fc" id="findCount"></span>
|
|
405
|
+
<button type="button" id="findPrev" title="이전 (⇧⏎)">↑</button>
|
|
406
|
+
<button type="button" id="findNext" title="다음 (⏎)">↓</button>
|
|
407
|
+
<button type="button" id="findClose" title="닫기 (esc)">×</button>
|
|
408
|
+
</div>
|
|
361
409
|
<div class="empty" id="empty">
|
|
362
410
|
<div class="card">
|
|
363
411
|
<div class="glyph">⌗ ⌗ ⌗</div>
|
|
@@ -421,6 +469,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
421
469
|
<input id="rvVcmd" placeholder="예: npm test — 정착 시 자동 실행" autocomplete="off" />
|
|
422
470
|
<button type="button" id="rvVsave">save</button>
|
|
423
471
|
</span>
|
|
472
|
+
<button type="button" id="rvWs" class="rv-ws" title="공백 표시(스페이스·탭)">␣ 공백</button>
|
|
424
473
|
<span id="rvHint" style="margin-left:auto;color:var(--faint)">정착하면 자동 검증 · 승자를 base 에 merge</span>
|
|
425
474
|
</div>
|
|
426
475
|
<div class="rv-cols" id="rvCols"></div>
|
|
@@ -479,12 +528,20 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
479
528
|
</div>
|
|
480
529
|
</div>
|
|
481
530
|
|
|
531
|
+
<div class="modal" id="palette">
|
|
532
|
+
<div class="pal">
|
|
533
|
+
<input id="palInput" placeholder="이동·명령 검색 (⌘K) · 세션·run·프로젝트·명령" autocomplete="off" spellcheck="false" />
|
|
534
|
+
<div class="pal-list" id="palList"></div>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
482
538
|
<div class="toast" id="toast"></div>
|
|
483
539
|
|
|
484
540
|
<script src="/vendor/xterm.js"></script>
|
|
485
541
|
<script src="/vendor/addon-fit.js"></script>
|
|
486
542
|
<script src="/vendor/addon-unicode11.js"></script>
|
|
487
543
|
<script src="/vendor/addon-web-links.js"></script>
|
|
544
|
+
<script src="/vendor/addon-search.js"></script>
|
|
488
545
|
<script src="/vendor/marked.js"></script>
|
|
489
546
|
<script>
|
|
490
547
|
// 모바일 = 터미널 우선을 유지하되 좁은 화면에 맞춤(드로어 트리 + 단일 터미널 + IME 입력바).
|
|
@@ -496,6 +553,23 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
496
553
|
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
554
|
var V_GLYPH = { pass:'✓ verify', fail:'✗ verify', running:'⋯ verify', error:'! verify' };
|
|
498
555
|
function vbadge(status){ if (!status || !V_GLYPH[status]) return ''; return '<span class="vbadge '+status+'" data-role="vbadge">'+V_GLYPH[status]+'</span>'; }
|
|
556
|
+
// 라이브 상태 — run 의 최신 이벤트에서 "지금 뭐 하는지"(도구명/사고)를 뽑는다. 실행 중일 때만.
|
|
557
|
+
function latestActivity(runId){
|
|
558
|
+
var r=runById[runId]; if(!r) return '';
|
|
559
|
+
if(r.status!=='running' && r.status!=='pending') return '';
|
|
560
|
+
var evs=r.events||[];
|
|
561
|
+
for(var i=evs.length-1;i>=0;i--){
|
|
562
|
+
var e=evs[i], k=e.kind;
|
|
563
|
+
if(k==='steer') return 'steer'; if(k==='ask') return 'asking';
|
|
564
|
+
if(k!=='assistant') continue;
|
|
565
|
+
try{ var o=JSON.parse(e.payload);
|
|
566
|
+
var c=o&&o.message&&o.message.content;
|
|
567
|
+
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'; } }
|
|
568
|
+
else if(o&&o.text) return 'thinking';
|
|
569
|
+
}catch(_){}
|
|
570
|
+
}
|
|
571
|
+
return r.status==='pending' ? 'starting' : 'working';
|
|
572
|
+
}
|
|
499
573
|
|
|
500
574
|
// ── fleet 상태 ──
|
|
501
575
|
var fleet = { machines:[], repos:[], tasks:[], groups:[], runs:[], providers:[] };
|
|
@@ -503,6 +577,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
503
577
|
var fold = {}; // 접힘 상태(repo/goal/task 노드)
|
|
504
578
|
function isFold(k){ return fold[k]===true; }
|
|
505
579
|
function repoOfRun(runId){ var r=runById[runId]; var t=r&&taskById[r.taskId]; return t?t.repoId:null; }
|
|
580
|
+
function runIsSession(runId){ var r=runById[runId]; var t=r&&taskById[r.taskId]; var rp=t&&repoById[t.repoId]; return !!(rp&&rp.kind==='sessions'); }
|
|
506
581
|
|
|
507
582
|
async function hydrate(){
|
|
508
583
|
try{
|
|
@@ -595,8 +670,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
595
670
|
+ '<span class="n">'+esc(t.title)+'</span></div>';
|
|
596
671
|
if (!isFold(tk)) rns.sort(function(a,b){return a.id-b.id;}).forEach(function(r){
|
|
597
672
|
var open = tabs[r.id] ? ' open' : '';
|
|
673
|
+
var act=latestActivity(r.id);
|
|
598
674
|
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
|
|
675
|
+
+ '<span class="n">r'+r.id+' · '+esc(r.status)+'</span>'+(act?'<span class="ract">'+esc(act)+'</span>':'')+'</div>';
|
|
600
676
|
});
|
|
601
677
|
return s;
|
|
602
678
|
}
|
|
@@ -623,6 +699,8 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
623
699
|
var tabOrder = []; // 탭 바 순서(runId)
|
|
624
700
|
var layout = { leaf:true, id:'L0', tab:null }; // 분할 트리 루트
|
|
625
701
|
var focusLeaf = 'L0';
|
|
702
|
+
var zoomLeaf = null; // 페인 최대화(줌) — 설정되면 그 리프만 전체 렌더
|
|
703
|
+
var bcastSel = {}; // 브로드캐스트 선택 대상(tab id → true). 비어있으면 전체.
|
|
626
704
|
var leafSeq = 1;
|
|
627
705
|
var viewerSeq = 1; // 뷰어 탭 키(문자열 'v#')
|
|
628
706
|
var MAX_LEAVES = 6;
|
|
@@ -682,6 +760,10 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
682
760
|
}
|
|
683
761
|
cb(links.length?links:undefined);
|
|
684
762
|
}}); }catch(e){}
|
|
763
|
+
// 스크롤백 검색(⌘F) — SearchAddon. t.search 에 보관, 검색바가 findNext/Previous 호출.
|
|
764
|
+
var search=null; try{ search=new window.SearchAddon.SearchAddon(); term.loadAddon(search);
|
|
765
|
+
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'; } } });
|
|
766
|
+
}catch(e){}
|
|
685
767
|
// 복사 배선: xterm 은 user-select:none 이라 네이티브 선택이 없다 → term.getSelection() 을 직접 클립보드로.
|
|
686
768
|
// ① 드래그 놓으면 자동 복사(select-to-copy) ② Cmd/Ctrl+C 로도 복사(선택 없으면 통과 → SIGINT).
|
|
687
769
|
var copySel=function(){ var s=''; try{ s=term.getSelection(); }catch(e){} if(!s) return false;
|
|
@@ -691,13 +773,15 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
691
773
|
host.addEventListener('mouseup', function(){ copySel(); });
|
|
692
774
|
host.addEventListener('touchend', function(){ copySel(); });
|
|
693
775
|
term.attachCustomKeyEventHandler(function(ev){
|
|
694
|
-
if(ev.type==='keydown' && (ev.metaKey||ev.ctrlKey)
|
|
695
|
-
if(term.hasSelection()){ copySel(); return false; }
|
|
776
|
+
if(ev.type==='keydown' && (ev.metaKey||ev.ctrlKey)){
|
|
777
|
+
if(ev.key==='c'||ev.key==='C'){ if(term.hasSelection()){ copySel(); return false; } }
|
|
778
|
+
// 전역 단축키(⌘K·⌘F·⌘⏎·⌘1~9)는 터미널로 보내지 않는다 — 문서 핸들러가 처리.
|
|
779
|
+
if(ev.key==='k'||ev.key==='K'||ev.key==='f'||ev.key==='F'||ev.key==='Enter'||ev.key==='.'||(ev.key>='1'&&ev.key<='9')) return false;
|
|
696
780
|
}
|
|
697
781
|
return true;
|
|
698
782
|
});
|
|
699
783
|
// 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 };
|
|
784
|
+
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
785
|
term.onData(function(d){ if(t.ws&&t.ws.readyState===1) t.ws.send(JSON.stringify({t:'i',d:d})); });
|
|
702
786
|
tabs[runId]=t; tabOrder.push(runId);
|
|
703
787
|
return t; // open·connect 는 attachHosts 에서(Phase 2 순서: open → connect)
|
|
@@ -759,7 +843,10 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
759
843
|
var fp=document.createElement('iframe'); fp.src=rawUrl; // 브라우저/Electron 내장 PDF 뷰어(수동적 — sandbox 불필요)
|
|
760
844
|
body.innerHTML=''; body.appendChild(fp);
|
|
761
845
|
} else if(d.kind==='image'){
|
|
762
|
-
body.innerHTML=''; var im=document.createElement('img'); im.
|
|
846
|
+
body.innerHTML=''; var im=document.createElement('img'); im.alt=t.name;
|
|
847
|
+
// 브라우저가 못 그리는 형식(HEIC/TIFF 등)이나 로드 실패 시 조용히 빈 화면 대신 이유+원본 링크
|
|
848
|
+
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>'; };
|
|
849
|
+
im.src=rawUrl; body.appendChild(im);
|
|
763
850
|
} else if(d.kind==='binary'){
|
|
764
851
|
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
852
|
} else { // text
|
|
@@ -849,15 +936,20 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
849
936
|
var leaf=document.createElement('div'); leaf.className='leaf'+(node.id===focusLeaf?' focus':''); leaf.dataset.leaf=node.id;
|
|
850
937
|
var t=node.tab!=null?tabs[node.tab]:null; var r=node.tab!=null?runById[node.tab]:null;
|
|
851
938
|
var head=document.createElement('div'); head.className='leaf-h'; head.dataset.leafhead=node.id;
|
|
939
|
+
var zoomBtn = '<button class="zoom" data-zoom="'+node.id+'" title="이 페인 최대화 (⌘⏎)">'+(zoomLeaf?'⤡':'⤢')+'</button>';
|
|
852
940
|
head.innerHTML = t
|
|
853
941
|
? (t.kind==='viewer'
|
|
854
942
|
? '<span class="st vdoc">▤</span><span class="nm">'+esc(t.name)+'</span>'
|
|
855
|
-
+ '<button class="x" title="이 페인 닫기(뷰어 유지)">×</button>'
|
|
943
|
+
+ zoomBtn + '<button class="x" title="이 페인 닫기(뷰어 유지)">×</button>'
|
|
856
944
|
: '<span class="st '+esc(r?r.status:'')+'"></span><span class="nm">'+esc(t.name)+'</span>'
|
|
945
|
+
+ '<span data-role="act" class="pane-act">'+esc(latestActivity(node.tab))+'</span>'
|
|
857
946
|
+ '<span data-role="chip" class="chip '+esc(r?r.status:'')+'">'+esc(r?r.status:'')+'</span>'
|
|
858
947
|
+ '<span data-role="vslot">'+vbadge(r&&r.verifyStatus)+'</span>'
|
|
948
|
+
+ '<button class="bsel'+(bcastSel[node.tab]?' on':'')+'" data-bcast="'+node.tab+'" title="브로드캐스트 대상 토글">'+(bcastSel[node.tab]?'◉':'◯')+'</button>'
|
|
949
|
+
+ (runIsSession(node.tab)?'':'<button class="pact" data-diff="'+node.id+'" title="이 run 의 diff (Review)">⧉</button>')
|
|
950
|
+
+ '<button class="pact" data-hist="'+node.id+'" title="이 run 히스토리 (대화·터미널)">↺</button>'
|
|
859
951
|
+ '<button class="lock" data-lock="'+node.id+'" title="이 페인에 시크릿/비밀번호 전송(터미널에 안 찍힘)">⊟</button>'
|
|
860
|
-
+ '<button class="x" title="이 페인 닫기(탭은 유지)">×</button>')
|
|
952
|
+
+ zoomBtn + '<button class="x" title="이 페인 닫기(탭은 유지)">×</button>')
|
|
861
953
|
: '<span class="nm" style="color:var(--faint)">빈 페인</span><button class="x" title="이 페인 닫기">×</button>';
|
|
862
954
|
var body=document.createElement('div'); body.className='leaf-body'; body.dataset.leafbody=node.id;
|
|
863
955
|
if (!t){ var em=document.createElement('div'); em.className='leaf-empty'; em.textContent='탭을 여기로 드래그하거나 탭을 클릭하세요'; body.appendChild(em); }
|
|
@@ -887,7 +979,11 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
887
979
|
$('panes').style.display = n?'flex':'none';
|
|
888
980
|
renderTabs();
|
|
889
981
|
var host=$('panes'); host.innerHTML='';
|
|
890
|
-
if (n){
|
|
982
|
+
if (n){
|
|
983
|
+
var zl = zoomLeaf ? findLeaf(zoomLeaf) : null; // 줌: 그 리프만 전체 렌더(레이아웃은 메모리에 보존)
|
|
984
|
+
var root = zl ? { leaf:true, id:zl.id, tab:zl.tab } : layout;
|
|
985
|
+
host.appendChild(buildNode(root)); attachHosts();
|
|
986
|
+
}
|
|
891
987
|
updateControls();
|
|
892
988
|
if (typeof reqMode!=='undefined') setMode(reqMode);
|
|
893
989
|
requestAnimationFrame(fitAllVisible);
|
|
@@ -900,9 +996,41 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
900
996
|
updateControls();
|
|
901
997
|
if (typeof reqMode!=='undefined' && reqMode!=='new') setMode(reqMode);
|
|
902
998
|
}
|
|
999
|
+
// 페인 최대화(줌) 토글 — 포커스 페인(또는 지정 leaf)만 전체로. 다시 누르면 복원.
|
|
1000
|
+
function toggleZoom(id){
|
|
1001
|
+
var target = id || focusLeaf;
|
|
1002
|
+
if(zoomLeaf===target){ zoomLeaf=null; }
|
|
1003
|
+
else { var l=findLeaf(target); if(!l || l.tab==null) return; zoomLeaf=target; focusLeaf=target; }
|
|
1004
|
+
render();
|
|
1005
|
+
}
|
|
1006
|
+
// 페인 순서(트리 순회) → ⌘1..9 로 N번째 포커스
|
|
1007
|
+
function leafOrder(){ var out=[]; eachLeaf(layout,function(l){ out.push(l); }); return out; }
|
|
1008
|
+
|
|
1009
|
+
// ── 레이아웃 저장/복원 (localStorage, 기기별) ──
|
|
1010
|
+
// 트리 + 각 페인의 탭을 복원 키(run=runId · viewer=경로)로 직렬화한다.
|
|
1011
|
+
function serializeNode(node){
|
|
1012
|
+
if(node.leaf){ var d=null; if(node.tab!=null){ var t=tabs[node.tab]; if(t){ d = (t.kind==='viewer') ? {type:'viewer',path:t.path,name:t.name} : {type:'run',runId:node.tab}; } }
|
|
1013
|
+
return {leaf:true, tab:d}; }
|
|
1014
|
+
return {split:true, dir:node.dir, ratio:node.ratio, a:serializeNode(node.a), b:serializeNode(node.b)};
|
|
1015
|
+
}
|
|
1016
|
+
function rebuildNode(sn){
|
|
1017
|
+
if(sn.leaf){ var tabId=null;
|
|
1018
|
+
if(sn.tab){ if(sn.tab.type==='viewer'){ var vt=ensureViewer(sn.tab.path, sn.tab.name); tabId=vt.runId; }
|
|
1019
|
+
else if(sn.tab.type==='run' && runById[sn.tab.runId]){ ensureTab(sn.tab.runId); tabId=sn.tab.runId; } }
|
|
1020
|
+
return {leaf:true, id:newLeafId(), tab:tabId}; }
|
|
1021
|
+
return {split:true, id:newLeafId(), dir:sn.dir, ratio:sn.ratio, a:rebuildNode(sn.a), b:rebuildNode(sn.b)};
|
|
1022
|
+
}
|
|
1023
|
+
function loadLayouts(){ try{ return JSON.parse(localStorage.getItem('coxpit.layouts')||'{}')||{}; }catch(e){ return {}; } }
|
|
1024
|
+
function saveLayouts(o){ try{ localStorage.setItem('coxpit.layouts', JSON.stringify(o)); }catch(e){} }
|
|
1025
|
+
function saveLayout(){ if(!tabOrder.length){ toast('저장할 페인이 없습니다'); return; } var nm=prompt('레이아웃 이름'); if(!nm||!nm.trim()) return; nm=nm.trim();
|
|
1026
|
+
var o=loadLayouts(); o[nm]={tree:serializeNode(layout), ts:Date.now()}; saveLayouts(o); toast('레이아웃 저장 · '+nm); }
|
|
1027
|
+
function restoreLayout(nm){ var o=loadLayouts(); var s=o[nm]; if(!s){ toast('없는 레이아웃'); return; }
|
|
1028
|
+
layout=rebuildNode(s.tree); zoomLeaf=null; var f=firstLeaf(); focusLeaf=f?f.id:'L0'; render(); renderTree(); toast('레이아웃 복원 · '+nm); }
|
|
1029
|
+
function deleteLayout(nm){ var o=loadLayouts(); delete o[nm]; saveLayouts(o); toast('레이아웃 삭제 · '+nm); }
|
|
903
1030
|
|
|
904
1031
|
// 탭 열기 = 포커스 슬롯에 표시(강제 분할 없음). 기존 호출부(openRunPane) 호환.
|
|
905
1032
|
function openTab(runId){
|
|
1033
|
+
zoomLeaf=null; // 새 탭은 보여야 하므로 줌 해제
|
|
906
1034
|
if(!tabs[runId]) ensureTab(runId); // 뷰어 탭은 ensureViewer 로 이미 생성됨 → 터미널로 오생성 방지
|
|
907
1035
|
var l=leafOfTab(runId);
|
|
908
1036
|
if (l){ setLeafFocus(l.id); return; }
|
|
@@ -915,6 +1043,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
915
1043
|
|
|
916
1044
|
function splitFocused(dir){
|
|
917
1045
|
if (!tabOrder.length) return;
|
|
1046
|
+
zoomLeaf=null;
|
|
918
1047
|
if (isMobile()){ toast('창분할은 데스크톱 전용 — 모바일은 탭으로 전환하세요'); return; }
|
|
919
1048
|
if (countLeaves()>=MAX_LEAVES){ toast('페인 최대 '+MAX_LEAVES+'개'); return; }
|
|
920
1049
|
var l=findLeaf(focusLeaf) || firstLeaf(); if(!l) return;
|
|
@@ -924,6 +1053,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
924
1053
|
focusLeaf=bId; render(); renderTree();
|
|
925
1054
|
}
|
|
926
1055
|
function closeSlot(id){
|
|
1056
|
+
zoomLeaf=null;
|
|
927
1057
|
if (countLeaves()<=1){ var only=findLeaf(id)||firstLeaf(); if(only) only.tab=null; if(only) focusLeaf=only.id; render(); renderTree(); return; }
|
|
928
1058
|
(function walk(node){
|
|
929
1059
|
if(node.leaf) return false;
|
|
@@ -935,6 +1065,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
935
1065
|
render(); renderTree();
|
|
936
1066
|
}
|
|
937
1067
|
function closeTab(runId){
|
|
1068
|
+
zoomLeaf=null;
|
|
938
1069
|
var t=tabs[runId]; if(!t) return; t.closing=true;
|
|
939
1070
|
try{ if(t.ws) t.ws.close(); }catch(e){}
|
|
940
1071
|
try{ if(t.ro) t.ro.disconnect(); }catch(e){}
|
|
@@ -953,6 +1084,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
953
1084
|
}
|
|
954
1085
|
function tileTabs(ids){
|
|
955
1086
|
ids=(ids||[]).filter(function(x){return x!=null;}); if(!ids.length) return;
|
|
1087
|
+
zoomLeaf=null;
|
|
956
1088
|
ids.forEach(ensureTab);
|
|
957
1089
|
layout=buildTiled(ids,'row'); var f=firstLeaf(); focusLeaf=f?f.id:'L0';
|
|
958
1090
|
render(); renderTree();
|
|
@@ -970,6 +1102,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
970
1102
|
var st=head.querySelector('.st'); if(st) st.className='st '+r.status;
|
|
971
1103
|
var chip=head.querySelector('[data-role=chip]'); if(chip){ chip.className='chip '+r.status; chip.textContent=r.status; }
|
|
972
1104
|
var vslot=head.querySelector('[data-role=vslot]'); if(vslot) vslot.innerHTML=vbadge(r.verifyStatus);
|
|
1105
|
+
var act=head.querySelector('[data-role=act]'); if(act) act.textContent=latestActivity(l.tab);
|
|
973
1106
|
});
|
|
974
1107
|
}
|
|
975
1108
|
|
|
@@ -1010,6 +1143,16 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1010
1143
|
|
|
1011
1144
|
// ── 페인(슬롯) 이벤트: 포커스·닫기·드롭·리사이즈 ──
|
|
1012
1145
|
$('panes').addEventListener('click', function(e){
|
|
1146
|
+
var zoomBtn=e.target.closest('[data-zoom]');
|
|
1147
|
+
if (zoomBtn){ e.stopPropagation(); toggleZoom(zoomBtn.getAttribute('data-zoom')); return; }
|
|
1148
|
+
var bcBtn=e.target.closest('[data-bcast]');
|
|
1149
|
+
if (bcBtn){ e.stopPropagation(); var bid=bcBtn.getAttribute('data-bcast'); bcastSel[bid]=!bcastSel[bid];
|
|
1150
|
+
bcBtn.classList.toggle('on', !!bcastSel[bid]); bcBtn.textContent = bcastSel[bid]?'◉':'◯';
|
|
1151
|
+
if(reqMode==='bcast') setMode('bcast'); return; }
|
|
1152
|
+
var diffBtn=e.target.closest('[data-diff]');
|
|
1153
|
+
if (diffBtn){ e.stopPropagation(); var dl=findLeaf(diffBtn.getAttribute('data-diff')); if(dl&&dl.tab!=null) openReviewForRun(dl.tab); return; }
|
|
1154
|
+
var histBtn=e.target.closest('[data-hist]');
|
|
1155
|
+
if (histBtn){ e.stopPropagation(); var hl=findLeaf(histBtn.getAttribute('data-hist')); if(hl&&hl.tab!=null){ setLeafFocus(hl.id); openHistory(); } return; }
|
|
1013
1156
|
var lockBtn=e.target.closest('[data-lock]');
|
|
1014
1157
|
if (lockBtn){ e.stopPropagation(); startSecretSend(lockBtn.getAttribute('data-lock'), lockBtn); return; }
|
|
1015
1158
|
var leaf=e.target.closest('[data-leaf]'); if(!leaf) return;
|
|
@@ -1047,6 +1190,106 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1047
1190
|
$('menuBtn').addEventListener('click', function(){ setDrawer(!$('rail').classList.contains('open')); });
|
|
1048
1191
|
$('scrim').addEventListener('click', function(){ setDrawer(false); });
|
|
1049
1192
|
|
|
1193
|
+
// ── 터미널 스크롤백 검색(⌘F) ──
|
|
1194
|
+
function focusedTermTab(){ var rid=focusedRunId(); var t=(rid!=null)?tabs[rid]:null; return (t && t.term && t.search) ? t : null; }
|
|
1195
|
+
var FIND_DECO={ matchBackground:'#3a4a2e', activeMatchBackground:'#4ec9b0', activeMatchColorOverviewRuler:'#4ec9b0', matchOverviewRuler:'#33415580' };
|
|
1196
|
+
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; }
|
|
1197
|
+
try{ if(dir<0) t.search.findPrevious(q,{decorations:FIND_DECO}); else t.search.findNext(q,{decorations:FIND_DECO}); }catch(e){} }
|
|
1198
|
+
function openFind(){ var t=focusedTermTab(); if(!t){ toast('검색할 터미널 페인을 먼저 선택하세요'); return; } $('termFind').hidden=false; var i=$('findInput'); i.focus(); i.select(); if(i.value) runFind(1); }
|
|
1199
|
+
function closeFind(){ $('termFind').hidden=true; var t=focusedTermTab(); if(t){ try{ t.search.clearDecorations(); t.term.focus(); }catch(e){} } }
|
|
1200
|
+
$('findInput').addEventListener('input', function(){ runFind(1); });
|
|
1201
|
+
$('findInput').addEventListener('keydown', function(e){ if(e.key==='Enter'){ e.preventDefault(); runFind(e.shiftKey?-1:1); } else if(e.key==='Escape'){ e.preventDefault(); closeFind(); } });
|
|
1202
|
+
$('findNext').addEventListener('click', function(){ runFind(1); });
|
|
1203
|
+
$('findPrev').addEventListener('click', function(){ runFind(-1); });
|
|
1204
|
+
$('findClose').addEventListener('click', closeFind);
|
|
1205
|
+
document.addEventListener('keydown', function(e){
|
|
1206
|
+
if((e.metaKey||e.ctrlKey) && (e.key==='f'||e.key==='F') && !e.shiftKey && !e.altKey){
|
|
1207
|
+
if(focusedTermTab()){ e.preventDefault(); openFind(); }
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// ── ⌘K 커맨드 팔레트 — 이동(세션·run·프로젝트) + 명령 ──
|
|
1212
|
+
var palItems=[], palSel=0;
|
|
1213
|
+
function paletteItems(){
|
|
1214
|
+
var items=[
|
|
1215
|
+
{k:'cmd', label:'새 작업 세션 열기', run:openSession},
|
|
1216
|
+
{k:'cmd', label:'파일 열기 (뷰어)', run:openFilePicker},
|
|
1217
|
+
{k:'cmd', label:'터미널 검색 (⌘F)', run:openFind},
|
|
1218
|
+
{k:'cmd', label:'세로 분할', run:function(){ splitFocused('row'); }},
|
|
1219
|
+
{k:'cmd', label:'가로 분할', run:function(){ splitFocused('col'); }},
|
|
1220
|
+
{k:'cmd', label:'포커스 페인 닫기', run:function(){ if(focusLeaf) closeSlot(focusLeaf); }},
|
|
1221
|
+
{k:'cmd', label:'포커스 모드 (트리 숨김, ⌘.)', run:toggleFocus},
|
|
1222
|
+
{k:'cmd', label:'페인 최대화 토글 (⌘⏎)', run:function(){ toggleZoom(focusLeaf); }},
|
|
1223
|
+
{k:'cmd', label:'레이아웃 저장 (현재 페인 배치)', run:saveLayout},
|
|
1224
|
+
{k:'cmd', label:'뷰어 / 히스토리', run:openHistory},
|
|
1225
|
+
{k:'cmd', label:'Review 열기 (비교·머지)', run:showReview},
|
|
1226
|
+
{k:'cmd', label:'시크릿 (env 주입)', run:openSecrets},
|
|
1227
|
+
{k:'cmd', label:'보드로 전환', run:function(){ location.href='/'; }}
|
|
1228
|
+
];
|
|
1229
|
+
// 세션 → 페인으로 열기
|
|
1230
|
+
var runs=[]; Object.keys(runById).forEach(function(id){ runs.push(runById[id]); });
|
|
1231
|
+
runs.sort(function(a,b){ return b.id-a.id; });
|
|
1232
|
+
runs.forEach(function(r){
|
|
1233
|
+
var task=taskById[r.taskId]; var repo=task&&repoById[task.repoId];
|
|
1234
|
+
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); }}); }
|
|
1235
|
+
});
|
|
1236
|
+
// run → 페인으로 열기 (최근 40개)
|
|
1237
|
+
var n=0; runs.forEach(function(r){
|
|
1238
|
+
var task=taskById[r.taskId]; var repo=task&&repoById[task.repoId];
|
|
1239
|
+
if(repo && repo.kind==='sessions') return; if(n>=40) return; n++;
|
|
1240
|
+
items.push({k:'run', label:'r'+r.id+' · '+(repo?repo.name:'?')+' / '+((task&&task.title)||''), hint:r.status, run:function(){ openRunPane(r.id); }});
|
|
1241
|
+
});
|
|
1242
|
+
// 프로젝트 → 요청바 대상 지정(팬아웃 준비)
|
|
1243
|
+
Object.keys(repoById).forEach(function(id){ var repo=repoById[id]; if(repo.kind==='sessions') return;
|
|
1244
|
+
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(); } }}); });
|
|
1245
|
+
// 저장된 레이아웃 → 복원 / 삭제
|
|
1246
|
+
var lays=loadLayouts(); var names=Object.keys(lays);
|
|
1247
|
+
names.forEach(function(nm){ items.push({k:'layout', label:'레이아웃 · '+nm, hint:'복원', run:function(){ restoreLayout(nm); }}); });
|
|
1248
|
+
names.forEach(function(nm){ items.push({k:'layout', label:'레이아웃 삭제 · '+nm, hint:'삭제', run:function(){ deleteLayout(nm); }}); });
|
|
1249
|
+
return items;
|
|
1250
|
+
}
|
|
1251
|
+
function renderPalette(q){
|
|
1252
|
+
q=(q||'').trim().toLowerCase();
|
|
1253
|
+
var all=paletteItems();
|
|
1254
|
+
palItems = q ? all.filter(function(it){ return (it.label+' '+(it.hint||'')+' '+it.k).toLowerCase().indexOf(q)>=0; }) : all;
|
|
1255
|
+
palSel=0;
|
|
1256
|
+
if(!palItems.length){ $('palList').innerHTML='<div class="pal-empty">일치 없음</div>'; return; }
|
|
1257
|
+
$('palList').innerHTML = palItems.map(function(it,i){
|
|
1258
|
+
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>';
|
|
1259
|
+
}).join('');
|
|
1260
|
+
}
|
|
1261
|
+
function palMove(d){ if(!palItems.length) return; palSel=(palSel+d+palItems.length)%palItems.length;
|
|
1262
|
+
var rows=$('palList').querySelectorAll('.pal-row'); rows.forEach(function(el,i){ el.classList.toggle('sel', i===palSel); });
|
|
1263
|
+
var sel=rows[palSel]; if(sel) sel.scrollIntoView({block:'nearest'}); }
|
|
1264
|
+
function palRun(i){ var it=palItems[i]; if(!it) return; closePalette(); try{ it.run(); }catch(e){} }
|
|
1265
|
+
function openPalette(){ $('palette').classList.add('on'); var i=$('palInput'); i.value=''; renderPalette(''); i.focus(); }
|
|
1266
|
+
function closePalette(){ $('palette').classList.remove('on'); }
|
|
1267
|
+
$('palInput').addEventListener('input', function(){ renderPalette(this.value); });
|
|
1268
|
+
$('palInput').addEventListener('keydown', function(e){
|
|
1269
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); palMove(1); }
|
|
1270
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); palMove(-1); }
|
|
1271
|
+
else if(e.key==='Enter'){ e.preventDefault(); palRun(palSel); }
|
|
1272
|
+
else if(e.key==='Escape'){ e.preventDefault(); closePalette(); }
|
|
1273
|
+
});
|
|
1274
|
+
$('palList').addEventListener('click', function(e){ var row=e.target.closest('[data-i]'); if(row) palRun(Number(row.dataset.i)); });
|
|
1275
|
+
$('palette').addEventListener('click', function(e){ if(e.target===this) closePalette(); });
|
|
1276
|
+
document.addEventListener('keydown', function(e){
|
|
1277
|
+
if((e.metaKey||e.ctrlKey) && (e.key==='k'||e.key==='K')){ e.preventDefault(); if($('palette').classList.contains('on')) closePalette(); else openPalette(); }
|
|
1278
|
+
});
|
|
1279
|
+
// ── 포커스 모드 — 트리·요청바 숨기고 페인만 ──
|
|
1280
|
+
function toggleFocus(){ var on=$('layout').classList.toggle('focusmode'); $('focusBtn').classList.toggle('on', on); requestAnimationFrame(fitAllVisible); setTimeout(fitAllVisible, 120); }
|
|
1281
|
+
$('focusBtn').addEventListener('click', toggleFocus);
|
|
1282
|
+
|
|
1283
|
+
// ── 페인 단축키: ⌘. 포커스 모드 · ⌘⏎ 최대화 토글 · ⌘1~9 N번째 페인 포커스 ──
|
|
1284
|
+
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'); }
|
|
1285
|
+
document.addEventListener('keydown', function(e){
|
|
1286
|
+
if(!(e.metaKey||e.ctrlKey) || e.altKey) return;
|
|
1287
|
+
if(e.key==='.'){ if(!typingInField(e)){ e.preventDefault(); toggleFocus(); } return; }
|
|
1288
|
+
if(!tabOrder.length || typingInField(e)) return;
|
|
1289
|
+
if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); toggleZoom(focusLeaf); return; }
|
|
1290
|
+
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); } }
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1050
1293
|
// ── 모바일 터미널 입력바 — 조합 완료 텍스트를 통째로 포커스 탭의 PTY 로(IME 안전) ──
|
|
1051
1294
|
function focusedWs(){ var rid=focusedRunId(); return (rid!=null && tabs[rid]) ? tabs[rid].ws : null; }
|
|
1052
1295
|
function termSendRaw(d){ var ws=focusedWs(); if(ws && ws.readyState===1) ws.send(JSON.stringify({t:'i',d:d})); }
|
|
@@ -1262,10 +1505,12 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1262
1505
|
else if (m==='steer'){ var rid=focusRun(); go.textContent='Steer ⏎';
|
|
1263
1506
|
$('reqTgt').textContent = rid?('➤ r'+rid+' 에 후속 지시'):'포커스한 페인 없음';
|
|
1264
1507
|
inp.placeholder = rid?('r'+rid+' 에이전트에게 다음 지시…'):'왼쪽 트리에서 run 을 열어 포커스하세요'; }
|
|
1265
|
-
else { go.textContent='Send ⏎'; var n=tabOrder.length;
|
|
1266
|
-
$('reqTgt').textContent = '⊞ 열린 탭 '+n+'
|
|
1267
|
-
inp.placeholder = n?('
|
|
1508
|
+
else { go.textContent='Send ⏎'; var n=tabOrder.length; var sel=bcastCount();
|
|
1509
|
+
$('reqTgt').textContent = sel ? ('⊞ 선택 '+sel+'개 페인에 전송') : ('⊞ 열린 탭 '+n+'개 전체에 전송');
|
|
1510
|
+
inp.placeholder = n?(sel?('선택한 '+sel+'개에만 그대로 전송'):'전체 '+n+'개 터미널에 전송 — 페인 ◯ 로 대상 선택'):'열린 탭이 없습니다'; }
|
|
1511
|
+
document.body.classList.toggle('bcastmode', m==='bcast');
|
|
1268
1512
|
}
|
|
1513
|
+
function bcastCount(){ var c=0; Object.keys(bcastSel).forEach(function(k){ if(bcastSel[k] && tabs[k] && tabs[k].ws) c++; }); return c; }
|
|
1269
1514
|
Array.prototype.forEach.call(document.querySelectorAll('.mode'), function(b){ b.addEventListener('click', function(){ setMode(b.dataset.mode); $('reqInput').focus(); }); });
|
|
1270
1515
|
|
|
1271
1516
|
var submitting = false;
|
|
@@ -1302,13 +1547,14 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1302
1547
|
else { var j = await res.json().catch(function(){return{};}); toast('steer 불가: '+(j.error||res.status)); }
|
|
1303
1548
|
}catch(e){ toast('steer 실패: '+e); }
|
|
1304
1549
|
finally{ submitting=false; $('reqGo').disabled=false; }
|
|
1305
|
-
} else { // broadcast
|
|
1550
|
+
} else { // broadcast — 선택 페인이 있으면 거기로만, 없으면 열린 탭 전체
|
|
1306
1551
|
if (!tabOrder.length){ toast('열린 탭이 없습니다'); return; }
|
|
1307
1552
|
var payload = text + '\\r';
|
|
1553
|
+
var onlySel = bcastCount()>0;
|
|
1308
1554
|
var sent=0;
|
|
1309
|
-
tabOrder.forEach(function(rid){ var t=tabs[rid]; if (t && t.ws && t.ws.readyState===1){ t.ws.send(JSON.stringify({t:'i',d:payload})); sent++; } });
|
|
1555
|
+
tabOrder.forEach(function(rid){ if(onlySel && !bcastSel[rid]) return; var t=tabs[rid]; if (t && t.ws && t.ws.readyState===1){ t.ws.send(JSON.stringify({t:'i',d:payload})); sent++; } });
|
|
1310
1556
|
inp.value='';
|
|
1311
|
-
toast('브로드캐스트 → '+sent+'개
|
|
1557
|
+
toast('브로드캐스트 → '+sent+'개'+(onlySel?' (선택)':' (전체)'));
|
|
1312
1558
|
}
|
|
1313
1559
|
}
|
|
1314
1560
|
$('reqGo').addEventListener('click', submitReq);
|
|
@@ -1318,6 +1564,8 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1318
1564
|
var reviewOn = false, rvTaskId = null;
|
|
1319
1565
|
function showTerminal(){ reviewOn=false; $('review').classList.remove('on'); $('vtReview').classList.remove('on'); $('vtTerm').classList.add('on'); }
|
|
1320
1566
|
function showReview(){ reviewOn=true; $('review').classList.add('on'); $('vtReview').classList.add('on'); $('vtTerm').classList.remove('on'); renderReviewPicker(); }
|
|
1567
|
+
// 페인 헤더 → 이 run 의 diff 를 Review 로 (그 run 의 태스크 선택 후 compare)
|
|
1568
|
+
function openReviewForRun(runId){ var r=runById[runId]; if(!r){ toast('run 정보 없음'); return; } showReview(); rvTaskId=r.taskId; try{ $('rvTask').value=String(r.taskId); }catch(e){} syncVcmd(); loadCompare(r.taskId); }
|
|
1321
1569
|
$('vtTerm').addEventListener('click', showTerminal);
|
|
1322
1570
|
$('vtReview').addEventListener('click', showReview);
|
|
1323
1571
|
function reviewableTasks(){
|
|
@@ -1339,10 +1587,14 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1339
1587
|
loadCompare(rvTaskId);
|
|
1340
1588
|
}
|
|
1341
1589
|
$('rvTask').addEventListener('change', function(){ rvTaskId=Number(this.value); syncVcmd(); loadCompare(rvTaskId); });
|
|
1590
|
+
$('rvWs').addEventListener('click', function(){ rvShowWs=!rvShowWs; this.classList.toggle('on', rvShowWs); if(rvTaskId!=null) loadCompare(rvTaskId); });
|
|
1591
|
+
var rvShowWs=false;
|
|
1592
|
+
function revealWs(e){ return e.replace(/ /g,'<span class="ws">·</span>').replace(/\\t/g,'<span class="ws">→\\u2003</span>'); }
|
|
1342
1593
|
function diffHTML(text){
|
|
1343
1594
|
if (!text || !text.trim()) return '<span style="color:var(--faint)">no changes</span>';
|
|
1344
1595
|
return text.split('\\n').map(function(l){
|
|
1345
1596
|
var e = esc(l) || ' ';
|
|
1597
|
+
if (rvShowWs && l.indexOf('diff --git')!==0 && l.indexOf('@@')!==0) e = revealWs(e);
|
|
1346
1598
|
if (l.indexOf('diff --git')===0 || l.indexOf('+++')===0 || l.indexOf('---')===0) return '<span class="dl-file">'+e+'</span>';
|
|
1347
1599
|
if (l.indexOf('@@')===0) return '<span class="dl-hunk">'+e+'</span>';
|
|
1348
1600
|
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
|
|