Render Markdown, add ask_human context, simplify inbox responses, group inbox by task

This commit is contained in:
Ian Gulliver
2026-10-14 10:37:04 -07:00
parent e9fd7de556
commit ca2644374d
25 changed files with 950 additions and 283 deletions
+12 -24
View File
@@ -145,54 +145,42 @@ func checkResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxStat
if c.Text != nil {
text = strings.TrimSpace(*c.Text)
}
question := item.Kind == api.InboxItemKindQuestion
question := item.Kind == api.Question
bad := func(format string, args ...any) (api.InboxState, *httpError) {
return "", &httpError{http.StatusBadRequest, fmt.Errorf("item %s: "+format, append([]any{item.Id}, args...)...)}
}
switch c.Type {
case api.InboxResponseTypeOption:
case api.Option:
if !question {
return bad("only questions have options")
}
if c.Option == nil || !hasOption(item, *c.Option) {
return bad("option must be one of the question's options")
}
return api.Answered, nil
case api.InboxResponseTypeFreeForm:
if !question {
return bad("only questions take answers")
}
if text == "" {
return bad("free_form needs text")
if text != "" {
return bad("option takes no text; use other")
}
return api.Answered, nil
case api.InboxResponseTypeQuestion:
if !question {
return bad("only questions take follow-up questions")
case api.Other:
if item.Kind == api.Report {
return bad("reports are only dismissed")
}
if text == "" {
return bad("question needs text")
return bad("other needs text")
}
return api.Open, nil
return api.Answered, nil
case api.InboxResponseTypeRedirect:
if text == "" {
return bad("redirect needs text")
}
return api.Redirected, nil
case api.InboxResponseTypeSnooze:
case api.Snooze:
if c.SnoozeUntil == nil || !c.SnoozeUntil.After(time.Now()) {
return bad("snooze needs a future snooze_until")
}
return api.Open, nil
case api.InboxResponseTypeDismiss:
case api.Dismiss:
if question {
return bad("questions need an answer or a redirect, not a dismissal")
return bad("questions need an option or other, not a dismissal")
}
return api.Dismissed, nil
}
+17 -17
View File
@@ -11,11 +11,12 @@ import (
func TestCheckResponse(t *testing.T) {
question := api.InboxItem{
Id: "q1",
Kind: api.InboxItemKindQuestion,
Kind: api.Question,
State: api.Open,
Payload: map[string]any{"options": []any{"red", "blue"}},
}
report := api.InboxItem{Id: "r1", Kind: api.InboxItemKindReport, State: api.Open, Payload: map[string]any{}}
report := api.InboxItem{Id: "r1", Kind: api.Report, State: api.Open, Payload: map[string]any{}}
failure := api.InboxItem{Id: "e1", Kind: api.SessionError, State: api.Open, Payload: map[string]any{}}
closed := question
closed.State = api.Answered
@@ -30,21 +31,20 @@ func TestCheckResponse(t *testing.T) {
state api.InboxState
status int
}{
{"option", question, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("blue")}, api.Answered, 0},
{"option with note", question, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("red"), Text: str("but darker")}, api.Answered, 0},
{"unknown option", question, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("green")}, "", http.StatusBadRequest},
{"missing option", question, api.InboxResponseCreate{Type: api.InboxResponseTypeOption}, "", http.StatusBadRequest},
{"free form", question, api.InboxResponseCreate{Type: api.InboxResponseTypeFreeForm, Text: str("green")}, api.Answered, 0},
{"empty free form", question, api.InboxResponseCreate{Type: api.InboxResponseTypeFreeForm, Text: str(" ")}, "", http.StatusBadRequest},
{"follow-up question", question, api.InboxResponseCreate{Type: api.InboxResponseTypeQuestion, Text: str("why?")}, api.Open, 0},
{"redirect question", question, api.InboxResponseCreate{Type: api.InboxResponseTypeRedirect, Text: str("stop")}, api.Redirected, 0},
{"redirect report", report, api.InboxResponseCreate{Type: api.InboxResponseTypeRedirect, Text: str("stop")}, api.Redirected, 0},
{"snooze", question, api.InboxResponseCreate{Type: api.InboxResponseTypeSnooze, SnoozeUntil: &future}, api.Open, 0},
{"snooze into past", question, api.InboxResponseCreate{Type: api.InboxResponseTypeSnooze, SnoozeUntil: &past}, "", http.StatusBadRequest},
{"dismiss report", report, api.InboxResponseCreate{Type: api.InboxResponseTypeDismiss}, api.Dismissed, 0},
{"dismiss question", question, api.InboxResponseCreate{Type: api.InboxResponseTypeDismiss}, "", http.StatusBadRequest},
{"option on report", report, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("red")}, "", http.StatusBadRequest},
{"already answered", closed, api.InboxResponseCreate{Type: api.InboxResponseTypeOption, Option: str("red")}, "", http.StatusConflict},
{"option", question, api.InboxResponseCreate{Type: api.Option, Option: str("blue")}, api.Answered, 0},
{"option with text", question, api.InboxResponseCreate{Type: api.Option, Option: str("red"), Text: str("but darker")}, "", http.StatusBadRequest},
{"unknown option", question, api.InboxResponseCreate{Type: api.Option, Option: str("green")}, "", http.StatusBadRequest},
{"missing option", question, api.InboxResponseCreate{Type: api.Option}, "", http.StatusBadRequest},
{"other", question, api.InboxResponseCreate{Type: api.Other, Text: str("green")}, api.Answered, 0},
{"empty other", question, api.InboxResponseCreate{Type: api.Other, Text: str(" ")}, "", http.StatusBadRequest},
{"other on error", failure, api.InboxResponseCreate{Type: api.Other, Text: str("retry")}, api.Answered, 0},
{"other on report", report, api.InboxResponseCreate{Type: api.Other, Text: str("stop")}, "", http.StatusBadRequest},
{"snooze", question, api.InboxResponseCreate{Type: api.Snooze, SnoozeUntil: &future}, api.Open, 0},
{"snooze into past", question, api.InboxResponseCreate{Type: api.Snooze, SnoozeUntil: &past}, "", http.StatusBadRequest},
{"dismiss report", report, api.InboxResponseCreate{Type: api.Dismiss}, api.Dismissed, 0},
{"dismiss question", question, api.InboxResponseCreate{Type: api.Dismiss}, "", http.StatusBadRequest},
{"option on report", report, api.InboxResponseCreate{Type: api.Option, Option: str("red")}, "", http.StatusBadRequest},
{"already answered", closed, api.InboxResponseCreate{Type: api.Option, Option: str("red")}, "", http.StatusConflict},
}
for _, c := range cases {