add moveing of tabs and groups, multi browser support, auto complite into terminal, extract html and adding testing
This commit is contained in:
+107
-16
@@ -11,7 +11,12 @@ let port = null;
|
||||
|
||||
// ── Connection management ─────────────────────────────────────────────────────
|
||||
|
||||
function connect() {
|
||||
async function getProfileAlias() {
|
||||
const { profileAlias } = await chrome.storage.local.get("profileAlias");
|
||||
return profileAlias || "default";
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (port) return;
|
||||
try {
|
||||
port = chrome.runtime.connectNative(NATIVE_HOST);
|
||||
@@ -21,7 +26,10 @@ function connect() {
|
||||
const err = chrome.runtime.lastError;
|
||||
if (err) console.warn("[browser-cli] Native host disconnected:", err.message);
|
||||
});
|
||||
console.log("[browser-cli] Connected to native host");
|
||||
// Send hello so native host knows which profile/alias this is
|
||||
const alias = await getProfileAlias();
|
||||
port.postMessage({ type: "hello", alias });
|
||||
console.log("[browser-cli] Connected to native host as profile:", alias);
|
||||
} catch (e) {
|
||||
port = null;
|
||||
console.error("[browser-cli] Failed to connect:", e);
|
||||
@@ -45,6 +53,8 @@ async function onMessage(msg) {
|
||||
const { id, command, args } = msg;
|
||||
if (!id || !command) return;
|
||||
|
||||
console.log("[browser-cli] ←", command, args);
|
||||
|
||||
let data, error;
|
||||
try {
|
||||
data = await dispatch(command, args || {});
|
||||
@@ -53,8 +63,10 @@ async function onMessage(msg) {
|
||||
}
|
||||
|
||||
if (error !== undefined) {
|
||||
console.log("[browser-cli] → ERROR", command, error);
|
||||
port.postMessage({ id, success: false, error });
|
||||
} else {
|
||||
console.log("[browser-cli] →", command, data);
|
||||
port.postMessage({ id, success: true, data });
|
||||
}
|
||||
}
|
||||
@@ -90,6 +102,7 @@ async function dispatch(command, args) {
|
||||
case "group.close": return groupClose(args);
|
||||
case "group.open": return groupOpen(args);
|
||||
case "group.add_tab": return groupAddTab(args);
|
||||
case "group.move": return groupMove(args);
|
||||
|
||||
// ── Windows ───────────────────────────────────────────────────────────
|
||||
case "windows.list": return windowsList();
|
||||
@@ -110,6 +123,7 @@ async function dispatch(command, args) {
|
||||
case "extract.images": return domOp("extractImages", args);
|
||||
case "extract.text": return domOp("extractText", args);
|
||||
case "extract.json": return domOp("extractJson", args);
|
||||
case "extract.html": return tabsHtml({});
|
||||
|
||||
// ── Session ───────────────────────────────────────────────────────────
|
||||
case "session.save": return sessionSave(args);
|
||||
@@ -120,7 +134,8 @@ async function dispatch(command, args) {
|
||||
case "session.auto_save": return sessionAutoSave(args);
|
||||
|
||||
// ── Misc ──────────────────────────────────────────────────────────────
|
||||
case "clients.list": return clientsList();
|
||||
case "clients.list": return clientsList();
|
||||
case "clients.rename_profile": return clientsRenameProfile(args);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown command: ${command}`);
|
||||
@@ -170,12 +185,19 @@ async function navForward({ tabId }) {
|
||||
}
|
||||
|
||||
async function navFocus({ pattern }) {
|
||||
const all = await chrome.tabs.query({});
|
||||
const match = all.find(t => t.url && t.url.includes(pattern));
|
||||
// If pattern is a plain integer, treat it as a tab ID
|
||||
const asInt = parseInt(pattern);
|
||||
let match;
|
||||
if (!isNaN(asInt) && String(asInt) === String(pattern)) {
|
||||
match = await chrome.tabs.get(asInt);
|
||||
} else {
|
||||
const all = await chrome.tabs.query({});
|
||||
match = all.find(t => (t.url && t.url.includes(pattern)) || (t.pendingUrl && t.pendingUrl.includes(pattern)));
|
||||
}
|
||||
if (!match) return null;
|
||||
await chrome.windows.update(match.windowId, { focused: true });
|
||||
await chrome.tabs.update(match.id, { active: true });
|
||||
return { id: match.id, url: match.url, title: match.title };
|
||||
return { id: match.id, url: match.url || match.pendingUrl, title: match.title };
|
||||
}
|
||||
|
||||
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||
@@ -221,11 +243,20 @@ async function tabsClose({ tabId, inactive, duplicates }) {
|
||||
return { closed: toClose.length };
|
||||
}
|
||||
|
||||
async function tabsMove({ tabId, groupId, windowId, index }) {
|
||||
async function tabsMove({ tabId, groupId, windowId, index, forward, backward }) {
|
||||
const moveProps = {};
|
||||
if (windowId != null) moveProps.windowId = windowId;
|
||||
if (index != null) moveProps.index = index;
|
||||
else moveProps.index = -1;
|
||||
|
||||
if (forward || backward) {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
if (forward) moveProps.index = tab.index + 2; // +2 because Chrome shifts after removal
|
||||
else moveProps.index = Math.max(0, tab.index - 1);
|
||||
} else if (index != null) {
|
||||
moveProps.index = index;
|
||||
} else {
|
||||
moveProps.index = -1;
|
||||
}
|
||||
|
||||
await chrome.tabs.move(tabId, moveProps);
|
||||
if (groupId != null) {
|
||||
await chrome.tabs.group({ tabIds: [tabId], groupId });
|
||||
@@ -260,13 +291,41 @@ async function tabsQuery({ search }) {
|
||||
).map(tabInfo);
|
||||
}
|
||||
|
||||
async function executeScript(options, retries = 3) {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await chrome.scripting.executeScript(options);
|
||||
} catch (e) {
|
||||
if (i < retries - 1 && e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")) && !e.message.includes("error page")) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function tabsHtml({ tabId }) {
|
||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => document.documentElement.outerHTML,
|
||||
});
|
||||
return results[0]?.result || "";
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||
if (!isScriptableUrl(tab.url || tab.pendingUrl || "")) {
|
||||
throw new Error(`Cannot get HTML of ${tab.url || tab.pendingUrl} — navigate to a regular web page first`);
|
||||
}
|
||||
try {
|
||||
const results = await executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => document.documentElement.outerHTML,
|
||||
});
|
||||
return results[0]?.result || "";
|
||||
} catch (e) {
|
||||
const transient = e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")) && !e.message.includes("error page");
|
||||
if (i < 2 && transient) {
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function tabsDedupe() {
|
||||
@@ -371,6 +430,31 @@ async function groupAddTab({ group, url }) {
|
||||
return { tabId: tab.id, groupId };
|
||||
}
|
||||
|
||||
async function groupMove({ group, forward, backward }) {
|
||||
const groupId = await resolveGroupId(group);
|
||||
const groupInfo = await chrome.tabGroups.get(groupId);
|
||||
const allTabs = await chrome.tabs.query({ windowId: groupInfo.windowId });
|
||||
allTabs.sort((a, b) => a.index - b.index);
|
||||
|
||||
const groupTabs = allTabs.filter(t => t.groupId === groupId);
|
||||
if (!groupTabs.length) throw new Error(`No tabs found in group '${group}'`);
|
||||
|
||||
const firstIdx = groupTabs[0].index;
|
||||
const lastIdx = groupTabs[groupTabs.length - 1].index;
|
||||
|
||||
if (forward) {
|
||||
const tabAfter = allTabs.find(t => t.index > lastIdx && t.groupId !== groupId);
|
||||
if (!tabAfter) return { groupId, moved: false };
|
||||
await chrome.tabs.move(groupTabs.map(t => t.id), { index: tabAfter.index + 1 });
|
||||
} else if (backward) {
|
||||
const tabsBefore = allTabs.filter(t => t.index < firstIdx && t.groupId !== groupId);
|
||||
const tabBefore = tabsBefore[tabsBefore.length - 1];
|
||||
if (!tabBefore) return { groupId, moved: false };
|
||||
await chrome.tabs.move(groupTabs.map(t => t.id), { index: tabBefore.index });
|
||||
}
|
||||
return { groupId, moved: true };
|
||||
}
|
||||
|
||||
// ── Windows ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function windowsList() {
|
||||
@@ -420,7 +504,7 @@ async function domOp(funcName, args) {
|
||||
if (!isScriptableUrl(tab.url)) {
|
||||
throw new Error(`Cannot run DOM commands on ${tab.url} — navigate to a regular web page first`);
|
||||
}
|
||||
const results = await chrome.scripting.executeScript({
|
||||
const results = await executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: contentDispatch,
|
||||
args: [funcName, args],
|
||||
@@ -568,14 +652,21 @@ async function autoSaveHandler() {
|
||||
|
||||
async function clientsList() {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const alias = await getProfileAlias();
|
||||
return [{
|
||||
name: "Chrome",
|
||||
version: navigator.userAgent.match(/Chrome\/([\d.]+)/)?.[1] || "unknown",
|
||||
platform: navigator.platform,
|
||||
extensionVersion: manifest.version,
|
||||
profile: alias,
|
||||
}];
|
||||
}
|
||||
|
||||
async function clientsRenameProfile({ alias }) {
|
||||
await chrome.storage.local.set({ profileAlias: alias });
|
||||
return { alias };
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function getActiveTab() {
|
||||
|
||||
Reference in New Issue
Block a user