@workerdeck/ui 2.9.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 +54 -7
- package/build/index.mjs +34 -16
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +76 -1
- package/build/{SessionPanel-BClL7zUS.mjs → use-path-links-DO_79-wX.mjs} +834 -303
- package/build/use-path-links-DO_79-wX.mjs.map +1 -0
- package/build/workspace.mjs +3 -83
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/Composer.tsx +7 -4
- package/src/components/agent/PromptTokenText.tsx +11 -3
- package/src/components/agent/SessionInfoDialog.tsx +1 -1
- package/src/components/agent/SessionItem.tsx +13 -8
- package/src/components/agent/SessionList.tsx +1 -1
- package/src/components/agent/SessionPanel.tsx +6 -1
- package/src/components/agent/StatusBar.tsx +1 -1
- package/src/components/agent/Transcript.tsx +83 -79
- package/src/components/agent/TranscriptItemView.tsx +3 -1
- package/src/components/agent/TranscriptRows.tsx +60 -56
- package/src/components/agent/UsageDialog.tsx +101 -7
- package/src/components/agent/composer-commands.ts +14 -2
- package/src/components/agent/pulse.tsx +53 -16
- package/src/components/agent/use-path-links.ts +5 -19
- package/src/components/agent/use-transcript-jumps.ts +25 -0
- package/src/components/prompt-area/clipboard-helpers.ts +13 -4
- package/src/components/prompt-area/dom-helpers.ts +10 -1
- package/src/components/prompt-area/index.ts +10 -1
- package/src/components/prompt-area/prompt-area-engine.ts +23 -14
- package/src/components/prompt-area/types.ts +20 -0
- package/src/components/prompt-area/use-prompt-area-keydown.ts +2 -1
- package/src/components/prompt-area/use-prompt-area.ts +8 -2
- package/src/components/terminal/StatusLine.tsx +3 -2
- 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 +99 -12
- 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/index.ts +3 -1
- package/src/lib/path-match.ts +51 -0
- package/src/lib/session-cost.ts +75 -0
- package/src/lib/tool-icon.ts +5 -1
- package/src/styles/terminal.css +93 -0
- package/src/styles/theme.css +1 -1
- package/build/SessionPanel-BClL7zUS.mjs.map +0 -1
|
@@ -26,6 +26,31 @@ export function repinToBottom(stick: RepinTarget): void {
|
|
|
26
26
|
state.scrollTop = state.calculatedTargetScrollTop
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// A scroller whose height changes clamps `scrollTop`, and the browser reports that clamp as an
|
|
30
|
+
// ordinary scroll event, which the library reads as scroll-up intent and answers by escaping the
|
|
31
|
+
// bottom lock. Its guard against exactly that, `resizeDifference`, is set only by the observer on
|
|
32
|
+
// the *content*, so a composer collapsing after send breaks the lock while the view still sits at
|
|
33
|
+
// the bottom, and the turn's later output streams past unfollowed (GOTCHAS "A scroller resize
|
|
34
|
+
// escapes the bottom lock"). Set the flag ourselves, on the library's schedule. A live send re-pin
|
|
35
|
+
// owns `state.animation` and `scrollToBottom` would clear it, dropping the `ignoreEscapes` hold
|
|
36
|
+
// into the momentum tail, so press that scroll by hand instead.
|
|
37
|
+
export function absorbScrollerResize(stick: RepinTarget, difference: number): void {
|
|
38
|
+
const { state } = stick
|
|
39
|
+
state.resizeDifference = difference
|
|
40
|
+
requestAnimationFrame(() => {
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
if (state.resizeDifference === difference) {
|
|
43
|
+
state.resizeDifference = 0
|
|
44
|
+
}
|
|
45
|
+
}, 1)
|
|
46
|
+
})
|
|
47
|
+
if (state.animation?.ignoreEscapes) {
|
|
48
|
+
state.scrollTop = state.calculatedTargetScrollTop
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
void stick.scrollToBottom('instant')
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
export function useTranscriptJumps(options: {
|
|
30
55
|
rows: TranscriptRow[]
|
|
31
56
|
terminal: boolean
|
|
@@ -6,8 +6,16 @@
|
|
|
6
6
|
* clipboard I/O concerns alongside its DOM traversal and chip-accessor helpers.
|
|
7
7
|
*/
|
|
8
8
|
import type { Segment, ChipSegment } from './types.ts'
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
chipNodeToSegment,
|
|
11
|
+
getChipDisplay,
|
|
12
|
+
getChipSigil,
|
|
13
|
+
getChipTrigger,
|
|
14
|
+
getSelectionRange,
|
|
15
|
+
isChipElement,
|
|
16
|
+
isHTMLElement,
|
|
17
|
+
} from './dom-helpers.ts'
|
|
18
|
+
import { chipPlainText, mergeAdjacentTextSegments } from './prompt-area-engine.ts'
|
|
11
19
|
import { getTextLengthInRange } from './cursor-helpers.ts'
|
|
12
20
|
|
|
13
21
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -63,7 +71,7 @@ export function serializeFragmentToPlainText(fragment: DocumentFragment): string
|
|
|
63
71
|
text += value
|
|
64
72
|
},
|
|
65
73
|
onChip: (node) => {
|
|
66
|
-
text += (getChipTrigger(node) ?? '') + (getChipDisplay(node) ?? '')
|
|
74
|
+
text += (getChipSigil(node) ?? getChipTrigger(node) ?? '') + (getChipDisplay(node) ?? '')
|
|
67
75
|
},
|
|
68
76
|
onBreak: () => {
|
|
69
77
|
text += '\n'
|
|
@@ -129,6 +137,7 @@ export function parseSegmentsFromClipboard(json: string): Segment[] | null {
|
|
|
129
137
|
trigger: item.trigger,
|
|
130
138
|
value: item.value,
|
|
131
139
|
displayText: item.displayText,
|
|
140
|
+
...(typeof item.sigil === 'string' ? { sigil: item.sigil } : {}),
|
|
132
141
|
...(item.data !== undefined ? { data: item.data } : {}),
|
|
133
142
|
...(item.autoResolved ? { autoResolved: true } : {}),
|
|
134
143
|
}
|
|
@@ -173,7 +182,7 @@ export function insertSegmentsAtCursor(currentSegments: Segment[], pastedSegment
|
|
|
173
182
|
|
|
174
183
|
for (const seg of currentSegments) {
|
|
175
184
|
if (seg.type === 'chip') {
|
|
176
|
-
const chipLen = seg.
|
|
185
|
+
const chipLen = chipPlainText(seg).length
|
|
177
186
|
if (offset >= cursorOffset) {
|
|
178
187
|
insertOnce()
|
|
179
188
|
}
|
|
@@ -139,6 +139,13 @@ export function getChipData(node: Node): unknown {
|
|
|
139
139
|
return safeJsonParse(raw)
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
export function getChipSigil(node: Node): string | undefined {
|
|
143
|
+
if (!isChipElement(node)) {
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
return node.dataset.chipSigil
|
|
147
|
+
}
|
|
148
|
+
|
|
142
149
|
/**
|
|
143
150
|
* Length of a chip's plain-text representation (`trigger + displayText`).
|
|
144
151
|
*
|
|
@@ -148,7 +155,7 @@ export function getChipData(node: Node): unknown {
|
|
|
148
155
|
* `textContent` for resilience against externally-mutated nodes.
|
|
149
156
|
*/
|
|
150
157
|
export function chipNodeTextLength(node: HTMLElement): number {
|
|
151
|
-
const trigger = node.dataset.chipTrigger ?? ''
|
|
158
|
+
const trigger = node.dataset.chipSigil ?? node.dataset.chipTrigger ?? ''
|
|
152
159
|
const display = node.dataset.chipDisplay ?? node.textContent ?? ''
|
|
153
160
|
return trigger.length + display.length
|
|
154
161
|
}
|
|
@@ -174,6 +181,7 @@ export function chipNodeToSegment(node: Node): ChipSegment | null {
|
|
|
174
181
|
return null
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
const sigil = getChipSigil(node)
|
|
177
185
|
const data = getChipData(node)
|
|
178
186
|
const autoResolved = getChipAutoResolved(node)
|
|
179
187
|
|
|
@@ -182,6 +190,7 @@ export function chipNodeToSegment(node: Node): ChipSegment | null {
|
|
|
182
190
|
trigger,
|
|
183
191
|
value,
|
|
184
192
|
displayText,
|
|
193
|
+
...(sigil !== undefined ? { sigil } : {}),
|
|
185
194
|
...(data !== undefined ? { data } : {}),
|
|
186
195
|
...(autoResolved ? { autoResolved: true } : {}),
|
|
187
196
|
}
|
|
@@ -7,4 +7,13 @@ export { PromptArea } from './prompt-area.tsx'
|
|
|
7
7
|
export { usePromptAreaState } from './use-prompt-area-state.ts'
|
|
8
8
|
export { commandTrigger, mentionTrigger, hashtagTrigger, launchTrigger } from './trigger-presets.ts'
|
|
9
9
|
export { segmentsToPlainText, plainTextToSegments, isSegmentsEmpty, getChipsByTrigger } from './segment-helpers.ts'
|
|
10
|
-
export type {
|
|
10
|
+
export type {
|
|
11
|
+
ChipOptions,
|
|
12
|
+
PromptAreaHandle,
|
|
13
|
+
PromptAreaProps,
|
|
14
|
+
Segment,
|
|
15
|
+
ChipSegment,
|
|
16
|
+
TextSegment,
|
|
17
|
+
TriggerConfig,
|
|
18
|
+
TriggerSuggestion,
|
|
19
|
+
} from './types.ts'
|
|
@@ -6,9 +6,16 @@ import type { Segment, ChipSegment, TriggerConfig, TriggerPosition, ActiveTrigge
|
|
|
6
6
|
|
|
7
7
|
// Serialization
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* A chip's plain-text form: `{sigil ?? trigger}{displayText}` (e.g., "@Alice").
|
|
11
|
+
*/
|
|
12
|
+
export function chipPlainText(seg: ChipSegment): string {
|
|
13
|
+
return `${seg.sigil ?? seg.trigger}${seg.displayText}`
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Converts an array of segments to a plain text string.
|
|
11
|
-
* Chips are represented
|
|
18
|
+
* Chips are represented by {@link chipPlainText}.
|
|
12
19
|
*/
|
|
13
20
|
export function segmentsToPlainText(segments: Segment[]): string {
|
|
14
21
|
return segments
|
|
@@ -16,7 +23,7 @@ export function segmentsToPlainText(segments: Segment[]): string {
|
|
|
16
23
|
if (seg.type === 'text') {
|
|
17
24
|
return seg.text
|
|
18
25
|
}
|
|
19
|
-
return
|
|
26
|
+
return chipPlainText(seg)
|
|
20
27
|
})
|
|
21
28
|
.join('')
|
|
22
29
|
}
|
|
@@ -45,7 +52,7 @@ export function truncateSegmentsToLength(segments: Segment[], maxLength: number)
|
|
|
45
52
|
const result: Segment[] = []
|
|
46
53
|
let length = 0
|
|
47
54
|
for (const seg of segments) {
|
|
48
|
-
const segLength = seg.type === 'text' ? seg.text.length : seg.
|
|
55
|
+
const segLength = seg.type === 'text' ? seg.text.length : chipPlainText(seg).length
|
|
49
56
|
if (length + segLength <= maxLength) {
|
|
50
57
|
result.push(seg)
|
|
51
58
|
length += segLength
|
|
@@ -195,7 +202,8 @@ export function detectActiveTrigger(text: string, cursorPos: number, triggers: T
|
|
|
195
202
|
export function resolveChip(
|
|
196
203
|
segments: Segment[],
|
|
197
204
|
activeTrigger: ActiveTrigger,
|
|
198
|
-
chip: { value: string; displayText: string; data?: unknown; autoResolved?: boolean },
|
|
205
|
+
chip: { value: string; displayText: string; sigil?: string; data?: unknown; autoResolved?: boolean },
|
|
206
|
+
trailingText = ' ',
|
|
199
207
|
): { segments: Segment[]; cursorOffset: number } {
|
|
200
208
|
const triggerStart = activeTrigger.startOffset
|
|
201
209
|
const triggerEnd = triggerStart + 1 + activeTrigger.query.length // +1 for trigger char
|
|
@@ -205,7 +213,7 @@ export function resolveChip(
|
|
|
205
213
|
|
|
206
214
|
for (const seg of segments) {
|
|
207
215
|
if (seg.type === 'chip') {
|
|
208
|
-
const chipText =
|
|
216
|
+
const chipText = chipPlainText(seg)
|
|
209
217
|
const chipStart = offset
|
|
210
218
|
const chipEnd = offset + chipText.length
|
|
211
219
|
|
|
@@ -239,16 +247,17 @@ export function resolveChip(
|
|
|
239
247
|
trigger: activeTrigger.config.char,
|
|
240
248
|
value: chip.value,
|
|
241
249
|
displayText: chip.displayText,
|
|
250
|
+
...(chip.sigil !== undefined ? { sigil: chip.sigil } : {}),
|
|
242
251
|
...(chip.data !== undefined ? { data: chip.data } : {}),
|
|
243
252
|
...(chip.autoResolved ? { autoResolved: true } : {}),
|
|
244
253
|
}
|
|
245
254
|
newSegments.push(newChip)
|
|
246
255
|
|
|
247
|
-
// Add trailing space after chip, then any remaining text
|
|
256
|
+
// Add the trailing text (a space by default) after the chip, then any remaining text
|
|
248
257
|
if (afterText) {
|
|
249
|
-
newSegments.push({ type: 'text', text:
|
|
258
|
+
newSegments.push({ type: 'text', text: trailingText + afterText.replace(/^\s/, '') })
|
|
250
259
|
} else {
|
|
251
|
-
newSegments.push({ type: 'text', text:
|
|
260
|
+
newSegments.push({ type: 'text', text: trailingText })
|
|
252
261
|
}
|
|
253
262
|
}
|
|
254
263
|
|
|
@@ -266,14 +275,14 @@ export function resolveChip(
|
|
|
266
275
|
if (seg.type === 'text') {
|
|
267
276
|
runningOffset += seg.text.length
|
|
268
277
|
} else {
|
|
269
|
-
runningOffset += seg.
|
|
278
|
+
runningOffset += chipPlainText(seg).length
|
|
270
279
|
if (seg.value === chip.value && seg.displayText === chip.displayText && seg.trigger === activeTrigger.config.char) {
|
|
271
280
|
lastChipEndOffset = runningOffset
|
|
272
281
|
}
|
|
273
282
|
}
|
|
274
283
|
}
|
|
275
|
-
//
|
|
276
|
-
const cursorOffset = lastChipEndOffset === -1 ? runningOffset : lastChipEndOffset +
|
|
284
|
+
// The caret lands after the chip and its trailing text
|
|
285
|
+
const cursorOffset = lastChipEndOffset === -1 ? runningOffset : lastChipEndOffset + trailingText.length
|
|
277
286
|
|
|
278
287
|
return { segments: merged, cursorOffset }
|
|
279
288
|
}
|
|
@@ -302,7 +311,7 @@ export function resolveText(
|
|
|
302
311
|
|
|
303
312
|
for (const seg of segments) {
|
|
304
313
|
if (seg.type === 'chip') {
|
|
305
|
-
const chipEnd = offset +
|
|
314
|
+
const chipEnd = offset + chipPlainText(seg).length
|
|
306
315
|
// A trigger range can never overlap a chip - chips are atomic - so a chip
|
|
307
316
|
// is either wholly before or wholly after, and is kept either way.
|
|
308
317
|
if (chipEnd <= triggerStart || offset >= triggerEnd) {
|
|
@@ -377,7 +386,7 @@ export function revertChipAtIndex(segments: Segment[], index: number): { segment
|
|
|
377
386
|
return null
|
|
378
387
|
}
|
|
379
388
|
|
|
380
|
-
const revertedText =
|
|
389
|
+
const revertedText = chipPlainText(seg)
|
|
381
390
|
const result = [...segments.slice(0, index), { type: 'text' as const, text: revertedText }, ...segments.slice(index + 1)]
|
|
382
391
|
return { segments: mergeAdjacentTextSegments(result), revertedText }
|
|
383
392
|
}
|
|
@@ -491,7 +500,7 @@ export function replaceTextRange(segments: Segment[], start: number, end: number
|
|
|
491
500
|
|
|
492
501
|
for (const seg of segments) {
|
|
493
502
|
if (seg.type === 'chip') {
|
|
494
|
-
const chipText =
|
|
503
|
+
const chipText = chipPlainText(seg)
|
|
495
504
|
const chipStart = offset
|
|
496
505
|
const chipEnd = offset + chipText.length
|
|
497
506
|
|
|
@@ -23,6 +23,12 @@ export type ChipSegment = {
|
|
|
23
23
|
value: string
|
|
24
24
|
/** The display text shown in the chip */
|
|
25
25
|
displayText: string
|
|
26
|
+
/**
|
|
27
|
+
* The character the chip serialises and renders with, when it differs from
|
|
28
|
+
* `trigger`. The trigger is the menu the chip came from; the sigil is the
|
|
29
|
+
* token the host parses. Unset, the trigger character is the sigil.
|
|
30
|
+
*/
|
|
31
|
+
sigil?: string
|
|
26
32
|
/** Optional data payload attached to the chip */
|
|
27
33
|
data?: unknown
|
|
28
34
|
/**
|
|
@@ -74,6 +80,11 @@ export type TriggerSuggestion = {
|
|
|
74
80
|
data?: unknown
|
|
75
81
|
}
|
|
76
82
|
|
|
83
|
+
export type ChipOptions = {
|
|
84
|
+
sigil?: string
|
|
85
|
+
trailingText?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
/**
|
|
78
89
|
* Configuration for a trigger character.
|
|
79
90
|
*/
|
|
@@ -113,6 +124,15 @@ export type TriggerConfig = {
|
|
|
113
124
|
* `onChipAdd` does not fire either.
|
|
114
125
|
*/
|
|
115
126
|
insertAsText?: (suggestion: TriggerSuggestion) => string | undefined
|
|
127
|
+
/**
|
|
128
|
+
* For 'dropdown' mode: per-suggestion overrides for the chip a selection
|
|
129
|
+
* resolves to. `sigil` replaces the trigger character in the chip's text and
|
|
130
|
+
* serialisation (a `/` menu inserting a `$name` token); `trailingText` is
|
|
131
|
+
* the editable text placed after the chip in place of the default single
|
|
132
|
+
* space (it is inserted verbatim, so include the leading space). Return
|
|
133
|
+
* undefined for the plain chip.
|
|
134
|
+
*/
|
|
135
|
+
chipOptions?: (suggestion: TriggerSuggestion) => ChipOptions | undefined
|
|
116
136
|
/**
|
|
117
137
|
* For 'callback' and 'launch' modes: called when the trigger is activated.
|
|
118
138
|
* Receives the full input text and cursor position. For 'launch' it fires on
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
revertChipAtIndex,
|
|
11
11
|
replaceTextRange,
|
|
12
12
|
toggleMarkdownWrap,
|
|
13
|
+
chipPlainText,
|
|
13
14
|
} from './prompt-area-engine.ts'
|
|
14
15
|
import {
|
|
15
16
|
getListContext,
|
|
@@ -138,7 +139,7 @@ export function usePromptAreaKeydown({
|
|
|
138
139
|
if (s.type === 'text') {
|
|
139
140
|
targetOffset += s.text.length
|
|
140
141
|
} else {
|
|
141
|
-
targetOffset += s.
|
|
142
|
+
targetOffset += chipPlainText(s).length
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
targetOffset += result.revertedText.length
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resolveChip,
|
|
13
13
|
resolveText,
|
|
14
14
|
truncateSegmentsToLength,
|
|
15
|
+
chipPlainText,
|
|
15
16
|
} from './prompt-area-engine.ts'
|
|
16
17
|
import {
|
|
17
18
|
autoFormatListPrefix,
|
|
@@ -223,6 +224,9 @@ export function usePromptArea({
|
|
|
223
224
|
chip.dataset.chipTrigger = seg.trigger
|
|
224
225
|
chip.dataset.chipValue = seg.value
|
|
225
226
|
chip.dataset.chipDisplay = seg.displayText
|
|
227
|
+
if (seg.sigil !== undefined) {
|
|
228
|
+
chip.dataset.chipSigil = seg.sigil
|
|
229
|
+
}
|
|
226
230
|
if (seg.data !== undefined) {
|
|
227
231
|
const json = safeJsonStringify(seg.data)
|
|
228
232
|
if (json) {
|
|
@@ -236,7 +240,7 @@ export function usePromptArea({
|
|
|
236
240
|
const chipStyle = triggerConfig?.chipStyle ?? 'pill'
|
|
237
241
|
chip.dataset.chipStyle = chipStyle
|
|
238
242
|
chip.className = cn('prompt-area-chip', chipStyle === 'inline' && 'prompt-area-chip--inline', triggerConfig?.chipClassName)
|
|
239
|
-
chip.textContent =
|
|
243
|
+
chip.textContent = chipPlainText(seg)
|
|
240
244
|
chip.setAttribute('role', 'button')
|
|
241
245
|
chip.setAttribute('tabindex', '-1')
|
|
242
246
|
editor.appendChild(chip)
|
|
@@ -598,10 +602,12 @@ export function usePromptArea({
|
|
|
598
602
|
}
|
|
599
603
|
|
|
600
604
|
const displayText = activeTrigger.config.onSelect?.(suggestion) ?? suggestion.label
|
|
605
|
+
const options = activeTrigger.config.chipOptions?.(suggestion)
|
|
601
606
|
|
|
602
607
|
const chipData = {
|
|
603
608
|
value: suggestion.value,
|
|
604
609
|
displayText: displayText || suggestion.label,
|
|
610
|
+
...(options?.sigil !== undefined ? { sigil: options.sigil } : {}),
|
|
605
611
|
data: suggestion.data,
|
|
606
612
|
}
|
|
607
613
|
|
|
@@ -616,7 +622,7 @@ export function usePromptArea({
|
|
|
616
622
|
}, 0)
|
|
617
623
|
return
|
|
618
624
|
}
|
|
619
|
-
const result = resolveChip(segments, activeTrigger, chipData)
|
|
625
|
+
const result = resolveChip(segments, activeTrigger, chipData, options?.trailingText)
|
|
620
626
|
|
|
621
627
|
onChange(result.segments)
|
|
622
628
|
renderSegmentsToDOM(result.segments)
|
|
@@ -75,10 +75,11 @@ export function TerminalStatusLine({
|
|
|
75
75
|
)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
const costUsd = state.costUsd ?? (state.totalCostUsd || undefined)
|
|
79
|
+
if (costUsd !== undefined && costUsd > 0) {
|
|
79
80
|
parts.push(
|
|
80
81
|
<Ink key="cost" tone="faint">
|
|
81
|
-
{formatCost(
|
|
82
|
+
{formatCost(costUsd)}
|
|
82
83
|
</Ink>,
|
|
83
84
|
)
|
|
84
85
|
}
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
CompactionRow,
|
|
9
9
|
FileRow,
|
|
10
10
|
NoticeRow,
|
|
11
|
-
|
|
11
|
+
PeerSendRow,
|
|
12
|
+
RunRow,
|
|
12
13
|
ThinkingRow,
|
|
13
14
|
ToolRow,
|
|
14
15
|
TurnResultRow,
|
|
@@ -17,8 +18,9 @@ import {
|
|
|
17
18
|
} from './items.tsx'
|
|
18
19
|
import { blockNeedsBlank, taskChildItems, terminalBlocks, type TaskBlock } from './blocks.ts'
|
|
19
20
|
import { usePulse } from '../agent/pulse.tsx'
|
|
21
|
+
import { PeerNamesProvider } from './peer-names.tsx'
|
|
20
22
|
import { Pressable, useRevealOnOpen } from './press.tsx'
|
|
21
|
-
import { taskBrief, taskBusy, taskFailed, taskSummary } from './tool-run.ts'
|
|
23
|
+
import { isPeerSend, taskBrief, taskBusy, taskFailed, taskSummary } from './tool-run.ts'
|
|
22
24
|
import { BRIEF_LINES } from './height.ts'
|
|
23
25
|
import { Blank, Row } from './row.tsx'
|
|
24
26
|
import { TerminalSurface } from './surface.tsx'
|
|
@@ -44,7 +46,7 @@ export function TerminalItemView({ item, fileUrl }: { item: TranscriptItem; file
|
|
|
44
46
|
return <ThinkingRow item={item} />
|
|
45
47
|
}
|
|
46
48
|
case 'tool_call': {
|
|
47
|
-
return <ToolRow item={item} />
|
|
49
|
+
return isPeerSend(item) ? <PeerSendRow item={item} /> : <ToolRow item={item} />
|
|
48
50
|
}
|
|
49
51
|
case 'turn_result': {
|
|
50
52
|
return <TurnResultRow item={item} />
|
|
@@ -110,7 +112,7 @@ export function TaskRow({
|
|
|
110
112
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
111
113
|
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
112
114
|
<Row glyph={busy ? pulse : '●'} glyphTone={failed ? 'red' : busy ? 'mark' : 'dim'} tone={failed ? 'red' : 'green'}>
|
|
113
|
-
{taskSummary(block.task, children)}
|
|
115
|
+
{busy ? <span className="term-shimmer">{taskSummary(block.task, children)}</span> : taskSummary(block.task, children)}
|
|
114
116
|
</Row>
|
|
115
117
|
</Pressable>
|
|
116
118
|
{open ? (
|
|
@@ -119,7 +121,7 @@ export function TaskRow({
|
|
|
119
121
|
{block.children.map((leaf, index) => (
|
|
120
122
|
<Fragment key={leaf.key}>
|
|
121
123
|
{index > 0 && blockNeedsBlank(block.children[index - 1]!, leaf) ? <Blank /> : null}
|
|
122
|
-
{'run' in leaf ? <
|
|
124
|
+
{'run' in leaf ? <RunRow items={leaf.run} /> : <TerminalItemView item={leaf.item} fileUrl={fileUrl} />}
|
|
123
125
|
</Fragment>
|
|
124
126
|
))}
|
|
125
127
|
</div>
|
|
@@ -170,28 +172,30 @@ export function TerminalTranscript({ state, fileUrl, fontSize, lineHeight, affor
|
|
|
170
172
|
bleed="1ch"
|
|
171
173
|
className={cn('term-transcript', className)}
|
|
172
174
|
>
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
{
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
175
|
+
<PeerNamesProvider items={state.items}>
|
|
176
|
+
{blocks.map((block, index) => (
|
|
177
|
+
<Fragment key={block.key}>
|
|
178
|
+
{index > 0 && blockNeedsBlank(blocks[index - 1]!, block) ? <Blank /> : null}
|
|
179
|
+
{'run' in block ? (
|
|
180
|
+
<RunRow items={block.run} />
|
|
181
|
+
) : 'item' in block ? (
|
|
182
|
+
<TerminalItemView item={block.item} fileUrl={fileUrl} />
|
|
183
|
+
) : (
|
|
184
|
+
<TaskRow block={block} fileUrl={fileUrl} />
|
|
185
|
+
)}
|
|
186
|
+
</Fragment>
|
|
187
|
+
))}
|
|
188
|
+
{working(state) ? (
|
|
189
|
+
<>
|
|
190
|
+
{state.items.length > 0 ? <Blank /> : null}
|
|
191
|
+
<WorkingRow
|
|
192
|
+
label={state.status === 'starting' ? 'Starting…' : 'Working…'}
|
|
193
|
+
startedAt={runStartedAt}
|
|
194
|
+
tokens={state.contextUsage?.totalTokens}
|
|
195
|
+
/>
|
|
196
|
+
</>
|
|
197
|
+
) : null}
|
|
198
|
+
</PeerNamesProvider>
|
|
195
199
|
</TerminalSurface>
|
|
196
200
|
)
|
|
197
201
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
2
|
-
import { foldsTogether } from './tool-run.ts'
|
|
2
|
+
import { foldsTogether, isPeerSend } from './tool-run.ts'
|
|
3
3
|
|
|
4
4
|
export type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
|
|
5
5
|
|
|
@@ -42,7 +42,7 @@ export function taskChildItems(block: TaskBlock): TranscriptItem[] {
|
|
|
42
42
|
|
|
43
43
|
function pushLeaf(out: LeafBlock[], item: TranscriptItem, index: number): void {
|
|
44
44
|
const previous = out.at(-1)
|
|
45
|
-
if (isRunCall(item)) {
|
|
45
|
+
if (isRunCall(item) && !isPeerSend(item)) {
|
|
46
46
|
if (previous && 'run' in previous && foldsTogether(previous.run[0]!, item)) {
|
|
47
47
|
previous.run.push(item)
|
|
48
48
|
previous.indices.push(index)
|
|
@@ -112,13 +112,21 @@ export function terminalBlocks(items: readonly TranscriptItem[], offset = 0, fol
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export function needsBlank(previous: TranscriptItem, next: TranscriptItem): boolean {
|
|
115
|
-
if (previous
|
|
116
|
-
return
|
|
115
|
+
if (isPeerSend(previous) || isPeerSend(next)) {
|
|
116
|
+
return true
|
|
117
117
|
}
|
|
118
|
-
return
|
|
118
|
+
return !(previous.kind === 'tool_call' && next.kind === 'tool_call')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function blockKind(block: TerminalBlock): string {
|
|
122
|
+
if (!('item' in block)) {
|
|
123
|
+
return 'tool_call'
|
|
124
|
+
}
|
|
125
|
+
// A peer send is a tool call on the wire and a message on the page, so it takes the blank line
|
|
126
|
+
// every other message gets rather than sitting flush against the run above it.
|
|
127
|
+
return isPeerSend(block.item) ? 'peer_send' : block.item.kind
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
export function blockNeedsBlank(previous: TerminalBlock, next: TerminalBlock): boolean {
|
|
122
|
-
|
|
123
|
-
return !(kind(previous) === 'tool_call' && kind(next) === 'tool_call')
|
|
131
|
+
return !(blockKind(previous) === 'tool_call' && blockKind(next) === 'tool_call')
|
|
124
132
|
}
|
|
@@ -5,7 +5,7 @@ import { taskChildItems, type TerminalBlock, type ToolCallItem } from './blocks.
|
|
|
5
5
|
import { IMAGE_BOX_LINES } from './image-box.ts'
|
|
6
6
|
import { collapsedResult } from './result-preview.ts'
|
|
7
7
|
import { todoLine, todoPreview } from './todos.ts'
|
|
8
|
-
import { runSummary, taskSummary } from './tool-run.ts'
|
|
8
|
+
import { isPeerSend, peerName, planRun, runSummary, taskSummary } from './tool-run.ts'
|
|
9
9
|
|
|
10
10
|
export type CellMetrics = {
|
|
11
11
|
width: number
|
|
@@ -595,8 +595,9 @@ export function itemHeight(item: TranscriptItem, m: CellMetrics): ComputedHeight
|
|
|
595
595
|
acc = add(acc, rowH(item.attachments.map((a) => a.name).join(', '), m, { extraPx }))
|
|
596
596
|
}
|
|
597
597
|
if (item.text) {
|
|
598
|
-
|
|
599
|
-
|
|
598
|
+
const who = item.origin ? `${peerName(item.origin)}: ` : ''
|
|
599
|
+
for (const [index, line] of item.text.split('\n').entries()) {
|
|
600
|
+
acc = add(acc, rowH((index === 0 ? who : '') + (line || ' '), m, { extraPx }))
|
|
600
601
|
}
|
|
601
602
|
}
|
|
602
603
|
return acc
|
|
@@ -608,6 +609,10 @@ export function itemHeight(item: TranscriptItem, m: CellMetrics): ComputedHeight
|
|
|
608
609
|
return rowH(item.text, m, { extraPx })
|
|
609
610
|
}
|
|
610
611
|
case 'tool_call': {
|
|
612
|
+
// A closed peer send is exactly one ellipsised row, whatever the message's length.
|
|
613
|
+
if (isPeerSend(item)) {
|
|
614
|
+
return { px: m.line + extraPx, exact: true }
|
|
615
|
+
}
|
|
611
616
|
return toolRowHeight(item, m, extraPx)
|
|
612
617
|
}
|
|
613
618
|
case 'turn_result': {
|
|
@@ -640,8 +645,12 @@ export function blockHeight(block: TerminalBlock, m: CellMetrics): ComputedHeigh
|
|
|
640
645
|
return rowH(taskSummary(block.task, taskChildItems(block)), m)
|
|
641
646
|
}
|
|
642
647
|
if ('run' in block) {
|
|
643
|
-
const
|
|
644
|
-
|
|
648
|
+
const plan = planRun(block.run)
|
|
649
|
+
if (plan.kind === 'call') {
|
|
650
|
+
return itemHeight(plan.item, m)
|
|
651
|
+
}
|
|
652
|
+
const busy = plan.items.some((item) => item.status === 'running' || item.status === 'pending')
|
|
653
|
+
return rowH(runSummary(plan.items, busy), m)
|
|
645
654
|
}
|
|
646
655
|
return itemHeight(block.item, m)
|
|
647
656
|
}
|