Skip to content

feat: support drag and drop img in web-shell - #8696

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
water-in-stone:feat/support-drag-drop-img
Aug 10, 2026
Merged

feat: support drag and drop img in web-shell#8696
wenshao merged 5 commits into
QwenLM:mainfrom
water-in-stone:feat/support-drag-drop-img

Conversation

@water-in-stone

@water-in-stone water-in-stone commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds image drag-and-drop support to every Web Shell composer while reusing the existing paste, attachment preview, and multimodal prompt pipeline.

It supports PNG, JPEG, GIF, WebP, and BMP images, preserves attachment order across files and batches, and allows image-only prompts in the main chat, split panes, and side tasks. It also provides drop-state feedback, attachment removal, queued submission, edit, and retry behavior.

The prompt lifecycle has been hardened to:

  • Limit image reading to four concurrent readers and an estimated 8 MiB base64 ingestion budget.
  • Prevent submission while images are still being read.
  • Prevent stale readers from updating a cleared or replaced composer.
  • Correlate admission and queue lifecycle events by prompt ID.
  • Avoid duplicate or missing user messages when start, terminal, and HTTP response events arrive out of order.
  • Preserve text, images, and input annotations across safe edit and retry flows.
  • Avoid automatic recovery or retry when admission may have succeeded but the response was lost.
  • Distinguish prompts that were removed before dispatch from prompts that started and were later cancelled.

The daemon wire format, ACP/Core protocol, and public Web Shell API remain unchanged.

Why it's needed

Web Shell already supported pasting images, but dropping image files onto the composer was not handled. Browser default drop behavior could also insert unwanted content or navigate away from the page.

The existing asynchronous ingestion and queue lifecycle had additional edge cases: attachments could be reordered, an image could be omitted if the prompt was submitted before reading completed, and response/event races could produce duplicate transcript entries or lose recoverable payload data.

This PR closes those gaps and makes image prompts reliable across direct, queued, image-only, retry, and editing flows.

Reviewer Test Plan

How to verify

  1. Open Web Shell with an image-capable model.
  2. Drag PNG and BMP files onto the composer and confirm that:
    • The page does not navigate away.
    • Drop highlighting is cleared after the drop.
    • Thumbnails appear in selection order.
  3. Send the images without entering text and confirm the image-only prompt appears once in the transcript.
  4. Remove one attachment before sending and confirm only the remaining image is submitted.
  5. Paste PNG, JPEG, GIF, WebP, and BMP images and confirm paste behavior matches drag-and-drop behavior.
  6. Drop unsupported or over-budget files and confirm they are skipped with aggregated feedback while valid files are retained.
  7. Submit an image-only prompt while another turn is active and confirm it is queued, can be edited or removed, and does not disappear because its text is empty.
  8. Confirm definite admission failures remain retryable, while ambiguous transport failures are marked as uncertain and are not automatically resent.
  9. Run the focused automated checks:
    • Web Shell: 7 focused test files, 530 tests passed.
    • WebUI: 2 focused test files, 237 tests passed.
    • Web Shell and WebUI lint passed.
    • Root build and typecheck passed.
    • Chromium Web Shell smoke tests passed for the drag-and-drop implementation.
    • Prettier and git diff --check passed.

Evidence (Before & After)

Before After
Dropping image files was not handled and could trigger browser default behavior. Dropped files become ordered composer attachments without navigating away.
Image-only prompts could be treated as empty submissions. Image-only prompts work in direct, split, side-task, and queued flows.
Image readers could complete out of order or update a stale composer. Ingestion is ordered, bounded, and isolated by composer ownership.
Admission response races could duplicate messages or lose recoverable payloads. Prompt-ID lifecycle correlation preserves complete payloads and appends each executed prompt once.

Automated Chromium coverage verifies PNG/BMP drop, preview ordering, image-only submission, deletion, request payload contents, transcript rendering, admission failure retry, and post-admission turn-error retry.

20260807-170544_._.s.video.meeting_0807_video.mp4

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Risk & Scope

  • Main risk or tradeoff: The main risk is asynchronous prompt admission and queue-event ordering. Focused tests cover response loss, early lifecycle events, cancellation, confirmed removal, owner changes, and duplicate prevention. The client-side 8 MiB budget protects browser resources but is intentionally separate from daemon or proxy request limits.
  • Not validated / out of scope: Manual Windows, Linux, and Firefox validation; restoring original image bytes from text-only daemon summaries after a page reload; SVG, TIFF, HEIC, PDF, directories, remote URLs, and cross-provider BMP image equivalence. Anthropic retains its existing unsupported-media text fallback for BMP.
  • Breaking changes / migration notes: None. The daemon wire format, ACP/Core protocol, and public Web Shell API are unchanged. Only internal WebUI action contracts were extended.

