Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
112 changes: 112 additions & 0 deletions apps/app/src/components/drafts/LegacyDraftImport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import type { DraftContentInput, DraftOptions } from "@bb/server-contract";
import { useRootComposeProjectId } from "@/lib/root-compose-selection";
import {
usePromptBoxEnvironmentPreference,
usePromptBoxMachinePreference,
usePromptBoxModelPreference,
usePromptBoxPermissionModePreference,
usePromptBoxProviderPreference,
usePromptBoxReasoningLevelPreference,
usePromptBoxServiceTierPreference,
} from "@/hooks/thread-creation-options/persisted-selection-fields";
import { sanitizeStoredEnvironmentValue } from "@/hooks/useThreadCreationOptions";
import { parseEnvironmentValue } from "@/components/pickers/environment-picker-value";
import { appToast } from "@/components/ui/app-toast";
import { importLegacyNewThreadDraft } from "@/lib/drafts/legacy-import";

const IMPORT_TOAST_ID = "legacy-new-thread-draft-import";

export function LegacyDraftImport() {
const [projectId] = useRootComposeProjectId();
const { value: providerId } = usePromptBoxProviderPreference();
const { value: model } = usePromptBoxModelPreference(providerId);
const { value: reasoningLevel } =
usePromptBoxReasoningLevelPreference(providerId);
const { value: serviceTier } = usePromptBoxServiceTierPreference();
const { value: permissionMode } = usePromptBoxPermissionModePreference();
const { value: environmentValue } =
usePromptBoxEnvironmentPreference(projectId);
const { value: machineId } = usePromptBoxMachinePreference(projectId);
const seed = useMemo((): Omit<DraftContentInput, "prompt"> => {
const parsed = parseEnvironmentValue(
sanitizeStoredEnvironmentValue(environmentValue),
);
const environment: DraftOptions["environment"] =
parsed?.type === "provider"
? {
type: "provider",
environmentProviderId: parsed.environmentProviderId,
machine:
machineId === "" ? null : { type: "existing", hostId: machineId },
inputs: null,
}
: parsed?.type === "reuse" && parsed.environmentId !== null
? { type: "reuse", environmentId: parsed.environmentId }
: null;
return {
projectId,
options: {
providerId: providerId || null,
model: model || null,
reasoningLevel: reasoningLevel || null,
serviceTier: serviceTier || null,
permissionMode: permissionMode || null,
environment,
},
};
}, [
environmentValue,
machineId,
model,
permissionMode,
projectId,
providerId,
reasoningLevel,
serviceTier,
]);
const seedRef = useRef(seed);
seedRef.current = seed;
const running = useRef(false);
const run = useCallback(async () => {
if (running.current) return;
running.current = true;
try {
let newerLegacyValue = true;
while (newerLegacyValue) {
const result = await importLegacyNewThreadDraft(seedRef.current);
if (result.error !== null) {
appToast.error("Could not save your existing draft", {
id: IMPORT_TOAST_ID,
description: "Your original draft is still on this device.",
duration: Infinity,
action: {
label: "Retry",
onClick: () => {
void run();
},
},
});
return;
}
newerLegacyValue = result.newerLegacyValue;
}
appToast.dismiss(IMPORT_TOAST_ID);
} finally {
running.current = false;
}
}, []);
useEffect(() => {
void run();
const onStorage = (event: StorageEvent) => {
if (event.key === "bb.promptbox.contents-draft-3") void run();
};
window.addEventListener("storage", onStorage);
window.addEventListener("online", run);
return () => {
window.removeEventListener("storage", onStorage);
window.removeEventListener("online", run);
};
}, [run]);
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AppLayout } from "./AppLayout";

vi.mock("@/lib/drafts/resource-runtime", () => ({
createNewThreadDraft: vi.fn(() => `drf_${crypto.randomUUID()}`),
initializeNewThreadDraft: vi.fn(),
}));

vi.mock("@/components/drafts/LegacyDraftImport", () => ({
LegacyDraftImport: () => null,
}));

const ROOT_COMPOSE_PROJECT_ID_STORAGE_KEY = "bb.root-compose.project-id";

const mockUseThread = vi.hoisted(() => vi.fn());
Expand Down
35 changes: 32 additions & 3 deletions apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LegacyDraftImport } from "@/components/drafts/LegacyDraftImport";
import { type MouseEvent as ReactMouseEvent, type ReactNode } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { flushSync } from "react-dom";
Expand Down Expand Up @@ -95,12 +96,17 @@ import {
shouldRestoreIOSViewportOnKeyboardDismissal,
useMobileVisualViewportHeight,
} from "./useMobileVisualViewportHeight";
import { createNewThreadDraft } from "@/lib/drafts/resource-runtime";
import { openDraftInSplit } from "@/lib/split-layout/openDraftInSplit";
import { wsManager } from "@/lib/ws";
import { splitLayoutAtom } from "@/lib/split-layout/atoms";
import { findPaneByThread } from "@/lib/split-layout";
import { applyThreadOpenToLayout } from "@/views/thread-detail/splitThreadNavigation";
import { useAppSettingsRouteMemory } from "@/hooks/useAppSettingsRouteMemory";
import { useSetRootComposeProjectId } from "@/lib/root-compose-selection";
import {
useRootComposeProjectId,
useSetRootComposeProjectId,
} from "@/lib/root-compose-selection";
import { BackToAppCommandHandler } from "./BackToAppCommandHandler";

