webui: Refactor tool calls
diff --git a/webui/src/web-components/sketch-tool-calls.ts b/webui/src/web-components/sketch-tool-calls.ts
index 8842aee..8044e33 100644
--- a/webui/src/web-components/sketch-tool-calls.ts
+++ b/webui/src/web-components/sketch-tool-calls.ts
@@ -2,8 +2,15 @@
import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { ToolCall } from "../types";
-import "./sketch-tool-card";
+import "./sketch-tool-card-bash";
+import "./sketch-tool-card-codereview";
+import "./sketch-tool-card-done";
+import "./sketch-tool-card-generic";
+import "./sketch-tool-card-multiple-choice";
+import "./sketch-tool-card-patch";
import "./sketch-tool-card-screenshot";
+import "./sketch-tool-card-think";
+import "./sketch-tool-card-title";
@customElement("sketch-tool-calls")
export class SketchToolCalls extends LitElement {
diff --git a/webui/src/web-components/sketch-tool-card-bash.ts b/webui/src/web-components/sketch-tool-card-bash.ts
new file mode 100644
index 0000000..75e65d9
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-bash.ts
@@ -0,0 +1,119 @@
+import { LitElement, css, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ToolCall } from "../types";
+
+// Common styles shared across all tool cards
+export const commonStyles = css`
+ pre {
+ background: rgb(236, 236, 236);
+ color: black;
+ padding: 0.5em;
+ border-radius: 4px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ max-width: 100%;
+ width: 100%;
+ box-sizing: border-box;
+ overflow-wrap: break-word;
+ }
+ .summary-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 100%;
+ font-family: monospace;
+ }
+`;
+
+@customElement("sketch-tool-card-bash")
+export class SketchToolCardBash extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+
+ static styles = [
+ commonStyles,
+ css`
+ .input {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ }
+ .input pre {
+ width: 100%;
+ margin-bottom: 0;
+ border-radius: 4px 4px 0 0;
+ box-sizing: border-box;
+ }
+ .result pre {
+ margin-top: 0;
+ color: #555;
+ border-radius: 0 0 4px 4px;
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .result pre.scrollable-on-hover {
+ max-height: 300px;
+ overflow-y: auto;
+ }
+ .tool-call-result-container {
+ width: 100%;
+ position: relative;
+ }
+ .background-badge {
+ display: inline-block;
+ background-color: #6200ea;
+ color: white;
+ font-size: 10px;
+ font-weight: bold;
+ padding: 2px 6px;
+ border-radius: 10px;
+ margin-left: 8px;
+ vertical-align: middle;
+ }
+ .command-wrapper {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ `,
+ ];
+
+ render() {
+ const inputData = JSON.parse(this.toolCall?.input || "{}");
+ const isBackground = inputData?.background === true;
+ const backgroundIcon = isBackground ? "๐ " : "";
+
+ return html` <sketch-tool-card
+ .open=${this.open}
+ .toolCall=${this.toolCall}
+ >
+ <span slot="summary" class="summary-text">
+ <div class="command-wrapper">
+ ${backgroundIcon}${inputData?.command}
+ </div>
+ </span>
+ <div slot="input" class="input">
+ <div class="tool-call-result-container">
+ <pre>${backgroundIcon}${inputData?.command}</pre>
+ </div>
+ </div>
+ ${this.toolCall?.result_message?.tool_result
+ ? html`<div slot="result" class="result">
+ <div class="tool-call-result-container">
+ <pre class="tool-call-result">
+${this.toolCall?.result_message.tool_result}</pre
+ >
+ </div>
+ </div>`
+ : ""}
+ </sketch-tool-card>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-bash": SketchToolCardBash;
+ }
+}
diff --git a/webui/src/web-components/sketch-tool-card-codereview.ts b/webui/src/web-components/sketch-tool-card-codereview.ts
new file mode 100644
index 0000000..41a256b
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-codereview.ts
@@ -0,0 +1,37 @@
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ToolCall } from "../types";
+
+@customElement("sketch-tool-card-codereview")
+export class SketchToolCardCodeReview extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+
+ // Determine the status icon based on the content of the result message
+ getStatusIcon(resultText: string): string {
+ if (!resultText) return "";
+ if (resultText === "OK") return "โ๏ธ";
+ if (resultText.includes("# Errors")) return "โ ๏ธ";
+ if (resultText.includes("# Info")) return "โน๏ธ";
+ if (resultText.includes("uncommitted changes in repo")) return "๐งน";
+ if (resultText.includes("no new commits have been added")) return "๐ฃ";
+ if (resultText.includes("git repo is not clean")) return "๐งผ";
+ return "โ";
+ }
+
+ render() {
+ const resultText = this.toolCall?.result_message?.tool_result || "";
+ const statusIcon = this.getStatusIcon(resultText);
+
+ return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text">${statusIcon}</span>
+ <div slot="result"><pre>${resultText}</pre></div>
+ </sketch-tool-card>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-codereview": SketchToolCardCodeReview;
+ }
+}
diff --git a/webui/src/web-components/sketch-tool-card-done.ts b/webui/src/web-components/sketch-tool-card-done.ts
new file mode 100644
index 0000000..860ef75
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-done.ts
@@ -0,0 +1,37 @@
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ToolCall } from "../types";
+
+
+@customElement("sketch-tool-card-done")
+export class SketchToolCardDone extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+
+ render() {
+ const doneInput = JSON.parse(this.toolCall.input);
+ return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text"></span>
+ <div slot="result">
+ ${Object.keys(doneInput.checklist_items).map((key) => {
+ const item = doneInput.checklist_items[key];
+ let statusIcon = "โ";
+ if (item.status == "yes") {
+ statusIcon = "๐";
+ } else if (item.status == "not applicable") {
+ statusIcon = "๐คทโ๏ธ";
+ }
+ return html`<div>
+ <span>${statusIcon}</span> ${key}:${item.status}
+ </div>`;
+ })}
+ </div>
+ </sketch-tool-card>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-done": SketchToolCardDone;
+ }
+}
diff --git a/webui/src/web-components/sketch-tool-card-generic.ts b/webui/src/web-components/sketch-tool-card-generic.ts
new file mode 100644
index 0000000..ab92bc7
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-generic.ts
@@ -0,0 +1,32 @@
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ToolCall } from "../types";
+
+
+@customElement("sketch-tool-card-generic")
+export class SketchToolCardGeneric extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+
+ render() {
+ return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text">${this.toolCall?.input}</span>
+ <div slot="input">
+ Input:
+ <pre>${this.toolCall?.input}</pre>
+ </div>
+ <div slot="result">
+ Result:
+ ${this.toolCall?.result_message?.tool_result
+ ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
+ : ""}
+ </div>
+ </sketch-tool-card>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-generic": SketchToolCardGeneric;
+ }
+}
diff --git a/webui/src/web-components/sketch-tool-card-multiple-choice.ts b/webui/src/web-components/sketch-tool-card-multiple-choice.ts
new file mode 100644
index 0000000..d69ec6b
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-multiple-choice.ts
@@ -0,0 +1,151 @@
+import { LitElement, css, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ToolCall, MultipleChoiceOption, MultipleChoiceParams } from "../types";
+
+@customElement("sketch-tool-card-multiple-choice")
+export class SketchToolCardMultipleChoice extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+ @property() selectedOption: MultipleChoiceOption = null;
+
+ static styles = css`
+ .options-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin: 10px 0;
+ }
+ .option {
+ display: inline-flex;
+ align-items: center;
+ padding: 8px 12px;
+ border-radius: 4px;
+ background-color: #f5f5f5;
+ cursor: pointer;
+ transition: all 0.2s;
+ border: 1px solid transparent;
+ user-select: none;
+ }
+ .option:hover {
+ background-color: #e0e0e0;
+ border-color: #ccc;
+ transform: translateY(-1px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+ .option:active {
+ transform: translateY(0);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ background-color: #d5d5d5;
+ }
+ .option.selected {
+ background-color: #e3f2fd;
+ border-color: #2196f3;
+ border-width: 1px;
+ border-style: solid;
+ }
+ .option-checkmark {
+ margin-left: 6px;
+ color: #2196f3;
+ }
+ .summary-text {
+ font-style: italic;
+ padding: 0.5em;
+ }
+ .summary-text strong {
+ font-style: normal;
+ color: #2196f3;
+ font-weight: 600;
+ }
+ `;
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.updateSelectedOption();
+ }
+
+ updated(changedProps) {
+ if (changedProps.has("toolCall")) {
+ this.updateSelectedOption();
+ }
+ }
+
+ updateSelectedOption() {
+ if (this.toolCall?.result_message?.tool_result) {
+ try {
+ this.selectedOption = JSON.parse(
+ this.toolCall.result_message.tool_result,
+ ).selected;
+ } catch (e) {
+ console.error("Error parsing result:", e);
+ }
+ } else {
+ this.selectedOption = null;
+ }
+ }
+
+ async handleOptionClick(choice) {
+ this.selectedOption = this.selectedOption === choice ? null : choice;
+
+ const event = new CustomEvent("multiple-choice-selected", {
+ detail: {
+ responseText: this.selectedOption.responseText,
+ toolCall: this.toolCall,
+ },
+ bubbles: true,
+ composed: true,
+ });
+ this.dispatchEvent(event);
+ }
+
+ render() {
+ let choices = [];
+ let question = "";
+ try {
+ 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);
+ }
+
+ const summaryContent =
+ this.selectedOption !== null
+ ? html`<span class="summary-text">
+ ${question}: <strong>${this.selectedOption.caption}</strong>
+ </span>`
+ : html`<span class="summary-text">${question}</span>`;
+
+ return html`
+ <div class="multiple-choice-card">
+ ${summaryContent}
+ <div class="options-container">
+ ${choices.map((choice) => {
+ const isSelected =
+ this.selectedOption !== null && this.selectedOption === choice;
+ return html`
+ <div
+ class="option ${isSelected ? "selected" : ""}"
+ @click=${() => this.handleOptionClick(choice)}
+ title="${choice.responseText}"
+ >
+ <span class="option-label">${choice.caption}</span>
+ ${isSelected
+ ? html`<span class="option-checkmark">โ</span>`
+ : ""}
+ </div>
+ `;
+ })}
+ </div>
+ </div>
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
+ }
+}
diff --git a/webui/src/web-components/sketch-tool-card-patch.ts b/webui/src/web-components/sketch-tool-card-patch.ts
new file mode 100644
index 0000000..6e93ba2
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-patch.ts
@@ -0,0 +1,46 @@
+import { LitElement, css, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ToolCall } from "../types";
+
+
+@customElement("sketch-tool-card-patch")
+export class SketchToolCardPatch extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+
+ static styles = css`
+ .summary-text {
+ color: #555;
+ font-family: monospace;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-radius: 3px;
+ }
+ `;
+
+ render() {
+ const patchInput = JSON.parse(this.toolCall?.input);
+ return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text">
+ ${patchInput?.path}: ${patchInput.patches.length}
+ edit${patchInput.patches.length > 1 ? "s" : ""}
+ </span>
+ <div slot="input">
+ ${patchInput.patches.map((patch) => {
+ return html`Patch operation: <b>${patch.operation}</b>
+ <pre>${patch.newText}</pre>`;
+ })}
+ </div>
+ <div slot="result">
+ <pre>${this.toolCall?.result_message?.tool_result}</pre>
+ </div>
+ </sketch-tool-card>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-patch": SketchToolCardPatch;
+ }
+}
diff --git a/webui/src/web-components/sketch-tool-card-think.ts b/webui/src/web-components/sketch-tool-card-think.ts
new file mode 100644
index 0000000..fc0fe09
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-think.ts
@@ -0,0 +1,70 @@
+import { LitElement, css, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { unsafeHTML } from "lit/directives/unsafe-html.js";
+import { marked } from "marked";
+import { ToolCall } from "../types";
+
+@customElement("sketch-tool-card-think")
+export class SketchToolCardThink extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+
+ static styles = css`
+ .thought-bubble {
+ overflow-x: auto;
+ margin-bottom: 3px;
+ font-family: monospace;
+ padding: 3px 5px;
+ background: rgb(236, 236, 236);
+ border-radius: 6px;
+ user-select: text;
+ cursor: text;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ font-size: 13px;
+ line-height: 1.3;
+ }
+ .summary-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-family: monospace;
+ }
+ `;
+
+ render() {
+ return html`
+ <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text">
+ ${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}
+ </span>
+ <div slot="input" class="thought-bubble">
+ <div class="markdown-content">
+ ${unsafeHTML(
+ renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
+ )}
+ </div>
+ </div>
+ </sketch-tool-card>
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-think": SketchToolCardThink;
+ }
+}
+
+function renderMarkdown(markdownContent: string): string {
+ try {
+ return marked.parse(markdownContent, {
+ gfm: true,
+ breaks: true,
+ async: false,
+ }) as string;
+ } catch (error) {
+ console.error("Error rendering markdown:", error);
+ return markdownContent;
+ }
+}
diff --git a/webui/src/web-components/sketch-tool-card-title.ts b/webui/src/web-components/sketch-tool-card-title.ts
new file mode 100644
index 0000000..50b3380
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-title.ts
@@ -0,0 +1,45 @@
+import { LitElement, css, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ToolCall } from "../types";
+
+
+@customElement("sketch-tool-card-title")
+export class SketchToolCardTitle extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+
+ static styles = css`
+ .summary-text {
+ font-style: italic;
+ }
+ pre {
+ display: inline;
+ font-family: monospace;
+ background: rgb(236, 236, 236);
+ padding: 2px 4px;
+ border-radius: 2px;
+ margin: 0;
+ }
+ `;
+
+ render() {
+ const inputData = JSON.parse(this.toolCall?.input || "{}");
+ return html`
+ <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text">
+ Title: "${inputData.title}" | Branch: sketch/${inputData.branch_name}
+ </span>
+ <div slot="input">
+ <div>Set title to: <b>${inputData.title}</b></div>
+ <div>Set branch to: <code>sketch/${inputData.branch_name}</code></div>
+ </div>
+ </sketch-tool-card>
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-title": SketchToolCardTitle;
+ }
+}
diff --git a/webui/src/web-components/sketch-tool-card.ts b/webui/src/web-components/sketch-tool-card.ts
index b4bdd64..8849159 100644
--- a/webui/src/web-components/sketch-tool-card.ts
+++ b/webui/src/web-components/sketch-tool-card.ts
@@ -1,45 +1,6 @@
import { css, html, LitElement } from "lit";
-import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { customElement, property, state } from "lit/decorators.js";
-import { ToolCall, MultipleChoiceOption, MultipleChoiceParams } from "../types";
-import { marked, MarkedOptions } from "marked";
-
-// Shared utility function for markdown rendering
-function renderMarkdown(markdownContent: string): string {
- try {
- return marked.parse(markdownContent, {
- gfm: true,
- breaks: true,
- async: false,
- }) as string;
- } catch (error) {
- console.error("Error rendering markdown:", error);
- return markdownContent;
- }
-}
-
-// Common styles shared across all tool cards
-const commonStyles = css`
- pre {
- background: rgb(236, 236, 236);
- color: black;
- padding: 0.5em;
- border-radius: 4px;
- white-space: pre-wrap;
- word-break: break-word;
- max-width: 100%;
- width: 100%;
- box-sizing: border-box;
- overflow-wrap: break-word;
- }
- .summary-text {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
- font-family: monospace;
- }
-`;
+import { ToolCall } from "../types";
@customElement("sketch-tool-card")
export class SketchToolCard extends LitElement {
@@ -259,439 +220,8 @@
}
}
-@customElement("sketch-tool-card-bash")
-export class SketchToolCardBash extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
-
- static styles = [
- commonStyles,
- css`
- .input {
- display: flex;
- width: 100%;
- flex-direction: column;
- }
- .input pre {
- width: 100%;
- margin-bottom: 0;
- border-radius: 4px 4px 0 0;
- box-sizing: border-box;
- }
- .result pre {
- margin-top: 0;
- color: #555;
- border-radius: 0 0 4px 4px;
- width: 100%;
- box-sizing: border-box;
- }
- .result pre.scrollable-on-hover {
- max-height: 300px;
- overflow-y: auto;
- }
- .tool-call-result-container {
- width: 100%;
- position: relative;
- }
- .background-badge {
- display: inline-block;
- background-color: #6200ea;
- color: white;
- font-size: 10px;
- font-weight: bold;
- padding: 2px 6px;
- border-radius: 10px;
- margin-left: 8px;
- vertical-align: middle;
- }
- .command-wrapper {
- display: inline-block;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- `,
- ];
-
- render() {
- const inputData = JSON.parse(this.toolCall?.input || "{}");
- const isBackground = inputData?.background === true;
- const backgroundIcon = isBackground ? "๐ " : "";
-
- return html` <sketch-tool-card
- .open=${this.open}
- .toolCall=${this.toolCall}
- >
- <span slot="summary" class="summary-text">
- <div class="command-wrapper">
- ${backgroundIcon}${inputData?.command}
- </div>
- </span>
- <div slot="input" class="input">
- <div class="tool-call-result-container">
- <pre>${backgroundIcon}${inputData?.command}</pre>
- </div>
- </div>
- ${this.toolCall?.result_message?.tool_result
- ? html`<div slot="result" class="result">
- <div class="tool-call-result-container">
- <pre class="tool-call-result">
-${this.toolCall?.result_message.tool_result}</pre
- >
- </div>
- </div>`
- : ""}
- </sketch-tool-card>`;
- }
-}
-
-@customElement("sketch-tool-card-codereview")
-export class SketchToolCardCodeReview extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
-
- // Determine the status icon based on the content of the result message
- getStatusIcon(resultText: string): string {
- if (!resultText) return "";
- if (resultText === "OK") return "โ๏ธ";
- if (resultText.includes("# Errors")) return "โ ๏ธ";
- if (resultText.includes("# Info")) return "โน๏ธ";
- if (resultText.includes("uncommitted changes in repo")) return "๐งน";
- if (resultText.includes("no new commits have been added")) return "๐ฃ";
- if (resultText.includes("git repo is not clean")) return "๐งผ";
- return "โ";
- }
-
- render() {
- const resultText = this.toolCall?.result_message?.tool_result || "";
- const statusIcon = this.getStatusIcon(resultText);
-
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">${statusIcon}</span>
- <div slot="result"><pre>${resultText}</pre></div>
- </sketch-tool-card>`;
- }
-}
-
-@customElement("sketch-tool-card-done")
-export class SketchToolCardDone extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
-
- render() {
- const doneInput = JSON.parse(this.toolCall.input);
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text"></span>
- <div slot="result">
- ${Object.keys(doneInput.checklist_items).map((key) => {
- const item = doneInput.checklist_items[key];
- let statusIcon = "โ";
- if (item.status == "yes") {
- statusIcon = "๐";
- } else if (item.status == "not applicable") {
- statusIcon = "๐คทโ๏ธ";
- }
- return html`<div>
- <span>${statusIcon}</span> ${key}:${item.status}
- </div>`;
- })}
- </div>
- </sketch-tool-card>`;
- }
-}
-
-@customElement("sketch-tool-card-patch")
-export class SketchToolCardPatch extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
-
- static styles = css`
- .summary-text {
- color: #555;
- font-family: monospace;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- border-radius: 3px;
- }
- `;
-
- render() {
- const patchInput = JSON.parse(this.toolCall?.input);
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">
- ${patchInput?.path}: ${patchInput.patches.length}
- edit${patchInput.patches.length > 1 ? "s" : ""}
- </span>
- <div slot="input">
- ${patchInput.patches.map((patch) => {
- return html`Patch operation: <b>${patch.operation}</b>
- <pre>${patch.newText}</pre>`;
- })}
- </div>
- <div slot="result">
- <pre>${this.toolCall?.result_message?.tool_result}</pre>
- </div>
- </sketch-tool-card>`;
- }
-}
-
-@customElement("sketch-tool-card-think")
-export class SketchToolCardThink extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
-
- static styles = css`
- .thought-bubble {
- overflow-x: auto;
- margin-bottom: 3px;
- font-family: monospace;
- padding: 3px 5px;
- background: rgb(236, 236, 236);
- border-radius: 6px;
- user-select: text;
- cursor: text;
- -webkit-user-select: text;
- -moz-user-select: text;
- -ms-user-select: text;
- font-size: 13px;
- line-height: 1.3;
- }
- .summary-text {
- overflow: hidden;
- text-overflow: ellipsis;
- font-family: monospace;
- }
- `;
-
- render() {
- return html`
- <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">
- ${JSON.parse(this.toolCall?.input)?.thoughts?.split("\n")[0]}
- </span>
- <div slot="input" class="thought-bubble">
- <div class="markdown-content">
- ${unsafeHTML(
- renderMarkdown(JSON.parse(this.toolCall?.input)?.thoughts),
- )}
- </div>
- </div>
- </sketch-tool-card>
- `;
- }
-}
-
-@customElement("sketch-tool-card-title")
-export class SketchToolCardTitle extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
-
- static styles = css`
- .summary-text {
- font-style: italic;
- }
- pre {
- display: inline;
- font-family: monospace;
- background: rgb(236, 236, 236);
- padding: 2px 4px;
- border-radius: 2px;
- margin: 0;
- }
- `;
-
- render() {
- const inputData = JSON.parse(this.toolCall?.input || "{}");
- return html`
- <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">
- Title: "${inputData.title}" | Branch: sketch/${inputData.branch_name}
- </span>
- <div slot="input">
- <div>Set title to: <b>${inputData.title}</b></div>
- <div>Set branch to: <code>sketch/${inputData.branch_name}</code></div>
- </div>
- </sketch-tool-card>
- `;
- }
-}
-
-@customElement("sketch-tool-card-multiple-choice")
-export class SketchToolCardMultipleChoice extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
- @property() selectedOption: MultipleChoiceOption = null;
-
- static styles = css`
- .options-container {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- gap: 8px;
- margin: 10px 0;
- }
- .option {
- display: inline-flex;
- align-items: center;
- padding: 8px 12px;
- border-radius: 4px;
- background-color: #f5f5f5;
- cursor: pointer;
- transition: all 0.2s;
- border: 1px solid transparent;
- user-select: none;
- }
- .option:hover {
- background-color: #e0e0e0;
- border-color: #ccc;
- transform: translateY(-1px);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .option:active {
- transform: translateY(0);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- background-color: #d5d5d5;
- }
- .option.selected {
- background-color: #e3f2fd;
- border-color: #2196f3;
- border-width: 1px;
- border-style: solid;
- }
- .option-checkmark {
- margin-left: 6px;
- color: #2196f3;
- }
- .summary-text {
- font-style: italic;
- padding: 0.5em;
- }
- .summary-text strong {
- font-style: normal;
- color: #2196f3;
- font-weight: 600;
- }
- `;
-
- connectedCallback() {
- super.connectedCallback();
- this.updateSelectedOption();
- }
-
- updated(changedProps) {
- if (changedProps.has("toolCall")) {
- this.updateSelectedOption();
- }
- }
-
- updateSelectedOption() {
- if (this.toolCall?.result_message?.tool_result) {
- try {
- this.selectedOption = JSON.parse(
- this.toolCall.result_message.tool_result,
- ).selected;
- } catch (e) {
- console.error("Error parsing result:", e);
- }
- } else {
- this.selectedOption = null;
- }
- }
-
- async handleOptionClick(choice) {
- this.selectedOption = this.selectedOption === choice ? null : choice;
-
- const event = new CustomEvent("multiple-choice-selected", {
- detail: {
- responseText: this.selectedOption.responseText,
- toolCall: this.toolCall,
- },
- bubbles: true,
- composed: true,
- });
- this.dispatchEvent(event);
- }
-
- render() {
- let choices = [];
- let question = "";
- try {
- 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);
- }
-
- const summaryContent =
- this.selectedOption !== null
- ? html`<span class="summary-text">
- ${question}: <strong>${this.selectedOption.caption}</strong>
- </span>`
- : html`<span class="summary-text">${question}</span>`;
-
- return html`
- <div class="multiple-choice-card">
- ${summaryContent}
- <div class="options-container">
- ${choices.map((choice) => {
- const isSelected =
- this.selectedOption !== null && this.selectedOption === choice;
- return html`
- <div
- class="option ${isSelected ? "selected" : ""}"
- @click=${() => this.handleOptionClick(choice)}
- title="${choice.responseText}"
- >
- <span class="option-label">${choice.caption}</span>
- ${isSelected
- ? html`<span class="option-checkmark">โ</span>`
- : ""}
- </div>
- `;
- })}
- </div>
- </div>
- `;
- }
-}
-
-@customElement("sketch-tool-card-generic")
-export class SketchToolCardGeneric extends LitElement {
- @property() toolCall: ToolCall;
- @property() open: boolean;
-
- render() {
- return html`<sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
- <span slot="summary" class="summary-text">${this.toolCall?.input}</span>
- <div slot="input">
- Input:
- <pre>${this.toolCall?.input}</pre>
- </div>
- <div slot="result">
- Result:
- ${this.toolCall?.result_message?.tool_result
- ? html`<pre>${this.toolCall?.result_message.tool_result}</pre>`
- : ""}
- </div>
- </sketch-tool-card>`;
- }
-}
-
declare global {
interface HTMLElementTagNameMap {
"sketch-tool-card": SketchToolCard;
- "sketch-tool-card-generic": SketchToolCardGeneric;
- "sketch-tool-card-bash": SketchToolCardBash;
- "sketch-tool-card-codereview": SketchToolCardCodeReview;
- "sketch-tool-card-done": SketchToolCardDone;
- "sketch-tool-card-patch": SketchToolCardPatch;
- "sketch-tool-card-think": SketchToolCardThink;
- "sketch-tool-card-title": SketchToolCardTitle;
- "sketch-tool-card-multiple-choice": SketchToolCardMultipleChoice;
}
}