# Paste and focus reference

> PasteService clipboard session tagging, Cmd+V synthesis, timing constants, FocusedEditable AX budget walk, Chromium enableWebAccessibility prewarm, heldInHistory when no editable field, and TargetAppSnapshot paste-target resolution chain.

- Repository: cartesia-ai/InkIt
- GitHub: https://github.com/cartesia-ai/InkIt
- Human docs: https://grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b
- Complete Markdown: https://grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b/llms-full.txt

## Source Files

- `InkIt/PasteService.swift`
- `InkIt/ContextSnapshot.swift`
- `InkIt/AppCoordinator.swift`
- `InkIt/AXTreeDumper.swift`
- `InkItTests/AXBudgetTests.swift`

---

---
title: "Paste and focus reference"
description: "PasteService clipboard session tagging, Cmd+V synthesis, timing constants, FocusedEditable AX budget walk, Chromium enableWebAccessibility prewarm, heldInHistory when no editable field, and TargetAppSnapshot paste-target resolution chain."
---

InkIt inserts dictated text by swapping `NSPasteboard.general`, synthesizing **Cmd+V** through a HID event tap, and restoring the user's prior clipboard off the critical path. Paste only runs after `FocusedEditable.current()` confirms an editable control is focused at hotkey release; otherwise the transcript is saved to History and the UI briefly enters `heldInHistory`.

## End-to-end paste path

At **hotkey press**, `AppCoordinator.startDictation()` resolves a press-time paste target, snapshots it as `TargetAppSnapshot`, and prewarms Chromium accessibility on that PID. At **hotkey release**, STT finalizes, optional polish runs, then `FocusedEditable` re-probes the live focus. If editable, `PasteService.paste(text:targetApp:completion:)` runs; if not, `TranscriptHistoryStore.add` persists the text and `showHeldInHistoryNotice()` surfaces a 2.5s confirmation.

```mermaid
sequenceDiagram
    participant User
    participant AppCoordinator
    participant FocusedEditable
    participant PasteService
    participant TargetApp

    User->>AppCoordinator: hotkey press
    AppCoordinator->>AppCoordinator: resolve pasteTargetApp + TargetAppSnapshot
    AppCoordinator->>FocusedEditable: enableWebAccessibility(pid)
    User->>AppCoordinator: hotkey release
    AppCoordinator->>AppCoordinator: STT finalize + optional polish
    AppCoordinator->>FocusedEditable: current() [AX budget 1.5s]
    alt editable focus
        AppCoordinator->>PasteService: paste(text, focus.app ?? capturedTarget)
        PasteService->>TargetApp: clipboard swap + Cmd+V
        PasteService-->>AppCoordinator: completion(true)
        AppCoordinator->>AppCoordinator: history.add, state → idle
    else no editable focus
        AppCoordinator->>AppCoordinator: history.add (pasteMs: 0)
        AppCoordinator->>AppCoordinator: heldInHistory (2.5s)
    end
```

<Info>
Press-time target resolution is a hint for activation and logging. Release-time `FocusedEditable` is authoritative for whether paste happens and which app receives Cmd+V.
</Info>

## TargetAppSnapshot and paste-target resolution

`TargetAppSnapshot` captures bundle ID, localized name, and PID at press. It is used for debug logging and polish context tags; InkIt does not read the target app's Accessibility tree for content.

<ResponseField name="TargetAppSnapshot" type="struct">
  <ResponseField name="bundleIdentifier" type="String?" />
  <ResponseField name="localizedName" type="String" />
  <ResponseField name="processIdentifier" type="pid_t" />
</ResponseField>

### Press-time resolution chain

`AppCoordinator` resolves `pasteTargetApp` in `startDictation()`:

| Step | Condition | Result |
|------|-----------|--------|
| 1 | `NSWorkspace.shared.frontmostApplication` is not InkIt | Use frontmost app |
| 2 | InkIt is frontmost | Fall back to `lastExternalApp` |
| 3 | `lastExternalApp` still nil (no prior activation event) | `seedLastExternalApp()` picks first regular non-InkIt running app |

`lastExternalApp` updates on `NSWorkspace.didActivateApplicationNotification`, excluding InkIt's own bundle ID.

`contextTargetSnapshot = TargetAppSnapshot.capture(from: pasteTargetApp)` runs immediately after resolution. Both `pasteTargetApp` and `contextTargetSnapshot` are **captured into the `onClosed` closure** at press time so mid-flight errors or stale callbacks cannot wipe the recording-start target.