Linked Issues

Closes #8321

中文说明

What this PR does

本 PR 为所有 Web Shell composer 增加图片拖放能力,并复用现有的图片粘贴、附件预览和多模态 prompt 链路。

支持 PNG、JPEG、GIF、WebP 和 BMP,能够保持多文件及多批次附件顺序,并让主聊天、split pane 和 side task 都可以发送 image-only prompt。同时覆盖拖放状态反馈、附件删除、排队发送、编辑和重试。

本次还强化了 prompt 生命周期:

  • 图片读取最多使用四个并发 reader,并设置估算为 8 MiB base64 数据的客户端摄取预算。
  • 图片读取完成前禁止提交,避免发送不完整的 prompt。
  • composer 被清空或切换后,旧 reader 不再污染新状态。
  • admission 与队列生命周期事件通过 prompt ID 精确关联。
  • started、terminal 和 HTTP response 乱序时,不会重复或遗漏 user message。
  • 安全编辑和重试时保留文字、图片及 input annotations。
  • admission 可能成功但 response 丢失时,不自动恢复或重试,避免重复发送。
  • 区分“尚未 dispatch 就被删除”和“已经开始执行后被取消”的 prompt。

daemon wire format、ACP/Core 协议及公开 Web Shell API 均保持不变。

Why it's needed

Web Shell 已经支持粘贴图片,但将图片文件拖入 composer 时不会被接管,浏览器默认行为还可能插入无效内容或离开当前页面。

原有异步摄取和队列生命周期也存在一些边界问题:附件可能因为读取完成顺序不同而乱序;读取完成前提交会遗漏图片;response 与生命周期事件发生竞态时,可能产生重复 transcript message 或丢失可恢复的完整 payload。

本 PR 补齐这些缺口,使 direct、queued、image-only、retry 和 edit 等图片 prompt 流程更加可靠。

Reviewer Test Plan

How to verify

  1. 使用支持图片的模型打开 Web Shell。
  2. 将 PNG 和 BMP 文件拖入 composer,确认:
    • 页面不会跳转。
    • drop 完成后高亮状态被清理。
    • 缩略图按照选择顺序显示。
  3. 不输入文字直接发送,确认 image-only prompt 在 transcript 中只出现一次。
  4. 发送前删除一张附件,确认只提交剩余图片。
  5. 分别粘贴 PNG、JPEG、GIF、WebP 和 BMP,确认 paste 与 drag-and-drop 行为一致。
  6. 拖入不支持或超过客户端预算的文件,确认这些文件被跳过并显示聚合提示,合法文件仍然保留。
  7. 在已有 turn 执行期间提交 image-only prompt,确认它会正常排队,可以编辑或删除,不会因为文字为空而丢失。
  8. 确认明确的 admission 拒绝仍可安全重试;结果不明确的 transport failure 会标记为 uncertain,且不会自动重新发送。
  9. 运行聚焦验证:
    • Web Shell:7 个聚焦测试文件,共 530 tests,通过。
    • WebUI:2 个聚焦测试文件,共 237 tests,通过。
    • Web Shell 和 WebUI lint 通过。
    • 全仓 build 和 typecheck 通过。
    • 图片拖放实现的 Chromium Web Shell smoke tests 通过。
    • Prettier 和 git diff --check 通过。

Evidence (Before & After)

Before After
图片文件 drop 未被接管,可能触发浏览器默认行为。 drop 后图片按顺序成为 composer 附件,页面不会离开。
image-only prompt 可能被当作空提交。 main、split、side-task 和 queued 流程均支持 image-only prompt。
reader 可能乱序完成或写入已经失效的 composer。 摄取顺序稳定、资源有界,并按 composer owner 隔离。
admission response 竞态可能重复 message 或丢失完整 payload。 通过 prompt ID 关联生命周期,保留完整 payload,每个已执行 prompt 只追加一次。

Chromium 自动化覆盖 PNG/BMP drop、预览顺序、image-only 提交、附件删除、请求内容、transcript 渲染、admission failure retry 和 post-admission turn-error retry。

20260807-170544_._.s.video.meeting_0807_video.mp4

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

