@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,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.
|