mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add breakpoints to todo lists for auto-approve pause
This PR implements breakpoints for to-do list items that pause auto-approve mode when a todo with a breakpoint becomes in_progress. Changes: - Add breakpoint field to TodoItem type in packages/types/src/todo.ts - Add breakpoint toggle UI in TodoListDisplay.tsx with visual indicator - Add toggleTodoBreakpoint message handler in webviewMessageHandler.ts - Update checkAutoApproval to pause when in-progress todo has breakpoint - Add breakpointHit message to trigger celebration sound on pause - Play celebration sound (same as task complete) when breakpoint is hit - Breakpoints are session-only (disposable, not persisted across sessions) - Breakpoints clear automatically after being hit Closes #10399
This commit is contained in:
parent
2068531801
commit
98d2fbe981
9 changed files with 119 additions and 31 deletions
|
|
@ -14,6 +14,7 @@ export const todoItemSchema = z.object({
|
|||
id: z.string(),
|
||||
content: z.string(),
|
||||
status: todoStatusSchema,
|
||||
breakpoint: z.boolean().optional(), // When true, auto-approve pauses when this item becomes in_progress
|
||||
})
|
||||
|
||||
export type TodoItem = z.infer<typeof todoItemSchema>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type ClineAsk, type McpServerUse, type FollowUpData, isNonBlockingAsk } from "@roo-code/types"
|
||||
import { type ClineAsk, type McpServerUse, type FollowUpData, type TodoItem, isNonBlockingAsk } from "@roo-code/types"
|
||||
|
||||
import type { ClineSayTool, ExtensionState } from "../../shared/ExtensionMessage"
|
||||
import { ClineAskResponse } from "../../shared/WebviewMessage"
|
||||
|
|
@ -32,7 +32,7 @@ export type AutoApprovalStateOptions =
|
|||
export type CheckAutoApprovalResult =
|
||||
| { decision: "approve" }
|
||||
| { decision: "deny" }
|
||||
| { decision: "ask" }
|
||||
| { decision: "ask"; breakpointHit?: boolean }
|
||||
| {
|
||||
decision: "timeout"
|
||||
timeout: number
|
||||
|
|
@ -44,11 +44,13 @@ export async function checkAutoApproval({
|
|||
ask,
|
||||
text,
|
||||
isProtected,
|
||||
todoList,
|
||||
}: {
|
||||
state?: Pick<ExtensionState, AutoApprovalState | AutoApprovalStateOptions>
|
||||
ask: ClineAsk
|
||||
text?: string
|
||||
isProtected?: boolean
|
||||
todoList?: TodoItem[]
|
||||
}): Promise<CheckAutoApprovalResult> {
|
||||
if (isNonBlockingAsk(ask)) {
|
||||
return { decision: "approve" }
|
||||
|
|
@ -58,6 +60,15 @@ export async function checkAutoApproval({
|
|||
return { decision: "ask" }
|
||||
}
|
||||
|
||||
// Check for breakpoint on current in-progress todo item
|
||||
// If there's an in-progress todo with a breakpoint, pause auto-approval
|
||||
if (todoList && todoList.length > 0) {
|
||||
const inProgressTodo = todoList.find((t) => t.status === "in_progress")
|
||||
if (inProgressTodo?.breakpoint) {
|
||||
return { decision: "ask", breakpointHit: true }
|
||||
}
|
||||
}
|
||||
|
||||
if (ask === "followup") {
|
||||
if (state.alwaysAllowFollowupQuestions === true) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1150,12 +1150,20 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// Automatically approve if the ask according to the user's settings.
|
||||
const provider = this.providerRef.deref()
|
||||
const state = provider ? await provider.getState() : undefined
|
||||
const approval = await checkAutoApproval({ state, ask: type, text, isProtected })
|
||||
const approval = await checkAutoApproval({ state, ask: type, text, isProtected, todoList: this.todoList })
|
||||
|
||||
if (approval.decision === "approve") {
|
||||
this.approveAsk()
|
||||
} else if (approval.decision === "deny") {
|
||||
this.denyAsk()
|
||||
} else if (approval.decision === "ask" && "breakpointHit" in approval && approval.breakpointHit) {
|
||||
// Breakpoint hit: send notification to play celebration sound and clear the breakpoint
|
||||
provider?.postMessageToWebview({ type: "breakpointHit" })
|
||||
// Clear the breakpoint after it's hit (disposable, as per user request)
|
||||
const inProgressTodo = this.todoList?.find((t) => t.status === "in_progress")
|
||||
if (inProgressTodo) {
|
||||
inProgressTodo.breakpoint = false
|
||||
}
|
||||
} else if (approval.decision === "timeout") {
|
||||
// Store the auto-approval timeout so it can be cancelled if user interacts
|
||||
this.autoApprovalTimeoutRef = setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -1735,6 +1735,17 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
break
|
||||
}
|
||||
case "toggleTodoBreakpoint": {
|
||||
const { todoId, breakpoint } = (message.values ?? {}) as { todoId?: string; breakpoint?: boolean }
|
||||
const currentTask = provider.getCurrentTask()
|
||||
if (todoId && currentTask?.todoList) {
|
||||
const todo = currentTask.todoList.find((t) => t.id === todoId)
|
||||
if (todo) {
|
||||
todo.breakpoint = breakpoint
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "refreshCustomTools": {
|
||||
try {
|
||||
const toolDirs = getRooDirectoriesForCwd(getCurrentCwd()).map((dir) => path.join(dir, "tools"))
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ export interface ExtensionMessage {
|
|||
| "dismissedUpsells"
|
||||
| "organizationSwitchResult"
|
||||
| "interactionRequired"
|
||||
| "breakpointHit"
|
||||
| "browserSessionUpdate"
|
||||
| "browserSessionNavigate"
|
||||
| "claudeCodeRateLimits"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export type EditQueuedMessagePayload = Pick<QueuedMessage, "id" | "text" | "imag
|
|||
export interface WebviewMessage {
|
||||
type:
|
||||
| "updateTodoList"
|
||||
| "toggleTodoBreakpoint"
|
||||
| "deleteMultipleTasksWithIds"
|
||||
| "currentApiConfigName"
|
||||
| "saveApiConfiguration"
|
||||
|
|
|
|||
|
|
@ -867,6 +867,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "interactionRequired":
|
||||
playSound("notification")
|
||||
break
|
||||
case "breakpointHit":
|
||||
// Play celebration sound when a todo breakpoint is hit (same as task complete)
|
||||
playSound("celebration")
|
||||
break
|
||||
}
|
||||
// textAreaRef.current is not explicitly required here since React
|
||||
// guarantees that ref will be stable across re-renders, and we're
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import { cn } from "@/lib/utils"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { t } from "i18next"
|
||||
import { ArrowRight, Check, ListChecks, SquareDashed } from "lucide-react"
|
||||
import { useState, useRef, useMemo, useEffect } from "react"
|
||||
import { ArrowRight, Check, Circle, ListChecks, SquareDashed } from "lucide-react"
|
||||
import { useState, useRef, useMemo, useEffect, useCallback } from "react"
|
||||
|
||||
type TodoStatus = "completed" | "in_progress" | "pending"
|
||||
|
||||
interface TodoItem {
|
||||
id: string
|
||||
content: string
|
||||
status: TodoStatus
|
||||
breakpoint?: boolean
|
||||
}
|
||||
|
||||
function getTodoIcon(status: TodoStatus | null) {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
|
|
@ -16,22 +24,42 @@ function getTodoIcon(status: TodoStatus | null) {
|
|||
}
|
||||
}
|
||||
|
||||
export function TodoListDisplay({ todos }: { todos: any[] }) {
|
||||
export function TodoListDisplay({ todos }: { todos: TodoItem[] }) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(true)
|
||||
// Session-only breakpoint state (doesn't persist across sessions)
|
||||
const [breakpoints, setBreakpoints] = useState<Record<string, boolean>>({})
|
||||
const ulRef = useRef<HTMLUListElement>(null)
|
||||
const itemRefs = useRef<(HTMLLIElement | null)[]>([])
|
||||
const scrollIndex = useMemo(() => {
|
||||
const inProgressIdx = todos.findIndex((todo: any) => todo.status === "in_progress")
|
||||
const inProgressIdx = todos.findIndex((todo: TodoItem) => todo.status === "in_progress")
|
||||
if (inProgressIdx !== -1) return inProgressIdx
|
||||
return todos.findIndex((todo: any) => todo.status !== "completed")
|
||||
return todos.findIndex((todo: TodoItem) => todo.status !== "completed")
|
||||
}, [todos])
|
||||
|
||||
// Find the most important todo to display when collapsed
|
||||
const mostImportantTodo = useMemo(() => {
|
||||
const inProgress = todos.find((todo: any) => todo.status === "in_progress")
|
||||
const inProgress = todos.find((todo: TodoItem) => todo.status === "in_progress")
|
||||
if (inProgress) return inProgress
|
||||
return todos.find((todo: any) => todo.status !== "completed")
|
||||
return todos.find((todo: TodoItem) => todo.status !== "completed")
|
||||
}, [todos])
|
||||
|
||||
// Toggle breakpoint on a todo item
|
||||
const toggleBreakpoint = useCallback(
|
||||
(todoId: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation() // Prevent collapsing the list
|
||||
const newBreakpointState = !breakpoints[todoId]
|
||||
setBreakpoints((prev) => ({
|
||||
...prev,
|
||||
[todoId]: newBreakpointState,
|
||||
}))
|
||||
// Notify extension about the breakpoint change
|
||||
vscode.postMessage({
|
||||
type: "toggleTodoBreakpoint",
|
||||
values: { todoId, breakpoint: newBreakpointState },
|
||||
})
|
||||
},
|
||||
[breakpoints],
|
||||
)
|
||||
useEffect(() => {
|
||||
if (isCollapsed) return
|
||||
if (!ulRef.current) return
|
||||
|
|
@ -78,26 +106,47 @@ export function TodoListDisplay({ todos }: { todos: any[] }) {
|
|||
)}
|
||||
</div>
|
||||
{/* Inline expanded list */}
|
||||
{!isCollapsed && (
|
||||
<ul ref={ulRef} className="list-none max-h-[300px] overflow-y-auto mt-2 -mb-1 pb-0 px-2 cursor-default">
|
||||
{todos.map((todo: any, idx: number) => {
|
||||
const icon = getTodoIcon(todo.status as TodoStatus)
|
||||
return (
|
||||
<li
|
||||
key={todo.id || todo.content}
|
||||
ref={(el) => (itemRefs.current[idx] = el)}
|
||||
className={cn(
|
||||
"font-light flex flex-row gap-2 items-start min-h-[20px] leading-normal mb-2",
|
||||
todo.status === "in_progress" && "text-vscode-charts-yellow",
|
||||
todo.status !== "in_progress" && todo.status !== "completed" && "opacity-60",
|
||||
)}>
|
||||
{icon}
|
||||
<span>{todo.content}</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<ul ref={ulRef} className="list-none max-h-[300px] overflow-y-auto mt-2 -mb-1 pb-0 px-2 cursor-default">
|
||||
{todos.map((todo: TodoItem, idx: number) => {
|
||||
const icon = getTodoIcon(todo.status as TodoStatus)
|
||||
const hasBreakpoint = breakpoints[todo.id] || false
|
||||
const canHaveBreakpoint = todo.status === "pending" // Only pending items can have breakpoints
|
||||
return (
|
||||
<li
|
||||
key={todo.id || todo.content}
|
||||
ref={(el) => (itemRefs.current[idx] = el)}
|
||||
className={cn(
|
||||
"font-light flex flex-row gap-2 items-start min-h-[20px] leading-normal mb-2 group",
|
||||
todo.status === "in_progress" && "text-vscode-charts-yellow",
|
||||
todo.status !== "in_progress" && todo.status !== "completed" && "opacity-60",
|
||||
)}>
|
||||
{icon}
|
||||
<span className="flex-1">{todo.content}</span>
|
||||
{/* Breakpoint toggle button - only show for pending items */}
|
||||
{canHaveBreakpoint && (
|
||||
<button
|
||||
onClick={(e) => toggleBreakpoint(todo.id, e)}
|
||||
className={cn(
|
||||
"shrink-0 p-0.5 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors",
|
||||
hasBreakpoint
|
||||
? "text-vscode-charts-red opacity-100"
|
||||
: "text-vscode-descriptionForeground opacity-0 group-hover:opacity-60",
|
||||
)}
|
||||
title={
|
||||
hasBreakpoint ? t("chat:todo.removeBreakpoint") : t("chat:todo.addBreakpoint")
|
||||
}>
|
||||
<Circle
|
||||
className={cn("size-2.5", hasBreakpoint && "fill-current")}
|
||||
strokeWidth={hasBreakpoint ? 0 : 2}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -470,6 +470,8 @@
|
|||
"complete": "{{total}} to-dos done",
|
||||
"updated": "Updated the to-do list",
|
||||
"completed": "Completed",
|
||||
"started": "Started"
|
||||
"started": "Started",
|
||||
"addBreakpoint": "Add breakpoint (pauses auto-approve)",
|
||||
"removeBreakpoint": "Remove breakpoint"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue