@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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/client/attribution.ts +7 -0
  4. package/client/conversations.ts +229 -0
  5. package/client/main.tsx +885 -0
  6. package/client/peer-label.ts +98 -0
  7. package/client/peer-status.tsx +201 -0
  8. package/client/settings-prototype.tsx +395 -0
  9. package/client/tool-call.ts +115 -0
  10. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  11. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  12. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  13. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  14. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  15. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  16. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  17. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  18. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  19. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  20. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  21. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  22. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  23. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  24. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  25. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  26. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  27. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  28. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  29. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  30. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  31. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  32. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  33. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  34. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  35. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  36. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  37. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  38. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  39. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  40. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  41. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  42. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  43. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  44. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  45. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  46. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  47. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  48. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  49. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  50. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  51. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  52. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  53. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  54. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  55. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  56. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  57. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  58. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  59. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  60. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  61. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  62. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  63. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  64. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  65. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  66. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  67. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  68. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  69. package/client/via-x-comms.tsx +15 -0
  70. package/client/x-comms-conversation.tsx +449 -0
  71. package/client/x-comms-panel.tsx +6 -0
  72. package/client/x-comms-pill.tsx +193 -0
  73. package/client/x-comms-timeline.tsx +176 -0
  74. package/client/x-comms-tool-call.tsx +85 -0
  75. package/docs/mesh.md +115 -0
  76. package/mcp/LICENSE +202 -0
  77. package/mcp/README.md +287 -0
  78. package/mcp/mcp-config.example.json +8 -0
  79. package/mcp/package-lock.json +1186 -0
  80. package/mcp/package.json +43 -0
  81. package/mcp/paseo-cross-daemon-comms.example.json +5 -0
  82. package/mcp/paseo-x-comms.bundled.mjs +36964 -0
  83. package/mcp/paseo-x-comms.mjs +630 -0
  84. package/mcp/test/fixtures/extensions/block/10-block.mjs +4 -0
  85. package/mcp/test/fixtures/extensions/broken-hook/10-thrower.mjs +6 -0
  86. package/mcp/test/fixtures/extensions/broken-hook/20-good.mjs +4 -0
  87. package/mcp/test/fixtures/extensions/broken-load/10-thrower.mjs +4 -0
  88. package/mcp/test/fixtures/extensions/broken-load/20-good.mjs +4 -0
  89. package/mcp/test/fixtures/extensions/custom-tool/10-tool.mjs +12 -0
  90. package/mcp/test/fixtures/extensions/tool-block/10-block.mjs +8 -0
  91. package/mcp/test/fixtures/extensions/transform/10-transform.mjs +15 -0
  92. package/mcp/test/fixtures/fake-paseo.mjs +92 -0
  93. package/mcp/test/register-ts-hooks.mjs +6 -0
  94. package/mcp/test/resolve-ts-hooks.mjs +46 -0
  95. package/package.json +58 -0
  96. package/paseo-plugin.json +7 -0
  97. package/server/conversations-snapshot.ts +249 -0
  98. package/server/handlers.ts +1081 -0
  99. package/server/injection.ts +152 -0
  100. package/server/local-send.ts +153 -0
  101. package/server/mcp-client.ts +47 -0
  102. package/server/outbox.ts +233 -0
  103. package/server/peer-channel.ts +151 -0
  104. package/server/peer-status.ts +233 -0
  105. package/server/presence.ts +204 -0
  106. package/server/registry.ts +215 -0
  107. package/server/relay-status.ts +9 -0
  108. package/server/server-status.ts +138 -0
  109. package/server/settings.ts +81 -0
  110. package/server/snapshot.ts +176 -0
  111. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  112. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  113. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  114. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  115. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  116. package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
  117. package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
  118. package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
  119. package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
  120. package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
  121. package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
  122. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  123. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  124. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  125. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  126. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  127. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  128. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  129. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  130. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  131. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  132. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  133. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  134. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  135. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  136. package/shared/conversations-snapshot.ts +39 -0
  137. package/shared/envelope.ts +92 -0
  138. package/shared/outbox.ts +21 -0
  139. package/shared/registry.ts +366 -0
  140. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  141. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  142. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  143. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  144. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  145. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  146. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  147. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  148. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  149. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  150. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  151. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  152. package/shared/version.ts +2 -0
