vibecore 0.3.0__py3-none-any.whl → 0.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vibecore/agents/default.py +3 -3
- vibecore/agents/task.py +3 -3
- vibecore/cli.py +67 -43
- vibecore/context.py +74 -11
- vibecore/flow.py +335 -73
- vibecore/handlers/stream_handler.py +35 -56
- vibecore/main.py +70 -272
- vibecore/session/jsonl_session.py +3 -1
- vibecore/session/loader.py +2 -2
- vibecore/settings.py +48 -1
- vibecore/tools/file/executor.py +59 -13
- vibecore/tools/file/tools.py +9 -9
- vibecore/tools/path_validator.py +251 -0
- vibecore/tools/python/helpers.py +2 -2
- vibecore/tools/python/tools.py +2 -2
- vibecore/tools/shell/executor.py +63 -7
- vibecore/tools/shell/tools.py +9 -9
- vibecore/tools/task/executor.py +2 -2
- vibecore/tools/task/tools.py +2 -2
- vibecore/tools/todo/manager.py +2 -10
- vibecore/tools/todo/models.py +5 -14
- vibecore/tools/todo/tools.py +5 -5
- vibecore/tools/webfetch/tools.py +1 -4
- vibecore/tools/websearch/ddgs/backend.py +1 -1
- vibecore/tools/websearch/tools.py +1 -4
- vibecore/widgets/core.py +3 -17
- vibecore/widgets/feedback.py +164 -0
- vibecore/widgets/feedback.tcss +121 -0
- vibecore/widgets/messages.py +22 -2
- vibecore/widgets/messages.tcss +28 -0
- vibecore/widgets/tool_messages.py +19 -4
- vibecore/widgets/tool_messages.tcss +23 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/METADATA +122 -29
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/RECORD +37 -34
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/WHEEL +0 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/entry_points.txt +0 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/licenses/LICENSE +0 -0
vibecore/tools/shell/tools.py
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from agents import RunContextWrapper, function_tool
|
|
4
4
|
|
|
5
|
-
from vibecore.context import
|
|
5
|
+
from vibecore.context import PathValidatorContext
|
|
6
6
|
|
|
7
7
|
from .executor import bash_executor, glob_files, grep_files, list_directory
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@function_tool
|
|
11
11
|
async def bash(
|
|
12
|
-
ctx: RunContextWrapper[
|
|
12
|
+
ctx: RunContextWrapper[PathValidatorContext],
|
|
13
13
|
command: str,
|
|
14
14
|
timeout: int | None = None,
|
|
15
15
|
description: str | None = None,
|
|
@@ -58,7 +58,7 @@ async def bash(
|
|
|
58
58
|
Returns:
|
|
59
59
|
The command output or error message
|
|
60
60
|
"""
|
|
61
|
-
output, exit_code = await bash_executor(command, timeout)
|
|
61
|
+
output, exit_code = await bash_executor(ctx, command, timeout)
|
|
62
62
|
if exit_code != 0:
|
|
63
63
|
return f"{output}\nExit code: {exit_code}"
|
|
64
64
|
return output
|
|
@@ -66,7 +66,7 @@ async def bash(
|
|
|
66
66
|
|
|
67
67
|
@function_tool
|
|
68
68
|
async def glob(
|
|
69
|
-
ctx: RunContextWrapper[
|
|
69
|
+
ctx: RunContextWrapper[PathValidatorContext],
|
|
70
70
|
pattern: str,
|
|
71
71
|
path: str | None = None,
|
|
72
72
|
) -> str:
|
|
@@ -90,7 +90,7 @@ async def glob(
|
|
|
90
90
|
Returns:
|
|
91
91
|
List of matching file paths, one per line
|
|
92
92
|
"""
|
|
93
|
-
files = await glob_files(pattern, path)
|
|
93
|
+
files = await glob_files(ctx, pattern, path)
|
|
94
94
|
if files and files[0].startswith("Error:"):
|
|
95
95
|
return files[0]
|
|
96
96
|
return "\n".join(files) if files else "No files found matching pattern"
|
|
@@ -98,7 +98,7 @@ async def glob(
|
|
|
98
98
|
|
|
99
99
|
@function_tool
|
|
100
100
|
async def grep(
|
|
101
|
-
ctx: RunContextWrapper[
|
|
101
|
+
ctx: RunContextWrapper[PathValidatorContext],
|
|
102
102
|
pattern: str,
|
|
103
103
|
path: str | None = None,
|
|
104
104
|
include: str | None = None,
|
|
@@ -124,7 +124,7 @@ async def grep(
|
|
|
124
124
|
Returns:
|
|
125
125
|
List of file paths containing matches, one per line
|
|
126
126
|
"""
|
|
127
|
-
files = await grep_files(pattern, path, include)
|
|
127
|
+
files = await grep_files(ctx, pattern, path, include)
|
|
128
128
|
if files and files[0].startswith("Error:"):
|
|
129
129
|
return files[0]
|
|
130
130
|
return "\n".join(files) if files else "No files found containing pattern"
|
|
@@ -132,7 +132,7 @@ async def grep(
|
|
|
132
132
|
|
|
133
133
|
@function_tool
|
|
134
134
|
async def ls(
|
|
135
|
-
ctx: RunContextWrapper[
|
|
135
|
+
ctx: RunContextWrapper[PathValidatorContext],
|
|
136
136
|
path: str,
|
|
137
137
|
ignore: list[str] | None = None,
|
|
138
138
|
) -> str:
|
|
@@ -150,7 +150,7 @@ async def ls(
|
|
|
150
150
|
Returns:
|
|
151
151
|
List of entries in the directory, one per line
|
|
152
152
|
"""
|
|
153
|
-
entries = await list_directory(path, ignore)
|
|
153
|
+
entries = await list_directory(ctx, path, ignore)
|
|
154
154
|
if entries and entries[0].startswith("Error:"):
|
|
155
155
|
return entries[0]
|
|
156
156
|
return "\n".join(entries) if entries else "Empty directory"
|
vibecore/tools/task/executor.py
CHANGED
|
@@ -8,12 +8,12 @@ from agents import (
|
|
|
8
8
|
from textual import log
|
|
9
9
|
|
|
10
10
|
from vibecore.agents.task import create_task_agent
|
|
11
|
-
from vibecore.context import
|
|
11
|
+
from vibecore.context import FullVibecoreContext
|
|
12
12
|
from vibecore.settings import settings
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
async def execute_task(
|
|
16
|
-
context:
|
|
16
|
+
context: FullVibecoreContext,
|
|
17
17
|
description: str,
|
|
18
18
|
prompt: str,
|
|
19
19
|
tool_name: str,
|
vibecore/tools/task/tools.py
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
from agents import function_tool
|
|
4
4
|
from agents.tool_context import ToolContext
|
|
5
5
|
|
|
6
|
-
from vibecore.context import
|
|
6
|
+
from vibecore.context import FullVibecoreContext
|
|
7
7
|
|
|
8
8
|
from .executor import execute_task
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@function_tool
|
|
12
12
|
async def task(
|
|
13
|
-
ctx: ToolContext[
|
|
13
|
+
ctx: ToolContext[FullVibecoreContext],
|
|
14
14
|
description: str,
|
|
15
15
|
prompt: str,
|
|
16
16
|
) -> str:
|
vibecore/tools/todo/manager.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from .models import TodoItem
|
|
5
|
+
from .models import TodoItem
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class TodoManager:
|
|
@@ -20,12 +20,4 @@ class TodoManager:
|
|
|
20
20
|
|
|
21
21
|
def write(self, todos: list[dict[str, Any]]) -> None:
|
|
22
22
|
"""Replace the entire todo list."""
|
|
23
|
-
self.todos = [
|
|
24
|
-
TodoItem(
|
|
25
|
-
id=todo["id"],
|
|
26
|
-
content=todo["content"],
|
|
27
|
-
status=TodoStatus(todo["status"]),
|
|
28
|
-
priority=TodoPriority(todo["priority"]),
|
|
29
|
-
)
|
|
30
|
-
for todo in todos
|
|
31
|
-
]
|
|
23
|
+
self.todos = [TodoItem(**todo) for todo in todos]
|
vibecore/tools/todo/models.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Todo data models."""
|
|
2
2
|
|
|
3
3
|
import uuid
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
4
|
from enum import Enum
|
|
6
5
|
|
|
7
|
-
from pydantic import BaseModel
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class TodoStatus(str, Enum):
|
|
@@ -19,18 +18,10 @@ class TodoPriority(str, Enum):
|
|
|
19
18
|
LOW = "low"
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
class TodoItem:
|
|
24
|
-
content: str
|
|
25
|
-
status: TodoStatus
|
|
26
|
-
priority: TodoPriority
|
|
27
|
-
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class TodoItemModel(BaseModel):
|
|
21
|
+
class TodoItem(BaseModel):
|
|
31
22
|
"""Pydantic model for todo items."""
|
|
32
23
|
|
|
33
|
-
id: str
|
|
24
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
34
25
|
content: str
|
|
35
|
-
status:
|
|
36
|
-
priority:
|
|
26
|
+
status: TodoStatus
|
|
27
|
+
priority: TodoPriority
|
vibecore/tools/todo/tools.py
CHANGED
|
@@ -4,13 +4,13 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
from agents import RunContextWrapper, function_tool
|
|
6
6
|
|
|
7
|
-
from vibecore.context import
|
|
7
|
+
from vibecore.context import TodoToolContext
|
|
8
8
|
|
|
9
|
-
from .models import
|
|
9
|
+
from .models import TodoItem
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@function_tool
|
|
13
|
-
async def todo_read(ctx: RunContextWrapper[
|
|
13
|
+
async def todo_read(ctx: RunContextWrapper[TodoToolContext]) -> list[dict[str, Any]]:
|
|
14
14
|
"""Use this tool to read the current to-do list for the session. This tool should be used proactively and
|
|
15
15
|
frequently to ensure that you are aware of the status of the current task list. You should make use of this
|
|
16
16
|
tool as often as possible, especially in the following situations:
|
|
@@ -38,7 +38,7 @@ async def todo_read(ctx: RunContextWrapper[VibecoreContext]) -> list[dict[str, A
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
@function_tool
|
|
41
|
-
async def todo_write(ctx: RunContextWrapper[
|
|
41
|
+
async def todo_write(ctx: RunContextWrapper[TodoToolContext], todos: list[TodoItem]) -> str:
|
|
42
42
|
"""Use this tool to create and manage a structured task list for your current coding session. This helps you
|
|
43
43
|
track progress, organize complex tasks, and demonstrate thoroughness to the user. It also helps the user
|
|
44
44
|
understand the progress of the task and overall progress of their requests.
|
|
@@ -106,6 +106,6 @@ async def todo_write(ctx: RunContextWrapper[VibecoreContext], todos: list[TodoIt
|
|
|
106
106
|
Success message.
|
|
107
107
|
"""
|
|
108
108
|
# Convert Pydantic models to dicts for the implementation
|
|
109
|
-
todos_dict = [todo.model_dump() for todo in todos]
|
|
109
|
+
todos_dict = [todo.model_dump() if isinstance(todo, TodoItem) else TodoItem(**todo).model_dump() for todo in todos]
|
|
110
110
|
ctx.context.todo_manager.write(todos_dict)
|
|
111
111
|
return "Todo list updated successfully."
|
vibecore/tools/webfetch/tools.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Webfetch tool for Vibecore agents."""
|
|
2
2
|
|
|
3
|
-
from agents import
|
|
4
|
-
|
|
5
|
-
from vibecore.context import VibecoreContext
|
|
3
|
+
from agents import function_tool
|
|
6
4
|
|
|
7
5
|
from .executor import fetch_url
|
|
8
6
|
from .models import WebFetchParams
|
|
@@ -10,7 +8,6 @@ from .models import WebFetchParams
|
|
|
10
8
|
|
|
11
9
|
@function_tool
|
|
12
10
|
async def webfetch(
|
|
13
|
-
ctx: RunContextWrapper[VibecoreContext],
|
|
14
11
|
url: str,
|
|
15
12
|
timeout: int = 30,
|
|
16
13
|
follow_redirects: bool = True,
|
|
@@ -31,7 +31,7 @@ class DDGSBackend(WebSearchBackend):
|
|
|
31
31
|
|
|
32
32
|
# Perform search (synchronous call)
|
|
33
33
|
# ddgs.text() expects 'query' as first positional argument
|
|
34
|
-
raw_results = ddgs.text(
|
|
34
|
+
raw_results = ddgs.text( # type: ignore
|
|
35
35
|
query=params.query,
|
|
36
36
|
region=params.region,
|
|
37
37
|
safesearch=params.safesearch,
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Websearch tool for Vibecore agents."""
|
|
2
2
|
|
|
3
|
-
from agents import
|
|
4
|
-
|
|
5
|
-
from vibecore.context import VibecoreContext
|
|
3
|
+
from agents import function_tool
|
|
6
4
|
|
|
7
5
|
from .executor import perform_websearch
|
|
8
6
|
from .models import SearchParams
|
|
@@ -10,7 +8,6 @@ from .models import SearchParams
|
|
|
10
8
|
|
|
11
9
|
@function_tool
|
|
12
10
|
async def websearch(
|
|
13
|
-
ctx: RunContextWrapper[VibecoreContext],
|
|
14
11
|
query: str,
|
|
15
12
|
max_results: int = 5,
|
|
16
13
|
region: str | None = None,
|
vibecore/widgets/core.py
CHANGED
|
@@ -5,7 +5,6 @@ from typing import ClassVar
|
|
|
5
5
|
from textual import events
|
|
6
6
|
from textual.app import ComposeResult
|
|
7
7
|
from textual.containers import Horizontal, ScrollableContainer, Vertical
|
|
8
|
-
from textual.geometry import Size
|
|
9
8
|
from textual.message import Message
|
|
10
9
|
from textual.widget import Widget
|
|
11
10
|
from textual.widgets import Footer, ProgressBar, Static, TextArea
|
|
@@ -94,10 +93,10 @@ class MyTextArea(TextArea):
|
|
|
94
93
|
history = []
|
|
95
94
|
|
|
96
95
|
# First, load history from the session
|
|
97
|
-
if app.session:
|
|
96
|
+
if app.runner.session:
|
|
98
97
|
try:
|
|
99
98
|
# Get all items from the session
|
|
100
|
-
session_items = await app.session.get_items()
|
|
99
|
+
session_items = await app.runner.session.get_items()
|
|
101
100
|
for item in session_items:
|
|
102
101
|
# Filter for user messages
|
|
103
102
|
if isinstance(item, dict):
|
|
@@ -113,13 +112,6 @@ class MyTextArea(TextArea):
|
|
|
113
112
|
|
|
114
113
|
log(f"Error loading session history: {e}")
|
|
115
114
|
|
|
116
|
-
# Then add current session's messages (in memory, not yet persisted)
|
|
117
|
-
for item in app.input_items:
|
|
118
|
-
if isinstance(item, dict) and item.get("role") == "user":
|
|
119
|
-
content = item.get("content")
|
|
120
|
-
if isinstance(content, str) and content not in history:
|
|
121
|
-
history.append(content)
|
|
122
|
-
|
|
123
115
|
return history
|
|
124
116
|
return []
|
|
125
117
|
|
|
@@ -218,13 +210,7 @@ class MyTextArea(TextArea):
|
|
|
218
210
|
class MainScroll(ScrollableContainer):
|
|
219
211
|
"""A container with vertical layout and an automatic scrollbar on the Y axis."""
|
|
220
212
|
|
|
221
|
-
|
|
222
|
-
"""Scroll to the bottom when resized = when new content is added."""
|
|
223
|
-
# If the scroll is near the end, keep the scroll sticky to the end
|
|
224
|
-
epsilon = 30
|
|
225
|
-
in_the_end = (size.height - (self.scroll_target_y + self.scrollable_size.height)) < epsilon
|
|
226
|
-
if size.height > self.scrollable_size.height and in_the_end:
|
|
227
|
-
self.scroll_end(animate=False)
|
|
213
|
+
...
|
|
228
214
|
|
|
229
215
|
|
|
230
216
|
class LoadingWidget(Widget):
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Feedback widget for collecting user feedback."""
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal, Vertical
|
|
5
|
+
from textual.content import Content
|
|
6
|
+
from textual.message import Message
|
|
7
|
+
from textual.reactive import reactive
|
|
8
|
+
from textual.widgets import Button, Checkbox, Static, TextArea
|
|
9
|
+
|
|
10
|
+
from vibecore.widgets.messages import BaseMessage, MessageHeader, MessageStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FeedbackWidget(BaseMessage):
|
|
14
|
+
"""A widget to collect user feedback with Good/Bad rating and optional text comment."""
|
|
15
|
+
|
|
16
|
+
class FeedbackSubmitted(Message):
|
|
17
|
+
"""Event emitted when feedback is submitted."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, rating: str, comment: str, criteria: dict[str, bool]) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Construct a FeedbackSubmitted message.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
rating: The rating ("good" or "bad").
|
|
25
|
+
comment: Optional text comment from the user.
|
|
26
|
+
criteria: Dict of criterion name to boolean value.
|
|
27
|
+
"""
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.rating = rating
|
|
30
|
+
self.comment = comment
|
|
31
|
+
self.criteria = criteria
|
|
32
|
+
|
|
33
|
+
rating: reactive[str | None] = reactive(None)
|
|
34
|
+
comment: reactive[str] = reactive("")
|
|
35
|
+
submitted: reactive[bool] = reactive(False)
|
|
36
|
+
show_comment_input: reactive[bool] = reactive(False)
|
|
37
|
+
show_criteria: reactive[bool] = reactive(False)
|
|
38
|
+
|
|
39
|
+
def __init__(self, prompt: str = "How was this response?", **kwargs) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Construct a FeedbackWidget.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
prompt: The prompt text to display.
|
|
45
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(status=MessageStatus.IDLE, **kwargs)
|
|
48
|
+
self.prompt = prompt
|
|
49
|
+
self.add_class("feedback-widget")
|
|
50
|
+
|
|
51
|
+
def get_header_params(self) -> tuple[str, str, bool]:
|
|
52
|
+
"""Get parameters for MessageHeader."""
|
|
53
|
+
return ("⏺", self.prompt, False)
|
|
54
|
+
|
|
55
|
+
def compose(self) -> ComposeResult:
|
|
56
|
+
"""Create child widgets for the feedback widget."""
|
|
57
|
+
yield MessageHeader("⏺", self.prompt, status=self.status)
|
|
58
|
+
|
|
59
|
+
with Horizontal(classes="feedback-controls"):
|
|
60
|
+
yield Button("👍 Good", id="feedback-good", classes="feedback-button good-button", variant="success")
|
|
61
|
+
yield Button("👎 Bad", id="feedback-bad", classes="feedback-button bad-button", variant="error")
|
|
62
|
+
|
|
63
|
+
# Feedback form containing criteria, comment area, and submit button (shown after rating selection)
|
|
64
|
+
with Vertical(id="feedback-form", classes="feedback-form"):
|
|
65
|
+
# Structured feedback criteria checkboxes
|
|
66
|
+
with Vertical(classes="feedback-criteria"):
|
|
67
|
+
yield Static("Please check any criteria that apply:", classes="criteria-label")
|
|
68
|
+
yield Checkbox("Factual accuracy - Was the information correct?", id="criteria-accuracy")
|
|
69
|
+
yield Checkbox("Task completion - Did it fully address the request?", id="criteria-completion")
|
|
70
|
+
yield Checkbox(
|
|
71
|
+
"Instruction following - Did it follow specific constraints or requirements?",
|
|
72
|
+
id="criteria-instructions",
|
|
73
|
+
)
|
|
74
|
+
yield Checkbox(
|
|
75
|
+
"Good format/structure - Are the format and structure appropriate?", id="criteria-format"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Comment text area
|
|
79
|
+
with Vertical(classes="feedback-comment-area"):
|
|
80
|
+
yield Static("Optional comment:", classes="comment-label")
|
|
81
|
+
yield TextArea(id="feedback-textarea", classes="feedback-textarea", show_line_numbers=False)
|
|
82
|
+
|
|
83
|
+
# Submit button
|
|
84
|
+
yield Button("Submit", id="feedback-submit", classes="feedback-submit-button", variant="primary")
|
|
85
|
+
|
|
86
|
+
with Vertical(id="feedback-result", classes="feedback-result"):
|
|
87
|
+
yield Static("", id="feedback-result-text")
|
|
88
|
+
|
|
89
|
+
def on_mount(self) -> None:
|
|
90
|
+
"""Initialize widget state on mount."""
|
|
91
|
+
# Hide feedback form and result initially
|
|
92
|
+
self.query_one("#feedback-form").display = False
|
|
93
|
+
self.query_one("#feedback-result").display = False
|
|
94
|
+
|
|
95
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
96
|
+
"""Handle button press events."""
|
|
97
|
+
button_id = event.button.id
|
|
98
|
+
|
|
99
|
+
if self.submitted:
|
|
100
|
+
return # Ignore clicks after submission
|
|
101
|
+
|
|
102
|
+
if button_id == "feedback-good":
|
|
103
|
+
self.rating = "good"
|
|
104
|
+
self._show_feedback_form()
|
|
105
|
+
event.button.add_class("selected")
|
|
106
|
+
self.query_one("#feedback-bad").remove_class("selected")
|
|
107
|
+
|
|
108
|
+
elif button_id == "feedback-bad":
|
|
109
|
+
self.rating = "bad"
|
|
110
|
+
self._show_feedback_form()
|
|
111
|
+
event.button.add_class("selected")
|
|
112
|
+
self.query_one("#feedback-good").remove_class("selected")
|
|
113
|
+
|
|
114
|
+
elif button_id == "feedback-submit":
|
|
115
|
+
self._submit_feedback()
|
|
116
|
+
|
|
117
|
+
def _show_feedback_form(self) -> None:
|
|
118
|
+
"""Show the feedback form (criteria, comment area, and submit button)."""
|
|
119
|
+
self.show_criteria = True
|
|
120
|
+
self.show_comment_input = True
|
|
121
|
+
feedback_form = self.query_one("#feedback-form")
|
|
122
|
+
feedback_form.display = True
|
|
123
|
+
feedback_form.refresh()
|
|
124
|
+
|
|
125
|
+
def _get_criteria_values(self) -> dict[str, bool]:
|
|
126
|
+
"""Get the current state of all criteria checkboxes."""
|
|
127
|
+
return {
|
|
128
|
+
"accuracy": self.query_one("#criteria-accuracy", Checkbox).value,
|
|
129
|
+
"completion": self.query_one("#criteria-completion", Checkbox).value,
|
|
130
|
+
"instructions": self.query_one("#criteria-instructions", Checkbox).value,
|
|
131
|
+
"format": self.query_one("#criteria-format", Checkbox).value,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def _submit_feedback(self) -> None:
|
|
135
|
+
"""Submit the feedback."""
|
|
136
|
+
if not self.rating:
|
|
137
|
+
# Require a rating before submission
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
self.submitted = True
|
|
141
|
+
self.comment = self.query_one("#feedback-textarea", TextArea).text
|
|
142
|
+
criteria = self._get_criteria_values()
|
|
143
|
+
|
|
144
|
+
# Hide controls and form, show result
|
|
145
|
+
self.query_one(".feedback-controls").display = False
|
|
146
|
+
self.query_one("#feedback-form").display = False
|
|
147
|
+
|
|
148
|
+
# Show result
|
|
149
|
+
result_container = self.query_one("#feedback-result")
|
|
150
|
+
result_text = self.query_one("#feedback-result-text", Static)
|
|
151
|
+
|
|
152
|
+
rating_emoji = "👍" if self.rating == "good" else "👎"
|
|
153
|
+
result_msg = f"{rating_emoji} Thank you for your feedback!"
|
|
154
|
+
if self.comment:
|
|
155
|
+
result_msg += f"\nComment: {self.comment}"
|
|
156
|
+
|
|
157
|
+
result_text.update(Content(result_msg))
|
|
158
|
+
result_container.display = True
|
|
159
|
+
|
|
160
|
+
# Update status to success
|
|
161
|
+
self.status = MessageStatus.SUCCESS
|
|
162
|
+
|
|
163
|
+
# Emit feedback event
|
|
164
|
+
self.post_message(self.FeedbackSubmitted(self.rating, self.comment, criteria))
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* Feedback widget styles */
|
|
2
|
+
|
|
3
|
+
FeedbackWidget {
|
|
4
|
+
color: $text;
|
|
5
|
+
|
|
6
|
+
.feedback-controls {
|
|
7
|
+
height: auto;
|
|
8
|
+
width: 1fr;
|
|
9
|
+
padding: 0 0 0 4;
|
|
10
|
+
margin-top: 1;
|
|
11
|
+
|
|
12
|
+
.feedback-button {
|
|
13
|
+
height: 3;
|
|
14
|
+
min-width: 12;
|
|
15
|
+
margin-right: 1;
|
|
16
|
+
|
|
17
|
+
&.selected {
|
|
18
|
+
text-style: bold;
|
|
19
|
+
border: heavy;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&.good-button {
|
|
23
|
+
background: $success;
|
|
24
|
+
color: $text;
|
|
25
|
+
|
|
26
|
+
&:hover {
|
|
27
|
+
background: $success-lighten-1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&.selected {
|
|
31
|
+
background: $success-darken-1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&.bad-button {
|
|
36
|
+
background: $error;
|
|
37
|
+
color: $text;
|
|
38
|
+
|
|
39
|
+
&:hover {
|
|
40
|
+
background: $error-lighten-1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.selected {
|
|
44
|
+
background: $error-darken-1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.feedback-form {
|
|
52
|
+
height: auto;
|
|
53
|
+
width: 1fr;
|
|
54
|
+
padding: 0 0 0 4;
|
|
55
|
+
margin-top: 1;
|
|
56
|
+
|
|
57
|
+
.feedback-criteria {
|
|
58
|
+
height: auto;
|
|
59
|
+
width: 1fr;
|
|
60
|
+
|
|
61
|
+
.criteria-label {
|
|
62
|
+
color: $text-muted;
|
|
63
|
+
margin-bottom: 0;
|
|
64
|
+
height: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Checkbox {
|
|
68
|
+
border: none;
|
|
69
|
+
margin: 0;
|
|
70
|
+
padding: 0;
|
|
71
|
+
color: $text;
|
|
72
|
+
|
|
73
|
+
&:focus {
|
|
74
|
+
background: $panel;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.feedback-comment-area {
|
|
80
|
+
height: auto;
|
|
81
|
+
width: 1fr;
|
|
82
|
+
margin-top: 1;
|
|
83
|
+
|
|
84
|
+
.comment-label {
|
|
85
|
+
color: $text-muted;
|
|
86
|
+
margin-bottom: 0;
|
|
87
|
+
height: 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.feedback-textarea {
|
|
91
|
+
height: 6;
|
|
92
|
+
width: 1fr;
|
|
93
|
+
border: tall $primary;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.feedback-submit-button {
|
|
98
|
+
height: 3;
|
|
99
|
+
min-width: 10;
|
|
100
|
+
margin-top: 1;
|
|
101
|
+
background: $primary;
|
|
102
|
+
color: $text;
|
|
103
|
+
|
|
104
|
+
&:hover {
|
|
105
|
+
background: $primary-lighten-1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.feedback-result {
|
|
111
|
+
height: auto;
|
|
112
|
+
width: 1fr;
|
|
113
|
+
padding: 1 0 0 4;
|
|
114
|
+
margin-top: 1;
|
|
115
|
+
color: $success;
|
|
116
|
+
|
|
117
|
+
#feedback-result-text {
|
|
118
|
+
color: $success;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
vibecore/widgets/messages.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from enum import StrEnum
|
|
2
3
|
|
|
3
4
|
from textual.app import ComposeResult
|
|
5
|
+
from textual.containers import Horizontal
|
|
4
6
|
from textual.content import Content
|
|
5
7
|
from textual.reactive import reactive
|
|
6
8
|
from textual.widget import Widget
|
|
7
|
-
from textual.widgets import Markdown, Static
|
|
9
|
+
from textual.widgets import Button, Markdown, Static
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class MessageStatus(StrEnum):
|
|
@@ -83,11 +85,16 @@ class MessageHeader(Widget):
|
|
|
83
85
|
# self.query_one(".prefix").visible = self._prefix_visible
|
|
84
86
|
|
|
85
87
|
def _on_mount(self, event) -> None:
|
|
88
|
+
disable_blink = bool(os.environ.get("TEXTUAL_SNAPSHOT_TEMPDIR"))
|
|
86
89
|
self.blink_timer = self.set_interval(
|
|
87
90
|
0.5,
|
|
88
91
|
self._toggle_cursor_blink_visible,
|
|
89
|
-
pause=(self.status != MessageStatus.EXECUTING),
|
|
92
|
+
pause=(self.status != MessageStatus.EXECUTING) or disable_blink,
|
|
90
93
|
)
|
|
94
|
+
# Ensure the prefix starts visible for executing statuses so snapshot tests
|
|
95
|
+
# and initial renders see the indicator before the first timer tick hides it.
|
|
96
|
+
if self.status == MessageStatus.EXECUTING:
|
|
97
|
+
self._prefix_visible = True
|
|
91
98
|
|
|
92
99
|
|
|
93
100
|
class BaseMessage(Widget):
|
|
@@ -167,6 +174,19 @@ class AgentMessage(BaseMessage):
|
|
|
167
174
|
"""Get parameters for MessageHeader."""
|
|
168
175
|
return ("⏺", self.text, True)
|
|
169
176
|
|
|
177
|
+
def compose(self) -> ComposeResult:
|
|
178
|
+
"""Create child widgets for the agent message."""
|
|
179
|
+
prefix, text, use_markdown = self.get_header_params()
|
|
180
|
+
with Horizontal(classes="agent-message-header"):
|
|
181
|
+
yield MessageHeader(prefix, text, status=self.status, use_markdown=use_markdown)
|
|
182
|
+
yield Button("Copy", classes="copy-button", variant="primary")
|
|
183
|
+
|
|
184
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
185
|
+
"""Handle button press events."""
|
|
186
|
+
if event.button.has_class("copy-button"):
|
|
187
|
+
# Copy the markdown text to clipboard
|
|
188
|
+
self.app.copy_to_clipboard(self.text)
|
|
189
|
+
|
|
170
190
|
def update(self, text: str, status: MessageStatus | None = None) -> None:
|
|
171
191
|
"""Update the text of the agent message."""
|
|
172
192
|
self.text = text
|
vibecore/widgets/messages.tcss
CHANGED
|
@@ -62,6 +62,34 @@ UserMessage {
|
|
|
62
62
|
|
|
63
63
|
AgentMessage {
|
|
64
64
|
color: $text;
|
|
65
|
+
|
|
66
|
+
Horizontal.agent-message-header {
|
|
67
|
+
height: auto;
|
|
68
|
+
width: 1fr;
|
|
69
|
+
layers: main button;
|
|
70
|
+
|
|
71
|
+
.copy-button {
|
|
72
|
+
layer: button;
|
|
73
|
+
dock: right;
|
|
74
|
+
height: 1;
|
|
75
|
+
width: 8;
|
|
76
|
+
min-width: 8;
|
|
77
|
+
margin: 0;
|
|
78
|
+
padding: 0;
|
|
79
|
+
background: $secondary;
|
|
80
|
+
color: $text;
|
|
81
|
+
border: none;
|
|
82
|
+
|
|
83
|
+
&:hover {
|
|
84
|
+
background: $secondary-lighten-1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&:focus {
|
|
88
|
+
background: $secondary-lighten-1;
|
|
89
|
+
text-style: bold;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
SystemMessage {
|