superqode 0.1.5__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.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,953 @@
1
+ """
2
+ SuperQode Agent Streaming - Real-time Agent Communication
3
+
4
+ Implements ACP (Agent Client Protocol) for streaming agent output in real-time.
5
+ Supports OpenCode and other ACP-compatible agents.
6
+
7
+ Features:
8
+ - Real-time message streaming (agent responses, thoughts, tool calls)
9
+ - Interactive permission requests
10
+ - Plan tracking with live updates
11
+ - Colorful SuperQode styling
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import json
18
+ import os
19
+ from dataclasses import dataclass, field
20
+ from enum import Enum
21
+ from pathlib import Path
22
+ from typing import Any, Callable, Dict, List, Optional, Literal
23
+ from time import monotonic
24
+
25
+ # ============================================================================
26
+ # THEME & COLORS (SuperQode style)
27
+ # ============================================================================
28
+
29
+ STREAM_COLORS = {
30
+ "message": "#a855f7", # Purple - agent messages
31
+ "thought": "#ec4899", # Pink - thinking
32
+ "tool": "#f97316", # Orange - tool calls
33
+ "plan": "#06b6d4", # Cyan - plan updates
34
+ "success": "#22c55e", # Green - completed
35
+ "error": "#ef4444", # Red - errors
36
+ "warning": "#f59e0b", # Amber - warnings
37
+ "pending": "#71717a", # Gray - pending
38
+ "progress": "#3b82f6", # Blue - in progress
39
+ }
40
+
41
+ STREAM_ICONS = {
42
+ "message": "💬",
43
+ "thought": "💭",
44
+ "tool_read": "📖",
45
+ "tool_edit": "✏️",
46
+ "tool_delete": "🗑️",
47
+ "tool_execute": "⚡",
48
+ "tool_search": "🔍",
49
+ "tool_think": "🧠",
50
+ "tool_fetch": "🌐",
51
+ "tool_other": "🔧",
52
+ "plan": "📋",
53
+ "permission": "🔐",
54
+ "success": "✅",
55
+ "error": "❌",
56
+ "pending": "⏳",
57
+ "progress": "🔄",
58
+ }
59
+
60
+
61
+ # ============================================================================
62
+ # MESSAGE TYPES
63
+ # ============================================================================
64
+
65
+
66
+ class StreamEventType(Enum):
67
+ """Types of streaming events from agents."""
68
+
69
+ MESSAGE_CHUNK = "message_chunk" # Agent text response
70
+ THOUGHT_CHUNK = "thought_chunk" # Agent thinking
71
+ TOOL_CALL = "tool_call" # Tool invocation started
72
+ TOOL_UPDATE = "tool_update" # Tool status update
73
+ PLAN = "plan" # Plan with tasks
74
+ PERMISSION = "permission" # Permission request
75
+ MODE_UPDATE = "mode_update" # Mode change
76
+ STATUS = "status" # Status line update
77
+ ERROR = "error" # Error occurred
78
+ COMPLETE = "complete" # Agent finished
79
+
80
+
81
+ class ToolKind(Enum):
82
+ """Types of tool operations."""
83
+
84
+ READ = "read"
85
+ EDIT = "edit"
86
+ DELETE = "delete"
87
+ MOVE = "move"
88
+ SEARCH = "search"
89
+ EXECUTE = "execute"
90
+ THINK = "think"
91
+ FETCH = "fetch"
92
+ SWITCH_MODE = "switch_mode"
93
+ OTHER = "other"
94
+
95
+
96
+ class ToolStatus(Enum):
97
+ """Status of a tool call."""
98
+
99
+ PENDING = "pending"
100
+ IN_PROGRESS = "in_progress"
101
+ COMPLETED = "completed"
102
+ FAILED = "failed"
103
+
104
+
105
+ class TaskStatus(Enum):
106
+ """Status of a plan task."""
107
+
108
+ PENDING = "pending"
109
+ IN_PROGRESS = "in_progress"
110
+ COMPLETED = "completed"
111
+
112
+
113
+ class TaskPriority(Enum):
114
+ """Priority of a plan task."""
115
+
116
+ HIGH = "high"
117
+ MEDIUM = "medium"
118
+ LOW = "low"
119
+
120
+
121
+ # ============================================================================
122
+ # DATA CLASSES
123
+ # ============================================================================
124
+
125
+
126
+ @dataclass
127
+ class StreamMessage:
128
+ """A chunk of agent message text."""
129
+
130
+ text: str
131
+ is_complete: bool = False
132
+
133
+
134
+ @dataclass
135
+ class StreamThought:
136
+ """Agent's thinking/reasoning."""
137
+
138
+ text: str
139
+
140
+
141
+ @dataclass
142
+ class ToolCallContent:
143
+ """Content within a tool call (diff, terminal, etc.)."""
144
+
145
+ type: str # "content", "diff", "terminal"
146
+ data: Dict[str, Any] = field(default_factory=dict)
147
+
148
+
149
+ @dataclass
150
+ class StreamToolCall:
151
+ """A tool call from the agent."""
152
+
153
+ tool_id: str
154
+ title: str
155
+ kind: ToolKind = ToolKind.OTHER
156
+ status: ToolStatus = ToolStatus.PENDING
157
+ content: List[ToolCallContent] = field(default_factory=list)
158
+ locations: List[Dict[str, Any]] = field(default_factory=list)
159
+ raw_input: Dict[str, Any] = field(default_factory=dict)
160
+ raw_output: Dict[str, Any] = field(default_factory=dict)
161
+
162
+
163
+ @dataclass
164
+ class PlanTask:
165
+ """A task in the agent's plan."""
166
+
167
+ content: str
168
+ status: TaskStatus = TaskStatus.PENDING
169
+ priority: TaskPriority = TaskPriority.MEDIUM
170
+
171
+
172
+ @dataclass
173
+ class StreamPlan:
174
+ """Agent's plan with tasks."""
175
+
176
+ tasks: List[PlanTask] = field(default_factory=list)
177
+
178
+
179
+ @dataclass
180
+ class PermissionOption:
181
+ """An option for permission request."""
182
+
183
+ option_id: str
184
+ name: str
185
+ kind: str # allow_once, allow_always, reject_once, reject_always
186
+
187
+
188
+ @dataclass
189
+ class StreamPermission:
190
+ """Permission request from agent."""
191
+
192
+ tool_call: StreamToolCall
193
+ options: List[PermissionOption] = field(default_factory=list)
194
+ result_future: Optional[asyncio.Future] = None
195
+
196
+
197
+ @dataclass
198
+ class StreamEvent:
199
+ """A streaming event from the agent."""
200
+
201
+ event_type: StreamEventType
202
+ data: Any # StreamMessage, StreamThought, StreamToolCall, etc.
203
+ timestamp: float = field(default_factory=monotonic)
204
+
205
+
206
+ # ============================================================================
207
+ # JSON-RPC HELPERS
208
+ # ============================================================================
209
+
210
+
211
+ class JSONRPCError(Exception):
212
+ """JSON-RPC error."""
213
+
214
+ def __init__(self, code: int, message: str, data: Any = None):
215
+ self.code = code
216
+ self.message = message
217
+ self.data = data
218
+ super().__init__(f"JSON-RPC Error {code}: {message}")
219
+
220
+
221
+ def make_request(method: str, params: Dict[str, Any], request_id: int) -> bytes:
222
+ """Create a JSON-RPC request."""
223
+ request = {
224
+ "jsonrpc": "2.0",
225
+ "id": request_id,
226
+ "method": method,
227
+ "params": params,
228
+ }
229
+ return json.dumps(request).encode("utf-8") + b"\n"
230
+
231
+
232
+ def make_response(request_id: int, result: Any) -> bytes:
233
+ """Create a JSON-RPC response."""
234
+ response = {
235
+ "jsonrpc": "2.0",
236
+ "id": request_id,
237
+ "result": result,
238
+ }
239
+ return json.dumps(response).encode("utf-8") + b"\n"
240
+
241
+
242
+ def parse_message(line: bytes) -> Optional[Dict[str, Any]]:
243
+ """Parse a JSON-RPC message from a line."""
244
+ try:
245
+ text = line.decode("utf-8").strip()
246
+ if not text:
247
+ return None
248
+ return json.loads(text)
249
+ except (json.JSONDecodeError, UnicodeDecodeError):
250
+ return None
251
+
252
+
253
+ # ============================================================================
254
+ # AGENT STREAM CLIENT
255
+ # ============================================================================
256
+
257
+
258
+ class AgentStreamClient:
259
+ """
260
+ Real-time streaming client for ACP-compatible agents.
261
+
262
+ Spawns agent subprocess and streams JSON-RPC messages for live updates.
263
+ """
264
+
265
+ PROTOCOL_VERSION = 1
266
+
267
+ def __init__(
268
+ self,
269
+ project_root: Path,
270
+ agent_command: str,
271
+ on_event: Optional[Callable[[StreamEvent], None]] = None,
272
+ ):
273
+ self.project_root = project_root
274
+ self.agent_command = agent_command
275
+ self.on_event = on_event
276
+
277
+ self._process: Optional[asyncio.subprocess.Process] = None
278
+ self._request_id = 0
279
+ self._pending_requests: Dict[int, asyncio.Future] = {}
280
+ self._session_id: str = ""
281
+ self._tool_calls: Dict[str, StreamToolCall] = {}
282
+ self._current_message: str = ""
283
+ self._running = False
284
+
285
+ @property
286
+ def is_running(self) -> bool:
287
+ return self._running and self._process is not None
288
+
289
+ def _next_request_id(self) -> int:
290
+ self._request_id += 1
291
+ return self._request_id
292
+
293
+ def _emit(self, event_type: StreamEventType, data: Any):
294
+ """Emit a streaming event."""
295
+ event = StreamEvent(event_type=event_type, data=data)
296
+ if self.on_event:
297
+ self.on_event(event)
298
+
299
+ async def start(self) -> bool:
300
+ """Start the agent subprocess."""
301
+ if self._process is not None:
302
+ return True
303
+
304
+ env = os.environ.copy()
305
+ env["SUPERQODE_CWD"] = str(self.project_root.absolute())
306
+
307
+ try:
308
+ self._process = await asyncio.create_subprocess_shell(
309
+ self.agent_command,
310
+ stdin=asyncio.subprocess.PIPE,
311
+ stdout=asyncio.subprocess.PIPE,
312
+ stderr=asyncio.subprocess.PIPE,
313
+ env=env,
314
+ cwd=str(self.project_root),
315
+ limit=10 * 1024 * 1024, # 10MB buffer
316
+ )
317
+ self._running = True
318
+
319
+ # Start reading stdout in background
320
+ asyncio.create_task(self._read_loop())
321
+
322
+ # Initialize ACP
323
+ await self._initialize()
324
+ await self._new_session()
325
+
326
+ return True
327
+
328
+ except Exception as e:
329
+ self._emit(StreamEventType.ERROR, str(e))
330
+ return False
331
+
332
+ async def stop(self):
333
+ """Stop the agent subprocess."""
334
+ self._running = False
335
+ if self._process:
336
+ self._process.terminate()
337
+ try:
338
+ await asyncio.wait_for(self._process.wait(), timeout=5.0)
339
+ except asyncio.TimeoutError:
340
+ self._process.kill()
341
+ self._process = None
342
+
343
+ async def _send(self, method: str, params: Dict[str, Any]) -> asyncio.Future:
344
+ """Send a JSON-RPC request and return a future for the response."""
345
+ if not self._process or not self._process.stdin:
346
+ raise RuntimeError("Agent not started")
347
+
348
+ request_id = self._next_request_id()
349
+ future: asyncio.Future = asyncio.Future()
350
+ self._pending_requests[request_id] = future
351
+
352
+ request = make_request(method, params, request_id)
353
+ self._process.stdin.write(request)
354
+ await self._process.stdin.drain()
355
+
356
+ return future
357
+
358
+ async def _respond(self, request_id: int, result: Any):
359
+ """Send a JSON-RPC response."""
360
+ if not self._process or not self._process.stdin:
361
+ return
362
+
363
+ response = make_response(request_id, result)
364
+ self._process.stdin.write(response)
365
+ await self._process.stdin.drain()
366
+
367
+ async def _read_loop(self):
368
+ """Read and process messages from agent stdout."""
369
+ if not self._process or not self._process.stdout:
370
+ return
371
+
372
+ while self._running:
373
+ try:
374
+ line = await self._process.stdout.readline()
375
+ if not line:
376
+ break
377
+
378
+ msg = parse_message(line)
379
+ if msg:
380
+ await self._handle_message(msg)
381
+
382
+ except Exception as e:
383
+ self._emit(StreamEventType.ERROR, f"Read error: {e}")
384
+ break
385
+
386
+ self._running = False
387
+ self._emit(StreamEventType.COMPLETE, None)
388
+
389
+ async def _handle_message(self, msg: Dict[str, Any]):
390
+ """Handle an incoming JSON-RPC message."""
391
+ # Check if it's a response to a pending request
392
+ if "result" in msg or "error" in msg:
393
+ request_id = msg.get("id")
394
+ if request_id and request_id in self._pending_requests:
395
+ future = self._pending_requests.pop(request_id)
396
+ if "error" in msg:
397
+ err = msg["error"]
398
+ future.set_exception(
399
+ JSONRPCError(
400
+ err.get("code", -1),
401
+ err.get("message", "Unknown error"),
402
+ err.get("data"),
403
+ )
404
+ )
405
+ else:
406
+ future.set_result(msg.get("result"))
407
+ return
408
+
409
+ # It's a notification or request from the agent
410
+ method = msg.get("method", "")
411
+ params = msg.get("params", {})
412
+ request_id = msg.get("id")
413
+
414
+ if method == "session/update":
415
+ await self._handle_session_update(params, request_id)
416
+ elif method == "session/request_permission":
417
+ await self._handle_permission_request(params, request_id)
418
+ elif method == "fs/read_text_file":
419
+ await self._handle_read_file(params, request_id)
420
+ elif method == "fs/write_text_file":
421
+ await self._handle_write_file(params, request_id)
422
+ elif method == "terminal/create":
423
+ await self._handle_terminal_create(params, request_id)
424
+ elif method == "terminal/output":
425
+ await self._handle_terminal_output(params, request_id)
426
+ elif method == "terminal/kill":
427
+ await self._handle_terminal_kill(params, request_id)
428
+
429
+ async def _handle_session_update(self, params: Dict[str, Any], request_id: Optional[int]):
430
+ """Handle session/update notifications."""
431
+ update = params.get("update", {})
432
+ update_type = update.get("sessionUpdate", "")
433
+
434
+ if update_type == "agent_message_chunk":
435
+ content = update.get("content", {})
436
+ text = content.get("text", "")
437
+ self._current_message += text
438
+ self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text=text))
439
+
440
+ elif update_type == "agent_thought_chunk":
441
+ content = update.get("content", {})
442
+ text = content.get("text", "")
443
+ self._emit(StreamEventType.THOUGHT_CHUNK, StreamThought(text=text))
444
+
445
+ elif update_type == "tool_call":
446
+ tool_call = self._parse_tool_call(update)
447
+ self._tool_calls[tool_call.tool_id] = tool_call
448
+ self._emit(StreamEventType.TOOL_CALL, tool_call)
449
+
450
+ elif update_type == "tool_call_update":
451
+ tool_id = update.get("toolCallId", "")
452
+ if tool_id in self._tool_calls:
453
+ tool_call = self._tool_calls[tool_id]
454
+ self._update_tool_call(tool_call, update)
455
+ self._emit(StreamEventType.TOOL_UPDATE, tool_call)
456
+ else:
457
+ # Create new tool call from update
458
+ tool_call = self._parse_tool_call(update)
459
+ self._tool_calls[tool_id] = tool_call
460
+ self._emit(StreamEventType.TOOL_CALL, tool_call)
461
+
462
+ elif update_type == "plan":
463
+ entries = update.get("entries", [])
464
+ plan = StreamPlan(
465
+ tasks=[
466
+ PlanTask(
467
+ content=e.get("content", ""),
468
+ status=TaskStatus(e.get("status", "pending")),
469
+ priority=TaskPriority(e.get("priority", "medium")),
470
+ )
471
+ for e in entries
472
+ ]
473
+ )
474
+ self._emit(StreamEventType.PLAN, plan)
475
+
476
+ def _parse_tool_call(self, data: Dict[str, Any]) -> StreamToolCall:
477
+ """Parse a tool call from JSON data."""
478
+ kind_str = data.get("kind", "other")
479
+ try:
480
+ kind = ToolKind(kind_str)
481
+ except ValueError:
482
+ kind = ToolKind.OTHER
483
+
484
+ status_str = data.get("status", "pending")
485
+ try:
486
+ status = ToolStatus(status_str)
487
+ except ValueError:
488
+ status = ToolStatus.PENDING
489
+
490
+ content = []
491
+ for c in data.get("content", []):
492
+ content.append(ToolCallContent(type=c.get("type", "content"), data=c))
493
+
494
+ return StreamToolCall(
495
+ tool_id=data.get("toolCallId", ""),
496
+ title=data.get("title", "Tool Call"),
497
+ kind=kind,
498
+ status=status,
499
+ content=content,
500
+ locations=data.get("locations", []),
501
+ raw_input=data.get("rawInput", {}),
502
+ raw_output=data.get("rawOutput", {}),
503
+ )
504
+
505
+ def _update_tool_call(self, tool_call: StreamToolCall, update: Dict[str, Any]):
506
+ """Update a tool call with new data."""
507
+ if "title" in update and update["title"]:
508
+ tool_call.title = update["title"]
509
+ if "kind" in update and update["kind"]:
510
+ try:
511
+ tool_call.kind = ToolKind(update["kind"])
512
+ except ValueError:
513
+ pass
514
+ if "status" in update and update["status"]:
515
+ try:
516
+ tool_call.status = ToolStatus(update["status"])
517
+ except ValueError:
518
+ pass
519
+ if "content" in update and update["content"]:
520
+ tool_call.content = [
521
+ ToolCallContent(type=c.get("type", "content"), data=c) for c in update["content"]
522
+ ]
523
+ if "locations" in update:
524
+ tool_call.locations = update["locations"]
525
+ if "rawInput" in update:
526
+ tool_call.raw_input = update["rawInput"]
527
+ if "rawOutput" in update:
528
+ tool_call.raw_output = update["rawOutput"]
529
+
530
+ async def _handle_permission_request(self, params: Dict[str, Any], request_id: Optional[int]):
531
+ """Handle permission request from agent."""
532
+ options_data = params.get("options", [])
533
+ tool_call_data = params.get("toolCall", {})
534
+
535
+ options = [
536
+ PermissionOption(
537
+ option_id=o.get("optionId", ""),
538
+ name=o.get("name", ""),
539
+ kind=o.get("kind", "allow_once"),
540
+ )
541
+ for o in options_data
542
+ ]
543
+
544
+ tool_call = self._parse_tool_call(tool_call_data)
545
+
546
+ # Create future for response
547
+ result_future: asyncio.Future = asyncio.Future()
548
+
549
+ permission = StreamPermission(
550
+ tool_call=tool_call,
551
+ options=options,
552
+ result_future=result_future,
553
+ )
554
+
555
+ self._emit(StreamEventType.PERMISSION, permission)
556
+
557
+ # Wait for user response
558
+ try:
559
+ selected_option_id = await asyncio.wait_for(result_future, timeout=300)
560
+
561
+ if request_id is not None:
562
+ await self._respond(
563
+ request_id,
564
+ {
565
+ "outcome": {
566
+ "outcome": "selected",
567
+ "optionId": selected_option_id,
568
+ }
569
+ },
570
+ )
571
+ except asyncio.TimeoutError:
572
+ if request_id is not None:
573
+ await self._respond(request_id, {"outcome": {"outcome": "cancelled"}})
574
+
575
+ async def _handle_read_file(self, params: Dict[str, Any], request_id: Optional[int]):
576
+ """Handle file read request from agent."""
577
+ path = params.get("path", "")
578
+ line = params.get("line")
579
+ limit = params.get("limit")
580
+
581
+ read_path = self.project_root / path
582
+ try:
583
+ text = read_path.read_text(encoding="utf-8", errors="ignore")
584
+ if line is not None:
585
+ line = max(0, line - 1)
586
+ lines = text.splitlines()
587
+ if limit is None:
588
+ text = "\n".join(lines[line:])
589
+ else:
590
+ text = "\n".join(lines[line : line + limit])
591
+ except IOError:
592
+ text = ""
593
+
594
+ if request_id is not None:
595
+ await self._respond(request_id, {"content": text})
596
+
597
+ async def _handle_write_file(self, params: Dict[str, Any], request_id: Optional[int]):
598
+ """Handle file write request from agent."""
599
+ path = params.get("path", "")
600
+ content = params.get("content", "")
601
+
602
+ write_path = self.project_root / path
603
+ write_path.parent.mkdir(parents=True, exist_ok=True)
604
+ write_path.write_text(content, encoding="utf-8")
605
+
606
+ if request_id is not None:
607
+ await self._respond(request_id, {})
608
+
609
+ async def _handle_terminal_create(self, params: Dict[str, Any], request_id: Optional[int]):
610
+ """Handle terminal create request."""
611
+ # For now, just acknowledge - full terminal support can be added later
612
+ terminal_id = f"terminal-{self._next_request_id()}"
613
+ if request_id is not None:
614
+ await self._respond(request_id, {"terminalId": terminal_id})
615
+
616
+ async def _handle_terminal_output(self, params: Dict[str, Any], request_id: Optional[int]):
617
+ """Handle terminal output request."""
618
+ if request_id is not None:
619
+ await self._respond(
620
+ request_id,
621
+ {
622
+ "output": "",
623
+ "truncated": False,
624
+ },
625
+ )
626
+
627
+ async def _handle_terminal_kill(self, params: Dict[str, Any], request_id: Optional[int]):
628
+ """Handle terminal kill request."""
629
+ if request_id is not None:
630
+ await self._respond(request_id, {})
631
+
632
+ async def _initialize(self):
633
+ """Initialize ACP protocol."""
634
+ future = await self._send(
635
+ "initialize",
636
+ {
637
+ "protocolVersion": self.PROTOCOL_VERSION,
638
+ "clientCapabilities": {
639
+ "fs": {"readTextFile": True, "writeTextFile": True},
640
+ "terminal": True,
641
+ },
642
+ "clientInfo": {
643
+ "name": "SuperQode",
644
+ "title": "SuperQode - Multi-Agent Coding Team",
645
+ "version": "1.0.0",
646
+ },
647
+ },
648
+ )
649
+
650
+ try:
651
+ result = await asyncio.wait_for(future, timeout=30)
652
+ return result
653
+ except asyncio.TimeoutError:
654
+ raise RuntimeError("Agent initialization timed out")
655
+
656
+ async def _new_session(self):
657
+ """Create a new ACP session."""
658
+ future = await self._send(
659
+ "session/new",
660
+ {
661
+ "projectRoot": str(self.project_root),
662
+ "mcpServers": [],
663
+ },
664
+ )
665
+
666
+ try:
667
+ result = await asyncio.wait_for(future, timeout=30)
668
+ self._session_id = result.get("sessionId", "")
669
+ return result
670
+ except asyncio.TimeoutError:
671
+ raise RuntimeError("Session creation timed out")
672
+
673
+ async def send_prompt(self, prompt: str) -> Optional[str]:
674
+ """Send a prompt to the agent and stream the response."""
675
+ self._current_message = ""
676
+
677
+ future = await self._send(
678
+ "session/prompt",
679
+ {
680
+ "sessionId": self._session_id,
681
+ "content": [{"type": "text", "text": prompt}],
682
+ },
683
+ )
684
+
685
+ try:
686
+ result = await future
687
+ # Mark message as complete
688
+ self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text="", is_complete=True))
689
+ return result.get("stopReason")
690
+ except JSONRPCError as e:
691
+ self._emit(StreamEventType.ERROR, str(e))
692
+ return None
693
+
694
+ async def cancel(self) -> bool:
695
+ """Cancel the current operation."""
696
+ try:
697
+ future = await self._send(
698
+ "session/cancel",
699
+ {
700
+ "sessionId": self._session_id,
701
+ "options": {},
702
+ },
703
+ )
704
+ await asyncio.wait_for(future, timeout=5)
705
+ return True
706
+ except Exception:
707
+ return False
708
+
709
+ async def reset_session(self) -> bool:
710
+ """
711
+ Reset the session (e.g., after model change).
712
+
713
+ Creates a new session without restarting the agent process.
714
+
715
+ Returns:
716
+ True if reset was successful, False otherwise.
717
+ """
718
+ try:
719
+ # Cancel any pending operations
720
+ await self.cancel()
721
+
722
+ # Clear internal state
723
+ self._tool_calls.clear()
724
+ self._current_message = ""
725
+ self._pending_requests.clear()
726
+
727
+ # Create new session
728
+ await self._new_session()
729
+
730
+ return True
731
+ except Exception as e:
732
+ self._emit(StreamEventType.ERROR, f"Session reset failed: {e}")
733
+ return False
734
+
735
+ async def switch_agent(self, new_command: str) -> bool:
736
+ """
737
+ Switch to a different agent command.
738
+
739
+ Stops the current agent and starts a new one with the given command.
740
+
741
+ Args:
742
+ new_command: The new agent command to run.
743
+
744
+ Returns:
745
+ True if switch was successful, False otherwise.
746
+ """
747
+ try:
748
+ # Stop current agent
749
+ await self.stop()
750
+
751
+ # Update command
752
+ self.agent_command = new_command
753
+
754
+ # Clear state
755
+ self._tool_calls.clear()
756
+ self._current_message = ""
757
+ self._pending_requests.clear()
758
+ self._session_id = ""
759
+ self._request_id = 0
760
+
761
+ # Start fresh
762
+ return await self.start()
763
+
764
+ except Exception as e:
765
+ self._emit(StreamEventType.ERROR, f"Agent switch failed: {e}")
766
+ return False
767
+
768
+ def get_session_id(self) -> str:
769
+ """Get the current session ID."""
770
+ return self._session_id
771
+
772
+
773
+ # ============================================================================
774
+ # SIMPLE STREAMING CLIENT (for non-ACP agents like basic OpenCode)
775
+ # ============================================================================
776
+
777
+
778
+ class SimpleStreamClient:
779
+ """
780
+ Simple streaming client for agents that output plain text.
781
+
782
+ Reads stdout line by line and emits message events.
783
+ """
784
+
785
+ def __init__(
786
+ self,
787
+ project_root: Path,
788
+ command: List[str],
789
+ on_event: Optional[Callable[[StreamEvent], None]] = None,
790
+ ):
791
+ self.project_root = project_root
792
+ self.command = command
793
+ self.on_event = on_event
794
+
795
+ self._process: Optional[asyncio.subprocess.Process] = None
796
+ self._running = False
797
+
798
+ @property
799
+ def is_running(self) -> bool:
800
+ return self._running and self._process is not None
801
+
802
+ def _emit(self, event_type: StreamEventType, data: Any):
803
+ """Emit a streaming event."""
804
+ event = StreamEvent(event_type=event_type, data=data)
805
+ if self.on_event:
806
+ self.on_event(event)
807
+
808
+ async def start(self) -> bool:
809
+ """Start the subprocess."""
810
+ if self._process is not None:
811
+ return True
812
+
813
+ try:
814
+ self._process = await asyncio.create_subprocess_exec(
815
+ *self.command,
816
+ stdin=asyncio.subprocess.PIPE,
817
+ stdout=asyncio.subprocess.PIPE,
818
+ stderr=asyncio.subprocess.STDOUT,
819
+ cwd=str(self.project_root),
820
+ )
821
+ self._running = True
822
+ return True
823
+ except Exception as e:
824
+ self._emit(StreamEventType.ERROR, str(e))
825
+ return False
826
+
827
+ async def stop(self):
828
+ """Stop the subprocess."""
829
+ self._running = False
830
+ if self._process:
831
+ self._process.terminate()
832
+ try:
833
+ await asyncio.wait_for(self._process.wait(), timeout=5.0)
834
+ except asyncio.TimeoutError:
835
+ self._process.kill()
836
+ self._process = None
837
+
838
+ async def run_and_stream(self) -> int:
839
+ """Run the command and stream output."""
840
+ if not self._process or not self._process.stdout:
841
+ return -1
842
+
843
+ buffer = ""
844
+ while self._running:
845
+ try:
846
+ chunk = await self._process.stdout.read(256)
847
+ if not chunk:
848
+ break
849
+
850
+ text = chunk.decode("utf-8", errors="replace")
851
+ buffer += text
852
+
853
+ # Emit line by line for cleaner output
854
+ while "\n" in buffer:
855
+ line, buffer = buffer.split("\n", 1)
856
+ self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text=line + "\n"))
857
+
858
+ except Exception as e:
859
+ self._emit(StreamEventType.ERROR, str(e))
860
+ break
861
+
862
+ # Emit remaining buffer
863
+ if buffer:
864
+ self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text=buffer))
865
+
866
+ # Wait for process to complete
867
+ if self._process:
868
+ await self._process.wait()
869
+ return_code = self._process.returncode or 0
870
+ else:
871
+ return_code = -1
872
+
873
+ self._emit(StreamEventType.MESSAGE_CHUNK, StreamMessage(text="", is_complete=True))
874
+ self._emit(StreamEventType.COMPLETE, return_code)
875
+
876
+ return return_code
877
+
878
+
879
+ # ============================================================================
880
+ # RENDERING HELPERS (SuperQode colorful style)
881
+ # ============================================================================
882
+
883
+
884
+ def get_tool_icon(kind: ToolKind) -> str:
885
+ """Get icon for tool kind."""
886
+ icons = {
887
+ ToolKind.READ: "📖",
888
+ ToolKind.EDIT: "✏️",
889
+ ToolKind.DELETE: "🗑️",
890
+ ToolKind.MOVE: "📦",
891
+ ToolKind.SEARCH: "🔍",
892
+ ToolKind.EXECUTE: "⚡",
893
+ ToolKind.THINK: "🧠",
894
+ ToolKind.FETCH: "🌐",
895
+ ToolKind.SWITCH_MODE: "🔄",
896
+ ToolKind.OTHER: "🔧",
897
+ }
898
+ return icons.get(kind, "🔧")
899
+
900
+
901
+ def get_status_icon(status: ToolStatus) -> str:
902
+ """Get icon for tool status."""
903
+ icons = {
904
+ ToolStatus.PENDING: "⏳",
905
+ ToolStatus.IN_PROGRESS: "🔄",
906
+ ToolStatus.COMPLETED: "✅",
907
+ ToolStatus.FAILED: "❌",
908
+ }
909
+ return icons.get(status, "○")
910
+
911
+
912
+ def get_status_color(status: ToolStatus) -> str:
913
+ """Get color for tool status."""
914
+ colors = {
915
+ ToolStatus.PENDING: STREAM_COLORS["pending"],
916
+ ToolStatus.IN_PROGRESS: STREAM_COLORS["progress"],
917
+ ToolStatus.COMPLETED: STREAM_COLORS["success"],
918
+ ToolStatus.FAILED: STREAM_COLORS["error"],
919
+ }
920
+ return colors.get(status, STREAM_COLORS["pending"])
921
+
922
+
923
+ def get_task_icon(status: TaskStatus) -> str:
924
+ """Get icon for task status."""
925
+ icons = {
926
+ TaskStatus.PENDING: "○",
927
+ TaskStatus.IN_PROGRESS: "●",
928
+ TaskStatus.COMPLETED: "✓",
929
+ }
930
+ return icons.get(status, "○")
931
+
932
+
933
+ def get_task_color(status: TaskStatus) -> str:
934
+ """Get color for task status."""
935
+ colors = {
936
+ TaskStatus.PENDING: STREAM_COLORS["pending"],
937
+ TaskStatus.IN_PROGRESS: STREAM_COLORS["progress"],
938
+ TaskStatus.COMPLETED: STREAM_COLORS["success"],
939
+ }
940
+ return colors.get(status, STREAM_COLORS["pending"])
941
+
942
+
943
+ def format_tool_call_title(tool_call: StreamToolCall) -> str:
944
+ """Format tool call title with icon."""
945
+ icon = get_tool_icon(tool_call.kind)
946
+ status_icon = get_status_icon(tool_call.status)
947
+ return f"{status_icon} {icon} {tool_call.title}"
948
+
949
+
950
+ def format_plan_task(task: PlanTask, index: int) -> str:
951
+ """Format a plan task."""
952
+ icon = get_task_icon(task.status)
953
+ return f" {icon} {index}. {task.content}"