[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)))))
;; Process content (defun claude-display-loading-indicator ()
(if (and content (arrayp content) (> (length content) 0)) "Display a loading indicator in the conversation buffer."
(dotimes (i (length content)) (let ((buffer (claude-ensure-conversation-buffer))
(let* ((item (aref content i)) (timestamp (format-time-string "%H:%M:%S")))
(type (cdr (assoc 'type item)))) (with-current-buffer buffer
(when (string= type "text") (let ((inhibit-read-only t))
(let ((text (cdr (assoc 'text item)))) (save-excursion
(insert text "\n\n"))))) (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))))))
;; Handle unexpected response format (defun claude-remove-loading-indicator ()
(insert (propertize "Unexpected response format from Claude API.\n" "Remove the loading indicator from the conversation buffer."
'face 'font-lock-warning-face)) (when (and (boundp 'claude-loading-indicator-point)
(insert "Response data: " (prin1-to-string data))) claude-loading-indicator-point)
(let ((buffer (claude-ensure-conversation-buffer)))
;; Add helpful instructions at the bottom
(goto-char (point-max))
(insert (propertize "\n──────────────────────────────────────\n"
'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
(goto-char (point-min))))
;; Only display the buffer if requested and not already visible
(when claude-auto-display-results
(unless (get-buffer-window buffer)
(display-buffer-other-window buffer)))))
(defun claude-send-message (prompt &optional system-prompt tools)
"Send PROMPT to Claude and display the response.
If SYSTEM-PROMPT is provided, include it in the request.
If TOOLS is provided, enable tool use."
(let ((data `((model . ,claude-model)
(max_tokens . ,claude-max-tokens)
(messages . [((role . "user")
(content . ,prompt))]))))
;; Add system prompt if provided
(when system-prompt
(setq data (append data `((system . ,system-prompt)))))
;; Add tools if provided
(when tools
(setq data (append data `((tools . ,tools)
(tool_choice . "auto")))))
;; Display the buffer with a loading message
(let ((buffer (claude-ensure-buffer)))
(with-current-buffer buffer (with-current-buffer buffer
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(erase-buffer) (save-excursion
(insert "Sending request to Claude...\n\n"))) (goto-char claude-loading-indicator-point)
(when claude-auto-display-results (forward-line -3) ;; Go to the start of the loading message
(display-buffer buffer))) (delete-region (point) claude-loading-indicator-point)))))))
;; Send request (defun claude-process-response (data)
"Process and display the response DATA from Claude."
(let ((content (cdr (assoc 'content data))))
(claude-remove-loading-indicator)
(if (and content (arrayp content) (> (length content) 0))
(let ((response-text ""))
;; Collect text content from all response parts
(dotimes (i (length content))
(let* ((item (aref content i))
(type (cdr (assoc 'type item))))
(when (string= type "text")
(let ((text (cdr (assoc 'text item))))
(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
(let ((error-message "Received an unexpected response format from Claude API."))
(claude-display-assistant-message
(concat error-message "\n\nResponse data: " (prin1-to-string data))))))
;; Move to input area
(with-current-buffer (claude-ensure-conversation-buffer)
(goto-char (point-max))))
(defun claude-get-context-content ()
"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))
(defun claude-send-message (prompt)
"Send PROMPT to Claude and display the response in the conversation buffer."
;; Add user message to history
(claude-append-to-conversation "user" prompt)
;; Display the user's message
(claude-display-user-message prompt)
;; Show loading indicator
(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)
(messages . ,messages-array))))
;; Add tools if enabled
(when claude-tools-enabled
(setq data (append data `((tools . ,(mapcar (lambda (tool)
`((name . ,(plist-get tool :name))
(description . ,(plist-get tool :description))
(parameters . ,(plist-get tool :parameters))))
claude-tools))
(tool_choice . "auto")))))
;; Send the 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)
(with-current-buffer buffer (claude-display-assistant-message (format "Error: %s" error-thrown))))))
(let ((inhibit-read-only t))
(erase-buffer)
(insert (format "Error: %s" error-thrown)))))))))
;;; Interactive commands: (defun claude-send-input ()
"Send the current input to 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))) (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-code-review () (defun claude-cancel-input ()
"Ask Claude to review the code in the current buffer." "Cancel the current input."
(interactive) (interactive)
(let ((code (buffer-substring-no-properties (point-min) (point-max)))) (let ((buffer (claude-ensure-conversation-buffer)))
(claude-send-message (with-current-buffer buffer
(concat "Please review the following code and suggest improvements:\n\n```\n" (let ((inhibit-read-only t))
code "\n```")))) (delete-region claude-input-marker (point-max))))))
(defun claude-explain-code () (defun claude-new-conversation ()
"Ask Claude to explain the selected code." "Start a new conversation with Claude."
(interactive) (interactive)
(if (use-region-p) (setq claude-conversation-history nil)
(let ((code (buffer-substring-no-properties (region-beginning) (region-end)))) (setq claude-context-buffers nil)
(claude-send-message (let ((buffer (claude-ensure-conversation-buffer)))
(concat "Please explain what this code does in detail:\n\n```\n" (with-current-buffer buffer
code "\n```"))) (claude-insert-conversation-header)))
(message "No region selected"))) (message "Started a new conversation with Claude"))
(defun claude-complete-code () (defun claude-add-context-buffer (buffer)
"Ask Claude to complete the code at point." "Add BUFFER to the list of context buffers."
(interactive (list (read-buffer "Add buffer as context: " (other-buffer (current-buffer) t))))
(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) (interactive)
(let* ((buffer-text (buffer-substring-no-properties (point-min) (point-max))) (if claude-context-buffers
(cursor-pos (point)) (let ((buffer-list (mapconcat #'buffer-name claude-context-buffers ", ")))
(text-before (buffer-substring-no-properties (point-min) cursor-pos)) (message "Context buffers: %s" buffer-list))
(text-after (buffer-substring-no-properties cursor-pos (point-max)))) (message "No context buffers set")))
(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 () (defun claude-toggle-tool-use ()
"Prompt for input and send to Claude." "Toggle the use of tools in the conversation."
(interactive) (interactive)
(let ((prompt (read-string "Ask Claude: "))) (setq claude-tools-enabled (not claude-tools-enabled))
(claude-send-message prompt))) (message "Claude tool use %s" (if claude-tools-enabled "enabled" "disabled")))
(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
(let ((inhibit-read-only t))
(erase-buffer)
(insert "Refreshing last request to Claude...\n\n"))))
;; Re-send the same request
(claude-send-message prompt system-prompt tools)
(message "Refreshing Claude request..."))
(message "No previous Claude request to refresh")))
;;; Tool use functionality: ;;; Tool use functionality:
@ -300,95 +440,113 @@ 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))))
(cond
((string= type "text")
(insert (cdr (assoc 'text item)) "\n\n"))
((string= type "tool_use")
(let* ((tool-use (cdr (assoc 'tool_use item)))
(tool-name (cdr (assoc 'name tool-use)))
(parameters (cdr (assoc 'parameters tool-use)))
(handler (gethash tool-name claude-tool-handlers)))
;; Check if we have this tool ;; Process all content items
(if handler (dotimes (i (length content-items))
(let ((tool-result (claude-execute-tool tool-name parameters))) (let* ((item (aref content-items i))
(insert (format "Tool call: %s\n" tool-name)) (type (cdr (assoc 'type item))))
(insert (format "Parameters: %s\n" (json-encode parameters))) (cond
(insert (format "Result: %s\n\n" tool-result)) ((string= type "text")
(setq response-text
(concat response-text (cdr (assoc 'text item)) "\n\n")))
;; Send the tool result back to Claude ((string= type "tool_use")
(claude-send-tool-result data tool-name tool-result)) (let* ((tool-use (cdr (assoc 'tool_use item)))
(tool-name (cdr (assoc 'name tool-use)))
(parameters (cdr (assoc 'parameters tool-use)))
(handler (gethash tool-name claude-tool-handlers)))
;; Tool not found - record the request and notify ;; Check if we have this tool
(let ((description (format "Tool requested by Claude for task: %s" (if handler
(or (cdr (assoc 'description tool-use)) (let ((tool-result (claude-execute-tool tool-name parameters)))
"No description provided"))) (setq tool-usage-text
(param-structure (or (cdr (assoc 'parameter_structure tool-use)) (concat tool-usage-text
parameters))) (format "Tool call: %s\n" tool-name)
(format "Parameters: %s\n" (json-encode parameters))
(format "Result: %s\n\n" tool-result)))
;; Record the tool request ;; Send the tool result back to Claude
(claude-request-tool tool-name description param-structure) (claude-send-tool-result data tool-name tool-result))
;; Notify Claude about missing tool ;; Tool not found - record the request and notify
(insert (format "Tool requested: %s\n" tool-name)) (let ((description (format "Tool requested by Claude for task: %s"
(insert (format "This tool is not currently available.\n")) (or (cdr (assoc 'description tool-use))
(insert "The request has been recorded. You can implement it with M-x claude-list-requested-tools.\n\n") "No description provided")))
(param-structure (or (cdr (assoc 'parameter_structure tool-use))
parameters)))
;; Send error message back to Claude ;; Record the tool request
(let ((error-msg (format "The requested tool '%s' is not currently available. Would you like me to suggest an alternative approach?" tool-name))) (claude-request-tool tool-name description param-structure)
(claude-send-tool-result data tool-name error-msg))))))))))))))
;; Add to response text
(setq tool-usage-text
(concat tool-usage-text
(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
(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))))))))))
;; 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)))
(request ;; Find the tool call ID
"https://api.anthropic.com/v1/messages" (let* ((content-items (cdr (assoc 'content (aref (cdr (assoc 'messages data)) 0)))))
:type "POST" (dotimes (i (length content-items))
:headers `(("Content-Type" . "application/json") (let* ((item (aref content-items i))
("x-api-key" . ,api-key) (type (cdr (assoc 'type item))))
("anthropic-version" . "2023-06-01")) (when (string= type "tool_use")
:data (json-encode (setq tool-call-id (cdr (assoc 'id (cdr (assoc 'tool_use item)))))))))
`((model . ,claude-model)
(max_tokens . ,claude-max-tokens) (when tool-call-id
(messages . ,(vconcat (cdr (assoc 'messages data)) ;; Show loading indicator
`[((role . "assistant") (claude-display-loading-indicator)
(content . [((type . "tool_result")
(tool_result . ((tool_call_id . ,tool-call-id) (request
(content . ,tool-result))))]))])))) "https://api.anthropic.com/v1/messages"
:parser 'json-read :type "POST"
:success (cl-function :headers `(("Content-Type" . "application/json")
(lambda (&key data &allow-other-keys) ("x-api-key" . ,api-key)
(claude-handle-tool-response data buffer))) ("anthropic-version" . "2023-06-01"))
:error (cl-function :data (json-encode
(lambda (&key error-thrown &allow-other-keys) `((model . ,claude-model)
(with-current-buffer buffer (max_tokens . ,claude-max-tokens)
(let ((inhibit-read-only t)) (messages . ,(vconcat (cdr (assoc 'messages data))
(erase-buffer) `[((role . "assistant")
(insert (format "Error: %s" error-thrown))))))))) (content . [((type . "tool_result")
(tool_result . ((tool_call_id . ,tool-call-id)
(content . ,tool-result))))]))]))))
:parser 'json-read
:success (cl-function
(lambda (&key data &allow-other-keys)
(claude-handle-tool-response data buffer)))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(claude-remove-loading-indicator)
(claude-display-assistant-message (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.")