xg-cli 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. xg/__init__.py +3 -0
  2. xg/__main__.py +3 -0
  3. xg/adaptive/__init__.py +27 -0
  4. xg/adaptive/calibrate.py +191 -0
  5. xg/adaptive/feedback.py +170 -0
  6. xg/adaptive/learned_rules.py +284 -0
  7. xg/adaptive/signals.py +125 -0
  8. xg/adaptive/store.py +164 -0
  9. xg/agent/__init__.py +0 -0
  10. xg/agent/plan.py +631 -0
  11. xg/agent/react.py +268 -0
  12. xg/agent/team.py +1793 -0
  13. xg/assets/router.lgb +0 -0
  14. xg/assets/router_semantics.json +21278 -0
  15. xg/assets/router_semantics.onnx +0 -0
  16. xg/cli/__init__.py +0 -0
  17. xg/cli/app.py +1372 -0
  18. xg/cli/commands.py +921 -0
  19. xg/cli/completion.py +740 -0
  20. xg/cli/help.py +185 -0
  21. xg/cli/train.py +200 -0
  22. xg/config/__init__.py +0 -0
  23. xg/config/env_writer.py +121 -0
  24. xg/config/manager.py +446 -0
  25. xg/config/mcp.py +229 -0
  26. xg/config/provider_service.py +267 -0
  27. xg/config/providers.py +46 -0
  28. xg/config/settings.py +258 -0
  29. xg/config/skills.py +100 -0
  30. xg/config/smart_router_service.py +121 -0
  31. xg/config/web.py +140 -0
  32. xg/input_history/__init__.py +7 -0
  33. xg/input_history/models.py +28 -0
  34. xg/input_history/persistence.py +126 -0
  35. xg/input_history/policy.py +39 -0
  36. xg/input_history/prompt_toolkit.py +36 -0
  37. xg/input_history/store.py +118 -0
  38. xg/llm/__init__.py +0 -0
  39. xg/llm/client.py +49 -0
  40. xg/llm/factory.py +40 -0
  41. xg/llm/openai_compat.py +275 -0
  42. xg/llm/types.py +98 -0
  43. xg/mcp/__init__.py +4 -0
  44. xg/mcp/http.py +192 -0
  45. xg/mcp/manager.py +726 -0
  46. xg/mcp/models.py +86 -0
  47. xg/mcp/protocol.py +62 -0
  48. xg/mcp/resources.py +72 -0
  49. xg/mcp/schema.py +137 -0
  50. xg/mcp/stdio.py +210 -0
  51. xg/mcp/transport.py +66 -0
  52. xg/memory/__init__.py +15 -0
  53. xg/memory/context.py +327 -0
  54. xg/memory/manager.py +111 -0
  55. xg/memory/models.py +41 -0
  56. xg/memory/project.py +187 -0
  57. xg/memory/store.py +144 -0
  58. xg/router/__init__.py +124 -0
  59. xg/router/features.py +66 -0
  60. xg/router/keywords.py +50 -0
  61. xg/router/ml_router.py +178 -0
  62. xg/router/model_tiers.py +73 -0
  63. xg/router/postprocess.py +167 -0
  64. xg/router/rule_router.py +77 -0
  65. xg/router/semantic.py +138 -0
  66. xg/safety/__init__.py +0 -0
  67. xg/safety/audit.py +96 -0
  68. xg/safety/guards.py +106 -0
  69. xg/safety/hitl.py +73 -0
  70. xg/skill/__init__.py +9 -0
  71. xg/skill/errors.py +45 -0
  72. xg/skill/loader.py +42 -0
  73. xg/skill/models.py +57 -0
  74. xg/skill/parser.py +93 -0
  75. xg/skill/policy.py +40 -0
  76. xg/skill/prompt.py +45 -0
  77. xg/skill/registry.py +169 -0
  78. xg/tool/__init__.py +0 -0
  79. xg/tool/builtin.py +356 -0
  80. xg/tool/registry.py +228 -0
  81. xg/tui/__init__.py +34 -0
  82. xg/tui/app.py +612 -0
  83. xg/tui/controller.py +1296 -0
  84. xg/tui/diagrams/__init__.py +22 -0
  85. xg/tui/diagrams/layout.py +110 -0
  86. xg/tui/diagrams/markdown.py +39 -0
  87. xg/tui/diagrams/model.py +36 -0
  88. xg/tui/diagrams/parser.py +119 -0
  89. xg/tui/diagrams/renderer.py +551 -0
  90. xg/tui/i18n.py +169 -0
  91. xg/tui/messages.py +45 -0
  92. xg/tui/plan_renderables.py +147 -0
  93. xg/tui/reducer.py +1029 -0
  94. xg/tui/renderables.py +252 -0
  95. xg/tui/state.py +240 -0
  96. xg/tui/theme.tcss +208 -0
  97. xg/tui/widgets/__init__.py +1 -0
  98. xg/tui/widgets/action_card.py +94 -0
  99. xg/tui/widgets/agent_group_card.py +39 -0
  100. xg/tui/widgets/approval_modal.py +59 -0
  101. xg/tui/widgets/collapsible_card.py +40 -0
  102. xg/tui/widgets/command_suggestions.py +128 -0
  103. xg/tui/widgets/composer.py +151 -0
  104. xg/tui/widgets/config_panel.py +198 -0
  105. xg/tui/widgets/confirm_modal.py +31 -0
  106. xg/tui/widgets/footer.py +9 -0
  107. xg/tui/widgets/header.py +118 -0
  108. xg/tui/widgets/inspector.py +378 -0
  109. xg/tui/widgets/plan_modal.py +53 -0
  110. xg/tui/widgets/provider_form.py +141 -0
  111. xg/tui/widgets/queue_status.py +30 -0
  112. xg/tui/widgets/smart_router_form.py +95 -0
  113. xg/tui/widgets/transcript.py +354 -0
  114. xg/tui/workers.py +14 -0
  115. xg/web/__init__.py +21 -0
  116. xg/web/errors.py +57 -0
  117. xg/web/extract.py +127 -0
  118. xg/web/fetch.py +106 -0
  119. xg/web/markdown.py +77 -0
  120. xg/web/models.py +91 -0
  121. xg/web/providers.py +79 -0
  122. xg/web/search.py +118 -0
  123. xg/web/searxng.py +27 -0
  124. xg/web/serpapi.py +29 -0
  125. xg/web/url_policy.py +110 -0
  126. xg/web/zhipu.py +27 -0
  127. xg_cli-1.0.dist-info/METADATA +284 -0
  128. xg_cli-1.0.dist-info/RECORD +130 -0
  129. xg_cli-1.0.dist-info/WHEEL +4 -0
  130. xg_cli-1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,94 @@
