@xpufx/paseo-x-comms 0.3.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 +149 -0
- package/client/attribution.ts +7 -0
- package/client/conversations.ts +229 -0
- package/client/main.tsx +885 -0
- package/client/peer-label.ts +98 -0
- package/client/peer-status.tsx +201 -0
- package/client/settings-prototype.tsx +395 -0
- package/client/tool-call.ts +115 -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/client/via-x-comms.tsx +15 -0
- package/client/x-comms-conversation.tsx +449 -0
- package/client/x-comms-panel.tsx +6 -0
- package/client/x-comms-pill.tsx +193 -0
- package/client/x-comms-timeline.tsx +176 -0
- package/client/x-comms-tool-call.tsx +85 -0
- package/docs/mesh.md +115 -0
- package/mcp/LICENSE +202 -0
- package/mcp/README.md +287 -0
- package/mcp/mcp-config.example.json +8 -0
- package/mcp/package-lock.json +1186 -0
- package/mcp/package.json +43 -0
- package/mcp/paseo-cross-daemon-comms.example.json +5 -0
- package/mcp/paseo-x-comms.bundled.mjs +36964 -0
- package/mcp/paseo-x-comms.mjs +630 -0
- package/mcp/test/fixtures/extensions/block/10-block.mjs +4 -0
- package/mcp/test/fixtures/extensions/broken-hook/10-thrower.mjs +6 -0
- package/mcp/test/fixtures/extensions/broken-hook/20-good.mjs +4 -0
- package/mcp/test/fixtures/extensions/broken-load/10-thrower.mjs +4 -0
- package/mcp/test/fixtures/extensions/broken-load/20-good.mjs +4 -0
- package/mcp/test/fixtures/extensions/custom-tool/10-tool.mjs +12 -0
- package/mcp/test/fixtures/extensions/tool-block/10-block.mjs +8 -0
- package/mcp/test/fixtures/extensions/transform/10-transform.mjs +15 -0
- package/mcp/test/fixtures/fake-paseo.mjs +92 -0
- package/mcp/test/register-ts-hooks.mjs +6 -0
- package/mcp/test/resolve-ts-hooks.mjs +46 -0
- package/package.json +58 -0
- package/paseo-plugin.json +7 -0
- package/server/conversations-snapshot.ts +249 -0
- package/server/handlers.ts +1081 -0
- package/server/injection.ts +152 -0
- package/server/local-send.ts +153 -0
- package/server/mcp-client.ts +47 -0
- package/server/outbox.ts +233 -0
- package/server/peer-channel.ts +151 -0
- package/server/peer-status.ts +233 -0
- package/server/presence.ts +204 -0
- package/server/registry.ts +215 -0
- package/server/relay-status.ts +9 -0
- package/server/server-status.ts +138 -0
- package/server/settings.ts +81 -0
- package/server/snapshot.ts +176 -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/client.ts +349 -0
- package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
- package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
- package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
- package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
- package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -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/conversations-snapshot.ts +39 -0
- package/shared/envelope.ts +92 -0
- package/shared/outbox.ts +21 -0
- package/shared/registry.ts +366 -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,98 @@
|
|
|
1
|
+
import { useRpc } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { registryReadRpc } from "../shared/registry";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Canonical daemon display: `alias (srv_xxx)`. Both halves always render
|
|
7
|
+
* together: never a bare id, never a bare alias. Peers without a registry
|
|
8
|
+
* entry (unpaired senders) render as `unknown (srv_xxx)`.
|
|
9
|
+
*/
|
|
10
|
+
export function formatPeerDisplay(alias: string | null | undefined, serverId: string | null | undefined): string {
|
|
11
|
+
const id = serverId ?? "unknown-id";
|
|
12
|
+
const name = alias ?? "unknown";
|
|
13
|
+
return `${name} (${id})`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function aliasFor(serverId: string | null | undefined, byServerId: Map<string, string>): string | null {
|
|
17
|
+
if (!serverId) return null;
|
|
18
|
+
return byServerId.get(serverId) ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a serverId to its local registry alias. Returns null while
|
|
23
|
+
* loading or when the id is not paired.
|
|
24
|
+
*/
|
|
25
|
+
export function usePeerAlias(serverId: string | null | undefined): string | null {
|
|
26
|
+
const callRead = useRpc(registryReadRpc);
|
|
27
|
+
const read = useQuery({
|
|
28
|
+
queryKey: ["registry-read"],
|
|
29
|
+
queryFn: () => callRead({}),
|
|
30
|
+
staleTime: 300_000,
|
|
31
|
+
refetchOnWindowFocus: false,
|
|
32
|
+
});
|
|
33
|
+
if (!serverId || !read.data) return null;
|
|
34
|
+
const byServerId = new Map<string, string>();
|
|
35
|
+
for (const daemon of read.data.daemons) {
|
|
36
|
+
if (daemon.serverId) byServerId.set(daemon.serverId, daemon.name);
|
|
37
|
+
}
|
|
38
|
+
return aliasFor(serverId, byServerId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format a peer when both halves are already known (list rows, dump views,
|
|
43
|
+
* confirmations). Falls back to `unknown (srv)` when the alias is missing
|
|
44
|
+
* rather than rendering either half bare.
|
|
45
|
+
*/
|
|
46
|
+
export function usePeerDisplay(serverId: string | null | undefined, knownAlias?: string | null): string {
|
|
47
|
+
const resolved = usePeerAlias(serverId);
|
|
48
|
+
return formatPeerDisplay(knownAlias ?? resolved, serverId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CounterpartyRef {
|
|
52
|
+
daemon?: string | null;
|
|
53
|
+
daemonServerId?: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Split a counterparty ref into display halves. Either field may carry
|
|
58
|
+
* either half depending on where the ref was built, so srv_-shaped values
|
|
59
|
+
* always count as the id half.
|
|
60
|
+
*/
|
|
61
|
+
export function splitCounterparty(cp: CounterpartyRef): { alias: string | null; serverId: string | null } {
|
|
62
|
+
const values = [cp.daemon, cp.daemonServerId].filter((v): v is string => !!v);
|
|
63
|
+
const serverId = values.find((v) => v.startsWith("srv_")) ?? null;
|
|
64
|
+
const alias = values.find((v) => !v.startsWith("srv_")) ?? null;
|
|
65
|
+
return { alias, serverId };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatCounterparty(cp: CounterpartyRef): string {
|
|
69
|
+
const { alias, serverId } = splitCounterparty(cp);
|
|
70
|
+
return formatPeerDisplay(alias, serverId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Full counterparty label with registry enrichment: fills a missing half
|
|
75
|
+
* from the local registry (name to id or id to alias) before formatting.
|
|
76
|
+
* Shares the cached registry-read query with the other hooks.
|
|
77
|
+
*/
|
|
78
|
+
export function useCounterpartyLabel(cp: CounterpartyRef): string {
|
|
79
|
+
const callRead = useRpc(registryReadRpc);
|
|
80
|
+
const read = useQuery({
|
|
81
|
+
queryKey: ["registry-read"],
|
|
82
|
+
queryFn: () => callRead({}),
|
|
83
|
+
staleTime: 300_000,
|
|
84
|
+
refetchOnWindowFocus: false,
|
|
85
|
+
});
|
|
86
|
+
const { alias, serverId } = splitCounterparty(cp);
|
|
87
|
+
let id = serverId;
|
|
88
|
+
let name = alias;
|
|
89
|
+
if (read.data) {
|
|
90
|
+
if (!id && name) {
|
|
91
|
+
id = read.data.daemons.find((d) => d.name === name)?.serverId ?? null;
|
|
92
|
+
}
|
|
93
|
+
if (!name && id) {
|
|
94
|
+
name = read.data.daemons.find((d) => d.serverId === id)?.name ?? null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return formatPeerDisplay(name, id);
|
|
98
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { type PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import React, { useMemo } from "react";
|
|
3
|
+
import { Text, View } from "react-native";
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
EmptyState,
|
|
9
|
+
KeyValue,
|
|
10
|
+
KeyValueGroup,
|
|
11
|
+
ModalBody,
|
|
12
|
+
SectionHeader,
|
|
13
|
+
StatusDot,
|
|
14
|
+
usePluginTheme,
|
|
15
|
+
useRpcQuery,
|
|
16
|
+
} from "paseo-plugin-helper/client";
|
|
17
|
+
import { formatPeerDisplay } from "./peer-label";
|
|
18
|
+
import { ViaXComms } from "./via-x-comms";
|
|
19
|
+
import { peerStatusRpc } from "../shared/registry";
|
|
20
|
+
|
|
21
|
+
const X_COMMS_CONTENT_MAX_WIDTH = 600;
|
|
22
|
+
const PEER_STATUS_POLL_MS = 30_000;
|
|
23
|
+
|
|
24
|
+
type PeerStatusEntry = {
|
|
25
|
+
name: string;
|
|
26
|
+
value: string;
|
|
27
|
+
valid: boolean;
|
|
28
|
+
reachable: boolean;
|
|
29
|
+
error: string | null;
|
|
30
|
+
transport: "relay" | "direct" | "unknown";
|
|
31
|
+
serverId: string | null;
|
|
32
|
+
hostname: string | null;
|
|
33
|
+
version: string | null;
|
|
34
|
+
pid: number | null;
|
|
35
|
+
listen: string | null;
|
|
36
|
+
relayEnabled: boolean | null;
|
|
37
|
+
relayEndpoint: string | null;
|
|
38
|
+
providerCount: number | null;
|
|
39
|
+
providersAvailable: number | null;
|
|
40
|
+
hubState: string | null;
|
|
41
|
+
hubDaemonId: string | null;
|
|
42
|
+
hubOrigin: string | null;
|
|
43
|
+
hubLastError: string | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function ReachabilityBadge({ entry }: { entry: PeerStatusEntry }) {
|
|
47
|
+
if (!entry.reachable) return <Badge label="down" variant="danger" dot />;
|
|
48
|
+
if (!entry.valid) return <Badge label="invalid" variant="warning" dot />;
|
|
49
|
+
return <Badge label="up" variant="success" dot />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function relayLabel(entry: PeerStatusEntry): string {
|
|
53
|
+
if (entry.relayEnabled === null) return "unknown";
|
|
54
|
+
if (!entry.relayEnabled) return "disabled";
|
|
55
|
+
return entry.relayEndpoint ? `enabled · ${entry.relayEndpoint}` : "enabled";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function providersLabel(entry: PeerStatusEntry): string {
|
|
59
|
+
if (entry.providerCount === null) return "unknown";
|
|
60
|
+
const available = entry.providersAvailable ?? 0;
|
|
61
|
+
return `${available}/${entry.providerCount} available`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hubLabel(entry: PeerStatusEntry): string {
|
|
65
|
+
if (!entry.hubState) return "unknown";
|
|
66
|
+
const parts = [entry.hubState];
|
|
67
|
+
if (entry.hubOrigin) parts.push(entry.hubOrigin);
|
|
68
|
+
if (entry.hubLastError) parts.push(`error: ${entry.hubLastError}`);
|
|
69
|
+
return parts.join(" · ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function PeerCard({ entry }: { entry: PeerStatusEntry }) {
|
|
73
|
+
const { colors } = usePluginTheme();
|
|
74
|
+
return (
|
|
75
|
+
<Card variant="elevated">
|
|
76
|
+
<Card.Header
|
|
77
|
+
title={formatPeerDisplay(entry.name, entry.serverId)}
|
|
78
|
+
subtitle={entry.hostname && entry.hostname !== entry.name ? entry.hostname : undefined}
|
|
79
|
+
badge={<ReachabilityBadge entry={entry} />}
|
|
80
|
+
/>
|
|
81
|
+
<KeyValue label="Host value" value={entry.value} mono copyable truncate="end" truncateMaxLength={36} />
|
|
82
|
+
<KeyValueGroup>
|
|
83
|
+
<KeyValue label="Transport" value={entry.transport} mono />
|
|
84
|
+
<KeyValue label="Version" value={entry.version ?? ""} mono />
|
|
85
|
+
<KeyValue label="Listen" value={entry.listen ?? ""} copyable mono />
|
|
86
|
+
<KeyValue
|
|
87
|
+
label="PID"
|
|
88
|
+
value={entry.pid === null ? "" : String(entry.pid)}
|
|
89
|
+
mono
|
|
90
|
+
/>
|
|
91
|
+
<KeyValue label="Relay" value={relayLabel(entry)} mono />
|
|
92
|
+
<KeyValue label="Providers" value={providersLabel(entry)} mono />
|
|
93
|
+
</KeyValueGroup>
|
|
94
|
+
<KeyValue label="Hub relationship" value={hubLabel(entry)} mono />
|
|
95
|
+
{!entry.reachable ? (
|
|
96
|
+
<Text selectable style={{ color: colors.statusDanger, fontSize: 12 }}>
|
|
97
|
+
{entry.error ?? "unreachable"}
|
|
98
|
+
</Text>
|
|
99
|
+
) : null}
|
|
100
|
+
</Card>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Live reachability/identity view of the known peer daemons (#7). Renders the
|
|
106
|
+
* client-side x-comms registry and probes each registered host with its own
|
|
107
|
+
* `daemon.get_status`; an unreachable host shows as down without failing the
|
|
108
|
+
* rest of the surface.
|
|
109
|
+
*/
|
|
110
|
+
export function PeerStatusSurface({ theme }: PluginSurfaceProps) {
|
|
111
|
+
const { colors } = usePluginTheme();
|
|
112
|
+
const status = useRpcQuery(peerStatusRpc, {}, {
|
|
113
|
+
staleTime: PEER_STATUS_POLL_MS / 2,
|
|
114
|
+
refetchInterval: PEER_STATUS_POLL_MS,
|
|
115
|
+
refetchOnWindowFocus: false,
|
|
116
|
+
retry: false,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const entries = status.data?.results ?? [];
|
|
120
|
+
const summary = useMemo(() => {
|
|
121
|
+
const up = entries.filter((entry) => entry.reachable).length;
|
|
122
|
+
return { up, total: entries.length };
|
|
123
|
+
}, [entries]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<ModalBody
|
|
127
|
+
headerMode="pinned"
|
|
128
|
+
maxContentWidth={X_COMMS_CONTENT_MAX_WIDTH}
|
|
129
|
+
header={
|
|
130
|
+
<Card variant="elevated">
|
|
131
|
+
<Card.Header
|
|
132
|
+
title="Known peer daemons"
|
|
133
|
+
subtitle="Registry entries probed live with each peer's daemon.get_status."
|
|
134
|
+
badge={
|
|
135
|
+
status.isPending
|
|
136
|
+
? <Badge label="probing…" variant="neutral" dot />
|
|
137
|
+
: <Badge label={`${summary.up}/${summary.total} up`} variant={summary.total > 0 && summary.up === summary.total ? "success" : "warning"} dot />
|
|
138
|
+
}
|
|
139
|
+
action={
|
|
140
|
+
<Button
|
|
141
|
+
label="Refresh"
|
|
142
|
+
size="sm"
|
|
143
|
+
variant="secondary"
|
|
144
|
+
icon="RefreshCw"
|
|
145
|
+
loading={status.isFetching}
|
|
146
|
+
disabled={status.isFetching}
|
|
147
|
+
onPress={() => void status.refetch()}
|
|
148
|
+
/>
|
|
149
|
+
}
|
|
150
|
+
/>
|
|
151
|
+
</Card>
|
|
152
|
+
}
|
|
153
|
+
refreshing={status.isFetching}
|
|
154
|
+
onRefresh={() => void status.refetch()}
|
|
155
|
+
contentContainerStyle={{ paddingTop: 6 }}
|
|
156
|
+
>
|
|
157
|
+
<SectionHeader title="Peers" count={entries.length} />
|
|
158
|
+
|
|
159
|
+
{status.isPending ? (
|
|
160
|
+
<Card>
|
|
161
|
+
<Card.Header title="Probing peers…" subtitle="Dialing each registered daemon." />
|
|
162
|
+
</Card>
|
|
163
|
+
) : null}
|
|
164
|
+
|
|
165
|
+
{status.error ? (
|
|
166
|
+
<Card variant="elevated">
|
|
167
|
+
<Card.Header
|
|
168
|
+
title="Probe RPC failed"
|
|
169
|
+
subtitle={status.error.message}
|
|
170
|
+
badge={<Badge label="error" variant="danger" dot />}
|
|
171
|
+
/>
|
|
172
|
+
</Card>
|
|
173
|
+
) : null}
|
|
174
|
+
|
|
175
|
+
{!status.isPending && !status.error && entries.length === 0 ? (
|
|
176
|
+
<EmptyState
|
|
177
|
+
title="No known peer daemons."
|
|
178
|
+
description="Add a paired daemon in the Current surface; it will appear here with live status."
|
|
179
|
+
/>
|
|
180
|
+
) : null}
|
|
181
|
+
|
|
182
|
+
{entries.map((entry) => (
|
|
183
|
+
<PeerCard key={entry.name} entry={entry} />
|
|
184
|
+
))}
|
|
185
|
+
|
|
186
|
+
{entries.length > 0 ? (
|
|
187
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
188
|
+
<StatusDot
|
|
189
|
+
variant={summary.total > 0 && summary.up === summary.total ? "success" : "warning"}
|
|
190
|
+
size="sm"
|
|
191
|
+
/>
|
|
192
|
+
<Text style={{ color: colors.foregroundMuted, fontSize: 11 }}>
|
|
193
|
+
{summary.up} of {summary.total} known peers reachable.
|
|
194
|
+
</Text>
|
|
195
|
+
</View>
|
|
196
|
+
) : null}
|
|
197
|
+
|
|
198
|
+
<ViaXComms theme={theme} />
|
|
199
|
+
</ModalBody>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
|
|
3
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ActionBar,
|
|
6
|
+
Badge,
|
|
7
|
+
Button,
|
|
8
|
+
Card,
|
|
9
|
+
Collapsible,
|
|
10
|
+
EmptyState,
|
|
11
|
+
FormRow,
|
|
12
|
+
KeyValue,
|
|
13
|
+
KeyValueGroup,
|
|
14
|
+
ModalBody,
|
|
15
|
+
SectionHeader,
|
|
16
|
+
StatusDot,
|
|
17
|
+
TextInput,
|
|
18
|
+
Toggle,
|
|
19
|
+
} from "paseo-plugin-helper/client";
|
|
20
|
+
import { formatPeerDisplay } from "./peer-label";
|
|
21
|
+
import { ViaXComms } from "./via-x-comms";
|
|
22
|
+
import {
|
|
23
|
+
daemonAddRpc,
|
|
24
|
+
daemonHealthRpc,
|
|
25
|
+
daemonRemoveRpc,
|
|
26
|
+
directHostMismatch,
|
|
27
|
+
identitySyncRpc,
|
|
28
|
+
registryReadRpc,
|
|
29
|
+
serverStatusRpc,
|
|
30
|
+
snapshotRefreshRpc,
|
|
31
|
+
uiPrefsGetRpc,
|
|
32
|
+
uiPrefsSetRpc,
|
|
33
|
+
} from "../shared/registry";
|
|
34
|
+
|
|
35
|
+
const HOST_FORM_HINT =
|
|
36
|
+
"Full pairing link (https://app.paseo.sh/#offer=…) or a direct daemon host (host:port, tcp://…, unix://…).";
|
|
37
|
+
|
|
38
|
+
// Keep the settings surface a readable centered column instead of stretching
|
|
39
|
+
// edge-to-edge on large viewports. The cap lives in the helper
|
|
40
|
+
// (`ModalBody maxContentWidth`); this is the only place the surface picks
|
|
41
|
+
// the value.
|
|
42
|
+
const X_COMMS_CONTENT_MAX_WIDTH = 600;
|
|
43
|
+
|
|
44
|
+
// Prototype settings surface for #97. Built only from paseo-plugin-helper/client
|
|
45
|
+
// primitives; the current page in main.tsx is untouched and stays the default tab.
|
|
46
|
+
// Wired to the live registry/health/server/prefs RPCs — no local mock state.
|
|
47
|
+
|
|
48
|
+
function ReachabilityBadge({
|
|
49
|
+
health,
|
|
50
|
+
}: {
|
|
51
|
+
health?: { reachable: boolean; agentCount: number | null } | null;
|
|
52
|
+
}) {
|
|
53
|
+
if (!health) return <Badge label="checking…" variant="neutral" dot />;
|
|
54
|
+
if (health.reachable) {
|
|
55
|
+
return (
|
|
56
|
+
<Badge
|
|
57
|
+
label={health.agentCount !== null ? `reachable (${health.agentCount} agents)` : "reachable"}
|
|
58
|
+
variant="success"
|
|
59
|
+
dot
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return <Badge label="unreachable" variant="danger" dot />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ErrorRow({ label, message }: { label: string; message?: string | null }) {
|
|
67
|
+
if (!message) return null;
|
|
68
|
+
return (
|
|
69
|
+
<FormRow label={label} description={message}>
|
|
70
|
+
<Badge label="error" variant="danger" dot />
|
|
71
|
+
</FormRow>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function SettingsPrototype({ theme }: PluginSurfaceProps) {
|
|
76
|
+
const callRead = useRpc(registryReadRpc);
|
|
77
|
+
const callHealth = useRpc(daemonHealthRpc);
|
|
78
|
+
const callAdd = useRpc(daemonAddRpc);
|
|
79
|
+
const callRemove = useRpc(daemonRemoveRpc);
|
|
80
|
+
const callPrefsGet = useRpc(uiPrefsGetRpc);
|
|
81
|
+
const callPrefsSet = useRpc(uiPrefsSetRpc);
|
|
82
|
+
const callSnapshotRefresh = useRpc(snapshotRefreshRpc);
|
|
83
|
+
const callIdentitySync = useRpc(identitySyncRpc);
|
|
84
|
+
const callStatus = useRpc(serverStatusRpc);
|
|
85
|
+
|
|
86
|
+
const read = useQuery({ queryKey: ["registry-read"], queryFn: () => callRead({}) });
|
|
87
|
+
const health = useQuery({
|
|
88
|
+
queryKey: ["daemon-health"],
|
|
89
|
+
queryFn: () => callHealth({}),
|
|
90
|
+
staleTime: 30_000,
|
|
91
|
+
refetchOnWindowFocus: false,
|
|
92
|
+
});
|
|
93
|
+
const prefs = useQuery({ queryKey: ["ui-prefs"], queryFn: () => callPrefsGet({}) });
|
|
94
|
+
const status = useQuery({ queryKey: ["server-status"], queryFn: () => callStatus({}) });
|
|
95
|
+
|
|
96
|
+
const [newName, setNewName] = useState("");
|
|
97
|
+
const [newValue, setNewValue] = useState("");
|
|
98
|
+
const [confirmRemove, setConfirmRemove] = useState<string | null>(null);
|
|
99
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
100
|
+
|
|
101
|
+
const refetchAll = useCallback(() => {
|
|
102
|
+
void read.refetch();
|
|
103
|
+
void health.refetch();
|
|
104
|
+
void prefs.refetch();
|
|
105
|
+
}, [read, health, prefs]);
|
|
106
|
+
|
|
107
|
+
const add = useMutation({ mutationFn: callAdd, onSuccess: refetchAll });
|
|
108
|
+
const remove = useMutation({
|
|
109
|
+
mutationFn: callRemove,
|
|
110
|
+
onSuccess: () => {
|
|
111
|
+
setConfirmRemove(null);
|
|
112
|
+
refetchAll();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const prefsSet = useMutation({ mutationFn: callPrefsSet });
|
|
116
|
+
const identitySync = useMutation({
|
|
117
|
+
mutationFn: () => callIdentitySync({}),
|
|
118
|
+
onSuccess: () => void read.refetch(),
|
|
119
|
+
});
|
|
120
|
+
const snapshotRefresh = useMutation({
|
|
121
|
+
mutationFn: () => callSnapshotRefresh({}),
|
|
122
|
+
onSuccess: () => {
|
|
123
|
+
setRefreshing(false);
|
|
124
|
+
refetchAll();
|
|
125
|
+
void status.refetch();
|
|
126
|
+
},
|
|
127
|
+
onError: () => setRefreshing(false),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const handleRefresh = useCallback(() => {
|
|
131
|
+
setRefreshing(true);
|
|
132
|
+
void identitySync
|
|
133
|
+
.mutateAsync()
|
|
134
|
+
.then(() => snapshotRefresh.mutate())
|
|
135
|
+
.catch(() => snapshotRefresh.mutate());
|
|
136
|
+
}, [identitySync, snapshotRefresh]);
|
|
137
|
+
|
|
138
|
+
const healthByName = useMemo(() => {
|
|
139
|
+
const map: Record<string, { reachable: boolean; error: string | null; agentCount: number | null }> = {};
|
|
140
|
+
for (const result of health.data?.results ?? []) map[result.name] = result;
|
|
141
|
+
return map;
|
|
142
|
+
}, [health.data]);
|
|
143
|
+
|
|
144
|
+
const daemonEnabled = prefs.data?.daemonEnabled ?? {};
|
|
145
|
+
const prereqsCollapsed = prefs.data?.prereqsCollapsed ?? true;
|
|
146
|
+
const presenceEnabled = prefs.data?.presenceEnabled ?? true;
|
|
147
|
+
const injectionEnabled = prefs.data?.injectionEnabled ?? true;
|
|
148
|
+
const outboxExpirySeconds = prefs.data?.outboxExpirySeconds ?? 600;
|
|
149
|
+
const [expiryDraft, setExpiryDraft] = useState<string | null>(null);
|
|
150
|
+
|
|
151
|
+
const persistPrefs = useCallback(
|
|
152
|
+
(patch: {
|
|
153
|
+
prereqsCollapsed?: boolean;
|
|
154
|
+
presenceEnabled?: boolean;
|
|
155
|
+
injectionEnabled?: boolean;
|
|
156
|
+
outboxExpirySeconds?: number;
|
|
157
|
+
daemonEnabled?: Record<string, boolean>;
|
|
158
|
+
}) => {
|
|
159
|
+
prefsSet.mutate(
|
|
160
|
+
{
|
|
161
|
+
prereqsCollapsed,
|
|
162
|
+
presenceEnabled,
|
|
163
|
+
injectionEnabled,
|
|
164
|
+
...patch,
|
|
165
|
+
},
|
|
166
|
+
{ onSuccess: () => void prefs.refetch() },
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
[prefsSet, prefs, prereqsCollapsed, presenceEnabled, injectionEnabled],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const commitExpiry = useCallback(() => {
|
|
173
|
+
const seconds = Number.parseInt(expiryDraft ?? "", 10);
|
|
174
|
+
setExpiryDraft(null);
|
|
175
|
+
if (!Number.isFinite(seconds) || seconds <= 0 || seconds === outboxExpirySeconds) return;
|
|
176
|
+
persistPrefs({ outboxExpirySeconds: seconds });
|
|
177
|
+
}, [expiryDraft, outboxExpirySeconds, persistPrefs]);
|
|
178
|
+
|
|
179
|
+
const canAdd = newName.trim().length > 0 && newValue.trim().length > 0;
|
|
180
|
+
const mismatch =
|
|
181
|
+
newName.trim().length > 0 && newValue.trim().length > 0 && !newValue.includes("#offer=")
|
|
182
|
+
? directHostMismatch(newName.trim(), newValue.trim())
|
|
183
|
+
: null;
|
|
184
|
+
const daemonCount = read.data?.daemons.length ?? 0;
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<ModalBody
|
|
188
|
+
headerMode="pinned"
|
|
189
|
+
maxContentWidth={X_COMMS_CONTENT_MAX_WIDTH}
|
|
190
|
+
header={
|
|
191
|
+
<Card variant="elevated">
|
|
192
|
+
<Card.Header
|
|
193
|
+
title="X-comms settings"
|
|
194
|
+
subtitle="Prototype surface built from paseo-plugin-helper primitives."
|
|
195
|
+
badge={<Badge label="prototype" variant="warning" />}
|
|
196
|
+
action={
|
|
197
|
+
<Button
|
|
198
|
+
label="Refresh"
|
|
199
|
+
size="sm"
|
|
200
|
+
variant="secondary"
|
|
201
|
+
icon="RefreshCw"
|
|
202
|
+
loading={refreshing}
|
|
203
|
+
disabled={refreshing}
|
|
204
|
+
onPress={handleRefresh}
|
|
205
|
+
/>
|
|
206
|
+
}
|
|
207
|
+
/>
|
|
208
|
+
</Card>
|
|
209
|
+
}
|
|
210
|
+
refreshing={refreshing}
|
|
211
|
+
onRefresh={handleRefresh}
|
|
212
|
+
contentContainerStyle={{ paddingTop: 6 }}
|
|
213
|
+
>
|
|
214
|
+
<SectionHeader title="Server" />
|
|
215
|
+
<Card variant="elevated">
|
|
216
|
+
<Card.Header
|
|
217
|
+
title={status.data?.installed ? `Bundled server · v${status.data.version ?? "?"}` : "Bundled server"}
|
|
218
|
+
subtitle={status.data?.error ?? status.data?.installPath ?? "Locating bundled server…"}
|
|
219
|
+
/>
|
|
220
|
+
</Card>
|
|
221
|
+
|
|
222
|
+
<SectionHeader title="Daemons" count={daemonCount} />
|
|
223
|
+
{read.isPending ? (
|
|
224
|
+
<Card>
|
|
225
|
+
<Card.Header title="Loading…" subtitle="Reading the daemon registry." />
|
|
226
|
+
</Card>
|
|
227
|
+
) : null}
|
|
228
|
+
{!read.isPending && read.data && !read.data.exists ? (
|
|
229
|
+
<EmptyState title="No registry file yet." description="Add your first daemon below." />
|
|
230
|
+
) : null}
|
|
231
|
+
{read.data && !read.data.validJson ? (
|
|
232
|
+
<Card variant="elevated">
|
|
233
|
+
<Card.Header
|
|
234
|
+
title="Registry is not valid JSON"
|
|
235
|
+
subtitle={read.data.parseError ?? undefined}
|
|
236
|
+
badge={<Badge label="error" variant="danger" dot />}
|
|
237
|
+
/>
|
|
238
|
+
</Card>
|
|
239
|
+
) : null}
|
|
240
|
+
{health.isPending && (read.data?.daemons.length ?? 0) > 0 ? (
|
|
241
|
+
<Card>
|
|
242
|
+
<Card.Header title="Checking health…" subtitle="Probing registered daemons." />
|
|
243
|
+
</Card>
|
|
244
|
+
) : null}
|
|
245
|
+
|
|
246
|
+
{(read.data?.daemons ?? []).map((daemon) => {
|
|
247
|
+
const h = healthByName[daemon.name];
|
|
248
|
+
const enabled = daemonEnabled[daemon.name] !== false;
|
|
249
|
+
const healthDetail = h
|
|
250
|
+
? (h.error ?? (h.reachable ? `${h.agentCount ?? 0} agents reachable` : "unreachable"))
|
|
251
|
+
: "Health check pending…";
|
|
252
|
+
return (
|
|
253
|
+
<Card key={daemon.name} variant="elevated">
|
|
254
|
+
<Card.Header
|
|
255
|
+
title={formatPeerDisplay(daemon.name, daemon.serverId)}
|
|
256
|
+
subtitle={daemon.hostname && daemon.hostname !== daemon.name ? daemon.hostname : undefined}
|
|
257
|
+
badge={enabled ? <ReachabilityBadge health={h} /> : <Badge label="disabled" variant="neutral" dot />}
|
|
258
|
+
/>
|
|
259
|
+
<KeyValue
|
|
260
|
+
label="Host value"
|
|
261
|
+
value={daemon.value}
|
|
262
|
+
mono
|
|
263
|
+
copyable
|
|
264
|
+
truncate="end"
|
|
265
|
+
truncateMaxLength={36}
|
|
266
|
+
layout="inline"
|
|
267
|
+
/>
|
|
268
|
+
<FormRow
|
|
269
|
+
layout="inline"
|
|
270
|
+
label="Health"
|
|
271
|
+
description={enabled ? healthDetail : `Disabled in preferences · ${healthDetail}`}
|
|
272
|
+
>
|
|
273
|
+
<StatusDot
|
|
274
|
+
variant={enabled ? (h?.reachable ? "success" : h ? "danger" : "neutral") : "neutral"}
|
|
275
|
+
size="sm"
|
|
276
|
+
/>
|
|
277
|
+
</FormRow>
|
|
278
|
+
<FormRow
|
|
279
|
+
layout="inline"
|
|
280
|
+
label="Enabled"
|
|
281
|
+
description="Include this daemon in the x-comms mesh."
|
|
282
|
+
>
|
|
283
|
+
<Toggle
|
|
284
|
+
value={enabled}
|
|
285
|
+
disabled={prefs.isPending}
|
|
286
|
+
onValueChange={(next) =>
|
|
287
|
+
persistPrefs({ daemonEnabled: { ...daemonEnabled, [daemon.name]: next } })
|
|
288
|
+
}
|
|
289
|
+
/>
|
|
290
|
+
</FormRow>
|
|
291
|
+
{confirmRemove === daemon.name ? (
|
|
292
|
+
<ActionBar align="flex-start">
|
|
293
|
+
<Button
|
|
294
|
+
label="Confirm remove"
|
|
295
|
+
size="sm"
|
|
296
|
+
variant="danger"
|
|
297
|
+
loading={remove.isPending}
|
|
298
|
+
disabled={remove.isPending}
|
|
299
|
+
onPress={() => remove.mutate({ name: daemon.name })}
|
|
300
|
+
/>
|
|
301
|
+
<Button label="Cancel" size="sm" variant="secondary" onPress={() => setConfirmRemove(null)} />
|
|
302
|
+
</ActionBar>
|
|
303
|
+
) : (
|
|
304
|
+
<ActionBar align="flex-start">
|
|
305
|
+
<Button label="Remove" size="sm" variant="ghost" onPress={() => setConfirmRemove(daemon.name)} />
|
|
306
|
+
</ActionBar>
|
|
307
|
+
)}
|
|
308
|
+
</Card>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
<ErrorRow label="Remove failed" message={remove.error?.message} />
|
|
312
|
+
|
|
313
|
+
<SectionHeader title="Add daemon" />
|
|
314
|
+
<Card variant="elevated">
|
|
315
|
+
<FormRow label="Name" description="The daemon's real name; derived automatically for relay links.">
|
|
316
|
+
<TextInput value={newName} onChangeText={setNewName} autoCapitalize="none" autoCorrect={false} />
|
|
317
|
+
</FormRow>
|
|
318
|
+
<FormRow label="Host value" description={HOST_FORM_HINT}>
|
|
319
|
+
<TextInput
|
|
320
|
+
mono
|
|
321
|
+
value={newValue}
|
|
322
|
+
onChangeText={setNewValue}
|
|
323
|
+
autoCapitalize="none"
|
|
324
|
+
autoCorrect={false}
|
|
325
|
+
placeholder={HOST_FORM_HINT}
|
|
326
|
+
errorText={mismatch ?? undefined}
|
|
327
|
+
/>
|
|
328
|
+
</FormRow>
|
|
329
|
+
<ErrorRow label="Add failed" message={add.error?.message} />
|
|
330
|
+
<ActionBar align="flex-start">
|
|
331
|
+
<Button
|
|
332
|
+
label="Add daemon"
|
|
333
|
+
variant="primary"
|
|
334
|
+
loading={add.isPending}
|
|
335
|
+
disabled={!canAdd || add.isPending}
|
|
336
|
+
onPress={() => add.mutate({ name: newName.trim(), value: newValue.trim() })}
|
|
337
|
+
/>
|
|
338
|
+
</ActionBar>
|
|
339
|
+
</Card>
|
|
340
|
+
|
|
341
|
+
<SectionHeader title="Preferences" />
|
|
342
|
+
<Card variant="elevated">
|
|
343
|
+
<Collapsible
|
|
344
|
+
title="Prerequisites"
|
|
345
|
+
subtitle="Bundled server paths and checks."
|
|
346
|
+
icon="Server"
|
|
347
|
+
isExpanded={!prereqsCollapsed}
|
|
348
|
+
onToggle={(expanded) => persistPrefs({ prereqsCollapsed: !expanded })}
|
|
349
|
+
>
|
|
350
|
+
<KeyValueGroup>
|
|
351
|
+
<KeyValue
|
|
352
|
+
label="Install path"
|
|
353
|
+
value={status.data?.installPath ?? ""}
|
|
354
|
+
mono
|
|
355
|
+
copyable={Boolean(status.data?.installPath)}
|
|
356
|
+
truncate="path"
|
|
357
|
+
/>
|
|
358
|
+
</KeyValueGroup>
|
|
359
|
+
</Collapsible>
|
|
360
|
+
<FormRow label="Presence announcements" description="Broadcast this daemon's agents to the x-comms mesh.">
|
|
361
|
+
<Toggle
|
|
362
|
+
value={presenceEnabled}
|
|
363
|
+
disabled={prefs.isPending}
|
|
364
|
+
onValueChange={(next) => persistPrefs({ presenceEnabled: next })}
|
|
365
|
+
/>
|
|
366
|
+
</FormRow>
|
|
367
|
+
<FormRow
|
|
368
|
+
label="Agent introduction injection"
|
|
369
|
+
description="Inject x-comms peer context when agents are created."
|
|
370
|
+
>
|
|
371
|
+
<Toggle
|
|
372
|
+
value={injectionEnabled}
|
|
373
|
+
disabled={prefs.isPending}
|
|
374
|
+
onValueChange={(next) => persistPrefs({ injectionEnabled: next })}
|
|
375
|
+
/>
|
|
376
|
+
</FormRow>
|
|
377
|
+
<FormRow
|
|
378
|
+
label="Outbox expiry (seconds)"
|
|
379
|
+
description="How long an undelivered message is retried before the sender is notified. Default 600. Press Enter to save."
|
|
380
|
+
>
|
|
381
|
+
<TextInput
|
|
382
|
+
value={expiryDraft ?? String(outboxExpirySeconds)}
|
|
383
|
+
onChangeText={setExpiryDraft}
|
|
384
|
+
keyboardType="number-pad"
|
|
385
|
+
disabled={prefs.isPending}
|
|
386
|
+
onSubmitEditing={commitExpiry}
|
|
387
|
+
/>
|
|
388
|
+
</FormRow>
|
|
389
|
+
<ErrorRow label="Preferences update failed" message={prefsSet.error?.message} />
|
|
390
|
+
</Card>
|
|
391
|
+
|
|
392
|
+
<ViaXComms theme={theme} />
|
|
393
|
+
</ModalBody>
|
|
394
|
+
);
|
|
395
|
+
}
|