Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,473 changes: 7,473 additions & 0 deletions cli-manifest.json

Large diffs are not rendered by default.

2,737 changes: 1,870 additions & 867 deletions extension/dist/background.js

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,13 @@
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"homepage_url": "https://github.com/jackwener/opencli"
"homepage_url": "https://github.com/jackwener/opencli",
"commands": {
"get-page-state": {
"suggested_key": {
"default": "Alt+S"
},
"description": "Get page state"
}
}
}
32 changes: 32 additions & 0 deletions extension/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@
border-radius: 3px;
font-size: 11px;
}
.action-button {
margin-top: 14px;
width: 100%;
padding: 10px;
background: #007aff;
color: white;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.action-button:hover {
background: #0066cc;
}
.action-button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.result {
margin-top: 14px;
padding: 10px;
border-radius: 8px;
background: #f5f5f5;
font-size: 11px;
color: #333;
max-height: 200px;
overflow-y: auto;
display: none;
}
.footer {
margin-top: 14px;
text-align: center;
Expand All @@ -76,6 +106,8 @@ <h1>OpenCLI</h1>
<div class="hint" id="hint">
This is normal. The extension connects automatically when you run any <code>opencli</code> command.
</div>
<button class="action-button" id="stateButton" disabled>Get Page State</button>
<div class="result" id="result"></div>
<div class="footer">
<a href="https://github.com/jackwener/opencli" target="_blank">Documentation</a>
</div>
Expand Down
49 changes: 49 additions & 0 deletions extension/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,72 @@ chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
const dot = document.getElementById('dot');
const status = document.getElementById('status');
const hint = document.getElementById('hint');
const stateButton = document.getElementById('stateButton');

if (chrome.runtime.lastError || !resp) {
dot.className = 'dot disconnected';
status.innerHTML = '<strong>No daemon connected</strong>';
hint.style.display = 'block';
stateButton.disabled = true;
return;
}
if (resp.connected) {
dot.className = 'dot connected';
status.innerHTML = '<strong>Connected to daemon</strong>';
hint.style.display = 'none';
stateButton.disabled = false;
} else if (resp.reconnecting) {
dot.className = 'dot connecting';
status.innerHTML = '<strong>Reconnecting...</strong>';
hint.style.display = 'none';
stateButton.disabled = true;
} else {
dot.className = 'dot disconnected';
status.innerHTML = '<strong>No daemon connected</strong>';
hint.style.display = 'block';
stateButton.disabled = true;
}
});

// Add event listener for state button
document.getElementById('stateButton').addEventListener('click', async () => {
const resultDiv = document.getElementById('result');
const stateButton = document.getElementById('stateButton');

// Show loading state
stateButton.disabled = true;
stateButton.textContent = 'Loading...';
resultDiv.style.display = 'block';
resultDiv.textContent = 'Getting page state...';

try {
// Send message to background script to get page state
chrome.runtime.sendMessage({ type: 'getPageState' }, (response) => {
if (chrome.runtime.lastError) {
resultDiv.textContent = `Error: ${chrome.runtime.lastError.message}`;
stateButton.disabled = false;
stateButton.textContent = 'Get Page State';
return;
}

if (response && response.ok) {
// Format the result
if (typeof response.data === 'string') {
resultDiv.textContent = response.data;
} else {
resultDiv.textContent = JSON.stringify(response.data, null, 2);
}
} else {
resultDiv.textContent = `Error: ${response?.error || 'Failed to get page state'}`;
}

// Reset button state
stateButton.disabled = false;
stateButton.textContent = 'Get Page State';
});
} catch (error) {
resultDiv.textContent = `Error: ${error.message}`;
stateButton.disabled = false;
stateButton.textContent = 'Get Page State';
}
});
162 changes: 162 additions & 0 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Command, Result } from './protocol';
import { DAEMON_WS_URL, DAEMON_PING_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
import * as executor from './cdp';
import * as identity from './identity';
import { generateSnapshotJs } from '@src/browser/dom-snapshot.js';

let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
Expand Down Expand Up @@ -265,10 +266,171 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null,
});
} else if (msg?.type === 'getPageState') {
// Handle getPageState message
handleGetPageState().then(result => {
sendResponse(result);
}).catch(err => {
sendResponse({
ok: false,
error: err instanceof Error ? err.message : String(err),
});
});
return true; // Indicates we will send a response asynchronously
}
return false;
});

// ─── Keyboard Shortcut Listener ────────────────────────────────────

