Answer refinements
This commit is contained in:
@@ -23,5 +23,3 @@
|
||||
Switcher and Project rows show one derived label instead of the raw session state, by precedence: "Needs your answer" (an open inbox item), "Merging" or "Merge held", "Error", "Working", "Idle".
|
||||
- Unread summary marker
|
||||
Switcher and Project rows show a "New summary" marker, with text and an accessible label, when a task has a summary the user hasn't read. Selecting it opens the Transcript at that summary. Read state is per browser and per user, by event seq; a summary counts as read only when shown in a visible transcript or through "Mark seen", not by opening another tab or switching tasks. Task carries its latest summary seq so the marker survives reload without loading transcripts.
|
||||
- Answer refinements
|
||||
Choice blocks get clearer focus and arrow-key navigation. A "Let the agent decide" answer sends "Use your judgment for this decision and continue within the existing task instructions." and appears in the answer history; it never stands in for a required owner authorization.
|
||||
|
||||
+1
-1
@@ -226,7 +226,7 @@ An open byre page shows a browser notification for each new inbox item, raised b
|
||||
|
||||
The left pane's **Settings** tab asks the browser for notification permission and edits the preferences.
|
||||
|
||||
The Inbox tab shows one item at a time, the task's oldest, filling the tab without a card frame; the next appears when it is answered. Each item is answered on its own; there is no batch submission. A pick-one question's choices are clickable blocks, and clicking one sends that answer at once. A choice's block (or a pick-any choice's checkbox label) holds the whole list item: its first line, which is the choice's text, and everything under it, such as an example's paragraph or code block. A pick-any question's choices are checkboxes, sent with the Submit button at the bottom of the card. Below the choices, an "Other…" block in the same style opens the only text field. A card has at most one Submit button: on a pick-any question it sends the checked choices with the Other text alongside (one `option` response), or the Other text alone as `other` when nothing is checked. An error has a "Reply…" block (`other`) and a Dismiss button. The Other and Reply text boxes send with the Submit button under them or on Enter (Shift+Enter for a newline), like the message box, and fold back into their block when left empty. The list updates in place: a card stays attached while its item is unchanged, so task and inbox updates from other agents don't pull focus out of a text box being typed in.
|
||||
The Inbox tab shows one item at a time, the task's oldest, filling the tab without a card frame; the next appears when it is answered. Each item is answered on its own; there is no batch submission. A pick-one question's choices are clickable blocks, and clicking one sends that answer at once. A choice's block (or a pick-any choice's checkbox label) holds the whole list item: its first line, which is the choice's text, and everything under it, such as an example's paragraph or code block. A pick-any question's choices are checkboxes, sent with the Submit button at the bottom of the card. Below the choices, a "Let the agent decide" block sends at once an `other` answer reading "Use your judgment for this decision and continue within the existing task instructions." (`delegateText` in `web/src/inbox.ts`), which shows in the card's answer list like any other; the `ask_human` description tells agents that this answer settles only that decision and never grants an authorization the owner must give explicitly. Under it, an "Other…" block in the same style opens the only text field. The choice blocks, the checkboxes and these two blocks take focus with Tab, and ArrowDown/ArrowUp move focus between them; the focused one gets a 2px accent ring. A card has at most one Submit button: on a pick-any question it sends the checked choices with the Other text alongside (one `option` response), or the Other text alone as `other` when nothing is checked. An error has a "Reply…" block (`other`) and a Dismiss button. The Other and Reply text boxes send with the Submit button under them or on Enter (Shift+Enter for a newline), like the message box, and fold back into their block when left empty. The list updates in place: a card stays attached while its item is unchanged, so task and inbox updates from other agents don't pull focus out of a text box being typed in.
|
||||
|
||||
There is no snoozing. The `snoozed_until` and `snooze_until` columns are still in the schema, always empty, so builds from older branches keep working against the same database; nothing reads them. For the same reason `inbox_items` keeps its `priority` and `blocking` columns, which nothing reads or writes; new rows take their defaults (0 and 1, which older builds read as a blocking question).
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ func (m *Manager) askTool(l *live) mcp.Tool {
|
||||
"its example, code, commands and trade-offs, so the human never has to match a paragraph above to an option below. " +
|
||||
"Text above the list covers only what all the choices share. " +
|
||||
"End your turn after asking; the answer arrives as your next message. " +
|
||||
"An answer of \"Use your judgment for this decision and continue within the existing task instructions.\" lets you settle that one decision yourself; " +
|
||||
"it never grants an authorization the owner must give explicitly, such as to deploy, push or commit: ask for that on its own. " +
|
||||
"One call is one decision, answered by picking from its own choices. When you have several decisions, such as a design with several choices, " +
|
||||
"never bundle them into one call that asks for approval of them all: call this once per decision, each with its own context and choices; they are answered together. " +
|
||||
"To answer a follow-up question from the human, call this again with the thread_id they gave you.",
|
||||
|
||||
Vendored
+18
-2
@@ -7,6 +7,7 @@ import { diagram } from "./mermaid.js";
|
||||
import { reviewCard } from "./reviews.js";
|
||||
import { focusTab, onEach, state } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
const delegateText = "Use your judgment for this decision and continue within the existing task instructions.";
|
||||
export function mountInbox(root) {
|
||||
const list = h("div", { class: "inbox-list" });
|
||||
const cards = new Map();
|
||||
@@ -181,12 +182,15 @@ function card(item) {
|
||||
};
|
||||
return h("div", { class: "text-entry" }, block, editor);
|
||||
};
|
||||
const delegate = () => clickable(h("div", { class: "pick other", role: "button", tabIndex: 0 }, "Let the agent decide"), () => {
|
||||
void submit({ item_id: item.id, type: "other", text: delegateText });
|
||||
});
|
||||
const renderActions = () => {
|
||||
switch (item.kind) {
|
||||
case "question": {
|
||||
const placeholder = "Your answer, a question back, or new instructions";
|
||||
if (!multiple) {
|
||||
replace(actions, textEntry("Other…", placeholder));
|
||||
replace(actions, delegate(), textEntry("Other…", placeholder));
|
||||
return;
|
||||
}
|
||||
const send = h("button", { type: "button", class: "primary" }, "Submit");
|
||||
@@ -207,7 +211,7 @@ function card(item) {
|
||||
const other = textEntry("Other…", placeholder, { onInput: refresh, onSubmit: go });
|
||||
send.onclick = go;
|
||||
refresh();
|
||||
replace(actions, other, h("div", { class: "submit-row" }, send));
|
||||
replace(actions, delegate(), other, h("div", { class: "submit-row" }, send));
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
@@ -231,6 +235,18 @@ function card(item) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
el.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") {
|
||||
return;
|
||||
}
|
||||
const stops = [...el.querySelectorAll(".pick, .option.choice-box input")].filter((s) => s.offsetParent !== null);
|
||||
const at = stops.indexOf(e.target);
|
||||
if (at < 0) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
stops[Math.min(Math.max(at + (e.key === "ArrowDown" ? 1 : -1), 0), stops.length - 1)].focus();
|
||||
});
|
||||
renderActions();
|
||||
return el;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+22
-2
@@ -8,6 +8,8 @@ import { reviewCard } from "./reviews.js";
|
||||
import { focusTab, onEach, state } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
|
||||
const delegateText = "Use your judgment for this decision and continue within the existing task instructions.";
|
||||
|
||||
export function mountInbox(root: HTMLElement): () => void {
|
||||
const list = h("div", { class: "inbox-list" });
|
||||
const cards = new Map<string, { sig: string; el: HTMLElement }>();
|
||||
@@ -208,12 +210,17 @@ function card(item: InboxItem): HTMLElement {
|
||||
return h("div", { class: "text-entry" }, block, editor);
|
||||
};
|
||||
|
||||
const delegate = () =>
|
||||
clickable(h("div", { class: "pick other", role: "button", tabIndex: 0 }, "Let the agent decide"), () => {
|
||||
void submit({ item_id: item.id, type: "other", text: delegateText });
|
||||
});
|
||||
|
||||
const renderActions = () => {
|
||||
switch (item.kind) {
|
||||
case "question": {
|
||||
const placeholder = "Your answer, a question back, or new instructions";
|
||||
if (!multiple) {
|
||||
replace(actions, textEntry("Other…", placeholder));
|
||||
replace(actions, delegate(), textEntry("Other…", placeholder));
|
||||
return;
|
||||
}
|
||||
const send = h("button", { type: "button", class: "primary" }, "Submit");
|
||||
@@ -234,7 +241,7 @@ function card(item: InboxItem): HTMLElement {
|
||||
const other = textEntry("Other…", placeholder, { onInput: refresh, onSubmit: go });
|
||||
send.onclick = go;
|
||||
refresh();
|
||||
replace(actions, other, h("div", { class: "submit-row" }, send));
|
||||
replace(actions, delegate(), other, h("div", { class: "submit-row" }, send));
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
@@ -266,6 +273,19 @@ function card(item: InboxItem): HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") {
|
||||
return;
|
||||
}
|
||||
const stops = [...el.querySelectorAll<HTMLElement>(".pick, .option.choice-box input")].filter((s) => s.offsetParent !== null);
|
||||
const at = stops.indexOf(e.target as HTMLElement);
|
||||
if (at < 0) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
stops[Math.min(Math.max(at + (e.key === "ArrowDown" ? 1 : -1), 0), stops.length - 1)]!.focus();
|
||||
});
|
||||
|
||||
renderActions();
|
||||
return el;
|
||||
}
|
||||
|
||||
+15
-1
@@ -1716,10 +1716,20 @@ pre.file-text .line::before {
|
||||
}
|
||||
|
||||
.pick.choice-box:hover > .choice-head,
|
||||
.pick.choice-box:focus-visible > .choice-head {
|
||||
.pick.choice-box:focus-visible > .choice-head,
|
||||
.option.choice-box:has(input:focus-visible) > .choice-head {
|
||||
background: var(--accent-tint);
|
||||
}
|
||||
|
||||
.option.choice-box:has(input:focus-visible) {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent);
|
||||
}
|
||||
|
||||
.choice-head input:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pick {
|
||||
display: block;
|
||||
padding: 4px 10px;
|
||||
@@ -1739,6 +1749,10 @@ pre.file-text .line::before {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pick:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--accent);
|
||||
}
|
||||
|
||||
.pick[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user