titan-cli 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.
- titan_cli/__init__.py +3 -0
- titan_cli/__main__.py +4 -0
- titan_cli/ai/__init__.py +0 -0
- titan_cli/ai/agents/__init__.py +15 -0
- titan_cli/ai/agents/base.py +152 -0
- titan_cli/ai/client.py +170 -0
- titan_cli/ai/constants.py +56 -0
- titan_cli/ai/exceptions.py +48 -0
- titan_cli/ai/models.py +34 -0
- titan_cli/ai/oauth_helper.py +120 -0
- titan_cli/ai/providers/__init__.py +9 -0
- titan_cli/ai/providers/anthropic.py +117 -0
- titan_cli/ai/providers/base.py +75 -0
- titan_cli/ai/providers/gemini.py +278 -0
- titan_cli/cli.py +59 -0
- titan_cli/clients/__init__.py +1 -0
- titan_cli/clients/gcloud_client.py +52 -0
- titan_cli/core/__init__.py +3 -0
- titan_cli/core/config.py +274 -0
- titan_cli/core/discovery.py +51 -0
- titan_cli/core/errors.py +81 -0
- titan_cli/core/models.py +52 -0
- titan_cli/core/plugins/available.py +36 -0
- titan_cli/core/plugins/models.py +67 -0
- titan_cli/core/plugins/plugin_base.py +108 -0
- titan_cli/core/plugins/plugin_registry.py +163 -0
- titan_cli/core/secrets.py +141 -0
- titan_cli/core/workflows/__init__.py +22 -0
- titan_cli/core/workflows/models.py +88 -0
- titan_cli/core/workflows/project_step_source.py +86 -0
- titan_cli/core/workflows/workflow_exceptions.py +17 -0
- titan_cli/core/workflows/workflow_filter_service.py +137 -0
- titan_cli/core/workflows/workflow_registry.py +419 -0
- titan_cli/core/workflows/workflow_sources.py +307 -0
- titan_cli/engine/__init__.py +39 -0
- titan_cli/engine/builder.py +159 -0
- titan_cli/engine/context.py +82 -0
- titan_cli/engine/mock_context.py +176 -0
- titan_cli/engine/results.py +91 -0
- titan_cli/engine/steps/ai_assistant_step.py +185 -0
- titan_cli/engine/steps/command_step.py +93 -0
- titan_cli/engine/utils/__init__.py +3 -0
- titan_cli/engine/utils/venv.py +31 -0
- titan_cli/engine/workflow_executor.py +187 -0
- titan_cli/external_cli/__init__.py +0 -0
- titan_cli/external_cli/configs.py +17 -0
- titan_cli/external_cli/launcher.py +65 -0
- titan_cli/messages.py +121 -0
- titan_cli/ui/tui/__init__.py +205 -0
- titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
- titan_cli/ui/tui/app.py +113 -0
- titan_cli/ui/tui/icons.py +70 -0
- titan_cli/ui/tui/screens/__init__.py +24 -0
- titan_cli/ui/tui/screens/ai_config.py +498 -0
- titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
- titan_cli/ui/tui/screens/base.py +110 -0
- titan_cli/ui/tui/screens/cli_launcher.py +151 -0
- titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
- titan_cli/ui/tui/screens/main_menu.py +162 -0
- titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
- titan_cli/ui/tui/screens/plugin_management.py +377 -0
- titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
- titan_cli/ui/tui/screens/workflow_execution.py +592 -0
- titan_cli/ui/tui/screens/workflows.py +249 -0
- titan_cli/ui/tui/textual_components.py +537 -0
- titan_cli/ui/tui/textual_workflow_executor.py +405 -0
- titan_cli/ui/tui/theme.py +102 -0
- titan_cli/ui/tui/widgets/__init__.py +40 -0
- titan_cli/ui/tui/widgets/button.py +108 -0
- titan_cli/ui/tui/widgets/header.py +116 -0
- titan_cli/ui/tui/widgets/panel.py +81 -0
- titan_cli/ui/tui/widgets/status_bar.py +115 -0
- titan_cli/ui/tui/widgets/table.py +77 -0
- titan_cli/ui/tui/widgets/text.py +177 -0
- titan_cli/utils/__init__.py +0 -0
- titan_cli/utils/autoupdate.py +155 -0
- titan_cli-0.1.0.dist-info/METADATA +149 -0
- titan_cli-0.1.0.dist-info/RECORD +146 -0
- titan_cli-0.1.0.dist-info/WHEEL +4 -0
- titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
- titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- titan_plugin_git/__init__.py +1 -0
- titan_plugin_git/clients/__init__.py +8 -0
- titan_plugin_git/clients/git_client.py +772 -0
- titan_plugin_git/exceptions.py +40 -0
- titan_plugin_git/messages.py +112 -0
- titan_plugin_git/models.py +39 -0
- titan_plugin_git/plugin.py +118 -0
- titan_plugin_git/steps/__init__.py +1 -0
- titan_plugin_git/steps/ai_commit_message_step.py +171 -0
- titan_plugin_git/steps/branch_steps.py +104 -0
- titan_plugin_git/steps/commit_step.py +80 -0
- titan_plugin_git/steps/push_step.py +63 -0
- titan_plugin_git/steps/status_step.py +59 -0
- titan_plugin_git/workflows/__previews__/__init__.py +1 -0
- titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
- titan_plugin_git/workflows/commit-ai.yaml +28 -0
- titan_plugin_github/__init__.py +11 -0
- titan_plugin_github/agents/__init__.py +6 -0
- titan_plugin_github/agents/config_loader.py +130 -0
- titan_plugin_github/agents/issue_generator.py +353 -0
- titan_plugin_github/agents/pr_agent.py +528 -0
- titan_plugin_github/clients/__init__.py +8 -0
- titan_plugin_github/clients/github_client.py +1105 -0
- titan_plugin_github/config/__init__.py +0 -0
- titan_plugin_github/config/pr_agent.toml +85 -0
- titan_plugin_github/exceptions.py +28 -0
- titan_plugin_github/messages.py +88 -0
- titan_plugin_github/models.py +330 -0
- titan_plugin_github/plugin.py +131 -0
- titan_plugin_github/steps/__init__.py +12 -0
- titan_plugin_github/steps/ai_pr_step.py +172 -0
- titan_plugin_github/steps/create_pr_step.py +86 -0
- titan_plugin_github/steps/github_prompt_steps.py +171 -0
- titan_plugin_github/steps/issue_steps.py +143 -0
- titan_plugin_github/steps/preview_step.py +40 -0
- titan_plugin_github/utils.py +82 -0
- titan_plugin_github/workflows/__previews__/__init__.py +1 -0
- titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
- titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
- titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
- titan_plugin_jira/__init__.py +8 -0
- titan_plugin_jira/agents/__init__.py +6 -0
- titan_plugin_jira/agents/config_loader.py +154 -0
- titan_plugin_jira/agents/jira_agent.py +553 -0
- titan_plugin_jira/agents/prompts.py +364 -0
- titan_plugin_jira/agents/response_parser.py +435 -0
- titan_plugin_jira/agents/token_tracker.py +223 -0
- titan_plugin_jira/agents/validators.py +246 -0
- titan_plugin_jira/clients/jira_client.py +745 -0
- titan_plugin_jira/config/jira_agent.toml +92 -0
- titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
- titan_plugin_jira/exceptions.py +37 -0
- titan_plugin_jira/formatters/__init__.py +6 -0
- titan_plugin_jira/formatters/markdown_formatter.py +245 -0
- titan_plugin_jira/messages.py +115 -0
- titan_plugin_jira/models.py +89 -0
- titan_plugin_jira/plugin.py +264 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
- titan_plugin_jira/steps/get_issue_step.py +82 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
- titan_plugin_jira/steps/search_saved_query_step.py +238 -0
- titan_plugin_jira/utils/__init__.py +13 -0
- titan_plugin_jira/utils/issue_sorter.py +140 -0
- titan_plugin_jira/utils/saved_queries.py +150 -0
- titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Screen
|
|
3
|
+
|
|
4
|
+
Base class for all Titan TUI screens with consistent layout.
|
|
5
|
+
"""
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.screen import Screen
|
|
8
|
+
|
|
9
|
+
from titan_cli.core.config import TitanConfig
|
|
10
|
+
from titan_cli.ui.tui.widgets.status_bar import StatusBarWidget
|
|
11
|
+
from titan_cli.ui.tui.widgets.header import HeaderWidget
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseScreen(Screen):
|
|
15
|
+
"""
|
|
16
|
+
Base screen with consistent layout for all Titan screens.
|
|
17
|
+
|
|
18
|
+
Provides:
|
|
19
|
+
- Header (top)
|
|
20
|
+
- Content area (middle) - to be defined by subclasses
|
|
21
|
+
- StatusBar (bottom, above footer)
|
|
22
|
+
- Footer (bottom)
|
|
23
|
+
|
|
24
|
+
Subclasses should override `compose_content()` to define their content.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
CSS = """
|
|
28
|
+
BaseScreen {
|
|
29
|
+
background: $surface;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#screen-content {
|
|
33
|
+
height: 1fr;
|
|
34
|
+
overflow-y: auto;
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: TitanConfig, title: str = "Titan CLI", show_back: bool = False, show_status_bar: bool = True, **kwargs):
|
|
39
|
+
"""
|
|
40
|
+
Initialize base screen.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: TitanConfig instance
|
|
44
|
+
title: Title to display in header
|
|
45
|
+
show_back: Whether to show back button in header
|
|
46
|
+
show_status_bar: Whether to show status bar at bottom
|
|
47
|
+
"""
|
|
48
|
+
super().__init__(**kwargs)
|
|
49
|
+
self.config = config
|
|
50
|
+
self.screen_title = title
|
|
51
|
+
self.show_back = show_back
|
|
52
|
+
self.show_status_bar = show_status_bar
|
|
53
|
+
|
|
54
|
+
def compose(self) -> ComposeResult:
|
|
55
|
+
"""Compose the base screen layout."""
|
|
56
|
+
# Header with title and optional back button
|
|
57
|
+
yield HeaderWidget(title=self.screen_title, show_back=self.show_back)
|
|
58
|
+
|
|
59
|
+
# Content area - subclasses define this
|
|
60
|
+
yield from self.compose_content()
|
|
61
|
+
|
|
62
|
+
# StatusBar with current config values (optional)
|
|
63
|
+
if self.show_status_bar:
|
|
64
|
+
status_bar = StatusBarWidget(id="status-bar")
|
|
65
|
+
self._update_status_bar(status_bar)
|
|
66
|
+
yield status_bar
|
|
67
|
+
|
|
68
|
+
def _update_status_bar(self, status_bar: StatusBarWidget) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Update status bar with current config values.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
status_bar: StatusBarWidget to update
|
|
74
|
+
"""
|
|
75
|
+
# Get git status
|
|
76
|
+
git_branch = "N/A"
|
|
77
|
+
try:
|
|
78
|
+
git_plugin = self.config.registry.get_plugin("git")
|
|
79
|
+
if git_plugin and git_plugin.is_available():
|
|
80
|
+
git_client = git_plugin.get_client()
|
|
81
|
+
git_status = git_client.get_status()
|
|
82
|
+
git_branch = git_status.branch if git_status else "N/A"
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Get AI info directly from config
|
|
87
|
+
ai_info = "N/A"
|
|
88
|
+
if self.config.config and self.config.config.ai and self.config.config.ai.default:
|
|
89
|
+
default_provider_id = self.config.config.ai.default
|
|
90
|
+
if default_provider_id in self.config.config.ai.providers:
|
|
91
|
+
provider_cfg = self.config.config.ai.providers[default_provider_id]
|
|
92
|
+
provider_name = provider_cfg.provider
|
|
93
|
+
model = provider_cfg.model or "default"
|
|
94
|
+
ai_info = f"{provider_name}/{model}"
|
|
95
|
+
|
|
96
|
+
# Get project name directly from config
|
|
97
|
+
project_name = self.config.get_project_name() or "N/A"
|
|
98
|
+
|
|
99
|
+
# Update status bar
|
|
100
|
+
status_bar.git_branch = git_branch
|
|
101
|
+
status_bar.ai_info = ai_info
|
|
102
|
+
status_bar.project_name = project_name
|
|
103
|
+
|
|
104
|
+
def on_header_widget_back_pressed(self, message: HeaderWidget.BackPressed) -> None:
|
|
105
|
+
"""Handle back button press from header."""
|
|
106
|
+
self.action_go_back()
|
|
107
|
+
|
|
108
|
+
def action_go_back(self) -> None:
|
|
109
|
+
"""Go back to previous screen. Override in subclasses if needed."""
|
|
110
|
+
self.app.pop_screen()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Launcher Screen
|
|
3
|
+
|
|
4
|
+
Screen for launching external CLI tools (Claude, Gemini, etc.)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import Static, OptionList
|
|
9
|
+
from textual.widgets.option_list import Option
|
|
10
|
+
from textual.containers import Container
|
|
11
|
+
from textual.binding import Binding
|
|
12
|
+
|
|
13
|
+
from titan_cli.external_cli.launcher import CLILauncher
|
|
14
|
+
from titan_cli.external_cli.configs import CLI_REGISTRY
|
|
15
|
+
from .base import BaseScreen
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CLILauncherScreen(BaseScreen):
|
|
19
|
+
"""
|
|
20
|
+
Screen for selecting and launching external CLI tools.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
BINDINGS = [
|
|
24
|
+
Binding("escape", "back", "Back"),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
CSS = """
|
|
28
|
+
CLILauncherScreen {
|
|
29
|
+
align: center middle;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#launcher-container {
|
|
33
|
+
width: 70%;
|
|
34
|
+
height: auto;
|
|
35
|
+
background: $surface-lighten-1;
|
|
36
|
+
border: solid $primary;
|
|
37
|
+
margin: 1;
|
|
38
|
+
padding: 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#launcher-title {
|
|
42
|
+
text-align: center;
|
|
43
|
+
color: $primary;
|
|
44
|
+
text-style: bold;
|
|
45
|
+
margin-bottom: 2;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.info-text {
|
|
49
|
+
color: $text-muted;
|
|
50
|
+
text-align: center;
|
|
51
|
+
margin-bottom: 2;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#cli-options {
|
|
55
|
+
height: auto;
|
|
56
|
+
border: none;
|
|
57
|
+
background: $surface-lighten-1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#cli-options > .option-list--option {
|
|
61
|
+
padding: 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#cli-options > .option-list--option-highlighted {
|
|
65
|
+
padding: 1;
|
|
66
|
+
}
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, config):
|
|
70
|
+
super().__init__(config, show_back=True)
|
|
71
|
+
self.available_clis = self._get_available_clis()
|
|
72
|
+
|
|
73
|
+
def _get_available_clis(self) -> dict:
|
|
74
|
+
"""Get list of available CLI tools."""
|
|
75
|
+
available = {}
|
|
76
|
+
for cli_name, config in CLI_REGISTRY.items():
|
|
77
|
+
launcher = CLILauncher(
|
|
78
|
+
cli_name,
|
|
79
|
+
install_instructions=config.get("install_instructions"),
|
|
80
|
+
prompt_flag=config.get("prompt_flag")
|
|
81
|
+
)
|
|
82
|
+
if launcher.is_available():
|
|
83
|
+
available[cli_name] = {
|
|
84
|
+
"launcher": launcher,
|
|
85
|
+
"display_name": config.get("display_name", cli_name)
|
|
86
|
+
}
|
|
87
|
+
return available
|
|
88
|
+
|
|
89
|
+
def compose_content(self) -> ComposeResult:
|
|
90
|
+
"""Compose the CLI launcher screen."""
|
|
91
|
+
with Container(id="launcher-container"):
|
|
92
|
+
yield Static("🚀 Launch External CLI", id="launcher-title")
|
|
93
|
+
|
|
94
|
+
if not self.available_clis:
|
|
95
|
+
yield Static(
|
|
96
|
+
"⚠️ No external CLI tools found.\n\n"
|
|
97
|
+
"Install Claude CLI or Gemini CLI to use this feature.\n\n"
|
|
98
|
+
"Press ESC to go back.",
|
|
99
|
+
classes="info-text"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
yield Static(
|
|
103
|
+
"Select a CLI tool to launch:",
|
|
104
|
+
classes="info-text"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
options = [
|
|
108
|
+
Option(info["display_name"], id=cli_name)
|
|
109
|
+
for cli_name, info in self.available_clis.items()
|
|
110
|
+
]
|
|
111
|
+
yield OptionList(*options, id="cli-options")
|
|
112
|
+
|
|
113
|
+
def on_mount(self) -> None:
|
|
114
|
+
"""Focus the CLI list when mounted."""
|
|
115
|
+
if self.available_clis:
|
|
116
|
+
self.query_one("#cli-options").focus()
|
|
117
|
+
|
|
118
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
119
|
+
"""Handle CLI selection and launch immediately."""
|
|
120
|
+
cli_name = event.option.id
|
|
121
|
+
|
|
122
|
+
if cli_name not in self.available_clis:
|
|
123
|
+
self.app.notify("Invalid CLI selection", severity="error")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# Get launcher
|
|
127
|
+
launcher = self.available_clis[cli_name]["launcher"]
|
|
128
|
+
display_name = self.available_clis[cli_name]["display_name"]
|
|
129
|
+
|
|
130
|
+
# Notify user
|
|
131
|
+
self.app.notify(f"Launching {display_name}...")
|
|
132
|
+
|
|
133
|
+
# Suspend TUI and launch CLI
|
|
134
|
+
with self.app.suspend():
|
|
135
|
+
exit_code = launcher.launch(prompt=None, cwd=".")
|
|
136
|
+
|
|
137
|
+
# Show result
|
|
138
|
+
if exit_code == 0:
|
|
139
|
+
self.app.notify(f"{display_name} exited successfully", severity="information")
|
|
140
|
+
else:
|
|
141
|
+
self.app.notify(
|
|
142
|
+
f"{display_name} exited with code {exit_code}",
|
|
143
|
+
severity="warning"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Go back to main menu
|
|
147
|
+
self.action_back()
|
|
148
|
+
|
|
149
|
+
def action_back(self) -> None:
|
|
150
|
+
"""Go back to main menu."""
|
|
151
|
+
self.app.pop_screen()
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global Setup Wizard Screen
|
|
3
|
+
|
|
4
|
+
First-time installation wizard for configuring Titan globally.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import Static
|
|
9
|
+
from textual.containers import Container, Horizontal, VerticalScroll
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
|
|
12
|
+
from titan_cli.ui.tui.icons import Icons
|
|
13
|
+
from titan_cli.ui.tui.widgets import Text, DimText, Button, BoldText
|
|
14
|
+
from .base import BaseScreen
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StepIndicator(Static):
|
|
18
|
+
"""Widget showing a single step with status indicator."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, step_number: int, title: str, status: str = "pending"):
|
|
21
|
+
self.step_number = step_number
|
|
22
|
+
self.title = title
|
|
23
|
+
self.status = status
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
def render(self) -> str:
|
|
27
|
+
"""Render the step with appropriate icon."""
|
|
28
|
+
if self.status == "completed":
|
|
29
|
+
icon = Icons.SUCCESS
|
|
30
|
+
style = "dim"
|
|
31
|
+
elif self.status == "in_progress":
|
|
32
|
+
icon = Icons.RUNNING
|
|
33
|
+
style = "bold cyan"
|
|
34
|
+
else: # pending
|
|
35
|
+
icon = Icons.PENDING
|
|
36
|
+
style = "dim"
|
|
37
|
+
|
|
38
|
+
return f"[{style}]{icon} {self.step_number}. {self.title}[/{style}]"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GlobalSetupWizardScreen(BaseScreen):
|
|
42
|
+
"""
|
|
43
|
+
First-time setup wizard for Titan.
|
|
44
|
+
|
|
45
|
+
This wizard runs when Titan is launched for the first time
|
|
46
|
+
and ~/.titan/config.toml doesn't exist.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
BINDINGS = [
|
|
50
|
+
Binding("escape", "cancel", "Cancel"),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
CSS = """
|
|
54
|
+
GlobalSetupWizardScreen {
|
|
55
|
+
align: center middle;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#wizard-container {
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 1fr;
|
|
61
|
+
background: $surface-lighten-1;
|
|
62
|
+
padding: 0 2 1 2;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#steps-panel {
|
|
66
|
+
width: 20%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
border: round $primary;
|
|
69
|
+
border-title-align: center;
|
|
70
|
+
background: $surface-lighten-1;
|
|
71
|
+
padding: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#steps-content {
|
|
75
|
+
padding: 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
StepIndicator {
|
|
79
|
+
height: auto;
|
|
80
|
+
margin-bottom: 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#content-panel {
|
|
84
|
+
width: 80%;
|
|
85
|
+
height: 100%;
|
|
86
|
+
border: round $primary;
|
|
87
|
+
border-title-align: center;
|
|
88
|
+
background: $surface-lighten-1;
|
|
89
|
+
padding: 0;
|
|
90
|
+
layout: vertical;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#content-scroll {
|
|
94
|
+
height: 1fr;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#content-area {
|
|
98
|
+
padding: 1;
|
|
99
|
+
height: auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#content-title {
|
|
103
|
+
color: $accent;
|
|
104
|
+
text-style: bold;
|
|
105
|
+
margin-bottom: 2;
|
|
106
|
+
height: auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#content-body {
|
|
110
|
+
height: auto;
|
|
111
|
+
margin-bottom: 2;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#button-container {
|
|
115
|
+
height: auto;
|
|
116
|
+
padding: 1 2;
|
|
117
|
+
background: $surface-lighten-1;
|
|
118
|
+
border-top: solid $primary;
|
|
119
|
+
align: right middle;
|
|
120
|
+
}
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, config):
|
|
124
|
+
super().__init__(
|
|
125
|
+
config,
|
|
126
|
+
title=f"{Icons.SETTINGS} Titan Setup Wizard",
|
|
127
|
+
show_back=False,
|
|
128
|
+
show_status_bar=False
|
|
129
|
+
)
|
|
130
|
+
self.current_step = 0
|
|
131
|
+
self.wizard_data = {}
|
|
132
|
+
self._mounted = False
|
|
133
|
+
|
|
134
|
+
# Define all wizard steps
|
|
135
|
+
self.steps = [
|
|
136
|
+
{"id": "welcome", "title": "Welcome"},
|
|
137
|
+
{"id": "complete", "title": "Setup Complete"},
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
def compose_content(self) -> ComposeResult:
|
|
141
|
+
"""Compose the wizard screen with two panels."""
|
|
142
|
+
with Container(id="wizard-container"):
|
|
143
|
+
with Horizontal():
|
|
144
|
+
# Left panel: Steps
|
|
145
|
+
left_panel = VerticalScroll(id="steps-panel")
|
|
146
|
+
left_panel.border_title = "Setup Steps"
|
|
147
|
+
with left_panel:
|
|
148
|
+
with Container(id="steps-content"):
|
|
149
|
+
for i, step in enumerate(self.steps, 1):
|
|
150
|
+
status = "in_progress" if i == 1 else "pending"
|
|
151
|
+
yield StepIndicator(i, step["title"], status=status)
|
|
152
|
+
|
|
153
|
+
# Right panel: Content
|
|
154
|
+
right_panel = Container(id="content-panel")
|
|
155
|
+
right_panel.border_title = "Setup Configuration"
|
|
156
|
+
with right_panel:
|
|
157
|
+
with VerticalScroll(id="content-scroll"):
|
|
158
|
+
with Container(id="content-area"):
|
|
159
|
+
yield Static("", id="content-title")
|
|
160
|
+
yield Container(id="content-body")
|
|
161
|
+
|
|
162
|
+
# Bottom buttons
|
|
163
|
+
with Horizontal(id="button-container"):
|
|
164
|
+
yield Button("Back", variant="default", id="back-button", disabled=True)
|
|
165
|
+
yield Button("Next", variant="primary", id="next-button")
|
|
166
|
+
yield Button("Cancel", variant="default", id="cancel-button")
|
|
167
|
+
|
|
168
|
+
def on_mount(self) -> None:
|
|
169
|
+
"""Load the first step when mounted."""
|
|
170
|
+
if not self._mounted:
|
|
171
|
+
self._mounted = True
|
|
172
|
+
self.load_step(0)
|
|
173
|
+
|
|
174
|
+
def load_step(self, step_index: int) -> None:
|
|
175
|
+
"""Load content for the given step."""
|
|
176
|
+
self.current_step = step_index
|
|
177
|
+
step = self.steps[step_index]
|
|
178
|
+
|
|
179
|
+
# Update step indicators
|
|
180
|
+
for i, indicator in enumerate(self.query(StepIndicator)):
|
|
181
|
+
if i < step_index:
|
|
182
|
+
indicator.status = "completed"
|
|
183
|
+
elif i == step_index:
|
|
184
|
+
indicator.status = "in_progress"
|
|
185
|
+
else:
|
|
186
|
+
indicator.status = "pending"
|
|
187
|
+
indicator.refresh()
|
|
188
|
+
|
|
189
|
+
# Update buttons
|
|
190
|
+
back_button = self.query_one("#back-button", Button)
|
|
191
|
+
back_button.disabled = (step_index == 0)
|
|
192
|
+
|
|
193
|
+
# Change Next button label based on step
|
|
194
|
+
next_button = self.query_one("#next-button", Button)
|
|
195
|
+
if step_index == len(self.steps) - 1:
|
|
196
|
+
next_button.label = "Finish"
|
|
197
|
+
else:
|
|
198
|
+
next_button.label = "Next"
|
|
199
|
+
|
|
200
|
+
# Load step content
|
|
201
|
+
content_title = self.query_one("#content-title", Static)
|
|
202
|
+
content_body = self.query_one("#content-body", Container)
|
|
203
|
+
|
|
204
|
+
if step["id"] == "welcome":
|
|
205
|
+
self.load_welcome_step(content_title, content_body)
|
|
206
|
+
elif step["id"] == "complete":
|
|
207
|
+
self.load_complete_step(content_title, content_body)
|
|
208
|
+
|
|
209
|
+
def load_welcome_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
210
|
+
"""Load Welcome step."""
|
|
211
|
+
title_widget.update("Welcome to Titan CLI!")
|
|
212
|
+
|
|
213
|
+
# Clear previous content
|
|
214
|
+
body_widget.remove_children()
|
|
215
|
+
|
|
216
|
+
# Add welcome message
|
|
217
|
+
welcome_text = Text(
|
|
218
|
+
"Thank you for installing Titan CLI!\n\n"
|
|
219
|
+
"Titan is a powerful development tools orchestrator that helps you manage Git, GitHub, "
|
|
220
|
+
"Jira, and other services with AI-powered workflows.\n\n"
|
|
221
|
+
"This wizard will help you configure Titan for first-time use."
|
|
222
|
+
)
|
|
223
|
+
body_widget.mount(welcome_text)
|
|
224
|
+
|
|
225
|
+
# Add features info
|
|
226
|
+
features = DimText(
|
|
227
|
+
"\n\nKey Features:\n"
|
|
228
|
+
" • AI-powered commit messages and PR descriptions\n"
|
|
229
|
+
" • Automated workflows for common tasks\n"
|
|
230
|
+
" • Seamless integration with Git, GitHub, and Jira\n"
|
|
231
|
+
" • Extensible plugin system\n"
|
|
232
|
+
" • Modern terminal UI"
|
|
233
|
+
)
|
|
234
|
+
body_widget.mount(features)
|
|
235
|
+
|
|
236
|
+
# Add next steps
|
|
237
|
+
next_steps = Text(
|
|
238
|
+
"\n\nNext, you'll configure your AI provider (Claude or Gemini).\n"
|
|
239
|
+
"This is required to use Titan's AI-powered features."
|
|
240
|
+
)
|
|
241
|
+
body_widget.mount(next_steps)
|
|
242
|
+
|
|
243
|
+
def load_complete_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
244
|
+
"""Load Setup Complete step."""
|
|
245
|
+
title_widget.update("Setup Complete!")
|
|
246
|
+
body_widget.remove_children()
|
|
247
|
+
|
|
248
|
+
# Add completion message
|
|
249
|
+
completion_text = BoldText(
|
|
250
|
+
"Congratulations! Titan has been configured successfully.\n"
|
|
251
|
+
)
|
|
252
|
+
body_widget.mount(completion_text)
|
|
253
|
+
|
|
254
|
+
# Add next steps
|
|
255
|
+
next_steps = Text(
|
|
256
|
+
"\n\nWhat's Next?\n\n"
|
|
257
|
+
"When you run Titan from a project directory, you'll be prompted to configure "
|
|
258
|
+
"that specific project.\n\n"
|
|
259
|
+
"For now, Titan is ready to use!"
|
|
260
|
+
)
|
|
261
|
+
body_widget.mount(next_steps)
|
|
262
|
+
|
|
263
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
264
|
+
"""Handle button presses."""
|
|
265
|
+
if event.button.id == "next-button":
|
|
266
|
+
self.handle_next()
|
|
267
|
+
elif event.button.id == "back-button":
|
|
268
|
+
self.handle_back()
|
|
269
|
+
elif event.button.id == "cancel-button":
|
|
270
|
+
self.action_cancel()
|
|
271
|
+
|
|
272
|
+
def handle_next(self) -> None:
|
|
273
|
+
"""Move to next step or complete setup."""
|
|
274
|
+
# Validate and save current step data
|
|
275
|
+
if not self.validate_and_save_step():
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
# If on last step, complete setup
|
|
279
|
+
if self.current_step == len(self.steps) - 1:
|
|
280
|
+
self.complete_setup()
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
# If on welcome step, launch AI wizard before going to complete
|
|
284
|
+
if self.steps[self.current_step]["id"] == "welcome":
|
|
285
|
+
# Launch AI configuration wizard
|
|
286
|
+
from .ai_config_wizard import AIConfigWizardScreen
|
|
287
|
+
|
|
288
|
+
def on_ai_wizard_complete(result=None):
|
|
289
|
+
"""Callback when AI wizard is dismissed."""
|
|
290
|
+
import logging
|
|
291
|
+
logger = logging.getLogger('titan_cli.ui.tui.screens.project_setup_wizard')
|
|
292
|
+
logger.debug(f"AI wizard complete with result={result}")
|
|
293
|
+
|
|
294
|
+
# Only proceed if AI was configured successfully
|
|
295
|
+
if result is True:
|
|
296
|
+
logger.debug(f"AI configured successfully, moving to step {self.current_step + 1}")
|
|
297
|
+
self.load_step(self.current_step + 1)
|
|
298
|
+
else:
|
|
299
|
+
logger.debug("AI wizard cancelled, staying on current step")
|
|
300
|
+
self.app.notify(
|
|
301
|
+
"AI configuration is required to use Titan. Please configure an AI provider.",
|
|
302
|
+
severity="warning"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
self.app.push_screen(AIConfigWizardScreen(self.config), on_ai_wizard_complete)
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# Move to next step
|
|
309
|
+
if self.current_step < len(self.steps) - 1:
|
|
310
|
+
self.load_step(self.current_step + 1)
|
|
311
|
+
|
|
312
|
+
def validate_and_save_step(self) -> bool:
|
|
313
|
+
"""Validate and save data from current step."""
|
|
314
|
+
step = self.steps[self.current_step]
|
|
315
|
+
|
|
316
|
+
if step["id"] == "welcome":
|
|
317
|
+
# No validation needed for welcome step
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
# No other validation needed
|
|
321
|
+
return True
|
|
322
|
+
|
|
323
|
+
def handle_back(self) -> None:
|
|
324
|
+
"""Move to previous step."""
|
|
325
|
+
if self.current_step > 0:
|
|
326
|
+
self.load_step(self.current_step - 1)
|
|
327
|
+
|
|
328
|
+
def complete_setup(self) -> None:
|
|
329
|
+
"""Complete the global setup."""
|
|
330
|
+
import tomli
|
|
331
|
+
import tomli_w
|
|
332
|
+
from titan_cli.core.config import TitanConfig
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
# Create ~/.titan directory
|
|
336
|
+
global_config_path = TitanConfig.GLOBAL_CONFIG
|
|
337
|
+
global_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
|
|
339
|
+
# Load existing global config (AI wizard may have already written to it)
|
|
340
|
+
global_config_data = {}
|
|
341
|
+
if global_config_path.exists():
|
|
342
|
+
with open(global_config_path, "rb") as f:
|
|
343
|
+
global_config_data = tomli.load(f)
|
|
344
|
+
|
|
345
|
+
# Ensure version is set
|
|
346
|
+
if "version" not in global_config_data:
|
|
347
|
+
global_config_data["version"] = "1.0"
|
|
348
|
+
|
|
349
|
+
# Save global config (preserving any AI configuration)
|
|
350
|
+
with open(global_config_path, "wb") as f:
|
|
351
|
+
tomli_w.dump(global_config_data, f)
|
|
352
|
+
|
|
353
|
+
self.app.notify("Global setup completed successfully!", severity="information")
|
|
354
|
+
|
|
355
|
+
# Close wizard - the callback will handle navigation
|
|
356
|
+
self.dismiss()
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
self.app.notify(f"Failed to complete setup: {e}", severity="error")
|
|
360
|
+
|
|
361
|
+
def action_cancel(self) -> None:
|
|
362
|
+
"""Cancel the setup wizard."""
|
|
363
|
+
self.app.exit(message="Setup cancelled. Titan requires initial configuration to run.")
|