macOS 26.0、Node.js v22.22.3、本地 Web Shell mock daemon,以及 Chromium Playwright smoke tests。

Risk & Scope

  • Main risk or tradeoff: 主要风险是异步 prompt admission 与队列事件的乱序。聚焦测试覆盖 response 丢失、生命周期事件早到、取消、确认删除、owner 切换及重复消息防护。客户端 8 MiB 预算用于保护浏览器资源,不等同于 daemon 或代理的请求上限。
  • Not validated / out of scope: 未进行 Windows、Linux 和 Firefox 人工验证;不支持页面重载后从 text-only daemon summary 恢复原图片;SVG、TIFF、HEIC、PDF、目录和远程 URL 不在范围内;不承诺 BMP 在所有 provider 上都保持图片语义,Anthropic 继续使用既有的 unsupported-media 文本降级。
  • Breaking changes / migration notes: 无 breaking change,无需迁移。daemon wire format、ACP/Core 协议和公开 Web Shell API 不变,仅扩展了内部 WebUI action contract。

Linked Issues

Closes #8321

heyang.why added 3 commits August 7, 2026 16:40
Allow Web Shell composers to ingest image files reliably while
preserving the existing multimodal prompt protocol.

- Share ordered image ingestion across desktop and mobile editors
- Support image-only prompts and BMP preview and provider-safe handling
- Preserve queued payloads across retries and uncertain outcomes
- Add lifecycle guards, user feedback, unit coverage, and browser tests
Preserve complete prompt payloads and prevent duplicate or uncertain
delivery states when admission responses race with queue lifecycle
events.

- Correlate admission, queue, and terminal events by prompt ID
- Restore images and input annotations across retry and edit flows
- Bound image reader concurrency and encoded attachment memory
- Reconcile confirmed removals and explain ambiguous queue entries
Document the reviewed admission, recovery, and resource invariants.
Keep the design aligned with the hardened Web Shell implementation.

- Record bounded image ingestion and encoded-data budgeting
- Clarify prompt lifecycle correlation and confirmed removal behavior
- Describe annotation restoration and internal action boundaries
- Update focused validation evidence and acceptance criteria
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on 4a372ab and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— 4a372ab 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: real and observed — linked issue #8321 reports that Web Shell lacks the image drag-and-drop the TUI already supports (feature parity), and the issue carries the welcome-pr label, so the direction was pre-endorsed.

Direction: aligned — the TUI has handled image drag-and-drop since #3518, and this closes the gap for Web Shell. The claude-code CHANGELOG has no direct drag-and-drop reference, but image-input handling is an active area there.

Size: large. ~2,069 production logic lines (web-shell + webui), ~2,139 test lines, and a 688-line design doc. The packages/core changes are test-only (~119 lines across two converter test files). Per the 1000+ line advisory: worth considering whether part of this — especially the prompt-lifecycle hardening — could be split out. Informational only, not a blocker.

Approach: drag-and-drop ingestion reuses the existing paste/attachment pipeline, which is the right call, and the design doc is welcome. One question before deeper review: the PR also hardens the prompt admission/queue lifecycle (out-of-order start/terminal/HTTP events, duplicate prevention, ambiguous-failure handling). Some of that is entangled with image-only prompts and async ingestion, but parts look separable from drag-and-drop itself — was splitting that out considered?

Risk: no elevated risk signals — no non-test files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:真实且已被观测到——关联 issue #8321 指出 Web Shell 缺少 TUI 已支持的图片拖拽(功能对齐),且该 issue 带有 welcome-pr 标签,方向已获认可。

方向:对齐——TUI 自 #3518 起支持图片拖拽,此 PR 补齐 Web Shell 的能力。claude-code CHANGELOG 没有直接的拖拽条目,但图片输入处理是其活跃领域。

规模:较大。约 2,069 行生产逻辑(web-shell + webui)、约 2,139 行测试、688 行设计文档。packages/core 的改动仅为测试(约 119 行,两个 converter 测试文件)。按 1000+ 行大 PR 建议:可考虑是否能拆分部分内容(尤其是 prompt 生命周期加固)。仅为提示,不阻塞。

方案:拖拽摄取复用了现有粘贴/附件管线,方向正确,设计文档也很好。一个问题:PR 同时加固了 prompt admission/队列生命周期(start/terminal/HTTP 乱序事件、去重、不确定失败处理)。其中一部分与纯图片 prompt、异步摄取耦合,但另一部分看起来可以与拖拽功能本身解耦——是否考虑过拆分?

