From 32d719dc43d5ffab33883f430b1028b8450140a8 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Tue, 12 May 2026 14:39:20 +0200 Subject: [PATCH] fix: hardcode event-driven paste submit --- .../src/app-state/settings/persistence.ts | 5 --- src/renderer/src/app-state/settings/types.ts | 25 ++--------- .../features/settings/lib/settingsRegistry.ts | 23 ----------- .../tile-tree/TileLeaf/claudePaste.ts | 41 +++++++++---------- .../tile-tree/TileLeaf/useComposerKeybinds.ts | 24 ++++------- 5 files changed, 31 insertions(+), 87 deletions(-) diff --git a/src/renderer/src/app-state/settings/persistence.ts b/src/renderer/src/app-state/settings/persistence.ts index 3fb1318b9..13cadf7d1 100644 --- a/src/renderer/src/app-state/settings/persistence.ts +++ b/src/renderer/src/app-state/settings/persistence.ts @@ -45,11 +45,6 @@ export function coerceSettings(value: unknown): Settings { // setting's docstring. Anything looser (e.g. `!== false`) would // flip the default to ON for fresh installs. dispatchProjectTerminal: parsed.dispatchProjectTerminal === true, - // `!== false` so default-ON applies on fresh installs AND when - // the persisted value is missing entirely (older localStorage - // blobs predating this setting). The only way to land OFF is an - // explicit `false`, which is exactly the toggle UX we want. - eventDrivenPasteSubmit: parsed.eventDrivenPasteSubmit !== false, // WHY membership check via WORKSPACE_MODES rather than a literal // === 'dispatch': keeps the source of truth in one array so adding // a new mode label later (if ever) only requires editing types.ts. diff --git a/src/renderer/src/app-state/settings/types.ts b/src/renderer/src/app-state/settings/types.ts index f621e73e8..7ba529e76 100644 --- a/src/renderer/src/app-state/settings/types.ts +++ b/src/renderer/src/app-state/settings/types.ts @@ -132,28 +132,10 @@ export type Settings = { * the "I turned it off but it came back" symptom hard to reason * about: fresh workspaces, new tabs, and any code path that re- * entered dispatch defaulted the flag to ON. Moving the gate to a - * global setting collapses the toggle surface to one place the user - * controls and removes the "terminal always mounted even when turned - * off" failure mode. */ + * global setting collapses the toggle surface to one place the user + * controls and removes the "terminal always mounted even when turned + * off" failure mode. */ dispatchProjectTerminal: boolean - /** Event-driven paste-submit. When ON (default), `claudePaste.ts` - * polls Claude's live TUI screen for `[Pasted text #N]` before - * sending `\r`, instead of waiting a fixed 125 ms wall clock. - * Falls back to the timer if the placeholder doesn't appear within - * 2 s — the timer path is the existing production behavior, so - * flipping this setting OFF restores cc-shell v0.0.2 paste-submit - * behavior exactly. - * - * Default ON because the PTY-isolated harness at - * `vendor/in_progress/paste-submit-repro/` shows event-driven is - * 10/10 reliable AND faster on average (~58 ms wait vs. 125 ms - * unconditional). Even though the harness CANNOT reproduce the - * user-facing "first Enter does nothing" symptom (production has - * an additional failure mode we haven't isolated yet — see the - * per-paste debug-dump diagnostic landed in this same PR), - * event-driven cannot be WORSE than the timer in any modeled - * scenario, so shipping ON is strictly defensive. */ - eventDrivenPasteSubmit: boolean } export const DEFAULT_SETTINGS: Settings = { @@ -171,5 +153,4 @@ export const DEFAULT_SETTINGS: Settings = { aggressiveDebugPersistence: false, defaultWorkspaceMode: 'grid', dispatchProjectTerminal: false, - eventDrivenPasteSubmit: true, } diff --git a/src/renderer/src/features/settings/lib/settingsRegistry.ts b/src/renderer/src/features/settings/lib/settingsRegistry.ts index 81715b689..9811ac245 100644 --- a/src/renderer/src/features/settings/lib/settingsRegistry.ts +++ b/src/renderer/src/features/settings/lib/settingsRegistry.ts @@ -224,29 +224,6 @@ export function getSettingsRegistry(): SettingDefinition[] { onToggle: (ctx, value) => ctx.onChange({ dispatchProjectTerminal: value }), }, }, - { - // Track C of the paste-submit work. Default ON; flipping OFF - // restores the pre-fix wall-clock 125 ms timer path. The - // primary reason this toggle exists at all is that the - // event-driven path depends on Claude's TUI continuing to - // render `[Pasted text #N]` verbatim — if a future Claude - // update renames or removes that placeholder, users may want - // a quick escape hatch back to the timer without waiting for - // us to ship a fix. Categorized as `workspace` (not - // `experimental`) because every cc-shell user pastes into - // Claude; this is a quality-of-life setting, not an opt-in. - id: 'event-driven-paste-submit', - category: 'workspace', - title: 'Event-Driven Paste Submit', - description: - 'Wait for Claude\'s "[Pasted text #N]" placeholder before sending Enter, instead of a 125 ms wall-clock delay. Faster and more reliable. Turn off only if your Claude TUI version stops rendering the placeholder.', - keywords: ['paste', 'submit', 'enter', 'claude', 'event-driven', 'placeholder', 'composer'], - control: { - type: 'toggle', - getValue: settings => settings.eventDrivenPasteSubmit, - onToggle: (ctx, value) => ctx.onChange({ eventDrivenPasteSubmit: value }), - }, - }, { id: 'aggressive-debug-persistence', category: 'experimental', diff --git a/src/renderer/src/workspace/tile-tree/TileLeaf/claudePaste.ts b/src/renderer/src/workspace/tile-tree/TileLeaf/claudePaste.ts index 71fa53225..b3330c7cf 100644 --- a/src/renderer/src/workspace/tile-tree/TileLeaf/claudePaste.ts +++ b/src/renderer/src/workspace/tile-tree/TileLeaf/claudePaste.ts @@ -14,27 +14,25 @@ // // We fix this at the shell boundary instead of forking Claude's paste // logic: for Claude only, any prompt that is likely to be treated as -// a paste gets delivered in TWO phases — first the payload, then -// (depending on the chosen strategy) a separate submit write. +// a paste gets delivered in TWO phases — first the payload, then a +// separate submit write after Claude visibly acknowledges the paste. // -// TWO submit-after-paste strategies coexist here: +// The primary strategy is event-driven and not user-configurable: // -// * EVENT-DRIVEN (default, when `eventDriven` is set): poll -// Claude's screen snapshot for `[Pasted text #N]` and send `\r` -// the instant the placeholder appears. Load-independent — -// works at 10 ms or 10 s. This is the strategy the paste-submit -// repro harness at `vendor/in_progress/paste-submit-repro/` -// showed to be 10/10 reliable with average waits ~58 ms. +// * EVENT-DRIVEN: poll Claude's screen snapshot for +// `[Pasted text #N]` and send `\r` the instant the placeholder +// appears. Load-independent — works at 10 ms or 10 s. This is +// the strategy the paste-submit repro harness at +// `vendor/in_progress/paste-submit-repro/` showed to be 10/10 +// reliable with average waits ~58 ms. // // * WALL-CLOCK FALLBACK: wait `delayMs` (default 125 ms — slightly // above Claude's 100 ms paste completion timeout) then send `\r`. -// Kept as a fallback because the event-driven path depends on -// Claude continuing to render the `[Pasted text #N]` placeholder -// verbatim. A future Claude UI rename would break placeholder -// detection but should not brick paste-submit entirely. The -// harness shows the timer races even at 1000 ms under load, so -// this is genuinely just a "don't be worse than yesterday" floor, -// not a load-bearing primary path. +// Kept only as a fallback because the event-driven path depends on +// Claude continuing to render the placeholder. A future Claude UI +// rename should degrade paste-submit, not brick it entirely. The +// harness shows the timer races even at 1000 ms under load, so it +// must never be exposed as a selectable primary path again. // // Why these numbers: // - 100 chars: empirical lower bound on text length where Claude's @@ -54,7 +52,8 @@ // - 125 ms is slightly above Claude's 100 ms paste completion // timeout. Going lower risks reintroducing the race; going much // higher makes submit feel laggy on every long paste. Only -// consulted when the event-driven path times out or is disabled. +// consulted only when the event-driven path times out or cannot +// reach the live Claude session. // - 500 ms event-driven safety bound (down from 2000 ms): for // bracketed pastes that DO NOT cross Claude's inline→collapse // threshold, the `[Pasted text #N]` placeholder never renders, so @@ -94,10 +93,10 @@ export type ClaudePasteOpts = { * every PTY write in this paste emits an IPC:write event to the * journal. */ pasteId?: string - /** Event-driven submit. When provided AND `enabled`, poll Claude's - * screen snapshot for `[Pasted text #N]` before sending `\r`, - * instead of relying on `delayMs`. Falls back to the timer path - * after `CLAUDE_PASTE_EVENT_DRIVEN_TIMEOUT_MS`. */ + /** Event-driven submit for Claude paste-like text. The composer always + * provides this for Claude; optional in the type only because lower-level + * helpers are also used by Codex and image-path paste flows that must not + * wait for Claude's text placeholder. */ eventDriven?: { enabled: boolean; sessionId: string } } diff --git a/src/renderer/src/workspace/tile-tree/TileLeaf/useComposerKeybinds.ts b/src/renderer/src/workspace/tile-tree/TileLeaf/useComposerKeybinds.ts index b917ab0ff..7c114be50 100644 --- a/src/renderer/src/workspace/tile-tree/TileLeaf/useComposerKeybinds.ts +++ b/src/renderer/src/workspace/tile-tree/TileLeaf/useComposerKeybinds.ts @@ -2,7 +2,6 @@ import { useState } from 'react' import { extractAssistantInProgress } from '@shared/parsers/extractAssistant' -import { useAppStore } from '@renderer/app-state/hooks' import type { SessionId } from '@renderer/workspace/types' import type { SessionRuntime, Workspace } from '@renderer/workspace/workspaceStore' import { @@ -76,13 +75,6 @@ export function useComposerKeybinds({ // runtime.picker, but we use slashMode to decide whether keys // should be forwarded vs. stored locally). const [slashMode, setSlashMode] = useState(false) - // Event-driven paste-submit. When ON (default) the paste flow - // polls for Claude's `[Pasted text #N]` placeholder before sending - // `\r` instead of relying on a wall-clock timer; see claudePaste.ts - // for the full strategy comment. Read here so the toggle takes - // effect on the next paste without remount. - const eventDrivenPasteSubmit = useAppStore(state => state.settings.eventDrivenPasteSubmit) - const exitSlashMode = () => { setSlashMode(false) setInputText('') @@ -330,24 +322,24 @@ export function useComposerKeybinds({ // composer until a later keypress nudges it through // the normal submit path. // - // Event-driven mode (default ON via settings) polls Claude's - // screen for `[Pasted text #N]` and submits the instant it - // appears — load-independent, dramatically faster than the - // 125 ms wall-clock floor. Wall-clock delay is kept as a - // safety fallback if the placeholder never materializes - // (e.g. future Claude UI rename); see claudePaste.ts. + // Claude paste-like text always takes the event-driven path: + // wait until the TUI has visibly committed the paste placeholder, + // then send Enter. The old 125 ms timer is not a setting because + // real production traces showed it races Claude's accumulator; it + // remains only as an internal fallback when placeholder detection + // cannot run or times out. See claudePaste.ts. window.api.recordPasteDebugEvent(pasteId, { layer: 'RENDER', event: 'route:claude-paste-like', data: { inputLen: input.length, hasNewline: input.includes('\n'), - eventDriven: eventDrivenPasteSubmit, + eventDriven: true, }, }) await sendBracketedPasteThenSubmit(send, input, CLAUDE_PASTE_SUBMIT_DELAY_MS, { pasteId, - eventDriven: { enabled: eventDrivenPasteSubmit, sessionId }, + eventDriven: { enabled: true, sessionId }, }) } else { window.api.recordPasteDebugEvent(pasteId, {