Skip to content

Commit af87a8b

Browse files
yaadatayaadata
authored andcommitted
refactor(codex): init is mainly a facade, no behavior change (#10)
Reviewed-on: https://codeberg.org/yaadata/codex.nvim/pulls/10 Co-authored-by: Yadi Abdalhalim <abdalhalim.yaadata@gmail.com> Co-committed-by: Yadi Abdalhalim <abdalhalim.yaadata@gmail.com>
1 parent f913c52 commit af87a8b

8 files changed

Lines changed: 898 additions & 666 deletions

File tree

docs/architecture.md

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
codex.nvim is a Neovim plugin that orchestrates an embedded terminal session for
66
the Codex CLI. The architecture centres on a **pluggable provider** abstraction:
77
all terminal management (opening, closing, sending text, focus, toggling) is
8-
delegated to a provider that satisfies a 9-method interface contract, while the
9-
core module (`init.lua`) owns session lifecycle, dependency wiring, and the
10-
public Lua API.
8+
delegated to a provider that satisfies a 9-method interface contract. The core
9+
module (`init.lua`) acts as a thin **facade** -- it owns dependency wiring and
10+
the public Lua API, while delegating session lifecycle, send dispatch, and
11+
mention orchestration to dedicated modules.
1112

1213
```
1314
plugin/codex.lua (entry point, version guard, load guard)
1415
1516
16-
lua/codex/init.lua (public API, session lifecycle, DI container)
17+
lua/codex/init.lua (public API facade, DI container, setup wiring)
1718
1819
├──► config.lua (defaults, validation, deep merge)
1920
├──► logger.lua (level-gated vim.notify wrapper)
@@ -24,12 +25,16 @@ lua/codex/init.lua (public API, session lifecycle, DI container)
2425
│ └── snacks.lua
2526
├──► context/
2627
│ ├── formatter.lua (selection + mention payload formatting)
28+
│ ├── mention.lua (mention orchestration, prompt capture/restore)
2729
│ ├── path.lua (CWD-relative path normalization)
2830
│ └── selection.lua (visual selection extraction)
2931
├──► runtime/
30-
│ └── send_queue.lua (FIFO queue + retry timer for startup readiness)
32+
│ ├── send_dispatch.lua (send pipeline, queue/retry orchestration)
33+
│ ├── send_queue.lua (FIFO queue + retry timer for startup readiness)
34+
│ └── terminal_io.lua (terminal I/O utilities + constants)
3135
└──► state/
32-
└── session_store.lua (session registry, alive/dead tracking)
36+
├── session_lifecycle.lua (session open/close/toggle/focus operations)
37+
└── session_store.lua (session registry, alive/dead tracking)
3338
```
3439

3540
## Directory Layout
@@ -40,9 +45,9 @@ codex.nvim/
4045
│ └── codex.lua # Entry point. Guards Neovim >= 0.11.0 and
4146
│ # prevents double-loading via vim.g.loaded_codex.
4247
├── lua/codex/
43-
│ ├── init.lua # Public API surface (setup, open, close, toggle,
48+
│ ├── init.lua # Public API facade (setup, open, close, toggle,
4449
│ │ # send, send_selection, mention_file, mention_directory, resume, etc.).
45-
│ │ # Owns the DI container and session lifecycle.
50+
│ │ # Owns the DI container, setup wiring, and thin delegates.
4651
│ ├── config.lua # Default config table, vim.validate-based
4752
│ │ # validation, and deep-merge with user options.
4853
│ ├── types.lua # EmmyLua annotations only (no runtime code).
@@ -66,15 +71,28 @@ codex.nvim/
6671
│ │ ├── formatter.lua # Formats selection payloads (fenced code blocks with
6772
│ │ │ # adaptive backtick fencing) and /mention payloads
6873
│ │ │ # (auto-quoting paths with special characters).
74+
│ │ ├── mention.lua # Mention orchestration: captures terminal prompt input,
75+
│ │ │ # dispatches /mention payloads, auto-submits, and restores
76+
│ │ │ # previously typed text. Uses create(opts) constructor.
6977
│ │ ├── path.lua # Normalizes file paths to CWD-relative form via
7078
│ │ │ # fnamemodify(":."). Falls back to the original path
7179
│ │ │ # on error.
7280
│ │ └── selection.lua # Extracts visual selection from the current buffer.
7381
│ │ # Resolves range via command args or visual marks.
7482
│ ├── runtime/
75-
│ │ └── send_queue.lua # Startup-readiness send queue. Owns retry scheduling
76-
│ │ # and FIFO flushing for deferred payload dispatch.
83+
│ │ ├── send_dispatch.lua # Send pipeline with startup/retry orchestration.
84+
│ │ │ # Builds PendingSend items, submits to send_queue,
85+
│ │ │ # handles session readiness checks and timeout logic.
86+
│ │ │ # Uses create(opts) constructor.
87+
│ │ ├── send_queue.lua # Startup-readiness send queue. Owns retry scheduling
88+
│ │ │ # and FIFO flushing for deferred payload dispatch.
89+
│ │ └── terminal_io.lua # Pure/near-pure terminal I/O utilities: bracketed paste
90+
│ │ # encoding, termcode expansion, prompt line parsing,
91+
│ │ # ANSI stripping, and shared constants.
7792
│ └── state/
93+
│ ├── session_lifecycle.lua # Session lifecycle operations: open, close, toggle,
94+
│ │ # focus, alive/ready checks, post-send refocus.
95+
│ │ # All functions take (deps, config) explicitly.
7896
│ └── session_store.lua # In-memory session registry. Tracks sessions by ID
7997
│ # with alive/dead lifecycle, active session pointer,
8098
│ # and monotonic counter for ID generation.
@@ -98,9 +116,11 @@ codex.nvim/
98116

99117
## Key Design Patterns
100118

101-
### Module Pattern
119+
### Module Patterns
102120

103-
Every Lua module follows the standard Neovim module pattern:
121+
#### Stateless modules
122+
123+
Most modules follow the standard Neovim module pattern:
104124

105125
```lua
106126
local M = {}
@@ -115,7 +135,36 @@ return M
115135
```
116136

117137
Functions on `M` are the public API; bare `local function` declarations are
118-
private. This convention is consistent across every file in `lua/codex/`.
138+
private. Examples: `terminal_io.lua`, `session_lifecycle.lua`, `config.lua`.
139+
140+
#### Constructor modules
141+
142+
Modules that need runtime accessors (closures over `get_deps`, `get_config`,
143+
etc.) use a `create(opts)` constructor that returns a table of bound functions:
144+
145+
```lua
146+
local M = {}
147+
148+
function M.create(opts)
149+
local get_deps = opts.get_deps
150+
151+
local function private_helper() ... end
152+
153+
local function public_method()
154+
local deps = get_deps()
155+
...
156+
end
157+
158+
return { public_method = public_method }
159+
end
160+
161+
return M
162+
```
163+
164+
`init.lua` calls `create()` during `setup()` and stores the returned instance
165+
in `state`. This pattern avoids circular dependencies -- extracted modules never
166+
`require("codex")` back. Examples: `send_dispatch.lua`, `mention.lua`,
167+
`send_queue.lua`.
119168

120169
### Provider Abstraction
121170

@@ -154,8 +203,13 @@ tracks:
154203

155204
The store exposes `create`, `get`, `get_active`, `mark_dead`, `remove`, `list`,
156205
and `reset`. Only one session is "active" at a time. When a provider fires its
157-
`on_exit` callback, `init.lua` walks the session list to find the matching
158-
handle and calls `mark_dead`.
206+
`on_exit` callback, `session_lifecycle.mark_session_dead_by_handle()` walks the
207+
session list to find the matching handle and calls `mark_dead`.
208+
209+
`state/session_lifecycle.lua` builds on the session store with higher-level
210+
operations: `open_session`, `close_session`, `toggle_session`, `focus_session`,
211+
and alive/ready checks. All functions take `(deps, config)` explicitly, making
212+
them stateless and testable without the full `init.lua` setup.
159213

160214
### Dependency Injection
161215

@@ -182,6 +236,18 @@ override the corresponding defaults. The `_deps` key is then stripped before
182236
config validation. This enables full isolation in unit tests: every collaborator
183237
(including `vim` itself) can be replaced with a mock.
184238

239+
After resolving deps and config, `setup()` wires the constructor modules:
240+
241+
1. `send_dispatch.create()` -- receives `get_deps`, `get_config`,
242+
`get_send_queue`, and an `open_session` closure.
243+
2. `send_queue.new()` -- receives the send dispatch's
244+
`process_pending_send_item` as its process callback.
245+
3. `mention.create()` -- receives `get_deps`, `get_config`, and a
246+
`dispatch_send` closure that delegates to the send dispatch instance.
247+
248+
This wiring order allows `send_dispatch` to reference the send queue (via
249+
closure) even though the queue is created after the dispatch instance.
250+
185251
### Error Handling
186252

187253
The codebase uses two error-reporting strategies:
@@ -202,15 +268,16 @@ failures downstream.
202268
### Auto-Open
203269

204270
APIs that need an active session (`send`, `send_command`, `focus`,
205-
`send_selection`, `mention_file`, `mention_directory`) automatically open one when needed. The
206-
lower-level `send` API opens without focus, while command-facing flows
207-
(`:CodexSend`, `:CodexAdd`) ensure the terminal is opened with focus before
208-
payload dispatch. If the provider handle is not yet ready, payloads are queued
209-
and retried on a timer (`terminal.startup.retry_interval_ms`) until ready or
210-
timeout (`terminal.startup.timeout_ms`). Queueing/scheduling is implemented in
211-
`runtime/send_queue.lua`, while `init.lua` owns session/open/reopen decisions.
212-
Providers apply a startup grace delay via `terminal.startup.grace_ms` before
213-
reporting readiness.
271+
`send_selection`, `mention_file`, `mention_directory`) automatically open one
272+
when needed. The lower-level `send` API opens without focus, while
273+
command-facing flows (`:CodexSend`, `:CodexMentionFile`) ensure the terminal is
274+
opened with focus before payload dispatch. If the provider handle is not yet
275+
ready, payloads are queued and retried on a timer
276+
(`terminal.startup.retry_interval_ms`) until ready or timeout
277+
(`terminal.startup.timeout_ms`). Queueing/scheduling is implemented in
278+
`runtime/send_queue.lua`, while `runtime/send_dispatch.lua` owns
279+
session/open/reopen decisions. Providers apply a startup grace delay via
280+
`terminal.startup.grace_ms` before reporting readiness.
214281

