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();
}
}