fix(app): make the window-level clipboard fallback the sole ⌘V path - #130

Merged
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch
Aug 7, 2026
Merged

fix(app): make the window-level clipboard fallback the sole ⌘V path#130
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

Important

Problem

Pasting into the chat composer inside the Amicode webview inserted the same text twice. The marker route (sibling PR from fix/paste-duplication-amicode-261) suppresses only the window-level fallback; the composer's own mod+V interceptor remains a second insert path, and the remote retest reproduced the double paste.

Approach

Make the window-level clipboard fallback the sole mod+V path: remove the composers' own mod+V keydown interceptors and the now-dead markers. Key reason: a single path cannot double-insert, and no suppression mechanism has to stay correct across two listeners.

Approaches Considered

  • Remove the composer's own handler (chosen) — one insert path remains; nothing left to disagree about. Accepted cost: plain Ctrl+V pastes text only.
  • Add stopPropagation to the global fallback — superficially tempting and actively harmful: the composer's handler is the only path that tries the image bridge, and suppressing it would silently kill screenshot paste while appearing to fix the bug.
  • Mark the composer with the self-owner attribute (marker route, sibling PR) — suppresses only the fallback; the composer's interceptor stays live; remote retest reproduced the double paste.

Scope

  • In: removal of the v1 composer's mod+V keydown block (prompt-input.tsx), the v2 composer's handleFramedPaste (interaction.ts, attachments.ts), the data-amc-clipboard="self" markers on both editors, and the structural test rewritten to assert a single mod+V path.
  • Out: the fallback's logic, unchanged. Copy and cut, which deliberately stay on the global path. The native paste path (Ctrl+Shift+V, context menu), which still attaches images. The bridge protocol and message kinds.

Assumptions / Open Qs

  • Whether plain-Ctrl+V being text-only is an acceptable long-term trade is a pending design decision; this PR is the stopgap that makes the webview single-paste while that is decided.

What was failing

Two mod+V handlers are live inside the framed app, and both insert.

The window-level fallback runs in the capture phase and serves every editable that has no bridged paste of its own — necessary because native paste never fires inside the webview iframe.

The composer's own handler intercepts mod+V, tries the image bridge first (screenshots), then falls back to text, and inserts through the composer's structured model.

The fallback calls preventDefault but deliberately not stopPropagation. That is correct for its purpose, but it means the composer's interceptor still receives the keystroke afterwards — and both handlers bridge the clipboard into the document. Two paths, two inserts. Marking the composer only exempted the first path; the second remained, which is why the marker route alone did not hold up under remote retest.

