ata-coder 2.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. ata_coder/__init__.py +1 -0
  2. ata_coder/agent.py +874 -0
  3. ata_coder/agent_compact.py +190 -0
  4. ata_coder/agent_controller.py +218 -0
  5. ata_coder/agent_extension.py +69 -0
  6. ata_coder/agent_routing.py +105 -0
  7. ata_coder/agent_subsystems.py +72 -0
  8. ata_coder/agent_tools.py +318 -0
  9. ata_coder/agent_undo.py +63 -0
  10. ata_coder/anthropic_client.py +465 -0
  11. ata_coder/change_tracker.py +368 -0
  12. ata_coder/clawd_integration.py +574 -0
  13. ata_coder/commands/__init__.py +128 -0
  14. ata_coder/commands/_core.py +184 -0
  15. ata_coder/commands/_safety.py +95 -0
  16. ata_coder/commands/_settings.py +241 -0
  17. ata_coder/commands/_workflow.py +451 -0
  18. ata_coder/commands.py +974 -0
  19. ata_coder/config.py +257 -0
  20. ata_coder/core/__init__.py +35 -0
  21. ata_coder/core/events.py +73 -0
  22. ata_coder/core/queue.py +85 -0
  23. ata_coder/core/state.py +17 -0
  24. ata_coder/event_queue.py +5 -0
  25. ata_coder/extension.py +654 -0
  26. ata_coder/extensions/__init__.py +1 -0
  27. ata_coder/extensions/hello_skill.py +47 -0
  28. ata_coder/fool_proof.py +295 -0
  29. ata_coder/git_workflow.py +371 -0
  30. ata_coder/gui.py +511 -0
  31. ata_coder/llm_client.py +543 -0
  32. ata_coder/main.py +814 -0
  33. ata_coder/mcp_client.py +1095 -0
  34. ata_coder/memory.py +539 -0
  35. ata_coder/model_registry.py +134 -0
  36. ata_coder/model_router.py +105 -0
  37. ata_coder/permissions.py +274 -0
  38. ata_coder/privilege.py +464 -0
  39. ata_coder/project.py +273 -0
  40. ata_coder/prompt_template.py +423 -0
  41. ata_coder/prompts/auto-mode.md +7 -0
  42. ata_coder/prompts/coding-rules.md +40 -0
  43. ata_coder/prompts/execution-guardrails.md +14 -0
  44. ata_coder/prompts/memory-system.md +24 -0
  45. ata_coder/prompts/output-style.md +23 -0
  46. ata_coder/prompts/safety.md +17 -0
  47. ata_coder/prompts/slash-commands.md +24 -0
  48. ata_coder/prompts/sub-agents.md +38 -0
  49. ata_coder/prompts/system-reminders.md +17 -0
  50. ata_coder/prompts/system.md +105 -0
  51. ata_coder/prompts/tool-policy.md +46 -0
  52. ata_coder/repl_theme.py +99 -0
  53. ata_coder/repl_tracker.py +89 -0
  54. ata_coder/repl_ui.py +1214 -0
  55. ata_coder/safety_guard.py +434 -0
  56. ata_coder/self_correct.py +346 -0
  57. ata_coder/server.py +882 -0
  58. ata_coder/server_session.py +159 -0
  59. ata_coder/server_shell.py +129 -0
  60. ata_coder/session.py +431 -0
  61. ata_coder/settings.py +439 -0
  62. ata_coder/setup_wizard.py +136 -0
  63. ata_coder/skill_extension.py +92 -0
  64. ata_coder/skills/architect/SKILL.md +42 -0
  65. ata_coder/skills/code-reviewer/SKILL.md +37 -0
  66. ata_coder/skills/codecraft/SKILL.md +452 -0
  67. ata_coder/skills/debugger/SKILL.md +45 -0
  68. ata_coder/skills/doc-writer/SKILL.md +36 -0
  69. ata_coder/skills/general-coder/SKILL.md +76 -0
  70. ata_coder/skills/math-calculator/README.md +40 -0
  71. ata_coder/skills/math-calculator/SKILL.md +59 -0
  72. ata_coder/skills/math-calculator/handler.py +103 -0
  73. ata_coder/skills/math-calculator/prompts/system.md +8 -0
  74. ata_coder/skills/math-calculator/requirements.txt +2 -0
  75. ata_coder/skills/math-calculator/resources/constants.json +8 -0
  76. ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
  77. ata_coder/skills/security-auditor/SKILL.md +40 -0
  78. ata_coder/skills/test-writer/SKILL.md +36 -0
  79. ata_coder/skills/weather-skill/README.md +45 -0
  80. ata_coder/skills/weather-skill/handler.py +76 -0
  81. ata_coder/skills/weather-skill/manifest.json +48 -0
  82. ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
  83. ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
  84. ata_coder/skills/weather-skill/requirements.txt +1 -0
  85. ata_coder/skills/weather-skill/resources/city_list.json +17 -0
  86. ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
  87. ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
  88. ata_coder/skills/weather-skill/weather_utils.py +50 -0
  89. ata_coder/skills.py +1014 -0
  90. ata_coder/sub_agent.py +273 -0
  91. ata_coder/sub_agent_manager.py +203 -0
  92. ata_coder/system_prompt_builder.py +146 -0
  93. ata_coder/task_planner.py +391 -0
  94. ata_coder/terminal.py +318 -0
  95. ata_coder/test_runner.py +219 -0
  96. ata_coder/thread_supervisor.py +195 -0
  97. ata_coder/tool_defs.py +335 -0
  98. ata_coder/tools/__init__.py +11 -0
  99. ata_coder/tools/definitions.py +335 -0
  100. ata_coder/tools/executor.py +1036 -0
  101. ata_coder/tools/result.py +26 -0
  102. ata_coder/tools/subagent.py +332 -0
  103. ata_coder/tools/web.py +361 -0
  104. ata_coder/tools.py +1576 -0
  105. ata_coder/types.py +92 -0
  106. ata_coder/utils.py +113 -0
  107. ata_coder/web/css/style.css +180 -0
  108. ata_coder/web/index.html +84 -0
  109. ata_coder/web/js/app.js +489 -0
  110. ata_coder/web/package-lock.json +25 -0
  111. ata_coder/web/package.json +10 -0
  112. ata_coder/web/tsconfig.json +13 -0
  113. ata_coder-2.4.2.dist-info/METADATA +799 -0
  114. ata_coder-2.4.2.dist-info/RECORD +118 -0
  115. ata_coder-2.4.2.dist-info/WHEEL +5 -0
  116. ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
  117. ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
  118. ata_coder-2.4.2.dist-info/top_level.txt +1 -0
