feat: enhance UI with commit pulsing animation and improved copy icon

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s80fdaac660ea645ek
diff --git a/loop/agent.go b/loop/agent.go
index cb22b84..909700e 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -105,7 +105,7 @@
 	SessionID() string
 
 	// DetectGitChanges checks for new git commits and pushes them if found
-	DetectGitChanges(ctx context.Context)
+	DetectGitChanges(ctx context.Context) error
 
 	// OutstandingLLMCallCount returns the number of outstanding LLM calls.
 	OutstandingLLMCallCount() int
@@ -1421,13 +1421,14 @@
 }
 
 // DetectGitChanges checks for new git commits and pushes them if found
-func (a *Agent) DetectGitChanges(ctx context.Context) {
+func (a *Agent) DetectGitChanges(ctx context.Context) error {
 	// Check for git commits
 	_, err := a.handleGitCommits(ctx)
 	if err != nil {
-		// Just log the error, don't stop execution
 		slog.WarnContext(ctx, "Failed to check for new git commits", "error", err)
+		return fmt.Errorf("failed to check for new git commits: %w", err)
 	}
+	return nil
 }
 
 // processGitChanges checks for new git commits, runs autoformatters if needed, and returns any messages generated
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 94cbb78..ede1644 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -1381,7 +1381,10 @@
 	}
 
 	// Detect git changes to push and notify user
-	s.agent.DetectGitChanges(r.Context())
+	if err = s.agent.DetectGitChanges(r.Context()); err != nil {
+		http.Error(w, fmt.Sprintf("Error detecting git changes: %v", err), http.StatusInternalServerError)
+		return
+	}
 
 	// Return simple success response
 	w.WriteHeader(http.StatusOK)
diff --git a/loop/server/loophttp_test.go b/loop/server/loophttp_test.go
index e3d144b..949a95a 100644
--- a/loop/server/loophttp_test.go
+++ b/loop/server/loophttp_test.go
@@ -242,7 +242,7 @@
 func (m *mockAgent) SuggestReprompt(ctx context.Context) (string, error) { return "", nil }
 func (m *mockAgent) IsInContainer() bool                                 { return false }
 func (m *mockAgent) FirstMessageIndex() int                              { return 0 }
-func (m *mockAgent) DetectGitChanges(ctx context.Context)                {} // TestSSEStream tests the SSE stream endpoint
+func (m *mockAgent) DetectGitChanges(ctx context.Context) error          { return nil } // TestSSEStream tests the SSE stream endpoint
 func TestSSEStream(t *testing.T) {
 	// Create a mock agent with initial messages
 	mockAgent := &mockAgent{
diff --git a/webui/src/web-components/sketch-container-status.ts b/webui/src/web-components/sketch-container-status.ts
index 0d2b4f6..c6b862d 100644
--- a/webui/src/web-components/sketch-container-status.ts
+++ b/webui/src/web-components/sketch-container-status.ts
@@ -39,6 +39,28 @@
       color: #0366d6;
     }
 
+    /* Pulse animation for new commits */
+    @keyframes pulse {
+      0% {
+        transform: scale(1);
+        opacity: 1;
+      }
+      50% {
+        transform: scale(1.05);
+        opacity: 0.8;
+      }
+      100% {
+        transform: scale(1);
+        opacity: 1;
+      }
+    }
+
+    .pulse {
+      animation: pulse 1.5s ease-in-out;
+      background-color: rgba(38, 132, 255, 0.1);
+      border-radius: 3px;
+    }
+
     .last-commit-title {
       color: #666;
       font-family: system-ui, sans-serif;
@@ -99,6 +121,32 @@
       font-size: 12px;
     }
 
+    .copied-indicator {
+      position: absolute;
+      top: 0;
+      left: 0;
+      background: rgba(0, 0, 0, 0.7);
+      color: white;
+      padding: 2px 6px;
+      border-radius: 3px;
+      font-size: 11px;
+      pointer-events: none;
+      z-index: 10;
+    }
+
+    .copy-icon {
+      margin-left: 4px;
+      opacity: 0.7;
+    }
+
+    .copy-icon svg {
+      vertical-align: middle;
+    }
+
+    .last-commit-main:hover .copy-icon {
+      opacity: 1;
+    }
+
     .info-container {
       display: flex;
       align-items: center;
@@ -332,11 +380,33 @@
         // Get the first commit from the list
         const commit = message.commits[0];
         if (commit) {
+          // Check if the commit hash has changed
+          const hasChanged =
+            !this.lastCommit || this.lastCommit.hash !== commit.hash;
+
           this.lastCommit = {
             hash: commit.hash,
             pushedBranch: commit.pushed_branch,
           };
           this.lastCommitCopied = false;
+
+          // Add pulse animation if the commit changed
+          if (hasChanged) {
+            // Find the last commit element
+            setTimeout(() => {
+              const lastCommitEl =
+                this.shadowRoot?.querySelector(".last-commit-main");
+              if (lastCommitEl) {
+                // Add the pulse class
+                lastCommitEl.classList.add("pulse");
+
+                // Remove the pulse class after animation completes
+                setTimeout(() => {
+                  lastCommitEl.classList.remove("pulse");
+                }, 1500);
+              }
+            }, 0);
+          }
         }
       }
     }
@@ -358,10 +428,10 @@
       .writeText(textToCopy)
       .then(() => {
         this.lastCommitCopied = true;
-        // Reset the copied state after 2 seconds
+        // Reset the copied state after 1.5 seconds
         setTimeout(() => {
           this.lastCommitCopied = false;
-        }, 2000);
+        }, 1500);
       })
       .catch((err) => {
         console.error("Failed to copy commit info:", err);
@@ -620,9 +690,6 @@
               @click=${(e: MouseEvent) => this.copyCommitInfo(e)}
               title="Click to copy"
             >
-              ${this.lastCommitCopied
-                ? html`<span class="copied-indicator">Copied!</span>`
-                : ""}
               ${this.lastCommit
                 ? this.lastCommit.pushedBranch
                   ? html`<span class="commit-branch-indicator main-grid-commit"
@@ -632,6 +699,45 @@
                       >${this.lastCommit.hash.substring(0, 8)}</span
                     >`
                 : html`<span class="no-commit-indicator">N/A</span>`}
+              <span class="copy-icon">
+                ${this.lastCommitCopied
+                  ? html`<svg
+                      xmlns="http://www.w3.org/2000/svg"
+                      width="16"
+                      height="16"
+                      viewBox="0 0 24 24"
+                      fill="none"
+                      stroke="currentColor"
+                      stroke-width="2"
+                      stroke-linecap="round"
+                      stroke-linejoin="round"
+                    >
+                      <path d="M20 6L9 17l-5-5"></path>
+                    </svg>`
+                  : html`<svg
+                      xmlns="http://www.w3.org/2000/svg"
+                      width="16"
+                      height="16"
+                      viewBox="0 0 24 24"
+                      fill="none"
+                      stroke="currentColor"
+                      stroke-width="2"
+                      stroke-linecap="round"
+                      stroke-linejoin="round"
+                    >
+                      <rect
+                        x="9"
+                        y="9"
+                        width="13"
+                        height="13"
+                        rx="2"
+                        ry="2"
+                      ></rect>
+                      <path
+                        d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
+                      ></path>
+                    </svg>`}
+              </span>
             </div>
           </div>
         </div>