@workerdeck/ui 0.20.0 → 0.22.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 (43) hide show
  1. package/README.md +3 -2
  2. package/build/{SessionPanel-CnU_IJ3-.d.mts → SessionPanel-13l25ubU.d.mts} +55 -7
  3. package/build/{SessionPanel-CHktI2CF.mjs → SessionPanel-DgwjH4Ve.mjs} +642 -231
  4. package/build/SessionPanel-DgwjH4Ve.mjs.map +1 -0
  5. package/build/index.d.mts +309 -53
  6. package/build/index.mjs +534 -397
  7. package/build/index.mjs.map +1 -1
  8. package/build/scoped.css +3547 -0
  9. package/build/workspace.d.mts +10 -1
  10. package/build/workspace.mjs +4 -2
  11. package/build/workspace.mjs.map +1 -1
  12. package/package.json +15 -7
  13. package/src/components/agent/Composer.tsx +10 -10
  14. package/src/components/agent/ContextDialog.tsx +3 -2
  15. package/src/components/agent/Conversation.tsx +1 -1
  16. package/src/components/agent/McpDialog.tsx +3 -1
  17. package/src/components/agent/Scrubber.tsx +248 -0
  18. package/src/components/agent/SessionBrowser.tsx +113 -293
  19. package/src/components/agent/SessionItem.tsx +538 -0
  20. package/src/components/agent/SessionList.tsx +3 -1
  21. package/src/components/agent/SessionPanel.tsx +112 -21
  22. package/src/components/agent/SessionStatusIcon.tsx +43 -0
  23. package/src/components/agent/SessionSteps.tsx +118 -61
  24. package/src/components/agent/SessionWorkspace.tsx +11 -0
  25. package/src/components/agent/SkillsDialog.tsx +3 -2
  26. package/src/components/agent/StatusBar.tsx +1 -1
  27. package/src/components/agent/Transcript.tsx +147 -73
  28. package/src/components/agent/UsageDialog.tsx +3 -1
  29. package/src/components/agent/scrubber-marks.ts +277 -0
  30. package/src/components/terminal/PermissionPrompt.tsx +3 -0
  31. package/src/components/terminal/QuestionPrompt.tsx +3 -0
  32. package/src/components/terminal/StatusLine.tsx +3 -1
  33. package/src/components/ui/AlertDialog.tsx +23 -20
  34. package/src/components/ui/Dialog.tsx +26 -23
  35. package/src/components/ui/Menu.tsx +20 -17
  36. package/src/components/ui/PortalScope.tsx +27 -0
  37. package/src/components/ui/Select.tsx +19 -16
  38. package/src/components/ui/Tooltip.tsx +13 -10
  39. package/src/index.ts +5 -1
  40. package/src/styles/scoped.entry.css +16 -0
  41. package/src/styles/terminal.css +1 -1
  42. package/src/styles/theme.css +223 -8
  43. package/build/SessionPanel-CHktI2CF.mjs.map +0 -1
@@ -27,12 +27,14 @@ export interface TerminalPermissionPromptProps {
27
27
  /** `message` is fed back to the agent, which can then try something else;
28
28
  * `interrupt` also stops the turn. */
29
29
  onDeny: (requestId: string, message?: string, interrupt?: boolean) => void
30
+ className?: string
30
31
  }
31
32
 
