@xpufx/paseo-top 0.4.3 → 0.4.4
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/client/pill-labels.ts +30 -6
- package/client/pill.tsx +40 -61
- package/client/settings-events.ts +33 -0
- package/client/surface.tsx +6 -2
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +22 -2
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +32 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +17 -1
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +14 -1
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +142 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +16 -2
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +7 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +131 -63
- package/client/vendor/paseo-plugin-helper/components/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +4 -3
- package/client/vendor/paseo-plugin-helper/host.ts +12 -58
- package/client/vendor/paseo-plugin-helper/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +17 -27
- package/client/vendor/paseo-plugin-helper/pill.tsx +68 -321
- package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
- package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
- package/index.server.ts +7 -0
- package/package.json +11 -2
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
- package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
- package/server/vendor/paseo-plugin-helper/tickets.ts +1281 -0
- package/shared/helper-version.ts +20 -0
- package/shared/multi-host.ts +11 -4
- package/shared/resources.ts +1 -1
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +1010 -0
- package/shared/version.ts +1 -1
|
@@ -28,12 +28,10 @@ export interface ModalBodyProps {
|
|
|
28
28
|
header?: ReactNode;
|
|
29
29
|
headerStyle?: StyleProp<ViewStyle>;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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.
|
|
31
|
+
* Advisory density hint. It carries no width floor: the host `Modal` has no
|
|
32
|
+
* size prop, so a content-side `minWidth` would override the host dialog
|
|
33
|
+
* allocation and clip inside narrow docks (paseo#641). Use `maxContentWidth`
|
|
34
|
+
* for a readable dense column. Ignored on compact/mobile surfaces.
|
|
37
35
|
*/
|
|
38
36
|
size?: ModalBodySize;
|
|
39
37
|
/**
|
|
@@ -92,9 +90,10 @@ export type ModalBodyScrollOwner = "helper" | "host" | "required" | "popover";
|
|
|
92
90
|
|
|
93
91
|
export const ModalBodyScrollOwnerContext = createContext<ModalBodyScrollOwner>("helper");
|
|
94
92
|
|
|
95
|
-
//
|
|
96
|
-
// `
|
|
97
|
-
|
|
93
|
+
// `size` is advisory and intentionally carries no width floor. A content-side
|
|
94
|
+
// `minWidth` overrides the host dialog allocation instead of deferring to it,
|
|
95
|
+
// so a narrow dock (explorer/sidebar) clips the content it was meant to widen
|
|
96
|
+
// (paseo#641). Use `maxContentWidth` to keep a dense column readable.
|
|
98
97
|
|
|
99
98
|
/**
|
|
100
99
|
* Mobile-safe scrollable body for Paseo <Modal.Content>.
|
|
@@ -142,7 +141,6 @@ export function ModalBody({
|
|
|
142
141
|
header,
|
|
143
142
|
headerStyle,
|
|
144
143
|
headerMode = "scroll",
|
|
145
|
-
size = "default",
|
|
146
144
|
maxContentWidth,
|
|
147
145
|
scrollMode = "auto",
|
|
148
146
|
debugTag,
|
|
@@ -160,14 +158,11 @@ export function ModalBody({
|
|
|
160
158
|
const helperOwnsScroll =
|
|
161
159
|
!hostOwnsScroll &&
|
|
162
160
|
(scrollOwner === "required" || scrollMode === "always" || isCompact || isMobile);
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
size === "large" && !isMobile && !isCompact && scrollOwner !== "popover"
|
|
169
|
-
? styles.largeDialog
|
|
170
|
-
: undefined;
|
|
161
|
+
// `size` no longer contributes width. The host `Modal` has no size prop, so
|
|
162
|
+
// there is no host allocation to request and nothing to condition on: a
|
|
163
|
+
// content-side floor is the only lever available, and it fights the host by
|
|
164
|
+
// refusing to shrink inside a narrow dialog. Data-dense surfaces get their
|
|
165
|
+
// reading width from `maxContentWidth` on the content column instead.
|
|
171
166
|
const ResolvedScrollView = selectHostScrollView(
|
|
172
167
|
getOptionalClientHost(),
|
|
173
168
|
FallbackScrollView as unknown as HostScrollView,
|
|
@@ -224,7 +219,7 @@ export function ModalBody({
|
|
|
224
219
|
const scrollBody = (
|
|
225
220
|
<ResolvedScrollView
|
|
226
221
|
ref={setRefs}
|
|
227
|
-
style={[{ backgroundColor: colors.surface0 }, styles.container,
|
|
222
|
+
style={[{ backgroundColor: colors.surface0 }, styles.container, style]}
|
|
228
223
|
nestedScrollEnabled={true}
|
|
229
224
|
keyboardShouldPersistTaps="handled"
|
|
230
225
|
showsVerticalScrollIndicator={true}
|
|
@@ -265,7 +260,7 @@ export function ModalBody({
|
|
|
265
260
|
} as unknown as ViewStyle)
|
|
266
261
|
: undefined;
|
|
267
262
|
const plainBody = (
|
|
268
|
-
<View style={[styles.plainBody,
|
|
263
|
+
<View style={[styles.plainBody, style]}>
|
|
269
264
|
{header ? (
|
|
270
265
|
<View style={[styles.header, stickyHeaderStyle, headerStyle]}>{header}</View>
|
|
271
266
|
) : null}
|
|
@@ -296,7 +291,7 @@ export function ModalBody({
|
|
|
296
291
|
return (
|
|
297
292
|
<ResolvedScrollView
|
|
298
293
|
ref={setRefs}
|
|
299
|
-
style={[{ backgroundColor: colors.surface0 }, styles.container,
|
|
294
|
+
style={[{ backgroundColor: colors.surface0 }, styles.container, style]}
|
|
300
295
|
nestedScrollEnabled={true}
|
|
301
296
|
keyboardShouldPersistTaps="handled"
|
|
302
297
|
showsVerticalScrollIndicator={true}
|
|
@@ -330,7 +325,7 @@ export function ModalBody({
|
|
|
330
325
|
}
|
|
331
326
|
|
|
332
327
|
return (
|
|
333
|
-
<View style={[styles.screen
|
|
328
|
+
<View style={[styles.screen]}>
|
|
334
329
|
<View style={[styles.header, headerStyle]}>{header}</View>
|
|
335
330
|
{scrollBody}
|
|
336
331
|
</View>
|
|
@@ -338,11 +333,6 @@ export function ModalBody({
|
|
|
338
333
|
}
|
|
339
334
|
|
|
340
335
|
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
336
|
screen: {
|
|
347
337
|
flex: 1,
|
|
348
338
|
minHeight: 0,
|
|
@@ -2,35 +2,29 @@ import React, {
|
|
|
2
2
|
useEffect,
|
|
3
3
|
useLayoutEffect,
|
|
4
4
|
useMemo,
|
|
5
|
-
useState,
|
|
6
5
|
useSyncExternalStore,
|
|
7
6
|
type ComponentType,
|
|
8
7
|
type ReactNode,
|
|
9
8
|
} from "react";
|
|
10
|
-
import { StyleSheet,
|
|
9
|
+
import { StyleSheet, useWindowDimensions, View } from "react-native";
|
|
11
10
|
import {
|
|
12
11
|
getClientHost,
|
|
13
12
|
type ComposerPillRegistrar,
|
|
14
|
-
type ComposerPillRegistration,
|
|
15
|
-
type ComposerPillRegistrationHandle,
|
|
16
13
|
type HostLayout,
|
|
17
14
|
type HostPillProps,
|
|
18
15
|
type HostTheme,
|
|
19
16
|
type PluginCleanup,
|
|
20
17
|
} from "./host";
|
|
18
|
+
import type {
|
|
19
|
+
PluginButtonContentProps,
|
|
20
|
+
PluginButtonIconProps,
|
|
21
|
+
PluginButtonRegistration,
|
|
22
|
+
} from "@getpaseo/plugin/client";
|
|
21
23
|
import { PluginThemeProvider } from "./theme/provider";
|
|
22
|
-
import { useResponsive } from "./theme/useResponsive";
|
|
23
24
|
import type { VisualFlair } from "./theme/flair";
|
|
24
25
|
import { ModalBodyScrollOwnerContext } from "./layout/ModalBody";
|
|
25
26
|
import { reportSuppressed } from "../../../shared/vendor/paseo-plugin-helper/suppressed";
|
|
26
27
|
|
|
27
|
-
export interface RenderPillProps<TPayload = any> extends HostPillProps {
|
|
28
|
-
isOpen: boolean;
|
|
29
|
-
open: (payload?: TPayload) => void;
|
|
30
|
-
close: () => void;
|
|
31
|
-
toggle: (payload?: TPayload) => void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
28
|
export interface RenderModalProps<TPayload = any> extends HostPillProps {
|
|
35
29
|
close: () => void;
|
|
36
30
|
payload?: TPayload;
|
|
@@ -69,8 +63,6 @@ export function resolvePillModalScrollable(hostScroll?: boolean): boolean {
|
|
|
69
63
|
return hostScroll === true;
|
|
70
64
|
}
|
|
71
65
|
|
|
72
|
-
let probeSequence = 0;
|
|
73
|
-
|
|
74
66
|
const NOOP_SUBSCRIBE = (): (() => void) => () => {};
|
|
75
67
|
|
|
76
68
|
export interface RegisterComposerPillOptions<TPayload = any> {
|
|
@@ -118,48 +110,25 @@ export interface RegisterComposerPillOptions<TPayload = any> {
|
|
|
118
110
|
flair?: Partial<VisualFlair>;
|
|
119
111
|
|
|
120
112
|
/**
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* Optional compact badge text shown inside the default pill in compact mode. Defaults to `badgeText`.
|
|
127
|
-
*/
|
|
128
|
-
compactBadgeText?: string;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Optional callback to resolve default payload when the outer host pill is clicked.
|
|
132
|
-
* Receives agentId and workspaceId.
|
|
133
|
-
*/
|
|
134
|
-
resolveDefaultPayload?: (context: { agentId: string; workspaceId: string }) => TPayload | undefined;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Custom pill body renderer if you want to replace the default pill layout.
|
|
138
|
-
* Receives `isOpen`, `open`, `close`, and `toggle` along with standard pill props.\
|
|
139
|
-
*/
|
|
140
|
-
renderPill?: (props: RenderPillProps<TPayload>) => ReactNode;
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Resolves the live pill label (and optionally icon) on button-shaped hosts (Paseo 0.8+), where the
|
|
144
|
-
* pill body is host-rendered from a static `label` and `icon` string and `renderPill`
|
|
145
|
-
* never mounts. Called once at registration and then every
|
|
146
|
-
* `refreshIntervalMs`. Keep it cheap and synchronous when possible; async
|
|
147
|
-
* resolvers are awaited. Returning `undefined` leaves the current label/icon.
|
|
113
|
+
* Resolves the live pill label (and optionally icon). The host renders the
|
|
114
|
+
* pill body itself, so this is the only channel for live pill text. Called
|
|
115
|
+
* once at registration and then every `refreshIntervalMs`. Keep it cheap and
|
|
116
|
+
* synchronous when possible; async resolvers are awaited. Returning
|
|
117
|
+
* `undefined` leaves the current label/icon.
|
|
148
118
|
* Can return a plain string (label) or an object `{ label?: string; icon?: string }`.
|
|
149
119
|
* Cycle modes can advance rotation state on each call.
|
|
150
120
|
*/
|
|
151
121
|
resolveLabel?: PillLabelResolver;
|
|
152
122
|
|
|
153
123
|
/**
|
|
154
|
-
* Optional standalone resolver for the
|
|
155
|
-
*
|
|
124
|
+
* Optional standalone resolver for the pill icon. Evaluated alongside
|
|
125
|
+
* `resolveLabel` on each tick.
|
|
156
126
|
*/
|
|
157
127
|
resolveIcon?: PillIconResolver;
|
|
158
128
|
|
|
159
129
|
/**
|
|
160
|
-
* Poll interval for `resolveLabel
|
|
130
|
+
* Poll interval for `resolveLabel`. Defaults to 5000ms
|
|
161
131
|
* when `resolveLabel` or `resolveIcon` is set. Set to 0 to resolve once at registration.
|
|
162
|
-
* Ignored on legacy hosts (their `renderPill` re-renders via React state).
|
|
163
132
|
*/
|
|
164
133
|
refreshIntervalMs?: number;
|
|
165
134
|
|
|
@@ -178,31 +147,28 @@ export interface RegisterComposerPillOptions<TPayload = any> {
|
|
|
178
147
|
popoverWidth?: number;
|
|
179
148
|
|
|
180
149
|
/**
|
|
181
|
-
* Renders the content inside the
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* popovers, so live pill text comes from `resolveLabel` instead.
|
|
150
|
+
* Renders the content inside the pill's surface.
|
|
151
|
+
* The host owns the surface: with the default `"popover"` presentation it
|
|
152
|
+
* anchors this output to the pill at the host surface width (expect a
|
|
153
|
+
* narrow column, not a wide modal; keep content vertically stacked and
|
|
154
|
+
* reflowing), and with `"centered"` it renders it from the pill's icon.
|
|
155
|
+
* Live pill text therefore comes from `resolveLabel`, not from this renderer.
|
|
188
156
|
*
|
|
189
|
-
*
|
|
190
|
-
* on the modal paths (legacy + centered). Never render another
|
|
191
|
-
* `<Modal.Content>` here — use `HostModalSection` from
|
|
157
|
+
* Never render a host `<Modal.Content>` here — use `HostModalSection` from
|
|
192
158
|
* `paseo-plugin-helper/ui` for fluid content. (`HostModalContent` is only
|
|
193
159
|
* for plugins that open their OWN host `<Modal>`.)
|
|
194
160
|
*/
|
|
195
161
|
renderModal?: (props: RenderModalProps<TPayload>) => ReactNode;
|
|
196
162
|
|
|
197
163
|
/**
|
|
198
|
-
* Host-owned scroll for the
|
|
199
|
-
* - `false` (default
|
|
164
|
+
* Host-owned scroll for the centered modal path (#219).
|
|
165
|
+
* - `false` (default): the wrapper renders
|
|
200
166
|
* `<Modal.Content scrollable={false}>` (bounded dialog) and
|
|
201
167
|
* `ModalBody`-based content owns the one scroller.
|
|
202
168
|
* - `true`: the wrapper renders `<Modal.Content scrollable={true}>` so the
|
|
203
169
|
* host scrolls, and `renderModal` must provide fluid content with NO
|
|
204
170
|
* nested `<Modal.Content>` or scroller (`HostModalSection`).
|
|
205
|
-
* Exactly one `<Modal.Content>` is rendered in both modes. The
|
|
171
|
+
* Exactly one `<Modal.Content>` is rendered in both modes. The popover
|
|
206
172
|
* path is unaffected (plain host-owned container either way).
|
|
207
173
|
*/
|
|
208
174
|
hostScroll?: boolean;
|
|
@@ -228,9 +194,9 @@ export interface RegisterComposerPillOptions<TPayload = any> {
|
|
|
228
194
|
presentation?: "popover" | "centered";
|
|
229
195
|
|
|
230
196
|
/**
|
|
231
|
-
* Called when a pill cannot be registered on the current host
|
|
232
|
-
*
|
|
233
|
-
*
|
|
197
|
+
* Called when a pill cannot be registered on the current host. Reporting
|
|
198
|
+
* instead of throwing keeps the rest of the plugin client alive; render the
|
|
199
|
+
* message in your own panel to make it visible.
|
|
234
200
|
*/
|
|
235
201
|
onError?: (info: { agentId: string; workspaceId: string; error: Error }) => void;
|
|
236
202
|
}
|
|
@@ -238,32 +204,27 @@ export interface RegisterComposerPillOptions<TPayload = any> {
|
|
|
238
204
|
/**
|
|
239
205
|
* Registers an agent-scoped composer pill and modal lifecycle.
|
|
240
206
|
* Manages agent subscription events, unmount cleanup, and pill-to-modal activation.
|
|
241
|
-
*
|
|
242
|
-
* Works against both host generations: legacy `{Component, onPress}` pills
|
|
243
|
-
* (Paseo 0.7 and beta apps) and `button`-descriptor pills (Paseo 0.8+), detected
|
|
244
|
-
* once per call with a throwaway probe registration that is removed immediately.
|
|
245
207
|
*/
|
|
246
208
|
export function registerComposerPill<TPayload = any>(
|
|
247
209
|
client: ComposerPillRegistrar,
|
|
248
210
|
options: RegisterComposerPillOptions<TPayload>,
|
|
249
211
|
): PluginCleanup {
|
|
250
212
|
const { Icon, Modal } = getClientHost();
|
|
251
|
-
const openers = new Map<string, (payload?: TPayload) => void>();
|
|
252
213
|
const pills = new Map<
|
|
253
214
|
string,
|
|
254
215
|
{
|
|
255
216
|
dispose: () => void;
|
|
256
217
|
workspaceId: string;
|
|
257
|
-
registration?:
|
|
218
|
+
registration?: PluginButtonRegistration;
|
|
258
219
|
}
|
|
259
220
|
>();
|
|
260
221
|
const pushedDescriptors = new WeakMap<object, { label?: string; icon?: string }>();
|
|
261
|
-
// A
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
222
|
+
// A pill can only be resolved while the host actually renders it. The host
|
|
223
|
+
// mounts the pill's custom icon component for every visible composer pill, and
|
|
224
|
+
// that mount is the plugin's only visibility signal: its mount/unmount
|
|
225
|
+
// brackets the pill's on-screen lifetime. Polling every registered agent
|
|
226
|
+
// regardless of that signal ran one ticker per agent in the install, which
|
|
227
|
+
// stampedes the daemon on a host with many agents.
|
|
267
228
|
const visiblePillMounts = new Map<string, number>();
|
|
268
229
|
// Agent ids whose anchored popover is currently mounted. While a popover is
|
|
269
230
|
// open the host re-renders the whole composer on every agent-stream message;
|
|
@@ -280,7 +241,6 @@ export function registerComposerPill<TPayload = any>(
|
|
|
280
241
|
const centeredOpenAgents = new Set<string>();
|
|
281
242
|
const centeredListenersByAgent = new Map<string, Set<() => void>>();
|
|
282
243
|
let sharedLabelTimer: ReturnType<typeof setInterval> | null = null;
|
|
283
|
-
let detectedShape: "button" | "legacy" | null = null;
|
|
284
244
|
let disposed = false;
|
|
285
245
|
|
|
286
246
|
function defaultPillIcon(): string {
|
|
@@ -385,22 +345,8 @@ export function registerComposerPill<TPayload = any>(
|
|
|
385
345
|
* `registration.update({ icon })`, so the probe component never gets
|
|
386
346
|
* replaced by a plain string and the mount signal survives every icon swap.
|
|
387
347
|
*/
|
|
388
|
-
function makePillIcon(agentId: string): ComponentType<{
|
|
389
|
-
|
|
390
|
-
color?: string;
|
|
391
|
-
theme?: HostTheme;
|
|
392
|
-
layout?: HostLayout;
|
|
393
|
-
host?: { id: string; label: string };
|
|
394
|
-
workspaceId?: string;
|
|
395
|
-
}> {
|
|
396
|
-
return function PillVisibilityIcon(props: {
|
|
397
|
-
size?: number;
|
|
398
|
-
color?: string;
|
|
399
|
-
theme?: HostTheme;
|
|
400
|
-
layout?: HostLayout;
|
|
401
|
-
host?: { id: string; label: string };
|
|
402
|
-
workspaceId?: string;
|
|
403
|
-
}) {
|
|
348
|
+
function makePillIcon(agentId: string): ComponentType<PluginButtonIconProps> {
|
|
349
|
+
return function PillVisibilityIcon(props: PluginButtonIconProps) {
|
|
404
350
|
const subscribe = useMemo(() => subscribePillIcon(agentId), [agentId]);
|
|
405
351
|
const name = useSyncExternalStore(
|
|
406
352
|
subscribe,
|
|
@@ -426,7 +372,7 @@ export function registerComposerPill<TPayload = any>(
|
|
|
426
372
|
|
|
427
373
|
const icon = <Icon name={name} size={props.size ?? 14} color={props.color ?? ""} />;
|
|
428
374
|
const { theme, layout } = props;
|
|
429
|
-
if (!centered || !options.renderModal
|
|
375
|
+
if (!centered || !options.renderModal) return icon;
|
|
430
376
|
|
|
431
377
|
const modalIconElement = React.isValidElement(options.modalIcon) ? (
|
|
432
378
|
options.modalIcon
|
|
@@ -477,6 +423,7 @@ export function registerComposerPill<TPayload = any>(
|
|
|
477
423
|
onStartShouldSetResponder={() => true}
|
|
478
424
|
onTouchEnd={stopBubbling}
|
|
479
425
|
{...(eventBoundaryProps as any)}
|
|
426
|
+
style={{ flex: 1, minHeight: 0 }}
|
|
480
427
|
>
|
|
481
428
|
<PluginThemeProvider theme={theme} layout={layout} flair={options.flair}>
|
|
482
429
|
<ModalBodyScrollOwnerContext.Provider
|
|
@@ -484,7 +431,7 @@ export function registerComposerPill<TPayload = any>(
|
|
|
484
431
|
>
|
|
485
432
|
{options.renderModal({
|
|
486
433
|
agentId,
|
|
487
|
-
workspaceId: props.workspaceId
|
|
434
|
+
workspaceId: props.workspaceId,
|
|
488
435
|
theme,
|
|
489
436
|
layout,
|
|
490
437
|
host: props.host ?? { id: "", label: "" },
|
|
@@ -502,7 +449,7 @@ export function registerComposerPill<TPayload = any>(
|
|
|
502
449
|
};
|
|
503
450
|
}
|
|
504
451
|
|
|
505
|
-
//
|
|
452
|
+
// Popover scroll ownership: Paseo's MenuSurface/FloatingScrollView or
|
|
506
453
|
// BottomSheetScrollView already owns the viewport. Keep this wrapper plain,
|
|
507
454
|
// unconstrained, and mark the subtree so ModalBody does not add a second
|
|
508
455
|
// ScrollView. Fixed heights or overflow clipping here cut off mobile content.
|
|
@@ -562,98 +509,7 @@ export function registerComposerPill<TPayload = any>(
|
|
|
562
509
|
);
|
|
563
510
|
}
|
|
564
511
|
|
|
565
|
-
function
|
|
566
|
-
const [open, setOpen] = useState(false);
|
|
567
|
-
const [payload, setPayload] = useState<TPayload | undefined>(undefined);
|
|
568
|
-
|
|
569
|
-
useEffect(() => {
|
|
570
|
-
openers.set(props.agentId, (incomingPayload?: TPayload) => {
|
|
571
|
-
setPayload(incomingPayload);
|
|
572
|
-
setOpen(true);
|
|
573
|
-
});
|
|
574
|
-
return () => {
|
|
575
|
-
openers.delete(props.agentId);
|
|
576
|
-
};
|
|
577
|
-
}, [props.agentId]);
|
|
578
|
-
|
|
579
|
-
const effectiveModalTitle = options.modalTitle ?? options.title;
|
|
580
|
-
|
|
581
|
-
const modalIconElement = useMemo(() => {
|
|
582
|
-
if (React.isValidElement(options.modalIcon)) {
|
|
583
|
-
return options.modalIcon;
|
|
584
|
-
}
|
|
585
|
-
const iconName =
|
|
586
|
-
typeof options.modalIcon === "string" ? options.modalIcon : options.icon;
|
|
587
|
-
if (iconName) {
|
|
588
|
-
return <Icon name={iconName} size={16} color={props.theme.colors.foreground} />;
|
|
589
|
-
}
|
|
590
|
-
return undefined;
|
|
591
|
-
}, [options.modalIcon, options.icon, props.theme.colors.foreground]);
|
|
592
|
-
|
|
593
|
-
const renderPillProps: RenderPillProps<TPayload> = {
|
|
594
|
-
...props,
|
|
595
|
-
isOpen: open,
|
|
596
|
-
open: (customPayload?: TPayload) => {
|
|
597
|
-
setPayload(customPayload);
|
|
598
|
-
setOpen(true);
|
|
599
|
-
},
|
|
600
|
-
close: () => setOpen(false),
|
|
601
|
-
toggle: (customPayload?: TPayload) => {
|
|
602
|
-
setPayload(customPayload);
|
|
603
|
-
setOpen((prev) => !prev);
|
|
604
|
-
},
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
return (
|
|
608
|
-
<PluginThemeProvider theme={props.theme} layout={props.layout} flair={options.flair}>
|
|
609
|
-
{options.renderPill ? (
|
|
610
|
-
options.renderPill(renderPillProps)
|
|
611
|
-
) : (
|
|
612
|
-
<DefaultPillBody
|
|
613
|
-
title={options.title}
|
|
614
|
-
compactTitle={options.compactTitle}
|
|
615
|
-
icon={options.icon}
|
|
616
|
-
compactIcon={options.compactIcon}
|
|
617
|
-
badgeText={options.badgeText}
|
|
618
|
-
compactBadgeText={options.compactBadgeText}
|
|
619
|
-
theme={props.theme}
|
|
620
|
-
/>
|
|
621
|
-
)}
|
|
622
|
-
|
|
623
|
-
<Modal
|
|
624
|
-
title={effectiveModalTitle}
|
|
625
|
-
icon={modalIconElement}
|
|
626
|
-
open={open}
|
|
627
|
-
onOpenChange={(nextOpen) => {
|
|
628
|
-
setOpen(nextOpen);
|
|
629
|
-
if (!nextOpen) {
|
|
630
|
-
setPayload(undefined);
|
|
631
|
-
}
|
|
632
|
-
}}
|
|
633
|
-
>
|
|
634
|
-
<Modal.Content scrollable={resolvePillModalScrollable(options.hostScroll)}>
|
|
635
|
-
{open ? (
|
|
636
|
-
<PluginThemeProvider theme={props.theme} layout={props.layout} flair={options.flair}>
|
|
637
|
-
<ModalBodyScrollOwnerContext.Provider
|
|
638
|
-
value={resolvePillModalScrollable(options.hostScroll) ? "host" : "required"}
|
|
639
|
-
>
|
|
640
|
-
{options.renderModal?.({
|
|
641
|
-
...props,
|
|
642
|
-
close: () => setOpen(false),
|
|
643
|
-
payload,
|
|
644
|
-
})}
|
|
645
|
-
</ModalBodyScrollOwnerContext.Provider>
|
|
646
|
-
</PluginThemeProvider>
|
|
647
|
-
) : null}
|
|
648
|
-
</Modal.Content>
|
|
649
|
-
|
|
650
|
-
</Modal>
|
|
651
|
-
</PluginThemeProvider>
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function toCleanup(registration: ComposerPillRegistration): PluginCleanup {
|
|
656
|
-
if (typeof registration === "function") return registration;
|
|
512
|
+
function toCleanup(registration: PluginButtonRegistration): PluginCleanup {
|
|
657
513
|
return () => registration.remove();
|
|
658
514
|
}
|
|
659
515
|
|
|
@@ -675,9 +531,8 @@ export function registerComposerPill<TPayload = any>(
|
|
|
675
531
|
function resolveAndPushLabel(
|
|
676
532
|
agentId: string,
|
|
677
533
|
workspaceId: string,
|
|
678
|
-
registration:
|
|
534
|
+
registration: PluginButtonRegistration,
|
|
679
535
|
): void {
|
|
680
|
-
if (typeof registration === "function") return;
|
|
681
536
|
if (!options.resolveLabel && !options.resolveIcon) return;
|
|
682
537
|
const ctx = { agentId, workspaceId };
|
|
683
538
|
Promise.all([
|
|
@@ -727,83 +582,41 @@ export function registerComposerPill<TPayload = any>(
|
|
|
727
582
|
});
|
|
728
583
|
}
|
|
729
584
|
|
|
730
|
-
function
|
|
731
|
-
|
|
732
|
-
const probeId = `php-probe-${probeSequence}`;
|
|
585
|
+
function addPill(agentId: string, workspaceId: string) {
|
|
586
|
+
if (disposed || pills.has(agentId)) return;
|
|
733
587
|
try {
|
|
734
588
|
const registration = client.addComposerPill({
|
|
735
|
-
id:
|
|
589
|
+
id: options.id,
|
|
736
590
|
workspaceId,
|
|
737
591
|
agentId,
|
|
738
592
|
button: {
|
|
739
|
-
title:
|
|
740
|
-
icon
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
593
|
+
title: options.title,
|
|
594
|
+
// The icon is both the visibility signal and the live-icon renderer.
|
|
595
|
+
icon: makePillIcon(agentId),
|
|
596
|
+
label: options.title,
|
|
597
|
+
behavior: options.onPress
|
|
598
|
+
? { kind: "action", onPress: () => void options.onPress?.() }
|
|
599
|
+
: options.presentation === "centered"
|
|
600
|
+
? {
|
|
601
|
+
kind: "action",
|
|
602
|
+
onPress: () => setCenteredOpen(agentId, !centeredOpenAgents.has(agentId)),
|
|
603
|
+
}
|
|
604
|
+
: {
|
|
605
|
+
kind: "popover",
|
|
606
|
+
Content: PillPopoverContent as ComponentType<PluginButtonContentProps>,
|
|
607
|
+
},
|
|
745
608
|
},
|
|
746
609
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
} catch {
|
|
750
|
-
return "legacy";
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function addPill(agentId: string, workspaceId: string) {
|
|
755
|
-
if (disposed || pills.has(agentId)) return;
|
|
756
|
-
try {
|
|
757
|
-
if (!detectedShape) {
|
|
758
|
-
detectedShape = detectShape(agentId, workspaceId);
|
|
759
|
-
}
|
|
760
|
-
if (detectedShape === "button") {
|
|
761
|
-
const registration = client.addComposerPill({
|
|
762
|
-
id: options.id,
|
|
763
|
-
workspaceId,
|
|
764
|
-
agentId,
|
|
765
|
-
button: {
|
|
766
|
-
title: options.title,
|
|
767
|
-
// The probe is the visibility signal and the live-icon renderer.
|
|
768
|
-
icon: makePillIcon(agentId),
|
|
769
|
-
label: options.title,
|
|
770
|
-
behavior: options.onPress
|
|
771
|
-
? { kind: "action", onPress: () => void options.onPress?.() }
|
|
772
|
-
: options.presentation === "centered"
|
|
773
|
-
? {
|
|
774
|
-
kind: "action",
|
|
775
|
-
onPress: () => setCenteredOpen(agentId, !centeredOpenAgents.has(agentId)),
|
|
776
|
-
}
|
|
777
|
-
: {
|
|
778
|
-
kind: "popover",
|
|
779
|
-
Content: PillPopoverContent as ComponentType<any>,
|
|
780
|
-
},
|
|
781
|
-
},
|
|
782
|
-
});
|
|
783
|
-
const handle = typeof registration === "function" ? undefined : registration;
|
|
784
|
-
pills.set(agentId, { dispose: toCleanup(registration), workspaceId, registration: handle });
|
|
785
|
-
// Resolve once so the label is correct even before the probe mounts;
|
|
786
|
-
// ongoing polling is owned by the shared, visibility-gated timer.
|
|
787
|
-
if ((options.resolveLabel || options.resolveIcon) && handle) {
|
|
788
|
-
resolveAndPushLabel(agentId, workspaceId, handle);
|
|
789
|
-
}
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
const cleanup = client.addComposerPill({
|
|
793
|
-
id: options.id,
|
|
794
|
-
title: options.title,
|
|
610
|
+
pills.set(agentId, {
|
|
611
|
+
dispose: toCleanup(registration),
|
|
795
612
|
workspaceId,
|
|
796
|
-
|
|
797
|
-
Component: PillHost,
|
|
798
|
-
onPress() {
|
|
799
|
-
const opener = openers.get(agentId);
|
|
800
|
-
if (opener) {
|
|
801
|
-
const defaultPayload = options.resolveDefaultPayload?.({ agentId, workspaceId });
|
|
802
|
-
opener(defaultPayload);
|
|
803
|
-
}
|
|
804
|
-
},
|
|
613
|
+
registration,
|
|
805
614
|
});
|
|
806
|
-
|
|
615
|
+
// Resolve once so the label is correct even before the icon mounts;
|
|
616
|
+
// ongoing polling is owned by the shared, visibility-gated timer.
|
|
617
|
+
if (options.resolveLabel || options.resolveIcon) {
|
|
618
|
+
resolveAndPushLabel(agentId, workspaceId, registration);
|
|
619
|
+
}
|
|
807
620
|
} catch (error) {
|
|
808
621
|
reportError(agentId, workspaceId, error);
|
|
809
622
|
}
|
|
@@ -816,7 +629,6 @@ export function registerComposerPill<TPayload = any>(
|
|
|
816
629
|
visiblePillMounts.delete(agentId);
|
|
817
630
|
iconValueByAgent.delete(agentId);
|
|
818
631
|
iconListenersByAgent.delete(agentId);
|
|
819
|
-
openers.delete(agentId);
|
|
820
632
|
syncSharedLabelTimer();
|
|
821
633
|
}
|
|
822
634
|
|
|
@@ -858,76 +670,11 @@ export function registerComposerPill<TPayload = any>(
|
|
|
858
670
|
visiblePillMounts.clear();
|
|
859
671
|
iconValueByAgent.clear();
|
|
860
672
|
iconListenersByAgent.clear();
|
|
861
|
-
openers.clear();
|
|
862
673
|
};
|
|
863
674
|
}
|
|
864
675
|
|
|
865
|
-
interface DefaultPillBodyProps {
|
|
866
|
-
title: string;
|
|
867
|
-
compactTitle?: string;
|
|
868
|
-
icon?: string;
|
|
869
|
-
compactIcon?: string;
|
|
870
|
-
badgeText?: string;
|
|
871
|
-
compactBadgeText?: string;
|
|
872
|
-
theme: HostPillProps["theme"];
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function DefaultPillBody({
|
|
876
|
-
title,
|
|
877
|
-
compactTitle,
|
|
878
|
-
icon,
|
|
879
|
-
compactIcon,
|
|
880
|
-
badgeText,
|
|
881
|
-
compactBadgeText,
|
|
882
|
-
theme,
|
|
883
|
-
}: DefaultPillBodyProps) {
|
|
884
|
-
const { Icon } = getClientHost();
|
|
885
|
-
const { isCompact } = useResponsive();
|
|
886
|
-
|
|
887
|
-
const effectiveTitle = isCompact && compactTitle ? compactTitle : title;
|
|
888
|
-
const effectiveIcon = isCompact && compactIcon ? compactIcon : icon;
|
|
889
|
-
const effectiveBadge =
|
|
890
|
-
isCompact && compactBadgeText !== undefined ? compactBadgeText : badgeText;
|
|
891
|
-
|
|
892
|
-
return (
|
|
893
|
-
<View style={styles.pillContainer}>
|
|
894
|
-
{effectiveIcon && <Icon name={effectiveIcon} size={13} color={theme.colors.foreground} />}
|
|
895
|
-
{effectiveTitle ? (
|
|
896
|
-
<Text style={[styles.title, { color: theme.colors.foreground }]}>{effectiveTitle}</Text>
|
|
897
|
-
) : null}
|
|
898
|
-
{effectiveBadge && (
|
|
899
|
-
<View style={[styles.badge, { backgroundColor: theme.colors.surface1 }]}>
|
|
900
|
-
<Text style={[styles.badgeText, { color: theme.colors.foregroundMuted }]}>
|
|
901
|
-
{effectiveBadge}
|
|
902
|
-
</Text>
|
|
903
|
-
</View>
|
|
904
|
-
)}
|
|
905
|
-
</View>
|
|
906
|
-
);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
676
|
const styles = StyleSheet.create({
|
|
910
677
|
popoverContainer: {
|
|
911
678
|
width: "100%",
|
|
912
679
|
},
|
|
913
|
-
pillContainer: {
|
|
914
|
-
flexDirection: "row",
|
|
915
|
-
alignItems: "center",
|
|
916
|
-
gap: 6,
|
|
917
|
-
paddingHorizontal: 8,
|
|
918
|
-
paddingVertical: 3,
|
|
919
|
-
},
|
|
920
|
-
title: {
|
|
921
|
-
fontSize: 12,
|
|
922
|
-
fontWeight: "500",
|
|
923
|
-
},
|
|
924
|
-
badge: {
|
|
925
|
-
borderRadius: 999,
|
|
926
|
-
paddingHorizontal: 5,
|
|
927
|
-
paddingVertical: 1,
|
|
928
|
-
},
|
|
929
|
-
badgeText: {
|
|
930
|
-
fontSize: 10,
|
|
931
|
-
fontWeight: "600",
|
|
932
|
-
},
|
|
933
680
|
});
|