What changed

  • packages/app/src/components/prompt-input.tsx — the v1 composer's mod+V keydown interception block removed; the marker and its comment removed; unused imports dropped.
  • packages/session-ui/src/v2/components/prompt-input/interaction.ts — the mod+V interception (amicode patch Fix chat scroll jitter during streaming; respect reduced motion #11 parity block) removed; onKeyDown no longer special-cases V.
  • packages/session-ui/src/v2/components/prompt-input/attachments.tshandleFramedPaste removed; the paste-time fallbacks (readClipboardImage/readClipboardText via the bridge) remain on the native handlePaste path.
  • packages/session-ui/src/v2/components/prompt-input/index.tsx — marker and comment removed.
  • packages/app/src/utils/global-clipboard.ts — header re-documented: this module is now the sole mod+V path in the webview; the marker contract covers only the profile fields' paste fallback.
  • packages/app/src/components/prompt-input-clipboard-structure.test.ts — rewritten to assert the single-path contract: no composer-level V interception, no markers in either composer, the fallback owns the mod+V branch, and the native paste wiring with bridge fallbacks is intact.

Verification

  • Structural test: 5 passing (single-path assertions listed above).
  • packages/app unit suite: 827 passing, 0 failing (was 828 — the superseded v1-marker assertion is replaced by the single-path rewrite).
  • bun typecheck: clean in packages/app and packages/session-ui.
  • Remote GUI retest (amicode:main + this branch): Ctrl+V pastes exactly once; Ctrl+Shift+V pastes once with images intact. The retested binary is the stamped build (.buildinfo: branch fix/paste-duplication-patch, commit 0ecbc8aab, dirty: false at test time), so the verified artefact is attributable.

Key Decisions

  • Removal over suppression. A suppression mechanism leaves two listeners that must stay in agreement; removal leaves one. This is the same class of bug as the sweep's other findings — a mechanism and its integration disagreeing — and the single-path design has no agreement left to maintain.
  • Text-only on plain Ctrl+V, accepted for the stopgap. The extension bridge reads text; images on plain Ctrl+V are sacrificed. Native paste (Ctrl+Shift+V, context menu) keeps the image path, so screenshots remain reachable while the design question is decided.
  • Markers deleted, not kept. With the composer interceptor gone there is no second listener to exempt; a marker asserting nothing would be inert code.

Constraints & Invariants

  • The fallback's behaviour for every other editable is untouched.
  • Copy and cut still mirror to the OS clipboard through the global path.
  • No change to the bridge protocol or to the set of message kinds the host accepts.
  • No change to the keybind matcher or the native paste handler.

Source

Closesharmoniqs/amicode#261 (stopgap; acceptance pending the design decision above).

Companion to the marker-route PR from fix/paste-duplication-amicode-261; the two are alternatives, not stacked changes. Found during a bug sweep of the amicode ↔ vendored-opencode seam; related seam work is harmoniqs/amicode#243.

Notes

Why this shipped instead of the marker route: the marker mechanism and its single integration disagreed, and the retest showed the mechanism suppressing only one of two insert paths. "One path" is not a compromise on correctness — it is the only arrangement where the double insert is structurally impossible. The cost is a UX decision (plain Ctrl+V text-only) that is explicitly parked for design review, not silently accepted here.

Amicode Sweepand others added 5 commits August 7, 2026 08:05
Paste inserted the same text twice inside the amicode webview.
Two mod+V handlers are live in the framed app: the window-level capture-phase
fallback in utils/global-clipboard.ts, and the composer's own image-first
handler in prompt-input.tsx. The fallback exempts elements matching
CLIPBOARD_SELF_SELECTOR, and it calls preventDefault() but deliberately not
stopPropagation() — so an unmarked composer receives both insertions.
The marker had been applied to the home-cards credential fields but never to
the composer, even though global-clipboard.ts's own header names "the prompt
input's Cmd+V handler" as the element the exemption exists for.
Marking the composer is the correct fix rather than adding stopPropagation():
the composer's handler is the only path that tries the image bridge, so
suppressing it would silently kill screenshot paste. The marker owns PASTE
only; Cmd+C / Cmd+X keep mirroring through the global path.
Adds a structural guard. The existing global-clipboard test asserts the
exemption against a synthetic element it builds itself, which is precisely why
this shipped — the mechanism was covered, its single real integration was not.
Closesharmoniqs/amicode#261
…leanup of inputs forwarded to controller that should not be
@gennadiryan
gennadiryan marked this pull request as ready for review August 7, 2026 22:17
@gennadiryan
gennadiryan merged commit e246c2b into local/amicodeAug 7, 2026
1 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: paste duplicates input — clipboard content inserted twice

1 participant

@gennadiryan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(app): make the window-level clipboard fallback the sole ⌘V path - #130

Merged
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch
Aug 7, 2026
Merged

fix(app): make the window-level clipboard fallback the sole ⌘V path#130
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

Important

Problem

Pasting into the chat composer inside the Amicode webview inserted the same text twice. The marker route (sibling PR from fix/paste-duplication-amicode-261) suppresses only the window-level fallback; the composer's own mod+V interceptor remains a second insert path, and the remote retest reproduced the double paste.

Approach

Make the window-level clipboard fallback the sole mod+V path: remove the composers' own mod+V keydown interceptors and the now-dead markers. Key reason: a single path cannot double-insert, and no suppression mechanism has to stay correct across two listeners.

Approaches Considered

  • Remove the composer's own handler (chosen) — one insert path remains; nothing left to disagree about. Accepted cost: plain Ctrl+V pastes text only.
  • Add stopPropagation to the global fallback — superficially tempting and actively harmful: the composer's handler is the only path that tries the image bridge, and suppressing it would silently kill screenshot paste while appearing to fix the bug.
  • Mark the composer with the self-owner attribute (marker route, sibling PR) — suppresses only the fallback; the composer's interceptor stays live; remote retest reproduced the double paste.

Scope

  • In: removal of the v1 composer's mod+V keydown block (prompt-input.tsx), the v2 composer's handleFramedPaste (interaction.ts, attachments.ts), the data-amc-clipboard="self" markers on both editors, and the structural test rewritten to assert a single mod+V path.
  • Out: the fallback's logic, unchanged. Copy and cut, which deliberately stay on the global path. The native paste path (Ctrl+Shift+V, context menu), which still attaches images. The bridge protocol and message kinds.

Assumptions / Open Qs

  • Whether plain-Ctrl+V being text-only is an acceptable long-term trade is a pending design decision; this PR is the stopgap that makes the webview single-paste while that is decided.

What was failing

Two mod+V handlers are live inside the framed app, and both insert.

The window-level fallback runs in the capture phase and serves every editable that has no bridged paste of its own — necessary because native paste never fires inside the webview iframe.

The composer's own handler intercepts mod+V, tries the image bridge first (screenshots), then falls back to text, and inserts through the composer's structured model.

The fallback calls preventDefault but deliberately not stopPropagation. That is correct for its purpose, but it means the composer's interceptor still receives the keystroke afterwards — and both handlers bridge the clipboard into the document. Two paths, two inserts. Marking the composer only exempted the first path; the second remained, which is why the marker route alone did not hold up under remote retest.

What changed

  • packages/app/src/components/prompt-input.tsx — the v1 composer's mod+V keydown interception block removed; the marker and its comment removed; unused imports dropped.
  • packages/session-ui/src/v2/components/prompt-input/interaction.ts — the mod+V interception (amicode patch Fix chat scroll jitter during streaming; respect reduced motion #11 parity block) removed; onKeyDown no longer special-cases V.
  • packages/session-ui/src/v2/components/prompt-input/attachments.tshandleFramedPaste removed; the paste-time fallbacks (readClipboardImage/readClipboardText via the bridge) remain on the native handlePaste path.
  • packages/session-ui/src/v2/components/prompt-input/index.tsx — marker and comment removed.
  • packages/app/src/utils/global-clipboard.ts — header re-documented: this module is now the sole mod+V path in the webview; the marker contract covers only the profile fields' paste fallback.
  • packages/app/src/components/prompt-input-clipboard-structure.test.ts — rewritten to assert the single-path contract: no composer-level V interception, no markers in either composer, the fallback owns the mod+V branch, and the native paste wiring with bridge fallbacks is intact.

Verification

  • Structural test: 5 passing (single-path assertions listed above).
  • packages/app unit suite: 827 passing, 0 failing (was 828 — the superseded v1-marker assertion is replaced by the single-path rewrite).
  • bun typecheck: clean in packages/app and packages/session-ui.
  • Remote GUI retest (amicode:main + this branch): Ctrl+V pastes exactly once; Ctrl+Shift+V pastes once with images intact. The retested binary is the stamped build (.buildinfo: branch fix/paste-duplication-patch, commit 0ecbc8aab, dirty: false at test time), so the verified artefact is attributable.

Key Decisions

  • Removal over suppression. A suppression mechanism leaves two listeners that must stay in agreement; removal leaves one. This is the same class of bug as the sweep's other findings — a mechanism and its integration disagreeing — and the single-path design has no agreement left to maintain.
  • Text-only on plain Ctrl+V, accepted for the stopgap. The extension bridge reads text; images on plain Ctrl+V are sacrificed. Native paste (Ctrl+Shift+V, context menu) keeps the image path, so screenshots remain reachable while the design question is decided.
  • Markers deleted, not kept. With the composer interceptor gone there is no second listener to exempt; a marker asserting nothing would be inert code.

Constraints & Invariants

  • The fallback's behaviour for every other editable is untouched.
  • Copy and cut still mirror to the OS clipboard through the global path.
  • No change to the bridge protocol or to the set of message kinds the host accepts.
  • No change to the keybind matcher or the native paste handler.

Source

Closesharmoniqs/amicode#261 (stopgap; acceptance pending the design decision above).

Companion to the marker-route PR from fix/paste-duplication-amicode-261; the two are alternatives, not stacked changes. Found during a bug sweep of the amicode ↔ vendored-opencode seam; related seam work is harmoniqs/amicode#243.

Notes

Why this shipped instead of the marker route: the marker mechanism and its single integration disagreed, and the retest showed the mechanism suppressing only one of two insert paths. "One path" is not a compromise on correctness — it is the only arrangement where the double insert is structurally impossible. The cost is a UX decision (plain Ctrl+V text-only) that is explicitly parked for design review, not silently accepted here.

Amicode Sweepand others added 5 commits August 7, 2026 08:05
Paste inserted the same text twice inside the amicode webview.
Two mod+V handlers are live in the framed app: the window-level capture-phase
fallback in utils/global-clipboard.ts, and the composer's own image-first
handler in prompt-input.tsx. The fallback exempts elements matching
CLIPBOARD_SELF_SELECTOR, and it calls preventDefault() but deliberately not
stopPropagation() — so an unmarked composer receives both insertions.
The marker had been applied to the home-cards credential fields but never to
the composer, even though global-clipboard.ts's own header names "the prompt
input's Cmd+V handler" as the element the exemption exists for.
Marking the composer is the correct fix rather than adding stopPropagation():
the composer's handler is the only path that tries the image bridge, so
suppressing it would silently kill screenshot paste. The marker owns PASTE
only; Cmd+C / Cmd+X keep mirroring through the global path.
Adds a structural guard. The existing global-clipboard test asserts the
exemption against a synthetic element it builds itself, which is precisely why
this shipped — the mechanism was covered, its single real integration was not.
Closesharmoniqs/amicode#261
…leanup of inputs forwarded to controller that should not be
@gennadiryan
gennadiryan marked this pull request as ready for review August 7, 2026 22:17
@gennadiryan
gennadiryan merged commit e246c2b into local/amicodeAug 7, 2026
1 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: paste duplicates input — clipboard content inserted twice

1 participant

@gennadiryan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app): make the window-level clipboard fallback the sole ⌘V path - #130

Merged
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch
Aug 7, 2026
Merged

fix(app): make the window-level clipboard fallback the sole ⌘V path#130
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

Important

Problem

Pasting into the chat composer inside the Amicode webview inserted the same text twice. The marker route (sibling PR from fix/paste-duplication-amicode-261) suppresses only the window-level fallback; the composer's own mod+V interceptor remains a second insert path, and the remote retest reproduced the double paste.

Approach

Make the window-level clipboard fallback the sole mod+V path: remove the composers' own mod+V keydown interceptors and the now-dead markers. Key reason: a single path cannot double-insert, and no suppression mechanism has to stay correct across two listeners.

Approaches Considered

  • Remove the composer's own handler (chosen) — one insert path remains; nothing left to disagree about. Accepted cost: plain Ctrl+V pastes text only.
  • Add stopPropagation to the global fallback — superficially tempting and actively harmful: the composer's handler is the only path that tries the image bridge, and suppressing it would silently kill screenshot paste while appearing to fix the bug.
  • Mark the composer with the self-owner attribute (marker route, sibling PR) — suppresses only the fallback; the composer's interceptor stays live; remote retest reproduced the double paste.

Scope

  • In: removal of the v1 composer's mod+V keydown block (prompt-input.tsx), the v2 composer's handleFramedPaste (interaction.ts, attachments.ts), the data-amc-clipboard="self" markers on both editors, and the structural test rewritten to assert a single mod+V path.
  • Out: the fallback's logic, unchanged. Copy and cut, which deliberately stay on the global path. The native paste path (Ctrl+Shift+V, context menu), which still attaches images. The bridge protocol and message kinds.

Assumptions / Open Qs

  • Whether plain-Ctrl+V being text-only is an acceptable long-term trade is a pending design decision; this PR is the stopgap that makes the webview single-paste while that is decided.

What was failing

Two mod+V handlers are live inside the framed app, and both insert.

The window-level fallback runs in the capture phase and serves every editable that has no bridged paste of its own — necessary because native paste never fires inside the webview iframe.

The composer's own handler intercepts mod+V, tries the image bridge first (screenshots), then falls back to text, and inserts through the composer's structured model.

The fallback calls preventDefault but deliberately not stopPropagation. That is correct for its purpose, but it means the composer's interceptor still receives the keystroke afterwards — and both handlers bridge the clipboard into the document. Two paths, two inserts. Marking the composer only exempted the first path; the second remained, which is why the marker route alone did not hold up under remote retest.

What changed

  • packages/app/src/components/prompt-input.tsx — the v1 composer's mod+V keydown interception block removed; the marker and its comment removed; unused imports dropped.
  • packages/session-ui/src/v2/components/prompt-input/interaction.ts — the mod+V interception (amicode patch Fix chat scroll jitter during streaming; respect reduced motion #11 parity block) removed; onKeyDown no longer special-cases V.
  • packages/session-ui/src/v2/components/prompt-input/attachments.tshandleFramedPaste removed; the paste-time fallbacks (readClipboardImage/readClipboardText via the bridge) remain on the native handlePaste path.
  • packages/session-ui/src/v2/components/prompt-input/index.tsx — marker and comment removed.
  • packages/app/src/utils/global-clipboard.ts — header re-documented: this module is now the sole mod+V path in the webview; the marker contract covers only the profile fields' paste fallback.
  • packages/app/src/components/prompt-input-clipboard-structure.test.ts — rewritten to assert the single-path contract: no composer-level V interception, no markers in either composer, the fallback owns the mod+V branch, and the native paste wiring with bridge fallbacks is intact.

Verification

  • Structural test: 5 passing (single-path assertions listed above).
  • packages/app unit suite: 827 passing, 0 failing (was 828 — the superseded v1-marker assertion is replaced by the single-path rewrite).
  • bun typecheck: clean in packages/app and packages/session-ui.
  • Remote GUI retest (amicode:main + this branch): Ctrl+V pastes exactly once; Ctrl+Shift+V pastes once with images intact. The retested binary is the stamped build (.buildinfo: branch fix/paste-duplication-patch, commit 0ecbc8aab, dirty: false at test time), so the verified artefact is attributable.

Key Decisions

  • Removal over suppression. A suppression mechanism leaves two listeners that must stay in agreement; removal leaves one. This is the same class of bug as the sweep's other findings — a mechanism and its integration disagreeing — and the single-path design has no agreement left to maintain.
  • Text-only on plain Ctrl+V, accepted for the stopgap. The extension bridge reads text; images on plain Ctrl+V are sacrificed. Native paste (Ctrl+Shift+V, context menu) keeps the image path, so screenshots remain reachable while the design question is decided.
  • Markers deleted, not kept. With the composer interceptor gone there is no second listener to exempt; a marker asserting nothing would be inert code.

Constraints & Invariants

  • The fallback's behaviour for every other editable is untouched.
  • Copy and cut still mirror to the OS clipboard through the global path.
  • No change to the bridge protocol or to the set of message kinds the host accepts.
  • No change to the keybind matcher or the native paste handler.

Source

Closesharmoniqs/amicode#261 (stopgap; acceptance pending the design decision above).

Companion to the marker-route PR from fix/paste-duplication-amicode-261; the two are alternatives, not stacked changes. Found during a bug sweep of the amicode ↔ vendored-opencode seam; related seam work is harmoniqs/amicode#243.

Notes

Why this shipped instead of the marker route: the marker mechanism and its single integration disagreed, and the retest showed the mechanism suppressing only one of two insert paths. "One path" is not a compromise on correctness — it is the only arrangement where the double insert is structurally impossible. The cost is a UX decision (plain Ctrl+V text-only) that is explicitly parked for design review, not silently accepted here.

Amicode Sweepand others added 5 commits August 7, 2026 08:05
Paste inserted the same text twice inside the amicode webview.
Two mod+V handlers are live in the framed app: the window-level capture-phase
fallback in utils/global-clipboard.ts, and the composer's own image-first
handler in prompt-input.tsx. The fallback exempts elements matching
CLIPBOARD_SELF_SELECTOR, and it calls preventDefault() but deliberately not
stopPropagation() — so an unmarked composer receives both insertions.
The marker had been applied to the home-cards credential fields but never to
the composer, even though global-clipboard.ts's own header names "the prompt
input's Cmd+V handler" as the element the exemption exists for.
Marking the composer is the correct fix rather than adding stopPropagation():
the composer's handler is the only path that tries the image bridge, so
suppressing it would silently kill screenshot paste. The marker owns PASTE
only; Cmd+C / Cmd+X keep mirroring through the global path.
Adds a structural guard. The existing global-clipboard test asserts the
exemption against a synthetic element it builds itself, which is precisely why
this shipped — the mechanism was covered, its single real integration was not.
Closesharmoniqs/amicode#261
…leanup of inputs forwarded to controller that should not be
@gennadiryan
gennadiryan marked this pull request as ready for review August 7, 2026 22:17
@gennadiryan
gennadiryan merged commit e246c2b into local/amicodeAug 7, 2026
1 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: paste duplicates input — clipboard content inserted twice

1 participant

@gennadiryan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app): make the window-level clipboard fallback the sole ⌘V path - #130

Merged
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch
Aug 7, 2026
Merged

fix(app): make the window-level clipboard fallback the sole ⌘V path#130
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

Important

Problem

Pasting into the chat composer inside the Amicode webview inserted the same text twice. The marker route (sibling PR from fix/paste-duplication-amicode-261) suppresses only the window-level fallback; the composer's own mod+V interceptor remains a second insert path, and the remote retest reproduced the double paste.

Approach

Make the window-level clipboard fallback the sole mod+V path: remove the composers' own mod+V keydown interceptors and the now-dead markers. Key reason: a single path cannot double-insert, and no suppression mechanism has to stay correct across two listeners.

Approaches Considered

  • Remove the composer's own handler (chosen) — one insert path remains; nothing left to disagree about. Accepted cost: plain Ctrl+V pastes text only.
  • Add stopPropagation to the global fallback — superficially tempting and actively harmful: the composer's handler is the only path that tries the image bridge, and suppressing it would silently kill screenshot paste while appearing to fix the bug.
  • Mark the composer with the self-owner attribute (marker route, sibling PR) — suppresses only the fallback; the composer's interceptor stays live; remote retest reproduced the double paste.

Scope

  • In: removal of the v1 composer's mod+V keydown block (prompt-input.tsx), the v2 composer's handleFramedPaste (interaction.ts, attachments.ts), the data-amc-clipboard="self" markers on both editors, and the structural test rewritten to assert a single mod+V path.
  • Out: the fallback's logic, unchanged. Copy and cut, which deliberately stay on the global path. The native paste path (Ctrl+Shift+V, context menu), which still attaches images. The bridge protocol and message kinds.

Assumptions / Open Qs

  • Whether plain-Ctrl+V being text-only is an acceptable long-term trade is a pending design decision; this PR is the stopgap that makes the webview single-paste while that is decided.

What was failing

Two mod+V handlers are live inside the framed app, and both insert.

The window-level fallback runs in the capture phase and serves every editable that has no bridged paste of its own — necessary because native paste never fires inside the webview iframe.

The composer's own handler intercepts mod+V, tries the image bridge first (screenshots), then falls back to text, and inserts through the composer's structured model.

The fallback calls preventDefault but deliberately not stopPropagation. That is correct for its purpose, but it means the composer's interceptor still receives the keystroke afterwards — and both handlers bridge the clipboard into the document. Two paths, two inserts. Marking the composer only exempted the first path; the second remained, which is why the marker route alone did not hold up under remote retest.

What changed

  • packages/app/src/components/prompt-input.tsx — the v1 composer's mod+V keydown interception block removed; the marker and its comment removed; unused imports dropped.
  • packages/session-ui/src/v2/components/prompt-input/interaction.ts — the mod+V interception (amicode patch Fix chat scroll jitter during streaming; respect reduced motion #11 parity block) removed; onKeyDown no longer special-cases V.
  • packages/session-ui/src/v2/components/prompt-input/attachments.tshandleFramedPaste removed; the paste-time fallbacks (readClipboardImage/readClipboardText via the bridge) remain on the native handlePaste path.
  • packages/session-ui/src/v2/components/prompt-input/index.tsx — marker and comment removed.
  • packages/app/src/utils/global-clipboard.ts — header re-documented: this module is now the sole mod+V path in the webview; the marker contract covers only the profile fields' paste fallback.
  • packages/app/src/components/prompt-input-clipboard-structure.test.ts — rewritten to assert the single-path contract: no composer-level V interception, no markers in either composer, the fallback owns the mod+V branch, and the native paste wiring with bridge fallbacks is intact.

Verification

  • Structural test: 5 passing (single-path assertions listed above).
  • packages/app unit suite: 827 passing, 0 failing (was 828 — the superseded v1-marker assertion is replaced by the single-path rewrite).
  • bun typecheck: clean in packages/app and packages/session-ui.
  • Remote GUI retest (amicode:main + this branch): Ctrl+V pastes exactly once; Ctrl+Shift+V pastes once with images intact. The retested binary is the stamped build (.buildinfo: branch fix/paste-duplication-patch, commit 0ecbc8aab, dirty: false at test time), so the verified artefact is attributable.

Key Decisions

  • Removal over suppression. A suppression mechanism leaves two listeners that must stay in agreement; removal leaves one. This is the same class of bug as the sweep's other findings — a mechanism and its integration disagreeing — and the single-path design has no agreement left to maintain.
  • Text-only on plain Ctrl+V, accepted for the stopgap. The extension bridge reads text; images on plain Ctrl+V are sacrificed. Native paste (Ctrl+Shift+V, context menu) keeps the image path, so screenshots remain reachable while the design question is decided.
  • Markers deleted, not kept. With the composer interceptor gone there is no second listener to exempt; a marker asserting nothing would be inert code.

Constraints & Invariants

  • The fallback's behaviour for every other editable is untouched.
  • Copy and cut still mirror to the OS clipboard through the global path.
  • No change to the bridge protocol or to the set of message kinds the host accepts.
  • No change to the keybind matcher or the native paste handler.

Source

Closesharmoniqs/amicode#261 (stopgap; acceptance pending the design decision above).

Companion to the marker-route PR from fix/paste-duplication-amicode-261; the two are alternatives, not stacked changes. Found during a bug sweep of the amicode ↔ vendored-opencode seam; related seam work is harmoniqs/amicode#243.

Notes

Why this shipped instead of the marker route: the marker mechanism and its single integration disagreed, and the retest showed the mechanism suppressing only one of two insert paths. "One path" is not a compromise on correctness — it is the only arrangement where the double insert is structurally impossible. The cost is a UX decision (plain Ctrl+V text-only) that is explicitly parked for design review, not silently accepted here.

Amicode Sweepand others added 5 commits August 7, 2026 08:05
Paste inserted the same text twice inside the amicode webview.
Two mod+V handlers are live in the framed app: the window-level capture-phase
fallback in utils/global-clipboard.ts, and the composer's own image-first
handler in prompt-input.tsx. The fallback exempts elements matching
CLIPBOARD_SELF_SELECTOR, and it calls preventDefault() but deliberately not
stopPropagation() — so an unmarked composer receives both insertions.
The marker had been applied to the home-cards credential fields but never to
the composer, even though global-clipboard.ts's own header names "the prompt
input's Cmd+V handler" as the element the exemption exists for.
Marking the composer is the correct fix rather than adding stopPropagation():
the composer's handler is the only path that tries the image bridge, so
suppressing it would silently kill screenshot paste. The marker owns PASTE
only; Cmd+C / Cmd+X keep mirroring through the global path.
Adds a structural guard. The existing global-clipboard test asserts the
exemption against a synthetic element it builds itself, which is precisely why
this shipped — the mechanism was covered, its single real integration was not.
Closesharmoniqs/amicode#261
…leanup of inputs forwarded to controller that should not be
@gennadiryan
gennadiryan marked this pull request as ready for review August 7, 2026 22:17
@gennadiryan
gennadiryan merged commit e246c2b into local/amicodeAug 7, 2026
1 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: paste duplicates input — clipboard content inserted twice

1 participant

@gennadiryan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(app): make the window-level clipboard fallback the sole ⌘V path - #130

Merged
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch
Aug 7, 2026
Merged

fix(app): make the window-level clipboard fallback the sole ⌘V path#130
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

Important

Problem

Pasting into the chat composer inside the Amicode webview inserted the same text twice. The marker route (sibling PR from fix/paste-duplication-amicode-261) suppresses only the window-level fallback; the composer's own mod+V interceptor remains a second insert path, and the remote retest reproduced the double paste.

Approach

Make the window-level clipboard fallback the sole mod+V path: remove the composers' own mod+V keydown interceptors and the now-dead markers. Key reason: a single path cannot double-insert, and no suppression mechanism has to stay correct across two listeners.

Approaches Considered

  • Remove the composer's own handler (chosen) — one insert path remains; nothing left to disagree about. Accepted cost: plain Ctrl+V pastes text only.
  • Add stopPropagation to the global fallback — superficially tempting and actively harmful: the composer's handler is the only path that tries the image bridge, and suppressing it would silently kill screenshot paste while appearing to fix the bug.
  • Mark the composer with the self-owner attribute (marker route, sibling PR) — suppresses only the fallback; the composer's interceptor stays live; remote retest reproduced the double paste.

Scope

  • In: removal of the v1 composer's mod+V keydown block (prompt-input.tsx), the v2 composer's handleFramedPaste (interaction.ts, attachments.ts), the data-amc-clipboard="self" markers on both editors, and the structural test rewritten to assert a single mod+V path.
  • Out: the fallback's logic, unchanged. Copy and cut, which deliberately stay on the global path. The native paste path (Ctrl+Shift+V, context menu), which still attaches images. The bridge protocol and message kinds.

Assumptions / Open Qs

  • Whether plain-Ctrl+V being text-only is an acceptable long-term trade is a pending design decision; this PR is the stopgap that makes the webview single-paste while that is decided.

What was failing

Two mod+V handlers are live inside the framed app, and both insert.

The window-level fallback runs in the capture phase and serves every editable that has no bridged paste of its own — necessary because native paste never fires inside the webview iframe.

The composer's own handler intercepts mod+V, tries the image bridge first (screenshots), then falls back to text, and inserts through the composer's structured model.

The fallback calls preventDefault but deliberately not stopPropagation. That is correct for its purpose, but it means the composer's interceptor still receives the keystroke afterwards — and both handlers bridge the clipboard into the document. Two paths, two inserts. Marking the composer only exempted the first path; the second remained, which is why the marker route alone did not hold up under remote retest.

What changed

  • packages/app/src/components/prompt-input.tsx — the v1 composer's mod+V keydown interception block removed; the marker and its comment removed; unused imports dropped.
  • packages/session-ui/src/v2/components/prompt-input/interaction.ts — the mod+V interception (amicode patch Fix chat scroll jitter during streaming; respect reduced motion #11 parity block) removed; onKeyDown no longer special-cases V.
  • packages/session-ui/src/v2/components/prompt-input/attachments.tshandleFramedPaste removed; the paste-time fallbacks (readClipboardImage/readClipboardText via the bridge) remain on the native handlePaste path.
  • packages/session-ui/src/v2/components/prompt-input/index.tsx — marker and comment removed.
  • packages/app/src/utils/global-clipboard.ts — header re-documented: this module is now the sole mod+V path in the webview; the marker contract covers only the profile fields' paste fallback.
  • packages/app/src/components/prompt-input-clipboard-structure.test.ts — rewritten to assert the single-path contract: no composer-level V interception, no markers in either composer, the fallback owns the mod+V branch, and the native paste wiring with bridge fallbacks is intact.

Verification

  • Structural test: 5 passing (single-path assertions listed above).
  • packages/app unit suite: 827 passing, 0 failing (was 828 — the superseded v1-marker assertion is replaced by the single-path rewrite).
  • bun typecheck: clean in packages/app and packages/session-ui.
  • Remote GUI retest (amicode:main + this branch): Ctrl+V pastes exactly once; Ctrl+Shift+V pastes once with images intact. The retested binary is the stamped build (.buildinfo: branch fix/paste-duplication-patch, commit 0ecbc8aab, dirty: false at test time), so the verified artefact is attributable.

Key Decisions

  • Removal over suppression. A suppression mechanism leaves two listeners that must stay in agreement; removal leaves one. This is the same class of bug as the sweep's other findings — a mechanism and its integration disagreeing — and the single-path design has no agreement left to maintain.
  • Text-only on plain Ctrl+V, accepted for the stopgap. The extension bridge reads text; images on plain Ctrl+V are sacrificed. Native paste (Ctrl+Shift+V, context menu) keeps the image path, so screenshots remain reachable while the design question is decided.
  • Markers deleted, not kept. With the composer interceptor gone there is no second listener to exempt; a marker asserting nothing would be inert code.

Constraints & Invariants

  • The fallback's behaviour for every other editable is untouched.
  • Copy and cut still mirror to the OS clipboard through the global path.
  • No change to the bridge protocol or to the set of message kinds the host accepts.
  • No change to the keybind matcher or the native paste handler.

Source

Closesharmoniqs/amicode#261 (stopgap; acceptance pending the design decision above).

Companion to the marker-route PR from fix/paste-duplication-amicode-261; the two are alternatives, not stacked changes. Found during a bug sweep of the amicode ↔ vendored-opencode seam; related seam work is harmoniqs/amicode#243.

Notes

Why this shipped instead of the marker route: the marker mechanism and its single integration disagreed, and the retest showed the mechanism suppressing only one of two insert paths. "One path" is not a compromise on correctness — it is the only arrangement where the double insert is structurally impossible. The cost is a UX decision (plain Ctrl+V text-only) that is explicitly parked for design review, not silently accepted here.

Amicode Sweepand others added 5 commits August 7, 2026 08:05
Paste inserted the same text twice inside the amicode webview.
Two mod+V handlers are live in the framed app: the window-level capture-phase
fallback in utils/global-clipboard.ts, and the composer's own image-first
handler in prompt-input.tsx. The fallback exempts elements matching
CLIPBOARD_SELF_SELECTOR, and it calls preventDefault() but deliberately not
stopPropagation() — so an unmarked composer receives both insertions.
The marker had been applied to the home-cards credential fields but never to
the composer, even though global-clipboard.ts's own header names "the prompt
input's Cmd+V handler" as the element the exemption exists for.
Marking the composer is the correct fix rather than adding stopPropagation():
the composer's handler is the only path that tries the image bridge, so
suppressing it would silently kill screenshot paste. The marker owns PASTE
only; Cmd+C / Cmd+X keep mirroring through the global path.
Adds a structural guard. The existing global-clipboard test asserts the
exemption against a synthetic element it builds itself, which is precisely why
this shipped — the mechanism was covered, its single real integration was not.
Closesharmoniqs/amicode#261
…leanup of inputs forwarded to controller that should not be
@gennadiryan
gennadiryan marked this pull request as ready for review August 7, 2026 22:17
@gennadiryan
gennadiryan merged commit e246c2b into local/amicodeAug 7, 2026
1 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: paste duplicates input — clipboard content inserted twice

1 participant

@gennadiryan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app): make the window-level clipboard fallback the sole ⌘V path - #130

Merged
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch
Aug 7, 2026
Merged

fix(app): make the window-level clipboard fallback the sole ⌘V path#130
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

Important

Problem

Pasting into the chat composer inside the Amicode webview inserted the same text twice. The marker route (sibling PR from fix/paste-duplication-amicode-261) suppresses only the window-level fallback; the composer's own mod+V interceptor remains a second insert path, and the remote retest reproduced the double paste.

Approach

Make the window-level clipboard fallback the sole mod+V path: remove the composers' own mod+V keydown interceptors and the now-dead markers. Key reason: a single path cannot double-insert, and no suppression mechanism has to stay correct across two listeners.

Approaches Considered

  • Remove the composer's own handler (chosen) — one insert path remains; nothing left to disagree about. Accepted cost: plain Ctrl+V pastes text only.
  • Add stopPropagation to the global fallback — superficially tempting and actively harmful: the composer's handler is the only path that tries the image bridge, and suppressing it would silently kill screenshot paste while appearing to fix the bug.
  • Mark the composer with the self-owner attribute (marker route, sibling PR) — suppresses only the fallback; the composer's interceptor stays live; remote retest reproduced the double paste.

Scope

  • In: removal of the v1 composer's mod+V keydown block (prompt-input.tsx), the v2 composer's handleFramedPaste (interaction.ts, attachments.ts), the data-amc-clipboard="self" markers on both editors, and the structural test rewritten to assert a single mod+V path.
  • Out: the fallback's logic, unchanged. Copy and cut, which deliberately stay on the global path. The native paste path (Ctrl+Shift+V, context menu), which still attaches images. The bridge protocol and message kinds.

Assumptions / Open Qs

  • Whether plain-Ctrl+V being text-only is an acceptable long-term trade is a pending design decision; this PR is the stopgap that makes the webview single-paste while that is decided.

What was failing

Two mod+V handlers are live inside the framed app, and both insert.

The window-level fallback runs in the capture phase and serves every editable that has no bridged paste of its own — necessary because native paste never fires inside the webview iframe.

The composer's own handler intercepts mod+V, tries the image bridge first (screenshots), then falls back to text, and inserts through the composer's structured model.

The fallback calls preventDefault but deliberately not stopPropagation. That is correct for its purpose, but it means the composer's interceptor still receives the keystroke afterwards — and both handlers bridge the clipboard into the document. Two paths, two inserts. Marking the composer only exempted the first path; the second remained, which is why the marker route alone did not hold up under remote retest.

What changed

  • packages/app/src/components/prompt-input.tsx — the v1 composer's mod+V keydown interception block removed; the marker and its comment removed; unused imports dropped.
  • packages/session-ui/src/v2/components/prompt-input/interaction.ts — the mod+V interception (amicode patch Fix chat scroll jitter during streaming; respect reduced motion #11 parity block) removed; onKeyDown no longer special-cases V.
  • packages/session-ui/src/v2/components/prompt-input/attachments.tshandleFramedPaste removed; the paste-time fallbacks (readClipboardImage/readClipboardText via the bridge) remain on the native handlePaste path.
  • packages/session-ui/src/v2/components/prompt-input/index.tsx — marker and comment removed.
  • packages/app/src/utils/global-clipboard.ts — header re-documented: this module is now the sole mod+V path in the webview; the marker contract covers only the profile fields' paste fallback.
  • packages/app/src/components/prompt-input-clipboard-structure.test.ts — rewritten to assert the single-path contract: no composer-level V interception, no markers in either composer, the fallback owns the mod+V branch, and the native paste wiring with bridge fallbacks is intact.

Verification

  • Structural test: 5 passing (single-path assertions listed above).
  • packages/app unit suite: 827 passing, 0 failing (was 828 — the superseded v1-marker assertion is replaced by the single-path rewrite).
  • bun typecheck: clean in packages/app and packages/session-ui.
  • Remote GUI retest (amicode:main + this branch): Ctrl+V pastes exactly once; Ctrl+Shift+V pastes once with images intact. The retested binary is the stamped build (.buildinfo: branch fix/paste-duplication-patch, commit 0ecbc8aab, dirty: false at test time), so the verified artefact is attributable.

Key Decisions

  • Removal over suppression. A suppression mechanism leaves two listeners that must stay in agreement; removal leaves one. This is the same class of bug as the sweep's other findings — a mechanism and its integration disagreeing — and the single-path design has no agreement left to maintain.
  • Text-only on plain Ctrl+V, accepted for the stopgap. The extension bridge reads text; images on plain Ctrl+V are sacrificed. Native paste (Ctrl+Shift+V, context menu) keeps the image path, so screenshots remain reachable while the design question is decided.
  • Markers deleted, not kept. With the composer interceptor gone there is no second listener to exempt; a marker asserting nothing would be inert code.

Constraints & Invariants

  • The fallback's behaviour for every other editable is untouched.
  • Copy and cut still mirror to the OS clipboard through the global path.
  • No change to the bridge protocol or to the set of message kinds the host accepts.
  • No change to the keybind matcher or the native paste handler.

Source

Closesharmoniqs/amicode#261 (stopgap; acceptance pending the design decision above).

Companion to the marker-route PR from fix/paste-duplication-amicode-261; the two are alternatives, not stacked changes. Found during a bug sweep of the amicode ↔ vendored-opencode seam; related seam work is harmoniqs/amicode#243.

Notes

Why this shipped instead of the marker route: the marker mechanism and its single integration disagreed, and the retest showed the mechanism suppressing only one of two insert paths. "One path" is not a compromise on correctness — it is the only arrangement where the double insert is structurally impossible. The cost is a UX decision (plain Ctrl+V text-only) that is explicitly parked for design review, not silently accepted here.

Amicode Sweepand others added 5 commits August 7, 2026 08:05
Paste inserted the same text twice inside the amicode webview.
Two mod+V handlers are live in the framed app: the window-level capture-phase
fallback in utils/global-clipboard.ts, and the composer's own image-first
handler in prompt-input.tsx. The fallback exempts elements matching
CLIPBOARD_SELF_SELECTOR, and it calls preventDefault() but deliberately not
stopPropagation() — so an unmarked composer receives both insertions.
The marker had been applied to the home-cards credential fields but never to
the composer, even though global-clipboard.ts's own header names "the prompt
input's Cmd+V handler" as the element the exemption exists for.
Marking the composer is the correct fix rather than adding stopPropagation():
the composer's handler is the only path that tries the image bridge, so
suppressing it would silently kill screenshot paste. The marker owns PASTE
only; Cmd+C / Cmd+X keep mirroring through the global path.
Adds a structural guard. The existing global-clipboard test asserts the
exemption against a synthetic element it builds itself, which is precisely why
this shipped — the mechanism was covered, its single real integration was not.
Closesharmoniqs/amicode#261
…leanup of inputs forwarded to controller that should not be
@gennadiryan
gennadiryan marked this pull request as ready for review August 7, 2026 22:17
@gennadiryan
gennadiryan merged commit e246c2b into local/amicodeAug 7, 2026
1 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: paste duplicates input — clipboard content inserted twice

1 participant

@gennadiryan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app): make the window-level clipboard fallback the sole ⌘V path - #130

Merged
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch
Aug 7, 2026
Merged

fix(app): make the window-level clipboard fallback the sole ⌘V path#130
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

Important

Problem

Pasting into the chat composer inside the Amicode webview inserted the same text twice. The marker route (sibling PR from fix/paste-duplication-amicode-261) suppresses only the window-level fallback; the composer's own mod+V interceptor remains a second insert path, and the remote retest reproduced the double paste.

Approach

Make the window-level clipboard fallback the sole mod+V path: remove the composers' own mod+V keydown interceptors and the now-dead markers. Key reason: a single path cannot double-insert, and no suppression mechanism has to stay correct across two listeners.

Approaches Considered

  • Remove the composer's own handler (chosen) — one insert path remains; nothing left to disagree about. Accepted cost: plain Ctrl+V pastes text only.
  • Add stopPropagation to the global fallback — superficially tempting and actively harmful: the composer's handler is the only path that tries the image bridge, and suppressing it would silently kill screenshot paste while appearing to fix the bug.
  • Mark the composer with the self-owner attribute (marker route, sibling PR) — suppresses only the fallback; the composer's interceptor stays live; remote retest reproduced the double paste.

Scope

  • In: removal of the v1 composer's mod+V keydown block (prompt-input.tsx), the v2 composer's handleFramedPaste (interaction.ts, attachments.ts), the data-amc-clipboard="self" markers on both editors, and the structural test rewritten to assert a single mod+V path.
  • Out: the fallback's logic, unchanged. Copy and cut, which deliberately stay on the global path. The native paste path (Ctrl+Shift+V, context menu), which still attaches images. The bridge protocol and message kinds.

Assumptions / Open Qs

  • Whether plain-Ctrl+V being text-only is an acceptable long-term trade is a pending design decision; this PR is the stopgap that makes the webview single-paste while that is decided.

What was failing

Two mod+V handlers are live inside the framed app, and both insert.

The window-level fallback runs in the capture phase and serves every editable that has no bridged paste of its own — necessary because native paste never fires inside the webview iframe.

The composer's own handler intercepts mod+V, tries the image bridge first (screenshots), then falls back to text, and inserts through the composer's structured model.

The fallback calls preventDefault but deliberately not stopPropagation. That is correct for its purpose, but it means the composer's interceptor still receives the keystroke afterwards — and both handlers bridge the clipboard into the document. Two paths, two inserts. Marking the composer only exempted the first path; the second remained, which is why the marker route alone did not hold up under remote retest.

What changed

  • packages/app/src/components/prompt-input.tsx — the v1 composer's mod+V keydown interception block removed; the marker and its comment removed; unused imports dropped.
  • packages/session-ui/src/v2/components/prompt-input/interaction.ts — the mod+V interception (amicode patch Fix chat scroll jitter during streaming; respect reduced motion #11 parity block) removed; onKeyDown no longer special-cases V.
  • packages/session-ui/src/v2/components/prompt-input/attachments.tshandleFramedPaste removed; the paste-time fallbacks (readClipboardImage/readClipboardText via the bridge) remain on the native handlePaste path.
  • packages/session-ui/src/v2/components/prompt-input/index.tsx — marker and comment removed.
  • packages/app/src/utils/global-clipboard.ts — header re-documented: this module is now the sole mod+V path in the webview; the marker contract covers only the profile fields' paste fallback.
  • packages/app/src/components/prompt-input-clipboard-structure.test.ts — rewritten to assert the single-path contract: no composer-level V interception, no markers in either composer, the fallback owns the mod+V branch, and the native paste wiring with bridge fallbacks is intact.

Verification

  • Structural test: 5 passing (single-path assertions listed above).
  • packages/app unit suite: 827 passing, 0 failing (was 828 — the superseded v1-marker assertion is replaced by the single-path rewrite).
  • bun typecheck: clean in packages/app and packages/session-ui.
  • Remote GUI retest (amicode:main + this branch): Ctrl+V pastes exactly once; Ctrl+Shift+V pastes once with images intact. The retested binary is the stamped build (.buildinfo: branch fix/paste-duplication-patch, commit 0ecbc8aab, dirty: false at test time), so the verified artefact is attributable.

Key Decisions

  • Removal over suppression. A suppression mechanism leaves two listeners that must stay in agreement; removal leaves one. This is the same class of bug as the sweep's other findings — a mechanism and its integration disagreeing — and the single-path design has no agreement left to maintain.
  • Text-only on plain Ctrl+V, accepted for the stopgap. The extension bridge reads text; images on plain Ctrl+V are sacrificed. Native paste (Ctrl+Shift+V, context menu) keeps the image path, so screenshots remain reachable while the design question is decided.
  • Markers deleted, not kept. With the composer interceptor gone there is no second listener to exempt; a marker asserting nothing would be inert code.

Constraints & Invariants

  • The fallback's behaviour for every other editable is untouched.
  • Copy and cut still mirror to the OS clipboard through the global path.
  • No change to the bridge protocol or to the set of message kinds the host accepts.
  • No change to the keybind matcher or the native paste handler.

Source

Closesharmoniqs/amicode#261 (stopgap; acceptance pending the design decision above).

Companion to the marker-route PR from fix/paste-duplication-amicode-261; the two are alternatives, not stacked changes. Found during a bug sweep of the amicode ↔ vendored-opencode seam; related seam work is harmoniqs/amicode#243.

Notes

Why this shipped instead of the marker route: the marker mechanism and its single integration disagreed, and the retest showed the mechanism suppressing only one of two insert paths. "One path" is not a compromise on correctness — it is the only arrangement where the double insert is structurally impossible. The cost is a UX decision (plain Ctrl+V text-only) that is explicitly parked for design review, not silently accepted here.

Amicode Sweepand others added 5 commits August 7, 2026 08:05
Paste inserted the same text twice inside the amicode webview.
Two mod+V handlers are live in the framed app: the window-level capture-phase
fallback in utils/global-clipboard.ts, and the composer's own image-first
handler in prompt-input.tsx. The fallback exempts elements matching
CLIPBOARD_SELF_SELECTOR, and it calls preventDefault() but deliberately not
stopPropagation() — so an unmarked composer receives both insertions.
The marker had been applied to the home-cards credential fields but never to
the composer, even though global-clipboard.ts's own header names "the prompt
input's Cmd+V handler" as the element the exemption exists for.
Marking the composer is the correct fix rather than adding stopPropagation():
the composer's handler is the only path that tries the image bridge, so
suppressing it would silently kill screenshot paste. The marker owns PASTE
only; Cmd+C / Cmd+X keep mirroring through the global path.
Adds a structural guard. The existing global-clipboard test asserts the
exemption against a synthetic element it builds itself, which is precisely why
this shipped — the mechanism was covered, its single real integration was not.
Closesharmoniqs/amicode#261
…leanup of inputs forwarded to controller that should not be
@gennadiryan
gennadiryan marked this pull request as ready for review August 7, 2026 22:17
@gennadiryan
gennadiryan merged commit e246c2b into local/amicodeAug 7, 2026
1 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: paste duplicates input — clipboard content inserted twice

1 participant

@gennadiryan
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(app): make the window-level clipboard fallback the sole ⌘V path - #130

Merged
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch
Aug 7, 2026
Merged

fix(app): make the window-level clipboard fallback the sole ⌘V path#130
gennadiryan merged 5 commits into
local/amicodefrom
fix/paste-duplication-patch

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

Important

Problem

Pasting into the chat composer inside the Amicode webview inserted the same text twice. The marker route (sibling PR from fix/paste-duplication-amicode-261) suppresses only the window-level fallback; the composer's own mod+V interceptor remains a second insert path, and the remote retest reproduced the double paste.

Approach

Make the window-level clipboard fallback the sole mod+V path: remove the composers' own mod+V keydown interceptors and the now-dead markers. Key reason: a single path cannot double-insert, and no suppression mechanism has to stay correct across two listeners.

Approaches Considered

  • Remove the composer's own handler (chosen) — one insert path remains; nothing left to disagree about. Accepted cost: plain Ctrl+V pastes text only.
  • Add stopPropagation to the global fallback — superficially tempting and actively harmful: the composer's handler is the only path that tries the image bridge, and suppressing it would silently kill screenshot paste while appearing to fix the bug.
  • Mark the composer with the self-owner attribute (marker route, sibling PR) — suppresses only the fallback; the composer's interceptor stays live; remote retest reproduced the double paste.

Scope

  • In: removal of the v1 composer's mod+V keydown block (prompt-input.tsx), the v2 composer's handleFramedPaste (interaction.ts, attachments.ts), the data-amc-clipboard="self" markers on both editors, and the structural test rewritten to assert a single mod+V path.
  • Out: the fallback's logic, unchanged. Copy and cut, which deliberately stay on the global path. The native paste path (Ctrl+Shift+V, context menu), which still attaches images. The bridge protocol and message kinds.

Assumptions / Open Qs

  • Whether plain-Ctrl+V being text-only is an acceptable long-term trade is a pending design decision; this PR is the stopgap that makes the webview single-paste while that is decided.

What was failing

Two mod+V handlers are live inside the framed app, and both insert.

The window-level fallback runs in the capture phase and serves every editable that has no bridged paste of its own — necessary because native paste never fires inside the webview iframe.

The composer's own handler intercepts mod+V, tries the image bridge first (screenshots), then falls back to text, and inserts through the composer's structured model.

The fallback calls preventDefault but deliberately not stopPropagation. That is correct for its purpose, but it means the composer's interceptor still receives the keystroke afterwards — and both handlers bridge the clipboard into the document. Two paths, two inserts. Marking the composer only exempted the first path; the second remained, which is why the marker route alone did not hold up under remote retest.

What changed

  • packages/app/src/components/prompt-input.tsx — the v1 composer's mod+V keydown interception block removed; the marker and its comment removed; unused imports dropped.
  • packages/session-ui/src/v2/components/prompt-input/interaction.ts — the mod+V interception (amicode patch Fix chat scroll jitter during streaming; respect reduced motion #11 parity block) removed; onKeyDown no longer special-cases V.
  • packages/session-ui/src/v2/components/prompt-input/attachments.tshandleFramedPaste removed; the paste-time fallbacks (readClipboardImage/readClipboardText via the bridge) remain on the native handlePaste path.
  • packages/session-ui/src/v2/components/prompt-input/index.tsx — marker and comment removed.
  • packages/app/src/utils/global-clipboard.ts — header re-documented: this module is now the sole mod+V path in the webview; the marker contract covers only the profile fields' paste fallback.
  • packages/app/src/components/prompt-input-clipboard-structure.test.ts — rewritten to assert the single-path contract: no composer-level V interception, no markers in either composer, the fallback owns the mod+V branch, and the native paste wiring with bridge fallbacks is intact.

Verification

  • Structural test: 5 passing (single-path assertions listed above).
  • packages/app unit suite: 827 passing, 0 failing (was 828 — the superseded v1-marker assertion is replaced by the single-path rewrite).
  • bun typecheck: clean in packages/app and packages/session-ui.
  • Remote GUI retest (amicode:main + this branch): Ctrl+V pastes exactly once; Ctrl+Shift+V pastes once with images intact. The retested binary is the stamped build (.buildinfo: branch fix/paste-duplication-patch, commit 0ecbc8aab, dirty: false at test time), so the verified artefact is attributable.

Key Decisions

  • Removal over suppression. A suppression mechanism leaves two listeners that must stay in agreement; removal leaves one. This is the same class of bug as the sweep's other findings — a mechanism and its integration disagreeing — and the single-path design has no agreement left to maintain.
  • Text-only on plain Ctrl+V, accepted for the stopgap. The extension bridge reads text; images on plain Ctrl+V are sacrificed. Native paste (Ctrl+Shift+V, context menu) keeps the image path, so screenshots remain reachable while the design question is decided.
  • Markers deleted, not kept. With the composer interceptor gone there is no second listener to exempt; a marker asserting nothing would be inert code.

Constraints & Invariants

  • The fallback's behaviour for every other editable is untouched.
  • Copy and cut still mirror to the OS clipboard through the global path.
  • No change to the bridge protocol or to the set of message kinds the host accepts.
  • No change to the keybind matcher or the native paste handler.

Source

Closesharmoniqs/amicode#261 (stopgap; acceptance pending the design decision above).

Companion to the marker-route PR from fix/paste-duplication-amicode-261; the two are alternatives, not stacked changes. Found during a bug sweep of the amicode ↔ vendored-opencode seam; related seam work is harmoniqs/amicode#243.

Notes

Why this shipped instead of the marker route: the marker mechanism and its single integration disagreed, and the retest showed the mechanism suppressing only one of two insert paths. "One path" is not a compromise on correctness — it is the only arrangement where the double insert is structurally impossible. The cost is a UX decision (plain Ctrl+V text-only) that is explicitly parked for design review, not silently accepted here.

Amicode Sweepand others added 5 commits August 7, 2026 08:05
Paste inserted the same text twice inside the amicode webview.
Two mod+V handlers are live in the framed app: the window-level capture-phase
fallback in utils/global-clipboard.ts, and the composer's own image-first
handler in prompt-input.tsx. The fallback exempts elements matching
CLIPBOARD_SELF_SELECTOR, and it calls preventDefault() but deliberately not
stopPropagation() — so an unmarked composer receives both insertions.
The marker had been applied to the home-cards credential fields but never to
the composer, even though global-clipboard.ts's own header names "the prompt
input's Cmd+V handler" as the element the exemption exists for.
Marking the composer is the correct fix rather than adding stopPropagation():
the composer's handler is the only path that tries the image bridge, so
suppressing it would silently kill screenshot paste. The marker owns PASTE
only; Cmd+C / Cmd+X keep mirroring through the global path.
Adds a structural guard. The existing global-clipboard test asserts the
exemption against a synthetic element it builds itself, which is precisely why
this shipped — the mechanism was covered, its single real integration was not.
Closesharmoniqs/amicode#261
…leanup of inputs forwarded to controller that should not be
@gennadiryan
gennadiryan marked this pull request as ready for review August 7, 2026 22:17
@gennadiryan
gennadiryan merged commit e246c2b into local/amicodeAug 7, 2026
1 of 4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: paste duplicates input — clipboard content inserted twice

1 participant

@gennadiryan