@workerdeck/ui 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/{SessionPanel-13l25ubU.d.mts → SessionPanel-BobynUo4.d.mts} +61 -1
- package/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DUt2VzXG.mjs} +50 -12
- package/build/{SessionPanel-DgwjH4Ve.mjs.map → SessionPanel-DUt2VzXG.mjs.map} +1 -1
- package/build/index.d.mts +1 -1
- package/build/index.mjs +1 -1
- package/build/scoped.css +65 -1
- package/build/workspace.d.mts +7 -1
- package/build/workspace.mjs +4 -2
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/Response.tsx +4 -0
- package/src/components/agent/SessionPanel.tsx +112 -9
- package/src/components/agent/SessionWorkspace.tsx +8 -0
- package/src/components/agent/StatusBar.tsx +2 -2
- package/src/styles/theme.css +54 -1
|
@@ -416,6 +416,62 @@ export interface SessionPanelProps {
|
|
|
416
416
|
* "`>_` Tell the agent what to do." placeholder.
|
|
417
417
|
*/
|
|
418
418
|
emptyState?: ReactNode
|
|
419
|
+
/**
|
|
420
|
+
* Called when a link in the transcript is clicked. The embedder decides what
|
|
421
|
+
* happens: navigate in-app, open a browser tab, show a confirmation, or
|
|
422
|
+
* suppress.
|
|
423
|
+
*
|
|
424
|
+
* Return `true` (or a truthy value) to indicate the click was handled — the
|
|
425
|
+
* default action (`window.open(href, '_blank')`) is suppressed. Return
|
|
426
|
+
* `false` / `undefined` / nothing to let the browser open the link normally.
|
|
427
|
+
*
|
|
428
|
+
* Absent means "browser default" — links open in a new tab as Streamdown's
|
|
429
|
+
* `target="_blank"` intends. VS Code's webview overrides this through its own
|
|
430
|
+
* native handler (the "allow once / add to allowlist" dialog) and does not
|
|
431
|
+
* need this prop.
|
|
432
|
+
*
|
|
433
|
+
* **Typical embedder patterns:**
|
|
434
|
+
* - Relative URLs → in-app navigation, no confirmation
|
|
435
|
+
* - External URLs → confirmation dialog, or open unconditionally
|
|
436
|
+
* - Suppress all links → `() => true`
|
|
437
|
+
*/
|
|
438
|
+
onLinkClick?: (href: string) => boolean | void
|
|
439
|
+
/**
|
|
440
|
+
* Client-side tool handlers. Each key is a tool name the model can call; the
|
|
441
|
+
* handler receives the model's input and returns a result. The tool's
|
|
442
|
+
* **schema** must be registered server-side (via `tools` on
|
|
443
|
+
* `ProviderRunnerOptions`), but the handler runs here — right where the data
|
|
444
|
+
* the tool needs lives.
|
|
445
|
+
*
|
|
446
|
+
* Shorthand for `toolHost.clientTools`; when both are set, this wins for
|
|
447
|
+
* overlapping names.
|
|
448
|
+
*
|
|
449
|
+
* ```tsx
|
|
450
|
+
* <SessionPanel
|
|
451
|
+
* clientTools={{
|
|
452
|
+
* app_navigate: async (input) => {
|
|
453
|
+
* router.push((input as { path: string }).path)
|
|
454
|
+
* return { value: 'navigated' }
|
|
455
|
+
* },
|
|
456
|
+
* }}
|
|
457
|
+
* />
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
clientTools?: Record<string, import('@workerdeck/react').ClientToolHandler>
|
|
461
|
+
/**
|
|
462
|
+
* Base font size in **whole pixels**. Drives the overall scale of everything
|
|
463
|
+
* the panel draws — prompt, output, markdown, status bar — in both variants.
|
|
464
|
+
*
|
|
465
|
+
* Under the terminal theme it sets `--term-font-size` and derives
|
|
466
|
+
* `--term-line` at the CLI's own 13 : 18 ratio (unless {@link terminalMetrics}
|
|
467
|
+
* overrides those individually). Under cards it sets the panel root's
|
|
468
|
+
* `font-size`, which scales every `rem`/`em`-based token the type scale uses.
|
|
469
|
+
*
|
|
470
|
+
* Absent means "platform default": 13 px for the terminal theme, the
|
|
471
|
+
* inherited body size for cards. That is the right choice for a host that has
|
|
472
|
+
* no preference — the panel reads at the size the rest of the app does.
|
|
473
|
+
*/
|
|
474
|
+
fontSize?: number
|
|
419
475
|
className?: string
|
|
420
476
|
}
|
|
421
477
|
|
|
@@ -529,10 +585,23 @@ export function SessionPanel({
|
|
|
529
585
|
unseen,
|
|
530
586
|
readOnly = false,
|
|
531
587
|
toolHost,
|
|
588
|
+
clientTools,
|
|
532
589
|
cacheTranscript,
|
|
533
590
|
emptyState,
|
|
591
|
+
onLinkClick,
|
|
592
|
+
fontSize,
|
|
534
593
|
className,
|
|
535
594
|
}: SessionPanelProps) {
|
|
595
|
+
// ── Font size resolution ──────────────────────────────────────────────
|
|
596
|
+
// `fontSize` is the panel-wide knob; `terminalMetrics` is the terminal-
|
|
597
|
+
// specific override that predates it. The two compose: `fontSize` provides
|
|
598
|
+
// the default the terminal derives from (13 : 18 ratio), and individual
|
|
599
|
+
// terminalMetrics fields can still override each axis.
|
|
600
|
+
const effectiveTermFontSize = terminalMetrics?.fontSize ?? fontSize
|
|
601
|
+
const effectiveTermLineHeight =
|
|
602
|
+
terminalMetrics?.lineHeight ??
|
|
603
|
+
(fontSize !== undefined ? Math.round(fontSize * (18 / 13)) : undefined)
|
|
604
|
+
|
|
536
605
|
const external = panelSurface === 'external'
|
|
537
606
|
const statusExternal = statusSurface === 'external'
|
|
538
607
|
// Both non-internal surfaces take the pickers out of the composer, which is
|
|
@@ -714,7 +783,14 @@ export function SessionPanel({
|
|
|
714
783
|
// SAME handle the panel attached with — the bridge asks the first attached
|
|
715
784
|
// client. Free for Claude sessions: the guest loads lazily on the first call,
|
|
716
785
|
// which for them never comes.
|
|
717
|
-
useToolCallHost(
|
|
786
|
+
useToolCallHost(
|
|
787
|
+
handle,
|
|
788
|
+
toolHost === false
|
|
789
|
+
? { enabled: false }
|
|
790
|
+
: clientTools
|
|
791
|
+
? { ...toolHost, clientTools: { ...toolHost?.clientTools, ...clientTools } }
|
|
792
|
+
: toolHost,
|
|
793
|
+
)
|
|
718
794
|
const terminal = transcriptVariant === 'terminal'
|
|
719
795
|
|
|
720
796
|
// What the strip reads. The frame's items and its spawning call both come from
|
|
@@ -1012,6 +1088,31 @@ export function SessionPanel({
|
|
|
1012
1088
|
/>
|
|
1013
1089
|
)
|
|
1014
1090
|
|
|
1091
|
+
// ── Link click handler ────────────────────────────────────────────────
|
|
1092
|
+
// When onLinkClick is provided, intercept <a> clicks on the panel root so
|
|
1093
|
+
// the embedder controls navigation. Capture phase so it fires before any
|
|
1094
|
+
// default handling. Only installed when the prop is present — a host that
|
|
1095
|
+
// does not provide it (VS Code, the dashboard) gets the browser / webview
|
|
1096
|
+
// default, which is the right thing in both cases.
|
|
1097
|
+
const panelRef = useRef<HTMLDivElement>(null)
|
|
1098
|
+
useEffect(() => {
|
|
1099
|
+
if (!onLinkClick) return
|
|
1100
|
+
const el = panelRef.current
|
|
1101
|
+
if (!el) return
|
|
1102
|
+
const handler = (e: MouseEvent) => {
|
|
1103
|
+
const anchor = (e.target as HTMLElement)?.closest?.('a[href]') as HTMLAnchorElement | null
|
|
1104
|
+
if (!anchor) return
|
|
1105
|
+
const href = anchor.getAttribute('href')
|
|
1106
|
+
if (!href) return
|
|
1107
|
+
if (onLinkClick(href)) {
|
|
1108
|
+
e.preventDefault()
|
|
1109
|
+
e.stopPropagation()
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
el.addEventListener('click', handler, true)
|
|
1113
|
+
return () => el.removeEventListener('click', handler, true)
|
|
1114
|
+
}, [onLinkClick])
|
|
1115
|
+
|
|
1015
1116
|
// Dead-space clicks land in the composer. Anything the user actually aimed at
|
|
1016
1117
|
// — a control, a link, the end of a drag-selection — keeps its own meaning;
|
|
1017
1118
|
// this only claims what was left over.
|
|
@@ -1036,13 +1137,15 @@ export function SessionPanel({
|
|
|
1036
1137
|
<ToolResultFetchProvider value={loadFullResult}>
|
|
1037
1138
|
<ToolResultImageProvider value={resultImages}>
|
|
1038
1139
|
<div
|
|
1140
|
+
ref={panelRef}
|
|
1039
1141
|
data-slot='session-panel'
|
|
1040
1142
|
// The typeface is a cascade fact, not a React one — one attribute here,
|
|
1041
1143
|
// and the `[data-agent-font]` rule in theme.css does the rest. Nothing
|
|
1042
1144
|
// outside this subtree can pick it up.
|
|
1043
1145
|
data-agent-font={transcriptFont}
|
|
1044
1146
|
onClick={handleClick}
|
|
1045
|
-
className={cn('flex h-full min-h-0 flex-col overflow-hidden bg-bg', className)}
|
|
1147
|
+
className={cn('flex h-full min-h-0 flex-col overflow-hidden bg-bg', className)}
|
|
1148
|
+
style={fontSize !== undefined ? { '--wd-font-size': `${Math.round(fontSize)}px` } as React.CSSProperties : undefined}>
|
|
1046
1149
|
{headerTakesActions ? header({ actions: menu }) : header}
|
|
1047
1150
|
{statusPlacement === 'top' ? statusBar : null}
|
|
1048
1151
|
{protocolMismatch !== undefined ? (
|
|
@@ -1067,8 +1170,8 @@ export function SessionPanel({
|
|
|
1067
1170
|
label={subagentFallbackLabel}
|
|
1068
1171
|
onBack={leaveSubagent}
|
|
1069
1172
|
terminal={terminal}
|
|
1070
|
-
fontSize={
|
|
1071
|
-
lineHeight={
|
|
1173
|
+
fontSize={effectiveTermFontSize}
|
|
1174
|
+
lineHeight={effectiveTermLineHeight}
|
|
1072
1175
|
/>
|
|
1073
1176
|
) : null}
|
|
1074
1177
|
<Transcript
|
|
@@ -1084,8 +1187,8 @@ export function SessionPanel({
|
|
|
1084
1187
|
hostImage={hostImage}
|
|
1085
1188
|
variant={transcriptVariant}
|
|
1086
1189
|
density={transcriptDensity}
|
|
1087
|
-
fontSize={
|
|
1088
|
-
lineHeight={
|
|
1190
|
+
fontSize={effectiveTermFontSize}
|
|
1191
|
+
lineHeight={effectiveTermLineHeight}
|
|
1089
1192
|
affordances={affordances}
|
|
1090
1193
|
stickyPrompt={stickyPrompt}
|
|
1091
1194
|
scrubber={scrubber}
|
|
@@ -1147,7 +1250,7 @@ export function SessionPanel({
|
|
|
1147
1250
|
session is actually driven. */}
|
|
1148
1251
|
{!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
|
|
1149
1252
|
<div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
|
|
1150
|
-
<PromptSurface terminal={terminal} metrics={
|
|
1253
|
+
<PromptSurface terminal={terminal} metrics={{ fontSize: effectiveTermFontSize, lineHeight: effectiveTermLineHeight }} affordances={affordances}>
|
|
1151
1254
|
{state.pendingApprovals.map((request) => {
|
|
1152
1255
|
const isQuestion =
|
|
1153
1256
|
request.toolName === 'AskUserQuestion' &&
|
|
@@ -1215,8 +1318,8 @@ export function SessionPanel({
|
|
|
1215
1318
|
}
|
|
1216
1319
|
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
1217
1320
|
toolbar={controlsExternal ? undefined : sessionControls}
|
|
1218
|
-
fontSize={
|
|
1219
|
-
lineHeight={
|
|
1321
|
+
fontSize={effectiveTermFontSize}
|
|
1322
|
+
lineHeight={effectiveTermLineHeight}
|
|
1220
1323
|
affordances={affordances}
|
|
1221
1324
|
/>
|
|
1222
1325
|
)}
|
|
@@ -53,6 +53,10 @@ export interface SessionWorkspaceProps {
|
|
|
53
53
|
/** Which end of the panel the status bar sits at — see `SessionPanel`. */
|
|
54
54
|
statusPlacement?: SessionPanelProps['statusPlacement']
|
|
55
55
|
controlsSurface?: SessionPanelProps['controlsSurface']
|
|
56
|
+
/** Base font size in whole pixels — see `SessionPanel.fontSize`. */
|
|
57
|
+
fontSize?: SessionPanelProps['fontSize']
|
|
58
|
+
/** Link click handler — see `SessionPanel.onLinkClick`. */
|
|
59
|
+
onLinkClick?: SessionPanelProps['onLinkClick']
|
|
56
60
|
unseen?: SessionPanelProps['unseen']
|
|
57
61
|
/** Viewer mode — no composer, no approval prompts. Forwarded verbatim to
|
|
58
62
|
* {@link SessionPanel}; the file tree and editor are unaffected, since reading
|
|
@@ -117,6 +121,8 @@ export function SessionWorkspace({
|
|
|
117
121
|
onSubagentChange,
|
|
118
122
|
statusPlacement,
|
|
119
123
|
controlsSurface,
|
|
124
|
+
fontSize,
|
|
125
|
+
onLinkClick,
|
|
120
126
|
unseen,
|
|
121
127
|
readOnly,
|
|
122
128
|
onVitals,
|
|
@@ -323,6 +329,8 @@ export function SessionWorkspace({
|
|
|
323
329
|
reveal={reveal}
|
|
324
330
|
onSubagentChange={onSubagentChange}
|
|
325
331
|
controlsSurface={controlsSurface}
|
|
332
|
+
fontSize={fontSize}
|
|
333
|
+
onLinkClick={onLinkClick}
|
|
326
334
|
statusPlacement={statusPlacement}
|
|
327
335
|
unseen={unseen}
|
|
328
336
|
readOnly={readOnly}
|
|
@@ -262,10 +262,10 @@ export function StatusBar({
|
|
|
262
262
|
</span>
|
|
263
263
|
</Slot>
|
|
264
264
|
) : null}
|
|
265
|
-
{controls}
|
|
265
|
+
{controls ? <span className='self-center'>{controls}</span> : null}
|
|
266
266
|
<span className='flex-1' />
|
|
267
267
|
<span className='font-mono text-label text-fg-3'>{formatCost(state.totalCostUsd)}</span>
|
|
268
|
-
{actions}
|
|
268
|
+
{actions ? <span className='self-center'>{actions}</span> : null}
|
|
269
269
|
</div>
|
|
270
270
|
)
|
|
271
271
|
}
|
package/src/styles/theme.css
CHANGED
|
@@ -485,6 +485,59 @@
|
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
/* ── Panel font size & Streamdown overrides ──────────────────────────────
|
|
489
|
+
*
|
|
490
|
+
* OUTSIDE `@layer base` so these beat Tailwind utility classes in the cascade
|
|
491
|
+
* (utilities live in a layer; unlayered rules always win over layered ones).
|
|
492
|
+
*
|
|
493
|
+
* `--wd-font-size` is set on the session panel root by the `fontSize` prop.
|
|
494
|
+
* When present it drives all text in the cards variant — body text, headings,
|
|
495
|
+
* code, tables — through one value. When absent the fallback `var(--text-body-sm)`
|
|
496
|
+
* resolves to the same `0.8125rem` the utility class used to set.
|
|
497
|
+
*
|
|
498
|
+
* Streamdown ships `rem`-based heading sizes (text-3xl … text-sm) that resolve
|
|
499
|
+
* against the document root and ignore the panel's local font-size. The `em`-based
|
|
500
|
+
* overrides below scale with `--wd-font-size`. The specificity confines this to
|
|
501
|
+
* cards-mode assistant output; the terminal theme renders headings as weight-only
|
|
502
|
+
* (no size change) through its own components and is unaffected.
|
|
503
|
+
*
|
|
504
|
+
* The heading scale mirrors Streamdown's hierarchy but at a tighter range — its
|
|
505
|
+
* 30 → 14 px span (h1 → h6) sits inside a 13px body, so the ratio is condensed:
|
|
506
|
+
* h1 at 1.5em, descending to h6 at 1em.
|
|
507
|
+
*/
|
|
508
|
+
[data-slot='message-content'] {
|
|
509
|
+
font-size: var(--wd-font-size, var(--text-body-sm));
|
|
510
|
+
line-height: 1.6;
|
|
511
|
+
}
|
|
512
|
+
[data-slot='message-content'] h1 { font-size: 1.5em; line-height: 1.35; margin-top: 1.5em; }
|
|
513
|
+
[data-slot='message-content'] h2 { font-size: 1.3em; line-height: 1.35; margin-top: 1.5em; }
|
|
514
|
+
[data-slot='message-content'] h3 { font-size: 1.15em; line-height: 1.4; margin-top: 1.25em; }
|
|
515
|
+
[data-slot='message-content'] h4 { font-size: 1.05em; line-height: 1.4; margin-top: 1.25em; }
|
|
516
|
+
[data-slot='message-content'] h5 { font-size: 1em; line-height: 1.4; margin-top: 1em; }
|
|
517
|
+
[data-slot='message-content'] h6 { font-size: 1em; line-height: 1.4; margin-top: 1em; }
|
|
518
|
+
[data-slot='message-content'] pre { font-size: 0.9em; }
|
|
519
|
+
[data-slot='message-content'] code { font-size: 0.9em; }
|
|
520
|
+
[data-slot='message-content'] pre code { font-size: inherit; }
|
|
521
|
+
/* Table, cells, and headers: Streamdown puts `text-sm` (rem) on td/th directly,
|
|
522
|
+
so overriding the table alone is not enough — the cell class wins. */
|
|
523
|
+
[data-slot='message-content'] table { font-size: 0.95em; }
|
|
524
|
+
[data-slot='message-content'] td { font-size: inherit; }
|
|
525
|
+
[data-slot='message-content'] th { font-size: inherit; }
|
|
526
|
+
/* Code block body wrapper: Streamdown puts `text-sm` on the wrapper div around
|
|
527
|
+
the <pre>; override so it inherits the 0.9em from the pre rule above. */
|
|
528
|
+
[data-slot='message-content'] [data-streamdown='code-block-body'] { font-size: 0.9em; }
|
|
529
|
+
/* Inline code: Streamdown puts `text-sm` directly on the <code>. */
|
|
530
|
+
[data-slot='message-content'] :not(pre) > code { font-size: 0.9em; }
|
|
531
|
+
/* The composer's prompt area and its chips inherit the panel's font size so the
|
|
532
|
+
prompt and the output read at the same scale. The two-selector specificity
|
|
533
|
+
beats the utility class on the element. */
|
|
534
|
+
[data-slot='session-panel'] .prompt-area-container {
|
|
535
|
+
font-size: var(--wd-font-size, var(--text-body-sm));
|
|
536
|
+
}
|
|
537
|
+
[data-slot='session-panel'] .prompt-area-chip {
|
|
538
|
+
font-size: var(--wd-font-size, 0.8125rem);
|
|
539
|
+
}
|
|
540
|
+
|
|
488
541
|
/* The inset frame's gradient "shine" edge (app shells). Do NOT add bg-* on top. */
|
|
489
542
|
.frame-shine {
|
|
490
543
|
border: 1px solid transparent;
|
|
@@ -672,7 +725,7 @@
|
|
|
672
725
|
overflow: hidden;
|
|
673
726
|
white-space: nowrap;
|
|
674
727
|
text-overflow: ellipsis;
|
|
675
|
-
font-size: var(--text-body-sm);
|
|
728
|
+
font-size: var(--wd-font-size, var(--text-body-sm));
|
|
676
729
|
color: var(--fg-2);
|
|
677
730
|
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
678
731
|
backdrop-filter: blur(12px);
|