@workerdeck/ui 0.16.0 → 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 +7 -0
- package/build/{SessionPanel-B9CHoq8x.d.mts → SessionPanel-CnU_IJ3-.d.mts} +36 -9
- package/build/{SessionPanel-DII9MmQ8.mjs → SessionPanel-DPx8Iz8a.mjs} +1432 -384
- package/build/SessionPanel-DPx8Iz8a.mjs.map +1 -0
- package/build/{format-DfI_je9S.d.mts → format-ljc3lKpA.d.mts} +1 -1
- package/build/format.d.mts +16 -5
- package/build/format.mjs +2 -3
- package/build/index.d.mts +288 -8
- package/build/index.mjs +620 -165
- package/build/index.mjs.map +1 -1
- package/build/{format-DqR56Y8l.mjs → status-BE-zg88x.mjs} +154 -2
- package/build/status-BE-zg88x.mjs.map +1 -0
- package/build/workspace.d.mts +4 -1
- package/build/workspace.mjs +4 -3
- package/build/workspace.mjs.map +1 -1
- package/package.json +8 -6
- package/src/components/agent/ContextRing.tsx +41 -0
- package/src/components/agent/EngineIcon.tsx +40 -0
- package/src/components/agent/ProjectIcon.tsx +119 -0
- package/src/components/agent/SessionBrowser.tsx +191 -17
- package/src/components/agent/SessionPanel.tsx +177 -2
- package/src/components/agent/SessionSteps.tsx +233 -0
- package/src/components/agent/SessionWorkspace.tsx +4 -0
- package/src/components/agent/StatusBar.tsx +4 -2
- package/src/components/agent/SubagentStrip.tsx +134 -0
- package/src/components/agent/ToolCallCard.tsx +72 -5
- package/src/components/agent/Transcript.tsx +212 -23
- package/src/components/agent/tool-result-fetch.tsx +36 -0
- package/src/components/agent/tool-result-image.tsx +209 -0
- package/src/components/agent/transcript-rows.ts +122 -23
- package/src/components/terminal/TerminalTranscript.tsx +154 -2
- package/src/components/terminal/affordances.tsx +34 -0
- package/src/components/terminal/blocks.ts +260 -0
- package/src/components/terminal/height.ts +73 -6
- package/src/components/terminal/image-box.ts +53 -0
- package/src/components/terminal/items.tsx +116 -79
- package/src/components/terminal/result-preview.ts +20 -6
- package/src/components/terminal/scrubber.tsx +172 -26
- package/src/components/terminal/tool-run.ts +177 -0
- package/src/index.ts +20 -1
- package/src/lib/status.ts +16 -3
- package/src/styles/terminal.css +85 -5
- package/src/styles/theme.css +42 -0
- package/build/SessionPanel-DII9MmQ8.mjs.map +0 -1
- package/build/format-DqR56Y8l.mjs.map +0 -1
- package/build/status-Ydzi7n6j.mjs +0 -143
- package/build/status-Ydzi7n6j.mjs.map +0 -1
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `result-preview.ts` exists. Two spellings would be two different heights.
|
|
18
18
|
*/
|
|
19
19
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
20
|
+
import { toolInputPreview } from '../../lib/format.ts'
|
|
20
21
|
import { isShellTool } from '../../lib/tool-icon.ts'
|
|
21
22
|
|
|
22
23
|
type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
|
|
@@ -89,3 +90,179 @@ export function runSummary(items: readonly ToolCallItem[], busy: boolean): strin
|
|
|
89
90
|
// read, 1 shell" reads as though the sentence ended and then carried on.
|
|
90
91
|
return `${verb}${n} tool${n === 1 ? '' : 's'} · ${breakdown}${tail}`
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
/* ── The task block's one line ─────────────────────────────────────────────
|
|
95
|
+
*
|
|
96
|
+
* A `Task` call and everything its subagent produced collapse to one row (see
|
|
97
|
+
* `blocks.ts`), and these are that row's words. Same contract as `runSummary`:
|
|
98
|
+
* `height.ts` wraps these exact strings to predict the row's pixel height with
|
|
99
|
+
* no DOM, so the component must render them verbatim — two spellings would be
|
|
100
|
+
* two different heights.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
const clip = (text: string, max = 80): string =>
|
|
104
|
+
text.length > max ? text.slice(0, max - 1) + '…' : text
|
|
105
|
+
|
|
106
|
+
const trimmed = (value: unknown): string | undefined =>
|
|
107
|
+
typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The row's identity half: which task this is.
|
|
111
|
+
*
|
|
112
|
+
* The Claude SDK's `Task` input carries `subagent_type` (e.g. "Explore") and a
|
|
113
|
+
* 3–5 word `description`, and both are worth the line: parallel tasks are the
|
|
114
|
+
* whole reason the block exists, and two rows both reading `Task(…)` answer
|
|
115
|
+
* nothing. `Task(Explore · find the auth check)` — falling back to the
|
|
116
|
+
* ordinary input preview when an engine sends neither, so the header is never
|
|
117
|
+
* emptier than a plain tool row's.
|
|
118
|
+
*/
|
|
119
|
+
export function taskLabel(task: ToolCallItem): string {
|
|
120
|
+
return `${task.name}(${taskIdentity(task)})`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The inner half of {@link taskLabel} — `Explore · find the auth check` — without
|
|
125
|
+
* the `Task(…)` wrapper.
|
|
126
|
+
*
|
|
127
|
+
* Split out for the sub-agent takeover's header, which names the agent it is
|
|
128
|
+
* showing and has no room (or reason) to repeat the tool's own name: the whole
|
|
129
|
+
* surface *is* that Task. Extracted rather than re-spelled so the header and the
|
|
130
|
+
* row it was opened from cannot drift, and so both keep matching protocol's
|
|
131
|
+
* `subagentLabel`, which reads the same two input fields for the sessions list.
|
|
132
|
+
*/
|
|
133
|
+
export function taskIdentity(task: ToolCallItem): string {
|
|
134
|
+
const input = task.input as { description?: unknown; subagent_type?: unknown } | null
|
|
135
|
+
const description = trimmed(input?.description)
|
|
136
|
+
const agent = trimmed(input?.subagent_type)
|
|
137
|
+
return agent && description
|
|
138
|
+
? `${agent} · ${clip(description)}`
|
|
139
|
+
: (agent ?? (description ? clip(description) : toolInputPreview(task.input)))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* **What this agent was actually asked** — the sub-agent's brief, or undefined
|
|
144
|
+
* when the engine did not give us one.
|
|
145
|
+
*
|
|
146
|
+
* Measured against a live session rather than assumed: the Agent SDK's call
|
|
147
|
+
* carries `{description, subagent_type, run_in_background, prompt}`, and
|
|
148
|
+
* `prompt` is the instruction.
|
|
149
|
+
*
|
|
150
|
+
* **Whether it also arrives in the stream depends on the agent.** A foreground
|
|
151
|
+
* `Task` forwards its brief as a real nested `user_message` (that is what
|
|
152
|
+
* `forwardSubagentText` buys and what the reducer stamps a `parentToolUseId`
|
|
153
|
+
* on), so `subagentItems` picks it up and the frame already has it. A
|
|
154
|
+
* **background** agent does not: measured on a session with eight of them,
|
|
155
|
+
* zero items carried a `parentToolUseId` on a `user` kind. Those runs are
|
|
156
|
+
* exactly the ones you open a takeover on — an agent working while you read
|
|
157
|
+
* something else — and their brief was nowhere at all.
|
|
158
|
+
*
|
|
159
|
+
* So this is the *fallback*, not the source: the callers splice it in only when
|
|
160
|
+
* the frame carries no brief of its own, or the same instruction would be drawn
|
|
161
|
+
* twice.
|
|
162
|
+
*
|
|
163
|
+
* `description` is deliberately not a fallback: it is the 3–5 word label the
|
|
164
|
+
* header already prints, and repeating it as a brief would claim we know the
|
|
165
|
+
* instruction when we do not. **Codex genuinely has none** — its `spawn_agent`
|
|
166
|
+
* message is an encrypted blob on the wire — so on that engine this is
|
|
167
|
+
* undefined and the row is not drawn, rather than drawn empty.
|
|
168
|
+
*/
|
|
169
|
+
export function taskBrief(task: ToolCallItem): string | undefined {
|
|
170
|
+
const input = task.input as { prompt?: unknown } | null
|
|
171
|
+
return trimmed(input?.prompt)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const callBusy = (call: ToolCallItem): boolean =>
|
|
175
|
+
call.status === 'running' || call.status === 'pending'
|
|
176
|
+
|
|
177
|
+
/** Did this one call fail? Both spellings are needed: an out-of-loop execution
|
|
178
|
+
* failure sets `status` with no `is_error` block to read, and an engine can flag
|
|
179
|
+
* `is_error` on a call the reducer has not settled yet. */
|
|
180
|
+
export const callFailed = (call: ToolCallItem): boolean =>
|
|
181
|
+
call.status === 'failed' || call.result?.isError === true
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Does a folded run colour red? **Only when its last call failed.**
|
|
185
|
+
*
|
|
186
|
+
* It used to be `some`, on the argument that a failure should colour the block
|
|
187
|
+
* rather than fragment it. The argument was right about not fragmenting and
|
|
188
|
+
* wrong about `some`: a run is a sequence the model worked through, and a
|
|
189
|
+
* failure it recovered from two calls later is how work goes — a grep that
|
|
190
|
+
* matched nothing, a build fixed on the second go. Reddening the whole run for
|
|
191
|
+
* it means a normal working session is painted red, which spends the colour
|
|
192
|
+
* that should have been left for the one thing still broken.
|
|
193
|
+
*
|
|
194
|
+
* The last call is the run's *outcome*, and an outcome is what a collapsed row
|
|
195
|
+
* can honestly claim. The failures inside it are not hidden — they are one
|
|
196
|
+
* press away, each red on its own row, and the recap counts every one. The
|
|
197
|
+
* **scrubber agrees with this rule** rather than overriding it: it marks a
|
|
198
|
+
* failed call only when the call is its row's outcome, which for a run is
|
|
199
|
+
* exactly this one. It used to mark every member on the argument that the
|
|
200
|
+
* rail asks a different question; against a real session that was nine alarms
|
|
201
|
+
* on the rail for a transcript reddening one row.
|
|
202
|
+
*/
|
|
203
|
+
export function runFailed(items: readonly ToolCallItem[]): boolean {
|
|
204
|
+
const last = items[items.length - 1]
|
|
205
|
+
return last !== undefined && callFailed(last)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Is anything inside still going? The call itself, normally — the Task
|
|
209
|
+
* settles only when its subagent finishes — but a bridged or deferred child
|
|
210
|
+
* can outlive it, and a pulse that stopped while a child still worked would
|
|
211
|
+
* read as a hang. */
|
|
212
|
+
export function taskBusy(task: ToolCallItem, children: readonly TranscriptItem[]): boolean {
|
|
213
|
+
return callBusy(task) || children.some((child) => child.kind === 'tool_call' && callBusy(child))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Does the row colour red? **The task's own outcome, and nothing else.**
|
|
218
|
+
*
|
|
219
|
+
* It used to be "or any child call's", which does not survive contact with a
|
|
220
|
+
* real subagent: an agent that ran a hundred calls, one of them a grep that
|
|
221
|
+
* matched nothing, came back with a red line saying it had failed. It had not —
|
|
222
|
+
* it had done exactly what it was asked, and the transcript said otherwise in
|
|
223
|
+
* the one colour reserved for things that need a human.
|
|
224
|
+
*
|
|
225
|
+
* This is the call `SubagentInfo.status` already makes, and it made it for this
|
|
226
|
+
* reason (see `packages/protocol`): the sub-agent's **own** `tool_result`
|
|
227
|
+
* `is_error`, deliberately not `taskFailed`. The argument there was that a
|
|
228
|
+
* nothing-matched grep must not read as a failed run *beside a session name*;
|
|
229
|
+
* what a hundred-call agent shows is that it must not read that way beside the
|
|
230
|
+
* `Task` row either. Two surfaces, one rule, one spelling.
|
|
231
|
+
*
|
|
232
|
+
* Nothing is concealed by this. A failed child is red on its own row, one press
|
|
233
|
+
* away, and the recap counts it. The **scrubber follows this rule too** and no
|
|
234
|
+
* longer marks such a child: a red tick on the rail says precisely what this
|
|
235
|
+
* row is forbidden from saying. The sub-agent band still says an agent ran
|
|
236
|
+
* here, and the task's own red tick still says it came back broken — which is
|
|
237
|
+
* what the two channels are for.
|
|
238
|
+
*/
|
|
239
|
+
export function taskFailed(task: ToolCallItem): boolean {
|
|
240
|
+
return callFailed(task)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The collapsed task row's one line: identity, then scale.
|
|
245
|
+
*
|
|
246
|
+
* `Task(Explore · find the auth check) · 7 tools…` while the subagent works —
|
|
247
|
+
* the count grows as it does, which is the row's progress reading, and the
|
|
248
|
+
* trailing ellipsis is the same in-flight signal `runSummary` uses (the pulse
|
|
249
|
+
* in the gutter carries the beat). Settled, the ellipsis drops:
|
|
250
|
+
* `… · 7 tools`. "Tools" and not "tool calls" because `runSummary` already
|
|
251
|
+
* chose that word for the same count one row over.
|
|
252
|
+
*
|
|
253
|
+
* The counts are counted from the absorbed children, never read from the
|
|
254
|
+
* engine's structured Task output — WorkerDeck does not plumb structured tool
|
|
255
|
+
* results to clients, so a transcript replayed tomorrow must spell the same
|
|
256
|
+
* line from the same items it holds today.
|
|
257
|
+
*
|
|
258
|
+
* With no tool calls yet — the subagent thinking, or only its brief arrived —
|
|
259
|
+
* the line says `working…`, because `0 tools…` reads as a stall; settled with
|
|
260
|
+
* none it says `done`.
|
|
261
|
+
*/
|
|
262
|
+
export function taskSummary(task: ToolCallItem, children: readonly TranscriptItem[]): string {
|
|
263
|
+
const busy = taskBusy(task, children)
|
|
264
|
+
const calls = children.reduce((n, child) => n + (child.kind === 'tool_call' ? 1 : 0), 0)
|
|
265
|
+
const label = taskLabel(task)
|
|
266
|
+
if (calls === 0) return busy ? `${label} · working…` : `${label} · done`
|
|
267
|
+
return `${label} · ${calls} tool${calls === 1 ? '' : 's'}${busy ? '…' : ''}`
|
|
268
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -180,13 +180,32 @@ export {
|
|
|
180
180
|
export { SessionStatusIcon } from './components/agent/SessionBrowser.tsx'
|
|
181
181
|
// Lifted out of the VS Code sidebar once the dashboard grew a collapsed rail
|
|
182
182
|
// that needs the same glyph — two copies of a trademark set is one too many.
|
|
183
|
-
export {
|
|
183
|
+
export {
|
|
184
|
+
EngineIcon,
|
|
185
|
+
engineMark,
|
|
186
|
+
vendorMarkClass,
|
|
187
|
+
vendorTextClass,
|
|
188
|
+
} from './components/agent/EngineIcon.tsx'
|
|
189
|
+
// Lifted out of the sidebar for the same reason: the work under a row is a
|
|
190
|
+
// protocol fact, so every list annotates it with one component.
|
|
191
|
+
export {
|
|
192
|
+
type Step,
|
|
193
|
+
StepRow,
|
|
194
|
+
StepToggle,
|
|
195
|
+
runningSteps,
|
|
196
|
+
sessionSteps,
|
|
197
|
+
} from './components/agent/SessionSteps.tsx'
|
|
198
|
+
// The sub-agent takeover's one line. Exported for hosts that draw their own
|
|
199
|
+
// panel chrome; the panel raises it itself.
|
|
200
|
+
export { SubagentStrip } from './components/agent/SubagentStrip.tsx'
|
|
201
|
+
export { ProjectIcon } from './components/agent/ProjectIcon.tsx'
|
|
184
202
|
export {
|
|
185
203
|
SessionEmptyState,
|
|
186
204
|
type SessionEmptyStateProps,
|
|
187
205
|
} from './components/agent/SessionEmptyState.tsx'
|
|
188
206
|
export { PromptTokenText } from './components/agent/PromptTokenText.tsx'
|
|
189
207
|
export { STATUS_META } from './components/agent/status.ts'
|
|
208
|
+
export { ContextRing } from './components/agent/ContextRing.tsx'
|
|
190
209
|
|
|
191
210
|
// Utilities
|
|
192
211
|
export { cn } from './lib/utils.ts'
|
package/src/lib/status.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModelOption, RateLimitInfo, SessionStatus } from '@workerdeck/protocol'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* How a session's live readings become a status line — the pure half, so every
|
|
@@ -174,7 +174,20 @@ export function modelLabel(vitals: ModelReadings | undefined): string {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/** Context percentage as its meter severity — the reading and the colour come
|
|
177
|
-
* from one place so a panel and a status bar never disagree.
|
|
178
|
-
|
|
177
|
+
* from one place so a panel and a status bar never disagree.
|
|
178
|
+
*
|
|
179
|
+
* Takes only the number it reads, so the compact `ContextReading` that rides
|
|
180
|
+
* the sessions list and the full `ContextUsage` on the event stream are coloured
|
|
181
|
+
* by one rule rather than two. */
|
|
182
|
+
export function contextSeverity(usage: { percentage: number } | undefined): StatusSeverity {
|
|
179
183
|
return meterSeverity(usage?.percentage)
|
|
180
184
|
}
|
|
185
|
+
|
|
186
|
+
/** {@link meterSeverity} as a text colour class. Here rather than beside each
|
|
187
|
+
* meter because the status bar, the context dialog and a sessions-list row all
|
|
188
|
+
* paint the same reading, and a fourth copy of the thresholds is a fourth
|
|
189
|
+
* chance for one surface to call 81% orange and another call it grey. */
|
|
190
|
+
export function meterColorClass(pct: number | undefined): string {
|
|
191
|
+
const severity = meterSeverity(pct)
|
|
192
|
+
return severity === 'error' ? 'text-danger' : severity === 'warning' ? 'text-warning' : 'text-fg-3'
|
|
193
|
+
}
|
package/src/styles/terminal.css
CHANGED
|
@@ -99,6 +99,13 @@
|
|
|
99
99
|
/* The user's own prompt row. */
|
|
100
100
|
--term-user-bg: rgb(255 255 255 / 0.05);
|
|
101
101
|
--term-row-hover: rgb(255 255 255 / 0.05);
|
|
102
|
+
/* Behind an OPEN block. Yellow, not neutral, and the theme's one deliberate
|
|
103
|
+
reuse of that tone for something other than "waiting on you": an open block
|
|
104
|
+
is a state the reader put the transcript into, and on the phone it wants
|
|
105
|
+
the same colour as the rail mark that says so. Kept very low — this washes
|
|
106
|
+
whole regions, and at band strength an opened run would shout louder than
|
|
107
|
+
anything inside it. */
|
|
108
|
+
--term-open-wash: rgb(215 186 125 / 0.1);
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
[data-theme='light'] [data-terminal],
|
|
@@ -125,6 +132,7 @@
|
|
|
125
132
|
--term-band-bg: rgb(0 0 0 / 0.04);
|
|
126
133
|
--term-user-bg: rgb(0 0 0 / 0.05);
|
|
127
134
|
--term-row-hover: rgb(0 0 0 / 0.04);
|
|
135
|
+
--term-open-wash: rgb(215 186 125 / 0.25);
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
/* ── Rows ────────────────────────────────────────────────────────────────────
|
|
@@ -284,11 +292,30 @@
|
|
|
284
292
|
stack without a step. (The other half is `useRevealOnOpen`, which brings that
|
|
285
293
|
first line back when it has gone above the fold.) */
|
|
286
294
|
[data-terminal] .term-open {
|
|
287
|
-
background: var(--term-
|
|
295
|
+
background: var(--term-open-wash);
|
|
288
296
|
margin-inline: calc(-1 * var(--term-bleed, 0px));
|
|
289
297
|
padding-inline: var(--term-bleed, 0px);
|
|
290
298
|
}
|
|
291
299
|
|
|
300
|
+
/* Another frame of reference: the rows a subagent produced, inside the `Task`
|
|
301
|
+
that spawned them.
|
|
302
|
+
|
|
303
|
+
Two cells of indent, whole — the theme's rule is that horizontal measures are
|
|
304
|
+
`ch`, and these rows carry markers in a gutter that has to keep landing on the
|
|
305
|
+
grid the rows above it use. The rule itself is drawn *inside* that padding
|
|
306
|
+
with an inset shadow rather than as a border, which is what makes the 2ch
|
|
307
|
+
exact: a 1px border is layout, and would put every nested glyph one pixel off
|
|
308
|
+
the column for the sake of chrome.
|
|
309
|
+
|
|
310
|
+
`--term-faint` rather than the cards `--border` token, and that is the whole
|
|
311
|
+
reason this class exists: the nested block sits on the *open* block's wash,
|
|
312
|
+
and a border colour tuned for the base ground resolved to rgb(31,31,31) on it
|
|
313
|
+
— a rule nobody could see. The same trap `row-hover` is alpha to avoid. */
|
|
314
|
+
[data-terminal] .term-nested {
|
|
315
|
+
padding-left: 2ch;
|
|
316
|
+
box-shadow: inset 1px 0 0 var(--term-faint);
|
|
317
|
+
}
|
|
318
|
+
|
|
292
319
|
.term-hoverable {
|
|
293
320
|
position: relative;
|
|
294
321
|
}
|
|
@@ -920,8 +947,8 @@
|
|
|
920
947
|
as long without a tall solid bar shouting over the rail. A minimum-height
|
|
921
948
|
mark shows only the solid segment, so short and long marks need no separate
|
|
922
949
|
rules; the *lane* stays the pointer's target, so a 2px mark is still
|
|
923
|
-
findable.
|
|
924
|
-
|
|
950
|
+
findable. The alarms (error, approval) stay solid: they are alarms, not
|
|
951
|
+
extents. */
|
|
925
952
|
.term-scrubber .term-scrub-mark[data-kind='user'] {
|
|
926
953
|
background: linear-gradient(
|
|
927
954
|
to bottom,
|
|
@@ -929,6 +956,19 @@
|
|
|
929
956
|
color-mix(in srgb, var(--term-blue) 25%, transparent) 2px
|
|
930
957
|
);
|
|
931
958
|
}
|
|
959
|
+
/* A sub-agent's stretch of the transcript, in the input lane: green, because
|
|
960
|
+
every other colour on this rail is already spoken for and none of them means
|
|
961
|
+
"somebody else's working" — blue is you, white is the answer, red is an
|
|
962
|
+
alarm, magenta is your bookmark, yellow is the session waiting on you. Drawn
|
|
963
|
+
as an extent (2px head, 25% tail) like the two marks it shares its geometry
|
|
964
|
+
with: collapsed it is a tick, expanded it is the band the sub-agent covers. */
|
|
965
|
+
.term-scrubber .term-scrub-mark[data-kind='subagent'] {
|
|
966
|
+
background: linear-gradient(
|
|
967
|
+
to bottom,
|
|
968
|
+
var(--term-green) 0 2px,
|
|
969
|
+
color-mix(in srgb, var(--term-green) 25%, transparent) 2px
|
|
970
|
+
);
|
|
971
|
+
}
|
|
932
972
|
.term-scrubber .term-scrub-mark[data-kind='turn'] {
|
|
933
973
|
background: linear-gradient(
|
|
934
974
|
to bottom,
|
|
@@ -944,8 +984,8 @@
|
|
|
944
984
|
);
|
|
945
985
|
}
|
|
946
986
|
.term-scrubber .term-scrub-mark[data-kind='error'] { background: var(--term-red); }
|
|
947
|
-
/* A failed tool call is an alarm, so it is
|
|
948
|
-
|
|
987
|
+
/* A failed tool call is an alarm, so it is solid like the rest of them — in the
|
|
988
|
+
response lane, since it is something the run produced — but at 55%, which is the one thing keeping the rail readable. A session
|
|
949
989
|
error is rare and a turn failure rarer; a tool that failed and was recovered
|
|
950
990
|
from is routine (a grep that matched nothing, a build fixed on the second go),
|
|
951
991
|
and at full strength a normal working session paints the rail solid red and
|
|
@@ -992,6 +1032,16 @@
|
|
|
992
1032
|
pointer-events: none;
|
|
993
1033
|
z-index: 11;
|
|
994
1034
|
}
|
|
1035
|
+
/* The sub-agent's brief, collapsed: clipped on the same wrapped lines
|
|
1036
|
+
`briefPx` counts, so the height the virtualizer reserved and the height the
|
|
1037
|
+
row draws cannot disagree. The count itself comes from the component
|
|
1038
|
+
(`BRIEF_LINES`), one number rather than one here and one there. */
|
|
1039
|
+
.term-brief-clip {
|
|
1040
|
+
display: -webkit-box;
|
|
1041
|
+
-webkit-box-orient: vertical;
|
|
1042
|
+
overflow: hidden;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
995
1045
|
.term-scrubber .term-scrub-ex {
|
|
996
1046
|
display: -webkit-box;
|
|
997
1047
|
-webkit-line-clamp: 4;
|
|
@@ -1009,3 +1059,33 @@
|
|
|
1009
1059
|
[data-term-scrubber-host]::-webkit-scrollbar {
|
|
1010
1060
|
display: none;
|
|
1011
1061
|
}
|
|
1062
|
+
|
|
1063
|
+
/* ── Images ──────────────────────────────────────────────────────────────────
|
|
1064
|
+
*
|
|
1065
|
+
* A picture a tool returned, drawn in a box of whole lines (`IMAGE_BOX_LINES`,
|
|
1066
|
+
* set inline by `TerminalImage` so the constant has one spelling across the
|
|
1067
|
+
* renderer and the height calculator). The height is on the element and nothing
|
|
1068
|
+
* here may add to it: no margin, no border, no padding — a box that is not
|
|
1069
|
+
* exactly N lines is a row the virtualizer was told the wrong size for.
|
|
1070
|
+
*
|
|
1071
|
+
* `object-fit: contain` with a top-left origin rather than a centred one: the
|
|
1072
|
+
* grid's origin is the top-left of the body cell, and a screenshot floating in
|
|
1073
|
+
* the middle of its box reads as a figure in a document instead of output on a
|
|
1074
|
+
* line. */
|
|
1075
|
+
.term-image {
|
|
1076
|
+
display: block;
|
|
1077
|
+
overflow: hidden;
|
|
1078
|
+
}
|
|
1079
|
+
.term-image > img {
|
|
1080
|
+
max-width: 100%;
|
|
1081
|
+
height: 100%;
|
|
1082
|
+
object-fit: contain;
|
|
1083
|
+
object-position: left top;
|
|
1084
|
+
}
|
|
1085
|
+
/* The wash is the placeholder's whole visual: it says "something is reserved
|
|
1086
|
+
here" for as long as the bytes are in flight, and it says it at the same
|
|
1087
|
+
height the picture will occupy. */
|
|
1088
|
+
.term-image[data-state='pending'],
|
|
1089
|
+
.term-image[data-state='failed'] {
|
|
1090
|
+
background: var(--term-band-bg);
|
|
1091
|
+
}
|
package/src/styles/theme.css
CHANGED
|
@@ -51,6 +51,37 @@
|
|
|
51
51
|
--fg-4: #a1a1a1;
|
|
52
52
|
--fg-disabled: #c9c9c9;
|
|
53
53
|
|
|
54
|
+
/* ---------- Vendor ----------
|
|
55
|
+
* The engine's own colour, worn by its mark and (where the brand allows) by
|
|
56
|
+
* the model name beside it in a sessions list. Nowhere else.
|
|
57
|
+
*
|
|
58
|
+
* This is the one place a brand colour is allowed in the product, and it earns
|
|
59
|
+
* it by doing a job nothing else on the row does: the list already spends blue
|
|
60
|
+
* on running, amber on waiting, red on failed and muted on idle, so none of
|
|
61
|
+
* those can also say *whose engine this is* — and the mark alone is a 12px
|
|
62
|
+
* silhouette.
|
|
63
|
+
*
|
|
64
|
+
* Two values per vendor, because that glyph has to hold against both grounds:
|
|
65
|
+
* the dark-ground value is lifted and the light-ground one darkened.
|
|
66
|
+
*
|
|
67
|
+
* **OpenAI's is monochrome, and that is their rule rather than our taste**:
|
|
68
|
+
* their guidelines forbid adding colour to the mark, so it is pure white on
|
|
69
|
+
* dark and a near-black on light — both sanctioned, and the only kind of pair
|
|
70
|
+
* legible on both grounds. A green was tried first and is wrong, however well
|
|
71
|
+
* it reads. Near-black rather than `#000` because pure black would sit *harder*
|
|
72
|
+
* than the session title above it, so the mark would out-weigh the name it is
|
|
73
|
+
* labelling.
|
|
74
|
+
*
|
|
75
|
+
* That makes the two vendors asymmetric, which is the honest outcome: coral is
|
|
76
|
+
* Anthropic's brand and monochrome is OpenAI's, so a rule that gave both an
|
|
77
|
+
* accent would misrepresent one of them. It also means OpenAI's token is at
|
|
78
|
+
* full contrast, which is why only the **mark** wears it — see
|
|
79
|
+
* `vendorTextClass` in `EngineIcon.tsx`.
|
|
80
|
+
*
|
|
81
|
+
* A third vendor is two declarations here and one map entry there. */
|
|
82
|
+
--vendor-claude: #b3573a;
|
|
83
|
+
--vendor-openai: #373737;
|
|
84
|
+
|
|
54
85
|
/* ---------- Accent ----------
|
|
55
86
|
* VS Code blue (`#0078d4`) — the product's primary. It is the *same* hue in
|
|
56
87
|
* both themes rather than the inverse of the canvas: a primary that flips
|
|
@@ -122,6 +153,10 @@
|
|
|
122
153
|
--fg-4: #6e6e6e;
|
|
123
154
|
--fg-disabled: #454545;
|
|
124
155
|
|
|
156
|
+
/* ---------- Vendor ---------- (see the light block for the whole argument) */
|
|
157
|
+
--vendor-claude: #cc7c5e;
|
|
158
|
+
--vendor-openai: #ffffff;
|
|
159
|
+
|
|
125
160
|
/* ---------- Accent ----------
|
|
126
161
|
* The same blue as light. `--accent` stays `#0078d4` because it is a *fill*
|
|
127
162
|
* carrying white text (4.6:1) — the brighter `#3794ff` would drop that to
|
|
@@ -275,6 +310,13 @@
|
|
|
275
310
|
--color-info: var(--info);
|
|
276
311
|
--color-info-bg: var(--info-bg);
|
|
277
312
|
|
|
313
|
+
/* Vendor colours as real utilities (`text-vendor-claude`), not hand-written
|
|
314
|
+
classes: `cn`'s tailwind-merge only knows to *replace* a colour it can parse,
|
|
315
|
+
and `EngineIcon` ships its own `text-fg-3` that the vendor class has to win
|
|
316
|
+
against rather than merely follow in source order. */
|
|
317
|
+
--color-vendor-claude: var(--vendor-claude);
|
|
318
|
+
--color-vendor-openai: var(--vendor-openai);
|
|
319
|
+
|
|
278
320
|
/* Stock-name bridge → utilities (bg-primary / text-muted-foreground / ...). */
|
|
279
321
|
--color-primary: var(--primary);
|
|
280
322
|
--color-primary-foreground: var(--primary-foreground);
|