Handle hidden runtimes during Welcome kickoff

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-07-27 13:30:21 +01:00
parent 048d34c526
commit c1c262b6c5
4 changed files with 124 additions and 0 deletions
@@ -8,6 +8,7 @@ import {
buildWelcomeKickoffOpenerSendInput,
classifyWelcomeKickoffResolution,
createWelcomeKickoffCoordinator,
hasEnabledWelcomeRuntime,
mergeKickoffEvents,
resolveWelcomeAgentReadiness,
resolveWelcomeAgentSet,
@@ -56,6 +57,26 @@ test("welcome readiness ignores a hidden logged-in runtime", () => {
);
});
test("welcome provisioning requires an enabled available runtime", () => {
const runtimes = [
{
id: "goose",
label: "Goose",
availability: "available",
authStatus: { status: "not_applicable" },
},
{
id: "claude",
label: "Claude",
availability: "adapter_missing",
authStatus: { status: "unknown" },
},
];
assert.equal(hasEnabledWelcomeRuntime(runtimes, ["goose"]), false);
assert.equal(hasEnabledWelcomeRuntime(runtimes, []), true);
});
test("resolveWelcomeAgentSet orders agents by stable persona identity", () => {
assert.deepEqual(resolveWelcomeAgentSet([bumble, fizz, honey]), {
lead: fizz,
@@ -67,6 +67,15 @@ export function resolveWelcomeAgentReadiness(
);
}
export function hasEnabledWelcomeRuntime(
runtimes: readonly AcpRuntimeCatalogEntry[],
disabledRuntimeIds: readonly string[],
) {
return filterEnabledAcpRuntimes(runtimes, disabledRuntimeIds).some(
(runtime) => runtime.availability === "available",
);
}
function formatAgentNames(agents: readonly ManagedAgent[]) {
if (agents.length === 0) return "";
if (agents.length === 1) return agents[0]?.name ?? "";
@@ -579,11 +588,17 @@ export function useWelcomeKickoff(
),
[disabledRuntimeIds, globalConfig, runtimesQuery.data],
);
const canProvisionWelcomeTeam = React.useMemo(
() =>
hasEnabledWelcomeRuntime(runtimesQuery.data ?? [], disabledRuntimeIds),
[disabledRuntimeIds, runtimesQuery.data],
);
React.useEffect(() => {
if (
!channelId ||
!isActiveWelcome ||
configLoading ||
managedAgentsQuery.isPending ||
runtimesQuery.isPending
) {
return;
@@ -596,6 +611,21 @@ export function useWelcomeKickoff(
focusedWelcomeChannelRef.current !== channelId;
void (async () => {
try {
if (await markerExists(channelId, closerMarker)) {
return;
}
if (!canProvisionWelcomeTeam) {
if (!agentSet) return;
await sendManagedAgentChannelMessage({
agentPubkey: agentSet.lead.pubkey,
channelId,
content: WELCOME_KICKOFF_PROVIDER_MESSAGE,
marker: providerMarker,
markerScope: "channel",
});
return;
}
const welcomeTeam = await ensureWelcomeTeam(
channelId,
activeCommunity?.relayUrl,
@@ -715,9 +745,12 @@ export function useWelcomeKickoff(
})();
}, [
activeCommunity?.relayUrl,
agentSet,
canProvisionWelcomeTeam,
channelId,
configLoading,
isActiveWelcome,
managedAgentsQuery.isPending,
onKickoffOpenerPosted,
queryClient,
readiness,
+4
View File
@@ -75,6 +75,7 @@ type MockManagedAgentSeed = {
name: string;
avatarUrl?: string | null;
personaId?: string | null;
teamId?: string | null;
/** Harness/runtime id pin; `null` = inherit from persona (native default). */
runtime?: string | null;
status?: RawManagedAgent["status"];
@@ -714,6 +715,7 @@ type RawManagedAgent = {
pubkey: string;
name: string;
persona_id: string | null;
team_id?: string | null;
/** Record-level harness/runtime pin (`null` when inheriting from the persona). */
runtime: string | null;
relay_url: string;
@@ -1467,6 +1469,7 @@ function cloneManagedAgent(agent: MockManagedAgent): RawManagedAgent {
pubkey: agent.pubkey,
name: agent.name,
persona_id: agent.persona_id,
team_id: agent.team_id ?? null,
runtime: agent.runtime ?? null,
relay_url: agent.relay_url,
acp_command: agent.acp_command,
@@ -2001,6 +2004,7 @@ function buildSeededManagedAgent(seed: MockManagedAgentSeed): MockManagedAgent {
pubkey: seed.pubkey,
name: seed.name,
persona_id: seed.personaId ?? null,
team_id: seed.teamId ?? null,
// Native serde always emits this key (`null` when unpinned) — the bridge
// must mirror the wire shape, not omit the key.
runtime: seed.runtime ?? null,
+66
View File
@@ -2,6 +2,7 @@ import { hexToBytes } from "@noble/hashes/utils.js";
import { expect, test, type Page } from "@playwright/test";
import { nsecEncode } from "nostr-tools/nip19";
import { ACP_RUNTIME_VISIBILITY_STORAGE_KEY } from "@/features/agents/lib/runtimeVisibilityPreference";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
import { installFakeCamera } from "../helpers/fakeCamera";
import {
@@ -2726,6 +2727,71 @@ test("first-run onboarding lands before Welcome team bootstrap completes", async
expect(await commandCount(page, "create_managed_agent")).toBe(3);
});
test("first-run onboarding reuses the Welcome team when every harness is hidden", async ({
page,
}) => {
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
await page.addInitScript((storageKey) => {
window.localStorage.setItem(storageKey, JSON.stringify(["goose"]));
}, ACP_RUNTIME_VISIBILITY_STORAGE_KEY);
await installMockBridge(
page,
{
acpRuntimesCatalog: [
{
id: "goose",
label: "Goose",
avatar_url: "",
availability: "available",
command: "goose",
binary_path: "/usr/local/bin/goose",
default_args: ["acp"],
mcp_command: null,
install_hint: "Install Goose",
install_instructions_url: "https://block.github.io/goose/",
can_auto_install: true,
requires_external_cli: true,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
login_hint: null,
},
],
managedAgents: [
{
pubkey: "f".repeat(64),
name: "Fizz",
personaId: "builtin:fizz",
teamId: "builtin-team:welcome",
},
{
pubkey: "e".repeat(64),
name: "Honey",
personaId: "builtin:honey",
teamId: "builtin-team:welcome",
},
{
pubkey: "d".repeat(64),
name: "Bumble",
personaId: "builtin:bumble",
teamId: "builtin-team:welcome",
},
],
},
{ skipOnboardingSeed: true },
);
await page.goto("/");
await page.getByTestId("onboarding-display-name").fill("Morty QA");
await completeProfileOnboarding(page);
await expectPrivateWelcomeLanding(page);
await expect(page.getByTestId("message-timeline")).toContainText(
"connect to an AI provider in Settings",
);
expect(await commandCount(page, "create_managed_agent")).toBe(0);
});
test("existing relay profile with display name auto-skips onboarding without localStorage", async ({
page,
}) => {