Skip to content

Commit 613ff53

Browse files
feat: implement recurring task support with weekly and monthly scheduling logic, and fix rendering issues
- fixes #17 - completes #19
1 parent e6ae9f1 commit 613ff53

22 files changed

Lines changed: 751 additions & 205 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ 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.0]
9+
- **Recurring Tasks**: Introduced a robust, minimal system for tasks that automatically reappear.
10+
- Supports Weekly recurrence (e.g. `mon,wed,fri`).
11+
- Supports Monthly recurrence (e.g. `15`).
12+
- Automatically generates the next instance when a recurring task is marked as done.
13+
- Smart due date computation with missed cycle protection.
14+
- Integrated into the task editor TUI for seamless workflow.
15+
- **Enhanced Recurrence Previews**: Added real-time "Next Occurrence" date previews to the task editor, providing instant feedback as you type recurrence rules.
16+
- **AI & MCP Recurrence Support**: Extended the Gemini-powered assistant and MCP server to support managing recurring tasks via natural language and external AI agents.
17+
- **Backward Compatibility**: Ensured that existing tasks and older database records default safely to `none` recurrence, preventing validation errors.
18+
- **Task ID Visibility**: Added a new setting to toggle the visibility of task IDs in the detail view.
19+
- Configurable via `config.toml` (`show_id = true/false`) or the Settings TUI.
20+
- Useful for users who prefer a cleaner interface without technical metadata.
21+
22+
### Fixed
23+
- **Background Bleed**: Background not fully filling terminal viewport when using colored themes.
24+
825
## [1.3.5]
926
- **CLI Validation**: Added robust validation for subcommands and flags. Kairo now warns the user and provides helpful guidance when an invalid command or flag is provided.
1027
- **Global Flags**: Added support for `-h`/`--help` and `-v`/`--version` as global flags.

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,17 @@ Your tasks are yours. They don't belong in someone else's cloud.
9292
### It's fast — genuinely fast
9393
Sub-millisecond fuzzy search. Full keyboard control. Vim bindings (`j/k/gg/G`). Natural language deadlines like `tomorrow 10am` or `next friday`. You never have to leave the keyboard.
9494

95+
### Recurring tasks
96+
Tasks can automatically reappear based on a schedule. Weekly (e.g. `mon,wed,fri`) or Monthly (e.g. `15`). When a recurring task is completed, Kairo automatically generates the next instance with a smart due date preview in the editor, ensuring you never miss a beat.
97+
9598
### It respects your data
9699
SQLite storage with WAL mode. Fully offline. Optional Git-backed sync — no backend, no account, no lock-in. Export to JSON, CSV, Markdown, or plain text whenever you want.
97100

98101
### It grows with you
99-
A Lua plugin system lets you hook into task events. A headless CLI API means you can automate anything. And an MCP server opens Kairo up to AI agents that can read and manage your tasks directly.
102+
A Lua plugin system lets you hook into task events. A headless CLI API means you can automate anything. And an MCP server opens Kairo up to AI agents that can read and manage your tasks directly — now with full support for recurring schedules.
100103

101104
### AI — when you want it, invisible when you don't
102-
Optional Gemini integration (`gemini-3.1-flash-lite-preview` / `gemini-2.0-flash-lite` / `gemini-2.5-flash-lite`). Toggle it with `ctrl+a`. It never runs unless you invoke it. Your workflow, your call.
105+
Optional Gemini integration (`gemini-3.1-flash-lite-preview` / `gemini-2.0-flash-lite` / `gemini-2.5-flash-lite`). Toggle it with `ctrl+a`. It never runs unless you invoke it. Now you can create and manage complex recurring tasks using simple natural language prompts. Your workflow, your call.
103106

104107
### Beautiful by default
105108
32 built-in themes. Live switching with `t`. Bento-style layout. Real-time Markdown preview (`ctrl+p`). Cinematic animations for create, complete, and delete (with a global toggle in `ctrl+s` to disable them for maximum speed). It's a terminal app that you'll actually enjoy looking at.
@@ -220,6 +223,15 @@ Auto-generated on first run:
220223
- **macOS:** `~/Library/Application Support/kairo/config.toml`
221224
- **Windows:** `%APPDATA%\kairo\config.toml`
222225

226+
| Option | Description | Default |
227+
|---|---|---|
228+
| `theme` | UI theme name | `catppuccin` |
229+
| `vim_mode` | Enable Vim keybindings | `false` |
230+
| `show_help` | Show help footer | `true` |
231+
| `show_id` | Show task IDs in detail view | `true` |
232+
| `animations` | Enable UI animations | `true` |
233+
| `rainbow` | Animated rainbow logo | `false` |
234+
223235
Prefer in-app settings? `ctrl+s` opens the settings menu.
224236

225237
---

VERSION.txt

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

configs/kairo.example.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
theme = "catppuccin"
33
vim_mode = false
44
show_help = true
5+
show_id = true
56
rainbow = false
67
animations = true
78
mcp_enabled = false

