Inline question choices, compact one-click inbox cards, drop snooze
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
choiceLine = regexp.MustCompile(`^\s*[-*+]\s+(\([ *]\)|\[[ xX]\])\s+(\S.*?)\s*$`)
|
||||
choiceFence = regexp.MustCompile("^\\s*(```|~~~)")
|
||||
)
|
||||
|
||||
type choice struct {
|
||||
text string
|
||||
checkbox bool
|
||||
recommended bool
|
||||
}
|
||||
|
||||
func parseChoices(markdown string) []choice {
|
||||
choices := []choice{}
|
||||
fence := ""
|
||||
for _, line := range strings.Split(markdown, "\n") {
|
||||
if f := choiceFence.FindStringSubmatch(line); f != nil {
|
||||
switch fence {
|
||||
case "":
|
||||
fence = f[1]
|
||||
case f[1]:
|
||||
fence = ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
if fence != "" {
|
||||
continue
|
||||
}
|
||||
m := choiceLine.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
choices = append(choices, choice{
|
||||
text: m[2],
|
||||
checkbox: m[1][0] == '[',
|
||||
recommended: m[1][1] != ' ',
|
||||
})
|
||||
}
|
||||
return choices
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseChoices(t *testing.T) {
|
||||
src := "Background with a [link](https://example.com) and (parens).\n\n" +
|
||||
"- ( ) **Merge now**: ships today\n" +
|
||||
"* (*) Wait for review \n" +
|
||||
" - [ ] nested checkbox\n" +
|
||||
"+ [x] checked\n" +
|
||||
"- [X] upper\n" +
|
||||
"- plain item\n" +
|
||||
"-( ) no space\n" +
|
||||
"- ( )\n" +
|
||||
"```\n- (*) fenced\n```\n" +
|
||||
"~~~\n- ( ) tilde fenced\n~~~\n" +
|
||||
"> - ( ) quoted\n"
|
||||
|
||||
got := parseChoices(src)
|
||||
want := []choice{
|
||||
{text: "**Merge now**: ships today"},
|
||||
{text: "Wait for review", recommended: true},
|
||||
{text: "nested checkbox", checkbox: true},
|
||||
{text: "checked", checkbox: true, recommended: true},
|
||||
{text: "upper", checkbox: true, recommended: true},
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("parseChoices:\n got %+v\nwant %+v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,13 @@ import (
|
||||
const guidance = `You are running inside byre. The human reads your work in byre's web UI, not a terminal.
|
||||
- Ask every question with mcp__byre__ask_human, never in prose.
|
||||
- End every turn with a short final message for the human: what you did, anything you departed from in the plan or instructions, risks, and what's next. byre shows it as the turn's summary in the transcript and the inbox. State every departure from the plan.
|
||||
- In that final message and in ask_human context, put long supporting detail (logs, full lists, data) in collapsed sections the human expands on demand: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
|
||||
- Don't commit. byre commits everything in the worktree at the end of each turn, using the turn's final message.
|
||||
- Don't push, don't switch or touch other branches, and don't run git maintenance (gc, prune, worktree prune). byre handles merging and pushing.`
|
||||
|
||||
const proseQuestionNudge = "Your final message asks the human something in prose. Ask it with mcp__byre__ask_human " +
|
||||
"(context, question, options, recommended_option, why_blocked), then end your turn with a short summary."
|
||||
"(context with the choices inline as `- ( ) choice` / `- (*) recommended` or `- [ ] choice` / `- [x] recommended` list items, question, why_blocked), " +
|
||||
"then end your turn with a short summary."
|
||||
|
||||
var (
|
||||
fencedCode = regexp.MustCompile("(?s)```.*?```")
|
||||
|
||||
+61
-24
@@ -21,14 +21,12 @@ var attachmentKinds = []api.AttachmentKind{
|
||||
}
|
||||
|
||||
type askArgs struct {
|
||||
Context string `json:"context"`
|
||||
Question string `json:"question"`
|
||||
WhyBlocked string `json:"why_blocked"`
|
||||
Options []string `json:"options"`
|
||||
RecommendedOption string `json:"recommended_option"`
|
||||
Attachments []api.Attachment `json:"attachments"`
|
||||
Blocking *bool `json:"blocking"`
|
||||
ThreadID string `json:"thread_id"`
|
||||
Context string `json:"context"`
|
||||
Question string `json:"question"`
|
||||
WhyBlocked string `json:"why_blocked"`
|
||||
Attachments []api.Attachment `json:"attachments"`
|
||||
Blocking *bool `json:"blocking"`
|
||||
ThreadID string `json:"thread_id"`
|
||||
}
|
||||
|
||||
func (m *Manager) askTool(l *live) mcp.Tool {
|
||||
@@ -37,18 +35,20 @@ func (m *Manager) askTool(l *live) mcp.Tool {
|
||||
Description: "Ask the human a question through their inbox. Use this for every question; never ask in plain prose. " +
|
||||
"The human answers from their inbox without having read your transcript, so every question must stand on its own: " +
|
||||
"put the background they need in context. " +
|
||||
"Offer concrete options and recommend one. If blocking is true, end your turn after asking; " +
|
||||
"Write the choices inline in context as Markdown list items, each with what it would concretely do: " +
|
||||
"`- ( ) choice` for pick-one choices (mark the one you recommend `- (*) choice`), or " +
|
||||
"`- [ ] choice` for pick-any choices (mark the ones you recommend `- [x] choice`). " +
|
||||
"The human picks them where they appear; there is no separate list of options. " +
|
||||
"If blocking is true, end your turn after asking; " +
|
||||
"the answer arrives as your next message. If blocking is false, continue with work that doesn't depend on the answer. " +
|
||||
"Ask several questions in one turn by calling this once per question; they are answered together. " +
|
||||
"To answer a follow-up question from the human, call this again with the thread_id they gave you.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {"type": "string", "description": "Everything the human needs to answer well without reading your transcript: what you are working on, what you found, the relevant facts and data, and what each option would concretely do. Several paragraphs is normal. Markdown is rendered."},
|
||||
"context": {"type": "string", "description": "Everything the human needs to answer well without reading your transcript: what you are working on, what you found, the relevant facts and data, and the choices. Several paragraphs is normal. Markdown is rendered. Each choice is a list item on its own line, at least two, all of one kind: '- ( ) Merge now: ships the fix today' / '- (*) Wait for review: ...' (pick one; exactly one (*) marks your recommendation), or '- [ ] Fix the tests' / '- [x] Update the docs' (pick any; [x] marks the ones you recommend). The choice's text is the whole line after the marker, and it comes back verbatim as the answer. Put long supporting detail (logs, full data, per-option analysis) in collapsed sections the human expands on demand: a line '<details><summary>Title</summary>', the Markdown content, then a line '</details>'."},
|
||||
"question": {"type": "string", "description": "The question itself, in one or two sentences. Markdown is rendered."},
|
||||
"why_blocked": {"type": "string", "description": "What you can't do until this is answered, and why you can't decide it yourself."},
|
||||
"options": {"type": "array", "items": {"type": "string"}, "minItems": 2},
|
||||
"recommended_option": {"type": "string", "description": "One of options."},
|
||||
"attachments": {"type": "array", "items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -68,7 +68,7 @@ func (m *Manager) askTool(l *live) mcp.Tool {
|
||||
"blocking": {"type": "boolean"},
|
||||
"thread_id": {"type": "string"}
|
||||
},
|
||||
"required": ["context", "question", "why_blocked", "options", "recommended_option", "blocking"]
|
||||
"required": ["context", "question", "why_blocked", "blocking"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.ask(ctx, l, raw)
|
||||
@@ -104,13 +104,24 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string
|
||||
attachments = []api.Attachment{}
|
||||
}
|
||||
|
||||
choices := parseChoices(args.Context)
|
||||
options := []string{}
|
||||
recommended := []string{}
|
||||
for _, c := range choices {
|
||||
options = append(options, c.text)
|
||||
if c.recommended {
|
||||
recommended = append(recommended, c.text)
|
||||
}
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"context": args.Context,
|
||||
"question": args.Question,
|
||||
"why_blocked": args.WhyBlocked,
|
||||
"options": args.Options,
|
||||
"recommended_option": args.RecommendedOption,
|
||||
"attachments": attachments,
|
||||
"context": args.Context,
|
||||
"question": args.Question,
|
||||
"why_blocked": args.WhyBlocked,
|
||||
"options": options,
|
||||
"multiple": choices[0].checkbox,
|
||||
"recommended": recommended,
|
||||
"attachments": attachments,
|
||||
}
|
||||
|
||||
err = m.store.CreateInboxItem(ctx, m.newItem(l, id, thread, api.Question, priority, *args.Blocking, store.ToPayload(payload)))
|
||||
@@ -193,11 +204,30 @@ func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error
|
||||
if strings.TrimSpace(args.WhyBlocked) == "" {
|
||||
problems = append(problems, "why_blocked is empty: say what you can't do until this is answered and why you can't decide it yourself")
|
||||
}
|
||||
if len(args.Options) < 2 {
|
||||
problems = append(problems, "options needs at least two concrete choices")
|
||||
choices := parseChoices(args.Context)
|
||||
if len(choices) < 2 {
|
||||
problems = append(problems, "context needs at least two choices, each a list item on its own line: `- ( ) choice` (pick one) or `- [ ] choice` (pick any)")
|
||||
}
|
||||
if !slices.Contains(args.Options, args.RecommendedOption) {
|
||||
problems = append(problems, "recommended_option must be exactly one of options")
|
||||
seen := map[string]bool{}
|
||||
radios := 0
|
||||
recommendedRadios := 0
|
||||
for _, c := range choices {
|
||||
if seen[c.text] {
|
||||
problems = append(problems, fmt.Sprintf("choice %q appears twice; each choice's text must be unique", c.text))
|
||||
}
|
||||
seen[c.text] = true
|
||||
if !c.checkbox {
|
||||
radios++
|
||||
if c.recommended {
|
||||
recommendedRadios++
|
||||
}
|
||||
}
|
||||
}
|
||||
if radios > 0 && radios < len(choices) {
|
||||
problems = append(problems, "choices mix `( )` and `[ ]`: use one kind per question")
|
||||
}
|
||||
if radios == len(choices) && len(choices) > 0 && recommendedRadios != 1 {
|
||||
problems = append(problems, "mark exactly one pick-one choice as recommended with `- (*) choice`")
|
||||
}
|
||||
if args.Blocking == nil {
|
||||
problems = append(problems, "blocking is required")
|
||||
@@ -337,7 +367,14 @@ func deliveryMessage(pending []store.Pending) string {
|
||||
|
||||
switch r.Type {
|
||||
case api.Option:
|
||||
fmt.Fprintf(b, "%s chose: %s\n", r.User, *r.Option)
|
||||
if p.Item.Payload["multiple"] == true {
|
||||
fmt.Fprintf(b, "%s checked:\n", r.User)
|
||||
for _, o := range *r.Options {
|
||||
fmt.Fprintf(b, "- %s\n", o)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s chose: %s\n", r.User, (*r.Options)[0])
|
||||
}
|
||||
case api.Other:
|
||||
if p.Item.Kind == api.Question {
|
||||
fmt.Fprintf(b, "%s answered (none of your options): %s\n", r.User, *r.Text)
|
||||
|
||||
@@ -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"`,
|
||||
|
||||
@@ -417,7 +417,7 @@ func (e *env) respond(m *agents.Manager, item api.InboxItem, typ api.InboxRespon
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
if option != "" {
|
||||
resp.Option = &option
|
||||
resp.Options = &[]string{option}
|
||||
}
|
||||
if text != "" {
|
||||
resp.Text = &text
|
||||
@@ -449,9 +449,17 @@ func TestAskHumanRoundTrip(t *testing.T) {
|
||||
t.Fatalf("want one open question, got %+v", questions)
|
||||
}
|
||||
q := questions[0]
|
||||
if !q.Blocking || q.Priority != api.Blocked || q.Payload["recommended_option"] != "red" {
|
||||
options, _ := q.Payload["options"].([]any)
|
||||
recommended, _ := q.Payload["recommended"].([]any)
|
||||
if !q.Blocking || q.Priority != api.Blocked || len(options) != 2 || len(recommended) != 1 || !strings.Contains(fmt.Sprint(recommended[0]), "red") {
|
||||
t.Fatalf("unexpected question %+v", q)
|
||||
}
|
||||
blue := ""
|
||||
for _, o := range options {
|
||||
if strings.Contains(fmt.Sprint(o), "blue") {
|
||||
blue = fmt.Sprint(o)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(e.task.WorktreePath, "color.txt")); err == nil {
|
||||
t.Fatalf("agent wrote color.txt before getting an answer")
|
||||
}
|
||||
@@ -461,12 +469,12 @@ func TestAskHumanRoundTrip(t *testing.T) {
|
||||
t.Fatalf("awaiting human: %d %v", waiting, err)
|
||||
}
|
||||
|
||||
e.respond(m, q, api.Option, "blue", "", api.Answered)
|
||||
e.respond(m, q, api.Option, blue, "", api.Answered)
|
||||
events := e.waitIdle()
|
||||
|
||||
delivered := false
|
||||
for _, ev := range events {
|
||||
if ev.Kind == "user.message" && strings.Contains(ev.Payload["text"].(string), "chose: blue") {
|
||||
if ev.Kind == "user.message" && strings.Contains(ev.Payload["text"].(string), "chose: "+blue) {
|
||||
delivered = true
|
||||
}
|
||||
}
|
||||
|
||||
+116
-137
@@ -209,7 +209,6 @@ const (
|
||||
Dismiss InboxResponseType = "dismiss"
|
||||
Option InboxResponseType = "option"
|
||||
Other InboxResponseType = "other"
|
||||
Snooze InboxResponseType = "snooze"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the InboxResponseType enum.
|
||||
@@ -221,8 +220,6 @@ func (e InboxResponseType) Valid() bool {
|
||||
return true
|
||||
case Other:
|
||||
return true
|
||||
case Snooze:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -449,17 +446,16 @@ type InboxItem struct {
|
||||
Id string `json:"id"`
|
||||
Kind InboxItemKind `json:"kind"`
|
||||
|
||||
// Payload question: context, question, why_blocked, options, recommended_option, attachments. report: text (the final message of a turn that ended normally). session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error.
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
Priority InboxItemPriority `json:"priority"`
|
||||
ProjectId string `json:"project_id"`
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||
ResolvedBy *string `json:"resolved_by,omitempty"`
|
||||
Responses []InboxResponse `json:"responses"`
|
||||
SnoozedUntil *time.Time `json:"snoozed_until,omitempty"`
|
||||
State InboxState `json:"state"`
|
||||
TaskId string `json:"task_id"`
|
||||
ThreadId string `json:"thread_id"`
|
||||
// Payload question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. report: text (the final message of a turn that ended normally). session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error.
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
Priority InboxItemPriority `json:"priority"`
|
||||
ProjectId string `json:"project_id"`
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||
ResolvedBy *string `json:"resolved_by,omitempty"`
|
||||
Responses []InboxResponse `json:"responses"`
|
||||
State InboxState `json:"state"`
|
||||
TaskId string `json:"task_id"`
|
||||
ThreadId string `json:"thread_id"`
|
||||
}
|
||||
|
||||
// InboxItemKind defines model for InboxItem.Kind.
|
||||
@@ -480,8 +476,7 @@ type InboxResponse struct {
|
||||
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
|
||||
Id string `json:"id"`
|
||||
ItemId string `json:"item_id"`
|
||||
Option *string `json:"option,omitempty"`
|
||||
SnoozeUntil *time.Time `json:"snooze_until,omitempty"`
|
||||
Options *[]string `json:"options,omitempty"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
Type InboxResponseType `json:"type"`
|
||||
User string `json:"user"`
|
||||
@@ -492,11 +487,8 @@ type InboxResponseCreate struct {
|
||||
Attachments *[]Attachment `json:"attachments,omitempty"`
|
||||
ItemId string `json:"item_id"`
|
||||
|
||||
// Option option: one of the question's options.
|
||||
Option *string `json:"option,omitempty"`
|
||||
|
||||
// SnoozeUntil snooze: when the item reappears.
|
||||
SnoozeUntil *time.Time `json:"snooze_until,omitempty"`
|
||||
// Options option: the chosen options; exactly one for a pick-one question, one or more for a pick-any question.
|
||||
Options *[]string `json:"options,omitempty"`
|
||||
|
||||
// Text other: the text sent to the agent.
|
||||
Text *string `json:"text,omitempty"`
|
||||
@@ -756,10 +748,9 @@ type StreamEventsParams struct {
|
||||
|
||||
// ListInboxParams defines parameters for ListInbox.
|
||||
type ListInboxParams struct {
|
||||
ProjectId *string `form:"project_id,omitempty" json:"project_id,omitempty"`
|
||||
TaskId *string `form:"task_id,omitempty" json:"task_id,omitempty"`
|
||||
State *InboxState `form:"state,omitempty" json:"state,omitempty"`
|
||||
IncludeSnoozed *bool `form:"include_snoozed,omitempty" json:"include_snoozed,omitempty"`
|
||||
ProjectId *string `form:"project_id,omitempty" json:"project_id,omitempty"`
|
||||
TaskId *string `form:"task_id,omitempty" json:"task_id,omitempty"`
|
||||
State *InboxState `form:"state,omitempty" json:"state,omitempty"`
|
||||
}
|
||||
|
||||
// RemovePushSubscriptionParams defines parameters for RemovePushSubscription.
|
||||
@@ -1079,19 +1070,6 @@ func (siw *ServerInterfaceWrapper) ListInbox(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
// ------------- Optional query parameter "include_snoozed" -------------
|
||||
|
||||
err = runtime.BindQueryParameterWithOptions("form", true, false, "include_snoozed", r.URL.Query(), ¶ms.IncludeSnoozed, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""})
|
||||
if err != nil {
|
||||
var requiredError *runtime.RequiredParameterError
|
||||
if errors.As(err, &requiredError) {
|
||||
siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "include_snoozed"})
|
||||
} else {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include_snoozed", Err: err})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.ListInbox(w, r, params)
|
||||
}))
|
||||
@@ -2004,105 +1982,106 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
// const string: with thousands of chunks the chained `+` fold is several
|
||||
// times slower for the Go compiler than parsing a slice literal.
|
||||
var swaggerSpec = []string{
|
||||
"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",
|
||||
"Hcb+fbj9YfYvm3e6vf2fAAAA//8=",
|
||||
"7D1/c9s2ll8Fw7sZJze0nHbTzpzzV5pkW+9uut44vc5cr6OFyCcJNQkoAGhFm/F3v8F7AAmKoCSrtpO9",
|
||||
"vb8ikwTwfuP9AvIpK1S9UhKkNdn5p0yDWSlpAP94o7XS7kehpAVp3U++WlWi4FYoefabUdI9M8USau5+",
|
||||
"/buGeXae/dtZN+sZvTVnNNvt7W2elWAKLVZukuyclplk7oX/1k31cgHSXmpVgMG/V1qtQFtBoC2BV3bp",
|
||||
"fvXnMrYprlkNXBrGmW20ZMIw3Ugp5IJxWTKpmLEaeM3gBqRlS24Y11rcQMnmSjOcYcrnFvTUQKFkaV4w",
|
||||
"+CgslH5euwS2IsDc5AslYZLlGcimzs5/ydR1lhMc7iEOzH7NM7tZAQKohVxkefbx1A04veFa8toh9Uv2",
|
||||
"AyL1VzeOfl75WeivN36u2zyruLFThH/K7ZAKPy9B9sB037O1VhYYZ5WQwKxiwhpPiizP5krXbqqs5BZO",
|
||||
"raghyzOl2dpNJSwzlmsL5SQ7l01VbaNzm2crUTpA/HMhLSxAuxcGjBFKTnvvu3HGcgsoWYYpyZDtVzSk",
|
||||
"XayP3fslMD/picPAjU8AZLm5Hlv0BmRJor316jbPNHxohIbScSSCvZuwHU5IBxS2uZIHIe2Yr2a/QWEd",
|
||||
"ADGaQ+kuNHALpedtkjUDlMKY2SaJ8QghalVCNRSgd/ChAYMi7z54waBe2U0k/0SBE8NKmPOmspMUSFpV",
|
||||
"yNmgGaJeVVCDtKCHGhGLQh+YDw00rfJ1Sr3mwjqldkrL2VwDMFMpyxpZgkYgCyWLRmuQxYYVfGViLRVl",
|
||||
"5chIc2d55k1ElqNBrOBwrY1ZeeUwuKCpB8//FtYavHnXLj549aqD5mCZDjgWFW9KSJKaPp7u1M0tXRjT",
|
||||
"ARKh1JSdZqAk9GQ0j4U8qSHW8mJZ+21nSz+4l40EGUCWU2fh0sZI1HyREDF8fM5Kbjn76d1fktJ8LWQZ",
|
||||
"U3cuEKdSzOdZmNj9yRcaLaojlSOB5CuzVM4eNLpKMqMGXXNiQR8sP9k5e0tfMKMaXUASvBVP7YcOyHOm",
|
||||
"YaVONVTcihtg7su0vrrNewjDfH7OFsIyDTfCsZZpLhdpIAKyO4y9tjvYs0u+HfX2CikyKSVO31VqlhAk",
|
||||
"cmym9Pmncav60JY4pWs94O6mPK/4KuE0LSo14wlr/1YZi2Y18pQsU7IAxgutjGG8qrzJNxHfI8atQE87",
|
||||
"A8TLUri5eXXZA2A47mBAVqA9BDm7hg2UbLbxDyKIAgW2yOkR74GZIlvr8PbpVoMx3mzsZlv4MDn3TdKU",
|
||||
"8cImHZE8424rmO7xno6Rz2DIEiZkUykev+vAX2nlfo4qNnzoASCk/fZ5UlLGVXzge33IPLC5J1MH4l4N",
|
||||
"+KOo4NXSGaoEzb14jsjkTEiuY92dKVUBlySvFewYqqpymjbEGpzDUJ6TW87tks1grjTg3/Ryp1lPOkuN",
|
||||
"iTckXpboXtSqFHOBPxFc8m9ofb/EtEDSJFycbS7g+u1qeUS7mBgt0cZY8drtkgNGrLgtliOimEQ7DRxN",
|
||||
"k1r6Qs7UxwsLdUIIDlGvWaWKa/c7KQzHKN/IQtvOBbrfTnIcwiulnfsQYAW0UnmmuYVpJep0hNnX54FH",
|
||||
"jbOfM9xjPlr25C3X16VaS7YWdkne81KJAgwTEgNGbtjfT9kT9pS5AX9nZ/jnf4Q/V6K4PlUSmNLuxS/s",
|
||||
"1/i7Xz7+Gn/H5YZVwlgmLNTmac4CQDlbLzdTpDqUOVMIsGFPgBdLD9CJwZlyJiRTugT9NGd1U1mxqoA9",
|
||||
"sboBjAjCOk9zpqFQdQ2yhJI9cZjVXF9DGRA8wfme5oy3HqeZMKL6OSPquFFzIXnFvIlnah5CEbvkltHs",
|
||||
"0olAVW2eTliPWecM/5mwmGXn7X7WPaWV2uDKvTilF6SAT3M/0//ILE8ZaaG0sJtYkjwxszxbeF8cbgSs",
|
||||
"Xci/EWmx2W3qNRhV3dxR7NtBI05RL++EUrEvnYSq/c4Pw42F5uRa800vmtw7CwZa+9xPu9TAy8MjpYiK",
|
||||
"cdg0MDvxzO121zIyMkHx5hfiqsgCxTQcNYVEr3JoDX8n/V8hHBjLCHlBw7/aZskWlQ4G16S28E5XD4Y4",
|
||||
"iigT4nKMMS+hEjeg72cLcDiMCZ83hD1UhxK6hZEzXTs+vAN737sBLgQzoA8U/4CNH+QXzXuM2+vEpWTs",
|
||||
"EUThQE7091R6cR62TgMybF8vGHzkha02zO2PlK5qt8tu5/ObZ+2cwugjt1eGjyaesHfj/xacdgmawMS9",
|
||||
"zYC0zCp8gMYp6YYeKTLbktEKBc63l+Xv/aptan3lfSJEArMtphbGJLexyLT3pgA3AZdm7RS3mwN/m2YF",
|
||||
"2kA54lC9pc1/TBIxB5QQje8qNWOiNGyuVc2alTPi7lk+JL5zs2gaxislF0aU0PLqbuwfSe2+dY9RwOxS",
|
||||
"GHJhuCxZxS1oJ4Rmwn72Sf9SzOegPdy2l3bH2fP4ITNrYYslmDiyCe6SMIhrWraScvqWb9gMfOKZqhCe",
|
||||
"LhrYQtyATEy2JW84c0rKflRWzH0Z61LD3KRqKKJYMglrJpwgkavqkCiJcM6sOR1tzJLJaLoJ7f2RWIQd",
|
||||
"fBrUOLHa38Irxo3zTdELDwPPmXNrJ+yvks02iYx7FJKgd5iYv+eOEi7pCXbA+KOSpwGk1iSNTNO5tImJ",
|
||||
"Yi94FyzkgycmeO+E1jR1zbWA5OAtQUiwIEa1W6qlYB+FlAxdkoP3KNUbz/PpTHM5EjGP7Fcu6h/xuGtl",
|
||||
"x16t1PSwCByNOa4RDxsA3C53tzSmJ/GYvd2B28EIDGAfgcOqQlUj+cE7BUJ8jTLS2FCCnQttLHvO/iy+",
|
||||
"21HwDDG4hgLETZvI2UKbm5GqyL4irPhHoioS1j0xbN5UFatALuzSBd6zjYWRTPChpdW2ftSLhXwgg3gQ",
|
||||
"sTxwe9liUtn9xjuFxyakr6hTwBGB6p685Cu3TxaqqUp5YtmKa4N+Wynwbw2FWkhhgBkhC2CzjYZQPD8k",
|
||||
"c+2wL3ymeMiNWhlcwnkKbqvO3QYFxpIQ9fyDXZ5aX55v9wRqnowtZElWNGb5Z9gkUnzNrBLF9JrebflF",
|
||||
"3MC3zxtdZcNCayMd3BqcY8YuT7/+5lu2UsJ5EKgig0xgt8wYeFfNLFp8oMMNWYtZgGlX80E004lhbigz",
|
||||
"UGiwKa0EWSLk6Tzn1998Wx67MA12QrXXE2qBaJfMCeUUtXz9+XO4uSut6tWxjm4qk1Wv7AF5ZPouSYtQ",
|
||||
"vj0ugVyouhZ2apZ8SK+XjN4yNWdWA7iv2NqFjs6qOCoJMjprpa/dByeG/fDm5eu2J2fNDbP8OukJH5fQ",
|
||||
"oOYVX8wZyp/l5vrEYKhgrO+fWodeo1D7RbC8xfNxpgZgS1WVhnHLKuDGtnixZkUb4SRuQhovH425OUOA",
|
||||
"L5VBm+/2K9sBH8A0OcnpVy98CWaOnWIwN2fOZp+5z83ZJ5+5uz1z484+ydvJHataeRZ4O/JSLBbQa9uw",
|
||||
"SlXTgldoFhstp0AlMHM9XTY1x8yvqFM9NMn+lCDB72ml90pVr2jy7TeNlm9wqa0XL831D37h7SEEx55W",
|
||||
"kUTOU2Y93YiIFAthR569ruJVWw4b1FfFDUyRm+kdn6DznXIpu/bmBvTG263QUIfbulNPzTGwyJkoqzYU",
|
||||
"NkzIompKKA/eknvtjqn0pK/o75oDq/7Oh5lN0UE7sCIbta0dXsatHFWRJgmKvYxpBYZRF5atNqGsP9JE",
|
||||
"QO7+CJ9W3mmZdjHuwT6O8aFtA+UU+wvSMWW/qUwrt4113WR8ZfJgTLDy5H5t2FpUFdm7g5nt1noH8xSf",
|
||||
"twLnPY5rJ/8Jz52s9KCGxLCYRg6oL3Md7JoS93aTMHRuSLXOmapKB0RIEd3NTd1BJmNgWlQiiF+i65Q2",
|
||||
"oDvtfonW3yGSP6g1cy4L4y2mmEOr+YYtFCZuVGMZ73cZ+5SYsIy8aYZ7ZFNcpzXhH6qeiZQtugoGxtcw",
|
||||
"AwiYL5KKVdjgRYqXY16Pb+kiWq4lN9gIHdhC/D6IKf+NoO0NGiL6RwZpYGwipc/7trrP477B2ZbELd32",
|
||||
"1rKvTEND35E5zfqhyUltPO+5uR5uO86bn/p+umFXwXgWp6iUaaV2pKnb0QedrBr0AkoKPk3Btd9v7pZx",
|
||||
"eoQeYwfmlHb7IVqEBDmK5kPDzbJ1iwldn0piRLUXcUbYLkEDkkIqu0RVVESVpEdMgFhhq5FkB75Cd3wA",
|
||||
"zFs3ltX8Gsx5xwUa0Egrqii4MeAUXGKRgjTTgJ1Gq6c7ffZ0WA37fVBdMo+XL2WQGBzoGjrZJcfpZZir",
|
||||
"e/Q2zNo9et3N7/Q/EHIAawhY7pJE7NfLceo+x1q92Z4/77QtalS6S57RYUjNYiatyonyhIPs1L2Lo7gg",
|
||||
"FwQp2t+hCCe5PxfVHYrvUXdbqvCnISXgLgrzQAYCYhw+d3uU3pACrYHyVQuyJbsZh6Tx6wUURuk7kk0Y",
|
||||
"k6LtUg5+lpzcewmDmfeE5g9/PiQF7U/Sa2siW1mnyyWv6EWwh7GAYe8R1yBPRsxl2r3Y08XYymIfDid0",
|
||||
"hpakwmBXF0ScWBCH/oQ+l1cLa7HpCtUsCpAon5dnQhpHjFGoolkSwT63S0O2Nr1aT+xTVBkkPWvvNhA1",
|
||||
"evD1Wx9jwJIs910TfXaXwqwqvpmOFjEWwk6h5qIafTs6tFILka4DxJXK6SpUPneZmmGpdJtUtFrexyiC",
|
||||
"MEYlCcEY0X5alUmTcTRlttvCUxCmgPE+75CHYMeg2G7oDA6rjw+CG0obn/vVvvE2Jd2ad+Qhu4Obrn3/",
|
||||
"mUdsSItbVNS5iix35iIKNHqaTrNlzybPJs+oWQYkX4nsPPvD5NnkD9Squ0TSnfGVOONlLeRZ59ksAB1E",
|
||||
"R2KUj4syO8++B3sVNvVem9rXz57d2+FUv0LidOoV6BvQjM7y4Y7eGO4czFv8GPGYVWpGnoMyCRf3yioN",
|
||||
"ziOknDcmFxoDzirx0CExYc4BnalyE7K+mq/p+xdYJnSMcK9KsFA429YaXwytELK6MS7UZJc/fp+zP12+",
|
||||
"+T5n31/80QUIP8PscsJeWqogfcPeiu+oh7RP65/apH1GcgHGfqfKzRadEayzhZj3CdwGEb4ZPOU+0NDf",
|
||||
"VrA4duxKHj10DbPVXcf2FMTqBm4HYvjVvYkhUj4hhFj8cVJUMidrE6dcz0n8U9O18J21Fb7nX/3h4K+3",
|
||||
"BPvsk/vnorzdpaKtzOxU0H8xwRky0VHyxJDK5uSvOOX2JEIl98x9fgd2ObuqeQ0WtIvrPmVu9w+nI2gv",
|
||||
"zIiH2bY05xFO2xj8GsQA8zWxhU6ZyFOs8tGnE/aGF0v648QwUTrLhefN4cMLOmbprRwei3JhyJ+u/vrj",
|
||||
"hP40DBMx7C/c2FN8cnrxmj1RlIeldx8a0BvWov0U28I0OAfEKQml2zAb5iFKWDuq7tOSWZqGS+Al+rCe",
|
||||
"ij2QsjwlEGM589vcz4mgd1MiPnec6te9mmbhoyW+nfoz/j3x3SusxBca2tvrsBtuVBQuul65nC3FYgnG",
|
||||
"stDN7ltdnrTnOxbcQs7oQELO5hvxFDsKZUgbU7qY/VFUjitMcq3VmvLv1DqrN8wqxplPHpwYho2zvtTn",
|
||||
"pk3x/S/CWAR0hOlbDOplJsZJOMLeLiq889DQ5X/Y3hEfZThAQHZvUocfAcDTVcNs8PBsJ7cFZhiokTIc",
|
||||
"MxKatYBO2GsKXo3jqnMeQ3fgtvyd9ZBLe10vscXX9PqqaW0h8aFpZrWgiyZY+FhjcaTXp+A+bU/9yAKT",
|
||||
"+M6alRX4C0Uk5lWceRu2R6JdamToN06Joz+UESRy3O063q/oHf44yKf53OLiNssG47+2RfTuPs8dNlH3",
|
||||
"9X8e4SGROH5yIO7xkDr0HzCOiWicpqkDtPM7OtV7GKeDyHKc00ER/Bg138JDkhFTNiMULHhVORVvDOCt",
|
||||
"RY4QTQJIyl54OO9fpaMEyQMo9LHECSrriXM3je20qoazOE/kOwmTrqc1bI2t+nGbvt3iUzh/MGzXN6H4",
|
||||
"mWLeMPf1MLxM5dj+D7LUEf807mH0SbQKKMu3vS3W6gYGnZsHuWxRu+MdLM+AwM9TNyM5qMrcuRS+8Mg0",
|
||||
"LISxuL17e5B0SN75zwzjbKbV2oA+MSSRMVH82ZwtQ8PCaLrHjAUMGV9wISnywdPadijML8syQcaHEOTB",
|
||||
"MgfJ8fNxWiFJjxW5uJ8ouY+4QOCy6z94eBconBk5wAF6WVUhrDFbYtVHggprYeYH4mvvIMYjJ8VaoqXt",
|
||||
"k4R1INQ9yMrZJ/9rjzcX0/uBjPIevPs437fn1lLhOOctRc6ztglzVBnf+9afh9dEbNs5MA7psguIwfah",
|
||||
"iy+J/mN7D2muiRuhox6Fth2grSl0KA/rySnz854uO3sI2xM1Dzyy4SEZGbc6jpAPHJZGBqoxyzN/jGbU",
|
||||
"KPlTOA9plPwSY0UKTAWfGPZfLy8vXjM6lsOuYUNHhaN1KWn8Z9igv+OmfcslX4CeeGdo1q+xhdbqs0/+",
|
||||
"1x4DHS7XfEBa9C7xHCWIv8r0QaxES4rjrHSKpggV9+daHwqAMTP10lyTjYq76U+MT8bRrQhLLks1n7Mn",
|
||||
"C8WrnJVQ4K2EJqerYN0TXjr/2OSUQ2xzcTnDXo6n/oA7drJg7y59HJ9lF4a194BSGy1qfHjte20Mr30/",
|
||||
"DlZf8WQ9Nr52+c0WXG6i056h6JvIBr4i8o+K79ePKr4BeiRTdLDHhT/Uczt5rExbUliFtKB1s/o84vpG",
|
||||
"lj7XQAcckEzDLfIiAPmFMLUlGnK0vcTm83LSq4T5LIy8IqUlJSdZJ/0Vpt+gEQxA6LRwdmrI8CuQpb8r",
|
||||
"5IGcov5NJAf5RY8rY0jEZWcwvtD0fXTKb48/0Xq4D+RM7HI2I0fzvj0Iwvw496FPvLOi66BO1obfdF3G",
|
||||
"/SOZbfjxpG2kzH0Pcs4aSb+o9UrigTv33OELBR7WmiyEFQupNDx12yzHA6c5buBch8tU2jZVatre0a09",
|
||||
"FvtQzg/b+fFoPx5BoJto0DZEx3TVWjIhS/joHAmpLLOqKZbp8p+XrdB/PmDayKFhqwKCvpwjjXWOj0es",
|
||||
"QzVnpnHYOcJgKjEUM0N3xhI4nVNMlaAdptndsqb3qxOBLgnVuAR96jw6dPy+tALhIytf6S9YTWoepvEl",
|
||||
"XgqLrdtOSPAuMke8Gdg1gGQc+7dJC9pcwKInnJMx4X1Nl6AfkJn3PeKHIzzWVkFnDn73PJ4hv3ue9ubf",
|
||||
"z6Ur7S27Y0ksbovlv7qO4PGlwzzMY9Ydr/zU6qaffetO4MgybEDh7Kiz3UIuaFfx32solKZNkBramIaa",
|
||||
"i0S04Q9pfU5/pT2Hdpzncj/O3L6uxYi4EHcdcmbgQ77VgnbpNszZhq24MXjOEA9Y48nmD2zGi2vcX934",
|
||||
"SbLdzFFrZ6PhPTQFjlgmPP/am6rmH0Xd1Nn5V8+ePcMrW/2fR3Ua3kMynu7GPyAb/xNeFkKn2T3TngQn",
|
||||
"7Ztnz55++U4y+mWPb4F8gNsmrSihRpc/+oPEp0ImE2Ptrda5v8wKz8WOWDMhrULXInipwlaxB+6/j851",
|
||||
"hlzauNc958aezpVec12arW+LJRTXKlym5rtHdWRuU3YWk3VcX3co+KPUE/ajP0csqDDvHr2DeWMcDkvn",
|
||||
"LQ3/U7EuE5Zw7vGYaNoUP248Tif09qbxvuz4/CwI6uMrUJuZ9hLsFUgFwu6QYFKKrWgTA1hV3VA7x4YV",
|
||||
"Ss4rUVg6yKNhARLlSC6Y/wklpa8n7Cq6TsuDE+fLMU41IaMVMlbh0ii6+kHCfQj2hfx/sb4PsQ4J0Z21",
|
||||
"8XD1x6OUx/u0P6xM3tOMgNKWI/Xl7M/7UsGyj8hYysppq8FtUtjoBrt+uSddFqDkbVwTuP88cf8qv0eu",
|
||||
"nx+iwFFV7Z9EV8P9cYcEFtFlc/14Ao9HCWef+7dHHHQxQ86uwYmYTV9XdxHdVhfdTYKbj0jeGuOvwFPr",
|
||||
"EFX6UMg5gmMHaK5aOjyGPWqvYTzQFrWE//Ijgia6ESIpTz+jO56wPXSlInrrztaM+h+lAiNPbDJ7195H",
|
||||
"8ZDdxWGNsQ5j/z5c1zD5p8073d7+bwAAAP//",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
+16
-13
@@ -25,9 +25,6 @@ func (s *Server) ListInbox(w http.ResponseWriter, r *http.Request, params api.Li
|
||||
if params.State != nil {
|
||||
f.State = *params.State
|
||||
}
|
||||
if params.IncludeSnoozed != nil {
|
||||
f.IncludeSnoozed = *params.IncludeSnoozed
|
||||
}
|
||||
|
||||
items, err := s.store.ListInbox(r.Context(), f)
|
||||
if err != nil {
|
||||
@@ -97,10 +94,9 @@ func (s *Server) RespondInbox(w http.ResponseWriter, r *http.Request) {
|
||||
ItemId: item.Id,
|
||||
User: actor(r),
|
||||
Type: create.Type,
|
||||
Option: create.Option,
|
||||
Options: create.Options,
|
||||
Text: create.Text,
|
||||
Attachments: attachments,
|
||||
SnoozeUntil: create.SnoozeUntil,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
},
|
||||
})
|
||||
@@ -155,8 +151,21 @@ func checkResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxStat
|
||||
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")
|
||||
if c.Options == nil || len(*c.Options) == 0 {
|
||||
return bad("option needs the chosen options")
|
||||
}
|
||||
if item.Payload["multiple"] != true && len(*c.Options) != 1 {
|
||||
return bad("a pick-one question takes exactly one option")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, o := range *c.Options {
|
||||
if !hasOption(item, o) {
|
||||
return bad("%q is not one of the question's options", o)
|
||||
}
|
||||
if seen[o] {
|
||||
return bad("%q chosen twice", o)
|
||||
}
|
||||
seen[o] = true
|
||||
}
|
||||
if text != "" {
|
||||
return bad("option takes no text; use other")
|
||||
@@ -172,12 +181,6 @@ func checkResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxStat
|
||||
}
|
||||
return api.Answered, nil
|
||||
|
||||
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.Dismiss:
|
||||
if question {
|
||||
return bad("questions need an option or other, not a dismissal")
|
||||
|
||||
@@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
)
|
||||
@@ -15,14 +14,19 @@ func TestCheckResponse(t *testing.T) {
|
||||
State: api.Open,
|
||||
Payload: map[string]any{"options": []any{"red", "blue"}},
|
||||
}
|
||||
checklist := api.InboxItem{
|
||||
Id: "q2",
|
||||
Kind: api.Question,
|
||||
State: api.Open,
|
||||
Payload: map[string]any{"options": []any{"tests", "docs", "lint"}, "multiple": true},
|
||||
}
|
||||
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
|
||||
|
||||
str := func(s string) *string { return &s }
|
||||
future := time.Now().Add(time.Hour)
|
||||
past := time.Now().Add(-time.Hour)
|
||||
opts := func(s ...string) *[]string { return &s }
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -31,20 +35,25 @@ func TestCheckResponse(t *testing.T) {
|
||||
state api.InboxState
|
||||
status int
|
||||
}{
|
||||
{"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},
|
||||
{"option", question, api.InboxResponseCreate{Type: api.Option, Options: opts("blue")}, api.Answered, 0},
|
||||
{"option with text", question, api.InboxResponseCreate{Type: api.Option, Options: opts("red"), Text: str("but darker")}, "", http.StatusBadRequest},
|
||||
{"unknown option", question, api.InboxResponseCreate{Type: api.Option, Options: opts("green")}, "", http.StatusBadRequest},
|
||||
{"missing option", question, api.InboxResponseCreate{Type: api.Option}, "", http.StatusBadRequest},
|
||||
{"no options", question, api.InboxResponseCreate{Type: api.Option, Options: opts()}, "", http.StatusBadRequest},
|
||||
{"two picks on pick-one", question, api.InboxResponseCreate{Type: api.Option, Options: opts("red", "blue")}, "", http.StatusBadRequest},
|
||||
{"checked", checklist, api.InboxResponseCreate{Type: api.Option, Options: opts("tests", "lint")}, api.Answered, 0},
|
||||
{"checked one", checklist, api.InboxResponseCreate{Type: api.Option, Options: opts("docs")}, api.Answered, 0},
|
||||
{"checked twice", checklist, api.InboxResponseCreate{Type: api.Option, Options: opts("docs", "docs")}, "", http.StatusBadRequest},
|
||||
{"checked unknown", checklist, api.InboxResponseCreate{Type: api.Option, Options: opts("docs", "deploy")}, "", 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},
|
||||
{"snooze", question, api.InboxResponseCreate{Type: "snooze"}, "", 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},
|
||||
{"option on report", report, api.InboxResponseCreate{Type: api.Option, Options: opts("red")}, "", http.StatusBadRequest},
|
||||
{"already answered", closed, api.InboxResponseCreate{Type: api.Option, Options: opts("red")}, "", http.StatusConflict},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
+33
-38
@@ -88,11 +88,10 @@ func (s *Store) GetInboxItem(ctx context.Context, id string) (api.InboxItem, err
|
||||
}
|
||||
|
||||
type InboxFilter struct {
|
||||
ProjectID string
|
||||
TaskID string
|
||||
SessionID string
|
||||
State api.InboxState
|
||||
IncludeSnoozed bool
|
||||
ProjectID string
|
||||
TaskID string
|
||||
SessionID string
|
||||
State api.InboxState
|
||||
}
|
||||
|
||||
func (s *Store) ListInbox(ctx context.Context, f InboxFilter) ([]api.InboxItem, error) {
|
||||
@@ -111,10 +110,6 @@ func (s *Store) ListInbox(ctx context.Context, f InboxFilter) ([]api.InboxItem,
|
||||
where = append(where, "agent_session_id = ?")
|
||||
args = append(args, f.SessionID)
|
||||
}
|
||||
if !f.IncludeSnoozed {
|
||||
where = append(where, "(snoozed_until IS NULL OR snoozed_until <= ?)")
|
||||
args = append(args, now())
|
||||
}
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id FROM inbox_items WHERE `+strings.Join(where, " AND ")+` ORDER BY created_at`, args...)
|
||||
if err != nil {
|
||||
@@ -162,21 +157,26 @@ func (s *Store) Respond(ctx context.Context, actor string, resolutions []Resolut
|
||||
return err
|
||||
}
|
||||
|
||||
var options *string
|
||||
if resp.Options != nil {
|
||||
b, err := json.Marshal(*resp.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s := string(b)
|
||||
options = &s
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO inbox_responses (id, item_id, user_login, type, option, text, attachments, snooze_until, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, resp.Id, resp.ItemId, resp.User, string(resp.Type), resp.Option, resp.Text, string(attachments),
|
||||
formatTimePtr(resp.SnoozeUntil), formatTime(resp.CreatedAt))
|
||||
INSERT INTO inbox_responses (id, item_id, user_login, type, options, text, attachments, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, resp.Id, resp.ItemId, resp.User, string(resp.Type), options, resp.Text, string(attachments), formatTime(resp.CreatedAt))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.State == api.Open {
|
||||
_, err = tx.ExecContext(ctx, `UPDATE inbox_items SET snoozed_until = ? WHERE id = ?`, formatTimePtr(resp.SnoozeUntil), resp.ItemId)
|
||||
} else {
|
||||
_, err = tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_by = ?, resolved_at = ?, snoozed_until = NULL WHERE id = ?`,
|
||||
string(r.State), actor, now(), resp.ItemId)
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_by = ?, resolved_at = ? WHERE id = ?`,
|
||||
string(r.State), actor, now(), resp.ItemId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,7 +194,7 @@ var ErrNotOpen = errors.New("inbox item is not open")
|
||||
|
||||
func (s *Store) WithdrawItem(ctx context.Context, id string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_at = ?, snoozed_until = NULL WHERE id = ? AND state = ?`,
|
||||
res, err := tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_at = ? WHERE id = ? AND state = ?`,
|
||||
string(api.Dismissed), now(), id, string(api.Open))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -228,7 +228,7 @@ func dismissTaskInbox(ctx context.Context, tx *sql.Tx, emit emitFunc, taskID, ac
|
||||
rows.Close()
|
||||
|
||||
for _, id := range ids {
|
||||
_, err = tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_by = ?, resolved_at = ?, snoozed_until = NULL WHERE id = ?`,
|
||||
_, err = tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_by = ?, resolved_at = ? WHERE id = ?`,
|
||||
string(api.Dismissed), actor, now(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -324,16 +324,15 @@ func getItem(ctx context.Context, q querier, id string) (api.InboxItem, error) {
|
||||
payload := ""
|
||||
state := ""
|
||||
createdAt := ""
|
||||
snoozed := sql.NullString{}
|
||||
resolvedBy := sql.NullString{}
|
||||
resolvedAt := sql.NullString{}
|
||||
|
||||
err := q.QueryRowContext(ctx, `
|
||||
SELECT id, project_id, task_id, agent_session_id, thread_id, kind, priority, blocking, payload, state,
|
||||
snoozed_until, created_at, resolved_by, resolved_at
|
||||
created_at, resolved_by, resolved_at
|
||||
FROM inbox_items WHERE id = ?
|
||||
`, id).Scan(&item.Id, &item.ProjectId, &item.TaskId, &item.AgentSessionId, &item.ThreadId, &kind, &priority, &item.Blocking,
|
||||
&payload, &state, &snoozed, &createdAt, &resolvedBy, &resolvedAt)
|
||||
&payload, &state, &createdAt, &resolvedBy, &resolvedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
@@ -349,10 +348,6 @@ func getItem(ctx context.Context, q querier, id string) (api.InboxItem, error) {
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
item.SnoozedUntil, err = parseTimePtr(snoozed)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
item.ResolvedAt, err = parseTimePtr(resolvedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -367,7 +362,7 @@ func getItem(ctx context.Context, q querier, id string) (api.InboxItem, error) {
|
||||
|
||||
func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxResponse, error) {
|
||||
rows, err := q.QueryContext(ctx, `
|
||||
SELECT id, item_id, user_login, type, option, text, attachments, snooze_until, created_at, delivered_at
|
||||
SELECT id, item_id, user_login, type, options, text, attachments, created_at, delivered_at
|
||||
FROM inbox_responses WHERE item_id = ? ORDER BY created_at
|
||||
`, itemID)
|
||||
if err != nil {
|
||||
@@ -379,21 +374,25 @@ func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxRes
|
||||
for rows.Next() {
|
||||
r := api.InboxResponse{}
|
||||
typ := ""
|
||||
option := sql.NullString{}
|
||||
options := sql.NullString{}
|
||||
text := sql.NullString{}
|
||||
attachments := ""
|
||||
snooze := sql.NullString{}
|
||||
createdAt := ""
|
||||
delivered := sql.NullString{}
|
||||
|
||||
err = rows.Scan(&r.Id, &r.ItemId, &r.User, &typ, &option, &text, &attachments, &snooze, &createdAt, &delivered)
|
||||
err = rows.Scan(&r.Id, &r.ItemId, &r.User, &typ, &options, &text, &attachments, &createdAt, &delivered)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.Type = api.InboxResponseType(typ)
|
||||
if option.Valid {
|
||||
r.Option = &option.String
|
||||
if options.Valid {
|
||||
chosen := []string{}
|
||||
err = json.Unmarshal([]byte(options.String), &chosen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Options = &chosen
|
||||
}
|
||||
if text.Valid {
|
||||
r.Text = &text.String
|
||||
@@ -402,10 +401,6 @@ func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxRes
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.SnoozeUntil, err = parseTimePtr(snooze)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.CreatedAt, err = parseTime(createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -194,6 +194,59 @@ func TestBlobsMigrationOverPositionalCopy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInlineQuestionChoicesMigration(t *testing.T) {
|
||||
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
|
||||
i := slices.IndexFunc(migrations, func(m migration) bool { return m.name == "inline-question-choices" })
|
||||
err := migrate(db, migrations[:i])
|
||||
if err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
insertReport(t, db, "r1", `{"text":"x"}`)
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO inbox_items (id, project_id, task_id, agent_session_id, thread_id, kind, priority, blocking, payload, state, created_at)
|
||||
VALUES ('q1', 'p1', 't1', 's1', 'q1', 'question', 0, 1,
|
||||
'{"context":"Pick a color.","question":"Which?","why_blocked":"w","options":["red","blue"],"recommended_option":"blue","attachments":[]}', 'answered', 'x');
|
||||
INSERT INTO inbox_responses (id, item_id, user_login, type, option, text, attachments, created_at) VALUES ('a1', 'q1', 'alice', 'option', 'red', NULL, '[]', 'x');
|
||||
INSERT INTO inbox_responses (id, item_id, user_login, type, option, text, attachments, created_at) VALUES ('a2', 'q1', 'alice', 'other', NULL, 'green', '[]', 'x');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert question: %v", err)
|
||||
}
|
||||
|
||||
err = migrate(db, migrations)
|
||||
if err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
payload := reportPayload(t, db, "q1")
|
||||
want := `{"context":"Pick a color.\n\n- ( ) red\n- (*) blue","question":"Which?","why_blocked":"w","options":["red","blue"],"attachments":[],"multiple":false,"recommended":["blue"]}`
|
||||
if payload != want {
|
||||
t.Fatalf("question payload\n got %s\nwant %s", payload, want)
|
||||
}
|
||||
if reportPayload(t, db, "r1") != `{"text":"x"}` {
|
||||
t.Fatalf("report payload changed")
|
||||
}
|
||||
|
||||
options := map[string]sql.NullString{}
|
||||
rows, err := db.Query(`SELECT id, options FROM inbox_responses`)
|
||||
if err != nil {
|
||||
t.Fatalf("responses: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
id := ""
|
||||
o := sql.NullString{}
|
||||
err = rows.Scan(&id, &o)
|
||||
if err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
options[id] = o
|
||||
}
|
||||
if options["a1"].String != `["red"]` || options["a2"].Valid {
|
||||
t.Fatalf("response options %+v", options)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenReportPayloadsRunsTwice(t *testing.T) {
|
||||
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
|
||||
err := migrate(db, migrations)
|
||||
|
||||
@@ -192,6 +192,20 @@ CREATE TABLE snapshots (
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE (task_id, n)
|
||||
);
|
||||
`}, {"inline-question-choices", `
|
||||
ALTER TABLE inbox_responses ADD COLUMN options TEXT;
|
||||
UPDATE inbox_responses SET options = json_array(option) WHERE option IS NOT NULL;
|
||||
ALTER TABLE inbox_responses DROP COLUMN option;
|
||||
|
||||
UPDATE inbox_items SET payload = json_set(json_remove(payload, '$.recommended_option'),
|
||||
'$.context', json_extract(payload, '$.context') || char(10) || char(10) || (
|
||||
SELECT group_concat(CASE WHEN o.value = json_extract(inbox_items.payload, '$.recommended_option') THEN '- (*) ' ELSE '- ( ) ' END || o.value, char(10))
|
||||
FROM json_each(inbox_items.payload, '$.options') o),
|
||||
'$.multiple', json('false'),
|
||||
'$.recommended', json_array(json_extract(payload, '$.recommended_option')))
|
||||
WHERE kind = 'question' AND json_type(payload, '$.recommended_option') IS NOT NULL;
|
||||
`}, {"unsnooze-inbox-items", `
|
||||
UPDATE inbox_items SET snoozed_until = NULL WHERE snoozed_until IS NOT NULL;
|
||||
`}}
|
||||
|
||||
type Store struct {
|
||||
|
||||
Reference in New Issue
Block a user