@runtypelabs/persona 4.23.2 → 4.25.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
- package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +278 -37
- package/dist/approval-ui.d.ts +278 -37
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
- package/dist/artifacts-ui.d.cts +273 -25
- package/dist/artifacts-ui.d.ts +273 -25
- package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +13 -13
- package/dist/context-mentions.d.cts +273 -25
- package/dist/context-mentions.d.ts +273 -25
- package/dist/event-stream-view.d.cts +273 -25
- package/dist/event-stream-view.d.ts +273 -25
- package/dist/forms-ui.d.cts +319 -27
- package/dist/forms-ui.d.ts +319 -27
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +344 -39
- package/dist/index.d.ts +344 -39
- package/dist/index.global.js +57 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +273 -25
- package/dist/session-reconnect.d.ts +273 -25
- package/dist/smart-dom-reader.d.cts +273 -25
- package/dist/smart-dom-reader.d.ts +273 -25
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +280 -25
- package/dist/theme-editor-preview.d.ts +280 -25
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +293 -27
- package/dist/theme-editor.d.ts +293 -27
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1522 -11
- package/dist/theme-reference.d.ts +1522 -11
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime-7DA7MJH4.js +1 -0
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +33 -0
- package/dist/voice-runtime.d.ts +33 -0
- package/dist/voice-runtime.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +3 -2
- package/src/__fixtures__/defaults-parity.v4.json +776 -0
- package/src/approval-ui-entry.ts +0 -1
- package/src/approval-ui-loader.ts +5 -4
- package/src/approval-ui.ts +0 -1
- package/src/client-identity-proof.test.ts +193 -0
- package/src/client-session-warm.test.ts +216 -0
- package/src/client-visitor-history.test.ts +6 -0
- package/src/client.ts +122 -30
- package/src/codegen.test.ts +24 -0
- package/src/components/activity-row.test.ts +100 -0
- package/src/components/activity-row.ts +136 -0
- package/src/components/approval-actions.test.ts +8 -5
- package/src/components/approval-actions.ts +19 -6
- package/src/components/approval-bubble.test.ts +56 -9
- package/src/components/approval-bubble.ts +36 -41
- package/src/components/composer-builder.ts +5 -2
- package/src/components/composer-parts.ts +10 -2
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +7 -4
- package/src/components/header-layouts.test.ts +12 -0
- package/src/components/header-layouts.ts +3 -2
- package/src/components/launcher.ts +11 -2
- package/src/components/message-bubble.test.ts +22 -0
- package/src/components/message-bubble.ts +7 -3
- package/src/components/panel.test.ts +52 -1
- package/src/components/panel.ts +47 -8
- package/src/components/pill-composer-builder.test.ts +2 -0
- package/src/components/pill-composer-builder.ts +0 -1
- package/src/components/reasoning-bubble.ts +29 -17
- package/src/components/suggestions.test.ts +1 -0
- package/src/components/suggestions.ts +0 -16
- package/src/components/tool-bubble.ts +131 -114
- package/src/components/tool-details.test.ts +87 -0
- package/src/components/tool-details.ts +102 -0
- package/src/components/welcome.test.ts +8 -0
- package/src/components/welcome.ts +4 -2
- package/src/defaults-parity.test.ts +158 -0
- package/src/defaults.test.ts +69 -0
- package/src/defaults.ts +128 -49
- package/src/generated/runtype-openapi-contract.ts +2 -2
- package/src/index-core.ts +4 -0
- package/src/install.test.ts +24 -0
- package/src/install.ts +3 -1
- package/src/launcher-global.ts +2 -1
- package/src/launcher-variants.test.ts +52 -0
- package/src/runtime/init-update-reset.test.ts +21 -0
- package/src/session.ts +59 -7
- package/src/session.voice.test.ts +72 -1
- package/src/styles/widget-styles.test.ts +19 -0
- package/src/styles/widget.css +295 -23
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/preview-utils.test.ts +8 -0
- package/src/theme-editor/preview-utils.ts +7 -3
- package/src/theme-editor/sections.test.ts +31 -0
- package/src/theme-editor/sections.ts +55 -1
- package/src/theme-editor/state-preferences.test.ts +49 -0
- package/src/theme-editor/state.ts +73 -15
- package/src/theme-reference.test.ts +20 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +33 -1
- package/src/types/theme.ts +80 -3
- package/src/types.ts +207 -22
- package/src/ui.activity-row.test.ts +91 -0
- package/src/ui.approval-plugin.test.ts +63 -0
- package/src/ui.composer-compact.test.ts +24 -2
- package/src/ui.composer-placement.test.ts +23 -0
- package/src/ui.detached-panel.test.ts +33 -6
- package/src/ui.docked.test.ts +19 -0
- package/src/ui.header-update-stability.test.ts +25 -0
- package/src/ui.identity-proof.test.ts +58 -0
- package/src/ui.launcher-teaser.test.ts +25 -0
- package/src/ui.message-width.test.ts +17 -2
- package/src/ui.reasoning-display.test.ts +92 -0
- package/src/ui.rebuild-composer.test.ts +20 -3
- package/src/ui.scroll.test.ts +32 -0
- package/src/ui.session-init.test.ts +312 -0
- package/src/ui.status-indicator.test.ts +82 -0
- package/src/ui.suggest-replies.test.ts +11 -0
- package/src/ui.ts +422 -74
- package/src/ui.v5-defaults.test.ts +144 -0
- package/src/ui.voice-prewarm.test.ts +222 -0
- package/src/ui.welcome.test.ts +92 -3
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
- package/src/utils/code-generators.ts +14 -0
- package/src/utils/composer-compact.test.ts +0 -2
- package/src/utils/composer-compact.ts +2 -4
- package/src/utils/composer-placement.ts +2 -3
- package/src/utils/config-merge.test.ts +81 -5
- package/src/utils/config-merge.ts +13 -3
- package/src/utils/defaults-provenance.ts +67 -0
- package/src/utils/dock.ts +7 -1
- package/src/utils/launcher-variant.ts +16 -0
- package/src/utils/panel-config.ts +97 -0
- package/src/utils/status-indicator.ts +39 -0
- package/src/utils/theme.test.ts +182 -0
- package/src/utils/theme.ts +131 -18
- package/src/utils/tokens.test.ts +122 -1
- package/src/utils/tokens.ts +177 -17
- package/src/voice/runtype-voice-provider.ts +238 -27
- package/src/voice/voice.test.ts +272 -2
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- package/dist/approval-ui-K7R5Q5OC.js +0 -1
- package/dist/voice-runtime-EFR3R5ES.js +0 -1
package/src/utils/tokens.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CIRCLE_LAUNCHER_TOKENS } from "./launcher-variant";
|
|
1
2
|
import type {
|
|
2
3
|
DeepPartial,
|
|
3
4
|
PersonaTheme,
|
|
@@ -9,8 +10,8 @@ import type {
|
|
|
9
10
|
SemanticTokens,
|
|
10
11
|
} from '../types/theme';
|
|
11
12
|
import {
|
|
12
|
-
DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH,
|
|
13
13
|
DEFAULT_FLOATING_LAUNCHER_WIDTH,
|
|
14
|
+
resolveDefaultsVersion,
|
|
14
15
|
} from '../defaults';
|
|
15
16
|
|
|
16
17
|
// Detached/docked panel defaults, shared by the panel token defaults, the alias
|
|
@@ -25,7 +26,7 @@ const spacing = (() => {
|
|
|
25
26
|
return s as Record<0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64, string>;
|
|
26
27
|
})();
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
const DEFAULT_PALETTE_BASE = {
|
|
29
30
|
colors: {
|
|
30
31
|
primary: {
|
|
31
32
|
50: '#ffffff',
|
|
@@ -209,7 +210,7 @@ export const DEFAULT_PALETTE = {
|
|
|
209
210
|
},
|
|
210
211
|
};
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
const DEFAULT_SEMANTIC_BASE: SemanticTokens = {
|
|
213
214
|
colors: {
|
|
214
215
|
primary: 'palette.colors.primary.500',
|
|
215
216
|
secondary: 'palette.colors.secondary.500',
|
|
@@ -257,7 +258,7 @@ export const DEFAULT_SEMANTIC: SemanticTokens = {
|
|
|
257
258
|
},
|
|
258
259
|
};
|
|
259
260
|
|
|
260
|
-
|
|
261
|
+
const DEFAULT_COMPONENTS_BASE: ComponentTokens = {
|
|
261
262
|
button: {
|
|
262
263
|
primary: {
|
|
263
264
|
// Primary Actions role: solid primary
|
|
@@ -305,13 +306,31 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
305
306
|
},
|
|
306
307
|
panel: {
|
|
307
308
|
width: DEFAULT_FLOATING_LAUNCHER_WIDTH,
|
|
308
|
-
maxWidth
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
// Matches the existing floating runtime geometry. `maxWidth` and
|
|
310
|
+
// `maxHeight` intentionally do not impose a second cap; ui.ts applies
|
|
311
|
+
// the responsive width and heightOffset rules for this mode.
|
|
312
|
+
maxWidth: 'none',
|
|
313
|
+
height: 'min(640px, max(200px, calc(100vh - 64px)))',
|
|
314
|
+
maxHeight: 'none',
|
|
311
315
|
borderRadius: 'palette.radius.xl',
|
|
312
316
|
shadow: 'palette.shadows.xl',
|
|
313
317
|
inset: DEFAULT_PANEL_INSET,
|
|
314
318
|
canvasBackground: DEFAULT_PANEL_CANVAS_BACKGROUND,
|
|
319
|
+
modes: {
|
|
320
|
+
floating: {
|
|
321
|
+
border: '1px solid var(--persona-border)',
|
|
322
|
+
shadow: 'palette.shadows.xl',
|
|
323
|
+
borderRadius: 'palette.radius.xl',
|
|
324
|
+
},
|
|
325
|
+
inline: {
|
|
326
|
+
border: '1px solid var(--persona-border)',
|
|
327
|
+
shadow: 'none',
|
|
328
|
+
borderRadius: 'palette.radius.xl',
|
|
329
|
+
},
|
|
330
|
+
docked: { border: 'none', shadow: 'none', borderRadius: 'palette.radius.xl' },
|
|
331
|
+
sidebar: { border: 'none', borderRadius: '0' },
|
|
332
|
+
mobile: { border: 'none', shadow: 'none', borderRadius: '0' },
|
|
333
|
+
},
|
|
315
334
|
},
|
|
316
335
|
header: {
|
|
317
336
|
// Header role: solid primary. Border, subtitle, and action icons stay
|
|
@@ -319,11 +338,13 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
319
338
|
background: 'palette.colors.primary.500',
|
|
320
339
|
foreground: 'palette.colors.primary.50',
|
|
321
340
|
borderRadius: 'palette.radius.xl palette.radius.xl 0 0',
|
|
322
|
-
padding: '
|
|
341
|
+
padding: '20px 24px',
|
|
342
|
+
minimalPadding: '16px 24px',
|
|
323
343
|
iconBackground: 'palette.colors.primary.600',
|
|
324
344
|
iconForeground: 'palette.colors.primary.50',
|
|
325
345
|
},
|
|
326
346
|
message: {
|
|
347
|
+
gap: '12px',
|
|
327
348
|
user: {
|
|
328
349
|
// User Messages role: solid primary
|
|
329
350
|
background: 'palette.colors.primary.500',
|
|
@@ -532,6 +553,110 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
532
553
|
},
|
|
533
554
|
};
|
|
534
555
|
|
|
556
|
+
/** Shared defaults with a small opt-in V5 overlay. */
|
|
557
|
+
const DEFAULT_PALETTE_GRAY_V4 = DEFAULT_PALETTE_BASE.colors.gray;
|
|
558
|
+
const DEFAULT_PALETTE_GRAY_V5 = {
|
|
559
|
+
50: '#fafafa', 100: '#f5f5f5', 200: '#e5e5e5', 300: '#d4d4d4',
|
|
560
|
+
400: '#a3a3a3', 500: '#737373', 600: '#525252', 700: '#404040',
|
|
561
|
+
800: '#262626', 900: '#171717', 950: '#0a0a0a',
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
export const DEFAULT_PALETTE_V4 = {
|
|
565
|
+
...DEFAULT_PALETTE_BASE,
|
|
566
|
+
colors: { ...DEFAULT_PALETTE_BASE.colors, gray: DEFAULT_PALETTE_GRAY_V4 },
|
|
567
|
+
};
|
|
568
|
+
export const DEFAULT_PALETTE_V5 = {
|
|
569
|
+
...DEFAULT_PALETTE_BASE,
|
|
570
|
+
colors: { ...DEFAULT_PALETTE_BASE.colors, gray: DEFAULT_PALETTE_GRAY_V5 },
|
|
571
|
+
radius: { none: '0px', sm: '4px', md: '8px', lg: '12px', xl: '16px', '2xl': '24px', full: '9999px' },
|
|
572
|
+
};
|
|
573
|
+
export const DEFAULT_PALETTE = DEFAULT_PALETTE_V4;
|
|
574
|
+
|
|
575
|
+
export const DEFAULT_SEMANTIC_V4 = DEFAULT_SEMANTIC_BASE;
|
|
576
|
+
export const DEFAULT_SEMANTIC_V5: SemanticTokens = {
|
|
577
|
+
...DEFAULT_SEMANTIC_BASE,
|
|
578
|
+
colors: {
|
|
579
|
+
...DEFAULT_SEMANTIC_BASE.colors,
|
|
580
|
+
surface: '#ffffff', background: '#ffffff', container: 'semantic.colors.surface',
|
|
581
|
+
textInverse: '#ffffff',
|
|
582
|
+
},
|
|
583
|
+
typography: { ...DEFAULT_SEMANTIC_BASE.typography, fontSize: '14px', lineHeight: '1.5' },
|
|
584
|
+
};
|
|
585
|
+
export const DEFAULT_SEMANTIC = DEFAULT_SEMANTIC_V4;
|
|
586
|
+
|
|
587
|
+
export const DEFAULT_COMPONENTS_V4 = DEFAULT_COMPONENTS_BASE;
|
|
588
|
+
export const DEFAULT_COMPONENTS_V5: ComponentTokens = {
|
|
589
|
+
...DEFAULT_COMPONENTS_BASE,
|
|
590
|
+
launcher: CIRCLE_LAUNCHER_TOKENS,
|
|
591
|
+
activity: { rowHeight: "32px", labelSize: "13px", iconSize: "16px", indent: "0px", groupIndent: "0px", groupGap: "0px", groupPadding: "0px", transcriptGap: "0px", responseGap: "12px", bodyPadding: "4px 0", bodySize: "13px", bodyLineHeight: "1.5", bodyFontWeight: "400", sectionGap: "8px", bodySurface: "transparent" },
|
|
592
|
+
toolBubble: { shadow: "none" },
|
|
593
|
+
reasoningBubble: { shadow: "none" },
|
|
594
|
+
input: { ...DEFAULT_COMPONENTS_BASE.input, borderRadius: 'palette.radius.full' },
|
|
595
|
+
introCard: { ...DEFAULT_COMPONENTS_BASE.introCard, title: { fontSize: '22px', fontWeight: '500' } },
|
|
596
|
+
suggestion: {
|
|
597
|
+
chip: { ...DEFAULT_COMPONENTS_BASE.suggestion!.chip, fontWeight: '400' },
|
|
598
|
+
card: { ...DEFAULT_COMPONENTS_BASE.suggestion!.card, fontWeight: '400' },
|
|
599
|
+
list: { ...DEFAULT_COMPONENTS_BASE.suggestion!.list, fontWeight: '400' },
|
|
600
|
+
},
|
|
601
|
+
panel: {
|
|
602
|
+
...DEFAULT_COMPONENTS_BASE.panel,
|
|
603
|
+
width: 'min(400px, calc(100vw - 24px))',
|
|
604
|
+
height: 'min(704px, calc(100dvh - 104px))',
|
|
605
|
+
borderRadius: 'palette.radius.xl',
|
|
606
|
+
},
|
|
607
|
+
header: {
|
|
608
|
+
...DEFAULT_COMPONENTS_BASE.header,
|
|
609
|
+
background: 'semantic.colors.container', foreground: 'semantic.colors.text',
|
|
610
|
+
padding: '8px 8px 8px 16px', minimalPadding: '8px 8px 8px 16px',
|
|
611
|
+
minHeight: '48px', borderBottom: 'none',
|
|
612
|
+
iconBackground: 'transparent', iconForeground: 'semantic.colors.text', iconScale: '1',
|
|
613
|
+
controlSize: '28px', controlIconSize: '18px', controlStrokeWidth: '1.5',
|
|
614
|
+
actionIconForeground: 'semantic.colors.textMuted',
|
|
615
|
+
controlBorderRadius: '8px',
|
|
616
|
+
controlHoverBackground: 'color-mix(in srgb, var(--persona-text) 8%, transparent)',
|
|
617
|
+
controlHoverForeground: 'semantic.colors.text',
|
|
618
|
+
controlFocusOutline: '2px solid color-mix(in srgb, var(--persona-text) 50%, transparent)',
|
|
619
|
+
title: { fontSize: '14px', fontWeight: '500', lineHeight: '1.5' },
|
|
620
|
+
},
|
|
621
|
+
message: {
|
|
622
|
+
...DEFAULT_COMPONENTS_BASE.message,
|
|
623
|
+
gap: '20px', fullscreenGap: '28px', topFadeHeight: '18px',
|
|
624
|
+
user: {
|
|
625
|
+
...DEFAULT_COMPONENTS_BASE.message.user,
|
|
626
|
+
background: 'palette.colors.gray.100', text: 'semantic.colors.text',
|
|
627
|
+
borderRadius: 'palette.radius.xl', shadow: 'none', padding: '8px 14px',
|
|
628
|
+
fontSize: '14px', lineHeight: '1.5',
|
|
629
|
+
},
|
|
630
|
+
assistant: {
|
|
631
|
+
...DEFAULT_COMPONENTS_BASE.message.assistant,
|
|
632
|
+
background: 'transparent', border: 'transparent', borderWidth: '0px',
|
|
633
|
+
borderRadius: '0px', shadow: 'none', padding: '0px',
|
|
634
|
+
fontSize: '14px', lineHeight: '1.55',
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
composer: {
|
|
638
|
+
...DEFAULT_COMPONENTS_BASE.composer,
|
|
639
|
+
padding: '8px', fontSize: '15px', lineHeight: '1.5', controlSize: '32px', controlIconSize: '20px',
|
|
640
|
+
sendIconSize: '18px', sendButtonRadius: '9999px', footerBorder: 'none',
|
|
641
|
+
overlayBand: 'linear-gradient(to bottom, transparent, var(--persona-container) 24px)',
|
|
642
|
+
focusBorderColor: 'color-mix(in srgb, var(--persona-text) 28%, var(--persona-border))',
|
|
643
|
+
focusRing: '1px solid color-mix(in srgb, var(--persona-text) 12%, transparent)',
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
export const DEFAULT_COMPONENTS = DEFAULT_COMPONENTS_V4;
|
|
647
|
+
|
|
648
|
+
export type ThemeDefaultsVersion = 'v4' | 'v5';
|
|
649
|
+
|
|
650
|
+
export const resolveThemeDefaultsVersion = (future?: { v5Defaults?: boolean }): ThemeDefaultsVersion =>
|
|
651
|
+
resolveDefaultsVersion({ future });
|
|
652
|
+
|
|
653
|
+
export const resolveThemeDefaults = (future?: { v5Defaults?: boolean }) => {
|
|
654
|
+
const version = resolveThemeDefaultsVersion(future);
|
|
655
|
+
return version === 'v5'
|
|
656
|
+
? { palette: DEFAULT_PALETTE_V5, semantic: DEFAULT_SEMANTIC_V5, components: DEFAULT_COMPONENTS_V5 }
|
|
657
|
+
: { palette: DEFAULT_PALETTE_V4, semantic: DEFAULT_SEMANTIC_V4, components: DEFAULT_COMPONENTS_V4 };
|
|
658
|
+
};
|
|
659
|
+
|
|
535
660
|
export function resolveTokenValue(theme: PersonaTheme, path: string): string | undefined {
|
|
536
661
|
if (
|
|
537
662
|
!path.startsWith('palette.') &&
|
|
@@ -673,10 +798,11 @@ export function createTheme(
|
|
|
673
798
|
userConfig?: DeepPartial<PersonaTheme>,
|
|
674
799
|
options: CreateThemeOptions = {}
|
|
675
800
|
): PersonaTheme {
|
|
801
|
+
const defaults = resolveThemeDefaults(options.future);
|
|
676
802
|
const baseTheme: PersonaTheme = {
|
|
677
|
-
palette:
|
|
678
|
-
semantic:
|
|
679
|
-
components:
|
|
803
|
+
palette: defaults.palette as PersonaTheme['palette'],
|
|
804
|
+
semantic: defaults.semantic as PersonaTheme['semantic'],
|
|
805
|
+
components: defaults.components as PersonaTheme['components'],
|
|
680
806
|
};
|
|
681
807
|
|
|
682
808
|
let theme: PersonaTheme = {
|
|
@@ -738,6 +864,30 @@ export function createTheme(
|
|
|
738
864
|
),
|
|
739
865
|
} as PersonaTheme;
|
|
740
866
|
|
|
867
|
+
// A shared header padding is also the minimal-layout override unless the
|
|
868
|
+
// host supplies the more specific token. Both keys remain emitted CSS vars,
|
|
869
|
+
// so live updates and scheme changes restyle existing header DOM in place.
|
|
870
|
+
const activity = userConfig?.components?.activity;
|
|
871
|
+
for (const kind of ['toolBubble', 'reasoningBubble'] as const) {
|
|
872
|
+
for (const field of ['rowHeight', 'labelSize', 'iconSize', 'indent', 'bodySurface'] as const) {
|
|
873
|
+
if (activity?.[field] !== undefined) theme.components[kind][field] = activity[field];
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const legacySurface = userConfig?.components?.collapsibleWidget?.surface;
|
|
877
|
+
if (legacySurface !== undefined && activity?.bodySurface === undefined) {
|
|
878
|
+
theme.components.activity = { ...theme.components.activity, bodySurface: legacySurface };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const suppliedHeader = userConfig?.components?.header;
|
|
882
|
+
if (suppliedHeader?.padding !== undefined && suppliedHeader.minimalPadding === undefined) {
|
|
883
|
+
theme.components.header.minimalPadding = theme.components.header.padding;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const suppliedMessage = userConfig?.components?.message;
|
|
887
|
+
if (suppliedMessage?.gap !== undefined && suppliedMessage.fullscreenGap === undefined) {
|
|
888
|
+
theme.components.message.fullscreenGap = theme.components.message.gap;
|
|
889
|
+
}
|
|
890
|
+
|
|
741
891
|
if (options.validate !== false) {
|
|
742
892
|
const validation = validateTheme(theme);
|
|
743
893
|
if (!validation.valid) {
|
|
@@ -1224,12 +1374,21 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
1224
1374
|
]);
|
|
1225
1375
|
|
|
1226
1376
|
// Collapsible widget chrome (tool/reasoning/approval bubbles)
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1377
|
+
const collapsibleWidgetAliases = [
|
|
1378
|
+
['container', 'surface'],
|
|
1379
|
+
['surface', 'surface'],
|
|
1380
|
+
['border', 'border'],
|
|
1381
|
+
] as const;
|
|
1382
|
+
for (const [name, semanticFallback] of collapsibleWidgetAliases) {
|
|
1383
|
+
const value =
|
|
1384
|
+
cssVars[`--persona-components-collapsibleWidget-${name}`] ??
|
|
1385
|
+
cssVars[`--persona-${semanticFallback}`];
|
|
1386
|
+
if (value !== undefined) {
|
|
1387
|
+
cssVars[`--persona-cw-${name}`] = value;
|
|
1388
|
+
// Kept for custom styles that adopted the original unprefixed aliases.
|
|
1389
|
+
cssVars[`--cw-${name}`] = value;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1233
1392
|
|
|
1234
1393
|
emitAliases(cssVars, [
|
|
1235
1394
|
['message-border', 'components-message-border', 'border'],
|
|
@@ -1246,6 +1405,7 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
1246
1405
|
['message-user-font-family', 'components-message-user-fontFamily'],
|
|
1247
1406
|
['message-user-line-height', 'components-message-user-lineHeight'],
|
|
1248
1407
|
['message-assistant-padding', 'components-message-assistant-padding'],
|
|
1408
|
+
['message-assistant-border-width', 'components-message-assistant-borderWidth'],
|
|
1249
1409
|
['message-assistant-max-width', 'components-message-assistant-maxWidth'],
|
|
1250
1410
|
['message-assistant-font-size', 'components-message-assistant-fontSize'],
|
|
1251
1411
|
['message-assistant-font-family', 'components-message-assistant-fontFamily'],
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
// A continuous always-hot mic is, in UX terms, a permanent barge-in session, so
|
|
17
17
|
// `getInterruptionMode()` reports the constant `'barge-in'` and the existing
|
|
18
18
|
// mic-button wiring (ui.ts) treats a click as "hang up at any state" unchanged.
|
|
19
|
+
//
|
|
20
|
+
// Prewarm (`prewarm()`, called on mic intent): by default a fire-and-forget
|
|
21
|
+
// `POST /v1/client/agents/:agentId/voice/prewarm`. With `prewarmMode: 'attach'`
|
|
22
|
+
// the WebSocket opens early with `clientCapabilities=attach` and the extra
|
|
23
|
+
// `runtype.attach` subprotocol. If the server negotiates `runtype.attach`, the
|
|
24
|
+
// socket idles (no call) until `startListening()` sends `{"type":"start"}`; if
|
|
25
|
+
// it negotiates plain `runtype.bearer`, it ignored attach and the call is
|
|
26
|
+
// already live, so `startListening()` adopts it as is.
|
|
19
27
|
|
|
20
28
|
import type {
|
|
21
29
|
VoiceProvider,
|
|
@@ -37,6 +45,29 @@ const CAPTURE_BUFFER_SIZE = 4096;
|
|
|
37
45
|
*/
|
|
38
46
|
const LEVEL_RMS_SCALE = 4;
|
|
39
47
|
const RIFF_MAGIC = 0x52494646; // "RIFF"
|
|
48
|
+
/** `prewarm()` warms at most once per this window per provider instance. */
|
|
49
|
+
const PREWARM_THROTTLE_MS = 30_000;
|
|
50
|
+
/** How long `startListening()` waits on an attach still handshaking. */
|
|
51
|
+
const ATTACH_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
52
|
+
/** Attached-socket idle window: server default and clamp range. */
|
|
53
|
+
const ATTACH_IDLE_DEFAULT_MS = 30_000;
|
|
54
|
+
const ATTACH_IDLE_MIN_MS = 30_000;
|
|
55
|
+
const ATTACH_IDLE_MAX_MS = 600_000;
|
|
56
|
+
|
|
57
|
+
/** A socket opened by an `'attach'` prewarm, waiting for the click. */
|
|
58
|
+
type AttachedSocket = {
|
|
59
|
+
ws: WebSocket;
|
|
60
|
+
/**
|
|
61
|
+
* `connecting` until the handshake completes; then `attached` (no call yet,
|
|
62
|
+
* needs `start`) or `live` (the server ignored attach and the call is live).
|
|
63
|
+
*/
|
|
64
|
+
state: "connecting" | "attached" | "live";
|
|
65
|
+
/** Settles when the handshake completes, fails, or the socket is released. */
|
|
66
|
+
ready: Promise<void>;
|
|
67
|
+
settle: () => void;
|
|
68
|
+
openedAt: number;
|
|
69
|
+
idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
+
};
|
|
40
71
|
|
|
41
72
|
/**
|
|
42
73
|
* Strip the canonical 44-byte WAV header (if present) and return the raw PCM16
|
|
@@ -63,6 +94,16 @@ function toWsBase(host: string): string {
|
|
|
63
94
|
return `${secure ? "wss:" : "ws:"}//${trimmed}`;
|
|
64
95
|
}
|
|
65
96
|
|
|
97
|
+
/** Derive an http(s):// base URL from a configured host (the ws base, reversed). */
|
|
98
|
+
function toHttpBase(host: string): string {
|
|
99
|
+
return toWsBase(host).replace(/^ws/i, "http");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function clampAttachIdleMs(ms: number | undefined): number {
|
|
103
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return ATTACH_IDLE_DEFAULT_MS;
|
|
104
|
+
return Math.min(ATTACH_IDLE_MAX_MS, Math.max(ATTACH_IDLE_MIN_MS, Math.round(ms)));
|
|
105
|
+
}
|
|
106
|
+
|
|
66
107
|
export class RuntypeVoiceProvider implements VoiceProvider {
|
|
67
108
|
type: "runtype" = "runtype";
|
|
68
109
|
|
|
@@ -87,6 +128,10 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
87
128
|
// Distinguishes a user-initiated close (code 1000) from a dropped connection.
|
|
88
129
|
private intentionalClose = false;
|
|
89
130
|
|
|
131
|
+
// Prewarm latch and the (at most one) socket an `'attach'` prewarm opened.
|
|
132
|
+
private lastPrewarmAt = Number.NEGATIVE_INFINITY;
|
|
133
|
+
private attached: AttachedSocket | null = null;
|
|
134
|
+
|
|
90
135
|
private resultCallbacks: ((result: VoiceResult) => void)[] = [];
|
|
91
136
|
private errorCallbacks: ((error: Error) => void)[] = [];
|
|
92
137
|
private statusCallbacks: ((status: VoiceStatus) => void)[] = [];
|
|
@@ -104,6 +149,153 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
104
149
|
/** No-op: the WS session opens lazily in `startListening` (the "call"). */
|
|
105
150
|
async connect(): Promise<void> {}
|
|
106
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Warm the voice path ahead of the click. Fire-and-forget and throttled to
|
|
154
|
+
* once per {@link PREWARM_THROTTLE_MS}; never emits an error or a status.
|
|
155
|
+
*/
|
|
156
|
+
prewarm(): void {
|
|
157
|
+
if (this.callLive || this.attached) return;
|
|
158
|
+
const agentId = this.config?.agentId;
|
|
159
|
+
const token = this.config?.clientToken;
|
|
160
|
+
const host = this.config?.host;
|
|
161
|
+
if (!agentId || !token || !host) return;
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
if (now - this.lastPrewarmAt < PREWARM_THROTTLE_MS) return;
|
|
164
|
+
this.lastPrewarmAt = now;
|
|
165
|
+
try {
|
|
166
|
+
if (this.config?.prewarmMode === "attach") {
|
|
167
|
+
this.attach(host, agentId, token);
|
|
168
|
+
} else {
|
|
169
|
+
this.requestPrewarm(host, agentId, token);
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// A prewarm must never surface an error; the click connects normally.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private requestPrewarm(host: string, agentId: string, token: string): void {
|
|
177
|
+
if (typeof fetch !== "function") return;
|
|
178
|
+
const url = `${toHttpBase(host)}/v1/client/agents/${encodeURIComponent(agentId)}/voice/prewarm`;
|
|
179
|
+
void fetch(url, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
182
|
+
keepalive: true,
|
|
183
|
+
}).catch(() => {});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private attach(host: string, agentId: string, token: string): void {
|
|
187
|
+
const params = new URLSearchParams({ clientCapabilities: "attach" });
|
|
188
|
+
const configuredIdleMs = this.config?.attachIdleMs;
|
|
189
|
+
const idleMs = clampAttachIdleMs(configuredIdleMs);
|
|
190
|
+
if (configuredIdleMs !== undefined) params.set("attachIdleMs", String(idleMs));
|
|
191
|
+
const ws = new WebSocket(`${this.voiceSocketUrl(host, agentId)}?${params}`, [
|
|
192
|
+
"runtype.bearer",
|
|
193
|
+
"runtype.attach",
|
|
194
|
+
token,
|
|
195
|
+
]);
|
|
196
|
+
ws.binaryType = "arraybuffer";
|
|
197
|
+
let settle: () => void = () => {};
|
|
198
|
+
const ready = new Promise<void>((resolve) => {
|
|
199
|
+
settle = resolve;
|
|
200
|
+
});
|
|
201
|
+
const entry: AttachedSocket = {
|
|
202
|
+
ws,
|
|
203
|
+
state: "connecting",
|
|
204
|
+
ready,
|
|
205
|
+
settle,
|
|
206
|
+
openedAt: 0,
|
|
207
|
+
idleTimer: undefined,
|
|
208
|
+
};
|
|
209
|
+
this.attached = entry;
|
|
210
|
+
|
|
211
|
+
ws.onopen = () => {
|
|
212
|
+
if (this.attached !== entry) return;
|
|
213
|
+
entry.state = ws.protocol === "runtype.attach" ? "attached" : "live";
|
|
214
|
+
entry.openedAt = Date.now();
|
|
215
|
+
this.armAttachIdle(entry, idleMs);
|
|
216
|
+
if (entry.state === "attached") ws.send('{"type":"ping"}');
|
|
217
|
+
entry.settle();
|
|
218
|
+
};
|
|
219
|
+
ws.onmessage = (event) => {
|
|
220
|
+
if (this.attached !== entry || typeof event.data !== "string") return;
|
|
221
|
+
let msg: { type?: unknown; idleMs?: unknown };
|
|
222
|
+
try {
|
|
223
|
+
msg = JSON.parse(event.data);
|
|
224
|
+
} catch {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// The server's idle window is authoritative; close no later than it does.
|
|
228
|
+
if (msg.type === "attached" && typeof msg.idleMs === "number") {
|
|
229
|
+
this.armAttachIdle(entry, msg.idleMs - (Date.now() - entry.openedAt));
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
ws.onerror = () => {};
|
|
233
|
+
ws.onclose = () => {
|
|
234
|
+
if (this.attached === entry) this.releaseAttached();
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private armAttachIdle(entry: AttachedSocket, ms: number): void {
|
|
239
|
+
clearTimeout(entry.idleTimer);
|
|
240
|
+
entry.idleTimer = setTimeout(() => {
|
|
241
|
+
if (this.attached === entry) this.releaseAttached();
|
|
242
|
+
}, Math.max(0, ms));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Close and forget the prewarmed socket, if any. Silent by design. */
|
|
246
|
+
private releaseAttached(): void {
|
|
247
|
+
const entry = this.attached;
|
|
248
|
+
if (!entry) return;
|
|
249
|
+
this.attached = null;
|
|
250
|
+
clearTimeout(entry.idleTimer);
|
|
251
|
+
const { ws } = entry;
|
|
252
|
+
ws.onopen = null;
|
|
253
|
+
ws.onmessage = null;
|
|
254
|
+
ws.onerror = null;
|
|
255
|
+
ws.onclose = null;
|
|
256
|
+
entry.settle();
|
|
257
|
+
if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) {
|
|
258
|
+
try {
|
|
259
|
+
ws.close(1000, "prewarm released");
|
|
260
|
+
} catch {
|
|
261
|
+
// ignore
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Hand the prewarmed socket to the call, waiting up to
|
|
268
|
+
* {@link ATTACH_HANDSHAKE_TIMEOUT_MS} for a handshake still in flight.
|
|
269
|
+
* Resolves `null` when there is none, it stalled, or it dropped, so the
|
|
270
|
+
* caller opens a fresh socket.
|
|
271
|
+
*/
|
|
272
|
+
private async takeAttachedSocket(): Promise<{ ws: WebSocket; live: boolean } | null> {
|
|
273
|
+
const entry = this.attached;
|
|
274
|
+
if (!entry) return null;
|
|
275
|
+
if (entry.state === "connecting") {
|
|
276
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
277
|
+
await Promise.race([
|
|
278
|
+
entry.ready,
|
|
279
|
+
new Promise<void>((resolve) => {
|
|
280
|
+
timer = setTimeout(resolve, ATTACH_HANDSHAKE_TIMEOUT_MS);
|
|
281
|
+
}),
|
|
282
|
+
]);
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
}
|
|
285
|
+
if (this.attached !== entry) return null;
|
|
286
|
+
if (entry.state === "connecting" || entry.ws.readyState !== WebSocket.OPEN) {
|
|
287
|
+
this.releaseAttached();
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
this.attached = null;
|
|
291
|
+
clearTimeout(entry.idleTimer);
|
|
292
|
+
return { ws: entry.ws, live: entry.state === "live" };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private voiceSocketUrl(host: string, agentId: string): string {
|
|
296
|
+
return `${toWsBase(host)}/ws/agents/${encodeURIComponent(agentId)}/voice`;
|
|
297
|
+
}
|
|
298
|
+
|
|
107
299
|
/** Start the call: acquire mic, open the WS, stream PCM until hang-up. */
|
|
108
300
|
async startListening(): Promise<void> {
|
|
109
301
|
if (this.callLive) return; // idempotent: a call is already live
|
|
@@ -164,11 +356,25 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
164
356
|
}
|
|
165
357
|
});
|
|
166
358
|
|
|
167
|
-
const
|
|
359
|
+
const adopted = await this.takeAttachedSocket();
|
|
360
|
+
if (generation !== this.callGeneration) {
|
|
361
|
+
adopted?.ws.close(1000, "client ended call");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (adopted) {
|
|
365
|
+
const { ws } = adopted;
|
|
366
|
+
this.ws = ws;
|
|
367
|
+
this.bindCallSocket(ws, generation);
|
|
368
|
+
if (!adopted.live) ws.send('{"type":"start"}');
|
|
369
|
+
this.emitStatus("listening");
|
|
370
|
+
this.startCapture(captureContext, stream, ws, generation);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
168
374
|
// Token rides the subprotocol; `runtype.bearer` is the marker the server
|
|
169
375
|
// echoes as the negotiated subprotocol (browsers fail the handshake if an
|
|
170
376
|
// offered subprotocol goes unanswered).
|
|
171
|
-
const ws = new WebSocket(
|
|
377
|
+
const ws = new WebSocket(this.voiceSocketUrl(host, agentId), ["runtype.bearer", token]);
|
|
172
378
|
ws.binaryType = "arraybuffer";
|
|
173
379
|
this.ws = ws;
|
|
174
380
|
|
|
@@ -177,31 +383,7 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
177
383
|
this.emitStatus("listening");
|
|
178
384
|
this.startCapture(captureContext, stream, ws, generation);
|
|
179
385
|
};
|
|
180
|
-
|
|
181
|
-
ws.onmessage = (event) => this.handleMessage(event, generation);
|
|
182
|
-
|
|
183
|
-
ws.onerror = () => {
|
|
184
|
-
if (generation !== this.callGeneration) return;
|
|
185
|
-
this.emitError(new Error("Voice connection failed"));
|
|
186
|
-
this.emitStatus("error");
|
|
187
|
-
this.cleanup();
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
ws.onclose = (evt) => {
|
|
191
|
-
if (this.intentionalClose) {
|
|
192
|
-
this.intentionalClose = false;
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (generation !== this.callGeneration) return;
|
|
196
|
-
if (evt.code !== 1000) {
|
|
197
|
-
const codeMsg = evt.code ? ` (code ${evt.code})` : "";
|
|
198
|
-
this.emitError(new Error(`Voice connection closed${codeMsg}`));
|
|
199
|
-
this.emitStatus("error");
|
|
200
|
-
} else {
|
|
201
|
-
this.emitStatus("idle");
|
|
202
|
-
}
|
|
203
|
-
this.cleanup();
|
|
204
|
-
};
|
|
386
|
+
this.bindCallSocket(ws, generation);
|
|
205
387
|
} catch (error) {
|
|
206
388
|
this.cleanup();
|
|
207
389
|
this.emitError(error as Error);
|
|
@@ -210,6 +392,34 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
210
392
|
}
|
|
211
393
|
}
|
|
212
394
|
|
|
395
|
+
/** Route a call socket's frames, errors, and close into this call. */
|
|
396
|
+
private bindCallSocket(ws: WebSocket, generation: number): void {
|
|
397
|
+
ws.onmessage = (event) => this.handleMessage(event, generation);
|
|
398
|
+
|
|
399
|
+
ws.onerror = () => {
|
|
400
|
+
if (generation !== this.callGeneration) return;
|
|
401
|
+
this.emitError(new Error("Voice connection failed"));
|
|
402
|
+
this.emitStatus("error");
|
|
403
|
+
this.cleanup();
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
ws.onclose = (evt) => {
|
|
407
|
+
if (this.intentionalClose) {
|
|
408
|
+
this.intentionalClose = false;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (generation !== this.callGeneration) return;
|
|
412
|
+
if (evt.code !== 1000) {
|
|
413
|
+
const codeMsg = evt.code ? ` (code ${evt.code})` : "";
|
|
414
|
+
this.emitError(new Error(`Voice connection closed${codeMsg}`));
|
|
415
|
+
this.emitStatus("error");
|
|
416
|
+
} else {
|
|
417
|
+
this.emitStatus("idle");
|
|
418
|
+
}
|
|
419
|
+
this.cleanup();
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
213
423
|
/** End the call (hang up). */
|
|
214
424
|
async stopListening(): Promise<void> {
|
|
215
425
|
this.cleanup();
|
|
@@ -374,6 +584,7 @@ export class RuntypeVoiceProvider implements VoiceProvider {
|
|
|
374
584
|
this.callGeneration += 1;
|
|
375
585
|
this.callLive = false;
|
|
376
586
|
this.isSpeaking = false;
|
|
587
|
+
this.releaseAttached();
|
|
377
588
|
|
|
378
589
|
if (this.processor) {
|
|
379
590
|
this.processor.onaudioprocess = null;
|