Skip to content

macOS: editing keys for Claude Code cards (newline, select-all, copy/cut/paste) and UTF-8 locale - #145

Closed
mrcertis wants to merge 4 commits into
howdeploy:mainfrom
mrcertis:claude/macos-claude-draft-keys
Closed

mrcertis wants to merge 4 commits into
howdeploy:mainfrom
mrcertis:claude/macos-claude-draft-keys

Conversation

@mrcertis

Copy link
Copy Markdown
Contributor

Summary

Claude Code cards on macOS get the editing keys people expect from a text field:

  • Enter, Shift+Enter, Command+Enter reach Claude as distinct sequences (\r, CSI 13;2u, CSI 13;9u), as they already do for Codex. What each one does stays Claude's choice: with ~/.claude/keybindings.json mapping enter/shift+enter to chat:newline and cmd+enter to chat:submit, Enter writes a new line and Command+Enter sends.
  • Command+A selects the whole prompt draft (a highlight layer over the prompt rows). With it selected, Command+C copies (the selection stays), Command+X cuts, typing or Command+V replaces, Backspace/Delete clears; any other key or a click drops the selection. Without it, Command+A keeps xterm's behaviour.
  • UTF-8 locale: a macOS launch with no locale gets LANG set to the system's UTF-8 locale (en_US.UTF-8 when the system lacks it), so pbcopy, vi and friends stop turning Cyrillic into mojibake.

How the draft actions work

Claude Code has no select-all, so CanvasTTY acts on the draft through Claude's own external-editor round trip (Ctrl+G) with a bundled $VISUAL helper (src/agent-runtime/draft-editor.sh):

  • main writes a request file req.<n> into the card's folder (<userData>/claude-draft/<session>), sends ^G, and holds the card's input meanwhile, so keys typed during the round trip arrive after it;
  • the helper claims the request by an atomic rename, saves the draft for the clipboard, and empties it for cut/clear; a paste then reads the clipboard in its turn;
  • a request Claude does not pick up in time is cancelled by the same rename, and a late ^G uses the cancel up: it never opens an editor;
  • with no request, Ctrl+G is an ordinary Ctrl+G and the person's own VISUAL/EDITOR opens (arguments kept);
  • the helper only acts on Claude's own prompt file (claude-prompt-*.md): git commit or crontab -e run by Claude's tools get the person's editor.

Only local macOS Claude launches get the helper. A plugin or a remote environment that sets its own editor (in the environment or in its --settings env) keeps it.

Claude Code 2.1 background sessions

From Claude Code 2.1, /bg, /resume inside a session and the agent view move a conversation into a background session that runs under Claude's own supervisor (claude daemon run), not in the card's process. Such a session gets the launch's --settings but not its environment. So the helper variables and the locale are also passed through --settings env, merged with CanvasTTY's other inline settings. Verified live: they reach a background session. A background session may be shown by another card than the one that started it, so the helper also answers a request in another card's folder after its own. A cancel is only ever the card's own.

Agent isolation

The draft root is private app data, hidden from isolated launches. Each Claude card may write and read only its own draft folder (ownWritable, derived from the session, not from the environment). A real-seatbelt test runs the helper inside the layer.

Notes for maintainers

  • Agent view and per-card state (outside this PR). The shared Claude supervisor keeps the environment of whichever card started it, including CANVASTTY_TERMINAL_SESSION_ID and the browser/runtime gateway variables. So background sessions shown in other cards talk to the starting card's gateways, which may be gone. Whether background sessions stay inside agent isolation depends on which launch started the supervisor; not verified here. Possible directions: per-card values through --settings (secrets not in argv), or CLAUDE_CODE_DISABLE_AGENT_VIEW=1 for isolated cards.
  • Command+Enter always sends CSI 13;9u on macOS Claude cards. Without a cmd+enter binding in Claude's keybindings, what Claude does with it is Claude's default.

Test plan

  • npm test: 2155 pass; the one failure, "real CLIs start inside the layer" (opencode JSON Parse error), fails the same way on clean main
  • npm run typecheck
  • New tests: the real helper through copy/cut/clear/paste, late and never-handled Ctrl+G, request/cancel races, prompt-file filter, cross-card requests, own-only cancels; TerminalManager --settings env, plugin editor and locale; seatbelt with the real helper
  • Built locally as a signed macOS arm64 app (1.8.1 + this branch); the draft keys were used day to day in an earlier local build

🤖 Generated with Claude Code

mrcertis and others added 4 commits October 13, 2026 16:27
- Enter, Shift+Enter and Command+Enter reach Claude Code as distinct
  sequences, as they already do for Codex.
- Command+A selects the whole Claude draft (shown as a layer over the
  prompt rows); Command+C/X copy/cut it, typing, paste or Backspace
  replace or clear it. Actions run through Claude's Ctrl+G external
  editor round trip with a bundled $VISUAL helper; input is held while
  it runs and a Ctrl+G picked up late is cancelled, never opening an
  editor.
- macOS launches without any locale get LC_CTYPE=UTF-8 so pbcopy and
  friends stop turning Cyrillic into mojibake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Requests are files req.<n> claimed or cancelled by an atomic rename, so a
  Ctrl+G Claude never handles no longer blocks later actions and a request
  can't be both acted on and cancelled.
- A session's actions run one after another; input typed meanwhile keeps
  its place, and held input still settles answered prompts.
- Only a Claude launched with the helper (SessionMetadata.claudeDraft) gets
  the whole-draft keys; other Claude cards keep xterm's own Command+A.
- Command+A selects only a prompt visible on screen; a dialog in its place
  or a restarted Claude drops the selection.
- The helper keeps the draft when it cannot save a copy and ignores unknown
  requests; the helper path may not contain shell syntax.
- A localeless macOS launch gets LANG set to the system's UTF-8 locale
  (en_US.UTF-8 when the system lacks it), a name ssh can pass on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pass the draft helper and the locale through Claude's --settings env as
  well: a background session (/bg, /resume, agent view) runs under Claude's
  supervisor and gets this launch's --settings, not its environment.
- The helper answers a request in another card's folder after its own, so a
  background session shown by another card still works; it acts only on
  Claude's own prompt file (claude-prompt-*.md), never on a file a program
  Claude runs opens (git commit, crontab -e).
- A cancel the helper cannot remove is not used up: that Ctrl+G opens the
  editor.
- Agent isolation: a Claude card's own draft folder is writable and readable
  again; the draft root is private app data, hidden from other launches.
- Command+V over a selected draft reads the clipboard when its turn comes,
  so keys pressed meanwhile stay after the paste.
- Every launch resets the card's draft queue; leftovers of an earlier run are
  removed at startup; Command+A over a dialog drops an earlier selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tor and locale

- Inside agent isolation the shared draft root is hidden, so the helper no
  longer requires it: it looks in its own card's folder, then in whichever
  other card folders it can see. The seatbelt test now runs the real helper.
- A cancel is used up only from the card's own folder: another card's
  timed-out action never swallows an ordinary Ctrl+G here.
- A plugin that sets VISUAL or EDITOR in its own --settings env keeps Ctrl+G
  (no helper), and a locale a plugin sets is not replaced in the --settings
  env that background sessions get.
- A request that cannot be claimed but is still there (also a dangling
  link) stops the helper instead of looping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mrcertis

Copy link
Copy Markdown
Contributor Author

Superseded by #146: the complete, tested version of this work (this branch was an early state with bugs we have since fixed).

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.

1 participant