internal/ai/gemini.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Context Data: %s
6767
You have TOTAL control over the user's tasks, projects, UI themes, and Lua plugins through tool calls.
6868
You can:
6969
- Manage tasks (create, update, delete, list, tags, priority, status, deadline).
70+
- Recurring tasks: use 'recurrence' (none|weekly|monthly), 'recurrence_weekly' (e.g. ["mon", "wed"]), 'recurrence_monthly' (e.g. 15).
7071
- Change the UI theme (e.g. catppuccin, dracula, nord, midnight, etc.) using 'set_theme'.
7172
- Manage Lua plugins (list, read, write, delete) using 'plugin_*' actions.
7273
- Configure AI settings and export data.

internal/ai/tools.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func GetKairoTools() *genai.Tool {
3131
},
3232
"payload": {
3333
Type: genai.TypeObject,
34-
Description: "JSON payload for the action. For 'set_theme', use 'theme' (string). For 'plugin_list', use {}. For 'plugin_get/delete', use 'name' (string). For 'plugin_write', use 'name' and 'content' (string). Tasks use title, description, tags, priority, status, deadline.",
34+
Description: "JSON payload for the action. For 'set_theme', use 'theme' (string). For 'plugin_list', use {}. For 'plugin_get/delete', use 'name' (string). For 'plugin_write', use 'name' and 'content' (string). Tasks use title, description, tags, priority, status, deadline, recurrence (none|weekly|monthly), recurrence_weekly (array of strings), recurrence_monthly (number).",
3535
},
3636
},
3737
Required: []string{"action", "payload"},

