coxpit 6.3.5 → 6.3.8

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/hud.ts +141 -74
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coxpit",
3
- "version": "6.3.5",
3
+ "version": "6.3.8",
4
4
  "description": "Self-hosted cockpit for running a fleet of AI coding agents across your own machines — parallel worktree runs, live board, compare & merge, web terminal, design capture.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/src/hud.ts CHANGED
@@ -8,8 +8,13 @@
8
8
  // [!] 이 페이지는 **브라우저에서 단독으로 돈다**. 창(프레임리스·항상 위)·전역 단축키·크기 변경은
9
9
  // 데스크톱 쪽 일(K1/K2, desktop/main.cjs)이고, 여기는 그쪽을 한 줄도 모른다 —
10
10
  // 배치 상태를 <html data-hud="pill|list|detail"> 에 적고, 창이 붙어 있을 때만
11
- // window.coxpitHud.size({state,w,h}) 로 "이만큼 필요하다"고 말할 뿐이다(없으면 아무 일도 없다).
11
+ // window.coxpitHud.size({state,w,h,fit}) 로 "이만큼 필요하다"고 말할 뿐이다(없으면 아무 일도 없다).
12
12
  // 창을 실제로 줄이고 늘리는 것도, 화면 밖으로 안 나가게 물리는 것도 전부 메인 프로세스다.
13
+ //
14
+ // 크기의 규칙 하나: **카드가 창을 채우고, 창은 내용을 따른다.** 예전에는 창이 고정 높이(380·440)라
15
+ // 내용보다 훨씬 컸고, 남는 자리는 투명해서 보이지도 않으면서 그 아래 것의 클릭을 먹었다.
16
+ // 이제 목록·상세는 자기 내용 높이를 재서 보내고(fit), 그 위에서 **사람이 끈 크기가 언제나 이긴다**.
17
+ // 그리고 어떤 줄도 두 줄로 접히지 않는다 — 접히는 것은 에이전트가 한 말(.d-q pre · .d-tail)뿐이다.
13
18
  // 보이는데 열리지 않는 알약은 없느니만 못하다 — 눌러서 펼쳐져야 하고 그러면서도 끌어 옮길 수 있어야 하니,
14
19
  // drag 영역과 클릭 대상은 같은 요소일 수 없다: 알약은 눌리고, 창은 알약 안의 홈(.pgrip)으로 옮긴다.
15
20
  //
@@ -40,8 +45,10 @@ export const HUD_HTML = /* html */ `<!doctype html>
40
45
  html,body{height:100%;overscroll-behavior:none}
41
46
  /* 지면은 보드·코크핏과 같은 --bg 다. (데스크톱 창이 프레임리스·투명으로 이걸 띄울 때는
42
47
  그 실행분에서 투명 배경을 덧입힌다 — 지금은 브라우저에서 단독으로 열려도 어두워야 한다.) */
48
+ /* 지면은 곧 창이다 — 카드가 뷰포트를 꽉 채우고, 여백은 둥근 모서리와 테두리가 살 만큼(5px)만 남긴다.
49
+ 남는 자리를 두면 그만큼이 **보이지 않는 채로 클릭만 먹는** 죽은 영역이 된다(늘 위에 뜨는 창이라 더 나쁘다). */
43
50
  body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:13px;line-height:1.45;
44
- -webkit-font-smoothing:antialiased;overflow:hidden;display:flex;align-items:flex-start;justify-content:center;padding:6px}
51
+ -webkit-font-smoothing:antialiased;overflow:hidden;display:flex;align-items:stretch;justify-content:center;padding:5px}
45
52
  button{font-family:var(--sans);cursor:pointer}
46
53
  :focus-visible{outline:2px solid rgba(78,201,176,.5);outline-offset:1px;border-radius:4px}
47
54
  *{scrollbar-width:thin;scrollbar-color:var(--line-hi) transparent}
@@ -52,24 +59,25 @@ export const HUD_HTML = /* html */ `<!doctype html>
52
59
  /* 세 가지 배치 = 한 장의 세 상태. data-hud 는 데스크톱이 창 크기를 맞출 때 읽어 갈 자리이기도 하다. */
53
60
  html[data-hud="pill"] #panel{display:none}
