@xpufx/paseo-top 0.4.4 → 0.4.7

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 (130) hide show
  1. package/OVERVIEW.md +18 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +94 -1
  6. package/client/pill.tsx +692 -616
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -234
  48. package/npm-shrinkwrap.json +44 -0
  49. package/package.json +8 -4
  50. package/paseo-plugin.json +2 -0
  51. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  52. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  53. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  54. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  55. package/screenshots/paseo-top-dashboard.jpg +0 -0
  56. package/screenshots/paseo-top-general-view.jpg +0 -0
  57. package/server/resources.ts +98 -0
  58. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  59. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  60. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  61. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  62. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  63. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  64. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  65. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  66. package/server/vendor/permission-audit/index.ts +1 -0
  67. package/server/vendor/permission-audit/server.ts +839 -0
  68. package/shared/helper-version.ts +1 -1
  69. package/shared/multi-host.ts +153 -159
  70. package/shared/resources.ts +145 -0
  71. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  72. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  73. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  74. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  75. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  76. package/shared/vendor/permission-audit/filter.ts +61 -0
  77. package/shared/vendor/permission-audit/index.ts +2 -0
  78. package/shared/vendor/permission-audit/shared.ts +119 -0
  79. package/shared/version.ts +1 -1
  80. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  81. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  82. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  83. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  84. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  85. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  86. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  87. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  88. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  89. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  90. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  91. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  92. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  121. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  122. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  123. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  124. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  125. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  126. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  127. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -1,7 +1,63 @@
