@workerdeck/ui 3.2.0 → 3.4.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 +24 -5
- package/build/index.mjs +297 -62
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +52 -3
- package/build/{use-path-links-C6Fh5zdx.mjs → use-path-links-WCsysQPy.mjs} +337 -83
- 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/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 +106 -17
- package/src/components/agent/SessionFilters.tsx +2 -2
- package/src/components/agent/SessionGroups.tsx +248 -0
- package/src/components/agent/SessionItem.tsx +3 -1
- 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 +35 -3
- package/src/components/terminal/live-tool.ts +71 -0
- package/src/components/terminal/tool-run.ts +6 -3
- package/src/index.ts +8 -1
- package/src/styles/terminal.css +9 -2
- package/src/styles/theme.css +3 -1
- package/build/use-path-links-C6Fh5zdx.mjs.map +0 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { DragEvent, HTMLAttributes, ReactNode } from 'react'
|
|
3
|
+
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react'
|
|
4
|
+
import {
|
|
5
|
+
addCustomGroup,
|
|
6
|
+
moveCustomGroup,
|
|
7
|
+
moveToCustomGroup,
|
|
8
|
+
newCustomGroupId,
|
|
9
|
+
removeCustomGroup,
|
|
10
|
+
renameCustomGroup,
|
|
11
|
+
} from '@workerdeck/protocol'
|
|
12
|
+
import type { CustomGroup, SessionGroup } from '@workerdeck/protocol'
|
|
13
|
+
import { Button } from '../ui/Button.tsx'
|
|
14
|
+
import { Input } from '../ui/Input.tsx'
|
|
15
|
+
import { cn } from '../../lib/utils.ts'
|
|
16
|
+
|
|
17
|
+
type DragItem = { kind: 'session'; key: string } | { kind: 'group'; id: string }
|
|
18
|
+
|
|
19
|
+
export type GroupDrag = ReturnType<typeof useGroupDrag>
|
|
20
|
+
|
|
21
|
+
// The payload lives in state rather than `dataTransfer`, which is unreadable during `dragover`; `setData` is still
|
|
22
|
+
// written because some engines refuse to start a drag without it.
|
|
23
|
+
export function useGroupDrag(groups: readonly CustomGroup[], onChange: (groups: CustomGroup[]) => void) {
|
|
24
|
+
const [item, setItem] = useState<DragItem>()
|
|
25
|
+
const [over, setOver] = useState<string>()
|
|
26
|
+
const end = () => {
|
|
27
|
+
setItem(undefined)
|
|
28
|
+
setOver(undefined)
|
|
29
|
+
}
|
|
30
|
+
const start = (e: DragEvent, next: DragItem) => {
|
|
31
|
+
e.stopPropagation()
|
|
32
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
33
|
+
e.dataTransfer.setData('text/plain', next.kind === 'session' ? next.key : next.id)
|
|
34
|
+
setItem(next)
|
|
35
|
+
}
|
|
36
|
+
const accept = (e: DragEvent, spot: string) => {
|
|
37
|
+
e.preventDefault()
|
|
38
|
+
e.stopPropagation()
|
|
39
|
+
e.dataTransfer.dropEffect = 'move'
|
|
40
|
+
setOver(spot)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function session(key: string, group: SessionGroup): HTMLAttributes<HTMLDivElement> {
|
|
44
|
+
return {
|
|
45
|
+
draggable: true,
|
|
46
|
+
onDragStart: (e) => start(e, { kind: 'session', key }),
|
|
47
|
+
onDragEnd: end,
|
|
48
|
+
onDragOver: (e) => {
|
|
49
|
+
if (item?.kind === 'session') {
|
|
50
|
+
accept(e, `row:${key}`)
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
onDrop: (e) => {
|
|
54
|
+
if (item?.kind !== 'session') {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
e.preventDefault()
|
|
58
|
+
e.stopPropagation()
|
|
59
|
+
if (item.key !== key) {
|
|
60
|
+
onChange(moveToCustomGroup(groups, item.key, group.custom, group.custom ? key : undefined))
|
|
61
|
+
}
|
|
62
|
+
end()
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function container(group: SessionGroup): HTMLAttributes<HTMLDivElement> {
|
|
68
|
+
return {
|
|
69
|
+
onDragOver: (e) => {
|
|
70
|
+
if (item) {
|
|
71
|
+
accept(e, `group:${group.key}`)
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
onDrop: (e) => {
|
|
75
|
+
e.preventDefault()
|
|
76
|
+
if (item?.kind === 'session') {
|
|
77
|
+
onChange(moveToCustomGroup(groups, item.key, group.custom))
|
|
78
|
+
} else if (item?.kind === 'group') {
|
|
79
|
+
onChange(moveCustomGroup(groups, item.id, group.custom))
|
|
80
|
+
}
|
|
81
|
+
end()
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function header(group: SessionGroup): HTMLAttributes<HTMLDivElement> {
|
|
87
|
+
const id = group.custom
|
|
88
|
+
if (id === undefined) {
|
|
89
|
+
return {}
|
|
90
|
+
}
|
|
91
|
+
return { draggable: true, onDragStart: (e) => start(e, { kind: 'group', id }), onDragEnd: end }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
dragging: item !== undefined,
|
|
96
|
+
isOver: (spot: string) => over === spot,
|
|
97
|
+
session,
|
|
98
|
+
container,
|
|
99
|
+
header,
|
|
100
|
+
create: (name = 'New group') => {
|
|
101
|
+
const id = newCustomGroupId()
|
|
102
|
+
onChange(addCustomGroup(groups, name, id))
|
|
103
|
+
return id
|
|
104
|
+
},
|
|
105
|
+
rename: (id: string, name: string) => onChange(renameCustomGroup(groups, id, name)),
|
|
106
|
+
remove: (id: string) => onChange(removeCustomGroup(groups, id)),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function CustomGroupHeader({
|
|
111
|
+
group,
|
|
112
|
+
editing,
|
|
113
|
+
onEditingChange,
|
|
114
|
+
onRename,
|
|
115
|
+
onRemove,
|
|
116
|
+
dragProps,
|
|
117
|
+
}: {
|
|
118
|
+
group: SessionGroup
|
|
119
|
+
editing: boolean
|
|
120
|
+
onEditingChange: (editing: boolean) => void
|
|
121
|
+
onRename: (name: string) => void
|
|
122
|
+
onRemove: () => void
|
|
123
|
+
dragProps: HTMLAttributes<HTMLDivElement>
|
|
124
|
+
}) {
|
|
125
|
+
if (editing) {
|
|
126
|
+
return <GroupNameInput initial={group.label ?? ''} onRename={onRename} onDone={() => onEditingChange(false)} />
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const custom = group.custom !== undefined
|
|
130
|
+
return (
|
|
131
|
+
<GroupHeading
|
|
132
|
+
{...dragProps}
|
|
133
|
+
label={group.label ?? ''}
|
|
134
|
+
count={group.rows.length}
|
|
135
|
+
className={cn('group/heading', custom && 'cursor-grab')}
|
|
136
|
+
onDoubleClick={custom ? () => onEditingChange(true) : undefined}
|
|
137
|
+
leading={custom ? <GripVertical className="-ml-1.5 size-3 opacity-0 group-hover/heading:opacity-60" /> : undefined}
|
|
138
|
+
actions={
|
|
139
|
+
custom ? (
|
|
140
|
+
<>
|
|
141
|
+
<HeadingAction label="Rename group" onClick={() => onEditingChange(true)}>
|
|
142
|
+
<Pencil className="size-3" />
|
|
143
|
+
</HeadingAction>
|
|
144
|
+
<HeadingAction label="Delete group" title="Delete the group - its sessions move to Ungrouped" onClick={onRemove}>
|
|
145
|
+
<Trash2 className="size-3" />
|
|
146
|
+
</HeadingAction>
|
|
147
|
+
</>
|
|
148
|
+
) : undefined
|
|
149
|
+
}
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function GroupNameInput({ initial, onRename, onDone }: { initial: string; onRename: (name: string) => void; onDone: () => void }) {
|
|
155
|
+
const [draft, setDraft] = useState(initial)
|
|
156
|
+
const commit = () => {
|
|
157
|
+
const name = draft.trim()
|
|
158
|
+
if (name && name !== initial) {
|
|
159
|
+
onRename(name)
|
|
160
|
+
}
|
|
161
|
+
onDone()
|
|
162
|
+
}
|
|
163
|
+
return (
|
|
164
|
+
<div className="px-2">
|
|
165
|
+
<Input
|
|
166
|
+
autoFocus
|
|
167
|
+
aria-label="Group name"
|
|
168
|
+
value={draft}
|
|
169
|
+
onFocus={(e) => e.currentTarget.select()}
|
|
170
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
171
|
+
onBlur={commit}
|
|
172
|
+
onKeyDown={(e) => {
|
|
173
|
+
if (e.key === 'Enter') {
|
|
174
|
+
commit()
|
|
175
|
+
} else if (e.key === 'Escape') {
|
|
176
|
+
e.stopPropagation()
|
|
177
|
+
onDone()
|
|
178
|
+
}
|
|
179
|
+
}}
|
|
180
|
+
className="h-6 text-label"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function GroupHeading({
|
|
187
|
+
label,
|
|
188
|
+
count,
|
|
189
|
+
leading,
|
|
190
|
+
actions,
|
|
191
|
+
className,
|
|
192
|
+
...rest
|
|
193
|
+
}: {
|
|
194
|
+
label: ReactNode
|
|
195
|
+
count: number
|
|
196
|
+
leading?: ReactNode
|
|
197
|
+
actions?: ReactNode
|
|
198
|
+
} & HTMLAttributes<HTMLDivElement>) {
|
|
199
|
+
return (
|
|
200
|
+
<div {...rest} className={cn('group/heading flex min-h-5 items-center gap-2 px-2 text-label font-medium text-fg-4', className)}>
|
|
201
|
+
{leading}
|
|
202
|
+
<span className="min-w-0 truncate uppercase tracking-wide">{label}</span>
|
|
203
|
+
<span className="text-fg-4/70">{count}</span>
|
|
204
|
+
{actions ? (
|
|
205
|
+
<span className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover/heading:opacity-100 focus-within:opacity-100">
|
|
206
|
+
{actions}
|
|
207
|
+
</span>
|
|
208
|
+
) : null}
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function HeadingAction({
|
|
214
|
+
label,
|
|
215
|
+
title,
|
|
216
|
+
onClick,
|
|
217
|
+
children,
|
|
218
|
+
}: {
|
|
219
|
+
label: string
|
|
220
|
+
title?: string
|
|
221
|
+
onClick: () => void
|
|
222
|
+
children: ReactNode
|
|
223
|
+
}) {
|
|
224
|
+
return (
|
|
225
|
+
<Button
|
|
226
|
+
variant="ghost"
|
|
227
|
+
size="icon-sm"
|
|
228
|
+
aria-label={label}
|
|
229
|
+
title={title ?? label}
|
|
230
|
+
className="size-5 text-fg-3"
|
|
231
|
+
onClick={(e) => {
|
|
232
|
+
e.stopPropagation()
|
|
233
|
+
onClick()
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{children}
|
|
237
|
+
</Button>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function NewGroupButton({ onClick }: { onClick: () => void }) {
|
|
242
|
+
return (
|
|
243
|
+
<Button variant="ghost" size="xs" className="mx-1 self-start text-fg-3" onClick={onClick}>
|
|
244
|
+
<Plus />
|
|
245
|
+
New group
|
|
246
|
+
</Button>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
@@ -130,6 +130,8 @@ export function SessionItem({
|
|
|
130
130
|
<div
|
|
131
131
|
data-slot="session-item"
|
|
132
132
|
data-active={active || undefined}
|
|
133
|
+
data-asleep={info.engineAsleep || undefined}
|
|
134
|
+
title={info.engineAsleep ? 'Asleep, wakes on your next message' : undefined}
|
|
133
135
|
role="button"
|
|
134
136
|
tabIndex={0}
|
|
135
137
|
onClick={(e) => {
|
|
@@ -154,7 +156,7 @@ export function SessionItem({
|
|
|
154
156
|
className,
|
|
155
157
|
)}
|
|
156
158
|
>
|
|
157
|
-
<div className=
|
|
159
|
+
<div className={cn('flex flex-col gap-1 py-0.5 pr-0.5 pl-1.5', info.engineAsleep && 'opacity-60')}>
|
|
158
160
|
<div className="flex h-5 items-center gap-1.5 overflow-hidden">
|
|
159
161
|
<Gutter>
|
|
160
162
|
<SessionStatusIcon row={row} />
|
|
@@ -54,14 +54,23 @@ import { HostFilesDialog } from './HostFilesDialog.tsx'
|
|
|
54
54
|
import { McpDialog } from './McpDialog.tsx'
|
|
55
55
|
import { SkillsDialog } from './SkillsDialog.tsx'
|
|
56
56
|
import { ModelSelect } from './ModelSelect.tsx'
|
|
57
|
+
import { EffortSelect, effortChoices } from './EffortSelect.tsx'
|
|
57
58
|
import { PermissionModeSelect, permissionModeChoices, type PermissionModeChoice } from './PermissionModeSelect.tsx'
|
|
58
59
|
import { SubagentStrip } from './SubagentStrip.tsx'
|
|
59
60
|
import { useSubagentFrame } from './use-subagent-frame.ts'
|
|
60
61
|
import { ShellStrip } from './ShellStrip.tsx'
|
|
61
62
|
import { ShellTerminal } from './ShellTerminal.tsx'
|
|
62
63
|
import { useShellFrame } from './use-shell-frame.ts'
|
|
63
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
BookmarkProvider,
|
|
66
|
+
TaskControlProvider,
|
|
67
|
+
type BookmarkHandle,
|
|
68
|
+
type TaskControlHandle,
|
|
69
|
+
type TerminalAffordances,
|
|
70
|
+
} from '../terminal/affordances.tsx'
|
|
64
71
|
import type { FileLinkOpener } from '../terminal/file-link.tsx'
|
|
72
|
+
import { backgroundableSince, SEND_NOW_AFTER_MS } from '../terminal/live-tool.ts'
|
|
73
|
+
import { useTicker } from '../terminal/items.tsx'
|
|
65
74
|
import { ApprovalPrompts, type ApprovalPromptProps } from './ApprovalPrompts.tsx'
|
|
66
75
|
import { SessionPanelProviders } from './session-panel-providers.tsx'
|
|
67
76
|
import { useCatchUp } from './use-catch-up.ts'
|
|
@@ -127,6 +136,7 @@ export interface SessionPanelProps {
|
|
|
127
136
|
|
|
128
137
|
export type SessionControls = {
|
|
129
138
|
setModel: (model?: string) => void
|
|
139
|
+
setEffort: (effort?: string) => void
|
|
130
140
|
setPermissionMode: (mode: PermissionMode) => void
|
|
131
141
|
interrupt: () => void
|
|
132
142
|
focusComposer: () => void
|
|
@@ -169,6 +179,8 @@ export type SessionVitals = {
|
|
|
169
179
|
capabilities: TranscriptState['capabilities']
|
|
170
180
|
model: string | undefined
|
|
171
181
|
models: ModelOption[]
|
|
182
|
+
effort: TranscriptState['effort']
|
|
183
|
+
efforts: readonly string[]
|
|
172
184
|
permissionMode: TranscriptState['permissionMode']
|
|
173
185
|
permissionModes: PermissionModeChoice[]
|
|
174
186
|
skills: SkillInfo[] | undefined
|
|
@@ -252,8 +264,10 @@ export function SessionPanel({
|
|
|
252
264
|
approve,
|
|
253
265
|
deny,
|
|
254
266
|
interrupt,
|
|
267
|
+
backgroundTask,
|
|
255
268
|
clearContext,
|
|
256
269
|
setModel,
|
|
270
|
+
setEffort,
|
|
257
271
|
setPermissionMode,
|
|
258
272
|
reconnectNow,
|
|
259
273
|
loadFullResult,
|
|
@@ -351,9 +365,13 @@ export function SessionPanel({
|
|
|
351
365
|
)
|
|
352
366
|
|
|
353
367
|
const hasModels = models.length > 0
|
|
368
|
+
const efforts = useMemo(
|
|
369
|
+
() => effortChoices(models, effectiveModel ?? state.model, capabilities),
|
|
370
|
+
[models, effectiveModel, state.model, capabilities],
|
|
371
|
+
)
|
|
354
372
|
const clientCommands = useMemo(
|
|
355
|
-
() => buildClientCommands({ capabilities, hasModels, setModel, setPermissionMode, clearContext, openPanel }),
|
|
356
|
-
[capabilities, hasModels, openPanel, clearContext, setModel, setPermissionMode],
|
|
373
|
+
() => buildClientCommands({ capabilities, hasModels, efforts, setModel, setEffort, setPermissionMode, clearContext, openPanel }),
|
|
374
|
+
[capabilities, hasModels, efforts, openPanel, clearContext, setModel, setEffort, setPermissionMode],
|
|
357
375
|
)
|
|
358
376
|
|
|
359
377
|
const composerCommands = useMemo(
|
|
@@ -382,6 +400,8 @@ export function SessionPanel({
|
|
|
382
400
|
capabilities: state.capabilities,
|
|
383
401
|
model: vitalsModel,
|
|
384
402
|
models,
|
|
403
|
+
effort: state.effort,
|
|
404
|
+
efforts,
|
|
385
405
|
permissionMode: state.permissionMode,
|
|
386
406
|
permissionModes,
|
|
387
407
|
skills: state.skills,
|
|
@@ -406,10 +426,11 @@ export function SessionPanel({
|
|
|
406
426
|
|
|
407
427
|
const onControlsRef = useRef(onControls)
|
|
408
428
|
onControlsRef.current = onControls
|
|
409
|
-
const setters = useRef({ setModel, setPermissionMode, interrupt })
|
|
410
|
-
setters.current = { setModel, setPermissionMode, interrupt }
|
|
429
|
+
const setters = useRef({ setModel, setEffort, setPermissionMode, interrupt })
|
|
430
|
+
setters.current = { setModel, setEffort, setPermissionMode, interrupt }
|
|
411
431
|
const controls = useRef<SessionControls>({
|
|
412
432
|
setModel: (model) => setters.current.setModel(model),
|
|
433
|
+
setEffort: (effort) => setters.current.setEffort(effort),
|
|
413
434
|
setPermissionMode: (mode) => setters.current.setPermissionMode(mode),
|
|
414
435
|
interrupt: () => setters.current.interrupt(),
|
|
415
436
|
focusComposer: () => composerRef.current?.focus(),
|
|
@@ -426,6 +447,12 @@ export function SessionPanel({
|
|
|
426
447
|
[bookmarkSet, onToggleBookmark],
|
|
427
448
|
)
|
|
428
449
|
|
|
450
|
+
const canBackgroundTasks = !readOnly && capabilities.backgroundTasks === true
|
|
451
|
+
const taskControl = useMemo<TaskControlHandle | undefined>(
|
|
452
|
+
() => (canBackgroundTasks ? { background: backgroundTask } : undefined),
|
|
453
|
+
[canBackgroundTasks, backgroundTask],
|
|
454
|
+
)
|
|
455
|
+
|
|
429
456
|
const busy = state.status === 'running' || state.status === 'awaiting_approval'
|
|
430
457
|
const ended = state.status === 'failed' || state.status === 'closed'
|
|
431
458
|
const attachments = useAttachments(client, sessionId, {
|
|
@@ -463,6 +490,14 @@ export function SessionPanel({
|
|
|
463
490
|
send(text, attachmentIds)
|
|
464
491
|
}
|
|
465
492
|
|
|
493
|
+
const foregroundSince = canBackgroundTasks ? backgroundableSince(state.items) : undefined
|
|
494
|
+
const tick = useTicker(foregroundSince !== undefined)
|
|
495
|
+
const runningForeground = foregroundSince !== undefined && tick - foregroundSince >= SEND_NOW_AFTER_MS
|
|
496
|
+
const handleSendNow = (text: string, attachmentIds: string[]) => {
|
|
497
|
+
handleSend(text, attachmentIds)
|
|
498
|
+
backgroundTask()
|
|
499
|
+
}
|
|
500
|
+
|
|
466
501
|
const menuShows: Record<MenuPanel, boolean> = {
|
|
467
502
|
context: capabilities.contextUsage,
|
|
468
503
|
usage: capabilities.rateLimits,
|
|
@@ -505,6 +540,15 @@ export function SessionPanel({
|
|
|
505
540
|
className={controlsInStatus ? 'h-5' : undefined}
|
|
506
541
|
/>
|
|
507
542
|
) : null}
|
|
543
|
+
{efforts.length && state.effort !== null ? (
|
|
544
|
+
<EffortSelect
|
|
545
|
+
efforts={efforts}
|
|
546
|
+
effort={state.effort}
|
|
547
|
+
onEffortChange={setEffort}
|
|
548
|
+
disabled={ended}
|
|
549
|
+
className={controlsInStatus ? 'h-5' : undefined}
|
|
550
|
+
/>
|
|
551
|
+
) : null}
|
|
508
552
|
{state.permissionMode ? (
|
|
509
553
|
<PermissionModeSelect
|
|
510
554
|
mode={state.permissionMode}
|
|
@@ -630,29 +674,31 @@ export function SessionPanel({
|
|
|
630
674
|
<ShellTerminal key={framedShellId} handle={handle} shellId={framedShellId} fontSize={cell.fontSize} />
|
|
631
675
|
) : (
|
|
632
676
|
<BookmarkProvider value={bookmarkHandle}>
|
|
633
|
-
<
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
677
|
+
<TaskControlProvider value={taskControl}>
|
|
678
|
+
<Transcript
|
|
679
|
+
key={subagentId ?? 'session'}
|
|
680
|
+
state={state}
|
|
681
|
+
fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
|
|
682
|
+
attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
|
|
683
|
+
canBrowseFiles={hostFiles.available}
|
|
684
|
+
sessionNames={peers.names}
|
|
685
|
+
hostImage={hostImage}
|
|
686
|
+
variant={transcriptVariant}
|
|
687
|
+
{...cell}
|
|
688
|
+
affordances={affordances}
|
|
689
|
+
stickyPrompt={stickyPrompt}
|
|
690
|
+
scrubber={scrubber}
|
|
691
|
+
bookmarks={bookmarks}
|
|
692
|
+
replaying={replaying}
|
|
693
|
+
catchUp={catchUp && newCount > 0 ? { from: catchUp.itemCount, since: catchUp.since } : undefined}
|
|
694
|
+
reveal={frameReturnReveal ?? reveal}
|
|
695
|
+
frame={subagentId === undefined ? undefined : { parentToolUseId: subagentId }}
|
|
696
|
+
onOpenSubagent={enterSubagent}
|
|
697
|
+
emptyState={emptyState}
|
|
698
|
+
jumpToRecapRef={jumpToRecap}
|
|
699
|
+
repinRef={repinTranscript}
|
|
700
|
+
/>
|
|
701
|
+
</TaskControlProvider>
|
|
656
702
|
</BookmarkProvider>
|
|
657
703
|
)}
|
|
658
704
|
{catchUp && newCount > 0 && !replaying && subagentId === undefined && framedShellId === undefined ? (
|
|
@@ -680,6 +726,7 @@ export function SessionPanel({
|
|
|
680
726
|
<Composer
|
|
681
727
|
ref={composerRef}
|
|
682
728
|
onSend={handleSend}
|
|
729
|
+
onSendNow={runningForeground ? handleSendNow : undefined}
|
|
683
730
|
onInterrupt={interrupt}
|
|
684
731
|
busy={busy}
|
|
685
732
|
disabled={ended || !sessionId}
|
|
@@ -12,9 +12,10 @@ import { ToolCallCard } from './ToolCallCard.tsx'
|
|
|
12
12
|
import { Row } from '../terminal/row.tsx'
|
|
13
13
|
import { TerminalItemView } from '../terminal/TerminalTranscript.tsx'
|
|
14
14
|
import { ViewableImage } from './image-viewer.tsx'
|
|
15
|
-
import { peerLabel } from '../terminal/items.tsx'
|
|
15
|
+
import { peerLabel, useTicker } from '../terminal/items.tsx'
|
|
16
|
+
import { canBackground, elapsedLabel, liveTailLines, toolBusy } from '../terminal/live-tool.ts'
|
|
16
17
|
import { ShellItemActions, useShellActions, useVerifyRunning } from './shell-actions.tsx'
|
|
17
|
-
import { BookmarkAction, CopyAction, WithActions } from '../terminal/affordances.tsx'
|
|
18
|
+
import { BackgroundAction, BookmarkAction, CopyAction, WithActions } from '../terminal/affordances.tsx'
|
|
18
19
|
import { shellBodyLines, shellFailed, shellFooterText, shellLabel, shellStatusText } from '../terminal/shell-row.ts'
|
|
19
20
|
|
|
20
21
|
function TurnResultRow({ item }: { item: Extract<TranscriptItem, { kind: 'turn_result' }> }) {
|
|
@@ -145,16 +146,33 @@ function ToolCard({
|
|
|
145
146
|
<WithActions
|
|
146
147
|
actions={
|
|
147
148
|
<>
|
|
149
|
+
{canBackground(item) ? <BackgroundAction toolUseId={item.id} /> : null}
|
|
148
150
|
<BookmarkAction id={item.id} />
|
|
149
151
|
{copyable ? <CopyAction text={copyable} label="Copy" /> : null}
|
|
150
152
|
</>
|
|
151
153
|
}
|
|
152
154
|
>
|
|
153
155
|
<ToolCallCard item={item} hostImage={hostImage} />
|
|
156
|
+
{toolBusy(item) ? <LiveToolStrip item={item} /> : null}
|
|
154
157
|
</WithActions>
|
|
155
158
|
)
|
|
156
159
|
}
|
|
157
160
|
|
|
161
|
+
function LiveToolStrip({ item }: { item: Extract<TranscriptItem, { kind: 'tool_call' }> }) {
|
|
162
|
+
const now = useTicker(item.ts !== undefined)
|
|
163
|
+
const elapsed = elapsedLabel(item.ts, now)
|
|
164
|
+
const tail = liveTailLines(item)
|
|
165
|
+
if (!elapsed && tail.length === 0) {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
<div data-slot="tool-live" className="mt-1 text-label text-fg-4">
|
|
170
|
+
{elapsed ? <div>running {elapsed}</div> : null}
|
|
171
|
+
{tail.length > 0 ? <pre className="overflow-x-auto font-mono whitespace-pre-wrap text-fg-3">{tail.join('\n')}</pre> : null}
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
158
176
|
function ShellCard({ item }: { item: ShellItem }) {
|
|
159
177
|
const [open, setOpen] = useState(false)
|
|
160
178
|
const actions = useShellActions()
|
|
@@ -176,7 +176,9 @@ export type ClientCommandPanel = 'info' | 'context' | 'usage' | 'mcp' | 'skills'
|
|
|
176
176
|
export type ClientCommandSources = {
|
|
177
177
|
capabilities: Pick<EngineCapabilities, 'permissionModes' | 'clearContext' | 'mcpStatus' | 'contextUsage' | 'rateLimits' | 'skillsList'>
|
|
178
178
|
hasModels: boolean
|
|
179
|
+
efforts?: readonly string[]
|
|
179
180
|
setModel: (model: string) => void
|
|
181
|
+
setEffort?: (effort?: string) => void
|
|
180
182
|
setPermissionMode: (mode: PermissionMode) => void
|
|
181
183
|
clearContext: () => void
|
|
182
184
|
openPanel: (panel: ClientCommandPanel) => void
|
|
@@ -185,7 +187,9 @@ export type ClientCommandSources = {
|
|
|
185
187
|
export function buildClientCommands({
|
|
186
188
|
capabilities,
|
|
187
189
|
hasModels,
|
|
190
|
+
efforts = [],
|
|
188
191
|
setModel,
|
|
192
|
+
setEffort,
|
|
189
193
|
setPermissionMode,
|
|
190
194
|
clearContext,
|
|
191
195
|
openPanel,
|
|
@@ -208,6 +212,26 @@ export function buildClientCommands({
|
|
|
208
212
|
},
|
|
209
213
|
})
|
|
210
214
|
}
|
|
215
|
+
if (efforts.length && setEffort) {
|
|
216
|
+
built.push({
|
|
217
|
+
name: 'effort',
|
|
218
|
+
description: `Set the reasoning effort (${efforts.join(', ')}, default)`,
|
|
219
|
+
argumentHint: '<level>',
|
|
220
|
+
requiresArgs: true,
|
|
221
|
+
run: (args) => {
|
|
222
|
+
const wanted = args.split(/\s+/)[0]
|
|
223
|
+
if (wanted === 'default') {
|
|
224
|
+
setEffort(undefined)
|
|
225
|
+
return true
|
|
226
|
+
}
|
|
227
|
+
if (!wanted || !efforts.includes(wanted)) {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
setEffort(wanted)
|
|
231
|
+
return true
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
}
|
|
211
235
|
if (modes.length > 1) {
|
|
212
236
|
built.push({
|
|
213
237
|
name: 'permissions',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Bot, BotOff, Check, Copy, Maximize2, SquareTerminal, Star, X, type LucideIcon } from 'lucide-react'
|
|
1
|
+
import { ArrowDownToLine, Bot, BotOff, Check, Copy, Maximize2, SquareTerminal, Star, X, type LucideIcon } from 'lucide-react'
|
|
2
2
|
import { createContext, useContext, useState, type ReactNode } from 'react'
|
|
3
3
|
import { copyText } from '../../lib/clipboard.ts'
|
|
4
4
|
import { cn } from '../../lib/utils.ts'
|
|
@@ -134,6 +134,34 @@ export function BookmarkAction({ id }: { id: string }) {
|
|
|
134
134
|
)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Present only where the engine can move a running tool call to the background (Claude's Ctrl+B).
|
|
138
|
+
export type TaskControlHandle = { background: (toolUseId?: string) => void }
|
|
139
|
+
|
|
140
|
+
const TaskControlContext = createContext<TaskControlHandle | undefined>(undefined)
|
|
141
|
+
|
|
142
|
+
export function TaskControlProvider({ value, children }: { value: TaskControlHandle | undefined; children: ReactNode }) {
|
|
143
|
+
return <TaskControlContext.Provider value={value}>{children}</TaskControlContext.Provider>
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function useTaskControl(): TaskControlHandle | undefined {
|
|
147
|
+
return useContext(TaskControlContext)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function BackgroundAction({ toolUseId }: { toolUseId: string }) {
|
|
151
|
+
const handle = useContext(TaskControlContext)
|
|
152
|
+
if (!handle) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
return (
|
|
156
|
+
<ActionButton
|
|
157
|
+
icon={ArrowDownToLine}
|
|
158
|
+
word="background"
|
|
159
|
+
label="Move to background (the command keeps running, the agent continues)"
|
|
160
|
+
onPress={() => handle.background(toolUseId)}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
137
165
|
export function OpenSubagentAction({ onOpen, label = 'Open sub-agent' }: { onOpen: () => void; label?: string }) {
|
|
138
166
|
return <ActionButton icon={Maximize2} word="open" label={label} onPress={onOpen} />
|
|
139
167
|
}
|
|
@@ -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
|
}
|