// Listen for keyboard shortcuts
chrome.commands.onCommand.addListener((command) => {
if (command === 'get-page-state') {
handleGetPageState().then(result => {
// Show notification if needed
if (result.ok) {
chrome.notifications.create('opencli-page-state', {
type: 'basic',
title: 'OpenCLI',
message: 'Page state captured successfully',
iconUrl: 'icons/icon-48.png'
});
} else {
chrome.notifications.create('opencli-page-state-error', {
type: 'basic',
title: 'OpenCLI Error',
message: `Failed to capture page state: ${result.error}`,
iconUrl: 'icons/icon-48.png'
});
}
});
}
});

// ─── Get Page State Handler ─────────────────────────────────────────

async function handleGetPageState(): Promise<Result> {
const workspace = 'browser:default';
try {
// 绑定到当前活跃标签页
const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
const allTabs = await chrome.tabs.query({});
const boundTab = activeTabs.find((tab) => tab.id && (tab.url?.startsWith('http://') || tab.url?.startsWith('https://')))
?? fallbackTabs.find((tab) => tab.id && (tab.url?.startsWith('http://') || tab.url?.startsWith('https://')))
?? allTabs.find((tab) => tab.id && (tab.url?.startsWith('http://') || tab.url?.startsWith('https://')));

if (!boundTab?.id) {
return {
id: 'popup-state',
ok: false,
error: 'No active debuggable tab found',
};
}

// 设置工作区会话
setWorkspaceSession(workspace, {
windowId: boundTab.windowId,
owned: false,
preferredTabId: boundTab.id,
});
resetWindowIdleTimer(workspace);

// 生成快照脚本
const snapshotJs = generateSnapshotJs({
viewportExpand: 2000,
maxDepth: 50,
interactiveOnly: false,
maxTextLength: 120,
includeScrollInfo: true,
bboxDedup: true,
includeShadowDom: true,
includeIframes: true,
maxIframes: 5,
paintOrderCheck: true,
annotateRefs: true,
reportHidden: true,
filterAds: true,
markdownTables: true,
previousHashes: null,
});

// 执行快照脚本
const aggressive = workspace.startsWith('browser:') || workspace.startsWith('operate:');
const data = await executor.evaluateAsync(boundTab.id, snapshotJs, aggressive);

// 在页面上标记元素序号
const markElementsScript = `
(() => {
'use strict';

// 移除之前的标记
document.querySelectorAll('.opencli-element-mark').forEach(el => el.remove());

// 遍历所有带有 data-opencli-ref 属性的元素
document.querySelectorAll('[data-opencli-ref]').forEach(el => {
try {
const rect = el.getBoundingClientRect();
const ref = el.getAttribute('data-opencli-ref');

if (rect.width > 0 && rect.height > 0 && ref) {
// 创建标记元素
const mark = document.createElement('div');
mark.className = 'opencli-element-mark';
mark.textContent = ref;
mark.style.position = 'absolute';
mark.style.left = '0';
mark.style.top = '0';
mark.style.transform = 'translate(-50%, -50%)';
mark.style.background = 'rgba(255, 0, 0, 0.8)';
mark.style.color = 'white';
mark.style.fontSize = '12px';
mark.style.fontWeight = 'bold';
mark.style.padding = '2px 6px';
mark.style.borderRadius = '10px';
mark.style.zIndex = '9999';
mark.style.pointerEvents = 'none';
mark.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.3)';

// 计算中心位置
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;

// 设置位置
mark.style.left = centerX + 'px';
mark.style.top = centerY + 'px';

// 添加到文档
document.body.appendChild(mark);
}
} catch (e) {
// 忽略错误
}
});
})()
`;

// 执行标记脚本
try {
await executor.evaluateAsync(boundTab.id, markElementsScript, aggressive);
} catch (err) {
// 忽略标记错误,不影响主功能
}

return {
id: 'popup-state',
ok: true,
data,
};
} catch (err) {
return {
id: 'popup-state',
ok: false,
error: err instanceof Error ? err.message : String(err),
};
}
}

// ─── Command dispatcher ─────────────────────────────────────────────

async function handleCommand(cmd: Command): Promise<Result> {
Expand Down
5 changes: 5 additions & 0 deletions extension/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ export default defineConfig({
target: 'esnext',
minify: false,
},
resolve: {
alias: {
'@src': resolve(__dirname, '../src'),
},
},
});
Loading