tool_use: add multiplechoice support to Agent
This implements the "dumb" approach - the tool itself just tells
the llm that it rendered the options to the user, and it's done.
If the user selects one of the options, we paste its response text
into the chat input textarea on the frontend. The user is of
course free to ignore the question or the options presented.
This keeps no association between user response and the original
tool_use block that solicited it from the user. I.e. the user
response message doesn't include the original tool_use_id value
it it. It looks as though the user typed it by hand.
diff --git a/webui/src/types.ts b/webui/src/types.ts
index e7d9386..9bc1e6d 100644
--- a/webui/src/types.ts
+++ b/webui/src/types.ts
@@ -85,6 +85,16 @@
inside_working_dir?: string;
}
+export interface MultipleChoiceOption {
+ caption: string;
+ responseText: string;
+}
+
+export interface MultipleChoiceParams {
+ question: string;
+ responseOptions: MultipleChoiceOption[] | null;
+}
+
export type CodingAgentMessageType = 'user' | 'agent' | 'error' | 'budget' | 'tool' | 'commit' | 'auto';
export type Duration = number;
diff --git a/webui/src/web-components/sketch-app-shell.ts b/webui/src/web-components/sketch-app-shell.ts
index b73c83a..c2d5e6a 100644
--- a/webui/src/web-components/sketch-app-shell.ts
+++ b/webui/src/web-components/sketch-app-shell.ts
@@ -16,6 +16,7 @@
import "./sketch-restart-modal";
import { createRef, ref } from "lit/directives/ref.js";
+import { SketchChatInput } from "./sketch-chat-input";
type ViewMode = "chat" | "diff" | "charts" | "terminal";
@@ -389,7 +390,8 @@
// Binding methods to this
this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
- this._handlePopState = this._handlePopState.bind(this);
+ this._handleMutlipleChoiceSelected =
+ this._handleMutlipleChoiceSelected.bind(this);
this._handleStopClick = this._handleStopClick.bind(this);
this._handleNotificationsToggle =
this._handleNotificationsToggle.bind(this);
@@ -427,6 +429,10 @@
// Add window focus/blur listeners for controlling notifications
window.addEventListener("focus", this._handleWindowFocus);
window.addEventListener("blur", this._handleWindowBlur);
+ window.addEventListener(
+ "multiple-choice-selected",
+ this._handleMutlipleChoiceSelected,
+ );
// register event listeners
this.dataManager.addEventListener(
@@ -460,6 +466,10 @@
window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
window.removeEventListener("focus", this._handleWindowFocus);
window.removeEventListener("blur", this._handleWindowBlur);
+ window.removeEventListener(
+ "multiple-choice-selected",
+ this._handleMutlipleChoiceSelected,
+ );
// unregister data manager event listeners
this.dataManager.removeEventListener(
@@ -528,6 +538,10 @@
}
}
+ private _handleMultipleChoice(event: CustomEvent) {
+ window.console.log("_handleMultipleChoice", event);
+ this._sendChat;
+ }
/**
* Listen for commit diff event
* @param commitHash The commit hash to show diff for
@@ -878,8 +892,20 @@
this.restartModalOpen = false;
}
+ async _handleMutlipleChoiceSelected(e: CustomEvent) {
+ const chatInput = this.shadowRoot?.querySelector(
+ "sketch-chat-input",
+ ) as SketchChatInput;
+ if (chatInput) {
+ chatInput.content = e.detail.responseText;
+ chatInput.focus();
+ }
+ }
+
async _sendChat(e: CustomEvent) {
console.log("app shell: _sendChat", e);
+ e.preventDefault();
+ e.stopPropagation();
const message = e.detail.message?.trim();
if (message == "") {
return;
diff --git a/webui/src/web-components/sketch-tool-calls.ts b/webui/src/web-components/sketch-tool-calls.ts
index 3f036c2..4f49df9 100644
--- a/webui/src/web-components/sketch-tool-calls.ts
+++ b/webui/src/web-components/sketch-tool-calls.ts
@@ -83,6 +83,11 @@
.open=${open}
.toolCall=${toolCall}
></sketch-tool-card-done>`;
+ case "multiplechoice":
+ return html`<sketch-tool-card-multiple-choice
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-multiple-choice>`;
case "patch":
return html`<sketch-tool-card-patch
.open=${open}
diff --git a/webui/src/web-components/sketch-tool-card.ts b/webui/src/web-components/sketch-tool-card.ts
index fc5d0b8..80dd2e9 100644
--- a/webui/src/web-components/sketch-tool-card.ts
+++ b/webui/src/web-components/sketch-tool-card.ts
@@ -1,7 +1,7 @@
import { css, html, LitElement } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { customElement, property } from "lit/decorators.js";
-import { ToolCall } from "../types";
+import { ToolCall, MultipleChoiceOption, MultipleChoiceParams } from "../types";
import { marked, MarkedOptions } from "marked";
function renderMarkdown(markdownContent: string): string {
@@ -606,7 +606,7 @@
open: boolean;
@property()
- selectedOption: string | number | null = null;
+ selectedOption: MultipleChoiceOption = null;
static styles = css`
.options-container {
@@ -666,6 +666,7 @@
.summary-text {
font-style: italic;
+ padding: 0.5em;
}
.summary-text strong {
@@ -710,14 +711,13 @@
).selected;
} catch (e) {
console.error("Error parsing result:", e);
- this.selectedOption = this.toolCall.result_message.tool_result;
}
} else {
this.selectedOption = null;
}
}
- handleOptionClick(choice) {
+ async handleOptionClick(choice) {
// If this option is already selected, unselect it (toggle behavior)
if (this.selectedOption === choice) {
this.selectedOption = null;
@@ -727,8 +727,11 @@
}
// Dispatch a custom event that can be listened to by parent components
- const event = new CustomEvent("option-selected", {
- detail: { selected: this.selectedOption },
+ const event = new CustomEvent("multiple-choice-selected", {
+ detail: {
+ responseText: this.selectedOption.responseText,
+ toolCall: this.toolCall,
+ },
bubbles: true,
composed: true,
});
@@ -740,8 +743,10 @@
let choices = [];
let question = "";
try {
- const inputData = JSON.parse(this.toolCall?.input || "{}");
- choices = inputData.choices || [];
+ const inputData = JSON.parse(
+ this.toolCall?.input || "{}",
+ ) as MultipleChoiceParams;
+ choices = inputData.responseOptions || [];
question = inputData.question || "Please select an option:";
} catch (e) {
console.error("Error parsing multiple-choice input:", e);
@@ -751,29 +756,24 @@
const summaryContent =
this.selectedOption !== null
? html`<span class="summary-text"
- >${question}: <strong>${this.selectedOption}</strong></span
+ >${question}: <strong>${this.selectedOption.caption}</strong></span
>`
: html`<span class="summary-text">${question}</span>`;
- return html` <sketch-tool-card
- .open=${this.open}
- .toolCall=${this.toolCall}
- >
- <span slot="summary">${summaryContent}</span>
- <div slot="input">
- <p>${question}</p>
+ return html`
+ <div class="multiple-choice-card">
+ ${summaryContent}
<div class="options-container">
- ${choices.map((choice, index) => {
+ ${choices.map((choice) => {
const isSelected =
- this.selectedOption !== null &&
- (this.selectedOption === choice || this.selectedOption === index);
+ this.selectedOption !== null && this.selectedOption === choice;
return html`
<div
class="option ${isSelected ? "selected" : ""}"
@click=${() => this.handleOptionClick(choice)}
+ title="${choice.responseText}"
>
- <span class="option-index">${index + 1}</span>
- <span class="option-label">${choice}</span>
+ <span class="option-label">${choice.caption}</span>
${isSelected
? html`<span class="option-checkmark">✓</span>`
: ""}
@@ -782,12 +782,7 @@
})}
</div>
</div>
- <div slot="result">
- ${this.toolCall?.result_message && this.selectedOption
- ? html`<p>Selected: <strong>${this.selectedOption}</strong></p>`
- : ""}
- </div>
- </sketch-tool-card>`;
+ `;
}
}