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>