From 7b3513a34a547bdf37ed90f0561622c35d738c20 Mon Sep 17 00:00:00 2001 From: Tyler <109685178+tlongwell-block@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:47:32 -0400 Subject: [PATCH 01/12] feat(desktop): allow agents to manage relay profiles (#2009) Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> --- desktop/src-tauri/src/app_state.rs | 5 +++++ desktop/src-tauri/src/commands/agent_settings.rs | 10 +++++++++- desktop/src-tauri/src/commands/agents.rs | 6 +++++- desktop/src-tauri/src/commands/agents_profile.rs | 14 ++++++++++++++ desktop/src-tauri/src/commands/workspace.rs | 8 ++++++++ desktop/src-tauri/src/lib.rs | 1 + .../src/features/communities/useCommunityInit.ts | 2 ++ .../settings/ui/ExperimentalFeaturesCard.tsx | 13 ++++++++++++- desktop/src/shared/api/tauri.ts | 5 +++++ desktop/src/testing/e2eBridge.ts | 2 ++ preview-features.json | 8 ++++++++ 11 files changed, 71 insertions(+), 3 deletions(-) diff --git a/desktop/src-tauri/src/app_state.rs b/desktop/src-tauri/src/app_state.rs index 0246c0a618..c7eda559e4 100644 --- a/desktop/src-tauri/src/app_state.rs +++ b/desktop/src-tauri/src/app_state.rs @@ -25,6 +25,10 @@ pub struct AppState { /// restore. `apply_workspace` consumes it after installing the workspace /// relay and identity, so agents never start against the fallback relay. pub managed_agent_restore_pending: AtomicBool, + /// Whether desktop may repair managed-agent kind:0 profiles from its local + /// records. Disabled by the agent-managed profiles experiment so an agent's + /// own profile updates are not overwritten on start or restore. + pub managed_agent_profile_reconcile_enabled: AtomicBool, /// Shared shutdown signal checked by launch-time agent restoration. pub shutdown_started: AtomicBool, /// Serializes the restore spawn/register transition with shutdown cleanup, @@ -161,6 +165,7 @@ pub fn build_app_state() -> AppState { .unwrap_or_else(|_| reqwest::Client::new()), relay_url_override: Mutex::new(None), managed_agent_restore_pending: AtomicBool::new(false), + managed_agent_profile_reconcile_enabled: AtomicBool::new(true), shutdown_started: AtomicBool::new(false), managed_agent_restore_transition: Mutex::new(()), identity_mutation: Mutex::new(()), diff --git a/desktop/src-tauri/src/commands/agent_settings.rs b/desktop/src-tauri/src/commands/agent_settings.rs index 54b52bd197..b654158401 100644 --- a/desktop/src-tauri/src/commands/agent_settings.rs +++ b/desktop/src-tauri/src/commands/agent_settings.rs @@ -1,4 +1,5 @@ -use tauri::{AppHandle, Manager}; +use std::sync::atomic::Ordering; +use tauri::{AppHandle, Manager, State}; use crate::{ app_state::AppState, @@ -10,6 +11,13 @@ use crate::{ util::now_iso, }; +#[tauri::command] +pub fn set_agent_managed_profiles(enabled: bool, state: State<'_, AppState>) { + state + .managed_agent_profile_reconcile_enabled + .store(!enabled, Ordering::Release); +} + #[tauri::command] pub async fn set_managed_agent_start_on_app_launch( pubkey: String, diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 1718d54de2..6ebe6b7c4a 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -991,7 +991,11 @@ pub async fn start_managed_agent( // profile is published on the relay. This self-heals cases where the initial // profile sync at creation time failed silently. For legacy records (pre-PR-921) // with no persisted avatar, this also backfills the avatar from the relay. - if result.is_ok() { + if result.is_ok() + && state + .managed_agent_profile_reconcile_enabled + .load(std::sync::atomic::Ordering::Acquire) + { let reconcile_pubkey = pubkey.clone(); let reconcile_app = app.clone(); tauri::async_runtime::spawn(async move { diff --git a/desktop/src-tauri/src/commands/agents_profile.rs b/desktop/src-tauri/src/commands/agents_profile.rs index 236fd9f4cc..0675d4c48f 100644 --- a/desktop/src-tauri/src/commands/agents_profile.rs +++ b/desktop/src-tauri/src/commands/agents_profile.rs @@ -81,6 +81,13 @@ pub(crate) async fn reconcile_agent_profile( &relay_ws_url_with_override(state), ); + if !state + .managed_agent_profile_reconcile_enabled + .load(std::sync::atomic::Ordering::Acquire) + { + return Ok(()); + } + // Query the relay for the agent's existing kind:0 profile. let existing = query_agent_profile(state, &relay_url, agent_pubkey).await?; @@ -136,6 +143,13 @@ pub(crate) async fn reconcile_agent_profile( let agent_keys = Keys::parse(&data.private_key_nsec) .map_err(|e| format!("failed to parse agent keys: {e}"))?; + if !state + .managed_agent_profile_reconcile_enabled + .load(std::sync::atomic::Ordering::Acquire) + { + return Ok(()); + } + sync_managed_agent_profile( state, &relay_url, diff --git a/desktop/src-tauri/src/commands/workspace.rs b/desktop/src-tauri/src/commands/workspace.rs index c602bdce42..6bd28bc7ec 100644 --- a/desktop/src-tauri/src/commands/workspace.rs +++ b/desktop/src-tauri/src/commands/workspace.rs @@ -103,6 +103,7 @@ pub async fn apply_workspace( relay_url: String, nsec: Option, repos_dir: Option, + agent_managed_profiles: Option, app: AppHandle, ) -> Result<(), String> { let restore_app = app.clone(); @@ -148,6 +149,13 @@ pub async fn apply_workspace( *keys_guard = keys; } + // Keep the backend-side reconcile guard aligned with the frontend + // experiment before launch-time restore can spawn any agents. Missing + // means the stable behavior: desktop remains authoritative. + state + .managed_agent_profile_reconcile_enabled + .store(!agent_managed_profiles.unwrap_or(false), Ordering::Release); + // ── Filesystem side-effect (non-fatal) ──────────────────────────────── // Persist the *effective* repos_dir (None when the candidate failed // validation) for the backend to read at boot, then re-point REPOS to diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 9a4eb3dcb1..276f8d14d6 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -847,6 +847,7 @@ pub fn run() { create_managed_agent, start_managed_agent, stop_managed_agent, + set_agent_managed_profiles, set_managed_agent_start_on_app_launch, set_managed_agent_auto_restart, delete_managed_agent, diff --git a/desktop/src/features/communities/useCommunityInit.ts b/desktop/src/features/communities/useCommunityInit.ts index a82d381714..dc2452b507 100644 --- a/desktop/src/features/communities/useCommunityInit.ts +++ b/desktop/src/features/communities/useCommunityInit.ts @@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react"; import { relayClient } from "@/shared/api/relayClient"; import { applyCommunity, getDefaultRelayUrl } from "@/shared/api/tauri"; import { getIdentity } from "@/shared/api/tauriIdentity"; +import { getOverrides } from "@/shared/features"; import { resetMediaCaches } from "@/shared/lib/mediaUrl"; import { clearSearchHitEventCache } from "@/app/navigation/searchHitEventCache"; import { initDraftStore } from "@/features/messages/lib/useDrafts"; @@ -157,6 +158,7 @@ export function useCommunityInit( undefined, activeCommunity.token, activeCommunity.reposDir, + getOverrides().agentManagedProfiles === true, ); } catch (error) { // A bad `repos_dir` no longer reaches here — `apply_workspace` treats diff --git a/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx b/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx index 4684829d32..5476f151d0 100644 --- a/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx +++ b/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx @@ -1,3 +1,4 @@ +import { setAgentManagedProfiles } from "@/shared/api/tauri"; import { desktopFeatures, useFeatureToggle } from "@/shared/features"; import type { FeatureDefinition } from "@/shared/features"; import { Switch } from "@/shared/ui/switch"; @@ -19,7 +20,17 @@ function FeatureRow({ feature }: { feature: FeatureDefinition }) { aria-labelledby={`${switchId}-label`} checked={enabled} data-testid={switchId} - onCheckedChange={toggle} + onCheckedChange={(value) => { + toggle(value); + if (feature.id === "agentManagedProfiles") { + void setAgentManagedProfiles(value).catch((error) => { + console.error( + "Failed to apply agent-managed profiles setting:", + error, + ); + }); + } + }} /> ); diff --git a/desktop/src/shared/api/tauri.ts b/desktop/src/shared/api/tauri.ts index 4c6be8eab5..9082db841d 100644 --- a/desktop/src/shared/api/tauri.ts +++ b/desktop/src/shared/api/tauri.ts @@ -1061,12 +1061,14 @@ export async function applyCommunity( nsec?: string, token?: string, reposDir?: string, + agentManagedProfiles?: boolean, ): Promise { await invokeTauri("apply_workspace", { relayUrl, nsec: nsec ?? null, token: token ?? null, reposDir: reposDir ?? null, + agentManagedProfiles: agentManagedProfiles ?? false, }); } @@ -1079,6 +1081,9 @@ export async function validateReposDir(dir: string): Promise { export const setPreventSleepActive = (active: boolean) => invokeTauri("set_prevent_sleep_active", { active }); +export const setAgentManagedProfiles = (enabled: boolean) => + invokeTauri("set_agent_managed_profiles", { enabled }); + /** Returns true on macOS, Windows, and Linux AppImage installs. * Returns false on Linux non-AppImage packages (e.g. .deb) where * Tauri's updater cannot swap the binary. */ diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index a2f9263fbb..08d4205094 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -9267,6 +9267,8 @@ export function maybeInstallE2eTauriMocks() { return handleStopManagedAgent( payload as Parameters[0], ); + case "set_agent_managed_profiles": + return undefined; case "set_managed_agent_auto_restart": return handleSetManagedAgentAutoRestart( payload as Parameters[0], diff --git a/preview-features.json b/preview-features.json index 38ea181bb3..ad8090d5ad 100644 --- a/preview-features.json +++ b/preview-features.json @@ -32,6 +32,14 @@ "platforms": [ "desktop" ] + }, + { + "id": "agentManagedProfiles", + "name": "Agent-managed profiles", + "description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy", + "platforms": [ + "desktop" + ] } ] } From 64c63daf028e92809ea704b52168b46ad2f83d06 Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Thu, 16 Jul 2026 17:50:03 -0700 Subject: [PATCH 02/12] chore(release): release Buzz Desktop version 0.4.11 (#2010) Signed-off-by: npub1k766u8wq088nkr9vy94uqlh9kmw5956g593a7xk7cks7frhgctmsr00p27 Co-authored-by: npub1k766u8wq088nkr9vy94uqlh9kmw5956g593a7xk7cks7frhgctmsr00p27 --- .intersect/sadscan.yaml | 4 ++++ CHANGELOG.md | 8 ++++++++ desktop/package.json | 2 +- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- 6 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 .intersect/sadscan.yaml diff --git a/.intersect/sadscan.yaml b/.intersect/sadscan.yaml new file mode 100644 index 0000000000..a321714bcb --- /dev/null +++ b/.intersect/sadscan.yaml @@ -0,0 +1,4 @@ +# Cargo registry checksums are integrity hashes generated by Cargo, not payment data. +exclude_rules_for_files: + sq.pii.cc.visa: + - Cargo.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b924d7135..209757f8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v0.4.11 + +- Fix policy receipts for cold-launch invite links ([#2005](https://github.com/block/buzz/pull/2005)) ([`1d8d006a`](https://github.com/block/buzz/commit/1d8d006aaf078ffd5b92eae06334b7bb908b680e)) +- Preserve persistent agent audience order ([#1989](https://github.com/block/buzz/pull/1989)) ([`fb8e6827`](https://github.com/block/buzz/commit/fb8e6827c758c73c9c6e16965dcdcecd00a5827c)) +- feat(desktop): create channels in sections ([#1996](https://github.com/block/buzz/pull/1996)) ([`9b649740`](https://github.com/block/buzz/commit/9b6497402f343e380f6c79b97a01c2d52bd12f17)) +- feat(desktop): graduate community rail ([#1995](https://github.com/block/buzz/pull/1995)) ([`f06b59ff`](https://github.com/block/buzz/commit/f06b59ff13bef0c031c8e4c3bdf979e120ea76d5)) + + ## v0.4.10 - feat(desktop): onboarding step dots, identity/key page restyle, overflow scroll ([#1993](https://github.com/block/buzz/pull/1993)) ([`f2522995c`](https://github.com/block/buzz/commit/f2522995cbea67c96b79d5de6c19c665e465027d)) diff --git a/desktop/package.json b/desktop/package.json index 0d5dca3fbe..faf7748f23 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "buzz", "private": true, - "version": "0.4.10", + "version": "0.4.11", "type": "module", "scripts": { "dev": "vite", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index c59ba28343..6474b0f6ea 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -972,7 +972,7 @@ dependencies = [ [[package]] name = "buzz-desktop" -version = "0.4.10" +version = "0.4.11" dependencies = [ "anyhow", "arboard", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index cf8bb280ec..15492f44db 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "buzz-desktop" -version = "0.4.10" +version = "0.4.11" description = "Buzz desktop app" authors = ["you"] edition = "2021" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 5ecfa89e94..6a59d07454 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Buzz", - "version": "0.4.10", + "version": "0.4.11", "identifier": "xyz.block.buzz.app", "build": { "beforeDevCommand": { From f054df7366c45a9c5614f7b375e3eecf637e4e68 Mon Sep 17 00:00:00 2001 From: Wes Date: Thu, 16 Jul 2026 20:49:45 -0600 Subject: [PATCH 03/12] feat(desktop): reskin provider, config, community, profile, and team onboarding pages (#2003) Signed-off-by: Wes Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> --- .../agents/ui/GlobalAgentConfigFields.tsx | 82 +++- .../features/communities/ui/WelcomeSetup.tsx | 52 ++- .../onboarding/ui/CommunityOnboardingFlow.tsx | 207 +++++++--- .../onboarding/ui/DefaultConfigStep.tsx | 177 +++++++++ .../onboarding/ui/MachineOnboardingFlow.tsx | 19 +- .../onboarding/ui/OnboardingStepDots.tsx | 6 +- .../src/features/onboarding/ui/SetupStep.tsx | 373 ++++++------------ desktop/src/features/onboarding/ui/types.ts | 5 + .../e2e/onboarding-agent-defaults.spec.ts | 115 ++++-- 9 files changed, 647 insertions(+), 389 deletions(-) create mode 100644 desktop/src/features/onboarding/ui/DefaultConfigStep.tsx diff --git a/desktop/src/features/agents/ui/GlobalAgentConfigFields.tsx b/desktop/src/features/agents/ui/GlobalAgentConfigFields.tsx index a5d2335ac7..0b9dcc5aa6 100644 --- a/desktop/src/features/agents/ui/GlobalAgentConfigFields.tsx +++ b/desktop/src/features/agents/ui/GlobalAgentConfigFields.tsx @@ -23,8 +23,11 @@ import { BLOCK_BUILD_HIDDEN_PROVIDER_IDS, CUSTOM_PROVIDER_DROPDOWN_VALUE, getPersonaProviderOptions, + getProviderApiKeyEnvVar, + requiredCredentialEnvKeys, } from "@/features/agents/ui/personaDialogPickers"; import { AgentModelField } from "@/features/agents/ui/personaProviderModelFields"; +import { PersonaProviderApiKeyField } from "@/features/agents/ui/PersonaProviderApiKeyField"; import { usePersonaModelDiscovery } from "@/features/agents/ui/usePersonaModelDiscovery"; import { BUZZ_AGENT_THINKING_EFFORT, @@ -100,6 +103,25 @@ export function GlobalAgentConfigFields({ const providerValue = config.provider ?? ""; const providerForDiscovery = isCustomProvider ? "" : providerValue; + const credentialProvider = isCustomProvider ? "" : effectiveProvider; + const requiredEnvKeys = requiredCredentialEnvKeys( + "buzz-agent", + credentialProvider, + ); + const apiKeyEnvVar = getProviderApiKeyEnvVar(credentialProvider); + const advancedRequiredEnvKeys = requiredEnvKeys.filter( + (key) => + key !== apiKeyEnvVar && !bakedEnv.some((entry) => entry.key === key), + ); + const apiKeyValue = apiKeyEnvVar ? (config.env_vars[apiKeyEnvVar] ?? "") : ""; + const bakedEnvKeys = React.useMemo( + () => bakedEnv.map((entry) => entry.key), + [bakedEnv], + ); + const apiKeyInherited = + apiKeyEnvVar !== null && + apiKeyValue.length === 0 && + bakedEnvKeys.includes(apiKeyEnvVar); const { discoveredModelOptions, @@ -131,21 +153,32 @@ export function GlobalAgentConfigFields({ }); function handleProviderChange(value: string) { + const previousApiKey = getProviderApiKeyEnvVar(effectiveProvider); if (value === CUSTOM_PROVIDER_DROPDOWN_VALUE) { + const nextEnvVars = { ...config.env_vars }; + if (previousApiKey) delete nextEnvVars[previousApiKey]; onIsCustomProviderChange(true); + onConfigChange({ ...config, env_vars: nextEnvVars, provider: null }); return; } - if (value === AUTO_PROVIDER_DROPDOWN_VALUE || value === "") { - onIsCustomProviderChange(false); - onConfigChange({ ...config, provider: null }); - } else { - onIsCustomProviderChange(false); - onConfigChange({ - ...config, - provider: value, - model: value === "relay-mesh" ? config.model || "auto" : config.model, - }); + const nextProvider = + value === AUTO_PROVIDER_DROPDOWN_VALUE || value === "" ? null : value; + const nextApiKey = getProviderApiKeyEnvVar( + nextProvider ?? bakedProvider ?? "", + ); + const nextEnvVars = { ...config.env_vars }; + if (previousApiKey && previousApiKey !== nextApiKey) { + delete nextEnvVars[previousApiKey]; } + + onIsCustomProviderChange(false); + onConfigChange({ + ...config, + env_vars: nextEnvVars, + provider: nextProvider, + model: + nextProvider === "relay-mesh" ? config.model || "auto" : config.model, + }); } function handleCustomProviderInput(value: string) { @@ -168,10 +201,6 @@ export function GlobalAgentConfigFields({ onConfigChange({ ...config, env_vars: merged }); } - const bakedEnvKeys = React.useMemo( - () => bakedEnv.map((e) => e.key), - [bakedEnv], - ); // On internal Block builds, BUZZ_AGENT_PROVIDER is baked in and a boot // migration rewrites v1→v2. Hide the legacy v1 option so it is not offered // for new selections; OSS builds show it. @@ -237,6 +266,29 @@ export function GlobalAgentConfigFields({

+ {apiKeyEnvVar ? ( +
+ + onConfigChange({ + ...config, + env_vars: { ...config.env_vars, [apiKeyEnvVar]: value }, + }) + } + value={apiKeyValue} + /> +
+ ) : null} + {/* Model field */}
k !== BUZZ_AGENT_THINKING_EFFORT, diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx index c911a629e7..988a87eb87 100644 --- a/desktop/src/features/communities/ui/WelcomeSetup.tsx +++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx @@ -13,6 +13,7 @@ import { pubkeyToNpub } from "@/shared/lib/nostrUtils"; import { Button } from "@/shared/ui/button"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme"; +import { OnboardingStepDots } from "@/features/onboarding/ui/OnboardingStepDots"; type WelcomeSetupPage = "welcome" | "join" | "invite"; type WelcomeTransitionMode = "initial" | OnboardingTransitionDirection; @@ -94,7 +95,8 @@ export function WelcomeSetup({ data-system-color-scheme={systemColorScheme} > -
+ +
{page === "welcome" ? ( - Buzz -

- Welcome to Buzz -

-

- Choose how you want to get started. -

-
+
+

+ Join or create a community +

+

+ Request access to an existing community or create a new one. +

+
+
{isLocalDevRelayUrl(defaultRelayUrl) ? null : ( - + )} - - - +
) : page === "join" ? ( diff --git a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx index a48926902e..a6fe5f84a0 100644 --- a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { useQueryClient } from "@tanstack/react-query"; -import { Bot, Users } from "lucide-react"; +import { Plus, Users, X } from "lucide-react"; import { markCommunityOnboardingComplete, @@ -8,15 +8,69 @@ import { } from "@/features/onboarding/communityOnboarding"; import { initializeStarterChannels } from "@/features/onboarding/hooks"; import { useClaimInvite } from "@/features/onboarding/useClaimInvite"; -import { AvatarUpload } from "@/features/profile/ui/AvatarUpload"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; +import { + parseEmojiAvatarDataUrl, + ProfileAvatarEditor, +} from "@/features/profile/ui/ProfileAvatarEditor"; import { updateProfile } from "@/shared/api/tauriProfiles"; import { getIdentity } from "@/shared/api/tauriIdentity"; import { listPersonas } from "@/shared/api/tauriPersonas"; import type { AgentPersona } from "@/shared/api/types"; +import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; +import { OnboardingStepDots } from "./OnboardingStepDots"; + +const NEUTRAL_EMOJI_PICKER_THEME_VARS = { + "--buzz-emoji-picker-rgb-background": + "var(--buzz-onboarding-emoji-picker-background)", + "--buzz-emoji-picker-rgb-color": "var(--buzz-onboarding-emoji-picker-color)", + "--buzz-emoji-picker-rgb-input": "var(--buzz-onboarding-emoji-picker-input)", +} as React.CSSProperties; + +function AvatarCircle({ + avatarUrl, + onClick, + previewName, +}: { + avatarUrl: string; + onClick: () => void; + previewName: string; +}) { + const emojiAvatar = parseEmojiAvatarDataUrl(avatarUrl); + const hasAvatar = avatarUrl.trim().length > 0; + + return ( + + ); +} export function CommunityOnboardingFlow({ onConnect, @@ -28,6 +82,7 @@ export function CommunityOnboardingFlow({ const [displayName, setDisplayName] = React.useState(""); const [avatarUrl, setAvatarUrl] = React.useState(""); const [isUploadingAvatar, setIsUploadingAvatar] = React.useState(false); + const [isAvatarEditorOpen, setIsAvatarEditorOpen] = React.useState(false); const [starterPersonas, setStarterPersonas] = React.useState( [], ); @@ -102,13 +157,25 @@ export function CommunityOnboardingFlow({ } }; + const isProfileStage = transaction.stage === "profile"; + const isTeamStage = + transaction.stage === "team-intro" || transaction.stage === "finalizing"; + return (
-
+ {isProfileStage || isTeamStage ? ( + + ) : null} +
{transaction.stage === "claiming" || transaction.stage === "connecting" ? ( <> @@ -116,7 +183,7 @@ export function CommunityOnboardingFlow({

Joining {transaction.communityName}

-

+

{transaction.error ?? (transaction.stage === "claiming" ? "Accepting your invite…" @@ -124,74 +191,113 @@ export function CommunityOnboardingFlow({

{transaction.error ? ( - + ) : null} -
- ) : transaction.stage === "profile" ? ( - <> -

- How should you appear here? -

-

- Your name and avatar are specific to {transaction.communityName}. -

-
- setDisplayName(event.target.value)} - placeholder="Your name" - value={displayName} - /> - + + setAvatarUrl("")} + emojiPickerTheme="auto" + emojiPickerThemeVars={NEUTRAL_EMOJI_PICKER_THEME_VARS} + onDone={() => setIsAvatarEditorOpen(false)} onUploadingChange={setIsUploadingAvatar} onUrlChange={setAvatarUrl} previewName={displayName.trim() || "Your profile"} - showClear={avatarUrl.length > 0} testIdPrefix="community-avatar" /> - {transaction.error ? ( -

{transaction.error}

- ) : null} -
- + ) : ( + <> +

Build your profile

+

+ Add a name and avatar. They’ll show up on your messages, + reactions, and agent handoffs. +

+
+ setIsAvatarEditorOpen(true)} + previewName={displayName.trim() || "Your profile"} + /> +
+
+ + setDisplayName(event.target.value)} + placeholder="First and last name" + value={displayName} + /> +
+ {transaction.error ? ( +

+ {transaction.error} +

+ ) : null} +
+ +
+ + ) ) : ( <> - -

- Meet your starter team -

-

- Fizz helps you build, Honey helps you communicate, and Bumble - helps you research. They’ll be ready when you need them. +

Meet your starter team

+

+ Buzz lets you bring multiple agents into the same workspace. This + team will help you get started using Buzz.

{starterPersonas.length > 0 ? ( -
+
{starterPersonas.map((persona) => (
- + {persona.displayName}
@@ -203,9 +309,9 @@ export function CommunityOnboardingFlow({ {transaction.error}

) : null} -
+
{transaction.error ? ( + + +
+ + ); +} diff --git a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx index 35243f8496..8bfec109bb 100644 --- a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx @@ -9,13 +9,14 @@ import { import { Button } from "@/shared/ui/button"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; import { BackupStep } from "./BackupStep"; +import { DefaultConfigStep } from "./DefaultConfigStep"; import { LandingBees } from "./LandingBees"; import { NostrKeyImportForm } from "./NostrKeyImportForm"; import { OnboardingSlideTransition } from "./OnboardingSlideTransition"; import { OnboardingStepDots } from "./OnboardingStepDots"; import { SetupStep } from "./SetupStep"; -type MachinePage = "identity" | "key-import" | "backup" | "setup"; +type MachinePage = "identity" | "key-import" | "backup" | "setup" | "config"; export function MachineOnboardingFlow({ complete, @@ -89,14 +90,16 @@ export function MachineOnboardingFlow({ return (
{page === "identity" ? : null} {page !== "identity" ? ( - + ) : null}
{page === "identity" ? ( @@ -168,11 +171,19 @@ export function MachineOnboardingFlow({ onBack={() => setPage("identity")} onNext={() => setPage("setup")} /> - ) : ( + ) : page === "setup" ? ( setPage(identityWasImported ? "key-import" : "backup"), + next: () => setPage("config"), + }} + direction="forward" + /> + ) : ( + setPage("setup"), complete: () => complete(selectedPubkey ?? undefined), }} direction="forward" diff --git a/desktop/src/features/onboarding/ui/OnboardingStepDots.tsx b/desktop/src/features/onboarding/ui/OnboardingStepDots.tsx index b970eb5610..5e3a8551b9 100644 --- a/desktop/src/features/onboarding/ui/OnboardingStepDots.tsx +++ b/desktop/src/features/onboarding/ui/OnboardingStepDots.tsx @@ -1,10 +1,10 @@ import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; /** - * Positions in the first-launch flow: landing, identity/key, provider setup, - * community choice, community profile, meet the team. + * Positions in the first-launch flow: landing, identity/key, harness setup, + * default config, community choice, community profile, meet the team. */ -export const TOTAL_ONBOARDING_PAGES = 6; +export const TOTAL_ONBOARDING_PAGES = 7; /** * Top-left paging indicator for the first-launch flow. The bee marks the diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx index f99feff3fc..ebd08816a1 100644 --- a/desktop/src/features/onboarding/ui/SetupStep.tsx +++ b/desktop/src/features/onboarding/ui/SetupStep.tsx @@ -4,8 +4,8 @@ import { AlertTriangle, Check, ExternalLink, + Info, Plus, - RefreshCw, TerminalSquare, } from "lucide-react"; @@ -15,32 +15,19 @@ import { useGitBashPrerequisiteQuery, } from "@/features/agents/hooks"; import { describeResolvedCommand } from "@/features/agents/ui/agentUi"; -import { - GlobalAgentConfigFields, - EMPTY_GLOBAL_CONFIG, -} from "@/features/agents/ui/GlobalAgentConfigFields"; -import { createSaveCoalescer } from "./saveCoalescer"; -import { getBakedBuildEnv, type BakedEnvEntry } from "@/shared/api/tauri"; -import { - getGlobalAgentConfig, - setGlobalAgentConfig, -} from "@/shared/api/tauriGlobalAgentConfig"; -import type { - AcpRuntimeCatalogEntry, - GlobalAgentConfig, -} from "@/shared/api/types"; +import type { AcpRuntimeCatalogEntry } from "@/shared/api/types"; import { getInstallErrorMessage } from "@/shared/lib/installError"; import { cn } from "@/shared/lib/cn"; import { useTheme } from "@/shared/theme/ThemeProvider"; import { Badge } from "@/shared/ui/badge"; import { Button } from "@/shared/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { Spinner } from "@/shared/ui/spinner"; import { type OnboardingTransitionDirection, OnboardingSlideTransition, } from "./OnboardingSlideTransition"; import type { SetupStepActions, SetupStepState } from "./types"; -import { resolveAgentReadiness } from "./agentReadiness"; type SetupStepProps = { actions: SetupStepActions; @@ -58,150 +45,6 @@ type InstallResultState = { success: boolean; }; -function AgentDefaultsSection() { - const runtimesQuery = useAcpRuntimesQuery(); - const [config, setConfig] = - React.useState(EMPTY_GLOBAL_CONFIG); - const [isLoading, setIsLoading] = React.useState(true); - const [isCustomProvider, setIsCustomProvider] = React.useState(false); - const [isCustomModelEditing, setIsCustomModelEditing] = React.useState(false); - const [bakedEnv, setBakedEnv] = React.useState([]); - const coalescerRef = React.useRef<{ - enqueue: (value: GlobalAgentConfig) => void; - cancel: () => void; - } | null>(null); - - React.useEffect(() => { - let unmounted = false; - - getGlobalAgentConfig() - .then((loaded) => { - if (!unmounted) { - setConfig(loaded); - setIsLoading(false); - } - }) - .catch(() => { - if (!unmounted) setIsLoading(false); - }); - getBakedBuildEnv() - .then((env) => { - if (!unmounted) setBakedEnv(env); - }) - .catch(() => undefined); - - // The coalescer serializes autosaves and drains any edit that arrived - // while a previous save was in flight. Cancel on unmount so a slow - // in-flight request never calls setState on an unmounted component. - const coalescer = createSaveCoalescer( - // set_global_agent_config returns a save result (config + restart - // counts); the coalescer round-trips the persisted config only. - async (next) => (await setGlobalAgentConfig(next)).config, - () => undefined, // saving state not surfaced in this autosave UX - (saved) => { - if (!unmounted) setConfig(saved); - }, - ); - coalescerRef.current = coalescer; - - return () => { - unmounted = true; - coalescer.cancel(); - }; - }, []); - - const buzzAgentRuntime = React.useMemo( - () => (runtimesQuery.data ?? []).find((r) => r.id === "buzz-agent"), - [runtimesQuery.data], - ); - - const readiness = resolveAgentReadiness(runtimesQuery.data ?? [], config); - - return ( -
-
-
-

- Agent defaults -

-

- Configure the LLM provider and credentials that buzz-agent uses, or - connect a CLI harness like Claude or Goose above. -

-
- -
- {readiness.ready ? ( - - {readiness.reason === "cli" - ? `${readiness.runtimeLabel} ready` - : "buzz-agent configured"} - - ) : ( - - Not configured - - )} - -
-
- - {isLoading ? ( -
- - Loading… -
- ) : ( - { - // Always apply optimistically so the UI never reverts mid-save, - // then enqueue the persist — the coalescer serialises multiple - // rapid edits into a single trailing request. - setConfig(next); - coalescerRef.current?.enqueue(next); - }} - onCustomModelEditingChange={setIsCustomModelEditing} - onIsCustomProviderChange={setIsCustomProvider} - /> - )} - - {!readiness.ready ? ( -

- You can finish now and configure agents later in Settings. -

- ) : null} -
- ); -} - function useSetupStepState(): SetupStepState { const runtimesQuery = useAcpRuntimesQuery(); const items = runtimesQuery.data ?? []; @@ -225,25 +68,24 @@ function RuntimeIcon({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { if (runtime.avatarUrl && !imageFailed) { return ( -
- setImageFailed(true)} - src={runtime.avatarUrl} - /> -
+ setImageFailed(true)} + src={runtime.avatarUrl} + /> ); } return ( -
- -
+ ); } @@ -264,7 +106,7 @@ function RuntimeStatus({ return (
@@ -274,7 +116,7 @@ function RuntimeStatus({ if (installError) { return ( -
+
); @@ -282,8 +124,15 @@ function RuntimeStatus({ if (runtime.availability === "available" || installSuccess) { return ( -
- +
+
); } @@ -292,7 +141,7 @@ function RuntimeStatus({ return ( + + + + + +
+
-
-

- {runtime.label} -

- {isAvailable ? ( - - Installed - - ) : null} -
- - - +

+ {runtime.label} +

+ {!isAvailable && !installError ? ( +

+ {runtimeDetailText(runtime)} +

+ ) : null} {installError ? ( -

+

{installError}

) : null} - {installSuccess && runtime.availability !== "available" ? ( -

- Installed successfully. You can finish onboarding now. -

+

Installed

) : null}
- -
); } @@ -483,10 +366,10 @@ function GitBashPrerequisiteCard() { return (
@@ -572,23 +455,21 @@ function RuntimeProvidersSection({ } return ( -
-
-
-

- Agent harnesses -

-

- Buzz can launch local ACP-compatible agent harnesses. Install or - verify the runtimes this desktop app can see. -

-
+
+
+

+ Use the models that fit the task +

+

+ These are the local agent harnesses Buzz detected. You choose a + harness when creating each agent. +

{items.length > 0 ? ( -
+
{items.map((runtime) => ( ) : isChecking ? ( -
+
Looking for compatible runtimes...
) : errorMessage ? null : (

No compatible ACP runtimes detected yet. You can finish setup now and @@ -618,7 +499,7 @@ function RuntimeProvidersSection({ )} {errorMessage ? ( -

+

{errorMessage}

) : null} @@ -635,27 +516,25 @@ function SetupStepContent({ return ( - - -
+
) : null}
diff --git a/desktop/src/shared/api/tauriMesh.ts b/desktop/src/shared/api/tauriMesh.ts index 24ee02d277..03f4701e34 100644 --- a/desktop/src/shared/api/tauriMesh.ts +++ b/desktop/src/shared/api/tauriMesh.ts @@ -68,6 +68,11 @@ export type MeshCatalogEntry = { fit: MeshModelFit; installed: boolean; recommended: boolean; + /** + * Buzz-curated pick — known to survive the agent harness. Curated entries + * render above the fold; everything else is "advanced". + */ + curated: boolean; }; export type MeshModelCatalog = { @@ -75,7 +80,7 @@ export type MeshModelCatalog = { vramDisplay: string; vramGb: number; recommended: string | null; - /** Ranked: recommended first, then by fit, then larger first within a fit. */ + /** Ranked: recommended first, then curated, then by fit, larger first. */ entries: MeshCatalogEntry[]; }; From 76298a5e2b182b0ef9895b699e7670e12932fb20 Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Thu, 16 Jul 2026 20:13:59 -0700 Subject: [PATCH 05/12] fix(desktop): restore public relay media previews in 0.4.12 (#2014) Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> --- CHANGELOG.md | 7 +++++++ desktop/package.json | 2 +- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- desktop/src/features/communities/useCommunityInit.ts | 7 +++++++ 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 209757f8fa..f8c7d0033b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.4.12 + +- fix(desktop): proxy authenticated relay media after cold-start community initialization +- fix(desktop): keep shared-compute consumers admitted (WIP: multi-workspace publishing) ([#2000](https://github.com/block/buzz/pull/2000)) ([`5d77fa57`](https://github.com/block/buzz/commit/5d77fa57)) +- feat(desktop): reskin provider, config, community, profile, and team onboarding pages ([#2003](https://github.com/block/buzz/pull/2003)) ([`f054df73`](https://github.com/block/buzz/commit/f054df73)) + + ## v0.4.11 - Fix policy receipts for cold-launch invite links ([#2005](https://github.com/block/buzz/pull/2005)) ([`1d8d006a`](https://github.com/block/buzz/commit/1d8d006aaf078ffd5b92eae06334b7bb908b680e)) diff --git a/desktop/package.json b/desktop/package.json index faf7748f23..6ea9fdfe96 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "buzz", "private": true, - "version": "0.4.11", + "version": "0.4.12", "type": "module", "scripts": { "dev": "vite", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 6474b0f6ea..8b9b413e1b 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -972,7 +972,7 @@ dependencies = [ [[package]] name = "buzz-desktop" -version = "0.4.11" +version = "0.4.12" dependencies = [ "anyhow", "arboard", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 15492f44db..3a2738271d 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "buzz-desktop" -version = "0.4.11" +version = "0.4.12" description = "Buzz desktop app" authors = ["you"] edition = "2021" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 6a59d07454..7cf708d14b 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Buzz", - "version": "0.4.11", + "version": "0.4.12", "identifier": "xyz.block.buzz.app", "build": { "beforeDevCommand": { diff --git a/desktop/src/features/communities/useCommunityInit.ts b/desktop/src/features/communities/useCommunityInit.ts index dc2452b507..eda5fb43a0 100644 --- a/desktop/src/features/communities/useCommunityInit.ts +++ b/desktop/src/features/communities/useCommunityInit.ts @@ -186,6 +186,13 @@ export function useCommunityInit( } if (!cancelled) { + // Refresh relay-derived media state only after the backend has installed + // this community's relay override. On cold launch, mediaUrl.ts may have + // eagerly cached the default relay origin before applyCommunity ran; + // leaving that stale value makes authenticated relay media look external + // and bypass the localhost proxy. + resetMediaCaches(); + // Initialise the draft store for this identity so localStorage drafts // are scoped to the correct pubkey before the app renders. if (activeCommunity.pubkey) { From 256542da418027670652b8b8b4b96a17a3e2cd35 Mon Sep 17 00:00:00 2001 From: Cameron Hotchkies Date: Thu, 16 Jul 2026 20:31:45 -0700 Subject: [PATCH 06/12] fix(e2e): replace flaky frame-count assertion with stable height check (#2011) Signed-off-by: Cameron Hotchkies Co-authored-by: npub1cn4wxjqpz52r62hvt88u2kfxvlkpy3dvqdu7sagdzd6fwdahlz0q24c648 --- desktop/tests/e2e/agents.spec.ts | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts index 21ca0bd349..c37d4eb7d8 100644 --- a/desktop/tests/e2e/agents.spec.ts +++ b/desktop/tests/e2e/agents.spec.ts @@ -1121,25 +1121,9 @@ test("share access controls include the selected memories", async ({ .getByRole("menuitemradio", { name: "Agent + core memory" }) .click(); await expect(linkAccess).toHaveText("Agent + core memory"); - const shareCardHeightSamples = await shareMainCard.evaluate( - async (element) => { - const samples: number[] = []; - const start = performance.now(); - - await new Promise((resolve) => { - const sample = (now: number) => { - samples.push(element.getBoundingClientRect().height); - if (now - start >= 280) { - resolve(); - return; - } - requestAnimationFrame(sample); - }; - requestAnimationFrame(sample); - }); - - return samples; - }, + await waitForAnimations(page); + const expandedShareCardHeight = await shareMainCard.evaluate( + (element) => element.getBoundingClientRect().height, ); const inlineMemoryWarning = shareDialog.getByTestId( "persona-share-memory-warning", @@ -1151,10 +1135,7 @@ test("share access controls include the selected memories", async ({ await expect(inlineMemoryWarning).toContainText( "Only share it with people you trust.", ); - expect(shareCardHeightSamples.at(-1)).toBeGreaterThan(initialShareCardHeight); - expect( - new Set(shareCardHeightSamples.map((height) => Math.round(height))).size, - ).toBeGreaterThan(2); + expect(expandedShareCardHeight).toBeGreaterThan(initialShareCardHeight); await page.getByTestId("persona-share-copy-link").click(); const memoryConfirmation = page.getByTestId( "persona-share-memory-confirmation", From 43bd104de577c3df79c38913fe923ca0c84bf593 Mon Sep 17 00:00:00 2001 From: cynfria Date: Thu, 16 Jul 2026 20:34:30 -0700 Subject: [PATCH 07/12] =?UTF-8?q?feat(desktop):=20onboarding=20polish=20?= =?UTF-8?q?=E2=80=94=20docked=20CTAs,=20backup=20key=20card,=20dot=20grid?= =?UTF-8?q?=20(#2008)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Wes Co-authored-by: Claude Opus 4.8 Co-authored-by: Wes Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> --- desktop/playwright.config.ts | 1 + .../features/communities/ui/WelcomeSetup.tsx | 14 +- .../src/features/onboarding/ui/AvatarStep.tsx | 198 ++++++----- .../src/features/onboarding/ui/BackupStep.tsx | 19 +- .../onboarding/ui/CommunityOnboardingFlow.tsx | 312 +++++++++--------- .../onboarding/ui/DefaultConfigStep.tsx | 14 +- .../onboarding/ui/MachineOnboardingFlow.tsx | 195 ++++++----- .../onboarding/ui/NostrKeyImportForm.tsx | 33 +- .../onboarding/ui/NsecMaskedDisplay.tsx | 74 +++-- .../onboarding/ui/OnboardingChrome.tsx | 65 ++++ .../features/onboarding/ui/OnboardingFlow.tsx | 248 +++++++------- .../onboarding/ui/OnboardingFooter.tsx | 84 +++++ .../onboarding/ui/OnboardingStepDots.tsx | 40 --- .../features/onboarding/ui/ProfileStep.tsx | 12 +- .../src/features/onboarding/ui/SetupStep.tsx | 14 +- .../src/shared/styles/globals/components.css | 44 ++- desktop/tailwind.config.js | 4 + desktop/tests/e2e/identity-lost.spec.ts | 6 +- .../tests/e2e/onboarding-avatar-skip.spec.ts | 7 +- desktop/tests/e2e/onboarding-backup.spec.ts | 8 +- .../onboarding-docked-cta-screenshots.spec.ts | 70 ++++ 21 files changed, 859 insertions(+), 603 deletions(-) create mode 100644 desktop/src/features/onboarding/ui/OnboardingChrome.tsx create mode 100644 desktop/src/features/onboarding/ui/OnboardingFooter.tsx delete mode 100644 desktop/src/features/onboarding/ui/OnboardingStepDots.tsx create mode 100644 desktop/tests/e2e/onboarding-docked-cta-screenshots.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index fa6f9ae19f..b6296f0f3a 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ name: "smoke", testMatch: [ "**/smoke.spec.ts", + "**/onboarding-docked-cta-screenshots.spec.ts", "**/navigation.spec.ts", "**/channels.spec.ts", "**/channel-shared-header-backdrop.spec.ts", diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx index 988a87eb87..d6a6888426 100644 --- a/desktop/src/features/communities/ui/WelcomeSetup.tsx +++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx @@ -13,7 +13,7 @@ import { pubkeyToNpub } from "@/shared/lib/nostrUtils"; import { Button } from "@/shared/ui/button"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme"; -import { OnboardingStepDots } from "@/features/onboarding/ui/OnboardingStepDots"; +import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome"; type WelcomeSetupPage = "welcome" | "join" | "invite"; type WelcomeTransitionMode = "initial" | OnboardingTransitionDirection; @@ -95,7 +95,7 @@ export function WelcomeSetup({ data-system-color-scheme={systemColorScheme} > - +
{page === "welcome" ? (
-

+

Join or create a community

@@ -152,9 +152,7 @@ export function WelcomeSetup({ transitionKey={`join-${transitionDirection}`} >

-

- Join a community -

+

Join a community

Send your public key to a community owner. Keep Buzz open; once they add you, their invite link will continue setup here. @@ -211,9 +209,7 @@ export function WelcomeSetup({ transitionKey={`invite-${transitionDirection}`} >

-

- I have an invite link -

+

I have an invite link

Keep this page open, then click the invite link you received. Buzz will continue automatically. You can also paste it below. diff --git a/desktop/src/features/onboarding/ui/AvatarStep.tsx b/desktop/src/features/onboarding/ui/AvatarStep.tsx index 20fa8b4586..7333bea5c9 100644 --- a/desktop/src/features/onboarding/ui/AvatarStep.tsx +++ b/desktop/src/features/onboarding/ui/AvatarStep.tsx @@ -8,7 +8,8 @@ import { Plus } from "lucide-react"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { Spinner } from "@/shared/ui/spinner"; -import { StepProgress } from "@/shared/ui/step-progress"; +import { ONBOARDING_PRIMARY_CTA_CLASS } from "./OnboardingChrome"; +import { OnboardingFooter } from "./OnboardingFooter"; import { AnimatePresence, motion } from "motion/react"; import * as React from "react"; import { @@ -26,8 +27,6 @@ type AvatarStepProps = { submit: ProfileStepActions["submit"]; updateAvatarUrl: ProfileStepActions["updateAvatarUrl"]; }; - /** Step number for the compact progress bar (mobile layout). */ - currentStep: number; direction: OnboardingTransitionDirection; /** When true, a ghost "Skip for now" button is always visible (not just on error). */ showAlwaysSkip?: boolean; @@ -35,8 +34,6 @@ type AvatarStepProps = { ProfileStepState, "avatar" | "isSaving" | "isUploadingAvatar" | "name" | "saveRecovery" >; - /** Total steps for the compact progress bar (mobile layout). */ - totalSteps?: number; }; function ErrorBanner({ message }: { message: string | null }) { @@ -125,7 +122,6 @@ function AvatarPreview({ function AvatarStepActions({ canSubmit, - currentStep, hidden, isSaving, isUploadingAvatar, @@ -135,10 +131,8 @@ function AvatarStepActions({ onSubmit, saveRecovery, showAlwaysSkip, - totalSteps, }: { canSubmit: boolean; - currentStep: number; hidden: boolean; isSaving: boolean; isUploadingAvatar: boolean; @@ -148,118 +142,108 @@ function AvatarStepActions({ onSubmit: () => void; saveRecovery: ProfileStepState["saveRecovery"]; showAlwaysSkip: boolean; - totalSteps?: number; }) { const areNavigationActionsDisabled = isSaving || isUploadingAvatar; return ( - - {hidden ? null : ( - - - - {saveRecovery.canSkipForNow ? ( - // Error-recovery path: exits onboarding entirely when there is no - // saved display name to fall back on. + + {saveRecovery.canSkipForNow ? ( + // Error-recovery path: exits onboarding entirely when there is no + // saved display name to fall back on. + + ) : showAlwaysSkip && !saveRecovery.errorMessage ? ( + // Normal path: advances to the theme step without saving an avatar. + + ) : null} + + {saveRecovery.canAdvanceWithoutSaving ? ( + + ) : null} + + - ) : showAlwaysSkip && !saveRecovery.errorMessage ? ( - // Normal path: advances to the theme step without saving an avatar. - - ) : null} - - {saveRecovery.canAdvanceWithoutSaving ? ( - - ) : null} - - - - - - )} - + + )} + + ); } export function AvatarStep({ actions, - currentStep, direction, showAlwaysSkip = false, state, - totalSteps, }: AvatarStepProps) { const { advanceWithoutSaving, @@ -328,7 +312,11 @@ export function AvatarStep({ return (

-

+

Next, add a display image

-

+

Choose an image or emoji as your avatar

@@ -411,7 +399,6 @@ export function AvatarStep({