54
61
  html[data-hud="list"] #pill,html[data-hud="detail"] #pill{display:none}
55
- html[data-hud="pill"] body{align-items:flex-start}
62
+ html[data-hud="pill"] body{align-items:flex-start;padding:6px}
56
63
 
57
64
  /* ── K3. 접힌 알약 — 이것이 평상시의 전부다 ───────────────────────────── */
58
- .pill{display:inline-flex;align-items:center;gap:8px;height:26px;padding:0 11px 0 7px;border-radius:999px;
65
+ /* 높이 30px = 손가락·커서가 놓치지 않는 최소치. 알약은 누르라고 있는 것이니 작아서는 안 된다. */
66
+ .pill{display:inline-flex;align-items:center;gap:8px;height:30px;padding:0 13px 0 8px;border-radius:999px;
59
67
  background:var(--surface);border:1px solid var(--line);box-shadow:0 8px 28px rgba(0,0,0,.35);
60
- font-family:var(--mono);font-size:11px;color:var(--muted)}
68
+ font-family:var(--mono);font-size:12px;color:var(--muted)}
61
69
  .pill:hover{border-color:var(--line-hi);color:var(--ink)}
62
70
  /* app-region: 프레임 없는 창(K1)에서 drag 영역은 클릭을 **삼킨다** — 창이 대신 움직인다.
63
71
  그래서 알약 자체는 no-drag(눌러서 펼치는 것이 알약의 일이고, 옮기는 일보다 훨씬 잦다)이고,
64
72
  창을 옮기는 손잡이는 알약 안의 작은 홈 하나뿐이다. 브라우저에서는 둘 다 아무 뜻도 없다. */
65
73
  .pill,.pill>*{-webkit-app-region:no-drag}
66
74
  .pill>.pgrip{-webkit-app-region:drag}
67
- .pgrip{flex:none;color:var(--faint);font-family:var(--mono);font-size:10px;line-height:1;
68
- letter-spacing:-.06em;padding:0 2px;opacity:.65;cursor:grab}
75
+ .pgrip{flex:none;color:var(--faint);font-family:var(--mono);font-size:10.5px;line-height:1;
76
+ letter-spacing:-.06em;padding:0 3px;opacity:.65;cursor:grab}
69
77
  .pill:hover .pgrip{opacity:1}
70
78
  .pdots{display:inline-flex;align-items:center;gap:4px}
71
- .pwait{color:var(--blocked);font-size:11px;letter-spacing:.02em}
72
- .pquiet{color:var(--faint);font-size:10.5px}
79
+ .pwait{color:var(--blocked);font-size:12px;letter-spacing:.02em;white-space:nowrap}
80
+ .pquiet{color:var(--faint);font-size:11.5px;white-space:nowrap}
73
81
 
74
82
  /* 상태 점 — Part A 매핑 그대로(working=--running · waiting=--blocked 맥박 · idle=--faint · exited=--done) */
75
83
  .st{width:7px;height:7px;border-radius:50%;background:var(--faint);display:inline-block;flex:none}
