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
52 changes: 48 additions & 4 deletions apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { type MouseEvent as ReactMouseEvent, type ReactNode } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
lazy,
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { flushSync } from "react-dom";
import { atom, useAtom, useAtomValue, useStore } from "jotai";
import { atomWithStorage } from "jotai/utils";
Expand Down Expand Up @@ -110,14 +118,25 @@ 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 LegacyDraftImport = lazy(() =>
import("@/components/drafts/LegacyDraftImport").then((module) => ({
default: module.LegacyDraftImport,
})),
);

const SIDEBAR_WIDTH_KEY = "bb.sidebar.width";
const SIDEBAR_OPEN_KEY = "bb.sidebar.open";
const SIDEBAR_MIN_WIDTH = 240;
Expand Down Expand Up @@ -419,6 +438,7 @@ export function AppLayout({ children }: AppLayoutProps) {
const { appRoutePath, settingsRoutePath, toolsBackRoutePath } =
useAppSettingsRouteMemory();
const setRootComposeProjectId = useSetRootComposeProjectId();
const [rootComposeProjectId] = useRootComposeProjectId();
useEffect(
() =>
wsManager.onThreadOpen((signal) => {
Expand All @@ -443,12 +463,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 @@ -749,6 +790,9 @@ export function AppLayout({ children }: AppLayoutProps) {
return (
<TooltipProvider delayDuration={300} disableHoverableContent>
<ProjectActionsProvider>
<Suspense fallback={null}>
<LegacyDraftImport />
</Suspense>
<ThreadTitleMentionResourcesProvider {...titleMentionResources}>
<ThreadActionsProvider>
<SidebarStateBridge>
Expand Down
16 changes: 13 additions & 3 deletions apps/app/src/components/plugin/PluginNavSidebarItems.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,15 @@ import { maximizedPaneIdAtom, splitLayoutAtom } from "@/lib/split-layout/atoms";
import {
countPanes,
findPaneByContent,
listPanes,
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 +203,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 @@ -317,6 +323,7 @@ beforeEach(() => {
resetPluginFrontendBootStateForTest();
markPluginFrontendsSettled();
window.localStorage.clear();
window.sessionStorage.clear();
resetAllCrashedPluginSlotsForTest();
vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "warn").mockImplementation(() => {});
Expand All @@ -330,6 +337,7 @@ afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
window.localStorage.clear();
window.sessionStorage.clear();
});

describe("PluginNavSidebarItems", () => {
Expand Down Expand Up @@ -668,7 +676,7 @@ describe("PluginNavSidebarItems", () => {
root: {
type: "split",
dir: "row",
sizes: [1, 1, 1],
sizes: [1 / 3, 1 / 3, 1 / 3],
children: [
{
type: "pane",
Expand Down Expand Up @@ -1277,7 +1285,9 @@ describe("PluginNavSidebarItems", () => {
const layout = store.get(splitLayoutAtom)!;
expect(countPanes(layout.root)).toBe(2);
expect(
findPaneByContent(layout.root, { kind: "new-thread" }),
listPanes(layout.root).find(
(pane) => pane.content.kind === "new-thread",
),
).not.toBeNull();
expect(
findPaneByContent(layout.root, {
Expand Down
19 changes: 10 additions & 9 deletions apps/app/src/components/plugin/PluginNavSidebarItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export interface BuiltInSidebarNavEntry {
icon: ReactNode;
content: ReactNode;
disabled?: boolean;
splitContent?: PaneContent;
splitContent?: PaneContent | (() => PaneContent);
onActivate: (event: SidebarNavActivationModifiers) => void;
}

Expand Down Expand Up @@ -617,14 +617,15 @@ function SidebarNavigationOverflowItem({
}) {
const splitActions = usePaneContentSplitActions();
const [isActionsOpen, setIsActionsOpen] = useState(false);
const content: PaneContent | undefined = isPluginSidebarNavRow(row)
? {
kind: "plugin-panel",
pluginId: row.chrome.pluginId,
panelPath: row.chrome.path,
subPath: "",
}
: row.splitContent;
const content: PaneContent | (() => PaneContent) | undefined =
isPluginSidebarNavRow(row)
? {
kind: "plugin-panel",
pluginId: row.chrome.pluginId,
panelPath: row.chrome.path,
subPath: "",
}
: row.splitContent;
const disabled = !isPluginSidebarNavRow(row) && row.disabled;
const canSplit =
splitEnabled &&
Expand Down
Loading
Loading