Skip to content

Commit 5d656de

Browse files
authored
Add configurable browser preview start page (#421)
- Store a browser preview start page in app settings - Use it when opening blank preview tabs - Show validation and fallback behavior in Settings
1 parent f3f8fdc commit 5d656de

4 files changed

Lines changed: 110 additions & 2 deletions

File tree

apps/web/src/appSettings.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { describe, expect, it } from "vitest";
33

44
import {
55
AppSettingsSchema,
6+
DEFAULT_BROWSER_PREVIEW_START_PAGE_URL,
67
DEFAULT_PR_REVIEW_REQUEST_CHANGES_TONE,
78
DEFAULT_SIDEBAR_FONT_SIZE,
89
DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT,
910
DEFAULT_SIDEBAR_SPACING,
1011
DEFAULT_SIDEBAR_THREAD_ROW_HEIGHT,
12+
resolveBrowserPreviewStartPageUrl,
1113
} from "./appSettings";
1214

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

2325
expect(settings.showNotificationDetails).toBe(false);
2426
expect(settings.includeDiagnosticsTipsInCopy).toBe(false);
27+
expect(settings.browserPreviewStartPageUrl).toBe("");
2528
});
2629

2730
it("defaults sidebar appearance controls", () => {
@@ -57,3 +60,19 @@ describe("AppSettingsSchema", () => {
5760
expect(settings.prReviewRequestChangesTone).toBe(DEFAULT_PR_REVIEW_REQUEST_CHANGES_TONE);
5861
});
5962
});
63+
64+
describe("resolveBrowserPreviewStartPageUrl", () => {
65+
it("falls back to the default start page for blank or invalid values", () => {
66+
expect(resolveBrowserPreviewStartPageUrl("")).toBe(DEFAULT_BROWSER_PREVIEW_START_PAGE_URL);
67+
expect(resolveBrowserPreviewStartPageUrl("not-a-url")).toBe(
68+
DEFAULT_BROWSER_PREVIEW_START_PAGE_URL,
69+
);
70+
});
71+
72+
it("normalizes valid http and https URLs", () => {
73+
expect(resolveBrowserPreviewStartPageUrl(" https://example.com ")).toBe("https://example.com/");
74+
expect(resolveBrowserPreviewStartPageUrl("http://localhost:3000/path")).toBe(
75+
"http://localhost:3000/path",
76+
);
77+
});
78+
});

apps/web/src/appSettings.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
normalizeModelSlug,
1212
resolveSelectableModel,
1313
} from "@okcode/shared/model";
14+
import { validateHttpPreviewUrl } from "@okcode/shared/preview";
1415
import { APP_LOCALE_PREFERENCES } from "./i18n/types";
1516
import { useLocalStorage } from "./hooks/useLocalStorage";
1617
import { EnvMode } from "./components/BranchToolbar.logic";
@@ -32,6 +33,7 @@ export const DEFAULT_SIDEBAR_FONT_SIZE = 12;
3233
export const SIDEBAR_SPACING_MIN = 4;
3334
export const SIDEBAR_SPACING_MAX = 12;
3435
export const DEFAULT_SIDEBAR_SPACING = 8;
36+
export const DEFAULT_BROWSER_PREVIEW_START_PAGE_URL = "https://www.google.com/";
3537