### Release-time verified target

After polish, `FocusedEditable.current()` returns `Result(isEditable:app:)`. On paste:

```swift
paste.paste(text: correction.text, targetApp: focus.app ?? capturedTargetApp)
```

The release-verified `focus.app` wins; `capturedTargetApp` is the fallback when the AX probe returns a PID but no `NSRunningApplication`.

## PasteService

`PasteService` is the sole text-insertion primitive. It does not use AX value injection — only clipboard swap plus synthetic paste.

### Clipboard session tagging

Before Cmd+V, `PasteService`:

1. Snapshots existing `NSPasteboardItem` data (preserving non-string types).
2. Clears the pasteboard and writes the dictated string.
3. Tags the write with a per-paste UUID on `com.cartesia.InkIt.PasteSession`.
4. Marks the entry transient via `org.nspasteboard.TransientType` so clipboard-history apps skip dictated text.

Restore runs **0.4s after Cmd+V** (off the critical path). It proceeds only when both the string content and session UUID still match — if the user or another app wrote to the clipboard in between, InkIt leaves their data intact.

### Cmd+V synthesis

`synthesizeCmdV()` builds paired key-down/key-up `CGEvent`s for `kVK_ANSI_V` with `.maskCommand`, sourced from `.hidSystemState`, and posts to `.cghidEventTap`. Accessibility permission is required for this path (dictation already gates on AX at start).

### Timing constants

| Constant | Value | When paid |
|----------|-------|-----------|
| `clipboardSettleDelay` | 80 ms | Always, before Cmd+V |
| `activationFocusDelay` | 120 ms | Only when target is not already frontmost |
| `clipboardRestoreDelay` | 400 ms | After Cmd+V, before restore |

**Pre-delay** before Cmd+V:

- Target already frontmost (or `targetApp == nil`): `0.08s`
- Target needs `activate()`: `0.08s + 0.12s = 0.20s`

`activate()` is skipped when the target is already active or terminated. InkIt's recorder is a non-activating panel, so the common case pays only the 80 ms settle delay.

`completion(true)` fires immediately after Cmd+V — that marks the end of perceived paste latency. Clipboard restore does not block the user-visible result.

<ParamField body="paste(text:targetApp:completion:)" type="method">
  <ParamField body="text" type="String" required>
    Final transcript (polished or raw) to insert.
  </ParamField>
  <ParamField body="targetApp" type="NSRunningApplication?" required={false}>
    App to activate before paste. `nil` means paste into whatever is frontmost with no activation wait.
  </ParamField>
  <ParamField body="completion" type="(Bool) -> Void" required>
    Called on main after Cmd+V. `false` only when `setString` fails; restore failures are silent.
  </ParamField>
</ParamField>

## FocusedEditable and AX budget

`FocusedEditable` answers one question at release: **is an editable control focused right now?** Press-time target resolution can be stale if the user clicked a non-editable surface or focus moved during dictation.

### AX.run primitive

All Accessibility walks route through `AX.run(budget:_:)`, which:

- Spawns a detached `Task` at `.userInitiated` priority (never main thread).
- Passes a `deadline` exactly `budget` seconds from invocation.
- Returns whatever the closure produced when the deadline expires.

`FocusedEditable.current()` uses a **1.5 s** budget. The closure must check `deadline` inside its own loops; `Task` cancellation cannot preempt synchronous AX IPC already in flight.

`AXBudgetTests` locks this contract: off-main execution, deadline placement, and partial results when time expires.

### Editability probe resolution chain

Inside `resolve(deadline:)`:

| Stage | Mechanism | Purpose |
|-------|-----------|---------|
| Fast path | System-wide `kAXFocusedUIElementAttribute` → `isEditable` | Native fields, most Electron inputs |
| App descent | `AXUIElementCreateApplication(pid)` → `kAXFocusedUIElementAttribute` chain (`maxHops: 6`) | Chromium stops at web-area container |
| Subtree scan | Focused window BFS, `maxNodes: 1500`, match `kAXFocused` + editable | Slack Lexical-style composers |
| Container descent | Descend from system-wide focused element | Last resort |

`isEditable` primary signal: `kAXValueAttribute` is settable. Fallback roles: `kAXTextFieldRole`, `kAXTextAreaRole`, `kAXComboBoxRole`.

