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[ :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