@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,61 @@
1
+ import { isToolCallEntry, type AuditRecord, type PermissionDecision } from "./shared.js";
2
+
3
+ export type DecisionFilter = "all" | PermissionDecision;
4
+ export type AuditTypeFilter = "all" | "permission" | "tool_call";
5
+
6
+ export function formatAuditTime(iso: string): string {
7
+ const ms = Date.parse(iso);
8
+ if (Number.isNaN(ms)) return iso;
9
+ return new Date(ms).toLocaleString();
10
+ }
11
+
12
+ /**
13
+ * Middle-ellipsis for long values, keeping the leading key and the trailing
14
+ * identifier. `maxLength` is the content budget; the returned string can be one
15
+ * character longer once the ellipsis is inserted.
16
+ */
17
+ function truncateMiddle(text: string, maxLength = 120): string {
18
+ if (text.length <= maxLength) return text;
19
+ const head = Math.ceil(maxLength / 2);
20
+ const tail = Math.floor(maxLength / 2);
21
+ return `${text.slice(0, head)}…${text.slice(text.length - tail)}`;
22
+ }
23
+
24
+ export function summarizeAuditInput(input: unknown): string {
25
+ if (input === null || input === undefined) return "—";
26
+ if (typeof input === "string") return truncateMiddle(input);
27
+ try {
28
+ return truncateMiddle(JSON.stringify(input));
29
+ } catch {
30
+ return "—";
31
+ }
32
+ }
33
+
34
+ export function filterAuditEntries(
35
+ records: AuditRecord[],
36
+ decision: DecisionFilter,
37
+ search: string,
38
+ type: AuditTypeFilter = "all",
39
+ ): AuditRecord[] {
40
+ const needle = search.trim().toLowerCase();
41
+ return records.filter((record) => {
42
+ const toolCall = isToolCallEntry(record);
43
+ if (type === "permission" && toolCall) return false;
44
+ if (type === "tool_call" && !toolCall) return false;
45
+ // Decisions only exist on permission records; a decision filter excludes tool calls.
46
+ if (decision !== "all" && (toolCall || record.decision !== decision)) return false;
47
+ if (!needle) return true;
48
+ const haystack = [
49
+ record.name,
50
+ record.kind,
51
+ record.agentId,
52
+ record.agentModel ?? "",
53
+ summarizeAuditInput(record.input),
54
+ toolCall ? record.outcome : record.decision,
55
+ toolCall ? record.turnId ?? "" : "",
56
+ ]
57
+ .join(" ")
58
+ .toLowerCase();
59
+ return haystack.includes(needle);
60
+ });
61
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./shared.js";
2
+ export * from "./filter.js";
@@ -0,0 +1,119 @@
1
+ import { z } from "zod";
2
+ import { defineContract } from "../paseo-plugin-helper/index";
3
+
4
+ export const PermissionDecisionSchema = z.enum(["pending", "allow", "deny"]);
5
+ export type PermissionDecision = z.infer<typeof PermissionDecisionSchema>;
6
+
7
+ export const PermissionAuditEntrySchema = z.object({
8
+ id: z.string().min(1),
9
+ timestamp: z.string().min(1),
10
+ agentId: z.string().min(1),
11
+ agentTitle: z.string().optional(),
12
+ agentModel: z.string().optional(),
13
+ agentProvider: z.string().optional(),
14
+ agentMode: z.string().optional(),
15
+ agentCwd: z.string().optional(),
16
+ kind: z.string().min(1),
17
+ name: z.string().min(1),
18
+ input: z.unknown(),
19
+ decision: PermissionDecisionSchema,
20
+ updatedInput: z.unknown().optional(),
21
+ denyReason: z.string().optional(),
22
+ });
23
+ export type PermissionAuditEntry = z.infer<typeof PermissionAuditEntrySchema>;
24
+
25
+ export const TOOL_CALL_RECORD_TYPE = "tool_call";
26
+
27
+ export const ToolCallOutcomeSchema = z.enum(["success", "failure", "canceled"]);
28
+ export type ToolCallOutcome = z.infer<typeof ToolCallOutcomeSchema>;
29
+
30
+ /**
31
+ * A tool invocation captured from the `agent.turn_ended` timeline. Shares the
32
+ * permission entry's attribution fields and is appended to the same JSONL log;
33
+ * `recordType` discriminates the two on read.
34
+ */
35
+ export const ToolCallAuditEntrySchema = z.object({
36
+ recordType: z.literal(TOOL_CALL_RECORD_TYPE),
37
+ id: z.string().min(1),
38
+ timestamp: z.string().min(1),
39
+ turnId: z.string().optional(),
40
+ /** Index of the item in the turn timeline; orders calls that share a timestamp. */
41
+ sequence: z.number().int().nonnegative().optional(),
42
+ agentId: z.string().min(1),
43
+ agentTitle: z.string().optional(),
44
+ agentModel: z.string().optional(),
45
+ agentProvider: z.string().optional(),
46
+ agentMode: z.string().optional(),
47
+ agentCwd: z.string().optional(),
48
+ kind: z.string().min(1),
49
+ name: z.string().min(1),
50
+ input: z.unknown(),
51
+ outcome: ToolCallOutcomeSchema,
52
+ error: z.unknown().optional(),
53
+ result: z.unknown().optional(),
54
+ });
55
+ export type ToolCallAuditEntry = z.infer<typeof ToolCallAuditEntrySchema>;
56
+
57
+ export const AuditRecordSchema = z.union([
58
+ ToolCallAuditEntrySchema,
59
+ PermissionAuditEntrySchema,
60
+ ]);
61
+ export type AuditRecord = z.infer<typeof AuditRecordSchema>;
62
+
63
+ export function isToolCallEntry(record: AuditRecord): record is ToolCallAuditEntry {
64
+ return (record as { recordType?: unknown }).recordType === TOOL_CALL_RECORD_TYPE;
65
+ }
66
+
67
+ export function isPermissionEntry(record: AuditRecord): record is PermissionAuditEntry {
68
+ return !isToolCallEntry(record);
69
+ }
70
+
71
+ /** Stable identity for dedupe: call ids and request ids share a namespace. */
72
+ export function auditRecordKey(record: AuditRecord): string {
73
+ return `${isToolCallEntry(record) ? TOOL_CALL_RECORD_TYPE : "permission"}:${record.id}`;
74
+ }
75
+
76
+ export const AuditRecordTypeSchema = z.enum(["permission", TOOL_CALL_RECORD_TYPE]);
77
+ export type AuditRecordType = z.infer<typeof AuditRecordTypeSchema>;
78
+
79
+ export const PermissionQueryFilterSchema = z.object({
80
+ agentId: z.string().min(1).optional(),
81
+ model: z.string().min(1).optional(),
82
+ provider: z.string().min(1).optional(),
83
+ decision: PermissionDecisionSchema.optional(),
84
+ outcome: ToolCallOutcomeSchema.optional(),
85
+ recordType: AuditRecordTypeSchema.optional(),
86
+ kind: z.string().min(1).optional(),
87
+ from: z.string().min(1).optional(),
88
+ to: z.string().min(1).optional(),
89
+ search: z.string().optional(),
90
+ limit: z.number().int().min(1).max(1000).default(100),
91
+ });
92
+ export type PermissionQueryFilter = z.input<typeof PermissionQueryFilterSchema>;
93
+
94
+ const auditQueryOutput = z.object({
95
+ entries: z.array(AuditRecordSchema),
96
+ total: z.number().int().nonnegative(),
97
+ });
98
+
99
+ export const permissionAuditQuery = defineContract({
100
+ name: "permission-audit.query",
101
+ description: "Query logged permission decisions, filtered by agent, model, date range, or decision",
102
+ input: PermissionQueryFilterSchema,
103
+ output: auditQueryOutput,
104
+ });
105
+
106
+ export const permissionLoggerQuery = defineContract({
107
+ name: "permission-logger.query",
108
+ description: "Query logged permission decisions, filtered by agent, model, date range, or decision",
109
+ input: PermissionQueryFilterSchema,
110
+ output: auditQueryOutput,
111
+ });
112
+
113
+ export const PERMISSION_AUDIT_PLUGIN_ID = "permission-logger";
114
+ export const PERMISSION_AUDIT_FILENAME = "permissions.jsonl";
115
+
116
+ export interface PermissionLogPaths {
117
+ primary: string;
118
+ legacy: string;
119
+ }
package/shared/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by paseo-plugin-helper. Do not edit.
2
- export const PLUGIN_VERSION = "0.4.0+a50ea39";
2
+ export const PLUGIN_VERSION = "0.4.5+50ac5ba8";
@@ -1,493 +0,0 @@
1
- import React, { useState, type ReactNode } from "react";
2
- import {
3
- Image,
4
- Linking,
5
- StyleSheet,
6
- Text,
7
- View,
8
- type ImageSourcePropType,
9
- type StyleProp,
10
- type ViewStyle,
11
- } from "react-native";
12
- import { getClientHost } from "../host";
13
- import { usePluginTheme } from "../theme/provider";
14
- import { useResponsive } from "../theme/useResponsive";
15
- import { Card } from "./Card";
16
- import { Badge } from "./Badge";
17
- import { Button } from "./Button";
18
- import { KeyValue, KeyValueGroup } from "./KeyValue";
19
- import { copyToClipboard } from "../utils/clipboard";
20
- import { triggerHaptic } from "../utils/haptics";
21
-
22
- export interface AboutLink {
23
- label: string;
24
- url: string;
25
- icon?: string;
26
- }
27
-
28
- export interface AboutSectionProps {
29
- /**
30
- * Name of the plugin.
31
- */
32
- name: string;
33
-
34
- /**
35
- * Short description or tagline.
36
- */
37
- description?: string;
38
-
39
- /**
40
- * Semantic version string (e.g. from `PLUGIN_VERSION` or package.json).
41
- */
42
- version: string;
43
-
44
- /**
45
- * Author or organization name.
46
- */
47
- author?: string;
48
-
49
- /**
50
- * Logo or icon to display.
51
- * - A React Native image source object: `{ uri: "https://..." }` or `require("./assets/logo.png")`
52
- * - A string starting with "http" (e.g. avatar/logo URL)
53
- * - A Paseo Lucide icon name (e.g. "Cpu", "Layers", "Sparkles")
54
- * - If omitted and `repository` or `author` is a GitHub link/username, defaults to the GitHub avatar!
55
- */
56
- logo?: ImageSourcePropType | string;
57
-
58
- /**
59
- * Repository URL (e.g. "https://github.com/xpufx/paseo-top").
60
- */
61
- repository?: string;
62
-
63
- /**
64
- * Issue tracker URL (e.g. "https://github.com/xpufx/paseo-top/issues").
65
- */
66
- issues?: string;
67
-
68
- /**
69
- * Documentation website or wiki URL.
70
- */
71
- homepage?: string;
72
-
73
- /**
74
- * License identifier (e.g. "MIT", "Apache-2.0").
75
- */
76
- license?: string;
77
-
78
- /**
79
- * Custom additional links.
80
- */
81
- links?: AboutLink[];
82
-
83
- /**
84
- * Extra diagnostic or environmental items to display in the details section.
85
- */
86
- extraItems?: Array<{
87
- label: string;
88
- value: string;
89
- subValue?: string;
90
- copyable?: boolean;
91
- }>;
92
-
93
- /**
94
- * Whether to display the "Copy Diagnostics" button. Default: true.
95
- */
96
- showDiagnosticsCopy?: boolean;
97
-
98
- /**
99
- * Optional custom container style.
100
- */
101
- style?: StyleProp<ViewStyle>;
102
-
103
- /**
104
- * Visual density. `"tiny"` scales all fonts to the smallest readable
105
- * size for dense About pages. Default: `"default"`.
106
- */
107
- density?: "default" | "compact" | "tiny";
108
- }
109
-
110
- /**
111
- * Extracts a GitHub owner/organization from a repository URL or author name.
112
- */
113
- function resolveGitHubAvatarUrl(repo?: string, author?: string): string | null {
114
- if (repo) {
115
- const match = repo.match(/github\.com[/:]([a-zA-Z0-9_-]+)/);
116
- if (match?.[1]) {
117
- return `https://github.com/${match[1]}.png?size=128`;
118
- }
119
- }
120
- if (author && !author.includes(" ") && !author.includes("@")) {
121
- return `https://github.com/${author}.png?size=128`;
122
- }
123
- return null;
124
- }
125
-
126
- /**
127
- * `<AboutSection>` provides a standardized, responsive plugin information and diagnostics view.
128
- *
129
- * Features:
130
- * - Displays plugin branding, author, description, version, and license badges.
131
- * - Supports custom logo images, local asset requires, Lucide icons, or auto-resolved GitHub avatars.
132
- * - One-click "Copy Diagnostics" button formatting system info for GitHub issue triage.
133
- * - Pre-styled external links with native browser launch via React Native `Linking`.
134
- */
135
- export function AboutSection({
136
- name,
137
- description,
138
- version,
139
- author,
140
- logo,
141
- repository,
142
- issues,
143
- homepage,
144
- license = "MIT",
145
- links = [],
146
- extraItems = [],
147
- showDiagnosticsCopy = true,
148
- style,
149
- density = "default",
150
- }: AboutSectionProps) {
151
- const { Icon } = getClientHost();
152
- const { colors, resolveRadius, typography } = usePluginTheme();
153
- const { isCompact, platform } = useResponsive();
154
- const [copied, setCopied] = useState(false);
155
- const isTiny = density === "tiny";
156
-
157
- // 1. Resolve Logo / Avatar
158
- let resolvedLogoNode: ReactNode = null;
159
- const radius = resolveRadius("md");
160
-
161
- if (logo) {
162
- if (typeof logo === "string") {
163
- if (logo.startsWith("http://") || logo.startsWith("https://")) {
164
- resolvedLogoNode = (
165
- <Image
166
- source={{ uri: logo }}
167
- style={[styles.logoImage, { borderRadius: radius }]}
168
- />
169
- );
170
- } else {
171
- resolvedLogoNode = (
172
- <View
173
- style={[
174
- styles.logoIconFallback,
175
- {
176
- backgroundColor: colors.surface2,
177
- borderRadius: radius,
178
- borderColor: colors.border,
179
- },
180
- ]}
181
- >
182
- <Icon name={logo} size={26} color={colors.accent} />
183
- </View>
184
- );
185
- }
186
- } else {
187
- resolvedLogoNode = (
188
- <Image
189
- source={logo}
190
- style={[styles.logoImage, { borderRadius: radius }]}
191
- />
192
- );
193
- }
194
- } else {
195
- // Attempt automatic GitHub avatar resolution
196
- const githubAvatar = resolveGitHubAvatarUrl(repository, author);
197
- if (githubAvatar) {
198
- resolvedLogoNode = (
199
- <Image
200
- source={{ uri: githubAvatar }}
201
- style={[styles.logoImage, { borderRadius: radius }]}
202
- />
203
- );
204
- } else {
205
- resolvedLogoNode = (
206
- <View
207
- style={[
208
- styles.logoIconFallback,
209
- {
210
- backgroundColor: colors.surface2,
211
- borderRadius: radius,
212
- borderColor: colors.border,
213
- },
214
- ]}
215
- >
216
- <Icon name="Layers" size={26} color={colors.accent} />
217
- </View>
218
- );
219
- }
220
- }
221
-
222
- // 2. Open URL helper
223
- const handleOpenUrl = async (url: string) => {
224
- try {
225
- triggerHaptic("light");
226
- const canOpen = await Linking.canOpenURL(url);
227
- if (canOpen) {
228
- await Linking.openURL(url);
229
- }
230
- } catch {
231
- // Ignore linking errors
232
- }
233
- };
234
-
235
- // 3. Diagnostics copy helper
236
- const handleCopyDiagnostics = async () => {
237
- triggerHaptic("success");
238
- const lines = [
239
- `Plugin: ${name} v${version}`,
240
- author ? `Author: ${author}` : null,
241
- `License: ${license}`,
242
- `Platform: ${platform} (${isCompact ? "compact" : "regular"})`,
243
- repository ? `Repository: ${repository}` : null,
244
- ...extraItems.map((item) => `${item.label}: ${item.value}`),
245
- ].filter(Boolean);
246
-
247
- await copyToClipboard(lines.join("\n"));
248
- setCopied(true);
249
- setTimeout(() => setCopied(false), 2500);
250
- };
251
-
252
- // 4. Assemble external links
253
- const allLinks: AboutLink[] = [
254
- ...(repository ? [{ label: "Repository", url: repository, icon: "ExternalLink" }] : []),
255
- ...(issues ? [{ label: "Report Issue", url: issues, icon: "Bug" }] : []),
256
- ...(homepage ? [{ label: "Documentation", url: homepage, icon: "BookOpen" }] : []),
257
- ...links,
258
- ];
259
-
260
- return (
261
- <View style={[styles.container, isTiny && tinyStyles.container, style]}>
262
- {/* Header Banner Card */}
263
- <Card variant="elevated" style={isTiny ? tinyStyles.card : undefined}>
264
- <View style={[styles.headerRow, isTiny && tinyStyles.headerRow]}>
265
- {resolvedLogoNode}
266
-
267
- <View style={[styles.metaColumn, isTiny && tinyStyles.metaColumn]}>
268
- <View style={styles.identityTitleRow}>
269
- <Text
270
- numberOfLines={2}
271
- style={[
272
- styles.identityTitle,
273
- {
274
- color: colors.foreground,
275
- ...typography.heading,
276
- },
277
- isTiny && tinyStyles.title,
278
- ]}
279
- >
280
- {name}
281
- </Text>
282
- <Badge
283
- variant="accent"
284
- label={`v${version}`}
285
- textStyle={isTiny ? tinyStyles.text9 : undefined}
286
- />
287
- </View>
288
-
289
- {description ? (
290
- <Text
291
- numberOfLines={3}
292
- style={[
293
- styles.identityDescription,
294
- { color: colors.foregroundMuted, ...typography.body },
295
- isTiny && tinyStyles.text9,
296
- ]}
297
- >
298
- {description}
299
- </Text>
300
- ) : null}
301
-
302
- {author ? (
303
- <Text
304
- style={[
305
- styles.authorText,
306
- { color: colors.foregroundMuted, ...typography.caption },
307
- isTiny && tinyStyles.text9,
308
- ]}
309
- >
310
- by {author}
311
- </Text>
312
- ) : null}
313
-
314
- {license ? (
315
- <View style={styles.titleRow}>
316
- <Badge
317
- variant="neutral"
318
- label={license}
319
- textStyle={isTiny ? tinyStyles.text9 : undefined}
320
- />
321
- </View>
322
- ) : null}
323
- </View>
324
- </View>
325
-
326
- {/* Action Buttons: External Links & Diagnostics */}
327
- <View style={[styles.actionsRow, isTiny && tinyStyles.actionsRow]}>
328
- {allLinks.map((link) => (
329
- <Button
330
- key={link.url}
331
- size="sm"
332
- variant="secondary"
333
- icon={link.icon ?? "ExternalLink"}
334
- label={link.label}
335
- onPress={() => handleOpenUrl(link.url)}
336
- textStyle={isTiny ? tinyStyles.text9 : undefined}
337
- />
338
- ))}
339
-
340
- {showDiagnosticsCopy && (
341
- <Button
342
- size="sm"
343
- variant={copied ? "primary" : "ghost"}
344
- icon={copied ? "Check" : "Copy"}
345
- label={copied ? "Diagnostics Copied!" : "Copy Diagnostics"}
346
- onPress={handleCopyDiagnostics}
347
- textStyle={isTiny ? tinyStyles.text9 : undefined}
348
- />
349
- )}
350
- </View>
351
- </Card>
352
-
353
- {/* Environment & Extra Diagnostics Details */}
354
- <Card variant="elevated" style={isTiny ? tinyStyles.card : undefined}>
355
- <Card.Header
356
- title="Runtime Environment"
357
- subtitle="Diagnostics for issue reports and system verification"
358
- titleStyle={isTiny ? tinyStyles.title : undefined}
359
- subtitleStyle={isTiny ? tinyStyles.text9 : undefined}
360
- />
361
- <KeyValueGroup columns={isCompact ? 1 : 2} gap={isTiny ? 6 : undefined}>
362
- <KeyValue
363
- label="Plugin Version"
364
- value={`v${version}`}
365
- copyable
366
- labelStyle={isTiny ? tinyStyles.text9 : undefined}
367
- valueStyle={isTiny ? tinyStyles.text9 : undefined}
368
- />
369
- <KeyValue
370
- label="Client Platform"
371
- value={platform}
372
- labelStyle={isTiny ? tinyStyles.text9 : undefined}
373
- valueStyle={isTiny ? tinyStyles.text9 : undefined}
374
- />
375
- {author ? (
376
- <KeyValue
377
- label="Author"
378
- value={author}
379
- labelStyle={isTiny ? tinyStyles.text9 : undefined}
380
- valueStyle={isTiny ? tinyStyles.text9 : undefined}
381
- />
382
- ) : null}
383
- {license ? (
384
- <KeyValue
385
- label="License"
386
- value={license}
387
- labelStyle={isTiny ? tinyStyles.text9 : undefined}
388
- valueStyle={isTiny ? tinyStyles.text9 : undefined}
389
- />
390
- ) : null}
391
- {extraItems.map((item, idx) => (
392
- <KeyValue
393
- key={idx}
394
- label={item.label}
395
- value={item.value}
396
- subValue={item.subValue}
397
- copyable={item.copyable}
398
- labelStyle={isTiny ? tinyStyles.text9 : undefined}
399
- valueStyle={isTiny ? tinyStyles.text9 : undefined}
400
- />
401
- ))}
402
- </KeyValueGroup>
403
- </Card>
404
- </View>
405
- );
406
- }
407
-
408
- const styles = StyleSheet.create({
409
- container: {
410
- gap: 12,
411
- },
412
- headerRow: {
413
- flexDirection: "row",
414
- alignItems: "center",
415
- gap: 14,
416
- },
417
- logoImage: {
418
- width: 48,
419
- height: 48,
420
- resizeMode: "cover",
421
- },
422
- logoIconFallback: {
423
- width: 48,
424
- height: 48,
425
- alignItems: "center",
426
- justifyContent: "center",
427
- borderWidth: 1,
428
- },
429
- metaColumn: {
430
- flex: 1,
431
- gap: 5,
432
- minWidth: 0,
433
- },
434
- identityTitleRow: {
435
- flexDirection: "row",
436
- alignItems: "center",
437
- flexWrap: "wrap",
438
- gap: 6,
439
- },
440
- identityTitle: {
441
- flexShrink: 1,
442
- },
443
- identityDescription: {
444
- flexShrink: 1,
445
- },
446
- titleRow: {
447
- flexDirection: "row",
448
- alignItems: "center",
449
- flexWrap: "wrap",
450
- gap: 6,
451
- },
452
- authorText: {
453
- flexShrink: 1,
454
- },
455
- actionsRow: {
456
- flexDirection: "row",
457
- alignItems: "center",
458
- flexWrap: "wrap",
459
- gap: 8,
460
- marginTop: 12,
461
- paddingTop: 10,
462
- borderTopWidth: StyleSheet.hairlineWidth,
463
- borderTopColor: "rgba(128, 128, 128, 0.2)",
464
- },
465
- });
466
-
467
- const tinyStyles = StyleSheet.create({
468
- container: {
469
- gap: 6,
470
- },
471
- card: {
472
- paddingHorizontal: 8,
473
- paddingVertical: 8,
474
- gap: 6,
475
- },
476
- title: {
477
- fontSize: 11,
478
- },
479
- text9: {
480
- fontSize: 9,
481
- },
482
- headerRow: {
483
- gap: 8,
484
- },
485
- metaColumn: {
486
- gap: 4,
487
- },
488
- actionsRow: {
489
- gap: 6,
490
- marginTop: 8,
491
- paddingTop: 6,
492
- },
493
- });