feat: add annotations, undo system, and schema updates

Add annotations as JSON column on tasks table with Annotate/Denotate
methods and CLI commands. Add undo system backed by change_log with
lightweight undo_stack table (capped at 10 entries). All mutating CLI
commands (add, done, delete, modify, start, stop) now record undo
entries. Undo restores prior task state from change_log data.

Schema changes (in v1 migration):
- annotations TEXT column on tasks
- undo_stack table
- annotations field in change_log triggers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 13:54:58 +01:00
parent 6fb8a40a43
commit 7aaaa86a0a
16 changed files with 753 additions and 76 deletions
+37
View File
@@ -198,6 +198,17 @@ func generateEditableContent(task *engine.Task) string {
sb.WriteString("tags: \n")
}
// Annotations
sb.WriteString("\n# Annotations (add/remove/modify lines below)\n")
if len(task.Annotations) > 0 {
for i, ann := range task.Annotations {
ts := time.Unix(ann.Timestamp, 0).Format("2006-01-02 15:04")
sb.WriteString(fmt.Sprintf("annotation.%d: %s | %s\n", i+1, ts, ann.Text))
}
}
sb.WriteString("# To add: annotation.N: YYYY-MM-DD HH:MM | text\n")
sb.WriteString("# To remove: delete the line\n")
return sb.String()
}
@@ -362,6 +373,32 @@ func applyNonStatusFields(task *engine.Task, fields map[string]string) error {
}
}
// Annotations - rebuild from annotation.N fields
var newAnnotations []engine.Annotation
for key, value := range fields {
if strings.HasPrefix(key, "annotation.") && value != "" {
parts := strings.SplitN(value, " | ", 2)
if len(parts) == 2 {
ts, err := time.Parse("2006-01-02 15:04", strings.TrimSpace(parts[0]))
if err != nil {
// If we can't parse the timestamp, use current time
ts = time.Now()
}
newAnnotations = append(newAnnotations, engine.Annotation{
Timestamp: ts.Unix(),
Text: strings.TrimSpace(parts[1]),
})
} else {
// No timestamp separator, treat entire value as text
newAnnotations = append(newAnnotations, engine.Annotation{
Timestamp: time.Now().Unix(),
Text: strings.TrimSpace(value),
})
}
}
}
task.Annotations = newAnnotations
// Tags - replace all tags
if tagsStr, ok := fields["tags"]; ok {
// Remove all existing tags