@workerdeck/ui 3.3.0 → 3.5.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/build/{SessionPanel-BNNc8grK.d.mts → SessionPanel-DIi4sEgh.d.mts} +5 -2
- package/build/index.d.mts +71 -6
- package/build/index.mjs +613 -159
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +104 -3
- package/build/{use-path-links-Cby4i_VL.mjs → use-path-links-WCsysQPy.mjs} +332 -79
- package/build/use-path-links-WCsysQPy.mjs.map +1 -0
- package/build/workspace.d.mts +1 -1
- package/build/workspace.mjs +1 -1
- package/package.json +4 -4
- package/src/components/agent/AgentAvatar.tsx +117 -0
- package/src/components/agent/Composer.tsx +29 -6
- package/src/components/agent/EffortSelect.tsx +64 -0
- package/src/components/agent/ModelSelect.tsx +1 -1
- package/src/components/agent/SessionBrowser.tsx +267 -42
- package/src/components/agent/SessionGroups.tsx +131 -21
- package/src/components/agent/SessionItem.tsx +182 -69
- package/src/components/agent/SessionPanel.tsx +75 -28
- package/src/components/agent/TranscriptItemView.tsx +20 -2
- package/src/components/agent/composer-commands.ts +24 -0
- package/src/components/terminal/affordances.tsx +29 -1
- package/src/components/terminal/height.ts +12 -2
- package/src/components/terminal/items.tsx +33 -1
- package/src/components/terminal/live-tool.ts +71 -0
- package/src/index.ts +12 -0
- package/src/lib/team-drop.ts +65 -0
- package/src/styles/terminal.css +9 -2
- package/src/styles/theme.css +20 -1
- package/build/use-path-links-Cby4i_VL.mjs.map +0 -1
|
@@ -4,6 +4,7 @@ import { compactionText, formatBytes, formatCost, formatDuration, toolInputPrevi
|
|
|
4
4
|
import { taskChildItems, type TerminalBlock, type ToolCallItem } from './blocks.ts'
|
|
5
5
|
import { IMAGE_BOX_LINES, hostImagePathOf } from './image-box.ts'
|
|
6
6
|
import { collapsedResult } from './result-preview.ts'
|
|
7
|
+
import { ELAPSED_WIDEST, liveTailLines, runStartedAt, runTailLines, toolBusy } from './live-tool.ts'
|
|
7
8
|
import { shellBodyLines, shellFooterText, shellHeaderText } from './shell-row.ts'
|
|
8
9
|
import { todoLine, todoPreview } from './todos.ts'
|
|
9
10
|
import { isPeerSend, peerName, planRun, runSummary, taskSummary } from './tool-run.ts'
|
|
@@ -565,7 +566,11 @@ function toolRowHeight(item: ToolCallItem, m: CellMetrics, extraPx: number): Acc
|
|
|
565
566
|
const todos = todoPreview(item.name, item.input)
|
|
566
567
|
const preview = todos ? todos.summary : toolInputPreview(item.input)
|
|
567
568
|
const backend = item.backend && item.backend !== 'server' ? ` · ${item.backend}` : ''
|
|
568
|
-
|
|
569
|
+
const elapsed = toolBusy(item) && item.ts !== undefined ? ELAPSED_WIDEST : ''
|
|
570
|
+
let acc = rowH(`${item.name}(${preview})${backend}${elapsed}`, m, { gutterCells: 2, extraPx })
|
|
571
|
+
for (const line of liveTailLines(item)) {
|
|
572
|
+
acc = add(acc, rowH(line || ' ', m, { indentCells: 3, gutterCells: 3, extraPx }))
|
|
573
|
+
}
|
|
569
574
|
|
|
570
575
|
const boxes = (item.result?.images?.length ?? 0) + (hostImagePathOf(item) === undefined ? 0 : 1)
|
|
571
576
|
if (boxes > 0) {
|
|
@@ -680,7 +685,12 @@ export function blockHeight(block: TerminalBlock, m: CellMetrics): ComputedHeigh
|
|
|
680
685
|
return itemHeight(plan.item, m)
|
|
681
686
|
}
|
|
682
687
|
const busy = plan.items.some((item) => item.status === 'running' || item.status === 'pending')
|
|
683
|
-
|
|
688
|
+
const elapsed = runStartedAt(plan.items) === undefined ? '' : ELAPSED_WIDEST
|
|
689
|
+
let acc = rowH(runSummary(plan.items, busy) + elapsed, m)
|
|
690
|
+
for (const line of runTailLines(plan.items)) {
|
|
691
|
+
acc = add(acc, rowH(line || ' ', m, { indentCells: 3, gutterCells: 3 }))
|
|
692
|
+
}
|
|
693
|
+
return acc
|
|
684
694
|
}
|
|
685
695
|
return itemHeight(block.item, m)
|
|
686
696
|
}
|
|
@@ -5,7 +5,8 @@ import { compactionText, formatBytes, formatCost, formatDuration, toolInputPrevi
|
|
|
5
5
|
import { isMutatingTool } from '../../lib/tool-icon.ts'
|
|
6
6
|
import { usePulse } from '../agent/pulse.tsx'
|
|
7
7
|
import { PromptTokenText } from '../agent/PromptTokenText.tsx'
|
|
8
|
-
import { ActionPlacementProvider, BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
8
|
+
import { ActionPlacementProvider, BackgroundAction, BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
9
|
+
import { canBackground, elapsedLabel, liveTailLines, runStartedAt, runTailLines } from './live-tool.ts'
|
|
9
10
|
import { TerminalDiff } from './diff.tsx'
|
|
10
11
|
import { TerminalMarkdown } from './markdown.tsx'
|
|
11
12
|
import { usePeerNames } from './peer-names.tsx'
|
|
@@ -191,6 +192,9 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
191
192
|
const busy = status === 'running' || status === 'pending'
|
|
192
193
|
const isError = status === 'failed' || item.result?.isError === true
|
|
193
194
|
const pulse = usePulse(busy)
|
|
195
|
+
const now = useTicker(busy && item.ts !== undefined)
|
|
196
|
+
const elapsed = busy ? elapsedLabel(item.ts, now) : undefined
|
|
197
|
+
const tail = liveTailLines(item)
|
|
194
198
|
|
|
195
199
|
const text = item.result?.text ?? ''
|
|
196
200
|
const lines = text.trimEnd().split('\n')
|
|
@@ -214,6 +218,7 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
214
218
|
<WithActions
|
|
215
219
|
actions={
|
|
216
220
|
<>
|
|
221
|
+
{canBackground(item) ? <BackgroundAction toolUseId={item.id} /> : null}
|
|
217
222
|
<BookmarkAction id={item.id} />
|
|
218
223
|
{copyable ? <CopyAction text={copyable} label="Copy" /> : null}
|
|
219
224
|
</>
|
|
@@ -225,10 +230,18 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
225
230
|
{title ?? item.name}
|
|
226
231
|
</Ink>
|
|
227
232
|
<Ink tone="dim">({todos ? todos.summary : toolInputPreview(item.input)})</Ink>
|
|
233
|
+
{elapsed ? (
|
|
234
|
+
<Ink tone="faint">
|
|
235
|
+
{' '}
|
|
236
|
+
·{'\u00a0'}
|
|
237
|
+
{elapsed}
|
|
238
|
+
</Ink>
|
|
239
|
+
) : null}
|
|
228
240
|
{title && open ? <Ink tone="faint"> · {item.name}</Ink> : null}
|
|
229
241
|
{item.backend && item.backend !== 'server' ? <Ink tone="faint"> · {item.backend}</Ink> : null}
|
|
230
242
|
</Row>
|
|
231
243
|
</Pressable>
|
|
244
|
+
<LiveTail lines={tail} />
|
|
232
245
|
{hostPath !== undefined ? <TerminalHostImage path={hostPath} /> : null}
|
|
233
246
|
{item.result?.images?.map((image) => (
|
|
234
247
|
<TerminalImage key={image.partIndex} toolUseId={item.id} name={resultImageName(item.name, image)} image={image} />
|
|
@@ -373,14 +386,25 @@ export function ToolRunRow({ items }: { items: ToolCallItem[] }) {
|
|
|
373
386
|
})
|
|
374
387
|
const failed = runFailed(items)
|
|
375
388
|
const pulse = usePulse(busy)
|
|
389
|
+
const startedAt = runStartedAt(items)
|
|
390
|
+
const now = useTicker(startedAt !== undefined)
|
|
391
|
+
const elapsed = elapsedLabel(startedAt, now)
|
|
376
392
|
|
|
377
393
|
return (
|
|
378
394
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
379
395
|
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
380
396
|
<Row glyph={busy ? pulse : undefined} glyphTone={busy ? 'mark' : undefined} tone={failed ? 'red' : 'dim'}>
|
|
381
397
|
{busy ? <span className="term-shimmer">{runSummary(items, busy)}</span> : runSummary(items, busy)}
|
|
398
|
+
{elapsed ? (
|
|
399
|
+
<Ink tone="faint">
|
|
400
|
+
{' '}
|
|
401
|
+
·{'\u00a0'}
|
|
402
|
+
{elapsed}
|
|
403
|
+
</Ink>
|
|
404
|
+
) : null}
|
|
382
405
|
</Row>
|
|
383
406
|
</Pressable>
|
|
407
|
+
{open ? null : <LiveTail lines={runTailLines(items)} />}
|
|
384
408
|
{open ? (
|
|
385
409
|
<div>
|
|
386
410
|
{items.map((item, index) => (
|
|
@@ -511,6 +535,14 @@ export function FileRow({ item, href }: { item: Extract<TranscriptItem, { kind:
|
|
|
511
535
|
)
|
|
512
536
|
}
|
|
513
537
|
|
|
538
|
+
function LiveTail({ lines }: { lines: string[] }) {
|
|
539
|
+
return lines.map((line, index) => (
|
|
540
|
+
<Row key={index} indent={1} columns={3} glyph={index === 0 ? '⎿' : undefined} tone="faint">
|
|
541
|
+
{line || ' '}
|
|
542
|
+
</Row>
|
|
543
|
+
))
|
|
544
|
+
}
|
|
545
|
+
|
|
514
546
|
export function useTicker(on: boolean): number {
|
|
515
547
|
const [now, setNow] = useState(() => Date.now())
|
|
516
548
|
useEffect(() => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { formatDuration } from '../../lib/format.ts'
|
|
2
|
+
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
|
+
import type { ToolCallItem } from './blocks.ts'
|
|
4
|
+
|
|
5
|
+
export const LIVE_TAIL_LINES = 5
|
|
6
|
+
export const ELAPSED_AFTER_MS = 5000
|
|
7
|
+
export const SEND_NOW_AFTER_MS = 5000
|
|
8
|
+
// The widest label the header grows by while busy, so a height estimate never undercounts a wrap.
|
|
9
|
+
export const ELAPSED_WIDEST = ' ·\u00a059m\u00a059s'
|
|
10
|
+
|
|
11
|
+
const BACKGROUNDABLE = new Set(['Bash', 'Task', 'Agent'])
|
|
12
|
+
|
|
13
|
+
export function toolBusy(item: ToolCallItem): boolean {
|
|
14
|
+
const status = item.status ?? (item.result === undefined ? 'running' : 'settled')
|
|
15
|
+
return status === 'running' || status === 'pending'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function liveTailLines(item: ToolCallItem): string[] {
|
|
19
|
+
if (!item.liveTail || !toolBusy(item)) {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
return item.liveTail.split('\n').slice(-LIVE_TAIL_LINES)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function elapsedLabel(startedAt: number | undefined, now: number): string | undefined {
|
|
26
|
+
if (startedAt === undefined || now - startedAt < ELAPSED_AFTER_MS) {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
return formatDuration(Math.floor((now - startedAt) / 1000) * 1000).replace(' ', '\u00a0')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function runStartedAt(items: readonly ToolCallItem[]): number | undefined {
|
|
33
|
+
let earliest: number | undefined
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
if (toolBusy(item) && item.ts !== undefined && (earliest === undefined || item.ts < earliest)) {
|
|
36
|
+
earliest = item.ts
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return earliest
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function canBackground(item: ToolCallItem): boolean {
|
|
43
|
+
return BACKGROUNDABLE.has(item.name) && item.parentToolUseId == null && toolBusy(item)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runTailLines(items: readonly ToolCallItem[]): string[] {
|
|
47
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
48
|
+
const lines = liveTailLines(items[index]!)
|
|
49
|
+
if (lines.length > 0) {
|
|
50
|
+
return lines
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Walks back only through the current turn: a running call is never older than the last prompt.
|
|
57
|
+
// The earliest start among them, or 0 when a call carries no `ts`.
|
|
58
|
+
export function backgroundableSince(items: readonly TranscriptItem[]): number | undefined {
|
|
59
|
+
let since: number | undefined
|
|
60
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
61
|
+
const item = items[index]!
|
|
62
|
+
if (item.kind === 'user' && item.parentToolUseId == null) {
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
if (item.kind === 'tool_call' && canBackground(item)) {
|
|
66
|
+
const ts = item.ts ?? 0
|
|
67
|
+
since = since === undefined ? ts : Math.min(since, ts)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return since
|
|
71
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -123,6 +123,7 @@ export {
|
|
|
123
123
|
type ComposerRow,
|
|
124
124
|
} from './components/agent/composer-commands.ts'
|
|
125
125
|
export { ModelSelect, type ModelSelectProps } from './components/agent/ModelSelect.tsx'
|
|
126
|
+
export { EffortSelect, effortChoices, type EffortSelectProps } from './components/agent/EffortSelect.tsx'
|
|
126
127
|
export {
|
|
127
128
|
PERMISSION_MODES,
|
|
128
129
|
PermissionModeSelect,
|
|
@@ -163,6 +164,7 @@ export {
|
|
|
163
164
|
type GroupTarget,
|
|
164
165
|
type SessionBrowserProps,
|
|
165
166
|
} from './components/agent/SessionBrowser.tsx'
|
|
167
|
+
export { teamDropRefusal, type TeamMove } from './lib/team-drop.ts'
|
|
166
168
|
export {
|
|
167
169
|
STEP_DISPLAY_OPTIONS,
|
|
168
170
|
SessionFilters,
|
|
@@ -172,6 +174,16 @@ export {
|
|
|
172
174
|
type SessionFiltersProps,
|
|
173
175
|
} from './components/agent/SessionFilters.tsx'
|
|
174
176
|
export { SessionItem, type SelectModifiers, type SessionItemProps } from './components/agent/SessionItem.tsx'
|
|
177
|
+
export {
|
|
178
|
+
AgentAvatar,
|
|
179
|
+
AgentHeading,
|
|
180
|
+
LeadChip,
|
|
181
|
+
avatarOf,
|
|
182
|
+
type AgentAvatarImage,
|
|
183
|
+
type AgentAvatarProps,
|
|
184
|
+
type AgentAvatars,
|
|
185
|
+
type AgentHeadingProps,
|
|
186
|
+
} from './components/agent/AgentAvatar.tsx'
|
|
175
187
|
export { SessionStatusIcon } from './components/agent/SessionStatusIcon.tsx'
|
|
176
188
|
export { EngineIcon, engineMark, vendorMarkClass, vendorTextClass } from './components/agent/EngineIcon.tsx'
|
|
177
189
|
export {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SessionRow } from '@workerdeck/protocol'
|
|
2
|
+
|
|
3
|
+
export type DropZone = 'before' | 'join' | 'after'
|
|
4
|
+
|
|
5
|
+
// What a drop asks of the host: `lead: null` leaves the team. Joining renumbers the team densely from 0, so
|
|
6
|
+
// `siblings` lists the other members whose `order` changes; teams are small and most of them never moved.
|
|
7
|
+
export type TeamMove = { row: SessionRow; lead: SessionRow | null; order?: number; siblings?: { row: SessionRow; order: number }[] }
|
|
8
|
+
|
|
9
|
+
export function dropZone(offsetY: number, height: number): DropZone {
|
|
10
|
+
if (height <= 0) {
|
|
11
|
+
return 'join'
|
|
12
|
+
}
|
|
13
|
+
const at = offsetY / height
|
|
14
|
+
return at < 0.25 ? 'before' : at > 0.75 ? 'after' : 'join'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Mirrors the gateway's `leadRefusal`, so a drag can say no before it asks; the 409 stays the authority.
|
|
18
|
+
export function teamDropRefusal(dragged: SessionRow, lead: SessionRow): string | undefined {
|
|
19
|
+
const mover = dragged.info.agent
|
|
20
|
+
const target = lead.info.agent
|
|
21
|
+
if (dragged.hostId !== lead.hostId) {
|
|
22
|
+
return 'a team stays on one gateway'
|
|
23
|
+
}
|
|
24
|
+
if (!target) {
|
|
25
|
+
return 'only an agent can lead a team'
|
|
26
|
+
}
|
|
27
|
+
if (mover?.id === target.id || dragged.info.id === lead.info.id) {
|
|
28
|
+
return 'an agent cannot lead its own team'
|
|
29
|
+
}
|
|
30
|
+
if (target.lead !== undefined) {
|
|
31
|
+
return `${target.name} is a member of a team; teams are one level deep`
|
|
32
|
+
}
|
|
33
|
+
if (mover?.leads) {
|
|
34
|
+
return `${mover.name} leads a team; teams are one level deep`
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The move a drop over a member asks for: join (or reorder within) that member's team, at the member's place.
|
|
40
|
+
export function memberDrop(dragged: SessionRow, lead: SessionRow, member: SessionRow, zone: DropZone): TeamMove | string {
|
|
41
|
+
const refusal = teamDropRefusal(dragged, lead)
|
|
42
|
+
if (refusal) {
|
|
43
|
+
return refusal
|
|
44
|
+
}
|
|
45
|
+
const others = (lead.members ?? []).filter((row) => row.info.id !== dragged.info.id || row.hostId !== dragged.hostId)
|
|
46
|
+
const at = others.findIndex((row) => row.info.id === member.info.id && row.hostId === member.hostId)
|
|
47
|
+
const index = at < 0 ? others.length : zone === 'before' ? at : at + 1
|
|
48
|
+
return placeMember(dragged, lead, others, index)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function joinDrop(dragged: SessionRow, lead: SessionRow): TeamMove | string {
|
|
52
|
+
const refusal = teamDropRefusal(dragged, lead)
|
|
53
|
+
if (refusal) {
|
|
54
|
+
return refusal
|
|
55
|
+
}
|
|
56
|
+
const others = (lead.members ?? []).filter((row) => row.info.id !== dragged.info.id || row.hostId !== dragged.hostId)
|
|
57
|
+
return placeMember(dragged, lead, others, others.length)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function placeMember(dragged: SessionRow, lead: SessionRow, others: readonly SessionRow[], index: number): TeamMove {
|
|
61
|
+
const siblings = others
|
|
62
|
+
.map((row, i) => ({ row, order: i < index ? i : i + 1 }))
|
|
63
|
+
.filter(({ row, order }) => row.info.agent?.order !== order)
|
|
64
|
+
return { row: dragged, lead, order: index, ...(siblings.length ? { siblings } : {}) }
|
|
65
|
+
}
|
package/src/styles/terminal.css
CHANGED
|
@@ -483,9 +483,13 @@
|
|
|
483
483
|
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:not(.absolute) {
|
|
484
484
|
position: relative;
|
|
485
485
|
}
|
|
486
|
-
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:
|
|
486
|
+
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:focus-within {
|
|
487
487
|
z-index: 1;
|
|
488
488
|
}
|
|
489
|
+
/* Above a focused row too: the row just clicked open keeps focus and would cover the bar beneath the hovered one. */
|
|
490
|
+
:is([data-terminal], [data-cards]) [data-slot='transcript-rows'] [data-index]:hover {
|
|
491
|
+
z-index: 2;
|
|
492
|
+
}
|
|
489
493
|
@media (hover: none) {
|
|
490
494
|
:is([data-terminal], [data-cards]) .term-actions {
|
|
491
495
|
opacity: 1;
|
|
@@ -785,9 +789,12 @@ button.term-link {
|
|
|
785
789
|
* it is the same `--bg`, and the row it covers is its own content.
|
|
786
790
|
*/
|
|
787
791
|
[data-terminal] [data-slot='transcript-rows'] > [data-sticky-lane] {
|
|
788
|
-
z-index: 3;
|
|
789
792
|
pointer-events: none;
|
|
790
793
|
}
|
|
794
|
+
/* Raised only while pinned: a raised idle lane covered the hover bar of the row above its prompt. */
|
|
795
|
+
[data-terminal] [data-slot='transcript-rows'] > [data-sticky-lane]:has(> [data-sticky-headlane] > [data-stuck]) {
|
|
796
|
+
z-index: 3;
|
|
797
|
+
}
|
|
791
798
|
|
|
792
799
|
/* The prompt itself is *inside* the lane, not beneath it, so the lane's `none`
|
|
793
800
|
reached it too and made a user message the one block in the transcript the
|
package/src/styles/theme.css
CHANGED
|
@@ -666,6 +666,23 @@
|
|
|
666
666
|
opacity: 0;
|
|
667
667
|
animation: wd-hold-appear 150ms ease-out 600ms forwards;
|
|
668
668
|
}
|
|
669
|
+
@keyframes wd-avatar-strip {
|
|
670
|
+
to {
|
|
671
|
+
background-position-x: var(--wd-strip-end);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
.wd-avatar-strip {
|
|
675
|
+
image-rendering: pixelated;
|
|
676
|
+
background-repeat: no-repeat;
|
|
677
|
+
animation-name: wd-avatar-strip;
|
|
678
|
+
animation-timing-function: steps(var(--wd-strip-steps));
|
|
679
|
+
animation-iteration-count: infinite;
|
|
680
|
+
}
|
|
681
|
+
@media (prefers-reduced-motion: reduce) {
|
|
682
|
+
.wd-avatar-strip {
|
|
683
|
+
animation: none;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
669
686
|
|
|
670
687
|
@keyframes prompt-area-chip-ripple {
|
|
671
688
|
0% {
|
|
@@ -743,9 +760,11 @@
|
|
|
743
760
|
* row's first line, so it has no in-flow y to match, and the 1st prompt (no
|
|
744
761
|
* gap) and every later one (pt-4/pt-2) share these rules unchanged. */
|
|
745
762
|
[data-slot='transcript-rows'] > [data-sticky-lane='cards'] {
|
|
746
|
-
z-index: 3;
|
|
747
763
|
pointer-events: none;
|
|
748
764
|
}
|
|
765
|
+
[data-slot='transcript-rows'] > [data-sticky-lane='cards']:has(> [data-sticky-headlane] > [data-stuck]) {
|
|
766
|
+
z-index: 3;
|
|
767
|
+
}
|
|
749
768
|
|
|
750
769
|
/* The head's own strip: the full lane, out of flow - see terminal.css for why
|
|
751
770
|
sticky confinement wants the border box, not a cancelled margin box. */
|