@workerdeck/ui 2.15.0 → 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/README.md +6 -5
- package/build/{SessionPanel-BVKqPs_3.d.mts → SessionPanel-BNNc8grK.d.mts} +18 -16
- package/build/index.d.mts +194 -49
- package/build/index.mjs +562 -260
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +170 -21
- package/build/{use-path-links-BgWXmfAS.mjs → use-path-links-C6Fh5zdx.mjs} +3312 -2758
- package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
- package/build/workspace.d.mts +3 -3
- package/build/workspace.mjs +3 -3
- package/build/workspace.mjs.map +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 +22 -17
- package/src/components/agent/QuestionPrompt.tsx +6 -41
- package/src/components/agent/Response.tsx +8 -1
- package/src/components/agent/Scrubber.tsx +6 -75
- package/src/components/agent/SessionBrowser.tsx +131 -214
- package/src/components/agent/SessionFilters.tsx +266 -0
- package/src/components/agent/SessionInfoDialog.tsx +47 -63
- package/src/components/agent/SessionItem.tsx +29 -6
- package/src/components/agent/SessionPanel.tsx +249 -498
- package/src/components/agent/SessionSteps.tsx +98 -14
- package/src/components/agent/SessionWorkspace.tsx +3 -3
- package/src/components/agent/ShellStrip.tsx +43 -36
- package/src/components/agent/ShellTerminal.tsx +1 -1
- package/src/components/agent/SubagentStrip.tsx +13 -30
- package/src/components/agent/ToolCallCard.tsx +18 -41
- package/src/components/agent/Transcript.tsx +111 -93
- package/src/components/agent/TranscriptItemView.tsx +80 -67
- package/src/components/agent/TranscriptRows.tsx +4 -2
- package/src/components/agent/composer-commands.ts +80 -1
- package/src/components/agent/image-viewer.tsx +326 -0
- package/src/components/agent/markdown-image.tsx +139 -0
- 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 +38 -1
- package/src/components/agent/tool-result-image.tsx +40 -0
- package/src/components/agent/transcript-variant.tsx +3 -21
- 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/prompt-area/use-prompt-area-keydown.ts +4 -2
- package/src/components/terminal/PermissionPrompt.tsx +31 -28
- package/src/components/terminal/QuestionPrompt.tsx +10 -46
- package/src/components/terminal/TerminalTranscript.tsx +23 -89
- package/src/components/terminal/affordances.tsx +100 -74
- package/src/components/terminal/height.ts +48 -32
- package/src/components/terminal/image-box.ts +31 -0
- package/src/components/terminal/items.tsx +70 -30
- package/src/components/terminal/markdown.tsx +4 -1
- package/src/components/terminal/scrubber.tsx +25 -272
- package/src/components/terminal/shell-row.ts +17 -3
- package/src/components/ui/Popover.tsx +29 -0
- package/src/components/ui/Select.tsx +25 -0
- package/src/index.ts +65 -12
- 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/src/lib/shell-request.ts +115 -0
- package/src/lib/tool-icon.ts +13 -1
- package/src/styles/terminal.css +129 -22
- package/src/styles/theme.css +4 -0
- package/build/use-path-links-BgWXmfAS.mjs.map +0 -1
- package/src/components/prompt-area/use-markdown-mode.ts +0 -103
|
@@ -4,7 +4,7 @@ import { cn } from '../../lib/utils.ts'
|
|
|
4
4
|
import { Conversation, ConversationContent, ConversationScrollButton } from './Conversation.tsx'
|
|
5
5
|
import { Loader } from './Loader.tsx'
|
|
6
6
|
import { SessionEmptyState } from './SessionEmptyState.tsx'
|
|
7
|
-
import { type TerminalAffordances } from '../terminal/affordances.tsx'
|
|
7
|
+
import { AffordanceProvider, resolveAffordances, type TerminalAffordances } from '../terminal/affordances.tsx'
|
|
8
8
|
import { WorkingRow } from '../terminal/items.tsx'
|
|
9
9
|
import { subagentItems, terminalBlocks, type ToolCallItem } from '../terminal/blocks.ts'
|
|
10
10
|
import { taskBrief, taskBusy } from '../terminal/tool-run.ts'
|
|
@@ -12,7 +12,8 @@ import { type TranscriptRow } from './transcript-rows.ts'
|
|
|
12
12
|
import { TranscriptRows } from './TranscriptRows.tsx'
|
|
13
13
|
import { SessionNamesProvider, SkillNamesProvider } from './PromptTokenText.tsx'
|
|
14
14
|
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
15
|
-
import { ROW_GAP, TranscriptVariantProvider, type
|
|
15
|
+
import { ROW_GAP, TranscriptVariantProvider, type TranscriptVariant } from './transcript-variant.tsx'
|
|
16
|
+
import { HostImageProvider } from './tool-result-image.tsx'
|
|
16
17
|
|
|
17
18
|
function useRunStart(status: TranscriptState['status']): number | undefined {
|
|
18
19
|
const running = status === 'running' || status === 'starting'
|
|
@@ -54,7 +55,11 @@ function TerminalShell({
|
|
|
54
55
|
children: ReactNode
|
|
55
56
|
}) {
|
|
56
57
|
if (!active) {
|
|
57
|
-
return
|
|
58
|
+
return (
|
|
59
|
+
<div data-cards="" className="contents">
|
|
60
|
+
<AffordanceProvider value={resolveAffordances(affordances)}>{children}</AffordanceProvider>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
58
63
|
}
|
|
59
64
|
return (
|
|
60
65
|
<TerminalSurface fontSize={fontSize} lineHeight={lineHeight} affordances={affordances} bleed="1ch" className="term-transcript">
|
|
@@ -72,7 +77,6 @@ export interface TranscriptProps {
|
|
|
72
77
|
canBrowseFiles?: boolean
|
|
73
78
|
hostImage?: (path: string) => Promise<string | undefined>
|
|
74
79
|
variant?: TranscriptVariant
|
|
75
|
-
density?: TranscriptDensity
|
|
76
80
|
fontSize?: number
|
|
77
81
|
lineHeight?: number
|
|
78
82
|
affordances?: TerminalAffordances | boolean
|
|
@@ -99,7 +103,6 @@ export function Transcript({
|
|
|
99
103
|
canBrowseFiles,
|
|
100
104
|
hostImage,
|
|
101
105
|
variant = 'cards',
|
|
102
|
-
density = 'comfortable',
|
|
103
106
|
fontSize,
|
|
104
107
|
lineHeight,
|
|
105
108
|
affordances,
|
|
@@ -124,7 +127,7 @@ export function Transcript({
|
|
|
124
127
|
frame ? state.items.find((item): item is ToolCallItem => item.kind === 'tool_call' && item.id === frame.parentToolUseId) : undefined,
|
|
125
128
|
[state.items, frame],
|
|
126
129
|
)
|
|
127
|
-
const gap = ROW_GAP[variant]
|
|
130
|
+
const gap = ROW_GAP[variant]
|
|
128
131
|
const runStartedAt = useRunStart(state.status)
|
|
129
132
|
const boundary = !frame && catchUp && catchUp.from > 0 && catchUp.from < state.items.length ? catchUp.from : undefined
|
|
130
133
|
const recap = useMemo(() => (boundary === undefined ? undefined : recapLine(summarizeSince(state, boundary))), [state, boundary])
|
|
@@ -143,98 +146,113 @@ export function Transcript({
|
|
|
143
146
|
const skillNames = useMemo(() => state.skills?.map((skill) => skill.name), [state.skills])
|
|
144
147
|
return (
|
|
145
148
|
<TranscriptVariantProvider value={variant}>
|
|
146
|
-
<
|
|
147
|
-
<
|
|
148
|
-
<
|
|
149
|
-
<
|
|
150
|
-
<
|
|
151
|
-
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
emptyState
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
168
|
-
) : null}
|
|
169
|
-
{frame && frameTask === undefined && !replaying ? null : (
|
|
170
|
-
<TranscriptRows
|
|
171
|
-
rows={rows}
|
|
172
|
-
boundary={boundary}
|
|
173
|
-
since={catchUp?.since}
|
|
174
|
-
terminal={terminal}
|
|
175
|
-
replaying={replaying}
|
|
176
|
-
stickyPrompt={!frame && stickyPrompt}
|
|
177
|
-
gap={gap}
|
|
178
|
-
fontSize={fontSize}
|
|
179
|
-
lineHeight={lineHeight}
|
|
180
|
-
items={items}
|
|
181
|
-
pendingApprovals={state.pendingApprovals}
|
|
182
|
-
scrubber={scrubber}
|
|
183
|
-
bookmarks={frame ? undefined : bookmarks}
|
|
184
|
-
affordances={affordances}
|
|
185
|
-
fileUrl={fileUrl}
|
|
186
|
-
attachmentUrl={attachmentUrl}
|
|
187
|
-
hostImage={hostImage}
|
|
188
|
-
jumpToRecapRef={jumpToRecapRef}
|
|
189
|
-
repinRef={repinRef}
|
|
190
|
-
reveal={frame ? undefined : reveal}
|
|
191
|
-
frameParentId={frame?.parentToolUseId}
|
|
192
|
-
onOpenSubagent={frame ? undefined : onOpenSubagent}
|
|
193
|
-
/>
|
|
194
|
-
)}
|
|
195
|
-
{(frame ? frameTask !== undefined && taskBusy(frameTask, items) : showLoader(state)) ? (
|
|
196
|
-
terminal ? (
|
|
197
|
-
<>
|
|
198
|
-
{state.items.length > 0 ? <div className="term-blank" aria-hidden /> : null}
|
|
199
|
-
<WorkingRow
|
|
200
|
-
label={state.status === 'starting' ? 'Starting…' : 'Working…'}
|
|
201
|
-
startedAt={runStartedAt}
|
|
202
|
-
tokens={state.contextUsage?.totalTokens}
|
|
149
|
+
<HostImageProvider value={hostImage}>
|
|
150
|
+
<SkillNamesProvider names={skillNames}>
|
|
151
|
+
<SessionNamesProvider names={sessionNames}>
|
|
152
|
+
<Conversation className={cn(replaying && 'invisible', className)}>
|
|
153
|
+
<ConversationContent className={cn(terminal && 'gap-0 p-0')}>
|
|
154
|
+
<TerminalShell active={terminal} fontSize={fontSize} lineHeight={lineHeight} affordances={affordances}>
|
|
155
|
+
{frame ? (
|
|
156
|
+
frameTask === undefined && !replaying ? (
|
|
157
|
+
<div className={cn(terminal ? 'term-row text-fg-4' : 'p-4 text-body-sm text-fg-4')}>
|
|
158
|
+
This sub-agent's work is not in this transcript.
|
|
159
|
+
</div>
|
|
160
|
+
) : null
|
|
161
|
+
) : items.length === 0 && state.status !== 'starting' ? (
|
|
162
|
+
emptyState !== undefined ? (
|
|
163
|
+
emptyState
|
|
164
|
+
) : (
|
|
165
|
+
<SessionEmptyState
|
|
166
|
+
cwd={state.cwd}
|
|
167
|
+
hasCommands={!!state.commands?.length}
|
|
168
|
+
hasSkills={!!state.skills?.some((s) => s.enabled)}
|
|
169
|
+
canBrowseFiles={canBrowseFiles}
|
|
203
170
|
/>
|
|
204
|
-
|
|
205
|
-
) :
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
aria-hidden
|
|
216
|
-
className="wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden"
|
|
217
|
-
>
|
|
218
|
-
<div className={cn('mx-auto w-full max-w-[var(--wd-transcript-max-width)]', !terminal && 'px-4 py-4')}>
|
|
219
|
-
{terminal ? (
|
|
220
|
-
<TerminalSurface
|
|
171
|
+
)
|
|
172
|
+
) : null}
|
|
173
|
+
{frame && frameTask === undefined && !replaying ? null : (
|
|
174
|
+
<TranscriptRows
|
|
175
|
+
rows={rows}
|
|
176
|
+
boundary={boundary}
|
|
177
|
+
since={catchUp?.since}
|
|
178
|
+
terminal={terminal}
|
|
179
|
+
replaying={replaying}
|
|
180
|
+
stickyPrompt={!frame && stickyPrompt}
|
|
181
|
+
gap={gap}
|
|
221
182
|
fontSize={fontSize}
|
|
222
183
|
lineHeight={lineHeight}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
184
|
+
items={items}
|
|
185
|
+
pendingApprovals={state.pendingApprovals}
|
|
186
|
+
scrubber={scrubber}
|
|
187
|
+
bookmarks={frame ? undefined : bookmarks}
|
|
188
|
+
affordances={affordances}
|
|
189
|
+
fileUrl={fileUrl}
|
|
190
|
+
attachmentUrl={attachmentUrl}
|
|
191
|
+
hostImage={hostImage}
|
|
192
|
+
jumpToRecapRef={jumpToRecapRef}
|
|
193
|
+
repinRef={repinRef}
|
|
194
|
+
reveal={frame ? undefined : reveal}
|
|
195
|
+
frameParentId={frame?.parentToolUseId}
|
|
196
|
+
onOpenSubagent={frame ? undefined : onOpenSubagent}
|
|
197
|
+
/>
|
|
231
198
|
)}
|
|
199
|
+
{(frame ? frameTask !== undefined && taskBusy(frameTask, items) : showLoader(state)) ? (
|
|
200
|
+
terminal ? (
|
|
201
|
+
<>
|
|
202
|
+
{state.items.length > 0 ? <div className="term-blank" aria-hidden /> : null}
|
|
203
|
+
<WorkingRow
|
|
204
|
+
label={state.status === 'starting' ? 'Starting…' : 'Working…'}
|
|
205
|
+
startedAt={runStartedAt}
|
|
206
|
+
tokens={state.contextUsage?.totalTokens}
|
|
207
|
+
/>
|
|
208
|
+
</>
|
|
209
|
+
) : (
|
|
210
|
+
<Loader label={state.status === 'starting' ? 'Starting session…' : undefined} />
|
|
211
|
+
)
|
|
212
|
+
) : null}
|
|
213
|
+
</TerminalShell>
|
|
214
|
+
</ConversationContent>
|
|
215
|
+
<ConversationScrollButton />
|
|
216
|
+
{replaying ? (
|
|
217
|
+
<div
|
|
218
|
+
data-slot="transcript-hold"
|
|
219
|
+
aria-hidden
|
|
220
|
+
className="wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden"
|
|
221
|
+
>
|
|
222
|
+
<div className={cn('mx-auto w-full max-w-[var(--wd-transcript-max-width)]', !terminal && 'px-4 py-4')}>
|
|
223
|
+
{terminal ? (
|
|
224
|
+
<TerminalSurface
|
|
225
|
+
fontSize={fontSize}
|
|
226
|
+
lineHeight={lineHeight}
|
|
227
|
+
affordances={false}
|
|
228
|
+
bleed="1ch"
|
|
229
|
+
className="term-transcript"
|
|
230
|
+
>
|
|
231
|
+
<WorkingRow label="Loading…" />
|
|
232
|
+
</TerminalSurface>
|
|
233
|
+
) : (
|
|
234
|
+
<Loader label="Loading session…" />
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
232
237
|
</div>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
</
|
|
236
|
-
</
|
|
237
|
-
</
|
|
238
|
+
) : null}
|
|
239
|
+
</Conversation>
|
|
240
|
+
</SessionNamesProvider>
|
|
241
|
+
</SkillNamesProvider>
|
|
242
|
+
</HostImageProvider>
|
|
238
243
|
</TranscriptVariantProvider>
|
|
239
244
|
)
|
|
240
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'
|
|
@@ -11,8 +11,10 @@ import { Response } from './Response.tsx'
|
|
|
11
11
|
import { ToolCallCard } from './ToolCallCard.tsx'
|
|
12
12
|
import { Row } from '../terminal/row.tsx'
|
|
13
13
|
import { TerminalItemView } from '../terminal/TerminalTranscript.tsx'
|
|
14
|
+
import { ViewableImage } from './image-viewer.tsx'
|
|
14
15
|
import { peerLabel } from '../terminal/items.tsx'
|
|
15
|
-
import { useShellActions } from './shell-actions.tsx'
|
|
16
|
+
import { ShellItemActions, useShellActions, useVerifyRunning } from './shell-actions.tsx'
|
|
17
|
+
import { BookmarkAction, CopyAction, WithActions } from '../terminal/affordances.tsx'
|
|
16
18
|
import { shellBodyLines, shellFailed, shellFooterText, shellLabel, shellStatusText } from '../terminal/shell-row.ts'
|
|
17
19
|
|
|
18
20
|
function TurnResultRow({ item }: { item: Extract<TranscriptItem, { kind: 'turn_result' }> }) {
|
|
@@ -67,36 +69,47 @@ export function TranscriptItemView({
|
|
|
67
69
|
terminal?: boolean
|
|
68
70
|
}) {
|
|
69
71
|
if (terminal) {
|
|
70
|
-
return <TerminalItemView item={item} fileUrl={fileUrl} />
|
|
72
|
+
return <TerminalItemView item={item} fileUrl={fileUrl} attachmentUrl={attachmentUrl} />
|
|
71
73
|
}
|
|
72
74
|
switch (item.kind) {
|
|
73
75
|
case 'user': {
|
|
74
76
|
return (
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
<WithActions actions={<BookmarkAction id={item.id} />}>
|
|
78
|
+
<Message from="user">
|
|
79
|
+
{item.origin ? <div className="wd-peer-origin text-xs text-muted-foreground">{peerLabel(item.origin)}</div> : null}
|
|
80
|
+
{item.attachments?.length ? <SentAttachments attachments={item.attachments} attachmentUrl={attachmentUrl} /> : null}
|
|
81
|
+
{item.text ? (
|
|
82
|
+
<MessageContent>
|
|
83
|
+
<PromptTokenText text={item.text} />
|
|
84
|
+
</MessageContent>
|
|
85
|
+
) : null}
|
|
86
|
+
</Message>
|
|
87
|
+
</WithActions>
|
|
84
88
|
)
|
|
85
89
|
}
|
|
86
90
|
case 'assistant_text': {
|
|
87
91
|
return (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
<WithActions
|
|
93
|
+
actions={
|
|
94
|
+
<>
|
|
95
|
+
<BookmarkAction id={item.id} />
|
|
96
|
+
{item.streaming ? null : <CopyAction text={item.text} label="Copy message" />}
|
|
97
|
+
</>
|
|
98
|
+
}
|
|
99
|
+
>
|
|
100
|
+
<Message from="assistant">
|
|
101
|
+
<MessageContent>
|
|
102
|
+
<Response streaming={item.streaming}>{item.text}</Response>
|
|
103
|
+
</MessageContent>
|
|
104
|
+
</Message>
|
|
105
|
+
</WithActions>
|
|
93
106
|
)
|
|
94
107
|
}
|
|
95
108
|
case 'thinking': {
|
|
96
109
|
return <Reasoning isStreaming={item.id === 'streaming-thinking'}>{item.text}</Reasoning>
|
|
97
110
|
}
|
|
98
111
|
case 'tool_call': {
|
|
99
|
-
return <
|
|
112
|
+
return <ToolCard item={item} hostImage={hostImage} />
|
|
100
113
|
}
|
|
101
114
|
case 'turn_result': {
|
|
102
115
|
return <TurnResultRow item={item} />
|
|
@@ -119,70 +132,68 @@ export function TranscriptItemView({
|
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
|
|
135
|
+
function ToolCard({
|
|
136
|
+
item,
|
|
137
|
+
hostImage,
|
|
138
|
+
}: {
|
|
139
|
+
item: Extract<TranscriptItem, { kind: 'tool_call' }>
|
|
140
|
+
hostImage?: (path: string) => Promise<string | undefined>
|
|
141
|
+
}) {
|
|
142
|
+
const command = (item.input as { command?: unknown } | null)?.command
|
|
143
|
+
const copyable = typeof command === 'string' ? command : (item.result?.text ?? '')
|
|
144
|
+
return (
|
|
145
|
+
<WithActions
|
|
146
|
+
actions={
|
|
147
|
+
<>
|
|
148
|
+
<BookmarkAction id={item.id} />
|
|
149
|
+
{copyable ? <CopyAction text={copyable} label="Copy" /> : null}
|
|
150
|
+
</>
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
<ToolCallCard item={item} hostImage={hostImage} />
|
|
154
|
+
</WithActions>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
122
158
|
function ShellCard({ item }: { item: ShellItem }) {
|
|
123
159
|
const [open, setOpen] = useState(false)
|
|
124
160
|
const actions = useShellActions()
|
|
125
|
-
const running = item.shell.status === 'running'
|
|
126
161
|
const failed = shellFailed(item)
|
|
127
162
|
const lines = shellBodyLines(item, open)
|
|
128
163
|
const footer = shellFooterText(item, open, lines.length)
|
|
129
164
|
|
|
130
165
|
const shellId = item.shell.id
|
|
131
|
-
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
if (running) {
|
|
134
|
-
void verify(shellId)
|
|
135
|
-
}
|
|
136
|
-
}, [running, shellId, verify])
|
|
166
|
+
useVerifyRunning(item)
|
|
137
167
|
|
|
138
168
|
return (
|
|
139
|
-
<
|
|
140
|
-
<div className="
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
type="button"
|
|
144
|
-
aria-expanded={open}
|
|
145
|
-
className="min-w-0 flex-1 truncate text-left font-mono text-label text-fg-1"
|
|
146
|
-
onClick={() => {
|
|
147
|
-
const next = !open
|
|
148
|
-
setOpen(next)
|
|
149
|
-
if (next && item.truncated && item.expanded === undefined && !item.missing) {
|
|
150
|
-
void actions.loadOutput(shellId)
|
|
151
|
-
}
|
|
152
|
-
}}
|
|
153
|
-
>
|
|
154
|
-
{shellLabel(item)}
|
|
155
|
-
</button>
|
|
156
|
-
<span className={cn('shrink-0 text-label', failed ? 'text-danger' : 'text-fg-4')}>{shellStatusText(item)}</span>
|
|
157
|
-
{actions.open ? (
|
|
158
|
-
<button
|
|
159
|
-
type="button"
|
|
160
|
-
aria-label="Open terminal"
|
|
161
|
-
title="Open terminal"
|
|
162
|
-
className="shrink-0 text-label text-fg-3 hover:text-fg-1"
|
|
163
|
-
onClick={() => actions.open?.(shellId)}
|
|
164
|
-
>
|
|
165
|
-
⤢
|
|
166
|
-
</button>
|
|
167
|
-
) : null}
|
|
168
|
-
{running ? (
|
|
169
|
+
<WithActions actions={<ShellItemActions item={item} />}>
|
|
170
|
+
<div data-slot="shell" className="overflow-hidden rounded-md border border-border bg-surface">
|
|
171
|
+
<div className="flex items-center gap-2 px-3 py-1.5">
|
|
172
|
+
<span className="font-mono text-label text-[var(--wd-shell-accent)]">$</span>
|
|
169
173
|
<button
|
|
170
174
|
type="button"
|
|
171
|
-
aria-
|
|
172
|
-
className="
|
|
173
|
-
onClick={() =>
|
|
175
|
+
aria-expanded={open}
|
|
176
|
+
className="min-w-0 flex-1 truncate text-left font-mono text-label text-fg-1"
|
|
177
|
+
onClick={() => {
|
|
178
|
+
const next = !open
|
|
179
|
+
setOpen(next)
|
|
180
|
+
if (next && item.truncated && item.expanded === undefined && !item.missing) {
|
|
181
|
+
void actions.loadOutput(shellId)
|
|
182
|
+
}
|
|
183
|
+
}}
|
|
174
184
|
>
|
|
175
|
-
|
|
185
|
+
{shellLabel(item)}
|
|
176
186
|
</button>
|
|
187
|
+
<span className={cn('shrink-0 text-label', failed ? 'text-danger' : 'text-fg-4')}>{shellStatusText(item)}</span>
|
|
188
|
+
</div>
|
|
189
|
+
{lines.length > 0 ? (
|
|
190
|
+
<pre className="overflow-x-auto border-t border-border px-3 py-2 font-mono text-label whitespace-pre-wrap text-fg-2">
|
|
191
|
+
{lines.join('\n')}
|
|
192
|
+
</pre>
|
|
177
193
|
) : null}
|
|
194
|
+
{footer ? <div className="px-3 pb-2 text-label text-fg-4">{footer}</div> : null}
|
|
178
195
|
</div>
|
|
179
|
-
|
|
180
|
-
<pre className="overflow-x-auto border-t border-border px-3 py-2 font-mono text-label whitespace-pre-wrap text-fg-2">
|
|
181
|
-
{lines.join('\n')}
|
|
182
|
-
</pre>
|
|
183
|
-
) : null}
|
|
184
|
-
{footer ? <div className="px-3 pb-2 text-label text-fg-4">{footer}</div> : null}
|
|
185
|
-
</div>
|
|
196
|
+
</WithActions>
|
|
186
197
|
)
|
|
187
198
|
}
|
|
188
199
|
|
|
@@ -233,7 +244,9 @@ function SentAttachments({
|
|
|
233
244
|
{attachments.map((attachment) => {
|
|
234
245
|
const href = attachmentUrl?.(attachment.id)
|
|
235
246
|
return attachment.mediaType.startsWith('image/') && href ? (
|
|
236
|
-
<
|
|
247
|
+
<ViewableImage key={attachment.id} image={{ src: href, name: attachment.name }} className="rounded-md">
|
|
248
|
+
<img src={href} alt={attachment.name} className="size-20 rounded-md border border-border object-cover" />
|
|
249
|
+
</ViewableImage>
|
|
237
250
|
) : (
|
|
238
251
|
<span key={attachment.id} className="rounded-full border border-border bg-surface px-2.5 py-1 text-body-xs text-fg-3">
|
|
239
252
|
{attachment.name}
|
|
@@ -5,7 +5,7 @@ import { useStickToBottomContext } from 'use-stick-to-bottom'
|
|
|
5
5
|
import type { PermissionRequest } from '@workerdeck/protocol'
|
|
6
6
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
7
7
|
import { cn } from '../../lib/utils.ts'
|
|
8
|
-
import { resolveAffordances, type TerminalAffordances } from '../terminal/affordances.tsx'
|
|
8
|
+
import { ActionPlacementProvider, resolveAffordances, type TerminalAffordances } from '../terminal/affordances.tsx'
|
|
9
9
|
import { RunRow } from '../terminal/items.tsx'
|
|
10
10
|
import { parentOf } from '../terminal/blocks.ts'
|
|
11
11
|
import { briefPx, estimateBlockPx } from '../terminal/height.ts'
|
|
@@ -324,7 +324,8 @@ export function TranscriptRows({
|
|
|
324
324
|
{virtualizer.getVirtualItems().map((virtualRow) => {
|
|
325
325
|
const row = rows[virtualRow.index]
|
|
326
326
|
const gapClass = virtualRow.index > 0 && (!terminal || gapBefore(rows, virtualRow.index)) ? gap.className : undefined
|
|
327
|
-
const
|
|
327
|
+
const flushBelow = terminal && virtualRow.index < rows.length - 1 && !gapBefore(rows, virtualRow.index + 1)
|
|
328
|
+
const view =
|
|
328
329
|
'run' in row ? (
|
|
329
330
|
<div className={cn(read(boundary, row.index) && 'opacity-45')}>
|
|
330
331
|
<RunRow items={row.run} />
|
|
@@ -348,6 +349,7 @@ export function TranscriptRows({
|
|
|
348
349
|
<TaskRow block={row} fileUrl={fileUrl} onOpenSubagent={onOpenSubagent} />
|
|
349
350
|
</div>
|
|
350
351
|
)
|
|
352
|
+
const content = <ActionPlacementProvider value={flushBelow ? 'inline' : 'below'}>{view}</ActionPlacementProvider>
|
|
351
353
|
if (stickyPrompt && 'item' in row && row.item.kind === 'user' && parentOf(row.item) === undefined) {
|
|
352
354
|
const next = promptRows.find((index) => index > virtualRow.index)
|
|
353
355
|
const laneEnd = next === undefined ? virtualizer.getTotalSize() : (measurements[next]?.start ?? virtualRow.start)
|
|
@@ -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
|
+
}
|