@xpufx/paseo-top 0.4.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 +114 -0
- package/client/pill-labels.ts +480 -0
- package/client/pill.tsx +2461 -0
- package/client/resources-query.ts +68 -0
- package/client/settings-ui.tsx +50 -0
- package/client/surface.tsx +364 -0
- package/client/telemetry-copy.ts +105 -0
- package/client/telemetry.tsx +486 -0
- package/client/turn-counter.ts +151 -0
- package/client/turn-panel.tsx +182 -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/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/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/examples/pills/battery.jsonc +23 -0
- package/examples/pills/disk-usage.jsonc +18 -0
- package/examples/pills/docker-containers.jsonc +22 -0
- package/examples/pills/git-dirty.jsonc +22 -0
- package/examples/pills/gpu-nvidia.jsonc +22 -0
- package/package.json +37 -0
- package/paseo-plugin.json +4 -0
- package/server/resources.ts +784 -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/resources.ts +686 -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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-run splitting for search highlighting, shared by every helper surface
|
|
3
|
+
* that paints matched query text and by the forges search surfaces that scroll
|
|
4
|
+
* to a match. A query is matched literally via `indexOf` on lowercased strings —
|
|
5
|
+
* never compiled as a regular expression — so user input cannot inject a
|
|
6
|
+
* pattern.
|
|
7
|
+
*
|
|
8
|
+
* Remote forge search is fuzzy: a returned issue may match a query it does not
|
|
9
|
+
* contain verbatim. When the literal query is absent the splitter highlights
|
|
10
|
+
* the query's multi-character tokens at word starts, so a fuzzy result still
|
|
11
|
+
* reads. Callers on a single primary label may additionally opt into marking
|
|
12
|
+
* the whole field when not even a token hits (`fallbackToWholeField`); callers
|
|
13
|
+
* that paint many small runs leave it off. `hasFuzzyHighlight` is the
|
|
14
|
+
* whole-surface test for go-to-match.
|
|
15
|
+
*
|
|
16
|
+
* `normalizeSearchQuery` strips surrounding quotes and whitespace for the local
|
|
17
|
+
* filter/highlight paths; remote search keeps the raw query.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface HighlightPart {
|
|
21
|
+
/** The run's source text, in its original casing. */
|
|
22
|
+
text: string;
|
|
23
|
+
/** True when this run is an occurrence of the (normalized) query. */
|
|
24
|
+
matched: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TOKEN_PATTERN = /[^\p{L}\p{N}]+/u;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Normalize a search query for local matching and highlighting: trim, then peel
|
|
31
|
+
* any surrounding pair of matching single/double quotes (`"meta"`, `'meta'`,
|
|
32
|
+
* `""meta""`). Remote forge search keeps the raw query — Forgejo treats quotes
|
|
33
|
+
* as a phrase operator — so only the local paths normalize, letting quoted and
|
|
34
|
+
* unquoted input filter and paint identically. A lone quote is left intact.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizeSearchQuery(query: string): string {
|
|
37
|
+
let value = query.trim();
|
|
38
|
+
while (value.length >= 2) {
|
|
39
|
+
const first = value[0];
|
|
40
|
+
const last = value[value.length - 1];
|
|
41
|
+
if ((first !== '"' && first !== "'") || first !== last) break;
|
|
42
|
+
value = value.slice(1, -1).trim();
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface HighlightOptions {
|
|
48
|
+
/**
|
|
49
|
+
* When true and neither a literal nor a token hit exists, the whole text is
|
|
50
|
+
* returned as a single matched run so a fuzzy search result is still visibly
|
|
51
|
+
* marked. Off by default: callers that paint many small runs (label chips,
|
|
52
|
+
* markdown spans) must not light every one of them up.
|
|
53
|
+
*/
|
|
54
|
+
fallbackToWholeField?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Query pieces used for the token fallback. A query with no literal hit is
|
|
59
|
+
* split on non-alphanumeric boundaries; single-character tokens are dropped so
|
|
60
|
+
* a fuzzy query cannot paint a stray letter in every result.
|
|
61
|
+
*/
|
|
62
|
+
function queryTokens(needle: string): string[] {
|
|
63
|
+
return needle.split(TOKEN_PATTERN).filter((token) => token.length >= 2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** True when `index` begins a word (a word-start query token may prefix it). */
|
|
67
|
+
function isWordStartAt(haystack: string, index: number): boolean {
|
|
68
|
+
return index === 0 || TOKEN_PATTERN.test(haystack[index - 1]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Earliest word-start occurrence of `token` in `haystack` at or after `from`. */
|
|
72
|
+
function firstTokenStart(haystack: string, token: string, from: number): number {
|
|
73
|
+
let index = haystack.indexOf(token, from);
|
|
74
|
+
while (index !== -1) {
|
|
75
|
+
if (isWordStartAt(haystack, index)) return index;
|
|
76
|
+
index = haystack.indexOf(token, index + 1);
|
|
77
|
+
}
|
|
78
|
+
return -1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Split `text` into alternating unmatched/matched runs for every
|
|
83
|
+
* case-insensitive occurrence of `query`. Surrounding whitespace and matching
|
|
84
|
+
* surrounding quotes on the query are ignored; an empty or whitespace-only
|
|
85
|
+
* query (or empty text) yields the whole text as a single unmatched run.
|
|
86
|
+
*
|
|
87
|
+
* When the literal query is absent the splitter highlights the query's
|
|
88
|
+
* multi-character tokens at word starts. If no token hits either, the text is
|
|
89
|
+
* left unmatched unless the caller opted into `fallbackToWholeField`, which
|
|
90
|
+
* marks the whole field so a fuzzy result is never silently unmarked.
|
|
91
|
+
*/
|
|
92
|
+
export function splitHighlightParts(
|
|
93
|
+
text: string,
|
|
94
|
+
query: string,
|
|
95
|
+
options: HighlightOptions = {},
|
|
96
|
+
): HighlightPart[] {
|
|
97
|
+
const needle = normalizeSearchQuery(query).toLowerCase();
|
|
98
|
+
if (!needle || !text) return [{ text, matched: false }];
|
|
99
|
+
const haystack = text.toLowerCase();
|
|
100
|
+
if (haystack.includes(needle)) return splitLiteralParts(text, haystack, needle);
|
|
101
|
+
const tokens = queryTokens(needle);
|
|
102
|
+
const tokenParts = tokens.length ? splitTokenParts(text, haystack, tokens) : null;
|
|
103
|
+
if (tokenParts) return tokenParts;
|
|
104
|
+
// Whole-field fallback: a fuzzy result may match on text the query never
|
|
105
|
+
// contains, so paint the field rather than leave the row unmarked.
|
|
106
|
+
return options.fallbackToWholeField ? [{ text, matched: true }] : [{ text, matched: false }];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Runs for every non-overlapping literal occurrence of `needle`. */
|
|
110
|
+
function splitLiteralParts(text: string, haystack: string, needle: string): HighlightPart[] {
|
|
111
|
+
const parts: HighlightPart[] = [];
|
|
112
|
+
let cursor = 0;
|
|
113
|
+
while (cursor < text.length) {
|
|
114
|
+
const hit = haystack.indexOf(needle, cursor);
|
|
115
|
+
if (hit === -1) {
|
|
116
|
+
parts.push({ text: text.slice(cursor), matched: false });
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (hit > cursor) parts.push({ text: text.slice(cursor, hit), matched: false });
|
|
120
|
+
parts.push({ text: text.slice(hit, hit + needle.length), matched: true });
|
|
121
|
+
cursor = hit + needle.length;
|
|
122
|
+
}
|
|
123
|
+
return parts.length > 0 ? parts : [{ text, matched: false }];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Runs matching any `token` beginning at a word boundary, so a fuzzy query
|
|
128
|
+
* paints the token that prefixes a word (`meta` inside `metadata`) and never a
|
|
129
|
+
* fragment mid-word (`eta` inside `metadata`). The longest token wins a shared
|
|
130
|
+
* start; ties fall to the earliest occurrence. Returns null when no token is
|
|
131
|
+
* found, letting the caller apply its whole-field policy.
|
|
132
|
+
*/
|
|
133
|
+
function splitTokenParts(
|
|
134
|
+
text: string,
|
|
135
|
+
haystack: string,
|
|
136
|
+
tokens: string[],
|
|
137
|
+
): HighlightPart[] | null {
|
|
138
|
+
const sorted = [...tokens].sort((a, b) => b.length - a.length);
|
|
139
|
+
const parts: HighlightPart[] = [];
|
|
140
|
+
let matchedAny = false;
|
|
141
|
+
let cursor = 0;
|
|
142
|
+
while (cursor < text.length) {
|
|
143
|
+
let best: { index: number; token: string } | null = null;
|
|
144
|
+
for (const token of sorted) {
|
|
145
|
+
const index = firstTokenStart(haystack, token, cursor);
|
|
146
|
+
if (index === -1) continue;
|
|
147
|
+
if (best === null || index < best.index) best = { index, token };
|
|
148
|
+
}
|
|
149
|
+
if (best === null) {
|
|
150
|
+
parts.push({ text: text.slice(cursor), matched: false });
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (best.index > cursor) parts.push({ text: text.slice(cursor, best.index), matched: false });
|
|
154
|
+
parts.push({ text: text.slice(best.index, best.index + best.token.length), matched: true });
|
|
155
|
+
matchedAny = true;
|
|
156
|
+
cursor = best.index + best.token.length;
|
|
157
|
+
}
|
|
158
|
+
return matchedAny ? parts : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether `text` contains the literal trimmed query (case-insensitively) —
|
|
163
|
+
* the precise, pre-fuzzy test callers use to tell a true hit from the token
|
|
164
|
+
* fallback.
|
|
165
|
+
*/
|
|
166
|
+
export function hasHighlightMatch(text: string, query: string): boolean {
|
|
167
|
+
const needle = normalizeSearchQuery(query).toLowerCase();
|
|
168
|
+
return needle.length > 0 && text.toLowerCase().includes(needle);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether `text` highlights at all for the trimmed `query`: a literal hit or,
|
|
173
|
+
* failing that, a word-start hit for any of the query's multi-character tokens.
|
|
174
|
+
* Unlike `splitHighlightParts` this never reports the whole-field fallback, so
|
|
175
|
+
* it pinpoints the result/section that genuinely mentions a fuzzy query rather
|
|
176
|
+
* than every field.
|
|
177
|
+
*/
|
|
178
|
+
export function hasFuzzyHighlight(text: string, query: string): boolean {
|
|
179
|
+
const needle = normalizeSearchQuery(query).toLowerCase();
|
|
180
|
+
if (!needle || !text) return false;
|
|
181
|
+
const haystack = text.toLowerCase();
|
|
182
|
+
if (haystack.includes(needle)) return true;
|
|
183
|
+
return queryTokens(needle).some((token) => firstTokenStart(haystack, token, 0) !== -1);
|
|
184
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./rpc";
|
|
3
|
+
export * from "./formatters";
|
|
4
|
+
export * from "./settings";
|
|
5
|
+
export * from "./suite-settings";
|
|
6
|
+
export * from "./async";
|
|
7
|
+
export * from "./suppressed";
|
|
8
|
+
export * from "./custom-pills";
|
|
9
|
+
export * from "./forge";
|
|
10
|
+
export * from "./highlight";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
|
2
|
+
|
|
3
|
+
export interface PluginRpcContract<
|
|
4
|
+
InputSchema extends ZodType = ZodType,
|
|
5
|
+
OutputSchema extends ZodType = ZodType,
|
|
6
|
+
> {
|
|
7
|
+
name: string;
|
|
8
|
+
input: InputSchema;
|
|
9
|
+
output: OutputSchema;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const RPC_NAME = /^[a-z][a-z0-9._-]*$/;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Define a typed Paseo RPC contract conforming to the Paseo RPC protocol.
|
|
16
|
+
* Implemented locally without a runtime dependency on the Paseo SDK.
|
|
17
|
+
*/
|
|
18
|
+
export function defineRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
|
19
|
+
definition: { name: string; input: InputSchema; output: OutputSchema },
|
|
20
|
+
): PluginRpcContract<InputSchema, OutputSchema> {
|
|
21
|
+
const name = definition.name.trim();
|
|
22
|
+
if (!RPC_NAME.test(name)) {
|
|
23
|
+
throw new Error(`Invalid plugin RPC method: ${definition.name}`);
|
|
24
|
+
}
|
|
25
|
+
return { ...definition, name };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type RpcInput<TContract> = TContract extends PluginRpcContract<infer TInput, any>
|
|
29
|
+
? ZodInput<TInput>
|
|
30
|
+
: unknown;
|
|
31
|
+
|
|
32
|
+
export type RpcOutput<TContract> = TContract extends PluginRpcContract<any, infer TOutput>
|
|
33
|
+
? ZodOutput<TOutput>
|
|
34
|
+
: unknown;
|
|
35
|
+
|
|
36
|
+
export interface DefineRpcOptions<
|
|
37
|
+
TName extends string = string,
|
|
38
|
+
TInput extends ZodType = ZodType,
|
|
39
|
+
TOutput extends ZodType = ZodType,
|
|
40
|
+
> {
|
|
41
|
+
name: TName;
|
|
42
|
+
input: TInput;
|
|
43
|
+
output: TOutput;
|
|
44
|
+
description?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Enhanced helper for defining a typed Paseo RPC contract with optional description.
|
|
49
|
+
*/
|
|
50
|
+
export function defineContract<
|
|
51
|
+
TName extends string,
|
|
52
|
+
TInput extends ZodType,
|
|
53
|
+
TOutput extends ZodType,
|
|
54
|
+
>(
|
|
55
|
+
options: DefineRpcOptions<TName, TInput, TOutput>,
|
|
56
|
+
): PluginRpcContract<TInput, TOutput> & { readonly description?: string } {
|
|
57
|
+
const contract = defineRpc({
|
|
58
|
+
name: options.name,
|
|
59
|
+
input: options.input,
|
|
60
|
+
output: options.output,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (options.description) {
|
|
64
|
+
Object.defineProperty(contract, "description", {
|
|
65
|
+
value: options.description,
|
|
66
|
+
enumerable: true,
|
|
67
|
+
writable: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return contract as PluginRpcContract<TInput, TOutput> & { readonly description?: string };
|
|
72
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z, type ZodType } from "zod";
|
|
2
|
+
import { defineContract, type PluginRpcContract } from "./rpc";
|
|
3
|
+
|
|
4
|
+
export const SettingsEmptyInputSchema = z
|
|
5
|
+
.union([z.void(), z.record(z.string(), z.unknown())])
|
|
6
|
+
.optional();
|
|
7
|
+
export type SettingsEmptyInput = z.infer<typeof SettingsEmptyInputSchema>;
|
|
8
|
+
|
|
9
|
+
export interface SettingsContract<TSettings extends Record<string, any>> {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly schema: ZodType<TSettings>;
|
|
12
|
+
readonly defaultSettings: TSettings;
|
|
13
|
+
readonly get: PluginRpcContract<ZodType<SettingsEmptyInput>, ZodType<TSettings>>;
|
|
14
|
+
readonly update: PluginRpcContract<ZodType<Partial<TSettings>>, ZodType<TSettings>>;
|
|
15
|
+
readonly reset: PluginRpcContract<ZodType<SettingsEmptyInput>, ZodType<TSettings>>;
|
|
16
|
+
readonly description?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DefineSettingsContractOptions<TSettings extends Record<string, any>> {
|
|
20
|
+
/**
|
|
21
|
+
* Unique name for the settings domain (e.g. "top.settings" or "myplugin.config").
|
|
22
|
+
* Automatically normalizes invalid RPC characters.
|
|
23
|
+
*/
|
|
24
|
+
name: string;
|
|
25
|
+
/**
|
|
26
|
+
* Zod Object schema representing the full settings shape.
|
|
27
|
+
* Use `.default(...)` on fields to provide default values.
|
|
28
|
+
*/
|
|
29
|
+
schema: ZodType<TSettings> & { partial?: () => ZodType<Partial<TSettings>> };
|
|
30
|
+
/**
|
|
31
|
+
* Optional default data override if schema fields do not all specify `.default()`.
|
|
32
|
+
*/
|
|
33
|
+
defaultData?: Partial<TSettings>;
|
|
34
|
+
/**
|
|
35
|
+
* Optional human-readable description of the settings.
|
|
36
|
+
*/
|
|
37
|
+
description?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Strips `.default(...)` wrappers recursively from a Zod schema so that
|
|
42
|
+
* omitted fields remain undefined rather than being populated with default values
|
|
43
|
+
* during partial updates.
|
|
44
|
+
*/
|
|
45
|
+
function stripDefaults(schema: any): any {
|
|
46
|
+
if (!schema || typeof schema !== "object") {
|
|
47
|
+
return schema;
|
|
48
|
+
}
|
|
49
|
+
if (schema instanceof z.ZodDefault) {
|
|
50
|
+
return stripDefaults(schema._def.innerType);
|
|
51
|
+
}
|
|
52
|
+
if (schema instanceof z.ZodOptional) {
|
|
53
|
+
return stripDefaults(schema._def.innerType).optional();
|
|
54
|
+
}
|
|
55
|
+
if (schema instanceof z.ZodNullable) {
|
|
56
|
+
return stripDefaults(schema._def.innerType).nullable();
|
|
57
|
+
}
|
|
58
|
+
// Handle ZodEffects, ZodBranded, ZodReadonly, ZodCatch
|
|
59
|
+
if (schema._def && schema._def.schema) {
|
|
60
|
+
return stripDefaults(schema._def.schema);
|
|
61
|
+
}
|
|
62
|
+
if (schema instanceof z.ZodObject) {
|
|
63
|
+
const shape = schema.shape;
|
|
64
|
+
const newShape: Record<string, any> = {};
|
|
65
|
+
for (const key of Object.keys(shape)) {
|
|
66
|
+
newShape[key] = stripDefaults(shape[key]).optional();
|
|
67
|
+
}
|
|
68
|
+
let res = z.object(newShape);
|
|
69
|
+
const unknownKeys = (schema._def as any)?.unknownKeys;
|
|
70
|
+
if (unknownKeys === "passthrough") {
|
|
71
|
+
res = res.passthrough() as any;
|
|
72
|
+
} else if (unknownKeys === "strict") {
|
|
73
|
+
res = res.strict() as any;
|
|
74
|
+
}
|
|
75
|
+
return res;
|
|
76
|
+
}
|
|
77
|
+
return typeof schema.optional === "function" ? schema.optional() : schema;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Defines a pair of typed Paseo RPC contracts (get, update, reset) for plugin settings.
|
|
82
|
+
*/
|
|
83
|
+
export function defineSettingsContract<TSettings extends Record<string, any>>(
|
|
84
|
+
options: DefineSettingsContractOptions<TSettings>,
|
|
85
|
+
): SettingsContract<TSettings> {
|
|
86
|
+
const { name, schema, defaultData, description } = options;
|
|
87
|
+
|
|
88
|
+
let computedDefaults: TSettings;
|
|
89
|
+
try {
|
|
90
|
+
computedDefaults = schema.parse(defaultData ?? {}) as TSettings;
|
|
91
|
+
} catch {
|
|
92
|
+
computedDefaults = (defaultData ?? {}) as TSettings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sanitizedName = name
|
|
96
|
+
.trim()
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.replace(/[^a-z0-9._-]/g, "_");
|
|
99
|
+
|
|
100
|
+
const partialSchema = (
|
|
101
|
+
schema instanceof z.ZodObject
|
|
102
|
+
? stripDefaults(schema)
|
|
103
|
+
: typeof (schema as any).partial === "function"
|
|
104
|
+
? (schema as any).partial()
|
|
105
|
+
: z.record(z.string(), z.unknown())
|
|
106
|
+
) as ZodType<Partial<TSettings>>;
|
|
107
|
+
|
|
108
|
+
const getContract = defineContract({
|
|
109
|
+
name: `${sanitizedName}.get`,
|
|
110
|
+
input: SettingsEmptyInputSchema as ZodType<SettingsEmptyInput>,
|
|
111
|
+
output: schema,
|
|
112
|
+
description: description ? `Get ${description}` : `Get ${name} settings`,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const updateContract = defineContract({
|
|
116
|
+
name: `${sanitizedName}.update`,
|
|
117
|
+
input: partialSchema,
|
|
118
|
+
output: schema,
|
|
119
|
+
description: description ? `Update ${description}` : `Update ${name} settings`,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const resetContract = defineContract({
|
|
123
|
+
name: `${sanitizedName}.reset`,
|
|
124
|
+
input: SettingsEmptyInputSchema as ZodType<SettingsEmptyInput>,
|
|
125
|
+
output: schema,
|
|
126
|
+
description: description ? `Reset ${description}` : `Reset ${name} settings to defaults`,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
name: sanitizedName,
|
|
131
|
+
schema,
|
|
132
|
+
defaultSettings: computedDefaults,
|
|
133
|
+
get: getContract,
|
|
134
|
+
update: updateContract,
|
|
135
|
+
reset: resetContract,
|
|
136
|
+
...(description !== undefined ? { description } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineSettingsContract } from "./settings";
|
|
3
|
+
|
|
4
|
+
export const SuiteSettingsSchema = z.object({
|
|
5
|
+
suiteTitle: z.string().default("xpufx Suite"),
|
|
6
|
+
accentColor: z.string().default("#6366f1"),
|
|
7
|
+
density: z.enum(["compact", "comfortable", "spacious"]).default("comfortable"),
|
|
8
|
+
showSuiteTabs: z.boolean().default(true),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type SuiteSettings = z.infer<typeof SuiteSettingsSchema>;
|
|
12
|
+
|
|
13
|
+
export const SuiteSettingsContract = defineSettingsContract({
|
|
14
|
+
name: "xpufx.suite.settings",
|
|
15
|
+
schema: SuiteSettingsSchema,
|
|
16
|
+
description: "Shared xpufx suite settings",
|
|
17
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface SuppressedSink {
|
|
2
|
+
debug?(message: string, data?: unknown): void;
|
|
3
|
+
warn?(message: string, data?: unknown): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function messageOf(error: unknown): string {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Surfaces a caught/suppressed error to the plugin log at debug level
|
|
12
|
+
* (warn when `level: "warn"`). Fire-and-forget `catch(() => undefined)`
|
|
13
|
+
* sites should route through here so `paseo plugin logs` shows them
|
|
14
|
+
* when dev debug logging is enabled.
|
|
15
|
+
*/
|
|
16
|
+
export function reportSuppressed(
|
|
17
|
+
sink: Pick<SuppressedSink, "debug" | "warn"> | undefined,
|
|
18
|
+
context: string,
|
|
19
|
+
error: unknown,
|
|
20
|
+
level: "debug" | "warn" = "debug",
|
|
21
|
+
): void {
|
|
22
|
+
try {
|
|
23
|
+
if (level === "warn") {
|
|
24
|
+
sink?.warn?.(`${context}: ${messageOf(error)}`, error);
|
|
25
|
+
} else {
|
|
26
|
+
sink?.debug?.(`${context}: ${messageOf(error)}`, error);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// Reporting must never throw.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural theme types for Paseo plugins.
|
|
3
|
+
*
|
|
4
|
+
* These interfaces mirror the Paseo host theme shapes without importing any
|
|
5
|
+
* Paseo SDK module, so `paseo-plugin-helper/shared` (and everything
|
|
6
|
+
* built on it) typechecks and bundles identically against Paseo v0.7 and
|
|
7
|
+
* Paseo v0.8 SDKs.
|
|
8
|
+
*/
|
|
9
|
+
export interface ThemeColors {
|
|
10
|
+
readonly surface0: string;
|
|
11
|
+
readonly surface1: string;
|
|
12
|
+
readonly surface2: string;
|
|
13
|
+
readonly border: string;
|
|
14
|
+
readonly foreground: string;
|
|
15
|
+
readonly foregroundMuted: string;
|
|
16
|
+
readonly accent: string;
|
|
17
|
+
readonly accentForeground: string;
|
|
18
|
+
readonly statusSuccess: string;
|
|
19
|
+
readonly statusWarning: string;
|
|
20
|
+
readonly statusDanger: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PluginTheme {
|
|
24
|
+
readonly colors: ThemeColors;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type PlatformType = "ios" | "android" | "web";
|
|
28
|
+
|
|
29
|
+
export interface ResponsiveLayout {
|
|
30
|
+
compact: boolean;
|
|
31
|
+
platform: PlatformType;
|
|
32
|
+
width?: number;
|
|
33
|
+
height?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type StatusVariant = "neutral" | "success" | "warning" | "danger" | "accent" | "info";
|