coxpit 3.6.0 → 3.7.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 +1 -1
- package/src/board.ts +100 -11
- package/src/orchestrator.ts +29 -0
- package/src/server.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.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": "MIT",
|
package/src/board.ts
CHANGED
|
@@ -290,6 +290,23 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
290
290
|
.cmp-review code{font-family:var(--mono);font-size:.9em;background:#0e1118;padding:1px 5px;border-radius:4px;color:var(--brand)}
|
|
291
291
|
.cmp-review p{margin:0 0 8px}
|
|
292
292
|
|
|
293
|
+
/* ── doc mode (rendered output) ─────────── */
|
|
294
|
+
.doc-h{font-family:var(--mono);font-size:10.5px;color:var(--brand);padding:8px 0 4px;
|
|
295
|
+
border-bottom:1px solid var(--line);margin-bottom:8px;word-break:break-all}
|
|
296
|
+
.doc-md{font-size:13px;line-height:1.65;color:var(--muted);margin-bottom:16px;font-family:var(--sans)}
|
|
297
|
+
.doc-md h1,.doc-md h2{font-size:14px;color:var(--ink);margin:12px 0 6px}
|
|
298
|
+
.doc-md h3{font-size:12.5px;color:var(--ink);margin:10px 0 4px}
|
|
299
|
+
.doc-md ul{margin:4px 0 8px;padding-left:18px}
|
|
300
|
+
.doc-md li{margin-bottom:3px}
|
|
301
|
+
.doc-md strong{color:var(--ink)}
|
|
302
|
+
.doc-md code{font-family:var(--mono);font-size:.9em;background:#0e1118;padding:1px 5px;border-radius:4px;color:var(--brand)}
|
|
303
|
+
.doc-md p{margin:0 0 8px}
|
|
304
|
+
.doc-frame{width:100%;height:420px;border:1px solid var(--line);border-radius:8px;background:#fff;margin-bottom:16px}
|
|
305
|
+
.pane-tgl{margin-left:auto;font-size:10px;padding:2px 9px;text-transform:none;letter-spacing:0}
|
|
306
|
+
/* run 모달의 diff 라인 = steer 참조 클릭 타깃 */
|
|
307
|
+
#mDiffWrap .dl-line{cursor:pointer;border-radius:3px}
|
|
308
|
+
#mDiffWrap .dl-line:hover{background:rgba(78,201,176,.09)}
|
|
309
|
+
|
|
293
310
|
/* ── terminal ───────────────────────────── */
|
|
294
311
|
.term-body{flex:1;min-height:0;background:#0b0d12;padding:8px 4px 4px 10px}
|
|
295
312
|
#xterm{width:100%;height:100%}
|
|
@@ -432,8 +449,9 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
432
449
|
<div class="pane-c tl" id="mTimeline"></div>
|
|
433
450
|
</div>
|
|
434
451
|
<div class="pane">
|
|
435
|
-
<div class="pane-h">Diff <span id="mStat" style="text-transform:none;letter-spacing:0"></span
|
|
436
|
-
|
|
452
|
+
<div class="pane-h" style="display:flex;align-items:center;gap:8px">Diff <span id="mStat" style="text-transform:none;letter-spacing:0"></span>
|
|
453
|
+
<button class="btn-ghost sm pane-tgl" id="mDocsTgl" hidden>Rendered</button></div>
|
|
454
|
+
<div class="pane-c" id="mDiffWrap"><pre class="diff" id="mDiff">loading…</pre></div>
|
|
437
455
|
</div>
|
|
438
456
|
</div>
|
|
439
457
|
<div class="modal-f" id="steerRow" style="border-top:1px solid var(--line)">
|
|
@@ -463,6 +481,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
463
481
|
<div class="modal-h">
|
|
464
482
|
<span class="title" id="cmpTitle">Compare</span>
|
|
465
483
|
<button class="btn sm" id="cmpAI">AI review</button>
|
|
484
|
+
<button class="btn-ghost sm" id="cmpDocsTgl">Rendered</button>
|
|
466
485
|
<button class="btn-ghost sm" id="cmpRefresh">Refresh</button>
|
|
467
486
|
<button class="x" id="cmpClose" aria-label="close">×</button>
|
|
468
487
|
</div>
|
|
@@ -544,6 +563,7 @@ let repos = [], machines = [], captures = [];
|
|
|
544
563
|
|
|
545
564
|
const $ = (id) => document.getElementById(id);
|
|
546
565
|
const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c]));
|
|
566
|
+
const escA = (s) => esc(s).replace(/"/g, '"');
|
|
547
567
|
const statusColor = (s) => 'var(--s-' + (s||'pending') + ', var(--muted))';
|
|
548
568
|
|
|
549
569
|
/* ── custom toast / confirm (시스템 alert·confirm 대체) ── */
|
|
@@ -716,12 +736,23 @@ function summarize(kind, payload){
|
|
|
716
736
|
}
|
|
717
737
|
function diffHTML(text){
|
|
718
738
|
if (!text.trim()) return '<span style="color:var(--faint)">no changes</span>';
|
|
739
|
+
let file = '';
|
|
719
740
|
return text.split('\\n').map(l=>{
|
|
720
741
|
const e = esc(l);
|
|
721
|
-
if (l.startsWith('
|
|
742
|
+
if (l.startsWith('+++')){
|
|
743
|
+
const f = l.replace(/^\\+\\+\\+ [ab]\\//,'').trim();
|
|
744
|
+
if (f && f !== '/dev/null') file = f;
|
|
745
|
+
return '<span class="dl-file">'+e+'</span>';
|
|
746
|
+
}
|
|
747
|
+
if (l.startsWith('---')){
|
|
748
|
+
const f = l.replace(/^--- [ab]\\//,'').trim();
|
|
749
|
+
if (f && f !== '/dev/null') file = f;
|
|
750
|
+
return '<span class="dl-file">'+e+'</span>';
|
|
751
|
+
}
|
|
752
|
+
if (l.startsWith('diff --git')) return '<span class="dl-file">'+e+'</span>';
|
|
722
753
|
if (l.startsWith('@@')) return '<span class="dl-hunk">'+e+'</span>';
|
|
723
|
-
if (l.startsWith('+')) return '<span class="dl-add">'+e+'</span>';
|
|
724
|
-
if (l.startsWith('-')) return '<span class="dl-del">'+e+'</span>';
|
|
754
|
+
if (l.startsWith('+')) return '<span class="dl-add dl-line" data-f="'+escA(file)+'" title="click to reference in a steer">'+e+'</span>';
|
|
755
|
+
if (l.startsWith('-')) return '<span class="dl-del dl-line" data-f="'+escA(file)+'" title="click to reference in a steer">'+e+'</span>';
|
|
725
756
|
return e;
|
|
726
757
|
}).join('\\n');
|
|
727
758
|
}
|
|
@@ -956,17 +987,59 @@ function paintModal(){
|
|
|
956
987
|
).join('') || '<span style="color:var(--faint)">no events yet</span>';
|
|
957
988
|
}
|
|
958
989
|
async function loadDiff(){
|
|
959
|
-
if (openRunId==null) return;
|
|
960
|
-
$('mDiff')
|
|
990
|
+
if (openRunId==null || docMode) return;
|
|
991
|
+
const pre = $('mDiff'); if (!pre) return;
|
|
992
|
+
pre.textContent = 'loading…'; $('mStat').textContent='';
|
|
961
993
|
try{
|
|
962
994
|
const d = await fetch('/api/runs/'+openRunId+'/diff').then(x=>x.json());
|
|
963
|
-
if (!d.ok){
|
|
995
|
+
if (!d.ok){ pre.textContent = d.stat||'no worktree'; return; }
|
|
964
996
|
const files = d.stat ? d.stat.split('\\n').filter(Boolean).length : 0;
|
|
965
997
|
$('mStat').textContent = files ? '· '+files+' file'+(files>1?'s':'') : '· clean';
|
|
966
|
-
|
|
967
|
-
|
|
998
|
+
// 변경분에 문서(md/html)가 있으면 Rendered 토글 노출
|
|
999
|
+
$('mDocsTgl').hidden = !/\\.(md|markdown|html?|htm)$/im.test(d.stat||'');
|
|
1000
|
+
pre.innerHTML = diffHTML(d.diff||'');
|
|
1001
|
+
}catch{ pre.textContent = 'diff failed'; }
|
|
1002
|
+
}
|
|
1003
|
+
/* ── doc 모드 — diff 대신 렌더된 문서 산출물 ── */
|
|
1004
|
+
let docMode = false;
|
|
1005
|
+
function docsHTML(docs){
|
|
1006
|
+
if (!docs.length) return '<span style="color:var(--faint)">no changed docs (md/html) in this worktree</span>';
|
|
1007
|
+
return docs.map(d=>'<div class="doc-h">'+esc(d.path)+'</div>'
|
|
1008
|
+
+ (d.kind==='md'
|
|
1009
|
+
? '<div class="doc-md">'+mdLite(d.content)+'</div>'
|
|
1010
|
+
: '<iframe class="doc-frame" sandbox="" srcdoc="'+escA(d.content)+'"></iframe>')).join('');
|
|
1011
|
+
}
|
|
1012
|
+
async function paintDocs(){
|
|
1013
|
+
if (openRunId==null) return;
|
|
1014
|
+
$('mDiffWrap').innerHTML = '<span style="color:var(--faint)">rendering…</span>';
|
|
1015
|
+
try{
|
|
1016
|
+
const d = await fetch('/api/runs/'+openRunId+'/docs').then(x=>x.json());
|
|
1017
|
+
$('mDiffWrap').innerHTML = docsHTML(d.docs||[]);
|
|
1018
|
+
}catch{ $('mDiffWrap').textContent = 'docs failed'; }
|
|
1019
|
+
}
|
|
1020
|
+
function setDocMode(on){
|
|
1021
|
+
docMode = on;
|
|
1022
|
+
$('mDocsTgl').textContent = on ? 'Diff' : 'Rendered';
|
|
1023
|
+
if (on) paintDocs();
|
|
1024
|
+
else { $('mDiffWrap').innerHTML = '<pre class="diff" id="mDiff">loading…</pre>'; loadDiff(); }
|
|
1025
|
+
}
|
|
1026
|
+
$('mDocsTgl').addEventListener('click', ()=>setDocMode(!docMode));
|
|
1027
|
+
/* diff 라인 클릭 → steer 입력에 참조 인용 (정착 run 에서만 — steerRow 표시 중일 때) */
|
|
1028
|
+
$('mDiffWrap').addEventListener('click', (e)=>{
|
|
1029
|
+
const t = e.target.closest ? e.target.closest('.dl-line') : null;
|
|
1030
|
+
if (!t) return;
|
|
1031
|
+
if ($('steerRow').style.display === 'none') return;
|
|
1032
|
+
const line = (t.textContent||'').slice(1).trim().slice(0,120);
|
|
1033
|
+
const f = t.dataset.f || '';
|
|
1034
|
+
$('steerInput').value = (f ? f+': ' : '') + '"'+line+'" — ';
|
|
1035
|
+
$('steerInput').focus();
|
|
1036
|
+
});
|
|
1037
|
+
function openModal(id){
|
|
1038
|
+
openRunId = id;
|
|
1039
|
+
if (docMode){ docMode=false; $('mDocsTgl').textContent='Rendered'; $('mDiffWrap').innerHTML='<pre class="diff" id="mDiff">loading…</pre>'; }
|
|
1040
|
+
$('mDocsTgl').hidden = true;
|
|
1041
|
+
paintModal(); $('overlay').classList.add('open'); loadDiff();
|
|
968
1042
|
}
|
|
969
|
-
function openModal(id){ openRunId = id; paintModal(); $('overlay').classList.add('open'); loadDiff(); }
|
|
970
1043
|
function closeModal(){ openRunId=null; $('overlay').classList.remove('open'); }
|
|
971
1044
|
/* ── select mode (integrate) ── */
|
|
972
1045
|
let selectMode = false;
|
|
@@ -1090,13 +1163,20 @@ $('mCloseTask').addEventListener('click', async ()=>{
|
|
|
1090
1163
|
|
|
1091
1164
|
/* ── compare view ── */
|
|
1092
1165
|
let cmpTaskId = null;
|
|
1166
|
+
let cmpDocMode = false;
|
|
1093
1167
|
async function openCompare(taskId){
|
|
1094
1168
|
cmpTaskId = taskId;
|
|
1169
|
+
cmpDocMode = false; $('cmpDocsTgl').textContent = 'Rendered';
|
|
1095
1170
|
$('cmpReview').hidden = true; $('cmpReview').innerHTML = '';
|
|
1096
1171
|
$('cmpOverlay').classList.add('open');
|
|
1097
1172
|
$('cmpBody').innerHTML = '<div class="empty" style="flex:1">loading…</div>';
|
|
1098
1173
|
await paintCompare();
|
|
1099
1174
|
}
|
|
1175
|
+
$('cmpDocsTgl').addEventListener('click', async ()=>{
|
|
1176
|
+
cmpDocMode = !cmpDocMode;
|
|
1177
|
+
$('cmpDocsTgl').textContent = cmpDocMode ? 'Diff' : 'Rendered';
|
|
1178
|
+
await paintCompare();
|
|
1179
|
+
});
|
|
1100
1180
|
async function paintCompare(){
|
|
1101
1181
|
if (cmpTaskId==null) return;
|
|
1102
1182
|
let d;
|
|
@@ -1123,6 +1203,15 @@ async function paintCompare(){
|
|
|
1123
1203
|
+ '<button class="btn sm" data-merge="'+r.id+'"'+(mergeable?'':' disabled')+'>Merge this</button>')
|
|
1124
1204
|
+ '</div></div>';
|
|
1125
1205
|
}).join('');
|
|
1206
|
+
// doc 모드 — 각 열의 diff 를 렌더된 문서로 치환 (열별 비동기, 도착 순)
|
|
1207
|
+
if (cmpDocMode){
|
|
1208
|
+
for (const r of d.runs){
|
|
1209
|
+
fetch('/api/runs/'+r.id+'/docs').then(x=>x.json()).then(j=>{
|
|
1210
|
+
const col = document.querySelector('.cmp-col[data-run="'+r.id+'"] .cmp-diff');
|
|
1211
|
+
if (col && cmpDocMode) col.innerHTML = docsHTML(j.docs||[]);
|
|
1212
|
+
}).catch(()=>{});
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1126
1215
|
}
|
|
1127
1216
|
$('cmpBody').addEventListener('click', async (e)=>{
|
|
1128
1217
|
const prBtn = e.target.closest('button[data-pr]');
|
package/src/orchestrator.ts
CHANGED
|
@@ -327,6 +327,35 @@ export async function getRunDiff(runId: number): Promise<{ ok: boolean; diff: st
|
|
|
327
327
|
return { ok: true, stat: stat.trim(), diff: diff.slice(0, 200_000) };
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Doc 모드 — run worktree 의 변경 파일 중 문서(md/html)만 내용째 회수.
|
|
332
|
+
* 보드가 diff 대신 "렌더된 산출물"을 보여줄 때 사용 (읽기 전용).
|
|
333
|
+
*/
|
|
334
|
+
export async function getRunDocs(runId: number): Promise<{
|
|
335
|
+
ok: boolean; docs: Array<{ path: string; kind: 'md' | 'html'; content: string }>; detail?: string;
|
|
336
|
+
}> {
|
|
337
|
+
const ctx = await loadContext(runId);
|
|
338
|
+
const rr = await db.select().from(agentRuns).where(eq(agentRuns.id, runId)).limit(1);
|
|
339
|
+
const run = rr[0];
|
|
340
|
+
if (!ctx || !run || !run.worktreePath) return { ok: false, docs: [], detail: 'no worktree' };
|
|
341
|
+
const wt = run.worktreePath;
|
|
342
|
+
// 변경 파일 목록 = tracked 수정 + untracked (status --porcelain 경로 필드)
|
|
343
|
+
const ls = await runShellOn(ctx.machine, `git -C ${shq(wt)} status --porcelain`, 15000);
|
|
344
|
+
if (!ls.ok) return { ok: false, docs: [], detail: 'status failed' };
|
|
345
|
+
const kindOf = (p: string): 'md' | 'html' | null =>
|
|
346
|
+
/\.(md|markdown)$/i.test(p) ? 'md' : /\.(html?|htm)$/i.test(p) ? 'html' : null;
|
|
347
|
+
const paths = ls.stdout.split('\n')
|
|
348
|
+
.map((l) => l.slice(3).trim().replace(/^"|"$/g, ''))
|
|
349
|
+
.filter((p) => p && kindOf(p) !== null)
|
|
350
|
+
.slice(0, 6); // 문서 비교 용도 — 상한
|
|
351
|
+
const docs: Array<{ path: string; kind: 'md' | 'html'; content: string }> = [];
|
|
352
|
+
for (const p of paths) {
|
|
353
|
+
const cat = await runShellOn(ctx.machine, `head -c 200000 ${shq(ppath.join(wt, p))}`, 15000);
|
|
354
|
+
if (cat.ok) docs.push({ path: p, kind: kindOf(p)!, content: cat.stdout });
|
|
355
|
+
}
|
|
356
|
+
return { ok: true, docs };
|
|
357
|
+
}
|
|
358
|
+
|
|
330
359
|
/**
|
|
331
360
|
* base 동기화 — 오래 사는 세션의 worktree 에 base 브랜치 최신을 머지한다.
|
|
332
361
|
* 충돌 시 자동 abort — 그땐 steer 로 에이전트에게 머지를 맡기라고 안내.
|
package/src/server.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { db } from './db';
|
|
|
12
12
|
import { machines, repos, tasks, agentRuns, agentEvents, designCaptures } from './db/schema';
|
|
13
13
|
import { BOOKMARKLET_JS } from './design';
|
|
14
14
|
import { runShellOn, shq } from './exec';
|
|
15
|
-
import { launchRun, cleanupRun, stopRun, getRunDiff, mergeRun, getRunTermInfo, steerRun, exportRun, prRun, integrateRuns, planFanout, reviewTask, syncRun, openWorkbench } from './orchestrator';
|
|
15
|
+
import { launchRun, cleanupRun, stopRun, getRunDiff, getRunDocs, mergeRun, getRunTermInfo, steerRun, exportRun, prRun, integrateRuns, planFanout, reviewTask, syncRun, openWorkbench } from './orchestrator';
|
|
16
16
|
import { openTerm } from './term';
|
|
17
17
|
import { addSink, removeSink, broadcast } from './hub';
|
|
18
18
|
import { getProvider, listProviders } from './providers';
|
|
@@ -509,6 +509,14 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
509
509
|
return getRunDiff(id);
|
|
510
510
|
});
|
|
511
511
|
|
|
512
|
+
// Doc 모드 — 변경된 문서(md/html)를 내용째 (렌더 비교용, 읽기 전용)
|
|
513
|
+
app.get('/api/runs/:id/docs', async (req, reply) => {
|
|
514
|
+
const id = Number((req.params as { id: string }).id);
|
|
515
|
+
const rr = await db.select().from(agentRuns).where(eq(agentRuns.id, id)).limit(1);
|
|
516
|
+
if (!rr[0]) return reply.code(404).send({ error: 'not found' });
|
|
517
|
+
return getRunDocs(id);
|
|
518
|
+
});
|
|
519
|
+
|
|
512
520
|
// 라이브 스트림 좌석 — 오케스트레이터가 run/event 를 여기로 broadcast.
|
|
513
521
|
app.get('/ws', { websocket: true }, (socket) => {
|
|
514
522
|
addSink(socket);
|