If `AXIsProcessTrusted()` is false at release (revoked mid-session edge), `current()` returns `isEditable: false, app: nil` — transcript goes to History rather than gambling Cmd+V.

<Warning>
Synchronous AX on the main thread stalls the UI and the Fn event tap, which can freeze modifier keys system-wide. Do not hand-roll AX walks outside `AX.run`.
</Warning>

## Chromium enableWebAccessibility prewarm

Chromium and Electron apps build their accessible tree lazily. Until marked accessibility-active, the system-wide focus query often stops at a web-area shell and the real textbox is invisible.

At **hotkey press**, when `pasteTargetApp` has a PID:

```swift
FocusedEditable.enableWebAccessibility(pid: targetPid)
```

This sets on the application element (off main thread, idempotent):

- `AXManualAccessibility` → `true`
- `AXEnhancedUserInterface` → `true`

Native apps ignore these harmlessly. At **release**, `resolve` re-asserts the same attributes on the app element for the focus-moved-mid-dictation case.

Prewarm at press overlaps tree construction with speaking time so the release-time probe is less likely to false-negative into `heldInHistory`.

## heldInHistory when no editable field

When `focus.isEditable` is false after polish:

1. `TranscriptHistoryStore.add` persists the text with `pasteMs: 0` in latency.
2. `showHeldInHistoryNotice()` sets `state = .heldInHistory`.
3. After **2.5 s**, state returns to `.idle` if still in `heldInHistory`.

HUD and menu bar feedback:

| Surface | Signal |
|---------|--------|
| Notch HUD | Notice pill: "Saved to History" |
| Menu bar label | `⬇ Ink` |
| Menu bar icon | `tray.and.arrow.down` |
| `statusText` | "Saved to History" |

The transcript is copyable from History. Pressing the hotkey again from `heldInHistory` starts a new dictation (same as from `idle` or `error`).

<Note>
`heldInHistory` is not an error state. It is a deliberate hold when Cmd+V would land in the wrong app or nowhere.
</Note>

## Latency accounting

`TranscriptHistoryStore.Latency` records per-stage milliseconds from hotkey release:

| Field | heldInHistory | Successful paste |
|-------|---------------|------------------|
| `transcribeMs` | release → final transcript | same |
| `polishMs` | transcript → polish finish | same |
| `pasteMs` | `0` | polish finish → paste completion |

`totalMs` (user-facing) is `transcribeMs + polishMs` only; `pasteMs` is diagnostic.

## Debug tooling

`AXTreeDumper` (Settings Diagnostics) walks the resolved target app's AX tree to `~/Library/Logs/InkIt-debug.log`. It uses separate limits (`nodeLimit: 5000`, `wallClockLimit: 5.0s`) from the paste-time `FocusedEditable` probe and is not on the dictation hot path.

## Failure modes

| Symptom | Cause | Behavior |
|---------|-------|------------|
| Text in History, not at cursor | No editable focus at release | `heldInHistory`, copy from History |
| "Paste failed" notch error | `setString` returned false | `setError`, no history row on that path |
| Paste into wrong app | Would occur without release probe | Prevented by `FocusedEditable` gate |
| Chromium app always held | Tree not built before probe | Press-time `enableWebAccessibility` prewarm |
| Slow AX tree | Deep DOM | Budget-bounded walk; 1.5s generous for real apps |
| Clipboard not restored | User copied during 400ms window | Session UUID mismatch; user's clipboard kept |

## Related pages

<CardGroup>
  <Card title="Dictation pipeline" href="/dictation-pipeline">
    End-to-end flow from hotkey through STT, polish, focus check, paste, and history persistence.
  </Card>
  <Card title="Dictation state machine" href="/dictation-state-machine">
    `DictationState` lifecycle including `pasting`, `heldInHistory`, and transition triggers.
  </Card>
  <Card title="Permissions model" href="/permissions-model">
    Accessibility requirement for Cmd+V synthesis and AX focus probes.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    First dictation verification and recovery when no editable field is focused.
  </Card>
  <Card title="Runtime troubleshooting" href="/runtime-troubleshooting">
    Duplicate bundle instances, debug logging, and non-API paste/focus issues.
  </Card>
  <Card title="Testing and CI" href="/testing-and-ci">
    `AXBudgetTests` regression contracts for the shared AX traversal primitive.
  </Card>
</CardGroup>
