@workerdeck/ui 3.3.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 +19 -5
- package/build/index.mjs +26 -7
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +16 -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/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 +26 -2
- package/src/components/agent/SessionGroups.tsx +1 -1
- 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 +33 -1
- package/src/components/terminal/live-tool.ts +71 -0
- package/src/index.ts +1 -0
- package/src/styles/terminal.css +9 -2
- package/src/styles/theme.css +3 -1
- package/build/use-path-links-Cby4i_VL.mjs.map +0 -1
package/build/workspace.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as SessionPanelProps } from "./SessionPanel-
|
|
1
|
+
import { r as SessionPanelProps } from "./SessionPanel-DIi4sEgh.mjs";
|
|
2
2
|
import { CSSProperties } from "react";
|
|
3
3
|
import { ShellInfo } from "@workerdeck/protocol";
|
|
4
4
|
import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
|
package/build/workspace.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $t as
|
|
1
|
+
import { $t as Splitter, N as ShellTerminal, Nn as cn, On as Input, Y as EntryIcon, i as SessionPanel, in as Tip, jn as Button, n as usePathLinks, on as TooltipProvider, t as resolveAgainstCwd, tn as Spinner } from "./use-path-links-WCsysQPy.mjs";
|
|
2
2
|
import { m as formatBytes } from "./status-kmsBhuFG.mjs";
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workerdeck/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Styled agent-control component library for WorkerDeck: session panel, transcript with streaming, tool-call cards, permission prompts, session list, composer. Tailwind v4 + Base UI + cva. Pairs with the headless @workerdeck/react layer.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"streamdown": "^2.6.0",
|
|
40
40
|
"tailwind-merge": "^3.7.0",
|
|
41
41
|
"use-stick-to-bottom": "^1.1.6",
|
|
42
|
-
"@workerdeck/
|
|
43
|
-
"@workerdeck/react": "3.
|
|
44
|
-
"@workerdeck/
|
|
42
|
+
"@workerdeck/client": "3.4.0",
|
|
43
|
+
"@workerdeck/react": "3.4.0",
|
|
44
|
+
"@workerdeck/protocol": "3.4.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"monaco-editor": "^0.56.0",
|
|
@@ -39,6 +39,8 @@ export type ComposerHandle = {
|
|
|
39
39
|
|
|
40
40
|
export interface ComposerProps {
|
|
41
41
|
onSend: (text: string, attachmentIds: string[]) => void
|
|
42
|
+
// Present while a foreground command the engine can background is running: ⌥↵ sends and moves it to the background.
|
|
43
|
+
onSendNow?: (text: string, attachmentIds: string[]) => void
|
|
42
44
|
onInterrupt: () => void
|
|
43
45
|
busy: boolean
|
|
44
46
|
disabled?: boolean
|
|
@@ -70,6 +72,7 @@ export interface ComposerProps {
|
|
|
70
72
|
|
|
71
73
|
export function Composer({
|
|
72
74
|
onSend,
|
|
75
|
+
onSendNow,
|
|
73
76
|
onInterrupt,
|
|
74
77
|
busy,
|
|
75
78
|
disabled,
|
|
@@ -184,7 +187,7 @@ export function Composer({
|
|
|
184
187
|
rankPeerSessions(query, peers).map((peer) => ({
|
|
185
188
|
value: peer.slug,
|
|
186
189
|
label: peer.label,
|
|
187
|
-
description: [peer.engine, peer.project, peer.status].filter(Boolean).join(' · '),
|
|
190
|
+
description: [peer.gateway, peer.engine, peer.project, peer.status].filter(Boolean).join(' · '),
|
|
188
191
|
icon: <Users className="size-3.5 text-fg-3" />,
|
|
189
192
|
})),
|
|
190
193
|
onSelect: (suggestion) => suggestion.value,
|
|
@@ -214,7 +217,7 @@ export function Composer({
|
|
|
214
217
|
const staged = attachments?.items ?? []
|
|
215
218
|
const canSend = !disabled && (!isEmpty || staged.length > 0) && !attachments?.uploading && !attachments?.hasFailure
|
|
216
219
|
|
|
217
|
-
const submit = () => {
|
|
220
|
+
const submit = (now = false) => {
|
|
218
221
|
if (!canSend) {
|
|
219
222
|
return
|
|
220
223
|
}
|
|
@@ -230,7 +233,8 @@ export function Composer({
|
|
|
230
233
|
focus()
|
|
231
234
|
return
|
|
232
235
|
}
|
|
233
|
-
|
|
236
|
+
const deliver = now && onSendNow ? onSendNow : onSend
|
|
237
|
+
deliver(plainText.trim(), attachments?.readyIds ?? [])
|
|
234
238
|
attachments?.clear()
|
|
235
239
|
draft?.clear()
|
|
236
240
|
clear()
|
|
@@ -275,6 +279,11 @@ export function Composer({
|
|
|
275
279
|
setHelpOpen(true)
|
|
276
280
|
return
|
|
277
281
|
}
|
|
282
|
+
if (e.key === 'Enter' && e.altKey && onSendNow && canSend) {
|
|
283
|
+
e.preventDefault()
|
|
284
|
+
submit(true)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
278
287
|
if (helpOpen) {
|
|
279
288
|
setHelpOpen(false)
|
|
280
289
|
}
|
|
@@ -351,9 +360,16 @@ export function Composer({
|
|
|
351
360
|
</span>
|
|
352
361
|
)
|
|
353
362
|
|
|
363
|
+
const sendNow =
|
|
364
|
+
onSendNow && canSend && !shellMode ? (
|
|
365
|
+
<button type="button" className="term-press term-link text-label" onClick={() => submit(true)}>
|
|
366
|
+
⌥↵ send now - moves the running command to the background
|
|
367
|
+
</button>
|
|
368
|
+
) : null
|
|
369
|
+
|
|
354
370
|
const interrupting = busy && !canSend
|
|
355
371
|
const submitButton = terminal ? (
|
|
356
|
-
<GlyphButton label="Send" disabled={!canSend} onClick={submit} tone={canSend ? 'blue' : undefined}>
|
|
372
|
+
<GlyphButton label="Send" disabled={!canSend} onClick={() => submit()} tone={canSend ? 'blue' : undefined}>
|
|
357
373
|
↵
|
|
358
374
|
</GlyphButton>
|
|
359
375
|
) : interrupting ? (
|
|
@@ -361,7 +377,7 @@ export function Composer({
|
|
|
361
377
|
<Square className="size-3" />
|
|
362
378
|
</Button>
|
|
363
379
|
) : (
|
|
364
|
-
<Button size="icon-sm" aria-label="Send" className="rounded-full" disabled={!canSend} onClick={submit}>
|
|
380
|
+
<Button size="icon-sm" aria-label="Send" className="rounded-full" disabled={!canSend} onClick={() => submit()}>
|
|
365
381
|
<ArrowUp className="size-4" />
|
|
366
382
|
</Button>
|
|
367
383
|
)
|
|
@@ -402,7 +418,7 @@ export function Composer({
|
|
|
402
418
|
triggers={triggers}
|
|
403
419
|
markdown={false}
|
|
404
420
|
normalizeBullets={false}
|
|
405
|
-
onSubmit={submit}
|
|
421
|
+
onSubmit={() => submit()}
|
|
406
422
|
disabled={disabled}
|
|
407
423
|
placeholder={disabled ? 'Session ended' : shellMode ? SHELL_PLACEHOLDER : placeholder}
|
|
408
424
|
minHeight={minHeight}
|
|
@@ -428,6 +444,12 @@ export function Composer({
|
|
|
428
444
|
className={cn('term-composer', shellMode && 'term-composer-shell', disabled && 'opacity-60')}
|
|
429
445
|
>
|
|
430
446
|
<div className="term-composer-body">
|
|
447
|
+
{sendNow ? (
|
|
448
|
+
<div className="term-row">
|
|
449
|
+
<span aria-hidden className="term-gutter" />
|
|
450
|
+
{sendNow}
|
|
451
|
+
</div>
|
|
452
|
+
) : null}
|
|
431
453
|
{stagedStrip}
|
|
432
454
|
<div className="term-row">
|
|
433
455
|
{canAttach && !shellMode ? fileField : null}
|
|
@@ -464,6 +486,7 @@ export function Composer({
|
|
|
464
486
|
return (
|
|
465
487
|
<div data-slot="composer" className={cn('px-[var(--wd-composer-padding)] pb-[var(--wd-composer-padding)]', className)}>
|
|
466
488
|
{helpRow}
|
|
489
|
+
{sendNow ? <div className="mx-auto mb-1 w-full max-w-[var(--wd-transcript-max-width)] text-text-muted">{sendNow}</div> : null}
|
|
467
490
|
<div
|
|
468
491
|
{...dropHandlers}
|
|
469
492
|
className={cn(
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { EngineCapabilities, ModelOption } from '@workerdeck/protocol'
|
|
2
|
+
import { Brain } from 'lucide-react'
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
4
|
+
import { cn } from '../../lib/utils.ts'
|
|
5
|
+
import { matchModel } from './ModelSelect.tsx'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_VALUE = 'default'
|
|
8
|
+
|
|
9
|
+
export function effortChoices(
|
|
10
|
+
models: readonly ModelOption[],
|
|
11
|
+
model: string | undefined,
|
|
12
|
+
capabilities: Pick<EngineCapabilities, 'reasoningEfforts'>,
|
|
13
|
+
): readonly string[] {
|
|
14
|
+
return matchModel(models, model)?.reasoningEfforts ?? capabilities.reasoningEfforts ?? []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EffortSelectProps {
|
|
18
|
+
efforts: readonly string[]
|
|
19
|
+
effort?: string | null
|
|
20
|
+
onEffortChange: (effort?: string) => void
|
|
21
|
+
variant?: 'toolbar' | 'form'
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function EffortSelect({ efforts, effort, onEffortChange, variant = 'toolbar', disabled, className }: EffortSelectProps) {
|
|
27
|
+
const items = [{ value: DEFAULT_VALUE, label: 'Default' }, ...efforts.map((value) => ({ value, label: value }))]
|
|
28
|
+
return (
|
|
29
|
+
<Select
|
|
30
|
+
items={items}
|
|
31
|
+
value={effort ?? null}
|
|
32
|
+
onValueChange={(value) => {
|
|
33
|
+
if (typeof value !== 'string' || value === effort) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
onEffortChange(value === DEFAULT_VALUE ? undefined : value)
|
|
37
|
+
}}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
>
|
|
40
|
+
<SelectTrigger
|
|
41
|
+
aria-label="Reasoning effort"
|
|
42
|
+
className={cn(
|
|
43
|
+
variant === 'toolbar' && 'h-6 max-w-32 border-transparent bg-transparent text-fg-3 hover:bg-surface-hover',
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<span className={cn('flex items-center gap-1 truncate', variant === 'toolbar' && 'font-mono text-label')}>
|
|
48
|
+
<Brain className="size-3 shrink-0" />
|
|
49
|
+
<SelectValue placeholder="effort" />
|
|
50
|
+
</span>
|
|
51
|
+
</SelectTrigger>
|
|
52
|
+
<SelectContent className="min-w-40">
|
|
53
|
+
{items.map((item) => (
|
|
54
|
+
<SelectItem key={item.value} value={item.value}>
|
|
55
|
+
<SelectItemText>
|
|
56
|
+
<span className="font-medium">{item.label}</span>
|
|
57
|
+
</SelectItemText>
|
|
58
|
+
{item.value === DEFAULT_VALUE ? <span className="text-label text-fg-4">The model's configured default</span> : null}
|
|
59
|
+
</SelectItem>
|
|
60
|
+
))}
|
|
61
|
+
</SelectContent>
|
|
62
|
+
</Select>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -39,7 +39,7 @@ function optionMatches(option: ModelOption, model: string): boolean {
|
|
|
39
39
|
return token !== '' && token === family(dropVariant(option.value))
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function matchModel(models: ModelOption[], model?: string): ModelOption | undefined {
|
|
42
|
+
export function matchModel(models: readonly ModelOption[], model?: string): ModelOption | undefined {
|
|
43
43
|
if (!model) {
|
|
44
44
|
return undefined
|
|
45
45
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
2
|
import type { HTMLAttributes, ReactNode } from 'react'
|
|
3
|
-
import { Eraser, FolderOpen, Layers, Pencil, Plus, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
3
|
+
import { BedDouble, Eraser, FolderOpen, Layers, Pencil, Plus, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
4
4
|
import { clearFilters, filterRows, groupRows, hasFacetFilter, scopeActive, sessionKey, subsetSummary } from '@workerdeck/protocol'
|
|
5
5
|
import type { SessionGroup, SessionRow, SessionTask, StepDisplay, SubagentDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
|
|
6
6
|
import { Button } from '../ui/Button.tsx'
|
|
@@ -34,6 +34,7 @@ export interface SessionBrowserProps {
|
|
|
34
34
|
onDelete?: (row: SessionRow) => void
|
|
35
35
|
onRename?: (row: SessionRow, title: string) => void
|
|
36
36
|
onClearContext?: (row: SessionRow) => void
|
|
37
|
+
onSleep?: (row: SessionRow) => void
|
|
37
38
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
38
39
|
onSelectTask?: (row: SessionRow, task: SessionTask) => void
|
|
39
40
|
onStopTask?: (row: SessionRow, toolUseId: string) => void
|
|
@@ -74,6 +75,7 @@ export function SessionBrowser({
|
|
|
74
75
|
onDelete,
|
|
75
76
|
onRename,
|
|
76
77
|
onClearContext,
|
|
78
|
+
onSleep,
|
|
77
79
|
onSelectSubagent,
|
|
78
80
|
onSelectTask,
|
|
79
81
|
onStopTask,
|
|
@@ -119,7 +121,17 @@ export function SessionBrowser({
|
|
|
119
121
|
const target = project && group.hostId ? { hostId: group.hostId, cwd: group.cwd } : undefined
|
|
120
122
|
return (
|
|
121
123
|
<GroupHeading
|
|
122
|
-
label={
|
|
124
|
+
label={
|
|
125
|
+
group.gateway && group.project ? (
|
|
126
|
+
<>
|
|
127
|
+
{group.gateway}
|
|
128
|
+
<span aria-hidden> · </span>
|
|
129
|
+
<span className="text-fg-1">{group.project}</span>
|
|
130
|
+
</>
|
|
131
|
+
) : (
|
|
132
|
+
group.label
|
|
133
|
+
)
|
|
134
|
+
}
|
|
123
135
|
count={group.rows.length}
|
|
124
136
|
title={project && group.cwd ? group.cwd : undefined}
|
|
125
137
|
leading={
|
|
@@ -218,6 +230,7 @@ export function SessionBrowser({
|
|
|
218
230
|
onDelete={onDelete}
|
|
219
231
|
onRename={onRename}
|
|
220
232
|
onClearContext={onClearContext}
|
|
233
|
+
onSleep={onSleep}
|
|
221
234
|
onSelectSubagent={onSelectSubagent}
|
|
222
235
|
onSelectTask={onSelectTask}
|
|
223
236
|
onStopTask={onStopTask}
|
|
@@ -259,6 +272,7 @@ interface SessionRowItemProps {
|
|
|
259
272
|
onDelete?: (row: SessionRow) => void
|
|
260
273
|
onRename?: (row: SessionRow, title: string) => void
|
|
261
274
|
onClearContext?: (row: SessionRow) => void
|
|
275
|
+
onSleep?: (row: SessionRow) => void
|
|
262
276
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
263
277
|
onSelectTask?: (row: SessionRow, task: SessionTask) => void
|
|
264
278
|
onStopTask?: (row: SessionRow, toolUseId: string) => void
|
|
@@ -286,6 +300,7 @@ function SessionRowItem({
|
|
|
286
300
|
onDelete,
|
|
287
301
|
onRename,
|
|
288
302
|
onClearContext,
|
|
303
|
+
onSleep,
|
|
289
304
|
onSelectSubagent,
|
|
290
305
|
onSelectTask,
|
|
291
306
|
onStopTask,
|
|
@@ -336,6 +351,15 @@ function SessionRowItem({
|
|
|
336
351
|
<Eraser className="size-3 text-fg-3" />
|
|
337
352
|
</RowAction>
|
|
338
353
|
) : null}
|
|
354
|
+
{onSleep && info.capabilities?.engineSleep && info.status === 'idle' && !info.engineAsleep ? (
|
|
355
|
+
<RowAction
|
|
356
|
+
label="Sleep session"
|
|
357
|
+
title="Stop the engine process to free its memory - the session stays here and wakes on your next message"
|
|
358
|
+
onClick={() => onSleep(row)}
|
|
359
|
+
>
|
|
360
|
+
<BedDouble className="size-3 text-fg-3" />
|
|
361
|
+
</RowAction>
|
|
362
|
+
) : null}
|
|
339
363
|
{onDelete ? (
|
|
340
364
|
<RowAction label="Close session" onClick={() => onDelete(row)}>
|
|
341
365
|
<Trash2 className="size-3 text-fg-3" />
|
|
@@ -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
|
}
|