215282
## Component Interaction
216283

@@ -243,6 +310,8 @@ Key types:
243310
| `codex.SelectionSpec` | class | Visual selection data (defined in `formatter.lua`) |
244311
| `codex.SelectionOpts` | class | Options for selection extraction (defined in `selection.lua`) |
245312
| `codex.UserCommandOpts` | class | Neovim user command callback argument shape |
313+
| `codex.PendingSend` | class | Queued send item (defined in `send_dispatch.lua`) |
314+
| `codex.DispatchSendOpts` | class | Options for dispatch_send (defined in `send_dispatch.lua`) |
246315
| `codex.ResumeOpts` | class | Options for the resume API (defined in `init.lua`) |
247316
| `codex.SendResult` | alias | Boolean result alias (defined in `init.lua`) |
248317

docs/command-interactions.md

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
## Overview
44

55
This document is the canonical reference for how `:Codex*` commands map from
6-
`lua/codex/nvim/commands.lua` into the public API in `lua/codex/init.lua` and
7-
then into provider/session/runtime collaborators.
6+
`lua/codex/nvim/commands.lua` into the public API in `lua/codex/init.lua` (the
7+
facade) and then into `session_lifecycle`, `send_dispatch`, `mention`, and
8+
provider collaborators.
89

910
## Command Mapping
1011

