mycode-coding-agent 0.1.0__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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Textual screens for the MyCode welcome and startup flow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from textual.css.query import NoMatches
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Vertical
|
|
10
|
+
from textual.screen import ModalScreen, Screen
|
|
11
|
+
from textual.widgets import Button, Input, LoadingIndicator, OptionList, Static
|
|
12
|
+
from textual.widgets.option_list import Option
|
|
13
|
+
|
|
14
|
+
from mycode.application.sessions import SessionStartRequest
|
|
15
|
+
from mycode.mcp.trust import MCPTrustRequest
|
|
16
|
+
from mycode.permissions import ConfirmationRequest, ConfirmationResult
|
|
17
|
+
from mycode.persistence.session_store import SessionRecord
|
|
18
|
+
from mycode.presentation.tui.widgets import (
|
|
19
|
+
SPLASH_LOGO,
|
|
20
|
+
ConversationView,
|
|
21
|
+
HeaderBar,
|
|
22
|
+
StatusBar,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _session_options(sessions: Sequence[SessionRecord]) -> list[Option]:
|
|
27
|
+
options = [
|
|
28
|
+
Option("Continue latest", id="continue"),
|
|
29
|
+
Option("New session", id="new"),
|
|
30
|
+
]
|
|
31
|
+
options.extend(
|
|
32
|
+
Option(
|
|
33
|
+
f"Resume: {session.title} {session.id[:8]}",
|
|
34
|
+
id=f"resume:{session.id}",
|
|
35
|
+
)
|
|
36
|
+
for session in sessions
|
|
37
|
+
)
|
|
38
|
+
return options
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WelcomeScreen(Screen[None]):
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
workspace: str,
|
|
46
|
+
model: str,
|
|
47
|
+
sessions: Sequence[SessionRecord] = (),
|
|
48
|
+
enabled: bool = False,
|
|
49
|
+
notice: str = "",
|
|
50
|
+
error: str = "",
|
|
51
|
+
) -> None:
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.workspace = workspace
|
|
54
|
+
self.model = model
|
|
55
|
+
self.sessions = tuple(sessions)
|
|
56
|
+
self.enabled = enabled
|
|
57
|
+
self.notice = notice
|
|
58
|
+
self.error = error
|
|
59
|
+
|
|
60
|
+
def compose(self) -> ComposeResult:
|
|
61
|
+
with Vertical(id="welcome-content"):
|
|
62
|
+
yield Static(SPLASH_LOGO, id="welcome-logo", markup=False)
|
|
63
|
+
yield Static(
|
|
64
|
+
f"Workspace {self.workspace}\nModel {self.model}",
|
|
65
|
+
id="welcome-meta",
|
|
66
|
+
markup=False,
|
|
67
|
+
)
|
|
68
|
+
if self.error:
|
|
69
|
+
yield Static(f"⚠ {self.error}", id="welcome-error")
|
|
70
|
+
elif self.notice:
|
|
71
|
+
yield Static(self.notice, id="welcome-notice")
|
|
72
|
+
yield OptionList(
|
|
73
|
+
*_session_options(self.sessions),
|
|
74
|
+
id="session-options",
|
|
75
|
+
disabled=not self.enabled,
|
|
76
|
+
)
|
|
77
|
+
yield Static(
|
|
78
|
+
"↑ ↓ select Enter confirm",
|
|
79
|
+
id="welcome-help",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def on_mount(self) -> None:
|
|
83
|
+
if self.enabled:
|
|
84
|
+
self.call_after_refresh(self._focus_options)
|
|
85
|
+
|
|
86
|
+
def _focus_options(self) -> None:
|
|
87
|
+
if not self.is_mounted or self.app.screen is not self:
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
self.query_one(OptionList).focus()
|
|
91
|
+
except NoMatches:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def request_from_option(option_id: str | None) -> SessionStartRequest | None:
|
|
96
|
+
if option_id == "new":
|
|
97
|
+
return SessionStartRequest(mode="new")
|
|
98
|
+
if option_id == "continue":
|
|
99
|
+
return SessionStartRequest(mode="continue")
|
|
100
|
+
if option_id is not None and option_id.startswith("resume:"):
|
|
101
|
+
return SessionStartRequest(
|
|
102
|
+
mode="resume",
|
|
103
|
+
session_id=option_id.removeprefix("resume:"),
|
|
104
|
+
)
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class LoadingScreen(Screen[None]):
|
|
109
|
+
def __init__(self, *, workspace: str, model: str, request_label: str) -> None:
|
|
110
|
+
super().__init__()
|
|
111
|
+
self.workspace = workspace
|
|
112
|
+
self.model = model
|
|
113
|
+
self.request_label = request_label
|
|
114
|
+
self.progress = "Opening session..."
|
|
115
|
+
|
|
116
|
+
def compose(self) -> ComposeResult:
|
|
117
|
+
with Vertical(id="loading-content"):
|
|
118
|
+
yield Static(SPLASH_LOGO, id="loading-logo", markup=False)
|
|
119
|
+
yield Static(
|
|
120
|
+
f"Workspace {self.workspace}\nModel {self.model}",
|
|
121
|
+
id="loading-meta",
|
|
122
|
+
markup=False,
|
|
123
|
+
)
|
|
124
|
+
yield Static(f"Session {self.request_label}", id="loading-session")
|
|
125
|
+
yield LoadingIndicator(id="loading-indicator")
|
|
126
|
+
yield Static(self.progress, id="loading-status")
|
|
127
|
+
|
|
128
|
+
def set_progress(self, value: str) -> None:
|
|
129
|
+
self.progress = value
|
|
130
|
+
if self.is_mounted:
|
|
131
|
+
self.query_one("#loading-status", Static).update(value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MainScreen(Screen[None]):
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
workspace: str,
|
|
139
|
+
model: str,
|
|
140
|
+
history: tuple[tuple[str, str], ...] = (),
|
|
141
|
+
) -> None:
|
|
142
|
+
super().__init__()
|
|
143
|
+
self.workspace = workspace
|
|
144
|
+
self.model = model
|
|
145
|
+
self.history = history
|
|
146
|
+
|
|
147
|
+
def compose(self) -> ComposeResult:
|
|
148
|
+
yield HeaderBar(self.workspace, self.model, id="header")
|
|
149
|
+
yield ConversationView(id="conversation")
|
|
150
|
+
yield StatusBar("Runtime Ready", id="status")
|
|
151
|
+
yield Input(placeholder="Ask MyCode...", id="prompt")
|
|
152
|
+
|
|
153
|
+
def on_mount(self) -> None:
|
|
154
|
+
self.app._activate_main_screen(self)
|
|
155
|
+
self.query_one(Input).focus()
|
|
156
|
+
|
|
157
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
158
|
+
content = event.value
|
|
159
|
+
event.input.value = ""
|
|
160
|
+
if content.strip():
|
|
161
|
+
self.app.submit_user_message(content)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class MCPTrustScreen(ModalScreen[bool]):
|
|
165
|
+
BINDINGS = [("escape", "reject", "Reject")]
|
|
166
|
+
|
|
167
|
+
def __init__(self, request: MCPTrustRequest) -> None:
|
|
168
|
+
super().__init__()
|
|
169
|
+
self.request = request
|
|
170
|
+
|
|
171
|
+
def compose(self) -> ComposeResult:
|
|
172
|
+
lines = [
|
|
173
|
+
"Project MCP trust request",
|
|
174
|
+
"Review the safe configuration summary before enabling MCP.",
|
|
175
|
+
"",
|
|
176
|
+
]
|
|
177
|
+
for server in self.request.servers:
|
|
178
|
+
lines.extend((f"server: {server.alias}", f"transport: {server.transport}"))
|
|
179
|
+
if server.transport == "stdio":
|
|
180
|
+
lines.append(f"command: {server.command!r}")
|
|
181
|
+
lines.append(f"args: {list(server.args)!r}")
|
|
182
|
+
if server.env_keys:
|
|
183
|
+
lines.append(f"env keys: {list(server.env_keys)!r}")
|
|
184
|
+
else:
|
|
185
|
+
lines.append(f"url template: {server.url_template!r}")
|
|
186
|
+
if server.destination is not None:
|
|
187
|
+
lines.append(f"destination: {server.destination!r}")
|
|
188
|
+
if server.header_keys:
|
|
189
|
+
lines.append(f"header keys: {list(server.header_keys)!r}")
|
|
190
|
+
lines.append("")
|
|
191
|
+
|
|
192
|
+
with Vertical(id="mcp-trust-dialog"):
|
|
193
|
+
yield Static("\n".join(lines), id="mcp-trust-details")
|
|
194
|
+
with Vertical(id="mcp-trust-actions"):
|
|
195
|
+
yield Button("Approve", id="mcp-trust-approve", variant="success")
|
|
196
|
+
yield Button("Reject", id="mcp-trust-reject", variant="error")
|
|
197
|
+
|
|
198
|
+
def on_mount(self) -> None:
|
|
199
|
+
self.query_one("#mcp-trust-approve", Button).focus()
|
|
200
|
+
|
|
201
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
202
|
+
if event.button.id == "mcp-trust-approve":
|
|
203
|
+
self.dismiss(True)
|
|
204
|
+
elif event.button.id == "mcp-trust-reject":
|
|
205
|
+
self.dismiss(False)
|
|
206
|
+
|
|
207
|
+
def action_reject(self) -> None:
|
|
208
|
+
self.dismiss(False)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
_PERMISSION_METADATA_KEYS = (
|
|
212
|
+
"resolved_path",
|
|
213
|
+
"workspace_root",
|
|
214
|
+
"path_scope",
|
|
215
|
+
"pattern_scope",
|
|
216
|
+
"command_display",
|
|
217
|
+
"resolved_cwd",
|
|
218
|
+
"cwd_scope",
|
|
219
|
+
"command_risk_category",
|
|
220
|
+
"command_risk",
|
|
221
|
+
"command_risk_reason",
|
|
222
|
+
"memory_scope",
|
|
223
|
+
"memory_kind",
|
|
224
|
+
"memory_key",
|
|
225
|
+
"memory_path",
|
|
226
|
+
"skill_name",
|
|
227
|
+
"skill_source",
|
|
228
|
+
"script",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _permission_details(request: ConfirmationRequest) -> str:
|
|
233
|
+
permission_request = request.permission_request
|
|
234
|
+
lines = [
|
|
235
|
+
f"tool: {permission_request.tool_name}",
|
|
236
|
+
f"capability: {permission_request.capability}",
|
|
237
|
+
f"action: {permission_request.action}",
|
|
238
|
+
]
|
|
239
|
+
if permission_request.target is not None:
|
|
240
|
+
lines.append(f"target: {permission_request.target}")
|
|
241
|
+
lines.append(f"reason: {request.permission_decision.reason}")
|
|
242
|
+
if request.prompt:
|
|
243
|
+
lines.append(f"prompt: {request.prompt}")
|
|
244
|
+
|
|
245
|
+
metadata = {**request.permission_decision.metadata, **request.metadata}
|
|
246
|
+
for key in _PERMISSION_METADATA_KEYS:
|
|
247
|
+
value = metadata.get(key)
|
|
248
|
+
if value is None:
|
|
249
|
+
continue
|
|
250
|
+
rendered = str(value)
|
|
251
|
+
if len(rendered) > 240:
|
|
252
|
+
rendered = rendered[:237] + "..."
|
|
253
|
+
lines.append(f"{key}: {rendered}")
|
|
254
|
+
return "\n".join(lines)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class PermissionScreen(ModalScreen[ConfirmationResult]):
|
|
258
|
+
BINDINGS = [("escape", "deny", "Deny")]
|
|
259
|
+
|
|
260
|
+
def __init__(self, request: ConfirmationRequest) -> None:
|
|
261
|
+
super().__init__()
|
|
262
|
+
self.request = request
|
|
263
|
+
|
|
264
|
+
def compose(self) -> ComposeResult:
|
|
265
|
+
with Vertical(id="permission-dialog"):
|
|
266
|
+
yield Static(
|
|
267
|
+
"Permission required\n\n" + _permission_details(self.request),
|
|
268
|
+
id="permission-details",
|
|
269
|
+
markup=False,
|
|
270
|
+
)
|
|
271
|
+
with Vertical(id="permission-actions"):
|
|
272
|
+
yield Button("Allow once", id="permission-allow-once", variant="success")
|
|
273
|
+
yield Button("Allow task", id="permission-allow-task", variant="success")
|
|
274
|
+
yield Button("Allow session", id="permission-allow-session", variant="success")
|
|
275
|
+
yield Button("Deny", id="permission-deny", variant="error")
|
|
276
|
+
|
|
277
|
+
def on_mount(self) -> None:
|
|
278
|
+
self.query_one("#permission-allow-once", Button).focus()
|
|
279
|
+
|
|
280
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
281
|
+
scopes = {
|
|
282
|
+
"permission-allow-once": "once",
|
|
283
|
+
"permission-allow-task": "task",
|
|
284
|
+
"permission-allow-session": "session",
|
|
285
|
+
}
|
|
286
|
+
if event.button.id in scopes:
|
|
287
|
+
self.dismiss(ConfirmationResult.approved(scope=scopes[event.button.id]))
|
|
288
|
+
elif event.button.id == "permission-deny":
|
|
289
|
+
self.dismiss(
|
|
290
|
+
ConfirmationResult.rejected(message="Permission denied by user.")
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def action_deny(self) -> None:
|
|
294
|
+
self.dismiss(
|
|
295
|
+
ConfirmationResult.rejected(message="Permission denied by user.")
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
__all__ = [
|
|
300
|
+
"LoadingScreen",
|
|
301
|
+
"MCPTrustScreen",
|
|
302
|
+
"MainScreen",
|
|
303
|
+
"PermissionScreen",
|
|
304
|
+
"WelcomeScreen",
|
|
305
|
+
]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Reusable widgets for the MyCode Textual presentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from textual.containers import VerticalScroll
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SPLASH_LOGO = r""" ╭──────────╮
|
|
13
|
+
│ >_ │
|
|
14
|
+
│ MYCODE │
|
|
15
|
+
╰──────────╯
|
|
16
|
+
|
|
17
|
+
__ __ ____ _
|
|
18
|
+
| \/ |_ _/ ___|___ __| | ___
|
|
19
|
+
| |\/| | | | | | / _ \ / _` |/ _ \
|
|
20
|
+
| | | | |_| | |__| (_) | (_| | __/
|
|
21
|
+
|_| |_|\__, |\____\___/ \__,_|\___|
|
|
22
|
+
|___/
|
|
23
|
+
|
|
24
|
+
Extensible Terminal Coding Agent"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class _ConversationEntry:
|
|
29
|
+
kind: str
|
|
30
|
+
content: str
|
|
31
|
+
name: str = ""
|
|
32
|
+
arguments: str = ""
|
|
33
|
+
completed: bool = False
|
|
34
|
+
ok: bool = True
|
|
35
|
+
summary: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _ConversationEntryWidget(Static):
|
|
39
|
+
"""Render one conversation entry that can be updated in place."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, entry: _ConversationEntry) -> None:
|
|
42
|
+
self.entry = entry
|
|
43
|
+
super().__init__(self._render_entry(entry), markup=False)
|
|
44
|
+
|
|
45
|
+
def update_entry(self, entry: _ConversationEntry) -> None:
|
|
46
|
+
self.entry = entry
|
|
47
|
+
self.update(self._render_entry(entry))
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _render_entry(entry: _ConversationEntry) -> Text:
|
|
51
|
+
if entry.kind == "user":
|
|
52
|
+
return Text.assemble(("You", "bold cyan"), "\n", entry.content)
|
|
53
|
+
if entry.kind == "assistant":
|
|
54
|
+
return Text.assemble(("MyCode", "bold green"), "\n", entry.content)
|
|
55
|
+
if entry.kind == "tool":
|
|
56
|
+
if not entry.completed:
|
|
57
|
+
line = f"› {entry.name}"
|
|
58
|
+
if entry.arguments:
|
|
59
|
+
line += f" {entry.arguments}"
|
|
60
|
+
return Text(line, style="yellow")
|
|
61
|
+
marker = "✓" if entry.ok else "✗"
|
|
62
|
+
line = f"{marker} {entry.name}"
|
|
63
|
+
if not entry.ok and entry.summary:
|
|
64
|
+
line += f" {entry.summary}"
|
|
65
|
+
return Text(line, style="green" if entry.ok else "red")
|
|
66
|
+
|
|
67
|
+
style = {
|
|
68
|
+
"warning": "yellow",
|
|
69
|
+
"error": "red",
|
|
70
|
+
"info": "dim",
|
|
71
|
+
}.get(entry.kind, "dim")
|
|
72
|
+
return Text(entry.content, style=style)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConversationView(VerticalScroll):
|
|
76
|
+
"""Incremental conversation projection with a model-backed transcript."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
79
|
+
super().__init__(*args, **kwargs)
|
|
80
|
+
self._entries: list[_ConversationEntry] = []
|
|
81
|
+
self._entry_widgets: dict[int, _ConversationEntryWidget] = {}
|
|
82
|
+
|
|
83
|
+
def add_user_message(self, content: str) -> None:
|
|
84
|
+
self._entries.append(_ConversationEntry("user", content))
|
|
85
|
+
self._mount_entry(len(self._entries) - 1)
|
|
86
|
+
|
|
87
|
+
def add_assistant_message(self, content: str) -> None:
|
|
88
|
+
self._entries.append(_ConversationEntry("assistant", content))
|
|
89
|
+
self._mount_entry(len(self._entries) - 1)
|
|
90
|
+
|
|
91
|
+
def add_history_message(self, role: str, content: str) -> None:
|
|
92
|
+
if role == "user":
|
|
93
|
+
self.add_user_message(content)
|
|
94
|
+
elif role == "assistant":
|
|
95
|
+
self.add_assistant_message(content)
|
|
96
|
+
|
|
97
|
+
def append_assistant_delta(self, content: str) -> None:
|
|
98
|
+
if self._entries and self._entries[-1].kind == "assistant":
|
|
99
|
+
entry_index = len(self._entries) - 1
|
|
100
|
+
self._entries[entry_index].content += content
|
|
101
|
+
self._refresh_entry(entry_index)
|
|
102
|
+
else:
|
|
103
|
+
self.add_assistant_message(content)
|
|
104
|
+
|
|
105
|
+
def add_notice(self, content: str, *, level: str = "info") -> None:
|
|
106
|
+
self._entries.append(_ConversationEntry(level, content))
|
|
107
|
+
self._mount_entry(len(self._entries) - 1)
|
|
108
|
+
|
|
109
|
+
def add_tool_activity(self, name: str, arguments: str) -> int:
|
|
110
|
+
token = len(self._entries)
|
|
111
|
+
self._entries.append(
|
|
112
|
+
_ConversationEntry("tool", "", name=name, arguments=arguments)
|
|
113
|
+
)
|
|
114
|
+
self._mount_entry(token)
|
|
115
|
+
return token
|
|
116
|
+
|
|
117
|
+
def complete_tool_activity(
|
|
118
|
+
self,
|
|
119
|
+
token: int,
|
|
120
|
+
name: str,
|
|
121
|
+
*,
|
|
122
|
+
ok: bool,
|
|
123
|
+
summary: str,
|
|
124
|
+
) -> None:
|
|
125
|
+
if token >= len(self._entries) or self._entries[token].kind != "tool":
|
|
126
|
+
self.add_notice(
|
|
127
|
+
f"⚠ tool result without pending call: {name}",
|
|
128
|
+
level="warning",
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
entry = self._entries[token]
|
|
132
|
+
entry.completed = True
|
|
133
|
+
entry.ok = ok
|
|
134
|
+
entry.summary = summary
|
|
135
|
+
self._refresh_entry(token)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def transcript_text(self) -> str:
|
|
139
|
+
lines: list[str] = []
|
|
140
|
+
for entry in self._entries:
|
|
141
|
+
if entry.kind in {"user", "assistant", "info", "warning", "error"}:
|
|
142
|
+
lines.append(entry.content)
|
|
143
|
+
elif entry.kind == "tool":
|
|
144
|
+
if entry.completed:
|
|
145
|
+
marker = "✓" if entry.ok else "✗"
|
|
146
|
+
line = f"{marker} {entry.name}"
|
|
147
|
+
if not entry.ok and entry.summary:
|
|
148
|
+
line += f" {entry.summary}"
|
|
149
|
+
lines.append(line)
|
|
150
|
+
else:
|
|
151
|
+
line = f"› {entry.name}"
|
|
152
|
+
if entry.arguments:
|
|
153
|
+
line += f" {entry.arguments}"
|
|
154
|
+
lines.append(line)
|
|
155
|
+
return "\n".join(lines)
|
|
156
|
+
|
|
157
|
+
def _mount_entry(self, index: int) -> None:
|
|
158
|
+
widget = _ConversationEntryWidget(self._entries[index])
|
|
159
|
+
self._entry_widgets[index] = widget
|
|
160
|
+
self.mount(widget)
|
|
161
|
+
self.call_after_refresh(self._scroll_to_end)
|
|
162
|
+
|
|
163
|
+
def _refresh_entry(self, index: int) -> None:
|
|
164
|
+
widget = self._entry_widgets.get(index)
|
|
165
|
+
if widget is None:
|
|
166
|
+
self._mount_entry(index)
|
|
167
|
+
return
|
|
168
|
+
widget.update_entry(self._entries[index])
|
|
169
|
+
self.call_after_refresh(self._scroll_to_end)
|
|
170
|
+
|
|
171
|
+
def _scroll_to_end(self) -> None:
|
|
172
|
+
if self.is_mounted:
|
|
173
|
+
self.scroll_end(animate=False, immediate=True, force=True)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class HeaderBar(Static):
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
workspace: str = "—",
|
|
180
|
+
model: str = "—",
|
|
181
|
+
*args,
|
|
182
|
+
**kwargs,
|
|
183
|
+
) -> None:
|
|
184
|
+
super().__init__(*args, **kwargs)
|
|
185
|
+
self._workspace = workspace or "—"
|
|
186
|
+
self._model = model or "—"
|
|
187
|
+
self._session = "—"
|
|
188
|
+
|
|
189
|
+
def on_mount(self) -> None:
|
|
190
|
+
self._refresh()
|
|
191
|
+
|
|
192
|
+
def set_session(self, value: str) -> None:
|
|
193
|
+
self._session = value or "—"
|
|
194
|
+
self._refresh()
|
|
195
|
+
|
|
196
|
+
def _refresh(self) -> None:
|
|
197
|
+
self.update(
|
|
198
|
+
Text(
|
|
199
|
+
f">_ MyCode {self._workspace} {self._model} "
|
|
200
|
+
f"session: {self._session}",
|
|
201
|
+
style="bold",
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class StatusBar(Static):
|
|
207
|
+
def __init__(self, value: str = "UI Ready", *args, **kwargs) -> None:
|
|
208
|
+
super().__init__(value, *args, **kwargs)
|
|
209
|
+
|
|
210
|
+
def set_status(self, value: str) -> None:
|
|
211
|
+
self.update(Text(value))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
__all__ = ["ConversationView", "HeaderBar", "SPLASH_LOGO", "StatusBar"]
|
mycode/project.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class ProjectIdentity:
|
|
9
|
+
key: str
|
|
10
|
+
workspace_root: Path
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_workspace(cls, workspace_root: str | Path) -> "ProjectIdentity":
|
|
14
|
+
resolved = Path(workspace_root).resolve(strict=False)
|
|
15
|
+
if not resolved.exists():
|
|
16
|
+
raise ValueError(f"Workspace root does not exist: {resolved}")
|
|
17
|
+
if not resolved.is_dir():
|
|
18
|
+
raise ValueError(f"Workspace root is not a directory: {resolved}")
|
|
19
|
+
|
|
20
|
+
normalized = os.path.normcase(str(resolved)).replace("\\", "/")
|
|
21
|
+
key = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
|
22
|
+
return cls(key=key, workspace_root=resolved)
|