Inline question choices, compact one-click inbox cards, drop snooze
This commit is contained in:
+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