coxpit 4.7.0 → 5.0.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/.env.example +5 -3
- package/README.md +17 -3
- package/bin/coxpit.js +30 -6
- package/package.json +1 -1
- package/src/auth.ts +43 -12
- package/src/authkey.ts +260 -0
- package/src/board.ts +510 -181
- package/src/icons.ts +46 -0
- package/src/index.ts +22 -6
- package/src/login.ts +131 -0
- package/src/server.ts +129 -7
package/src/board.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// 데몬이 서빙하는 단일 페이지 플릿 콘솔(빌드 스텝 0, 자가완결).
|
|
2
2
|
// /api/fleet 로 하이드레이트 → /ws 구독 델타 → run 상세(타임라인·diff·터미널)·비교/머지.
|
|
3
|
+
import { ICON_SPRITE, ICON_CSS, ICON_JS_HELPER } from './icons.js';
|
|
4
|
+
|
|
3
5
|
export const BOARD_HTML = /* html */ `<!doctype html>
|
|
4
6
|
<html lang="en">
|
|
5
7
|
<head>
|
|
@@ -21,8 +23,12 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
21
23
|
*{box-sizing:border-box}
|
|
22
24
|
[hidden]{display:none !important}
|
|
23
25
|
html,body{height:100%}
|
|
26
|
+
/* mobile stability — kill the iOS left-right rubber-band + horizontal pan.
|
|
27
|
+
overscroll-behavior on the root stops the rubber-band; overflow-x:hidden is on BODY only
|
|
28
|
+
(NOT html — overflow on html breaks iOS position:fixed/sticky, which the header + drawer use). */
|
|
29
|
+
html{overscroll-behavior:none}
|
|
24
30
|
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px;line-height:1.5;
|
|
25
|
-
-webkit-font-smoothing:antialiased}
|
|
31
|
+
-webkit-font-smoothing:antialiased;overflow-x:hidden;overscroll-behavior-x:none}
|
|
26
32
|
::selection{background:var(--brand-dim)}
|
|
27
33
|
::-webkit-scrollbar{width:9px;height:9px}
|
|
28
34
|
::-webkit-scrollbar-thumb{background:#252c3a;border-radius:5px;border:2px solid var(--bg)}
|
|
@@ -80,6 +86,94 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
80
86
|
color:var(--faint);margin:2px 0 -2px}
|
|
81
87
|
.btn[disabled]{opacity:.4;cursor:not-allowed;filter:none}
|
|
82
88
|
|
|
89
|
+
/* ── navigator rail (v5.0) — machine · repos · view nav · New ── */
|
|
90
|
+
aside.rail{padding:12px 10px;gap:4px;overflow-y:auto}
|
|
91
|
+
.rail .machine{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11.5px;
|
|
92
|
+
color:var(--ink);border:1px solid var(--line-hi);border-radius:8px;padding:7px 10px;margin-bottom:6px;
|
|
93
|
+
cursor:pointer;background:transparent;text-align:left;width:100%}
|
|
94
|
+
.rail .machine:hover{border-color:rgba(78,201,176,.4)}
|
|
95
|
+
.rail .machine .ic{width:14px;height:14px;color:var(--muted)}
|
|
96
|
+
.rail .machine .msp{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
97
|
+
.rail .machine .mdot2{width:6px;height:6px;border-radius:50%;background:var(--faint);flex:none}
|
|
98
|
+
.rail .machine .mdot2.on{background:var(--s-done)}
|
|
99
|
+
.rlabel{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.13em;
|
|
100
|
+
color:var(--faint);margin:8px 6px 4px}
|
|
101
|
+
/* rail repo rows — #repoList + the add row(#repoAdd) 만 (라이브러리 캡처 .repo 는 건드리지 않음) */
|
|
102
|
+
#repoList .repo,.repo.add{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:8px;cursor:pointer;
|
|
103
|
+
color:var(--muted);font-family:var(--sans);font-size:13px;border:none;background:transparent;
|
|
104
|
+
position:relative;width:100%;text-align:left}
|
|
105
|
+
#repoList .repo:hover,.repo.add:hover{background:var(--surface2);color:var(--ink)}
|
|
106
|
+
#repoList .repo.on{background:var(--brand-dim);color:var(--ink)}
|
|
107
|
+
#repoList .repo .ic,.repo.add .ic{width:15px;height:15px;color:var(--faint)}
|
|
108
|
+
#repoList .repo.on .ic{color:var(--brand)}
|
|
109
|
+
#repoList .repo .nm,.repo.add .nm{flex:1;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:inherit}
|
|
110
|
+
#repoList .repo .cnt{font-family:var(--mono);font-size:10px;color:var(--brand);background:var(--brand-dim);
|
|
111
|
+
border-radius:99px;padding:1px 7px;flex:none}
|
|
112
|
+
#repoList .repo .attn{width:7px;height:7px;border-radius:50%;background:var(--s-failed);flex:none;
|
|
113
|
+
box-shadow:0 0 0 2px var(--surface)}
|
|
114
|
+
.repo.add{color:var(--faint);font-size:12px}
|
|
115
|
+
#repoList .repo .rowmenu{display:none;align-items:center;gap:2px;flex:none}
|
|
116
|
+
#repoList .repo:hover .rowmenu{display:flex}
|
|
117
|
+
#repoList .repo:hover .cnt{display:none}
|
|
118
|
+
#repoList .repo .rmbtn{background:none;border:none;color:var(--faint);cursor:pointer;padding:2px;
|
|
119
|
+
display:inline-flex;border-radius:5px}
|
|
120
|
+
#repoList .repo .rmbtn:hover{color:var(--ink);background:var(--line)}
|
|
121
|
+
#repoList .repo .rmbtn .ic{width:13px;height:13px;color:inherit}
|
|
122
|
+
.navi{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:8px;color:var(--muted);
|
|
123
|
+
cursor:pointer;font-size:13px;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
|
|
124
|
+
.navi:hover{background:var(--surface2)}
|
|
125
|
+
.navi.on{background:var(--surface2);color:var(--ink)}
|
|
126
|
+
.navi .ic{width:15px;height:15px}
|
|
127
|
+
.navi.on .ic{color:var(--brand)}
|
|
128
|
+
.navi .nsp{flex:1}
|
|
129
|
+
.navi .n{font-family:var(--mono);font-size:10.5px;color:var(--faint)}
|
|
130
|
+
.railsp{flex:1;min-height:8px}
|
|
131
|
+
.newbtn{display:flex;align-items:center;justify-content:center;gap:7px;background:var(--brand);
|
|
132
|
+
color:var(--brand-ink);border:none;border-radius:var(--r-ctl);font-family:var(--sans);font-weight:700;
|
|
133
|
+
font-size:13px;padding:10px;cursor:pointer;margin-top:8px}
|
|
134
|
+
.newbtn:hover{filter:brightness(1.08)}
|
|
135
|
+
.newbtn[disabled]{opacity:.4;cursor:not-allowed;filter:none}
|
|
136
|
+
.newbtn .ic{width:16px;height:16px}
|
|
137
|
+
.newnote{font-family:var(--mono);font-size:9.5px;color:var(--faint);text-align:center;margin-top:5px}
|
|
138
|
+
/* mobile FAB (v5.0 pocket board) — floating + opens the compose sheet; hidden on desktop */
|
|
139
|
+
.fab{display:none;position:fixed;z-index:28;right:18px;
|
|
140
|
+
right:calc(18px + env(safe-area-inset-right));bottom:20px;bottom:calc(20px + env(safe-area-inset-bottom));
|
|
141
|
+
width:52px;height:52px;border-radius:50%;border:none;background:var(--brand);color:var(--brand-ink);
|
|
142
|
+
box-shadow:0 6px 20px rgba(0,0,0,.4);cursor:pointer;align-items:center;justify-content:center}
|
|
143
|
+
.fab:active{filter:brightness(.94)}
|
|
144
|
+
.fab .ic{width:24px;height:24px}
|
|
145
|
+
/* machine menu (native select 대체) — 레일 컨텍스트 앵커 */
|
|
146
|
+
.mmenu{position:absolute;z-index:45;min-width:170px;background:var(--surface2);border:1px solid var(--line-hi);
|
|
147
|
+
border-radius:9px;box-shadow:var(--shadow);padding:4px;display:none}
|
|
148
|
+
.mmenu.open{display:block}
|
|
149
|
+
.mmenu .mopt{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;
|
|
150
|
+
font-family:var(--mono);font-size:12px;color:var(--muted);cursor:pointer;white-space:nowrap}
|
|
151
|
+
.mmenu .mopt:hover{background:var(--surface);color:var(--ink)}
|
|
152
|
+
.mmenu .mopt.on{color:var(--brand)}
|
|
153
|
+
.mmenu .mopt .mdot2{width:6px;height:6px;border-radius:50%;background:var(--faint);flex:none}
|
|
154
|
+
.mmenu .mopt .mdot2.on{background:var(--s-done)}
|
|
155
|
+
/* ── New sheet (v5.0 Part B — focused compose Task|Goal|Workbench) ── */
|
|
156
|
+
.sheet{width:min(400px,94vw);max-height:88vh;background:var(--surface);
|
|
157
|
+
border:1px solid var(--line-hi);border-radius:14px;box-shadow:var(--shadow);
|
|
158
|
+
display:flex;flex-direction:column;gap:13px;padding:16px 18px 16px}
|
|
159
|
+
.sheet-h{display:flex;align-items:center;gap:9px;font-family:var(--mono);font-size:12px}
|
|
160
|
+
.sheet-h .t{color:var(--ink);font-weight:700}.sheet-h .r{color:var(--faint);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
161
|
+
.sheet-h .sp{flex:1}.sheet-h .x{color:var(--muted);cursor:pointer;background:none;border:none;padding:2px}
|
|
162
|
+
.sheet #taskForm{display:flex;flex-direction:column;gap:13px;min-height:0}
|
|
163
|
+
.sheet-body{display:flex;flex-direction:column;gap:8px;overflow-y:auto;min-height:0;max-height:60vh;padding:1px}
|
|
164
|
+
.sheet-f{display:flex;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--line)}
|
|
165
|
+
.sheet-f #modeSeg{flex:1}
|
|
166
|
+
.sheet-f #runFleetBtn{flex:0 0 auto}
|
|
167
|
+
/* progressive Options reveal (rarely-used Task fields) */
|
|
168
|
+
.opt{border:1px solid var(--line);border-radius:var(--r-ctl);background:#0e1118;overflow:hidden}
|
|
169
|
+
.opt-h{width:100%;display:flex;align-items:center;gap:8px;background:transparent;border:none;cursor:pointer;
|
|
170
|
+
padding:8px 10px;font-size:12px;font-weight:600;color:var(--muted);text-align:left}
|
|
171
|
+
.opt-h:hover{color:var(--ink)}
|
|
172
|
+
.opt-h .opt-car{width:14px;height:14px;color:var(--faint);transform:rotate(0deg);transition:transform .16s}
|
|
173
|
+
.opt-h[aria-expanded="true"] .opt-car{transform:rotate(90deg)}
|
|
174
|
+
.opt-h .opt-sub{margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--faint);font-weight:500}
|
|
175
|
+
.opt-b{display:flex;flex-direction:column;gap:8px;padding:2px 10px 11px}
|
|
176
|
+
|
|
83
177
|
/* ── repo browser ── */
|
|
84
178
|
.brw{width:min(560px,94vw);max-height:78vh;background:var(--surface);border:1px solid var(--line);
|
|
85
179
|
border-radius:14px;box-shadow:var(--shadow);display:flex;flex-direction:column;overflow:hidden}
|
|
@@ -400,7 +494,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
400
494
|
.rmt-tbl th{color:var(--faint);font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.1em}
|
|
401
495
|
.rmt-tbl code{font-family:var(--mono);font-size:10.5px;color:var(--brand);white-space:nowrap}
|
|
402
496
|
.rmt-tbl .star{color:var(--brand)}
|
|
403
|
-
/* header
|
|
497
|
+
/* header remote-access affordance shares the ghost-button look */
|
|
404
498
|
|
|
405
499
|
/* ── toasts ─────────────────────────────── */
|
|
406
500
|
.toasts{position:fixed;top:66px;right:18px;z-index:60;display:flex;flex-direction:column;gap:8px;
|
|
@@ -476,8 +570,8 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
476
570
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
477
571
|
|
|
478
572
|
/* ── AI review panel (compare) ── */
|
|
479
|
-
.cmp-review{
|
|
480
|
-
max-
|
|
573
|
+
.cmp-review{flex:1;min-height:0;overflow:auto;background:var(--surface2);
|
|
574
|
+
padding:20px max(20px,calc((100% - 880px)/2));font-size:13.5px;line-height:1.7;color:var(--muted)}
|
|
481
575
|
.cmp-review[hidden]{display:none}
|
|
482
576
|
.cmp-review h2{font-size:13px;color:var(--brand);margin:14px 0 6px;letter-spacing:.02em}
|
|
483
577
|
.cmp-review h3{font-size:12.5px;color:var(--ink);margin:12px 0 4px}
|
|
@@ -505,7 +599,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
505
599
|
#outDetail .dl-line{cursor:pointer;border-radius:3px}
|
|
506
600
|
#outDetail .dl-line:hover{background:rgba(78,201,176,.09)}
|
|
507
601
|
|
|
508
|
-
/* ── v4.7
|
|
602
|
+
/* ── v4.7 outputs contract (contract strip) — required-outputs summary under the modal header ── */
|
|
509
603
|
.contract{display:flex;flex-wrap:wrap;align-items:center;gap:7px;padding:9px 18px;
|
|
510
604
|
border-bottom:1px solid var(--line);background:var(--surface2);
|
|
511
605
|
font-family:var(--mono);font-size:10.5px;color:var(--faint);line-height:1.5}
|
|
@@ -529,6 +623,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
529
623
|
.ocard.miss{opacity:.72;border-style:dashed;cursor:default}
|
|
530
624
|
.ocard.miss:hover{transform:none;border-color:var(--line)}
|
|
531
625
|
.ocard .og{font-size:15px;line-height:1;flex:0 0 auto;width:20px;text-align:center;color:var(--muted)}
|
|
626
|
+
.ocard.miss .og{color:var(--s-preparing)}
|
|
532
627
|
.ocard .ob{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
533
628
|
.ocard .ot{font-family:var(--sans);font-size:12.5px;color:var(--ink);font-weight:600;
|
|
534
629
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -577,11 +672,14 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
577
672
|
.layout{grid-template-columns:1fr;min-height:calc(100dvh - 55px)}
|
|
578
673
|
/* 사이드바 = 오프캔버스 드로어 — 플릿이 첫 화면, 런처는 ☰ 뒤에 */
|
|
579
674
|
.menu-btn{display:inline-flex}
|
|
580
|
-
aside{position:fixed;top:54px;bottom:0;left:0;width:min(86vw,340px);z-index:30;
|
|
581
|
-
transform:translateX(-103%);transition:transform .22s ease;overflow-y:auto;
|
|
675
|
+
aside{position:fixed;top:54px;bottom:0;left:0;width:min(86vw,340px);max-width:100vw;z-index:30;
|
|
676
|
+
transform:translateX(-103%);transition:transform .22s ease;overflow-y:auto;overflow-x:hidden;
|
|
582
677
|
-webkit-overflow-scrolling:touch;box-shadow:var(--shadow)}
|
|
583
678
|
aside.open{transform:translateX(0)}
|
|
584
679
|
.scrim.on{display:block}
|
|
680
|
+
/* pocket board — drawer is JUST the navigator; the + FAB opens the compose sheet */
|
|
681
|
+
.fab{display:inline-flex}
|
|
682
|
+
#newBtn,.newnote{display:none}
|
|
585
683
|
header{padding:0 12px;gap:9px}
|
|
586
684
|
.brand .sub,.daemon-badge,.machines{display:none}
|
|
587
685
|
main{padding:14px}
|
|
@@ -603,37 +701,88 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
603
701
|
.cmp-col{min-width:0;border-right:none;border-bottom:1px solid var(--line);flex:0 0 auto;max-height:72vh}
|
|
604
702
|
}
|
|
605
703
|
@media (prefers-reduced-motion:reduce){aside{transition:none}}
|
|
704
|
+
${ICON_CSS}
|
|
606
705
|
</style>
|
|
607
706
|
</head>
|
|
608
707
|
<body>
|
|
708
|
+
${ICON_SPRITE}
|
|
609
709
|
<header>
|
|
610
710
|
<button class="btn-ghost sm menu-btn" id="menuBtn" aria-label="open launcher">☰</button>
|
|
611
711
|
<div class="brand"><span class="mark">coxpit</span><span class="sub">fleet console</span></div>
|
|
612
712
|
<span class="daemon-badge" id="daemonBadge" style="display:none"></span>
|
|
613
|
-
<div class="seg view-seg" id="viewSeg">
|
|
614
|
-
<button type="button" class="seg-opt on" data-view="active">Active</button>
|
|
615
|
-
<button type="button" class="seg-opt" data-view="archive">Archive <span id="archN" class="seg-hint"></span></button>
|
|
616
|
-
</div>
|
|
617
713
|
<div class="ws"><span class="dot" id="wsdot"></span><span id="wstext">connecting</span></div>
|
|
618
|
-
<button class="btn-ghost sm" id="bell" title="notify when a run settles"
|
|
619
|
-
<button class="btn-ghost sm" id="remoteBtn" title="reach this daemon from elsewhere (Tailscale · recipes)"
|
|
714
|
+
<button class="btn-ghost sm" id="bell" title="notify when a run settles"><svg class="ic"><use href="#i-bell-off"/></svg></button>
|
|
715
|
+
<button class="btn-ghost sm" id="remoteBtn" title="reach this daemon from elsewhere (Tailscale · recipes)"><svg class="ic"><use href="#i-external-link"/></svg></button>
|
|
620
716
|
<div class="machines" id="machines"></div>
|
|
621
717
|
</header>
|
|
622
718
|
<div class="scrim" id="scrim"></div>
|
|
719
|
+
<!-- pocket board FAB (v5.0 Part C) — mobile-only; opens the compose sheet -->
|
|
720
|
+
<button type="button" class="fab" id="fab" aria-label="new task"><svg class="ic"><use href="#i-plus"/></svg></button>
|
|
623
721
|
<div class="layout">
|
|
624
|
-
<aside>
|
|
625
|
-
<
|
|
626
|
-
<
|
|
627
|
-
<
|
|
628
|
-
<
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
722
|
+
<aside class="rail">
|
|
723
|
+
<button type="button" class="machine" id="machineSwitch">
|
|
724
|
+
<svg class="ic"><use href="#i-server"/></svg>
|
|
725
|
+
<span class="msp" id="machineLbl">local</span>
|
|
726
|
+
<span class="mdot2" id="machineDot"></span>
|
|
727
|
+
</button>
|
|
728
|
+
<div class="mmenu" id="machineMenu"></div>
|
|
729
|
+
|
|
730
|
+
<p class="rlabel">Repositories</p>
|
|
731
|
+
<div id="repoList"></div>
|
|
732
|
+
<button type="button" class="repo add" id="repoAdd">
|
|
733
|
+
<svg class="ic"><use href="#i-plus"/></svg><span class="nm">Add repository…</span>
|
|
734
|
+
</button>
|
|
735
|
+
|
|
736
|
+
<p class="rlabel">View</p>
|
|
737
|
+
<nav id="viewNav">
|
|
738
|
+
<button type="button" class="navi on" data-view="active">
|
|
739
|
+
<svg class="ic"><use href="#i-layers"/></svg><span>Active</span><span class="nsp"></span><span class="n" id="navActiveN"></span>
|
|
740
|
+
</button>
|
|
741
|
+
<button type="button" class="navi" data-view="goals">
|
|
742
|
+
<svg class="ic"><use href="#i-target"/></svg><span>Goals</span><span class="nsp"></span><span class="n" id="navGoalsN"></span>
|
|
743
|
+
</button>
|
|
744
|
+
<button type="button" class="navi" data-view="archive">
|
|
745
|
+
<svg class="ic"><use href="#i-archive"/></svg><span>Archive</span><span class="nsp"></span><span class="n" id="navArchiveN"></span>
|
|
746
|
+
</button>
|
|
747
|
+
</nav>
|
|
748
|
+
|
|
749
|
+
<div class="railsp"></div>
|
|
750
|
+
<button type="button" class="newbtn" id="newBtn"><svg class="ic"><use href="#i-plus"/></svg> New</button>
|
|
751
|
+
<p class="newnote">Task · Goal · Workbench</p>
|
|
752
|
+
|
|
753
|
+
<details class="sect" id="libraryBox" style="margin-top:12px">
|
|
754
|
+
<summary class="sect-label" style="cursor:pointer;list-style:none">Library · design captures ▾</summary>
|
|
755
|
+
<div id="captures" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
|
|
756
|
+
<a id="bmk" class="btn-ghost sm" style="text-decoration:none;text-align:center;display:block;padding:6px;margin-top:6px"
|
|
757
|
+
title="drag me to your bookmarks bar, then click it on your running app">⌖ coxpit inspect</a>
|
|
758
|
+
<span style="font-size:11px;color:var(--faint)">Drag to bookmarks. Click it on your app, then click an element.
|
|
759
|
+
With auth on, append ?k=<pass> to the script URL.</span>
|
|
760
|
+
</details>
|
|
761
|
+
</aside>
|
|
762
|
+
|
|
763
|
+
<!-- New sheet (v5.0 Part B — focused compose: Task | Goal | Workbench) -->
|
|
764
|
+
<div class="overlay" id="newSheet">
|
|
765
|
+
<div class="sheet" id="sheetCard">
|
|
766
|
+
<div class="sheet-h"><svg class="ic" style="color:var(--brand)"><use href="#i-plus"/></svg>
|
|
767
|
+
<span class="t">New</span><span class="r" id="sheetRepoLbl"></span><span class="sp"></span>
|
|
768
|
+
<button type="button" class="x" id="sheetClose" aria-label="close"><svg class="ic"><use href="#i-x"/></svg></button></div>
|
|
769
|
+
|
|
770
|
+
<div class="seg" id="launchTabs" role="group" aria-label="launch type">
|
|
771
|
+
<button type="button" class="seg-opt on" data-tab="task">Task</button>
|
|
772
|
+
<button type="button" class="seg-opt" data-tab="goal">Goal</button>
|
|
773
|
+
<button type="button" class="seg-opt" data-tab="bench">Workbench</button>
|
|
774
|
+
</div>
|
|
775
|
+
|
|
776
|
+
<!-- hidden state selects (target machine/repo — driven by the rail + repo picker) -->
|
|
777
|
+
<select id="repoMachine" hidden></select>
|
|
778
|
+
<select id="taskRepo" hidden></select>
|
|
779
|
+
<!-- repo picker affordances (opened from the Add-repository rail button / handlers) -->
|
|
780
|
+
<div class="row" id="repoActions" hidden>
|
|
632
781
|
<button type="button" class="btn-ghost sm" id="repoBrowse" style="flex:1">Browse…</button>
|
|
633
782
|
<button type="button" class="btn-ghost sm" id="repoNew" style="flex:0 0 auto" title="start a new project — empty folder in, scaffolded repo out">New</button>
|
|
634
783
|
<button type="button" class="btn-ghost sm" id="repoManual" style="flex:0 0 auto" title="type an absolute path">Path</button>
|
|
635
|
-
<button type="button" class="btn-ghost sm" id="repoBranch" style="flex:0 0 auto" title="change the base branch — merges, Sync base and PRs all target it"
|
|
636
|
-
<button type="button" class="btn-ghost sm" id="repoRemove" style="flex:0 0 auto" title="remove selected repository from coxpit"
|
|
784
|
+
<button type="button" class="btn-ghost sm" id="repoBranch" style="flex:0 0 auto" title="change the base branch — merges, Sync base and PRs all target it"><svg class="ic"><use href="#i-branch"/></svg></button>
|
|
785
|
+
<button type="button" class="btn-ghost sm" id="repoRemove" style="flex:0 0 auto" title="remove selected repository from coxpit"><svg class="ic"><use href="#i-x"/></svg></button>
|
|
637
786
|
</div>
|
|
638
787
|
<form id="repoForm" hidden>
|
|
639
788
|
<div class="row">
|
|
@@ -641,68 +790,68 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
641
790
|
<button class="btn-ghost sm" type="submit" style="flex:0 0 auto">Register</button>
|
|
642
791
|
</div>
|
|
643
792
|
</form>
|
|
644
|
-
</div>
|
|
645
793
|
|
|
646
|
-
<div class="sect">
|
|
647
|
-
<p class="sect-label">Start</p>
|
|
648
|
-
<div class="seg" id="launchTabs">
|
|
649
|
-
<button type="button" class="seg-opt on" data-tab="task">Task</button>
|
|
650
|
-
<button type="button" class="seg-opt" data-tab="goal">Goal</button>
|
|
651
|
-
<button type="button" class="seg-opt" data-tab="bench">Workbench</button>
|
|
652
|
-
</div>
|
|
653
794
|
<form id="taskForm">
|
|
654
|
-
<div
|
|
655
|
-
<
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
<
|
|
660
|
-
|
|
795
|
+
<div class="sheet-body">
|
|
796
|
+
<div id="panelTask" style="display:flex;flex-direction:column;gap:8px">
|
|
797
|
+
<input id="taskTitle" placeholder="Task title" />
|
|
798
|
+
<textarea id="taskPrompt" placeholder="Prompt — target files, constraints, how to verify"></textarea>
|
|
799
|
+
<button type="button" class="btn-ghost sm" id="ghImport">From GitHub issue / PR…</button>
|
|
800
|
+
<div class="seg" id="provSeg" role="group" aria-label="agent provider">
|
|
801
|
+
<button type="button" class="seg-opt on" data-agent="claude-code">Claude</button>
|
|
802
|
+
<button type="button" class="seg-opt" data-agent="codex">Codex</button>
|
|
803
|
+
</div>
|
|
804
|
+
<div class="opt" id="taskOpt">
|
|
805
|
+
<button type="button" class="opt-h" id="taskOptToggle" aria-expanded="false">
|
|
806
|
+
<svg class="ic opt-car"><use href="#i-chevron"/></svg>
|
|
807
|
+
<span>Options</span><span class="opt-sub">model · design · deliverables</span>
|
|
808
|
+
</button>
|
|
809
|
+
<div class="opt-b" id="taskOptBody" hidden>
|
|
810
|
+
<p class="flabel">model · optional</p>
|
|
811
|
+
<input id="taskModel" placeholder="CLI default" list="modelHist" autocomplete="off" />
|
|
812
|
+
<datalist id="modelHist"></datalist>
|
|
813
|
+
<p class="flabel">design capture · optional</p>
|
|
814
|
+
<select id="taskCapture"><option value="">no design capture</option></select>
|
|
815
|
+
<p class="flabel">deliverables · optional (contract)</p>
|
|
816
|
+
<div class="ochips" id="taskOutputs" role="group" aria-label="declared deliverables">
|
|
817
|
+
<button type="button" class="ochip" data-out="answer">Answer</button>
|
|
818
|
+
<button type="button" class="ochip" data-out="code">Code</button>
|
|
819
|
+
<button type="button" class="ochip" data-out="doc">Doc</button>
|
|
820
|
+
<button type="button" class="ochip" data-out="page">Page</button>
|
|
821
|
+
<button type="button" class="ochip" data-out="file">File</button>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
661
825
|
</div>
|
|
662
|
-
<
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
<
|
|
671
|
-
<
|
|
672
|
-
<button type="button" class="ochip" data-out="page">페이지</button>
|
|
673
|
-
<button type="button" class="ochip" data-out="file">파일</button>
|
|
826
|
+
<div id="panelGoal" hidden style="flex-direction:column;gap:8px">
|
|
827
|
+
<textarea id="planGoal" placeholder="One goal — a planner agent reads the repo, splits it into independent tasks, and launches them all. Converge with Select runs → Integrate."></textarea>
|
|
828
|
+
<div class="seg" id="provSegGoal" role="group" aria-label="agent provider">
|
|
829
|
+
<button type="button" class="seg-opt on" data-agent="claude-code">Claude</button>
|
|
830
|
+
<button type="button" class="seg-opt" data-agent="codex">Codex</button>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
<div id="panelBench" hidden style="flex-direction:column;gap:8px">
|
|
834
|
+
<input id="benchTitle" placeholder="Workbench name · optional" />
|
|
835
|
+
<p style="font-size:11.5px;color:var(--faint);margin:0;line-height:1.55">Isolated worktree + terminal. Work interactively — run <span style="color:var(--brand);font-family:var(--mono)">claude</span> inside, take hours — then decide the merge from the card.</p>
|
|
674
836
|
</div>
|
|
675
837
|
</div>
|
|
676
|
-
<div
|
|
677
|
-
<
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
<
|
|
682
|
-
</div>
|
|
683
|
-
<div class="seg" id="modeSeg" role="group" aria-label="agent mode">
|
|
684
|
-
<button type="button" class="seg-opt" data-real="0">Dry run</button>
|
|
685
|
-
<button type="button" class="seg-opt" data-real="1">Real agent<span class="seg-hint">spends credits</span></button>
|
|
686
|
-
</div>
|
|
687
|
-
<input type="checkbox" id="taskReal" hidden />
|
|
688
|
-
<div class="row">
|
|
838
|
+
<div class="sheet-f" id="sheetFooter">
|
|
839
|
+
<div class="seg" id="modeSeg" role="group" aria-label="agent mode">
|
|
840
|
+
<button type="button" class="seg-opt" data-real="0">Dry run</button>
|
|
841
|
+
<button type="button" class="seg-opt" data-real="1">Real agent<span class="seg-hint">spends credits</span></button>
|
|
842
|
+
</div>
|
|
843
|
+
<input type="checkbox" id="taskReal" hidden />
|
|
689
844
|
<input id="taskCount" class="narrow" type="number" min="1" max="8" value="1" title="agents — 1 for a job, N to explore variants" />
|
|
690
|
-
<button class="btn" type="submit" id="runFleetBtn">Run fleet</button>
|
|
845
|
+
<button class="btn" type="submit" id="runFleetBtn"><svg class="ic"><use href="#i-play"/></svg> Run fleet</button>
|
|
691
846
|
</div>
|
|
692
847
|
</form>
|
|
693
848
|
</div>
|
|
694
|
-
|
|
695
|
-
<details class="sect" id="libraryBox">
|
|
696
|
-
<summary class="sect-label" style="cursor:pointer;list-style:none">Library · design captures ▾</summary>
|
|
697
|
-
<div id="captures" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
|
|
698
|
-
<a id="bmk" class="btn-ghost sm" style="text-decoration:none;text-align:center;display:block;padding:6px;margin-top:6px"
|
|
699
|
-
title="drag me to your bookmarks bar, then click it on your running app">⌖ coxpit inspect</a>
|
|
700
|
-
<span style="font-size:11px;color:var(--faint)">Drag to bookmarks. Click it on your app, then click an element.
|
|
701
|
-
With auth on, append ?k=<pass> to the script URL.</span>
|
|
702
|
-
</details>
|
|
703
|
-
</aside>
|
|
849
|
+
</div>
|
|
704
850
|
<main>
|
|
705
|
-
<div class="toolbar"
|
|
851
|
+
<div class="toolbar" style="display:flex;align-items:center;gap:12px">
|
|
852
|
+
<span id="boardHint" style="font-family:var(--mono);font-size:11px;color:var(--faint)"></span>
|
|
853
|
+
<span style="flex:1"></span>
|
|
854
|
+
<button class="btn-ghost sm" id="selToggle">Select runs</button></div>
|
|
706
855
|
<div class="grid" id="grid"></div>
|
|
707
856
|
<div class="empty" id="empty">
|
|
708
857
|
<span class="glyph">▚▞▚</span>
|
|
@@ -714,7 +863,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
714
863
|
<input id="archQ" placeholder="search title…" autocomplete="off" />
|
|
715
864
|
<select id="archRepo"><option value="">all repos</option></select>
|
|
716
865
|
<button type="button" class="btn-ghost sm" id="reclaimBtn" hidden
|
|
717
|
-
title="remove worktrees left by cleaned/failed runs — reclaims disk (active work untouched)"
|
|
866
|
+
title="remove worktrees left by cleaned/failed runs — reclaims disk (active work untouched)"><svg class="ic"><use href="#i-recycle"/></svg> Reclaim <span id="reclaimHint"></span></button>
|
|
718
867
|
</div>
|
|
719
868
|
<div id="archList"></div>
|
|
720
869
|
<div style="text-align:center;margin-top:14px"><button class="btn-ghost sm" id="archMore" hidden>load 50 more</button></div>
|
|
@@ -728,7 +877,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
728
877
|
<span class="rid" id="mRid"></span>
|
|
729
878
|
<span class="title" id="mTitle"></span>
|
|
730
879
|
<span class="chip" id="mChip"><i></i><span id="mChipTxt"></span></span>
|
|
731
|
-
<button class="x" id="mClose" aria-label="close"
|
|
880
|
+
<button class="x" id="mClose" aria-label="close"><svg class="ic"><use href="#i-x"/></svg></button>
|
|
732
881
|
</div>
|
|
733
882
|
<div class="contract" id="mContract" hidden></div>
|
|
734
883
|
<div class="modal-b">
|
|
@@ -741,7 +890,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
741
890
|
<div id="outWrap">
|
|
742
891
|
<div id="outCards"><div class="ocards-empty">loading…</div></div>
|
|
743
892
|
<div id="outDetail">
|
|
744
|
-
<div class="oback" id="outBack">‹
|
|
893
|
+
<div class="oback" id="outBack">‹ Outputs</div>
|
|
745
894
|
<div class="odetail-c" id="outDetailC"></div>
|
|
746
895
|
</div>
|
|
747
896
|
</div>
|
|
@@ -753,14 +902,14 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
753
902
|
<button type="button" class="seg-opt" data-mode="ask">Ask</button>
|
|
754
903
|
</div>
|
|
755
904
|
<input id="steerInput" placeholder="Next instruction — same session & worktree…" style="flex:1" />
|
|
756
|
-
<button class="btn sm" id="steerSend">Send</button>
|
|
905
|
+
<button class="btn sm" id="steerSend"><svg class="ic"><use href="#i-pencil"/></svg> Send</button>
|
|
757
906
|
</div>
|
|
758
907
|
<div class="modal-f">
|
|
759
|
-
<button class="btn-ghost sm" id="mTerm">Terminal</button>
|
|
760
|
-
<button class="btn-ghost sm" id="mRefreshDiff">Refresh outputs</button>
|
|
908
|
+
<button class="btn-ghost sm" id="mTerm"><svg class="ic"><use href="#i-terminal"/></svg> Terminal</button>
|
|
909
|
+
<button class="btn-ghost sm" id="mRefreshDiff"><svg class="ic"><use href="#i-refresh"/></svg> Refresh outputs</button>
|
|
761
910
|
<button class="btn-ghost sm" id="mCompare">Compare runs</button>
|
|
762
|
-
<button class="btn-ghost sm" id="mExport">Export files…</button>
|
|
763
|
-
<button class="btn-ghost sm" id="mSync">Sync base</button>
|
|
911
|
+
<button class="btn-ghost sm" id="mExport"><svg class="ic"><use href="#i-download"/></svg> Export files…</button>
|
|
912
|
+
<button class="btn-ghost sm" id="mSync"><svg class="ic"><use href="#i-branch"/></svg> Sync base</button>
|
|
764
913
|
<button class="btn-ghost sm" id="mShare" title="create a read-only share link (no auth, snapshot view)">Share</button>
|
|
765
914
|
<span class="spacer"></span>
|
|
766
915
|
<button class="btn-danger sm" id="mStop">Stop</button>
|
|
@@ -776,15 +925,15 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
776
925
|
<span class="rh-glyph">⌒</span>
|
|
777
926
|
<span class="rh-t" id="roomTitle">Goal</span>
|
|
778
927
|
<span class="rh-n" id="roomCount"></span>
|
|
779
|
-
<button class="x" id="roomClose" aria-label="close"
|
|
928
|
+
<button class="x" id="roomClose" aria-label="close"><svg class="ic"><use href="#i-x"/></svg></button>
|
|
780
929
|
</div>
|
|
781
930
|
<div class="chips" id="roomChips"></div>
|
|
782
931
|
<div class="gbar" id="roomGbar">
|
|
783
932
|
<span class="selc" id="roomSelC">☑ 0 selected</span>
|
|
784
|
-
<button type="button" class="brand" id="roomIntegrateSel">Integrate
|
|
933
|
+
<button type="button" class="brand" id="roomIntegrateSel">Integrate selected (0)</button>
|
|
785
934
|
<span class="sp"></span>
|
|
786
|
-
<button type="button" id="roomReviewAll"
|
|
787
|
-
<button type="button" class="danger" id="roomGroupClose"
|
|
935
|
+
<button type="button" id="roomReviewAll">Review all</button>
|
|
936
|
+
<button type="button" class="danger" id="roomGroupClose">Close group</button>
|
|
788
937
|
</div>
|
|
789
938
|
<div class="body">
|
|
790
939
|
<div class="runs" id="roomRuns"></div>
|
|
@@ -798,7 +947,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
798
947
|
<div class="comp-hint" id="roomHint"></div>
|
|
799
948
|
<textarea id="roomInput" placeholder="New attempt prompt, or a broadcast to the settled runs…"></textarea>
|
|
800
949
|
<div class="verbs" id="roomVerbs">
|
|
801
|
-
<button type="button" class="btn-ghost sm" id="roomNew"
|
|
950
|
+
<button type="button" class="btn-ghost sm" id="roomNew"><svg class="ic"><use href="#i-plus"/></svg> New attempt</button>
|
|
802
951
|
<button type="button" class="btn-ghost sm" id="roomBroadcast">→ Broadcast</button>
|
|
803
952
|
<span class="grow"></span>
|
|
804
953
|
<div class="conv-menu" id="roomConvMenu">
|
|
@@ -824,7 +973,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
824
973
|
<button class="btn sm" id="cmpAI">AI review</button>
|
|
825
974
|
<button class="btn-ghost sm" id="cmpDocsTgl">Rendered</button>
|
|
826
975
|
<button class="btn-ghost sm" id="cmpRefresh">Refresh</button>
|
|
827
|
-
<button class="x" id="cmpClose" aria-label="close"
|
|
976
|
+
<button class="x" id="cmpClose" aria-label="close"><svg class="ic"><use href="#i-x"/></svg></button>
|
|
828
977
|
</div>
|
|
829
978
|
<div class="cmp-review" id="cmpReview" hidden></div>
|
|
830
979
|
<div class="cmp" id="cmpBody"></div>
|
|
@@ -838,7 +987,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
838
987
|
<span class="title" id="termTitle">terminal</span>
|
|
839
988
|
<div class="term-tabs" id="termTabs"></div>
|
|
840
989
|
<span class="term-hint">tmux session · Ctrl-b d detaches · Esc closes</span>
|
|
841
|
-
<button class="x" id="termClose" aria-label="close"
|
|
990
|
+
<button class="x" id="termClose" aria-label="close"><svg class="ic"><use href="#i-x"/></svg></button>
|
|
842
991
|
</div>
|
|
843
992
|
<div class="term-body"><div id="xterm"></div></div>
|
|
844
993
|
<div class="term-ibar" id="termIbar">
|
|
@@ -860,12 +1009,12 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
860
1009
|
<button class="btn-ghost sm" id="brwUp">↑ Up</button>
|
|
861
1010
|
<button class="btn-ghost sm" id="brwHome">Home</button>
|
|
862
1011
|
<span class="brw-path" id="brwPath"></span>
|
|
863
|
-
<button class="x" id="brwClose" aria-label="close"
|
|
1012
|
+
<button class="x" id="brwClose" aria-label="close"><svg class="ic"><use href="#i-x"/></svg></button>
|
|
864
1013
|
</div>
|
|
865
1014
|
<div class="brw-list" id="brwList"></div>
|
|
866
1015
|
<div class="brw-f">
|
|
867
1016
|
<span class="hint"><span style="color:var(--brand)">git</span> badge = repo (Register) · empty folder = Start here</span>
|
|
868
|
-
<button class="btn-ghost sm" id="brwNewFolder"
|
|
1017
|
+
<button class="btn-ghost sm" id="brwNewFolder"><svg class="ic"><use href="#i-plus"/></svg> New folder here</button>
|
|
869
1018
|
<button class="btn sm" id="brwRegHere" style="display:none">Register this folder</button>
|
|
870
1019
|
</div>
|
|
871
1020
|
<form class="brw-f" id="brwNewForm" hidden>
|
|
@@ -977,19 +1126,23 @@ const runs = new Map(); // runId -> run object
|
|
|
977
1126
|
const tasks = new Map(); // taskId -> task
|
|
978
1127
|
const groups = new Map(); // groupId -> {id, kind, title}
|
|
979
1128
|
let repos = [], machines = [], captures = [];
|
|
1129
|
+
// v5.0 rail — 선택된 repo 로 보드 스코프(client-side). null = All repositories.
|
|
1130
|
+
let selectedRepo = null;
|
|
1131
|
+
try { const s = localStorage.getItem('coxpit.repo'); selectedRepo = s ? Number(s) : null; } catch {}
|
|
980
1132
|
let daemonPort = 8210; // real config.port — filled from /api/fleet daemon block (recipes interpolate it)
|
|
981
1133
|
let remoteAuthOpen = false; // true = no password → Funnel guard on (from /api/fleet daemon.authOpen)
|
|
982
1134
|
|
|
983
1135
|
const $ = (id) => document.getElementById(id);
|
|
984
1136
|
const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c]));
|
|
985
1137
|
const escA = (s) => esc(s).replace(/"/g, '"');
|
|
1138
|
+
${ICON_JS_HELPER} // ic('x') → '<svg class="ic"><use href="#i-x"/></svg>' (Lucide 스프라이트)
|
|
986
1139
|
const statusColor = (s) => 'var(--s-' + (s||'pending') + ', var(--muted))';
|
|
987
1140
|
|
|
988
1141
|
/* ── custom toast / confirm (시스템 alert·confirm 대체) ── */
|
|
989
1142
|
function toast(msg, kind){
|
|
990
1143
|
const el = document.createElement('div');
|
|
991
1144
|
el.className = 'toast ' + (kind==='error'?'err':kind==='ok'?'ok':'');
|
|
992
|
-
el.innerHTML = '<span class="tk">'+(kind==='error'?'
|
|
1145
|
+
el.innerHTML = '<span class="tk">'+(kind==='error'?ic('x'):kind==='ok'?ic('check'):'·')+'</span><span>'+esc(msg)+'</span>';
|
|
993
1146
|
$('toasts').appendChild(el);
|
|
994
1147
|
setTimeout(()=>{ el.style.opacity='0'; el.style.transition='opacity .25s'; setTimeout(()=>el.remove(),260); }, 4200);
|
|
995
1148
|
}
|
|
@@ -1041,7 +1194,7 @@ async function brwGo(p){
|
|
|
1041
1194
|
$('brwList').innerHTML =
|
|
1042
1195
|
(d.error ? '<div class="brw-row"><span class="nm" style="color:var(--s-failed)">'+esc(d.error)+'</span></div>' : '')
|
|
1043
1196
|
+ (d.dirs.map(x =>
|
|
1044
|
-
'<div class="brw-row" data-n="'+esc(x.name)+'"><span class="ico"
|
|
1197
|
+
'<div class="brw-row" data-n="'+esc(x.name)+'"><span class="ico">'+ic('folder')+'</span><span class="nm">'+esc(x.name)+'</span>'
|
|
1045
1198
|
+ (x.isRepo ? '<span class="gitchip">git</span><button type="button" class="btn sm" data-reg="'+esc(x.name)+'">Register</button>'
|
|
1046
1199
|
: x.isEmpty ? '<button type="button" class="btn-ghost sm" data-start="'+esc(x.name)+'">Start here</button>' : '')
|
|
1047
1200
|
+ '</div>').join('')
|
|
@@ -1249,11 +1402,16 @@ function bandHTML(g, grpRuns){
|
|
|
1249
1402
|
+ '<button class="gband-fold" data-gfold="'+g.id+'" title="fold">'+(folded?'▸':'▾')+'</button></div>'
|
|
1250
1403
|
+ '<div class="gband-grid">'+cards+'</div></div>';
|
|
1251
1404
|
}
|
|
1405
|
+
// v5.0 — 레일에서 고른 repo 로 run 을 스코프(client-side). selectedRepo=null → 전체.
|
|
1406
|
+
function runInScope(r){
|
|
1407
|
+
if (selectedRepo==null) return true;
|
|
1408
|
+
const t = tasks.get(r.taskId);
|
|
1409
|
+
return t ? t.repoId===selectedRepo : false;
|
|
1410
|
+
}
|
|
1252
1411
|
function render(){
|
|
1253
1412
|
if (boardView==='archive') return; // 아카이브 뷰는 자체 리스트 — grid 안 건드림
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
if (!list.length){ paintOnboarding(); $('grid').innerHTML=''; return; }
|
|
1413
|
+
const goalsOnly = boardView==='goals'; // Goals 뷰 = 그룹 밴드만
|
|
1414
|
+
const list = [...runs.values()].filter(runInScope).sort((a,b)=>b.id-a.id);
|
|
1257
1415
|
// 그룹 파티션 — grouped run 은 밴드로 클러스터, ungrouped 는 뒤에 flat.
|
|
1258
1416
|
const byGroup = new Map(); const flat = [];
|
|
1259
1417
|
for (const r of list){
|
|
@@ -1262,12 +1420,25 @@ function render(){
|
|
|
1262
1420
|
if (gid==null) flat.push(r);
|
|
1263
1421
|
else { if(!byGroup.has(gid)) byGroup.set(gid, []); byGroup.get(gid).push(r); }
|
|
1264
1422
|
}
|
|
1423
|
+
const visible = goalsOnly ? byGroup.size : list.length;
|
|
1424
|
+
$('empty').style.display = visible ? 'none' : 'flex';
|
|
1425
|
+
renderRail(); // 레일 카운트 배지·attention 을 라이브로 (paintNavCounts·boardHint 포함)
|
|
1426
|
+
if (!visible){ paintOnboarding(goalsOnly); $('grid').innerHTML=''; return; }
|
|
1265
1427
|
let html = '';
|
|
1266
1428
|
for (const gid of [...byGroup.keys()].sort((a,b)=>b-a)) html += bandHTML(groups.get(gid), byGroup.get(gid));
|
|
1267
|
-
html += flat.map(cardHTML).join('');
|
|
1429
|
+
if (!goalsOnly) html += flat.map(cardHTML).join('');
|
|
1268
1430
|
$('grid').innerHTML = html;
|
|
1431
|
+
paintBoardHint();
|
|
1269
1432
|
if (termRunId!=null) termTabsRender(); // 터미널 열려있으면 세션 탭도 동기화
|
|
1270
1433
|
}
|
|
1434
|
+
// 보드 상단 힌트: <repo> · <view> · N runs (스코프 반영)
|
|
1435
|
+
function paintBoardHint(){
|
|
1436
|
+
const el = $('boardHint'); if(!el) return;
|
|
1437
|
+
const scope = selectedRepo!=null ? (repos.find(r=>r.id===selectedRepo)||{}).name || 'repo' : 'All repositories';
|
|
1438
|
+
const label = boardView==='goals' ? 'Goals' : 'Active';
|
|
1439
|
+
const n = [...runs.values()].filter(runInScope).length;
|
|
1440
|
+
el.textContent = scope + ' · ' + label + ' · ' + n + ' run' + (n===1?'':'s');
|
|
1441
|
+
}
|
|
1271
1442
|
|
|
1272
1443
|
/* ── active / archive view ─────────────────────────────── */
|
|
1273
1444
|
let boardView = 'active';
|
|
@@ -1275,7 +1446,7 @@ let archOffset = 0, archTotal = 0;
|
|
|
1275
1446
|
const ARCH_LIMIT = 50;
|
|
1276
1447
|
function setView(v){
|
|
1277
1448
|
boardView = v;
|
|
1278
|
-
document.querySelectorAll('#
|
|
1449
|
+
document.querySelectorAll('#viewNav .navi').forEach(b=>{
|
|
1279
1450
|
const on = b.dataset.view===v;
|
|
1280
1451
|
b.classList.toggle('on', on); b.setAttribute('aria-pressed', on?'true':'false');
|
|
1281
1452
|
});
|
|
@@ -1283,11 +1454,19 @@ function setView(v){
|
|
|
1283
1454
|
$('archive').hidden = !archive;
|
|
1284
1455
|
$('grid').style.display = archive ? 'none' : '';
|
|
1285
1456
|
$('empty').style.display = archive ? 'none' : ($('grid').innerHTML ? 'none' : 'flex');
|
|
1286
|
-
document.querySelector('.toolbar').style.display = archive ? 'none' : '';
|
|
1287
|
-
if (archive){ paintArchRepos(); archFetch(true); reclaimRefresh(); }
|
|
1457
|
+
document.querySelector('.toolbar').style.display = archive ? 'none' : 'flex';
|
|
1458
|
+
if (archive){ paintArchRepos(); $('archRepo').value = selectedRepo!=null ? String(selectedRepo) : ''; archFetch(true); reclaimRefresh(); }
|
|
1288
1459
|
else render();
|
|
1289
1460
|
}
|
|
1290
|
-
document.querySelectorAll('#
|
|
1461
|
+
document.querySelectorAll('#viewNav .navi').forEach(b=>b.addEventListener('click', ()=>setView(b.dataset.view)));
|
|
1462
|
+
// 뷰 nav 카운트 — Active=스코프 run 수, Goals=스코프 그룹 수, Archive=닫힌 태스크 수(hydrate)
|
|
1463
|
+
function paintNavCounts(){
|
|
1464
|
+
const scoped = [...runs.values()].filter(runInScope);
|
|
1465
|
+
$('navActiveN').textContent = scoped.length || '';
|
|
1466
|
+
const gset = new Set();
|
|
1467
|
+
for (const r of scoped){ const t = tasks.get(r.taskId); if (t && t.groupId!=null && groups.has(t.groupId)) gset.add(t.groupId); }
|
|
1468
|
+
$('navGoalsN').textContent = gset.size || '';
|
|
1469
|
+
}
|
|
1291
1470
|
function paintArchRepos(){
|
|
1292
1471
|
const sel = $('archRepo'); const cur = sel.value;
|
|
1293
1472
|
sel.innerHTML = '<option value="">all repos</option>' + repos.map(r=>'<option value="'+r.id+'">'+esc(r.name)+'</option>').join('');
|
|
@@ -1375,11 +1554,19 @@ async function probeFirstMachine(){
|
|
|
1375
1554
|
}
|
|
1376
1555
|
function chkRow(name, ok, val){
|
|
1377
1556
|
const cls = ok===null ? 'wait' : ok ? 'ok' : 'bad';
|
|
1378
|
-
const st = ok===null ? '…' : ok ? '
|
|
1557
|
+
const st = ok===null ? '…' : ok ? ic('check') : ic('x');
|
|
1379
1558
|
return '<div class="chk '+cls+'"><span class="st">'+st+'</span><span class="nm">'+esc(name)+'</span>'
|
|
1380
1559
|
+ '<span class="v">'+esc(val||'')+'</span></div>';
|
|
1381
1560
|
}
|
|
1382
|
-
function paintOnboarding(){
|
|
1561
|
+
function paintOnboarding(goalsOnly){
|
|
1562
|
+
// 스코프/뷰 필터 때문에 비어 보이는 경우(전체엔 run 이 있음) → 가벼운 안내만.
|
|
1563
|
+
const totalRuns = runs.size;
|
|
1564
|
+
if (totalRuns && (goalsOnly || selectedRepo!=null)){
|
|
1565
|
+
const msg = goalsOnly ? 'No goal groups here' : 'No runs in this repository';
|
|
1566
|
+
$('empty').innerHTML = '<span class="glyph">▚▞▚</span><span>'+esc(msg)+'</span>'
|
|
1567
|
+
+ '<span style="color:#3d4657">'+(selectedRepo!=null?'switch to <b>All repositories</b> or hit + New':'plan a Goal with + New')+'</span>';
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1383
1570
|
const r = readiness;
|
|
1384
1571
|
const agentBin = r && r.agent ? r.agent.bin : 'claude';
|
|
1385
1572
|
let checks;
|
|
@@ -1423,7 +1610,7 @@ function cardHTML(r){
|
|
|
1423
1610
|
const selCls = (selectMode?' selmode':'') + (selected.has(r.id)?' selected':'') + (closed?' closed':'');
|
|
1424
1611
|
return '<div class="card'+selCls+'" id="card-'+r.id+'">'
|
|
1425
1612
|
+ '<div class="card-h"><span class="rid">r'+r.id+'</span><span class="title">'+title+'</span>'
|
|
1426
|
-
+ '<span class="selbox"
|
|
1613
|
+
+ '<span class="selbox">'+ic('check')+'</span>'+chipHTML(r.status)+'</div>'
|
|
1427
1614
|
+ '<div class="meta"><span>branch <b>'+esc(r.branch||'—')+'</b></span>'
|
|
1428
1615
|
+ '<span>files <b>'+(r.filesChanged??0)+'</b></span>'
|
|
1429
1616
|
+ '<span>'+esc(r.agent||'')+'</span>'
|
|
@@ -1432,7 +1619,7 @@ function cardHTML(r){
|
|
|
1432
1619
|
+ (task && task.parentRunId ? '<span title="spawned by agent r'+task.parentRunId+'">↳ by r'+task.parentRunId+'</span>' : '')
|
|
1433
1620
|
+ (r.sessionId && ['done','failed','stopped'].includes(r.status)
|
|
1434
1621
|
? '<span class="resumable" title="agent session preserved — open the run and Send a next instruction to continue">↻ resumable</span>' : '')
|
|
1435
|
-
+ (r.prUrl ? '<a href="'+esc(r.prUrl)+'" target="_blank" rel="noopener" style="margin-left:auto">PR
|
|
1622
|
+
+ (r.prUrl ? '<a href="'+esc(r.prUrl)+'" target="_blank" rel="noopener" style="margin-left:auto">PR '+ic('external-link')+'</a>' : '')
|
|
1436
1623
|
+ '</div>'
|
|
1437
1624
|
+ '<div class="log">'+evs+'</div></div>';
|
|
1438
1625
|
}
|
|
@@ -1448,7 +1635,7 @@ function upsertRun(patch){
|
|
|
1448
1635
|
async function hydrate(){
|
|
1449
1636
|
const r = await fetch('/api/fleet?view=active').then(x=>x.json());
|
|
1450
1637
|
machines = r.machines||[]; repos = r.repos||[]; captures = r.captures||[];
|
|
1451
|
-
if (r.counts){ const n = r.counts.closedTasks||0; $('
|
|
1638
|
+
if (r.counts){ const n = r.counts.closedTasks||0; $('navArchiveN').textContent = n ? String(n) : ''; }
|
|
1452
1639
|
tasks.clear();
|
|
1453
1640
|
(r.tasks||[]).forEach(t => tasks.set(t.id, t));
|
|
1454
1641
|
groups.clear();
|
|
@@ -1468,6 +1655,8 @@ async function hydrate(){
|
|
|
1468
1655
|
paintSidebar(); render();
|
|
1469
1656
|
}
|
|
1470
1657
|
function paintSidebar(){
|
|
1658
|
+
// 스코프 정합 — 사라진 repo 를 물고 있으면 스코프 해제
|
|
1659
|
+
if (selectedRepo!=null && !repos.some(r=>r.id===selectedRepo)){ selectedRepo = null; try{ localStorage.removeItem('coxpit.repo'); }catch{} }
|
|
1471
1660
|
$('machines').innerHTML = machines.map(m =>
|
|
1472
1661
|
'<span class="mchip"><span class="mdot '+(m.online?'on':'')+'"></span><b>'+esc(m.slug)+'</b></span>').join('');
|
|
1473
1662
|
$('repoMachine').innerHTML = machines.map(m=>'<option value="'+esc(m.slug)+'">'+esc(m.slug)+'</option>').join('');
|
|
@@ -1475,8 +1664,12 @@ function paintSidebar(){
|
|
|
1475
1664
|
$('taskRepo').innerHTML = repos.length
|
|
1476
1665
|
? repos.map(r=>'<option value="'+r.id+'">'+esc(r.name)+' · '+esc(r.defaultBranch)+'</option>').join('')
|
|
1477
1666
|
: '<option value="">register a repo — Browse… ↓</option>';
|
|
1478
|
-
|
|
1667
|
+
// 런처 타겟 기본값 = 스코프된 repo(있으면), 아니면 직전 선택 유지
|
|
1668
|
+
if (selectedRepo!=null && repos.some(r=>r.id===selectedRepo)) $('taskRepo').value = String(selectedRepo);
|
|
1669
|
+
else if (prevRepo && repos.some(r=>String(r.id)===prevRepo)) $('taskRepo').value = prevRepo;
|
|
1479
1670
|
$('runFleetBtn').disabled = !repos.length;
|
|
1671
|
+
$('newBtn').disabled = !repos.length && false; // New 는 항상 열림(repo 등록 UI 포함)
|
|
1672
|
+
renderRail();
|
|
1480
1673
|
const capSel = $('taskCapture');
|
|
1481
1674
|
const cur = capSel.value;
|
|
1482
1675
|
capSel.innerHTML = '<option value="">no design capture</option>' + captures.map(c=>
|
|
@@ -1491,34 +1684,104 @@ function paintSidebar(){
|
|
|
1491
1684
|
$('bmk').href = "javascript:(function(){var s=document.createElement('script');s.src='"
|
|
1492
1685
|
+ location.origin + "/design/bookmarklet.js';document.body.appendChild(s)})()";
|
|
1493
1686
|
}
|
|
1687
|
+
/* ── v5.0 navigator rail — machine switcher · repo list(counts+attention+scope) · nav counts ── */
|
|
1688
|
+
const FAILED_STATES = ['failed','error'];
|
|
1689
|
+
function railCounts(){
|
|
1690
|
+
// repoId → { active, attn } — /api/fleet active runs 를 client-side 로 그룹
|
|
1691
|
+
const m = new Map();
|
|
1692
|
+
for (const r of runs.values()){
|
|
1693
|
+
const t = tasks.get(r.taskId); if (!t) continue;
|
|
1694
|
+
const rec = m.get(t.repoId) || { active:0, attn:false };
|
|
1695
|
+
rec.active++;
|
|
1696
|
+
if (FAILED_STATES.includes(r.status)) rec.attn = true;
|
|
1697
|
+
m.set(t.repoId, rec);
|
|
1698
|
+
}
|
|
1699
|
+
return m;
|
|
1700
|
+
}
|
|
1701
|
+
function renderRail(){
|
|
1702
|
+
// machine switcher — 첫 online 머신(없으면 첫 머신) 을 표시, 선택은 #repoMachine 이 보관
|
|
1703
|
+
const cur = $('repoMachine').value || (machines[0] && machines[0].slug) || 'local';
|
|
1704
|
+
const cm = machines.find(x=>x.slug===cur) || machines[0];
|
|
1705
|
+
$('machineLbl').textContent = cm ? cm.slug : 'local';
|
|
1706
|
+
$('machineDot').classList.toggle('on', !!(cm && cm.online));
|
|
1707
|
+
$('machineMenu').innerHTML = machines.map(m=>
|
|
1708
|
+
'<div class="mopt'+(m.slug===cur?' on':'')+'" data-m="'+escA(m.slug)+'">'
|
|
1709
|
+
+ '<span class="mdot2'+(m.online?' on':'')+'"></span>'+esc(m.slug)+'</div>').join('')
|
|
1710
|
+
|| '<div class="mopt">no machines</div>';
|
|
1711
|
+
// repo list
|
|
1712
|
+
const cnt = railCounts();
|
|
1713
|
+
let html = '';
|
|
1714
|
+
if (repos.length){
|
|
1715
|
+
html += '<button type="button" class="repo'+(selectedRepo==null?' on':'')+'" data-repo="all">'
|
|
1716
|
+
+ ic('layers')+'<span class="nm">All repositories</span></button>';
|
|
1717
|
+
for (const r of repos){
|
|
1718
|
+
const c = cnt.get(r.id) || { active:0, attn:false };
|
|
1719
|
+
const on = selectedRepo===r.id;
|
|
1720
|
+
html += '<button type="button" class="repo'+(on?' on':'')+'" data-repo="'+r.id+'" title="'+escA(r.path||r.name)+'">'
|
|
1721
|
+
+ ic('folder')+'<span class="nm">'+esc(r.name)+'</span>'
|
|
1722
|
+
+ (c.attn ? '<span class="attn" title="a run needs a hand"></span>' : '')
|
|
1723
|
+
+ (c.active>0 ? '<span class="cnt">'+c.active+'</span>' : '')
|
|
1724
|
+
+ '<span class="rowmenu">'
|
|
1725
|
+
+ '<button type="button" class="rmbtn" data-rbranch="'+r.id+'" title="base branch">'+ic('branch')+'</button>'
|
|
1726
|
+
+ '<button type="button" class="rmbtn" data-rremove="'+r.id+'" title="remove">'+ic('x')+'</button>'
|
|
1727
|
+
+ '</span></button>';
|
|
1728
|
+
}
|
|
1729
|
+
} else {
|
|
1730
|
+
// empty state — 첫 repo 추가 CTA
|
|
1731
|
+
html += '<div style="padding:10px 6px;font-size:12px;color:var(--faint);line-height:1.6">'
|
|
1732
|
+
+ 'No repositories yet.<br><b style="color:var(--brand)">Add your first repository</b> to start.</div>';
|
|
1733
|
+
}
|
|
1734
|
+
$('repoList').innerHTML = html;
|
|
1735
|
+
paintNavCounts();
|
|
1736
|
+
paintBoardHint();
|
|
1737
|
+
}
|
|
1738
|
+
// 스코프 설정 — 레일 repo 클릭
|
|
1739
|
+
function setScope(repoId){
|
|
1740
|
+
selectedRepo = repoId;
|
|
1741
|
+
try { if (repoId==null) localStorage.removeItem('coxpit.repo'); else localStorage.setItem('coxpit.repo', String(repoId)); } catch {}
|
|
1742
|
+
if (repoId!=null) $('taskRepo').value = String(repoId), syncSelect('taskRepo');
|
|
1743
|
+
renderRail();
|
|
1744
|
+
if (boardView!=='archive') render();
|
|
1745
|
+
else { paintArchRepos(); $('archRepo').value = repoId!=null ? String(repoId) : ''; syncSelect('archRepo'); archFetch(true); }
|
|
1746
|
+
}
|
|
1747
|
+
$('repoList').addEventListener('click', (e)=>{
|
|
1748
|
+
const br = e.target.closest('button[data-rbranch]');
|
|
1749
|
+
if (br){ e.stopPropagation(); openBranchFor(Number(br.dataset.rbranch)); return; }
|
|
1750
|
+
const rm = e.target.closest('button[data-rremove]');
|
|
1751
|
+
if (rm){ e.stopPropagation(); removeRepoById(Number(rm.dataset.rremove)); return; }
|
|
1752
|
+
const row = e.target.closest('.repo[data-repo]'); if(!row) return;
|
|
1753
|
+
setScope(row.dataset.repo==='all' ? null : Number(row.dataset.repo));
|
|
1754
|
+
});
|
|
1494
1755
|
$('captures').addEventListener('click', async (e)=>{
|
|
1495
1756
|
const b = e.target.closest('button[data-delcap]'); if(!b) return;
|
|
1496
1757
|
await fetch('/api/design/'+b.dataset.delcap,{method:'DELETE'});
|
|
1497
1758
|
hydrate();
|
|
1498
1759
|
});
|
|
1499
|
-
|
|
1500
|
-
const rid = $('taskRepo').value;
|
|
1760
|
+
async function removeRepoById(rid){
|
|
1501
1761
|
if (!rid){ toast('no repository selected', 'error'); return; }
|
|
1502
1762
|
const yes = await confirmUI('Remove the selected repository from coxpit?',
|
|
1503
1763
|
{ sub: 'The repo itself is untouched — only the registration is removed. Refused while it has open tasks.', danger: true, okLabel: 'Remove' });
|
|
1504
1764
|
if (!yes) return;
|
|
1505
1765
|
const res = await fetch('/api/repos/'+rid,{method:'DELETE'});
|
|
1506
1766
|
const j = await res.json().catch(()=>({}));
|
|
1507
|
-
if (res.ok){ toast('repo removed', 'ok'); hydrate(); }
|
|
1767
|
+
if (res.ok){ if (selectedRepo===Number(rid)) setScope(null); toast('repo removed', 'ok'); hydrate(); }
|
|
1508
1768
|
else toast('remove: '+(j.detail||res.status), 'error');
|
|
1509
|
-
}
|
|
1769
|
+
}
|
|
1770
|
+
$('repoRemove').addEventListener('click', ()=>removeRepoById($('taskRepo').value));
|
|
1510
1771
|
/* ── 기본 브랜치 변경 ── */
|
|
1511
|
-
|
|
1512
|
-
|
|
1772
|
+
let brRepoId = null;
|
|
1773
|
+
function openBranchFor(rid){
|
|
1513
1774
|
if (!rid){ toast('no repository selected', 'error'); return; }
|
|
1775
|
+
brRepoId = rid;
|
|
1514
1776
|
const repo = repos.find(r=>String(r.id)===String(rid));
|
|
1515
1777
|
$('brInput').value = repo ? repo.defaultBranch : '';
|
|
1516
1778
|
$('brOverlay').classList.add('open'); $('brInput').focus();
|
|
1517
|
-
}
|
|
1779
|
+
}
|
|
1780
|
+
$('repoBranch').addEventListener('click', ()=>openBranchFor($('taskRepo').value));
|
|
1518
1781
|
$('brCancel').addEventListener('click', ()=>$('brOverlay').classList.remove('open'));
|
|
1519
1782
|
$('brOverlay').addEventListener('click',(e)=>{ if(e.target===$('brOverlay')) $('brOverlay').classList.remove('open'); });
|
|
1520
1783
|
async function brSave(){
|
|
1521
|
-
const rid =
|
|
1784
|
+
const rid = brRepoId; const branch = $('brInput').value.trim();
|
|
1522
1785
|
if (!rid || !branch) return;
|
|
1523
1786
|
const res = await fetch('/api/repos/'+rid,{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({defaultBranch:branch})});
|
|
1524
1787
|
const j = await res.json().catch(()=>({}));
|
|
@@ -1535,7 +1798,7 @@ $('repoManual').addEventListener('click', ()=>{
|
|
|
1535
1798
|
/* ── 완료 알림(브라우저) — 벨 토글, run 정착 시 통지 ── */
|
|
1536
1799
|
let notifyOn = false;
|
|
1537
1800
|
try { notifyOn = localStorage.getItem('coxpit.notify') === '1' && Notification.permission === 'granted'; } catch {}
|
|
1538
|
-
function paintBell(){ $('bell').
|
|
1801
|
+
function paintBell(){ $('bell').innerHTML = ic(notifyOn ? 'bell' : 'bell-off'); }
|
|
1539
1802
|
$('bell').addEventListener('click', async ()=>{
|
|
1540
1803
|
if (!('Notification' in window)){ toast('this browser has no notification support', 'error'); return; }
|
|
1541
1804
|
if (!notifyOn){
|
|
@@ -1634,22 +1897,22 @@ function paintModal(){
|
|
|
1634
1897
|
/outputs 로 카드 목록을 받아 렌더하고, 클릭하면 타입별 실뷰어를 오른쪽에 띄운다.
|
|
1635
1898
|
answer/doc → mdLite · page → sandbox iframe · code → 기존 diff 렌더러 · file → 이미지/다운로드. */
|
|
1636
1899
|
let outCards = []; // 이 run 의 마지막 카드 목록(RunOutputCard[])
|
|
1637
|
-
const
|
|
1638
|
-
const OUT_LABEL = { answer:'
|
|
1900
|
+
const OUT_ICON = { answer:'message', code:'code', doc:'file', page:'image', file:'image' };
|
|
1901
|
+
const OUT_LABEL = { answer:'Answer', code:'Code', doc:'Doc', page:'Page', file:'File' };
|
|
1639
1902
|
function contractHTML(cards){
|
|
1640
1903
|
const declared = cards.filter(c=>c.required);
|
|
1641
1904
|
const aux = cards.filter(c=>!c.required && c.present);
|
|
1642
1905
|
if (!declared.length && !aux.length) return ''; // 계약도 부수도 없으면 스트립 숨김
|
|
1643
|
-
let h = '<span class="clabel"
|
|
1906
|
+
let h = '<span class="clabel">Required outputs (contract):</span>';
|
|
1644
1907
|
if (declared.length){
|
|
1645
1908
|
h += declared.map(c=>{
|
|
1646
1909
|
const ok = c.present;
|
|
1647
|
-
return '<span class="req '+(ok?'ok':'warn')+'"><span class="rg">'+(ok?'
|
|
1910
|
+
return '<span class="req '+(ok?'ok':'warn')+'"><span class="rg">'+ic(ok?'check':'alert-triangle')+'</span>'
|
|
1648
1911
|
+ esc(OUT_LABEL[c.type]||c.type)+'</span>';
|
|
1649
1912
|
}).join('');
|
|
1650
|
-
} else h += '<span class="req aux"
|
|
1913
|
+
} else h += '<span class="req aux">none</span>';
|
|
1651
1914
|
if (aux.length){
|
|
1652
|
-
h += '<span class="csep">—</span><span class="clabel"
|
|
1915
|
+
h += '<span class="csep">—</span><span class="clabel">Extra:</span>'
|
|
1653
1916
|
+ aux.map(c=>'<span class="req aux">'+esc(OUT_LABEL[c.type]||c.type)+'</span>').join('');
|
|
1654
1917
|
}
|
|
1655
1918
|
return h;
|
|
@@ -1657,16 +1920,16 @@ function contractHTML(cards){
|
|
|
1657
1920
|
function outCardHTML(c, i){
|
|
1658
1921
|
const miss = !c.present;
|
|
1659
1922
|
const badge = c.required
|
|
1660
|
-
? '<span class="obadge '+(c.present?'req':'warn')+'"
|
|
1661
|
-
: '<span class="obadge"
|
|
1662
|
-
const
|
|
1663
|
-
const meta = miss ? '
|
|
1923
|
+
? '<span class="obadge '+(c.present?'req':'warn')+'">Required</span>'
|
|
1924
|
+
: '<span class="obadge">Extra</span>';
|
|
1925
|
+
const gname = miss ? 'alert-triangle' : (OUT_ICON[c.type] || 'circle');
|
|
1926
|
+
const meta = miss ? 'output not met — '+esc(c.meta||'') : esc(c.meta||'');
|
|
1664
1927
|
return '<div class="ocard'+(miss?' miss':'')+'" data-oi="'+i+'">'
|
|
1665
|
-
+ '<span class="og">'+
|
|
1928
|
+
+ '<span class="og">'+ic(gname)+'</span>'
|
|
1666
1929
|
+ '<span class="ob"><span class="ot">'+esc(c.title||c.type)+'</span>'
|
|
1667
1930
|
+ '<span class="om">'+meta+'</span></span>'
|
|
1668
1931
|
+ badge
|
|
1669
|
-
+ (miss?'':'<span class="oc"
|
|
1932
|
+
+ (miss?'':'<span class="oc">'+ic('chevron')+'</span>')
|
|
1670
1933
|
+ '</div>';
|
|
1671
1934
|
}
|
|
1672
1935
|
/* run 형태로 기본 카드 선택 — 코드 변경 없고 answer/doc 있으면 그걸, 코드 위주면 code,
|
|
@@ -1679,7 +1942,7 @@ function pickDefaultCard(cards){
|
|
|
1679
1942
|
if (!hasCode && docish) return cards.indexOf(docish);
|
|
1680
1943
|
if (hasCode){
|
|
1681
1944
|
const codeCard = present.find(c=>c.type==='code');
|
|
1682
|
-
//
|
|
1945
|
+
// even when code-heavy, a declared doc/answer wins the contract (code is always reachable via its card)
|
|
1683
1946
|
const declaredDoc = present.find(c=>c.required && (c.type==='answer'||c.type==='doc'||c.type==='page'));
|
|
1684
1947
|
if (declaredDoc) return cards.indexOf(declaredDoc);
|
|
1685
1948
|
return cards.indexOf(codeCard);
|
|
@@ -1994,6 +2257,7 @@ async function openCompare(taskId){
|
|
|
1994
2257
|
cmpTaskId = taskId;
|
|
1995
2258
|
cmpDocMode = false; $('cmpDocsTgl').textContent = 'Rendered';
|
|
1996
2259
|
$('cmpReview').hidden = true; $('cmpReview').innerHTML = '';
|
|
2260
|
+
$('cmpBody').hidden = false; $('cmpAI').textContent = 'AI review'; $('cmpAI').classList.remove('on');
|
|
1997
2261
|
$('cmpOverlay').classList.add('open');
|
|
1998
2262
|
$('cmpBody').innerHTML = '<div class="empty" style="flex:1">loading…</div>';
|
|
1999
2263
|
await paintCompare();
|
|
@@ -2021,7 +2285,7 @@ async function paintCompare(){
|
|
|
2021
2285
|
+ '<div class="cmp-meta" title="'+esc(summary)+'">'+(summary?esc(summary):'—')+'</div>'
|
|
2022
2286
|
+ '<div class="cmp-diff"><pre class="diff">'+diffHTML(r.diff||'')+'</pre></div>'
|
|
2023
2287
|
+ '<div class="cmp-f"><span class="msg" id="cmpMsg-'+r.id+'">'
|
|
2024
|
-
+ (r.prUrl ? '<a href="'+esc(r.prUrl)+'" target="_blank" rel="noopener">PR
|
|
2288
|
+
+ (r.prUrl ? '<a href="'+esc(r.prUrl)+'" target="_blank" rel="noopener">PR '+ic('external-link')+' '+esc(r.prUrl.split('/').slice(-1)[0])+'</a>' : '')
|
|
2025
2289
|
+ '</span>'
|
|
2026
2290
|
+ (merged
|
|
2027
2291
|
? chipHTML('merged')
|
|
@@ -2084,18 +2348,30 @@ function mdLite(src){
|
|
|
2084
2348
|
}
|
|
2085
2349
|
$('cmpAI').addEventListener('click', async ()=>{
|
|
2086
2350
|
if (cmpTaskId==null) return;
|
|
2087
|
-
const yes = await confirmUI('Run an AI review of these implementations?',
|
|
2088
|
-
{ sub: 'A reviewer agent reads every diff and summarizes each approach, pros/cons, and a recommendation — so you judge instead of reading all the code. Real agent, spends credits (~1–2 min).', okLabel: 'Review' });
|
|
2089
|
-
if (!yes) return;
|
|
2090
2351
|
const btn = $('cmpAI');
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2352
|
+
// 이미 리뷰가 떠 있으면 → diff 로 토글백
|
|
2353
|
+
if (!$('cmpReview').hidden){
|
|
2354
|
+
$('cmpReview').hidden = true; $('cmpBody').hidden = false;
|
|
2355
|
+
btn.classList.remove('on'); btn.textContent = 'AI review';
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
// 아직 안 불러왔으면 확인 후 리뷰 실행(같은 compare 안에선 1회만)
|
|
2359
|
+
if (!$('cmpReview').innerHTML.trim()){
|
|
2360
|
+
const yes = await confirmUI('Run an AI review of these implementations?',
|
|
2361
|
+
{ sub: 'A reviewer agent reads every diff and summarizes each approach, pros/cons, and a recommendation — so you judge instead of reading all the code. Real agent, spends credits (~1–2 min).', okLabel: 'Review' });
|
|
2362
|
+
if (!yes) return;
|
|
2363
|
+
btn.disabled = true; btn.textContent = 'Reviewing…';
|
|
2364
|
+
try{
|
|
2365
|
+
const res = await fetch('/api/tasks/'+cmpTaskId+'/review',{method:'POST',
|
|
2366
|
+
headers:{'content-type':'application/json'}, body:JSON.stringify({real:true})});
|
|
2367
|
+
const j = await res.json().catch(()=>({}));
|
|
2368
|
+
if (!res.ok){ toast('review: '+(j.detail||res.status), 'error'); btn.disabled = false; btn.textContent = 'AI review'; return; }
|
|
2369
|
+
$('cmpReview').innerHTML = mdLite(j.review||'');
|
|
2370
|
+
} finally { btn.disabled = false; }
|
|
2371
|
+
}
|
|
2372
|
+
// 리뷰를 풀높이로, diff 컬럼은 숨김
|
|
2373
|
+
$('cmpReview').hidden = false; $('cmpBody').hidden = true;
|
|
2374
|
+
btn.classList.add('on'); btn.textContent = 'Diffs';
|
|
2099
2375
|
});
|
|
2100
2376
|
$('mCompare').addEventListener('click', ()=>{
|
|
2101
2377
|
if (openRunId==null) return;
|
|
@@ -2141,15 +2417,15 @@ function roomRunRowHTML(r){
|
|
|
2141
2417
|
const meta = (r.agent?esc(r.agent)+' · ':'') + (r.filesChanged? '+'+r.filesChanged+'f' : 'no changes');
|
|
2142
2418
|
let acts;
|
|
2143
2419
|
if (merged){
|
|
2144
|
-
acts = '<div class="acts"><button class="b ghost" data-ract="open" data-rrid="'+r.runId+'"
|
|
2420
|
+
acts = '<div class="acts"><button class="b ghost" data-ract="open" data-rrid="'+r.runId+'">Open</button></div>';
|
|
2145
2421
|
} else if (running){
|
|
2146
|
-
acts = '<div class="acts"><button class="b ghost" data-ract="open" data-rrid="'+r.runId+'"
|
|
2422
|
+
acts = '<div class="acts"><button class="b ghost" data-ract="open" data-rrid="'+r.runId+'">Open · decide once settled</button></div>';
|
|
2147
2423
|
} else {
|
|
2148
2424
|
acts = '<div class="acts">'
|
|
2149
|
-
+ '<button class="b ghost" data-ract="review" data-rrid="'+r.runId+'"
|
|
2150
|
-
+ '<button class="b" data-ract="fix" data-rrid="'+r.runId+'"
|
|
2151
|
-
+ '<button class="b merge" data-ract="merge" data-rrid="'+r.runId+'"
|
|
2152
|
-
+ '<button class="b close" data-ract="close" data-rrid="'+r.runId+'"
|
|
2425
|
+
+ '<button class="b ghost" data-ract="review" data-rrid="'+r.runId+'">Review</button>'
|
|
2426
|
+
+ '<button class="b" data-ract="fix" data-rrid="'+r.runId+'">Steer</button>'
|
|
2427
|
+
+ '<button class="b merge" data-ract="merge" data-rrid="'+r.runId+'">Merge</button>'
|
|
2428
|
+
+ '<button class="b close" data-ract="close" data-rrid="'+r.runId+'">Close</button>'
|
|
2153
2429
|
+ '</div>';
|
|
2154
2430
|
}
|
|
2155
2431
|
const badge = merged
|
|
@@ -2157,7 +2433,7 @@ function roomRunRowHTML(r){
|
|
|
2157
2433
|
: (running ? '<span class="run-badge running">running</span>' : '');
|
|
2158
2434
|
// running·merged 는 체크박스로 선택 불가(정착·미머지만 Integrate 대상)
|
|
2159
2435
|
const selectable = !merged && !running && (r.filesChanged||0)>0;
|
|
2160
|
-
const cb = '<span class="cb" data-rcb="'+(selectable?r.runId:'')+'"'+(selectable?'':' style="opacity:.4;cursor:default"')+'>'+(sel?'
|
|
2436
|
+
const cb = '<span class="cb" data-rcb="'+(selectable?r.runId:'')+'"'+(selectable?'':' style="opacity:.4;cursor:default"')+'>'+(sel?ic('check'):'')+'</span>';
|
|
2161
2437
|
return '<div class="run'+(sel?' sel':'')+(merged?' dim':'')+(open?' open':'')+'" data-rrun="'+r.runId+'">'
|
|
2162
2438
|
+ '<div class="run-h">'+cb
|
|
2163
2439
|
+ '<span class="dot '+dotCls+'"></span>'
|
|
@@ -2172,10 +2448,10 @@ function roomRunRowHTML(r){
|
|
|
2172
2448
|
+ '<div class="run-b" data-rbody="'+r.runId+'">'
|
|
2173
2449
|
+ '<div class="rout" data-rout="'+r.runId+'"><div class="rout-cards"><div class="ocards-empty">…</div></div></div>'
|
|
2174
2450
|
+ (roomDone(s) && r.steerable
|
|
2175
|
-
? '<div class="fix"><input data-rfix="'+r.runId+'" placeholder="
|
|
2176
|
-
+ '<button class="b" data-ract="fixsend" data-rrid="'+r.runId+'"
|
|
2177
|
-
: '<div class="review"><span class="rk">·</span><span class="rt"
|
|
2178
|
-
+ (running?'
|
|
2451
|
+
? '<div class="fix"><input data-rfix="'+r.runId+'" placeholder="Next instruction — same session & worktree…" />'
|
|
2452
|
+
+ '<button class="b" data-ract="fixsend" data-rrid="'+r.runId+'">Steer → continue session</button></div>'
|
|
2453
|
+
: '<div class="review"><span class="rk">·</span><span class="rt">This run can\\'t be steered ('
|
|
2454
|
+
+ (running?'still running':(roomDone(s)?'no session — dry/cleaned up':'status '+esc(s)))+').</span></div>')
|
|
2179
2455
|
+ '</div></div>';
|
|
2180
2456
|
}
|
|
2181
2457
|
function roomRenderRuns(){
|
|
@@ -2192,7 +2468,7 @@ function roomRenderRuns(){
|
|
|
2192
2468
|
function roomUpdateGbar(){
|
|
2193
2469
|
const n = roomSel.size;
|
|
2194
2470
|
$('roomSelC').textContent = '☑ '+n+' selected';
|
|
2195
|
-
$('roomIntegrateSel').textContent = 'Integrate
|
|
2471
|
+
$('roomIntegrateSel').textContent = 'Integrate selected ('+n+')';
|
|
2196
2472
|
$('roomIntegrateSel').disabled = n===0;
|
|
2197
2473
|
}
|
|
2198
2474
|
/* 펼친 행의 출력 카드 로드 — 모달과 같은 /outputs + renderOutCardInto 재사용(중복 없음). */
|
|
@@ -2217,7 +2493,7 @@ function roomOpenCard(rid, i){
|
|
|
2217
2493
|
let det = wrap.querySelector('.rout-detail');
|
|
2218
2494
|
if (!det){
|
|
2219
2495
|
det = document.createElement('div'); det.className='rout-detail';
|
|
2220
|
-
det.innerHTML = '<div class="rout-back">‹
|
|
2496
|
+
det.innerHTML = '<div class="rout-back">‹ Outputs</div><div class="rout-body"></div>';
|
|
2221
2497
|
wrap.appendChild(det);
|
|
2222
2498
|
}
|
|
2223
2499
|
det.style.display='';
|
|
@@ -2335,7 +2611,7 @@ $('roomRuns').addEventListener('click', async (e)=>{
|
|
|
2335
2611
|
e.stopPropagation();
|
|
2336
2612
|
const id = Number(cb.dataset.rcb); if (!id) return;
|
|
2337
2613
|
if (roomSel.has(id)) roomSel.delete(id); else roomSel.add(id);
|
|
2338
|
-
cb.
|
|
2614
|
+
cb.innerHTML = roomSel.has(id) ? ic('check') : '';
|
|
2339
2615
|
cb.closest('.run').classList.toggle('sel', roomSel.has(id));
|
|
2340
2616
|
roomUpdateGbar();
|
|
2341
2617
|
return;
|
|
@@ -2363,7 +2639,7 @@ async function roomRunAction(act, rid){
|
|
|
2363
2639
|
if (act==='fix'){ // 펼치고 steer 입력에 포커스
|
|
2364
2640
|
if (!roomOpen.has(rid)){ roomOpen.add(rid); const row=document.querySelector('.run[data-rrun="'+rid+'"]'); if(row){ row.classList.add('open'); roomLoadRunOutputs(rid); } }
|
|
2365
2641
|
const inp = document.querySelector('[data-rfix="'+rid+'"]'); if (inp) inp.focus();
|
|
2366
|
-
else toast('r'+rid+': steer
|
|
2642
|
+
else toast('r'+rid+': can\\'t steer (needs a settled run with a session)', 'error');
|
|
2367
2643
|
return;
|
|
2368
2644
|
}
|
|
2369
2645
|
if (act==='fixsend'){
|
|
@@ -2371,7 +2647,7 @@ async function roomRunAction(act, rid){
|
|
|
2371
2647
|
if (!msg){ if(inp) inp.focus(); return; }
|
|
2372
2648
|
const res = await fetch('/api/runs/'+rid+'/steer',{method:'POST',
|
|
2373
2649
|
headers:{'content-type':'application/json'}, body:JSON.stringify({message:msg, mode:'work'})});
|
|
2374
|
-
if (res.ok){ if(inp) inp.value=''; toast('r'+rid+':
|
|
2650
|
+
if (res.ok){ if(inp) inp.value=''; toast('r'+rid+': steered — continuing the same session', 'ok'); }
|
|
2375
2651
|
else { const j = await res.json().catch(()=>({})); toast('steer: '+(j.detail||res.status), 'error'); }
|
|
2376
2652
|
return;
|
|
2377
2653
|
}
|
|
@@ -2410,7 +2686,7 @@ async function roomRunAction(act, rid){
|
|
|
2410
2686
|
return;
|
|
2411
2687
|
}
|
|
2412
2688
|
}
|
|
2413
|
-
/* [리뷰] — 그 run 의 태스크에 reviewTask(/tasks/:id/review) 를 돌려
|
|
2689
|
+
/* [리뷰] — 그 run 의 태스크에 reviewTask(/tasks/:id/review) 를 돌려 요약을 행 안에 인라인 표시. */
|
|
2414
2690
|
async function reviewOneRun(r){
|
|
2415
2691
|
const row = document.querySelector('.run[data-rrun="'+r.runId+'"]'); if(!row) return;
|
|
2416
2692
|
if (!roomOpen.has(r.runId)){ roomOpen.add(r.runId); row.classList.add('open'); roomLoadRunOutputs(r.runId); }
|
|
@@ -2418,21 +2694,21 @@ async function reviewOneRun(r){
|
|
|
2418
2694
|
let rv = body.querySelector('.review.airev');
|
|
2419
2695
|
if (!rv){ rv = document.createElement('div'); rv.className='review airev'; body.insertBefore(rv, body.querySelector('.fix')||null); }
|
|
2420
2696
|
const real = $('taskReal').checked;
|
|
2421
|
-
rv.innerHTML = '<span class="rk"
|
|
2697
|
+
rv.innerHTML = '<span class="rk">'+ic('message')+' Review</span><span class="rt">reviewing… ('+(real?'real':'dry')+')</span>';
|
|
2422
2698
|
try{
|
|
2423
2699
|
const res = await fetch('/api/tasks/'+r.taskId+'/review',{method:'POST',
|
|
2424
2700
|
headers:{'content-type':'application/json'}, body:JSON.stringify({real})});
|
|
2425
2701
|
const j = await res.json().catch(()=>({}));
|
|
2426
|
-
if (res.ok){ rv.innerHTML = '<span class="rk"
|
|
2427
|
-
else { rv.innerHTML = '<span class="rk"
|
|
2428
|
-
}catch{ rv.innerHTML = '<span class="rk"
|
|
2702
|
+
if (res.ok){ rv.innerHTML = '<span class="rk">'+ic('message')+' Review</span><span class="rt">'+mdLite(j.review||'(no summary)')+'</span>'; }
|
|
2703
|
+
else { rv.innerHTML = '<span class="rk">'+ic('message')+' Review</span><span class="rt">review failed — '+esc(j.detail||String(res.status))+'</span>'; toast('review: '+(j.detail||res.status), 'error'); }
|
|
2704
|
+
}catch{ rv.innerHTML = '<span class="rk">'+ic('message')+' Review</span><span class="rt">review request failed</span>'; }
|
|
2429
2705
|
}
|
|
2430
2706
|
/* steer 입력 Enter → 전송 */
|
|
2431
2707
|
$('roomRuns').addEventListener('keydown', (e)=>{
|
|
2432
2708
|
const inp = e.target.closest ? e.target.closest('[data-rfix]') : null;
|
|
2433
2709
|
if (inp && e.key==='Enter'){ e.preventDefault(); roomRunAction('fixsend', Number(inp.dataset.rfix)); }
|
|
2434
2710
|
});
|
|
2435
|
-
/* ──
|
|
2711
|
+
/* ── group action bar — Integrate selected · Review all · Close group (all reuse existing flows) ── */
|
|
2436
2712
|
$('roomIntegrateSel').addEventListener('click', async ()=>{
|
|
2437
2713
|
if (!roomSel.size){ toast('select settled runs with changes first', 'error'); return; }
|
|
2438
2714
|
// 기존 select mode + selbar + /api/integrate 재사용 — 방을 닫고 선택을 밴드 select 로 옮긴다.
|
|
@@ -2461,7 +2737,7 @@ $('roomReviewAll').addEventListener('click', async ()=>{
|
|
|
2461
2737
|
$('roomGroupClose').addEventListener('click', async ()=>{
|
|
2462
2738
|
if (roomGroupId==null) return;
|
|
2463
2739
|
const g = roomGroupId;
|
|
2464
|
-
await closeGroup(g); //
|
|
2740
|
+
await closeGroup(g); // reuse existing group close (guard · atRisk tally)
|
|
2465
2741
|
if (roomGroupId===g) await roomLoad();
|
|
2466
2742
|
});
|
|
2467
2743
|
/* + New attempt — 같은 그룹에 새 시도 발사 (현재 dry/real 토글 반영) */
|
|
@@ -2693,24 +2969,38 @@ $('mTerm').addEventListener('click', ()=>{
|
|
|
2693
2969
|
});
|
|
2694
2970
|
|
|
2695
2971
|
/* ── forms ── */
|
|
2696
|
-
/* ──
|
|
2972
|
+
/* ── New sheet type seg — Task | Goal | Workbench (body + footer adapt, mock's setV) ── */
|
|
2697
2973
|
let lTab = 'task';
|
|
2698
|
-
const L_LABEL = { task: 'Run fleet', goal: 'Plan &
|
|
2974
|
+
const L_LABEL = { task: 'Run fleet', goal: 'Plan & run', bench: 'Open workbench' };
|
|
2975
|
+
const L_ICON = { task: 'play', goal: 'target', bench: 'terminal' };
|
|
2976
|
+
function setV(tab){
|
|
2977
|
+
lTab = tab;
|
|
2978
|
+
document.querySelectorAll('#launchTabs .seg-opt').forEach(x=>x.classList.toggle('on', x.dataset.tab===tab));
|
|
2979
|
+
$('panelTask').style.display = tab==='task' ? 'flex' : 'none';
|
|
2980
|
+
$('panelGoal').hidden = tab!=='goal';
|
|
2981
|
+
$('panelGoal').style.display = tab==='goal' ? 'flex' : 'none';
|
|
2982
|
+
$('panelBench').hidden = tab!=='bench';
|
|
2983
|
+
$('panelBench').style.display = tab==='bench' ? 'flex' : 'none';
|
|
2984
|
+
// footer adapts: Task=Dry/Real·count·Run fleet · Goal=Dry/Real·Plan & run · Workbench=Open workbench only
|
|
2985
|
+
$('modeSeg').style.display = tab==='bench' ? 'none' : 'flex'; // workbench 는 에이전트 없음
|
|
2986
|
+
$('taskCount').style.display = tab==='task' ? '' : 'none'; // count 는 Task 만(플래너·워크벤치는 1)
|
|
2987
|
+
$('runFleetBtn').innerHTML = ic(L_ICON[tab]) + ' ' + L_LABEL[tab];
|
|
2988
|
+
}
|
|
2699
2989
|
document.querySelectorAll('#launchTabs .seg-opt').forEach(b=>{
|
|
2700
|
-
b.addEventListener('click', ()=>
|
|
2701
|
-
lTab = b.dataset.tab;
|
|
2702
|
-
document.querySelectorAll('#launchTabs .seg-opt').forEach(x=>x.classList.toggle('on', x===b));
|
|
2703
|
-
$('panelTask').style.display = lTab==='task' ? 'flex' : 'none';
|
|
2704
|
-
$('panelGoal').hidden = lTab!=='goal';
|
|
2705
|
-
$('panelGoal').style.display = lTab==='goal' ? 'flex' : 'none';
|
|
2706
|
-
$('panelBench').hidden = lTab!=='bench';
|
|
2707
|
-
$('panelBench').style.display = lTab==='bench' ? 'flex' : 'none';
|
|
2708
|
-
$('modeSeg').style.display = lTab==='bench' ? 'none' : 'flex'; // workbench 는 에이전트 없음
|
|
2709
|
-
$('taskCount').style.display = lTab==='task' ? '' : 'none';
|
|
2710
|
-
$('runFleetBtn').textContent = L_LABEL[lTab];
|
|
2711
|
-
});
|
|
2990
|
+
b.addEventListener('click', ()=>setV(b.dataset.tab));
|
|
2712
2991
|
});
|
|
2713
2992
|
|
|
2993
|
+
/* ── progressive Options reveal (rarely-used Task fields; expanded state remembered per session) ── */
|
|
2994
|
+
function setTaskOpt(open){
|
|
2995
|
+
$('taskOptToggle').setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
2996
|
+
$('taskOptBody').hidden = !open;
|
|
2997
|
+
try{ sessionStorage.setItem('coxpit.taskOpt', open ? '1' : '0'); }catch{}
|
|
2998
|
+
}
|
|
2999
|
+
$('taskOptToggle').addEventListener('click', ()=>{
|
|
3000
|
+
setTaskOpt($('taskOptToggle').getAttribute('aria-expanded') !== 'true');
|
|
3001
|
+
});
|
|
3002
|
+
setTaskOpt((()=>{ try{ return sessionStorage.getItem('coxpit.taskOpt')==='1'; }catch{ return false; } })());
|
|
3003
|
+
|
|
2714
3004
|
async function submitGoal(){
|
|
2715
3005
|
const repoId = Number($('taskRepo').value);
|
|
2716
3006
|
const goal = $('planGoal').value.trim();
|
|
@@ -2723,7 +3013,7 @@ async function submitGoal(){
|
|
|
2723
3013
|
const res = await fetch('/api/plan',{method:'POST',headers:{'content-type':'application/json'},
|
|
2724
3014
|
body:JSON.stringify({repoId, goal, real})});
|
|
2725
3015
|
const j = await res.json().catch(()=>({}));
|
|
2726
|
-
if (res.ok){ toast(j.tasks.length+' task(s) planned & launched', 'ok'); $('planGoal').value=''; hydrate(); }
|
|
3016
|
+
if (res.ok){ toast(j.tasks.length+' task(s) planned & launched', 'ok'); $('planGoal').value=''; closeLaunch(); hydrate(); }
|
|
2727
3017
|
else toast('plan: '+(j.detail||j.error||res.status), 'error');
|
|
2728
3018
|
} finally { btn.disabled = false; btn.textContent = L_LABEL[lTab]; }
|
|
2729
3019
|
}
|
|
@@ -2737,6 +3027,7 @@ async function submitBench(){
|
|
|
2737
3027
|
if (!res.ok){ toast('workbench: '+(j.detail||j.error||res.status), 'error'); return; }
|
|
2738
3028
|
toast('workbench open — terminal attached', 'ok');
|
|
2739
3029
|
$('benchTitle').value='';
|
|
3030
|
+
closeLaunch();
|
|
2740
3031
|
await hydrate();
|
|
2741
3032
|
openTerm(j.runId); // 바로 터미널로
|
|
2742
3033
|
}
|
|
@@ -2776,6 +3067,7 @@ $('taskForm').addEventListener('submit', async (e)=>{
|
|
|
2776
3067
|
body:JSON.stringify({count:Number($('taskCount').value)||1, real: $('taskReal').checked, agent: selAgent, model})});
|
|
2777
3068
|
if (model) rememberModel(model);
|
|
2778
3069
|
$('taskTitle').value=''; $('taskPrompt').value=''; clearOutputs();
|
|
3070
|
+
closeLaunch();
|
|
2779
3071
|
});
|
|
2780
3072
|
/* ── deliverable 계약 칩 — 선택된 타입 배열을 tasks.outputs 로 보낸다(기본=빈=오늘의 동작) ── */
|
|
2781
3073
|
const OUTPUT_ORDER = ['answer','code','doc','page','file'];
|
|
@@ -2824,7 +3116,7 @@ setMode(savedMode === '1', false);
|
|
|
2824
3116
|
|
|
2825
3117
|
/* ── provider segmented control — Task 탭 전용(Goal 플래너·Workbench 는 무관) ── */
|
|
2826
3118
|
let selAgent = 'claude-code';
|
|
2827
|
-
const provOpts = Array.from(document.querySelectorAll('#provSeg .seg-opt'));
|
|
3119
|
+
const provOpts = Array.from(document.querySelectorAll('#provSeg .seg-opt, #provSegGoal .seg-opt'));
|
|
2828
3120
|
function setProvider(id, persist){
|
|
2829
3121
|
selAgent = id;
|
|
2830
3122
|
for (const b of provOpts){
|
|
@@ -3063,6 +3355,43 @@ function setDrawer(on){ asideEl.classList.toggle('open', on); $('scrim').classLi
|
|
|
3063
3355
|
$('menuBtn').addEventListener('click', ()=>setDrawer(!asideEl.classList.contains('open')));
|
|
3064
3356
|
$('scrim').addEventListener('click', ()=>setDrawer(false));
|
|
3065
3357
|
|
|
3358
|
+
/* ── v5.0 rail interactions — machine switcher · New sheet · Add repository ── */
|
|
3359
|
+
function openLaunch(tab){
|
|
3360
|
+
const r = repos.find(x=>x.id===selectedRepo);
|
|
3361
|
+
$('sheetRepoLbl').textContent = r ? ('· '+r.name) : (repos.length ? '· all repositories' : '· register a repo first');
|
|
3362
|
+
setV(tab || 'task'); // + New 는 Task 로 열림; 타입 seg 가 안에서 전환
|
|
3363
|
+
$('newSheet').classList.add('open');
|
|
3364
|
+
setDrawer(false);
|
|
3365
|
+
}
|
|
3366
|
+
function closeLaunch(){ $('newSheet').classList.remove('open'); $('repoActions').hidden = true; $('repoForm').hidden = true; }
|
|
3367
|
+
$('newBtn').addEventListener('click', ()=>openLaunch('task'));
|
|
3368
|
+
$('fab').addEventListener('click', ()=>openLaunch('task')); // mobile pocket-board FAB → same sheet
|
|
3369
|
+
$('sheetClose').addEventListener('click', closeLaunch);
|
|
3370
|
+
$('newSheet').addEventListener('click', (e)=>{ if(e.target===$('newSheet')) closeLaunch(); });
|
|
3371
|
+
// Add repository — 시트를 열고 repo 피커(Browse/New-folder/Path 내장)를 노출
|
|
3372
|
+
$('repoAdd').addEventListener('click', ()=>{ openLaunch('task'); $('repoActions').hidden = false; $('repoBrowse').click(); });
|
|
3373
|
+
// machine switcher — 컴팩트 버튼 + 메뉴(기존 #repoMachine 이 선택 상태 보관)
|
|
3374
|
+
function positionMachineMenu(){
|
|
3375
|
+
const b = $('machineSwitch').getBoundingClientRect();
|
|
3376
|
+
const mm = $('machineMenu');
|
|
3377
|
+
mm.style.left = b.left + 'px';
|
|
3378
|
+
mm.style.top = (b.bottom + 4) + 'px';
|
|
3379
|
+
}
|
|
3380
|
+
$('machineSwitch').addEventListener('click', (e)=>{
|
|
3381
|
+
e.stopPropagation();
|
|
3382
|
+
const mm = $('machineMenu');
|
|
3383
|
+
const was = mm.classList.contains('open');
|
|
3384
|
+
closeDropdowns(); mm.classList.remove('open');
|
|
3385
|
+
if (!was){ positionMachineMenu(); mm.classList.add('open'); }
|
|
3386
|
+
});
|
|
3387
|
+
$('machineMenu').addEventListener('click', (e)=>{
|
|
3388
|
+
const o = e.target.closest('.mopt[data-m]'); if(!o) return;
|
|
3389
|
+
$('repoMachine').value = o.dataset.m; syncSelect('repoMachine');
|
|
3390
|
+
$('machineMenu').classList.remove('open');
|
|
3391
|
+
renderRail();
|
|
3392
|
+
});
|
|
3393
|
+
document.addEventListener('click', ()=>$('machineMenu').classList.remove('open'));
|
|
3394
|
+
|
|
3066
3395
|
/* 딥링크 — /?run=N 이면 하이드레이션 후 그 run 모달을 연다 (웹훅 링크·알림용) */
|
|
3067
3396
|
function openFromURL(){
|
|
3068
3397
|
const q = new URLSearchParams(location.search).get('run');
|