coxpit 5.11.0 → 5.12.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/LICENSE.md +717 -0
- package/README.md +5 -1
- package/package.json +2 -2
- package/src/cockpit.ts +333 -144
- package/src/server.ts +14 -0
- package/LICENSE +0 -21
package/src/cockpit.ts
CHANGED
|
@@ -74,36 +74,57 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
74
74
|
.tree-sep{height:1px;background:var(--line);margin:8px 4px}
|
|
75
75
|
.railfoot{margin-top:auto;padding:8px;color:var(--faint);font-size:11px;border-top:1px solid var(--line)}
|
|
76
76
|
|
|
77
|
-
/* ──
|
|
78
|
-
.stage{display:flex;flex-direction:column;min-width:0;background:var(--bg)}
|
|
79
|
-
.
|
|
80
|
-
.
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
.
|
|
88
|
-
.
|
|
89
|
-
.
|
|
90
|
-
.
|
|
77
|
+
/* ── 탭 바 + 분할 트리 페인 ── */
|
|
78
|
+
.stage{display:flex;flex-direction:column;min-width:0;background:var(--bg);position:relative}
|
|
79
|
+
.tabbar{display:flex;align-items:stretch;height:38px;border-bottom:1px solid var(--line);background:var(--panel);font-family:var(--mono)}
|
|
80
|
+
.tabs{flex:1;display:flex;align-items:stretch;overflow-x:auto;overflow-y:hidden}
|
|
81
|
+
.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}
|
|
82
|
+
.tab:hover{background:var(--surface)}
|
|
83
|
+
.tab.shown{color:var(--ink);background:var(--bg);box-shadow:inset 0 -2px 0 var(--brand)}
|
|
84
|
+
.tab.drag{opacity:.4}
|
|
85
|
+
.tab .st{flex:none}
|
|
86
|
+
.tab .nm{overflow:hidden;text-overflow:ellipsis}
|
|
87
|
+
.tab .ren{font:inherit;color:var(--ink);background:var(--panel);border:1px solid var(--brand);border-radius:4px;padding:0 4px;width:120px;outline:none}
|
|
88
|
+
.tab .x{color:var(--faint);font-size:13px;padding:0 2px;border:none;background:none;cursor:pointer;line-height:1}
|
|
89
|
+
.tab .x:hover{color:var(--failed)}
|
|
90
|
+
.tabctl{display:flex;align-items:center;gap:6px;padding:0 10px;border-left:1px solid var(--line)}
|
|
91
|
+
.tc-btn{background:none;border:1px solid var(--line);border-radius:6px;color:var(--muted);font-size:11px;padding:3px 8px;cursor:pointer;font-family:var(--mono)}
|
|
92
|
+
.tc-btn:hover:not([disabled]){color:var(--ink);border-color:var(--line-hi)}
|
|
93
|
+
.tc-btn[disabled]{opacity:.4;cursor:default}
|
|
94
|
+
.tc-btn.session{color:var(--brand);border-color:rgba(78,201,176,.35)}
|
|
95
|
+
.tc-btn.session:hover:not([disabled]){background:var(--brand-dim);border-color:var(--brand)}
|
|
96
|
+
|
|
97
|
+
.panes{flex:1;display:none;min-height:0;min-width:0} /* 초기 숨김 — 탭 있을 때만 flex */
|
|
98
|
+
.node{display:flex;min-width:0;min-height:0;flex:1 1 0}
|
|
99
|
+
.node.row{flex-direction:row} .node.col{flex-direction:column}
|
|
100
|
+
.gutter{flex:none;background:var(--line);z-index:2}
|
|
101
|
+
.gutter.row{width:5px;cursor:col-resize} .gutter.col{height:5px;cursor:row-resize}
|
|
102
|
+
.gutter:hover,.gutter.drag{background:var(--brand)}
|
|
103
|
+
.leaf{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;flex:1 1 0;background:var(--bg)}
|
|
104
|
+
.leaf.focus{box-shadow:inset 0 0 0 1px rgba(78,201,176,.45)}
|
|
105
|
+
.leaf.drop{box-shadow:inset 0 0 0 2px var(--brand)}
|
|
106
|
+
.leaf-h{display:flex;align-items:center;gap:8px;height:26px;padding:0 6px 0 10px;background:var(--surface);border-bottom:1px solid var(--line);font-family:var(--mono);font-size:11px;color:var(--muted);flex:none;cursor:pointer}
|
|
107
|
+
.leaf.focus .leaf-h{background:var(--brand-dim)}
|
|
108
|
+
.leaf-h .nm{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--ink)}
|
|
109
|
+
.leaf-h .x{color:var(--faint);border:none;background:none;cursor:pointer;font-size:13px}
|
|
110
|
+
.leaf-h .x:hover{color:var(--failed)}
|
|
111
|
+
.chip{font-size:9px;text-transform:uppercase;letter-spacing:.04em;padding:1px 6px;border-radius:999px}
|
|
91
112
|
.chip.running{color:var(--running);background:rgba(85,167,224,.14)}
|
|
92
113
|
.chip.done,.chip.merged{color:var(--done);background:rgba(88,179,104,.14)}
|
|
93
114
|
.chip.blocked,.chip.preparing,.chip.pending,.chip.starting{color:var(--blocked);background:rgba(214,162,73,.16)}
|
|
94
115
|
.chip.failed,.chip.error{color:var(--failed);background:rgba(226,91,103,.14)}
|
|
95
116
|
.chip.open{color:var(--open);background:rgba(127,156,245,.14)}
|
|
96
117
|
.chip.stopped{color:var(--stopped);background:rgba(181,139,224,.14)}
|
|
97
|
-
.pane-h .title{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--faint)}
|
|
98
|
-
.pane-h .x{color:var(--faint);border:none;background:none;cursor:pointer;font-size:13px;padding:0 3px}
|
|
99
|
-
.pane-h .x:hover{color:var(--failed)}
|
|
100
118
|
.vbadge{font-size:9px;font-weight:600;letter-spacing:.03em;padding:1px 6px;border-radius:999px;white-space:nowrap}
|
|
101
119
|
.vbadge.pass{color:var(--done);background:rgba(88,179,104,.16)}
|
|
102
120
|
.vbadge.fail{color:var(--failed);background:rgba(226,91,103,.16)}
|
|
103
121
|
.vbadge.running{color:var(--blocked);background:rgba(214,162,73,.16)}
|
|
104
122
|
.vbadge.error{color:var(--failed);background:rgba(226,91,103,.12)}
|
|
105
|
-
.
|
|
106
|
-
.
|
|
123
|
+
.leaf-body{flex:1;min-height:0;min-width:0;display:flex}
|
|
124
|
+
.leaf-body.drop{box-shadow:inset 0 0 0 2px var(--brand);background:var(--brand-dim)}
|
|
125
|
+
.leaf-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--faint);font-family:var(--mono);font-size:11.5px;text-align:center;padding:12px}
|
|
126
|
+
.term-host{flex:1;min-height:0;min-width:0;padding:4px 2px 2px 8px}
|
|
127
|
+
.term-host .xterm{height:100%}
|
|
107
128
|
|
|
108
129
|
.empty{flex:1;display:flex;align-items:center;justify-content:center;text-align:center;padding:24px}
|
|
109
130
|
.empty .card{max-width:440px}
|
|
@@ -152,6 +173,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
152
173
|
.pick-f .go{margin-left:auto;font-family:var(--mono);font-size:12px;font-weight:600;color:var(--brand-ink);background:var(--brand);border:none;border-radius:8px;padding:9px 15px;cursor:pointer}
|
|
153
174
|
.pick-f .home{font-family:var(--mono);font-size:11px;color:var(--muted);background:none;border:1px solid var(--line);border-radius:7px;padding:7px 11px;cursor:pointer}
|
|
154
175
|
.pick-f .home:hover{color:var(--ink);border-color:var(--line-hi)}
|
|
176
|
+
.pick-name{flex:1;font-family:var(--mono);font-size:12px;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:7px 10px}
|
|
177
|
+
.pick-name:focus{outline:none;border-color:var(--brand)}
|
|
178
|
+
.pick-name::placeholder{color:var(--faint)}
|
|
155
179
|
|
|
156
180
|
.lbl .lnk{color:var(--brand);cursor:pointer;font-size:10px;letter-spacing:0;text-transform:none}
|
|
157
181
|
.tnode.session{padding-left:20px;cursor:pointer} .tnode.session:hover{background:var(--surface)}
|
|
@@ -211,23 +235,25 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
211
235
|
<aside class="rail">
|
|
212
236
|
<div class="lbl"><span>Workspace</span><span id="machName" style="color:var(--faint)">local</span></div>
|
|
213
237
|
<div id="tree"></div>
|
|
214
|
-
<div class="railfoot">클릭한 run
|
|
238
|
+
<div class="railfoot">클릭한 run·세션은 <b>탭</b>으로 열립니다 · split 으로 페인을 나란히 배치</div>
|
|
215
239
|
</aside>
|
|
216
240
|
|
|
217
241
|
<main class="stage">
|
|
218
|
-
<div class="
|
|
219
|
-
<
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
242
|
+
<div class="tabbar">
|
|
243
|
+
<div class="tabs" id="tabs"></div>
|
|
244
|
+
<div class="tabctl">
|
|
245
|
+
<button class="tc-btn" id="splitRow" title="세로 분할 — 포커스 페인을 좌우로" disabled>▐ split</button>
|
|
246
|
+
<button class="tc-btn" id="splitCol" title="가로 분할 — 포커스 페인을 상하로" disabled>▬ split</button>
|
|
247
|
+
<button class="tc-btn session" id="sessionBtn" title="자유 세션(폴더 지정 터미널) 열기">+ Session</button>
|
|
248
|
+
<button class="tc-btn" id="closeBtn" title="포커스 페인 닫기(탭은 유지)" disabled>× pane</button>
|
|
249
|
+
</div>
|
|
224
250
|
</div>
|
|
225
251
|
<div class="panes" id="panes"></div>
|
|
226
252
|
<div class="empty" id="empty">
|
|
227
253
|
<div class="card">
|
|
228
254
|
<div class="glyph">⌗ ⌗ ⌗</div>
|
|
229
255
|
<h1>여기서 작업을 시작하세요</h1>
|
|
230
|
-
<p>직접 몰고 갈 <b>작업 세션</b>(자유 터미널)을 열거나, 아래 요청바로 에이전트를 팬아웃하세요. 트리의 <b>run</b> 을 클릭해도
|
|
256
|
+
<p>직접 몰고 갈 <b>작업 세션</b>(자유 터미널)을 열거나, 아래 요청바로 에이전트를 팬아웃하세요. 트리의 <b>run</b> 을 클릭해도 <b>탭</b>으로 열립니다.</p>
|
|
231
257
|
<button class="cta" id="sessionCta">+ 새 작업 세션 열기</button>
|
|
232
258
|
<div class="hint">세션 = <b>지정한 폴더</b>의 tmux 셸(특정 프로젝트에 소속되지 않음). 그 안에서 <code>claude</code> 를 띄워 “이 프로젝트 구현해줘” 처럼 직접 지시할 수 있습니다.</div>
|
|
233
259
|
</div>
|
|
@@ -275,7 +301,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
275
301
|
<div class="pick-list" id="pickList"></div>
|
|
276
302
|
<div class="pick-f">
|
|
277
303
|
<button class="home" id="pickHome" title="홈으로">⌂ home</button>
|
|
278
|
-
<
|
|
304
|
+
<input class="pick-name" id="pickName" placeholder="세션 이름 (선택 — 비우면 폴더명)" autocomplete="off" />
|
|
279
305
|
<button class="go" id="pickGo">여기서 열기</button>
|
|
280
306
|
</div>
|
|
281
307
|
</div>
|
|
@@ -328,7 +354,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
328
354
|
var repos = (fleet.repos||[]).filter(function(r){ return r.kind!=='sessions'; });
|
|
329
355
|
fillSelect($('reqRepo'), repos, function(r){return String(r.id);}, function(r){return r.name;}, true);
|
|
330
356
|
// repo 미선택 상태면 포커스 페인의 repo 로 기본
|
|
331
|
-
if (
|
|
357
|
+
var fr=(typeof focusedRunId==='function')?focusedRunId():null; if (fr!=null){ var rp = repoOfRun(fr); if (rp!=null) $('reqRepo').value=String(rp); }
|
|
332
358
|
var provs = fleet.providers||[];
|
|
333
359
|
if (provs.length) fillSelect($('reqAgent'), provs, function(p){return p.id;}, function(p){return p.label||p.id;}, true);
|
|
334
360
|
else if (!$('reqAgent').options.length){ var o=document.createElement('option'); o.value='claude-code'; o.textContent='claude-code'; $('reqAgent').appendChild(o); }
|
|
@@ -353,7 +379,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
353
379
|
html += '<div class="lbl"><span>Sessions</span><span class="lnk" data-newsession="1">+ 새 세션</span></div>';
|
|
354
380
|
if (sessRuns.length){
|
|
355
381
|
sessRuns.forEach(function(s){
|
|
356
|
-
var r=s.run; var open =
|
|
382
|
+
var r=s.run; var open = tabs[r.id] ? ' open' : '';
|
|
357
383
|
html += '<div class="tnode session'+open+'" data-run="'+r.id+'"><span class="st '+esc(r.status)+'"></span>'
|
|
358
384
|
+ '<span class="n">'+esc(s.title||'session')+'</span><span class="p">'+esc((r.worktreePath||'').replace(/^.*\\/([^/]+\\/[^/]+)$/,'…/$1'))+'</span></div>';
|
|
359
385
|
});
|
|
@@ -389,7 +415,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
389
415
|
var s = '<div class="tnode task" data-fold="'+tk+'"><span class="car">'+(rns.length?(isFold(tk)?'▸':'▾'):' ')+'</span>'
|
|
390
416
|
+ '<span class="n">'+esc(t.title)+'</span></div>';
|
|
391
417
|
if (!isFold(tk)) rns.sort(function(a,b){return a.id-b.id;}).forEach(function(r){
|
|
392
|
-
var open =
|
|
418
|
+
var open = tabs[r.id] ? ' open' : '';
|
|
393
419
|
s += '<div class="tnode run'+open+'" data-run="'+r.id+'"><span class="st '+esc(r.status)+'"></span>'
|
|
394
420
|
+ '<span class="n">r'+r.id+' · '+esc(r.status)+'</span></div>';
|
|
395
421
|
});
|
|
@@ -402,128 +428,290 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
402
428
|
var fn = e.target.closest('[data-fold]');
|
|
403
429
|
if (fn){ var k = fn.dataset.fold; fold[k] = !isFold(k); renderTree(); }
|
|
404
430
|
});
|
|
431
|
+
// 트리 세션 행 더블클릭 → 이름 변경
|
|
432
|
+
$('tree').addEventListener('dblclick', function(e){
|
|
433
|
+
var s = e.target.closest('.tnode.session[data-run]'); if(!s) return;
|
|
434
|
+
var runId=+s.dataset.run; var r=runById[runId]; if(!r) return;
|
|
435
|
+
var cur=(taskById[r.taskId]||{}).title||''; var v=prompt('세션 이름', cur);
|
|
436
|
+
if(v!=null && v.trim()) renameTask(r.taskId, v.trim());
|
|
437
|
+
});
|
|
405
438
|
|
|
406
|
-
// ──
|
|
407
|
-
|
|
408
|
-
var
|
|
409
|
-
var
|
|
410
|
-
var
|
|
411
|
-
var
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
function setFocus(id){
|
|
433
|
-
focusId = id;
|
|
434
|
-
panes.forEach(function(p){ p.el.classList.toggle('focus', p.id===id); });
|
|
435
|
-
$('closeBtn').disabled = !focusId;
|
|
436
|
-
var p = panes.find(function(x){return x.id===id;});
|
|
437
|
-
if (p && p.term) try{ p.term.focus(); }catch(e){}
|
|
438
|
-
if (typeof reqMode!=='undefined' && reqMode!=='new') setMode(reqMode);
|
|
439
|
+
// ── 탭 + 분할 트리 페인 ──
|
|
440
|
+
// 탭 = 열린 세션/run. 탭이 xterm·WS 를 소유(안 보여도 살아있음). 슬롯(leaf)에 배치돼 표시된다.
|
|
441
|
+
var tabs = {}; // runId -> { runId, name, term, fit, ws, retry, closing, ro, host }
|
|
442
|
+
var tabOrder = []; // 탭 바 순서(runId)
|
|
443
|
+
var layout = { leaf:true, id:'L0', tab:null }; // 분할 트리 루트
|
|
444
|
+
var focusLeaf = 'L0';
|
|
445
|
+
var leafSeq = 1;
|
|
446
|
+
var MAX_LEAVES = 6;
|
|
447
|
+
var dragRunId = null;
|
|
448
|
+
|
|
449
|
+
function newLeafId(){ return 'L'+(leafSeq++); }
|
|
450
|
+
function eachLeaf(node, fn){ if(node.leaf){ fn(node); } else { eachLeaf(node.a,fn); eachLeaf(node.b,fn); } }
|
|
451
|
+
function findLeaf(id, node){ node=node||layout; if(node.leaf) return node.id===id?node:null; return findLeaf(id,node.a)||findLeaf(id,node.b); }
|
|
452
|
+
function findSplit(id, node){ node=node||layout; if(node.leaf) return null; if(node.id===id) return node; return findSplit(id,node.a)||findSplit(id,node.b); }
|
|
453
|
+
function leafOfTab(runId){ var f=null; eachLeaf(layout,function(l){ if(l.tab===runId) f=l; }); return f; }
|
|
454
|
+
function firstLeaf(){ var f=null; eachLeaf(layout,function(l){ if(!f) f=l; }); return f; }
|
|
455
|
+
function countLeaves(){ var n=0; eachLeaf(layout,function(){n++;}); return n; }
|
|
456
|
+
function focusedRunId(){ var l=findLeaf(focusLeaf); return l?l.tab:null; }
|
|
457
|
+
function focusRun(){ return focusedRunId(); } // 기존 호출부(steer/review) 호환
|
|
458
|
+
function copyInto(dst,src){ Object.keys(dst).forEach(function(k){delete dst[k];}); Object.keys(src).forEach(function(k){dst[k]=src[k];}); }
|
|
459
|
+
|
|
460
|
+
function tabName(runId){
|
|
461
|
+
var r=runById[runId]; if(!r) return 'r'+runId;
|
|
462
|
+
var task=taskById[r.taskId]; var rp=task&&repoById[task.repoId];
|
|
463
|
+
if (rp && rp.kind==='sessions') return (task&&task.title)||('session r'+runId);
|
|
464
|
+
return 'r'+runId;
|
|
439
465
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
var
|
|
445
|
-
var p = { id: ++paneSeq, runId: runId, retry: 0, closing: false };
|
|
446
|
-
var el = document.createElement('div'); el.className = 'pane'; p.el = el;
|
|
447
|
-
el.innerHTML = '<div class="pane-h"><span class="rid">r'+runId+'</span>'
|
|
448
|
-
+ '<span class="chip '+esc(r?r.status:'')+'" data-role="chip">'+esc(r?r.status:'')+'</span>'
|
|
449
|
-
+ '<span data-role="vslot">'+vbadge(r&&r.verifyStatus)+'</span>'
|
|
450
|
-
+ '<span class="title" data-role="title">'+esc(r&&r.tmuxWindow||'terminal')+'</span>'
|
|
451
|
-
+ '<button class="x" title="close">×</button></div>'
|
|
452
|
-
+ '<div class="pane-term"></div>';
|
|
453
|
-
$('panes').appendChild(el);
|
|
454
|
-
el.querySelector('.pane-h').addEventListener('click', function(ev){
|
|
455
|
-
if (ev.target.closest('.x')) return; setFocus(p.id);
|
|
456
|
-
});
|
|
457
|
-
el.querySelector('.x').addEventListener('click', function(){ closePane(p.id); });
|
|
458
|
-
// xterm
|
|
459
|
-
var term = new window.Terminal({
|
|
466
|
+
|
|
467
|
+
function ensureTab(runId){
|
|
468
|
+
if (tabs[runId]) return tabs[runId];
|
|
469
|
+
var host=document.createElement('div'); host.className='term-host';
|
|
470
|
+
var term=new window.Terminal({
|
|
460
471
|
fontFamily: "ui-monospace, 'SF Mono', Menlo, Monaco, 'Apple SD Gothic Neo', 'Noto Sans KR', monospace",
|
|
461
472
|
fontSize: 12, cursorBlink: true, allowProposedApi: true, scrollback: 4000,
|
|
462
473
|
theme: { background:'#0b0d12', foreground:'#dee4ec', cursor:'#4ec9b0', selectionBackground:'rgba(78,201,176,.25)', black:'#1c212c', brightBlack:'#5c6675' },
|
|
463
474
|
});
|
|
464
|
-
|
|
465
|
-
p.fit = new window.FitAddon.FitAddon(); term.loadAddon(p.fit);
|
|
475
|
+
var fit=new window.FitAddon.FitAddon(); term.loadAddon(fit);
|
|
466
476
|
try{ term.loadAddon(new window.Unicode11Addon.Unicode11Addon()); term.unicode.activeVersion='11'; }catch(e){}
|
|
467
|
-
term.open(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
function connectPane(p){
|
|
477
|
-
if (p.closing || !p.term) return;
|
|
478
|
-
try{ p.fit.fit(); }catch(e){}
|
|
477
|
+
// term.open 은 host 가 DOM 에 붙은 뒤(attachHosts) 최초 1회 — detached 에서 open 하면 렌더러가 안 뜬다.
|
|
478
|
+
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 };
|
|
479
|
+
term.onData(function(d){ if(t.ws&&t.ws.readyState===1) t.ws.send(JSON.stringify({t:'i',d:d})); });
|
|
480
|
+
tabs[runId]=t; tabOrder.push(runId);
|
|
481
|
+
return t; // open·connect 는 attachHosts 에서(Phase 2 순서: open → connect)
|
|
482
|
+
}
|
|
483
|
+
function connectTab(t){
|
|
484
|
+
if (t.closing || !t.term) return;
|
|
485
|
+
try{ t.fit.fit(); }catch(e){}
|
|
479
486
|
var proto = location.protocol==='https:'?'wss':'ws';
|
|
480
|
-
var sock = new WebSocket(proto+'://'+location.host+'/ws/term/'+
|
|
481
|
-
|
|
482
|
-
sock.onopen = function(){ if (sock!==
|
|
487
|
+
var sock = new WebSocket(proto+'://'+location.host+'/ws/term/'+t.runId+'?cols='+t.term.cols+'&rows='+t.term.rows);
|
|
488
|
+
t.ws = sock;
|
|
489
|
+
sock.onopen = function(){ if (sock!==t.ws){ try{sock.close();}catch(e){} return; } t.retry=0; };
|
|
483
490
|
sock.onmessage = function(m){
|
|
484
|
-
if (sock!==
|
|
491
|
+
if (sock!==t.ws || !t.term) return;
|
|
485
492
|
try{ var d = JSON.parse(m.data);
|
|
486
|
-
if (d.t==='o')
|
|
487
|
-
else if (d.t==='err')
|
|
488
|
-
else if (d.t==='exit')
|
|
493
|
+
if (d.t==='o') t.term.write(d.d);
|
|
494
|
+
else if (d.t==='err') t.term.write('\\r\\n\\x1b[31m'+d.d+'\\x1b[0m\\r\\n');
|
|
495
|
+
else if (d.t==='exit') t.term.write('\\r\\n\\x1b[90m[session ended — 재연결 시 소생]\\x1b[0m\\r\\n');
|
|
489
496
|
}catch(e){}
|
|
490
497
|
};
|
|
491
498
|
sock.onclose = function(){
|
|
492
|
-
if (sock!==
|
|
493
|
-
var delay = Math.min(8000, 800 * Math.pow(2,
|
|
494
|
-
setTimeout(function(){
|
|
499
|
+
if (sock!==t.ws || t.closing) return;
|
|
500
|
+
var delay = Math.min(8000, 800 * Math.pow(2, t.retry++));
|
|
501
|
+
setTimeout(function(){ connectTab(t); }, delay);
|
|
495
502
|
};
|
|
496
503
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
504
|
+
|
|
505
|
+
function fitTab(t){ if(!t||!t.fit||!t.term) return; try{ t.fit.fit(); if(t.ws&&t.ws.readyState===1) t.ws.send(JSON.stringify({t:'r',cols:t.term.cols,rows:t.term.rows})); }catch(e){} }
|
|
506
|
+
function fitAllVisible(){ eachLeaf(layout,function(l){ if(l.tab!=null && tabs[l.tab] && tabs[l.tab].host.isConnected) fitTab(tabs[l.tab]); }); }
|
|
507
|
+
|
|
508
|
+
// ── 탭 바 렌더(터미널 없음 — 언제든 안전) ──
|
|
509
|
+
function renderTabs(){
|
|
510
|
+
var shown={}; eachLeaf(layout,function(l){ if(l.tab!=null) shown[l.tab]=true; });
|
|
511
|
+
var html='';
|
|
512
|
+
tabOrder.forEach(function(runId){
|
|
513
|
+
var t=tabs[runId]; if(!t) return; var r=runById[runId];
|
|
514
|
+
html += '<div class="tab'+(shown[runId]?' shown':'')+'" draggable="true" data-tab="'+runId+'" title="더블클릭=이름변경 · 드래그=페인에 배치">'
|
|
515
|
+
+ '<span class="st '+esc(r?r.status:'')+'"></span>'
|
|
516
|
+
+ '<span class="nm">'+esc(t.name)+'</span>'
|
|
517
|
+
+ '<button class="x" title="탭 닫기(터미널 종료)">×</button></div>';
|
|
518
|
+
});
|
|
519
|
+
$('tabs').innerHTML = html;
|
|
520
|
+
}
|
|
521
|
+
function buildNode(node){
|
|
522
|
+
if (node.leaf){
|
|
523
|
+
var leaf=document.createElement('div'); leaf.className='leaf'+(node.id===focusLeaf?' focus':''); leaf.dataset.leaf=node.id;
|
|
524
|
+
var t=node.tab!=null?tabs[node.tab]:null; var r=node.tab!=null?runById[node.tab]:null;
|
|
525
|
+
var head=document.createElement('div'); head.className='leaf-h'; head.dataset.leafhead=node.id;
|
|
526
|
+
head.innerHTML = t
|
|
527
|
+
? '<span class="st '+esc(r?r.status:'')+'"></span><span class="nm">'+esc(t.name)+'</span>'
|
|
528
|
+
+ '<span data-role="chip" class="chip '+esc(r?r.status:'')+'">'+esc(r?r.status:'')+'</span>'
|
|
529
|
+
+ '<span data-role="vslot">'+vbadge(r&&r.verifyStatus)+'</span>'
|
|
530
|
+
+ '<button class="x" title="이 페인 닫기(탭은 유지)">×</button>'
|
|
531
|
+
: '<span class="nm" style="color:var(--faint)">빈 페인</span><button class="x" title="이 페인 닫기">×</button>';
|
|
532
|
+
var body=document.createElement('div'); body.className='leaf-body'; body.dataset.leafbody=node.id;
|
|
533
|
+
if (!t){ var em=document.createElement('div'); em.className='leaf-empty'; em.textContent='탭을 여기로 드래그하거나 탭을 클릭하세요'; body.appendChild(em); }
|
|
534
|
+
leaf.appendChild(head); leaf.appendChild(body); return leaf;
|
|
535
|
+
}
|
|
536
|
+
var el=document.createElement('div'); el.className='node '+(node.dir==='col'?'col':'row'); el.dataset.split=node.id;
|
|
537
|
+
var a=buildNode(node.a), b=buildNode(node.b);
|
|
538
|
+
a.style.flexGrow=String(node.ratio); a.style.flexBasis='0';
|
|
539
|
+
b.style.flexGrow=String(1-node.ratio); b.style.flexBasis='0';
|
|
540
|
+
var g=document.createElement('div'); g.className='gutter '+(node.dir==='col'?'col':'row'); g.dataset.gutter=node.id;
|
|
541
|
+
el.appendChild(a); el.appendChild(g); el.appendChild(b); return el;
|
|
542
|
+
}
|
|
543
|
+
// host 를 DOM 에 붙이고, 최초 1회 open → connect(Phase 2 순서). fit 은 render 의 rAF 에서.
|
|
544
|
+
function attachHosts(){ eachLeaf(layout,function(l){ if(l.tab==null||!tabs[l.tab]) return; var t=tabs[l.tab]; var body=$('panes').querySelector('[data-leafbody="'+l.id+'"]'); if(!body) return; body.appendChild(t.host);
|
|
545
|
+
if(!t.opened){ try{ t.term.open(t.host); t.opened=true; }catch(e){} }
|
|
546
|
+
if(!t.connected){ t.connected=true; connectTab(t); }
|
|
547
|
+
}); }
|
|
548
|
+
function updateControls(){
|
|
549
|
+
var has=tabOrder.length>0; var canSplit=has && countLeaves()<MAX_LEAVES;
|
|
550
|
+
$('splitRow').disabled=!canSplit; $('splitCol').disabled=!canSplit; $('closeBtn').disabled=!has;
|
|
551
|
+
}
|
|
552
|
+
// 구조 변경 시 전체 재구성(탭 열기·닫기·분할·드롭·리사이즈완료)
|
|
553
|
+
function render(){
|
|
554
|
+
var n=tabOrder.length;
|
|
555
|
+
$('empty').style.display = n?'none':'flex';
|
|
556
|
+
$('panes').style.display = n?'flex':'none';
|
|
557
|
+
renderTabs();
|
|
558
|
+
var host=$('panes'); host.innerHTML='';
|
|
559
|
+
if (n){ host.appendChild(buildNode(layout)); attachHosts(); }
|
|
560
|
+
updateControls();
|
|
561
|
+
if (typeof reqMode!=='undefined') setMode(reqMode);
|
|
562
|
+
requestAnimationFrame(fitAllVisible);
|
|
563
|
+
setTimeout(fitAllVisible, 60); // 레이아웃 확정 후 재핏(초기 0-size 보정)
|
|
564
|
+
}
|
|
565
|
+
function setLeafFocus(id){
|
|
566
|
+
focusLeaf=id;
|
|
567
|
+
Array.prototype.forEach.call($('panes').querySelectorAll('.leaf'),function(el){ el.classList.toggle('focus', el.dataset.leaf===id); });
|
|
568
|
+
var rid=focusedRunId(); if(rid!=null && tabs[rid]) try{ tabs[rid].term.focus(); }catch(e){}
|
|
569
|
+
updateControls();
|
|
570
|
+
if (typeof reqMode!=='undefined' && reqMode!=='new') setMode(reqMode);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 탭 열기 = 포커스 슬롯에 표시(강제 분할 없음). 기존 호출부(openRunPane) 호환.
|
|
574
|
+
function openTab(runId){
|
|
575
|
+
ensureTab(runId);
|
|
576
|
+
var l=leafOfTab(runId);
|
|
577
|
+
if (l){ setLeafFocus(l.id); return; }
|
|
578
|
+
var f=findLeaf(focusLeaf) || firstLeaf();
|
|
579
|
+
if (!f){ layout={leaf:true,id:'L0',tab:runId}; focusLeaf='L0'; }
|
|
580
|
+
else { f.tab=runId; focusLeaf=f.id; }
|
|
581
|
+
render(); renderTree();
|
|
582
|
+
}
|
|
583
|
+
function openRunPane(runId){ return openTab(runId); }
|
|
584
|
+
|
|
585
|
+
function splitFocused(dir){
|
|
586
|
+
if (!tabOrder.length) return;
|
|
587
|
+
if (countLeaves()>=MAX_LEAVES){ toast('페인 최대 '+MAX_LEAVES+'개'); return; }
|
|
588
|
+
var l=findLeaf(focusLeaf) || firstLeaf(); if(!l) return;
|
|
589
|
+
var keep=l.tab; var aId=l.id, bId=newLeafId();
|
|
590
|
+
delete l.tab; delete l.leaf; l.id=newLeafId(); l.split=true; l.dir=dir; l.ratio=0.5;
|
|
591
|
+
l.a={leaf:true,id:aId,tab:keep}; l.b={leaf:true,id:bId,tab:null};
|
|
592
|
+
focusLeaf=bId; render(); renderTree();
|
|
593
|
+
}
|
|
594
|
+
function closeSlot(id){
|
|
595
|
+
if (countLeaves()<=1){ var only=findLeaf(id)||firstLeaf(); if(only) only.tab=null; if(only) focusLeaf=only.id; render(); renderTree(); return; }
|
|
596
|
+
(function walk(node){
|
|
597
|
+
if(node.leaf) return false;
|
|
598
|
+
if(node.a.leaf && node.a.id===id){ copyInto(node,node.b); return true; }
|
|
599
|
+
if(node.b.leaf && node.b.id===id){ copyInto(node,node.a); return true; }
|
|
600
|
+
return walk(node.a)||walk(node.b);
|
|
601
|
+
})(layout);
|
|
602
|
+
if(!findLeaf(focusLeaf)){ var f=firstLeaf(); focusLeaf=f?f.id:'L0'; }
|
|
603
|
+
render(); renderTree();
|
|
604
|
+
}
|
|
605
|
+
function closeTab(runId){
|
|
606
|
+
var t=tabs[runId]; if(!t) return; t.closing=true;
|
|
607
|
+
try{ if(t.ws) t.ws.close(); }catch(e){}
|
|
608
|
+
try{ if(t.ro) t.ro.disconnect(); }catch(e){}
|
|
609
|
+
try{ t.term.dispose(); }catch(e){}
|
|
610
|
+
eachLeaf(layout,function(l){ if(l.tab===runId) l.tab=null; });
|
|
611
|
+
delete tabs[runId]; tabOrder=tabOrder.filter(function(x){return x!==runId;});
|
|
612
|
+
render(); renderTree();
|
|
613
|
+
}
|
|
614
|
+
// 팬아웃 — N개 탭을 만들고 자동 타일 배치(비교용)
|
|
615
|
+
function buildTiled(ids,dir){
|
|
616
|
+
if(ids.length===1) return {leaf:true,id:newLeafId(),tab:ids[0]};
|
|
617
|
+
var mid=Math.ceil(ids.length/2);
|
|
618
|
+
return {split:true,id:newLeafId(),dir:dir,ratio:mid/ids.length,
|
|
619
|
+
a:buildTiled(ids.slice(0,mid),dir==='row'?'col':'row'),
|
|
620
|
+
b:buildTiled(ids.slice(mid),dir==='row'?'col':'row')};
|
|
621
|
+
}
|
|
622
|
+
function tileTabs(ids){
|
|
623
|
+
ids=(ids||[]).filter(function(x){return x!=null;}); if(!ids.length) return;
|
|
624
|
+
ids.forEach(ensureTab);
|
|
625
|
+
layout=buildTiled(ids,'row'); var f=firstLeaf(); focusLeaf=f?f.id:'L0';
|
|
626
|
+
render(); renderTree();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// fleet 갱신 시 라벨·상태만 갱신(페인 DOM 재구성 X — 터미널 유지). 사라진 run 탭 정리.
|
|
512
630
|
function syncPanes(){
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if
|
|
518
|
-
|
|
631
|
+
tabOrder.slice().forEach(function(runId){ if(!runById[runId]) closeTab(runId); });
|
|
632
|
+
tabOrder.forEach(function(runId){ var t=tabs[runId]; if(t) t.name=tabName(runId); });
|
|
633
|
+
renderTabs();
|
|
634
|
+
eachLeaf(layout,function(l){
|
|
635
|
+
if(l.tab==null) return; var r=runById[l.tab]; if(!r) return;
|
|
636
|
+
var head=$('panes').querySelector('[data-leafhead="'+l.id+'"]'); if(!head) return;
|
|
637
|
+
var nm=head.querySelector('.nm'); if(nm && tabs[l.tab]) nm.textContent=tabs[l.tab].name;
|
|
638
|
+
var st=head.querySelector('.st'); if(st) st.className='st '+r.status;
|
|
639
|
+
var chip=head.querySelector('[data-role=chip]'); if(chip){ chip.className='chip '+r.status; chip.textContent=r.status; }
|
|
640
|
+
var vslot=head.querySelector('[data-role=vslot]'); if(vslot) vslot.innerHTML=vbadge(r.verifyStatus);
|
|
519
641
|
});
|
|
520
642
|
}
|
|
521
|
-
|
|
643
|
+
|
|
644
|
+
// 인라인 이름 변경(탭 더블클릭)
|
|
645
|
+
function startRename(runId, tabEl){
|
|
646
|
+
var r=runById[runId]; var t=tabs[runId]; if(!r||!t) return;
|
|
647
|
+
var nm=tabEl.querySelector('.nm'); if(!nm) return;
|
|
648
|
+
var input=document.createElement('input'); input.className='ren'; input.value=t.name;
|
|
649
|
+
nm.replaceWith(input); input.focus(); input.select();
|
|
650
|
+
var done=false;
|
|
651
|
+
function finish(commit){ if(done) return; done=true;
|
|
652
|
+
var val=input.value.trim();
|
|
653
|
+
var span=document.createElement('span'); span.className='nm'; span.textContent=(commit&&val)?val:t.name;
|
|
654
|
+
input.replaceWith(span);
|
|
655
|
+
if(commit && val && val!==t.name) renameTask(r.taskId, val);
|
|
656
|
+
}
|
|
657
|
+
input.addEventListener('keydown', function(e){ e.stopPropagation(); if(e.key==='Enter'){ e.preventDefault(); finish(true); } else if(e.key==='Escape'){ finish(false); } });
|
|
658
|
+
input.addEventListener('blur', function(){ finish(true); });
|
|
659
|
+
input.addEventListener('click', function(e){ e.stopPropagation(); });
|
|
660
|
+
}
|
|
661
|
+
async function renameTask(taskId, title){
|
|
662
|
+
try{
|
|
663
|
+
var res=await fetch('/api/tasks/'+taskId,{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({title:title})});
|
|
664
|
+
if(res.ok){ if(taskById[taskId]) taskById[taskId].title=title; toast('이름 변경 · '+title); await hydrate(); }
|
|
665
|
+
else { var j=await res.json().catch(function(){return{};}); toast('이름 변경 실패: '+(j.error||res.status)); }
|
|
666
|
+
}catch(e){ toast('이름 변경 실패: '+e); }
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ── 탭 바 이벤트 ──
|
|
670
|
+
$('tabs').addEventListener('click', function(e){
|
|
671
|
+
var tabEl=e.target.closest('[data-tab]'); if(!tabEl) return; var runId=+tabEl.dataset.tab;
|
|
672
|
+
if (e.target.closest('.x')){ closeTab(runId); return; }
|
|
673
|
+
openTab(runId);
|
|
674
|
+
});
|
|
675
|
+
$('tabs').addEventListener('dblclick', function(e){ var tabEl=e.target.closest('[data-tab]'); if(tabEl) startRename(+tabEl.dataset.tab, tabEl); });
|
|
676
|
+
$('tabs').addEventListener('dragstart', function(e){ var tabEl=e.target.closest('[data-tab]'); if(!tabEl) return; dragRunId=+tabEl.dataset.tab; tabEl.classList.add('drag'); try{ e.dataTransfer.effectAllowed='move'; e.dataTransfer.setData('text/plain', String(dragRunId)); }catch(_){} });
|
|
677
|
+
$('tabs').addEventListener('dragend', function(e){ var tabEl=e.target.closest('[data-tab]'); if(tabEl) tabEl.classList.remove('drag'); dragRunId=null; });
|
|
678
|
+
|
|
679
|
+
// ── 페인(슬롯) 이벤트: 포커스·닫기·드롭·리사이즈 ──
|
|
680
|
+
$('panes').addEventListener('click', function(e){
|
|
681
|
+
var leaf=e.target.closest('[data-leaf]'); if(!leaf) return;
|
|
682
|
+
if (e.target.closest('.leaf-h .x')){ closeSlot(leaf.dataset.leaf); return; }
|
|
683
|
+
setLeafFocus(leaf.dataset.leaf);
|
|
684
|
+
});
|
|
685
|
+
$('panes').addEventListener('dragover', function(e){ var body=e.target.closest('[data-leafbody]'); if(body && dragRunId!=null){ e.preventDefault(); body.classList.add('drop'); } });
|
|
686
|
+
$('panes').addEventListener('dragleave', function(e){ var body=e.target.closest('[data-leafbody]'); if(body) body.classList.remove('drop'); });
|
|
687
|
+
$('panes').addEventListener('drop', function(e){
|
|
688
|
+
var body=e.target.closest('[data-leafbody]'); if(!body || dragRunId==null) return; e.preventDefault(); body.classList.remove('drop');
|
|
689
|
+
var l=findLeaf(body.dataset.leafbody); if(!l) return;
|
|
690
|
+
var rid=dragRunId; eachLeaf(layout,function(x){ if(x.tab===rid) x.tab=null; });
|
|
691
|
+
ensureTab(rid); l.tab=rid; focusLeaf=l.id; render(); renderTree();
|
|
692
|
+
});
|
|
693
|
+
$('panes').addEventListener('mousedown', function(e){
|
|
694
|
+
var g=e.target.closest('[data-gutter]'); if(!g) return; e.preventDefault();
|
|
695
|
+
var node=findSplit(g.dataset.gutter); if(!node) return;
|
|
696
|
+
var container=g.parentElement; var rect=container.getBoundingClientRect(); var horiz=node.dir==='row';
|
|
697
|
+
g.classList.add('drag');
|
|
698
|
+
function mv(ev){
|
|
699
|
+
var pos=horiz?(ev.clientX-rect.left)/rect.width:(ev.clientY-rect.top)/rect.height;
|
|
700
|
+
node.ratio=Math.max(0.1,Math.min(0.9,pos));
|
|
701
|
+
var a=container.children[0], b=container.children[2];
|
|
702
|
+
if(a&&b){ a.style.flexGrow=String(node.ratio); b.style.flexGrow=String(1-node.ratio); }
|
|
703
|
+
}
|
|
704
|
+
function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up); g.classList.remove('drag'); fitAllVisible(); }
|
|
705
|
+
document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
|
|
706
|
+
});
|
|
707
|
+
$('splitRow').addEventListener('click', function(){ splitFocused('row'); });
|
|
708
|
+
$('splitCol').addEventListener('click', function(){ splitFocused('col'); });
|
|
709
|
+
$('closeBtn').addEventListener('click', function(){ if(focusLeaf) closeSlot(focusLeaf); });
|
|
522
710
|
|
|
523
711
|
// ── 자유 세션 — 폴더를 지정해 tmux 셸(프로젝트 비소속). ──
|
|
524
712
|
var pickPathCur = '';
|
|
525
713
|
function machineSlug(){ return (fleet.machines && fleet.machines[0] && fleet.machines[0].slug) || 'local'; }
|
|
526
|
-
function openSession(){ $('pickModal').classList.add('on'); browseTo(''); }
|
|
714
|
+
function openSession(){ $('pickModal').classList.add('on'); $('pickName').value=''; browseTo(''); }
|
|
527
715
|
function closePicker(){ $('pickModal').classList.remove('on'); }
|
|
528
716
|
async function browseTo(p){
|
|
529
717
|
try{
|
|
@@ -549,20 +737,21 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
549
737
|
if (openingSession || !pickPathCur) return;
|
|
550
738
|
openingSession=true; $('pickGo').disabled=true;
|
|
551
739
|
try{
|
|
740
|
+
var nm = $('pickName').value.trim();
|
|
552
741
|
var res = await fetch('/api/session',{method:'POST',headers:{'content-type':'application/json'},
|
|
553
|
-
body:JSON.stringify({machineSlug:machineSlug(), path:pickPathCur})});
|
|
742
|
+
body:JSON.stringify({machineSlug:machineSlug(), path:pickPathCur, title:nm})});
|
|
554
743
|
var j = await res.json().catch(function(){return{};});
|
|
555
|
-
if (res.ok && j.runId){ closePicker(); await hydrate(); openRunPane(j.runId); toast('세션 열림 · '+pickPathCur); }
|
|
744
|
+
if (res.ok && j.runId){ closePicker(); await hydrate(); openRunPane(j.runId); toast('세션 열림 · '+(nm||pickPathCur)); }
|
|
556
745
|
else toast('세션 실패: '+(j.detail||j.error||res.status));
|
|
557
746
|
}catch(e){ toast('세션 실패: '+e); }
|
|
558
747
|
finally{ openingSession=false; $('pickGo').disabled=false; }
|
|
559
748
|
});
|
|
749
|
+
$('pickName').addEventListener('keydown', function(e){ if(e.key==='Enter'){ e.preventDefault(); $('pickGo').click(); } });
|
|
560
750
|
$('sessionBtn').addEventListener('click', openSession);
|
|
561
751
|
$('sessionCta').addEventListener('click', openSession);
|
|
562
752
|
|
|
563
753
|
// ── 요청바: New(팬아웃) / Steer / Broadcast ──
|
|
564
754
|
var reqMode = 'new';
|
|
565
|
-
function focusRun(){ if (!focusId) return null; var p = panes.find(function(x){return x.id===focusId;}); return p?p.runId:null; }
|
|
566
755
|
function setMode(m){
|
|
567
756
|
reqMode = m;
|
|
568
757
|
Array.prototype.forEach.call(document.querySelectorAll('.mode'), function(b){ b.classList.toggle('on', b.dataset.mode===m); });
|
|
@@ -574,9 +763,9 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
574
763
|
else if (m==='steer'){ var rid=focusRun(); go.textContent='Steer ⏎';
|
|
575
764
|
$('reqTgt').textContent = rid?('➤ r'+rid+' 에 후속 지시'):'포커스한 페인 없음';
|
|
576
765
|
inp.placeholder = rid?('r'+rid+' 에이전트에게 다음 지시…'):'왼쪽 트리에서 run 을 열어 포커스하세요'; }
|
|
577
|
-
else { go.textContent='Send ⏎'; var n=
|
|
578
|
-
$('reqTgt').textContent = '⊞ 열린
|
|
579
|
-
inp.placeholder = n?('열린 '+n+'개 터미널에 그대로 전송 (엔터 포함)'):'열린
|
|
766
|
+
else { go.textContent='Send ⏎'; var n=tabOrder.length;
|
|
767
|
+
$('reqTgt').textContent = '⊞ 열린 탭 '+n+'개에 브로드캐스트';
|
|
768
|
+
inp.placeholder = n?('열린 '+n+'개 터미널에 그대로 전송 (엔터 포함)'):'열린 탭이 없습니다'; }
|
|
580
769
|
}
|
|
581
770
|
Array.prototype.forEach.call(document.querySelectorAll('.mode'), function(b){ b.addEventListener('click', function(){ setMode(b.dataset.mode); $('reqInput').focus(); }); });
|
|
582
771
|
|
|
@@ -598,8 +787,8 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
598
787
|
var ids = (rr&&rr.runs||[]).map(function(x){return x.id;});
|
|
599
788
|
inp.value='';
|
|
600
789
|
await hydrate();
|
|
601
|
-
//
|
|
602
|
-
ids
|
|
790
|
+
// 팬아웃은 비교가 목적 — N개 탭을 만들고 자동 타일 배치
|
|
791
|
+
tileTabs(ids);
|
|
603
792
|
toast(ids.length+'개 에이전트 팬아웃 · r'+ids.join(' r'));
|
|
604
793
|
}catch(e){ toast('요청 실패: '+e); }
|
|
605
794
|
finally{ submitting=false; $('reqGo').disabled=false; }
|
|
@@ -615,12 +804,12 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
615
804
|
}catch(e){ toast('steer 실패: '+e); }
|
|
616
805
|
finally{ submitting=false; $('reqGo').disabled=false; }
|
|
617
806
|
} else { // broadcast
|
|
618
|
-
if (!
|
|
807
|
+
if (!tabOrder.length){ toast('열린 탭이 없습니다'); return; }
|
|
619
808
|
var payload = text + '\\r';
|
|
620
809
|
var sent=0;
|
|
621
|
-
|
|
810
|
+
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++; } });
|
|
622
811
|
inp.value='';
|
|
623
|
-
toast('브로드캐스트 → '+sent+'개
|
|
812
|
+
toast('브로드캐스트 → '+sent+'개 탭');
|
|
624
813
|
}
|
|
625
814
|
}
|
|
626
815
|
$('reqGo').addEventListener('click', submitReq);
|
|
@@ -749,10 +938,10 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
749
938
|
function scheduleHydrate(){ if (hydT) return; hydT = setTimeout(function(){ hydT=null; hydrate(); }, 400); }
|
|
750
939
|
|
|
751
940
|
setMode('new');
|
|
752
|
-
|
|
941
|
+
render(); // 초기 빈 상태 정합(탭 없음 → empty)
|
|
753
942
|
hydrate();
|
|
754
943
|
wsConnect();
|
|
755
|
-
window.addEventListener('resize',
|
|
944
|
+
window.addEventListener('resize', fitAllVisible);
|
|
756
945
|
</script>
|
|
757
946
|
</body>
|
|
758
947
|
</html>`;
|