@workerdeck/ui 0.9.0 → 0.12.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 (65) hide show
  1. package/README.md +81 -3
  2. package/build/SessionPanel-Dy9lQrOV.d.mts +319 -0
  3. package/build/SessionPanel-NQ8ksCfj.mjs +8474 -0
  4. package/build/SessionPanel-NQ8ksCfj.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 +66 -0
  9. package/build/format.mjs +119 -0
  10. package/build/format.mjs.map +1 -0
  11. package/build/index.d.mts +671 -88
  12. package/build/index.mjs +387 -5160
  13. package/build/index.mjs.map +1 -1
  14. package/build/workspace.d.mts +226 -0
  15. package/build/workspace.mjs +861 -0
  16. package/build/workspace.mjs.map +1 -0
  17. package/package.json +22 -4
  18. package/src/components/agent/CodeEditor.tsx +300 -0
  19. package/src/components/agent/Composer.tsx +522 -87
  20. package/src/components/agent/ContextDialog.tsx +99 -0
  21. package/src/components/agent/Conversation.tsx +11 -3
  22. package/src/components/agent/EditorTabs.tsx +165 -0
  23. package/src/components/agent/FileCard.tsx +26 -0
  24. package/src/components/agent/FileTree.tsx +287 -0
  25. package/src/components/agent/FileViewer.tsx +148 -0
  26. package/src/components/agent/HostFilesDialog.tsx +218 -0
  27. package/src/components/agent/Loader.tsx +82 -14
  28. package/src/components/agent/McpDialog.tsx +363 -0
  29. package/src/components/agent/Message.tsx +51 -17
  30. package/src/components/agent/ModelSelect.tsx +34 -6
  31. package/src/components/agent/PermissionModeSelect.tsx +133 -22
  32. package/src/components/agent/PermissionPrompt.tsx +164 -6
  33. package/src/components/agent/PromptTokenText.tsx +39 -0
  34. package/src/components/agent/QuestionPrompt.tsx +122 -0
  35. package/src/components/agent/Reasoning.tsx +20 -5
  36. package/src/components/agent/Response.tsx +128 -0
  37. package/src/components/agent/SessionBrowser.tsx +428 -0
  38. package/src/components/agent/SessionEmptyState.tsx +65 -0
  39. package/src/components/agent/SessionInfoDialog.tsx +163 -0
  40. package/src/components/agent/SessionPanel.tsx +783 -91
  41. package/src/components/agent/SessionWorkspace.tsx +317 -0
  42. package/src/components/agent/SkillsDialog.tsx +195 -0
  43. package/src/components/agent/StatusBar.tsx +85 -18
  44. package/src/components/agent/ToolCallCard.tsx +252 -30
  45. package/src/components/agent/Transcript.tsx +513 -30
  46. package/src/components/agent/UsageDialog.tsx +168 -0
  47. package/src/components/agent/line-prompt.tsx +249 -0
  48. package/src/components/agent/pulse.tsx +60 -0
  49. package/src/components/agent/transcript-variant.tsx +123 -0
  50. package/src/components/prompt-area/prompt-area-engine.ts +53 -0
  51. package/src/components/prompt-area/types.ts +15 -0
  52. package/src/components/prompt-area/use-prompt-area.ts +20 -0
  53. package/src/components/ui/CodeBlock.tsx +40 -2
  54. package/src/components/ui/CopyButton.tsx +28 -3
  55. package/src/components/ui/Dialog.tsx +92 -0
  56. package/src/components/ui/Menu.tsx +55 -0
  57. package/src/components/ui/Splitter.tsx +133 -0
  58. package/src/components/ui/Tooltip.tsx +22 -5
  59. package/src/format.ts +11 -0
  60. package/src/index.ts +67 -2
  61. package/src/lib/clipboard.ts +56 -0
  62. package/src/lib/format.ts +114 -0
  63. package/src/lib/status.ts +124 -0
  64. package/src/lib/tool-icon.ts +96 -0
  65. package/src/workspace.ts +28 -0
