From 2e29896283ea2e3f8235f81e8bac36b2fe497eab Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:19:23 +0900 Subject: [PATCH] fix: prevent review skip and improve slide-to-approve button Expand the plan status guard to intercept executing transitions from both "interviewing" and "review" states, fixing a bug where LLM could bypass user review. Fix dead rejection message by writing to session history instead of discarded local variable. Enhance slide button with larger track/thumb, SVG arrow icon, shimmer animation, and glow effect. Co-Authored-By: Claude Opus 4.6 --- pkg/agent/loop.go | 14 +++++----- pkg/miniapp/static/index.html | 51 +++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index e2d06eb44..7212f9883 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -825,18 +825,16 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt // 5a. Auto-advance plan phases after LLM iteration postStatus := agent.ContextBuilder.GetPlanStatus() if agent.ContextBuilder.HasActivePlan() && postStatus == "executing" { - // Intercept: if AI changed status from interviewing to executing, - // hijack to "review" and show the plan for user approval. - if preStatus == "interviewing" { + // Intercept: if AI changed status to executing without user approval + // (from interviewing or review), validate and set to "review". + if preStatus == "interviewing" || preStatus == "review" { if err := agent.ContextBuilder.ValidatePlanStructure(); err != nil { _ = agent.ContextBuilder.SetPlanStatus("interviewing") logger.WarnCF("agent", "Reverted plan to interviewing: "+err.Error(), map[string]interface{}{"agent_id": agent.ID}) - // Inject rejection so LLM knows what to fix - messages = append(messages, providers.Message{ - Role: "user", - Content: "[System] Plan rejected: " + err.Error() + ". Fix and try again.", - }) + // Inject rejection into session history so LLM sees it next iteration + rejectionMsg := "[System] Plan rejected: " + err.Error() + ". Fix and try again." + agent.Sessions.AddMessage(opts.SessionKey, "user", rejectionMsg) } else { _ = agent.ContextBuilder.SetPlanStatus("review") if !constants.IsInternalChannel(opts.Channel) { diff --git a/pkg/miniapp/static/index.html b/pkg/miniapp/static/index.html index a892f15cb..bbe9c13bc 100644 --- a/pkg/miniapp/static/index.html +++ b/pkg/miniapp/static/index.html @@ -487,37 +487,47 @@ .slide-approve-track { position: relative; - height: 52px; - border-radius: 26px; + height: 56px; + border-radius: 28px; overflow: hidden; touch-action: none; - transition: background 0.3s, border-color 0.3s; + border: 1.5px solid; + border-image: linear-gradient(135deg, var(--btn), var(--glass-border)) 1; + border-image: none; + border-color: var(--btn); + box-shadow: 0 2px 8px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.08); + transition: background 0.3s, border-color 0.3s, box-shadow 0.3s; } .slide-approve-track.approved { background: var(--done); border-color: var(--done); + box-shadow: 0 0 16px rgba(76,175,80,0.4), 0 2px 8px rgba(0,0,0,0.18); } .slide-approve-thumb { position: absolute; - top: 2px; - left: 2px; - width: 48px; - height: 48px; + top: 3px; + left: 3px; + width: 50px; + height: 50px; border-radius: 50%; background: var(--btn); color: var(--btn-text); display: flex; align-items: center; justify-content: center; - font-size: 16px; - font-weight: 700; cursor: grab; transition: left 0.3s cubic-bezier(0.25, 1, 0.5, 1); z-index: 1; user-select: none; -webkit-user-select: none; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + } + + .slide-approve-thumb svg { + width: 22px; + height: 22px; } .slide-approve-thumb.dragging { @@ -525,6 +535,11 @@ cursor: grabbing; } + @keyframes shimmer { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } + } + .slide-approve-label { position: absolute; top: 0; @@ -534,17 +549,19 @@ display: flex; align-items: center; justify-content: center; - color: var(--hint); - font-size: 14px; + color: var(--text); + font-size: 15px; font-weight: 600; pointer-events: none; user-select: none; -webkit-user-select: none; transition: color 0.3s; + animation: shimmer 2.5s ease-in-out infinite; } .slide-approve-track.approved .slide-approve-label { color: #fff; + animation: none; } @@ -736,7 +753,7 @@ function renderPlanFromData(data) { if (data.status === 'review') { html += `
-
»
+
Slide to Approve
`; @@ -766,7 +783,7 @@ function setupSlideApprove() { var thumbStartLeft = 0; function getMaxLeft() { - return track.offsetWidth - thumb.offsetWidth - 4; // 2px padding each side + return track.offsetWidth - thumb.offsetWidth - 6; // 3px padding each side } function onStart(e) { @@ -775,7 +792,7 @@ function setupSlideApprove() { thumb.classList.add('dragging'); var clientX = e.touches ? e.touches[0].clientX : e.clientX; startX = clientX; - thumbStartLeft = thumb.offsetLeft - 2; // subtract initial 2px offset + thumbStartLeft = thumb.offsetLeft - 3; // subtract initial 3px offset e.preventDefault(); } @@ -784,7 +801,7 @@ function setupSlideApprove() { var clientX = e.touches ? e.touches[0].clientX : e.clientX; var dx = clientX - startX; var newLeft = Math.max(0, Math.min(thumbStartLeft + dx, getMaxLeft())); - thumb.style.left = (newLeft + 2) + 'px'; + thumb.style.left = (newLeft + 3) + 'px'; e.preventDefault(); } @@ -792,7 +809,7 @@ function setupSlideApprove() { if (!dragging) return; dragging = false; thumb.classList.remove('dragging'); - var currentLeft = thumb.offsetLeft - 2; + var currentLeft = thumb.offsetLeft - 3; var maxLeft = getMaxLeft(); if (currentLeft >= maxLeft * 0.8) { // Approved @@ -802,7 +819,7 @@ function setupSlideApprove() { sendCommand('/plan start'); } else { // Snap back - thumb.style.left = '2px'; + thumb.style.left = '3px'; } }