@workerdeck/ui 2.10.0 → 2.11.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/index.d.mts +4 -3
- package/build/index.mjs +12 -8
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +56 -1
- package/build/{use-path-links-CvcxdUXL.mjs → use-path-links-DO_79-wX.mjs} +405 -176
- package/build/use-path-links-DO_79-wX.mjs.map +1 -0
- package/build/workspace.mjs +1 -1
- package/package.json +4 -4
- package/src/components/agent/SessionItem.tsx +4 -3
- package/src/components/agent/SessionPanel.tsx +2 -0
- package/src/components/agent/TranscriptItemView.tsx +2 -0
- package/src/components/agent/TranscriptRows.tsx +60 -56
- package/src/components/agent/UsageDialog.tsx +19 -6
- package/src/components/agent/pulse.tsx +53 -16
- package/src/components/agent/use-transcript-jumps.ts +25 -0
- package/src/components/terminal/TerminalTranscript.tsx +31 -27
- package/src/components/terminal/blocks.ts +15 -7
- package/src/components/terminal/height.ts +14 -5
- package/src/components/terminal/items.tsx +97 -11
- package/src/components/terminal/peer-names.tsx +16 -0
- package/src/components/terminal/row.tsx +1 -1
- package/src/components/terminal/tool-run.ts +68 -0
- package/src/lib/session-cost.ts +12 -2
- package/src/lib/tool-icon.ts +5 -1
- package/src/styles/terminal.css +93 -0
- package/src/styles/theme.css +1 -1
- package/build/use-path-links-CvcxdUXL.mjs.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
1
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
2
|
+
import type { MessageOrigin } from '@workerdeck/protocol'
|
|
2
3
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
4
|
import { compactionText, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
4
5
|
import { isMutatingTool } from '../../lib/tool-icon.ts'
|
|
@@ -7,13 +8,14 @@ import { PromptTokenText } from '../agent/PromptTokenText.tsx'
|
|
|
7
8
|
import { BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
8
9
|
import { TerminalDiff } from './diff.tsx'
|
|
9
10
|
import { TerminalMarkdown } from './markdown.tsx'
|
|
11
|
+
import { usePeerNames } from './peer-names.tsx'
|
|
10
12
|
import { Pressable, useRevealOnOpen } from './press.tsx'
|
|
11
13
|
import { IMAGE_BOX_LINES, IMAGE_UNAVAILABLE, imagePlaceholder } from './image-box.ts'
|
|
12
14
|
import { collapsedResult } from './result-preview.ts'
|
|
13
15
|
import { useToolResultFetcher } from '../agent/tool-result-fetch.tsx'
|
|
14
16
|
import { useToolTitle } from '../agent/tool-titles.tsx'
|
|
15
17
|
import { useToolResultImageSrc } from '../agent/tool-result-image.tsx'
|
|
16
|
-
import { runFailed, runSummary } from './tool-run.ts'
|
|
18
|
+
import { isPeerSend, peerName, peerOneLine, peerSendTarget, peerSendText, planRun, runFailed, runSummary } from './tool-run.ts'
|
|
17
19
|
import { todoLine, todoPreview, type TodoPreview, type TodoStatus } from './todos.ts'
|
|
18
20
|
import { type ToolCallItem } from './blocks.ts'
|
|
19
21
|
import { Band, Blank, Ink, Row, type Tone } from './row.tsx'
|
|
@@ -35,21 +37,97 @@ function clipToChars(lines: string[], maxChars: number): string[] {
|
|
|
35
37
|
return out
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export function peerLabel(origin: MessageOrigin): string {
|
|
41
|
+
const who = origin.name ? `${origin.name} (${origin.sessionId.slice(0, 8)})` : origin.sessionId.slice(0, 8)
|
|
42
|
+
return `message from ${origin.engine ? `${origin.engine} session ` : 'session '}${who}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const PEER_IN_GLYPH = '↦'
|
|
46
|
+
export const PEER_OUT_GLYPH = '↤'
|
|
47
|
+
|
|
48
|
+
// The row peer traffic draws on, both directions. Never folded into a tool run and never summarised
|
|
49
|
+
// away: one agent talking to another is the transcript's headline, not its evidence.
|
|
50
|
+
function PeerRow({ glyph, who, body, tone = 'peer', clip }: { glyph: string; who: string; body: ReactNode; tone?: Tone; clip?: boolean }) {
|
|
51
|
+
const line = (
|
|
52
|
+
<>
|
|
53
|
+
<Ink tone={tone} bold>
|
|
54
|
+
{who}
|
|
55
|
+
</Ink>
|
|
56
|
+
{': '}
|
|
57
|
+
{body}
|
|
58
|
+
</>
|
|
59
|
+
)
|
|
60
|
+
return (
|
|
61
|
+
<Row glyph={glyph} glyphTone={tone} tone={tone}>
|
|
62
|
+
{clip ? <span className="term-clip-1">{line}</span> : line}
|
|
63
|
+
</Row>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function PeerSendRow({ item }: { item: ToolCallItem }) {
|
|
68
|
+
const [open, setOpen] = useState(false)
|
|
69
|
+
const names = usePeerNames()
|
|
70
|
+
const reveal = useRevealOnOpen(open)
|
|
71
|
+
const lines = peerSendText(item).split('\n')
|
|
72
|
+
const failed = item.status === 'failed' || item.result?.isError === true
|
|
73
|
+
const tone: Tone = failed ? 'red' : 'peer'
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<WithActions actions={<BookmarkAction id={item.id} />}>
|
|
77
|
+
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
78
|
+
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
79
|
+
<PeerRow
|
|
80
|
+
glyph={PEER_OUT_GLYPH}
|
|
81
|
+
who={peerSendTarget(item, names)}
|
|
82
|
+
tone={tone}
|
|
83
|
+
clip={!open}
|
|
84
|
+
body={open ? lines[0] || ' ' : peerOneLine(lines.join(' '))}
|
|
85
|
+
/>
|
|
86
|
+
</Pressable>
|
|
87
|
+
{open ? (
|
|
88
|
+
<div>
|
|
89
|
+
{lines.slice(1).map((line, index) => (
|
|
90
|
+
<Row key={index} tone={tone}>
|
|
91
|
+
{line || ' '}
|
|
92
|
+
</Row>
|
|
93
|
+
))}
|
|
94
|
+
{item.result ? <Row tone="faint">{peerOneLine(item.result.text)}</Row> : null}
|
|
95
|
+
</div>
|
|
96
|
+
) : null}
|
|
97
|
+
</div>
|
|
98
|
+
</WithActions>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
38
102
|
export function UserRow({ item }: { item: Extract<TranscriptItem, { kind: 'user' }> }) {
|
|
39
103
|
return (
|
|
40
104
|
<WithActions actions={<BookmarkAction id={item.id} />}>
|
|
41
|
-
<div className="term-user">
|
|
105
|
+
<div className="term-user" data-peer={item.origin ? '' : undefined}>
|
|
42
106
|
{item.attachments?.length ? (
|
|
43
107
|
<Row glyph={PROMPT_GLYPH} glyphTone="dim" tone="dim">
|
|
44
108
|
{item.attachments.map((attachment) => attachment.name).join(', ')}
|
|
45
109
|
</Row>
|
|
46
110
|
) : null}
|
|
47
111
|
{item.text
|
|
48
|
-
? item.text.split('\n').map((line, index) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
112
|
+
? item.text.split('\n').map((line, index) =>
|
|
113
|
+
item.origin && index === 0 ? (
|
|
114
|
+
<PeerRow
|
|
115
|
+
key={index}
|
|
116
|
+
glyph={PEER_IN_GLYPH}
|
|
117
|
+
who={peerName(item.origin)}
|
|
118
|
+
body={line ? <PromptTokenText text={line} /> : ' '}
|
|
119
|
+
/>
|
|
120
|
+
) : (
|
|
121
|
+
<Row
|
|
122
|
+
key={index}
|
|
123
|
+
glyph={!item.origin && index === 0 ? PROMPT_GLYPH : undefined}
|
|
124
|
+
glyphTone="dim"
|
|
125
|
+
tone={item.origin ? 'peer' : 'fg'}
|
|
126
|
+
>
|
|
127
|
+
{line ? <PromptTokenText text={line} /> : ' '}
|
|
128
|
+
</Row>
|
|
129
|
+
),
|
|
130
|
+
)
|
|
53
131
|
: null}
|
|
54
132
|
</div>
|
|
55
133
|
</WithActions>
|
|
@@ -128,7 +206,7 @@ export function ToolRow({ item }: { item: ToolCallItem }) {
|
|
|
128
206
|
>
|
|
129
207
|
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
130
208
|
<Row glyph={busy ? pulse : '●'} glyphTone={tone} tone="fg" title={title ? item.name : undefined}>
|
|
131
|
-
<Ink bold tone="bright">
|
|
209
|
+
<Ink bold tone="bright" className={busy ? 'term-shimmer' : undefined}>
|
|
132
210
|
{title ?? item.name}
|
|
133
211
|
</Ink>
|
|
134
212
|
<Ink tone="dim">({todos ? todos.summary : toolInputPreview(item.input)})</Ink>
|
|
@@ -253,7 +331,7 @@ export function ToolRunRow({ items }: { items: ToolCallItem[] }) {
|
|
|
253
331
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
254
332
|
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
255
333
|
<Row glyph={busy ? pulse : undefined} glyphTone={busy ? 'mark' : undefined} tone={failed ? 'red' : 'dim'}>
|
|
256
|
-
{runSummary(items, busy)}
|
|
334
|
+
{busy ? <span className="term-shimmer">{runSummary(items, busy)}</span> : runSummary(items, busy)}
|
|
257
335
|
</Row>
|
|
258
336
|
</Pressable>
|
|
259
337
|
{open ? (
|
|
@@ -267,6 +345,14 @@ export function ToolRunRow({ items }: { items: ToolCallItem[] }) {
|
|
|
267
345
|
)
|
|
268
346
|
}
|
|
269
347
|
|
|
348
|
+
export function RunRow({ items }: { items: ToolCallItem[] }) {
|
|
349
|
+
const plan = planRun(items)
|
|
350
|
+
if (plan.kind === 'summary') {
|
|
351
|
+
return <ToolRunRow items={plan.items} />
|
|
352
|
+
}
|
|
353
|
+
return isPeerSend(plan.item) ? <PeerSendRow item={plan.item} /> : <ToolRow item={plan.item} />
|
|
354
|
+
}
|
|
355
|
+
|
|
270
356
|
export function TurnResultRow({ item }: { item: Extract<TranscriptItem, { kind: 'turn_result' }> }) {
|
|
271
357
|
return (
|
|
272
358
|
<div>
|
|
@@ -335,7 +421,7 @@ export function WorkingRow({ label, startedAt, tokens }: { label: string; starte
|
|
|
335
421
|
const readings = [elapsed, tokens ? `↓ ${(tokens / 1000).toFixed(1)}k tokens` : undefined].filter(Boolean)
|
|
336
422
|
return (
|
|
337
423
|
<Row glyph={pulse} glyphTone="mark" tone="mark">
|
|
338
|
-
{label}
|
|
424
|
+
<span className="term-shimmer">{label}</span>
|
|
339
425
|
{readings.length ? <Ink tone="faint"> ({readings.join(' · ')})</Ink> : null}
|
|
340
426
|
</Row>
|
|
341
427
|
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ReactNode } from 'react'
|
|
2
|
+
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
|
+
import { peerNamesOf } from './tool-run.ts'
|
|
4
|
+
|
|
5
|
+
const EMPTY: ReadonlyMap<string, string> = new Map()
|
|
6
|
+
|
|
7
|
+
const PeerNamesContext = createContext<ReadonlyMap<string, string>>(EMPTY)
|
|
8
|
+
|
|
9
|
+
export function PeerNamesProvider({ items, children }: { items: readonly TranscriptItem[]; children: ReactNode }) {
|
|
10
|
+
const names = useMemo(() => peerNamesOf(items), [items])
|
|
11
|
+
return <PeerNamesContext.Provider value={names}>{children}</PeerNamesContext.Provider>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function usePeerNames(): ReadonlyMap<string, string> {
|
|
15
|
+
return useContext(PeerNamesContext)
|
|
16
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'
|
|
2
2
|
import { cn } from '../../lib/utils.ts'
|
|
3
3
|
|
|
4
|
-
export type Tone = 'fg' | 'bright' | 'dim' | 'faint' | 'mark' | 'blue' | 'green' | 'red' | 'yellow' | 'magenta'
|
|
4
|
+
export type Tone = 'fg' | 'bright' | 'dim' | 'faint' | 'mark' | 'blue' | 'green' | 'red' | 'yellow' | 'magenta' | 'peer'
|
|
5
5
|
|
|
6
6
|
export interface RowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
7
7
|
glyph?: ReactNode
|
|
@@ -1,10 +1,69 @@
|
|
|
1
|
+
import { isPeerSendTool, peerDeliveredTo, type MessageOrigin } from '@workerdeck/protocol'
|
|
1
2
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
2
3
|
import { toolInputPreview } from '../../lib/format.ts'
|
|
3
4
|
import { isShellTool } from '../../lib/tool-icon.ts'
|
|
4
5
|
|
|
5
6
|
type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
|
|
6
7
|
|
|
8
|
+
// A message to another session draws as itself, always. Folded into a run it becomes one tick of
|
|
9
|
+
// `Ran 4 tools`, which is exactly the collapse that hid the conversation the reader came for.
|
|
10
|
+
export function isPeerSend(item: TranscriptItem): boolean {
|
|
11
|
+
return item.kind === 'tool_call' && isPeerSendTool(item.name)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function peerName(origin: MessageOrigin): string {
|
|
15
|
+
return origin.name ?? origin.sessionId.slice(0, 8)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Every peer this transcript has named, by session id. A conversation names its peers twice over: an
|
|
19
|
+
// arriving message carries the sender's name, a delivery receipt carries the recipient's. Either one
|
|
20
|
+
// answers for the other direction, so a send that was only ever acknowledged by id still draws as a
|
|
21
|
+
// name once the peer speaks, and a transcript recorded before the receipts carried names heals itself.
|
|
22
|
+
export function peerNamesOf(items: readonly TranscriptItem[]): ReadonlyMap<string, string> {
|
|
23
|
+
const names = new Map<string, string>()
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
if (item.kind === 'user') {
|
|
26
|
+
if (item.origin?.name) {
|
|
27
|
+
names.set(item.origin.sessionId, item.origin.name)
|
|
28
|
+
}
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
if (item.kind === 'tool_call' && isPeerSendTool(item.name) && item.result && !item.result.isError) {
|
|
32
|
+
const delivered = peerDeliveredTo(item.result.text)
|
|
33
|
+
if (delivered?.name) {
|
|
34
|
+
names.set(delivered.sessionId, delivered.name)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return names
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Who a `peers_send` went to, read back out of the tool's own reply. Until the call settles there is
|
|
42
|
+
// no reply to read, so the row falls back to what the rest of the transcript knows, then to the id the
|
|
43
|
+
// model addressed.
|
|
44
|
+
export function peerSendTarget(item: ToolCallItem, names?: ReadonlyMap<string, string>): string {
|
|
45
|
+
const delivered = item.result && !item.result.isError ? peerDeliveredTo(item.result.text) : undefined
|
|
46
|
+
const addressed = (item.input as { sessionId?: unknown } | null)?.sessionId
|
|
47
|
+
const sessionId = delivered?.sessionId ?? (typeof addressed === 'string' ? addressed : undefined)
|
|
48
|
+
if (sessionId === undefined) {
|
|
49
|
+
return 'peer'
|
|
50
|
+
}
|
|
51
|
+
return delivered?.name ?? names?.get(sessionId) ?? sessionId.slice(0, 8)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function peerSendText(item: ToolCallItem): string {
|
|
55
|
+
const text = (item.input as { text?: unknown } | null)?.text
|
|
56
|
+
return typeof text === 'string' ? text : ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function peerOneLine(text: string): string {
|
|
60
|
+
return text.replace(/\s+/g, ' ').trim()
|
|
61
|
+
}
|
|
62
|
+
|
|
7
63
|
export function foldsTogether(a: ToolCallItem, b: ToolCallItem): boolean {
|
|
64
|
+
if (isPeerSend(a) || isPeerSend(b)) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
8
67
|
return a.parentToolUseId === b.parentToolUseId
|
|
9
68
|
}
|
|
10
69
|
|
|
@@ -19,6 +78,15 @@ export function toolFamily(name: string): string {
|
|
|
19
78
|
return name.toLowerCase()
|
|
20
79
|
}
|
|
21
80
|
|
|
81
|
+
export type RunPlan = { kind: 'call'; item: ToolCallItem } | { kind: 'summary'; items: ToolCallItem[] }
|
|
82
|
+
|
|
83
|
+
// Matches iOS's `planRun` (`TerminalPlanner.swift`): a run of exactly one call draws as the call
|
|
84
|
+
// itself, because the summary row would occupy the same one row while throwing away the tool's
|
|
85
|
+
// name, input and result preview.
|
|
86
|
+
export function planRun(items: ToolCallItem[]): RunPlan {
|
|
87
|
+
return items.length === 1 ? { kind: 'call', item: items[0]! } : { kind: 'summary', items }
|
|
88
|
+
}
|
|
89
|
+
|
|
22
90
|
export function runSummary(items: readonly ToolCallItem[], busy: boolean): string {
|
|
23
91
|
const verb = busy ? 'Running ' : 'Ran '
|
|
24
92
|
const tail = busy ? '…' : ''
|
package/src/lib/session-cost.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ByModel,
|
|
3
|
+
type CostBasis,
|
|
3
4
|
type CostBreakdown,
|
|
5
|
+
type PricingTable,
|
|
4
6
|
type TokenUsage,
|
|
5
7
|
UNPRICED_WARN_SHARE,
|
|
6
8
|
canonicalModel,
|
|
@@ -8,6 +10,7 @@ import {
|
|
|
8
10
|
costOfByModel,
|
|
9
11
|
rateFor,
|
|
10
12
|
totalTokens,
|
|
13
|
+
unknownBasisModels,
|
|
11
14
|
} from '@workerdeck/protocol'
|
|
12
15
|
|
|
13
16
|
export type SessionCostRow = {
|
|
@@ -15,6 +18,7 @@ export type SessionCostRow = {
|
|
|
15
18
|
usage: TokenUsage
|
|
16
19
|
tokens: number
|
|
17
20
|
costUsd: number | undefined
|
|
21
|
+
costBasis: CostBasis | undefined
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export type SessionCost = {
|
|
@@ -25,6 +29,8 @@ export type SessionCost = {
|
|
|
25
29
|
rows: SessionCostRow[]
|
|
26
30
|
unpricedShare: number
|
|
27
31
|
showUnpricedWarning: boolean
|
|
32
|
+
// Models the engine itself could not match to a rate table, so the figure *it* reports for them is a guess.
|
|
33
|
+
unknownBasisModels: string[]
|
|
28
34
|
// Both figures price the same tokens, one by the engine and one by us, so a gap is the thing worth reading.
|
|
29
35
|
gapUsd: number | undefined
|
|
30
36
|
}
|
|
@@ -33,13 +39,15 @@ export type SessionCostInput = {
|
|
|
33
39
|
costUsd?: number
|
|
34
40
|
totalCostUsd?: number
|
|
35
41
|
usageByModel?: ByModel
|
|
42
|
+
pricing?: PricingTable
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
const GAP_SHARE = 0.02
|
|
39
46
|
|
|
40
47
|
export function sessionCost(input: SessionCostInput): SessionCost {
|
|
41
48
|
const byModel = input.usageByModel ?? {}
|
|
42
|
-
const
|
|
49
|
+
const pricing = input.pricing
|
|
50
|
+
const breakdown = costOfByModel(byModel, pricing)
|
|
43
51
|
const computedUsd = input.costUsd ?? (breakdown.unpriced || Object.keys(byModel).length === 0 ? undefined : breakdown.total)
|
|
44
52
|
const reportedUsd = input.totalCostUsd
|
|
45
53
|
const rows = Object.entries(byModel)
|
|
@@ -47,7 +55,8 @@ export function sessionCost(input: SessionCostInput): SessionCost {
|
|
|
47
55
|
model: canonicalModel(model),
|
|
48
56
|
usage,
|
|
49
57
|
tokens: totalTokens(usage),
|
|
50
|
-
costUsd: rateFor(model) ? costOf(usage, model).total : undefined,
|
|
58
|
+
costUsd: rateFor(model, pricing) ? costOf(usage, model, pricing).total : undefined,
|
|
59
|
+
costBasis: usage.costBasis,
|
|
51
60
|
}))
|
|
52
61
|
.sort((a, b) => b.tokens - a.tokens)
|
|
53
62
|
const gap = computedUsd !== undefined && reportedUsd !== undefined ? reportedUsd - computedUsd : undefined
|
|
@@ -60,6 +69,7 @@ export function sessionCost(input: SessionCostInput): SessionCost {
|
|
|
60
69
|
rows,
|
|
61
70
|
unpricedShare: breakdown.unpricedShare,
|
|
62
71
|
showUnpricedWarning: breakdown.unpricedShare > UNPRICED_WARN_SHARE,
|
|
72
|
+
unknownBasisModels: unknownBasisModels(byModel),
|
|
63
73
|
gapUsd: worthShowing ? gap : undefined,
|
|
64
74
|
}
|
|
65
75
|
}
|
package/src/lib/tool-icon.ts
CHANGED
|
@@ -52,7 +52,11 @@ export function toolIcon(toolName: string): LucideIcon {
|
|
|
52
52
|
case 'Agent': {
|
|
53
53
|
return UsersRound
|
|
54
54
|
}
|
|
55
|
-
case 'TodoWrite':
|
|
55
|
+
case 'TodoWrite':
|
|
56
|
+
case 'TaskCreate':
|
|
57
|
+
case 'TaskUpdate':
|
|
58
|
+
case 'TaskGet':
|
|
59
|
+
case 'TaskList': {
|
|
56
60
|
return CheckSquare
|
|
57
61
|
}
|
|
58
62
|
case 'Skill': {
|
package/src/styles/terminal.css
CHANGED
|
@@ -79,11 +79,24 @@
|
|
|
79
79
|
--term-faint: #6a6a6a;
|
|
80
80
|
|
|
81
81
|
--term-mark: #d97757;
|
|
82
|
+
/* The working mark in motion. Five stops travelling violet to cyan and back,
|
|
83
|
+
lifted from box's `paint()` so an in-progress line reads the same here as
|
|
84
|
+
it does in the terminal. Deliberately not the clay accent: the mark states
|
|
85
|
+
whose turn it is, the shimmer states that something is still moving. */
|
|
86
|
+
--term-shimmer-1: #c5a3e8;
|
|
87
|
+
--term-shimmer-2: #a3a4e8;
|
|
88
|
+
--term-shimmer-3: #7b7ef5;
|
|
89
|
+
--term-shimmer-4: #8aaeea;
|
|
90
|
+
--term-shimmer-5: #a8e5ed;
|
|
82
91
|
--term-blue: #afb9fe;
|
|
83
92
|
--term-green: #4ec9a0;
|
|
84
93
|
--term-red: #f14c4c;
|
|
85
94
|
--term-yellow: #d7ba7d;
|
|
86
95
|
--term-magenta: #c586c0;
|
|
96
|
+
/* Peer traffic - a message from or to another session. Cyan is the one ANSI slot
|
|
97
|
+
the theme had left, and peer rows must not be confusable with a tool call
|
|
98
|
+
(blue), a success (green) or a wait (yellow). */
|
|
99
|
+
--term-peer: #56d4dd;
|
|
87
100
|
|
|
88
101
|
/* Diff bands. Full-bleed backgrounds, so they are tuned to sit under body
|
|
89
102
|
text rather than to be looked at: the CLI's added block is a dark green
|
|
@@ -118,11 +131,19 @@
|
|
|
118
131
|
--term-faint: #8d8d8d;
|
|
119
132
|
|
|
120
133
|
--term-mark: #bf5b3d;
|
|
134
|
+
/* Same five stops walked down for a light ground; the dark set's cyan is
|
|
135
|
+
invisible on white. */
|
|
136
|
+
--term-shimmer-1: #7a4fb0;
|
|
137
|
+
--term-shimmer-2: #5457b8;
|
|
138
|
+
--term-shimmer-3: #4347c9;
|
|
139
|
+
--term-shimmer-4: #3f6fb5;
|
|
140
|
+
--term-shimmer-5: #1f7d8c;
|
|
121
141
|
--term-blue: #0a66c2;
|
|
122
142
|
--term-green: #16794a;
|
|
123
143
|
--term-red: #c62828;
|
|
124
144
|
--term-yellow: #8a6d00;
|
|
125
145
|
--term-magenta: #8b3a8b;
|
|
146
|
+
--term-peer: #0e7490;
|
|
126
147
|
|
|
127
148
|
--term-add-fg: #12492a;
|
|
128
149
|
--term-add-bg: #e2f6e5;
|
|
@@ -262,6 +283,63 @@
|
|
|
262
283
|
[data-terminal] [data-tone='magenta'] {
|
|
263
284
|
color: var(--term-magenta);
|
|
264
285
|
}
|
|
286
|
+
[data-terminal] [data-tone='peer'] {
|
|
287
|
+
color: var(--term-peer);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* Work in progress. One element, never a span per character: the transcript
|
|
291
|
+
measures its own rows and supports text selection, and both break the moment
|
|
292
|
+
a line is shredded into glyphs. The tile is periodic (it ends on the colour
|
|
293
|
+
it starts on) and the animation travels exactly one tile width, so the loop
|
|
294
|
+
has no seam. Scoped under `[data-terminal]` to outrank the tone rules, which
|
|
295
|
+
would otherwise repaint the clipped text opaque. */
|
|
296
|
+
[data-terminal] .term-shimmer {
|
|
297
|
+
background-image: linear-gradient(
|
|
298
|
+
90deg,
|
|
299
|
+
var(--term-shimmer-1),
|
|
300
|
+
var(--term-shimmer-2),
|
|
301
|
+
var(--term-shimmer-3),
|
|
302
|
+
var(--term-shimmer-4),
|
|
303
|
+
var(--term-shimmer-5),
|
|
304
|
+
var(--term-shimmer-4),
|
|
305
|
+
var(--term-shimmer-3),
|
|
306
|
+
var(--term-shimmer-2),
|
|
307
|
+
var(--term-shimmer-1)
|
|
308
|
+
);
|
|
309
|
+
background-size: 200% 100%;
|
|
310
|
+
-webkit-background-clip: text;
|
|
311
|
+
background-clip: text;
|
|
312
|
+
-webkit-text-fill-color: transparent;
|
|
313
|
+
color: transparent;
|
|
314
|
+
animation: term-shimmer 3s linear infinite;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@keyframes term-shimmer {
|
|
318
|
+
from {
|
|
319
|
+
background-position: 0% 0;
|
|
320
|
+
}
|
|
321
|
+
to {
|
|
322
|
+
background-position: 200% 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Clipped text with a transparent fill selects invisibly - the highlight paints
|
|
327
|
+
under glyphs that have no colour of their own. */
|
|
328
|
+
[data-terminal] .term-shimmer::selection {
|
|
329
|
+
color: var(--term-bright);
|
|
330
|
+
-webkit-text-fill-color: var(--term-bright);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@media (prefers-reduced-motion: reduce) {
|
|
334
|
+
[data-terminal] .term-shimmer {
|
|
335
|
+
animation: none;
|
|
336
|
+
background-image: none;
|
|
337
|
+
-webkit-background-clip: border-box;
|
|
338
|
+
background-clip: border-box;
|
|
339
|
+
-webkit-text-fill-color: var(--term-shimmer-3);
|
|
340
|
+
color: var(--term-shimmer-3);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
265
343
|
|
|
266
344
|
/* Weight is the only other axis. A terminal has one type size, so emphasis has
|
|
267
345
|
to be carried by weight and colour - never by a larger glyph. */
|
|
@@ -411,6 +489,12 @@
|
|
|
411
489
|
cursor: text;
|
|
412
490
|
}
|
|
413
491
|
|
|
492
|
+
/* A peer's message is not what you typed, so it drops the prompt band: the peer
|
|
493
|
+
tone and the arrow carry it, and the band is reserved for the human's turn. */
|
|
494
|
+
[data-terminal] .term-user[data-peer] {
|
|
495
|
+
background: none;
|
|
496
|
+
}
|
|
497
|
+
|
|
414
498
|
[data-terminal] .term-link {
|
|
415
499
|
text-decoration: underline;
|
|
416
500
|
text-underline-offset: 2px;
|
|
@@ -1130,6 +1214,15 @@ button.term-link {
|
|
|
1130
1214
|
overflow: hidden;
|
|
1131
1215
|
}
|
|
1132
1216
|
|
|
1217
|
+
/* A closed peer send: one row, ellipsised. The row must stay exactly one line
|
|
1218
|
+
tall or the virtualizer's estimate for it is wrong. */
|
|
1219
|
+
.term-clip-1 {
|
|
1220
|
+
display: block;
|
|
1221
|
+
overflow: hidden;
|
|
1222
|
+
white-space: nowrap;
|
|
1223
|
+
text-overflow: ellipsis;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1133
1226
|
.term-scrubber .term-scrub-ex {
|
|
1134
1227
|
display: -webkit-box;
|
|
1135
1228
|
-webkit-line-clamp: 4;
|
package/src/styles/theme.css
CHANGED
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
|
272
272
|
|
|
273
273
|
--cw-font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
274
|
-
--cw-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code', Menlo, Consolas, monospace;
|
|
274
|
+
--cw-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
275
275
|
|
|
276
276
|
--motion-fast: 120ms;
|
|
277
277
|
--motion-base: 150ms;
|