@xpufx/paseo-top 0.4.3 → 0.4.5

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 (130) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +124 -7
  6. package/client/pill.tsx +724 -669
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +33 -0
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +310 -274
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +21 -61
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +118 -338
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/lifecycle/surface.tsx +140 -0
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +55 -47
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -227
  48. package/package.json +14 -2
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +4 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  61. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  62. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  63. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  64. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  65. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  66. package/server/vendor/paseo-plugin-helper/tickets.ts +1232 -0
  67. package/server/vendor/permission-audit/index.ts +1 -0
  68. package/server/vendor/permission-audit/server.ts +839 -0
  69. package/shared/helper-version.ts +20 -0
  70. package/shared/multi-host.ts +156 -155
  71. package/shared/resources.ts +146 -1
  72. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  73. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  74. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  75. package/shared/vendor/paseo-plugin-helper/index.ts +2 -0
  76. package/shared/vendor/paseo-plugin-helper/tickets.ts +1104 -0
  77. package/shared/vendor/permission-audit/filter.ts +61 -0
  78. package/shared/vendor/permission-audit/index.ts +2 -0
  79. package/shared/vendor/permission-audit/shared.ts +119 -0
  80. package/shared/version.ts +1 -1
  81. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  82. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  83. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -156
  84. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -184
  85. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -225
  86. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  87. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  88. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  89. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  90. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  91. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  92. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -75
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -118
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -80
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -215
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -25
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -224
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -29
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -378
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/surface.tsx +0 -80
  121. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  122. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  123. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  124. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  125. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  126. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  127. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -0,0 +1,821 @@
