coxpit 4.8.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/board.ts +552 -153
- package/src/icons.ts +6 -0
- package/src/login.ts +40 -33
- package/src/server.ts +52 -11
package/src/board.ts
CHANGED
|
@@ -23,8 +23,12 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
23
23
|
*{box-sizing:border-box}
|
|
24
24
|
[hidden]{display:none !important}
|
|
25
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}
|
|
26
30
|
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px;line-height:1.5;
|
|
27
|
-
-webkit-font-smoothing:antialiased}
|
|
31
|
+
-webkit-font-smoothing:antialiased;overflow-x:hidden;overscroll-behavior-x:none}
|
|
28
32
|
::selection{background:var(--brand-dim)}
|
|
29
33
|
::-webkit-scrollbar{width:9px;height:9px}
|
|
30
34
|
::-webkit-scrollbar-thumb{background:#252c3a;border-radius:5px;border:2px solid var(--bg)}
|
|
@@ -72,9 +76,29 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
72
76
|
input:focus,select:focus,textarea:focus{border-color:rgba(78,201,176,.55);outline:none}
|
|
73
77
|
input::placeholder,textarea::placeholder{color:var(--faint)}
|
|
74
78
|
textarea{resize:vertical;min-height:56px;line-height:1.5}
|
|
79
|
+
/* no native browser chrome — any bare <select> strips the OS arrow and draws a token chevron
|
|
80
|
+
(dressed selects hide entirely; this covers the undressed fallback like #archRepo) */
|
|
81
|
+
select{appearance:none;-webkit-appearance:none;-moz-appearance:none;padding-right:26px;
|
|
82
|
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238792a2' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
|
83
|
+
background-repeat:no-repeat;background-position:right 8px center;background-size:13px}
|
|
84
|
+
select option{background:#12151c;color:var(--ink)}
|
|
85
|
+
/* kill the native number spinner — a token stepper (below) replaces it */
|
|
86
|
+
input[type=number]{appearance:none;-webkit-appearance:none;-moz-appearance:textfield}
|
|
87
|
+
input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
|
|
75
88
|
.row{display:flex;gap:8px}
|
|
76
89
|
.row>*{flex:1}
|
|
77
90
|
.row .narrow{flex:0 0 64px}
|
|
91
|
+
/* count stepper — token − N + (btn-ghost feel, dark, no native spinner) */
|
|
92
|
+
.stepper{display:flex;align-items:center;gap:0;flex:0 0 auto;border:1px solid var(--line);
|
|
93
|
+
border-radius:var(--r-ctl);background:#0e1118;overflow:hidden}
|
|
94
|
+
.stepper .stp{display:inline-flex;align-items:center;justify-content:center;width:26px;height:32px;
|
|
95
|
+
background:transparent;border:none;color:var(--muted);cursor:pointer;font-size:15px;line-height:1;
|
|
96
|
+
transition:background .15s,color .15s}
|
|
97
|
+
.stepper .stp:hover{background:var(--surface2);color:var(--ink)}
|
|
98
|
+
.stepper .stp .ic{width:14px;height:14px}
|
|
99
|
+
.stepper input.narrow{flex:0 0 30px;width:30px;text-align:center;border:none;border-radius:0;
|
|
100
|
+
background:transparent;padding:8px 0;font-variant-numeric:tabular-nums}
|
|
101
|
+
.stepper input.narrow:focus{border:none}
|
|
78
102
|
.check{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);cursor:pointer;user-select:none}
|
|
79
103
|
.check input{width:auto;accent-color:var(--brand)}
|
|
80
104
|
|
|
@@ -82,6 +106,95 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
82
106
|
color:var(--faint);margin:2px 0 -2px}
|
|
83
107
|
.btn[disabled]{opacity:.4;cursor:not-allowed;filter:none}
|
|
84
108
|
|
|
109
|
+
/* ── navigator rail (v5.0) — machine · repos · view nav · New ── */
|
|
110
|
+
aside.rail{padding:12px 10px;gap:4px;overflow-y:auto}
|
|
111
|
+
.rail .machine{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11.5px;
|
|
112
|
+
color:var(--ink);border:1px solid var(--line-hi);border-radius:8px;padding:7px 10px;margin-bottom:6px;
|
|
113
|
+
cursor:pointer;background:transparent;text-align:left;width:100%}
|
|
114
|
+
.rail .machine:hover{border-color:rgba(78,201,176,.4)}
|
|
115
|
+
.rail .machine .ic{width:14px;height:14px;color:var(--muted)}
|
|
116
|
+
.rail .machine .msp{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
117
|
+
.rail .machine .mdot2{width:6px;height:6px;border-radius:50%;background:var(--faint);flex:none}
|
|
118
|
+
.rail .machine .mdot2.on{background:var(--s-done)}
|
|
119
|
+
.rlabel{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.13em;
|
|
120
|
+
color:var(--faint);margin:8px 6px 4px}
|
|
121
|
+
/* rail repo rows — #repoList + the add row(#repoAdd) 만 (라이브러리 캡처 .repo 는 건드리지 않음) */
|
|
122
|
+
#repoList .repo,.repo.add{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:8px;cursor:pointer;
|
|
123
|
+
color:var(--muted);font-family:var(--sans);font-size:13px;border:none;background:transparent;
|
|
124
|
+
position:relative;width:100%;text-align:left}
|
|
125
|
+
#repoList .repo:hover,.repo.add:hover{background:var(--surface2);color:var(--ink)}
|
|
126
|
+
#repoList .repo.on{background:var(--brand-dim);color:var(--ink)}
|
|
127
|
+
#repoList .repo .ic,.repo.add .ic{width:15px;height:15px;color:var(--faint)}
|
|
128
|
+
#repoList .repo.on .ic{color:var(--brand)}
|
|
129
|
+
#repoList .repo .nm,.repo.add .nm{flex:1;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:inherit}
|
|
130
|
+
#repoList .repo .cnt{font-family:var(--mono);font-size:10px;color:var(--brand);background:var(--brand-dim);
|
|
131
|
+
border-radius:99px;padding:1px 7px;flex:none}
|
|
132
|
+
#repoList .repo .attn{width:7px;height:7px;border-radius:50%;background:var(--s-failed);flex:none;
|
|
133
|
+
box-shadow:0 0 0 2px var(--surface)}
|
|
134
|
+
.repo.add{color:var(--faint);font-size:12px}
|
|
135
|
+
#repoList .repo .rowmenu{display:none;align-items:center;gap:2px;flex:none}
|
|
136
|
+
#repoList .repo:hover .rowmenu{display:flex}
|
|
137
|
+
#repoList .repo:hover .cnt{display:none}
|
|
138
|
+
/* rail repo-row actions — icon-ghost, never white: transparent bg · muted icon · hover ink + surface */
|
|
139
|
+
#repoList .repo .rmbtn{background:transparent;border:1px solid transparent;color:var(--muted);cursor:pointer;
|
|
140
|
+
padding:3px;display:inline-flex;border-radius:6px;transition:color .15s,background .15s,border-color .15s}
|
|
141
|
+
#repoList .repo .rmbtn:hover{color:var(--ink);background:var(--surface);border-color:var(--line-hi)}
|
|
142
|
+
#repoList .repo .rmbtn .ic{width:13px;height:13px;color:inherit}
|
|
143
|
+
.navi{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:8px;color:var(--muted);
|
|
144
|
+
cursor:pointer;font-size:13px;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
|
|
145
|
+
.navi:hover{background:var(--surface2)}
|
|
146
|
+
.navi.on{background:var(--surface2);color:var(--ink)}
|
|
147
|
+
.navi .ic{width:15px;height:15px}
|
|
148
|
+
.navi.on .ic{color:var(--brand)}
|
|
149
|
+
.navi .nsp{flex:1}
|
|
150
|
+
.navi .n{font-family:var(--mono);font-size:10.5px;color:var(--faint)}
|
|
151
|
+
.railsp{flex:1;min-height:8px}
|
|
152
|
+
.newbtn{display:flex;align-items:center;justify-content:center;gap:7px;background:var(--brand);
|
|
153
|
+
color:var(--brand-ink);border:none;border-radius:var(--r-ctl);font-family:var(--sans);font-weight:700;
|
|
154
|
+
font-size:13px;padding:10px;cursor:pointer;margin-top:8px}
|
|
155
|
+
.newbtn:hover{filter:brightness(1.08)}
|
|
156
|
+
.newbtn[disabled]{opacity:.4;cursor:not-allowed;filter:none}
|
|
157
|
+
.newbtn .ic{width:16px;height:16px}
|
|
158
|
+
.newnote{font-family:var(--mono);font-size:9.5px;color:var(--faint);text-align:center;margin-top:5px}
|
|
159
|
+
/* mobile FAB (v5.0 pocket board) — floating + opens the compose sheet; hidden on desktop */
|
|
160
|
+
.fab{display:none;position:fixed;z-index:28;right:18px;
|
|
161
|
+
right:calc(18px + env(safe-area-inset-right));bottom:20px;bottom:calc(20px + env(safe-area-inset-bottom));
|
|
162
|
+
width:52px;height:52px;border-radius:50%;border:none;background:var(--brand);color:var(--brand-ink);
|
|
163
|
+
box-shadow:0 6px 20px rgba(0,0,0,.4);cursor:pointer;align-items:center;justify-content:center}
|
|
164
|
+
.fab:active{filter:brightness(.94)}
|
|
165
|
+
.fab .ic{width:24px;height:24px}
|
|
166
|
+
/* machine menu (native select 대체) — 레일 컨텍스트 앵커 */
|
|
167
|
+
.mmenu{position:absolute;z-index:45;min-width:170px;background:var(--surface2);border:1px solid var(--line-hi);
|
|
168
|
+
border-radius:9px;box-shadow:var(--shadow);padding:4px;display:none}
|
|
169
|
+
.mmenu.open{display:block}
|
|
170
|
+
.mmenu .mopt{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;
|
|
171
|
+
font-family:var(--mono);font-size:12px;color:var(--muted);cursor:pointer;white-space:nowrap}
|
|
172
|
+
.mmenu .mopt:hover{background:var(--surface);color:var(--ink)}
|
|
173
|
+
.mmenu .mopt.on{color:var(--brand)}
|
|
174
|
+
.mmenu .mopt .mdot2{width:6px;height:6px;border-radius:50%;background:var(--faint);flex:none}
|
|
175
|
+
.mmenu .mopt .mdot2.on{background:var(--s-done)}
|
|
176
|
+
/* ── New sheet (v5.0 Part B — focused compose Task|Goal|Workbench) ── */
|
|
177
|
+
.sheet{width:min(400px,94vw);max-height:88vh;background:var(--surface);
|
|
178
|
+
border:1px solid var(--line-hi);border-radius:14px;box-shadow:var(--shadow);
|
|
179
|
+
display:flex;flex-direction:column;gap:13px;padding:16px 18px 16px}
|
|
180
|
+
.sheet-h{display:flex;align-items:center;gap:9px;font-family:var(--mono);font-size:12px}
|
|
181
|
+
.sheet-h .t{color:var(--ink);font-weight:700}.sheet-h .r{color:var(--faint);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
182
|
+
.sheet-h .sp{flex:1}.sheet-h .x{color:var(--muted);cursor:pointer;background:none;border:none;padding:2px}
|
|
183
|
+
.sheet #taskForm{display:flex;flex-direction:column;gap:13px;min-height:0}
|
|
184
|
+
.sheet-body{display:flex;flex-direction:column;gap:8px;overflow-y:auto;min-height:0;max-height:60vh;padding:1px}
|
|
185
|
+
.sheet-f{display:flex;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--line)}
|
|
186
|
+
.sheet-f #modeSeg{flex:1}
|
|
187
|
+
.sheet-f #runFleetBtn{flex:0 0 auto}
|
|
188
|
+
/* progressive Options reveal (rarely-used Task fields) */
|
|
189
|
+
.opt{border:1px solid var(--line);border-radius:var(--r-ctl);background:#0e1118;overflow:hidden}
|
|
190
|
+
.opt-h{width:100%;display:flex;align-items:center;gap:8px;background:transparent;border:none;cursor:pointer;
|
|
191
|
+
padding:8px 10px;font-size:12px;font-weight:600;color:var(--muted);text-align:left}
|
|
192
|
+
.opt-h:hover{color:var(--ink)}
|
|
193
|
+
.opt-h .opt-car{width:14px;height:14px;color:var(--faint);transform:rotate(0deg);transition:transform .16s}
|
|
194
|
+
.opt-h[aria-expanded="true"] .opt-car{transform:rotate(90deg)}
|
|
195
|
+
.opt-h .opt-sub{margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--faint);font-weight:500}
|
|
196
|
+
.opt-b{display:flex;flex-direction:column;gap:8px;padding:2px 10px 11px}
|
|
197
|
+
|
|
85
198
|
/* ── repo browser ── */
|
|
86
199
|
.brw{width:min(560px,94vw);max-height:78vh;background:var(--surface);border:1px solid var(--line);
|
|
87
200
|
border-radius:14px;box-shadow:var(--shadow);display:flex;flex-direction:column;overflow:hidden}
|
|
@@ -119,14 +232,34 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
119
232
|
.dd-opt:hover{background:var(--surface);color:var(--ink)}
|
|
120
233
|
.dd-opt.on{color:var(--brand)}
|
|
121
234
|
.dd-opt.on::before{content:'✓ ';font-size:10px}
|
|
235
|
+
/* model combo — free-text input + a .dd-style recent-models menu (no native datalist/white popup) */
|
|
236
|
+
.mcombo{position:relative;display:flex;align-items:stretch;gap:0}
|
|
237
|
+
.mcombo input{border-top-right-radius:0;border-bottom-right-radius:0;border-right:none}
|
|
238
|
+
.mcombo .mc-tog{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:30px;
|
|
239
|
+
background:#0e1118;color:var(--faint);border:1px solid var(--line);border-left:none;
|
|
240
|
+
border-top-right-radius:var(--r-ctl);border-bottom-right-radius:var(--r-ctl);cursor:pointer;
|
|
241
|
+
transition:color .15s,border-color .15s}
|
|
242
|
+
.mcombo .mc-tog:hover{color:var(--ink)}
|
|
243
|
+
.mcombo .mc-tog .ic{width:13px;height:13px;transition:transform .15s}
|
|
244
|
+
.mcombo.open input,.mcombo.open .mc-tog{border-color:rgba(78,201,176,.55)}
|
|
245
|
+
.mcombo.open .mc-tog .ic{transform:rotate(180deg)}
|
|
246
|
+
.mcombo .dd-panel{top:calc(100% + 5px)}
|
|
247
|
+
.mcombo.open .dd-panel{display:block}
|
|
248
|
+
.mc-empty{padding:7px 10px;font-family:var(--mono);font-size:11px;color:var(--faint)}
|
|
122
249
|
.seg{display:flex;gap:3px;padding:3px;border:1px solid var(--line);border-radius:var(--r-ctl);background:#0e1118}
|
|
123
|
-
|
|
250
|
+
/* slim single-row option — matches the type/provider seg height; the risky hint rides inline, subtle */
|
|
251
|
+
.seg-opt{flex:1;display:inline-flex;flex-direction:row;align-items:center;justify-content:center;gap:5px;
|
|
252
|
+
padding:6px 8px;border:none;min-width:0;
|
|
124
253
|
background:transparent;color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;
|
|
125
254
|
border-radius:calc(var(--r-ctl) - 3px);transition:background .15s,color .15s}
|
|
126
255
|
.seg-opt:hover{color:var(--ink)}
|
|
127
256
|
.seg-opt.on{background:var(--surface2);color:var(--ink)}
|
|
128
257
|
.seg-opt[data-real="1"].on{background:var(--brand);color:var(--brand-ink)}
|
|
129
|
-
.seg-hint{font-family:var(--mono);font-size:
|
|
258
|
+
.seg-hint{font-family:var(--mono);font-size:9px;font-weight:500;text-transform:uppercase;letter-spacing:.05em;
|
|
259
|
+
opacity:.6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
260
|
+
/* the credit hint only reads once the risky option is chosen — keeps the resting seg clean & slim */
|
|
261
|
+
.seg-opt[data-real="1"]:not(.on) .seg-hint{display:none}
|
|
262
|
+
.seg-opt[data-real="1"].on .seg-hint{opacity:.75}
|
|
130
263
|
/* ── deliverable 계약 칩(Task compose) ── */
|
|
131
264
|
.ochips{display:flex;flex-wrap:wrap;gap:5px}
|
|
132
265
|
.ochip{background:transparent;color:var(--muted);border:1px solid var(--line);cursor:pointer;
|
|
@@ -478,8 +611,8 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
478
611
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
479
612
|
|
|
480
613
|
/* ── AI review panel (compare) ── */
|
|
481
|
-
.cmp-review{
|
|
482
|
-
max-
|
|
614
|
+
.cmp-review{flex:1;min-height:0;overflow:auto;background:var(--surface2);
|
|
615
|
+
padding:20px max(20px,calc((100% - 880px)/2));font-size:13.5px;line-height:1.7;color:var(--muted)}
|
|
483
616
|
.cmp-review[hidden]{display:none}
|
|
484
617
|
.cmp-review h2{font-size:13px;color:var(--brand);margin:14px 0 6px;letter-spacing:.02em}
|
|
485
618
|
.cmp-review h3{font-size:12.5px;color:var(--ink);margin:12px 0 4px}
|
|
@@ -507,7 +640,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
507
640
|
#outDetail .dl-line{cursor:pointer;border-radius:3px}
|
|
508
641
|
#outDetail .dl-line:hover{background:rgba(78,201,176,.09)}
|
|
509
642
|
|
|
510
|
-
/* ── v4.7
|
|
643
|
+
/* ── v4.7 outputs contract (contract strip) — required-outputs summary under the modal header ── */
|
|
511
644
|
.contract{display:flex;flex-wrap:wrap;align-items:center;gap:7px;padding:9px 18px;
|
|
512
645
|
border-bottom:1px solid var(--line);background:var(--surface2);
|
|
513
646
|
font-family:var(--mono);font-size:10.5px;color:var(--faint);line-height:1.5}
|
|
@@ -580,11 +713,14 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
580
713
|
.layout{grid-template-columns:1fr;min-height:calc(100dvh - 55px)}
|
|
581
714
|
/* 사이드바 = 오프캔버스 드로어 — 플릿이 첫 화면, 런처는 ☰ 뒤에 */
|
|
582
715
|
.menu-btn{display:inline-flex}
|
|
583
|
-
aside{position:fixed;top:54px;bottom:0;left:0;width:min(86vw,340px);z-index:30;
|
|
584
|
-
transform:translateX(-103%);transition:transform .22s ease;overflow-y:auto;
|
|
716
|
+
aside{position:fixed;top:54px;bottom:0;left:0;width:min(86vw,340px);max-width:100vw;z-index:30;
|
|
717
|
+
transform:translateX(-103%);transition:transform .22s ease;overflow-y:auto;overflow-x:hidden;
|
|
585
718
|
-webkit-overflow-scrolling:touch;box-shadow:var(--shadow)}
|
|
586
719
|
aside.open{transform:translateX(0)}
|
|
587
720
|
.scrim.on{display:block}
|
|
721
|
+
/* pocket board — drawer is JUST the navigator; the + FAB opens the compose sheet */
|
|
722
|
+
.fab{display:inline-flex}
|
|
723
|
+
#newBtn,.newnote{display:none}
|
|
588
724
|
header{padding:0 12px;gap:9px}
|
|
589
725
|
.brand .sub,.daemon-badge,.machines{display:none}
|
|
590
726
|
main{padding:14px}
|
|
@@ -615,25 +751,74 @@ ${ICON_SPRITE}
|
|
|
615
751
|
<button class="btn-ghost sm menu-btn" id="menuBtn" aria-label="open launcher">☰</button>
|
|
616
752
|
<div class="brand"><span class="mark">coxpit</span><span class="sub">fleet console</span></div>
|
|
617
753
|
<span class="daemon-badge" id="daemonBadge" style="display:none"></span>
|
|
618
|
-
<div class="seg view-seg" id="viewSeg">
|
|
619
|
-
<button type="button" class="seg-opt on" data-view="active">Active</button>
|
|
620
|
-
<button type="button" class="seg-opt" data-view="archive">Archive <span id="archN" class="seg-hint"></span></button>
|
|
621
|
-
</div>
|
|
622
754
|
<div class="ws"><span class="dot" id="wsdot"></span><span id="wstext">connecting</span></div>
|
|
623
755
|
<button class="btn-ghost sm" id="bell" title="notify when a run settles"><svg class="ic"><use href="#i-bell-off"/></svg></button>
|
|
624
756
|
<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>
|
|
625
757
|
<div class="machines" id="machines"></div>
|
|
626
758
|
</header>
|
|
627
759
|
<div class="scrim" id="scrim"></div>
|
|
760
|
+
<!-- pocket board FAB (v5.0 Part C) — mobile-only; opens the compose sheet -->
|
|
761
|
+
<button type="button" class="fab" id="fab" aria-label="new task"><svg class="ic"><use href="#i-plus"/></svg></button>
|
|
628
762
|
<div class="layout">
|
|
629
|
-
<aside>
|
|
630
|
-
<
|
|
631
|
-
<
|
|
632
|
-
<
|
|
633
|
-
<
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
763
|
+
<aside class="rail">
|
|
764
|
+
<button type="button" class="machine" id="machineSwitch">
|
|
765
|
+
<svg class="ic"><use href="#i-server"/></svg>
|
|
766
|
+
<span class="msp" id="machineLbl">local</span>
|
|
767
|
+
<span class="mdot2" id="machineDot"></span>
|
|
768
|
+
</button>
|
|
769
|
+
<div class="mmenu" id="machineMenu"></div>
|
|
770
|
+
|
|
771
|
+
<p class="rlabel">Repositories</p>
|
|
772
|
+
<div id="repoList"></div>
|
|
773
|
+
<button type="button" class="repo add" id="repoAdd">
|
|
774
|
+
<svg class="ic"><use href="#i-plus"/></svg><span class="nm">Add repository…</span>
|
|
775
|
+
</button>
|
|
776
|
+
|
|
777
|
+
<p class="rlabel">View</p>
|
|
778
|
+
<nav id="viewNav">
|
|
779
|
+
<button type="button" class="navi on" data-view="active">
|
|
780
|
+
<svg class="ic"><use href="#i-layers"/></svg><span>Active</span><span class="nsp"></span><span class="n" id="navActiveN"></span>
|
|
781
|
+
</button>
|
|
782
|
+
<button type="button" class="navi" data-view="goals">
|
|
783
|
+
<svg class="ic"><use href="#i-target"/></svg><span>Goals</span><span class="nsp"></span><span class="n" id="navGoalsN"></span>
|
|
784
|
+
</button>
|
|
785
|
+
<button type="button" class="navi" data-view="archive">
|
|
786
|
+
<svg class="ic"><use href="#i-archive"/></svg><span>Archive</span><span class="nsp"></span><span class="n" id="navArchiveN"></span>
|
|
787
|
+
</button>
|
|
788
|
+
</nav>
|
|
789
|
+
|
|
790
|
+
<div class="railsp"></div>
|
|
791
|
+
<button type="button" class="newbtn" id="newBtn"><svg class="ic"><use href="#i-plus"/></svg> New</button>
|
|
792
|
+
<p class="newnote">Task · Goal · Workbench</p>
|
|
793
|
+
|
|
794
|
+
<details class="sect" id="libraryBox" style="margin-top:12px">
|
|
795
|
+
<summary class="sect-label" style="cursor:pointer;list-style:none">Library · design captures ▾</summary>
|
|
796
|
+
<div id="captures" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
|
|
797
|
+
<a id="bmk" class="btn-ghost sm" style="text-decoration:none;text-align:center;display:block;padding:6px;margin-top:6px"
|
|
798
|
+
title="drag me to your bookmarks bar, then click it on your running app">⌖ coxpit inspect</a>
|
|
799
|
+
<span style="font-size:11px;color:var(--faint)">Drag to bookmarks. Click it on your app, then click an element.
|
|
800
|
+
With auth on, append ?k=<pass> to the script URL.</span>
|
|
801
|
+
</details>
|
|
802
|
+
</aside>
|
|
803
|
+
|
|
804
|
+
<!-- New sheet (v5.0 Part B — focused compose: Task | Goal | Workbench) -->
|
|
805
|
+
<div class="overlay" id="newSheet">
|
|
806
|
+
<div class="sheet" id="sheetCard">
|
|
807
|
+
<div class="sheet-h"><svg class="ic" style="color:var(--brand)"><use href="#i-plus"/></svg>
|
|
808
|
+
<span class="t">New</span><span class="r" id="sheetRepoLbl"></span><span class="sp"></span>
|
|
809
|
+
<button type="button" class="x" id="sheetClose" aria-label="close"><svg class="ic"><use href="#i-x"/></svg></button></div>
|
|
810
|
+
|
|
811
|
+
<div class="seg" id="launchTabs" role="group" aria-label="launch type">
|
|
812
|
+
<button type="button" class="seg-opt on" data-tab="task">Task</button>
|
|
813
|
+
<button type="button" class="seg-opt" data-tab="goal">Goal</button>
|
|
814
|
+
<button type="button" class="seg-opt" data-tab="bench">Workbench</button>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
817
|
+
<!-- hidden state selects (target machine/repo — driven by the rail + repo picker) -->
|
|
818
|
+
<select id="repoMachine" hidden></select>
|
|
819
|
+
<select id="taskRepo" hidden></select>
|
|
820
|
+
<!-- repo picker affordances (opened from the Add-repository rail button / handlers) -->
|
|
821
|
+
<div class="row" id="repoActions" hidden>
|
|
637
822
|
<button type="button" class="btn-ghost sm" id="repoBrowse" style="flex:1">Browse…</button>
|
|
638
823
|
<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>
|
|
639
824
|
<button type="button" class="btn-ghost sm" id="repoManual" style="flex:0 0 auto" title="type an absolute path">Path</button>
|
|
@@ -646,68 +831,75 @@ ${ICON_SPRITE}
|
|
|
646
831
|
<button class="btn-ghost sm" type="submit" style="flex:0 0 auto">Register</button>
|
|
647
832
|
</div>
|
|
648
833
|
</form>
|
|
649
|
-
</div>
|
|
650
834
|
|
|
651
|
-
<div class="sect">
|
|
652
|
-
<p class="sect-label">Start</p>
|
|
653
|
-
<div class="seg" id="launchTabs">
|
|
654
|
-
<button type="button" class="seg-opt on" data-tab="task">Task</button>
|
|
655
|
-
<button type="button" class="seg-opt" data-tab="goal">Goal</button>
|
|
656
|
-
<button type="button" class="seg-opt" data-tab="bench">Workbench</button>
|
|
657
|
-
</div>
|
|
658
835
|
<form id="taskForm">
|
|
659
|
-
<div
|
|
660
|
-
<
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
<
|
|
665
|
-
|
|
836
|
+
<div class="sheet-body">
|
|
837
|
+
<div id="panelTask" style="display:flex;flex-direction:column;gap:8px">
|
|
838
|
+
<input id="taskTitle" placeholder="Task title" />
|
|
839
|
+
<textarea id="taskPrompt" placeholder="Prompt — target files, constraints, how to verify"></textarea>
|
|
840
|
+
<button type="button" class="btn-ghost sm" id="ghImport">From GitHub issue / PR…</button>
|
|
841
|
+
<div class="seg" id="provSeg" role="group" aria-label="agent provider">
|
|
842
|
+
<button type="button" class="seg-opt on" data-agent="claude-code">Claude</button>
|
|
843
|
+
<button type="button" class="seg-opt" data-agent="codex">Codex</button>
|
|
844
|
+
</div>
|
|
845
|
+
<div class="opt" id="taskOpt">
|
|
846
|
+
<button type="button" class="opt-h" id="taskOptToggle" aria-expanded="false">
|
|
847
|
+
<svg class="ic opt-car"><use href="#i-chevron"/></svg>
|
|
848
|
+
<span>Options</span><span class="opt-sub">model · design · deliverables</span>
|
|
849
|
+
</button>
|
|
850
|
+
<div class="opt-b" id="taskOptBody" hidden>
|
|
851
|
+
<p class="flabel">model · optional</p>
|
|
852
|
+
<div class="mcombo" id="modelCombo">
|
|
853
|
+
<input id="taskModel" placeholder="CLI default" autocomplete="off" spellcheck="false" />
|
|
854
|
+
<button type="button" class="mc-tog" id="modelTog" aria-label="recent models" title="recent models"><svg class="ic"><use href="#i-chevron-down"/></svg></button>
|
|
855
|
+
<div class="dd-panel" id="modelMenu"></div>
|
|
856
|
+
</div>
|
|
857
|
+
<p class="flabel">design capture · optional</p>
|
|
858
|
+
<select id="taskCapture"><option value="">no design capture</option></select>
|
|
859
|
+
<p class="flabel">deliverables · optional (contract)</p>
|
|
860
|
+
<div class="ochips" id="taskOutputs" role="group" aria-label="declared deliverables">
|
|
861
|
+
<button type="button" class="ochip" data-out="answer">Answer</button>
|
|
862
|
+
<button type="button" class="ochip" data-out="code">Code</button>
|
|
863
|
+
<button type="button" class="ochip" data-out="doc">Doc</button>
|
|
864
|
+
<button type="button" class="ochip" data-out="page">Page</button>
|
|
865
|
+
<button type="button" class="ochip" data-out="file">File</button>
|
|
866
|
+
</div>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
666
869
|
</div>
|
|
667
|
-
<
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
<
|
|
676
|
-
<
|
|
677
|
-
<button type="button" class="ochip" data-out="page">페이지</button>
|
|
678
|
-
<button type="button" class="ochip" data-out="file">파일</button>
|
|
870
|
+
<div id="panelGoal" hidden style="flex-direction:column;gap:8px">
|
|
871
|
+
<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>
|
|
872
|
+
<div class="seg" id="provSegGoal" role="group" aria-label="agent provider">
|
|
873
|
+
<button type="button" class="seg-opt on" data-agent="claude-code">Claude</button>
|
|
874
|
+
<button type="button" class="seg-opt" data-agent="codex">Codex</button>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
<div id="panelBench" hidden style="flex-direction:column;gap:8px">
|
|
878
|
+
<input id="benchTitle" placeholder="Workbench name · optional" />
|
|
879
|
+
<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>
|
|
679
880
|
</div>
|
|
680
881
|
</div>
|
|
681
|
-
<div
|
|
682
|
-
<
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
<
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
<input type="checkbox" id="taskReal" hidden />
|
|
693
|
-
<div class="row">
|
|
694
|
-
<input id="taskCount" class="narrow" type="number" min="1" max="8" value="1" title="agents — 1 for a job, N to explore variants" />
|
|
882
|
+
<div class="sheet-f" id="sheetFooter">
|
|
883
|
+
<div class="seg" id="modeSeg" role="group" aria-label="agent mode">
|
|
884
|
+
<button type="button" class="seg-opt" data-real="0">Dry run</button>
|
|
885
|
+
<button type="button" class="seg-opt" data-real="1">Real agent<span class="seg-hint">spends credits</span></button>
|
|
886
|
+
</div>
|
|
887
|
+
<input type="checkbox" id="taskReal" hidden />
|
|
888
|
+
<div class="stepper" id="taskCountStep" title="agents — 1 for a job, N to explore variants">
|
|
889
|
+
<button type="button" class="stp" id="cntDown" aria-label="fewer agents"><svg class="ic"><use href="#i-minus"/></svg></button>
|
|
890
|
+
<input id="taskCount" class="narrow" type="number" min="1" max="8" value="1" inputmode="numeric" aria-label="agent count" />
|
|
891
|
+
<button type="button" class="stp" id="cntUp" aria-label="more agents"><svg class="ic"><use href="#i-plus"/></svg></button>
|
|
892
|
+
</div>
|
|
695
893
|
<button class="btn" type="submit" id="runFleetBtn"><svg class="ic"><use href="#i-play"/></svg> Run fleet</button>
|
|
696
894
|
</div>
|
|
697
895
|
</form>
|
|
698
896
|
</div>
|
|
699
|
-
|
|
700
|
-
<details class="sect" id="libraryBox">
|
|
701
|
-
<summary class="sect-label" style="cursor:pointer;list-style:none">Library · design captures ▾</summary>
|
|
702
|
-
<div id="captures" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
|
|
703
|
-
<a id="bmk" class="btn-ghost sm" style="text-decoration:none;text-align:center;display:block;padding:6px;margin-top:6px"
|
|
704
|
-
title="drag me to your bookmarks bar, then click it on your running app">⌖ coxpit inspect</a>
|
|
705
|
-
<span style="font-size:11px;color:var(--faint)">Drag to bookmarks. Click it on your app, then click an element.
|
|
706
|
-
With auth on, append ?k=<pass> to the script URL.</span>
|
|
707
|
-
</details>
|
|
708
|
-
</aside>
|
|
897
|
+
</div>
|
|
709
898
|
<main>
|
|
710
|
-
<div class="toolbar"
|
|
899
|
+
<div class="toolbar" style="display:flex;align-items:center;gap:12px">
|
|
900
|
+
<span id="boardHint" style="font-family:var(--mono);font-size:11px;color:var(--faint)"></span>
|
|
901
|
+
<span style="flex:1"></span>
|
|
902
|
+
<button class="btn-ghost sm" id="selToggle">Select runs</button></div>
|
|
711
903
|
<div class="grid" id="grid"></div>
|
|
712
904
|
<div class="empty" id="empty">
|
|
713
905
|
<span class="glyph">▚▞▚</span>
|
|
@@ -746,7 +938,7 @@ ${ICON_SPRITE}
|
|
|
746
938
|
<div id="outWrap">
|
|
747
939
|
<div id="outCards"><div class="ocards-empty">loading…</div></div>
|
|
748
940
|
<div id="outDetail">
|
|
749
|
-
<div class="oback" id="outBack">‹
|
|
941
|
+
<div class="oback" id="outBack">‹ Outputs</div>
|
|
750
942
|
<div class="odetail-c" id="outDetailC"></div>
|
|
751
943
|
</div>
|
|
752
944
|
</div>
|
|
@@ -786,10 +978,10 @@ ${ICON_SPRITE}
|
|
|
786
978
|
<div class="chips" id="roomChips"></div>
|
|
787
979
|
<div class="gbar" id="roomGbar">
|
|
788
980
|
<span class="selc" id="roomSelC">☑ 0 selected</span>
|
|
789
|
-
<button type="button" class="brand" id="roomIntegrateSel">Integrate
|
|
981
|
+
<button type="button" class="brand" id="roomIntegrateSel">Integrate selected (0)</button>
|
|
790
982
|
<span class="sp"></span>
|
|
791
|
-
<button type="button" id="roomReviewAll"
|
|
792
|
-
<button type="button" class="danger" id="roomGroupClose"
|
|
983
|
+
<button type="button" id="roomReviewAll">Review all</button>
|
|
984
|
+
<button type="button" class="danger" id="roomGroupClose">Close group</button>
|
|
793
985
|
</div>
|
|
794
986
|
<div class="body">
|
|
795
987
|
<div class="runs" id="roomRuns"></div>
|
|
@@ -982,6 +1174,9 @@ const runs = new Map(); // runId -> run object
|
|
|
982
1174
|
const tasks = new Map(); // taskId -> task
|
|
983
1175
|
const groups = new Map(); // groupId -> {id, kind, title}
|
|
984
1176
|
let repos = [], machines = [], captures = [];
|
|
1177
|
+
// v5.0 rail — 선택된 repo 로 보드 스코프(client-side). null = All repositories.
|
|
1178
|
+
let selectedRepo = null;
|
|
1179
|
+
try { const s = localStorage.getItem('coxpit.repo'); selectedRepo = s ? Number(s) : null; } catch {}
|
|
985
1180
|
let daemonPort = 8210; // real config.port — filled from /api/fleet daemon block (recipes interpolate it)
|
|
986
1181
|
let remoteAuthOpen = false; // true = no password → Funnel guard on (from /api/fleet daemon.authOpen)
|
|
987
1182
|
|
|
@@ -1255,11 +1450,16 @@ function bandHTML(g, grpRuns){
|
|
|
1255
1450
|
+ '<button class="gband-fold" data-gfold="'+g.id+'" title="fold">'+(folded?'▸':'▾')+'</button></div>'
|
|
1256
1451
|
+ '<div class="gband-grid">'+cards+'</div></div>';
|
|
1257
1452
|
}
|
|
1453
|
+
// v5.0 — 레일에서 고른 repo 로 run 을 스코프(client-side). selectedRepo=null → 전체.
|
|
1454
|
+
function runInScope(r){
|
|
1455
|
+
if (selectedRepo==null) return true;
|
|
1456
|
+
const t = tasks.get(r.taskId);
|
|
1457
|
+
return t ? t.repoId===selectedRepo : false;
|
|
1458
|
+
}
|
|
1258
1459
|
function render(){
|
|
1259
1460
|
if (boardView==='archive') return; // 아카이브 뷰는 자체 리스트 — grid 안 건드림
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
if (!list.length){ paintOnboarding(); $('grid').innerHTML=''; return; }
|
|
1461
|
+
const goalsOnly = boardView==='goals'; // Goals 뷰 = 그룹 밴드만
|
|
1462
|
+
const list = [...runs.values()].filter(runInScope).sort((a,b)=>b.id-a.id);
|
|
1263
1463
|
// 그룹 파티션 — grouped run 은 밴드로 클러스터, ungrouped 는 뒤에 flat.
|
|
1264
1464
|
const byGroup = new Map(); const flat = [];
|
|
1265
1465
|
for (const r of list){
|
|
@@ -1268,12 +1468,25 @@ function render(){
|
|
|
1268
1468
|
if (gid==null) flat.push(r);
|
|
1269
1469
|
else { if(!byGroup.has(gid)) byGroup.set(gid, []); byGroup.get(gid).push(r); }
|
|
1270
1470
|
}
|
|
1471
|
+
const visible = goalsOnly ? byGroup.size : list.length;
|
|
1472
|
+
$('empty').style.display = visible ? 'none' : 'flex';
|
|
1473
|
+
renderRail(); // 레일 카운트 배지·attention 을 라이브로 (paintNavCounts·boardHint 포함)
|
|
1474
|
+
if (!visible){ paintOnboarding(goalsOnly); $('grid').innerHTML=''; return; }
|
|
1271
1475
|
let html = '';
|
|
1272
1476
|
for (const gid of [...byGroup.keys()].sort((a,b)=>b-a)) html += bandHTML(groups.get(gid), byGroup.get(gid));
|
|
1273
|
-
html += flat.map(cardHTML).join('');
|
|
1477
|
+
if (!goalsOnly) html += flat.map(cardHTML).join('');
|
|
1274
1478
|
$('grid').innerHTML = html;
|
|
1479
|
+
paintBoardHint();
|
|
1275
1480
|
if (termRunId!=null) termTabsRender(); // 터미널 열려있으면 세션 탭도 동기화
|
|
1276
1481
|
}
|
|
1482
|
+
// 보드 상단 힌트: <repo> · <view> · N runs (스코프 반영)
|
|
1483
|
+
function paintBoardHint(){
|
|
1484
|
+
const el = $('boardHint'); if(!el) return;
|
|
1485
|
+
const scope = selectedRepo!=null ? (repos.find(r=>r.id===selectedRepo)||{}).name || 'repo' : 'All repositories';
|
|
1486
|
+
const label = boardView==='goals' ? 'Goals' : 'Active';
|
|
1487
|
+
const n = [...runs.values()].filter(runInScope).length;
|
|
1488
|
+
el.textContent = scope + ' · ' + label + ' · ' + n + ' run' + (n===1?'':'s');
|
|
1489
|
+
}
|
|
1277
1490
|
|
|
1278
1491
|
/* ── active / archive view ─────────────────────────────── */
|
|
1279
1492
|
let boardView = 'active';
|
|
@@ -1281,7 +1494,7 @@ let archOffset = 0, archTotal = 0;
|
|
|
1281
1494
|
const ARCH_LIMIT = 50;
|
|
1282
1495
|
function setView(v){
|
|
1283
1496
|
boardView = v;
|
|
1284
|
-
document.querySelectorAll('#
|
|
1497
|
+
document.querySelectorAll('#viewNav .navi').forEach(b=>{
|
|
1285
1498
|
const on = b.dataset.view===v;
|
|
1286
1499
|
b.classList.toggle('on', on); b.setAttribute('aria-pressed', on?'true':'false');
|
|
1287
1500
|
});
|
|
@@ -1289,11 +1502,19 @@ function setView(v){
|
|
|
1289
1502
|
$('archive').hidden = !archive;
|
|
1290
1503
|
$('grid').style.display = archive ? 'none' : '';
|
|
1291
1504
|
$('empty').style.display = archive ? 'none' : ($('grid').innerHTML ? 'none' : 'flex');
|
|
1292
|
-
document.querySelector('.toolbar').style.display = archive ? 'none' : '';
|
|
1293
|
-
if (archive){ paintArchRepos(); archFetch(true); reclaimRefresh(); }
|
|
1505
|
+
document.querySelector('.toolbar').style.display = archive ? 'none' : 'flex';
|
|
1506
|
+
if (archive){ paintArchRepos(); $('archRepo').value = selectedRepo!=null ? String(selectedRepo) : ''; archFetch(true); reclaimRefresh(); }
|
|
1294
1507
|
else render();
|
|
1295
1508
|
}
|
|
1296
|
-
document.querySelectorAll('#
|
|
1509
|
+
document.querySelectorAll('#viewNav .navi').forEach(b=>b.addEventListener('click', ()=>setView(b.dataset.view)));
|
|
1510
|
+
// 뷰 nav 카운트 — Active=스코프 run 수, Goals=스코프 그룹 수, Archive=닫힌 태스크 수(hydrate)
|
|
1511
|
+
function paintNavCounts(){
|
|
1512
|
+
const scoped = [...runs.values()].filter(runInScope);
|
|
1513
|
+
$('navActiveN').textContent = scoped.length || '';
|
|
1514
|
+
const gset = new Set();
|
|
1515
|
+
for (const r of scoped){ const t = tasks.get(r.taskId); if (t && t.groupId!=null && groups.has(t.groupId)) gset.add(t.groupId); }
|
|
1516
|
+
$('navGoalsN').textContent = gset.size || '';
|
|
1517
|
+
}
|
|
1297
1518
|
function paintArchRepos(){
|
|
1298
1519
|
const sel = $('archRepo'); const cur = sel.value;
|
|
1299
1520
|
sel.innerHTML = '<option value="">all repos</option>' + repos.map(r=>'<option value="'+r.id+'">'+esc(r.name)+'</option>').join('');
|
|
@@ -1385,7 +1606,15 @@ function chkRow(name, ok, val){
|
|
|
1385
1606
|
return '<div class="chk '+cls+'"><span class="st">'+st+'</span><span class="nm">'+esc(name)+'</span>'
|
|
1386
1607
|
+ '<span class="v">'+esc(val||'')+'</span></div>';
|
|
1387
1608
|
}
|
|
1388
|
-
function paintOnboarding(){
|
|
1609
|
+
function paintOnboarding(goalsOnly){
|
|
1610
|
+
// 스코프/뷰 필터 때문에 비어 보이는 경우(전체엔 run 이 있음) → 가벼운 안내만.
|
|
1611
|
+
const totalRuns = runs.size;
|
|
1612
|
+
if (totalRuns && (goalsOnly || selectedRepo!=null)){
|
|
1613
|
+
const msg = goalsOnly ? 'No goal groups here' : 'No runs in this repository';
|
|
1614
|
+
$('empty').innerHTML = '<span class="glyph">▚▞▚</span><span>'+esc(msg)+'</span>'
|
|
1615
|
+
+ '<span style="color:#3d4657">'+(selectedRepo!=null?'switch to <b>All repositories</b> or hit + New':'plan a Goal with + New')+'</span>';
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1389
1618
|
const r = readiness;
|
|
1390
1619
|
const agentBin = r && r.agent ? r.agent.bin : 'claude';
|
|
1391
1620
|
let checks;
|
|
@@ -1454,7 +1683,7 @@ function upsertRun(patch){
|
|
|
1454
1683
|
async function hydrate(){
|
|
1455
1684
|
const r = await fetch('/api/fleet?view=active').then(x=>x.json());
|
|
1456
1685
|
machines = r.machines||[]; repos = r.repos||[]; captures = r.captures||[];
|
|
1457
|
-
if (r.counts){ const n = r.counts.closedTasks||0; $('
|
|
1686
|
+
if (r.counts){ const n = r.counts.closedTasks||0; $('navArchiveN').textContent = n ? String(n) : ''; }
|
|
1458
1687
|
tasks.clear();
|
|
1459
1688
|
(r.tasks||[]).forEach(t => tasks.set(t.id, t));
|
|
1460
1689
|
groups.clear();
|
|
@@ -1474,6 +1703,8 @@ async function hydrate(){
|
|
|
1474
1703
|
paintSidebar(); render();
|
|
1475
1704
|
}
|
|
1476
1705
|
function paintSidebar(){
|
|
1706
|
+
// 스코프 정합 — 사라진 repo 를 물고 있으면 스코프 해제
|
|
1707
|
+
if (selectedRepo!=null && !repos.some(r=>r.id===selectedRepo)){ selectedRepo = null; try{ localStorage.removeItem('coxpit.repo'); }catch{} }
|
|
1477
1708
|
$('machines').innerHTML = machines.map(m =>
|
|
1478
1709
|
'<span class="mchip"><span class="mdot '+(m.online?'on':'')+'"></span><b>'+esc(m.slug)+'</b></span>').join('');
|
|
1479
1710
|
$('repoMachine').innerHTML = machines.map(m=>'<option value="'+esc(m.slug)+'">'+esc(m.slug)+'</option>').join('');
|
|
@@ -1481,8 +1712,12 @@ function paintSidebar(){
|
|
|
1481
1712
|
$('taskRepo').innerHTML = repos.length
|
|
1482
1713
|
? repos.map(r=>'<option value="'+r.id+'">'+esc(r.name)+' · '+esc(r.defaultBranch)+'</option>').join('')
|
|
1483
1714
|
: '<option value="">register a repo — Browse… ↓</option>';
|
|
1484
|
-
|
|
1715
|
+
// 런처 타겟 기본값 = 스코프된 repo(있으면), 아니면 직전 선택 유지
|
|
1716
|
+
if (selectedRepo!=null && repos.some(r=>r.id===selectedRepo)) $('taskRepo').value = String(selectedRepo);
|
|
1717
|
+
else if (prevRepo && repos.some(r=>String(r.id)===prevRepo)) $('taskRepo').value = prevRepo;
|
|
1485
1718
|
$('runFleetBtn').disabled = !repos.length;
|
|
1719
|
+
$('newBtn').disabled = !repos.length && false; // New 는 항상 열림(repo 등록 UI 포함)
|
|
1720
|
+
renderRail();
|
|
1486
1721
|
const capSel = $('taskCapture');
|
|
1487
1722
|
const cur = capSel.value;
|
|
1488
1723
|
capSel.innerHTML = '<option value="">no design capture</option>' + captures.map(c=>
|
|
@@ -1497,34 +1732,104 @@ function paintSidebar(){
|
|
|
1497
1732
|
$('bmk').href = "javascript:(function(){var s=document.createElement('script');s.src='"
|
|
1498
1733
|
+ location.origin + "/design/bookmarklet.js';document.body.appendChild(s)})()";
|
|
1499
1734
|
}
|
|
1735
|
+
/* ── v5.0 navigator rail — machine switcher · repo list(counts+attention+scope) · nav counts ── */
|
|
1736
|
+
const FAILED_STATES = ['failed','error'];
|
|
1737
|
+
function railCounts(){
|
|
1738
|
+
// repoId → { active, attn } — /api/fleet active runs 를 client-side 로 그룹
|
|
1739
|
+
const m = new Map();
|
|
1740
|
+
for (const r of runs.values()){
|
|
1741
|
+
const t = tasks.get(r.taskId); if (!t) continue;
|
|
1742
|
+
const rec = m.get(t.repoId) || { active:0, attn:false };
|
|
1743
|
+
rec.active++;
|
|
1744
|
+
if (FAILED_STATES.includes(r.status)) rec.attn = true;
|
|
1745
|
+
m.set(t.repoId, rec);
|
|
1746
|
+
}
|
|
1747
|
+
return m;
|
|
1748
|
+
}
|
|
1749
|
+
function renderRail(){
|
|
1750
|
+
// machine switcher — 첫 online 머신(없으면 첫 머신) 을 표시, 선택은 #repoMachine 이 보관
|
|
1751
|
+
const cur = $('repoMachine').value || (machines[0] && machines[0].slug) || 'local';
|
|
1752
|
+
const cm = machines.find(x=>x.slug===cur) || machines[0];
|
|
1753
|
+
$('machineLbl').textContent = cm ? cm.slug : 'local';
|
|
1754
|
+
$('machineDot').classList.toggle('on', !!(cm && cm.online));
|
|
1755
|
+
$('machineMenu').innerHTML = machines.map(m=>
|
|
1756
|
+
'<div class="mopt'+(m.slug===cur?' on':'')+'" data-m="'+escA(m.slug)+'">'
|
|
1757
|
+
+ '<span class="mdot2'+(m.online?' on':'')+'"></span>'+esc(m.slug)+'</div>').join('')
|
|
1758
|
+
|| '<div class="mopt">no machines</div>';
|
|
1759
|
+
// repo list
|
|
1760
|
+
const cnt = railCounts();
|
|
1761
|
+
let html = '';
|
|
1762
|
+
if (repos.length){
|
|
1763
|
+
html += '<button type="button" class="repo'+(selectedRepo==null?' on':'')+'" data-repo="all">'
|
|
1764
|
+
+ ic('layers')+'<span class="nm">All repositories</span></button>';
|
|
1765
|
+
for (const r of repos){
|
|
1766
|
+
const c = cnt.get(r.id) || { active:0, attn:false };
|
|
1767
|
+
const on = selectedRepo===r.id;
|
|
1768
|
+
html += '<button type="button" class="repo'+(on?' on':'')+'" data-repo="'+r.id+'" title="'+escA(r.path||r.name)+'">'
|
|
1769
|
+
+ ic('folder')+'<span class="nm">'+esc(r.name)+'</span>'
|
|
1770
|
+
+ (c.attn ? '<span class="attn" title="a run needs a hand"></span>' : '')
|
|
1771
|
+
+ (c.active>0 ? '<span class="cnt">'+c.active+'</span>' : '')
|
|
1772
|
+
+ '<span class="rowmenu">'
|
|
1773
|
+
+ '<button type="button" class="rmbtn" data-rbranch="'+r.id+'" title="base branch">'+ic('branch')+'</button>'
|
|
1774
|
+
+ '<button type="button" class="rmbtn" data-rremove="'+r.id+'" title="remove">'+ic('x')+'</button>'
|
|
1775
|
+
+ '</span></button>';
|
|
1776
|
+
}
|
|
1777
|
+
} else {
|
|
1778
|
+
// empty state — 첫 repo 추가 CTA
|
|
1779
|
+
html += '<div style="padding:10px 6px;font-size:12px;color:var(--faint);line-height:1.6">'
|
|
1780
|
+
+ 'No repositories yet.<br><b style="color:var(--brand)">Add your first repository</b> to start.</div>';
|
|
1781
|
+
}
|
|
1782
|
+
$('repoList').innerHTML = html;
|
|
1783
|
+
paintNavCounts();
|
|
1784
|
+
paintBoardHint();
|
|
1785
|
+
}
|
|
1786
|
+
// 스코프 설정 — 레일 repo 클릭
|
|
1787
|
+
function setScope(repoId){
|
|
1788
|
+
selectedRepo = repoId;
|
|
1789
|
+
try { if (repoId==null) localStorage.removeItem('coxpit.repo'); else localStorage.setItem('coxpit.repo', String(repoId)); } catch {}
|
|
1790
|
+
if (repoId!=null) $('taskRepo').value = String(repoId), syncSelect('taskRepo');
|
|
1791
|
+
renderRail();
|
|
1792
|
+
if (boardView!=='archive') render();
|
|
1793
|
+
else { paintArchRepos(); $('archRepo').value = repoId!=null ? String(repoId) : ''; syncSelect('archRepo'); archFetch(true); }
|
|
1794
|
+
}
|
|
1795
|
+
$('repoList').addEventListener('click', (e)=>{
|
|
1796
|
+
const br = e.target.closest('button[data-rbranch]');
|
|
1797
|
+
if (br){ e.stopPropagation(); openBranchFor(Number(br.dataset.rbranch)); return; }
|
|
1798
|
+
const rm = e.target.closest('button[data-rremove]');
|
|
1799
|
+
if (rm){ e.stopPropagation(); removeRepoById(Number(rm.dataset.rremove)); return; }
|
|
1800
|
+
const row = e.target.closest('.repo[data-repo]'); if(!row) return;
|
|
1801
|
+
setScope(row.dataset.repo==='all' ? null : Number(row.dataset.repo));
|
|
1802
|
+
});
|
|
1500
1803
|
$('captures').addEventListener('click', async (e)=>{
|
|
1501
1804
|
const b = e.target.closest('button[data-delcap]'); if(!b) return;
|
|
1502
1805
|
await fetch('/api/design/'+b.dataset.delcap,{method:'DELETE'});
|
|
1503
1806
|
hydrate();
|
|
1504
1807
|
});
|
|
1505
|
-
|
|
1506
|
-
const rid = $('taskRepo').value;
|
|
1808
|
+
async function removeRepoById(rid){
|
|
1507
1809
|
if (!rid){ toast('no repository selected', 'error'); return; }
|
|
1508
1810
|
const yes = await confirmUI('Remove the selected repository from coxpit?',
|
|
1509
1811
|
{ sub: 'The repo itself is untouched — only the registration is removed. Refused while it has open tasks.', danger: true, okLabel: 'Remove' });
|
|
1510
1812
|
if (!yes) return;
|
|
1511
1813
|
const res = await fetch('/api/repos/'+rid,{method:'DELETE'});
|
|
1512
1814
|
const j = await res.json().catch(()=>({}));
|
|
1513
|
-
if (res.ok){ toast('repo removed', 'ok'); hydrate(); }
|
|
1815
|
+
if (res.ok){ if (selectedRepo===Number(rid)) setScope(null); toast('repo removed', 'ok'); hydrate(); }
|
|
1514
1816
|
else toast('remove: '+(j.detail||res.status), 'error');
|
|
1515
|
-
}
|
|
1817
|
+
}
|
|
1818
|
+
$('repoRemove').addEventListener('click', ()=>removeRepoById($('taskRepo').value));
|
|
1516
1819
|
/* ── 기본 브랜치 변경 ── */
|
|
1517
|
-
|
|
1518
|
-
|
|
1820
|
+
let brRepoId = null;
|
|
1821
|
+
function openBranchFor(rid){
|
|
1519
1822
|
if (!rid){ toast('no repository selected', 'error'); return; }
|
|
1823
|
+
brRepoId = rid;
|
|
1520
1824
|
const repo = repos.find(r=>String(r.id)===String(rid));
|
|
1521
1825
|
$('brInput').value = repo ? repo.defaultBranch : '';
|
|
1522
1826
|
$('brOverlay').classList.add('open'); $('brInput').focus();
|
|
1523
|
-
}
|
|
1827
|
+
}
|
|
1828
|
+
$('repoBranch').addEventListener('click', ()=>openBranchFor($('taskRepo').value));
|
|
1524
1829
|
$('brCancel').addEventListener('click', ()=>$('brOverlay').classList.remove('open'));
|
|
1525
1830
|
$('brOverlay').addEventListener('click',(e)=>{ if(e.target===$('brOverlay')) $('brOverlay').classList.remove('open'); });
|
|
1526
1831
|
async function brSave(){
|
|
1527
|
-
const rid =
|
|
1832
|
+
const rid = brRepoId; const branch = $('brInput').value.trim();
|
|
1528
1833
|
if (!rid || !branch) return;
|
|
1529
1834
|
const res = await fetch('/api/repos/'+rid,{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({defaultBranch:branch})});
|
|
1530
1835
|
const j = await res.json().catch(()=>({}));
|
|
@@ -1641,21 +1946,21 @@ function paintModal(){
|
|
|
1641
1946
|
answer/doc → mdLite · page → sandbox iframe · code → 기존 diff 렌더러 · file → 이미지/다운로드. */
|
|
1642
1947
|
let outCards = []; // 이 run 의 마지막 카드 목록(RunOutputCard[])
|
|
1643
1948
|
const OUT_ICON = { answer:'message', code:'code', doc:'file', page:'image', file:'image' };
|
|
1644
|
-
const OUT_LABEL = { answer:'
|
|
1949
|
+
const OUT_LABEL = { answer:'Answer', code:'Code', doc:'Doc', page:'Page', file:'File' };
|
|
1645
1950
|
function contractHTML(cards){
|
|
1646
1951
|
const declared = cards.filter(c=>c.required);
|
|
1647
1952
|
const aux = cards.filter(c=>!c.required && c.present);
|
|
1648
1953
|
if (!declared.length && !aux.length) return ''; // 계약도 부수도 없으면 스트립 숨김
|
|
1649
|
-
let h = '<span class="clabel"
|
|
1954
|
+
let h = '<span class="clabel">Required outputs (contract):</span>';
|
|
1650
1955
|
if (declared.length){
|
|
1651
1956
|
h += declared.map(c=>{
|
|
1652
1957
|
const ok = c.present;
|
|
1653
1958
|
return '<span class="req '+(ok?'ok':'warn')+'"><span class="rg">'+ic(ok?'check':'alert-triangle')+'</span>'
|
|
1654
1959
|
+ esc(OUT_LABEL[c.type]||c.type)+'</span>';
|
|
1655
1960
|
}).join('');
|
|
1656
|
-
} else h += '<span class="req aux"
|
|
1961
|
+
} else h += '<span class="req aux">none</span>';
|
|
1657
1962
|
if (aux.length){
|
|
1658
|
-
h += '<span class="csep">—</span><span class="clabel"
|
|
1963
|
+
h += '<span class="csep">—</span><span class="clabel">Extra:</span>'
|
|
1659
1964
|
+ aux.map(c=>'<span class="req aux">'+esc(OUT_LABEL[c.type]||c.type)+'</span>').join('');
|
|
1660
1965
|
}
|
|
1661
1966
|
return h;
|
|
@@ -1663,10 +1968,10 @@ function contractHTML(cards){
|
|
|
1663
1968
|
function outCardHTML(c, i){
|
|
1664
1969
|
const miss = !c.present;
|
|
1665
1970
|
const badge = c.required
|
|
1666
|
-
? '<span class="obadge '+(c.present?'req':'warn')+'"
|
|
1667
|
-
: '<span class="obadge"
|
|
1971
|
+
? '<span class="obadge '+(c.present?'req':'warn')+'">Required</span>'
|
|
1972
|
+
: '<span class="obadge">Extra</span>';
|
|
1668
1973
|
const gname = miss ? 'alert-triangle' : (OUT_ICON[c.type] || 'circle');
|
|
1669
|
-
const meta = miss ? '
|
|
1974
|
+
const meta = miss ? 'output not met — '+esc(c.meta||'') : esc(c.meta||'');
|
|
1670
1975
|
return '<div class="ocard'+(miss?' miss':'')+'" data-oi="'+i+'">'
|
|
1671
1976
|
+ '<span class="og">'+ic(gname)+'</span>'
|
|
1672
1977
|
+ '<span class="ob"><span class="ot">'+esc(c.title||c.type)+'</span>'
|
|
@@ -1685,7 +1990,7 @@ function pickDefaultCard(cards){
|
|
|
1685
1990
|
if (!hasCode && docish) return cards.indexOf(docish);
|
|
1686
1991
|
if (hasCode){
|
|
1687
1992
|
const codeCard = present.find(c=>c.type==='code');
|
|
1688
|
-
//
|
|
1993
|
+
// even when code-heavy, a declared doc/answer wins the contract (code is always reachable via its card)
|
|
1689
1994
|
const declaredDoc = present.find(c=>c.required && (c.type==='answer'||c.type==='doc'||c.type==='page'));
|
|
1690
1995
|
if (declaredDoc) return cards.indexOf(declaredDoc);
|
|
1691
1996
|
return cards.indexOf(codeCard);
|
|
@@ -2000,6 +2305,7 @@ async function openCompare(taskId){
|
|
|
2000
2305
|
cmpTaskId = taskId;
|
|
2001
2306
|
cmpDocMode = false; $('cmpDocsTgl').textContent = 'Rendered';
|
|
2002
2307
|
$('cmpReview').hidden = true; $('cmpReview').innerHTML = '';
|
|
2308
|
+
$('cmpBody').hidden = false; $('cmpAI').textContent = 'AI review'; $('cmpAI').classList.remove('on');
|
|
2003
2309
|
$('cmpOverlay').classList.add('open');
|
|
2004
2310
|
$('cmpBody').innerHTML = '<div class="empty" style="flex:1">loading…</div>';
|
|
2005
2311
|
await paintCompare();
|
|
@@ -2090,18 +2396,30 @@ function mdLite(src){
|
|
|
2090
2396
|
}
|
|
2091
2397
|
$('cmpAI').addEventListener('click', async ()=>{
|
|
2092
2398
|
if (cmpTaskId==null) return;
|
|
2093
|
-
const yes = await confirmUI('Run an AI review of these implementations?',
|
|
2094
|
-
{ 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' });
|
|
2095
|
-
if (!yes) return;
|
|
2096
2399
|
const btn = $('cmpAI');
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2400
|
+
// 이미 리뷰가 떠 있으면 → diff 로 토글백
|
|
2401
|
+
if (!$('cmpReview').hidden){
|
|
2402
|
+
$('cmpReview').hidden = true; $('cmpBody').hidden = false;
|
|
2403
|
+
btn.classList.remove('on'); btn.textContent = 'AI review';
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
// 아직 안 불러왔으면 확인 후 리뷰 실행(같은 compare 안에선 1회만)
|
|
2407
|
+
if (!$('cmpReview').innerHTML.trim()){
|
|
2408
|
+
const yes = await confirmUI('Run an AI review of these implementations?',
|
|
2409
|
+
{ 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' });
|
|
2410
|
+
if (!yes) return;
|
|
2411
|
+
btn.disabled = true; btn.textContent = 'Reviewing…';
|
|
2412
|
+
try{
|
|
2413
|
+
const res = await fetch('/api/tasks/'+cmpTaskId+'/review',{method:'POST',
|
|
2414
|
+
headers:{'content-type':'application/json'}, body:JSON.stringify({real:true})});
|
|
2415
|
+
const j = await res.json().catch(()=>({}));
|
|
2416
|
+
if (!res.ok){ toast('review: '+(j.detail||res.status), 'error'); btn.disabled = false; btn.textContent = 'AI review'; return; }
|
|
2417
|
+
$('cmpReview').innerHTML = mdLite(j.review||'');
|
|
2418
|
+
} finally { btn.disabled = false; }
|
|
2419
|
+
}
|
|
2420
|
+
// 리뷰를 풀높이로, diff 컬럼은 숨김
|
|
2421
|
+
$('cmpReview').hidden = false; $('cmpBody').hidden = true;
|
|
2422
|
+
btn.classList.add('on'); btn.textContent = 'Diffs';
|
|
2105
2423
|
});
|
|
2106
2424
|
$('mCompare').addEventListener('click', ()=>{
|
|
2107
2425
|
if (openRunId==null) return;
|
|
@@ -2147,15 +2465,15 @@ function roomRunRowHTML(r){
|
|
|
2147
2465
|
const meta = (r.agent?esc(r.agent)+' · ':'') + (r.filesChanged? '+'+r.filesChanged+'f' : 'no changes');
|
|
2148
2466
|
let acts;
|
|
2149
2467
|
if (merged){
|
|
2150
|
-
acts = '<div class="acts"><button class="b ghost" data-ract="open" data-rrid="'+r.runId+'"
|
|
2468
|
+
acts = '<div class="acts"><button class="b ghost" data-ract="open" data-rrid="'+r.runId+'">Open</button></div>';
|
|
2151
2469
|
} else if (running){
|
|
2152
|
-
acts = '<div class="acts"><button class="b ghost" data-ract="open" data-rrid="'+r.runId+'"
|
|
2470
|
+
acts = '<div class="acts"><button class="b ghost" data-ract="open" data-rrid="'+r.runId+'">Open · decide once settled</button></div>';
|
|
2153
2471
|
} else {
|
|
2154
2472
|
acts = '<div class="acts">'
|
|
2155
|
-
+ '<button class="b ghost" data-ract="review" data-rrid="'+r.runId+'"
|
|
2156
|
-
+ '<button class="b" data-ract="fix" data-rrid="'+r.runId+'"
|
|
2157
|
-
+ '<button class="b merge" data-ract="merge" data-rrid="'+r.runId+'"
|
|
2158
|
-
+ '<button class="b close" data-ract="close" data-rrid="'+r.runId+'"
|
|
2473
|
+
+ '<button class="b ghost" data-ract="review" data-rrid="'+r.runId+'">Review</button>'
|
|
2474
|
+
+ '<button class="b" data-ract="fix" data-rrid="'+r.runId+'">Steer</button>'
|
|
2475
|
+
+ '<button class="b merge" data-ract="merge" data-rrid="'+r.runId+'">Merge</button>'
|
|
2476
|
+
+ '<button class="b close" data-ract="close" data-rrid="'+r.runId+'">Close</button>'
|
|
2159
2477
|
+ '</div>';
|
|
2160
2478
|
}
|
|
2161
2479
|
const badge = merged
|
|
@@ -2178,10 +2496,10 @@ function roomRunRowHTML(r){
|
|
|
2178
2496
|
+ '<div class="run-b" data-rbody="'+r.runId+'">'
|
|
2179
2497
|
+ '<div class="rout" data-rout="'+r.runId+'"><div class="rout-cards"><div class="ocards-empty">…</div></div></div>'
|
|
2180
2498
|
+ (roomDone(s) && r.steerable
|
|
2181
|
-
? '<div class="fix"><input data-rfix="'+r.runId+'" placeholder="
|
|
2182
|
-
+ '<button class="b" data-ract="fixsend" data-rrid="'+r.runId+'"
|
|
2183
|
-
: '<div class="review"><span class="rk">·</span><span class="rt"
|
|
2184
|
-
+ (running?'
|
|
2499
|
+
? '<div class="fix"><input data-rfix="'+r.runId+'" placeholder="Next instruction — same session & worktree…" />'
|
|
2500
|
+
+ '<button class="b" data-ract="fixsend" data-rrid="'+r.runId+'">Steer → continue session</button></div>'
|
|
2501
|
+
: '<div class="review"><span class="rk">·</span><span class="rt">This run can\\'t be steered ('
|
|
2502
|
+
+ (running?'still running':(roomDone(s)?'no session — dry/cleaned up':'status '+esc(s)))+').</span></div>')
|
|
2185
2503
|
+ '</div></div>';
|
|
2186
2504
|
}
|
|
2187
2505
|
function roomRenderRuns(){
|
|
@@ -2198,7 +2516,7 @@ function roomRenderRuns(){
|
|
|
2198
2516
|
function roomUpdateGbar(){
|
|
2199
2517
|
const n = roomSel.size;
|
|
2200
2518
|
$('roomSelC').textContent = '☑ '+n+' selected';
|
|
2201
|
-
$('roomIntegrateSel').textContent = 'Integrate
|
|
2519
|
+
$('roomIntegrateSel').textContent = 'Integrate selected ('+n+')';
|
|
2202
2520
|
$('roomIntegrateSel').disabled = n===0;
|
|
2203
2521
|
}
|
|
2204
2522
|
/* 펼친 행의 출력 카드 로드 — 모달과 같은 /outputs + renderOutCardInto 재사용(중복 없음). */
|
|
@@ -2223,7 +2541,7 @@ function roomOpenCard(rid, i){
|
|
|
2223
2541
|
let det = wrap.querySelector('.rout-detail');
|
|
2224
2542
|
if (!det){
|
|
2225
2543
|
det = document.createElement('div'); det.className='rout-detail';
|
|
2226
|
-
det.innerHTML = '<div class="rout-back">‹
|
|
2544
|
+
det.innerHTML = '<div class="rout-back">‹ Outputs</div><div class="rout-body"></div>';
|
|
2227
2545
|
wrap.appendChild(det);
|
|
2228
2546
|
}
|
|
2229
2547
|
det.style.display='';
|
|
@@ -2369,7 +2687,7 @@ async function roomRunAction(act, rid){
|
|
|
2369
2687
|
if (act==='fix'){ // 펼치고 steer 입력에 포커스
|
|
2370
2688
|
if (!roomOpen.has(rid)){ roomOpen.add(rid); const row=document.querySelector('.run[data-rrun="'+rid+'"]'); if(row){ row.classList.add('open'); roomLoadRunOutputs(rid); } }
|
|
2371
2689
|
const inp = document.querySelector('[data-rfix="'+rid+'"]'); if (inp) inp.focus();
|
|
2372
|
-
else toast('r'+rid+': steer
|
|
2690
|
+
else toast('r'+rid+': can\\'t steer (needs a settled run with a session)', 'error');
|
|
2373
2691
|
return;
|
|
2374
2692
|
}
|
|
2375
2693
|
if (act==='fixsend'){
|
|
@@ -2377,7 +2695,7 @@ async function roomRunAction(act, rid){
|
|
|
2377
2695
|
if (!msg){ if(inp) inp.focus(); return; }
|
|
2378
2696
|
const res = await fetch('/api/runs/'+rid+'/steer',{method:'POST',
|
|
2379
2697
|
headers:{'content-type':'application/json'}, body:JSON.stringify({message:msg, mode:'work'})});
|
|
2380
|
-
if (res.ok){ if(inp) inp.value=''; toast('r'+rid+':
|
|
2698
|
+
if (res.ok){ if(inp) inp.value=''; toast('r'+rid+': steered — continuing the same session', 'ok'); }
|
|
2381
2699
|
else { const j = await res.json().catch(()=>({})); toast('steer: '+(j.detail||res.status), 'error'); }
|
|
2382
2700
|
return;
|
|
2383
2701
|
}
|
|
@@ -2424,21 +2742,21 @@ async function reviewOneRun(r){
|
|
|
2424
2742
|
let rv = body.querySelector('.review.airev');
|
|
2425
2743
|
if (!rv){ rv = document.createElement('div'); rv.className='review airev'; body.insertBefore(rv, body.querySelector('.fix')||null); }
|
|
2426
2744
|
const real = $('taskReal').checked;
|
|
2427
|
-
rv.innerHTML = '<span class="rk">'+ic('message')+'
|
|
2745
|
+
rv.innerHTML = '<span class="rk">'+ic('message')+' Review</span><span class="rt">reviewing… ('+(real?'real':'dry')+')</span>';
|
|
2428
2746
|
try{
|
|
2429
2747
|
const res = await fetch('/api/tasks/'+r.taskId+'/review',{method:'POST',
|
|
2430
2748
|
headers:{'content-type':'application/json'}, body:JSON.stringify({real})});
|
|
2431
2749
|
const j = await res.json().catch(()=>({}));
|
|
2432
|
-
if (res.ok){ rv.innerHTML = '<span class="rk">'+ic('message')+'
|
|
2433
|
-
else { rv.innerHTML = '<span class="rk">'+ic('message')+'
|
|
2434
|
-
}catch{ rv.innerHTML = '<span class="rk">'+ic('message')+'
|
|
2750
|
+
if (res.ok){ rv.innerHTML = '<span class="rk">'+ic('message')+' Review</span><span class="rt">'+mdLite(j.review||'(no summary)')+'</span>'; }
|
|
2751
|
+
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'); }
|
|
2752
|
+
}catch{ rv.innerHTML = '<span class="rk">'+ic('message')+' Review</span><span class="rt">review request failed</span>'; }
|
|
2435
2753
|
}
|
|
2436
2754
|
/* steer 입력 Enter → 전송 */
|
|
2437
2755
|
$('roomRuns').addEventListener('keydown', (e)=>{
|
|
2438
2756
|
const inp = e.target.closest ? e.target.closest('[data-rfix]') : null;
|
|
2439
2757
|
if (inp && e.key==='Enter'){ e.preventDefault(); roomRunAction('fixsend', Number(inp.dataset.rfix)); }
|
|
2440
2758
|
});
|
|
2441
|
-
/* ──
|
|
2759
|
+
/* ── group action bar — Integrate selected · Review all · Close group (all reuse existing flows) ── */
|
|
2442
2760
|
$('roomIntegrateSel').addEventListener('click', async ()=>{
|
|
2443
2761
|
if (!roomSel.size){ toast('select settled runs with changes first', 'error'); return; }
|
|
2444
2762
|
// 기존 select mode + selbar + /api/integrate 재사용 — 방을 닫고 선택을 밴드 select 로 옮긴다.
|
|
@@ -2467,7 +2785,7 @@ $('roomReviewAll').addEventListener('click', async ()=>{
|
|
|
2467
2785
|
$('roomGroupClose').addEventListener('click', async ()=>{
|
|
2468
2786
|
if (roomGroupId==null) return;
|
|
2469
2787
|
const g = roomGroupId;
|
|
2470
|
-
await closeGroup(g); //
|
|
2788
|
+
await closeGroup(g); // reuse existing group close (guard · atRisk tally)
|
|
2471
2789
|
if (roomGroupId===g) await roomLoad();
|
|
2472
2790
|
});
|
|
2473
2791
|
/* + New attempt — 같은 그룹에 새 시도 발사 (현재 dry/real 토글 반영) */
|
|
@@ -2699,24 +3017,38 @@ $('mTerm').addEventListener('click', ()=>{
|
|
|
2699
3017
|
});
|
|
2700
3018
|
|
|
2701
3019
|
/* ── forms ── */
|
|
2702
|
-
/* ──
|
|
3020
|
+
/* ── New sheet type seg — Task | Goal | Workbench (body + footer adapt, mock's setV) ── */
|
|
2703
3021
|
let lTab = 'task';
|
|
2704
|
-
const L_LABEL = { task: 'Run fleet', goal: 'Plan &
|
|
3022
|
+
const L_LABEL = { task: 'Run fleet', goal: 'Plan & run', bench: 'Open workbench' };
|
|
3023
|
+
const L_ICON = { task: 'play', goal: 'target', bench: 'terminal' };
|
|
3024
|
+
function setV(tab){
|
|
3025
|
+
lTab = tab;
|
|
3026
|
+
document.querySelectorAll('#launchTabs .seg-opt').forEach(x=>x.classList.toggle('on', x.dataset.tab===tab));
|
|
3027
|
+
$('panelTask').style.display = tab==='task' ? 'flex' : 'none';
|
|
3028
|
+
$('panelGoal').hidden = tab!=='goal';
|
|
3029
|
+
$('panelGoal').style.display = tab==='goal' ? 'flex' : 'none';
|
|
3030
|
+
$('panelBench').hidden = tab!=='bench';
|
|
3031
|
+
$('panelBench').style.display = tab==='bench' ? 'flex' : 'none';
|
|
3032
|
+
// footer adapts: Task=Dry/Real·count·Run fleet · Goal=Dry/Real·Plan & run · Workbench=Open workbench only
|
|
3033
|
+
$('modeSeg').style.display = tab==='bench' ? 'none' : 'flex'; // workbench 는 에이전트 없음
|
|
3034
|
+
$('taskCountStep').style.display = tab==='task' ? 'flex' : 'none'; // count 는 Task 만(플래너·워크벤치는 1)
|
|
3035
|
+
$('runFleetBtn').innerHTML = ic(L_ICON[tab]) + ' ' + L_LABEL[tab];
|
|
3036
|
+
}
|
|
2705
3037
|
document.querySelectorAll('#launchTabs .seg-opt').forEach(b=>{
|
|
2706
|
-
b.addEventListener('click', ()=>
|
|
2707
|
-
lTab = b.dataset.tab;
|
|
2708
|
-
document.querySelectorAll('#launchTabs .seg-opt').forEach(x=>x.classList.toggle('on', x===b));
|
|
2709
|
-
$('panelTask').style.display = lTab==='task' ? 'flex' : 'none';
|
|
2710
|
-
$('panelGoal').hidden = lTab!=='goal';
|
|
2711
|
-
$('panelGoal').style.display = lTab==='goal' ? 'flex' : 'none';
|
|
2712
|
-
$('panelBench').hidden = lTab!=='bench';
|
|
2713
|
-
$('panelBench').style.display = lTab==='bench' ? 'flex' : 'none';
|
|
2714
|
-
$('modeSeg').style.display = lTab==='bench' ? 'none' : 'flex'; // workbench 는 에이전트 없음
|
|
2715
|
-
$('taskCount').style.display = lTab==='task' ? '' : 'none';
|
|
2716
|
-
$('runFleetBtn').textContent = L_LABEL[lTab];
|
|
2717
|
-
});
|
|
3038
|
+
b.addEventListener('click', ()=>setV(b.dataset.tab));
|
|
2718
3039
|
});
|
|
2719
3040
|
|
|
3041
|
+
/* ── progressive Options reveal (rarely-used Task fields; expanded state remembered per session) ── */
|
|
3042
|
+
function setTaskOpt(open){
|
|
3043
|
+
$('taskOptToggle').setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
3044
|
+
$('taskOptBody').hidden = !open;
|
|
3045
|
+
try{ sessionStorage.setItem('coxpit.taskOpt', open ? '1' : '0'); }catch{}
|
|
3046
|
+
}
|
|
3047
|
+
$('taskOptToggle').addEventListener('click', ()=>{
|
|
3048
|
+
setTaskOpt($('taskOptToggle').getAttribute('aria-expanded') !== 'true');
|
|
3049
|
+
});
|
|
3050
|
+
setTaskOpt((()=>{ try{ return sessionStorage.getItem('coxpit.taskOpt')==='1'; }catch{ return false; } })());
|
|
3051
|
+
|
|
2720
3052
|
async function submitGoal(){
|
|
2721
3053
|
const repoId = Number($('taskRepo').value);
|
|
2722
3054
|
const goal = $('planGoal').value.trim();
|
|
@@ -2729,7 +3061,7 @@ async function submitGoal(){
|
|
|
2729
3061
|
const res = await fetch('/api/plan',{method:'POST',headers:{'content-type':'application/json'},
|
|
2730
3062
|
body:JSON.stringify({repoId, goal, real})});
|
|
2731
3063
|
const j = await res.json().catch(()=>({}));
|
|
2732
|
-
if (res.ok){ toast(j.tasks.length+' task(s) planned & launched', 'ok'); $('planGoal').value=''; hydrate(); }
|
|
3064
|
+
if (res.ok){ toast(j.tasks.length+' task(s) planned & launched', 'ok'); $('planGoal').value=''; closeLaunch(); hydrate(); }
|
|
2733
3065
|
else toast('plan: '+(j.detail||j.error||res.status), 'error');
|
|
2734
3066
|
} finally { btn.disabled = false; btn.textContent = L_LABEL[lTab]; }
|
|
2735
3067
|
}
|
|
@@ -2743,6 +3075,7 @@ async function submitBench(){
|
|
|
2743
3075
|
if (!res.ok){ toast('workbench: '+(j.detail||j.error||res.status), 'error'); return; }
|
|
2744
3076
|
toast('workbench open — terminal attached', 'ok');
|
|
2745
3077
|
$('benchTitle').value='';
|
|
3078
|
+
closeLaunch();
|
|
2746
3079
|
await hydrate();
|
|
2747
3080
|
openTerm(j.runId); // 바로 터미널로
|
|
2748
3081
|
}
|
|
@@ -2782,6 +3115,7 @@ $('taskForm').addEventListener('submit', async (e)=>{
|
|
|
2782
3115
|
body:JSON.stringify({count:Number($('taskCount').value)||1, real: $('taskReal').checked, agent: selAgent, model})});
|
|
2783
3116
|
if (model) rememberModel(model);
|
|
2784
3117
|
$('taskTitle').value=''; $('taskPrompt').value=''; clearOutputs();
|
|
3118
|
+
closeLaunch();
|
|
2785
3119
|
});
|
|
2786
3120
|
/* ── deliverable 계약 칩 — 선택된 타입 배열을 tasks.outputs 로 보낸다(기본=빈=오늘의 동작) ── */
|
|
2787
3121
|
const OUTPUT_ORDER = ['answer','code','doc','page','file'];
|
|
@@ -2805,12 +3139,40 @@ function rememberModel(m){
|
|
|
2805
3139
|
}catch{}
|
|
2806
3140
|
paintModelHist();
|
|
2807
3141
|
}
|
|
3142
|
+
function modelHistList(){
|
|
3143
|
+
try{ return JSON.parse(localStorage.getItem('coxpit.models')||'[]'); }catch{ return []; }
|
|
3144
|
+
}
|
|
2808
3145
|
function paintModelHist(){
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
$('
|
|
3146
|
+
const h = modelHistList();
|
|
3147
|
+
const cur = $('taskModel').value.trim();
|
|
3148
|
+
$('modelMenu').innerHTML = h.length
|
|
3149
|
+
? h.map(m=>'<div class="dd-opt'+(m===cur?' on':'')+'" data-m="'+escA(m)+'">'+esc(m)+'</div>').join('')
|
|
3150
|
+
: '<div class="mc-empty">no recent models</div>';
|
|
2812
3151
|
}
|
|
2813
3152
|
paintModelHist();
|
|
3153
|
+
// model combo — free text stays; the caret opens a .dd-style recent-models menu (no native datalist)
|
|
3154
|
+
function closeModelMenu(){ $('modelCombo').classList.remove('open'); }
|
|
3155
|
+
$('modelTog').addEventListener('click', (e)=>{
|
|
3156
|
+
e.stopPropagation();
|
|
3157
|
+
const open = $('modelCombo').classList.contains('open');
|
|
3158
|
+
closeDropdowns(); closeModelMenu();
|
|
3159
|
+
if (!open){ paintModelHist(); $('modelCombo').classList.add('open'); }
|
|
3160
|
+
});
|
|
3161
|
+
$('modelMenu').addEventListener('click', (e)=>{
|
|
3162
|
+
const o = e.target.closest('.dd-opt'); if(!o) return;
|
|
3163
|
+
$('taskModel').value = o.dataset.m; closeModelMenu(); $('taskModel').focus();
|
|
3164
|
+
});
|
|
3165
|
+
document.addEventListener('click', closeModelMenu);
|
|
3166
|
+
|
|
3167
|
+
/* ── count stepper — token −/+ mirror the number input, clamped 1..8 ── */
|
|
3168
|
+
function stepCount(delta){
|
|
3169
|
+
const el = $('taskCount');
|
|
3170
|
+
const n = Math.max(1, Math.min(8, (Number(el.value)||1) + delta));
|
|
3171
|
+
el.value = String(n);
|
|
3172
|
+
}
|
|
3173
|
+
$('cntDown').addEventListener('click', ()=>stepCount(-1));
|
|
3174
|
+
$('cntUp').addEventListener('click', ()=>stepCount(1));
|
|
3175
|
+
$('taskCount').addEventListener('change', ()=>stepCount(0)); // clamp manual typing
|
|
2814
3176
|
|
|
2815
3177
|
/* ── agent mode segmented control (mirrors hidden #taskReal) ── */
|
|
2816
3178
|
const segOpts = Array.from(document.querySelectorAll('#modeSeg .seg-opt'));
|
|
@@ -2830,7 +3192,7 @@ setMode(savedMode === '1', false);
|
|
|
2830
3192
|
|
|
2831
3193
|
/* ── provider segmented control — Task 탭 전용(Goal 플래너·Workbench 는 무관) ── */
|
|
2832
3194
|
let selAgent = 'claude-code';
|
|
2833
|
-
const provOpts = Array.from(document.querySelectorAll('#provSeg .seg-opt'));
|
|
3195
|
+
const provOpts = Array.from(document.querySelectorAll('#provSeg .seg-opt, #provSegGoal .seg-opt'));
|
|
2834
3196
|
function setProvider(id, persist){
|
|
2835
3197
|
selAgent = id;
|
|
2836
3198
|
for (const b of provOpts){
|
|
@@ -3069,6 +3431,43 @@ function setDrawer(on){ asideEl.classList.toggle('open', on); $('scrim').classLi
|
|
|
3069
3431
|
$('menuBtn').addEventListener('click', ()=>setDrawer(!asideEl.classList.contains('open')));
|
|
3070
3432
|
$('scrim').addEventListener('click', ()=>setDrawer(false));
|
|
3071
3433
|
|
|
3434
|
+
/* ── v5.0 rail interactions — machine switcher · New sheet · Add repository ── */
|
|
3435
|
+
function openLaunch(tab){
|
|
3436
|
+
const r = repos.find(x=>x.id===selectedRepo);
|
|
3437
|
+
$('sheetRepoLbl').textContent = r ? ('· '+r.name) : (repos.length ? '· all repositories' : '· register a repo first');
|
|
3438
|
+
setV(tab || 'task'); // + New 는 Task 로 열림; 타입 seg 가 안에서 전환
|
|
3439
|
+
$('newSheet').classList.add('open');
|
|
3440
|
+
setDrawer(false);
|
|
3441
|
+
}
|
|
3442
|
+
function closeLaunch(){ $('newSheet').classList.remove('open'); $('repoActions').hidden = true; $('repoForm').hidden = true; }
|
|
3443
|
+
$('newBtn').addEventListener('click', ()=>openLaunch('task'));
|
|
3444
|
+
$('fab').addEventListener('click', ()=>openLaunch('task')); // mobile pocket-board FAB → same sheet
|
|
3445
|
+
$('sheetClose').addEventListener('click', closeLaunch);
|
|
3446
|
+
$('newSheet').addEventListener('click', (e)=>{ if(e.target===$('newSheet')) closeLaunch(); });
|
|
3447
|
+
// Add repository — 시트를 열고 repo 피커(Browse/New-folder/Path 내장)를 노출
|
|
3448
|
+
$('repoAdd').addEventListener('click', ()=>{ openLaunch('task'); $('repoActions').hidden = false; $('repoBrowse').click(); });
|
|
3449
|
+
// machine switcher — 컴팩트 버튼 + 메뉴(기존 #repoMachine 이 선택 상태 보관)
|
|
3450
|
+
function positionMachineMenu(){
|
|
3451
|
+
const b = $('machineSwitch').getBoundingClientRect();
|
|
3452
|
+
const mm = $('machineMenu');
|
|
3453
|
+
mm.style.left = b.left + 'px';
|
|
3454
|
+
mm.style.top = (b.bottom + 4) + 'px';
|
|
3455
|
+
}
|
|
3456
|
+
$('machineSwitch').addEventListener('click', (e)=>{
|
|
3457
|
+
e.stopPropagation();
|
|
3458
|
+
const mm = $('machineMenu');
|
|
3459
|
+
const was = mm.classList.contains('open');
|
|
3460
|
+
closeDropdowns(); mm.classList.remove('open');
|
|
3461
|
+
if (!was){ positionMachineMenu(); mm.classList.add('open'); }
|
|
3462
|
+
});
|
|
3463
|
+
$('machineMenu').addEventListener('click', (e)=>{
|
|
3464
|
+
const o = e.target.closest('.mopt[data-m]'); if(!o) return;
|
|
3465
|
+
$('repoMachine').value = o.dataset.m; syncSelect('repoMachine');
|
|
3466
|
+
$('machineMenu').classList.remove('open');
|
|
3467
|
+
renderRail();
|
|
3468
|
+
});
|
|
3469
|
+
document.addEventListener('click', ()=>$('machineMenu').classList.remove('open'));
|
|
3470
|
+
|
|
3072
3471
|
/* 딥링크 — /?run=N 이면 하이드레이션 후 그 run 모달을 연다 (웹훅 링크·알림용) */
|
|
3073
3472
|
function openFromURL(){
|
|
3074
3473
|
const q = new URLSearchParams(location.search).get('run');
|