Skip to content

Skip ghost text in VS Code / Cursor integrated terminals#656

Open
FuJacob wants to merge 1 commit into
mainfrom
fix/vscode-integrated-terminal-suggestions
Open

Skip ghost text in VS Code / Cursor integrated terminals#656
FuJacob wants to merge 1 commit into
mainfrom
fix/vscode-integrated-terminal-suggestions

Conversation

@FuJacob

@FuJacob FuJacob commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

Cotabby showed ghost text in the VS Code integrated terminal (#647), overlapping shell prompts and interactive command output. The terminal, code editor, and Copilot chat all share one bundle id (com.microsoft.VSCode), so the existing app-level terminal blocklist (TerminalAppDetector.isTerminal) could only block or allow all three together. This recognizes the integrated terminal at the surface level — the focused element's AXDOMClassList (xterm.js focuses an AXTextField whose class is xterm-helper-textarea) — and suppresses suggestions there by default, while the editor and Copilot chat keep working. A new Suggest in Integrated Terminals toggle (Apps pane, off by default) lets power users opt back in. Generalizes for free to Cursor/Windsurf and browser-hosted web terminals (all xterm.js).

Validation

xcodebuild test -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' \
  -only-testing:CotabbyTests/TerminalAppDetectorTests \
  -only-testing:CotabbyTests/SuggestionAvailabilityEvaluatorTests \
  CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO
# ** TEST SUCCEEDED **  43 tests, 0 failures (7 new)

xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build
# ** BUILD SUCCEEDED **

swiftlint lint --quiet
# clean (changed files)

The detection signal was verified live against VS Code 1.123's real accessibility tree: the focused terminal input is AXTextField with AXDOMClassList = ["xterm-helper-textarea"] (flagged AXFocused=true), while the Monaco editor and Copilot chat inputs are native-edit-context inside monaco-editor — no xterm class. Not yet driven end-to-end in the running app.

Linked issues

Fixes #647

Risk / rollout notes

  • New persisted setting suggestInIntegratedTerminals (UserDefaults key cotabbySuggestInIntegratedTerminals, default false). Existing installs get the default, i.e. the terminal is suppressed — consistent with how standalone terminal apps are already skipped. Threaded through SuggestionSettingsData / SuggestionSettingsStore / the snapshot publisher.
  • New FocusedInputSnapshot.isIntegratedTerminal, resolved from one extra AXDOMClassList read on the focused element. The read is placed after a real editable field has resolved, so idle / non-editable focus polls pay nothing; native apps don't vend the attribute.
  • Behavior change: users who currently see suggestions in the VS Code / Cursor integrated terminal will stop seeing them by default. The editor and Copilot chat in the same window are unaffected, and the existing per-app rule still disables all of VS Code if wanted.
  • No pbxproj / schema migration.

Greptile Summary

Introduces surface-level detection of xterm.js integrated terminals (VS Code, Cursor, Windsurf) by reading AXDOMClassList from the focused AX element, solving the problem that the terminal, editor, and Copilot chat all share one bundle ID and cannot be separated with the existing app-level blocklist. A new suggestInIntegratedTerminals UserDefaults flag (default false) threads through the entire settings stack and availability evaluator, suppressing ghost text in the terminal while leaving the code editor and Copilot chat unaffected.

  • Adds FocusedInputSnapshot.isIntegratedTerminal (resolved once per real-editable-field poll via AXHelper.stringArrayValue) and TerminalAppDetector.isIntegratedTerminal(domClassList:) which matches any class with an xterm prefix.
  • Plumbs the new suggestInIntegratedTerminals setting through SuggestionSettingsDataSuggestionSettingsModelSuggestionSettingsSnapshot → all five SuggestionAvailabilityEvaluator call sites, with a CombineLatest pair nested inside the existing CombineLatest4 to stay within Combine's publisher-count limit.
  • Adds a toggle in the Apps settings pane and ships 7 new unit tests covering both the detector and the evaluator gating logic.

Confidence Score: 4/5

Safe to merge; the change adds a new off-by-default suppression path that is well-isolated and does not alter existing terminal or editor behavior.

The detection heuristic (hasPrefix("xterm")) is intentionally broad for forward-compatibility, but it would also silently suppress ghost text in any VS Code extension panel that happens to use xterm.js for non-terminal output display. This is a known tradeoff documented in comments, not an oversight, but it creates a hidden interaction for future extension authors or power users running xterm.js-based log viewers inside VS Code.

Cotabby/Support/TerminalAppDetector.swift — the prefix-match strategy is the only area worth revisiting if users report suggestions missing in non-terminal xterm.js panels.

Important Files Changed

Filename Overview
Cotabby/Support/TerminalAppDetector.swift Adds isIntegratedTerminal(domClassList:) using a broad xterm-prefix match; intentional but could over-suppress in non-terminal xterm.js VS Code extension panels.
Cotabby/Services/Focus/FocusSnapshotResolver.swift Reads AXDOMClassList from the focused element only after a real editable field resolves, keeping the idle-poll fast path free of extra AX round-trips; correctly populates isIntegratedTerminal on the snapshot.
Cotabby/Support/SuggestionAvailabilityEvaluator.swift Integrated-terminal gate is inserted at the right place in the check chain (after app-level terminal blocklist, before permission guards); default false correctly suppresses terminals out of the box.
Cotabby/Models/SuggestionSettingsModel.swift Correctly nests CombineLatest($extendedContext, $suggestInIntegratedTerminals) as the third slot of the outer CombineLatest4 to stay within Combine's 4-publisher cap; snapshot publisher correctly destructures and forwards the new flag.
Cotabby/Support/AXHelper.swift Adds stringArrayValue(for:on:) — thin cast of copyAttributeValue result to [String]?; Swift correctly bridges Chromium's CFArray/NSArray of CFStrings via the conditional cast.
Cotabby/Models/FocusModels.swift Adds isIntegratedTerminal: Bool with a default of false; keeps all existing call sites source-compatible.
Cotabby/Support/SuggestionSettingsStore.swift Persists the new flag under cotabbySuggestInIntegratedTerminals with a false default (via ?? false on missing key); correctly included in saveAll.
Cotabby/UI/Settings/Panes/AppsPaneView.swift Adds the Integrated Terminals section and toggle in the Apps pane with a clear description; binding correctly routes through setSuggestInIntegratedTerminals.
CotabbyTests/TerminalAppDetectorTests.swift Good coverage: verified xterm-helper-textarea, prefix-match sibling, Monaco/non-xterm classes, and empty list; all live-verified class names used.
CotabbyTests/SuggestionAvailabilityEvaluatorTests.swift Three focused tests cover: suppressed-by-default, allowed-when-opted-in, and shouldSchedulePrediction both with and without opt-in.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Focus poll fires] --> B{Real editable\nfield resolved?}
    B -- No --> C[Return nil snapshot]
    B -- Yes --> D[Read AXDOMClassList\nfrom focusedElement]
    D --> E[TerminalAppDetector\n.isIntegratedTerminal]
    E --> F[FocusedInputSnapshot\nisIntegratedTerminal set]
    F --> G[SuggestionAvailabilityEvaluator\n.disabledReason]
    G --> H{isTerminal\nbundle ID?}
    H -- Yes --> I[Block: terminal app]
    H -- No --> J{isIntegratedTerminal\n&& !suggestInTerminals?}
    J -- Yes --> K[Block: integrated terminal]
    J -- No --> L{inputMonitoring\ngranted?}
    L -- No --> M[Block: permission]
    L -- Yes --> N[Suggest ✓]
Loading

Comments Outside Diff (1)

  1. Cotabby/Support/TerminalAppDetector.swift, line 383-384 (link)

    P2 xterm prefix may suppress suggestions in non-terminal xterm.js panels

    hasPrefix("xterm") will also match any VS Code extension or Electron app that renders output/log viewer panels using xterm.js (e.g., xterm-viewer, xterm-output). If a user has focus inside such a panel's editable field, suggestions will be suppressed even though it isn't a shell terminal. The current design is intentional (per the doc-comment: "keeps detection working if xterm renames its input node"), but the tradeoff means any future xterm.js-powered non-terminal component inside VS Code silently loses ghost text.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Codex Fix in Claude Code

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "Skip ghost text in VS Code / Cursor inte..." | Re-trigger Greptile

The integrated terminal, code editor, and Copilot chat all share one bundle id
(com.microsoft.VSCode), so the app-level TerminalAppDetector blocklist can only
block or allow all three together. Detect the terminal at the surface level
instead: xterm.js focuses an AXTextField whose AXDOMClassList contains
`xterm-helper-textarea`, which the Monaco editor and chat (native-edit-context)
never carry. Verified live against VS Code's accessibility tree.

FocusSnapshotResolver reads AXDOMClassList on the resolved focused element and
records FocusedInputSnapshot.isIntegratedTerminal; SuggestionAvailabilityEvaluator
suppresses suggestions and visual-context capture there unless the new
suggestInIntegratedTerminals setting is on (default off; toggle in the Apps pane).
Editor and Copilot chat keep working; standalone terminal apps are unchanged.
Generalizes to Cursor/Windsurf and browser-hosted web terminals (all xterm.js).
Sign up for free to 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] the app keep showing suggestion in vs code terminal even when no command typed

1 participant