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
25 changes: 10 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,20 +167,15 @@ customizations.
"api_key_file": "~/.config/gemini.token",
"default_model": "gemini-3.8-flash"
}
}
}
```
Configure per-project settings such as whether compilation is needed, shell commands for compiling/building code and running tests when invoked by chat tools (`build_code` and `test_code`), and the directory path where temporary git worktrees can be created. Project configuration is stored in `~/.var/vimini/projects/<project_name>`:
```json
{
"version": "0.1",
"configuration": {
"build-command": "make -j$(nproc)",
"test-command": "pytest",
"compilation-needed": false,
"worktree": "~/.var/vimini/worktree"
},
"files": []
"ui": {
"thinking_color": "DarkGray",
"prompt_color": "DarkBlue",
"waiting_color": "Green",
"service_color": "Green",
"error_color": "Red",
"prompt_hint_color": "Green"
}
}
```

Expand All @@ -198,9 +193,9 @@ Opens an interactive configuration editor in a new split window to configure glo

**How to use the global configuration manager:**
* Move the cursor to any configuration option or backend line.
* **`<Enter>` or `e`**: Edits the selected value. On `active_backend`, enter one of the configured backend names. On a backend header, rename the backend.
* **`<Enter>` or `e`**: Edits the selected value. On `active_backend`, enter one of the configured backend names. On a backend header, rename the backend. Under `UI PREFERENCES`, customize colors (e.g. `thinking_color`, `prompt_color`, `waiting_color`, `service_color`, `error_color`, `prompt_hint_color`).
* **`a`**: Adds a new backend and prompts for API type, default model, API key file, and base URL for OpenAI-compatible backends.
* **`d`**: Clears the selected backend field or deletes the selected backend after confirmation. The last backend cannot be deleted.
* **`d`**: Clears the selected backend field, deletes a backend after confirmation, or resets a UI preference back to default. The last backend cannot be deleted.
* **`r`**: Resets all global configuration options to their last saved state.
* **`q`**: Closes the configuration window. If modifications were made, a confirmation prompt asks whether to save changes permanently to `~/.var/vimini/config.json`.

Expand Down
25 changes: 20 additions & 5 deletions python3/vimini/agent/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,9 @@ def execute_project_tool(self, tool, cmd, current_req_id, conn):
f" {project_file_path}\n"
)
self.send_response(
current_req_id, conn, result={"status": "chunk", "text": msg}
current_req_id,
conn,
result={"status": "chunk", "text": msg, "tool_output": True},
)
return f"The tool '{tool}' is not available for this project because no {tool_label} command is configured in project options."

Expand All @@ -414,6 +416,7 @@ def execute_project_tool(self, tool, cmd, current_req_id, conn):
result={
"status": "chunk",
"text": f"\n[Executing {tool_label} command: {cmd}...]\n",
"tool_output": True,
},
)

Expand All @@ -433,7 +436,9 @@ def execute_project_tool(self, tool, cmd, current_req_id, conn):
for line in iter(proc.stdout.readline, ""):
output_lines.append(line)
self.send_response(
current_req_id, conn, result={"status": "chunk", "text": line}
current_req_id,
conn,
result={"status": "chunk", "text": line, "tool_output": True},
)

proc.wait()
Expand All @@ -442,7 +447,9 @@ def execute_project_tool(self, tool, cmd, current_req_id, conn):
except Exception as e:
err_text = f"Execution error: {e}\n"
self.send_response(
current_req_id, conn, result={"status": "chunk", "text": err_text}
current_req_id,
conn,
result={"status": "chunk", "text": err_text, "tool_output": True},
)
returncode = -1
full_output = err_text.strip()
Expand All @@ -452,7 +459,9 @@ def execute_project_tool(self, tool, cmd, current_req_id, conn):
)
chat_msg = f"\n[{tool_label.capitalize()} command {status_str}]\n"
self.send_response(
current_req_id, conn, result={"status": "chunk", "text": chat_msg}
current_req_id,
conn,
result={"status": "chunk", "text": chat_msg, "tool_output": True},
)

if returncode == 0:
Expand Down Expand Up @@ -899,7 +908,11 @@ def process_prompt_stream(current_prompt, current_req_id):
self.send_response(
current_req_id,
conn,
result={"status": "chunk", "text": msg},
result={
"status": "chunk",
"text": msg,
"tool_output": True,
},
)
responses.append(
ToolResponse(
Expand All @@ -923,6 +936,7 @@ def process_prompt_stream(current_prompt, current_req_id):
result={
"status": "chunk",
"text": f"\n[Agent {tool_label} command rejected due to schema error:\n{res_or_err}]\n",
"tool_output": True,
},
)
responses.append(
Expand Down Expand Up @@ -1157,6 +1171,7 @@ def process_prompt_stream(current_prompt, current_req_id):
result={
"status": "chunk",
"text": f"\n[{tool_label.capitalize()} command execution rejected by user]\n",
"tool_output": True,
},
)
result_text = (
Expand Down
133 changes: 112 additions & 21 deletions python3/vimini/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import subprocess
import os
from vimini import util, context
from vimini.handler import StreamBufferHandler, register_handler, unregister_handler
from vimini.handler import (
StreamBufferHandler,
register_handler,
unregister_handler,
get_handler,
T_PREFIX,
)
from vimini.common.util import get_project_config
from vimini.code import _DIFF_SEPARATOR, _process_x_diff_chunks

Expand All @@ -14,6 +20,14 @@

Q_prefix = "< "
A_prefix = "> "
T_prefix = T_PREFIX


def _get_a_prefix():
model_name = util.get_model_name()
if model_name:
return f"{model_name}{A_prefix}"
return A_prefix


def _to_str(val):
Expand Down Expand Up @@ -111,9 +125,7 @@ def _open_prompt_window(req_id):

prompt_buf[:] = [HINT_MSG, ""]

vim.command(
"highlight default ViminiPromptHint ctermfg=Green guifg=Green cterm=italic gui=italic"
)
util.setup_highlights()
vim.command("syntax match ViminiPromptHint '^Press .* to submit prompt$'")

# Buffer-local mappings to submit prompt
Expand Down Expand Up @@ -242,14 +254,18 @@ def submit_prompt(prompt_buf_num=None, prompt_text=None):
finally:
chat_buf.options["modifiable"] = 0

h = get_handler(req_id)
if isinstance(h, ChatChannelHandler):
h.reset_turn_state()

lines_to_add = []
last_line = chat_buf[-1] if len(chat_buf) > 0 else ""
if last_line != "":
lines_to_add.append("")
for pl in feedback.split("\n"):
lines_to_add.append(f"{Q_prefix}{pl}")
lines_to_add.append("---")
lines_to_add.append(A_prefix)
lines_to_add.append(_get_a_prefix())
_write_to_buffer(chat_buf, lines_to_add)
_set_waiting(chat_buf, True)

Expand Down Expand Up @@ -374,9 +390,7 @@ def _open_denial_prompt_window(req_id):

prompt_buf[:] = [HINT_MSG, DENIAL_PREFIX]

vim.command(
"highlight default ViminiPromptHint ctermfg=Green guifg=Green cterm=italic gui=italic"
)
util.setup_highlights()
vim.command("syntax match ViminiPromptHint '^Press .* to submit prompt$'")

# Buffer-local mappings to submit prompt
Expand Down Expand Up @@ -660,6 +674,65 @@ class ChatChannelHandler(StreamBufferHandler):

def __init__(self, req_id, buffer=None):
super().__init__(req_id, buffer=buffer, name_hint="Vimini Chat")
self.reset_turn_state()

def reset_turn_state(self):
super().reset_turn_state()
self.in_response = False

def _has_pending_a_prefix(self, buffer):
if not buffer:
return False
last = str(buffer[-1])
if last.startswith(T_prefix):
return False
if last == _get_a_prefix() or last == A_prefix:
return True
if len(buffer) >= 2 and buffer[-2] == "---" and last.endswith(A_prefix):
model_part = last[: -len(A_prefix)]
return bool(model_part) and all(
c.isalnum() or c in "-._ " for c in model_part
)
return False

def _clear_pending_a_prefix(self, buffer):
if self._has_pending_a_prefix(buffer):
try:
was_modifiable = bool(buffer.options["modifiable"])
except Exception:
was_modifiable = True
try:
buffer.options["modifiable"] = 1
if len(buffer) == 1:
buffer[:] = []
else:
del buffer[-1]
except Exception:
pass
finally:
if not was_modifiable:
try:
buffer.options["modifiable"] = 0
except Exception:
pass

def _write_response_chunk(self, buffer, text):
if not text:
return
if not self.in_response:
clean_text = text.lstrip("\n")
if not clean_text:
return
if self._has_pending_a_prefix(buffer):
_write_to_buffer(buffer, clean_text, append_to_last=True)
else:
prefixed_text = f"{_get_a_prefix()}{clean_text}"
if len(buffer) > 0 and not (len(buffer) == 1 and buffer[0] == ""):
prefixed_text = "\n" + prefixed_text
_write_to_buffer(buffer, prefixed_text, append_to_last=True)
self.in_response = True
else:
_write_to_buffer(buffer, text, append_to_last=True)

def handle_response(self, result):
if not isinstance(result, dict):
Expand All @@ -680,14 +753,26 @@ def handle_response(self, result):
except Exception:
verbose = True
if verbose and thought_text:
_write_to_buffer(buffer, thought_text, append_to_last=True)
self.in_response = False
if not self.in_thought:
self._clear_pending_a_prefix(buffer)
self.handle_thought(thought_text, verbose=True)

elif status == "chunk":
text = result.get("text", "")
if text:
_write_to_buffer(buffer, text, append_to_last=True)
self.in_thought = False
self.thought_at_line_start = True
if result.get("tool_output"):
self.reset_turn_state()
self._clear_pending_a_prefix(buffer)
_write_to_buffer(buffer, text, append_to_last=True)
else:
self._write_response_chunk(buffer, text)

elif status == "tool_use_requested":
self.reset_turn_state()
self._clear_pending_a_prefix(buffer)
tool = result.get("tool", "")
temp_file = result.get("temp_file")

Expand Down Expand Up @@ -753,15 +838,22 @@ def handle_response(self, result):
_set_waiting(buffer, False)
text = result.get("text", "")
if text:
_write_to_buffer(buffer, text, append_to_last=True)
self.in_thought = False
self.thought_at_line_start = True
self._write_response_chunk(buffer, text)
self.reset_turn_state()
_write_to_buffer(buffer, ["", WAITING_MSG])

elif status == "terminated":
_set_waiting(buffer, False)
self.reset_turn_state()
self._clear_pending_a_prefix(buffer)
self.finished = True

elif status == "error":
_set_waiting(buffer, False)
self.reset_turn_state()
self._clear_pending_a_prefix(buffer)
err_msg = result.get("error", "Unknown error")
error_lines = [""]
for line in str(err_msg).split("\n"):
Expand Down Expand Up @@ -801,6 +893,11 @@ def _send_prompt(prompt, buffer):
finally:
buffer.options["modifiable"] = 0

req_id = _to_str(buffer.vars.get("vimini_job_id", ""))
h = get_handler(req_id)
if isinstance(h, ChatChannelHandler):
h.reset_turn_state()

last_line = buffer[-1] if len(buffer) > 0 else ""
lines_to_add = []
if last_line != "":
Expand All @@ -810,7 +907,7 @@ def _send_prompt(prompt, buffer):
for pl in prompt_lines:
lines_to_add.append(f"{Q_prefix}{pl}")
lines_to_add.append("---")
lines_to_add.append(A_prefix)
lines_to_add.append(_get_a_prefix())

_write_to_buffer(buffer, lines_to_add)

Expand All @@ -824,7 +921,7 @@ def _send_prompt(prompt, buffer):
verbose = True
req = {
"jsonrpc": "2.0",
"id": _to_str(buffer.vars.get("vimini_job_id", "")),
"id": req_id,
"method": "chat",
"params": {
"verbose": verbose,
Expand Down Expand Up @@ -864,16 +961,10 @@ def chat():
buffer.options["filetype"] = "markdown"
buffer.options["swapfile"] = 0
buffer.options["modifiable"] = 0
vim.command("highlight default ViminiWaiting ctermfg=Green guifg=Green")
vim.command("highlight default ViminiPrompt ctermfg=DarkBlue guifg=DarkBlue")
vim.command(
"highlight default ViminiService ctermfg=Green guifg=Green cterm=italic gui=italic"
)
vim.command(
"highlight default ViminiError ctermfg=Red guifg=Red cterm=italic gui=italic"
)
util.setup_highlights()
vim.command("syntax match ViminiWaiting '^\\(Welcome.*\\|Waiting for prompt.*\\)'")
vim.command("syntax match ViminiPrompt '^< .*'")
vim.command("syntax match ViminiThinking '^T>.*'")
vim.command("syntax match ViminiService '^Agent Requested: .*'")
vim.command("syntax match ViminiError '^\\(\\[Error:.*\\|Error:.*\\)'")
vim.command("syntax match ViminiError '^Please prompt again to retry later.*'")
Expand Down
6 changes: 6 additions & 0 deletions python3/vimini/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def handle_response(self, result):

elif status == "completed":
self.finished = True
self.reset_turn_state()
files_to_process = result.get("files", [])
diff_output = result.get("diff_output", "")
project_root = (
Expand All @@ -73,6 +74,8 @@ def handle_response(self, result):
)

vim.command(f"call setbufvar({buf_num}, '&filetype', 'diff')")
util.setup_highlights()
vim.command("syntax match ViminiThinking '^T>.*'")

base_buffer_name = f"[{self.req_id}] Vimini Code"
try:
Expand All @@ -82,6 +85,7 @@ def handle_response(self, result):

elif status == "error":
self.finished = True
self.reset_turn_state()
err_msg = result.get("error", "Unknown error")
if buf_num:
util.write_to_buffer(
Expand Down Expand Up @@ -176,6 +180,8 @@ def code(prompt, verbose=False):
vim.command("setlocal bufhidden=wipe")
vim.command("setlocal noswapfile")
vim.command("setlocal filetype=markdown")
util.setup_highlights()
vim.command("syntax match ViminiThinking '^T>.*'")

code_buffer = vim.current.buffer
code_buffer_num = code_buffer.number
Expand Down
Loading
Loading