Files
buzz/desktop/src/features/projects/useProjectChannelRequests.ts
thomaspblock de188ebf06 feat(projects): add agent and CLI project-home support (#6590)
## Summary
- inject bounded project-home identity and repository context into
managed agent sessions
- add project-aware CLI flows for creating projects, repositories,
issues, and related channels
- match project homes through the existing relay query surface, then
filter channel metadata client-side

This is Part 1 of the channel-first Projects stack. Part 2 contains
project creation and model foundations.

## Testing
- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings`
- `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test
ignored
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- validate project-home agent context and project-aware CLI commands
against a staging relay
- healthy signals: project context matches the active channel, explicit
repo coordinates remain stable, and normal channels receive no project
block
- failure signals: cross-channel project context, duplicate project
creation, or commands targeting an unrelated repository; mitigate by
reverting this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
2026-08-26 12:09:06 -07:00

181 lines
5.9 KiB
TypeScript

import * as React from "react";
import { toast } from "sonner";
import { classifyAgentManagementOrigin } from "@/features/agents/agentManagementBuffer";
import { subscribeProjectChannelRequests } from "@/features/agents/observerRelayStore";
import { useManagedAgentsQuery } from "@/features/agents/hooks";
import { useChannelsQuery } from "@/features/channels/hooks";
import { useChannelTemplatesQuery } from "@/features/channel-templates/hooks";
import { useProjectsQuery } from "@/features/projects/hooks";
import type { ProjectChannelRequest } from "@/features/projects/projectChannelRequest";
import {
advanceProjectChannelRequestQueue,
createProjectChannelRequestQueue,
enqueueProjectChannelRequest,
type AcceptedProjectChannelRequest,
} from "@/features/projects/projectChannelRequestQueue";
import { useAddProjectChannelMutation } from "@/features/projects/useAddProjectChannel";
import { useIdentityQuery } from "@/shared/api/hooks";
import { normalizePubkey } from "@/shared/lib/pubkey";
export function useProjectChannelRequests() {
const projectsQuery = useProjectsQuery();
const channelsQuery = useChannelsQuery();
const identityQuery = useIdentityQuery();
const managedAgentsQuery = useManagedAgentsQuery();
const templatesQuery = useChannelTemplatesQuery();
const mutation = useAddProjectChannelMutation();
const [request, setRequest] = React.useState<ProjectChannelRequest | null>(
null,
);
const [sourceAgentPubkey, setSourceAgentPubkey] = React.useState<
string | null
>(null);
const [error, setError] = React.useState<string | null>(null);
const acceptedRequests = React.useRef(createProjectChannelRequestQueue());
const bufferedRequests = React.useRef<
Array<{ agentPubkey: string; request: ProjectChannelRequest }>
>([]);
const managedAgentsRef = React.useRef(managedAgentsQuery.data);
const channelsRef = React.useRef(channelsQuery.data);
const accept = React.useEffectEvent(
(agentPubkey: string, next: ProjectChannelRequest) => {
if (
classifyAgentManagementOrigin(
managedAgentsRef.current,
channelsRef.current,
agentPubkey,
next.request.homeChannelId,
) !== "accept"
) {
return;
}
const result = enqueueProjectChannelRequest(acceptedRequests.current, {
agentPubkey,
request: next,
});
if (result.status === "show") showRequest(result.candidate);
},
);
React.useEffect(() => {
managedAgentsRef.current = managedAgentsQuery.data;
channelsRef.current = channelsQuery.data;
if (!managedAgentsQuery.data || !channelsQuery.data) return;
const buffered = bufferedRequests.current.splice(0);
for (const candidate of buffered) {
accept(candidate.agentPubkey, candidate.request);
}
}, [channelsQuery.data, managedAgentsQuery.data]);
React.useEffect(
() =>
subscribeProjectChannelRequests((agentPubkey, next) => {
const origin = classifyAgentManagementOrigin(
managedAgentsRef.current,
channelsRef.current,
agentPubkey,
next.request.homeChannelId,
);
if (origin === "buffer") {
bufferedRequests.current.push({ agentPubkey, request: next });
if (bufferedRequests.current.length > 100) {
bufferedRequests.current.shift();
}
return;
}
accept(agentPubkey, next);
}),
[],
);
function showRequest(candidate: AcceptedProjectChannelRequest | null) {
setError(null);
setSourceAgentPubkey(candidate?.agentPubkey ?? null);
setRequest(candidate?.request ?? null);
}
function advanceRequestQueue() {
showRequest(advanceProjectChannelRequestQueue(acceptedRequests.current));
}
const project =
request == null
? null
: ([...(projectsQuery.data ?? [])]
.filter(
(candidate) =>
!candidate.legacy &&
candidate.visibility !== "unlisted" &&
candidate.projectChannelId === request.request.homeChannelId,
)
.sort((left, right) => left.createdAt - right.createdAt)[0] ?? null);
const template =
request?.request.templateName == null
? null
: ((templatesQuery.data ?? []).find(
(candidate) =>
candidate.name.trim().toLocaleLowerCase() ===
request.request.templateName?.trim().toLocaleLowerCase(),
) ?? null);
function dismiss() {
if (mutation.isPending) return;
advanceRequestQueue();
}
async function approve() {
if (!request || !sourceAgentPubkey || !project) {
setError(
"The project home could not be resolved. Refresh and try again.",
);
return;
}
const identity = identityQuery.data?.pubkey;
const owner = normalizePubkey(project.owner);
const agent = normalizePubkey(sourceAgentPubkey);
if ((!identity || normalizePubkey(identity) !== owner) && agent !== owner) {
setError("Only the project owner can approve this channel.");
return;
}
if (request.request.templateName && !template) {
setError(
`Channel template "${request.request.templateName}" is not available on this device.`,
);
return;
}
setError(null);
try {
await mutation.mutateAsync({
description: request.request.description,
name: request.request.name,
ownerControlAgentPubkey:
agent === owner ? sourceAgentPubkey : undefined,
project,
templateId: template?.id,
ttlSeconds: request.request.ttlSeconds,
visibility: request.request.visibility,
});
toast.success(`Channel "#${request.request.name}" created.`);
advanceRequestQueue();
} catch (nextError) {
setError(
nextError instanceof Error
? nextError.message
: "Failed to create the project channel.",
);
}
}
return {
approve,
dismiss,
error,
isPending: mutation.isPending,
project,
request,
template,
};
}