-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcodex.el
More file actions
3073 lines (2762 loc) · 125 KB
/
Copy pathcodex.el
File metadata and controls
3073 lines (2762 loc) · 125 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
;;; codex.el --- Emacs integration for OpenAI Codex CLI -*- lexical-binding: t; -*-
;; Author: Pablo Stafforini
;; Version: 0.4.0
;; Package-Requires: ((emacs "28.1") (transient "0.9.3") (inheritenv "0.2") (eat "0.9.4"))
;; Keywords: tools, ai
;; URL: https://github.com/benthamite/codex
;;; Commentary:
;; An Emacs interface to the OpenAI Codex CLI. This package provides
;; convenient ways to interact with Codex from within Emacs, including
;; sending commands, toggling the Codex window, and accessing slash commands.
;; Modeled after `claude-code.el'.
;;; Code:
;;;; Require dependencies
(require 'transient)
(require 'project)
(require 'cl-lib)
(require 'inheritenv)
(require 'json)
(require 'server)
(require 'seq)
(require 'subr-x)
;;;; Customization groups
(defgroup codex nil
"OpenAI Codex CLI interface for Emacs."
:group 'tools)
(defgroup codex-window nil
"Window management settings for Codex."
:group 'codex)
;;;; Faces
(defface codex-repl-face
nil
"Face for Codex REPL."
:group 'codex)
;;;; Core customization options
(defcustom codex-program "codex"
"Path to the Codex binary."
:type 'string
:group 'codex)
(defcustom codex-program-switches nil
"List of extra CLI flags to pass to terminal Codex sessions."
:type '(repeat string)
:group 'codex)
(defcustom codex-terminal-backend 'eat
"Backend to use for Codex.
The \\='eat and \\='vterm backends run the terminal TUI. The
\\='app-server backend renders Codex protocol events directly in
Emacs."
:type '(radio (const :tag "Eat terminal emulator" eat)
(const :tag "Native app-server renderer" app-server)
(const :tag "Vterm terminal emulator" vterm))
:group 'codex)
(defvar codex-app-server-program-switches)
(defvar codex--app-server-pending-startup-action)
(defvar codex--app-server-pending-startup-session-id)
(defvar codex--app-server-deferred-resume-prompt)
(defvar codex--app-server-thread-id)
(declare-function codex--app-server-ensure-direct-input "codex-app-server" ())
(declare-function codex--app-server-input-active-p "codex-app-server" ())
(declare-function codex--app-server-prompt-input "codex-app-server" ())
(declare-function codex--terminal-prompt-input "codex-eat" ())
(defcustom codex-use-alt-screen nil
"Whether to use Codex's alt-screen TUI.
When nil (default), pass `--no-alt-screen' for inline/scrollback mode.
This is the safer default for Emacs terminal buffers because Codex's
alternate-screen TUI can leave `eat' with stale screen state after
interrupts, prompt editing, or heavy redraws. When non-nil, run Codex
with its default alt-screen TUI."
:type 'boolean
:group 'codex)
(defcustom codex-disable-terminal-resize-reflow t
"Whether to disable Codex's experimental terminal resize reflow.
The Codex CLI feature `terminal_resize_reflow' rebuilds terminal
scrollback after width changes. In Emacs terminal buffers, especially
with `--no-alt-screen', the Emacs buffer is the retained session
history, so CLI-side scrollback rebuilds can make the displayed buffer
diverge from the JSONL transcript. When non-nil, pass
`--disable terminal_resize_reflow' to new Codex sessions."
:type 'boolean
:group 'codex)
(defcustom codex-skill-extra-roots nil
"Extra directories the app server should scan for skills.
Sent with `skills/extraRoots/set' when an app-server thread starts, so
skills kept outside the standard locations become available to the agent.
Without this the agent only sees the roots the CLI discovers itself, even
when an Emacs-side integration knows about others.
Only used by the app-server backend; terminal backends inherit whatever
the CLI discovers."
:type '(repeat directory)
:group 'codex)
(defcustom codex-term-name nil
"Terminal type override to use for Codex REPL.
When nil, Codex uses a backend-appropriate TERM value. This lets eat
advertise its bundled eat-* terminfo instead of an xterm terminfo that
does not describe eat precisely."
:type '(choice (const :tag "Use Codex backend default" nil)
string)
:group 'codex)
(defun codex--legacy-implicit-term-name-p ()
"Return non-nil when `codex-term-name' still has the old implicit default."
(and (equal codex-term-name "xterm-256color")
(not (get 'codex-term-name 'customized-value))
(not (get 'codex-term-name 'saved-value))))
(defun codex--migrate-legacy-term-name ()
"Reset the old implicit `codex-term-name' default to the new backend default."
;; Reloading a newer codex.el over an older one preserves the old defcustom
;; value in memory. Do not let that stale default keep forcing xterm into
;; eat; keep explicit Custom values intact.
(when (codex--legacy-implicit-term-name-p)
(setq codex-term-name nil)))
(codex--migrate-legacy-term-name)
(defcustom codex-startup-delay 0.1
"Delay in seconds after starting Codex before displaying buffer.
This helps fix terminal layout issues that can occur if the buffer
is displayed before Codex is fully initialized."
:type 'number
:group 'codex)
(defcustom codex-confirm-kill t
"Whether to ask for confirmation before killing Codex instances."
:type 'boolean
:group 'codex)
(defcustom codex-newline-keybinding-style 'newline-on-shift-return
"Key binding style for entering newlines and sending messages.
This controls how the return key and its modifiers behave in Codex
buffers:
- \\='newline-on-shift-return: S-return enters a line break, RET sends
the command (default).
- \\='newline-on-alt-return: M-return enters a line break, RET sends
the command.
- \\='shift-return-to-send: RET enters a line break, S-return sends the
command.
- \\='super-return-to-send: RET enters a line break, s-return sends the
command.
`\"S\"' is the shift key. `\"s\"' is the hyper key, which is the
COMMAND key on macOS.
The line-break action is delivered to Codex as Ctrl+J, which the Codex
CLI binds to its `insert_newline' editor action by default."
:type '(choice
(const :tag "Newline on shift-return (S-return for newline, RET to send)"
newline-on-shift-return)
(const :tag "Newline on alt-return (M-return for newline, RET to send)"
newline-on-alt-return)
(const :tag "Shift-return to send (RET for newline, S-return to send)"
shift-return-to-send)
(const :tag "Super-return to send (RET for newline, s-return to send)"
super-return-to-send))
:group 'codex)
;;;; Sandbox and approval customization
(defcustom codex-sandbox-mode nil
"Sandbox mode for Codex.
When nil, the CLI default is used. Otherwise, pass `--sandbox MODE'."
:type '(choice (const :tag "CLI default" nil)
(const :tag "Read-only" read-only)
(const :tag "Workspace write" workspace-write)
(const :tag "Full access (dangerous)" danger-full-access))
:group 'codex)
(defcustom codex-approval-policy nil
"Approval policy for Codex.
When nil, the CLI default is used. Otherwise, pass `--ask-for-approval POLICY'."
:type '(choice (const :tag "CLI default" nil)
(const :tag "Untrusted" untrusted)
(const :tag "On request" on-request)
(const :tag "Never" never))
:group 'codex)
(defcustom codex-full-auto nil
"Whether to bypass approvals and sandboxing for Codex.
When non-nil, overrides sandbox and approval settings."
:type 'boolean
:group 'codex)
;;;; Model and profile customization
(defcustom codex-model nil
"Model override for Codex (e.g., \"gpt-5.4\").
When nil, the CLI default is used."
:type '(choice (const :tag "CLI default" nil) string)
:group 'codex)
(defcustom codex-profile nil
"Config profile name for Codex.
When nil, the CLI default is used."
:type '(choice (const :tag "CLI default" nil) string)
:group 'codex)
(defcustom codex-reasoning-effort nil
"Reasoning effort override for Codex.
When nil, the CLI default is used."
:type '(choice (const :tag "CLI default" nil) string)
:group 'codex)
;;;; Hooks integration customization
(defcustom codex-enable-hooks t
"Whether to auto-configure hooks in config.toml and hooks.json."
:type 'boolean
:group 'codex)
(defcustom codex-hooks-config-path "~/.codex/config.toml"
"Path to the Codex config.toml file."
:type 'string
:group 'codex)
(defcustom codex-hooks-json-path "~/.codex/hooks.json"
"Path to the Codex hooks.json file."
:type 'string
:group 'codex)
(defcustom codex-transcript-sessions-directory "~/.codex/sessions"
"Directory containing Codex JSONL session transcripts."
:type 'directory
:group 'codex)
(defcustom codex-transcript-catch-up-on-stop nil
"Whether Stop hooks append missing final transcript messages.
This repairs Codex terminal buffers when the terminal emulator misses
or corrupts the final TUI output for a turn. The JSONL transcript is
treated as authoritative; catch-up text is appended only when the
message is not already present in the buffer.
When the terminal already shows the start of the message, only the
missing suffix is inserted. Repair text is placed before the active
prompt when one is visible, so it does not appear after typed input."
:type 'boolean
:group 'codex)
(defun codex--migrate-transcript-catch-up-default ()
"Reset old implicit transcript catch-up default to nil."
(unless (or (get 'codex-transcript-catch-up-on-stop 'customized-value)
(get 'codex-transcript-catch-up-on-stop 'saved-value))
(setq-default codex-transcript-catch-up-on-stop nil)
(setq codex-transcript-catch-up-on-stop nil)))
(codex--migrate-transcript-catch-up-default)
(defcustom codex-emacsclient-program nil
"Path to emacsclient for Codex hook dispatch.
When nil, use the first emacsclient found in PATH when hooks are
configured."
:type '(choice (const :tag "Find emacsclient in PATH" nil)
file)
:group 'codex)
;;;; Notification customization
(defcustom codex-enable-notifications t
"Whether to show notifications when Codex finishes and awaits input."
:type 'boolean
:group 'codex)
(defcustom codex-notification-function 'codex-default-notification
"Function to call for notifications.
The function is called with two arguments: TITLE and MESSAGE."
:type 'function
:group 'codex)
;;;; Window management customization
(defcustom codex-no-delete-other-windows nil
"Whether to prevent Codex windows from being deleted by `delete-other-windows'."
:type 'boolean
:group 'codex-window)
(defcustom codex-toggle-auto-select nil
"Whether to automatically select the Codex buffer after toggling it open."
:type 'boolean
:group 'codex-window)
(defcustom codex-optimize-window-resize t
"Whether to optimize terminal window resizing to prevent unnecessary reflows.
When non-nil, terminal reflows are only triggered when the window size
changes."
:type 'boolean
:group 'codex)
;;;; Image support customization
(defcustom codex-default-images nil
"Images to attach at startup via `--image'."
:type '(repeat string)
:group 'codex)
;;;; Emacs hooks
(defcustom codex-start-hook nil
"Hook run after Codex starts."
:type 'hook
:group 'codex)
(defcustom codex-command-submitted-hook nil
"Abnormal hook run before input is submitted to a Codex session.
Each function is called with one argument, the session buffer, with
that buffer current. The hook runs for programmatic submissions via
`codex--send-command-to-buffer', for interactive Return presses via
`codex--terminal-send-return', for `:return' TUI actions, and for
every turn the app-server backend submits through
`codex--app-server-submit-command', including queued turns flushed
after a turn completes and prompts sent from the compose buffer. A
single submission may run the hook more than once: the eat backend
also schedules deferred Return events, and app-server programmatic
sends pass through two chokepoints. Hook functions must be
idempotent."
:type 'hook
:group 'codex)
(defcustom codex-process-environment-functions nil
"Abnormal hook for setting up environment variables for Codex.
Functions receive two arguments: the Codex buffer name and the directory.
Each should return a list of strings in the format \"VAR=VALUE\"."
:type 'hook
:group 'codex)
(defvar codex-event-hook nil
"Hook run when Codex CLI triggers events.
Functions are called with one argument: a plist with :type,
:buffer-name, :json-data, and :args. This is an abnormal hook:
dispatch stops at the first function that returns non-nil, and that
value is returned to the Codex CLI hook process.")
;;;; Forward declarations for flycheck
(declare-function flycheck-overlay-errors-at "flycheck")
(declare-function flycheck-error-filename "flycheck")
(declare-function flycheck-error-line "flycheck")
(declare-function flycheck-error-message "flycheck")
;;;; Forward declarations for server
(defvar server-eval-args-left nil
"Arguments passed to the current `emacsclient --eval' request.")
;;;; Forward declarations for debug
(defvar debug-on-next-call)
;;;; Internal state variables
(defvar codex--directory-buffer-map (make-hash-table :test 'equal)
"Hash table mapping directories to user-selected Codex buffers.")
(defvar codex--managed-advice-refcounts (make-hash-table :test 'equal)
"Reference counts for global advice registrations shared across Codex buffers.")
(defvar codex--window-sizes (make-hash-table :test 'eq :weakness 'key)
"Hash table mapping windows to their last known sizes for Codex terminals.")
(defvar-local codex--managed-advice-specs nil
"Advice registrations owned by the current Codex buffer.")
(defvar-local codex--buffer-directory nil
"Directory associated with the current Codex buffer.")
(defvar-local codex--buffer-instance-name nil
"Instance name associated with the current Codex buffer.")
(defvar-local codex--session-id nil
"Codex session id associated with the current buffer.")
(defvar-local codex--session-transcript-file nil
"JSONL transcript file for the Codex session in the current buffer.")
(defvar-local codex--transcript-last-catch-up-message nil
"Last transcript catch-up message appended to the current buffer.")
(defvar codex--transcript-file-cache (make-hash-table :test 'equal)
"Cache mapping transcript roots and session ids to JSONL transcript files.")
(defvar codex-command-history nil
"History of commands sent to Codex.")
;;;; Key bindings
;;;###autoload
(defvar codex-command-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "/") 'codex-slash-commands)
(define-key map (kbd "b") 'codex-switch-to-buffer)
(define-key map (kbd "B") 'codex-select-buffer)
(define-key map (kbd "c") 'codex)
(define-key map (kbd "R") 'codex-resume)
(define-key map (kbd "f") 'codex-fork)
(define-key map (kbd "i") 'codex-new-instance)
(define-key map (kbd "d") 'codex-start-in-directory)
(define-key map (kbd "e") 'codex-fix-error-at-point)
(define-key map (kbd "k") 'codex-kill)
(define-key map (kbd "K") 'codex-kill-all)
(define-key map (kbd "l") 'codex-redraw)
(define-key map (kbd "m") 'codex-transient)
(define-key map (kbd "n") 'codex-send-escape)
(define-key map (kbd "r") 'codex-send-region)
(define-key map (kbd "s") 'codex-send-command)
(define-key map (kbd "t") 'codex-toggle)
(define-key map (kbd "x") 'codex-send-command-with-context)
(define-key map (kbd "y") 'codex-send-return)
(define-key map (kbd "z") 'codex-toggle-read-only-mode)
(define-key map (kbd "1") 'codex-send-1)
(define-key map (kbd "2") 'codex-send-2)
(define-key map (kbd "3") 'codex-send-3)
(define-key map (kbd "M") 'codex-cycle-permissions)
(define-key map (kbd "o") 'codex-send-buffer-file)
(define-key map (kbd "I") 'codex-send-image)
(define-key map (kbd "E") 'codex-edit-previous-message)
(define-key map (kbd "TAB") 'codex-queue-followup)
map)
"Keymap for Codex commands.")
;;;; Transient menus
;;;###autoload (autoload 'codex-transient "codex" nil t)
(transient-define-prefix codex-transient ()
"Codex command menu."
["Codex Menu"
["Start/Stop Codex"
("c" "Start Codex" codex)
("d" "Start in directory" codex-start-in-directory)
("R" "Resume session" codex-resume)
("f" "Fork session" codex-fork)
("i" "New instance" codex-new-instance)
("k" "Kill Codex" codex-kill)
("K" "Kill all instances" codex-kill-all)]
["Send Commands"
("s" "Send command" codex-send-command)
("x" "Send command with context" codex-send-command-with-context)
("r" "Send region or buffer" codex-send-region)
("o" "Send buffer file" codex-send-buffer-file)
("I" "Send image" codex-send-image)
("e" "Fix error at point" codex-fix-error-at-point)
("/" "Slash commands" codex-slash-commands)]
["Manage Codex"
("t" "Toggle window" codex-toggle)
("b" "Switch to buffer" codex-switch-to-buffer)
("B" "Select from all buffers" codex-select-buffer)
("l" "Redraw terminal" codex-redraw)
("z" "Toggle read-only mode" codex-toggle-read-only-mode)
("M" "Cycle permissions" codex-cycle-permissions :transient t)]
["Quick Responses"
("y" "Send <return>" codex-send-return)
("n" "Send <escape>" codex-send-escape)
("E" "Edit previous message" codex-edit-previous-message)
("TAB" "Queue follow-up" codex-queue-followup)
("1" "Send \"1\"" codex-send-1)
("2" "Send \"2\"" codex-send-2)
("3" "Send \"3\"" codex-send-3)]
["Model & Config"
(codex--infix-model)
(codex--infix-reasoning-effort)
(codex--infix-sandbox-mode)
(codex--infix-approval-policy)
(codex--infix-profile)]])
;;;;; Transient infixes for Model & Config
(transient-define-infix codex--infix-model ()
:class 'transient-lisp-variable
:variable 'codex-model
:key "g m"
:description "Model"
:reader (lambda (_prompt _initial-input _history)
(codex--read-optional-string "Model (empty for default): "
codex-model)))
(transient-define-infix codex--infix-reasoning-effort ()
:class 'transient-lisp-variable
:variable 'codex-reasoning-effort
:key "g e"
:description "Reasoning effort"
:reader (lambda (_prompt _initial-input _history)
(codex--read-optional-string
"Reasoning effort (empty for default): "
codex-reasoning-effort)))
(transient-define-infix codex--infix-sandbox-mode ()
:class 'transient-lisp-variable
:variable 'codex-sandbox-mode
:key "g s"
:description "Sandbox mode"
:reader (lambda (_prompt _initial-input _history)
(let ((choice (completing-read "Sandbox mode: "
'("default" "read-only" "workspace-write" "danger-full-access")
nil t)))
(pcase choice
("default" nil)
("read-only" 'read-only)
("workspace-write" 'workspace-write)
("danger-full-access" 'danger-full-access)))))
(transient-define-infix codex--infix-approval-policy ()
:class 'transient-lisp-variable
:variable 'codex-approval-policy
:key "g a"
:description "Approval policy"
:reader (lambda (_prompt _initial-input _history)
(let ((choice (completing-read "Approval policy: "
'("default" "untrusted" "on-request" "never")
nil t)))
(pcase choice
("default" nil)
("untrusted" 'untrusted)
("on-request" 'on-request)
("never" 'never)))))
(transient-define-infix codex--infix-profile ()
:class 'transient-lisp-variable
:variable 'codex-profile
:key "g p"
:description "Profile"
:reader (lambda (_prompt _initial-input _history)
(codex--read-optional-string "Profile (empty for default): "
codex-profile)))
;;;###autoload (autoload 'codex-slash-commands "codex" nil t)
(transient-define-prefix codex-slash-commands ()
"Codex slash commands menu."
["Slash Commands"
["Core"
("h" "Help" (lambda () (interactive) (codex--do-send-command "/help")))
("c" "Clear" (lambda () (interactive) (codex--do-send-command "/clear")))
("C" "Compact" (lambda () (interactive) (codex--do-send-command "/compact")))
("s" "Status" (lambda () (interactive) (codex--do-send-command "/status")))
("n" "New" (lambda () (interactive) (codex--do-send-command "/new")))
("q" "Quit" (lambda () (interactive) (codex--do-send-command "/quit")))]
["Navigation & Review"
("d" "Diff" (lambda () (interactive) (codex--do-send-command "/diff")))
("r" "Review" (lambda () (interactive) (codex--do-send-command "/review")))
("f" "Fork" (lambda () (interactive) (codex--do-send-command "/fork")))
("R" "Resume" (lambda () (interactive) (codex--do-send-command "/resume")))
("y" "Copy" (lambda () (interactive) (codex--do-send-command "/copy")))]
["Configuration"
("p" "Permissions" (lambda () (interactive) (codex--do-send-command "/permissions")))
("m" "Model" (lambda () (interactive) (codex--do-send-command "/model")))
("F" "Fast" (lambda () (interactive) (codex--do-send-command "/fast")))
("P" "Plan" (lambda () (interactive) (codex--do-send-command "/plan")))
("i" "Init" (lambda () (interactive) (codex--do-send-command "/init")))
("S" "Statusline" (lambda () (interactive) (codex--do-send-command "/statusline")))
("T" "Theme" (lambda () (interactive) (codex--do-send-command "/theme")))
("D" "Debug config" (lambda () (interactive) (codex--do-send-command "/debug-config")))]
["Features & Tools"
("e" "Experimental" (lambda () (interactive) (codex--do-send-command "/experimental")))
("M" "MCP" (lambda () (interactive) (codex--do-send-command "/mcp")))
("a" "Agent" (lambda () (interactive) (codex--do-send-command "/agent")))
("A" "Apps" (lambda () (interactive) (codex--do-send-command "/apps")))
("k" "Skills" (lambda () (interactive) (codex--do-send-command "/skills")))
("g" "Plugins" (lambda () (interactive) (codex--do-send-command "/plugins")))
("H" "Hooks" (lambda () (interactive) (codex--do-send-command "/hooks")))
("@" "Mention" (lambda () (interactive) (codex--do-send-command "/mention")))
("!" "PS" (lambda () (interactive) (codex--do-send-command "/ps")))]
["Account & Identity"
("l" "Logout" (lambda () (interactive) (codex--do-send-command "/logout")))
("Y" "Personality" (lambda () (interactive) (codex--do-send-command "/personality")))
("b" "Feedback" (lambda () (interactive) (codex--do-send-command "/feedback")))
("u" "Usage" (lambda () (interactive) (codex--do-send-command "/usage")))
("x" "Delete thread" (lambda () (interactive) (codex--do-send-command "/delete")))]])
;;;; Terminal abstraction layer
;;;;; Generic function definitions
(cl-defgeneric codex--term-make (backend buffer-name program &optional switches)
"Create a terminal using BACKEND in BUFFER-NAME running PROGRAM.
Optional SWITCHES are command-line arguments to PROGRAM.
Returns the buffer containing the terminal.")
(cl-defgeneric codex--term-send-string (backend string)
"Send STRING to the terminal using BACKEND.")
(cl-defgeneric codex--term-send-action (backend action &optional payload)
"Send terminal ACTION with optional PAYLOAD using BACKEND.")
(cl-defgeneric codex--term-submit-command (backend command)
"Type COMMAND into the current terminal using BACKEND and submit it.")
(cl-defgeneric codex--term-kill-process (backend buffer)
"Kill the terminal process in BUFFER using BACKEND.")
(cl-defgeneric codex--term-read-only-mode (backend)
"Switch current terminal to read-only mode using BACKEND.")
(cl-defgeneric codex--term-interactive-mode (backend)
"Switch current terminal to interactive mode using BACKEND.")
(cl-defgeneric codex--term-in-read-only-p (backend)
"Check if current terminal is in read-only mode using BACKEND.")
(cl-defgeneric codex--term-configure (backend)
"Configure terminal in current buffer with BACKEND specific settings.")
(cl-defgeneric codex--term-customize-faces (backend)
"Apply face customizations for the terminal using BACKEND.")
(cl-defgeneric codex--term-get-adjust-process-window-size-fn (backend)
"Get the BACKEND specific function that adjusts window size.")
(cl-defgeneric codex--term-post-start (backend)
"Run BACKEND specific post-start setup in the current Codex buffer.")
(cl-defgeneric codex--term-cleanup (backend)
"Clean up BACKEND specific buffer-local state before killing the buffer.")
(cl-defmethod codex--term-cleanup (_backend)
"Default cleanup for terminal backends.")
;;;; Private utility functions
(defun codex--shell-command-from-argv (program &optional switches)
"Return a shell-safe command string for PROGRAM.
SWITCHES is an optional list of command-line arguments."
(mapconcat #'shell-quote-argument
(cons program switches)
" "))
(defun codex--read-optional-string (prompt initial-input)
"Read PROMPT with INITIAL-INPUT and return nil for empty input."
(let ((value (read-string prompt initial-input)))
(unless (string-empty-p value)
value)))
(defun codex--acquire-managed-advice (target where function)
"Register FUNCTION as WHERE advice on TARGET for the current buffer."
(let ((spec (list target where function)))
(unless (member spec codex--managed-advice-specs)
(push spec codex--managed-advice-specs)
(let ((count (gethash spec codex--managed-advice-refcounts 0)))
(when (zerop count)
(advice-add target where function))
(puthash spec (1+ count) codex--managed-advice-refcounts)))))
(defun codex--release-managed-advices ()
"Release advice registrations owned by the current buffer."
(dolist (spec codex--managed-advice-specs)
(pcase-let ((`(,target ,_where ,function) spec))
(let ((count (gethash spec codex--managed-advice-refcounts 0)))
(if (> count 1)
(puthash spec (1- count) codex--managed-advice-refcounts)
(remhash spec codex--managed-advice-refcounts)
(advice-remove target function)))))
(setq codex--managed-advice-specs nil))
(defmacro codex--with-buffer (&rest body)
"Execute BODY in the selected Codex buffer and display that buffer."
`(if-let ((codex-buffer (codex--get-or-prompt-for-buffer)))
(with-current-buffer codex-buffer
,@body
(display-buffer codex-buffer))
(codex--show-not-running-message)))
(defun codex--terminal-send-return ()
"Send Return to the current Codex terminal buffer."
(interactive)
(codex--run-command-submitted-hook)
(codex--term-send-action codex-terminal-backend :return))
(defun codex--terminal-insert-newline ()
"Insert a line break in the current Codex prompt."
(interactive)
(codex--term-send-action codex-terminal-backend :newline))
(defun codex--terminal-send-tab ()
"Send Tab to the current Codex terminal buffer."
(interactive)
(codex--term-send-action codex-terminal-backend :tab))
(defun codex--term-setup-keymap (backend)
"Set up the local Codex terminal keymap for BACKEND."
(let ((map (make-sparse-keymap)))
(set-keymap-parent map (current-local-map))
(define-key map (kbd "C-g") #'codex-send-escape)
(define-key map (kbd "C-l") #'codex-redraw)
(define-key map (kbd "C-c C-o") #'codex-app-server-expand-output)
(define-key map (kbd "M-<left>") #'codex-previous-agent)
(define-key map (kbd "M-<right>") #'codex-next-agent)
(define-key map (kbd "TAB") #'codex--terminal-send-tab)
(define-key map [tab] #'codex--terminal-send-tab)
(when (eq backend 'app-server)
(define-key map (kbd "@") #'codex-app-server-insert-file-reference)
(define-key map (kbd "$") #'codex-app-server-insert-mention)
(define-key map (kbd "C-v") #'codex-app-server-paste-image)
(define-key map (kbd "<escape>") #'codex-send-escape)
(define-key map (kbd "C-c C-e") #'codex-app-server-open-editor)
(define-key map (kbd "M-<up>") #'codex-app-server-edit-last-queued)
(define-key map (kbd "C-c C-<up>") #'codex-app-server-reasoning-up)
(define-key map (kbd "C-c C-<down>") #'codex-app-server-reasoning-down)
(define-key map (kbd "M-p") #'codex-app-server-previous-input)
(define-key map (kbd "M-n") #'codex-app-server-next-input)
(define-key map (kbd "C-c C-r") #'codex-app-server-search-input-history))
(codex--term-bind-newline-keys map)
(use-local-map map)))
(defun codex--term-bind-newline-keys (map)
"Bind Codex newline and submit keys in MAP."
(pcase codex-newline-keybinding-style
('newline-on-shift-return
(define-key map (kbd "<S-return>") #'codex--terminal-insert-newline)
(define-key map (kbd "<return>") #'codex--terminal-send-return))
('newline-on-alt-return
(define-key map (kbd "<M-return>") #'codex--terminal-insert-newline)
(define-key map (kbd "<return>") #'codex--terminal-send-return))
('shift-return-to-send
(define-key map (kbd "<return>") #'codex--terminal-insert-newline)
(define-key map (kbd "<S-return>") #'codex--terminal-send-return))
('super-return-to-send
(define-key map (kbd "<return>") #'codex--terminal-insert-newline)
(define-key map (kbd "<s-return>") #'codex--terminal-send-return))))
(defun codex--buffer-p (buffer)
"Return non-nil if BUFFER is a Codex buffer."
(let ((name (cond
((stringp buffer) buffer)
((buffer-live-p buffer) (buffer-name buffer)))))
(when-let* ((parsed (codex--parse-buffer-name name)))
(not (string-empty-p (car parsed))))))
(defun codex--directory ()
"Get the root Codex directory for the current buffer.
If not in a project and no buffer file, return `default-directory'."
(let* ((project (project-current))
(current-file (buffer-file-name)))
(cond
(project (project-root project))
(current-file (file-name-directory current-file))
(t default-directory))))
(defun codex--find-all-codex-buffers ()
"Find all active Codex buffers across all directories."
(cl-remove-if-not #'codex--active-buffer-p (buffer-list)))
(defun codex--active-buffer-p (buffer)
"Return non-nil if BUFFER is an active Codex terminal buffer."
(and (codex--buffer-p buffer)
(codex--buffer-process-live-p buffer)))
(defun codex--buffer-process-live-p (buffer)
"Return non-nil if BUFFER has a live terminal process."
(when (buffer-live-p buffer)
(when-let* ((process (get-buffer-process buffer)))
(process-live-p process))))
(defun codex--buffer-directory-for (buffer)
"Return the directory associated with Codex BUFFER."
(or (buffer-local-value 'codex--buffer-directory buffer)
(codex--extract-directory-from-buffer-name (buffer-name buffer))))
(defun codex--buffer-instance-name-for (buffer)
"Return the instance name associated with Codex BUFFER."
(or (buffer-local-value 'codex--buffer-instance-name buffer)
(codex--extract-instance-name-from-buffer-name (buffer-name buffer))))
(defun codex--find-codex-buffers-for-directory (directory)
"Find all active Codex buffers for a specific DIRECTORY."
(let ((target-dir (file-truename (abbreviate-file-name directory))))
(cl-remove-if-not
(lambda (buf)
(when-let ((buf-dir (codex--buffer-directory-for buf)))
(string= target-dir
(file-truename (abbreviate-file-name buf-dir)))))
(codex--find-all-codex-buffers))))
(defun codex--buffer-name-instance-separator (payload)
"Return the index of the instance separator in Codex buffer PAYLOAD.
PAYLOAD is the text between the `*codex:' prefix and trailing `*'. The
separator is the last colon whose suffix is non-empty and contains no slash or
backslash. This keeps path colons such as `C:/repo' or `/tmp/a:b/' in the
directory part while splitting `/tmp/project/:tests' before `tests'."
(let ((search-end (length payload))
separator)
(while (and (not separator)
(setq separator (cl-position ?: payload :from-end t :end search-end)))
(let ((suffix (substring payload (1+ separator))))
(if (or (string-empty-p suffix)
(string-match-p "[/\\\\]" suffix))
(setq search-end separator
separator nil))))
separator))
(defun codex--parse-buffer-name (buffer-name)
"Parse Codex BUFFER-NAME into (DIRECTORY INSTANCE-NAME)."
(when (and (stringp buffer-name)
(string-prefix-p "*codex:" buffer-name)
(string-suffix-p "*" buffer-name))
(let* ((payload (substring buffer-name (length "*codex:") -1))
(separator (codex--buffer-name-instance-separator payload)))
(list (if separator
(substring payload 0 separator)
payload)
(when separator
(substring payload (1+ separator)))))))
(defun codex--extract-directory-from-buffer-name (buffer-name)
"Extract the directory path from a Codex BUFFER-NAME.
For example, *codex:/path/to/project/:tests* returns /path/to/project/."
(car (codex--parse-buffer-name buffer-name)))
(defun codex--extract-instance-name-from-buffer-name (buffer-name)
"Extract the instance name from a Codex BUFFER-NAME.
For example, *codex:/path/to/project/:tests* returns \"tests\"."
(cadr (codex--parse-buffer-name buffer-name)))
(defun codex--buffer-display-name (buffer)
"Create a display name for Codex BUFFER."
(let* ((dir (codex--buffer-directory-for buffer))
(instance-name (codex--buffer-instance-name-for buffer)))
(if instance-name
(format "%s:%s (%s)"
(file-name-nondirectory (directory-file-name dir))
instance-name
dir)
(format "%s (%s)"
(file-name-nondirectory (directory-file-name dir))
dir))))
(defun codex--buffers-to-choices (buffers &optional simple-format)
"Convert BUFFERS list to an alist of (display-name . buffer) pairs.
If SIMPLE-FORMAT is non-nil, use just the instance name."
(mapcar (lambda (buf)
(let ((display-name (if simple-format
(or (codex--buffer-instance-name-for buf)
"default")
(codex--buffer-display-name buf))))
(cons display-name buf)))
buffers))
(defun codex--select-buffer-from-choices (prompt buffers &optional simple-format)
"Prompt user to select a buffer from BUFFERS list using PROMPT.
If SIMPLE-FORMAT is non-nil, use simplified display names."
(when buffers
(let* ((choices (codex--buffers-to-choices buffers simple-format))
(selection (completing-read prompt
(mapcar #'car choices)
nil t)))
(cdr (assoc selection choices)))))
(defun codex--prompt-for-codex-buffer ()
"Prompt user to select from available Codex buffers."
(let* ((current-dir (codex--directory))
(codex-buffers (codex--find-all-codex-buffers)))
(when codex-buffers
(let* ((prompt (substitute-command-keys
(format "No Codex instance running in %s. Cancel (\\[keyboard-quit]), or select instance: "
(abbreviate-file-name current-dir))))
(selected-buffer (codex--select-buffer-from-choices prompt codex-buffers)))
(when selected-buffer
(puthash current-dir selected-buffer codex--directory-buffer-map))
selected-buffer))))
(defun codex--get-or-prompt-for-buffer ()
"Get Codex buffer for current directory or prompt for selection."
(or (when (codex--active-buffer-p (current-buffer))
(current-buffer))
(let* ((current-dir (codex--directory))
(dir-buffers (codex--find-codex-buffers-for-directory current-dir)))
(cond
((> (length dir-buffers) 1)
(codex--select-buffer-from-choices
(format "Select Codex instance for %s: "
(abbreviate-file-name current-dir))
dir-buffers
t))
((= (length dir-buffers) 1)
(car dir-buffers))
(t
(let ((remembered-buffer (gethash current-dir codex--directory-buffer-map)))
(if (codex--active-buffer-p remembered-buffer)
remembered-buffer
(remhash current-dir codex--directory-buffer-map)
(let ((other-buffers (codex--find-all-codex-buffers)))
(when other-buffers
(codex--prompt-for-codex-buffer))))))))))
(defun codex--buffer-name (&optional instance-name)
"Generate the Codex buffer name based on project or current buffer file.
If INSTANCE-NAME is provided, include it in the buffer name."
(let ((dir (codex--directory)))
(unless dir
(error "Cannot determine Codex directory - no `default-directory'!"))
(codex--buffer-name-for-directory dir instance-name)))
(defun codex--valid-instance-name-p (instance-name)
"Return non-nil if INSTANCE-NAME is safe to encode in a Codex buffer name."
(not (string-match-p "[:*/\\\\\n\r]" instance-name)))
(defun codex--prompt-for-instance-name (dir existing-instance-names &optional force-prompt)
"Prompt user for a new instance name for directory DIR.
EXISTING-INSTANCE-NAMES is a list of existing instance names.
If FORCE-PROMPT is non-nil, always prompt even if no instances exist."
(if (or existing-instance-names force-prompt)
(let ((proposed-name ""))
(while (or (string-empty-p proposed-name)
(not (codex--valid-instance-name-p proposed-name))
(member proposed-name existing-instance-names))
(setq proposed-name
(read-string (if (and existing-instance-names (not force-prompt))
(format "Instances already running for %s (existing: %s), new instance name: "
(abbreviate-file-name dir)
(mapconcat #'identity existing-instance-names ", "))
(format "Instance name for %s: " (abbreviate-file-name dir)))
nil nil proposed-name))
(cond
((string-empty-p proposed-name)
(message "Instance name cannot be empty. Please enter a name.")
(sit-for 1))
((not (codex--valid-instance-name-p proposed-name))
(message "Instance name '%s' contains reserved characters (:, /, \\, *)." proposed-name)
(sit-for 1))
((member proposed-name existing-instance-names)
(message "Instance name '%s' already exists. Please choose a different name." proposed-name)
(sit-for 1))))
proposed-name)
"default"))
(defun codex--show-not-running-message ()
"Show a message that Codex is not running in any directory."
(message "Codex is not running"))
(defun codex--kill-buffer (buffer)
"Kill a Codex BUFFER by cleaning up hooks and processes."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(codex--term-kill-process codex-terminal-backend buffer))))
(defun codex--cleanup-directory-mapping ()
"Remove entries from directory-buffer map when this buffer is killed."
(let ((dying-buffer (current-buffer)))
(maphash (lambda (dir buffer)
(when (eq buffer dying-buffer)
(remhash dir codex--directory-buffer-map)))
codex--directory-buffer-map)))
(defun codex--cleanup-buffer-state ()
"Clean up Codex buffer-local state before the current buffer is killed."
(codex--term-cleanup codex-terminal-backend)
(codex--release-managed-advices)
(codex--cleanup-directory-mapping))
(defun codex--get-buffer-file-name ()
"Get the file name associated with the current buffer."
(when buffer-file-name
(file-local-name (file-truename buffer-file-name))))
(defun codex--format-file-reference (&optional file-name line-start line-end)
"Format a file reference in the @file:line style.
FILE-NAME is the file path. LINE-START is the starting line number.
LINE-END is the ending line number for a range."
(let ((file (or file-name (codex--get-buffer-file-name)))
(start (or line-start (line-number-at-pos nil t)))
(end line-end))
(when file
(if end
(format "@%s:%d-%d" file start end)
(format "@%s:%d" file start)))))
(defun codex--do-send-command (cmd)
"Send command CMD to Codex if a Codex buffer exists.
After sending the command, move point to the end of the buffer."
(if-let ((codex-buffer (codex--get-or-prompt-for-buffer)))
(codex--send-command-to-buffer cmd codex-buffer)
(codex--show-not-running-message)
nil))
(defun codex--send-command-to-buffer (cmd buffer)
"Send command CMD to Codex BUFFER and submit it."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(when (eq codex-terminal-backend 'app-server)
(codex--app-server-ensure-direct-input)))
(codex--run-command-submitted-hook buffer)
(let ((window (or (get-buffer-window buffer)
(display-buffer buffer))))
(if window
(with-selected-window window
(with-current-buffer buffer
(codex--term-submit-command codex-terminal-backend cmd)))
(with-current-buffer buffer
(codex--term-submit-command codex-terminal-backend cmd))))
buffer))
(defun codex--run-command-submitted-hook (&optional buffer)
"Run `codex-command-submitted-hook' for BUFFER or the current buffer."
(let ((target (or buffer (current-buffer))))
(when (buffer-live-p target)
(with-current-buffer target
(run-hook-with-args 'codex-command-submitted-hook target)))))
(defun codex-prompt-input (&optional buffer)
"Return pending prompt input text in BUFFER, or nil when empty.
BUFFER defaults to the current buffer. App-server buffers report the
text after the input marker; terminal buffers parse the prompt line
using `codex--prompt-marker-regexp'. Placeholder autosuggestion text