@scemoon/cdh 1.0.4 → 1.0.6
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.
- package/ai-dlc-skill/SKILL.md +4 -2
- package/ai-dlc-skill/agents/master.md +6 -0
- package/ai-dlc-skill/agents/plan-agent.md +2 -2
- package/ai-dlc-skill/agents/understand-agent.md +4 -4
- package/ai-dlc-skill/agents/verify-agent.md +1 -1
- package/ai-dlc-skill/architecture/project-structure.md +10 -10
- package/ai-dlc-skill/brownfield/README.md +2 -2
- package/ai-dlc-skill/brownfield/generate-context.sh +3 -3
- package/ai-dlc-skill/brownfield/scripts/extract-api.sh +5 -5
- package/ai-dlc-skill/components/backend.md +1 -1
- package/ai-dlc-skill/components/web.md +2 -2
- package/ai-dlc-skill/contracts/README.md +3 -3
- package/ai-dlc-skill/cross-tool/cline/export.sh +1 -1
- package/ai-dlc-skill/cross-tool/copilot/export.sh +1 -1
- package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +7 -3
- package/ai-dlc-skill/cross-tool/onecode/export.sh +14 -8
- package/ai-dlc-skill/phases/deliver/lifecycle.md +6 -6
- package/ai-dlc-skill/phases/deliver/prompt.md +2 -2
- package/ai-dlc-skill/phases/deliver/rules.md +1 -1
- package/ai-dlc-skill/phases/plan/lifecycle.md +11 -11
- package/ai-dlc-skill/phases/plan/prompt.md +6 -6
- package/ai-dlc-skill/phases/plan/rules.md +1 -1
- package/ai-dlc-skill/phases/understand/entry.md +1 -1
- package/ai-dlc-skill/phases/understand/lifecycle.md +7 -7
- package/ai-dlc-skill/phases/understand/prompt.md +8 -8
- package/ai-dlc-skill/phases/understand/rules.md +5 -5
- package/ai-dlc-skill/phases/verify/lifecycle.md +24 -24
- package/ai-dlc-skill/phases/verify/prompt.md +8 -8
- package/ai-dlc-skill/phases/verify/rules.md +9 -9
- package/ai-dlc-skill/providers/README.md +2 -2
- package/ai-dlc-skill/providers/aliyun/preview.yaml +2 -2
- package/ai-dlc-skill/providers/tcb/preview.yaml +2 -2
- package/ai-dlc-skill/requirements.md +7 -7
- package/ai-dlc-skill/skill.yaml +4 -4
- package/ai-dlc-skill/templates/integration/CHANGELOG.md +2 -2
- package/ai-dlc-skill/templates/integration/contract-diff.md +7 -7
- package/ai-dlc-skill/templates/integration/contract-spec.md +7 -7
- package/ai-dlc-skill/templates/integration/runtime-contract.yaml +1 -1
- package/ai-dlc-skill/templates/project/README.md +18 -19
- package/ai-dlc-skill/templates/project/template.md +21 -21
- package/ai-dlc-skill/walkthrough/collect.sh +1 -1
- package/ai-dlc-skill/walkthrough/template.md +2 -2
- package/ai-dlc-skill/workflows/ai-dlc.yaml +12 -12
- package/ai-dlc-skill/workflows/deployment.yaml +4 -4
- package/cdh/__pycache__/cdh_mcp_injector.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_mcp_loader.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_mcp_manager.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_session_aggregator.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_skill_loader.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cdh_skill_manager.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cli.cpython-313.pyc +0 -0
- package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/cdh/cdh_mcp_injector.py +115 -0
- package/cdh/cdh_mcp_loader.py +67 -0
- package/cdh/cdh_mcp_manager.py +73 -0
- package/cdh/cdh_session_aggregator.py +258 -0
- package/cdh/cdh_skill_manager.py +105 -0
- package/cdh/cli.py +64 -4
- package/cdh/scaffold.py +192 -41
- package/onecode/__init__.py +1 -1
- package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
- package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
- package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/project_doc.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
- package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
- package/onecode/agent/agents/types.py +47 -164
- package/onecode/agent/attachments.py +0 -1
- package/onecode/agent/cdh_loader.py +17 -25
- package/onecode/agent/context.py +8 -3
- package/onecode/agent/engine.py +835 -220
- package/onecode/agent/hooks.py +1 -1
- package/onecode/agent/onecode_agent_acp.py +460 -47
- package/onecode/agent/onecode_agent_acp.py.bak +2447 -0
- package/onecode/agent/permissions.py +0 -1
- package/onecode/agent/project_doc.py +19 -0
- package/onecode/agent/session.py +17 -3
- package/onecode/agent/snapshot.py +3 -4
- package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/agent_tools.py +4 -6
- package/onecode/agent/tools/apply_patch_tool.py +2 -8
- package/onecode/agent/tools/bash_tool.py +32 -12
- package/onecode/agent/tools/communication_tools.py +4 -5
- package/onecode/agent/tools/config_tool.py +3 -3
- package/onecode/agent/tools/cron_tools.py +4 -5
- package/onecode/agent/tools/file_ops.py +37 -3
- package/onecode/agent/tools/file_tools.py +10 -11
- package/onecode/agent/tools/git_tools.py +2 -2
- package/onecode/agent/tools/lsp_tools.py +2 -2
- package/onecode/agent/tools/mcp_tools.py +3 -5
- package/onecode/agent/tools/permission_handler.py +1 -1
- package/onecode/agent/tools/protocol.py +1 -1
- package/onecode/agent/tools/registry.py +69 -2
- package/onecode/agent/tools/sandbox.py +110 -16
- package/onecode/agent/tools/skill_tools.py +2 -2
- package/onecode/agent/tools/todo_tools.py +38 -19
- package/onecode/agent/tools/web_tools.py +6 -7
- package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
- package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
- package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
- package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
- package/onecode/cli.py +186 -6
- package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
- package/onecode/codebase/chunker.py +0 -5
- package/onecode/codebase/indexer.py +14 -1
- package/onecode/codebase/retriever.py +13 -2
- package/onecode/codebase/storage.py +5 -4
- package/onecode/commands.py +81 -6
- package/onecode/config.py +41 -12
- package/onecode/config_screen.py +7 -3
- package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/mcp/manager.py +2 -3
- package/onecode/memory/__init__.py +7 -53
- package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
- package/onecode/memory/backend.py +13 -39
- package/onecode/memory/pyramid.py +2 -71
- package/onecode/memory/recall.py +38 -3
- package/onecode/migrate.py +122 -0
- package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
- package/onecode/models/messages.py +58 -0
- package/onecode/models/provider.py +20 -2
- package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/anthropic_provider.py +59 -12
- package/onecode/models/providers/glm_provider.py +40 -0
- package/onecode/models/providers/minimax_provider.py +3 -0
- package/onecode/models/providers/minimaxi_provider.py +3 -0
- package/onecode/models/providers/ollama_provider.py +3 -0
- package/onecode/server/app.py +1 -1
- package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/skills/argument_substitution.py +0 -1
- package/onecode/skills/bootstrap.py +124 -0
- package/onecode/skills/loader.py +35 -53
- package/onecode/skills/manager.py +2 -2
- package/onecode/storage/activity.py +3 -4
- package/onecode/storage/session.py +3 -4
- package/onecode/tasks/manager.py +23 -10
- package/onecode/trace/tracer.py +2 -3
- package/package.json +1 -1
- package/pyproject.toml +5 -1
- package/run.js +68 -14
- package/tui/__pycache__/about.cpython-314.pyc +0 -0
- package/tui/__pycache__/app.cpython-313.pyc +0 -0
- package/tui/__pycache__/app.cpython-314.pyc +0 -0
- package/tui/__pycache__/danger.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
- package/tui/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
- package/tui/__pycache__/paths.cpython-314.pyc +0 -0
- package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
- package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
- package/tui/acp/agent.py +125 -58
- package/tui/acp/api.py +12 -0
- package/tui/acp/messages.py +3 -1
- package/tui/acp/prompt.py +0 -57
- package/tui/acp/protocol.py +6 -0
- package/tui/app.py +11 -33
- package/tui/messages.py +2 -0
- package/tui/path_complete.py +1 -1
- package/tui/paths.py +16 -0
- package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/main.cpython-313.pyc +0 -0
- package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/permissions.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/tool_content_screen.cpython-314.pyc +0 -0
- package/tui/screens/component_picker.py +35 -15
- package/tui/screens/diff_screen.py +84 -0
- package/tui/screens/log.py +25 -25
- package/tui/screens/main.py +48 -26
- package/tui/screens/projects.tcss +5 -1
- package/tui/screens/projects_app.py +6 -6
- package/tui/screens/projects_screen.py +19 -39
- package/tui/screens/sessions.py +3 -5
- package/tui/session_tracker.py +3 -0
- package/tui/settings_schema.py +1 -1
- package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
- package/tui/visuals/columns.py +1 -2
- package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
- package/tui/widgets/agent_response.py +0 -1
- package/tui/widgets/agent_thought.py +0 -1
- package/tui/widgets/conversation.py +157 -14
- package/tui/widgets/diff_view.py +10 -0
- package/tui/widgets/load_more.py +24 -0
- package/tui/widgets/modified_files.py +86 -12
- package/tui/widgets/plan.py +1 -1
- package/tui/widgets/project_grid_select.py +9 -2
- package/tui/widgets/session_grid_select.py +6 -0
- package/tui/widgets/side_bar.py +10 -0
- package/tui/widgets/subagent.py +92 -58
- package/tui/widgets/subagent_screen.py +638 -115
- package/tui/widgets/terminal.py +1 -2
- package/tui/widgets/terminal_tool.py +11 -2
- package/tui/widgets/tool_call.py +35 -2
- package/onecode/memory/offload.py +0 -114
- package/onecode/memory/symbolic.py +0 -164
|
@@ -1,24 +1,93 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from textual import on
|
|
3
9
|
from textual.app import ComposeResult
|
|
4
10
|
from textual.binding import Binding, BindingType
|
|
5
|
-
from textual.containers import Vertical
|
|
11
|
+
from textual.containers import Grid, Vertical, VerticalGroup, VerticalScroll
|
|
12
|
+
from textual.css.query import NoMatches
|
|
13
|
+
from textual.geometry import Offset, Region, Spacing
|
|
14
|
+
from textual.layouts.grid import GridLayout
|
|
15
|
+
from textual.layouts.vertical import WidgetPlacement
|
|
16
|
+
from textual.reactive import reactive
|
|
6
17
|
from textual.screen import Screen
|
|
7
|
-
from textual.
|
|
18
|
+
from textual.widget import Widget
|
|
19
|
+
from textual.widgets import Header, Static
|
|
20
|
+
|
|
21
|
+
from rich.segment import Segment
|
|
22
|
+
from textual.strip import Strip
|
|
8
23
|
|
|
24
|
+
from tui.menus import MenuItem
|
|
25
|
+
from tui.protocol import BlockProtocol, ExpandProtocol, MenuProtocol
|
|
26
|
+
from tui.widgets.agent_response import AgentResponse
|
|
27
|
+
from tui.widgets.agent_thought import AgentThought
|
|
28
|
+
from tui.widgets.conversation import Cursor
|
|
29
|
+
from tui.widgets.menu import Menu
|
|
9
30
|
from tui.widgets.subagent import SubAgent
|
|
31
|
+
from tui.widgets.tool_call import ToolCall as ToolCallWidget
|
|
32
|
+
|
|
33
|
+
_sa_logger = logging.getLogger("tui.widgets.subagent_screen")
|
|
34
|
+
_DEBUG_LOG = Path.home() / ".cdh" / "logs" / "subagent_cursor_debug.log"
|
|
35
|
+
|
|
36
|
+
_TODO_TOOLS: frozenset[str] = frozenset({
|
|
37
|
+
"TodoCreate", "TodoGet", "TodoList", "TodoUpdate",
|
|
38
|
+
"TodoOutput", "TodoStop", "TodoClear",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
_SELECTABLE_BLOCK_TYPES: tuple[type[Widget], ...] = (
|
|
42
|
+
AgentResponse,
|
|
43
|
+
AgentThought,
|
|
44
|
+
ToolCallWidget,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_selectable(child: Widget) -> bool:
|
|
49
|
+
return isinstance(child, _SELECTABLE_BLOCK_TYPES)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SubAgentCursorContainer(Vertical):
|
|
53
|
+
def render_lines(self, crop: Region) -> list[Strip]:
|
|
54
|
+
rich_style = self.visual_style.rich_style
|
|
55
|
+
strips = [Strip([Segment("▌", rich_style)], cell_length=1)] * crop.height
|
|
56
|
+
if crop.y == 0 and strips:
|
|
57
|
+
strips[0] = Strip([Segment(" ", rich_style)], cell_length=1)
|
|
58
|
+
return strips
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SubAgentContentsGrid(Grid):
|
|
62
|
+
BLANK = True
|
|
63
|
+
|
|
64
|
+
def pre_layout(self, layout) -> None:
|
|
65
|
+
assert isinstance(layout, GridLayout)
|
|
66
|
+
layout.stretch_height = True
|
|
10
67
|
|
|
11
68
|
|
|
12
69
|
class SubAgentScreen(Screen):
|
|
13
70
|
"""Full-screen view of a SubAgent output.
|
|
14
71
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
72
|
+
Renders identically to the main Conversation:
|
|
73
|
+
- AgentResponse for streaming text (MarkdownStream)
|
|
74
|
+
- ToolCall cards mounted inline between text blocks
|
|
75
|
+
- AgentThought (thinking) as inline siblings at position of occurrence
|
|
76
|
+
- All widgets as siblings in a single #content container
|
|
77
|
+
- Vertical-bar block cursor in a left-side CursorContainer, matching
|
|
78
|
+
Conversation's visual treatment.
|
|
79
|
+
|
|
80
|
+
Mirrors Conversation's block cursor: Ctrl+J/K move between blocks,
|
|
81
|
+
Enter opens a Menu (copy / maximize), Ctrl+X toggles expand/collapse
|
|
82
|
+
on ExpandProtocol blocks.
|
|
18
83
|
"""
|
|
19
84
|
|
|
20
|
-
BINDINGS: list[BindingType] = [
|
|
85
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
21
86
|
Binding("escape", "app.pop_screen", "Back", priority=True),
|
|
87
|
+
Binding("ctrl+j", "cursor_down", "Next block", show=False, priority=True),
|
|
88
|
+
Binding("ctrl+k", "cursor_up", "Prev block", show=False, priority=True),
|
|
89
|
+
Binding("enter", "select_block", "Block menu", show=False, priority=True),
|
|
90
|
+
Binding("ctrl+x", "toggle_expand", "Toggle expand", show=False, priority=True),
|
|
22
91
|
Binding("up", "scroll_up", "Up", priority=True),
|
|
23
92
|
Binding("down", "scroll_down", "Down", priority=True),
|
|
24
93
|
Binding("pageup", "scroll_page_up", "Page Up", priority=True),
|
|
@@ -29,158 +98,612 @@ class SubAgentScreen(Screen):
|
|
|
29
98
|
|
|
30
99
|
DEFAULT_CSS = """
|
|
31
100
|
SubAgentScreen {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
width: 95%;
|
|
36
|
-
max-width: 160;
|
|
37
|
-
height: 95%;
|
|
38
|
-
border: thick $primary;
|
|
39
|
-
background: $surface;
|
|
40
|
-
}
|
|
101
|
+
background: $surface;
|
|
102
|
+
padding-left: 1;
|
|
103
|
+
}
|
|
41
104
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
background: $boost;
|
|
48
|
-
}
|
|
105
|
+
#scroll {
|
|
106
|
+
height: 1fr;
|
|
107
|
+
overflow-y: auto;
|
|
108
|
+
scrollbar-gutter: stable;
|
|
109
|
+
}
|
|
49
110
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
111
|
+
#content-grid {
|
|
112
|
+
layout: grid;
|
|
113
|
+
grid-size: 2 1;
|
|
114
|
+
grid-columns: 1 1fr;
|
|
115
|
+
grid-gutter: 0;
|
|
116
|
+
height: auto;
|
|
117
|
+
width: 1fr;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#cursor-container {
|
|
121
|
+
width: 1;
|
|
122
|
+
height: auto;
|
|
123
|
+
color: $foreground 7%;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#cursor-container > Cursor {
|
|
127
|
+
height: 0;
|
|
128
|
+
width: 1;
|
|
129
|
+
border-left: outer $text-accent;
|
|
130
|
+
visibility: visible;
|
|
131
|
+
&.-blink {
|
|
132
|
+
border-left: outer $text-accent 20%;
|
|
55
133
|
}
|
|
56
134
|
}
|
|
135
|
+
|
|
136
|
+
#content {
|
|
137
|
+
layout: stream;
|
|
138
|
+
width: 1fr;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
height: auto;
|
|
141
|
+
padding: 0 0 0 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#content > AgentResponse {
|
|
145
|
+
min-height: 1;
|
|
146
|
+
padding: 0 0 0 0;
|
|
147
|
+
overflow-x: auto;
|
|
148
|
+
scrollbar-size-horizontal: 0;
|
|
149
|
+
layout: stream;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#content > ToolCall {
|
|
153
|
+
margin: 0 0 0 1 !important;
|
|
154
|
+
width: 1fr;
|
|
155
|
+
layout: stream;
|
|
156
|
+
height: auto;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#content > AgentThought {
|
|
160
|
+
margin: 1 1 1 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#content > ToolCall ToolCallHeader {
|
|
164
|
+
color: $text-secondary;
|
|
165
|
+
pointer: pointer;
|
|
166
|
+
width: auto;
|
|
167
|
+
max-width: 1fr;
|
|
168
|
+
margin: 0 1 0 0;
|
|
169
|
+
text-wrap: nowrap;
|
|
170
|
+
text-overflow: ellipsis;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#content > ToolCall #tool-content {
|
|
174
|
+
display: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#content > ToolCall.-has-content #tool-content {
|
|
178
|
+
margin: 1 1 1 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#content > ToolCall.-expanded #tool-content {
|
|
182
|
+
display: block;
|
|
183
|
+
max-height: 60vh;
|
|
184
|
+
overflow-y: hidden;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#content > ToolCall.-expanded ToolCallHeader {
|
|
188
|
+
text-wrap: wrap;
|
|
189
|
+
text-overflow: fold;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#content > AgentResponse > MarkdownBlock:last-child,
|
|
193
|
+
#content > AgentThought > #thought-content MarkdownBlock:last-child {
|
|
194
|
+
margin-bottom: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#footer {
|
|
198
|
+
dock: bottom;
|
|
199
|
+
height: 1;
|
|
200
|
+
padding: 0 1;
|
|
201
|
+
color: $text-muted;
|
|
202
|
+
background: $boost;
|
|
203
|
+
}
|
|
57
204
|
"""
|
|
58
205
|
|
|
206
|
+
class Content(VerticalGroup, can_focus=False):
|
|
207
|
+
"""Matches Conversation.Contents — strips bottom margin from last child."""
|
|
208
|
+
|
|
209
|
+
def process_layout(
|
|
210
|
+
self, placements: list[WidgetPlacement]
|
|
211
|
+
) -> list[WidgetPlacement]:
|
|
212
|
+
if placements:
|
|
213
|
+
last = placements[-1]
|
|
214
|
+
top, right, _bottom, left = last.margin
|
|
215
|
+
placements[-1] = last._replace(
|
|
216
|
+
margin=Spacing(top, right, 0, left)
|
|
217
|
+
)
|
|
218
|
+
return placements
|
|
219
|
+
|
|
220
|
+
cursor_offset: reactive[int] = reactive(-1)
|
|
221
|
+
|
|
59
222
|
def __init__(self, subagent: SubAgent) -> None:
|
|
60
223
|
super().__init__()
|
|
224
|
+
_DEBUG_LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
225
|
+
_sa_logger.addHandler(logging.FileHandler(str(_DEBUG_LOG), mode="w"))
|
|
226
|
+
_sa_logger.setLevel(logging.DEBUG)
|
|
61
227
|
self.subagent = subagent
|
|
228
|
+
self._event_lock = asyncio.Lock()
|
|
229
|
+
self._current_response: AgentResponse | None = None
|
|
230
|
+
self._current_thought: AgentThought | None = None
|
|
62
231
|
self._auto_follow = True
|
|
63
|
-
self.
|
|
64
|
-
self._last_content_hash: str = ""
|
|
232
|
+
self._active_menu: Menu | None = None
|
|
65
233
|
|
|
66
234
|
def compose(self) -> ComposeResult:
|
|
67
235
|
yield Header(show_clock=False)
|
|
68
|
-
with
|
|
69
|
-
|
|
236
|
+
with VerticalScroll(id="scroll"):
|
|
237
|
+
with SubAgentContentsGrid(id="content-grid"):
|
|
238
|
+
with SubAgentCursorContainer(id="cursor-container"):
|
|
239
|
+
yield Cursor()
|
|
240
|
+
yield self.Content(id="content")
|
|
70
241
|
yield Static(
|
|
71
242
|
"[$text-muted]esc[/] 返回 "
|
|
72
|
-
"[$text-muted]
|
|
73
|
-
"[$text-muted]
|
|
74
|
-
"[$text-muted]
|
|
75
|
-
|
|
243
|
+
"[$text-muted]ctrl+j/k[/] 移动 "
|
|
244
|
+
"[$text-muted]enter[/] 菜单 "
|
|
245
|
+
"[$text-muted]ctrl+x[/] 折叠 "
|
|
246
|
+
"[$text-muted]↑↓[/] 滚动",
|
|
247
|
+
id="footer",
|
|
76
248
|
)
|
|
77
249
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self.
|
|
83
|
-
|
|
250
|
+
# ── Lifecycle ──
|
|
251
|
+
|
|
252
|
+
async def on_mount(self) -> None:
|
|
253
|
+
# Subscribe first so we don't miss events during replay
|
|
254
|
+
self.subagent._screen_handler = self._on_event
|
|
255
|
+
# Replay all existing events with yields so the UI can render progressively
|
|
256
|
+
await self._replay_all()
|
|
257
|
+
try:
|
|
258
|
+
self.query_one("#scroll", VerticalScroll).focus()
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
84
261
|
|
|
85
262
|
def on_unmount(self) -> None:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
263
|
+
self.subagent._screen_handler = None
|
|
264
|
+
self._current_response = None
|
|
265
|
+
self._current_thought = None
|
|
89
266
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
267
|
+
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
|
|
268
|
+
if self._active_menu is not None and action in (
|
|
269
|
+
"select_block",
|
|
270
|
+
"scroll_up",
|
|
271
|
+
"scroll_down",
|
|
272
|
+
"scroll_page_up",
|
|
273
|
+
"scroll_page_down",
|
|
274
|
+
"scroll_home",
|
|
275
|
+
"scroll_end",
|
|
276
|
+
"app.pop_screen",
|
|
277
|
+
):
|
|
278
|
+
return False
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
# ── Event processing ──
|
|
282
|
+
|
|
283
|
+
async def _replay_all(self) -> None:
|
|
284
|
+
"""Process every event already in ``subagent._events``.
|
|
285
|
+
|
|
286
|
+
Each event is followed by ``await asyncio.sleep(0)`` so the layout
|
|
287
|
+
can settle between mounts – this avoids a long blocking stall
|
|
288
|
+
before the screen becomes visible.
|
|
289
|
+
"""
|
|
105
290
|
try:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
291
|
+
container = self.query_one("#content", VerticalGroup)
|
|
292
|
+
for event_type, data in self.subagent._events:
|
|
293
|
+
await self._handle_one(container, event_type, data)
|
|
294
|
+
await asyncio.sleep(0)
|
|
295
|
+
await self._flush_streams()
|
|
296
|
+
self._post_process()
|
|
110
297
|
except Exception:
|
|
111
|
-
|
|
298
|
+
_sa_logger.exception("_replay_all failed")
|
|
112
299
|
|
|
113
|
-
def
|
|
114
|
-
|
|
300
|
+
async def _on_event(self, event_type: str, data) -> None:
|
|
301
|
+
"""Handle a single real-time event from the SubAgent.
|
|
302
|
+
|
|
303
|
+
Called via ``run_worker`` inside ``SubAgent.*`` methods.
|
|
304
|
+
The lock serialises concurrently-scheduled workers.
|
|
305
|
+
"""
|
|
306
|
+
async with self._event_lock:
|
|
307
|
+
try:
|
|
308
|
+
container = self.query_one("#content", VerticalGroup)
|
|
309
|
+
await self._handle_one(container, event_type, data)
|
|
310
|
+
self._post_process()
|
|
311
|
+
except Exception:
|
|
312
|
+
_sa_logger.exception("_on_event failed")
|
|
313
|
+
|
|
314
|
+
async def _handle_one(self, container: VerticalGroup, event_type: str, data) -> None:
|
|
315
|
+
"""Dispatch a single event to the appropriate handler."""
|
|
316
|
+
if event_type == "thinking":
|
|
317
|
+
await self._handle_thinking(container, data)
|
|
318
|
+
elif event_type == "text":
|
|
319
|
+
self._complete_current_thought()
|
|
320
|
+
await self._handle_text(container, data)
|
|
321
|
+
elif event_type == "tool":
|
|
322
|
+
self._complete_current_thought()
|
|
323
|
+
await self._handle_tool(container, data)
|
|
324
|
+
elif event_type == "complete":
|
|
325
|
+
self._complete_current_thought()
|
|
326
|
+
|
|
327
|
+
def _post_process(self) -> None:
|
|
328
|
+
"""Common work after every event – title, footer, cursor, scroll."""
|
|
115
329
|
try:
|
|
116
|
-
self.
|
|
330
|
+
if self._current_thought is not None and not self._current_thought._completed:
|
|
331
|
+
if self.subagent._status in ("completed", "failed"):
|
|
332
|
+
self._current_thought.mark_completed()
|
|
333
|
+
self._current_thought = None
|
|
334
|
+
self._update_title()
|
|
335
|
+
self._update_footer()
|
|
336
|
+
self._refresh_block_cursor()
|
|
337
|
+
if self._auto_follow:
|
|
338
|
+
self.call_after_refresh(self._scroll_to_end)
|
|
117
339
|
except Exception:
|
|
118
|
-
|
|
340
|
+
_sa_logger.exception("_post_process failed")
|
|
119
341
|
|
|
120
|
-
def
|
|
342
|
+
async def _handle_thinking(self, container: VerticalGroup, fragment: str) -> None:
|
|
343
|
+
if self._current_thought is None:
|
|
344
|
+
self._current_thought = AgentThought(fragment)
|
|
345
|
+
await container.mount(self._current_thought)
|
|
346
|
+
else:
|
|
347
|
+
await self._current_thought.append_fragment(fragment)
|
|
348
|
+
|
|
349
|
+
async def _handle_text(self, container: VerticalGroup, fragment: str) -> None:
|
|
350
|
+
if self._current_response is None:
|
|
351
|
+
self._current_response = AgentResponse("")
|
|
352
|
+
await container.mount(self._current_response)
|
|
353
|
+
await self._current_response.append_fragment(fragment)
|
|
354
|
+
|
|
355
|
+
async def _handle_tool(self, container: VerticalGroup, data) -> None:
|
|
356
|
+
tool_id, tc = data
|
|
357
|
+
self._current_response = None
|
|
358
|
+
|
|
359
|
+
# Skip todo tool calls — not relevant in subagent view
|
|
360
|
+
title = tc.get("title", "") if isinstance(tc, dict) else ""
|
|
361
|
+
tool_name = title.split(":")[0] if ":" in title else title
|
|
362
|
+
if tool_name in _TODO_TOOLS:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
scr_id = f"scr-{tool_id}"
|
|
121
366
|
try:
|
|
122
|
-
|
|
123
|
-
|
|
367
|
+
existing: ToolCallWidget = container.get_child_by_id(scr_id) # type: ignore[assignment]
|
|
368
|
+
await existing.update_tool_call(tc)
|
|
369
|
+
except NoMatches:
|
|
370
|
+
await container.mount(ToolCallWidget(tc, id=scr_id))
|
|
371
|
+
except Exception:
|
|
372
|
+
_sa_logger.exception("_handle_tool failed %s", tool_id)
|
|
373
|
+
|
|
374
|
+
def _complete_current_thought(self) -> None:
|
|
375
|
+
if self._current_thought is not None and not self._current_thought._completed:
|
|
376
|
+
self._current_thought.mark_completed()
|
|
377
|
+
self._current_thought = None
|
|
378
|
+
|
|
379
|
+
def _scroll_to_end(self) -> None:
|
|
380
|
+
try:
|
|
381
|
+
self.query_one("#scroll", VerticalScroll).scroll_end(animate=True)
|
|
124
382
|
except Exception:
|
|
125
383
|
pass
|
|
126
384
|
|
|
127
|
-
def
|
|
385
|
+
async def _flush_streams(self) -> None:
|
|
386
|
+
"""Ensure all markdown streams have finished processing before cursor navigation."""
|
|
387
|
+
try:
|
|
388
|
+
container = self.query_one("#content", VerticalGroup)
|
|
389
|
+
except Exception:
|
|
390
|
+
return
|
|
391
|
+
for child in container.children:
|
|
392
|
+
stream = getattr(child, "_stream", None)
|
|
393
|
+
if stream is not None:
|
|
394
|
+
_sa_logger.warning(
|
|
395
|
+
"▓ flush stream | widget=%s | task_alive=%s",
|
|
396
|
+
type(child).__name__,
|
|
397
|
+
stream._task is not None and not stream._task.done() if stream._task else False,
|
|
398
|
+
)
|
|
399
|
+
try:
|
|
400
|
+
await stream.stop()
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
else:
|
|
404
|
+
_sa_logger.warning(
|
|
405
|
+
"▓ no stream | widget=%s | children=%s",
|
|
406
|
+
type(child).__name__,
|
|
407
|
+
len(child.children) if hasattr(child, 'children') else '?',
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# ── Block cursor ──
|
|
411
|
+
|
|
412
|
+
def _selectable_children(self) -> list[Widget]:
|
|
413
|
+
try:
|
|
414
|
+
container = self.query_one("#content", VerticalGroup)
|
|
415
|
+
except Exception:
|
|
416
|
+
return []
|
|
417
|
+
return [child for child in container.children if _is_selectable(child)]
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def cursor_block(self) -> Widget | None:
|
|
421
|
+
if self.cursor_offset == -1:
|
|
422
|
+
return None
|
|
423
|
+
children = self._selectable_children()
|
|
424
|
+
if not children or self.cursor_offset >= len(children):
|
|
425
|
+
return None
|
|
426
|
+
return children[self.cursor_offset]
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def cursor_block_child(self) -> Widget | None:
|
|
430
|
+
if (block := self.cursor_block) is not None:
|
|
431
|
+
if isinstance(block, BlockProtocol):
|
|
432
|
+
inner = block.get_cursor_block()
|
|
433
|
+
if inner is not None:
|
|
434
|
+
_sa_logger.warning(
|
|
435
|
+
"┃ child inner=%s children=%s attached=%s",
|
|
436
|
+
type(inner).__name__,
|
|
437
|
+
len(block.children) if hasattr(block, 'children') else '?',
|
|
438
|
+
inner.is_attached,
|
|
439
|
+
)
|
|
440
|
+
return inner
|
|
441
|
+
_sa_logger.warning(
|
|
442
|
+
"┃ child fallback → outer=%s children=%s",
|
|
443
|
+
type(block).__name__,
|
|
444
|
+
len(block.children) if hasattr(block, 'children') else '?',
|
|
445
|
+
)
|
|
446
|
+
return block
|
|
447
|
+
|
|
448
|
+
def _cursor_widget(self) -> Cursor | None:
|
|
449
|
+
try:
|
|
450
|
+
return self.query_one("#cursor-container Cursor", Cursor)
|
|
451
|
+
except Exception:
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
def _content_region(self) -> Region | None:
|
|
455
|
+
try:
|
|
456
|
+
return self.query_one("#content", VerticalGroup).content_region
|
|
457
|
+
except Exception:
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
def _refresh_block_cursor(self) -> None:
|
|
461
|
+
cursor = self._cursor_widget()
|
|
462
|
+
if cursor is None:
|
|
463
|
+
return
|
|
464
|
+
block = self.cursor_block_child
|
|
465
|
+
if block is not None:
|
|
466
|
+
cursor.visible = True
|
|
467
|
+
cursor.follow(block)
|
|
468
|
+
_sa_logger.warning(
|
|
469
|
+
"┃ follow | block=%s | offset=(%s,%s) | h=%s | visible=%s",
|
|
470
|
+
type(block).__name__,
|
|
471
|
+
cursor.offset.x, cursor.offset.y,
|
|
472
|
+
cursor.styles.height,
|
|
473
|
+
cursor.visible,
|
|
474
|
+
)
|
|
475
|
+
scroll = self._scroll_widget()
|
|
476
|
+
if scroll is not None:
|
|
477
|
+
scroll.focus()
|
|
478
|
+
self.call_after_refresh(
|
|
479
|
+
scroll.scroll_to_center, block, immediate=True
|
|
480
|
+
)
|
|
481
|
+
else:
|
|
482
|
+
cursor.visible = False
|
|
483
|
+
cursor.follow(None)
|
|
484
|
+
self.refresh_bindings()
|
|
485
|
+
|
|
486
|
+
def action_cursor_up(self) -> None:
|
|
487
|
+
children = self._selectable_children()
|
|
488
|
+
_sa_logger.warning(
|
|
489
|
+
"▲ cursor_up | offset=%s | num_children=%s | types=%s",
|
|
490
|
+
self.cursor_offset,
|
|
491
|
+
len(children),
|
|
492
|
+
[type(c).__name__ for c in children],
|
|
493
|
+
)
|
|
494
|
+
if not children:
|
|
495
|
+
return
|
|
496
|
+
if self.cursor_offset == -1:
|
|
497
|
+
self.cursor_offset = len(children) - 1
|
|
498
|
+
cursor_block = self.cursor_block
|
|
499
|
+
if isinstance(cursor_block, BlockProtocol):
|
|
500
|
+
cursor_block.block_cursor_clear()
|
|
501
|
+
cursor_block.block_cursor_up()
|
|
502
|
+
else:
|
|
503
|
+
cursor_block = self.cursor_block
|
|
504
|
+
if isinstance(cursor_block, BlockProtocol):
|
|
505
|
+
if cursor_block.block_cursor_up() is None:
|
|
506
|
+
if self.cursor_offset > 0:
|
|
507
|
+
self.cursor_offset -= 1
|
|
508
|
+
cursor_block = self.cursor_block
|
|
509
|
+
if isinstance(cursor_block, BlockProtocol):
|
|
510
|
+
cursor_block.block_cursor_clear()
|
|
511
|
+
cursor_block.block_cursor_up()
|
|
512
|
+
else:
|
|
513
|
+
if self.cursor_offset > 0:
|
|
514
|
+
self.cursor_offset -= 1
|
|
515
|
+
cursor_block = self.cursor_block
|
|
516
|
+
if isinstance(cursor_block, BlockProtocol):
|
|
517
|
+
cursor_block.block_cursor_clear()
|
|
518
|
+
cursor_block.block_cursor_up()
|
|
128
519
|
self._auto_follow = False
|
|
520
|
+
self._refresh_block_cursor()
|
|
521
|
+
|
|
522
|
+
def action_cursor_down(self) -> None:
|
|
523
|
+
children = self._selectable_children()
|
|
524
|
+
if not children or self.cursor_offset == -1:
|
|
525
|
+
return
|
|
526
|
+
cursor_block = self.cursor_block
|
|
527
|
+
if isinstance(cursor_block, BlockProtocol):
|
|
528
|
+
if cursor_block.block_cursor_down() is None:
|
|
529
|
+
if self.cursor_offset < len(children) - 1:
|
|
530
|
+
self.cursor_offset += 1
|
|
531
|
+
cursor_block = self.cursor_block
|
|
532
|
+
if isinstance(cursor_block, BlockProtocol):
|
|
533
|
+
cursor_block.block_cursor_clear()
|
|
534
|
+
cursor_block.block_cursor_down()
|
|
535
|
+
else:
|
|
536
|
+
if self.cursor_offset < len(children) - 1:
|
|
537
|
+
self.cursor_offset += 1
|
|
538
|
+
cursor_block = self.cursor_block
|
|
539
|
+
if isinstance(cursor_block, BlockProtocol):
|
|
540
|
+
cursor_block.block_cursor_clear()
|
|
541
|
+
cursor_block.block_cursor_down()
|
|
542
|
+
self._auto_follow = False
|
|
543
|
+
self._refresh_block_cursor()
|
|
544
|
+
|
|
545
|
+
async def action_select_block(self) -> None:
|
|
546
|
+
block = self.cursor_block
|
|
547
|
+
if block is None:
|
|
548
|
+
return
|
|
549
|
+
menu_options: list[MenuItem] = [
|
|
550
|
+
MenuItem("[u]C[/]opy to clipboard", "copy_to_clipboard", "c"),
|
|
551
|
+
]
|
|
552
|
+
if getattr(block, "ALLOW_MAXIMIZE", False):
|
|
553
|
+
menu_options.append(MenuItem("[u]M[/u]aximize", "maximize_block", "m"))
|
|
554
|
+
if isinstance(block, MenuProtocol):
|
|
555
|
+
menu_options.extend(block.get_block_menu())
|
|
556
|
+
if isinstance(block, ExpandProtocol):
|
|
557
|
+
if block.is_block_expanded():
|
|
558
|
+
menu_options.append(MenuItem("[u]C[/]ollapse", "collapse_block", "x"))
|
|
559
|
+
else:
|
|
560
|
+
menu_options.append(MenuItem("E[x]pand", "expand_block", "x"))
|
|
561
|
+
menu = Menu(block, menu_options)
|
|
129
562
|
try:
|
|
130
|
-
self.query_one("#
|
|
563
|
+
container = self.query_one("#content", VerticalGroup)
|
|
564
|
+
region = container.content_region
|
|
565
|
+
menu.styles.offset = Offset(
|
|
566
|
+
region.x + 1,
|
|
567
|
+
block.virtual_region.y + container.virtual_region.y,
|
|
568
|
+
)
|
|
131
569
|
except Exception:
|
|
132
570
|
pass
|
|
571
|
+
await self.mount(menu)
|
|
572
|
+
menu.focus()
|
|
573
|
+
self._active_menu = menu
|
|
133
574
|
|
|
134
|
-
|
|
575
|
+
@on(Menu.OptionSelected)
|
|
576
|
+
async def on_menu_option_selected(self, event: Menu.OptionSelected) -> None:
|
|
577
|
+
event.stop()
|
|
578
|
+
event.menu.display = False
|
|
579
|
+
if event.action is not None:
|
|
580
|
+
await self.run_action(event.action, {"block": event.owner})
|
|
581
|
+
self._active_menu = None
|
|
582
|
+
self.call_after_refresh(event.menu.remove)
|
|
583
|
+
|
|
584
|
+
@on(Menu.Dismissed)
|
|
585
|
+
async def on_menu_dismissed(self, event: Menu.Dismissed) -> None:
|
|
586
|
+
event.stop()
|
|
587
|
+
if event.menu.has_focus:
|
|
588
|
+
self.query_one("#scroll", VerticalScroll).focus(scroll_visible=False)
|
|
589
|
+
self._active_menu = None
|
|
590
|
+
await event.menu.remove()
|
|
591
|
+
|
|
592
|
+
def action_copy_to_clipboard(self, block: Widget | None = None) -> None:
|
|
593
|
+
if block is None:
|
|
594
|
+
block = self.cursor_block
|
|
595
|
+
if isinstance(block, MenuProtocol):
|
|
596
|
+
text = block.get_block_content("clipboard")
|
|
597
|
+
else:
|
|
598
|
+
text = None
|
|
599
|
+
if text:
|
|
600
|
+
self.app.copy_to_clipboard(text)
|
|
601
|
+
self.flash("Copied to clipboard")
|
|
602
|
+
|
|
603
|
+
def action_maximize_block(self) -> None:
|
|
604
|
+
if (block := self.cursor_block) is not None:
|
|
605
|
+
self.app.screen.maximize(block, container=False)
|
|
606
|
+
block.focus()
|
|
607
|
+
|
|
608
|
+
def action_expand_block(self) -> None:
|
|
609
|
+
if (block := self.cursor_block) is not None and isinstance(block, ExpandProtocol):
|
|
610
|
+
block.expand_block()
|
|
611
|
+
self._refresh_block_cursor()
|
|
612
|
+
|
|
613
|
+
def action_collapse_block(self) -> None:
|
|
614
|
+
if (block := self.cursor_block) is not None and isinstance(block, ExpandProtocol):
|
|
615
|
+
block.collapse_block()
|
|
616
|
+
self._refresh_block_cursor()
|
|
617
|
+
|
|
618
|
+
def action_toggle_expand(self) -> None:
|
|
619
|
+
block = self.cursor_block
|
|
620
|
+
if block is None:
|
|
621
|
+
# No cursor → fall back to "toggle latest completed thought"
|
|
622
|
+
try:
|
|
623
|
+
container = self.query_one("#content", VerticalGroup)
|
|
624
|
+
for child in reversed(container.children):
|
|
625
|
+
if isinstance(child, AgentThought) and child._completed:
|
|
626
|
+
child.action_toggle()
|
|
627
|
+
return
|
|
628
|
+
except Exception:
|
|
629
|
+
return
|
|
630
|
+
return
|
|
631
|
+
if isinstance(block, ExpandProtocol):
|
|
632
|
+
if block.is_block_expanded():
|
|
633
|
+
block.collapse_block()
|
|
634
|
+
else:
|
|
635
|
+
block.expand_block()
|
|
636
|
+
self._refresh_block_cursor()
|
|
637
|
+
|
|
638
|
+
# ── Title / Footer ──
|
|
639
|
+
|
|
640
|
+
def _update_title(self) -> None:
|
|
641
|
+
sa = self.subagent
|
|
642
|
+
status_icon = {"running": "🔄", "completed": "✔", "failed": "✗"}.get(sa._status, "?")
|
|
643
|
+
self.title = f"{status_icon} Subagent ({sa.agent_type})"
|
|
644
|
+
chunks = len("".join(sa._chunks))
|
|
645
|
+
tools = len(sa._tool_calls)
|
|
646
|
+
parts = [sa._status]
|
|
647
|
+
if chunks:
|
|
648
|
+
parts.append(f"{chunks}B")
|
|
649
|
+
if tools:
|
|
650
|
+
parts.append(f"{tools} tool calls")
|
|
651
|
+
self.sub_title = " | ".join(parts)
|
|
652
|
+
|
|
653
|
+
def _update_footer(self) -> None:
|
|
135
654
|
try:
|
|
136
|
-
|
|
137
|
-
|
|
655
|
+
self.query_one("#footer", Static).update(
|
|
656
|
+
"[$text-muted]esc[/] 返回 "
|
|
657
|
+
"[$text-muted]ctrl+j/k[/] 移动 "
|
|
658
|
+
"[$text-muted]enter[/] 菜单 "
|
|
659
|
+
"[$text-muted]ctrl+x[/] 折叠 "
|
|
660
|
+
"[$text-muted]↑↓[/] 滚动"
|
|
661
|
+
)
|
|
138
662
|
except Exception:
|
|
139
663
|
pass
|
|
140
664
|
|
|
141
|
-
|
|
142
|
-
|
|
665
|
+
# ── Scroll actions ──
|
|
666
|
+
|
|
667
|
+
def _scroll_widget(self) -> VerticalScroll | None:
|
|
143
668
|
try:
|
|
144
|
-
self.query_one("#
|
|
669
|
+
return self.query_one("#scroll", VerticalScroll)
|
|
145
670
|
except Exception:
|
|
146
|
-
|
|
671
|
+
return None
|
|
672
|
+
|
|
673
|
+
def action_scroll_up(self) -> None:
|
|
674
|
+
self._auto_follow = False
|
|
675
|
+
if w := self._scroll_widget():
|
|
676
|
+
w.scroll_up()
|
|
677
|
+
|
|
678
|
+
def action_scroll_down(self) -> None:
|
|
679
|
+
if w := self._scroll_widget():
|
|
680
|
+
w.scroll_down()
|
|
681
|
+
|
|
682
|
+
def action_scroll_page_up(self) -> None:
|
|
683
|
+
self._auto_follow = False
|
|
684
|
+
if w := self._scroll_widget():
|
|
685
|
+
w.scroll_page_up()
|
|
686
|
+
|
|
687
|
+
def action_scroll_page_down(self) -> None:
|
|
688
|
+
if w := self._scroll_widget():
|
|
689
|
+
w.scroll_page_down()
|
|
690
|
+
|
|
691
|
+
def action_scroll_home(self) -> None:
|
|
692
|
+
self._auto_follow = False
|
|
693
|
+
if w := self._scroll_widget():
|
|
694
|
+
w.scroll_home()
|
|
147
695
|
|
|
148
696
|
def action_scroll_end(self) -> None:
|
|
149
697
|
self._auto_follow = True
|
|
698
|
+
if w := self._scroll_widget():
|
|
699
|
+
w.scroll_end(animate=True)
|
|
700
|
+
|
|
701
|
+
# ── Scroll tracking ──
|
|
702
|
+
|
|
703
|
+
def on_scroll(self, event) -> None:
|
|
150
704
|
try:
|
|
151
|
-
self.query_one("#
|
|
705
|
+
scroll = self.query_one("#scroll", VerticalScroll)
|
|
706
|
+
at_end = scroll.scroll_y >= scroll.max_scroll_y - 1
|
|
707
|
+
self._auto_follow = at_end
|
|
152
708
|
except Exception:
|
|
153
|
-
pass
|
|
154
|
-
|
|
155
|
-
def _build_content(self) -> str:
|
|
156
|
-
sa = self.subagent
|
|
157
|
-
parts: list[str] = []
|
|
158
|
-
parts.append(f"# @ {sa.agent_type} [{sa._status}]\n")
|
|
159
|
-
if sa.prompt:
|
|
160
|
-
prompt_preview = sa.prompt if len(sa.prompt) <= 200 else sa.prompt[:200] + "..."
|
|
161
|
-
parts.append(
|
|
162
|
-
"\n## 任务\n\n"
|
|
163
|
-
f"> {prompt_preview}\n\n"
|
|
164
|
-
"---\n"
|
|
165
|
-
)
|
|
166
|
-
if sa._thinking_chunks:
|
|
167
|
-
parts.append("## 思考\n")
|
|
168
|
-
parts.append("".join(sa._thinking_chunks))
|
|
169
|
-
parts.append("\n\n---\n\n")
|
|
170
|
-
raw = "".join(sa._chunks)
|
|
171
|
-
if sa._status == "failed":
|
|
172
|
-
parts.append(
|
|
173
|
-
f"## ❌ Failed\n\n"
|
|
174
|
-
f"**Error:** {sa._error}\n\n"
|
|
175
|
-
"---\n\n"
|
|
176
|
-
"### Partial output\n\n"
|
|
177
|
-
f"```\n{(raw or '(none)')[:4000]}\n```"
|
|
178
|
-
)
|
|
179
|
-
elif raw:
|
|
180
|
-
parts.append("## 输出\n\n")
|
|
181
|
-
parts.append(raw)
|
|
182
|
-
if sa._status == "running":
|
|
183
|
-
parts.append("\n\n*…streaming*")
|
|
184
|
-
else:
|
|
185
|
-
parts.append("## 输出\n\n_(streaming…)_")
|
|
186
|
-
return "".join(parts)
|
|
709
|
+
pass
|