mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Restore managed agents on launch and recover chat after relay restarts (#93)
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
startManagedAgent,
|
||||
stopManagedAgent,
|
||||
} from "@/shared/api/tauri";
|
||||
import { setManagedAgentStartOnAppLaunch } from "@/shared/api/tauriManagedAgents";
|
||||
import type {
|
||||
CreateManagedAgentInput,
|
||||
ManagedAgent,
|
||||
@@ -155,6 +156,24 @@ export function useStopManagedAgentMutation() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useSetManagedAgentStartOnAppLaunchMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
pubkey,
|
||||
startOnAppLaunch,
|
||||
}: {
|
||||
pubkey: string;
|
||||
startOnAppLaunch: boolean;
|
||||
}) => setManagedAgentStartOnAppLaunch(pubkey, startOnAppLaunch),
|
||||
onSettled: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey });
|
||||
await queryClient.invalidateQueries({ queryKey: relayAgentsQueryKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteManagedAgentMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useManagedAgentsQuery,
|
||||
useMintManagedAgentTokenMutation,
|
||||
useRelayAgentsQuery,
|
||||
useSetManagedAgentStartOnAppLaunchMutation,
|
||||
useStartManagedAgentMutation,
|
||||
useStopManagedAgentMutation,
|
||||
} from "@/features/agents/hooks";
|
||||
@@ -28,6 +29,7 @@ export function AgentsView() {
|
||||
const managedAgentsQuery = useManagedAgentsQuery();
|
||||
const startMutation = useStartManagedAgentMutation();
|
||||
const stopMutation = useStopManagedAgentMutation();
|
||||
const startOnLaunchMutation = useSetManagedAgentStartOnAppLaunchMutation();
|
||||
const deleteMutation = useDeleteManagedAgentMutation();
|
||||
const mintTokenMutation = useMintManagedAgentTokenMutation();
|
||||
const [isCreateOpen, setIsCreateOpen] = React.useState(false);
|
||||
@@ -124,6 +126,32 @@ export function AgentsView() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleStartOnAppLaunch(
|
||||
pubkey: string,
|
||||
startOnAppLaunch: boolean,
|
||||
) {
|
||||
setActionNoticeMessage(null);
|
||||
setActionErrorMessage(null);
|
||||
|
||||
try {
|
||||
const updated = await startOnLaunchMutation.mutateAsync({
|
||||
pubkey,
|
||||
startOnAppLaunch,
|
||||
});
|
||||
setActionNoticeMessage(
|
||||
updated.startOnAppLaunch
|
||||
? `Will start ${updated.name} automatically when the desktop app opens.`
|
||||
: `${updated.name} will stay manual-start only.`,
|
||||
);
|
||||
} catch (error) {
|
||||
setActionErrorMessage(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to update startup preference.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMintToken(pubkey: string, name: string) {
|
||||
setActionNoticeMessage(null);
|
||||
setActionErrorMessage(null);
|
||||
@@ -177,6 +205,7 @@ export function AgentsView() {
|
||||
const isActionPending =
|
||||
startMutation.isPending ||
|
||||
stopMutation.isPending ||
|
||||
startOnLaunchMutation.isPending ||
|
||||
deleteMutation.isPending ||
|
||||
mintTokenMutation.isPending;
|
||||
|
||||
@@ -218,6 +247,9 @@ export function AgentsView() {
|
||||
onStop={(pubkey) => {
|
||||
void handleStop(pubkey);
|
||||
}}
|
||||
onToggleStartOnAppLaunch={(pubkey, startOnAppLaunch) => {
|
||||
void handleToggleStartOnAppLaunch(pubkey, startOnAppLaunch);
|
||||
}}
|
||||
selectedAgentPubkey={selectedAgent?.pubkey ?? null}
|
||||
/>
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export function CreateAgentDialog({
|
||||
const [relayUrl, setRelayUrl] = React.useState("");
|
||||
const [mintToken, setMintToken] = React.useState(true);
|
||||
const [spawnAfterCreate, setSpawnAfterCreate] = React.useState(true);
|
||||
const [startOnAppLaunch, setStartOnAppLaunch] = React.useState(true);
|
||||
const [tokenName, setTokenName] = React.useState("");
|
||||
const [selectedScopes, setSelectedScopes] = React.useState<Set<TokenScope>>(
|
||||
() => new Set<TokenScope>(DEFAULT_MANAGED_AGENT_SCOPES),
|
||||
@@ -128,6 +129,7 @@ export function CreateAgentDialog({
|
||||
setRelayUrl("");
|
||||
setMintToken(true);
|
||||
setSpawnAfterCreate(true);
|
||||
setStartOnAppLaunch(true);
|
||||
setTokenName("");
|
||||
setSelectedScopes(new Set<TokenScope>(DEFAULT_MANAGED_AGENT_SCOPES));
|
||||
setAcpCommand("sprout-acp");
|
||||
@@ -215,6 +217,7 @@ export function CreateAgentDialog({
|
||||
tokenName: tokenName.trim() || undefined,
|
||||
tokenScopes: [...selectedScopes],
|
||||
spawnAfterCreate,
|
||||
startOnAppLaunch,
|
||||
};
|
||||
const created = await createMutation.mutateAsync(input);
|
||||
handleOpenChange(false);
|
||||
@@ -258,12 +261,16 @@ export function CreateAgentDialog({
|
||||
setMintToken((current) => !current);
|
||||
}
|
||||
}}
|
||||
onToggleStartOnAppLaunch={() => {
|
||||
setStartOnAppLaunch((current) => !current);
|
||||
}}
|
||||
onToggleSpawnAfterCreate={() => {
|
||||
if (!spawnToggleDisabled) {
|
||||
setSpawnAfterCreate((current) => !current);
|
||||
}
|
||||
}}
|
||||
prereqs={prereqs}
|
||||
startOnAppLaunch={startOnAppLaunch}
|
||||
spawnAfterCreate={spawnAfterCreate}
|
||||
spawnToggleDisabled={spawnToggleDisabled}
|
||||
/>
|
||||
|
||||
@@ -261,9 +261,11 @@ export function CreateAgentOptionToggles({
|
||||
mintToken,
|
||||
mintToggleDisabled,
|
||||
prereqs,
|
||||
startOnAppLaunch,
|
||||
spawnAfterCreate,
|
||||
spawnToggleDisabled,
|
||||
onToggleMintToken,
|
||||
onToggleStartOnAppLaunch,
|
||||
onToggleSpawnAfterCreate,
|
||||
}: {
|
||||
isMintSupported: boolean;
|
||||
@@ -271,13 +273,15 @@ export function CreateAgentOptionToggles({
|
||||
mintToken: boolean;
|
||||
mintToggleDisabled: boolean;
|
||||
prereqs: ManagedAgentPrereqs | null;
|
||||
startOnAppLaunch: boolean;
|
||||
spawnAfterCreate: boolean;
|
||||
spawnToggleDisabled: boolean;
|
||||
onToggleMintToken: () => void;
|
||||
onToggleStartOnAppLaunch: () => void;
|
||||
onToggleSpawnAfterCreate: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<button
|
||||
aria-pressed={mintToken}
|
||||
className={cn(
|
||||
@@ -299,6 +303,26 @@ export function CreateAgentOptionToggles({
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-pressed={startOnAppLaunch}
|
||||
className={cn(
|
||||
"rounded-2xl border px-4 py-3 text-left transition-colors",
|
||||
startOnAppLaunch
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-border/70 bg-background/70",
|
||||
)}
|
||||
onClick={onToggleStartOnAppLaunch}
|
||||
type="button"
|
||||
>
|
||||
<p className="text-sm font-semibold tracking-tight">
|
||||
Start on app launch
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Reopen this local ACP harness automatically when the desktop app
|
||||
starts.
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-pressed={spawnAfterCreate}
|
||||
className={cn(
|
||||
|
||||
@@ -15,6 +15,7 @@ import { formatTimestamp, truncatePubkey } from "./agentUi";
|
||||
|
||||
export function ManagedAgentCard({
|
||||
agent,
|
||||
onToggleStartOnAppLaunch,
|
||||
onAddToChannel,
|
||||
isSelected,
|
||||
onDelete,
|
||||
@@ -24,6 +25,7 @@ export function ManagedAgentCard({
|
||||
onStop,
|
||||
}: {
|
||||
agent: ManagedAgent;
|
||||
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
|
||||
onAddToChannel: (agent: ManagedAgent) => void;
|
||||
isSelected: boolean;
|
||||
onDelete: (pubkey: string) => void;
|
||||
@@ -137,6 +139,19 @@ export function ManagedAgentCard({
|
||||
Mint token
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch)
|
||||
}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{agent.startOnAppLaunch
|
||||
? "Launch on open: on"
|
||||
: "Launch on open: off"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => onDelete(agent.pubkey)}
|
||||
size="sm"
|
||||
|
||||
@@ -21,6 +21,7 @@ export function ManagedAgentsSection({
|
||||
onSelect,
|
||||
onStart,
|
||||
onStop,
|
||||
onToggleStartOnAppLaunch,
|
||||
}: {
|
||||
actionErrorMessage: string | null;
|
||||
actionNoticeMessage: string | null;
|
||||
@@ -37,6 +38,7 @@ export function ManagedAgentsSection({
|
||||
onSelect: (pubkey: string) => void;
|
||||
onStart: (pubkey: string) => void;
|
||||
onStop: (pubkey: string) => void;
|
||||
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
@@ -119,6 +121,11 @@ export function ManagedAgentsSection({
|
||||
onStop(pubkey);
|
||||
}
|
||||
}}
|
||||
onToggleStartOnAppLaunch={(pubkey, startOnAppLaunch) => {
|
||||
if (!isActionPending) {
|
||||
onToggleStartOnAppLaunch(pubkey, startOnAppLaunch);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -178,6 +178,17 @@ export function useChannelSubscription(channel: Channel | null) {
|
||||
|
||||
let isDisposed = false;
|
||||
let cleanup: (() => Promise<void>) | undefined;
|
||||
const disposeReconnectListener = relayClient.subscribeToReconnects(() => {
|
||||
void syncLatestHistory().catch((error) => {
|
||||
if (!isDisposed) {
|
||||
console.error(
|
||||
"Failed to refresh channel history after reconnecting",
|
||||
channelId,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
relayClient
|
||||
.subscribeToChannel(channelId, (event) => {
|
||||
@@ -209,6 +220,7 @@ export function useChannelSubscription(channel: Channel | null) {
|
||||
|
||||
return () => {
|
||||
isDisposed = true;
|
||||
disposeReconnectListener();
|
||||
if (cleanup) {
|
||||
void cleanup();
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import type { Channel, RelayEvent } from "@/shared/api/types";
|
||||
import {
|
||||
KIND_STREAM_MESSAGE,
|
||||
KIND_STREAM_MESSAGE_DIFF,
|
||||
KIND_STREAM_MESSAGE_EDIT,
|
||||
KIND_STREAM_MESSAGE_V2,
|
||||
KIND_TYPING_INDICATOR,
|
||||
} from "@/shared/constants/kinds";
|
||||
|
||||
@@ -40,8 +38,6 @@ function isTypingCompletionEvent(event: RelayEvent | null | undefined) {
|
||||
|
||||
return (
|
||||
event.kind === KIND_STREAM_MESSAGE ||
|
||||
event.kind === KIND_STREAM_MESSAGE_V2 ||
|
||||
event.kind === KIND_STREAM_MESSAGE_EDIT ||
|
||||
event.kind === KIND_STREAM_MESSAGE_DIFF
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user