ata_coder/gui.py ADDED
@@ -0,0 +1,511 @@
1
+ """
2
+ ATA Coder — tkinter Desktop GUI (zero extra dependencies)
3
+
4
+ Polished, modern interface with:
5
+ - Message bubbles with user/agent color distinction
6
+ - Real-time streaming via background thread
7
+ - Buffered reasoning (single block, not per-token spam)
8
+ - Slash-command autocomplete popup
9
+ - Dark theme with GitHub-inspired palette
10
+ - Smooth auto-scroll
11
+
12
+ Launch via: ata gui
13
+ """
14
+
15
+ import logging
16
+ import queue
17
+ import re
18
+ import sys
19
+ import threading
20
+ import tkinter as tk
21
+ from pathlib import Path
22
+ from tkinter import simpledialog
23
+ from typing import Any
24
+ import asyncio
25
+
26
+ _PKG = str(Path(__file__).parent.resolve())
27
+ if _PKG not in sys.path:
28
+ sys.path.insert(0, _PKG)
29
+
30
+ from .agent import (
31
+ CoderAgent, CompleteEvent, ErrorEvent, ReasoningEvent,
32
+ SkillChangedEvent, TextDeltaEvent, ThinkingEvent,
33
+ ToolCallEvent, ToolResultEvent,
34
+ )
35
+ from .agent_subsystems import AgentSubsystems
36
+ from .commands import get_command_list
37
+ from .config import AppConfig, get_config
38
+ from .main import __version__
39
+ from .permissions import PermissionMode, PermissionStore
40
+ from .skills import get_skill_manager
41
+ from .tools import ToolExecutor
42
+ from .utils import brief_args
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ # ── Theme ──────────────────────────────────────────────────────────────────────
47
+
48
+ T = {
49
+ "bg": "#0d1117",
50
+ "surface": "#161b22",
51
+ "overlay": "#1c2128",
52
+ "border": "#30363d",
53
+ "fg": "#c9d1d9",
54
+ "fg_dim": "#8b949e",
55
+ "muted": "#484f58",
56
+ "accent": "#58a6ff",
57
+ "green": "#3fb950",
58
+ "red": "#f85149",
59
+ "yellow": "#d2991d",
60
+ "purple": "#d2a8ff",
61
+ "teal": "#7ee787",
62
+ }
63
+
64
+ # ── Fonts ─────────────────────────────────────────────────────────────────────
65
+
66
+ def _pick_font(size: int, bold: bool = False) -> tuple:
67
+ for name in ("Cascadia Code", "Consolas", "Courier New", "monospace"):
68
+ try:
69
+ tk.Label(font=(name, size)).destroy()
70
+ return (name, size, "bold") if bold else (name, size)
71
+ except Exception:
72
+ continue
73
+ return ("monospace", size, "bold") if bold else ("monospace", size)
74
+
75
+ FT = _pick_font(11)
76
+ FTB = _pick_font(11, bold=True)
77
+ FTS = _pick_font(9)
78
+ FTM = _pick_font(10)
79
+ FTIB = _pick_font(11, bold=True) # italic-bold via slant if available
80
+
81
+
82
+ # ═══════════════════════════════════════════════════════════════════════════════
83
+
84
+ class AtaCoderGUI(tk.Tk):
85
+ """Polished tkinter desktop GUI for ATA Coder."""
86
+
87
+ def __init__(self, config: AppConfig | None = None, skill: str = ""):
88
+ super().__init__()
89
+ self._config = config or get_config()
90
+ self._active_skill = skill or "general-coder"
91
+ self._eq: queue.Queue = queue.Queue()
92
+ self._running = False
93
+ self._reasoning_buf = ""
94
+ self._reasoning_line_start = ""
95
+ self._insert_buffer = "" # for heading detection across streaming chunks
96
+
97
+ # ── Subsystems ──────────────────────────────────────────────────
98
+ self._skill_mgr = get_skill_manager()
99
+ self._perms = PermissionStore()
100
+ self._perms.set_category_rule("shell", PermissionMode.ALLOW)
101
+ self._perms.set_category_rule("write", PermissionMode.ALLOW)
102
+ self._tool_exec = ToolExecutor(self._config.agent)
103
+ self._tool_exec.setup_file_cache(
104
+ Path(self._config.agent.workspace_dir) / ".ata_coder" / "files")
105
+
106
+ self._agent = CoderAgent(
107
+ config=self._config, tool_executor=self._tool_exec,
108
+ subsystems=AgentSubsystems(skills=self._skill_mgr, permissions=self._perms))
109
+ self._agent._event_queue = self._eq
110
+ self._agent.llm.on_usage(lambda p, c: self.after(0, self._update_tokens))
111
+
112
+ self._commands = get_command_list()
113
+
114
+ # ── Window ───────────────────────────────────────────────────────
115
+ self.configure(bg=T["bg"])
116
+ self.title(f"ATA Coder — {self._config.llm.model}")
117
+ self.geometry("960x700")
118
+ self.minsize(620, 400)
119
+
120
+ self._build_ui()
121
+ self.protocol("WM_DELETE_WINDOW", self._on_close)
122
+ self._poll_interval = 40
123
+ self.after(self._poll_interval, self._poll)
124
+
125
+ # ═══════════════════════════════════════════════════════════════════════
126
+ # UI
127
+ # ═══════════════════════════════════════════════════════════════════════
128
+
129
+ def _build_ui(self) -> None:
130
+ # ── Top bar ──────────────────────────────────────────────────────
131
+ top = tk.Frame(self, bg=T["surface"], height=42, highlightthickness=0)
132
+ top.pack(fill=tk.X, side=tk.TOP)
133
+ top.pack_propagate(False)
134
+
135
+ tk.Label(top, text=" ⚡ ATA Coder", font=FTB, bg=T["surface"],
136
+ fg=T["accent"]).pack(side=tk.LEFT, pady=4)
137
+
138
+ self._model_lbl = tk.Label(top, text=self._config.llm.model[:28],
139
+ font=FTS, bg=T["overlay"], fg=T["fg_dim"], padx=10, pady=3,
140
+ cursor="hand2")
141
+ self._model_lbl.pack(side=tk.RIGHT, padx=(0,12), pady=6)
142
+ self._model_lbl.bind("<Button-1>", lambda e: self._change_model())
143
+ tk.Label(top, text="model", font=FTS, bg=T["surface"],
144
+ fg=T["muted"]).pack(side=tk.RIGHT, padx=(0,4))
145
+
146
+ # Accent line
147
+ tk.Frame(self, bg=T["border"], height=1).pack(fill=tk.X, side=tk.TOP)
148
+
149
+ # ── Chat ─────────────────────────────────────────────────────────
150
+ cf = tk.Frame(self, bg=T["bg"])
151
+ cf.pack(fill=tk.BOTH, expand=True, padx=8, pady=(4,2))
152
+
153
+ self._chat = tk.Text(cf, bg=T["bg"], fg=T["fg"], font=FT,
154
+ wrap=tk.WORD, state=tk.DISABLED, relief=tk.FLAT,
155
+ borderwidth=0, padx=20, pady=12, cursor="arrow",
156
+ yscrollcommand=lambda *a: self._sb.set(*a),
157
+ selectbackground=T["border"], selectforeground=T["fg"])
158
+ self._chat.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
159
+
160
+ self._sb = tk.Scrollbar(cf, bg=T["surface"], troughcolor=T["bg"],
161
+ activebackground=T["muted"], command=self._chat.yview, width=7,
162
+ borderwidth=0, highlightthickness=0)
163
+ self._sb.pack(side=tk.RIGHT, fill=tk.Y)
164
+
165
+ # Tags
166
+ c = self._chat.tag_configure
167
+ c("user", foreground=T["accent"], font=FTB, lmargin1=0, lmargin2=0, spacing1=14, spacing3=6)
168
+ c("agent", foreground=T["fg"], font=FT, lmargin1=0, lmargin2=0, spacing1=0, spacing3=0)
169
+ c("think", foreground=T["teal"], font=FT, lmargin1=28,lmargin2=0, spacing1=6, spacing3=2, background=T["surface"])
170
+ c("tool_h", foreground=T["purple"], font=FTS, lmargin1=32,lmargin2=0, spacing1=8, spacing3=0)
171
+ c("tool_ok", foreground=T["green"], font=FTS, lmargin1=44,lmargin2=0, spacing1=0, spacing3=2)
172
+ c("tool_er", foreground=T["red"], font=FTS, lmargin1=44,lmargin2=0, spacing1=0, spacing3=2)
173
+ c("error", foreground=T["red"], font=FTB, lmargin1=0, lmargin2=0, spacing1=8, spacing3=6)
174
+ c("sep", foreground=T["muted"], font=FTS, lmargin1=0, lmargin2=0, spacing1=8, spacing3=6)
175
+ c("status", foreground=T["muted"], font=FTS, lmargin1=0, lmargin2=0, spacing1=4, spacing3=2)
176
+ c("heading", foreground=T["fg"], font=FTB, lmargin1=0, lmargin2=0, spacing1=8, spacing3=2)
177
+
178
+ # ── Input ────────────────────────────────────────────────────────
179
+ inf = tk.Frame(self, bg=T["surface"], highlightthickness=0)
180
+ inf.pack(fill=tk.X, side=tk.BOTTOM, padx=8, pady=(0,8))
181
+ tk.Frame(inf, bg=T["border"], height=1).pack(fill=tk.X, side=tk.TOP)
182
+
183
+ row = tk.Frame(inf, bg=T["surface"])
184
+ row.pack(fill=tk.X, padx=10, pady=8)
185
+
186
+ self._input = tk.Entry(row, bg=T["overlay"], fg=T["fg"], font=FT,
187
+ relief=tk.FLAT, insertbackground=T["fg"], insertwidth=2,
188
+ borderwidth=0, highlightthickness=0)
189
+ self._input.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(6,8), ipady=3)
190
+ self._input.bind("<Return>", self._on_send)
191
+ self._input.bind("<KeyRelease>", self._on_key)
192
+ self._input.focus_set()
193
+
194
+ self._send_btn = tk.Label(row, text=" Send ", font=FTB,
195
+ bg=T["green"], fg="#ffffff", padx=16, pady=5, cursor="hand2")
196
+ self._send_btn.pack(side=tk.RIGHT)
197
+ self._send_btn.bind("<Button-1>", lambda e: self._on_send())
198
+
199
+ # ── Command popup ────────────────────────────────────────────────
200
+ self._popup: tk.Toplevel | None = None
201
+ self._popup_lb: tk.Listbox | None = None
202
+ self._popup_debounce_id: str = ""
203
+
204
+ # ── Status bar ───────────────────────────────────────────────────
205
+ sf = tk.Frame(self, bg=T["surface"], height=26, highlightthickness=0)
206
+ sf.pack(fill=tk.X, side=tk.BOTTOM)
207
+ sf.pack_propagate(False)
208
+ tk.Frame(sf, bg=T["border"], height=1).pack(fill=tk.X, side=tk.TOP)
209
+
210
+ self._status_tok = tk.Label(sf, text="tokens: ~0", font=FTS,
211
+ bg=T["surface"], fg=T["muted"])
212
+ self._status_tok.pack(side=tk.RIGHT, padx=(0,12))
213
+
214
+ self._status_sk = tk.Label(sf, text=f"skill: {self._active_skill}",
215
+ font=FTS, bg=T["surface"], fg=T["muted"])
216
+ self._status_sk.pack(side=tk.RIGHT, padx=(0,16))
217
+
218
+ self._status_lbl = tk.Label(sf, text="● Ready", font=FTS,
219
+ bg=T["surface"], fg=T["green"])
220
+ self._status_lbl.pack(side=tk.LEFT, padx=(10,0))
221
+
222
+ # ── Welcome ──────────────────────────────────────────────────────
223
+ self._append(f"ATA Coder v{__version__}", "user")
224
+ self._append(f"Model {self._config.llm.model}", "status")
225
+ self._append(f"Workspace {self._config.agent.workspace_dir}", "status")
226
+ self._append("Type / for commands, or just start chatting.", "status")
227
+ self._append("", "status")
228
+
229
+ # ═══════════════════════════════════════════════════════════════════════
230
+ # Chat display helpers
231
+ # ═══════════════════════════════════════════════════════════════════════
232
+
233
+ def _append(self, text: str, tag: str = "agent") -> str:
234
+ self._chat.configure(state=tk.NORMAL)
235
+ idx = self._chat.index(tk.END + "-1c")
236
+ self._chat.insert(tk.END, text + "\n", tag)
237
+ self._chat.configure(state=tk.DISABLED)
238
+ self._chat.see(tk.END)
239
+ return idx
240
+
241
+ def _insert(self, text: str, tag: str = "agent") -> None:
242
+ self._chat.configure(state=tk.NORMAL)
243
+ self._chat.insert(tk.END, text, tag)
244
+ self._chat.configure(state=tk.DISABLED)
245
+ self._chat.see(tk.END)
246
+
247
+ def _insert_streaming(self, text: str) -> None:
248
+ """Insert streaming agent text with ### heading detection.
249
+ Buffers partial lines across chunks so headings split mid-prefix
250
+ are still detected correctly.
251
+ """
252
+ self._insert_buffer += text
253
+ lines = self._insert_buffer.split('\n')
254
+ self._insert_buffer = lines.pop() # keep incomplete last line
255
+
256
+ self._chat.configure(state=tk.NORMAL)
257
+ for line in lines:
258
+ m = re.match(r'^(#{1,3}) (.+)', line)
259
+ if m:
260
+ self._chat.insert(tk.END, m.group(2) + '\n', "heading")
261
+ else:
262
+ self._chat.insert(tk.END, line + '\n', "agent")
263
+ self._chat.configure(state=tk.DISABLED)
264
+ self._chat.see(tk.END)
265
+
266
+ def _flush_insert_buffer(self) -> None:
267
+ """Flush any remaining partial line in the insert buffer."""
268
+ if self._insert_buffer:
269
+ self._chat.configure(state=tk.NORMAL)
270
+ m = re.match(r'^(#{1,3}) (.+)', self._insert_buffer)
271
+ if m:
272
+ self._chat.insert(tk.END, m.group(2) + '\n', "heading")
273
+ else:
274
+ self._chat.insert(tk.END, self._insert_buffer + '\n', "agent")
275
+ self._chat.configure(state=tk.DISABLED)
276
+ self._chat.see(tk.END)
277
+ self._insert_buffer = ""
278
+
279
+ def _replace_range(self, start: str, end: str, text: str, tag: str) -> None:
280
+ self._chat.configure(state=tk.NORMAL)
281
+ self._chat.delete(start, end)
282
+ self._chat.insert(start, text, tag)
283
+ self._chat.configure(state=tk.DISABLED)
284
+ self._chat.see(tk.END)
285
+
286
+ # ═══════════════════════════════════════════════════════════════════════
287
+ # Reasoning buffer
288
+ # ═══════════════════════════════════════════════════════════════════════
289
+
290
+ def _reasoning_show(self, text: str = "", *, final: bool = False) -> None:
291
+ """Accumulate and display the model's reasoning in a dimmed block.
292
+
293
+ Call with each chunk; call with final=True to flush on tool call / completion.
294
+ """
295
+ if text:
296
+ if not self._reasoning_buf:
297
+ self._reasoning_line_start = self._chat.index(tk.END + "-1c")
298
+ self._reasoning_buf += text
299
+ if not self._reasoning_buf:
300
+ return
301
+ # Truncate to roughly fit the chat pane width (monospace, ~8px/char)
302
+ try:
303
+ width = self._chat.winfo_width()
304
+ except Exception:
305
+ width = 0
306
+ if not isinstance(width, int) or width < 200:
307
+ width = 960
308
+ max_chars = max(60, (width - 80) // 8)
309
+ content = self._reasoning_buf.strip()
310
+ if len(content) > max_chars:
311
+ content = content[:max_chars] + "…"
312
+ self._replace_range(self._reasoning_line_start, tk.END,
313
+ f" ▸ {content}\n", "think")
314
+ if final:
315
+ self._reasoning_buf = ""
316
+ self._reasoning_line_start = ""
317
+
318
+ # ═══════════════════════════════════════════════════════════════════════
319
+ # Command popup
320
+ # ═══════════════════════════════════════════════════════════════════════
321
+
322
+ def _show_popup(self, filter_text: str = "") -> None:
323
+ matches = [(n, d) for n, d in self._commands if n.startswith(filter_text)]
324
+ if not matches:
325
+ self._hide_popup()
326
+ return
327
+
328
+ if self._popup is None:
329
+ self._popup = tk.Toplevel(self)
330
+ self._popup.wm_overrideredirect(True)
331
+ self._popup.configure(bg=T["overlay"], highlightbackground=T["border"],
332
+ highlightthickness=1)
333
+ self._popup_lb = tk.Listbox(self._popup, bg=T["overlay"], fg=T["fg"],
334
+ font=FTM, relief=tk.FLAT, selectbackground="#1f6feb33",
335
+ selectforeground=T["accent"], activestyle="none",
336
+ borderwidth=0, highlightthickness=0)
337
+ self._popup_lb.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
338
+ self._popup_lb.bind("<ButtonRelease-1>", self._on_popup_select)
339
+ self._popup_lb.bind("<Return>", self._on_popup_select)
340
+ self._popup_lb.bind("<Escape>", lambda e: self._hide_popup())
341
+
342
+ self._popup_lb.delete(0, tk.END)
343
+ max_w = 0
344
+ for name, desc in matches:
345
+ line = f" {name:<22} {desc}"
346
+ self._popup_lb.insert(tk.END, line)
347
+ if len(line) > max_w:
348
+ max_w = len(line)
349
+ self._popup_lb.configure(height=min(len(matches), 12), width=max_w + 2)
350
+ if matches:
351
+ self._popup_lb.selection_set(0)
352
+
353
+ x = self._input.winfo_rootx()
354
+ y = self._input.winfo_rooty() - (min(len(matches), 12) * 20 + 12)
355
+ self._popup.geometry(f"+{x}+{y}")
356
+ self._popup.deiconify()
357
+ self._popup.lift()
358
+
359
+ def _hide_popup(self) -> None:
360
+ if self._popup:
361
+ self._popup.withdraw()
362
+ self._input.focus_set()
363
+
364
+ def _on_popup_select(self, event: tk.Event | None = None) -> None:
365
+ if self._popup_lb is None:
366
+ return
367
+ sel = self._popup_lb.curselection()
368
+ if sel:
369
+ cmd_name = self._popup_lb.get(sel[0]).strip().split()[0]
370
+ self._input.delete(0, tk.END)
371
+ self._input.insert(0, cmd_name + " ")
372
+ self._input.icursor(tk.END)
373
+ self._hide_popup()
374
+
375
+ def _on_key(self, event: tk.Event) -> None:
376
+ text = self._input.get()
377
+ if text.startswith("/"):
378
+ self._debounce_popup(text)
379
+ else:
380
+ self._hide_popup()
381
+
382
+ def _debounce_popup(self, filter_text: str) -> None:
383
+ """Schedule popup refresh 150ms after last keystroke."""
384
+ if self._popup_debounce_id:
385
+ self.after_cancel(self._popup_debounce_id)
386
+ self._popup_debounce_id = self.after(150, lambda: self._show_popup(filter_text))
387
+
388
+ # ═══════════════════════════════════════════════════════════════════════
389
+ # Event poll
390
+ # ═══════════════════════════════════════════════════════════════════════
391
+
392
+ def _poll(self) -> None:
393
+ try:
394
+ while True:
395
+ self._dispatch(self._eq.get_nowait())
396
+ except queue.Empty:
397
+ pass
398
+ self.after(self._poll_interval, self._poll)
399
+
400
+ def _dispatch(self, ev: Any) -> None:
401
+ if isinstance(ev, TextDeltaEvent):
402
+ self._reasoning_show(final=True)
403
+ self._insert_streaming(ev.text)
404
+
405
+ elif isinstance(ev, ReasoningEvent):
406
+ self._reasoning_show(ev.text)
407
+
408
+ elif isinstance(ev, ThinkingEvent):
409
+ self._status_lbl.configure(text="● Thinking…", fg=T["yellow"])
410
+
411
+ elif isinstance(ev, ToolCallEvent):
412
+ self._reasoning_show(final=True)
413
+ self._flush_insert_buffer()
414
+ icon = {"builtin": "◆", "mcp": "◇"}.get(ev.source, "○")
415
+ args = brief_args(ev.arguments)
416
+ self._append(f" {icon} {ev.tool_name} {args}", "tool_h")
417
+ self._status_lbl.configure(text=f"● {ev.tool_name}", fg=T["purple"])
418
+
419
+ elif isinstance(ev, ToolResultEvent):
420
+ if ev.result.success:
421
+ out = (ev.result.output or "").replace("\n", " ")[:150]
422
+ self._append(f" ✓ {out}", "tool_ok")
423
+ else:
424
+ err = (ev.result.error or "unknown error")[:150]
425
+ self._append(f" ✗ {err}", "tool_er")
426
+
427
+ elif isinstance(ev, SkillChangedEvent):
428
+ self._active_skill = ev.skill_name
429
+ self._status_sk.configure(text=f"skill: {ev.skill_name}")
430
+
431
+ elif isinstance(ev, ErrorEvent):
432
+ self._reasoning_show(final=True)
433
+ self._flush_insert_buffer()
434
+ self._append(f"● {ev.error}", "error")
435
+ self._status_lbl.configure(text="● Error", fg=T["red"])
436
+
437
+ elif isinstance(ev, CompleteEvent):
438
+ self._reasoning_show(final=True)
439
+ self._flush_insert_buffer()
440
+ self._running = False
441
+ self._status_lbl.configure(text="● Ready", fg=T["green"])
442
+ self._send_btn.configure(text=" Send ", bg=T["green"], fg="#ffffff")
443
+ self._append(f"── {ev.total_tool_calls} tools · {ev.total_time:.1f}s ──", "sep")
444
+
445
+ # ═══════════════════════════════════════════════════════════════════════
446
+ # Actions
447
+ # ═══════════════════════════════════════════════════════════════════════
448
+
449
+ def _on_send(self, event: tk.Event | None = None) -> None:
450
+ self._hide_popup()
451
+ if self._running:
452
+ return
453
+ text = self._input.get().strip()
454
+ if not text:
455
+ return
456
+ self._input.delete(0, tk.END)
457
+ self._running = True
458
+
459
+ self._append("", "status")
460
+ self._append(f"You {text}", "user")
461
+
462
+ self._send_btn.configure(text=" … ", bg=T["surface"], fg=T["muted"])
463
+ self._status_lbl.configure(text="● Thinking…", fg=T["yellow"])
464
+
465
+ threading.Thread(target=self._run, args=(text,), daemon=True).start()
466
+
467
+ def _run(self, task: str) -> None:
468
+ try:
469
+ asyncio.run(self._agent.run(task, stream=True, skill_name=self._active_skill or None))
470
+ except Exception as e:
471
+ self._eq.put(ErrorEvent(f"Agent error: {e}"))
472
+ self._eq.put(CompleteEvent(0, 0))
473
+
474
+ def _change_model(self) -> None:
475
+ cur = self._config.llm.model
476
+ m = simpledialog.askstring("Change Model", "Model name:", initialvalue=cur)
477
+ if m and m.strip() and m.strip() != cur:
478
+ m = m.strip()
479
+ self._config.llm.model = m
480
+ self._agent.llm.set_model(m)
481
+ self._agent.llm.register_tools(self._agent._all_tools)
482
+ self.title(f"ATA Coder — {m}")
483
+ self._model_lbl.configure(text=m[:28])
484
+ self._append(f" ↻ Model → {m}", "status")
485
+
486
+ def _update_tokens(self) -> None:
487
+ total = self._agent.llm.total_tokens
488
+ self._status_tok.configure(text=f"tokens: ~{total:,}")
489
+
490
+ def _on_close(self) -> None:
491
+ try:
492
+ asyncio.run(self._agent.shutdown())
493
+ self._tool_exec.clear_file_cache()
494
+ except Exception:
495
+ pass
496
+ self.destroy()
497
+
498
+
499
+ # ── Entry ──────────────────────────────────────────────────────────────────────
500
+
501
+ def launch_gui(config: AppConfig | None = None, skill: str = "") -> None:
502
+ AtaCoderGUI(config=config, skill=skill).mainloop()
503
+
504
+
505
+ def main() -> None:
506
+ logging.basicConfig(level=logging.INFO)
507
+ launch_gui()
508
+
509
+
510
+ if __name__ == "__main__":
511
+ main()