@workerdeck/ui 3.0.1 → 3.2.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,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
2
|
import type { WorkerDeckClient } from '@workerdeck/client'
|
|
3
|
-
import
|
|
3
|
+
import { useAsync } from '@workerdeck/react'
|
|
4
|
+
import { errorMessage, type McpServerActionRequest, type McpServerStatusInfo, type McpServerToolInfo } from '@workerdeck/protocol'
|
|
4
5
|
import { ChevronLeft, ChevronRight, Power, PowerOff, RotateCw } from 'lucide-react'
|
|
5
6
|
import { Badge, type BadgeProps } from '../ui/Badge.tsx'
|
|
6
7
|
import { Button } from '../ui/Button.tsx'
|
|
@@ -17,6 +18,27 @@ export interface McpDialogProps {
|
|
|
17
18
|
className?: string
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
export interface McpPanelModel {
|
|
22
|
+
servers: McpServerStatusInfo[] | undefined
|
|
23
|
+
server: McpServerStatusInfo | undefined
|
|
24
|
+
tool: McpServerToolInfo | undefined
|
|
25
|
+
loading: boolean
|
|
26
|
+
busyServer: string | undefined
|
|
27
|
+
error: string | undefined
|
|
28
|
+
title: string
|
|
29
|
+
description: string | undefined
|
|
30
|
+
selectServer: (name: string | undefined) => void
|
|
31
|
+
selectTool: (name: string | undefined) => void
|
|
32
|
+
back: (() => void) | undefined
|
|
33
|
+
reload: () => void
|
|
34
|
+
act: (name: string, action: McpServerActionRequest['action']) => Promise<void>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface McpPanelProps {
|
|
38
|
+
model: McpPanelModel
|
|
39
|
+
canManageServers?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
const STATUS_VARIANT: Record<string, NonNullable<BadgeProps['variant']>> = {
|
|
21
43
|
connected: 'success',
|
|
22
44
|
failed: 'danger',
|
|
@@ -26,94 +48,106 @@ const STATUS_VARIANT: Record<string, NonNullable<BadgeProps['variant']>> = {
|
|
|
26
48
|
}
|
|
27
49
|
|
|
28
50
|
export function McpDialog({ client, sessionId, open, onOpenChange, canManageServers = true, className }: McpDialogProps) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
51
|
+
const mcp = useMcpPanel(client, sessionId, open)
|
|
52
|
+
return (
|
|
53
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
54
|
+
<DialogContent className={className}>
|
|
55
|
+
<DialogHeader title={mcp.title} description={mcp.description} actions={<McpPanelActions model={mcp} />} />
|
|
56
|
+
<DialogBody>
|
|
57
|
+
<McpPanel model={mcp} canManageServers={canManageServers} />
|
|
58
|
+
</DialogBody>
|
|
59
|
+
</DialogContent>
|
|
60
|
+
</Dialog>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function useMcpPanel(client: WorkerDeckClient | undefined, sessionId: string | undefined, active = true): McpPanelModel {
|
|
65
|
+
const listed = useAsync(() => client!.listMcpServers(sessionId!), [client, sessionId], { enabled: active && !!client && !!sessionId })
|
|
66
|
+
const [actError, setActError] = useState<string | undefined>()
|
|
32
67
|
const [busyServer, setBusyServer] = useState<string | undefined>()
|
|
33
68
|
const [selectedServer, setSelectedServer] = useState<string | undefined>()
|
|
34
69
|
const [selectedTool, setSelectedTool] = useState<string | undefined>()
|
|
35
70
|
|
|
36
|
-
const load = useCallback(async () => {
|
|
37
|
-
if (!sessionId) {
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
setLoading(true)
|
|
41
|
-
setError(undefined)
|
|
42
|
-
try {
|
|
43
|
-
setServers(await client.listMcpServers(sessionId))
|
|
44
|
-
} catch (e) {
|
|
45
|
-
setError(e instanceof Error ? e.message : 'Could not read MCP status')
|
|
46
|
-
} finally {
|
|
47
|
-
setLoading(false)
|
|
48
|
-
}
|
|
49
|
-
}, [client, sessionId])
|
|
50
|
-
|
|
51
71
|
useEffect(() => {
|
|
52
|
-
if (
|
|
53
|
-
|
|
72
|
+
if (active) {
|
|
73
|
+
setSelectedServer(undefined)
|
|
74
|
+
setSelectedTool(undefined)
|
|
75
|
+
setActError(undefined)
|
|
54
76
|
}
|
|
55
|
-
|
|
56
|
-
setSelectedTool(undefined)
|
|
57
|
-
void load()
|
|
58
|
-
}, [open, load])
|
|
59
|
-
|
|
60
|
-
const act = async (name: string, action: McpServerActionRequest['action']) => {
|
|
61
|
-
if (!sessionId) {
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
setBusyServer(name)
|
|
65
|
-
setError(undefined)
|
|
66
|
-
try {
|
|
67
|
-
setServers(await client.mcpServerAction(sessionId, name, action))
|
|
68
|
-
} catch (e) {
|
|
69
|
-
setError(e instanceof Error ? e.message : `Could not ${action} ${name}`)
|
|
70
|
-
} finally {
|
|
71
|
-
setBusyServer(undefined)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
77
|
+
}, [active, client, sessionId])
|
|
74
78
|
|
|
79
|
+
const servers = listed.data
|
|
75
80
|
const server = servers?.find((s) => s.name === selectedServer)
|
|
76
81
|
const tool = server?.tools?.find((t) => t.name === selectedTool)
|
|
77
|
-
|
|
82
|
+
return {
|
|
83
|
+
servers,
|
|
84
|
+
server,
|
|
85
|
+
tool,
|
|
86
|
+
loading: listed.loading,
|
|
87
|
+
busyServer,
|
|
88
|
+
error: actError ?? (listed.error === undefined ? undefined : errorMessage(listed.error, 'Could not read MCP status')),
|
|
89
|
+
title: tool?.name ?? server?.name ?? 'MCP servers',
|
|
90
|
+
description: tool ? `${server?.name} tool` : server ? server.serverInfo?.name : undefined,
|
|
91
|
+
selectServer: setSelectedServer,
|
|
92
|
+
selectTool: setSelectedTool,
|
|
93
|
+
back: selectedServer ? () => (selectedTool ? setSelectedTool(undefined) : setSelectedServer(undefined)) : undefined,
|
|
94
|
+
reload: () => {
|
|
95
|
+
setActError(undefined)
|
|
96
|
+
void listed.reload()
|
|
97
|
+
},
|
|
98
|
+
act: async (name, action) => {
|
|
99
|
+
if (!client || !sessionId) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
setBusyServer(name)
|
|
103
|
+
setActError(undefined)
|
|
104
|
+
try {
|
|
105
|
+
listed.setData(await client.mcpServerAction(sessionId, name, action))
|
|
106
|
+
} catch (e) {
|
|
107
|
+
setActError(errorMessage(e, `Could not ${action} ${name}`))
|
|
108
|
+
} finally {
|
|
109
|
+
setBusyServer(undefined)
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
78
114
|
|
|
115
|
+
export function McpPanelActions({ model }: { model: McpPanelModel }) {
|
|
116
|
+
if (model.back) {
|
|
117
|
+
return (
|
|
118
|
+
<Button variant="ghost" size="xs" onClick={model.back}>
|
|
119
|
+
<ChevronLeft className="size-3.5" />
|
|
120
|
+
Back
|
|
121
|
+
</Button>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
79
124
|
return (
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
125
|
+
<Button variant="ghost" size="xs" onClick={model.reload} disabled={model.loading}>
|
|
126
|
+
{model.loading ? <Spinner className="size-3 text-current" /> : <RotateCw className="size-3" />}
|
|
127
|
+
Refresh
|
|
128
|
+
</Button>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function McpPanel({ model, canManageServers = true }: McpPanelProps) {
|
|
133
|
+
const { error, servers, server, tool } = model
|
|
134
|
+
return (
|
|
135
|
+
<>
|
|
136
|
+
{error ? <div className="mb-3 rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger">{error}</div> : null}
|
|
137
|
+
{tool ? (
|
|
138
|
+
<ToolView tool={tool} />
|
|
139
|
+
) : server ? (
|
|
140
|
+
<ServerView
|
|
141
|
+
server={server}
|
|
142
|
+
busy={model.busyServer === server.name}
|
|
143
|
+
canManage={canManageServers}
|
|
144
|
+
onAct={(action) => void model.act(server.name, action)}
|
|
145
|
+
onSelectTool={model.selectTool}
|
|
98
146
|
/>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
) : server ? (
|
|
104
|
-
<ServerView
|
|
105
|
-
server={server}
|
|
106
|
-
busy={busyServer === server.name}
|
|
107
|
-
canManage={canManageServers}
|
|
108
|
-
onAct={(action) => void act(server.name, action)}
|
|
109
|
-
onSelectTool={setSelectedTool}
|
|
110
|
-
/>
|
|
111
|
-
) : error && !servers ? null : (
|
|
112
|
-
<ServerList servers={servers} loading={loading} onSelect={setSelectedServer} />
|
|
113
|
-
)}
|
|
114
|
-
</DialogBody>
|
|
115
|
-
</DialogContent>
|
|
116
|
-
</Dialog>
|
|
147
|
+
) : error && !servers ? null : (
|
|
148
|
+
<ServerList servers={servers} loading={model.loading} onSelect={model.selectServer} />
|
|
149
|
+
)}
|
|
150
|
+
</>
|
|
117
151
|
)
|
|
118
152
|
}
|
|
119
153
|
|
|
@@ -5,8 +5,8 @@ import { Button } from '../ui/Button.tsx'
|
|
|
5
5
|
import { Input } from '../ui/Input.tsx'
|
|
6
6
|
import { cn } from '../../lib/utils.ts'
|
|
7
7
|
import { toolInputPreview } from '../../lib/format.ts'
|
|
8
|
-
import {
|
|
9
|
-
import { shellRequestLines
|
|
8
|
+
import { permissionPromptModel } from '../../lib/permission-prompt.ts'
|
|
9
|
+
import { shellRequestLines } from '../../lib/shell-request.ts'
|
|
10
10
|
import { toolIcon } from '../../lib/tool-icon.ts'
|
|
11
11
|
import { Response } from './Response.tsx'
|
|
12
12
|
|
|
@@ -29,8 +29,9 @@ export function PermissionPrompt({ request, onApprove, onDeny, className }: Perm
|
|
|
29
29
|
setDenying(false)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
32
|
+
const model = permissionPromptModel(request)
|
|
33
|
+
const { plan, shell } = model
|
|
34
|
+
const [allow, keep, stop] = model.choices
|
|
34
35
|
const summary = toolInputPreview(request.input)
|
|
35
36
|
const ToolIcon = toolIcon(request.toolName)
|
|
36
37
|
|
|
@@ -39,15 +40,9 @@ export function PermissionPrompt({ request, onApprove, onDeny, className }: Perm
|
|
|
39
40
|
<div className="flex items-start gap-2.5">
|
|
40
41
|
<Hand className="mt-0.5 size-4 shrink-0 text-warning" />
|
|
41
42
|
<div className="min-w-0 flex-1">
|
|
42
|
-
<div className="text-body-sm font-medium text-fg-1">
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: shell
|
|
46
|
-
? shellRequestTitle(shell)
|
|
47
|
-
: (request.title ?? request.displayName ?? 'Permission needed')}
|
|
48
|
-
</div>
|
|
49
|
-
{request.description ? <div className="mt-0.5 text-label text-fg-3">{request.description}</div> : null}
|
|
50
|
-
{request.decisionReason ? <div className="mt-0.5 text-label text-fg-4">{request.decisionReason}</div> : null}
|
|
43
|
+
<div className="text-body-sm font-medium text-fg-1">{model.heading}</div>
|
|
44
|
+
{model.description ? <div className="mt-0.5 text-label text-fg-3">{model.description}</div> : null}
|
|
45
|
+
{model.decisionReason ? <div className="mt-0.5 text-label text-fg-4">{model.decisionReason}</div> : null}
|
|
51
46
|
{plan ? (
|
|
52
47
|
<div className="mt-1.5 max-h-72 overflow-y-auto rounded-md bg-code-bg px-2.5 py-2 text-body-sm text-fg-2">
|
|
53
48
|
<Response>{plan}</Response>
|
|
@@ -84,13 +79,13 @@ export function PermissionPrompt({ request, onApprove, onDeny, className }: Perm
|
|
|
84
79
|
</div>
|
|
85
80
|
<div className="flex shrink-0 flex-wrap justify-end gap-1.5">
|
|
86
81
|
<Button size="sm" onClick={() => onApprove(request.id)}>
|
|
87
|
-
{
|
|
82
|
+
{allow.button}
|
|
88
83
|
</Button>
|
|
89
84
|
<Button size="sm" variant="outline" onClick={() => setDenying((v) => !v)}>
|
|
90
|
-
{
|
|
85
|
+
{keep.button}
|
|
91
86
|
</Button>
|
|
92
87
|
<Button size="sm" variant="outline" onClick={() => deny(true)}>
|
|
93
|
-
{
|
|
88
|
+
{stop.button}
|
|
94
89
|
</Button>
|
|
95
90
|
</div>
|
|
96
91
|
</div>
|
|
@@ -108,18 +103,14 @@ export function PermissionPrompt({ request, onApprove, onDeny, className }: Perm
|
|
|
108
103
|
setDenying(false)
|
|
109
104
|
}
|
|
110
105
|
}}
|
|
111
|
-
placeholder={
|
|
112
|
-
plan
|
|
113
|
-
? 'What should change? (optional) - the agent keeps planning and reads this'
|
|
114
|
-
: 'Reason (optional) - the agent reads this and can try something else'
|
|
115
|
-
}
|
|
106
|
+
placeholder={model.denyPlaceholder}
|
|
116
107
|
className="h-7 flex-1"
|
|
117
108
|
/>
|
|
118
109
|
<Button size="sm" variant="outline" onClick={() => setDenying(false)}>
|
|
119
110
|
Cancel
|
|
120
111
|
</Button>
|
|
121
112
|
<Button size="sm" variant="destructive" onClick={() => deny(false)}>
|
|
122
|
-
{
|
|
113
|
+
{keep.button}
|
|
123
114
|
</Button>
|
|
124
115
|
</div>
|
|
125
116
|
) : null}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
1
|
import { parseUserQuestions, type PermissionRequest, type QuestionBehavior } from '@workerdeck/protocol'
|
|
3
2
|
|
|
4
3
|
// Re-exported, not redefined: the parse moved to protocol so the CLI's Live Activity builder could
|
|
@@ -7,6 +6,7 @@ import { MessageCircleQuestion, X } from 'lucide-react'
|
|
|
7
6
|
import { Badge } from '../ui/Badge.tsx'
|
|
8
7
|
import { Button } from '../ui/Button.tsx'
|
|
9
8
|
import { Textarea } from '../ui/Textarea.tsx'
|
|
9
|
+
import { useQuestionAnswers } from '../../lib/question-answers.ts'
|
|
10
10
|
import { cn } from '../../lib/utils.ts'
|
|
11
11
|
|
|
12
12
|
export { parseUserQuestions }
|
|
@@ -23,18 +23,6 @@ export const QUESTION_BEHAVIORS: QuestionBehaviorMeta[] = [
|
|
|
23
23
|
{ value: 'deny', label: 'Disabled', description: 'the agent is told to decide on its own' },
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
-
type Selection = { labels: string[]; other: string; otherActive: boolean }
|
|
27
|
-
|
|
28
|
-
const EMPTY_SELECTION: Selection = { labels: [], other: '', otherActive: false }
|
|
29
|
-
|
|
30
|
-
function answerFor(selection: Selection): string {
|
|
31
|
-
const parts = [...selection.labels]
|
|
32
|
-
if (selection.otherActive && selection.other.trim()) {
|
|
33
|
-
parts.push(selection.other.trim())
|
|
34
|
-
}
|
|
35
|
-
return parts.join(', ')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
26
|
export interface QuestionPromptProps {
|
|
39
27
|
request: PermissionRequest
|
|
40
28
|
onAnswer: (requestId: string, updatedInput: Record<string, unknown>) => void
|
|
@@ -44,32 +32,9 @@ export interface QuestionPromptProps {
|
|
|
44
32
|
|
|
45
33
|
export function QuestionPrompt({ request, onAnswer, onDismiss, className }: QuestionPromptProps) {
|
|
46
34
|
const questions = parseUserQuestions(request.input)
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const update = (index: number, patch: Partial<Selection>) => {
|
|
50
|
-
setSelections((prev) => prev.map((s, i) => (i === index ? { ...s, ...patch } : s)))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const toggle = (index: number, label: string, multiSelect: boolean) => {
|
|
54
|
-
const current = selections[index] ?? EMPTY_SELECTION
|
|
55
|
-
if (multiSelect) {
|
|
56
|
-
update(index, {
|
|
57
|
-
labels: current.labels.includes(label) ? current.labels.filter((l) => l !== label) : [...current.labels, label],
|
|
58
|
-
})
|
|
59
|
-
} else {
|
|
60
|
-
update(index, { labels: current.labels[0] === label ? [] : [label], otherActive: false })
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const complete = questions.every((_, i) => answerFor(selections[i] ?? EMPTY_SELECTION) !== '')
|
|
35
|
+
const { selectionFor, update, toggle, toggleOther, complete, answers } = useQuestionAnswers(questions)
|
|
65
36
|
|
|
66
|
-
const submit = () => {
|
|
67
|
-
const answers: Record<string, string> = {}
|
|
68
|
-
questions.forEach((q, i) => {
|
|
69
|
-
answers[q.question] = answerFor(selections[i] ?? EMPTY_SELECTION)
|
|
70
|
-
})
|
|
71
|
-
onAnswer(request.id, { ...request.input, answers })
|
|
72
|
-
}
|
|
37
|
+
const submit = () => onAnswer(request.id, { ...request.input, answers: answers() })
|
|
73
38
|
|
|
74
39
|
return (
|
|
75
40
|
<div data-slot="question-prompt" className={cn('rounded-lg border border-info/40 bg-info-bg p-3', className)}>
|
|
@@ -77,7 +42,7 @@ export function QuestionPrompt({ request, onAnswer, onDismiss, className }: Ques
|
|
|
77
42
|
<MessageCircleQuestion className="mt-0.5 size-4 shrink-0 text-info" />
|
|
78
43
|
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
|
79
44
|
{questions.map((q, index) => {
|
|
80
|
-
const selection =
|
|
45
|
+
const selection = selectionFor(index)
|
|
81
46
|
return (
|
|
82
47
|
<div key={index} className="flex flex-col gap-1.5">
|
|
83
48
|
<div className="flex items-center gap-2">
|
|
@@ -91,7 +56,7 @@ export function QuestionPrompt({ request, onAnswer, onDismiss, className }: Ques
|
|
|
91
56
|
<button
|
|
92
57
|
key={option.label}
|
|
93
58
|
type="button"
|
|
94
|
-
onClick={() => toggle(index, option.label
|
|
59
|
+
onClick={() => toggle(index, option.label)}
|
|
95
60
|
className={cn(
|
|
96
61
|
'rounded-md border px-2.5 py-1.5 text-left transition-colors',
|
|
97
62
|
selected ? 'border-info bg-bg' : 'border-border bg-bg/50 hover:border-border-strong hover:bg-bg',
|
|
@@ -110,7 +75,7 @@ export function QuestionPrompt({ request, onAnswer, onDismiss, className }: Ques
|
|
|
110
75
|
<div className="flex items-center gap-2">
|
|
111
76
|
<button
|
|
112
77
|
type="button"
|
|
113
|
-
onClick={() =>
|
|
78
|
+
onClick={() => toggleOther(index)}
|
|
114
79
|
className={cn(
|
|
115
80
|
'shrink-0 rounded-md border px-2.5 py-1.5 text-body-sm font-medium transition-colors',
|
|
116
81
|
selection.otherActive
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useMemo, useRef, useState
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import type { PermissionRequest } from '@workerdeck/protocol'
|
|
3
3
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
4
|
-
import { toolInputPreview } from '../../lib/format.ts'
|
|
5
4
|
import { cn } from '../../lib/utils.ts'
|
|
6
5
|
import {
|
|
7
6
|
approvalCluster,
|
|
8
7
|
buildMarks,
|
|
9
8
|
clusterMarks,
|
|
10
|
-
doneLine,
|
|
11
|
-
excerpt,
|
|
12
|
-
KIND_NAME,
|
|
13
9
|
nearestMember,
|
|
10
|
+
proportionalPlacement,
|
|
14
11
|
type Cluster,
|
|
15
12
|
type Mark,
|
|
16
13
|
} from './scrubber-marks.ts'
|
|
14
|
+
import { CARD_PEEK, peekContent } from './scrubber-peek.tsx'
|
|
17
15
|
|
|
18
16
|
export interface ScrubberProps {
|
|
19
17
|
items: readonly TranscriptItem[]
|
|
@@ -26,74 +24,6 @@ export interface ScrubberProps {
|
|
|
26
24
|
className?: string
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
function peekContent(
|
|
30
|
-
cluster: Cluster,
|
|
31
|
-
first: Mark | undefined,
|
|
32
|
-
items: readonly TranscriptItem[],
|
|
33
|
-
pendingApprovals: readonly PermissionRequest[],
|
|
34
|
-
): ReactNode {
|
|
35
|
-
const more = cluster.marks.length > 1 ? ` · ${cluster.marks.length} marks` : ''
|
|
36
|
-
let body: ReactNode = null
|
|
37
|
-
if (cluster.kind === 'approval') {
|
|
38
|
-
const request = pendingApprovals[0]
|
|
39
|
-
body = request ? (
|
|
40
|
-
<>
|
|
41
|
-
<div>{request.title ?? 'Permission required'}</div>
|
|
42
|
-
<div className="wd-scrub-ex" data-tone="muted">
|
|
43
|
-
{`${request.displayName ?? request.toolName}(${toolInputPreview(request.input)})`}
|
|
44
|
-
</div>
|
|
45
|
-
</>
|
|
46
|
-
) : null
|
|
47
|
-
} else if (first && first.kind !== 'recap') {
|
|
48
|
-
const item = items[first.itemIndex]
|
|
49
|
-
if (first.kind === 'turn' || first.kind === 'turnFailed') {
|
|
50
|
-
const turn = first.turnIndex === undefined ? undefined : items[first.turnIndex]
|
|
51
|
-
body = (
|
|
52
|
-
<>
|
|
53
|
-
{item?.kind === 'assistant_text' ? <div className="wd-scrub-ex">{item.text}</div> : null}
|
|
54
|
-
{turn?.kind === 'turn_result' ? (
|
|
55
|
-
<>
|
|
56
|
-
<div data-tone={turn.isError ? 'danger' : 'muted'}>{doneLine(turn)}</div>
|
|
57
|
-
{turn.errors?.map((message, index) => (
|
|
58
|
-
<div key={index} data-tone="danger">
|
|
59
|
-
{message}
|
|
60
|
-
</div>
|
|
61
|
-
))}
|
|
62
|
-
</>
|
|
63
|
-
) : null}
|
|
64
|
-
</>
|
|
65
|
-
)
|
|
66
|
-
} else if (item) {
|
|
67
|
-
const failure =
|
|
68
|
-
first.kind === 'toolFailed' && item.kind === 'tool_call'
|
|
69
|
-
? item.result?.text.split('\n').find((line) => line.trim() !== '')
|
|
70
|
-
: undefined
|
|
71
|
-
body = (
|
|
72
|
-
<>
|
|
73
|
-
<div className="wd-scrub-ex" data-tone={first.kind === 'error' || first.kind === 'toolFailed' ? 'danger' : undefined}>
|
|
74
|
-
{first.kind === 'user' ? <span data-tone="muted">{'❯ '}</span> : null}
|
|
75
|
-
{excerpt(item)}
|
|
76
|
-
</div>
|
|
77
|
-
{failure ? (
|
|
78
|
-
<div className="wd-scrub-ex" data-tone="danger">
|
|
79
|
-
{failure}
|
|
80
|
-
</div>
|
|
81
|
-
) : null}
|
|
82
|
-
</>
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return (
|
|
87
|
-
<>
|
|
88
|
-
<div data-tone="muted">
|
|
89
|
-
{KIND_NAME[first?.kind ?? cluster.kind]}
|
|
90
|
-
{more}
|
|
91
|
-
</div>
|
|
92
|
-
{body}
|
|
93
|
-
</>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
27
|
export function Scrubber({
|
|
98
28
|
items,
|
|
99
29
|
pendingApprovals,
|
|
@@ -138,7 +68,8 @@ export function Scrubber({
|
|
|
138
68
|
if (railH <= 0) {
|
|
139
69
|
return []
|
|
140
70
|
}
|
|
141
|
-
const
|
|
71
|
+
const marks = buildMarks(items, { frameParentId, bookmarks, recapItemIndex })
|
|
72
|
+
const built = clusterMarks(marks, proportionalPlacement(railH, items.length))
|
|
142
73
|
if (pendingApprovals.length > 0) {
|
|
143
74
|
built.push(approvalCluster(railH))
|
|
144
75
|
}
|
|
@@ -191,7 +122,7 @@ export function Scrubber({
|
|
|
191
122
|
))}
|
|
192
123
|
{peek ? (
|
|
193
124
|
<div ref={peekRef} className="wd-scrub-peek" style={{ top: peek.y }}>
|
|
194
|
-
{peekContent(peek.cluster, peek.mark, items, pendingApprovals)}
|
|
125
|
+
{peekContent({ cluster: peek.cluster, first: peek.mark, items, pendingApprovals }, CARD_PEEK)}
|
|
195
126
|
</div>
|
|
196
127
|
) : null}
|
|
197
128
|
</div>
|