diff --git a/.gitignore b/.gitignore
index 1beb88f89..f4d96a8bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,3 +59,6 @@ flatpak/node-sources.json
# Claude Code local settings
.claude/settings.local.json
.claude/worktrees/
+
+# Headless screenshots taken while working
+.shots/
diff --git a/apps/yaak-proxy/components/DecryptListEditor.tsx b/apps/yaak-proxy/components/DecryptListEditor.tsx
index 74f956d0a..fa788ee95 100644
--- a/apps/yaak-proxy/components/DecryptListEditor.tsx
+++ b/apps/yaak-proxy/components/DecryptListEditor.tsx
@@ -1,10 +1,9 @@
import { Prec } from "@codemirror/state";
import { type EditorView, keymap } from "@codemirror/view";
import type { DecryptListEntry, SeenTargets } from "@yaakapp-internal/proxy-lib";
-import { Dialog, Icon, IconButton, Input } from "@yaakapp-internal/ui";
+import { Icon, IconButton, Input } from "@yaakapp-internal/ui";
import { filterAutocomplete } from "@yaakapp-internal/ui/editor";
import classNames from "classnames";
-import { atom, useAtom } from "jotai";
import { useMemo, useRef, useState } from "react";
import { useDecryptList, useEditDecryptList } from "../hooks/useDecryptList";
import { useRpcQuery } from "../hooks/useRpcQuery";
@@ -13,8 +12,8 @@ import { decryptFilterOptions, entryLabel, normalizeEntry } from "../lib/decrypt
/**
* What HTTPS is decrypted: one row per entry, the user's in order and then the built-in
* exclusions, each removable, and an input that adds one, suggesting `host:`, `app:` and `!`,
- * then the hosts and apps seen in the capture. Saves as it changes. Placed by Settings; until
- * that exists, by the dialog the state pill's More… opens.
+ * then the hosts and apps seen in the capture. Saves as it changes. Placed by Settings' Proxy
+ * tab.
*/
export function DecryptListEditor({ className }: { className?: string }) {
const list = useDecryptList();
@@ -167,21 +166,3 @@ function EntryInput({
);
}
-
-/** Opens the decrypt list from the state pill's More…, until Settings holds it */
-export const decryptListDialogOpenAtom = atom(false);
-
-export function DecryptListDialog() {
- const [open, setOpen] = useAtom(decryptListDialogOpenAtom);
- return (
-
- );
-}
diff --git a/apps/yaak-proxy/components/FlowCard.tsx b/apps/yaak-proxy/components/FlowCard.tsx
new file mode 100644
index 000000000..977b04326
--- /dev/null
+++ b/apps/yaak-proxy/components/FlowCard.tsx
@@ -0,0 +1,307 @@
+import { formatSize } from "@yaakapp-internal/lib/formatSize";
+import type { Flow, FlowBody } from "@yaakapp-internal/proxy-lib";
+import {
+ CountBadge,
+ Dropdown,
+ EmptyStateText,
+ type ExtraPaneTab,
+ HttpRequestPaneView,
+ HttpResponsePaneView,
+ type HttpBodyResult,
+ Icon,
+ IconButton,
+ parseCookieHeaders,
+ ResponseCookies,
+ type ResponseViewMode,
+ SplitLayout,
+} from "@yaakapp-internal/ui";
+import classNames from "classnames";
+import { useSetAtom } from "jotai";
+import { useCallback, useMemo, useState } from "react";
+import { BODY_TYPE_BINARY } from "@yaakapp-internal/ui/httpTypes";
+import { findHeader } from "../lib/body";
+import {
+ browserTabMemory,
+ defaultTabFor,
+ type FlowPane,
+ learnTab,
+ REQUEST_TAB_AUTH,
+ REQUEST_TAB_BODY,
+ REQUEST_TAB_COOKIES,
+ REQUEST_TAB_HEADERS,
+ REQUEST_TAB_PARAMS,
+ RESPONSE_TAB_BODY,
+ RESPONSE_TAB_COOKIES,
+ RESPONSE_TAB_HEADERS,
+ RESPONSE_TAB_INFO,
+ RESPONSE_TAB_MESSAGES,
+ RESPONSE_TAB_TIMELINE,
+} from "../lib/defaultTab";
+import { saveDialogFlowAtom, useFlowActions } from "../lib/flowActions";
+import {
+ type FlowBodyData,
+ flowBodyLoader,
+ flowCookiesView,
+ flowRequestToView,
+ flowResponseToView,
+} from "../lib/flowViews";
+import { messagesTabLabel } from "../lib/messages";
+import { FlowBodyViewer, useFlowBody } from "./FlowBodyViewer";
+import { FlowInfo } from "./FlowInfo";
+import { FlowRequestBody } from "./FlowRequestBody";
+import { FlowMessages } from "./FlowMessages";
+import { FlowTimeline } from "./FlowTimeline";
+import { ProxyEditorHost } from "./ProxyEditorHost";
+
+/**
+ * One card for a flow: the request half (its URL line and tabs), a hairline, and the response
+ * half (its status line and tabs). The halves sit perpendicular to the list: stacked when the
+ * list is beside the card, side by side when it is above.
+ */
+export function FlowCard({ flow, listBeside }: { flow: Flow; listBeside: boolean }) {
+ return (
+
+ (
+
- {flow.error != null && (
-
- {flow.error}
-
- )}
- {flow.cancelled && (
-
- {flow.status == null
- ? "The app cancelled the request before a response came back."
- : "The app cancelled the request before the response finished."}
-
- )}
-
-
- {noResponse ? (
-
-
- {flow.cancelled ? "Cancelled" : "No response"}
-
-
- {flow.cancelled
- ? "The app cancelled the request before a response came back."
- : "The request never got a response."}{" "}
- The Request tab has what the app sent.
-
-
- {params.map(([name, value], i) => (
- // oxlint-disable-next-line react/no-array-index-key -- a name can repeat
-
- {value}
-
- ))}
-
-
- );
-}
-
-export function queryParams(url: string): [string, string][] {
- try {
- return [...new URL(url).searchParams.entries()];
- } catch {
- return [];
- }
-}
-
-function Missing() {
- return --;
-}
diff --git a/apps/yaak-proxy/components/FlowInfo.tsx b/apps/yaak-proxy/components/FlowInfo.tsx
new file mode 100644
index 000000000..601f997fd
--- /dev/null
+++ b/apps/yaak-proxy/components/FlowInfo.tsx
@@ -0,0 +1,125 @@
+import { formatSize } from "@yaakapp-internal/lib/formatSize";
+import type { Flow } from "@yaakapp-internal/proxy-lib";
+import { DetailsBanner, formatMillis, KeyValueRow, KeyValueRows } from "@yaakapp-internal/ui";
+import { format } from "date-fns";
+import type { HTMLAttributes, ReactElement, ReactNode } from "react";
+import { utcTimestamp } from "../lib/flowViews";
+import { folderOf } from "../lib/search";
+import { AppIcon } from "./AppIcon";
+
+/**
+ * Where a flow came from and how it travelled: in a proxy you didn't send the request, so the
+ * connection on each leg, the app and its process, timing, sizes and the flow's record are part
+ * of the answer.
+ */
+export function FlowInfo({ flow }: { flow: Flow }) {
+ const folder = folderOf(flow);
+ const clientPort = portOf(flow.clientAddr);
+
+ return (
+