@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ArrowRight, Check, CircleAlert } from 'lucide-react'
|
|
2
|
-
import {
|
|
3
|
-
import type { SessionInfo, ShellInfo, SubagentDisplay, SubagentInfo } from '@workerdeck/protocol'
|
|
1
|
+
import { ArrowRight, Check, Circle, CircleAlert } from 'lucide-react'
|
|
2
|
+
import { displayedTasks, isAgentRecord, subagentLabel, visibleShells, visibleSubagents } from '@workerdeck/protocol'
|
|
3
|
+
import type { SessionInfo, SessionTask, ShellInfo, StepDisplay, SubagentDisplay, SubagentInfo } from '@workerdeck/protocol'
|
|
4
4
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
5
5
|
import { cn } from '../../lib/utils.ts'
|
|
6
6
|
import { AgentWriteIcon } from '../terminal/affordances.tsx'
|
|
@@ -16,34 +16,45 @@ import {
|
|
|
16
16
|
shellTitle,
|
|
17
17
|
} from '../terminal/shell-row.ts'
|
|
18
18
|
|
|
19
|
-
export type StepKind = 'agent' | 'shell'
|
|
19
|
+
export type StepKind = 'agent' | 'shell' | 'task'
|
|
20
20
|
|
|
21
21
|
export type Step = {
|
|
22
22
|
key: string
|
|
23
23
|
kind: StepKind
|
|
24
24
|
label: string
|
|
25
25
|
noun: string
|
|
26
|
-
state: 'done' | 'running' | 'failed'
|
|
26
|
+
state: 'done' | 'running' | 'failed' | 'pending'
|
|
27
27
|
detail?: string
|
|
28
28
|
title: string
|
|
29
29
|
onSelect: () => void
|
|
30
30
|
onKill?: () => void
|
|
31
|
+
killLabel?: string
|
|
31
32
|
agentWrite?: { granted: boolean; label: string; toggle: () => void }
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export type ShellStepOptions = {
|
|
35
36
|
now: number
|
|
37
|
+
show?: StepDisplay
|
|
36
38
|
onSelect: (shellId: string) => void
|
|
37
39
|
onKill?: (shellId: string) => void
|
|
38
40
|
// Offered only where the session's agent holds the shell write tools.
|
|
39
41
|
onAgentWrite?: (shellId: string, enabled: boolean) => void
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
export type TaskStepOptions = {
|
|
45
|
+
show: StepDisplay
|
|
46
|
+
// Whether the session is still working: a checklist item left in progress after the turn ended is not running.
|
|
47
|
+
live: boolean
|
|
48
|
+
onSelect: (task: SessionTask) => void
|
|
49
|
+
onStop?: (toolUseId: string) => void
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
export function sessionSteps(
|
|
43
53
|
info: SessionInfo,
|
|
44
54
|
onSelect: (toolUseId: string) => void,
|
|
45
55
|
show: SubagentDisplay = 'all',
|
|
46
56
|
shells?: ShellStepOptions,
|
|
57
|
+
tasks?: TaskStepOptions,
|
|
47
58
|
): Step[] {
|
|
48
59
|
const agents: Step[] = visibleSubagents(info, show)
|
|
49
60
|
.filter(isAgentRecord)
|
|
@@ -57,11 +68,30 @@ export function sessionSteps(
|
|
|
57
68
|
title: `${subagentLabel(sub)} · ${sub.toolCount} tool${sub.toolCount === 1 ? '' : 's'}`,
|
|
58
69
|
onSelect: () => onSelect(sub.toolUseId),
|
|
59
70
|
}))
|
|
71
|
+
const taskSteps = tasks === undefined ? [] : displayedTasks(info, tasks.show).map((task) => taskStep(task, tasks))
|
|
60
72
|
if (shells === undefined) {
|
|
61
|
-
return agents
|
|
73
|
+
return [...agents, ...taskSteps]
|
|
62
74
|
}
|
|
63
75
|
const grants = info.shellAgentWrite !== undefined
|
|
64
|
-
|
|
76
|
+
const shellSteps = visibleShells(info, shells.show ?? 'active', shells.now).map((shell) => shellStep(shell, shells, grants))
|
|
77
|
+
return [...agents, ...taskSteps, ...shellSteps]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function taskStep(task: SessionTask, options: TaskStepOptions): Step {
|
|
81
|
+
const stalled = task.source === 'checklist' && task.state === 'running' && !options.live
|
|
82
|
+
const toolUseId = task.toolUseId
|
|
83
|
+
return {
|
|
84
|
+
key: task.key,
|
|
85
|
+
kind: 'task',
|
|
86
|
+
label: task.label,
|
|
87
|
+
noun: 'task',
|
|
88
|
+
state: stalled ? 'pending' : task.state,
|
|
89
|
+
detail: task.detail,
|
|
90
|
+
title: stalled ? `${task.label} · left in progress` : task.label,
|
|
91
|
+
onSelect: () => options.onSelect(task),
|
|
92
|
+
onKill: task.stoppable && toolUseId !== undefined && options.onStop ? () => options.onStop?.(toolUseId) : undefined,
|
|
93
|
+
killLabel: 'Stop this task',
|
|
94
|
+
}
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
function shellStep(shell: ShellInfo, options: ShellStepOptions, grants: boolean): Step {
|
|
@@ -77,6 +107,7 @@ function shellStep(shell: ShellInfo, options: ShellStepOptions, grants: boolean)
|
|
|
77
107
|
title: `${shellTitle(shell)} · ${status}`,
|
|
78
108
|
onSelect: () => options.onSelect(shell.id),
|
|
79
109
|
onKill: running && options.onKill ? () => options.onKill?.(shell.id) : undefined,
|
|
110
|
+
killLabel: 'Kill this shell',
|
|
80
111
|
agentWrite:
|
|
81
112
|
grants && options.onAgentWrite && shellGrantable(shell)
|
|
82
113
|
? {
|
|
@@ -103,7 +134,16 @@ function stepState(status: SubagentInfo['status']): Step['state'] {
|
|
|
103
134
|
}
|
|
104
135
|
|
|
105
136
|
export function StepRow({ step, active = false, onSelect }: { step: Step; active?: boolean; onSelect: () => void }) {
|
|
106
|
-
const body =
|
|
137
|
+
const body =
|
|
138
|
+
step.state === 'failed'
|
|
139
|
+
? 'text-danger'
|
|
140
|
+
: step.kind === 'shell'
|
|
141
|
+
? 'text-[var(--wd-shell-accent)]'
|
|
142
|
+
: step.kind === 'task'
|
|
143
|
+
? step.state === 'pending'
|
|
144
|
+
? 'text-fg-4'
|
|
145
|
+
: 'text-fg-2'
|
|
146
|
+
: 'text-success'
|
|
107
147
|
return (
|
|
108
148
|
<div className={cn('flex w-full items-center rounded-[4px] pr-1.5', active ? 'bg-row-selected' : 'hover:bg-row-active', body)}>
|
|
109
149
|
<button
|
|
@@ -119,7 +159,7 @@ export function StepRow({ step, active = false, onSelect }: { step: Step; active
|
|
|
119
159
|
<StepIcon step={step} />
|
|
120
160
|
<span className="min-w-0 flex-1 truncate">{step.label}</span>
|
|
121
161
|
{step.detail ? <span className="shrink-0 tabular-nums text-fg-4">{step.detail}</span> : null}
|
|
122
|
-
{step.kind === '
|
|
162
|
+
{step.kind === 'agent' ? <ArrowRight className="size-3.5 shrink-0 text-fg-4" /> : null}
|
|
123
163
|
</button>
|
|
124
164
|
{step.agentWrite ? (
|
|
125
165
|
<button
|
|
@@ -142,8 +182,8 @@ export function StepRow({ step, active = false, onSelect }: { step: Step; active
|
|
|
142
182
|
{step.onKill ? (
|
|
143
183
|
<button
|
|
144
184
|
type="button"
|
|
145
|
-
aria-label={
|
|
146
|
-
title=
|
|
185
|
+
aria-label={`${step.kind === 'task' ? 'Stop' : 'Kill'} ${step.label}`}
|
|
186
|
+
title={step.killLabel}
|
|
147
187
|
onClick={(e) => {
|
|
148
188
|
e.stopPropagation()
|
|
149
189
|
step.onKill?.()
|
|
@@ -168,6 +208,9 @@ function StepIcon({ step }: { step: Step }) {
|
|
|
168
208
|
case 'failed': {
|
|
169
209
|
return <CircleAlert className="size-[11px] shrink-0" />
|
|
170
210
|
}
|
|
211
|
+
case 'pending': {
|
|
212
|
+
return <Circle className="size-[11px] shrink-0" />
|
|
213
|
+
}
|
|
171
214
|
default: {
|
|
172
215
|
return <Check className="size-[11px] shrink-0" />
|
|
173
216
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { X } from 'lucide-react'
|
|
2
2
|
import type { ShellInfo } from '@workerdeck/protocol'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
|
-
import { Button } from '../ui/Button.tsx'
|
|
5
|
-
import { Ink, Row } from '../terminal/row.tsx'
|
|
6
|
-
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
7
4
|
import { shellAgentWriteLabel, shellGrantable, shellInfoFailed, shellInfoStatusText, shellTitle } from '../terminal/shell-row.ts'
|
|
8
|
-
import { AgentWriteAction, AgentWriteIcon, KillShellAction
|
|
5
|
+
import { AgentWriteAction, AgentWriteIcon, KillShellAction } from '../terminal/affordances.tsx'
|
|
6
|
+
import { FrameStrip } from './FrameStrip.tsx'
|
|
9
7
|
|
|
10
8
|
export interface ShellStripProps {
|
|
11
9
|
shell: ShellInfo | undefined
|
|
@@ -29,43 +27,13 @@ export function ShellStrip({ shell, label, cols, onBack, onKill, onAgentWrite, t
|
|
|
29
27
|
const granted = shell?.agentWrite === true
|
|
30
28
|
const grant = shell && onAgentWrite && shellGrantable(shell) ? () => onAgentWrite(!granted) : undefined
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{grant && shell ? <AgentWriteAction granted={granted} label={shellAgentWriteLabel(shell)} onToggle={grant} /> : null}
|
|
40
|
-
{running && onKill ? <KillShellAction onKill={onKill} /> : null}
|
|
41
|
-
</>
|
|
42
|
-
}
|
|
43
|
-
>
|
|
44
|
-
<Row glyph="←" glyphTone="dim" indent={1} tone={failed ? 'red' : 'magenta'}>
|
|
45
|
-
<button type="button" onClick={onBack} aria-label="Back to the session" className="cursor-pointer text-left">
|
|
46
|
-
{name}
|
|
47
|
-
</button>
|
|
48
|
-
{status ? <Ink tone={failed ? 'red' : running ? 'magenta' : 'dim'}> · {status}</Ink> : null}
|
|
49
|
-
{detail ? <Ink tone="faint"> · {detail}</Ink> : null}
|
|
50
|
-
</Row>
|
|
51
|
-
</WithActions>
|
|
52
|
-
</TerminalSurface>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1.5">
|
|
58
|
-
<Button variant="ghost" size="sm" onClick={onBack} className="h-6 gap-1 px-1.5">
|
|
59
|
-
<ArrowLeft className="size-3.5" />
|
|
60
|
-
Back
|
|
61
|
-
</Button>
|
|
62
|
-
<span className={cn('min-w-0 flex-1 truncate font-mono text-body-sm', failed ? 'text-danger' : 'text-fg-2')}>{name}</span>
|
|
63
|
-
{status ? (
|
|
64
|
-
<span className={cn('shrink-0 text-label', failed ? 'text-danger' : running ? 'text-[var(--wd-shell-accent)]' : 'text-fg-3')}>
|
|
65
|
-
{status}
|
|
66
|
-
</span>
|
|
67
|
-
) : null}
|
|
68
|
-
{detail ? <span className="shrink-0 text-label text-fg-4">{detail}</span> : null}
|
|
30
|
+
const actions = terminal ? (
|
|
31
|
+
<>
|
|
32
|
+
{grant && shell ? <AgentWriteAction granted={granted} label={shellAgentWriteLabel(shell)} onToggle={grant} /> : null}
|
|
33
|
+
{running && onKill ? <KillShellAction onKill={onKill} /> : null}
|
|
34
|
+
</>
|
|
35
|
+
) : (
|
|
36
|
+
<>
|
|
69
37
|
{grant && shell ? (
|
|
70
38
|
<button
|
|
71
39
|
type="button"
|
|
@@ -89,6 +57,22 @@ export function ShellStrip({ shell, label, cols, onBack, onKill, onAgentWrite, t
|
|
|
89
57
|
<X className="size-3.5" />
|
|
90
58
|
</button>
|
|
91
59
|
) : null}
|
|
92
|
-
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<FrameStrip
|
|
65
|
+
kind="shell"
|
|
66
|
+
name={name}
|
|
67
|
+
status={status}
|
|
68
|
+
detail={detail}
|
|
69
|
+
failed={failed}
|
|
70
|
+
live={running}
|
|
71
|
+
onBack={onBack}
|
|
72
|
+
actions={actions}
|
|
73
|
+
terminal={terminal}
|
|
74
|
+
fontSize={fontSize}
|
|
75
|
+
lineHeight={lineHeight}
|
|
76
|
+
/>
|
|
93
77
|
)
|
|
94
78
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import { ArrowLeft } from 'lucide-react'
|
|
2
1
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
|
-
import { cn } from '../../lib/utils.ts'
|
|
4
2
|
import { formatDuration } from '../../lib/format.ts'
|
|
5
|
-
import { Button } from '../ui/Button.tsx'
|
|
6
|
-
import { Ink, Row } from '../terminal/row.tsx'
|
|
7
|
-
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
8
3
|
import { taskBusy, taskFailed, taskIdentity } from '../terminal/tool-run.ts'
|
|
9
4
|
import type { ToolCallItem } from '../terminal/blocks.ts'
|
|
10
5
|
import { usePulse } from './pulse.tsx'
|
|
11
6
|
import { useTicker } from '../terminal/items.tsx'
|
|
7
|
+
import { FrameStrip } from './FrameStrip.tsx'
|
|
12
8
|
|
|
13
9
|
export function SubagentStrip({
|
|
14
10
|
task,
|
|
@@ -39,31 +35,18 @@ export function SubagentStrip({
|
|
|
39
35
|
const status = !task ? undefined : failed ? 'failed' : busy ? `${pulse} working…` : 'done'
|
|
40
36
|
const detail = [tools > 0 ? `${tools} tool${tools === 1 ? '' : 's'}` : undefined, elapsed].filter(Boolean).join(' · ')
|
|
41
37
|
|
|
42
|
-
if (terminal) {
|
|
43
|
-
return (
|
|
44
|
-
<TerminalSurface fontSize={fontSize} lineHeight={lineHeight} className="shrink-0">
|
|
45
|
-
<button type="button" onClick={onBack} aria-label="Back to the session" className="block w-full cursor-pointer text-left">
|
|
46
|
-
<Row glyph="←" glyphTone="dim" indent={1} tone={failed ? 'red' : 'green'}>
|
|
47
|
-
{name}
|
|
48
|
-
{status ? <Ink tone={failed ? 'red' : busy ? 'mark' : 'dim'}> · {status}</Ink> : null}
|
|
49
|
-
{detail ? <Ink tone="faint"> · {detail}</Ink> : null}
|
|
50
|
-
</Row>
|
|
51
|
-
</button>
|
|
52
|
-
</TerminalSurface>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
38
|
return (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
39
|
+
<FrameStrip
|
|
40
|
+
kind="subagent"
|
|
41
|
+
name={name}
|
|
42
|
+
status={status}
|
|
43
|
+
detail={detail}
|
|
44
|
+
failed={failed}
|
|
45
|
+
live={busy}
|
|
46
|
+
onBack={onBack}
|
|
47
|
+
terminal={terminal}
|
|
48
|
+
fontSize={fontSize}
|
|
49
|
+
lineHeight={lineHeight}
|
|
50
|
+
/>
|
|
68
51
|
)
|
|
69
52
|
}
|
|
@@ -243,3 +243,16 @@ export function Transcript({
|
|
|
243
243
|
</TranscriptVariantProvider>
|
|
244
244
|
)
|
|
245
245
|
}
|
|
246
|
+
|
|
247
|
+
export interface TerminalTranscriptProps {
|
|
248
|
+
state: TranscriptState
|
|
249
|
+
fileUrl?: (path: string) => string
|
|
250
|
+
fontSize?: number
|
|
251
|
+
lineHeight?: number
|
|
252
|
+
affordances?: TerminalAffordances | boolean
|
|
253
|
+
className?: string
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function TerminalTranscript(props: TerminalTranscriptProps) {
|
|
257
|
+
return <Transcript {...props} variant="terminal" />
|
|
258
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MessageAttachment } from '@workerdeck/protocol'
|
|
2
|
-
import {
|
|
2
|
+
import { useState } from 'react'
|
|
3
3
|
import type { ShellItem, TranscriptItem } from '@workerdeck/react'
|
|
4
4
|
import { cn } from '../../lib/utils.ts'
|
|
5
5
|
import { compactionText, formatCost, formatDuration, formatRelativeTime } from '../../lib/format.ts'
|
|
@@ -13,17 +13,9 @@ import { Row } from '../terminal/row.tsx'
|
|
|
13
13
|
import { TerminalItemView } from '../terminal/TerminalTranscript.tsx'
|
|
14
14
|
import { ViewableImage } from './image-viewer.tsx'
|
|
15
15
|
import { peerLabel } from '../terminal/items.tsx'
|
|
16
|
-
import { useShellActions } from './shell-actions.tsx'
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
shellAgentWriteLabel,
|
|
20
|
-
shellBodyLines,
|
|
21
|
-
shellFailed,
|
|
22
|
-
shellFooterText,
|
|
23
|
-
shellGrantable,
|
|
24
|
-
shellLabel,
|
|
25
|
-
shellStatusText,
|
|
26
|
-
} from '../terminal/shell-row.ts'
|
|
16
|
+
import { ShellItemActions, useShellActions, useVerifyRunning } from './shell-actions.tsx'
|
|
17
|
+
import { BookmarkAction, CopyAction, WithActions } from '../terminal/affordances.tsx'
|
|
18
|
+
import { shellBodyLines, shellFailed, shellFooterText, shellLabel, shellStatusText } from '../terminal/shell-row.ts'
|
|
27
19
|
|
|
28
20
|
function TurnResultRow({ item }: { item: Extract<TranscriptItem, { kind: 'turn_result' }> }) {
|
|
29
21
|
return (
|
|
@@ -166,37 +158,15 @@ function ToolCard({
|
|
|
166
158
|
function ShellCard({ item }: { item: ShellItem }) {
|
|
167
159
|
const [open, setOpen] = useState(false)
|
|
168
160
|
const actions = useShellActions()
|
|
169
|
-
const running = item.shell.status === 'running'
|
|
170
161
|
const failed = shellFailed(item)
|
|
171
162
|
const lines = shellBodyLines(item, open)
|
|
172
163
|
const footer = shellFooterText(item, open, lines.length)
|
|
173
164
|
|
|
174
165
|
const shellId = item.shell.id
|
|
175
|
-
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
if (running) {
|
|
178
|
-
void verify(shellId)
|
|
179
|
-
}
|
|
180
|
-
}, [running, shellId, verify])
|
|
166
|
+
useVerifyRunning(item)
|
|
181
167
|
|
|
182
168
|
return (
|
|
183
|
-
<WithActions
|
|
184
|
-
actions={
|
|
185
|
-
<>
|
|
186
|
-
{actions.open ? <OpenShellAction onOpen={() => actions.open?.(shellId)} /> : null}
|
|
187
|
-
{actions.agentWrite && shellGrantable(item.shell) ? (
|
|
188
|
-
<AgentWriteAction
|
|
189
|
-
granted={item.shell.agentWrite === true}
|
|
190
|
-
label={shellAgentWriteLabel(item.shell)}
|
|
191
|
-
onToggle={() => void actions.agentWrite?.(shellId, item.shell.agentWrite !== true)}
|
|
192
|
-
/>
|
|
193
|
-
) : null}
|
|
194
|
-
{running ? <KillShellAction onKill={() => void actions.kill(shellId)} /> : null}
|
|
195
|
-
<BookmarkAction id={item.id} />
|
|
196
|
-
<CopyAction text={item.shell.command} label="Copy command" />
|
|
197
|
-
</>
|
|
198
|
-
}
|
|
199
|
-
>
|
|
169
|
+
<WithActions actions={<ShellItemActions item={item} />}>
|
|
200
170
|
<div data-slot="shell" className="overflow-hidden rounded-md border border-border bg-surface">
|
|
201
171
|
<div className="flex items-center gap-2 px-3 py-1.5">
|
|
202
172
|
<span className="font-mono text-label text-[var(--wd-shell-accent)]">$</span>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SkillInfo, SlashCommandInfo } from '@workerdeck/protocol'
|
|
1
|
+
import type { EngineCapabilities, PermissionMode, SkillInfo, SlashCommandInfo } from '@workerdeck/protocol'
|
|
2
2
|
|
|
3
3
|
export type ClientCommand = {
|
|
4
4
|
name: string
|
|
@@ -170,3 +170,82 @@ export function composerCommandRows(rows: readonly ComposerRow[]): ComposerComma
|
|
|
170
170
|
},
|
|
171
171
|
)
|
|
172
172
|
}
|
|
173
|
+
|
|
174
|
+
export type ClientCommandPanel = 'info' | 'context' | 'usage' | 'mcp' | 'skills'
|
|
175
|
+
|
|
176
|
+
export type ClientCommandSources = {
|
|
177
|
+
capabilities: Pick<EngineCapabilities, 'permissionModes' | 'clearContext' | 'mcpStatus' | 'contextUsage' | 'rateLimits' | 'skillsList'>
|
|
178
|
+
hasModels: boolean
|
|
179
|
+
setModel: (model: string) => void
|
|
180
|
+
setPermissionMode: (mode: PermissionMode) => void
|
|
181
|
+
clearContext: () => void
|
|
182
|
+
openPanel: (panel: ClientCommandPanel) => void
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function buildClientCommands({
|
|
186
|
+
capabilities,
|
|
187
|
+
hasModels,
|
|
188
|
+
setModel,
|
|
189
|
+
setPermissionMode,
|
|
190
|
+
clearContext,
|
|
191
|
+
openPanel,
|
|
192
|
+
}: ClientCommandSources): ClientCommand[] {
|
|
193
|
+
const modes = capabilities.permissionModes
|
|
194
|
+
const built: ClientCommand[] = []
|
|
195
|
+
if (hasModels) {
|
|
196
|
+
built.push({
|
|
197
|
+
name: 'model',
|
|
198
|
+
description: 'Switch the model for this session',
|
|
199
|
+
argumentHint: '<model>',
|
|
200
|
+
requiresArgs: true,
|
|
201
|
+
run: (args) => {
|
|
202
|
+
const wanted = args.split(/\s+/)[0]
|
|
203
|
+
if (!wanted) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
setModel(wanted)
|
|
207
|
+
return true
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
if (modes.length > 1) {
|
|
212
|
+
built.push({
|
|
213
|
+
name: 'permissions',
|
|
214
|
+
description: `Set the permission mode (${modes.join(', ')})`,
|
|
215
|
+
argumentHint: '<mode>',
|
|
216
|
+
requiresArgs: true,
|
|
217
|
+
run: (args) => {
|
|
218
|
+
const wanted = args.split(/\s+/)[0] as PermissionMode
|
|
219
|
+
if (!modes.includes(wanted)) {
|
|
220
|
+
return false
|
|
221
|
+
}
|
|
222
|
+
setPermissionMode(wanted)
|
|
223
|
+
return true
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
if (capabilities.clearContext) {
|
|
228
|
+
built.push({
|
|
229
|
+
name: 'clear',
|
|
230
|
+
description: 'Clear the conversation - the session keeps running and the old one stays resumable',
|
|
231
|
+
run: () => {
|
|
232
|
+
clearContext()
|
|
233
|
+
return true
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
if (capabilities.mcpStatus) {
|
|
238
|
+
built.push({ name: 'mcp', description: 'MCP servers and their status', run: () => (openPanel('mcp'), true) })
|
|
239
|
+
}
|
|
240
|
+
if (capabilities.contextUsage) {
|
|
241
|
+
built.push({ name: 'context', description: 'Context window usage', run: () => (openPanel('context'), true) })
|
|
242
|
+
}
|
|
243
|
+
built.push({ name: 'status', description: 'Session details', run: () => (openPanel('info'), true) })
|
|
244
|
+
if (capabilities.rateLimits) {
|
|
245
|
+
built.push({ name: 'usage', description: 'Rate limits and spend', run: () => (openPanel('usage'), true) })
|
|
246
|
+
}
|
|
247
|
+
if (capabilities.skillsList) {
|
|
248
|
+
built.push({ name: 'skills', description: 'Browse the skills this session can use', run: () => (openPanel('skills'), true) })
|
|
249
|
+
}
|
|
250
|
+
return built
|
|
251
|
+
}
|
|
@@ -11,6 +11,10 @@ const LOCAL_PREFIX = 'https://local-image.invalid/'
|
|
|
11
11
|
|
|
12
12
|
const WEB_SOURCE = /^(?:https?:|data:image\/|blob:)/i
|
|
13
13
|
|
|
14
|
+
const REMOTE_SOURCE = /^https?:/i
|
|
15
|
+
|
|
16
|
+
export const REMOTE_IMAGE_NOTICE = 'remote image, not loaded'
|
|
17
|
+
|
|
14
18
|
type HastNode = { type: string; tagName?: string; properties?: Record<string, unknown>; children?: HastNode[] }
|
|
15
19
|
|
|
16
20
|
function shieldLocalImages() {
|
|
@@ -47,9 +51,53 @@ export function localImagePath(src: string | undefined): string | undefined {
|
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
// A remote image loads the moment it renders, so a transcript an agent was talked into writing could beacon out
|
|
55
|
+
// whatever it can read. It is shown as a link instead: opening it is the user's decision, and a navigation, not a load.
|
|
56
|
+
export function remoteImageHost(src: string | undefined): string | undefined {
|
|
57
|
+
if (!src || !REMOTE_SOURCE.test(src.trim()) || localImagePath(src) !== undefined) {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return new URL(src.trim()).host
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
export function MarkdownImage({ src, alt, terminal }: { src?: unknown; alt?: unknown; terminal?: boolean }) {
|
|
51
|
-
const links = useFileLinks()
|
|
52
68
|
const raw = typeof src === 'string' ? src : undefined
|
|
69
|
+
const remoteHost = remoteImageHost(raw)
|
|
70
|
+
if (remoteHost !== undefined) {
|
|
71
|
+
return <RemoteImageLink href={raw!.trim()} host={remoteHost} alt={alt} terminal={terminal} />
|
|
72
|
+
}
|
|
73
|
+
return <LoadedMarkdownImage raw={raw} alt={alt} terminal={terminal} />
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function RemoteImageLink({ href, host, alt, terminal }: { href: string; host: string; alt?: unknown; terminal?: boolean }) {
|
|
77
|
+
const label = typeof alt === 'string' && alt ? `${alt} · ` : ''
|
|
78
|
+
const text = `${label}${host} (${REMOTE_IMAGE_NOTICE})`
|
|
79
|
+
if (terminal) {
|
|
80
|
+
return (
|
|
81
|
+
<a href={href} target="_blank" rel="noopener noreferrer" data-tone="faint" title={href}>
|
|
82
|
+
{text}
|
|
83
|
+
</a>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
<a
|
|
88
|
+
href={href}
|
|
89
|
+
target="_blank"
|
|
90
|
+
rel="noopener noreferrer"
|
|
91
|
+
title={href}
|
|
92
|
+
className="my-1 inline-block rounded-md border border-border bg-surface-hover px-2 py-1 text-label text-fg-4"
|
|
93
|
+
>
|
|
94
|
+
{text}
|
|
95
|
+
</a>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function LoadedMarkdownImage({ raw, alt, terminal }: { raw?: string; alt?: unknown; terminal?: boolean }) {
|
|
100
|
+
const links = useFileLinks()
|
|
53
101
|
const local = localImagePath(raw)
|
|
54
102
|
const path = local === undefined ? undefined : parseFileLink(local, links?.cwd)?.path
|
|
55
103
|
const host = useHostImageSrc(path)
|
|
@@ -8,9 +8,12 @@ export type MarkKind = 'user' | 'subagent' | 'shell' | 'turn' | 'turnFailed' | '
|
|
|
8
8
|
export type Mark = {
|
|
9
9
|
kind: MarkKind
|
|
10
10
|
itemIndex: number
|
|
11
|
+
rowIndex?: number
|
|
11
12
|
turnIndex?: number
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export type Placement = (mark: Mark) => { y: number; h: number }
|
|
16
|
+
|
|
14
17
|
export type Cluster = {
|
|
15
18
|
lane: Lane
|
|
16
19
|
kind: MarkKind
|
|
@@ -106,11 +109,30 @@ export interface BuildMarksOptions {
|
|
|
106
109
|
frameParentId?: string
|
|
107
110
|
bookmarks?: readonly number[]
|
|
108
111
|
recapItemIndex?: number
|
|
112
|
+
rowIndexFor?: (itemIndex: number) => number
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function toolCallFailed(item: TranscriptItem): boolean {
|
|
116
|
+
return item.kind === 'tool_call' && (item.status === 'failed' || item.result?.isError === true)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function rowOutcomes(
|
|
120
|
+
items: readonly TranscriptItem[],
|
|
121
|
+
frameParentId: string | undefined,
|
|
122
|
+
rowIndexFor: (itemIndex: number) => number,
|
|
123
|
+
): Set<number> {
|
|
124
|
+
const outcome = new Map<number, number>()
|
|
125
|
+
items.forEach((item, index) => {
|
|
126
|
+
if (item.kind === 'tool_call' && parentOf(item) === frameParentId) {
|
|
127
|
+
outcome.set(rowIndexFor(index), index)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
return new Set(outcome.values())
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
export function buildMarks(
|
|
112
134
|
items: readonly TranscriptItem[],
|
|
113
|
-
{ frameParentId, bookmarks = [], recapItemIndex }: BuildMarksOptions = {},
|
|
135
|
+
{ frameParentId, bookmarks = [], recapItemIndex, rowIndexFor = (index) => index }: BuildMarksOptions = {},
|
|
114
136
|
): Mark[] {
|
|
115
137
|
const marks: Mark[] = []
|
|
116
138
|
const subagentParents = new Set<string>()
|
|
@@ -120,6 +142,7 @@ export function buildMarks(
|
|
|
120
142
|
subagentParents.add(parent)
|
|
121
143
|
}
|
|
122
144
|
}
|
|
145
|
+
const outcomes = rowOutcomes(items, frameParentId, rowIndexFor)
|
|
123
146
|
let segment: Segment = {}
|
|
124
147
|
const closeSegment = () => {
|
|
125
148
|
const anchor = segment.response ?? segment.turn
|
|
@@ -147,11 +170,7 @@ export function buildMarks(
|
|
|
147
170
|
marks.push({ kind: 'error', itemIndex: index })
|
|
148
171
|
} else if (item.kind === 'shell') {
|
|
149
172
|
marks.push({ kind: 'shell', itemIndex: index })
|
|
150
|
-
} else if (
|
|
151
|
-
item.kind === 'tool_call' &&
|
|
152
|
-
parentOf(item) === frameParentId &&
|
|
153
|
-
(item.status === 'failed' || item.result?.isError === true)
|
|
154
|
-
) {
|
|
173
|
+
} else if (toolCallFailed(item) && outcomes.has(index)) {
|
|
155
174
|
marks.push({ kind: 'toolFailed', itemIndex: index })
|
|
156
175
|
} else if (item.kind === 'assistant_text' && parentOf(item) === frameParentId) {
|
|
157
176
|
if (frameParentId !== undefined) {
|
|
@@ -173,22 +192,25 @@ export function buildMarks(
|
|
|
173
192
|
return marks
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
export function
|
|
195
|
+
export function proportionalPlacement(railH: number, itemCount: number): Placement {
|
|
177
196
|
const count = Math.max(1, itemCount)
|
|
178
197
|
const h = Math.max(MIN_MARK, Math.round(railH / count))
|
|
179
|
-
|
|
198
|
+
return (mark) => ({ y: Math.min(Math.max(0, railH - h), Math.round((mark.itemIndex / count) * railH)), h })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function clusterMarks(marks: readonly Mark[], place: Placement): Cluster[] {
|
|
202
|
+
const lanes = new Map<Lane, { mark: Mark; y: number; h: number }[]>()
|
|
180
203
|
for (const mark of marks) {
|
|
181
|
-
const y = Math.min(Math.max(0, railH - h), Math.round((mark.itemIndex / count) * railH))
|
|
182
204
|
const lane = LANE[mark.kind]
|
|
183
205
|
const list = lanes.get(lane) ?? []
|
|
184
|
-
list.push({ mark,
|
|
206
|
+
list.push({ mark, ...place(mark) })
|
|
185
207
|
lanes.set(lane, list)
|
|
186
208
|
}
|
|
187
209
|
const clusters: Cluster[] = []
|
|
188
210
|
for (const [lane, list] of lanes) {
|
|
189
211
|
list.sort((a, b) => a.y - b.y)
|
|
190
212
|
let current: Cluster | null = null
|
|
191
|
-
for (const { mark, y } of list) {
|
|
213
|
+
for (const { mark, y, h } of list) {
|
|
192
214
|
if (current && y <= current.y + current.h + 1) {
|
|
193
215
|
current.h = Math.max(current.h, y + h - current.y)
|
|
194
216
|
if (LOUDNESS[mark.kind] > LOUDNESS[current.kind]) {
|