webui: split sketch-tool-card into per-tool cards
diff --git a/loop/webui/src/web-components/sketch-tool-calls.ts b/loop/webui/src/web-components/sketch-tool-calls.ts
index e7a5c74..2b4c426 100644
--- a/loop/webui/src/web-components/sketch-tool-calls.ts
+++ b/loop/webui/src/web-components/sketch-tool-calls.ts
@@ -1,172 +1,27 @@
 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";
+import "./sketch-tool-card";
 
 @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 */
+      /* Container for all tool calls */
     }
 
-    .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 */
+    /* Header for tool calls section */
     .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;
-    }
-
+    /* Card container */
     .tool-call-card {
       display: flex;
       flex-direction: column;
@@ -175,42 +30,7 @@
       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;
-    }
-
+    /* Status indicators for tool calls */
     .tool-call-status {
       margin-right: 4px;
       text-align: center;
@@ -222,70 +42,6 @@
       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);
@@ -294,344 +50,73 @@
         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
-    >`;
-
+  cardForToolCall(toolCall: ToolCall, open: boolean) {
     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>`;
+        return html`<sketch-tool-card-bash
+          .open=${open}
+          .toolCall=${toolCall}
+        ></sketch-tool-card-bash>`;
       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>`;
+        return html`<sketch-tool-card-codereview
+          .open=${open}
+          .toolCall=${toolCall}
+        ></sketch-tool-card-codereview>`;
       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}
-        `;
+        return html`<sketch-tool-card-done
+          .open=${open}
+          .toolCall=${toolCall}
+        ></sketch-tool-card-done>`;
+      case "patch":
+        return html`<sketch-tool-card-patch
+          .open=${open}
+          .toolCall=${toolCall}
+        ></sketch-tool-card-patch>`;
+      case "think":
+        return html`<sketch-tool-card-think
+          .open=${open}
+          .toolCall=${toolCall}
+        ></sketch-tool-card-think>`;
+      case "title":
+        return html`<sketch-tool-card-title
+          .open=${open}
+          .toolCall=${toolCall}
+        ></sketch-tool-card-title>`;
     }
+    return html`<sketch-tool-card-generic
+      .open=${open}
+      .toolCall=${toolCall}
+    ></sketch-tool-card-generic>`;
   }
+
   render() {
-    return html` <div class="tool-calls-container">
+    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)}
+        ${this.toolCalls?.map((toolCall, idx) => {
+          let lastCall = false;
+          if (idx == this.toolCalls?.length - 1) {
+            lastCall = true;
+          }
+          return html`<div
+            id="${toolCall.tool_call_id}"
+            class="tool-call-card ${toolCall.name}"
+          >
+            ${this.cardForToolCall(toolCall, lastCall)}
           </div>`;
         })}
       </div>