3638
export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
3739
export type TimestampFormat = typeof TimestampFormat.Type;
@@ -96,6 +98,9 @@ export const AppSettingsSchema = Schema.Struct({
9698
includeDiagnosticsTipsInCopy: Schema.Boolean.pipe(withDefaults(() => false)),
9799
locale: AppLocale.pipe(withDefaults(() => DEFAULT_APP_LOCALE)),
98100
openLinksExternally: Schema.Boolean.pipe(withDefaults(() => false)),
101+
browserPreviewStartPageUrl: Schema.String.check(Schema.isMaxLength(4096)).pipe(
102+
withDefaults(() => ""),
103+
),
99104
sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(
100105
withDefaults(() => DEFAULT_SIDEBAR_PROJECT_SORT_ORDER),
101106
),
@@ -227,6 +232,7 @@ function normalizeAppSettings(settings: AppSettings): AppSettings {
227232
return {
228233
...settings,
229234
backgroundImageUrl: settings.backgroundImageUrl.trim(),
235+
browserPreviewStartPageUrl: settings.browserPreviewStartPageUrl.trim(),
230236
backgroundImageOpacity: clampBackgroundOpacity(settings.backgroundImageOpacity),
231237
sidebarOpacity: clampOpacity(settings.sidebarOpacity),
232238
sidebarProjectRowHeight: clampSidebarProjectRowHeight(settings.sidebarProjectRowHeight),
@@ -377,6 +383,16 @@ export function getProviderStartOptions(
377383
return Object.keys(providerOptions).length > 0 ? providerOptions : undefined;
378384
}
379385

386+
export function resolveBrowserPreviewStartPageUrl(rawUrl: string | null | undefined): string {
387+
const trimmedUrl = rawUrl?.trim() ?? "";
388+
if (trimmedUrl.length === 0) {
389+
return DEFAULT_BROWSER_PREVIEW_START_PAGE_URL;
390+
}
391+
392+
const validatedUrl = validateHttpPreviewUrl(trimmedUrl);
393+
return validatedUrl.ok ? validatedUrl.url : DEFAULT_BROWSER_PREVIEW_START_PAGE_URL;
394+
}
395+
380396
export function useAppSettings() {
381397
const [settings, setSettings] = useLocalStorage(
382398
APP_SETTINGS_STORAGE_KEY,

apps/web/src/components/PreviewPanel.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from "lucide-react";
2525

2626
import { validateHttpPreviewUrl } from "@okcode/shared/preview";
27+
import { resolveBrowserPreviewStartPageUrl, useAppSettings } from "~/appSettings";
2728
import { readDesktopPreviewBridge } from "~/desktopPreview";
2829
import {
2930
type BrowserPresetId,
@@ -145,6 +146,7 @@ function resolveViewportDimensions(
145146
}
146147

147148
export function PreviewPanel({ projectId, threadId, onClose }: PreviewPanelProps) {
149+
const { settings } = useAppSettings();
148150
const previewBridge = readDesktopPreviewBridge();
149151
const setProjectOpen = usePreviewStateStore((state) => state.setProjectOpen);
150152
const favoriteUrls = usePreviewStateStore((state) => state.favoriteUrls);
@@ -412,8 +414,10 @@ export function PreviewPanel({ projectId, threadId, onClose }: PreviewPanelProps
412414
return;
413415
}
414416
}
415-
// Create tab with a default page
416-
void previewBridge?.createTab({ url: "https://www.google.com", threadId });
417+
void previewBridge?.createTab({
418+
url: resolveBrowserPreviewStartPageUrl(settings.browserPreviewStartPageUrl),
419+
threadId,
420+
});
417421
};
418422

419423
const onClosePreview = () => {

apps/web/src/routes/_chat.settings.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ import {
3434
DEFAULT_GIT_TEXT_GENERATION_MODEL,
3535
} from "@okcode/contracts";
3636
import { getModelOptions, normalizeModelSlug } from "@okcode/shared/model";
37+
import { validateHttpPreviewUrl } from "@okcode/shared/preview";
3738
import {
39+
DEFAULT_BROWSER_PREVIEW_START_PAGE_URL,
3840
DEFAULT_SIDEBAR_FONT_SIZE,
3941
DEFAULT_SIDEBAR_PROJECT_ROW_HEIGHT,
4042
DEFAULT_SIDEBAR_SPACING,
@@ -46,6 +48,7 @@ import {
4648
MODEL_PROVIDER_SETTINGS,
4749
patchCustomModels,
4850
PrReviewRequestChangesTone,
51+
resolveBrowserPreviewStartPageUrl,
4952
SIDEBAR_FONT_SIZE_MAX,
5053
SIDEBAR_FONT_SIZE_MIN,
5154
SIDEBAR_PROJECT_ROW_HEIGHT_MAX,
@@ -758,6 +761,14 @@ function SettingsRouteView() {
758761
const { settings, defaults, updateSettings, resetSettings } = useAppSettings();
759762
const serverConfigQuery = useQuery(serverConfigQueryOptions());
760763
const queryClient = useQueryClient();
764+
const trimmedBrowserPreviewStartPageUrl = settings.browserPreviewStartPageUrl.trim();
765+
const browserPreviewStartPageValidation =
766+
trimmedBrowserPreviewStartPageUrl.length > 0
767+
? validateHttpPreviewUrl(trimmedBrowserPreviewStartPageUrl)
768+
: null;
769+
const effectiveBrowserPreviewStartPageUrl = resolveBrowserPreviewStartPageUrl(
770+
settings.browserPreviewStartPageUrl,
771+
);
761772
const projects = useStore((state) => state.projects);
762773
const threads = useStore((state) => state.threads);
763774
const [selectedProjectId, setSelectedProjectId] = useState<ProjectId | null>(
@@ -2133,6 +2144,64 @@ function SettingsRouteView() {
21332144
}
21342145
/>
21352146

2147+
<SettingsRow
2148+
title="Browser preview start page"
2149+
description="Used when opening a new browser preview tab without typing a URL first."
2150+
status={
2151+
trimmedBrowserPreviewStartPageUrl.length === 0 ? (
2152+
<>
2153+
Blank uses the default start page:{" "}
2154+
<code>{DEFAULT_BROWSER_PREVIEW_START_PAGE_URL}</code>
2155+
</>
2156+
) : browserPreviewStartPageValidation?.ok ? (
2157+
<>
2158+
New blank preview tabs will open at{" "}
2159+
<code>{browserPreviewStartPageValidation.url}</code>.
2160+
</>
2161+
) : (
2162+
<>
2163+
<span className="text-destructive">
2164+
Invalid URL. Falling back to{" "}
2165+
<code>{DEFAULT_BROWSER_PREVIEW_START_PAGE_URL}</code>.
2166+
</span>
2167+
<span className="mt-1 block break-all">
2168+
Effective start page:{" "}
2169+
<code>{effectiveBrowserPreviewStartPageUrl}</code>
2170+
</span>
2171+
</>
2172+
)
2173+
}
2174+
resetAction={
2175+
settings.browserPreviewStartPageUrl !==
2176+
defaults.browserPreviewStartPageUrl ? (
2177+
<SettingResetButton
2178+
label="browser preview start page"
2179+
onClick={() =>
2180+
updateSettings({
2181+
browserPreviewStartPageUrl: defaults.browserPreviewStartPageUrl,
2182+
})
2183+
}
2184+
/>
2185+
) : null
2186+
}
2187+
control={
2188+
<Input
2189+
value={settings.browserPreviewStartPageUrl}
2190+
onChange={(event) =>
2191+
updateSettings({
2192+
browserPreviewStartPageUrl: event.target.value,
2193+
})
2194+
}
2195+
placeholder={DEFAULT_BROWSER_PREVIEW_START_PAGE_URL}
2196+
aria-label="Browser preview start page"
2197+
autoCapitalize="off"
2198+
autoCorrect="off"
2199+
spellCheck={false}
2200+
className="w-full sm:w-72"
2201+
/>
2202+
}
2203+
/>
2204+
21362205
<SettingsRow
21372206
title="Code Preview Autosave"
21382207
description="Automatically save edits made in the built-in code preview after a short delay."

0 commit comments

Comments
 (0)