forked from xenodium/agent-shell
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent-shell.el
More file actions
7008 lines (6423 loc) · 342 KB
/
agent-shell.el
File metadata and controls
7008 lines (6423 loc) · 342 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; agent-shell.el --- Native agentic integrations for Claude Code, Gemini CLI, etc -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Alvaro Ramirez
;; Author: Alvaro Ramirez https://xenodium.com
;; URL: https://github.com/xenodium/agent-shell
;; Version: 0.50.1
;; Package-Requires: ((emacs "29.1") (shell-maker "0.90.1") (acp "0.11.1"))
(defconst agent-shell--version "0.50.1")
;; 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:
;;
;; `agent-shell' offers a native `comint' shell experience to
;; interact with any agent powered by ACP (Agent Client Protocol).
;;
;; `agent-shell' currently provides access to Claude Code, Cursor,
;; Gemini CLI, Goose, Codex, OpenCode, Qwen, and Auggie amongst other agents.
;;
;; This package depends on the `acp' package to provide the ACP layer
;; as per https://agentclientprotocol.com spec.
;;
;; Report issues at https://github.com/xenodium/agent-shell/issues
;;
;; ✨ Support this work https://github.com/sponsors/xenodium ✨
;;; Code:
(require 'acp)
(eval-when-compile
(require 'cl-lib))
(require 'dired)
(require 'diff)
(require 'json)
(require 'map)
(unless (require 'markdown-overlays nil 'noerror)
(error "Please update 'shell-maker' to v0.90.1 or newer"))
(require 'agent-shell-anthropic)
(require 'agent-shell-auggie)
(require 'agent-shell-cline)
(require 'agent-shell-completion)
(require 'agent-shell-cursor)
(require 'agent-shell-devcontainer)
(require 'agent-shell-diff)
(require 'agent-shell-experimental)
(require 'agent-shell-droid)
(require 'agent-shell-github)
(require 'agent-shell-google)
(require 'agent-shell-goose)
(require 'agent-shell-heartbeat)
(require 'agent-shell-active-message)
(require 'agent-shell-alert)
(require 'agent-shell-kiro)
(require 'agent-shell-mistral)
(require 'agent-shell-openai)
(require 'agent-shell-opencode)
(require 'agent-shell-pi)
(require 'agent-shell-project)
(require 'agent-shell-qwen)
(require 'agent-shell-styles)
(require 'agent-shell-usage)
(require 'agent-shell-worktree)
(require 'agent-shell-ui)
(require 'agent-shell-viewport)
(require 'image)
(require 'markdown-overlays)
(require 'shell-maker)
(require 'svg nil :noerror)
(require 'transient)
;; Optional flycheck integration (used in agent-shell--get-flycheck-error-context)
(declare-function flycheck-overlay-errors-at "flycheck" (pos))
(declare-function flycheck-error-pos "flycheck" (err))
(declare-function flycheck-error-end-line "flycheck" (err))
(declare-function flycheck-error-end-column "flycheck" (err))
(declare-function flycheck-error-level "flycheck" (err))
(declare-function flycheck-error-message "flycheck" (err))
(declare-function flycheck-error-line "flycheck" (err))
(declare-function flycheck-error-column "flycheck" (err))
;; Declare as special so byte-compilation doesn't turn `let' bindings into
;; lexical bindings (which would not affect `auto-insert' behavior).
(defvar auto-insert)
(defcustom agent-shell-permission-icon "⚠"
"Icon displayed when shell commands require permission to execute.
You may use \"\" as an SF Symbol on macOS."
:type 'string
:group 'agent-shell)
(defcustom agent-shell-thought-process-icon "💡"
"Icon displayed during the AI's thought process.
You may use \"\" as an SF Symbol on macOS."
:type 'string
:group 'agent-shell)
(defcustom agent-shell-thought-process-expand-by-default nil
"Whether thought process sections should be expanded by default.
When nil (the default), thought process sections are collapsed.
When non-nil, thought process sections are expanded."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-tool-use-expand-by-default nil
"Whether tool use sections should be expanded by default.
When nil (the default), tool use sections are collapsed.
When non-nil, tool use sections are expanded."
:type 'boolean
:group 'agent-shell)
(defvar agent-shell-mode-hook nil
"Hook run after an `agent-shell-mode' buffer is fully initialized.
Runs after the buffer-local state has been set up, so it is safe to
call `agent-shell-subscribe-to' from here.")
(defvar agent-shell-permission-responder-function nil
"When non-nil, a function called before showing the permission prompt.
Return non-nil to indicate the request was handled (UI is skipped).
Return nil to fall back to the interactive permission dialog.
Called with an alist containing:
:tool-call - the tool call alist with :title, :kind, :status,
:permission-request-id, and optionally :diff
:options - enriched actions, each with :kind, :option-id,
:label, :option, :char
:respond - function taking an option-id to respond programmatically
See `agent-shell-permission-allow-always' for a built-in handler
that auto-approves all requests.
Example -- auto-approve reads:
(setq agent-shell-permission-responder-function
(lambda (permission)
(when-let (((equal (map-elt (map-elt permission :tool-call) :kind)
\"read\"))
(choice (seq-find
(lambda (option)
(equal (map-elt option :kind) \"allow_once\"))
(map-elt permission :options))))
(funcall (map-elt permission :respond)
(map-elt choice :option-id))
t)))")
(defun agent-shell-permission-allow-always (permission)
"Auto-approve all PERMISSION requests.
Intended for use with `agent-shell-permission-responder-function'.
Example:
(setq agent-shell-permission-responder-function
#\\='agent-shell-permission-allow-always)"
(when-let ((choice (seq-find
(lambda (option) (equal (map-elt option :kind) "allow_once"))
(map-elt permission :options))))
(funcall (map-elt permission :respond)
(map-elt choice :option-id))
t))
(defcustom agent-shell-user-message-expand-by-default nil
"Whether user message sections should be expanded by default.
When nil (the default), user message sections are collapsed.
When non-nil, user message sections are expanded."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-show-config-icons t
"Whether to show icons in agent config selection."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-path-resolver-function nil
"Function for resolving remote paths on the local file-system, and vice versa.
Expects a function that takes the path as its single argument, and
returns the resolved path. Set to nil to disable mapping."
:type 'function
:group 'agent-shell)
(defvaralias
'agent-shell-container-command-runner
'agent-shell-command-prefix)
(defcustom agent-shell-command-prefix nil
"Prefix to apply when executing agent commands and shell commands.
Can be a list of strings or a function or lambda that takes a buffer and
returns a list of strings.
Example for static list of strings:
\\='(\"devcontainer\" \"exec\" \"--workspace-folder\" \".\")
Example for a lambda:
(lambda (buffer)
(let ((config (agent-shell-get-config buffer)))
(pcase (map-elt config :identifier)
(\\='claude-code \\='(\"docker\" \"exec\" \"claude-dev\" \"--\"))
(\\='gemini-cli \\='(\"docker\" \"exec\" \"gemini-dev\" \"--\"))
(_ (error \"Unknown identifier\")))))"
:type '(choice (repeat string) function)
:group 'agent-shell)
(defcustom agent-shell-section-functions nil
"Abnormal hook run after overlays are applied (experimental).
Called in `agent-shell--update-fragment' after all overlays
are applied. Each function is called with a range alist containing:
:block - The block range with :start and :end positions
:body - The body range (if present)
:label-left - The left label range (if present)
:label-right - The right label range (if present)
:padding - The padding range with :start and :end (if present)"
:type 'hook
:group 'agent-shell)
(defcustom agent-shell-highlight-blocks nil
"Whether or not to highlight source blocks.
Highlighting source blocks is currently turned off by default
as we need a more efficient mechanism.
See https://github.com/xenodium/agent-shell/issues/119"
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-confirm-interrupt t
"Whether to prompt for confirmation before interrupting.
When non-nil (the default), `agent-shell-interrupt' and related
commands ask \"Interrupt?\" via `y-or-n-p' before cancelling the
in-progress request. Set to nil to interrupt immediately without
prompting."
:type 'boolean
:group 'agent-shell)
(defun agent-shell-interrupt-confirmed-p ()
"Prompt the user to confirm an interrupt and return non-nil if confirmed.
When `agent-shell-confirm-interrupt' is nil, skip the prompt and return t."
(or (not agent-shell-confirm-interrupt)
(y-or-n-p "Interrupt?")))
(defcustom agent-shell-context-sources '(files region error line)
"Sources to consider when determining \\<agent-shell-mode-map>\\[agent-shell] automatic context.
Each element can be:
- A symbol: `files', `region', `error', or `line'
- A function: Called with no arguments, should return context or nil
Sources are checked in order until one returns non-nil."
:type '(repeat (choice (const :tag "Buffer files" files)
(const :tag "Selected region" region)
(const :tag "Error at point" error)
(const :tag "Current line" line)
(function :tag "Custom function")))
:group 'agent-shell)
(cl-defun agent-shell--make-acp-client (&key command
command-params
environment-variables
context-buffer)
"Create an ACP client.
COMMAND, COMMAND-PARAMS, ENVIRONMENT-VARIABLES, and CONTEXT-BUFFER are
passed through to `acp-make-client'."
(let* ((full-command (append (list command) command-params))
(wrapped-command (agent-shell--build-command-for-execution full-command)))
(acp-make-client :command (car wrapped-command)
:command-params (cdr wrapped-command)
:environment-variables environment-variables
:context-buffer context-buffer
:outgoing-request-decorator (when context-buffer
(map-elt (buffer-local-value 'agent-shell--state context-buffer)
:outgoing-request-decorator)))))
(defcustom agent-shell-text-file-capabilities t
"Whether agents are initialized with read/write text file capabilities.
See `acp-make-initialize-request' for details."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-write-inhibit-minor-modes '(aggressive-indent-mode)
"List of minor mode commands to inhibit during `fs/write_text_file' edits.
Each element is a minor mode command symbol, such as
`aggressive-indent-mode'.
Agent Shell disables any listed modes that are enabled in the target
buffer before applying `fs/write_text_file' edits, and then restores
them.
Modes whose variables are not buffer-local in the target buffer (for
example, globalized minor modes) are ignored."
:type '(repeat symbol)
:group 'agent-shell)
(defcustom agent-shell-display-action
'(display-buffer-same-window)
"Display action for agent shell buffers.
See `display-buffer' for the format of display actions."
:type '(cons (repeat function) alist)
:group 'agent-shell)
(defcustom agent-shell-prefer-viewport-interaction nil
"Non-nil makes `agent-shell' prefer viewport interaction over shell interaction.
For example, `agent-shell-send*' will insert text into the viewport
buffer instead of the shell buffer. If no viewport buffer exists, one
will be created."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-embed-file-size-limit 102400
"Maximum file size in bytes for embedding with ContentBlock::Resource.
Files larger than this will use ContentBlock::ResourceLink instead.
Default is 100KB (102400 bytes)."
:type 'integer
:group 'agent-shell)
(defcustom agent-shell-header-style (if (display-graphic-p) 'graphical 'text)
"Style for agent shell buffer headers.
Can be one of:
\='graphical: Display header with icon and styled text.
\='text: Display simple text-only header.
nil: Display no header."
:type '(choice (const :tag "Graphical" graphical)
(const :tag "Text only" text)
(const :tag "No header" nil))
:group 'agent-shell)
(defcustom agent-shell-show-session-id nil
"Non-nil to display the session ID in the header and session selection.
When enabled, the session ID is shown after the directory path in the
header and as an additional column in the session selection prompt.
Only appears when a session is active."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-show-welcome-message t
"Non-nil to show welcome message."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-show-busy-indicator t
"Non-nil to show the busy indicator animation in the header and mode line."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-busy-indicator-frames 'wide
"Frames for the busy indicator animation.
Can be a symbol selecting a predefined style, or a list of frame strings.
When providing custom frames, do not include leading spaces as padding
is added automatically."
:type '(choice (const :tag "Wave (pulses up and down)" wave)
(const :tag "Dots Block (circular spin)" dots-block)
(const :tag "Dots Round (circular spin)" dots-round)
(const :tag "Wide (horizontal blocks)" wide)
(repeat :tag "Custom frames" string))
:group 'agent-shell)
(defcustom agent-shell-screenshot-command
(if (eq system-type 'darwin)
'("/usr/sbin/screencapture" "-i")
;; ImageMagick is common on Linux and many other *nix systems.
'("/usr/bin/import"))
"The program to use for capturing screenshots.
Assume screenshot file path will be appended to this list."
:type '(repeat string)
:group 'agent-shell)
(defcustom agent-shell-clipboard-image-handlers
(list
(list (cons :command "wl-paste")
(cons :save (lambda (file-path)
(with-temp-buffer
(let* ((coding-system-for-read 'binary)
(exit-code (call-process "wl-paste" nil (list t nil) nil "--type" "image/png")))
(if (zerop exit-code)
(write-region nil nil file-path)
(error "Command wl-paste failed with exit code %d" exit-code)))))))
(list (cons :command "pngpaste")
(cons :save (lambda (file-path)
(let ((exit-code (call-process "pngpaste" nil nil nil file-path)))
(unless (zerop exit-code)
(error "Command pngpaste failed with exit code %d" exit-code))))))
(list (cons :command "xclip")
(cons :save (lambda (file-path)
(when-let* ((targets (and (eq (window-system) 'x)
(gui-get-selection 'CLIPBOARD 'TARGETS)))
((vectorp targets))
((not (seq-contains-p targets 'image/png))))
(error "No image/png in clipboard"))
(with-temp-buffer
(set-buffer-multibyte nil)
(let ((exit-code (call-process "xclip" nil t nil
"-selection" "clipboard"
"-t" "image/png" "-o")))
(unless (zerop exit-code)
(error "Command xclip failed with exit code %d" exit-code))
(write-region (point-min) (point-max) file-path nil 'silent)))))))
"Handlers for saving clipboard images to a file.
Each handler is an alist with the following keys:
:command The executable name to look up via `executable-find'.
:save A function taking FILE-PATH that saves the clipboard
image there, signaling an error on failure.
Handlers are tried in order. The first whose :command is found
on the system is used."
:type '(repeat (alist :key-type symbol :value-type sexp))
:group 'agent-shell)
(defcustom agent-shell-buffer-name-format 'default
"Format to use when generating agent shell buffer names.
Each element can be:
- Default: For example \='Claude Agent @ My Project\='
- Kebab case: For example \='claude-agent @ my-project\='
- A function: Called with agent name and project name."
:type '(choice (const :tag "Default" default)
(const :tag "Kebab case" kebab-case)
(function :tag "Custom format"))
:group 'agent-shell)
;;;###autoload
(cl-defun agent-shell-make-agent-config (&key identifier
mode-line-name welcome-function
buffer-name shell-prompt shell-prompt-regexp
client-maker
needs-authentication
authenticate-request-maker
default-model-id
default-session-mode-id
icon-name
install-instructions)
"Create an agent configuration alist.
Keyword arguments:
- IDENTIFIER: Symbol identifying agent type (e.g., \\='claude-code)
- MODE-LINE-NAME: Name to display in the mode line
- WELCOME-FUNCTION: Function to call for welcome message
- BUFFER-NAME: Name of the agent buffer
- SHELL-PROMPT: The shell prompt string
- SHELL-PROMPT-REGEXP: Regexp to match the shell prompt
- CLIENT-MAKER: Function to create the client
- NEEDS-AUTHENTICATION: Non-nil authentication is required
- AUTHENTICATE-REQUEST-MAKER: Function to create authentication requests
- DEFAULT-MODEL-ID: Default model ID (function returning value).
- DEFAULT-SESSION-MODE-ID: Default session mode ID (function returning value).
- ICON-NAME: Name of the icon to use
- INSTALL-INSTRUCTIONS: Instructions to show when executable is not found
Returns an alist with all specified values."
`((:identifier . ,identifier)
(:mode-line-name . ,mode-line-name)
(:welcome-function . ,welcome-function) ;; function
(:buffer-name . ,buffer-name)
(:shell-prompt . ,shell-prompt)
(:shell-prompt-regexp . ,shell-prompt-regexp)
(:client-maker . ,client-maker) ;; function
(:needs-authentication . ,needs-authentication)
(:authenticate-request-maker . ,authenticate-request-maker) ;; function
(:default-model-id . ,default-model-id) ;; function
(:default-session-mode-id . ,default-session-mode-id) ;; function
(:icon-name . ,icon-name)
(:install-instructions . ,install-instructions)))
(defun agent-shell--make-default-agent-configs ()
"Create a list of default agent configs.
This function aggregates agents from OpenAI, Anthropic, Google,
Goose, Cursor, Auggie, and others."
(list (agent-shell-auggie-make-agent-config)
(agent-shell-anthropic-make-claude-code-config)
(agent-shell-cline-make-agent-config)
(agent-shell-openai-make-codex-config)
(agent-shell-cursor-make-agent-config)
(agent-shell-droid-make-agent-config)
(agent-shell-github-make-copilot-config)
(agent-shell-google-make-gemini-config)
(agent-shell-goose-make-agent-config)
(agent-shell-kiro-make-config)
(agent-shell-mistral-make-config)
(agent-shell-opencode-make-agent-config)
(agent-shell-pi-make-agent-config)
(agent-shell-qwen-make-agent-config)))
(defcustom agent-shell-agent-configs
(agent-shell--make-default-agent-configs)
"The list of known agent configurations.
See `agent-shell-*-make-*-config' for details."
:type '(repeat (alist :key-type symbol :value-type sexp))
:group 'agent-shell)
(defcustom agent-shell-preferred-agent-config nil
"Default agent to use for all new shells.
If this is set, `agent-shell' will unconditionally use this
agent and not prompt you to select one.
Can be set to a symbol identifier (e.g., `claude-code') or a full
configuration alist for backwards compatibility."
:type '(choice (const :tag "None (prompt each time)" nil)
(const :tag "Auggie" auggie)
(const :tag "Claude Code" claude-code)
(const :tag "Cline" cline)
(const :tag "Codex" codex)
(const :tag "Copilot" copilot)
(const :tag "Cursor" cursor)
(const :tag "Droid" droid)
(const :tag "Gemini CLI" gemini-cli)
(const :tag "Goose" goose)
(const :tag "Kiro" kiro)
(const :tag "Mistral" le-chat)
(const :tag "OpenCode" opencode)
(const :tag "Pi" pi)
(const :tag "Qwen Code" qwen-code)
(symbol :tag "Custom identifier")
(alist :tag "Full configuration (legacy)"
:key-type symbol :value-type sexp))
:group 'agent-shell)
(defcustom agent-shell-prefer-session-resume t
"Prefer ACP session resume over session load when both are available.
When non-nil (and supported by agent), prefer ACP session resumes over loading."
:type 'boolean
:group 'agent-shell)
(defcustom agent-shell-session-strategy 'prompt
"How to handle sessions when starting a new shell.
Available values:
`new-deferred': Start a new session, but defer initialization until the
first prompt is submitted.
`new': Always start a new session.
`latest': Always load/resume the latest session.
`prompt': Always prompt to choose a session (or start a new one)."
:type '(choice (const :tag "New session, deferred init" new-deferred)
(const :tag "Always start new session" new)
(const :tag "Load latest session" latest)
(const :tag "Prompt for session" prompt))
:group 'agent-shell)
(defcustom agent-shell-outgoing-request-decorator nil
"Function to decorate outgoing ACP requests before they are sent.
When non-nil, this function is called with each outgoing request alist
and must return the (possibly modified) request. This is useful for
injecting agent-specific metadata (e.g. system prompt extensions) into
requests.
The function receives the full request alist (with :method, :params, etc.)
and should return the decorated request. Returning nil is treated as an
error and the original request is sent unchanged.
This is passed through to `acp-make-client' as :outgoing-request-decorator.
The keyword argument to `agent-shell-start' takes precedence over this
variable when both are set."
:type '(choice (const :tag "None" nil)
function)
:group 'agent-shell)
(defun agent-shell--resolve-preferred-config ()
"Resolve `agent-shell-preferred-agent-config' to a full configuration.
If the value is a symbol, look it up in `agent-shell-agent-configs'.
If it's already an alist (legacy format), return it as-is.
Returns nil if no matching configuration is found."
(cond
((null agent-shell-preferred-agent-config) nil)
((symbolp agent-shell-preferred-agent-config)
(seq-find (lambda (config)
(eq (map-elt config :identifier)
agent-shell-preferred-agent-config))
agent-shell-agent-configs))
((listp agent-shell-preferred-agent-config)
agent-shell-preferred-agent-config)))
(defcustom agent-shell-mcp-servers nil
"List of MCP servers to initialize when creating a new session.
Each element should be an alist representing an MCP server configuration
following the ACP schema for McpServer as defined at:
https://agentclientprotocol.com/protocol/schema#mcpserver
The schema supports three transport variants:
1. Stdio Transport (universally supported):
((name . \"server-name\")
(command . \"/path/to/executable\")
(args . (\"arg1\" \"arg2\"))
(env . (((name . \"ENV_VAR\") (value . \"value\")))))
2. HTTP Transport (requires mcpCapabilities.http):
((name . \"server-name\")
(type . \"http\")
(url . \"https://example.com/mcp\")
(headers . (((name . \"Authorization\") (value . \"Bearer token\")))))
3. SSE Transport (requires mcpCapabilities.sse):
((name . \"server-name\")
(type . \"sse\")
(url . \"https://example.com/mcp\")
(headers . (((name . \"Authorization\") (value . \"Bearer token\")))))
Example configuration with multiple servers:
(setq agent-shell-mcp-servers
\='(((name . \"notion\")
(type . \"http\")
(url . \"https://mcp.notion.com/mcp\")
(headers . ()))
((name . \"filesystem\")
(command . \"npx\")
(args . (\"-y\"
\"@modelcontextprotocol/server-filesystem\" \"/tmp\"))
(env . ()))))
Lambdas can be used anywhere in the configuration hierarchy for dynamic
evaluation at session startup time. This is useful for values that
depend on runtime context like the current working directory
\(`agent-shell-cwd'). Note: only lambdas are evaluated, not named
functions, to avoid accidentally calling external symbols.
For example, using the `claude-code-ide' package (see its documentation
for more details), you can embed a lambda for the URL that registers
the session and returns the appropriate endpoint:
(setq agent-shell-mcp-servers
\='(((name . \"emacs\")
(type . \"http\")
(headers . ())
(url . (lambda ()
(require \='claude-code-ide-mcp-server)
(let* ((project-dir (agent-shell-cwd))
(session-id (format \"agent-shell-%s-%s\"
(file-name-nondirectory
(directory-file-name project-dir))
(format-time-string \"%Y%m%d-%H%M%S\"))))
(puthash session-id `(:project-dir ,project-dir)
claude-code-ide-mcp-server--sessions)
(format \"http://localhost:%d/mcp/%s\"
(claude-code-ide-mcp-server-ensure-server)
session-id)))))))"
:type '(repeat (choice (alist :key-type symbol :value-type sexp) function))
:group 'agent-shell)
;;; Debug logging
(defvar agent-shell-logging-enabled nil
"When non-nil, write debug messages to the log buffer.")
(defvar agent-shell--log-buffer-max-bytes (* 100 1000 1000)
"Maximum size of the log buffer in bytes.")
(defun agent-shell--make-log-buffer (shell-buffer)
"Create a log buffer for SHELL-BUFFER.
The name is derived from SHELL-BUFFER's name at creation time."
(let ((name (format "%s log*" (string-remove-suffix
"*" (buffer-name shell-buffer)))))
(with-current-buffer (get-buffer-create name)
(buffer-disable-undo)
(current-buffer))))
(defun agent-shell--log (label format-string &rest args)
"Log message with LABEL using FORMAT-STRING and ARGS.
Does nothing unless `agent-shell-logging-enabled' is non-nil.
Must be called from an agent-shell-mode buffer."
(when agent-shell-logging-enabled
(when-let ((log-buffer (map-elt (agent-shell--state) :log-buffer)))
(when (buffer-live-p log-buffer)
(let ((body (apply #'format format-string args)))
(with-current-buffer log-buffer
(goto-char (point-max))
(let ((entry-start (point)))
(insert (if label
(format "%s >\n\n%s\n\n" label body)
(format "%s\n\n" body)))
(when (< entry-start (point))
(add-text-properties entry-start (1+ entry-start)
'(agent-shell-log-boundary t)))))
(agent-shell--trim-log-buffer log-buffer))))))
(defun agent-shell--trim-log-buffer (buffer)
"Trim BUFFER to `agent-shell--log-buffer-max-bytes' at message boundaries."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(save-excursion
(let ((total-bytes (1- (position-bytes (point-max)))))
(when (< agent-shell--log-buffer-max-bytes total-bytes)
(goto-char (byte-to-position (- total-bytes agent-shell--log-buffer-max-bytes)))
(when (get-text-property (point) 'agent-shell-log-boundary)
(forward-char 1))
(delete-region (point-min)
(next-single-property-change
(point) 'agent-shell-log-boundary nil (point-max)))))))))
(defun agent-shell--save-buffer-to-file (buffer file)
"Write contents of BUFFER to FILE if BUFFER is live and non-empty."
(when (and (buffer-live-p buffer)
(< 0 (buffer-size buffer)))
(with-current-buffer buffer
(save-restriction
(widen)
(write-region (point-min) (point-max) file)))
t))
(defun agent-shell-debug-save-to (directory)
"Save debug buffers for the current shell to DIRECTORY.
When called interactively, prompts for a directory.
Writes the following files:
log.txt - agent-shell log buffer contents
shell.txt - shell buffer contents
messages.txt - *Messages* buffer contents"
(interactive
(list (read-directory-name "Save debug logs to: "
(expand-file-name
(format "agent-shell-debug-%s/"
(format-time-string "%Y%m%d-%H%M%S"))
temporary-file-directory))))
(unless directory
(error "directory is required"))
(let ((directory (file-name-as-directory (expand-file-name directory)))
(saved-files nil))
(make-directory directory t)
(when (agent-shell--save-buffer-to-file
(map-elt (agent-shell--state) :log-buffer)
(expand-file-name "log.txt" directory))
(push "log.txt" saved-files))
(when (agent-shell--save-buffer-to-file
(map-elt (agent-shell--state) :buffer)
(expand-file-name "shell.txt" directory))
(push "shell.txt" saved-files))
(when (agent-shell--save-buffer-to-file
(get-buffer "*Messages*")
(expand-file-name "messages.txt" directory))
(push "messages.txt" saved-files))
(when-let ((client (map-elt (agent-shell--state) :client)))
(when (agent-shell--save-buffer-to-file
(acp-traffic-buffer :client client)
(expand-file-name "traffic.txt" directory))
(push "traffic.txt" saved-files))
(when (agent-shell--save-buffer-to-file
(acp-logs-buffer :client client)
(expand-file-name "acp-log.txt" directory))
(push "acp-log.txt" saved-files)))
(if saved-files
(message "Saved %s to %s" (string-join (nreverse saved-files) ", ") directory)
(message "No debug data to save"))
directory))
(cl-defun agent-shell--make-state (&key agent-config buffer client-maker needs-authentication authenticate-request-maker heartbeat outgoing-request-decorator)
"Construct shell agent state with AGENT-CONFIG and BUFFER.
Shell state is provider-dependent and needs CLIENT-MAKER, NEEDS-AUTHENTICATION,
HEARTBEAT, AUTHENTICATE-REQUEST-MAKER, and optionally
OUTGOING-REQUEST-DECORATOR (passed through to `acp-make-client')."
(list (cons :agent-config agent-config)
(cons :buffer buffer)
(cons :log-buffer (when buffer (agent-shell--make-log-buffer buffer)))
(cons :client nil)
(cons :client-maker client-maker)
(cons :outgoing-request-decorator outgoing-request-decorator)
(cons :heartbeat heartbeat)
(cons :initialized nil)
(cons :needs-authentication needs-authentication)
(cons :authenticate-request-maker authenticate-request-maker)
(cons :authenticated nil)
(cons :set-model nil)
(cons :set-session-mode nil)
(cons :session (list (cons :id nil)
(cons :mode-id nil)
(cons :modes nil)))
(cons :last-entry-type nil)
(cons :chunked-group-count 0)
(cons :request-count 0)
(cons :tool-calls nil)
(cons :available-commands nil)
(cons :available-modes nil)
(cons :supports-session-list nil)
(cons :supports-session-load nil)
(cons :supports-session-resume nil)
(cons :supports-session-fork nil)
(cons :resume-session-id nil)
(cons :fork-session-id nil)
(cons :prompt-capabilities nil)
(cons :event-subscriptions nil)
(cons :active-requests nil)
(cons :pending-requests nil)
(cons :usage (list (cons :total-tokens 0)
(cons :input-tokens 0)
(cons :output-tokens 0)
(cons :thought-tokens 0)
(cons :cached-read-tokens 0)
(cons :cached-write-tokens 0)
(cons :context-used 0)
(cons :context-size 0)
(cons :cost-amount 0.0)
(cons :cost-currency nil)))
(cons :idle-notification-timer nil)))
(defvar-local agent-shell--state
(agent-shell--make-state))
(defvar agent-shell-idle-notification-delay 30
"Seconds of idle time before sending a terminal notification.
Defaults to 30. When non-nil, a timer starts each time an agent
turn completes. If the user does not interact with the buffer
within this many seconds, a desktop notification is sent via OSC
escape sequences. Any user input in the buffer cancels the
pending notification. Set to nil to disable idle notifications.")
(defvar-local agent-shell--transcript-file nil
"Path to the shell's transcript file.")
(defvar agent-shell--shell-maker-config nil)
;;;###autoload
(defun agent-shell (&optional arg)
"Start or reuse an existing agent shell.
`agent-shell' carries some DWIM (do what I mean) behaviour.
If in a project without a shell, offer to create one.
If already in a shell, invoke `agent-shell-toggle'.
If a region is active or point is on relevant context (ie.
`dired' files or image buffers), carry them over to the
shell input.
See `agent-shell-context-sources' on how to control DWIM
behaviour.
With \\[universal-argument] prefix ARG, force start a new shell.
With \\[universal-argument] \\[universal-argument] prefix ARG, prompt to pick an existing shell."
(interactive "P")
(cond
((equal arg '(16))
(agent-shell--dwim :switch-to-shell t))
((equal arg '(4))
(agent-shell--dwim :new-shell t))
(t
(agent-shell--dwim))))
(cl-defun agent-shell--dwim (&key config new-shell switch-to-shell)
"Start or reuse an agent shell with DWIM behavior.
CONFIG is the agent configuration to use.
NEW-SHELL when non-nil forces starting a new shell.
SWITCH-TO-SHELL when non-nil prompts to pick an existing shell.
NEW-SHELL and SWITCH-TO-SHELL are mutually exclusive.
This function respects `agent-shell-prefer-viewport-interaction' and
handles viewport mode detection, existing shell reuse, and project context."
(when (and new-shell switch-to-shell)
(error ":new-shell and :switch-to-shell are mutually exclusive"))
(if agent-shell-prefer-viewport-interaction
(if (and (not new-shell)
(or (derived-mode-p 'agent-shell-viewport-view-mode)
(derived-mode-p 'agent-shell-viewport-edit-mode)))
(agent-shell-toggle)
(let* ((shell-buffer
(cond (switch-to-shell
(get-buffer
(completing-read "Switch to shell: "
(mapcar #'buffer-name (or (agent-shell-buffers)
(user-error "No shells available")))
nil t)))
(new-shell
(agent-shell--start :config (or config
(agent-shell--resolve-preferred-config)
(agent-shell-select-config
:prompt "Start new agent: ")
(error "No agent config found"))
:no-focus t
:new-session t))
(t
(agent-shell--shell-buffer))))
(text (agent-shell--context :shell-buffer shell-buffer)))
(if (and (eq (buffer-local-value 'agent-shell-session-strategy shell-buffer) 'prompt)
(not (map-nested-elt (buffer-local-value 'agent-shell--state shell-buffer)
'(:session :id))))
;; Defer viewport display until session is selected.
(agent-shell-subscribe-to
:shell-buffer shell-buffer
:event 'session-selected
:on-event (lambda (_event)
(agent-shell-viewport--show-buffer
:append text
:shell-buffer shell-buffer)))
(agent-shell-viewport--show-buffer
:append text
:shell-buffer shell-buffer))))
(cond (switch-to-shell
(let* ((shell-buffer
(get-buffer
(completing-read "Switch to shell: "
(mapcar #'buffer-name (or (agent-shell-buffers)
(user-error "No shells available")))
nil t)))
(text (agent-shell--context :shell-buffer shell-buffer)))
(agent-shell--display-buffer shell-buffer)
(when text
(agent-shell--insert-to-shell-buffer :text text
:shell-buffer shell-buffer))))
(new-shell
(agent-shell-start :config (or config
(agent-shell--resolve-preferred-config)
(agent-shell-select-config
:prompt "Start new agent: ")
(error "No agent config found"))))
(t
(if (derived-mode-p 'agent-shell-mode)
(let* ((shell-buffer (agent-shell--shell-buffer :no-create t))
(text (agent-shell--context :shell-buffer shell-buffer)))
(agent-shell-toggle)
(when text
(agent-shell--insert-to-shell-buffer :text text
:shell-buffer shell-buffer)))
(let* ((shell-buffer (agent-shell--shell-buffer))
(text (agent-shell--context :shell-buffer shell-buffer)))
(if (and (eq (buffer-local-value 'agent-shell-session-strategy shell-buffer) 'prompt)
(not (map-nested-elt (buffer-local-value 'agent-shell--state shell-buffer)
'(:session :id))))
;; Defer viewport display until session is selected.
(agent-shell-subscribe-to
:shell-buffer shell-buffer
:event 'session-selected
:on-event (lambda (_event)
(agent-shell--display-buffer shell-buffer)
(when text
(agent-shell--insert-to-shell-buffer :text text
:shell-buffer shell-buffer))))
(agent-shell--display-buffer shell-buffer)
(when text
(agent-shell--insert-to-shell-buffer :text text
:shell-buffer shell-buffer)))))))))
;;;###autoload
(defun agent-shell-toggle ()
"Toggle agent shell display."
(interactive)
(let ((shell-buffer (if agent-shell-prefer-viewport-interaction
(agent-shell-viewport--buffer)
(or (agent-shell--current-shell)
(seq-first (agent-shell-project-buffers))
(seq-first (agent-shell-buffers))))))
(unless shell-buffer
(user-error "No agent shell buffers available for current project"))
(if-let ((window (get-buffer-window shell-buffer)))
(if (and (> (count-windows) 1)
(not (bound-and-true-p transient--prefix)))
(delete-window window)
(switch-to-prev-buffer))
(agent-shell--display-buffer shell-buffer))))
;;;###autoload
(defun agent-shell-new-shell ()
"Start a new agent shell.
Always prompts for agent selection, even if existing shells are available."
(interactive)
(agent-shell '(4)))
;;;###autoload
(defun agent-shell-new-temp-shell ()
"Start a new agent shell in a temporary directory.