From a89e46ced9af05488848fc9937bbfda4c3693010 Mon Sep 17 00:00:00 2001 From: Xiao Yang Date: Sat, 9 May 2026 13:39:08 +0800 Subject: [PATCH 01/12] fix(frontend): surface reasoning_content / thinking_blocks on event cards When an assistant turn has no tool call and empty content but non-empty reasoning (e.g. qwen-flash thinking-only responses), the timeline showed a mystery empty "AGENT / Role: assistant" card with no clue why. The SSE-payload whitelist in normalizeFrontendEvent was dropping the fields even after the visualizer added them. Carry reasoning_content and thinking_blocks through the visualizer for both MessageEvent and ActionEvent, pass them through the normalizer, and render a collapsed grey Reasoning/Thinking expander on the card. Also bump agent-sdk pin to 3799d1cf so qwen3-coder-style XML tool calls that arrive in reasoning_content get recovered into structured tool calls instead of stalling the agent loop on empty messages. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/index.html | 37 ++++++++++++++++++++++++++++++++++++- pyproject.toml | 4 ++-- server/agent/visualizer.py | 18 ++++++++++++++++++ uv.lock | 8 ++++---- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 0959910..45ea3f7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -416,6 +416,29 @@ opacity: 0.7; } + .event-reasoning { + color: #888888; + font-size: 11px; + font-family: monospace; + margin: 4px 0 6px 0; + border-left: 2px solid #444444; + padding-left: 8px; + } + + .event-reasoning summary { + cursor: pointer; + color: #aaaaaa; + opacity: 0.85; + user-select: none; + } + + .event-reasoning pre { + margin: 4px 0 0 0; + white-space: pre-wrap; + word-break: break-word; + color: #888888; + } + .prompt-line { color: #cccccc; } @@ -6017,7 +6040,9 @@

Sisyphus

tool_name: data.tool_name || fallback.tool_name || null, tool_call_id: data.tool_call_id || fallback.tool_call_id || null, help_request: data.help_request || fallback.help_request || null, - awaiting_user_help: Boolean(data.awaiting_user_help || fallback.awaiting_user_help) + awaiting_user_help: Boolean(data.awaiting_user_help || fallback.awaiting_user_help), + reasoning_content: data.reasoning_content || fallback.reasoning_content || null, + thinking_blocks: data.thinking_blocks || fallback.thinking_blocks || null }; } @@ -11177,6 +11202,16 @@

Sisyphus

metadataHtml += `
Sender: ${escapeHtml(event.sender)}
`; } } + // Reasoning / thinking — surfaced for both MessageEvent and ActionEvent + if (event.reasoning_content) { + metadataHtml += `
Reasoning
${escapeHtml(event.reasoning_content)}
`; + } + if (event.thinking_blocks && event.thinking_blocks.length > 0) { + const tbText = event.thinking_blocks + .map(tb => tb.thinking || tb.text || JSON.stringify(tb)) + .join('\n\n'); + metadataHtml += `
Thinking
${escapeHtml(tbText)}
`; + } // Create HTML eventLine.innerHTML = ` diff --git a/pyproject.toml b/pyproject.toml index d467cd4..b9cb737 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,5 +76,5 @@ override-dependencies = [ ] [tool.uv.sources] -openhands-sdk = { git = "https://github.com/softpudding/agent-sdk.git", subdirectory = "openhands-sdk", rev = "1ac8fff47e78cc5cc65b5261859f3b2ec01ff282" } -openhands-tools = { git = "https://github.com/softpudding/agent-sdk.git", subdirectory = "openhands-tools", rev = "1ac8fff47e78cc5cc65b5261859f3b2ec01ff282" } +openhands-sdk = { git = "https://github.com/softpudding/agent-sdk.git", subdirectory = "openhands-sdk", rev = "3799d1cf2af72f8ce21a4942ffab67ffe208b551" } +openhands-tools = { git = "https://github.com/softpudding/agent-sdk.git", subdirectory = "openhands-tools", rev = "3799d1cf2af72f8ce21a4942ffab67ffe208b551" } diff --git a/server/agent/visualizer.py b/server/agent/visualizer.py index 545d9ef..b452032 100644 --- a/server/agent/visualizer.py +++ b/server/agent/visualizer.py @@ -106,6 +106,14 @@ def on_event(self, event: Event) -> None: sse_data["action"] = str(event.action) if event.summary: sse_data["summary"] = str(event.summary) + rc = getattr(event, "reasoning_content", None) + if rc: + sse_data["reasoning_content"] = rc + tbs = getattr(event, "thinking_blocks", None) + if tbs: + sse_data["thinking_blocks"] = [ + tb.model_dump() for tb in tbs + ] if event.tool_name == PLEASE_HELP_ME_TOOL_NAME and event.action: help_request = getattr(event.action, "message", None) if isinstance(help_request, str) and help_request.strip(): @@ -144,6 +152,16 @@ def on_event(self, event: Event) -> None: elif isinstance(event, MessageEvent): # MessageEvent has llm_message with role information sse_data["role"] = event.llm_message.role + # Surface reasoning so it's visible in the frontend even when + # `content` is empty (e.g. qwen-flash thinking-only responses). + rc = getattr(event.llm_message, "reasoning_content", None) + if rc: + sse_data["reasoning_content"] = rc + tbs = getattr(event.llm_message, "thinking_blocks", None) + if tbs: + sse_data["thinking_blocks"] = [ + tb.model_dump() for tb in tbs + ] # Also include activated_skills if present if event.activated_skills: sse_data["activated_skills"] = event.activated_skills diff --git a/uv.lock b/uv.lock index ae027e7..dc71d98 100644 --- a/uv.lock +++ b/uv.lock @@ -1678,8 +1678,8 @@ requires-dist = [ { name = "litellm", git = "https://github.com/softpudding/litellm.git?rev=363075400d97a5252fd2eb60c4f8d44bb529057c" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" }, { name = "numpy", specifier = ">=1.24.0" }, - { name = "openhands-sdk", git = "https://github.com/softpudding/agent-sdk.git?subdirectory=openhands-sdk&rev=1ac8fff47e78cc5cc65b5261859f3b2ec01ff282" }, - { name = "openhands-tools", git = "https://github.com/softpudding/agent-sdk.git?subdirectory=openhands-tools&rev=1ac8fff47e78cc5cc65b5261859f3b2ec01ff282" }, + { name = "openhands-sdk", git = "https://github.com/softpudding/agent-sdk.git?subdirectory=openhands-sdk&rev=3799d1cf2af72f8ce21a4942ffab67ffe208b551" }, + { name = "openhands-tools", git = "https://github.com/softpudding/agent-sdk.git?subdirectory=openhands-tools&rev=3799d1cf2af72f8ce21a4942ffab67ffe208b551" }, { name = "pillow", specifier = ">=10.0.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "pydantic", specifier = ">=2.5.0" }, @@ -2224,7 +2224,7 @@ wheels = [ [[package]] name = "openhands-sdk" version = "1.12.0" -source = { git = "https://github.com/softpudding/agent-sdk.git?subdirectory=openhands-sdk&rev=1ac8fff47e78cc5cc65b5261859f3b2ec01ff282#1ac8fff47e78cc5cc65b5261859f3b2ec01ff282" } +source = { git = "https://github.com/softpudding/agent-sdk.git?subdirectory=openhands-sdk&rev=3799d1cf2af72f8ce21a4942ffab67ffe208b551#3799d1cf2af72f8ce21a4942ffab67ffe208b551" } dependencies = [ { name = "agent-client-protocol" }, { name = "deprecation" }, @@ -2244,7 +2244,7 @@ dependencies = [ [[package]] name = "openhands-tools" version = "1.12.0" -source = { git = "https://github.com/softpudding/agent-sdk.git?subdirectory=openhands-tools&rev=1ac8fff47e78cc5cc65b5261859f3b2ec01ff282#1ac8fff47e78cc5cc65b5261859f3b2ec01ff282" } +source = { git = "https://github.com/softpudding/agent-sdk.git?subdirectory=openhands-tools&rev=3799d1cf2af72f8ce21a4942ffab67ffe208b551#3799d1cf2af72f8ce21a4942ffab67ffe208b551" } dependencies = [ { name = "bashlex" }, { name = "binaryornot" }, From 2470ebf9aa01dc830ac719f720847c21bb5070cc Mon Sep 17 00:00:00 2001 From: Xiao Yang Date: Sat, 9 May 2026 15:58:09 +0800 Subject: [PATCH 02/12] refactor(pixel-confirm): drop the zoom-crop, render preview at original size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The confirmation preview used to crop and rescale the viewport around the click target. That gave the agent a screenshot whose coordinate system did not match every other screenshot in the conversation, so a "retarget" reply could land pixels picked from zoom space — wrong. Return the full viewport screenshot instead, with the existing yellow target box and orange candidate outlines drawn on the live DOM (which the screenshot picks up naturally) plus a canvas-side fail-safe in device-pixel space. The agent now confirms the marked element, or emits a fresh coordinate from the same coordinate system it sees everywhere else — extension-side detection no longer constrains the retarget, since fresh-pixel estimates remain valid. Update both small- and big-model mouse_tool.j2 prompts to describe the preview in affirmative terms (no "zoomed crop" wording) and to invite either a candidate-center retarget or a fresh-pixel estimate. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/commands/pixel-confirm-render.ts | 165 ++---------------- server/agent/prompts/big_model/mouse_tool.j2 | 6 +- .../agent/prompts/small_model/mouse_tool.j2 | 6 +- server/agent/tools/browser_executor.py | 7 +- 4 files changed, 27 insertions(+), 157 deletions(-) diff --git a/extension/src/commands/pixel-confirm-render.ts b/extension/src/commands/pixel-confirm-render.ts index 58cfd7d..0413c60 100644 --- a/extension/src/commands/pixel-confirm-render.ts +++ b/extension/src/commands/pixel-confirm-render.ts @@ -1,17 +1,19 @@ /** * Pixel-Confirmation Render Module * - * Produces a zoomed confirmation screenshot for a pending pixel mouse action - * (click or drag). Two visual modes: + * Produces a confirmation screenshot for a pending pixel mouse action + * (click or drag) at the page's original viewport size, so the agent's + * coordinate system matches what it sees in every other screenshot. Two + * visual modes: * - * - 'pixel_hit' → YELLOW box around the hit element + zoom-crop centered on it. - * - 'pixel_miss' → red crosshair at the click coord + thin grey outlines on - * nearby candidate elements + zoom-crop centered on the click. + * - 'pixel_hit' → YELLOW box around the hit element. + * - 'pixel_miss' → orange dashed outlines on nearby candidate elements; + * no crosshair (the candidates already tell the agent + * where to re-aim). * - * Both modes capture a fresh viewport screenshot (no virtual cursor — we draw - * our own crosshair / box so the cursor sprite would be redundant) and return - * a base64 PNG data URL keyed under `screenshot_data_url` to match the shape - * used by other 2PC previews. + * Both modes capture a fresh viewport screenshot (no virtual cursor) and + * return a base64 PNG data URL keyed under `screenshot_data_url` to match + * the shape used by other 2PC previews. */ import { captureScreenshot, compressIfNeeded } from './screenshot'; @@ -33,12 +35,6 @@ const DRAG_LINE_COLOR = 'rgba(255, 212, 0, 0.85)'; const DRAG_LINE_WIDTH = 3; const DRAG_ARROW_HEAD = 14; -const BASE_CONTEXT_PADDING_X = 96; -const BASE_CONTEXT_PADDING_Y = 112; -const BASE_MIN_CROP_WIDTH = 520; -const BASE_MIN_CROP_HEIGHT = 320; -const MIN_CROP_RATIO = 0.58; - interface BBox { x: number; y: number; @@ -67,11 +63,6 @@ export interface PixelConfirmRenderResult { screenshot_data_url: string; viewport: { width: number; height: number }; scale: number; - crop: BBox; -} - -function clamp(value: number, min: number, max: number): number { - return Math.max(min, Math.min(max, value)); } function expandBbox(b: BBox, padding: number): BBox { @@ -83,115 +74,6 @@ function expandBbox(b: BBox, padding: number): BBox { }; } -function unionBbox(boxes: BBox[]): BBox { - if (boxes.length === 0) return { x: 0, y: 0, width: 0, height: 0 }; - let x1 = Infinity; - let y1 = Infinity; - let x2 = -Infinity; - let y2 = -Infinity; - for (const b of boxes) { - x1 = Math.min(x1, b.x); - y1 = Math.min(y1, b.y); - x2 = Math.max(x2, b.x + b.width); - y2 = Math.max(y2, b.y + b.height); - } - return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 }; -} - -function chooseCropCenter(request: PixelConfirmRenderRequest): { - center: PointXY; - focusBbox: BBox; -} { - if (request.mode === 'pixel_hit' && request.target_bbox) { - const focus = request.drag_end - ? unionBbox([ - request.target_bbox, - { - x: request.drag_end.x, - y: request.drag_end.y, - width: 1, - height: 1, - }, - ]) - : request.target_bbox; - return { - center: { - x: focus.x + focus.width / 2, - y: focus.y + focus.height / 2, - }, - focusBbox: focus, - }; - } - // pixel_miss or hit without bbox → center on the click point. - const focus: BBox = request.drag_end - ? unionBbox([ - { x: request.x, y: request.y, width: 1, height: 1 }, - { - x: request.drag_end.x, - y: request.drag_end.y, - width: 1, - height: 1, - }, - ]) - : { x: request.x - 1, y: request.y - 1, width: 2, height: 2 }; - return { - center: { - x: focus.x + focus.width / 2, - y: focus.y + focus.height / 2, - }, - focusBbox: focus, - }; -} - -function calculateCrop( - imageWidth: number, - imageHeight: number, - scale: number, - request: PixelConfirmRenderRequest, -): BBox { - const { focusBbox } = chooseCropCenter(request); - - const focusDevice = { - x: focusBbox.x * scale, - y: focusBbox.y * scale, - width: Math.max(1, focusBbox.width * scale), - height: Math.max(1, focusBbox.height * scale), - }; - - const contextX = BASE_CONTEXT_PADDING_X * scale; - const contextY = BASE_CONTEXT_PADDING_Y * scale; - const minCropW = Math.min( - imageWidth, - Math.max(BASE_MIN_CROP_WIDTH * scale, imageWidth * MIN_CROP_RATIO), - ); - const minCropH = Math.min( - imageHeight, - Math.max(BASE_MIN_CROP_HEIGHT * scale, imageHeight * MIN_CROP_RATIO), - ); - - const desiredW = Math.max(minCropW, focusDevice.width + contextX * 2); - const desiredH = Math.max(minCropH, focusDevice.height + contextY * 2); - - const cropW = Math.min(imageWidth, Math.round(desiredW)); - const cropH = Math.min(imageHeight, Math.round(desiredH)); - - const centerX = focusDevice.x + focusDevice.width / 2; - const centerY = focusDevice.y + focusDevice.height / 2; - - const cropX = clamp( - Math.round(centerX - cropW / 2), - 0, - Math.max(0, imageWidth - cropW), - ); - const cropY = clamp( - Math.round(centerY - cropH / 2), - 0, - Math.max(0, imageHeight - cropH), - ); - - return { x: cropX, y: cropY, width: cropW, height: cropH }; -} - function drawCandidateOutline( ctx: OffscreenCanvasRenderingContext2D, rect: BBox, @@ -476,38 +358,26 @@ export async function renderPixelConfirm( const actualScaleY = viewportHeight > 0 ? bitmap.height / viewportHeight : 1; const scale = (actualScaleX + actualScaleY) / 2 || 1; - const crop = calculateCrop(bitmap.width, bitmap.height, scale, request); - - const canvas = new OffscreenCanvas(crop.width, crop.height); + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); const ctx = canvas.getContext('2d'); if (!ctx) { bitmap.close(); throw new Error('[PixelConfirmRender] Failed to acquire 2d context'); } - ctx.drawImage( - bitmap, - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height, - ); + ctx.drawImage(bitmap, 0, 0); bitmap.close(); const toDeviceRect = (b: BBox): BBox => ({ - x: Math.round(b.x * scale - crop.x), - y: Math.round(b.y * scale - crop.y), + x: Math.round(b.x * scale), + y: Math.round(b.y * scale), width: Math.max(1, Math.round(b.width * scale)), height: Math.max(1, Math.round(b.height * scale)), }); const toDevicePoint = (p: PointXY): PointXY => ({ - x: Math.round(p.x * scale - crop.x), - y: Math.round(p.y * scale - crop.y), + x: Math.round(p.x * scale), + y: Math.round(p.y * scale), }); // Candidate outlines first (so the hit box / crosshair sits on top). @@ -557,6 +427,5 @@ export async function renderPixelConfirm( screenshot_data_url: compressed, viewport: { width: viewportWidth, height: viewportHeight }, scale, - crop, }; } diff --git a/server/agent/prompts/big_model/mouse_tool.j2 b/server/agent/prompts/big_model/mouse_tool.j2 index 6c496ee..b471608 100644 --- a/server/agent/prompts/big_model/mouse_tool.j2 +++ b/server/agent/prompts/big_model/mouse_tool.j2 @@ -59,7 +59,7 @@ Commit a pending click or drag that was previewed in the previous response. { "action": "confirm" } ``` -Only valid right after a preview-style observation (zoomed crop with a yellow box or red crosshair). See **Confirmation previews** below. +Use this right after a confirmation preview. See **Confirmation previews** below. ### scroll Scroll at the cursor's current position by `amount` CSS pixels in `direction`. `amount` is always positive — `direction` carries the sign. @@ -80,12 +80,12 @@ Return the cursor to the viewport center. ## Confirmation previews -When `click` or `drag` lands in an area with several interactable controls close together, the next observation is a zoomed crop showing exactly what your coordinate selected. The same outlines are also painted onto the live page DOM, so the screenshot reflects what a human watching the browser would see. +When `click` or `drag` lands in an area with several interactable controls close together, the next observation is the page at its normal size with the target marked. The outlines are painted onto the live page DOM, so the screenshot reflects what a human watching the browser would see. - A **yellow** outline marks the element the click would commit on. - **Orange dashed** outlines mark nearby candidates. The message lists each candidate's HTML and center coordinates in `[0, 1000]` space. -Check the yellow-highlighted element. If it matches what you wanted to click (or drag), reply `{ "action": "confirm" }` to commit. If it does not, re-emit `click` (or `drag`) with one of the listed candidate centers as the `coordinate`. +If the yellow-highlighted element matches what you wanted to click (or drag), reply `{ "action": "confirm" }` to commit. To retarget, emit `click` (or `drag`) again with a new `coordinate` — pick a center from the candidate list, or estimate a fresh pixel from the screenshot. For a drag preview, the same rules apply at each endpoint. `confirm` commits the drag as previewed; otherwise re-emit `drag` with corrected `start_coordinate` and `end_coordinate`. diff --git a/server/agent/prompts/small_model/mouse_tool.j2 b/server/agent/prompts/small_model/mouse_tool.j2 index 35f8fa1..36312f2 100644 --- a/server/agent/prompts/small_model/mouse_tool.j2 +++ b/server/agent/prompts/small_model/mouse_tool.j2 @@ -60,16 +60,16 @@ Commit a previewed click or drag. ```json { "action": "confirm" } ``` -Only valid after a preview observation (zoomed crop with a yellow box or red crosshair). See **Confirmation previews** below. +Use this right after a confirmation preview. See **Confirmation previews** below. ## Confirmation previews -If `click` or `drag` falls in a crowded area, the next observation is a zoomed crop. The same outlines are also painted on the live page DOM. +When `click` or `drag` falls in a crowded area, the next observation is the page at its normal size with the target marked. - A **yellow** outline marks the element the click would commit on. - **Orange dashed** outlines mark nearby candidates, listed in the message with HTML and center coordinates in `[0, 1000]` space. -Check the yellow-highlighted element. If it matches your intent, reply `{ "action": "confirm" }` to commit. Otherwise, re-emit `click` (or `drag`) with one of the listed candidate centers as the `coordinate`. +If the yellow-highlighted element matches your intent, reply `{ "action": "confirm" }` to commit. To retarget, emit `click` (or `drag`) again with a new `coordinate` — pick a center from the candidate list, or estimate a fresh pixel from the screenshot. ## Patterns diff --git a/server/agent/tools/browser_executor.py b/server/agent/tools/browser_executor.py index be42fe7..b66fc64 100644 --- a/server/agent/tools/browser_executor.py +++ b/server/agent/tools/browser_executor.py @@ -1355,9 +1355,10 @@ def _build_pixel_gate_message( ) -> str: """Compose the human-readable confirmation message for the agent. - Kept terse on purpose: the zoomed crop already shows the yellow - target and orange neighbors visually, so the message contributes - only the candidate list (HTML + centers) and one-line guidance. + Kept terse on purpose: the preview screenshot already shows the + yellow target and orange neighbors visually at original size, so + the message contributes only the candidate list (HTML + centers) + and one-line guidance. """ lines: list[str] = [] if kind == "click": From 6de8677738ee735f247075fb6b369fad9fc92580 Mon Sep 17 00:00:00 2001 From: Xiao Yang Date: Tue, 12 May 2026 18:39:13 +0800 Subject: [PATCH 03/12] fix(eval): drive move picker drills down; runner supports --tests subset The drive eval's Move-items dialog rendered every folder as a single flat scrollable list, forcing the agent to scan ~30 path-labelled rows to find a known nested target. Replace with a real drill-down: breadcrumb header at the top (clickable segments navigate up) and a list of direct child folders for the current location (click to drill in). The current breadcrumb tail is the destination, so 'Move items' commits to wherever you've navigated to. Add a --tests flag to evaluate_browser_agent.py that filters the all-tests scheduler to a named subset, so rerunning a handful of failing cases doesn't burn the whole benchmark slot. Co-Authored-By: Claude Opus 4.7 (1M context) --- eval/drive/css/drive.css | 55 ++++++++++++++++++++++++++++++---- eval/drive/js/drive.js | 42 +++++++++++++++++++------- eval/evaluate_browser_agent.py | 21 +++++++++++++ 3 files changed, 102 insertions(+), 16 deletions(-) diff --git a/eval/drive/css/drive.css b/eval/drive/css/drive.css index 05e3f54..a508ff4 100644 --- a/eval/drive/css/drive.css +++ b/eval/drive/css/drive.css @@ -196,12 +196,56 @@ } .drive-destination-picker { - max-height: 320px; - overflow: auto; border: 1px solid var(--mock-border); border-radius: 18px; - padding: 12px; background: rgba(15, 23, 34, 0.03); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.drive-destination-breadcrumb { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; + padding: 10px 14px; + border-bottom: 1px solid var(--mock-border); + background: rgba(15, 23, 34, 0.04); +} + +.drive-destination-crumb { + background: transparent; + border: 0; + padding: 4px 8px; + border-radius: 8px; + color: var(--mock-text); + font-size: 13px; + cursor: pointer; +} + +.drive-destination-crumb:hover { + background: rgba(15, 23, 34, 0.06); +} + +.drive-destination-crumb.active { + background: rgba(15, 157, 88, 0.12); + color: rgba(15, 157, 88, 0.95); + font-weight: 600; +} + +.drive-destination-sep { + color: rgba(15, 23, 34, 0.35); + font-size: 13px; +} + +.drive-destination-list { + max-height: 280px; + overflow: auto; + padding: 8px; + display: flex; + flex-direction: column; + gap: 2px; } .drive-destination-item { @@ -216,9 +260,8 @@ color: var(--mock-text); } -.drive-destination-item.active { - border-color: rgba(15, 157, 88, 0.24); - background: rgba(15, 157, 88, 0.08); +.drive-destination-item:hover { + background: rgba(15, 23, 34, 0.05); } .drive-upload-option { diff --git a/eval/drive/js/drive.js b/eval/drive/js/drive.js index f1b34a5..8b4e896 100644 --- a/eval/drive/js/drive.js +++ b/eval/drive/js/drive.js @@ -370,24 +370,46 @@ window.tracker = new AgentTracker("drive.google.com", "hard"); `; } - function getAllDestinationFolders(state) { - return state.items.filter((item) => item.type === "folder"); + function getChildFolders(state, parentId) { + return state.items.filter( + (item) => item.type === "folder" && item.parentId === parentId && item.section === "my-drive", + ); } function renderDestinationPicker(state, activeDestinationId) { - return ` -
- ${getAllDestinationFolders(state) + const trail = getFolderPath(state, activeDestinationId); + const children = getChildFolders(state, activeDestinationId || null); + + const breadcrumb = ` +
+ + ${trail + .map((folder, idx) => { + const isLast = idx === trail.length - 1; + return `/`; + }) + .join("")} +
+ `; + + const list = children.length + ? children .map((folder) => { - const path = getFolderPath(state, folder.parentId).map((item) => item.name).join(" / "); + const hasSub = getChildFolders(state, folder.id).length > 0; return ` - `; }) - .join("")} + .join("") + : `

No subfolders. The current folder is selected as the destination.

`; + + return ` +
+ ${breadcrumb} +
${list}
`; } @@ -439,7 +461,7 @@ window.tracker = new AgentTracker("drive.google.com", "hard"); ${renderDestinationPicker(state, modal.destinationId || null)}