From 9e371b4a2554dc12adddbe7f74200899a795504e Mon Sep 17 00:00:00 2001 From: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Date: Thu, 24 Sep 2026 22:16:12 -0700 Subject: [PATCH 1/5] feat(profiles): open targeted agent editor from owner profile Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: Kalvin Chau Signed-off-by: Kalvin Chau --- docs/profiles.md | 8 +- src/bundled/agents/AgentControlPanel.tsx | 43 +++- src/bundled/agents/AgentsPage.test.tsx | 219 +++++++++++++++++- src/bundled/agents/AgentsPage.tsx | 55 ++++- src/bundled/agents/edit-route.test.ts | 18 ++ src/bundled/agents/edit-route.ts | 13 ++ src/bundled/agents/index.tsx | 17 +- .../profiles/ProfileAgentIdentity.test.tsx | 80 +++++++ .../profiles/ProfileInstances.test.tsx | 65 ++++++ src/bundled/profiles/ProfileInstances.tsx | 32 +++ src/bundled/profiles/ProfilePanel.tsx | 4 +- 11 files changed, 540 insertions(+), 14 deletions(-) create mode 100644 src/bundled/agents/edit-route.test.ts create mode 100644 src/bundled/agents/edit-route.ts diff --git a/docs/profiles.md b/docs/profiles.md index 21edb16e5..100095b06 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -22,8 +22,7 @@ The host error, runtime-unavailable reason, unconfirmed-status notice and Retry are shown once, by the profile actions. Browser/unavailable hosts, loading/error without evidence, unknown keys and records saved for another community render nothing, leaving the public identity. Community switches re-filter immediately. -Start/Stop/Restart are the separate profile actions above; harness logs stay on -Agents. +Start/Stop/Restart are the separate profile actions above. The owner-only **Agent instructions** ingress in Info opens the targeted Agents-page editor for a unique native record; harness logs stay on Agents. ## Owner runtime tab @@ -276,7 +275,7 @@ unambiguous native match; an instance view selects that record by ID. Archive labels come from `session.archives`, keyed by identity, rather than native process status. Archived rows use the same explicit target and remain navigable. The existing public profile tabs and actions are reused, not replaced by an Agents -page or a second editor. Browser fixtures exercise live/archived selection, +page or a second editor. The owner-only ingress requires a verified NIP-OA owner and a ready, unique native match in this community; the versioned agent-identity route opens the existing editor, never a guessed sibling. Browser fixtures exercise live/archived selection, tabs, back, keyboard close/focus and deletion through the real plugin/Channels host in Chromium and WebKit. React tests cover exact actions, failed-read recovery, non-owner denial and scope changes with synthetic native data. @@ -336,8 +335,7 @@ Retired relay presentations cannot dispatch commands. The separate runtime child owns badges and runtime detail; actions do not infer relay readiness. -Info adds no edit ingress; the owner Runtime tab reuses the Agents editor -dialog rather than a route or second editor. Mounted React regression tests exercise exact dispatch, pending/failure/ +Info adds an owner-gated route to the Agents editor for a unique native match; the owner Runtime tab also reuses the editor dialog. Mounted React regression tests exercise exact dispatch, pending/failure/ recovery and profile/community lifecycle through the real controller projection with a synthetic native host. Live process/credential handover and rendered native acceptance remain attended checks, not established by these tests. diff --git a/src/bundled/agents/AgentControlPanel.tsx b/src/bundled/agents/AgentControlPanel.tsx index a20a0dedf..fbc55f9ed 100644 --- a/src/bundled/agents/AgentControlPanel.tsx +++ b/src/bundled/agents/AgentControlPanel.tsx @@ -1,5 +1,7 @@ import type { useIdentityNames } from "../../features/identity-names/react"; import { useAgentControl } from "../../features/agents/control-react"; +import { sameCommunityAgents } from "../../features/agents/choices"; +import type { PageNavigation } from "../../features/navigation/service"; import { useEffect, useState, type ReactNode } from "react"; import type { AgentControl, @@ -23,11 +25,17 @@ export function AgentControlPanel({ createOwner, resolveName, children, + editTarget, + editRequest, + onCloseTarget, }: { resolveName?: ReturnType; control: AgentControl; importDestination?: string; createOwner?: string | undefined; + editTarget?: string | null; + editRequest?: PageNavigation; + onCloseTarget?: () => void; children?: ( state: AgentControlState, edit: (agent: AgentView, avatar?: string) => void, @@ -49,8 +57,10 @@ export function AgentControlPanel({ avatar?: string; } | null>(null); const [deleting, setDeleting] = useState(null); - const edit = (agent: AgentView, avatar?: string) => + const edit = (agent: AgentView, avatar?: string) => { setSelected({ id: agent.id, ...(avatar ? { avatar } : {}) }); + if (editTarget) onCloseTarget?.(); + }; const duplicate = (agent: AgentView) => setAdding({ destination: agent.relayUrl, @@ -81,7 +91,36 @@ export function AgentControlPanel({ ...facts, { pubkey: agent.pubkey, name: agent.name, isAgent: true }, ]) ?? agent.name; - const editing = state.data?.agents.find((agent) => agent.id === selected?.id); + // Route selection takes precedence over card-local editing. Never guess among + // multiple native records for the same public identity in this community. + const routed = + editTarget && importDestination && createOwner + ? sameCommunityAgents( + state.data?.agents ?? [], + `${importDestination}:${createOwner}`, + ).filter((agent) => agent.pubkey === editTarget) + : []; + const editing = editTarget + ? routed.length === 1 + ? routed[0] + : undefined + : state.data?.agents.find((agent) => agent.id === selected?.id); + useEffect(() => { + if ( + !editRequest || + editRequest.signal.aborted || + state.status === "loading" || + state.status === "idle" + ) + return; + if (state.status !== "ready") { + editRequest.complete({ status: "failed", reason: "unavailable" }); + } else if (editing) { + editRequest.complete({ status: "opened" }); + } else { + editRequest.complete({ status: "failed", reason: "not-found" }); + } + }, [editRequest, editing, state.status]); const deletion = state.data?.agents.find((agent) => agent.id === deleting); return (
{ function setup( mode = "ready", configure?: (fixture: ReturnType) => void, + navigation?: PageNavigation, + open?: ( + target: OpenTarget, + options?: { replace?: boolean }, + ) => Promise<{ status: "opened" }>, ) { const f = controlFixture(); configure?.(f); @@ -76,7 +83,12 @@ function setup( } : owned.session; let snapshot: RelaySnapshot = { - status: mode === "disconnected" ? "disconnected" : "ready", + status: + mode === "disconnected" + ? "disconnected" + : mode === "connecting" + ? "connecting" + : "ready", scope: mode === "connected" ? `wss://relay.example.test:${"de".repeat(32)}` @@ -104,7 +116,14 @@ function setup( }); snapshot = { ...snapshot, session: { ...session, names } }; disposals.push(() => names.dispose()); - render(); + render( + , + ); return { f, read, @@ -113,6 +132,16 @@ function setup( snapshot = { status: "ready", scope, generation, session }; for (const listener of listeners) listener(); }, + connect() { + snapshot = { + status: "ready", + scope: `wss://relay.example.test:${"de".repeat(32)}`, + viewer: "de".repeat(32), + generation: snapshot.generation, + session: snapshot.session, + }; + for (const listener of listeners) listener(); + }, }; } it("shows one managed card per exact destination and keeps unimported templates out of My agents", async () => { @@ -1384,3 +1413,189 @@ it("keeps collisions across different cross-community aliases and edits the exac ), ); }); + +function routed(pubkey: string) { + const complete = vi.fn(() => true); + const target: OpenTarget = { + version: 1, + kind: "page", + pluginId: "buzz.agents", + pageId: "agents", + scope: { + viewer: "de".repeat(32), + communityOrigin: "https://relay.example.test", + }, + route: { version: 1, params: { pubkey } }, + }; + const navigation = { + target, + signal: new AbortController().signal, + complete, + forSession() { + return this; + }, + } as unknown as PageNavigation; + return { navigation, complete }; +} + +it("opens the exact native agent editor on the routed page and acknowledges its presentation", async () => { + const { navigation, complete } = routed("ab".repeat(32)); + const { f } = setup("connected", undefined, navigation); + const dialog = await screen.findByRole("dialog", { name: "Edit agent" }); + expect(within(dialog).getByLabelText("Agent instructions")).toBeVisible(); + await waitFor(() => + expect(complete).toHaveBeenCalledWith({ status: "opened" }), + ); + fireEvent.change(within(dialog).getByLabelText("Name"), { + target: { value: "Targeted" }, + }); + fireEvent.click(within(dialog).getByRole("button", { name: "Save changes" })); + await waitFor(() => + expect( + f.calls.find((call) => call.action === "save")?.payload, + ).toMatchObject({ id: "fixture-agent" }), + ); +}); + +it("rejects missing and ambiguous route targets instead of choosing a namesake", async () => { + const { navigation, complete } = routed("ab".repeat(32)); + setup( + "connected", + (f) => f.data.agents.push({ ...structuredClone(f.agent), id: "duplicate" }), + navigation, + ); + await waitFor(() => + expect(complete).toHaveBeenCalledWith({ + status: "failed", + reason: "not-found", + }), + ); + expect(screen.queryByRole("dialog", { name: "Edit agent" })).toBeNull(); +}); + +it("rejects an edit route for a different community", async () => { + const { navigation, complete } = routed("ab".repeat(32)); + setup( + "connected", + (f) => { + f.agent.relayUrl = "wss://other.example"; + }, + navigation, + ); + await waitFor(() => + expect(complete).toHaveBeenCalledWith({ + status: "failed", + reason: "not-found", + }), + ); + expect(screen.queryByRole("dialog", { name: "Edit agent" })).toBeNull(); +}); + +it("closes a routed editor back to the unrouted Agents page", async () => { + const { navigation } = routed("ab".repeat(32)); + const open = vi.fn( + async (_target: OpenTarget, _options?: { replace?: boolean }) => ({ + status: "opened" as const, + }), + ); + setup("connected", undefined, navigation, open); + const dialog = await screen.findByRole("dialog", { name: "Edit agent" }); + await userEvent + .setup() + .click(within(dialog).getByRole("button", { name: "Close editor" })); + expect(open).toHaveBeenCalledWith( + { + version: 1, + kind: "page", + pluginId: "buzz.agents", + pageId: "agents", + scope: { + viewer: "de".repeat(32), + communityOrigin: "https://relay.example.test", + }, + }, + { replace: true }, + ); +}); + +it("retains a routed draft when the native save fails", async () => { + const { navigation } = routed("ab".repeat(32)); + const { f } = setup( + "connected", + (fixture) => fixture.failSave(true), + navigation, + ); + const dialog = await screen.findByRole("dialog", { name: "Edit agent" }); + fireEvent.change(within(dialog).getByLabelText("Name"), { + target: { value: "Unsaved draft" }, + }); + fireEvent.click(within(dialog).getByRole("button", { name: "Save changes" })); + await within(dialog).findByText(/Your edits are retained/); + expect(within(dialog).getByLabelText("Name")).toHaveValue("Unsaved draft"); + expect(f.calls.some((call) => call.action === "save")).toBe(true); +}); + +it("waits for a connecting relay before opening a routed editor", async () => { + const { navigation, complete } = routed("ab".repeat(32)); + const page = setup("connecting", undefined, navigation); + expect(complete).not.toHaveBeenCalled(); + await waitFor(() => expect(page.control.snapshot().status).toBe("ready")); + expect(complete).not.toHaveBeenCalled(); + act(() => page.connect()); + await screen.findByRole("dialog", { name: "Edit agent" }); + await waitFor(() => + expect(complete).toHaveBeenCalledWith({ status: "opened" }), + ); + expect(complete).not.toHaveBeenCalledWith({ + status: "failed", + reason: "unavailable", + }); +}); + +it("acknowledges the unrouted Agents page", async () => { + const { navigation, complete } = routed("ab".repeat(32)); + const target = navigation.target as Extract; + const unrouted = { + ...navigation, + target: { ...target, route: undefined }, + } as unknown as PageNavigation; + setup("connected", undefined, unrouted); + await waitFor(() => + expect(complete).toHaveBeenCalledWith({ status: "opened" }), + ); +}); + +it("clears an obsolete route before editing another card", async () => { + const { navigation } = routed("ab".repeat(32)); + const open = vi.fn( + async (_target: OpenTarget, _options?: { replace?: boolean }) => ({ + status: "opened" as const, + }), + ); + setup( + "connected", + (f) => { + f.data.agents.splice(0, 1); + }, + navigation, + open, + ); + await waitFor(() => + expect(screen.queryByRole("dialog", { name: "Edit agent" })).toBeNull(), + ); + const cards = await screen.findAllByRole("article", { + name: "Agent Fixture agent", + }); + const other = cards.find((card) => + card.textContent?.includes("wss://second.example"), + ); + if (!other) throw Error("Second destination missing"); + fireEvent.click( + within(other).getByRole("button", { name: "Actions for Fixture agent" }), + ); + fireEvent.click(await screen.findByRole("menuitem", { name: "Edit" })); + expect(open).toHaveBeenCalledWith( + expect.not.objectContaining({ route: expect.anything() }), + { replace: true }, + ); +}); diff --git a/src/bundled/agents/AgentsPage.tsx b/src/bundled/agents/AgentsPage.tsx index 1b76cddcd..a6aa5c6fa 100644 --- a/src/bundled/agents/AgentsPage.tsx +++ b/src/bundled/agents/AgentsPage.tsx @@ -1,5 +1,9 @@ import { useIdentityNames } from "../../features/identity-names/react"; -import { useEffect, useSyncExternalStore } from "react"; +import { useEffect, useMemo, useSyncExternalStore } from "react"; +import type { PageProps } from "../../features/pages/service"; +import type { OpenTarget } from "../../features/navigation/targets"; +import type { OpenResult } from "../../features/navigation/controller"; +import { editAgentRoute } from "./edit-route"; import type { AgentControl, AgentControlState, @@ -18,12 +22,40 @@ import { ManagedAgentActions } from "./ManagedAgentActions"; export function AgentsPage({ relay, control, -}: { + navigation, + open, +}: PageProps & { relay: RelayData; control?: AgentControl; + open?: ( + target: OpenTarget, + options?: { replace?: boolean }, + ) => Promise; }) { const connection = useRelayConnection(relay); const resolveName = useIdentityNames(connection.session.names); + const request = useMemo( + () => navigation?.forSession(relay, connection), + [navigation, relay, connection], + ); + const target = request?.target; + const editTarget = + target?.kind === "page" && target.route + ? editAgentRoute(target.route.params) + : null; + useEffect(() => { + if (!request || request.signal.aborted) return; + // The routed edit destination must not acknowledge an unrelated page. + if (target?.kind !== "page") return; + if (!editTarget && !target.route) request.complete({ status: "opened" }); + else if (!editTarget) + request.complete({ status: "failed", reason: "unavailable" }); + else if ( + !control || + (connection.status !== "ready" && connection.status !== "connecting") + ) + request.complete({ status: "failed", reason: "unavailable" }); + }, [request, target, editTarget, control, connection.status]); let importDestination = ""; if ( connection.viewer && @@ -62,6 +94,25 @@ export function AgentsPage({ {control ? ( { + if (target?.kind === "page" && open) + void open( + { + version: 1, + kind: "page", + pluginId: target.pluginId, + pageId: target.pageId, + ...(target.scope !== undefined + ? { scope: target.scope } + : {}), + }, + { replace: true }, + ); + }} resolveName={resolveName} importDestination={importDestination} createOwner={ diff --git a/src/bundled/agents/edit-route.test.ts b/src/bundled/agents/edit-route.test.ts new file mode 100644 index 000000000..53c84b7a1 --- /dev/null +++ b/src/bundled/agents/edit-route.test.ts @@ -0,0 +1,18 @@ +import { expect, it } from "vitest"; +import { editAgentRoute } from "./edit-route"; + +const pubkey = "a".repeat(64); +it("accepts only the exact lowercase agent identity route", () => { + expect(editAgentRoute({ pubkey })).toBe(pubkey); + for (const params of [ + null, + [], + {}, + { pubkey: pubkey.toUpperCase() }, + { pubkey, action: "save" }, + { pubkey: "not-a-key" }, + { id: pubkey }, + ]) { + expect(editAgentRoute(params)).toBeNull(); + } +}); diff --git a/src/bundled/agents/edit-route.ts b/src/bundled/agents/edit-route.ts new file mode 100644 index 000000000..808a271f4 --- /dev/null +++ b/src/bundled/agents/edit-route.ts @@ -0,0 +1,13 @@ +import type { JsonValue } from "../../features/navigation/targets"; + +/** A public identity is an address; the native snapshot supplies the exact edit record. */ +export function editAgentRoute(params: JsonValue): string | null { + if (!params || typeof params !== "object" || Array.isArray(params)) + return null; + const keys = Object.keys(params); + if (keys.length !== 1 || keys[0] !== "pubkey") return null; + const pubkey = (params as { readonly pubkey: JsonValue }).pubkey; + return typeof pubkey === "string" && /^[0-9a-f]{64}$/.test(pubkey) + ? pubkey + : null; +} diff --git a/src/bundled/agents/index.tsx b/src/bundled/agents/index.tsx index 2a7511715..d9d3e32af 100644 --- a/src/bundled/agents/index.tsx +++ b/src/bundled/agents/index.tsx @@ -1,6 +1,7 @@ import type { PluginModule } from "../../plugins/api"; import { AgentsPage } from "./AgentsPage"; -export const inject = ["pages", "relay", "agentControl"]; +import { editAgentRoute } from "./edit-route"; +export const inject = ["pages", "relay", "agentControl", "navigation"]; export const apply: PluginModule["apply"] = (ctx) => { const relay = ctx.relay; const control = ctx.agentControl; @@ -8,6 +9,18 @@ export const apply: PluginModule["apply"] = (ctx) => { id: "agents", title: "Agents", layout: "workspace", - component: () => , + handlesNavigation: true, + route: { + version: 1, + validate: (params) => editAgentRoute(params) !== null, + }, + component: (props) => ( + + ), }); }; diff --git a/src/bundled/profiles/ProfileAgentIdentity.test.tsx b/src/bundled/profiles/ProfileAgentIdentity.test.tsx index 0eeafe5e8..92f59d563 100644 --- a/src/bundled/profiles/ProfileAgentIdentity.test.tsx +++ b/src/bundled/profiles/ProfileAgentIdentity.test.tsx @@ -8,6 +8,9 @@ import { bytesToHex } from "nostr-tools/utils"; import { StrictMode } from "react"; import { afterEach, expect, it, vi } from "vitest"; import { attestedOwner } from "../../features/agents/owner-attestation"; +import { controlFixture } from "../../features/agents/control-testing"; +import { createAgentControl } from "../../features/agents/control"; +import type { Navigation } from "../../features/navigation/controller"; import { profileTarget } from "../../features/profiles/target"; import type { ReadFilter, RelayEvent } from "../../features/relay/events"; import type { LiveCallbacks } from "../../features/relay/live"; @@ -622,3 +625,80 @@ it.each(["remove", "replace", "equal-time removal"])( } }, ); + +it("offers instructions only for a signed owner with a unique native instance", async () => { + const agent = keypair(); + const ownerKey = keypair(); + const outsider = keypair(); + for (const viewer of [ownerKey, outsider]) { + const head = timedProfile(agent, [auth(agent, ownerKey)], 3); + const h = createRelaySession({ + viewer: viewer.pubkey, + relayAuthor: relayKey.pubkey, + scope: "wss://relay.example.test", + media: () => undefined, + query: async (filters) => + filters.some((filter) => filter.kinds?.includes(0)) ? [head] : [], + subscribe: () => ({ update() {}, retry() {}, dispose() {} }), + }); + owners.push(h); + const fixture = controlFixture(); + fixture.agent.pubkey = agent.pubkey; + const control = createAgentControl(fixture.host); + const snapshot = { + status: "ready" as const, + generation: 1, + viewer: viewer.pubkey, + scope: `https://relay.example.test:${viewer.pubkey}`, + session: h.session, + }; + const relay: RelayData = { + snapshot: () => snapshot, + subscribe: () => () => {}, + retry() {}, + disconnect() {}, + clearCache: async () => {}, + }; + const open = vi.fn(async () => ({ status: "opened" as const })); + const panel = render( + {}} + />, + ); + await screen.findByRole("heading", { name: "Helper" }); + await waitFor(() => + expect( + screen.getByRole("region", { name: "Linked agent instances" }), + ).toHaveTextContent("Fixture agent"), + ); + if (viewer === ownerKey) { + const button = await screen.findByRole("button", { + name: "Agent instructions", + }); + await userEvent.setup().click(button); + expect(open).toHaveBeenCalledWith({ + version: 1, + kind: "page", + pluginId: "buzz.agents", + pageId: "agents", + scope: { + viewer: viewer.pubkey, + communityOrigin: "https://relay.example.test", + }, + route: { version: 1, params: { pubkey: agent.pubkey } }, + }); + } else { + await screen.findByRole("button", { name: "View in Agents" }); + expect( + screen.queryByRole("button", { name: "Agent instructions" }), + ).toBeNull(); + expect(open).not.toHaveBeenCalled(); + } + panel.unmount(); + control.dispose(); + } +}); diff --git a/src/bundled/profiles/ProfileInstances.test.tsx b/src/bundled/profiles/ProfileInstances.test.tsx index 4161a8ed9..9dfda8641 100644 --- a/src/bundled/profiles/ProfileInstances.test.tsx +++ b/src/bundled/profiles/ProfileInstances.test.tsx @@ -11,6 +11,7 @@ import type { AgentControl, AgentControlState, } from "../../features/agents/control"; +import type { Navigation } from "../../features/navigation/controller"; import type { RelaySession } from "../../features/relay/session"; import { instanceTarget } from "../../features/profiles/instance-target"; import { ProfileInstances } from "./ProfileInstances"; @@ -42,6 +43,8 @@ function fixture() { refresh, } as unknown as AgentControl; const open = vi.fn(() => true); + const openPage = vi.fn(async () => ({ status: "opened" as const })); + const navigation = { open: openPage } as unknown as Navigation; const context = { open, canOpen: () => true, channelId: "channel" }; let archiveSnapshot = { status: "idle", archived: [] as string[] }; const archiveListeners = new Set<() => void>(); @@ -69,6 +72,8 @@ function fixture() { return { control, context, + navigation, + openPage, session, open, update, @@ -85,6 +90,7 @@ it("uses native exact identity and community, never library display links", () = { + const f = fixture(); + const props = { + control: f.control, + context: f.context, + navigation: f.navigation, + session: f.session, + pubkey: person, + viewer, + scope: `https://relay.example.test:${viewer}`, + communityOrigin: "https://relay.example.test", + knownAgent: true, + }; + const page = render(); + f.update({ + status: "ready", + busy: false, + error: null, + data: { + runtimeAvailable: true, + agents: [ + instance("one", person, "wss://relay.example.test"), + ] as NonNullable["agents"], + }, + }); + expect( + screen.queryByRole("button", { name: "Agent instructions" }), + ).toBeNull(); + page.rerender(); + expect( + screen.getByRole("button", { name: "Agent instructions" }), + ).toBeTruthy(); + f.update({ + status: "ready", + busy: false, + error: null, + data: { + runtimeAvailable: true, + agents: [ + instance("one", person, "wss://relay.example.test"), + instance("two", person, "wss://relay.example.test"), + ] as NonNullable["agents"], + }, + }); + expect( + screen.queryByRole("button", { name: "Agent instructions" }), + ).toBeNull(); + expect(f.openPage).not.toHaveBeenCalled(); +}); diff --git a/src/bundled/profiles/ProfileInstances.tsx b/src/bundled/profiles/ProfileInstances.tsx index 8c6cbad35..08bc9529a 100644 --- a/src/bundled/profiles/ProfileInstances.tsx +++ b/src/bundled/profiles/ProfileInstances.tsx @@ -1,6 +1,7 @@ import { useEffect, useSyncExternalStore } from "react"; import { sameCommunityAgents } from "../../features/agents/choices"; import type { AgentControl } from "../../features/agents/control"; +import type { Navigation } from "../../features/navigation/controller"; import type { PanelProps } from "../../features/panels/service"; import type { RelaySession } from "../../features/relay/session"; import { instanceTarget } from "../../features/profiles/instance-target"; @@ -10,6 +11,7 @@ import { Button } from "../../shared/design-system/ui/Button"; export function ProfileInstances({ control, context, + navigation, session, canOpenPrivate = false, selectedId, @@ -18,10 +20,12 @@ export function ProfileInstances({ communityOrigin, viewer, knownAgent, + owned = false, errorHandledByActions = false, }: { control: AgentControl; context?: PanelProps["context"]; + navigation?: Navigation | undefined; session: RelaySession; canOpenPrivate?: boolean; selectedId?: string | undefined; @@ -30,6 +34,7 @@ export function ProfileInstances({ communityOrigin: string | undefined; viewer: string | undefined; knownAgent: boolean; + owned?: boolean; /** The composed Info actions surface owns controller failure and recovery. */ errorHandledByActions?: boolean; }) { @@ -127,6 +132,33 @@ export function ProfileInstances({ })} )} + {owned && instances.length === 1 && navigation && viewer && ( + + )}
); } diff --git a/src/bundled/profiles/ProfilePanel.tsx b/src/bundled/profiles/ProfilePanel.tsx index 2e519af79..06064c4d8 100644 --- a/src/bundled/profiles/ProfilePanel.tsx +++ b/src/bundled/profiles/ProfilePanel.tsx @@ -329,13 +329,15 @@ function ProfileDetails({ control={control} pubkey={pubkey} context={context} + navigation={navigation} session={session} canOpenPrivate={verifiedOwner === viewer && !!viewer} selectedId={instanceId} scope={scope} communityOrigin={communityOrigin} viewer={viewer} - knownAgent={agentPubkeys.has(pubkey)} + knownAgent={knownAgent} + owned={knownAgent && !!viewer && verifiedOwner === viewer} /> )}
From 2845d3cbe63856de15b4ac7e2c38e77135dc3e47 Mon Sep 17 00:00:00 2001 From: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Date: Fri, 25 Sep 2026 04:06:47 -0700 Subject: [PATCH 2/5] fix: reconnect agents relay on routed navigation retry Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: Kalvin Chau Signed-off-by: Kalvin Chau --- src/app/entity-navigation.test.tsx | 109 ++++++++++++++++++++++++++++- src/app/navigation.ts | 4 +- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/src/app/entity-navigation.test.tsx b/src/app/entity-navigation.test.tsx index 3296d5b59..e7fcc5b70 100644 --- a/src/app/entity-navigation.test.tsx +++ b/src/app/entity-navigation.test.tsx @@ -1,6 +1,13 @@ // @vitest-environment jsdom import "@testing-library/jest-dom/vitest"; -import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; +import { + act, + cleanup, + render, + screen, + waitFor, + within, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, expect, it, vi } from "vitest"; import { finalizeEvent, getPublicKey } from "nostr-tools"; @@ -9,6 +16,14 @@ import { createServices, type AppServices } from "./services"; import { bindDeepLinks } from "../features/navigation/deep-links"; import { matchesEvent } from "../features/relay/projection"; import type { ReadFilter } from "../features/relay/events"; +import { createAgentControl } from "../features/agents/control"; +import { controlFixture } from "../features/agents/control-testing"; + +let agentFixture: ReturnType | undefined; +vi.mock("../features/agents/control-native", () => ({ + createNativeAgentControl: () => + createAgentControl(agentFixture?.host ?? null), +})); vi.mock("../bundled", async () => ({ bundledPlugins: [ @@ -16,6 +31,10 @@ vi.mock("../bundled", async () => ({ manifest: { id: "buzz.projects", name: "Projects", apiVersion: 1 }, module: await import("../bundled/projects"), }, + { + manifest: { id: "buzz.agents", name: "Agents", apiVersion: 1 }, + module: await import("../bundled/agents"), + }, ], })); const key = new Uint8Array(32).fill(6), @@ -41,6 +60,7 @@ afterEach(async () => { stop(); await services?.dispose(); services = undefined; + agentFixture = undefined; vi.unstubAllGlobals(); vi.unstubAllEnvs(); localStorage.clear(); @@ -129,3 +149,90 @@ it("keeps an OS entity intent through real community selection and Retry in App" screen.queryByRole("button", { name: "Retry navigation" }), ).not.toBeInTheDocument(); }); + +it("reconnects a failed routed Agents edit from the shell and opens its exact editor", async () => { + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + }, + ); + vi.stubEnv("VITE_BUZZ_LIVE", "1"); + agentFixture = controlFixture(); + agentFixture.agent.relayUrl = "wss://community.example"; + agentFixture.data.agents.push({ + ...structuredClone(agentFixture.agent), + id: "different-agent", + pubkey: "cd".repeat(32), + name: "Different agent", + }); + localStorage.setItem( + `buzz-client.v1:${viewer}`, + JSON.stringify({ + profile: { name: "Fixture", picture: "" }, + memberships: [{ id: origin, name: "Fixture community" }], + selected: origin, + }), + ); + let attempts = 0; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string) => { + if (url.endsWith("/identity")) return Response.json({ viewer }); + if (url.endsWith("/register")) return Response.json({}); + if (url.endsWith("/session")) { + attempts++; + return attempts === 1 + ? Response.json({ error: "relay offline" }, { status: 503 }) + : Response.json({ + viewer, + relayAuthor: viewer, + relayUrl: "wss://community.example", + }); + } + if (url.endsWith("/query")) return Response.json([]); + return Response.json({}); + }), + ); + services = createServices(); + const current = services; + const target = { + version: 1 as const, + kind: "page" as const, + pluginId: "buzz.agents", + pageId: "agents", + scope: { viewer, communityOrigin: origin }, + route: { + version: 1 as const, + params: { pubkey: agentFixture.agent.pubkey }, + }, + }; + render(); + await waitFor(() => expect(current.relay.snapshot().status).toBe("error")); + void current.navigation.open(target); + await screen.findByRole("button", { name: "Retry navigation" }); + expect(current.navigation.snapshot()).toMatchObject({ + status: "failed", + reason: "unavailable", + entry: { target }, + }); + expect(screen.queryByRole("dialog", { name: "Edit agent" })).toBeNull(); + expect(attempts).toBe(1); + + await userEvent.click( + screen.getByRole("button", { name: "Retry navigation" }), + ); + await waitFor(() => expect(current.relay.snapshot().status).toBe("ready")); + const dialog = await screen.findByRole("dialog", { name: "Edit agent" }); + expect(within(dialog).getByLabelText("Name")).toHaveValue("Fixture agent"); + expect(within(dialog).getByLabelText("Agent instructions")).toHaveValue( + "Help with the project.", + ); + expect(current.navigation.snapshot()).toMatchObject({ + status: "opened", + entry: { target }, + }); + expect(attempts).toBe(2); +}); diff --git a/src/app/navigation.ts b/src/app/navigation.ts index 63154791d..f7467d520 100644 --- a/src/app/navigation.ts +++ b/src/app/navigation.ts @@ -243,7 +243,9 @@ export function useAppNavigation(services: AppServices) { // Retrying presentation must also repair its failed dependency. Only touch the // selected, authorized destination; never reconnect an unrelated community. if ( - (pageKey === channelsKey || pageKey === "buzz.projects/projects") && + (pageKey === channelsKey || + pageKey === "buzz.projects/projects" || + pageKey === "buzz.agents/agents") && !state.ingress && !failure && !waiting From 207cc5e3f56a32c0e8e52ccdb2ad09fc225ed820 Mon Sep 17 00:00:00 2001 From: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Date: Fri, 25 Sep 2026 09:07:14 -0700 Subject: [PATCH 3/5] fix: preserve routed editor close after profile rebase Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: Kalvin Chau Signed-off-by: Kalvin Chau --- docs/profiles.md | 13 ++++++++++--- src/bundled/agents/AgentControlPanel.tsx | 6 ++++-- src/bundled/profiles/ProfileAgentIdentity.test.tsx | 2 +- src/bundled/profiles/ProfileInstances.test.tsx | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/profiles.md b/docs/profiles.md index 100095b06..f329123a8 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -22,7 +22,9 @@ The host error, runtime-unavailable reason, unconfirmed-status notice and Retry are shown once, by the profile actions. Browser/unavailable hosts, loading/error without evidence, unknown keys and records saved for another community render nothing, leaving the public identity. Community switches re-filter immediately. -Start/Stop/Restart are the separate profile actions above. The owner-only **Agent instructions** ingress in Info opens the targeted Agents-page editor for a unique native record; harness logs stay on Agents. +Start/Stop/Restart are the separate profile actions above. The owner-only +**Agent instructions** ingress in Info opens the targeted Agents-page editor +for a unique native record; harness logs stay on Agents. ## Owner runtime tab @@ -275,7 +277,10 @@ unambiguous native match; an instance view selects that record by ID. Archive labels come from `session.archives`, keyed by identity, rather than native process status. Archived rows use the same explicit target and remain navigable. The existing public profile tabs and actions are reused, not replaced by an Agents -page or a second editor. The owner-only ingress requires a verified NIP-OA owner and a ready, unique native match in this community; the versioned agent-identity route opens the existing editor, never a guessed sibling. Browser fixtures exercise live/archived selection, +page or a second editor. The owner-only ingress requires a verified NIP-OA +owner and a ready, unique native match in this community; the versioned +agent-identity route opens the existing editor, never a guessed sibling. +Browser fixtures exercise live/archived selection, tabs, back, keyboard close/focus and deletion through the real plugin/Channels host in Chromium and WebKit. React tests cover exact actions, failed-read recovery, non-owner denial and scope changes with synthetic native data. @@ -335,7 +340,9 @@ Retired relay presentations cannot dispatch commands. The separate runtime child owns badges and runtime detail; actions do not infer relay readiness. -Info adds an owner-gated route to the Agents editor for a unique native match; the owner Runtime tab also reuses the editor dialog. Mounted React regression tests exercise exact dispatch, pending/failure/ +Info adds an owner-gated route to the Agents editor for a unique native match; +the owner Runtime tab also reuses the editor dialog. Mounted React regression +tests exercise exact dispatch, pending/failure/ recovery and profile/community lifecycle through the real controller projection with a synthetic native host. Live process/credential handover and rendered native acceptance remain attended checks, not established by these tests. diff --git a/src/bundled/agents/AgentControlPanel.tsx b/src/bundled/agents/AgentControlPanel.tsx index fbc55f9ed..95e317fe5 100644 --- a/src/bundled/agents/AgentControlPanel.tsx +++ b/src/bundled/agents/AgentControlPanel.tsx @@ -233,8 +233,10 @@ export function AgentControlPanel({ displayName={label(editing)} control={control} state={state} - avatar={selected?.avatar} - onClose={() => setSelected(null)} + avatar={editTarget ? undefined : selected?.avatar} + onClose={ + editTarget ? (onCloseTarget ?? (() => {})) : () => setSelected(null) + } /> )} {deletion && control.delete && ( diff --git a/src/bundled/profiles/ProfileAgentIdentity.test.tsx b/src/bundled/profiles/ProfileAgentIdentity.test.tsx index 92f59d563..54b745901 100644 --- a/src/bundled/profiles/ProfileAgentIdentity.test.tsx +++ b/src/bundled/profiles/ProfileAgentIdentity.test.tsx @@ -692,7 +692,7 @@ it("offers instructions only for a signed owner with a unique native instance", route: { version: 1, params: { pubkey: agent.pubkey } }, }); } else { - await screen.findByRole("button", { name: "View in Agents" }); + await screen.findByRole("region", { name: "Linked agent instances" }); expect( screen.queryByRole("button", { name: "Agent instructions" }), ).toBeNull(); diff --git a/src/bundled/profiles/ProfileInstances.test.tsx b/src/bundled/profiles/ProfileInstances.test.tsx index 9dfda8641..5a15d7dd9 100644 --- a/src/bundled/profiles/ProfileInstances.test.tsx +++ b/src/bundled/profiles/ProfileInstances.test.tsx @@ -98,6 +98,7 @@ it("uses native exact identity and community, never library display links", () = scope={`https://relay.example.test:${viewer}`} communityOrigin="https://relay.example.test" knownAgent={true} + owned />, ); expect(f.refresh).toHaveBeenCalledOnce(); From c1691d7fa4073cbe63722131cc8abf5995b9830c Mon Sep 17 00:00:00 2001 From: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Date: Fri, 25 Sep 2026 09:36:12 -0700 Subject: [PATCH 4/5] fix(profiles): edit agent instructions in place Co-authored-by: Kalvin Chau Signed-off-by: Kalvin Chau --- docs/profiles.md | 10 +-- .../profiles/ProfileAgentIdentity.test.tsx | 58 ++++++++++++---- .../profiles/ProfileAgentRuntime.test.tsx | 3 +- src/bundled/profiles/ProfileAgentRuntime.tsx | 39 ++++++++--- .../profiles/ProfileInstances.test.tsx | 66 ------------------- src/bundled/profiles/ProfileInstances.tsx | 32 --------- src/bundled/profiles/ProfilePanel.tsx | 3 +- 7 files changed, 85 insertions(+), 126 deletions(-) diff --git a/docs/profiles.md b/docs/profiles.md index f329123a8..dfe955177 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -13,8 +13,10 @@ custody is app-wide: the viewer is not an owner check, so any account in this app on the same relay sees the same local record. It shows process status (still "relay readiness unverified", not a listening badge), saved harness/provider/model/workspace (labelled as saved settings, since -environment overrides may change what is launched), collapsible instructions, -saved-vs-running revision drift and diagnostics. Environment keys and +environment overrides may change what is launched), saved-vs-running revision +drift and diagnostics. Instructions are no longer displayed in the read-only +summary; the verified owner opens the existing native editor in place with +**Agent instructions**, without leaving the profile. Environment keys and arguments are not shown. Opening the Info tab requests a status read; concurrent requests coalesce. This summary adds no polling and observes the profile actions' existing refresh. A failed read keeps the last evidence. @@ -23,8 +25,8 @@ are shown once, by the profile actions. Browser/unavailable hosts, loading/error without evidence, unknown keys and records saved for another community render nothing, leaving the public identity. Community switches re-filter immediately. Start/Stop/Restart are the separate profile actions above. The owner-only -**Agent instructions** ingress in Info opens the targeted Agents-page editor -for a unique native record; harness logs stay on Agents. +**Agent instructions** ingress in Info requires a unique native record; harness +logs stay on Agents. ## Owner runtime tab diff --git a/src/bundled/profiles/ProfileAgentIdentity.test.tsx b/src/bundled/profiles/ProfileAgentIdentity.test.tsx index 54b745901..abcd56c44 100644 --- a/src/bundled/profiles/ProfileAgentIdentity.test.tsx +++ b/src/bundled/profiles/ProfileAgentIdentity.test.tsx @@ -2,7 +2,14 @@ import "@testing-library/jest-dom/vitest"; import { createHash } from "node:crypto"; import { schnorr } from "@noble/curves/secp256k1.js"; -import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; +import { + act, + cleanup, + render, + screen, + waitFor, + within, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { bytesToHex } from "nostr-tools/utils"; import { StrictMode } from "react"; @@ -679,20 +686,49 @@ it("offers instructions only for a signed owner with a unique native instance", const button = await screen.findByRole("button", { name: "Agent instructions", }); + expect(screen.queryByText("Instructions")).toBeNull(); + expect( + screen.getAllByRole("button", { name: "Agent instructions" }), + ).toHaveLength(1); await userEvent.setup().click(button); - expect(open).toHaveBeenCalledWith({ - version: 1, - kind: "page", - pluginId: "buzz.agents", - pageId: "agents", - scope: { - viewer: viewer.pubkey, - communityOrigin: "https://relay.example.test", - }, - route: { version: 1, params: { pubkey: agent.pubkey } }, + const dialog = await screen.findByRole("dialog", { name: "Edit agent" }); + expect(within(dialog).getByLabelText("Agent instructions")).toHaveValue( + "Help with the project.", + ); + expect(open).not.toHaveBeenCalled(); + await userEvent + .setup() + .clear(within(dialog).getByLabelText("Agent instructions")); + await userEvent + .setup() + .type( + within(dialog).getByLabelText("Agent instructions"), + "Updated instructions.", + ); + await userEvent + .setup() + .click(within(dialog).getByRole("button", { name: "Save changes" })); + await waitFor(() => + expect(fixture.calls.some((call) => call.action === "save")).toBe(true), + ); + expect( + fixture.calls.find((call) => call.action === "save")?.payload, + ).toMatchObject({ + id: fixture.agent.id, + expectedRevision: 1, + edit: { systemPrompt: "Updated instructions." }, }); + await userEvent + .setup() + .click(within(dialog).getByRole("button", { name: "Close editor" })); + expect(screen.queryByRole("dialog", { name: "Edit agent" })).toBeNull(); + expect( + screen.getByRole("region", { name: "Profile details" }), + ).toBeVisible(); + expect(open).not.toHaveBeenCalled(); } else { await screen.findByRole("region", { name: "Linked agent instances" }); + expect(screen.queryByText("Instructions")).toBeNull(); expect( screen.queryByRole("button", { name: "Agent instructions" }), ).toBeNull(); diff --git a/src/bundled/profiles/ProfileAgentRuntime.test.tsx b/src/bundled/profiles/ProfileAgentRuntime.test.tsx index 1dd161fcc..7f5e33b27 100644 --- a/src/bundled/profiles/ProfileAgentRuntime.test.tsx +++ b/src/bundled/profiles/ProfileAgentRuntime.test.tsx @@ -167,8 +167,7 @@ it("summarizes only the exact key in the active community, and drops it when the screen.getByRole("region", { name: "Local agent actions" }), ).toHaveTextContent("Last start exited early."); expect(summary).not.toHaveTextContent("Process stopped"); - await userEvent.click(within(summary).getByText("Instructions")); - expect(within(summary).getByText("Help with the project.")).toBeVisible(); + expect(within(summary).queryByText("Instructions")).toBeNull(); await userEvent.click(within(summary).getByText("Host diagnostics")); expect( within(summary).getByText(/spawned pid 42\s+listener exited/), diff --git a/src/bundled/profiles/ProfileAgentRuntime.tsx b/src/bundled/profiles/ProfileAgentRuntime.tsx index 8438c16c9..a65afbe4a 100644 --- a/src/bundled/profiles/ProfileAgentRuntime.tsx +++ b/src/bundled/profiles/ProfileAgentRuntime.tsx @@ -1,23 +1,28 @@ -import { useEffect, useSyncExternalStore } from "react"; +import { useEffect, useState, useSyncExternalStore } from "react"; import type { AgentControl } from "../../features/agents/control"; import { exactProfileAgent } from "../../features/profiles/instance-target"; import { agentProcessLabel } from "../agents/agent-edit"; +import { AgentEditor } from "../agents/AgentEditor"; +import { Button } from "../../shared/design-system/ui/Button"; import styles from "./Profiles.module.css"; -/** Read-only native evidence for this exact key in the active community. Anything - * else renders nothing, leaving the ordinary public profile. Errors, runtime - * availability and status recovery belong to ProfileAgentActions. */ +/** Native status and saved-settings summary for this exact key in the active + * community. The verified owner can open the existing editor in place when the + * native identity is unambiguous; the caller supplies ownership evidence. */ export function ProfileAgentRuntime({ control, scope, pubkey, instanceId, + owned = false, }: { control: AgentControl; scope: string; pubkey: string; instanceId?: string | undefined; + owned?: boolean; }) { + const [editing, setEditing] = useState(false); const state = useSyncExternalStore( control.subscribe, control.snapshot, @@ -27,6 +32,9 @@ export function ProfileAgentRuntime({ void control.refresh(); }, [control]); const data = state.data; + const uniqueAgent = data + ? exactProfileAgent(data.agents, scope, pubkey) + : undefined; const agent = data ? exactProfileAgent(data.agents, scope, pubkey, instanceId) : undefined; @@ -64,11 +72,16 @@ export function ProfileAgentRuntime({ )} - {agent.systemPrompt && ( -
- Instructions -

{agent.systemPrompt}

-
+ {owned && uniqueAgent?.id === agent.id && state.status === "ready" && ( + )} {!!agent.diagnostics.length && (
@@ -78,6 +91,14 @@ export function ProfileAgentRuntime({
)} + {editing && owned && uniqueAgent?.id === agent.id && ( + setEditing(false)} + /> + )} ); } diff --git a/src/bundled/profiles/ProfileInstances.test.tsx b/src/bundled/profiles/ProfileInstances.test.tsx index 5a15d7dd9..4161a8ed9 100644 --- a/src/bundled/profiles/ProfileInstances.test.tsx +++ b/src/bundled/profiles/ProfileInstances.test.tsx @@ -11,7 +11,6 @@ import type { AgentControl, AgentControlState, } from "../../features/agents/control"; -import type { Navigation } from "../../features/navigation/controller"; import type { RelaySession } from "../../features/relay/session"; import { instanceTarget } from "../../features/profiles/instance-target"; import { ProfileInstances } from "./ProfileInstances"; @@ -43,8 +42,6 @@ function fixture() { refresh, } as unknown as AgentControl; const open = vi.fn(() => true); - const openPage = vi.fn(async () => ({ status: "opened" as const })); - const navigation = { open: openPage } as unknown as Navigation; const context = { open, canOpen: () => true, channelId: "channel" }; let archiveSnapshot = { status: "idle", archived: [] as string[] }; const archiveListeners = new Set<() => void>(); @@ -72,8 +69,6 @@ function fixture() { return { control, context, - navigation, - openPage, session, open, update, @@ -90,7 +85,6 @@ it("uses native exact identity and community, never library display links", () = , ); expect(f.refresh).toHaveBeenCalledOnce(); @@ -121,15 +114,6 @@ it("uses native exact identity and community, never library display links", () = expect(screen.getByText("matched")).toBeTruthy(); expect(f.ensureArchives).toHaveBeenCalledOnce(); expect(screen.queryByText(/wrong-key|wrong-relay|stopped/)).toBeNull(); - fireEvent.click(screen.getByRole("button", { name: "Agent instructions" })); - expect(f.openPage).toHaveBeenCalledWith({ - version: 1, - kind: "page", - pluginId: "buzz.agents", - pageId: "agents", - scope: { viewer, communityOrigin: "https://relay.example.test" }, - route: { version: 1, params: { pubkey: person } }, - }); fireEvent.click(screen.getByRole("button", { name: "matched" })); expect(f.open).toHaveBeenCalledWith( instanceTarget({ @@ -275,53 +259,3 @@ it("does not refresh or display instances for a human without a native match", ( screen.queryByRole("region", { name: "Linked agent instances" }), ).toBeNull(); }); - -it("hides edit ingress without owner evidence or an unambiguous ready native match", () => { - const f = fixture(); - const props = { - control: f.control, - context: f.context, - navigation: f.navigation, - session: f.session, - pubkey: person, - viewer, - scope: `https://relay.example.test:${viewer}`, - communityOrigin: "https://relay.example.test", - knownAgent: true, - }; - const page = render(); - f.update({ - status: "ready", - busy: false, - error: null, - data: { - runtimeAvailable: true, - agents: [ - instance("one", person, "wss://relay.example.test"), - ] as NonNullable["agents"], - }, - }); - expect( - screen.queryByRole("button", { name: "Agent instructions" }), - ).toBeNull(); - page.rerender(); - expect( - screen.getByRole("button", { name: "Agent instructions" }), - ).toBeTruthy(); - f.update({ - status: "ready", - busy: false, - error: null, - data: { - runtimeAvailable: true, - agents: [ - instance("one", person, "wss://relay.example.test"), - instance("two", person, "wss://relay.example.test"), - ] as NonNullable["agents"], - }, - }); - expect( - screen.queryByRole("button", { name: "Agent instructions" }), - ).toBeNull(); - expect(f.openPage).not.toHaveBeenCalled(); -}); diff --git a/src/bundled/profiles/ProfileInstances.tsx b/src/bundled/profiles/ProfileInstances.tsx index 08bc9529a..8c6cbad35 100644 --- a/src/bundled/profiles/ProfileInstances.tsx +++ b/src/bundled/profiles/ProfileInstances.tsx @@ -1,7 +1,6 @@ import { useEffect, useSyncExternalStore } from "react"; import { sameCommunityAgents } from "../../features/agents/choices"; import type { AgentControl } from "../../features/agents/control"; -import type { Navigation } from "../../features/navigation/controller"; import type { PanelProps } from "../../features/panels/service"; import type { RelaySession } from "../../features/relay/session"; import { instanceTarget } from "../../features/profiles/instance-target"; @@ -11,7 +10,6 @@ import { Button } from "../../shared/design-system/ui/Button"; export function ProfileInstances({ control, context, - navigation, session, canOpenPrivate = false, selectedId, @@ -20,12 +18,10 @@ export function ProfileInstances({ communityOrigin, viewer, knownAgent, - owned = false, errorHandledByActions = false, }: { control: AgentControl; context?: PanelProps["context"]; - navigation?: Navigation | undefined; session: RelaySession; canOpenPrivate?: boolean; selectedId?: string | undefined; @@ -34,7 +30,6 @@ export function ProfileInstances({ communityOrigin: string | undefined; viewer: string | undefined; knownAgent: boolean; - owned?: boolean; /** The composed Info actions surface owns controller failure and recovery. */ errorHandledByActions?: boolean; }) { @@ -132,33 +127,6 @@ export function ProfileInstances({ })} )} - {owned && instances.length === 1 && navigation && viewer && ( - - )} ); } diff --git a/src/bundled/profiles/ProfilePanel.tsx b/src/bundled/profiles/ProfilePanel.tsx index 06064c4d8..bffd95e5c 100644 --- a/src/bundled/profiles/ProfilePanel.tsx +++ b/src/bundled/profiles/ProfilePanel.tsx @@ -315,6 +315,7 @@ function ProfileDetails({ scope={scope} pubkey={pubkey} instanceId={instanceId} + owned={isOwner} /> )} {children} @@ -329,7 +330,6 @@ function ProfileDetails({ control={control} pubkey={pubkey} context={context} - navigation={navigation} session={session} canOpenPrivate={verifiedOwner === viewer && !!viewer} selectedId={instanceId} @@ -337,7 +337,6 @@ function ProfileDetails({ communityOrigin={communityOrigin} viewer={viewer} knownAgent={knownAgent} - owned={knownAgent && !!viewer && verifiedOwner === viewer} /> )}
From 5b2bfd0d1dd5cfa355847f7f6d461912c0cdaba8 Mon Sep 17 00:00:00 2001 From: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Date: Fri, 25 Sep 2026 09:47:41 -0700 Subject: [PATCH 5/5] fix(profiles): give agent instructions button a filled affordance Co-authored-by: Kalvin Chau Signed-off-by: Kalvin Chau --- src/bundled/profiles/ProfileAgentRuntime.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundled/profiles/ProfileAgentRuntime.tsx b/src/bundled/profiles/ProfileAgentRuntime.tsx index a65afbe4a..1d02a172c 100644 --- a/src/bundled/profiles/ProfileAgentRuntime.tsx +++ b/src/bundled/profiles/ProfileAgentRuntime.tsx @@ -75,7 +75,7 @@ export function ProfileAgentRuntime({ {owned && uniqueAgent?.id === agent.id && state.status === "ready" && (