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 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-22 15:19:23 +09:00
parent 60f80572d8
commit 552c819d55
2 changed files with 40 additions and 25 deletions

View file

@ -825,18 +825,16 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
// 5a. Auto-advance plan phases after LLM iteration // 5a. Auto-advance plan phases after LLM iteration
postStatus := agent.ContextBuilder.GetPlanStatus() postStatus := agent.ContextBuilder.GetPlanStatus()
if agent.ContextBuilder.HasActivePlan() && postStatus == "executing" { if agent.ContextBuilder.HasActivePlan() && postStatus == "executing" {
// Intercept: if AI changed status from interviewing to executing, // Intercept: if AI changed status to executing without user approval
// hijack to "review" and show the plan for user approval. // (from interviewing or review), validate and set to "review".
if preStatus == "interviewing" { if preStatus == "interviewing" || preStatus == "review" {
if err := agent.ContextBuilder.ValidatePlanStructure(); err != nil { if err := agent.ContextBuilder.ValidatePlanStructure(); err != nil {
_ = agent.ContextBuilder.SetPlanStatus("interviewing") _ = agent.ContextBuilder.SetPlanStatus("interviewing")
logger.WarnCF("agent", "Reverted plan to interviewing: "+err.Error(), logger.WarnCF("agent", "Reverted plan to interviewing: "+err.Error(),
map[string]interface{}{"agent_id": agent.ID}) map[string]interface{}{"agent_id": agent.ID})
// Inject rejection so LLM knows what to fix // Inject rejection into session history so LLM sees it next iteration
messages = append(messages, providers.Message{ rejectionMsg := "[System] Plan rejected: " + err.Error() + ". Fix and try again."
Role: "user", agent.Sessions.AddMessage(opts.SessionKey, "user", rejectionMsg)
Content: "[System] Plan rejected: " + err.Error() + ". Fix and try again.",
})
} else { } else {
_ = agent.ContextBuilder.SetPlanStatus("review") _ = agent.ContextBuilder.SetPlanStatus("review")
if !constants.IsInternalChannel(opts.Channel) { if !constants.IsInternalChannel(opts.Channel) {

View file

@ -487,37 +487,47 @@
.slide-approve-track { .slide-approve-track {
position: relative; position: relative;
height: 52px; height: 56px;
border-radius: 26px; border-radius: 28px;
overflow: hidden; overflow: hidden;
touch-action: none; 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 { .slide-approve-track.approved {
background: var(--done); background: var(--done);
border-color: 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 { .slide-approve-thumb {
position: absolute; position: absolute;
top: 2px; top: 3px;
left: 2px; left: 3px;
width: 48px; width: 50px;
height: 48px; height: 50px;
border-radius: 50%; border-radius: 50%;
background: var(--btn); background: var(--btn);
color: var(--btn-text); color: var(--btn-text);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px;
font-weight: 700;
cursor: grab; cursor: grab;
transition: left 0.3s cubic-bezier(0.25, 1, 0.5, 1); transition: left 0.3s cubic-bezier(0.25, 1, 0.5, 1);
z-index: 1; z-index: 1;
user-select: none; user-select: none;
-webkit-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 { .slide-approve-thumb.dragging {
@ -525,6 +535,11 @@
cursor: grabbing; cursor: grabbing;
} }
@keyframes shimmer {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
.slide-approve-label { .slide-approve-label {
position: absolute; position: absolute;
top: 0; top: 0;
@ -534,17 +549,19 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--hint); color: var(--text);
font-size: 14px; font-size: 15px;
font-weight: 600; font-weight: 600;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
transition: color 0.3s; transition: color 0.3s;
animation: shimmer 2.5s ease-in-out infinite;
} }
.slide-approve-track.approved .slide-approve-label { .slide-approve-track.approved .slide-approve-label {
color: #fff; color: #fff;
animation: none;
} }
</style> </style>
@ -736,7 +753,7 @@ function renderPlanFromData(data) {
if (data.status === 'review') { if (data.status === 'review') {
html += `<div class="slide-approve-wrap"> html += `<div class="slide-approve-wrap">
<div class="slide-approve-track glass glass-interactive"> <div class="slide-approve-track glass glass-interactive">
<div class="slide-approve-thumb">&raquo;</div> <div class="slide-approve-thumb"><svg viewBox="0 0 24 24"><path d="M5 12h14m-6-6 6 6-6 6" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></div>
<div class="slide-approve-label">Slide to Approve</div> <div class="slide-approve-label">Slide to Approve</div>
</div> </div>
</div>`; </div>`;
@ -766,7 +783,7 @@ function setupSlideApprove() {
var thumbStartLeft = 0; var thumbStartLeft = 0;
function getMaxLeft() { 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) { function onStart(e) {
@ -775,7 +792,7 @@ function setupSlideApprove() {
thumb.classList.add('dragging'); thumb.classList.add('dragging');
var clientX = e.touches ? e.touches[0].clientX : e.clientX; var clientX = e.touches ? e.touches[0].clientX : e.clientX;
startX = clientX; startX = clientX;
thumbStartLeft = thumb.offsetLeft - 2; // subtract initial 2px offset thumbStartLeft = thumb.offsetLeft - 3; // subtract initial 3px offset
e.preventDefault(); e.preventDefault();
} }
@ -784,7 +801,7 @@ function setupSlideApprove() {
var clientX = e.touches ? e.touches[0].clientX : e.clientX; var clientX = e.touches ? e.touches[0].clientX : e.clientX;
var dx = clientX - startX; var dx = clientX - startX;
var newLeft = Math.max(0, Math.min(thumbStartLeft + dx, getMaxLeft())); var newLeft = Math.max(0, Math.min(thumbStartLeft + dx, getMaxLeft()));
thumb.style.left = (newLeft + 2) + 'px'; thumb.style.left = (newLeft + 3) + 'px';
e.preventDefault(); e.preventDefault();
} }
@ -792,7 +809,7 @@ function setupSlideApprove() {
if (!dragging) return; if (!dragging) return;
dragging = false; dragging = false;
thumb.classList.remove('dragging'); thumb.classList.remove('dragging');
var currentLeft = thumb.offsetLeft - 2; var currentLeft = thumb.offsetLeft - 3;
var maxLeft = getMaxLeft(); var maxLeft = getMaxLeft();
if (currentLeft >= maxLeft * 0.8) { if (currentLeft >= maxLeft * 0.8) {
// Approved // Approved
@ -802,7 +819,7 @@ function setupSlideApprove() {
sendCommand('/plan start'); sendCommand('/plan start');
} else { } else {
// Snap back // Snap back
thumb.style.left = '2px'; thumb.style.left = '3px';
} }
} }