@xpufx/paseo-mcp-tools 0.1.3

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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/client/__snapshots__/attribution.test.tsx.snap +170 -0
  4. package/client/commands.ts +113 -0
  5. package/client/mcp-query.tsx +39 -0
  6. package/client/pill.tsx +886 -0
  7. package/client/timeline.tsx +136 -0
  8. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  9. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  10. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  11. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  12. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  13. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  14. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  15. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  16. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  17. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  18. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  19. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  20. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  21. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  22. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  23. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  24. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  25. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  26. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  27. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  28. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  29. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  30. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  31. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  32. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  33. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  34. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  35. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  36. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  37. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  38. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  39. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  40. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  41. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  42. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  43. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  44. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  45. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  46. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  47. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  48. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  49. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  50. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  51. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  52. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  53. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  54. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  55. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  56. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  57. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  58. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  59. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  60. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  61. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  62. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  63. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  64. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  65. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  66. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  67. package/docs/TEST_METHODOLOGY.md +29 -0
  68. package/package.json +41 -0
  69. package/paseo-plugin.json +4 -0
  70. package/scripts/version.mjs +4 -0
  71. package/server/discovery/extract.ts +277 -0
  72. package/server/discovery/types.ts +23 -0
  73. package/server/health/health.ts +274 -0
  74. package/server/inject.ts +32 -0
  75. package/server/mcp.ts +365 -0
  76. package/server/providers/README.md +51 -0
  77. package/server/providers/antigravity.ts +22 -0
  78. package/server/providers/catalog.ts +6 -0
  79. package/server/providers/claude.ts +21 -0
  80. package/server/providers/codex.ts +85 -0
  81. package/server/providers/family.ts +24 -0
  82. package/server/providers/index.ts +11 -0
  83. package/server/providers/opencode.ts +95 -0
  84. package/server/providers/paseo.ts +112 -0
  85. package/server/providers/pi.ts +39 -0
  86. package/server/settings.ts +19 -0
  87. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  88. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  89. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  90. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  91. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  92. package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
  93. package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
  94. package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
  95. package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
  96. package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
  97. package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
  98. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  99. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  100. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  101. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  102. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  103. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  104. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  105. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  106. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  107. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  108. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  109. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  110. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  111. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  112. package/shared/mcp.ts +207 -0
  113. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  114. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  115. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  116. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  117. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  118. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  119. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  120. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  121. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  122. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  123. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  124. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  125. package/shared/version.ts +2 -0
@@ -0,0 +1,378 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useRef,
5
+ type ComponentType,
6
+ type ReactNode,
7
+ type Ref,
8
+ } from "react";
9
+ import {
10
+ RefreshControl,
11
+ ScrollView as FallbackScrollView,
12
+ StyleSheet,
13
+ View,
14
+ type ScrollView as ScrollViewInstance,
15
+ type ScrollViewProps,
16
+ type StyleProp,
17
+ type ViewStyle,
18
+ } from "react-native";
19
+ import { getOptionalClientHost, selectHostScrollView, type HostScrollView } from "../host";
20
+ import { usePluginTheme } from "../theme/provider";
21
+
22
+ export type ModalBodySize = "default" | "large";
23
+
24
+ export interface ModalBodyProps {
25
+ children: ReactNode;
26
+ style?: StyleProp<ViewStyle>;
27
+ contentContainerStyle?: StyleProp<ViewStyle>;
28
+ header?: ReactNode;
29
+ headerStyle?: StyleProp<ViewStyle>;
30
+ /**
31
+ * Host dialog size preset. "default" (default) is fully fluid inside the
32
+ * host-allocated dialog. "large" opts into the helper's documented wide
33
+ * extent on desktop so data-dense modals/surfaces get room, and is ignored on
34
+ * mobile (the bottom sheet is already full-bleed) and inside composer
35
+ * popovers (the host owns that narrow viewport). Use this instead of adding a
36
+ * per-plugin width/minWidth literal; the host still owns the final size.
37
+ */
38
+ size?: ModalBodySize;
39
+ /**
40
+ * Optional upper bound (px) on the content column width. On large viewports
41
+ * the host still allocates a wide dialog, but the body's content column stays
42
+ * readable instead of stretching edge-to-edge: `width: "100%"` keeps it fluid
43
+ * below the cap and `alignSelf: "center"` centers the capped column. Undefined
44
+ * (default) preserves the fully fluid body. This does not dictate the dialog
45
+ * frame; widen the frame with `size` when dense content genuinely needs room.
46
+ */
47
+ maxContentWidth?: number;
48
+ /**
49
+ * "scroll" (default): header renders INSIDE the helper-owned compact/mobile
50
+ * ScrollView and moves with content. "pinned": header renders above that
51
+ * compact/mobile scroller; on desktop the host remains the scroll owner.
52
+ */
53
+ headerMode?: "pinned" | "scroll";
54
+ /**
55
+ * Scroll ownership override. "auto" (default) renders the helper-owned
56
+ * scroller only on compact/mobile surfaces and defers to the host elsewhere.
57
+ * "always" makes the helper the scroll owner on every surface; use it when
58
+ * the host supplies no scroller because the content view is bounded
59
+ * (`ModalContent` passes it for `<Modal.Content scrollable={false}>`).
60
+ */
61
+ scrollMode?: "auto" | "always";
62
+ /**
63
+ * When set, ModalBody logs measured layout values (viewport height,
64
+ * content height) via onLayout/onContentSizeChange under this tag, e.g.
65
+ * `[ModalBody:mcp] viewport=… content=…`. Use on-device to see which
66
+ * container actually scrolls instead of guessing from theory (#110).
67
+ */
68
+ debugTag?: string;
69
+ extraBottomInset?: number;
70
+ refreshing?: boolean;
71
+ onRefresh?: () => void | Promise<void>;
72
+ stickToEnd?: boolean;
73
+ scrollRef?: Ref<ScrollViewInstance>;
74
+ }
75
+
76
+ /**
77
+ * Scroll-ownership signal for `ModalBody`.
78
+ *
79
+ * - `"helper"` — the default; `ModalBody` decides from the surface
80
+ * (compact/mobile) and `scrollMode`.
81
+ * - `"host"` — an ancestor host view already provides the one scroller
82
+ * (0.8 composer popovers). `ModalBody` renders plain content.
83
+ * - `"required"` — the ancestor has NO host scroller and the content is
84
+ * host-sized, so `ModalBody` MUST own the scroll on every surface. Set by
85
+ * `registerSidebarSurface` (a plugin surface is a full host page whose body
86
+ * is not wrapped in a host scroller) and by `ModalContent`.
87
+ *
88
+ * Adding the `"required"` member is additive: the existing two values keep
89
+ * their meaning and the default stays `"helper"`.
90
+ */
91
+ export type ModalBodyScrollOwner = "helper" | "host" | "required";
92
+
93
+ export const ModalBodyScrollOwnerContext = createContext<ModalBodyScrollOwner>("helper");
94
+
95
+ // The single documented home for the large dialog preset. Prefer
96
+ // `ModalBody size="large"` over adding a per-plugin width/minWidth literal.
97
+ const LARGE_DIALOG_MIN_WIDTH = 640;
98
+
99
+ /**
100
+ * Mobile-safe scrollable body for Paseo <Modal.Content>.
101
+ *
102
+ * Size contract: a modal takes the host-allocated dialog size and is fluid
103
+ * within it. `ModalBody` fills that allocation (`flex: 1`, `minHeight: 0`,
104
+ * `width: "100%"`) and never lets its children drive the dialog frame, so the
105
+ * modal stays stable while content loads, refreshes, or grows. Do not wrap it
106
+ * in a container that hardcodes `minWidth`/`minHeight`/`width`/`height` or that
107
+ * sizes itself to its children - that reintroduces content-driven resize/redraw.
108
+ * Shrinkable text uses `minWidth: 0` + `flexShrink: 1`, never a fixed dimension.
109
+ *
110
+ * Host behavior differs by platform: on desktop the host owns a bounded dialog
111
+ * and its outer scroll, so `ModalBody` renders plain content and adds no second
112
+ * scroll region. On mobile the host presents a bottom sheet
113
+ * (`AdaptiveModalSheet`) that already owns the viewport and sheet gesture, so
114
+ * `ModalBody` defers to a host-provided scroller (or a plain view) and only adds
115
+ * the safe bottom inset. Plugin code must not guess either size.
116
+ *
117
+ * Scroll ownership: ordinary compact/mobile modal content uses this helper
118
+ * scroller. A 0.8 composer popover is different: Paseo's MenuSurface already
119
+ * supplies the sole outer scroller, and registerComposerPill marks that
120
+ * subtree through ModalBodyScrollOwnerContext so this component renders plain
121
+ * content instead. Pass `scrollMode="always"` when the host content view is
122
+ * bounded and supplies no scroller (`ModalContent` does this), so the helper
123
+ * scrolls on desktop too instead of clipping the bounded dialog.
124
+ * "pinned" is opt-in. Desktop surfaces retain host-owned scrolling so they do
125
+ * not create a second scrollbar; web hosts pin the header with sticky layout.
126
+ * Automatically calculates responsive bottom padding so controls are not cut off
127
+ * by mobile home bars or virtual keyboards.
128
+ * Supports pull-to-refresh on mobile via `refreshing` and `onRefresh`.
129
+ * Uses the host ScrollView from initClientHelpers when supplied (sheet-gesture
130
+ * integrated on Paseo v0.8), otherwise plain React Native ScrollView.
131
+ * Pass `stickToEnd` for conversation-style views that track new content, or
132
+ * `scrollRef` for imperative scrolling.
133
+ * Pass `header` for a pinned navbar (e.g. <Tabs>): it renders above the
134
+ * scroller in a flex column, so the header stays fixed while the body scrolls.
135
+ * Requires the host <Modal.Content scrollable={false}> so no outer sheet
136
+ * scroller drags the header along.
137
+ */
138
+ export function ModalBody({
139
+ children,
140
+ style,
141
+ contentContainerStyle,
142
+ header,
143
+ headerStyle,
144
+ headerMode = "scroll",
145
+ size = "default",
146
+ maxContentWidth,
147
+ scrollMode = "auto",
148
+ debugTag,
149
+ extraBottomInset = 0,
150
+ refreshing = false,
151
+ onRefresh,
152
+ stickToEnd = false,
153
+ scrollRef,
154
+ }: ModalBodyProps) {
155
+ const { isCompact, isMobile, layout, padding, colors } = usePluginTheme();
156
+ const scrollOwner = useContext(ModalBodyScrollOwnerContext);
157
+ const hostOwnsScroll = scrollOwner === "host";
158
+ // "required": an ancestor proved the host supplies no scroller (plugin
159
+ // surface) — own the scroll on every surface, including non-compact desktop.
160
+ const helperOwnsScroll =
161
+ !hostOwnsScroll &&
162
+ (scrollOwner === "required" || scrollMode === "always" || isCompact || isMobile);
163
+ // The large preset is the ONE documented place a plugin can ask for a wide
164
+ // dialog frame. It is a desktop-only content minimum: the mobile sheet is
165
+ // already full-bleed, and a composer popover viewport is host-owned and
166
+ // intentionally narrow, so both ignore the preset.
167
+ const sizeStyle =
168
+ size === "large" && !isMobile && !isCompact && !hostOwnsScroll
169
+ ? styles.largeDialog
170
+ : undefined;
171
+ const ResolvedScrollView = selectHostScrollView(
172
+ getOptionalClientHost(),
173
+ FallbackScrollView as unknown as HostScrollView,
174
+ );
175
+ // Optional readability cap on the content column.
176
+ //
177
+ // `width: "100%"` keeps the column fluid up to the cap. Centering must use
178
+ // auto side margins, NOT `alignSelf: "center"`: on a ScrollView content
179
+ // container `alignSelf` stops the column stretching to the viewport, so it
180
+ // sizes to its children instead — a wrapping pill row then grows as wide as
181
+ // its pills and never wraps (reported as "pills row rigid" on x-comms).
182
+ // Auto margins center the column while preserving full-width stretch.
183
+ const columnStyle =
184
+ maxContentWidth !== undefined
185
+ ? ({
186
+ width: "100%",
187
+ maxWidth: maxContentWidth,
188
+ marginLeft: "auto",
189
+ marginRight: "auto",
190
+ } as ViewStyle)
191
+ : undefined;
192
+ const innerRef = useRef<ScrollViewInstance>(null);
193
+
194
+ const setRefs = (node: ScrollViewInstance | null) => {
195
+ (innerRef as { current: ScrollViewInstance | null }).current = node;
196
+ if (typeof scrollRef === "function") {
197
+ scrollRef(node);
198
+ } else if (scrollRef) {
199
+ (scrollRef as { current: ScrollViewInstance | null }).current = node;
200
+ }
201
+ };
202
+
203
+ // Only real mobile platforms reserve the large bottom inset that clears
204
+ // navigation bars; a compact desktop popover does not need it.
205
+ const bottomPadding = (isMobile ? 48 : 20) + extraBottomInset;
206
+
207
+ const refreshControl = onRefresh ? (
208
+ <RefreshControl
209
+ refreshing={refreshing}
210
+ onRefresh={onRefresh}
211
+ tintColor={colors.accent}
212
+ colors={[colors.accent]}
213
+ />
214
+ ) : undefined;
215
+
216
+ // Compact without an injected host scroller still scrolls via the plain
217
+ // fallback: the helper pill sets <Modal.Content scrollable={false}>, so no
218
+ // outer sheet scroller fights the inner one.
219
+ const log = debugTag
220
+ ? (kind: string, v: Record<string, unknown>) =>
221
+ // eslint-disable-next-line no-console
222
+ console.log(`[ModalBody:${debugTag}] ${kind}`, JSON.stringify(v))
223
+ : undefined;
224
+ const scrollBody = (
225
+ <ResolvedScrollView
226
+ ref={setRefs}
227
+ style={[{ backgroundColor: colors.surface0 }, styles.container, sizeStyle, style]}
228
+ nestedScrollEnabled={true}
229
+ keyboardShouldPersistTaps="handled"
230
+ showsVerticalScrollIndicator={true}
231
+ refreshControl={refreshControl}
232
+ onLayout={
233
+ log
234
+ ? (e: { nativeEvent: { layout: { height: number; width: number } } }) =>
235
+ log("viewport", e.nativeEvent.layout)
236
+ : undefined
237
+ }
238
+ onContentSizeChange={(w: number, h: number) => {
239
+ log?.("content", { width: w, height: h });
240
+ if (stickToEnd) innerRef.current?.scrollToEnd({ animated: true });
241
+ }}
242
+ contentContainerStyle={[
243
+ styles.content,
244
+ {
245
+ paddingHorizontal: padding.horizontal,
246
+ paddingTop: padding.vertical,
247
+ paddingBottom: bottomPadding,
248
+ gap: padding.gap,
249
+ },
250
+ columnStyle,
251
+ contentContainerStyle,
252
+ ]}
253
+ >
254
+ {children}
255
+ </ResolvedScrollView>
256
+ );
257
+
258
+ const stickyHeaderStyle =
259
+ headerMode === "pinned" && layout.platform === "web"
260
+ ? ({
261
+ position: "sticky",
262
+ top: 0,
263
+ zIndex: 10,
264
+ backgroundColor: colors.surface0,
265
+ } as unknown as ViewStyle)
266
+ : undefined;
267
+ const plainBody = (
268
+ <View style={[styles.plainBody, sizeStyle, style]}>
269
+ {header ? (
270
+ <View style={[styles.header, stickyHeaderStyle, headerStyle]}>{header}</View>
271
+ ) : null}
272
+ <View
273
+ style={[
274
+ styles.plainContent,
275
+ {
276
+ paddingHorizontal: padding.horizontal,
277
+ paddingTop: padding.vertical,
278
+ paddingBottom: bottomPadding,
279
+ gap: padding.gap,
280
+ },
281
+ columnStyle,
282
+ contentContainerStyle,
283
+ ]}
284
+ >
285
+ {children}
286
+ </View>
287
+ </View>
288
+ );
289
+
290
+ if (!helperOwnsScroll) return plainBody;
291
+ if (!header) return scrollBody;
292
+
293
+ // "scroll" keeps the header in the content flow for compact/mobile hosts.
294
+ // "pinned" is handled below with a helper-owned scroller on every platform.
295
+ if (headerMode === "scroll") {
296
+ return (
297
+ <ResolvedScrollView
298
+ ref={setRefs}
299
+ style={[{ backgroundColor: colors.surface0 }, styles.container, sizeStyle, style]}
300
+ nestedScrollEnabled={true}
301
+ keyboardShouldPersistTaps="handled"
302
+ showsVerticalScrollIndicator={true}
303
+ refreshControl={refreshControl}
304
+ onLayout={
305
+ log
306
+ ? (e: { nativeEvent: { layout: { height: number; width: number } } }) =>
307
+ log("viewport", e.nativeEvent.layout)
308
+ : undefined
309
+ }
310
+ onContentSizeChange={(w: number, h: number) => {
311
+ log?.("content", { width: w, height: h });
312
+ if (stickToEnd) innerRef.current?.scrollToEnd({ animated: true });
313
+ }}
314
+ contentContainerStyle={[
315
+ styles.content,
316
+ {
317
+ paddingHorizontal: padding.horizontal,
318
+ paddingTop: padding.vertical,
319
+ paddingBottom: bottomPadding,
320
+ gap: padding.gap,
321
+ },
322
+ columnStyle,
323
+ contentContainerStyle,
324
+ ]}
325
+ >
326
+ <View style={[styles.header, headerStyle]}>{header}</View>
327
+ {children}
328
+ </ResolvedScrollView>
329
+ );
330
+ }
331
+
332
+ return (
333
+ <View style={[styles.screen, sizeStyle]}>
334
+ <View style={[styles.header, headerStyle]}>{header}</View>
335
+ {scrollBody}
336
+ </View>
337
+ );
338
+ }
339
+
340
+ const styles = StyleSheet.create({
341
+ // Desktop-only wide extent for `size="large"`. The host still owns the final
342
+ // dialog size; this only asks a content-sizing host for more room.
343
+ largeDialog: {
344
+ minWidth: LARGE_DIALOG_MIN_WIDTH,
345
+ },
346
+ screen: {
347
+ flex: 1,
348
+ minHeight: 0,
349
+ width: "100%",
350
+ },
351
+ header: {
352
+ width: "100%",
353
+ flexShrink: 0,
354
+ },
355
+ plainBody: {
356
+ flex: 1,
357
+ minHeight: 0,
358
+ width: "100%",
359
+ maxWidth: "100%",
360
+ },
361
+ plainContent: {
362
+ flexGrow: 1,
363
+ minHeight: 0,
364
+ width: "100%",
365
+ maxWidth: "100%",
366
+ },
367
+ container: {
368
+ flex: 1,
369
+ minHeight: 0,
370
+ width: "100%",
371
+ maxWidth: "100%",
372
+ },
373
+ content: {
374
+ flexGrow: 1,
375
+ width: "100%",
376
+ maxWidth: "100%",
377
+ },
378
+ });
@@ -0,0 +1,49 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { getClientHost } from "../host";
3
+ import { ModalBody, type ModalBodyProps } from "./ModalBody";
4
+
5
+ export interface ModalContentProps extends Omit<ModalBodyProps, "scrollMode"> {
6
+ children: ReactNode;
7
+ }
8
+
9
+ /**
10
+ * Helper-owned modal body for plugins that open their own host `<Modal>`.
11
+ *
12
+ * Use this instead of the raw host `<Modal.Content>`: it wraps the host content
13
+ * view AND the shared `ModalBody` contract in one element, so a plugin cannot
14
+ * accidentally end up content-sized.
15
+ *
16
+ * Why the raw host `Modal.Content` resizes: Paseo maps
17
+ * `<Modal.Content scrollable={true}>` (the host default) to a desktop card with
18
+ * no explicit height, so the dialog grows/shrinks with its children on every
19
+ * data change. This wrapper always passes `scrollable={false}`, which makes the
20
+ * host allocate a bounded dialog (`desktopHeight: "85%"`). Because that host
21
+ * content view then supplies no scroller, the wrapper also forces
22
+ * `ModalBody scrollMode="always"`, so the helper owns the one scroll region on
23
+ * every surface and the bounded dialog scrolls instead of clipping.
24
+ *
25
+ * The size contract is `ModalBody`'s: it takes the host-allocated dialog size
26
+ * and is fluid within it; `size?: "default" | "large"` is the only size escape
27
+ * hatch. Do not add per-plugin width/minWidth/height literals around it.
28
+ *
29
+ * ```tsx
30
+ * <Modal title="…" open={open} onOpenChange={setOpen}>
31
+ * <ModalContent size="default">
32
+ * …cards, controls, rows; content never drives the dialog frame…
33
+ * </ModalContent>
34
+ * </Modal>
35
+ * ```
36
+ *
37
+ * Accepts every `ModalBody` prop (header, headerMode, refreshing, onRefresh,
38
+ * stickToEnd, scrollRef, debugTag, style, contentContainerStyle, …).
39
+ */
40
+ export function ModalContent({ children, ...bodyProps }: ModalContentProps) {
41
+ const { Modal } = getClientHost();
42
+ return (
43
+ <Modal.Content scrollable={false}>
44
+ <ModalBody scrollMode="always" {...bodyProps}>
45
+ {children}
46
+ </ModalBody>
47
+ </Modal.Content>
48
+ );
49
+ }
@@ -0,0 +1,39 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { View, type StyleProp, type ViewStyle } from "react-native";
3
+ import { usePluginTheme } from "../theme/provider";
4
+ import { resolveSpacing, type SpacingValue } from "../theme/tokens";
5
+
6
+ export interface RowProps {
7
+ children?: ReactNode;
8
+ /** Gap between children: a spacing token or raw px. Defaults to the theme gap. */
9
+ gap?: SpacingValue;
10
+ /** Allow children to wrap onto the next line. Default: false. */
11
+ wrap?: boolean;
12
+ align?: ViewStyle["alignItems"];
13
+ justify?: ViewStyle["justifyContent"];
14
+ style?: StyleProp<ViewStyle>;
15
+ testID?: string;
16
+ }
17
+
18
+ /**
19
+ * Horizontal flexbox row with theme-derived gap. A thin vocabulary wrapper so
20
+ * plugins stop hand-rolling `<View style={{ flexDirection: "row", gap }}>`.
21
+ */
22
+ export function Row({ children, gap, wrap = false, align, justify, style, testID }: RowProps) {
23
+ const { padding } = usePluginTheme();
24
+
25
+ return (
26
+ <View
27
+ testID={testID}
28
+ style={[
29
+ { flexDirection: "row", gap: resolveSpacing(gap, padding.gap) },
30
+ wrap && { flexWrap: "wrap" },
31
+ align !== undefined && { alignItems: align },
32
+ justify !== undefined && { justifyContent: justify },
33
+ style,
34
+ ]}
35
+ >
36
+ {children}
37
+ </View>
38
+ );
39
+ }
@@ -0,0 +1,39 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { View, type StyleProp, type ViewStyle } from "react-native";
3
+ import { usePluginTheme } from "../theme/provider";
4
+ import { resolveSpacing, type SpacingValue } from "../theme/tokens";
5
+
6
+ export interface StackProps {
7
+ children?: ReactNode;
8
+ /** Gap between children: a spacing token or raw px. Defaults to the theme gap. */
9
+ gap?: SpacingValue;
10
+ align?: ViewStyle["alignItems"];
11
+ justify?: ViewStyle["justifyContent"];
12
+ style?: StyleProp<ViewStyle>;
13
+ testID?: string;
14
+ }
15
+
16
+ /**
17
+ * Vertical flexbox stack with theme-derived gap. The deliberate column default,
18
+ * named so it composes visually alongside {@link Row}.
19
+ */
20
+ export function Stack({ children, gap, align, justify, style, testID }: StackProps) {
21
+ const { padding } = usePluginTheme();
22
+
23
+ return (
24
+ <View
25
+ testID={testID}
26
+ style={[
27
+ { flexDirection: "column", gap: resolveSpacing(gap, padding.gap) },
28
+ align !== undefined && { alignItems: align },
29
+ justify !== undefined && { justifyContent: justify },
30
+ style,
31
+ ]}
32
+ >
33
+ {children}
34
+ </View>
35
+ );
36
+ }
37
+
38
+ /** Explicit vertical-stack alias; identical to {@link Stack}. */
39
+ export const VStack = Stack;
@@ -0,0 +1,7 @@
1
+ export * from "./ModalBody";
2
+ export * from "./ModalContent";
3
+ export * from "./ActionBar";
4
+ export * from "./FormRow";
5
+ export * from "./Row";
6
+ export * from "./Stack";
7
+ export * from "./Grid";
@@ -0,0 +1,81 @@
1
+ import React, { type ComponentType } from "react";
2
+ import type {
3
+ HostWorkspacePanelProps as PluginWorkspacePanelProps,
4
+ HostAgentPanelProps as PluginAgentPanelProps,
5
+ } from "./host";
6
+ import { PluginThemeProvider } from "./theme/provider";
7
+ import type { VisualFlair } from "./theme/flair";
8
+
9
+ /**
10
+ * Structural registrar interface satisfied by both Paseo v0.7 PluginContext
11
+ * and Paseo v0.8 PluginClientContext.
12
+ */
13
+ export interface WorkspacePanelRegistrar {
14
+ addWorkspacePanel(contribution: any): any;
15
+ }
16
+
17
+ export interface RegisterWorkspacePanelOptions {
18
+ id: string;
19
+ title: string;
20
+ icon: string;
21
+ Component: ComponentType<PluginWorkspacePanelProps>;
22
+ flair?: VisualFlair;
23
+ }
24
+
25
+ export interface RegisterAgentPanelOptions {
26
+ id: string;
27
+ title: string;
28
+ icon: string;
29
+ Component: ComponentType<PluginAgentPanelProps>;
30
+ flair?: VisualFlair;
31
+ }
32
+
33
+ /**
34
+ * Registers a workspace-scoped panel with automatic `<PluginThemeProvider>` injection.
35
+ * Works with both Paseo v0.7 PluginContext and Paseo v0.8 PluginClientContext.
36
+ */
37
+ export function registerWorkspacePanel(
38
+ plugin: WorkspacePanelRegistrar,
39
+ options: RegisterWorkspacePanelOptions,
40
+ ): void {
41
+ const { id, title, icon, Component, flair } = options;
42
+
43
+ const WrappedComponent: ComponentType<PluginWorkspacePanelProps> = (props) => (
44
+ <PluginThemeProvider theme={props.theme} layout={props.layout} flair={flair}>
45
+ <Component {...props} />
46
+ </PluginThemeProvider>
47
+ );
48
+
49
+ plugin.addWorkspacePanel({
50
+ id,
51
+ title,
52
+ icon,
53
+ context: "workspace",
54
+ Component: WrappedComponent,
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Registers an agent-scoped panel with automatic `<PluginThemeProvider>` injection.
60
+ * Works with both Paseo v0.7 PluginContext and Paseo v0.8 PluginClientContext.
61
+ */
62
+ export function registerAgentPanel(
63
+ plugin: WorkspacePanelRegistrar,
64
+ options: RegisterAgentPanelOptions,
65
+ ): void {
66
+ const { id, title, icon, Component, flair } = options;
67
+
68
+ const WrappedComponent: ComponentType<PluginAgentPanelProps> = (props) => (
69
+ <PluginThemeProvider theme={props.theme} layout={props.layout} flair={flair}>
70
+ <Component {...props} />
71
+ </PluginThemeProvider>
72
+ );
73
+
74
+ plugin.addWorkspacePanel({
75
+ id,
76
+ title,
77
+ icon,
78
+ context: "agent",
79
+ Component: WrappedComponent,
80
+ });
81
+ }