mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Merge Lane C: frontend pair-runtime routing (e29aa1673)
Pair-scoped agent runtime UI, observer lifecycle-frame routing via typed Tauri wrapper, expected-path rejection handling. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
Tyler Longwell
commit
e7c9408035
@@ -1,7 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Outlet, useLocation } from "@tanstack/react-router";
|
||||
|
||||
import { deriveShellRoute } from "@/app/AppShell.helpers";
|
||||
import { AppShellProvider } from "@/app/AppShellContext";
|
||||
import * as BuzzTheme from "@/app/BuzzThemeSurfaces";
|
||||
@@ -37,6 +36,7 @@ import { setDesktopAppBadge } from "@/features/notifications/lib/desktop";
|
||||
import { PreventSleepProvider } from "@/features/agents/usePreventSleep";
|
||||
import { requestOpenCreateAgent } from "@/features/agents/openCreateAgentEvent";
|
||||
import { useAgentsDataRefresh } from "@/features/agents/lib/useAgentsDataRefresh";
|
||||
import { useManagedAgentRuntimeReconciliation } from "@/features/agents/useManagedAgentRuntimeReconciliation";
|
||||
import { useAutoRestartPolicy } from "@/features/agents/lib/useAutoRestartPolicy";
|
||||
import { usePersonaSync } from "@/features/agents/lib/usePersonaSync";
|
||||
import { useAgentObserverIngestion } from "@/features/agents/useAgentObserverIngestion";
|
||||
@@ -89,7 +89,6 @@ import { useMessageDeepLinks } from "@/shared/useMessageDeepLinks";
|
||||
import { SidebarInset, SidebarProvider } from "@/shared/ui/sidebar";
|
||||
import { RelayConnectionOverlay } from "@/app/RelayConnectionOverlay";
|
||||
import { useSidebarRelayConnectionCard } from "@/features/sidebar/ui/useSidebarRelayConnectionCard";
|
||||
|
||||
const LazySettingsScreen = React.lazy(async () => {
|
||||
const module = await import("@/features/settings/ui/SettingsScreen");
|
||||
return { default: module.SettingsScreen };
|
||||
@@ -115,6 +114,7 @@ export function AppShell() {
|
||||
const mainInsetRef = React.useRef<HTMLElement>(null);
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
useManagedAgentRuntimeReconciliation(communitiesHook.communities); // sync storage snapshot
|
||||
const {
|
||||
goAgents,
|
||||
goChannel,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
relayAgentsQueryKey,
|
||||
teamsQueryKey,
|
||||
} from "@/features/agents/hooks";
|
||||
import { managedAgentRuntimesQueryKey } from "@/features/agents/managedAgentRuntimeHooks";
|
||||
|
||||
// Trailing-coalesce window: a backfill burst (up to 500 inbound events fed
|
||||
// one-by-one through reconcile) fires one `agents-data-changed` per event.
|
||||
@@ -27,6 +28,12 @@ export function useAgentsDataRefresh(): void {
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const unlistenRuntime = listen("managed-agent-runtime-status", () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: managedAgentRuntimesQueryKey,
|
||||
});
|
||||
});
|
||||
|
||||
const unlisten = listen("agents-data-changed", () => {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
@@ -40,6 +47,7 @@ export function useAgentsDataRefresh(): void {
|
||||
return () => {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
void unlisten.then((fn) => fn());
|
||||
void unlistenRuntime.then((fn) => fn());
|
||||
};
|
||||
}, [queryClient]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
listManagedAgentRuntimes,
|
||||
restartManagedAgentRuntime,
|
||||
startManagedAgentRuntime,
|
||||
stopManagedAgentRuntime,
|
||||
} from "@/shared/api/tauriManagedAgents";
|
||||
import type { ManagedAgentRuntimeStatus } from "@/shared/api/types";
|
||||
|
||||
export const managedAgentRuntimesQueryKey = ["managed-agent-runtimes"] as const;
|
||||
|
||||
export function useManagedAgentRuntimesQuery(options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
enabled: options?.enabled ?? true,
|
||||
queryKey: managedAgentRuntimesQueryKey,
|
||||
queryFn: listManagedAgentRuntimes,
|
||||
});
|
||||
}
|
||||
|
||||
export function useManagedAgentRuntimeAction() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
action,
|
||||
pubkey,
|
||||
relayUrl,
|
||||
}: {
|
||||
action: "start" | "stop" | "restart";
|
||||
pubkey: string;
|
||||
relayUrl: string;
|
||||
}) => {
|
||||
if (action === "stop") return stopManagedAgentRuntime(pubkey, relayUrl);
|
||||
if (action === "restart") {
|
||||
return restartManagedAgentRuntime(pubkey, relayUrl);
|
||||
}
|
||||
return startManagedAgentRuntime(pubkey, relayUrl);
|
||||
},
|
||||
onSuccess: (runtime) => {
|
||||
queryClient.setQueryData<ManagedAgentRuntimeStatus[]>(
|
||||
managedAgentRuntimesQueryKey,
|
||||
(current = []) => {
|
||||
const index = current.findIndex(
|
||||
(candidate) =>
|
||||
candidate.pubkey === runtime.pubkey &&
|
||||
candidate.relayUrl === runtime.relayUrl,
|
||||
);
|
||||
if (index === -1) return [...current, runtime];
|
||||
return current.map((candidate, candidateIndex) =>
|
||||
candidateIndex === index ? runtime : candidate,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
agentCommunityAvailability,
|
||||
agentCommunityStatusDetail,
|
||||
findManagedAgentRuntime,
|
||||
managedAgentRuntimeKey,
|
||||
} from "./managedAgentRuntimeStatus.ts";
|
||||
|
||||
const runtime = (overrides = {}) => ({
|
||||
pubkey: "aa",
|
||||
relayUrl: "wss://relay.example",
|
||||
localSetup: true,
|
||||
lifecycle: "ready",
|
||||
pid: 1,
|
||||
error: null,
|
||||
logPath: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("projects every backend lifecycle to the four product labels", () => {
|
||||
assert.equal(agentCommunityAvailability(runtime()), "Here");
|
||||
for (const lifecycle of ["starting", "listening", "waking"]) {
|
||||
assert.equal(agentCommunityAvailability(runtime({ lifecycle })), "Waking");
|
||||
}
|
||||
for (const lifecycle of ["failed", "stopped"]) {
|
||||
assert.equal(
|
||||
agentCommunityAvailability(runtime({ lifecycle })),
|
||||
"Unavailable",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("backend-authoritative local setup takes precedence", () => {
|
||||
assert.equal(
|
||||
agentCommunityAvailability(
|
||||
runtime({ localSetup: false, lifecycle: "ready" }),
|
||||
),
|
||||
"Needs setup on this device",
|
||||
);
|
||||
});
|
||||
|
||||
test("unavailable detail distinguishes stopped and failed", () => {
|
||||
assert.equal(
|
||||
agentCommunityStatusDetail(runtime({ lifecycle: "stopped" })),
|
||||
"Stopped by you",
|
||||
);
|
||||
assert.equal(
|
||||
agentCommunityStatusDetail(
|
||||
runtime({ lifecycle: "failed", error: "Relay timed out" }),
|
||||
),
|
||||
"Relay timed out",
|
||||
);
|
||||
});
|
||||
|
||||
test("pair key cannot collide at component boundaries", () => {
|
||||
assert.notEqual(
|
||||
managedAgentRuntimeKey(runtime({ pubkey: "ab", relayUrl: "c" })),
|
||||
managedAgentRuntimeKey(runtime({ pubkey: "a", relayUrl: "bc" })),
|
||||
);
|
||||
});
|
||||
|
||||
test("selects one relay without collapsing same-pubkey pairs", () => {
|
||||
const runtimes = [
|
||||
runtime({ relayUrl: "wss://a.example", lifecycle: "ready" }),
|
||||
runtime({ relayUrl: "wss://b.example", lifecycle: "failed" }),
|
||||
];
|
||||
assert.equal(
|
||||
findManagedAgentRuntime(runtimes, "AA", "wss://b.example")?.lifecycle,
|
||||
"failed",
|
||||
);
|
||||
assert.equal(
|
||||
findManagedAgentRuntime(runtimes, "aa", "wss://c.example"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ManagedAgentRuntimeStatus } from "@/shared/api/types";
|
||||
|
||||
export type AgentCommunityAvailability =
|
||||
| "Here"
|
||||
| "Waking"
|
||||
| "Needs setup on this device"
|
||||
| "Unavailable";
|
||||
|
||||
export function agentCommunityAvailability(
|
||||
runtime: ManagedAgentRuntimeStatus,
|
||||
): AgentCommunityAvailability {
|
||||
if (!runtime.localSetup) return "Needs setup on this device";
|
||||
|
||||
switch (runtime.lifecycle) {
|
||||
case "starting":
|
||||
case "listening":
|
||||
case "waking":
|
||||
return "Waking";
|
||||
case "ready":
|
||||
return "Here";
|
||||
case "failed":
|
||||
case "stopped":
|
||||
return "Unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
export function agentCommunityStatusDetail(
|
||||
runtime: ManagedAgentRuntimeStatus,
|
||||
): string | null {
|
||||
if (!runtime.localSetup)
|
||||
return "Set up this agent on this device to start it.";
|
||||
if (runtime.lifecycle === "stopped") return "Stopped by you";
|
||||
if (runtime.lifecycle === "failed")
|
||||
return runtime.error ?? "Could not connect";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function managedAgentRuntimeKey(
|
||||
runtime: Pick<ManagedAgentRuntimeStatus, "pubkey" | "relayUrl">,
|
||||
): string {
|
||||
return JSON.stringify([runtime.pubkey, runtime.relayUrl]);
|
||||
}
|
||||
|
||||
export function findManagedAgentRuntime(
|
||||
runtimes: readonly ManagedAgentRuntimeStatus[],
|
||||
pubkey: string,
|
||||
relayUrl: string,
|
||||
): ManagedAgentRuntimeStatus | undefined {
|
||||
const normalizedPubkey = pubkey.toLowerCase();
|
||||
return runtimes.find(
|
||||
(runtime) =>
|
||||
runtime.pubkey.toLowerCase() === normalizedPubkey &&
|
||||
(runtime.relayUrl === relayUrl || runtime.requestedRelayUrl === relayUrl),
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { subscribeToAgentObserverFrames } from "@/shared/api/observerRelay";
|
||||
import type { RelayEvent, ManagedAgent } from "@/shared/api/types";
|
||||
import type { ControlResultFrame } from "@/shared/api/types";
|
||||
import { putAgentSessionConfig } from "@/shared/api/tauri";
|
||||
import { putManagedAgentRuntimeLifecycle } from "@/shared/api/tauriManagedAgents";
|
||||
import { getIdentity } from "@/shared/api/tauriIdentity";
|
||||
import { decryptObserverEvent } from "@/shared/api/tauriObserver";
|
||||
import {
|
||||
@@ -402,6 +403,12 @@ async function handleRelayObserverEvent(
|
||||
onSessionConfigCaptured?.(agentPubkey);
|
||||
} else if (parsed.kind === "control_result") {
|
||||
dispatchControlResult(agentPubkey, parsed.payload);
|
||||
} else if (parsed.kind === "managed_agent_runtime_lifecycle") {
|
||||
void putManagedAgentRuntimeLifecycle(agentPubkey, parsed.payload).catch(
|
||||
(error) => {
|
||||
console.debug("Late/untracked lifecycle frame dropped:", error);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (activeGeneration !== generation) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import * as React from "react";
|
||||
|
||||
import { managedAgentRuntimesQueryKey } from "@/features/agents/managedAgentRuntimeHooks";
|
||||
import { reconcileManagedAgentRuntimes } from "@/shared/api/tauriManagedAgents";
|
||||
|
||||
export function useManagedAgentRuntimeReconciliation(
|
||||
communities: readonly { relayUrl: string }[],
|
||||
): void {
|
||||
const queryClient = useQueryClient();
|
||||
const reconciled = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (reconciled.current) return;
|
||||
reconciled.current = true;
|
||||
|
||||
void reconcileManagedAgentRuntimes(communities)
|
||||
.then((runtimes) => {
|
||||
queryClient.setQueryData(managedAgentRuntimesQueryKey, runtimes);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(
|
||||
"[managed-agent-runtimes] startup reconcile failed:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}, [communities, queryClient]);
|
||||
}
|
||||
@@ -50,6 +50,8 @@ import {
|
||||
MODAL_SEARCH_SHELL_CLASS,
|
||||
} from "@/shared/ui/modalSearchStyles";
|
||||
import { MembersSidebarMemberCard } from "./MembersSidebarMemberCard";
|
||||
import { useManagedAgentRuntimesQuery } from "@/features/agents/managedAgentRuntimeHooks";
|
||||
import { findManagedAgentRuntime } from "@/features/agents/managedAgentRuntimeStatus";
|
||||
import { EditRespondToDialog } from "./EditRespondToDialog";
|
||||
import { useMembersSidebarActions } from "./useMembersSidebarActions";
|
||||
import { useMembersSidebarModeration } from "./useMembersSidebarModeration";
|
||||
@@ -124,6 +126,7 @@ type MembersSidebarProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onViewActivity?: (pubkey: string) => void;
|
||||
relayUrl?: string;
|
||||
};
|
||||
|
||||
export function MembersSidebar({
|
||||
@@ -132,8 +135,12 @@ export function MembersSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
onViewActivity,
|
||||
relayUrl,
|
||||
}: MembersSidebarProps) {
|
||||
const channelId = channel?.id ?? null;
|
||||
const managedAgentRuntimesQuery = useManagedAgentRuntimesQuery({
|
||||
enabled: open,
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
@@ -587,6 +594,15 @@ export function MembersSidebar({
|
||||
? managedAgentByPubkey.get(normalizePubkey(member.pubkey))
|
||||
: undefined
|
||||
}
|
||||
managedAgentRuntime={
|
||||
memberIsBot && relayUrl
|
||||
? findManagedAgentRuntime(
|
||||
managedAgentRuntimesQuery.data ?? [],
|
||||
member.pubkey,
|
||||
relayUrl,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
member={member}
|
||||
memberIsBot={memberIsBot}
|
||||
memberAvatarLabel={
|
||||
|
||||
@@ -20,13 +20,16 @@ import {
|
||||
} from "@/features/agents/lib/managedAgentControlActions";
|
||||
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
|
||||
import { PresenceDot } from "@/features/presence/ui/PresenceBadge";
|
||||
import { agentCommunityAvailability } from "@/features/agents/managedAgentRuntimeStatus";
|
||||
import { truncatePubkey } from "@/shared/lib/pubkey";
|
||||
import type {
|
||||
ChannelMember,
|
||||
ManagedAgent,
|
||||
ManagedAgentRuntimeStatus,
|
||||
PresenceStatus,
|
||||
} from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -45,6 +48,7 @@ type MembersSidebarMemberCardProps = {
|
||||
isActionPending: boolean;
|
||||
isArchived: boolean;
|
||||
managedAgent?: ManagedAgent;
|
||||
managedAgentRuntime?: ManagedAgentRuntimeStatus;
|
||||
member: ChannelMember;
|
||||
memberAvatarLabel: string;
|
||||
memberIsBot: boolean;
|
||||
@@ -104,19 +108,6 @@ function formatRespondToLabel(agent: ManagedAgent) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatManagedAgentStatus(agent: ManagedAgent) {
|
||||
switch (agent.status) {
|
||||
case "running":
|
||||
return "Running";
|
||||
case "stopped":
|
||||
return "Stopped";
|
||||
case "deployed":
|
||||
return "Deployed";
|
||||
case "not_deployed":
|
||||
return "Not deployed";
|
||||
}
|
||||
}
|
||||
|
||||
export function MembersSidebarMemberCard({
|
||||
canChangeRole,
|
||||
canModerate,
|
||||
@@ -124,6 +115,7 @@ export function MembersSidebarMemberCard({
|
||||
isActionPending,
|
||||
isArchived,
|
||||
managedAgent,
|
||||
managedAgentRuntime,
|
||||
member,
|
||||
memberAvatarLabel,
|
||||
memberIsBot,
|
||||
@@ -205,13 +197,21 @@ export function MembersSidebarMemberCard({
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{managedAgent ? (
|
||||
<span
|
||||
className="sr-only"
|
||||
{managedAgentRuntime || managedAgent ? (
|
||||
<Badge
|
||||
className="mt-1 normal-case tracking-normal"
|
||||
data-testid={`sidebar-managed-agent-status-${member.pubkey}`}
|
||||
variant={
|
||||
managedAgentRuntime &&
|
||||
agentCommunityAvailability(managedAgentRuntime) === "Here"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{formatManagedAgentStatus(managedAgent)}
|
||||
</span>
|
||||
{managedAgentRuntime
|
||||
? agentCommunityAvailability(managedAgentRuntime)
|
||||
: "Unavailable"}
|
||||
</Badge>
|
||||
) : null}
|
||||
{managedAgent ? (
|
||||
<span
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { useManagedAgentsQuery } from "@/features/agents/hooks";
|
||||
import {
|
||||
useManagedAgentRuntimeAction,
|
||||
useManagedAgentRuntimesQuery,
|
||||
} from "@/features/agents/managedAgentRuntimeHooks";
|
||||
import {
|
||||
agentCommunityAvailability,
|
||||
agentCommunityStatusDetail,
|
||||
managedAgentRuntimeKey,
|
||||
} from "@/features/agents/managedAgentRuntimeStatus";
|
||||
import type { ManagedAgentRuntimeStatus } from "@/shared/api/types";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { truncatePubkey } from "@/shared/lib/pubkey";
|
||||
import { SettingsSectionHeader } from "./SettingsSectionHeader";
|
||||
|
||||
export function ActiveAgentCommunitiesSettingsCard() {
|
||||
const agentsQuery = useManagedAgentsQuery();
|
||||
const runtimesQuery = useManagedAgentRuntimesQuery();
|
||||
const action = useManagedAgentRuntimeAction();
|
||||
const [pendingRuntimeKey, setPendingRuntimeKey] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const agentNames = React.useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
(agentsQuery.data ?? []).map((agent) => [
|
||||
agent.pubkey.toLowerCase(),
|
||||
agent.name,
|
||||
]),
|
||||
),
|
||||
[agentsQuery.data],
|
||||
);
|
||||
const runtimes = runtimesQuery.data ?? [];
|
||||
|
||||
async function runAction(runtime: ManagedAgentRuntimeStatus) {
|
||||
setPendingRuntimeKey(managedAgentRuntimeKey(runtime));
|
||||
try {
|
||||
await action.mutateAsync({
|
||||
action:
|
||||
runtime.lifecycle === "starting" ||
|
||||
runtime.lifecycle === "listening" ||
|
||||
runtime.lifecycle === "waking" ||
|
||||
runtime.lifecycle === "ready"
|
||||
? "stop"
|
||||
: runtime.lifecycle === "stopped"
|
||||
? "start"
|
||||
: "restart",
|
||||
pubkey: runtime.pubkey,
|
||||
relayUrl: runtime.relayUrl,
|
||||
});
|
||||
} finally {
|
||||
setPendingRuntimeKey(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="min-w-0" data-testid="active-agent-communities">
|
||||
<SettingsSectionHeader
|
||||
title="Active in communities"
|
||||
description="See and control each community where this device runs your agents."
|
||||
/>
|
||||
<div className="overflow-hidden rounded-xl border border-border/60">
|
||||
{runtimesQuery.isPending ? (
|
||||
<p className="px-4 py-3 text-sm text-muted-foreground">Loading…</p>
|
||||
) : runtimes.length === 0 ? (
|
||||
<p className="px-4 py-3 text-sm text-muted-foreground">
|
||||
No agent community runtimes found.
|
||||
</p>
|
||||
) : (
|
||||
runtimes.map((runtime) => {
|
||||
const status = agentCommunityAvailability(runtime);
|
||||
const detail = agentCommunityStatusDetail(runtime);
|
||||
const runtimeKey = managedAgentRuntimeKey(runtime);
|
||||
const pending = pendingRuntimeKey === runtimeKey;
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-3 border-b border-border/60 px-4 py-3 last:border-b-0"
|
||||
data-pubkey={runtime.pubkey}
|
||||
data-relay-url={runtime.relayUrl}
|
||||
data-testid="agent-community-runtime"
|
||||
key={runtimeKey}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm font-medium">
|
||||
{agentNames.get(runtime.pubkey.toLowerCase()) ??
|
||||
truncatePubkey(runtime.pubkey)}
|
||||
</p>
|
||||
<Badge
|
||||
variant={status === "Here" ? "default" : "secondary"}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{runtime.relayUrl}
|
||||
</p>
|
||||
{detail ? (
|
||||
<p className="text-xs text-muted-foreground">{detail}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{runtime.localSetup ? (
|
||||
<Button
|
||||
disabled={pending}
|
||||
onClick={() => void runAction(runtime)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{pending
|
||||
? "Working…"
|
||||
: runtime.lifecycle === "stopped"
|
||||
? "Start"
|
||||
: runtime.lifecycle === "failed"
|
||||
? "Restart"
|
||||
: "Stop"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
{action.error instanceof Error ? (
|
||||
<p className="mt-2 text-sm text-destructive">{action.error.message}</p>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -64,6 +64,7 @@ import { MobilePairingCard } from "./MobilePairingCard";
|
||||
import { ModerationQueueCard } from "./ModerationQueueCard";
|
||||
import { NotificationSettingsCard } from "./NotificationSettingsCard";
|
||||
import { PreventSleepSettingsCard } from "./PreventSleepSettingsCard";
|
||||
import { ActiveAgentCommunitiesSettingsCard } from "./ActiveAgentCommunitiesSettingsCard";
|
||||
import { GlobalAgentConfigSettingsCard } from "./GlobalAgentConfigSettingsCard";
|
||||
import { HostedCommunitiesSettingsCard } from "./HostedCommunitiesSettingsCard";
|
||||
import { ProfileSettingsCard } from "./ProfileSettingsCard";
|
||||
@@ -725,6 +726,7 @@ export function renderSettingsSection(
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<PreventSleepSettingsCard />
|
||||
<ActiveAgentCommunitiesSettingsCard />
|
||||
<GlobalAgentConfigSettingsCard />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,10 @@ import {
|
||||
invokeTauri,
|
||||
type RawManagedAgent,
|
||||
} from "@/shared/api/tauri";
|
||||
import type { ManagedAgent } from "@/shared/api/types";
|
||||
import type {
|
||||
ManagedAgent,
|
||||
ManagedAgentRuntimeStatus,
|
||||
} from "@/shared/api/types";
|
||||
|
||||
export async function startManagedAgent(pubkey: string): Promise<ManagedAgent> {
|
||||
const response = await invokeTauri<RawManagedAgent>("start_managed_agent", {
|
||||
@@ -46,3 +49,48 @@ export async function setManagedAgentAutoRestart(
|
||||
);
|
||||
return fromRawManagedAgent(response);
|
||||
}
|
||||
|
||||
export async function listManagedAgentRuntimes(): Promise<
|
||||
ManagedAgentRuntimeStatus[]
|
||||
> {
|
||||
return invokeTauri<ManagedAgentRuntimeStatus[]>(
|
||||
"list_managed_agent_runtimes",
|
||||
);
|
||||
}
|
||||
|
||||
export async function startManagedAgentRuntime(
|
||||
pubkey: string,
|
||||
relayUrl: string,
|
||||
): Promise<ManagedAgentRuntimeStatus> {
|
||||
return invokeTauri("start_managed_agent_runtime", { pubkey, relayUrl });
|
||||
}
|
||||
|
||||
export async function stopManagedAgentRuntime(
|
||||
pubkey: string,
|
||||
relayUrl: string,
|
||||
): Promise<ManagedAgentRuntimeStatus> {
|
||||
return invokeTauri("stop_managed_agent_runtime", { pubkey, relayUrl });
|
||||
}
|
||||
|
||||
export async function restartManagedAgentRuntime(
|
||||
pubkey: string,
|
||||
relayUrl: string,
|
||||
): Promise<ManagedAgentRuntimeStatus> {
|
||||
return invokeTauri("restart_managed_agent_runtime", { pubkey, relayUrl });
|
||||
}
|
||||
|
||||
export async function putManagedAgentRuntimeLifecycle(
|
||||
outerPubkey: string,
|
||||
payload: unknown,
|
||||
): Promise<ManagedAgentRuntimeStatus> {
|
||||
return invokeTauri("put_managed_agent_runtime_lifecycle", {
|
||||
outerPubkey,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
export async function reconcileManagedAgentRuntimes(
|
||||
communities: readonly { relayUrl: string }[],
|
||||
): Promise<ManagedAgentRuntimeStatus[]> {
|
||||
return invokeTauri("reconcile_managed_agent_runtimes", { communities });
|
||||
}
|
||||
|
||||
@@ -311,6 +311,27 @@ export type RelayAgent = {
|
||||
respondToAllowlist: string[];
|
||||
};
|
||||
|
||||
export type ManagedAgentRuntimeLifecycle =
|
||||
| "starting"
|
||||
| "listening"
|
||||
| "waking"
|
||||
| "ready"
|
||||
| "failed"
|
||||
| "stopped";
|
||||
|
||||
export type ManagedAgentRuntimeStatus = {
|
||||
pubkey: string;
|
||||
/** Exact submitted descriptor, present only on startup reconcile results. */
|
||||
requestedRelayUrl?: string;
|
||||
/** Canonical, backend-owned pair identity component. Do not normalize in TS. */
|
||||
relayUrl: string;
|
||||
localSetup: boolean;
|
||||
lifecycle: ManagedAgentRuntimeLifecycle;
|
||||
pid: number | null;
|
||||
error: string | null;
|
||||
logPath: string | null;
|
||||
};
|
||||
|
||||
export type ManagedAgentBackend =
|
||||
| { type: "local" }
|
||||
| { type: "provider"; id: string; config: Record<string, unknown> };
|
||||
|
||||
Reference in New Issue
Block a user