mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): worktree support — workspace boundary + shared identity (#610)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "sprout"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -33,6 +33,7 @@ type OnboardingGateStage = "blocking" | "onboarding" | "ready";
|
||||
|
||||
type UseFirstRunOnboardingGateOptions = {
|
||||
currentPubkey: string | null;
|
||||
hasExistingProfile: boolean;
|
||||
identityIsFetching: boolean;
|
||||
identityStatus: QueryStatus;
|
||||
profileStatus: QueryStatus;
|
||||
@@ -125,6 +126,7 @@ function resolveOnboardingGateStage({
|
||||
|
||||
export function useFirstRunOnboardingGate({
|
||||
currentPubkey,
|
||||
hasExistingProfile,
|
||||
identityIsFetching,
|
||||
identityStatus,
|
||||
profileStatus,
|
||||
@@ -166,14 +168,37 @@ export function useFirstRunOnboardingGate({
|
||||
return;
|
||||
}
|
||||
|
||||
// If the relay already has a real profile for this pubkey, the user has
|
||||
// been onboarded elsewhere — skip the gate and persist the completion so
|
||||
// future launches in this data dir don't re-check.
|
||||
if (hasExistingProfile) {
|
||||
if (typeof window !== "undefined" && currentPubkey) {
|
||||
window.localStorage.setItem(
|
||||
onboardingCompletionStorageKey(currentPubkey),
|
||||
"true",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setGateState((current) =>
|
||||
updateActiveGateState(current, currentPubkey, (activeGateState) => ({
|
||||
...activeGateState,
|
||||
hasSettledCurrentPubkey: true,
|
||||
isOpen: !activeGateState.hasCompletedCurrentPubkey,
|
||||
})),
|
||||
updateActiveGateState(current, currentPubkey, (activeGateState) => {
|
||||
const alreadyOnboarded =
|
||||
activeGateState.hasCompletedCurrentPubkey || hasExistingProfile;
|
||||
return {
|
||||
...activeGateState,
|
||||
hasCompletedCurrentPubkey: alreadyOnboarded,
|
||||
hasSettledCurrentPubkey: true,
|
||||
isOpen: !alreadyOnboarded,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, [currentPubkey, hasSettledCurrentPubkey, identityStatus, profileStatus]);
|
||||
}, [
|
||||
currentPubkey,
|
||||
hasExistingProfile,
|
||||
hasSettledCurrentPubkey,
|
||||
identityStatus,
|
||||
profileStatus,
|
||||
]);
|
||||
|
||||
const skipForNow = React.useCallback(() => {
|
||||
setGateState((current) =>
|
||||
@@ -213,6 +238,14 @@ export function useFirstRunOnboardingGate({
|
||||
};
|
||||
}
|
||||
|
||||
function hasRealDisplayName(displayName?: string | null): boolean {
|
||||
if (!displayName) return false;
|
||||
const trimmed = displayName.trim();
|
||||
if (trimmed.length === 0) return false;
|
||||
const lower = trimmed.toLowerCase();
|
||||
return !lower.startsWith("npub1") && !lower.startsWith("nostr:npub1");
|
||||
}
|
||||
|
||||
export function useAppOnboardingState() {
|
||||
const queryClient = useQueryClient();
|
||||
const identityQuery = useIdentityQuery();
|
||||
@@ -221,6 +254,7 @@ export function useAppOnboardingState() {
|
||||
const profileQuery = useProfileQuery();
|
||||
const onboardingGate = useFirstRunOnboardingGate({
|
||||
currentPubkey,
|
||||
hasExistingProfile: hasRealDisplayName(profileQuery.data?.displayName),
|
||||
identityIsFetching: identityQuery.fetchStatus === "fetching",
|
||||
identityStatus: identityQuery.status,
|
||||
profileStatus: profileQuery.status,
|
||||
|
||||
@@ -9,6 +9,10 @@ const BLANK_TYLER_IDENTITY = {
|
||||
...TEST_IDENTITIES.tyler,
|
||||
username: "",
|
||||
};
|
||||
const FIRST_RUN_ALICE = {
|
||||
...TEST_IDENTITIES.alice,
|
||||
username: "",
|
||||
};
|
||||
|
||||
type TestIdentity = {
|
||||
privateKey: string;
|
||||
@@ -139,17 +143,16 @@ test("page 1 accepts an avatar URL as the secondary avatar path", async ({
|
||||
test("first-run onboarding keeps the shell hidden through both pages and only marks Home seen after finish", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedActiveIdentity(page, TEST_IDENTITIES.alice);
|
||||
await seedActiveIdentity(page, FIRST_RUN_ALICE);
|
||||
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("onboarding-gate")).toBeVisible();
|
||||
await expect(page.getByTestId("onboarding-page-1")).toBeVisible();
|
||||
await expect(page.getByTestId("onboarding-display-name")).toHaveValue(
|
||||
"alice",
|
||||
);
|
||||
await expect(page.getByTestId("onboarding-display-name")).toHaveValue("");
|
||||
await expectNoHomeSeenEntries(page);
|
||||
|
||||
await page.getByTestId("onboarding-display-name").fill("Alice");
|
||||
await continueToSetupPage(page);
|
||||
await expectShellHidden(page);
|
||||
await expect(page.getByTestId("onboarding-provider-goose")).toBeVisible();
|
||||
@@ -161,6 +164,17 @@ test("first-run onboarding keeps the shell hidden through both pages and only ma
|
||||
await expectHomeSeenCount(page, 2);
|
||||
});
|
||||
|
||||
test("existing relay profile auto-skips onboarding without localStorage completion", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedActiveIdentity(page, TEST_IDENTITIES.alice);
|
||||
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("onboarding-gate")).toHaveCount(0);
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("Home");
|
||||
});
|
||||
|
||||
test("finishing onboarding auto-joins the #general channel for a new member", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -179,7 +193,7 @@ test("finishing onboarding auto-joins the #general channel for a new member", as
|
||||
test("page 2 falls back to Doctor guidance when ACP tools are not installed", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedActiveIdentity(page, TEST_IDENTITIES.alice);
|
||||
await seedActiveIdentity(page, FIRST_RUN_ALICE);
|
||||
await installMockBridge(
|
||||
page,
|
||||
{
|
||||
@@ -189,6 +203,7 @@ test("page 2 falls back to Doctor guidance when ACP tools are not installed", as
|
||||
);
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("onboarding-display-name").fill("Alice");
|
||||
await continueToSetupPage(page);
|
||||
await expect(page.getByTestId("onboarding-acp-empty")).toBeVisible();
|
||||
await expect(
|
||||
|
||||
+20
-1
@@ -5,6 +5,7 @@
|
||||
# SPROUT_RELAY_PORT, SPROUT_RELAY_URL
|
||||
# SPROUT_INSTANCE_SLUG, SPROUT_WORKTREE_LABEL, VITE_DEV_BRANCH (worktrees only)
|
||||
# SPROUT_TAURI_CONFIG
|
||||
# SPROUT_PRIVATE_KEY (worktrees only, when SPROUT_SHARE_IDENTITY=1)
|
||||
|
||||
WORKTREE_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
|
||||
@@ -24,13 +25,31 @@ unset VITE_DEV_BRANCH
|
||||
|
||||
# In worktrees, extract a label from the branch name and derive a unique app
|
||||
# identity and icon so multiple local desktop instances can run side by side.
|
||||
#
|
||||
# Worktree detection: compare --git-dir to --git-common-dir. In the main
|
||||
# working tree these are identical; in any worktree (whether under .worktrees/,
|
||||
# .claude/worktrees/, or elsewhere on disk) they differ.
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
GIT_DIR=$(git rev-parse --git-dir)
|
||||
if [[ "$GIT_DIR" == *".git/worktrees/"* ]]; then
|
||||
GIT_COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null)
|
||||
if [[ -n "$GIT_COMMON_DIR" && "$GIT_DIR" != "$GIT_COMMON_DIR" ]]; then
|
||||
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
|
||||
export SPROUT_WORKTREE_LABEL="${BRANCH_NAME##*/}"
|
||||
export SPROUT_INSTANCE_SLUG=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
|
||||
|
||||
# SPROUT_SHARE_IDENTITY=1: reuse the main dev checkout's Nostr key so
|
||||
# worktrees skip onboarding and share the same identity. The per-worktree
|
||||
# identifier is kept so concurrent instances don't collide on
|
||||
# tauri-plugin-single-instance or the app data directory.
|
||||
if [[ "${SPROUT_SHARE_IDENTITY:-0}" == "1" ]]; then
|
||||
CANONICAL_KEY="$HOME/Library/Application Support/xyz.block.sprout.app.dev/identity.key"
|
||||
if [[ -f "$CANONICAL_KEY" ]]; then
|
||||
export SPROUT_PRIVATE_KEY="$(cat "$CANONICAL_KEY")"
|
||||
else
|
||||
echo "⚠ SPROUT_SHARE_IDENTITY=1 but no identity found at $CANONICAL_KEY — run Sprout from repo root first" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
ICON_DIR="$(pwd)/src-tauri/target/dev-icons"
|
||||
mkdir -p "$ICON_DIR"
|
||||
DEV_ICON="$ICON_DIR/icon.icns"
|
||||
|
||||
Reference in New Issue
Block a user