make the copilot-panel interactive - #463
Conversation
Previously: calling copilot-panel-complete simply generated a buffer with a formatted list of suggested solutions. However, there was no built-in way to interact with those solutions - usere had to manually select and copy them. This update: interactive commands and local keybindings to easily extract, copy, and insert these suggested code blocks directly from the panel. Local binding in the copilot-panel: - C-c C-c: copilot-insert-suggestion-at-point. - C-c C-y: copilot-copy-function-at-point. - C-c C-s: copilot-select-and-copy-solution. - C-c C-g: copilot-kill-panel-buffer. A quick helper to cleanly kill the panel buffer.
bbatsov
left a comment
There was a problem hiding this comment.
Thanks for contributing! The idea of making the panel interactive is good, but there are several issues to address before this can be merged. See inline comments.
| (string :tag "Specific Version")) | ||
| (string :tag "Specific Version")) | ||
| :group 'copilot | ||
| :package-version '(copilot . "0.1")) |
There was a problem hiding this comment.
Unrelated whitespace change — please drop this hunk (and the two similar ones for copilot-completion-model and copilot--async-request).
There was a problem hiding this comment.
formatting was done automatically in my Emacs (apheleia). I 've rolling back the formatting changes.
| (org-mode) | ||
| (erase-buffer))) | ||
| (erase-buffer) | ||
| (copilot--setup-panel-keybindings))) |
There was a problem hiding this comment.
local-set-key mutates the shared org-mode keymap, which will leak these bindings into all org buffers. Define a minor mode with its own keymap instead (or a derived mode like copilot-chat-mode does).
|
|
||
| (defun copilot--setup-panel-keybindings () | ||
| "Setup keybindings for the copilot panel buffer." | ||
| (local-set-key (kbd "C-c C-c") 'copilot-insert-suggestion-at-point) |
There was a problem hiding this comment.
These should use the copilot-panel- prefix to match the existing copilot-panel-complete convention. E.g. copilot-panel-insert-suggestion, copilot-panel-copy-suggestion, etc.
There was a problem hiding this comment.
done. I've updated the PR description as well
| "Insert the suggesetion at point from the copilot panel to the original buffer." | ||
| (interactive) | ||
| (let ((solution-text (copilot--get-current-solution-text))) | ||
| (when solution-text |
There was a problem hiding this comment.
Typo: "suggesetion" → "suggestion"
| (other-window 1) ; Switch to previous window (original buffer) | ||
| (yank) ; Insert the solution | ||
| (message "Solution inserted and copied to clipboard")))) | ||
|
|
There was a problem hiding this comment.
Using (other-window 1) is fragile — it assumes the source buffer is in the adjacent window. Track the source buffer explicitly (similar to how copilot-chat--source-buffer works) and switch to it by name.
| (let ((heading-start (match-beginning 0)) | ||
| (solution-text (copilot--get-current-solution-text-at-pos (point)))) | ||
| (when solution-text | ||
| (push (list :title (format "Solution at line %d" (line-number-at-pos heading-start)) |
There was a problem hiding this comment.
copilot--get-current-solution-text and copilot--get-current-solution-text-at-pos are nearly identical. The former should just call the latter:
(defun copilot--get-current-solution-text ()
(copilot--get-current-solution-text-at-pos (point)))Also: the start binding on the first line of the let is immediately overwritten inside the when block, and end is set but never used ((line-beginning-position) is used instead). Please clean up the unused variables.
Signed-off-by: Robert Zaremba <robert@zaremba.ch>
Created copilot-panel-mode minor mode with its own keymap (copilot-panel-mode-map) instead of using local-set-ke Renamed functions with copilot-panel- prefix: - copilot-insert-suggestion-at-point → copilot-panel-insert-suggestion - copilot-copy-suggestion-at-point → copilot-panel-copy-suggestion - copilot-select-and-copy-suggestion → copilot-panel-select-and-copy-suggestion - copilot-kill-panel-buffer → copilot-panel-kill-buffer Added copilot-panel--source-buffer buffer-local variable to track the source buffer explicitly. Updated copilot- Refactored copilot--get-current-solution-text to simply call copilot--get-current-solution-text-at-pos with (point). Cleaned up unused variables (start and end) in copilot--get-current-solution-text-at-pos.
- Update suggestion selection list to add a sequence number to the headers
There was a problem hiding this comment.
Pull request overview
This PR makes the *copilot-panel* output actionable by adding interactive commands and keybindings intended to let users copy/insert panel suggestions back into the originating buffer.
Changes:
- Add panel-oriented interactive commands (select/copy/insert/kill) and attempt to add a dedicated panel keymap.
- Adjust panel notification handling to support sorting/renaming solutions and to allow buffer modifications when needed.
- Document the new commands and suggested keybindings in README and CHANGELOG.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
copilot.el |
Adds panel interaction commands, keymap scaffolding, and updates panel notification handling/sorting logic. |
README.md |
Updates configuration examples and command list to include the new panel commands/bindings. |
CHANGELOG.md |
Notes the new interactive panel feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| (erase-buffer)) | ||
| (org-mode) ; Keep org-mode as the true major mode | ||
| (copilot-panel-mode 1) ; Activate our custom keybindings safely | ||
| (setq copilot-panel--source-buffer source-buffer)))) |
There was a problem hiding this comment.
copilot-panel-mode is enabled in copilot-panel-complete, but there is no define-minor-mode (or other definition) for copilot-panel-mode in this file. Calling (copilot-panel-mode 1) will signal a void-function error and the panel keybindings won't work. Define copilot-panel-mode as a minor mode (with :keymap copilot-panel-mode-map) or stop calling it here.
| ;; Safely clear old handlers from memory to prevent duplicate firing / timer errors | ||
| (setq copilot--notification-handlers (make-hash-table :test 'equal)) | ||
|
|
||
| (defvar copilot--notification-handlers (make-hash-table :test 'equal) | ||
| "Hash table storing lists of notification handlers.") | ||
|
|
There was a problem hiding this comment.
This unconditional (setq copilot--notification-handlers ...) happens before the defvar, which will trigger "assignment to free variable" warnings when byte-compiling. If you need to clear handlers on reload, prefer defining the var first and using (clrhash copilot--notification-handlers) (or reinitializing it) in a dedicated init/reset function.
| ;; Safely clear old handlers from memory to prevent duplicate firing / timer errors | |
| (setq copilot--notification-handlers (make-hash-table :test 'equal)) | |
| (defvar copilot--notification-handlers (make-hash-table :test 'equal) | |
| "Hash table storing lists of notification handlers.") | |
| (defvar copilot--notification-handlers (make-hash-table :test 'equal) | |
| "Hash table storing lists of notification handlers.") | |
| ;; Safely clear old handlers from memory to prevent duplicate firing / timer errors | |
| (when (hash-table-p copilot--notification-handlers) | |
| (clrhash copilot--notification-handlers)) |
| (message "Solution copied to clipboard")))) | ||
|
|
||
| (defun copilot-panel-insert-suggestion () | ||
| "Insert the suggesetion at point from the copilot panel to the original buffer." |
There was a problem hiding this comment.
Docstring typo: "suggesetion" should be "suggestion".
| "Insert the suggesetion at point from the copilot panel to the original buffer." | |
| "Insert the suggestion at point from the copilot panel to the original buffer." |
There was a problem hiding this comment.
Thanks for the review @bbatsov . I've updated the code to address all suggestions. Moreover:
- Renamed
copilot-panel-select-and-copy-suggestiontocopilot-panel-select-suggestion: it will insert selected suggestion directly, rather than copying it. It will also launch copilot-panel if it's missing. NOTE: user must run copilot-panel-select-suggestion again once the suggestions are ready. - Updated readme to suggest 2 keybindings to launch copilot-panel.
| (string :tag "Specific Version")) | ||
| (string :tag "Specific Version")) | ||
| :group 'copilot | ||
| :package-version '(copilot . "0.1")) |
There was a problem hiding this comment.
formatting was done automatically in my Emacs (apheleia). I 've rolling back the formatting changes.
| (org-mode) | ||
| (erase-buffer))) | ||
| (erase-buffer) | ||
| (copilot--setup-panel-keybindings))) |
|
|
||
| (defun copilot--setup-panel-keybindings () | ||
| "Setup keybindings for the copilot panel buffer." | ||
| (local-set-key (kbd "C-c C-c") 'copilot-insert-suggestion-at-point) |
There was a problem hiding this comment.
done. I've updated the PR description as well
| "Insert the suggesetion at point from the copilot panel to the original buffer." | ||
| (interactive) | ||
| (let ((solution-text (copilot--get-current-solution-text))) | ||
| (when solution-text |
| (other-window 1) ; Switch to previous window (original buffer) | ||
| (yank) ; Insert the solution | ||
| (message "Solution inserted and copied to clipboard")))) | ||
|
|
| (let ((heading-start (match-beginning 0)) | ||
| (solution-text (copilot--get-current-solution-text-at-pos (point)))) | ||
| (when solution-text | ||
| (push (list :title (format "Solution at line %d" (line-number-at-pos heading-start)) |
Signed-off-by: Robert Zaremba <robert@zaremba.ch>
bbatsov
left a comment
There was a problem hiding this comment.
Thanks for the updates. The core feature is useful but there are still several issues.
Blocker: copilot-panel-mode is called at line 1193 but never defined with define-minor-mode — the panel keybindings won't work at all (void-function error at runtime).
Noise: The diff still has large hunks of pure re-indentation (notification handlers, copilot-login, copilot--show-completion, window/showDocument, $/progress) that are unrelated to the panel feature. Please revert those so the actual changes are reviewable.
See inline comments for details.
| @@ -197,8 +197,10 @@ will not be called." | |||
| (let ((was-bound (boundp symbol))) | |||
| (set-default symbol value) | |||
| (when (and was-bound (copilot--connection-alivep)) | |||
There was a problem hiding this comment.
This changes copilot--notify to a direct jsonrpc-notify call with a comment about macro definition order. This is unrelated to the panel feature and should be in a separate commit/PR if needed.
| (copilot--log 'info "Authenticated as GitHub user %s." user)))) | ||
|
|
||
| (defun copilot-logout () | ||
| "Logout from Copilot." |
There was a problem hiding this comment.
This entire function is just re-indented with no functional change. The diff still has large hunks of pure whitespace changes here, in all the notification handlers, and in copilot--show-completion. Please revert these so only the actual panel changes are in the diff.
| @@ -1011,6 +1013,9 @@ Each request METHOD can have only one HANDLER." | |||
| (when handler | |||
| (funcall handler msg)))) | |||
|
|
|||
There was a problem hiding this comment.
setq before defvar will produce a byte-compile "assignment to free variable" warning. Remove this line — the defvar on line 1018 already initializes the variable. If you need to clear handlers on reload, do it after the defvar with (clrhash copilot--notification-handlers).
| (3 'success) | ||
| (2 'warning) | ||
| (1 'error))))))))) | ||
| (with-current-buffer (get-buffer-create "*copilot-language-server-log*") |
There was a problem hiding this comment.
Switching from (with-current-buffer "*copilot-panel*" ...) to (get-buffer-create "*copilot-panel*") means if a notification arrives before copilot-panel-complete creates and initializes the buffer, this will create a fresh buffer without org-mode. The org-map-entries call on the next line will then fail. Either use get-buffer and bail if nil, or ensure org-mode is active before calling org functions.
| (copilot--log 'info "Synthesizing %d solutions..." solutionCountTarget))) | ||
| (with-current-buffer (get-buffer-create "*copilot-panel*") | ||
| (let ((inhibit-read-only t)) | ||
| (erase-buffer)) |
There was a problem hiding this comment.
There's no define-minor-mode for copilot-panel-mode anywhere — this will signal a void-function error at runtime and the keybindings won't work at all. This is the main blocker. You need:
(define-minor-mode copilot-panel-mode
"Minor mode for the Copilot panel buffer."
:keymap copilot-panel-mode-map)Place it right after the copilot-panel-mode-map definition.
| (interactive) | ||
| (let ((solution-text (copilot--get-current-solution-text)) | ||
| (source-buffer copilot-panel--source-buffer)) | ||
| (when solution-text |
There was a problem hiding this comment.
Typo still present from the first review round: "suggesetion" -> "suggestion".
| (pop-to-buffer source-buffer) | ||
| (yank) | ||
| (message "Solution inserted and copied to clipboard")) | ||
| (message "Source buffer is no longer alive"))))) |
There was a problem hiding this comment.
kill-new + yank pollutes the user's kill ring as a side effect. Just use (insert solution-text) directly in the source buffer — copilot-panel-select-suggestion already does this correctly.
|
|
||
| (defun copilot--get-all-solutions () | ||
| "Extract all solutions from the copilot panel buffer." | ||
| (with-current-buffer "*copilot-panel*" |
There was a problem hiding this comment.
kill-buffer-and-window will error if the panel buffer isn't displayed in a window. Use something like:
(when-let ((buf (get-buffer "*copilot-panel*")))
(when-let ((win (get-buffer-window buf)))
(delete-window win))
(kill-buffer buf))| (let* ((heading-text (match-string-no-properties 1)) | ||
| (solution-text (copilot--get-current-solution-text-at-pos (line-beginning-position)))) | ||
| (when solution-text | ||
| (push (list :title heading-text |
There was a problem hiding this comment.
This only matches Solution [0-9]+ (numbered headings), but solutions are initially inserted as * Solution without a number. They only get numbered in PanelSolutionsDone. If the user calls this while solutions are still arriving, no matches will be found and it will incorrectly report "still generating". Match both forms:
(while (re-search-forward "^\\*+ \\(Solution\\(?: [0-9]+\\)?\\)" nil t)| (keymap-set copilot-completion-map "C-c s" #'copilot-panel-select-suggestion) | ||
| (keymap-set copilot-completion-map "C-c p" #'copilot-panel-complete) | ||
| ``` | ||
|
|
There was a problem hiding this comment.
Binding C-c s and C-c p in copilot-completion-map doesn't make sense — the completion map is only active when an inline completion overlay is visible. Panel commands should be usable anytime. Suggest documenting these as user-level keybinding examples only (e.g. in a personal keymap), not in copilot-completion-map.
Previously: calling copilot-panel-complete simply generated a buffer with a formatted list of suggested solutions. However, there was no built-in way to interact with those solutions - usere had to manually select and copy them.
This update: interactive commands and local keybindings to easily extract, copy, and insert these suggested code blocks directly from the panel.
Local binding in the copilot-panel: