Skip to content

Commit db85bb7

Browse files
committed
fix: open Claude side panel by clicking extension icon
Instead of navigating to the sidepanel URL (which opens it as a broken full tab), click on the pinned extension icon at coordinates (1775, 55) to open the proper side panel. - Add OpenSidePanel() function that uses Computer.ClickMouse API - Add ExtensionIconX/Y constants for the click coordinates - Update send, status, and chat commands to call OpenSidePanel first - Update Playwright scripts to wait for the side panel to appear instead of trying to navigate to it
1 parent 4bad056 commit db85bb7

File tree

8 files changed

+83
-30
lines changed

8 files changed

+83
-30
lines changed

cmd/claude/chat.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ func runChatWithBrowser(ctx context.Context, client kernel.Client, browserID str
6161
return fmt.Errorf("failed to get browser: %w", err)
6262
}
6363

64+
// Open the side panel by clicking the extension icon
65+
if err := claude.OpenSidePanel(ctx, client, browser.SessionID); err != nil {
66+
return fmt.Errorf("failed to open side panel: %w", err)
67+
}
68+
6469
// Check Claude status first
6570
pterm.Info.Println("Checking Claude extension status...")
6671
statusResult, err := client.Browsers.Playwright.Execute(ctx, browser.SessionID, kernel.BrowserPlaywrightExecuteParams{

cmd/claude/send.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ func runSend(cmd *cobra.Command, args []string) error {
107107
return fmt.Errorf("failed to get browser: %w", err)
108108
}
109109

110+
// Open the side panel by clicking the extension icon
111+
if err := claude.OpenSidePanel(ctx, client, browser.SessionID); err != nil {
112+
return fmt.Errorf("failed to open side panel: %w", err)
113+
}
114+
110115
// Build the script with environment variables
111116
script := fmt.Sprintf(`
112117
process.env.CLAUDE_MESSAGE = %s;

cmd/claude/status.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ func runStatus(cmd *cobra.Command, args []string) error {
5555
return fmt.Errorf("failed to get browser: %w", err)
5656
}
5757

58+
// Open the side panel by clicking the extension icon
59+
if err := claude.OpenSidePanel(ctx, client, browser.SessionID); err != nil {
60+
return fmt.Errorf("failed to open side panel: %w", err)
61+
}
62+
5863
// Execute the status check script
5964
if outputFormat != "json" {
6065
pterm.Info.Println("Checking Claude extension status...")

internal/claude/constants.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,12 @@ const (
3232

3333
// BundleAuthStorageDir is the directory name for auth storage within the bundle
3434
BundleAuthStorageDir = "auth-storage"
35+
36+
// ExtensionIconX is the X coordinate for clicking the pinned Claude extension icon
37+
// to open the side panel (for 1920x1080 screen resolution)
38+
ExtensionIconX = 1775
39+
40+
// ExtensionIconY is the Y coordinate for clicking the pinned Claude extension icon
41+
// to open the side panel (for 1920x1080 screen resolution)
42+
ExtensionIconY = 55
3543
)

internal/claude/loader.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ func pinExtension(ctx context.Context, client kernel.Client, browserID, extensio
228228
return nil
229229
}
230230

231+
// OpenSidePanel clicks on the pinned Claude extension icon to open the side panel.
232+
// This uses the computer API to click at the known coordinates of the extension icon.
233+
func OpenSidePanel(ctx context.Context, client kernel.Client, browserID string) error {
234+
return client.Browsers.Computer.ClickMouse(ctx, browserID, kernel.BrowserComputerClickMouseParams{
235+
X: ExtensionIconX,
236+
Y: ExtensionIconY,
237+
})
238+
}
239+
231240
// createTempZip creates a temporary zip file from a directory.
232241
func createTempZip(srcDir string) (string, error) {
233242
tmpFile, err := os.CreateTemp("", "claude-*.zip")

internal/claude/scripts/check_status.js

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Check the status of the Claude extension.
22
// This script is executed via Kernel's Playwright API.
3+
// The side panel should already be open (via OpenSidePanel click).
34
//
45
// Output:
56
// - Returns JSON with extension status information
67

78
const EXTENSION_ID = 'fcoeoabgfenejglbffodgkkbkcdhcgfn';
8-
const SIDEPANEL_URL = `chrome-extension://${EXTENSION_ID}/sidepanel.html?mode=window`;
99

1010
const status = {
1111
extensionLoaded: false,
@@ -15,20 +15,26 @@ const status = {
1515
};
1616

1717
try {
18-
// Try to open the sidepanel
19-
const sidepanel = await context.newPage();
20-
21-
try {
22-
await sidepanel.goto(SIDEPANEL_URL, { timeout: 10000 });
23-
await sidepanel.waitForLoadState('networkidle', { timeout: 10000 });
24-
status.extensionLoaded = true;
25-
} catch (e) {
26-
status.error = 'Extension not loaded or not accessible';
18+
// Wait for the side panel to appear (it was opened by clicking the extension icon)
19+
let sidepanel = null;
20+
const maxWaitMs = 10000;
21+
const startWait = Date.now();
22+
while (Date.now() - startWait < maxWaitMs) {
23+
sidepanel = context.pages().find(p => p.url().includes('sidepanel.html'));
24+
if (sidepanel) break;
25+
await new Promise(r => setTimeout(r, 500));
26+
}
27+
28+
if (!sidepanel) {
29+
status.error = 'Side panel not found. Extension may not be loaded or pinned.';
2730
return status;
2831
}
2932

30-
// Wait a bit for the UI to initialize
31-
await sidepanel.waitForTimeout(2000);
33+
status.extensionLoaded = true;
34+
35+
// Wait for the UI to initialize
36+
await sidepanel.waitForLoadState('networkidle');
37+
await sidepanel.waitForTimeout(1000);
3238

3339
// Check for authentication indicators
3440
// Look for chat input (indicates authenticated) - Claude uses ProseMirror contenteditable
@@ -54,9 +60,6 @@ try {
5460
const responses = await sidepanel.$$('div.claude-response');
5561
status.hasConversation = responses.length > 0;
5662

57-
// Close the test page
58-
await sidepanel.close();
59-
6063
} catch (e) {
6164
status.error = e.message;
6265
}

internal/claude/scripts/send_message.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Send a message to Claude and wait for the response.
22
// This script is executed via Kernel's Playwright API.
3+
// The side panel should already be open (via OpenSidePanel click).
34
//
45
// Input (via environment variables set before this script):
56
// - process.env.CLAUDE_MESSAGE: The message to send
@@ -9,7 +10,6 @@
910
// - Returns JSON with { response: string, model?: string }
1011

1112
const EXTENSION_ID = 'fcoeoabgfenejglbffodgkkbkcdhcgfn';
12-
const SIDEPANEL_URL = `chrome-extension://${EXTENSION_ID}/sidepanel.html?mode=window`;
1313

1414
const message = process.env.CLAUDE_MESSAGE;
1515
const timeoutMs = parseInt(process.env.CLAUDE_TIMEOUT_MS || '120000', 10);
@@ -18,16 +18,24 @@ if (!message) {
1818
throw new Error('CLAUDE_MESSAGE environment variable is required');
1919
}
2020

21-
// Find or open the sidepanel page
22-
let sidepanel = context.pages().find(p => p.url().includes('sidepanel.html'));
21+
// Wait for the side panel to appear (it was opened by clicking the extension icon)
22+
let sidepanel = null;
23+
const maxWaitMs = 10000;
24+
const startWait = Date.now();
25+
while (Date.now() - startWait < maxWaitMs) {
26+
sidepanel = context.pages().find(p => p.url().includes('sidepanel.html'));
27+
if (sidepanel) break;
28+
await new Promise(r => setTimeout(r, 500));
29+
}
30+
2331
if (!sidepanel) {
24-
sidepanel = await context.newPage();
25-
await sidepanel.goto(SIDEPANEL_URL);
26-
await sidepanel.waitForLoadState('networkidle');
27-
// Wait for the UI to fully initialize
28-
await sidepanel.waitForTimeout(2000);
32+
throw new Error('Side panel not found. Make sure the extension is loaded and pinned.');
2933
}
3034

35+
// Wait for the UI to fully initialize
36+
await sidepanel.waitForLoadState('networkidle');
37+
await sidepanel.waitForTimeout(1000);
38+
3139
// Check if we're authenticated by looking for the chat input
3240
// Claude uses a contenteditable div with ProseMirror
3341
const inputSelector = '[contenteditable="true"].ProseMirror, textarea';

internal/claude/scripts/stream_chat.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Interactive streaming chat with Claude.
22
// This script is executed via Kernel's Playwright API.
3+
// The side panel should already be open (via OpenSidePanel click).
34
//
45
// Communication protocol:
56
// - Reads JSON commands from stdin: { "type": "message", "content": "..." }
@@ -10,7 +11,6 @@
1011
// - { "type": "error", "message": "..." } - Error occurred
1112

1213
const EXTENSION_ID = 'fcoeoabgfenejglbffodgkkbkcdhcgfn';
13-
const SIDEPANEL_URL = `chrome-extension://${EXTENSION_ID}/sidepanel.html?mode=window`;
1414

1515
function emit(event) {
1616
console.log(JSON.stringify(event));
@@ -77,15 +77,25 @@ async function sendMessage(page, message) {
7777
emit({ type: 'error', message: 'Response timeout' });
7878
}
7979

80-
// Open the sidepanel
81-
let sidepanel = context.pages().find(p => p.url().includes('sidepanel.html'));
80+
// Wait for the side panel to appear (it was opened by clicking the extension icon)
81+
let sidepanel = null;
82+
const maxWaitMs = 10000;
83+
const startWait = Date.now();
84+
while (Date.now() - startWait < maxWaitMs) {
85+
sidepanel = context.pages().find(p => p.url().includes('sidepanel.html'));
86+
if (sidepanel) break;
87+
await new Promise(r => setTimeout(r, 500));
88+
}
89+
8290
if (!sidepanel) {
83-
sidepanel = await context.newPage();
84-
await sidepanel.goto(SIDEPANEL_URL);
85-
await sidepanel.waitForLoadState('networkidle');
86-
await sidepanel.waitForTimeout(2000);
91+
emit({ type: 'error', message: 'Side panel not found. Extension may not be loaded or pinned.' });
92+
return { error: 'side panel not found' };
8793
}
8894

95+
// Wait for the UI to initialize
96+
await sidepanel.waitForLoadState('networkidle');
97+
await sidepanel.waitForTimeout(1000);
98+
8999
// Check if authenticated
90100
const inputSelector = '[contenteditable="true"].ProseMirror, textarea';
91101
const input = await sidepanel.waitForSelector(inputSelector, {

0 commit comments

Comments
 (0)