loop: add todo checklist
This should improve Sketch's executive function and user communication.
diff --git a/webui/src/web-components/sketch-todo-panel.ts b/webui/src/web-components/sketch-todo-panel.ts
new file mode 100644
index 0000000..d8e34f0
--- /dev/null
+++ b/webui/src/web-components/sketch-todo-panel.ts
@@ -0,0 +1,259 @@
+import { css, html, LitElement } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { unsafeHTML } from "lit/directives/unsafe-html.js";
+import { TodoList, TodoItem } from "../types.js";
+
+@customElement("sketch-todo-panel")
+export class SketchTodoPanel extends LitElement {
+ @property()
+ visible: boolean = false;
+
+ @state()
+ private todoList: TodoList | null = null;
+
+ @state()
+ private loading: boolean = false;
+
+ @state()
+ private error: string = "";
+
+ static styles = css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background-color: transparent; /* Let parent handle background */
+ overflow: hidden; /* Ensure proper clipping */
+ }
+
+ .todo-header {
+ padding: 8px 12px;
+ border-bottom: 1px solid #e0e0e0;
+ background-color: #f5f5f5;
+ font-weight: 600;
+ font-size: 13px;
+ color: #333;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .todo-icon {
+ width: 14px;
+ height: 14px;
+ color: #666;
+ }
+
+ .todo-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+ padding-bottom: 20px; /* Extra bottom padding for better scrolling */
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ font-size: 12px;
+ line-height: 1.4;
+ /* Ensure scrollbar is always accessible */
+ min-height: 0;
+ }
+
+ .todo-content.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #666;
+ }
+
+ .todo-content.error {
+ color: #d32f2f;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .todo-content.empty {
+ color: #999;
+ font-style: italic;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* Todo item styling */
+ .todo-item {
+ display: flex;
+ align-items: flex-start;
+ padding: 8px;
+ margin-bottom: 6px;
+ border-radius: 4px;
+ background-color: #fff;
+ border: 1px solid #e0e0e0;
+ gap: 8px;
+ }
+
+ .todo-item.queued {
+ border-left: 3px solid #e0e0e0;
+ }
+
+ .todo-item.in-progress {
+ border-left: 3px solid #e0e0e0;
+ }
+
+ .todo-item.completed {
+ border-left: 3px solid #e0e0e0;
+ }
+
+
+
+ .todo-status-icon {
+ font-size: 14px;
+ margin-top: 1px;
+ flex-shrink: 0;
+ }
+
+
+
+ .todo-main {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .todo-content-text {
+ font-size: 12px;
+ line-height: 1.3;
+ color: #333;
+ word-wrap: break-word;
+ }
+
+
+
+ .todo-header-text {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .todo-count {
+ background-color: #e0e0e0;
+ color: #666;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-size: 10px;
+ font-weight: normal;
+ }
+
+ /* Loading spinner */
+ .spinner {
+ width: 20px;
+ height: 20px;
+ border: 2px solid #f3f3f3;
+ border-top: 2px solid #3498db;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-right: 8px;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+ `;
+
+ updateTodoContent(content: string) {
+ try {
+ if (!content.trim()) {
+ this.todoList = null;
+ } else {
+ this.todoList = JSON.parse(content) as TodoList;
+ }
+ this.loading = false;
+ this.error = "";
+ } catch (error) {
+ console.error("Failed to parse todo content:", error);
+ this.error = "Failed to parse todo data";
+ this.todoList = null;
+ this.loading = false;
+ }
+ }
+
+
+
+ private renderTodoItem(item: TodoItem) {
+ const statusIcon = {
+ queued: '⚪',
+ 'in-progress': '🦉',
+ completed: '✅'
+ }[item.status] || '?';
+
+ 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>
+ </div>
+ `;
+ }
+
+ render() {
+ if (!this.visible) {
+ return html``;
+ }
+
+ const todoIcon = html`
+ <svg class="todo-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M9 11l3 3L22 4"></path>
+ <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+ </svg>
+ `;
+
+ let contentElement;
+ if (this.loading) {
+ contentElement = html`
+ <div class="todo-content loading">
+ <div class="spinner"></div>
+ Loading todos...
+ </div>
+ `;
+ } else if (this.error) {
+ contentElement = html`
+ <div class="todo-content error">
+ Error: ${this.error}
+ </div>
+ `;
+ } else if (!this.todoList || !this.todoList.items || this.todoList.items.length === 0) {
+ contentElement = html`
+ <div class="todo-content empty">
+ No todos available
+ </div>
+ `;
+ } else {
+ const totalCount = this.todoList.items.length;
+ const completedCount = this.todoList.items.filter(item => item.status === 'completed').length;
+ const inProgressCount = this.todoList.items.filter(item => item.status === 'in-progress').length;
+
+ contentElement = html`
+ <div class="todo-header">
+ <div class="todo-header-text">
+ ${todoIcon}
+ <span>Sketching...</span>
+ <span class="todo-count">${completedCount}/${totalCount}</span>
+ </div>
+ </div>
+ <div class="todo-content">
+ ${this.todoList.items.map(item => this.renderTodoItem(item))}
+ </div>
+ `;
+ }
+
+ return html`
+ ${contentElement}
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-todo-panel": SketchTodoPanel;
+ }
+}
\ No newline at end of file