@@ -17,6 +17,7 @@ import {
17
17
  plainTextToSegments,
18
18
  segmentsEqual,
19
19
  resolveChip,
20
+ resolveText,
20
21
  removeChipAtIndex,
21
22
  revertChipAtIndex,
22
23
  replaceTextRange,
@@ -929,6 +930,25 @@ export function usePromptArea({
929
930
  if (!activeTrigger) return
930
931
 
931
932
  const segments = readSegmentsFromDOM()
933
+
934
+ // Text-resolved suggestions leave before any chip machinery runs: there
935
+ // is no chip, so there is nothing for the chip-click editing path or the
936
+ // onChipAdd callback below to be about.
937
+ const asText = activeTrigger.config.insertAsText?.(suggestion)
938
+ if (asText !== undefined) {
939
+ const inserted = resolveText(segments, activeTrigger, asText)
940
+ events.pushUndo(segments)
941
+ onChange(inserted.segments)
942
+ renderSegmentsToDOM(inserted.segments)
943
+ const editor = editorRef.current
944
+ if (editor) setCursorAtOffset(editor, inserted.cursorOffset)
945
+ dismissTrigger()
946
+ setTimeout(() => {
947
+ editorRef.current?.focus()
948
+ }, 0)
949
+ return
950
+ }
951
+
932
952
  const displayText = activeTrigger.config.onSelect?.(suggestion) ?? suggestion.label
933
953
 
934
954
  const chipData = {
@@ -7,17 +7,55 @@ export interface CodeBlockProps {
7
7
  /** Header label, e.g. a language or "Parameters". */
8
8
  label?: ReactNode
9
9
  copyable?: boolean
10
+ /**
11
+ * `'panel'` (default) is the framed card: border, header strip, rounded.
12
+ * `'plain'` is the terminal treatment — the label is a dim line, the code a
13
+ * flat band, and copy is a character. For surfaces where a box around every
14
+ * payload is more chrome than content (the transcript's `lines` variant).
15
+ *
16
+ * A prop rather than a read of the transcript's variant context: this is a
17
+ * `ui/` primitive and has no business knowing what a transcript is. The agent
18
+ * component that renders it does.
19
+ */
20
+ variant?: 'panel' | 'plain'
10
21
  className?: string
11
22
  }
12
23
 
13
24
  /** Plain (unhighlighted) code panel for structured data like tool inputs. Markdown code
14
25
  * inside assistant responses is highlighted by <Response> instead. */
15
- export function CodeBlock({ code, label, copyable = true, className }: CodeBlockProps) {
26
+ export function CodeBlock({
27
+ code,
28
+ label,
29
+ copyable = true,
30
+ variant = 'panel',
31
+ className,
32
+ }: CodeBlockProps) {
33
+ const plain = variant === 'plain'
34
+ const header = label !== undefined || copyable
35
+
36
+ if (plain) {
37
+ return (
38
+ <div data-slot='code-block' data-variant='plain' className={cn('min-w-0', className)}>
39
+ {header ? (
40
+ <div className='flex items-baseline justify-between gap-2'>
41
+ <span className='truncate text-label leading-5 text-fg-4'>{label}</span>
42
+ {copyable ? (
43
+ <CopyButton glyph value={code} className='size-5 shrink-0 rounded-sm' />
44
+ ) : null}
45
+ </div>
46
+ ) : null}
47
+ <pre className='max-h-64 overflow-auto bg-code-bg px-2 font-mono text-label leading-5 whitespace-pre-wrap text-fg-2'>
48
+ {code}
49
+ </pre>
50
+ </div>
51
+ )
52
+ }
53
+
16
54
  return (
17
55
  <div
18
56
  data-slot='code-block'
19
57
  className={cn('overflow-hidden rounded-md border border-border bg-code-bg', className)}>
20
- {label !== undefined || copyable ? (
58
+ {header ? (
21
59
  <div className='flex h-8 items-center justify-between border-b border-border px-2.5'>
22
60
  <span className='font-mono text-label text-fg-3'>{label}</span>
23
61
  {copyable ? <CopyButton value={code} /> : null}
@@ -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,11 @@
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'
11
+ export * from './lib/status.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,14 @@ 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
+ TranscriptDensityProvider,
87
+ TranscriptVariantProvider,
88
+ useTranscriptDensity,
89
+ useTranscriptVariant,
90
+ type TranscriptDensity,
91
+ type TranscriptVariant,
92
+ } from './components/agent/transcript-variant.tsx'
59
93
  export { Response, type ResponseProps } from './components/agent/Response.tsx'
60
94
  export { Reasoning, type ReasoningProps } from './components/agent/Reasoning.tsx'
61
95
  export { Loader } from './components/agent/Loader.tsx'
@@ -69,31 +103,62 @@ export {
69
103
  type QuestionPromptProps,
70
104
  type QuestionBehaviorMeta,
71
105
  } from './components/agent/QuestionPrompt.tsx'
72
- export { Composer, type ComposerProps } from './components/agent/Composer.tsx'
106
+ export {
107
+ Composer,
108
+ skillPrompt,
109
+ type ComposerFileMatch,
110
+ type ComposerHandle,
111
+ type ComposerProps,
112
+ } from './components/agent/Composer.tsx'
73
113
  export { ModelSelect, type ModelSelectProps } from './components/agent/ModelSelect.tsx'
74
114
  export {
75
115
  PERMISSION_MODES,
76
116
  PermissionModeSelect,
117
+ permissionModeChoices,
118
+ permissionModeMeta,
119
+ type PermissionModeChoice,
77
120
  type PermissionModeMeta,
78
121
  type PermissionModeSelectProps,
79
122
  } from './components/agent/PermissionModeSelect.tsx'
80
123
  export { StatusBar, type StatusBarProps } from './components/agent/StatusBar.tsx'
124
+ export { ContextDialog, type ContextDialogProps } from './components/agent/ContextDialog.tsx'
125
+ export { UsageDialog, type UsageDialogProps } from './components/agent/UsageDialog.tsx'
126
+ export {
127
+ SessionInfoDialog,
128
+ type SessionInfoDialogProps,
129
+ } from './components/agent/SessionInfoDialog.tsx'
130
+ export { McpDialog, type McpDialogProps } from './components/agent/McpDialog.tsx'
131
+ export { SkillsDialog, type SkillsDialogProps } from './components/agent/SkillsDialog.tsx'
132
+ export { HostFilesDialog, type HostFilesDialogProps } from './components/agent/HostFilesDialog.tsx'
81
133
  export {
82
134
  SessionList,
83
135
  SessionListItem,
84
136
  type SessionListItemProps,
85
137
  type SessionListProps,
86
138
  } from './components/agent/SessionList.tsx'
139
+ export { SessionBrowser, type SessionBrowserProps } from './components/agent/SessionBrowser.tsx'
140
+ export {
141
+ SessionEmptyState,
142
+ type SessionEmptyStateProps,
143
+ } from './components/agent/SessionEmptyState.tsx'
144
+ export { PromptTokenText } from './components/agent/PromptTokenText.tsx'
87
145
  export { STATUS_META } from './components/agent/status.ts'
88
146
 
89
147
  // Utilities
90
148
  export { cn } from './lib/utils.ts'
149
+ export { copyText } from './lib/clipboard.ts'
150
+ export { isMutatingTool, toolIcon } from './lib/tool-icon.ts'
91
151
  export {
152
+ formatAgoPrecise,
92
153
  formatBytes,
93
154
  formatCost,
94
155
  formatCountdown,
95
156
  formatDuration,
157
+ formatRateLimitWindow,
158
+ formatRateLimitWindowLong,
96
159
  formatRelativeTime,
97
160
  formatTokens,
161
+ friendlyModel,
162
+ rateLimitWindowSeconds,
98
163
  toolInputPreview,
99
164
  } 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
+ }