@@ -36,6 +37,9 @@ User calls require("codex").setup(opts)
3637
init.lua setup()
3738
|- build deps (default_deps + opts._deps)
3839
|- apply config defaults + validation
40+
|- send_dispatch.create({ get_deps, get_config, get_send_queue, open_session })
41+
|- send_queue.new({ process = send_dispatch.process_pending_send_item })
42+
|- mention.create({ get_deps, get_config, dispatch_send })
3943
|- commands.register()
4044
|- keymaps.register(config)
4145
| |- unregister stale Codex keymaps from previous setup
@@ -56,16 +60,15 @@ User runs :Codex (or :Codex!)
5660
commands.lua -> codex.toggle() (or codex.open(true) for :Codex!)
5761
|
5862
v
59-
init.lua
60-
|- toggle():
61-
| |- ensure_setup()
63+
init.lua -> session_lifecycle
64+
|- toggle_session(deps, config):
6265
| |- session_store.get_active()
6366
| |- providers.resolve(config.terminal.provider)
6467
| |- [active + provider.is_alive(handle)]
6568
| | \- provider.toggle(handle, cmd, args, env, config)
6669
| | \- if new_handle returned, update session.handle
67-
| \- [no active session] open_session(args, focus=true)
68-
\- open(true): open_session(args, focus=true)
70+
| \- [no active session] open_session(deps, config, args, focus=true)
71+
\- open_session(deps, config, args, focus=true):
6972
|- provider.open(cmd, args, env, config, focus, on_exit_cb)
7073
\- session_store.create({ handle, cmd, cwd, provider_name })
7174
```
@@ -79,11 +82,12 @@ User runs :CodexFocus
7982
commands.lua -> codex.focus()
8083
|
8184
v
82-
init.lua focus()
85+
init.lua focus() -> session_lifecycle
8386
|- ensure_setup()
84-
|- session_store.get_active() + get_provider()
85-
|- [active + alive] -> provider.focus(session.handle)
86-
\- [no active/alive session] -> codex.open(true)
87+
|- focus_session(deps, config)
88+
| |- session_store.get_active() + get_provider()
89+
| \- [active + alive] -> provider.focus(session.handle), return true
90+
\- [not focused] -> codex.open(true)
8791
```
8892

