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/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 🔗 affordance shares the ghost-button look */
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{border-bottom:1px solid var(--line);background:var(--surface2);padding:14px 20px;
480
- max-height:42vh;overflow:auto;font-size:13px;line-height:1.65;color:var(--muted)}
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 산출물 계약(contract strip) — 모달 헤더 아래 요청 산출물 요약 ── */
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">🔕</button>
619
- <button class="btn-ghost sm" id="remoteBtn" title="reach this daemon from elsewhere (Tailscale · recipes)">🔗</button>
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
- <div class="sect">
626
- <p class="sect-label">Context</p>
627
- <p class="flabel">machine</p>
628
- <select id="repoMachine"></select>
629
- <p class="flabel">repository</p>
630
- <select id="taskRepo"></select>
631
- <div class="row">
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=&lt;pass&gt; 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">⎇</button>
636
- <button type="button" class="btn-ghost sm" id="repoRemove" style="flex:0 0 auto" title="remove selected repository from coxpit">×</button>
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 id="panelTask" style="display:flex;flex-direction:column;gap:8px">
655
- <input id="taskTitle" placeholder="Task title" />
656
- <textarea id="taskPrompt" placeholder="Prompt target files, constraints, how to verify"></textarea>
657
- <button type="button" class="btn-ghost sm" id="ghImport">From GitHub issue / PR…</button>
658
- <div class="seg" id="provSeg" role="group" aria-label="agent provider">
659
- <button type="button" class="seg-opt on" data-agent="claude-code">Claude</button>
660
- <button type="button" class="seg-opt" data-agent="codex">Codex</button>
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
- <p class="flabel">model · optional</p>
663
- <input id="taskModel" placeholder="CLI default" list="modelHist" autocomplete="off" />
664
- <datalist id="modelHist"></datalist>
665
- <p class="flabel">design capture · optional</p>
666
- <select id="taskCapture"><option value="">no design capture</option></select>
667
- <p class="flabel">deliverables · optional (계약)</p>
668
- <div class="ochips" id="taskOutputs" role="group" aria-label="declared deliverables">
669
- <button type="button" class="ochip" data-out="answer">답변</button>
670
- <button type="button" class="ochip" data-out="code">코드</button>
671
- <button type="button" class="ochip" data-out="doc">문서</button>
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 id="panelGoal" hidden style="flex-direction:column;gap:8px">
677
- <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>
678
- </div>
679
- <div id="panelBench" hidden style="flex-direction:column;gap:8px">
680
- <input id="benchTitle" placeholder="Workbench name · optional" />
681
- <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>
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=&lt;pass&gt; to the script URL.</span>
702
- </details>
703
- </aside>
849
+ </div>
704
850
  <main>
705
- <div class="toolbar"><button class="btn-ghost sm" id="selToggle">Select runs</button></div>
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)">♻ Reclaim <span id="reclaimHint"></span></button>
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">×</button>
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">‹ 산출물 목록</div>
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 &amp; 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">×</button>
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 선택 (0)</button>
933
+ <button type="button" class="brand" id="roomIntegrateSel">Integrate selected (0)</button>
785
934
  <span class="sp"></span>
