Skip to content
Merged
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
22 changes: 10 additions & 12 deletions apps/web/src/lib/openGitHubUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DesktopBridge, NativeApi, ProjectId, ThreadId } from "@okcode/cont

import { readDesktopPreviewBridge } from "~/desktopPreview";
import { readNativeApi } from "~/nativeApi";
import { usePreviewStateStore } from "~/previewStateStore";
import { openUrlInAppBrowser } from "~/lib/openUrlInAppBrowser";

export interface OpenGitHubUrlInput {
url: string;
Expand Down Expand Up @@ -36,17 +36,15 @@ export function canOpenGitHubUrlInPreview(input: OpenGitHubUrlInput): boolean {
}

export async function openGitHubUrl(input: OpenGitHubUrlInput): Promise<"preview" | "external"> {
const previewBridge = input.previewBridge ?? readDesktopPreviewBridge();
const setPreviewOpen = input.setPreviewOpen ?? usePreviewStateStore.getState().setProjectOpen;

if (
isGitHubHttpUrl(input.url) &&
previewBridge !== null &&
input.projectId !== null &&
input.threadId !== null
) {
setPreviewOpen(input.projectId, true);
await previewBridge.createTab({ url: input.url, threadId: input.threadId });
if (isGitHubHttpUrl(input.url) && input.projectId !== null && input.threadId !== null) {
await openUrlInAppBrowser({
url: input.url,
projectId: input.projectId,
threadId: input.threadId,
previewBridge: input.previewBridge ?? readDesktopPreviewBridge(),
setPreviewOpen: input.setPreviewOpen,
nativeApi: input.nativeApi,
});
return "preview";
}

Expand Down
73 changes: 73 additions & 0 deletions apps/web/src/lib/openUrlInAppBrowser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it, vi } from "vitest";

import type { DesktopBridge, NativeApi, ProjectId, ThreadId } from "@okcode/contracts";

import { openUrlInAppBrowser } from "./openUrlInAppBrowser";

function projectId(value: string): ProjectId {
return value as ProjectId;
}

function threadId(value: string): ThreadId {
return value as ThreadId;
}

describe("openUrlInAppBrowser", () => {
it("opens in the desktop preview when project and thread ids are available", async () => {
const createTab = vi.fn<DesktopBridge["preview"]["createTab"]>().mockResolvedValue({
tabId: "tab-1",
state: { tabs: [], activeTabId: null, visible: false },
});
const setPreviewOpen = vi.fn();

const result = await openUrlInAppBrowser({
url: "https://tweakcn.com",
projectId: projectId("project-1"),
threadId: threadId("thread-1"),
previewBridge: { createTab } as unknown as DesktopBridge["preview"],
setPreviewOpen,
});

expect(result).toBe("preview");
expect(setPreviewOpen).toHaveBeenCalledWith(projectId("project-1"), true);
expect(createTab).toHaveBeenCalledWith({
url: "https://tweakcn.com",
threadId: threadId("thread-1"),
});
});

it("pops the preview out when requested", async () => {
const createTab = vi.fn<DesktopBridge["preview"]["createTab"]>().mockResolvedValue({
tabId: "tab-1",
state: { tabs: [], activeTabId: null, visible: false },
});
const popOut = vi.fn<DesktopBridge["preview"]["popOut"]>().mockResolvedValue(undefined);

const result = await openUrlInAppBrowser({
url: "https://tweakcn.com",
projectId: projectId("project-1"),
threadId: threadId("thread-1"),
previewBridge: { createTab, popOut } as unknown as DesktopBridge["preview"],
setPreviewOpen: vi.fn(),
popOut: true,
});

expect(result).toBe("popout");
expect(createTab).toHaveBeenCalledOnce();
expect(popOut).toHaveBeenCalledOnce();
});

it("falls back to an external open when preview context is unavailable", async () => {
const openExternal = vi.fn<NativeApi["shell"]["openExternal"]>().mockResolvedValue(undefined);

const result = await openUrlInAppBrowser({
url: "https://tweakcn.com",
projectId: null,
threadId: null,
nativeApi: { shell: { openExternal } } as unknown as NativeApi,
});

expect(result).toBe("external");
expect(openExternal).toHaveBeenCalledWith("https://tweakcn.com");
});
});
40 changes: 40 additions & 0 deletions apps/web/src/lib/openUrlInAppBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { DesktopBridge, NativeApi, ProjectId, ThreadId } from "@okcode/contracts";

import { readDesktopPreviewBridge } from "~/desktopPreview";
import { readNativeApi } from "~/nativeApi";
import { usePreviewStateStore } from "~/previewStateStore";

export interface OpenUrlInAppBrowserInput {
url: string;
projectId: ProjectId | null;
threadId: ThreadId | null;
nativeApi?: NativeApi | undefined;
previewBridge?: DesktopBridge["preview"] | null | undefined;
setPreviewOpen?: ((projectId: ProjectId, open: boolean) => void) | undefined;
popOut?: boolean | undefined;
}

export async function openUrlInAppBrowser(
input: OpenUrlInAppBrowserInput,
): Promise<"preview" | "popout" | "external"> {
const previewBridge = input.previewBridge ?? readDesktopPreviewBridge();
const setPreviewOpen = input.setPreviewOpen ?? usePreviewStateStore.getState().setProjectOpen;

if (previewBridge !== null && input.projectId !== null && input.threadId !== null) {
setPreviewOpen(input.projectId, true);
await previewBridge.createTab({ url: input.url, threadId: input.threadId });
if (input.popOut) {
await previewBridge.popOut();
return "popout";
}
return "preview";
}

const nativeApi = input.nativeApi ?? readNativeApi();
if (!nativeApi) {
throw new Error("Link opening is unavailable.");
}

await nativeApi.shell.openExternal(input.url);
return "external";
}
42 changes: 42 additions & 0 deletions apps/web/src/routes/_chat.settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CheckCircle2Icon,
ChevronDownIcon,
CpuIcon,
GlobeIcon,
GitBranchIcon,
ImportIcon,
KeyboardIcon,
Expand Down Expand Up @@ -95,6 +96,7 @@ import {
setStoredRadiusOverride,
type CustomThemeData,
} from "../lib/customTheme";
import { openUrlInAppBrowser } from "../lib/openUrlInAppBrowser";
import { serverConfigQueryOptions, serverQueryKeys } from "../lib/serverReactQuery";
import { cn } from "../lib/utils";
import { ensureNativeApi, readNativeApi } from "../nativeApi";
Expand Down Expand Up @@ -598,6 +600,7 @@ function SettingsRouteView() {
const serverConfigQuery = useQuery(serverConfigQueryOptions());
const queryClient = useQueryClient();
const projects = useStore((state) => state.projects);
const threads = useStore((state) => state.threads);
const [selectedProjectId, setSelectedProjectId] = useState<ProjectId | null>(
() => projects[0]?.id ?? null,
);
Expand Down Expand Up @@ -644,6 +647,15 @@ function SettingsRouteView() {
const selectedProjectEnvironmentVariablesQuery = useQuery(
projectEnvironmentVariablesQueryOptions(activeProjectId),
);
const activeProjectPreviewThreadId =
activeProjectId === null
? null
: (threads
.filter((thread) => thread.projectId === activeProjectId)
.toSorted((a, b) =>
(b.updatedAt ?? b.createdAt).localeCompare(a.updatedAt ?? a.createdAt),
)
.at(0)?.id ?? null);

useEffect(() => {
if (projects.length === 0) {
Expand Down Expand Up @@ -776,6 +788,19 @@ function SettingsRouteView() {
...(fontSizeOverride !== null ? ["Code font size"] : []),
];

const openTweakcn = useCallback(() => {
void openUrlInAppBrowser({
url: "https://tweakcn.com",
projectId: activeProjectId,
threadId: activeProjectPreviewThreadId,
popOut: true,
nativeApi: readNativeApi(),
}).catch(() => {
const nativeApi = ensureNativeApi();
return nativeApi.shell.openExternal("https://tweakcn.com");
});
}, [activeProjectId, activeProjectPreviewThreadId]);

const openKeybindingsFile = useCallback(() => {
if (!keybindingsConfigPath) return;
setOpenKeybindingsError(null);
Expand Down Expand Up @@ -1140,6 +1165,23 @@ function SettingsRouteView() {
))}
</SelectPopup>
</Select>
<Tooltip>
<TooltipTrigger
render={
<Button
size="xs"
variant="outline"
onClick={openTweakcn}
aria-label="Open tweakcn"
>
<GlobeIcon className="size-3.5" />
</Button>
}
/>
<TooltipPopup side="top">
Open tweakcn in the in-app browser
</TooltipPopup>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
Expand Down
Loading