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.
- iac_code/__init__.py +2 -0
- iac_code/acp/__init__.py +97 -0
- iac_code/acp/convert.py +423 -0
- iac_code/acp/http_sse.py +448 -0
- iac_code/acp/mcp.py +54 -0
- iac_code/acp/metrics.py +71 -0
- iac_code/acp/server.py +662 -0
- iac_code/acp/session.py +446 -0
- iac_code/acp/slash_registry.py +125 -0
- iac_code/acp/state.py +99 -0
- iac_code/acp/tools.py +112 -0
- iac_code/acp/types.py +13 -0
- iac_code/acp/version.py +26 -0
- iac_code/agent/__init__.py +19 -0
- iac_code/agent/agent_loop.py +640 -0
- iac_code/agent/agent_tool.py +269 -0
- iac_code/agent/agent_types.py +87 -0
- iac_code/agent/message.py +153 -0
- iac_code/agent/system_prompt.py +313 -0
- iac_code/cli/__init__.py +3 -0
- iac_code/cli/headless.py +114 -0
- iac_code/cli/main.py +246 -0
- iac_code/cli/output_formats.py +125 -0
- iac_code/commands/__init__.py +93 -0
- iac_code/commands/auth.py +1055 -0
- iac_code/commands/clear.py +34 -0
- iac_code/commands/compact.py +43 -0
- iac_code/commands/debug.py +45 -0
- iac_code/commands/effort.py +116 -0
- iac_code/commands/exit.py +10 -0
- iac_code/commands/help.py +49 -0
- iac_code/commands/model.py +130 -0
- iac_code/commands/registry.py +245 -0
- iac_code/commands/resume.py +49 -0
- iac_code/commands/tasks.py +41 -0
- iac_code/config.py +304 -0
- iac_code/i18n/__init__.py +141 -0
- iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
- iac_code/memory/__init__.py +1 -0
- iac_code/memory/memory_manager.py +92 -0
- iac_code/memory/memory_tools.py +88 -0
- iac_code/providers/__init__.py +1 -0
- iac_code/providers/anthropic_provider.py +284 -0
- iac_code/providers/base.py +128 -0
- iac_code/providers/dashscope_provider.py +47 -0
- iac_code/providers/deepseek_provider.py +36 -0
- iac_code/providers/manager.py +399 -0
- iac_code/providers/openai_provider.py +344 -0
- iac_code/providers/retry.py +58 -0
- iac_code/providers/stream_watchdog.py +47 -0
- iac_code/providers/thinking.py +164 -0
- iac_code/services/__init__.py +1 -0
- iac_code/services/agent_factory.py +127 -0
- iac_code/services/cloud_credentials.py +22 -0
- iac_code/services/context_manager.py +221 -0
- iac_code/services/providers/__init__.py +1 -0
- iac_code/services/providers/aliyun.py +232 -0
- iac_code/services/session_index.py +281 -0
- iac_code/services/session_storage.py +245 -0
- iac_code/services/telemetry/__init__.py +66 -0
- iac_code/services/telemetry/attributes.py +84 -0
- iac_code/services/telemetry/client.py +330 -0
- iac_code/services/telemetry/config.py +76 -0
- iac_code/services/telemetry/constants.py +75 -0
- iac_code/services/telemetry/content_serializer.py +124 -0
- iac_code/services/telemetry/events.py +42 -0
- iac_code/services/telemetry/fallback.py +59 -0
- iac_code/services/telemetry/identity.py +73 -0
- iac_code/services/telemetry/metrics.py +62 -0
- iac_code/services/telemetry/names.py +199 -0
- iac_code/services/telemetry/sanitize.py +88 -0
- iac_code/services/telemetry/sink.py +67 -0
- iac_code/services/telemetry/tracing.py +38 -0
- iac_code/services/telemetry/types.py +13 -0
- iac_code/services/token_budget.py +54 -0
- iac_code/services/token_counter.py +76 -0
- iac_code/skills/__init__.py +1 -0
- iac_code/skills/bundled/__init__.py +94 -0
- iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
- iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
- iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
- iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
- iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
- iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
- iac_code/skills/bundled/simplify.py +28 -0
- iac_code/skills/discovery.py +136 -0
- iac_code/skills/frontmatter.py +119 -0
- iac_code/skills/listing.py +92 -0
- iac_code/skills/loader.py +42 -0
- iac_code/skills/processor.py +81 -0
- iac_code/skills/renderer.py +157 -0
- iac_code/skills/skill_definition.py +82 -0
- iac_code/skills/skill_tool.py +261 -0
- iac_code/state/__init__.py +5 -0
- iac_code/state/app_state.py +122 -0
- iac_code/tasks/__init__.py +1 -0
- iac_code/tasks/notification_queue.py +28 -0
- iac_code/tasks/task_state.py +66 -0
- iac_code/tasks/task_tools.py +114 -0
- iac_code/tools/__init__.py +8 -0
- iac_code/tools/base.py +226 -0
- iac_code/tools/bash.py +133 -0
- iac_code/tools/cloud/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
- iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
- iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
- iac_code/tools/cloud/aliyun/ros_client.py +56 -0
- iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
- iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
- iac_code/tools/cloud/base_api.py +162 -0
- iac_code/tools/cloud/base_stack.py +242 -0
- iac_code/tools/cloud/registry.py +20 -0
- iac_code/tools/cloud/types.py +105 -0
- iac_code/tools/edit_file.py +121 -0
- iac_code/tools/glob.py +103 -0
- iac_code/tools/grep.py +254 -0
- iac_code/tools/list_files.py +104 -0
- iac_code/tools/read_file.py +127 -0
- iac_code/tools/result_storage.py +39 -0
- iac_code/tools/tool_executor.py +165 -0
- iac_code/tools/web_fetch.py +177 -0
- iac_code/tools/write_file.py +88 -0
- iac_code/types/__init__.py +40 -0
- iac_code/types/permissions.py +26 -0
- iac_code/types/skill_source.py +11 -0
- iac_code/types/stream_events.py +227 -0
- iac_code/ui/__init__.py +5 -0
- iac_code/ui/banner.py +110 -0
- iac_code/ui/components/__init__.py +0 -0
- iac_code/ui/components/dialog.py +142 -0
- iac_code/ui/components/divider.py +20 -0
- iac_code/ui/components/fuzzy_picker.py +308 -0
- iac_code/ui/components/progress_bar.py +54 -0
- iac_code/ui/components/search_box.py +165 -0
- iac_code/ui/components/select.py +319 -0
- iac_code/ui/components/status_icon.py +42 -0
- iac_code/ui/components/tabs.py +128 -0
- iac_code/ui/core/__init__.py +0 -0
- iac_code/ui/core/in_place_render.py +129 -0
- iac_code/ui/core/input_history.py +118 -0
- iac_code/ui/core/key_event.py +41 -0
- iac_code/ui/core/prompt_input.py +507 -0
- iac_code/ui/core/raw_input.py +302 -0
- iac_code/ui/core/screen.py +80 -0
- iac_code/ui/dialogs/__init__.py +0 -0
- iac_code/ui/dialogs/global_search.py +178 -0
- iac_code/ui/dialogs/history_search.py +100 -0
- iac_code/ui/dialogs/model_picker.py +280 -0
- iac_code/ui/dialogs/quick_open.py +108 -0
- iac_code/ui/dialogs/resume_picker.py +749 -0
- iac_code/ui/keybindings/__init__.py +0 -0
- iac_code/ui/keybindings/manager.py +124 -0
- iac_code/ui/renderer.py +1535 -0
- iac_code/ui/repl.py +772 -0
- iac_code/ui/spinner.py +112 -0
- iac_code/ui/suggestions/__init__.py +0 -0
- iac_code/ui/suggestions/aggregator.py +171 -0
- iac_code/ui/suggestions/command_provider.py +43 -0
- iac_code/ui/suggestions/directory_provider.py +95 -0
- iac_code/ui/suggestions/file_provider.py +121 -0
- iac_code/ui/suggestions/shell_history_provider.py +108 -0
- iac_code/ui/suggestions/token_extractor.py +77 -0
- iac_code/ui/suggestions/types.py +45 -0
- iac_code/ui/transcript_view.py +199 -0
- iac_code/utils/__init__.py +0 -0
- iac_code/utils/background_housekeeping.py +53 -0
- iac_code/utils/cleanup.py +68 -0
- iac_code/utils/json_utils.py +60 -0
- iac_code/utils/log.py +150 -0
- iac_code/utils/project_paths.py +74 -0
- iac_code/utils/tool_input_parser.py +62 -0
- iac_code-0.1.0.dist-info/LICENSE +201 -0
- iac_code-0.1.0.dist-info/METADATA +64 -0
- iac_code-0.1.0.dist-info/RECORD +184 -0
- iac_code-0.1.0.dist-info/WHEEL +5 -0
- iac_code-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|
iac_code/ui/__init__.py
ADDED
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)
|