Skip to content

Commit cd9866a

Browse files
committed
feat(browser-ext): implement createChromeStorageAdapter and refactor storage usage
- Added createChromeStorageAdapter function to provide a consistent interface for Chrome extension storage. - Refactored existing storage usage in hooks to utilize the new adapter. - Removed legacy storage utilities for cleaner codebase. - Enhanced storage functionality with real-time change watching and batch operations in the Storage class. - Updated imports to reflect the new storage structure.
1 parent aaffd33 commit cd9866a

File tree

11 files changed

+362
-154
lines changed

11 files changed

+362
-154
lines changed

packages/browser-ext/src/adapters/storage-adapter.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { KeyValueStorage } from "@aipexstudio/aipex-core";
2+
import type { StorageAdapter } from "../types/adapter";
23

34
/**
45
* ChromeStorageAdapter - Implements KeyValueStorage interface using Chrome Storage API
@@ -46,8 +47,83 @@ export class ChromeStorageAdapter<T> implements KeyValueStorage<T> {
4647
}
4748

4849
/**
49-
* Simple Chrome Storage wrapper for non-core usage
50-
* This is a convenience wrapper for browser-ext specific storage needs
50+
* Create a StorageAdapter implementation for Chrome extension storage
51+
* This implements the StorageAdapter interface used by hooks
52+
*/
53+
export function createChromeStorageAdapter(
54+
area: "local" | "sync" = "local",
55+
): StorageAdapter {
56+
return {
57+
async get<T>(key: string): Promise<T | undefined> {
58+
return new Promise((resolve) => {
59+
if (typeof chrome !== "undefined" && chrome.storage?.[area]) {
60+
chrome.storage[area].get(key, (result) => {
61+
resolve((result[key] as T | undefined) ?? undefined);
62+
});
63+
} else {
64+
// Fallback to localStorage in non-extension environments
65+
try {
66+
const value = localStorage.getItem(key);
67+
resolve(value ? (JSON.parse(value) as T) : undefined);
68+
} catch {
69+
resolve(undefined);
70+
}
71+
}
72+
});
73+
},
74+
async set<T>(key: string, value: T): Promise<void> {
75+
return new Promise((resolve) => {
76+
if (typeof chrome !== "undefined" && chrome.storage?.[area]) {
77+
chrome.storage[area].set({ [key]: value }, () => resolve());
78+
} else {
79+
// Fallback to localStorage
80+
localStorage.setItem(key, JSON.stringify(value));
81+
resolve();
82+
}
83+
});
84+
},
85+
async remove(key: string): Promise<void> {
86+
return new Promise((resolve) => {
87+
if (typeof chrome !== "undefined" && chrome.storage?.[area]) {
88+
chrome.storage[area].remove(key, () => resolve());
89+
} else {
90+
// Fallback to localStorage
91+
localStorage.removeItem(key);
92+
resolve();
93+
}
94+
});
95+
},
96+
async clear(): Promise<void> {
97+
return new Promise((resolve) => {
98+
if (typeof chrome !== "undefined" && chrome.storage?.[area]) {
99+
chrome.storage[area].clear(() => resolve());
100+
} else {
101+
// Fallback to localStorage
102+
localStorage.clear();
103+
resolve();
104+
}
105+
});
106+
},
107+
};
108+
}
109+
110+
/**
111+
* Storage - A feature-rich Chrome Storage wrapper
112+
*
113+
* This class provides additional features beyond the StorageAdapter interface:
114+
* - Real-time change watching (watch method)
115+
* - Batch operations (getAll)
116+
* - Direct Chrome Storage API access
117+
*
118+
* Use this class when you need:
119+
* 1. To watch for storage changes in real-time
120+
* 2. Browser-extension specific storage needs
121+
* 3. More direct control over Chrome Storage API
122+
*
123+
* Use StorageAdapter (createChromeStorageAdapter) when:
124+
* 1. You need a simple get/set/remove interface
125+
* 2. You want consistent interface for testing
126+
* 3. You're working with hook-based configuration (like useChatConfig)
51127
*/
52128
export class Storage {
53129
private area: chrome.storage.StorageArea;
@@ -81,6 +157,10 @@ export class Storage {
81157
});
82158
}
83159

160+
/**
161+
* Watch for changes to a specific key
162+
* Returns an unwatch function to stop listening
163+
*/
84164
watch<T = any>(
85165
key: string,
86166
callback: (change: { newValue?: T; oldValue?: T }) => void,

packages/browser-ext/src/hooks/use-chat-config.ts

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useEffect, useState } from "react";
2+
import { createChromeStorageAdapter } from "../adapters/storage-adapter";
23
import type { ChatSettings, StorageAdapter } from "../types";
34

45
/**
@@ -43,47 +44,10 @@ const defaultStorageAdapter: StorageAdapter = {
4344

4445
/**
4546
* Chrome extension storage adapter
47+
* Exported for use in other hooks and components
4648
*/
47-
export const chromeStorageAdapter: StorageAdapter = {
48-
async get<T>(key: string): Promise<T | undefined> {
49-
return new Promise((resolve) => {
50-
if (typeof chrome !== "undefined" && chrome.storage?.local) {
51-
chrome.storage.local.get(key, (result) => {
52-
resolve((result[key] as T | undefined) ?? undefined);
53-
});
54-
} else {
55-
void defaultStorageAdapter.get<T>(key).then(resolve);
56-
}
57-
});
58-
},
59-
async set<T>(key: string, value: T): Promise<void> {
60-
return new Promise((resolve) => {
61-
if (typeof chrome !== "undefined" && chrome.storage?.local) {
62-
chrome.storage.local.set({ [key]: value }, () => resolve());
63-
} else {
64-
void defaultStorageAdapter.set(key, value).then(resolve);
65-
}
66-
});
67-
},
68-
async remove(key: string): Promise<void> {
69-
return new Promise((resolve) => {
70-
if (typeof chrome !== "undefined" && chrome.storage?.local) {
71-
chrome.storage.local.remove(key, () => resolve());
72-
} else {
73-
void defaultStorageAdapter.remove(key).then(resolve);
74-
}
75-
});
76-
},
77-
async clear(): Promise<void> {
78-
return new Promise((resolve) => {
79-
if (typeof chrome !== "undefined" && chrome.storage?.local) {
80-
chrome.storage.local.clear(() => resolve());
81-
} else {
82-
void defaultStorageAdapter.clear().then(resolve);
83-
}
84-
});
85-
},
86-
};
49+
export const chromeStorageAdapter: StorageAdapter =
50+
createChromeStorageAdapter();
8751

8852
export interface UseChatConfigOptions {
8953
/** Initial settings (will be overridden by stored values) */

packages/browser-ext/src/hooks/use-storage.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useMemo, useState } from "react";
22
import { Storage } from "../adapters/storage-adapter";
33

44
/**
@@ -11,9 +11,10 @@ export function useStorage<T = any>(
1111
const [value, setValue] = useState<T | undefined>(defaultValue);
1212
const [isLoading, setIsLoading] = useState(true);
1313

14-
useEffect(() => {
15-
const storage = new Storage();
14+
// Create storage instance once
15+
const storage = useMemo(() => new Storage(), []);
1616

17+
useEffect(() => {
1718
// Load initial value
1819
void storage.get<T>(key).then((storedValue: T | undefined) => {
1920
setValue(storedValue ?? defaultValue);
@@ -26,10 +27,9 @@ export function useStorage<T = any>(
2627
});
2728

2829
return unwatch;
29-
}, [key, defaultValue]);
30+
}, [key, defaultValue, storage]);
3031

3132
const setStoredValue = async (newValue: T) => {
32-
const storage = new Storage();
3333
await storage.set(key, newValue);
3434
setValue(newValue);
3535
};

packages/browser-ext/src/i18n/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Storage } from "~/lib/storage";
1+
import { Storage } from "~/adapters/storage-adapter";
22
// Language resource imports
33
import enTranslations from "./locales/en.json";
44
import zhTranslations from "./locales/zh.json";

packages/browser-ext/src/lib/context-providers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ export function searchContexts(
279279
const lowerQuery = query.toLowerCase();
280280
return contexts.filter(
281281
(ctx) =>
282-
ctx.label.toLowerCase().includes(lowerQuery) ??
282+
ctx.label.toLowerCase().includes(lowerQuery) ||
283283
ctx.value.toLowerCase().includes(lowerQuery),
284284
);
285285
}

packages/browser-ext/src/lib/services/host-access-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Storage } from "~/lib/storage";
1+
import { Storage } from "~/adapters/storage-adapter";
22

33
export type HostAccessMode = "whitelist" | "blocklist" | "include-all";
44

packages/browser-ext/src/lib/storage.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/browser-ext/src/tools/page-tools.ts

Lines changed: 18 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { tool } from "@aipexstudio/aipex-core";
22
import { z } from "zod/v3";
3+
import { executeScriptInActiveTab, getActiveTab } from "./utils";
34

45
/**
56
* Get information about the current active page
@@ -10,14 +11,7 @@ export const getPageInfoTool = tool({
1011
"Get information about the current active page (URL, title, etc.)",
1112
parameters: z.object({}),
1213
execute: async () => {
13-
const [tab] = await chrome.tabs.query({
14-
active: true,
15-
currentWindow: true,
16-
});
17-
18-
if (!tab) {
19-
throw new Error("No active tab found");
20-
}
14+
const tab = await getActiveTab();
2115

2216
return {
2317
url: tab.url,
@@ -45,18 +39,8 @@ export const scrollPageTool = tool({
4539
.describe("Number of pixels to scroll (for up/down)"),
4640
}),
4741
execute: async ({ direction, pixels = 500 }) => {
48-
const [tab] = await chrome.tabs.query({
49-
active: true,
50-
currentWindow: true,
51-
});
52-
53-
if (!tab?.id) {
54-
throw new Error("No active tab found");
55-
}
56-
57-
await chrome.scripting.executeScript({
58-
target: { tabId: tab.id },
59-
func: (dir: string, px: number) => {
42+
await executeScriptInActiveTab(
43+
(dir: string, px: number) => {
6044
switch (dir) {
6145
case "up":
6246
window.scrollBy({ top: -px, behavior: "smooth" });
@@ -75,8 +59,8 @@ export const scrollPageTool = tool({
7559
break;
7660
}
7761
},
78-
args: [direction, pixels],
79-
});
62+
[direction, pixels],
63+
);
8064

8165
return { success: true, direction, scrolled: pixels };
8266
},
@@ -125,25 +109,14 @@ export const getPageContentTool = tool({
125109
.describe("CSS selector to get content from (default: body)"),
126110
}),
127111
execute: async ({ selector = "body" }) => {
128-
const [tab] = await chrome.tabs.query({
129-
active: true,
130-
currentWindow: true,
131-
});
132-
133-
if (!tab?.id) {
134-
throw new Error("No active tab found");
135-
}
136-
137-
const results = await chrome.scripting.executeScript({
138-
target: { tabId: tab.id },
139-
func: (sel: string) => {
112+
const content = await executeScriptInActiveTab(
113+
(sel: string) => {
140114
const element = document.querySelector(sel);
141115
return element ? element.textContent : null;
142116
},
143-
args: [selector],
144-
});
117+
[selector],
118+
);
145119

146-
const content = results[0]?.result;
147120
if (!content) {
148121
throw new Error(`No content found for selector: ${selector}`);
149122
}
@@ -162,18 +135,8 @@ export const clickElementTool = tool({
162135
selector: z.string().describe("CSS selector of the element to click"),
163136
}),
164137
execute: async ({ selector }) => {
165-
const [tab] = await chrome.tabs.query({
166-
active: true,
167-
currentWindow: true,
168-
});
169-
170-
if (!tab?.id) {
171-
throw new Error("No active tab found");
172-
}
173-
174-
const results = await chrome.scripting.executeScript({
175-
target: { tabId: tab.id },
176-
func: (sel: string) => {
138+
const result = await executeScriptInActiveTab(
139+
(sel: string) => {
177140
const element = document.querySelector(sel);
178141
if (!element) {
179142
return { success: false, error: "Element not found" };
@@ -184,10 +147,9 @@ export const clickElementTool = tool({
184147
}
185148
return { success: false, error: "Element is not clickable" };
186149
},
187-
args: [selector],
188-
});
150+
[selector],
151+
);
189152

190-
const result = results[0]?.result;
191153
if (!result?.success) {
192154
throw new Error(result?.error ?? "Failed to click element");
193155
}
@@ -207,18 +169,8 @@ export const fillFormFieldTool = tool({
207169
value: z.string().describe("Value to fill in the field"),
208170
}),
209171
execute: async ({ selector, value }) => {
210-
const [tab] = await chrome.tabs.query({
211-
active: true,
212-
currentWindow: true,
213-
});
214-
215-
if (!tab?.id) {
216-
throw new Error("No active tab found");
217-
}
218-
219-
const results = await chrome.scripting.executeScript({
220-
target: { tabId: tab.id },
221-
func: (sel: string, val: string) => {
172+
const result = await executeScriptInActiveTab(
173+
(sel: string, val: string) => {
222174
const element = document.querySelector(sel);
223175
if (!element) {
224176
return { success: false, error: "Element not found" };
@@ -234,10 +186,9 @@ export const fillFormFieldTool = tool({
234186
}
235187
return { success: false, error: "Element is not an input field" };
236188
},
237-
args: [selector, value],
238-
});
189+
[selector, value],
190+
);
239191

240-
const result = results[0]?.result;
241192
if (!result?.success) {
242193
throw new Error(result?.error ?? "Failed to fill form field");
243194
}

0 commit comments

Comments
 (0)