8993
### `:CodexClose`
@@ -95,7 +99,7 @@ User runs :CodexClose
9599
commands.lua -> codex.close()
96100
|
97101
v
98-
init.lua close()
102+
init.lua close() -> session_lifecycle.close_session(deps, config, send_queue)
99103
|- session_store.get_active()
100104
|- [no active session] -> send_queue.reset() and return
101105
\- [active session]
@@ -115,9 +119,9 @@ commands.lua -> codex.clear_input()
115119
v
116120
init.lua clear_input()
117121
|- ensure_setup()
118-
|- session_store.get_active() + get_provider()
122+
|- session_lifecycle.get_active_session_and_provider(deps, config)
119123
|- [no alive session] -> return false, "no active Codex session"
120-
\- [alive session] -> provider.send(handle, encode_termcode("<C-c>"))
124+
\- [alive session] -> provider.send(handle, terminal_io.encode_termcode(deps, "<C-c>"))
121125
```
122126

123127
### `:CodexSend` (Range or Visual Selection)
@@ -140,9 +144,9 @@ init.lua send_selection()
140144
| \- return SelectionSpec { path, start_line, end_line, filetype, lines }
141145
|- formatter.format_selection(spec)
142146
| \- build fenced code block with adaptive backtick fencing
143-
\- dispatch_send(encode_bracketed_paste(payload), { open_focus=true, post_focus=true })
147+
\- send_dispatch.dispatch_send(terminal_io.encode_bracketed_paste(payload), ...)
144148
|- [active + ready] -> provider.send(session.handle, text)
145-
|- [no active session] -> open_session(args, focus=true)
149+
|- [no active session] -> session_lifecycle.open_session(...)
146150
\- [not ready yet] -> queue + retry loop until ready/timeout
147151
```
148152