@@ -81,88 +89,101 @@ export const HUD_HTML = /* html */ `<!doctype html>
81
89
  @media (prefers-reduced-motion:reduce){ .st.as-waiting{animation:none} }
82
90
 
83
91
  /* ── K4. 펼친 분류 목록 ─────────────────────────────────────────────── */
84
- .panel{width:100%;max-width:250px;display:flex;flex-direction:column;max-height:calc(100vh - 12px);
92
+ /* 카드는 창을 **채운다**(max-widthmax-height 없다). 창이 얼마만 해야 하는지는
93
+ 아래 fitHeight() 가 재서 창에 말해 주고, 그다음부터는 사람이 끈 크기가 이긴다.
94
+ 넘치면 카드 밖이 아니라 목록 안에서 스크롤한다 — 머리와 발은 늘 붙어 있어야 한다. */
95
+ .panel{width:100%;height:100%;min-height:0;display:flex;flex-direction:column;
85
96
  background:var(--surface);border:1px solid var(--line);border-radius:10px;
86
97
  box-shadow:0 8px 28px rgba(0,0,0,.35);overflow:hidden}
87
- html[data-hud="detail"] .panel{max-width:570px}
88
- .hh{display:flex;align-items:center;gap:7px;padding:7px 9px;border-bottom:1px solid var(--line);
89
- font-family:var(--mono);font-size:11px;-webkit-app-region:drag}
98
+ /* 머리는 **언제나 한 줄**이다. 좁아지면 꼬리부터 잘리고, 꼬리에 오는 것이 가장 덜 중요한 것
99
+ (대기 → 도는 것 → 노는 것)이라 idle 수가 맨 먼저 사라진다. 줄임표는 .subs 맡는다. */
100
+ .hh{display:flex;flex-wrap:nowrap;align-items:center;gap:7px;padding:7px 11px;border-bottom:1px solid var(--line);
101
+ font-family:var(--mono);font-size:12.5px;white-space:nowrap;-webkit-app-region:drag}
90
102
  .hh>button{-webkit-app-region:no-drag}
91
- .lead{color:var(--blocked);white-space:nowrap}
103
+ .lead{flex:none;color:var(--blocked);white-space:nowrap}
92
104
  .lead.clear{color:var(--done)}
93
- .subs{color:var(--faint);font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
94
- .hx{margin-left:auto;background:none;border:none;color:var(--faint);font-family:var(--mono);font-size:13px;
95
- line-height:1;padding:2px 5px;border-radius:5px}
105
+ .subs{flex:1 1 auto;min-width:0;color:var(--faint);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
106
+ .hx{flex:none;background:none;border:none;color:var(--faint);font-family:var(--mono);font-size:14px;
107
+ line-height:1;padding:3px 6px;border-radius:5px}
96
108
  .hx:hover{color:var(--ink);background:var(--surface2)}
97
109
  .hbody{display:flex;min-height:0;flex:1}
98
- .hlist{width:250px;flex:none;overflow-y:auto;padding:5px 0}
99
- html[data-hud="list"] .hlist{width:100%}
100
- .hdetail{flex:1;min-width:0;border-left:1px solid var(--line);overflow-y:auto;padding:9px 11px;background:var(--panel)}
110
+ /* 목록은 남는 높이를 전부 먹고 **자기 안에서** 스크롤한다(flex:1 + min-height:0).
111
+ 상세일 때만 왼쪽 기둥으로 고정폭이 되고, 오른쪽이 나머지를 가진다. */
112
+ .hlist{width:100%;flex:1 1 auto;min-width:0;min-height:0;overflow-y:auto;padding:4px 0}
113
+ html[data-hud="detail"] .hlist{width:240px;flex:none}
114
+ .hdetail{flex:1;min-width:0;min-height:0;border-left:1px solid var(--line);overflow-y:auto;padding:10px 12px;background:var(--panel)}
101
115
  html[data-hud="list"] .hdetail{display:none}
102
- .lbl{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--faint);
103
- padding:6px 11px 3px}
104
- .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;background:none;border:none;
105
- padding:5px 11px;font-family:var(--mono);font-size:11.5px;color:var(--muted)}
116
+ .lbl{font-family:var(--mono);font-size:10.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--faint);
117
+ padding:8px 12px 3px}
118
+ /* 행도 한 줄이다 — 이름은 제 몫만 쥐고(flex:0 1 auto), 길은 남는 폭을 먹다가 줄임표로 끝내고,
119
+ 경과 시간은 절대 밀려나지 않는다(flex:none). 폭이 좁아져도 두 줄이 되는 일은 없다. */
120
+ .row{display:flex;align-items:center;gap:8px;width:100%;min-height:30px;text-align:left;background:none;border:none;
121
+ padding:7px 12px;font-family:var(--mono);font-size:13px;color:var(--muted);white-space:nowrap}
106
122
  .row:hover{background:var(--surface2);color:var(--ink)}
107
123
  .row.on{background:var(--brand-dim);color:var(--ink);box-shadow:inset 2px 0 0 var(--brand)}
108
- .row .rn{color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:46%}
124
+ .row .rn{flex:0 1 auto;min-width:0;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
109
125
  .row.wait .rn{color:var(--blocked)}
110
- .row .rp{color:var(--faint);font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
111
- .row .rel{margin-left:auto;color:var(--faint);font-size:10px;flex:none}
112
- .dryc{flex:none;font-family:var(--mono);font-size:9px;color:var(--faint);border:1px solid var(--line-hi);
113
- border-radius:4px;padding:0 3px;letter-spacing:.04em}
114
- .cnt{display:block;width:100%;text-align:left;background:none;border:none;padding:5px 11px;
115
- font-family:var(--mono);font-size:10.5px;color:var(--faint)}
126
+ .row .rp{flex:1 1 0;min-width:0;color:var(--faint);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
127
+ .row .rel{flex:none;color:var(--faint);font-size:11px;white-space:nowrap}
128
+ .dryc{flex:none;font-family:var(--mono);font-size:10.5px;color:var(--faint);border:1px solid var(--line-hi);
129
+ border-radius:4px;padding:0 4px;letter-spacing:.04em;white-space:nowrap}
130
+ .cnt{display:block;width:100%;min-height:30px;text-align:left;background:none;border:none;padding:7px 12px;
131
+ font-family:var(--mono);font-size:12px;color:var(--faint);white-space:nowrap}
116
132
  .cnt:hover{color:var(--ink)}
117
- .clearbox{padding:11px;font-family:var(--mono);font-size:11px;color:var(--muted);line-height:1.6}
133
+ .clearbox{padding:12px;font-family:var(--mono);font-size:12.5px;color:var(--muted);line-height:1.6}
118
134
  .clearbox .big{color:var(--done);display:block;margin-bottom:3px}
119
- .hfoot{display:flex;align-items:center;gap:8px;padding:5px 9px;border-top:1px solid var(--line);
120
- font-family:var(--mono);font-size:9.5px;color:var(--faint)}
135
+ .hfoot{display:flex;align-items:center;gap:8px;padding:6px 11px;border-top:1px solid var(--line);
136
+ font-family:var(--mono);font-size:11px;color:var(--faint);white-space:nowrap;overflow:hidden}
121
137
  .hver{margin-left:auto;opacity:.8}
122
138
 
123
139
  /* ── K5. 상세(목록 오른쪽) ────────────────────────────────────────── */
124
- .d-h{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;margin-bottom:6px}
125
- .d-h .dnm{color:var(--ink)}
126
- .d-chip{font-size:9.5px;color:var(--faint)}
140
+ .d-h{display:flex;flex-wrap:nowrap;align-items:center;gap:7px;font-family:var(--mono);font-size:14px;
141
+ margin-bottom:6px;white-space:nowrap}
142
+ .d-h .dnm{flex:0 1 auto;min-width:0;color:var(--ink);overflow:hidden;text-overflow:ellipsis}
143
+ .d-chip{flex:none;font-size:11px;color:var(--faint)}
127
144
  .d-chip.as-working{color:var(--running)} .d-chip.as-waiting{color:var(--blocked)}
128
145
  .d-chip.as-idle{color:var(--faint)} .d-chip.as-exited{color:var(--done)}
129
- .d-meta{font-family:var(--mono);font-size:10px;color:var(--faint);margin-bottom:8px;
146
+ .d-meta{font-family:var(--mono);font-size:11.5px;color:var(--faint);margin-bottom:9px;
147
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
148
+ .d-q{border:1px solid var(--line-hi);border-radius:8px;background:var(--surface);padding:9px 10px;margin-bottom:9px}
149
+ .d-q .qk{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;
150
+ color:var(--faint);margin-bottom:5px}
151
+ /* 이 페이지에서 **줄이 접히는 곳은 여기뿐**이다 — 에이전트가 한 말은 줄여 쓸 수 없으니 접고,
152
+ 대신 높이를 물려서(판의 40% 남짓) 카드 하나가 창을 다 먹지 않게 한다. */
153
+ .d-q pre{margin:0;font-family:var(--mono);font-size:12.5px;color:var(--ink);white-space:pre-wrap;word-break:break-word;
154
+ max-height:min(40vh,260px);overflow:auto}
155
+ .d-q .unk{font-family:var(--mono);font-size:12.5px;color:var(--muted)}
156
+ .d-sec{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);
157
+ margin:10px 0 4px}
158
+ .d-now{font-family:var(--mono);font-size:12.5px;color:var(--ink);margin-bottom:4px;
130
159
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
131
- .d-q{border:1px solid var(--line-hi);border-radius:8px;background:var(--surface);padding:8px 9px;margin-bottom:8px}
132
- .d-q .qk{display:block;font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;
133
- color:var(--faint);margin-bottom:4px}
134
- .d-q pre{margin:0;font-family:var(--mono);font-size:11px;color:var(--ink);white-space:pre-wrap;word-break:break-word;
135
- max-height:150px;overflow:auto}
136
- .d-q .unk{font-family:var(--mono);font-size:11px;color:var(--muted)}
137
- .d-sec{font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);
138
- margin:9px 0 4px}
139
- .d-now{font-family:var(--mono);font-size:11.5px;color:var(--ink);margin-bottom:4px}
140
- .d-tl{border-left:1px solid var(--line);padding-left:8px;max-height:130px;overflow:auto}
141
- .d-l{display:flex;gap:6px;font-family:var(--mono);font-size:10.5px;line-height:1.5}
142
- .d-l .ak{flex:none;color:var(--faint);min-width:42px}
143
- .d-l .at{color:var(--muted);white-space:pre-wrap;word-break:break-word}
144
- .d-tail{margin:0;font-family:var(--mono);font-size:10.5px;color:var(--muted);background:var(--panel);
145
- border:1px solid var(--line);border-radius:7px;padding:7px 8px;max-height:210px;overflow:auto;
160
+ .d-tl{border-left:1px solid var(--line);padding-left:9px;max-height:min(30vh,170px);overflow:auto}
161
+ .d-l{display:flex;gap:7px;font-family:var(--mono);font-size:11.5px;line-height:1.55}
162
+ .d-l .ak{flex:none;color:var(--faint);min-width:46px}
163
+ .d-l .at{flex:1 1 0;min-width:0;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
164
+ /* 터미널 꼬리는 화면 그대로라 접는 것이 맞다(.d-q pre 와 같은 부류 — 사람이 쓴 것이 아니라 나온 것). */
165
+ .d-tail{margin:0;font-family:var(--mono);font-size:12px;color:var(--muted);background:var(--panel);
166
+ border:1px solid var(--line);border-radius:7px;padding:8px 9px;max-height:min(45vh,240px);overflow:auto;
146
167
  white-space:pre-wrap;word-break:break-word}
147
- .d-diff{margin:6px 0 0;font-family:var(--mono);font-size:10px;color:var(--muted);max-height:160px;overflow:auto;
148
- white-space:pre;background:var(--panel);border:1px solid var(--line);border-radius:7px;padding:7px 8px}
149
- .d-bar{display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin-top:9px;padding-top:8px;border-top:1px solid var(--line)}
150
- .dbtn{font-family:var(--mono);font-size:10px;color:var(--muted);background:none;border:1px solid var(--line);
151
- border-radius:6px;padding:3px 8px}
168
+ .d-diff{margin:7px 0 0;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:min(35vh,200px);overflow:auto;
169
+ white-space:pre;background:var(--panel);border:1px solid var(--line);border-radius:7px;padding:8px 9px}
170
+ .d-bar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:10px;padding-top:9px;border-top:1px solid var(--line)}
171
+ .dbtn{font-family:var(--mono);font-size:12.5px;color:var(--muted);background:none;border:1px solid var(--line);
172
+ border-radius:6px;padding:5px 10px;white-space:nowrap}
152
173
  .dbtn:hover,.dbtn:focus-visible{color:var(--ink);border-color:var(--line-hi);background:var(--surface2)}
153
174
  .dbtn.prim{color:var(--brand);border-color:rgba(78,201,176,.4)}
154
175
  .dbtn.danger:hover{color:var(--failed);border-color:var(--failed)}
155
176
  /* 빠른 답은 대기의 색(--blocked)을 빌린다 — 코크핏 .qbtn 과 같은 주의색, 새 색 없음 */
156
- .qbtn{font-family:var(--mono);font-size:10.5px;color:var(--blocked);background:none;border:1px solid var(--line-hi);
157
- border-radius:999px;padding:3px 10px}
177
+ .qbtn{font-family:var(--mono);font-size:12.5px;color:var(--blocked);background:none;border:1px solid var(--line-hi);
178
+ border-radius:999px;padding:5px 12px;white-space:nowrap}
158
179
  .qbtn:hover,.qbtn:focus-visible{color:var(--ink);border-color:var(--blocked)}
159
- .d-steer{display:flex;gap:5px;margin-top:8px}
160
- .d-steer input{flex:1;min-width:0;font-family:var(--mono);font-size:11px;color:var(--ink);background:var(--panel);
161
- border:1px solid var(--line-hi);border-radius:6px;padding:5px 8px;outline:none}
180
+ .d-steer{display:flex;gap:6px;margin-top:9px}
181
+ .d-steer input{flex:1;min-width:0;font-family:var(--mono);font-size:12.5px;color:var(--ink);background:var(--panel);
182
+ border:1px solid var(--line-hi);border-radius:6px;padding:6px 9px;outline:none}
162
183
  .d-steer input:focus{border-color:var(--brand)}
163
- .d-empty{font-family:var(--mono);font-size:11px;color:var(--faint);padding:14px 4px}
184
+ .d-empty{font-family:var(--mono);font-size:12.5px;color:var(--faint);padding:16px 4px}
164
185
  .toast{position:fixed;left:50%;bottom:9px;transform:translateX(-50%);max-width:92%;
165
- font-family:var(--mono);font-size:10.5px;color:var(--ink);background:var(--surface2);
186
+ font-family:var(--mono);font-size:11.5px;color:var(--ink);background:var(--surface2);
166
187
  border:1px solid var(--line-hi);border-radius:7px;padding:5px 10px;opacity:0;pointer-events:none;
167
188
  transition:opacity .16s ease;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
168
189
  .toast.on{opacity:1}
@@ -209,20 +230,57 @@ export const HUD_HTML = /* html */ `<!doctype html>
209
230
  // 창을 줄이고 늘리는 것은 **여기가 아니다**(K1, 데스크톱 창). 이 페이지는 원하는 크기를
