webui: add TODO item comment functionality similar to diff comments

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s1040817099be5124k
diff --git a/webui/src/web-components/sketch-chat-input.ts b/webui/src/web-components/sketch-chat-input.ts
index 355c9fc..9667155 100644
--- a/webui/src/web-components/sketch-chat-input.ts
+++ b/webui/src/web-components/sketch-chat-input.ts
@@ -126,6 +126,7 @@
   constructor() {
     super();
     this._handleDiffComment = this._handleDiffComment.bind(this);
+    this._handleTodoComment = this._handleTodoComment.bind(this);
     this._handleDragOver = this._handleDragOver.bind(this);
     this._handleDragEnter = this._handleDragEnter.bind(this);
     this._handleDragLeave = this._handleDragLeave.bind(this);
@@ -135,6 +136,7 @@
   connectedCallback() {
     super.connectedCallback();
     window.addEventListener("diff-comment", this._handleDiffComment);
+    window.addEventListener("todo-comment", this._handleTodoComment);
   }
 
   // Utility function to handle file uploads (used by both paste and drop handlers)
@@ -271,10 +273,22 @@
     requestAnimationFrame(() => this.adjustChatSpacing());
   }
 
+  private _handleTodoComment(event: CustomEvent) {
+    const { comment } = event.detail;
+    if (!comment) return;
+
+    if (this.content != "") {
+      this.content += "\n\n";
+    }
+    this.content += comment;
+    requestAnimationFrame(() => this.adjustChatSpacing());
+  }
+
   // See https://lit.dev/docs/components/lifecycle/
   disconnectedCallback() {
     super.disconnectedCallback();
     window.removeEventListener("diff-comment", this._handleDiffComment);
+    window.removeEventListener("todo-comment", this._handleTodoComment);
 
     // Clean up drag and drop event listeners
     const container = this.renderRoot.querySelector(".chat-container");
diff --git a/webui/src/web-components/sketch-todo-panel.ts b/webui/src/web-components/sketch-todo-panel.ts
index 540397b..7b58b18 100644
--- a/webui/src/web-components/sketch-todo-panel.ts
+++ b/webui/src/web-components/sketch-todo-panel.ts
@@ -17,6 +17,15 @@
   @state()
   private error: string = "";
 
+  @state()
+  private showCommentBox: boolean = false;
+
+  @state()
+  private commentingItem: TodoItem | null = null;
+
+  @state()
+  private commentText: string = "";
+
   static styles = css`
     :host {
       display: flex;
@@ -93,6 +102,7 @@
       background-color: #fff;
       border: 1px solid #e0e0e0;
       gap: 8px;
+      min-height: 24px; /* Ensure consistent height */
     }
 
     .todo-item.queued {
@@ -125,6 +135,177 @@
       word-wrap: break-word;
     }
 
+    .todo-item-content {
+      display: flex;
+      align-items: flex-start;
+      justify-content: space-between;
+      width: 100%;
+      min-height: 20px; /* Ensure consistent height */
+    }
+
+    .todo-text-section {
+      flex: 1;
+      min-width: 0;
+      padding-right: 8px; /* Space between text and button column */
+    }
+
+    .todo-actions-column {
+      flex-shrink: 0;
+      display: flex;
+      align-items: flex-start;
+      width: 24px; /* Fixed width for button column */
+      justify-content: center;
+    }
+
+    .comment-button {
+      background: none;
+      border: none;
+      cursor: pointer;
+      font-size: 14px;
+      padding: 2px;
+      color: #666;
+      opacity: 0.7;
+      transition: opacity 0.2s ease;
+      width: 20px;
+      height: 20px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .comment-button:hover {
+      opacity: 1;
+      background-color: rgba(0, 0, 0, 0.05);
+      border-radius: 3px;
+    }
+
+    /* Comment box overlay */
+    .comment-overlay {
+      position: fixed;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background-color: rgba(0, 0, 0, 0.3);
+      z-index: 10000;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      animation: fadeIn 0.2s ease-in-out;
+    }
+
+    .comment-box {
+      background-color: white;
+      border: 1px solid #ddd;
+      border-radius: 6px;
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+      padding: 16px;
+      width: 400px;
+      max-width: 90vw;
+      max-height: 80vh;
+      overflow-y: auto;
+    }
+
+    .comment-box-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 12px;
+    }
+
+    .comment-box-header h3 {
+      margin: 0;
+      font-size: 14px;
+      font-weight: 500;
+    }
+
+    .close-button {
+      background: none;
+      border: none;
+      cursor: pointer;
+      font-size: 18px;
+      color: #666;
+      padding: 2px 6px;
+    }
+
+    .close-button:hover {
+      color: #333;
+    }
+
+    .todo-context {
+      background-color: #f8f9fa;
+      border: 1px solid #e9ecef;
+      border-radius: 4px;
+      padding: 8px;
+      margin-bottom: 12px;
+      font-size: 12px;
+    }
+
+    .todo-context-status {
+      font-weight: 500;
+      color: #666;
+      margin-bottom: 4px;
+    }
+
+    .todo-context-task {
+      color: #333;
+    }
+
+    .comment-textarea {
+      width: 100%;
+      min-height: 80px;
+      padding: 8px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      resize: vertical;
+      font-family: inherit;
+      font-size: 12px;
+      margin-bottom: 12px;
+      box-sizing: border-box;
+    }
+
+    .comment-actions {
+      display: flex;
+      justify-content: flex-end;
+      gap: 8px;
+    }
+
+    .comment-actions button {
+      padding: 6px 12px;
+      border-radius: 4px;
+      cursor: pointer;
+      font-size: 12px;
+    }
+
+    .cancel-button {
+      background-color: transparent;
+      border: 1px solid #ddd;
+      color: #666;
+    }
+
+    .cancel-button:hover {
+      background-color: #f5f5f5;
+    }
+
+    .submit-button {
+      background-color: #4285f4;
+      color: white;
+      border: none;
+    }
+
+    .submit-button:hover {
+      background-color: #3367d6;
+    }
+
+    @keyframes fadeIn {
+      from {
+        opacity: 0;
+      }
+      to {
+        opacity: 1;
+      }
+    }
+
     .todo-header-text {
       display: flex;
       align-items: center;
@@ -186,11 +367,27 @@
         completed: "✅",
       }[item.status] || "?";
 
+    // Only show comment button for non-completed items
+    const showCommentButton = item.status !== "completed";
+
     return html`
       <div class="todo-item ${item.status}">
         <div class="todo-status-icon">${statusIcon}</div>
-        <div class="todo-main">
-          <div class="todo-content-text">${item.task}</div>
+        <div class="todo-item-content">
+          <div class="todo-text-section">
+            <div class="todo-content-text">${item.task}</div>
+          </div>
+          <div class="todo-actions-column">
+            ${showCommentButton ? html`
+              <button 
+                class="comment-button" 
+                @click="${() => this.openCommentBox(item)}"
+                title="Add comment about this TODO item"
+              >
+                💬
+              </button>
+            ` : ""}
+          </div>
         </div>
       </div>
     `;
@@ -260,7 +457,113 @@
       `;
     }
 
