Skip to content

Commit 2548f58

Browse files
intel352claude
andcommitted
feat: add splash screen and provider onboarding flow
- Branded ASCII art splash with tick-based reveal animation - Multi-step provider setup wizard (type, API key, URL, model, test) - Page state machine in app.go: splash → onboarding → chat - Supports Anthropic, OpenAI, Ollama, and Google Gemini providers - First provider configured becomes the default automatically Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8c506bc commit 2548f58

4 files changed

Lines changed: 835 additions & 53 deletions

File tree

internal/tui/app.go

Lines changed: 150 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,28 @@ import (
1515
"github.com/GoCodeAlone/ratchet-cli/internal/tui/theme"
1616
)
1717

18+
type appPage int
19+
20+
const (
21+
pageSplash appPage = iota
22+
pageOnboarding
23+
pageChat
24+
)
25+
26+
// ProvidersCheckedMsg carries the result of the async provider list check.
27+
type ProvidersCheckedMsg struct {
28+
Providers []*pb.Provider
29+
}
30+
1831
// App is the root Bubbletea v2 model.
1932
type App struct {
2033
client *client.Client
2134
sessionID string
35+
session *pb.Session
2236
chat pages.ChatModel
2337
team pages.TeamModel
38+
splash pages.SplashModel
39+
onboarding pages.OnboardingModel
2440
sidebar components.SidebarModel
2541
theme theme.Theme
2642
dark bool
@@ -29,110 +45,191 @@ type App struct {
2945
showSidebar bool
3046
showTeam bool
3147
ready bool
48+
page appPage
49+
50+
// Coordination between splash animation and provider check.
51+
splashDone bool
52+
providersReady bool
53+
providers []*pb.Provider
3254
}
3355

3456
// NewApp creates the root TUI application model.
3557
func NewApp(c *client.Client, session *pb.Session, t theme.Theme, dark bool) App {
36-
chat := pages.NewChat(c, session.GetId(), t, dark)
37-
team := pages.NewTeam()
58+
splash := pages.NewSplash()
3859
sidebar := components.NewSidebar([]*pb.Session{session}, session.GetId())
3960
return App{
4061
client: c,
4162
sessionID: session.GetId(),
42-
chat: chat,
43-
team: team,
63+
session: session,
64+
splash: splash,
4465
sidebar: sidebar,
4566
theme: t,
4667
dark: dark,
68+
page: pageSplash,
4769
}
4870
}
4971

5072
func (a App) Init() tea.Cmd {
5173
return tea.Batch(
52-
a.chat.Init(),
74+
a.splash.Init(),
75+
a.checkProviders(),
5376
)
5477
}
5578

79+
func (a App) checkProviders() tea.Cmd {
80+
return func() tea.Msg {
81+
resp, err := a.client.ListProviders(context.Background())
82+
if err != nil {
83+
return ProvidersCheckedMsg{Providers: nil}
84+
}
85+
return ProvidersCheckedMsg{Providers: resp.Providers}
86+
}
87+
}
88+
5689
func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5790
var cmds []tea.Cmd
5891

5992
switch msg := msg.(type) {
60-
case tea.KeyPressMsg:
61-
switch msg.String() {
62-
case "ctrl+c":
63-
return a, tea.Quit
64-
case "ctrl+d":
65-
// Detach: quit TUI, leave session running
66-
return a, tea.Quit
67-
case "ctrl+s":
68-
a.showSidebar = !a.showSidebar
69-
if a.showSidebar {
70-
a.showTeam = false
71-
}
72-
case "ctrl+t":
73-
a.showTeam = !a.showTeam
74-
if a.showTeam {
75-
a.showSidebar = false
76-
}
77-
}
7893
case tea.WindowSizeMsg:
7994
a.width = msg.Width
8095
a.height = msg.Height
8196
a.ready = true
97+
98+
case tea.KeyPressMsg:
99+
if msg.String() == "ctrl+c" {
100+
return a, tea.Quit
101+
}
102+
// Chat-only shortcuts
103+
if a.page == pageChat {
104+
switch msg.String() {
105+
case "ctrl+d":
106+
return a, tea.Quit
107+
case "ctrl+s":
108+
a.showSidebar = !a.showSidebar
109+
if a.showSidebar {
110+
a.showTeam = false
111+
}
112+
case "ctrl+t":
113+
a.showTeam = !a.showTeam
114+
if a.showTeam {
115+
a.showSidebar = false
116+
}
117+
}
118+
}
119+
120+
case pages.SplashDoneMsg:
121+
a.splashDone = true
122+
if a.providersReady {
123+
return a.transitionFromSplash()
124+
}
125+
return a, nil
126+
127+
case ProvidersCheckedMsg:
128+
a.providersReady = true
129+
a.providers = msg.Providers
130+
if a.splashDone {
131+
return a.transitionFromSplash()
132+
}
133+
return a, nil
134+
135+
case pages.OnboardingDoneMsg:
136+
return a.transitionToChat()
137+
82138
case components.SessionSelectedMsg:
83139
a.sessionID = msg.SessionID
84140
a.showSidebar = false
141+
85142
case components.SessionKillMsg:
86143
go func() {
87144
a.client.KillSession(context.Background(), msg.SessionID)
88145
}()
89146
}
90147

91-
// Route key events to active panel
92-
if a.showSidebar {
93-
var sidebarCmd tea.Cmd
94-
a.sidebar, sidebarCmd = a.sidebar.Update(msg)
95-
cmds = append(cmds, sidebarCmd)
96-
} else if a.showTeam {
97-
var teamCmd tea.Cmd
98-
a.team, teamCmd = a.team.Update(msg)
99-
cmds = append(cmds, teamCmd)
100-
} else {
101-
var chatCmd tea.Cmd
102-
a.chat, chatCmd = a.chat.Update(msg)
103-
cmds = append(cmds, chatCmd)
148+
// Route updates to active page
149+
switch a.page {
150+
case pageSplash:
151+
var splashCmd tea.Cmd
152+
a.splash, splashCmd = a.splash.Update(msg)
153+
cmds = append(cmds, splashCmd)
154+
155+
case pageOnboarding:
156+
var obCmd tea.Cmd
157+
a.onboarding, obCmd = a.onboarding.Update(msg)
158+
cmds = append(cmds, obCmd)
159+
160+
case pageChat:
161+
if a.showSidebar {
162+
var sidebarCmd tea.Cmd
163+
a.sidebar, sidebarCmd = a.sidebar.Update(msg)
164+
cmds = append(cmds, sidebarCmd)
165+
} else if a.showTeam {
166+
var teamCmd tea.Cmd
167+
a.team, teamCmd = a.team.Update(msg)
168+
cmds = append(cmds, teamCmd)
169+
} else {
170+
var chatCmd tea.Cmd
171+
a.chat, chatCmd = a.chat.Update(msg)
172+
cmds = append(cmds, chatCmd)
173+
}
104174
}
105175

106176
return a, tea.Batch(cmds...)
107177
}
108178

179+
func (a App) transitionFromSplash() (tea.Model, tea.Cmd) {
180+
if len(a.providers) == 0 {
181+
a.onboarding = pages.NewOnboarding(a.client, a.theme)
182+
a.page = pageOnboarding
183+
return a, a.onboarding.Init()
184+
}
185+
return a.transitionToChat()
186+
}
187+
188+
func (a App) transitionToChat() (tea.Model, tea.Cmd) {
189+
chat := pages.NewChat(a.client, a.sessionID, a.theme, a.dark)
190+
team := pages.NewTeam()
191+
a.chat = chat
192+
a.team = team
193+
a.page = pageChat
194+
return a, a.chat.Init()
195+
}
196+
109197
func (a App) View() tea.View {
110198
if !a.ready {
111199
v := tea.NewView("Connecting to ratchet daemon...")
112200
return v
113201
}
114202

115-
header := a.renderHeader()
116-
var body string
203+
var content string
204+
205+
switch a.page {
206+
case pageSplash:
207+
content = a.splash.View(a.theme, a.width, a.height)
208+
209+
case pageOnboarding:
210+
content = a.onboarding.View(a.theme, a.width, a.height)
117211

118-
switch {
119-
case a.showSidebar:
120-
sidebarWidth := 30
121-
if a.width > 0 && sidebarWidth > a.width/3 {
122-
sidebarWidth = a.width / 3
212+
case pageChat:
213+
header := a.renderHeader()
214+
var body string
215+
switch {
216+
case a.showSidebar:
217+
sidebarWidth := 30
218+
if a.width > 0 && sidebarWidth > a.width/3 {
219+
sidebarWidth = a.width / 3
220+
}
221+
sidebarView := a.sidebar.SetSize(sidebarWidth, a.height-3).View(a.theme)
222+
chatView := a.chat.View(a.theme)
223+
body = joinColumns(sidebarView, chatView, sidebarWidth, a.width)
224+
case a.showTeam:
225+
teamView := a.team.SetSize(a.width, a.height-3).View(a.theme)
226+
body = teamView
227+
default:
228+
body = a.chat.View(a.theme)
123229
}
124-
sidebarView := a.sidebar.SetSize(sidebarWidth, a.height-3).View(a.theme)
125-
chatView := a.chat.View(a.theme)
126-
body = joinColumns(sidebarView, chatView, sidebarWidth, a.width)
127-
case a.showTeam:
128-
teamView := a.team.SetSize(a.width, a.height-3).View(a.theme)
129-
body = teamView
130-
default:
131-
body = a.chat.View(a.theme)
230+
content = header + "\n" + body
132231
}
133232

134-
content := header + "\n" + body
135-
136233
view := tea.NewView(content)
137234
view.AltScreen = true
138235
return view

0 commit comments

Comments
 (0)