@@ -0,0 +1,193 @@
1
+ import { useRpc } from "@getpaseo/plugin/client";
2
+ import { Icon } from "@getpaseo/plugin/client/react-native";
3
+ import {
4
+ AboutSection,
5
+ ActionBar,
6
+ Card,
7
+ EmptyState,
8
+ FormRow,
9
+ ModalBody,
10
+ StatusDot,
11
+ Tabs,
12
+ Toggle,
13
+ registerComposerPill,
14
+ usePluginTheme,
15
+ type ComposerPillRegistrar,
16
+ type RenderModalProps,
17
+ type RenderPillProps,
18
+ } from "paseo-plugin-helper/client";
19
+ import { useEffect, useMemo, useState } from "react";
20
+ import { Text } from "react-native";
21
+ import { CrossDaemonConversation } from "./x-comms-conversation";
22
+ import { uiPrefsGetRpc, uiPrefsSetRpc } from "../shared/registry";
23
+
24
+ // Keep the modal a readable centered column instead of stretching edge-to-edge
25
+ // on large viewports. The cap lives in the helper (`ModalBody
26
+ // maxContentWidth`); this is the only place the pill picks the value.
27
+ const X_COMMS_CONTENT_MAX_WIDTH = 600;
28
+
29
+ // Raw Text is retained only for the composer pill label and the muted
30
+ // reload-needed caption. Every tab, layout, settings row, toggle, status and
31
+ // empty state goes through a paseo-plugin-helper primitive.
32
+ function CrossDaemonPill(props: RenderPillProps) {
33
+ const { theme } = props;
34
+ const style = useMemo(
35
+ () => ({ color: theme.colors.accent, flexShrink: 1, fontSize: 10 }),
36
+ [theme],
37
+ );
38
+ return (
39
+ <>
40
+ <Icon name="PhoneOutgoing" size={9} color={theme.colors.accent} />
41
+ <Text numberOfLines={1} style={style}>
42
+ X-comms
43
+ </Text>
44
+ </>
45
+ );
46
+ }
47
+
48
+ function XCommsSettings() {
49
+ const callGet = useRpc(uiPrefsGetRpc);
50
+ const callSet = useRpc(uiPrefsSetRpc);
51
+ const { colors } = usePluginTheme();
52
+ const [presence, setPresence] = useState<boolean | null>(null);
53
+ const [injection, setInjection] = useState<boolean | null>(null);
54
+ const [collapsed, setCollapsed] = useState(false);
55
+ const [saving, setSaving] = useState(false);
56
+ const [error, setError] = useState<string | null>(null);
57
+ useEffect(() => {
58
+ let live = true;
59
+ callGet({})
60
+ .then((prefs) => {
61
+ if (!live) return;
62
+ setPresence(prefs.presenceEnabled);
63
+ setInjection(prefs.injectionEnabled);
64
+ setCollapsed(prefs.prereqsCollapsed);
65
+ })
66
+ .catch((cause: unknown) => {
67
+ if (live) setError(cause instanceof Error ? cause.message : String(cause));
68
+ });
69
+ return () => {
70
+ live = false;
71
+ };
72
+ }, [callGet]);
73
+ const save = (next: { presenceEnabled: boolean; injectionEnabled: boolean }) => {
74
+ setSaving(true);
75
+ setError(null);
76
+ callSet({ prereqsCollapsed: collapsed, ...next })
77
+ .then((prefs) => {
78
+ setPresence(prefs.presenceEnabled);
79
+ setInjection(prefs.injectionEnabled);
80
+ })
81
+ .catch((cause: unknown) => setError(cause instanceof Error ? cause.message : String(cause)))
82
+ .finally(() => setSaving(false));
83
+ };
84
+ const loading = presence === null || injection === null;
85
+ return (
86
+ <>
87
+ {loading ? (
88
+ error ? (
89
+ <EmptyState icon="AlertTriangle" title="Couldn't load settings" description={error} />
90
+ ) : (
91
+ <EmptyState icon="Sliders" title="Loading settings…" />
92
+ )
93
+ ) : (
94
+ <Card>
95
+ <Card.Header title="Preferences" />
96
+ <FormRow
97
+ label="Presence"
98
+ description="Announce local agent births and retracts to paired daemons."
99
+ >
100
+ <Toggle
101
+ value={presence}
102
+ onValueChange={(next) => {
103
+ setPresence(next);
104
+ save({ presenceEnabled: next, injectionEnabled: injection });
105
+ }}
106
+ disabled={saving}
107
+ />
108
+ </FormRow>
109
+ <FormRow
110
+ label="MCP injection"
111
+ description="Add the x-comms server to every newborn agent."
112
+ >
113
+ <Toggle
114
+ value={injection}
115
+ onValueChange={(next) => {
116
+ setInjection(next);
117
+ save({ presenceEnabled: presence, injectionEnabled: next });
118
+ }}
119
+ disabled={saving}
120
+ />
121
+ </FormRow>
122
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11 }}>
123
+ Changes take effect after the plugin reloads.
124
+ </Text>
125
+ </Card>
126
+ )}
127
+ {!loading && (saving || error) ? (
128
+ <ActionBar align="flex-start">
129
+ <StatusDot variant={error ? "danger" : "warning"} pulse={!error} />
130
+ <Text
131
+ selectable
132
+ style={{
133
+ color: error ? colors.statusDanger : colors.foregroundMuted,
134
+ fontSize: 12,
135
+ flexShrink: 1,
136
+ }}
137
+ >
138
+ {error ?? "Saving…"}
139
+ </Text>
140
+ </ActionBar>
141
+ ) : null}
142
+ </>
143
+ );
144
+ }
145
+
146
+ const X_COMMS_TABS = [
147
+ { id: "chat", label: "Chat" },
148
+ { id: "settings", label: "Settings" },
149
+ { id: "about", label: "About" },
150
+ ];
151
+
152
+ function XCommsModalContent({ theme, agentId }: { theme: RenderModalProps["theme"]; agentId: string }) {
153
+ const [tab, setTab] = useState("chat");
154
+ return (
155
+ <ModalBody
156
+ header={<Tabs tabs={X_COMMS_TABS} activeTab={tab} onTabChange={setTab} />}
157
+ headerMode="pinned"
158
+ maxContentWidth={X_COMMS_CONTENT_MAX_WIDTH}
159
+ headerStyle={{ paddingHorizontal: 12, paddingTop: 12, paddingBottom: 6 }}
160
+ >
161
+ {tab === "about" ? (
162
+ <AboutSection
163
+ name="X-comms"
164
+ description="Cross-daemon agent conversation over Paseo Relay."
165
+ version="0.3.0"
166
+ repository="https://github.com/xpufx/paseo-x-comms"
167
+ license="MIT"
168
+ density="tiny"
169
+ />
170
+ ) : tab === "settings" ? (
171
+ <XCommsSettings />
172
+ ) : (
173
+ <CrossDaemonConversation theme={theme} agentId={agentId} />
174
+ )}
175
+ </ModalBody>
176
+ );
177
+ }
178
+
179
+ /**
180
+ * One composer pill per agent. Lifecycle (agent subscription, pill mount and
181
+ * unmount, modal open state, theme) is managed by registerComposerPill; this
182
+ * module only supplies the pill body and modal content.
183
+ */
184
+ export function contributeClient(client: ComposerPillRegistrar) {
185
+ return registerComposerPill(client, {
186
+ id: "x-comms",
187
+ title: "X-comms",
188
+ icon: "PhoneOutgoing",
189
+ modalTitle: "X-comms",
190
+ renderPill: (props) => <CrossDaemonPill {...props} />,
191
+ renderModal: (props) => <XCommsModalContent theme={props.theme} agentId={props.agentId} />,
192
+ });
193
+ }
@@ -0,0 +1,176 @@
1
+ import { z } from "zod";
2
+ import { type PluginTimelineItemProps, type PluginTimelineTransformerContribution, type PluginTimelineRendererContribution } from "@getpaseo/plugin/client";
3
+ import type { PluginTheme } from "@getpaseo/plugin";
4
+ import { Icon, copyText, useToast } from "@getpaseo/plugin/client/react-native";
5
+ import { useCallback, useMemo, useState } from "react";
6
+ import { Pressable, Text, View } from "react-native";
7
+ import { EnvelopeSchema, cardSignal, isOverflowing, parseEnvelope, type CrossDaemonEnvelope } from "../shared/envelope";
8
+ import {
9
+ OUTBOX_NOTICE_KIND,
10
+ OUTBOX_NOTICE_VERSION,
11
+ OutboxNoticeSchema,
12
+ type OutboxNotice,
13
+ } from "../shared/outbox";
14
+ import { formatPeerDisplay, usePeerAlias } from "./peer-label";
15
+ import { ViaXComms } from "./via-x-comms";
16
+
17
+ export { parseEnvelope, type CrossDaemonEnvelope };
18
+
19
+ const ItemSchema = z.object({
20
+ envelope: EnvelopeSchema,
21
+ body: z.string(),
22
+ });
23
+
24
+ function senderLabel(env: CrossDaemonEnvelope, alias: string | null): string {
25
+ const s = env.xComms.sender;
26
+ const name = s.agentName ?? s.agentId ?? "unknown agent";
27
+ return `${name} @ ${formatPeerDisplay(alias, s.daemonServerId)}`;
28
+ }
29
+
30
+ function MessageBody({ theme, body }: { theme: PluginTheme; body: string }) {
31
+ const toast = useToast();
32
+ const [expanded, setExpanded] = useState(false);
33
+ const [overflows, setOverflows] = useState(false);
34
+ const onTextLayout = useCallback(
35
+ (e: { nativeEvent: { lines: unknown[] } }) => {
36
+ const next = isOverflowing(e.nativeEvent.lines.length);
37
+ setOverflows((prev) => (prev === next ? prev : next));
38
+ },
39
+ [],
40
+ );
41
+ const copy = useCallback(() => {
42
+ copyText(body).then(
43
+ () => toast.show("Copied", { variant: "success" }),
44
+ () => toast.error("Copy failed"),
45
+ );
46
+ }, [body, toast]);
47
+ if (!expanded) {
48
+ return (
49
+ <View>
50
+ <Text
51
+ style={{ color: theme.colors.foreground, fontSize: 13 }}
52
+ numberOfLines={3}
53
+ ellipsizeMode="tail"
54
+ onTextLayout={onTextLayout}
55
+ >
56
+ {body}
57
+ </Text>
58
+ {overflows ? (
59
+ <Pressable accessibilityRole="button" onPress={() => setExpanded(true)} hitSlop={8}>
60
+ <Text style={{ color: theme.colors.accent, fontSize: 12, marginTop: 2 }}>Show more</Text>
61
+ </Pressable>
62
+ ) : null}
63
+ </View>
64
+ );
65
+ }
66
+ return (
67
+ <View
68
+ style={{
69
+ borderWidth: 1,
70
+ borderColor: theme.colors.border,
71
+ borderRadius: 6,
72
+ padding: 8,
73
+ marginTop: 2,
74
+ flexDirection: "row",
75
+ }}
76
+ >
77
+ <Text style={{ color: theme.colors.foreground, fontSize: 13, flexShrink: 1, flexGrow: 1 }} selectable>
78
+ {body}
79
+ </Text>
80
+ <View style={{ flexDirection: "column", gap: 8, marginLeft: 8 }}>
81
+ <Pressable accessibilityRole="button" accessibilityLabel="Copy message" onPress={copy} hitSlop={10}>
82
+ <Text style={{ color: theme.colors.accent, fontSize: 14 }}>⧉</Text>
83
+ </Pressable>
84
+ <Pressable accessibilityRole="button" onPress={() => setExpanded(false)} hitSlop={8}>
85
+ <Text style={{ color: theme.colors.accent, fontSize: 12 }}>Less</Text>
86
+ </Pressable>
87
+ </View>
88
+ </View>
89
+ );
90
+ }
91
+
92
+ function CrossDaemonMessage({ theme, agentId, item }: PluginTimelineItemProps<z.infer<typeof ItemSchema>>) {
93
+ const alias = usePeerAlias(item.data.envelope.xComms.sender.daemonServerId);
94
+ const label = useMemo(
95
+ () => senderLabel(item.data.envelope, alias),
96
+ [item.data.envelope, alias],
97
+ );
98
+ const { direction, userSent } = cardSignal(item.data.envelope, agentId);
99
+ const incoming = direction === "incoming";
100
+ const signalColor = userSent ? theme.colors.statusDanger : theme.colors.foregroundMuted;
101
+ return (
102
+ <View style={{ paddingVertical: 4 }}>
103
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6, marginBottom: 2 }}>
104
+ <Icon
105
+ name={incoming ? "PhoneIncoming" : "PhoneOutgoing"}
106
+ size={14}
107
+ color={signalColor}
108
+ />
109
+ <Text style={{ color: signalColor, fontSize: 12, fontWeight: "600" as const }}>
110
+ x-comms · {incoming ? "Incoming" : "Outgoing"} · {label}
111
+ </Text>
112
+ </View>
113
+ {item.data.body.length > 0 ? (
114
+ <MessageBody theme={theme} body={item.data.body} />
115
+ ) : null}
116
+ <ViaXComms theme={theme} />
117
+ </View>
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Timeline transformer: paseo calls this for every user_message. If the text
123
+ * carries our meta envelope, we emit a plugin-typed item our renderer draws
124
+ * distinctly. The envelope is the only discriminator.
125
+ */
126
+ export const crossDaemonTransformer: PluginTimelineTransformerContribution<"user_message"> = {
127
+ id: "x-comms-message",
128
+ query: { itemType: "user_message" },
129
+ transform({ item }) {
130
+ const parsed = parseEnvelope(item.text);
131
+ if (!parsed) return undefined;
132
+ return {
133
+ items: [
134
+ {
135
+ type: "plugin",
136
+ kind: "x-comms-message",
137
+ version: 1,
138
+ data: { envelope: parsed.envelope, body: parsed.body },
139
+ },
140
+ ],
141
+ };
142
+ },
143
+ };
144
+
145
+ export const crossDaemonRenderer: PluginTimelineRendererContribution<typeof ItemSchema> = {
146
+ kind: "x-comms-message",
147
+ version: 1,
148
+ schema: ItemSchema,
149
+ Component: CrossDaemonMessage,
150
+ };
151
+
152
+ /**
153
+ * Expiry notice for a message the outbox gave up on. Local to the sender's
154
+ * timeline; never part of the wire envelope.
155
+ */
156
+ function OutboxNoticeCard({ theme, item }: PluginTimelineItemProps<OutboxNotice>) {
157
+ return (
158
+ <View style={{ paddingVertical: 4 }}>
159
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6, marginBottom: 2 }}>
160
+ <Icon name="AlertTriangle" size={14} color={theme.colors.statusWarning} />
161
+ <Text style={{ color: theme.colors.statusWarning, fontSize: 12, fontWeight: "600" as const }}>
162
+ x-comms · Delivery failed
163
+ </Text>
164
+ </View>
165
+ <Text style={{ color: theme.colors.foreground, fontSize: 13 }}>{item.data.reason}</Text>
166
+ <ViaXComms theme={theme} />
167
+ </View>
168
+ );
169
+ }
170
+
171
+ export const outboxNoticeRenderer: PluginTimelineRendererContribution<typeof OutboxNoticeSchema> = {
172
+ kind: OUTBOX_NOTICE_KIND,
173
+ version: OUTBOX_NOTICE_VERSION,
174
+ schema: OutboxNoticeSchema,
175
+ Component: OutboxNoticeCard,
176
+ };
@@ -0,0 +1,85 @@
1
+ import { type PluginTimelineItemProps, type PluginTimelineTransformerContribution, type PluginTimelineRendererContribution } from "@getpaseo/plugin/client";
2
+ import { Icon } from "@getpaseo/plugin/client/react-native";
3
+ import { Badge, Card, CodeBlock, StatusDot } from "paseo-plugin-helper/client";
4
+ import { Text, View } from "react-native";
5
+ import { ViaXComms } from "./via-x-comms";
6
+ import { usePeerDisplay } from "./peer-label";
7
+ import { ToolCallCardSchema, summarizeToolCall, type ToolCallCardData } from "./tool-call";
8
+
9
+ function statusVariant(status: string): "success" | "warning" | "danger" | "info" {
10
+ if (status === "completed") return "success";
11
+ if (status === "running") return "info";
12
+ if (status === "failed" || status === "canceled") return "danger";
13
+ return "warning";
14
+ }
15
+
16
+ function ToolCallCard({ theme, item }: PluginTimelineItemProps<ToolCallCardData>) {
17
+ const data = item.data;
18
+ const peer = usePeerDisplay(data.targetServerId, data.targetAlias);
19
+ const title = data.agentId ? `${data.tool} · ${data.agentId.slice(0, 8)}` : data.tool;
20
+ return (
21
+ <Card>
22
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6, marginBottom: 4 }}>
23
+ <Icon name="PhoneOutgoing" size={13} color={theme.colors.accent} />
24
+ <Text style={{ color: theme.colors.foreground, fontSize: 12, fontWeight: "600" as const, flexShrink: 1 }}>
25
+ {title}
26
+ </Text>
27
+ <StatusDot variant={statusVariant(data.status)} size="sm" />
28
+ <Badge label={data.status} variant={statusVariant(data.status)} />
29
+ </View>
30
+ {(data.targetAlias || data.targetServerId) ? (
31
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12, marginBottom: 4 }} selectable>
32
+ {peer}
33
+ </Text>
34
+ ) : null}
35
+ {data.output ? (
36
+ <CodeBlock code={data.output} language="json" copyable maxHeight={220} />
37
+ ) : null}
38
+ <ViaXComms theme={theme} />
39
+ </Card>
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Timeline transformer: matches tool_call items for x_comms_* tools and
45
+ * replaces the raw JSON row with a card. Matching is a pure name-prefix
46
+ * check with no visibility toggles: tool calls never render as JSON walls.
47
+ */
48
+ export const crossDaemonToolCallTransformer: PluginTimelineTransformerContribution<"tool_call"> = {
49
+ id: "x-comms-tool-call",
50
+ query: { itemType: "tool_call" },
51
+ transform({ item }) {
52
+ const summary = summarizeToolCall({
53
+ name: item.name,
54
+ status: item.status,
55
+ error: item.status === "failed" ? item.error : undefined,
56
+ detail: item.detail,
57
+ });
58
+ if (!summary) return undefined;
59
+ return {
60
+ items: [
61
+ {
62
+ type: "plugin",
63
+ kind: "x-comms-tool-call",
64
+ version: 1,
65
+ data: {
66
+ tool: summary.tool,
67
+ agentId: summary.agentId,
68
+ targetAlias: summary.targetAlias,
69
+ targetServerId: summary.targetServerId,
70
+ status: summary.status,
71
+ failed: summary.failed,
72
+ output: summary.output,
73
+ },
74
+ },
75
+ ],
76
+ };
77
+ },
78
+ };
79
+
80
+ export const crossDaemonToolCallRenderer: PluginTimelineRendererContribution<typeof ToolCallCardSchema> = {
81
+ kind: "x-comms-tool-call",
82
+ version: 1,
83
+ schema: ToolCallCardSchema,
84
+ Component: ToolCallCard,
85
+ };
package/docs/mesh.md ADDED
@@ -0,0 +1,115 @@
1
+ # Daemon Mesh: Presence, Injection, and Visibility
2
+
3
+ How Paseo daemons find each other, how agents get tools without being asked,
4
+ and how the fleet stays observable. Three mechanisms, each independent,
5
+ composable in that order.
6
+
7
+ This mirrors `docs/mesh.md` in `paseo-plugin-helper` (0.8 line): both repos
8
+ tell the same story and share one vocabulary (announce/retract, tombstones,
9
+ intended vs actual). The helper doc is the vision; this file records what
10
+ x-comms implements and what is still ahead.
11
+
12
+ ## Thesis
13
+
14
+ Presence RPCs plus link-identity is the foundation; reliable agent messaging
15
+ rides on top of it. Conversation (`x_comms_send` and friends) is the top
16
+ layer. Nothing below it ever speaks in chat messages.
17
+
18
+ ## Layer 0: Link identity
19
+
20
+ Every peer link is authenticated. Sender identity (serverId) comes from the
21
+ link itself, never from message payloads.
22
+
23
+ What x-comms does today:
24
+ - Outbound: after `DaemonClient.connect()`, the handshake identity from
25
+ `getLastServerInfoMessage()` is checked against the expected serverId
26
+ embedded in the registry relay offer. A mismatch aborts before any
27
+ payload is sent (`server/peer-channel.ts`).
28
+ - Inbound: RPC handlers have no link context, so the receiver accepts only
29
+ entries whose serverId matches an explicitly paired peer (seed links
30
+ stay explicit). Anything else is dropped and counted as rejected.
31
+ - Own identity is read locally (`~/.paseo/server-id`, falling back to a
32
+ local `paseo daemon status` probe), never from agent config or payloads.
33
+
34
+ Known gap: true receiver-side link verification needs daemon support; the
35
+ known-peer check is the approximation until then.
36
+
37
+ ## Layer 1: Presence (control plane, invisible)
38
+
39
+ - `presence.announce` (birth batch) and `presence.retract`, keyed by
40
+ `(serverId, agentId)`, carried as plugin RPCs over the authenticated
41
+ peer channel (`server/peer-channel.ts` via `DaemonClient.invokePluginRpc`).
42
+ Never through conversation sends.
43
+ - Local hooks only: `on("agent.created")` buffers and announces,
44
+ `on("agent.archived")` retracts immediately (`index.server.ts`).
45
+ - Store (`server/presence.ts`, `PluginStorage` pattern, `presence.json`):
46
+ live entries, sticky tombstones, bounded seen-id LRU (1000), queued
47
+ retracts per peer with retry piggybacked on the next outbound pass.
48
+ - Anti-loop rules, in order of importance:
49
+ 1. Seen-id LRU drops duplicates.
50
+ 2. Never forward gossip: only locally-originated events leave the daemon
51
+ (origin tracking plus the transport edge only sends local entries).
52
+ 3. Retract-before-reannounce with sticky tombstones (kept 2x the live
53
+ TTL) so delayed births cannot resurrect the dead.
54
+ 4. TTL (7 days live) purely as a safety net; firing logs an alert.
55
+ - Explicitly deferred: vector clocks, anti-entropy sync, Merkle anything.
56
+ - Birth payload is minimal: `(serverId, agentId, name, provider,
57
+ timestamp)`. No cwd, workspace, or project.
58
+ - Debug surface: `presence.list` RPC (live, tombstones, pending count).
59
+ No UI.
60
+ - Scope flag: daemon-wide `presenceEnabled` in plugin settings (on by
61
+ default). It gates local capture and fan-out; inbound processing
62
+ continues so re-enabling finds warm state.
63
+ - Known limitation: births missed while a peer is offline are not
64
+ backfilled in this slice. Retracts are queued and retried.
65
+
66
+ ## Layer 2: Injection (tools by default)
67
+
68
+ Implemented on this branch (`server/injection.ts`, wired in
69
+ `index.server.ts`):
70
+ - `registerMcpInjection` from the helper wraps `server.before`
71
+ ("agent.create") with merge-preserving, non-mutating semantics.
72
+ - Key scheme `x-comms.<serverId>` (fallback plain `x-comms` with a logged
73
+ warning if the local server id is unreadable).
74
+ - Stdio config uses `process.execPath` plus the runtime-resolved bundled
75
+ server path, so it works from git checkouts on foreign hosts.
76
+ - No provider filter (all agents). Daemon-wide `injectionEnabled` toggle
77
+ in plugin settings, default on; changes apply on plugin reload.
78
+ - Per-agent opt-out deferred. MCP server, envelope, and presence untouched.
79
+
80
+ ## Layer 3: Visibility (intended vs actual)
81
+
82
+ Not implemented. Planned: injection snapshot plus diff against live
83
+ session configs. Separate slice.
84
+
85
+ ## Layer 4: Conversation reliability (sender-side outbox)
86
+
87
+ Implemented (`server/outbox.ts` + the plugin server, #12):
88
+ - Failed `conversation.send` calls are held in an `outbox.json` store and
89
+ retried over the same `x_comms_send` path: exponential backoff
90
+ (5s doubling to 60s), a 15s periodic sweep, and an immediate retry when a
91
+ peer's reachability flips false -> true (health/snapshot).
92
+ - Held messages expire after a configurable window (default 10 min); on
93
+ expiry the sender gets an `x-comms-outbox-notice` timeline item with the
94
+ reason, then the entry is dropped.
95
+ - Scope: this covers plugin-server sends (panel/composer). Agent-initiated
96
+ `x_comms_send` calls run in an ephemeral per-session MCP process and are not
97
+ outboxed.
98
+ - Delivery path (`conversation.send`): a target that resolves to THIS daemon's
99
+ serverId sends natively through the host `PaseoApi` (`server/local-send.ts`);
100
+ every remote target goes through the bundled MCP server, which stamps the
101
+ envelope and shells out to `paseo send --host`. `PaseoApi.send()` is bound to
102
+ one daemon connection and carries no host/`serverId` (`PaseoAgentSendOptions`
103
+ has only `messageId`/`images`/`attachments`), so there is no host-targeted SDK
104
+ call to replace the remote shell-out with. Both producers stamp the identical
105
+ version-4 envelope, so the wire contract is unchanged.
106
+ - Not implemented: UUID-keyed idempotency. The conversation envelope has no
107
+ message-id slot and receivers keep no seen-id set for messages, so a retry
108
+ after an ambiguous failure can re-deliver. Adding it needs a wire-format
109
+ change (separate decision).
110
+
111
+ ## Trust corollary
112
+
113
+ Any installed plugin can run these hooks, so peering with a daemon means
114
+ trusting its plugin list. Daemon-wide kill switches (disable the plugin,
115
+ or the `presenceEnabled` flag) must always work instantly.