-    return html` ${contentElement} `;
+    return html`
+      ${contentElement}
+      
+      ${this.showCommentBox ? this.renderCommentBox() : ""}
+    `;
+  }
+
+  private renderCommentBox() {
+    if (!this.commentingItem) return "";
+
+    const statusText = {
+      queued: "Queued",
+      "in-progress": "In Progress", 
+      completed: "Completed"
+    }[this.commentingItem.status] || this.commentingItem.status;
+
+    return html`
+      <div class="comment-overlay" @click="${this.handleOverlayClick}">
+        <div class="comment-box" @click="${this.stopPropagation}">
+          <div class="comment-box-header">
+            <h3>Comment on TODO Item</h3>
+            <button class="close-button" @click="${this.closeCommentBox}">
+              ×
+            </button>
+          </div>
+          
+          <div class="todo-context">
+            <div class="todo-context-status">Status: ${statusText}</div>
+            <div class="todo-context-task">${this.commentingItem.task}</div>
+          </div>
+          
+          <textarea
+            class="comment-textarea"
+            placeholder="Type your comment about this TODO item..."
+            .value="${this.commentText}"
+            @input="${this.handleCommentInput}"
+          ></textarea>
+          
+          <div class="comment-actions">
+            <button class="cancel-button" @click="${this.closeCommentBox}">
+              Cancel
+            </button>
+            <button class="submit-button" @click="${this.submitComment}">
+              Add Comment
+            </button>
+          </div>
+        </div>
+      </div>
+    `;
+  }
+
+  private openCommentBox(item: TodoItem) {
+    this.commentingItem = item;
+    this.commentText = "";
+    this.showCommentBox = true;
+  }
+
+  private closeCommentBox() {
+    this.showCommentBox = false;
+    this.commentingItem = null;
+    this.commentText = "";
+  }
+
+  private handleOverlayClick(e: Event) {
+    // Close when clicking outside the comment box
+    this.closeCommentBox();
+  }
+
+  private stopPropagation(e: Event) {
+    // Prevent clicks inside the comment box from closing it
+    e.stopPropagation();
+  }
+
+  private handleCommentInput(e: Event) {
+    const target = e.target as HTMLTextAreaElement;
+    this.commentText = target.value;
+  }
+
+  private submitComment() {
+    if (!this.commentingItem || !this.commentText.trim()) {
+      return;
+    }
+
+    // Format the comment similar to diff comments
+    const statusText = {
+      queued: "Queued",
+      "in-progress": "In Progress", 
+      completed: "Completed"
+    }[this.commentingItem.status] || this.commentingItem.status;
+
+    const formattedComment = `\`\`\`
+TODO Item (${statusText}): ${this.commentingItem.task}
+\`\`\`
+
+${this.commentText}`;
+
+    // Dispatch a custom event similar to diff comments
+    const event = new CustomEvent("todo-comment", {
+      detail: { comment: formattedComment },
+      bubbles: true,
+      composed: true,
+    });
+
+    this.dispatchEvent(event);
+    
+    // Close the comment box
+    this.closeCommentBox();
   }
 }