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,253 @@
|
|
|
1
|
+
"""Thread-to-UI bridges for synchronous presentation interactions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from threading import Event, Lock
|
|
8
|
+
from typing import TYPE_CHECKING, Callable
|
|
9
|
+
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
|
|
12
|
+
from mycode.mcp.trust import MCPTrustRequest, MCPTrustWarning
|
|
13
|
+
from mycode.permissions import ConfirmationRequest, ConfirmationResult
|
|
14
|
+
from mycode.subagents.contracts import SubAgentTask
|
|
15
|
+
from mycode.subagents.lifecycle import SubAgentStateTransition
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from mycode.subagents.runtime import SubAgentExecution
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(eq=False)
|
|
22
|
+
class MCPTrustResponseHandle:
|
|
23
|
+
request: MCPTrustRequest
|
|
24
|
+
_event: Event = field(default_factory=Event, init=False, repr=False)
|
|
25
|
+
_lock: Lock = field(default_factory=Lock, init=False, repr=False)
|
|
26
|
+
_approved: bool = field(default=False, init=False, repr=False)
|
|
27
|
+
_resolved: bool = field(default=False, init=False, repr=False)
|
|
28
|
+
|
|
29
|
+
def resolve(self, approved: bool) -> None:
|
|
30
|
+
with self._lock:
|
|
31
|
+
if self._resolved:
|
|
32
|
+
return
|
|
33
|
+
self._approved = approved
|
|
34
|
+
self._resolved = True
|
|
35
|
+
self._event.set()
|
|
36
|
+
|
|
37
|
+
def wait(self) -> bool:
|
|
38
|
+
self._event.wait()
|
|
39
|
+
return self._approved
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MCPTrustRequestMessage(Message):
|
|
43
|
+
def __init__(self, handle: MCPTrustResponseHandle) -> None:
|
|
44
|
+
self.handle = handle
|
|
45
|
+
super().__init__()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MCPTrustWarningMessage(Message):
|
|
49
|
+
def __init__(self, warning: MCPTrustWarning) -> None:
|
|
50
|
+
self.warning = warning
|
|
51
|
+
super().__init__()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(eq=False)
|
|
55
|
+
class PermissionResponseHandle:
|
|
56
|
+
request: ConfirmationRequest
|
|
57
|
+
_event: Event = field(default_factory=Event, init=False, repr=False)
|
|
58
|
+
_lock: Lock = field(default_factory=Lock, init=False, repr=False)
|
|
59
|
+
_result: ConfirmationResult = field(
|
|
60
|
+
default_factory=ConfirmationResult.rejected,
|
|
61
|
+
init=False,
|
|
62
|
+
repr=False,
|
|
63
|
+
)
|
|
64
|
+
_resolved: bool = field(default=False, init=False, repr=False)
|
|
65
|
+
|
|
66
|
+
def resolve(self, result: ConfirmationResult) -> None:
|
|
67
|
+
with self._lock:
|
|
68
|
+
if self._resolved:
|
|
69
|
+
return
|
|
70
|
+
self._result = result
|
|
71
|
+
self._resolved = True
|
|
72
|
+
self._event.set()
|
|
73
|
+
|
|
74
|
+
def wait(self) -> ConfirmationResult:
|
|
75
|
+
self._event.wait()
|
|
76
|
+
return self._result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PermissionRequestMessage(Message):
|
|
80
|
+
def __init__(self, handle: PermissionResponseHandle) -> None:
|
|
81
|
+
self.handle = handle
|
|
82
|
+
super().__init__()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TuiMCPTrustConfirmer:
|
|
86
|
+
"""Adapt synchronous MCP trust confirmation to Textual messages."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, post_message: Callable[[Message], bool]) -> None:
|
|
89
|
+
self._post_message = post_message
|
|
90
|
+
self._lock = Lock()
|
|
91
|
+
self._pending: set[MCPTrustResponseHandle] = set()
|
|
92
|
+
self._closed = False
|
|
93
|
+
|
|
94
|
+
def confirm(self, request: MCPTrustRequest) -> bool:
|
|
95
|
+
handle = MCPTrustResponseHandle(request)
|
|
96
|
+
with self._lock:
|
|
97
|
+
if self._closed:
|
|
98
|
+
return False
|
|
99
|
+
self._pending.add(handle)
|
|
100
|
+
try:
|
|
101
|
+
if not self._post_message(MCPTrustRequestMessage(handle)):
|
|
102
|
+
handle.resolve(False)
|
|
103
|
+
return handle.wait()
|
|
104
|
+
finally:
|
|
105
|
+
with self._lock:
|
|
106
|
+
self._pending.discard(handle)
|
|
107
|
+
|
|
108
|
+
def report_warning(self, warning: MCPTrustWarning) -> None:
|
|
109
|
+
self._post_message(MCPTrustWarningMessage(warning))
|
|
110
|
+
|
|
111
|
+
def reject_all(self) -> None:
|
|
112
|
+
with self._lock:
|
|
113
|
+
self._closed = True
|
|
114
|
+
pending = tuple(self._pending)
|
|
115
|
+
self._pending.clear()
|
|
116
|
+
for handle in pending:
|
|
117
|
+
handle.resolve(False)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TuiConfirmer:
|
|
121
|
+
"""Bridge synchronous Permission confirmation to the Textual UI."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, post_message: Callable[[Message], bool]) -> None:
|
|
124
|
+
self._post_message = post_message
|
|
125
|
+
self._lock = Lock()
|
|
126
|
+
self._pending: set[PermissionResponseHandle] = set()
|
|
127
|
+
self._closed = False
|
|
128
|
+
|
|
129
|
+
def confirm(self, request: ConfirmationRequest) -> ConfirmationResult:
|
|
130
|
+
handle = PermissionResponseHandle(request)
|
|
131
|
+
with self._lock:
|
|
132
|
+
if self._closed:
|
|
133
|
+
return ConfirmationResult.rejected(
|
|
134
|
+
message="Permission confirmation unavailable.",
|
|
135
|
+
)
|
|
136
|
+
self._pending.add(handle)
|
|
137
|
+
try:
|
|
138
|
+
if not self._post_message(PermissionRequestMessage(handle)):
|
|
139
|
+
handle.resolve(
|
|
140
|
+
ConfirmationResult.rejected(
|
|
141
|
+
message="Permission confirmation unavailable.",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
return handle.wait()
|
|
145
|
+
finally:
|
|
146
|
+
with self._lock:
|
|
147
|
+
self._pending.discard(handle)
|
|
148
|
+
|
|
149
|
+
def reject_all(self) -> None:
|
|
150
|
+
with self._lock:
|
|
151
|
+
self._closed = True
|
|
152
|
+
pending = tuple(self._pending)
|
|
153
|
+
self._pending.clear()
|
|
154
|
+
for handle in pending:
|
|
155
|
+
handle.resolve(
|
|
156
|
+
ConfirmationResult.rejected(
|
|
157
|
+
message="Permission confirmation unavailable.",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class SubAgentStateMessage(Message):
|
|
163
|
+
"""Bridge a SubAgent state transition into the UI thread."""
|
|
164
|
+
|
|
165
|
+
def __init__(self, transition: SubAgentStateTransition) -> None:
|
|
166
|
+
self.transition = transition
|
|
167
|
+
super().__init__()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class SubAgentResultMessage(Message):
|
|
171
|
+
"""Bridge a final SubAgent execution result into the UI thread."""
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
task: SubAgentTask,
|
|
176
|
+
execution: SubAgentExecution,
|
|
177
|
+
) -> None:
|
|
178
|
+
self.task = task
|
|
179
|
+
self.execution = execution
|
|
180
|
+
super().__init__()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class TuiSubAgentObserver:
|
|
184
|
+
"""Bridge SubAgent lifecycle events into the Textual message queue.
|
|
185
|
+
|
|
186
|
+
Callbacks run on the SubAgent worker thread. They only post Textual
|
|
187
|
+
Messages; the UI thread owns every widget update. Snapshot and tool-audit
|
|
188
|
+
events are never rendered in the single-conversation timeline.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, post_message: Callable[[Message], bool]) -> None:
|
|
192
|
+
self._post_message = post_message
|
|
193
|
+
self._closed = False
|
|
194
|
+
|
|
195
|
+
def on_state(
|
|
196
|
+
self,
|
|
197
|
+
task: SubAgentTask,
|
|
198
|
+
transition: SubAgentStateTransition,
|
|
199
|
+
) -> None:
|
|
200
|
+
del task
|
|
201
|
+
self._safe_post(SubAgentStateMessage(transition))
|
|
202
|
+
|
|
203
|
+
def on_snapshot(
|
|
204
|
+
self,
|
|
205
|
+
task: SubAgentTask,
|
|
206
|
+
run_id: str,
|
|
207
|
+
snapshot,
|
|
208
|
+
occurred_at: datetime,
|
|
209
|
+
) -> None:
|
|
210
|
+
del task, run_id, snapshot, occurred_at
|
|
211
|
+
|
|
212
|
+
def on_tool_audit(
|
|
213
|
+
self,
|
|
214
|
+
task: SubAgentTask,
|
|
215
|
+
run_id: str,
|
|
216
|
+
audit,
|
|
217
|
+
occurred_at: datetime,
|
|
218
|
+
) -> None:
|
|
219
|
+
del task, run_id, audit, occurred_at
|
|
220
|
+
|
|
221
|
+
def on_result(
|
|
222
|
+
self,
|
|
223
|
+
task: SubAgentTask,
|
|
224
|
+
execution: SubAgentExecution,
|
|
225
|
+
occurred_at: datetime,
|
|
226
|
+
) -> None:
|
|
227
|
+
del occurred_at
|
|
228
|
+
self._safe_post(SubAgentResultMessage(task, execution))
|
|
229
|
+
|
|
230
|
+
def close(self) -> None:
|
|
231
|
+
self._closed = True
|
|
232
|
+
|
|
233
|
+
def _safe_post(self, message: Message) -> None:
|
|
234
|
+
if self._closed:
|
|
235
|
+
return
|
|
236
|
+
try:
|
|
237
|
+
self._post_message(message)
|
|
238
|
+
except Exception: # noqa: BLE001 - app may be closing; never raise into SubAgent thread
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
__all__ = [
|
|
243
|
+
"MCPTrustRequestMessage",
|
|
244
|
+
"MCPTrustResponseHandle",
|
|
245
|
+
"MCPTrustWarningMessage",
|
|
246
|
+
"PermissionRequestMessage",
|
|
247
|
+
"PermissionResponseHandle",
|
|
248
|
+
"SubAgentResultMessage",
|
|
249
|
+
"SubAgentStateMessage",
|
|
250
|
+
"TuiConfirmer",
|
|
251
|
+
"TuiMCPTrustConfirmer",
|
|
252
|
+
"TuiSubAgentObserver",
|
|
253
|
+
]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Translate application and agent events into TUI view updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from mycode.agent.events import AgentEvent
|
|
10
|
+
from mycode.application.events import RuntimeEvent
|
|
11
|
+
from mycode.event_format import summarize_event_content, summarize_tool_arguments
|
|
12
|
+
|
|
13
|
+
SUBAGENT_TUI_SUMMARY_CHARS = 160
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TuiConversation(Protocol):
|
|
17
|
+
def add_user_message(self, content: str) -> None: ...
|
|
18
|
+
|
|
19
|
+
def append_assistant_delta(self, content: str) -> None: ...
|
|
20
|
+
|
|
21
|
+
def add_notice(self, content: str, *, level: str = "info") -> None: ...
|
|
22
|
+
|
|
23
|
+
def add_tool_activity(self, name: str, arguments: str) -> int: ...
|
|
24
|
+
|
|
25
|
+
def complete_tool_activity(
|
|
26
|
+
self,
|
|
27
|
+
token: int,
|
|
28
|
+
name: str,
|
|
29
|
+
*,
|
|
30
|
+
ok: bool,
|
|
31
|
+
summary: str,
|
|
32
|
+
) -> None: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TuiHeader(Protocol):
|
|
36
|
+
def set_session(self, value: str) -> None: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TuiStatus(Protocol):
|
|
40
|
+
def set_status(self, value: str) -> None: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class TuiPresenter:
|
|
45
|
+
"""Keep event interpretation out of Textual widgets.
|
|
46
|
+
|
|
47
|
+
The presenter accepts the existing RuntimeEvent and AgentEvent contracts.
|
|
48
|
+
It deliberately has no dependency on AgentRunner or AgentApplicationSession;
|
|
49
|
+
the TUI application owns startup and feeds the resulting events here.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
conversation: TuiConversation
|
|
53
|
+
header: TuiHeader
|
|
54
|
+
status: TuiStatus
|
|
55
|
+
_pending_tool_rows: deque[tuple[int, str]] = field(
|
|
56
|
+
default_factory=deque,
|
|
57
|
+
init=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def show_user_message(self, content: str) -> None:
|
|
61
|
+
self.conversation.add_user_message(content)
|
|
62
|
+
self.status.set_status("UI Ready")
|
|
63
|
+
|
|
64
|
+
def present(self, event: RuntimeEvent) -> None:
|
|
65
|
+
if event.type == "runtime_ready":
|
|
66
|
+
self._present_runtime_ready(event)
|
|
67
|
+
return
|
|
68
|
+
if event.type == "mcp_status":
|
|
69
|
+
self._present_mcp_status(event)
|
|
70
|
+
return
|
|
71
|
+
if event.type == "agent" and event.agent_event is not None:
|
|
72
|
+
self.present_agent_event(event.agent_event)
|
|
73
|
+
return
|
|
74
|
+
if event.type == "turn_finished":
|
|
75
|
+
self._pending_tool_rows.clear()
|
|
76
|
+
self.status.set_status("Ready")
|
|
77
|
+
|
|
78
|
+
def present_agent_event(self, event: AgentEvent) -> None:
|
|
79
|
+
if event.type in {"reasoning_delta", "reasoning_state"}:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if event.type == "turn":
|
|
83
|
+
if event.turn_number is not None and event.max_turns is not None:
|
|
84
|
+
self.conversation.add_notice(
|
|
85
|
+
f"· turn {event.turn_number}/{event.max_turns}"
|
|
86
|
+
)
|
|
87
|
+
if event.content:
|
|
88
|
+
self.conversation.add_notice(
|
|
89
|
+
f"· turn notice {summarize_event_content(event.content)}"
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
if event.type == "context":
|
|
94
|
+
summary = summarize_event_content(event.content)
|
|
95
|
+
if summary:
|
|
96
|
+
self.conversation.add_notice(f"· context {summary}")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if event.type == "progress":
|
|
100
|
+
if event.progress is None:
|
|
101
|
+
return
|
|
102
|
+
progress = event.progress
|
|
103
|
+
self.conversation.add_notice(
|
|
104
|
+
"· progress "
|
|
105
|
+
f"reason={progress.reason} "
|
|
106
|
+
f"stagnation={progress.stagnation_turns} "
|
|
107
|
+
f"repeat={progress.same_tool_repeat} "
|
|
108
|
+
f"result_repeat={progress.same_result_repeat} "
|
|
109
|
+
f"resource_repeat={progress.resource_repeat}"
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if event.type == "model_start":
|
|
114
|
+
self.status.set_status("Thinking…")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if event.type == "model_retry":
|
|
118
|
+
if event.model_retry is None:
|
|
119
|
+
return
|
|
120
|
+
retry = event.model_retry
|
|
121
|
+
self.status.set_status("Retrying…")
|
|
122
|
+
self.conversation.add_notice(
|
|
123
|
+
"⚠ model retry "
|
|
124
|
+
f"{retry.attempt}/{retry.max_retries} "
|
|
125
|
+
f"after {retry.delay_seconds:.1f}s "
|
|
126
|
+
f"error={retry.error_type}",
|
|
127
|
+
level="warning",
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if event.type == "text_delta":
|
|
132
|
+
if event.content:
|
|
133
|
+
self.status.set_status("Responding…")
|
|
134
|
+
self.conversation.append_assistant_delta(event.content)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if event.type == "tool_call":
|
|
138
|
+
if event.tool_call is None:
|
|
139
|
+
return
|
|
140
|
+
tool_call = event.tool_call
|
|
141
|
+
arguments = summarize_tool_arguments(
|
|
142
|
+
tool_call.name,
|
|
143
|
+
tool_call.arguments,
|
|
144
|
+
)
|
|
145
|
+
token = self.conversation.add_tool_activity(
|
|
146
|
+
tool_call.name,
|
|
147
|
+
arguments,
|
|
148
|
+
)
|
|
149
|
+
self._pending_tool_rows.append((token, tool_call.name))
|
|
150
|
+
self.status.set_status("Running…")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if event.type == "tool_result":
|
|
154
|
+
if event.tool_result is None:
|
|
155
|
+
return
|
|
156
|
+
result = event.tool_result
|
|
157
|
+
if self._pending_tool_rows:
|
|
158
|
+
token, name = self._pending_tool_rows.popleft()
|
|
159
|
+
else:
|
|
160
|
+
metadata_name = result.metadata.get("tool_name")
|
|
161
|
+
name = metadata_name if isinstance(metadata_name, str) else "tool"
|
|
162
|
+
token = self.conversation.add_tool_activity(name, "")
|
|
163
|
+
summary = summarize_event_content(
|
|
164
|
+
result.content if result.ok else result.error
|
|
165
|
+
)
|
|
166
|
+
self.conversation.complete_tool_activity(
|
|
167
|
+
token,
|
|
168
|
+
name,
|
|
169
|
+
ok=result.ok,
|
|
170
|
+
summary=summary,
|
|
171
|
+
)
|
|
172
|
+
self.status.set_status("Running…" if not result.ok else "Thinking…")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if event.type == "artifact_warning":
|
|
176
|
+
summary = summarize_event_content(event.content)
|
|
177
|
+
if summary:
|
|
178
|
+
self.conversation.add_notice(f"⚠ {summary}", level="warning")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
if event.type == "error":
|
|
182
|
+
summary = summarize_event_content(event.error or event.content)
|
|
183
|
+
self.conversation.add_notice(f"✗ {summary}", level="error")
|
|
184
|
+
self.status.set_status("Error")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if event.type == "stop":
|
|
188
|
+
reason = event.stop_reason or "unknown"
|
|
189
|
+
content = summarize_event_content(event.content)
|
|
190
|
+
message = f"· stop reason={reason}"
|
|
191
|
+
if content:
|
|
192
|
+
message += f" {content}"
|
|
193
|
+
self.conversation.add_notice(message)
|
|
194
|
+
self.status.set_status("Stopped")
|
|
195
|
+
|
|
196
|
+
def present_subagent_transition(self, transition) -> None:
|
|
197
|
+
"""Render the few high-value SubAgent state transitions.
|
|
198
|
+
|
|
199
|
+
Completed and failed envelopes are shown once from on_result; the
|
|
200
|
+
intermediate transitions only annotate start, waiting for permission
|
|
201
|
+
and interruption (which may never reach on_result on an interrupt).
|
|
202
|
+
"""
|
|
203
|
+
role = transition.role.capitalize()
|
|
204
|
+
if transition.state == "running" and transition.reason == "run_started":
|
|
205
|
+
self.conversation.add_notice(f"› {role} started")
|
|
206
|
+
return
|
|
207
|
+
if transition.state == "awaiting_confirmation":
|
|
208
|
+
self.conversation.add_notice(f"· {role} waiting for permission")
|
|
209
|
+
return
|
|
210
|
+
if transition.state == "interrupted":
|
|
211
|
+
self.conversation.add_notice(
|
|
212
|
+
f"⚠ {role} interrupted",
|
|
213
|
+
level="warning",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def present_subagent_result(self, execution) -> None:
|
|
217
|
+
result = execution.result
|
|
218
|
+
role = result.role.capitalize()
|
|
219
|
+
if result.status == "completed":
|
|
220
|
+
marker, level = "✓", "info"
|
|
221
|
+
elif result.status == "interrupted":
|
|
222
|
+
marker, level = "⚠", "warning"
|
|
223
|
+
else:
|
|
224
|
+
marker, level = "✗", "error"
|
|
225
|
+
line = f"{marker} {role} {result.status}"
|
|
226
|
+
summary = _subagent_summary(result.summary)
|
|
227
|
+
if summary:
|
|
228
|
+
line += f" {summary}"
|
|
229
|
+
self.conversation.add_notice(line, level=level)
|
|
230
|
+
|
|
231
|
+
def _present_runtime_ready(self, event: RuntimeEvent) -> None:
|
|
232
|
+
session = event.session_title or event.session_id or "—"
|
|
233
|
+
self.header.set_session(session)
|
|
234
|
+
self.conversation.add_notice(f"· runtime ready session={session}")
|
|
235
|
+
for warning in (*event.instruction_warnings, *event.skill_warnings):
|
|
236
|
+
summary = summarize_event_content(warning)
|
|
237
|
+
if summary:
|
|
238
|
+
self.conversation.add_notice(
|
|
239
|
+
f"⚠ startup {summary}",
|
|
240
|
+
level="warning",
|
|
241
|
+
)
|
|
242
|
+
self.status.set_status("UI Ready")
|
|
243
|
+
|
|
244
|
+
def _present_mcp_status(self, event: RuntimeEvent) -> None:
|
|
245
|
+
if event.mcp_status is None:
|
|
246
|
+
return
|
|
247
|
+
status = event.mcp_status
|
|
248
|
+
if status.status == "connected":
|
|
249
|
+
self.conversation.add_notice(
|
|
250
|
+
f"· MCP {status.alias} connected ({status.tool_count} tools)"
|
|
251
|
+
)
|
|
252
|
+
return
|
|
253
|
+
detail = summarize_event_content(
|
|
254
|
+
status.error_summary or status.error_type or "unavailable"
|
|
255
|
+
)
|
|
256
|
+
self.conversation.add_notice(
|
|
257
|
+
f"⚠ MCP {status.alias} unavailable: {detail}",
|
|
258
|
+
level="warning",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _subagent_summary(value: str) -> str:
|
|
263
|
+
normalized = " ".join(value.split())
|
|
264
|
+
if len(normalized) <= SUBAGENT_TUI_SUMMARY_CHARS:
|
|
265
|
+
return normalized
|
|
266
|
+
return normalized[: SUBAGENT_TUI_SUMMARY_CHARS - 3].rstrip() + "..."
|