package main import ( "context" "log" "strings" "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/router" "github.com/kami/maven/internal/store" ) // Task capture on the voice/chat path (Vikunja #130). // // Two halves, both deliberately small: // // - captureTaskFromNote runs at the top of actionNote. An utterance that // explicitly files a task ("добавь в задачи купить молоко") goes to the task // store instead of the note store. Anything without an explicit marker is // still a note — see router.ParseTaskCapture for why "надо бы поспать" must // not become a task. // - queryTasks is a query source that reads the list back. // // Nothing here speaks unprompted. Tasks are answered when asked about; no tick // rule reads the table. // captureTaskFromNote claims the turn when the utterance explicitly files a // task, returning the reply. ("", false) hands the turn back to the note path. func (h *reactiveHandler) captureTaskFromNote(ctx context.Context, dec router.Decision) (string, bool) { text, ok := router.ParseTaskCapture(dec.Utterance) if !ok { return "", false } resp, err := h.api.CaptureTask(ctx, ipc.CaptureTaskReq{ Text: text, Source: "tap:voice", Status: store.TaskOpen, // he stated it himself — not a candidate Ts: h.now(), }) if err != nil { log.Printf("voice: capture task: %v", err) return "не получилось записать задачу.", true } if !resp.Created { return "это уже в списке.", true } return "записала: " + text, true } // queryTasks — "какие у меня задачи?", "что мне нужно сделать?". // // Reads the live set and recites it. Newest first, which is the order the store // returns: this source has no opinion about which task matters more, and // pretending otherwise would be a guess. Ranking is Vikunja #129. func (h *reactiveHandler) queryTasks(ctx context.Context, t *queryTurn) (string, bool) { if !router.IsTaskListQuery(t.dec.Utterance) { return "", false } tasks, err := h.api.ListTasks(ctx, "live") if err != nil { log.Printf("voice: list tasks: %v", err) return "не получилось посмотреть задачи.", true } return formatTaskListRU(tasks), true } // formatTaskListRU renders the live task list the way Maven says it. Candidates // are named as candidates — a task she pulled out of his mail is something she // suggests, and saying it in the same breath as work he actually stated would // put words in his mouth. func formatTaskListRU(tasks []ipc.Task) string { var open, cands []string for _, t := range tasks { switch t.Status { case store.TaskCandidate: cands = append(cands, t.Text) default: open = append(open, t.Text) } } if len(open) == 0 && len(cands) == 0 { return "задач нет." } var b strings.Builder if len(open) > 0 { b.WriteString("в списке: ") b.WriteString(strings.Join(open, "; ")) b.WriteString(".") } if len(cands) > 0 { if b.Len() > 0 { b.WriteString(" ") } b.WriteString("ещё я нашла, но ты не подтвердил: ") b.WriteString(strings.Join(cands, "; ")) b.WriteString(".") } return b.String() }