add moveing of tabs and groups, multi browser support, auto complite into terminal, extract html and adding testing

This commit is contained in:
2026-04-09 01:41:01 +02:00
parent 0cb2f1cb3f
commit ab4ba97886
19 changed files with 1069 additions and 57 deletions
+107 -16
View File
@@ -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() {