Drop cookie import; click-through notifications; per-app zoom

The cookie import is gone. It worked mechanically - 43 cookies decrypted
from Arc and verifiably visible to the page - but Google, Microsoft and
Odoo all refused the imported sessions, because each binds a session to
the browser that created it. Signing in once inside the app is simpler
and actually works, so the whole path is deleted rather than kept as a
feature that mostly fails. That takes rusqlite, aes, cbc, pbkdf2, hmac,
sha1 and sha2 out of the build with it.

Notifications are now raised through mac-notification-sys rather than
Tauri's notification plugin, because the plugin cannot report that one
was clicked. A click switches to the app that raised it and then runs
the page's own click handler - the only thing that knows which message
the notification was about.

Zoom is per app, on a fixed ladder so Cmd+0 returns to exactly 100%.
The shortcuts are menu-bar accelerators rather than a key listener,
since the keystroke has to work while a remote page has focus.

The hidden-element count is off the nav rows.
This commit is contained in:
2026-09-01 12:53:52 +02:00
parent f057268103
commit 3525d454bf
18 changed files with 416 additions and 1073 deletions
+15 -1
View File
@@ -6,7 +6,7 @@ import Nav from "./components/Nav";
import Settings from "./components/Settings";
import { BTN_PRIMARY } from "./components/ui";
import { useAppearance, type Theme } from "./hooks/useAppearance";
import type { Config, Group, HiddenEvent, SwitchEvent } from "./types";
import type { Config, Group, HiddenEvent, NotificationClick, SwitchEvent } from "./types";
export default function App() {
const [config, setConfig] = useState<Config | null>(null);
@@ -89,6 +89,20 @@ export default function App() {
setFocusHidden(e.payload);
setSettingsOpen(true);
}),
// A macOS banner was clicked. Switch to the app that raised it, then let
// that page's own handler run — it is the only thing that knows which
// message the notification was about.
listen<NotificationClick>("notification-clicked", async (e) => {
await api.focusWindow();
setSettingsOpen(false);
setActiveId(e.payload.appId);
await api.setActive(e.payload.appId);
await api.notificationClick(e.payload.appId, e.payload.notificationId);
}),
// Zoom changed from the menu bar; keep Settings' sliders honest.
listen<[string, number]>("zoom-changed", () => {
void api.getConfig().then(setConfig);
}),
];
return () => {
unlisten.forEach((p) => p.then((f) => f()));
+7 -6
View File
@@ -1,7 +1,7 @@
/** Every call into Rust, in one place. */
import { invoke } from "@tauri-apps/api/core";
import type { Browser, Config, Group, PairResult, WorkApp } from "./types";
import type { Config, Group, WorkApp } from "./types";
export const getConfig = () => invoke<Config>("get_config");
export const bootstrap = () => invoke<void>("bootstrap");
@@ -40,11 +40,12 @@ export const setHidden = (appId: string, hidden: string[]) =>
invoke<Config>("set_hidden", { appId, hidden });
export const pickHidden = (appId: string) => invoke<void>("pick_hidden", { appId });
export const listBrowsers = () => invoke<Browser[]>("list_browsers");
export const pairBrowser = (browser: string) =>
invoke<PairResult>("pair_browser", { browser });
export const testNotification = (appId: string) =>
invoke<void>("test_notification", { appId });
export const notificationStatus = () => invoke<string>("notification_status");
export const probeCookies = (appId: string) => invoke<void>("probe_cookies", { appId });
export const cookieProbe = () => invoke<string>("cookie_probe");
export const setZoom = (appId: string, zoom: number) =>
invoke<Config>("set_zoom", { appId, zoom });
export const notificationClick = (appId: string, notificationId: string) =>
invoke<void>("notification_click", { appId, notificationId });
export const focusWindow = () => invoke<void>("focus_window");
-8
View File
@@ -137,14 +137,6 @@ export default function Nav({
>
<Favicon url={app.url} name={app.name} />
<span className="truncate">{app.name}</span>
{app.hidden.length > 0 && (
<span
title={`${app.hidden.length} element${app.hidden.length === 1 ? "" : "s"} hidden here`}
className="ml-auto font-mono text-[10px] opacity-50"
>
{app.hidden.length}
</span>
)}
</button>
</li>
);
+32 -94
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import * as api from "../api";
import type { Theme } from "../hooks/useAppearance";
import type { Browser, Config, Group, PairResult, WorkApp } from "../types";
import type { Config, Group, WorkApp } from "../types";
import { Trash } from "./icons";
import {
BTN,
@@ -17,7 +17,6 @@ import {
SUBPANEL,
SectionHeading,
Segmented,
Spinner,
} from "./ui";
interface Props {
@@ -42,12 +41,7 @@ export default function Settings({
const [error, setError] = useState<string | null>(null);
const [confirmDelete, setConfirmDelete] = useState<WorkApp | null>(null);
const [browsers, setBrowsers] = useState<Browser[]>([]);
const [browser, setBrowser] = useState("");
const [pairing, setPairing] = useState(false);
const [notifyStatus, setNotifyStatus] = useState<string | null>(null);
const [cookieStatus, setCookieStatus] = useState<string | null>(null);
const [paired, setPaired] = useState<PairResult | null>(null);
const groups = [...config.groups].sort((a, b) => a.order - b.order);
@@ -58,12 +52,6 @@ export default function Settings({
(a, b) => groupRank(a.groupId) - groupRank(b.groupId) || a.order - b.order,
);
useEffect(() => {
api.listBrowsers().then((b) => {
setBrowsers(b);
setBrowser(config.settings.pairedBrowser ?? b[0]?.id ?? "");
});
}, [config.settings.pairedBrowser]);
useEffect(() => {
if (!focusHiddenFor) return;
@@ -95,20 +83,6 @@ export default function Settings({
const patch = (app: WorkApp, fields: Partial<WorkApp>) =>
run(() => api.updateApp({ ...app, ...fields }));
const pair = async () => {
if (!browser) return;
setPairing(true);
setPaired(null);
try {
setPaired(await api.pairBrowser(browser));
onConfig(await api.getConfig());
setError(null);
} catch (e) {
setError(String(e));
} finally {
setPairing(false);
}
};
const withHidden = orderedApps.filter((a) => a.hidden.length > 0);
@@ -247,75 +221,38 @@ export default function Settings({
</p>
</section>
{/* -------------------------------------------------- pairing */}
{/* ------------------------------------------------------ zoom */}
<section className="space-y-2">
<SectionHeading>Browser pairing</SectionHeading>
{browsers.length === 0 ? (
<p className={HELP}>No browser with a readable cookie store was found.</p>
) : (
<>
<div className={`${SUBPANEL} flex items-end gap-2`}>
<label className="flex-1 space-y-1">
<span className={LABEL}>Import cookies from</span>
<select value={browser} onChange={(e) => setBrowser(e.target.value)}
className={`${INPUT} w-full cursor-pointer`}>
{browsers.map((b) => (
<option key={b.id} value={b.id}>{b.label}</option>
))}
</select>
</label>
<button
onClick={async () => {
if (!activeId) return;
setCookieStatus("checking…");
await api.probeCookies(activeId);
await new Promise((r) => setTimeout(r, 400));
setCookieStatus(await api.cookieProbe());
}}
disabled={!activeId}
title="What the current app can actually see"
className={BTN}
>
Check cookies
</button>
<button onClick={pair} disabled={pairing || !browser} className={BTN_PRIMARY}>
{pairing ? <Spinner /> : config.settings.lastPairedAt ? "Pair again" : "Pair"}
</button>
<SectionHeading>Zoom</SectionHeading>
<div className="space-y-1.5">
{orderedApps.map((app) => (
<div
key={app.id}
className="flex items-center gap-3 rounded-lg border border-slate-200 px-2 py-1.5 dark:border-slate-800"
>
<Favicon url={app.url} name={app.name} />
<span className="w-28 shrink-0 truncate text-[12px]">{app.name}</span>
<input
type="range"
min={50}
max={200}
step={5}
value={Math.round((app.zoom ?? 1) * 100)}
onChange={(e) =>
run(() => api.setZoom(app.id, Number(e.target.value) / 100))
}
className="min-w-0 flex-1 cursor-pointer accent-sky-500"
/>
<span className="w-12 shrink-0 text-right font-mono text-[11px] tabular-nums text-slate-500 dark:text-slate-400">
{Math.round((app.zoom ?? 1) * 100)}%
</span>
</div>
{cookieStatus && (
<p className={`${HELP} font-mono`}>{cookieStatus}</p>
)}
{config.settings.lastPairedAt && !paired && (
<p className={HELP}>Last paired {config.settings.lastPairedAt}.</p>
)}
{paired && (
<div className="space-y-1 rounded-lg bg-sky-500/10 px-3 py-2">
<p className="text-[12px] text-sky-700 dark:text-sky-300">
Imported {paired.imported} cookie{paired.imported === 1 ? "" : "s"} across{" "}
{paired.domains} domain{paired.domains === 1 ? "" : "s"}.
</p>
{paired.domainNames.length > 0 && (
<p className="font-mono text-[11px] leading-relaxed text-slate-600 dark:text-slate-300">
{paired.domainNames.join(" ")}
</p>
)}
{paired.warnings.map((w, i) => (
<p key={i} className="text-[11px] text-slate-600 dark:text-slate-300">{w}</p>
))}
</div>
)}
<p className={HELP}>
Only cookies for the apps above and their sign-in hosts are read nothing
else leaves the browser. macOS asks for Keychain permission the first time.
Google and Microsoft tie sessions to the browser that made them, so those two
may still ask you to sign in once; after that it sticks.
</p>
</>
)}
))}
</div>
<p className={HELP}>
+ and zoom the app you are in; 0 puts it back to 100%. Each app keeps
its own size.
</p>
</section>
{/* --------------------------------------------- notifications */}
@@ -335,6 +272,7 @@ export default function Settings({
>
Check permission
</button>
<span className={HELP}>Clicking one opens the message it is about.</span>
</div>
{notifyStatus && (
<p className={`${HELP} font-mono`}>{notifyStatus}</p>
+8 -13
View File
@@ -7,6 +7,8 @@ export interface WorkApp {
userAgent: string | null;
/** CSS selectors this app hides on every page. */
hidden: string[];
/** Page zoom, remembered per app. */
zoom: number;
order: number;
}
@@ -20,8 +22,6 @@ export interface Group {
export interface Settings {
navCollapsed: boolean;
theme: "system" | "light" | "dark";
pairedBrowser: string | null;
lastPairedAt: string | null;
}
export interface Config {
@@ -42,21 +42,16 @@ export interface UrlEvent {
url: string;
}
export interface Browser {
id: string;
label: string;
available: boolean;
}
export interface PairResult {
imported: number;
domains: number;
domainNames: string[];
warnings: string[];
}
/** Rust recorded a selector the user right-clicked away. */
export interface HiddenEvent {
appId: string;
selector: string;
}
/** The user clicked a macOS banner an app raised. */
export interface NotificationClick {
appId: string;
notificationId: string;
}