1
1
  import type { StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
2
2
 
3
3
  /**
4
- * Converts a hex color and opacity (0.0 to 1.0) into an 8-character hex or rgba string.
4
+ * The single host-color fallback owned by the helper.
5
+ *
6
+ * A host `theme` prop normally supplies every token, but a partial or older
7
+ * payload may omit some. Rather than each plugin inventing its own
8
+ * `colors.border || "#334155"` guard, the helper resolves a theme once, here,
9
+ * and every adapter reads the resolved token. Plugins may pass this palette to
10
+ * code that renders outside a provider (recipes, previews, tests); they must
11
+ * not define their own literal fallbacks.
12
+ */
13
+ export const FALLBACK_COLORS: ThemeColors = {
14
+ surface0: "#18181b",
15
+ surface1: "#27272a",
16
+ surface2: "#3f3f46",
17
+ border: "#3f3f46",
18
+ foreground: "#fafafa",
19
+ foregroundMuted: "#a1a1aa",
20
+ accent: "#3b82f6",
21
+ accentForeground: "#ffffff",
22
+ statusSuccess: "#22c55e",
23
+ statusWarning: "#eab308",
24
+ statusDanger: "#ef4444",
25
+ };
26
+
27
+ /**
28
+ * The host theme carries no shadow token, so shadows use this documented
29
+ * overlay constant owned by the helper. It is not a `ThemeColors` token and is
30
+ * intentionally not part of the palette a render guard checks; it is the one
31
+ * allowed non-theme color.
32
+ */
33
+ export const HOST_SHADOW_COLOR = "rgba(0, 0, 0, 0.4)";
34
+
35
+ /**
36
+ * Fills a possibly-partial host `theme.colors` from {@link FALLBACK_COLORS}.
37
+ * This is the one place a missing token becomes a literal, so surfaces never
38
+ * need a per-property `|| "#hex"` guard.
39
+ */
40
+ export function resolveHostColors(colors?: Partial<ThemeColors> | null): ThemeColors {
41
+ return { ...FALLBACK_COLORS, ...(colors ?? {}) };
42
+ }
43
+
44
+ /**
45
+ * Paseo Plugin Helper — UI color tokens (`paseo-plugin-helper/ui`).
46
+ *
47
+ * Pure color math for the ui/ adapter layer. These helpers are deliberately
48
+ * self-contained: they never read the DOM, never scrape CSS variables, and
49
+ * never import the frozen `client/` theme system. Colors arrive from the host
50
+ * `theme` prop via {@link HostThemeProvider} and are combined here.
51
+ */
52
+
53
+ /**
54
+ * Converts a hex color and opacity (0.0 to 1.0) into an 8-character hex or
55
+ * rgba string.
56
+ *
57
+ * - `#rgb` / `#rrggbb` / `#rrggbbaa` → `#rrggbbaa` (alpha channel replaced)
58
+ * - `rgb()` / `rgba()` → `rgba(r, g, b, opacity)`
59
+ * - anything else is returned unchanged (named colors, `transparent`, …)
60
+ * - empty/invalid color degrades to `rgba(0, 0, 0, opacity)`
5
61
  */
6
62
  export function alpha(color: string, opacity: number): string {
7
63
  if (!color) return `rgba(0, 0, 0, ${opacity})`;
@@ -39,42 +95,6 @@ export function alpha(color: string, opacity: number): string {
39
95
  return color;
40
96
  }
41
97
 
42
- /**
43
- * Calculates relative luminance (WCAG 2.0 formula) from a hex color.
44
- */
45
- export function getLuminance(hexColor: string): number {
46
- let hex = hexColor.replace("#", "");
47
- if (hex.length === 3) {
48
- hex = hex
49
- .split("")
50
- .map((c) => c + c)
51
- .join("");
52
- }
53
- const r = parseInt(hex.slice(0, 2), 16) / 255;
54
- const g = parseInt(hex.slice(2, 4), 16) / 255;
55
- const b = parseInt(hex.slice(4, 6), 16) / 255;
56
-
57
- const toLinear = (c: number) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
58
-
59
- return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
60
- }
61
-
62
- /**
63
- * Returns either lightText or darkText depending on which has highest contrast against bgHex.
64
- */
65
- export function getContrastColor(
66
- bgHex: string,
67
- lightText = "#ffffff",
68
- darkText = "#0f172a",
69
- ): string {
70
- try {
71
- const lum = getLuminance(bgHex);
72
- return lum > 0.45 ? darkText : lightText;
73
- } catch {
74
- return lightText;
75
- }
76
- }
77
-
78
98
  /**
79
99
  * Resolves a StatusVariant to a corresponding theme color hex string.
80
100
  */
@@ -102,7 +122,8 @@ export function getStatusColor(
102
122
  }
103
123
 
104
124
  /**
105
- * Returns background, text, and border styling colors for a given status variant.
125
+ * Returns background, text, and border styling colors for a given status
126
+ * variant, derived from the host theme colors via {@link alpha}.
106
127
  */
107
128
  export function getVariantPalette(
108
129
  variant: StatusVariant,
@@ -116,3 +137,39 @@ export function getVariantPalette(
116
137
  border: alpha(base, 0.3),
117
138
  };
118
139
  }
140
+
141
+ /**
142
+ * Calculates relative luminance (WCAG 2.0 formula) from a hex color.
143
+ */
144
+ export function getLuminance(hexColor: string): number {
145
+ let hex = hexColor.replace("#", "");
146
+ if (hex.length === 3) {
147
+ hex = hex
148
+ .split("")
149
+ .map((c) => c + c)
150
+ .join("");
151
+ }
152
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
153
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
154
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
155
+
156
+ const toLinear = (c: number) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
157
+
158
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
159
+ }
160
+
161
+ /**
162
+ * Returns either lightText or darkText depending on which has highest contrast against bgHex.
163
+ */
164
+ export function getContrastColor(
165
+ bgHex: string,
166
+ lightText = "#ffffff",
167
+ darkText = "#0f172a",
168
+ ): string {
169
+ try {
170
+ const lum = getLuminance(bgHex);
171
+ return lum > 0.45 ? darkText : lightText;
172
+ } catch {
173
+ return lightText;
174
+ }
175
+ }
@@ -0,0 +1,109 @@
1
+ import React, { createContext, useContext, type ReactNode } from "react";
2
+ import type { PluginTheme, ResponsiveLayout, StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
3
+ import { alpha, getStatusColor, getVariantPalette, FALLBACK_COLORS, resolveHostColors } from "./host-color";
4
+
5
+ /**
6
+ * Paseo Plugin Helper — Host theme context (`paseo-plugin-helper/ui`).
7
+ *
8
+ * The ui/ adapter layer's ONLY source of color tokens. The host hands every
9
+ * surface a `theme` prop (`PluginTheme`); `HostThemeProvider` carries those
10
+ * colors to every ui/ adapter below it.
11
+ *
12
+ * Contract — what this context deliberately does NOT do:
13
+ * - No DOM CSS variable scraping. Colors come from the host `theme` prop only;
14
+ * the removed legacy theme provider's `getComputedStyle` scraper is gone.
15
+ * - No flair, density, typography, or padding machinery. The host owns the
16
+ * design language; ui/ only consumes its color tokens.
17
+ * - No scroll-ownership state machine. See `ui/modal.tsx`.
18
+ *
19
+ * Usage:
20
+ * ```tsx
21
+ * <HostThemeProvider theme={props.theme}>
22
+ * <HostCard>…</HostCard>
23
+ * </HostThemeProvider>
24
+ * ```
25
+ */
26
+
27
+ export interface HostTheme {
28
+ /** Host theme colors, straight from the host `theme` prop. */
29
+ colors: ThemeColors;
30
+ /** Color-token opacity helper bound to nothing but its arguments. */
31
+ alpha: (color: string, opacity: number) => string;
32
+ /** Status variant → host theme color. */
33
+ getStatusColor: (variant: StatusVariant) => string;
34
+ /** Status variant → { bg, text, border } palette derived via `alpha`. */
35
+ getVariantPalette: (variant: StatusVariant) => { bg: string; text: string; border: string };
36
+ }
37
+
38
+ /**
39
+ * Neutral fallback palette so ui/ adapters still render outside a provider
40
+ * (tests, previews, host surfaces that have not migrated yet). It is the single
41
+ * helper-owned fallback; see {@link FALLBACK_COLORS}. A supplied `theme` is
42
+ * merged over it, so a partial host payload never paints a raw literal.
43
+ */
44
+ function createHostTheme(colors: ThemeColors): HostTheme {
45
+ return {
46
+ colors,
47
+ alpha,
48
+ getStatusColor: (variant) => getStatusColor(variant, colors),
49
+ getVariantPalette: (variant) => getVariantPalette(variant, colors),
50
+ };
51
+ }
52
+
53
+ const FALLBACK_HOST_THEME: HostTheme = createHostTheme(FALLBACK_COLORS);
54
+
55
+ const HostThemeContext = createContext<HostTheme>(FALLBACK_HOST_THEME);
56
+
57
+ export interface HostThemeProviderProps {
58
+ /** The host `theme` prop for this surface — the single source of colors. */
59
+ theme: PluginTheme;
60
+ children: ReactNode;
61
+ }
62
+
63
+ /**
64
+ * Provides host theme colors to every ui/ adapter in the subtree. Same `theme`
65
+ * prop shape the removed legacy `PluginThemeProvider` took, but no scraped
66
+ * variables and no flair.
67
+ */
68
+ export function HostThemeProvider({ theme, children }: HostThemeProviderProps) {
69
+ const value = createHostTheme(resolveHostColors(theme.colors));
70
+ return <HostThemeContext.Provider value={value}>{children}</HostThemeContext.Provider>;
71
+ }
72
+
73
+ /**
74
+ * Reads the host theme provided by {@link HostThemeProvider}. Falls back to a
75
+ * static neutral palette when no provider is above — adapters never throw and
76
+ * never scrape.
77
+ */
78
+ export function useHostTheme(): HostTheme {
79
+ return useContext(HostThemeContext);
80
+ }
81
+
82
+ // --- Host layout -----------------------------------------------------------
83
+
84
+ const DEFAULT_LAYOUT: ResponsiveLayout = {
85
+ compact: false,
86
+ platform: "web",
87
+ };
88
+
89
+ const HostLayoutContext = createContext<ResponsiveLayout>(DEFAULT_LAYOUT);
90
+
91
+ export interface HostLayoutProviderProps {
92
+ /** The host `layout` prop for this surface. */
93
+ layout: ResponsiveLayout;
94
+ children: ReactNode;
95
+ }
96
+
97
+ /**
98
+ * Provides the host layout descriptor (compact / platform / width) to ui/
99
+ * adapters that branch on it (e.g. `HostResponsive`). Optional: adapters
100
+ * fall back to a wide-web default when no provider is above.
101
+ */
102
+ export function HostLayoutProvider({ layout, children }: HostLayoutProviderProps) {
103
+ return <HostLayoutContext.Provider value={layout}>{children}</HostLayoutContext.Provider>;
104
+ }
105
+
106
+ /** Reads the host layout descriptor. */
107
+ export function useHostLayout(): ResponsiveLayout {
108
+ return useContext(HostLayoutContext);
109
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Paseo Plugin Helper — Lifecycle (headless, client-safe, UI-free).
3
+ *
4
+ * Import from `paseo-plugin-helper/lifecycle` for the host-registration
5
+ * engines — composer pills, sidebar surfaces, workspace/agent panels — plus
6
+ * clipboard and haptics helpers. These are client-bundle safe (React Native,
7
+ * no `node:*`) but intentionally separate from `paseo-plugin-helper/core`,
8
+ * whose purity contract forbids `react-native`.
9
+ *
10
+ * The bespoke `client/` design system was removed (#938); this entry mounts
11
+ * the host theme itself and imports only the headless host seam from `core`.
12
+ */
13
+ export * from "./pill";
14
+ export * from "./surface";
15
+ export * from "./panel";
16
+ export * from "./clipboard";
17
+ export * from "./haptics";
18
+ export * from "./scroll-owner";
19
+ export * from "./host-theme";
20
+ export * from "./host-color";
21
+ export * from "./flair";
22
+ export * from "./providers";
23
+ export * from "./multi-host";
@@ -0,0 +1,66 @@
1
+ import * as pluginClient from "@getpaseo/plugin/client";
2
+ import type { PluginHostSummary } from "@getpaseo/plugin/client";
3
+
4
+ /**
5
+ * The desktop host bundle supplies `useHosts`/`getPaseoClient`; the mobile
6
+ * bundle omits them, so a direct named call throws `useHosts is not a
7
+ * function` and takes the whole surface with it (#1043, #1057-G). These seams
8
+ * reach the primitives through a feature-detected namespace so a surface
9
+ * degrades to "no hosts" instead of crashing the plugin.
10
+ */
11
+
12
+ /** The host summary shape `useHosts()` returns (the SDK's `PluginHostSummary`). */
13
+ export type OptionalHostSummary = PluginHostSummary;
14
+
15
+ /** The borrowed per-host client `getPaseoClient(serverId)` returns. */
16
+ export type OptionalPaseoClient = ReturnType<typeof pluginClient.getPaseoClient>;
17
+
18
+ interface MultiHostPrimitives {
19
+ useHosts?: () => readonly PluginHostSummary[];
20
+ getPaseoClient?: (serverId: string) => OptionalPaseoClient;
21
+ }
22
+
23
+ /**
24
+ * Read a function export without tripping a test runner's module-mock `get`
25
+ * trap. A partial mock throws when an unlisted export is read; `Object.keys`
26
+ * only asks for the keys the module has, so an absent primitive reads as
27
+ * `undefined` on mock and real host alike.
28
+ */
29
+ function readFunction<T>(source: unknown, key: string): T | undefined {
30
+ if (source === null || typeof source !== "object") return undefined;
31
+ if (!Object.keys(source).includes(key)) return undefined;
32
+ const value = (source as Record<string, unknown>)[key];
33
+ return typeof value === "function" ? (value as T) : undefined;
34
+ }
35
+
36
+ const hostPrimitives: MultiHostPrimitives = {
37
+ useHosts: readFunction(pluginClient, "useHosts"),
38
+ getPaseoClient: readFunction(pluginClient, "getPaseoClient"),
39
+ };
40
+ const EMPTY_HOSTS: readonly PluginHostSummary[] = [];
41
+ const noHosts = () => EMPTY_HOSTS;
42
+
43
+ // Hook identity is module-constant, so a given host calls the same function on
44
+ // every render and the hook order never changes.
45
+ const useHosts = hostPrimitives.useHosts ?? noHosts;
46
+
47
+ /** True when the running host supplies the multi-host primitives. */
48
+ export function isMultiHostSupported(): boolean {
49
+ return (
50
+ typeof hostPrimitives.useHosts === "function" &&
51
+ typeof hostPrimitives.getPaseoClient === "function"
52
+ );
53
+ }
54
+
55
+ /**
56
+ * `useHosts()` when the host supplies it, otherwise an empty host list, so a
57
+ * surface that enumerates hosts degrades instead of crashing the bundle.
58
+ */
59
+ export function useOptionalHosts(): readonly PluginHostSummary[] {
60
+ return useHosts();
61
+ }
62
+
63
+ /** Borrow an online host's API, or `undefined` when the host omits the seam. */
64
+ export function getOptionalPaseoClient(serverId: string): OptionalPaseoClient | undefined {
65
+ return hostPrimitives.getPaseoClient?.(serverId);
66
+ }
@@ -2,9 +2,9 @@ import React, { type ComponentType } from "react";
2
2
  import type {
3
3
  HostWorkspacePanelProps as PluginWorkspacePanelProps,
4
4
  HostAgentPanelProps as PluginAgentPanelProps,
5
- } from "./host";
6
- import { PluginThemeProvider } from "./theme/provider";
7
- import type { VisualFlair } from "./theme/flair";
5
+ } from "../core/host";
6
+ import type { VisualFlair } from "./flair";
7
+ import { RegistrarThemeScope } from "./providers";
8
8
 
9
9
  /**
10
10
  * Structural registrar interface satisfied by both Paseo v0.7 PluginContext
@@ -43,9 +43,9 @@ export function registerWorkspacePanel(
43
43
  const { id, title, icon, Component, flair, locations } = options;
44
44
 
45
45
  const WrappedComponent: ComponentType<PluginWorkspacePanelProps> = (props) => (
46
- <PluginThemeProvider theme={props.theme} layout={props.layout} flair={flair}>
46
+ <RegistrarThemeScope theme={props.theme} layout={props.layout} flair={flair}>
47
47
  <Component {...props} />
48
- </PluginThemeProvider>
48
+ </RegistrarThemeScope>
49
49
  );
50
50
 
51
51
  const registration = plugin.addWorkspacePanel({
@@ -74,9 +74,9 @@ export function registerAgentPanel(
74
74
  const { id, title, icon, Component, flair } = options;
75
75
 
76
76
  const WrappedComponent: ComponentType<PluginAgentPanelProps> = (props) => (
77
- <PluginThemeProvider theme={props.theme} layout={props.layout} flair={flair}>
77
+ <RegistrarThemeScope theme={props.theme} layout={props.layout} flair={flair}>
78
78
  <Component {...props} />
79
- </PluginThemeProvider>
79
+ </RegistrarThemeScope>
80
80
  );
81
81
 
82
82
  plugin.addWorkspacePanel({
@@ -14,16 +14,16 @@ import {
14
14
  type HostPillProps,
15
15
  type HostTheme,
16
16
  type PluginCleanup,
17
- } from "./host";
17
+ } from "../core/host";
18
18
  import type {
19
19
  PluginButtonContentProps,
20
20
  PluginButtonIconProps,
21
21
  PluginButtonRegistration,
22
22
  } from "@getpaseo/plugin/client";
23
- import { PluginThemeProvider } from "./theme/provider";
24
- import type { VisualFlair } from "./theme/flair";
25
- import { ModalBodyScrollOwnerContext } from "./layout/ModalBody";
26
- import { reportSuppressed } from "../../../shared/vendor/paseo-plugin-helper/suppressed";
23
+ import type { VisualFlair } from "./flair";
24
+ import { ModalBodyScrollOwnerContext } from "./scroll-owner";
25
+ import { RegistrarThemeScope } from "./providers";
26
+ import { reportSuppressed } from "../../../../shared/vendor/paseo-plugin-helper/suppressed";
27
27
 
28
28
  export interface RenderModalProps<TPayload = any> extends HostPillProps {
29
29
  close: () => void;
@@ -423,9 +423,13 @@ export function registerComposerPill<TPayload = any>(
423
423
  onStartShouldSetResponder={() => true}
424
424
  onTouchEnd={stopBubbling}
425
425
  {...(eventBoundaryProps as any)}
426
- style={{ flex: 1, minHeight: 0 }}
426
+ style={
427
+ resolvePillModalScrollable(options.hostScroll)
428
+ ? { width: "100%" }
429
+ : { flex: 1, minHeight: 0, width: "100%" }
430
+ }
427
431
  >
428
- <PluginThemeProvider theme={theme} layout={layout} flair={options.flair}>
432
+ <RegistrarThemeScope theme={theme} layout={layout} flair={options.flair}>
429
433
  <ModalBodyScrollOwnerContext.Provider
430
434
  value={resolvePillModalScrollable(options.hostScroll) ? "host" : "required"}
431
435
  >
@@ -438,7 +442,7 @@ export function registerComposerPill<TPayload = any>(
438
442
  close: () => setCenteredOpen(agentId, false),
439
443
  })}
440
444
  </ModalBodyScrollOwnerContext.Provider>
441
- </PluginThemeProvider>
445
+ </RegistrarThemeScope>
442
446
  </View>
443
447
  ) : null}
444
448
  </Modal.Content>
@@ -470,11 +474,14 @@ export function registerComposerPill<TPayload = any>(
470
474
  !props.layout.compact && options.popoverWidth
471
475
  ? Math.max(0, Math.min(options.popoverWidth, windowWidth - 24))
472
476
  : undefined;
473
- // The host remains the scroll owner, but it needs a finite child extent to
474
- // establish the popover viewport. Do not add overflow clipping or an inner
475
- // ScrollView here: either would compete with the host's popover/sheet
476
- // scroller.
477
- const frameMaxHeight = Math.max(1, Math.min(560, windowHeight * 0.8));
477
+ // The host remains the scroll owner, but desktop needs a finite child extent to
478
+ // establish the popover viewport. Compact mobile presents a full-bleed sheet
479
+ // whose BottomSheetScrollView dynamically sizes and scrolls, so capping height
480
+ // here would clip long content instead of scrolling it.
481
+ const isMobile = props.layout.platform === "ios" || props.layout.platform === "android";
482
+ const frameMaxHeight = props.layout.compact && isMobile
483
+ ? undefined
484
+ : Math.max(1, Math.min(560, windowHeight * 0.8));
478
485
  // Layout effect, not effect: the flag must be set in the same commit the
479
486
  // popover mounts, before any label timer can interleave. An effect runs
480
487
  // after paint, leaving a window where one tick can still publish and
@@ -493,7 +500,7 @@ export function registerComposerPill<TPayload = any>(
493
500
  host: props.host ?? { id: "", label: "" },
494
501
  };
495
502
  return (
496
- <PluginThemeProvider theme={props.theme} layout={props.layout} flair={options.flair}>
503
+ <RegistrarThemeScope theme={props.theme} layout={props.layout} flair={options.flair}>
497
504
  <View
498
505
  style={{
499
506
  ...styles.popoverContainer,
@@ -505,7 +512,7 @@ export function registerComposerPill<TPayload = any>(
505
512
  {options.renderModal?.({ ...pillProps, close: props.close })}
506
513
  </ModalBodyScrollOwnerContext.Provider>
507
514
  </View>
508
- </PluginThemeProvider>
515
+ </RegistrarThemeScope>
509
516
  );
510
517
  }
511
518
 
@@ -632,6 +639,8 @@ export function registerComposerPill<TPayload = any>(
632
639
  syncSharedLabelTimer();
633
640
  }
634
641
 
642
+ const lifetime = new AbortController();
643
+
635
644
  const unsubscribe = client.paseo.agents.subscribe((update) => {
636
645
  if (disposed) return;
637
646
  if ("agentId" in update && update.kind === "remove") {
@@ -645,14 +654,37 @@ export function registerComposerPill<TPayload = any>(
645
654
  });
646
655
 
647
656
  client.paseo.agents
648
- .list()
657
+ .list({ subscribe: {}, signal: lifetime.signal })
649
658
  .then((result) => {
650
659
  if (disposed) return;
651
- for (const { agent } of result.entries) {
652
- if (agent.workspaceId) addPill(agent.id, agent.workspaceId);
660
+ if (result.entries) {
661
+ for (const { agent } of result.entries) {
662
+ if (agent.workspaceId) addPill(agent.id, agent.workspaceId);
663
+ }
664
+ }
665
+ if (result.subscription) {
666
+ result.subscription.subscribe({
667
+ snapshot: ({ entries }: { entries: Array<{ agent: { id: string; workspaceId?: string | null } }> }) => {
668
+ if (disposed) return;
669
+ for (const { agent } of entries) {
670
+ if (agent.workspaceId) addPill(agent.id, agent.workspaceId);
671
+ }
672
+ },
673
+ update: (message: any) => {
674
+ if (disposed) return;
675
+ if (message?.type !== "agent_update") return;
676
+ const update = message.payload;
677
+ if ("agentId" in update && update.kind === "remove") {
678
+ removePill(update.agentId);
679
+ } else if ("agent" in update && update.agent?.workspaceId) {
680
+ addPill(update.agent.id, update.agent.workspaceId);
681
+ }
682
+ },
683
+ });
653
684
  }
654
685
  })
655
686
  .catch((error) => {
687
+ if (lifetime.signal.aborted || disposed) return;
656
688
  // Silently dropping this leaves a plugin with zero pills and no clue why.
657
689
  reportSuppressed(console, "registerComposerPill agents.list() failed", error, "warn");
658
690
  });
@@ -660,6 +692,7 @@ export function registerComposerPill<TPayload = any>(
660
692
  return () => {
661
693
  if (disposed) return;
662
694
  disposed = true;
695
+ lifetime.abort();
663
696
  unsubscribe();
664
697
  if (sharedLabelTimer) {
665
698
  clearInterval(sharedLabelTimer);
@@ -0,0 +1,46 @@
1
+ import React, { type ComponentType, type ReactNode } from "react";
2
+ import type { PluginTheme } from "../../../../shared/vendor/paseo-plugin-helper/types";
3
+ import type { HostLayout } from "../core/host";
4
+ import type { VisualFlair } from "./flair";
5
+ import { HostThemeProvider } from "./host-theme";
6
+
7
+ /**
8
+ * Presentational seam for the lifecycle registration engines.
9
+ *
10
+ * The bespoke `client/` design system was removed (#938), so no installer
11
+ * remains: `RegistrarThemeScope` mounts only `HostThemeProvider` (the host
12
+ * `theme` prop drives every ui/ adapter). The legacy-provider hook stays for
13
+ * callers that mounted their own theme provider before the removal.
14
+ */
15
+ export interface RegistrarThemeProps {
16
+ theme: PluginTheme;
17
+ layout?: HostLayout;
18
+ flair?: Partial<VisualFlair>;
19
+ children: ReactNode;
20
+ }
21
+
22
+ export type RegistrarLegacyThemeProvider = ComponentType<RegistrarThemeProps>;
23
+
24
+ let legacyThemeProvider: RegistrarLegacyThemeProvider | undefined;
25
+
26
+ /**
27
+ * Installed once by a legacy theme shim. Keeping it out of the `lifecycle/`
28
+ * entry's runtime imports is what lets that entry stay free of any UI kit.
29
+ */
30
+ export function setRegistrarLegacyThemeProvider(
31
+ provider: RegistrarLegacyThemeProvider | undefined,
32
+ ): void {
33
+ legacyThemeProvider = provider;
34
+ }
35
+
36
+ /** Mounts the host theme (always) and the legacy provider (when installed). */
37
+ export function RegistrarThemeScope({ theme, layout, flair, children }: RegistrarThemeProps) {
38
+ const hostTheme = <HostThemeProvider theme={theme}>{children}</HostThemeProvider>;
39
+ const Legacy = legacyThemeProvider;
40
+ if (!Legacy) return hostTheme;
41
+ return (
42
+ <Legacy theme={theme} layout={layout} flair={flair}>
43
+ {hostTheme}
44
+ </Legacy>
45
+ );
46
+ }
@@ -0,0 +1,26 @@
1
+ import { createContext } from "react";
2
+
3
+ /**
4
+ * Scroll-ownership signal emitted by the lifecycle registration engines.
5
+ *
6
+ * - `"helper"` — the default; the content owns its own scrolling.
7
+ * - `"host"` — an ancestor host view already provides the one scroller
8
+ * (0.8 composer popovers), so the content renders plain.
9
+ * - `"required"` — the ancestor has NO host scroller and the content is
10
+ * host-sized, so it MUST own the scroll on every surface. Set by
11
+ * `registerSidebarSurface` (a plugin surface is a full host page whose body
12
+ * is not wrapped in a host scroller).
13
+ * - `"popover"` — the host anchored-popover already supplies the sole scroller.
14
+ *
15
+ * The context lives in `lifecycle/` so the registration engines can mark the
16
+ * subtrees they render without importing any UI kit. It survived the removal of
17
+ * the bespoke `ModalBody` reader (#938) as a signal plugins and the host can
18
+ * still consume.
19
+ */
20
+ export type ModalBodyScrollOwner = "helper" | "host" | "required" | "popover";
21
+
22
+ const SCROLL_OWNER_CONTEXT_KEY = Symbol.for("paseo.modalBodyScrollOwnerContext");
23
+
24
+ export const ModalBodyScrollOwnerContext: React.Context<ModalBodyScrollOwner> =
25
+ ((globalThis as any)[SCROLL_OWNER_CONTEXT_KEY] ??= createContext<ModalBodyScrollOwner>("helper"));
26
+
@@ -1,9 +1,9 @@
1
1
  import React, { type ComponentType } from "react";
2
2
  import { View } from "react-native";
3
- import type { HostSurfaceProps as PluginSurfaceProps } from "./host";
4
- import { PluginThemeProvider } from "./theme/provider";
5
- import { ModalBodyScrollOwnerContext } from "./layout/ModalBody";
6
- import type { VisualFlair } from "./theme/flair";
3
+ import type { HostSurfaceProps as PluginSurfaceProps } from "../core/host";
4
+ import type { VisualFlair } from "./flair";
5
+ import { ModalBodyScrollOwnerContext } from "./scroll-owner";
6
+ import { RegistrarThemeScope } from "./providers";
7
7
 
8
8
  /**
9
9
  * Structural registrar interface satisfied by both Paseo v0.7 PluginContext
@@ -101,7 +101,7 @@ export function registerSidebarSurface(
101
101
  } = options;
102
102
 
103
103
  const WrappedComponent: ComponentType<PluginSurfaceProps> = (props) => (
104
- <PluginThemeProvider theme={props.theme} layout={props.layout} flair={flair}>
104
+ <RegistrarThemeScope theme={props.theme} layout={props.layout} flair={flair}>
105
105
  <ModalBodyScrollOwnerContext.Provider value="required">
106
106
  {maxContentWidth === false ? (
107
107
  <Component {...props} />
@@ -111,7 +111,7 @@ export function registerSidebarSurface(
111
111
  </SurfaceContentColumn>
112
112
  )}
113
113
  </ModalBodyScrollOwnerContext.Provider>
114
- </PluginThemeProvider>
114
+ </RegistrarThemeScope>
115
115
  );
116
116
 
117
117
  const surfaceRegistration = plugin.addSurface(id, WrappedComponent);
@@ -0,0 +1 @@
1
+ export * from "../lifecycle/host-color";