Skip to content

feat(shell): host clipboard via OSC 52 on grain sh - #24

Merged
cxdy merged 1 commit into
mainfrom
fix/shell-osc52-clipboard
Jul 29, 2026
Merged

feat(shell): host clipboard via OSC 52 on grain sh#24
cxdy merged 1 commit into
mainfrom
fix/shell-osc52-clipboard

Conversation

@cxdy

@cxdy cxdy commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Grok Build (and similar TUIs) copy on highlight via OSC 52 when they are not on the local OS clipboard. That works on a laptop terminal, but grain sh only forwarded PTY bytes — terminals without native OSC 52 (e.g. Apple Terminal) never applied the sequence, and even capable terminals could miss host-side copy depending on the path.

This change intercepts OSC 52 on the host grain sh client (agent WebSocket shell, including remote GRAIN_API proxy) and writes the decoded payload to the local clipboard:

Host Helper
macOS pbcopy
Linux wl-copy, else xclip, else xsel

Also handles tmux DCS-wrapped OSC 52 (ESC P tmux; …). Sequences still pass through by default so iTerm2/Ghostty/Kitty keep native handling.

Env Effect
GRAIN_OSC52_CLIPBOARD=0 Disable intercept
GRAIN_OSC52_PASSTHROUGH=0 Swallow sequence after host copy

Test plan

  • go test ./internal/osc52/
  • go test ./internal/agent/
  • grain sh into a sandbox, run Grok Build, highlight text → host clipboard has it
  • Same over remote CLI (GRAIN_API=… grain sh)
  • GRAIN_OSC52_CLIPBOARD=0 grain sh → no host copy

Intercept OSC 52 (and tmux DCS-wrapped) sequences on the grain sh client
path and write the payload to the local clipboard so guest TUIs like Grok
Build can copy on highlight. Works for local agent PTY and remote
daemon-proxied shells. Disable with GRAIN_OSC52_CLIPBOARD=0.
@github-actions

Copy link
Copy Markdown

Coverage (lines): 79% ✅ — minimum 75%

