@xpufx/paseo-helper-demo 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/client/pill.tsx +1249 -0
- package/client/suite-settings.tsx +77 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/core/index.ts +64 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/ui/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/ui/modal.tsx +105 -0
- package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/package.json +35 -0
- package/paseo-plugin.json +6 -0
- package/server/demo.ts +195 -0
- package/server/suite-settings.ts +23 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/demo.ts +155 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- package/shared/version.ts +2 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ScrollView as FallbackScrollView,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
View,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
getClientHost,
|
|
11
|
+
getOptionalClientHost,
|
|
12
|
+
selectHostScrollView,
|
|
13
|
+
type HostScrollView,
|
|
14
|
+
} from "../host";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Host-delegating modal content for `paseo-plugin-helper/ui`.
|
|
18
|
+
*
|
|
19
|
+
* Contract — ONLY for content rendered directly inside your OWN host
|
|
20
|
+
* `<Modal>` (e.g. x-comms style own-modal surfaces):
|
|
21
|
+
* - Scroll ownership stays with the host: renders the injected host
|
|
22
|
+
* `<Modal.Content>` with its default `scrollable` behavior and adds NO
|
|
23
|
+
* helper-owned scroller, so sheet gestures, keyboard avoidance, and
|
|
24
|
+
* safe-area clearance keep working on compact/mobile and desktop alike.
|
|
25
|
+
* - No width opinions: there is deliberately NO `maxContentWidth` /
|
|
26
|
+
* `size="large"` prop. The host allocates the dialog frame; this wrapper
|
|
27
|
+
* fills it fluidly (`width: "100%"`).
|
|
28
|
+
* - No theme scraping: colors come from the host `theme` prop via
|
|
29
|
+
* `PluginThemeProvider`, never from DOM CSS variables.
|
|
30
|
+
* - NEVER render this inside `registerComposerPill` `renderModal`: the pill
|
|
31
|
+
* host already provides the one `<Modal.Content>` (or no modal at all on
|
|
32
|
+
* 0.8 popovers), so a nested `<Modal.Content>` violates the host contract
|
|
33
|
+
* and crashes the plugin. Use `HostModalSection` there instead.
|
|
34
|
+
*/
|
|
35
|
+
export function HostModalContent({
|
|
36
|
+
children,
|
|
37
|
+
style,
|
|
38
|
+
contentContainerStyle,
|
|
39
|
+
}: {
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
style?: StyleProp<ViewStyle>;
|
|
42
|
+
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
43
|
+
}) {
|
|
44
|
+
const { Modal } = getClientHost();
|
|
45
|
+
return (
|
|
46
|
+
<Modal.Content>
|
|
47
|
+
<View style={[styles.fluid, style, contentContainerStyle]}>{children}</View>
|
|
48
|
+
</Modal.Content>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Explicit single scroll owner for surfaces where the host supplies NO
|
|
54
|
+
* scroller (sidebar surfaces, settings screens). Prefers the injected host
|
|
55
|
+
* ScrollView (sheet-gesture integrated on Paseo v0.8) and falls back to plain
|
|
56
|
+
* React Native ScrollView. Callers own the decision to scroll; this component
|
|
57
|
+
* never nests itself inside another scroller.
|
|
58
|
+
*/
|
|
59
|
+
export function HostScroll({
|
|
60
|
+
children,
|
|
61
|
+
style,
|
|
62
|
+
contentContainerStyle,
|
|
63
|
+
}: {
|
|
64
|
+
children: ReactNode;
|
|
65
|
+
style?: StyleProp<ViewStyle>;
|
|
66
|
+
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
67
|
+
}) {
|
|
68
|
+
const ResolvedScrollView = selectHostScrollView(
|
|
69
|
+
getOptionalClientHost(),
|
|
70
|
+
FallbackScrollView as unknown as HostScrollView,
|
|
71
|
+
);
|
|
72
|
+
return (
|
|
73
|
+
<ResolvedScrollView style={[styles.fluid, style]} contentContainerStyle={contentContainerStyle}>
|
|
74
|
+
{children}
|
|
75
|
+
</ResolvedScrollView>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const styles = StyleSheet.create({
|
|
80
|
+
fluid: {
|
|
81
|
+
width: "100%",
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fluid inner content for pill-embedded modals (`registerComposerPill`
|
|
87
|
+
* `renderModal`).
|
|
88
|
+
*
|
|
89
|
+
* The pill host already provides the one `<Modal.Content>` (legacy/centered
|
|
90
|
+
* modal paths) or no modal at all (0.8 popover path, where the host owns the
|
|
91
|
+
* outer scroll). This renders a plain fluid `<View>` — no `<Modal.Content>`,
|
|
92
|
+
* no scroller, no width caps — so exactly one `<Modal.Content>` exists and
|
|
93
|
+
* nothing nests. Pair with `hostScroll: true` on the pill registration so
|
|
94
|
+
* the wrapper leaves scrolling to the host; without it the wrapper bounds
|
|
95
|
+
* the dialog (`scrollable={false}`) for legacy `ModalBody` content.
|
|
96
|
+
*/
|
|
97
|
+
export function HostModalSection({
|
|
98
|
+
children,
|
|
99
|
+
style,
|
|
100
|
+
}: {
|
|
101
|
+
children: ReactNode;
|
|
102
|
+
style?: StyleProp<ViewStyle>;
|
|
103
|
+
}) {
|
|
104
|
+
return <View style={[styles.fluid, style]}>{children}</View>;
|
|
105
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paseo Plugin Helper — Settings UI adapters.
|
|
3
|
+
*
|
|
4
|
+
* These re-export the bundle-driven settings renderer from `client/`, whose
|
|
5
|
+
* entire contract is already upstream-shaped: the caller passes the host's
|
|
6
|
+
* native `@getpaseo/plugin/client/ui` primitives
|
|
7
|
+
* (`SettingsSection`, `SettingsCard`, `SettingsSwitch`, `SettingsSelect`,
|
|
8
|
+
* `SettingsInput`) as the `ui` bundle, and the helper only maps a zod
|
|
9
|
+
* settings contract to fields — no bespoke switches, rows, or cards.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import * as UpstreamUi from "@getpaseo/plugin/client/ui";
|
|
13
|
+
* registerHelperSettingsScreen(client, contract, { ui: UpstreamUi });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
contractSchemaToFields,
|
|
19
|
+
registerHelperSettingsScreen,
|
|
20
|
+
} from "../settings-screen";
|
|
21
|
+
export type {
|
|
22
|
+
HelperSettingsUiBundle,
|
|
23
|
+
HelperSettingsCardProps,
|
|
24
|
+
HelperSettingsSectionProps,
|
|
25
|
+
HelperSettingsRowBaseProps,
|
|
26
|
+
HelperSettingsSwitchProps,
|
|
27
|
+
HelperSettingsSelectProps,
|
|
28
|
+
HelperSettingsInputProps,
|
|
29
|
+
HelperSettingsSelectComponent,
|
|
30
|
+
HelperSettingsScreenContribution,
|
|
31
|
+
HelperSettingsScreenRegistrar,
|
|
32
|
+
HelperSettingsFieldKind,
|
|
33
|
+
HelperSettingsField,
|
|
34
|
+
HelperSettingsFieldOverrides,
|
|
35
|
+
RegisterHelperSettingsScreenOptions,
|
|
36
|
+
} from "../settings-screen";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { HostToast } from "../host";
|
|
2
|
+
import { getOptionalClientHost } from "../host";
|
|
3
|
+
|
|
4
|
+
export interface CopyToClipboardOptions {
|
|
5
|
+
toast?: HostToast;
|
|
6
|
+
toastMessage?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ClipboardTier = "navigator" | "host" | "rnAsync" | "rnSync" | "execCommand";
|
|
10
|
+
|
|
11
|
+
export interface ClipboardEnvironment {
|
|
12
|
+
hasNavigatorClipboard: boolean;
|
|
13
|
+
hasHostCopyText: boolean;
|
|
14
|
+
hasRnClipboard: boolean;
|
|
15
|
+
hasRnSetStringAsync: boolean;
|
|
16
|
+
isDom: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic tier order for an explicit copy.
|
|
21
|
+
*
|
|
22
|
+
* Web's `navigator.clipboard.writeText` is the only API that rejects when the
|
|
23
|
+
* clipboard did not change, so it leads. The synchronous
|
|
24
|
+
* `react-native-web` `Clipboard.setString` reports success even when its
|
|
25
|
+
* `document.execCommand("copy")` fails, which leaves the previous clipboard
|
|
26
|
+
* item in place while the UI claims success (xpufx-org/paseo#278); it is only
|
|
27
|
+
* usable off-DOM (native), where it is the real platform clipboard. In a DOM
|
|
28
|
+
* the checked `execCommand` fallback is preferred to that unverifiable path.
|
|
29
|
+
*/
|
|
30
|
+
export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
|
|
31
|
+
const tiers: ClipboardTier[] = [];
|
|
32
|
+
if (env.hasNavigatorClipboard) tiers.push("navigator");
|
|
33
|
+
if (env.hasHostCopyText) tiers.push("host");
|
|
34
|
+
if (env.hasRnSetStringAsync) {
|
|
35
|
+
tiers.push("rnAsync");
|
|
36
|
+
} else if (env.hasRnClipboard && !env.isDom) {
|
|
37
|
+
tiers.push("rnSync");
|
|
38
|
+
}
|
|
39
|
+
tiers.push("execCommand");
|
|
40
|
+
return tiers;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Robust cross-platform clipboard copy helper for Paseo plugins.
|
|
45
|
+
* Works seamlessly across React Native (mobile), web, and desktop.
|
|
46
|
+
*
|
|
47
|
+
* Tier order comes from `clipboardTierOrder`; every tier reports failure
|
|
48
|
+
* honestly so a denied or blocked write never leaves the previous clipboard
|
|
49
|
+
* item behind under a fake success.
|
|
50
|
+
*/
|
|
51
|
+
export async function copyToClipboard(
|
|
52
|
+
text: string,
|
|
53
|
+
options?: CopyToClipboardOptions,
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
if (text === null || text === undefined) return false;
|
|
56
|
+
const str = String(text);
|
|
57
|
+
|
|
58
|
+
const globalObj = typeof globalThis !== "undefined" ? (globalThis as any) : {};
|
|
59
|
+
|
|
60
|
+
let rn: any;
|
|
61
|
+
try {
|
|
62
|
+
rn = require("react-native");
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore require error if not in RN context
|
|
65
|
+
}
|
|
66
|
+
const rnClipboard = rn?.Clipboard;
|
|
67
|
+
const rnSetStringAsync =
|
|
68
|
+
typeof rnClipboard?.setStringAsync === "function" ? rnClipboard.setStringAsync : undefined;
|
|
69
|
+
const rnSetString =
|
|
70
|
+
typeof rnClipboard?.setString === "function" ? rnClipboard.setString : undefined;
|
|
71
|
+
const copyText = getOptionalClientHost()?.copyText;
|
|
72
|
+
|
|
73
|
+
const isDom =
|
|
74
|
+
typeof globalObj.document !== "undefined" &&
|
|
75
|
+
typeof globalObj.document?.createElement === "function";
|
|
76
|
+
|
|
77
|
+
const tiers = clipboardTierOrder({
|
|
78
|
+
hasNavigatorClipboard: Boolean(globalObj.navigator?.clipboard?.writeText),
|
|
79
|
+
hasHostCopyText: Boolean(copyText),
|
|
80
|
+
hasRnClipboard: Boolean(rnSetStringAsync ?? rnSetString),
|
|
81
|
+
hasRnSetStringAsync: Boolean(rnSetStringAsync),
|
|
82
|
+
isDom,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let success = false;
|
|
86
|
+
for (const tier of tiers) {
|
|
87
|
+
if (success) break;
|
|
88
|
+
try {
|
|
89
|
+
switch (tier) {
|
|
90
|
+
case "navigator":
|
|
91
|
+
await globalObj.navigator.clipboard.writeText(str);
|
|
92
|
+
success = true;
|
|
93
|
+
break;
|
|
94
|
+
case "host":
|
|
95
|
+
if (copyText) {
|
|
96
|
+
await copyText(str);
|
|
97
|
+
success = true;
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "rnAsync":
|
|
101
|
+
// Expo's async API returns a boolean; a rejected promise falls through.
|
|
102
|
+
success = (await rnSetStringAsync(str)) !== false;
|
|
103
|
+
break;
|
|
104
|
+
case "rnSync":
|
|
105
|
+
// Only reached off-DOM, where this is the native platform clipboard.
|
|
106
|
+
rnSetString(str);
|
|
107
|
+
success = true;
|
|
108
|
+
break;
|
|
109
|
+
case "execCommand": {
|
|
110
|
+
const doc = globalObj.document;
|
|
111
|
+
if (doc?.createElement && doc?.body) {
|
|
112
|
+
const textarea = doc.createElement("textarea");
|
|
113
|
+
textarea.value = str;
|
|
114
|
+
textarea.style.position = "fixed";
|
|
115
|
+
textarea.style.opacity = "0";
|
|
116
|
+
textarea.style.left = "-9999px";
|
|
117
|
+
doc.body.appendChild(textarea);
|
|
118
|
+
textarea.focus();
|
|
119
|
+
textarea.select();
|
|
120
|
+
const res = doc.execCommand("copy");
|
|
121
|
+
doc.body.removeChild(textarea);
|
|
122
|
+
success = res === true;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Tier failed; try the next one.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (success && options?.toast) {
|
|
133
|
+
try {
|
|
134
|
+
const toastAny = options.toast as any;
|
|
135
|
+
if (typeof toastAny.copied === "function") {
|
|
136
|
+
toastAny.copied(options.toastMessage);
|
|
137
|
+
} else if (typeof toastAny.show === "function") {
|
|
138
|
+
const msg = options.toastMessage
|
|
139
|
+
? `Copied ${options.toastMessage} to clipboard`
|
|
140
|
+
: "Copied to clipboard";
|
|
141
|
+
toastAny.show(msg, { variant: "success" });
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore toast failure
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return success;
|
|
149
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type HapticFeedbackType =
|
|
2
|
+
| "light"
|
|
3
|
+
| "medium"
|
|
4
|
+
| "heavy"
|
|
5
|
+
| "success"
|
|
6
|
+
| "warning"
|
|
7
|
+
| "error";
|
|
8
|
+
|
|
9
|
+
const PATTERNS: Record<HapticFeedbackType, number | number[]> = {
|
|
10
|
+
light: 10,
|
|
11
|
+
medium: 25,
|
|
12
|
+
heavy: 45,
|
|
13
|
+
success: [15, 40, 20],
|
|
14
|
+
warning: [30, 50, 30],
|
|
15
|
+
error: [40, 60, 40, 60, 40],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cross-platform haptic feedback helper for Paseo plugins.
|
|
20
|
+
* Supports web vibration API and graceful fallback when vibration is unavailable.
|
|
21
|
+
*/
|
|
22
|
+
export function triggerHaptic(type: HapticFeedbackType = "light"): boolean {
|
|
23
|
+
try {
|
|
24
|
+
const globalObj = typeof globalThis !== "undefined" ? (globalThis as any) : {};
|
|
25
|
+
const nav = globalObj.navigator;
|
|
26
|
+
if (typeof nav?.vibrate === "function") {
|
|
27
|
+
const pattern = PATTERNS[type] ?? 15;
|
|
28
|
+
return Boolean(nav.vibrate(pattern));
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignore environments where vibration permissions are denied or unavailable
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xpufx/paseo-helper-demo",
|
|
3
|
+
"private": false,
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"stamp": "node -e \"import('./server/vendor/paseo-plugin-helper/version.ts').then(m => m.stampVersion({ targetFile: 'shared/version.ts' }))\"",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@getpaseo/plugin": "0.9.0-beta.2",
|
|
12
|
+
"@tanstack/react-query": "^5.90.11",
|
|
13
|
+
"@types/react": "^19.0.10",
|
|
14
|
+
"react": "19.1.0",
|
|
15
|
+
"react-native": "0.81.5",
|
|
16
|
+
"typescript": "^5.9.3",
|
|
17
|
+
"zod": "^4.4.3"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"paseo-plugin.json",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"client",
|
|
24
|
+
"server",
|
|
25
|
+
"shared",
|
|
26
|
+
"!**/*.test.ts",
|
|
27
|
+
"!**/*.test.tsx",
|
|
28
|
+
"!**/*.test.mjs",
|
|
29
|
+
"!**/.DS_Store"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/xpufx/paseo.git"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/server/demo.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPluginLogger,
|
|
3
|
+
createPeriodicTask,
|
|
4
|
+
findAvailablePort,
|
|
5
|
+
getSystemMetrics,
|
|
6
|
+
getAgentIdentity,
|
|
7
|
+
PluginStorage,
|
|
8
|
+
createSettingsHandlers,
|
|
9
|
+
createWorkspaceBeacon,
|
|
10
|
+
type BeaconDaemonClient,
|
|
11
|
+
} from "paseo-plugin-helper/server";
|
|
12
|
+
import type { PluginHandlerContext } from "@getpaseo/plugin/server";
|
|
13
|
+
import { demoSettingsContract, type DemoData, type DemoSettings } from "../shared/demo.js";
|
|
14
|
+
import { PLUGIN_VERSION } from "../shared/version.js";
|
|
15
|
+
|
|
16
|
+
export const log = createPluginLogger("helper-demo");
|
|
17
|
+
|
|
18
|
+
export const demoStorage = new PluginStorage<DemoSettings>("helper-demo", "settings.json", {
|
|
19
|
+
schema: demoSettingsContract.schema,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const settingsHandlers = createSettingsHandlers(demoSettingsContract, demoStorage, {
|
|
23
|
+
onUpdate: (newSettings) => {
|
|
24
|
+
log.info("Demo settings updated via RPC:", newSettings);
|
|
25
|
+
},
|
|
26
|
+
onReset: () => {
|
|
27
|
+
log.info("Demo settings reset to default values");
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let daemonPort = 4280;
|
|
32
|
+
let backgroundTicks = 0;
|
|
33
|
+
|
|
34
|
+
findAvailablePort(4280, 20).then((port) => {
|
|
35
|
+
daemonPort = port;
|
|
36
|
+
log.info(`Showcase demo background service verified on port: ${port}`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const backgroundWorker = createPeriodicTask({
|
|
40
|
+
intervalMs: 3000,
|
|
41
|
+
runImmediately: true,
|
|
42
|
+
task: () => {
|
|
43
|
+
backgroundTicks++;
|
|
44
|
+
},
|
|
45
|
+
onError: (err) => {
|
|
46
|
+
log.error("Periodic background task error:", err);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export function handleGetDemoData(): DemoData {
|
|
51
|
+
const metrics = getSystemMetrics();
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
version: PLUGIN_VERSION,
|
|
55
|
+
hostname: metrics.hostname,
|
|
56
|
+
platform: metrics.platform,
|
|
57
|
+
cpuModel: metrics.cpu.model,
|
|
58
|
+
cpuUsagePercent: metrics.cpu.usagePercent,
|
|
59
|
+
memoryUsedPercent: Math.round(metrics.memory.usedPercent),
|
|
60
|
+
memoryUsedBytes: metrics.memory.usedBytes,
|
|
61
|
+
memoryTotalBytes: metrics.memory.totalBytes,
|
|
62
|
+
loadAvg: metrics.cpu.loadAverage,
|
|
63
|
+
uptimeSeconds: metrics.uptimeSeconds,
|
|
64
|
+
daemonPort,
|
|
65
|
+
backgroundTicks,
|
|
66
|
+
items: [
|
|
67
|
+
{ id: "1", name: "Core Agent Supervisor", category: "Core", status: "running", loadPercent: 14 },
|
|
68
|
+
{ id: "2", name: "Vector Index Pipeline", category: "Data", status: "syncing", loadPercent: 48 },
|
|
69
|
+
{ id: "3", name: "MCP Tool Gateway", category: "Network", status: "running", loadPercent: 6 },
|
|
70
|
+
{ id: "4", name: "Local Disk Compactor", category: "Storage", status: "idle", loadPercent: 0 },
|
|
71
|
+
{ id: "5", name: "Peer Gossip Protocol", category: "Network", status: "warning", loadPercent: 82 },
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function handleTriggerDemoAction(input: { actionName: string }) {
|
|
77
|
+
log.info(`Received demo action: "${input.actionName}" at tick ${backgroundTicks}`);
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
message: `Triggered action "${input.actionName}" (Worker tick #${backgroundTicks})`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function handleGetAgentIdentity() {
|
|
85
|
+
const identity = await getAgentIdentity();
|
|
86
|
+
return { identity };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const demoBeacon = createWorkspaceBeacon();
|
|
90
|
+
|
|
91
|
+
function extractDaemonClient(paseo: PluginHandlerContext["paseo"]): BeaconDaemonClient | null {
|
|
92
|
+
const candidate = paseo as unknown as Record<string, unknown>;
|
|
93
|
+
if (typeof candidate["setWorkspaceLabel"] === "function") {
|
|
94
|
+
return candidate as unknown as BeaconDaemonClient;
|
|
95
|
+
}
|
|
96
|
+
for (const key of ["daemonClient", "client", "daemon"]) {
|
|
97
|
+
const nested = candidate[key] as Record<string, unknown> | undefined;
|
|
98
|
+
if (nested && typeof nested["setWorkspaceLabel"] === "function") {
|
|
99
|
+
return nested as unknown as BeaconDaemonClient;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return paseo as unknown as BeaconDaemonClient;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function wireBeaconForWorkspace(paseo: PluginHandlerContext["paseo"], workspaceId: string): void {
|
|
106
|
+
demoBeacon.setOptions({
|
|
107
|
+
workspaceHandle: {
|
|
108
|
+
setTitle: (title: string) => paseo.workspaces.ref(workspaceId).setTitle(title),
|
|
109
|
+
},
|
|
110
|
+
daemonClient: extractDaemonClient(paseo),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function resolveBaseTitle(
|
|
115
|
+
paseo: PluginHandlerContext["paseo"],
|
|
116
|
+
workspaceId: string,
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
if (demoBeacon.hasOriginalTitle(workspaceId)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const handle = paseo.workspaces.ref(workspaceId);
|
|
123
|
+
const snapshot = handle.current() ?? (await handle.refresh());
|
|
124
|
+
let rawTitle = (snapshot as { title?: unknown; name?: unknown } | null)?.title;
|
|
125
|
+
if (typeof rawTitle !== "string" || rawTitle.trim().length === 0) {
|
|
126
|
+
rawTitle = (snapshot as { name?: unknown } | null)?.name as string | undefined;
|
|
127
|
+
}
|
|
128
|
+
if (typeof rawTitle === "string" && rawTitle.trim().length > 0) {
|
|
129
|
+
// Strip any leftover beacon suffix if present from previous run
|
|
130
|
+
const cleaned = rawTitle.replace(/\s*[●🟢🟠○◉].*$/, "").trim();
|
|
131
|
+
const finalTitle = cleaned.length > 0 ? cleaned : rawTitle;
|
|
132
|
+
demoBeacon.setOriginalTitle(workspaceId, finalTitle);
|
|
133
|
+
demoBeacon.setOptions({ baseTitle: finalTitle });
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Best-effort only; beacon falls back to stored/original title.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function handleDemoBeaconSet(
|
|
141
|
+
input: { workspaceId: string; name: string; color: string },
|
|
142
|
+
context: PluginHandlerContext,
|
|
143
|
+
) {
|
|
144
|
+
wireBeaconForWorkspace(context.paseo, input.workspaceId);
|
|
145
|
+
await resolveBaseTitle(context.paseo, input.workspaceId);
|
|
146
|
+
const result = await demoBeacon.set({
|
|
147
|
+
workspaceId: input.workspaceId,
|
|
148
|
+
name: input.name,
|
|
149
|
+
color: input.color,
|
|
150
|
+
titleSuffix: ` 🟢 ${input.name}`,
|
|
151
|
+
});
|
|
152
|
+
log.info(`Demo beacon set "${input.name}" (${input.color}) on ${input.workspaceId}`);
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
message: `Beacon "${input.name}" (${input.color}) applied`,
|
|
156
|
+
labelApplied: result.labelApplied,
|
|
157
|
+
titleApplied: result.titleApplied,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function handleDemoBeaconBlink(
|
|
162
|
+
input: { workspaceId: string; rounds: number },
|
|
163
|
+
context: PluginHandlerContext,
|
|
164
|
+
) {
|
|
165
|
+
wireBeaconForWorkspace(context.paseo, input.workspaceId);
|
|
166
|
+
await resolveBaseTitle(context.paseo, input.workspaceId);
|
|
167
|
+
demoBeacon.blink({
|
|
168
|
+
workspaceId: input.workspaceId,
|
|
169
|
+
a: { name: "DEMO:ACTIVE", color: "emerald", titleSuffix: " 🟢 RUNNING" },
|
|
170
|
+
b: { name: "DEMO:WAITING", color: "orange", titleSuffix: " 🟠 WAITING" },
|
|
171
|
+
intervalMs: 1000,
|
|
172
|
+
rounds: input.rounds,
|
|
173
|
+
restoreOnDone: true,
|
|
174
|
+
});
|
|
175
|
+
log.info(`Demo beacon blink started on ${input.workspaceId} for ${input.rounds} rounds`);
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
message: `Beacon blinking 🟢 RUNNING <-> 🟠 WAITING for ${input.rounds} rounds (auto-restores)`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function handleDemoBeaconClear(
|
|
183
|
+
input: { workspaceId: string; name: string },
|
|
184
|
+
context: PluginHandlerContext,
|
|
185
|
+
) {
|
|
186
|
+
wireBeaconForWorkspace(context.paseo, input.workspaceId);
|
|
187
|
+
const result = await demoBeacon.clear({ workspaceId: input.workspaceId, name: input.name });
|
|
188
|
+
log.info(`Demo beacon cleared on ${input.workspaceId}`);
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
message: "Beacon cleared, original title restored",
|
|
192
|
+
labelCleared: result.labelCleared,
|
|
193
|
+
titleRestored: result.titleRestored,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createSharedPluginSettings } from "paseo-plugin-helper/server";
|
|
2
|
+
import {
|
|
3
|
+
SuiteSettingsContract,
|
|
4
|
+
SuiteSettingsSchema,
|
|
5
|
+
type SuiteSettings,
|
|
6
|
+
} from "paseo-plugin-helper/shared";
|
|
7
|
+
import { log } from "./demo.js";
|
|
8
|
+
|
|
9
|
+
export const suiteSettings = createSharedPluginSettings<SuiteSettings>({
|
|
10
|
+
suite: "xpufx-suite",
|
|
11
|
+
schema: SuiteSettingsSchema,
|
|
12
|
+
contract: SuiteSettingsContract,
|
|
13
|
+
description: "Shared xpufx suite settings (demo plugin side)",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const suiteSettingsHandlers = suiteSettings.createHandlers({
|
|
17
|
+
onUpdate: (next) => {
|
|
18
|
+
log.info("Shared suite settings updated via demo plugin:", next);
|
|
19
|
+
},
|
|
20
|
+
onReset: () => {
|
|
21
|
+
log.info("Shared suite settings reset via demo plugin");
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { safeExec } from "./process";
|
|
2
|
+
|
|
3
|
+
export interface AgentIdentity {
|
|
4
|
+
id?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
provider?: string;
|
|
8
|
+
repo?: string;
|
|
9
|
+
branch?: string;
|
|
10
|
+
envelopeText?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentIdentityOptions {
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
envelopeCommand?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ENV_KEYS = [
|
|
19
|
+
"AGENT_ID",
|
|
20
|
+
"AGENT_NAME",
|
|
21
|
+
"AGENT_MODEL",
|
|
22
|
+
"AGENT_PROVIDER",
|
|
23
|
+
"PASEO_AGENT_ID",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
function readEnvIdentity(env: NodeJS.ProcessEnv = process.env): AgentIdentity | null {
|
|
27
|
+
const id = env.PASEO_AGENT_ID || env.AGENT_ID || undefined;
|
|
28
|
+
const name = env.AGENT_NAME || undefined;
|
|
29
|
+
const model = env.AGENT_MODEL || undefined;
|
|
30
|
+
const provider = env.AGENT_PROVIDER || undefined;
|
|
31
|
+
if (!id && !name && !model && !provider) return null;
|
|
32
|
+
return { id, name, model, provider };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeEnvelopeJson(raw: unknown): AgentIdentity | null {
|
|
36
|
+
if (!raw || typeof raw !== "object") return null;
|
|
37
|
+
const o = raw as Record<string, unknown>;
|
|
38
|
+
const str = (v: unknown): string | undefined =>
|
|
39
|
+
typeof v === "string" && v.length > 0 ? v : undefined;
|
|
40
|
+
const identity: AgentIdentity = {
|
|
41
|
+
id: str(o.id) ?? str(o.agentId),
|
|
42
|
+
name: str(o.name) ?? str(o.agentName),
|
|
43
|
+
model: str(o.model),
|
|
44
|
+
provider: str(o.provider),
|
|
45
|
+
repo: str(o.repo) ?? str(o.workspace),
|
|
46
|
+
branch: str(o.branch),
|
|
47
|
+
envelopeText: str(o.envelopeText) ?? str(o.envelope),
|
|
48
|
+
};
|
|
49
|
+
if (!identity.id && !identity.name && !identity.model && !identity.provider && !identity.repo && !identity.branch) return null;
|
|
50
|
+
return identity;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getAgentIdentity(
|
|
54
|
+
options: AgentIdentityOptions = {},
|
|
55
|
+
): Promise<AgentIdentity | null> {
|
|
56
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
57
|
+
const command = options.envelopeCommand ?? "xpufx-tool envelope --format json";
|
|
58
|
+
const envIdentity = readEnvIdentity();
|
|
59
|
+
void ENV_KEYS;
|
|
60
|
+
try {
|
|
61
|
+
const result = await safeExec(command, { timeoutMs });
|
|
62
|
+
if (result.code !== 0) return envIdentity;
|
|
63
|
+
const text = result.stdout.trim();
|
|
64
|
+
if (!text) return envIdentity;
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = JSON.parse(text);
|
|
67
|
+
const cliIdentity = normalizeEnvelopeJson(parsed);
|
|
68
|
+
if (!cliIdentity) return envIdentity;
|
|
69
|
+
if (!envIdentity) return cliIdentity;
|
|
70
|
+
return {
|
|
71
|
+
id: envIdentity.id ?? cliIdentity.id,
|
|
72
|
+
name: envIdentity.name ?? cliIdentity.name,
|
|
73
|
+
model: envIdentity.model ?? cliIdentity.model,
|
|
74
|
+
provider: envIdentity.provider ?? cliIdentity.provider,
|
|
75
|
+
repo: cliIdentity.repo,
|
|
76
|
+
branch: cliIdentity.branch,
|
|
77
|
+
envelopeText: cliIdentity.envelopeText,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return envIdentity;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
return envIdentity;
|
|
84
|
+
}
|
|
85
|
+
}
|