1
+ import React, { useState, type ReactNode } from "react";
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ ScrollView as FallbackScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ type AccessibilityRole,
10
+ type GestureResponderEvent,
11
+ type StyleProp,
12
+ type TextStyle,
13
+ type ViewStyle,
14
+ } from "react-native";
15
+ import { getClientHost } from "../core/host";
16
+ import { copyToClipboard } from "../lifecycle/clipboard";
17
+ import { useHostTheme, useHostLayout } from "./theme";
18
+ import { alpha } from "./color";
19
+ import { spacing } from "./layout";
20
+ import { HostBadge, HostKeyValue } from "./content";
21
+ import { HostButton } from "./controls";
22
+
23
+ /**
24
+ * Paseo Plugin Helper — UI text & misc adapters (`paseo-plugin-helper/ui`).
25
+ *
26
+ * Thin, host-delegating text adapters
27
+ * (CodeBlock, CommandBox, TruncatedText, …). Same contract as the rest of ui/:
28
+ * host theme colors only, no scroll ownership, no design-system machinery.
29
+ */
30
+
31
+ // --- Code block ------------------------------------------------------------
32
+
33
+ export interface HostCodeBlockProps {
34
+ code: string;
35
+ language?: string;
36
+ title?: string;
37
+ maxHeight?: number;
38
+ copyable?: boolean;
39
+ style?: StyleProp<ViewStyle>;
40
+ textStyle?: StyleProp<TextStyle>;
41
+ }
42
+
43
+ /** Bounded code block with optional copy button. */
44
+ export function HostCodeBlock({
45
+ code,
46
+ language,
47
+ title,
48
+ maxHeight = 320,
49
+ copyable = true,
50
+ style,
51
+ textStyle,
52
+ }: HostCodeBlockProps) {
53
+ const { Icon, useToast } = getClientHost();
54
+ const { colors } = useHostTheme();
55
+ const toast = useToast();
56
+ const [copied, setCopied] = useState(false);
57
+
58
+ const handleCopy = async () => {
59
+ const ok = await copyToClipboard(code, { toast, toastMessage: title || "Code" });
60
+ if (ok) {
61
+ setCopied(true);
62
+ setTimeout(() => setCopied(false), 2000);
63
+ }
64
+ };
65
+
66
+ const fontFamily = Platform.select({
67
+ ios: "Menlo",
68
+ android: "monospace",
69
+ default: "monospace",
70
+ });
71
+
72
+ return (
73
+ <View
74
+ style={[
75
+ styles.codeContainer,
76
+ {
77
+ backgroundColor: colors.surface0,
78
+ borderColor: colors.border,
79
+ },
80
+ style,
81
+ ]}
82
+ >
83
+ {(title || language || copyable) && (
84
+ <View style={[styles.codeHeader, { borderBottomColor: alpha(colors.border, 0.7) }]}>
85
+ <View style={styles.codeHeaderLeft}>
86
+ {title ? (
87
+ <Text style={[styles.codeTitle, { color: colors.foreground }]}>{title}</Text>
88
+ ) : language ? (
89
+ <Text style={[styles.codeLanguage, { color: colors.foregroundMuted }]}>
90
+ {language.toUpperCase()}
91
+ </Text>
92
+ ) : null}
93
+ </View>
94
+ {copyable ? (
95
+ <Pressable
96
+ onPress={handleCopy}
97
+ accessibilityRole="button"
98
+ accessibilityLabel="Copy code"
99
+ hitSlop={4}
100
+ style={({ pressed }) => [
101
+ styles.codeCopyButton,
102
+ {
103
+ backgroundColor: pressed ? colors.surface2 : colors.surface1,
104
+ borderColor: colors.border,
105
+ },
106
+ ]}
107
+ >
108
+ <Icon
109
+ name={copied ? "Check" : "Copy"}
110
+ size={12}
111
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
112
+ />
113
+ </Pressable>
114
+ ) : null}
115
+ </View>
116
+ )}
117
+ {/* The snippet scroller is a plain React Native ScrollView: the host
118
+ ScrollView is a sheet-gesture pan controller and collapses/crashes
119
+ when nested inside the host sheet (#219). */}
120
+ <FallbackScrollView
121
+ style={{ maxHeight }}
122
+ contentContainerStyle={styles.codeScrollContent}
123
+ horizontal={false}
124
+ >
125
+ <Text
126
+ selectable
127
+ style={[
128
+ styles.codeText,
129
+ {
130
+ color: colors.foreground,
131
+ fontFamily,
132
+ },
133
+ textStyle,
134
+ ]}
135
+ >
136
+ {code}
137
+ </Text>
138
+ </FallbackScrollView>
139
+ </View>
140
+ );
141
+ }
142
+
143
+ // --- Command box -----------------------------------------------------------
144
+
145
+ export interface HostCommandBoxProps {
146
+ argv?: string[];
147
+ command?: string;
148
+ style?: StyleProp<ViewStyle>;
149
+ textStyle?: StyleProp<TextStyle>;
150
+ copyLabel?: string;
151
+ }
152
+
153
+ /** Formats an argv array into a shell command line. */
154
+ export function formatCommandLine(argv: string[]): string {
155
+ return argv.map((arg) => (arg.includes(" ") ? JSON.stringify(arg) : arg)).join(" ");
156
+ }
157
+
158
+ /** Shell command display with copy-to-clipboard. */
159
+ export function HostCommandBox({
160
+ argv = [],
161
+ command,
162
+ style,
163
+ textStyle,
164
+ copyLabel = "Copy command",
165
+ }: HostCommandBoxProps): React.ReactElement | null {
166
+ const { colors } = useHostTheme();
167
+ const { Icon, useToast } = getClientHost();
168
+ const toast = useToast();
169
+ const [copied, setCopied] = useState(false);
170
+
171
+ const fullCommand = command ?? formatCommandLine(argv);
172
+ const [prog, ...rest] = argv;
173
+
174
+ const handleCopy = async () => {
175
+ if (!fullCommand) return;
176
+ const ok = await copyToClipboard(fullCommand, { toast, toastMessage: "Command" });
177
+ if (ok) {
178
+ setCopied(true);
179
+ setTimeout(() => setCopied(false), 2000);
180
+ }
181
+ };
182
+
183
+ const fontFamily = Platform.select({
184
+ ios: "Menlo",
185
+ android: "monospace",
186
+ default: "monospace",
187
+ });
188
+
189
+ return (
190
+ <View
191
+ style={[
192
+ styles.commandContainer,
193
+ {
194
+ backgroundColor: colors.surface2,
195
+ borderColor: colors.border,
196
+ },
197
+ style,
198
+ ]}
199
+ >
200
+ <View style={styles.commandTextContainer}>
201
+ <Text style={[styles.commandPrompt, { color: colors.statusWarning, fontFamily }]}>
202
+ {prog ? `$ ${prog}` : "$"}
203
+ </Text>
204
+ {rest.length > 0 ? (
205
+ <Text style={[styles.commandArgs, { color: colors.foreground, fontFamily }]}>
206
+ {" "}
207
+ {rest.join(" ")}
208
+ </Text>
209
+ ) : null}
210
+ </View>
211
+ <Pressable
212
+ onPress={handleCopy}
213
+ accessibilityRole="button"
214
+ accessibilityLabel={copyLabel}
215
+ hitSlop={4}
216
+ style={({ pressed }) => [
217
+ styles.commandCopyButton,
218
+ {
219
+ backgroundColor: pressed ? colors.surface1 : "transparent",
220
+ opacity: pressed ? 0.7 : 1,
221
+ },
222
+ ]}
223
+ >
224
+ <Icon
225
+ name={copied ? "Check" : "Copy"}
226
+ size={12}
227
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
228
+ />
229
+ </Pressable>
230
+ </View>
231
+ );
232
+ }
233
+
234
+ // --- Truncated text --------------------------------------------------------
235
+
236
+ export type HostTruncateMode = "end" | "middle" | "path";
237
+
238
+ export interface HostTruncatedTextProps {
239
+ text: string;
240
+ maxLength?: number;
241
+ mode?: HostTruncateMode;
242
+ copyable?: boolean;
243
+ mono?: boolean;
244
+ toastMessage?: string;
245
+ style?: StyleProp<ViewStyle>;
246
+ textStyle?: StyleProp<TextStyle>;
247
+ }
248
+
249
+ /** Single-line text with truncation and optional copy-to-clipboard. */
250
+ export function HostTruncatedText({
251
+ text,
252
+ maxLength = 32,
253
+ mode = "middle",
254
+ copyable = true,
255
+ mono = true,
256
+ toastMessage = "Copied",
257
+ style,
258
+ textStyle,
259
+ }: HostTruncatedTextProps) {
260
+ const { colors } = useHostTheme();
261
+ const { Icon, useToast } = getClientHost();
262
+ const toast = useToast();
263
+ const [copied, setCopied] = useState(false);
264
+
265
+ if (!text) {
266
+ return <Text style={[{ color: colors.foregroundMuted }, textStyle]}>-</Text>;
267
+ }
268
+
269
+ let formattedText = text;
270
+ if (text.length > maxLength) {
271
+ switch (mode) {
272
+ case "path":
273
+ formattedText = `…${text.slice(-maxLength)}`;
274
+ break;
275
+ case "end":
276
+ formattedText = `${text.slice(0, maxLength)}…`;
277
+ break;
278
+ case "middle":
279
+ default: {
280
+ const half = Math.floor(maxLength / 2);
281
+ formattedText = `${text.slice(0, half)}…${text.slice(-half)}`;
282
+ break;
283
+ }
284
+ }
285
+ }
286
+
287
+ const handleCopy = async () => {
288
+ if (!copyable || !text) return;
289
+ const ok = await copyToClipboard(text, { toast, toastMessage });
290
+ if (ok) {
291
+ setCopied(true);
292
+ setTimeout(() => setCopied(false), 2000);
293
+ }
294
+ };
295
+
296
+ const fontFamily = mono
297
+ ? Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" })
298
+ : undefined;
299
+
300
+ return (
301
+ <View style={[styles.truncatedContainer, style]}>
302
+ <Text
303
+ selectable
304
+ numberOfLines={1}
305
+ ellipsizeMode="clip"
306
+ accessibilityLabel={text}
307
+ style={[
308
+ styles.truncatedText,
309
+ {
310
+ color: colors.foreground,
311
+ fontFamily,
312
+ },
313
+ textStyle,
314
+ ]}
315
+ >
316
+ {formattedText}
317
+ </Text>
318
+ {copyable ? (
319
+ <Pressable
320
+ onPress={handleCopy}
321
+ hitSlop={8}
322
+ style={styles.truncatedCopyBtn}
323
+ accessibilityRole="button"
324
+ accessibilityLabel={`Copy ${text}`}
325
+ >
326
+ <Icon
327
+ name={copied ? "Check" : "Copy"}
328
+ size={13}
329
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
330
+ />
331
+ </Pressable>
332
+ ) : null}
333
+ </View>
334
+ );
335
+ }
336
+
337
+ // --- Highlighted text ------------------------------------------------------
338
+
339
+ export interface HostHighlightedTextProps {
340
+ text: string;
341
+ query: string;
342
+ style?: StyleProp<TextStyle>;
343
+ highlightStyle?: StyleProp<TextStyle>;
344
+ numberOfLines?: number;
345
+ selectable?: boolean;
346
+ fuzzyFallback?: boolean;
347
+ }
348
+
349
+ interface TextPart {
350
+ text: string;
351
+ matched: boolean;
352
+ }
353
+
354
+ /**
355
+ * Renders text with case-insensitive query matches highlighted using the host
356
+ * accent. The query is matched literally, never as a regex.
357
+ */
358
+ export function HostHighlightedText({
359
+ text,
360
+ query,
361
+ style,
362
+ highlightStyle,
363
+ numberOfLines,
364
+ selectable = true,
365
+ fuzzyFallback = false,
366
+ }: HostHighlightedTextProps) {
367
+ const { colors } = useHostTheme();
368
+
369
+ const parts = splitHighlightParts(text, query, fuzzyFallback);
370
+ const matchStyle: StyleProp<TextStyle> = [
371
+ { backgroundColor: colors.accent, color: colors.accentForeground },
372
+ highlightStyle,
373
+ ];
374
+ return (
375
+ <Text style={style} numberOfLines={numberOfLines} selectable={selectable}>
376
+ {parts.map((part, index) =>
377
+ part.matched ? (
378
+ <Text key={`m${index}`} style={matchStyle}>
379
+ {part.text}
380
+ </Text>
381
+ ) : (
382
+ part.text
383
+ ),
384
+ )}
385
+ </Text>
386
+ );
387
+ }
388
+
389
+ function splitHighlightParts(text: string, query: string, fuzzyFallback: boolean): TextPart[] {
390
+ if (!query) return [{ text, matched: false }];
391
+ const lowerText = text.toLowerCase();
392
+ const lowerQuery = query.toLowerCase();
393
+ const parts: TextPart[] = [];
394
+ let cursor = 0;
395
+ let hit = lowerText.indexOf(lowerQuery, cursor);
396
+ while (hit !== -1) {
397
+ if (hit > cursor) parts.push({ text: text.slice(cursor, hit), matched: false });
398
+ parts.push({ text: text.slice(hit, hit + query.length), matched: true });
399
+ cursor = hit + query.length;
400
+ hit = lowerText.indexOf(lowerQuery, cursor);
401
+ }
402
+ if (cursor < text.length) parts.push({ text: text.slice(cursor), matched: false });
403
+ if (parts.length === 0 || !parts.some((p) => p.matched)) {
404
+ if (fuzzyFallback) return [{ text, matched: true }];
405
+ return [{ text, matched: false }];
406
+ }
407
+ return parts;
408
+ }
409
+
410
+ // --- Responsive ------------------------------------------------------------
411
+
412
+ export interface HostResponsiveProps {
413
+ desktop?: ReactNode;
414
+ mobile?: ReactNode;
415
+ compact?: ReactNode;
416
+ wide?: ReactNode;
417
+ children?: ReactNode | ((responsive: HostResponsiveLayout) => ReactNode);
418
+ }
419
+
420
+ export interface HostResponsiveLayout {
421
+ isCompact: boolean;
422
+ isMobile: boolean;
423
+ isWide: boolean;
424
+ select<T>(options: { desktop?: T; mobile?: T; compact?: T; wide?: T } & Record<string, T>): T | undefined;
425
+ }
426
+
427
+ /** Picks children by host layout (compact / mobile / wide). */
428
+ export function HostResponsive({ desktop, mobile, compact, wide, children }: HostResponsiveProps) {
429
+ const layout = useHostLayout();
430
+ const isMobile = layout.platform === "ios" || layout.platform === "android";
431
+ const isCompact = layout.compact || isMobile;
432
+ const isWide = !isCompact;
433
+
434
+ const responsive: HostResponsiveLayout = {
435
+ isCompact,
436
+ isMobile,
437
+ isWide,
438
+ select: <T,>(options: { desktop?: T; mobile?: T; compact?: T; wide?: T } & Record<string, T>): T | undefined =>
439
+ options.compact ?? options.mobile ?? options.wide ?? options.desktop ?? options.default,
440
+ };
441
+
442
+ if (typeof children === "function") {
443
+ return <>{children(responsive)}</>;
444
+ }
445
+
446
+ const selected = responsive.select<ReactNode>({ desktop, mobile, compact, wide });
447
+ return <>{selected ?? children ?? null}</>;
448
+ }
449
+
450
+ // --- Interactive row -------------------------------------------------------
451
+
452
+ export interface HostInteractiveRowProps {
453
+ children?: ReactNode;
454
+ onPress?: (event: GestureResponderEvent) => void;
455
+ title?: string;
456
+ disabled?: boolean;
457
+ accessibilityRole?: AccessibilityRole;
458
+ accessibilityLabel?: string;
459
+ accessibilityHint?: string;
460
+ testID?: string;
461
+ style?: StyleProp<ViewStyle>;
462
+ pressedOpacity?: number;
463
+ disabledOpacity?: number;
464
+ hitSlop?: number;
465
+ }
466
+
467
+ /** Pressable row with pressed feedback. */
468
+ export function HostInteractiveRow({
469
+ children,
470
+ onPress,
471
+ title,
472
+ disabled = false,
473
+ accessibilityRole,
474
+ accessibilityLabel,
475
+ accessibilityHint,
476
+ testID,
477
+ style,
478
+ pressedOpacity = 0.7,
479
+ disabledOpacity = 0.45,
480
+ hitSlop,
481
+ }: HostInteractiveRowProps) {
482
+ const { colors, alpha: alphaColor } = useHostTheme();
483
+ const [pressed, setPressed] = useState(false);
484
+
485
+ const webProps = {
486
+ onMouseEnter: () => {},
487
+ onMouseLeave: () => {},
488
+ ...(title ? { title } : {}),
489
+ } as any;
490
+
491
+ return (
492
+ <Pressable
493
+ onPress={onPress}
494
+ disabled={disabled}
495
+ accessibilityRole={accessibilityRole}
496
+ accessibilityLabel={accessibilityLabel}
497
+ accessibilityHint={accessibilityHint}
498
+ testID={testID}
499
+ hitSlop={hitSlop}
500
+ onPressIn={() => setPressed(true)}
501
+ onPressOut={() => setPressed(false)}
502
+ style={({ pressed: isPressed }) => [
503
+ styles.interactiveRow,
504
+ {
505
+ backgroundColor: pressed || isPressed ? alphaColor(colors.surface1, 0.5) : "transparent",
506
+ opacity: disabled ? disabledOpacity : pressed || isPressed ? pressedOpacity : 1,
507
+ },
508
+ style,
509
+ ]}
510
+ {...webProps}
511
+ >
512
+ {children}
513
+ </Pressable>
514
+ );
515
+ }
516
+
517
+ // --- About section ---------------------------------------------------------
518
+
519
+ export interface HostAboutLink {
520
+ label: string;
521
+ url: string;
522
+ icon?: string;
523
+ }
524
+
525
+ export interface HostAboutSectionProps {
526
+ name: string;
527
+ description?: string;
528
+ version: string;
529
+ author?: string;
530
+ repository?: string;
531
+ issues?: string;
532
+ homepage?: string;
533
+ license?: string;
534
+ links?: HostAboutLink[];
535
+ extraItems?: Array<{
536
+ label: string;
537
+ value: string;
538
+ subValue?: string;
539
+ copyable?: boolean;
540
+ }>;
541
+ style?: StyleProp<ViewStyle>;
542
+ }
543
+
544
+ /** Plugin identity card: name, version, author, links, and extra key-values. */
545
+ export function HostAboutSection({
546
+ name,
547
+ description,
548
+ version,
549
+ author,
550
+ repository,
551
+ issues,
552
+ homepage,
553
+ license = "MIT",
554
+ links = [],
555
+ extraItems = [],
556
+ style,
557
+ }: HostAboutSectionProps) {
558
+ const { colors } = useHostTheme();
559
+ return (
560
+ <View style={[styles.aboutContainer, style]}>
561
+ <Text style={[styles.aboutName, { color: colors.foreground }]}>{name}</Text>
562
+ {description ? (
563
+ <Text style={[styles.aboutDescription, { color: colors.foregroundMuted }]}>
564
+ {description}
565
+ </Text>
566
+ ) : null}
567
+ <View style={styles.aboutMeta}>
568
+ <HostBadge label={`v${version}`} variant="accent" size="sm" />
569
+ {author ? (
570
+ <Text style={[styles.aboutMetaText, { color: colors.foregroundMuted }]}>{author}</Text>
571
+ ) : null}
572
+ <Text style={[styles.aboutMetaText, { color: colors.foregroundMuted }]}>{license}</Text>
573
+ </View>
574
+ {extraItems.length > 0 ? (
575
+ <View style={styles.aboutItems}>
576
+ {extraItems.map((item) => (
577
+ <HostKeyValue
578
+ key={item.label}
579
+ label={item.label}
580
+ value={item.value}
581
+ subValue={item.subValue}
582
+ copyable={item.copyable}
583
+ />
584
+ ))}
585
+ </View>
586
+ ) : null}
587
+ {links.length > 0 ? (
588
+ <View style={styles.aboutLinks}>
589
+ {links.map((link) => (
590
+ <HostButton
591
+ key={link.label}
592
+ label={link.label}
593
+ variant="ghost"
594
+ size="sm"
595
+ icon={link.icon}
596
+ onPress={() => {
597
+ // External URLs open via the host's Linking when available.
598
+ import("react-native").then(({ Linking }) => {
599
+ Linking?.openURL?.(link.url).catch(() => {});
600
+ });
601
+ }}
602
+ />
603
+ ))}
604
+ </View>
605
+ ) : null}
606
+ {repository ? (
607
+ <Text style={[styles.aboutRepo, { color: colors.foregroundMuted }]}>{repository}</Text>
608
+ ) : null}
609
+ </View>
610
+ );
611
+ }
612
+
613
+ // --- Attention beacon ------------------------------------------------------
614
+
615
+ export type HostAttentionBeaconMode = "radar" | "glow" | "pulse" | "badge";
616
+ export type HostAttentionBeaconTone = "warning" | "accent" | "danger";
617
+
618
+ export interface HostAttentionBeaconProps {
619
+ children: ReactNode;
620
+ mode?: HostAttentionBeaconMode;
621
+ tone?: HostAttentionBeaconTone;
622
+ color?: string;
623
+ active?: boolean;
624
+ style?: StyleProp<ViewStyle>;
625
+ accessibilityLabel?: string;
626
+ testID?: string;
627
+ badgeIcon?: string | ReactNode;
628
+ }
629
+
630
+ /**
631
+ * Attention beacon: a host-themed dot/badge overlay that pulses while active.
632
+ * Thin by design — no halo/radar animation machinery; the host owns motion.
633
+ */
634
+ export function HostAttentionBeacon({
635
+ children,
636
+ mode = "pulse",
637
+ tone = "warning",
638
+ color,
639
+ active = true,
640
+ style,
641
+ accessibilityLabel,
642
+ testID,
643
+ badgeIcon,
644
+ }: HostAttentionBeaconProps) {
645
+ const { Icon } = getClientHost();
646
+ const { colors, getStatusColor } = useHostTheme();
647
+ const beaconColor = color ?? getStatusColor(tone === "warning" ? "warning" : tone === "danger" ? "danger" : "accent");
648
+
649
+ const showDot = active && (mode === "pulse" || mode === "radar" || mode === "glow");
650
+ const showBadge = active && mode === "badge";
651
+
652
+ return (
653
+ <View style={[styles.beaconContainer, style]}>
654
+ {children}
655
+ {showDot ? (
656
+ <View
657
+ testID={testID}
658
+ accessibilityLabel={accessibilityLabel}
659
+ style={[
660
+ styles.beaconDot,
661
+ {
662
+ backgroundColor: beaconColor,
663
+ opacity: mode === "pulse" ? 0.75 : 1,
664
+ },
665
+ ]}
666
+ />
667
+ ) : null}
668
+ {showBadge ? (
669
+ <View style={styles.beaconBadge}>
670
+ {badgeIcon ? (
671
+ typeof badgeIcon === "string" ? (
672
+ <Icon name={badgeIcon} size={10} color={colors.accentForeground} />
673
+ ) : (
674
+ badgeIcon
675
+ )
676
+ ) : (
677
+ <View style={[styles.beaconDot, { backgroundColor: beaconColor }]} />
678
+ )}
679
+ </View>
680
+ ) : null}
681
+ </View>
682
+ );
683
+ }
684
+
685
+ const styles = StyleSheet.create({
686
+ codeContainer: {
687
+ borderWidth: 1,
688
+ borderRadius: 10,
689
+ overflow: "hidden",
690
+ },
691
+ codeHeader: {
692
+ flexDirection: "row",
693
+ alignItems: "center",
694
+ justifyContent: "space-between",
695
+ paddingHorizontal: spacing.md,
696
+ paddingVertical: spacing.xs,
697
+ borderBottomWidth: 1,
698
+ },
699
+ codeHeaderLeft: {
700
+ flex: 1,
701
+ },
702
+ codeTitle: {
703
+ fontSize: 12,
704
+ fontWeight: "600",
705
+ },
706
+ codeLanguage: {
707
+ fontSize: 10,
708
+ fontWeight: "600",
709
+ letterSpacing: 0.5,
710
+ },
711
+ codeCopyButton: {
712
+ borderWidth: 1,
713
+ borderRadius: 6,
714
+ padding: 4,
715
+ },
716
+ codeScrollContent: {
717
+ padding: spacing.md,
718
+ },
719
+ codeText: {
720
+ fontSize: 12,
721
+ lineHeight: 18,
722
+ },
723
+ commandContainer: {
724
+ flexDirection: "row",
725
+ alignItems: "center",
726
+ justifyContent: "space-between",
727
+ gap: spacing.sm,
728
+ borderWidth: 1,
729
+ borderRadius: 8,
730
+ paddingHorizontal: spacing.md,
731
+ paddingVertical: spacing.sm,
732
+ },
733
+ commandTextContainer: {
734
+ flexDirection: "row",
735
+ flex: 1,
736
+ flexShrink: 1,
737
+ },
738
+ commandPrompt: {
739
+ fontSize: 12,
740
+ fontWeight: "700",
741
+ },
742
+ commandArgs: {
743
+ fontSize: 12,
744
+ },
745
+ commandCopyButton: {
746
+ padding: 4,
747
+ },
748
+ truncatedContainer: {
749
+ flexDirection: "row",
750
+ alignItems: "center",
751
+ gap: spacing.xs,
752
+ },
753
+ truncatedText: {
754
+ flex: 1,
755
+ flexShrink: 1,
756
+ fontSize: 12,
757
+ },
758
+ truncatedCopyBtn: {
759
+ padding: 2,
760
+ },
761
+ interactiveRow: {
762
+ flexDirection: "row",
763
+ alignItems: "center",
764
+ borderRadius: 8,
765
+ paddingVertical: spacing.sm,
766
+ paddingHorizontal: spacing.md,
767
+ },
768
+ aboutContainer: {
769
+ gap: spacing.sm,
770
+ alignItems: "flex-start",
771
+ },
772
+ aboutName: {
773
+ fontSize: 16,
774
+ fontWeight: "700",
775
+ },
776
+ aboutDescription: {
777
+ fontSize: 12,
778
+ },
779
+ aboutMeta: {
780
+ flexDirection: "row",
781
+ alignItems: "center",
782
+ gap: spacing.sm,
783
+ },
784
+ aboutMetaText: {
785
+ fontSize: 11,
786
+ },
787
+ aboutItems: {
788
+ gap: spacing.xs,
789
+ width: "100%",
790
+ },
791
+ aboutLinks: {
792
+ flexDirection: "row",
793
+ flexWrap: "wrap",
794
+ gap: spacing.xs,
795
+ },
796
+ aboutRepo: {
797
+ fontSize: 10,
798
+ },
799
+ beaconContainer: {
800
+ position: "relative",
801
+ },
802
+ beaconDot: {
803
+ position: "absolute",
804
+ top: -2,
805
+ right: -2,
806
+ width: 8,
807
+ height: 8,
808
+ borderRadius: 4,
809
+ },
810
+ beaconBadge: {
811
+ position: "absolute",
812
+ top: -6,
813
+ right: -6,
814
+ minWidth: 16,
815
+ height: 16,
816
+ borderRadius: 8,
817
+ alignItems: "center",
818
+ justifyContent: "center",
819
+ backgroundColor: "transparent",
820
+ },
821
+ });