210
231
  // <html data-hud> 에 적어 두고, 자기 안에서는 그 값으로 배치만 바꾼다.
211
232
  // 데스크톱 창에 실려 있으면 그 크기를 한 번 더 **말해 준다** — 창을 만지는 쪽은 언제나 저쪽이다.
212
- // 알약 120x26 · 목록 250 · 상세 570 (+ 지면 여백 6px 양쪽). 창은 이 값을 화면 크기로 한 번 더 물린다.
213
- var HUD_SIZE={ pill:{w:132,h:38}, list:{w:262,h:380}, detail:{w:582,h:440} };
233
+ // 폭은 표의 기본값(알약은 값), 높이는 목록·상세 모두 **잰 값**이다(fit:true).
234
+ // 창은 값을 화면(workArea)으로 물리고, 사람이 손으로 끈 크기가 있으면 그쪽을 택한다.
235
+ var GUT=5; // 패널 상태의 지면 여백 — CSS body{padding:5px} 과 같은 값
236
+ var HUD_SIZE={ pill:{w:140,h:42}, list:{w:320,h:260}, detail:{w:680,h:400} };
237
+ // 스크롤 영역이 **실제로 담고 있는** 높이. scrollHeight 를 쓰면 안 된다 —
238
+ // 그 값은 clientHeight 아래로 내려가지 않아서, 창이 한 번 커지고 나면 내용이 줄어도
239
+ // 계속 "지금만큼 필요하다"고 답한다(창은 자랄 줄만 알고 줄 줄은 모르게 된다).
240
+ // 그래서 자식들이 차지한 자리를 직접 잰다. 스크롤된 상태에서도 맞게 scrollTop 을 더한다.
241
+ function contentH(el){
242
+ if(!el) return 0;
243
+ var kids=el.children; if(!kids.length) return 0;
244
+ var r=el.getBoundingClientRect(), bottom=null;
245
+ for(var i=0;i<kids.length;i++){
246
+ var k=kids[i].getBoundingClientRect();
247
+ if(!k.height) continue; // 숨겨진 것은 자리를 차지하지 않는다
248
+ var b=(k.top-r.top)+el.scrollTop+k.height;
249
+ if(bottom===null||b>bottom) bottom=b;
250
+ }
251
+ if(bottom===null) return 0;
252
+ var pad=0;
253
+ try{ pad=parseFloat(getComputedStyle(el).paddingBottom)||0; }catch(e){}
254
+ return Math.ceil(bottom+pad); // 위쪽 padding 은 첫 자식의 자리에 이미 들어 있다
255
+ }
256
+ // 카드가 **원하는 높이** = 머리 + 본문(물리지 않은 내용) + 발 + 테두리.
257
+ function fitHeight(){
258
+ var p=$('panel'); if(!p) return 0;
259
+ var hh=p.querySelector('.hh'), ft=p.querySelector('.hfoot');
260
+ var body=contentH($('hudList'));
261
+ if(layout==='detail') body=Math.max(body, contentH($('hudDetail'))); // 두 기둥 중 **긴 쪽**이 카드의 키다
262
+ if(!body) return 0; // 아직 아무것도 안 그렸다 — 기본값이 낫다
263
+ return Math.ceil((hh?hh.offsetHeight:0)+(ft?ft.offsetHeight:0)+body+2);
264
+ }
214
265
  function reportSize(next){
215
266
  var api=window.coxpitHud;
216
267
  if(!api||typeof api.size!=='function') return; // 브라우저에서 단독으로 열린 경우 — 아무 일도 없다
217
268
  var s=HUD_SIZE[next]||HUD_SIZE.pill;
218
- var w=s.w, h=s.h;
269
+ var w=s.w, h=s.h, fit=false;
219
270
  // 알약만은 **재서** 말한다 — 폭이 점 수에 따라 달라져서 고정값이면 잘리거나 빈자리가 남는다.
220
271
  if(next==='pill'){
221
272
  var el=$('pill');
222
273
  if(el&&el.offsetWidth){ w=Math.ceil(el.offsetWidth)+12; h=Math.ceil(el.offsetHeight)+12; }
274
+ } else {
275
+ var m=fitHeight();
276
+ if(m>0) h=m+GUT*2;
277
+ fit=true; // 목록·상세의 높이는 **내용이 정한다**(창이 그만큼만 커진다)
223
278
  }
224
- try{ api.size({state:next, w:w, h:h}); }catch(e){}
279
+ try{ api.size({state:next, w:w, h:h, fit:fit}); }catch(e){}
225
280
  }
