From b9d4f76193fc89b9e7061058acb92a636bdd26b6 Mon Sep 17 00:00:00 2001 From: WkdXeqtr <200361342+WkdXeqtr@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:58:20 +0400 Subject: [PATCH] Add dropdown to pick header names and values Adds a chevron on pair editor name and value inputs that opens the same options as inline autocomplete, so suggestions can be found by clicking instead of only by typing. Long values like User-Agent show a short label but insert the full string. Row action menus move to a vertical ellipsis so the chevron only ever means "pick a value into this field". Also restores the per-worktree Tauri config handling in run-dev.mjs, which a merge on the branch had reverted. --- apps/yaak-client/components/HeadersEditor.tsx | 40 +++++++++--- apps/yaak-client/components/core/Input.tsx | 63 +++++++++++++++++++ .../components/core/PairEditor.tsx | 15 ++++- apps/yaak-client/lib/data/acceptLanguages.ts | 15 +++++ apps/yaak-client/lib/data/cacheControl.ts | 16 +++++ .../lib/data/headerValuePresets.ts | 7 +++ apps/yaak-client/lib/data/userAgents.ts | 51 +++++++++++++++ 7 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 apps/yaak-client/lib/data/acceptLanguages.ts create mode 100644 apps/yaak-client/lib/data/cacheControl.ts create mode 100644 apps/yaak-client/lib/data/headerValuePresets.ts create mode 100644 apps/yaak-client/lib/data/userAgents.ts diff --git a/apps/yaak-client/components/HeadersEditor.tsx b/apps/yaak-client/components/HeadersEditor.tsx index 790ca958..fa1d8010 100644 --- a/apps/yaak-client/components/HeadersEditor.tsx +++ b/apps/yaak-client/components/HeadersEditor.tsx @@ -1,14 +1,17 @@ import type { HttpRequestHeader } from "@yaakapp-internal/models"; -import type { GenericCompletionOption } from "@yaakapp-internal/plugins"; import { HStack } from "@yaakapp-internal/ui"; +import { acceptLanguages } from "../lib/data/acceptLanguages"; +import { cacheControlDirectives } from "../lib/data/cacheControl"; import { charsets } from "../lib/data/charsets"; import { connections } from "../lib/data/connections"; import { encodings } from "../lib/data/encodings"; +import type { HeaderValuePreset } from "../lib/data/headerValuePresets"; import { headerNames } from "../lib/data/headerNames"; import { mimeTypes } from "../lib/data/mimetypes"; +import { userAgents } from "../lib/data/userAgents"; import { CountBadge } from "./core/CountBadge"; import { DetailsBanner } from "./core/DetailsBanner"; -import type { GenericCompletionConfig } from "./core/Editor/genericCompletion"; +import type { GenericCompletion, GenericCompletionConfig } from "./core/Editor/genericCompletion"; import type { InputProps } from "./core/Input"; import type { Pair, PairEditorProps } from "./core/PairEditor"; import { PairEditorRow } from "./core/PairEditor"; @@ -105,12 +108,21 @@ export function HeadersEditor({ const MIN_MATCH = 3; -const headerOptionsMap: Record = { +const headerOptionsMap: Record = { "content-type": mimeTypes, accept: ["*/*", ...mimeTypes], "accept-encoding": encodings, + "content-encoding": encodings, connection: connections, "accept-charset": charsets, + "accept-language": acceptLanguages, + "cache-control": cacheControlDirectives, + "user-agent": userAgents, + pragma: ["no-cache"], + te: ["trailers", "compress", "deflate", "gzip"], + dnt: ["1", "0"], + "upgrade-insecure-requests": ["1"], + "x-requested-with": ["XMLHttpRequest"], }; const valueType = (pair: Pair): InputProps["type"] => { @@ -132,12 +144,22 @@ const valueType = (pair: Pair): InputProps["type"] => { const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => { const name = headerName.toLowerCase().trim(); - const options: GenericCompletionOption[] = - headerOptionsMap[name]?.map((o) => ({ - label: o, - type: "constant", - boost: 1, // Put above other completions - })) ?? []; + const options: GenericCompletion[] = + headerOptionsMap[name]?.map((o) => + typeof o === "string" + ? { + label: o, + type: "constant", + boost: 1, // Put above other completions + } + : { + // Show the short label but insert the full value (e.g. User-Agent) + label: o.label, + apply: o.value, + type: "constant", + boost: 1, + }, + ) ?? []; return { minMatch: MIN_MATCH, options }; }; diff --git a/apps/yaak-client/components/core/Input.tsx b/apps/yaak-client/components/core/Input.tsx index 8d74eaaf..c0063274 100644 --- a/apps/yaak-client/components/core/Input.tsx +++ b/apps/yaak-client/components/core/Input.tsx @@ -22,6 +22,7 @@ import { Button } from "./Button"; import type { DropdownItem } from "./Dropdown"; import { Dropdown } from "./Dropdown"; import type { EditorProps } from "./Editor/Editor"; +import type { GenericCompletion } from "./Editor/genericCompletion"; import { Editor } from "./Editor/LazyEditor"; import { IconButton } from "./IconButton"; import { IconTooltip } from "./IconTooltip"; @@ -63,6 +64,13 @@ export type InputProps = Pick< placeholder?: string; required?: boolean; rightSlot?: ReactNode; + /** + * Show a chevron that opens the `autocomplete` options as a menu, so a value can be picked + * without knowing what to type. Opt-in, because plenty of inputs pass `autocomplete` without + * wanting the affordance (the URL bar completes from history, and a chevron there would be + * noise). Renders nothing when there are no options to show. + */ + showOptionsPicker?: boolean; size?: "2xs" | "xs" | "sm" | "md" | "auto"; stateKey: EditorProps["stateKey"]; extraExtensions?: EditorProps["extraExtensions"]; @@ -115,6 +123,7 @@ function BaseInput({ readOnly, required, rightSlot, + showOptionsPicker, size = "md", stateKey, tint, @@ -156,6 +165,17 @@ function BaseInput({ [], ); + // Replace the whole value with a picked option. Dispatching runs it through the editor's + // onChange, same as typing, so callers see one consistent change path. + const insertOption = useCallback((value: string) => { + const view = editorRef.current; + if (view == null) return; + view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } }); + view.focus(); + }, []); + + const pickerOptions = showOptionsPicker && !disabled ? (props.autocomplete?.options ?? []) : []; + const setEditorRef = useCallback( (h: EditorView | null) => { editorRef.current = h; @@ -350,12 +370,55 @@ function BaseInput({ onClick={() => setObscured((o) => !o)} /> )} + {pickerOptions.length > 0 && ( + + )} {rightSlot} ); } +/** + * Chevron that opens the input's autocomplete options as a menu, for browsing values rather than + * typing them. Shows `label` but inserts `apply` when set, so a long value can be listed under a + * short name (e.g. a User-Agent). + * + * Skipped in the tab order: once focus is in the input, inline autocomplete offers the same + * options, so this would only add a stop between fields. Hidden when the nearest container is + * narrow and the input needs the width more — written as a `max` query so it stays visible for + * inputs that have no container ancestor at all. + */ +function OptionsPickerDropdown({ + options, + onSelect, +}: { + options: GenericCompletion[]; + onSelect: (value: string) => void; +}) { + const items = useMemo( + () => + options.map((o) => ({ + label: o.label, + onSelect: () => onSelect(typeof o.apply === "string" ? o.apply : o.label), + })), + [options, onSelect], + ); + + return ( + + + + ); +} + function validateRequire(v: string) { return v.length > 0; } diff --git a/apps/yaak-client/components/core/PairEditor.tsx b/apps/yaak-client/components/core/PairEditor.tsx index be5f85f5..72352fc2 100644 --- a/apps/yaak-client/components/core/PairEditor.tsx +++ b/apps/yaak-client/components/core/PairEditor.tsx @@ -647,6 +647,10 @@ export function PairEditorRow({ [setDraggableRef, setDroppableRef], ); + // Skip the trailing placeholder row. It exists to start a new pair, so a picker there would + // imply it's already a real row. `Input` handles the rest, including having no options to show. + const showOptionsPicker = !isLast; + return (
{pair.isFile ? ( @@ -753,6 +758,7 @@ export function PairEditorRow({ autocomplete={valueAutocomplete?.(pair.name)} autocompleteFunctions={valueAutocompleteFunctions} autocompleteVariables={valueAutocompleteVariablesFiltered} + showOptionsPicker={showOptionsPicker} /> )}
@@ -772,8 +778,10 @@ export function PairEditorRow({ @@ -897,7 +905,8 @@ function FileActionsDropdown({ diff --git a/apps/yaak-client/lib/data/acceptLanguages.ts b/apps/yaak-client/lib/data/acceptLanguages.ts new file mode 100644 index 00000000..2f0b5a3a --- /dev/null +++ b/apps/yaak-client/lib/data/acceptLanguages.ts @@ -0,0 +1,15 @@ +// Common Accept-Language values. +export const acceptLanguages = [ + "*", + "en", + "en-US", + "en-US,en;q=0.9", + "en-GB,en;q=0.9", + "de-DE,de;q=0.9,en;q=0.8", + "fr-FR,fr;q=0.9,en;q=0.8", + "es-ES,es;q=0.9,en;q=0.8", + "ru-RU,ru;q=0.9,en;q=0.8", + "uk-UA,uk;q=0.9,en;q=0.8", + "zh-CN,zh;q=0.9,en;q=0.8", + "ja-JP,ja;q=0.9,en;q=0.8", +]; diff --git a/apps/yaak-client/lib/data/cacheControl.ts b/apps/yaak-client/lib/data/cacheControl.ts new file mode 100644 index 00000000..ea69436c --- /dev/null +++ b/apps/yaak-client/lib/data/cacheControl.ts @@ -0,0 +1,16 @@ +// Common Cache-Control directives (request and response). +export const cacheControlDirectives = [ + "no-cache", + "no-store", + "no-transform", + "max-age=0", + "max-age=3600", + "max-age=86400", + "s-maxage=3600", + "must-revalidate", + "proxy-revalidate", + "stale-while-revalidate=60", + "public", + "private", + "immutable", +]; diff --git a/apps/yaak-client/lib/data/headerValuePresets.ts b/apps/yaak-client/lib/data/headerValuePresets.ts new file mode 100644 index 00000000..7fef3abd --- /dev/null +++ b/apps/yaak-client/lib/data/headerValuePresets.ts @@ -0,0 +1,7 @@ +/** + * A suggested value for a header. A plain string is used when the displayed + * label and the inserted value are the same (e.g. mime types). The object form + * lets us show a short, readable `label` (e.g. "Chrome (Windows)") while + * inserting a longer `value` (the full User-Agent string). + */ +export type HeaderValuePreset = string | { label: string; value: string }; diff --git a/apps/yaak-client/lib/data/userAgents.ts b/apps/yaak-client/lib/data/userAgents.ts new file mode 100644 index 00000000..f7e45b2a --- /dev/null +++ b/apps/yaak-client/lib/data/userAgents.ts @@ -0,0 +1,51 @@ +import type { HeaderValuePreset } from "./headerValuePresets"; + +// Common, real-world User-Agent strings. The short `label` is shown in the +// dropdown/autocomplete, while the full UA string is what gets inserted. +// +// Browsers freeze parts of their UA: Chrome reports its minor version as 0.0.0 +// and Safari reports macOS as 10_15_7, regardless of the actual version. +export const userAgents: HeaderValuePreset[] = [ + { + label: "Chrome 151 · Windows 10/11 · x64", + value: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + }, + { + label: "Edge 151 · Windows 10/11 · x64", + value: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0", + }, + { + label: "Firefox 153 · Windows 10/11 · x64", + value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0", + }, + { + label: "Chrome 151 · macOS", + value: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + }, + { + label: "Safari 27 · macOS", + value: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Safari/605.1.15", + }, + { + label: "Chrome 151 · Linux · x64", + value: + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + }, + { + label: "Safari 27 · iOS 27 · iPhone", + value: + "Mozilla/5.0 (iPhone; CPU iPhone OS 27_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Mobile/15E148 Safari/604.1", + }, + { + label: "Chrome 151 · Android 16 · Pixel 10", + value: + "Mozilla/5.0 (Linux; Android 16; Pixel 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Mobile Safari/537.36", + }, + { label: "curl 8.21.0 · CLI", value: "curl/8.21.0" }, + { label: "Postman 7.56 · API client", value: "PostmanRuntime/7.56.1" }, + { label: "Insomnia 13.1 · API client", value: "insomnia/13.1.0" }, +];