风险:无升级风险信号——没有非测试文件命中与 revert 相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 4a372ab37171d2b67a7f632611475d2afd7ce857 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head dc75bf6. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 12 render-shaping files:

  • packages/web-shell/client/App.module.css
  • packages/web-shell/client/App.tsx
  • packages/web-shell/client/components/ChatEditor.module.css
  • packages/web-shell/client/components/ChatEditor.tsx
  • packages/web-shell/client/components/ChatPane.module.css
  • packages/web-shell/client/components/ChatPane.tsx
  • packages/web-shell/client/components/QueuedPromptDisplay.tsx
  • packages/web-shell/client/components/SplitView.tsx
  • …and 4 more.

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Code review

I read the full diff against my own baseline proposal (reuse the paste pipeline for dropped files, preventDefault on dragover/drop, relax the submit guard for image-only prompts, bound the async reads). The PR matches that direction and goes further where it matters.

What's done well:

  • imageIngestion.ts is a clean, focused module: MIME normalization handles BMP variants and falls back to file extension for application/octet-stream, the batch budget is computed on base64-encoded size (~8 MiB), readers are concurrency-capped at 4, and results keep selection order. extractImageTransfer correctly distinguishes drop semantics (claim any file payload so the browser never navigates away, even for unsupported files) from paste semantics (only claim image items so text paste stays native).
  • The ingestion "lane" in useComposerCore uses a generation counter so a session switch or clear aborts active readers and drops stale batch results — the exact race that would otherwise attach images to the wrong composer. Submit is blocked while batches are pending (canSubmit / pendingImageBatchCount), so images can't be silently omitted by a fast Enter.
  • The queue-lifecycle hardening in useQueuedPrompts is the dense part of the diff, and it is real: prompt-id correlation across pending_prompt_started / terminal events / admission responses, removedAfterAbort handling, summary-only rows for server summaries that lack image bytes after a reload, and an explicit unknown admission state with restore/discard instead of blind auto-retry. The conservative choice (never auto-resend when the admission outcome is ambiguous) is the right one for avoiding double submission.
  • Image-only prompts are threaded consistently through every submit path (main chat, split panes, side tasks, queue), including the small but easy-to-miss isShellMode && prompt fix so an image-only submit doesn't produce a bare !.
  • The packages/core touch is test-only: BMP coverage in the Anthropic unsupported-media fallback and OpenAI modality tests. No core production code changes. The webui API additions (onAdmissionStarted, removedAfterAbort) are optional and backward-compatible; the daemon wire format is unchanged.
  • Conventions are followed: design doc under docs/design/, bilingual i18n strings, colocated tests, and window.confirm for destructive confirmations matches existing web-shell usage (ScheduledTasksDialog, GoalsDialog).

No critical blockers found. One behavioral note reviewers should be aware of: the unknown admission treatment applies to text-only prompts too, not just image prompts — if an admission POST starts and its response is lost (or fails with anything other than 413/501), the composer locks behind a restore/discard banner until the user resolves it. That's the documented design (better than silently resending), the banner offers both exits, and switching sessions clears it, but it is a user-visible change beyond drag-and-drop itself. Scope-wise, the lifecycle hardening is large (~2,069 production lines overall); it is entangled enough with image-only/queued flows that splitting looks impractical in practice, but the advisory from the gate comment stands.

The prompt-admission lifecycle is the part most worth a reviewer's attention, so here is the flow as implemented:

sequenceDiagram
    participant P1 as Composer
    participant P2 as Submit path
    participant P3 as Daemon actions
    participant P4 as Daemon
    participant P5 as Queue lifecycle
    P1->>P2: submit text and images
    P2->>P3: sendPrompt with onAdmissionStarted
    P3-->>P2: admission started
    P3->>P4: submitPrompt request
    alt response accepted
        P4-->>P3: accepted with prompt id
        P3-->>P2: onAdmitted, run or queue the turn
    else response lost or ambiguous
        P3-->>P2: error after start
        P2->>P2: mark admission unknown, lock composer until resolved
    else 413 or 501
        P3-->>P2: definite rejection, restore payload for retry
    end
    P4-->>P5: started and terminal queue events
    P5->>P5: correlate by prompt id before appending to transcript
