coxpit 5.25.0 → 5.26.1
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/cockpit.ts +83 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.26.1",
|
|
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/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 ── */
|
|
@@ -82,7 +87,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
82
87
|
/* ── 탭 바 + 분할 트리 페인 ── */
|
|
83
88
|
.stage{display:flex;flex-direction:column;min-width:0;background:var(--bg);position:relative}
|
|
84
89
|
/* ── 터미널 스크롤백 검색바(⌘F) ── */
|
|
85
|
-
.term-find{position:absolute;top:44px;right:14px;z-index:30;
|
|
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}
|
|
86
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}
|
|
87
94
|
.term-find input:focus{border-color:var(--brand)}
|
|
88
95
|
.term-find .fc{font-family:var(--mono);font-size:10.5px;color:var(--faint);min-width:20px;text-align:center}
|
|
@@ -240,6 +247,11 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
240
247
|
.tnode.run .ract{margin-left:6px;font-size:10px;color:var(--brand);opacity:.85}
|
|
241
248
|
.leaf-h .zoom{color:var(--faint);border:none;background:none;cursor:pointer;font-size:12px;padding:0 2px}
|
|
242
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)}
|
|
243
255
|
.leaf-h .lock{color:var(--faint);border:none;background:none;cursor:pointer;font-size:12px;padding:0 2px}
|
|
244
256
|
.leaf-h .lock:hover{color:var(--brand)}
|
|
245
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}
|
|
@@ -367,7 +379,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
367
379
|
</header>
|
|
368
380
|
|
|
369
381
|
<div class="scrim" id="scrim"></div>
|
|
370
|
-
<div class="layout">
|
|
382
|
+
<div class="layout" id="layout">
|
|
371
383
|
<aside class="rail" id="rail">
|
|
372
384
|
<div class="lbl"><span>Workspace</span><span id="machName" style="color:var(--faint)">local</span></div>
|
|
373
385
|
<div id="tree"></div>
|
|
@@ -382,6 +394,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
382
394
|
<button class="tc-btn" id="splitCol" title="가로 분할 — 포커스 페인을 상하로" disabled>▬ split</button>
|
|
383
395
|
<button class="tc-btn session" id="sessionBtn" title="자유 세션(폴더 지정 터미널) 열기">+<span class="b-txt"> Session</span></button>
|
|
384
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>
|
|
385
398
|
<button class="tc-btn" id="closeBtn" title="포커스 페인 닫기(탭은 유지)" disabled>×<span class="b-txt"> pane</span></button>
|
|
386
399
|
</div>
|
|
387
400
|
</div>
|
|
@@ -564,6 +577,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
564
577
|
var fold = {}; // 접힘 상태(repo/goal/task 노드)
|
|
565
578
|
function isFold(k){ return fold[k]===true; }
|
|
566
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'); }
|
|
567
581
|
|
|
568
582
|
async function hydrate(){
|
|
569
583
|
try{
|
|
@@ -686,6 +700,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
686
700
|
var layout = { leaf:true, id:'L0', tab:null }; // 분할 트리 루트
|
|
687
701
|
var focusLeaf = 'L0';
|
|
688
702
|
var zoomLeaf = null; // 페인 최대화(줌) — 설정되면 그 리프만 전체 렌더
|
|
703
|
+
var bcastSel = {}; // 브로드캐스트 선택 대상(tab id → true). 비어있으면 전체.
|
|
689
704
|
var leafSeq = 1;
|
|
690
705
|
var viewerSeq = 1; // 뷰어 탭 키(문자열 'v#')
|
|
691
706
|
var MAX_LEAVES = 6;
|
|
@@ -761,7 +776,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
761
776
|
if(ev.type==='keydown' && (ev.metaKey||ev.ctrlKey)){
|
|
762
777
|
if(ev.key==='c'||ev.key==='C'){ if(term.hasSelection()){ copySel(); return false; } }
|
|
763
778
|
// 전역 단축키(⌘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;
|
|
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;
|
|
765
780
|
}
|
|
766
781
|
return true;
|
|
767
782
|
});
|
|
@@ -930,6 +945,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
930
945
|
+ '<span data-role="act" class="pane-act">'+esc(latestActivity(node.tab))+'</span>'
|
|
931
946
|
+ '<span data-role="chip" class="chip '+esc(r?r.status:'')+'">'+esc(r?r.status:'')+'</span>'
|
|
932
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>'
|
|
933
951
|
+ '<button class="lock" data-lock="'+node.id+'" title="이 페인에 시크릿/비밀번호 전송(터미널에 안 찍힘)">⊟</button>'
|
|
934
952
|
+ zoomBtn + '<button class="x" title="이 페인 닫기(탭은 유지)">×</button>')
|
|
935
953
|
: '<span class="nm" style="color:var(--faint)">빈 페인</span><button class="x" title="이 페인 닫기">×</button>';
|
|
@@ -988,6 +1006,28 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
988
1006
|
// 페인 순서(트리 순회) → ⌘1..9 로 N번째 포커스
|
|
989
1007
|
function leafOrder(){ var out=[]; eachLeaf(layout,function(l){ out.push(l); }); return out; }
|
|
990
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); }
|
|
1030
|
+
|
|
991
1031
|
// 탭 열기 = 포커스 슬롯에 표시(강제 분할 없음). 기존 호출부(openRunPane) 호환.
|
|
992
1032
|
function openTab(runId){
|
|
993
1033
|
zoomLeaf=null; // 새 탭은 보여야 하므로 줌 해제
|
|
@@ -1105,6 +1145,14 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1105
1145
|
$('panes').addEventListener('click', function(e){
|
|
1106
1146
|
var zoomBtn=e.target.closest('[data-zoom]');
|
|
1107
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; }
|
|
1108
1156
|
var lockBtn=e.target.closest('[data-lock]');
|
|
1109
1157
|
if (lockBtn){ e.stopPropagation(); startSecretSend(lockBtn.getAttribute('data-lock'), lockBtn); return; }
|
|
1110
1158
|
var leaf=e.target.closest('[data-leaf]'); if(!leaf) return;
|
|
@@ -1170,6 +1218,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1170
1218
|
{k:'cmd', label:'세로 분할', run:function(){ splitFocused('row'); }},
|
|
1171
1219
|
{k:'cmd', label:'가로 분할', run:function(){ splitFocused('col'); }},
|
|
1172
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},
|
|
1173
1224
|
{k:'cmd', label:'뷰어 / 히스토리', run:openHistory},
|
|
1174
1225
|
{k:'cmd', label:'Review 열기 (비교·머지)', run:showReview},
|
|
1175
1226
|
{k:'cmd', label:'시크릿 (env 주입)', run:openSecrets},
|
|
@@ -1191,6 +1242,10 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1191
1242
|
// 프로젝트 → 요청바 대상 지정(팬아웃 준비)
|
|
1192
1243
|
Object.keys(repoById).forEach(function(id){ var repo=repoById[id]; if(repo.kind==='sessions') return;
|
|
1193
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); }}); });
|
|
1194
1249
|
return items;
|
|
1195
1250
|
}
|
|
1196
1251
|
function renderPalette(q){
|
|
@@ -1221,10 +1276,15 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1221
1276
|
document.addEventListener('keydown', function(e){
|
|
1222
1277
|
if((e.metaKey||e.ctrlKey) && (e.key==='k'||e.key==='K')){ e.preventDefault(); if($('palette').classList.contains('on')) closePalette(); else openPalette(); }
|
|
1223
1278
|
});
|
|
1224
|
-
// ──
|
|
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번째 페인 포커스 ──
|
|
1225
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'); }
|
|
1226
1285
|
document.addEventListener('keydown', function(e){
|
|
1227
1286
|
if(!(e.metaKey||e.ctrlKey) || e.altKey) return;
|
|
1287
|
+
if(e.key==='.'){ if(!typingInField(e)){ e.preventDefault(); toggleFocus(); } return; }
|
|
1228
1288
|
if(!tabOrder.length || typingInField(e)) return;
|
|
1229
1289
|
if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); toggleZoom(focusLeaf); return; }
|
|
1230
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); } }
|
|
@@ -1301,8 +1361,11 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1301
1361
|
function openSession(){ $('pickModal').classList.add('on'); $('pickName').value=''; browseTo(''); }
|
|
1302
1362
|
function closePicker(){ $('pickModal').classList.remove('on'); }
|
|
1303
1363
|
async function browseTo(p){
|
|
1364
|
+
$('pickList').innerHTML = '<div class="pick-row" style="cursor:default;color:var(--faint)">불러오는 중…</div>';
|
|
1304
1365
|
try{
|
|
1305
|
-
var
|
|
1366
|
+
var res = await fetch('/api/browse'+(p?('?path='+encodeURIComponent(p)):''));
|
|
1367
|
+
if(!res.ok){ $('pickList').innerHTML = '<div class="pick-row" style="color:var(--failed)">'+(res.status===401?'인증이 만료됐어요 — 새로고침 후 다시 로그인':'폴더를 읽을 수 없습니다 (HTTP '+res.status+')')+'</div>'; return; }
|
|
1368
|
+
var d = await res.json();
|
|
1306
1369
|
pickPathCur = d.path; $('pickPath').textContent = d.path;
|
|
1307
1370
|
var html = '';
|
|
1308
1371
|
if (d.parent && d.parent!==d.path) html += '<div class="pick-row up" data-go="'+esc(d.parent)+'"><span class="ic">↑</span><span>..</span></div>';
|
|
@@ -1342,8 +1405,11 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1342
1405
|
function closeFilePicker(){ $('fpickModal').classList.remove('on'); }
|
|
1343
1406
|
function fpIcon(kind){ return kind==='md'?'M':kind==='html'?'H':kind==='pdf'?'P':kind==='image'?'I':kind==='binary'?'·':'T'; }
|
|
1344
1407
|
async function fpTo(p){
|
|
1408
|
+
$('fpList').innerHTML = '<div class="pick-row" style="cursor:default;color:var(--faint)">불러오는 중…</div>';
|
|
1345
1409
|
try{
|
|
1346
|
-
var
|
|
1410
|
+
var res = await fetch('/api/fs/list'+(p?('?path='+encodeURIComponent(p)):''));
|
|
1411
|
+
if(!res.ok){ $('fpList').innerHTML = '<div class="pick-row" style="color:var(--failed)">'+(res.status===401?'인증이 만료됐어요 — 새로고침':'읽을 수 없습니다 (HTTP '+res.status+')')+'</div>'; return; }
|
|
1412
|
+
var d = await res.json();
|
|
1347
1413
|
fpDirCur = d.path; $('fpPath').textContent = d.path; if($('fpHint')) $('fpHint').textContent='폴더=이동 · 파일=뷰어로 열기';
|
|
1348
1414
|
var html='';
|
|
1349
1415
|
if (d.parent && d.parent!==d.path) html += '<div class="pick-row up" data-dir="'+esc(d.parent)+'"><span class="ic">↑</span><span>..</span></div>';
|
|
@@ -1445,10 +1511,12 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1445
1511
|
else if (m==='steer'){ var rid=focusRun(); go.textContent='Steer ⏎';
|
|
1446
1512
|
$('reqTgt').textContent = rid?('➤ r'+rid+' 에 후속 지시'):'포커스한 페인 없음';
|
|
1447
1513
|
inp.placeholder = rid?('r'+rid+' 에이전트에게 다음 지시…'):'왼쪽 트리에서 run 을 열어 포커스하세요'; }
|
|
1448
|
-
else { go.textContent='Send ⏎'; var n=tabOrder.length;
|
|
1449
|
-
$('reqTgt').textContent = '⊞ 열린 탭 '+n+'
|
|
1450
|
-
inp.placeholder = n?('
|
|
1514
|
+
else { go.textContent='Send ⏎'; var n=tabOrder.length; var sel=bcastCount();
|
|
1515
|
+
$('reqTgt').textContent = sel ? ('⊞ 선택 '+sel+'개 페인에 전송') : ('⊞ 열린 탭 '+n+'개 전체에 전송');
|
|
1516
|
+
inp.placeholder = n?(sel?('선택한 '+sel+'개에만 그대로 전송'):'전체 '+n+'개 터미널에 전송 — 페인 ◯ 로 대상 선택'):'열린 탭이 없습니다'; }
|
|
1517
|
+
document.body.classList.toggle('bcastmode', m==='bcast');
|
|
1451
1518
|
}
|
|
1519
|
+
function bcastCount(){ var c=0; Object.keys(bcastSel).forEach(function(k){ if(bcastSel[k] && tabs[k] && tabs[k].ws) c++; }); return c; }
|
|
1452
1520
|
Array.prototype.forEach.call(document.querySelectorAll('.mode'), function(b){ b.addEventListener('click', function(){ setMode(b.dataset.mode); $('reqInput').focus(); }); });
|
|
1453
1521
|
|
|
1454
1522
|
var submitting = false;
|
|
@@ -1485,13 +1553,14 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1485
1553
|
else { var j = await res.json().catch(function(){return{};}); toast('steer 불가: '+(j.error||res.status)); }
|
|
1486
1554
|
}catch(e){ toast('steer 실패: '+e); }
|
|
1487
1555
|
finally{ submitting=false; $('reqGo').disabled=false; }
|
|
1488
|
-
} else { // broadcast
|
|
1556
|
+
} else { // broadcast — 선택 페인이 있으면 거기로만, 없으면 열린 탭 전체
|
|
1489
1557
|
if (!tabOrder.length){ toast('열린 탭이 없습니다'); return; }
|
|
1490
1558
|
var payload = text + '\\r';
|
|
1559
|
+
var onlySel = bcastCount()>0;
|
|
1491
1560
|
var sent=0;
|
|
1492
|
-
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++; } });
|
|
1561
|
+
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++; } });
|
|
1493
1562
|
inp.value='';
|
|
1494
|
-
toast('브로드캐스트 → '+sent+'개
|
|
1563
|
+
toast('브로드캐스트 → '+sent+'개'+(onlySel?' (선택)':' (전체)'));
|
|
1495
1564
|
}
|
|
1496
1565
|
}
|
|
1497
1566
|
$('reqGo').addEventListener('click', submitReq);
|
|
@@ -1501,6 +1570,8 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
1501
1570
|
var reviewOn = false, rvTaskId = null;
|
|
1502
1571
|
function showTerminal(){ reviewOn=false; $('review').classList.remove('on'); $('vtReview').classList.remove('on'); $('vtTerm').classList.add('on'); }
|
|
1503
1572
|
function showReview(){ reviewOn=true; $('review').classList.add('on'); $('vtReview').classList.add('on'); $('vtTerm').classList.remove('on'); renderReviewPicker(); }
|
|
1573
|
+
// 페인 헤더 → 이 run 의 diff 를 Review 로 (그 run 의 태스크 선택 후 compare)
|
|
1574
|
+
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); }
|
|
1504
1575
|
$('vtTerm').addEventListener('click', showTerminal);
|
|
1505
1576
|
$('vtReview').addEventListener('click', showReview);
|
|
1506
1577
|
function reviewableTasks(){
|