Skip to content

feat: support copy command - #12362

Closed
gyliu513 wants to merge 1 commit into
NousResearch:mainfrom
gyliu513:copy
Closed

gyliu513 wants to merge 1 commit into
NousResearch:mainfrom
gyliu513:copy

Conversation

@gyliu513

Copy link
Copy Markdown

What does this PR do?

Add /copy support

Related Issue

Fixes #11835

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

Here are all the changes made for this feature:

Python — Classic CLI

cli.py

  • Added _CODE_BLOCK_RE regex and _extract_code_blocks() helper (line ~119) to parse fenced code blocks from markdown text
  • Modified _write_osc52_clipboard() to try native clipboard (pbcopy/xclip/xsel) first, falling back to OSC 52 only over SSH
  • Added _copy_to_system_clipboard() method — runs pbcopy on macOS, wl-copy/xclip/xsel on Linux via subprocess
  • Rewrote _handle_copy_command() — detects code blocks and delegates to the interactive picker when present
  • Added _copy_with_picker() — builds picker labels from code blocks and calls curses_copy_picker
  • Added _copy_text_to_clipboard() — extracted clipboard copy + confirmation message
  • Added _write_copy_to_file() — prompts for file path (with smart extension default) and writes selected content

hermes_cli/curses_ui.py

  • Added curses_copy_picker() — curses-based single-select with Enter (copy) / w (write to file) / Esc (cancel)
  • Added _copy_picker_numbered_fallback() — text-based fallback for non-curses terminals

hermes_cli/commands.py

  • Updated CommandDef description for /copy to mention the interactive picker

TypeScript — TUI (Ink)

ui-tui/src/app/interfaces.ts

  • Added CopyPickerState and CopyPickerItem interfaces
  • Added copyPicker: CopyPickerState | null to OverlayState

ui-tui/src/app/overlayStore.ts

  • Added copyPicker: null to default overlay state
  • Included copyPicker in the $isBlocked computed store

ui-tui/src/app/useInputHandlers.ts

  • Added keyboard handling for the copy picker overlay (up/down/Enter/w/Esc)

ui-tui/src/components/appOverlays.tsx

  • Added copy picker rendering inside FloatingOverlays with cursor highlight
  • Updated hasAny guard to include copyPicker

ui-tui/src/app/slash/commands/core.ts

  • Enhanced /copy run handler to extract code blocks and open the picker overlay when blocks are present

ui-tui/src/lib/text.ts

  • Added CodeBlock interface and extractCodeBlocks() function

Tests

tests/cli/test_cli_copy_command.py

  • Updated existing tests to use _copy_to_system_clipboard mock instead of _write_osc52_clipboard
  • Added test_copy_falls_back_to_osc52_when_native_unavailable
  • Added test_extract_code_blocks_finds_fenced_blocks, test_extract_code_blocks_no_lang_defaults_to_text, test_extract_code_blocks_returns_empty_for_no_blocks
  • Added test_copy_opens_picker_when_code_blocks_present, test_copy_no_picker_when_no_code_blocks
  • Added test_copy_with_picker_copies_full_response, test_copy_with_picker_copies_specific_block, test_copy_with_picker_cancel, test_copy_with_picker_write_to_file
  • Added test_curses_copy_picker_fallback_non_tty
  • Added test_system_clipboard_uses_pbcopy_on_macos, test_system_clipboard_skipped_over_ssh

How to Test

● how are you
────────────────────────────────────────


╭─ ⚕ Hermes ───────────────────────────────────────────────────────────────────────────────────────────────╮
    I’m running smoothly—just a bunch of code eager to help you. How can I assist you now?
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯

⚙️  /copy
  Copied assistant response #5 to clipboard
 ⚕ gpt-oss:20b │ 15.9K/131.1K │ [█░░░░░░░░░] 12% │ 7m

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform:

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

For New Skills

  • This skill is broadly useful to most users (if bundled) — see Contributing Guide
  • SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
  • No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
  • I've tested the skill end-to-end: hermes --toolsets skills -q "Use the X skill to do Y"

Screenshots / Logs

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Apr 23, 2026
@OutThisLife
OutThisLife requested a review from Copilot April 25, 2026 19:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds /copy command support across the classic Python CLI and the Ink-based TUI, including interactive selection of fenced code blocks and improved clipboard handling.

Changes:

  • Classic CLI: implement code-block extraction, interactive picker, native clipboard copy with OSC52 fallback, and “write to file” flow.
  • TUI: add a copy-picker overlay state/UI + keyboard handling, and enhance /copy to open the picker when code blocks are detected.
  • Tests: expand /copy coverage for clipboard fallback behavior, code-block parsing, picker flows, and non-TTY fallback.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
