@workerdeck/ui 0.23.0 → 1.0.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-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
- package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
- package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
- package/build/format-B8ZM9LPy.d.mts +16 -0
- package/build/format.d.mts +8 -75
- package/build/format.mjs +2 -2
- package/build/index.d.mts +63 -1138
- package/build/index.mjs +4 -300
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -23
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +5 -126
- package/build/workspace.mjs +11 -143
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +3 -9
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
|
@@ -5,26 +5,9 @@ export interface MessageProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
5
5
|
from: 'user' | 'assistant'
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* One chat turn row.
|
|
10
|
-
*
|
|
11
|
-
* `cards`: user messages sit in a bubble, assistant content is flat and
|
|
12
|
-
* full-width. Both are **left-aligned**: the transcript is a log read top to
|
|
13
|
-
* bottom, and an editor-shaped host (a full-width session view beside a sessions
|
|
14
|
-
* rail) has no right edge to anchor to — a bubble drifting right in a 1600px
|
|
15
|
-
* column separates a prompt from the reply it produced. The bubble alone is
|
|
16
|
-
* enough to say who spoke.
|
|
17
|
-
*
|
|
18
|
-
* The terminal theme draws none of this: it is its own renderer
|
|
19
|
-
* (`components/terminal/`) and mounts instead of these rows.
|
|
20
|
-
*/
|
|
21
8
|
export function Message({ from, className, children, ...props }: MessageProps) {
|
|
22
9
|
return (
|
|
23
|
-
<div
|
|
24
|
-
data-slot='message'
|
|
25
|
-
data-from={from}
|
|
26
|
-
className={cn('flex w-full flex-col items-start gap-1', className)}
|
|
27
|
-
{...props}>
|
|
10
|
+
<div data-slot="message" data-from={from} className={cn('flex w-full flex-col items-start gap-1', className)} {...props}>
|
|
28
11
|
{children}
|
|
29
12
|
</div>
|
|
30
13
|
)
|
|
@@ -33,11 +16,9 @@ export function Message({ from, className, children, ...props }: MessageProps) {
|
|
|
33
16
|
export function MessageContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
34
17
|
return (
|
|
35
18
|
<div
|
|
36
|
-
data-slot=
|
|
19
|
+
data-slot="message-content"
|
|
37
20
|
className={cn(
|
|
38
21
|
'min-w-0 text-body-sm leading-6 text-fg-1',
|
|
39
|
-
// Bubble treatment only within a user message row.
|
|
40
|
-
// The tail points bottom-*left* now that the bubble is left-aligned.
|
|
41
22
|
'in-data-[from=user]:max-w-[85%] in-data-[from=user]:rounded-lg in-data-[from=user]:rounded-bl-sm',
|
|
42
23
|
'in-data-[from=user]:bg-accent-bg in-data-[from=user]:px-3 in-data-[from=user]:py-2',
|
|
43
24
|
'in-data-[from=user]:whitespace-pre-wrap',
|
|
@@ -1,112 +1,83 @@
|
|
|
1
1
|
import type { ModelOption } from '@workerdeck/protocol'
|
|
2
|
-
import {
|
|
3
|
-
Select,
|
|
4
|
-
SelectContent,
|
|
5
|
-
SelectItem,
|
|
6
|
-
SelectItemText,
|
|
7
|
-
SelectTrigger,
|
|
8
|
-
SelectValue,
|
|
9
|
-
} from '../ui/Select.tsx'
|
|
2
|
+
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
10
3
|
import { cn } from '../../lib/utils.ts'
|
|
11
4
|
|
|
12
5
|
export interface ModelSelectProps {
|
|
13
|
-
/** Models the session can switch to (TranscriptState.models). */
|
|
14
6
|
models: ModelOption[]
|
|
15
|
-
/** The session's current model id (TranscriptState.model), possibly decorated
|
|
16
|
-
* (e.g. "claude-fable-5[1m]") — matched leniently against the options.
|
|
17
|
-
* `undefined` selects the list's default row, if it has one. */
|
|
18
7
|
model?: string
|
|
19
|
-
/** `undefined` = back to the CLI's default model. */
|
|
20
8
|
onModelChange: (model?: string) => void
|
|
21
|
-
/** 'toolbar' (default) is the composer's compact borderless trigger;
|
|
22
|
-
* 'form' is a standard field-sized Select for create/settings forms. */
|
|
23
9
|
variant?: 'toolbar' | 'form'
|
|
24
10
|
disabled?: boolean
|
|
25
11
|
className?: string
|
|
26
12
|
}
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
14
|
+
function isDefaultOption(value: string) {
|
|
15
|
+
return value === 'default'
|
|
16
|
+
}
|
|
31
17
|
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
function dropVariant(id: string) {
|
|
19
|
+
return id.replace(/\[.*\]$/, '')
|
|
20
|
+
}
|
|
34
21
|
|
|
35
|
-
/** 'claude-opus-4-8' → "opus", 'sonnet' → "sonnet". The vendor prefix and the
|
|
36
|
-
* version tail are dropped; what is left is the name a person would say. */
|
|
37
22
|
function family(id: string): string {
|
|
38
23
|
const parts = id.toLowerCase().split('-')
|
|
39
|
-
if (parts[0] === 'claude')
|
|
24
|
+
if (parts[0] === 'claude') {
|
|
25
|
+
parts.shift()
|
|
26
|
+
}
|
|
40
27
|
return parts[0] ?? ''
|
|
41
28
|
}
|
|
42
29
|
|
|
43
|
-
/**
|
|
44
|
-
* Whether a row is the one naming `model`.
|
|
45
|
-
*
|
|
46
|
-
* Three passes, narrowest first, because the rows and the id a session *reports*
|
|
47
|
-
* are written differently: the rows are aliases ('opus[1m]', 'sonnet',
|
|
48
|
-
* 'claude-fable-5[1m]') and a session reports a resolved wire id
|
|
49
|
-
* ('claude-opus-5[1m]'). `resolvedModel` is the server's own answer to this and
|
|
50
|
-
* wins when present; the family fallback covers a server that doesn't send it,
|
|
51
|
-
* which is the difference between the chip reading "Opus 5" and reading
|
|
52
|
-
* `claude-opus-5[1m]`. Kept identical to the iOS client's `ModelOption.matches`.
|
|
53
|
-
*/
|
|
54
30
|
function optionMatches(option: ModelOption, model: string): boolean {
|
|
55
|
-
if (model === option.value || model === option.resolvedModel)
|
|
31
|
+
if (model === option.value || model === option.resolvedModel) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
56
34
|
const stripped = dropVariant(model)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (option.resolvedModel) return stripped === dropVariant(option.resolvedModel)
|
|
35
|
+
if (option.resolvedModel) {
|
|
36
|
+
return stripped === dropVariant(option.resolvedModel)
|
|
37
|
+
}
|
|
61
38
|
const token = family(stripped)
|
|
62
39
|
return token !== '' && token === family(dropVariant(option.value))
|
|
63
40
|
}
|
|
64
41
|
|
|
65
|
-
/** Find the option matching a (possibly decorated/aliased) session model id. */
|
|
66
42
|
function matchModel(models: ModelOption[], model?: string): ModelOption | undefined {
|
|
67
|
-
if (!model)
|
|
43
|
+
if (!model) {
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
68
46
|
return models.filter((m) => !isDefaultOption(m.value)).find((m) => optionMatches(m, model))
|
|
69
47
|
}
|
|
70
48
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export function ModelSelect({
|
|
74
|
-
models,
|
|
75
|
-
model,
|
|
76
|
-
onModelChange,
|
|
77
|
-
variant = 'toolbar',
|
|
78
|
-
disabled,
|
|
79
|
-
className,
|
|
80
|
-
}: ModelSelectProps) {
|
|
81
|
-
const selected =
|
|
82
|
-
matchModel(models, model) ?? (model ? undefined : models.find((m) => isDefaultOption(m.value)))
|
|
49
|
+
export function ModelSelect({ models, model, onModelChange, variant = 'toolbar', disabled, className }: ModelSelectProps) {
|
|
50
|
+
const selected = matchModel(models, model) ?? (model ? undefined : models.find((m) => isDefaultOption(m.value)))
|
|
83
51
|
return (
|
|
84
52
|
<Select
|
|
85
53
|
items={models.map((m) => ({ value: m.value, label: m.displayName }))}
|
|
86
54
|
value={selected?.value ?? null}
|
|
87
55
|
onValueChange={(value) => {
|
|
88
|
-
if (typeof value !== 'string' || value === selected?.value)
|
|
56
|
+
if (typeof value !== 'string' || value === selected?.value) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
89
59
|
onModelChange(isDefaultOption(value) ? undefined : value)
|
|
90
60
|
}}
|
|
91
|
-
disabled={disabled}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
>
|
|
92
63
|
<SelectTrigger
|
|
93
|
-
aria-label=
|
|
64
|
+
aria-label="Model"
|
|
94
65
|
className={cn(
|
|
95
|
-
variant === 'toolbar' &&
|
|
96
|
-
'h-6 max-w-56 border-transparent bg-transparent text-fg-3 hover:bg-surface-hover',
|
|
66
|
+
variant === 'toolbar' && 'h-6 max-w-56 border-transparent bg-transparent text-fg-3 hover:bg-surface-hover',
|
|
97
67
|
className,
|
|
98
|
-
)}
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
99
70
|
<span className={cn('truncate', variant === 'toolbar' && 'font-mono text-label')}>
|
|
100
71
|
<SelectValue placeholder={model ?? 'model'} />
|
|
101
72
|
</span>
|
|
102
73
|
</SelectTrigger>
|
|
103
|
-
<SelectContent className=
|
|
74
|
+
<SelectContent className="min-w-72">
|
|
104
75
|
{models.map((m) => (
|
|
105
76
|
<SelectItem key={m.value} value={m.value}>
|
|
106
77
|
<SelectItemText>
|
|
107
|
-
<span className=
|
|
78
|
+
<span className="font-medium">{m.displayName}</span>
|
|
108
79
|
</SelectItemText>
|
|
109
|
-
{m.description ? <span className=
|
|
80
|
+
{m.description ? <span className="text-label text-fg-4">{m.description}</span> : null}
|
|
110
81
|
</SelectItem>
|
|
111
82
|
))}
|
|
112
83
|
</SelectContent>
|
|
@@ -1,39 +1,18 @@
|
|
|
1
1
|
import type { LucideIcon } from 'lucide-react'
|
|
2
2
|
import type { PermissionMode } from '@workerdeck/protocol'
|
|
3
3
|
import { ClipboardList, Code, Hand, ShieldCheck, TriangleAlert, Zap } from 'lucide-react'
|
|
4
|
-
import {
|
|
5
|
-
Select,
|
|
6
|
-
SelectContent,
|
|
7
|
-
SelectItem,
|
|
8
|
-
SelectItemText,
|
|
9
|
-
SelectTrigger,
|
|
10
|
-
SelectValue,
|
|
11
|
-
} from '../ui/Select.tsx'
|
|
4
|
+
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
12
5
|
import { cn } from '../../lib/utils.ts'
|
|
13
6
|
|
|
14
7
|
export type PermissionModeMeta = {
|
|
15
8
|
value: PermissionMode
|
|
16
|
-
/** The name Claude Code itself uses. */
|
|
17
9
|
label: string
|
|
18
|
-
/** The chip form, for bars where the label shares a line with three other
|
|
19
|
-
* things and "Bypass permissions" would eat half of it. */
|
|
20
10
|
shortLabel: string
|
|
21
|
-
/** What the mode actually does — the CLI's own one-liners. */
|
|
22
11
|
description: string
|
|
23
12
|
icon: LucideIcon
|
|
24
13
|
dangerous?: boolean
|
|
25
14
|
}
|
|
26
15
|
|
|
27
|
-
/**
|
|
28
|
-
* The modes surfaced across UI surfaces (session creation, in-session switcher),
|
|
29
|
-
* ordered by how much of the approval gate they give away.
|
|
30
|
-
*
|
|
31
|
-
* Notably `default` is **"Manual"**: the wire value is `default`, but calling it
|
|
32
|
-
* that in the UI conflates a real mode (ask me every time) with "whatever the
|
|
33
|
-
* server picked", which is the one confusion a mode chip exists to avoid. The
|
|
34
|
-
* naming, the icons and the summaries are shared with the iOS app on purpose —
|
|
35
|
-
* the two surfaces should read as the same list.
|
|
36
|
-
*/
|
|
37
16
|
export const PERMISSION_MODES: PermissionModeMeta[] = [
|
|
38
17
|
{
|
|
39
18
|
value: 'default',
|
|
@@ -60,10 +39,6 @@ export const PERMISSION_MODES: PermissionModeMeta[] = [
|
|
|
60
39
|
value: 'auto',
|
|
61
40
|
label: 'Auto',
|
|
62
41
|
shortLabel: 'Auto',
|
|
63
|
-
// Engine-neutral on purpose: on claude this is the operator-configurable
|
|
64
|
-
// auto-mode classifier, on codex it is `approvalsReviewer: 'auto_review'`,
|
|
65
|
-
// a fixed OpenAI-prompted subagent. Both "something other than you decides";
|
|
66
|
-
// naming Claude here reads as a bug on a codex session.
|
|
67
42
|
description: 'The agent handles permission decisions',
|
|
68
43
|
icon: Zap,
|
|
69
44
|
},
|
|
@@ -71,8 +46,6 @@ export const PERMISSION_MODES: PermissionModeMeta[] = [
|
|
|
71
46
|
value: 'dontAsk',
|
|
72
47
|
label: "Don't ask",
|
|
73
48
|
shortLabel: "Don't ask",
|
|
74
|
-
// The CLI's own definition, and the opposite of bypass: it never prompts,
|
|
75
|
-
// and anything not already permitted is denied rather than allowed.
|
|
76
49
|
description: 'Never ask — deny anything not pre-approved',
|
|
77
50
|
icon: ShieldCheck,
|
|
78
51
|
},
|
|
@@ -86,67 +59,40 @@ export const PERMISSION_MODES: PermissionModeMeta[] = [
|
|
|
86
59
|
},
|
|
87
60
|
]
|
|
88
61
|
|
|
89
|
-
export
|
|
90
|
-
PERMISSION_MODES.find((m) => m.value === mode)
|
|
62
|
+
export function permissionModeMeta(mode: PermissionMode): PermissionModeMeta | undefined {
|
|
63
|
+
return PERMISSION_MODES.find((m) => m.value === mode)
|
|
64
|
+
}
|
|
91
65
|
|
|
92
|
-
/** One offered mode as plain data — no icon, no React. What a host chrome
|
|
93
|
-
* outside the panel (a VS Code QuickPick) needs to draw the same list. */
|
|
94
66
|
export type PermissionModeChoice = {
|
|
95
67
|
value: PermissionMode
|
|
96
68
|
label: string
|
|
97
69
|
description: string
|
|
98
70
|
dangerous?: boolean
|
|
99
|
-
/** Offered but unreachable: a session not started for bypass can never gain
|
|
100
|
-
* it, and saying so beats omitting the row. */
|
|
101
71
|
disabled?: boolean
|
|
102
72
|
}
|
|
103
73
|
|
|
104
|
-
|
|
105
|
-
* The modes this session may actually be switched into, with the reasons baked
|
|
106
|
-
* in — the same filtering {@link PermissionModeSelect} applies, so an embedder
|
|
107
|
-
* rendering its own picker cannot drift from the panel's.
|
|
108
|
-
*/
|
|
109
|
-
export function permissionModeChoices(
|
|
110
|
-
modes?: readonly PermissionMode[],
|
|
111
|
-
canBypass?: boolean,
|
|
112
|
-
): PermissionModeChoice[] {
|
|
74
|
+
export function permissionModeChoices(modes?: readonly PermissionMode[], canBypass?: boolean): PermissionModeChoice[] {
|
|
113
75
|
const offered = modes ? PERMISSION_MODES.filter((m) => modes.includes(m.value)) : PERMISSION_MODES
|
|
114
76
|
return offered.map((m) => ({
|
|
115
77
|
value: m.value,
|
|
116
78
|
label: m.label,
|
|
117
79
|
description:
|
|
118
|
-
m.value === 'bypassPermissions' && canBypass === false
|
|
119
|
-
? 'Only available to a session started in this mode'
|
|
120
|
-
: m.description,
|
|
80
|
+
m.value === 'bypassPermissions' && canBypass === false ? 'Only available to a session started in this mode' : m.description,
|
|
121
81
|
dangerous: m.dangerous,
|
|
122
82
|
disabled: m.value === 'bypassPermissions' && canBypass === false,
|
|
123
83
|
}))
|
|
124
84
|
}
|
|
125
85
|
|
|
126
86
|
export interface PermissionModeSelectProps {
|
|
127
|
-
/** The session's current mode (TranscriptState.permissionMode). */
|
|
128
87
|
mode?: PermissionMode
|
|
129
88
|
onModeChange: (mode: PermissionMode) => void
|
|
130
|
-
/** Restrict what is offered — most of {@link PERMISSION_MODES} is Claude Code
|
|
131
|
-
* vocabulary the other engines have no meaning for. Defaults to all of them;
|
|
132
|
-
* pass the session's `capabilities.permissionModes`. */
|
|
133
89
|
modes?: readonly PermissionMode[]
|
|
134
|
-
/**
|
|
135
|
-
* Whether this session may be switched into `bypassPermissions` at all. The
|
|
136
|
-
* CLI refuses unless the process was spawned for it, so a session that didn't
|
|
137
|
-
* ask up front can never gain it. The row is shown disabled rather than hidden
|
|
138
|
-
* — "you can't have this here" is a more useful answer than a row that
|
|
139
|
-
* silently isn't there. `undefined` (an older server) offers it.
|
|
140
|
-
*/
|
|
141
90
|
canBypass?: boolean
|
|
142
|
-
/** 'toolbar' (default) is the composer's compact borderless trigger;
|
|
143
|
-
* 'form' is a standard field-sized Select for create/settings forms. */
|
|
144
91
|
variant?: 'toolbar' | 'form'
|
|
145
92
|
disabled?: boolean
|
|
146
93
|
className?: string
|
|
147
94
|
}
|
|
148
95
|
|
|
149
|
-
/** Permission-mode switcher: compact in the composer toolbar, field-sized in forms. */
|
|
150
96
|
export function PermissionModeSelect({
|
|
151
97
|
mode,
|
|
152
98
|
onModeChange,
|
|
@@ -158,46 +104,42 @@ export function PermissionModeSelect({
|
|
|
158
104
|
}: PermissionModeSelectProps) {
|
|
159
105
|
const dangerous = mode === 'bypassPermissions'
|
|
160
106
|
const offered = modes ? PERMISSION_MODES.filter((m) => modes.includes(m.value)) : PERMISSION_MODES
|
|
161
|
-
const unavailable = (meta: PermissionModeMeta) =>
|
|
162
|
-
meta.value === 'bypassPermissions' && canBypass === false
|
|
107
|
+
const unavailable = (meta: PermissionModeMeta) => meta.value === 'bypassPermissions' && canBypass === false
|
|
163
108
|
return (
|
|
164
109
|
<Select
|
|
165
110
|
items={offered.map((m) => ({ value: m.value, label: m.label }))}
|
|
166
111
|
value={mode ?? null}
|
|
167
112
|
onValueChange={(value) => {
|
|
168
|
-
if (typeof value === 'string' && value !== mode)
|
|
113
|
+
if (typeof value === 'string' && value !== mode) {
|
|
114
|
+
onModeChange(value as PermissionMode)
|
|
115
|
+
}
|
|
169
116
|
}}
|
|
170
|
-
disabled={disabled}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
>
|
|
171
119
|
<SelectTrigger
|
|
172
|
-
aria-label=
|
|
120
|
+
aria-label="Permission mode"
|
|
173
121
|
className={cn(
|
|
174
|
-
variant === 'toolbar' &&
|
|
175
|
-
'h-6 max-w-44 border-transparent bg-transparent hover:bg-surface-hover',
|
|
122
|
+
variant === 'toolbar' && 'h-6 max-w-44 border-transparent bg-transparent hover:bg-surface-hover',
|
|
176
123
|
dangerous ? 'text-danger' : variant === 'toolbar' ? 'text-fg-3' : undefined,
|
|
177
124
|
className,
|
|
178
|
-
)}
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
179
127
|
<span className={cn('truncate', variant === 'toolbar' && 'text-label')}>
|
|
180
|
-
<SelectValue placeholder=
|
|
128
|
+
<SelectValue placeholder="permissions" />
|
|
181
129
|
</span>
|
|
182
130
|
</SelectTrigger>
|
|
183
|
-
<SelectContent className=
|
|
131
|
+
<SelectContent className="min-w-64">
|
|
184
132
|
{offered.map((m) => {
|
|
185
133
|
const blocked = unavailable(m)
|
|
186
134
|
return (
|
|
187
135
|
<SelectItem key={m.value} value={m.value} disabled={blocked}>
|
|
188
136
|
<SelectItemText>
|
|
189
|
-
<span className=
|
|
190
|
-
<m.icon
|
|
191
|
-
className={cn('size-3.5 shrink-0', m.dangerous ? 'text-danger' : 'text-fg-3')}
|
|
192
|
-
/>
|
|
137
|
+
<span className="flex items-center gap-2">
|
|
138
|
+
<m.icon className={cn('size-3.5 shrink-0', m.dangerous ? 'text-danger' : 'text-fg-3')} />
|
|
193
139
|
<span className={cn('font-medium', m.dangerous && 'text-danger')}>{m.label}</span>
|
|
194
140
|
</span>
|
|
195
141
|
</SelectItemText>
|
|
196
|
-
<span
|
|
197
|
-
className={cn(
|
|
198
|
-
'pl-5.5 text-label',
|
|
199
|
-
blocked ? 'text-fg-4' : m.dangerous ? 'text-danger/80' : 'text-fg-4',
|
|
200
|
-
)}>
|
|
142
|
+
<span className={cn('pl-5.5 text-label', blocked ? 'text-fg-4' : m.dangerous ? 'text-danger/80' : 'text-fg-4')}>
|
|
201
143
|
{blocked ? 'Only available to a session started in this mode' : m.description}
|
|
202
144
|
</span>
|
|
203
145
|
</SelectItem>
|
|
@@ -5,29 +5,17 @@ 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 { planFromRequest } from '../../lib/plan-request.ts'
|
|
8
9
|
import { toolIcon } from '../../lib/tool-icon.ts'
|
|
10
|
+
import { Response } from './Response.tsx'
|
|
9
11
|
|
|
10
12
|
export interface PermissionPromptProps {
|
|
11
13
|
request: PermissionRequest
|
|
12
14
|
onApprove: (requestId: string) => void
|
|
13
|
-
/** `message` is fed back to the agent, which can then try something else;
|
|
14
|
-
* `interrupt` also stops the turn. */
|
|
15
15
|
onDeny: (requestId: string, message?: string, interrupt?: boolean) => void
|
|
16
16
|
className?: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Generic allow/deny prompt for a pending permission request.
|
|
21
|
-
*
|
|
22
|
-
* Three outcomes, not two: denying usually means "not that, try something else",
|
|
23
|
-
* so plain Deny lets the turn continue (with an optional reason the agent reads)
|
|
24
|
-
* while "Deny & stop" also interrupts.
|
|
25
|
-
*
|
|
26
|
-
* The heading is whatever the engine authored — `title`, else `displayName`.
|
|
27
|
-
* Composing "wants to use {tool}" instead would be wrong for codex, where an
|
|
28
|
-
* approval is usually an *escalation after a sandbox refusal* and the runner has
|
|
29
|
-
* already written the sentence that says so.
|
|
30
|
-
*/
|
|
31
19
|
export function PermissionPrompt({ request, onApprove, onDeny, className }: PermissionPromptProps) {
|
|
32
20
|
const [showInput, setShowInput] = useState(false)
|
|
33
21
|
const [denying, setDenying] = useState(false)
|
|
@@ -40,76 +28,84 @@ export function PermissionPrompt({ request, onApprove, onDeny, className }: Perm
|
|
|
40
28
|
setDenying(false)
|
|
41
29
|
}
|
|
42
30
|
|
|
31
|
+
const plan = planFromRequest(request)
|
|
43
32
|
const summary = toolInputPreview(request.input)
|
|
44
33
|
const ToolIcon = toolIcon(request.toolName)
|
|
45
34
|
|
|
46
35
|
return (
|
|
47
|
-
<div
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<div className='text-body-sm font-medium text-fg-1'>
|
|
54
|
-
{request.title ?? request.displayName ?? 'Permission needed'}
|
|
36
|
+
<div data-slot="permission-prompt" className={cn('rounded-lg border border-warning/40 bg-warning-bg p-3', className)}>
|
|
37
|
+
<div className="flex items-start gap-2.5">
|
|
38
|
+
<Hand className="mt-0.5 size-4 shrink-0 text-warning" />
|
|
39
|
+
<div className="min-w-0 flex-1">
|
|
40
|
+
<div className="text-body-sm font-medium text-fg-1">
|
|
41
|
+
{plan ? 'Plan ready for review' : (request.title ?? request.displayName ?? 'Permission needed')}
|
|
55
42
|
</div>
|
|
56
|
-
{request.description ?
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
{request.description ? <div className="mt-0.5 text-label text-fg-3">{request.description}</div> : null}
|
|
44
|
+
{request.decisionReason ? <div className="mt-0.5 text-label text-fg-4">{request.decisionReason}</div> : null}
|
|
45
|
+
{plan ? (
|
|
46
|
+
<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">
|
|
47
|
+
<Response>{plan}</Response>
|
|
48
|
+
</div>
|
|
49
|
+
) : (
|
|
50
|
+
<>
|
|
51
|
+
<div className="mt-1.5 flex min-w-0 items-center gap-2">
|
|
52
|
+
<ToolIcon className="size-3 shrink-0 text-fg-3" />
|
|
53
|
+
<span className="shrink-0 font-mono text-label font-medium text-fg-2">{request.toolName}</span>
|
|
54
|
+
{summary ? <span className="min-w-0 truncate font-mono text-label text-fg-4">{summary}</span> : null}
|
|
55
|
+
</div>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
className="mt-1 font-mono text-label text-fg-3 underline-offset-2 hover:underline"
|
|
59
|
+
onClick={() => setShowInput((v) => !v)}
|
|
60
|
+
>
|
|
61
|
+
{showInput ? 'Hide' : 'Show'} {request.toolName} input
|
|
62
|
+
</button>
|
|
63
|
+
{showInput ? (
|
|
64
|
+
<pre className="mt-1.5 max-h-48 overflow-auto rounded-md bg-code-bg px-2.5 py-1.5 font-mono text-label whitespace-pre-wrap text-fg-2">
|
|
65
|
+
{JSON.stringify(request.input, null, 2)}
|
|
66
|
+
</pre>
|
|
67
|
+
) : null}
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
82
70
|
</div>
|
|
83
|
-
<div className=
|
|
84
|
-
<Button size=
|
|
85
|
-
Allow
|
|
71
|
+
<div className="flex shrink-0 flex-wrap justify-end gap-1.5">
|
|
72
|
+
<Button size="sm" onClick={() => onApprove(request.id)}>
|
|
73
|
+
{plan ? 'Approve plan' : 'Allow'}
|
|
86
74
|
</Button>
|
|
87
|
-
<Button size=
|
|
88
|
-
Deny
|
|
75
|
+
<Button size="sm" variant="outline" onClick={() => setDenying((v) => !v)}>
|
|
76
|
+
{plan ? 'Keep planning' : 'Deny'}
|
|
89
77
|
</Button>
|
|
90
|
-
<Button size=
|
|
91
|
-
Deny &
|
|
78
|
+
<Button size="sm" variant="outline" onClick={() => deny(true)}>
|
|
79
|
+
{plan ? 'Stop the turn' : 'Deny & stop'}
|
|
92
80
|
</Button>
|
|
93
81
|
</div>
|
|
94
82
|
</div>
|
|
95
83
|
{denying ? (
|
|
96
|
-
<div className=
|
|
84
|
+
<div className="mt-2.5 flex items-center gap-2 border-t border-warning/30 pt-2.5">
|
|
97
85
|
<Input
|
|
98
86
|
autoFocus
|
|
99
87
|
value={reason}
|
|
100
88
|
onChange={(e) => setReason(e.target.value)}
|
|
101
89
|
onKeyDown={(e) => {
|
|
102
|
-
if (e.key === 'Enter')
|
|
103
|
-
|
|
90
|
+
if (e.key === 'Enter') {
|
|
91
|
+
deny(false)
|
|
92
|
+
}
|
|
93
|
+
if (e.key === 'Escape') {
|
|
94
|
+
setDenying(false)
|
|
95
|
+
}
|
|
104
96
|
}}
|
|
105
|
-
placeholder=
|
|
106
|
-
|
|
97
|
+
placeholder={
|
|
98
|
+
plan
|
|
99
|
+
? 'What should change? (optional) — the agent keeps planning and reads this'
|
|
100
|
+
: 'Reason (optional) — the agent reads this and can try something else'
|
|
101
|
+
}
|
|
102
|
+
className="h-7 flex-1"
|
|
107
103
|
/>
|
|
108
|
-
<Button size=
|
|
104
|
+
<Button size="sm" variant="outline" onClick={() => setDenying(false)}>
|
|
109
105
|
Cancel
|
|
110
106
|
</Button>
|
|
111
|
-
<Button size=
|
|
112
|
-
Deny
|
|
107
|
+
<Button size="sm" variant="destructive" onClick={() => deny(false)}>
|
|
108
|
+
{plan ? 'Keep planning' : 'Deny'}
|
|
113
109
|
</Button>
|
|
114
110
|
</div>
|
|
115
111
|
) : null}
|