@workerdeck/ui 0.7.0 → 0.11.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 +81 -3
- package/build/SessionPanel-CyhygZx_.d.mts +277 -0
- package/build/SessionPanel-_U8tjX29.mjs +8409 -0
- package/build/SessionPanel-_U8tjX29.mjs.map +1 -0
- package/build/format-DqR56Y8l.mjs +162 -0
- package/build/format-DqR56Y8l.mjs.map +1 -0
- package/build/format-ljc3lKpA.d.mts +59 -0
- package/build/format.d.mts +2 -0
- package/build/format.mjs +2 -0
- package/build/index.d.mts +615 -87
- package/build/index.mjs +6 -5081
- package/build/index.mjs.map +1 -1
- package/build/workspace.d.mts +199 -0
- package/build/workspace.mjs +849 -0
- package/build/workspace.mjs.map +1 -0
- package/package.json +22 -4
- package/src/components/agent/CodeEditor.tsx +300 -0
- package/src/components/agent/Composer.tsx +522 -87
- package/src/components/agent/ContextDialog.tsx +99 -0
- package/src/components/agent/Conversation.tsx +11 -3
- package/src/components/agent/EditorTabs.tsx +165 -0
- package/src/components/agent/FileCard.tsx +26 -0
- package/src/components/agent/FileTree.tsx +287 -0
- package/src/components/agent/FileViewer.tsx +148 -0
- package/src/components/agent/HostFilesDialog.tsx +218 -0
- package/src/components/agent/Loader.tsx +120 -14
- package/src/components/agent/McpDialog.tsx +363 -0
- package/src/components/agent/Message.tsx +51 -17
- package/src/components/agent/ModelSelect.tsx +34 -6
- package/src/components/agent/PermissionModeSelect.tsx +133 -22
- package/src/components/agent/PermissionPrompt.tsx +164 -6
- package/src/components/agent/PromptTokenText.tsx +39 -0
- package/src/components/agent/QuestionPrompt.tsx +122 -0
- package/src/components/agent/Reasoning.tsx +20 -5
- package/src/components/agent/Response.tsx +128 -0
- package/src/components/agent/SessionEmptyState.tsx +65 -0
- package/src/components/agent/SessionInfoDialog.tsx +163 -0
- package/src/components/agent/SessionPanel.tsx +756 -90
- package/src/components/agent/SessionWorkspace.tsx +282 -0
- package/src/components/agent/SkillsDialog.tsx +195 -0
- package/src/components/agent/StatusBar.tsx +85 -18
- package/src/components/agent/ToolCallCard.tsx +243 -30
- package/src/components/agent/Transcript.tsx +540 -27
- package/src/components/agent/UsageDialog.tsx +168 -0
- package/src/components/agent/line-prompt.tsx +249 -0
- package/src/components/agent/transcript-variant.tsx +61 -0
- package/src/components/prompt-area/prompt-area-engine.ts +53 -0
- package/src/components/prompt-area/types.ts +15 -0
- package/src/components/prompt-area/use-prompt-area.ts +20 -0
- package/src/components/ui/CodeBlock.tsx +40 -2
- package/src/components/ui/CopyButton.tsx +28 -3
- package/src/components/ui/Dialog.tsx +92 -0
- package/src/components/ui/Menu.tsx +55 -0
- package/src/components/ui/Splitter.tsx +133 -0
- package/src/components/ui/Tooltip.tsx +22 -5
- package/src/format.ts +10 -0
- package/src/index.ts +63 -2
- package/src/lib/clipboard.ts +56 -0
- package/src/lib/format.ts +114 -0
- package/src/lib/tool-icon.ts +96 -0
- package/src/workspace.ts +28 -0
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Check, Copy } from 'lucide-react'
|
|
3
3
|
import { Button, type ButtonProps } from './Button.tsx'
|
|
4
|
+
import { copyText } from '../../lib/clipboard.ts'
|
|
4
5
|
import { cn } from '../../lib/utils.ts'
|
|
5
6
|
|
|
6
7
|
export interface CopyButtonProps extends Omit<ButtonProps, 'onClick' | 'children'> {
|
|
7
8
|
value: string
|
|
9
|
+
/** Draw the state as characters (`⧉` / `✓`) rather than line-art icons — for
|
|
10
|
+
* terminal-styled surfaces, where an SVG reads as another app's button. */
|
|
11
|
+
glyph?: boolean
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
export function CopyButton({
|
|
14
|
+
export function CopyButton({
|
|
15
|
+
value,
|
|
16
|
+
glyph,
|
|
17
|
+
className,
|
|
18
|
+
variant = 'ghost',
|
|
19
|
+
size = 'icon-sm',
|
|
20
|
+
...props
|
|
21
|
+
}: CopyButtonProps) {
|
|
11
22
|
const [copied, setCopied] = useState(false)
|
|
12
23
|
return (
|
|
13
24
|
<Button
|
|
@@ -16,13 +27,27 @@ export function CopyButton({ value, className, variant = 'ghost', size = 'icon-s
|
|
|
16
27
|
aria-label='Copy'
|
|
17
28
|
className={cn('text-fg-3', className)}
|
|
18
29
|
onClick={() => {
|
|
19
|
-
|
|
30
|
+
// Through `copyText`, which falls back for insecure origins — the
|
|
31
|
+
// dashboard on a LAN address has no `navigator.clipboard` at all, and
|
|
32
|
+
// reaching straight for `.writeText` there throws.
|
|
33
|
+
void copyText(value).then((ok) => {
|
|
34
|
+
// Only tick when it really copied: a check mark over an empty
|
|
35
|
+
// clipboard is worse than no feedback.
|
|
36
|
+
if (!ok) return
|
|
20
37
|
setCopied(true)
|
|
21
38
|
setTimeout(() => setCopied(false), 1500)
|
|
22
39
|
})
|
|
23
40
|
}}
|
|
24
41
|
{...props}>
|
|
25
|
-
{
|
|
42
|
+
{glyph ? (
|
|
43
|
+
<span className={cn('font-mono text-body-sm leading-5', copied && 'text-success')}>
|
|
44
|
+
{copied ? '✓' : '⧉'}
|
|
45
|
+
</span>
|
|
46
|
+
) : copied ? (
|
|
47
|
+
<Check className='size-3.5 text-success' />
|
|
48
|
+
) : (
|
|
49
|
+
<Copy className='size-3.5' />
|
|
50
|
+
)}
|
|
26
51
|
</Button>
|
|
27
52
|
)
|
|
28
53
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'react'
|
|
2
|
+
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
|
|
3
|
+
import { X } from 'lucide-react'
|
|
4
|
+
import { cn } from '../../lib/utils.ts'
|
|
5
|
+
|
|
6
|
+
export const Dialog = DialogPrimitive.Root
|
|
7
|
+
export const DialogTrigger = DialogPrimitive.Trigger
|
|
8
|
+
export const DialogClose = DialogPrimitive.Close
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A dismissible panel, sized for reading rather than confirming — the web
|
|
12
|
+
* counterpart of the iOS app's detail sheets (context, usage, session info, MCP).
|
|
13
|
+
*
|
|
14
|
+
* Taller than {@link AlertDialogContent} and scrollable inside, because these
|
|
15
|
+
* carry lists whose length is the engine's business, not the layout's.
|
|
16
|
+
*/
|
|
17
|
+
export const DialogContent: FunctionComponent<
|
|
18
|
+
DialogPrimitive.Popup.Props & { size?: 'sm' | 'md' | 'lg' }
|
|
19
|
+
> = ({ className, children, size = 'md', ...props }) => (
|
|
20
|
+
<DialogPrimitive.Portal>
|
|
21
|
+
<DialogPrimitive.Backdrop
|
|
22
|
+
className={cn(
|
|
23
|
+
'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
|
|
24
|
+
'transition-opacity duration-(--motion-base)',
|
|
25
|
+
'data-starting-style:opacity-0 data-ending-style:opacity-0',
|
|
26
|
+
)}
|
|
27
|
+
/>
|
|
28
|
+
<DialogPrimitive.Popup
|
|
29
|
+
data-slot='dialog-content'
|
|
30
|
+
className={cn(
|
|
31
|
+
'fixed top-1/2 left-1/2 z-70 flex max-h-[min(42rem,calc(100dvh-3rem))] -translate-x-1/2 -translate-y-1/2 flex-col',
|
|
32
|
+
size === 'sm' && 'w-[min(24rem,calc(100vw-2rem))]',
|
|
33
|
+
size === 'md' && 'w-[min(32rem,calc(100vw-2rem))]',
|
|
34
|
+
size === 'lg' && 'w-[min(46rem,calc(100vw-2rem))]',
|
|
35
|
+
'rounded-lg border border-border bg-surface shadow-(--shadow-lg) outline-none',
|
|
36
|
+
'transition-[opacity,transform] duration-(--motion-base)',
|
|
37
|
+
'data-starting-style:scale-95 data-starting-style:opacity-0',
|
|
38
|
+
'data-ending-style:scale-95 data-ending-style:opacity-0',
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...props}>
|
|
42
|
+
{children}
|
|
43
|
+
</DialogPrimitive.Popup>
|
|
44
|
+
</DialogPrimitive.Portal>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
/** Title row with the close button, pinned above the scrolling body. */
|
|
48
|
+
export const DialogHeader: FunctionComponent<{
|
|
49
|
+
title: string
|
|
50
|
+
description?: string
|
|
51
|
+
/** Rendered between the title and the close button. */
|
|
52
|
+
actions?: React.ReactNode
|
|
53
|
+
}> = ({ title, description, actions }) => (
|
|
54
|
+
<div className='flex items-start gap-2 border-b border-border px-4 py-3'>
|
|
55
|
+
<div className='min-w-0 flex-1'>
|
|
56
|
+
<DialogPrimitive.Title className='truncate text-body-sm font-semibold text-text'>
|
|
57
|
+
{title}
|
|
58
|
+
</DialogPrimitive.Title>
|
|
59
|
+
{description ? (
|
|
60
|
+
<DialogPrimitive.Description className='mt-0.5 text-label text-fg-4'>
|
|
61
|
+
{description}
|
|
62
|
+
</DialogPrimitive.Description>
|
|
63
|
+
) : null}
|
|
64
|
+
</div>
|
|
65
|
+
{actions}
|
|
66
|
+
<DialogPrimitive.Close
|
|
67
|
+
aria-label='Close'
|
|
68
|
+
className='-mr-1 flex size-6 shrink-0 items-center justify-center rounded-md text-fg-3 transition-colors outline-none hover:bg-surface-hover hover:text-fg-1'>
|
|
69
|
+
<X className='size-3.5' />
|
|
70
|
+
</DialogPrimitive.Close>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
/** The scrolling region under the header. */
|
|
75
|
+
export const DialogBody: FunctionComponent<React.HTMLAttributes<HTMLDivElement>> = ({
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}) => <div className={cn('min-h-0 flex-1 overflow-y-auto p-4', className)} {...props} />
|
|
79
|
+
|
|
80
|
+
/** A label/value row — the shape every one of these panels is mostly made of. */
|
|
81
|
+
export const DialogRow: FunctionComponent<{
|
|
82
|
+
label: string
|
|
83
|
+
children: React.ReactNode
|
|
84
|
+
mono?: boolean
|
|
85
|
+
}> = ({ label, children, mono }) => (
|
|
86
|
+
<div className='flex items-baseline justify-between gap-4 py-1.5'>
|
|
87
|
+
<span className='shrink-0 text-label text-fg-3'>{label}</span>
|
|
88
|
+
<span className={cn('min-w-0 truncate text-right text-body-sm text-fg-1', mono && 'font-mono text-label')}>
|
|
89
|
+
{children}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'react'
|
|
2
|
+
import { Menu as MenuPrimitive } from '@base-ui/react/menu'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
|
|
5
|
+
export const Menu = MenuPrimitive.Root
|
|
6
|
+
export const MenuTrigger = MenuPrimitive.Trigger
|
|
7
|
+
|
|
8
|
+
export const MenuContent: FunctionComponent<
|
|
9
|
+
MenuPrimitive.Popup.Props &
|
|
10
|
+
Pick<MenuPrimitive.Positioner.Props, 'align' | 'side' | 'sideOffset'>
|
|
11
|
+
> = ({ className, align = 'end', side = 'bottom', sideOffset = 6, ...props }) => (
|
|
12
|
+
<MenuPrimitive.Portal>
|
|
13
|
+
<MenuPrimitive.Positioner
|
|
14
|
+
align={align}
|
|
15
|
+
side={side}
|
|
16
|
+
sideOffset={sideOffset}
|
|
17
|
+
className='isolate z-60 outline-none'>
|
|
18
|
+
<MenuPrimitive.Popup
|
|
19
|
+
data-slot='menu-content'
|
|
20
|
+
className={cn(
|
|
21
|
+
'min-w-48 rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
|
|
22
|
+
'transition-[opacity,transform] duration-(--motion-base)',
|
|
23
|
+
'data-starting-style:scale-95 data-starting-style:opacity-0',
|
|
24
|
+
'data-ending-style:scale-95 data-ending-style:opacity-0',
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
</MenuPrimitive.Positioner>
|
|
30
|
+
</MenuPrimitive.Portal>
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
export const MenuItem: FunctionComponent<MenuPrimitive.Item.Props & { destructive?: boolean }> = ({
|
|
34
|
+
className,
|
|
35
|
+
destructive,
|
|
36
|
+
...props
|
|
37
|
+
}) => (
|
|
38
|
+
<MenuPrimitive.Item
|
|
39
|
+
data-slot='menu-item'
|
|
40
|
+
className={cn(
|
|
41
|
+
'flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-body-sm outline-none select-none',
|
|
42
|
+
'data-highlighted:bg-surface-hover',
|
|
43
|
+
destructive ? 'text-danger' : 'text-text',
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
export const MenuSeparator: FunctionComponent<MenuPrimitive.Separator.Props> = ({
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}) => (
|
|
54
|
+
<MenuPrimitive.Separator className={cn('my-1 h-px bg-border', className)} {...props} />
|
|
55
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useRef,
|
|
4
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
5
|
+
type PointerEvent as ReactPointerEvent,
|
|
6
|
+
} from 'react'
|
|
7
|
+
import { cn } from '../../lib/utils.ts'
|
|
8
|
+
|
|
9
|
+
export interface SplitterProps {
|
|
10
|
+
/**
|
|
11
|
+
* ARIA's sense of the word: a `vertical` splitter is a vertical bar between
|
|
12
|
+
* two side-by-side panes, and it resizes a **width**. A `horizontal` one sits
|
|
13
|
+
* between stacked panes and resizes a **height**.
|
|
14
|
+
*/
|
|
15
|
+
orientation: 'vertical' | 'horizontal'
|
|
16
|
+
/** Current size of the pane this splitter controls, in pixels. */
|
|
17
|
+
value: number
|
|
18
|
+
onValueChange: (value: number) => void
|
|
19
|
+
min: number
|
|
20
|
+
max: number
|
|
21
|
+
/** Keyboard step. */
|
|
22
|
+
step?: number
|
|
23
|
+
/** Set when dragging the splitter *away* from the origin should shrink the
|
|
24
|
+
* controlled pane — i.e. the pane is on the right or the bottom. */
|
|
25
|
+
inverted?: boolean
|
|
26
|
+
/** Required: "Resize" alone does not say which of two splitters this is. */
|
|
27
|
+
'aria-label': string
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A draggable pane divider.
|
|
33
|
+
*
|
|
34
|
+
* Hand-rolled rather than depended on: `@base-ui/react` ships no splitter, the
|
|
35
|
+
* behaviour is a hundred lines of pointer events, and this repo's instinct at
|
|
36
|
+
* this layer is to own it (the composer is vendored for the same reason).
|
|
37
|
+
*
|
|
38
|
+
* Pointer capture is what makes it survive a fast drag — without it the pointer
|
|
39
|
+
* leaves the 5px bar within a frame and the moves go to whatever is underneath,
|
|
40
|
+
* which for this layout is an iframe-free but still selection-happy code pane.
|
|
41
|
+
* The drag origin is captured on pointerdown and every move is measured against
|
|
42
|
+
* it, so the pane cannot drift relative to the cursor over a long drag the way
|
|
43
|
+
* per-move deltas do once clamping is involved.
|
|
44
|
+
*
|
|
45
|
+
* Keyboard-operable and announced as a separator, because a pane you can only
|
|
46
|
+
* size by dragging is a pane some people cannot size.
|
|
47
|
+
*/
|
|
48
|
+
export function Splitter({
|
|
49
|
+
orientation,
|
|
50
|
+
value,
|
|
51
|
+
onValueChange,
|
|
52
|
+
min,
|
|
53
|
+
max,
|
|
54
|
+
step = 16,
|
|
55
|
+
inverted,
|
|
56
|
+
'aria-label': label,
|
|
57
|
+
className,
|
|
58
|
+
}: SplitterProps) {
|
|
59
|
+
const drag = useRef<{ origin: number; start: number } | null>(null)
|
|
60
|
+
const vertical = orientation === 'vertical'
|
|
61
|
+
|
|
62
|
+
const clamp = useCallback((next: number) => Math.min(max, Math.max(min, next)), [min, max])
|
|
63
|
+
|
|
64
|
+
const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
65
|
+
// Secondary buttons open context menus; they are not drags.
|
|
66
|
+
if (event.button !== 0) return
|
|
67
|
+
event.preventDefault()
|
|
68
|
+
event.currentTarget.setPointerCapture(event.pointerId)
|
|
69
|
+
drag.current = { origin: vertical ? event.clientX : event.clientY, start: value }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
73
|
+
const state = drag.current
|
|
74
|
+
if (!state) return
|
|
75
|
+
const delta = (vertical ? event.clientX : event.clientY) - state.origin
|
|
76
|
+
onValueChange(clamp(state.start + (inverted ? -delta : delta)))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
80
|
+
if (!drag.current) return
|
|
81
|
+
drag.current = null
|
|
82
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
83
|
+
event.currentTarget.releasePointerCapture(event.pointerId)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
88
|
+
// The arrows that move *along* this splitter's axis of travel; the other
|
|
89
|
+
// pair is left to the page, which is what a separator should do with them.
|
|
90
|
+
const grow = vertical ? 'ArrowRight' : 'ArrowDown'
|
|
91
|
+
const shrink = vertical ? 'ArrowLeft' : 'ArrowUp'
|
|
92
|
+
const direction = inverted ? -1 : 1
|
|
93
|
+
if (event.key === grow) onValueChange(clamp(value + step * direction))
|
|
94
|
+
else if (event.key === shrink) onValueChange(clamp(value - step * direction))
|
|
95
|
+
else if (event.key === 'Home') onValueChange(min)
|
|
96
|
+
else if (event.key === 'End') onValueChange(max)
|
|
97
|
+
else return
|
|
98
|
+
event.preventDefault()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
data-slot='splitter'
|
|
104
|
+
role='separator'
|
|
105
|
+
tabIndex={0}
|
|
106
|
+
aria-label={label}
|
|
107
|
+
aria-orientation={orientation}
|
|
108
|
+
aria-valuenow={Math.round(value)}
|
|
109
|
+
aria-valuemin={min}
|
|
110
|
+
aria-valuemax={max}
|
|
111
|
+
onPointerDown={onPointerDown}
|
|
112
|
+
onPointerMove={onPointerMove}
|
|
113
|
+
onPointerUp={endDrag}
|
|
114
|
+
onPointerCancel={endDrag}
|
|
115
|
+
onKeyDown={onKeyDown}
|
|
116
|
+
className={cn(
|
|
117
|
+
// A 1px line that reads as a border, with a larger invisible grab area
|
|
118
|
+
// around it — a hairline is an honest divider and a cruel target.
|
|
119
|
+
'group relative shrink-0 touch-none bg-border transition-colors',
|
|
120
|
+
'hover:bg-border-strong focus-visible:bg-accent focus-visible:outline-none',
|
|
121
|
+
vertical ? 'w-px cursor-col-resize' : 'h-px cursor-row-resize',
|
|
122
|
+
className,
|
|
123
|
+
)}>
|
|
124
|
+
<span
|
|
125
|
+
aria-hidden
|
|
126
|
+
className={cn(
|
|
127
|
+
'absolute',
|
|
128
|
+
vertical ? '-inset-x-[3px] inset-y-0' : '-inset-y-[3px] inset-x-0',
|
|
129
|
+
)}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FunctionComponent, type ReactNode } from 'react'
|
|
1
|
+
import { type FunctionComponent, type ReactElement, type ReactNode } from 'react'
|
|
2
2
|
import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
4
|
|
|
@@ -21,14 +21,31 @@ export const TooltipContent: FunctionComponent<
|
|
|
21
21
|
</TooltipPrimitive.Portal>
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Convenience wrapper: `<Tip content="..."><Button/></Tip>`.
|
|
26
|
+
*
|
|
27
|
+
* Pass `render` when the trigger must *be* an element you already have — a tab, a
|
|
28
|
+
* row — rather than something wrapped in a span. The default span is fine beside
|
|
29
|
+
* a button but would break any layout that styles its own children (a flex tab
|
|
30
|
+
* strip gets an extra box between the container and its items).
|
|
31
|
+
*/
|
|
32
|
+
export function Tip({
|
|
33
|
+
content,
|
|
34
|
+
render,
|
|
35
|
+
side,
|
|
36
|
+
children,
|
|
37
|
+
}: {
|
|
38
|
+
content: ReactNode
|
|
39
|
+
render?: ReactElement
|
|
40
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
41
|
+
children?: ReactNode
|
|
42
|
+
}) {
|
|
26
43
|
return (
|
|
27
44
|
<TooltipPrimitive.Root>
|
|
28
|
-
<TooltipPrimitive.Trigger render={<span className='inline-flex' />}>
|
|
45
|
+
<TooltipPrimitive.Trigger render={render ?? <span className='inline-flex' />}>
|
|
29
46
|
{children}
|
|
30
47
|
</TooltipPrimitive.Trigger>
|
|
31
|
-
<TooltipContent>{content}</TooltipContent>
|
|
48
|
+
<TooltipContent side={side}>{content}</TooltipContent>
|
|
32
49
|
</TooltipPrimitive.Root>
|
|
33
50
|
)
|
|
34
51
|
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The formatting helpers, on their own entry point.
|
|
3
|
+
*
|
|
4
|
+
* `src/index.ts` pulls in React and every component with it; a host that renders
|
|
5
|
+
* session readings *outside* React — the VS Code extension host drawing them in
|
|
6
|
+
* the window status bar, say — needs the numbers spelled the same way without
|
|
7
|
+
* paying for that. These functions are pure and dependency-free, so the subpath
|
|
8
|
+
* costs nothing and keeps one truth for what "45.2k" and "2h 10m" mean.
|
|
9
|
+
*/
|
|
10
|
+
export * from './lib/format.ts'
|
package/src/index.ts
CHANGED
|
@@ -20,11 +20,28 @@ export {
|
|
|
20
20
|
AlertDialogTitle,
|
|
21
21
|
AlertDialogTrigger,
|
|
22
22
|
} from './components/ui/AlertDialog.tsx'
|
|
23
|
+
export {
|
|
24
|
+
Menu,
|
|
25
|
+
MenuContent,
|
|
26
|
+
MenuItem,
|
|
27
|
+
MenuSeparator,
|
|
28
|
+
MenuTrigger,
|
|
29
|
+
} from './components/ui/Menu.tsx'
|
|
30
|
+
export {
|
|
31
|
+
Dialog,
|
|
32
|
+
DialogBody,
|
|
33
|
+
DialogClose,
|
|
34
|
+
DialogContent,
|
|
35
|
+
DialogHeader,
|
|
36
|
+
DialogRow,
|
|
37
|
+
DialogTrigger,
|
|
38
|
+
} from './components/ui/Dialog.tsx'
|
|
23
39
|
export { Tip, TooltipContent, TooltipProvider } from './components/ui/Tooltip.tsx'
|
|
24
40
|
export { Toaster, toast } from './components/ui/Sonner.tsx'
|
|
25
41
|
export { CopyButton, type CopyButtonProps } from './components/ui/CopyButton.tsx'
|
|
26
42
|
export { Spinner } from './components/ui/Spinner.tsx'
|
|
27
43
|
export { CodeBlock, type CodeBlockProps } from './components/ui/CodeBlock.tsx'
|
|
44
|
+
export { Splitter, type SplitterProps } from './components/ui/Splitter.tsx'
|
|
28
45
|
export { ProgressRing, type ProgressRingProps } from './components/ui/ProgressRing.tsx'
|
|
29
46
|
// Prompt input (vendored just-marketing/prompt-area, themed to these tokens)
|
|
30
47
|
export {
|
|
@@ -47,7 +64,16 @@ export {
|
|
|
47
64
|
} from './components/prompt-area/index.ts'
|
|
48
65
|
|
|
49
66
|
// Agent-control components
|
|
50
|
-
export {
|
|
67
|
+
export {
|
|
68
|
+
SessionPanel,
|
|
69
|
+
type SessionControls,
|
|
70
|
+
type SessionPanelProps,
|
|
71
|
+
type SessionSurfacePanel,
|
|
72
|
+
type SessionVitals,
|
|
73
|
+
} from './components/agent/SessionPanel.tsx'
|
|
74
|
+
// The workspace layout and its Monaco editor live at `@workerdeck/ui/workspace`
|
|
75
|
+
// — deliberately unreachable from here, so importing this entry never drags
|
|
76
|
+
// Monaco into the bundle. See `src/workspace.ts`.
|
|
51
77
|
export { Transcript, type TranscriptProps } from './components/agent/Transcript.tsx'
|
|
52
78
|
export {
|
|
53
79
|
Conversation,
|
|
@@ -56,6 +82,11 @@ export {
|
|
|
56
82
|
type ConversationProps,
|
|
57
83
|
} from './components/agent/Conversation.tsx'
|
|
58
84
|
export { Message, MessageContent, type MessageProps } from './components/agent/Message.tsx'
|
|
85
|
+
export {
|
|
86
|
+
TranscriptVariantProvider,
|
|
87
|
+
useTranscriptVariant,
|
|
88
|
+
type TranscriptVariant,
|
|
89
|
+
} from './components/agent/transcript-variant.tsx'
|
|
59
90
|
export { Response, type ResponseProps } from './components/agent/Response.tsx'
|
|
60
91
|
export { Reasoning, type ReasoningProps } from './components/agent/Reasoning.tsx'
|
|
61
92
|
export { Loader } from './components/agent/Loader.tsx'
|
|
@@ -69,31 +100,61 @@ export {
|
|
|
69
100
|
type QuestionPromptProps,
|
|
70
101
|
type QuestionBehaviorMeta,
|
|
71
102
|
} from './components/agent/QuestionPrompt.tsx'
|
|
72
|
-
export {
|
|
103
|
+
export {
|
|
104
|
+
Composer,
|
|
105
|
+
skillPrompt,
|
|
106
|
+
type ComposerFileMatch,
|
|
107
|
+
type ComposerHandle,
|
|
108
|
+
type ComposerProps,
|
|
109
|
+
} from './components/agent/Composer.tsx'
|
|
73
110
|
export { ModelSelect, type ModelSelectProps } from './components/agent/ModelSelect.tsx'
|
|
74
111
|
export {
|
|
75
112
|
PERMISSION_MODES,
|
|
76
113
|
PermissionModeSelect,
|
|
114
|
+
permissionModeChoices,
|
|
115
|
+
permissionModeMeta,
|
|
116
|
+
type PermissionModeChoice,
|
|
77
117
|
type PermissionModeMeta,
|
|
78
118
|
type PermissionModeSelectProps,
|
|
79
119
|
} from './components/agent/PermissionModeSelect.tsx'
|
|
80
120
|
export { StatusBar, type StatusBarProps } from './components/agent/StatusBar.tsx'
|
|
121
|
+
export { ContextDialog, type ContextDialogProps } from './components/agent/ContextDialog.tsx'
|
|
122
|
+
export { UsageDialog, type UsageDialogProps } from './components/agent/UsageDialog.tsx'
|
|
123
|
+
export {
|
|
124
|
+
SessionInfoDialog,
|
|
125
|
+
type SessionInfoDialogProps,
|
|
126
|
+
} from './components/agent/SessionInfoDialog.tsx'
|
|
127
|
+
export { McpDialog, type McpDialogProps } from './components/agent/McpDialog.tsx'
|
|
128
|
+
export { SkillsDialog, type SkillsDialogProps } from './components/agent/SkillsDialog.tsx'
|
|
129
|
+
export { HostFilesDialog, type HostFilesDialogProps } from './components/agent/HostFilesDialog.tsx'
|
|
81
130
|
export {
|
|
82
131
|
SessionList,
|
|
83
132
|
SessionListItem,
|
|
84
133
|
type SessionListItemProps,
|
|
85
134
|
type SessionListProps,
|
|
86
135
|
} from './components/agent/SessionList.tsx'
|
|
136
|
+
export {
|
|
137
|
+
SessionEmptyState,
|
|
138
|
+
type SessionEmptyStateProps,
|
|
139
|
+
} from './components/agent/SessionEmptyState.tsx'
|
|
140
|
+
export { PromptTokenText } from './components/agent/PromptTokenText.tsx'
|
|
87
141
|
export { STATUS_META } from './components/agent/status.ts'
|
|
88
142
|
|
|
89
143
|
// Utilities
|
|
90
144
|
export { cn } from './lib/utils.ts'
|
|
145
|
+
export { copyText } from './lib/clipboard.ts'
|
|
146
|
+
export { isMutatingTool, toolIcon } from './lib/tool-icon.ts'
|
|
91
147
|
export {
|
|
148
|
+
formatAgoPrecise,
|
|
92
149
|
formatBytes,
|
|
93
150
|
formatCost,
|
|
94
151
|
formatCountdown,
|
|
95
152
|
formatDuration,
|
|
153
|
+
formatRateLimitWindow,
|
|
154
|
+
formatRateLimitWindowLong,
|
|
96
155
|
formatRelativeTime,
|
|
97
156
|
formatTokens,
|
|
157
|
+
friendlyModel,
|
|
158
|
+
rateLimitWindowSeconds,
|
|
98
159
|
toolInputPreview,
|
|
99
160
|
} from './lib/format.ts'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy text to the clipboard, on origins where the modern API does not exist.
|
|
3
|
+
*
|
|
4
|
+
* `navigator.clipboard` is gated on a **secure context**: HTTPS, or localhost.
|
|
5
|
+
* A WorkerDeck dashboard reached the way it is meant to be reached — plain HTTP
|
|
6
|
+
* on a LAN address, from a laptop or a phone — is neither, so `navigator.clipboard`
|
|
7
|
+
* is `undefined` there and touching `.writeText` throws outright. That is the
|
|
8
|
+
* normal deployment, not an edge case, which is why this falls back rather than
|
|
9
|
+
* feature-detecting into a disabled button.
|
|
10
|
+
*
|
|
11
|
+
* The fallback is `document.execCommand('copy')` over an off-screen textarea.
|
|
12
|
+
* It is deprecated and it is also the only thing that works here; every browser
|
|
13
|
+
* still implements it. Returns whether the text actually landed, so a caller can
|
|
14
|
+
* avoid claiming success it did not have.
|
|
15
|
+
*/
|
|
16
|
+
export async function copyText(value: string): Promise<boolean> {
|
|
17
|
+
// Optional-chained, not `in`-checked: on an insecure origin the property is
|
|
18
|
+
// absent entirely, and some embedded webviews expose a `clipboard` object
|
|
19
|
+
// whose `writeText` rejects. Both end up in the fallback.
|
|
20
|
+
try {
|
|
21
|
+
if (navigator.clipboard?.writeText) {
|
|
22
|
+
await navigator.clipboard.writeText(value)
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Permission denied, or a webview that lied about having the API.
|
|
27
|
+
}
|
|
28
|
+
return legacyCopy(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function legacyCopy(value: string): boolean {
|
|
32
|
+
if (typeof document === 'undefined') return false
|
|
33
|
+
const textarea = document.createElement('textarea')
|
|
34
|
+
textarea.value = value
|
|
35
|
+
// Off-screen rather than hidden: `display: none` and `visibility: hidden`
|
|
36
|
+
// elements cannot hold a selection, so the copy would silently do nothing.
|
|
37
|
+
// `readOnly` keeps the mobile keyboard from appearing for the instant it exists.
|
|
38
|
+
textarea.setAttribute('readonly', '')
|
|
39
|
+
textarea.style.position = 'fixed'
|
|
40
|
+
textarea.style.top = '-9999px'
|
|
41
|
+
textarea.style.opacity = '0'
|
|
42
|
+
document.body.appendChild(textarea)
|
|
43
|
+
// Preserve where the user was: selecting steals the current selection, and on
|
|
44
|
+
// a text field mid-edit that is visible and annoying.
|
|
45
|
+
const previous = document.activeElement
|
|
46
|
+
try {
|
|
47
|
+
textarea.select()
|
|
48
|
+
textarea.setSelectionRange(0, value.length)
|
|
49
|
+
return document.execCommand('copy')
|
|
50
|
+
} catch {
|
|
51
|
+
return false
|
|
52
|
+
} finally {
|
|
53
|
+
document.body.removeChild(textarea)
|
|
54
|
+
if (previous instanceof HTMLElement) previous.focus()
|
|
55
|
+
}
|
|
56
|
+
}
|