From c0c03f5aa18f48346ccca4a11c9ec9432fa7acc2 Mon Sep 17 00:00:00 2001 From: peopleinfo <49970466+peopleinfo@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:33:39 +0700 Subject: [PATCH 01/11] Add security score helper and tests --- apps/ui/src/renderer/src/lib/securityScore.js | 87 ++++++ .../renderer/src/views/AntiBrowserView.jsx | 279 +++++++++++++++++- apps/ui/tests/unit/security-score.test.js | 57 ++++ 3 files changed, 416 insertions(+), 7 deletions(-) create mode 100644 apps/ui/src/renderer/src/lib/securityScore.js create mode 100644 apps/ui/tests/unit/security-score.test.js diff --git a/apps/ui/src/renderer/src/lib/securityScore.js b/apps/ui/src/renderer/src/lib/securityScore.js new file mode 100644 index 0000000..cede793 --- /dev/null +++ b/apps/ui/src/renderer/src/lib/securityScore.js @@ -0,0 +1,87 @@ +const buildProtectionChecks = (profile) => [ + { + id: "webrtc", + label: "WebRTC leak protection", + enabled: Boolean(profile?.settings?.blockWebRTC), + }, + { + id: "canvas", + label: "Canvas fingerprint masking", + enabled: Boolean(profile?.settings?.blockCanvasFingerprint), + }, + { + id: "audio", + label: "Audio fingerprint masking", + enabled: Boolean(profile?.settings?.blockAudioFingerprint), + }, + { + id: "webgl", + label: "WebGL shielding", + enabled: Boolean(profile?.settings?.blockWebGL), + }, + { + id: "fonts", + label: "Font enumeration blocking", + enabled: Boolean(profile?.settings?.blockFonts), + }, + { + id: "geo", + label: "Geolocation guarding", + enabled: Boolean(profile?.settings?.blockGeolocation), + }, +]; + +const getScoreTone = (normalizedScore) => { + if (normalizedScore >= 80) return "bg-emerald-500"; + if (normalizedScore >= 60) return "bg-amber-500"; + return "bg-rose-500"; +}; + +const getScoreLabel = (normalizedScore) => { + if (normalizedScore >= 80) return "High"; + if (normalizedScore >= 60) return "Medium"; + return "Low"; +}; + +export const getSecurityScore = ({ + profiles = [], + proxies = [], + activeProfileId = null, +} = {}) => { + const stats = { + proxies: proxies.length, + activeProxies: proxies.filter((proxy) => proxy.status === "active").length, + profiles: profiles.length, + runningProfiles: profiles.filter((profile) => profile.status === "running") + .length, + }; + + const activeProfile = profiles.find( + (profile) => profile.id === activeProfileId + ); + const activeProxy = proxies.find( + (proxy) => proxy.id === activeProfile?.proxyId + ); + + const protectionChecks = buildProtectionChecks(activeProfile); + const protectionScore = protectionChecks.filter((item) => item.enabled).length; + const rawScore = + 30 + + (stats.profiles > 0 ? 10 : 0) + + (activeProfileId ? 10 : 0) + + (stats.runningProfiles > 0 ? 10 : 0) + + (activeProxy?.status === "active" ? 15 : 0) + + protectionScore * 5; + const normalizedScore = Math.min(Math.max(rawScore, 0), 100); + + return { + stats, + activeProfile, + activeProxy, + protectionChecks, + protectionScore, + normalizedScore, + scoreLabel: getScoreLabel(normalizedScore), + scoreTone: getScoreTone(normalizedScore), + }; +}; diff --git a/apps/ui/src/renderer/src/views/AntiBrowserView.jsx b/apps/ui/src/renderer/src/views/AntiBrowserView.jsx index c58af85..db58f48 100644 --- a/apps/ui/src/renderer/src/views/AntiBrowserView.jsx +++ b/apps/ui/src/renderer/src/views/AntiBrowserView.jsx @@ -1,9 +1,17 @@ -import { Globe, Network, Settings2, Shield, Users } from "lucide-react"; +import { Globe, Network, Settings2, Shield, ShieldCheck, Users } from "lucide-react"; import React, { useCallback, useState } from "react"; import { toast } from "sonner"; import BrowserProfilesPanel from "../components/BrowserProfilesPanel"; import ProxyManagementPanel from "../components/ProxyManagementPanel"; +import { Badge } from "../components/ui/badge"; import { Button } from "../components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../components/ui/card"; import { Tabs, TabsContent, @@ -17,6 +25,7 @@ import { TooltipTrigger, } from "../components/ui/tooltip"; import { cn } from "../lib/utils"; +import { getSecurityScore } from "../lib/securityScore"; import { useBrowserProfileStore } from "../stores/browserProfileStore"; import { useBrowserTabsStore } from "../stores/browserTabsStore"; import { useProxyStore } from "../stores/proxyStore"; @@ -62,12 +71,15 @@ export function AntiBrowserView({ children }) { [openUrlTab, profiles, setActiveProfile] ); - const stats = { - proxies: proxies.length, - activeProxies: proxies.filter((p) => p.status === "active").length, - profiles: profiles.length, - runningProfiles: profiles.filter((p) => p.status === "running").length, - }; + const { + stats, + activeProxy, + protectionChecks, + protectionScore, + normalizedScore, + scoreLabel, + scoreTone, + } = getSecurityScore({ profiles, proxies, activeProfileId }); return (
@@ -133,6 +145,14 @@ export function AntiBrowserView({ children }) { )} + + + Security Score + Score + @@ -203,6 +223,251 @@ export function AntiBrowserView({ children }) { {activeMainTab === "browser" && (
{children}
)} + + {activeMainTab === "score" && ( +
+
+ + +
+
+ + Anti-Browser Security Score + + + Confidence indicator for your current protection setup. + +
+ + {scoreLabel} Trust + +
+
+ +
+
+

+ {normalizedScore} +

+

+ of 100 secure points +

+
+
+

Based on proxy routing and profile protections.

+

Enable more safeguards to improve trust.

+
+
+
+
+
+
+
+ Profiles configured + + {stats.profiles} + +
+
+ Active proxies + + {stats.activeProxies} + +
+
+ Running profiles + + {stats.runningProfiles} + +
+
+ Active profile + + {activeProfileId ? "Selected" : "None"} + +
+
+ Protection checks enabled + + {protectionScore}/{protectionChecks.length} + +
+
+ Active proxy status + + {activeProxy?.status || "Not set"} + +
+
+ + +
+ + + Network identity + + Basic IP and routing details for the active profile. + + + +
+ Exit IP / Host + + {activeProxy?.host || "Not set"} + +
+
+ Proxy type + + {activeProxy?.type || "Not set"} + +
+
+ Location + + {activeProxy?.country || activeProxy?.city + ? `${activeProxy?.city || "Unknown"}, ${ + activeProxy?.country || "Unknown" + }` + : "Unknown"} + +
+
+ ISP + + {activeProxy?.isp || "Unknown"} + +
+
+
+ + + Protection coverage + + Shows which anti-fingerprint checks are active. + + + +
    + {protectionChecks.map((check) => ( +
  • + {check.label} + + {check.enabled ? "Enabled" : "Disabled"} + +
  • + ))} +
+
+
+
+
+
+ + + Next improvements + + Follow these steps to raise your security score. + + + +
    +
  • + Activate a browser profile for protection. + + {activeProfileId ? "Done" : "Pending"} + +
  • +
  • + Assign a working proxy to the active profile. + + {activeProxy?.status === "active" ? "Active" : "Pending"} + +
  • +
  • + Start at least one profile session. + 0 ? "default" : "outline" + } + > + {stats.runningProfiles > 0 ? "Running" : "Pending"} + +
  • +
  • + Enable all fingerprint protections. + + {protectionScore === protectionChecks.length + ? "Complete" + : "Pending"} + +
  • +
+
+
+ + + Score checklist + + Improve trust by completing these safeguards. + + + +
    +
  • + At least one browser profile is configured. + 0 ? "default" : "outline"}> + {stats.profiles > 0 ? "Done" : "Pending"} + +
  • +
  • + Attach a live proxy for session routing. + 0 ? "default" : "outline"} + > + {stats.activeProxies > 0 ? "Active" : "Pending"} + +
  • +
  • + Start a profile to activate protections. + 0 ? "default" : "outline" + } + > + {stats.runningProfiles > 0 ? "Running" : "Pending"} + +
  • +
  • + Confirm an active profile is selected. + + {activeProfileId ? "Selected" : "Pending"} + +
  • +
+
+
+
+
+ )}
); diff --git a/apps/ui/tests/unit/security-score.test.js b/apps/ui/tests/unit/security-score.test.js new file mode 100644 index 0000000..f9b4d03 --- /dev/null +++ b/apps/ui/tests/unit/security-score.test.js @@ -0,0 +1,57 @@ +describe("getSecurityScore", () => { + test("returns a low baseline score when no data is configured", async () => { + const { getSecurityScore } = await import( + "../../src/renderer/src/lib/securityScore.js" + ); + + const result = getSecurityScore(); + + expect(result.normalizedScore).toBe(30); + expect(result.scoreLabel).toBe("Low"); + expect(result.stats).toEqual({ + proxies: 0, + activeProxies: 0, + profiles: 0, + runningProfiles: 0, + }); + expect(result.protectionScore).toBe(0); + }); + + test("reaches a high score with active protections and proxy", async () => { + const { getSecurityScore } = await import( + "../../src/renderer/src/lib/securityScore.js" + ); + + const profiles = [ + { + id: "profile-1", + status: "running", + proxyId: "proxy-1", + settings: { + blockWebRTC: true, + blockCanvasFingerprint: true, + blockAudioFingerprint: true, + blockWebGL: true, + blockFonts: true, + blockGeolocation: true, + }, + }, + ]; + const proxies = [ + { + id: "proxy-1", + status: "active", + }, + ]; + + const result = getSecurityScore({ + profiles, + proxies, + activeProfileId: "profile-1", + }); + + expect(result.protectionScore).toBe(6); + expect(result.normalizedScore).toBe(100); + expect(result.scoreLabel).toBe("High"); + }); +}); From f0e6b0d9e71b9eb620aa44f32172d18e6d7bb733 Mon Sep 17 00:00:00 2001 From: peopleinfo <49970466+peopleinfo@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:58:28 +0700 Subject: [PATCH 02/11] Refactor security score checklist logic --- apps/ui/src/renderer/src/lib/securityScore.js | 83 +++++++++++++++-- .../renderer/src/views/AntiBrowserView.jsx | 92 +++++-------------- 2 files changed, 98 insertions(+), 77 deletions(-) diff --git a/apps/ui/src/renderer/src/lib/securityScore.js b/apps/ui/src/renderer/src/lib/securityScore.js index cede793..d306f2d 100644 --- a/apps/ui/src/renderer/src/lib/securityScore.js +++ b/apps/ui/src/renderer/src/lib/securityScore.js @@ -1,36 +1,43 @@ -const buildProtectionChecks = (profile) => [ +const PROTECTION_CHECK_DEFINITIONS = [ { id: "webrtc", label: "WebRTC leak protection", - enabled: Boolean(profile?.settings?.blockWebRTC), + settingKey: "blockWebRTC", }, { id: "canvas", label: "Canvas fingerprint masking", - enabled: Boolean(profile?.settings?.blockCanvasFingerprint), + settingKey: "blockCanvasFingerprint", }, { id: "audio", label: "Audio fingerprint masking", - enabled: Boolean(profile?.settings?.blockAudioFingerprint), + settingKey: "blockAudioFingerprint", }, { id: "webgl", label: "WebGL shielding", - enabled: Boolean(profile?.settings?.blockWebGL), + settingKey: "blockWebGL", }, { id: "fonts", label: "Font enumeration blocking", - enabled: Boolean(profile?.settings?.blockFonts), + settingKey: "blockFonts", }, { id: "geo", label: "Geolocation guarding", - enabled: Boolean(profile?.settings?.blockGeolocation), + settingKey: "blockGeolocation", }, ]; +const buildProtectionChecks = (profile) => + PROTECTION_CHECK_DEFINITIONS.map((check) => ({ + id: check.id, + label: check.label, + enabled: Boolean(profile?.settings?.[check.settingKey]), + })); + const getScoreTone = (normalizedScore) => { if (normalizedScore >= 80) return "bg-emerald-500"; if (normalizedScore >= 60) return "bg-amber-500"; @@ -73,6 +80,66 @@ export const getSecurityScore = ({ (activeProxy?.status === "active" ? 15 : 0) + protectionScore * 5; const normalizedScore = Math.min(Math.max(rawScore, 0), 100); + const checklistItems = [ + { + id: "profiles", + label: "At least one browser profile is configured.", + done: stats.profiles > 0, + doneLabel: "Done", + pendingLabel: "Pending", + }, + { + id: "proxy", + label: "Attach a live proxy for session routing.", + done: stats.activeProxies > 0, + doneLabel: "Active", + pendingLabel: "Pending", + }, + { + id: "running", + label: "Start a profile to activate protections.", + done: stats.runningProfiles > 0, + doneLabel: "Running", + pendingLabel: "Pending", + }, + { + id: "active-profile", + label: "Confirm an active profile is selected.", + done: Boolean(activeProfileId), + doneLabel: "Selected", + pendingLabel: "Pending", + }, + ]; + const improvementItems = [ + { + id: "activate-profile", + label: "Activate a browser profile for protection.", + done: Boolean(activeProfileId), + doneLabel: "Done", + pendingLabel: "Pending", + }, + { + id: "active-proxy", + label: "Assign a working proxy to the active profile.", + done: activeProxy?.status === "active", + doneLabel: "Active", + pendingLabel: "Pending", + }, + { + id: "run-profile", + label: "Start at least one profile session.", + done: stats.runningProfiles > 0, + doneLabel: "Running", + pendingLabel: "Pending", + }, + { + id: "enable-protections", + label: "Enable all fingerprint protections.", + done: protectionScore === protectionChecks.length, + doneLabel: "Complete", + pendingLabel: "Pending", + }, + ]; return { stats, @@ -81,6 +148,8 @@ export const getSecurityScore = ({ protectionChecks, protectionScore, normalizedScore, + checklistItems, + improvementItems, scoreLabel: getScoreLabel(normalizedScore), scoreTone: getScoreTone(normalizedScore), }; diff --git a/apps/ui/src/renderer/src/views/AntiBrowserView.jsx b/apps/ui/src/renderer/src/views/AntiBrowserView.jsx index db58f48..ff48eb4 100644 --- a/apps/ui/src/renderer/src/views/AntiBrowserView.jsx +++ b/apps/ui/src/renderer/src/views/AntiBrowserView.jsx @@ -76,6 +76,8 @@ export function AntiBrowserView({ children }) { activeProxy, protectionChecks, protectionScore, + checklistItems, + improvementItems, normalizedScore, scoreLabel, scoreTone, @@ -378,48 +380,17 @@ export function AntiBrowserView({ children }) { @@ -432,36 +403,17 @@ export function AntiBrowserView({ children }) { From 112fabf8e1d03a6cc9710d1b070c579483871de8 Mon Sep 17 00:00:00 2001 From: zila Date: Fri, 9 Jan 2026 23:52:10 +0700 Subject: [PATCH 03/11] fix(anti-detection): improve header generation robustness and add tests --- apps/ui/src/main/anti-detection/index.js | 45 ++++++++++++++--------- apps/ui/tests/unit/stealth-script.test.js | 32 ++++++++++++++++ 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/apps/ui/src/main/anti-detection/index.js b/apps/ui/src/main/anti-detection/index.js index 0d570c4..7a075ca 100644 --- a/apps/ui/src/main/anti-detection/index.js +++ b/apps/ui/src/main/anti-detection/index.js @@ -185,9 +185,11 @@ async function setupSessionAntiDetection(ses, profile) { // Add SEC headers for Chromium browsers if (userAgent && userAgent.includes("Chrome")) { - headers["Sec-CH-UA"] = generateSecChUa(profile); - headers["Sec-CH-UA-Mobile"] = profile.category === "mobile" ? "?1" : "?0"; - headers["Sec-CH-UA-Platform"] = getPlatformHint(profile); + const profileWithUserAgent = { ...profile, userAgent }; + headers["Sec-CH-UA"] = generateSecChUa(profileWithUserAgent); + headers["Sec-CH-UA-Mobile"] = + profile?.category === "mobile" ? "?1" : "?0"; + headers["Sec-CH-UA-Platform"] = getPlatformHint(profileWithUserAgent); headers["Sec-Fetch-Dest"] = headers["Sec-Fetch-Dest"] || "document"; headers["Sec-Fetch-Mode"] = headers["Sec-Fetch-Mode"] || "navigate"; headers["Sec-Fetch-Site"] = headers["Sec-Fetch-Site"] || "none"; @@ -245,16 +247,21 @@ async function setupSessionAntiDetection(ses, profile) { * Generate Sec-CH-UA header value */ function generateSecChUa(profile) { - if (profile.userAgent.includes("Chrome")) { - const match = profile.userAgent.match(/Chrome\/(\d+)/); + const userAgent = + typeof profile?.userAgent === "string" ? profile.userAgent : ""; + + if (userAgent.includes("Edg")) { + const match = userAgent.match(/Edg\/(\d+)/); const version = match ? match[1] : "122"; - return `"Chromium";v="${version}", "Google Chrome";v="${version}", "Not(A:Brand";v="24"`; + return `"Chromium";v="${version}", "Microsoft Edge";v="${version}", "Not(A:Brand";v="24"`; } - if (profile.userAgent.includes("Edg")) { - const match = profile.userAgent.match(/Edg\/(\d+)/); + + if (userAgent.includes("Chrome")) { + const match = userAgent.match(/Chrome\/(\d+)/); const version = match ? match[1] : "122"; - return `"Chromium";v="${version}", "Microsoft Edge";v="${version}", "Not(A:Brand";v="24"`; + return `"Chromium";v="${version}", "Google Chrome";v="${version}", "Not(A:Brand";v="24"`; } + return `"Chromium";v="122", "Not(A:Brand";v="24"`; } @@ -262,14 +269,16 @@ function generateSecChUa(profile) { * Get platform hint from profile */ function getPlatformHint(profile) { - if (profile.platform.includes("Win")) return '"Windows"'; - if (profile.platform.includes("Mac")) return '"macOS"'; - if (profile.platform.includes("Linux")) return '"Linux"'; - if (profile.platform.includes("iPhone")) return '"iOS"'; - if ( - profile.platform.includes("armv") || - profile.userAgent.includes("Android") - ) + const platform = + typeof profile?.platform === "string" ? profile.platform : ""; + const userAgent = + typeof profile?.userAgent === "string" ? profile.userAgent : ""; + + if (platform.includes("Win")) return '"Windows"'; + if (platform.includes("Mac")) return '"macOS"'; + if (platform.includes("Linux")) return '"Linux"'; + if (platform.includes("iPhone")) return '"iOS"'; + if (platform.includes("armv") || userAgent.includes("Android")) return '"Android"'; return '"Unknown"'; } @@ -464,4 +473,6 @@ module.exports = { getAllProfiles, getProfile, getRandomProfile, + generateSecChUa, + getPlatformHint, }; diff --git a/apps/ui/tests/unit/stealth-script.test.js b/apps/ui/tests/unit/stealth-script.test.js index eaf3985..42f513a 100644 --- a/apps/ui/tests/unit/stealth-script.test.js +++ b/apps/ui/tests/unit/stealth-script.test.js @@ -9,6 +9,10 @@ const { generateMinimalStealthScript, } = require("../../src/main/anti-detection/stealth-script"); const { getProfile } = require("../../src/main/anti-detection/device-profiles"); +const { + generateSecChUa, + getPlatformHint, +} = require("../../src/main/anti-detection"); test.describe("Stealth Script Generator", () => { test.describe("generateStealthScript()", () => { @@ -249,3 +253,31 @@ test.describe("Stealth Script Content Analysis", () => { expect(script).toContain("console.debug"); }); }); + +test.describe("Anti-Detection Header Helpers", () => { + test("generateSecChUa() should not throw for missing profile", () => { + expect(() => generateSecChUa()).not.toThrow(); + expect(typeof generateSecChUa()).toBe("string"); + }); + + test("generateSecChUa() should extract Chrome major version", () => { + const value = generateSecChUa({ + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + }); + expect(value).toContain('"Google Chrome";v="123"'); + }); + + test("generateSecChUa() should prefer Edge when UA contains Edg", () => { + const value = generateSecChUa({ + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", + }); + expect(value).toContain('"Microsoft Edge";v="122"'); + }); + + test("getPlatformHint() should not throw for missing profile", () => { + expect(() => getPlatformHint()).not.toThrow(); + expect(getPlatformHint()).toBe('"Unknown"'); + }); +}); From 84adf918b723107a812392fc656bcde1a1c25f58 Mon Sep 17 00:00:00 2001 From: zila Date: Sat, 10 Jan 2026 00:56:07 +0700 Subject: [PATCH 04/11] fix(AntiBrowserView): hide browser view when switching tabs --- apps/ui/src/renderer/src/views/AntiBrowserView.jsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/renderer/src/views/AntiBrowserView.jsx b/apps/ui/src/renderer/src/views/AntiBrowserView.jsx index ff48eb4..c06262f 100644 --- a/apps/ui/src/renderer/src/views/AntiBrowserView.jsx +++ b/apps/ui/src/renderer/src/views/AntiBrowserView.jsx @@ -1,5 +1,5 @@ import { Globe, Network, Settings2, Shield, ShieldCheck, Users } from "lucide-react"; -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import BrowserProfilesPanel from "../components/BrowserProfilesPanel"; import ProxyManagementPanel from "../components/ProxyManagementPanel"; @@ -43,6 +43,13 @@ export function AntiBrowserView({ children }) { const proxies = useProxyStore((s) => s.proxies); const openUrlTab = useBrowserTabsStore((s) => s.openUrlTab); + useEffect(() => { + if (activeMainTab === "browser") return; + if (window.electronAPI?.browserView?.hideAll) { + window.electronAPI.browserView.hideAll().catch(() => {}); + } + }, [activeMainTab]); + const handleProxySelect = useCallback((id) => { setSelectedProxyId(id); }, []); From b0629cb84fa655b697752acf23145b5e7a4b71fb Mon Sep 17 00:00:00 2001 From: zila Date: Sat, 10 Jan 2026 01:15:01 +0700 Subject: [PATCH 05/11] style(ui): adjust padding, spacing and dimensions for consistency --- apps/ui/src/renderer/src/layouts/MainLayout.jsx | 4 ++-- apps/ui/src/renderer/src/views/LaunchpadView.jsx | 10 +++++----- apps/ui/src/renderer/src/views/ResourcesView.jsx | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/ui/src/renderer/src/layouts/MainLayout.jsx b/apps/ui/src/renderer/src/layouts/MainLayout.jsx index 82b0ff3..c80a008 100644 --- a/apps/ui/src/renderer/src/layouts/MainLayout.jsx +++ b/apps/ui/src/renderer/src/layouts/MainLayout.jsx @@ -909,7 +909,7 @@ export default function MainLayout({
diff --git a/apps/ui/src/renderer/src/views/LaunchpadView.jsx b/apps/ui/src/renderer/src/views/LaunchpadView.jsx index 17e8d36..7f11630 100644 --- a/apps/ui/src/renderer/src/views/LaunchpadView.jsx +++ b/apps/ui/src/renderer/src/views/LaunchpadView.jsx @@ -421,7 +421,7 @@ export default function LaunchpadView() {
-
+
) : (
-
+
{pagedGridActions.map((a) => { const Icon = a.icon; return ( @@ -568,13 +568,13 @@ export default function LaunchpadView() { > - + diff --git a/apps/ui/src/renderer/src/views/ResourcesView.jsx b/apps/ui/src/renderer/src/views/ResourcesView.jsx index 5660dac..4fd92a4 100644 --- a/apps/ui/src/renderer/src/views/ResourcesView.jsx +++ b/apps/ui/src/renderer/src/views/ResourcesView.jsx @@ -1,8 +1,8 @@ -import React, { useEffect, useMemo, useState } from "react"; import { Button, Input, Modal, + message, Popconfirm, Segmented, Select, @@ -10,8 +10,8 @@ import { Table, Tag, Tooltip, - message, } from "antd"; +import React, { useEffect, useMemo, useState } from "react"; import { useLocation } from "react-router-dom"; import { useResourceStore } from "../stores/resourceStore"; @@ -19,11 +19,11 @@ const COLUMN_LAYOUT = { table: { background: "var(--color-bg-container)", borderRadius: 12, - padding: 16, + padding: 12, border: "1px solid var(--color-border)", }, header: { - marginBottom: 16, + marginBottom: 12, display: "flex", alignItems: "center", justifyContent: "space-between", @@ -433,8 +433,8 @@ export default function ResourcesView() { return (
Date: Sat, 10 Jan 2026 01:58:33 +0700 Subject: [PATCH 06/11] feat(browser): add loading state to browser tabs --- apps/ui/src/main/index.js | 4 + .../renderer/src/stores/browserTabsStore.js | 1 + .../renderer/src/views/BrowserToolView.jsx | 104 +++++++++++++++++- 3 files changed, 105 insertions(+), 4 deletions(-) diff --git a/apps/ui/src/main/index.js b/apps/ui/src/main/index.js index 6ad7d21..17beb83 100644 --- a/apps/ui/src/main/index.js +++ b/apps/ui/src/main/index.js @@ -828,6 +828,7 @@ function notifyBrowserState(tabId) { url: view.webContents.getURL(), canGoBack: view.webContents.canGoBack(), canGoForward: view.webContents.canGoForward(), + isLoading: view.webContents.isLoading(), }); } @@ -895,6 +896,9 @@ async function ensureBrowserView(tabId, options = {}) { view.webContents.on("did-navigate", () => notifyBrowserState(tabId)); view.webContents.on("did-navigate-in-page", () => notifyBrowserState(tabId)); + view.webContents.on("did-start-loading", () => notifyBrowserState(tabId)); + view.webContents.on("did-stop-loading", () => notifyBrowserState(tabId)); + view.webContents.on("did-fail-load", () => notifyBrowserState(tabId)); // Inject anti-detection stealth script after page load view.webContents.on("did-finish-load", async () => { diff --git a/apps/ui/src/renderer/src/stores/browserTabsStore.js b/apps/ui/src/renderer/src/stores/browserTabsStore.js index 85c5543..f802281 100644 --- a/apps/ui/src/renderer/src/stores/browserTabsStore.js +++ b/apps/ui/src/renderer/src/stores/browserTabsStore.js @@ -106,6 +106,7 @@ export const useBrowserTabsStore = create( url: trimmed, canGoBack: false, canGoForward: false, + isLoading: true, }, }, })); diff --git a/apps/ui/src/renderer/src/views/BrowserToolView.jsx b/apps/ui/src/renderer/src/views/BrowserToolView.jsx index 83b7feb..9e7480a 100644 --- a/apps/ui/src/renderer/src/views/BrowserToolView.jsx +++ b/apps/ui/src/renderer/src/views/BrowserToolView.jsx @@ -8,6 +8,7 @@ import { FileJson, Globe, Image, + Loader2, Maximize2, Minimize2, Monitor, @@ -69,8 +70,8 @@ import { TooltipTrigger, } from "../components/ui/tooltip"; import { cn } from "../lib/utils"; -import { useBrowserTabsStore } from "../stores/browserTabsStore"; import { useBrowserProfileStore } from "../stores/browserProfileStore"; +import { useBrowserTabsStore } from "../stores/browserTabsStore"; import { useProxyStore } from "../stores/proxyStore"; import { useResourceStore } from "../stores/resourceStore"; import { copyToClipboard, generateElementCode } from "../utils/codeGenerator"; @@ -549,6 +550,56 @@ function Dashboard({ onOpenUrl }) { group: "Docs", }, { title: "Lucide Icons", url: "https://lucide.dev", group: "Docs" }, + { + title: "ChatGPT", + url: "https://chatgpt.com", + group: "AI", + }, + { + title: "Claude", + url: "https://claude.ai", + group: "AI", + }, + { + title: "Gemini", + url: "https://gemini.google.com", + group: "AI", + }, + { + title: "Perplexity", + url: "https://www.perplexity.ai", + group: "AI", + }, + { + title: "Grok", + url: "https://grok.com", + group: "AI", + }, + { + title: "DeepSeek", + url: "https://chat.deepseek.com", + group: "AI", + }, + { + title: "Qwen", + url: "https://chat.qwen.ai", + group: "AI", + }, + { + title: "Kimi K2", + url: "https://kimi.moonshot.cn", + group: "AI", + }, + { + title: "Mistral", + url: "https://chat.mistral.ai", + group: "AI", + }, + { + title: "Doubao", + url: "https://www.doubao.com", + group: "AI", + }, { title: "Shadcn Chat", url: "https://context7.com/websites/ui_shadcn?tab=chat", @@ -646,9 +697,10 @@ function Dashboard({ onOpenUrl }) { (it) => it.group === "Entertainment" ); const musicItems = filtered.filter((it) => it.group === "Music"); + const aiItems = filtered.filter((it) => it.group === "AI"); const docsItems = filtered.filter((it) => it.group === "Docs"); const moreItems = filtered.filter( - (it) => !["Entertainment", "Music", "Docs"].includes(it.group) + (it) => !["Entertainment", "Music", "AI", "Docs"].includes(it.group) ); return ( @@ -682,6 +734,7 @@ function Dashboard({ onOpenUrl }) { Docs Entertainment Music + AI More @@ -718,6 +771,23 @@ function Dashboard({ onOpenUrl }) { ))}
+ +
+ {aiItems.map((it) => ( + + ))} +
+
{docsItems.map((it) => ( @@ -2326,6 +2396,7 @@ export default function BrowserToolView() { url: payload.url, canGoBack: payload.canGoBack, canGoForward: payload.canGoForward, + isLoading: payload.isLoading, }); if (payload.url) addHistoryEntry(payload.url, payload.url); }); @@ -2457,7 +2528,7 @@ export default function BrowserToolView() { if (!activeIsBrowser) return; try { - updateTabState(resolvedActiveTabId, { url }); + updateTabState(resolvedActiveTabId, { url, isLoading: true }); addHistoryEntry(url, url); if (hasElectronView) { @@ -2503,6 +2574,9 @@ export default function BrowserToolView() { > {tabs.map((t, idx) => { const isActive = t.id === resolvedActiveTabId; + const tState = tabStateById[t.id] || {}; + const isLoading = tState.isLoading; + return (
+ {isLoading && ( + + )} {t.title} @@ -2636,12 +2713,18 @@ export default function BrowserToolView() { return; } if (iframeRef.current) { + updateTabState(resolvedActiveTabId, { isLoading: true }); iframeRef.current.contentWindow?.location?.reload?.(); } }} aria-label="Reload" > - +
@@ -2742,12 +2825,25 @@ export default function BrowserToolView() { {activeTab?.kind === "browser" ? (
+ {activeTabState.isLoading && ( +
+
+ + + Loading... + +
+
+ )} {!hasElectronView ? (