Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e51106e
chore(desktop): start onboarding v3 collaboration
klopez4212 Aug 26, 2026
55af8df
Prototype Buzz onboarding V3 workshop flow
klopez4212 Aug 26, 2026
597164d
Complete onboarding V3 workshop preview
klopez4212 Aug 27, 2026
1aeab6d
Merge remote-tracking branch 'origin/main' into kennylopez-buzz-onboa…
klopez4212 Aug 27, 2026
750dcfc
Expand onboarding V3 workshop preview
klopez4212 Aug 27, 2026
381a3e9
refactor(onboarding): clarify Nostr identity preview
delkc Aug 27, 2026
f7a3dca
refactor(onboarding): clarify community preview
delkc Aug 27, 2026
d91b5e1
feat(desktop): refine onboarding v3 workshop preview
klopez4212 Aug 28, 2026
e5b0dfa
Merge remote-tracking branch 'origin/main' into kennylopez-buzz-onboa…
klopez4212 Aug 28, 2026
dce214a
refactor(desktop): split onboarding preview journey
klopez4212 Aug 28, 2026
56a2511
refactor(onboarding): clarify identity backup flow
delkc Aug 28, 2026
18c00f2
Merge remote-tracking branch 'origin/main' into kennylopez-buzz-onboa…
delkc Aug 28, 2026
9271f6f
refactor(onboarding): explore private welcome space
delkc Aug 28, 2026
b137e1b
Merge remote-tracking branch 'origin/main' into kennylopez-buzz-onboa…
delkc Aug 28, 2026
f672940
fix(onboarding): restore key sign-in preview
delkc Aug 28, 2026
4c341c0
refactor(onboarding): simplify private key guidance
delkc Aug 28, 2026
d8f34ce
Merge remote-tracking branch 'origin/main' into kennylopez-buzz-onboa…
delkc Aug 28, 2026
6789114
feat(onboarding): refine V3 workshop preview
klopez4212 Aug 29, 2026
1a57547
feat(onboarding): polish V3 workshop card flow
klopez4212 Aug 29, 2026
857e26e
refactor(onboarding): align V3 card presentation
klopez4212 Aug 29, 2026
a5fcda0
feat(onboarding): extend V3 workshop preview
klopez4212 Aug 30, 2026
826aaf2
feat(onboarding): refine V3 identity and welcome flow
klopez4212 Aug 30, 2026
290e3de
Prototype alternate AI client onboarding paths
klopez4212 Sep 1, 2026
15abc74
Merge remote-tracking branch 'origin/main' into kennylopez-buzz-onboa…
klopez4212 Sep 1, 2026
a1a9899
Refine onboarding V3 workshop flow
klopez4212 Sep 1, 2026
cdc5499
Merge remote-tracking branch 'origin/main' into kennylopez-buzz-onboa…
klopez4212 Sep 1, 2026
6dbc70f
Split onboarding harness preview catalog
klopez4212 Sep 1, 2026
3375ca8
Refine onboarding backup-ready actions
klopez4212 Sep 1, 2026
e0947d1
refactor(onboarding): clarify AI connection flow
delkc Sep 1, 2026
f85b1c2
Merge remote-tracking branch 'origin/main' into kennylopez-buzz-onboa…
delkc Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,41 @@ desktop-standalone *ARGS: _ensure-sidecar-stubs
echo "Starting standalone desktop on Vite port ${BUZZ_VITE_PORT}; no relay services were started"
pnpm exec tauri dev --config "$BUZZ_TAURI_CONFIG" {{ARGS}}

