Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/renderer/src/app-state/settings/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 3 additions & 22 deletions src/renderer/src/app-state/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -171,5 +153,4 @@ export const DEFAULT_SETTINGS: Settings = {
aggressiveDebugPersistence: false,
defaultWorkspaceMode: 'grid',
dispatchProjectTerminal: false,
eventDrivenPasteSubmit: true,
}
23 changes: 0 additions & 23 deletions src/renderer/src/features/settings/lib/settingsRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
41 changes: 20 additions & 21 deletions src/renderer/src/workspace/tile-tree/TileLeaf/claudePaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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('')
Expand Down Expand Up @@ -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, {
Expand Down