Loading
Files changed (30 of 37 shown)
File What changed
docs/design/web-shell/web-shell-image-drag-and-drop.md Design doc covering the feature and lifecycle rules
packages/core/src/core/anthropicContentGenerator/converter.test.ts Extends unsupported-media fallback test to cover BMP
packages/core/src/core/openaiContentGenerator/converter.test.ts Asserts BMP image data is kept when image modality is on
packages/web-shell/client/App.test.tsx Image-only dispatch and admission-unknown lock tests
packages/web-shell/client/App.tsx Admission-unknown state, retry owner tracking, image-only dispatch
packages/web-shell/client/components/ChatEditor.tsx Wires drag handlers, busy send button, guarded image removal
packages/web-shell/client/components/ChatPane.test.tsx Pane-level admission-unknown and image-only tests
packages/web-shell/client/components/ChatPane.tsx Per-pane admission tracking and image-only submit
packages/web-shell/client/components/QueuedPromptDisplay.test.tsx Unknown-payload row action tests
packages/web-shell/client/components/QueuedPromptDisplay.tsx Restore and discard actions, summary-only edit lock
packages/web-shell/client/components/SplitView.tsx Forwards ingestion notice callback to panes
packages/web-shell/client/components/artifacts/ArtifactPanel.tsx Forwards ingestion notice callback
packages/web-shell/client/components/artifacts/SideTaskPanel.tsx Image-only prompts count for naming an empty side task
packages/web-shell/client/components/messages/Markdown.tsx Adds bmp to the safe image data-URI allowlist
packages/web-shell/client/e2e/web-shell.smoke.spec.ts Playwright drop of PNG plus BMP asserting the wire payload
packages/web-shell/client/hooks/useComposerCore.dom.test.tsx Drop claim, batch order, annotation remapping tests
packages/web-shell/client/hooks/useComposerCore.mobile.dom.test.tsx Mobile composer variant coverage
packages/web-shell/client/hooks/useComposerCore.ts Ingestion lane, drag handlers, restored annotation mapping
packages/web-shell/client/hooks/useQueuedPrompts.dom.test.tsx Queue race and owner-change correlation tests
packages/web-shell/client/hooks/useQueuedPrompts.ts Prompt-id lifecycle correlation and unknown admission rows
packages/web-shell/client/i18n.tsx EN and ZH strings for notices and uncertain delivery
packages/web-shell/client/utils/imageIngestion.test.ts MIME normalization, budget, and concurrency tests
packages/web-shell/client/utils/imageIngestion.ts Shared drop and paste extraction with bounded reads
packages/web-shell/client/utils/promptAdmission.test.ts HTTP status classification tests
packages/web-shell/client/utils/promptAdmission.ts Classifies 413 and 501 as definite admission rejection
packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx Covers onAdmissionStarted and abort cleanup result
packages/webui/src/daemon/session/actions.test.ts Covers removedAfterAbort and admission callback
packages/webui/src/daemon/session/actions.ts Reports removedAfterAbort and fires onAdmissionStarted
packages/webui/src/daemon/session/promptContent.test.ts Image-only BMP prompt content test
packages/webui/src/daemon/session/types.ts Adds optional onAdmissionStarted and removedAfterAbort
…and 7 more files CSS modules plus small test files for ChatEditor, Markdown, UserMessage, SideTaskPanel