32
33
  export function TerminalPermissionPrompt({
33
34
  request,
34
35
  onApprove,
35
36
  onDeny,
37
+ className,
36
38
  }: TerminalPermissionPromptProps) {
37
39
  const [focused, setFocused] = useState(0)
38
40
  const [denying, setDenying] = useState(false)
@@ -75,6 +77,7 @@ export function TerminalPermissionPrompt({
75
77
  return (
76
78
  <div
77
79
  data-slot='permission-prompt'
80
+ className={className}
78
81
  onKeyDown={(event) => {
79
82
  if (event.key === 'Escape') {
80
83
  event.preventDefault()
@@ -40,12 +40,14 @@ export interface TerminalQuestionPromptProps {
40
40
  onAnswer: (requestId: string, updatedInput: Record<string, unknown>) => void
41
41
  /** Deny the tool — the model proceeds without an answer. */
42
42
  onDismiss: (requestId: string, message?: string) => void
43
+ className?: string
43
44
  }
44
45
 
45
46
  export function TerminalQuestionPrompt({
46
47
  request,
47
48
  onAnswer,
48
49
  onDismiss,
50
+ className,
49
51
  }: TerminalQuestionPromptProps) {
50
52
  const questions = parseUserQuestions(request.input)
51
53
  const [selections, setSelections] = useState<Selection[]>(() => questions.map(() => EMPTY))
@@ -97,6 +99,7 @@ export function TerminalQuestionPrompt({
97
99
  return (
98
100
  <div
99
101
  data-slot='question-prompt'
102
+ className={className}
100
103
  onKeyDown={(event) => {
101
104
  if (event.key === 'Escape') {
102
105
  event.preventDefault()
@@ -38,6 +38,7 @@ const SEVERITY_TONE: Record<StatusSeverity, Tone> = {
38
38
  }
39
39
 
40
40
  export interface TerminalStatusLineProps {
41
+ className?: string
41
42
  state: TranscriptState
42
43
  /** Plan windows to read from, when they should not be the session's own — the
43
44
  * gateway's per-profile state merged over this transcript's. See
@@ -75,6 +76,7 @@ function Reading({
75
76
  }
76
77
 
77
78
  export function TerminalStatusLine({
79
+ className,
78
80
  state,
79
81
  rateLimits,
80
82
  connection,
@@ -147,7 +149,7 @@ export function TerminalStatusLine({
147
149
  }
148
150
 
149
151
  return (
150
- <Row data-slot='status-line' tone='dim'>
152
+ <Row data-slot='status-line' tone='dim' className={className}>
151
153
  {parts.map((part, index) => (
152
154
  <span key={index}>
153
155
  {index > 0 ? <Ink tone='faint'> · </Ink> : null}
@@ -1,6 +1,7 @@
1
1
  import { type FunctionComponent } from 'react'
2
2
  import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'
3
3
  import { cn } from '../../lib/utils.ts'
4
+ import { PortalScope } from './PortalScope.tsx'
4
5
 
5
6
  export const AlertDialog = AlertDialogPrimitive.Root
6
7
  export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
@@ -12,26 +13,28 @@ export const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Pr
12
13
  ...props
13
14
  }) => (
14
15
  <AlertDialogPrimitive.Portal>
15
- <AlertDialogPrimitive.Backdrop
16
- className={cn(
17
- 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
18
- 'transition-opacity duration-(--motion-base)',
19
- 'data-starting-style:opacity-0 data-ending-style:opacity-0',
20
- )}
21
- />
22
- <AlertDialogPrimitive.Popup
23
- data-slot='alert-dialog-content'
24
- className={cn(
25
- 'fixed top-1/2 left-1/2 z-70 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2',
26
- 'rounded-lg border border-border bg-surface p-5 shadow-(--shadow-lg) outline-none',
27
- 'transition-[opacity,transform] duration-(--motion-base)',
28
- 'data-starting-style:scale-95 data-starting-style:opacity-0',
29
- 'data-ending-style:scale-95 data-ending-style:opacity-0',
30
- className,
31
- )}
32
- {...props}>
33
- {children}
34
- </AlertDialogPrimitive.Popup>
16
+ <PortalScope>
17
+ <AlertDialogPrimitive.Backdrop
18
+ className={cn(
19
+ 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
20
+ 'transition-opacity duration-(--motion-base)',
21
+ 'data-starting-style:opacity-0 data-ending-style:opacity-0',
22
+ )}
23
+ />
24
+ <AlertDialogPrimitive.Popup
25
+ data-slot='alert-dialog-content'
26
+ className={cn(
27
+ 'fixed top-1/2 left-1/2 z-70 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2',
28
+ 'rounded-lg border border-border bg-surface p-5 shadow-(--shadow-lg) outline-none',
29
+ 'transition-[opacity,transform] duration-(--motion-base)',
30
+ 'data-starting-style:scale-95 data-starting-style:opacity-0',
31
+ 'data-ending-style:scale-95 data-ending-style:opacity-0',
32
+ className,
33
+ )}
34
+ {...props}>
35
+ {children}
36
+ </AlertDialogPrimitive.Popup>
37
+ </PortalScope>
35
38
  </AlertDialogPrimitive.Portal>
36
39
  )
37
40
 
@@ -2,6 +2,7 @@ import { type FunctionComponent } from 'react'
2
2
  import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
3
3
  import { X } from 'lucide-react'
4
4
  import { cn } from '../../lib/utils.ts'
5
+ import { PortalScope } from './PortalScope.tsx'
5
6
 
6
7
  export const Dialog = DialogPrimitive.Root
7
8
  export const DialogTrigger = DialogPrimitive.Trigger
@@ -18,29 +19,31 @@ export const DialogContent: FunctionComponent<
18
19
  DialogPrimitive.Popup.Props & { size?: 'sm' | 'md' | 'lg' }
19
20
  > = ({ className, children, size = 'md', ...props }) => (
20
21
  <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>
22
+ <PortalScope>
23
+ <DialogPrimitive.Backdrop
24
+ className={cn(
25
+ 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
26
+ 'transition-opacity duration-(--motion-base)',
27
+ 'data-starting-style:opacity-0 data-ending-style:opacity-0',
28
+ )}
29
+ />
30
+ <DialogPrimitive.Popup
31
+ data-slot='dialog-content'
32
+ className={cn(
33
+ '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',
34
+ size === 'sm' && 'w-[min(24rem,calc(100vw-2rem))]',
35
+ size === 'md' && 'w-[min(32rem,calc(100vw-2rem))]',
36
+ size === 'lg' && 'w-[min(46rem,calc(100vw-2rem))]',
37
+ 'rounded-lg border border-border bg-surface shadow-(--shadow-lg) outline-none',
38
+ 'transition-[opacity,transform] duration-(--motion-base)',
39
+ 'data-starting-style:scale-95 data-starting-style:opacity-0',
40
+ 'data-ending-style:scale-95 data-ending-style:opacity-0',
41
+ className,
42
+ )}
43
+ {...props}>
44
+ {children}
45
+ </DialogPrimitive.Popup>
46
+ </PortalScope>
44
47
  </DialogPrimitive.Portal>
45
48
  )
46
49
 
@@ -1,6 +1,7 @@
1
1
  import { type FunctionComponent } from 'react'
2
2
  import { Menu as MenuPrimitive } from '@base-ui/react/menu'
3
3
  import { cn } from '../../lib/utils.ts'
4
+ import { PortalScope } from './PortalScope.tsx'
4
5
 
5
6
  export const Menu = MenuPrimitive.Root
6
7
  export const MenuTrigger = MenuPrimitive.Trigger
@@ -10,23 +11,25 @@ export const MenuContent: FunctionComponent<
10
11
  Pick<MenuPrimitive.Positioner.Props, 'align' | 'side' | 'sideOffset'>
11
12
  > = ({ className, align = 'end', side = 'bottom', sideOffset = 6, ...props }) => (
12
13
  <MenuPrimitive.Portal>
13
- <MenuPrimitive.Positioner
14
- align={align}
15
- side={side}
16
- sideOffset={sideOffset}
17
- className='isolate z-80 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>
14
+ <PortalScope>
15
+ <MenuPrimitive.Positioner
16
+ align={align}
17
+ side={side}
18
+ sideOffset={sideOffset}
19
+ className='isolate z-80 outline-none'>
20
+ <MenuPrimitive.Popup
21
+ data-slot='menu-content'
22
+ className={cn(
23
+ 'min-w-48 rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
24
+ 'transition-[opacity,transform] duration-(--motion-base)',
25
+ 'data-starting-style:scale-95 data-starting-style:opacity-0',
26
+ 'data-ending-style:scale-95 data-ending-style:opacity-0',
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ </MenuPrimitive.Positioner>
32
+ </PortalScope>
30
33
  </MenuPrimitive.Portal>
31
34
  )
32
35
 
@@ -0,0 +1,27 @@
1
+ import { type FunctionComponent, type ReactNode } from 'react'
2
+
3
+ /**
4
+ * Re-establishes the `.wd-root` styling scope inside a portal.
5
+ *
6
+ * The scoped stylesheet (`@workerdeck/ui/scoped.css`) rewrites every rule to
7
+ * live under `.wd-root`, so an embedder's page keeps its own design system and
8
+ * the panel keeps this one. Base UI popups (Menu, Dialog, AlertDialog, Select,
9
+ * Tooltip) portal to `document.body` — OUTSIDE the embedder's wrapper — so
10
+ * without this they would render unstyled, or worse, styled by the host.
11
+ *
12
+ * `display: contents` (inline, so no stylesheet needs to load for it) makes the
13
+ * element generate no box: positioning, hit-testing and the popup's own layout
14
+ * are untouched. It still carries the class, which is all the scope needs —
15
+ * design tokens declared on `.wd-root` inherit through it, and every
16
+ * `.wd-root <x>` rule matches below it. Theme keeps working because the dark
17
+ * token block matches `[data-theme='dark']` on ANY ancestor, and `document.body`
18
+ * sits under the host's themed `<html>` just like the panel does.
19
+ *
20
+ * Under the classic `theme.css` integration the class matches no rule and the
21
+ * element is inert, so the wrapper is unconditional.
22
+ */
23
+ export const PortalScope: FunctionComponent<{ children?: ReactNode }> = ({ children }) => (
24
+ <div className='wd-root' style={{ display: 'contents' }}>
25
+ {children}
26
+ </div>
27
+ )
@@ -2,6 +2,7 @@ import { type FunctionComponent } from 'react'
2
2
  import { Select as SelectPrimitive } from '@base-ui/react/select'
3
3
  import { Check, ChevronsUpDown } from 'lucide-react'
4
4
  import { cn } from '../../lib/utils.ts'
5
+ import { PortalScope } from './PortalScope.tsx'
5
6
 
6
7
  export const Select = SelectPrimitive.Root
7
8
  export const SelectValue = SelectPrimitive.Value
@@ -40,22 +41,24 @@ export const SelectContent: FunctionComponent<
40
41
  ...props
41
42
  }) => (
42
43
  <SelectPrimitive.Portal>
43
- <SelectPrimitive.Positioner
44
- align={align}
45
- alignItemWithTrigger={alignItemWithTrigger}
46
- side={side}
47
- sideOffset={sideOffset}
48
- className='isolate z-80 outline-none'>
49
- <SelectPrimitive.Popup
50
- data-slot='select-content'
51
- className={cn(
52
- 'max-h-[min(24rem,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto',
53
- 'rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
54
- className,
55
- )}
56
- {...props}
57
- />
58
- </SelectPrimitive.Positioner>
44
+ <PortalScope>
45
+ <SelectPrimitive.Positioner
46
+ align={align}
47
+ alignItemWithTrigger={alignItemWithTrigger}
48
+ side={side}
49
+ sideOffset={sideOffset}
50
+ className='isolate z-80 outline-none'>
51
+ <SelectPrimitive.Popup
52
+ data-slot='select-content'
53
+ className={cn(
54
+ 'max-h-[min(24rem,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto',
55
+ 'rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ </SelectPrimitive.Positioner>
61
+ </PortalScope>
59
62
  </SelectPrimitive.Portal>
60
63
  )
61
64
 
@@ -1,6 +1,7 @@
1
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
+ import { PortalScope } from './PortalScope.tsx'
4
5
 
5
6
  export const TooltipProvider = TooltipPrimitive.Provider
6
7
 
@@ -8,16 +9,18 @@ export const TooltipContent: FunctionComponent<
8
9
  TooltipPrimitive.Popup.Props & Pick<TooltipPrimitive.Positioner.Props, 'side' | 'sideOffset'>
9
10
  > = ({ className, side = 'top', sideOffset = 6, ...props }) => (
10
11
  <TooltipPrimitive.Portal>
11
- <TooltipPrimitive.Positioner side={side} sideOffset={sideOffset} className='isolate z-90'>
12
- <TooltipPrimitive.Popup
13
- data-slot='tooltip-content'
14
- className={cn(
15
- 'rounded-md border border-border bg-surface px-2 py-1 text-label text-fg-2 shadow-(--shadow-md) outline-none',
16
- className,
17
- )}
18
- {...props}
19
- />
20
- </TooltipPrimitive.Positioner>
12
+ <PortalScope>
13
+ <TooltipPrimitive.Positioner side={side} sideOffset={sideOffset} className='isolate z-90'>
14
+ <TooltipPrimitive.Popup
15
+ data-slot='tooltip-content'
16
+ className={cn(
17
+ 'rounded-md border border-border bg-surface px-2 py-1 text-label text-fg-2 shadow-(--shadow-md) outline-none',
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ </TooltipPrimitive.Positioner>
23
+ </PortalScope>
21
24
  </TooltipPrimitive.Portal>
22
25
  )
23
26
 
package/src/index.ts CHANGED
@@ -177,7 +177,11 @@ export {
177
177
  rowShapeClass,
178
178
  type SessionBrowserProps,
179
179
  } from './components/agent/SessionBrowser.tsx'
180
- export { SessionStatusIcon } from './components/agent/SessionBrowser.tsx'
180
+ // The session card itself — one component, every client. `SessionBrowser` is the
181
+ // dashboard's list *around* it; a host that wants only the card (the VS Code
182
+ // sidebar does, because the filtering chrome above it is native) takes this.
183
+ export { SessionItem, type SessionItemProps } from './components/agent/SessionItem.tsx'
184
+ export { SessionStatusIcon } from './components/agent/SessionStatusIcon.tsx'
181
185
  // Lifted out of the VS Code sidebar once the dashboard grew a collapsed rail
182
186
  // that needs the same glyph — two copies of a trademark set is one too many.
183
187
  export {
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Build entry for the SCOPED stylesheet (`@workerdeck/ui/scoped.css`) — see
3
+ * `scripts/build-scoped-css.mjs`, which compiles this with the Tailwind CLI and
4
+ * then rewrites every selector under `.wd-root`. Not shipped as-is and not an
5
+ * export: embedders import the compiled `scoped.css` artifact.
6
+ *
7
+ * `source(none)` + explicit `@source`: the utilities compiled here are exactly
8
+ * the ones this package's own components (and streamdown, whose markdown
9
+ * renderer ships Tailwind-classed markup) use — hermetic, independent of any
10
+ * host app's Tailwind build.
11
+ */
12
+ @import 'tailwindcss' source(none);
13
+ @source '../components';
14
+ @source '../lib';
15
+ @source '../../node_modules/streamdown/dist';
16
+ @import './theme.css';
@@ -595,7 +595,7 @@
595
595
  [data-terminal] .term-composer-body {
596
596
  margin-inline: auto;
597
597
  width: 100%;
598
- max-width: var(--wd-content-max-w, 48rem);
598
+ max-width: var(--wd-transcript-max-width);
599
599
  padding-inline: var(--term-bleed, 0px);
600
600
  /* Air on each side of the prompt, and a plain pixel value rather than a
601
601
  fraction of `--term-line`: this is chrome between two rules, not a