786
- <button type="button" id="roomReviewAll">전체 리뷰</button>
787
- <button type="button" class="danger" id="roomGroupClose">그룹 클로즈</button>
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">+ New attempt</button>
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">×</button>
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">×</button>
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">×</button>
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">+ New folder here</button>
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) => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
985
1137
  const escA = (s) => esc(s).replace(/"/g, '&quot;');
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'?'':kind==='ok'?'':'·')+'</span><span>'+esc(msg)+'</span>';
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">▸</span><span class="nm">'+esc(x.name)+'</span>'
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 list = [...runs.values()].sort((a,b)=>b.id-a.id);
1255
- $('empty').style.display = list.length ? 'none' : 'flex';
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('#viewSeg .seg-opt').forEach(b=>{
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('#viewSeg .seg-opt').forEach(b=>b.addEventListener('click', ()=>setView(b.dataset.view)));
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">✓</span>'+chipHTML(r.status)+'</div>'
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 ↗</a>' : '')
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; $('archN').textContent = n ? '· '+n : ''; }
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
- if (prevRepo && repos.some(r=>String(r.id)===prevRepo)) $('taskRepo').value = prevRepo;
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
- $('repoRemove').addEventListener('click', async ()=>{
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
- $('repoBranch').addEventListener('click', ()=>{
1512
- const rid = $('taskRepo').value;
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 = $('taskRepo').value; const branch = $('brInput').value.trim();
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').textContent = notifyOn ? '🔔' : '🔕'; }
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 OUT_GLYPH = { answer:'', code:'‹›', doc:'', page:'', file:'' };
1638
- const OUT_LABEL = { answer:'답변', code:'코드', doc:'문서', page:'페이지', file:'파일' };
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">요청 산출물(계약):</span>';
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?'':'!')+'</span>'
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">없음</span>';
1913
+ } else h += '<span class="req aux">none</span>';
1651
1914
  if (aux.length){
1652
- h += '<span class="csep">—</span><span class="clabel">부수:</span>'
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')+'">요청됨</span>'
1661
- : '<span class="obadge">부수</span>';
1662
- const glyph = OUT_GLYPH[c.type] || '';
1663
- const meta = miss ? '산출물 미충족 — '+esc(c.meta||'') : esc(c.meta||'');
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">'+esc(glyph)+'</span>'
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">›</span>')
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 '+esc(r.prUrl.split('/').slice(-1)[0])+'</a>' : '')
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
- btn.disabled = true; btn.textContent = 'Reviewing…';
2092
- try{
2093
- const res = await fetch('/api/tasks/'+cmpTaskId+'/review',{method:'POST',
2094
- headers:{'content-type':'application/json'}, body:JSON.stringify({real:true})});
2095
- const j = await res.json().catch(()=>({}));
2096
- if (res.ok){ $('cmpReview').innerHTML = mdLite(j.review||''); $('cmpReview').hidden = false; }
2097
- else toast('review: '+(j.detail||res.status), 'error');
2098
- } finally { btn.disabled = false; btn.textContent = 'AI review'; }
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+'">열기</button></div>';
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+'">열기 · 정착 결정</button></div>';
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+'">리뷰</button>'
2150
- + '<button class="b" data-ract="fix" data-rrid="'+r.runId+'">수정</button>'
2151
- + '<button class="b merge" data-ract="merge" data-rrid="'+r.runId+'">머지</button>'
2152
- + '<button class="b close" data-ract="close" data-rrid="'+r.runId+'">클로즈</button>'
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?'':'')+'</span>';
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="수정 지시같은 세션·worktree 이어서…" />'
2176
- + '<button class="b" data-ract="fixsend" data-rrid="'+r.runId+'">수정 지시 세션 이어서</button></div>'
2177
- : '<div class="review"><span class="rk">·</span><span class="rt">이 run steer 불가('
2178
- + (running?'아직 실행 중':(roomDone(s)?'세션 없음 — dry/정리됨':'상태 '+esc(s)))+').</span></div>')
2451
+ ? '<div class="fix"><input data-rfix="'+r.runId+'" placeholder="Next instructionsame session &amp; worktree" />'
2452
+ + '<button class="b" data-ract="fixsend" data-rrid="'+r.runId+'">Steercontinue 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 선택 ('+n+')';
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">‹ 산출물 목록</div><div class="rout-body"></div>';
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.textContent = roomSel.has(id) ? '' : '';
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 불가(정착·세션 필요)', 'error');
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+': 수정 지시 같은 세션 이어서', 'ok'); }
2650
+ if (res.ok){ if(inp) inp.value=''; toast('r'+rid+': steeredcontinuing 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">◆ 리뷰</span><span class="rt">reviewing… ('+(real?'real':'dry')+')</span>';
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">◆ 리뷰</span><span class="rt">'+mdLite(j.review||'(no summary)')+'</span>'; }
2427
- else { rv.innerHTML = '<span class="rk">◆ 리뷰</span><span class="rt">review 실패 — '+esc(j.detail||String(res.status))+'</span>'; toast('review: '+(j.detail||res.status), 'error'); }
2428
- }catch{ rv.innerHTML = '<span class="rk">◆ 리뷰</span><span class="rt">review 요청 실패</span>'; }
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
- /* ── 그룹 액션 — Integrate 선택 · 전체 리뷰 · 그룹 클로즈(전부 기존 흐름 재사용) ── */
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); // 기존 그룹 클로즈(guard·atRisk 집계 재사용)
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
- /* ── 런처 하나의 시작점: Task | Goal | Workbench ── */
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 & fan out', bench: 'Open workbench' };
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');