Skip to content

Commit 1888ee3

Browse files
authored
Feat: Add whisper speech-to-text input command with post-transcription actions for insert, editable send-to-session, and clipboard copy (#268)
* Add Whisper speech-to-text input and tests * Rename menu entry v to Git/GitHub action * update HISTORY * Rename menu label to GitHub PR AI Action * Refactor speech-to-text and lazy-load prompt mode * fix test suites * Add comparison table and clarify differences * Enable AI session links by default
1 parent cd5eb26 commit 1888ee3

18 files changed

Lines changed: 738 additions & 169 deletions

HISTORY.org

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Release history
33

44
** Main branch change
5+
- Feat: Add whisper speech-to-text input command with post-transcription actions for insert, editable send-to-session, and clipboard copy
6+
- Fix: Slowness of ai coding session given clickable session link feature
57

68
** 1.65
79
- Feat: Add agent-shell integration for ai-code-behaviors, by davidwuchn

README.org

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,24 @@ An Emacs interface for AI-assisted software development. *The purpose is to prov
2727

2828
- I switch between different CLI-based AI tools in Emacs: Claude Code / OpenAI Codex / Gemini CLI / etc. If you also use different AI tools inside Emacs, but want to keep the same user interface and experience, this package is for you.
2929

30-
- Lots of features and tools are ported from [[https://github.com/tninja/aider.el][aider.el]]. If you like the features in aider.el, but wish to switch to modern AI coding CLI, this package is also for you.
30+
- If you are deciding between the main AI coding packages inside Emacs, the most important question is this: do you want one consistent coding workflow across many AI tools, or do you want the deepest integration for one specific agent runtime? The table below focuses on the user-visible differences that matter day to day.
31+
32+
| Feature | ai-code-interface.el | claude-code-ide.el | agent-shell | eca-emacs |
33+
|-------------+-----------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------+------------------------------------------------------------------------------|
34+
| Switching | Best when you move between Codex, Claude Code, Gemini CLI, Copilot CLI, and other supported backends without relearning commands | Claude Code only | Many ACP agents, but through one ACP shell model | ECA agents and models inside one ECA workspace |
35+
| Context | Current file, visible buffers, function or region, saved repo context, prompt files, and @ file completion | Active file, selection, project data, diagnostics, and editor-aware mentions | Shell session context, project files, transcripts, screenshots, and clipboard image support | File and range references, repomap, cursor auto-context, and workspace-aware chats |
36+
| Editor tools | Built-in Emacs MCP tools for project info, imenu, xref, and tree-sitter, plus MCP debug and config helpers | Deepest Claude-specific Emacs MCP bridge, including diagnostics and custom Emacs tools exposed to Claude | MCP server configuration is available, but the main focus is the ACP shell session | MCP-aware chat settings and tool-call handling, but centered on the ECA server |
37+
| Actions | Task-first actions such as change code, implement TODO, explain, ask, refactor, and prompt workflows | Claude-first IDE prompting, mentions, and editor-aware operations | Shell-first agent interaction; coding style depends on the ACP agent you connect | Chat-first coding, context injection, and rewrite flows |
38+
| Dev loop | Built-in TDD cycle, diff review, Magit helpers, and refactoring actions | Strong Claude-specific ediff flow with diagnostics-aware review | Strong queue, transcript, model-switching, and container-friendly shell workflow | Strong approvals, rewrite diff, timelines, and saved chats |
39+
| Sessions | One consistent workflow even when the backend changes underneath | Multiple Claude sessions per project | Multiple ACP shells with queueing and transcripts | Multiple chats per workspace with agent and model switching |
40+
| Visibility | Review-oriented commands, prompt files, and backend-aware session buffers | Session list, debug buffer, resume flow, and IDE diff controls | Queue view, transcripts, screenshots, and shell-level visibility | Tool-call approvals, expandable blocks, timeline view, and chat state indicators |
41+
| Best fit | You want one Emacs UX across many AI coding tools | You want the deepest Claude Code integration in Emacs | You want ACP-native shells and transcript-heavy workflows | You want a polished server-backed chat workspace |
42+
43+
- All four packages share the same broad goal: keep AI-assisted coding inside Emacs, preserve project context, and make long-running AI sessions practical. The difference is where they place the center of gravity. =claude-code-ide.el= goes deepest on Claude Code plus MCP-powered editor awareness. =agent-shell= goes deepest on ACP and shell-native agent operations. =eca-emacs= goes deepest on a dedicated server-backed chat UX.
44+
45+
- =ai-code-interface.el= is different because its biggest advantage is *one stable coding workflow across many AI backends*. Unlike =claude-code-ide.el=, which is centered on Claude Code, =agent-shell=, which is centered on ACP shells, or =eca-emacs=, which is centered on the ECA server, =ai-code-interface.el= keeps the same menu, context gathering, prompt construction, refactoring actions, TDD helpers, git-oriented review flow, and Emacs MCP tools while the backend changes underneath. In practice, that means switching from Claude Code to Codex, Gemini CLI, Copilot CLI, or another backend does not force you to relearn how you work inside Emacs.
46+
47+
- That also means this package is a good fit when you do not want to pick a single winner forever. You can keep using specialized backends when they are the right tool for a task, but still drive them from one Emacs-centered interface focused on code changes, review, and iterative development instead of one vendor-specific chat surface.
3148

3249
- Screenshot
3350

@@ -109,6 +126,7 @@ Enable installation of packages from MELPA by adding an entry to package-archive
109126

110127
*** Optional Dependencies
111128
- `helm`: For an enhanced auto-completion experience (`ai-code-input.el`).
129+
- `whisper`: For speech-to-text input via `ai-code-speech-to-text-input`. Record audio, press `RET` to stop, then choose whether to insert the transcription into the current buffer, send it to the active AI coding session with editable initial input, or copy it to the clipboard.
112130
- `gptel`: For intelligent, AI-generated content headlines in the prompt file.
113131
- ai-code-task-use-gptel-filename: When non-nil, file name created by `ai-code-create-or-open-task-file` or `ai-code-create-file-or-dir` will have auto-generated filenames created by GPTel
114132
- ai-code-notes-use-gptel-headline: When non-nil, notes created by `ai-code-take-notes` will have auto-generated headlines created by GPTel
@@ -131,7 +149,7 @@ Enable installation of packages from MELPA by adding an entry to package-archive
131149
[[./clickable_link_ai_session.png]]
132150
- *Context-Aware Code Actions*: The menu exposes dedicated entries for changing code (`c`), implementing TODOs (`i`), asking questions (`q`), explaining code (`x`), sending free-form commands (`<SPC>`), and refreshing AI context (`@`). Each command automatically captures the surrounding function, region, or clipboard contents (via `C-u`) to keep prompts precise.
133151
- *Agile Development Workflows*: Use the refactoring navigator (`r`), the guided TDD cycle (`t`), and the pull/review diff helper (`v`) to keep AI-assisted work aligned with agile best practices. Prompt authoring is first-class through quick access to the prompt file (`p`), build/test helper (`b`), and AI-assisted shell/file execution (`!`). In prompt files, send the current block with `C-c C-c`.
134-
- *Productivity & Debugging Utilities*: Initialize project navigation assets (`.`), investigate exceptions (`e`), auto-fix Flycheck issues in scope (`f`), copy or open file paths formatted for prompts (`k`, `o`), generate MCP inspector commands (`m`), capture session notes straight into Org (`n`), and toggle desktop notifications (`N`) to get alerted when AI responses are ready in background sessions.
152+
- *Productivity & Debugging Utilities*: Initialize project navigation assets (`.`), investigate exceptions (`e`), auto-fix Flycheck issues in scope (`f`), copy or open file paths formatted for prompts (`k`, `o`), generate MCP inspector commands (`m`), capture session notes straight into Org (`n`), dictate prompts with speech-to-text (`:`), and toggle desktop notifications (`N`) to get alerted when AI responses are ready in background sessions.
135153
- *Seamless Prompt Management*: Open the prompt file via `ai-code-open-prompt-file` (stored under `.ai.code.files/.ai.code.prompt.org` by default), send regions with `ai-code-prompt-send-block`, and reuse prompt snippets via `yasnippet` to keep conversations organized.
136154
- *Interactive Chat & Context Tools*: Dedicated buffers hold long-running chats, automatically enriched with file paths, diffs, and history from Magit or Git commands for richer AI responses.
137155
- *AI-Assisted Bash Commands*: From Dired, shell, eshell, or vterm, run `C-c a !` and type natural-language commands prefixed with `:` (e.g., `:count lines of python code recursively`); the tool generates the shell command for review and executes it in a compile buffer.
@@ -147,6 +165,7 @@ Enable installation of packages from MELPA by adding an entry to package-archive
147165
- *One-prompt TDD with refactoring*: Press `C-c a`, then `t` (`ai-code-tdd-cycle`) and choose `5. Red + Green + Blue (One prompt)` to generate tests, implement code, run tests, and then refactor the changed code in one flow.
148166
- *Reviewing a Pull Request*: Press `C-c a`, then `v` (`ai-code-pull-or-review-diff-file`). Choose to generate a diff between two branches. The diff will be created in a new buffer, and you'll be prompted to start a review.
149167
- *Multiple Sessions Support*: Start more AI coding session with C-c a a after launching one. Select active session with C-c a z. Prompt with above command will be sent to the selected session.
168+
- *Speech to Text Input*: Press `C-c a`, then `:` (`ai-code-speech-to-text-input`). Speak your prompt, press `RET` to stop recording, then choose whether to insert the transcription into the current buffer, send it to the active AI coding session after editing it in `ai-code-read-string`, or copy it to the clipboard.
150169

151170
*** Context Engineering
152171

ai-code-input.el

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@
2929
(declare-function ai-code-backends-infra--terminal-send-backspace "ai-code-backends-infra" ())
3030
(declare-function ai-code--prompt-filepath-candidates "ai-code-prompt-mode" ())
3131
(declare-function ai-code--git-root "ai-code-file" (&optional dir))
32+
(declare-function ai-code--insert-prompt "ai-code-prompt-mode" (prompt))
33+
(declare-function whisper-run "whisper" ())
34+
35+
(defvar whisper-after-transcription-hook nil
36+
"Hook run by whisper.el after transcription finishes.")
37+
38+
(defconst ai-code--speech-to-text-buffer-name "*whisper-stdout*"
39+
"Buffer name used by whisper.el transcription output.")
40+
41+
(defconst ai-code--speech-to-text-actions
42+
'("Insert to current buffer"
43+
"Send to AI coding session"
44+
"Copy to clipboard")
45+
"Completion choices for speech-to-text transcription handling.")
3246

3347
;;;###autoload
3448
(defun ai-code-plain-read-string (prompt &optional initial-input candidate-list)
@@ -118,6 +132,66 @@ INITIAL-INPUT is optional initial input string.
118132
CANDIDATE-LIST is an optional list of candidate strings to show before history."
119133
(ai-code-helm-read-string-with-history prompt "ai-code-helm-read-string-history.el" initial-input candidate-list))
120134

135+
(defun ai-code--speech-to-text-apply-transcription (origin-buffer)
136+
"Read transcription from current buffer and apply a chosen speech action.
137+
The transcription is obtained from the current buffer, then if
138+
ORIGIN-BUFFER is still live and the transcription is non-empty,
139+
prompt the user to choose an action (insert into ORIGIN-BUFFER,
140+
send to an AI coding session, or copy to the clipboard) and
141+
perform that action."
142+
(let ((transcription
143+
(string-trim
144+
(buffer-substring-no-properties (point-min) (point-max)))))
145+
(when (and (buffer-live-p origin-buffer)
146+
(not (string-empty-p transcription)))
147+
(ai-code--speech-to-text-handle-action
148+
origin-buffer
149+
transcription
150+
(completing-read "Choose speech action: "
151+
ai-code--speech-to-text-actions
152+
nil t)))))
153+
154+
(defun ai-code--speech-to-text-handle-action (origin-buffer transcription action)
155+
"Handle speech ACTION for TRANSCRIPTION from ORIGIN-BUFFER."
156+
(pcase action
157+
("Insert to current buffer"
158+
(with-current-buffer origin-buffer
159+
(insert transcription)))
160+
("Send to AI coding session"
161+
(ai-code--speech-to-text-send-prompt origin-buffer transcription))
162+
("Copy to clipboard"
163+
(kill-new transcription))))
164+
165+
(defun ai-code--speech-to-text-send-prompt (origin-buffer transcription)
166+
"Edit TRANSCRIPTION from ORIGIN-BUFFER and send the result to the AI session."
167+
(when-let ((prompt (with-current-buffer origin-buffer
168+
(ai-code-read-string "Send to AI: " transcription))))
169+
(require 'ai-code-prompt-mode)
170+
(ai-code--insert-prompt prompt)))
171+
172+
;;;###autoload
173+
(defun ai-code-speech-to-text-input ()
174+
"Record audio with whisper.el, then choose how to use the transcription.
175+
176+
After recording and transcription, you can insert the text at point in the
177+
original buffer, send it to an AI coding session, or copy it to the clipboard."
178+
(interactive)
179+
(unless (require 'whisper nil t)
180+
(user-error "Whisper.el is not available, please install it first"))
181+
(let ((origin-buffer (current-buffer)))
182+
(with-current-buffer (get-buffer-create ai-code--speech-to-text-buffer-name)
183+
(erase-buffer)
184+
(setq-local whisper-after-transcription-hook nil)
185+
(add-hook 'whisper-after-transcription-hook
186+
(lambda ()
187+
(ai-code--speech-to-text-apply-transcription origin-buffer))
188+
nil t)
189+
(whisper-run)
190+
(message "Recording audio. Press RET when you are done.")
191+
(while (not (equal ?\r (read-char)))
192+
(sit-for 0.5))
193+
(whisper-run))))
194+
121195
;;;###autoload
122196
(when (featurep 'helm)
123197
(setq ai-code--read-string-fn #'ai-code-helm-read-string))

ai-code-session-link.el

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
(defvar ai-code-backends-infra--session-directory nil
2121
"Session working directory set by ai-code-backends-infra buffers.")
2222

23-
(defcustom ai-code-session-link-enabled nil
23+
(defcustom ai-code-session-link-enabled t
2424
"When non-nil, make supported links clickable in AI session buffers.
2525
2626
Disable this if you prefer to avoid the extra linkification work on

ai-code.el

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ Shows the current backend label to the right."
465465
(ai-code--infix-select-code-change-auto-test)
466466
("r" "Refactor Code" ai-code-refactor-book-method)
467467
("t" "Test Driven Development" ai-code-tdd-cycle)
468-
("v" "Pull or Review Code Change" ai-code-pull-or-review-diff-file)
468+
("v" "GitHub PR AI Action" ai-code-pull-or-review-diff-file)
469469
("!" "Run Current File or Command" ai-code-run-current-file-or-shell-cmd)
470470
("b" "Build / Test (AI follow-up)" ai-code-build-or-test-project)
471471
("K" "Create or open task file" ai-code-create-or-open-task-file)
@@ -479,6 +479,7 @@ Shows the current backend label to the right."
479479
("o" "Open recent file (C-u: insert)" ai-code-git-repo-recent-modified-files)
480480
("p" "Open prompt history file" ai-code-open-prompt-file)
481481
("m" "Debug python MCP server" ai-code-debug-mcp)
482+
(":" "Speech to text input" ai-code-speech-to-text-input)
482483
("N" "Toggle notifications" ai-code-notifications-toggle))
483484

484485
(transient-define-prefix ai-code-menu-default ()

test/stubs/eca-chat.el

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
;;; eca-chat.el --- Minimal ECA chat test stub -*- lexical-binding: t; -*-
2+
3+
;; SPDX-License-Identifier: Apache-2.0
4+
5+
;;; Commentary:
6+
;; Lightweight ECA chat stub for batch tests.
7+
8+
;;; Code:
9+
10+
(defun eca-chat-open (&optional _session)
11+
"Return nil in the lightweight test stub."
12+
nil)
13+
14+
(defun eca-chat-send-prompt (_message)
15+
"Return nil in the lightweight test stub."
16+
nil)
17+
18+
(defun eca-chat--get-last-buffer (_session)
19+
"Return the current buffer in the lightweight test stub."
20+
(current-buffer))
21+
22+
(defun eca-chat-add-workspace-root ()
23+
"Return nil in the lightweight test stub."
24+
(interactive)
25+
nil)
26+
27+
(defun eca-chat-add-file-context (&rest _args)
28+
"Return nil in the lightweight test stub."
29+
nil)
30+
31+
(defun eca-chat-add-repo-map-context (&rest _args)
32+
"Return nil in the lightweight test stub."
33+
nil)
34+
35+
(provide 'eca-chat)
36+
37+
;;; eca-chat.el ends here

test/stubs/eca-process.el

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
;;; eca-process.el --- Minimal ECA process test stub -*- lexical-binding: t; -*-
2+
3+
;; SPDX-License-Identifier: Apache-2.0
4+
5+
;;; Commentary:
6+
;; Lightweight ECA process stub for batch tests.
7+
8+
;;; Code:
9+
10+
(defun eca-process--download-url (_version)
11+
"Return an empty download URL in the lightweight test stub."
12+
"")
13+
14+
(provide 'eca-process)
15+
16+
;;; eca-process.el ends here

test/stubs/eca-util.el

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
;;; eca-util.el --- Minimal ECA util test stub -*- lexical-binding: t; -*-
2+
3+
;; SPDX-License-Identifier: Apache-2.0
4+
5+
;;; Commentary:
6+
;; Lightweight ECA utility stub for unit tests that do not need the real runtime.
7+
8+
;;; Code:
9+
10+
(require 'cl-lib)
11+
12+
(defvar eca--sessions nil
13+
"Minimal session store for the test stub.")
14+
15+
(defvar eca--stub-session nil
16+
"Current session object used by the test stub.")
17+
18+
(defun eca-session ()
19+
"Return the current stub session."
20+
eca--stub-session)
21+
22+
(defun eca-vals (map)
23+
"Return MAP values for alists and hash tables."
24+
(cond
25+
((hash-table-p map)
26+
(let (values)
27+
(maphash (lambda (_key value) (push value values)) map)
28+
(nreverse values)))
29+
((listp map)
30+
(mapcar #'cdr map))
31+
(t nil)))
32+
33+
(defun eca-info (format-string &rest args)
34+
"Format FORMAT-STRING with ARGS for the test stub."
35+
(apply #'format format-string args))
36+
37+
(defun eca--session-id (session)
38+
"Return SESSION id or a default stub value."
39+
(or (plist-get session :id) 1))
40+
41+
(defun eca--session-status (session)
42+
"Return SESSION status or a default stub value."
43+
(or (plist-get session :status) 'running))
44+
45+
(defun eca--session-workspace-folders (session)
46+
"Return SESSION workspace folders."
47+
(plist-get session :workspace-folders))
48+
49+
(defun eca-create-session (workspace-folders)
50+
"Create a new stub session for WORKSPACE-FOLDERS."
51+
(setq eca--stub-session (list :id 1 :status 'running :workspace-folders workspace-folders)))
52+
53+
(defun eca-delete-session (_session)
54+
"Delete the current stub session."
55+
(setq eca--stub-session nil)
56+
t)
57+
58+
(defun eca-assert-session-running (_session)
59+
"Pretend the current stub session is running."
60+
t)
61+
62+
(defun eca--session-add-workspace-folder (session folder)
63+
"Return SESSION with FOLDER adjoined to its workspace list."
64+
(plist-put session
65+
:workspace-folders
66+
(cl-adjoin folder (plist-get session :workspace-folders) :test #'string=)))
67+
68+
(defun eca-get (map key)
69+
"Get KEY from MAP for hash tables and plists."
70+
(cond
71+
((hash-table-p map) (gethash key map))
72+
((listp map) (plist-get map key))
73+
(t nil)))
74+
75+
(provide 'eca-util)
76+
77+
;;; eca-util.el ends here

0 commit comments

Comments
 (0)