Packages

CMDC TUI — 基于 ExRatatui 的 AI Agent 终端交互界面

Current section

Files

Jump to
cmdc_tui lib cmdc_tui.ex
Raw

lib/cmdc_tui.ex

defmodule CmdcTui do
@moduledoc """
CMDC TUI — 基于 ExRatatui 的 AI Agent 终端交互界面。
## 快速开始
# 启动 TUI,连接已有的 CMDC session
CmdcTui.start(session: session_pid)
# 或者让外部先创建 Agent,拿到 session 后启动 TUI
{:ok, session} = CMDC.create_agent(model: "anthropic:claude-sonnet-4-5", ...)
CmdcTui.start(session: session)
"""
require Logger
@doc """
启动 TUI 应用(阻塞到退出)。
直接模式:`ExRatatui.run` + 紧凑事件循环。
`poll_event``draw` 在同一线程顺序执行,无 DirtyIo scheduler 切换开销,
`debug_input.exs` 一致的零延迟体验。
## 选项
- `:session` — 已有 CMDC session pid
- `:session_id` — 会话 ID(可选,用于显示)
- `:model_name` — 模型名称(可选,显示用)
"""
@spec start(keyword()) :: :ok | {:error, term()}
def start(opts \\ []) do
prev_level = Logger.level()
Logger.configure(level: :none)
write_tty("\e[>1u")
result =
ExRatatui.run(fn terminal ->
textarea = ExRatatui.textarea_new()
opts = Keyword.put(opts, :textarea, textarea)
state = CmdcTui.State.new(opts)
if state.session, do: CMDC.subscribe(state.session)
r = loop(terminal, state)
if state.session, do: CMDC.unsubscribe(state.session)
r
end)
write_tty("\e[<u")
Logger.configure(level: prev_level)
result
end
# 紧凑事件循环——不在 try 内,保证尾递归优化
defp loop(terminal, state) do
draw(terminal, state)
case ExRatatui.poll_event(50) do
%ExRatatui.Event.Key{code: "c", modifiers: ["ctrl"]} ->
:ok
%ExRatatui.Event.Key{} = key ->
case handle_key(key, state) do
{:ok, new_state} ->
new_state = drain_keys(new_state)
loop(terminal, new_state)
:stop ->
:ok
end
_ ->
state = receive_cmdc_events(state)
state = tick_throbber(state)
loop(terminal, state)
end
end
defp handle_key(key, state) do
case CmdcTui.KeyHandler.handle(key, state) do
{:noreply, new_state} -> {:ok, new_state}
{:stop, _} -> :stop
end
end
defp drain_keys(state) do
case ExRatatui.poll_event(0) do
%ExRatatui.Event.Key{code: "c", modifiers: ["ctrl"]} ->
state
%ExRatatui.Event.Key{} = key ->
case handle_key(key, state) do
{:ok, new_state} -> drain_keys(new_state)
:stop -> state
end
_ ->
state
end
end
defp receive_cmdc_events(state) do
receive do
{:cmdc_event, _sid, event} ->
state = CmdcTui.EventRouter.route(event, state)
receive_cmdc_events(state)
after
0 -> state
end
end
defp tick_throbber(%{agent_status: s} = state) when s in [:streaming, :running] do
%{state | throbber_step: state.throbber_step + 1}
end
defp tick_throbber(state), do: state
defp draw(terminal, state) do
{w, h} = ExRatatui.terminal_size()
frame = %ExRatatui.Frame{width: w, height: h}
widgets = CmdcTui.App.render(state, frame)
ExRatatui.draw(terminal, widgets)
end
# Kitty keyboard protocol — CSI u
# flags=1 (DISAMBIGUATE_ESCAPE_CODES): 让终端区分 Enter vs Shift+Enter
# 在 ExRatatui.run 回调外部(raw mode 之前)写入,避免干扰 crossterm
defp write_tty(data) do
case :file.open(~c"/dev/tty", [:write, :raw, :binary]) do
{:ok, fd} ->
:file.write(fd, data)
:file.close(fd)
_ ->
:ok
end
end
end