diff --git a/src/App.vue b/src/App.vue
index b4e9897..f02e1ac 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,221 +1,29 @@
diff --git a/src/features/browser-assistant/sort.ts b/src/features/browser-assistant/sort.ts
new file mode 100644
index 0000000..e41ee26
--- /dev/null
+++ b/src/features/browser-assistant/sort.ts
@@ -0,0 +1,76 @@
+import type {
+ BookmarkSortKey,
+ BookmarkSummary,
+ ExtensionSortKey,
+ ExtensionSummary,
+ ProfileSortKey,
+ ProfileSummary,
+} from "./types";
+
+export function compareText(left: string, right: string) {
+ return left.localeCompare(right, undefined, {
+ sensitivity: "base",
+ numeric: true,
+ });
+}
+
+export function compareProfileId(left: string, right: string) {
+ const leftKey = profileSortKeyValue(left);
+ const rightKey = profileSortKeyValue(right);
+ if (leftKey.group !== rightKey.group) return leftKey.group - rightKey.group;
+ if (leftKey.number !== rightKey.number) return leftKey.number - rightKey.number;
+ return compareText(leftKey.text, rightKey.text);
+}
+
+export function sortProfiles(items: ProfileSummary[], sortKey: ProfileSortKey) {
+ const profiles = [...items];
+ return profiles.sort((left, right) => {
+ if (sortKey === "email") {
+ return (
+ compareText(left.email ?? "", right.email ?? "") ||
+ compareText(left.name, right.name) ||
+ compareProfileId(left.id, right.id)
+ );
+ }
+ if (sortKey === "id") {
+ return compareProfileId(left.id, right.id);
+ }
+ return compareText(left.name, right.name) || compareProfileId(left.id, right.id);
+ });
+}
+
+export function sortExtensions(items: ExtensionSummary[], sortKey: ExtensionSortKey) {
+ const extensions = [...items];
+ return extensions.sort((left, right) => {
+ if (sortKey === "id") {
+ return compareText(left.id, right.id);
+ }
+ return compareText(left.name, right.name) || compareText(left.id, right.id);
+ });
+}
+
+export function sortBookmarks(items: BookmarkSummary[], sortKey: BookmarkSortKey) {
+ const bookmarks = [...items];
+ return bookmarks.sort((left, right) => {
+ if (sortKey === "url") {
+ return compareText(left.url, right.url);
+ }
+ return compareText(left.title, right.title) || compareText(left.url, right.url);
+ });
+}
+
+function profileSortKeyValue(profileId: string) {
+ if (profileId === "Default") {
+ return { group: 0, number: 0, text: profileId };
+ }
+
+ const profileNumber = profileId.startsWith("Profile ")
+ ? Number(profileId.slice("Profile ".length))
+ : Number.NaN;
+
+ if (!Number.isNaN(profileNumber)) {
+ return { group: 1, number: profileNumber, text: profileId };
+ }
+
+ return { group: 2, number: Number.MAX_SAFE_INTEGER, text: profileId };
+}
diff --git a/src/features/browser-assistant/types.ts b/src/features/browser-assistant/types.ts
new file mode 100644
index 0000000..1bea25d
--- /dev/null
+++ b/src/features/browser-assistant/types.ts
@@ -0,0 +1,47 @@
+export type BrowserStats = {
+ profileCount: number;
+ extensionCount: number;
+ bookmarkCount: number;
+};
+
+export type ProfileSummary = {
+ id: string;
+ name: string;
+ email: string | null;
+ avatarDataUrl: string | null;
+ avatarLabel: string;
+ path: string;
+};
+
+export type ExtensionSummary = {
+ id: string;
+ name: string;
+ version: string | null;
+ iconDataUrl: string | null;
+ profileIds: string[];
+};
+
+export type BookmarkSummary = {
+ url: string;
+ title: string;
+ profileIds: string[];
+};
+
+export type ProfileSortKey = "name" | "email" | "id";
+export type ExtensionSortKey = "name" | "id";
+export type BookmarkSortKey = "title" | "url";
+export type ActiveSection = "profiles" | "extensions" | "bookmarks";
+
+export type BrowserView = {
+ browserId: string;
+ browserName: string;
+ dataRoot: string;
+ profiles: ProfileSummary[];
+ extensions: ExtensionSummary[];
+ bookmarks: BookmarkSummary[];
+ stats: BrowserStats;
+};
+
+export type ScanResponse = {
+ browsers: BrowserView[];
+};
diff --git a/src/features/browser-assistant/useBrowserAssistant.ts b/src/features/browser-assistant/useBrowserAssistant.ts
new file mode 100644
index 0000000..43276ec
--- /dev/null
+++ b/src/features/browser-assistant/useBrowserAssistant.ts
@@ -0,0 +1,157 @@
+import { computed, onMounted, ref, watch } from "vue";
+import { invoke } from "@tauri-apps/api/core";
+
+import { sortBookmarks, sortExtensions, sortProfiles } from "./sort";
+import type {
+ ActiveSection,
+ BookmarkSortKey,
+ BrowserView,
+ ExtensionSortKey,
+ ProfileSortKey,
+ ScanResponse,
+} from "./types";
+
+export function useBrowserAssistant() {
+ const loading = ref(true);
+ const error = ref("");
+ const response = ref({ browsers: [] });
+ const selectedBrowserId = ref("");
+ const activeSection = ref("profiles");
+ const expandedExtensionIds = ref([]);
+ const expandedBookmarkUrls = ref([]);
+ const profileSortKey = ref("name");
+ const extensionSortKey = ref("name");
+ const bookmarkSortKey = ref("title");
+
+ const browsers = computed(() => response.value.browsers);
+ const currentBrowser = computed(
+ () =>
+ browsers.value.find((browser) => browser.browserId === selectedBrowserId.value) ??
+ browsers.value[0] ??
+ null,
+ );
+
+ const sortedProfiles = computed(() =>
+ sortProfiles(currentBrowser.value?.profiles ?? [], profileSortKey.value),
+ );
+ const sortedExtensions = computed(() =>
+ sortExtensions(currentBrowser.value?.extensions ?? [], extensionSortKey.value),
+ );
+ const sortedBookmarks = computed(() =>
+ sortBookmarks(currentBrowser.value?.bookmarks ?? [], bookmarkSortKey.value),
+ );
+
+ watch(
+ browsers,
+ (items) => {
+ if (!items.length) {
+ selectedBrowserId.value = "";
+ return;
+ }
+
+ const hasSelected = items.some(
+ (browser) => browser.browserId === selectedBrowserId.value,
+ );
+
+ if (!hasSelected) {
+ selectedBrowserId.value = items[0].browserId;
+ }
+ },
+ { immediate: true },
+ );
+
+ watch(selectedBrowserId, () => {
+ expandedExtensionIds.value = [];
+ expandedBookmarkUrls.value = [];
+ });
+
+ async function scanBrowsers() {
+ loading.value = true;
+ error.value = "";
+
+ try {
+ response.value = await invoke("scan_browsers");
+ } catch (scanError) {
+ error.value =
+ scanError instanceof Error
+ ? scanError.message
+ : "Failed to scan browser data.";
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ function browserMonogram(browserId: string) {
+ if (browserId === "chrome") return "CH";
+ if (browserId === "edge") return "ED";
+ if (browserId === "brave") return "BR";
+ return browserId.slice(0, 2).toUpperCase();
+ }
+
+ function extensionMonogram(name: string) {
+ return name.trim().slice(0, 1).toUpperCase() || "?";
+ }
+
+ function domainFromUrl(url: string) {
+ try {
+ return new URL(url).hostname;
+ } catch {
+ return url;
+ }
+ }
+
+ function sectionCount(section: ActiveSection) {
+ if (!currentBrowser.value) return 0;
+ if (section === "profiles") return currentBrowser.value.profiles.length;
+ if (section === "extensions") return currentBrowser.value.extensions.length;
+ return currentBrowser.value.bookmarks.length;
+ }
+
+ function toggleExtensionProfiles(extensionId: string) {
+ expandedExtensionIds.value = expandedExtensionIds.value.includes(extensionId)
+ ? expandedExtensionIds.value.filter((id) => id !== extensionId)
+ : [...expandedExtensionIds.value, extensionId];
+ }
+
+ function toggleBookmarkProfiles(url: string) {
+ expandedBookmarkUrls.value = expandedBookmarkUrls.value.includes(url)
+ ? expandedBookmarkUrls.value.filter((value) => value !== url)
+ : [...expandedBookmarkUrls.value, url];
+ }
+
+ function extensionProfilesExpanded(extensionId: string) {
+ return expandedExtensionIds.value.includes(extensionId);
+ }
+
+ function bookmarkProfilesExpanded(url: string) {
+ return expandedBookmarkUrls.value.includes(url);
+ }
+
+ onMounted(() => {
+ void scanBrowsers();
+ });
+
+ return {
+ activeSection,
+ bookmarkProfilesExpanded,
+ bookmarkSortKey,
+ browserMonogram,
+ browsers,
+ currentBrowser,
+ domainFromUrl,
+ error,
+ extensionMonogram,
+ extensionProfilesExpanded,
+ extensionSortKey,
+ loading,
+ profileSortKey,
+ scanBrowsers,
+ sectionCount,
+ selectedBrowserId,
+ sortedBookmarks,
+ sortedExtensions,
+ sortedProfiles,
+ toggleBookmarkProfiles,
+ toggleExtensionProfiles,
+ };
+}