[emacs] More hacking, snapshot to sync

This commit is contained in:
John Doty 2025-04-25 18:10:39 +00:00
parent cdc09a3c8c
commit 1fd66682e5
3 changed files with 505 additions and 275 deletions

View file

@ -57,8 +57,28 @@
'(org-log-done t) '(org-log-done t)
'(org-odd-levels-only t) '(org-odd-levels-only t)
'(org-todo-keywords '((sequence "TODO" "|" "DONE" "ABANDONED" "DEFERRED"))) '(org-todo-keywords '((sequence "TODO" "|" "DONE" "ABANDONED" "DEFERRED")))
'(package-check-signature nil)
'(package-selected-packages '(package-selected-packages
'(gptel fish-mode editorconfig jsonnet-mode scala-ts-mode adaptive-wrap add-node-modules-path ag auto-complete auto-complete-nxml bazel blacken cider clang-format clojure-mode color-theme-monokai color-theme-sanityinc-solarized color-theme-sanityinc-tomorrow company company-jedi company-lsp compat cquery dash-functional deadgrep dockerfile-mode doom-themes earthfile-mode eglot eglot-java elm-mode esup exec-path-from-shell filladapt flycheck flycheck-elm flycheck-rust flymake flyspell fsharp-mode geiser gnu-elpa-keyring-update go-autocomplete go-mode graphviz-dot-mode hack-mode haxe-mode howm ink-mode js2-mode js2-refactor json-mode lsp-hack lsp-pyright lsp-ui lua-mode magit markdown-mode merlin mocha modus-themes monky monokai-theme multi-term mustache-mode nyan-mode paredit popup prettier-js projectile protobuf-mode python-mode rjsx-mode ruby-mode rust-mode sql-indent swift-mode switch-window terraform-mode thrift tide tree-sitter tss tuareg typescript-mode use-package vterm web-mode wgrep xref-js2 xterm-color yaml-mode zig-mode)) '(adaptive-wrap add-node-modules-path ag auto-complete auto-complete-nxml
bazel blacken cider clang-format clipetty clojure-mode
color-theme-monokai color-theme-sanityinc-solarized
color-theme-sanityinc-tomorrow company company-jedi
company-lsp compat cquery dash-functional deadgrep
dockerfile-mode doom-themes earthfile-mode editorconfig
eglot eglot-java elm-mode esup exec-path-from-shell
filladapt fish-mode flycheck flycheck-elm flycheck-rust
flymake flyspell fsharp-mode geiser
gnu-elpa-keyring-update go-autocomplete go-mode gptel
graphviz-dot-mode hack-mode haxe-mode howm ink-mode
js2-mode js2-refactor json-mode jsonnet-mode lsp-hack
lsp-pyright lsp-ui lua-mode magit markdown-mode merlin
mocha modus-themes monky monokai-theme multi-term
mustache-mode nyan-mode paredit popup prettier-js
projectile protobuf-mode python-mode request rjsx-mode
ruby-mode rust-mode scala-mode scala-ts-mode sql-indent
swift-mode switch-window terraform-mode thrift tide
tree-sitter tss tuareg typescript-mode use-package vterm
web-mode wgrep xref-js2 xterm-color yaml-mode zig-mode))
'(reb-re-syntax 'string) '(reb-re-syntax 'string)
'(rmail-mail-new-frame t) '(rmail-mail-new-frame t)
'(safe-local-variable-values '(safe-local-variable-values
@ -73,8 +93,8 @@
(whitespace-mode 0) (whitespace-mode 0)
(whitespace-mode 1)) (whitespace-mode 1))
(whitespace-line-column . 80) (whitespace-line-column . 80)
(whitespace-style face trailing lines-tail) (whitespace-style face trailing lines-tail) (require-final-newline . t)))
(require-final-newline . t))) '(scala-indent:use-javadoc-style t)
'(scroll-conservatively 1) '(scroll-conservatively 1)
'(scroll-step 1) '(scroll-step 1)
'(sd-user-email "johndoty@microsoft.com") '(sd-user-email "johndoty@microsoft.com")
@ -86,6 +106,7 @@
'(tags-revert-without-query t) '(tags-revert-without-query t)
'(tramp-completion-reread-directory-timeout nil) '(tramp-completion-reread-directory-timeout nil)
'(tramp-default-method "sshx") '(tramp-default-method "sshx")
'(tramp-use-connection-share nil)
'(tramp-use-ssh-controlmaster-options nil) '(tramp-use-ssh-controlmaster-options nil)
'(transient-mark-mode t) '(transient-mark-mode t)
'(truncate-lines t) '(truncate-lines t)

View file

