mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
The strip, quiet encrypted rows, warning rows for refused handshakes, the encrypted card with named actions, trust at the first Decrypt, the toast with Undo and Reconnect, the pill's Decrypt everything switch, Settings' Decryption section, the Encrypted sidebar row, and two-step setup. Both body tabs read Body with a type badge; Info leads the request half with the timeline folded in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
211 lines
7.8 KiB
TypeScript
211 lines
7.8 KiB
TypeScript
import { Button, Icon, InlineCode, LoadingIcon } from "@yaakapp-internal/ui";
|
|
import classNames from "classnames";
|
|
import { useAtomValue } from "jotai";
|
|
import type { ReactNode } from "react";
|
|
import { useRpcMutation } from "../hooks/useRpcMutation";
|
|
import { encryptingPercent } from "../lib/encrypting";
|
|
import { proxyStateAtom } from "../lib/store";
|
|
import { useCopyEnvironment, useProxySetup, useSystemProxySwitch } from "./ProxyStatus";
|
|
|
|
type StepState = "todo" | "next" | "wait" | "done" | "fail";
|
|
|
|
/**
|
|
* The list's empty state until traffic arrives: turn on the system proxy, then see traffic. The
|
|
* certificate is trusted at the first Decrypt, not here, as nothing needs it before. Not
|
|
* dismissable and nothing remembered; the first flow replaces it. Copy environment beside it
|
|
* serves someone who wants one terminal proxied and nothing system-wide.
|
|
*/
|
|
export function FirstRun() {
|
|
const proxyState = useAtomValue(proxyStateAtom);
|
|
const setup = useProxySetup();
|
|
const { systemProxy, setSystemProxy } = setup;
|
|
const systemSwitch = useSystemProxySwitch(setup);
|
|
const execute = useRpcMutation("execute_action");
|
|
const retry = useRpcMutation("take_over");
|
|
const copyEnvironment = useCopyEnvironment();
|
|
|
|
const running = proxyState?.state === "running";
|
|
// Changed only by the process that owns the engine, so not from here while the CLI does.
|
|
const otherOwner = proxyState?.otherOwner != null;
|
|
const recording = proxyState?.recording !== false;
|
|
const systemSupported = systemProxy.data?.supported !== false;
|
|
const systemOn = systemProxy.data?.enabled === true;
|
|
|
|
const systemState: StepState = setSystemProxy.isPending
|
|
? "wait"
|
|
: systemOn
|
|
? "done"
|
|
: systemSwitch.failed
|
|
? "fail"
|
|
: "next";
|
|
const ready = systemOn || !systemSupported;
|
|
const encrypting = !running && !otherOwner ? encryptingPercent(proxyState) : null;
|
|
const trafficState: StepState =
|
|
encrypting != null ? "wait" : !running && !otherOwner ? "fail" : ready ? "wait" : "todo";
|
|
|
|
return (
|
|
<div className="grid place-items-center h-full px-10 pb-16 pt-6">
|
|
<div className="w-full max-w-[40rem]">
|
|
<div className="text-xl font-semibold mb-1.5">Set up Yaak Proxy</div>
|
|
<div className="text-text-subtle text-sm leading-relaxed mb-4">
|
|
{systemSupported
|
|
? "Two steps, once. Then every app on this Mac sends its traffic here."
|
|
: "Traffic appears here as apps send it."}
|
|
</div>
|
|
<div className="rounded-lg border border-border-subtle bg-surface divide-y divide-border-subtle">
|
|
{systemSupported && (
|
|
<Step
|
|
n={1}
|
|
state={systemState}
|
|
title={systemOn ? "System proxy on" : "Turn on the system proxy"}
|
|
description={
|
|
otherOwner && !systemOn
|
|
? proxyState?.otherOwner?.owner === "cli"
|
|
? "The CLI owns the proxy. Take over to turn this on from here."
|
|
: "Another Yaak Proxy window owns the engine. Quit it to use this one."
|
|
: systemSwitch.description
|
|
}
|
|
action={
|
|
(systemState === "next" || systemState === "fail") && running ? (
|
|
<Button
|
|
size="2xs"
|
|
variant={systemState === "fail" ? "border" : "solid"}
|
|
color={systemState === "fail" ? "default" : "primary"}
|
|
onClick={() => setSystemProxy.mutate({ enabled: true })}
|
|
>
|
|
{systemState === "fail" ? "Try Again" : "Turn On"}
|
|
</Button>
|
|
) : null
|
|
}
|
|
/>
|
|
)}
|
|
<Step
|
|
n={systemSupported ? 2 : 1}
|
|
state={trafficState}
|
|
title={
|
|
encrypting != null
|
|
? `Encrypting stored traffic, ${encrypting}`
|
|
: trafficState === "wait"
|
|
? "Waiting for traffic"
|
|
: "See traffic"
|
|
}
|
|
description={
|
|
encrypting != null
|
|
? "Traffic captured before this version is being encrypted on disk. Recording starts when it's done."
|
|
: trafficState === "fail"
|
|
? (retry.error?.message ?? proxyState?.startError ?? "The proxy has not started.")
|
|
: !recording
|
|
? "Recording is paused, so traffic passes through without being stored."
|
|
: trafficState === "wait"
|
|
? "Open a website or send a request, and its flow appears here."
|
|
: "Flows appear here as apps send requests."
|
|
}
|
|
action={
|
|
trafficState === "fail" ? (
|
|
<Button
|
|
size="2xs"
|
|
variant="border"
|
|
isLoading={retry.isPending}
|
|
onClick={() => retry.mutate({})}
|
|
>
|
|
Retry
|
|
</Button>
|
|
) : !recording && running ? (
|
|
<Button
|
|
size="2xs"
|
|
color="primary"
|
|
onClick={() => execute.mutate({ scope: "global", action: "recording_toggle" })}
|
|
>
|
|
Resume Recording
|
|
</Button>
|
|
) : null
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-3 mt-4 text-sm text-text-subtle">
|
|
<span>Only need one terminal or one app? Copy the environment instead.</span>
|
|
<Button
|
|
size="2xs"
|
|
variant="border"
|
|
className="ml-auto shrink-0"
|
|
leftSlot={<Icon icon="square_terminal" size="sm" />}
|
|
onClick={() => void copyEnvironment()}
|
|
>
|
|
Copy Environment
|
|
</Button>
|
|
</div>
|
|
<div className="mt-2 text-sm text-text-subtle">
|
|
Working with an agent? <InlineCode>yaak mcp</InlineCode> gives it the proxy's flows and
|
|
actions.
|
|
</div>
|
|
<div className="mt-2 text-sm text-text-subtle">
|
|
Captured traffic is encrypted on disk with a key kept in your keychain, so macOS may ask
|
|
to let Yaak Proxy use it.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Step({
|
|
n,
|
|
state,
|
|
title,
|
|
description,
|
|
action,
|
|
}: {
|
|
n: number;
|
|
state: StepState;
|
|
title: string;
|
|
description: ReactNode;
|
|
action: ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="grid grid-cols-[1.4rem_minmax(0,1fr)_auto] gap-x-3 items-center px-4 py-3">
|
|
<StepMark n={n} state={state} />
|
|
<div className="min-w-0">
|
|
<div className={classNames("text-sm", state === "todo" ? "text-text-subtle" : "text-text")}>
|
|
{title}
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
"text-xs mt-0.5",
|
|
state === "fail" ? "text-danger" : "text-text-subtle",
|
|
)}
|
|
>
|
|
{description}
|
|
</div>
|
|
</div>
|
|
<div>{action}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StepMark({ n, state }: { n: number; state: StepState }) {
|
|
const base = "w-[1.4rem] h-[1.4rem] rounded-full grid place-items-center text-xs";
|
|
switch (state) {
|
|
case "done":
|
|
return (
|
|
<span className={classNames(base, "bg-success/15 text-success")}>
|
|
<Icon icon="check" size="sm" />
|
|
</span>
|
|
);
|
|
case "fail":
|
|
return (
|
|
<span className={classNames(base, "bg-danger/15 text-danger")}>
|
|
<Icon icon="x" size="sm" />
|
|
</span>
|
|
);
|
|
case "wait":
|
|
return (
|
|
<span className={classNames(base, "text-text-subtle")}>
|
|
<LoadingIcon size="sm" />
|
|
</span>
|
|
);
|
|
case "next":
|
|
return <span className={classNames(base, "bg-primary text-white font-semibold")}>{n}</span>;
|
|
case "todo":
|
|
return <span className={classNames(base, "border border-border text-text-subtle")}>{n}</span>;
|
|
}
|
|
}
|