agentic.elで起動するプロセスのバッファ名にわかりやすい名前を付ける

OpenAI CodexやAnthropic Claude CodeなどのAIコーディングエージェントを使う時、 agentic.el という自作のEmacs拡張を経由して使っている。この拡張はvtermでバッファを作成し、その上でAIコーディングエージェントをDockerコンテナとして起動する。その際のバッファ名には連番が付くが、バッファ名からではそのエージェントが何のために起動したのか分からなくなってしまう。そこで本稿では、バッファ名の付け方を工夫し分かりやすい文言を付けるように、この自前拡張を修正する。

agentic.elでのバッファ名の付け方

agentic.el は自作のEmacs拡張で、AIコーディングエージェントを使用する際に必要となる事前準備を行いプロセスを起動する。初期化シーケンスでは、大まかに次のようなことを行っている。

  1. 入力に基いて、各AIコーディングエージェントのDockerコンテナを起動するコマンドを作る
  2. vtermを開始する
  3. 1で作成した起動コマンドを2で作成したvtermのバッファに流し込み、AIコーディングエージェントをDocker コンテナとして起動する
  4. 必要に応じて設定を反映させるためのシンボリックリンクを作成する

バッファ名は1ないし2の手順の時に決定しており、たとえば *Agentic: OpenAI Codex*:0 といった形式している。複数のエージェントを起動する場合には、この数字が増えていく。

連番を付けるだけでは何の作業をしているのか分からなくなる

複数のエージェントを起動する場合には、この数字が増えていく。これはこれで悪くはないのだが、そのバッファが何の作業をしているのかということをバッファ名から判断することができない。平行して起動するエージェントの数が10を越えるような状況では、それが何の作業か、何が目的なのか、そして僕が誰なのか、何も分からなくなってくる。苦しい。そこで、そのバッファ名に分かりやすい説明を付け加える事にすることにした。

説明の文章はTITLEを探す

僕の作業の場合、作業ごとにindex.ja.org、index.org、README.org、PROJECT.orgといたファイルを作って作業している。そして、それらのファイルのどこかには #+TITLE: という org-mode 形式の属性を記述している。そのタイトルこそが、今回の作業のテーマと言える。2.の手順でvtermを開始する前に、これらのファイルの中から #+TITLE: を検索し、見つけ出した文字列を起動バッファ名の文字列の後ろに結合すれば良さそうだ。

実装

agentic.el に2つの関数を追加し、既存のバッファ名生成箇所を差し替えた。

追加した関数

agentic--get-org-title

カレントディレクトリにある index.ja.org, index.org, README.org, PROJECT.org をこの順に探索し、最初に見つかった #+TITLE: の値を返す関数。

