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.
Files changed (146) hide show
  1. titan_cli/__init__.py +3 -0
  2. titan_cli/__main__.py +4 -0
  3. titan_cli/ai/__init__.py +0 -0
  4. titan_cli/ai/agents/__init__.py +15 -0
  5. titan_cli/ai/agents/base.py +152 -0
  6. titan_cli/ai/client.py +170 -0
  7. titan_cli/ai/constants.py +56 -0
  8. titan_cli/ai/exceptions.py +48 -0
  9. titan_cli/ai/models.py +34 -0
  10. titan_cli/ai/oauth_helper.py +120 -0
  11. titan_cli/ai/providers/__init__.py +9 -0
  12. titan_cli/ai/providers/anthropic.py +117 -0
  13. titan_cli/ai/providers/base.py +75 -0
  14. titan_cli/ai/providers/gemini.py +278 -0
  15. titan_cli/cli.py +59 -0
  16. titan_cli/clients/__init__.py +1 -0
  17. titan_cli/clients/gcloud_client.py +52 -0
  18. titan_cli/core/__init__.py +3 -0
  19. titan_cli/core/config.py +274 -0
  20. titan_cli/core/discovery.py +51 -0
  21. titan_cli/core/errors.py +81 -0
  22. titan_cli/core/models.py +52 -0
  23. titan_cli/core/plugins/available.py +36 -0
  24. titan_cli/core/plugins/models.py +67 -0
  25. titan_cli/core/plugins/plugin_base.py +108 -0
  26. titan_cli/core/plugins/plugin_registry.py +163 -0
  27. titan_cli/core/secrets.py +141 -0
  28. titan_cli/core/workflows/__init__.py +22 -0
  29. titan_cli/core/workflows/models.py +88 -0
  30. titan_cli/core/workflows/project_step_source.py +86 -0
  31. titan_cli/core/workflows/workflow_exceptions.py +17 -0
  32. titan_cli/core/workflows/workflow_filter_service.py +137 -0
  33. titan_cli/core/workflows/workflow_registry.py +419 -0
  34. titan_cli/core/workflows/workflow_sources.py +307 -0
  35. titan_cli/engine/__init__.py +39 -0
  36. titan_cli/engine/builder.py +159 -0
  37. titan_cli/engine/context.py +82 -0
  38. titan_cli/engine/mock_context.py +176 -0
  39. titan_cli/engine/results.py +91 -0
  40. titan_cli/engine/steps/ai_assistant_step.py +185 -0
  41. titan_cli/engine/steps/command_step.py +93 -0
  42. titan_cli/engine/utils/__init__.py +3 -0
  43. titan_cli/engine/utils/venv.py +31 -0
  44. titan_cli/engine/workflow_executor.py +187 -0
  45. titan_cli/external_cli/__init__.py +0 -0
  46. titan_cli/external_cli/configs.py +17 -0
  47. titan_cli/external_cli/launcher.py +65 -0
  48. titan_cli/messages.py +121 -0
  49. titan_cli/ui/tui/__init__.py +205 -0
  50. titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
  51. titan_cli/ui/tui/app.py +113 -0
  52. titan_cli/ui/tui/icons.py +70 -0
  53. titan_cli/ui/tui/screens/__init__.py +24 -0
  54. titan_cli/ui/tui/screens/ai_config.py +498 -0
  55. titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
  56. titan_cli/ui/tui/screens/base.py +110 -0
  57. titan_cli/ui/tui/screens/cli_launcher.py +151 -0
  58. titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
  59. titan_cli/ui/tui/screens/main_menu.py +162 -0
  60. titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
  61. titan_cli/ui/tui/screens/plugin_management.py +377 -0
  62. titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
  63. titan_cli/ui/tui/screens/workflow_execution.py +592 -0
  64. titan_cli/ui/tui/screens/workflows.py +249 -0
  65. titan_cli/ui/tui/textual_components.py +537 -0
  66. titan_cli/ui/tui/textual_workflow_executor.py +405 -0
  67. titan_cli/ui/tui/theme.py +102 -0
  68. titan_cli/ui/tui/widgets/__init__.py +40 -0
  69. titan_cli/ui/tui/widgets/button.py +108 -0
  70. titan_cli/ui/tui/widgets/header.py +116 -0
  71. titan_cli/ui/tui/widgets/panel.py +81 -0
  72. titan_cli/ui/tui/widgets/status_bar.py +115 -0
  73. titan_cli/ui/tui/widgets/table.py +77 -0
  74. titan_cli/ui/tui/widgets/text.py +177 -0
  75. titan_cli/utils/__init__.py +0 -0
  76. titan_cli/utils/autoupdate.py +155 -0
  77. titan_cli-0.1.0.dist-info/METADATA +149 -0
  78. titan_cli-0.1.0.dist-info/RECORD +146 -0
  79. titan_cli-0.1.0.dist-info/WHEEL +4 -0
  80. titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
  81. titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  82. titan_plugin_git/__init__.py +1 -0
  83. titan_plugin_git/clients/__init__.py +8 -0
  84. titan_plugin_git/clients/git_client.py +772 -0
  85. titan_plugin_git/exceptions.py +40 -0
  86. titan_plugin_git/messages.py +112 -0
  87. titan_plugin_git/models.py +39 -0
  88. titan_plugin_git/plugin.py +118 -0
  89. titan_plugin_git/steps/__init__.py +1 -0
  90. titan_plugin_git/steps/ai_commit_message_step.py +171 -0
  91. titan_plugin_git/steps/branch_steps.py +104 -0
  92. titan_plugin_git/steps/commit_step.py +80 -0
  93. titan_plugin_git/steps/push_step.py +63 -0
  94. titan_plugin_git/steps/status_step.py +59 -0
  95. titan_plugin_git/workflows/__previews__/__init__.py +1 -0
  96. titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
  97. titan_plugin_git/workflows/commit-ai.yaml +28 -0
  98. titan_plugin_github/__init__.py +11 -0
  99. titan_plugin_github/agents/__init__.py +6 -0
  100. titan_plugin_github/agents/config_loader.py +130 -0
  101. titan_plugin_github/agents/issue_generator.py +353 -0
  102. titan_plugin_github/agents/pr_agent.py +528 -0
  103. titan_plugin_github/clients/__init__.py +8 -0
  104. titan_plugin_github/clients/github_client.py +1105 -0
  105. titan_plugin_github/config/__init__.py +0 -0
  106. titan_plugin_github/config/pr_agent.toml +85 -0
  107. titan_plugin_github/exceptions.py +28 -0
  108. titan_plugin_github/messages.py +88 -0
  109. titan_plugin_github/models.py +330 -0
  110. titan_plugin_github/plugin.py +131 -0
  111. titan_plugin_github/steps/__init__.py +12 -0
  112. titan_plugin_github/steps/ai_pr_step.py +172 -0
  113. titan_plugin_github/steps/create_pr_step.py +86 -0
  114. titan_plugin_github/steps/github_prompt_steps.py +171 -0
  115. titan_plugin_github/steps/issue_steps.py +143 -0
  116. titan_plugin_github/steps/preview_step.py +40 -0
  117. titan_plugin_github/utils.py +82 -0
  118. titan_plugin_github/workflows/__previews__/__init__.py +1 -0
  119. titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
  120. titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
  121. titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
  122. titan_plugin_jira/__init__.py +8 -0
  123. titan_plugin_jira/agents/__init__.py +6 -0
  124. titan_plugin_jira/agents/config_loader.py +154 -0
  125. titan_plugin_jira/agents/jira_agent.py +553 -0
  126. titan_plugin_jira/agents/prompts.py +364 -0
  127. titan_plugin_jira/agents/response_parser.py +435 -0
  128. titan_plugin_jira/agents/token_tracker.py +223 -0
  129. titan_plugin_jira/agents/validators.py +246 -0
  130. titan_plugin_jira/clients/jira_client.py +745 -0
  131. titan_plugin_jira/config/jira_agent.toml +92 -0
  132. titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
  133. titan_plugin_jira/exceptions.py +37 -0
  134. titan_plugin_jira/formatters/__init__.py +6 -0
  135. titan_plugin_jira/formatters/markdown_formatter.py +245 -0
  136. titan_plugin_jira/messages.py +115 -0
  137. titan_plugin_jira/models.py +89 -0
  138. titan_plugin_jira/plugin.py +264 -0
  139. titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
  140. titan_plugin_jira/steps/get_issue_step.py +82 -0
  141. titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
  142. titan_plugin_jira/steps/search_saved_query_step.py +238 -0
  143. titan_plugin_jira/utils/__init__.py +13 -0
  144. titan_plugin_jira/utils/issue_sorter.py +140 -0
  145. titan_plugin_jira/utils/saved_queries.py +150 -0
  146. 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.")