281
+ // 내용은 자라고 줄어든다(행이 늘고, 늦게 온 터미널 꼬리가 채워지고, 접힌 것이 펴진다).
282
+ // 그때마다 한 번씩 다시 말해 준다 — 창이 내용을 따라가는 것은 한 번이 아니라 계속이다.
283
+ function reflow(){ if(layout!=='pill') reportSize(layout); }
226
284
  var layout='pill';
227
285
  function setLayout(next){
228
286
  layout=next;
@@ -349,6 +407,7 @@ ${ACTIVITY_JS}
349
407
  cur=(mk!=null)?ids.indexOf(mk):-1;
350
408
  if(cur<0) curId=null;
351
409
  paintPill();
410
+ reflow(); // 행이 늘거나 줄었다 = 창이 원하는 높이도 달라졌다
352
411
  }
353
412
 
354
413
  // ── K3. 알약 — 점 + 대기 수. 대기가 없으면 조용하다(맥박 없음). ──────────
@@ -444,11 +503,12 @@ ${ACTIVITY_JS}
444
503
  var el=$('hudDetail').querySelector('[data-role=dtl]'); if(!el) return;
445
504
  var r=runById[runId];
446
505
  var lines=humanLines((r&&r.events)||[]).slice(-6);
447
- if(!lines.length){ el.innerHTML='<div class="d-l"><span class="at">아직 이벤트가 없습니다</span></div>'; return; }
506
+ if(!lines.length){ el.innerHTML='<div class="d-l"><span class="at">아직 이벤트가 없습니다</span></div>'; reflow(); return; }
448
507
  var h=''; lines.forEach(function(l){
449
508
  h+='<div class="d-l"><span class="ak">'+esc(l.k)+'</span><span class="at">'+esc(String(l.t).slice(0,200))+'</span></div>';
450
509
  });
451
510
  el.innerHTML=h;
511
+ reflow();
452
512
  }
453
513
  // 기다리는 에이전트가 **무엇을 묻는지** — 코크핏을 열지 않고 알아야 하는 한 가지.
454
514
  // 출처는 둘뿐이다: 터미널의 지금 화면(스크롤백 꼬리) 또는 파싱된 스트림의 마지막 말.
@@ -465,14 +525,17 @@ ${ACTIVITY_JS}
465
525
  if(tail){
466
526
  var keep=tail.split('\\n').slice(-14).join('\\n');
467
527
  box.innerHTML='<span class="qk">터미널 마지막 화면</span><pre>'+esc(keep)+'</pre>';
528
+ reflow();
468
529
  return;
469
530
  }
470
- if(lastSaid){ box.innerHTML='<span class="qk">마지막 말</span><pre>'+esc(lastSaid.slice(0,600))+'</pre>'; return; }
531
+ if(lastSaid){ box.innerHTML='<span class="qk">마지막 말</span><pre>'+esc(lastSaid.slice(0,600))+'</pre>'; reflow(); return; }
471
532
  box.innerHTML='<span class="qk">pending</span><div class="unk">무엇을 묻는지 읽지 못했습니다 \\u2014 터미널을 열어 확인하세요</div>';