@ -1401,44 +1401,24 @@ Do this when you edit your project view."
;; ================================================================= ;; =================================================================
;; AI Shit ;; AI Shit
;; ================================================================= ;; =================================================================
(use-package request :ensure) (defun claude-get-api-key ()
"Get Claude API key from auth-source."
(let ((auth-info (nth 0 (auth-source-search :host "anthropic.com"
:user "claude-api"
:require '(:secret)
:create t))))
(if auth-info
(let ((secret (plist-get auth-info :secret)))
(if (functionp secret)
(funcall secret)
secret))
(error "Claude API key not found in auth-source"))))
(use-package claude (use-package gptel :ensure
:load-path "~/site-lisp/"
:custom
(claude-model "claude-3-7-sonnet-20250219") ;; Current model as of March 2025
(claude-max-tokens 4000)
(claude-auto-display-results t)
:config :config
;; If you want to add any custom tools, add them here (setq
;; (claude-register-tool gptel-model 'claude-3-7-sonnet-20250219 ; "claude-3-opus-20240229" also available
;; '(:name "my_custom_tool" gptel-backend (gptel-make-anthropic "Claude"
;; :description "A custom tool that does something specific" :stream t :key #'claude-get-api-key))
;; :parameters ((properties )
;; (param1 (title . "Parameter 1")
;; (type . "string")
;; (description . "Description of parameter 1"))))))
;; (claude-register-tool-handler
;; "my_custom_tool"
;; (lambda (parameters)
;; ;; Your implementation here
;; (format "Tool executed with param: %s" (cdr (assoc 'param1 parameters)))))
;; Enable the minor mode for keybindings
(claude-mode 1)
:bind (:map claude-mode-map
;; You can customize the keybindings if you prefer different ones
("C-c C-a a" . claude-prompt-and-send) ;; Add a custom binding
;; Default bindings included by claude-mode:
;; C-c C-a s - claude-send-region
;; C-c C-a b - claude-send-buffer
;; C-c C-a r - claude-code-review
;; C-c C-a e - claude-explain-code
;; C-c C-a c - claude-complete-code
;; C-c C-a t - claude-send-with-tools
;; C-c C-a l - claude-list-requested-tools
;; C-c C-a p - claude-prompt-and-send
))
;;; init.el ends here ;;; init.el ends here

View file

@ -9,6 +9,9 @@
;; This package provides integration with the Claude AI assistant from ;; This package provides integration with the Claude AI assistant from
;; Anthropic. It includes basic functionality like sending text to Claude, as ;; Anthropic. It includes basic functionality like sending text to Claude, as
;; well as code-specific features and tool use capabilities. ;; well as code-specific features and tool use capabilities.
;;
;; It now features a conversational interface with support for context buffers
;; and optional tool use.
;;; Code: ;;; Code:
@ -37,29 +40,77 @@
:type 'boolean :type 'boolean
:group 'claude) :group 'claude)
(defcustom claude-conversation-buffer-name "*Claude Conversation*"
"Name of the buffer for Claude conversations."
:type 'string
:group 'claude)
(defcustom claude-enable-markdown-fontification t
"If non-nil, enable markdown fontification in the conversation buffer."
:type 'boolean
:group 'claude)
(defcustom claude-user-message-face 'font-lock-keyword-face
"Face for user messages in the conversation buffer."
:type 'face
:group 'claude)
(defcustom claude-assistant-message-face 'font-lock-comment-face
"Face for Claude's messages in the conversation buffer."
:type 'face
:group 'claude)
(defcustom claude-timestamp-face 'font-lock-doc-face
"Face for timestamps in the conversation buffer."
:type 'face
:group 'claude)
(defcustom claude-separator-string "────────────────────────────────────────────────────────────"
"Separator string used between messages in the conversation buffer."
:type 'string
:group 'claude)
;;; Core functionality: ;;; Core functionality:
(defvar claude-buffer-name "*Claude*" (defvar claude-conversation-history nil
"Name of the buffer for Claude interactions.") "History of conversation with Claude as a list of alists with keys 'role' and 'content'.")
(defvar claude-last-request nil (defvar claude-context-buffers nil
"Store the last request data sent to Claude.") "List of buffers to provide as context for the conversation.")
(defvar claude-response-mode-map (defvar claude-tools-enabled nil
"If non-nil, enable tool use in the conversation.")
(defvar claude-conversation-mode-map
(let ((map (make-sparse-keymap))) (let ((map (make-sparse-keymap)))
(define-key map "q" 'quit-window) (define-key map (kbd "RET") 'claude-send-input)
(define-key map "r" 'claude-refresh-last-request) ;; We'll define this function later (define-key map (kbd "C-c C-c") 'claude-send-input)
(define-key map (kbd "C-c C-k") 'claude-cancel-input)
;; Change these keybindings to avoid conflict with the global prefix
(define-key map (kbd "C-c a") 'claude-add-context-buffer) ;; was C-c C-a
(define-key map (kbd "C-c r") 'claude-remove-context-buffer) ;; was C-c C-r
(define-key map (kbd "C-c l") 'claude-list-context-buffers) ;; was C-c C-l
(define-key map (kbd "C-c t") 'claude-toggle-tool-use) ;; was C-c C-t
(define-key map (kbd "C-c n") 'claude-new-conversation) ;; was C-c C-n
map) map)
"Keymap for Claude response buffers.") "Keymap for Claude conversation buffers.")
(define-derived-mode claude-response-mode markdown-mode "Claude" (define-derived-mode claude-conversation-mode text-mode "Claude Conversation"
"Major mode for viewing Claude AI responses." "Major mode for conversing with Claude AI assistant."
(use-local-map claude-response-mode-map)
;; Enable visual line mode for word wrapping ;; Enable visual line mode for word wrapping
(visual-line-mode 1) (visual-line-mode 1)
;; Make buffer read-only by default (setq-local indent-tabs-mode nil)
(setq buffer-read-only t))
(use-local-map claude-conversation-mode-map)
(when claude-enable-markdown-fontification
(when (fboundp 'markdown-mode)
;; We only want the font-lock features of markdown mode
(setq-local font-lock-defaults (cadr (assoc 'font-lock-defaults
(symbol-plist 'markdown-mode))))))
;; Make the bottom of the buffer input-only
(setq-local claude-input-marker (point-max-marker))
(set-marker-insertion-type claude-input-marker nil)) ;; ?
(defun claude-get-api-key () (defun claude-get-api-key ()
"Get Claude API key from auth-source." "Get Claude API key from auth-source."
@ -74,24 +125,34 @@
secret)) secret))
(error "Claude API key not found in auth-source")))) (error "Claude API key not found in auth-source"))))
(defun claude-ensure-buffer () (defun claude-ensure-conversation-buffer ()
"Ensure the Claude buffer exists with proper formatting and keybindings." "Ensure the Claude conversation buffer exists and return it."
(let ((buffer (get-buffer-create claude-buffer-name))) (let ((buffer (get-buffer-create claude-conversation-buffer-name)))
(with-current-buffer buffer (with-current-buffer buffer
(unless (eq major-mode 'claude-response-mode) (unless (eq major-mode 'claude-conversation-mode)
;; Use our custom mode that has the q key binding (claude-conversation-mode)
(if (fboundp 'markdown-mode) ;; Check if markdown-mode is available (claude-insert-conversation-header)))
(claude-response-mode) ;; Use our derived mode if markdown is available
;; Fallback if markdown-mode isn't available
(special-mode) ;; special-mode also has the q key binding
(visual-line-mode 1))
;; Set word-wrap and margins regardless of mode
(setq word-wrap t)
(setq left-margin-width 2
right-margin-width 2)))
buffer)) buffer))
(defun claude-insert-conversation-header ()
"Insert the header for a new conversation."
(let ((inhibit-read-only t))
(erase-buffer)
(insert (propertize "Claude Conversation" 'face 'bold) "\n\n")
(insert "Type your message below and press RET or C-c C-c to send.\n")
(insert "Commands:\n")
(insert " C-c a: Add a context buffer\n") ;; was C-c C-a
(insert " C-c r: Remove a context buffer\n") ;; was C-c C-r
(insert " C-c l: List context buffers\n") ;; was C-c C-l
(insert " C-c t: Toggle tool use\n") ;; was C-c C-t
(insert " C-c n: Start a new conversation\n") ;; was C-c C-n
(insert "\n")
(insert (propertize claude-separator-string 'face 'shadow))
(insert "\n\n")
;; Set the input marker at the end of the buffer
(setq-local claude-input-marker (point-marker))
(set-marker-insertion-type claude-input-marker nil)))
(defun claude-send-request (data &optional callback error-callback) (defun claude-send-request (data &optional callback error-callback)
"Send request with DATA to Claude API. "Send request with DATA to Claude API.
If CALLBACK is provided, call it with the response data. If CALLBACK is provided, call it with the response data.
@ -115,158 +176,237 @@ If ERROR-CALLBACK is provided, call it with any error."
(funcall error-callback error-thrown) (funcall error-callback error-thrown)
(message "Claude API error: %s" error-thrown))))))) (message "Claude API error: %s" error-thrown)))))))
(defun claude-display-response (data) (defun claude-append-to-conversation (role content)
"Display the response DATA from Claude with nice formatting." "Append a message with ROLE and CONTENT to the conversation history."
(let ((content (cdr (assoc 'content data))) (add-to-list 'claude-conversation-history
(buffer (claude-ensure-buffer))) `((role . ,role)
(content . ,content))
t))
(defun claude-display-user-message (message)
"Display the user MESSAGE in the conversation buffer."
(let ((buffer (claude-ensure-conversation-buffer))
(timestamp (format-time-string "%H:%M:%S")))
(with-current-buffer buffer (with-current-buffer buffer
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
;; Clear the buffer (save-excursion
(erase-buffer) (goto-char claude-input-marker)
(insert "\n")
(insert (propertize (format "[You - %s]" timestamp)
'face claude-timestamp-face))
(insert "\n\n")
(insert (propertize message 'face claude-user-message-face))
(insert "\n\n")
(insert (propertize claude-separator-string 'face 'shadow))
(insert "\n\n")))
(setq-local claude-input-marker (point-max-marker)))))
;; Add a timestamp header (defun claude-display-assistant-message (content)
(insert (propertize "Display the assistant's CONTENT in the conversation buffer."
(format "Claude response at %s\n\n" (let ((buffer (claude-ensure-conversation-buffer))
(format-time-string "%H:%M:%S")) (timestamp (format-time-string "%H:%M:%S")))
'face 'font-lock-comment-face)) (with-current-buffer buffer
(let ((inhibit-read-only t))
(save-excursion
(goto-char claude-input-marker)
(insert "\n")
(insert (propertize (format "[Claude - %s]" timestamp)
'face claude-timestamp-face))
(insert "\n\n")
(insert (propertize content 'face claude-assistant-message-face))
(insert "\n\n")
(insert (propertize claude-separator-string 'face 'shadow))
(insert "\n\n")))
(setq-local claude-input-marker (point-max-marker))
(goto-char (point-max)))))
(defun claude-display-loading-indicator ()
"Display a loading indicator in the conversation buffer."
(let ((buffer (claude-ensure-conversation-buffer))
(timestamp (format-time-string "%H:%M:%S")))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(save-excursion
(goto-char claude-input-marker)
(insert "\n")
(insert (propertize (format "[Claude - %s]" timestamp)
'face claude-timestamp-face))
(insert "\n\n")
(insert (propertize "Thinking..." 'face 'italic))
;; Don't insert separator yet
(insert "\n\n")
(setq-local claude-loading-indicator-point (point-marker))
(set-marker-insertion-type claude-loading-indicator-point t))))))
(defun claude-remove-loading-indicator ()
"Remove the loading indicator from the conversation buffer."
(when (and (boundp 'claude-loading-indicator-point)
claude-loading-indicator-point)
(let ((buffer (claude-ensure-conversation-buffer)))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(save-excursion
(goto-char claude-loading-indicator-point)
(forward-line -3) ;; Go to the start of the loading message
(delete-region (point) claude-loading-indicator-point)))))))
(defun claude-process-response (data)
"Process and display the response DATA from Claude."
(let ((content (cdr (assoc 'content data))))
(claude-remove-loading-indicator)
;; Process content
(if (and content (arrayp content) (> (length content) 0)) (if (and content (arrayp content) (> (length content) 0))
(let ((response-text ""))
;; Collect text content from all response parts
(dotimes (i (length content)) (dotimes (i (length content))
(let* ((item (aref content i)) (let* ((item (aref content i))
(type (cdr (assoc 'type item)))) (type (cdr (assoc 'type item))))
(when (string= type "text") (when (string= type "text")
(let ((text (cdr (assoc 'text item)))) (let ((text (cdr (assoc 'text item))))
(insert text "\n\n"))))) (setq response-text (concat response-text text))))))
;; Add to conversation history
(claude-append-to-conversation "assistant" response-text)
;; Display the response
(claude-display-assistant-message response-text))
;; Handle unexpected response format ;; Handle unexpected response format
(insert (propertize "Unexpected response format from Claude API.\n" (let ((error-message "Received an unexpected response format from Claude API."))
'face 'font-lock-warning-face)) (claude-display-assistant-message
(insert "Response data: " (prin1-to-string data))) (concat error-message "\n\nResponse data: " (prin1-to-string data))))))
;; Add helpful instructions at the bottom ;; Move to input area
(goto-char (point-max)) (with-current-buffer (claude-ensure-conversation-buffer)
(insert (propertize "\n──────────────────────────────────────\n" (goto-char (point-max))))
'face 'font-lock-comment-face))
(insert (propertize "Press q to close this window\n"
'face 'font-lock-comment-face))
;; Return to the start of the buffer (defun claude-get-context-content ()
(goto-char (point-min)))) "Get the content from all context buffers."
(let ((context-content ""))
(dolist (buffer claude-context-buffers)
(when (buffer-live-p buffer)
(with-current-buffer buffer
(setq context-content
(concat context-content
(format "\n\nContent from buffer '%s':\n\n%s"
(buffer-name buffer)
(buffer-substring-no-properties (point-min) (point-max))))))))
context-content))
;; Only display the buffer if requested and not already visible (defun claude-send-message (prompt)
(when claude-auto-display-results "Send PROMPT to Claude and display the response in the conversation buffer."
(unless (get-buffer-window buffer) ;; Add user message to history
(display-buffer-other-window buffer))))) (claude-append-to-conversation "user" prompt)
(defun claude-send-message (prompt &optional system-prompt tools) ;; Display the user's message
"Send PROMPT to Claude and display the response. (claude-display-user-message prompt)
If SYSTEM-PROMPT is provided, include it in the request.
If TOOLS is provided, enable tool use." ;; Show loading indicator
(let ((data `((model . ,claude-model) (claude-display-loading-indicator)
;; Prepare messages for the API
(let* ((context (claude-get-context-content))
(final-prompt (if (string-empty-p context)
prompt
(concat prompt "\n\n" context)))
(messages-array (vconcat
(apply #'vector
(mapcar (lambda (msg)
`((role . ,(cdr (assoc 'role msg)))
(content . ,(cdr (assoc 'content msg)))))
claude-conversation-history))))
(data `((model . ,claude-model)
(max_tokens . ,claude-max-tokens) (max_tokens . ,claude-max-tokens)
(messages . [((role . "user") (messages . ,messages-array))))
(content . ,prompt))]))))
;; Add system prompt if provided ;; Add tools if enabled
(when system-prompt (when claude-tools-enabled
(setq data (append data `((system . ,system-prompt))))) (setq data (append data `((tools . ,(mapcar (lambda (tool)
`((name . ,(plist-get tool :name))
;; Add tools if provided (description . ,(plist-get tool :description))
(when tools (parameters . ,(plist-get tool :parameters))))
(setq data (append data `((tools . ,tools) claude-tools))
(tool_choice . "auto"))))) (tool_choice . "auto")))))
;; Display the buffer with a loading message ;; Send the request
(let ((buffer (claude-ensure-buffer)))
(with-current-buffer buffer
(let ((inhibit-read-only t))
(erase-buffer)
(insert "Sending request to Claude...\n\n")))
(when claude-auto-display-results
(display-buffer buffer)))
;; Send request
(claude-send-request (claude-send-request
data data
(lambda (data) (lambda (data)
(if tools (if (and claude-tools-enabled
(claude-handle-tool-response data (claude-ensure-buffer)) (or (assoc 'tool_use (aref (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0))) 0))
(claude-display-response data))) (assoc 'tool_result (aref (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0))) 0))))
(claude-handle-tool-response data (claude-ensure-conversation-buffer))
(claude-process-response data)))
(lambda (error-thrown) (lambda (error-thrown)
(let ((buffer (claude-ensure-buffer))) (claude-remove-loading-indicator)
(claude-display-assistant-message (format "Error: %s" error-thrown))))))
(defun claude-send-input ()
"Send the current input to Claude."
(interactive)
(let ((buffer (claude-ensure-conversation-buffer)))
(with-current-buffer buffer
(let ((input (buffer-substring-no-properties claude-input-marker (point-max))))
(when (not (string-empty-p (string-trim input)))
;; Delete the input text
(let ((inhibit-read-only t))
(delete-region claude-input-marker (point-max)))
;; Send the message
(claude-send-message (string-trim input)))))))
(defun claude-cancel-input ()
"Cancel the current input."
(interactive)
(let ((buffer (claude-ensure-conversation-buffer)))
(with-current-buffer buffer (with-current-buffer buffer
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(erase-buffer) (delete-region claude-input-marker (point-max))))))
(insert (format "Error: %s" error-thrown)))))))))
;;; Interactive commands: (defun claude-new-conversation ()
"Start a new conversation with Claude."
(defun claude-send-region (start end)
"Send the region between START and END to Claude and display the response."
(interactive "r")
(let ((prompt (buffer-substring-no-properties start end)))
(claude-send-message prompt)))
(defun claude-send-buffer ()
"Send the entire buffer to Claude."
(interactive) (interactive)
(claude-send-region (point-min) (point-max))) (setq claude-conversation-history nil)
(setq claude-context-buffers nil)
(defun claude-code-review () (let ((buffer (claude-ensure-conversation-buffer)))
"Ask Claude to review the code in the current buffer."
(interactive)
(let ((code (buffer-substring-no-properties (point-min) (point-max))))
(claude-send-message
(concat "Please review the following code and suggest improvements:\n\n```\n"
code "\n```"))))
(defun claude-explain-code ()
"Ask Claude to explain the selected code."
(interactive)
(if (use-region-p)
(let ((code (buffer-substring-no-properties (region-beginning) (region-end))))
(claude-send-message
(concat "Please explain what this code does in detail:\n\n```\n"
code "\n```")))
(message "No region selected")))
(defun claude-complete-code ()
"Ask Claude to complete the code at point."
(interactive)
(let* ((buffer-text (buffer-substring-no-properties (point-min) (point-max)))
(cursor-pos (point))
(text-before (buffer-substring-no-properties (point-min) cursor-pos))
(text-after (buffer-substring-no-properties cursor-pos (point-max))))
(claude-send-message
(concat "I'm writing code and need you to continue it from where the cursor is marked with [CURSOR]. Only provide the code that should replace [CURSOR], nothing else.\n\n```\n"
text-before "[CURSOR]" text-after "\n```"))))
(defun claude-prompt-and-send ()
"Prompt for input and send to Claude."
(interactive)
(let ((prompt (read-string "Ask Claude: ")))
(claude-send-message prompt)))
(defun claude-refresh-last-request ()
"Refresh the last Claude request by sending it again."
(interactive)
(if claude-last-request
(let ((prompt (plist-get claude-last-request :prompt))
(system-prompt (plist-get claude-last-request :system-prompt))
(tools (plist-get claude-last-request :tools)))
;; Display refreshing message
(let ((buffer (claude-ensure-buffer)))
(with-current-buffer buffer (with-current-buffer buffer
(let ((inhibit-read-only t)) (claude-insert-conversation-header)))
(erase-buffer) (message "Started a new conversation with Claude"))
(insert "Refreshing last request to Claude...\n\n"))))
;; Re-send the same request (defun claude-add-context-buffer (buffer)
(claude-send-message prompt system-prompt tools) "Add BUFFER to the list of context buffers."
(message "Refreshing Claude request...")) (interactive (list (read-buffer "Add buffer as context: " (other-buffer (current-buffer) t))))
(message "No previous Claude request to refresh"))) (let ((buf (get-buffer buffer)))
(when buf
(unless (memq buf claude-context-buffers)
(add-to-list 'claude-context-buffers buf)
(message "Added %s to context buffers" (buffer-name buf))))))
(defun claude-remove-context-buffer (buffer)
"Remove BUFFER from the list of context buffers."
(interactive
(list
(when claude-context-buffers
(let ((buffer-names (mapcar #'buffer-name claude-context-buffers)))
(completing-read "Remove buffer from context: " buffer-names nil t)))))
(when buffer
(let ((buf (get-buffer buffer)))
(when buf
(setq claude-context-buffers (delq buf claude-context-buffers))
(message "Removed %s from context buffers" (buffer-name buf))))))
(defun claude-list-context-buffers ()
"List all context buffers in the minibuffer."
(interactive)
(if claude-context-buffers
(let ((buffer-list (mapconcat #'buffer-name claude-context-buffers ", ")))
(message "Context buffers: %s" buffer-list))
(message "No context buffers set")))
(defun claude-toggle-tool-use ()
"Toggle the use of tools in the conversation."
(interactive)
(setq claude-tools-enabled (not claude-tools-enabled))
(message "Claude tool use %s" (if claude-tools-enabled "enabled" "disabled")))
;;; Tool use functionality: ;;; Tool use functionality:
@ -300,29 +440,25 @@ The handler will be called with the parameters passed by Claude."
(funcall handler parameters) (funcall handler parameters)
(format "Error: No handler registered for tool %s" tool-name)))) (format "Error: No handler registered for tool %s" tool-name))))
(defun claude-send-with-tools (prompt)
"Send PROMPT to Claude with tools enabled and handle the response."
(interactive "sPrompt: ")
(let ((tools-json (mapcar (lambda (tool)
`((name . ,(plist-get tool :name))
(description . ,(plist-get tool :description))
(parameters . ,(plist-get tool :parameters))))
claude-tools)))
(claude-send-message prompt nil tools-json)))
(defun claude-handle-tool-response (data buffer) (defun claude-handle-tool-response (data buffer)
"Handle response DATA from Claude that may contain tool calls. "Handle response DATA from Claude that may contain tool calls.
Display results in BUFFER." Display results in BUFFER."
(let ((message (aref (cdr (assoc 'messages data)) 0))) (let ((message (aref (cdr (assoc 'messages data)) 0)))
(claude-remove-loading-indicator)
(with-current-buffer buffer (with-current-buffer buffer
(let ((inhibit-read-only t)) (let ((content-items (cdr (assoc 'content message)))
(erase-buffer) (tool-usage-text "")
(let ((content-items (cdr (assoc 'content message)))) (response-text ""))
(dolist (item content-items)
(let ((type (cdr (assoc 'type item)))) ;; Process all content items
(dotimes (i (length content-items))
(let* ((item (aref content-items i))
(type (cdr (assoc 'type item))))
(cond (cond
((string= type "text") ((string= type "text")
(insert (cdr (assoc 'text item)) "\n\n")) (setq response-text
(concat response-text (cdr (assoc 'text item)) "\n\n")))
((string= type "tool_use") ((string= type "tool_use")
(let* ((tool-use (cdr (assoc 'tool_use item))) (let* ((tool-use (cdr (assoc 'tool_use item)))
(tool-name (cdr (assoc 'name tool-use))) (tool-name (cdr (assoc 'name tool-use)))
@ -332,9 +468,11 @@ Display results in BUFFER."
;; Check if we have this tool ;; Check if we have this tool
(if handler (if handler
(let ((tool-result (claude-execute-tool tool-name parameters))) (let ((tool-result (claude-execute-tool tool-name parameters)))
(insert (format "Tool call: %s\n" tool-name)) (setq tool-usage-text
(insert (format "Parameters: %s\n" (json-encode parameters))) (concat tool-usage-text
(insert (format "Result: %s\n\n" tool-result)) (format "Tool call: %s\n" tool-name)
(format "Parameters: %s\n" (json-encode parameters))
(format "Result: %s\n\n" tool-result)))
;; Send the tool result back to Claude ;; Send the tool result back to Claude
(claude-send-tool-result data tool-name tool-result)) (claude-send-tool-result data tool-name tool-result))
@ -349,21 +487,43 @@ Display results in BUFFER."
;; Record the tool request ;; Record the tool request
(claude-request-tool tool-name description param-structure) (claude-request-tool tool-name description param-structure)
;; Notify Claude about missing tool ;; Add to response text
(insert (format "Tool requested: %s\n" tool-name)) (setq tool-usage-text
(insert (format "This tool is not currently available.\n")) (concat tool-usage-text
(insert "The request has been recorded. You can implement it with M-x claude-list-requested-tools.\n\n") (format "Tool requested: %s\n" tool-name)
"This tool is not currently available.\n"
"The request has been recorded. You can implement it with M-x claude-list-requested-tools.\n\n"))
;; Send error message back to Claude ;; Send error message back to Claude
(let ((error-msg (format "The requested tool '%s' is not currently available. Would you like me to suggest an alternative approach?" tool-name))) (let ((error-msg (format "The requested tool '%s' is not currently available. Would you like me to suggest an alternative approach?" tool-name)))
(claude-send-tool-result data tool-name error-msg)))))))))))))) (claude-send-tool-result data tool-name error-msg))))))))))
;; Display the combined message
(let ((full-response (concat response-text
(if (not (string-empty-p tool-usage-text))
(concat "\n## Tool Usage\n\n" tool-usage-text)
""))))
(claude-append-to-conversation "assistant" full-response)
(claude-display-assistant-message full-response)))))
(defun claude-send-tool-result (data tool-name tool-result) (defun claude-send-tool-result (data tool-name tool-result)
"Send TOOL-RESULT for TOOL-NAME back to Claude based on the original DATA." "Send TOOL-RESULT for TOOL-NAME back to Claude based on the original DATA."
(let ((message-id (cdr (assoc 'id (aref (cdr (assoc 'messages data)) 0)))) (let ((message-id (cdr (assoc 'id (aref (cdr (assoc 'messages data)) 0))))
(tool-call-id (cdr (assoc 'id (cdr (assoc 'tool_use (aref (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0))) 0)))))) (tool-call-id nil)
(api-key (claude-get-api-key)) (api-key (claude-get-api-key))
(buffer (claude-ensure-buffer))) (buffer (claude-ensure-conversation-buffer)))
;; Find the tool call ID
(let* ((content-items (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0)))))
(dotimes (i (length content-items))
(let* ((item (aref content-items i))
(type (cdr (assoc 'type item))))
(when (string= type "tool_use")
(setq tool-call-id (cdr (assoc 'id (cdr (assoc 'tool_use item)))))))))
(when tool-call-id
;; Show loading indicator
(claude-display-loading-indicator)
(request (request
"https://api.anthropic.com/v1/messages" "https://api.anthropic.com/v1/messages"
@ -385,10 +545,8 @@ Display results in BUFFER."
(claude-handle-tool-response data buffer))) (claude-handle-tool-response data buffer)))
:error (cl-function :error (cl-function
(lambda (&key error-thrown &allow-other-keys) (lambda (&key error-thrown &allow-other-keys)
(with-current-buffer buffer (claude-remove-loading-indicator)
(let ((inhibit-read-only t)) (claude-display-assistant-message (format "Error: %s" error-thrown))))))))
(erase-buffer)
(insert (format "Error: %s" error-thrown)))))))))
(defun claude-request-tool (tool-name description parameters) (defun claude-request-tool (tool-name description parameters)
"Record a request from Claude for a tool that isn't available." "Record a request from Claude for a tool that isn't available."
@ -635,21 +793,92 @@ Display results in BUFFER."
(claude-clear-tools) (claude-clear-tools)
(claude-register-filesystem-tools) (claude-register-filesystem-tools)
(claude-register-emacs-tools) (claude-register-emacs-tools)
;; Uncomment the line below if you want shell tools enabled by default
;; (claude-register-shell-tools) ;; (claude-register-shell-tools)
) )
;;; Interactive commands:
(defun claude-start-conversation ()
"Start or switch to a conversation with Claude."
(interactive)
(let ((buffer (claude-ensure-conversation-buffer)))
(switch-to-buffer buffer)))
(defun claude-send-region (start end)
"Send the region between START and END to Claude in the conversation buffer."
(interactive "r")
(let ((prompt (buffer-substring-no-properties start end)))
(claude-start-conversation)
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char claude-input-marker)
(insert prompt)
(claude-send-input))))
(defun claude-send-buffer ()
"Send the entire buffer to Claude in the conversation buffer."
(interactive)
(claude-send-region (point-min) (point-max)))
(defun claude-code-review ()
"Ask Claude to review the code in the current buffer."
(interactive)
(let ((code (buffer-substring-no-properties (point-min) (point-max)))
(buffer-name (buffer-name))
(buffer (current-buffer)))
;; Add the current buffer as context
(unless (memq buffer claude-context-buffers)
(add-to-list 'claude-context-buffers buffer))
;; Start the conversation
(claude-start-conversation)
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char claude-input-marker)
(insert (format "Please review the code in %s and suggest improvements." buffer-name))
(claude-send-input))))
(defun claude-explain-code ()
"Ask Claude to explain the selected code or current buffer."
(interactive)
(let ((buffer (current-buffer)))
;; Add the current buffer as context
(unless (memq buffer claude-context-buffers)
(add-to-list 'claude-context-buffers buffer))
;; Start the conversation
(claude-start-conversation)
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char claude-input-marker)
(if (use-region-p)
(insert "Please explain the selected code in detail.")
(insert (format "Please explain the code in %s in detail." (buffer-name buffer))))
(claude-send-input))))
(defun claude-complete-code ()
"Ask Claude to complete the code at point."
(interactive)
(let* ((buffer (current-buffer))
(cursor-pos (point))
(buffer-name (buffer-name)))
;; Add the current buffer as context
(unless (memq buffer claude-context-buffers)
(add-to-list 'claude-context-buffers buffer))
;; Start the conversation
(claude-start-conversation)
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char claude-input-marker)
(insert (format "Complete the code at the cursor position in %s. The cursor is at position %d."
buffer-name cursor-pos))
(claude-send-input))))
;;; Keybindings: ;;; Keybindings:
(defvar claude-mode-map (defvar claude-mode-map
(let ((map (make-sparse-keymap))) (let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-a c") 'claude-start-conversation)
(define-key map (kbd "C-c C-a s") 'claude-send-region) (define-key map (kbd "C-c C-a s") 'claude-send-region)
(define-key map (kbd "C-c C-a b") 'claude-send-buffer) (define-key map (kbd "C-c C-a b") 'claude-send-buffer)
(define-key map (kbd "C-c C-a r") 'claude-code-review) (define-key map (kbd "C-c C-a r") 'claude-code-review)
(define-key map (kbd "C-c C-a e") 'claude-explain-code) (define-key map (kbd "C-c C-a e") 'claude-explain-code)
(define-key map (kbd "C-c C-a c") 'claude-complete-code) (define-key map (kbd "C-c C-a o") 'claude-complete-code)
(define-key map (kbd "C-c C-a t") 'claude-send-with-tools)
(define-key map (kbd "C-c C-a l") 'claude-list-requested-tools)
(define-key map (kbd "C-c C-a p") 'claude-prompt-and-send)
map) map)
"Keymap for Claude mode.") "Keymap for Claude mode.")