axio-tui 0.6.2__tar.gz → 0.7.0__tar.gz

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 (27) hide show
  1. {axio_tui-0.6.2 → axio_tui-0.7.0}/PKG-INFO +5 -5
  2. {axio_tui-0.6.2 → axio_tui-0.7.0}/README.md +2 -2
  3. {axio_tui-0.6.2 → axio_tui-0.7.0}/pyproject.toml +3 -3
  4. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/app.py +8 -8
  5. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/plugin.py +6 -6
  6. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/screens.py +9 -9
  7. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/tools.py +10 -10
  8. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_guard_screens.py +6 -4
  9. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_plugin.py +7 -7
  10. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_plugin_toggle.py +6 -4
  11. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_tools.py +16 -13
  12. {axio_tui-0.6.2 → axio_tui-0.7.0}/.github/workflows/publish.yml +0 -0
  13. {axio_tui-0.6.2 → axio_tui-0.7.0}/.github/workflows/tests.yml +0 -0
  14. {axio_tui-0.6.2 → axio_tui-0.7.0}/.gitignore +0 -0
  15. {axio_tui-0.6.2 → axio_tui-0.7.0}/LICENSE +0 -0
  16. {axio_tui-0.6.2 → axio_tui-0.7.0}/Makefile +0 -0
  17. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/__init__.py +0 -0
  18. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/__main__.py +0 -0
  19. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/args.py +0 -0
  20. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/prompt.py +0 -0
  21. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/sqlite_config.py +0 -0
  22. {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/transport_registry.py +0 -0
  23. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/conftest.py +0 -0
  24. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_nav.py +0 -0
  25. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_sqlite_config.py +0 -0
  26. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_tool_status_widget.py +0 -0
  27. {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_transport_registry.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axio-tui
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: Textual TUI application for Axio
5
- Project-URL: Homepage, https://github.com/axio-agent/axio-tui
6
- Project-URL: Repository, https://github.com/axio-agent/axio-tui
5
+ Project-URL: Homepage, https://github.com/mosquito/axio-agent
6
+ Project-URL: Repository, https://github.com/mosquito/axio-agent
7
7
  License: MIT
8
8
  License-File: LICENSE
9
9
  Keywords: agent,ai,chat,llm,terminal,textual,tui
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
  [![Python](https://img.shields.io/pypi/pyversions/axio-tui)](https://pypi.org/project/axio-tui/)
41
41
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
42
42
 
43
- Textual TUI application for [axio](https://github.com/axio-agent/axio).
43
+ Textual TUI application for [axio](https://github.com/mosquito/axio-agent).
44
44
 
45
45
  A full-featured terminal chat interface with session management, a plugin system for transports and tools, and a built-in SQLite context store. Pick your LLM backend, load your tools, and start chatting — all from the terminal.
46
46
 
@@ -150,7 +150,7 @@ textual-serve axio_tui.__main__:app
150
150
 
151
151
  ## Part of the axio ecosystem
152
152
 
153
- [axio](https://github.com/axio-agent/axio) · [axio-transport-anthropic](https://github.com/axio-agent/axio-transport-anthropic) · [axio-tui-guards](https://github.com/axio-agent/axio-tui-guards) · [axio-tools-local](https://github.com/axio-agent/axio-tools-local) · [axio-tools-mcp](https://github.com/axio-agent/axio-tools-mcp) · [axio-tools-docker](https://github.com/axio-agent/axio-tools-docker)
153
+ [axio](https://github.com/mosquito/axio-agent) · [axio-transport-anthropic](https://github.com/mosquito/axio-agent) · [axio-tui-guards](https://github.com/mosquito/axio-agent) · [axio-tools-local](https://github.com/mosquito/axio-agent) · [axio-tools-mcp](https://github.com/mosquito/axio-agent) · [axio-tools-docker](https://github.com/mosquito/axio-agent)
154
154
 
155
155
  ## License
156
156
 
@@ -4,7 +4,7 @@
4
4
  [![Python](https://img.shields.io/pypi/pyversions/axio-tui)](https://pypi.org/project/axio-tui/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
6
 
7
- Textual TUI application for [axio](https://github.com/axio-agent/axio).
7
+ Textual TUI application for [axio](https://github.com/mosquito/axio-agent).
8
8
 
9
9
  A full-featured terminal chat interface with session management, a plugin system for transports and tools, and a built-in SQLite context store. Pick your LLM backend, load your tools, and start chatting — all from the terminal.
10
10
 
@@ -114,7 +114,7 @@ textual-serve axio_tui.__main__:app
114
114
 
115
115
  ## Part of the axio ecosystem
116
116
 
117
- [axio](https://github.com/axio-agent/axio) · [axio-transport-anthropic](https://github.com/axio-agent/axio-transport-anthropic) · [axio-tui-guards](https://github.com/axio-agent/axio-tui-guards) · [axio-tools-local](https://github.com/axio-agent/axio-tools-local) · [axio-tools-mcp](https://github.com/axio-agent/axio-tools-mcp) · [axio-tools-docker](https://github.com/axio-agent/axio-tools-docker)
117
+ [axio](https://github.com/mosquito/axio-agent) · [axio-transport-anthropic](https://github.com/mosquito/axio-agent) · [axio-tui-guards](https://github.com/mosquito/axio-agent) · [axio-tools-local](https://github.com/mosquito/axio-agent) · [axio-tools-mcp](https://github.com/mosquito/axio-agent) · [axio-tools-docker](https://github.com/mosquito/axio-agent)
118
118
 
119
119
  ## License
120
120
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "axio-tui"
3
- version = "0.6.2"
3
+ version = "0.7.0"
4
4
  description = "Textual TUI application for Axio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -36,8 +36,8 @@ axio = "axio_tui.__main__:main"
36
36
 
37
37
 
38
38
  [project.urls]
39
- Homepage = "https://github.com/axio-agent/axio-tui"
40
- Repository = "https://github.com/axio-agent/axio-tui"
39
+ Homepage = "https://github.com/mosquito/axio-agent"
40
+ Repository = "https://github.com/mosquito/axio-agent"
41
41
  [build-system]
42
42
  requires = ["hatchling"]
43
43
  build-backend = "hatchling.build"
@@ -378,14 +378,14 @@ class AgentApp(App[None]):
378
378
  self._last_input_tokens = 0
379
379
  self._chat_model: ModelSpec | None = None
380
380
  self._model_name = ""
381
- self._all_tools: list[Tool] = []
381
+ self._all_tools: list[Tool[Any]] = []
382
382
  self._disabled_plugins: set[str] = set()
383
383
  self._disabled_transports: set[str] = set()
384
384
  self._disabled_guards: set[str] = set()
385
385
  self._guard_tool_map: dict[str, set[str]] = {}
386
386
  self._guard_instances: dict[str, PermissionGuard] = {}
387
387
  self._guard_classes: dict[str, type[PermissionGuard]] = {}
388
- self._active_tools: list[Tool] = []
388
+ self._active_tools: list[Tool[Any]] = []
389
389
  self._chat_transport: Any = None
390
390
  self._selector_classes: dict[str, type] = {}
391
391
  self._selector_instances: dict[str, Any] = {}
@@ -535,7 +535,7 @@ class AgentApp(App[None]):
535
535
  self._chat_transport = chat_transport
536
536
 
537
537
  # Discover tools and guards via entry points
538
- tools: list[Tool] = discover_tools()
538
+ tools: list[Tool[Any]] = discover_tools()
539
539
  self._guard_classes = discover_guards()
540
540
 
541
541
  # Load disabled plugins from config
@@ -842,7 +842,7 @@ class AgentApp(App[None]):
842
842
  return None
843
843
 
844
844
  def _rebuild_agent_tools(self) -> None:
845
- tools: list[Tool] = []
845
+ tools: list[Tool[Any]] = []
846
846
  for t in self._all_tools:
847
847
  if t.name in self._disabled_plugins:
848
848
  continue
@@ -1005,7 +1005,7 @@ class AgentApp(App[None]):
1005
1005
  return False
1006
1006
  return self._last_input_tokens / self._chat_model.context_window > 0.7
1007
1007
 
1008
- async def _compact_with_progress(self, max_messages: int = 20) -> list[Message] | None:
1008
+ async def _compact_with_progress(self) -> list[Message] | None:
1009
1009
  """Run compaction while showing a live elapsed-time counter in the chat."""
1010
1010
  if self._agent is None:
1011
1011
  return None
@@ -1027,7 +1027,7 @@ class AgentApp(App[None]):
1027
1027
  transport = self._transports.make_transport(cb.transport, cb.model)
1028
1028
  else:
1029
1029
  transport = self._agent.transport
1030
- return await compact_context(self._chat_context, transport, max_messages=max_messages)
1030
+ return await compact_context(self._chat_context, transport)
1031
1031
  finally:
1032
1032
  timer.stop()
1033
1033
  await widget.remove()
@@ -1209,7 +1209,7 @@ class AgentApp(App[None]):
1209
1209
 
1210
1210
  def _on_tools_plugin_dismissed(self) -> None:
1211
1211
  # Rebuild tools list with all plugin tools
1212
- tools: list[Tool] = discover_tools()
1212
+ tools: list[Tool[Any]] = discover_tools()
1213
1213
  for plugin in self._tools_plugins.values():
1214
1214
  tools.extend(plugin.all_tools)
1215
1215
  self._all_tools = tools
@@ -1297,7 +1297,7 @@ class AgentApp(App[None]):
1297
1297
 
1298
1298
  self._is_running = True
1299
1299
  if self._should_compact():
1300
- messages = await self._compact_with_progress(max_messages=0)
1300
+ messages = await self._compact_with_progress()
1301
1301
  if messages is not None:
1302
1302
  await self._replace_context(messages)
1303
1303
  self._last_input_tokens = 0
@@ -27,7 +27,7 @@ class ToolsPlugin(Protocol):
27
27
  async def init(self, config: Any = None, global_config: Any = None) -> None: ...
28
28
 
29
29
  @property
30
- def all_tools(self) -> list[Tool]: ...
30
+ def all_tools(self) -> list[Tool[Any]]: ...
31
31
 
32
32
  def settings_screen(self) -> Any: ...
33
33
 
@@ -42,7 +42,7 @@ TOOLS_SETTINGS_GROUP = "axio.tools.settings"
42
42
  SELECTOR_GROUP = "axio.selector"
43
43
 
44
44
 
45
- def _handler_description(handler: type[ToolHandler]) -> str:
45
+ def _handler_description(handler: type[ToolHandler[Any]]) -> str:
46
46
  """Extract handler docstring as tool description."""
47
47
  doc = handler.__doc__
48
48
  if doc:
@@ -50,9 +50,9 @@ def _handler_description(handler: type[ToolHandler]) -> str:
50
50
  return handler.__name__
51
51
 
52
52
 
53
- def discover_tools() -> list[Tool]:
53
+ def discover_tools() -> list[Tool[Any]]:
54
54
  """Load ToolHandler classes from 'axio.tools' entry points, build Tool objects."""
55
- tools: list[Tool] = []
55
+ tools: list[Tool[Any]] = []
56
56
  for ep in entry_points(group=TOOLS_GROUP):
57
57
  try:
58
58
  handler = ep.load()
@@ -74,9 +74,9 @@ def discover_tools() -> list[Tool]:
74
74
  return tools
75
75
 
76
76
 
77
- def discover_tools_by_package() -> dict[str, list[Tool]]:
77
+ def discover_tools_by_package() -> dict[str, list[Tool[Any]]]:
78
78
  """Return tools from 'axio.tools' entry points grouped by distribution package name."""
79
- groups: dict[str, list[Tool]] = {}
79
+ groups: dict[str, list[Tool[Any]]] = {}
80
80
  for ep in entry_points(group=TOOLS_GROUP):
81
81
  try:
82
82
  handler = ep.load()
@@ -247,13 +247,13 @@ class ToolSelectScreen(ModalScreen[set[str] | None]):
247
247
  #tool-list { height: 1fr; }
248
248
  """
249
249
 
250
- def __init__(self, groups: dict[str, list[Tool]], disabled: set[str]) -> None:
250
+ def __init__(self, groups: dict[str, list[Tool[Any]]], disabled: set[str]) -> None:
251
251
  super().__init__()
252
252
  self._groups = groups
253
- self._all_tools: list[Tool] = [t for ts in groups.values() for t in ts]
253
+ self._all_tools: list[Tool[Any]] = [t for ts in groups.values() for t in ts]
254
254
  self._disabled = set(disabled)
255
255
  self._filter_query = ""
256
- self._items: list[str | Tool] = []
256
+ self._items: list[str | Tool[Any]] = []
257
257
  self._rebuild_items()
258
258
 
259
259
  def _rebuild_items(self) -> None:
@@ -261,7 +261,7 @@ class ToolSelectScreen(ModalScreen[set[str] | None]):
261
261
  if q:
262
262
  self._items = [t for t in self._all_tools if q in t.name.lower() or q in t.description.lower()]
263
263
  return
264
- items: list[str | Tool] = []
264
+ items: list[str | Tool[Any]] = []
265
265
  for pkg, tools in self._groups.items():
266
266
  items.append(pkg)
267
267
  items.extend(tools)
@@ -274,12 +274,12 @@ class ToolSelectScreen(ModalScreen[set[str] | None]):
274
274
  mark = "[*]" if n_on == total else ("[ ]" if n_on == 0 else "[-]")
275
275
  return f"{mark} ── {pkg} ({n_on}/{total}) ──"
276
276
 
277
- def _tool_line(self, tool: Tool, *, indent: bool = False) -> str:
277
+ def _tool_line(self, tool: Tool[Any], *, indent: bool = False) -> str:
278
278
  mark = "[ ]" if tool.name in self._disabled else "[*]"
279
279
  prefix = " " if indent else ""
280
280
  return f"{prefix}{mark} {tool.name:<22} {tool.description}"
281
281
 
282
- def _format(self, item: str | Tool) -> str:
282
+ def _format(self, item: str | Tool[Any]) -> str:
283
283
  if isinstance(item, str):
284
284
  return self._group_header(item)
285
285
  return self._tool_line(item, indent=not self._filter_query)
@@ -323,7 +323,7 @@ class ToolSelectScreen(ModalScreen[set[str] | None]):
323
323
  self._rebuild_items()
324
324
  self._refresh_list()
325
325
 
326
- def _select(self, item: str | Tool) -> None:
326
+ def _select(self, item: str | Tool[Any]) -> None:
327
327
  if isinstance(item, str):
328
328
  tools = self._groups[item]
329
329
  all_on = all(t.name not in self._disabled for t in tools)
@@ -400,7 +400,7 @@ class PluginHubScreen(ModalScreen[None]):
400
400
 
401
401
  def __init__(
402
402
  self,
403
- tool_groups: dict[str, list[Tool]],
403
+ tool_groups: dict[str, list[Tool[Any]]],
404
404
  transport_available: list[str],
405
405
  transport_discovered: list[str],
406
406
  transport_model_counts: dict[str, int],
@@ -419,7 +419,7 @@ class PluginHubScreen(ModalScreen[None]):
419
419
  ) -> None:
420
420
  super().__init__()
421
421
  self._tool_groups = tool_groups
422
- self._all_tools: list[Tool] = [t for ts in tool_groups.values() for t in ts]
422
+ self._all_tools: list[Tool[Any]] = [t for ts in tool_groups.values() for t in ts]
423
423
  self._transport_available = transport_available
424
424
  self._transport_discovered = transport_discovered
425
425
  self._transport_model_counts = transport_model_counts
@@ -19,7 +19,7 @@ def _short(value: Any, limit: int = 60) -> str:
19
19
  return s if len(s) <= limit else s[:limit] + "..."
20
20
 
21
21
 
22
- class StatusLine(ToolHandler):
22
+ class StatusLine(ToolHandler[Any]):
23
23
  """Set a short status message shown to the user in the TUI status bar.
24
24
  Call this at the start of every turn before other tools to indicate
25
25
  what you are about to do (e.g. "Reading project files",
@@ -31,13 +31,13 @@ class StatusLine(ToolHandler):
31
31
  def __repr__(self) -> str:
32
32
  return f"StatusLine(message={self.message!r})"
33
33
 
34
- async def __call__(self) -> str:
34
+ async def __call__(self, context: Any) -> str:
35
35
  if StatusLine._callback:
36
36
  StatusLine._callback(self.message)
37
37
  return "ok"
38
38
 
39
39
 
40
- class Confirm(ToolHandler):
40
+ class Confirm(ToolHandler[Any]):
41
41
  """Submit a safety verdict for a pending tool call. Used by the
42
42
  LLM-based permission guard. Verdict must be SAFE, RISKY, or DENY.
43
43
  Provide a clear reason and a category for the action."""
@@ -49,7 +49,7 @@ class Confirm(ToolHandler):
49
49
  def __repr__(self) -> str:
50
50
  return f"Confirm(verdict={self.verdict!r}, category={self.category!r})"
51
51
 
52
- async def __call__(self) -> str:
52
+ async def __call__(self, context: Any) -> str:
53
53
  return self.verdict
54
54
 
55
55
 
@@ -142,7 +142,7 @@ SUBAGENT_SYSTEM_PROMPT = """\
142
142
  </subagent_behavior>"""
143
143
 
144
144
 
145
- class SubAgent(ToolHandler):
145
+ class SubAgent(ToolHandler[Any]):
146
146
  """Delegate a task to an independent sub-agent. The sub-agent receives
147
147
  conversation context and has access to the same tools (except subagent).
148
148
  Up to 3 sub-agents run concurrently. Use for parallelizing independent
@@ -157,11 +157,11 @@ class SubAgent(ToolHandler):
157
157
  def __repr__(self) -> str:
158
158
  return f"SubAgent(task={_short(self.task)!r})"
159
159
 
160
- async def __call__(self) -> str:
160
+ async def __call__(self, context: Any) -> str:
161
161
  if SubAgent._factory is None:
162
162
  return "SubAgent is not configured"
163
- agent, context = await SubAgent._factory()
164
- return await agent.run(self.task, context)
163
+ agent, store = await SubAgent._factory()
164
+ return await agent.run(self.task, store)
165
165
 
166
166
 
167
167
  _IMAGE_MEDIA_TYPES: dict[str, str] = {
@@ -173,7 +173,7 @@ _IMAGE_MEDIA_TYPES: dict[str, str] = {
173
173
  }
174
174
 
175
175
 
176
- class VisionAnalyze(ToolHandler):
176
+ class VisionAnalyze(ToolHandler[Any]):
177
177
  """Analyze an image file using a vision-capable model. Supports
178
178
  JPEG, PNG, GIF, and WebP formats. Provide a path to the image and
179
179
  an optional prompt describing what to look for. Returns the model's
@@ -186,7 +186,7 @@ class VisionAnalyze(ToolHandler):
186
186
  def __repr__(self) -> str:
187
187
  return f"VisionAnalyze(path={_short(self.path)!r})"
188
188
 
189
- async def __call__(self) -> str:
189
+ async def __call__(self, context: Any) -> str:
190
190
  import os
191
191
 
192
192
  if VisionAnalyze._transport is None:
@@ -2,19 +2,21 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Any
6
+
5
7
  from axio.tool import Tool, ToolHandler
6
8
 
7
9
  from axio_tui.screens import GuardSelectScreen, GuardToolsScreen, PluginHubScreen, ToolDetailScreen
8
10
 
9
11
 
10
- class _StubHandler(ToolHandler):
12
+ class _StubHandler(ToolHandler[Any]):
11
13
  """Stub tool handler."""
12
14
 
13
- async def __call__(self) -> str:
15
+ async def __call__(self, context: Any) -> str:
14
16
  return ""
15
17
 
16
18
 
17
- def _make_tools(*names: str) -> list[Tool]:
19
+ def _make_tools(*names: str) -> list[Tool[Any]]:
18
20
  return [Tool(name=n, description=f"Description for {n}", handler=_StubHandler) for n in names]
19
21
 
20
22
 
@@ -109,7 +111,7 @@ class TestGuardSelectScreen:
109
111
 
110
112
 
111
113
  def _make_hub(
112
- tools: list[Tool],
114
+ tools: list[Tool[Any]],
113
115
  disabled_plugins: set[str] = frozenset(), # type: ignore[assignment]
114
116
  disabled_guards: set[str] = frozenset(), # type: ignore[assignment]
115
117
  guard_tool_map: dict[str, set[str]] | None = None,
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import ClassVar
5
+ from typing import Any, ClassVar
6
6
  from unittest.mock import MagicMock, patch
7
7
 
8
8
  from axio.permission import PermissionGuard
@@ -11,29 +11,29 @@ from axio.tool import ToolHandler
11
11
  from axio_tui.plugin import discover_guards, discover_tools, discover_transports
12
12
 
13
13
 
14
- class _EchoHandler(ToolHandler):
14
+ class _EchoHandler(ToolHandler[Any]):
15
15
  """Echo back input text."""
16
16
 
17
17
  text: str = ""
18
18
 
19
- async def __call__(self) -> str:
19
+ async def __call__(self, context: Any) -> str:
20
20
  return self.text
21
21
 
22
22
 
23
- class _NoDocHandler(ToolHandler):
23
+ class _NoDocHandler(ToolHandler[Any]):
24
24
  text: str = ""
25
25
 
26
- async def __call__(self) -> str:
26
+ async def __call__(self, context: Any) -> str:
27
27
  return self.text
28
28
 
29
29
 
30
- class _ConcurrentHandler(ToolHandler):
30
+ class _ConcurrentHandler(ToolHandler[Any]):
31
31
  """Handler with concurrency limit."""
32
32
 
33
33
  _tool_concurrency: ClassVar[int | None] = 2
34
34
  text: str = ""
35
35
 
36
- async def __call__(self) -> str:
36
+ async def __call__(self, context: Any) -> str:
37
37
  return self.text
38
38
 
39
39
 
@@ -2,24 +2,26 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Any
6
+
5
7
  import pytest
6
8
  from axio.tool import Tool, ToolHandler
7
9
 
8
10
  from axio_tui.screens import ToolSelectScreen
9
11
 
10
12
 
11
- class _StubHandler(ToolHandler):
13
+ class _StubHandler(ToolHandler[Any]):
12
14
  """Stub tool handler."""
13
15
 
14
- async def __call__(self) -> str:
16
+ async def __call__(self, context: Any) -> str:
15
17
  return ""
16
18
 
17
19
 
18
- def _make_tools(*names: str) -> list[Tool]:
20
+ def _make_tools(*names: str) -> list[Tool[Any]]:
19
21
  return [Tool(name=n, description=f"Description for {n}", handler=_StubHandler) for n in names]
20
22
 
21
23
 
22
- def _screen(tools: list[Tool], disabled: set[str]) -> ToolSelectScreen:
24
+ def _screen(tools: list[Tool[Any]], disabled: set[str]) -> ToolSelectScreen:
23
25
  return ToolSelectScreen(groups={"pkg": tools}, disabled=disabled)
24
26
 
25
27
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import os
6
6
  from collections.abc import AsyncIterator
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
10
  import pytest
10
11
  from axio.agent import Agent
@@ -20,13 +21,13 @@ from axio_tui.tools import Confirm, StatusLine, SubAgent, VisionAnalyze
20
21
  class TestStatusLine:
21
22
  async def test_returns_ok(self) -> None:
22
23
  handler = StatusLine(message="working")
23
- assert await handler() == "ok"
24
+ assert await handler({}) == "ok"
24
25
 
25
26
 
26
27
  class TestConfirm:
27
28
  async def test_returns_verdict(self) -> None:
28
29
  handler = Confirm(verdict="SAFE", reason="harmless", category="read")
29
- assert await handler() == "SAFE"
30
+ assert await handler({}) == "SAFE"
30
31
 
31
32
  async def test_repr(self) -> None:
32
33
  handler = Confirm(verdict="DENY", reason="bad", category="exec")
@@ -36,7 +37,7 @@ class TestConfirm:
36
37
 
37
38
 
38
39
  class TestSubAgent:
39
- def _make_agent(self, transport: StubTransport, tools: list[Tool] | None = None) -> Agent:
40
+ def _make_agent(self, transport: StubTransport, tools: list[Tool[Any]] | None = None) -> Agent:
40
41
  return Agent(system="test", tools=tools or [], transport=transport, max_iterations=5)
41
42
 
42
43
  async def test_returns_subagent_result(self) -> None:
@@ -49,7 +50,7 @@ class TestSubAgent:
49
50
  SubAgent._factory = factory
50
51
  try:
51
52
  handler = SubAgent(task="do something")
52
- result = await handler()
53
+ result = await handler({})
53
54
  assert result == "sub-result"
54
55
  finally:
55
56
  SubAgent._factory = None
@@ -57,7 +58,7 @@ class TestSubAgent:
57
58
  async def test_no_factory_returns_error(self) -> None:
58
59
  SubAgent._factory = None
59
60
  handler = SubAgent(task="do something")
60
- result = await handler()
61
+ result = await handler({})
61
62
  assert result == "SubAgent is not configured"
62
63
 
63
64
  async def test_context_forking(self) -> None:
@@ -80,7 +81,7 @@ class TestSubAgent:
80
81
  SubAgent._factory = factory
81
82
  try:
82
83
  handler = SubAgent(task="check context")
83
- await handler()
84
+ await handler({})
84
85
  assert len(received_context) == 1
85
86
  history = await received_context[0].get_history()
86
87
  assert history[0].content[0].text == "hello" # type: ignore[attr-defined]
@@ -97,7 +98,7 @@ class TestSubAgent:
97
98
  try:
98
99
  handler = SubAgent(task="boom")
99
100
  with pytest.raises(RuntimeError, match="factory failed"):
100
- await handler()
101
+ await handler({})
101
102
  finally:
102
103
  SubAgent._factory = None
103
104
 
@@ -152,7 +153,7 @@ class TestVisionAnalyze:
152
153
  async def test_no_transport_returns_error(self) -> None:
153
154
  VisionAnalyze._transport = None
154
155
  handler = VisionAnalyze(path="img.png")
155
- result = await handler()
156
+ result = await handler({})
156
157
  assert "not configured" in result
157
158
 
158
159
  async def test_file_not_found(self, tmp_path: Path) -> None:
@@ -161,7 +162,7 @@ class TestVisionAnalyze:
161
162
  try:
162
163
  os.chdir(tmp_path)
163
164
  handler = VisionAnalyze(path="missing.png")
164
- result = await handler()
165
+ result = await handler({})
165
166
  assert "File not found" in result
166
167
  finally:
167
168
  os.chdir(old_cwd)
@@ -174,7 +175,7 @@ class TestVisionAnalyze:
174
175
  try:
175
176
  os.chdir(tmp_path)
176
177
  handler = VisionAnalyze(path="file.bmp")
177
- result = await handler()
178
+ result = await handler({})
178
179
  assert "Unsupported image format" in result
179
180
  finally:
180
181
  os.chdir(old_cwd)
@@ -188,7 +189,7 @@ class TestVisionAnalyze:
188
189
  try:
189
190
  os.chdir(tmp_path)
190
191
  handler = VisionAnalyze(path="photo.png", prompt="What is this?")
191
- result = await handler()
192
+ result = await handler({})
192
193
  assert result == "A red pixel"
193
194
  finally:
194
195
  os.chdir(old_cwd)
@@ -202,7 +203,9 @@ class TestVisionAnalyze:
202
203
  captured: list[list[Message]] = []
203
204
 
204
205
  class CapturingTransport(StubTransport):
205
- def stream(self, messages: list[Message], tools: list[Tool], system: str) -> AsyncIterator[StreamEvent]:
206
+ def stream(
207
+ self, messages: list[Message], tools: list[Tool[Any]], system: str
208
+ ) -> AsyncIterator[StreamEvent]:
206
209
  captured.append(messages)
207
210
  return super().stream(messages, tools, system)
208
211
 
@@ -212,7 +215,7 @@ class TestVisionAnalyze:
212
215
  try:
213
216
  os.chdir(tmp_path)
214
217
  handler = VisionAnalyze(path="test.jpg", prompt="Describe it")
215
- await handler()
218
+ await handler({})
216
219
  assert len(captured) == 1
217
220
  msg = captured[0][0]
218
221
  assert len(msg.content) == 2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes