typebulb 0.17.3 → 0.18.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/README.md +16 -14
- package/dist/agents/claude/client.js +144 -138
- package/dist/agents/pi/client.js +1379 -0
- package/dist/agents/pi/index.html +2 -0
- package/dist/agents/pi/styles.css +1317 -0
- package/dist/index.js +353 -331
- package/dist/lint/index.d.ts +8 -0
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint/index.js +23 -0
- package/dist/lint/index.js.map +1 -1
- package/dist/render.js +52 -46
- package/dist/servers.d.ts +6 -0
- package/dist/servers.js +95 -89
- package/package.json +1 -1
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
/* Light is the default; data-theme="dark" is set by the host (typebulb.com
|
|
3
|
+
or the typebulb CLI, sourced from prefers-color-scheme).
|
|
4
|
+
GRAY RAMP — every gray rides ONE neutral tint (R=G=B), like the main app's
|
|
5
|
+
bg1–bg5 ramp. Lightness may step; the HUE never changes. The bug to avoid is a
|
|
6
|
+
gray in a *different* tint (a cool/blue panel beside a neutral one), not the
|
|
7
|
+
number of steps. App chrome (statusbar, picker, modals) sits on the fixed
|
|
8
|
+
--panel. Document panels — code, callouts, tables, user bubbles, open tools —
|
|
9
|
+
paint --veil instead: a translucent neutral that matches --panel when alone on
|
|
10
|
+
the page but COMPOUNDS when panels nest (a code block in a callout, a panel in a
|
|
11
|
+
user bubble), so every depth separates on its own — no level-counting, no cap. */
|
|
12
|
+
--bg: rgb(255, 255, 255);
|
|
13
|
+
--fg: rgb(28, 28, 30);
|
|
14
|
+
--muted: rgb(96, 96, 100);
|
|
15
|
+
--panel: rgb(242, 242, 242);
|
|
16
|
+
--border: rgb(221, 221, 221);
|
|
17
|
+
--accent: rgb(58, 125, 232);
|
|
18
|
+
--veil: rgba(0, 0, 0, 0.05); /* one shade step; alpha-stacks as panels nest */
|
|
19
|
+
--tool-bg: var(--panel);
|
|
20
|
+
--err: rgb(206, 60, 60);
|
|
21
|
+
--warn: rgb(176, 120, 12); /* amber — the cache-leak (cost-caution) tier, milder than --err */
|
|
22
|
+
--diff-add: rgb(40, 140, 70);
|
|
23
|
+
--fold-marker: rgb(24, 145, 72);
|
|
24
|
+
/* Prose measure for text (bubbles + notes) — kept to a readable line length.
|
|
25
|
+
Diagrams break out wider than this; see .md .mermaid. */
|
|
26
|
+
--content-max: 800px;
|
|
27
|
+
color-scheme: light;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
html[data-theme="dark"] {
|
|
31
|
+
/* Neutral ramp, same tint as the main app: page 18 → surface 32 → border 52. */
|
|
32
|
+
--bg: rgb(18, 19, 20);
|
|
33
|
+
--fg: rgb(190, 190, 190);
|
|
34
|
+
--muted: rgb(150, 150, 150);
|
|
35
|
+
--panel: rgb(32, 32, 32);
|
|
36
|
+
--border: rgb(52, 52, 52);
|
|
37
|
+
--accent: rgb(122, 162, 250);
|
|
38
|
+
--veil: rgba(255, 255, 255, 0.06);
|
|
39
|
+
--tool-bg: var(--panel);
|
|
40
|
+
--err: rgb(230, 60, 60);
|
|
41
|
+
--warn: rgb(232, 184, 80); /* amber — the cache-leak (cost-caution) tier, milder than --err */
|
|
42
|
+
--diff-add: rgb(100, 210, 130);
|
|
43
|
+
--fold-marker: rgb(0, 255, 0);
|
|
44
|
+
color-scheme: dark;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Aliases for beautiful-mermaid, indirecting around a var-name collision with the
|
|
48
|
+
bulb's theme (see the renderMermaidSVG call). They still resolve to the live
|
|
49
|
+
theme at the use site. */
|
|
50
|
+
:root {
|
|
51
|
+
--mm-bg: var(--bg);
|
|
52
|
+
--mm-fg: var(--fg);
|
|
53
|
+
--mm-line: var(--muted);
|
|
54
|
+
--mm-accent: var(--accent);
|
|
55
|
+
--mm-muted: var(--muted);
|
|
56
|
+
--mm-surface: var(--tool-bg);
|
|
57
|
+
--mm-border: var(--border);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
* { box-sizing: border-box; }
|
|
61
|
+
|
|
62
|
+
body {
|
|
63
|
+
margin: 0;
|
|
64
|
+
height: 100vh;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
color: var(--fg);
|
|
67
|
+
background: var(--bg);
|
|
68
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
69
|
+
font-size: 15px;
|
|
70
|
+
line-height: 1.55;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* position:relative anchors the overlaid (absolute) statusbar to the viewport box. */
|
|
74
|
+
.app { position: relative; display: flex; flex-direction: column; height: 100vh; }
|
|
75
|
+
|
|
76
|
+
/* Bottom status strip, overlaid (not in flow) on the chat — which runs
|
|
77
|
+
full-height to the very bottom. The strip itself has NO fill, so the message
|
|
78
|
+
text shows through everywhere, right to the bottom edge. Only the items (the
|
|
79
|
+
pills) are opaque, so they overlay the text with visual precedence rather than
|
|
80
|
+
the strip cutting it off with a band. No border — the typebulb host's rounded
|
|
81
|
+
card is the only frame. */
|
|
82
|
+
.statusbar {
|
|
83
|
+
position: absolute;
|
|
84
|
+
left: 0;
|
|
85
|
+
right: 0;
|
|
86
|
+
bottom: 0;
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: .65rem;
|
|
90
|
+
/* .4rem inset on top/left/bottom; the right is larger to clear the scrollbar
|
|
91
|
+
the statusbar overlays — so the *visible* gap to the content edge matches the
|
|
92
|
+
other sides instead of the pills sitting flush against the scrollbar. */
|
|
93
|
+
padding: .4rem 1.35rem .4rem .4rem;
|
|
94
|
+
font-size: .8rem;
|
|
95
|
+
/* Drop the inherited 1.55 line-height so the bar's own strut doesn't add
|
|
96
|
+
phantom ascender/descender room above the pills. Everything in the bar
|
|
97
|
+
is a fixed-height pill, which looks right at 1. */
|
|
98
|
+
line-height: 1;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
}
|
|
101
|
+
.statusbar-actions {
|
|
102
|
+
margin-left: auto;
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: .4rem;
|
|
105
|
+
align-items: center;
|
|
106
|
+
}
|
|
107
|
+
/* Chip shape shared by the status-bar pills, interactive (session chip) and
|
|
108
|
+
passive (token counter). One rule = one source of vertical alignment, no
|
|
109
|
+
element-type drift between siblings; inline-flex + line-height:1 + fixed
|
|
110
|
+
height centers the glyph run by the box, not by the inherited line-box.
|
|
111
|
+
Opaque so they read cleanly over the message text the transparent strip lets
|
|
112
|
+
through. */
|
|
113
|
+
.pill, .token {
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
line-height: 1;
|
|
118
|
+
height: 1.7rem;
|
|
119
|
+
padding: 0 .7rem;
|
|
120
|
+
border-radius: 7px;
|
|
121
|
+
background: var(--panel);
|
|
122
|
+
border: 1px solid var(--border);
|
|
123
|
+
color: var(--muted);
|
|
124
|
+
font-size: .8rem;
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
user-select: none;
|
|
127
|
+
}
|
|
128
|
+
/* Pills are <button>s: reset UA chrome (font/appearance/margin) and add the
|
|
129
|
+
interactive affordance. Slightly larger text than the passive indicator. */
|
|
130
|
+
.pill {
|
|
131
|
+
font: inherit;
|
|
132
|
+
font-size: .85rem;
|
|
133
|
+
appearance: none;
|
|
134
|
+
margin: 0;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
/* `.on` latches the hover accent — the bar's only persistent state cue (every other
|
|
138
|
+
pill accents on hover alone). */
|
|
139
|
+
.pill:hover, .pill.on { border-color: var(--accent); color: var(--accent); }
|
|
140
|
+
/* Square glyph pill (the prose-mode toggle): width pinned to the pill height rather than
|
|
141
|
+
padding-driven — the bar's first non-word pill. The emoji can't take currentColor, so it
|
|
142
|
+
carries state itself: grayscale at rest, color while hovered or on — filtered on the
|
|
143
|
+
inner span, never the button, or it would wash the accent border too. */
|
|
144
|
+
.pill.glyph { width: 1.7rem; padding: 0; font-size: .9rem; }
|
|
145
|
+
.pill.glyph .glyph-img { filter: grayscale(1); opacity: .75; }
|
|
146
|
+
.pill.glyph:hover .glyph-img, .pill.glyph.on .glyph-img { filter: none; opacity: 1; }
|
|
147
|
+
|
|
148
|
+
/* The two status-bar chips each anchor an upward popover (token breakdown /
|
|
149
|
+
session list). Wrap = positioning context; the popover anchors bottom/right and
|
|
150
|
+
grows up-and-left — the chips sit in the right-aligned cluster, so left:0 would
|
|
151
|
+
run them off the viewport's right edge. */
|
|
152
|
+
.token-wrap, .sid-wrap, .servers-wrap, .model-wrap { position: relative; display: inline-block; }
|
|
153
|
+
/* Each pill renders its wrap unconditionally, emitting an empty div when it has
|
|
154
|
+
nothing to show (no tokens / no session / nothing running). An empty wrap is
|
|
155
|
+
still a flex child, so .statusbar-actions' gap lands on both its sides —
|
|
156
|
+
doubling the visible gap between its neighbours. Collapse the empties so the
|
|
157
|
+
cluster spaces evenly regardless of which pills are present. */
|
|
158
|
+
.token-wrap:empty, .sid-wrap:empty, .servers-wrap:empty, .model-wrap:empty { display: none; }
|
|
159
|
+
.picker, .servers-pop {
|
|
160
|
+
--menu-h: 428px; /* one height for both statusbar menus — they sit side by side, so a
|
|
161
|
+
mismatched cap reads as a glitch; .picker grows to it, .servers-pop
|
|
162
|
+
too (the drilled-in console is the one fixed-height exception, see
|
|
163
|
+
.log-mode). Sized to show exactly 12 rows
|
|
164
|
+
(12 × --menu-row-h + 11 gaps + filter + padding) — recognition fades
|
|
165
|
+
past ~12 and search owns the tail. Plain px: desktop-only surface,
|
|
166
|
+
every desktop window fits it. 428, not 427: at 427 the two menus land
|
|
167
|
+
on different device-pixel roundings, a 1px sibling mismatch — the
|
|
168
|
+
lesser evil is 1px off the ideal height. */
|
|
169
|
+
--menu-row-h: 29px; /* one row height for both menus' lists — rows must match pitch exactly
|
|
170
|
+
or the side-by-side menus read as two designs. Whole px, eyeball-tuned;
|
|
171
|
+
the row paddings are kept below it so this floor is what governs. */
|
|
172
|
+
/* ONE geometry contract for both menus — sizing policy, padding, stacking. Stating it twice is
|
|
173
|
+
how every sibling discrepancy of June '26 happened (row pitch, gaps, fixed-vs-grow). max-height,
|
|
174
|
+
not height: a menu hugs its content and caps at the 12 rows above — a fixed height left a
|
|
175
|
+
1-bulb project with a row floating atop a 400px void. Per-menu rules keep only width. */
|
|
176
|
+
max-height: var(--menu-h);
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
padding: .35rem;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: .15rem;
|
|
182
|
+
position: absolute;
|
|
183
|
+
bottom: calc(100% + .35rem);
|
|
184
|
+
right: 0;
|
|
185
|
+
z-index: 10;
|
|
186
|
+
background: var(--panel);
|
|
187
|
+
border: 1px solid var(--border);
|
|
188
|
+
border-radius: 10px;
|
|
189
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, .45);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ---- Shimmer — the one "working" cue. One gradient, one keyframes, composed via class, never
|
|
193
|
+
re-pasted (paste-drift is how each new shimmer kept landing at its own speed). `.shimmer` sweeps
|
|
194
|
+
the element's background; `.shimmer-text` clips the sweep to the glyphs alone. Knobs, set by the
|
|
195
|
+
using rule: --shimmer-base (the gradient's resting color — transparent for see-through chrome, an
|
|
196
|
+
opaque surface for pills that must cover content) and --shimmer-mix (accent strength). Pick the
|
|
197
|
+
cadence by the band's LINEAR speed, which is what reads consistent: travel is proportional to
|
|
198
|
+
element width (background-size 250%), and ~2rem/s is the calibrated pace — so narrow chrome
|
|
199
|
+
(a 1.7rem play button) takes the 2.2s default and anything text-width or wider takes
|
|
200
|
+
`.shimmer-slow` (7s). A fast sweep reads as urgency, not progress. */
|
|
201
|
+
/* The state longhands are !important: this class composes onto elements whose resting rules set a
|
|
202
|
+
`background:` shorthand at arbitrary specificity (button resets, .overlay-pill, :hover tints) —
|
|
203
|
+
any of which would silently kill the gradient (the bug that ate the play button's shimmer). A
|
|
204
|
+
working cue must win while it's on; resting background-color still shows beneath the sweep. */
|
|
205
|
+
.shimmer, .shimmer-text { background-size: 250% 100% !important; animation: bulb-shimmer var(--shimmer-speed, 2.2s) linear infinite !important; }
|
|
206
|
+
.shimmer {
|
|
207
|
+
background-image: linear-gradient(100deg, var(--shimmer-base, transparent) 30%, color-mix(in srgb, var(--accent) var(--shimmer-mix, 35%), var(--shimmer-base, transparent)) 50%, var(--shimmer-base, transparent) 70%) !important;
|
|
208
|
+
background-color: var(--shimmer-base, transparent);
|
|
209
|
+
}
|
|
210
|
+
.shimmer-text {
|
|
211
|
+
/* Shrink-to-fit, so the sweep paces the text's own length, not the container's — on a full-width
|
|
212
|
+
box the band spends most of its run off-glyph (same move as typebulb.com's shimmer.ts). */
|
|
213
|
+
display: inline-block;
|
|
214
|
+
/* background-clip:text paints the glyphs through this element's own box, so the box must be tall
|
|
215
|
+
enough to cover descenders — inheriting the statusbar's line-height:1 clips the "g" in "probing"/
|
|
216
|
+
"working…" at the baseline. A descender-clearing line-height restores the paint area; the button
|
|
217
|
+
centres this box, so the extra height never unbalances the row. */
|
|
218
|
+
line-height: 1.3;
|
|
219
|
+
background-image: linear-gradient(100deg, currentColor 35%, var(--accent) 50%, currentColor 65%) !important;
|
|
220
|
+
-webkit-background-clip: text;
|
|
221
|
+
background-clip: text;
|
|
222
|
+
-webkit-text-fill-color: transparent;
|
|
223
|
+
}
|
|
224
|
+
.shimmer-slow { --shimmer-speed: 7s; }
|
|
225
|
+
@keyframes bulb-shimmer { from { background-position: 250% 0; } to { background-position: -250% 0; } }
|
|
226
|
+
@media (prefers-reduced-motion: reduce) {
|
|
227
|
+
.shimmer { animation: none !important; background: color-mix(in srgb, var(--accent) 15%, var(--shimmer-base, transparent)) !important; }
|
|
228
|
+
.shimmer-text { animation: none !important; background: none !important; -webkit-text-fill-color: currentColor; }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* CC mid-turn: the token pill doubles as the working indicator — an accent shimmer
|
|
232
|
+
over the chip, the count frozen until the turn flushes. Passive — driven by the
|
|
233
|
+
live-chain leaf, not a live token stream (the JSONL is flushed in batches, so this
|
|
234
|
+
can sit lit through a multi-minute buffering window — hence shimmer-slow). The sweep
|
|
235
|
+
blends with --panel, not transparent like the launch buttons': statusbar pills must
|
|
236
|
+
stay opaque over the message text the strip lets through. */
|
|
237
|
+
.token.busy { border-color: var(--accent); --shimmer-base: var(--panel); --shimmer-mix: 25%; }
|
|
238
|
+
|
|
239
|
+
/* Session list — shares the popover chrome (.picker above); these are
|
|
240
|
+
just its own dimensions. */
|
|
241
|
+
.picker { width: min(440px, calc(100vw - 2rem)); }
|
|
242
|
+
/* The filter (.bulb-filter-control) pins at the bottom — the popover's anchored edge, so it never
|
|
243
|
+
jumps as the filtered list changes height; the row list scrolls inside the capped box, resting at
|
|
244
|
+
its end (newest-at-bottom) — same shape as the launcher's .servers-pop / .bulb-list. */
|
|
245
|
+
.picker-list { display: flex; flex-direction: column; gap: .15rem; flex: 1; min-height: 0; overflow-y: auto; padding-right: 4px; }
|
|
246
|
+
.picker-empty { padding: .8rem; color: var(--muted); font-size: .85rem; text-align: center; }
|
|
247
|
+
.picker-row {
|
|
248
|
+
display: flex;
|
|
249
|
+
flex-direction: column;
|
|
250
|
+
/* flex:none — the explicit min-height below replaces flexbox's implicit min-height:auto floor,
|
|
251
|
+
which would otherwise let an overflowing list squash rows below their content (snippet rows
|
|
252
|
+
rendered as overlapping text). The list scrolls; rows never compress. */
|
|
253
|
+
flex: none;
|
|
254
|
+
gap: .2rem;
|
|
255
|
+
/* min-height, not height: a full-text result row grows for its snippet line. */
|
|
256
|
+
padding: .35rem .55rem;
|
|
257
|
+
min-height: var(--menu-row-h);
|
|
258
|
+
box-sizing: border-box;
|
|
259
|
+
justify-content: center;
|
|
260
|
+
border-radius: 6px;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
font-size: .85rem;
|
|
263
|
+
}
|
|
264
|
+
.picker-row-main { display: flex; gap: .6rem; align-items: baseline; }
|
|
265
|
+
.picker:focus { outline: none; } /* the list owns its own highlight; no focus ring */
|
|
266
|
+
.picker-row:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
267
|
+
/* Keyboard cursor (arrow keys / hover) — same borderless tint as the launcher's .server-row.active. */
|
|
268
|
+
.picker-row.active { background: color-mix(in srgb, var(--accent) 16%, transparent); }
|
|
269
|
+
/* The attached session — a leading accent dot in its own gutter slot, reserved (transparent) on
|
|
270
|
+
every row so titles align whether or not it's shown, and filled only on the attached row so it
|
|
271
|
+
reads as "you are here" even when the cursor has moved off. */
|
|
272
|
+
.picker-dot {
|
|
273
|
+
flex: none;
|
|
274
|
+
align-self: center;
|
|
275
|
+
width: 7px;
|
|
276
|
+
height: 7px;
|
|
277
|
+
border-radius: 50%;
|
|
278
|
+
background: transparent;
|
|
279
|
+
}
|
|
280
|
+
.picker-row.current .picker-dot { background: var(--accent); }
|
|
281
|
+
.picker-preview {
|
|
282
|
+
flex: 1; min-width: 0;
|
|
283
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
284
|
+
color: var(--fg);
|
|
285
|
+
}
|
|
286
|
+
.picker-time {
|
|
287
|
+
color: var(--muted);
|
|
288
|
+
font-size: .72rem;
|
|
289
|
+
min-width: 2.6rem; text-align: right;
|
|
290
|
+
}
|
|
291
|
+
.picker-hits { color: var(--muted); font-size: .72rem; white-space: nowrap; }
|
|
292
|
+
/* Full-text result context: first matching turn, indented past the dot gutter into the title column. */
|
|
293
|
+
.picker-snippet {
|
|
294
|
+
color: var(--muted);
|
|
295
|
+
font-size: .78rem;
|
|
296
|
+
line-height: 1.4;
|
|
297
|
+
margin-left: calc(7px + .6rem);
|
|
298
|
+
overflow-wrap: anywhere;
|
|
299
|
+
}
|
|
300
|
+
.picker-mark {
|
|
301
|
+
background: color-mix(in srgb, var(--accent) 30%, transparent);
|
|
302
|
+
border-radius: 2px;
|
|
303
|
+
color: var(--fg);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* Agent switcher (TB-Agent-Switcher.md) — reuses the .picker popover + rows; just its own width and
|
|
307
|
+
the no-key entry form. The active model is marked with .picker-row.current (the accent dot), like
|
|
308
|
+
the attached session; the "active" tag rides .picker-time's muted right-aligned slot. */
|
|
309
|
+
.model-picker { width: min(380px, calc(100vw - 2rem)); }
|
|
310
|
+
.model-err { color: var(--danger, #f87171); font-size: .76rem; padding: .3rem .55rem; }
|
|
311
|
+
/* Key-entry form: the popover flips to this in place of the list when no key is set yet. */
|
|
312
|
+
.key-entry { gap: .5rem; padding: .7rem; }
|
|
313
|
+
.key-entry-head { font-size: .85rem; color: var(--fg); }
|
|
314
|
+
.key-entry .bulb-filter { width: 100%; }
|
|
315
|
+
.key-entry-acts { display: flex; gap: .4rem; justify-content: flex-end; }
|
|
316
|
+
.key-entry-note { font-size: .72rem; color: var(--muted); line-height: 1.4; }
|
|
317
|
+
|
|
318
|
+
/* Agent switcher watchdog (TB-Agent-Switcher.md). TWO tiers, both surfaced live while the menu is
|
|
319
|
+
closed: RED when an Anthropic model is riding the OpenRouter route (the $-per-turn Opus footgun), and
|
|
320
|
+
AMBER when the route served a turn uncached (the cache flip — a cost leak, pennies-scale, so quieter
|
|
321
|
+
than red so the red stays meaningful). The latch outranks the .on accent (equal specificity → source
|
|
322
|
+
order wins): coloured border + text, the bar's loudest states. */
|
|
323
|
+
.pill.danger, .pill.danger:hover { border-color: var(--err); color: var(--err); }
|
|
324
|
+
.pill.warn, .pill.warn:hover { border-color: var(--warn); color: var(--warn); }
|
|
325
|
+
/* The watchdog banner: rendered by Root just above the statusbar, anchored to the .app box. Centered,
|
|
326
|
+
capped width, opaque so it reads over the message text the strip lets through. Only present when a
|
|
327
|
+
tier is live, so it never costs layout in the common case. .danger is red + a glow ring (loud);
|
|
328
|
+
.warn is amber, no ring (a quieter cost cue). */
|
|
329
|
+
.model-banner {
|
|
330
|
+
position: absolute;
|
|
331
|
+
left: 0; right: 0; bottom: 2.6rem;
|
|
332
|
+
margin: 0 auto;
|
|
333
|
+
max-width: min(640px, calc(100vw - 2rem));
|
|
334
|
+
display: flex;
|
|
335
|
+
align-items: flex-start;
|
|
336
|
+
gap: .5rem;
|
|
337
|
+
padding: .55rem .75rem;
|
|
338
|
+
border-radius: 8px;
|
|
339
|
+
background: var(--bg);
|
|
340
|
+
color: var(--fg);
|
|
341
|
+
font-size: .8rem;
|
|
342
|
+
line-height: 1.4;
|
|
343
|
+
}
|
|
344
|
+
.model-banner.danger { border: 1px solid var(--err); box-shadow: 0 0 0 2px color-mix(in srgb, var(--err) 22%, transparent), 0 4px 14px rgba(0,0,0,.25); }
|
|
345
|
+
.model-banner.warn { border: 1px solid var(--warn); box-shadow: 0 4px 14px rgba(0,0,0,.22); }
|
|
346
|
+
.model-banner-icon { flex: none; }
|
|
347
|
+
.model-banner-text { color: var(--fg); }
|
|
348
|
+
/* L2 predictor: a status line at the top of the model popover saying what the next send will bill.
|
|
349
|
+
Muted by default; red when the live model is the Anthropic-via-OpenRouter footgun. */
|
|
350
|
+
/* The model menu's top section: ONE row — the route headline (ellipsizing) + the reasoning dial pinned
|
|
351
|
+
right. Height is one model-row minus its own border, so the row + border + the popover gap to the list
|
|
352
|
+
equal one list-row pitch — the section subtracts a whole row, never a fraction, so no model is clipped
|
|
353
|
+
under the scroll. (Eyeball-tune the height; exact pixel math isn't worth it.) The headline carries only
|
|
354
|
+
what the user cares about; reroute/uncached mechanics live in its title, not on the line. */
|
|
355
|
+
.model-head {
|
|
356
|
+
display: flex; align-items: center; justify-content: space-between; gap: .5rem; flex: none;
|
|
357
|
+
height: calc(var(--menu-row-h) - 1px); box-sizing: border-box;
|
|
358
|
+
/* left padding lines "More info" up with the model titles (row padding .55rem + dot 7px + gap .6rem);
|
|
359
|
+
right padding lines the dial up with the per-row "probe" column (row padding .55rem + list
|
|
360
|
+
padding-right 4px + scrollbar ~15px — the scrollbar is effectively always present, so it's accounted
|
|
361
|
+
for unconditionally; tweak the 15px by eye). */
|
|
362
|
+
padding: 0 calc(.55rem + 4px + 15px) 0 calc(.55rem + 7px + .6rem);
|
|
363
|
+
border-bottom: 1px solid var(--border);
|
|
364
|
+
font-size: .74rem; line-height: 1.35; color: var(--muted); /* line-height clears "g"/"y" descenders */
|
|
365
|
+
}
|
|
366
|
+
/* The external-override warning is the one case that still puts words on this row. */
|
|
367
|
+
.model-route { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
368
|
+
.model-route.warn { color: var(--warn); }
|
|
369
|
+
/* "More info" — a quiet text link (not a pill); drills into the routing/reasoning explainer (diag chrome). */
|
|
370
|
+
.model-info-toggle {
|
|
371
|
+
appearance: none; background: none; border: 0; margin: 0; padding: 0;
|
|
372
|
+
font: inherit; font-size: .74rem; color: var(--accent); cursor: pointer; flex: none;
|
|
373
|
+
}
|
|
374
|
+
.model-info-toggle:hover { text-decoration: underline; }
|
|
375
|
+
/* The reasoning label sits a clear gap to the left of the dial; the pair hugs the right edge. */
|
|
376
|
+
.reasoning-ctl { display: flex; align-items: center; gap: .5rem; flex: none; }
|
|
377
|
+
.reasoning-label { flex: none; }
|
|
378
|
+
|
|
379
|
+
/* The reasoning dial — a Min/Low/Med/High native select styled as an accent pill (a full combobox is
|
|
380
|
+
over-powered for four fixed levels), pinned to the right of the headline so the dropdown is the row's
|
|
381
|
+
last element. */
|
|
382
|
+
.reasoning-select {
|
|
383
|
+
appearance: none; margin: 0; font: inherit; font-size: .72rem;
|
|
384
|
+
width: 4rem; /* match the probe button so the dial shares its column */
|
|
385
|
+
/* centred text (symmetric padding) so the label sits like the probe button's centred caption; the
|
|
386
|
+
chevron still rides the right padding zone. */
|
|
387
|
+
height: 1.5rem; padding: 0 1.1rem; text-align: center; text-align-last: center; border-radius: 999px;
|
|
388
|
+
border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
|
|
389
|
+
/* OPAQUE (mixed over --panel, not transparent) so the OPEN native option list matches the CLOSED
|
|
390
|
+
pill — a transparent fill left the open list on the browser's default bg, a jarring mismatch.
|
|
391
|
+
The option rows reuse the same --rs-bg so the two states are identical. */
|
|
392
|
+
--rs-bg: color-mix(in srgb, var(--accent) 14%, var(--panel));
|
|
393
|
+
background-color: var(--rs-bg);
|
|
394
|
+
color: var(--accent); cursor: pointer;
|
|
395
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='none' stroke='%237aa2fa' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M1 1l4 4 4-4'/%3E%3C/svg%3E");
|
|
396
|
+
background-repeat: no-repeat; background-position: right .5rem center;
|
|
397
|
+
}
|
|
398
|
+
.reasoning-select option { background-color: var(--rs-bg); color: var(--fg); }
|
|
399
|
+
.reasoning-select:hover { border-color: var(--accent); }
|
|
400
|
+
.reasoning-select:focus { outline: none; border-color: var(--accent); }
|
|
401
|
+
|
|
402
|
+
/* The Anthropic brand glyph stands in for the word on the default pill (same square footprint as the
|
|
403
|
+
prose toggle). currentColor tracks the pill's muted→accent state; grayscale at rest like the monkey. */
|
|
404
|
+
.anthropic-glyph { display: block; }
|
|
405
|
+
.pill.glyph .anthropic-glyph { filter: grayscale(1); opacity: .8; }
|
|
406
|
+
.pill.glyph:hover .anthropic-glyph { filter: none; opacity: 1; }
|
|
407
|
+
|
|
408
|
+
.model-row .picker-row-main { align-items: center; }
|
|
409
|
+
/* Hold the shared --menu-row-h pitch (TB-Agent-Mirror.md: "rows must match pitch exactly or the
|
|
410
|
+
side-by-side menus read as two designs"). The trailing control is the tallest per-row element; like
|
|
411
|
+
the launcher's .server-row, trim the inherited .picker-row vertical padding so it stays under the
|
|
412
|
+
--menu-row-h floor and that floor — not the padded content — governs the row height. The highlight
|
|
413
|
+
still fills the full 29px (it paints the row box, not the padding). */
|
|
414
|
+
.model-row { padding-top: .05rem; padding-bottom: .05rem; }
|
|
415
|
+
/* The Anthropic (default) row is its own group below the OpenRouter models — it's the distinct "off the
|
|
416
|
+
proxy, back to your subscription" choice, not just another model. Marked with a subtle background wash
|
|
417
|
+
rather than a divider line: a line fought the rows' rounded corners + inset highlights (poking corners,
|
|
418
|
+
uneven gaps), whereas the wash inherits the row's own 6px radius, so it reads as a contained band that
|
|
419
|
+
sits naturally. Zero layout impact; lower specificity than :hover/.active, so the row still gives its
|
|
420
|
+
normal accent feedback on top. */
|
|
421
|
+
.model-default-row { background: color-mix(in srgb, var(--fg) 6%, transparent); }
|
|
422
|
+
/* Right-aligned trailing cluster: the green `active` marker (only on the current row) then the
|
|
423
|
+
fixed-width status control. The status control is rightmost + fixed width, so its column never moves
|
|
424
|
+
as rows probe — least musical chairs. */
|
|
425
|
+
.model-trailing { flex: none; display: flex; gap: .35rem; justify-content: flex-end; align-items: center; }
|
|
426
|
+
/* ONE button look AND width shared by every trailing pill — probe, probing, the measured status, and
|
|
427
|
+
the active marker — so they all line up and a transition only ever changes colour/text, never the
|
|
428
|
+
shape. The width is snug to the widest short label ("uncached"); the labels are kept terse precisely
|
|
429
|
+
so this stays tight rather than gutter-heavy. */
|
|
430
|
+
.model-btn {
|
|
431
|
+
appearance: none; margin: 0; font: inherit; font-size: .72rem;
|
|
432
|
+
width: 4rem; height: 1.5rem; padding: 0 .25rem; border-radius: 999px;
|
|
433
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
434
|
+
border: 1px solid var(--border); background: transparent; color: var(--muted);
|
|
435
|
+
white-space: nowrap; cursor: pointer;
|
|
436
|
+
}
|
|
437
|
+
.model-status.probe:hover { color: var(--accent); border-color: var(--accent); }
|
|
438
|
+
/* Measured states. cached = the blue accent (the positive "good route" cue); uncached/unavailable =
|
|
439
|
+
red (the states that bite on cost); cache-unconfirmed = neutral. All reuse existing theme colours. */
|
|
440
|
+
.model-status.ok { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
|
|
441
|
+
.model-status.bad { color: var(--err); border-color: color-mix(in srgb, var(--err) 50%, transparent); background: color-mix(in srgb, var(--err) 12%, transparent); }
|
|
442
|
+
.model-status.warn { color: var(--muted); border-color: var(--border); }
|
|
443
|
+
.model-status.ok:hover, .model-status.bad:hover, .model-status.warn:hover { filter: brightness(1.12); text-decoration: underline; }
|
|
444
|
+
/* Same pill as every other state — only the text/colour change across probe → probing → result, never
|
|
445
|
+
the box. Non-interactive (cursor default); the accent-tinted border holds against the row's accent
|
|
446
|
+
hover tint, where the faint default --border washed out. */
|
|
447
|
+
.model-status.probing { color: var(--muted); border-color: color-mix(in srgb, var(--accent) 35%, transparent); cursor: default; }
|
|
448
|
+
/* The active marker — the lime/green live-axis colour (--fold-marker), matching the launcher's running
|
|
449
|
+
:port. A cue, not a control: no hover, default cursor. */
|
|
450
|
+
.model-active { color: var(--fold-marker); border-color: color-mix(in srgb, var(--fold-marker) 50%, transparent); background: color-mix(in srgb, var(--fold-marker) 12%, transparent); cursor: default; }
|
|
451
|
+
/* Don't double-mark the current row: the green `active` pill is the marker now, so drop the accent dot
|
|
452
|
+
fill (keep the gutter transparent for name alignment). */
|
|
453
|
+
.model-picker .picker-row.current .picker-dot { background: transparent; }
|
|
454
|
+
|
|
455
|
+
/* Drill-in surface — the probe diagnostics and the "More info" routing/reasoning explainer (the
|
|
456
|
+
launcher's logs analogue). Flips the whole popover; reuses the console head/back chrome. Held at the
|
|
457
|
+
FULL menu height (not content-sized) so drilling in from the list never resizes the popover — the body
|
|
458
|
+
fills and scrolls if the content is long, leaving quiet space if it's short. */
|
|
459
|
+
.model-picker.diag-mode { padding: 0; gap: 0; height: var(--menu-h); }
|
|
460
|
+
.model-picker.diag-mode .diag-body { flex: 1; min-height: 0; overflow-y: auto; }
|
|
461
|
+
.model-picker.diag-mode:focus { outline: none; }
|
|
462
|
+
.diag-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .35rem .5rem; background: var(--panel); border-bottom: 1px solid var(--border); }
|
|
463
|
+
.diag-head-acts { display: flex; gap: .4rem; align-items: center; flex: none; }
|
|
464
|
+
.diag-back:disabled { opacity: .5; cursor: default; }
|
|
465
|
+
/* line-height clears descenders: the popover inherits the statusbar's line-height:1, and overflow:hidden
|
|
466
|
+
would otherwise crop the tails of "g"/"y" (same root cause as .shimmer-text's fix). */
|
|
467
|
+
.diag-name { min-width: 0; color: var(--fg); font-size: .8rem; line-height: 1.35; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
468
|
+
.diag-back { font: inherit; font-size: .76rem; appearance: none; margin: 0; padding: .15rem .5rem; border-radius: 5px; background: transparent; border: 1px solid var(--border); color: var(--muted); cursor: pointer; white-space: nowrap; }
|
|
469
|
+
.diag-back:hover { color: var(--accent); border-color: var(--accent); }
|
|
470
|
+
.diag-body { display: flex; flex-direction: column; gap: .4rem; padding: .6rem; background: var(--bg); }
|
|
471
|
+
.diag-line { font-size: .8rem; line-height: 1.45; overflow-wrap: anywhere; }
|
|
472
|
+
/* Hanging-indent bullet under a lead-in line — wrapped lines align past the “• ”. */
|
|
473
|
+
.diag-bullet { padding-left: 1rem; text-indent: -0.55rem; }
|
|
474
|
+
.diag-line.ok { color: var(--diff-add); }
|
|
475
|
+
.diag-line.bad { color: var(--err); }
|
|
476
|
+
.diag-line.warn { color: var(--fg); }
|
|
477
|
+
.diag-line.muted { color: var(--muted); }
|
|
478
|
+
.diag-line.small { font-size: .72rem; }
|
|
479
|
+
|
|
480
|
+
/* Running-breakouts list — shares the popover chrome (.servers-pop above); its own dimensions +
|
|
481
|
+
per-row layout: the play/stop button + name read together on the left (▶/■ Title, name flex:1),
|
|
482
|
+
and the metadata/controls form a right-aligned cluster of fixed-width columns (logs · trust ·
|
|
483
|
+
time/port) that line up row-to-row — right-anchored, so the rightmost (time/port) always aligns and
|
|
484
|
+
the rest stack inward (logs and time/port share a width; trust is wider, to fit "restricted"). The
|
|
485
|
+
filter pins at the bottom and the list scrolls inside (see .bulb-list). */
|
|
486
|
+
.servers-pop { width: min(560px, calc(100vw - 2rem)); }
|
|
487
|
+
.server-row {
|
|
488
|
+
/* Subgrid: all rows share ONE set of column tracks (defined on .bulb-list). A per-row grid sizes
|
|
489
|
+
its columns independently — which is exactly what broke alignment (a stopped row with no
|
|
490
|
+
logs/port sized its tracks differently and its trust toggle drifted right). Spanning the parent's
|
|
491
|
+
tracks instead makes trust/logs/time line up row-to-row, while the parent's auto columns size to
|
|
492
|
+
the widest content so the cluster stays tight (no reserved min-width slack). Absent trust/logs
|
|
493
|
+
render an empty cell that still holds its column. */
|
|
494
|
+
display: grid;
|
|
495
|
+
grid-template-columns: subgrid;
|
|
496
|
+
grid-column: 1 / -1;
|
|
497
|
+
align-items: center;
|
|
498
|
+
/* Inset rounded highlight, matching the session picker. The background is the subgrid track area
|
|
499
|
+
(not bled out to the edge — that ran it under the scrollbar on the right while staying clear on
|
|
500
|
+
the left, an asymmetry). Text gets its breathing room inside the highlight via end-item margins
|
|
501
|
+
(.server-stop/.bulb-launch on the left, .server-port/.bulb-time on the right) rather than row
|
|
502
|
+
padding, which would shift the subgrid tracks and break alignment. */
|
|
503
|
+
padding: .05rem 0;
|
|
504
|
+
min-height: var(--menu-row-h);
|
|
505
|
+
border-radius: 6px;
|
|
506
|
+
font-size: .85rem;
|
|
507
|
+
}
|
|
508
|
+
.server-row:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
509
|
+
/* Keyboard-highlighted row (arrow-key cursor) — a touch stronger than hover so it stands out. */
|
|
510
|
+
.server-row.active { background: color-mix(in srgb, var(--accent) 16%, transparent); }
|
|
511
|
+
/* Breakout spotlight (bulbsPill.ts): the new bulb's row stays bright while the rest dim, until the
|
|
512
|
+
menu closes or that row is hovered. The transition eases the dim in and back out. */
|
|
513
|
+
.server-row { transition: opacity .3s ease; }
|
|
514
|
+
.server-row.dimmed { opacity: .3; }
|
|
515
|
+
.server-name {
|
|
516
|
+
flex: 1; min-width: 0;
|
|
517
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
518
|
+
color: var(--accent); text-decoration: none; cursor: pointer;
|
|
519
|
+
}
|
|
520
|
+
.server-name:hover { text-decoration: underline; }
|
|
521
|
+
/* Port links to the running server (localhost:<port>) — painted the bright fold-marker green
|
|
522
|
+
(lime in dark, deep green in light), not the name's blue accent: the :port sits at the row's
|
|
523
|
+
far right, away from the play button, so making it the most prominent thing in the row lets the
|
|
524
|
+
eye snap to it the moment a just-launched row lands. The green also distinguishes "open the live
|
|
525
|
+
app" (port) from "open the source" (the blue name), and echoes the fold-marker on collapsed bulbs. */
|
|
526
|
+
.server-port { color: var(--fold-marker); font-size: .76rem; font-variant-numeric: tabular-nums; text-decoration: none; text-align: right; margin-right: .4rem; }
|
|
527
|
+
a.server-port:hover { text-decoration: underline; }
|
|
528
|
+
/* Logs reads like a link (flips to this server's console), not a button. */
|
|
529
|
+
.server-logs { color: var(--muted); font-size: .76rem; text-decoration: none; cursor: pointer; margin-left: .5rem; }
|
|
530
|
+
.server-logs:hover, .server-logs.on { color: var(--accent); text-decoration: underline; }
|
|
531
|
+
/* Launcher filter box — pinned above the list; type to narrow (matches name + path). The control
|
|
532
|
+
wrapper pins to the top and anchors the clear (×) at the far right (position:relative + the
|
|
533
|
+
input's right padding reserves its room). */
|
|
534
|
+
.bulb-filter-control { position: relative; flex: none; margin-top: .35rem; }
|
|
535
|
+
.bulb-filter {
|
|
536
|
+
font: inherit;
|
|
537
|
+
font-size: .82rem;
|
|
538
|
+
width: 100%;
|
|
539
|
+
box-sizing: border-box;
|
|
540
|
+
padding: .35rem 1.7rem .35rem .5rem;
|
|
541
|
+
border-radius: 6px;
|
|
542
|
+
background: var(--bg);
|
|
543
|
+
border: 1px solid var(--border);
|
|
544
|
+
color: var(--fg);
|
|
545
|
+
}
|
|
546
|
+
.bulb-filter::placeholder { color: var(--muted); }
|
|
547
|
+
.bulb-filter:focus { outline: none; border-color: var(--accent); }
|
|
548
|
+
/* Clear (×) — only rendered when the filter is non-empty; wipes it and refocuses the box. */
|
|
549
|
+
.bulb-filter-clear {
|
|
550
|
+
position: absolute; right: .3rem; top: 50%; transform: translateY(-50%);
|
|
551
|
+
appearance: none; border: none; background: transparent; cursor: pointer;
|
|
552
|
+
color: var(--muted); font-size: 1.05rem; line-height: 1; padding: 0 .3rem; border-radius: 4px;
|
|
553
|
+
}
|
|
554
|
+
.bulb-filter-clear:hover { color: var(--accent); }
|
|
555
|
+
/* Filter-mode toggle (full-text search) — inset at the input's right edge; the clear × shifts
|
|
556
|
+
inward beside it. Both comboboxes render it (SearchComboboxPill's modeToggle). */
|
|
557
|
+
.bulb-filter-control.has-trailing .bulb-filter { padding-right: 3.8rem; }
|
|
558
|
+
.bulb-filter-control.has-trailing .bulb-filter-clear { right: 2.5rem; }
|
|
559
|
+
.bulb-filter-mode {
|
|
560
|
+
/* Stretches flush to the input's top/right/bottom, inside its 1px border; the right corners
|
|
561
|
+
match the input's 6px radius so the latched fill reads as part of the box. */
|
|
562
|
+
position: absolute; right: 1px; top: 1px; bottom: 1px;
|
|
563
|
+
appearance: none; border: none; background: transparent; cursor: pointer;
|
|
564
|
+
font-size: .8rem; line-height: 1; padding: 0 .45rem;
|
|
565
|
+
border-radius: 0 5px 5px 0;
|
|
566
|
+
}
|
|
567
|
+
/* Same state cue as the prose pill's glyph: the emoji can't take currentColor, so it goes
|
|
568
|
+
grayscale at rest, color while hovered or on — filtered on the inner span, never the button. */
|
|
569
|
+
.bulb-filter-mode .glyph-img { filter: grayscale(1); opacity: .75; }
|
|
570
|
+
.bulb-filter-mode:hover .glyph-img, .bulb-filter-mode.on .glyph-img { filter: none; opacity: 1; }
|
|
571
|
+
/* Latched: a --panel fill — the popover's own surface, one step off the input's --bg. */
|
|
572
|
+
.bulb-filter-mode.on { background: var(--panel); }
|
|
573
|
+
/* Scrolls within the fixed-height popover so the filter stays put and the box never grows. */
|
|
574
|
+
/* The shared grid for the launcher rows: each .server-row is a subgrid spanning these tracks, so
|
|
575
|
+
columns line up row-to-row. auto cols size to their widest content (tight, no slack); 1fr is the
|
|
576
|
+
name. A small right padding keeps the selected-row highlight off the scrollbar. */
|
|
577
|
+
/* grid-auto-rows: max-content — the grid twin of .picker-row's flex:none: when the list overflows
|
|
578
|
+
the popover cap, plain auto rows compress to each row's min-height contribution, so a full-text
|
|
579
|
+
row's snippet line bleeds under the next row as overlapping text. max-content tracks never
|
|
580
|
+
compress; the list scrolls, rows keep their content height. */
|
|
581
|
+
.bulb-list { display: grid; grid-template-columns: auto 1fr auto auto auto; grid-auto-rows: max-content; column-gap: .6rem; row-gap: .15rem; align-items: center; align-content: start; padding-right: 4px; flex: 1; min-height: 0; overflow-y: auto; }
|
|
582
|
+
/* A stopped bulb's name isn't a link (no running URL), so it stays plain foreground. */
|
|
583
|
+
.server-name.stopped { color: var(--fg); }
|
|
584
|
+
/* The 1fr name column: name + (in full-text mode) its hit count read together, the badge
|
|
585
|
+
right-aligned at the column's end by the name's flex:1 — the session row's preview/hits layout. */
|
|
586
|
+
.bulb-name-cell { display: flex; align-items: baseline; gap: .6rem; min-width: 0; }
|
|
587
|
+
/* Full-text snippet in the launcher: a second internal row of the row's subgrid, aligned under the
|
|
588
|
+
name column (the session picker indents past its dot gutter instead — override that margin). */
|
|
589
|
+
.bulb-list .picker-snippet { grid-column: 2 / -1; margin: 0 .4rem .2rem 0; }
|
|
590
|
+
.bulb-time { color: var(--muted); font-size: .72rem; font-variant-numeric: tabular-nums; text-align: right; margin-right: .4rem; }
|
|
591
|
+
/* The row's chrome buttons share one reset; sizing + colour differ below. Play/stop are round icon
|
|
592
|
+
buttons (friendlier, space-economic); the trust switch is text. Logs is a link, not a button
|
|
593
|
+
(see .server-logs above), so it's not in this group. */
|
|
594
|
+
.server-stop, .bulb-launch, .trust-toggle {
|
|
595
|
+
font: inherit; appearance: none; margin: 0; cursor: pointer; box-sizing: border-box;
|
|
596
|
+
background: transparent; border: 1px solid var(--border); border-radius: 5px;
|
|
597
|
+
height: 1.7rem; display: inline-flex; align-items: center; justify-content: center;
|
|
598
|
+
}
|
|
599
|
+
/* Shorter than the icon buttons: a bordered text pill at full row height reads as cramped, where
|
|
600
|
+
the round play/stop icons are meant to fill it. */
|
|
601
|
+
.trust-toggle { font-size: .76rem; height: 1.45rem; padding: 0 .5rem; color: var(--muted); justify-self: center; }
|
|
602
|
+
.trust-toggle:hover, .trust-toggle.on { color: var(--accent); border-color: var(--accent); }
|
|
603
|
+
.server-stop, .bulb-launch { width: 1.7rem; padding: 0; border-radius: 50%; margin-left: .4rem; }
|
|
604
|
+
.btn-icon { display: block; }
|
|
605
|
+
/* A stopped bulb's play button is dormant — the title is the primary launch target — so it's
|
|
606
|
+
hidden until the row is hovered or keyboard-selected. visibility (not display) reserves its slot,
|
|
607
|
+
so the name column doesn't shift when it appears. Painted the fold-marker green, matching the
|
|
608
|
+
:port link: green is the live-app axis (play to launch → :port to open it), distinct from the
|
|
609
|
+
blue source/name. The green even travels left→right as a bulb goes stopped→running. */
|
|
610
|
+
.bulb-launch { color: var(--fold-marker); visibility: hidden; }
|
|
611
|
+
.server-row:hover .bulb-launch, .server-row.active .bulb-launch { visibility: visible; }
|
|
612
|
+
.bulb-launch:hover { border-color: var(--fold-marker); background: color-mix(in srgb, var(--fold-marker) 12%, transparent); }
|
|
613
|
+
/* Launching: stays visible (hover-independent) and shimmers until the row flips to a stop button,
|
|
614
|
+
so the ~2s wait reads as "working" rather than a dead click. Transient — driven by the in-memory
|
|
615
|
+
launching set, never persisted. */
|
|
616
|
+
.bulb-launch.launching { visibility: visible; border-color: var(--fold-marker); }
|
|
617
|
+
.server-stop { color: var(--muted); }
|
|
618
|
+
.server-stop:hover { color: var(--err); border-color: var(--err); }
|
|
619
|
+
/* Elevation prompt (VS-Code-Workspace-Trust style) — a modal over the whole view, since a
|
|
620
|
+
denied capability is worth interrupting for. Backdrop dims; the card holds the choice. */
|
|
621
|
+
.trust-back {
|
|
622
|
+
position: fixed; inset: 0; z-index: 50;
|
|
623
|
+
background: rgba(0, 0, 0, .5);
|
|
624
|
+
display: flex; align-items: center; justify-content: center;
|
|
625
|
+
padding: 1rem;
|
|
626
|
+
}
|
|
627
|
+
.trust-modal {
|
|
628
|
+
width: min(420px, 100%);
|
|
629
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
|
|
630
|
+
box-shadow: 0 12px 48px rgba(0, 0, 0, .5);
|
|
631
|
+
padding: 1rem 1.1rem; display: flex; flex-direction: column; gap: .6rem;
|
|
632
|
+
}
|
|
633
|
+
.trust-modal-h { font-size: 1rem; font-weight: 600; color: var(--fg); }
|
|
634
|
+
.trust-modal-b { font-size: .85rem; color: var(--fg); line-height: 1.45; }
|
|
635
|
+
.trust-modal-warn { font-size: .78rem; color: var(--muted); line-height: 1.4; }
|
|
636
|
+
.trust-modal-acts { display: flex; justify-content: flex-end; gap: .5rem; margin-top: .2rem; }
|
|
637
|
+
.trust-no, .trust-yes {
|
|
638
|
+
font: inherit; font-size: .82rem; appearance: none; margin: 0; cursor: pointer;
|
|
639
|
+
padding: .35rem .8rem; border-radius: 7px; border: 1px solid var(--border); background: transparent; color: var(--fg);
|
|
640
|
+
}
|
|
641
|
+
.trust-no:hover { border-color: var(--fg); }
|
|
642
|
+
.trust-yes {
|
|
643
|
+
color: var(--accent);
|
|
644
|
+
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
|
|
645
|
+
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
646
|
+
}
|
|
647
|
+
.trust-yes:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
648
|
+
/* Drilled-in streaming console for one running server. It takes over the whole bulbs
|
|
649
|
+
popover (the list flips to it; ← back flips home), so it's wider than the list and its
|
|
650
|
+
body fills the popover's height. overflow:hidden lets the rounded popover border clip the
|
|
651
|
+
square head/body; the body scrolls internally. */
|
|
652
|
+
.servers-pop.log-mode {
|
|
653
|
+
padding: 0;
|
|
654
|
+
/* The one fixed-height surface: a console wants its full frame regardless of how little it has
|
|
655
|
+
logged yet — a height that tracked the log would grow line by line. */
|
|
656
|
+
height: var(--menu-h);
|
|
657
|
+
}
|
|
658
|
+
.servers-pop.log-mode:focus { outline: none; }
|
|
659
|
+
.bulb-log-head {
|
|
660
|
+
display: flex; align-items: center; justify-content: flex-end; gap: .5rem;
|
|
661
|
+
padding: .3rem .5rem; background: var(--panel);
|
|
662
|
+
border-bottom: 1px solid var(--border);
|
|
663
|
+
}
|
|
664
|
+
.bulb-log-back {
|
|
665
|
+
font: inherit; font-size: .76rem; appearance: none; margin: 0;
|
|
666
|
+
padding: .15rem .5rem; border-radius: 5px;
|
|
667
|
+
background: transparent; border: 1px solid var(--border);
|
|
668
|
+
color: var(--muted); cursor: pointer; white-space: nowrap;
|
|
669
|
+
}
|
|
670
|
+
.bulb-log-back:hover { color: var(--accent); border-color: var(--accent); }
|
|
671
|
+
.bulb-log-name { min-width: 0; color: var(--muted); font-size: .72rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
672
|
+
.bulb-log-body {
|
|
673
|
+
flex: 1; min-height: 0;
|
|
674
|
+
margin: 0; padding: .5rem .6rem;
|
|
675
|
+
overflow: auto; /* top-anchored normal flow — output reads top→down */
|
|
676
|
+
background: var(--bg);
|
|
677
|
+
font: .72rem/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
678
|
+
color: var(--fg);
|
|
679
|
+
}
|
|
680
|
+
.bulb-log-text { white-space: pre-wrap; word-break: break-word; }
|
|
681
|
+
/* messages */
|
|
682
|
+
.messages {
|
|
683
|
+
flex: 1;
|
|
684
|
+
overflow-y: auto;
|
|
685
|
+
/* Content runs under the overlaid statusbar rather than stopping above it.
|
|
686
|
+
Gutters are symmetric: the turn stripe is absolutely positioned, so it claims no layout
|
|
687
|
+
width and the left gutter needs no extra room for it (see Invariants). The bottom is
|
|
688
|
+
larger to clear the overlaid statusbar pills (~2.1rem:
|
|
689
|
+
.4rem inset + 1.7rem pill) so the last line isn't hidden behind them. */
|
|
690
|
+
padding: 1.25rem 1.75rem 2.5rem 1.75rem;
|
|
691
|
+
display: flex;
|
|
692
|
+
flex-direction: column;
|
|
693
|
+
gap: 1rem;
|
|
694
|
+
}
|
|
695
|
+
.note {
|
|
696
|
+
display: flex;
|
|
697
|
+
align-items: baseline;
|
|
698
|
+
gap: .5rem;
|
|
699
|
+
color: var(--muted);
|
|
700
|
+
font-size: .9rem;
|
|
701
|
+
max-width: var(--content-max);
|
|
702
|
+
width: 100%;
|
|
703
|
+
margin: 0 auto;
|
|
704
|
+
}
|
|
705
|
+
.bubble {
|
|
706
|
+
position: relative;
|
|
707
|
+
max-width: var(--content-max);
|
|
708
|
+
width: 100%;
|
|
709
|
+
margin: 0 auto;
|
|
710
|
+
}
|
|
711
|
+
.bubble.user {
|
|
712
|
+
background: var(--veil);
|
|
713
|
+
border: 1px solid var(--border);
|
|
714
|
+
border-radius: 12px;
|
|
715
|
+
padding: .7rem .9rem;
|
|
716
|
+
}
|
|
717
|
+
/* Consecutive tool-only bubbles sit tight via negative margin (flex `gap`
|
|
718
|
+
can't be overridden per sibling pair). The adjacent-sibling selector leaves
|
|
719
|
+
the first tools-only bubble's gap intact, preserving the turn boundary. */
|
|
720
|
+
.bubble.tools-only + .bubble.tools-only { margin-top: -.9rem; }
|
|
721
|
+
|
|
722
|
+
/* Turn stripe — a 2px colored bar to the left of each bubble. All bubbles in
|
|
723
|
+
one user→assistant turn share a color; the palette cycles through 5 so
|
|
724
|
+
adjacent turns are visually distinct when scrolling. Sits outside the
|
|
725
|
+
bubble's left edge in the messages container's padding gutter.
|
|
726
|
+
By default top/bottom extend half a gap beyond the bubble so same-turn
|
|
727
|
+
neighbours' stripes meet seamlessly across the 1rem flex gap. The two
|
|
728
|
+
turn-boundary overrides below clip those extensions so a turn's stripe
|
|
729
|
+
starts at the top of its user bubble and ends at the bottom of its last
|
|
730
|
+
bubble — leaving the full 2rem inter-turn gap stripe-free as a separator.
|
|
731
|
+
--border-w compensates the 1px padding-edge offset that user bubbles' border
|
|
732
|
+
would otherwise introduce — without it, stripes alternate 1px inset between
|
|
733
|
+
user/assistant rows. */
|
|
734
|
+
.bubble { --border-w: 0px; }
|
|
735
|
+
.bubble.user { --border-w: 1px; }
|
|
736
|
+
.bubble::before {
|
|
737
|
+
content: '';
|
|
738
|
+
position: absolute;
|
|
739
|
+
/* Offsets subtract --border-w: absolute positioning anchors to the
|
|
740
|
+
padding-edge (inside the border), so a user bubble's 1px border would
|
|
741
|
+
otherwise shave 1px off each stripe edge. */
|
|
742
|
+
/* Stripe hugs the column (1rem into the left gutter); absolute, so it consumes no layout width —
|
|
743
|
+
which is why the gutters can be symmetric (no asymmetric padding needed to "make room"), and
|
|
744
|
+
thus why breakouts need no nudge. - var(--border-w): a user bubble's 1px border pushes the
|
|
745
|
+
padding edge (the abs-pos anchor) 1px right, so without it that stripe sits 1px right of the
|
|
746
|
+
assistant bubbles' — compensate. */
|
|
747
|
+
left: calc(-1rem - var(--border-w));
|
|
748
|
+
top: calc(-.5rem - var(--border-w));
|
|
749
|
+
bottom: calc(-.5rem - var(--border-w));
|
|
750
|
+
width: 1px;
|
|
751
|
+
background: var(--turn-color, transparent);
|
|
752
|
+
}
|
|
753
|
+
/* Clip stripe at turn boundaries. A user bubble always starts a turn → no
|
|
754
|
+
upward extension. A bubble followed by a user bubble is the last of its
|
|
755
|
+
turn → no downward extension. Same for the very last bubble in the list
|
|
756
|
+
(an in-flight turn whose last assistant message is the end of the stream).
|
|
757
|
+
Same `0 - var(--border-w)` form as the default top/bottom so user bubbles'
|
|
758
|
+
1px border still aligns with the stripe edge. */
|
|
759
|
+
.bubble.user::before { top: calc(0px - var(--border-w)); }
|
|
760
|
+
.bubble:has(+ .bubble.user)::before,
|
|
761
|
+
.bubble:last-child::before { bottom: calc(0px - var(--border-w)); }
|
|
762
|
+
/* Extra gap before each new turn (user bubble preceded by anything). Combined
|
|
763
|
+
with the container's 1rem gap → ~2rem visible between turns vs ~1rem within
|
|
764
|
+
a turn. Doesn't fire on the very first bubble (no preceding sibling). */
|
|
765
|
+
.bubble + .bubble.user { margin-top: 1rem; }
|
|
766
|
+
.bubble.turn-0 { --turn-color: #4cb35a; } /* green */
|
|
767
|
+
.bubble.turn-1 { --turn-color: #e89a3c; } /* amber */
|
|
768
|
+
.bubble.turn-2 { --turn-color: #d36b9a; } /* pink */
|
|
769
|
+
.bubble.turn-3 { --turn-color: #9970d6; } /* purple */
|
|
770
|
+
.bubble.turn-4 { --turn-color: #3cb8b3; } /* teal */
|
|
771
|
+
|
|
772
|
+
/* rendered markdown */
|
|
773
|
+
.md { word-break: break-word; }
|
|
774
|
+
/* Markdown links (often many file:line citations per message) — restrained like
|
|
775
|
+
the rest of the bulb's links: accent, no persistent underline, reveal it only
|
|
776
|
+
on hover. Without this they'd carry the browser-default underline-on-every-one. */
|
|
777
|
+
.md a { color: var(--accent); text-decoration: none; }
|
|
778
|
+
.md a:hover { text-decoration: underline; }
|
|
779
|
+
.md :first-child { margin-top: 0; }
|
|
780
|
+
.md :last-child { margin-bottom: 0; }
|
|
781
|
+
.md p { margin: .5rem 0; }
|
|
782
|
+
/* Thematic break — also the divider between merged consecutive user sends (see applyUser): faint
|
|
783
|
+
so the bubble still reads as one turn, but legible in both themes. Mixed off --fg (text contrast)
|
|
784
|
+
rather than --border + opacity: --border is itself dim in dark mode, so dimming it again left the
|
|
785
|
+
line invisible there — and the faintness is a property of the line's color, not the element. */
|
|
786
|
+
.md hr {
|
|
787
|
+
border: none;
|
|
788
|
+
border-top: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
|
|
789
|
+
margin: .55rem 0;
|
|
790
|
+
}
|
|
791
|
+
.md pre {
|
|
792
|
+
background: var(--veil);
|
|
793
|
+
border: 1px solid var(--border);
|
|
794
|
+
border-radius: 8px;
|
|
795
|
+
padding: .7rem .85rem;
|
|
796
|
+
overflow-x: auto;
|
|
797
|
+
}
|
|
798
|
+
/* A pasted image (server renders an <img> from a base64 block, see blockToMarkdown) is capped to the
|
|
799
|
+
column and never blows the layout — height follows the width. */
|
|
800
|
+
.md img { max-width: 100%; height: auto; border-radius: 6px; }
|
|
801
|
+
/* Blockquote: left bar + faint tint + muted text — a quiet "quoted" callout. The bar
|
|
802
|
+
is mixed toward --border so it stays distinct from the bright turn stripe in the
|
|
803
|
+
gutter, and the tint is light enough not to read as a code/output panel. */
|
|
804
|
+
.md blockquote {
|
|
805
|
+
margin: .5rem 0;
|
|
806
|
+
padding: .35rem .85rem;
|
|
807
|
+
border-left: 3px solid var(--border);
|
|
808
|
+
background: var(--veil);
|
|
809
|
+
border-radius: 0 6px 6px 0;
|
|
810
|
+
color: var(--muted);
|
|
811
|
+
}
|
|
812
|
+
.md blockquote :first-child { margin-top: 0; }
|
|
813
|
+
.md blockquote :last-child { margin-bottom: 0; }
|
|
814
|
+
.md code {
|
|
815
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
816
|
+
font-size: .88em;
|
|
817
|
+
}
|
|
818
|
+
.md :not(pre) > code {
|
|
819
|
+
background: var(--veil);
|
|
820
|
+
padding: .1em .35em;
|
|
821
|
+
border-radius: 5px;
|
|
822
|
+
}
|
|
823
|
+
.md table { border-collapse: collapse; }
|
|
824
|
+
.md th, .md td { border: 1px solid var(--border); padding: .3rem .55rem; }
|
|
825
|
+
/* A faint header band breaks the grid's monotony and gives the table a head/body structure — tonal
|
|
826
|
+
(`--veil`, the document-panel shade), never a hue, so it reads as structure not opinion and follows
|
|
827
|
+
the host light/dark theme. As a panel the veil steps one shade above the table's container, so the
|
|
828
|
+
header still reads when the table sits inside a bubble or callout. The header text stays `--fg`:
|
|
829
|
+
weight + the fill carry it, a tint would be the opinionated bit. Left-align to the body cells. */
|
|
830
|
+
.md th { background: var(--veil); font-weight: 600; text-align: left; }
|
|
831
|
+
/* Disclosure (<details>/<summary>) from the safe-HTML allowlist (markdown.ts): a quiet --veil container,
|
|
832
|
+
subtler than a code block — assistant prose sits directly on the page, so a fill stands out; keep it
|
|
833
|
+
restrained and let the border define it. Summary is the clickable caret row (native marker), accent
|
|
834
|
+
on hover like a link. */
|
|
835
|
+
.md details {
|
|
836
|
+
margin: .5rem 0;
|
|
837
|
+
padding: .35rem .7rem;
|
|
838
|
+
background: var(--veil);
|
|
839
|
+
border: 1px solid var(--border);
|
|
840
|
+
border-radius: 8px;
|
|
841
|
+
}
|
|
842
|
+
.md summary { cursor: pointer; font-weight: 600; }
|
|
843
|
+
.md summary:hover { color: var(--accent); }
|
|
844
|
+
|
|
845
|
+
/* The full-lane breakout geometry, defined ONCE and shared by the two breakouts (the mermaid
|
|
846
|
+
embed and a spread bulb). `100%` in --lane-ml resolves against each user's own containing block
|
|
847
|
+
(prose width) at point of use, so both centre on the column axis — which is the viewport centre,
|
|
848
|
+
since the gutters are symmetric. One home so the geometry can't drift between the two (the bug
|
|
849
|
+
that let an off-centre nudge survive in one rule after being removed from the other). */
|
|
850
|
+
.md { --lane-w: calc(100vw - 2rem); --lane-ml: calc((100% - var(--lane-w)) / 2); }
|
|
851
|
+
|
|
852
|
+
/* Full-lane breakout — the mermaid embed (whole element) and a SPREAD bulb's iframe step out of
|
|
853
|
+
the prose column to the transcript width so wide content gets room. Both use --lane-w/--lane-ml
|
|
854
|
+
and extend symmetrically from the column, which is viewport-centred (symmetric gutters), so
|
|
855
|
+
neither needs an off-centre nudge (see Invariants in Specs-Bulbs/Claude-Bulb/Claude-Bulb.md). A
|
|
856
|
+
bulb's *wrapper* never breaks out — only its inner surface does — so the wrapper and its controls
|
|
857
|
+
stay at prose-right. position + z-index:0 lift the box over the turn stripe it crosses; the
|
|
858
|
+
mermaid SVG is transparent so a bg backs it, the bulb iframe is opaque so it occludes the stripe. */
|
|
859
|
+
.md .mermaid {
|
|
860
|
+
width: var(--lane-w);
|
|
861
|
+
/* Vertical gap must be >= the box-shadow spread (1rem below): a smaller gap lets the bg ring
|
|
862
|
+
paint over the half-line of prose above/below — the trap the opaque bulb embed dodges by
|
|
863
|
+
having no shadow, which the transparent mermaid can't. */
|
|
864
|
+
margin: 1rem 0;
|
|
865
|
+
margin-left: var(--lane-ml);
|
|
866
|
+
margin-right: auto;
|
|
867
|
+
position: relative;
|
|
868
|
+
z-index: 0;
|
|
869
|
+
background: var(--bg);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/* Diagram specifics: scroll when wider than the lane; `safe center` centres one that fits and
|
|
873
|
+
left-aligns one that overflows, so a wide flowchart scrolls from its start node. The SVG is
|
|
874
|
+
transparent, so the turn stripe the breakout crosses shows through it — a bg box-shadow backs
|
|
875
|
+
the diagram to occlude it (and clears the h-scrollbar edge). The bulb embed is opaque (its
|
|
876
|
+
iframe fills the box), so it occludes the stripe itself and gets NO shadow — a shadow there
|
|
877
|
+
would paint its 1rem ring over the half-line of prose just above the embed (the gap is .6rem). */
|
|
878
|
+
.md .mermaid {
|
|
879
|
+
overflow-x: auto;
|
|
880
|
+
display: flex;
|
|
881
|
+
justify-content: safe center;
|
|
882
|
+
box-shadow: 0 0 0 1rem var(--bg);
|
|
883
|
+
}
|
|
884
|
+
/* flex-shrink:0: otherwise the SVG shrinks to the flex container and its viewBox
|
|
885
|
+
scales it down — no overflow, no scrollbar. Natural width is what scrolls. */
|
|
886
|
+
.md .mermaid svg { max-width: none; height: auto; flex: 0 0 auto; }
|
|
887
|
+
|
|
888
|
+
/* Raw ```svg``` embeds: no breakout — these are drawings (smiley, plot), not wide
|
|
889
|
+
diagrams, so keep them in the prose column, centred and capped at its width. They
|
|
890
|
+
share the `.embed` stripe-chop below (a drawing earns its own row like a bulb). */
|
|
891
|
+
.md .svg-embed { justify-content: center; }
|
|
892
|
+
.md .svg-embed svg { max-width: 100%; height: auto; }
|
|
893
|
+
|
|
894
|
+
/* Markdown tables: chop in-column (the `.embed` rule below) and `safe center` — a table that fits its
|
|
895
|
+
box centres, and one that overflows falls back to left so it scrolls from its first column rather than
|
|
896
|
+
clipping. Centring holds in both modes: a small in-column table sits centred in the prose column, a
|
|
897
|
+
spread one centred in the lane. The scroll lives on the inner `.table-scroll`, not the `.embed`
|
|
898
|
+
wrapper, so a horizontal scrollbar rides above the chop's bottom padding instead of flush against the
|
|
899
|
+
next line (a scrollbar sits at its own container's bottom edge). It's the centred flex child, capped at
|
|
900
|
+
the box so it shrinks-to-fit and scrolls only what overflows; `max-width:100%` + the overflow-induced
|
|
901
|
+
0 min-width let it shrink below the table's natural width. `spread` widens the wrapper to the lane (the
|
|
902
|
+
mermaid geometry) for a table fitTableEmbeds found too wide for the column. The opaque `.embed`
|
|
903
|
+
padding/z-index still occludes the turn stripe the spread crosses — no box-shadow needed. */
|
|
904
|
+
.md .table-embed { justify-content: safe center; }
|
|
905
|
+
.md .table-embed .table-scroll { overflow-x: auto; max-width: 100%; }
|
|
906
|
+
/* max-content keeps the table at its natural (unwrapped) width so it overflows `.table-scroll` and earns
|
|
907
|
+
a scrollbar, rather than wrapping its cells to fit — the bar is the point, and it now rides inside the
|
|
908
|
+
chop padding. */
|
|
909
|
+
/* Two ceilings work together so a prose-heavy table neither scrolls forever nor squashes to mush:
|
|
910
|
+
- per-column (the cell `max-width` — in an auto-layout table every cell in a column shares one width,
|
|
911
|
+
so a cell cap *is* a column cap): no single column hogs; long text wraps at a readable measure.
|
|
912
|
+
- whole-table (`max-width: var(--lane-w)`): the table never grows past the lane, so when columns don't
|
|
913
|
+
all fit, the browser's auto-layout *shares* the lane among them — wrapping each to its slice rather
|
|
914
|
+
than scrolling. It only scrolls once even longest-word-only columns can't fit the lane (genuinely too
|
|
915
|
+
many columns). `width: max-content` is still the *preferred* width, so a small table stays natural
|
|
916
|
+
and a wide one spreads; the lane cap just bounds the worst case. The floor lever (a per-cell
|
|
917
|
+
`min-width`) — turn squash into scroll past some readable minimum — is deliberately left off until a
|
|
918
|
+
real table needs it. `word-break: normal` resets the global `.md { word-break: break-word }` (which
|
|
919
|
+
behaves like overflow-wrap:anywhere): left inherited, that global lets the lane cap squeeze a column
|
|
920
|
+
below its longest word and snap it mid-character ("status" → "statu·s"). Reset to word-boundary
|
|
921
|
+
wrapping so each column floors at its longest whole word and a too-tight table scrolls instead of
|
|
922
|
+
shattering text; a pathological no-space token (a long URL) then scrolls rather than breaking. */
|
|
923
|
+
.md .table-embed table { margin: 0; width: max-content; max-width: var(--lane-w); }
|
|
924
|
+
.md .table-embed th, .md .table-embed td { max-width: 40ch; word-break: normal; }
|
|
925
|
+
.md .table-embed.spread {
|
|
926
|
+
width: var(--lane-w);
|
|
927
|
+
margin-left: var(--lane-ml);
|
|
928
|
+
margin-right: auto;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/* ````bulb```` embeds: a sandboxed nested app. createBulbFrame owns the iframe (auto-height,
|
|
932
|
+
borderless). No frame of our own — like the mermaid/svg embeds, it sits in the flow rather
|
|
933
|
+
than in a box, so it reads as part of the transcript; the rounded clip just tidies the corners
|
|
934
|
+
of a bulb that paints its own background. `fit` (default) keeps it in the prose column and
|
|
935
|
+
caps its height (below); `spread` adds full-lane breakout + spacing from the shared rule above.
|
|
936
|
+
position:relative anchors the controls overlay in BOTH modes (spread re-declares it for the
|
|
937
|
+
z-index lift). .err is the compile-failure fallback; .bulb-err-strip is a runtime-error strip
|
|
938
|
+
under a live embed (both monospace, muted red). */
|
|
939
|
+
/* The stripe-chop, shared by every opaque in-column visual artifact (the fit bulb and a raw
|
|
940
|
+
`svg` — each opts in by carrying `.embed`). An opaque bg over position:relative + z-index:0
|
|
941
|
+
paints above .bubble::before; a -1rem left extension reaches the stripe in the gutter without a
|
|
942
|
+
full breakout (padding-left:1rem keeps the content in the prose column, and the box's right edge
|
|
943
|
+
stays at prose-right, so a bulb's controls anchored there don't move). The vertical gap is opaque
|
|
944
|
+
padding, not margin: it occludes the stripe top and bottom so the cut is flush with the prose (no
|
|
945
|
+
stub in a transparent gap), symmetric above/below to separate the artifact from surrounding text.
|
|
946
|
+
margin top/bottom are zeroed — and the abutting prose margins too (below) — so the gap butts the
|
|
947
|
+
text. Mermaid is NOT on this path: it's transparent and breaks out, occluding via box-shadow.
|
|
948
|
+
Per-artifact layout (flex direction, centring) lives on .bulb-embed / .svg-embed. */
|
|
949
|
+
.md .embed {
|
|
950
|
+
display: flex;
|
|
951
|
+
position: relative;
|
|
952
|
+
z-index: 0;
|
|
953
|
+
background: var(--bg);
|
|
954
|
+
margin: 0 0 0 -1rem;
|
|
955
|
+
padding: 1.1rem 0 1.1rem 1rem;
|
|
956
|
+
}
|
|
957
|
+
/* A bulb stacks its controls/frame/code vertically; in spread mode the inner iframe's own
|
|
958
|
+
breakout covers the stripe too (the wrapper still chops in column). */
|
|
959
|
+
.md .bulb-embed { flex-direction: column; }
|
|
960
|
+
/* Inline (default): cap the embed's height so a tall bulb doesn't run away down the transcript.
|
|
961
|
+
Past the cap the embed scrolls internally (its own overflow, set by the host↔embed protocol —
|
|
962
|
+
the iframe element can't scroll its srcdoc from out here). spread removes the cap. */
|
|
963
|
+
.md .bulb-embed.fit iframe { max-height: 80dvh; }
|
|
964
|
+
/* The frame host is a layout-less wrapper (domeleon owns the node; we parent the live iframe into
|
|
965
|
+
it once) — display:contents lifts the iframe to be a flex child of .bulb-embed, so the sizing and
|
|
966
|
+
spread-breakout rules target it directly, exactly as when it was appended as a direct child. The
|
|
967
|
+
code toggle hides it via .code-open rather than unmounting, so the bulb keeps its state. */
|
|
968
|
+
.bulb-frame { display: contents; }
|
|
969
|
+
.md .bulb-embed.code-open .bulb-frame { display: none; }
|
|
970
|
+
/* Spread: the inner surface (live iframe, or the code view behind the toggle) breaks out to the
|
|
971
|
+
lane while the wrapper stays in the prose column, so the controls hold at prose-right. Shares the
|
|
972
|
+
--lane-* geometry; centred on the column axis so toggling spread doesn't shift a column-fitting
|
|
973
|
+
bulb sideways. `width !important` beats createBulbFrame's inline width:100% on the iframe
|
|
974
|
+
(harmless on the code view, which has none); both opaque, so position/z-index:0 lift them over
|
|
975
|
+
the turn stripe they now cross. */
|
|
976
|
+
.md .bulb-embed.spread iframe,
|
|
977
|
+
.md .bulb-embed.spread > .bulb-code {
|
|
978
|
+
width: var(--lane-w) !important;
|
|
979
|
+
margin-left: var(--lane-ml);
|
|
980
|
+
margin-right: auto;
|
|
981
|
+
position: relative;
|
|
982
|
+
z-index: 0;
|
|
983
|
+
}
|
|
984
|
+
/* Kill the transparent margin between an embed and the prose it abuts (top and bottom):
|
|
985
|
+
the embed's own opaque padding is the gap, and a leftover prose margin would let the
|
|
986
|
+
turn stripe show through it as a stub past the last/next line of text. */
|
|
987
|
+
.md :has(+ .embed) { margin-bottom: 0; }
|
|
988
|
+
.md .embed + * { margin-top: 0; }
|
|
989
|
+
/* The rounded clip lives on the inner surfaces, not the embed (whose padding is now the
|
|
990
|
+
gap), so the bulb's own card — the iframe, or the code view behind the toggle — is
|
|
991
|
+
what carries the corners. */
|
|
992
|
+
.md .bulb-embed iframe,
|
|
993
|
+
.md .bulb-embed > .bulb-code {
|
|
994
|
+
border-radius: 8px;
|
|
995
|
+
}
|
|
996
|
+
/* Overlay pill: a small hover-revealed control floated at a container's
|
|
997
|
+
bottom-right, shared by the embed breakout button and the message copy button.
|
|
998
|
+
Muted at rest, accent on hover (instant, matching the status pills). The
|
|
999
|
+
container supplies the reveal; per-button bits layer on top. */
|
|
1000
|
+
.overlay-pill {
|
|
1001
|
+
position: absolute;
|
|
1002
|
+
bottom: .5rem;
|
|
1003
|
+
right: .5rem;
|
|
1004
|
+
font: inherit;
|
|
1005
|
+
font-size: .72rem;
|
|
1006
|
+
background: var(--panel);
|
|
1007
|
+
border: 1px solid var(--border);
|
|
1008
|
+
border-radius: 6px;
|
|
1009
|
+
color: var(--muted);
|
|
1010
|
+
padding: .12rem .45rem;
|
|
1011
|
+
/* These are word buttons that swap labels in place (fit⇄spread, code⇄run, copy⇄copied), so a
|
|
1012
|
+
min-width keeps the button from jumping as the word's length changes — "fit" is much narrower
|
|
1013
|
+
than "spread". Centered text so the floor padding sits evenly. (An icon button wouldn't want
|
|
1014
|
+
this, but every pill here is a word.) */
|
|
1015
|
+
min-width: 3.5rem;
|
|
1016
|
+
text-align: center;
|
|
1017
|
+
cursor: pointer;
|
|
1018
|
+
opacity: 0;
|
|
1019
|
+
transition: opacity .2s ease;
|
|
1020
|
+
}
|
|
1021
|
+
.overlay-pill:hover { color: var(--accent); border-color: var(--accent); }
|
|
1022
|
+
|
|
1023
|
+
/* Per-fence copy pill (code / svg / mermaid) — one reveal rule for every copyable block, since the
|
|
1024
|
+
fence rule stamps `.copyable` on each. The svg/mermaid containers are already position:relative for
|
|
1025
|
+
the chop/breakout; the code-block wrapper needs it for the absolute pill. The pill is positioned
|
|
1026
|
+
(absolute), so it paints over the static fence content regardless of DOM order. */
|
|
1027
|
+
.md .copyable { position: relative; }
|
|
1028
|
+
.md .copyable:hover .copy-src,
|
|
1029
|
+
.md .copy-src:focus-visible { opacity: 1; }
|
|
1030
|
+
.md .copy-src.done { color: var(--accent); border-color: var(--accent); }
|
|
1031
|
+
/* The wrapper carries the code block's vertical rhythm (it, not the <pre>, is the `.md` flow child
|
|
1032
|
+
now, so the first/last-child margin resets land on it); the <pre> sits flush inside. */
|
|
1033
|
+
.md .code-block { margin: .6rem 0; }
|
|
1034
|
+
.md .code-block pre { margin: 0; }
|
|
1035
|
+
|
|
1036
|
+
/* Controls (code ⇄ run, then copy, then breakout) — a centered overlay straddling the
|
|
1037
|
+
bulb's top edge: clear of the running bulb below, free to overlap the prose above.
|
|
1038
|
+
Absolute, so toggling run↔code (which resizes the bulb below) never moves it, and so
|
|
1039
|
+
the bulb's vertical gap stays small. Revealed on embed-hover or focus; inside the row
|
|
1040
|
+
the shared .overlay-pill drops its own corner-anchoring. */
|
|
1041
|
+
.md .bulb-controls {
|
|
1042
|
+
position: absolute;
|
|
1043
|
+
top: 0;
|
|
1044
|
+
right: .5rem;
|
|
1045
|
+
z-index: 2;
|
|
1046
|
+
display: flex;
|
|
1047
|
+
gap: .4rem;
|
|
1048
|
+
transform: translateY(-50%);
|
|
1049
|
+
}
|
|
1050
|
+
.md .bulb-controls .overlay-pill { position: static; }
|
|
1051
|
+
.md .bulb-embed:hover .bulb-controls .overlay-pill,
|
|
1052
|
+
.md .bulb-controls .overlay-pill:focus-visible { opacity: 1; }
|
|
1053
|
+
.md .bulb-breakout:disabled { cursor: default; opacity: 1; }
|
|
1054
|
+
/* Breakout in flight: shimmers through the ~2s write+spawn wait so the click reads as "working"
|
|
1055
|
+
rather than dead — same treatment as the launcher's play button (.bulb-launch.launching).
|
|
1056
|
+
:disabled already keeps it visible if the pointer leaves. */
|
|
1057
|
+
.md .bulb-breakout.launching { color: var(--accent); border-color: var(--accent); }
|
|
1058
|
+
/* Copy pill "copied" flash — mirrors the message .copy.done accent cue, scoped to the
|
|
1059
|
+
embed control so it doesn't pick up the bubble-hover reveal. */
|
|
1060
|
+
.md .bulb-copy.done { color: var(--accent); border-color: var(--accent); }
|
|
1061
|
+
/* Source view behind the toggle — the bulb's .bulb.md rendered as markdown (file
|
|
1062
|
+
labels + fenced code panels via the shared .md rules). Just a scroll container
|
|
1063
|
+
here, capped so a long bulb doesn't blow out the transcript. No `display` — the
|
|
1064
|
+
[hidden] attribute the toggle flips drives visibility. */
|
|
1065
|
+
/* Code view: a streamlined source listing — one labeled bar per file with the code
|
|
1066
|
+
flush beneath, not a stack of bordered panels. The container is just the scroll box;
|
|
1067
|
+
the file-label bar is the only fill (overriding .md pre's panel chrome below). */
|
|
1068
|
+
.md .bulb-code {
|
|
1069
|
+
margin: 0;
|
|
1070
|
+
padding: 0;
|
|
1071
|
+
max-height: 480px;
|
|
1072
|
+
overflow: auto;
|
|
1073
|
+
font-size: .85rem;
|
|
1074
|
+
/* A distinct surface from the transcript: the live bulb blends into the flow, but the
|
|
1075
|
+
code view reads as an inspector panel. Code panels (.md pre below) are transparent, so
|
|
1076
|
+
they show this through. */
|
|
1077
|
+
background: var(--tool-bg);
|
|
1078
|
+
}
|
|
1079
|
+
/* **file.ext** labels render as <p><strong>…</strong></p>; a bulb's source has no other
|
|
1080
|
+
top-level prose, so styling the paragraph as a header bar only ever hits a label. */
|
|
1081
|
+
.md .bulb-code p {
|
|
1082
|
+
margin: 0;
|
|
1083
|
+
padding: .3rem .85rem;
|
|
1084
|
+
/* A touch darker than the code surface so the file-label header stays distinct now
|
|
1085
|
+
that the whole code view shares one background. */
|
|
1086
|
+
background: color-mix(in srgb, var(--fg) 6%, var(--tool-bg));
|
|
1087
|
+
border-top: 1px solid var(--border);
|
|
1088
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
1089
|
+
font-size: .78rem;
|
|
1090
|
+
color: var(--muted);
|
|
1091
|
+
}
|
|
1092
|
+
.md .bulb-code p:first-child { border-top: none; }
|
|
1093
|
+
.md .bulb-code p strong { font-weight: 600; color: var(--fg); }
|
|
1094
|
+
/* Code flush on the embed bg — drop the inherited .md pre box (border/bg/radius). */
|
|
1095
|
+
.md .bulb-code pre {
|
|
1096
|
+
margin: 0;
|
|
1097
|
+
padding: .5rem .85rem .85rem;
|
|
1098
|
+
border: none;
|
|
1099
|
+
border-radius: 0;
|
|
1100
|
+
background: transparent;
|
|
1101
|
+
overflow-x: auto;
|
|
1102
|
+
}
|
|
1103
|
+
/* hljs token colors for the code view, mapped to the bulb's own theme vars (no hljs
|
|
1104
|
+
theme stylesheet) so highlighting follows light/dark rather than baking a palette
|
|
1105
|
+
that would clash in dark mode. Minimal token set — enough for legible TS/HTML/CSS/
|
|
1106
|
+
JSON/markdown without chasing every grammar scope. */
|
|
1107
|
+
.bulb-code .hljs-comment, .bulb-code .hljs-quote { color: var(--muted); font-style: italic; }
|
|
1108
|
+
.bulb-code .hljs-keyword, .bulb-code .hljs-literal, .bulb-code .hljs-built_in { color: var(--accent); }
|
|
1109
|
+
.bulb-code .hljs-tag, .bulb-code .hljs-name, .bulb-code .hljs-selector-tag { color: var(--accent); }
|
|
1110
|
+
.bulb-code .hljs-string, .bulb-code .hljs-regexp, .bulb-code .hljs-meta .hljs-string {
|
|
1111
|
+
color: color-mix(in srgb, var(--diff-add) 85%, var(--fg));
|
|
1112
|
+
}
|
|
1113
|
+
.bulb-code .hljs-number { color: color-mix(in srgb, var(--accent) 55%, var(--fg)); }
|
|
1114
|
+
.bulb-code .hljs-attr, .bulb-code .hljs-attribute, .bulb-code .hljs-property, .bulb-code .hljs-type {
|
|
1115
|
+
color: color-mix(in srgb, var(--accent) 75%, var(--fg));
|
|
1116
|
+
}
|
|
1117
|
+
.bulb-code .hljs-title, .bulb-code .hljs-title.function_, .bulb-code .hljs-section {
|
|
1118
|
+
color: var(--fg); font-weight: 600;
|
|
1119
|
+
}
|
|
1120
|
+
.bulb-code .hljs-symbol, .bulb-code .hljs-bullet, .bulb-code .hljs-punctuation { color: var(--muted); }
|
|
1121
|
+
.md .bulb-embed.err {
|
|
1122
|
+
padding: .6rem .8rem;
|
|
1123
|
+
font-family: ui-monospace, monospace;
|
|
1124
|
+
font-size: .85em;
|
|
1125
|
+
color: color-mix(in srgb, var(--err) 70%, var(--fg));
|
|
1126
|
+
white-space: pre-wrap;
|
|
1127
|
+
}
|
|
1128
|
+
.md .bulb-err-strip {
|
|
1129
|
+
padding: .45rem .7rem;
|
|
1130
|
+
border-top: 1px solid var(--border);
|
|
1131
|
+
background: color-mix(in srgb, var(--err) 8%, var(--bg));
|
|
1132
|
+
font-family: ui-monospace, monospace;
|
|
1133
|
+
font-size: .8em;
|
|
1134
|
+
color: color-mix(in srgb, var(--err) 75%, var(--fg));
|
|
1135
|
+
white-space: pre-wrap;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/* Structural-defect strip (a malformed bulb that still rendered) — same shape as the error strip but
|
|
1139
|
+
amber: a warning, not a failure. */
|
|
1140
|
+
.md .bulb-warn-strip {
|
|
1141
|
+
padding: .45rem .7rem;
|
|
1142
|
+
border-top: 1px solid var(--border);
|
|
1143
|
+
background: color-mix(in srgb, rgb(200 140 30) 10%, var(--bg));
|
|
1144
|
+
font-family: ui-monospace, monospace;
|
|
1145
|
+
font-size: .8em;
|
|
1146
|
+
color: color-mix(in srgb, rgb(200 140 30) 80%, var(--fg));
|
|
1147
|
+
white-space: pre-wrap;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/* tool calls */
|
|
1151
|
+
/* Closed tool: just a single-line row, no chrome — a tool-only turn stays a
|
|
1152
|
+
tight list, not N stacked panels. Open tools get the panel treatment below. */
|
|
1153
|
+
.tool {
|
|
1154
|
+
font-size: .85rem;
|
|
1155
|
+
}
|
|
1156
|
+
/* Clickable caret row, shared by tool heads and the collapsed-turn summary: a flex
|
|
1157
|
+
row that's an unselectable click target. align-items:center centers the caret
|
|
1158
|
+
against the label box (verb↔summary alignment is handled inside .tool-label). */
|
|
1159
|
+
.tool-head, .turn-summary {
|
|
1160
|
+
display: flex;
|
|
1161
|
+
align-items: center;
|
|
1162
|
+
gap: .5rem;
|
|
1163
|
+
padding: .15rem 0;
|
|
1164
|
+
cursor: pointer;
|
|
1165
|
+
user-select: none;
|
|
1166
|
+
}
|
|
1167
|
+
.tool.open {
|
|
1168
|
+
margin-top: .55rem;
|
|
1169
|
+
background: var(--veil);
|
|
1170
|
+
border: 1px solid var(--border);
|
|
1171
|
+
border-radius: 8px;
|
|
1172
|
+
}
|
|
1173
|
+
.tool.open .tool-head {
|
|
1174
|
+
padding: .4rem .6rem;
|
|
1175
|
+
}
|
|
1176
|
+
/* line-height: 1 keeps the oversized caret's box from inflating the row height. */
|
|
1177
|
+
.tool-caret { color: var(--muted); font-size: 1.4rem; line-height: 1; width: 1ch; }
|
|
1178
|
+
.tool-name { font-weight: 600; color: var(--accent); }
|
|
1179
|
+
/* Errors here are routine (a first-read miss the retry fixes), so a failed tool
|
|
1180
|
+
recolors just its verb rather than boxing the whole row — a quiet signal. */
|
|
1181
|
+
.tool.err .tool-name { color: var(--err); }
|
|
1182
|
+
|
|
1183
|
+
/* Collapsed-turn summary strip (intermediate steps hidden behind the final
|
|
1184
|
+
answer). Same caret/row shape as a tool head, italic muted label. */
|
|
1185
|
+
.turn-summary { color: var(--muted); font-size: .85rem; }
|
|
1186
|
+
.turn-summary-text { font-style: italic; }
|
|
1187
|
+
.turn-summary:hover .turn-summary-text { color: var(--fg); }
|
|
1188
|
+
|
|
1189
|
+
/* An expanded abandoned-branch (fork) stub renders its orphan messages here — set apart from the live
|
|
1190
|
+
transcript with a left rule and slight fade, signalling "this is not the active conversation"
|
|
1191
|
+
(TB-LostMessage.md). The stub header itself is a .turn-summary row above. */
|
|
1192
|
+
.fork-body {
|
|
1193
|
+
margin-top: .4rem;
|
|
1194
|
+
padding-left: .6rem;
|
|
1195
|
+
border-left: 2px solid color-mix(in srgb, var(--fg) 18%, transparent);
|
|
1196
|
+
opacity: .82;
|
|
1197
|
+
/* --turn-color is a custom property and INHERITS — the stub's own .bubble.turn-N sets it, so without
|
|
1198
|
+
this reset every nested orphan bubble inherits the parent turn's color and redraws the stripe
|
|
1199
|
+
(the recursive-line bug). Reset to transparent so .bubble::before falls back to no stripe here. */
|
|
1200
|
+
--turn-color: transparent;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/* A superseded embed (an earlier same-name version) folds to this "💡 <Name> — Version N" stub — it
|
|
1204
|
+
reclaims the bulb's full height. Marker-green text (--fold-marker: lime in dark, deep green in light)
|
|
1205
|
+
on a neutral gray fill (--panel) that hugs the text (width:max-content), no border: text-hugging and
|
|
1206
|
+
borderless keeps it distinct from the page's full-width bordered panels and clear of the live bulb's
|
|
1207
|
+
hover pills, and the colored title rides a gray pill rather than a green block. Its look never depends
|
|
1208
|
+
on outcome: errors show only inside the embed when expanded, so the title never changes. Click to
|
|
1209
|
+
expand the older bulb (rerun fresh) back in. */
|
|
1210
|
+
.bulb-fold {
|
|
1211
|
+
display: flex;
|
|
1212
|
+
width: max-content;
|
|
1213
|
+
max-width: 100%;
|
|
1214
|
+
align-items: center;
|
|
1215
|
+
gap: .5rem;
|
|
1216
|
+
padding: .3rem .55rem;
|
|
1217
|
+
border-radius: 6px;
|
|
1218
|
+
background: var(--panel);
|
|
1219
|
+
color: var(--fold-marker);
|
|
1220
|
+
font-size: .9rem;
|
|
1221
|
+
font-weight: 600;
|
|
1222
|
+
cursor: pointer;
|
|
1223
|
+
user-select: none;
|
|
1224
|
+
}
|
|
1225
|
+
/* The fold is the sole child of its `.md` wrapper, so the `.md :first-child`/`:last-child` margin
|
|
1226
|
+
reset (above) would zero a plain `margin` here. Scope it under `.md` so it outweighs that reset
|
|
1227
|
+
(equal specificity, later in source) and the stub actually gets its breathing room. */
|
|
1228
|
+
.md .bulb-fold { margin: .7rem 0; }
|
|
1229
|
+
.bulb-fold:hover { background: color-mix(in srgb, var(--fg) 8%, var(--panel)); }
|
|
1230
|
+
.bulb-fold .tool-caret { color: inherit; } /* follow the accent row color */
|
|
1231
|
+
/* Verb + summary are inline siblings sharing one line box and font, so they sit
|
|
1232
|
+
on a common baseline. Ellipsis truncation lives on the wrapper, not the
|
|
1233
|
+
children — overflow:hidden on the summary itself skews its baseline. */
|
|
1234
|
+
.tool-label {
|
|
1235
|
+
flex: 1;
|
|
1236
|
+
min-width: 0;
|
|
1237
|
+
white-space: nowrap;
|
|
1238
|
+
overflow: hidden;
|
|
1239
|
+
text-overflow: ellipsis;
|
|
1240
|
+
}
|
|
1241
|
+
.tool-sum {
|
|
1242
|
+
margin-left: .5rem;
|
|
1243
|
+
color: var(--muted);
|
|
1244
|
+
/* Re-enable selection the .tool-head blankets off: the verb/caret stay an
|
|
1245
|
+
unselectable click target, but the summary is real data (path, pattern,
|
|
1246
|
+
command) worth copying. A drag selects; a plain click still toggles. */
|
|
1247
|
+
user-select: text;
|
|
1248
|
+
}
|
|
1249
|
+
.tool-run { color: var(--muted); }
|
|
1250
|
+
|
|
1251
|
+
/* Code-display panels inside an open tool card: raw input/output (.tool-in,
|
|
1252
|
+
.tool-out) and per-hunk diff halves (.diff-old, .diff-new). All four share
|
|
1253
|
+
the same monospace block geometry — padding, max-height with scroll,
|
|
1254
|
+
pre-wrap with word-break, top-divider — so consolidating here means edits
|
|
1255
|
+
to "how a code panel looks" land in one place. Color treatment is the only
|
|
1256
|
+
per-variant difference. */
|
|
1257
|
+
.tool-in, .tool-out, .diff-old, .diff-new {
|
|
1258
|
+
margin: 0;
|
|
1259
|
+
padding: .55rem .7rem;
|
|
1260
|
+
border-top: 1px solid var(--border);
|
|
1261
|
+
white-space: pre-wrap;
|
|
1262
|
+
word-break: break-word;
|
|
1263
|
+
font-family: ui-monospace, Menlo, monospace;
|
|
1264
|
+
font-size: .8rem;
|
|
1265
|
+
max-height: 320px;
|
|
1266
|
+
overflow: auto;
|
|
1267
|
+
}
|
|
1268
|
+
.tool-out { color: var(--muted); }
|
|
1269
|
+
/* The band carries the add/remove signal; text stays --fg so it's readable. Colouring the text the same
|
|
1270
|
+
hue as its fill (red-on-red) is what washes a diff out — the band alone is the convention. */
|
|
1271
|
+
.diff-old {
|
|
1272
|
+
background: color-mix(in srgb, var(--err) 24%, transparent);
|
|
1273
|
+
color: var(--fg);
|
|
1274
|
+
}
|
|
1275
|
+
.diff-new {
|
|
1276
|
+
background: color-mix(in srgb, var(--diff-add) 24%, transparent);
|
|
1277
|
+
color: var(--fg);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/* Path link: plain accent, underline only on hover. skip-ink:none so the
|
|
1281
|
+
underline stays solid under a path's descenders and slashes. */
|
|
1282
|
+
.tool-sum.link {
|
|
1283
|
+
color: var(--accent);
|
|
1284
|
+
text-decoration: none;
|
|
1285
|
+
}
|
|
1286
|
+
.tool-sum.link:hover { text-decoration: underline; text-decoration-skip-ink: none; }
|
|
1287
|
+
|
|
1288
|
+
.diff { display: flex; flex-direction: column; }
|
|
1289
|
+
.diff-step {
|
|
1290
|
+
padding: .35rem .7rem;
|
|
1291
|
+
border-top: 1px solid var(--border);
|
|
1292
|
+
color: var(--muted);
|
|
1293
|
+
font-size: .72rem;
|
|
1294
|
+
text-transform: uppercase;
|
|
1295
|
+
letter-spacing: .06em;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.thinking { margin-bottom: .4rem; font-size: .82rem; color: var(--muted); }
|
|
1299
|
+
/* The <summary> toggle defaults to a text cursor — give it the pointer every other expandable uses. */
|
|
1300
|
+
.thinking summary { cursor: pointer; }
|
|
1301
|
+
.thinking pre { white-space: pre-wrap; }
|
|
1302
|
+
|
|
1303
|
+
/* Copy button: an .overlay-pill revealed on bubble-hover. */
|
|
1304
|
+
.bubble:hover .copy { opacity: 1; }
|
|
1305
|
+
/* "copied" flash: eases into accent on click (CopyButton is a Component, so its
|
|
1306
|
+
node persists across re-renders and the transition actually runs). The color
|
|
1307
|
+
transition lives only on .done, not the base pill, so hover stays instant — the
|
|
1308
|
+
trade is the revert snaps back rather than easing out. */
|
|
1309
|
+
.copy.done {
|
|
1310
|
+
opacity: 1;
|
|
1311
|
+
color: var(--accent);
|
|
1312
|
+
border-color: var(--accent);
|
|
1313
|
+
transition: opacity .2s ease, color .5s ease, border-color .5s ease;
|
|
1314
|
+
}
|
|
1315
|
+
/* Assistant bubbles carry no padding, so the shared .5rem bottom would float the
|
|
1316
|
+
pill above the last line — pin it to the text's bottom edge. */
|
|
1317
|
+
.bubble.assistant .copy { bottom: 0; }
|