Skip to content

copilot chat poc #343

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,27 @@ For example:
(setq copilot-network-proxy '(:host "127.0.0.1" :port 7890))
```

### copilot-on-request
Register a handler to be called when a request of type method is received. Return JSON serializable as result or calling `jsonrpc-error` for errors. [readmore](https://www.gnu.org/software/emacs/manual/html_node/elisp/JSONRPC-Overview.html)

For example:
```elisp
; Display desktop notification if emacs is built with d-bus
(copilot-on-request
'window/showMessageRequest
(lambda (msg) (notifications-notify :title "Emacs Copilot" :body (plist-get msg :message))))
```

### copilot-on-notification
Register a listener for copilot notifications.

For example:
```elisp
(copilot-on-notification
'window/logMessage
(lambda (msg) (message (plist-get msg :message))
```

## Known Issues

### Wrong Position of Other Completion Popups
Expand All @@ -319,7 +340,7 @@ But I decided to allow them to coexist, allowing you to choose a better one at a
## Reporting Bugs

+ Make sure you have restarted your Emacs (and rebuild the plugin if necessary) after updating the plugin.
+ Please enable event logging by customize `copilot-log-max` (to e.g. 1000), then paste related logs in the `*copilot events*` and `*copilot stderr*` buffer.
+ Please enable event logging by customize `copilot-log-max` (to e.g. 1000) and enable debug log `(setq copilot-server-args '("--stdio" "--debug"))`, then paste related logs in the `*copilot events*`, `*copilot stderr*` and `*copilot agent log*` buffer.
+ If an exception is thrown, please also paste the stack trace (use `M-x toggle-debug-on-error` to enable stack trace).

## Roadmap
Expand Down
145 changes: 145 additions & 0 deletions copilot-chat.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
(require 'json)
(require 'copilot)

(defun buffer-file-uri ()
(interactive)
(let ((file-name (buffer-file-name)))
(and file-name (concat "file://" file-name))))


(defvar buffer-context nil)
(defvar conversation-id nil)
(defvar work-done-token -1)

(defconst chat-buffer-name "*Copilot Chat*")

(defun conversation-turn (conv-id chat-msg)
(setq work-done-token (1+ work-done-token))
(copilot--async-request
'conversation/turn
(list :workDoneToken work-done-token
:conversationId conv-id
:message chat-msg)))



(defun my-chat-send-message (source)
"Prompt the user to enter a message and add it to the chat buffer."
(let ((chat-msg (read-string "Enter your message: "))
(chat-buffer (get-buffer chat-buffer-name)))
(setq buffer-context (current-buffer))
(message "[buffer-context] %s" buffer-context)
(if (and chat-buffer conversation-id)
(with-current-buffer chat-buffer
(display-buffer chat-buffer)
(end-of-buffer)
(message "conversation/turn %s" chat-buffer)
(insert (concat "User: " chat-msg "\n\n"))
(conversation-turn conversation-id chat-msg))
(let
((new-chat-buffer (get-buffer-create chat-buffer-name))
(document-uri (buffer-file-uri)))
(display-buffer new-chat-buffer)
(with-current-buffer new-chat-buffer
(insert (concat "User: " chat-msg "\n\n"))
(message "conversation/create")
(message document-uri)
(setq work-done-token (1+ work-done-token))
(copilot--async-request
'conversation/create
(list :workDoneToken work-done-token
:turns `[(:request ,chat-msg)]
:capabilities '(:skills [
"current-editor" ;to inform language server the editor support conversation/context current-editor skill
])
:doc (list :uri document-uri)
:source source
)
:success-fn (jsonrpc-lambda (&key conversationId turnId agentSlug)(setq conversation-id conversationId))))))))


;
(defun current-editor-skill (conversation-id turn-id)
(message "[conversation/context][buffer] %s" buffer-context)
(with-current-buffer buffer-context
(let ((res `[(:uri ,(buffer-file-uri)
:position (:line ,(line-number-at-pos) :character ,(current-column))
:visibleRange (:start (:line ,(line-number-at-pos (window-start)) :character 1)
:end (:line ,(1+ (line-number-at-pos (window-end))) :character 1))
,@(if (region-active-p)
`(:selection (:start (:line ,(line-number-at-pos (region-beginning)) :character 1)
:end (:line ,(1+ (line-number-at-pos (region-end))) :character 1))))

;:openedAt
;:activeAt
)
nil]))
(message "[conversation/context] %s" res)
res)))

(copilot-on-request
'conversation/context
(lambda (msg)
(message (format "[conversation/context][buffer] %s" buffer-context))
(with-current-buffer buffer-context
(let ((res (copilot--dbind (:conversationId conversation-id :turnId turn-id :skillId skill-id) msg
(cond
((string= skill-id "current-editor") (current-editor-skill conversation-id turn-id))
(t '[nil (:code -1 :message "handle skill")])))))
(message (json-encode res))
res))))

(defun on-progress(msg)
(message (format "[$/progress] %s" msg))
(copilot--dbind
(:token work-done-token
:value (:kind kind
:title title
:conversationId conversation-id
:turnId turn-id
:reply reply ; streaming response by lines when kind="report"
:suggestedTitle suggested-title ; receive at the end of turn of "panel" source coversation
:followUp follow-up ; receive at the end of turn of "panel" source coversation
:updatedDocuments updated-documents ; receive at the end of turn of "inline" source coversation
))

msg
(message "[progress][buffer] %s" (get-buffer chat-buffer-name))
(with-current-buffer (get-buffer chat-buffer-name)
(end-of-buffer)
(message "[progress][kind] %s" kind)
(message "[progress][updateDocuments] %s" updated-documents)
(cond
((string= kind "begin") (insert "Assistant: "))
((string= kind "end")
(when follow-up
(message "[Suggested followup]")
(insert "Suggested followup: ")
(insert follow-up-msg)
)
(when updated-documents
(message "[updated-documents]")
(message (format "[updated-documents] %s" updated-documents))
(update-documents updated-documents))
(insert "\n\n"))
(reply (message "[reply] %s" reply) (insert reply))))))

(copilot-on-notification '$/progress (lambda (message) (on-progress message)))


(defun update-documents (updated-documents)
(seq-do (lambda (doc)
(let ((uri (plist-get doc :uri))
(text (plist-get doc :text)))
(message "[update-documents] uri: %s" uri)
(message "[update-documents] text: %s" text)
(with-current-buffer (find-buffer-visiting uri)
(delete-region (point-min) (point-max))
(insert text))
))
updated-documents))

(defun chat-inline () (interactive) (my-chat-send-message "inline"))
(defun chat-panel () (interactive) (my-chat-send-message "panel"))

(provide 'copilot-chat)
97 changes: 72 additions & 25 deletions copilot.el
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ find indentation offset."
(defvar copilot--server-executable nil
"The dist directory containing agent.js file.")

(defcustom copilot-version "1.27.0"
(defcustom copilot-version "1.40.0"
"Copilot version.

The default value is the preferred version and ensures functionality.
Expand Down Expand Up @@ -290,6 +290,7 @@ SUCCESS-FN is the CALLBACK."
#'make-instance
'jsonrpc-process-connection
:name "copilot"
:request-dispatcher #'copilot--handle-request
:notification-dispatcher #'copilot--handle-notification
:process (make-process :name "copilot agent"
:command (append
Expand Down Expand Up @@ -328,6 +329,7 @@ Please upgrade the server via `M-x copilot-reinstall-server`"))
(setq copilot--connection (copilot--make-connection))
(message "Copilot agent started.")
(copilot--request 'initialize '(:capabilities (:workspace (:workspaceFolders t))))
(copilot--notify 'initialized '())
(copilot--async-request 'setEditorInfo
`(:editorInfo (:name "Emacs" :version ,emacs-version)
:editorPluginInfo (:name "copilot.el" :version ,copilot-version)
Expand Down Expand Up @@ -624,31 +626,76 @@ automatically, browse to %s." user-code verification-uri))
(defvar copilot--panel-lang nil
"Language of current panel solutions.")

(defvar copilot--request-handlers (make-hash-table :test 'equal)
"Hash table storing request handlers.")

(defun copilot-on-request (method handler)
"Register HANDLER to be called when a request of type METHOD is received.
Each METHOD can have only one HANDLER."
(puthash method handler copilot--request-handlers))

(defun copilot--handle-request (_ method msg)
"Handle MSG of type METHOD by calling the appropriate registered handler."
(let ((handler (gethash method copilot--request-handlers)))
(when handler
(funcall handler msg))))

(defvar copilot--notification-handlers (make-hash-table :test 'equal)
"Hash table storing lists of notification handlers.")

(defun copilot-on-notification (method handler)
"Register HANDLER to be called when a notification of type METHOD is received."
(let ((handlers (gethash method copilot--notification-handlers '())))
(puthash method (cons handler handlers) copilot--notification-handlers)))

(defun copilot--handle-notification (_ method msg)
"Handle MSG of type METHOD."
(when (eql method 'PanelSolution)
(copilot--dbind (:completionText completion-text :score completion-score) msg
(with-current-buffer "*copilot-panel*"
(unless (member (secure-hash 'sha256 completion-text)
(org-map-entries (lambda () (org-entry-get nil "SHA"))))
(save-excursion
(goto-char (point-max))
(insert "* Solution\n"
" :PROPERTIES:\n"
" :SCORE: " (number-to-string completion-score) "\n"
" :SHA: " (secure-hash 'sha256 completion-text) "\n"
" :END:\n"
"#+BEGIN_SRC " copilot--panel-lang "\n"
completion-text "\n#+END_SRC\n\n")
(call-interactively #'mark-whole-buffer)
(org-sort-entries nil ?R nil nil "SCORE"))))))
(when (eql method 'PanelSolutionsDone)
(message "Copilot: Finish synthesizing solutions.")
(display-buffer "*copilot-panel*")
(with-current-buffer "*copilot-panel*"
(save-excursion
(goto-char (point-max))
(insert "End of solutions.\n")))))
"Handle MSG of type METHOD by calling all appropriate registered handlers."
(let ((handlers (gethash method copilot--notification-handlers '())))
(dolist (handler handlers)
(funcall handler msg))))

(copilot-on-notification
'window/logMessage
(lambda (msg)
(copilot--dbind (:type log-level :message log-msg) msg
(with-current-buffer (get-buffer-create "*copilot agent log*")
(save-excursion
(goto-char (point-max))
(insert (propertize (concat log-msg "\n")
'face (pcase log-level
(4 '(:foreground "gray"))
(3 '(:foreground "green"))
(2 '(:foreground "yellow"))
(1 '(:foreground "red"))))))))))

(copilot-on-notification
'PanelSolution
(lambda (msg)
(copilot--dbind (:completionText completion-text :score completion-score) msg
(with-current-buffer "*copilot-panel*"
(unless (member (secure-hash 'sha256 completion-text)
(org-map-entries (lambda () (org-entry-get nil "SHA"))))
(save-excursion
(goto-char (point-max))
(insert "* Solution\n"
" :PROPERTIES:\n"
" :SCORE: " (number-to-string completion-score) "\n"
" :SHA: " (secure-hash 'sha256 completion-text) "\n"
" :END:\n"
"#+BEGIN_SRC " copilot--panel-lang "\n"
completion-text "\n#+END_SRC\n\n")
(call-interactively #'mark-whole-buffer)
(org-sort-entries nil ?R nil nil "SCORE")))))))

(copilot-on-notification
'PanelSolutionsDone
(lambda (_msg)
(message "Copilot: Finish synthesizing solutions.")
(display-buffer "*copilot-panel*")
(with-current-buffer "*copilot-panel*"
(save-excursion
(goto-char (point-max))
(insert "End of solutions.\n")))))

(defun copilot--get-panel-completions (callback)
"Get panel completions with CALLBACK."
Expand Down