# Launch a development-only onboarding workshop. The frontend uses React-only
# state and fixed mock data; native setup stops before migrations, identity
# persistence, keychain access, relay sync, or agent restore.
onboarding-preview *ARGS: _ensure-sidecar-stubs
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
cd {{desktop_dir}}
[[ -d node_modules ]] || pnpm install
unset BUZZ_AUTH_TAG BUZZ_RESET_WEBVIEW_STATE BUZZ_SHARE_IDENTITY
export BUZZ_ONBOARDING_PREVIEW=1
export BUZZ_RELAY_URL="ws://127.0.0.1:9"
# Fixed public test fixture: build_app_state never generates an identity,
# while the preview UI never receives or exposes this value.
export BUZZ_PRIVATE_KEY="3dbaebadb5dfd777ff25149ee230d907a15a9e1294b40b830661e65bb42f6c03"
source ../scripts/instance-env.sh
export BUZZ_PREVIEW_VITE_PORT=$((20001 + (BUZZ_VITE_PORT % 40000)))
BUZZ_TAURI_CONFIG=$(node -e '
const config = JSON.parse(process.env.BUZZ_TAURI_CONFIG);
const url = new URL(config.build.devUrl);
url.port = process.env.BUZZ_PREVIEW_VITE_PORT;
url.searchParams.set("onboardingPreview", "1");
config.build.devUrl = url.toString();
config.build.beforeDevCommand = `exec ./node_modules/.bin/vite --port ${process.env.BUZZ_PREVIEW_VITE_PORT} --strictPort`;
config.identifier = `xyz.block.buzz.onboarding-preview.p${process.env.BUZZ_PREVIEW_VITE_PORT}`;
config.productName = "Buzz Onboarding Preview";
process.stdout.write(JSON.stringify(config));
')
export BUZZ_TAURI_CONFIG
export BUZZ_DEV_KEYRING_SERVICE="buzz-desktop-onboarding-preview.${BUZZ_PREVIEW_VITE_PORT}"
INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.BUZZ_TAURI_CONFIG).identifier)")
trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true' EXIT
echo "Starting non-persistent onboarding preview on Vite port ${BUZZ_PREVIEW_VITE_PORT}; external relay access is disabled"
pnpm exec tauri dev --config "$BUZZ_TAURI_CONFIG" {{ARGS}}

# Run the desktop app against the internal staging relay (installs deps + builds agent tools automatically)
staging *ARGS: bootstrap _ensure-sidecar-stubs
#!/usr/bin/env bash
Expand Down
24 changes: 12 additions & 12 deletions desktop/src-tauri/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,18 +176,18 @@ pub fn build_media_fetch_client() -> reqwest::Result<reqwest::Client> {
}

