33from __future__ import annotations
44
55import json
6+ import os
7+ import shutil
8+ import subprocess
69import sys
10+ from dataclasses import dataclass
711from datetime import datetime , timezone
812from pathlib import Path
913
@@ -217,7 +221,10 @@ def transfer_cmd(
217221 _success (f"Created { to_a .title ()} session: { new_path .stem } " )
218222 _info (f" { new_path } " )
219223 _info ("" )
220- _info (_resume_help (to_a , new_path ))
224+ hint = _resume_hint (to_a , new_path )
225+ _info (hint .text )
226+ if hint .command :
227+ _prompt_copy (hint .command )
221228
222229
223230def _emit (transcript : CanonicalTranscript , fmt : str ) -> None :
@@ -230,33 +237,109 @@ def _emit(transcript: CanonicalTranscript, fmt: str) -> None:
230237 click .echo (to_markdown (transcript ))
231238
232239
233- def _resume_help (agent : str , path : Path ) -> str :
234- """Agent-specific instructions for opening the injected session."""
240+ @dataclass (frozen = True )
241+ class ResumeHint :
242+ """Instructions for resuming an injected session, plus a copyable command.
243+
244+ ``command`` is ``None`` when there is no single command to copy (e.g. the
245+ target agent only exposes a session picker UI).
246+ """
247+
248+ text : str
249+ command : str | None = None
250+
251+
252+ def _resume_hint (agent : str , path : Path ) -> ResumeHint :
235253 session_id = path .stem
236254 if agent == "claude" :
237- return (
238- "To continue in Claude Code:\n "
239- f" claude --resume { session_id } "
240- )
255+ command = f"claude --resume { session_id } "
256+ return ResumeHint (text = f"To continue in Claude Code:\n { command } " , command = command )
241257 if agent == "codex" :
242258 try :
243259 first = path .read_text (encoding = "utf-8" ).splitlines ()[0 ]
244260 record = json .loads (first )
245261 session_id = record .get ("payload" , {}).get ("id" ) or session_id
246262 except (OSError , json .JSONDecodeError , IndexError , AttributeError ):
247263 pass
248- return (
249- "To continue in Codex:\n "
250- f" codex resume { session_id } \n "
251- "(or just run `codex` in this directory — the most recent rollout is picked up)"
264+ command = f"codex resume { session_id } "
265+ return ResumeHint (
266+ text = (
267+ "To continue in Codex:\n "
268+ f" { command } \n "
269+ "(or just run `codex` in this directory — the most recent rollout is picked up)"
270+ ),
271+ command = command ,
252272 )
253273 if agent == "opencode" :
254- return (
255- "To continue in OpenCode:\n "
256- " opencode # then open the 'Handoff from <agent>' session from the session list\n "
257- f"(session id: { session_id } )"
274+ # OpenCode has no single-command resume — user picks from a session list.
275+ return ResumeHint (
276+ text = (
277+ "To continue in OpenCode:\n "
278+ " opencode # then open the 'Handoff from <agent>' session from the session list\n "
279+ f"(session id: { session_id } )"
280+ ),
281+ command = None ,
282+ )
283+ return ResumeHint (text = "Ready to continue!" , command = None )
284+
285+
286+ def _copy_to_clipboard (text : str ) -> bool :
287+ """Copy ``text`` to the system clipboard. Returns True on success."""
288+ candidates : list [list [str ]] = []
289+ if sys .platform == "darwin" :
290+ candidates = [["pbcopy" ]]
291+ elif sys .platform == "win32" :
292+ candidates = [["clip" ]]
293+ else :
294+ if os .environ .get ("WAYLAND_DISPLAY" ):
295+ candidates .append (["wl-copy" ])
296+ candidates .extend (
297+ [["xclip" , "-selection" , "clipboard" ], ["xsel" , "--clipboard" , "--input" ]]
258298 )
259- return "Ready to continue!"
299+
300+ for cmd in candidates :
301+ if not shutil .which (cmd [0 ]):
302+ continue
303+ try :
304+ subprocess .run (cmd , input = text , text = True , check = True , timeout = 2 )
305+ return True
306+ except (subprocess .SubprocessError , OSError ):
307+ continue
308+ return False
309+
310+
311+ def _prompt_copy (command : str ) -> None :
312+ """Offer to copy ``command`` to the clipboard on keypress.
313+
314+ Only runs in interactive terminals; a no-op when stdout/stdin aren't TTYs
315+ (e.g. piped output, CI, test harnesses).
316+ """
317+ if not (sys .stdin .isatty () and sys .stdout .isatty ()):
318+ return
319+
320+ click .echo ("" )
321+ prompt = click .style (
322+ "Press (c) to copy command, any other key to continue..." , fg = "cyan" , dim = True
323+ )
324+ click .echo (prompt , nl = False )
325+ try :
326+ ch = click .getchar (echo = False )
327+ except (KeyboardInterrupt , EOFError ):
328+ click .echo ("" )
329+ return
330+
331+ # Wipe the prompt line so the final output doesn't include the prompt text.
332+ click .echo ("\r \033 [2K" , nl = False )
333+ if ch and ch .lower () == "c" :
334+ if _copy_to_clipboard (command ):
335+ click .echo (click .style ("✓ Copied to clipboard." , fg = "green" ))
336+ else :
337+ click .echo (
338+ click .style (
339+ "✗ Could not copy (no clipboard tool found — install pbcopy/xclip/wl-copy)." ,
340+ fg = "yellow" ,
341+ )
342+ )
260343
261344
262345# ---------------------------------------------------------------------------
0 commit comments