From 2f92a9cb322fbaab9314f44f267093e367608d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?d=20=F0=9F=94=B9?= <258577966+voidborne-d@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:07:22 +0000 Subject: [PATCH 1/7] fix: add execCommand fallback for contenteditable input (#168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When typing into contenteditable elements (e.g. LinkedIn post editor), the synthetic event approach (Plan A) may fail silently — the events fire but the editor's internal state doesn't update, leaving the element empty. This adds an automatic fallback: after Plan A, we verify the text was actually inserted by checking element.innerText. If it wasn't, we fall back to execCommand('insertText') which integrates natively with most rich-text editors including LinkedIn, Quill, and Slate.js. The fallback uses proper Selection/Range API to select-all before replacing, and preserves the undo stack since execCommand is handled by the browser natively. Fixes #168 --- packages/page-controller/src/actions.ts | 38 +++++++++++++++++++------ 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/page-controller/src/actions.ts b/packages/page-controller/src/actions.ts index 983192f..e496b2d 100644 --- a/packages/page-controller/src/actions.ts +++ b/packages/page-controller/src/actions.ts @@ -119,11 +119,16 @@ export async function inputTextElement(element: HTMLElement, text: string) { // - Monaco/CodeMirror: Require direct JS instance access. No universal way to obtain. // - Draft.js: Not responsive to synthetic/execCommand/Range/DataTransfer. Unmaintained. // + // Strategy: Try Plan A (synthetic events) first, then verify and fall back + // to Plan B (execCommand) if the text wasn't actually inserted. + // // Plan A: Dispatch synthetic events - // Works: LinkedIn, React contenteditable, Quill. - // Fails: Slate.js + // Works: React contenteditable, Quill, some LinkedIn versions. + // Fails: Slate.js, some contenteditable editors that ignore synthetic events. // Sequence: beforeinput -> mutation -> input -> change -> blur + let planASucceeded = false + // Dispatch beforeinput + mutation + input for clearing if ( element.dispatchEvent( @@ -164,18 +169,33 @@ export async function inputTextElement(element: HTMLElement, text: string) { ) } + // Verify Plan A worked by checking if the text was actually inserted + const currentText = element.innerText.trim() + planASucceeded = currentText === text.trim() + + if (!planASucceeded) { + // Plan B: execCommand fallback (deprecated but widely supported) + // Works: LinkedIn, Quill, Slate.js, react contenteditable components. + // This approach integrates with the browser's undo stack and is handled + // natively by most rich-text editors. + element.focus() + + // Select all existing content and delete it + const selection = window.getSelection() + const range = document.createRange() + range.selectNodeContents(element) + selection?.removeAllRanges() + selection?.addRange(range) + + document.execCommand('delete', false) + document.execCommand('insertText', false, text) + } + // Dispatch change event (for good measure) element.dispatchEvent(new Event('change', { bubbles: true })) // Trigger blur for validation element.blur() - - // Plan B: execCommand (deprecated but works better for some editors) - // Works: LinkedIn, Quill, Slate.js, react contenteditable components - // - // document.execCommand('selectAll') - // document.execCommand('delete') - // document.execCommand('insertText', false, text) } else if (element instanceof HTMLTextAreaElement) { nativeTextAreaValueSetter.call(element, text) } else { From 0bc47a997dd9ab19da89ff9b1e2eb5abaabcaa0f Mon Sep 17 00:00:00 2001 From: adonis <513554676@qq.com> Date: Thu, 19 Mar 2026 23:54:08 +0800 Subject: [PATCH 2/7] feat(ext): rerun tasks from history --- .../src/components/HistoryDetail.tsx | 27 ++++++++++- .../extension/src/components/HistoryList.tsx | 47 +++++++++++++++---- .../src/entrypoints/sidepanel/App.tsx | 33 +++++++++---- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/packages/extension/src/components/HistoryDetail.tsx b/packages/extension/src/components/HistoryDetail.tsx index 2f07966..9fe2d59 100644 --- a/packages/extension/src/components/HistoryDetail.tsx +++ b/packages/extension/src/components/HistoryDetail.tsx @@ -1,4 +1,4 @@ -import { ArrowLeft } from 'lucide-react' +import { ArrowLeft, RotateCcw } from 'lucide-react' import { useEffect, useState } from 'react' import { Button } from '@/components/ui/button' @@ -6,7 +6,17 @@ import { type SessionRecord, getSession } from '@/lib/db' import { EventCard } from './cards' -export function HistoryDetail({ sessionId, onBack }: { sessionId: string; onBack: () => void }) { +export function HistoryDetail({ + sessionId, + onBack, + onRerun, + rerunDisabled = false, +}: { + sessionId: string + onBack: () => void + onRerun: (task: string) => void + rerunDisabled?: boolean +}) { const [session, setSession] = useState(null) useEffect(() => { @@ -37,6 +47,19 @@ export function HistoryDetail({ sessionId, onBack }: { sessionId: string; onBack
{session.task}
+
+ +
{/* Events (read-only) */} diff --git a/packages/extension/src/components/HistoryList.tsx b/packages/extension/src/components/HistoryList.tsx index c13bae2..096ac0f 100644 --- a/packages/extension/src/components/HistoryList.tsx +++ b/packages/extension/src/components/HistoryList.tsx @@ -1,4 +1,4 @@ -import { ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react' +import { ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' import { Button } from '@/components/ui/button' @@ -18,9 +18,13 @@ function timeAgo(ts: number): string { export function HistoryList({ onSelect, onBack, + onRerun, + rerunDisabled = false, }: { onSelect: (id: string) => void onBack: () => void + onRerun: (task: string) => void + rerunDisabled?: boolean }) { const [sessions, setSessions] = useState([]) const [loading, setLoading] = useState(true) @@ -41,6 +45,11 @@ export function HistoryList({ setSessions((prev) => prev.filter((s) => s.id !== id)) } + const handleRerun = (e: React.MouseEvent, task: string) => { + e.stopPropagation() + onRerun(task) + } + return (
{/* Header */} @@ -85,7 +94,12 @@ export function HistoryList({ role="button" tabIndex={0} onClick={() => onSelect(session.id)} - onKeyDown={(e) => e.key === 'Enter' && onSelect(session.id)} + onKeyDown={(e) => { + if (e.target !== e.currentTarget) return + if (e.key !== 'Enter' && e.key !== ' ') return + e.preventDefault() + onSelect(session.id) + }} className="w-full text-left px-3 py-2.5 border-b hover:bg-muted/50 transition-colors cursor-pointer flex items-start gap-2 group" > {/* Status icon */} @@ -103,14 +117,27 @@ export function HistoryList({

- {/* Delete */} - +
+ + +
))} diff --git a/packages/extension/src/entrypoints/sidepanel/App.tsx b/packages/extension/src/entrypoints/sidepanel/App.tsx index db02f53..9835460 100644 --- a/packages/extension/src/entrypoints/sidepanel/App.tsx +++ b/packages/extension/src/entrypoints/sidepanel/App.tsx @@ -56,19 +56,27 @@ export default function App() { } }, [history, activity]) - const handleSubmit = useCallback( - (e?: React.SyntheticEvent) => { - e?.preventDefault() - if (!inputValue.trim() || status === 'running') return + const runTask = useCallback( + (task: string) => { + const normalizedTask = task.trim() + if (!normalizedTask || status === 'running') return - const taskToExecute = inputValue.trim() setInputValue('') + setView({ name: 'chat' }) - execute(taskToExecute).catch((error) => { + execute(normalizedTask).catch((error) => { console.error('[SidePanel] Failed to execute task:', error) }) }, - [inputValue, status, execute] + [execute, status] + ) + + const handleSubmit = useCallback( + (e?: React.SyntheticEvent) => { + e?.preventDefault() + runTask(inputValue) + }, + [inputValue, runTask] ) const handleStop = useCallback(() => { @@ -103,12 +111,21 @@ export default function App() { setView({ name: 'history-detail', sessionId: id })} onBack={() => setView({ name: 'chat' })} + onRerun={runTask} + rerunDisabled={status === 'running'} /> ) } if (view.name === 'history-detail') { - return setView({ name: 'history' })} /> + return ( + setView({ name: 'history' })} + onRerun={runTask} + rerunDisabled={status === 'running'} + /> + ) } // --- Chat view --- From 2e18bd862d74702c448dbbeb662358c5e03073d0 Mon Sep 17 00:00:00 2001 From: voidborne-d Date: Fri, 20 Mar 2026 07:08:45 +0000 Subject: [PATCH 3/7] refactor: use const for planASucceeded, clarify LinkedIn comment --- packages/page-controller/src/actions.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/page-controller/src/actions.ts b/packages/page-controller/src/actions.ts index e496b2d..3a7c4fd 100644 --- a/packages/page-controller/src/actions.ts +++ b/packages/page-controller/src/actions.ts @@ -123,12 +123,10 @@ export async function inputTextElement(element: HTMLElement, text: string) { // to Plan B (execCommand) if the text wasn't actually inserted. // // Plan A: Dispatch synthetic events - // Works: React contenteditable, Quill, some LinkedIn versions. + // Works: React contenteditable, Quill, LinkedIn. // Fails: Slate.js, some contenteditable editors that ignore synthetic events. // Sequence: beforeinput -> mutation -> input -> change -> blur - let planASucceeded = false - // Dispatch beforeinput + mutation + input for clearing if ( element.dispatchEvent( @@ -170,8 +168,7 @@ export async function inputTextElement(element: HTMLElement, text: string) { } // Verify Plan A worked by checking if the text was actually inserted - const currentText = element.innerText.trim() - planASucceeded = currentText === text.trim() + const planASucceeded = element.innerText.trim() === text.trim() if (!planASucceeded) { // Plan B: execCommand fallback (deprecated but widely supported) From c89042f1425e1c2b237d5f68ba9c4d1433d65ff1 Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:36:10 +0800 Subject: [PATCH 4/7] chore: wording --- packages/page-controller/src/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/page-controller/src/actions.ts b/packages/page-controller/src/actions.ts index 3a7c4fd..3c948be 100644 --- a/packages/page-controller/src/actions.ts +++ b/packages/page-controller/src/actions.ts @@ -123,7 +123,7 @@ export async function inputTextElement(element: HTMLElement, text: string) { // to Plan B (execCommand) if the text wasn't actually inserted. // // Plan A: Dispatch synthetic events - // Works: React contenteditable, Quill, LinkedIn. + // Works: React contenteditable, Quill. // Fails: Slate.js, some contenteditable editors that ignore synthetic events. // Sequence: beforeinput -> mutation -> input -> change -> blur @@ -172,7 +172,7 @@ export async function inputTextElement(element: HTMLElement, text: string) { if (!planASucceeded) { // Plan B: execCommand fallback (deprecated but widely supported) - // Works: LinkedIn, Quill, Slate.js, react contenteditable components. + // Works: Quill, Slate.js, react contenteditable components. // This approach integrates with the browser's undo stack and is handled // natively by most rich-text editors. element.focus() From 05d16313c7d55fa6dcc72d1fe40fe1d8a02f6a8f Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:54:36 +0800 Subject: [PATCH 5/7] fix(PageController): add `mouseleave` event --- packages/page-controller/src/actions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/page-controller/src/actions.ts b/packages/page-controller/src/actions.ts index 53b3c6c..23a3c2a 100644 --- a/packages/page-controller/src/actions.ts +++ b/packages/page-controller/src/actions.ts @@ -54,6 +54,9 @@ function blurLastClickedElement() { lastClickedElement.dispatchEvent( new MouseEvent('mouseout', { bubbles: true, cancelable: true }) ) + lastClickedElement.dispatchEvent( + new MouseEvent('mouseleave', { bubbles: false, cancelable: true }) + ) lastClickedElement = null } } From 3459836a14d30f518828d89a9e4a718d2fde0fc9 Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:10:43 +0800 Subject: [PATCH 6/7] fix(PageController): lint error --- packages/page-controller/src/actions.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/page-controller/src/actions.ts b/packages/page-controller/src/actions.ts index 7269127..7a6cca6 100644 --- a/packages/page-controller/src/actions.ts +++ b/packages/page-controller/src/actions.ts @@ -187,7 +187,9 @@ export async function inputTextElement(element: HTMLElement, text: string) { selection?.removeAllRanges() selection?.addRange(range) + // eslint-disable-next-line @typescript-eslint/no-deprecated document.execCommand('delete', false) + // eslint-disable-next-line @typescript-eslint/no-deprecated document.execCommand('insertText', false, text) } From c35f367b39185990d4948cde6f542e09231e5d63 Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:36:23 +0800 Subject: [PATCH 7/7] feat(ext): style adjust; rm `rerunDisabled` --- .../src/components/HistoryDetail.tsx | 28 ++++++---- .../extension/src/components/HistoryList.tsx | 52 +++++++++---------- .../src/entrypoints/sidepanel/App.tsx | 2 - 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/packages/extension/src/components/HistoryDetail.tsx b/packages/extension/src/components/HistoryDetail.tsx index 9fe2d59..eff8ff9 100644 --- a/packages/extension/src/components/HistoryDetail.tsx +++ b/packages/extension/src/components/HistoryDetail.tsx @@ -1,8 +1,8 @@ -import { ArrowLeft, RotateCcw } from 'lucide-react' +import { ArrowLeft, RotateCcw, Trash2 } from 'lucide-react' import { useEffect, useState } from 'react' import { Button } from '@/components/ui/button' -import { type SessionRecord, getSession } from '@/lib/db' +import { type SessionRecord, deleteSession, getSession } from '@/lib/db' import { EventCard } from './cards' @@ -10,12 +10,10 @@ export function HistoryDetail({ sessionId, onBack, onRerun, - rerunDisabled = false, }: { sessionId: string onBack: () => void onRerun: (task: string) => void - rerunDisabled?: boolean }) { const [session, setSession] = useState(null) @@ -47,18 +45,26 @@ export function HistoryDetail({
{session.task}
-
- + +
diff --git a/packages/extension/src/components/HistoryList.tsx b/packages/extension/src/components/HistoryList.tsx index 096ac0f..176633b 100644 --- a/packages/extension/src/components/HistoryList.tsx +++ b/packages/extension/src/components/HistoryList.tsx @@ -19,12 +19,10 @@ export function HistoryList({ onSelect, onBack, onRerun, - rerunDisabled = false, }: { onSelect: (id: string) => void onBack: () => void onRerun: (task: string) => void - rerunDisabled?: boolean }) { const [sessions, setSessions] = useState([]) const [loading, setLoading] = useState(true) @@ -112,31 +110,31 @@ export function HistoryList({ {/* Content */}

{session.task}

-

- {timeAgo(session.createdAt)} · {session.history.length} steps -

-
- -
- - +
+

+ {timeAgo(session.createdAt)} · {session.history.length} steps +

+
+ + +
+
))} diff --git a/packages/extension/src/entrypoints/sidepanel/App.tsx b/packages/extension/src/entrypoints/sidepanel/App.tsx index 9835460..229c630 100644 --- a/packages/extension/src/entrypoints/sidepanel/App.tsx +++ b/packages/extension/src/entrypoints/sidepanel/App.tsx @@ -112,7 +112,6 @@ export default function App() { onSelect={(id) => setView({ name: 'history-detail', sessionId: id })} onBack={() => setView({ name: 'chat' })} onRerun={runTask} - rerunDisabled={status === 'running'} /> ) } @@ -123,7 +122,6 @@ export default function App() { sessionId={view.sessionId} onBack={() => setView({ name: 'history' })} onRerun={runTask} - rerunDisabled={status === 'running'} /> ) }