Skip to content
Open
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
5 changes: 4 additions & 1 deletion apps/desktop/src/app/DesktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
DesktopRuntimeArch,
DesktopRuntimeInfo,
} from "@t3tools/contracts";
import * as NodePath from "@effect/platform-node/NodePath";
import * as Config from "effect/Config";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -246,4 +247,6 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* (
});

export const layer = (input: MakeDesktopEnvironmentInput) =>
Layer.effect(DesktopEnvironment, makeDesktopEnvironment(input));
Layer.effect(DesktopEnvironment, makeDesktopEnvironment(input)).pipe(
Layer.provide(input.platform === "win32" ? NodePath.layerWin32 : NodePath.layerPosix),
);
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const clientSettings: ClientSettings = {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
contextMenuStyle: "default",
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
diffWordWrap: true,
Expand Down
54 changes: 54 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Link } from "@tanstack/react-router";
import { useCallback, useMemo, useRef, useState } from "react";
import {
defaultInstanceIdForDriver,
type ContextMenuStyle,
type DesktopUpdateChannel,
PROVIDER_DISPLAY_NAMES,
ProviderDriverKind,
Expand Down Expand Up @@ -101,6 +102,12 @@ const TIMESTAMP_FORMAT_LABELS = {
"24-hour": "24-hour",
} as const;

const CONTEXT_MENU_STYLE_LABELS = {
default: "Default",
native: "Native",
custom: "Custom",
} as const satisfies Record<ContextMenuStyle, string>;

const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex");

function withoutProviderInstanceKey<V>(
Expand Down Expand Up @@ -395,6 +402,9 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat
? ["Time format"]
: []),
...(settings.contextMenuStyle !== DEFAULT_UNIFIED_SETTINGS.contextMenuStyle
? ["Context menu style"]
: []),
...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount
? ["Visible threads"]
: []),
Expand Down Expand Up @@ -434,6 +444,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.confirmThreadArchive,
settings.confirmThreadDelete,
settings.addProjectBaseDirectory,
settings.contextMenuStyle,
settings.defaultThreadEnvMode,
settings.diffIgnoreWhitespace,
settings.diffWordWrap,
Expand All @@ -458,6 +469,7 @@ export function useSettingsRestore(onRestored?: () => void) {
setTheme("system");
updateSettings({
timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
contextMenuStyle: DEFAULT_UNIFIED_SETTINGS.contextMenuStyle,
diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap,
diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
Expand Down Expand Up @@ -594,6 +606,48 @@ export function GeneralSettingsPanel() {
}
/>

<SettingsRow
title="Context menus"
description="Default uses native desktop menus on macOS and custom menus elsewhere."
resetAction={
settings.contextMenuStyle !== DEFAULT_UNIFIED_SETTINGS.contextMenuStyle ? (
<SettingResetButton
label="context menu style"
onClick={() =>
updateSettings({
contextMenuStyle: DEFAULT_UNIFIED_SETTINGS.contextMenuStyle,
})
}
/>
) : null
}
control={
<Select
value={settings.contextMenuStyle}
onValueChange={(value) => {
if (value === "default" || value === "native" || value === "custom") {
updateSettings({ contextMenuStyle: value });
}
}}
>
<SelectTrigger className="w-full sm:w-40" aria-label="Context menu style">
<SelectValue>{CONTEXT_MENU_STYLE_LABELS[settings.contextMenuStyle]}</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
<SelectItem hideIndicator value="default">
{CONTEXT_MENU_STYLE_LABELS.default}
</SelectItem>
<SelectItem hideIndicator value="native">
{CONTEXT_MENU_STYLE_LABELS.native}
</SelectItem>
<SelectItem hideIndicator value="custom">
{CONTEXT_MENU_STYLE_LABELS.custom}
</SelectItem>
</SelectPopup>
</Select>
}
/>

<SettingsRow
title="Diff line wrapping"
description="Set the default wrap state when the diff panel opens."
Expand Down
13 changes: 11 additions & 2 deletions apps/web/src/localApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
CommandId,
DEFAULT_CLIENT_SETTINGS,
DEFAULT_SERVER_SETTINGS,
type DesktopBridge,
EnvironmentId,
Expand Down Expand Up @@ -591,9 +592,15 @@ describe("wsApi", () => {
});
});

it("forwards context menu metadata to the desktop bridge", async () => {
it("forwards context menu metadata to the desktop bridge for native menus", async () => {
const showContextMenu = vi.fn().mockResolvedValue("delete");
getWindowForTest().desktopBridge = makeDesktopBridge({ showContextMenu });
getWindowForTest().desktopBridge = makeDesktopBridge({
getClientSettings: async () => ({
...DEFAULT_CLIENT_SETTINGS,
contextMenuStyle: "native",
}),
showContextMenu,
});

const { createLocalApi } = await import("./localApi");
const api = createLocalApi(rpcClientMock as never);
Expand Down Expand Up @@ -632,6 +639,7 @@ describe("wsApi", () => {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
contextMenuStyle: "default" as const,
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
diffWordWrap: true,
Expand Down Expand Up @@ -695,6 +703,7 @@ describe("wsApi", () => {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
contextMenuStyle: "default" as const,
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
diffWordWrap: true,
Expand Down
39 changes: 36 additions & 3 deletions apps/web/src/localApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { ContextMenuItem, LocalApi } from "@t3tools/contracts";
import {
DEFAULT_CLIENT_SETTINGS,
type ContextMenuItem,
type ContextMenuStyle,
type LocalApi,
} from "@t3tools/contracts";
import type { WsRpcClient } from "@t3tools/client-runtime";

import { resetVcsStatusStateForTests } from "./lib/vcsStatusState";
Expand All @@ -25,13 +30,36 @@ import {
writeBrowserSavedEnvironmentRegistry,
writeBrowserSavedEnvironmentSecret,
} from "./clientPersistenceStorage";
import { isMacPlatform } from "./lib/utils";

let cachedApi: LocalApi | undefined;

function unavailableLocalBackendError(): Error {
return new Error("Local backend API is unavailable before a backend is paired.");
}

async function readContextMenuStyle(): Promise<ContextMenuStyle> {
try {
const settings = window.desktopBridge
? await window.desktopBridge.getClientSettings()
: readBrowserClientSettings();
return settings?.contextMenuStyle ?? DEFAULT_CLIENT_SETTINGS.contextMenuStyle;
} catch {
return DEFAULT_CLIENT_SETTINGS.contextMenuStyle;
}
}

function shouldUseNativeContextMenu(style: ContextMenuStyle): boolean {
if (style === "custom") {
return false;
}
if (style === "native") {
return true;
}
const platform = typeof navigator === "undefined" ? "" : navigator.platform;
return Boolean(window.desktopBridge) && isMacPlatform(platform);
}

function createBrowserLocalApi(rpcClient?: WsRpcClient): LocalApi {
return {
dialogs: {
Expand Down Expand Up @@ -68,8 +96,13 @@ function createBrowserLocalApi(rpcClient?: WsRpcClient): LocalApi {
items: readonly ContextMenuItem<T>[],
position?: { x: number; y: number },
): Promise<T | null> => {
if (window.desktopBridge) {
return window.desktopBridge.showContextMenu(items, position) as Promise<T | null>;
const style = await readContextMenuStyle();
if (shouldUseNativeContextMenu(style) && window.desktopBridge) {
try {
return (await window.desktopBridge.showContextMenu(items, position)) as T | null;
} catch {
return null;
}
}
return showContextMenuFallback(items, position);
},
Expand Down
8 changes: 8 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"])
export type TimestampFormat = typeof TimestampFormat.Type;
export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";

export const ContextMenuStyle = Schema.Literals(["default", "native", "custom"]);
export type ContextMenuStyle = typeof ContextMenuStyle.Type;
export const DEFAULT_CONTEXT_MENU_STYLE: ContextMenuStyle = "default";

export const SidebarProjectSortOrder = Schema.Literals(["updated_at", "created_at", "manual"]);
export type SidebarProjectSortOrder = typeof SidebarProjectSortOrder.Type;
export const DEFAULT_SIDEBAR_PROJECT_SORT_ORDER: SidebarProjectSortOrder = "updated_at";
Expand Down Expand Up @@ -43,6 +47,9 @@ export const ClientSettingsSchema = Schema.Struct({
autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
contextMenuStyle: ContextMenuStyle.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_CONTEXT_MENU_STYLE)),
),
dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe(
Schema.withDecodingDefault(Effect.succeed([])),
),
Expand Down Expand Up @@ -478,6 +485,7 @@ export const ClientSettingsPatch = Schema.Struct({
autoOpenPlanSidebar: Schema.optionalKey(Schema.Boolean),
confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
contextMenuStyle: Schema.optionalKey(ContextMenuStyle),
diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
diffWordWrap: Schema.optionalKey(Schema.Boolean),
favorites: Schema.optionalKey(
Expand Down
Loading