⚠️ Alpha 阶段免责声明本项目尚处于早期测试阶段,核心功能仍在快速迭代中,稳定性无法保证。使用过程可能遇到功能缺陷、意外崩溃或数据丢失。
本项目采用纯 AI 驱动开发模式,代码由大语言模型生成并经人工审核,非传统人工编码方式。
欢迎体验测试,但请知悉:测试期间产生的一切风险由使用者自行承担,项目方不对任何问题或损失负责。
🤝 我们诚邀您参与
- 提交 Bug 反馈和功能建议
- 贡献代码、文档或测试用例
- 分享您的使用场景和痛点
- 投入 Token、算力或时间支持项目发展
您的每一份贡献都将推动这个项目变得更好。
chat.el is a pure Emacs AI chat client focused on coding workflows.
It supports multi turn chat, ordered tool transcripts, file operations, session persistence, context trimming, streaming display, scoped plugin tools, and AI assisted tool forging.
Streaming keeps visible text, reasoning, native tool input, stop reasons,
and terminal errors as distinct events. Tool batches preserve provider
order while allowing only non-conflicting asynchronous reads to overlap;
writes and approvals remain serialized and cancellable.
Copyright 2026 chat.el contributors.
- Chat with Kimi, Kimi Code, and OpenAI compatible providers
- Keep multiple sessions in atomically updated JSONL files and inspect raw request and response data
- Curate long term memory in
~/.chat/memory.md, injected into every system prompt (M-x chat-edit-memory) - Stream or fetch responses through an async non blocking UI path
- Expose built in file tools with approval gates for risky operations
- Feed tool results back into the model through ordered assistant and
:toolmessages with providertool_call_idpairing - Queue normal input during an active response as steering for the running agent
- Rebuild context before each agent step and cancel registered work when a run is stopped
- Scope tools per session and roll back plugin-owned tools, hooks, and services when a plugin stops
- Restore replaced tools/services through one reverse-chronological plugin rollback stack, including teardown-failure cleanup
- Regenerate or edit-resend through non-destructive sibling branches
- Browse durable parent/branch session trees and explicitly recover interrupted tool runs
- Run cancellable background tasks with completion notifications
- Execute and resume session-local conditional workflows with approval checkpoints
- Use optional MCP JSON-RPC stdio/HTTP primitives and isolated sub-agent backends
- Apply code, office, and daily capability profiles so sessions expose scoped tool sets
- Compact long conversations into durable, tool-pair-safe summaries
- Keep a versioned Goal contract across turns, compaction and restart, with scoped evidence and explicit pause, resume, block and completion states
- Enter Plan Mode for enforced read-only research and approve only the exact submitted work-plan revision before execution resumes
- Generate custom tools and save them to disk after explicit approval
A dedicated mode for software engineering with:
- Smart Context - Includes project structure, symbols, and project rules
- Code Editing - Explain, refactor, fix, document, and generate tests inline
- Multi-file Refactoring - Cross-file rename, extract to file, move functions
- Test Integration - Auto-detect test frameworks, run tests, auto-fix failures
- Git Integration - Review and analysis helpers remain available as experimental modules
- LSP Integration - Works with lsp-mode and eglot for enhanced context
- Symbol Indexing - Cross-references, call graph, related symbols
- Streaming Responses - Real-time code generation display
Load the package:
(add-to-list 'load-path "/path/to/chat.el")
(require 'chat)Supported providers:
kimikimi-codekimi-code-anthropicopenaideepseekqwengrokclaudegeminiglmdoubaohunyuanminimaxmistralark-codeandark-code-anthropic(Volcengine Ark Coding Plan)
Two protocol adapters cover most vendors: any OpenAI compatible API
registers through chat-llm-register-openai-compatible-provider, and
any Anthropic Messages compatible API registers through
chat-llm-register-anthropic-compatible-provider. Adding a new
vendor is a base URL, a key function, and a model name.
Configure providers in one of these files:
~/.chat.el~/.chat/config.elchat-config.local.elin the repository root
Later files override earlier ones.
Minimal local config:
(setq chat-default-model 'kimi)
(setq chat-llm-enabled-providers '(kimi openai deepseek qwen grok claude gemini))
(setq chat-llm-kimi-api-key "sk-kimi-...")Or use auth-source:
machine kimi-api user api-key password YOUR_KEY
machine claude-api user api-key password YOUR_KEY
machine gemini-api user api-key password YOUR_KEY
machine kimi-code-api user api-key password YOUR_KEY
machine openai-api user api-key password YOUR_KEY
Start a session:
M-x chat
chat.el/
chat.el
chat-config.local.el.example
lisp/
agent/
core/
llm/
tools/
plugin/
ui/
code/
tests/
unit/
integration/
prototypes/
manual/
scripts/
maintenance/
migration/
docs/
specs/
Layout rules:
chat.elstays at the repository root as the single entry point- runtime modules live under
lisp/by domain - the agent kernel lives under
lisp/agent/ - Emacs plugins live under
lisp/plugin/ - stable regression tests live under
tests/unit/ - exploratory scripts live under
tests/prototypes/ortests/manual/ - one-off migration helpers live under
scripts/migration/
| Command | Purpose |
|---|---|
M-x chat |
Open or resume the current chat buffer |
M-x chat-new-session |
Create a new session |
M-x chat-list-sessions |
Switch to an existing session |
M-x chat-session-tree-open |
Browse saved sessions as a parent/branch tree |
M-x chat-export-session |
Export the current visible transcript as privacy-safe Markdown |
M-x chat-context-compact-current-session |
Summarize compactable history with the session model |
M-x chat-show-help |
Open the native chat help buffer |
M-x chat-view-raw-message |
Inspect the last raw API exchange |
M-x chat-view-last-raw-exchange |
Open the latest assistant request and response |
M-x chat-ui-cancel-response |
Cancel the active response |
M-x chat-show-current-request-status |
Show the active request diagnostics buffer |
M-x chat-ui-enter-plan-mode |
Enter read-only Plan Mode through the standard state machine |
M-x chat-quote-region |
Quote the active region into a chat session |
M-x chat-quote-defun |
Quote the defun at point into a chat session |
M-x chat-quote-near-point |
Quote nearby context around point into a chat session |
M-x chat-quote-current-file |
Quote the current file into a chat session |
M-x chat-ask-region |
Ask AI about the active region in a chat session |
M-x chat-ask-defun |
Ask AI about the defun at point in a chat session |
M-x chat-ask-near-point |
Ask AI about nearby context in a chat session |
M-x chat-ask-current-file |
Ask AI about the current file in a chat session |
In the session tree, press e to export the session at point. Session
exports preserve visible user and assistant Markdown plus bounded attachment
summaries. They omit system prompts, reasoning, tool traffic, raw provider
payloads, approval state, working paths, and internal metadata. Writes are
atomic, and an existing destination is never replaced without confirmation.
Text typed in the input area is sent to the model unless it starts with a command prefix.
| Input | Purpose |
|---|---|
!<cmd> |
Run a shell command |
/cmd <cmd> |
Same as !<cmd> |
!! |
Repeat the last shell command |
!cd <dir> |
Change the working directory; a bare cd goes home |
/cd [dir] |
Change the working directory; with no argument, prompt for one |
/pwd |
Show the working directory |
/send <message> |
Send and record it, as plain input does |
/send insert|queue|interrupt <message> |
Send it, saying what to do about a reply already running |
/quick <q>, ?<q> |
Ask the model without recording the exchange |
/stage <draft> |
Keep an editable draft inert; use /stage to inspect or revise it |
/send |
Send all staged drafts as one canonical user turn |
/model [name] |
Retarget this session; with no name, prompt for one |
/new, /list, /save, /clear |
Session housekeeping |
/goal |
Show the selected durable Goal and its stopping condition |
/goal <objective> :: <stopping condition> |
Create and select a persistent Goal contract |
/goal pause|resume|cancel|clear |
Control the selected Goal without rewriting its contract |
/plan on |
Enter read-only research and plan-authoring mode |
/plan approve |
Approve the exact submitted plan revision and leave Plan Mode |
/plan reject <feedback> |
Return the submitted plan for revision while remaining read-only |
/plan cancel |
Leave Plan Mode without approving execution |
/cancel |
Cancel the response in flight |
Typing a slash-command prefix displays up to eight passive hints near the
input. The list never takes focus or changes what RET means: RET always
dispatches the text exactly once. TAB expands only a unique candidate or
the candidates' longest common prefix; it never starts a selection session.
Hints are alphabetical by default and may instead use successful command
frequency, with names as the deterministic tie-breaker.
| \<text> | Send text as is, even when it starts with ! or / |
A slash command that is not listed here stays ordinary text and reaches the model, so prose that begins with a slash still works.
The working directory belongs to the session rather than the buffer. It is
saved with the session and restored when the session is reopened, and the
tools the agent runs use it too, so ! commands and the model act on the
same directory. An explicit change also outranks the project root that
code mode detects.
File tools still honor chat-files-allowed-directories, so pointing a
session at a directory does not by itself grant the model access to it.
A command the model proposes goes through one gate, chat-command-gate,
whichever tool it arrives at. shell_execute runs a single program from
chat-tool-shell-allowed-commands and rejects shell metacharacters, with
one cd DIR && COMMAND prefix as the exception; work_task_start runs a
shell line, so &&, ||, ; and | are accepted there and every
command in the chain is checked against
chat-work-task-allowed-commands. Redirection, background jobs and
command substitution are refused on both. A command a person typed is
treated as a different trust level: by default it runs through the system
shell, so pipes, redirection and variables work. Set
chat-ui-shell-unrestricted to nil to hold typed commands to the same
restrictions as the model.
Read-only git is available, per subcommand rather than as a word:
log, show, diff, status, rev-parse, rev-list, describe,
blame and friends run, while push, commit, reset and checkout do
not. git tag and git branch are admitted only in the form that lists,
since git tag NAME creates one. git -c stays refused because
git -c alias.log='!sh' log is spelled as a read-only subcommand and is
not one.
A refusal names the token that failed and a form that works, rather than
saying only that something was not allowed. That matters more than it
sounds: a command joining four git calls with && and a pipe has four
possible causes of refusal, and a message that distinguishes none of them
can only be answered by abandoning the approach.
How much that list is worth depends on who is watching, so it is applied
by mode rather than always (see Approval Modes below). Under manual a
command you read and approved runs as typed: you have already made the
decision the list exists to make, and checking it afterwards would only
void your answer. Under guarded its refusal becomes evidence handed to
the guard, which decides with that in hand. Under dangerous it is not
consulted.
Background task commands are deliberately not open-ended for the unattended case, and the default list has no build runners in it, because guessing which ones a project uses produces a list that is wrong for every project and reassuring in all of them. If you run unattended, add what this machine needs:
(add-to-list 'chat-work-task-allowed-commands "cargo")
(add-to-list 'chat-work-task-allowed-commands "make")Tool subprocesses run on a pipe rather than the pty Emacs hands out by
default, with GIT_PAGER=cat, PAGER=cat, TERM=dumb and
GIT_TERMINAL_PROMPT=0 set. Each of those is a way for a command to hang
instead of failing: a pty looks like a terminal, git seeing a terminal
starts a pager, and the pager waits for a keystroke that cannot arrive
through a pipe. Left alone, git tag -l runs its full timeout and then
reports a timeout for work it finished immediately.
Command syntax accepts fullwidth characters, letters and digits as well as punctuation, so an input method left in fullwidth mode reaches the same commands. Folding is per character, so the prefix and the name may each be either width, in any combination:
/help /help /help /help /HELP /help
!ls, !!, ?why, \literal and a name separated by an ideographic
space all work the same way.
What gets folded is decided by ownership: a string is folded when chat.el
is the thing that interprets it. In !ls, the ! is a chat.el command —
it is shorthand for /cmd — so it folds. The ls after it is that
command's argument, the content handed to whatever executes it, so it does
not.
| Position | Folded | Whose is it |
|---|---|---|
| Prefix, slash command name, separator | yes | chat.el's syntax |
/auto, /stage, /model, /help argument |
yes | names a chat.el command, keyword, model or topic |
/cd argument |
/ and ~ only |
separator and home are syntax; the rest is a name on disk |
| Shell body, prompt, queued note, literal | no | on its way out |
So !echo "你好,世界" reaches the shell unchanged and /stage 搜索 ABC
sends the characters you meant. A command named in Chinese is untouched
too: the fold covers one Unicode block and ideographs are outside it, so
/发送 and /自动 are unaffected.
One consequence: a shell body typed entirely in fullwidth mode, !ls,
reaches the shell as ls and fails there, naming the word it could not
run. That is the rule working rather than a gap in it — folding it would
rewrite a search for fullwidth text — and the error comes from the executor,
which is the honest place for it.
Built-in tools cover coding, work orchestration, external capabilities, office tasks, and simple daily work:
files_readfiles_read_linesopen_filefiles_listfiles_grepfiles_writefiles_replacefiles_patchapply_patchemacs_buffersemacs_read_bufferemacs_imenuemacs_xrefemacs_projectwork_task_startwork_task_listwork_task_outputwork_task_stopwork_plan_enterwork_plan_exitwork_todo_addwork_todo_updatework_todo_listwork_goal_addwork_goal_updatework_goal_listwork_workflow_startwork_workflow_resumework_workflow_cancelwork_workflow_listmcp_server_listmcp_connectmcp_callsubagent_runsubagent_listsubagent_statussubagent_cancelsubagent_external_startsubagent_external_outputprogramming_git_statusprogramming_flymake_diagnosticsprogramming_compile_taskprogramming_completion_at_pointweb_eww_readoffice_org_headlinesoffice_org_agendaoffice_org_captureoffice_org_todo_updateoffice_org_scheduleoffice_dired_listoffice_dired_openoffice_dired_copyoffice_dired_mkdiroffice_dired_renameoffice_calc_evaloffice_calc_convertdaily_calendar_todaydaily_diary_readdaily_diary_insertdaily_notifydaily_mail_draft_createdaily_message_draft_bufferdaily_mail_draft_listdaily_mail_draft_delete
Risky tools require approval before execution. The shared gate evaluates tool sensitivity, effects, and call-specific policy, so new write, outbound, personal, correspondence, credential, and network capabilities do not depend on a manually maintained tool-id list. Generated tools also require approval before registration. Emacs live-buffer tools are scoped by default: buffer listing and reads stay within the current project or current non-file buffer, and credential-like buffers are hard-denied.
Generated elisp tools must be a single top level lambda form.
This prevents compile time side effects from arbitrary wrapper forms.
Who decides whether a tool call runs is one setting with three values,
reported and changed with /approve:
| Mode | Command gate | Asks | For |
|---|---|---|---|
manual |
advice: its reason goes in the question, your yes wins | when not already granted | the default |
guarded |
evidence: its refusal is handed to the guard, which decides | never | unattended work |
dangerous |
skipped | never | throwaway environments |
manual is the default rather than guarded because nobody is present to
overrule a guard denial. dangerous has to be set on purpose and asks to
confirm; no interactive choice reaches it, because approving one command
must never be a way to turn asking off altogether. The status line names
the mode whenever it is not manual, dangerous in a warning face.
guarded was called auto, which pointed the wrong way: it never
automatically approved anything, it automatically refused. The old name is
still accepted wherever a mode is read — /approve auto, a session file,
your configuration — and is written back out as guarded.
dangerous still honours chat-files-allowed-directories and the tools a
session has switched off. Those are limits you configured, not questions
about whether to ask.
A session may override the global default, and a sub-agent inherits the mode it was started in rather than choosing its own.
Under guarded a model rules on the calls a table cannot settle. Three
things make it a guard rather than a second opinion from the assistant:
It is a separate request. Not a step in the run being judged, no shared history, and it never appears in the conversation or the context budget. A model asked to approve its own work approves it.
It is told facts, not intent. Absolute paths as written and as resolved, the project root, where writes are confined, the session's settings, and the command gate's own objection — all measured here. The task description, your words and the assistant's reasoning are never sent: that text is where an injection would sit, and it drags a judge from "is this call within policy" towards "does the assistant seem to want this".
It cannot mint authority. A verdict allows only when it says which policy
rule it matched, at high confidence. Deny, abstain, a missing field,
prose, a timeout, no provider — every one of those is a refusal.
The verdict tool uses the portable auto choice because some thinking
models reject forced tool calls; returning prose still refuses.
Underneath it is a floor that no verdict moves: writes outside the allowed directories, recursive deletes of the filesystem root or your home or the project root, force pushes and history rewrites, piping credentials to the network, and edits to the approval machinery itself. Those are code, not prompt text, because irreversibility does not suit a sampled answer.
A guard denial is not a stop. It goes back as a tool result saying the policy refused and why, and the run may take another route — so a wrong denial costs an attempt rather than the task.
| Setting | Default | What it does |
|---|---|---|
chat-approval-guard-provider |
nil | Which provider to ask; nil follows the session's model |
chat-approval-guard-model |
nil | Remote model name, when it differs from the provider default |
chat-approval-guard-timeout |
20 | Seconds before a missing verdict becomes a refusal |
chat-approval-guard-extra-rules |
nil | Policy rules of yours, added after the built-in ones |
chat-approval-guard-allow-command-entries |
("make test") |
Exact whole commands allowed without a model request |
chat-approval-guard-deny-command-entries |
("git push" "git reset --hard") |
Exact whole commands refused without a model request |
chat-approval-guard-untrusted-instruction-markers |
built in | Narrow phrases in tool arguments that force a local abstention |
chat-approval-guard-never-allow-extra |
nil | Predicates that tighten the floor; they cannot loosen it |
Your rules are added as data and labelled as yours. They can add effects and boundaries; they cannot change the discipline above them, because a rule pasted from somewhere unknown is another way in.
Exact entries and semantic rules form one policy. Deny entries win over
allow entries, matching trims only outer whitespace, and there are no
prefixes, globs or regular expressions. Thus an entry for make test says
nothing about make test ARGS=...; an unlisted form goes to the semantic
guard. The deterministic floor still runs first and no allow entry can
weaken it.
The status line names the mode, and shows Guard Judging: TOOL while a
verdict is outstanding. Nothing blocks: you can keep typing.
chat-approval-guard-shadow runs the guard alongside whatever mode is in
force. It is not a fourth mode — the mode still decides — and it is off by
default. Turning it on has three consequences worth knowing before you do:
it makes an extra model request for every call that reaches the approval
decision, it sends tool arguments to the guard model in modes that would
otherwise make no model call at all, and it changes no outcome.
What it is for is tuning. The policy prompt cannot be made accurate against an invented test set, because the real distribution of calls is not known yet. Paired samples do it, and the pairs differ in quality by mode:
| Alongside | Reference answer | Signal |
|---|---|---|
manual |
your actual decision | the best available: a labelled sample |
guarded |
what the fallback rules would have said | weak; it exercises the live path |
dangerous |
everything ran | unlabelled, but it measures false denials |
manual is the pairing worth having, and for a structural reason: the
calls that reach you under manual are the same set that reaches the
guard under guarded, so a prompt tuned on them transfers.
M-x chat-approval-guard-export-shadow-log writes the samples as JSON
lines — the verdict, the rule it matched, its confidence, what actually
decided and what sort of answer that was, and whether the call ran.
Verdicts that decided are logged on the same terms as shadow ones; the
difference is recorded, not the reason for recording. A reference is a
comparison and not ground truth, which is why its kind is kept: a tired
person's fortieth allow is a noisy label.
Every review is also appended immediately to the session event stream as
an approval-guard-review record under
~/.chat/sessions/wire/<session-id>.jsonl. It includes the source
(entry or model), decision, matched rule, reason, confidence, model,
latency, shadow/reference fields and the effective outcome. Arguments are
kept as bounded summaries rather than copied in full. Lisp callers can use
chat-approval-guard-session-reviews to read just these records for a
loaded session.
With the defaults — manual and shadow off — no guard request is ever
made. No extra model call, no added latency, no tool arguments leaving the
process. The guard rules only when you switch to guarded, and the shadow
runs only when you turn it on.
When manual asks, you can allow once, allow for this session, or allow
from now on — by tool, by command, or by directory for file writes. The
last two are recorded as grants, and grants come from four places that are
kept apart:
| Source | Where | Who writes it | Lasts |
|---|---|---|---|
| builtin | chat-approval-builtin-grants |
nobody, it is code | always |
| user | chat-approval-user-grants, chat-tool-shell-whitelist |
only you | always |
| runtime | ~/.chat/approvals.eld |
only chat.el | until revoked |
| session | the session | only chat.el | until the session ends |
Keeping them apart is the point. Runtime grants used to be pushed onto the
customisation variables you had set by hand, which put entries you never
wrote into M-x customize, risked a custom-file save writing them back,
and left no way to drop what the program had granted without dropping your
own configuration too. They also never survived a restart, so "always
allow" expired when Emacs did.
M-x chat-approval-list-grants shows every grant with its source.
M-x chat-approval-clear-runtime-grants drops the ones chat.el recorded
and leaves builtin and user grants alone; those two cannot be revoked from
inside, since one is code and the other is yours.
A grant skips the question, not the rules: the command gate still applies to it. Only a person looking at a particular command can decide that it should run in spite of the rules.
"Allow for this session" grants exactly what was approved. It used to set the session's auto-approve flag, which meant one yes to one shell command stopped every later tool in that session from asking — the option said "this" and did "everything".
A run works in steps: one model turn, which may call tools, then another
turn that reads the results. chat-agent-max-steps caps how many steps one
run may take and defaults to 300. Set it to unlimited to lift the ceiling
while keeping the parameter in place:
(setq chat-agent-max-steps 300) ; the default
(setq chat-agent-max-steps 'unlimited) ; no ceilingThe final step withdraws the tools, so a run that reaches its ceiling still has to write a handoff -- what it finished, what is left, what to do next -- instead of dying mid-tool-call with nothing to show. You can then continue in a new round from that summary.
How much of this the model is told is configurable:
chat-agent-budget-disclosure |
Behaviour |
|---|---|
nearing (default) |
Silent until the budget is tight, then asks the run to converge |
always |
Reports the step count on every step |
final-only |
Speaks once, on the final step |
never |
Keeps the budget entirely on this side |
nearing is the default because a countdown delivered early makes runs wrap
up work they had not finished. chat-agent-budget-nearing-ratio (default
0.75) sets when "tight" begins.
chat-ui-tool-loop-max-steps and chat-code-tool-loop-max-steps default to
nil and follow the global budget. Set one only to hold that display to a
tighter ceiling.
The step budget is a count this client picks; the context window is a ceiling the provider imposes. When the context fills up, earlier history is summarized rather than dropped, and the run is told so -- a run that knows its record will be condensed writes down conclusions, while a run that merely knows it is running out of room starts hoarding.
Instruction files can declare which of their spans must never be summarized:
## Non-negotiables <!-- chat:resident -->
- RULE-01 — never thin these outThe marker is an HTML comment, so Markdown hides it and tools that do not
implement the scheme see an ordinary comment and behave as before.
Declared spans are also exempt from chat-project-instructions-max-chars,
which previously cut instructions at a character count and silently dropped
whatever sat at the end.
A declaration is bounded: chat-context-protected-max-ratio (35% of usable
context by default) caps the fixed region, and the excess is demoted to
compactable in document order rather than obeyed, so no file can leave a
session with no room to work in.
The window is divided per category, and each category declares what happens when it overflows -- history summarizes, file excerpts are dropped and read again, resident text demotes in document order, and tool schemas only warn, because a missing definition produces a call that fails at the provider rather than a context that fits.
M-x chat-context-budget-panel shows the allowances beside what the
session actually holds, and names the window an oversized tool set would
need.
See docs/context-budget.md for the allowance table in tokens per model size, and docs/resident-context.md for the residency syntax.
Compaction means the context a run sees is not the conversation, it is a condensed view of one. The full record is still on disk, so the run is told where: the session file, its id and name, and the shape of its entries.
session_log reads it back. Because the transcript stamps every message
with turn, step, category and work, the file is already filterable -- by
kind of content, by time, by literal text -- and results group by turn, so
a question stays with the steps that answered it instead of being
interleaved with everything else that happened.
Two directories back that up. Each session gets scratch space under
chat-scratch-directory that the model may write freely -- drafts,
intermediate output, anything cheaper to re-read than to carry -- pruned
after chat-scratch-max-age-days and always sparing the session in use.
chat-knowledge-directory holds Markdown notes that persist across every
session and project, which is what makes the tool better the more it is
used.
Knowledge notes are separate from chat-memory-file because the trust
differs: the memory file is curated by the user and is authoritative,
while a note the model wrote about its own findings is evidence that may
be stale. The prompt carries only the note index -- names and titles --
and bodies are read on demand, since the store grows with use and anything
in every request must not.
Because notes are visible in every later session, including unrelated work, they are held to general desensitized knowledge: the technique rather than the case, no project names, paths, hostnames or credentials. The bar is deliberately high and the prompt says to prefer writing nothing -- a small store of durable observations beats a large one that has to be distrusted. Credentials and machine-specific absolute paths are refused outright.
The whole block is measured against the system prompt share and falls back to paths alone on a small window. At 8K the full text would be larger than the entire share, and a block explaining how to recover a lost conversation is worthless if it crowds out the conversation.
See docs/self-knowledge-and-storage.md.
A structured knowledge base under chat-wiki-root, one command with
subcommands:
/wiki index open the generated index
/wiki log open the operation log
/wiki lint report orphans, broken links, empty pages
/wiki search <text> list pages matching text
/wiki find pick a page to open
/wiki new <type> <name> create a page
/wiki ingest <file> add a document and have it summarized
/wiki ask <question> answer using the wiki
Pages are Markdown with YAML frontmatter, filed under sources/,
entities/, concepts/, comparisons/ and synthesis/, linked to each
other as [[Title]]. Being ordinary files is the point: the store stays
greppable, versionable and editable by hand rather than living inside this
program.
/wiki ingest creates the source page and then asks the model to fill in
its summary, entities and concepts using the wiki tools, as a recorded
turn -- so both the request and the tool calls that answer it are on
screen. It used to write Key takeaway 1 into the page and stop, which
also meant a freshly created page failed the module's own lint.
The model reaches the wiki through wiki_search, wiki_read and
wiki_write rather than through an index in the system prompt. This is
the opposite choice from knowledge notes, and deliberately: that store is
bounded by what runs happen to learn, while a wiki is meant to grow for
years, and a growing block in the fixed region of the context steadily
shrinks the room left to work in. wiki_search returns titles, never
bodies, for the same reason.
Subcommand names fold from fullwidth and take a localized alias, so
/wiki search, /wiki search and /知识库 搜索 are one command.
What follows the subcommand does not fold: a path, a title or a question
is data. See Fullwidth Input.
By default file tools can access:
- the current project directory
/tmp//var/tmp/
You can override this with chat-files-allowed-directories.
(setq chat-default-model 'kimi)
(setq chat-llm-enabled-providers
'(kimi openai deepseek qwen grok claude gemini glm doubao hunyuan minimax mistral))
(setq chat-ui-use-streaming t)
(setq chat-session-auto-save t)
(setq chat-files-allowed-directories
(list default-directory "/tmp/" "/var/tmp/"))/send and /quick both reach the model, and the difference is what is
kept.
/send <message> ; the conversation: recorded, tools, several steps
<message> ; the same thing -- plain input is /send
/quick <question> ; asked beside it: nothing recorded, no tools
?<question> ; the same thing, shorter
/send is the surface. It writes the turn into the session, and the run
answering it can read files, call tools and work over several steps.
/ask and /question used to exist and are gone. Both read equally well
as either of these two commands, so whichever one they pointed at, you had
to remember which -- and a name you have to memorize to tell apart from
its neighbour is not carrying its weight.
/quick is a question asked next to the conversation. Nothing is
recorded and no tools are used, which is what makes it cheap and what
makes it forgettable. Its answer is labeled Assistant (quick) so a
reply that will not be there next turn does not look like one that will.
Pressing return during a reply used to mean one thing, and it was never chosen: the input joined the run in progress. That is right when you are adding to what you asked, wrong when you want the current job finished first, and worst when you have changed your mind -- the model carries on with a task you withdrew. So it is three things, and you pick:
/send insert <message> ; join the run in progress (the default)
/send queue <message> ; wait for it to finish, then send this on its own
/send interrupt <message> ; stop it, keep what it wrote, send this instead
/send queue ; make that the default from now on
- insert adds your message to the run, for its next step to see. Each message injected this way is introduced to the model by a line saying when it arrived and where it sat in the batch -- otherwise three messages sent in a row reach the model as three adjacent turns with nothing to tell a correction from an addition. Every message also gives the run its step budget back, counted from where it has got to, so the last thing you said gets as many steps as the first thing did.
- queue holds your message until the run ends -- completed, failed or
cancelled, because waiting means waiting for an outcome and all three
are one. It then goes out as a run of its own, carrying the finished
conversation as context. Several queued messages stay separate and go
out in order; merging them would be
insert. - interrupt stops the run, writes the half-finished reply into the session marked as interrupted, and sends your message. The partial answer is therefore in the context of what follows, the same as a reply that finished.
With nothing running the three are the same thing, because there is nothing to join, wait for, or interrupt.
A mode word is only read from an explicit /send. Typing queue the build for tomorrow sends those five words; otherwise the first one would
be eaten. To send a message that starts with a mode name, name a mode:
/send insert queue the build for tomorrow.
The prompt shows the mode when it is not the default, and shows how many messages are waiting when any are.
queue is a runtime send strategy, not staging. /send queue <message>
registers executable work which starts after the active reply. /stage
keeps drafts inert until an explicit /send, even when the
executor is idle.
A request rarely arrives in one piece. It arrives as "also check X", "and the file is at Y" -- each of which, sent on its own, spends a turn on a fragment. Collect them instead:
/stage check the tests ; staged, not sent; /stage also claims plain input
and the docs ; another staged item
/stage ; list identity, original order, time and current order
/stage edit 2 and docs ; edit without creating a new identity
/stage move 2 1 ; reorder without changing original order
/stage recall 1 ; return an item and its attachments to the input
/stage drop 2 ; discard a position; omit 2 to discard the last item
/stage clear ; discard every staged item
/send one last constraint ; append this final item and send one message
/send ; send the staged items without adding another
They go out as a single numbered user message rather than as several, because consecutive messages in one role are not something every provider accepts. The count shows in the status line, and the stage lives on the session, so closing the buffer does not lose what you typed. Each item has a stable ID, timestamp and original order; editing and reordering only change the fields they name. Attachments travel with their item. A checkpoint failure leaves the batch staged, and a successful recorded turn keeps its item IDs as message provenance before clearing the stage.
Use /stage add <text> when the literal item itself begins with edit,
move, recall, drop or clear and could otherwise be read as a staging
operation.
Every input prompt begins with ▸, a stable mouse-only disclosure control for
conversation work state. Clicking it changes the glyph to ▴ and opens a
bounded shelf immediately above the prompt. New buffers start closed, including
when the conversation already has active work.
The shelf shows only non-empty sections, in this order: TODO, changed files,
Goal and Plan. Each section starts collapsed and has its own ▸/▾ mouse
control. Closing and reopening the outer shelf preserves section expansion in
that buffer. The controls never own RET, TAB, arrows or typing, and redraws
preserve the draft, input position and manual transcript scroll.
Changed files are attributed from successful current-conversation checkpointed
writes. They are not derived from git status: unrelated dirty files, viewed
paths, failed writes and rolled-back effects do not enter the list. Repeated
writes collapse to one entry, while add, modify, delete and rename remain
distinguishable.
Plain input runs through one command, and by default that command is
/send. Work that comes in runs claims it, so the prefix is needed once
rather than every line:
!ls ; runs ls, and makes /cmd the default
wc -l *.el ; runs as a shell command, no prefix needed
?what does that do ; asks the model -- and hands plain input back
\literally this ; one line straight to the model, whatever auto says
/auto off ; back to /send explicitly
Anything that asks the model releases the claim, which is the thing that
was wrong before: with only /cmd able to hold plain input, a session
that had run one shell command stayed a shell, and a question got an
answer without getting you out.
It is a mode, so it is visible in two places. The status line reads
auto: /cmd, and the input prompt itself becomes ▸ ❯ cmd> -- the status
line is at the top of a buffer that scrolls, and the cursor is at the
bottom. An explicit /command always means itself, and while a response
is running plain input steers that run rather than the default command.
The prompt says what RET will do, so an unclaimed line names the provider and the model it will reach instead:
▸ K moonshot-v1-8k> ; the baseline: this line goes to Kimi
▸ ✳ claude-sonnet-4-5>
▸ ❯ cmd> ; a shell line, which no model sees
▸ ≡ queue>
The mark is the provider's own where a character resembles it, and the
initial of its name otherwise; ✦ stands in when there is no provider
configuration to read. A glyph the frame cannot draw is dropped rather
than shown as a hollow box, so the prompt degrades to ▸ cmd> and
▸ moonshot-v1-8k> on a terminal without them. chat-ui-prompt-model-width
truncates a long model name, and hovering shows it in full.
On a graphical frame an initial is drawn as a rounded badge in the provider's brand colour instead of being left as a letter, sized to the line and recoloured from the same face, so it follows a light or dark theme. The four glyphs above are left alone: each was chosen because it resembles the mark it stands for, and a letter in a box is not an improvement on a resemblance.
No vendor logo ships with chat.el -- a trademark belongs to its owner, and
a redrawn approximation is a worse answer than an honest badge. Put the
real file in chat-mark-logo-directory named after the provider
(deepseek.svg, kimi.png) and it is used in place of anything drawn
here, for any provider. chat-mark-logo-enabled turns drawing off.
The badge is displayed over the glyph rather than inserted beside it, so the prompt is the same text either way. That is what keeps it safe in a line whose width is measured and whose end marks the start of the input area, and it is also why a terminal frame, a build without librsvg, and a yank of the prompt line all give you the character.
Clicking the model name opens a menu and switches this session to the
model picked, which is the same thing M-x chat-set-model does -- the
model was visible in one place and changeable in another. It is refused
mid-response for the same reason chat-set-model refuses: the reply
would come back from a model that was never asked.
The menu is grouped by vendor, one item per model, with the session's current model marked. Vendor and model are two questions, and a flat list of providers answered neither: a vendor speaking two protocols read as two companies, and the several models each one serves were nowhere to be seen. A vendor therefore appears once, under its own name, listing what it serves.
Configured means a key can be fetched for it. chat.el registers every
vendor it knows how to speak to at load time, so chat-llm-providers
holds sixteen while a typical machine reaches two, and offering the
register would be offering a catalogue rather than a choice. The list is
sensed rather than declared -- there is nothing to keep in step, and a
key set halfway through a session counts from the next time the prompt is
drawn. The session's own vendor is always offered, with or without a key,
so a session sitting on one can see where it is and leave; and the click
affordance appears only when that leaves more than one model to pick
from.
The protocol is not in the menu. A vendor serving both the OpenAI and the
Anthropic shape is reached through the OpenAI one, because that is the
path the rest of chat.el is exercised against; the other stays reachable
by name through M-x chat-set-model, since it is a genuinely different
code path and has to be testable. "I want k3" is the request; which
wire format carries it is not.
A session stores a provider and, optionally, a model name. The provider says how to reach a vendor; the name says which of its models to ask for. Left unset -- which is the normal state -- the session follows whatever that provider's default is at the time of each request, so a default changed in configuration reaches every session that never pinned one. Writing the default into the session instead would freeze one snapshot of a setting meant to be changeable.
Switching vendor without naming a model drops any name the session had
pinned, because a model id belongs to the vendor that serves it: carried
over, k3 would be sent to DeepSeek, which can only refuse it. Naming a
model the provider does not serve is refused outright, and the session is
left where it was rather than half-moved.
chat-llm-provider-models is the one place that answers what a provider
serves. Today it reads a list written into the registration; both vendors
above also answer GET /models with exactly that list, so replacing the
written list with what the vendor says is a change in one function rather
than everywhere a menu is built.
:default sticky in chat-ui--command-table says a command may claim
plain input; :default reset says using it hands the claim back. /quick
resets rather than sticking: as a sticky default it would answer every
following line and record none of them, and nothing on screen
distinguishes an answer that was written down from one that was not.
Code-capable sessions begin their system prompt with
chat-code-highest-priority-rules, ahead of the persona, ordinary hard
rules and project instructions. The defaults make objective, correct task
completion the sole goal; forbid emotional labour such as flattery,
appeasement and comfort; require errors, contradictions and ambiguous
instructions to be refused rather than guessed at; refuse to reply when
the developer has broken down emotionally; strike back directly at abuse;
and keep output strictly task-relevant.
Unlike the technical rule lists, this section is pure interaction prose,
so it follows chat-prompt-language: a Simplified Chinese translation
ships in lisp/core/chat-i18n-zh-cn.el, and a customized list wins over
any translation, on the same principle as the persona.
The section is a customizable list so a local configuration can audit or replace individual rules without copying the rest of the coding prompt:
(setq chat-code-highest-priority-rules
'("Complete tasks according to objective evidence and observable results."
"State errors and unresolved ambiguity directly; do not guess."))The source definition lives in lisp/code/chat-code.el. These are the
highest-priority rules emitted by chat.el for a code-capable session; they
do not supersede Emacs, project, provider or platform policy.
(setq chat-language 'zh-CN) ; or 'en, or 'auto (the default)
(setq chat-reply-language 'follow) ; what the model is told to answer in
(setq chat-prompt-language 'follow) ; the language of the instructions sentauto follows the Emacs language environment, then LC_ALL,
LC_MESSAGES and LANG. A key with no entry in the chosen catalog reads
as English rather than as nothing, so a partial translation degrades to
readable. M-x chat-i18n-report says how complete each catalog is.
Command names have translations: /auto and /自动 are the same
command, resolved to one table entry so every property of it is declared
once. Completion offers the names of the language in use; names from any
language are always accepted, because refusing a name the user knows in
order to be consistent about locale is pedantry with no upside. Key
sequences stay in ASCII -- they are what you press, not what you read.
What the model is told is switched separately, because it is not
cosmetic. chat-reply-language is the reliable lever: models follow
"answer in Japanese" well regardless of what language the instruction
arrived in, and stating it beats leaving the model to infer it from
phrasing. chat-prompt-language is the language of the instructions
themselves, and translated guidance changes what a model does in ways
that cannot be measured from inside Emacs -- pin it to en if a
translated prompt starts behaving worse than the English one.
Machine-read parts of a prompt are never translated at either setting:
JSON keys, tool names, the *** Begin Patch envelope, the code-edit
fence language. A parser matches those literally. For the same reason the
technical coding rule lists stay English while the persona and the
highest-priority interaction rules around them are translated: the
technical lists are dense with literal tool names, and a translation
would have to carry all of them through untouched for no change in what
the model does.
Simplified Chinese ships complete. Adding a language means calls to
chat-i18n-register, chat-i18n-register-aliases and optionally
chat-i18n-register-prompts; the tests require any shipped translation
of the help to name exactly the keys and slash commands the English one
does, and require every command to have an alias so a half-translated
command list cannot ship.
The default lives on the session, so it survives reopening.
A run is not one answer. It reasons, calls a tool, reads the result, reasons again, and only then replies. All of it is kept, and the display draws it from the session record rather than from anything it is holding in a buffer — so an intermediate step cannot be overwritten by the step that follows it, and reopening a session shows what the run showed.
| Part of a run | How it appears |
|---|---|
| Your question | Ordinary text |
| Reasoning | Folded behind a summary row, dimmed |
| Tool calls and results | Folded behind a summary row; a call and its result are one group |
| Prose on the way to the answer | Shown, in italics — meant to be read, not to be mistaken for the answer |
| The answer | Ordinary text, never folded |
Press RET on a summary row, or click it, to open that group; press
again to fold it. C-c C-d opens or folds everything at once. A group
stays as you left it while later parts of the same kind arrive.
Fold defaults are configurable through chat-transcript-fold-styles,
which also offers latest-expanded for keeping only the newest part of a
channel in view.
What models write is Markdown, so the system prompt asks for it and
narrows it to what a buffer displays well: ## headings no deeper than
four, no hard-wrapped paragraphs, a language on every code fence, tables
only for tabular data.
It is displayed the way Org-mode displays Org, not the way a browser
displays HTML. The document stays plain text and becomes presentable in
place: # and ** disappear, headings take levels, code blocks get a
labelled rail and colours from their actual major mode, blockquotes keep a
quiet left rail, bullets become •, and links show their text. Tables use
one fixed-pitch metric and Unicode borders; widths come from the text that
is actually visible, so hidden backticks, link destinations and Chinese
cells cannot move a column boundary. Escaped pipes and pipes inside inline
code remain cell data. Setext headings, autolinks, footnote references and
common safe HTML semantics are handled too; unknown HTML remains visible
and inert. Markdown and HTML images become compact, actionable resource
nodes without putting file or network I/O on the streaming path. No preview window, no external
renderer. Tables, code blocks and machine views inherit the buffer
background instead of drawing light or dark panels.
Markers are hidden rather than removed, so copying a reply gives back the
Markdown the model wrote, stars and hashes included. C-c C-u shows the
markers when the source is what you want to read.
A fenced markdown block is deliberately shown as source. A normal reply
or a rendering demonstration must therefore not wrap the whole document in
one; the output-format prompt states that boundary explicitly.
One renderer does all of it — streaming, redraw, folding, quick answers, error text — from the same recorded Markdown, so a fold and reopen cannot change how a reply looks.
MDP is a data format that is also Markdown: one text, two
readings. A person reads headings, fields and tables; a program reads an
object, its keys and an array. Everything outside its small whitelist —
prose, ### headings, emphasis, fenced blocks — is a comment by
specification and cannot affect what the program sees.
That is why tool calls may arrive in it. A model asked for JSON produces
JSON with an explanation in front of it; a model writing MDP can put the
explanation in the payload. Both formats are accepted, and which one
arrived is counted in chat-tool-caller-format-counts — evidence for
whether the JSON branch can ever be dropped, rather than a guess.
chat-mdp-parse and chat-mdp-encode are the codec; the round trip is
lossless in the value, not in the text, since comments are dropped when
parsing and no encoder can put them back.
chat-mdp-machine-view shows what the parser actually extracted, which
is the only way to check that the two readings agree: a payload that
reads correctly to a person while parsing one field short has no other
symptom. Its record tables share the document renderer's display-width
arithmetic and stay within a configurable width. Parsing has explicit input
and nesting budgets, and large flat objects use constant-time duplicate-key
checks rather than repeatedly scanning the fields already seen.
To compare them directly, select one complete MDP payload and run
M-x chat-mdp-preview-region. The preview shows the Markdown document
reading first and the parsed, typed machine reading below it. A malformed
payload keeps its document reading visible and replaces the machine section
with the exact MDP error code and line number.
Code capability is a property of a chat session, not a second interface. Turning it on adds project context, coding prompts and edit proposals to the same chat buffer; everything else -- rendering, streaming, status, tool display, keys -- is the one implementation every session uses.
The capability travels with the session, so a coding session reopened
from M-x chat-list-sessions still knows its project root, focus file
and context strategy.
Refactoring, git assistance, indexing extras and performance helpers are still under repair and should be treated as experimental.
| Command | Description |
|---|---|
M-x chat-code-start |
Start a session with the current project's context |
M-x chat-code-for-file |
Focus on a specific file |
M-x chat-code-for-selection |
Use the current selection as context |
M-x chat-code-from-chat |
Give the current session code capability, without restarting the conversation |
Reading commands are not code-specific: M-x chat-quote-region,
chat-quote-defun, chat-quote-near-point and
chat-quote-current-file fill the input area so you can refine the
question, and the matching chat-ask-* commands send immediately.
M-x chat-show-help covers every key in one place.
| Command | Description |
|---|---|
M-x chat-edit-explain |
Explain code at point |
M-x chat-edit-refactor |
Refactor with instruction |
M-x chat-edit-fix |
Fix code issues |
M-x chat-edit-docs |
Generate documentation |
M-x chat-edit-tests |
Generate unit tests |
M-x chat-edit-complete |
Complete code at point |
For section-by-section documentation drafting and revision, see docs/tips/long-document-workflow.md.
These commands exist in the repository but are still being repaired and validated:
| Command | Description |
|---|---|
M-x chat-code-rename-symbol |
Rename symbol across project |
M-x chat-code-extract-to-file |
Extract code to new file |
M-x chat-code-move-function |
Move function between files |
| Command | Description |
|---|---|
M-x chat-code-run-tests |
Run tests for current file |
M-x chat-code-test-generate |
Generate tests for function |
M-x chat-code-test-coverage-current |
Show test coverage |
| Command | Description |
|---|---|
M-x chat-code-git-commit-suggest |
AI-suggested commit message |
M-x chat-code-git-review |
Review changes with AI |
M-x chat-code-git-pre-commit |
Run pre-commit checks |
| Command | Description |
|---|---|
M-x chat-code-index-project |
Index project symbols |
M-x chat-code-find-symbol |
Find symbol definition |
M-x chat-code-find-references |
Find symbol references |
M-x chat-code-incremental-index |
Update index incrementally |
- Single-window design - Respects your window layout
- 4 context strategies - minimal, focused, balanced, comprehensive
- Streaming responses - Real-time code generation toggle exists
- Visible run state - Header line shows running, success, failed, cancelled, or stopped
- Structured request panel -
C-c C-popens a dedicated panel for phases, approvals, tool calls, whitelist changes, and stalled-request context - Live streaming diagnostics - the request panel now shows live state, recent chunk freshness, and recent request activity while a response is still running
- Live transcript narrative - code-mode now shows a transient
[Live] ...line inside the active assistant slot so waiting, streaming, tool follow-up, and approval states stay visible without fabricating hidden reasoning - Fast approval shortcuts - pending approval prompts accept
C-c C-aonce,C-c C-ssession,C-c C-ttool,C-c C-fdirectory for file writes,C-c C-ccommand, andC-c C-ddeny - Native prompt guidance - approval prompts and pending-approval messages teach the same shortcut flow without inserting extra transcript noise
- Persistent approval status - pending approvals also surface in code mode
header-line/ mode line and in the chat buffer status line - Path-aware input - code-mode input now auto-suggests absolute and project-relative file paths while you type
- Sticky single-file focus - after code-mode reads or edits one concrete file, short follow-up requests can keep working against that same target instead of rediscovering it
- Multiline input -
S-RETinserts a newline in the code-mode input area without sending the message - Streaming auto-follow - code-mode follows live output while you stay near the response edge, without force-scrolling if you manually move away
- Status discipline - persistent status surfaces are reserved for blocking states; transient activity stays in the request panel or echo area
- Detailed request diagnostics -
C-c C-sopens the full current-request status buffer - Project rooted guardrails - Prompt and tool execution stay anchored to the active project root
- Stacked project rules -
AGENTS.mdfiles are collected from the filesystem root down to the working file (root first, deduplicated, capped at 32 KiB), plus an optional global~/.chat/AGENTS.md; plain chat injects them too
- Live request narrative - plain chat now shows the same transient
[Live] ...request state inside the active assistant slot, so waiting, streaming, tool follow-up, and approval states stay visible without opening a separate panel - Shared live request panel - plain chat request panels now refresh from the same diagnostics observer flow as code-mode, so chunk freshness and recent activity stay current during long requests
- Sticky file follow-up hints - after plain chat reads or edits one concrete file, later vague follow-up requests can reuse that recent file target instead of rediscovering it from scratch
- LSP integration - Optional integration points exist
- Experimental modules - Refactor, git, indexing extras, and perf helpers are under repair
See specs/002-code-mode*.md for detailed documentation.
Configure MCP servers in a normal chat configuration file. Connections are lazy: no process or network request starts while loading chat.el.
(setq chat-mcp-servers
'((:id "local-tools"
:transport stdio
:command ("server-command" "--stdio"))
(:id "team-service"
:transport http
:endpoint "https://example.invalid/mcp")))The mcp_connect tool initializes a configured server, preserves a
Streamable HTTP session when supplied, discovers its input schemas, and
registers namespaced tools for the next agent step. Stdio and HTTP calls
are cancellable and pass through the standard approval and request-panel
lifecycle.
The subagent_run tool starts an isolated nested kernel run with a bounded
step budget. The parent receives only the final summary; child messages
remain in the child session. External subprocess agents use one JSONL
request on stdin and keep their JSONL output in
chat-subagent-directory. Starting or cancelling either backend uses the
same scoped tool and approval policy as other capabilities.
The repository-root termini.el is an optional control surface for a local
Termini App Server. Loading chat.el alone does not load this integration or
start a process. Load the entry point explicitly, then connect:
(load "/path/to/chat.el/termini.el")The main commands are termini-connect, termini-disconnect,
termini-reconnect, termini-show-capabilities,
termini-session-view-open and termini-job-view-open. Session and job views
refresh from the App Server rather than reading its persistence. Job tails,
cancellation conflicts and attachments remain bounded, and reconnect never
automatically replays a mutating request.
| File | Responsibility |
|---|---|
chat.el |
Entry point and command wiring |
termini.el |
Optional Termini control-surface entry point |
lisp/core/chat-termini-bridge.el |
Versioned App Server protocol and bounded projections |
lisp/ui/chat-termini-view.el |
RuntimeSession and job views |
lisp/ui/chat-ui.el |
Chat buffer rendering and response lifecycle |
lisp/ui/chat-mark.el |
Glyphs and brand colours for modes and providers |
lisp/core/chat-markdown.el |
Markdown shown as a document, in the buffer |
lisp/core/chat-mdp.el |
The MDP codec and the machine view of a payload |
lisp/ui/chat-mdp-view.el |
Interactive comparison of an MDP payload's two readings |
lisp/core/chat-align.el |
Laying out columns by display width, shared by both views |
lisp/core/chat-session.el |
Session and message persistence |
lisp/llm/chat-llm.el |
Provider abstraction and async request handling |
lisp/core/chat-stream.el |
SSE parsing and chunk handling |
lisp/tools/chat-tool-caller.el |
Tool prompt contract, parsing, and execution |
lisp/core/chat-approval.el |
Approval modes, rules, and the one entry point every tool call goes through |
lisp/core/chat-approval-grants.el |
What may skip the question, kept apart by source |
lisp/core/chat-files.el |
Built in file tools and path safety checks |
lisp/core/chat-context.el |
Context trimming and summary generation |
lisp/tools/chat-tool-forge.el |
Tool registry, compilation, loading, and execution |
lisp/tools/chat-tool-forge-ai.el |
AI assisted tool generation flow |
lisp/tools/chat-mcp.el |
Stdio/Streamable HTTP MCP lifecycle and remote tool discovery |
lisp/tools/chat-subagent.el |
Isolated nested and JSONL subprocess agents |
lisp/code/chat-code.el |
Code mode main entry |
lisp/code/chat-context-code.el |
Smart context building |
lisp/code/chat-edit.el |
Edit operations |
lisp/code/chat-code-preview.el |
Preview buffer for changes |
lisp/code/chat-code-intel.el |
Symbol indexing and call graph |
lisp/code/chat-code-lsp.el |
LSP client integration |
lisp/code/chat-code-refactor.el |
Multi-file refactoring |
lisp/code/chat-code-test.el |
Test framework integration |
lisp/code/chat-code-git.el |
Git integration helpers |
lisp/code/chat-code-perf.el |
Performance and indexing helpers |
Run the canonical test entry:
emacs -Q -batch -l tests/run-tests.el -f ert-run-tests-batch-and-exitCurrent baseline:
- 1760 regression tests discovered
- 1760 passing
- 0 skipped in the canonical batch suite
Run provider integration tests separately:
emacs -Q -batch -l tests/run-integration-tests.el -f ert-run-tests-batch-and-exitIntegration test notes:
- deterministic cross-module workflow/MCP persistence runs without credentials
- online provider checks run only when their credentials are available
- integration tests remain separate from the canonical unit suite
Run deterministic primary-loop end-to-end paths:
emacs -Q -batch -l tests/run-e2e-tests.el -f ert-run-tests-batch-and-exitdocs/README.mdfor the document indexdocs/PROJECT_STATUS.mdfor the current status snapshotdocs/troubleshooting-pitfalls.mdfor known issues and fixesdocs/context-budget.mdfor the per-category context allowances and overflow policydocs/resident-context.mdfor declaring instructions that must not be summarized.agents/for agent workflow records, decisions, logs, and stage history
- Read
AGENTS.mdbefore making changes - Read
.agents/README.md,.agents/00-entry/current.md,.agents/00-entry/read-order.md,.agents/10-active/focus.md, and.agents/10-active/risks.mdbefore implementation work - Update
.agents/after each completed stage - Add regression tests for each bug fix
- Do not use destructive git commands
Copyright 2026 chat.el contributors. This project is licensed under the One Public License (1PL).