Skip to content

Commit a2504ed

Browse files
feat: implement onboarding tour and architectural improvements for internal model and config management
1 parent 613ff53 commit a2504ed

8 files changed

Lines changed: 337 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.4.1]
9+
10+
### Added
11+
12+
- **Interactive Onboarding**: A complete, keyboard-driven welcome tour for new users with a "smooth af" animated slanted logo that cycles through theme colors. Teaches core navigation, task creation, and completion in under 60 seconds.
13+
- **Onboarding Trigger**: Added `ctrl+d` as a global shortcut to relaunch the welcome tour at any time.
14+
- **Auto-Onboarding**: Kairo now automatically launches the welcome tour on new installations to ensure a smooth first-time experience.
15+
- **AI Assistant Polish**: The AI Assistant shortcut (`ctrl+a`), footer pill, and help menu entry are now fully disabled and hidden if no Gemini API key is configured, providing a cleaner interface for new users.
16+
817
## [1.4.0]
918
- **Recurring Tasks**: Introduced a robust, minimal system for tasks that automatically reappear.
1019
- Supports Weekly recurrence (e.g. `mon,wed,fri`).

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,11 @@ Optional Gemini integration (`gemini-3.1-flash-lite-preview` / `gemini-2.0-flash
120120
| `t` | Switch theme |
121121
| `f` | Filter by tag |
122122
| `ctrl+p` | Command palette / Preview |
123-
| `ctrl+a` | AI panel |
124-
| `?` | Help |
125-
| `ctrl+s` | Settings |
126-
| `x` | Import/Export |
123+
| `ctrl+a` | AI panel | Hidden if API key is missing |
124+
| `?` | Help | |
125+
| `ctrl+s` | Settings | |
126+
| `x` | Import/Export | |
127+
| `ctrl+d` | Welcome tour | |
127128

128129
---
129130

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.4.0
1+
1.4.1

internal/app/model.go

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/programmersd21/kairo/internal/ui/help"
3333
"github.com/programmersd21/kairo/internal/ui/import_export_menu"
3434
"github.com/programmersd21/kairo/internal/ui/keymap"
35+
"github.com/programmersd21/kairo/internal/ui/onboarding"
3536
"github.com/programmersd21/kairo/internal/ui/palette"
3637
"github.com/programmersd21/kairo/internal/ui/plugin_menu"
3738
"github.com/programmersd21/kairo/internal/ui/render"
@@ -94,6 +95,7 @@ const (
9495
ModeConfirmQuit
9596
ModeSettings
9697
ModeImportExport
98+
ModeOnboarding
9799
)
98100

99101
type Model struct {
@@ -122,6 +124,7 @@ type Model struct {
122124
det detail.Model
123125
edit *editor.Model
124126
hlp help.Model
127+
onb onboarding.Model
125128
tm theme_menu.Model
126129
pm plugin_menu.Model
127130
set settings.Model
@@ -222,6 +225,7 @@ func New(ctx context.Context, cfg config.Config, svc service.TaskService) (tea.M
222225
m.pal = palette.New(m.s)
223226
m.det = detail.New(m.s)
224227
m.hlp = help.New(m.s, m.km)
228+
m.onb = onboarding.New(m.s, m.km)
225229
m.tm = theme_menu.New(m.s, nil)
226230
m.pm = plugin_menu.New(m.s)
227231
m.set = settings.New(m.s, cfg)
@@ -316,6 +320,11 @@ func New(ctx context.Context, cfg config.Config, svc service.TaskService) (tea.M
316320

317321
m.rebuildViews()
318322
m.activeIdx = 0
323+
324+
if !m.cfg.App.OnboardingCompleted {
325+
m.mode = ModeOnboarding
326+
}
327+
319328
return m, nil
320329
}
321330

@@ -357,6 +366,9 @@ func (m *Model) Init() tea.Cmd {
357366
if m.aiChan != nil {
358367
cmds = append(cmds, m.listenAICmd())
359368
}
369+
if m.mode == ModeOnboarding {
370+
cmds = append(cmds, m.onb.Init())
371+
}
360372
if m.cfg.App.MCPEnabled {
361373
cmds = append(cmds, m.startMCPCmd())
362374
}
@@ -391,6 +403,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
391403
case tea.WindowSizeMsg:
392404
m.width, m.height = x.Width, x.Height
393405
m.list.SetSize(x.Width, x.Height)
406+
m.onb.SetSize(x.Width, x.Height)
394407
m.pal.SetSize(x.Width, x.Height)
395408
m.det.SetSize(x.Width, x.Height)
396409
m.tm.SetSize(x.Width, x.Height)
@@ -436,6 +449,20 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
436449
m.rebuildPaletteIndex()
437450
return m, nil
438451

452+
case onboarding.CloseMsg:
453+
m.mode = ModeList
454+
if !x.Skipped {
455+
m.cfg.App.OnboardingCompleted = true
456+
_ = m.cfg.Save()
457+
}
458+
if m.cfg.App.Animations {
459+
m.transitioning = true
460+
m.transitionStarted = time.Now()
461+
m.animationGen++
462+
return m, m.viewTransitionTickCmd()
463+
}
464+
return m, nil
465+
439466
case palette.CloseMsg:
440467
if m.mode == ModePalette {
441468
m.mode = ModeList
@@ -491,6 +518,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
491518
if m.aiKey != "" {
492519
m.aiClient, _ = ai.NewClient(m.ctx, m.aiKey, m.cfg.App.AIModel)
493520
ai.SetService(m.svc)
521+
} else {
522+
m.aiClient = nil
494523
}
495524
m.km = keymap.FromConfig(m.cfg.Keymap)
496525
m.thBuiltin = theme.FindBuiltin(m.cfg.App.Theme)
@@ -980,6 +1009,20 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9801009
return m, nil
9811010
}
9821011

1012+
if km.String() == "ctrl+d" {
1013+
m.onb = onboarding.New(m.s, m.km)
1014+
m.onb.SetSize(m.width, m.height)
1015+
m.mode = ModeOnboarding
1016+
var animCmd tea.Cmd
1017+
if m.cfg.App.Animations {
1018+
m.transitioning = true
1019+
m.transitionStarted = time.Now()
1020+
m.animationGen++
1021+
animCmd = m.viewTransitionTickCmd()
1022+
}
1023+
return m, tea.Batch(m.onb.Init(), animCmd)
1024+
}
1025+
9831026
if keymapMatch(m.km.AIPanelToggle, km) {
9841027
m.aiPanel.Toggle()
9851028
m.aiPanel.SetSize(m.width, m.height)
@@ -1294,6 +1337,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
12941337
var cmd tea.Cmd
12951338
m.iem, cmd = m.iem.Update(msg)
12961339
return m, cmd
1340+
case ModeOnboarding:
1341+
var cmd tea.Cmd
1342+
m.onb, cmd = m.onb.Update(msg)
1343+
return m, cmd
12971344
}
12981345

12991346
return m, nil
@@ -1360,6 +1407,7 @@ func (m *Model) renderMainUI() string {
13601407
m.pm.SetSize(mainW, availableHeight)
13611408
m.set.SetSize(mainW, availableHeight)
13621409
m.hlp.SetSize(mainW, availableHeight)
1410+
m.hlp.AIEnabled = m.aiKey != ""
13631411
m.tm.SetSize(mainW, availableHeight)
13641412
m.iem.SetSize(mainW, availableHeight)
13651413
if m.edit != nil {
@@ -1403,6 +1451,8 @@ func (m *Model) renderMainUI() string {
14031451
}
14041452
case ModeImportExport:
14051453
body = m.iem.View()
1454+
case ModeOnboarding:
1455+
body = m.list.View()
14061456
default:
14071457
body = m.list.View()
14081458
}
@@ -1438,6 +1488,15 @@ func (m *Model) renderMainUI() string {
14381488
}
14391489

14401490
content := lipgloss.JoinVertical(lipgloss.Left, head, body, foot)
1491+
1492+
if m.mode == ModeOnboarding {
1493+
content = lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
1494+
m.onb.View(),
1495+
lipgloss.WithWhitespaceChars(" "),
1496+
lipgloss.WithWhitespaceBackground(m.s.Theme.Bg),
1497+
)
1498+
}
1499+
14411500
if m.aiPanel.Visible {
14421501
return lipgloss.JoinHorizontal(lipgloss.Top, content, m.aiPanel.View())
14431502
}
@@ -1780,9 +1839,11 @@ func (m *Model) renderFooter() string {
17801839
makePill(fk(m.km.ToggleStrike) + " " + styles.IconStrike + "done"),
17811840
makePill(fk(m.km.DeleteTask) + " " + styles.IconDelete + "delete"),
17821841
makePill(fk(m.km.Settings) + " settings"),
1783-
makePill(fk(m.km.AIPanelToggle) + " assistant"),
1784-
makePill(fk(m.km.Help) + " " + styles.IconHelp + "help"),
17851842
}
1843+
if m.aiKey != "" {
1844+
items = append(items, makePill(fk(m.km.AIPanelToggle)+" assistant"))
1845+
}
1846+
items = append(items, makePill(fk(m.km.Help)+" "+styles.IconHelp+"help"))
17861847
left = " " + strings.Join(items, sep)
17871848
}
17881849
}
@@ -2090,8 +2151,13 @@ func (m *Model) rebuildPaletteIndex() {
20902151
search.Item{ID: "cmd:view:tag", Kind: search.KindCommand, Title: "View: By Tag", Hint: "f"},
20912152
search.Item{ID: "cmd:view:priority", Kind: search.KindCommand, Title: "View: By Priority", Hint: "5"},
20922153
search.Item{ID: "cmd:import-export", Kind: search.KindCommand, Title: "Import/Export", Hint: "x"},
2154+
search.Item{ID: "cmd:onboarding", Kind: search.KindCommand, Title: "Welcome Tour", Hint: "ctrl+d"},
20932155
)
20942156

2157+
if m.aiKey != "" {
2158+
items = append(items, search.Item{ID: "cmd:ai", Kind: search.KindCommand, Title: "AI Assistant", Hint: "ctrl+a"})
2159+
}
2160+
20952161
for _, t := range m.tags {
20962162
items = append(items, search.Item{ID: t, Kind: search.KindTag, Title: "#" + t, Hint: "tag"})
20972163
}
@@ -2194,6 +2260,11 @@ func (m *Model) runCommand(id string) tea.Cmd {
21942260
case "cmd:import-export":
21952261
m.mode = ModeImportExport
21962262
return nil
2263+
case "cmd:onboarding":
2264+
m.onb = onboarding.New(m.s, m.km)
2265+
m.onb.SetSize(m.width, m.height)
2266+
m.mode = ModeOnboarding
2267+
return m.onb.Init()
21972268
}
21982269

21992270
if strings.HasPrefix(id, "cmd:view:plugin:") {

internal/config/config.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ type Config struct {
2222
}
2323

2424
type AppConfig struct {
25-
Theme string `toml:"theme"`
26-
VimMode bool `toml:"vim_mode"`
27-
ShowHelp bool `toml:"show_help"`
28-
ShowID bool `toml:"show_id"`
29-
Rainbow bool `toml:"rainbow"`
30-
GeminiAPIKey string `toml:"gemini_api_key"`
31-
AIModel string `toml:"ai_model"`
32-
MCPEnabled bool `toml:"mcp_enabled"`
33-
MCPPort string `toml:"mcp_port"`
34-
Animations bool `toml:"animations"`
25+
Theme string `toml:"theme"`
26+
VimMode bool `toml:"vim_mode"`
27+
ShowHelp bool `toml:"show_help"`
28+
ShowID bool `toml:"show_id"`
29+
Rainbow bool `toml:"rainbow"`
30+
GeminiAPIKey string `toml:"gemini_api_key"`
31+
AIModel string `toml:"ai_model"`
32+
MCPEnabled bool `toml:"mcp_enabled"`
33+
MCPPort string `toml:"mcp_port"`
34+
Animations bool `toml:"animations"`
35+
OnboardingCompleted bool `toml:"onboarding_completed"`
3536
}
3637

3738
type StorageConfig struct {

internal/lua/engine.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (e *Engine) SetupKairoAPI(L *lua.LState) {
6161
L.SetField(kairo, "notify", L.NewFunction(e.luaNotify))
6262

6363
// Meta
64-
L.SetField(kairo, "version", lua.LString("1.4.0"))
64+
L.SetField(kairo, "version", lua.LString("1.4.1"))
6565

6666
// Set as global
6767
L.SetGlobal("kairo", kairo)

internal/ui/help/model.go

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import (
1414
type CloseMsg struct{}
1515

1616
type Model struct {
17-
styles styles.Styles
18-
km keymap.Keymap
19-
width int
20-
height int
17+
styles styles.Styles
18+
km keymap.Keymap
19+
width int
20+
height int
21+
AIEnabled bool
2122
}
2223

2324
func New(s styles.Styles, km keymap.Keymap) Model {
@@ -59,6 +60,35 @@ func (m Model) View() string {
5960
return strings.Join(b.Keys(), ", ")
6061
}
6162

63+
appKeys := []struct {
64+
key string
65+
desc string
66+
}{
67+
{getK(m.km.Palette), styles.IconPalette + "Command palette"},
68+
{getK(m.km.TaskSearch), "󰍉 " + "Search tasks"},
69+
{getK(m.km.CycleTheme), "󰏘 " + "Theme menu"},
70+
{getK(m.km.OpenPluginDir), "󰝰 " + "Open plugins folder"},
71+
{getK(m.km.ManagePlugins), styles.IconPlugin + "Manage plugins"},
72+
{getK(m.km.Settings), "󰒓 " + "Settings menu"},
73+
{getK(m.km.ImportExport), "󰛖 " + "Import / Export menu"},
74+
{"ctrl+d", "󰙠 " + "Welcome tour"},
75+
}
76+
77+
if m.AIEnabled {
78+
appKeys = append(appKeys, struct{ key, desc string }{getK(m.km.AIPanelToggle), "🤖 " + "AI Assistant panel"})
79+
}
80+
81+
appKeys = append(appKeys, []struct {
82+
key string
83+
desc string
84+
}{
85+
{getK(m.km.Help), styles.IconHelp + "Show help"},
86+
{getK(m.km.Issues), styles.IconIssues + "Open GitHub issues"},
87+
{getK(m.km.Discussions), styles.IconDiscuss + "Open GitHub discussions"},
88+
{getK(m.km.Changelog), styles.IconChangelog + "Open changelog"},
89+
{getK(m.km.Quit), "󰈆 " + "Quit"},
90+
}...)
91+
6292
sections := []struct {
6393
title string
6494
keys []struct {
@@ -86,21 +116,7 @@ func (m Model) View() string {
86116
},
87117
{
88118
"App",
89-
[]struct{ key, desc string }{
90-
{getK(m.km.Palette), styles.IconPalette + "Command palette"},
91-
{getK(m.km.TaskSearch), "󰍉 " + "Search tasks"},
92-
{getK(m.km.CycleTheme), "󰏘 " + "Theme menu"},
93-
{getK(m.km.OpenPluginDir), "󰝰 " + "Open plugins folder"},
94-
{getK(m.km.ManagePlugins), styles.IconPlugin + "Manage plugins"},
95-
{getK(m.km.Settings), "󰒓 " + "Settings menu"},
96-
{getK(m.km.ImportExport), "󰛖 " + "Import / Export menu"},
97-
{getK(m.km.AIPanelToggle), "🤖 " + "AI Assistant panel"},
98-
{getK(m.km.Help), styles.IconHelp + "Show help"},
99-
{getK(m.km.Issues), styles.IconIssues + "Open GitHub issues"},
100-
{getK(m.km.Discussions), styles.IconDiscuss + "Open GitHub discussions"},
101-
{getK(m.km.Changelog), styles.IconChangelog + "Open changelog"},
102-
{getK(m.km.Quit), "󰈆 " + "Quit"},
103-
},
119+
appKeys,
104120
},
105121
}
106122

0 commit comments

Comments
 (0)