@workerdeck/ui 0.9.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.
Files changed (61) hide show
  1. package/README.md +81 -3
  2. package/build/SessionPanel-CyhygZx_.d.mts +277 -0
  3. package/build/SessionPanel-_U8tjX29.mjs +8409 -0
  4. package/build/SessionPanel-_U8tjX29.mjs.map +1 -0
  5. package/build/format-DqR56Y8l.mjs +162 -0
  6. package/build/format-DqR56Y8l.mjs.map +1 -0
  7. package/build/format-ljc3lKpA.d.mts +59 -0
  8. package/build/format.d.mts +2 -0
  9. package/build/format.mjs +2 -0
  10. package/build/index.d.mts +610 -87
  11. package/build/index.mjs +6 -5104
  12. package/build/index.mjs.map +1 -1
  13. package/build/workspace.d.mts +199 -0
  14. package/build/workspace.mjs +849 -0
  15. package/build/workspace.mjs.map +1 -0
  16. package/package.json +22 -4
  17. package/src/components/agent/CodeEditor.tsx +300 -0
  18. package/src/components/agent/Composer.tsx +522 -87
  19. package/src/components/agent/ContextDialog.tsx +99 -0
  20. package/src/components/agent/Conversation.tsx +11 -3
  21. package/src/components/agent/EditorTabs.tsx +165 -0
  22. package/src/components/agent/FileCard.tsx +26 -0
  23. package/src/components/agent/FileTree.tsx +287 -0
  24. package/src/components/agent/FileViewer.tsx +148 -0
  25. package/src/components/agent/HostFilesDialog.tsx +218 -0
  26. package/src/components/agent/Loader.tsx +120 -14
  27. package/src/components/agent/McpDialog.tsx +363 -0
  28. package/src/components/agent/Message.tsx +51 -17
  29. package/src/components/agent/ModelSelect.tsx +34 -6
  30. package/src/components/agent/PermissionModeSelect.tsx +133 -22
  31. package/src/components/agent/PermissionPrompt.tsx +164 -6
  32. package/src/components/agent/PromptTokenText.tsx +39 -0
  33. package/src/components/agent/QuestionPrompt.tsx +122 -0
  34. package/src/components/agent/Reasoning.tsx +20 -5
  35. package/src/components/agent/Response.tsx +128 -0
  36. package/src/components/agent/SessionEmptyState.tsx +65 -0
  37. package/src/components/agent/SessionInfoDialog.tsx +163 -0
  38. package/src/components/agent/SessionPanel.tsx +756 -91
  39. package/src/components/agent/SessionWorkspace.tsx +282 -0
  40. package/src/components/agent/SkillsDialog.tsx +195 -0
  41. package/src/components/agent/StatusBar.tsx +85 -18
  42. package/src/components/agent/ToolCallCard.tsx +243 -30
  43. package/src/components/agent/Transcript.tsx +495 -30
  44. package/src/components/agent/UsageDialog.tsx +168 -0
  45. package/src/components/agent/line-prompt.tsx +249 -0
  46. package/src/components/agent/transcript-variant.tsx +61 -0
  47. package/src/components/prompt-area/prompt-area-engine.ts +53 -0
  48. package/src/components/prompt-area/types.ts +15 -0
  49. package/src/components/prompt-area/use-prompt-area.ts +20 -0
  50. package/src/components/ui/CodeBlock.tsx +40 -2
  51. package/src/components/ui/CopyButton.tsx +28 -3
  52. package/src/components/ui/Dialog.tsx +92 -0
  53. package/src/components/ui/Menu.tsx +55 -0
  54. package/src/components/ui/Splitter.tsx +133 -0
  55. package/src/components/ui/Tooltip.tsx +22 -5
  56. package/src/format.ts +10 -0
  57. package/src/index.ts +63 -2
  58. package/src/lib/clipboard.ts +56 -0
  59. package/src/lib/format.ts +114 -0
  60. package/src/lib/tool-icon.ts +96 -0
  61. 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({ value, className, variant = 'ghost', size = 'icon-sm', ...props }: CopyButtonProps) {
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
- void navigator.clipboard.writeText(value).then(() => {
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
- {copied ? <Check className='size-3.5 text-success' /> : <Copy className='size-3.5' />}
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
- /** Convenience wrapper: <Tip content="..."><Button/></Tip> */
25
- export function Tip({ content, children }: { content: ReactNode; children: ReactNode }) {
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 { SessionPanel, type SessionPanelProps } from './components/agent/SessionPanel.tsx'
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 { Composer, type ComposerProps } from './components/agent/Composer.tsx'
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
+ }