deepagents-cli 0.0.29__tar.gz → 0.0.30__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.
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/CHANGELOG.md +14 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/PKG-INFO +2 -1
- deepagents_cli-0.0.30/deepagents_cli/_version.py +3 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/app.py +29 -7
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/main.py +159 -8
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/model_config.py +122 -1
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/ui.py +1 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/chat_input.py +0 -2
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/messages.py +52 -8
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/model_selector.py +137 -2
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/status.py +22 -1
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/pyproject.toml +4 -1
- deepagents_cli-0.0.30/tests/integration_tests/test_acp_mode.py +112 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_autocomplete.py +4 -1
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_chat_input.py +42 -0
- deepagents_cli-0.0.30/tests/unit_tests/test_main_acp_mode.py +182 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_messages.py +151 -1
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_model_config.py +226 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_model_selector.py +163 -1
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_model_switch.py +2 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/uv.lock +137 -233
- deepagents_cli-0.0.29/deepagents_cli/_version.py +0 -3
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/.gitignore +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/Makefile +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/README.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/__main__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/agent.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/app.tcss +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/built_in_skills/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/built_in_skills/skill-creator/SKILL.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/built_in_skills/skill-creator/scripts/init_skill.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/built_in_skills/skill-creator/scripts/quick_validate.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/clipboard.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/config.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/default_agent_prompt.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/file_ops.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/hooks.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/input.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/integrations/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/integrations/daytona.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/integrations/langsmith.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/integrations/modal.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/integrations/runloop.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/integrations/sandbox_factory.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/integrations/sandbox_provider.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/local_context.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/mcp_tools.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/mcp_trust.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/media_utils.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/non_interactive.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/project_utils.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/py.typed +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/sessions.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/skills/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/skills/commands.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/skills/load.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/subagents.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/system_prompt.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/textual_adapter.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/tool_display.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/tools.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/unicode_security.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/update_check.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/_links.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/approval.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/autocomplete.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/diff.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/history.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/loading.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/mcp_viewer.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/message_store.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/thread_selector.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/tool_renderers.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/tool_widgets.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/deepagents_cli/widgets/welcome.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/examples/skills/arxiv-search/SKILL.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/examples/skills/arxiv-search/arxiv_search.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/examples/skills/langgraph-docs/SKILL.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/examples/skills/skill-creator/SKILL.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/examples/skills/skill-creator/scripts/init_skill.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/examples/skills/skill-creator/scripts/quick_validate.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/examples/skills/web-research/SKILL.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/images/cli.png +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/scripts/check_imports.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/README.md +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/integration_tests/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/integration_tests/benchmarks/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/integration_tests/benchmarks/test_startup_benchmarks.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/integration_tests/conftest.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/integration_tests/test_sandbox_factory.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/integration_tests/test_sandbox_operations.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/conftest.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/skills/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/skills/test_commands.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/skills/test_load.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_agent.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_app.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_approval.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_args.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_charset.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_compact.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_compact_tool.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_config.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_end_to_end.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_exception_handling.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_file_ops.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_history.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_hooks.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_imports.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_input_parsing.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_local_context.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_main.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_main_args.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_mcp_tools.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_mcp_trust.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_mcp_viewer.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_media_utils.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_message_store.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_non_interactive.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_sessions.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_shell_allow_list.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_subagents.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_textual_adapter.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_thread_selector.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_token_tracker.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_ui.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_unicode_security.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_update_check.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_version.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/test_welcome.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/tools/__init__.py +0 -0
- {deepagents_cli-0.0.29 → deepagents_cli-0.0.30}/tests/unit_tests/tools/test_fetch_url.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.30](https://github.com/langchain-ai/deepagents/compare/deepagents-cli==0.0.29...deepagents-cli==0.0.30) (2026-03-07)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* `--acp` mode to run CLI agent as ACP server ([#1297](https://github.com/langchain-ai/deepagents/issues/1297)) ([c9ba00a](https://github.com/langchain-ai/deepagents/commit/c9ba00a56b7ee5e48b56b13f9f093bb8bf639700))
|
|
8
|
+
* Model detail footer + persist `--profile-override` on hot-swap ([#1700](https://github.com/langchain-ai/deepagents/issues/1700)) ([f2c8b54](https://github.com/langchain-ai/deepagents/commit/f2c8b54e9b4c541bf6f91139bfb9b6a2f20c8de0))
|
|
9
|
+
* Show message timestamp toast on click ([#1702](https://github.com/langchain-ai/deepagents/issues/1702)) ([4f403ec](https://github.com/langchain-ai/deepagents/commit/4f403ecb3332010062158ec30fd55f349654a533))
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* Expire `ctrl+c` quit window when toast disappears ([#1701](https://github.com/langchain-ai/deepagents/issues/1701)) ([38b5ea9](https://github.com/langchain-ai/deepagents/commit/38b5ea9484ab121c9b2919dd74469e82fce19b82))
|
|
14
|
+
* Preserve input text when escaping shell/command mode ([#1706](https://github.com/langchain-ai/deepagents/issues/1706)) ([3c00edb](https://github.com/langchain-ai/deepagents/commit/3c00edb93eddf74e87d58526a02be72577ed65b1))
|
|
15
|
+
* Right-align token count next to model name in status bar ([#1705](https://github.com/langchain-ai/deepagents/issues/1705)) ([311c919](https://github.com/langchain-ai/deepagents/commit/311c9191cf663540e1b62eb9452abecda5bc7b4f))
|
|
16
|
+
|
|
3
17
|
## [0.0.29](https://github.com/langchain-ai/deepagents/compare/deepagents-cli==0.0.28...deepagents-cli==0.0.29) (2026-03-06)
|
|
4
18
|
|
|
5
19
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.30
|
|
4
4
|
Summary: Terminal interface for Deep Agents - interactive AI agent with file operations, shell access, and sub-agent capabilities.
|
|
5
5
|
Project-URL: Homepage, https://docs.langchain.com/oss/python/deepagents/overview
|
|
6
6
|
Project-URL: Documentation, https://reference.langchain.com/python/deepagents/
|
|
@@ -25,6 +25,7 @@ Classifier: Topic :: Terminals
|
|
|
25
25
|
Requires-Python: <4.0,>=3.11
|
|
26
26
|
Requires-Dist: aiosqlite<1.0.0,>=0.19.0
|
|
27
27
|
Requires-Dist: daytona<1.0.0,>=0.113.0
|
|
28
|
+
Requires-Dist: deepagents-acp>=0.0.4
|
|
28
29
|
Requires-Dist: deepagents==0.4.7
|
|
29
30
|
Requires-Dist: langchain-mcp-adapters<1.0.0,>=0.2.0
|
|
30
31
|
Requires-Dist: langchain-openai<2.0.0,>=1.1.8
|
|
@@ -539,6 +539,7 @@ class DeepAgentsApp(App):
|
|
|
539
539
|
sandbox: SandboxBackendProtocol | None = None,
|
|
540
540
|
sandbox_type: str | None = None,
|
|
541
541
|
mcp_server_info: list[MCPServerInfo] | None = None,
|
|
542
|
+
profile_override: dict[str, Any] | None = None,
|
|
542
543
|
**kwargs: Any,
|
|
543
544
|
) -> None:
|
|
544
545
|
"""Initialize the Deep Agents application.
|
|
@@ -556,6 +557,8 @@ class DeepAgentsApp(App):
|
|
|
556
557
|
sandbox: Sandbox backend (for model hot-swap)
|
|
557
558
|
sandbox_type: Type of sandbox provider (for model hot-swap)
|
|
558
559
|
mcp_server_info: MCP server metadata for the `/mcp` viewer.
|
|
560
|
+
profile_override: Extra profile fields from `--profile-override`,
|
|
561
|
+
retained for model hot-swap and footer display.
|
|
559
562
|
**kwargs: Additional arguments passed to parent
|
|
560
563
|
"""
|
|
561
564
|
super().__init__(**kwargs)
|
|
@@ -573,6 +576,7 @@ class DeepAgentsApp(App):
|
|
|
573
576
|
self._sandbox = sandbox
|
|
574
577
|
self._sandbox_type = sandbox_type
|
|
575
578
|
self._mcp_server_info = mcp_server_info
|
|
579
|
+
self._profile_override = profile_override
|
|
576
580
|
self._mcp_tool_count = sum(len(s.tools) for s in (mcp_server_info or []))
|
|
577
581
|
self._status_bar: StatusBar | None = None
|
|
578
582
|
self._chat_input: ChatInput | None = None
|
|
@@ -1649,7 +1653,10 @@ class DeepAgentsApp(App):
|
|
|
1649
1653
|
|
|
1650
1654
|
try:
|
|
1651
1655
|
model_spec = f"{settings.model_provider}:{settings.model_name}"
|
|
1652
|
-
result = create_model(
|
|
1656
|
+
result = create_model(
|
|
1657
|
+
model_spec,
|
|
1658
|
+
profile_overrides=self._profile_override,
|
|
1659
|
+
)
|
|
1653
1660
|
model = result.model
|
|
1654
1661
|
except Exception as exc: # noqa: BLE001 # surface model config errors to user
|
|
1655
1662
|
await self._mount_message(
|
|
@@ -1659,10 +1666,10 @@ class DeepAgentsApp(App):
|
|
|
1659
1666
|
)
|
|
1660
1667
|
return
|
|
1661
1668
|
|
|
1662
|
-
# create_model()
|
|
1663
|
-
#
|
|
1664
|
-
#
|
|
1665
|
-
#
|
|
1669
|
+
# create_model() receives --profile-override via self._profile_override,
|
|
1670
|
+
# but settings.model_context_limit may have been set by additional
|
|
1671
|
+
# runtime logic. Patch it into the fresh model when it differs from
|
|
1672
|
+
# the profile value.
|
|
1666
1673
|
ctx = settings.model_context_limit
|
|
1667
1674
|
if ctx is not None:
|
|
1668
1675
|
# Guard against models that lack a profile dict
|
|
@@ -2340,6 +2347,10 @@ class DeepAgentsApp(App):
|
|
|
2340
2347
|
|
|
2341
2348
|
# Store message data for virtualization
|
|
2342
2349
|
message_data = MessageData.from_widget(widget)
|
|
2350
|
+
# Ensure the widget's DOM id matches the store id so that
|
|
2351
|
+
# features like click-to-show-timestamp can look it up.
|
|
2352
|
+
if not widget.id:
|
|
2353
|
+
widget.id = message_data.id
|
|
2343
2354
|
self._message_store.append(message_data)
|
|
2344
2355
|
|
|
2345
2356
|
# Queued-message widgets must always stay at the bottom so they
|
|
@@ -2485,7 +2496,9 @@ class DeepAgentsApp(App):
|
|
|
2485
2496
|
self.exit()
|
|
2486
2497
|
else:
|
|
2487
2498
|
self._quit_pending = True
|
|
2488
|
-
|
|
2499
|
+
quit_timeout = 3
|
|
2500
|
+
self.notify("Press Ctrl+C again to quit", timeout=quit_timeout)
|
|
2501
|
+
self.set_timer(quit_timeout, lambda: setattr(self, "_quit_pending", False))
|
|
2489
2502
|
|
|
2490
2503
|
def action_interrupt(self) -> None:
|
|
2491
2504
|
"""Handle escape key.
|
|
@@ -2733,6 +2746,7 @@ class DeepAgentsApp(App):
|
|
|
2733
2746
|
screen = ModelSelectorScreen(
|
|
2734
2747
|
current_model=settings.model_name,
|
|
2735
2748
|
current_provider=settings.model_provider,
|
|
2749
|
+
cli_profile_override=self._profile_override,
|
|
2736
2750
|
)
|
|
2737
2751
|
self.push_screen(screen, handle_result)
|
|
2738
2752
|
|
|
@@ -2982,7 +2996,11 @@ class DeepAgentsApp(App):
|
|
|
2982
2996
|
return
|
|
2983
2997
|
|
|
2984
2998
|
try:
|
|
2985
|
-
result = create_model(
|
|
2999
|
+
result = create_model(
|
|
3000
|
+
model_spec,
|
|
3001
|
+
extra_kwargs=extra_kwargs,
|
|
3002
|
+
profile_overrides=self._profile_override,
|
|
3003
|
+
)
|
|
2986
3004
|
except ModelConfigError as e:
|
|
2987
3005
|
await self._mount_message(ErrorMessage(str(e)))
|
|
2988
3006
|
return
|
|
@@ -3140,6 +3158,7 @@ async def run_textual_app(
|
|
|
3140
3158
|
sandbox: SandboxBackendProtocol | None = None,
|
|
3141
3159
|
sandbox_type: str | None = None,
|
|
3142
3160
|
mcp_server_info: list[MCPServerInfo] | None = None,
|
|
3161
|
+
profile_override: dict[str, Any] | None = None,
|
|
3143
3162
|
) -> AppResult:
|
|
3144
3163
|
"""Run the Textual application.
|
|
3145
3164
|
|
|
@@ -3156,6 +3175,8 @@ async def run_textual_app(
|
|
|
3156
3175
|
sandbox: Sandbox backend (for model hot-swap)
|
|
3157
3176
|
sandbox_type: Type of sandbox provider (for model hot-swap)
|
|
3158
3177
|
mcp_server_info: MCP server metadata for the `/mcp` viewer.
|
|
3178
|
+
profile_override: Extra profile fields from `--profile-override`,
|
|
3179
|
+
retained for model hot-swap and footer display.
|
|
3159
3180
|
|
|
3160
3181
|
Returns:
|
|
3161
3182
|
An `AppResult` with the return code and final thread ID.
|
|
@@ -3173,6 +3194,7 @@ async def run_textual_app(
|
|
|
3173
3194
|
sandbox=sandbox,
|
|
3174
3195
|
sandbox_type=sandbox_type,
|
|
3175
3196
|
mcp_server_info=mcp_server_info,
|
|
3197
|
+
profile_override=profile_override,
|
|
3176
3198
|
)
|
|
3177
3199
|
await app.run_async()
|
|
3178
3200
|
return AppResult(
|
|
@@ -436,6 +436,12 @@ def parse_args() -> argparse.Namespace:
|
|
|
436
436
|
except Exception:
|
|
437
437
|
logger.warning("Unexpected error looking up SDK version", exc_info=True)
|
|
438
438
|
sdk_version = "unknown"
|
|
439
|
+
parser.add_argument(
|
|
440
|
+
"--acp",
|
|
441
|
+
action="store_true",
|
|
442
|
+
help="Run as an ACP server over stdio instead of launching the Textual UI",
|
|
443
|
+
)
|
|
444
|
+
|
|
439
445
|
parser.add_argument(
|
|
440
446
|
"-v",
|
|
441
447
|
"--version",
|
|
@@ -629,6 +635,7 @@ async def run_textual_cli_async(
|
|
|
629
635
|
sandbox=sandbox_backend,
|
|
630
636
|
sandbox_type=sandbox_type if sandbox_type != "none" else None,
|
|
631
637
|
mcp_server_info=mcp_server_info,
|
|
638
|
+
profile_override=profile_override,
|
|
632
639
|
)
|
|
633
640
|
finally:
|
|
634
641
|
# Clean up MCP session manager if initialized
|
|
@@ -647,6 +654,114 @@ async def run_textual_cli_async(
|
|
|
647
654
|
return result
|
|
648
655
|
|
|
649
656
|
|
|
657
|
+
async def _run_acp_cli_async(
|
|
658
|
+
assistant_id: str,
|
|
659
|
+
*,
|
|
660
|
+
run_acp_agent: Callable[[Any], Any],
|
|
661
|
+
agent_server_cls: type[Any],
|
|
662
|
+
model_name: str | None = None,
|
|
663
|
+
model_params: dict[str, Any] | None = None,
|
|
664
|
+
profile_override: dict[str, Any] | None = None,
|
|
665
|
+
mcp_config_path: str | None = None,
|
|
666
|
+
no_mcp: bool = False,
|
|
667
|
+
trust_project_mcp: bool | None = None,
|
|
668
|
+
) -> int:
|
|
669
|
+
"""Run ACP server mode and return a process exit code.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
assistant_id: Agent identifier to initialize.
|
|
673
|
+
run_acp_agent: ACP server runner function.
|
|
674
|
+
agent_server_cls: ACP server class constructor.
|
|
675
|
+
model_name: Optional model name to use.
|
|
676
|
+
model_params: Extra kwargs from `--model-params` to pass to the model.
|
|
677
|
+
profile_override: Extra profile fields from `--profile-override`.
|
|
678
|
+
mcp_config_path: Optional path to MCP servers JSON configuration file.
|
|
679
|
+
no_mcp: Disable all MCP tool loading.
|
|
680
|
+
trust_project_mcp: Controls project-level stdio server trust.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Exit code for ACP mode.
|
|
684
|
+
"""
|
|
685
|
+
from deepagents_cli.agent import create_cli_agent
|
|
686
|
+
from deepagents_cli.config import create_model, settings
|
|
687
|
+
from deepagents_cli.model_config import ModelConfigError
|
|
688
|
+
from deepagents_cli.tools import fetch_url, http_request, web_search
|
|
689
|
+
|
|
690
|
+
try:
|
|
691
|
+
model_result = create_model(
|
|
692
|
+
model_name,
|
|
693
|
+
extra_kwargs=model_params,
|
|
694
|
+
profile_overrides=profile_override,
|
|
695
|
+
)
|
|
696
|
+
except ModelConfigError as exc:
|
|
697
|
+
sys.stderr.write(f"Error: {exc}\n")
|
|
698
|
+
sys.stderr.flush()
|
|
699
|
+
return 1
|
|
700
|
+
model_result.apply_to_settings()
|
|
701
|
+
|
|
702
|
+
tools: list[Any] = [http_request, fetch_url]
|
|
703
|
+
if settings.has_tavily:
|
|
704
|
+
tools.append(web_search)
|
|
705
|
+
|
|
706
|
+
mcp_session_manager = None
|
|
707
|
+
mcp_server_info = None
|
|
708
|
+
try:
|
|
709
|
+
from deepagents_cli.mcp_tools import resolve_and_load_mcp_tools
|
|
710
|
+
|
|
711
|
+
(
|
|
712
|
+
mcp_tools,
|
|
713
|
+
mcp_session_manager,
|
|
714
|
+
mcp_server_info,
|
|
715
|
+
) = await resolve_and_load_mcp_tools(
|
|
716
|
+
explicit_config_path=mcp_config_path,
|
|
717
|
+
no_mcp=no_mcp,
|
|
718
|
+
trust_project_mcp=trust_project_mcp,
|
|
719
|
+
)
|
|
720
|
+
tools.extend(mcp_tools)
|
|
721
|
+
except FileNotFoundError as exc:
|
|
722
|
+
msg = f"Error: MCP config file not found: {exc}\n"
|
|
723
|
+
sys.stderr.write(msg)
|
|
724
|
+
sys.stderr.flush()
|
|
725
|
+
return 1
|
|
726
|
+
except RuntimeError as exc:
|
|
727
|
+
msg = f"Error: Failed to load MCP tools: {exc}\n"
|
|
728
|
+
sys.stderr.write(msg)
|
|
729
|
+
sys.stderr.flush()
|
|
730
|
+
return 1
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
agent_graph, _backend = create_cli_agent(
|
|
734
|
+
model=model_result.model,
|
|
735
|
+
assistant_id=assistant_id,
|
|
736
|
+
tools=tools,
|
|
737
|
+
mcp_server_info=mcp_server_info,
|
|
738
|
+
)
|
|
739
|
+
except Exception as exc:
|
|
740
|
+
sys.stderr.write(f"Error: failed to create agent: {exc}\n")
|
|
741
|
+
sys.stderr.flush()
|
|
742
|
+
logger.debug("ACP agent creation failed", exc_info=True)
|
|
743
|
+
return 1
|
|
744
|
+
|
|
745
|
+
server = agent_server_cls(agent_graph) # Pregel is a CompiledStateGraph at runtime
|
|
746
|
+
exit_code = 0
|
|
747
|
+
try:
|
|
748
|
+
await run_acp_agent(server)
|
|
749
|
+
except KeyboardInterrupt:
|
|
750
|
+
pass
|
|
751
|
+
except Exception as exc:
|
|
752
|
+
sys.stderr.write(f"Error: ACP server failed: {exc}\n")
|
|
753
|
+
sys.stderr.flush()
|
|
754
|
+
logger.exception("ACP server crashed")
|
|
755
|
+
exit_code = 1
|
|
756
|
+
finally:
|
|
757
|
+
if mcp_session_manager is not None:
|
|
758
|
+
try:
|
|
759
|
+
await mcp_session_manager.cleanup()
|
|
760
|
+
except Exception:
|
|
761
|
+
logger.warning("MCP session cleanup failed", exc_info=True)
|
|
762
|
+
return exit_code
|
|
763
|
+
|
|
764
|
+
|
|
650
765
|
def apply_stdin_pipe(args: argparse.Namespace) -> None:
|
|
651
766
|
r"""Read piped stdin and merge it into the parsed CLI arguments.
|
|
652
767
|
|
|
@@ -888,20 +1003,16 @@ def cli_main() -> None:
|
|
|
888
1003
|
print(f"deepagents-cli {__version__}\ndeepagents (SDK) {sdk_version}") # noqa: T201 # CLI version output
|
|
889
1004
|
sys.exit(0)
|
|
890
1005
|
|
|
891
|
-
#
|
|
892
|
-
|
|
1006
|
+
# ACP mode does not require Textual, so skip UI dependency checks when
|
|
1007
|
+
# the flag is present in raw argv.
|
|
1008
|
+
if "--acp" not in sys.argv[1:]:
|
|
1009
|
+
check_cli_dependencies()
|
|
893
1010
|
|
|
894
1011
|
from deepagents_cli.config import console, settings
|
|
895
1012
|
|
|
896
1013
|
try:
|
|
897
1014
|
args = parse_args()
|
|
898
1015
|
|
|
899
|
-
# Apply shell-allow-list from command line if provided (overrides env var)
|
|
900
|
-
if args.shell_allow_list:
|
|
901
|
-
from deepagents_cli.config import parse_shell_allow_list
|
|
902
|
-
|
|
903
|
-
settings.shell_allow_list = parse_shell_allow_list(args.shell_allow_list)
|
|
904
|
-
|
|
905
1016
|
model_params: dict[str, Any] | None = None
|
|
906
1017
|
raw_kwargs = getattr(args, "model_params", None)
|
|
907
1018
|
if raw_kwargs:
|
|
@@ -936,6 +1047,46 @@ def cli_main() -> None:
|
|
|
936
1047
|
)
|
|
937
1048
|
sys.exit(1)
|
|
938
1049
|
|
|
1050
|
+
if getattr(args, "acp", False):
|
|
1051
|
+
try:
|
|
1052
|
+
from acp import run_agent as run_acp_agent
|
|
1053
|
+
from deepagents_acp.server import AgentServerACP
|
|
1054
|
+
except ImportError as exc:
|
|
1055
|
+
msg = (
|
|
1056
|
+
f"ACP dependencies not available: {exc}\n"
|
|
1057
|
+
"Install with: pip install deepagents-acp\n"
|
|
1058
|
+
)
|
|
1059
|
+
sys.stderr.write(msg)
|
|
1060
|
+
sys.stderr.flush()
|
|
1061
|
+
sys.exit(1)
|
|
1062
|
+
|
|
1063
|
+
if getattr(args, "no_mcp", False) and getattr(args, "mcp_config", None):
|
|
1064
|
+
msg = "Error: --no-mcp and --mcp-config are mutually exclusive\n"
|
|
1065
|
+
sys.stderr.write(msg)
|
|
1066
|
+
sys.stderr.flush()
|
|
1067
|
+
sys.exit(2)
|
|
1068
|
+
|
|
1069
|
+
exit_code = asyncio.run(
|
|
1070
|
+
_run_acp_cli_async(
|
|
1071
|
+
assistant_id=args.agent,
|
|
1072
|
+
run_acp_agent=run_acp_agent,
|
|
1073
|
+
agent_server_cls=AgentServerACP,
|
|
1074
|
+
model_name=getattr(args, "model", None),
|
|
1075
|
+
model_params=model_params,
|
|
1076
|
+
profile_override=profile_override,
|
|
1077
|
+
mcp_config_path=getattr(args, "mcp_config", None),
|
|
1078
|
+
no_mcp=getattr(args, "no_mcp", False),
|
|
1079
|
+
trust_project_mcp=getattr(args, "trust_project_mcp", False),
|
|
1080
|
+
)
|
|
1081
|
+
)
|
|
1082
|
+
sys.exit(exit_code)
|
|
1083
|
+
|
|
1084
|
+
# Apply shell-allow-list from command line if provided (overrides env var)
|
|
1085
|
+
if args.shell_allow_list:
|
|
1086
|
+
from deepagents_cli.config import parse_shell_allow_list
|
|
1087
|
+
|
|
1088
|
+
settings.shell_allow_list = parse_shell_allow_list(args.shell_allow_list)
|
|
1089
|
+
|
|
939
1090
|
apply_stdin_pipe(args)
|
|
940
1091
|
|
|
941
1092
|
if getattr(args, "no_mcp", False) and getattr(args, "mcp_config", None):
|
|
@@ -104,6 +104,20 @@ class ModelSpec:
|
|
|
104
104
|
return f"{self.provider}:{self.model}"
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
class ModelProfileEntry(TypedDict):
|
|
108
|
+
"""Profile data for a model with override tracking."""
|
|
109
|
+
|
|
110
|
+
profile: dict[str, Any]
|
|
111
|
+
"""Merged profile dict (upstream defaults + config.toml overrides).
|
|
112
|
+
|
|
113
|
+
Keys vary by provider (e.g., `max_input_tokens`, `tool_calling`).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
overridden_keys: frozenset[str]
|
|
117
|
+
"""Keys in `profile` whose values came from config.toml rather than the
|
|
118
|
+
upstream provider package."""
|
|
119
|
+
|
|
120
|
+
|
|
107
121
|
class ProviderConfig(TypedDict, total=False):
|
|
108
122
|
"""Configuration for a model provider.
|
|
109
123
|
|
|
@@ -194,6 +208,7 @@ registry fallback.
|
|
|
194
208
|
_available_models_cache: dict[str, list[str]] | None = None
|
|
195
209
|
_builtin_providers_cache: dict[str, Any] | None = None
|
|
196
210
|
_default_config_cache: ModelConfig | None = None
|
|
211
|
+
_profiles_cache: Mapping[str, ModelProfileEntry] | None = None
|
|
197
212
|
|
|
198
213
|
|
|
199
214
|
def clear_caches() -> None:
|
|
@@ -201,10 +216,11 @@ def clear_caches() -> None:
|
|
|
201
216
|
|
|
202
217
|
Intended for tests and for a hypothetical "refresh models" UI action.
|
|
203
218
|
"""
|
|
204
|
-
global _available_models_cache, _builtin_providers_cache, _default_config_cache # noqa: PLW0603 # Module-level caches require global statement
|
|
219
|
+
global _available_models_cache, _builtin_providers_cache, _default_config_cache, _profiles_cache # noqa: PLW0603, E501 # Module-level caches require global statement
|
|
205
220
|
_available_models_cache = None
|
|
206
221
|
_builtin_providers_cache = None
|
|
207
222
|
_default_config_cache = None
|
|
223
|
+
_profiles_cache = None
|
|
208
224
|
|
|
209
225
|
|
|
210
226
|
def _get_builtin_providers() -> dict[str, Any]:
|
|
@@ -384,6 +400,111 @@ def get_available_models() -> dict[str, list[str]]:
|
|
|
384
400
|
return available
|
|
385
401
|
|
|
386
402
|
|
|
403
|
+
def _build_entry(
|
|
404
|
+
base: dict[str, Any],
|
|
405
|
+
overrides: dict[str, Any],
|
|
406
|
+
cli_override: dict[str, Any] | None,
|
|
407
|
+
) -> ModelProfileEntry:
|
|
408
|
+
"""Build a profile entry by merging base, overrides, and CLI override.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
base: Upstream profile dict (empty for config-only models).
|
|
412
|
+
overrides: `config.toml` profile overrides.
|
|
413
|
+
cli_override: Extra fields from `--profile-override`.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Profile entry with merged data and override tracking.
|
|
417
|
+
"""
|
|
418
|
+
merged = {**base, **overrides}
|
|
419
|
+
overridden_keys = set(overrides)
|
|
420
|
+
if cli_override:
|
|
421
|
+
merged = {**merged, **cli_override}
|
|
422
|
+
overridden_keys |= set(cli_override)
|
|
423
|
+
return ModelProfileEntry(
|
|
424
|
+
profile=merged,
|
|
425
|
+
overridden_keys=frozenset(overridden_keys),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def get_model_profiles(
|
|
430
|
+
*,
|
|
431
|
+
cli_override: dict[str, Any] | None = None,
|
|
432
|
+
) -> Mapping[str, ModelProfileEntry]:
|
|
433
|
+
"""Load upstream profiles merged with config.toml overrides.
|
|
434
|
+
|
|
435
|
+
Keyed by `provider:model` spec string. Each entry contains the
|
|
436
|
+
merged profile dict and the set of keys overridden by config.toml.
|
|
437
|
+
|
|
438
|
+
Unlike `get_available_models()`, this includes all models from upstream
|
|
439
|
+
profiles regardless of capability filters (tool calling, text I/O).
|
|
440
|
+
|
|
441
|
+
Results are cached when `cli_override` is None; use `clear_caches()`
|
|
442
|
+
to reset. When `cli_override` is provided the cache is bypassed
|
|
443
|
+
because CLI overrides are session-specific.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
cli_override: Extra profile fields from `--profile-override`.
|
|
447
|
+
|
|
448
|
+
When provided, these are merged on top of every profile entry
|
|
449
|
+
(after upstream + config.toml) and their keys are added to
|
|
450
|
+
`overridden_keys`.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Read-only mapping of spec strings to profile entries.
|
|
454
|
+
"""
|
|
455
|
+
global _profiles_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
456
|
+
if cli_override is None and _profiles_cache is not None:
|
|
457
|
+
return _profiles_cache
|
|
458
|
+
|
|
459
|
+
result: dict[str, ModelProfileEntry] = {}
|
|
460
|
+
config = ModelConfig.load()
|
|
461
|
+
|
|
462
|
+
# Collect upstream profiles from provider packages.
|
|
463
|
+
seen_specs: set[str] = set()
|
|
464
|
+
provider_modules = _get_provider_profile_modules()
|
|
465
|
+
for provider, module_path in provider_modules:
|
|
466
|
+
try:
|
|
467
|
+
profiles = _load_provider_profiles(module_path)
|
|
468
|
+
except ImportError:
|
|
469
|
+
logger.debug(
|
|
470
|
+
"Could not import profiles from %s for provider '%s'",
|
|
471
|
+
module_path,
|
|
472
|
+
provider,
|
|
473
|
+
)
|
|
474
|
+
continue
|
|
475
|
+
except Exception:
|
|
476
|
+
logger.warning(
|
|
477
|
+
"Failed to load profiles from %s for provider '%s'",
|
|
478
|
+
module_path,
|
|
479
|
+
provider,
|
|
480
|
+
exc_info=True,
|
|
481
|
+
)
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
for model_name, upstream_profile in profiles.items():
|
|
485
|
+
spec = f"{provider}:{model_name}"
|
|
486
|
+
seen_specs.add(spec)
|
|
487
|
+
overrides = config.get_profile_overrides(provider, model_name=model_name)
|
|
488
|
+
result[spec] = _build_entry(upstream_profile, overrides, cli_override)
|
|
489
|
+
|
|
490
|
+
# Add config-only models that have no upstream profile.
|
|
491
|
+
for provider_name, provider_config in config.providers.items():
|
|
492
|
+
config_models = provider_config.get("models", [])
|
|
493
|
+
for model_name in config_models:
|
|
494
|
+
spec = f"{provider_name}:{model_name}"
|
|
495
|
+
if spec not in seen_specs:
|
|
496
|
+
overrides = config.get_profile_overrides(
|
|
497
|
+
provider_name, model_name=model_name
|
|
498
|
+
)
|
|
499
|
+
result[spec] = _build_entry({}, overrides, cli_override)
|
|
500
|
+
|
|
501
|
+
frozen = MappingProxyType(result)
|
|
502
|
+
# Only populate cache for the static (no CLI override) path.
|
|
503
|
+
if cli_override is None:
|
|
504
|
+
_profiles_cache = frozen
|
|
505
|
+
return frozen
|
|
506
|
+
|
|
507
|
+
|
|
387
508
|
def _is_langchain_supported_provider(provider: str) -> bool:
|
|
388
509
|
"""Check if a provider is in langchain's built-in provider registry.
|
|
389
510
|
|
|
@@ -114,6 +114,7 @@ def show_help() -> None:
|
|
|
114
114
|
)
|
|
115
115
|
console.print(" --default-model [MODEL] Set, show, or manage the default model")
|
|
116
116
|
console.print(" --clear-default-model Clear the default model")
|
|
117
|
+
console.print(" --acp Run as an ACP server over stdio")
|
|
117
118
|
console.print(" -v, --version Show deepagents CLI and SDK versions")
|
|
118
119
|
console.print(" -h, --help Show this help message and exit")
|
|
119
120
|
console.print()
|
|
@@ -1483,8 +1483,6 @@ class ChatInput(Vertical):
|
|
|
1483
1483
|
if self._completion_manager:
|
|
1484
1484
|
self._completion_manager.reset()
|
|
1485
1485
|
self.clear_completion_suggestions()
|
|
1486
|
-
if self._text_area:
|
|
1487
|
-
self._text_area.clear()
|
|
1488
1486
|
return True
|
|
1489
1487
|
|
|
1490
1488
|
def dismiss_completion(self) -> bool:
|
|
@@ -37,6 +37,46 @@ if TYPE_CHECKING:
|
|
|
37
37
|
logger = logging.getLogger(__name__)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def _show_timestamp_toast(widget: Static | Vertical) -> None:
|
|
41
|
+
"""Show a toast with the message's creation timestamp.
|
|
42
|
+
|
|
43
|
+
No-ops silently if the widget is not mounted or has no associated message
|
|
44
|
+
data in the store.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
widget: The message widget whose timestamp to display.
|
|
48
|
+
"""
|
|
49
|
+
from datetime import UTC, datetime
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
app = widget.app
|
|
53
|
+
except Exception: # noqa: BLE001 # Textual raises when widget has no app
|
|
54
|
+
return
|
|
55
|
+
if not widget.id:
|
|
56
|
+
return
|
|
57
|
+
store = app._message_store # type: ignore[attr-defined]
|
|
58
|
+
data = store.get_message(widget.id)
|
|
59
|
+
if not data:
|
|
60
|
+
return
|
|
61
|
+
dt = datetime.fromtimestamp(data.timestamp, tz=UTC).astimezone()
|
|
62
|
+
label = f"{dt:%b} {dt.day}, {dt.hour % 12 or 12}:{dt:%M:%S} {dt:%p}"
|
|
63
|
+
app.notify(label, timeout=3)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _TimestampClickMixin:
|
|
67
|
+
"""Mixin that shows a timestamp toast on click.
|
|
68
|
+
|
|
69
|
+
Add to any message widget that should display its creation timestamp when
|
|
70
|
+
clicked. Widgets needing additional click behavior (e.g. `ToolCallMessage`,
|
|
71
|
+
`AppMessage`) should override `on_click` and call `_show_timestamp_toast`
|
|
72
|
+
directly instead.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def on_click(self, event: Click) -> None: # noqa: ARG002 # Textual event handler
|
|
76
|
+
"""Show timestamp toast on click."""
|
|
77
|
+
_show_timestamp_toast(self) # type: ignore[arg-type]
|
|
78
|
+
|
|
79
|
+
|
|
40
80
|
def _mode_color(mode: str | None) -> str:
|
|
41
81
|
"""Return the color string for a mode, falling back to primary.
|
|
42
82
|
|
|
@@ -101,7 +141,7 @@ _TOOLS_WITH_HEADER_INFO: set[str] = {
|
|
|
101
141
|
}
|
|
102
142
|
|
|
103
143
|
|
|
104
|
-
class UserMessage(Static):
|
|
144
|
+
class UserMessage(_TimestampClickMixin, Static):
|
|
105
145
|
"""Widget displaying a user message."""
|
|
106
146
|
|
|
107
147
|
DEFAULT_CSS = """
|
|
@@ -231,7 +271,7 @@ class QueuedUserMessage(Static):
|
|
|
231
271
|
yield Static(text)
|
|
232
272
|
|
|
233
273
|
|
|
234
|
-
class AssistantMessage(Vertical):
|
|
274
|
+
class AssistantMessage(_TimestampClickMixin, Vertical):
|
|
235
275
|
"""Widget displaying an assistant message with markdown support.
|
|
236
276
|
|
|
237
277
|
Uses MarkdownStream for smoother streaming instead of re-rendering
|
|
@@ -652,9 +692,12 @@ class ToolCallMessage(Vertical):
|
|
|
652
692
|
self._update_output_display()
|
|
653
693
|
|
|
654
694
|
def on_click(self, event: Click) -> None:
|
|
655
|
-
"""
|
|
695
|
+
"""Toggle output expansion, or show timestamp if no output."""
|
|
656
696
|
event.stop() # Prevent click from bubbling up and scrolling
|
|
657
|
-
self.
|
|
697
|
+
if self._output:
|
|
698
|
+
self.toggle_output()
|
|
699
|
+
else:
|
|
700
|
+
_show_timestamp_toast(self)
|
|
658
701
|
|
|
659
702
|
def _format_output(
|
|
660
703
|
self, output: str, *, is_preview: bool = False
|
|
@@ -1168,7 +1211,7 @@ class ToolCallMessage(Vertical):
|
|
|
1168
1211
|
return filtered
|
|
1169
1212
|
|
|
1170
1213
|
|
|
1171
|
-
class DiffMessage(Static):
|
|
1214
|
+
class DiffMessage(_TimestampClickMixin, Static):
|
|
1172
1215
|
"""Widget displaying a diff with syntax highlighting."""
|
|
1173
1216
|
|
|
1174
1217
|
DEFAULT_CSS = """
|
|
@@ -1239,7 +1282,7 @@ class DiffMessage(Static):
|
|
|
1239
1282
|
self.styles.border = ("ascii", "cyan")
|
|
1240
1283
|
|
|
1241
1284
|
|
|
1242
|
-
class ErrorMessage(Static):
|
|
1285
|
+
class ErrorMessage(_TimestampClickMixin, Static):
|
|
1243
1286
|
"""Widget displaying an error message."""
|
|
1244
1287
|
|
|
1245
1288
|
DEFAULT_CSS = """
|
|
@@ -1307,9 +1350,10 @@ class AppMessage(Static):
|
|
|
1307
1350
|
)
|
|
1308
1351
|
super().__init__(content, **kwargs)
|
|
1309
1352
|
|
|
1310
|
-
def on_click(self, event: Click) -> None:
|
|
1311
|
-
"""Open Rich-style hyperlinks on single click."""
|
|
1353
|
+
def on_click(self, event: Click) -> None:
|
|
1354
|
+
"""Open Rich-style hyperlinks on single click and show timestamp."""
|
|
1312
1355
|
open_style_link(event)
|
|
1356
|
+
_show_timestamp_toast(self)
|
|
1313
1357
|
|
|
1314
1358
|
|
|
1315
1359
|
class SummarizationMessage(AppMessage):
|