Skip to content

fix(plugins): deliver browser exports from the downloads folder - #21

Merged
leowio merged 2 commits into
mainfrom
render/ws1-local-delivery
Oct 23, 2026
Merged

leowio merged 2 commits into
mainfrom
render/ws1-local-delivery

Conversation

@fun1206-bit

@fun1206-bit fun1206-bit commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Hosted MCP agents send a large share of ChatCut exports to cloud (Lambda) rendering, even though a connected editor tab can render them locally. The published plugin skills push them there:

  • export told agents to "return its downloadUrl", and in Claude Code to download that URL to a local path. A browser export has no URL, so the only way to follow the skill was cloud rendering.
  • Codex basics said "Codex cannot ask the editor UI to … export", which contradicts the default local export.
  • known-errors described export failures in cloud-render terms.

A browser export reaching "complete" also does not mean the file is on disk. On production Chrome holds the second and later automatic downloads until the user allows "Download multiple files". The Claude desktop Browser pane opens a save dialog for every export. In both cases the server and the editor still report the file as downloaded.

The monorepo's 10/11 local-delivery rewrite (9e97ab445b) never reached this repository, and plugins/ has since been removed from the monorepo, so the published skills must be fixed here.

Approach

A browser export is delivered when the agent finds the file on disk, not when the job completes. Order:

  1. Render locally by default (omit renderMode).
  2. Find the file by name in the user's downloads folder:
    • Use expectedFilename from the server.
    • Until the server change ships, fall back to the completed track_export entry's name, taking the newest match modified after submission.
    • Allow a (1) suffix.
    • Search for that name only; never list or read other files there.
  3. If the file is missing, ask the user for the one step that matches their browser, then look again. Follow the server's deliveryGuidance when present.
  4. If the user cannot complete that step, or it still fails, explain the blocker in one sentence and ask whether to export in the cloud. After they agree, use renderMode:"cloud" and deliver its downloadUrl. Cloud is never offered before the user has had that chance, and never used just to get a link or for a self-check.
  5. Point to ChatCut Desktop only when cloud cannot do it either: 4K, media that exists only on the user's computer and cannot be uploaded, or a refused or failed cloud render.

Self-checks use preview_timeline, a format:"audio" export, or a local 480p range, never a full-resolution export.

Changes

Host Files Change
Codex skills/export, skills/chatcut-plugin-basics, skills/known-errors, skills/verification, .mcp.json, .codex-plugin/plugin.json Delivery rules and fallback order above. Basics no longer require a download URL or claim the editor cannot export. Self-check rule. Version header. Version bump.
Claude Code same skills (chatcut-plugin-basics-claude), .claude-plugin/plugin.json, README.md Same text as Codex; the export skill is byte-identical. Basics also tell the user to click Save in the Browser pane's save dialog. The README discloses the version header and the name-only downloads lookup.
Grok Bot / Cursor skills/known-errors, mcp.json, .cursor-plugin/plugin.json A failed local export is not by itself a reason to switch to cloud; cloud only as a fallback the user agrees to. The export skill already used local delivery. Version header. Version bump.
Kimi Work skills/known-errors, .mcp.json, kimi.plugin.json, README.md Same known-errors change. The Kimi-supplied chatcut-export-guide is unchanged, as the Kimi README requires. Version header in both MCP definitions. Version bump.
Tests tests/export-delivery.test.mjs, tests/plugin-version-header.test.mjs Keep the Codex and Claude export skills identical and keep the delivery rules. Require each host's header to equal its manifest version, since versions are bumped by hand.

Versions and header

Host Version Header
Codex 1.10.17 → 1.10.18 x-chatcut-plugin-version: 1.10.18 (http_headers)
Claude Code 1.10.19 → 1.10.20 x-chatcut-plugin-version: 1.10.20 (headers)
Grok Bot / Cursor 1.10.16 → 1.10.17 x-chatcut-plugin-version: 1.10.17 (headers)
Kimi Work 1.10.16 → 1.10.17 x-chatcut-plugin-version: 1.10.17 (.mcp.json and kimi.plugin.json)

Existing x-chatcut-mcp-surface and x-chatcut-mcp-client headers are kept. The backend telemetry change records this header as chatcut_plugin_version on external_mcp_tool_call, so cloud-render share can be compared between old and new plugin users.

Expected impact

Together with the server-side wording change, the projected reduction is about 15–30% of weekly cloud render minutes. This is an estimate from the source mix of the 10/13–19 baseline, not a measurement. The plugin part only takes effect as users update, and the version header is what will measure it.

Verification

  • node --test tests/*.test.mjs: 82/82 pass.
  • claude plugin validate ./claude and claude plugin validate . (marketplace): pass.
  • Header transmission was captured with a local echo server and isolated CODEX_HOME / CLAUDE_CONFIG_DIR:
    • Codex CLI 0.154.0 sends x-chatcut-mcp-surface: codex and x-chatcut-plugin-version: 1.10.18 on every request, including OAuth discovery.
    • Claude Code 2.1.286 sends x-chatcut-mcp-client, x-chatcut-mcp-surface, and x-chatcut-plugin-version: 1.10.20.

Production, real path: new 1.10.18 vs old 1.10.17

Setup: codex exec with an isolated CODEX_HOME and each plugin installed from a local marketplace; production api.chatcut.io; an internal test project; first 5 seconds (frames 0–150), 1080p. The editor ran in a fresh-profile Chrome 155, driven only through the debugging protocol, and the owner signed in themselves.

Case New 1.10.18 Old 1.10.17
No editor open Local submit fails; asks the user to open the editor; no cloud Same
User replies "opened", editor still not connected Asks to refresh and check the account; no cloud Same
Editor open, first Chrome download Renders locally (1.7 s), finds ~/Downloads/<name>.mp4 by exact name, delivers the path; file is 5.08 s H.264/AAC with the size matching track_export (see the last row)
Second download held by Chrome's "Download multiple files" prompt Render completes, file not on disk. Agent: "allow 'Download multiple files' in the address bar, then click 'Download again' in the Export queue; no need to export again." No cloud. After the user did so, it found and delivered the file Render completes, file not on disk, yet it replies "Chrome started the download, check the download list" (false delivery). It also listed the user's whole downloads folder
Downloads already allowed n/a Finds the file by name and delivers the path

Across these 10 turns neither version chose cloud on its own. The production error text already says not to retry in the cloud, so the plugin's visible difference here is honest delivery when the download is held. Claude Code, where the old skill demanded a local download of a URL, could not be run end-to-end in this environment.

Fallback chain (final text, no editor available)

Turn User Agent
1 Export the first 5 s Asks to open the editor; does not mention cloud
2 "I can't open the editor in a browser" Explains the blocker in one sentence and asks whether to export in the cloud; does not mention Desktop; no submit
3 (earlier ordering, same consent wording) "OK, use the cloud" renderMode:"cloud", 5 s 1080p, done in 16.5 s, delivers downloadUrl (the only cloud render in testing)

An earlier draft offered cloud in the same message as the first "open the editor" request. The final text adds "Do not offer cloud before the user has had that chance", and the re-run confirmed it.

Found on production, outside this PR

  • track_export looked up by the short render ID that submit_export returns always reports a browser-compute export as unavailable since the 10/20 production deploy; fix in ChatCut-Inc/chatcut#4994.
  • track_export briefly reported a just-completed browser export as unavailable ("every connected editor tab answered without it"). It read correctly a few minutes later, and the tab had not reloaded.
  • track_export with timelineId returned no entries even though the browser entries had that timelineId.
  • With no editor, production now sometimes returns No compatible browser executor has capacity. without the "open the editor with get_editor_url" guidance. The new skill still leads the agent to ask the user to open the editor.
  • The editor's Export queue shows "Saved to your Downloads folder" for downloads that Chrome is still holding.

Not covered

  • An end-to-end Claude Code export (header sending and manifest validation were verified).
  • Real exports in the Claude Browser pane, WorkBuddy, and the Codex app's in-app browser. The Codex in-app browser step ("click Download again") is pending a manual check.
  • The server's expectedFilename and deliveryGuidance, which are not on production yet. Testing used the name fallback.
  • The (1) duplicate-name case.
  • The Desktop branch (cloud cannot do it).
  • Grok and Kimi beyond static checks.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

The published export skills still told agents to return a cloud
downloadUrl and said Codex could not ask the editor to export, so agents
picked cloud rendering for files the browser had already rendered. A
completed browser export is only delivered once the file is found on
disk: Chrome holds the second automatic download until the user allows
multiple downloads, and the Claude Browser pane waits on a save dialog.

- Codex and Claude export: render locally by default, find the file by
  expectedFilename (or the entry name) in the downloads folder, ask the
  user for the one browser-specific step when it is missing, then cloud
  with the user's agreement, and ChatCut Desktop only when cloud cannot.
- Basics: drop the download-URL requirement and the claim that the
  editor cannot export.
- Known errors (all hosts): a failed local export is not by itself a
  reason to switch to cloud.
- Verification: self-check with preview_timeline, audio, or a local 480p
  range instead of a full-resolution export.
- Send x-chatcut-plugin-version from every host so export behavior can
  be compared across plugin releases; bump Codex 1.10.18, Claude
  1.10.20, Grok 1.10.17, Kimi 1.10.17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fun1206-bit

Copy link
Copy Markdown
Contributor Author

@leowio 麻烦帮忙 review 一下这个 PR,补充说明它解决什么问题、预期收益多少。

解决的问题

已发布插件的导出 skill 还是旧写法,和服务端已上线的规则(#4998 等,10/21 12:43 上线)矛盾:

  • Codex 版要求“完成后返回 downloadUrl”,还写着“Codex 不能让编辑器导出”;
  • Claude Code 版要求“把成片下载到本地路径再交付”。

浏览器本地导出没有链接,agent 照旧 skill 做就只能改走云。这个 PR 把插件说法和服务端对齐:本地导出后按文件名在下载目录确认文件真的在了才算交付,找不到就按浏览器请用户补一步,用户做不到才在同意后走云。顺带三件事:自检不再导出全片;下载被浏览器拦住时如实告诉用户,不再说“已触发下载”;请求带上 x-chatcut-plugin-version,用来按插件版本对比新旧用户的选云比例。

为什么现在需要

服务端改动上线后的监控(最近 24 小时,剔除批量号):云渲染分钟比上周同时段少约 42%。但两个客户端差别很大:

来源 占剩余云渲染分钟 选云比例(基线 → 现在)
Codex 约 38% 38% → 10–17%
Claude Code 约 20% 77% → 42–63%

今天上午 Claude Code 的选云比例回升,主要来自 3 个高频用户(几小时内各导出 60–80 次),全部是旧版插件(请求不带版本号)。旧 Claude skill 要求“下载到本地路径”,正是这个 PR 改掉的地方。

预期收益(推算)

如果 Claude Code 的选云比例降到接近 Codex 的水平,它的云渲染分钟约少一半到三分之二,也就是每周约 2.5–3.5k 分钟,约 $0.3–0.7k/周,相当于目前剩余云渲染的 10–15%。Codex 侧服务端规则已经起作用,增量较小。实际效果要等用户升级插件后,按 chatcut_plugin_version 核实。

验证:生产上用隔离 Codex 对比新旧插件 13 轮,新版在 Chrome 挂起第二次下载时引导用户点“允许”、找到文件后交付,旧版会假交付;兜底链路先征得同意才走云。仓库测试 82/82。详情见 PR 描述。

@leowio
leowio merged commit 06e83c2 into main Oct 23, 2026
5 checks passed
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.

2 participants