blob: e7a5c749199d479b7c7c221e677c3da699c9f7c7 [file] [log] [blame]
import { css, html, LitElement } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { repeat } from "lit/directives/repeat.js";
import { customElement, property } from "lit/decorators.js";
import { State, ToolCall } from "../types";
import { marked, MarkedOptions } from "marked";
@customElement("sketch-tool-calls")
export class SketchToolCalls extends LitElement {
@property()
toolCalls: ToolCall[] = [];
// See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
// Note that these styles only apply to the scope of this web component's
// shadow DOM node, so they won't leak out or collide with CSS declared in
// other components or the containing web page (...unless you want it to do that).
static styles = css`
/* Tool calls container styles */
.tool-calls-container {
/* Removed dotted border */
}
.tool-calls-toggle {
cursor: pointer;
background-color: #f0f0f0;
padding: 5px 10px;
border: none;
border-radius: 4px;
text-align: left;
font-size: 12px;
margin-top: 5px;
color: #555;
font-weight: 500;
}
.tool-calls-toggle:hover {
background-color: #e0e0e0;
}
.tool-calls-details {
margin-top: 10px;
transition: max-height 0.3s ease;
}
.tool-calls-details.collapsed {
max-height: 0;
overflow: hidden;
margin-top: 0;
}
.tool-call {
background: #f9f9f9;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
border-left: 3px solid #4caf50;
}
.tool-call-header {
margin-bottom: 8px;
font-size: 14px;
padding: 2px 0;
}
/* Compact tool display styles */
.tool-compact-line {
font-family: monospace;
font-size: 12px;
line-height: 1.4;
padding: 4px 6px;
background: #f8f8f8;
border-radius: 3px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: flex;
align-items: center;
}
.tool-result-inline {
font-family: monospace;
color: #0066bb;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 400px;
display: inline-block;
vertical-align: middle;
}
.copy-inline-button {
font-size: 10px;
padding: 2px 4px;
margin-left: 8px;
background: #eee;
border: none;
border-radius: 3px;
cursor: pointer;
opacity: 0.7;
}
.copy-inline-button:hover {
opacity: 1;
background: #ddd;
}
.tool-input.compact,
.tool-result.compact {
margin: 2px 0;
padding: 4px;
font-size: 12px;
}
/* Removed old compact container CSS */
/* Ultra-compact tool call box styles */
.tool-calls-header {
/* Empty header - just small spacing */
}
.tool-call-boxes-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.tool-call-wrapper {
display: flex;
flex-direction: column;
margin-bottom: 4px;
}
.tool-call-box {
display: inline-flex;
align-items: center;
background: #f0f0f0;
border-radius: 4px;
padding: 3px 8px;
font-size: 12px;
cursor: pointer;
max-width: 320px;
position: relative;
border: 1px solid #ddd;
transition: background-color 0.2s;
}
.tool-call-box:hover {
background-color: #e8e8e8;
}
.tool-call-box.expanded {
background-color: #e0e0e0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom: 1px solid #ccc;
}
.tool-call-input {
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: monospace;
font-size: 11px;
}
.tool-call-card {
display: flex;
flex-direction: column;
background-color: white;
overflow: hidden;
cursor: pointer;
}
/* Compact view (default) */
.tool-call-compact-view {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9em;
white-space: nowrap;
overflow: visible; /* Don't hide overflow, we'll handle text truncation per element */
position: relative; /* For positioning the expand icon */
}
/* Expanded view (hidden by default) */
.tool-call-card.collapsed .tool-call-expanded-view {
display: none;
}
.tool-call-expanded-view {
display: flex;
flex-direction: column;
border-top: 1px solid #eee;
}
.tool-call-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background-color: #f0f0f0;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.tool-call-name {
color: gray;
}
.tool-call-status {
margin-right: 4px;
text-align: center;
}
.tool-call-status.spinner {
animation: spin 1s infinite linear;
display: inline-block;
width: 1em;
}
.tool-call-time {
margin-left: 8px;
font-size: 0.85em;
color: #666;
font-weight: normal;
}
.tool-call-input-preview {
color: #555;
font-family: var(--monospace-font);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30%;
background-color: rgba(240, 240, 240, 0.5);
padding: 2px 5px;
border-radius: 3px;
font-size: 0.9em;
}
.tool-call-result-preview {
color: #28a745;
font-family: var(--monospace-font);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 40%;
background-color: rgba(240, 248, 240, 0.5);
padding: 2px 5px;
border-radius: 3px;
font-size: 0.9em;
}
.tool-call-expand-icon {
position: absolute;
right: 10px;
font-size: 0.8em;
color: #888;
}
.tool-call-input {
padding: 6px 10px;
border-bottom: 1px solid #eee;
font-family: var(--monospace-font);
font-size: 0.9em;
white-space: pre-wrap;
word-break: break-all;
background-color: #f5f5f5;
}
.tool-call-result {
padding: 6px 10px;
font-family: var(--monospace-font);
font-size: 0.9em;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
.tool-call-result pre {
margin: 0;
white-space: pre-wrap;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Standalone tool messages (legacy/disconnected) */
.tool-details.standalone .tool-header {
border-radius: 4px;
background-color: #fff3cd;
border-color: #ffeeba;
}
.tool-details.standalone .tool-warning {
margin-left: 10px;
font-size: 0.85em;
color: #856404;
font-style: italic;
}
/* Tool call expanded view with sections */
.tool-call-section {
border-bottom: 1px solid #eee;
}
.tool-call-section:last-child {
border-bottom: none;
}
.tool-call-section-label {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background-color: #f5f5f5;
font-weight: bold;
font-size: 0.9em;
}
.tool-call-section-content {
padding: 0;
}
.tool-call-copy-btn {
background-color: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
padding: 2px 8px;
font-size: 0.8em;
cursor: pointer;
transition: background-color 0.2s;
}
.tool-call-copy-btn:hover {
background-color: #e0e0e0;
}
/* Override for tool call input in expanded view */
.tool-call-section-content .tool-call-input {
margin: 0;
padding: 8px 10px;
border: none;
background-color: #fff;
max-height: 300px;
overflow-y: auto;
}
.tool-call-card .tool-call-input-preview,
.tool-call-card .tool-call-result-preview {
font-family: monospace;
background: black;
padding: 1em;
}
.tool-call-input-preview {
color: white;
}
.tool-call-result-preview {
color: gray;
}
.tool-call-card.title {
font-style: italic;
}
.cancel-button {
background: rgb(76, 175, 80);
color: white;
border: none;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin: 5px;
}
.cancel-button:hover {
background: rgb(200, 35, 51) !important;
}
.thought-bubble {
position: relative;
background-color: #eee;
border-radius: 8px;
padding: 0.5em;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
margin-left: 24px;
margin-top: 24px;
margin-bottom: 12px;
max-width: 30%;
white-space: pre;
}
.thought-bubble .preview {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.thought-bubble:before {
content: "";
position: absolute;
top: -8px;
left: -8px;
width: 15px;
height: 15px;
background-color: #eee;
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.thought-bubble:after {
content: "";
position: absolute;
top: -16px;
left: -16px;
width: 8px;
height: 8px;
background-color: #eee;
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.patch-input-preview {
color: #555;
font-family: monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30%;
background-color: rgba(240, 240, 240, 0.5);
padding: 2px 5px;
border-radius: 3px;
font-size: 0.9em;
}
.codereview-OK {
color: green;
}
`;
constructor() {
super();
}
// See https://lit.dev/docs/components/lifecycle/
connectedCallback() {
super.connectedCallback();
}
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
}
renderMarkdown(markdownContent: string): string {
try {
// Set markdown options for proper code block highlighting and safety
const markedOptions: MarkedOptions = {
gfm: true, // GitHub Flavored Markdown
breaks: true, // Convert newlines to <br>
async: false,
// DOMPurify is recommended for production, but not included in this implementation
};
return marked.parse(markdownContent, markedOptions) as string;
} catch (error) {
console.error("Error rendering markdown:", error);
// Fallback to plain text if markdown parsing fails
return markdownContent;
}
}
_cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
console.log("cancelToolCall", tool_call_id, button);
button.innerText = "Cancelling";
button.disabled = true;
try {
const response = await fetch("cancel", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
tool_call_id: tool_call_id,
reason: "user requested cancellation",
}),
});
if (response.ok) {
console.log("cancel", tool_call_id, response);
button.parentElement.removeChild(button);
} else {
button.innerText = "Cancel";
console.log(`error trying to cancel ${tool_call_id}: `, response);
}
} catch (e) {
console.error("cancel", tool_call_id, e);
}
};
toolCard(toolCall: ToolCall) {
const toolCallStatus = toolCall.result_message
? toolCall.result_message.tool_error
? "❌"
: ""
: "⏳";
const cancelButton = toolCall.result_message
? ""
: html`<button
class="cancel-button"
title="Cancel this operation"
@click=${(e: Event) => {
e.stopPropagation();
const button = e.target as HTMLButtonElement;
this._cancelToolCall(toolCall.tool_call_id, button);
}}
>
Cancel
</button>`;
const status = html`<span
class="tool-call-status ${toolCall.result_message ? "" : "spinner"}"
>${toolCallStatus}</span
>`;
switch (toolCall.name) {
case "title":
const titleInput = JSON.parse(toolCall.input);
return html` <div class="tool-call-compact-view">
I've set the title of this sketch to <b>"${titleInput.title}"</b>
</div>`;
case "bash":
const bashInput = JSON.parse(toolCall.input);
return html` <div class="tool-call-compact-view">
${status}
<span class="tool-call-name">${toolCall.name}</span>
<pre class="tool-call-input-preview">${bashInput.command}</pre>
${toolCall.result_message
? html` ${toolCall.result_message.tool_result
? html` <pre class="tool-call-result-preview">
${toolCall.result_message.tool_result}</pre
>`
: ""}`
: cancelButton}
</div>`;
case "codereview":
return html` <div class="tool-call-compact-view">
${status}
<span class="tool-call-name">${toolCall.name}</span>
${cancelButton}
<code
class="codereview-preview codereview-${toolCall.result_message
?.tool_result}"
>${toolCall.result_message?.tool_result == "OK"
? "✔️"
: "⛔ " + toolCall.result_message?.tool_result}</code
>
</div>`;
case "think":
const thinkInput = JSON.parse(toolCall.input);
return html` <div class="tool-call-compact-view">
${status}
<span class="tool-call-name">${toolCall.name}</span>
<div class="thought-bubble">
<div class="preview">${thinkInput.thoughts}</div>
</div>
${cancelButton}
</div>`;
case "patch":
const patchInput = JSON.parse(toolCall.input);
return html` <div class="tool-call-compact-view">
${status}
<span class="tool-call-name">${toolCall.name}</span>
<div class="patch-input-preview">
<span class="patch-path">${patchInput.path}</span>:
${patchInput.patches.length}
edit${patchInput.patches.length > 1 ? "s" : ""}
</div>
${cancelButton}
</div>`;
case "done":
const doneInput = JSON.parse(toolCall.input);
return html` <div class="tool-call-compact-view">
${status}
<span class="tool-call-name">${toolCall.name}</span>
<div class="done-input-preview">
${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>
${cancelButton}
</div>`;
default: // Generic tool card:
return html`
<div class="tool-call-compact-view">
${status}
<span class="tool-call-name">${toolCall.name}</span>
<code class="tool-call-input-preview">${toolCall.input}</code>
${cancelButton}
<code class="tool-call-result-preview"
>${toolCall.result_message?.tool_result}</code
>
</div>
${toolCall.result_message?.tool_result}
`;
}
}
render() {
return html` <div class="tool-calls-container">
<div class="tool-calls-header"></div>
<div class="tool-call-cards-container">
${this.toolCalls?.map((toolCall) => {
return html`<div class="tool-call-card ${toolCall.name}">
${this.toolCard(toolCall)}
</div>`;
})}
</div>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"sketch-tool-calls": SketchToolCalls;
}
}