Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,705 changes: 118 additions & 1,587 deletions lua/opencode/api.lua

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions lua/opencode/commands/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# AGENTS.md (commands)

This directory defines the command execution pipeline.

## Scope

- Parse command input into structured intent (`parse.lua`)
- Bind intent to executable action (`init.lua`)
- Execute lifecycle (`dispatch.lua`)
- Wire slash/keymap/API into the same axis

## Hard Invariants

- Single execution entry: `dispatch.execute(ctx)`
- Single bind point: `commands.bind_action_context(...)`
- `parse.lua` does not bind execute functions
- No `dispatch.run`, no `route.execute`, no fake parse wrapper

## Hook Model

- Stage hooks: `before`, `after`, `error`, `finally`
- Global hook: no command filter (`command = '*'` or omitted)
- Command-scoped hook: `command = 'run'` or `{'run','review'}`
- Keep hook handlers side-effect aware and idempotent

## Expected Execution Shape

```text
entry (:Opencode | keymap | API | slash)
-> parse/build intent
-> bind_action_context (single bind point)
-> dispatch.execute (single execute point)
-> hooks(before/after/error/finally)
```

## Editing Rules

- Prefer deleting duplicated glue code over adding adapters
- Keep error normalization in `dispatch.lua`
- Keep notify behavior unchanged unless explicitly requested
- Do not split semantics by entry (`:Opencode` / keymap / API / slash)

## Allow / Disallow Examples

- Allowed:
- entry adapters call `commands.build_parsed_intent(...)` + `commands.execute_parsed_intent(...)`
- infra changes that keep `dispatch.execute(ctx)` as the single execute entry
- Disallowed:
- fallback branches such as `if commands.execute_parsed_intent then ... else ...`
- building action context outside `commands.bind_action_context(...)`
- adding per-entry behavior forks (`if source == 'keymap' then ...`)

## Quick Review Checklist

- Does new code bypass `dispatch.execute`?
- Does new code create a second bind path?
- Does parse start carrying execute logic again?
- Are hook semantics consistent for all entries?

## Reject Conditions

- Any new execute entry besides `dispatch.execute`
- Any new bind path besides `bind_action_context`
- Parse starts binding execute functions again

## Minimal Regression Commands

- `./run_tests.sh -t tests/unit/commands_dispatch_spec.lua`
- `./run_tests.sh -t tests/unit/commands_parse_spec.lua`
- `./run_tests.sh -t tests/unit/commands/command_axis_spec.lua`
- `./run_tests.sh -t tests/unit/keymap_spec.lua`
- `./run_tests.sh -t tests/unit/api_spec.lua -f "command routing"`

## Entry Notes For New Agents

- Read `parse.lua`, `init.lua`, `dispatch.lua` in this order before editing.
- Treat this directory as **execution infrastructure**, not feature surface.
- If a change needs new behavior, prefer changing handlers first; only touch command infrastructure when all entries must change together.
- Keep edits local and reversible: no new execution entry, no new bind path, no per-entry semantic split.
78 changes: 78 additions & 0 deletions lua/opencode/commands/complete.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
local M = {}

---@param items string[]
---@param prefix string
---@return string[]
local function filter_by_prefix(items, prefix)
return vim.tbl_filter(function(item)
return vim.startswith(item, prefix)
end, items)
end

---@return string[]
local function user_command_completions()
local config_file = require('opencode.config_file')
local user_commands = config_file.get_user_commands():wait()
if not user_commands then
return {}
end

local names = vim.tbl_keys(user_commands)
table.sort(names)
return names
end

---@type table<string, fun(): string[]>
local provider_completions = {
user_commands = user_command_completions,
}

---@param subcmd_def OpencodeUICommand
---@param num_parts integer
---@return string[]
local function resolve_subcommand_completions(subcmd_def, num_parts)
if num_parts <= 3 then
if type(subcmd_def.completions) == 'table' then
return subcmd_def.completions --[[@as string[] ]]
end

if type(subcmd_def.completion_provider_id) == 'string' then
local provider = provider_completions[subcmd_def.completion_provider_id]
return provider and provider() or {}
end
end

if num_parts <= 4 and type(subcmd_def.sub_completions) == 'table' then
return subcmd_def.sub_completions --[[@as string[] ]]
end

return {}
end

---@param command_definitions table<string, OpencodeUICommand>
---@param arg_lead string
---@param cmd_line string
---@return string[]
function M.complete_command(command_definitions, arg_lead, cmd_line)
local parts = vim.split(cmd_line, '%s+', { trimempty = false })
local num_parts = #parts

if num_parts <= 2 then
local subcommands = vim.tbl_keys(command_definitions)
table.sort(subcommands)
return vim.tbl_filter(function(cmd)
return vim.startswith(cmd, arg_lead)
end, subcommands)
end

local subcommand = parts[2]
local subcmd_def = command_definitions[subcommand]

if not subcmd_def then
return {}
end

return filter_by_prefix(resolve_subcommand_completions(subcmd_def, num_parts), arg_lead)
end

return M
Loading
Loading