1
+ """Inline action cards used for all TUI confirmations.
2
+
3
+ Cards are read-only text: context, risk and the input protocol. Decisions
4
+ are always typed into the main Composer, so no Button or inner Input lives
5
+ here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ from textual.app import ComposeResult
13
+ from textual.containers import Vertical
14
+ from textual.widgets import Static
15
+
16
+ from xg.tui.state import ApprovalRequest, ConfirmationRequest
17
+
18
+ APPROVAL_HINT = "输入 y 批准 · a 全部放行 · r 拒绝 · s 跳过 · e 修改参数"
19
+ CONFIRMATION_HINTS = {
20
+ "init": "输入 y 确认写入 · n 取消",
21
+ "memory_clear": "输入 clear 确认清空 · 其他输入取消",
22
+ }
23
+
24
+
25
+ class InlineConfirmationCard(Vertical):
26
+ def __init__(self, request: ConfirmationRequest) -> None:
27
+ # Per-kind class lets the theme give each confirmation its own
28
+ # border color while sharing one simple layout. No fixed widget id:
29
+ # TranscriptView keeps this card stable and updates its content in
30
+ # place, so no nested input or duplicate child id is required.
31
+ super().__init__(
32
+ classes=f"inline-action-card inline-confirmation-card confirmation-{request.kind}",
33
+ )
34
+ self.request = request
35
+ # The Static is created eagerly: compose() runs asynchronously, so
36
+ # updates must not depend on the card being mounted already.
37
+ hint = CONFIRMATION_HINTS.get(self.request.kind, "输入 y 确认 · n 取消")
38
+ self._text = Static(f"{self.request.title}\n\n{self.request.body}\n\n{hint}")
39
+
40
+ def compose(self) -> ComposeResult:
41
+ yield self._text
42
+
43
+ def update_request(self, request: ConfirmationRequest) -> None:
44
+ """Update the stable card without replacing its DOM node."""
45
+ self.request = request
46
+ hint = CONFIRMATION_HINTS.get(self.request.kind, "输入 y 确认 · n 取消")
47
+ self._text.update(f"{self.request.title}\n\n{self.request.body}\n\n{hint}")
48
+
49
+
50
+ class InlineApprovalCard(Vertical):
51
+ def __init__(self, request: ApprovalRequest) -> None:
52
+ super().__init__(classes="inline-action-card inline-approval-card")
53
+ self.request = request
54
+ self._mode = ""
55
+ self._modified_args: dict | None = None
56
+ self._text = Static(self._render_text(), classes="inline-approval-text")
57
+
58
+ def compose(self) -> ComposeResult:
59
+ yield self._text
60
+
61
+ def update_request(self, request: ApprovalRequest) -> None:
62
+ """Update request data while preserving the card and its mode."""
63
+ self.request = request
64
+ self._text.update(self._render_text())
65
+
66
+ def set_mode(self, mode: str = "", modified_args: dict | None = None) -> None:
67
+ """Switch the card text; empty mode means waiting for the decision."""
68
+ self._mode = mode
69
+ self._modified_args = modified_args
70
+ self._text.update(self._render_text())
71
+
72
+ def _render_text(self) -> str:
73
+ if self._mode == "approval_edit":
74
+ return (
75
+ f"需要人工审批:{self.request.tool_name}\n\n"
76
+ "请输入修改后的完整 JSON 参数。\n"
77
+ "修改后的参数仍会经过 PathGuard / CommandGuard 检查。\n"
78
+ "输入 Esc 取消修改。"
79
+ )
80
+ if self._mode == "approval_confirm_modified":
81
+ args = json.dumps(self._modified_args or {}, ensure_ascii=False, indent=2)
82
+ return (
83
+ f"需要人工审批:{self.request.tool_name}\n\n"
84
+ f"参数已修改为:\n{args}\n\n"
85
+ "输入 y 确认执行 · r 拒绝执行 · Esc 取消"
86
+ )
87
+ args = json.dumps(self.request.args, ensure_ascii=False, indent=2)
88
+ return (
89
+ "需要人工审批\n"
90
+ f"工具:{self.request.tool_name}\n"
91
+ f"敏感级别:{self.request.level}\n"
92
+ f"当前参数:\n{args}\n\n"
93
+ f"{APPROVAL_HINT}"
94
+ )
@@ -0,0 +1,39 @@
1
+ """Collapsible conversation group for one Team AgentRun."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.events import Click
6
+ from textual.widgets import Static
7
+
8
+ from xg.tui.messages import AgentGroupToggled
9
+ from xg.tui.renderables import agent_group_renderable
10
+ from xg.tui.state import AgentGroupState
11
+
12
+
13
+ class AgentGroupCard(Static):
14
+ """One Agent conversation inside the shared TranscriptView."""
15
+
16
+ can_focus = True
17
+ BINDINGS = [
18
+ ("enter", "toggle", "展开/折叠 Agent"),
19
+ ("space", "toggle", "展开/折叠 Agent"),
20
+ ]
21
+
22
+ def __init__(self, group: AgentGroupState) -> None:
23
+ self.group_id = group.group_id
24
+ self.group = group
25
+ super().__init__(agent_group_renderable(group))
26
+
27
+ def update_group(self, group: AgentGroupState) -> None:
28
+ self.group = group
29
+ self.update(agent_group_renderable(group))
30
+
31
+ def _toggle(self) -> None:
32
+ self.post_message(AgentGroupToggled(self.group_id))
33
+
34
+ def on_click(self, event: Click) -> None:
35
+ event.stop()
36
+ self._toggle()
37
+
38
+ def action_toggle(self) -> None:
39
+ self._toggle()
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Vertical
7
+ from textual.screen import ModalScreen
8
+ from textual.widgets import Button, Input, Static
9
+
10
+ from xg.safety.hitl import ApprovalDecision
11
+ from xg.tui.state import ApprovalRequest
12
+
13
+
14
+ class ApprovalModal(ModalScreen[str]):
15
+ BINDINGS = [("escape", "reject", "拒绝"), ("enter", "approve", "批准"), ("y", "approve", "批准"), ("a", "allow_all", "全部放行"), ("r", "reject", "拒绝"), ("s", "skip", "跳过"), ("e", "edit", "修改参数")]
16
+
17
+ def __init__(self, request: ApprovalRequest) -> None:
18
+ super().__init__()
19
+ self.request = request
20
+
21
+ def compose(self) -> ComposeResult:
22
+ args = json.dumps(self.request.args, ensure_ascii=False, indent=2)
23
+ with Vertical(id="approval-dialog"):
24
+ yield Static(f"需要审批:{self.request.tool_name}\n敏感级别:{self.request.level}\n\n{args}")
25
+ yield Input(placeholder="按 e 修改 JSON 参数", id="approval-json")
26
+ yield Button("批准 (Enter/y)", id="approve", variant="success")
27
+ yield Button("本会话全部放行 (a)", id="allow-all")
28
+ yield Button("拒绝 (r/Esc)", id="reject", variant="error")
29
+
30
+ def _close(self, value: str) -> None:
31
+ self.dismiss(value)
32
+
33
+ def action_approve(self) -> None:
34
+ self._close("approve")
35
+
36
+ def action_allow_all(self) -> None:
37
+ self._close("allow_all")
38
+
39
+ def action_reject(self) -> None:
40
+ self._close("reject")
41
+
42
+ def action_skip(self) -> None:
43
+ self._close("skip")
44
+
45
+ def action_edit(self) -> None:
46
+ self.query_one("#approval-json", Input).focus()
47
+
48
+ def on_input_submitted(self, event: Input.Submitted) -> None:
49
+ if event.input.id != "approval-json":
50
+ return
51
+ try:
52
+ json.loads(event.value)
53
+ except json.JSONDecodeError:
54
+ event.input.value = ""
55
+ return
56
+ self._close("modify:" + event.value)
57
+
58
+ def on_button_pressed(self, event: Button.Pressed) -> None:
59
+ self._close(event.button.id or "reject")
@@ -0,0 +1,40 @@
1
+ """Interactive execution-trace card for the fullscreen TUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.events import Click
6
+ from textual.widgets import Static
7
+
8
+ from xg.tui.messages import TraceCardToggled
9
+ from xg.tui.renderables import trace_renderable
10
+ from xg.tui.state import TranscriptItem
11
+
12
+
13
+ class CollapsibleCard(Static):
14
+ """State-driven card whose header/body can be toggled by mouse or key."""
15
+
16
+ can_focus = True
17
+ BINDINGS = [
18
+ ("enter", "toggle", "展开/折叠"),
19
+ ("space", "toggle", "展开/折叠"),
20
+ ]
21
+
22
+ def __init__(self, item: TranscriptItem) -> None:
23
+ self.item_id = item.id
24
+ self.item = item
25
+ super().__init__(trace_renderable(item))
26
+
27
+ def update_item(self, item: TranscriptItem) -> None:
28
+ self.item = item
29
+ self.update(trace_renderable(item))
30
+
31
+ def _toggle(self) -> None:
32
+ self.post_message(TraceCardToggled(self.item_id))
33
+
34
+ def on_click(self, event: Click) -> None:
35
+ event.stop()
36
+ self._toggle()
37
+
38
+ def action_toggle(self) -> None:
39
+ self._toggle()
40
+
@@ -0,0 +1,128 @@
1
+ """Local slash-command suggestions for the Composer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.text import Text
6
+ from textual.widgets import OptionList
7
+
8
+ from xg.cli.commands import SLASH_COMMANDS, SlashCommandSpec
9
+ from xg.cli.completion import (
10
+ CompletionCandidate,
11
+ CompletionProviderRegistry,
12
+ completion_candidates,
13
+ dynamic_candidates,
14
+ )
15
+ from xg.tui.messages import CommandSuggestionSelected
16
+
17
+
18
+ class CommandSuggestions(OptionList):
19
+ """A non-focusable, mouse-selectable list above the Composer.
20
+
21
+ The list shows layered candidates (command / subcommand / static option /
22
+ dynamic value) in front of a thin horizontal rule... The displayed text
23
+ keeps the candidate's insert_text separate, so help text never leaks into
24
+ the value.
25
+ """
26
+
27
+ can_focus = False
28
+
29
+ def __init__(self) -> None:
30
+ super().__init__(id="command-suggestions", markup=False)
31
+ self._visible_candidates: list[CompletionCandidate] = []
32
+ self._revision = 0
33
+ self._registry: CompletionProviderRegistry | None = None
34
+ self.display = False
35
+
36
+ def set_dynamic_registry(self, registry: CompletionProviderRegistry | None) -> None:
37
+ """Attach the runtime provider registry for dynamic value candidates."""
38
+ self._registry = registry
39
+
40
+ @property
41
+ def visible_candidates(self) -> list[CompletionCandidate]:
42
+ return self._visible_candidates
43
+
44
+ @property
45
+ def visible_specs(self) -> tuple[SlashCommandSpec, ...]:
46
+ """Top-level command specs (backward-compatible accessor).
47
+
48
+ Only meaningful while the candidate list is a pure command list; empty
49
+ otherwise. New callers should use ``visible_candidates`` instead.
50
+ """
51
+ return tuple(
52
+ spec
53
+ for cand in self._visible_candidates
54
+ if cand.kind == "command"
55
+ for spec in SLASH_COMMANDS
56
+ if spec.name == cand.insert_text
57
+ )
58
+
59
+ @property
60
+ def is_open(self) -> bool:
61
+ return self.display and bool(self._visible_candidates)
62
+
63
+ def update_query(self, query: str, cursor_position: int | None = None) -> None:
64
+ """Refresh layered candidates (static + dynamic) for the input line.
65
+
66
+ Static candidates come first, then dynamic value candidates (the
67
+ providers are synchronous and read only in-memory agent state), then
68
+ the list is re-rendered. Dynamic updates use a per-query revision so
69
+ a stale refresh from an older input never clobbers a newer one.
70
+ """
71
+ revision = self._revision + 1
72
+ self._revision = revision
73
+ static = completion_candidates(query, cursor_position)
74
+ dynamic = (
75
+ dynamic_candidates(query, cursor_position, self._registry)
76
+ if self._registry is not None
77
+ else []
78
+ )
79
+ candidates = list(static) + dynamic
80
+ # Defensive: if a newer update_query() bumped the revision before we
81
+ # applied this result, drop it. Synchronous path never races in
82
+ # practice, but the guard keeps a stale refresh from winning.
83
+ if self._revision != revision:
84
+ return
85
+ self._visible_candidates = candidates
86
+ if not candidates:
87
+ self.set_options(())
88
+ self.display = False
89
+ return
90
+
91
+ self.set_options(self._render_candidate(cand) for cand in candidates)
92
+ self.highlighted = 0
93
+ self.display = True
94
+
95
+ def close(self) -> None:
96
+ """Hide suggestions without changing the Composer value."""
97
+
98
+ self.display = False
99
+
100
+ def move_selection(self, direction: int) -> None:
101
+ if not self._visible_candidates:
102
+ return
103
+ current = self.highlighted if self.highlighted is not None else 0
104
+ self.highlighted = (current + direction) % len(self._visible_candidates)
105
+ self.scroll_to_highlight()
106
+
107
+ def selected_candidate(self) -> CompletionCandidate | None:
108
+ if self.highlighted is None or not self._visible_candidates:
109
+ return None
110
+ return self._visible_candidates[self.highlighted]
111
+
112
+ @staticmethod
113
+ def _render_candidate(candidate: CompletionCandidate) -> Text:
114
+ text = Text()
115
+ text.append(candidate.label, style="bold")
116
+ if candidate.detail:
117
+ text.append(" ")
118
+ text.append(candidate.detail, style="dim")
119
+ return text
120
+
121
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
122
+ """Turn a mouse click into a Composer fill operation."""
123
+
124
+ if 0 <= event.option_index < len(self._visible_candidates):
125
+ event.stop()
126
+ self.post_message(
127
+ CommandSuggestionSelected(self._visible_candidates[event.option_index])
128
+ )
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from textual.events import Key
6
+ from textual.widgets import Input
7
+
8
+ from xg.input_history import InputHistory
9
+ from xg.tui.widgets.command_suggestions import CommandSuggestions
10
+
11
+
12
+ class Composer(Input):
13
+ """Single-line Composer with bounded XG input history navigation."""
14
+
15
+ BINDINGS = [("escape", "escape_input", "清除输入或取消当前操作")]
16
+
17
+ def __init__(self) -> None:
18
+ super().__init__(placeholder="输入任务或 /help …", id="composer")
19
+ self.suggestions_enabled = True
20
+ self.input_history: InputHistory | None = None
21
+
22
+ def set_input_history(self, history: InputHistory | None) -> None:
23
+ self.input_history = history
24
+
25
+ def record_submission(self, text: str, accepted: bool) -> None:
26
+ if self.input_history is None:
27
+ return
28
+ if accepted:
29
+ self.input_history.add(text)
30
+ else:
31
+ self.input_history.reset_cursor()
32
+
33
+ def reset_history_cursor(self) -> None:
34
+ if self.input_history is not None:
35
+ self.input_history.reset_cursor()
36
+
37
+ def _history_blocked(self, app) -> bool:
38
+ if getattr(app, "_replan_mode", False):
39
+ return True
40
+ controller = getattr(app, "controller", None)
41
+ state = getattr(controller, "state", None)
42
+ return bool(
43
+ getattr(app, "_has_pending_plan", lambda: False)()
44
+ or getattr(state, "pending_approval", None) is not None
45
+ or getattr(state, "pending_confirmation", None) is not None
46
+ )
47
+
48
+ def _navigate_history(self, direction: str) -> None:
49
+ if self.input_history is None:
50
+ return
51
+ suggestions = self._suggestions()
52
+ if suggestions is not None:
53
+ suggestions.close()
54
+ value = (
55
+ self.input_history.previous(self.value)
56
+ if direction == "previous"
57
+ else self.input_history.next()
58
+ )
59
+ self.value = value
60
+ self.cursor_position = len(value)
61
+ self.focus()
62
+
63
+ def set_suggestions_enabled(self, enabled: bool) -> None:
64
+ self.suggestions_enabled = enabled
65
+
66
+ def _suggestions(self) -> CommandSuggestions | None:
67
+ try:
68
+ return self.app.query_one("#command-suggestions", CommandSuggestions)
69
+ except Exception:
70
+ # Input can receive an event while the app is mounting or
71
+ # unmounting. In that short window there is no suggestion widget.
72
+ return None
73
+
74
+ def on_input_changed(self, event: Input.Changed) -> None:
75
+ """Keep filtering local and leave submission to the App."""
76
+ if event.input is not self or not self.suggestions_enabled:
77
+ return
78
+ suggestions = self._suggestions()
79
+ if suggestions is not None:
80
+ suggestions.update_query(event.value, self.cursor_position)
81
+
82
+ def on_key(self, event: Key) -> None:
83
+ """Keep plan-review shortcuts working while the input stays enabled."""
84
+ app = self.app
85
+ suggestions = self._suggestions()
86
+ if suggestions is not None and suggestions.is_open:
87
+ if event.key == "up":
88
+ event.prevent_default()
89
+ event.stop()
90
+ suggestions.move_selection(-1)
91
+ return
92
+ if event.key == "down":
93
+ event.prevent_default()
94
+ event.stop()
95
+ suggestions.move_selection(1)
96
+ return
97
+ if event.key == "tab":
98
+ candidate = suggestions.selected_candidate()
99
+ if candidate is not None:
100
+ event.prevent_default()
101
+ event.stop()
102
+ app.complete_command_suggestion(candidate)
103
+ return
104
+ if event.key == "escape":
105
+ event.prevent_default()
106
+ event.stop()
107
+ suggestions.close()
108
+ return
109
+
110
+ has_pending_plan = getattr(app, "_has_pending_plan", lambda: False)()
111
+ if not self._history_blocked(app) and event.key == "up":
112
+ event.prevent_default()
113
+ event.stop()
114
+ self._navigate_history("previous")
115
+ return
116
+ if not self._history_blocked(app) and event.key == "down":
117
+ event.prevent_default()
118
+ event.stop()
119
+ self._navigate_history("next")
120
+ return
121
+ if not has_pending_plan or getattr(app, "_replan_mode", False):
122
+ if (
123
+ event.key == "d"
124
+ and not self.value
125
+ and getattr(app, "_has_detail_toggle_target", lambda: False)()
126
+ ):
127
+ event.prevent_default()
128
+ event.stop()
129
+ app.action_plan_details()
130
+ return
131
+ if event.key == "r":
132
+ event.prevent_default()
133
+ event.stop()
134
+ app.action_plan_replan()
135
+ elif event.key == "d":
136
+ event.prevent_default()
137
+ event.stop()
138
+ app.action_plan_details()
139
+ elif event.key == "enter":
140
+ event.prevent_default()
141
+ event.stop()
142
+ app.action_plan_execute()
143
+ elif event.key == "escape":
144
+ event.prevent_default()
145
+ event.stop()
146
+ asyncio.create_task(app.action_escape())
147
+
148
+ async def action_escape_input(self) -> None:
149
+ # Input consumes Escape before it bubbles to the App. Delegate to the
150
+ # app so idle, running, plan-review and replan states share one policy.
151
+ await self.app.action_escape()