Inline question choices, compact one-click inbox cards, drop snooze

This commit is contained in:
Ian Gulliver
2026-10-14 23:34:39 -07:00
parent 4dc86e5479
commit c7cfad88a1
30 changed files with 1022 additions and 647 deletions
+30 -9
View File
@@ -3,6 +3,7 @@ package agents
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"testing"
@@ -17,17 +18,29 @@ func TestValidateAsk(t *testing.T) {
l := &live{id: "s1"}
yes := true
ask := func(context string) askArgs {
return askArgs{Context: context, Question: "q", WhyBlocked: "w", Blocking: &yes}
}
withAttachment := ask("- (*) a\n- ( ) b")
withAttachment.Attachments = []api.Attachment{{Kind: "video"}}
cases := []struct {
name string
args askArgs
want []string
}{
{"valid", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "a", Blocking: &yes}, nil},
{"empty", askArgs{}, []string{"context is empty", "question is empty", "why_blocked is empty", "at least two", "recommended_option", "blocking is required"}},
{"one option", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a"}, RecommendedOption: "a", Blocking: &yes}, []string{"at least two"}},
{"bad recommendation", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "c", Blocking: &yes}, []string{"recommended_option"}},
{"bad attachment", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "a", Blocking: &yes,
Attachments: []api.Attachment{{Kind: "video"}}}, []string{`attachments[0].kind "video"`}},
{"pick one", ask("Background.\n\n- (*) a: does a\n- ( ) b: does b"), nil},
{"pick any", ask("- [ ] a\n- [x] b\n- [X] c"), nil},
{"pick any, none recommended", ask("- [ ] a\n- [ ] b"), nil},
{"empty", askArgs{}, []string{"context is empty", "question is empty", "why_blocked is empty", "at least two choices", "blocking is required"}},
{"plain list", ask("- a\n- b"), []string{"at least two choices"}},
{"one choice", ask("- (*) a"), []string{"at least two choices"}},
{"in a code fence", ask("```\n- (*) a\n- ( ) b\n```"), []string{"at least two choices"}},
{"no recommendation", ask("- ( ) a\n- ( ) b"), []string{"exactly one"}},
{"two recommendations", ask("- (*) a\n- (*) b"), []string{"exactly one"}},
{"mixed", ask("- (*) a\n- [ ] b"), []string{"mix"}},
{"duplicate", ask("- (*) a\n- ( ) a"), []string{`"a" appears twice`}},
{"bad attachment", withAttachment, []string{`attachments[0].kind "video"`}},
}
for _, c := range cases {
@@ -85,7 +98,7 @@ func TestCancelQuestion(t *testing.T) {
other := &live{id: "s2", dir: t.TempDir(), vendor: api.Claude, taskID: "t1", projectID: "p1"}
yes := true
raw, err := json.Marshal(askArgs{Context: "c", Question: "Red or blue?", WhyBlocked: "w", Options: []string{"red", "blue"}, RecommendedOption: "red", Blocking: &yes})
raw, err := json.Marshal(askArgs{Context: "- (*) red\n- ( ) blue", Question: "Red or blue?", WhyBlocked: "w", Blocking: &yes})
if err != nil {
t.Fatalf("marshal: %v", err)
}
@@ -98,6 +111,10 @@ func TestCancelQuestion(t *testing.T) {
t.Fatalf("inbox after ask: %+v %v", items, err)
}
id := items[0].Id
payload := items[0].Payload
if fmt.Sprint(payload["options"]) != "[red blue]" || payload["multiple"] != false || fmt.Sprint(payload["recommended"]) != "[red]" {
t.Fatalf("choices not parsed from context: %+v", payload)
}
if !strings.Contains(out, "question_id: "+id) || !strings.Contains(out, "cancel_question") {
t.Fatalf("ask result doesn't give the id and cancel_question: %s", out)
}
@@ -195,11 +212,14 @@ func TestBounceProseQuestion(t *testing.T) {
func TestDeliveryMessage(t *testing.T) {
str := func(s string) *string { return &s }
question := api.InboxItem{Id: "q1", ThreadId: "q1", Kind: api.Question, Payload: map[string]any{"question": "Which color?"}}
opts := func(s ...string) *[]string { return &s }
question := api.InboxItem{Id: "q1", ThreadId: "q1", Kind: api.Question, Payload: map[string]any{"question": "Which color?", "multiple": false}}
checklist := api.InboxItem{Id: "q2", ThreadId: "q2", Kind: api.Question, Payload: map[string]any{"question": "Which chores?", "multiple": true}}
failure := api.InboxItem{Id: "e1", ThreadId: "e1", Kind: api.SessionError, Payload: map[string]any{"error": "process died"}}
msg := deliveryMessage([]store.Pending{
{Item: question, Response: api.InboxResponse{User: "alice", Type: api.Option, Option: str("blue")}},
{Item: question, Response: api.InboxResponse{User: "alice", Type: api.Option, Options: opts("blue")}},
{Item: checklist, Response: api.InboxResponse{User: "alice", Type: api.Option, Options: opts("tests", "docs")}},
{Item: question, Response: api.InboxResponse{User: "bob", Type: api.Other, Text: str("what is it for?"),
Attachments: []api.Attachment{{Kind: api.AttachmentKindUrl, Url: str("https://example.com")}}}},
{Item: failure, Response: api.InboxResponse{User: "alice", Type: api.Other, Text: str("stop and write tests")}},
@@ -208,6 +228,7 @@ func TestDeliveryMessage(t *testing.T) {
for _, want := range []string{
"Your question q1: Which color?",
"alice chose: blue",
"Your question q2: Which chores?\nalice checked:\n- tests\n- docs\n",
"bob answered (none of your options): what is it for?",
`calling ask_human with thread_id "q1"`,
`"url":"https://example.com"`,