pub fn build_app_state() -> AppState {
// Env var takes precedence (dev/CI). If absent, resolve_persisted_identity()
// in setup() will replace the ephemeral placeholder with a persisted key.
let (keys, identity_storage) = match identity_from_env() {
Some(keys) => {
eprintln!(
"buzz-desktop: configured identity pubkey {}",
keys.public_key().to_hex()
);
(keys, IdentityStorage::Environment)
}
None => (Keys::generate(), IdentityStorage::Ephemeral),
};
// Env var takes precedence; setup() otherwise resolves a persisted identity.
let (keys, identity_storage) =
match crate::onboarding_preview::require_fixed_identity(identity_from_env()) {
Some(keys) => {
eprintln!(
"buzz-desktop: configured identity pubkey {}",
keys.public_key().to_hex()
);
(keys, IdentityStorage::Environment)
}
None => (Keys::generate(), IdentityStorage::Ephemeral),
};

AppState {
keys: Mutex::new(keys),
Expand Down
8 changes: 8 additions & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod native_websocket_batch;
mod nostr_bind;
pub mod nostr_convert;
mod observed_unread;
mod onboarding_preview;
mod persona_catalog;
mod prevent_sleep;
mod ptt_shortcut;
Expand Down Expand Up @@ -159,6 +160,10 @@ pub fn run() {
// commit the startup surface before revealing it.
let window = webview.window();

if onboarding_preview::reveal_window(&window) {
return;
}

#[cfg(target_os = "macos")]
{
set_initial_window_backing(&window);
Expand Down Expand Up @@ -236,6 +241,9 @@ pub fn run() {
.manage(channel_head_cache::ChannelHeadCacheStore::default())
.setup(move |app| {
let app_handle = app.handle().clone();
if onboarding_preview::skip_native_setup() {
return Ok(());
}
#[cfg(target_os = "macos")]
{
tray_menu::init(&app_handle)?;
Expand Down
55 changes: 55 additions & 0 deletions desktop/src-tauri/src/onboarding_preview.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const ONBOARDING_PREVIEW_ENV: &str = "BUZZ_ONBOARDING_PREVIEW";

fn enabled_for(debug_build: bool, value: Option<&str>) -> bool {
debug_build && value == Some("1")
}

/// Whether the development-only native onboarding workshop is active.
pub(crate) fn enabled() -> bool {
enabled_for(
cfg!(debug_assertions),
std::env::var(ONBOARDING_PREVIEW_ENV).ok().as_deref(),
)
}

/// Fail closed before app state could generate a placeholder identity.
pub(crate) fn require_fixed_identity<T>(identity: Option<T>) -> Option<T> {
assert!(
identity.is_some() || !enabled(),
"BUZZ_ONBOARDING_PREVIEW requires fixed mock identity data"
);
identity
}

/// Stop native setup before migrations, persistence, networking, or agents.
pub(crate) fn skip_native_setup() -> bool {
if !enabled() {
return false;
}
eprintln!(
"buzz-desktop: onboarding preview safety mode; skipped migrations, identity persistence, relay sync, and agent restore"
);
true
}

/// Preview does not wait for the normal app's first-render event.
pub(crate) fn reveal_window<R: tauri::Runtime>(window: &tauri::Window<R>) -> bool {
if !enabled() {
return false;
}
crate::initial_window::reveal_initial_window(window);
true
}

#[cfg(test)]
mod tests {
use super::enabled_for;

#[test]
fn preview_is_debug_only_and_requires_exact_opt_in() {
assert!(enabled_for(true, Some("1")));
assert!(!enabled_for(true, Some("true")));
assert!(!enabled_for(true, None));
assert!(!enabled_for(false, Some("1")));
}
}
8 changes: 4 additions & 4 deletions desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"windows": [
{
"title": "",
"width": 800,
"height": 600,
"width": 900,
"height": 650,
"maximized": true,
"visible": false,
"transparent": false,
Expand All @@ -30,8 +30,8 @@
"y": 25
},
"backgroundThrottling": "disabled",
"minWidth": 800,
"minHeight": 500
"minWidth": 900,
"minHeight": 650
}
],
"macOSPrivateApi": true,
Expand Down
98 changes: 98 additions & 0 deletions desktop/src/features/agents/ui/AgentAvatarOnboardingTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Plus } from "lucide-react";

import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import type { EmojiAvatarDescriptor } from "@/features/profile/ui/ProfileAvatarEditor.utils";
import { cn } from "@/shared/lib/cn";
import { PopoverTrigger } from "@/shared/ui/popover";
import { Spinner } from "@/shared/ui/spinner";

export function AgentAvatarOnboardingTrigger({
assetLabel,
avatarUrl,
disabled,
emojiAvatarPreview,
hasAvatar,
isAnimatedPreviewActive,
isAvatarPending,
label,
onPreviewContainerChange,
squishKey,
testIdPrefix,
}: {
assetLabel: string;
avatarUrl: string | null;
disabled: boolean;
emojiAvatarPreview: EmojiAvatarDescriptor | null;
hasAvatar: boolean;
isAnimatedPreviewActive: boolean;
isAvatarPending: boolean;
label: string;
onPreviewContainerChange: (container: HTMLDivElement | null) => void;
squishKey: number;
testIdPrefix: string;
}) {
const actionLabel = hasAvatar ? `Change ${assetLabel}` : `Add ${assetLabel}`;

return (
<div className="group/avatar-trigger relative h-full w-full">
<div
className="pointer-events-none absolute inset-0 z-10 overflow-visible"
data-testid={`${testIdPrefix}-animated-preview-slot`}
ref={onPreviewContainerChange}
/>
{isAnimatedPreviewActive ? null : isAvatarPending ? (
<div className="grid h-full w-full place-items-center rounded-full border-2 border-dashed border-border bg-background text-foreground shadow-xs">
<Spinner
aria-label={`Uploading ${assetLabel}`}
className="h-4 w-4 border-2"
/>
</div>
) : emojiAvatarPreview ? (
<div
aria-label={`${label} ${assetLabel}`}
className="flex h-full w-full items-center justify-center overflow-hidden rounded-full shadow-xs transition-[background-color] duration-150 ease-out motion-reduce:transition-none"
data-testid={`${testIdPrefix}-preview`}
role="img"
style={{ backgroundColor: emojiAvatarPreview.color }}
>
<span
className={cn(
"flex h-full w-full items-center justify-center text-6xl leading-none",
squishKey > 0 && "buzz-avatar-squish",
)}
key={squishKey}
>
{emojiAvatarPreview.emoji}
</span>
</div>
) : hasAvatar ? (
<ProfileAvatar
avatarUrl={avatarUrl}
className="h-full w-full text-4xl"
label={label}
testId={`${testIdPrefix}-preview`}
/>
) : (
<div
className="flex h-full w-full items-center justify-center rounded-full border-2 border-dashed border-border bg-background text-foreground shadow-xs transition-[background-color,border-color,color] duration-150 ease-out group-hover/avatar-trigger:border-foreground/30 group-hover/avatar-trigger:bg-[#f5f5f5] motion-reduce:transition-none"
data-testid={`${testIdPrefix}-empty`}
>
<Plus aria-hidden="true" className="size-9" />
</div>
)}
<PopoverTrigger asChild>
<button
aria-label={actionLabel}
className={cn(
"absolute inset-0 z-20 rounded-full bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-default disabled:opacity-60",
isAnimatedPreviewActive && "pointer-events-none",
)}
data-testid={`${testIdPrefix}-open`}
disabled={disabled || isAvatarPending}
title={actionLabel}
type="button"
/>
</PopoverTrigger>
</div>
);
}
Loading
Loading