@@ -155,17 +159,17 @@ User runs :CodexMentionFile [path] (or :CodexMentionDirectory [path])
155159
commands.lua -> codex.mention_file(path_or_nil) (or codex.mention_directory(path_or_nil))
156160
|
157161
v
158-
init.lua mention_file(path) / mention_directory(path)
162+
init.lua mention_file(path) / mention_directory(path) -> mention module
159163
|- ensure_setup()
160164
|- resolve path (arg or current buffer path via %:p / %:p:h)
161165
|- [missing path] -> log + return false, "current buffer has no file/directory path"
162166
|- path.to_relative(...)
163-
\- dispatch_mention(relative_path)
167+
\- mention.dispatch(relative_path)
164168
|- formatter.format_mention(relative_path)
165169
|- [active + alive] provider.focus(handle) before prompt capture
166170
|- capture_terminal_prompt_input() (best effort)
167171
|- mention_payload = clear_line_sequence + mention_text
168-
\- dispatch_send(mention_payload, { open_focus=true, pre_focus=true, command_path="/mention", on_sent=... })
172+
\- send_dispatch.dispatch_send(mention_payload, { open_focus=true, pre_focus=true, command_path="/mention", on_sent=... })
169173
|- on_sent: submit_with_enter_key("/mention")
170174
\- on_sent: restore captured prompt input via delayed dispatch_send(...)
171175
```
@@ -181,12 +185,12 @@ commands.lua -> codex.resume({ last = opts.bang })
181185
v
182186
init.lua resume(opts)
183187
|- ensure_setup()
184-
|- session_store.get_active() + get_provider()
188+
|- session_lifecycle.get_active_session_and_provider(deps, config)
185189
|- [active + alive session] -> send_command("resume") (in-process /resume)
186190
\- [no active/alive session]
187191
|- args = { "resume" }
188192
|- if opts.last then args += "--last"
189-
\- open_session(args, focus=true)
193+
\- session_lifecycle.open_session(deps, config, args, focus=true)
190194
```
191195

192196
### Slash Command Wrappers
@@ -195,7 +199,7 @@ These commands all route through `send_command()`, which normalizes the slash
195199
command, builds `"/<command>\n"`, and calls:
196200

197201
```text
198-
dispatch_send(payload, {
202+
send_dispatch.dispatch_send(payload, {
199203
open_focus = true,
200204
pre_focus = true,
201205
command_path = "/<command>",

docs/contributing.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ commit again.
176176

177177
### Module Structure
178178

179+
Stateless modules use the standard pattern:
180+
179181
```lua
180182
local M = {}
181183

@@ -188,6 +190,20 @@ function M.public_method() end
188190
return M
189191
```
190192

193+
Modules that need runtime accessors use a `create(opts)` constructor:
194+
195+
```lua
196+
local M = {}
197+
198+
function M.create(opts)
199+
local get_deps = opts.get_deps
200+
local function bound_method() ... end
201+
return { bound_method = bound_method }
202+
end
203+
204+
return M
205+
```
206+
191207
### Type Annotations
192208

193209
Use EmmyLua annotations for all public functions and types:
@@ -348,9 +364,12 @@ When cutting a new release tag:
348364

349365
## Adding a New Command
350366

351-
1. **API function** -- Add the public method in `lua/codex/init.lua` with a
352-
one-line LuaDoc summary (description first), `---@param`/`---@return`
353-
annotations, and an `ensure_setup()` guard.
367+
1. **API function** -- Add the public method in `lua/codex/init.lua` as a thin
368+
delegate with a one-line LuaDoc summary (description first),
369+
`---@param`/`---@return` annotations, and an `ensure_setup()` guard. Place
370+
the implementation logic in the appropriate extracted module
371+
(`session_lifecycle`, `send_dispatch`, or `mention`) if it fits an existing
372+
concern; otherwise keep it in `init.lua`.
354373
2. **User command** -- Register the `:Codex*` command in
355374
`lua/codex/nvim/commands.lua`, delegating to the API function.
356375
3. **Unit tests** -- Add test cases in `tests/unit/init_spec.lua` covering the

0 commit comments

Comments
 (0)