langchain-agentx-cli 0.1.4__tar.gz → 0.2.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.
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/PKG-INFO +1 -1
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/__init__.py +1 -1
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/app.py +3 -1
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/bridge/session_bridge.py +14 -7
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/cli.py +2 -2
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/screens/repl.py +23 -11
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/themes.py +5 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/clipboard.py +47 -13
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/message_selection.py +8 -7
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/__init__.py +4 -0
- langchain_agentx_cli-0.2.0/langchain_agentx_cli/tui/permissions/component_manager.py +148 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/consumer.py +34 -6
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/dialog.py +7 -2
- langchain_agentx_cli-0.2.0/langchain_agentx_cli/tui/permissions/queue.py +170 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/message_events.py +9 -4
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/message_list.py +22 -39
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/messages/__init__.py +2 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/messages/thinking_message.py +7 -8
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/pending_permission.py +3 -3
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/permission_inline.py +29 -23
- langchain_agentx_cli-0.2.0/langchain_agentx_cli/widgets/permission_keybindings.py +174 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/spinner.py +9 -1
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/base.py +11 -8
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli.egg-info/PKG-INFO +1 -1
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli.egg-info/SOURCES.txt +3 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/pyproject.toml +1 -1
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/LICENSE +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/README.md +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/__main__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/bootstrap.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/bridge/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/builtin/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/builtin/clear.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/builtin/compact.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/builtin/help.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/builtin/model.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/builtin/quit.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/builtin/theme.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/parser.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/providers/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/providers/sdk_commands.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/commands/registry.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/completion/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/completion/base.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/completion/command_source.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/completion/history_source.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/config.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/history/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/history/store.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/keybindings/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/keybindings/bindings.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/llm_config.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/permissions/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/permissions/factory.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/permissions/policy.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/prompts/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/prompts/assembler.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/prompts/sections.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/prompts/system_prompt.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/screens/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/session_factory.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/session_options.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/state/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/state/task_store.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/colors.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/detection.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/manager.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/settings.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/system_theme.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/watcher.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tools/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tools/registry.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/cache.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/config.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/inject.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/models.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/presenters/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/presenters/base.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/presenters/bash.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/presenters/fallback.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/permissions/presenters/file.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/safe_screen.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/welcome.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/compact_progress.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/completion_overlay.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/input_area.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/messages/assistant_message.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/messages/error_message.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/messages/rendering.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/task_panel.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/agent/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/agent/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/ask_user_question/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/ask_user_question/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/bash/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/bash/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/batch_edit/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/batch_edit/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/edit/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/edit/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/glob/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/glob/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/grep/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/grep/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/helpers.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/read/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/read/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/skill/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/skill/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/user_message/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/user_message/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/webfetch/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/webfetch/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/websearch/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/websearch/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/write/__init__.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/widgets/tools/write/widget.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli.egg-info/dependency_links.txt +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli.egg-info/entry_points.txt +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli.egg-info/not-zip-safe +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli.egg-info/requires.txt +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli.egg-info/top_level.txt +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/setup.cfg +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/tests/test_app_interrupt.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/tests/test_repl_commands.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/tests/test_repl_submit_flow.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/tests/test_repl_ui.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/tests/test_smoke.py +0 -0
- {langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/tests/test_welcome.py +0 -0
|
@@ -123,7 +123,9 @@ class ReplApp(App[None]):
|
|
|
123
123
|
self._exit_armed = False
|
|
124
124
|
|
|
125
125
|
def action_cancel_stream(self) -> None:
|
|
126
|
-
|
|
126
|
+
result = try_copy_message_selection(self)
|
|
127
|
+
if result is not None:
|
|
128
|
+
# 有选区被复制(无论成功与否),不执行后续操作
|
|
127
129
|
return
|
|
128
130
|
# Dismiss inline permission widget if active
|
|
129
131
|
try:
|
|
@@ -8,7 +8,7 @@ bridge/session_bridge.py — SessionBridge(SDK 事件 → Textual Message)
|
|
|
8
8
|
链路位置:
|
|
9
9
|
ReplApp → SessionBridge → AgentSession.stream_loop_events
|
|
10
10
|
→ LangGraphToLangchainAgentEventAdapter.adapt();
|
|
11
|
-
权限:AgentSessionPermissionResolver ← PermissionQueueConsumer。
|
|
11
|
+
权限:AgentSessionPermissionResolver ← PermissionQueueConsumer ← ConfirmQueue。
|
|
12
12
|
|
|
13
13
|
当前裁剪范围:
|
|
14
14
|
不解析 LangGraph 原始事件名;Resolver 超时 300s(见 tui/permissions/config.py)。
|
|
@@ -31,6 +31,7 @@ from langchain_agentx_cli.tui.permissions import (
|
|
|
31
31
|
)
|
|
32
32
|
from langchain_agentx_cli.tui.permissions.config import DEFAULT_PERMISSION_TIMEOUT_SECONDS
|
|
33
33
|
from langchain_agentx_cli.tui.permissions.dialog import DialogPermissionDecisionPort
|
|
34
|
+
from langchain_agentx_cli.tui.permissions.queue import ConfirmQueue
|
|
34
35
|
|
|
35
36
|
from langchain_agentx import (
|
|
36
37
|
LangGraphToLangchainAgentEventAdapter,
|
|
@@ -41,6 +42,7 @@ from langchain_agentx import (
|
|
|
41
42
|
from langchain_agentx_cli.widgets.messages import (
|
|
42
43
|
AssistantMessageChunk,
|
|
43
44
|
ContextCompactedMessage,
|
|
45
|
+
ContextCompactingMessage,
|
|
44
46
|
ErrorMessageOccurred,
|
|
45
47
|
ThinkingContentDelta,
|
|
46
48
|
ThinkingEnded,
|
|
@@ -78,8 +80,10 @@ class SessionBridge:
|
|
|
78
80
|
self._adapter = LangGraphToLangchainAgentEventAdapter(config=dict(MVP_ADAPTER_CONFIG))
|
|
79
81
|
self._current_worker: Any = None
|
|
80
82
|
self._streamed_text = False
|
|
83
|
+
# 对齐 CC: 集中式权限队列状态管理
|
|
81
84
|
self._permission_queue: asyncio.Queue[PermissionRequest] = asyncio.Queue()
|
|
82
85
|
self._permission_cache = SessionPermissionCache()
|
|
86
|
+
self._confirm_queue = ConfirmQueue(max_size=10) # 对齐 CC confirmQueue
|
|
83
87
|
self._permission_resolver = AgentSessionPermissionResolver(
|
|
84
88
|
request_queue=self._permission_queue,
|
|
85
89
|
timeout=DEFAULT_PERMISSION_TIMEOUT_SECONDS,
|
|
@@ -88,6 +92,7 @@ class SessionBridge:
|
|
|
88
92
|
self._permission_consumer = PermissionQueueConsumer(
|
|
89
93
|
queue=self._permission_queue,
|
|
90
94
|
cache=self._permission_cache,
|
|
95
|
+
confirm_queue=self._confirm_queue, # 传入 confirm_queue
|
|
91
96
|
app=app,
|
|
92
97
|
decision_port=DialogPermissionDecisionPort(app),
|
|
93
98
|
)
|
|
@@ -186,12 +191,9 @@ class SessionBridge:
|
|
|
186
191
|
self._emit_finish_answer(str(data.get("answer", "")))
|
|
187
192
|
self._post_ui(TurnInProgress(in_progress=False))
|
|
188
193
|
case LangchainAgentEventType.COMPACT_END:
|
|
189
|
-
self._post_ui(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
tokens_freed=int(data.get("tokens_freed", 0)),
|
|
193
|
-
)
|
|
194
|
-
)
|
|
194
|
+
self._post_ui(ContextCompactedMessage())
|
|
195
|
+
case LangchainAgentEventType.COMPACT_START:
|
|
196
|
+
self._post_ui(ContextCompactingMessage())
|
|
195
197
|
case _:
|
|
196
198
|
pass
|
|
197
199
|
|
|
@@ -226,6 +228,11 @@ class SessionBridge:
|
|
|
226
228
|
def permission_cache(self) -> SessionPermissionCache:
|
|
227
229
|
return self._permission_cache
|
|
228
230
|
|
|
231
|
+
@property
|
|
232
|
+
def confirm_queue(self) -> ConfirmQueue:
|
|
233
|
+
"""对齐 CC: confirmQueue 状态访问。"""
|
|
234
|
+
return self._confirm_queue
|
|
235
|
+
|
|
229
236
|
@property
|
|
230
237
|
def permission_resolver(self) -> AgentSessionPermissionResolver:
|
|
231
238
|
return self._permission_resolver
|
|
@@ -17,8 +17,8 @@ from pathlib import Path
|
|
|
17
17
|
|
|
18
18
|
import click
|
|
19
19
|
|
|
20
|
-
# 过滤 LangGraph 的
|
|
21
|
-
warnings.filterwarnings("ignore",
|
|
20
|
+
# 过滤 LangGraph 的 allowed_objects 警告(避免污染启动画面)
|
|
21
|
+
warnings.filterwarnings("ignore", message=".*allowed_objects.*")
|
|
22
22
|
|
|
23
23
|
from langchain_agentx_cli.app import ReplApp
|
|
24
24
|
from langchain_agentx_cli.bootstrap import CliEnvironmentError, EnvironmentChecker
|
{langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/screens/repl.py
RENAMED
|
@@ -29,6 +29,7 @@ from langchain_agentx_cli.widgets.message_list import MessageListWidget
|
|
|
29
29
|
from langchain_agentx_cli.widgets.messages import (
|
|
30
30
|
AssistantMessageChunk,
|
|
31
31
|
ContextCompactedMessage,
|
|
32
|
+
ContextCompactingMessage,
|
|
32
33
|
ErrorMessageOccurred,
|
|
33
34
|
InfoMessage,
|
|
34
35
|
PermissionPendingHidden,
|
|
@@ -130,13 +131,17 @@ class ReplScreen(Screen):
|
|
|
130
131
|
self._message_list().append_info(content)
|
|
131
132
|
|
|
132
133
|
def _copy_message_selection_if_any(self) -> bool:
|
|
133
|
-
|
|
134
|
-
if
|
|
134
|
+
result = try_copy_message_selection(self.app)
|
|
135
|
+
if result is None:
|
|
135
136
|
return False
|
|
137
|
+
copied, failure_reason = result
|
|
138
|
+
# 根据是否失败选择不同的 severity 和 timeout
|
|
139
|
+
severity = "warning" if failure_reason else "information"
|
|
140
|
+
timeout = 5 if failure_reason else 2
|
|
136
141
|
self.notify(
|
|
137
|
-
format_copied_toast(len(copied)),
|
|
138
|
-
severity=
|
|
139
|
-
timeout=
|
|
142
|
+
format_copied_toast(len(copied), failure_reason),
|
|
143
|
+
severity=severity,
|
|
144
|
+
timeout=timeout,
|
|
140
145
|
)
|
|
141
146
|
return True
|
|
142
147
|
|
|
@@ -236,12 +241,13 @@ class ReplScreen(Screen):
|
|
|
236
241
|
def on_spinner_completion(self, event: SpinnerCompletion) -> None:
|
|
237
242
|
self._message_list().append_spinner_status(event.content)
|
|
238
243
|
|
|
244
|
+
@on(ContextCompactingMessage)
|
|
245
|
+
def on_context_compacting(self, _event: ContextCompactingMessage) -> None:
|
|
246
|
+
self._message_list().spinner_enter_compacting()
|
|
247
|
+
|
|
239
248
|
@on(ContextCompactedMessage)
|
|
240
|
-
def on_context_compacted(self,
|
|
241
|
-
self._message_list().append_context_compacted(
|
|
242
|
-
summary=event.summary,
|
|
243
|
-
tokens_freed=event.tokens_freed,
|
|
244
|
-
)
|
|
249
|
+
def on_context_compacted(self, _event: ContextCompactedMessage) -> None:
|
|
250
|
+
self._message_list().append_context_compacted()
|
|
245
251
|
|
|
246
252
|
@on(ThinkingStarted)
|
|
247
253
|
def on_thinking_started(self, _event: ThinkingStarted) -> None:
|
|
@@ -265,10 +271,16 @@ class ReplScreen(Screen):
|
|
|
265
271
|
|
|
266
272
|
@on(PermissionPendingHidden)
|
|
267
273
|
def on_permission_pending_hidden(self, _event: PermissionPendingHidden) -> None:
|
|
274
|
+
"""权限确认隐藏后的清理。
|
|
275
|
+
|
|
276
|
+
修复:不立即抢回焦点,让用户自然操作或由下一个UI元素控制焦点。
|
|
277
|
+
如果输入区当前无焦点且无其他元素有焦点,再考虑恢复输入区焦点。
|
|
278
|
+
"""
|
|
268
279
|
self._message_list().hide_permission_pending()
|
|
269
280
|
input_area = self.query_one("#input-area", InputAreaWidget)
|
|
270
281
|
input_area.set_permission_pending(False)
|
|
271
|
-
|
|
282
|
+
# 不立即抢回焦点,避免打断用户交互流
|
|
283
|
+
# 焦点会在自然状态下返回到输入区(用户按键时)
|
|
272
284
|
|
|
273
285
|
# D10: Compact 进度条事件处理
|
|
274
286
|
|
{langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/theme/themes.py
RENAMED
|
@@ -39,6 +39,10 @@ def _build_rgb_theme(
|
|
|
39
39
|
palette: dict[str, str],
|
|
40
40
|
dark: bool,
|
|
41
41
|
) -> Theme:
|
|
42
|
+
variables = {
|
|
43
|
+
"screen-selection-background": "#264F78" if dark else "#B4D5FF",
|
|
44
|
+
"screen-selection-foreground": rgb_to_hex(palette["text"]),
|
|
45
|
+
}
|
|
42
46
|
return Theme(
|
|
43
47
|
name=name,
|
|
44
48
|
primary=rgb_to_hex(palette["claude"]),
|
|
@@ -52,6 +56,7 @@ def _build_rgb_theme(
|
|
|
52
56
|
surface=rgb_to_hex(palette["userMessageBackground"]),
|
|
53
57
|
panel=rgb_to_hex(palette["panel"]),
|
|
54
58
|
dark=dark,
|
|
59
|
+
variables=variables,
|
|
55
60
|
)
|
|
56
61
|
|
|
57
62
|
|
{langchain_agentx_cli-0.1.4 → langchain_agentx_cli-0.2.0}/langchain_agentx_cli/tui/clipboard.py
RENAMED
|
@@ -51,10 +51,11 @@ def _wrap_for_multiplexer(sequence: str) -> str:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def _write_osc52_to_tty(sequence: str) -> bool:
|
|
54
|
-
"""
|
|
54
|
+
"""直接写入终端 fd,对齐 CC 写 stdout 的行为。
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
CC (Ink) 通过 process.stdout.write(raw) 发送 OSC 52。
|
|
57
|
+
Textual 接管了 sys.stdout 但底层 fd 1 仍连接终端。
|
|
58
|
+
直接用 os.write(1, ...) 绕过 Python IO 层。
|
|
58
59
|
|
|
59
60
|
Args:
|
|
60
61
|
sequence: OSC 52 序列(已包装 tmux/screen)
|
|
@@ -62,23 +63,31 @@ def _write_osc52_to_tty(sequence: str) -> bool:
|
|
|
62
63
|
Returns:
|
|
63
64
|
是否成功写入
|
|
64
65
|
"""
|
|
65
|
-
|
|
66
|
+
encoded = sequence.encode("utf-8")
|
|
67
|
+
|
|
68
|
+
# 对齐 CC:写 stdout fd(网页终端通常只监听 stdout)
|
|
69
|
+
try:
|
|
70
|
+
os.write(1, encoded)
|
|
71
|
+
return True
|
|
72
|
+
except OSError:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# 回退:/dev/tty
|
|
66
76
|
try:
|
|
67
77
|
fd = os.open("/dev/tty", os.O_WRONLY)
|
|
68
78
|
try:
|
|
69
|
-
os.write(fd,
|
|
79
|
+
os.write(fd, encoded)
|
|
70
80
|
return True
|
|
71
81
|
finally:
|
|
72
82
|
os.close(fd)
|
|
73
83
|
except OSError:
|
|
74
84
|
pass
|
|
75
85
|
|
|
76
|
-
#
|
|
86
|
+
# 最后回退:stderr
|
|
77
87
|
try:
|
|
78
|
-
|
|
79
|
-
sys.stderr.flush()
|
|
88
|
+
os.write(2, encoded)
|
|
80
89
|
return True
|
|
81
|
-
except
|
|
90
|
+
except OSError:
|
|
82
91
|
return False
|
|
83
92
|
|
|
84
93
|
|
|
@@ -172,7 +181,7 @@ def _copy_native(text: str) -> bool:
|
|
|
172
181
|
return False
|
|
173
182
|
|
|
174
183
|
|
|
175
|
-
def copy_text(text: str) -> bool:
|
|
184
|
+
def copy_text(text: str) -> tuple[bool, str | None]:
|
|
176
185
|
"""
|
|
177
186
|
复制文本到剪贴板(对齐 CC setClipboard)。
|
|
178
187
|
|
|
@@ -184,10 +193,10 @@ def copy_text(text: str) -> bool:
|
|
|
184
193
|
text: 要复制的文本
|
|
185
194
|
|
|
186
195
|
Returns:
|
|
187
|
-
|
|
196
|
+
(是否成功, 失败原因描述) - 成功时第二个值为 None
|
|
188
197
|
"""
|
|
189
198
|
if not text:
|
|
190
|
-
return False
|
|
199
|
+
return False, "无文本内容"
|
|
191
200
|
|
|
192
201
|
# 1. 优先尝试 native copy(fire-and-forget)
|
|
193
202
|
native_ok = _copy_native(text)
|
|
@@ -199,4 +208,29 @@ def copy_text(text: str) -> bool:
|
|
|
199
208
|
|
|
200
209
|
# 直接写入 TTY,绕过 Textual 的 stdout 接管
|
|
201
210
|
osc52_ok = _write_osc52_to_tty(wrapped_seq)
|
|
202
|
-
|
|
211
|
+
|
|
212
|
+
if osc52_ok or native_ok:
|
|
213
|
+
return True, None
|
|
214
|
+
|
|
215
|
+
# 诊断失败原因
|
|
216
|
+
reasons = []
|
|
217
|
+
if platform.system().lower() == "linux":
|
|
218
|
+
if os.environ.get("SSH_CONNECTION"):
|
|
219
|
+
reasons.append("SSH 环境无法使用 xclip/xsel")
|
|
220
|
+
elif not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
|
|
221
|
+
reasons.append("无 DISPLAY/WAYLAND_DISPLAY 环境变量")
|
|
222
|
+
else:
|
|
223
|
+
cmd = _detect_linux_copy_command()
|
|
224
|
+
if cmd:
|
|
225
|
+
reasons.append(f"{cmd} 存在但调用失败")
|
|
226
|
+
else:
|
|
227
|
+
reasons.append("未安装 xclip/wl-copy/xsel(sudo yum install xclip)")
|
|
228
|
+
|
|
229
|
+
if os.environ.get("TMUX"):
|
|
230
|
+
reasons.append("tmux 环境可能不支持 OSC 52")
|
|
231
|
+
elif os.environ.get("STY"):
|
|
232
|
+
reasons.append("screen 环境可能不支持 OSC 52")
|
|
233
|
+
else:
|
|
234
|
+
reasons.append("终端可能不支持 OSC 52")
|
|
235
|
+
|
|
236
|
+
return False, ",".join(reasons)
|
|
@@ -46,8 +46,8 @@ def message_list_has_selection(screen: Screen) -> bool:
|
|
|
46
46
|
return any(selection_in_message_list(w) for w in selections if w.is_attached)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def try_copy_message_selection(app: App) -> str | None:
|
|
50
|
-
"""
|
|
49
|
+
def try_copy_message_selection(app: App) -> tuple[str, str | None] | None:
|
|
50
|
+
"""若消息区有选区则复制到剪贴板,返回 (已复制文本, 失败原因)。"""
|
|
51
51
|
try:
|
|
52
52
|
screen = app.screen
|
|
53
53
|
except ScreenStackError:
|
|
@@ -58,11 +58,12 @@ def try_copy_message_selection(app: App) -> str | None:
|
|
|
58
58
|
if not selected or not selected.strip():
|
|
59
59
|
return None
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
copy_text(selected)
|
|
61
|
+
# 对齐 CC:OSC 52 写 stdout + native copy 双重保险
|
|
62
|
+
ok, reason = copy_text(selected)
|
|
63
|
+
return (selected, reason)
|
|
63
64
|
|
|
64
|
-
return selected
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
def format_copied_toast(char_count: int, failure_reason: str | None = None) -> str:
|
|
67
|
+
if failure_reason:
|
|
68
|
+
return f"复制失败:{failure_reason}({char_count} 个字符)"
|
|
68
69
|
return f"已复制 {char_count} 个字符到剪贴板"
|
|
@@ -3,6 +3,7 @@ tui/permissions — 权限确认子系统(Queue + Future)。
|
|
|
3
3
|
|
|
4
4
|
职责:
|
|
5
5
|
消费 SDK AgentSessionPermissionResolver 队列,会话缓存,驱动用户确认(内联 Widget)。
|
|
6
|
+
集成 ConfirmQueue 状态管理(对齐 CC)。
|
|
6
7
|
|
|
7
8
|
链路位置:
|
|
8
9
|
SessionBridge 创建 Queue/Resolver → PermissionQueueConsumer → SDK Future。
|
|
@@ -24,15 +25,18 @@ from langchain_agentx_cli.tui.permissions.presenters import (
|
|
|
24
25
|
PermissionPresentation,
|
|
25
26
|
permission_presenter_for_tool,
|
|
26
27
|
)
|
|
28
|
+
from langchain_agentx_cli.tui.permissions.queue import ConfirmQueue, QueuedPermission
|
|
27
29
|
|
|
28
30
|
__all__ = [
|
|
29
31
|
"DEFAULT_PERMISSION_TIMEOUT_SECONDS",
|
|
32
|
+
"ConfirmQueue",
|
|
30
33
|
"DialogPermissionDecisionPort",
|
|
31
34
|
"NoDialogPermissionDecisionPort",
|
|
32
35
|
"PermissionDecision",
|
|
33
36
|
"PermissionDecisionPort",
|
|
34
37
|
"PermissionPresentation",
|
|
35
38
|
"PermissionQueueConsumer",
|
|
39
|
+
"QueuedPermission",
|
|
36
40
|
"SessionPermissionCache",
|
|
37
41
|
"inject_permission_resolver",
|
|
38
42
|
"permission_presenter_for_tool",
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tui/permissions/component_manager.py — 权限组件生命周期管理(对齐 CC)。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
管理权限 Widget 的挂载、卸载和焦点转移。
|
|
6
|
+
确保组件完全准备好后再显示,对齐 CC 的 Portal 和 autoFocus 行为。
|
|
7
|
+
|
|
8
|
+
链路位置:
|
|
9
|
+
ConfirmQueue → PermissionComponentManager → MessageListWidget。
|
|
10
|
+
|
|
11
|
+
对齐 CC:
|
|
12
|
+
- useEffect 自动处理焦点
|
|
13
|
+
- Portal 组件的挂载时机
|
|
14
|
+
- 组件卸载时的清理
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import logging
|
|
21
|
+
from typing import TYPE_CHECKING, Callable
|
|
22
|
+
|
|
23
|
+
from textual.timer import Timer
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from langchain_agentx_cli.tui.permissions.queue import ConfirmQueue, QueuedPermission
|
|
27
|
+
from langchain_agentx_cli.widgets.message_list import MessageListWidget
|
|
28
|
+
from langchain_agentx_cli.widgets.permission_inline import InlinePermissionWidget
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PermissionComponentManager:
|
|
34
|
+
"""权限组件生命周期管理器(对齐 CC 组件管理)。
|
|
35
|
+
|
|
36
|
+
CC 对照:
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (confirm && ref.current) {
|
|
39
|
+
ref.current.focus();
|
|
40
|
+
}
|
|
41
|
+
}, [confirm]);
|
|
42
|
+
|
|
43
|
+
提供类似功能:
|
|
44
|
+
- mount_widget() - 挂载并聚焦
|
|
45
|
+
- unmount_widget() - 卸载并清理
|
|
46
|
+
- ensure_focused() - 确保获得焦点
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, queue: ConfirmQueue, message_list: MessageListWidget) -> None:
|
|
50
|
+
self._queue = queue
|
|
51
|
+
self._message_list = message_list
|
|
52
|
+
self._focus_retry_timer: Timer | None = None
|
|
53
|
+
self._max_focus_retries = 5
|
|
54
|
+
|
|
55
|
+
async def mount_and_focus(
|
|
56
|
+
self,
|
|
57
|
+
queued: QueuedPermission,
|
|
58
|
+
widget: InlinePermissionWidget,
|
|
59
|
+
) -> bool:
|
|
60
|
+
"""挂载 Widget 并确保获得焦点(对齐 CC autoFocus)。
|
|
61
|
+
|
|
62
|
+
返回是否成功挂载并获得焦点。
|
|
63
|
+
"""
|
|
64
|
+
# 先挂载 widget
|
|
65
|
+
self._message_list.mount(widget)
|
|
66
|
+
queued.widget = widget
|
|
67
|
+
self._queue.set_active(queued.queue_id, widget)
|
|
68
|
+
|
|
69
|
+
# 滚动到底部
|
|
70
|
+
self._message_list._scroll_to_end(force=True)
|
|
71
|
+
|
|
72
|
+
# 尝试获取焦点
|
|
73
|
+
return await self._ensure_widget_focused(widget, retry=True)
|
|
74
|
+
|
|
75
|
+
async def _ensure_widget_focused(
|
|
76
|
+
self,
|
|
77
|
+
widget: InlinePermissionWidget,
|
|
78
|
+
retry: bool = False,
|
|
79
|
+
) -> bool:
|
|
80
|
+
"""确保 Widget 获得焦点(支持重试)。"""
|
|
81
|
+
retries = 0
|
|
82
|
+
max_retries = self._max_focus_retries if retry else 1
|
|
83
|
+
|
|
84
|
+
while retries < max_retries:
|
|
85
|
+
# 检查 widget 是否仍然 attached
|
|
86
|
+
if not widget.is_attached:
|
|
87
|
+
logger.warning("Widget detached, cannot focus")
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# 尝试获取焦点
|
|
91
|
+
widget.focus()
|
|
92
|
+
|
|
93
|
+
# 等待一帧让焦点状态更新
|
|
94
|
+
await asyncio.sleep(0.01)
|
|
95
|
+
|
|
96
|
+
# 验证是否成功获得焦点
|
|
97
|
+
if widget.has_focus:
|
|
98
|
+
logger.debug(f"Widget {widget.id} gained focus (attempt {retries + 1})")
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
retries += 1
|
|
102
|
+
if retries < max_retries:
|
|
103
|
+
await asyncio.sleep(0.05) # 短暂延迟后重试
|
|
104
|
+
|
|
105
|
+
logger.warning(f"Widget {widget.id} failed to gain focus after {max_retries} attempts")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def unmount(self, queue_id: str) -> None:
|
|
109
|
+
"""卸载 Widget 并清理。"""
|
|
110
|
+
queued = self._queue._queue.get(queue_id)
|
|
111
|
+
if queued is None:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
if queued.widget is not None:
|
|
115
|
+
if queued.widget.is_attached:
|
|
116
|
+
# 先 dismiss future
|
|
117
|
+
queued.widget.dismiss_externally()
|
|
118
|
+
# 再移除 widget
|
|
119
|
+
queued.widget.remove()
|
|
120
|
+
queued.widget = None
|
|
121
|
+
|
|
122
|
+
self._queue.remove(queue_id)
|
|
123
|
+
|
|
124
|
+
def unmount_all(self) -> None:
|
|
125
|
+
"""卸载所有 Widget。"""
|
|
126
|
+
for queue_id in list(self._queue._queue.keys()):
|
|
127
|
+
self.unmount(queue_id)
|
|
128
|
+
|
|
129
|
+
def get_focused_widget(self) -> InlinePermissionWidget | None:
|
|
130
|
+
"""获取当前有焦点的权限 Widget。"""
|
|
131
|
+
active = self._queue.get_active()
|
|
132
|
+
if active and active.widget and active.widget.has_focus:
|
|
133
|
+
return active.widget
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def cleanup(self) -> None:
|
|
137
|
+
"""清理所有资源。"""
|
|
138
|
+
self.unmount_all()
|
|
139
|
+
if self._focus_retry_timer:
|
|
140
|
+
self._focus_retry_timer.stop()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def create_component_manager(
|
|
144
|
+
queue: ConfirmQueue,
|
|
145
|
+
message_list: MessageListWidget,
|
|
146
|
+
) -> PermissionComponentManager:
|
|
147
|
+
"""创建组件管理器(工厂函数)。"""
|
|
148
|
+
return PermissionComponentManager(queue, message_list)
|
|
@@ -3,6 +3,7 @@ tui/permissions/consumer.py — SDK 权限队列消费者。
|
|
|
3
3
|
|
|
4
4
|
职责:
|
|
5
5
|
后台消费 PermissionRequest 队列,查缓存、委托决策端口、resolve Future。
|
|
6
|
+
集成 ConfirmQueue 状态管理(对齐 CC)。
|
|
6
7
|
|
|
7
8
|
链路位置:
|
|
8
9
|
ReplApp.on_mount 启动 → PermissionQueueConsumer → SDK AgentSessionPermissionResolver。
|
|
@@ -22,6 +23,7 @@ from langchain_agentx.tool_runtime.resolvers import PermissionRequest
|
|
|
22
23
|
from langchain_agentx_cli.tui.permissions.cache import SessionPermissionCache
|
|
23
24
|
from langchain_agentx_cli.tui.permissions.models import PermissionDecision
|
|
24
25
|
from langchain_agentx_cli.tui.permissions.presenters import permission_presenter_for_tool
|
|
26
|
+
from langchain_agentx_cli.tui.permissions.queue import ConfirmQueue
|
|
25
27
|
|
|
26
28
|
if TYPE_CHECKING:
|
|
27
29
|
from langchain_agentx_cli.app import ReplApp
|
|
@@ -54,7 +56,10 @@ class NoDialogPermissionDecisionPort:
|
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
class PermissionQueueConsumer:
|
|
57
|
-
"""消费 SDK 权限队列,resolve PermissionRequest.future。
|
|
59
|
+
"""消费 SDK 权限队列,resolve PermissionRequest.future。
|
|
60
|
+
|
|
61
|
+
对齐 CC: 集成 ConfirmQueue 状态管理。
|
|
62
|
+
"""
|
|
58
63
|
|
|
59
64
|
def __init__(
|
|
60
65
|
self,
|
|
@@ -63,11 +68,13 @@ class PermissionQueueConsumer:
|
|
|
63
68
|
app: ReplApp,
|
|
64
69
|
*,
|
|
65
70
|
decision_port: PermissionDecisionPort | None = None,
|
|
71
|
+
confirm_queue: ConfirmQueue | None = None,
|
|
66
72
|
) -> None:
|
|
67
73
|
self._queue = queue
|
|
68
74
|
self._cache = cache
|
|
69
75
|
self._app = app
|
|
70
76
|
self._decision_port = decision_port or NoDialogPermissionDecisionPort()
|
|
77
|
+
self._confirm_queue = confirm_queue # 对齐 CC confirmQueue
|
|
71
78
|
self._worker: Any = None
|
|
72
79
|
|
|
73
80
|
def start(self) -> None:
|
|
@@ -116,12 +123,29 @@ class PermissionQueueConsumer:
|
|
|
116
123
|
return self._cache.check(request.tool_name)
|
|
117
124
|
|
|
118
125
|
async def _handle_request(self, request: PermissionRequest) -> None:
|
|
126
|
+
"""处理权限请求(对齐 CC confirmQueue 流程)。"""
|
|
127
|
+
queued = None
|
|
119
128
|
try:
|
|
129
|
+
# 检查缓存
|
|
120
130
|
if self._is_cached(request):
|
|
121
131
|
self._resolve_future(request, True)
|
|
122
132
|
return
|
|
123
133
|
|
|
124
|
-
|
|
134
|
+
# 添加到 confirm_queue(对齐 CC addConfirm)
|
|
135
|
+
if self._confirm_queue is not None:
|
|
136
|
+
queued = await self._confirm_queue.add(request)
|
|
137
|
+
logger.debug(
|
|
138
|
+
f"Added to confirm_queue: {request.tool_name} (id={queued.queue_id})"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# 委托给决策端口
|
|
142
|
+
decision = await self._decision_port.resolve(
|
|
143
|
+
request,
|
|
144
|
+
self._cache,
|
|
145
|
+
queued, # 传递 queued 信息
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# 处理决策
|
|
125
149
|
if decision is PermissionDecision.ALLOW_SESSION:
|
|
126
150
|
presenter = permission_presenter_for_tool(
|
|
127
151
|
request.tool_name,
|
|
@@ -129,11 +153,11 @@ class PermissionQueueConsumer:
|
|
|
129
153
|
)
|
|
130
154
|
self._cache.add(request.tool_name, presenter.extract_pattern())
|
|
131
155
|
self._resolve_future(request, True)
|
|
132
|
-
|
|
133
|
-
if decision is PermissionDecision.ALLOW_ONCE:
|
|
156
|
+
elif decision is PermissionDecision.ALLOW_ONCE:
|
|
134
157
|
self._resolve_future(request, True)
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
else:
|
|
159
|
+
self._resolve_future(request, False)
|
|
160
|
+
|
|
137
161
|
except asyncio.CancelledError:
|
|
138
162
|
self._resolve_future(request, False)
|
|
139
163
|
raise
|
|
@@ -143,6 +167,10 @@ class PermissionQueueConsumer:
|
|
|
143
167
|
request.tool_name,
|
|
144
168
|
)
|
|
145
169
|
self._resolve_future(request, False)
|
|
170
|
+
finally:
|
|
171
|
+
# 从 confirm_queue 移除(对齐 CC removeConfirm)
|
|
172
|
+
if self._confirm_queue is not None and queued is not None:
|
|
173
|
+
self._confirm_queue.remove(queued.queue_id)
|
|
146
174
|
|
|
147
175
|
@staticmethod
|
|
148
176
|
def _resolve_future(request: PermissionRequest, allowed: bool) -> None:
|
|
@@ -3,6 +3,7 @@ tui/permissions/dialog.py — 内联权限确认决策端口。
|
|
|
3
3
|
|
|
4
4
|
职责:
|
|
5
5
|
在消息流中 mount InlinePermissionWidget,收集用户决策。
|
|
6
|
+
集成 ConfirmQueue 状态管理(对齐 CC)。
|
|
6
7
|
|
|
7
8
|
链路位置:
|
|
8
9
|
DialogPermissionDecisionPort → InlinePermissionWidget → Consumer resolve Future。
|
|
@@ -20,6 +21,7 @@ from langchain_agentx_cli.tui.permissions.cache import SessionPermissionCache
|
|
|
20
21
|
from langchain_agentx_cli.tui.permissions.models import PermissionDecision
|
|
21
22
|
from langchain_agentx_cli.tui.permissions.presenters import permission_presenter_for_tool
|
|
22
23
|
from langchain_agentx_cli.tui.permissions.presenters.base import PermissionPresentation
|
|
24
|
+
from langchain_agentx_cli.tui.permissions.queue import QueuedPermission
|
|
23
25
|
from langchain_agentx_cli.widgets.messages import PermissionPendingHidden, PermissionPendingShown
|
|
24
26
|
|
|
25
27
|
if TYPE_CHECKING:
|
|
@@ -46,6 +48,7 @@ class DialogPermissionDecisionPort:
|
|
|
46
48
|
self,
|
|
47
49
|
request: PermissionRequest,
|
|
48
50
|
cache: SessionPermissionCache,
|
|
51
|
+
queued: QueuedPermission | None = None, # 新增:对齐 CC confirmQueue
|
|
49
52
|
) -> PermissionDecision:
|
|
50
53
|
del cache
|
|
51
54
|
presentation = permission_presenter_for_tool(
|
|
@@ -53,9 +56,9 @@ class DialogPermissionDecisionPort:
|
|
|
53
56
|
request.ask_prompt,
|
|
54
57
|
).present()
|
|
55
58
|
|
|
56
|
-
self._post_pending(request, presentation)
|
|
59
|
+
self._post_pending(request, presentation, queued)
|
|
57
60
|
try:
|
|
58
|
-
return await self._resolve_inline(request, presentation)
|
|
61
|
+
return await self._resolve_inline(request, presentation, queued)
|
|
59
62
|
except asyncio.CancelledError:
|
|
60
63
|
return PermissionDecision.DENY
|
|
61
64
|
except Exception:
|
|
@@ -68,6 +71,7 @@ class DialogPermissionDecisionPort:
|
|
|
68
71
|
self,
|
|
69
72
|
request: PermissionRequest,
|
|
70
73
|
presentation: PermissionPresentation,
|
|
74
|
+
queued: QueuedPermission | None = None,
|
|
71
75
|
) -> PermissionDecision:
|
|
72
76
|
"""Mount inline widget,与 SDK Future 竞态。"""
|
|
73
77
|
from langchain_agentx_cli.widgets.permission_inline import InlinePermissionWidget
|
|
@@ -120,6 +124,7 @@ class DialogPermissionDecisionPort:
|
|
|
120
124
|
self,
|
|
121
125
|
request: PermissionRequest,
|
|
122
126
|
presentation: PermissionPresentation,
|
|
127
|
+
queued: QueuedPermission | None = None,
|
|
123
128
|
) -> None:
|
|
124
129
|
pass # Pending shown is posted with widget in _resolve_inline
|
|
125
130
|
|