@xpufx/paseo-top 0.4.2 → 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/README.md +11 -12
- package/client/multi-host-view.tsx +177 -0
- package/client/multi-host.ts +98 -0
- package/client/pill-labels.ts +30 -6
- package/client/pill.tsx +42 -61
- package/client/settings-events.ts +33 -0
- package/client/surface.tsx +10 -9
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +29 -3
- 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 +17 -2
- 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 +2 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +19 -29
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +24 -22
- package/client/vendor/paseo-plugin-helper/panel.tsx +11 -3
- package/client/vendor/paseo-plugin-helper/pill.tsx +135 -339
- package/client/vendor/paseo-plugin-helper/styles/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +550 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +3 -39
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +40 -1
- package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
- package/index.server.ts +7 -0
- package/package.json +12 -3
- package/paseo-plugin.json +1 -1
- 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 +348 -0
- 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
|
@@ -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
|
|
@@ -439,36 +385,71 @@ export function registerComposerPill<TPayload = any>(
|
|
|
439
385
|
color={theme.colors.foreground}
|
|
440
386
|
/>
|
|
441
387
|
);
|
|
388
|
+
const stopBubbling = (e: any) => {
|
|
389
|
+
if (e && typeof e.stopPropagation === "function") {
|
|
390
|
+
e.stopPropagation();
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const eventBoundaryProps = {
|
|
395
|
+
onClick: stopBubbling,
|
|
396
|
+
onMouseDown: stopBubbling,
|
|
397
|
+
onMouseUp: stopBubbling,
|
|
398
|
+
onPointerDown: stopBubbling,
|
|
399
|
+
onPointerUp: stopBubbling,
|
|
400
|
+
onTouchStart: stopBubbling,
|
|
401
|
+
onTouchEnd: stopBubbling,
|
|
402
|
+
onKeyDown: stopBubbling,
|
|
403
|
+
onKeyUp: stopBubbling,
|
|
404
|
+
};
|
|
405
|
+
|
|
442
406
|
return (
|
|
443
407
|
<>
|
|
444
408
|
{icon}
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
onOpenChange={(nextOpen: boolean) => setCenteredOpen(agentId, nextOpen)}
|
|
409
|
+
<View
|
|
410
|
+
onStartShouldSetResponder={() => true}
|
|
411
|
+
onTouchEnd={stopBubbling}
|
|
412
|
+
{...(eventBoundaryProps as any)}
|
|
450
413
|
>
|
|
451
|
-
<Modal
|
|
452
|
-
{
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
414
|
+
<Modal
|
|
415
|
+
title={options.modalTitle ?? options.title}
|
|
416
|
+
icon={modalIconElement}
|
|
417
|
+
open={centeredOpen}
|
|
418
|
+
onOpenChange={(nextOpen: boolean) => setCenteredOpen(agentId, nextOpen)}
|
|
419
|
+
>
|
|
420
|
+
<Modal.Content scrollable={resolvePillModalScrollable(options.hostScroll)}>
|
|
421
|
+
{centeredOpen ? (
|
|
422
|
+
<View
|
|
423
|
+
onStartShouldSetResponder={() => true}
|
|
424
|
+
onTouchEnd={stopBubbling}
|
|
425
|
+
{...(eventBoundaryProps as any)}
|
|
426
|
+
style={{ flex: 1, minHeight: 0 }}
|
|
427
|
+
>
|
|
428
|
+
<PluginThemeProvider theme={theme} layout={layout} flair={options.flair}>
|
|
429
|
+
<ModalBodyScrollOwnerContext.Provider
|
|
430
|
+
value={resolvePillModalScrollable(options.hostScroll) ? "host" : "required"}
|
|
431
|
+
>
|
|
432
|
+
{options.renderModal({
|
|
433
|
+
agentId,
|
|
434
|
+
workspaceId: props.workspaceId,
|
|
435
|
+
theme,
|
|
436
|
+
layout,
|
|
437
|
+
host: props.host ?? { id: "", label: "" },
|
|
438
|
+
close: () => setCenteredOpen(agentId, false),
|
|
439
|
+
})}
|
|
440
|
+
</ModalBodyScrollOwnerContext.Provider>
|
|
441
|
+
</PluginThemeProvider>
|
|
442
|
+
</View>
|
|
443
|
+
) : null}
|
|
444
|
+
</Modal.Content>
|
|
445
|
+
</Modal>
|
|
446
|
+
</View>
|
|
466
447
|
</>
|
|
467
448
|
);
|
|
468
449
|
};
|
|
469
450
|
}
|
|
470
451
|
|
|
471
|
-
//
|
|
452
|
+
// Popover scroll ownership: Paseo's MenuSurface/FloatingScrollView or
|
|
472
453
|
// BottomSheetScrollView already owns the viewport. Keep this wrapper plain,
|
|
473
454
|
// unconstrained, and mark the subtree so ModalBody does not add a second
|
|
474
455
|
// ScrollView. Fixed heights or overflow clipping here cut off mobile content.
|
|
@@ -480,7 +461,7 @@ export function registerComposerPill<TPayload = any>(
|
|
|
480
461
|
host?: { id: string; label: string };
|
|
481
462
|
close: () => void;
|
|
482
463
|
}) {
|
|
483
|
-
const { width: windowWidth } = useWindowDimensions();
|
|
464
|
+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
484
465
|
// The anchored popover is otherwise sized from its content, so reflowing
|
|
485
466
|
// content can move the frame under the pointer. Pin it to an explicit width
|
|
486
467
|
// (clamped to the viewport) and let children overflow into their own
|
|
@@ -489,6 +470,11 @@ export function registerComposerPill<TPayload = any>(
|
|
|
489
470
|
!props.layout.compact && options.popoverWidth
|
|
490
471
|
? Math.max(0, Math.min(options.popoverWidth, windowWidth - 24))
|
|
491
472
|
: 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));
|
|
492
478
|
// Layout effect, not effect: the flag must be set in the same commit the
|
|
493
479
|
// popover mounts, before any label timer can interleave. An effect runs
|
|
494
480
|
// after paint, leaving a window where one tick can still publish and
|
|
@@ -508,8 +494,14 @@ export function registerComposerPill<TPayload = any>(
|
|
|
508
494
|
};
|
|
509
495
|
return (
|
|
510
496
|
<PluginThemeProvider theme={props.theme} layout={props.layout} flair={options.flair}>
|
|
511
|
-
<View
|
|
512
|
-
|
|
497
|
+
<View
|
|
498
|
+
style={{
|
|
499
|
+
...styles.popoverContainer,
|
|
500
|
+
maxHeight: frameMaxHeight,
|
|
501
|
+
...(frameWidth ? { width: frameWidth } : null),
|
|
502
|
+
}}
|
|
503
|
+
>
|
|
504
|
+
<ModalBodyScrollOwnerContext.Provider value="popover">
|
|
513
505
|
{options.renderModal?.({ ...pillProps, close: props.close })}
|
|
514
506
|
</ModalBodyScrollOwnerContext.Provider>
|
|
515
507
|
</View>
|
|
@@ -517,94 +509,7 @@ export function registerComposerPill<TPayload = any>(
|
|
|
517
509
|
);
|
|
518
510
|
}
|
|
519
511
|
|
|
520
|
-
function
|
|
521
|
-
const [open, setOpen] = useState(false);
|
|
522
|
-
const [payload, setPayload] = useState<TPayload | undefined>(undefined);
|
|
523
|
-
|
|
524
|
-
useEffect(() => {
|
|
525
|
-
openers.set(props.agentId, (incomingPayload?: TPayload) => {
|
|
526
|
-
setPayload(incomingPayload);
|
|
527
|
-
setOpen(true);
|
|
528
|
-
});
|
|
529
|
-
return () => {
|
|
530
|
-
openers.delete(props.agentId);
|
|
531
|
-
};
|
|
532
|
-
}, [props.agentId]);
|
|
533
|
-
|
|
534
|
-
const effectiveModalTitle = options.modalTitle ?? options.title;
|
|
535
|
-
|
|
536
|
-
const modalIconElement = useMemo(() => {
|
|
537
|
-
if (React.isValidElement(options.modalIcon)) {
|
|
538
|
-
return options.modalIcon;
|
|
539
|
-
}
|
|
540
|
-
const iconName =
|
|
541
|
-
typeof options.modalIcon === "string" ? options.modalIcon : options.icon;
|
|
542
|
-
if (iconName) {
|
|
543
|
-
return <Icon name={iconName} size={16} color={props.theme.colors.foreground} />;
|
|
544
|
-
}
|
|
545
|
-
return undefined;
|
|
546
|
-
}, [options.modalIcon, options.icon, props.theme.colors.foreground]);
|
|
547
|
-
|
|
548
|
-
const renderPillProps: RenderPillProps<TPayload> = {
|
|
549
|
-
...props,
|
|
550
|
-
isOpen: open,
|
|
551
|
-
open: (customPayload?: TPayload) => {
|
|
552
|
-
setPayload(customPayload);
|
|
553
|
-
setOpen(true);
|
|
554
|
-
},
|
|
555
|
-
close: () => setOpen(false),
|
|
556
|
-
toggle: (customPayload?: TPayload) => {
|
|
557
|
-
setPayload(customPayload);
|
|
558
|
-
setOpen((prev) => !prev);
|
|
559
|
-
},
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
return (
|
|
563
|
-
<PluginThemeProvider theme={props.theme} layout={props.layout} flair={options.flair}>
|
|
564
|
-
{options.renderPill ? (
|
|
565
|
-
options.renderPill(renderPillProps)
|
|
566
|
-
) : (
|
|
567
|
-
<DefaultPillBody
|
|
568
|
-
title={options.title}
|
|
569
|
-
compactTitle={options.compactTitle}
|
|
570
|
-
icon={options.icon}
|
|
571
|
-
compactIcon={options.compactIcon}
|
|
572
|
-
badgeText={options.badgeText}
|
|
573
|
-
compactBadgeText={options.compactBadgeText}
|
|
574
|
-
theme={props.theme}
|
|
575
|
-
/>
|
|
576
|
-
)}
|
|
577
|
-
|
|
578
|
-
<Modal
|
|
579
|
-
title={effectiveModalTitle}
|
|
580
|
-
icon={modalIconElement}
|
|
581
|
-
open={open}
|
|
582
|
-
onOpenChange={(nextOpen) => {
|
|
583
|
-
setOpen(nextOpen);
|
|
584
|
-
if (!nextOpen) {
|
|
585
|
-
setPayload(undefined);
|
|
586
|
-
}
|
|
587
|
-
}}
|
|
588
|
-
>
|
|
589
|
-
<Modal.Content scrollable={resolvePillModalScrollable(options.hostScroll)}>
|
|
590
|
-
{open ? (
|
|
591
|
-
<PluginThemeProvider theme={props.theme} layout={props.layout} flair={options.flair}>
|
|
592
|
-
{options.renderModal?.({
|
|
593
|
-
...props,
|
|
594
|
-
close: () => setOpen(false),
|
|
595
|
-
payload,
|
|
596
|
-
})}
|
|
597
|
-
</PluginThemeProvider>
|
|
598
|
-
) : null}
|
|
599
|
-
</Modal.Content>
|
|
600
|
-
|
|
601
|
-
</Modal>
|
|
602
|
-
</PluginThemeProvider>
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function toCleanup(registration: ComposerPillRegistration): PluginCleanup {
|
|
607
|
-
if (typeof registration === "function") return registration;
|
|
512
|
+
function toCleanup(registration: PluginButtonRegistration): PluginCleanup {
|
|
608
513
|
return () => registration.remove();
|
|
609
514
|
}
|
|
610
515
|
|
|
@@ -626,9 +531,8 @@ export function registerComposerPill<TPayload = any>(
|
|
|
626
531
|
function resolveAndPushLabel(
|
|
627
532
|
agentId: string,
|
|
628
533
|
workspaceId: string,
|
|
629
|
-
registration:
|
|
534
|
+
registration: PluginButtonRegistration,
|
|
630
535
|
): void {
|
|
631
|
-
if (typeof registration === "function") return;
|
|
632
536
|
if (!options.resolveLabel && !options.resolveIcon) return;
|
|
633
537
|
const ctx = { agentId, workspaceId };
|
|
634
538
|
Promise.all([
|
|
@@ -678,83 +582,41 @@ export function registerComposerPill<TPayload = any>(
|
|
|
678
582
|
});
|
|
679
583
|
}
|
|
680
584
|
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
const probeId = `php-probe-${probeSequence}`;
|
|
585
|
+
function addPill(agentId: string, workspaceId: string) {
|
|
586
|
+
if (disposed || pills.has(agentId)) return;
|
|
684
587
|
try {
|
|
685
588
|
const registration = client.addComposerPill({
|
|
686
|
-
id:
|
|
589
|
+
id: options.id,
|
|
687
590
|
workspaceId,
|
|
688
591
|
agentId,
|
|
689
592
|
button: {
|
|
690
|
-
title:
|
|
691
|
-
icon
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
+
},
|
|
696
608
|
},
|
|
697
609
|
});
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
} catch {
|
|
701
|
-
return "legacy";
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function addPill(agentId: string, workspaceId: string) {
|
|
706
|
-
if (disposed || pills.has(agentId)) return;
|
|
707
|
-
try {
|
|
708
|
-
if (!detectedShape) {
|
|
709
|
-
detectedShape = detectShape(agentId, workspaceId);
|
|
710
|
-
}
|
|
711
|
-
if (detectedShape === "button") {
|
|
712
|
-
const registration = client.addComposerPill({
|
|
713
|
-
id: options.id,
|
|
714
|
-
workspaceId,
|
|
715
|
-
agentId,
|
|
716
|
-
button: {
|
|
717
|
-
title: options.title,
|
|
718
|
-
// The probe is the visibility signal and the live-icon renderer.
|
|
719
|
-
icon: makePillIcon(agentId),
|
|
720
|
-
label: options.title,
|
|
721
|
-
behavior: options.onPress
|
|
722
|
-
? { kind: "action", onPress: () => void options.onPress?.() }
|
|
723
|
-
: options.presentation === "centered"
|
|
724
|
-
? {
|
|
725
|
-
kind: "action",
|
|
726
|
-
onPress: () => setCenteredOpen(agentId, !centeredOpenAgents.has(agentId)),
|
|
727
|
-
}
|
|
728
|
-
: {
|
|
729
|
-
kind: "popover",
|
|
730
|
-
Content: PillPopoverContent as ComponentType<any>,
|
|
731
|
-
},
|
|
732
|
-
},
|
|
733
|
-
});
|
|
734
|
-
const handle = typeof registration === "function" ? undefined : registration;
|
|
735
|
-
pills.set(agentId, { dispose: toCleanup(registration), workspaceId, registration: handle });
|
|
736
|
-
// Resolve once so the label is correct even before the probe mounts;
|
|
737
|
-
// ongoing polling is owned by the shared, visibility-gated timer.
|
|
738
|
-
if ((options.resolveLabel || options.resolveIcon) && handle) {
|
|
739
|
-
resolveAndPushLabel(agentId, workspaceId, handle);
|
|
740
|
-
}
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
const cleanup = client.addComposerPill({
|
|
744
|
-
id: options.id,
|
|
745
|
-
title: options.title,
|
|
610
|
+
pills.set(agentId, {
|
|
611
|
+
dispose: toCleanup(registration),
|
|
746
612
|
workspaceId,
|
|
747
|
-
|
|
748
|
-
Component: PillHost,
|
|
749
|
-
onPress() {
|
|
750
|
-
const opener = openers.get(agentId);
|
|
751
|
-
if (opener) {
|
|
752
|
-
const defaultPayload = options.resolveDefaultPayload?.({ agentId, workspaceId });
|
|
753
|
-
opener(defaultPayload);
|
|
754
|
-
}
|
|
755
|
-
},
|
|
613
|
+
registration,
|
|
756
614
|
});
|
|
757
|
-
|
|
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
|
+
}
|
|
758
620
|
} catch (error) {
|
|
759
621
|
reportError(agentId, workspaceId, error);
|
|
760
622
|
}
|
|
@@ -767,7 +629,6 @@ export function registerComposerPill<TPayload = any>(
|
|
|
767
629
|
visiblePillMounts.delete(agentId);
|
|
768
630
|
iconValueByAgent.delete(agentId);
|
|
769
631
|
iconListenersByAgent.delete(agentId);
|
|
770
|
-
openers.delete(agentId);
|
|
771
632
|
syncSharedLabelTimer();
|
|
772
633
|
}
|
|
773
634
|
|
|
@@ -809,76 +670,11 @@ export function registerComposerPill<TPayload = any>(
|
|
|
809
670
|
visiblePillMounts.clear();
|
|
810
671
|
iconValueByAgent.clear();
|
|
811
672
|
iconListenersByAgent.clear();
|
|
812
|
-
openers.clear();
|
|
813
673
|
};
|
|
814
674
|
}
|
|
815
675
|
|
|
816
|
-
interface DefaultPillBodyProps {
|
|
817
|
-
title: string;
|
|
818
|
-
compactTitle?: string;
|
|
819
|
-
icon?: string;
|
|
820
|
-
compactIcon?: string;
|
|
821
|
-
badgeText?: string;
|
|
822
|
-
compactBadgeText?: string;
|
|
823
|
-
theme: HostPillProps["theme"];
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function DefaultPillBody({
|
|
827
|
-
title,
|
|
828
|
-
compactTitle,
|
|
829
|
-
icon,
|
|
830
|
-
compactIcon,
|
|
831
|
-
badgeText,
|
|
832
|
-
compactBadgeText,
|
|
833
|
-
theme,
|
|
834
|
-
}: DefaultPillBodyProps) {
|
|
835
|
-
const { Icon } = getClientHost();
|
|
836
|
-
const { isCompact } = useResponsive();
|
|
837
|
-
|
|
838
|
-
const effectiveTitle = isCompact && compactTitle ? compactTitle : title;
|
|
839
|
-
const effectiveIcon = isCompact && compactIcon ? compactIcon : icon;
|
|
840
|
-
const effectiveBadge =
|
|
841
|
-
isCompact && compactBadgeText !== undefined ? compactBadgeText : badgeText;
|
|
842
|
-
|
|
843
|
-
return (
|
|
844
|
-
<View style={styles.pillContainer}>
|
|
845
|
-
{effectiveIcon && <Icon name={effectiveIcon} size={13} color={theme.colors.foreground} />}
|
|
846
|
-
{effectiveTitle ? (
|
|
847
|
-
<Text style={[styles.title, { color: theme.colors.foreground }]}>{effectiveTitle}</Text>
|
|
848
|
-
) : null}
|
|
849
|
-
{effectiveBadge && (
|
|
850
|
-
<View style={[styles.badge, { backgroundColor: theme.colors.surface1 }]}>
|
|
851
|
-
<Text style={[styles.badgeText, { color: theme.colors.foregroundMuted }]}>
|
|
852
|
-
{effectiveBadge}
|
|
853
|
-
</Text>
|
|
854
|
-
</View>
|
|
855
|
-
)}
|
|
856
|
-
</View>
|
|
857
|
-
);
|
|
858
|
-
}
|
|
859
|
-
|
|
860
676
|
const styles = StyleSheet.create({
|
|
861
677
|
popoverContainer: {
|
|
862
678
|
width: "100%",
|
|
863
679
|
},
|
|
864
|
-
pillContainer: {
|
|
865
|
-
flexDirection: "row",
|
|
866
|
-
alignItems: "center",
|
|
867
|
-
gap: 6,
|
|
868
|
-
paddingHorizontal: 8,
|
|
869
|
-
paddingVertical: 3,
|
|
870
|
-
},
|
|
871
|
-
title: {
|
|
872
|
-
fontSize: 12,
|
|
873
|
-
fontWeight: "500",
|
|
874
|
-
},
|
|
875
|
-
badge: {
|
|
876
|
-
borderRadius: 999,
|
|
877
|
-
paddingHorizontal: 5,
|
|
878
|
-
paddingVertical: 1,
|
|
879
|
-
},
|
|
880
|
-
badgeText: {
|
|
881
|
-
fontSize: 10,
|
|
882
|
-
fontWeight: "600",
|
|
883
|
-
},
|
|
884
680
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./recipes";
|