Skip to content

Commit be7c6a6

Browse files
Danilo Verde RibeiroDanilo Verde Ribeiro
authored andcommitted
fix(ui): use CLI-persisted mode field instead of client-side assistant_mode mutation
- Read message.mode field from CLI-persisted JSON (already stored by opencode) - Remove client-side assistant_mode mutation in core.lua (lines 145-154) - Add message.mode to Message type definition, mark assistant_mode as deprecated - Update session_formatter to prefer message.mode over message.assistant_mode - Maintain backward compatibility with fallback chain: mode → assistant_mode → current_mode → ASSISTANT - Eliminates need for client-side mode persistence since CLI handles it
1 parent 82cf73e commit be7c6a6

4 files changed

Lines changed: 101 additions & 14 deletions

File tree

.specify/symbols_spec.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Buffers' High-Level Symbols Feature Spec
2+
3+
## Overview
4+
5+
This feature enhances the `recent_buffers` context in `opencode.context` by optionally including high-level symbols (e.g., functions, variables, classes) extracted via LSP's `document_symbols()` for eligible buffers. The goal is to provide a structured overview of buffer contents without sending full file text, reducing token usage (~80% savings for config files) while aiding navigation and understanding of plugin interactions (e.g., exports/requires in Lua files).
6+
7+
- **Integration Point**: Modify `M.get_recent_buffers()` to conditionally fetch and append symbols data to each buffer entry.
8+
- **Config Location**: Add `symbols_only` (boolean, default: false) under `config.context.recent_buffers`. When true, symbols replace full content summary; when false, symbols are additional metadata.
9+
- **Output Format**: Append `symbols` array to buffer objects in `recent_buffers` context, e.g.:
10+
```lua
11+
{
12+
bufnr = 1,
13+
name = '/path/to/file.lua',
14+
lastused = 1234567890,
15+
changed = false,
16+
symbols = {
17+
{ name = 'setup', kind = 'Function', range = { start = {1,0}, ['end'] = {5,0} }, detail = '...' },
18+
{ name = 'opts', kind = 'Variable', range = { start = {10,0}, ['end'] = {15,0} }, detail = '...' },
19+
}
20+
}
21+
```
22+
- **Benefits**:
23+
- Reduces noise in AI prompts by focusing on structure.
24+
- Highlights key elements (e.g., `setup()` in Neovim plugins).
25+
- Pairs with `cursor_surrounding` for deeper context on demand.
26+
- **Drawbacks & Mitigations**:
27+
- LSP dependency: Skip if no client attached.
28+
- Performance: Fetch only for recent/active buffers; cache if needed.
29+
- Parser limits: Relies on Treesitter/LSP; fallback to nil if unavailable.
30+
31+
## Requirements
32+
33+
1. **Configurability**: User can enable/disable via `config.context.recent_buffers.symbols_only`.
34+
2. **Data Extraction**: Use `vim.lsp.buf.document_symbols()` to get hierarchical symbols (functions, variables, etc.).
35+
3. **Context Inclusion**: Serialize symbols into JSON-friendly format for `format_message()` parts.
36+
4. **Filtering**: Only include symbols for buffers meeting all constraints (below).
37+
5. **Staleness Handling**: Prioritize buffers with `lastused > 0` (recently accessed); limit to top N (e.g., 5) for symbols fetch to avoid overhead.
38+
6. **Error Handling**: Gracefully skip symbols if LSP call fails (e.g., no client, timeout); log via `vim.notify`.
39+
7. **Test Coverage**: Unit tests for enabled/disabled cases, constraint edge cases (e.g., <100 lines, no LSP).
40+
41+
## Constraints
42+
43+
- **Line Count**: Fetch symbols only if buffer has >100 lines (`vim.api.nvim_buf_line_count(bufnr) > 100`). Rationale: Small files (<100 lines) are cheap to send fully; symbols add little value.
44+
- **LSP Attached**: Only if LSP client(s) attached to buffer (`#vim.lsp.get_active_clients({ bufnr = bufnr }) > 0`). Skip otherwise to avoid errors.
45+
- **Editable Buffer**: Only for editable, file-based buffers:
46+
- Not readonly (`not vim.api.nvim_buf_get_option(bufnr, 'readonly')`).
47+
- Not terminal (`not vim.api.nvim_buf_is_valid(bufnr) or vim.bo[bufnr].buftype ~= 'terminal'`).
48+
- Listed and in cwd (`buflisted = true` and `is_in_cwd(name)`).
49+
Rationale: Symbols irrelevant for non-editable (e.g., help, quickfix) or transient buffers.
50+
51+
## Implementation Steps
52+
53+
1. **Config Update** (`lua/opencode/config.lua`):
54+
- Add `symbols_only` to `recent_buffers` schema/defaults.
55+
- Deep merge support for user overrides.
56+
57+
2. **Core Logic** (`lua/opencode/context.lua`):
58+
- In `M.get_recent_buffers()`:
59+
- After collecting base buffers, loop over eligible ones.
60+
- For each: Check constraints (lines >100, LSP attached, editable).
61+
- If met and `symbols_only` enabled: Call `vim.lsp.buf_request(bufnr, 'textDocument/documentSymbol', {}, handler)`.
62+
- Handler: Flatten hierarchy to array of {name, kind, range, detail}; append to buffer entry.
63+
- Async handling: Use promises or callbacks to avoid blocking; collect results post-fetch.
64+
- Update `M.load()` to call `get_recent_buffers()` as before.
65+
- In `format_context_part('recent_buffers', ...)`: Ensure symbols serialize correctly (JSON-safe).
66+
67+
3. **Edge Cases**:
68+
- No symbols: Append empty array `symbols = {}`.
69+
- Partial fetch: If some buffers qualify, include for those only.
70+
- Limit: Cap symbols per buffer (e.g., top 20) to prevent bloat.
71+
- Fallback: If `symbols_only=true` but constraints fail, include basic buffer info without symbols.
72+
73+
4. **Testing** (`tests/unit/context_spec.lua`):
74+
- Mock `vim.lsp.get_active_clients()` to return clients/nil.
75+
- Mock `vim.api.nvim_buf_line_count()` for line checks.
76+
- Mock `vim.lsp.buf_request()` to simulate symbol responses.
77+
- Test: Enabled with constraints met → symbols included; failed constraint → skipped; disabled → no symbols.
78+
79+
5. **Verification**:
80+
- Run `./run_tests.sh` → All pass.
81+
- Manual: Enable in config, open large Lua file with LSP → Check context includes symbols.
82+
- Token Savings: Compare prompt sizes with/without.
83+
84+
## Dependencies
85+
86+
- Neovim >=0.8 (LSP APIs).
87+
- Treesitter/LSP setup for language (e.g., lua_ls for Lua).
88+
- No external libs; use built-in `vim.lsp`.
89+
90+
## Potential Extensions
91+
92+
- Cache symbols per buffer (invalidate on write).
93+
- Support Treesitter fallback if no LSP.
94+
- Filter symbols by kind (e.g., only functions/variables).
95+
96+
This spec ensures elegant, constraint-driven implementation aligned with project principles: simplicity, performance, and spec-driven development.

