diff --git a/.gitignore b/.gitignore index bd20776..75377f7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,8 @@ __pycache__ .zed .git .mypy_cache -.github -toad.log -log.txt +*.github +*.log uv.lock agent.jsonl diff --git a/src/toad/acp/agent.py b/src/toad/acp/agent.py index 60fc34d..a787b34 100644 --- a/src/toad/acp/agent.py +++ b/src/toad/acp/agent.py @@ -1,5 +1,6 @@ import asyncio +from contextlib import suppress from datetime import datetime import json import os @@ -103,6 +104,8 @@ class Agent(AgentBase): log_filename: str = generate_datetime_filename(f"{agent['name']}", ".txt") if log_path := os.environ.get("TOAD_LOG"): self._log_file_path = Path(log_path).resolve().absolute() + with suppress(OSError): + self._log_file_path.unlink(missing_ok=True) else: self._log_file_path = paths.get_log() / log_filename @@ -142,7 +145,7 @@ class Agent(AgentBase): """Write log in a thread.""" try: with log_file_path.open("at") as log_file: - log_file.write(line) + log_file.write(f"{line.rstrip()}\n") except OSError: pass @@ -556,8 +559,12 @@ class Agent(AgentBase): await self.acp_new_session() except jsonrpc.APIError as error: if isinstance(error.data, dict): - reason = str(error.data.get("reason") or "") - details = str(error.data.get("details") or "") + reason = str( + error.data.get("reason") or "Failed to initialize agent" + ) + details = str( + error.data.get("details") or error.data.get("error") or "" + ) else: reason = "Failed to initialize agent" details = "" diff --git a/src/toad/data/agents/kimi.com.toml b/src/toad/data/agents/kimi.com.toml index 814122b..e9988b1 100644 --- a/src/toad/data/agents/kimi.com.toml +++ b/src/toad/data/agents/kimi.com.toml @@ -13,7 +13,7 @@ publisher_url = "https://willmcgugan.github.io/" type = "coding" description = "Kimi CLI is a new CLI agent that can help you with your software development tasks and terminal operations." tags = [] -run_command."*" = "kimi --acp" +run_command."*" = "kimi acp" help = ''' # Kimi CLI diff --git a/src/toad/toad.tcss b/src/toad/toad.tcss index 9a5b0dd..84edb05 100644 --- a/src/toad/toad.tcss +++ b/src/toad/toad.tcss @@ -90,7 +90,7 @@ Checkbox { } -MarkdownNote { +Note,MarkdownNote { padding: 0 1 0 0; &.about MarkdownTable { # Make the tables compact in /about @@ -399,6 +399,17 @@ Welcome { } +ACPToolCallContent { + Markdown { + margin: 0; + padding-left: 0; + MarkdownBlock:last-of-type { + margin-bottom: 0; + } + } +} + + Prompt { padding: 0 0 0 0; height: auto; @@ -481,10 +492,12 @@ Prompt { } } & > * { + # height: 0; margin: 1 0 0 0; - } + } + &:empty { - display: none; + height: 1; } } #option-container { diff --git a/src/toad/widgets/acp_content.py b/src/toad/widgets/acp_content.py index 5155e8b..c2e5c9c 100644 --- a/src/toad/widgets/acp_content.py +++ b/src/toad/widgets/acp_content.py @@ -21,8 +21,14 @@ class ACPToolCallContent(containers.VerticalGroup): def compose(self) -> ComposeResult: for content in self._content: match content: - case {"type": "content", "content": {"text": text}}: - yield widgets.Label(text) + case { + "type": "content", + "content": { + "type": "text", + "text": text, + }, + }: + yield widgets.Markdown(text) case { "type": "diff", "oldText": old_text, diff --git a/src/toad/widgets/conversation.py b/src/toad/widgets/conversation.py index 4869a7f..5f5a21a 100644 --- a/src/toad/widgets/conversation.py +++ b/src/toad/widgets/conversation.py @@ -723,7 +723,7 @@ class Conversation(containers.Vertical): if message.message: error = Content.assemble( Content.from_markup(message.message).stylize("$text-error"), - " - ", + " — ", Content.from_markup(message.details.strip()).stylize("dim"), ) else: @@ -1138,10 +1138,13 @@ class Conversation(containers.Vertical): kind = tool_call_update.get("kind", None) title = tool_call_update.get("title", "") or "" - print("request_permission") - from textual import log - - print(tool_call_update) + contents = tool_call_update.get("content", []) or [] + # If all the content is diffs, we will set kind to "edit" to show the permisisons screen + for content in contents: + if content.get("type") != "diff": + break + else: + kind = "edit" if kind == "edit": diffs: list[tuple[str, str, str | None, str]] = [] diff --git a/src/toad/widgets/diff_view.py b/src/toad/widgets/diff_view.py index b90d94e..4297cbe 100644 --- a/src/toad/widgets/diff_view.py +++ b/src/toad/widgets/diff_view.py @@ -285,11 +285,10 @@ class DiffView(containers.VerticalGroup): text_lines_a = self.code_before.splitlines() text_lines_b = self.code_after.splitlines() sequence_matcher = difflib.SequenceMatcher( - # lambda character: character in " \t", - None, + lambda character: character in " \t", text_lines_a, text_lines_b, - # autojunk=True, + autojunk=True, ) self._grouped_opcodes = list(sequence_matcher.get_grouped_opcodes()) @@ -339,6 +338,7 @@ class DiffView(containers.VerticalGroup): ) code_a_spans: list[Span] = [] code_b_spans: list[Span] = [] + for tag, i1, i2, j1, j2 in sequence_matcher.get_opcodes(): if ( tag diff --git a/src/toad/widgets/question.py b/src/toad/widgets/question.py index 59969ee..a909ab8 100644 --- a/src/toad/widgets/question.py +++ b/src/toad/widgets/question.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Callable, Iterable +from typing import Any, Callable from textual.app import ComposeResult from textual import events, on @@ -11,7 +11,8 @@ from textual.content import Content from textual.reactive import var, reactive from textual.message import Message from textual.widget import Widget -from textual.widgets import Label + +from textual import widgets from toad.answer import Answer @@ -28,7 +29,7 @@ class Ask: callback: Callable[[Answer], Any] | None = None -class NonSelectableLabel(Label): +class NonSelectableLabel(widgets.Label): ALLOW_SELECT = False @@ -244,10 +245,10 @@ class Question(containers.VerticalGroup, can_focus=True): def compose(self) -> ComposeResult: if self.title: - yield Label(self.title, id="title", markup=False) + yield widgets.Label(self.title, id="title", markup=False) - if self._get_content is not None: - with containers.VerticalGroup(id="contents"): + with containers.VerticalGroup(id="contents"): + if self._get_content is not None: yield self._get_content() with containers.VerticalGroup(id="option-container"):