iac-code 0.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 (184) hide show
  1. iac_code/__init__.py +2 -0
  2. iac_code/acp/__init__.py +97 -0
  3. iac_code/acp/convert.py +423 -0
  4. iac_code/acp/http_sse.py +448 -0
  5. iac_code/acp/mcp.py +54 -0
  6. iac_code/acp/metrics.py +71 -0
  7. iac_code/acp/server.py +662 -0
  8. iac_code/acp/session.py +446 -0
  9. iac_code/acp/slash_registry.py +125 -0
  10. iac_code/acp/state.py +99 -0
  11. iac_code/acp/tools.py +112 -0
  12. iac_code/acp/types.py +13 -0
  13. iac_code/acp/version.py +26 -0
  14. iac_code/agent/__init__.py +19 -0
  15. iac_code/agent/agent_loop.py +640 -0
  16. iac_code/agent/agent_tool.py +269 -0
  17. iac_code/agent/agent_types.py +87 -0
  18. iac_code/agent/message.py +153 -0
  19. iac_code/agent/system_prompt.py +313 -0
  20. iac_code/cli/__init__.py +3 -0
  21. iac_code/cli/headless.py +114 -0
  22. iac_code/cli/main.py +246 -0
  23. iac_code/cli/output_formats.py +125 -0
  24. iac_code/commands/__init__.py +93 -0
  25. iac_code/commands/auth.py +1055 -0
  26. iac_code/commands/clear.py +34 -0
  27. iac_code/commands/compact.py +43 -0
  28. iac_code/commands/debug.py +45 -0
  29. iac_code/commands/effort.py +116 -0
  30. iac_code/commands/exit.py +10 -0
  31. iac_code/commands/help.py +49 -0
  32. iac_code/commands/model.py +130 -0
  33. iac_code/commands/registry.py +245 -0
  34. iac_code/commands/resume.py +49 -0
  35. iac_code/commands/tasks.py +41 -0
  36. iac_code/config.py +304 -0
  37. iac_code/i18n/__init__.py +141 -0
  38. iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
  39. iac_code/memory/__init__.py +1 -0
  40. iac_code/memory/memory_manager.py +92 -0
  41. iac_code/memory/memory_tools.py +88 -0
  42. iac_code/providers/__init__.py +1 -0
  43. iac_code/providers/anthropic_provider.py +284 -0
  44. iac_code/providers/base.py +128 -0
  45. iac_code/providers/dashscope_provider.py +47 -0
  46. iac_code/providers/deepseek_provider.py +36 -0
  47. iac_code/providers/manager.py +399 -0
  48. iac_code/providers/openai_provider.py +344 -0
  49. iac_code/providers/retry.py +58 -0
  50. iac_code/providers/stream_watchdog.py +47 -0
  51. iac_code/providers/thinking.py +164 -0
  52. iac_code/services/__init__.py +1 -0
  53. iac_code/services/agent_factory.py +127 -0
  54. iac_code/services/cloud_credentials.py +22 -0
  55. iac_code/services/context_manager.py +221 -0
  56. iac_code/services/providers/__init__.py +1 -0
  57. iac_code/services/providers/aliyun.py +232 -0
  58. iac_code/services/session_index.py +281 -0
  59. iac_code/services/session_storage.py +245 -0
  60. iac_code/services/telemetry/__init__.py +66 -0
  61. iac_code/services/telemetry/attributes.py +84 -0
  62. iac_code/services/telemetry/client.py +330 -0
  63. iac_code/services/telemetry/config.py +76 -0
  64. iac_code/services/telemetry/constants.py +75 -0
  65. iac_code/services/telemetry/content_serializer.py +124 -0
  66. iac_code/services/telemetry/events.py +42 -0
  67. iac_code/services/telemetry/fallback.py +59 -0
  68. iac_code/services/telemetry/identity.py +73 -0
  69. iac_code/services/telemetry/metrics.py +62 -0
  70. iac_code/services/telemetry/names.py +199 -0
  71. iac_code/services/telemetry/sanitize.py +88 -0
  72. iac_code/services/telemetry/sink.py +67 -0
  73. iac_code/services/telemetry/tracing.py +38 -0
  74. iac_code/services/telemetry/types.py +13 -0
  75. iac_code/services/token_budget.py +54 -0
  76. iac_code/services/token_counter.py +76 -0
  77. iac_code/skills/__init__.py +1 -0
  78. iac_code/skills/bundled/__init__.py +94 -0
  79. iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
  80. iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
  81. iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
  82. iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
  83. iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
  84. iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
  85. iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
  86. iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
  87. iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
  88. iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
  89. iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
  90. iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
  91. iac_code/skills/bundled/simplify.py +28 -0
  92. iac_code/skills/discovery.py +136 -0
  93. iac_code/skills/frontmatter.py +119 -0
  94. iac_code/skills/listing.py +92 -0
  95. iac_code/skills/loader.py +42 -0
  96. iac_code/skills/processor.py +81 -0
  97. iac_code/skills/renderer.py +157 -0
  98. iac_code/skills/skill_definition.py +82 -0
  99. iac_code/skills/skill_tool.py +261 -0
  100. iac_code/state/__init__.py +5 -0
  101. iac_code/state/app_state.py +122 -0
  102. iac_code/tasks/__init__.py +1 -0
  103. iac_code/tasks/notification_queue.py +28 -0
  104. iac_code/tasks/task_state.py +66 -0
  105. iac_code/tasks/task_tools.py +114 -0
  106. iac_code/tools/__init__.py +8 -0
  107. iac_code/tools/base.py +226 -0
  108. iac_code/tools/bash.py +133 -0
  109. iac_code/tools/cloud/__init__.py +0 -0
  110. iac_code/tools/cloud/aliyun/__init__.py +0 -0
  111. iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
  112. iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
  113. iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
  114. iac_code/tools/cloud/aliyun/ros_client.py +56 -0
  115. iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
  116. iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
  117. iac_code/tools/cloud/base_api.py +162 -0
  118. iac_code/tools/cloud/base_stack.py +242 -0
  119. iac_code/tools/cloud/registry.py +20 -0
  120. iac_code/tools/cloud/types.py +105 -0
  121. iac_code/tools/edit_file.py +121 -0
  122. iac_code/tools/glob.py +103 -0
  123. iac_code/tools/grep.py +254 -0
  124. iac_code/tools/list_files.py +104 -0
  125. iac_code/tools/read_file.py +127 -0
  126. iac_code/tools/result_storage.py +39 -0
  127. iac_code/tools/tool_executor.py +165 -0
  128. iac_code/tools/web_fetch.py +177 -0
  129. iac_code/tools/write_file.py +88 -0
  130. iac_code/types/__init__.py +40 -0
  131. iac_code/types/permissions.py +26 -0
  132. iac_code/types/skill_source.py +11 -0
  133. iac_code/types/stream_events.py +227 -0
  134. iac_code/ui/__init__.py +5 -0
  135. iac_code/ui/banner.py +110 -0
  136. iac_code/ui/components/__init__.py +0 -0
  137. iac_code/ui/components/dialog.py +142 -0
  138. iac_code/ui/components/divider.py +20 -0
  139. iac_code/ui/components/fuzzy_picker.py +308 -0
  140. iac_code/ui/components/progress_bar.py +54 -0
  141. iac_code/ui/components/search_box.py +165 -0
  142. iac_code/ui/components/select.py +319 -0
  143. iac_code/ui/components/status_icon.py +42 -0
  144. iac_code/ui/components/tabs.py +128 -0
  145. iac_code/ui/core/__init__.py +0 -0
  146. iac_code/ui/core/in_place_render.py +129 -0
  147. iac_code/ui/core/input_history.py +118 -0
  148. iac_code/ui/core/key_event.py +41 -0
  149. iac_code/ui/core/prompt_input.py +507 -0
  150. iac_code/ui/core/raw_input.py +302 -0
  151. iac_code/ui/core/screen.py +80 -0
  152. iac_code/ui/dialogs/__init__.py +0 -0
  153. iac_code/ui/dialogs/global_search.py +178 -0
  154. iac_code/ui/dialogs/history_search.py +100 -0
  155. iac_code/ui/dialogs/model_picker.py +280 -0
  156. iac_code/ui/dialogs/quick_open.py +108 -0
  157. iac_code/ui/dialogs/resume_picker.py +749 -0
  158. iac_code/ui/keybindings/__init__.py +0 -0
  159. iac_code/ui/keybindings/manager.py +124 -0
  160. iac_code/ui/renderer.py +1535 -0
  161. iac_code/ui/repl.py +772 -0
  162. iac_code/ui/spinner.py +112 -0
  163. iac_code/ui/suggestions/__init__.py +0 -0
  164. iac_code/ui/suggestions/aggregator.py +171 -0
  165. iac_code/ui/suggestions/command_provider.py +43 -0
  166. iac_code/ui/suggestions/directory_provider.py +95 -0
  167. iac_code/ui/suggestions/file_provider.py +121 -0
  168. iac_code/ui/suggestions/shell_history_provider.py +108 -0
  169. iac_code/ui/suggestions/token_extractor.py +77 -0
  170. iac_code/ui/suggestions/types.py +45 -0
  171. iac_code/ui/transcript_view.py +199 -0
  172. iac_code/utils/__init__.py +0 -0
  173. iac_code/utils/background_housekeeping.py +53 -0
  174. iac_code/utils/cleanup.py +68 -0
  175. iac_code/utils/json_utils.py +60 -0
  176. iac_code/utils/log.py +150 -0
  177. iac_code/utils/project_paths.py +74 -0
  178. iac_code/utils/tool_input_parser.py +62 -0
  179. iac_code-0.1.0.dist-info/LICENSE +201 -0
  180. iac_code-0.1.0.dist-info/METADATA +64 -0
  181. iac_code-0.1.0.dist-info/RECORD +184 -0
  182. iac_code-0.1.0.dist-info/WHEEL +5 -0
  183. iac_code-0.1.0.dist-info/entry_points.txt +2 -0
  184. iac_code-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,227 @@