cli.py Adds code-block extraction + picker and updates clipboard copy behavior (native-first, OSC52 fallback).
hermes_cli/curses_ui.py Introduces curses_copy_picker and a numbered fallback UI.
hermes_cli/commands.py Updates /copy help text to mention the interactive picker.
tests/cli/test_cli_copy_command.py Adds/updates tests for new /copy behavior, picker, and clipboard paths.
ui-tui/src/lib/text.ts Adds extractCodeBlocks() helper for the TUI /copy picker.
ui-tui/src/app/interfaces.ts Adds CopyPickerState / CopyPickerItem types and overlay state field.
ui-tui/src/app/overlayStore.ts Initializes copyPicker and includes it in the blocked-overlay computation.
ui-tui/src/app/slash/commands/core.ts Updates /copy to open the overlay picker when code blocks are present.
ui-tui/src/app/useInputHandlers.ts Adds navigation/selection/cancel handling for the copy picker overlay.
ui-tui/src/components/appOverlays.tsx Renders the copy picker overlay UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ui-tui/src/lib/text.ts
Comment on lines +199 to +210
const CODE_BLOCK_RE = /```(\w*)\n([\s\S]*?)```/g

export interface CodeBlock {
code: string
lang: string
}

export const extractCodeBlocks = (text: string): CodeBlock[] => {
const blocks: CodeBlock[] = []

for (const m of text.matchAll(CODE_BLOCK_RE)) {
blocks.push({ code: m[2]!.replace(/\n$/, ''), lang: m[1] || 'text' })

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractCodeBlocks is unlikely to detect many valid Markdown fenced code blocks because CODE_BLOCK_RE only allows \w* languages and only fences. This will miss common info strings like `shell-session`, `c++`, `tsconfig.json`, or fences with additional attributes/spaces, and it won’t match `~~~` fences (which the TUI markdown renderer and `estimateRows` already support). Consider reusing the existing fence parsing approach (variable-length/~~~ + trimming the full info string) so the /copy picker triggers consistently.

Copilot uses AI. Check for mistakes.
Comment thread cli.py
Comment on lines +120 to +127
r"```(\w*)\n(.*?)```", re.DOTALL
)


def _extract_code_blocks(text: str) -> list[tuple[str, str]]:
"""Return ``[(lang, code), ...]`` for every fenced code block in *text*."""
return [
(m.group(1) or "text", m.group(2).rstrip("\n"))

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fenced-code regex _CODE_BLOCK_RE only matches language tags made of \w* and requires the info string to be immediately followed by \n. This will fail to extract many real-world fenced blocks like shell-session, c++, or ```python linenos (and any fence using ~~~). Consider loosening the info-string capture (e.g., capture and trim everything up to the newline) and/or sharing the same fence parsing rules used elsewhere in the project so `/copy` reliably detects code blocks.

Suggested change
r"```(\w*)\n(.*?)```", re.DOTALL
)
def _extract_code_blocks(text: str) -> list[tuple[str, str]]:
"""Return ``[(lang, code), ...]`` for every fenced code block in *text*."""
return [
(m.group(1) or "text", m.group(2).rstrip("\n"))
r"^(?P<fence>`{3,}|~{3,})(?P<info>[^\n]*)\n(?P<code>.*?)(?:^(?P=fence)[ \t]*$)",
re.DOTALL | re.MULTILINE,
)
def _extract_code_blocks(text: str) -> list[tuple[str, str]]:
"""Return ``[(lang, code), ...]`` for every fenced code block in *text*."""
return [
(
(m.group("info").strip().split(None, 1)[0] if m.group("info").strip() else "text"),
m.group("code").rstrip("\n"),
)

Copilot uses AI. Check for mistakes.
))}

<Box marginTop={1}>
<Text color={ui.theme.color.dim}>↑↓ navigate · Enter copy · Esc cancel</Text>

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copy picker overlay advertises only "↑↓ navigate · Enter copy · Esc cancel", but useInputHandlers also implements a w key path (with different behavior/message). Either the hint should mention w, or the w handler should be removed/changed so the UI and controls stay in sync.

Suggested change
<Text color={ui.theme.color.dim}>↑↓ navigate · Enter copy · Esc cancel</Text>
<Text color={ui.theme.color.dim}>↑↓ navigate · Enter copy · w copy to file · Esc cancel</Text>

Copilot uses AI. Check for mistakes.
Comment on lines 92 to 93
mock_copy.assert_not_called()
assert any("Invalid response number" in str(call) for call in mock_print.call_args_list)

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comprehension for call in mock_print.call_args_list shadows the call symbol imported from unittest.mock (and the import is otherwise unused). This is confusing and may trip linting; consider renaming the loop variable and dropping the unused call import.

Copilot uses AI. Check for mistakes.
@alt-glitch alt-glitch mentioned this pull request May 9, 2026
1 task
@teknium1

Copy link
Copy Markdown
Collaborator

Closing as superseded by #20159.

Triage notes (high confidence):
Merged PR #20159 'fix(tui): native clipboard fallback for /copy command (salvage #15963)' (2026-05-05) implements the native pbcopy/wl-copy/xclip clipboard fallback for /copy; main cli.py:5573 has _handle_copy_command.

Thanks for the contribution — the underlying problem this PR addresses has been resolved by the linked PR on current main. If you believe this was closed in error, please comment and we'll reopen.

(Bulk-closed during a CLI PR triage sweep.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Introduce /copy for hermes

4 participants