internal/api/api.go

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -97,28 +97,34 @@ func (api *TaskAPI) cleanup(ctx context.Context) Response {
9797

9898
// TaskDTO is the data transfer object for tasks (matches Lua and JSON schema)
9999
type TaskDTO struct {
100-
ID string `json:"id"`
101-
Title string `json:"title"`
102-
Description string `json:"description,omitempty"`
103-
Tags []string `json:"tags,omitempty"`
104-
Priority int `json:"priority"`
105-
Status string `json:"status"`
106-
Deadline *string `json:"deadline,omitempty"`
107-
CreatedAt string `json:"created_at"`
108-
UpdatedAt string `json:"updated_at"`
100+
ID string `json:"id"`
101+
Title string `json:"title"`
102+
Description string `json:"description,omitempty"`
103+
Tags []string `json:"tags,omitempty"`
104+
Priority int `json:"priority"`
105+
Status string `json:"status"`
106+
Deadline *string `json:"deadline,omitempty"`
107+
Recurrence string `json:"recurrence,omitempty"`
108+
RecurrenceWeekly []string `json:"recurrence_weekly,omitempty"`
109+
RecurrenceMonthly int `json:"recurrence_monthly,omitempty"`
110+
CreatedAt string `json:"created_at"`
111+
UpdatedAt string `json:"updated_at"`
109112
}
110113

111114
// toDTO converts a core.Task to a DTO for serialization
112115
func toDTO(t core.Task) TaskDTO {
113116
dto := TaskDTO{
114-
ID: t.ID,
115-
Title: t.Title,
116-
Description: t.Description,
117-
Tags: t.Tags,
118-
Priority: int(t.Priority),
119-
Status: string(t.Status),
120-
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z"),
121-
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z"),
117+
ID: t.ID,
118+
Title: t.Title,
119+
Description: t.Description,
120+
Tags: t.Tags,
121+
Priority: int(t.Priority),
122+
Status: string(t.Status),
123+
Recurrence: string(t.Recurrence),
124+
RecurrenceWeekly: t.RecurrenceWeekly,
125+
RecurrenceMonthly: t.RecurrenceMonthly,
126+
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z"),
127+
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z"),
122128
}
123129
if t.Deadline != nil {
124130
s := t.Deadline.Format("2006-01-02T15:04:05Z")
@@ -130,12 +136,15 @@ func toDTO(t core.Task) TaskDTO {
130136
// handleCreate processes a create request
131137
func (api *TaskAPI) handleCreate(ctx context.Context, payload json.RawMessage) Response {
132138
type CreatePayload struct {
133-
Title string `json:"title"`
134-
Description string `json:"description,omitempty"`
135-
Tags []string `json:"tags,omitempty"`
136-
Priority *int `json:"priority,omitempty"`
137-
Status string `json:"status,omitempty"`
138-
Deadline *string `json:"deadline,omitempty"`
139+
Title string `json:"title"`
140+
Description string `json:"description,omitempty"`
141+
Tags []string `json:"tags,omitempty"`
142+
Priority *int `json:"priority,omitempty"`
143+
Status string `json:"status,omitempty"`
144+
Deadline *string `json:"deadline,omitempty"`
145+
Recurrence string `json:"recurrence,omitempty"`
146+
RecurrenceWeekly []string `json:"recurrence_weekly,omitempty"`
147+
RecurrenceMonthly *int `json:"recurrence_monthly,omitempty"`
139148
}
140149

141150
var p CreatePayload
@@ -154,10 +163,17 @@ func (api *TaskAPI) handleCreate(ctx context.Context, payload json.RawMessage) R
154163
}
155164

156165
task := core.Task{
157-
Title: p.Title,
158-
Description: p.Description,
159-
Tags: p.Tags,
160-
Status: core.StatusTodo,
166+
Title: p.Title,
167+
Description: p.Description,
168+
Tags: p.Tags,
169+
Status: core.StatusTodo,
170+
Recurrence: core.RecurrenceType(p.Recurrence),
171+
RecurrenceWeekly: p.RecurrenceWeekly,
172+
RecurrenceMonthly: 0,
173+
}
174+
175+
if p.RecurrenceMonthly != nil {
176+
task.RecurrenceMonthly = *p.RecurrenceMonthly
161177
}
162178

163179
if p.Status != "" {
@@ -225,13 +241,16 @@ func (api *TaskAPI) handleGet(ctx context.Context, payload json.RawMessage) Resp
225241
// handleUpdate processes an update request
226242
func (api *TaskAPI) handleUpdate(ctx context.Context, payload json.RawMessage) Response {
227243
type UpdatePayload struct {
228-
ID string `json:"id"`
229-
Title *string `json:"title,omitempty"`
230-
Description *string `json:"description,omitempty"`
231-
Tags []string `json:"tags,omitempty"`
232-
Priority *int `json:"priority,omitempty"`
233-
Status *string `json:"status,omitempty"`
234-
Deadline *string `json:"deadline,omitempty"`
244+
ID string `json:"id"`
245+
Title *string `json:"title,omitempty"`
246+
Description *string `json:"description,omitempty"`
247+
Tags []string `json:"tags,omitempty"`
248+
Priority *int `json:"priority,omitempty"`
249+
Status *string `json:"status,omitempty"`
250+
Deadline *string `json:"deadline,omitempty"`
251+
Recurrence *string `json:"recurrence,omitempty"`
252+
RecurrenceWeekly []string `json:"recurrence_weekly,omitempty"`
253+
RecurrenceMonthly *int `json:"recurrence_monthly,omitempty"`
235254
}
236255

237256
var p UpdatePayload
@@ -254,6 +273,17 @@ func (api *TaskAPI) handleUpdate(ctx context.Context, payload json.RawMessage) R
254273
Description: p.Description,
255274
}
256275

276+
if p.Recurrence != nil {
277+
r := core.RecurrenceType(*p.Recurrence)
278+
patch.Recurrence = &r
279+
}
280+
if len(p.RecurrenceWeekly) > 0 {
281+
patch.RecurrenceWeekly = &p.RecurrenceWeekly
282+
}
283+
if p.RecurrenceMonthly != nil {
284+
patch.RecurrenceMonthly = p.RecurrenceMonthly
285+
}
286+
257287
if len(p.Tags) > 0 {
258288
patch.Tags = &p.Tags
259289
}

internal/app/model.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
710710
cmds = append(cmds, cmd, m.listenAICmd())
711711

712712
if x.Chunk.Refresh {
713-
cmds = append(cmds, m.loadTagsCmd(), m.loadTasksCmd(), m.loadAllTasksCmd(), m.syncIfEnabledCmd())
713+
m.statusText = "AI updated database"
714+
m.isErr = false
715+
m.statusID++
716+
cmds = append(cmds, m.loadTagsCmd(), m.loadTasksCmd(), m.loadAllTasksCmd(), m.syncIfEnabledCmd(), m.clearStatusCmd(m.statusID))
714717
}
715718
return m, tea.Batch(cmds...)
716719

@@ -1351,6 +1354,7 @@ func (m *Model) renderMainUI() string {
13511354

13521355
// Update sizes dynamically — use mainW so components don't overflow
13531356
m.list.SetSize(mainW, availableHeight)
1357+
m.det.ShowID = m.cfg.App.ShowID
13541358
m.det.SetSize(mainW, availableHeight)
13551359
m.pal.SetSize(mainW, availableHeight)
13561360
m.pm.SetSize(mainW, availableHeight)

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type AppConfig struct {
2525
Theme string `toml:"theme"`
2626
VimMode bool `toml:"vim_mode"`
2727
ShowHelp bool `toml:"show_help"`
28+
ShowID bool `toml:"show_id"`
2829
Rainbow bool `toml:"rainbow"`
2930
GeminiAPIKey string `toml:"gemini_api_key"`
3031
AIModel string `toml:"ai_model"`
@@ -98,6 +99,7 @@ func Default() Config {
9899
Theme: "catppuccin",
99100
VimMode: false,
100101
ShowHelp: true,
102+
ShowID: true,
101103
Rainbow: false,
102104
AIModel: "gemini-3.1-flash-lite-preview",
103105
MCPEnabled: false,

0 commit comments

Comments
 (0)