(defun agentic--get-org-title ()
  "カレントディレクトリのorgファイルから#+TITLE:を検索して返す。
index.ja.org, index.org, README.org, PROJECT.orgの順に検索する。"
  (let ((title nil)
        (candidates '("index.ja.org" "index.org" "README.org" "PROJECT.org")))
    (catch 'found
      (dolist (filename candidates)
        (let ((filepath (expand-file-name filename default-directory)))
          (when (file-readable-p filepath)
            (with-temp-buffer
              (insert-file-contents filepath)
              (goto-char (point-min))
              (when (re-search-forward "^#\\+TITLE: *\\(.+\\)$" nil t)
                (setq title (string-trim (match-string 1)))
                (throw 'found title)))))))
    title))

ファイルを順番に試し、 catch/throw で最初に見つかった時点で探索を打ち切る。タイトルが見つからない場合は nil を返す。

agentic--new-buffer-name

base-name を受け取り、 *base*:NN: タイトル 形式のユニークなバッファ名を返す。既存のバッファと衝突しない番号を0から順に探す。タイトルが取得できない場合は *base*:NN 形式にフォールバックする。

(defun agentic--new-buffer-name (base-name)
  "BASE-NAMEとカレントディレクトリのタイトルを組み合わせた新しいバッファ名を返す。
形式: *Agentic: XXX*:NN: タイトル"
  (let ((title (agentic--get-org-title))
        (n 0)
        result)
    (while (progn
             (setq result
                   (if title
                       (format "%s:%02d: %s" base-name n title)
                     (format "%s:%02d" base-name n)))
             (get-buffer result))
      (setq n (1+ n)))
    result))

たとえば base-name*Agentic: OpenAI CodeX* でタイトルが foo であれば、1つ目は *Agentic: OpenAI CodeX*:00: foo 、2つ目は *Agentic: OpenAI CodeX*:01: foo となる。

差分

--- a/agentic.el
+++ b/agentic.el
@@ -18,6 +18,31 @@
     volumes))


+(defun agentic--get-org-title ()
+  "カレントディレクトリのorgファイルから#+TITLE:を検索して返す。
+index.ja.org, index.org, README.org, PROJECT.orgの順に検索する。"
+  (let ((title nil)
+        (candidates '("index.ja.org" "index.org" "README.org" "PROJECT.org")))
+    (catch 'found
+      (dolist (filename candidates)
+        (let ((filepath (expand-file-name filename default-directory)))
+          (when (file-readable-p filepath)
+            (with-temp-buffer
+              (insert-file-contents filepath)
+              (goto-char (point-min))
+              (when (re-search-forward "^#\\+TITLE: *\\(.+\\)$" nil t)
+                (setq title (string-trim (match-string 1)))
+                (throw 'found title)))))))
+    title))
+
+(defun agentic--new-buffer-name (base-name)
+  "BASE-NAMEとカレントディレクトリのタイトルを組み合わせた新しいバッファ名を返す。
+形式: *Agentic: XXX*:NN: タイトル"
+  (let ((title (agentic--get-org-title))
+        (n 0)
+        result)
+    (while (progn
+             (setq result
+                   (if title
+                       (format "%s:%02d: %s" base-name n title)
+                     (format "%s:%02d" base-name n)))
+             (get-buffer result))
+      (setq n (1+ n)))
+    result))
+
+
 (defun agentic--create-instance-old (new-buffer-name)
@@ -55,7 +80,7 @@
   (switch-to-buffer
    (if (string-equal buf-name "[ New ]") ;;
        (agentic--create-instance-old
-	(get-new-buffer-name "*Agentic: OpenAI CodeX*"))
+	(agentic--new-buffer-name "*Agentic: OpenAI CodeX*"))
      (get-buffer buf-name))))

 ;;;###autoload
@@ -73,7 +98,7 @@
   (if (string-equal buf-name "[ New ]")
       (with-current-buffer (vterm)
-	(rename-buffer (get-new-buffer-name "*Agentic: OpenAI CodeX*"))
+	(rename-buffer (agentic--new-buffer-name "*Agentic: OpenAI CodeX*"))
 	(vterm-send-string
 	 (concat
 	  "docker run -it "
@@ -127,7 +152,7 @@
   (switch-to-buffer
    (if (string-equal buf-name "[ New ]") ;;
        (agentic--anthropic-claudecode-create-instance
-	(get-new-buffer-name "*Agentic: Anthropic ClaudeCode*"))
+	(agentic--new-buffer-name "*Agentic: Anthropic ClaudeCode*"))
      (get-buffer buf-name))))

 (defun agentic--google-gemini-create-instance (new-buffer-name)
@@ -166,7 +191,7 @@
   (switch-to-buffer
    (if (string-equal buf-name "[ New ]") ;;
        (agentic--google-gemini-create-instance
-	(get-new-buffer-name "*Agentic: Google Gemini*"))
+	(agentic--new-buffer-name "*Agentic: Google Gemini*"))
      (get-buffer buf-name))))


@@ -206,7 +231,7 @@
   (switch-to-buffer
    (if (string-equal buf-name "[ New ]") ;;
        (agentic--new-instance
-	(get-new-buffer-name "*Agentic: GitHub Copilot*")
+	(agentic--new-buffer-name "*Agentic: GitHub Copilot*")
 	"$HOME/.github:/root/.github"
 	"sximada/agentic/all:202604 "
 	"copilot"

まとめ

agentic.elagentic--get-org-titleagentic--new-buffer-name の2関数を追加し、各エージェント起動関数の get-new-buffer-name 呼び出しを agentic--new-buffer-name に差し替えた。

カレントディレクトリに index.ja.org などの org-mode ファイルが存在し #+TITLE: が記述されていれば、vterm バッファ名に自動でそのタイトルが付加される。10個以上のエージェントを並行起動していても、バッファ名を見ればどの作業かが一目でわかるようになった。

タイトルが見つからない場合は従来どおりの連番バッファ名にフォールバックするため、既存の動作との互換性も保たれている。