coxpit 5.15.0 → 5.17.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 +2 -1
- package/src/cockpit.ts +74 -21
- package/src/orchestrator.ts +51 -1
- package/src/server.ts +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.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",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@libsql/client": "^0.14",
|
|
47
47
|
"@xterm/addon-fit": "^0.10.0",
|
|
48
48
|
"@xterm/addon-unicode11": "^0.8.0",
|
|
49
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
49
50
|
"@xterm/xterm": "^5.5.0",
|
|
50
51
|
"dotenv": "^16",
|
|
51
52
|
"drizzle-orm": "^0.38",
|
package/src/cockpit.ts
CHANGED
|
@@ -255,6 +255,17 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
255
255
|
.hist-refresh:hover{color:var(--ink);border-color:var(--line-hi)}
|
|
256
256
|
.hist-body{flex:1;margin:0;overflow:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;
|
|
257
257
|
padding:10px 12px;font-family:var(--mono);font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--ink);background:var(--bg)}
|
|
258
|
+
.hist-modes{display:inline-flex;gap:2px;margin-left:12px;border:1px solid var(--line);border-radius:7px;padding:2px}
|
|
259
|
+
.hm-tab{font-family:var(--mono);font-size:11px;color:var(--muted);background:none;border:none;border-radius:5px;padding:4px 9px;cursor:pointer}
|
|
260
|
+
.hm-tab.on{background:var(--brand-dim);color:var(--ink)}
|
|
261
|
+
.hist-chat{flex:1;overflow:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;display:flex;flex-direction:column;gap:10px;padding:12px;background:var(--bg)}
|
|
262
|
+
.ch-turn{display:flex} .ch-turn.user{justify-content:flex-end}
|
|
263
|
+
.ch-bubble{max-width:82%;padding:9px 12px;border-radius:12px;font-size:13px;line-height:1.55;word-break:break-word}
|
|
264
|
+
.ch-turn.user .ch-bubble{background:var(--brand-dim);color:var(--ink);border:1px solid rgba(78,201,176,.25);border-bottom-right-radius:4px}
|
|
265
|
+
.ch-turn.asst .ch-bubble{background:var(--surface);color:var(--ink);border:1px solid var(--line);border-bottom-left-radius:4px}
|
|
266
|
+
.ch-tools{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}
|
|
267
|
+
.ch-tool{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:1px 7px}
|
|
268
|
+
.hist-empty{color:var(--faint);text-align:center;padding:30px 16px;font-size:12.5px;margin:auto}
|
|
258
269
|
/* 터치 기기(아이패드 포함, 화면폭 무관): 입력바 노출 + 요청바/분할 숨김(하단 클러터·잘림 방지) */
|
|
259
270
|
body.touch .term-ibar{display:flex}
|
|
260
271
|
body.touch .reqbar{display:none}
|
|
@@ -295,7 +306,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
295
306
|
</div>
|
|
296
307
|
<div class="right">
|
|
297
308
|
<span class="ws" id="ws"><span class="dot"></span><span id="wstext" class="b-txt">connecting</span></span>
|
|
298
|
-
<button type="button" class="toggle" id="secretsBtn" title="시크릿(API 키) 관리 — 세션에 env 로 주입"
|
|
309
|
+
<button type="button" class="toggle" id="secretsBtn" title="시크릿(API 키) 관리 — 세션에 env 로 주입">∗<span class="b-txt"> Secrets</span></button>
|
|
299
310
|
<a class="toggle" href="/" title="보드(모니터) 뷰로">←<span class="b-txt"> Board</span></a>
|
|
300
311
|
</div>
|
|
301
312
|
</header>
|
|
@@ -330,7 +341,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
330
341
|
</div>
|
|
331
342
|
<div class="term-ibar" id="termIbar">
|
|
332
343
|
<div class="tkeys">
|
|
333
|
-
<button type="button" class="tkey scroll" id="histBtn" title="
|
|
344
|
+
<button type="button" class="tkey scroll" id="histBtn" title="뷰어 — 대화/터미널로 위 내용 보기(읽기 전용)">뷰어</button>
|
|
334
345
|
<button type="button" class="tkey scroll" data-k="copymode" title="터미널 안에서 스크롤 — tmux copy-mode 진입(⇞/↑ 로 위로, esc 로 나가기)">⇡ 스크롤</button>
|
|
335
346
|
<button type="button" class="tkey" data-k="esc" title="Esc">esc</button>
|
|
336
347
|
<button type="button" class="tkey" data-k="tab" title="Tab">tab</button>
|
|
@@ -402,7 +413,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
402
413
|
|
|
403
414
|
<div class="modal" id="secretsModal">
|
|
404
415
|
<div class="pick" style="width:min(520px,92vw)">
|
|
405
|
-
<div class="pick-h"><span class="t"
|
|
416
|
+
<div class="pick-h"><span class="t">시크릿 (env 주입)</span><button class="x" id="secretsClose" title="닫기">×</button></div>
|
|
406
417
|
<div style="padding:10px 15px;font-size:11px;color:var(--faint);border-bottom:1px solid var(--line)">여기 등록한 값은 <b>새 세션</b>을 열 때 tmux env 로 주입됩니다(스크롤백에 안 남음). 그 안의 <code>claude</code>·스크립트가 env 에서 읽어 프롬프트가 안 뜹니다. 이미 열린 세션엔 새로 열어야 적용됩니다.</div>
|
|
407
418
|
<div class="pick-list" id="secretsList"></div>
|
|
408
419
|
<div class="pick-f" style="gap:7px">
|
|
@@ -415,10 +426,15 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
415
426
|
|
|
416
427
|
<div class="modal" id="histModal">
|
|
417
428
|
<div class="pick" style="width:min(680px,96vw);max-height:88vh">
|
|
418
|
-
<div class="pick-h"><span class="t"
|
|
429
|
+
<div class="pick-h"><span class="t"><span id="histTitle">뷰어</span></span>
|
|
430
|
+
<div class="hist-modes">
|
|
431
|
+
<button type="button" class="hm-tab on" id="hmChat" title="Claude Code 대화로 보기">대화</button>
|
|
432
|
+
<button type="button" class="hm-tab" id="hmRaw" title="터미널 스크롤백 원문">터미널</button>
|
|
433
|
+
</div>
|
|
419
434
|
<button type="button" class="hist-refresh" id="histRefresh" title="지금 시점으로 다시 불러오기">↻</button>
|
|
420
435
|
<button class="x" id="histClose" title="닫기">×</button></div>
|
|
421
|
-
<
|
|
436
|
+
<div class="hist-chat" id="histChat"></div>
|
|
437
|
+
<pre class="hist-body" id="histBody" style="display:none">불러오는 중…</pre>
|
|
422
438
|
</div>
|
|
423
439
|
</div>
|
|
424
440
|
|
|
@@ -427,6 +443,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
427
443
|
<script src="/vendor/xterm.js"></script>
|
|
428
444
|
<script src="/vendor/addon-fit.js"></script>
|
|
429
445
|
<script src="/vendor/addon-unicode11.js"></script>
|
|
446
|
+
<script src="/vendor/addon-web-links.js"></script>
|
|
430
447
|
<script>
|
|
431
448
|
// 모바일 = 터미널 우선을 유지하되 좁은 화면에 맞춤(드로어 트리 + 단일 터미널 + IME 입력바).
|
|
432
449
|
// (이전엔 보드로 리다이렉트했지만, 이제 cockpit 을 모바일 대응)
|
|
@@ -435,7 +452,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
435
452
|
var $ = function(id){ return document.getElementById(id); };
|
|
436
453
|
|
|
437
454
|
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); }
|
|
438
|
-
var V_GLYPH = { pass:'✓ verify', fail:'✗ verify', running:'⋯ verify', error:'
|
|
455
|
+
var V_GLYPH = { pass:'✓ verify', fail:'✗ verify', running:'⋯ verify', error:'! verify' };
|
|
439
456
|
function vbadge(status){ if (!status || !V_GLYPH[status]) return ''; return '<span class="vbadge '+status+'" data-role="vbadge">'+V_GLYPH[status]+'</span>'; }
|
|
440
457
|
|
|
441
458
|
// ── fleet 상태 ──
|
|
@@ -590,6 +607,22 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
590
607
|
});
|
|
591
608
|
var fit=new window.FitAddon.FitAddon(); term.loadAddon(fit);
|
|
592
609
|
try{ term.loadAddon(new window.Unicode11Addon.Unicode11Addon()); term.unicode.activeVersion='11'; }catch(e){}
|
|
610
|
+
// URL 링크 클릭 가능(웹=새 탭, 데스크톱 앱=시스템 브라우저 — setWindowOpenHandler 가 external 로).
|
|
611
|
+
try{ term.loadAddon(new window.WebLinksAddon.WebLinksAddon(function(ev, uri){ window.open(uri, '_blank', 'noopener'); })); }catch(e){}
|
|
612
|
+
// 복사 배선: xterm 은 user-select:none 이라 네이티브 선택이 없다 → term.getSelection() 을 직접 클립보드로.
|
|
613
|
+
// ① 드래그 놓으면 자동 복사(select-to-copy) ② Cmd/Ctrl+C 로도 복사(선택 없으면 통과 → SIGINT).
|
|
614
|
+
var copySel=function(){ var s=''; try{ s=term.getSelection(); }catch(e){} if(!s) return false;
|
|
615
|
+
try{ if(navigator.clipboard&&navigator.clipboard.writeText) navigator.clipboard.writeText(s); }catch(e){}
|
|
616
|
+
try{ var ta=document.createElement('textarea'); ta.value=s; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }catch(e){}
|
|
617
|
+
return true; };
|
|
618
|
+
host.addEventListener('mouseup', function(){ copySel(); });
|
|
619
|
+
host.addEventListener('touchend', function(){ copySel(); });
|
|
620
|
+
term.attachCustomKeyEventHandler(function(ev){
|
|
621
|
+
if(ev.type==='keydown' && (ev.metaKey||ev.ctrlKey) && (ev.key==='c'||ev.key==='C')){
|
|
622
|
+
if(term.hasSelection()){ copySel(); return false; }
|
|
623
|
+
}
|
|
624
|
+
return true;
|
|
625
|
+
});
|
|
593
626
|
// term.open 은 host 가 DOM 에 붙은 뒤(attachHosts) 최초 1회 — detached 에서 open 하면 렌더러가 안 뜬다.
|
|
594
627
|
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 };
|
|
595
628
|
term.onData(function(d){ if(t.ws&&t.ws.readyState===1) t.ws.send(JSON.stringify({t:'i',d:d})); });
|
|
@@ -643,7 +676,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
643
676
|
? '<span class="st '+esc(r?r.status:'')+'"></span><span class="nm">'+esc(t.name)+'</span>'
|
|
644
677
|
+ '<span data-role="chip" class="chip '+esc(r?r.status:'')+'">'+esc(r?r.status:'')+'</span>'
|
|
645
678
|
+ '<span data-role="vslot">'+vbadge(r&&r.verifyStatus)+'</span>'
|
|
646
|
-
+ '<button class="lock" data-lock="'+node.id+'" title="이 페인에 시크릿/비밀번호 전송(터미널에 안 찍힘)"
|
|
679
|
+
+ '<button class="lock" data-lock="'+node.id+'" title="이 페인에 시크릿/비밀번호 전송(터미널에 안 찍힘)">⊟</button>'
|
|
647
680
|
+ '<button class="x" title="이 페인 닫기(탭은 유지)">×</button>'
|
|
648
681
|
: '<span class="nm" style="color:var(--faint)">빈 페인</span><button class="x" title="이 페인 닫기">×</button>';
|
|
649
682
|
var body=document.createElement('div'); body.className='leaf-body'; body.dataset.leafbody=node.id;
|
|
@@ -849,22 +882,42 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
849
882
|
else { var inp=$('termInput'); if(inp) inp.focus(); }
|
|
850
883
|
}); });
|
|
851
884
|
|
|
852
|
-
// ── (
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
$('histModal').classList.add('on');
|
|
856
|
-
var t=tabs[rid]; $('histTitle').textContent=(t?t.name:('r'+rid))+' · 히스토리';
|
|
857
|
-
var body=$('histBody'); body.textContent='불러오는 중…';
|
|
858
|
-
try{
|
|
859
|
-
var d=await (await fetch('/api/runs/'+rid+'/scrollback?lines=5000')).json();
|
|
860
|
-
body.textContent = (d && d.text) ? d.text : '(스크롤백 없음)';
|
|
861
|
-
body.scrollTop = body.scrollHeight; // 하단(최신)에서 시작 → 위로 스크롤해 과거 읽기
|
|
862
|
-
}catch(e){ body.textContent='불러오기 실패: '+e; }
|
|
863
|
-
}
|
|
885
|
+
// ── 뷰어 — 위 내용을 (대화) Claude Code 로그 / (터미널) 스크롤백 으로 읽기(읽기 전용) ──
|
|
886
|
+
var histMode='chat';
|
|
887
|
+
function openHistory(){ var rid=focusedRunId(); if(rid==null){ toast('포커스한 세션이 없습니다'); return; } $('histModal').classList.add('on'); loadHist(); }
|
|
864
888
|
function closeHistory(){ $('histModal').classList.remove('on'); }
|
|
889
|
+
function setHistMode(m){
|
|
890
|
+
histMode=m; $('hmChat').classList.toggle('on',m==='chat'); $('hmRaw').classList.toggle('on',m==='raw');
|
|
891
|
+
$('histChat').style.display = m==='chat'?'flex':'none'; $('histBody').style.display = m==='raw'?'block':'none';
|
|
892
|
+
loadHist();
|
|
893
|
+
}
|
|
894
|
+
function renderTurn(t){
|
|
895
|
+
var body = esc(t.text||'').replace(/\\n/g,'<br>');
|
|
896
|
+
var tools = (t.tools && t.tools.length) ? '<div class="ch-tools">'+t.tools.map(function(x){return '<span class="ch-tool">'+esc(x)+'</span>';}).join('')+'</div>' : '';
|
|
897
|
+
return '<div class="ch-turn '+(t.role==='user'?'user':'asst')+'"><div class="ch-bubble">'+body+tools+'</div></div>';
|
|
898
|
+
}
|
|
899
|
+
async function loadHist(){
|
|
900
|
+
var rid=focusedRunId(); if(rid==null) return;
|
|
901
|
+
var t=tabs[rid]; $('histTitle').textContent=(t?t.name:('r'+rid))+' · 뷰어';
|
|
902
|
+
if (histMode==='chat'){
|
|
903
|
+
var c=$('histChat'); c.innerHTML='<div class="hist-empty">불러오는 중…</div>';
|
|
904
|
+
try{
|
|
905
|
+
var d=await (await fetch('/api/runs/'+rid+'/chat')).json();
|
|
906
|
+
if (d.turns && d.turns.length){ c.innerHTML = d.turns.map(renderTurn).join(''); c.scrollTop=c.scrollHeight; }
|
|
907
|
+
else { c.innerHTML='<div class="hist-empty">이 폴더에 Claude Code 대화 로그가 없습니다.<br>터미널 탭으로 원문을 보세요.</div>'; }
|
|
908
|
+
}catch(e){ c.innerHTML='<div class="hist-empty">불러오기 실패</div>'; }
|
|
909
|
+
} else {
|
|
910
|
+
var b=$('histBody'); b.textContent='불러오는 중…';
|
|
911
|
+
try{ var d2=await (await fetch('/api/runs/'+rid+'/scrollback?lines=5000')).json();
|
|
912
|
+
b.textContent=(d2 && d2.text)?d2.text:'(스크롤백 없음)'; b.scrollTop=b.scrollHeight;
|
|
913
|
+
}catch(e){ b.textContent='불러오기 실패'; }
|
|
914
|
+
}
|
|
915
|
+
}
|
|
865
916
|
$('histBtn').addEventListener('click', openHistory);
|
|
866
|
-
$('histRefresh').addEventListener('click',
|
|
917
|
+
$('histRefresh').addEventListener('click', loadHist);
|
|
867
918
|
$('histClose').addEventListener('click', closeHistory);
|
|
919
|
+
$('hmChat').addEventListener('click', function(){ setHistMode('chat'); });
|
|
920
|
+
$('hmRaw').addEventListener('click', function(){ setHistMode('raw'); });
|
|
868
921
|
$('histModal').addEventListener('click', function(e){ if(e.target===this) closeHistory(); });
|
|
869
922
|
// 터치 기기(아이패드 포함) 판별 → body.touch (화면폭 무관하게 입력바 노출)
|
|
870
923
|
if (window.matchMedia('(pointer:coarse)').matches || (navigator.maxTouchPoints||0) > 0) document.body.classList.add('touch');
|
|
@@ -949,7 +1002,7 @@ export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
|
949
1002
|
var done=false;
|
|
950
1003
|
function finish(send){ if(done) return; done=true;
|
|
951
1004
|
if(send){ try{ t.ws.send(JSON.stringify({t:'i',d:input.value+'\\r'})); }catch(e){} toast('페인에 전송(엔터 포함)'); }
|
|
952
|
-
var nb=document.createElement('button'); nb.className='lock'; nb.setAttribute('data-lock',leafId); nb.title='이 페인에 시크릿/비밀번호 전송(터미널에 안 찍힘)'; nb.textContent='
|
|
1005
|
+
var nb=document.createElement('button'); nb.className='lock'; nb.setAttribute('data-lock',leafId); nb.title='이 페인에 시크릿/비밀번호 전송(터미널에 안 찍힘)'; nb.textContent='⊟';
|
|
953
1006
|
try{ input.replaceWith(nb); }catch(e){}
|
|
954
1007
|
}
|
|
955
1008
|
input.addEventListener('keydown', function(e){ e.stopPropagation(); if(e.key==='Enter'){ e.preventDefault(); finish(true); } else if(e.key==='Escape'){ finish(false); } });
|
package/src/orchestrator.ts
CHANGED
|
@@ -677,7 +677,57 @@ export async function getRunTermInfo(runId: number): Promise<{ machine: MachineT
|
|
|
677
677
|
}
|
|
678
678
|
|
|
679
679
|
/**
|
|
680
|
-
*
|
|
680
|
+
* 세션의 Claude Code 대화 로그(JSONL)를 대화형으로 파싱 — 뷰어의 "대화" 모드.
|
|
681
|
+
* cwd(worktreePath)를 [^a-zA-Z0-9]→'-' 로 바꾼 게 ~/.claude/projects/<...>/ 폴더명 → 최신 .jsonl.
|
|
682
|
+
* user/assistant turn 만 추출(tool_result 노이즈 제외, tool_use 는 칩으로).
|
|
683
|
+
*/
|
|
684
|
+
export async function getSessionChat(runId: number, maxTurns = 200): Promise<{
|
|
685
|
+
ok: boolean; turns: Array<{ role: string; text: string; tools?: string[] }>; note?: string;
|
|
686
|
+
}> {
|
|
687
|
+
const info = await getRunTermInfo(runId);
|
|
688
|
+
const rr = await db.select().from(agentRuns).where(eq(agentRuns.id, runId)).limit(1);
|
|
689
|
+
const run = rr[0];
|
|
690
|
+
if (!info || !run || !run.worktreePath) return { ok: false, turns: [], note: 'no session' };
|
|
691
|
+
const proj = run.worktreePath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
692
|
+
const cmd = `d=${shq(proj)}; f=$(ls -t "$HOME/.claude/projects/$d"/*.jsonl 2>/dev/null | head -1); [ -n "$f" ] && tail -n 8000 "$f" || true`;
|
|
693
|
+
const r = await runShellOn(info.machine, cmd, 15000);
|
|
694
|
+
if (!r.ok) return { ok: true, turns: [], note: 'no transcript' };
|
|
695
|
+
if (!r.stdout.trim()) return { ok: true, turns: [], note: 'no Claude Code transcript for this folder' };
|
|
696
|
+
const userText = (content: unknown): string => {
|
|
697
|
+
if (typeof content === 'string') return content;
|
|
698
|
+
if (!Array.isArray(content)) return '';
|
|
699
|
+
return content.filter((b) => b && typeof b === 'object' && (b as { type?: string }).type === 'text')
|
|
700
|
+
.map((b) => (b as { text?: string }).text ?? '').join('\n');
|
|
701
|
+
};
|
|
702
|
+
const asstParts = (content: unknown): { text: string; tools: string[] } => {
|
|
703
|
+
if (typeof content === 'string') return { text: content, tools: [] };
|
|
704
|
+
if (!Array.isArray(content)) return { text: '', tools: [] };
|
|
705
|
+
const text: string[] = []; const tools: string[] = [];
|
|
706
|
+
for (const b of content) {
|
|
707
|
+
const bb = b as { type?: string; text?: string; name?: string };
|
|
708
|
+
if (bb?.type === 'text' && bb.text) text.push(bb.text);
|
|
709
|
+
else if (bb?.type === 'tool_use' && bb.name) tools.push(bb.name);
|
|
710
|
+
}
|
|
711
|
+
return { text: text.join('\n'), tools };
|
|
712
|
+
};
|
|
713
|
+
const turns: Array<{ role: string; text: string; tools?: string[] }> = [];
|
|
714
|
+
for (const line of r.stdout.split('\n')) {
|
|
715
|
+
if (!line.trim()) continue;
|
|
716
|
+
let j: { type?: string; message?: { content?: unknown } };
|
|
717
|
+
try { j = JSON.parse(line); } catch { continue; }
|
|
718
|
+
if (j.type === 'user' && j.message) {
|
|
719
|
+
const text = userText(j.message.content);
|
|
720
|
+
if (text.trim()) turns.push({ role: 'user', text: text.slice(0, 8000) });
|
|
721
|
+
} else if (j.type === 'assistant' && j.message) {
|
|
722
|
+
const { text, tools } = asstParts(j.message.content);
|
|
723
|
+
if (text.trim() || tools.length) turns.push({ role: 'assistant', text: text.slice(0, 12000), tools });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return { ok: true, turns: maxTurns && turns.length > maxTurns ? turns.slice(-maxTurns) : turns };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* tmux 페인 스크롤백 스냅샷 — 모바일 "위 내용 읽기"(뷰어의 터미널 모드)용.
|
|
681
731
|
* capture-pane -S -N 으로 N 줄 위부터 현재까지 텍스트를 통째로 반환(읽기 전용).
|
|
682
732
|
*/
|
|
683
733
|
export async function getScrollback(runId: number, lines: number): Promise<{ ok: boolean; text: string }> {
|
package/src/server.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { db } from './db';
|
|
|
19
19
|
import { machines, repos, tasks, agentRuns, agentEvents, designCaptures, shareLinks, taskGroups, secrets } from './db/schema';
|
|
20
20
|
import { BOOKMARKLET_JS } from './design';
|
|
21
21
|
import { runShellOn, shq } from './exec';
|
|
22
|
-
import { launchRun, cleanupRun, stopRun, getRunDiff, loadRunDocs, mergeRun, getRunTermInfo, steerRun, exportRun, prRun, integrateRuns, planFanout, reviewTask, syncRun, openWorkbench, spawnSubtasks, listSubtasks, resolveAgentToken, taskCloseRisk, launchGroupTask, isRunLive, askGroupCoordinator, computeRunOutputs, normalizeOutputs, listReclaimableWorktrees, pruneWorktrees, noopSignal, groupOverlap, landTarget, mergePreview, startLandResolve, listDocuments, verifyRun, openSessionAt, getScrollback } from './orchestrator';
|
|
22
|
+
import { launchRun, cleanupRun, stopRun, getRunDiff, loadRunDocs, mergeRun, getRunTermInfo, steerRun, exportRun, prRun, integrateRuns, planFanout, reviewTask, syncRun, openWorkbench, spawnSubtasks, listSubtasks, resolveAgentToken, taskCloseRisk, launchGroupTask, isRunLive, askGroupCoordinator, computeRunOutputs, normalizeOutputs, listReclaimableWorktrees, pruneWorktrees, noopSignal, groupOverlap, landTarget, mergePreview, startLandResolve, listDocuments, verifyRun, openSessionAt, getScrollback, getSessionChat } from './orchestrator';
|
|
23
23
|
import { openTerm } from './term';
|
|
24
24
|
import { addSink, removeSink, broadcast } from './hub';
|
|
25
25
|
import { getProvider, listProviders } from './providers';
|
|
@@ -35,6 +35,7 @@ const VENDOR: Record<string, { pkg: string; rel: string; type: string }> = {
|
|
|
35
35
|
'xterm.css': { pkg: '@xterm/xterm/package.json', rel: 'css/xterm.css', type: 'text/css' },
|
|
36
36
|
'addon-fit.js': { pkg: '@xterm/addon-fit/package.json', rel: 'lib/addon-fit.js', type: 'text/javascript' },
|
|
37
37
|
'addon-unicode11.js': { pkg: '@xterm/addon-unicode11/package.json', rel: 'lib/addon-unicode11.js', type: 'text/javascript' },
|
|
38
|
+
'addon-web-links.js': { pkg: '@xterm/addon-web-links/package.json', rel: 'lib/addon-web-links.js', type: 'text/javascript' },
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
// ─── 읽기 전용 공유 페이지 (서버 렌더 스냅샷 — 스크립트 0, 액션 0) ───────────
|
|
@@ -1004,6 +1005,14 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
1004
1005
|
return reply.code(202).send(res);
|
|
1005
1006
|
});
|
|
1006
1007
|
|
|
1008
|
+
// 세션 대화 로그(Claude Code JSONL) — 뷰어의 "대화" 모드. 읽기 전용.
|
|
1009
|
+
app.get('/api/runs/:id/chat', async (req, reply) => {
|
|
1010
|
+
const id = Number((req.params as { id: string }).id);
|
|
1011
|
+
const rr = await db.select().from(agentRuns).where(eq(agentRuns.id, id)).limit(1);
|
|
1012
|
+
if (!rr[0]) return reply.code(404).send({ error: 'not found' });
|
|
1013
|
+
return await getSessionChat(id);
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1007
1016
|
// tmux 스크롤백 스냅샷 — 모바일 히스토리 오버레이(위 내용 읽기)용. 읽기 전용.
|
|
1008
1017
|
app.get('/api/runs/:id/scrollback', async (req, reply) => {
|
|
1009
1018
|
const id = Number((req.params as { id: string }).id);
|