@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
package/client/main.tsx
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
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 { Text, View } from "react-native";
|
|
5
|
+
import { Modal } from "@getpaseo/plugin/client/react-native";
|
|
6
|
+
import {
|
|
7
|
+
ActionBar,
|
|
8
|
+
Badge,
|
|
9
|
+
Button,
|
|
10
|
+
Card,
|
|
11
|
+
Collapsible,
|
|
12
|
+
EmptyState,
|
|
13
|
+
FormRow,
|
|
14
|
+
KeyValue,
|
|
15
|
+
KeyValueGroup,
|
|
16
|
+
ModalBody,
|
|
17
|
+
ModalContent,
|
|
18
|
+
SectionHeader,
|
|
19
|
+
Tabs,
|
|
20
|
+
TextInput,
|
|
21
|
+
usePluginTheme,
|
|
22
|
+
} from "paseo-plugin-helper/client";
|
|
23
|
+
import { formatPeerDisplay } from "./peer-label";
|
|
24
|
+
import { PeerStatusSurface } from "./peer-status";
|
|
25
|
+
import { SettingsPrototype } from "./settings-prototype";
|
|
26
|
+
import { ViaXComms } from "./via-x-comms";
|
|
27
|
+
import {
|
|
28
|
+
registryReadRpc,
|
|
29
|
+
daemonAddRpc,
|
|
30
|
+
daemonUpdateRpc,
|
|
31
|
+
daemonRemoveRpc,
|
|
32
|
+
daemonHealthRpc,
|
|
33
|
+
daemonProbeRpc,
|
|
34
|
+
uiPrefsGetRpc,
|
|
35
|
+
uiPrefsSetRpc,
|
|
36
|
+
snapshotRefreshRpc,
|
|
37
|
+
daemonDumpRpc,
|
|
38
|
+
identitySyncRpc,
|
|
39
|
+
serverStatusRpc,
|
|
40
|
+
introspectAgentsRpc,
|
|
41
|
+
introduceAgentsRpc,
|
|
42
|
+
directHostMismatch,
|
|
43
|
+
} from "../shared/registry";
|
|
44
|
+
|
|
45
|
+
const HOST_FORM_HINT =
|
|
46
|
+
"Full pairing link (https://app.paseo.sh/#offer=…) or a direct daemon host (host:port, tcp://…, unix://…).";
|
|
47
|
+
|
|
48
|
+
// Keep the main surface a readable centered column instead of stretching
|
|
49
|
+
// edge-to-edge on large viewports. The cap lives in the helper
|
|
50
|
+
// (`ModalBody maxContentWidth`); this is the only place the surface picks
|
|
51
|
+
// the value.
|
|
52
|
+
const X_COMMS_CONTENT_MAX_WIDTH = 600;
|
|
53
|
+
|
|
54
|
+
// Raw View/Text are kept only for plain content and layout composition
|
|
55
|
+
// (headings, error notices, debug dump lines, modal footers). Every
|
|
56
|
+
// interactive control, card, form row, status indicator, key/value display
|
|
57
|
+
// and empty/loading state uses a paseo-plugin-helper primitive.
|
|
58
|
+
function Notice({
|
|
59
|
+
children,
|
|
60
|
+
tone = "danger",
|
|
61
|
+
}: {
|
|
62
|
+
children: React.ReactNode;
|
|
63
|
+
tone?: "danger" | "muted";
|
|
64
|
+
}) {
|
|
65
|
+
const { colors } = usePluginTheme();
|
|
66
|
+
return (
|
|
67
|
+
<Text
|
|
68
|
+
selectable
|
|
69
|
+
style={{
|
|
70
|
+
color: tone === "danger" ? colors.statusDanger : colors.foregroundMuted,
|
|
71
|
+
fontSize: 12,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</Text>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function HealthBadge({
|
|
80
|
+
health,
|
|
81
|
+
}: {
|
|
82
|
+
health?: { reachable: boolean; agentCount: number | null } | null;
|
|
83
|
+
}) {
|
|
84
|
+
if (!health) return <Badge label="checking…" variant="neutral" dot />;
|
|
85
|
+
if (health.reachable) {
|
|
86
|
+
return (
|
|
87
|
+
<Badge
|
|
88
|
+
label={health.agentCount !== null ? `reachable (${health.agentCount} agents)` : "reachable"}
|
|
89
|
+
variant="success"
|
|
90
|
+
dot
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return <Badge label="unreachable" variant="danger" dot />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// #97: the operator asked for an entirely new prototype surface alongside the
|
|
98
|
+
// current one. MainSurface keeps the existing page as the default "Current" tab
|
|
99
|
+
// and renders the helper-primitives prototype behind the marked "Prototype" tab.
|
|
100
|
+
// #7 adds a third "Peers" tab: the known daemon registry with a live per-peer
|
|
101
|
+
// daemon.get_status probe. It is additive — the other two tabs are untouched.
|
|
102
|
+
export function MainSurface(props: PluginSurfaceProps) {
|
|
103
|
+
const { colors } = usePluginTheme();
|
|
104
|
+
const [surface, setSurface] = useState<"current" | "prototype" | "peers">("current");
|
|
105
|
+
return (
|
|
106
|
+
<View style={{ flex: 1, minHeight: 0, width: "100%", backgroundColor: colors.surface0 }}>
|
|
107
|
+
<View style={{ paddingHorizontal: 12, paddingTop: 12 }}>
|
|
108
|
+
<Tabs
|
|
109
|
+
tabs={[
|
|
110
|
+
{ id: "current", label: "Current" },
|
|
111
|
+
{ id: "prototype", label: "Prototype", badge: "new" },
|
|
112
|
+
{ id: "peers", label: "Peers" },
|
|
113
|
+
]}
|
|
114
|
+
activeTab={surface}
|
|
115
|
+
onTabChange={(id) => setSurface(id === "prototype" ? "prototype" : id === "peers" ? "peers" : "current")}
|
|
116
|
+
/>
|
|
117
|
+
</View>
|
|
118
|
+
{surface === "current" ? (
|
|
119
|
+
<CurrentSurface {...props} />
|
|
120
|
+
) : surface === "prototype" ? (
|
|
121
|
+
<SettingsPrototype {...props} />
|
|
122
|
+
) : (
|
|
123
|
+
<PeerStatusSurface {...props} />
|
|
124
|
+
)}
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function CurrentSurface({ theme }: PluginSurfaceProps) {
|
|
130
|
+
const { colors } = usePluginTheme();
|
|
131
|
+
const callRead = useRpc(registryReadRpc);
|
|
132
|
+
const callAdd = useRpc(daemonAddRpc);
|
|
133
|
+
const callUpdate = useRpc(daemonUpdateRpc);
|
|
134
|
+
const callRemove = useRpc(daemonRemoveRpc);
|
|
135
|
+
const callHealth = useRpc(daemonHealthRpc);
|
|
136
|
+
const callProbe = useRpc(daemonProbeRpc);
|
|
137
|
+
const callPrefsGet = useRpc(uiPrefsGetRpc);
|
|
138
|
+
const callPrefsSet = useRpc(uiPrefsSetRpc);
|
|
139
|
+
const callSnapshotRefresh = useRpc(snapshotRefreshRpc);
|
|
140
|
+
const callDump = useRpc(daemonDumpRpc);
|
|
141
|
+
const callIdentitySync = useRpc(identitySyncRpc);
|
|
142
|
+
const callStatus = useRpc(serverStatusRpc);
|
|
143
|
+
const callIntrospect = useRpc(introspectAgentsRpc);
|
|
144
|
+
const callIntroduce = useRpc(introduceAgentsRpc);
|
|
145
|
+
const [newName, setNewName] = useState("");
|
|
146
|
+
const [newValue, setNewValue] = useState("");
|
|
147
|
+
const [prereqsCollapsed, setPrereqsCollapsed] = useState<boolean>(true);
|
|
148
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
149
|
+
const [refreshedAt, setRefreshedAt] = useState<string | null>(null);
|
|
150
|
+
const [dumpState, setDumpState] = useState<Record<string, unknown> | null>(null);
|
|
151
|
+
const [dumpDaemon, setDumpDaemon] = useState<string | null>(null);
|
|
152
|
+
const [dumpOpen, setDumpOpen] = useState(false);
|
|
153
|
+
|
|
154
|
+
// Introduce: which picker (1 or 2) is expanded, selections, editable message.
|
|
155
|
+
const [expandedPicker, setExpandedPicker] = useState<1 | 2 | null>(null);
|
|
156
|
+
const [introFirst, setIntroFirst] = useState<{ daemon: string; agentId: string; shortId: string; name: string } | null>(null);
|
|
157
|
+
const [introSecond, setIntroSecond] = useState<{ daemon: string; agentId: string; shortId: string; name: string } | null>(null);
|
|
158
|
+
const [introMessage, setIntroMessage] = useState(
|
|
159
|
+
"Hello! I was asked to introduce you. This daemon can communicate with you directly via paseo-x-comms.",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const read = useQuery({ queryKey: ["registry-read"], queryFn: () => callRead({}) });
|
|
163
|
+
const health = useQuery({
|
|
164
|
+
queryKey: ["daemon-health"],
|
|
165
|
+
queryFn: () => callHealth({}),
|
|
166
|
+
// Health is a network probe; never let it block the UI.
|
|
167
|
+
staleTime: 30_000,
|
|
168
|
+
refetchOnWindowFocus: false,
|
|
169
|
+
});
|
|
170
|
+
const probe = useMutation({ mutationFn: callProbe });
|
|
171
|
+
const prefs = useQuery({ queryKey: ["ui-prefs"], queryFn: () => callPrefsGet({}) });
|
|
172
|
+
React.useEffect(() => {
|
|
173
|
+
if (prefs.data) setPrereqsCollapsed(prefs.data.prereqsCollapsed);
|
|
174
|
+
}, [prefs.data]);
|
|
175
|
+
const prefsSet = useMutation({ mutationFn: callPrefsSet });
|
|
176
|
+
const identitySync = useMutation({
|
|
177
|
+
mutationFn: () => callIdentitySync({}),
|
|
178
|
+
onSuccess: () => void read.refetch(),
|
|
179
|
+
});
|
|
180
|
+
const snapshotRefresh = useMutation({
|
|
181
|
+
mutationFn: () => callSnapshotRefresh({}),
|
|
182
|
+
onSuccess: (result) => {
|
|
183
|
+
setRefreshedAt(new Date(result.updatedAt).toLocaleTimeString());
|
|
184
|
+
setRefreshing(false);
|
|
185
|
+
void read.refetch();
|
|
186
|
+
void health.refetch();
|
|
187
|
+
void introspect.refetch();
|
|
188
|
+
void status.refetch();
|
|
189
|
+
},
|
|
190
|
+
onError: () => setRefreshing(false),
|
|
191
|
+
});
|
|
192
|
+
const dump = useMutation({
|
|
193
|
+
mutationFn: callDump,
|
|
194
|
+
onSuccess: (data) => setDumpState(data as unknown as Record<string, unknown>),
|
|
195
|
+
});
|
|
196
|
+
const introspect = useQuery({
|
|
197
|
+
queryKey: ["introspect"],
|
|
198
|
+
queryFn: () => callIntrospect({}),
|
|
199
|
+
staleTime: 30_000,
|
|
200
|
+
});
|
|
201
|
+
const introduce = useMutation({ mutationFn: callIntroduce });
|
|
202
|
+
const status = useQuery({ queryKey: ["server-status"], queryFn: () => callStatus({}) });
|
|
203
|
+
const add = useMutation({ mutationFn: callAdd });
|
|
204
|
+
const update = useMutation({ mutationFn: callUpdate });
|
|
205
|
+
const remove = useMutation({ mutationFn: callRemove });
|
|
206
|
+
|
|
207
|
+
// Editing state per daemon: current draft name + value (keyed by original name).
|
|
208
|
+
const [edits, setEdits] = useState<Record<string, { name: string; value: string }>>({});
|
|
209
|
+
const [expandedError, setExpandedError] = useState<Set<string>>(new Set());
|
|
210
|
+
const [rowProbe, setRowProbe] = useState<Record<string, { value: string; error: string | null; saved: boolean } | "pending">>({});
|
|
211
|
+
const [addProbe, setAddProbe] = useState<{ value: string; error: string | null } | null>(null);
|
|
212
|
+
const [adding, setAdding] = useState(false);
|
|
213
|
+
const [editing, setEditing] = useState<Set<string>>(new Set());
|
|
214
|
+
|
|
215
|
+
const applyResult = useCallback(
|
|
216
|
+
(result: unknown) => {
|
|
217
|
+
const r = result as { error?: string | null };
|
|
218
|
+
if (r?.error) throw new Error(r.error);
|
|
219
|
+
void read.refetch();
|
|
220
|
+
void health.refetch();
|
|
221
|
+
},
|
|
222
|
+
[read, health],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Probe a candidate. Returns "ok" (reachable), "unreachable" (show inline
|
|
226
|
+
// confirm), or "invalid" (format error). No modal; the caller renders the
|
|
227
|
+
// result at the row.
|
|
228
|
+
const probeValue = useCallback(
|
|
229
|
+
(value: string): Promise<"ok" | "unreachable" | "invalid"> =>
|
|
230
|
+
new Promise((resolve) => {
|
|
231
|
+
probe.mutate(
|
|
232
|
+
{ value },
|
|
233
|
+
{
|
|
234
|
+
onSuccess: (result) => {
|
|
235
|
+
if (!result.valid) resolve("invalid");
|
|
236
|
+
else if (!result.reachable) resolve("unreachable");
|
|
237
|
+
else resolve("ok");
|
|
238
|
+
},
|
|
239
|
+
onError: () => resolve("unreachable"),
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
}),
|
|
243
|
+
[probe],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const deriveHost = useCallback((value: string) => {
|
|
247
|
+
const offer = value.match(/#offer=([A-Za-z0-9_-]+)/);
|
|
248
|
+
if (offer && newName.trim().length === 0) {
|
|
249
|
+
try {
|
|
250
|
+
const payload = JSON.parse(Buffer.from(offer[1], "base64").toString("utf8"));
|
|
251
|
+
if (typeof payload.serverId === "string") setNewName(payload.serverId);
|
|
252
|
+
} catch { /* ignore */ }
|
|
253
|
+
}
|
|
254
|
+
}, [newName]);
|
|
255
|
+
|
|
256
|
+
const handleAdd = useCallback(() => {
|
|
257
|
+
const name = newName.trim();
|
|
258
|
+
const value = newValue.trim();
|
|
259
|
+
if (!name || !value) return;
|
|
260
|
+
setAdding(true);
|
|
261
|
+
probeValue(value).then((outcome) => {
|
|
262
|
+
if (outcome === "invalid") {
|
|
263
|
+
setAddProbe({ value, error: "not a valid host form" });
|
|
264
|
+
setAdding(false);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (outcome === "unreachable") {
|
|
268
|
+
setAddProbe({ value, error: null }); // inline confirm shown at the add row
|
|
269
|
+
setAdding(false);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
setAddProbe(null);
|
|
273
|
+
add.mutate({ name, value }, { onSuccess: applyResult, onSettled: () => setAdding(false) });
|
|
274
|
+
});
|
|
275
|
+
}, [add, newName, newValue, applyResult, probeValue]);
|
|
276
|
+
|
|
277
|
+
const confirmAddAnyway = useCallback(() => {
|
|
278
|
+
const value = newValue.trim();
|
|
279
|
+
const name = newName.trim();
|
|
280
|
+
if (!name || !value) return;
|
|
281
|
+
setAddProbe(null);
|
|
282
|
+
add.mutate({ name, value }, { onSuccess: applyResult });
|
|
283
|
+
}, [add, newName, newValue, applyResult]);
|
|
284
|
+
|
|
285
|
+
const handleSave = useCallback(
|
|
286
|
+
(originalName: string) => {
|
|
287
|
+
const draft = edits[originalName];
|
|
288
|
+
if (!draft) return;
|
|
289
|
+
const rename = draft.name.trim() !== originalName ? draft.name.trim() : undefined;
|
|
290
|
+
const value = draft.value.trim() !== "" ? draft.value.trim() : undefined;
|
|
291
|
+
if (!rename && !value) {
|
|
292
|
+
// Nothing changed; just close the editor.
|
|
293
|
+
setEditing((prev) => { const next = new Set(prev); next.delete(originalName); return next; });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const targetValue = value ?? draft.value.trim();
|
|
297
|
+
setRowProbe((prev) => ({ ...prev, [originalName]: "pending" }));
|
|
298
|
+
probeValue(targetValue).then((outcome) => {
|
|
299
|
+
if (outcome === "ok") {
|
|
300
|
+
setRowProbe((prev) => { const next = { ...prev }; delete next[originalName]; return next; });
|
|
301
|
+
update.mutate(
|
|
302
|
+
{ name: originalName, rename, value },
|
|
303
|
+
{
|
|
304
|
+
onSuccess: applyResult,
|
|
305
|
+
onSettled: () => {
|
|
306
|
+
setEdits((prev) => { const next = { ...prev }; delete next[originalName]; return next; });
|
|
307
|
+
setEditing((prev) => { const next = new Set(prev); next.delete(originalName); return next; });
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (outcome === "unreachable") {
|
|
314
|
+
setRowProbe((prev) => ({ ...prev, [originalName]: { value: targetValue, error: null, saved: false } }));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
setRowProbe((prev) => ({ ...prev, [originalName]: { value: targetValue, error: "not a valid host form", saved: false } }));
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
[update, edits, applyResult, probeValue],
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const confirmEditAnyway = useCallback(
|
|
324
|
+
(originalName: string) => {
|
|
325
|
+
const draft = edits[originalName];
|
|
326
|
+
if (!draft) return;
|
|
327
|
+
const rename = draft.name.trim() !== originalName ? draft.name.trim() : undefined;
|
|
328
|
+
const value = draft.value.trim() !== "" ? draft.value.trim() : undefined;
|
|
329
|
+
setRowProbe((prev) => { const next = { ...prev }; delete next[originalName]; return next; });
|
|
330
|
+
update.mutate(
|
|
331
|
+
{ name: originalName, rename, value },
|
|
332
|
+
{
|
|
333
|
+
onSuccess: applyResult,
|
|
334
|
+
onSettled: () => {
|
|
335
|
+
setEdits((prev) => { const next = { ...prev }; delete next[originalName]; return next; });
|
|
336
|
+
setEditing((prev) => { const next = new Set(prev); next.delete(originalName); return next; });
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
[update, edits, applyResult],
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const [pendingRemove, setPendingRemove] = useState<string | null>(null);
|
|
345
|
+
|
|
346
|
+
const handleRemove = useCallback(
|
|
347
|
+
(name: string) => {
|
|
348
|
+
setPendingRemove(name);
|
|
349
|
+
},
|
|
350
|
+
[],
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const confirmRemove = useCallback(() => {
|
|
354
|
+
if (!pendingRemove) return;
|
|
355
|
+
remove.mutate({ name: pendingRemove }, { onSuccess: applyResult });
|
|
356
|
+
setPendingRemove(null);
|
|
357
|
+
}, [pendingRemove, remove, applyResult]);
|
|
358
|
+
|
|
359
|
+
const canAdd = newName.trim().length > 0 && newValue.trim().length > 0;
|
|
360
|
+
|
|
361
|
+
const handleRefresh = useCallback(() => {
|
|
362
|
+
setRefreshing(true);
|
|
363
|
+
void identitySync
|
|
364
|
+
.mutateAsync()
|
|
365
|
+
.then(() => snapshotRefresh.mutate())
|
|
366
|
+
.catch(() => snapshotRefresh.mutate());
|
|
367
|
+
}, [identitySync, snapshotRefresh]);
|
|
368
|
+
|
|
369
|
+
const setPrereqs = useCallback(
|
|
370
|
+
(collapsed: boolean) => {
|
|
371
|
+
setPrereqsCollapsed(collapsed);
|
|
372
|
+
prefsSet.mutate({ prereqsCollapsed: collapsed });
|
|
373
|
+
},
|
|
374
|
+
[prefsSet],
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const healthByName = useMemo(() => {
|
|
378
|
+
const map: Record<string, { reachable: boolean; error: string | null; agentCount: number | null }> = {};
|
|
379
|
+
for (const result of health.data?.results ?? []) map[result.name] = result;
|
|
380
|
+
return map;
|
|
381
|
+
}, [health.data]);
|
|
382
|
+
|
|
383
|
+
const serverIdByName = useMemo(() => {
|
|
384
|
+
const map = new Map<string, string>();
|
|
385
|
+
for (const daemon of read.data?.daemons ?? []) {
|
|
386
|
+
if (daemon.serverId) map.set(daemon.name, daemon.serverId);
|
|
387
|
+
}
|
|
388
|
+
return map;
|
|
389
|
+
}, [read.data]);
|
|
390
|
+
|
|
391
|
+
const aliasByServerId = useMemo(() => {
|
|
392
|
+
const map = new Map<string, string>();
|
|
393
|
+
for (const daemon of read.data?.daemons ?? []) {
|
|
394
|
+
if (daemon.serverId) map.set(daemon.serverId, daemon.name);
|
|
395
|
+
}
|
|
396
|
+
return map;
|
|
397
|
+
}, [read.data]);
|
|
398
|
+
|
|
399
|
+
const peerLabelForName = useCallback(
|
|
400
|
+
(name: string) => formatPeerDisplay(name, serverIdByName.get(name) ?? null),
|
|
401
|
+
[serverIdByName],
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const sendTargetLabel = useCallback(
|
|
405
|
+
(daemon: string) => {
|
|
406
|
+
if (daemon.startsWith("srv_")) return formatPeerDisplay(aliasByServerId.get(daemon) ?? null, daemon);
|
|
407
|
+
return formatPeerDisplay(daemon, serverIdByName.get(daemon) ?? null);
|
|
408
|
+
},
|
|
409
|
+
[aliasByServerId, serverIdByName],
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const addMismatch =
|
|
413
|
+
newName.trim().length > 0 && newValue.trim().length > 0 && !newValue.includes("#offer=")
|
|
414
|
+
? directHostMismatch(newName.trim(), newValue.trim())
|
|
415
|
+
: null;
|
|
416
|
+
|
|
417
|
+
const daemonCount = read.data?.daemons.length ?? 0;
|
|
418
|
+
|
|
419
|
+
const renderAgentPicker = (slot: 1 | 2) => {
|
|
420
|
+
const selected = slot === 1 ? introFirst : introSecond;
|
|
421
|
+
return (
|
|
422
|
+
<Modal
|
|
423
|
+
title={`Select agent ${slot}`}
|
|
424
|
+
open={expandedPicker === slot}
|
|
425
|
+
onOpenChange={(open) => { if (!open) setExpandedPicker(null); }}
|
|
426
|
+
>
|
|
427
|
+
<ModalContent>
|
|
428
|
+
{introspect.isPending ? <Notice tone="muted">Loading agents…</Notice> : null}
|
|
429
|
+
{introspect.error ? <Notice>{introspect.error.message}</Notice> : null}
|
|
430
|
+
{(introspect.data?.daemons ?? []).map((daemon) => (
|
|
431
|
+
<View key={daemon.name}>
|
|
432
|
+
<SectionHeader
|
|
433
|
+
title={daemon.reachable ? peerLabelForName(daemon.name) : `${peerLabelForName(daemon.name)} (unreachable)`}
|
|
434
|
+
badgeVariant={daemon.reachable ? "success" : "danger"}
|
|
435
|
+
/>
|
|
436
|
+
{daemon.projects.map((project) => (
|
|
437
|
+
<View key={`${daemon.name}-${project.project}`}>
|
|
438
|
+
<Notice tone="muted">{project.project}</Notice>
|
|
439
|
+
{project.workspaces.map((workspace) => (
|
|
440
|
+
<View key={`${daemon.name}-${project.project}-${workspace.name}`}>
|
|
441
|
+
<Notice tone="muted">⌂ {workspace.name}</Notice>
|
|
442
|
+
{workspace.agents.map((agent) => {
|
|
443
|
+
const active = selected?.agentId === agent.agentId;
|
|
444
|
+
return (
|
|
445
|
+
<Button
|
|
446
|
+
key={agent.agentId}
|
|
447
|
+
size="sm"
|
|
448
|
+
variant={active ? "primary" : "secondary"}
|
|
449
|
+
icon={active ? "Check" : "Bot"}
|
|
450
|
+
label={`${agent.name} (${agent.shortId}) · ${agent.status}`}
|
|
451
|
+
style={{ alignSelf: "stretch", marginTop: 2 }}
|
|
452
|
+
onPress={() => {
|
|
453
|
+
const setSelected = slot === 1 ? setIntroFirst : setIntroSecond;
|
|
454
|
+
setSelected({ daemon: daemon.name, agentId: agent.agentId, shortId: agent.shortId, name: agent.name });
|
|
455
|
+
setExpandedPicker(null);
|
|
456
|
+
}}
|
|
457
|
+
/>
|
|
458
|
+
);
|
|
459
|
+
})}
|
|
460
|
+
</View>
|
|
461
|
+
))}
|
|
462
|
+
</View>
|
|
463
|
+
))}
|
|
464
|
+
</View>
|
|
465
|
+
))}
|
|
466
|
+
<ViaXComms theme={theme} />
|
|
467
|
+
</ModalContent>
|
|
468
|
+
</Modal>
|
|
469
|
+
);
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const snapshot = dumpState as any;
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<View style={{ flex: 1, minHeight: 0, width: "100%", backgroundColor: colors.surface0 }}>
|
|
476
|
+
<ModalBody
|
|
477
|
+
headerMode="pinned"
|
|
478
|
+
maxContentWidth={X_COMMS_CONTENT_MAX_WIDTH}
|
|
479
|
+
header={
|
|
480
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
|
|
481
|
+
<Text style={{ color: colors.foreground, fontSize: 20, fontWeight: "700" }}>X-comms</Text>
|
|
482
|
+
<Button
|
|
483
|
+
label="Refresh"
|
|
484
|
+
size="sm"
|
|
485
|
+
variant="secondary"
|
|
486
|
+
icon="RefreshCw"
|
|
487
|
+
loading={refreshing}
|
|
488
|
+
disabled={refreshing}
|
|
489
|
+
onPress={handleRefresh}
|
|
490
|
+
/>
|
|
491
|
+
</View>
|
|
492
|
+
}
|
|
493
|
+
headerStyle={{ backgroundColor: colors.surface0, paddingHorizontal: 12, paddingTop: 12, paddingBottom: 6 }}
|
|
494
|
+
contentContainerStyle={{ gap: 12, paddingHorizontal: 12, paddingBottom: 24, paddingTop: 6 }}
|
|
495
|
+
refreshing={refreshing}
|
|
496
|
+
onRefresh={handleRefresh}
|
|
497
|
+
>
|
|
498
|
+
{refreshedAt ? <Notice tone="muted">Last refresh: {refreshedAt}</Notice> : null}
|
|
499
|
+
|
|
500
|
+
<Collapsible
|
|
501
|
+
title="Server"
|
|
502
|
+
icon="Server"
|
|
503
|
+
isExpanded={!prereqsCollapsed}
|
|
504
|
+
onToggle={(expanded) => setPrereqs(!expanded)}
|
|
505
|
+
>
|
|
506
|
+
<KeyValueGroup>
|
|
507
|
+
<KeyValue
|
|
508
|
+
label="Bundled server"
|
|
509
|
+
value={status.data ? status.data.installPath : "bundled server"}
|
|
510
|
+
mono
|
|
511
|
+
copyable={Boolean(status.data)}
|
|
512
|
+
truncate="path"
|
|
513
|
+
/>
|
|
514
|
+
</KeyValueGroup>
|
|
515
|
+
</Collapsible>
|
|
516
|
+
|
|
517
|
+
<SectionHeader title="Registered daemons" count={daemonCount} />
|
|
518
|
+
{read.data?.registryPath ? (
|
|
519
|
+
<KeyValue label="Registry path" value={read.data.registryPath} mono copyable truncate="path" />
|
|
520
|
+
) : null}
|
|
521
|
+
{read.isPending ? (
|
|
522
|
+
<Card>
|
|
523
|
+
<Card.Header title="Loading…" subtitle="Reading the daemon registry." />
|
|
524
|
+
</Card>
|
|
525
|
+
) : null}
|
|
526
|
+
{!read.isPending && !read.data?.exists ? (
|
|
527
|
+
<EmptyState title="No registry file yet." description="Add your first daemon below." />
|
|
528
|
+
) : null}
|
|
529
|
+
{read.data && !read.data.validJson ? (
|
|
530
|
+
<Card variant="elevated">
|
|
531
|
+
<Card.Header
|
|
532
|
+
title="Registry is not valid JSON"
|
|
533
|
+
subtitle={read.data.parseError ?? undefined}
|
|
534
|
+
badge={<Badge label="error" variant="danger" dot />}
|
|
535
|
+
/>
|
|
536
|
+
</Card>
|
|
537
|
+
) : null}
|
|
538
|
+
{health.isPending ? <Notice tone="muted">Checking health…</Notice> : null}
|
|
539
|
+
|
|
540
|
+
{read.data?.daemons.map((daemon) => {
|
|
541
|
+
const draft = edits[daemon.name] ?? { name: daemon.name, value: daemon.value };
|
|
542
|
+
const h = healthByName[daemon.name];
|
|
543
|
+
const probeState = rowProbe[daemon.name];
|
|
544
|
+
const probeDetail = probeState && probeState !== "pending" ? probeState : null;
|
|
545
|
+
if (editing.has(daemon.name)) {
|
|
546
|
+
return (
|
|
547
|
+
<Card key={daemon.name} variant="elevated">
|
|
548
|
+
<Card.Header title={`Edit ${peerLabelForName(daemon.name)}`} subtitle="Probe runs before saving." />
|
|
549
|
+
<FormRow label="Name" description="The daemon's real name; derived for relay links.">
|
|
550
|
+
<TextInput
|
|
551
|
+
mono
|
|
552
|
+
value={draft.name}
|
|
553
|
+
onChangeText={(text) => setEdits((prev) => ({ ...prev, [daemon.name]: { name: text, value: draft.value } }))}
|
|
554
|
+
autoCapitalize="none"
|
|
555
|
+
autoCorrect={false}
|
|
556
|
+
/>
|
|
557
|
+
</FormRow>
|
|
558
|
+
<FormRow label="Host value" description={HOST_FORM_HINT}>
|
|
559
|
+
<TextInput
|
|
560
|
+
mono
|
|
561
|
+
value={draft.value}
|
|
562
|
+
onChangeText={(text) => setEdits((prev) => ({ ...prev, [daemon.name]: { name: draft.name, value: text } }))}
|
|
563
|
+
autoCapitalize="none"
|
|
564
|
+
autoCorrect={false}
|
|
565
|
+
/>
|
|
566
|
+
</FormRow>
|
|
567
|
+
{probeState === "pending" ? <Notice tone="muted">Probing host…</Notice> : null}
|
|
568
|
+
{probeDetail ? (
|
|
569
|
+
<Notice>{probeDetail.error ?? `unreachable now: ${probeDetail.value}`}</Notice>
|
|
570
|
+
) : null}
|
|
571
|
+
{probeDetail && !probeDetail.saved ? (
|
|
572
|
+
<ActionBar align="flex-start">
|
|
573
|
+
<Button label="Save anyway" size="sm" variant="danger" onPress={() => confirmEditAnyway(daemon.name)} />
|
|
574
|
+
<Button
|
|
575
|
+
label="Dismiss"
|
|
576
|
+
size="sm"
|
|
577
|
+
variant="secondary"
|
|
578
|
+
onPress={() => setRowProbe((prev) => { const next = { ...prev }; delete next[daemon.name]; return next; })}
|
|
579
|
+
/>
|
|
580
|
+
</ActionBar>
|
|
581
|
+
) : null}
|
|
582
|
+
<ActionBar align="flex-start">
|
|
583
|
+
<Button label="Save" size="sm" variant="primary" onPress={() => handleSave(daemon.name)} />
|
|
584
|
+
<Button
|
|
585
|
+
label="Cancel"
|
|
586
|
+
size="sm"
|
|
587
|
+
variant="ghost"
|
|
588
|
+
onPress={() => {
|
|
589
|
+
setEdits((prev) => { const next = { ...prev }; delete next[daemon.name]; return next; });
|
|
590
|
+
setEditing((prev) => { const next = new Set(prev); next.delete(daemon.name); return next; });
|
|
591
|
+
}}
|
|
592
|
+
/>
|
|
593
|
+
</ActionBar>
|
|
594
|
+
</Card>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return (
|
|
598
|
+
<Card key={daemon.name} variant="elevated">
|
|
599
|
+
<Card.Header
|
|
600
|
+
title={peerLabelForName(daemon.name)}
|
|
601
|
+
subtitle={daemon.hostname && daemon.hostname !== daemon.name ? daemon.hostname : undefined}
|
|
602
|
+
badge={<HealthBadge health={h} />}
|
|
603
|
+
/>
|
|
604
|
+
<KeyValue label="Host value" value={daemon.value} mono copyable truncate="end" truncateMaxLength={36} />
|
|
605
|
+
{h && !h.reachable ? (
|
|
606
|
+
<>
|
|
607
|
+
<Notice>{h.error ?? "unreachable"}</Notice>
|
|
608
|
+
<ActionBar align="flex-start">
|
|
609
|
+
<Button
|
|
610
|
+
label={expandedError.has(daemon.name) ? "Hide details" : "Details"}
|
|
611
|
+
size="sm"
|
|
612
|
+
variant="ghost"
|
|
613
|
+
onPress={() => setExpandedError((prev) => {
|
|
614
|
+
const next = new Set(prev);
|
|
615
|
+
if (next.has(daemon.name)) next.delete(daemon.name); else next.add(daemon.name);
|
|
616
|
+
return next;
|
|
617
|
+
})}
|
|
618
|
+
/>
|
|
619
|
+
</ActionBar>
|
|
620
|
+
{expandedError.has(daemon.name) ? (
|
|
621
|
+
<KeyValue label="Error detail" value={h.error ?? "unreachable"} mono copyable />
|
|
622
|
+
) : null}
|
|
623
|
+
</>
|
|
624
|
+
) : null}
|
|
625
|
+
<ActionBar align="flex-start">
|
|
626
|
+
<Button
|
|
627
|
+
label="Edit"
|
|
628
|
+
size="sm"
|
|
629
|
+
variant="secondary"
|
|
630
|
+
onPress={() => {
|
|
631
|
+
setEdits((prev) => ({ ...prev, [daemon.name]: { name: daemon.name, value: daemon.value } }));
|
|
632
|
+
setEditing((prev) => new Set(prev).add(daemon.name));
|
|
633
|
+
}}
|
|
634
|
+
/>
|
|
635
|
+
<Button
|
|
636
|
+
label="Debug"
|
|
637
|
+
size="sm"
|
|
638
|
+
variant="ghost"
|
|
639
|
+
onPress={() => { setDumpOpen(true); setDumpDaemon(daemon.name); dump.mutate({ daemon: daemon.name }); }}
|
|
640
|
+
/>
|
|
641
|
+
<Button label="Remove" size="sm" variant="danger" onPress={() => handleRemove(daemon.name)} />
|
|
642
|
+
</ActionBar>
|
|
643
|
+
</Card>
|
|
644
|
+
);
|
|
645
|
+
})}
|
|
646
|
+
|
|
647
|
+
<SectionHeader title="Add daemon" />
|
|
648
|
+
<Card>
|
|
649
|
+
<FormRow label="Name" description="The daemon's real name; derived automatically for relay links.">
|
|
650
|
+
<TextInput value={newName} onChangeText={setNewName} autoCapitalize="none" autoCorrect={false} />
|
|
651
|
+
</FormRow>
|
|
652
|
+
<FormRow label="Host value" description={HOST_FORM_HINT}>
|
|
653
|
+
<TextInput
|
|
654
|
+
mono
|
|
655
|
+
value={newValue}
|
|
656
|
+
onChangeText={(text) => { setNewValue(text); deriveHost(text); }}
|
|
657
|
+
autoCapitalize="none"
|
|
658
|
+
autoCorrect={false}
|
|
659
|
+
placeholder={HOST_FORM_HINT}
|
|
660
|
+
errorText={addMismatch ?? undefined}
|
|
661
|
+
/>
|
|
662
|
+
</FormRow>
|
|
663
|
+
{add.error ? <Notice>{add.error.message}</Notice> : null}
|
|
664
|
+
{adding ? <Notice tone="muted">Probing host…</Notice> : null}
|
|
665
|
+
{addProbe ? (
|
|
666
|
+
<>
|
|
667
|
+
<Notice>{addProbe.error ?? `unreachable now: ${addProbe.value}`}</Notice>
|
|
668
|
+
<ActionBar align="flex-start">
|
|
669
|
+
<Button label="Add anyway" size="sm" variant="danger" onPress={confirmAddAnyway} />
|
|
670
|
+
<Button label="Dismiss" size="sm" variant="secondary" onPress={() => setAddProbe(null)} />
|
|
671
|
+
</ActionBar>
|
|
672
|
+
</>
|
|
673
|
+
) : null}
|
|
674
|
+
<ActionBar align="flex-start">
|
|
675
|
+
<Button
|
|
676
|
+
label="Add daemon"
|
|
677
|
+
variant="primary"
|
|
678
|
+
loading={add.isPending || adding}
|
|
679
|
+
disabled={!canAdd || add.isPending || adding}
|
|
680
|
+
onPress={handleAdd}
|
|
681
|
+
/>
|
|
682
|
+
</ActionBar>
|
|
683
|
+
</Card>
|
|
684
|
+
{update.error ? <Notice>{update.error.message}</Notice> : null}
|
|
685
|
+
{remove.error ? <Notice>{remove.error.message}</Notice> : null}
|
|
686
|
+
|
|
687
|
+
<SectionHeader title="Introduce agents" />
|
|
688
|
+
<Card>
|
|
689
|
+
<FormRow label="Agents" description="Pick two agents on reachable daemons; a message is sent to both.">
|
|
690
|
+
{([1, 2] as const).map((slot) => {
|
|
691
|
+
const selected = slot === 1 ? introFirst : introSecond;
|
|
692
|
+
return (
|
|
693
|
+
<Button
|
|
694
|
+
key={`intro-${slot}`}
|
|
695
|
+
size="sm"
|
|
696
|
+
variant="secondary"
|
|
697
|
+
icon="Bot"
|
|
698
|
+
style={{ marginTop: 4, alignSelf: "stretch" }}
|
|
699
|
+
label={
|
|
700
|
+
selected
|
|
701
|
+
? `${selected.name} (${selected.shortId}) on ${selected.daemon}`
|
|
702
|
+
: `Select agent ${slot}…`
|
|
703
|
+
}
|
|
704
|
+
onPress={() => setExpandedPicker(slot)}
|
|
705
|
+
/>
|
|
706
|
+
);
|
|
707
|
+
})}
|
|
708
|
+
</FormRow>
|
|
709
|
+
<FormRow label="Message">
|
|
710
|
+
<TextInput
|
|
711
|
+
mono
|
|
712
|
+
multiline
|
|
713
|
+
numberOfLines={4}
|
|
714
|
+
value={introMessage}
|
|
715
|
+
onChangeText={setIntroMessage}
|
|
716
|
+
autoCapitalize="none"
|
|
717
|
+
autoCorrect={false}
|
|
718
|
+
inputStyle={{ minHeight: 90, textAlignVertical: "top" }}
|
|
719
|
+
/>
|
|
720
|
+
</FormRow>
|
|
721
|
+
{introduce.error ? <Notice>{introduce.error.message}</Notice> : null}
|
|
722
|
+
{!introFirst || !introSecond ? (
|
|
723
|
+
<Notice tone="muted">Select both agents above to enable sending.</Notice>
|
|
724
|
+
) : null}
|
|
725
|
+
<ActionBar align="flex-start">
|
|
726
|
+
<Button
|
|
727
|
+
label="Send introductions"
|
|
728
|
+
variant="primary"
|
|
729
|
+
loading={introduce.isPending}
|
|
730
|
+
disabled={!introFirst || !introSecond || introMessage.trim().length === 0 || introduce.isPending}
|
|
731
|
+
onPress={() =>
|
|
732
|
+
introduce.mutate({
|
|
733
|
+
first: { daemon: introFirst!.daemon, agentId: introFirst!.agentId, shortId: introFirst!.shortId, name: introFirst!.name },
|
|
734
|
+
second: { daemon: introSecond!.daemon, agentId: introSecond!.agentId, shortId: introSecond!.shortId, name: introSecond!.name },
|
|
735
|
+
message: introMessage,
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
/>
|
|
739
|
+
</ActionBar>
|
|
740
|
+
{introduce.data ? (
|
|
741
|
+
<KeyValueGroup columns={1}>
|
|
742
|
+
{introduce.data.sends.map((send) => (
|
|
743
|
+
<KeyValue
|
|
744
|
+
key={send.agentId}
|
|
745
|
+
label={send.ok ? "Sent" : "Failed"}
|
|
746
|
+
value={`${sendTargetLabel(send.daemon)}/${send.agentId}${send.ok ? "" : `: ${send.error}`}`}
|
|
747
|
+
valueStyle={{ color: send.ok ? colors.statusSuccess : colors.statusDanger }}
|
|
748
|
+
mono
|
|
749
|
+
/>
|
|
750
|
+
))}
|
|
751
|
+
</KeyValueGroup>
|
|
752
|
+
) : null}
|
|
753
|
+
</Card>
|
|
754
|
+
|
|
755
|
+
<ViaXComms theme={theme} />
|
|
756
|
+
</ModalBody>
|
|
757
|
+
|
|
758
|
+
<Modal
|
|
759
|
+
title={`Debug: ${dumpDaemon ?? ""}`}
|
|
760
|
+
open={dumpOpen}
|
|
761
|
+
onOpenChange={(open) => { if (!open) { setDumpOpen(false); setDumpState(null); } }}
|
|
762
|
+
>
|
|
763
|
+
<ModalContent>
|
|
764
|
+
<Card variant="elevated">
|
|
765
|
+
{dump.isPending && !dumpState ? (
|
|
766
|
+
<Card.Header title="Loading…" subtitle="Fetching daemon snapshot." />
|
|
767
|
+
) : snapshot ? (
|
|
768
|
+
<>
|
|
769
|
+
<Card.Header
|
|
770
|
+
title={formatPeerDisplay(snapshot.name, snapshot.serverId)}
|
|
771
|
+
subtitle={`transport: ${String(snapshot.transport ?? "-")}`}
|
|
772
|
+
badge={<Badge label={snapshot.reached ? "reached" : "unreachable"} variant={snapshot.reached ? "success" : "danger"} dot />}
|
|
773
|
+
/>
|
|
774
|
+
<KeyValueGroup>
|
|
775
|
+
{snapshot.error ? <KeyValue label="Error" value={snapshot.error} copyable mono /> : null}
|
|
776
|
+
{snapshot.hostname ? <KeyValue label="Hostname" value={snapshot.hostname} copyable mono /> : null}
|
|
777
|
+
{snapshot.version ? <KeyValue label="Version" value={`${snapshot.version}${snapshot.desktopManaged ? " (desktop-managed)" : ""}`} mono /> : null}
|
|
778
|
+
{snapshot.listen ? <KeyValue label="Listen" value={snapshot.listen} copyable mono /> : null}
|
|
779
|
+
{snapshot.pid ? <KeyValue label="PID" value={`${snapshot.pid}${snapshot.nodePath ? ` · node: ${snapshot.nodePath}` : ""}`} mono /> : null}
|
|
780
|
+
{snapshot.startedAt ? <KeyValue label="Started" value={snapshot.startedAt} mono /> : null}
|
|
781
|
+
<KeyValue
|
|
782
|
+
label="Relay"
|
|
783
|
+
value={snapshot.relayEnabled && snapshot.relayEndpoints?.length
|
|
784
|
+
? `enabled ${snapshot.relayEndpoints.join(", ")}`
|
|
785
|
+
: "disabled"}
|
|
786
|
+
mono
|
|
787
|
+
/>
|
|
788
|
+
</KeyValueGroup>
|
|
789
|
+
{snapshot.features ? (
|
|
790
|
+
<>
|
|
791
|
+
<SectionHeader title="Features" count={Object.entries(snapshot.features).filter(([, v]) => v).length} />
|
|
792
|
+
<KeyValueGroup columns={1}>
|
|
793
|
+
{Object.entries(snapshot.features).filter(([, v]) => v).map(([k]) => (
|
|
794
|
+
<KeyValue key={k} label={k} value="enabled" mono />
|
|
795
|
+
))}
|
|
796
|
+
</KeyValueGroup>
|
|
797
|
+
</>
|
|
798
|
+
) : null}
|
|
799
|
+
{snapshot.capabilities ? (
|
|
800
|
+
<>
|
|
801
|
+
<SectionHeader title="Capabilities" />
|
|
802
|
+
<KeyValueGroup columns={1}>
|
|
803
|
+
{Object.entries(snapshot.capabilities).map(([k, v]) => (
|
|
804
|
+
<KeyValue key={k} label={k} value={String(v)} mono />
|
|
805
|
+
))}
|
|
806
|
+
</KeyValueGroup>
|
|
807
|
+
</>
|
|
808
|
+
) : null}
|
|
809
|
+
<SectionHeader title="Agents" count={snapshot.agents?.length ?? 0} />
|
|
810
|
+
<KeyValueGroup columns={1}>
|
|
811
|
+
{(snapshot.agents ?? []).map((a: any) => (
|
|
812
|
+
<KeyValue
|
|
813
|
+
key={a.agentId}
|
|
814
|
+
label={`${a.status} ${a.name}`}
|
|
815
|
+
value={`(${a.shortId}) ${a.provider}${a.model ? `/${a.model}` : ""}${a.archived ? " [archived]" : ""}${a.cwd ? ` · ${a.cwd}` : ""}`}
|
|
816
|
+
mono
|
|
817
|
+
/>
|
|
818
|
+
))}
|
|
819
|
+
</KeyValueGroup>
|
|
820
|
+
<SectionHeader title="Workspaces" count={snapshot.workspaces?.length ?? 0} />
|
|
821
|
+
<KeyValueGroup columns={1}>
|
|
822
|
+
{(snapshot.workspaces ?? []).map((w: any) => (
|
|
823
|
+
<KeyValue key={w.id ?? w.name} label={`${w.project}/${w.name}`} value={`${w.isolation}${w.cwd ? ` · ${w.cwd}` : ""}`} mono />
|
|
824
|
+
))}
|
|
825
|
+
</KeyValueGroup>
|
|
826
|
+
<SectionHeader title="Projects" count={snapshot.projects?.length ?? 0} />
|
|
827
|
+
<KeyValueGroup columns={1}>
|
|
828
|
+
{(snapshot.projects ?? []).map((p: any) => (
|
|
829
|
+
<KeyValue key={p.id ?? p.name} label={p.name} value={p.source ? String(p.source) : "-"} mono />
|
|
830
|
+
))}
|
|
831
|
+
</KeyValueGroup>
|
|
832
|
+
<SectionHeader title="Providers" count={snapshot.providerCount ?? snapshot.providers?.length ?? 0} />
|
|
833
|
+
<KeyValueGroup columns={1}>
|
|
834
|
+
{(snapshot.providers ?? []).map((p: any) => (
|
|
835
|
+
<KeyValue key={String(p.provider)} label={String(p.provider)} value={p.available ? "ok" : `x${p.error ? ` · ${String(p.error)}` : ""}`} mono />
|
|
836
|
+
))}
|
|
837
|
+
</KeyValueGroup>
|
|
838
|
+
<SectionHeader title="Terminals" count={snapshot.terminals?.length ?? 0} />
|
|
839
|
+
<KeyValueGroup columns={1}>
|
|
840
|
+
{(snapshot.terminals ?? []).map((t: any) => (
|
|
841
|
+
<KeyValue key={String(t.id ?? t.name)} label={String(t.name ?? t.id)} value={`${t.status ? String(t.status) : "-"}${t.cwd ? ` · ${String(t.cwd)}` : ""}`} mono />
|
|
842
|
+
))}
|
|
843
|
+
</KeyValueGroup>
|
|
844
|
+
<SectionHeader title="Schedules" count={snapshot.schedules?.length ?? 0} />
|
|
845
|
+
<KeyValueGroup columns={1}>
|
|
846
|
+
{(snapshot.schedules ?? []).map((sched: any) => (
|
|
847
|
+
<KeyValue key={String(sched.id ?? sched.name)} label={String(sched.name)} value={String(sched.state)} mono />
|
|
848
|
+
))}
|
|
849
|
+
</KeyValueGroup>
|
|
850
|
+
<SectionHeader title="Permissions" count={snapshot.permissions?.length ?? 0} />
|
|
851
|
+
<KeyValueGroup columns={1}>
|
|
852
|
+
{(snapshot.permissions ?? []).map((p: any) => (
|
|
853
|
+
<KeyValue key={String(p.id)} label={String(p.name)} value={`(${String(p.agentId).slice(0, 8)})`} mono />
|
|
854
|
+
))}
|
|
855
|
+
</KeyValueGroup>
|
|
856
|
+
</>
|
|
857
|
+
) : null}
|
|
858
|
+
</Card>
|
|
859
|
+
<ViaXComms theme={theme} />
|
|
860
|
+
</ModalContent>
|
|
861
|
+
</Modal>
|
|
862
|
+
|
|
863
|
+
{renderAgentPicker(1)}
|
|
864
|
+
{renderAgentPicker(2)}
|
|
865
|
+
|
|
866
|
+
<Modal
|
|
867
|
+
title="Remove daemon"
|
|
868
|
+
open={pendingRemove !== null}
|
|
869
|
+
onOpenChange={(open) => { if (!open) setPendingRemove(null); }}
|
|
870
|
+
>
|
|
871
|
+
<ModalContent>
|
|
872
|
+
<Text selectable style={{ color: colors.foreground, fontSize: 13 }}>
|
|
873
|
+
Remove '{pendingRemove}' from the registry?
|
|
874
|
+
</Text>
|
|
875
|
+
<ActionBar align="flex-start">
|
|
876
|
+
<Button label="Remove" variant="danger" onPress={confirmRemove} />
|
|
877
|
+
<Button label="Cancel" variant="secondary" onPress={() => setPendingRemove(null)} />
|
|
878
|
+
</ActionBar>
|
|
879
|
+
<ViaXComms theme={theme} />
|
|
880
|
+
</ModalContent>
|
|
881
|
+
</Modal>
|
|
882
|
+
|
|
883
|
+
</View>
|
|
884
|
+
);
|
|
885
|
+
}
|