Put a question to the reader on a row of its own
An AskUserQuestion arrived in the middle of a run of tool calls and was folded into the collapsed card with them, so the one row where somebody was asked something -- and the answer they gave -- was hidden behind "Called 6 tools" like any other grep. It now starts a run of its own and ends the one before it, which needs no change to the grouping: a run of one is drawn as itself. The calls around it become a group before and a group after, so where the work stopped to ask is legible from the shape of the transcript without opening anything. Echo's `/ask` now runs three ordinary calls on each side of the question, because that is the shape this has to be looked at in and there was no way to produce it.
This commit is contained in:
1 parent
09f7f8d203
commit
bc0a48799c
3 files changed
+63
-9
No files matched your search
@@ -215,9 +215,20 @@ sealed class TranscriptItem {
|
||||
* Only ever consulted when the call is first folded in. That is what makes the name stable -- a run
|
||||
* keeps whatever it was called when it started, however many calls arrive at either end of it
|
||||
* afterwards.
|
||||
*
|
||||
* A question to the reader is in a run of its own, which is what puts it on the transcript as a row
|
||||
* rather than inside a collapsed "Called 6 tools" card. Two things follow from being alone: it is
|
||||
* always visible, since a run of one is drawn as itself rather than as a group; and the calls
|
||||
* around it fall into a group before it and a group after it, so where the reader was asked
|
||||
* something is legible in the shape of the transcript without opening anything. It ends the run
|
||||
* before it as well as starting a fresh one after -- the moment somebody was asked is a boundary in
|
||||
* the work, not a gap in the middle of one run.
|
||||
*/
|
||||
private fun runIdFor(items: List<TranscriptItem>, id: String): String =
|
||||
(items.lastOrNull() as? TranscriptItem.ToolRun)?.runId ?: id
|
||||
private fun runIdFor(items: List<TranscriptItem>, id: String, tool: String): String {
|
||||
val previous = items.lastOrNull() as? TranscriptItem.ToolRun ?: return id
|
||||
if (tool == ASK_USER_QUESTION || previous.tool == ASK_USER_QUESTION) return id
|
||||
return previous.runId
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a page of older items in front of the ones already loaded, healing whatever the page
|
||||
@@ -310,8 +321,15 @@ private fun adoptRun(
|
||||
earlier: List<TranscriptItem>,
|
||||
later: List<TranscriptItem>,
|
||||
): List<TranscriptItem> {
|
||||
val joining = (later.firstOrNull() as? TranscriptItem.ToolRun)?.runId ?: return earlier
|
||||
val tail = earlier.takeLastWhile { it is TranscriptItem.ToolRun }
|
||||
val first = later.firstOrNull() as? TranscriptItem.ToolRun ?: return earlier
|
||||
// A question is in a run of its own on both sides of the join, the same as it would be had
|
||||
// the two pages been folded as one -- see `runIdFor`. Without this the heal would merge a
|
||||
// group straight through the row the reader was asked something on.
|
||||
if (first.tool == ASK_USER_QUESTION) return earlier
|
||||
val joining = first.runId
|
||||
val tail = earlier.takeLastWhile {
|
||||
it is TranscriptItem.ToolRun && it.tool != ASK_USER_QUESTION
|
||||
}
|
||||
if (tail.isEmpty()) return earlier
|
||||
return earlier.dropLast(tail.size) +
|
||||
tail.map { (it as TranscriptItem.ToolRun).copy(runId = joining) }
|
||||
@@ -337,7 +355,7 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
|
||||
TranscriptItem.ToolRun(
|
||||
entry.seq,
|
||||
event.id,
|
||||
runIdFor(items, event.id),
|
||||
runIdFor(items, event.id, event.tool),
|
||||
event.tool,
|
||||
event.input,
|
||||
"",
|
||||
@@ -358,7 +376,10 @@ fun foldEvent(items: List<TranscriptItem>, entry: SeqEvent): List<TranscriptItem
|
||||
TranscriptItem.ToolRun(
|
||||
entry.seq,
|
||||
event.id,
|
||||
runIdFor(items, event.id),
|
||||
// The name is not known from an end alone, so a call that was an ask
|
||||
// cannot be recognised as one here; loading the page before this
|
||||
// replaces the row with the real thing, which is when it splits out.
|
||||
runIdFor(items, event.id, "tool"),
|
||||
"tool",
|
||||
"",
|
||||
event.output,
|
||||
|
||||
@@ -296,5 +296,10 @@ private fun PermissionAsk(ask: TranscriptItem.QuestionCard, onAnswer: (List<Stri
|
||||
}
|
||||
}
|
||||
|
||||
/** The tool whose input is a question rather than a command; see [AskUserQuestionBody]. */
|
||||
private const val ASK_USER_QUESTION = "AskUserQuestion"
|
||||
/**
|
||||
* The tool whose input is a question rather than a command; see [AskUserQuestionBody].
|
||||
*
|
||||
* Also what [runIdFor] breaks a run of calls on, so the row a reader answered is never folded
|
||||
* inside a collapsed group.
|
||||
*/
|
||||
const val ASK_USER_QUESTION = "AskUserQuestion"
|
||||
@@ -12,7 +12,10 @@
|
||||
//! - `/question [text]` -- a question, exercising the answer path.
|
||||
//! - `/ask` -- an AskUserQuestion call: two questions on one tool call,
|
||||
//! with descriptions, a preview and a multi-select, which is the shape
|
||||
//! that is awkward to get a real model to produce on demand.
|
||||
//! that is awkward to get a real model to produce on demand. Wrapped in
|
||||
//! a run of ordinary calls on each side, because being asked something
|
||||
//! happens in the middle of work and the screen has to keep it out of
|
||||
//! the collapsed group around it.
|
||||
//! - `/slow [seconds]` -- a turn that stays running (default 30), so states that only
|
||||
//! exist *while* something is happening can be looked at.
|
||||
//! - `/error [text]` -- a failure, which is otherwise awkward to cause.
|
||||
@@ -103,6 +106,26 @@ pub struct EchoDriver {
|
||||
}
|
||||
|
||||
impl EchoDriver {
|
||||
/// A short run of ordinary calls, to sit either side of something.
|
||||
///
|
||||
/// Three, because two is the fewest that groups and three makes it
|
||||
/// obvious the group is a group -- and because the point of the
|
||||
/// fixture is what a question looks like with work around it.
|
||||
fn some_calls(&self, label: &str) {
|
||||
for index in 0..3 {
|
||||
let id = format!("echo-{label}-{index}-{}", super::random_hex());
|
||||
self.emit(Event::ToolStart {
|
||||
id: id.clone(),
|
||||
tool: "echo-tool".to_string(),
|
||||
input: serde_json::json!({ "step": format!("{label} {index}") }),
|
||||
});
|
||||
self.emit(Event::ToolEnd {
|
||||
id,
|
||||
output: format!("{label} step {index} finished"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// An AskUserQuestion call, in the shape the CLI sends one.
|
||||
///
|
||||
/// Two questions on one call, because that is where the display is
|
||||
@@ -175,6 +198,7 @@ impl EchoDriver {
|
||||
self.emit(Event::Status {
|
||||
state: SessionStatus::Running,
|
||||
});
|
||||
self.some_calls("before");
|
||||
self.emit(Event::ToolStart {
|
||||
id: call.clone(),
|
||||
tool: "AskUserQuestion".to_string(),
|
||||
@@ -714,6 +738,10 @@ impl Driver for EchoDriver {
|
||||
id: call,
|
||||
output: format!("answered: {answer}"),
|
||||
});
|
||||
// The work carries on where it left off, which is what makes
|
||||
// the asked-here row a boundary with a group on each side
|
||||
// rather than the last thing in the turn.
|
||||
self.some_calls("after");
|
||||
} else {
|
||||
self.emit(Event::AssistantText {
|
||||
delta: format!("You answered: {answer}"),
|
||||
|
||||
Reference in new issue
Block a user