@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/client/pill.tsx +1249 -0
  4. package/client/suite-settings.tsx +77 -0
  5. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  6. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  7. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  8. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  9. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  10. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  11. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  12. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  13. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  14. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  15. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  16. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  17. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  18. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  19. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  20. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  21. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  22. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  23. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  24. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  25. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  26. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  27. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  28. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  29. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  30. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  31. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  32. package/client/vendor/paseo-plugin-helper/core/index.ts +64 -0
  33. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  34. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  35. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  36. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  37. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  38. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  39. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  40. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  41. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  42. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  43. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  44. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  45. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  46. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  47. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  48. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  49. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  50. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  51. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  52. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  53. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  54. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  55. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  56. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  57. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  58. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  59. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  60. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  61. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  62. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  63. package/client/vendor/paseo-plugin-helper/ui/index.ts +25 -0
  64. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +105 -0
  65. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  66. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  67. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  68. package/package.json +35 -0
  69. package/paseo-plugin.json +6 -0
  70. package/server/demo.ts +195 -0
  71. package/server/suite-settings.ts +23 -0
  72. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  73. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  74. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  75. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  76. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  77. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  78. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  79. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  80. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  81. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  82. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  83. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  84. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  85. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  86. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  87. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  88. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  89. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  90. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  91. package/shared/demo.ts +155 -0
  92. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  93. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  94. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  95. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  96. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  97. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  98. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  99. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  100. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  101. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  102. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  103. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  104. 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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "paseo-helper-demo",
3
+ "requirements": {
4
+ "paseo": ">=0.8.0"
5
+ }
6
+ }
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
+ }