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
19 changes: 19 additions & 0 deletions apps/web/src/appSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { describe, expect, it } from "vitest";

import {
AppSettingsSchema,
DEFAULT_BROWSER_PREVIEW_START_PAGE_URL,
DEFAULT_PR_REVIEW_REQUEST_CHANGES_TONE,
DEFAULT_SIDEBAR_FONT_SIZE,
DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT,
DEFAULT_SIDEBAR_SPACING,
DEFAULT_SIDEBAR_THREAD_ROW_HEIGHT,
resolveBrowserPreviewStartPageUrl,
} from "./appSettings";

describe("AppSettingsSchema", () => {
Expand All @@ -22,6 +24,7 @@ describe("AppSettingsSchema", () => {

expect(settings.showNotificationDetails).toBe(false);
expect(settings.includeDiagnosticsTipsInCopy).toBe(false);
expect(settings.browserPreviewStartPageUrl).toBe("");
});

it("defaults sidebar appearance controls", () => {
Expand Down Expand Up @@ -57,3 +60,19 @@ describe("AppSettingsSchema", () => {
expect(settings.prReviewRequestChangesTone).toBe(DEFAULT_PR_REVIEW_REQUEST_CHANGES_TONE);
});
});

describe("resolveBrowserPreviewStartPageUrl", () => {
it("falls back to the default start page for blank or invalid values", () => {
expect(resolveBrowserPreviewStartPageUrl("")).toBe(DEFAULT_BROWSER_PREVIEW_START_PAGE_URL);
expect(resolveBrowserPreviewStartPageUrl("not-a-url")).toBe(
DEFAULT_BROWSER_PREVIEW_START_PAGE_URL,
);
});

it("normalizes valid http and https URLs", () => {
expect(resolveBrowserPreviewStartPageUrl(" https://example.com ")).toBe("https://example.com/");
expect(resolveBrowserPreviewStartPageUrl("http://localhost:3000/path")).toBe(
"http://localhost:3000/path",
);
});
});
16 changes: 16 additions & 0 deletions apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
normalizeModelSlug,
resolveSelectableModel,
} from "@okcode/shared/model";
import { validateHttpPreviewUrl } from "@okcode/shared/preview";
import { APP_LOCALE_PREFERENCES } from "./i18n/types";
import { useLocalStorage } from "./hooks/useLocalStorage";
import { EnvMode } from "./components/BranchToolbar.logic";
Expand All @@ -32,6 +33,7 @@ export const DEFAULT_SIDEBAR_FONT_SIZE = 12;
export const SIDEBAR_SPACING_MIN = 4;
export const SIDEBAR_SPACING_MAX = 12;
export const DEFAULT_SIDEBAR_SPACING = 8;
export const DEFAULT_BROWSER_PREVIEW_START_PAGE_URL = "https://www.google.com/";

export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
export type TimestampFormat = typeof TimestampFormat.Type;
Expand Down Expand Up @@ -96,6 +98,9 @@ export const AppSettingsSchema = Schema.Struct({
includeDiagnosticsTipsInCopy: Schema.Boolean.pipe(withDefaults(() => false)),
locale: AppLocale.pipe(withDefaults(() => DEFAULT_APP_LOCALE)),
openLinksExternally: Schema.Boolean.pipe(withDefaults(() => false)),
browserPreviewStartPageUrl: Schema.String.check(Schema.isMaxLength(4096)).pipe(
withDefaults(() => ""),
),
sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(
withDefaults(() => DEFAULT_SIDEBAR_PROJECT_SORT_ORDER),
),
Expand Down Expand Up @@ -227,6 +232,7 @@ function normalizeAppSettings(settings: AppSettings): AppSettings {
return {
...settings,
backgroundImageUrl: settings.backgroundImageUrl.trim(),
browserPreviewStartPageUrl: settings.browserPreviewStartPageUrl.trim(),
backgroundImageOpacity: clampBackgroundOpacity(settings.backgroundImageOpacity),
sidebarOpacity: clampOpacity(settings.sidebarOpacity),
sidebarProjectRowHeight: clampSidebarProjectRowHeight(settings.sidebarProjectRowHeight),
Expand Down Expand Up @@ -377,6 +383,16 @@ export function getProviderStartOptions(
return Object.keys(providerOptions).length > 0 ? providerOptions : undefined;
}

export function resolveBrowserPreviewStartPageUrl(rawUrl: string | null | undefined): string {
const trimmedUrl = rawUrl?.trim() ?? "";
if (trimmedUrl.length === 0) {
return DEFAULT_BROWSER_PREVIEW_START_PAGE_URL;
}

const validatedUrl = validateHttpPreviewUrl(trimmedUrl);
return validatedUrl.ok ? validatedUrl.url : DEFAULT_BROWSER_PREVIEW_START_PAGE_URL;
}

export function useAppSettings() {
const [settings, setSettings] = useLocalStorage(
APP_SETTINGS_STORAGE_KEY,
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/components/PreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "lucide-react";

import { validateHttpPreviewUrl } from "@okcode/shared/preview";
import { resolveBrowserPreviewStartPageUrl, useAppSettings } from "~/appSettings";
import { readDesktopPreviewBridge } from "~/desktopPreview";
import {
type BrowserPresetId,
Expand Down Expand Up @@ -145,6 +146,7 @@ function resolveViewportDimensions(
}

export function PreviewPanel({ projectId, threadId, onClose }: PreviewPanelProps) {
const { settings } = useAppSettings();
const previewBridge = readDesktopPreviewBridge();
const setProjectOpen = usePreviewStateStore((state) => state.setProjectOpen);
const favoriteUrls = usePreviewStateStore((state) => state.favoriteUrls);
Expand Down Expand Up @@ -412,8 +414,10 @@ export function PreviewPanel({ projectId, threadId, onClose }: PreviewPanelProps
return;
}
}
// Create tab with a default page
void previewBridge?.createTab({ url: "https://www.google.com", threadId });
void previewBridge?.createTab({
url: resolveBrowserPreviewStartPageUrl(settings.browserPreviewStartPageUrl),
threadId,
});
};

const onClosePreview = () => {
Expand Down
69 changes: 69 additions & 0 deletions apps/web/src/routes/_chat.settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ import {
DEFAULT_GIT_TEXT_GENERATION_MODEL,
} from "@okcode/contracts";
import { getModelOptions, normalizeModelSlug } from "@okcode/shared/model";
import { validateHttpPreviewUrl } from "@okcode/shared/preview";
import {
DEFAULT_BROWSER_PREVIEW_START_PAGE_URL,
DEFAULT_SIDEBAR_FONT_SIZE,
DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT,
DEFAULT_SIDEBAR_SPACING,
Expand All @@ -46,6 +48,7 @@ import {
MODEL_PROVIDER_SETTINGS,
patchCustomModels,
PrReviewRequestChangesTone,
resolveBrowserPreviewStartPageUrl,
SIDEBAR_FONT_SIZE_MAX,
SIDEBAR_FONT_SIZE_MIN,
SIDEBAR_PROJECT_ROW_HEIGHT_MAX,
Expand Down Expand Up @@ -758,6 +761,14 @@ function SettingsRouteView() {
const { settings, defaults, updateSettings, resetSettings } = useAppSettings();
const serverConfigQuery = useQuery(serverConfigQueryOptions());
const queryClient = useQueryClient();
const trimmedBrowserPreviewStartPageUrl = settings.browserPreviewStartPageUrl.trim();
const browserPreviewStartPageValidation =
trimmedBrowserPreviewStartPageUrl.length > 0
? validateHttpPreviewUrl(trimmedBrowserPreviewStartPageUrl)
: null;
const effectiveBrowserPreviewStartPageUrl = resolveBrowserPreviewStartPageUrl(
settings.browserPreviewStartPageUrl,
);
const projects = useStore((state) => state.projects);
const threads = useStore((state) => state.threads);
const [selectedProjectId, setSelectedProjectId] = useState<ProjectId | null>(
Expand Down Expand Up @@ -2133,6 +2144,64 @@ function SettingsRouteView() {
}
/>

<SettingsRow
title="Browser preview start page"
description="Used when opening a new browser preview tab without typing a URL first."
status={
trimmedBrowserPreviewStartPageUrl.length === 0 ? (
<>
Blank uses the default start page:{" "}
<code>{DEFAULT_BROWSER_PREVIEW_START_PAGE_URL}</code>
</>
) : browserPreviewStartPageValidation?.ok ? (
<>
New blank preview tabs will open at{" "}
<code>{browserPreviewStartPageValidation.url}</code>.
</>
) : (
<>
<span className="text-destructive">
Invalid URL. Falling back to{" "}
<code>{DEFAULT_BROWSER_PREVIEW_START_PAGE_URL}</code>.
</span>
<span className="mt-1 block break-all">
Effective start page:{" "}
<code>{effectiveBrowserPreviewStartPageUrl}</code>
</span>
</>
)
}
resetAction={
settings.browserPreviewStartPageUrl !==
defaults.browserPreviewStartPageUrl ? (
<SettingResetButton
label="browser preview start page"
onClick={() =>
updateSettings({
browserPreviewStartPageUrl: defaults.browserPreviewStartPageUrl,
})
}
/>
) : null
}
control={
<Input
value={settings.browserPreviewStartPageUrl}
onChange={(event) =>
updateSettings({
browserPreviewStartPageUrl: event.target.value,
})
}
placeholder={DEFAULT_BROWSER_PREVIEW_START_PAGE_URL}
aria-label="Browser preview start page"
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
className="w-full sm:w-72"
/>
}
/>

<SettingsRow
title="Code Preview Autosave"
description="Automatically save edits made in the built-in code preview after a short delay."
Expand Down
Loading