lua/opencode/core.lua

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,6 @@ function M.after_run(prompt)
142142
require('opencode.history').write(prompt)
143143

144144
if state.windows then
145-
-- Persist assistant_mode on the latest assistant message if available
146-
local msgs = state.messages
147-
if msgs and #msgs > 0 then
148-
local last = msgs[#msgs]
149-
if last and last.role == 'assistant' then
150-
if not last.assistant_mode or last.assistant_mode == '' then
151-
last.assistant_mode = state.current_mode
152-
end
153-
end
154-
end
155145
ui.render_output()
156146
end
157147
end

lua/opencode/types.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@
257257
---@field providerID string Provider identifier
258258
---@field role 'user'|'assistant'|'system' Role of the message sender
259259
---@field system_role string|nil Role defined in system messages
260-
---@field assistant_mode string|nil Assistant mode active when message was created
260+
---@field mode string|nil Agent/mode used to create this message (from CLI)
261+
---@field assistant_mode string|nil Assistant mode active when message was created (deprecated)
261262
---@field error table
262263

263264
---@class RestorePoint

lua/opencode/ui/session_formatter.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,14 +293,14 @@ function M._format_message_header(message, msg_idx)
293293
M.output:add_empty_line()
294294
M.output:add_metadata({ msg_idx = msg_idx, part_idx = 1, role = role, type = 'header' })
295295

296-
-- Use the assistant_mode stored on the message only (stable label)
296+
-- Use the mode field from the message (stable label from CLI)
297297
local display_name
298298
if role == 'assistant' then
299-
local mode = message.assistant_mode
299+
local mode = message.mode or message.assistant_mode
300300
if mode and mode ~= '' then
301301
display_name = mode:upper()
302302
else
303-
-- For the most recent assistant message, show current_mode if assistant_mode is missing
303+
-- For the most recent assistant message, show current_mode if mode is missing
304304
-- This handles new messages that haven't been stamped yet
305305
local is_last_message = msg_idx == #state.messages
306306
if is_last_message and state.current_mode and state.current_mode ~= '' then

0 commit comments

Comments
 (0)