@workerdeck/ui 3.0.1 → 3.1.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-C1bU4yTM.d.mts → SessionPanel-BNNc8grK.d.mts} +9 -7
- package/build/index.d.mts +153 -45
- package/build/index.mjs +501 -226
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -6
- package/build/{use-path-links-BEWxt0ZM.mjs → use-path-links-C6Fh5zdx.mjs} +1926 -2086
- package/build/use-path-links-C6Fh5zdx.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/ApprovalPrompts.tsx +87 -0
- package/src/components/agent/Composer.tsx +25 -47
- package/src/components/agent/ContextDialog.tsx +75 -67
- package/src/components/agent/FrameStrip.tsx +74 -0
- package/src/components/agent/HostFilesDialog.tsx +3 -2
- package/src/components/agent/McpDialog.tsx +112 -78
- package/src/components/agent/PermissionPrompt.tsx +13 -22
- package/src/components/agent/QuestionPrompt.tsx +6 -41
- package/src/components/agent/Scrubber.tsx +6 -75
- package/src/components/agent/SessionBrowser.tsx +125 -214
- package/src/components/agent/SessionFilters.tsx +266 -0
- package/src/components/agent/SessionInfoDialog.tsx +47 -63
- package/src/components/agent/SessionItem.tsx +27 -7
- package/src/components/agent/SessionPanel.tsx +244 -488
- package/src/components/agent/SessionSteps.tsx +54 -11
- package/src/components/agent/ShellStrip.tsx +27 -43
- package/src/components/agent/SubagentStrip.tsx +13 -30
- package/src/components/agent/Transcript.tsx +13 -0
- package/src/components/agent/TranscriptItemView.tsx +6 -36
- package/src/components/agent/composer-commands.ts +80 -1
- package/src/components/agent/markdown-image.tsx +49 -1
- package/src/components/agent/scrubber-marks.ts +33 -11
- package/src/components/agent/scrubber-peek.tsx +122 -0
- package/src/components/agent/session-panel-providers.tsx +41 -0
- package/src/components/agent/shell-actions.tsx +36 -1
- package/src/components/agent/use-catch-up.ts +20 -0
- package/src/components/agent/use-frame.ts +88 -0
- package/src/components/agent/use-host-image.ts +81 -0
- package/src/components/agent/use-shell-frame.ts +12 -70
- package/src/components/agent/use-subagent-frame.ts +19 -75
- package/src/components/prompt-area/prompt-area-engine.ts +21 -20
- package/src/components/prompt-area/prompt-area-list-ops.ts +12 -15
- package/src/components/terminal/PermissionPrompt.tsx +19 -32
- package/src/components/terminal/QuestionPrompt.tsx +10 -46
- package/src/components/terminal/TerminalTranscript.tsx +3 -86
- package/src/components/terminal/items.tsx +5 -43
- package/src/components/terminal/scrubber.tsx +25 -272
- package/src/components/ui/Popover.tsx +29 -0
- package/src/components/ui/Select.tsx +25 -0
- package/src/index.ts +46 -8
- package/src/lib/file-link.ts +3 -16
- package/src/lib/permission-prompt.ts +57 -0
- package/src/lib/question-answers.ts +65 -0
- package/build/use-path-links-BEWxt0ZM.mjs.map +0 -1
- package/src/components/prompt-area/use-markdown-mode.ts +0 -103
|
@@ -210,8 +210,10 @@ export function resolveChip(
|
|
|
210
210
|
|
|
211
211
|
const newSegments: Segment[] = []
|
|
212
212
|
let offset = 0
|
|
213
|
+
let insertedChip: ChipSegment | undefined
|
|
213
214
|
|
|
214
|
-
|
|
215
|
+
// Merged first: the trigger range is typed text, so it then lies in exactly one text segment.
|
|
216
|
+
for (const seg of mergeAdjacentTextSegments(segments)) {
|
|
215
217
|
if (seg.type === 'chip') {
|
|
216
218
|
const chipText = chipPlainText(seg)
|
|
217
219
|
const chipStart = offset
|
|
@@ -252,6 +254,7 @@ export function resolveChip(
|
|
|
252
254
|
...(chip.autoResolved ? { autoResolved: true } : {}),
|
|
253
255
|
}
|
|
254
256
|
newSegments.push(newChip)
|
|
257
|
+
insertedChip = newChip
|
|
255
258
|
|
|
256
259
|
// Add the trailing text (a space by default) after the chip, then any remaining text
|
|
257
260
|
if (afterText) {
|
|
@@ -266,23 +269,10 @@ export function resolveChip(
|
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
const merged = mergeAdjacentTextSegments(newSegments)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let runningOffset = 0
|
|
274
|
-
for (const seg of merged) {
|
|
275
|
-
if (seg.type === 'text') {
|
|
276
|
-
runningOffset += seg.text.length
|
|
277
|
-
} else {
|
|
278
|
-
runningOffset += chipPlainText(seg).length
|
|
279
|
-
if (seg.value === chip.value && seg.displayText === chip.displayText && seg.trigger === activeTrigger.config.char) {
|
|
280
|
-
lastChipEndOffset = runningOffset
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
// The caret lands after the chip and its trailing text
|
|
285
|
-
const cursorOffset = lastChipEndOffset === -1 ? runningOffset : lastChipEndOffset + trailingText.length
|
|
272
|
+
// Everything before the trigger is untouched, so the new chip starts exactly where the trigger did.
|
|
273
|
+
const cursorOffset = insertedChip
|
|
274
|
+
? triggerStart + chipPlainText(insertedChip).length + trailingText.length
|
|
275
|
+
: segmentsToPlainText(merged).length
|
|
286
276
|
|
|
287
277
|
return { segments: merged, cursorOffset }
|
|
288
278
|
}
|
|
@@ -309,7 +299,7 @@ export function resolveText(
|
|
|
309
299
|
let offset = 0
|
|
310
300
|
let cursorOffset = triggerStart + text.length
|
|
311
301
|
|
|
312
|
-
for (const seg of segments) {
|
|
302
|
+
for (const seg of mergeAdjacentTextSegments(segments)) {
|
|
313
303
|
if (seg.type === 'chip') {
|
|
314
304
|
const chipEnd = offset + chipPlainText(seg).length
|
|
315
305
|
// A trigger range can never overlap a chip - chips are atomic - so a chip
|
|
@@ -510,8 +500,18 @@ export function replaceTextRange(segments: Segment[], start: number, end: number
|
|
|
510
500
|
inserted = true
|
|
511
501
|
}
|
|
512
502
|
|
|
513
|
-
if (
|
|
503
|
+
if (start === end) {
|
|
514
504
|
newSegments.push(seg)
|
|
505
|
+
// A collapsed insertion can never split an atomic chip, so one inside it lands just after.
|
|
506
|
+
if (!inserted && chipStart < start && start < chipEnd) {
|
|
507
|
+
newSegments.push({ type: 'text', text: replacement })
|
|
508
|
+
inserted = true
|
|
509
|
+
}
|
|
510
|
+
} else if (chipEnd <= start || chipStart >= end) {
|
|
511
|
+
newSegments.push(seg)
|
|
512
|
+
} else if (!inserted) {
|
|
513
|
+
newSegments.push({ type: 'text', text: replacement })
|
|
514
|
+
inserted = true
|
|
515
515
|
}
|
|
516
516
|
offset = chipEnd
|
|
517
517
|
} else {
|
|
@@ -709,6 +709,7 @@ export function segmentsEqual(a: Segment[], b: Segment[]): boolean {
|
|
|
709
709
|
sa.trigger !== sb.trigger ||
|
|
710
710
|
sa.value !== sb.value ||
|
|
711
711
|
sa.displayText !== sb.displayText ||
|
|
712
|
+
sa.sigil !== sb.sigil ||
|
|
712
713
|
sa.autoResolved !== sb.autoResolved
|
|
713
714
|
) {
|
|
714
715
|
return false
|
|
@@ -304,28 +304,25 @@ export function normalizeListPrefixText(text: string, markdownEnabled: boolean):
|
|
|
304
304
|
* preserved, while an unterminated fence does not suppress later bullets.
|
|
305
305
|
*/
|
|
306
306
|
export function normalizeListPrefixes(segments: Segment[], markdownEnabled: boolean): Segment[] {
|
|
307
|
-
const
|
|
308
|
-
segments.forEach((seg) => {
|
|
309
|
-
if (seg.type === 'text') {
|
|
310
|
-
globalLines.push(...seg.text.split('\n'))
|
|
311
|
-
}
|
|
312
|
-
})
|
|
313
|
-
const protectedLines = fenceProtectedLineIndices(globalLines)
|
|
307
|
+
const protectedLines = fenceProtectedLineIndices(segmentsToPlainText(segments).split('\n'))
|
|
314
308
|
|
|
315
|
-
let
|
|
309
|
+
let line = 0
|
|
310
|
+
// A chip is inline, so text right after one continues its line rather than starting a new one.
|
|
311
|
+
let atLineStart = true
|
|
316
312
|
let changed = false
|
|
317
313
|
const result = segments.map((seg) => {
|
|
318
314
|
if (seg.type !== 'text') {
|
|
315
|
+
atLineStart = false
|
|
319
316
|
return seg
|
|
320
317
|
}
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
.map((
|
|
324
|
-
const out = protectedLines.has(globalIndex) ? line : swapListPrefixLine(line, markdownEnabled)
|
|
325
|
-
globalIndex++
|
|
326
|
-
return out
|
|
327
|
-
})
|
|
318
|
+
const pieces = seg.text.split('\n')
|
|
319
|
+
const newText = pieces
|
|
320
|
+
.map((piece, k) => ((k > 0 || atLineStart) && !protectedLines.has(line + k) ? swapListPrefixLine(piece, markdownEnabled) : piece))
|
|
328
321
|
.join('\n')
|
|
322
|
+
line += pieces.length - 1
|
|
323
|
+
if (pieces.length > 1 || seg.text !== '') {
|
|
324
|
+
atLineStart = pieces[pieces.length - 1] === ''
|
|
325
|
+
}
|
|
329
326
|
if (newText === seg.text) {
|
|
330
327
|
return seg
|
|
331
328
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import type { PermissionRequest } from '@workerdeck/protocol'
|
|
3
3
|
import { toolInputPreview } from '../../lib/format.ts'
|
|
4
|
-
import {
|
|
5
|
-
import { shellRequestLines
|
|
4
|
+
import { permissionPromptModel } from '../../lib/permission-prompt.ts'
|
|
5
|
+
import { shellRequestLines } from '../../lib/shell-request.ts'
|
|
6
6
|
import { TerminalDiff, previewPatch } from './diff.tsx'
|
|
7
7
|
import { TerminalMarkdown } from './markdown.tsx'
|
|
8
8
|
import { Choices, Hint, PromptInput, PromptTitle, Rule } from './prompt.tsx'
|
|
@@ -29,15 +29,10 @@ export function TerminalPermissionPrompt({ request, onApprove, onDeny, className
|
|
|
29
29
|
setDenying(false)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
32
|
+
const model = permissionPromptModel(request)
|
|
33
|
+
const { plan, shell, heading } = model
|
|
34
34
|
const patch = plan || shell ? undefined : previewPatch(request.input)
|
|
35
35
|
const summary = plan || shell ? '' : toolInputPreview(request.input)
|
|
36
|
-
const heading = plan
|
|
37
|
-
? 'Plan ready for review'
|
|
38
|
-
: shell
|
|
39
|
-
? shellRequestTitle(shell)
|
|
40
|
-
: (request.displayName ?? request.title ?? 'Permission needed')
|
|
41
36
|
const subject = patch?.path ?? (summary || undefined)
|
|
42
37
|
|
|
43
38
|
const reasonInput = denying ? (
|
|
@@ -46,25 +41,16 @@ export function TerminalPermissionPrompt({ request, onApprove, onDeny, className
|
|
|
46
41
|
onChange={setReason}
|
|
47
42
|
onSubmit={() => deny(false)}
|
|
48
43
|
onCancel={() => setDenying(false)}
|
|
49
|
-
placeholder={
|
|
50
|
-
plan
|
|
51
|
-
? 'What should change? (optional) - the agent keeps planning and reads this'
|
|
52
|
-
: 'Reason (optional) - the agent reads this and can try something else'
|
|
53
|
-
}
|
|
44
|
+
placeholder={model.denyPlaceholder}
|
|
54
45
|
/>
|
|
55
46
|
) : undefined
|
|
56
47
|
|
|
57
|
-
const options =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: [
|
|
64
|
-
{ key: 'allow', label: 'Yes' },
|
|
65
|
-
{ key: 'deny', label: 'No, and tell the agent what to do differently', detail: reasonInput },
|
|
66
|
-
{ key: 'stop', label: 'No, and stop the turn', danger: true },
|
|
67
|
-
]
|
|
48
|
+
const options = model.choices.map((choice) => ({
|
|
49
|
+
key: choice.key,
|
|
50
|
+
label: choice.option,
|
|
51
|
+
danger: choice.danger,
|
|
52
|
+
detail: choice.key === 'deny' ? reasonInput : undefined,
|
|
53
|
+
}))
|
|
68
54
|
|
|
69
55
|
return (
|
|
70
56
|
<div
|
|
@@ -104,14 +90,14 @@ export function TerminalPermissionPrompt({ request, onApprove, onDeny, className
|
|
|
104
90
|
<Blank />
|
|
105
91
|
<Rule dashed />
|
|
106
92
|
</>
|
|
107
|
-
) :
|
|
93
|
+
) : model.description ? (
|
|
108
94
|
<>
|
|
109
|
-
<Row tone="dim">{
|
|
95
|
+
<Row tone="dim">{model.description}</Row>
|
|
110
96
|
<Blank />
|
|
111
97
|
</>
|
|
112
98
|
) : null}
|
|
113
|
-
{
|
|
114
|
-
<Row>{
|
|
99
|
+
{model.decisionReason ? <Row tone="faint">{model.decisionReason}</Row> : null}
|
|
100
|
+
<Row>{model.question}</Row>
|
|
115
101
|
<Choices
|
|
116
102
|
label={heading}
|
|
117
103
|
options={options}
|
|
@@ -119,11 +105,12 @@ export function TerminalPermissionPrompt({ request, onApprove, onDeny, className
|
|
|
119
105
|
onFocus={setFocused}
|
|
120
106
|
active={!denying}
|
|
121
107
|
onChoose={(index) => {
|
|
122
|
-
|
|
108
|
+
const key = model.choices[index]?.key
|
|
109
|
+
if (key === 'allow') {
|
|
123
110
|
onApprove(request.id)
|
|
124
|
-
} else if (
|
|
111
|
+
} else if (key === 'deny') {
|
|
125
112
|
setDenying(true)
|
|
126
|
-
} else {
|
|
113
|
+
} else if (key === 'stop') {
|
|
127
114
|
deny(true)
|
|
128
115
|
}
|
|
129
116
|
}}
|
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import type { PermissionRequest, UserQuestion } from '@workerdeck/protocol'
|
|
3
3
|
import { parseUserQuestions } from '../agent/QuestionPrompt.tsx'
|
|
4
|
+
import { answerFor, useQuestionAnswers, type Selection } from '../../lib/question-answers.ts'
|
|
4
5
|
import { Box, Choices, Hint, PromptInput, Rule, TabStrip, type Choice } from './prompt.tsx'
|
|
5
6
|
import { Blank, Ink, Row } from './row.tsx'
|
|
6
7
|
|
|
7
|
-
type Selection = { labels: string[]; other: string; otherActive: boolean }
|
|
8
|
-
|
|
9
|
-
const EMPTY: Selection = { labels: [], other: '', otherActive: false }
|
|
10
|
-
|
|
11
|
-
function answerFor(selection: Selection): string {
|
|
12
|
-
const parts = [...selection.labels]
|
|
13
|
-
if (selection.otherActive && selection.other.trim()) {
|
|
14
|
-
parts.push(selection.other.trim())
|
|
15
|
-
}
|
|
16
|
-
return parts.join(', ')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
8
|
export interface TerminalQuestionPromptProps {
|
|
20
9
|
request: PermissionRequest
|
|
21
10
|
onAnswer: (requestId: string, updatedInput: Record<string, unknown>) => void
|
|
@@ -25,43 +14,15 @@ export interface TerminalQuestionPromptProps {
|
|
|
25
14
|
|
|
26
15
|
export function TerminalQuestionPrompt({ request, onAnswer, onDismiss, className }: TerminalQuestionPromptProps) {
|
|
27
16
|
const questions = parseUserQuestions(request.input)
|
|
28
|
-
const
|
|
17
|
+
const { selectionFor, answered, complete, update, toggle, toggleOther, answers } = useQuestionAnswers(questions)
|
|
29
18
|
const [cursors, setCursors] = useState<number[]>(() => questions.map(() => 0))
|
|
30
19
|
const [tab, setTab] = useState(0)
|
|
31
20
|
const [reviewCursor, setReviewCursor] = useState(0)
|
|
32
21
|
|
|
33
22
|
const review = tab >= questions.length
|
|
34
|
-
const selection =
|
|
35
|
-
const answered = (index: number) => answerFor(selections[index] ?? EMPTY) !== ''
|
|
36
|
-
const complete = questions.every((_, index) => answered(index))
|
|
37
|
-
|
|
38
|
-
const update = (index: number, patch: Partial<Selection>) =>
|
|
39
|
-
setSelections((prev) => prev.map((s, i) => (i === index ? { ...s, ...patch } : s)))
|
|
40
|
-
|
|
41
|
-
const toggle = (index: number, label: string, multiSelect: boolean) => {
|
|
42
|
-
setSelections((prev) =>
|
|
43
|
-
prev.map((current, i) => {
|
|
44
|
-
if (i !== index) {
|
|
45
|
-
return current
|
|
46
|
-
}
|
|
47
|
-
if (!multiSelect) {
|
|
48
|
-
return { ...current, labels: [label], otherActive: false }
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
...current,
|
|
52
|
-
labels: current.labels.includes(label) ? current.labels.filter((l) => l !== label) : [...current.labels, label],
|
|
53
|
-
}
|
|
54
|
-
}),
|
|
55
|
-
)
|
|
56
|
-
}
|
|
23
|
+
const selection = selectionFor(tab)
|
|
57
24
|
|
|
58
|
-
const submit = () => {
|
|
59
|
-
const answers: Record<string, string> = {}
|
|
60
|
-
questions.forEach((question, index) => {
|
|
61
|
-
answers[question.question] = answerFor(selections[index] ?? EMPTY)
|
|
62
|
-
})
|
|
63
|
-
onAnswer(request.id, { ...request.input, answers })
|
|
64
|
-
}
|
|
25
|
+
const submit = () => onAnswer(request.id, { ...request.input, answers: answers() })
|
|
65
26
|
|
|
66
27
|
const dismiss = () => onDismiss(request.id, 'Question dismissed by user')
|
|
67
28
|
|
|
@@ -100,7 +61,7 @@ export function TerminalQuestionPrompt({ request, onAnswer, onDismiss, className
|
|
|
100
61
|
{review ? (
|
|
101
62
|
<ReviewStep
|
|
102
63
|
questions={questions}
|
|
103
|
-
answers={questions.map((_, index) => answerFor(
|
|
64
|
+
answers={questions.map((_, index) => answerFor(selectionFor(index)))}
|
|
104
65
|
complete={complete}
|
|
105
66
|
cursor={reviewCursor}
|
|
106
67
|
onCursor={setReviewCursor}
|
|
@@ -115,8 +76,9 @@ export function TerminalQuestionPrompt({ request, onAnswer, onDismiss, className
|
|
|
115
76
|
selection={selection}
|
|
116
77
|
cursor={cursors[tab] ?? 0}
|
|
117
78
|
onCursor={(next) => setCursors((prev) => prev.map((c, i) => (i === tab ? next : c)))}
|
|
118
|
-
onToggle={(label) => toggle(tab, label
|
|
79
|
+
onToggle={(label) => toggle(tab, label)}
|
|
119
80
|
onOther={(patch) => update(tab, patch)}
|
|
81
|
+
onToggleOther={() => toggleOther(tab)}
|
|
120
82
|
onAdvance={() => move(1)}
|
|
121
83
|
onDismiss={dismiss}
|
|
122
84
|
/>
|
|
@@ -138,6 +100,7 @@ function QuestionStep({
|
|
|
138
100
|
onCursor,
|
|
139
101
|
onToggle,
|
|
140
102
|
onOther,
|
|
103
|
+
onToggleOther,
|
|
141
104
|
onAdvance,
|
|
142
105
|
onDismiss,
|
|
143
106
|
}: {
|
|
@@ -147,6 +110,7 @@ function QuestionStep({
|
|
|
147
110
|
onCursor: (index: number) => void
|
|
148
111
|
onToggle: (label: string) => void
|
|
149
112
|
onOther: (patch: Partial<Selection>) => void
|
|
113
|
+
onToggleOther: () => void
|
|
150
114
|
onAdvance: () => void
|
|
151
115
|
onDismiss: () => void
|
|
152
116
|
}) {
|
|
@@ -214,7 +178,7 @@ function QuestionStep({
|
|
|
214
178
|
return
|
|
215
179
|
}
|
|
216
180
|
if (index === question.options.length) {
|
|
217
|
-
|
|
181
|
+
onToggleOther()
|
|
218
182
|
return
|
|
219
183
|
}
|
|
220
184
|
if (multiSelect && index === question.options.length + 1) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Fragment,
|
|
2
|
-
import type { TranscriptItem
|
|
1
|
+
import { Fragment, useMemo, useState } from 'react'
|
|
2
|
+
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
|
-
import type { TerminalAffordances } from './affordances.tsx'
|
|
5
4
|
import { ActionPlacementProvider, OpenSubagentAction, WithActions } from './affordances.tsx'
|
|
6
5
|
import {
|
|
7
6
|
AssistantRow,
|
|
@@ -15,25 +14,13 @@ import {
|
|
|
15
14
|
ToolRow,
|
|
16
15
|
TurnResultRow,
|
|
17
16
|
UserRow,
|
|
18
|
-
WorkingRow,
|
|
19
17
|
} from './items.tsx'
|
|
20
|
-
import { blockNeedsBlank, taskChildItems,
|
|
18
|
+
import { blockNeedsBlank, taskChildItems, type TaskBlock } from './blocks.ts'
|
|
21
19
|
import { usePulse } from '../agent/pulse.tsx'
|
|
22
|
-
import { PeerNamesProvider } from './peer-names.tsx'
|
|
23
20
|
import { Pressable, useRevealOnOpen } from './press.tsx'
|
|
24
21
|
import { isPeerSend, taskBrief, taskBusy, taskFailed, taskSummary } from './tool-run.ts'
|
|
25
22
|
import { BRIEF_LINES } from './height.ts'
|
|
26
23
|
import { Blank, Row } from './row.tsx'
|
|
27
|
-
import { TerminalSurface } from './surface.tsx'
|
|
28
|
-
|
|
29
|
-
export interface TerminalTranscriptProps {
|
|
30
|
-
state: TranscriptState
|
|
31
|
-
fileUrl?: (path: string) => string
|
|
32
|
-
fontSize?: number
|
|
33
|
-
lineHeight?: number
|
|
34
|
-
affordances?: TerminalAffordances | boolean
|
|
35
|
-
className?: string
|
|
36
|
-
}
|
|
37
24
|
|
|
38
25
|
export function TerminalItemView({
|
|
39
26
|
item,
|
|
@@ -150,73 +137,3 @@ export function TaskRow({
|
|
|
150
137
|
<WithActions actions={<OpenSubagentAction onOpen={() => onOpenSubagent(block.task.id)} />}>{row}</WithActions>
|
|
151
138
|
)
|
|
152
139
|
}
|
|
153
|
-
|
|
154
|
-
function useRunStart(status: TranscriptState['status']): number | undefined {
|
|
155
|
-
const running = status === 'running' || status === 'starting'
|
|
156
|
-
const [startedAt, setStartedAt] = useState<number | undefined>(undefined)
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
setStartedAt((previous) => (running ? (previous ?? Date.now()) : undefined))
|
|
159
|
-
}, [running])
|
|
160
|
-
return running ? startedAt : undefined
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function working(state: TranscriptState): boolean {
|
|
164
|
-
if (state.status !== 'running' && state.status !== 'starting') {
|
|
165
|
-
return false
|
|
166
|
-
}
|
|
167
|
-
const last = state.items.at(-1)
|
|
168
|
-
if (!last) {
|
|
169
|
-
return true
|
|
170
|
-
}
|
|
171
|
-
if (last.kind === 'assistant_text' && last.streaming) {
|
|
172
|
-
return false
|
|
173
|
-
}
|
|
174
|
-
if (last.kind === 'thinking' && last.id === 'streaming-thinking') {
|
|
175
|
-
return false
|
|
176
|
-
}
|
|
177
|
-
return true
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function TerminalTranscript({ state, fileUrl, fontSize, lineHeight, affordances, className }: TerminalTranscriptProps) {
|
|
181
|
-
const runStartedAt = useRunStart(state.status)
|
|
182
|
-
const blocks = useMemo(() => terminalBlocks(state.items), [state.items])
|
|
183
|
-
return (
|
|
184
|
-
<TerminalSurface
|
|
185
|
-
fontSize={fontSize}
|
|
186
|
-
lineHeight={lineHeight}
|
|
187
|
-
affordances={affordances}
|
|
188
|
-
bleed="1ch"
|
|
189
|
-
className={cn('term-transcript', className)}
|
|
190
|
-
>
|
|
191
|
-
<PeerNamesProvider items={state.items}>
|
|
192
|
-
{blocks.map((block, index) => {
|
|
193
|
-
const next = blocks[index + 1]
|
|
194
|
-
return (
|
|
195
|
-
<Fragment key={block.key}>
|
|
196
|
-
{index > 0 && blockNeedsBlank(blocks[index - 1]!, block) ? <Blank /> : null}
|
|
197
|
-
<ActionPlacementProvider value={next && !blockNeedsBlank(block, next) ? 'inline' : 'below'}>
|
|
198
|
-
{'run' in block ? (
|
|
199
|
-
<RunRow items={block.run} />
|
|
200
|
-
) : 'item' in block ? (
|
|
201
|
-
<TerminalItemView item={block.item} fileUrl={fileUrl} />
|
|
202
|
-
) : (
|
|
203
|
-
<TaskRow block={block} fileUrl={fileUrl} />
|
|
204
|
-
)}
|
|
205
|
-
</ActionPlacementProvider>
|
|
206
|
-
</Fragment>
|
|
207
|
-
)
|
|
208
|
-
})}
|
|
209
|
-
{working(state) ? (
|
|
210
|
-
<>
|
|
211
|
-
{state.items.length > 0 ? <Blank /> : null}
|
|
212
|
-
<WorkingRow
|
|
213
|
-
label={state.status === 'starting' ? 'Starting…' : 'Working…'}
|
|
214
|
-
startedAt={runStartedAt}
|
|
215
|
-
tokens={state.contextUsage?.totalTokens}
|
|
216
|
-
/>
|
|
217
|
-
</>
|
|
218
|
-
) : null}
|
|
219
|
-
</PeerNamesProvider>
|
|
220
|
-
</TerminalSurface>
|
|
221
|
-
)
|
|
222
|
-
}
|
|
@@ -5,15 +5,7 @@ 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 {
|
|
9
|
-
ActionPlacementProvider,
|
|
10
|
-
AgentWriteAction,
|
|
11
|
-
BookmarkAction,
|
|
12
|
-
CopyAction,
|
|
13
|
-
KillShellAction,
|
|
14
|
-
OpenShellAction,
|
|
15
|
-
WithActions,
|
|
16
|
-
} from './affordances.tsx'
|
|
8
|
+
import { ActionPlacementProvider, BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
17
9
|
import { TerminalDiff } from './diff.tsx'
|
|
18
10
|
import { TerminalMarkdown } from './markdown.tsx'
|
|
19
11
|
import { usePeerNames } from './peer-names.tsx'
|
|
@@ -26,17 +18,8 @@ import { useHostImageSrc, useToolResultImageSrc, type ToolResultImageState } fro
|
|
|
26
18
|
import { ViewableImage, useOpenImage } from '../agent/image-viewer.tsx'
|
|
27
19
|
import { isPeerSend, peerName, peerOneLine, peerSendTarget, peerSendText, planRun, runFailed, runSummary } from './tool-run.ts'
|
|
28
20
|
import { todoLine, todoPreview, type TodoPreview, type TodoStatus } from './todos.ts'
|
|
29
|
-
import { useShellActions } from '../agent/shell-actions.tsx'
|
|
30
|
-
import {
|
|
31
|
-
SHELL_GLYPH,
|
|
32
|
-
shellAgentWriteLabel,
|
|
33
|
-
shellBodyLines,
|
|
34
|
-
shellFailed,
|
|
35
|
-
shellFooterText,
|
|
36
|
-
shellGrantable,
|
|
37
|
-
shellLabel,
|
|
38
|
-
shellStatusText,
|
|
39
|
-
} from './shell-row.ts'
|
|
21
|
+
import { ShellItemActions, useShellActions, useVerifyRunning } from '../agent/shell-actions.tsx'
|
|
22
|
+
import { SHELL_GLYPH, shellBodyLines, shellFailed, shellFooterText, shellLabel, shellStatusText } from './shell-row.ts'
|
|
40
23
|
import { type ToolCallItem } from './blocks.ts'
|
|
41
24
|
import { Band, Blank, Ink, Row, type Tone } from './row.tsx'
|
|
42
25
|
|
|
@@ -471,12 +454,7 @@ export function ShellRow({ item }: { item: ShellItem }) {
|
|
|
471
454
|
const footer = shellFooterText(item, open, lines.length)
|
|
472
455
|
|
|
473
456
|
const shellId = item.shell.id
|
|
474
|
-
|
|
475
|
-
useEffect(() => {
|
|
476
|
-
if (running) {
|
|
477
|
-
void verify(shellId)
|
|
478
|
-
}
|
|
479
|
-
}, [running, shellId, verify])
|
|
457
|
+
useVerifyRunning(item)
|
|
480
458
|
|
|
481
459
|
const press = (modifiers: PressModifiers) => {
|
|
482
460
|
if ((modifiers.meta || modifiers.ctrl) && actions.open) {
|
|
@@ -494,23 +472,7 @@ export function ShellRow({ item }: { item: ShellItem }) {
|
|
|
494
472
|
|
|
495
473
|
return (
|
|
496
474
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
497
|
-
<WithActions
|
|
498
|
-
actions={
|
|
499
|
-
<>
|
|
500
|
-
{actions.open ? <OpenShellAction onOpen={() => actions.open?.(shellId)} /> : null}
|
|
501
|
-
{actions.agentWrite && shellGrantable(item.shell) ? (
|
|
502
|
-
<AgentWriteAction
|
|
503
|
-
granted={item.shell.agentWrite === true}
|
|
504
|
-
label={shellAgentWriteLabel(item.shell)}
|
|
505
|
-
onToggle={() => void actions.agentWrite?.(shellId, item.shell.agentWrite !== true)}
|
|
506
|
-
/>
|
|
507
|
-
) : null}
|
|
508
|
-
{running ? <KillShellAction onKill={() => void actions.kill(shellId)} /> : null}
|
|
509
|
-
<BookmarkAction id={item.id} />
|
|
510
|
-
<CopyAction text={item.shell.command} label="Copy command" />
|
|
511
|
-
</>
|
|
512
|
-
}
|
|
513
|
-
>
|
|
475
|
+
<WithActions actions={<ShellItemActions item={item} />}>
|
|
514
476
|
<Pressable onPress={press} expanded={open}>
|
|
515
477
|
<Row glyph={SHELL_GLYPH} glyphTone={tone} tone="fg">
|
|
516
478
|
<Ink bold tone="bright">
|