Keep Clear in the header with Cmd+Shift+K in its tooltip, and leave the port to the menu's address line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Gregory SchierandClaude Opus 5.5 authored and Stetta task tk_mwaz7asqhsi6jowm committed 2026-11-10 11:06:03 -07:00
1 parent a4061d5dee
commit d12beda8a9
5 files changed
+65 -70

No files matched your search

@@ -1,28 +1,19 @@
import { Dropdown, type DropdownItem, type DropdownRef } from "@yaakapp-internal/ui";
import { Dropdown, type DropdownItem, IconButton, useFormattedHotkey } from "@yaakapp-internal/ui";
import { useAtomValue, useSetAtom } from "jotai";
import type { Ref } from "react";
import { useState } from "react";
import { clearFlowsAtom, flowTotalAtom, lockedAtom } from "../lib/store";
/** Whether there are flows to clear: some, and readable, as they aren't while traffic is locked */
export function useCanClearFlows(): boolean {
const total = useAtomValue(flowTotalAtom);
const locked = useAtomValue(lockedAtom);
return locked == null && total > 0;
}
/**
* Clears every flow, once confirmed in its menu, which its hotkey or the status menu's Clear
* Traffic opens. It has no button of its own: it covers the element it is placed over, without
* taking the pointer, so the menu opens from there. The list empties at once; the flows are
* deleted in the background. Neither menu nor hotkey with nothing to clear, or while captured
* Clears every flow, once confirmed in its menu, which its hotkey opens too. The list empties at
* once; the flows are deleted in the background. Disabled with none to clear, or while captured
* traffic is locked.
*/
export function ClearFlowsMenu({ ref }: { ref?: Ref<DropdownRef> }) {
export function ClearFlowsButton() {
const total = useAtomValue(flowTotalAtom);
const canClear = useCanClearFlows();
const locked = useAtomValue(lockedAtom);
const clear = useSetAtom(clearFlowsAtom);
const [error, setError] = useState<string | null>(null);
const hotkey = useFormattedHotkey("flows_clear")?.join("");
const items: DropdownItem[] = [
{
@@ -57,13 +48,31 @@ export function ClearFlowsMenu({ ref }: { ref?: Ref<DropdownRef> }) {
},
];
// Nothing to clear, or nothing that can be read while traffic is locked: no menu, nor hotkey.
if (locked != null || total === 0) {
return (
<IconButton
className="pointer-events-auto"
icon="trash"
title={locked != null ? "Clear flows (traffic is locked)" : "Clear flows (none yet)"}
tooltip
size="sm"
iconColor="secondary"
disabled
/>
);
}
return (
<Dropdown
ref={ref}
items={canClear ? items : []}
hotKeyAction={canClear ? "flows_clear" : undefined}
>
<span aria-hidden className="absolute inset-0 pointer-events-none" />
<Dropdown items={items} hotKeyAction="flows_clear">
<IconButton
className="pointer-events-auto"
icon="trash"
title={hotkey == null ? "Clear flows" : `Clear flows ${hotkey}`}
tooltip
size="sm"
iconColor="secondary"
/>
</Dropdown>
);
}
+1 -1
View File
@@ -31,7 +31,7 @@ export function FlowDetail({ style, sideBySide }: { style?: CSSProperties; sideB
<Keys keys={["↑", "↓"]} />
<span>Pause or resume recording</span>
<Keys keys={["⌘", "R"]} />
<span>Clear traffic</span>
<span>Clear flows</span>
<Keys keys={["⌘", "⇧", "K"]} />
</div>
</EmptyState>
@@ -18,6 +18,7 @@ import { proxyStatusOf, statusTintColor } from "../lib/proxyStatus";
import { lockedAtom, noFlowsAtom, proxyStateAtom, systemProxyEverOnAtom } from "../lib/store";
import { getOsType } from "../lib/tauri";
import { AddColumnDialog } from "./AddColumnDialog";
import { ClearFlowsButton } from "./ClearFlowsButton";
import { FirstRun } from "./FirstRun";
import { FlowDetail } from "./FlowDetail";
import { FlowList } from "./FlowList";
@@ -95,6 +96,7 @@ export function ProxyLayout() {
</div>
</div>
<div data-tauri-drag-region className="flex items-center justify-end gap-1 pr-1">
<ClearFlowsButton />
<IconButton
className="pointer-events-auto"
icon={flowLayout === "horizontal" ? "columns_2" : "rows_2"}
+31 -47
View File
@@ -24,18 +24,15 @@ import { proxyStateAtom, systemProxyEverOnAtom } from "../lib/store";
import { helperPollInterval, recordingLabel, systemProxySwitch } from "../lib/systemProxy";
import { onWindowFocus } from "../lib/tauri";
import { useShowToast } from "../lib/toast";
import { ClearFlowsMenu, useCanClearFlows } from "./ClearFlowsMenu";
/**
* The header's state pill and its menu: the listening address, the state, Pause or Resume
* Recording, Clear Traffic, Override system proxy, and the certificate. The engine runs for as long as the app does, so the
* pill shows whether flows are being stored. A certificate that is not trusted steps out of the
* The header's state pill and its menu: the listening address, which copies itself, the state,
* Pause or Resume Recording, Override system proxy, and the certificate. The engine runs for as
* long as the app does, so the pill shows whether flows are being stored. A certificate that is not trusted steps out of the
* menu beside the pill until it is. Everything set once and left alone lives in Settings.
*/
export function ProxyStatus() {
const dropdownRef = useRef<DropdownRef>(null);
const clearRef = useRef<DropdownRef>(null);
const canClear = useCanClearFlows();
const [copied, setCopied] = useCopied();
const proxyState = useAtomValue(proxyStateAtom);
const setup = useProxySetup();
@@ -134,17 +131,6 @@ export function ProxyStatus() {
hotKeyLabelOnly: true,
onSelect: () => execute.mutate({ scope: "global", action: "recording_toggle" }),
},
{
label: "Clear Traffic",
icon: "trash",
disabled: !canClear,
hotKeyAction: "flows_clear",
hotKeyLabelOnly: true,
// Its confirmation opens from the pill once this menu has closed.
onSelect: () => {
setTimeout(() => clearRef.current?.open());
},
},
{ type: "separator" },
{
type: "content",
@@ -222,36 +208,33 @@ export function ProxyStatus() {
return (
<>
<span className="relative flex">
<Dropdown ref={dropdownRef} items={items}>
<Pill color={status === "stopped" ? "warning" : undefined}>
{status === "encrypting" ? (
<>
<LoadingIcon size="xs" />
<span className="text-text">Encrypting</span>
<span className="font-mono text-text-subtle">{encrypting}</span>
</>
) : status === "cli" && other != null ? (
<>
<StateDot state="cli" />
<span className="text-text">CLI</span>
</>
) : status === "recording" || status === "paused" ? (
<>
<StateDot state={status} />
<span className="text-text">{recording ? "Recording" : "Paused"}</span>
</>
) : (
<>
<Icon icon="alert_triangle" size="xs" />
Not running
</>
)}
<Icon icon="chevron_down" size="xs" className="text-text-subtlest" />
</Pill>
</Dropdown>
<ClearFlowsMenu ref={clearRef} />
</span>
<Dropdown ref={dropdownRef} items={items}>
<Pill color={status === "stopped" ? "warning" : undefined}>
{status === "encrypting" ? (
<>
<LoadingIcon size="xs" />
<span className="text-text">Encrypting</span>
<span className="font-mono text-text-subtle">{encrypting}</span>
</>
) : status === "cli" && other != null ? (
<>
<StateDot state="cli" />
<span className="text-text">CLI</span>
</>
) : status === "recording" || status === "paused" ? (
<>
<StateDot state={status} />
<span className="text-text">{recording ? "Recording" : "Paused"}</span>
</>
) : (
<>
<Icon icon="alert_triangle" size="xs" />
Not running
</>
)}
<Icon icon="chevron_down" size="xs" className="text-text-subtlest" />
</Pill>
</Dropdown>
{untrusted && (
<Pill color="warning" onClick={() => dropdownRef.current?.open()}>
<Icon icon="shield_off" size="xs" />
@@ -337,6 +320,7 @@ export function useCopyEnvironment() {
}
};
}
/** Whether something was just copied, for a moment, and the call that says it was */
function useCopied(): [boolean, () => void] {
const [copied, setCopied] = useState(false);
+1 -1
View File
@@ -43,7 +43,7 @@ pub fn all_global_actions() -> Vec<(ActionInvocation, ActionMetadata)> {
(
ActionInvocation::Global { action: GlobalAction::FlowsClear },
ActionMetadata {
label: "Clear Traffic".into(),
label: "Clear Flows".into(),
// Not Cmd+K, which is kept for a command palette.
default_hotkey: default_hotkey("Meta+Shift+K", "Ctrl+Shift+K"),
},