@workerdeck/ui 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/{SessionPanel-BNNc8grK.d.mts → SessionPanel-DIi4sEgh.d.mts} +5 -2
- package/build/index.d.mts +71 -6
- package/build/index.mjs +613 -159
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +104 -3
- package/build/{use-path-links-Cby4i_VL.mjs → use-path-links-WCsysQPy.mjs} +332 -79
- package/build/use-path-links-WCsysQPy.mjs.map +1 -0
- package/build/workspace.d.mts +1 -1
- package/build/workspace.mjs +1 -1
- package/package.json +4 -4
- package/src/components/agent/AgentAvatar.tsx +117 -0
- package/src/components/agent/Composer.tsx +29 -6
- package/src/components/agent/EffortSelect.tsx +64 -0
- package/src/components/agent/ModelSelect.tsx +1 -1
- package/src/components/agent/SessionBrowser.tsx +267 -42
- package/src/components/agent/SessionGroups.tsx +131 -21
- package/src/components/agent/SessionItem.tsx +182 -69
- package/src/components/agent/SessionPanel.tsx +75 -28
- package/src/components/agent/TranscriptItemView.tsx +20 -2
- package/src/components/agent/composer-commands.ts +24 -0
- package/src/components/terminal/affordances.tsx +29 -1
- package/src/components/terminal/height.ts +12 -2
- package/src/components/terminal/items.tsx +33 -1
- package/src/components/terminal/live-tool.ts +71 -0
- package/src/index.ts +12 -0
- package/src/lib/team-drop.ts +65 -0
- package/src/styles/terminal.css +9 -2
- package/src/styles/theme.css +20 -1
- package/build/use-path-links-Cby4i_VL.mjs.map +0 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Fragment, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Users } from 'lucide-react'
|
|
2
3
|
import type { ReactNode } from 'react'
|
|
3
4
|
import { projectLabel, projectName, projectSubpath, sessionLabel } from '@workerdeck/protocol'
|
|
4
|
-
import type { SessionRow, SessionTask, StepDisplay, SubagentDisplay } from '@workerdeck/protocol'
|
|
5
|
+
import type { SessionRow, SessionState, SessionTask, StepDisplay, SubagentDisplay } from '@workerdeck/protocol'
|
|
6
|
+
import { AgentAvatar, LeadChip, avatarOf, type AgentAvatars } from './AgentAvatar.tsx'
|
|
5
7
|
import { ContextRing } from './ContextRing.tsx'
|
|
6
8
|
import { EngineIcon, vendorMarkClass, vendorTextClass } from './EngineIcon.tsx'
|
|
7
9
|
import { ProjectIcon } from './ProjectIcon.tsx'
|
|
@@ -36,6 +38,19 @@ export interface SessionItemProps {
|
|
|
36
38
|
editing?: boolean
|
|
37
39
|
onEditingChange?: (editing: boolean) => void
|
|
38
40
|
actions?: ReactNode
|
|
41
|
+
// Drawn when `row.info.agent` is set: the card then leads with the avatar and names the agent.
|
|
42
|
+
avatars?: AgentAvatars
|
|
43
|
+
// `member` is the one-row card a team draws under its lead.
|
|
44
|
+
variant?: 'card' | 'member'
|
|
45
|
+
// The state the avatar badge draws, for a folded lead standing in for its team.
|
|
46
|
+
badgeState?: SessionState
|
|
47
|
+
// Unread summed over a folded team's members, drawn with the team glyph beside the lead's own.
|
|
48
|
+
teamUnseen?: number
|
|
49
|
+
// A folded lead's members, drawn on its first line before the context ring.
|
|
50
|
+
teamTray?: ReactNode
|
|
51
|
+
// Draws a session with no agent in the agent card's shape (an engine-mark tile for the avatar), so a list mixing
|
|
52
|
+
// agents and plain sessions keeps one column.
|
|
53
|
+
tile?: boolean
|
|
39
54
|
className?: string
|
|
40
55
|
}
|
|
41
56
|
|
|
@@ -64,6 +79,12 @@ export function SessionItem({
|
|
|
64
79
|
editing,
|
|
65
80
|
onEditingChange,
|
|
66
81
|
actions,
|
|
82
|
+
avatars,
|
|
83
|
+
variant = 'card',
|
|
84
|
+
badgeState,
|
|
85
|
+
teamUnseen = 0,
|
|
86
|
+
teamTray,
|
|
87
|
+
tile = false,
|
|
67
88
|
className,
|
|
68
89
|
}: SessionItemProps) {
|
|
69
90
|
const { info } = row
|
|
@@ -125,11 +146,61 @@ export function SessionItem({
|
|
|
125
146
|
},
|
|
126
147
|
)
|
|
127
148
|
const holdsOpenStep = steps.some((s) => s.key === activeStepKey)
|
|
149
|
+
const agent = info.agent
|
|
150
|
+
const label = agent ? agent.name : sessionLabel(info)
|
|
151
|
+
const editable = agent ? agent.name : (info.title ?? '')
|
|
152
|
+
const time = formatRelativeTime(info.lastActivityAt ?? info.createdAt)
|
|
153
|
+
const nameLabel =
|
|
154
|
+
isEditing && onRename ? (
|
|
155
|
+
<NameEditor
|
|
156
|
+
initial={editable}
|
|
157
|
+
placeholder={agent ? 'Agent name' : 'Session name'}
|
|
158
|
+
onCommit={(title) => {
|
|
159
|
+
setEditing(false)
|
|
160
|
+
if (title !== editable && (title || !agent)) {
|
|
161
|
+
onRename(title)
|
|
162
|
+
}
|
|
163
|
+
}}
|
|
164
|
+
onCancel={() => setEditing(false)}
|
|
165
|
+
/>
|
|
166
|
+
) : (
|
|
167
|
+
<span
|
|
168
|
+
onDoubleClick={
|
|
169
|
+
onRename && renameOn === 'doubleClick'
|
|
170
|
+
? (e) => {
|
|
171
|
+
e.stopPropagation()
|
|
172
|
+
setEditing(true)
|
|
173
|
+
}
|
|
174
|
+
: undefined
|
|
175
|
+
}
|
|
176
|
+
className="min-w-0 shrink truncate text-body-sm font-medium tracking-[-0.005em] text-fg-1"
|
|
177
|
+
>
|
|
178
|
+
{label}
|
|
179
|
+
</span>
|
|
180
|
+
)
|
|
181
|
+
const unread =
|
|
182
|
+
row.unseen > 0 ? (
|
|
183
|
+
<span
|
|
184
|
+
// Prose, not rows: `unseen` counts messages a person has not read (`proseCount`).
|
|
185
|
+
title={`${row.unseen} new message${row.unseen === 1 ? '' : 's'}`}
|
|
186
|
+
className={cn(
|
|
187
|
+
'flex h-4 min-w-6 shrink-0 items-center justify-center rounded-full px-2',
|
|
188
|
+
'text-[0.75rem] leading-none tracking-[-0.005em] tabular-nums',
|
|
189
|
+
// Grey while the turn is still producing, accent once it has stopped: the badge's colour
|
|
190
|
+
// answers "is this waiting for me", and a working session is not yet.
|
|
191
|
+
row.state === 'working' ? 'bg-badge text-badge-fg' : 'bg-accent text-accent-fg',
|
|
192
|
+
)}
|
|
193
|
+
>
|
|
194
|
+
{row.unseen}
|
|
195
|
+
</span>
|
|
196
|
+
) : null
|
|
128
197
|
|
|
129
198
|
return (
|
|
130
199
|
<div
|
|
131
200
|
data-slot="session-item"
|
|
132
201
|
data-active={active || undefined}
|
|
202
|
+
data-asleep={info.engineAsleep || undefined}
|
|
203
|
+
title={info.engineAsleep ? 'Asleep, wakes on your next message' : undefined}
|
|
133
204
|
role="button"
|
|
134
205
|
tabIndex={0}
|
|
135
206
|
onClick={(e) => {
|
|
@@ -154,75 +225,107 @@ export function SessionItem({
|
|
|
154
225
|
className,
|
|
155
226
|
)}
|
|
156
227
|
>
|
|
157
|
-
|
|
158
|
-
<div className=
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
</
|
|
162
|
-
{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
?
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
</
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
228
|
+
{agent && variant === 'member' ? (
|
|
229
|
+
<div className={cn('flex h-6 items-center gap-1.5 overflow-hidden py-0.5 pr-0.5 pl-1.5', info.engineAsleep && 'opacity-60')}>
|
|
230
|
+
<AgentAvatar row={row} image={avatarOf(avatars, agent)} size={22} />
|
|
231
|
+
{nameLabel}
|
|
232
|
+
{info.title ? <span className="min-w-0 flex-1 truncate text-body-sm text-fg-3">{info.title}</span> : <span className="flex-1" />}
|
|
233
|
+
{unread}
|
|
234
|
+
<span className="shrink-0 text-body-sm text-fg-4">{time}</span>
|
|
235
|
+
{actions}
|
|
236
|
+
</div>
|
|
237
|
+
) : agent || tile ? (
|
|
238
|
+
<div className={cn('flex items-center gap-2 py-0.5 pr-0.5 pl-1.5', info.engineAsleep && 'opacity-60')}>
|
|
239
|
+
<AgentAvatar row={row} image={avatarOf(avatars, agent)} state={badgeState} />
|
|
240
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
241
|
+
<div className="flex h-5 items-center gap-1.5 overflow-hidden">
|
|
242
|
+
{nameLabel}
|
|
243
|
+
{agent?.leads ? <LeadChip /> : null}
|
|
244
|
+
<span className="flex-1" />
|
|
245
|
+
{unread}
|
|
246
|
+
{teamUnseen > 0 ? (
|
|
247
|
+
<span
|
|
248
|
+
title={`${teamUnseen} new message${teamUnseen === 1 ? '' : 's'} in the team`}
|
|
249
|
+
className="flex h-4 shrink-0 items-center gap-0.5 rounded-full bg-accent px-1.5 text-[0.75rem] leading-none text-accent-fg tabular-nums"
|
|
250
|
+
>
|
|
251
|
+
<Users className="size-3" />
|
|
252
|
+
{teamUnseen}
|
|
253
|
+
</span>
|
|
254
|
+
) : null}
|
|
255
|
+
{teamTray}
|
|
256
|
+
<ContextRing usage={info.contextUsage} engine={engine} size={16} className="p-0.5" />
|
|
257
|
+
</div>
|
|
258
|
+
<div className="flex h-5 items-center gap-1.5 overflow-hidden text-body-sm tracking-[-0.005em]">
|
|
259
|
+
{agent ? (
|
|
260
|
+
<>
|
|
261
|
+
{info.title ? <span className="min-w-0 truncate text-fg-4">{info.title}</span> : null}
|
|
262
|
+
<span className="shrink-0 text-fg-4">
|
|
263
|
+
{model ? (
|
|
264
|
+
<>
|
|
265
|
+
{info.title ? '· ' : ''}
|
|
266
|
+
<span className={vendorTextClass(engine, info.model)}>{model}</span>
|
|
267
|
+
{' · '}
|
|
268
|
+
</>
|
|
269
|
+
) : info.title ? (
|
|
270
|
+
'· '
|
|
271
|
+
) : null}
|
|
272
|
+
{time}
|
|
273
|
+
</span>
|
|
274
|
+
</>
|
|
275
|
+
) : (
|
|
276
|
+
<>
|
|
277
|
+
<span className="min-w-0 truncate text-fg-4">
|
|
278
|
+
{parts.map((part, i) => (
|
|
279
|
+
<Fragment key={i}>
|
|
280
|
+
{i > 0 ? ' · ' : ''}
|
|
281
|
+
{part}
|
|
282
|
+
</Fragment>
|
|
283
|
+
))}
|
|
284
|
+
</span>
|
|
285
|
+
<span className="shrink-0 text-fg-4">
|
|
286
|
+
{parts.length > 0 ? '· ' : ''}
|
|
287
|
+
{time}
|
|
288
|
+
</span>
|
|
289
|
+
</>
|
|
198
290
|
)}
|
|
199
|
-
|
|
200
|
-
{
|
|
201
|
-
</
|
|
202
|
-
|
|
203
|
-
<ContextRing usage={info.contextUsage} engine={engine} size={16} className="p-0.5" />
|
|
291
|
+
<span className="min-w-0 flex-1" />
|
|
292
|
+
{actions}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
204
295
|
</div>
|
|
296
|
+
) : (
|
|
297
|
+
<div className={cn('flex flex-col gap-1 py-0.5 pr-0.5 pl-1.5', info.engineAsleep && 'opacity-60')}>
|
|
298
|
+
<div className="flex h-5 items-center gap-1.5 overflow-hidden">
|
|
299
|
+
<Gutter>
|
|
300
|
+
<SessionStatusIcon row={row} />
|
|
301
|
+
</Gutter>
|
|
302
|
+
{nameLabel}
|
|
303
|
+
<span className="flex-1" />
|
|
304
|
+
{unread}
|
|
305
|
+
<ContextRing usage={info.contextUsage} engine={engine} size={16} className="p-0.5" />
|
|
306
|
+
</div>
|
|
205
307
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
308
|
+
<div className="flex h-5 items-center gap-1.5 overflow-hidden text-body-sm tracking-[-0.005em]">
|
|
309
|
+
<Gutter>
|
|
310
|
+
<EngineIcon engine={engine} model={info.model} className={cn('size-4', vendorMarkClass(engine, info.model))} />
|
|
311
|
+
</Gutter>
|
|
312
|
+
<span className="min-w-0 truncate text-fg-4">
|
|
313
|
+
{parts.map((part, i) => (
|
|
314
|
+
<Fragment key={i}>
|
|
315
|
+
{i > 0 ? ' · ' : ''}
|
|
316
|
+
{part}
|
|
317
|
+
</Fragment>
|
|
318
|
+
))}
|
|
319
|
+
</span>
|
|
320
|
+
<span className="shrink-0 text-fg-4">
|
|
321
|
+
{parts.length > 0 ? '· ' : ''}
|
|
322
|
+
{time}
|
|
323
|
+
</span>
|
|
324
|
+
<span className="min-w-0 flex-1" />
|
|
325
|
+
{actions}
|
|
326
|
+
</div>
|
|
224
327
|
</div>
|
|
225
|
-
|
|
328
|
+
)}
|
|
226
329
|
|
|
227
330
|
{steps.length > 0 ? (
|
|
228
331
|
<div className="flex flex-col">
|
|
@@ -239,7 +342,17 @@ function Gutter({ children }: { children: ReactNode }) {
|
|
|
239
342
|
return <span className="flex size-4 shrink-0 items-center justify-center">{children}</span>
|
|
240
343
|
}
|
|
241
344
|
|
|
242
|
-
function NameEditor({
|
|
345
|
+
function NameEditor({
|
|
346
|
+
initial,
|
|
347
|
+
placeholder,
|
|
348
|
+
onCommit,
|
|
349
|
+
onCancel,
|
|
350
|
+
}: {
|
|
351
|
+
initial: string
|
|
352
|
+
placeholder: string
|
|
353
|
+
onCommit: (title: string) => void
|
|
354
|
+
onCancel: () => void
|
|
355
|
+
}) {
|
|
243
356
|
const [value, setValue] = useState(initial)
|
|
244
357
|
const ref = useRef<HTMLInputElement>(null)
|
|
245
358
|
useEffect(() => {
|
|
@@ -259,8 +372,8 @@ function NameEditor({ initial, onCommit, onCancel }: { initial: string; onCommit
|
|
|
259
372
|
ref={ref}
|
|
260
373
|
value={value}
|
|
261
374
|
spellCheck={false}
|
|
262
|
-
placeholder=
|
|
263
|
-
aria-label=
|
|
375
|
+
placeholder={placeholder}
|
|
376
|
+
aria-label={placeholder}
|
|
264
377
|
onClick={(e) => e.stopPropagation()}
|
|
265
378
|
onDoubleClick={(e) => e.stopPropagation()}
|
|
266
379
|
onChange={(e) => setValue(e.target.value)}
|
|
@@ -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
|
}
|