blob: 8849159469f5c9c42da018401e96a05aab42ddfd [file] [log] [blame]
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001import { css, html, LitElement } from "lit";
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00002import { customElement, property, state } from "lit/decorators.js";
Pokey Ruleef58e062025-05-07 13:32:58 +01003import { ToolCall } from "../types";
Pokey Rule5e8aead2025-05-06 16:21:57 +01004
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07005@customElement("sketch-tool-card")
6export class SketchToolCard extends LitElement {
Pokey Rule5e8aead2025-05-06 16:21:57 +01007 @property() toolCall: ToolCall;
8 @property() open: boolean;
9 @state() detailsVisible: boolean = false;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000010
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070011 static styles = css`
12 .tool-call {
13 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000014 flex-direction: column;
15 width: 100%;
16 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000017 .tool-row {
18 display: flex;
19 width: 100%;
20 box-sizing: border-box;
21 padding: 6px 8px 6px 12px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070022 align-items: center;
Pokey Rule5e8aead2025-05-06 16:21:57 +010023 gap: 8px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000024 cursor: pointer;
25 border-radius: 4px;
26 position: relative;
Pokey Rule5e8aead2025-05-06 16:21:57 +010027 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000028 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000029 .tool-row:hover {
30 background-color: rgba(0, 0, 0, 0.02);
31 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000032 .tool-name {
33 font-family: monospace;
34 font-weight: 500;
35 color: #444;
36 background-color: rgba(0, 0, 0, 0.05);
37 border-radius: 3px;
38 padding: 2px 6px;
39 flex-shrink: 0;
40 min-width: 45px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000041 font-size: 12px;
42 text-align: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070043 white-space: nowrap;
44 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000045 .tool-success {
46 color: #5cb85c;
47 font-size: 14px;
48 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000049 .tool-error {
Josh Bleecher Snydere750ec92025-05-05 23:01:57 +000050 color: #6c757d;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000051 font-size: 14px;
52 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000053 .tool-pending {
54 color: #f0ad4e;
55 font-size: 14px;
56 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000057 .summary-text {
58 white-space: nowrap;
59 text-overflow: ellipsis;
60 overflow: hidden;
61 flex-grow: 1;
62 flex-shrink: 1;
63 color: #444;
64 font-family: monospace;
65 font-size: 12px;
66 padding: 0 4px;
67 min-width: 50px;
Pokey Rule5e8aead2025-05-06 16:21:57 +010068 max-width: calc(100% - 250px);
69 display: inline-block;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000070 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000071 .tool-status {
72 display: flex;
73 align-items: center;
74 gap: 12px;
75 margin-left: auto;
76 flex-shrink: 0;
Pokey Rule5e8aead2025-05-06 16:21:57 +010077 min-width: 120px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000078 justify-content: flex-end;
79 padding-right: 8px;
80 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070081 .tool-call-status {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000082 display: flex;
83 align-items: center;
84 justify-content: center;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070085 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070086 .tool-call-status.spinner {
87 animation: spin 1s infinite linear;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070088 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -070089 @keyframes spin {
90 0% {
91 transform: rotate(0deg);
92 }
93 100% {
94 transform: rotate(360deg);
95 }
96 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000097 .elapsed {
98 font-size: 11px;
99 color: #777;
100 white-space: nowrap;
101 min-width: 40px;
102 text-align: right;
103 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000104 .tool-details {
105 padding: 8px;
106 background-color: rgba(0, 0, 0, 0.02);
107 margin-top: 1px;
108 border-top: 1px solid rgba(0, 0, 0, 0.05);
109 display: none;
110 font-family: monospace;
111 font-size: 12px;
112 color: #333;
113 border-radius: 0 0 4px 4px;
114 max-width: 100%;
115 width: 100%;
116 box-sizing: border-box;
Pokey Rule5e8aead2025-05-06 16:21:57 +0100117 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000118 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000119 .tool-details.visible {
120 display: block;
121 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700122 .cancel-button {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700123 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000124 color: white;
125 background-color: #d9534f;
126 border: none;
127 border-radius: 3px;
128 font-size: 11px;
129 padding: 2px 6px;
130 white-space: nowrap;
131 min-width: 50px;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700132 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700133 .cancel-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000134 background-color: #c9302c;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700135 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000136 .cancel-button[disabled] {
137 background-color: #999;
138 cursor: not-allowed;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700139 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700140 `;
141
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700142 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700143 button.innerText = "Cancelling";
144 button.disabled = true;
145 try {
146 const response = await fetch("cancel", {
147 method: "POST",
Pokey Rule5e8aead2025-05-06 16:21:57 +0100148 headers: { "Content-Type": "application/json" },
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700149 body: JSON.stringify({
150 tool_call_id: tool_call_id,
151 reason: "user requested cancellation",
152 }),
153 });
154 if (response.ok) {
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700155 button.parentElement.removeChild(button);
156 } else {
157 button.innerText = "Cancel";
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700158 }
159 } catch (e) {
160 console.error("cancel", tool_call_id, e);
161 }
162 };
163
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000164 _toggleDetails(e: Event) {
165 e.stopPropagation();
166 this.detailsVisible = !this.detailsVisible;
167 }
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700168
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000169 render() {
Pokey Rule5e8aead2025-05-06 16:21:57 +0100170 // Status indicator based on result
171 let statusIcon = html`<span class="tool-call-status spinner tool-pending"
172 >⏳</span
173 >`;
174 if (this.toolCall?.result_message) {
175 statusIcon = this.toolCall?.result_message.tool_error
176 ? html`<span class="tool-call-status tool-error">🔔</span>`
177 : html`<span class="tool-call-status tool-success">✓</span>`;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000178 }
179
180 // Cancel button for pending operations
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700181 const cancelButton = this.toolCall?.result_message
182 ? ""
183 : html`<button
184 class="cancel-button"
185 title="Cancel this operation"
186 @click=${(e: Event) => {
187 e.stopPropagation();
Pokey Rule5e8aead2025-05-06 16:21:57 +0100188 this._cancelToolCall(
189 this.toolCall?.tool_call_id,
190 e.target as HTMLButtonElement,
191 );
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700192 }}
193 >
194 Cancel
195 </button>`;
196
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000197 // Elapsed time display
198 const elapsed = this.toolCall?.result_message?.elapsed
Sean McCullough2deac842025-04-21 18:17:57 -0700199 ? html`<span class="elapsed"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000200 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
Sean McCullough2deac842025-04-21 18:17:57 -0700201 >`
Pokey Rule5e8aead2025-05-06 16:21:57 +0100202 : html`<span class="elapsed"></span>`;
Sean McCullough2deac842025-04-21 18:17:57 -0700203
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000204 // Initialize details visibility based on open property
205 if (this.open && !this.detailsVisible) {
206 this.detailsVisible = true;
207 }
208
209 return html`<div class="tool-call">
210 <div class="tool-row" @click=${this._toggleDetails}>
211 <span class="tool-name">${this.toolCall?.name}</span>
212 <span class="summary-text"><slot name="summary"></slot></span>
213 <div class="tool-status">${statusIcon} ${elapsed} ${cancelButton}</div>
214 </div>
215 <div class="tool-details ${this.detailsVisible ? "visible" : ""}">
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700216 <slot name="input"></slot>
217 <slot name="result"></slot>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000218 </div>
219 </div>`;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700220 }
221}
222
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700223declare global {
224 interface HTMLElementTagNameMap {
225 "sketch-tool-card": SketchToolCard;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700226 }
227}