package main import ( "context" "log" "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/router" "github.com/kami/maven/internal/store" "github.com/kami/maven/internal/tasks" ) // 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) { cap, ok := router.ParseTaskCapture(dec.Utterance) if !ok { return "", false } resp, err := h.api.CaptureTask(ctx, ipc.CaptureTaskReq{ Text: cap.Text, Source: "tap:voice", Status: store.TaskOpen, // he stated it himself — not a candidate Weight: cap.Weight, // 0 unless he said "срочно" / "важно" Ts: h.now(), }) if err != nil { log.Printf("voice: capture task: %v", err) return "не получилось записать задачу.", true } if resp.Promoted { // It was a candidate Maven derived from something she read, and he has // now said it himself. Saying "уже в списке" here would be answering a // confirmation with a shrug. return "поняла, беру в работу: " + cap.Text, true } if !resp.Created { return "это уже в списке.", true } return "записала: " + cap.Text, true } // queryTasks — "какие у меня задачи?", "что мне нужно сделать?". // // Reads the live set and recites it in priority order (Vikunja #129). The order // is computed by internal/tasks from what he told her — deadlines, the urgency // he stated, how long a task has been sitting — never asked of the model. The // rendering is the package's too, so the spoken list and the /tasks page can // never disagree about what comes first. func (h *reactiveHandler) queryTasks(ctx context.Context, t *queryTurn) (string, bool) { if !router.IsTaskListQuery(t.dec.Utterance) { return "", false } live, err := h.api.ListTasks(ctx, "live") if err != nil { log.Printf("voice: list tasks: %v", err) return "не получилось посмотреть задачи.", true } return tasks.FormatRU(tasks.Rank(taskItems(live), h.now())), true } // taskItems maps wire rows onto the ranker's input. Written here rather than in // internal/tasks so the ranker stays a pure package with no ipc (and therefore // no store, and therefore no cgo) dependency — the same posture as // internal/morning and internal/memory. func taskItems(ts []ipc.Task) []tasks.Item { out := make([]tasks.Item, len(ts)) for i, t := range ts { out[i] = tasks.Item{ ID: t.ID, Text: t.Text, Status: t.Status, Created: t.CreatedTs, Due: t.Due, Weight: t.Weight, } } return out }