mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
Filtered plugin observations shared a replaceable directory subscription with the app. Give each observation independent source ownership and route all application feeds through explicit demand, preserving combined listing and subscription.
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
import { useMutation } from "@tanstack/react-query";
|
|
import {
|
|
type PluginClientContext,
|
|
type PluginButtonRegistration,
|
|
type PluginWorkspacePanelProps,
|
|
useRpc,
|
|
useWorkspace,
|
|
} from "@getpaseo/plugin/client";
|
|
import { useCallback, useMemo } from "react";
|
|
import { Pressable, Text, View } from "react-native";
|
|
import { incrementRpc } from "../shared/increment";
|
|
|
|
export function contributeClient(client: PluginClientContext) {
|
|
const pills = new Map<string, PluginButtonRegistration>();
|
|
let stopped = false;
|
|
const lifetime = new AbortController();
|
|
const register = (agent: { id: string; workspaceId?: string | null }) => {
|
|
if (stopped || !agent.workspaceId) return;
|
|
pills.get(agent.id)?.remove();
|
|
const workspaceId = agent.workspaceId;
|
|
const remove = client.addComposerPill({
|
|
id: "open-counter",
|
|
workspaceId,
|
|
agentId: agent.id,
|
|
button: {
|
|
title: "Open plugin counter",
|
|
icon: "Blocks",
|
|
label: "Counter",
|
|
behavior: {
|
|
kind: "action",
|
|
onPress() {
|
|
client.openPanel("counter", { workspaceId });
|
|
},
|
|
},
|
|
},
|
|
});
|
|
pills.set(agent.id, remove);
|
|
};
|
|
const remove = (agentId: string) => {
|
|
pills.get(agentId)?.remove();
|
|
pills.delete(agentId);
|
|
};
|
|
void client.paseo.agents
|
|
.list({ subscribe: {}, signal: lifetime.signal })
|
|
.then(({ subscription }) => {
|
|
subscription.subscribe({
|
|
snapshot: ({ entries }) => {
|
|
for (const pill of pills.values()) pill.remove();
|
|
pills.clear();
|
|
for (const { agent } of entries) register(agent);
|
|
},
|
|
update: (message) => {
|
|
if (message.type !== "agent_update") return;
|
|
const update = message.payload;
|
|
if (update.kind === "remove") remove(update.agentId);
|
|
else register(update.agent);
|
|
},
|
|
});
|
|
return undefined;
|
|
})
|
|
.catch((error) => {
|
|
if (!stopped) console.error("Agent observation failed", error);
|
|
});
|
|
return () => {
|
|
stopped = true;
|
|
lifetime.abort();
|
|
for (const pill of pills.values()) pill.remove();
|
|
pills.clear();
|
|
};
|
|
}
|
|
|
|
export function ExamplePanel({ theme, layout, workspaceId }: PluginWorkspacePanelProps) {
|
|
const workspace = useWorkspace(workspaceId, ({ name }) => ({ name }));
|
|
const callIncrement = useRpc(incrementRpc);
|
|
const { data, error, isPending, mutate } = useMutation({ mutationFn: callIncrement });
|
|
const value = data?.value ?? 0;
|
|
const styles = useMemo(
|
|
() => ({
|
|
screen: {
|
|
flex: 1,
|
|
padding: layout.compact ? 16 : 24,
|
|
gap: 16,
|
|
backgroundColor: theme.colors.surface0,
|
|
},
|
|
title: { color: theme.colors.foreground, fontSize: layout.compact ? 20 : 24 },
|
|
detail: { color: theme.colors.foregroundMuted },
|
|
button: { padding: 14, borderRadius: 10, backgroundColor: theme.colors.accent },
|
|
buttonText: { color: theme.colors.accentForeground, textAlign: "center" as const },
|
|
error: { color: theme.colors.statusDanger },
|
|
}),
|
|
[theme, layout.compact],
|
|
);
|
|
const handleIncrement = useCallback(() => {
|
|
mutate({ value });
|
|
}, [mutate, value]);
|
|
|
|
return (
|
|
<View style={styles.screen}>
|
|
<Text style={styles.title}>Workspace plugin panel</Text>
|
|
<Text style={styles.detail}>{workspace?.name}</Text>
|
|
<Text style={styles.detail}>{data?.handledBy ?? "The RPC has not run yet."}</Text>
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
accessibilityLabel={`Increment plugin counter, currently ${value}`}
|
|
onPress={handleIncrement}
|
|
style={styles.button}
|
|
>
|
|
<Text style={styles.buttonText}>
|
|
{isPending ? "Calling daemon…" : `RPC counter: ${value}`}
|
|
</Text>
|
|
</Pressable>
|
|
{error ? <Text style={styles.error}>{error.message}</Text> : null}
|
|
</View>
|
|
);
|
|
}
|