const SIDEBAR_WIDTH_KEY = "bb.sidebar.width";
Expand Down Expand Up @@ -408,6 +414,7 @@ export function AppLayout({ children }: AppLayoutProps) {
toolsRoutePath,
} = useAppSettingsRouteMemory();
const setRootComposeProjectId = useSetRootComposeProjectId();
const [rootComposeProjectId] = useRootComposeProjectId();
useEffect(
() =>
wsManager.onThreadOpen((signal) => {
Expand All @@ -432,12 +439,33 @@ export function AppLayout({ children }: AppLayoutProps) {
}),
[isCompactViewport, navigate, store],
);
useEffect(
() =>
wsManager.onDraftOpen((signal) =>
openDraftInSplit({
store,
navigate,
draftId: signal.draftId,
split: signal.split,
isCompact: isCompactViewport,
}),
),
[isCompactViewport, navigate, store],
);
useAppCommandHandler("thread.new", () => {
if (projectId !== undefined) {
setRootComposeProjectId(projectId);
}
void navigate(getRootComposeRoutePath(), {
state: { focusPrompt: true },
const draftId = createNewThreadDraft({
projectId: projectId ?? rootComposeProjectId,
});
openDraftInSplit({
store,
navigate: (route, options) =>
navigate(route, { ...options, state: { focusPrompt: true } }),
draftId,
split: "replace",
isCompact: isCompactViewport,
});
return true;
});
Expand Down Expand Up @@ -712,6 +740,7 @@ export function AppLayout({ children }: AppLayoutProps) {

return (
<ProjectActionsProvider>
<LegacyDraftImport />
<ThreadTitleMentionResourcesProvider {...titleMentionResources}>
<ThreadActionsProvider>
<SidebarStateBridge>
Expand Down
11 changes: 9 additions & 2 deletions apps/app/src/components/plugin/PluginNavSidebarItems.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ import {
type SplitLayout,
} from "@/lib/split-layout";
import { usePublishPluginDetailOpener } from "./plugin-detail-opener";
vi.mock("@/lib/drafts/resource-runtime", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/drafts/resource-runtime")>()),
createNewThreadDraft: vi.fn(() => `drf_${crypto.randomUUID()}`),
}));

vi.mock("@/components/ui/app-toast", () => ({
appToast: {
dismiss: vi.fn(),
Expand Down Expand Up @@ -197,7 +202,7 @@ function renderSidebarItems(options: RenderSidebarItemsOptions = {}) {
root: {
type: "pane",
paneId: "pane-1",
content: { kind: "new-thread" },
content: { kind: "new-thread", draftId: "drf_sidebar_test" },
},
focusedPaneId: "pane-1",
});
Expand Down Expand Up @@ -313,6 +318,7 @@ beforeEach(() => {
resetPluginFrontendBootStateForTest();
markPluginFrontendsSettled();
window.localStorage.clear();
window.sessionStorage.clear();
resetAllCrashedPluginSlotsForTest();
vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "warn").mockImplementation(() => {});
Expand All @@ -326,6 +332,7 @@ afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
window.localStorage.clear();
window.sessionStorage.clear();
});

describe("PluginNavSidebarItems", () => {
Expand Down Expand Up @@ -659,7 +666,7 @@ describe("PluginNavSidebarItems", () => {
root: {
type: "split",
dir: "row",
sizes: [1, 1, 1],
sizes: [1 / 3, 1 / 3, 1 / 3],
children: [
{
type: "pane",
Expand Down
6 changes: 5 additions & 1 deletion apps/app/src/components/plugin/PluginNavSidebarItems.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createNewThreadDraft } from "@/lib/drafts/resource-runtime";
import {
useCallback,
useEffect,
Expand Down Expand Up @@ -270,7 +271,10 @@ function PluginNavSidebarItemList({
continue;
next =
listPanes(next.root).length === 1
? replacePaneContent(next, pane.paneId, { kind: "new-thread" })
? replacePaneContent(next, pane.paneId, {
kind: "new-thread",
draftId: createNewThreadDraft({}),
})
: removePane(next, pane.paneId);
}
}
Expand Down
Loading
Loading