Testing evidence (from the PR's own CI, fetched via API — no PR code was executed here)

The main unit suite is still running on the reviewed commit; nothing is red so far. The macOS/Windows test jobs and the CLI integration tests are skipped for this fork PR (authorization-gated), so the ubuntu jobs plus the PR's own focused suites are the available signal. The Qwen Triage Finalize job will update the table below once CI settles.

Final CI results for 4a372ab (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

The test files themselves target the claimed races directly (out-of-order terminal/admission events, S1→S2→S1 owner changes, exactly-once transcript append, restore/discard of uncertain payloads), and the Playwright smoke spec drives a real DataTransfer dragover/drop with generated PNG and BMP bytes and asserts the exact daemon wire payload — that is meaningful coverage, though synthetic drag events are not a real OS file drag. The author's reported numbers (530 web-shell + 237 webui focused tests passing) are their claim, not independently re-run here. Not verified: Windows/Linux browser behavior — the author's Tested-on table marks both as not tested.

Sandboxed verification would settle the remaining gap: @qwen-code /verify — that the drop-ingestion path and the admission-unknown composer lock are genuinely load-bearing versus the base build, and that the queue-lifecycle correlation actually changes behavior under the raced event orders, since this PR's suite could in principle pass with parts of the guard removed.

中文说明

代码审查:已通读全部 diff。实现方向正确——拖拽摄取复用了现有粘贴/附件管线,新的 imageIngestion.ts 模块职责清晰(MIME 归一化、8 MiB base64 预算、4 并发读取上限、保持选择顺序);useComposerCore 的摄取"泳道"用代际计数隔离会话切换/清空时的陈旧读取结果,读取未完成时阻止提交,图片不会被静默丢弃;useQueuedPrompts 的生命周期加固(prompt id 关联乱序事件、removedAfterAbort、重载后的 summary-only 行、不确定送达的 restore/discard 而非盲目自动重发)是 diff 中最密集的部分,选择保守且正确。纯图片 prompt 在所有提交路径(主聊天、分栏、侧任务、队列)中一致贯通。packages/core 仅改动测试(BMP 覆盖),无核心生产代码变更;webui API 为可选的向后兼容扩展,daemon 协议未变。未发现阻塞性问题。一个提醒:不确定送达(unknown admission)处理同样作用于纯文本 prompt——admission 请求发出后响应丢失时,composer 会锁定在 restore/discard 横幅之后直到用户处理,这是拖拽功能之外的用户可见行为变化。时序图展示了 admission 生命周期的实现方式;上方表格列出 37 个文件中的 30 个。

测试证据(来自 PR 自身的 CI,通过 API 获取——此处未执行任何 PR 代码):ubuntu 主单元测试仍在运行,目前没有红色检查;macOS/Windows 测试与 CLI 集成测试因 fork PR 授权门控被跳过。表格会在 CI 结束后由 finalize 任务更新。测试文件直接针对所声称的竞态(乱序事件、S1→S2→S1 owner 切换、transcript 恰好追加一次、不确定载荷的恢复/丢弃),Playwright 冒烟用例用真实 DataTransfer 拖放事件断言了确切的 daemon 请求载荷。作者报告的测试数字(530 + 237)是其声明,未在此独立复跑。未验证:Windows/Linux 浏览器行为(作者未测试)。沙箱验证可补齐剩余缺口:@qwen-code /verify 可验证拖拽摄取与 admission 锁定相对 base 构建确实承载行为变化。

Qwen Code · qwen3.8-max

Reviewed at 4a372ab37171d2b67a7f632611475d2afd7ce857 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid, well-tested feature work with no blocking findings; docking one point for the still-pending CI and the macOS-only manual testing.

Stepping back: this started as "Web Shell should accept dropped images like the TUI does" (#8321) and the PR delivers exactly that, plus the lifecycle work needed to make image-only and queued prompts actually reliable. Comparing against my independent proposal, the PR goes further than I would have on the first pass — the ingestion lane with generation-based isolation and the prompt-id event correlation are things I might have deferred — but the tests demonstrate those races are real rather than speculative: out-of-order terminal/admission events, session owner changes mid-flight, exactly-once transcript appends all have dedicated coverage that pins the behavior.

The code doesn't feel like it's trying too hard. The two new utilities are small and single-purpose, the drag-and-drop wiring reuses the existing paste pipeline rather than forking it, conventions are followed (design doc, bilingual strings, colocated tests), and core-package changes are test-only. My one standing reservation is the scope decision to let the admission-unknown lock affect text-only prompts too — it's the safe choice against double-sending, it's documented and tested, and a reviewer should still be aware the behavioral surface is wider than the title suggests. The author tested on macOS only; Windows/Linux browser behavior rests on CI and the synthetic-event Playwright spec.

I'm approving because the implementation is genuinely good, not because I ran out of objections. CI is still running on the reviewed commit, so approval is deferred until CI lands green on 4a372ab37171d2b67a7f632611475d2afd7ce857 — the finalize job will post the commit-pinned approval once every check completes, and withhold it if anything lands red or the head moves.

中文说明

置信度:4/5 —— 扎实、测试充分的特性实现,无阻塞性问题;因 CI 尚未结束且作者仅在 macOS 上手动测试,扣一分。

整体来看:需求源于 #8321(Web Shell 应像 TUI 一样支持图片拖拽),PR 完整交付了该能力,并补齐了让纯图片 prompt 与排队 prompt 真正可靠所需的生命周期处理。与我独立的方案对比,PR 走得更远——带代际隔离的摄取泳道、prompt id 事件关联,这些我第一遍可能会推迟——但测试证明这些竞态是真实存在的而非臆测:乱序的 terminal/admission 事件、会话 owner 中途切换、transcript 恰好追加一次,都有专门的覆盖。代码没有过度设计:两个新工具模块小而专注,拖拽接线复用了现有粘贴管线而非另起炉灶,项目约定(设计文档、双语字符串、同目录测试)均被遵守,core 包仅改动测试。唯一的保留意见是 admission-unknown 锁定同样作用于纯文本 prompt——这是避免重复发送的稳妥选择,有文档和测试支撑,但审阅者应意识到其行为面比标题所示更宽。作者仅在 macOS 上测试,Windows/Linux 浏览器行为依赖 CI 与合成事件的 Playwright 用例。批准是因为实现确实好,而不是因为没有理由拒绝。由于 CI 仍在所审 commit 上运行,批准将推迟到 CI 全绿后由 finalize 任务提交(绑定到所审 commit;如有红色检查或 head 移动则不会批准)。

Qwen Code · qwen3.8-max

Reviewed at 4a372ab37171d2b67a7f632611475d2afd7ce857 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship — CI landed green after the review. ✅

@water-in-stone water-in-stone changed the title Feat/support drag drop img feat: support drag drop img Aug 8, 2026
@water-in-stone

Copy link
Copy Markdown
Collaborator Author

@wenshao @yiliang114 Please take a look

@water-in-stone water-in-stone changed the title feat: support drag drop img feat: support drag drop img in web-shell Aug 8, 2026
@ytahdn

ytahdn commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Review Summary — PR #8696

Reviewed: 37 files, +4482/−414 at head of feat/support-drag-drop-img

What this PR does

Adds image drag-and-drop to every Web Shell composer, reusing the existing paste/attachment/multimodal pipeline, and hardens the async prompt lifecycle (ordered bounded ingestion, prompt-ID correlation, dedup, safe retry/edit).

Strengths

  • imageIngestion.ts is well-engineered. Bounded concurrency (4 readers) with a safe shared-index pool; 8 MiB base64 budget using the correct ceil(size/3)*4 estimate; order preserved via an indexed settled array; MIME normalization with BMP aliases (image/x-bmp, image/x-ms-bmp); distinct rejection reasons (unsupported/unavailable/too-large/read-failed).
  • promptAdmission.ts correctly separates definite rejections (413/501) from ambiguous transport failures, so only safe failures stay retryable and uncertain ones are not auto-resent.
  • Lifecycle correlation is thorough. useQueuedPrompts adds prompt-ID tracking, an owner token for session isolation, a bounded completed-prompt-id LRU (prevents duplicate/missing user messages), and explicit restore/discard for unknown prompts.
  • Drag-drop uses capture-phase handlers with preventDefault() (onDragEnterCapture/onDragOverCapture/onDropCapture), preventing browser navigation and unwanted content insertion; dropEffect='copy' on dragover.
  • Strong test coverage. 530 web-shell + 237 webui focused tests plus Chromium e2e smoke covering drop, ordering, image-only submit, removal, retry, and payload contents.

Observations

[Info] Size/complexity. The PR is large and touches many lifecycle paths; the complexity is justified by the races described, but it's a lot to land at once.

[Info] ownerTokenRef reset-during-render is an unusual pattern but acceptable here for session scoping.

No blocking issues. CI green (Test, web-shell E2E, Capture visuals). Only ci-bot has approved so far.

Approve.

ytahdn
ytahdn previously approved these changes Aug 8, 2026

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Well-engineered drag-drop ingestion (bounded, ordered, budgeted) and thorough prompt-lifecycle hardening (prompt-ID correlation, dedup, safe retry). Capture-phase preventDefault prevents navigation. Strong test coverage, CI green.

@water-in-stone

Copy link
Copy Markdown
Collaborator Author

@yiliang114 Please take a look

@wenshao
wenshao enabled auto-merge August 8, 2026 22:45
@water-in-stone water-in-stone changed the title feat: support drag drop img in web-shell feat: support drag and drop img in web-shell Aug 9, 2026
yiliang114
yiliang114 previously approved these changes Aug 9, 2026

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The hard parts check out: reader concurrency is genuinely bounded (worker-pool index handoff, batches serialized on a tail promise, cumulative 8 MiB budget inclusive at the boundary), stale readers are killed by lane identity + generation + abort with a post-await recheck, the race matrix is pinned by tests (terminal-before-response binding, no double append, removed-before-response no-append, exactly-once restore after definite rejection, unknown-payload restore/discard without resend), lost-response prompts are never auto-resent, and there is no new XSS surface (count-only notices, data-URL thumbnails, SVG stays blocked). The removed-before-dispatch vs started-then-cancelled distinction and the admission classifier are correct, and the core-package change is test-only.

One P2 worth fixing or testing away before merge: restoreQueuedPromptsToEditor drops the #7134 guard — images are now restored unconditionally after mergeRestoredPromptText, keyed by local row id. Same-row double restore is blocked, but if the payload text is already in the editor when a different row carrying the same payload is restored (re-typed/re-queued identical content, or a row re-materializing with a fresh id), the text dedupes while the images append again — exactly the case the old condition guarded. Suggested fix: skip image/annotation restore when nextText === currentText, or dedup on serverPromptId/payload hash; plus a restore-while-text-already-present regression test.

Minor notes: no content sniffing (extension/declared MIME trusted, parity with paste — the provider is the real validation boundary; worth a line in the design doc's trust-boundary section), drop protection is composer-scoped so drops on the message list still navigate (pre-existing), and the branch now conflicts with main and needs a rebase. Nothing blocks from my side.

…rop-img

# Conflicts:
#	packages/web-shell/client/App.tsx
@water-in-stone

water-in-stone commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

@ytahdn @yiliang114 All the conflicts have been resolved. Please take another look.

@water-in-stone

Copy link
Copy Markdown
Collaborator Author

@qwen-code-ci-bot @qwen-code-dev-bot Please take a look

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs.

@ytahdn

ytahdn commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

One P2 remains on the current head (e6c85df): restoreQueuedPromptsToEditor deduplicates restored text with mergeRestoredPromptText, but then restores images unconditionally. If the same payload text is already at the top of the editor—such as an identical prompt being retyped/requeued, or the payload reappearing under a fresh local row id—the text remains single while restoreImages() appends the attachments again. The new restoredPromptIdsRef only protects the same local row id, so it does not preserve the previous #7134 guard across equivalent rows.

I reproduced this against the current head with a focused regression test: prefill the editor with describe, enqueue describe with one image, then reject admission with HTTP 413. setText is correctly not called, but restoreImages is called once; all 35 existing tests in that file pass and the added regression fails.

Suggested minimal fix: when non-empty restored text is already present (nextText === currentText), skip restoring that payload’s images/annotations, while retaining the image-only restoration path. Please add the corresponding regression test before merge.

Skip payload attachments when restoring text is a no-op because the
same prompt text already exists in the composer.

- Restore images and annotations only when their text is inserted
- Preserve image-only restoration regardless of the current draft
- Add regression coverage for duplicate text with attachments
@water-in-stone

Copy link
Copy Markdown
Collaborator Author

@ytahdn @yiliang114 Fixed in dc75bf6.

restoreQueuedPromptsToEditor now restores images and input annotations for a non-empty payload only when its text is actually merged into the composer. Image-only payloads retain their existing restoration behavior.

I added the requested regression test using an existing describe draft, a queued describe payload with an image and annotation, and an HTTP 413 rejection. It fails before the fix and passes afterward.

Validation:

  • 42 relevant Vitest tests passed
  • Web Shell build and typecheck passed
  • Targeted ESLint, Prettier, and diff checks passed

Thanks for catching this.

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at dc75bf6. The duplicate attachment restoration issue is fixed: attachments are skipped when non-empty restored text is already present, while image-only restoration remains intact. The focused queue restoration suites pass 42/42 locally. No remaining blockers from my review.

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at head dc75bf6 after my approval was dismissed by the new push. The P2 is fixed exactly as recommended: restoreQueuedPromptsToEditor now tracks textWasRestored (true only when mergeRestoredPromptText actually changes the editor text) and skips image/annotation restore when the payload text is already present — normal restores and image-only prompts are unaffected, annotation offset alignment holds in both branches, and the new dom test reproduces the exact duplicate scenario (payload text already in editor + 413 reject) asserting no restore calls while the queue drains. The accepted tradeoff is the safer direction: in the dedupe case a not-yet-restored attachment is silently dropped instead of duplicated, and it stays visible to the user. I also verified the main-merge resolutions in e6c85df are clean unions — all four conflicted files keep the full PR wiring (the fourth onImageIngestionNotice site correctly folded into main's artifactPanelSharedProps), so nothing from the original review was dropped. CI green on this head. Nothing blocks merge.

@wenshao
wenshao added this pull request to the merge queue Aug 10, 2026
Merged via the queue into QwenLM:main with commit e465867 Aug 10, 2026
52 of 53 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.9.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image drag-and-drop with Web Shell UI

5 participants