Add cookie editing and inherited request settings

This commit is contained in:
Gregory Schier
2026-05-17 07:58:12 -07:00
parent dcfdf077e7
commit dc47b54b1c
42 changed files with 3789 additions and 932 deletions
@@ -7,12 +7,18 @@ import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
import { appInfo } from "../../lib/appInfo";
import { revealInFinderText } from "../../lib/reveal";
import { CargoFeature } from "../CargoFeature";
import { Checkbox } from "../core/Checkbox";
import { IconButton } from "../core/IconButton";
import { KeyValueRow, KeyValueRows } from "../core/KeyValueRow";
import { PlainInput } from "../core/PlainInput";
import { Select } from "../core/Select";
import { Separator } from "../core/Separator";
import {
ModelSettingRowBoolean,
ModelSettingRowNumber,
ModelSettingSelectControl,
SettingValue,
SettingRow,
SettingRowBoolean,
SettingRowSelect,
SettingsList,
SettingsSection,
} from "../core/SettingRow";
export function SettingsGeneral() {
const workspace = useAtomValue(activeWorkspaceAtom);
@@ -29,147 +35,159 @@ export function SettingsGeneral() {
<Heading>General</Heading>
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
</div>
<CargoFeature feature="updater">
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
<Select
name="updateChannel"
label="Update Channel"
labelPosition="left"
labelClassName="w-[14rem]"
size="sm"
value={settings.updateChannel}
onChange={(updateChannel) => patchModel(settings, { updateChannel })}
options={[
{ label: "Stable", value: "stable" },
{ label: "Beta (more frequent)", value: "beta" },
]}
/>
<IconButton
variant="border"
size="sm"
title="Check for updates"
icon="refresh"
spin={checkForUpdates.isPending}
onClick={() => checkForUpdates.mutateAsync()}
/>
</div>
<SettingsList className="space-y-8">
<CargoFeature feature="updater">
<SettingsSection title="Updates">
<SettingRow
title="Update Channel"
description="Choose whether Yaak should use stable releases or beta releases."
>
<div className="grid grid-cols-[12rem_auto] gap-1">
<ModelSettingSelectControl
model={settings}
modelKey="updateChannel"
label="Update Channel"
selectClassName="!w-full"
options={[
{ label: "Stable", value: "stable" },
{ label: "Beta", value: "beta" },
]}
/>
<IconButton
variant="border"
size="sm"
title="Check for updates"
icon="refresh"
spin={checkForUpdates.isPending}
onClick={() => checkForUpdates.mutateAsync()}
/>
</div>
</SettingRow>
<Select
name="autoupdate"
value={settings.autoupdate ? "auto" : "manual"}
label="Update Behavior"
labelPosition="left"
size="sm"
labelClassName="w-[14rem]"
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
options={[
{ label: "Automatic", value: "auto" },
{ label: "Manual", value: "manual" },
]}
/>
<Checkbox
className="pl-2 mt-1 ml-[14rem]"
checked={settings.autoDownloadUpdates}
disabled={!settings.autoupdate}
help="Automatically download Yaak updates (!50MB) in the background, so they will be immediately ready to install."
title="Automatically download updates"
onChange={(autoDownloadUpdates) => patchModel(settings, { autoDownloadUpdates })}
/>
<Checkbox
className="pl-2 mt-1 ml-[14rem]"
checked={settings.checkNotifications}
title="Check for notifications"
help="Periodically ping Yaak servers to check for relevant notifications."
onChange={(checkNotifications) => patchModel(settings, { checkNotifications })}
/>
<Checkbox
disabled
className="pl-2 mt-1 ml-[14rem]"
checked={false}
title="Send anonymous usage statistics"
help="Yaak is local-first and does not collect analytics or usage data 🔐"
onChange={(checkNotifications) => patchModel(settings, { checkNotifications })}
/>
</CargoFeature>
<Separator className="my-4" />
<Heading level={2}>
Workspace{" "}
<div className="inline-block ml-1 bg-surface-highlight px-2 py-0.5 rounded text text-shrink">
{workspace.name}
</div>
</Heading>
<VStack className="mt-1 w-full" space={3}>
<PlainInput
required
size="sm"
name="requestTimeout"
label="Request Timeout (ms)"
labelClassName="w-[14rem]"
placeholder="0"
labelPosition="left"
defaultValue={`${workspace.settingRequestTimeout}`}
validate={(value) => Number.parseInt(value, 10) >= 0}
onChange={(v) =>
patchModel(workspace, { settingRequestTimeout: Number.parseInt(v, 10) || 0 })
}
type="number"
/>
<Checkbox
checked={workspace.settingValidateCertificates}
help="When disabled, skip validation of server certificates, useful when interacting with self-signed certs."
title="Validate TLS certificates"
onChange={(settingValidateCertificates) =>
patchModel(workspace, { settingValidateCertificates })
}
/>
<Checkbox
checked={workspace.settingFollowRedirects}
title="Follow redirects"
onChange={(settingFollowRedirects) =>
patchModel(workspace, {
settingFollowRedirects,
})
}
/>
</VStack>
<Separator className="my-4" />
<Heading level={2}>App Info</Heading>
<KeyValueRows>
<KeyValueRow label="Version">{appInfo.version}</KeyValueRow>
<KeyValueRow
label="Data Directory"
rightSlot={
<IconButton
title={revealInFinderText}
icon="folder_open"
size="2xs"
onClick={() => revealItemInDir(appInfo.appDataDir)}
<SettingRowSelect
title="Update Behavior"
description="Choose whether updates are installed automatically or manually."
name="autoupdate"
value={settings.autoupdate ? "auto" : "manual"}
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
options={[
{ label: "Automatic", value: "auto" },
{ label: "Manual", value: "manual" },
]}
/>
<ModelSettingRowBoolean
model={settings}
modelKey="autoDownloadUpdates"
title="Automatically download updates"
description="Download Yaak updates in the background so they are ready to install."
disabled={!settings.autoupdate}
/>
<ModelSettingRowBoolean
model={settings}
modelKey="checkNotifications"
title="Check for notifications"
description="Periodically ping Yaak servers to check for relevant notifications."
/>
<SettingRowBoolean
title="Send anonymous usage statistics"
description="Yaak is local-first and does not collect analytics or usage data."
disabled
checked={false}
onChange={() => {}}
/>
</SettingsSection>
</CargoFeature>
<SettingsSection
title={
<>
Workspace{" "}
<span className="inline-block bg-surface-highlight px-2 py-0.5 rounded text">
{workspace.name}
</span>
</>
}
>
{appInfo.appDataDir}
</KeyValueRow>
<KeyValueRow
label="Logs Directory"
rightSlot={
<IconButton
title={revealInFinderText}
icon="folder_open"
size="2xs"
onClick={() => revealItemInDir(appInfo.appLogDir)}
<ModelSettingRowNumber
model={workspace}
modelKey="settingRequestTimeout"
title="Request Timeout"
description="Maximum request duration in milliseconds. Set to 0 to disable the timeout."
placeholder="0"
required
validate={(value) => Number.parseInt(value, 10) >= 0}
/>
<ModelSettingRowBoolean
model={workspace}
modelKey="settingValidateCertificates"
title="Validate TLS certificates"
description="When disabled, skip validation of server certificates."
/>
<ModelSettingRowBoolean
model={workspace}
modelKey="settingFollowRedirects"
title="Follow redirects"
description="Follow HTTP redirects automatically."
/>
<ModelSettingRowBoolean
model={workspace}
modelKey="settingSendCookies"
title="Automatically send cookies"
description="Attach matching cookies from the active cookie jar to outgoing requests."
/>
<ModelSettingRowBoolean
model={workspace}
modelKey="settingStoreCookies"
title="Automatically store cookies"
description="Save cookies from Set-Cookie response headers to the active cookie jar."
/>
</SettingsSection>
<SettingsSection title="App Info">
<SettingRow title="Version" description="Current Yaak version.">
<SettingValue value={appInfo.version} />
</SettingRow>
<SettingRow
title="Data Directory"
description="Where Yaak stores application data."
controlClassName="min-w-0 max-w-[min(42rem,55vw)] gap-2"
>
<SettingValue
value={appInfo.appDataDir}
actions={[
{
title: revealInFinderText,
icon: "folder_open",
onClick: () => revealItemInDir(appInfo.appDataDir),
},
]}
/>
}
>
{appInfo.appLogDir}
</KeyValueRow>
</KeyValueRows>
</SettingRow>
<SettingRow
title="Logs Directory"
description="Where Yaak writes application logs."
controlClassName="min-w-0 max-w-[min(42rem,55vw)] gap-2"
>
<SettingValue
value={appInfo.appLogDir}
actions={[
{
title: revealInFinderText,
icon: "folder_open",
onClick: () => revealItemInDir(appInfo.appLogDir),
},
]}
/>
</SettingRow>
</SettingsSection>
</SettingsList>
</VStack>
);
}
@@ -3,7 +3,7 @@ import { useFonts } from "@yaakapp-internal/fonts";
import { useLicense } from "@yaakapp-internal/license";
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { clamp, Heading, HStack, Icon, VStack } from "@yaakapp-internal/ui";
import { clamp, Heading, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
@@ -13,7 +13,16 @@ import { CargoFeature } from "../CargoFeature";
import { Button } from "../core/Button";
import { Checkbox } from "../core/Checkbox";
import { Link } from "../core/Link";
import { Select } from "../core/Select";
import {
ModelSettingRowBoolean,
ModelSettingRowSelect,
SettingRow,
SettingRowBoolean,
SettingRowSelect,
SettingSelectControl,
SettingsList,
SettingsSection,
} from "../core/SettingRow";
const NULL_FONT_VALUE = "__NULL_FONT__";
@@ -38,154 +47,172 @@ export function SettingsInterface() {
}
return (
<VStack space={3} className="mb-4">
<VStack space={1.5} className="mb-4">
<div className="mb-3">
<Heading>Interface</Heading>
<p className="text-text-subtle">Tweak settings related to the user interface.</p>
</div>
<Select
name="switchWorkspaceBehavior"
label="Open workspace behavior"
size="sm"
help="When opening a workspace, should it open in the current window or a new window?"
value={
settings.openWorkspaceNewWindow === true
? "new"
: settings.openWorkspaceNewWindow === false
? "current"
: "ask"
}
onChange={async (v) => {
if (v === "current") await patchModel(settings, { openWorkspaceNewWindow: false });
else if (v === "new") await patchModel(settings, { openWorkspaceNewWindow: true });
else await patchModel(settings, { openWorkspaceNewWindow: null });
}}
options={[
{ label: "Always ask", value: "ask" },
{ label: "Open in current window", value: "current" },
{ label: "Open in new window", value: "new" },
]}
/>
<HStack space={2} alignItems="end">
{fonts.data && (
<Select
size="sm"
name="uiFont"
label="Interface font"
value={settings.interfaceFont ?? NULL_FONT_VALUE}
options={[
{ label: "System default", value: NULL_FONT_VALUE },
...(fonts.data.uiFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
// Some people like monospace fonts for the UI
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
]}
<SettingsList className="space-y-8">
<SettingsSection title="Workspaces">
<SettingRowSelect
title="Open workspace behavior"
description="Choose what happens when opening another workspace."
name="switchWorkspaceBehavior"
value={
settings.openWorkspaceNewWindow === true
? "new"
: settings.openWorkspaceNewWindow === false
? "current"
: "ask"
}
onChange={async (v) => {
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { interfaceFont });
if (v === "current") await patchModel(settings, { openWorkspaceNewWindow: false });
else if (v === "new") await patchModel(settings, { openWorkspaceNewWindow: true });
else await patchModel(settings, { openWorkspaceNewWindow: null });
}}
/>
)}
<Select
hideLabel
size="sm"
name="interfaceFontSize"
label="Interface Font Size"
defaultValue="14"
value={`${settings.interfaceFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { interfaceFontSize: Number.parseInt(v, 10) })}
/>
</HStack>
<HStack space={2} alignItems="end">
{fonts.data && (
<Select
size="sm"
name="editorFont"
label="Editor font"
value={settings.editorFont ?? NULL_FONT_VALUE}
options={[
{ label: "System default", value: NULL_FONT_VALUE },
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
{ label: "Always ask", value: "ask" },
{ label: "Open in current window", value: "current" },
{ label: "Open in new window", value: "new" },
]}
onChange={async (v) => {
const editorFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { editorFont });
}}
/>
)}
<Select
hideLabel
size="sm"
name="editorFontSize"
label="Editor Font Size"
defaultValue="12"
value={`${settings.editorFontSize}`}
options={fontSizeOptions}
onChange={(v) =>
patchModel(settings, { editorFontSize: clamp(Number.parseInt(v, 10) || 14, 8, 30) })
}
/>
</HStack>
<Select
leftSlot={<Icon icon="keyboard" color="secondary" />}
size="sm"
name="editorKeymap"
label="Editor keymap"
value={`${settings.editorKeymap}`}
options={keymaps}
onChange={(v) => patchModel(settings, { editorKeymap: v })}
/>
<Checkbox
checked={settings.editorSoftWrap}
title="Wrap editor lines"
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
<Checkbox
checked={settings.coloredMethods}
title="Colorize request methods"
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
/>
<CargoFeature feature="license">
<LicenseSettings settings={settings} />
</CargoFeature>
</SettingsSection>
<NativeTitlebarSetting settings={settings} />
<SettingsSection title="Fonts">
<SettingRow
title="Interface font"
description="Font used for Yaak interface controls."
controlClassName="gap-1"
>
{fonts.data && (
<SettingSelectControl
name="uiFont"
label="Interface font"
selectClassName="!w-72"
value={settings.interfaceFont ?? NULL_FONT_VALUE}
defaultValue={NULL_FONT_VALUE}
options={[
{ label: "System default", value: NULL_FONT_VALUE },
...fonts.data.uiFonts.map((f) => ({ label: f, value: f })),
...fonts.data.editorFonts.map((f) => ({ label: f, value: f })),
]}
onChange={async (v) => {
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { interfaceFont });
}}
/>
)}
<SettingSelectControl
name="interfaceFontSize"
label="Interface Font Size"
selectClassName="!w-20"
value={`${settings.interfaceFontSize}`}
defaultValue="14"
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { interfaceFontSize: Number.parseInt(v, 10) })}
/>
</SettingRow>
{type() !== "macos" && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide window controls"
help="Hide the close/maximize/minimize controls on Windows or Linux"
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
/>
)}
<SettingRow
title="Editor font"
description="Font used in request and response editors."
controlClassName="gap-1"
>
{fonts.data && (
<SettingSelectControl
name="editorFont"
label="Editor font"
selectClassName="!w-72"
value={settings.editorFont ?? NULL_FONT_VALUE}
defaultValue={NULL_FONT_VALUE}
options={[
{ label: "System default", value: NULL_FONT_VALUE },
...fonts.data.editorFonts.map((f) => ({ label: f, value: f })),
]}
onChange={async (v) => {
const editorFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { editorFont });
}}
/>
)}
<SettingSelectControl
name="editorFontSize"
label="Editor Font Size"
selectClassName="!w-20"
value={`${settings.editorFontSize}`}
defaultValue="12"
options={fontSizeOptions}
onChange={(v) =>
patchModel(settings, {
editorFontSize: clamp(Number.parseInt(v, 10) || 14, 8, 30),
})
}
/>
</SettingRow>
</SettingsSection>
<SettingsSection title="Editor">
<ModelSettingRowSelect
model={settings}
modelKey="editorKeymap"
title="Editor keymap"
description="Keyboard shortcut preset used by text editors."
options={keymaps}
/>
<ModelSettingRowBoolean
model={settings}
modelKey="editorSoftWrap"
title="Wrap editor lines"
description="Wrap long lines in request and response editors."
/>
<ModelSettingRowBoolean
model={settings}
modelKey="coloredMethods"
title="Colorize request methods"
description="Use method-specific colors for HTTP request methods."
/>
</SettingsSection>
<SettingsSection title="Window">
<NativeTitlebarSetting settings={settings} />
{type() !== "macos" && (
<ModelSettingRowBoolean
model={settings}
modelKey="hideWindowControls"
title="Hide window controls"
description="Hide the close, maximize, and minimize controls on Windows or Linux."
/>
)}
</SettingsSection>
<CargoFeature feature="license">
<LicenseSettings settings={settings} />
</CargoFeature>
</SettingsList>
</VStack>
);
}
function NativeTitlebarSetting({ settings }: { settings: Settings }) {
const [nativeTitlebar, setNativeTitlebar] = useState(settings.useNativeTitlebar);
return (
<div className="flex gap-1 overflow-hidden h-2xs">
<SettingRow
title="Native title bar"
description="Use the operating system's standard title bar and window controls."
controlClassName="gap-2"
>
<Checkbox
hideLabel
size="md"
checked={nativeTitlebar}
title="Native title bar"
help="Use the operating system's standard title bar and window controls"
onChange={setNativeTitlebar}
/>
{settings.useNativeTitlebar !== nativeTitlebar && (
<Button
color="primary"
size="2xs"
size="xs"
onClick={async () => {
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
await invokeCmd("cmd_restart");
@@ -194,7 +221,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
Apply and Restart
</Button>
)}
</div>
</SettingRow>
);
}
@@ -205,37 +232,40 @@ function LicenseSettings({ settings }: { settings: Settings }) {
}
return (
<Checkbox
checked={settings.hideLicenseBadge}
title="Hide personal use badge"
onChange={async (hideLicenseBadge) => {
if (hideLicenseBadge) {
const confirmed = await showConfirm({
id: "hide-license-badge",
title: "Confirm Personal Use",
confirmText: "Confirm",
description: (
<VStack space={3}>
<p>Hey there 👋🏼</p>
<p>
Yaak is free for personal projects and learning.{" "}
<strong>If youre using Yaak at work, a license is required.</strong>
</p>
<p>
Licenses help keep Yaak independent and sustainable.{" "}
<Link href="https://yaak.app/pricing?s=badge">Purchase a License </Link>
</p>
</VStack>
),
requireTyping: "Personal Use",
color: "info",
});
if (!confirmed) {
return; // Cancel
<SettingsSection title="License">
<SettingRowBoolean
checked={settings.hideLicenseBadge}
title="Hide personal use badge"
description="Hide the personal-use badge from the interface."
onChange={async (hideLicenseBadge) => {
if (hideLicenseBadge) {
const confirmed = await showConfirm({
id: "hide-license-badge",
title: "Confirm Personal Use",
confirmText: "Confirm",
description: (
<VStack space={3}>
<p>Hey there 👋🏼</p>
<p>
Yaak is free for personal projects and learning.{" "}
<strong>If youre using Yaak at work, a license is required.</strong>
</p>
<p>
Licenses help keep Yaak independent and sustainable.{" "}
<Link href="https://yaak.app/pricing?s=badge">Purchase a License </Link>
</p>
</VStack>
),
requireTyping: "Personal Use",
color: "info",
});
if (!confirmed) {
return;
}
}
}
await patchModel(settings, { hideLicenseBadge });
}}
/>
await patchModel(settings, { hideLicenseBadge });
}}
/>
</SettingsSection>
);
}
@@ -1,13 +1,28 @@
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
import type { ProxySetting } from "@yaakapp-internal/models";
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai";
import { Checkbox } from "../core/Checkbox";
import { PlainInput } from "../core/PlainInput";
import { Select } from "../core/Select";
import { Separator } from "../core/Separator";
import {
SettingRowBoolean,
SettingRowSelect,
SettingRowText,
SettingsList,
SettingsSection,
} from "../core/SettingRow";
export function SettingsProxy() {
const settings = useAtomValue(settingsAtom);
const proxy = enabledProxyOrDefault(settings.proxy);
const patchProxy = async (patch: Partial<EnabledProxySetting>) => {
await patchModel(settings, {
proxy: {
...proxy,
...patch,
auth: Object.hasOwn(patch, "auth") ? (patch.auth ?? null) : proxy.auth,
},
});
};
return (
<VStack space={1.5} className="mb-4">
@@ -18,188 +33,146 @@ export function SettingsProxy() {
traffic, or routing through specific infrastructure.
</p>
</div>
<Select
name="proxy"
label="Proxy"
hideLabel
size="sm"
value={settings.proxy?.type ?? "automatic"}
onChange={async (v) => {
if (v === "automatic") {
await patchModel(settings, { proxy: undefined });
} else if (v === "enabled") {
await patchModel(settings, {
proxy: {
disabled: false,
type: "enabled",
http: "",
https: "",
auth: { user: "", password: "" },
bypass: "",
},
});
} else {
await patchModel(settings, { proxy: { type: "disabled" } });
}
}}
options={[
{ label: "Automatic proxy detection", value: "automatic" },
{ label: "Custom proxy configuration", value: "enabled" },
{ label: "No proxy", value: "disabled" },
]}
/>
{settings.proxy?.type === "enabled" && (
<VStack space={1.5}>
<Checkbox
className="my-3"
checked={!settings.proxy.disabled}
title="Enable proxy"
help="Use this to temporarily disable the proxy without losing the configuration"
onChange={async (enabled) => {
const { proxy } = settings;
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const auth = proxy?.type === "enabled" ? proxy.auth : null;
const disabled = !enabled;
await patchModel(settings, {
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
/>
<HStack space={1.5}>
<PlainInput
size="sm"
label={
<>
Proxy for <InlineCode>http://</InlineCode> traffic
</>
<SettingsList className="space-y-8">
<SettingsSection title="Proxy">
<SettingRowSelect
title="Proxy"
description="Choose how Yaak should discover or use proxy settings."
name="proxy"
value={settings.proxy?.type ?? "automatic"}
onChange={async (v) => {
if (v === "automatic") {
await patchModel(settings, { proxy: undefined });
} else if (v === "enabled") {
await patchModel(settings, { proxy });
} else {
await patchModel(settings, { proxy: { type: "disabled" } });
}
placeholder="localhost:9090"
defaultValue={settings.proxy?.http}
onChange={async (http) => {
const { proxy } = settings;
const https = proxy?.type === "enabled" ? proxy.https : "";
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const auth = proxy?.type === "enabled" ? proxy.auth : null;
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
await patchModel(settings, {
proxy: {
type: "enabled",
http,
https,
auth,
disabled,
bypass,
},
});
}}
/>
<PlainInput
size="sm"
label={
<>
Proxy for <InlineCode>https://</InlineCode> traffic
</>
}
placeholder="localhost:9090"
defaultValue={settings.proxy?.https}
onChange={async (https) => {
const { proxy } = settings;
const http = proxy?.type === "enabled" ? proxy.http : "";
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const auth = proxy?.type === "enabled" ? proxy.auth : null;
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
await patchModel(settings, {
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
/>
</HStack>
<Separator className="my-6" />
<Checkbox
checked={settings.proxy.auth != null}
title="Enable authentication"
onChange={async (enabled) => {
const { proxy } = settings;
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const auth = enabled ? { user: "", password: "" } : null;
await patchModel(settings, {
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
options={[
{ label: "Automatic proxy detection", value: "automatic" },
{ label: "Custom proxy configuration", value: "enabled" },
{ label: "No proxy", value: "disabled" },
]}
selectClassName="!w-64"
/>
</SettingsSection>
{settings.proxy.auth != null && (
<HStack space={1.5}>
<PlainInput
required
size="sm"
label="User"
placeholder="myUser"
defaultValue={settings.proxy.auth.user}
onChange={async (user) => {
const { proxy } = settings;
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const password = proxy?.type === "enabled" ? (proxy.auth?.password ?? "") : "";
const auth = { user, password };
await patchModel(settings, {
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
{settings.proxy?.type === "enabled" && (
<>
<SettingsSection title="Custom Proxy">
<SettingRowBoolean
checked={!settings.proxy.disabled}
title="Enable proxy"
description="Temporarily disable the proxy without losing the configuration."
onChange={(enabled) => patchProxy({ disabled: !enabled })}
/>
<PlainInput
size="sm"
label="Password"
type="password"
placeholder="s3cretPassw0rd"
defaultValue={settings.proxy.auth.password}
onChange={async (password) => {
const { proxy } = settings;
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const user = proxy?.type === "enabled" ? (proxy.auth?.user ?? "") : "";
const auth = { user, password };
await patchModel(settings, {
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
<SettingRowText
name="proxyHttp"
title={
<>
Proxy for <InlineCode>http://</InlineCode> traffic
</>
}
description="Proxy host used for unencrypted HTTP traffic."
value={settings.proxy.http}
placeholder="localhost:9090"
onChange={(http) => patchProxy({ http })}
/>
</HStack>
)}
{settings.proxy.type === "enabled" && (
<>
<Separator className="my-6" />
<PlainInput
label="Proxy Bypass"
help="Comma-separated list to bypass the proxy."
defaultValue={settings.proxy.bypass}
<SettingRowText
name="proxyHttps"
title={
<>
Proxy for <InlineCode>https://</InlineCode> traffic
</>
}
description="Proxy host used for HTTPS traffic."
value={settings.proxy.https}
placeholder="localhost:9090"
onChange={(https) => patchProxy({ https })}
/>
<SettingRowText
name="proxyBypass"
title="Proxy Bypass"
description="Comma-separated list of hosts that should bypass the proxy."
value={settings.proxy.bypass}
placeholder="127.0.0.1, *.example.com, localhost:3000"
onChange={async (bypass) => {
const { proxy } = settings;
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
const user = proxy?.type === "enabled" ? (proxy.auth?.user ?? "") : "";
const password = proxy?.type === "enabled" ? (proxy.auth?.password ?? "") : "";
const auth = { user, password };
await patchModel(settings, {
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
inputWidthClassName="!w-96"
onChange={(bypass) => patchProxy({ bypass })}
/>
</>
)}
</VStack>
)}
</SettingsSection>
<SettingsSection title="Authentication">
<SettingRowBoolean
checked={settings.proxy.auth != null}
title="Enable authentication"
description="Send proxy credentials with proxied requests."
onChange={(enabled) =>
patchProxy({ auth: enabled ? { user: "", password: "" } : null })
}
/>
{settings.proxy.auth != null && (
<>
<SettingRowText
required
name="proxyUser"
title="User"
description="Username for proxy authentication."
value={settings.proxy.auth.user}
placeholder="myUser"
onChange={(user) =>
patchProxy({
auth: {
user,
password:
settings.proxy?.type === "enabled"
? (settings.proxy.auth?.password ?? "")
: "",
},
})
}
/>
<SettingRowText
name="proxyPassword"
title="Password"
description="Password for proxy authentication."
value={settings.proxy.auth.password}
placeholder="s3cretPassw0rd"
type="password"
onChange={(password) =>
patchProxy({
auth: {
user:
settings.proxy?.type === "enabled"
? (settings.proxy.auth?.user ?? "")
: "",
password,
},
})
}
/>
</>
)}
</SettingsSection>
</>
)}
</SettingsList>
</VStack>
);
}
type EnabledProxySetting = Extract<ProxySetting, { type: "enabled" }>;
function enabledProxyOrDefault(proxy: ProxySetting | null): EnabledProxySetting {
if (proxy?.type === "enabled") return proxy;
return {
disabled: false,
type: "enabled",
http: "",
https: "",
auth: { user: "", password: "" },
bypass: "",
};
}
@@ -9,7 +9,12 @@ import type { ButtonProps } from "../core/Button";
import { IconButton } from "../core/IconButton";
import { Link } from "../core/Link";
import type { SelectProps } from "../core/Select";
import { Select } from "../core/Select";
import {
ModelSettingRowSelect,
SettingRowSelect,
SettingsList,
SettingsSection,
} from "../core/SettingRow";
const Editor = lazy(() => import("../core/Editor/Editor").then((m) => ({ default: m.Editor })));
@@ -67,7 +72,7 @@ export function SettingsTheme() {
}));
return (
<VStack space={3} className="mb-4">
<VStack space={1.5} className="mb-4">
<div className="mb-3">
<Heading>Theme</Heading>
<p className="text-text-subtle">
@@ -77,96 +82,92 @@ export function SettingsTheme() {
</Link>
</p>
</div>
<Select
name="appearance"
label="Appearance"
labelPosition="top"
size="sm"
value={settings.appearance}
onChange={(appearance) => patchModel(settings, { appearance })}
options={[
{ label: "Automatic", value: "system" },
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
]}
/>
<HStack space={2}>
{(settings.appearance === "system" || settings.appearance === "light") && (
<Select
hideLabel
leftSlot={<Icon icon="sun" color="secondary" />}
name="lightTheme"
label="Light Theme"
size="sm"
className="flex-1"
value={activeTheme.data.light.id}
options={lightThemes}
onChange={(themeLight) => patchModel(settings, { themeLight })}
<SettingsList className="space-y-8">
<SettingsSection title="Theme">
<ModelSettingRowSelect
model={settings}
modelKey="appearance"
title="Appearance"
description="Choose whether Yaak follows your system appearance or uses a fixed mode."
options={[
{ label: "Automatic", value: "system" },
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
]}
/>
)}
{(settings.appearance === "system" || settings.appearance === "dark") && (
<Select
hideLabel
name="darkTheme"
className="flex-1"
label="Dark Theme"
leftSlot={<Icon icon="moon" color="secondary" />}
size="sm"
value={activeTheme.data.dark.id}
options={darkThemes}
onChange={(themeDark) => patchModel(settings, { themeDark })}
/>
)}
</HStack>
{(settings.appearance === "system" || settings.appearance === "light") && (
<SettingRowSelect
name="lightTheme"
title="Light theme"
description="Theme used when Yaak is in light mode."
value={activeTheme.data.light.id}
options={lightThemes}
onChange={(themeLight) => patchModel(settings, { themeLight })}
/>
)}
{(settings.appearance === "system" || settings.appearance === "dark") && (
<SettingRowSelect
name="darkTheme"
title="Dark theme"
description="Theme used when Yaak is in dark mode."
value={activeTheme.data.dark.id}
options={darkThemes}
onChange={(themeDark) => patchModel(settings, { themeDark })}
/>
)}
</SettingsSection>
<VStack
space={3}
className="mt-3 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
>
<HStack className="text" space={1.5}>
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
<strong>{activeTheme.data.active.label}</strong>
<em>(preview)</em>
</HStack>
<HStack space={1.5} className="w-full">
{buttonColors.map((c, i) => (
<IconButton
key={c}
color={c}
size="2xs"
iconSize="xs"
icon={icons[i % icons.length] ?? "info"}
iconClassName="text"
title={`${c}`}
/>
))}
{buttonColors.map((c, i) => (
<IconButton
key={c}
color={c}
variant="border"
size="2xs"
iconSize="xs"
icon={icons[i % icons.length] ?? "info"}
iconClassName="text"
title={`${c}`}
/>
))}
</HStack>
<Suspense>
<Editor
defaultValue={[
"let foo = { // Demo code editor",
' foo: ("bar" || "baz" ?? \'qux\'),',
" baz: [1, 10.2, null, false, true],",
"};",
].join("\n")}
heightMode="auto"
language="javascript"
stateKey={null}
/>
</Suspense>
</VStack>
<SettingsSection title="Preview">
<VStack
space={3}
className="mt-4 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
>
<HStack className="text" space={1.5}>
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
<strong>{activeTheme.data.active.label}</strong>
<em>(preview)</em>
</HStack>
<HStack space={1.5} className="w-full">
{buttonColors.map((c, i) => (
<IconButton
key={c}
color={c}
size="2xs"
iconSize="xs"
icon={icons[i % icons.length] ?? "info"}
iconClassName="text"
title={`${c}`}
/>
))}
{buttonColors.map((c, i) => (
<IconButton
key={c}
color={c}
variant="border"
size="2xs"
iconSize="xs"
icon={icons[i % icons.length] ?? "info"}
iconClassName="text"
title={`${c}`}
/>
))}
</HStack>
<Suspense>
<Editor
defaultValue={[
"let foo = { // Demo code editor",
' foo: ("bar" || "baz" ?? \'qux\'),',
" baz: [1, 10.2, null, false, true],",
"};",
].join("\n")}
heightMode="auto"
language="javascript"
stateKey={null}
/>
</Suspense>
</VStack>
</SettingsSection>
</SettingsList>
</VStack>
);
}