doit-toolkit-cli 0.1.9__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.
- doit_cli/__init__.py +1356 -0
- doit_cli/cli/__init__.py +26 -0
- doit_cli/cli/analytics_command.py +616 -0
- doit_cli/cli/context_command.py +213 -0
- doit_cli/cli/diagram_command.py +304 -0
- doit_cli/cli/fixit_command.py +641 -0
- doit_cli/cli/hooks_command.py +211 -0
- doit_cli/cli/init_command.py +613 -0
- doit_cli/cli/memory_command.py +293 -0
- doit_cli/cli/status_command.py +117 -0
- doit_cli/cli/sync_prompts_command.py +248 -0
- doit_cli/cli/validate_command.py +196 -0
- doit_cli/cli/verify_command.py +204 -0
- doit_cli/cli/workflow_mixin.py +224 -0
- doit_cli/cli/xref_command.py +555 -0
- doit_cli/formatters/__init__.py +8 -0
- doit_cli/formatters/base.py +38 -0
- doit_cli/formatters/json_formatter.py +126 -0
- doit_cli/formatters/markdown_formatter.py +97 -0
- doit_cli/formatters/rich_formatter.py +257 -0
- doit_cli/main.py +49 -0
- doit_cli/models/__init__.py +139 -0
- doit_cli/models/agent.py +74 -0
- doit_cli/models/analytics_models.py +384 -0
- doit_cli/models/context_config.py +464 -0
- doit_cli/models/crossref_models.py +182 -0
- doit_cli/models/diagram_models.py +363 -0
- doit_cli/models/fixit_models.py +355 -0
- doit_cli/models/hook_config.py +125 -0
- doit_cli/models/project.py +91 -0
- doit_cli/models/results.py +121 -0
- doit_cli/models/search_models.py +228 -0
- doit_cli/models/status_models.py +195 -0
- doit_cli/models/sync_models.py +146 -0
- doit_cli/models/template.py +77 -0
- doit_cli/models/validation_models.py +175 -0
- doit_cli/models/workflow_models.py +319 -0
- doit_cli/prompts/__init__.py +5 -0
- doit_cli/prompts/fixit_prompts.py +344 -0
- doit_cli/prompts/interactive.py +390 -0
- doit_cli/rules/__init__.py +5 -0
- doit_cli/rules/builtin_rules.py +160 -0
- doit_cli/services/__init__.py +79 -0
- doit_cli/services/agent_detector.py +168 -0
- doit_cli/services/analytics_service.py +218 -0
- doit_cli/services/architecture_generator.py +290 -0
- doit_cli/services/backup_service.py +204 -0
- doit_cli/services/config_loader.py +113 -0
- doit_cli/services/context_loader.py +1121 -0
- doit_cli/services/coverage_calculator.py +142 -0
- doit_cli/services/crossref_service.py +237 -0
- doit_cli/services/cycle_time_calculator.py +134 -0
- doit_cli/services/date_inferrer.py +349 -0
- doit_cli/services/diagram_service.py +337 -0
- doit_cli/services/drift_detector.py +109 -0
- doit_cli/services/entity_parser.py +301 -0
- doit_cli/services/er_diagram_generator.py +197 -0
- doit_cli/services/fixit_service.py +699 -0
- doit_cli/services/github_service.py +192 -0
- doit_cli/services/hook_manager.py +258 -0
- doit_cli/services/hook_validator.py +528 -0
- doit_cli/services/input_validator.py +322 -0
- doit_cli/services/memory_search.py +527 -0
- doit_cli/services/mermaid_validator.py +334 -0
- doit_cli/services/prompt_transformer.py +91 -0
- doit_cli/services/prompt_writer.py +133 -0
- doit_cli/services/query_interpreter.py +428 -0
- doit_cli/services/report_exporter.py +219 -0
- doit_cli/services/report_generator.py +256 -0
- doit_cli/services/requirement_parser.py +112 -0
- doit_cli/services/roadmap_summarizer.py +209 -0
- doit_cli/services/rule_engine.py +443 -0
- doit_cli/services/scaffolder.py +215 -0
- doit_cli/services/score_calculator.py +172 -0
- doit_cli/services/section_parser.py +204 -0
- doit_cli/services/spec_scanner.py +327 -0
- doit_cli/services/state_manager.py +355 -0
- doit_cli/services/status_reporter.py +143 -0
- doit_cli/services/task_parser.py +347 -0
- doit_cli/services/template_manager.py +710 -0
- doit_cli/services/template_reader.py +158 -0
- doit_cli/services/user_journey_generator.py +214 -0
- doit_cli/services/user_story_parser.py +232 -0
- doit_cli/services/validation_service.py +188 -0
- doit_cli/services/validator.py +232 -0
- doit_cli/services/velocity_tracker.py +173 -0
- doit_cli/services/workflow_engine.py +405 -0
- doit_cli/templates/agent-file-template.md +28 -0
- doit_cli/templates/checklist-template.md +39 -0
- doit_cli/templates/commands/doit.checkin.md +363 -0
- doit_cli/templates/commands/doit.constitution.md +187 -0
- doit_cli/templates/commands/doit.documentit.md +485 -0
- doit_cli/templates/commands/doit.fixit.md +181 -0
- doit_cli/templates/commands/doit.implementit.md +265 -0
- doit_cli/templates/commands/doit.planit.md +262 -0
- doit_cli/templates/commands/doit.reviewit.md +355 -0
- doit_cli/templates/commands/doit.roadmapit.md +368 -0
- doit_cli/templates/commands/doit.scaffoldit.md +458 -0
- doit_cli/templates/commands/doit.specit.md +521 -0
- doit_cli/templates/commands/doit.taskit.md +304 -0
- doit_cli/templates/commands/doit.testit.md +277 -0
- doit_cli/templates/config/context.yaml +134 -0
- doit_cli/templates/config/hooks.yaml +93 -0
- doit_cli/templates/config/validation-rules.yaml +64 -0
- doit_cli/templates/github-issue-templates/epic.yml +78 -0
- doit_cli/templates/github-issue-templates/feature.yml +116 -0
- doit_cli/templates/github-issue-templates/task.yml +129 -0
- doit_cli/templates/hooks/.gitkeep +0 -0
- doit_cli/templates/hooks/post-commit.sh +25 -0
- doit_cli/templates/hooks/post-merge.sh +75 -0
- doit_cli/templates/hooks/pre-commit.sh +17 -0
- doit_cli/templates/hooks/pre-push.sh +18 -0
- doit_cli/templates/memory/completed_roadmap.md +50 -0
- doit_cli/templates/memory/constitution.md +125 -0
- doit_cli/templates/memory/roadmap.md +61 -0
- doit_cli/templates/plan-template.md +146 -0
- doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
- doit_cli/templates/scripts/bash/common.sh +156 -0
- doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
- doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
- doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
- doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
- doit_cli/templates/scripts/powershell/common.ps1 +137 -0
- doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
- doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
- doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
- doit_cli/templates/spec-template.md +159 -0
- doit_cli/templates/tasks-template.md +313 -0
- doit_cli/templates/vscode-settings.json +14 -0
- doit_toolkit_cli-0.1.9.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.9.dist-info/RECORD +134 -0
- doit_toolkit_cli-0.1.9.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.9.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.9.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Interactive prompts for fixit workflow.
|
|
2
|
+
|
|
3
|
+
This module provides Rich-based interactive prompts
|
|
4
|
+
for the bug-fix workflow user interactions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.prompt import Confirm, IntPrompt, Prompt
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from ..models.fixit_models import (
|
|
15
|
+
FixPhase,
|
|
16
|
+
FixPlan,
|
|
17
|
+
FixWorkflow,
|
|
18
|
+
GitHubIssue,
|
|
19
|
+
InvestigationFinding,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# Issue Selection Prompts
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def prompt_select_issue(bugs: list[GitHubIssue]) -> Optional[GitHubIssue]:
|
|
31
|
+
"""Display bugs and prompt user to select one.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
bugs: List of GitHubIssue objects to choose from.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Selected GitHubIssue or None if cancelled.
|
|
38
|
+
"""
|
|
39
|
+
if not bugs:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# Display table of bugs
|
|
43
|
+
table = Table(title="Open Bugs", show_header=True)
|
|
44
|
+
table.add_column("#", style="cyan", width=4)
|
|
45
|
+
table.add_column("Issue", style="cyan", width=6)
|
|
46
|
+
table.add_column("Title", style="white")
|
|
47
|
+
table.add_column("Labels", style="dim")
|
|
48
|
+
|
|
49
|
+
for idx, bug in enumerate(bugs, 1):
|
|
50
|
+
labels_str = ", ".join(bug.labels[:3]) if bug.labels else ""
|
|
51
|
+
if len(bug.labels) > 3:
|
|
52
|
+
labels_str += "..."
|
|
53
|
+
table.add_row(str(idx), f"#{bug.number}", bug.title[:60], labels_str)
|
|
54
|
+
|
|
55
|
+
console.print(table)
|
|
56
|
+
console.print()
|
|
57
|
+
|
|
58
|
+
# Prompt for selection
|
|
59
|
+
while True:
|
|
60
|
+
try:
|
|
61
|
+
choice = IntPrompt.ask(
|
|
62
|
+
"Select bug number to fix (or 0 to cancel)",
|
|
63
|
+
default=0,
|
|
64
|
+
)
|
|
65
|
+
if choice == 0:
|
|
66
|
+
return None
|
|
67
|
+
if 1 <= choice <= len(bugs):
|
|
68
|
+
return bugs[choice - 1]
|
|
69
|
+
console.print(f"[red]Please select a number between 1 and {len(bugs)}[/red]")
|
|
70
|
+
except KeyboardInterrupt:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# =============================================================================
|
|
75
|
+
# Workflow Display Functions
|
|
76
|
+
# =============================================================================
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def display_workflow_started(workflow: FixWorkflow, issue_id: int) -> None:
|
|
80
|
+
"""Display confirmation that workflow has started.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
workflow: The started FixWorkflow.
|
|
84
|
+
issue_id: GitHub issue number.
|
|
85
|
+
"""
|
|
86
|
+
console.print()
|
|
87
|
+
console.print(
|
|
88
|
+
Panel(
|
|
89
|
+
f"[green]Workflow started for issue [cyan]#{issue_id}[/cyan][/green]\n\n"
|
|
90
|
+
f"[dim]Branch:[/dim] [cyan]{workflow.branch_name}[/cyan]\n"
|
|
91
|
+
f"[dim]Phase:[/dim] [yellow]{workflow.phase.value}[/yellow]",
|
|
92
|
+
title="Bug Fix Workflow",
|
|
93
|
+
border_style="green",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def display_workflow_status(workflow: FixWorkflow) -> None:
|
|
99
|
+
"""Display detailed workflow status.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
workflow: The FixWorkflow to display.
|
|
103
|
+
"""
|
|
104
|
+
phase_style = _get_phase_style(workflow.phase)
|
|
105
|
+
|
|
106
|
+
# Build phase progress indicator
|
|
107
|
+
phases = [
|
|
108
|
+
FixPhase.INITIALIZED,
|
|
109
|
+
FixPhase.INVESTIGATING,
|
|
110
|
+
FixPhase.PLANNING,
|
|
111
|
+
FixPhase.REVIEWING,
|
|
112
|
+
FixPhase.APPROVED,
|
|
113
|
+
FixPhase.IMPLEMENTING,
|
|
114
|
+
FixPhase.COMPLETED,
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
progress_parts = []
|
|
118
|
+
current_found = False
|
|
119
|
+
for phase in phases:
|
|
120
|
+
if phase == workflow.phase:
|
|
121
|
+
progress_parts.append(f"[{phase_style}]{phase.value}[/{phase_style}]")
|
|
122
|
+
current_found = True
|
|
123
|
+
elif not current_found:
|
|
124
|
+
progress_parts.append(f"[green]{phase.value}[/green]")
|
|
125
|
+
else:
|
|
126
|
+
progress_parts.append(f"[dim]{phase.value}[/dim]")
|
|
127
|
+
|
|
128
|
+
progress_line = " → ".join(progress_parts)
|
|
129
|
+
|
|
130
|
+
console.print()
|
|
131
|
+
console.print(
|
|
132
|
+
Panel(
|
|
133
|
+
f"[dim]Issue:[/dim] [cyan]#{workflow.issue_id}[/cyan]\n"
|
|
134
|
+
f"[dim]Branch:[/dim] [white]{workflow.branch_name}[/white]\n"
|
|
135
|
+
f"[dim]Started:[/dim] {workflow.started_at.strftime('%Y-%m-%d %H:%M')}\n"
|
|
136
|
+
f"[dim]Updated:[/dim] {workflow.updated_at.strftime('%Y-%m-%d %H:%M')}\n\n"
|
|
137
|
+
f"[dim]Progress:[/dim]\n{progress_line}",
|
|
138
|
+
title=f"Fixit Workflow: {workflow.id}",
|
|
139
|
+
border_style=phase_style,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def display_investigation_findings(findings: list[InvestigationFinding]) -> None:
|
|
145
|
+
"""Display investigation findings.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
findings: List of findings to display.
|
|
149
|
+
"""
|
|
150
|
+
if not findings:
|
|
151
|
+
console.print("[yellow]No findings recorded yet.[/yellow]")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
table = Table(title="Investigation Findings")
|
|
155
|
+
table.add_column("Type", style="cyan", width=15)
|
|
156
|
+
table.add_column("Description", style="white")
|
|
157
|
+
table.add_column("Location", style="dim")
|
|
158
|
+
|
|
159
|
+
for finding in findings:
|
|
160
|
+
location = ""
|
|
161
|
+
if finding.file_path:
|
|
162
|
+
location = finding.file_path
|
|
163
|
+
if finding.line_number:
|
|
164
|
+
location += f":{finding.line_number}"
|
|
165
|
+
|
|
166
|
+
table.add_row(
|
|
167
|
+
finding.finding_type.value,
|
|
168
|
+
finding.description[:50] + "..." if len(finding.description) > 50 else finding.description,
|
|
169
|
+
location,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
console.print(table)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def display_fix_plan(plan: FixPlan) -> None:
|
|
176
|
+
"""Display fix plan details.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
plan: The FixPlan to display.
|
|
180
|
+
"""
|
|
181
|
+
# Status style
|
|
182
|
+
status_styles = {
|
|
183
|
+
"draft": "dim",
|
|
184
|
+
"pending_review": "yellow",
|
|
185
|
+
"revision_needed": "red",
|
|
186
|
+
"approved": "green",
|
|
187
|
+
}
|
|
188
|
+
status_style = status_styles.get(plan.status.value, "white")
|
|
189
|
+
|
|
190
|
+
# Risk level style
|
|
191
|
+
risk_styles = {
|
|
192
|
+
"low": "green",
|
|
193
|
+
"medium": "yellow",
|
|
194
|
+
"high": "red",
|
|
195
|
+
}
|
|
196
|
+
risk_style = risk_styles.get(plan.risk_level.value, "white")
|
|
197
|
+
|
|
198
|
+
# Build plan content
|
|
199
|
+
content = (
|
|
200
|
+
f"[dim]Plan ID:[/dim] {plan.id}\n"
|
|
201
|
+
f"[dim]Status:[/dim] [{status_style}]{plan.status.value}[/{status_style}]\n"
|
|
202
|
+
f"[dim]Risk Level:[/dim] [{risk_style}]{plan.risk_level.value}[/{risk_style}]\n\n"
|
|
203
|
+
f"[cyan]Root Cause:[/cyan]\n{plan.root_cause}\n\n"
|
|
204
|
+
f"[cyan]Proposed Solution:[/cyan]\n{plan.proposed_solution}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
console.print(
|
|
208
|
+
Panel(
|
|
209
|
+
content,
|
|
210
|
+
title="Fix Plan",
|
|
211
|
+
border_style=status_style,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Display affected files
|
|
216
|
+
if plan.affected_files:
|
|
217
|
+
table = Table(title="Affected Files")
|
|
218
|
+
table.add_column("File", style="cyan")
|
|
219
|
+
table.add_column("Change", style="dim", width=10)
|
|
220
|
+
table.add_column("Description", style="white")
|
|
221
|
+
|
|
222
|
+
for fc in plan.affected_files:
|
|
223
|
+
table.add_row(
|
|
224
|
+
fc.file_path,
|
|
225
|
+
fc.change_type.value,
|
|
226
|
+
fc.description[:50] + "..." if len(fc.description) > 50 else fc.description,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
console.print(table)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# =============================================================================
|
|
233
|
+
# Confirmation Prompts
|
|
234
|
+
# =============================================================================
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def confirm_start_workflow(issue: GitHubIssue) -> bool:
|
|
238
|
+
"""Confirm starting a workflow for an issue.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
issue: The GitHub issue.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if confirmed, False otherwise.
|
|
245
|
+
"""
|
|
246
|
+
console.print()
|
|
247
|
+
console.print(
|
|
248
|
+
Panel(
|
|
249
|
+
f"[cyan]#{issue.number}[/cyan] {issue.title}\n\n"
|
|
250
|
+
f"[dim]{issue.body[:200]}{'...' if len(issue.body) > 200 else ''}[/dim]",
|
|
251
|
+
title="Issue Details",
|
|
252
|
+
border_style="cyan",
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
console.print()
|
|
256
|
+
|
|
257
|
+
return Confirm.ask("Start fix workflow for this issue?", default=True)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def confirm_cancel_workflow(issue_id: int) -> bool:
|
|
261
|
+
"""Confirm cancelling a workflow.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
issue_id: GitHub issue number.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if confirmed, False otherwise.
|
|
268
|
+
"""
|
|
269
|
+
return Confirm.ask(
|
|
270
|
+
f"[yellow]Cancel workflow for issue #{issue_id}?[/yellow]",
|
|
271
|
+
default=False,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def confirm_complete_workflow(issue_id: int, close_issue: bool = True) -> bool:
|
|
276
|
+
"""Confirm completing a workflow.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
issue_id: GitHub issue number.
|
|
280
|
+
close_issue: Whether the issue will be closed.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
True if confirmed, False otherwise.
|
|
284
|
+
"""
|
|
285
|
+
msg = f"Complete workflow for issue #{issue_id}?"
|
|
286
|
+
if close_issue:
|
|
287
|
+
msg += " (Issue will be closed)"
|
|
288
|
+
|
|
289
|
+
return Confirm.ask(msg, default=True)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# =============================================================================
|
|
293
|
+
# Input Prompts
|
|
294
|
+
# =============================================================================
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def prompt_branch_name(default: str) -> str:
|
|
298
|
+
"""Prompt for custom branch name.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
default: Default branch name.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Branch name (custom or default).
|
|
305
|
+
"""
|
|
306
|
+
return Prompt.ask("Branch name", default=default)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def prompt_finding_description() -> str:
|
|
310
|
+
"""Prompt for a finding description.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Finding description.
|
|
314
|
+
"""
|
|
315
|
+
return Prompt.ask("Describe the finding")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def prompt_root_cause() -> str:
|
|
319
|
+
"""Prompt for root cause description.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Root cause description.
|
|
323
|
+
"""
|
|
324
|
+
return Prompt.ask("Describe the root cause")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# =============================================================================
|
|
328
|
+
# Helper Functions
|
|
329
|
+
# =============================================================================
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _get_phase_style(phase: FixPhase) -> str:
|
|
333
|
+
"""Get Rich style for a phase."""
|
|
334
|
+
styles = {
|
|
335
|
+
FixPhase.INITIALIZED: "dim",
|
|
336
|
+
FixPhase.INVESTIGATING: "yellow",
|
|
337
|
+
FixPhase.PLANNING: "yellow",
|
|
338
|
+
FixPhase.REVIEWING: "cyan",
|
|
339
|
+
FixPhase.APPROVED: "green",
|
|
340
|
+
FixPhase.IMPLEMENTING: "blue",
|
|
341
|
+
FixPhase.COMPLETED: "green bold",
|
|
342
|
+
FixPhase.CANCELLED: "red dim",
|
|
343
|
+
}
|
|
344
|
+
return styles.get(phase, "white")
|