Inline question choices, compact one-click inbox cards, drop snooze
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user