@workerdeck/ui 0.13.0 → 0.16.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 +72 -0
- package/build/{SessionPanel-CZMA44NM.d.mts → SessionPanel-B9CHoq8x.d.mts} +213 -27
- package/build/{SessionPanel-CKQa4i0Y.mjs → SessionPanel-DII9MmQ8.mjs} +5192 -2542
- package/build/SessionPanel-DII9MmQ8.mjs.map +1 -0
- package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
- package/build/format.d.mts +39 -4
- package/build/format.mjs +2 -118
- package/build/index.d.mts +494 -45
- package/build/index.mjs +182 -24
- package/build/index.mjs.map +1 -1
- package/build/status-Ydzi7n6j.mjs +143 -0
- package/build/status-Ydzi7n6j.mjs.map +1 -0
- package/build/workspace.d.mts +13 -1
- package/build/workspace.mjs +111 -5
- package/build/workspace.mjs.map +1 -1
- package/package.json +14 -7
- package/src/components/agent/Composer.tsx +251 -84
- package/src/components/agent/Conversation.tsx +12 -12
- package/src/components/agent/FileCard.tsx +0 -26
- package/src/components/agent/FileTree.tsx +9 -8
- package/src/components/agent/Loader.tsx +22 -72
- package/src/components/agent/Message.tsx +11 -43
- package/src/components/agent/PermissionPrompt.tsx +0 -92
- package/src/components/agent/QuestionPrompt.tsx +0 -122
- package/src/components/agent/Reasoning.tsx +5 -19
- package/src/components/agent/Response.tsx +1 -132
- package/src/components/agent/SessionBrowser.tsx +35 -25
- package/src/components/agent/SessionPanel.tsx +312 -63
- package/src/components/agent/SessionWorkspace.tsx +36 -0
- package/src/components/agent/StatusBar.tsx +70 -15
- package/src/components/agent/ToolCallCard.tsx +17 -111
- package/src/components/agent/Transcript.tsx +710 -203
- package/src/components/agent/UsageDialog.tsx +20 -106
- package/src/components/agent/UsageMeters.tsx +133 -0
- package/src/components/agent/pulse.tsx +3 -2
- package/src/components/agent/transcript-rows.ts +82 -0
- package/src/components/agent/transcript-variant.tsx +43 -51
- package/src/components/agent/use-height-epoch.ts +60 -0
- package/src/components/agent/use-path-links.ts +147 -0
- package/src/components/agent/use-transcript-jumps.ts +190 -0
- package/src/components/prompt-area/cursor-helpers.ts +65 -0
- package/src/components/prompt-area/use-prompt-area.ts +16 -10
- package/src/components/terminal/PermissionPrompt.tsx +119 -0
- package/src/components/terminal/QuestionPrompt.tsx +322 -0
- package/src/components/terminal/StatusLine.tsx +159 -0
- package/src/components/terminal/TerminalTranscript.tsx +147 -0
- package/src/components/terminal/affordances.tsx +118 -0
- package/src/components/terminal/diff.tsx +130 -0
- package/src/components/terminal/height.ts +727 -0
- package/src/components/terminal/items.tsx +449 -0
- package/src/components/terminal/markdown.tsx +191 -0
- package/src/components/terminal/press.tsx +120 -0
- package/src/components/terminal/prompt.tsx +343 -0
- package/src/components/terminal/result-preview.ts +72 -0
- package/src/components/terminal/row.tsx +132 -0
- package/src/components/terminal/scrubber.tsx +663 -0
- package/src/components/terminal/surface.tsx +80 -0
- package/src/components/terminal/tool-run.ts +91 -0
- package/src/components/ui/Badge.tsx +6 -1
- package/src/components/ui/Empty.tsx +56 -0
- package/src/components/ui/Splitter.tsx +14 -0
- package/src/index.ts +36 -0
- package/src/lib/status.ts +59 -3
- package/src/lib/tool-icon.ts +14 -0
- package/src/styles/terminal.css +1011 -0
- package/src/styles/theme.css +73 -0
- package/build/SessionPanel-CKQa4i0Y.mjs.map +0 -1
- package/build/format.mjs.map +0 -1
- package/src/components/agent/line-prompt.tsx +0 -249
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The terminal theme's geometry and palette.
|
|
3
|
+
*
|
|
4
|
+
* Plain CSS classes rather than Tailwind utilities, and deliberately. This is a
|
|
5
|
+
* *renderer*: every row it draws lands on the same character cell, and a grid
|
|
6
|
+
* that has to be reassembled from `gap-2 pl-[calc(...)] -mx-1` at each call site
|
|
7
|
+
* is a grid that drifts the first time someone edits one row. The components in
|
|
8
|
+
* `components/terminal/` are therefore thin — they choose a class and a tone,
|
|
9
|
+
* and the geometry lives here in one place where it can be read as a whole.
|
|
10
|
+
*
|
|
11
|
+
* It also means an embedder gets the geometry without configuring Tailwind's
|
|
12
|
+
* `@source` scanning: these class names exist whether or not the scanner found
|
|
13
|
+
* the components that use them.
|
|
14
|
+
*
|
|
15
|
+
* ── The two rules everything else follows ────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* 1. **Horizontal measures are `ch`, never px or rem.** `1ch` in a monospace
|
|
18
|
+
* face is exactly one cell, so a gutter is `2ch` and an indent level is
|
|
19
|
+
* `2ch`, and text lands on a column rather than near one. A wrapped line
|
|
20
|
+
* hangs under its own first character because the body is its own grid
|
|
21
|
+
* column — not because a padding and a negative text-indent were tuned
|
|
22
|
+
* against each other.
|
|
23
|
+
* 2. **Vertical measures are whole multiples of `--term-line`, never padding.**
|
|
24
|
+
* Blank space between blocks is a *blank line* (an empty row), the way it is
|
|
25
|
+
* in a terminal. Nothing has margins, so no two blocks can disagree about
|
|
26
|
+
* the rhythm, and a row's height is always an integer number of lines — the
|
|
27
|
+
* thing that keeps text off half-pixels and out of the blur.
|
|
28
|
+
*
|
|
29
|
+
* `--term-font-size` and `--term-line` must both be whole pixels. A line height
|
|
30
|
+
* of `1.5 × 13px` is 19.5px, and every second row of a long transcript then
|
|
31
|
+
* renders on a half-pixel: the text visibly softens and hard-edged backgrounds
|
|
32
|
+
* (the diff bands) show a seam. Integers are not a preference here.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
[data-terminal] {
|
|
36
|
+
/* Metrics. Overridable per host — VS Code hands its editor font down, and a
|
|
37
|
+
phone wants a larger cell — but always in whole pixels. */
|
|
38
|
+
--term-font-size: 13px;
|
|
39
|
+
--term-line: 18px;
|
|
40
|
+
/* One indent level, and the width of a gutter. The same value on purpose:
|
|
41
|
+
a child row's marker sits exactly where its parent's text began. */
|
|
42
|
+
--term-cell: 2ch;
|
|
43
|
+
|
|
44
|
+
font-family: var(--cw-font-mono);
|
|
45
|
+
font-size: var(--term-font-size);
|
|
46
|
+
line-height: var(--term-line);
|
|
47
|
+
color: var(--term-fg);
|
|
48
|
+
/* Ligatures turn `=>` and `!=` into glyphs that no longer occupy the cells
|
|
49
|
+
they were measured as. A grid renderer cannot have that. */
|
|
50
|
+
font-variant-ligatures: none;
|
|
51
|
+
font-feature-settings: 'liga' 0, 'calt' 0;
|
|
52
|
+
/* `ch` is the advance of `0`, so tabular figures keep digit columns — the
|
|
53
|
+
diff's line numbers — genuinely aligned. */
|
|
54
|
+
font-variant-numeric: tabular-nums;
|
|
55
|
+
-webkit-font-smoothing: antialiased;
|
|
56
|
+
tab-size: 2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── Palette ─────────────────────────────────────────────────────────────────
|
|
60
|
+
*
|
|
61
|
+
* The terminal's own colours, not the app's. `--accent` is VS Code blue because
|
|
62
|
+
* WorkerDeck's chrome shares an editor's visual language; a terminal shares a
|
|
63
|
+
* terminal's, and the two must not be forced to agree. Anything reading a
|
|
64
|
+
* `--term-*` token therefore keeps its colour when the surrounding app is
|
|
65
|
+
* re-themed, which is the point: the transcript looks like the CLI wherever it
|
|
66
|
+
* is embedded.
|
|
67
|
+
*
|
|
68
|
+
* Values are read off the Claude Code CLI on a dark terminal: body text is a
|
|
69
|
+
* light grey rather than white (white is reserved for emphasis, so bold has
|
|
70
|
+
* somewhere to go), meta lines are one step down again, and the working mark is
|
|
71
|
+
* the product's coral.
|
|
72
|
+
*/
|
|
73
|
+
[data-terminal] {
|
|
74
|
+
--term-fg: #d4d4d4;
|
|
75
|
+
--term-bright: #ffffff;
|
|
76
|
+
--term-dim: #8a8a8a;
|
|
77
|
+
--term-faint: #6a6a6a;
|
|
78
|
+
|
|
79
|
+
--term-mark: #d97757;
|
|
80
|
+
--term-blue: #afb9fe;
|
|
81
|
+
--term-green: #4ec9a0;
|
|
82
|
+
--term-red: #f14c4c;
|
|
83
|
+
--term-yellow: #d7ba7d;
|
|
84
|
+
--term-magenta: #c586c0;
|
|
85
|
+
|
|
86
|
+
/* Diff bands. Full-bleed backgrounds, so they are tuned to sit under body
|
|
87
|
+
text rather than to be looked at: the CLI's added block is a dark green
|
|
88
|
+
wash the text stays legible on, not a highlight. */
|
|
89
|
+
--term-add-fg: #b5e8a9;
|
|
90
|
+
--term-add-bg: #16301a;
|
|
91
|
+
--term-del-fg: #f0a6a6;
|
|
92
|
+
--term-del-bg: #3a1518;
|
|
93
|
+
/* The unchanged context inside a diff, and the line-number column. */
|
|
94
|
+
--term-diff-fg: #9a9a9a;
|
|
95
|
+
--term-diff-num: #5a5a5a;
|
|
96
|
+
|
|
97
|
+
/* A band of code or output. Barely there — one step off the canvas. */
|
|
98
|
+
--term-band-bg: rgb(255 255 255 / 0.04);
|
|
99
|
+
/* The user's own prompt row. */
|
|
100
|
+
--term-user-bg: rgb(255 255 255 / 0.05);
|
|
101
|
+
--term-row-hover: rgb(255 255 255 / 0.05);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[data-theme='light'] [data-terminal],
|
|
105
|
+
[data-terminal][data-theme='light'] {
|
|
106
|
+
--term-fg: #2f2f2f;
|
|
107
|
+
--term-bright: #000000;
|
|
108
|
+
--term-dim: #6b6b6b;
|
|
109
|
+
--term-faint: #8d8d8d;
|
|
110
|
+
|
|
111
|
+
--term-mark: #bf5b3d;
|
|
112
|
+
--term-blue: #0a66c2;
|
|
113
|
+
--term-green: #16794a;
|
|
114
|
+
--term-red: #c62828;
|
|
115
|
+
--term-yellow: #8a6d00;
|
|
116
|
+
--term-magenta: #8b3a8b;
|
|
117
|
+
|
|
118
|
+
--term-add-fg: #12492a;
|
|
119
|
+
--term-add-bg: #e2f6e5;
|
|
120
|
+
--term-del-fg: #7a1f24;
|
|
121
|
+
--term-del-bg: #fbe6e7;
|
|
122
|
+
--term-diff-fg: #4a4a4a;
|
|
123
|
+
--term-diff-num: #9a9a9a;
|
|
124
|
+
|
|
125
|
+
--term-band-bg: rgb(0 0 0 / 0.04);
|
|
126
|
+
--term-user-bg: rgb(0 0 0 / 0.05);
|
|
127
|
+
--term-row-hover: rgb(0 0 0 / 0.04);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ── Rows ────────────────────────────────────────────────────────────────────
|
|
131
|
+
*
|
|
132
|
+
* One row = a gutter cell and a body cell. The gutter holds the marker (`●`,
|
|
133
|
+
* `⎿`, `>`) or nothing at all; the body holds the text and wraps inside its own
|
|
134
|
+
* column, which is what gives every row its hanging indent for free.
|
|
135
|
+
*/
|
|
136
|
+
.term-row {
|
|
137
|
+
display: grid;
|
|
138
|
+
grid-template-columns: var(--term-cell) minmax(0, 1fr);
|
|
139
|
+
align-items: start;
|
|
140
|
+
min-height: var(--term-line);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Indent levels. A nested row steps one cell per level — the same 2 columns a
|
|
144
|
+
marker occupies, so a child's marker sits under its parent's first letter. */
|
|
145
|
+
.term-row[data-indent='1'] { padding-left: var(--term-cell); }
|
|
146
|
+
.term-row[data-indent='2'] { padding-left: calc(2 * var(--term-cell)); }
|
|
147
|
+
.term-row[data-indent='3'] { padding-left: calc(3 * var(--term-cell)); }
|
|
148
|
+
|
|
149
|
+
.term-gutter {
|
|
150
|
+
/* `pre` so a space-only gutter keeps its cell — an unmarked row must still
|
|
151
|
+
start its text on the same column as a marked one. */
|
|
152
|
+
white-space: pre;
|
|
153
|
+
user-select: none;
|
|
154
|
+
color: var(--term-dim);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.term-body {
|
|
158
|
+
min-width: 0;
|
|
159
|
+
/* Preserve the run's own spacing, wrap at the viewport edge. `break-word`
|
|
160
|
+
rather than `anywhere`: a long path should wrap, a sentence should not be
|
|
161
|
+
chopped mid-word when a shorter break exists. */
|
|
162
|
+
white-space: pre-wrap;
|
|
163
|
+
overflow-wrap: break-word;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* A blank line — the only vertical spacing in the theme. Exactly one row tall
|
|
167
|
+
and holding nothing, which is what a terminal's blank line is. */
|
|
168
|
+
.term-blank {
|
|
169
|
+
height: var(--term-line);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* A row `scrollIntoView`ed lands one line below the top edge rather than flush
|
|
173
|
+
against it, so the target reads as the start of something rather than as
|
|
174
|
+
content cut off by the frame. Only the recap's `'center'` jump still goes
|
|
175
|
+
through scrollIntoView — the scrubber's `'start'` jumps compute their own
|
|
176
|
+
offset (a sticky row's rect is wherever it is pinned) and land the line at
|
|
177
|
+
exactly the top edge. */
|
|
178
|
+
[data-terminal] [data-slot='transcript-rows'] [data-index] {
|
|
179
|
+
scroll-margin-block-start: var(--term-line);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* The same blank line, as padding on a virtualized row's measured wrapper.
|
|
183
|
+
Identical height, and it must be padding there rather than a `<Blank/>`: the
|
|
184
|
+
virtualizer measures one element per item, so space between two items has to
|
|
185
|
+
be part of one of them or it is not measured at all. */
|
|
186
|
+
[data-terminal] .term-row-gap {
|
|
187
|
+
padding-top: var(--term-line);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ── Bands ───────────────────────────────────────────────────────────────────
|
|
191
|
+
*
|
|
192
|
+
* A run of rows carrying a background: a code block, a command's output, a diff
|
|
193
|
+
* hunk. Full-bleed — the wash reaches the scroller's edges rather than stopping
|
|
194
|
+
* at a content gutter, because in a terminal the line *is* the full width.
|
|
195
|
+
* `--term-bleed` is how far out that is; the transcript sets it to its own
|
|
196
|
+
* padding and the band cancels it with matched negative margin and padding.
|
|
197
|
+
*/
|
|
198
|
+
.term-band {
|
|
199
|
+
background: var(--term-band-bg);
|
|
200
|
+
margin-inline: calc(-1 * var(--term-bleed, 0px));
|
|
201
|
+
padding-inline: var(--term-bleed, 0px);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ── Tones ───────────────────────────────────────────────────────────────────
|
|
205
|
+
*
|
|
206
|
+
* Colour as an attribute rather than a class per palette entry: a row component
|
|
207
|
+
* takes `tone` and passes it straight through, so adding a colour is one line
|
|
208
|
+
* here and nothing in the components.
|
|
209
|
+
*/
|
|
210
|
+
[data-terminal] [data-tone='fg'] { color: var(--term-fg); }
|
|
211
|
+
[data-terminal] [data-tone='bright'] { color: var(--term-bright); }
|
|
212
|
+
[data-terminal] [data-tone='dim'] { color: var(--term-dim); }
|
|
213
|
+
[data-terminal] [data-tone='faint'] { color: var(--term-faint); }
|
|
214
|
+
[data-terminal] [data-tone='mark'] { color: var(--term-mark); }
|
|
215
|
+
[data-terminal] [data-tone='blue'] { color: var(--term-blue); }
|
|
216
|
+
[data-terminal] [data-tone='green'] { color: var(--term-green); }
|
|
217
|
+
[data-terminal] [data-tone='red'] { color: var(--term-red); }
|
|
218
|
+
[data-terminal] [data-tone='yellow'] { color: var(--term-yellow); }
|
|
219
|
+
[data-terminal] [data-tone='magenta'] { color: var(--term-magenta); }
|
|
220
|
+
|
|
221
|
+
/* Weight is the only other axis. A terminal has one type size, so emphasis has
|
|
222
|
+
to be carried by weight and colour — never by a larger glyph. */
|
|
223
|
+
[data-terminal] [data-weight='bold'] { font-weight: 600; }
|
|
224
|
+
|
|
225
|
+
/* The scroller. Its horizontal padding IS `--term-bleed` — that is the whole
|
|
226
|
+
contract a full-bleed band relies on — and its vertical padding is a line, so
|
|
227
|
+
the first row starts one blank line down like every other block. */
|
|
228
|
+
[data-terminal].term-transcript {
|
|
229
|
+
padding-inline: var(--term-bleed, 0px);
|
|
230
|
+
padding-block: var(--term-line);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ── Interaction ─────────────────────────────────────────────────────────────
|
|
234
|
+
*
|
|
235
|
+
* A terminal has no buttons, but a transcript does have things you can open. The
|
|
236
|
+
* affordance is the hover fill on the row itself — nothing gains a border, a
|
|
237
|
+
* radius or a focus box that would take it off the grid.
|
|
238
|
+
*/
|
|
239
|
+
[data-terminal] .term-press {
|
|
240
|
+
display: block;
|
|
241
|
+
width: 100%;
|
|
242
|
+
margin: 0;
|
|
243
|
+
padding: 0;
|
|
244
|
+
border: 0;
|
|
245
|
+
background: none;
|
|
246
|
+
font: inherit;
|
|
247
|
+
color: inherit;
|
|
248
|
+
text-align: left;
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
/* A transcript is read far more often than it is opened, so a pressable row
|
|
251
|
+
still has to be text you can drag a selection out of. The other half of
|
|
252
|
+
this is in `press.tsx`: the click that ends such a drag is refused. */
|
|
253
|
+
user-select: text;
|
|
254
|
+
}
|
|
255
|
+
/* The hover fill lives with the other affordances below — it is one, and it is
|
|
256
|
+
switchable for the same reason. */
|
|
257
|
+
[data-terminal] .term-press:focus-visible {
|
|
258
|
+
outline: none;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ── Affordances ─────────────────────────────────────────────────────────────
|
|
262
|
+
*
|
|
263
|
+
* What a real terminal cannot do. Every one of these costs **no layout** — a
|
|
264
|
+
* fill is a background, an action is an absolutely positioned overlay one line
|
|
265
|
+
* tall — so switching them off leaves every glyph on the cell it was already on.
|
|
266
|
+
* See `components/terminal/affordances.tsx`.
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
/* The hover fill, gated on the surface's declaration. Without `hover` in the
|
|
270
|
+
list the selector never matches and a pressable row is inert to the pointer,
|
|
271
|
+
which is what a host projecting to a real terminal wants. */
|
|
272
|
+
[data-terminal][data-affordances~='hover'] .term-press:hover > .term-row,
|
|
273
|
+
[data-terminal][data-affordances~='hover'] .term-press:focus-visible > .term-row {
|
|
274
|
+
background: var(--term-row-hover);
|
|
275
|
+
margin-inline: calc(-1 * var(--term-bleed, 0px));
|
|
276
|
+
padding-inline: var(--term-bleed, 0px);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* An opened block keeps the fill the pointer gave it, for the whole of what it
|
|
280
|
+
opened. Eighty lines of output pushes its own first line off the top of the
|
|
281
|
+
screen, and without a wash there is nothing to say where the thing you
|
|
282
|
+
expanded began — or that these rows are one block at all. Full-bleed, like
|
|
283
|
+
every other wash; the hover fill inside it reaches the same edge, so the two
|
|
284
|
+
stack without a step. (The other half is `useRevealOnOpen`, which brings that
|
|
285
|
+
first line back when it has gone above the fold.) */
|
|
286
|
+
[data-terminal] .term-open {
|
|
287
|
+
background: var(--term-row-hover);
|
|
288
|
+
margin-inline: calc(-1 * var(--term-bleed, 0px));
|
|
289
|
+
padding-inline: var(--term-bleed, 0px);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.term-hoverable {
|
|
293
|
+
position: relative;
|
|
294
|
+
}
|
|
295
|
+
/* Pinned to the block's first line at its right edge. Absolute, so the rows
|
|
296
|
+
below are exactly where they would have been without it. */
|
|
297
|
+
[data-terminal] .term-actions {
|
|
298
|
+
position: absolute;
|
|
299
|
+
top: 0;
|
|
300
|
+
right: 0;
|
|
301
|
+
height: var(--term-line);
|
|
302
|
+
display: flex;
|
|
303
|
+
gap: 1ch;
|
|
304
|
+
padding-inline: 1ch;
|
|
305
|
+
background: var(--bg, transparent);
|
|
306
|
+
opacity: 0;
|
|
307
|
+
transition: opacity var(--motion-fast, 120ms);
|
|
308
|
+
}
|
|
309
|
+
[data-terminal] .term-hoverable:hover .term-actions,
|
|
310
|
+
[data-terminal] .term-hoverable:focus-within .term-actions {
|
|
311
|
+
opacity: 1;
|
|
312
|
+
}
|
|
313
|
+
@media (hover: none) {
|
|
314
|
+
/* No pointer to hover with: showing them always is the only way they exist at
|
|
315
|
+
all, and a phone has the room. */
|
|
316
|
+
[data-terminal] .term-actions {
|
|
317
|
+
opacity: 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
[data-terminal] .term-action {
|
|
321
|
+
font: inherit;
|
|
322
|
+
line-height: var(--term-line);
|
|
323
|
+
color: var(--term-dim);
|
|
324
|
+
background: none;
|
|
325
|
+
border: 0;
|
|
326
|
+
padding: 0;
|
|
327
|
+
cursor: pointer;
|
|
328
|
+
}
|
|
329
|
+
[data-terminal] .term-action:hover {
|
|
330
|
+
color: var(--term-fg);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* What you typed, on a band of its own — the CLI's own treatment. */
|
|
334
|
+
[data-terminal] .term-user {
|
|
335
|
+
background: var(--term-user-bg);
|
|
336
|
+
margin-inline: calc(-1 * var(--term-bleed, 0px));
|
|
337
|
+
padding-inline: var(--term-bleed, 0px);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
[data-terminal] .term-link {
|
|
341
|
+
text-decoration: underline;
|
|
342
|
+
text-underline-offset: 2px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* A file path, while the modifier that would open it is held.
|
|
346
|
+
*
|
|
347
|
+
* Only then, and that is the point: a path *is* clickable but only under
|
|
348
|
+
* Cmd/Ctrl, so it should only look clickable then — an always-underlined path
|
|
349
|
+
* promises a plain click that does nothing. Applied by `usePathLinks` using the
|
|
350
|
+
* same test the click itself makes, so nothing lights up that a click ignores.
|
|
351
|
+
* Outside `[data-terminal]` too, since a cards transcript names files as well. */
|
|
352
|
+
.wd-path-link {
|
|
353
|
+
text-decoration: underline;
|
|
354
|
+
text-underline-offset: 2px;
|
|
355
|
+
cursor: pointer;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* A status-line reading that leads somewhere. Inline and unstyled until hovered
|
|
359
|
+
— the line is meant to be quiet, and five underlined segments would make it
|
|
360
|
+
the loudest row on screen. */
|
|
361
|
+
[data-terminal] .term-reading {
|
|
362
|
+
font: inherit;
|
|
363
|
+
color: inherit;
|
|
364
|
+
background: none;
|
|
365
|
+
border: 0;
|
|
366
|
+
padding: 0;
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
}
|
|
369
|
+
[data-terminal][data-affordances~='hover'] .term-reading:hover {
|
|
370
|
+
text-decoration: underline;
|
|
371
|
+
text-underline-offset: 2px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* ── Prompts ─────────────────────────────────────────────────────────────────
|
|
375
|
+
*
|
|
376
|
+
* The affordances a form needs, drawn without a single box or border that would
|
|
377
|
+
* take a row off the grid. Strokes are backgrounds (zero layout), the focused
|
|
378
|
+
* row is a fill, and the only "control" is a row you can press a number on.
|
|
379
|
+
*/
|
|
380
|
+
|
|
381
|
+
/* One line tall with the stroke through its middle, full-bleed — the boundary
|
|
382
|
+
between the run and the decision. */
|
|
383
|
+
[data-terminal] .term-rule-row {
|
|
384
|
+
height: var(--term-line);
|
|
385
|
+
margin-inline: calc(-1 * var(--term-bleed, 0px));
|
|
386
|
+
background: linear-gradient(var(--term-dim), var(--term-dim)) left center / 100% 1px no-repeat;
|
|
387
|
+
}
|
|
388
|
+
/* Dashed, for a boundary *within* a prompt. A repeating gradient rather than a
|
|
389
|
+
dashed border, for the same no-layout reason. */
|
|
390
|
+
[data-terminal] .term-rule-dashed {
|
|
391
|
+
background: repeating-linear-gradient(
|
|
392
|
+
to right,
|
|
393
|
+
var(--term-faint) 0 4px,
|
|
394
|
+
transparent 4px 8px
|
|
395
|
+
)
|
|
396
|
+
left center / 100% 1px no-repeat;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* The focused option. A fill, never an outline: an outline is 2px of layout the
|
|
400
|
+
row does not have, and the browser's own focus ring would land between cells. */
|
|
401
|
+
[data-terminal] .term-row[data-focused] {
|
|
402
|
+
background: var(--term-row-hover);
|
|
403
|
+
margin-inline: calc(-1 * var(--term-bleed, 0px));
|
|
404
|
+
padding-inline: var(--term-bleed, 0px);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* An option's expanded detail (a preview, a reason field) lines up with the
|
|
408
|
+
option's own text — one gutter in, so it reads as belonging to that row. */
|
|
409
|
+
[data-terminal] .term-detail {
|
|
410
|
+
padding-left: 5ch;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* A framed payload: four 1px strokes as backgrounds, so the frame costs no
|
|
414
|
+
layout and the text inside stays on the cell it would have been on. */
|
|
415
|
+
[data-terminal] .term-box {
|
|
416
|
+
padding-inline: var(--term-cell);
|
|
417
|
+
/* Sized to its content, never to the viewport: a preview is a sample, and a
|
|
418
|
+
frame stretched across a wide screen reads as a panel the reader is meant
|
|
419
|
+
to work in. */
|
|
420
|
+
width: max-content;
|
|
421
|
+
max-width: 100%;
|
|
422
|
+
background:
|
|
423
|
+
linear-gradient(var(--term-faint), var(--term-faint)) top left / 100% 1px no-repeat,
|
|
424
|
+
linear-gradient(var(--term-faint), var(--term-faint)) bottom left / 100% 1px no-repeat,
|
|
425
|
+
linear-gradient(var(--term-faint), var(--term-faint)) top left / 1px 100% no-repeat,
|
|
426
|
+
linear-gradient(var(--term-faint), var(--term-faint)) top right / 1px 100% no-repeat;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* A question chip. Inline so the strip is one row; the active one is filled the
|
|
430
|
+
way the CLI fills it. */
|
|
431
|
+
[data-terminal] .term-tab {
|
|
432
|
+
font: inherit;
|
|
433
|
+
color: inherit;
|
|
434
|
+
background: none;
|
|
435
|
+
border: 0;
|
|
436
|
+
padding: 0 1ch;
|
|
437
|
+
margin: 0;
|
|
438
|
+
cursor: pointer;
|
|
439
|
+
}
|
|
440
|
+
[data-terminal] .term-tab-active {
|
|
441
|
+
background: var(--term-blue);
|
|
442
|
+
color: var(--bg, #000);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* The bare text field — a caret and a rule, no box. */
|
|
446
|
+
[data-terminal] .term-input {
|
|
447
|
+
width: 100%;
|
|
448
|
+
min-width: 0;
|
|
449
|
+
font: inherit;
|
|
450
|
+
color: var(--term-fg);
|
|
451
|
+
background: none;
|
|
452
|
+
border: 0;
|
|
453
|
+
padding: 0;
|
|
454
|
+
outline: none;
|
|
455
|
+
/* The terminal's own caret: a filled cell, not a hairline. It is the last
|
|
456
|
+
thing in the theme still drawn the way the web draws it, and it is the one
|
|
457
|
+
the eye is always on. `caret-animation: manual` hands the blink to the
|
|
458
|
+
author and we then decline to animate it, so the block sits steady — a
|
|
459
|
+
blinking block is a strobe at this size, where a blinking hairline was
|
|
460
|
+
merely a caret. Both are ignored where unsupported, which is exactly the
|
|
461
|
+
right failure: a thin blinking caret is a plain caret, not a broken one. */
|
|
462
|
+
caret-shape: block;
|
|
463
|
+
caret-animation: manual;
|
|
464
|
+
background: linear-gradient(var(--term-faint), var(--term-faint)) bottom left / 100% 1px no-repeat;
|
|
465
|
+
}
|
|
466
|
+
[data-terminal] .term-input:focus {
|
|
467
|
+
background-image: linear-gradient(var(--term-blue), var(--term-blue));
|
|
468
|
+
}
|
|
469
|
+
[data-terminal] .term-input::placeholder {
|
|
470
|
+
color: var(--term-faint);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* ── Sticky prompt ───────────────────────────────────────────────────────────
|
|
474
|
+
*
|
|
475
|
+
* The pinned prompt is the prompt's **first line**, and the pin is the
|
|
476
|
+
* browser's, not script's. `TranscriptRows` renders each prompt inside a lane
|
|
477
|
+
* spanning its turn; a one-line `sticky` **head** — the same row rendered
|
|
478
|
+
* again, clipped by height — leads the lane with its flow footprint cancelled
|
|
479
|
+
* (the negative bottom margin), so in flow it lies exactly over the real
|
|
480
|
+
* row's first line and is invisible. The compositor pins it (no per-scroll
|
|
481
|
+
* JS, no frame lag) and the lane's bottom edge pushes it off when the next
|
|
482
|
+
* prompt arrives. One line and not the row, because a pasted twenty-line
|
|
483
|
+
* prompt pinned whole covers the very answer being read.
|
|
484
|
+
*
|
|
485
|
+
* The head's visible line docks flush to the top edge of the scroller. Its
|
|
486
|
+
* inter-row gap is padding on the head (matching the row it overlays), so a
|
|
487
|
+
* gap-carrying head is one line taller and pins one line negative: the
|
|
488
|
+
* padding parks above the viewport edge and the line starts at zero — which
|
|
489
|
+
* is also what makes the pin engage exactly when the line itself reaches the
|
|
490
|
+
* top, not a line early.
|
|
491
|
+
*
|
|
492
|
+
* The lane spans the whole turn and the head is an overlay, so both are
|
|
493
|
+
* transparent to the pointer — the real row beneath owns interaction, and
|
|
494
|
+
* the head is `aria-hidden` in the markup. The z-index is on the lane —
|
|
495
|
+
* every row wrapper is its own stacking context (they are transformed), so
|
|
496
|
+
* the head's own z-index could never beat a later sibling.
|
|
497
|
+
*
|
|
498
|
+
* `--bg` is the panel's own ground, the same token the hover-revealed row
|
|
499
|
+
* actions cover themselves with — the user band is translucent by design, so
|
|
500
|
+
* without a ground the rows passing beneath would show straight through the
|
|
501
|
+
* pinned head. Opaque always, which is invisible in flow: the ground behind
|
|
502
|
+
* it is the same `--bg`, and the row it covers is its own content.
|
|
503
|
+
*/
|
|
504
|
+
[data-terminal] [data-slot='transcript-rows'] > [data-sticky-lane] {
|
|
505
|
+
z-index: 3;
|
|
506
|
+
pointer-events: none;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* The head's own strip: the full lane, out of flow, so the head needs no
|
|
510
|
+
footprint-cancelling margin — sticky confinement clamps the margin box, and
|
|
511
|
+
a negative margin shrinks it to nothing, letting the head overshoot the
|
|
512
|
+
lane's end by its own height (two pinned prompts on screen at once). */
|
|
513
|
+
[data-terminal] [data-sticky-lane] > [data-sticky-headlane] {
|
|
514
|
+
position: absolute;
|
|
515
|
+
inset: 0;
|
|
516
|
+
pointer-events: none;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* Hidden until actually stuck (`data-stuck`, set by the lane's sentinel
|
|
520
|
+
observer): an overlay visible in flow sits on the real row's first line and
|
|
521
|
+
swallows its selection highlight — the line reads as unselectable. */
|
|
522
|
+
[data-terminal] [data-sticky-headlane] > [data-sticky-head] {
|
|
523
|
+
position: sticky;
|
|
524
|
+
top: 0;
|
|
525
|
+
height: var(--term-line);
|
|
526
|
+
overflow: hidden;
|
|
527
|
+
background: var(--bg, transparent);
|
|
528
|
+
user-select: none;
|
|
529
|
+
visibility: hidden;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
[data-terminal] [data-sticky-headlane] > [data-sticky-head][data-stuck] {
|
|
533
|
+
visibility: visible;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
[data-terminal] [data-sticky-headlane] > [data-sticky-head].term-row-gap {
|
|
537
|
+
top: calc(-1 * var(--term-line));
|
|
538
|
+
height: calc(2 * var(--term-line));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/* ── Composer ────────────────────────────────────────────────────────────────
|
|
542
|
+
*
|
|
543
|
+
* The foot of the panel, and a *row* like every other: `>` in the gutter cell,
|
|
544
|
+
* the caret on the body column. That is the whole design — a typed line and a
|
|
545
|
+
* rendered one start on the same character, which no chat composer does and
|
|
546
|
+
* every terminal does for free.
|
|
547
|
+
*
|
|
548
|
+
* Docked rather than floating: no radius, no shadow, and a rule along the top
|
|
549
|
+
* *and* the bottom, both turning accent while anything inside has focus. Those
|
|
550
|
+
* two rules are the CLI's own frame for its prompt, and they are the only
|
|
551
|
+
* affordance — which is what the transcript above (no boxes anywhere) asks for.
|
|
552
|
+
* They also do the work a box would: the field reads as its own strip of the
|
|
553
|
+
* panel rather than as the last row of the transcript, without a border on the
|
|
554
|
+
* sides that would take the `❯` off the column the rows start on.
|
|
555
|
+
*
|
|
556
|
+
* The bottom rule is why the status bar under it draws none of its own — see
|
|
557
|
+
* `StatusBar.tsx`. Two adjacent 1px rules is a 2px rule with a seam in it.
|
|
558
|
+
*/
|
|
559
|
+
[data-terminal].term-composer {
|
|
560
|
+
border-top: 1px solid var(--term-faint);
|
|
561
|
+
border-bottom: 1px solid var(--term-faint);
|
|
562
|
+
transition: border-color 120ms;
|
|
563
|
+
}
|
|
564
|
+
/* The bar spans the panel; its contents sit in the transcript's own column, so
|
|
565
|
+
the `>` lands on the column the rows above it start on. Two elements because
|
|
566
|
+
those are two different widths — a rule that stopped at the content column
|
|
567
|
+
would read as a card, which is the one thing this theme has none of. */
|
|
568
|
+
[data-terminal] .term-composer-body {
|
|
569
|
+
margin-inline: auto;
|
|
570
|
+
width: 100%;
|
|
571
|
+
max-width: var(--wd-content-max-w, 48rem);
|
|
572
|
+
padding-inline: var(--term-bleed, 0px);
|
|
573
|
+
/* Air on each side of the prompt, and a plain pixel value rather than a
|
|
574
|
+
fraction of `--term-line`: this is chrome between two rules, not a
|
|
575
|
+
transcript row, and the whole-multiple rule governs rows — a computed
|
|
576
|
+
half-line would be exactly the fractional metric that rule exists to
|
|
577
|
+
forbid. */
|
|
578
|
+
padding-block: 8px;
|
|
579
|
+
}
|
|
580
|
+
[data-terminal].term-composer:focus-within,
|
|
581
|
+
[data-terminal].term-composer[data-dragging] {
|
|
582
|
+
border-color: var(--term-blue);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/* The contentEditable field. `font: inherit` is what makes it the cell — the
|
|
586
|
+
surface above already carries the size and the line height, and anything that
|
|
587
|
+
restates them here can disagree with the rows. */
|
|
588
|
+
[data-terminal] .term-composer-field {
|
|
589
|
+
font: inherit;
|
|
590
|
+
color: var(--term-fg);
|
|
591
|
+
outline: none;
|
|
592
|
+
/* The steady block caret, same as the prompt fields. See `.term-input`. */
|
|
593
|
+
caret-shape: block;
|
|
594
|
+
caret-animation: manual;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* Staged attachments, above the prompt.
|
|
598
|
+
*
|
|
599
|
+
* The card form wraps each thumbnail in a rounded box and hangs a round `✕`
|
|
600
|
+
* badge outside its top-right corner, over a strip with its own bottom rule.
|
|
601
|
+
* None of that survives here: this theme has no rounded corners and no badges
|
|
602
|
+
* floating off an edge, and the strip sits *inside* the composer's frame, which
|
|
603
|
+
* already draws the only rule there is — a second one directly above the field
|
|
604
|
+
* reads as the box this theme exists not to have.
|
|
605
|
+
*
|
|
606
|
+
* So the thumbnail is a plain 1px cell, and the remove control is tucked into
|
|
607
|
+
* its top-right corner with two rules of its own, which is how a terminal draws
|
|
608
|
+
* a control inside a frame rather than on top of one. `1px` rather than `0`
|
|
609
|
+
* puts it just inside the thumbnail's border instead of straddling it.
|
|
610
|
+
*
|
|
611
|
+
* The strip's only spacing is the 8px the composer already uses above and below
|
|
612
|
+
* its prompt — a plain pixel value, and deliberately, for the reason given
|
|
613
|
+
* there: this is chrome between rules, not a transcript row, so the
|
|
614
|
+
* whole-multiple-of-`--term-line` rule does not govern it. No side padding: the
|
|
615
|
+
* strip is already inside the body column, so padding here would step it in
|
|
616
|
+
* from the column every row above it starts on. */
|
|
617
|
+
[data-terminal] .term-attachments {
|
|
618
|
+
padding: 0 0 8px 0;
|
|
619
|
+
}
|
|
620
|
+
[data-terminal] .term-attachment {
|
|
621
|
+
border: 1px solid var(--term-faint);
|
|
622
|
+
}
|
|
623
|
+
/* A refused upload, in the terminal's own red. This needs to be said here
|
|
624
|
+
rather than left to the card form's `border-danger/50`: the rule above sets
|
|
625
|
+
the `border` shorthand, colour included, and outranks a utility class — so
|
|
626
|
+
without this the one state that must stand out is the one that looks
|
|
627
|
+
ordinary. */
|
|
628
|
+
[data-terminal] .term-attachment[data-failed] {
|
|
629
|
+
border-color: var(--term-red);
|
|
630
|
+
}
|
|
631
|
+
[data-terminal] .term-attachment-remove {
|
|
632
|
+
top: 1px;
|
|
633
|
+
right: 1px;
|
|
634
|
+
border-left: 1px solid var(--term-faint);
|
|
635
|
+
border-bottom: 1px solid var(--term-faint);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* A composer action as a character: one cell wide, one line tall, so pressing
|
|
639
|
+
it costs no width and removing it would move nothing. Dim until you reach for
|
|
640
|
+
it — the same restraint the row actions show. */
|
|
641
|
+
[data-terminal] .term-glyph {
|
|
642
|
+
flex: none;
|
|
643
|
+
width: var(--term-cell);
|
|
644
|
+
height: var(--term-line);
|
|
645
|
+
font: inherit;
|
|
646
|
+
line-height: var(--term-line);
|
|
647
|
+
text-align: center;
|
|
648
|
+
background: none;
|
|
649
|
+
border: 0;
|
|
650
|
+
padding: 0;
|
|
651
|
+
outline: none;
|
|
652
|
+
user-select: none;
|
|
653
|
+
cursor: pointer;
|
|
654
|
+
}
|
|
655
|
+
/* A glyph standing in the **gutter cell** is a marker, so it aligns like one.
|
|
656
|
+
The cell is 2ch and a character is 1ch, so `text-align: center` — right for a
|
|
657
|
+
standalone control at the row's trailing edge — lands the glyph half a cell
|
|
658
|
+
(≈3.9px at 13px/JetBrains Mono) to the right of the column every `❯` and `●`
|
|
659
|
+
in the transcript starts on. `start` is what `.term-gutter` uses, and on this
|
|
660
|
+
grid "nearly the same column" is just the wrong column. */
|
|
661
|
+
[data-terminal] .term-glyph[data-gutter] {
|
|
662
|
+
text-align: start;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/* `:not([data-tone])` because the tone rules and this one have the same
|
|
666
|
+
specificity and this one is further down the file: without it, a toned glyph
|
|
667
|
+
(the lit `↵`) would silently lose its colour to the default. */
|
|
668
|
+
[data-terminal] .term-glyph:not([data-tone]) {
|
|
669
|
+
color: var(--term-dim);
|
|
670
|
+
}
|
|
671
|
+
[data-terminal] .term-glyph:hover:not(:disabled),
|
|
672
|
+
[data-terminal] .term-glyph:focus-visible:not(:disabled) {
|
|
673
|
+
color: var(--term-bright);
|
|
674
|
+
}
|
|
675
|
+
[data-terminal] .term-glyph:disabled {
|
|
676
|
+
opacity: 0.4;
|
|
677
|
+
cursor: default;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/* ── Diff ────────────────────────────────────────────────────────────────────
|
|
681
|
+
*
|
|
682
|
+
* An added or removed line is a **band**, not coloured text: the wash runs the
|
|
683
|
+
* full width of the screen, the way it does in a terminal, so the shape of a
|
|
684
|
+
* change is readable from the scrollbar without reading a word of it. The line
|
|
685
|
+
* numbers ride inside the wash — they are part of the changed line.
|
|
686
|
+
*/
|
|
687
|
+
/* A diff belongs to the call above it, so it steps in one cell like any other
|
|
688
|
+
tool output. */
|
|
689
|
+
[data-terminal] .term-diff {
|
|
690
|
+
padding-left: var(--term-cell);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* The wash bleeds **right** only. Left it stops at the diff's own edge, because
|
|
694
|
+
that edge is the line-number column and a band running past it would swallow
|
|
695
|
+
the indent that says which tool call this diff belongs to. Right there is
|
|
696
|
+
nothing to preserve, and stopping at the longest line would make the shape of
|
|
697
|
+
the change depend on its text. */
|
|
698
|
+
[data-terminal] .term-diff .term-row[data-diff='add'],
|
|
699
|
+
[data-terminal] .term-diff .term-row[data-diff='remove'] {
|
|
700
|
+
margin-right: calc(-1 * var(--term-bleed, 0px));
|
|
701
|
+
padding-right: var(--term-bleed, 0px);
|
|
702
|
+
}
|
|
703
|
+
[data-terminal] .term-diff .term-row[data-diff='add'] {
|
|
704
|
+
background: var(--term-add-bg);
|
|
705
|
+
color: var(--term-add-fg);
|
|
706
|
+
}
|
|
707
|
+
[data-terminal] .term-diff .term-row[data-diff='remove'] {
|
|
708
|
+
background: var(--term-del-bg);
|
|
709
|
+
color: var(--term-del-fg);
|
|
710
|
+
}
|
|
711
|
+
[data-terminal] .term-diff .term-row[data-diff='context'] {
|
|
712
|
+
color: var(--term-diff-fg);
|
|
713
|
+
}
|
|
714
|
+
/* The number column is dimmer than the line it belongs to in every case — it is
|
|
715
|
+
an address, not content. `.term-gutter`'s own colour would flatten all three
|
|
716
|
+
to one grey, so each state dims its own. */
|
|
717
|
+
[data-terminal] .term-diff .term-row > .term-gutter {
|
|
718
|
+
color: var(--term-diff-num);
|
|
719
|
+
}
|
|
720
|
+
[data-terminal] .term-diff .term-row[data-diff='add'] > .term-gutter {
|
|
721
|
+
color: color-mix(in srgb, var(--term-add-fg) 65%, transparent);
|
|
722
|
+
}
|
|
723
|
+
[data-terminal] .term-diff .term-row[data-diff='remove'] > .term-gutter {
|
|
724
|
+
color: color-mix(in srgb, var(--term-del-fg) 65%, transparent);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* ── Markdown ────────────────────────────────────────────────────────────────
|
|
728
|
+
*
|
|
729
|
+
* The element map is in `components/terminal/markdown.tsx`; this is the little
|
|
730
|
+
* CSS it needs. Nothing here is an override — the renderer is told what to emit,
|
|
731
|
+
* so there is no default to undo and no `!important` anywhere in the theme.
|
|
732
|
+
*/
|
|
733
|
+
|
|
734
|
+
/* react-markdown emits the source's newlines as text nodes between blocks. The
|
|
735
|
+
surrounding rows are `pre-wrap` (a plain line of output must keep its own
|
|
736
|
+
spacing), which would render every one of those as a visible blank line — so
|
|
737
|
+
markdown bodies opt back out, and only a fenced block opts back in. */
|
|
738
|
+
.term-md {
|
|
739
|
+
white-space: normal;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* And it has to reach `.term-body` explicitly, because a list item *is* a Row
|
|
743
|
+
and Rows carry `pre-wrap` (a plain line of tool output must keep its own
|
|
744
|
+
spacing). Inherited, that turns every newline react-markdown emits between an
|
|
745
|
+
item's block children into a visible empty line — a nested list drew k+2
|
|
746
|
+
phantom blanks, so a two-line nested item was 72px of 18px rows. Models nest
|
|
747
|
+
lists constantly, and the grid audit cannot see it: the phantoms are whole
|
|
748
|
+
lines, so every row still lands on the grid. Fenced code opts back in for
|
|
749
|
+
itself (`.term-pre`), which is the only markdown that wants its own
|
|
750
|
+
whitespace. */
|
|
751
|
+
.term-md .term-body {
|
|
752
|
+
white-space: normal;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* The one vertical rule: consecutive blocks are one blank line apart. A margin
|
|
756
|
+
rather than an inserted row, because this row list belongs to the markdown
|
|
757
|
+
renderer — but it is exactly one line, so the grid holds. */
|
|
758
|
+
.term-md .term-block + .term-block {
|
|
759
|
+
margin-top: var(--term-line);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.term-md .term-list {
|
|
763
|
+
list-style: none;
|
|
764
|
+
margin: 0;
|
|
765
|
+
padding: 0;
|
|
766
|
+
}
|
|
767
|
+
.term-md .term-list-ordered {
|
|
768
|
+
counter-reset: term-ol;
|
|
769
|
+
}
|
|
770
|
+
/* Markers live in the row's gutter cell, so they cost columns rather than a
|
|
771
|
+
typographic indent: `- ` is two, `1. ` is three. */
|
|
772
|
+
.term-md .term-li > .term-gutter::before {
|
|
773
|
+
content: '-';
|
|
774
|
+
}
|
|
775
|
+
.term-md .term-list-ordered > .term-li {
|
|
776
|
+
--term-cell: 3ch;
|
|
777
|
+
counter-increment: term-ol;
|
|
778
|
+
}
|
|
779
|
+
.term-md .term-list-ordered > .term-li > .term-gutter::before {
|
|
780
|
+
content: counter(term-ol) '.';
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* A quote's rule is a background, not a border: a 1px border would push the
|
|
784
|
+
text 1px off the column it shares with every other line. */
|
|
785
|
+
.term-md .term-quote {
|
|
786
|
+
padding-left: var(--term-cell);
|
|
787
|
+
background: linear-gradient(var(--term-faint), var(--term-faint)) left / 1px 100% no-repeat;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* A horizontal rule is one line tall with the rule through its middle — the row
|
|
791
|
+
it occupies is a whole line either way. */
|
|
792
|
+
.term-md .term-rule {
|
|
793
|
+
height: var(--term-line);
|
|
794
|
+
background: linear-gradient(var(--term-faint), var(--term-faint)) left center / 100% 1px no-repeat;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.term-md .term-pre {
|
|
798
|
+
margin: 0;
|
|
799
|
+
font: inherit;
|
|
800
|
+
white-space: pre-wrap;
|
|
801
|
+
overflow-wrap: break-word;
|
|
802
|
+
color: var(--term-dim);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.term-md .term-inline-code {
|
|
806
|
+
font: inherit;
|
|
807
|
+
background: none;
|
|
808
|
+
padding: 0;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.term-md .term-em {
|
|
812
|
+
font-style: italic;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.term-md .term-table-wrap {
|
|
816
|
+
overflow-x: auto;
|
|
817
|
+
}
|
|
818
|
+
.term-md .term-table {
|
|
819
|
+
border-collapse: collapse;
|
|
820
|
+
/* `max-content` and **no** `max-width`. A cap of `100%` looks like the
|
|
821
|
+
defensive choice and is the opposite: it clamps a wide table below its
|
|
822
|
+
natural width, so the cells wrap and a 14-row table becomes 19 lines tall —
|
|
823
|
+
which breaks the theme's premise (a row is a line) and with it the height
|
|
824
|
+
calculator, whose table model counts rows. It also silently disables the
|
|
825
|
+
wrapper's own `overflow-x`, since a table that shrinks never overflows.
|
|
826
|
+
Letting it reach `max-content` puts every row back on one line and hands the
|
|
827
|
+
scrolling to `.term-table-wrap`, which exists for exactly that. */
|
|
828
|
+
width: max-content;
|
|
829
|
+
}
|
|
830
|
+
.term-md .term-table :is(th, td) {
|
|
831
|
+
/* Columns are cells, rows are lines. No vertical padding, ever. */
|
|
832
|
+
padding: 0 var(--term-cell) 0 0;
|
|
833
|
+
text-align: left;
|
|
834
|
+
vertical-align: top;
|
|
835
|
+
}
|
|
836
|
+
.term-md .term-thead {
|
|
837
|
+
/* The header rule, again as a shadow so it costs no layout. */
|
|
838
|
+
box-shadow: inset 0 -1px var(--term-diff-num);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/* ── Scrubber ────────────────────────────────────────────────────────────────
|
|
842
|
+
*
|
|
843
|
+
* The overview ruler (`components/terminal/scrubber.tsx`): a 12px rail of marks
|
|
844
|
+
* over the scroller's right edge — VS Code's strip beside the minimap, with
|
|
845
|
+
* this transcript's semantics. It is *chrome*, not rows: absolutely positioned
|
|
846
|
+
* over the scroll container so the transcript's grid is untouched, and drawn
|
|
847
|
+
* entirely from --term-* tokens so it follows the theme wherever the panel is
|
|
848
|
+
* embedded. Its width is the one deliberate exception to the theme's `ch` rule
|
|
849
|
+
* — see the note on `.term-scrubber` below.
|
|
850
|
+
*
|
|
851
|
+
* Interactivity is an affordance. Without `data-interactive` the rail is
|
|
852
|
+
* passive paint with pointer events off — and the native scrollbar, which
|
|
853
|
+
* stays in that mode, keeps working straight through it.
|
|
854
|
+
*/
|
|
855
|
+
[data-terminal].term-scrubber {
|
|
856
|
+
position: absolute;
|
|
857
|
+
top: 0;
|
|
858
|
+
right: 0;
|
|
859
|
+
bottom: 0;
|
|
860
|
+
/* Exactly 12px, and this is the one place the theme's `ch` rule is set aside
|
|
861
|
+
on purpose: the rail is chrome beside the grid rather than part of it, and
|
|
862
|
+
its width is a *hit target* (two 6px lanes) rather than a column of text.
|
|
863
|
+
A `ch` measure drifts with the font, which would make the lanes different
|
|
864
|
+
again in the VS Code dock. */
|
|
865
|
+
width: 12px;
|
|
866
|
+
z-index: 10;
|
|
867
|
+
pointer-events: none;
|
|
868
|
+
}
|
|
869
|
+
[data-terminal].term-scrubber[data-interactive] {
|
|
870
|
+
pointer-events: auto;
|
|
871
|
+
}
|
|
872
|
+
.term-scrubber .term-scrubber-body {
|
|
873
|
+
position: absolute;
|
|
874
|
+
inset: 0;
|
|
875
|
+
touch-action: none;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/* The viewport band — where you are: a faint wash with a hairline frame,
|
|
879
|
+
floated over the marks (z-index) so the framed region reads as a window
|
|
880
|
+
onto the rail rather than a stripe behind it. `--term-faint` at 25% rather
|
|
881
|
+
than a literal, so the light palette re-tints it. */
|
|
882
|
+
.term-scrubber .term-scrub-band {
|
|
883
|
+
position: absolute;
|
|
884
|
+
left: 0;
|
|
885
|
+
right: 0;
|
|
886
|
+
background: color-mix(in srgb, var(--term-faint) 25%, transparent);
|
|
887
|
+
box-shadow: 0 0 0 1px var(--term-faint) inset;
|
|
888
|
+
pointer-events: none;
|
|
889
|
+
z-index: 1;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/* There is no separate cursor line. The band above answers "where am I" on its
|
|
893
|
+
own — it is outlined, so its top edge already reads as an edge — and a 2px
|
|
894
|
+
blue rule laid over it was a second indicator of the same fact, in the
|
|
895
|
+
loudest colour the rail has. */
|
|
896
|
+
|
|
897
|
+
.term-scrubber .term-scrub-mark {
|
|
898
|
+
position: absolute;
|
|
899
|
+
min-height: 2px;
|
|
900
|
+
}
|
|
901
|
+
.term-scrubber[data-interactive] .term-scrub-mark {
|
|
902
|
+
cursor: pointer;
|
|
903
|
+
}
|
|
904
|
+
.term-scrubber[data-interactive] .term-scrub-mark:hover {
|
|
905
|
+
outline: 1px solid color-mix(in srgb, var(--term-bright) 40%, transparent);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/* Two lanes of 8px — what you asked, and what came back — and 'f', the
|
|
909
|
+
full-width lane for annotations *about* the run (an error, an approval, a
|
|
910
|
+
bookmark, the catch-up seam) rather than steps through it. Pixels, not
|
|
911
|
+
percentages: the whole point of the 16px rail is that a lane is a reliable
|
|
912
|
+
8px target, and a percentage would reintroduce the drift the fixed width was
|
|
913
|
+
chosen to remove. */
|
|
914
|
+
.term-scrubber .term-scrub-mark[data-lane='l'] { left: 0; width: 6px; }
|
|
915
|
+
.term-scrubber .term-scrub-mark[data-lane='r'] { left: 6px; width: 6px; }
|
|
916
|
+
.term-scrubber .term-scrub-mark[data-lane='f'] { left: 0; width: 12px; }
|
|
917
|
+
|
|
918
|
+
/* Lane marks are two-tone: the first 2px — the mark's own anchor — at full
|
|
919
|
+
strength, the rest of the row's extent a 25% tail, so a long answer reads
|
|
920
|
+
as long without a tall solid bar shouting over the rail. A minimum-height
|
|
921
|
+
mark shows only the solid segment, so short and long marks need no separate
|
|
922
|
+
rules; the *lane* stays the pointer's target, so a 2px mark is still
|
|
923
|
+
findable. Full-width annotations (error, approval) stay solid: they are
|
|
924
|
+
alarms, not extents. */
|
|
925
|
+
.term-scrubber .term-scrub-mark[data-kind='user'] {
|
|
926
|
+
background: linear-gradient(
|
|
927
|
+
to bottom,
|
|
928
|
+
var(--term-blue) 0 2px,
|
|
929
|
+
color-mix(in srgb, var(--term-blue) 25%, transparent) 2px
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
.term-scrubber .term-scrub-mark[data-kind='turn'] {
|
|
933
|
+
background: linear-gradient(
|
|
934
|
+
to bottom,
|
|
935
|
+
var(--term-fg) 0 2px,
|
|
936
|
+
color-mix(in srgb, var(--term-fg) 25%, transparent) 2px
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
.term-scrubber .term-scrub-mark[data-kind='turnFailed'] {
|
|
940
|
+
background: linear-gradient(
|
|
941
|
+
to bottom,
|
|
942
|
+
var(--term-red) 0 2px,
|
|
943
|
+
color-mix(in srgb, var(--term-red) 25%, transparent) 2px
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
.term-scrubber .term-scrub-mark[data-kind='error'] { background: var(--term-red); }
|
|
947
|
+
/* A failed tool call is an alarm, so it is full-width and solid like the rest of
|
|
948
|
+
them — but at 55%, which is the one thing keeping the rail readable. A session
|
|
949
|
+
error is rare and a turn failure rarer; a tool that failed and was recovered
|
|
950
|
+
from is routine (a grep that matched nothing, a build fixed on the second go),
|
|
951
|
+
and at full strength a normal working session paints the rail solid red and
|
|
952
|
+
the two errors that actually ended something stop standing out. Dimmer reads
|
|
953
|
+
as the same class of thing, one rank quieter — which is exactly its rank in
|
|
954
|
+
LOUDNESS. */
|
|
955
|
+
.term-scrubber .term-scrub-mark[data-kind='toolFailed'] {
|
|
956
|
+
background: color-mix(in srgb, var(--term-red) 55%, transparent);
|
|
957
|
+
}
|
|
958
|
+
.term-scrubber .term-scrub-mark[data-kind='bookmark'] { background: var(--term-magenta); }
|
|
959
|
+
.term-scrubber .term-scrub-mark[data-kind='approval'] {
|
|
960
|
+
background: var(--term-yellow);
|
|
961
|
+
animation: term-scrub-pulse 1.2s infinite;
|
|
962
|
+
}
|
|
963
|
+
/* The catch-up boundary: a dashed hairline centered in its 3px hit box. */
|
|
964
|
+
.term-scrubber .term-scrub-mark[data-kind='recap'] {
|
|
965
|
+
background: repeating-linear-gradient(to right, var(--term-faint) 0 3px, transparent 3px 6px)
|
|
966
|
+
left center / 100% 1px no-repeat;
|
|
967
|
+
}
|
|
968
|
+
@keyframes term-scrub-pulse {
|
|
969
|
+
50% { opacity: 0.3; }
|
|
970
|
+
}
|
|
971
|
+
@media (prefers-reduced-motion: reduce) {
|
|
972
|
+
.term-scrubber .term-scrub-mark[data-kind='approval'] {
|
|
973
|
+
animation: none;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/* The peek. Rendered from transcript state — the row it describes is usually
|
|
978
|
+
unmounted — and framed the theme's way: strokes as backgrounds, no border.
|
|
979
|
+
pointer-events: none, so it can never trap the pointer that revealed it. */
|
|
980
|
+
.term-scrubber .term-scrub-peek {
|
|
981
|
+
position: absolute;
|
|
982
|
+
right: calc(100% + 1ch);
|
|
983
|
+
width: 44ch;
|
|
984
|
+
max-width: 60vw;
|
|
985
|
+
padding: var(--term-line) 2ch;
|
|
986
|
+
background:
|
|
987
|
+
linear-gradient(var(--term-faint), var(--term-faint)) top left / 100% 1px no-repeat,
|
|
988
|
+
linear-gradient(var(--term-faint), var(--term-faint)) bottom left / 100% 1px no-repeat,
|
|
989
|
+
linear-gradient(var(--term-faint), var(--term-faint)) top left / 1px 100% no-repeat,
|
|
990
|
+
linear-gradient(var(--term-faint), var(--term-faint)) top right / 1px 100% no-repeat,
|
|
991
|
+
var(--bg-elevated, #1f1f1f);
|
|
992
|
+
pointer-events: none;
|
|
993
|
+
z-index: 11;
|
|
994
|
+
}
|
|
995
|
+
.term-scrubber .term-scrub-ex {
|
|
996
|
+
display: -webkit-box;
|
|
997
|
+
-webkit-line-clamp: 4;
|
|
998
|
+
-webkit-box-orient: vertical;
|
|
999
|
+
overflow: hidden;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/* While an interactive scrubber is mounted it IS the scrollbar; the transcript
|
|
1003
|
+
stamps this attribute on the scroll element. A passive rail leaves the
|
|
1004
|
+
native scrollbar alone — pointer-events: none above is what keeps that
|
|
1005
|
+
scrollbar reachable through the paint. */
|
|
1006
|
+
[data-term-scrubber-host] {
|
|
1007
|
+
scrollbar-width: none;
|
|
1008
|
+
}
|
|
1009
|
+
[data-term-scrubber-host]::-webkit-scrollbar {
|
|
1010
|
+
display: none;
|
|
1011
|
+
}
|