1
+ """Fine-grained streaming event types for Provider -> AgentLoop -> Renderer pipeline.
2
+
3
+ Replaces the old coarse-grained events (TextChunkEvent, ThinkingEvent, etc.).
4
+ These events flow from Provider through AgentLoop to Renderer unchanged.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Literal, Union
12
+
13
+
14
+ @dataclass
15
+ class Usage:
16
+ """Token usage from an API response."""
17
+
18
+ input_tokens: int = 0
19
+ output_tokens: int = 0
20
+ cache_creation_input_tokens: int = 0
21
+ cache_read_input_tokens: int = 0
22
+
23
+ @property
24
+ def total_tokens(self) -> int:
25
+ return self.input_tokens + self.output_tokens + self.cache_creation_input_tokens + self.cache_read_input_tokens
26
+
27
+
28
+ # -- Provider-originated events ------------------------------------------------
29
+
30
+
31
+ @dataclass
32
+ class MessageStartEvent:
33
+ """A new assistant message has started."""
34
+
35
+ message_id: str
36
+ type: Literal["message_start"] = "message_start"
37
+
38
+
39
+ @dataclass
40
+ class TextDeltaEvent:
41
+ """Incremental text content from the model."""
42
+
43
+ text: str
44
+ type: Literal["text_delta"] = "text_delta"
45
+
46
+
47
+ @dataclass
48
+ class ThinkingDeltaEvent:
49
+ """Incremental thinking/reasoning content."""
50
+
51
+ text: str
52
+ type: Literal["thinking_delta"] = "thinking_delta"
53
+
54
+
55
+ @dataclass
56
+ class ToolUseStartEvent:
57
+ """A tool call has started -- name is known, input not yet complete."""
58
+
59
+ tool_use_id: str
60
+ name: str
61
+ type: Literal["tool_use_start"] = "tool_use_start"
62
+
63
+
64
+ @dataclass
65
+ class ToolInputDeltaEvent:
66
+ """Incremental JSON input for a tool call."""
67
+
68
+ tool_use_id: str
69
+ partial_json: str
70
+ type: Literal["tool_input_delta"] = "tool_input_delta"
71
+
72
+
73
+ @dataclass
74
+ class ToolUseEndEvent:
75
+ """Tool call input is complete."""
76
+
77
+ tool_use_id: str
78
+ input: dict[str, Any]
79
+ type: Literal["tool_use_end"] = "tool_use_end"
80
+
81
+
82
+ @dataclass
83
+ class MessageEndEvent:
84
+ """The assistant message is complete."""
85
+
86
+ stop_reason: str
87
+ usage: Usage
88
+ type: Literal["message_end"] = "message_end"
89
+
90
+
91
+ @dataclass
92
+ class TombstoneEvent:
93
+ """Mark a previously-yielded message as orphaned (should be removed from UI/transcript)."""
94
+
95
+ message_id: str
96
+ type: Literal["tombstone"] = "tombstone"
97
+
98
+
99
+ @dataclass
100
+ class ErrorEvent:
101
+ """An error occurred during streaming."""
102
+
103
+ error: str
104
+ is_retryable: bool
105
+ type: Literal["error"] = "error"
106
+
107
+
108
+ # -- AgentLoop-originated events (consumed by Renderer) ------------------------
109
+
110
+
111
+ @dataclass
112
+ class ToolResultEvent:
113
+ """A tool has finished executing -- result available."""
114
+
115
+ tool_use_id: str
116
+ tool_name: str
117
+ result: str
118
+ is_error: bool = False
119
+ type: Literal["tool_result"] = "tool_result"
120
+
121
+
122
+ @dataclass
123
+ class PermissionRequestEvent:
124
+ """Tool execution requires user permission."""
125
+
126
+ tool_name: str
127
+ tool_input: dict[str, Any]
128
+ tool_use_id: str
129
+ response_future: asyncio.Future[bool] | None = field(default=None)
130
+ type: Literal["permission_request"] = "permission_request"
131
+
132
+
133
+ @dataclass
134
+ class CompactionEvent:
135
+ """Context auto-compaction occurred."""
136
+
137
+ original_tokens: int = 0
138
+ compacted_tokens: int = 0
139
+ type: Literal["compaction"] = "compaction"
140
+
141
+
142
+ @dataclass
143
+ class TaskNotificationEvent:
144
+ """A background agent task has completed/failed/stopped."""
145
+
146
+ task_id: str
147
+ description: str
148
+ status: str # "completed" | "failed" | "stopped"
149
+ result: str | None = None
150
+ error: str | None = None
151
+ type: Literal["task_notification"] = "task_notification"
152
+
153
+
154
+ @dataclass
155
+ class SubAgentToolEvent:
156
+ """A sub-agent's internal tool activity — forwarded to parent Renderer."""
157
+
158
+ parent_tool_use_id: str # The parent AgentTool's tool_use_id
159
+ child_tool_name: str # Tool name the sub-agent called
160
+ child_tool_input: dict # Tool input params
161
+ is_done: bool = False # Whether this child tool finished
162
+ is_error: bool = False
163
+ type: Literal["subagent_tool"] = "subagent_tool"
164
+
165
+
166
+ @dataclass
167
+ class StackProgressEvent:
168
+ """Real-time progress from a stack lifecycle operation."""
169
+
170
+ stack_id: str
171
+ stack_name: str
172
+ status: str
173
+ progress_percentage: float
174
+ resources: list[dict[str, Any]]
175
+ elapsed_seconds: int
176
+ type: Literal["stack_progress"] = "stack_progress"
177
+
178
+
179
+ @dataclass
180
+ class StackInstancesProgressEvent:
181
+ """Real-time progress from a StackGroup instances operation."""
182
+
183
+ stack_group_name: str
184
+ operation_id: str
185
+ status: str
186
+ progress_percentage: int
187
+ instances: list[dict[str, Any]]
188
+ elapsed_seconds: int
189
+ type: Literal["stack_instances_progress"] = "stack_instances_progress"
190
+
191
+
192
+ @dataclass
193
+ class PlanStep:
194
+ """A single step in an agent plan."""
195
+
196
+ content: str
197
+ status: Literal["pending", "in_progress", "completed"] = "pending"
198
+ priority: Literal["high", "medium", "low"] = "medium"
199
+
200
+
201
+ @dataclass
202
+ class PlanEvent:
203
+ """Agent plan creation or update."""
204
+
205
+ steps: list[PlanStep]
206
+ type: Literal["plan"] = "plan"
207
+
208
+
209
+ StreamEvent = Union[
210
+ MessageStartEvent,
211
+ TextDeltaEvent,
212
+ ThinkingDeltaEvent,
213
+ ToolUseStartEvent,
214
+ ToolInputDeltaEvent,
215
+ ToolUseEndEvent,
216
+ MessageEndEvent,
217
+ TombstoneEvent,
218
+ ErrorEvent,
219
+ ToolResultEvent,
220
+ PermissionRequestEvent,
221
+ CompactionEvent,
222
+ TaskNotificationEvent,
223
+ SubAgentToolEvent,
224
+ StackProgressEvent,
225
+ StackInstancesProgressEvent,
226
+ PlanEvent,
227
+ ]
@@ -0,0 +1,5 @@
1
+ """Rich-based inline UI"""
2
+
3
+ from iac_code.ui.repl import InlineREPL
4
+
5
+ __all__ = ["InlineREPL"]
iac_code/ui/banner.py ADDED
@@ -0,0 +1,110 @@
1
+ """Welcome banner rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ from pathlib import Path
7
+
8
+ from rich.align import Align
9
+ from rich.console import Group
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from iac_code.i18n import _
15
+
16
+ # Cloud logo (same as components/logo.py)
17
+ LOGO_LINES = [
18
+ " ▄▄███▄▄ ",
19
+ " ▄██████████▄▄ ",
20
+ " ▄█▀████████████▄ ",
21
+ "████████████████████",
22
+ " ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ",
23
+ ]
24
+
25
+ ACCENT = "bright_cyan"
26
+
27
+
28
+ def _get_provider_display() -> str:
29
+ """Get the active provider display name from settings, translated."""
30
+ try:
31
+ from iac_code.config import get_active_provider_key, get_provider_config
32
+
33
+ key = get_active_provider_key()
34
+ if not key:
35
+ return ""
36
+ name = get_provider_config(key).get("name", "")
37
+ provider_display_names = {
38
+ "DashScope": _("DashScope"),
39
+ "DashScope Token Plan": _("DashScope Token Plan"),
40
+ "OpenAI": _("OpenAI"),
41
+ "Anthropic": _("Anthropic"),
42
+ "DeepSeek": _("DeepSeek"),
43
+ "OpenAPI Compatible": _("OpenAPI Compatible"),
44
+ }
45
+ return provider_display_names.get(name, name)
46
+ except Exception:
47
+ return ""
48
+
49
+
50
+ def render_welcome_banner(model: str, cwd: str, session_id: str | None = None) -> Panel:
51
+ """Produce a Rich Panel for the welcome banner."""
52
+ # Username
53
+ try:
54
+ username = getpass.getuser()
55
+ username = username[0].upper() + username[1:] if username else "User"
56
+ except Exception:
57
+ username = "User"
58
+
59
+ # Logo
60
+ logo = Text()
61
+ for i, line in enumerate(LOGO_LINES):
62
+ if i > 0:
63
+ logo.append("\n")
64
+ logo.append(f" {line}", style="bright_cyan")
65
+
66
+ # Description (centered vertically beside the logo)
67
+ desc_text = Text(_("Your AI-powered Infrastructure as Code assistant"), style="italic white")
68
+
69
+ # Use a table for side-by-side layout with vertical centering
70
+ logo_table = Table(show_header=False, show_edge=False, box=None, padding=0, expand=True)
71
+ logo_table.add_column(ratio=1)
72
+ logo_table.add_column(ratio=2)
73
+ logo_table.add_row(logo, Align(desc_text, align="center", vertical="middle"))
74
+
75
+ # Shorten cwd
76
+ cwd_path = Path(cwd).resolve()
77
+ try:
78
+ cwd_display = "~/" + str(cwd_path.relative_to(Path.home()))
79
+ except ValueError:
80
+ cwd_display = str(cwd_path)
81
+
82
+ # Provider / model display
83
+ provider_name = _get_provider_display()
84
+ if provider_name and model:
85
+ model_display = f"{provider_name} / {model}"
86
+ else:
87
+ model_display = model
88
+
89
+ items = [
90
+ Text(),
91
+ Text(f" {_('Welcome back')} {username}!", style="bold"),
92
+ Text(),
93
+ logo_table,
94
+ Text(),
95
+ Text(f" {model_display}", style="dim") if model_display else Text(),
96
+ Text(f" {cwd_display}", style="dim"),
97
+ Text(f" {_('Session')}: {session_id}", style="dim") if session_id else Text(),
98
+ ]
99
+
100
+ from iac_code.utils.log import is_debug_enabled
101
+
102
+ if is_debug_enabled():
103
+ from iac_code.config import get_config_dir
104
+
105
+ log_path = get_config_dir() / "logs" / "latest.log"
106
+ items.append(Text())
107
+ items.append(Text(f" {_('Debug mode')}", style="bold yellow"))
108
+ items.append(Text(f" {_('Log file')}: {log_path}", style="dim yellow"))
109
+
110
+ return Panel(Group(*items), border_style=ACCENT, expand=True)
File without changes
@@ -0,0 +1,142 @@
1
+ """Modal dialog container component."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Callable
6
+
7
+ from rich.console import Console, RenderableType
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+
11
+ from iac_code.ui.keybindings.manager import KeyBinding, KeybindingManager
12
+
13
+
14
+ class Dialog:
15
+ """A modal dialog container that renders content inside a framed panel.
16
+
17
+ Two usage modes:
18
+
19
+ 1. :meth:`show` — render frame + body once (e.g. inside an existing loop).
20
+ 2. :meth:`run` — manage the full event loop, including in-place rendering
21
+ and dialog-context keybindings. Renders in the main buffer with
22
+ erase-and-redraw between frames so nothing leaks into scrollback.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ title: str,
28
+ keybinding_manager: KeybindingManager,
29
+ on_cancel: Callable[[], None],
30
+ subtitle: str | None = None,
31
+ footer_hints: list[tuple[str, str]] | None = None,
32
+ border_style: str = "blue",
33
+ ) -> None:
34
+ self._title = title
35
+ self._km = keybinding_manager
36
+ self._on_cancel = on_cancel
37
+ self._subtitle = subtitle
38
+ self._footer_hints = footer_hints or []
39
+ self._border_style = border_style
40
+ self._closed = False
41
+
42
+ # ------------------------------------------------------------------
43
+ # Public API
44
+ # ------------------------------------------------------------------
45
+
46
+ def show(self, body: RenderableType) -> None:
47
+ """Render the dialog frame and body to the console once."""
48
+ Console().print(self._build_frame(body))
49
+
50
+ def run(
51
+ self,
52
+ body_builder: Callable[[], RenderableType],
53
+ key_handler: Callable | None = None,
54
+ ) -> None:
55
+ """Run an in-place event loop until the dialog is closed.
56
+
57
+ Flow:
58
+
59
+ 1. Push "dialog" context to KeybindingManager.
60
+ 2. Register Escape and Ctrl+C as cancel.
61
+ 3. Loop: ``body_builder() → render → read_key → key_handler → km.resolve``
62
+ 4. On exit: erase the rendered frame, pop context, unregister.
63
+ """
64
+ from iac_code.ui.core.in_place_render import InPlaceRenderer
65
+ from iac_code.ui.core.raw_input import RawInputCapture
66
+
67
+ renderer = InPlaceRenderer(Console())
68
+ self._km.push_context("dialog")
69
+
70
+ def _cancel() -> bool:
71
+ self._on_cancel()
72
+ self.close()
73
+ return True
74
+
75
+ unregister_escape = self._km.register(
76
+ KeyBinding(key="escape", action="dialog_cancel", context="dialog", handler=_cancel)
77
+ )
78
+ unregister_ctrl_c = self._km.register(
79
+ KeyBinding(key="ctrl+c", action="dialog_cancel", context="dialog", handler=_cancel)
80
+ )
81
+
82
+ try:
83
+ with RawInputCapture() as cap:
84
+ while not self._closed:
85
+ body = body_builder()
86
+ renderer.render(self._build_frame(body))
87
+ key_event = cap.read_key(timeout=0.1)
88
+ if key_event is None:
89
+ continue
90
+ consumed = False
91
+ if key_handler is not None:
92
+ consumed = key_handler(key_event)
93
+ if not consumed:
94
+ self._km.resolve(key_event)
95
+ finally:
96
+ renderer.clear()
97
+ unregister_escape()
98
+ unregister_ctrl_c()
99
+ self._km.pop_context("dialog")
100
+
101
+ def close(self) -> None:
102
+ """Mark the dialog closed; the next loop iteration exits."""
103
+ self._closed = True
104
+
105
+ # ------------------------------------------------------------------
106
+ # Internal helpers
107
+ # ------------------------------------------------------------------
108
+
109
+ def _build_panel_body(self, body: RenderableType) -> RenderableType:
110
+ """Assemble subtitle + body + footer into a single renderable."""
111
+ from rich.console import Group
112
+
113
+ parts: list[RenderableType] = []
114
+
115
+ if self._subtitle:
116
+ parts.append(Text(self._subtitle, style="dim"))
117
+
118
+ parts.append(body)
119
+
120
+ if self._footer_hints:
121
+ footer = self._build_footer()
122
+ parts.append(footer)
123
+
124
+ if len(parts) == 1:
125
+ return parts[0]
126
+ return Group(*parts)
127
+
128
+ def _build_frame(self, body: RenderableType) -> Panel:
129
+ """Build the full Panel with title, body, and footer."""
130
+ panel_body = self._build_panel_body(body)
131
+ title_text = Text(self._title, style="bold")
132
+ return Panel(panel_body, title=title_text, border_style=self._border_style)
133
+
134
+ def _build_footer(self) -> Text:
135
+ """Build footer hints line."""
136
+ text = Text()
137
+ for i, (key_display, action) in enumerate(self._footer_hints):
138
+ if i > 0:
139
+ text.append(" ")
140
+ text.append(key_display, style="bold cyan")
141
+ text.append(f" {action}", style="dim")
142
+ return text
@@ -0,0 +1,20 @@
1
+ """Divider component wrapping Rich Rule."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.rule import Rule
6
+
7
+
8
+ class Divider:
9
+ """A horizontal divider line, optionally with centered text.
10
+
11
+ Wraps :class:`rich.rule.Rule`.
12
+ """
13
+
14
+ def __init__(self, text: str = "", style: str = "dim") -> None:
15
+ self.text = text
16
+ self.style = style
17
+
18
+ def render(self) -> Rule:
19
+ """Return a Rich Rule renderable."""
20
+ return Rule(title=self.text, style=self.style)