commit a241fe9 · 11982/15195 lines · cmd/* excluded

Coverage report by file (79% overall — click to expand)
File Coverage Missing
All files 79%
internal/osc52/writer.go 56% 55-57 62-66 78-82 84-86 93-95 107-111 119-121 139-147 152-159 168-170 183-185 191-193 2…
internal/sshkey/sshkey.go 68% 33-35 38-41 46-48 50-52
internal/mcp/tools.go 69% 389-391 393-395 401-406 427-429 463-468 471-476 481-484 514-516 524-526 531-533 538 544…
internal/cli/cp.go 70% 80-82 88 136-139 141-143 145 169-171 179-181 188-190 200-202 205-207 209-211 240-242 25…
internal/cli/root.go 70% 96-98 100-102 179-181 188-193 204-206 211-213 217-219 227-232 235-240 247-254 256 258-2…
internal/update/update.go 71% 85-87 89-91 93-98 112-114 135-137 169-171 195 220-222 224-226 228-230 240-245 247-249 2…
internal/mcp/connect.go 71% 56-58 68-72 79-81 83-85
internal/recipe/recipe.go 71% 151-153 155-157 159-161 170-172 181-183 192-194 201-203 214 226-228 231-233 235-238 249…
internal/daemon/daemon.go 73% 29-31 59-62 90-92 100-102 107-109 118-130
internal/cli/stats.go 73% 23-25 27-29 40-42
internal/proxy/store.go 73% 34-36 61 64-66 73-75 77-79 88-90 110-112 121-123 165 168-171 177-179 181-183 192-194 19…
internal/store/store.go 73% 21-23 39-41 43-45 47-49 61 74-78 107-109
internal/image/catalog.go 74% 69-79 119-132
internal/agent/server.go 75% 92-94 96-98 104 216-218 305-311 344-346 350-353 355-358 400-409 421-433 483-486 488-491…
internal/cli/image_doctor.go 75% 34-38 53-58 62-71 74-76 102-104 150-152 157 161-163 174-176 181-187 190-192 203-206 209…
internal/manager/manager.go 75% 88-90 185-187 203-207 209-211 217-219 240-242 254-260 265-270 358-362 389-391 439-441 4…
internal/cli/progress.go 75% 43-49 51-54 63-65 103-111 113-116 131-133 169-174 201-203
internal/mcp/http.go 75% 33-35 40-42 72-74 84-88
internal/cli/fwd.go 76% 46-48 127-129 131-133 136-138 143-145 149-151 189-192 207-209 211-213 221-226 233-235 2…
internal/cli/uninstall.go 76% 33-36 63-70 72-74 79-81 88-90 110-112 116-117 140-142
internal/presets/presets.go 76% 31-33 40-42 45-46 49-50 62-64
internal/secrets/store.go 76% 58-60 84-88 101 119-121 148-150 153-159 171-173 197-203 217-219 235 247-249 259 273-275…
internal/cli/proxy.go 76% 43-45 80-82 91-93 96-99 104-105 116 129-131 133-135 141-143 155-159 164 174-176 183-185…
internal/cli/logs.go 77% 37-39 44-46 52 55-59 84-86 104-109 113 123 127-128 141-143 158-160 182 209-211 219-221
internal/agent/client.go 77% 56-58 79-81 83-85 87-90 92-94 101-103 126-128 147-149 156-158 179-181 200-202 245-247 2…
internal/cli/mcp.go 78% 38-40 50-52 56-58 69-71 81
internal/hypervisor/qemu.go 78% 33-35 50-52 66-68 72-74 79-81 88-90 95-97 155-162 168-170 174-184 186-189 196-198 202-2…
internal/cli/recipe.go 78% 37-39 62-64 66-68 86-88 90-92 97-102
internal/image/pull.go 78% 106-108 136 157-159 170-172 175-177 188-190 201-205 214-218 220-223 237-239 262-264 267…
internal/api/api.go 78% 112-115 215-219 228-251 273-275 398-401 412-415 427-429 437-439 500-503 512-514 519-522…
scripts/mcp-handshake.go 78% 16-20 42-44 60-62
internal/hypervisor/disk_local.go 79% 38-40 45-47 73-76 78-80 112-114 123
internal/mcp/host.go 79% 68-72 79-81 87-92 115
internal/proxy/server.go 79% 65-67 114-118 124-127 168-170 173-175 178-181 191-194 198-202 205-208 222-224 227-233 2…
internal/hypervisor/virtiofs.go 80% 28-30 32-34 64-66 104-106 111-114 119-121 126-129 170-172 190-192 199-201
internal/cli/fs.go 80% 60-62 87-89 91-93 127-129 131-133 146-148 150-152 179-181 183-185 192-194 214-216 218-2…
internal/cli/volume.go 82% 24-29
internal/guest/sshwait.go 83% 32-34 39-41 58
internal/hypervisor/fsdev.go 83% 34-40
internal/agent/stats.go 83% 62-64 71-73 80-82 116-118 125-127
internal/hypervisor/qmp.go 84% 50-52 62-69 93-95 97-99
internal/api/client.go 85% 66-68 74-76 97-99 105-107 154-156 169-171 190-192 211-213 228-230 245-247 262-264 279-2…
internal/hypervisor/firecracker.go 86% 142-144 164-166 171-173 208-213 229-231 238-239 242-246 249-250 253-254 257-259 268-277…
internal/agent/readiness.go 87% 27-29 33 57-59
internal/cli/act.go 87% 97-99 101-103 112-116 119-121 143-145 150-152 166-168 209-211 218-220 254-256 337-339
internal/guest/agentdeploy.go 87% 59-61 65-67 75-77
internal/cloudinit/seed.go 90% 64-69 93
internal/cli/update.go 91% 42-44 131-135 140-142 177-178
internal/config/config.go 91% 325-327 345-347 363-365 409 420-422 450-452 459-461 471-473
internal/hypervisor/transport.go 91% 69-71
internal/agent/binary.go 91% 58-59 65-66
internal/hostbin/hostbin.go 91% 28-30
internal/cli/preset.go 92% 36-38
client/client.go 93% 96-98 186-188 194-196 218-220 226-228 274-276 448-450 508-510 517-519 567-569 610-612 6…
internal/hypervisor/netdev.go 93% 43-45
scripts/handshake_lib.go 93% 54-56
internal/cli/agent_dial.go 95% 44-48 150 159
internal/cloudinit/merge.go 96% 124-126 180-182 238-240
internal/agent/dial.go 97% 82-83
internal/cli/secret.go 97% 56-58 225-227
internal/cli/profile.go 97% 28-30
internal/hypervisor/mock.go 97% 161-163

@cxdy
cxdy merged commit 6788435 into main Jul 29, 2026
3 checks passed
@cxdy
cxdy deleted the fix/shell-osc52-clipboard branch July 29, 2026 08:46
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