Skip to content
Merged
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
240 changes: 130 additions & 110 deletions src/background/MessageRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { initPromise } from './index';

// Track tabs where content script has been injected
const injectedTabs = new Set<number>();
let elementSelectionInFlight = false;

/**
* Clear injection tracking for a specific tab.
Expand Down Expand Up @@ -843,139 +844,158 @@ async function handleContentMessage(

switch (message.type) {
case 'ELEMENT_SELECTED': {
// Get pending issue info from session storage
const result = await chrome.storage.session.get('pendingIssue');
const pendingIssue = result.pendingIssue as {
userPrompt: string;
issueType: 'enhancement' | 'fix';
} | undefined;

if (!pendingIssue) {
await sessionStateMachine.finishElementSelection();
if (elementSelectionInFlight) {
return false;
}

// Auto-generate name from prompt or use default
const autoName = pendingIssue.userPrompt
? pendingIssue.userPrompt.slice(0, 40).replace(/[^a-zA-Z0-9\s-]/g, '').trim() ||
(pendingIssue.issueType === 'enhancement' ? 'Enhancement' : 'Bug fix')
: pendingIssue.issueType === 'enhancement' ? 'Enhancement' : 'Bug fix';

// Create the issue with elements array
const issue: Issue = {
id: `issue_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: pendingIssue.issueType,
timestamp: Date.now(),
name: autoName,
userPrompt: pendingIssue.userPrompt,
elements: message.elements,
pageUrl: message.pageUrl,
};

await storageManager.addIssue(session.sessionId, issue);

// Clear pending issue
await chrome.storage.session.remove('pendingIssue');
elementSelectionInFlight = true;
let guardReleased = false;

// Return to monitoring state
await sessionStateMachine.finishElementSelection();

// Check if we should auto-copy to clipboard
try {
const { autoCopyOnLog } = await chrome.storage.local.get('autoCopyOnLog');
// Default to true if not set
if (autoCopyOnLog !== false) {
await chrome.storage.session.set({ autoCopyIssueId: issue.id });
// Get pending issue info from session storage
const result = await chrome.storage.session.get('pendingIssue');
const pendingIssue = result.pendingIssue as {
userPrompt: string;
issueType: 'enhancement' | 'fix';
} | undefined;

if (!pendingIssue) {
const currentSession = sessionStateMachine.getSession();
if (currentSession?.state === 'selecting_element') {
await sessionStateMachine.finishElementSelection();
}
return false;
}
} catch {
// Failed to check auto-copy setting
}

// Check if we should auto-send BEFORE opening popup
// Only use the active connection - no fallback
let shouldAutoSend = false;
let autoSendConnection: Connection | undefined;
let autoSendType: 'opencode' | 'vscode' | undefined;
// Auto-generate name from prompt or use default
const autoName = pendingIssue.userPrompt
? pendingIssue.userPrompt.slice(0, 40).replace(/[^a-zA-Z0-9\s-]/g, '').trim() ||
(pendingIssue.issueType === 'enhancement' ? 'Enhancement' : 'Bug fix')
: pendingIssue.issueType === 'enhancement' ? 'Enhancement' : 'Bug fix';

// Create the issue with elements array
const issue: Issue = {
id: `issue_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: pendingIssue.issueType,
timestamp: Date.now(),
name: autoName,
userPrompt: pendingIssue.userPrompt,
elements: message.elements,
pageUrl: message.pageUrl,
};

try {
const connections = await storageManager.getConnections();
await storageManager.addIssue(session.sessionId, issue);

// Find the active connection
autoSendConnection = connections.find(
(c) => c.isActive && c.enabled && c.autoSend !== false
);
// Clear pending issue
await chrome.storage.session.remove('pendingIssue');

if (autoSendConnection) {
if (autoSendConnection.type === 'opencode' && autoSendConnection.selectedSessionId) {
// Check if session is idle before sending
const status = await openCodeClient.getSessionStatus(
autoSendConnection.endpoint,
autoSendConnection.selectedSessionId
);
shouldAutoSend = status.type === 'idle';
autoSendType = 'opencode';
// Return to monitoring state
await sessionStateMachine.finishElementSelection();

elementSelectionInFlight = false;
guardReleased = true;

} else if (autoSendConnection.type === 'vscode' && autoSendConnection.selectedInstanceId) {
// VSCode doesn't have a busy/idle status, so we just send directly
shouldAutoSend = true;
autoSendType = 'vscode';
// Check if we should auto-copy to clipboard
try {
const { autoCopyOnLog } = await chrome.storage.local.get('autoCopyOnLog');
// Default to true if not set
if (autoCopyOnLog !== false) {
await chrome.storage.session.set({ autoCopyIssueId: issue.id });
}
} catch {
// Failed to check auto-copy setting
}
} catch {
// Failed to check auto-send eligibility
}

// Set auto-sending state BEFORE opening popup so it shows loading immediately
if (shouldAutoSend && autoSendType) {
await chrome.storage.session.set({
autoSendingIssueId: issue.id,
autoSendingConnectionType: autoSendType,
});
}

// Open the extension popup
try {
await chrome.action.openPopup();
} catch {
// Could not open popup
}
// Check if we should auto-send BEFORE opening popup
// Only use the active connection - no fallback
let shouldAutoSend = false;
let autoSendConnection: Connection | undefined;
let autoSendType: 'opencode' | 'vscode' | undefined;

// Now do the actual auto-send
if (shouldAutoSend && autoSendConnection) {
try {
const markdown = await markdownExporter.exportIssue(
session.sessionId,
issue.id
const connections = await storageManager.getConnections();

// Find the active connection
autoSendConnection = connections.find(
(c) => c.isActive && c.enabled && c.autoSend !== false
);

if (autoSendType === 'opencode' && autoSendConnection.selectedSessionId && autoSendConnection.selectedSessionDirectory) {
await openCodeClient.sendMessage(
autoSendConnection.endpoint,
autoSendConnection.selectedSessionId,
markdown,
autoSendConnection.selectedSessionDirectory
);
} else if (autoSendType === 'vscode' && autoSendConnection.selectedInstanceId && autoSendConnection.selectedInstancePort) {
await vsCodeClient.sendMessage(
autoSendConnection.selectedInstanceId,
autoSendConnection.selectedInstancePort,
markdown
if (autoSendConnection) {
if (autoSendConnection.type === 'opencode' && autoSendConnection.selectedSessionId) {
// Check if session is idle before sending
const status = await openCodeClient.getSessionStatus(
autoSendConnection.endpoint,
autoSendConnection.selectedSessionId
);
shouldAutoSend = status.type === 'idle';
autoSendType = 'opencode';

} else if (autoSendConnection.type === 'vscode' && autoSendConnection.selectedInstanceId) {
// VSCode doesn't have a busy/idle status, so we just send directly
shouldAutoSend = true;
autoSendType = 'vscode';
}
}
} catch {
// Failed to check auto-send eligibility
}

// Set auto-sending state BEFORE opening popup so it shows loading immediately
if (shouldAutoSend && autoSendType) {
await chrome.storage.session.set({
autoSendingIssueId: issue.id,
autoSendingConnectionType: autoSendType,
});
}

// Open the extension popup
try {
await chrome.action.openPopup();
} catch {
// Could not open popup
}

// Now do the actual auto-send
if (shouldAutoSend && autoSendConnection) {
try {
const markdown = await markdownExporter.exportIssue(
session.sessionId,
issue.id
);

if (autoSendType === 'opencode' && autoSendConnection.selectedSessionId && autoSendConnection.selectedSessionDirectory) {
await openCodeClient.sendMessage(
autoSendConnection.endpoint,
autoSendConnection.selectedSessionId,
markdown,
autoSendConnection.selectedSessionDirectory
);
} else if (autoSendType === 'vscode' && autoSendConnection.selectedInstanceId && autoSendConnection.selectedInstancePort) {
await vsCodeClient.sendMessage(
autoSendConnection.selectedInstanceId,
autoSendConnection.selectedInstancePort,
markdown
);
}

// Mark as exported
await storageManager.markIssueExported(issue.id);
// Clear auto-sending state on success
await chrome.storage.session.remove(['autoSendingIssueId', 'autoSendingConnectionType']);
} catch (error) {
console.warn('[MessageRouter] Auto-send failed:', error);
// Set error flag and clear sending state
await chrome.storage.session.set({ autoSendError: true });
await chrome.storage.session.remove(['autoSendingIssueId', 'autoSendingConnectionType']);
}
}

// Mark as exported
await storageManager.markIssueExported(issue.id);
// Clear auto-sending state on success
await chrome.storage.session.remove(['autoSendingIssueId', 'autoSendingConnectionType']);
} catch (error) {
console.warn('[MessageRouter] Auto-send failed:', error);
// Set error flag and clear sending state
await chrome.storage.session.set({ autoSendError: true });
await chrome.storage.session.remove(['autoSendingIssueId', 'autoSendingConnectionType']);
return true;
} finally {
if (!guardReleased) {
elementSelectionInFlight = false;
}
}

return true;
}

case 'ELEMENT_PICKER_CANCELLED': {
Expand Down
8 changes: 6 additions & 2 deletions src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DOM_CAPTURE_CONFIG } from '@/shared/constants';

// Timeout for React source extraction (ms)
const REACT_SOURCE_TIMEOUT = 500;
const CONTENT_SCRIPT_INIT_FLAG = '__CLANKERCONTEXT_CONTENT_SCRIPT_INITIALIZED__';

type ClickPoint = { x: number; y: number };
type ReactSourceRequest = {
Expand Down Expand Up @@ -789,5 +790,8 @@ function handlePickerKeyDown(event: KeyboardEvent): void {
}
}

// Initialize on load
init();
// Initialize on load (guard against duplicate injections)
if (!(window as any)[CONTENT_SCRIPT_INIT_FLAG]) {
(window as any)[CONTENT_SCRIPT_INIT_FLAG] = true;
init();
}