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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ NeoCode is a distinct product built from upstream T3 Code (`pingdotgg/t3code`),
| `apps/web/src/components/{desktop/DesktopAppActivationCoordinator,desktop/SshPasswordPromptDialog,onboarding/FirstRunGate,onboarding/WelcomeWizard,preview/PreviewPanel,cloud/RelayClientInstallDialog}.tsx`, `apps/web/src/components/{desktopUpdate.logic,desktopUpdate.logic.test}.ts` | NeoCode onboarding, connection, preview and update guidance; show NeoPi readiness first without changing enabled choices or initiating authentication; retain activation, recovery and update behavior |
| `apps/web/src/components/clerk/{MobileClientsUserProfilePage.logic,MobileClientsUserProfilePage.logic.test}.ts`, `apps/web/src/components/clerk/MobileClientsUserProfilePage.tsx`, `apps/web/src/connection/{clientMetadata,clientMetadata.test}.ts`, `apps/web/src/{cloud/linkEnvironment,desktopAppActivation}.ts` | NeoCode client and authorization display metadata; retain protocol fields, connection behavior and metadata assertions |
| `apps/web/src/components/settings/{BrowserImportWizard,CaptureShortcutConfig,ConnectionsSettings,IntegrationsSettings,KeybindingsSettings,LocalEnvironmentSetting,NotificationSettings,SettingsPanels,SnapShotSettings,SnapShotSetupDialog,ThemeImportDialog,ThemePreviewCircles}.tsx`, `apps/web/src/components/settings/providerStatus.ts`, `apps/web/src/routes/{_chat.index,_chat.pull-requests,_chat}.tsx` | Reuse web branding in settings, navigation, capture and provider guidance without changing entry points or saved choices |
| `apps/web/src/components/settings/FontFamilyPicker.tsx` | Classify installed monospace fonts only for an opened picker, in cancellable batches, instead of blocking Appearance on mount; the cache and scheduler live in fork-owned `installedMonospaceFonts.ts` |
| `packages/client-runtime/src/connection/compatibility.ts`, `packages/client-runtime/src/state/{pullRequestDiffHttp,pullRequestDiffHttp.test}.ts`, `packages/contracts/src/{browserImport,previewAutomation,pullRequest}.ts` | Neutral cross-client compatibility, rejected-session recovery and permission guidance; preserve schemas and protocol identities |
| `README.md`, `docs/user/install.md` | Identify NeoCode and upstream attribution; distinguish fork releases from upstream installers; explain NeoPi executable selection, retained capability diagnostics, per-instance discovery failures and Linux URL registration |
| `docs/operations/development.md` | Distinguish rebuilt source output from installed artifacts; document complete Linux source URL commands and their inherited-environment handling |
Expand Down
45 changes: 39 additions & 6 deletions apps/web/src/components/settings/FontFamilyPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { LegendList, type LegendListRef } from "@legendapp/list/react";
import { CheckIcon } from "lucide-react";
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { isMonospaceFamily, queryInstalledFontFamilies } from "../../appearanceFonts";
import { queryInstalledFontFamilies } from "../../appearanceFonts";
import { filterInstalledMonospaceFonts } from "../../installedMonospaceFonts";
import {
Combobox,
ComboboxEmpty,
Expand All @@ -14,6 +15,7 @@ import {
import { SelectButton } from "../ui/select";

const DEFAULT_FONT_VALUE = "__default__";
const EMPTY_FONT_FAMILIES: readonly string[] = [];

function supportsFontEnumeration(): boolean {
return (
Expand Down Expand Up @@ -139,10 +141,35 @@ export function FontFamilyPicker({
if (nextOpen) setQuery("");
};

const families = useMemo(() => {
if (enumeration.status !== "granted") return [];
return requireMonospace ? enumeration.families.filter(isMonospaceFamily) : enumeration.families;
}, [enumeration, requireMonospace]);
const [monospaceResult, setMonospaceResult] = useState<{
source: readonly string[];
families: readonly string[];
} | null>(null);
useEffect(() => {
// A closed picker must not measure every installed face during the
// Appearance mount. Stop scanning when it closes or the page unmounts.
if (!open || !requireMonospace || enumeration.status !== "granted") return;
const source = enumeration.families;
if (monospaceResult?.source === source) return;
const controller = new AbortController();
void filterInstalledMonospaceFonts(source, controller.signal).then((families) => {
if (!controller.signal.aborted) setMonospaceResult({ source, families });
});
return () => controller.abort();
}, [open, requireMonospace, enumeration, monospaceResult]);

const loadingMonospace =
requireMonospace &&
enumeration.status === "granted" &&
monospaceResult?.source !== enumeration.families;
const families =
enumeration.status !== "granted"
? EMPTY_FONT_FAMILIES
: requireMonospace
? monospaceResult?.source === enumeration.families
? monospaceResult.families
: EMPTY_FONT_FAMILIES
: enumeration.families;

const items = useMemo(() => {
const trimmedQuery = query.trim().toLowerCase();
Expand Down Expand Up @@ -212,7 +239,13 @@ export function FontFamilyPicker({
onChange={(event) => setQuery(event.target.value)}
/>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<ComboboxEmpty>No fonts found.</ComboboxEmpty>
{loadingMonospace ? (
<div role="status" className="px-3 py-2 text-sm text-muted-foreground">
Finding monospace fonts…
</div>
) : (
<ComboboxEmpty>No fonts found.</ComboboxEmpty>
)}
<div className="relative min-h-0 max-h-72 w-full flex-1 overflow-hidden">
<ComboboxListVirtualized>
<LegendList<string>
Expand Down
79 changes: 79 additions & 0 deletions apps/web/src/installedMonospaceFonts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";

import { isMonospaceFamily } from "./appearanceFonts";

vi.mock("./appearanceFonts", () => ({ isMonospaceFamily: vi.fn() }));

beforeEach(() => {
vi.resetModules();
vi.useFakeTimers();
vi.mocked(isMonospaceFamily).mockReset();
});

afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});

describe("installed monospace font discovery", () => {
it("lets other UI work run between font measurements and preserves family order", async () => {
const { filterInstalledMonospaceFonts } = await import("./installedMonospaceFonts");
let now = 0;
vi.spyOn(performance, "now").mockImplementation(() => now);
vi.mocked(isMonospaceFamily).mockImplementation((family) => {
now += 10;
return family !== "Proportional";
});
const result = filterInstalledMonospaceFonts(
["First Mono", "Proportional", "Last Mono"],
new AbortController().signal,
);
expect(isMonospaceFamily).not.toHaveBeenCalled();

let measuredBeforeInput = 0;
setTimeout(() => {
measuredBeforeInput = vi.mocked(isMonospaceFamily).mock.calls.length;
}, 0);
await vi.runAllTimersAsync();

expect(measuredBeforeInput).toBe(1);
expect(await result).toEqual(["First Mono", "Last Mono"]);
});

it("stops probing when a picker closes and reuses completed measurements on reopen", async () => {
const { filterInstalledMonospaceFonts } = await import("./installedMonospaceFonts");
const controller = new AbortController();
let now = 0;
vi.spyOn(performance, "now").mockImplementation(() => now);
vi.mocked(isMonospaceFamily).mockImplementation(() => {
now += 10;
return true;
});
const families = ["First Mono", "Second Mono", "Third Mono"];
const cancelled = filterInstalledMonospaceFonts(families, controller.signal);
setTimeout(() => controller.abort(), 0);
await vi.runAllTimersAsync();
await cancelled;
expect(isMonospaceFamily).toHaveBeenCalledTimes(1);

const reopened = filterInstalledMonospaceFonts(families, new AbortController().signal);
await vi.runAllTimersAsync();
expect(await reopened).toEqual(families);
expect(isMonospaceFamily).toHaveBeenCalledTimes(3);

const remounted = filterInstalledMonospaceFonts(families, new AbortController().signal);
await vi.runAllTimersAsync();
expect(await remounted).toEqual(families);
expect(isMonospaceFamily).toHaveBeenCalledTimes(3);
});

it("does not measure anything after unmounting before the first scan", async () => {
const { filterInstalledMonospaceFonts } = await import("./installedMonospaceFonts");
const controller = new AbortController();
const result = filterInstalledMonospaceFonts(["Unvisited Font"], controller.signal);
controller.abort();
await vi.runAllTimersAsync();
expect(await result).toEqual([]);
expect(isMonospaceFamily).not.toHaveBeenCalled();
});
});
40 changes: 40 additions & 0 deletions apps/web/src/installedMonospaceFonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isMonospaceFamily } from "./appearanceFonts";

// Installed families do not change during enumeration. Reuse measurements
// across the code and terminal pickers and across Appearance remounts.
const monospaceByFamily = new Map<string, boolean>();

/** Probe an opened picker without monopolizing the UI thread. */
export function filterInstalledMonospaceFonts(
families: readonly string[],
signal: AbortSignal,
): Promise<readonly string[]> {
return new Promise((resolve) => {
const matches: string[] = [];
let index = 0;
const scan = () => {
if (signal.aborted) {
resolve([]);
return;
}
const deadline = performance.now() + 8;
while (index < families.length) {
const family = families[index++]!;
let monospace = monospaceByFamily.get(family);
if (monospace === undefined) {
monospace = isMonospaceFamily(family);
monospaceByFamily.set(family, monospace);
}
if (monospace) matches.push(family);
if (performance.now() >= deadline) break;
}
if (index === families.length) {
resolve(matches);
} else {
setTimeout(scan, 0);
}
};
// Yield before the first measurement too, so opening paints immediately.
setTimeout(scan, 0);
});
}
Loading