@xpufx/paseo-helper-demo 0.2.0 → 0.2.1
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 +21 -0
- package/client/server-settings.tsx +350 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +11 -4
- package/index.client.tsx +14 -0
- package/index.server.ts +55 -0
- package/package.json +6 -2
- package/server/server-settings.ts +177 -0
- package/shared/server-settings.ts +143 -0
package/README.md
CHANGED
|
@@ -55,3 +55,24 @@ The modal navbar renders two ways, switchable in the Settings tab (`navigationSt
|
|
|
55
55
|
- `helper-demo.agent-identity`: active agent identity/session for self-inspection.
|
|
56
56
|
- `helper-demo.beacon-set` / `beacon-blink` / `beacon-clear`: workspace status beacon control.
|
|
57
57
|
- `helper-demo.settings`: persisted settings (`showCpuUsage`, `accentPillLabel`, `pollingRate`, `navigationStyle`, `highCpuThreshold`, flair fields).
|
|
58
|
+
|
|
59
|
+
## TEMP: upstream server settings handle (issue #62)
|
|
60
|
+
|
|
61
|
+
A **temporary** sidebar page (`Server Settings (temp)`) demonstrates the
|
|
62
|
+
upstream server-side settings handle added in Paseo `0.9.0-beta.1` (upstream
|
|
63
|
+
PR #4674), *not* our helper's `PluginStorage`/`registerSettingsRpc` layer:
|
|
64
|
+
|
|
65
|
+
- `index.server.ts` calls `server.registerSettings(defineSettings({ id: "server-demo", ... }))`
|
|
66
|
+
and keeps the returned `PluginSettings` handle.
|
|
67
|
+
- The daemon reads through `handle.read()` and subscribes through
|
|
68
|
+
`handle.subscribe()` (see `server/server-settings.ts`).
|
|
69
|
+
- The page (`client/server-settings.tsx`) renders the daemon's handle state
|
|
70
|
+
(`status` / `revision` / `values` / `subscribe()` event count) and writes the
|
|
71
|
+
same document through the host's auto-registered `settings.server-demo.*` RPC.
|
|
72
|
+
- The document lives at `~/.paseo/plugin-settings/paseo-helper-demo/server-demo.json`.
|
|
73
|
+
|
|
74
|
+
On hosts older than `0.9.0-beta.1`, `registerSettings()` returns `void`; the page
|
|
75
|
+
then reports the handle as unavailable and points at the SDK version. This page
|
|
76
|
+
is scaffolding for the #62 demo and is not wired into the pill/modal surfaces.
|
|
77
|
+
|
|
78
|
+
Tests: `npm test --workspace plugins/demo` (or `npx vitest run` in `plugins/demo`).
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CodeBlock,
|
|
9
|
+
FormRow,
|
|
10
|
+
KeyValue,
|
|
11
|
+
KeyValueGroup,
|
|
12
|
+
ModalBody,
|
|
13
|
+
SectionHeader,
|
|
14
|
+
Stack,
|
|
15
|
+
StatusDot,
|
|
16
|
+
TextInput,
|
|
17
|
+
Toggle,
|
|
18
|
+
registerSidebarSurface,
|
|
19
|
+
triggerHaptic,
|
|
20
|
+
usePluginTheme,
|
|
21
|
+
type SidebarSurfaceRegistrar,
|
|
22
|
+
} from "paseo-plugin-helper/client";
|
|
23
|
+
import { useRpcQuery, useRpcMutation } from "paseo-plugin-helper/core";
|
|
24
|
+
import {
|
|
25
|
+
DEMO_SERVER_SETTINGS_EXPECTED_PATH,
|
|
26
|
+
DEMO_SERVER_SETTINGS_ID,
|
|
27
|
+
DemoSettingsSchema,
|
|
28
|
+
demoSettingsRpc,
|
|
29
|
+
demoSettingsSnapshotContract,
|
|
30
|
+
type DemoSettingsValues,
|
|
31
|
+
type ServerSettingsSnapshot,
|
|
32
|
+
} from "../shared/server-settings.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* TEMP DEMO (issue #62) - not a production surface.
|
|
36
|
+
*
|
|
37
|
+
* Shows the upstream server-side settings handle for `id: "server-demo"`:
|
|
38
|
+
* the daemon's `registerSettings()` returned a `PluginSettings` handle and
|
|
39
|
+
* reads/subscribes through it. The write path is the host's auto-registered
|
|
40
|
+
* `settings.server-demo.*` RPC (same document the host `useSettings()` uses),
|
|
41
|
+
* never our helper `PluginStorage`/`registerSettingsRpc` layer.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
function statusBadge(snapshot: ServerSettingsSnapshot | undefined) {
|
|
45
|
+
if (!snapshot || snapshot.status === "uninitialized") {
|
|
46
|
+
return { variant: "warning" as const, label: "Awaiting first read" };
|
|
47
|
+
}
|
|
48
|
+
if (snapshot.status === "invalid") {
|
|
49
|
+
return { variant: "danger" as const, label: "Invalid document" };
|
|
50
|
+
}
|
|
51
|
+
return { variant: "success" as const, label: "ready" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleSource(snapshot: ServerSettingsSnapshot | undefined): string {
|
|
55
|
+
if (!snapshot) return "reading server handle...";
|
|
56
|
+
if (!snapshot.handleAvailable) return "unavailable (SDK < 0.9.0-beta.1)";
|
|
57
|
+
if (snapshot.subscribed) return "registerSettings() -> read() + subscribe()";
|
|
58
|
+
return "registerSettings() -> read() (subscribe inactive)";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ServerSettingsDemoSurface(_props: PluginSurfaceProps) {
|
|
62
|
+
const { colors, typography } = usePluginTheme();
|
|
63
|
+
const [label, setLabel] = useState<string>("");
|
|
64
|
+
|
|
65
|
+
// Snapshot reflects what the DAEMON observed on the handle; each poll issues a
|
|
66
|
+
// fresh handle.read() server-side (see handleGetServerSettingsSnapshot).
|
|
67
|
+
const { data, isLoading, refetch, error } = useRpcQuery(
|
|
68
|
+
demoSettingsSnapshotContract,
|
|
69
|
+
{},
|
|
70
|
+
{ refetchInterval: 2000 },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// The write side of the exact same document, through upstream's host RPC.
|
|
74
|
+
const readThroughRpc = useRpcQuery(demoSettingsRpc.read, {}, { refetchInterval: 5000 });
|
|
75
|
+
const { mutate: writeSettings, isPending: isSaving } = useRpcMutation(demoSettingsRpc.write, {
|
|
76
|
+
onSuccess: () => {
|
|
77
|
+
triggerHaptic("success");
|
|
78
|
+
void refetch();
|
|
79
|
+
void readThroughRpc.refetch();
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const { mutate: resetSettings, isPending: isResetting } = useRpcMutation(demoSettingsRpc.reset, {
|
|
83
|
+
onSuccess: () => {
|
|
84
|
+
triggerHaptic("warning");
|
|
85
|
+
void refetch();
|
|
86
|
+
void readThroughRpc.refetch();
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const rpcRead = readThroughRpc.data;
|
|
91
|
+
const values: DemoSettingsValues | undefined =
|
|
92
|
+
rpcRead?.status === "ready" ? DemoSettingsSchema.parse(rpcRead.values) : undefined;
|
|
93
|
+
const revision = data?.revision ?? rpcRead?.revision ?? "missing";
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (values && label === "") setLabel(String(values.label));
|
|
97
|
+
}, [values, label]);
|
|
98
|
+
|
|
99
|
+
const badge = statusBadge(data);
|
|
100
|
+
const eventHistory = useMemo(() => {
|
|
101
|
+
if (!data?.lastEventAt) return "no subscribe() events observed yet";
|
|
102
|
+
return `${data.eventCount} event(s); last ${data.lastEventAt} (${data.lastEventStatus})`;
|
|
103
|
+
}, [data]);
|
|
104
|
+
|
|
105
|
+
const handleAvailable = data?.handleAvailable ?? false;
|
|
106
|
+
const controlsDisabled = !handleAvailable || isSaving || isResetting || !values;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<ModalBody
|
|
110
|
+
headerMode="pinned"
|
|
111
|
+
refreshing={isLoading}
|
|
112
|
+
onRefresh={() => {
|
|
113
|
+
void refetch();
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<Stack gap={12}>
|
|
117
|
+
<Card variant="elevated">
|
|
118
|
+
<Card.Header
|
|
119
|
+
title="TEMP — Upstream server settings handle (#62)"
|
|
120
|
+
subtitle="registerSettings() -> read()/subscribe(), read on the daemon process"
|
|
121
|
+
badge={<Badge label="DEMO" variant="warning" />}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<KeyValueGroup columns={2}>
|
|
125
|
+
<KeyValue
|
|
126
|
+
label="Settings id"
|
|
127
|
+
value={data?.settingsId ?? DEMO_SERVER_SETTINGS_ID}
|
|
128
|
+
copyable
|
|
129
|
+
/>
|
|
130
|
+
<KeyValue label="Schema version" value={String(data?.schemaVersion ?? 1)} />
|
|
131
|
+
<KeyValue label="Source" value={handleSource(data)} />
|
|
132
|
+
<KeyValue
|
|
133
|
+
label="Handle available"
|
|
134
|
+
value={handleAvailable ? "yes" : "no"}
|
|
135
|
+
subValue={
|
|
136
|
+
handleAvailable
|
|
137
|
+
? "returned by registerSettings()"
|
|
138
|
+
: "older host runtime returned void"
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
</KeyValueGroup>
|
|
142
|
+
|
|
143
|
+
{data?.unavailableReason ? (
|
|
144
|
+
<Text style={{ color: colors.statusWarning, ...typography.caption }}>
|
|
145
|
+
{data.unavailableReason}
|
|
146
|
+
</Text>
|
|
147
|
+
) : null}
|
|
148
|
+
</Card>
|
|
149
|
+
|
|
150
|
+
<Card variant="elevated">
|
|
151
|
+
<Card.Header
|
|
152
|
+
title="handle.read() snapshot"
|
|
153
|
+
subtitle="State the daemon process observed, straight from the upstream handle"
|
|
154
|
+
badge={<Badge label={badge.label} variant={badge.variant} />}
|
|
155
|
+
/>
|
|
156
|
+
<KeyValueGroup columns={2}>
|
|
157
|
+
<KeyValue
|
|
158
|
+
label="Status"
|
|
159
|
+
value={data?.status ?? "loading"}
|
|
160
|
+
subValue={data?.error ?? undefined}
|
|
161
|
+
/>
|
|
162
|
+
<KeyValue
|
|
163
|
+
label="Revision"
|
|
164
|
+
value={revision}
|
|
165
|
+
subValue="sha256 of the stored document"
|
|
166
|
+
copyable={revision !== "missing"}
|
|
167
|
+
/>
|
|
168
|
+
<KeyValue label="Daemon read() calls" value={String(data?.readCount ?? 0)} />
|
|
169
|
+
<KeyValue
|
|
170
|
+
label="Subscribe active"
|
|
171
|
+
value={data?.subscribed ? "yes" : "no"}
|
|
172
|
+
subValue={eventHistory}
|
|
173
|
+
/>
|
|
174
|
+
</KeyValueGroup>
|
|
175
|
+
|
|
176
|
+
<SectionHeader title="Values seen by the server handle" />
|
|
177
|
+
{data?.values ? (
|
|
178
|
+
<KeyValueGroup columns={2}>
|
|
179
|
+
<KeyValue label="label" value={String(data.values.label)} copyable />
|
|
180
|
+
<KeyValue label="enabled" value={String(data.values.enabled)} />
|
|
181
|
+
<KeyValue label="threshold" value={String(data.values.threshold)} />
|
|
182
|
+
</KeyValueGroup>
|
|
183
|
+
) : (
|
|
184
|
+
<Text style={{ color: colors.foregroundMuted, ...typography.caption }}>
|
|
185
|
+
No ready values yet. The document may not exist on disk until the first write.
|
|
186
|
+
</Text>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
<Text style={{ color: colors.foregroundMuted, ...typography.caption }}>
|
|
190
|
+
Stored at {data?.expectedPath ?? DEMO_SERVER_SETTINGS_EXPECTED_PATH}
|
|
191
|
+
</Text>
|
|
192
|
+
</Card>
|
|
193
|
+
|
|
194
|
+
<Card variant="elevated">
|
|
195
|
+
<Card.Header
|
|
196
|
+
title="Write path (host settings.<id>.* RPC)"
|
|
197
|
+
subtitle="Same document, updated through upstream's persisted RPC — then re-read on the daemon"
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<FormRow
|
|
201
|
+
label="Label"
|
|
202
|
+
description="Persisted into the host-scoped document the handle reads"
|
|
203
|
+
>
|
|
204
|
+
<TextInput
|
|
205
|
+
value={label}
|
|
206
|
+
onChangeText={setLabel}
|
|
207
|
+
placeholder="upstream handle demo"
|
|
208
|
+
/>
|
|
209
|
+
</FormRow>
|
|
210
|
+
|
|
211
|
+
<FormRow label="Enabled" description="Boolean inside the same document" layout="inline">
|
|
212
|
+
<Toggle
|
|
213
|
+
value={Boolean(values?.enabled)}
|
|
214
|
+
onValueChange={(next) => {
|
|
215
|
+
triggerHaptic("light");
|
|
216
|
+
writeSettings({
|
|
217
|
+
revision,
|
|
218
|
+
values: {
|
|
219
|
+
label: values?.label ?? label,
|
|
220
|
+
enabled: next,
|
|
221
|
+
threshold: values?.threshold ?? 70,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
</FormRow>
|
|
227
|
+
|
|
228
|
+
<FormRow
|
|
229
|
+
label="Threshold"
|
|
230
|
+
description={`Integer 0-100 (current: ${values?.threshold ?? "—"})`}
|
|
231
|
+
>
|
|
232
|
+
<TextInput
|
|
233
|
+
value={values ? String(values.threshold) : ""}
|
|
234
|
+
keyboardType="numeric"
|
|
235
|
+
onChangeText={(text) => {
|
|
236
|
+
if (text.trim() === "") return;
|
|
237
|
+
const threshold = Number.parseInt(text, 10);
|
|
238
|
+
if (Number.isNaN(threshold)) return;
|
|
239
|
+
writeSettings({
|
|
240
|
+
revision,
|
|
241
|
+
values: {
|
|
242
|
+
label: values?.label ?? label,
|
|
243
|
+
enabled: values?.enabled ?? true,
|
|
244
|
+
threshold,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
</FormRow>
|
|
250
|
+
|
|
251
|
+
<Button
|
|
252
|
+
label={isSaving ? "Saving..." : "Save label to host settings"}
|
|
253
|
+
variant="primary"
|
|
254
|
+
size="sm"
|
|
255
|
+
disabled={controlsDisabled}
|
|
256
|
+
onPress={() => {
|
|
257
|
+
triggerHaptic("medium");
|
|
258
|
+
writeSettings({
|
|
259
|
+
revision,
|
|
260
|
+
values: {
|
|
261
|
+
label: label.trim() || "upstream handle demo",
|
|
262
|
+
enabled: values?.enabled ?? true,
|
|
263
|
+
threshold: values?.threshold ?? 70,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
<Stack gap={4}>
|
|
270
|
+
<Text style={{ color: colors.foregroundMuted, ...typography.caption }}>
|
|
271
|
+
RPC read status: {readThroughRpc.data?.status ?? (readThroughRpc.isLoading ? "loading" : "—")}
|
|
272
|
+
{readThroughRpc.data?.status === "invalid"
|
|
273
|
+
? ` — ${readThroughRpc.data.error}`
|
|
274
|
+
: ""}
|
|
275
|
+
</Text>
|
|
276
|
+
<Button
|
|
277
|
+
label={isResetting ? "Resetting..." : "Reset host settings document"}
|
|
278
|
+
variant="ghost"
|
|
279
|
+
size="sm"
|
|
280
|
+
disabled={!handleAvailable || isResetting}
|
|
281
|
+
onPress={() => {
|
|
282
|
+
triggerHaptic("warning");
|
|
283
|
+
resetSettings({ revision });
|
|
284
|
+
}}
|
|
285
|
+
/>
|
|
286
|
+
</Stack>
|
|
287
|
+
|
|
288
|
+
{error ? (
|
|
289
|
+
<Text style={{ color: colors.statusDanger, ...typography.caption }}>
|
|
290
|
+
Snapshot error: {error.message}
|
|
291
|
+
</Text>
|
|
292
|
+
) : null}
|
|
293
|
+
</Card>
|
|
294
|
+
|
|
295
|
+
<Card variant="elevated">
|
|
296
|
+
<Card.Header
|
|
297
|
+
title="What this demo proves"
|
|
298
|
+
subtitle="Upstream 0.9 server settings handle vs our helper storage layer"
|
|
299
|
+
/>
|
|
300
|
+
<Stack gap={4}>
|
|
301
|
+
<Text style={{ color: colors.foreground, ...typography.bodyStrong }}>
|
|
302
|
+
<StatusDot variant={handleAvailable ? "success" : "warning"} />{" "}
|
|
303
|
+
{handleAvailable
|
|
304
|
+
? "registerSettings() returned a handle"
|
|
305
|
+
: "handle unavailable on this runtime"}
|
|
306
|
+
</Text>
|
|
307
|
+
<Text style={{ color: colors.foregroundMuted, ...typography.caption }}>
|
|
308
|
+
The daemon reads and subscribes through the handle returned by
|
|
309
|
+
registerSettings(); nothing here touches paseo-plugin-helper's
|
|
310
|
+
PluginStorage/registerSettingsRpc. The host owns persistence
|
|
311
|
+
(revision-checked, atomic) and the client writes through the same
|
|
312
|
+
document via the auto-registered settings RPC.
|
|
313
|
+
</Text>
|
|
314
|
+
</Stack>
|
|
315
|
+
|
|
316
|
+
<CodeBlock
|
|
317
|
+
language="typescript"
|
|
318
|
+
title="server/index.server.ts"
|
|
319
|
+
code={`const settings = server.registerSettings(defineSettings({
|
|
320
|
+
id: "server-demo", scope: "host", version: 1,
|
|
321
|
+
schema: z.object({ label: z.string().default("upstream handle demo"), ... }),
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
// Server-side read + change subscription (upstream 0.9.0-beta.1+, PR #4674):
|
|
325
|
+
settings.subscribe((state) => { /* { status, revision, values|error } */ });
|
|
326
|
+
const state = await settings.read();
|
|
327
|
+
`}
|
|
328
|
+
copyable
|
|
329
|
+
/>
|
|
330
|
+
</Card>
|
|
331
|
+
</Stack>
|
|
332
|
+
</ModalBody>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Registers the temp demo surface + sidebar entry. Returns a disposer, or null
|
|
338
|
+
* when the host does not expose surface registration.
|
|
339
|
+
*/
|
|
340
|
+
export function registerServerSettingsDemoSurface(
|
|
341
|
+
client: SidebarSurfaceRegistrar,
|
|
342
|
+
): (() => void) | null {
|
|
343
|
+
if (!("addSurface" in client) || !("addSidebarItem" in client)) return null;
|
|
344
|
+
return registerSidebarSurface(client, {
|
|
345
|
+
id: "helper-demo-server-settings",
|
|
346
|
+
title: "Server Settings (temp)",
|
|
347
|
+
icon: "Sliders",
|
|
348
|
+
Component: ServerSettingsDemoSurface,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
@@ -24,16 +24,23 @@ export interface ClipboardEnvironment {
|
|
|
24
24
|
* `react-native-web` `Clipboard.setString` reports success even when its
|
|
25
25
|
* `document.execCommand("copy")` fails, which leaves the previous clipboard
|
|
26
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.
|
|
28
|
-
*
|
|
27
|
+
* usable off-DOM (native), where it is the real platform clipboard. The same
|
|
28
|
+
* applies to any `setStringAsync` whose DOM fallback is that unverified
|
|
29
|
+
* `execCommand`. In a DOM the checked `execCommand` fallback is preferred to
|
|
30
|
+
* those unverifiable paths.
|
|
29
31
|
*/
|
|
30
32
|
export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
|
|
31
33
|
const tiers: ClipboardTier[] = [];
|
|
32
34
|
if (env.hasNavigatorClipboard) tiers.push("navigator");
|
|
33
35
|
if (env.hasHostCopyText) tiers.push("host");
|
|
34
|
-
|
|
36
|
+
// On a DOM both RN-web paths are unverifiable: `setString` reports success
|
|
37
|
+
// even when its `execCommand` no-ops, and a `setStringAsync` that falls back
|
|
38
|
+
// to it does the same. Off-DOM they are the real native clipboard, so they
|
|
39
|
+
// lead there; on a DOM only the checked `execCommand` fallback is honest.
|
|
40
|
+
const rnDomOk = !env.isDom;
|
|
41
|
+
if (env.hasRnSetStringAsync && rnDomOk) {
|
|
35
42
|
tiers.push("rnAsync");
|
|
36
|
-
} else if (env.hasRnClipboard &&
|
|
43
|
+
} else if (env.hasRnClipboard && rnDomOk) {
|
|
37
44
|
tiers.push("rnSync");
|
|
38
45
|
}
|
|
39
46
|
tiers.push("execCommand");
|
package/index.client.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
import { contributeClient } from "./client/pill.js";
|
|
3
|
+
import { registerServerSettingsDemoSurface } from "./client/server-settings.js";
|
|
4
|
+
|
|
5
|
+
export default function contribute(client: PluginClientContext) {
|
|
6
|
+
const cleanupPill = contributeClient(client);
|
|
7
|
+
// TEMP DEMO (issue #62): a sidebar page showing the upstream server
|
|
8
|
+
// settings handle. Not wired into the production pill/modal surfaces.
|
|
9
|
+
const cleanupSettingsSurface = registerServerSettingsDemoSurface(client);
|
|
10
|
+
return () => {
|
|
11
|
+
cleanupPill();
|
|
12
|
+
cleanupSettingsSurface?.();
|
|
13
|
+
};
|
|
14
|
+
}
|
package/index.server.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import {
|
|
3
|
+
demoSettingsContract,
|
|
4
|
+
getDemoDataRpc,
|
|
5
|
+
triggerDemoActionRpc,
|
|
6
|
+
demoAgentIdentityContract,
|
|
7
|
+
demoBeaconSetContract,
|
|
8
|
+
demoBeaconBlinkContract,
|
|
9
|
+
demoBeaconClearContract,
|
|
10
|
+
} from "./shared/demo.js";
|
|
11
|
+
import {
|
|
12
|
+
handleGetDemoData,
|
|
13
|
+
handleTriggerDemoAction,
|
|
14
|
+
handleGetAgentIdentity,
|
|
15
|
+
handleDemoBeaconSet,
|
|
16
|
+
handleDemoBeaconBlink,
|
|
17
|
+
handleDemoBeaconClear,
|
|
18
|
+
settingsHandlers,
|
|
19
|
+
backgroundWorker,
|
|
20
|
+
demoBeacon,
|
|
21
|
+
log,
|
|
22
|
+
} from "./server/demo.js";
|
|
23
|
+
import { suiteSettings, suiteSettingsHandlers } from "./server/suite-settings.js";
|
|
24
|
+
import { demoSettingsSnapshotContract } from "./shared/server-settings.js";
|
|
25
|
+
import {
|
|
26
|
+
handleGetServerSettingsSnapshot,
|
|
27
|
+
registerDemoServerSettings,
|
|
28
|
+
} from "./server/server-settings.js";
|
|
29
|
+
|
|
30
|
+
export default function contribute(server: PluginServerContext) {
|
|
31
|
+
// TEMP DEMO (issue #62): upstream registerSettings() -> read()/subscribe().
|
|
32
|
+
const serverSettings = registerDemoServerSettings(server);
|
|
33
|
+
server.handle(demoSettingsSnapshotContract, handleGetServerSettingsSnapshot);
|
|
34
|
+
server.handle(demoSettingsContract.get, settingsHandlers.get);
|
|
35
|
+
server.handle(demoSettingsContract.update, settingsHandlers.update);
|
|
36
|
+
server.handle(demoSettingsContract.reset, settingsHandlers.reset);
|
|
37
|
+
server.handle(suiteSettings.contract.get, suiteSettingsHandlers.get);
|
|
38
|
+
server.handle(suiteSettings.contract.update, suiteSettingsHandlers.update);
|
|
39
|
+
server.handle(suiteSettings.contract.reset, suiteSettingsHandlers.reset);
|
|
40
|
+
server.handle(getDemoDataRpc, handleGetDemoData);
|
|
41
|
+
server.handle(triggerDemoActionRpc, handleTriggerDemoAction);
|
|
42
|
+
server.handle(demoAgentIdentityContract, handleGetAgentIdentity);
|
|
43
|
+
server.handle(demoBeaconSetContract, handleDemoBeaconSet);
|
|
44
|
+
server.handle(demoBeaconBlinkContract, handleDemoBeaconBlink);
|
|
45
|
+
server.handle(demoBeaconClearContract, handleDemoBeaconClear);
|
|
46
|
+
|
|
47
|
+
log.info("Helper demo server handlers registered");
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
backgroundWorker.stop();
|
|
51
|
+
demoBeacon.stopAll();
|
|
52
|
+
serverSettings.dispose();
|
|
53
|
+
log.info("Helper demo server background task stopped");
|
|
54
|
+
};
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
"name": "@xpufx/paseo-helper-demo",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.1",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"stamp": "node -e \"import('./server/vendor/paseo-plugin-helper/version.ts').then(m => m.stampVersion({ targetFile: 'shared/version.ts' }))\"",
|
|
8
|
-
"typecheck": "tsc --noEmit"
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"test": "vitest run --passWithNoTests"
|
|
9
10
|
},
|
|
10
11
|
"devDependencies": {
|
|
11
12
|
"@getpaseo/plugin": "0.9.0-beta.2",
|
|
@@ -14,12 +15,15 @@
|
|
|
14
15
|
"react": "19.1.0",
|
|
15
16
|
"react-native": "0.81.5",
|
|
16
17
|
"typescript": "^5.9.3",
|
|
18
|
+
"vitest": "^3.0.0",
|
|
17
19
|
"zod": "^4.4.3"
|
|
18
20
|
},
|
|
19
21
|
"files": [
|
|
20
22
|
"paseo-plugin.json",
|
|
21
23
|
"README.md",
|
|
22
24
|
"LICENSE",
|
|
25
|
+
"index.client.tsx",
|
|
26
|
+
"index.server.ts",
|
|
23
27
|
"client",
|
|
24
28
|
"server",
|
|
25
29
|
"shared",
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PluginHandlerContext,
|
|
3
|
+
PluginServerContext,
|
|
4
|
+
} from "@getpaseo/plugin/server";
|
|
5
|
+
import type { SettingsDefinition } from "@getpaseo/plugin";
|
|
6
|
+
import { createPluginLogger } from "paseo-plugin-helper/server";
|
|
7
|
+
import {
|
|
8
|
+
buildServerSettingsSnapshot,
|
|
9
|
+
demoSettingsDefinition,
|
|
10
|
+
DEMO_SERVER_SETTINGS_EXPECTED_PATH,
|
|
11
|
+
DEMO_SERVER_SETTINGS_ID,
|
|
12
|
+
type ServerSettingsHandleState,
|
|
13
|
+
type ServerSettingsSnapshot,
|
|
14
|
+
} from "../shared/server-settings.js";
|
|
15
|
+
|
|
16
|
+
// Own logger so this module is importable without the demo's background task.
|
|
17
|
+
const log = createPluginLogger("helper-demo", {
|
|
18
|
+
banner: false,
|
|
19
|
+
subsystem: "server-settings",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
type HandleState = ServerSettingsHandleState;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Structural shape of upstream's 0.9 `PluginSettings` handle. Typed locally so
|
|
26
|
+
* the module compiles and degrades cleanly on the 0.8 SDK, where
|
|
27
|
+
* `registerSettings()` returns `void` and no handle exists.
|
|
28
|
+
*/
|
|
29
|
+
interface SettingsHandleLike {
|
|
30
|
+
read(): Promise<HandleState>;
|
|
31
|
+
subscribe(listener: (state: HandleState) => void | Promise<void>): () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RegisterSettingsCapable {
|
|
35
|
+
registerSettings(definition: SettingsDefinition<typeof demoSettingsDefinition.schema>): unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isSettingsHandle(value: unknown): value is SettingsHandleLike {
|
|
39
|
+
if (typeof value !== "object" || value === null) return false;
|
|
40
|
+
const candidate = value as { read?: unknown; subscribe?: unknown };
|
|
41
|
+
return typeof candidate.read === "function" && typeof candidate.subscribe === "function";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* TEMP DEMO (issue #62): owns the upstream server settings handle for this
|
|
46
|
+
* plugin installation. Everything the demo shows about "server settings" comes
|
|
47
|
+
* from `handle.read()`/`handle.subscribe()` below - never from the helper's
|
|
48
|
+
* `PluginStorage`/`registerSettingsRpc` layer.
|
|
49
|
+
*/
|
|
50
|
+
class DemoSettingsHandle {
|
|
51
|
+
private readonly handle: SettingsHandleLike | null;
|
|
52
|
+
private readonly unavailableReason: string | null;
|
|
53
|
+
private current: HandleState | null = null;
|
|
54
|
+
private readCount = 0;
|
|
55
|
+
private eventCount = 0;
|
|
56
|
+
private lastEventAt: string | null = null;
|
|
57
|
+
private lastEventStatus: "ready" | "invalid" | null = null;
|
|
58
|
+
private subscribed = false;
|
|
59
|
+
private unsubscribe: (() => void) | null = null;
|
|
60
|
+
|
|
61
|
+
constructor(handle: SettingsHandleLike | null, unavailableReason: string | null) {
|
|
62
|
+
this.handle = handle;
|
|
63
|
+
this.unavailableReason = unavailableReason;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Subscribe before the first read so a concurrent client write during startup
|
|
68
|
+
* is not missed; the initial `read()` below establishes the baseline.
|
|
69
|
+
*/
|
|
70
|
+
async initialize(): Promise<void> {
|
|
71
|
+
if (this.handle) {
|
|
72
|
+
this.unsubscribe = this.handle.subscribe((state) => {
|
|
73
|
+
this.eventCount += 1;
|
|
74
|
+
this.lastEventAt = new Date().toISOString();
|
|
75
|
+
this.lastEventStatus = state.status === "ready" ? "ready" : "invalid";
|
|
76
|
+
this.current = state;
|
|
77
|
+
log.info("Demo upstream settings handle observed a change", {
|
|
78
|
+
status: state.status,
|
|
79
|
+
revision: state.revision,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
this.subscribed = true;
|
|
83
|
+
}
|
|
84
|
+
await this.refresh();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Reads through the upstream handle and returns the current snapshot. */
|
|
88
|
+
async refresh(): Promise<ServerSettingsSnapshot> {
|
|
89
|
+
if (!this.handle) return this.snapshot();
|
|
90
|
+
try {
|
|
91
|
+
this.current = await this.handle.read();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
94
|
+
this.current = { status: "invalid", revision: "unknown", error: detail };
|
|
95
|
+
}
|
|
96
|
+
this.readCount += 1;
|
|
97
|
+
return this.snapshot();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
dispose(): void {
|
|
101
|
+
this.unsubscribe?.();
|
|
102
|
+
this.unsubscribe = null;
|
|
103
|
+
this.subscribed = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private snapshot(): ServerSettingsSnapshot {
|
|
107
|
+
return buildServerSettingsSnapshot({
|
|
108
|
+
settingsId: DEMO_SERVER_SETTINGS_ID,
|
|
109
|
+
expectedPath: DEMO_SERVER_SETTINGS_EXPECTED_PATH,
|
|
110
|
+
handleAvailable: this.handle !== null,
|
|
111
|
+
unavailableReason: this.unavailableReason,
|
|
112
|
+
subscribed: this.subscribed,
|
|
113
|
+
readCount: this.readCount,
|
|
114
|
+
eventCount: this.eventCount,
|
|
115
|
+
lastEventAt: this.lastEventAt,
|
|
116
|
+
lastEventStatus: this.lastEventStatus,
|
|
117
|
+
schemaVersion: demoSettingsDefinition.version,
|
|
118
|
+
current: this.current,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let activeHandle: DemoSettingsHandle | null = null;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Registers the host-scoped demo settings document via upstream
|
|
127
|
+
* `registerSettings()` and captures the returned handle. Called once from
|
|
128
|
+
* `contribute()`; the async `initialize()` is intentionally not awaited so the
|
|
129
|
+
* synchronous contribution contract is preserved.
|
|
130
|
+
*/
|
|
131
|
+
export function registerDemoServerSettings(server: PluginServerContext): DemoSettingsHandle {
|
|
132
|
+
const capable = server as unknown as RegisterSettingsCapable;
|
|
133
|
+
const returned = capable.registerSettings(demoSettingsDefinition);
|
|
134
|
+
|
|
135
|
+
let handle: SettingsHandleLike | null = null;
|
|
136
|
+
let reason: string | null = null;
|
|
137
|
+
if (isSettingsHandle(returned)) {
|
|
138
|
+
handle = returned;
|
|
139
|
+
} else {
|
|
140
|
+
reason =
|
|
141
|
+
"registerSettings() did not return a PluginSettings handle. On Paseo < 0.9.0-beta.1 it returns void (the handle shipped in 0.9.0-beta.1, upstream PR #4674); this build is running against an older host runtime.";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const demo = new DemoSettingsHandle(handle, reason);
|
|
145
|
+
activeHandle = demo;
|
|
146
|
+
void demo.initialize().catch((error) => {
|
|
147
|
+
log.error("Demo upstream settings handle failed to initialize", error);
|
|
148
|
+
});
|
|
149
|
+
return demo;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function unavailableSnapshot(): ServerSettingsSnapshot {
|
|
153
|
+
return buildServerSettingsSnapshot({
|
|
154
|
+
settingsId: DEMO_SERVER_SETTINGS_ID,
|
|
155
|
+
expectedPath: DEMO_SERVER_SETTINGS_EXPECTED_PATH,
|
|
156
|
+
handleAvailable: false,
|
|
157
|
+
unavailableReason: "Settings handle was not registered (plugin server did not initialize it).",
|
|
158
|
+
subscribed: false,
|
|
159
|
+
readCount: 0,
|
|
160
|
+
eventCount: 0,
|
|
161
|
+
lastEventAt: null,
|
|
162
|
+
lastEventStatus: null,
|
|
163
|
+
schemaVersion: demoSettingsDefinition.version,
|
|
164
|
+
current: null,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** RPC handler backing the temp demo surface's snapshot read. */
|
|
169
|
+
export async function handleGetServerSettingsSnapshot(
|
|
170
|
+
_input: Record<string, never>,
|
|
171
|
+
_context?: PluginHandlerContext,
|
|
172
|
+
): Promise<ServerSettingsSnapshot> {
|
|
173
|
+
if (!activeHandle) return unavailableSnapshot();
|
|
174
|
+
return activeHandle.refresh();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type { DemoSettingsHandle };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { defineSettings, settingsRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { defineContract } from "paseo-plugin-helper/shared";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TEMP DEMO (issue #62): upstream server-side settings handle.
|
|
7
|
+
*
|
|
8
|
+
* The `SettingsDefinition` below is the single source of truth for a
|
|
9
|
+
* host-scoped settings document. Upstream `registerSettings()` on the server
|
|
10
|
+
* now returns a `PluginSettings` handle with `read()`/`subscribe()`; the client
|
|
11
|
+
* half is the auto-registered `settings.<id>.*` RPC plus the host `useSettings()`
|
|
12
|
+
* hook built on it. This module is intentionally outside our helper's
|
|
13
|
+
* `PluginStorage`/`registerSettingsRpc` layer - that is the point of the demo.
|
|
14
|
+
*/
|
|
15
|
+
export const DEMO_SERVER_SETTINGS_ID = "server-demo";
|
|
16
|
+
|
|
17
|
+
/** Upstream store layout, shown in the demo UI: `~/.paseo/plugin-settings/<pluginId>/<id>.json`. */
|
|
18
|
+
export const DEMO_SERVER_SETTINGS_EXPECTED_PATH =
|
|
19
|
+
"~/.paseo/plugin-settings/paseo-helper-demo/server-demo.json";
|
|
20
|
+
|
|
21
|
+
export const DemoSettingsSchema = z.object({
|
|
22
|
+
label: z.string().min(1).default("upstream handle demo"),
|
|
23
|
+
enabled: z.boolean().default(true),
|
|
24
|
+
threshold: z.number().int().min(0).max(100).default(70),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type DemoSettingsValues = z.infer<typeof DemoSettingsSchema>;
|
|
28
|
+
|
|
29
|
+
export const demoSettingsDefinition = defineSettings({
|
|
30
|
+
id: DEMO_SERVER_SETTINGS_ID,
|
|
31
|
+
scope: "host",
|
|
32
|
+
version: 1,
|
|
33
|
+
schema: DemoSettingsSchema,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** The host RPC the server handle auto-registers; the client writes through it. */
|
|
37
|
+
export const demoSettingsRpc = settingsRpc(DEMO_SERVER_SETTINGS_ID);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Server-handle snapshot: what the daemon process has observed via the
|
|
41
|
+
* upstream handle's `read()` and `subscribe()`, not our helper storage.
|
|
42
|
+
*/
|
|
43
|
+
export const ServerSettingsSnapshotSchema = z.object({
|
|
44
|
+
settingsId: z.string(),
|
|
45
|
+
expectedPath: z.string(),
|
|
46
|
+
handleAvailable: z.boolean(),
|
|
47
|
+
unavailableReason: z.string().nullable(),
|
|
48
|
+
subscribed: z.boolean(),
|
|
49
|
+
status: z.enum(["uninitialized", "ready", "invalid"]),
|
|
50
|
+
revision: z.string(),
|
|
51
|
+
values: DemoSettingsSchema.nullable(),
|
|
52
|
+
error: z.string().nullable(),
|
|
53
|
+
readCount: z.number(),
|
|
54
|
+
eventCount: z.number(),
|
|
55
|
+
lastEventAt: z.string().nullable(),
|
|
56
|
+
lastEventStatus: z.enum(["ready", "invalid"]).nullable(),
|
|
57
|
+
schemaVersion: z.number(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export type ServerSettingsSnapshot = z.infer<typeof ServerSettingsSnapshotSchema>;
|
|
61
|
+
|
|
62
|
+
/** A single state emitted by the upstream handle (structurally mirrors 0.9's PluginSettingsState). */
|
|
63
|
+
export type ServerSettingsHandleState =
|
|
64
|
+
| { status: "ready"; revision: string; values: unknown }
|
|
65
|
+
| { status: "invalid"; revision: string; error: string };
|
|
66
|
+
|
|
67
|
+
export interface ServerSettingsObservation {
|
|
68
|
+
settingsId: string;
|
|
69
|
+
expectedPath: string;
|
|
70
|
+
handleAvailable: boolean;
|
|
71
|
+
unavailableReason: string | null;
|
|
72
|
+
subscribed: boolean;
|
|
73
|
+
readCount: number;
|
|
74
|
+
eventCount: number;
|
|
75
|
+
lastEventAt: string | null;
|
|
76
|
+
lastEventStatus: "ready" | "invalid" | null;
|
|
77
|
+
schemaVersion: number;
|
|
78
|
+
current: ServerSettingsHandleState | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pure projection of the daemon's handle observations into the RPC snapshot
|
|
83
|
+
* shape. Kept free of the SDK so it is unit-testable; the server class only
|
|
84
|
+
* tracks counters and calls this.
|
|
85
|
+
*/
|
|
86
|
+
export function buildServerSettingsSnapshot(
|
|
87
|
+
observation: ServerSettingsObservation,
|
|
88
|
+
): ServerSettingsSnapshot {
|
|
89
|
+
const base = {
|
|
90
|
+
settingsId: observation.settingsId,
|
|
91
|
+
expectedPath: observation.expectedPath,
|
|
92
|
+
handleAvailable: observation.handleAvailable,
|
|
93
|
+
unavailableReason: observation.unavailableReason,
|
|
94
|
+
subscribed: observation.subscribed,
|
|
95
|
+
readCount: observation.readCount,
|
|
96
|
+
eventCount: observation.eventCount,
|
|
97
|
+
lastEventAt: observation.lastEventAt,
|
|
98
|
+
lastEventStatus: observation.lastEventStatus,
|
|
99
|
+
schemaVersion: observation.schemaVersion,
|
|
100
|
+
};
|
|
101
|
+
if (!observation.current) {
|
|
102
|
+
return ServerSettingsSnapshotSchema.parse({
|
|
103
|
+
...base,
|
|
104
|
+
status: "uninitialized",
|
|
105
|
+
revision: "missing",
|
|
106
|
+
values: null,
|
|
107
|
+
error: null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (observation.current.status === "ready") {
|
|
111
|
+
const parsed = DemoSettingsSchema.safeParse(observation.current.values);
|
|
112
|
+
if (!parsed.success) {
|
|
113
|
+
return ServerSettingsSnapshotSchema.parse({
|
|
114
|
+
...base,
|
|
115
|
+
status: "invalid",
|
|
116
|
+
revision: observation.current.revision,
|
|
117
|
+
values: null,
|
|
118
|
+
error: parsed.error.message,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return ServerSettingsSnapshotSchema.parse({
|
|
122
|
+
...base,
|
|
123
|
+
status: "ready",
|
|
124
|
+
revision: observation.current.revision,
|
|
125
|
+
values: parsed.data,
|
|
126
|
+
error: null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return ServerSettingsSnapshotSchema.parse({
|
|
130
|
+
...base,
|
|
131
|
+
status: "invalid",
|
|
132
|
+
revision: observation.current.revision,
|
|
133
|
+
values: null,
|
|
134
|
+
error: observation.current.error,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const demoSettingsSnapshotContract = defineContract({
|
|
139
|
+
name: "helper-demo.server-settings.snapshot",
|
|
140
|
+
description: "Server handle state observed via upstream registerSettings().read()/subscribe()",
|
|
141
|
+
input: z.object({}),
|
|
142
|
+
output: ServerSettingsSnapshotSchema,
|
|
143
|
+
});
|