diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-11 08:50:14 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-11 08:50:14 +0200 |
| commit | c74c3526fe719ff8127d17f44b67a33772adc334 (patch) | |
| tree | 7b05d6cad835f8704430ac38c4f22358601fe846 /lisp | |
| parent | c6579ddf761aaa1c922213310dc1f46347ea06ca (diff) | |
| download | emacs-c74c3526fe719ff8127d17f44b67a33772adc334.tar.gz | |
2026-07-11T08:50:16+02:00
Diffstat (limited to 'lisp')
| -rw-r--r-- | lisp/agent-shell-diff.el | 268 | ||||
| -rw-r--r-- | lisp/agent-shell-kimi.el | 612 |
2 files changed, 880 insertions, 0 deletions
diff --git a/lisp/agent-shell-diff.el b/lisp/agent-shell-diff.el new file mode 100644 index 0000000..b78bdf1 --- /dev/null +++ b/lisp/agent-shell-diff.el @@ -0,0 +1,268 @@ +;;; agent-shell-diff.el --- A quick way to query/display a diff. -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Alvaro Ramirez + +;; Author: Alvaro Ramirez https://xenodium.com +;; URL: https://github.com/xenodium/agent-shell + +;; This package is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 3, or (at your option) +;; any later version. + +;; This package is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; Report issues at https://github.com/xenodium/agent-shell/issues +;; +;; ✨ Please support this work https://github.com/sponsors/xenodium ✨ + +;;; Code: + +(eval-when-compile + (require 'cl-lib)) +(require 'ansi-color) +(require 'diff) +(require 'diff-mode) + +(defcustom agent-shell-diff-delta-command + '("delta" "--no-gitconfig" "--color-only") + "Command and arguments used to colorize diffs with delta. + +Set to nil to disable delta and fall back to the standard `diff-no-select' +output. delta must be installed and on `exec-path'." + :type '(choice (const :tag "Disable delta" nil) + (repeat string)) + :group 'agent-shell) + +(defvar-local agent-shell-diff--on-exit nil + "Function to call when the diff buffer is killed. + +This variable is automatically set by :on-exit from `agent-shell-diff' +and can be temporarily let-bound to nil to prevent the +on-exit callback from running when the buffer is killed.") + +(defvar-local agent-shell-diff--file nil + "Buffer-local file path associated with the diff.") + +(defvar-local agent-shell-diff--accept-all-command nil + "Buffer-local command to accept all changes in the diff.") + +(defvar-local agent-shell-diff--reject-all-command nil + "Buffer-local command to reject all changes in the diff.") + +(defvar-local agent-shell-diff--used-delta nil + "Non-nil when the current buffer's diff was generated using delta.") + +(defvar agent-shell-diff-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "n") #'diff-hunk-next) + (define-key map (kbd "p") #'diff-hunk-prev) + (define-key map (kbd "y") #'agent-shell-diff-accept-all) + (define-key map (kbd "C-c C-c") #'agent-shell-diff-reject-all) + (define-key map (kbd "f") #'agent-shell-diff-open-file) + (define-key map (kbd "q") #'kill-current-buffer) + map) + "Keymap for `agent-shell-diff-mode'.") + +(define-derived-mode agent-shell-diff-mode diff-mode "Agent-Shell-Diff" + "Major mode for `agent-shell' diff buffers. +Derives from `diff-mode'. Provides `agent-shell-diff-accept-all' +and `agent-shell-diff-reject-all' commands that can be rebound +via `agent-shell-diff-mode-map'." + :group 'agent-shell + ;; Don't inherit diff-mode-map (some bindings can be destructive). + (set-keymap-parent agent-shell-diff-mode-map nil) + (setq buffer-read-only t)) + +(defun agent-shell-diff-kill-buffer (buffer) + "Kill diff BUFFER, suppressing any `agent-shell-diff--on-exit' callback. +If BUFFER is not live, do nothing." + (when (buffer-live-p buffer) + (with-current-buffer buffer + (setq agent-shell-diff--on-exit nil)) + (kill-buffer buffer))) + +(defun agent-shell-diff-accept-all () + "Accept all changes in the current diff buffer." + (interactive) + (if agent-shell-diff--accept-all-command + (let ((buf (current-buffer))) + (funcall agent-shell-diff--accept-all-command) + (when (buffer-live-p buf) + (let ((agent-shell-diff--on-exit nil)) + (kill-buffer buf)))) + (user-error "No accept command available in this buffer"))) + +(defun agent-shell-diff-reject-all () + "Reject all changes in the current diff buffer." + (interactive) + (if agent-shell-diff--reject-all-command + (let ((buf (current-buffer))) + (when (funcall agent-shell-diff--reject-all-command) + (when (buffer-live-p buf) + (let ((agent-shell-diff--on-exit nil)) + (kill-buffer buf))))) + (user-error "No reject command available in this buffer"))) + +(cl-defun agent-shell-diff (&key old new on-exit on-accept on-reject title file) + "Display a diff between OLD and NEW strings in a buffer. + +Creates a new buffer showing the differences between OLD and NEW +using `agent-shell-diff-mode'. The buffer is read-only. + +When the buffer is killed, calls ON-EXIT with no arguments. + +Returns the newly created diff buffer. + +Arguments: + :OLD - Original string content + :NEW - Modified string content + :ON-EXIT - Function called with no arguments when buffer is killed + :ON-ACCEPT - Command to accept all changes + :ON-REJECT - Command to reject all changes + :TITLE - Optional title to display in header line + :FILE - File path" + (let* ((diff-buffer (generate-new-buffer "*agent-shell-diff*")) + (calling-window (selected-window)) + (calling-buffer (current-buffer)) + (interrupt-key (where-is-internal 'agent-shell-interrupt + (current-local-map) t))) + (unwind-protect + (progn + (with-current-buffer diff-buffer + (let ((inhibit-read-only t) + (diff-mode-read-only nil)) + (erase-buffer) + ;; Set mode before inserting diff so diff-no-select + ;; doesn't reset font-lock (see #316). + (agent-shell-diff-mode) + (agent-shell-diff--insert-diff old new file diff-buffer) + ;; Add overlays to hide scary text. + (save-excursion + (goto-char (point-min)) + ;; `diff-no-select' adds a command line at the top and + ;; "Diff finished." at the bottom. delta output does not, + ;; so only strip those when delta was not used. + (unless agent-shell-diff--used-delta + (delete-region (point) (progn (forward-line 1) (point))) + (delete-region (progn (goto-char (point-max)) (forward-line -1) (forward-line 0) (point)) + (point-max)) + (goto-char (point-min))) + (goto-char (point-min)) + ;; Hide --- and +++ lines + (while (re-search-forward "^\\(---\\|\\+\\+\\+\\).*\n" nil t) + (let ((overlay (make-overlay (match-beginning 0) (match-end 0)))) + (overlay-put overlay 'category 'diff-header) + (overlay-put overlay 'display "") + (overlay-put overlay 'evaporate t))) + ;; Replace @@ lines with "Changes" + (goto-char (point-min)) + (while (re-search-forward "^@@.*@@.*\n" nil t) + (let ((overlay (make-overlay (match-beginning 0) (match-end 0))) + (face 'diff-hunk-header)) ; or any face you prefer + (overlay-put overlay 'category 'diff-header) + ;; Intended display is: + ;; ╭─────────╮ + ;; │ changes │ + ;; ╰─────────╯ + ;; Using before-string so diff-hunk-next + ;; lands on "│" instead of "╭". + (overlay-put overlay 'before-string + (propertize "\n╭─────────╮\n" 'face face)) + (overlay-put overlay 'display + (propertize "│ changes │\n╰─────────╯\n\n" 'face face)) + (overlay-put overlay 'evaporate t))))) + (goto-char (point-min)) + (ignore-errors (diff-hunk-next)) + (setq agent-shell-diff--file file + agent-shell-diff--accept-all-command on-accept + agent-shell-diff--reject-all-command on-reject) + (when on-exit + (setq agent-shell-diff--on-exit on-exit) + (add-hook 'kill-buffer-hook + (lambda () + (when (and agent-shell-diff--on-exit + (buffer-live-p calling-buffer)) + (with-current-buffer calling-buffer + (funcall on-exit))) + ;; Give focus back to calling buffer. + (when (buffer-live-p calling-buffer) + (ignore-errors + (when (window-live-p calling-window) + (unless (eq (window-buffer calling-window) calling-buffer) + (set-window-buffer calling-window calling-buffer)) + (select-window calling-window))))) + nil t)) + (let ((map (copy-keymap agent-shell-diff-mode-map))) + (when (and interrupt-key + (not (lookup-key map interrupt-key))) + (define-key map interrupt-key #'agent-shell-diff-reject-all)) + (use-local-map map)) + (setq header-line-format + (substitute-command-keys + (concat + " " + (when title + (concat (propertize title 'face 'mode-line-emphasis) " ")) + "\\[diff-hunk-next] next hunk " + "\\[diff-hunk-prev] previous hunk " + "\\[agent-shell-diff-accept-all] accept " + "\\[agent-shell-diff-reject-all] reject " + "\\[agent-shell-diff-open-file] open " + "\\[kill-current-buffer] quit")))) + diff-buffer) + (pop-to-buffer diff-buffer '((display-buffer-use-some-window + display-buffer-same-window)))))) + +(defun agent-shell-diff-open-file () + "Open the file associated with the current diff buffer." + (interactive) + (if agent-shell-diff--file + (find-file agent-shell-diff--file) + (user-error "No file associated with this diff buffer"))) + +(defun agent-shell-diff--insert-diff (old new file buf) + "Insert diff from FILE between OLD and NEW strings in buffer BUF. + +When `agent-shell-diff-delta-command' is non-nil and delta is available +on `exec-path', pipe the unified diff through delta for syntax +highlighting; otherwise fall back to `diff-no-select'." + (let* ((suffix (format ".%s" (file-name-extension file))) + (old-file (make-temp-file "old" nil suffix)) + (new-file (make-temp-file "new" nil suffix)) + (use-delta (and agent-shell-diff-delta-command + (executable-find (car agent-shell-diff-delta-command))))) + (with-temp-file old-file (insert old)) + (with-temp-file new-file (insert new)) + (with-current-buffer buf + (setq agent-shell-diff--used-delta use-delta) + (if use-delta + (let ((coding-system-for-read 'utf-8) + (coding-system-for-write 'utf-8) + (command (concat "diff -U3 " + (shell-quote-argument old-file) + " " + (shell-quote-argument new-file) + " | " + (mapconcat #'shell-quote-argument + agent-shell-diff-delta-command + " ")))) + (call-process-shell-command command nil t nil) + ;; `ansi-color-apply-on-region' can drop properties in some + ;; Emacs builds, so apply to the string and re-insert. + (let ((colored (ansi-color-apply (buffer-string)))) + (erase-buffer) + (insert colored))) + (diff-no-select old-file new-file "-U3" t buf))))) + +(provide 'agent-shell-diff) + +;;; agent-shell-diff.el ends here diff --git a/lisp/agent-shell-kimi.el b/lisp/agent-shell-kimi.el new file mode 100644 index 0000000..3a9d948 --- /dev/null +++ b/lisp/agent-shell-kimi.el @@ -0,0 +1,612 @@ +;;; agent-shell-kimi.el --- Kimi Code agent configurations -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Nicolai Singh + +;; This package is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 3, or (at your option) +;; any later version. + +;; This package is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; This file includes Kimi Code configuration using `kimi acp'. +;; +;; It exposes the Kimi-specific flags that are available when starting an +;; ACP server (`--yolo', `--auto', `--plan', `--model', `--add-dir', +;; `--skills-dir') and integrates with Kimi's session index so recent +;; sessions can be resumed from Emacs. + +;;; Code: + +(eval-when-compile + (require 'cl-lib)) +(require 'json) +(require 'shell-maker) +(require 'acp) + +(declare-function agent-shell--indent-string "agent-shell") +(declare-function agent-shell--make-acp-client "agent-shell") +(declare-function agent-shell-make-agent-config "agent-shell") +(autoload 'agent-shell-make-agent-config "agent-shell") +(declare-function agent-shell--dwim "agent-shell") +(declare-function agent-shell-start "agent-shell") +(declare-function shell-maker-welcome-message "shell-maker") +(defvar shell-maker-prompt) +(defvar shell-maker-prompt-regexp) + +(defcustom agent-shell-kimi-acp-command + '("kimi" "acp") + "Command and parameters for the Kimi Code CLI client. + +The first element is the command name, and the rest are command parameters. +This is the base command; flags controlled by the other custom variables +in this file are prepended before the `acp' subcommand." + :type '(repeat string) + :group 'agent-shell) + +(defcustom agent-shell-kimi-default-model-id + nil + "Default Kimi model ID. + +Must be one of the model ID's displayed under \"Available models\" +when starting a new shell. + +Can be set to either a string or a function that returns a string." + :type '(choice (const nil) string function) + :group 'agent-shell) + +(defcustom agent-shell-kimi-default-session-mode-id + nil + "Default Kimi Code session mode ID. + +Must be one of the mode ID's displayed under \"Available modes\" +when starting a new shell. + +Can be set to either a string or a function that returns a string." + :type '(choice (const nil) string function) + :group 'agent-shell) + +(defcustom agent-shell-kimi-environment + nil + "Environment variables for the Kimi Code CLI client. + +This should be a list of environment variables to be used when +starting the Kimi client process." + :type '(repeat string) + :group 'agent-shell) + +(defcustom agent-shell-kimi-yolo + nil + "When non-nil, pass `--yolo' to `kimi' so all actions are auto-approved. + +See `kimi --help' for the exact semantics." + :type 'boolean + :group 'agent-shell) + +(defcustom agent-shell-kimi-auto + nil + "When non-nil, pass `--auto' to `kimi' to start in auto permission mode." + :type 'boolean + :group 'agent-shell) + +(defcustom agent-shell-kimi-plan + nil + "When non-nil, pass `--plan' to `kimi' to start in plan mode." + :type 'boolean + :group 'agent-shell) + +(defcustom agent-shell-kimi-model + nil + "Kimi model alias passed to `--model'. + +Can be either a string or a function that returns a string. When nil, +no `--model' flag is added and Kimi uses its configured default." + :type '(choice (const nil) string function) + :group 'agent-shell) + +(defcustom agent-shell-kimi-additional-dirs + nil + "List of additional workspace directories passed via `--add-dir'. + +Each entry should be an expanded directory path." + :type '(repeat string) + :group 'agent-shell) + +(defcustom agent-shell-kimi-skills-dirs + nil + "List of skills directories passed via `--skills-dir'. + +Each entry should be an expanded directory path." + :type '(repeat string) + :group 'agent-shell) + +(defcustom agent-shell-kimi-extra-command-params + nil + "Extra command-line parameters passed to `kimi' before the `acp' subcommand. + +Use this for flags that do not yet have a dedicated custom variable." + :type '(repeat string) + :group 'agent-shell) + +(defcustom agent-shell-kimi-session-index + (expand-file-name "~/.kimi-code/session_index.jsonl") + "Path to Kimi's session index file. + +Used by `agent-shell-kimi-resume-session' to list recent sessions." + :type 'file + :group 'agent-shell) + +(cl-defun agent-shell-kimi-make-authentication (&key api-key login none) + "Create a Kimi authentication configuration. + +API-KEY is a Kimi API key string or a function that returns one. +LOGIN when non-nil indicates login-based (device-code) authentication. +NONE when non-nil indicates no authentication method is requested. + +Only one of API-KEY, LOGIN, or NONE should be provided." + (when (> (seq-count #'identity (list api-key login none)) 1) + (error "Cannot specify multiple authentication methods - choose one")) + (unless (> (seq-count #'identity (list api-key login none)) 0) + (error "Must specify one of :api-key, :login, or :none")) + (cond + (api-key `((:api-key . ,api-key))) + (login `((:login . t))) + (none `((:none . t))))) + +(defcustom agent-shell-kimi-authentication + (agent-shell-kimi-make-authentication :none t) + "Configuration for Kimi authentication. + +For no explicit authentication (default): + + (setq agent-shell-kimi-authentication + (agent-shell-kimi-make-authentication :none t)) + +For login-based authentication: + + (setq agent-shell-kimi-authentication + (agent-shell-kimi-make-authentication :login t)) + +For API key (string): + + (setq agent-shell-kimi-authentication + (agent-shell-kimi-make-authentication :api-key \"your-key\")) + +For API key (function): + + (setq agent-shell-kimi-authentication + (agent-shell-kimi-make-authentication :api-key (lambda () ...)))" + :type 'alist + :group 'agent-shell) + +(defcustom agent-shell-kimi-login-command + '("kimi" "login") + "Command used by `agent-shell-kimi-login' to run the device-code flow." + :type '(repeat string) + :group 'agent-shell) + +(defcustom agent-shell-kimi-show-model-in-prompt + t + "When non-nil, include the active model/mode in the Kimi shell prompt." + :type 'boolean + :group 'agent-shell) + +;;;; Faces + +(defface agent-shell-kimi-prompt-face + '((t (:foreground "#81c8be" :weight bold :inherit fixed-pitch))) + "Face for the Kimi shell prompt." + :group 'agent-shell) + +(defface agent-shell-kimi-user-face + '((t (:inherit default))) + "Face for user messages in the Kimi shell." + :group 'agent-shell) + +(defface agent-shell-kimi-assistant-face + '((t (:foreground "#8caaee"))) + "Face for assistant messages in the Kimi shell." + :group 'agent-shell) + +(defface agent-shell-kimi-tool-face + '((t (:foreground "#ca9ee6"))) + "Face for tool-use sections in the Kimi shell." + :group 'agent-shell) + +(defface agent-shell-kimi-thought-face + '((t (:foreground "#a5adce" :slant italic))) + "Face for thought-process sections in the Kimi shell." + :group 'agent-shell) + +(defface agent-shell-kimi-error-face + '((t (:foreground "#e78284" :weight bold))) + "Face for errors in the Kimi shell." + :group 'agent-shell) + +;;;; Authentication & helpers + +(defun agent-shell-kimi-key () + "Get the Kimi API key from `agent-shell-kimi-authentication'." + (cond ((stringp (map-elt agent-shell-kimi-authentication :api-key)) + (map-elt agent-shell-kimi-authentication :api-key)) + ((functionp (map-elt agent-shell-kimi-authentication :api-key)) + (condition-case _err + (funcall (map-elt agent-shell-kimi-authentication :api-key)) + (error + "API key not found. Check out `agent-shell-kimi-authentication'"))) + (t + nil))) + +(defun agent-shell-kimi-login () + "Run `kimi login' to authenticate via the device-code flow." + (interactive) + (unless agent-shell-kimi-login-command + (user-error "`agent-shell-kimi-login-command' is not set")) + (let ((command (mapconcat #'shell-quote-argument agent-shell-kimi-login-command " "))) + (async-shell-command command "*Kimi login*"))) + +(defun agent-shell-kimi--resolve-string-or-function (value) + "Resolve VALUE if it is a function, otherwise return it as-is." + (if (functionp value) + (funcall value) + value)) + +(defun agent-shell-kimi--command-params () + "Return the complete list of parameters passed to `kimi' before `acp'." + (let ((params nil)) + (when agent-shell-kimi-yolo + (push "--yolo" params)) + (when agent-shell-kimi-auto + (push "--auto" params)) + (when agent-shell-kimi-plan + (push "--plan" params)) + (when-let* ((model (agent-shell-kimi--resolve-string-or-function agent-shell-kimi-model)) + ((not (string-empty-p model)))) + (push "--model" params) + (push model params)) + (dolist (dir (reverse agent-shell-kimi-additional-dirs)) + (push "--add-dir" params) + (push dir params)) + (dolist (dir (reverse agent-shell-kimi-skills-dirs)) + (push "--skills-dir" params) + (push dir params)) + (dolist (arg (reverse agent-shell-kimi-extra-command-params)) + (push arg params)) + (nreverse params))) + +(defun agent-shell-kimi--active-model-string () + "Return the currently configured Kimi model alias, or nil if none." + (when-let* ((model (agent-shell-kimi--resolve-string-or-function + agent-shell-kimi-model)) + ((not (string-empty-p model)))) + model)) + +(defun agent-shell-kimi--active-mode-string () + "Return the currently configured Kimi session mode ID, or nil if none." + (when-let* ((mode (agent-shell-kimi--resolve-string-or-function + agent-shell-kimi-default-session-mode-id)) + ((not (string-empty-p mode)))) + mode)) + +(defun agent-shell-kimi--prompt-string () + "Return the propertized Kimi shell prompt string." + (let ((extra nil)) + (when agent-shell-kimi-show-model-in-prompt + (let ((model (agent-shell-kimi--active-model-string)) + (mode (agent-shell-kimi--active-mode-string))) + (when (or model mode) + (setq extra (concat "(" + (or model "default") + (when mode (concat "|" mode)) + ")"))))) + (propertize (concat "Kimi" extra "> ") + 'font-lock-face 'agent-shell-kimi-prompt-face))) + +(defun agent-shell-kimi-update-prompt () + "Update the prompt in the current Kimi shell buffer." + (interactive) + (when (eq major-mode 'agent-shell-mode) + (setq-local shell-maker-prompt (agent-shell-kimi--prompt-string)) + (setq-local shell-maker-prompt-regexp + (concat "^" (regexp-quote (substring-no-properties shell-maker-prompt)))))) + +(defun agent-shell-kimi-make-config () + "Create a Kimi Code agent configuration. + +Returns an agent configuration alist using `agent-shell-make-agent-config'." + (agent-shell-make-agent-config + :identifier 'kimi + :mode-line-name "Kimi" + :buffer-name "Kimi" + :shell-prompt (agent-shell-kimi--prompt-string) + :shell-prompt-regexp "Kimi[^>]*> " + :welcome-function #'agent-shell-kimi--welcome-message + :client-maker (lambda (buffer) + (agent-shell-kimi-make-client :buffer buffer)) + :default-model-id (lambda () (agent-shell-kimi--resolve-string-or-function + agent-shell-kimi-default-model-id)) + :default-session-mode-id (lambda () (agent-shell-kimi--resolve-string-or-function + agent-shell-kimi-default-session-mode-id)) + :install-instructions "See https://www.kimi.com/code for installation.")) + +(defun agent-shell-kimi-start-agent () + "Start an interactive Kimi Code agent shell." + (interactive) + (agent-shell--dwim :config (agent-shell-kimi-make-config) + :new-shell t)) + +(defun agent-shell-kimi-start-coding () + "Start a Kimi shell for normal coding. + +This is equivalent to `agent-shell-kimi-start-agent' but explicitly +resets `--plan' and `--yolo' so the shell starts in plain coding mode." + (interactive) + (let ((agent-shell-kimi-plan nil) + (agent-shell-kimi-yolo nil)) + (agent-shell-kimi-start-agent))) + +(defun agent-shell-kimi-start-plan () + "Start a Kimi shell in plan mode (`--plan')." + (interactive) + (let ((agent-shell-kimi-plan t) + (agent-shell-kimi-yolo nil)) + (agent-shell-kimi-start-agent))) + +(defun agent-shell-kimi-start-yolo () + "Start a Kimi shell in yolo/auto-approve mode (`--yolo')." + (interactive) + (let ((agent-shell-kimi-yolo t)) + (agent-shell-kimi-start-agent))) + +(defun agent-shell-kimi-start-review () + "Start a Kimi shell tuned for code review. + +Enables `--plan' and, if no model is set, falls back to +`kimi-for-coding'. Customize `agent-shell-kimi-model' to change the +review model." + (interactive) + (let ((agent-shell-kimi-plan t) + (agent-shell-kimi-yolo nil) + (agent-shell-kimi-model (or agent-shell-kimi-model "kimi-for-coding"))) + (agent-shell-kimi-start-agent))) + +(cl-defun agent-shell-kimi-make-client (&key buffer) + "Create a Kimi Code ACP client with BUFFER as context." + (unless buffer + (error "Missing required argument: :buffer")) + (let* ((command (car agent-shell-kimi-acp-command)) + (base-params (cdr agent-shell-kimi-acp-command)) + (kimi-params (agent-shell-kimi--command-params)) + ;; Global `kimi' flags go before the `acp' subcommand. + (command-params (if (string= (car base-params) "acp") + (append kimi-params base-params) + (append base-params kimi-params))) + (env-vars (append agent-shell-kimi-environment + (when-let* ((api-key (agent-shell-kimi-key)) + ((not (map-elt agent-shell-kimi-authentication :none)))) + (list (format "KIMI_API_KEY=%s" api-key)))))) + (agent-shell--make-acp-client :command command + :command-params command-params + :environment-variables env-vars + :context-buffer buffer))) + +(defun agent-shell-kimi--read-jsonl (path) + "Return a list of parsed JSON objects from PATH (one per line)." + (when (file-exists-p path) + (with-temp-buffer + (insert-file-contents path) + (let ((result nil)) + (goto-char (point-min)) + (while (not (eobp)) + (let ((line (string-trim (thing-at-point 'line t)))) + (unless (string-empty-p line) + (push (json-parse-string line :object-type 'alist) result))) + (forward-line 1)) + (nreverse result))))) + +(defun agent-shell-kimi--session-mtime (session) + "Return the modification time of SESSION's directory, or 0 if missing." + (let ((dir (cdr (assq 'sessionDir session)))) + (if (file-directory-p dir) + (time-to-seconds (file-attribute-modification-time (file-attributes dir))) + 0))) + +(defun agent-shell-kimi--relative-time (seconds) + "Return a human-readable string for SECONDS since the epoch." + (let ((delta (max 0 (- (float-time) seconds)))) + (cond ((< delta 60) "just now") + ((< delta 3600) (format "%d min ago" (floor delta 60))) + ((< delta 86400) (format "%d hr ago" (floor delta 3600))) + ((< delta 604800) (format "%d days ago" (floor delta 86400))) + (t (format "%d wks ago" (floor delta 604800)))))) + +(defun agent-shell-kimi--session-project-name (session) + "Return a project-like name for SESSION's working directory." + (let ((dir (abbreviate-file-name (or (cdr (assq 'workDir session)) "")))) + (if (string-empty-p dir) + "?" + (file-name-nondirectory (directory-file-name dir))))) + +(defun agent-shell-kimi--session-label (session) + "Return a display label for SESSION suitable for `completing-read'." + (let ((id (cdr (assq 'sessionId session))) + (dir (abbreviate-file-name (or (cdr (assq 'workDir session)) ""))) + (project (agent-shell-kimi--session-project-name session)) + (mtime (agent-shell-kimi--session-mtime session))) + (format "%s %s %s %s" + project + (agent-shell-kimi--relative-time mtime) + dir + (or id "")))) + +(defun agent-shell-kimi--session-current-p (session) + "Return non-nil if SESSION's workDir is under `default-directory'." + (let ((dir (cdr (assq 'workDir session))) + (cwd (expand-file-name default-directory))) + (and dir + (string-prefix-p cwd (expand-file-name dir))))) + +(defun agent-shell-kimi-clean-session-index () + "Remove session index entries whose working directory no longer exists. + +Returns the number of removed entries. The original index file is +renamed with a timestamp backup before rewriting." + (interactive) + (unless (file-exists-p agent-shell-kimi-session-index) + (user-error "Session index not found: %s" agent-shell-kimi-session-index)) + (let* ((sessions (agent-shell-kimi--read-jsonl agent-shell-kimi-session-index)) + (valid (seq-filter (lambda (s) + (let ((dir (cdr (assq 'workDir s)))) + (and dir (file-directory-p dir)))) + sessions)) + (removed (- (length sessions) (length valid)))) + (when (> removed 0) + (rename-file agent-shell-kimi-session-index + (format "%s.bak-%s" agent-shell-kimi-session-index + (format-time-string "%Y%m%d-%H%M%S")) + t) + (with-temp-file agent-shell-kimi-session-index + (dolist (s valid) + (insert (json-encode s) "\n")))) + (message "Removed %d stale Kimi session(s)" removed) + removed)) + +(defun agent-shell-kimi-resume-session (cleanup) + "Resume a recent Kimi session by selecting from the session index. + +With prefix arg CLEANUP, first remove index entries whose working +directory no longer exists." + (interactive "P") + (when cleanup + (agent-shell-kimi-clean-session-index)) + (let* ((sessions (agent-shell-kimi--read-jsonl agent-shell-kimi-session-index)) + (sessions (seq-filter (lambda (s) + (let ((dir (cdr (assq 'workDir s)))) + (and dir (file-directory-p dir)))) + sessions)) + (sessions (seq-sort-by #'agent-shell-kimi--session-mtime #'> sessions)) + ;; Surface sessions from the current project first. + (sessions (append (seq-filter #'agent-shell-kimi--session-current-p sessions) + (seq-remove #'agent-shell-kimi--session-current-p sessions))) + (choices (mapcar (lambda (s) + (cons (agent-shell-kimi--session-label s) s)) + sessions)) + (choice (completing-read "Resume Kimi session: " choices nil t)) + (session (cdr (assoc choice choices))) + (dir (cdr (assq 'workDir session))) + (id (cdr (assq 'sessionId session)))) + (unless session + (user-error "No session selected")) + (unless (and dir (file-directory-p dir)) + (user-error "Session working directory no longer exists: %s" dir)) + (let ((default-directory dir)) + (agent-shell-start :config (agent-shell-kimi-make-config) + :session-id id)))) + +(defun agent-shell-kimi--welcome-status-line () + "Return a short status line for the Kimi welcome message." + (let ((model (or (agent-shell-kimi--active-model-string) "default")) + (mode (or (agent-shell-kimi--active-mode-string) "default"))) + (concat + (propertize "model" 'font-lock-face 'agent-shell-kimi-tool-face) + ": " model " " + (propertize "mode" 'font-lock-face 'agent-shell-kimi-tool-face) + ": " mode " " + (propertize "send" 'font-lock-face 'agent-shell-kimi-thought-face) " M-RET " + (propertize "interrupt" 'font-lock-face 'agent-shell-kimi-thought-face) " C-c C-c " + (propertize "menu" 'font-lock-face 'agent-shell-kimi-thought-face) " SPC a k"))) + +(defun agent-shell-kimi--welcome-message (config) + "Return Kimi ASCII art using `shell-maker' CONFIG." + (let ((art (agent-shell--indent-string 4 (agent-shell-kimi--ascii-art))) + (message (string-trim-left (shell-maker-welcome-message config) "\n"))) + (concat "\n\n" + art + "\n\n" + " " (agent-shell-kimi--welcome-status-line) "\n\n" + message))) + +(defun agent-shell-kimi--ascii-art () + "Kimi ASCII art." + (let* ((is-dark (eq (frame-parameter nil 'background-mode) 'dark)) + (text (string-trim " + ██╗ ██╗ ██╗ ███╗ ███╗ ██╗ + ██║ ██╔╝ ██║ ████╗ ████║ ██║ + █████╔╝ ██║ ██╔████╔██║ ██║ + ██╔═██╗ ██║ ██║╚██╔╝██║ ██║ + ██║ ██╗ ██║ ██║ ╚═╝ ██║ ██║ + ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ +" "\n"))) + (propertize text 'font-lock-face (if is-dark + '(:foreground "#dddddd" :inherit fixed-pitch) + '(:foreground "#000000" :inherit fixed-pitch))))) + +(defcustom agent-shell-kimi-model-choices + '("kimi-for-coding" "kimi-k2" "kimi-k2-5") + "List of model aliases offered by `agent-shell-kimi-set-model'. + +Set this to the model aliases your Kimi CLI installation supports." + :type '(repeat string) + :group 'agent-shell) + +(defun agent-shell-kimi-set-model (model) + "Interactively set `agent-shell-kimi-model' to MODEL. + +Use `agent-shell-kimi-model-choices' for completion. An empty choice +clears the model and uses Kimi's configured default." + (interactive + (list (completing-read "Kimi model (empty for default): " + agent-shell-kimi-model-choices + nil nil + (or (agent-shell-kimi--active-model-string) "")))) + (setq agent-shell-kimi-model (if (string-empty-p model) nil model)) + (message "Kimi model set to %s" (or agent-shell-kimi-model "default"))) + +(defun agent-shell-kimi-set-directory (dir) + "Add DIR to `agent-shell-kimi-additional-dirs'. + +Existing entries are preserved; duplicates are removed." + (interactive "DAdditional workspace directory: ") + (setq agent-shell-kimi-additional-dirs + (seq-uniq (append agent-shell-kimi-additional-dirs + (list (expand-file-name dir))))) + (message "Kimi additional dirs: %s" agent-shell-kimi-additional-dirs)) + +(defcustom agent-shell-kimi-pulse-on-done + t + "When non-nil, pulse the mode-line when Kimi finishes responding. + +Only applies when the Kimi buffer is not the selected window; the +pulse is meant as a subtle TUI notification." + :type 'boolean + :group 'agent-shell) + +(declare-function shell-maker--announce-response "shell-maker") + +(defun agent-shell-kimi-response-done-pulse (buffer) + "Pulse the mode-line to notify that BUFFER has finished responding. + +Meant to be added as :after advice to `shell-maker--announce-response'." + (when (and agent-shell-kimi-pulse-on-done + (buffer-live-p buffer) + (not (eq buffer (window-buffer (selected-window))))) + (let ((face 'agent-shell-kimi-prompt-face) + (original-face (face-attribute 'mode-line :inherit))) + (set-face-attribute 'mode-line nil :inherit face) + (run-with-timer 0.3 nil + (lambda () + (set-face-attribute 'mode-line nil :inherit original-face)))))) + +(advice-add 'shell-maker--announce-response :after #'agent-shell-kimi-response-done-pulse) + +(provide 'agent-shell-kimi) + +;;; agent-shell-kimi.el ends here |