533
+ reflow();
472
534
  }).catch(function(){
473
535
  if(sel!==runId) return;
474
536
  var box=$('hudDetail').querySelector('[data-role=qbox]'); if(!box) return;
475
537
  box.innerHTML='<span class="qk">pending</span><div class="unk">무엇을 묻는지 읽지 못했습니다 \\u2014 터미널을 열어 확인하세요</div>';
538
+ reflow();
476
539
  });
477
540
  }
478
541
  function loadTail(runId){
@@ -482,19 +545,22 @@ ${ACTIVITY_JS}
482
545
  if(d&&d.ok&&String(d.text||'').trim()){
483
546
  el.textContent=String(d.text).replace(/\\s+$/,'').split('\\n').slice(-30).join('\\n');
484
547
  } else el.textContent=(d&&d.text)||'터미널 화면을 읽지 못했습니다';
548
+ reflow();
485
549
  }).catch(function(){
486
550
  if(sel!==runId) return;
487
551
  var el=$('hudDetail').querySelector('[data-role=tail]'); if(el) el.textContent='터미널 화면을 읽지 못했습니다';
552
+ reflow();
488
553
  });
489
554
  }
490
555
  function loadDiff(runId){
491
556
  var el=$('hudDetail').querySelector('[data-role=ddiff]'); if(!el) return;
492
- el.hidden=false; el.textContent='Loading\\u2026';
557
+ el.hidden=false; el.textContent='Loading\\u2026'; reflow();
493
558
  fetch('/api/runs/'+runId+'/diff').then(function(x){ return x.json(); }).then(function(d){
494
559
  if(sel!==runId) return;
495
560
  if(d&&d.ok) el.textContent=String((d.stat?d.stat+'\\n\\n':'')+(d.diff||'')).split('\\n').slice(0,60).join('\\n')||'변경 없음';
496
561
  else el.textContent=(d&&d.stat)||'worktree 가 없어 diff 를 볼 수 없습니다';
497
- }).catch(function(){ if(sel===runId) el.textContent='diff 를 가져오지 못했습니다'; });
562
+ reflow();
563
+ }).catch(function(){ if(sel===runId){ el.textContent='diff 를 가져오지 못했습니다'; reflow(); } });
498
564
  }
499
565
 
500
566
  // ── 행동 — 전부 이미 있는 창구다(새로 생긴 것은 input 하나뿐) ───────────
@@ -573,7 +639,8 @@ ${ACTIVITY_JS}
573
639
  }
574
640
  btn.dataset.armed='1';
575
641
  btn.textContent='정말 삭제? (폴더는 보존)';
576
- setTimeout(function(){ if(btn&&btn.dataset.armed==='1'){ btn.dataset.armed=''; btn.textContent='삭제'; } }, 4000);
642
+ reflow(); // 버튼이 길어지면 줄이 늘 수 있다 — 창도 같이 따라간다
643
+ setTimeout(function(){ if(btn&&btn.dataset.armed==='1'){ btn.dataset.armed=''; btn.textContent='삭제'; reflow(); } }, 4000);
577
644
  }
578
645
 
579
646
  // ── 클릭 위임 ────────────────────────────────────────────────────────