@xiaobuyu/nodesign 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +22 -22
  9. package/server/api/assets/notes.js +144 -144
  10. package/server/api/assets.js +896 -896
  11. package/server/api/board.js +57 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +464 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1024 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +339 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +155 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/explain-style.js +237 -237
  120. package/server/engine/mcp/tools/export-handoff.js +133 -133
  121. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  122. package/server/engine/mcp/tools/generate-image.js +842 -842
  123. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  124. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  125. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  126. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  127. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  128. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  129. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  130. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  131. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  132. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  133. package/server/engine/mcp/tools/highlight.js +68 -68
  134. package/server/engine/mcp/tools/list-pages.js +222 -222
  135. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  136. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  137. package/server/engine/mcp/tools/organize-board.js +64 -64
  138. package/server/engine/mcp/tools/paint-still.js +434 -434
  139. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  140. package/server/engine/mcp/tools/preview-deck.js +130 -130
  141. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  142. package/server/engine/mcp/tools/publish-site.js +102 -102
  143. package/server/engine/mcp/tools/query-elements.js +195 -195
  144. package/server/engine/mcp/tools/read-board.js +103 -103
  145. package/server/engine/mcp/tools/read-document.js +93 -93
  146. package/server/engine/mcp/tools/read-page.js +264 -264
  147. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  148. package/server/engine/mcp/tools/record-decision.js +140 -140
  149. package/server/engine/mcp/tools/relate-on-board.js +110 -110
  150. package/server/engine/mcp/tools/report-issue.js +115 -115
  151. package/server/engine/mcp/tools/roll-film.js +266 -266
  152. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  153. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  154. package/server/engine/mcp/tools/screenshot.js +583 -583
  155. package/server/engine/mcp/tools/tier-gate.js +96 -96
  156. package/server/engine/mcp/tools/trace-motion.js +215 -215
  157. package/server/engine/mcp/tools/web-search.js +548 -548
  158. package/server/engine/motion/inventory.js +349 -349
  159. package/server/engine/perception/session.js +235 -235
  160. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  161. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
  162. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  163. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  164. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  165. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  169. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
  170. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  171. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  172. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
  173. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  174. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  175. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
  176. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  177. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  178. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  179. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  180. package/server/engine/runs/active-runs.js +698 -698
  181. package/server/engine/runs/live-turn.js +252 -252
  182. package/server/engine/runs/store.js +364 -364
  183. package/server/engine/runs/turn-relay.js +217 -217
  184. package/server/engine/runtime/workspace.js +164 -164
  185. package/server/lib/artifact-file-path.js +80 -80
  186. package/server/lib/artifact-target.js +284 -284
  187. package/server/lib/asset-refs.js +239 -239
  188. package/server/lib/async-queue.js +104 -104
  189. package/server/lib/auto-relations.js +130 -130
  190. package/server/lib/binding-types.js +60 -60
  191. package/server/lib/board-kind-sizes.js +62 -62
  192. package/server/lib/board-relations.js +133 -133
  193. package/server/lib/browse-proxy.js +180 -180
  194. package/server/lib/canvas-id.js +36 -36
  195. package/server/lib/cover.js +227 -227
  196. package/server/lib/danbooru-tags.js +291 -291
  197. package/server/lib/doc-extract.js +156 -156
  198. package/server/lib/docx/build-from-source.js +260 -260
  199. package/server/lib/docx/build.js +490 -490
  200. package/server/lib/docx/dump-styles.js +287 -287
  201. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  202. package/server/lib/docx/merge-runs.js +129 -129
  203. package/server/lib/docx/numbering.js +218 -218
  204. package/server/lib/docx/order.js +152 -152
  205. package/server/lib/docx/rawzip.js +172 -172
  206. package/server/lib/docx/render.js +92 -92
  207. package/server/lib/docx/text-lint.js +225 -225
  208. package/server/lib/docx/tokens.js +335 -335
  209. package/server/lib/docx/units.js +29 -29
  210. package/server/lib/docx/xml.js +291 -291
  211. package/server/lib/docx-pages.js +187 -187
  212. package/server/lib/export-collect.js +289 -289
  213. package/server/lib/export-package.js +210 -210
  214. package/server/lib/html-srcset.js +145 -145
  215. package/server/lib/image-variant.js +460 -460
  216. package/server/lib/ingress/forward-openai-chat.js +305 -305
  217. package/server/lib/ingress/openai-chat.js +491 -491
  218. package/server/lib/ingress/session-notice.js +66 -66
  219. package/server/lib/ingress/session-routes.js +68 -68
  220. package/server/lib/ingress/slot-probe.js +130 -130
  221. package/server/lib/ingress/upstream-billing.js +52 -52
  222. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  223. package/server/lib/ingress/upstream-truncation.js +48 -48
  224. package/server/lib/issues-store.js +182 -182
  225. package/server/lib/kinds/deck.js +72 -72
  226. package/server/lib/kinds/docx.js +283 -283
  227. package/server/lib/kinds/file-kinds.js +113 -113
  228. package/server/lib/kinds/index.js +202 -202
  229. package/server/lib/kinds/site.js +161 -161
  230. package/server/lib/model-ingress.js +543 -543
  231. package/server/lib/moderation.js +255 -255
  232. package/server/lib/notice-store.js +103 -103
  233. package/server/lib/plugin-install.js +104 -104
  234. package/server/lib/plugin-validator.js +721 -721
  235. package/server/lib/publish-store.js +115 -115
  236. package/server/lib/quick-summary.js +52 -52
  237. package/server/lib/quota.js +270 -270
  238. package/server/lib/rate-window.js +29 -29
  239. package/server/lib/reference-assets.js +92 -92
  240. package/server/lib/region-shot.js +135 -135
  241. package/server/lib/safe-path.js +73 -73
  242. package/server/lib/sdk-session.js +33 -33
  243. package/server/lib/showcase-store.js +92 -92
  244. package/server/lib/site-publish.js +465 -465
  245. package/server/lib/ssrf-guard.js +432 -432
  246. package/server/lib/task-scan.js +158 -158
  247. package/server/lib/tavern-json.js +154 -154
  248. package/server/lib/video-variant.js +237 -237
  249. package/server/lib/workspace-path.js +33 -33
  250. package/server/ops/fix-sdk-musl.mjs +40 -40
  251. package/server/ops/install-macos-fonts.sh +114 -114
  252. package/server/ops/macos-fonts.conf +336 -336
  253. package/server/ops/sandbox-shim/bwrap +57 -57
  254. package/server/projects/assets-summary.js +119 -119
  255. package/server/projects/auto-name.js +53 -53
  256. package/server/projects/board-store.js +640 -640
  257. package/server/projects/move-entry.js +103 -103
  258. package/server/projects/store.js +270 -270
  259. package/server/projects/ui-config.js +54 -54
  260. package/server/projects/workspace-templates.js +53 -53
  261. package/server/projects/workspace.js +1025 -1025
  262. package/server/runtime/local-config.js +177 -177
  263. package/server/runtime/profile.js +72 -72
  264. package/server/services/rembg-service.py +334 -334
  265. package/server/shared/README.md +25 -25
  266. package/server/shared/deck.js +54 -54
  267. package/server/shared/time.js +49 -49
  268. package/server/style-pipeline/README.md +53 -53
  269. package/server/ws/broker.js +36 -36
  270. package/server/ws/browse-channel.js +177 -177
  271. package/server/ws/index.js +386 -386
  272. package/web/README.md +89 -89
  273. package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
  274. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
  275. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
  276. package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
  277. package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
  278. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
  279. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
  280. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
  281. package/web/dist/index.html +17 -17
  282. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
@@ -1,278 +1,278 @@
1
- /**
2
- * mcp/tools/artifact-session.js — 产物会话四件:artifact_open / artifact_computer /
3
- * artifact_find / artifact_batch(2026-08-21)
4
- *
5
- * 成品检查的交互半边。以前七个感知工具全是一次性的(开一只新浏览器、量、关),
6
- * "点开菜单 → 填表 → 提交 → 看校验"只能靠 beforeShot 塞 JS 假装;hover 态、焦点
7
- * 顺序、方向键连按、拖拽、pointer lock 这些只认真实手势。现在:
8
- *
9
- * artifact_open 打开(或重载)一个产物到常驻会话,状态跨调用留着
10
- * artifact_computer browser_computer 同一套动作(runAction 共享一份),对着会话页
11
- * artifact_find 词法找元 → ref(refs.js 共享一份)
12
- * artifact_batch 一次往返跑一串(makeBatchTool 共享一份合同)
13
- *
14
- * 老工具(screenshot_canvas / trace_motion / get_computed_styles / explain_style /
15
- * query_elements)加 `live:true` 就对着会话里**现在这一页**量,胶片条/示波器/级联
16
- * 解释全能骑在交互态上 —— 那是它们各自独有、browser use 没有的量具。
17
- *
18
- * 坐标:产物视口按形态定(站 1440×900、deck 1920×1080…),截图可能被归一化缩过,
19
- * 会话记 frame={w,h,scale},模型读到的坐标 ÷ scale = 页面像素。artifact_open 的
20
- * 返回会把截图尺寸说清楚。
21
- */
22
-
23
- import { tool } from '@anthropic-ai/claude-agent-sdk';
24
- import { z } from 'zod';
25
- import { openSession, withSession, changedSinceOpen, peekSession, defaultViewportFor } from '../../perception/session.js';
26
- import { findInPage, formatMatches } from '../../browse/refs.js';
27
- import { runAction, ACTIONS_DOC, COMPUTER_SCHEMA, actionErrorText } from './browse-computer.js';
28
- import { makeBatchTool, HALT_TEXT } from './browse-find-batch.js';
29
- import { normalizeShot, visionTokens } from './helpers/shot-pipeline.js';
30
- import { degradedNote } from './helpers/perception-page.js';
31
- import { acquireArtifactPage, LIVE_PARAM_DESC } from './helpers/acquire-page.js';
32
- import { collectMotionInventory, formatMotionInventory } from '../../motion/inventory.js';
33
- import { CANVAS_PATH_DESC, resolveCanvasTarget, requireBrowsable } from '../../../lib/artifact-target.js';
34
-
35
- const asText = (text, isError = false) => ({ content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) });
36
-
37
- /** 会话页截图(不存桌面卡 —— 那是浏览通道的事);caption 说清截图空间 */
38
- async function sessionShot(entry, lead) {
39
- const buf = await entry.page.screenshot({ type: 'png' });
40
- const shot = await normalizeShot(buf);
41
- const f = entry.frame;
42
- return {
43
- content: [
44
- { type: 'text', text: [lead, `${f.w}×${f.h} (${f.scale < 1 ? `viewport ${entry.viewport.width}×${entry.viewport.height} scaled ×${f.scale.toFixed(2)}; ` : ''}coordinates you use next are these screenshot pixels, ≈${visionTokens(f.w, f.h)} tokens)`, shot.note].filter(Boolean).join(' · ') },
45
- { type: 'image', data: shot.data, mimeType: shot.mimeType },
46
- ],
47
- };
48
- }
49
-
50
- async function changedNote(entry) {
51
- const changed = await changedSinceOpen(entry);
52
- if (!changed.length) return null;
53
- const shown = changed.slice(0, 5).join(', ');
54
- return `⚠ files changed since this page was opened (${shown}${changed.length > 5 ? ` +${changed.length - 5}` : ''}) — the page is showing the OLD version. artifact_open again to reload (state resets).`;
55
- }
56
-
57
- export function makeArtifactOpenTool({ projectId, workspaceRoot, sessionId }) {
58
- return tool(
59
- 'artifact_open',
60
- `Open one of your artifacts (a site page, a deck, a game) in a persistent
61
- session browser — the starting point for interactive checking. Unlike
62
- screenshot_canvas (fresh load every call), the page stays open between calls:
63
- menus you open stay open, text you type stays typed, a game keeps its state.
64
- Then use artifact_computer (click / type / keys / drag / scroll / zoom /
65
- screenshot), artifact_find (element refs), artifact_batch (several steps in one
66
- round trip), and the measuring tools with live:true (screenshot_canvas filmstrip,
67
- trace_motion, get_computed_styles, explain_style, query_elements) to inspect
68
- the CURRENT interactive state.
69
-
70
- Opening the same file again reloads it (state resets). Opening a different file
71
- or device replaces the session. The page is loaded over http like the user
72
- preview (fetch/localStorage behave the same). After you edit files the open page
73
- is stale — every artifact_* result reminds you; reopen to see the new version.
74
-
75
- Returns the screenshot size: coordinates you pass to artifact_computer are
76
- pixels of that screenshot (a deck at 1920×1080 is scaled down to fit the vision
77
- limit; the mapping is handled for you).`,
78
- {
79
- path: z.string().optional().describe(CANVAS_PATH_DESC),
80
- device: z.enum(['desktop', 'tablet', 'mobile']).optional()
81
- .describe('SITE ONLY. Real device width to render at: desktop=1440, tablet=834, mobile=390. Default desktop.'),
82
- viewport: z.object({ width: z.number().int().min(320).max(2560), height: z.number().int().min(240).max(2560) }).optional()
83
- .describe('Explicit viewport; defaults to the artifact kind (site → device width × 900, deck → its aspect).'),
84
- },
85
- async ({ path: relPath, device, viewport }) => {
86
- try {
87
- const { entry, reloaded, gotoNote } = await openSession({ projectId, workspaceRoot, sessionId, relPath, device, viewport });
88
- const f = entry.frame;
89
- const lines = [
90
- `${reloaded ? 'Reloaded' : 'Opened'} ${entry.target.relPath} (${entry.target.kind}) at viewport ${entry.viewport.width}×${entry.viewport.height}.`,
91
- `Screenshots are ${f.w}×${f.h}${f.scale < 1 ? ` (viewport scaled ×${f.scale.toFixed(2)} to fit the vision limit — you still just use screenshot pixels)` : ' (1:1 with the viewport)'}, ≈${visionTokens(f.w, f.h)} tokens each.`,
92
- entry.viaHttp ? 'Loaded over http, same origin as the user preview.' : null,
93
- degradedNote(entry),
94
- gotoNote,
95
- 'Next: artifact_computer screenshot to look, artifact_find to get refs, or artifact_batch to do several steps at once.',
96
- ].filter(Boolean);
97
- return asText(lines.join('\n'));
98
- } catch (err) {
99
- return asText(`artifact_open 失败:${err.message}`, true);
100
- }
101
- },
102
- );
103
- }
104
-
105
- export function makeArtifactComputerTool({ projectId }) {
106
- return tool(
107
- 'artifact_computer',
108
- `Pointer, keyboard and pixel-level capture on the page open in the artifact
109
- session (artifact_open first). Same actions as browser_computer, aimed at YOUR
110
- artifact: click through your own UI, fill your own forms, play your own game
111
- with real key presses, drag your own sliders, hover to reveal your own tooltips,
112
- zoom into your own small text.
113
-
114
- Coordinates are pixels of the session screenshot (origin top-left; size is
115
- reported by artifact_open and by every screenshot). After zoom, coordinates
116
- are STILL full-screenshot pixels. Or target an element by ref from
117
- artifact_find.
118
-
119
- ${ACTIONS_DOC}
120
-
121
- One action per call. For a sequence (click field → type → Enter → look) use
122
- artifact_batch. To MEASURE the state you reached (easing filmstrip, computed
123
- styles, cascade), call screenshot_canvas / trace_motion / get_computed_styles /
124
- explain_style / query_elements with live:true.`,
125
- COMPUTER_SCHEMA,
126
- async (a) => {
127
- try {
128
- return await withSession(projectId, async (entry) => {
129
- const { page } = entry;
130
- const before = page.url();
131
- // 站内导航(点了 about.html)也要报:判据同 browser_computer —— 主帧发出导航请求
132
- let navStarted = false;
133
- const onReq = (req) => { try { if (req.isNavigationRequest() && req.frame() === page.mainFrame()) navStarted = true; } catch { /* */ } };
134
- page.on('request', onReq);
135
- let r;
136
- try {
137
- r = await runAction(page, a, { frame: entry.frame, shot: (_p, lead) => sessionShot(entry, lead) });
138
- } catch (err) {
139
- page.off('request', onReq);
140
- return asText(actionErrorText(a.action, err), true);
141
- }
142
- if (/click|key|type/.test(a.action) && !page.isClosed()) await page.waitForTimeout(250);
143
- page.off('request', onReq);
144
- if (!page.isClosed() && (navStarted || page.url() !== before)) {
145
- await page.waitForLoadState('domcontentloaded', { timeout: 8000 }).catch(() => {});
146
- }
147
- if (!page.isClosed() && page.url() !== before) {
148
- const title = await page.title().catch(() => '');
149
- let where = page.url();
150
- try { where = decodeURIComponent(new URL(where).pathname.replace(/^.*\/artifact-file\//, '')); } catch { /* */ }
151
- r.content.unshift({ type: 'text', text: `→ page changed: ${title || '(no title)'} — ${where} (refs from before are gone)` });
152
- }
153
- const note = await changedNote(entry);
154
- if (note) r.content.push({ type: 'text', text: note });
155
- return r;
156
- });
157
- } catch (err) {
158
- return asText(`artifact_computer 失败:${err.message}`, true);
159
- }
160
- },
161
- );
162
- }
163
-
164
- export function makeArtifactFindTool({ projectId }) {
165
- return tool(
166
- 'artifact_find',
167
- `Find elements on the page open in the artifact session by describing them
168
- ("submit button", "price input", "第二张卡片 链接"). Returns up to 20 matches
169
- with refs (ref_N) for artifact_computer and their screenshot coordinates.
170
- Matching is LEXICAL (visible text / aria-label / placeholder / alt / role /
171
- href), not semantic — use the words that appear on the element. You wrote this
172
- page, so a CSS selector via query_elements is often just as good; refs shine for
173
- canvas/WebGL UIs and anything you'd otherwise have to click by pixel. Refs die
174
- on reload/navigation.`,
175
- {
176
- query: z.string().min(1).max(120).describe('Words shown on the element plus an optional role word.'),
177
- limit: z.number().int().min(1).max(20).optional().describe('Max matches (default 20).'),
178
- },
179
- async ({ query, limit }) => {
180
- try {
181
- return await withSession(projectId, async (entry) => {
182
- const r = await findInPage(entry.page, { query, limit: limit ?? 20 });
183
- const s = entry.frame.scale;
184
- if (s < 1) {
185
- r.matches = r.matches.map(m => ({ ...m, x: Math.round(m.x * s), y: Math.round(m.y * s), w: Math.round(m.w * s), h: Math.round(m.h * s) }));
186
- }
187
- const lines = formatMatches(r, query).map(l => l.replace('browser_computer', 'artifact_computer').replace('browser_read', 'query_elements').replace('browser_find', 'artifact_find'));
188
- const note = await changedNote(entry);
189
- return asText([...lines, note].filter(Boolean).join('\n'));
190
- });
191
- } catch (err) {
192
- return asText(`artifact_find 失败:${err.message}`, true);
193
- }
194
- },
195
- );
196
- }
197
-
198
- /**
199
- * artifact_motion:自己的产物"靠什么在动"——跟 browser_capture 的 motion 档**同一份引擎**
200
- * (engine/motion/inventory.js),只是对着产物页。默认新开一只可复现;live:true 对着会话页
201
- * (会真滚一遍再滚回顶,reveal 会被触发 —— 查交互态之前先想清楚要不要)。
202
- */
203
- export function makeArtifactMotionTool({ projectId, workspaceRoot, sessionId }) {
204
- return tool(
205
- 'artifact_motion',
206
- `Inventory of WHAT moves on your artifact page and how: every stylesheet
207
- scanned for @keyframes / animation / transition / CSS scroll-driven animation
208
- (animation-timeline, scroll(), view()) / scroll-snap / sticky / reduced-motion;
209
- the browser's running animations (getAnimations: durations, easings, keyframes);
210
- motion libraries (GSAP + each ScrollTrigger's start/end/scrub/pin, Lenis, AOS…);
211
- and a REAL wheel scroll through the page that tells apart reveal-on-scroll
212
- elements (with their transition), scrub/parallax elements, and scroll hijacking.
213
-
214
- Same engine as browser_capture{kinds:['motion']} on reference sites — so you can
215
- compare "theirs vs mine" on equal terms: did my reveals actually wire up, is my
216
- ScrollTrigger range what I meant, did I forget prefers-reduced-motion. Pure
217
- declarations in your CSS you can Read; this tells you what the BROWSER sees at
218
- runtime, which is what the user sees.
219
-
220
- Fresh load by default (reproducible). live:true runs it on the artifact session's
221
- current page (it scrolls the page and returns to top — reveals get triggered).
222
- For how the motion LOOKS use screenshot_canvas frames; for numbers use trace_motion.`,
223
- {
224
- path: z.string().optional().describe(CANVAS_PATH_DESC),
225
- device: z.enum(['desktop', 'tablet', 'mobile']).optional().describe('SITE ONLY, fresh-load mode: viewport width to inventory at (default desktop).'),
226
- live: z.boolean().optional().describe(LIVE_PARAM_DESC),
227
- },
228
- async ({ path: relPath, device, live }) => {
229
- let acq;
230
- try {
231
- const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
232
- if (!target || !target.ok) return asText(target?.message || 'No artifact found — pass path.', true);
233
- const guard = requireBrowsable(target);
234
- if (guard) return asText(guard, true);
235
- const vp = await defaultViewportFor(target, device);
236
- acq = await acquireArtifactPage({ projectId, workspaceRoot, target, live, viewport: vp });
237
- const inv = await collectMotionInventory(acq.page);
238
- const lines = [
239
- `motion inventory — ${target.relPath} @ ${acq.viewport.width}x${acq.viewport.height}`,
240
- degradedNote(acq), acq.liveNote, acq.gotoNote, '',
241
- ...formatMotionInventory(inv),
242
- '', '→ to SEE it: screenshot_canvas { frames:[2-30 ms offsets], trigger/click or live:true }; to MEASURE a curve: trace_motion.',
243
- ].filter(l => l != null);
244
- return asText(lines.join('\n'));
245
- } catch (err) {
246
- return asText(`artifact_motion 失败:${err.message}`, true);
247
- } finally {
248
- await acq?.release?.();
249
- }
250
- },
251
- );
252
- }
253
-
254
- /** artifact_batch 能跑的:会话四件 + 五个能 live 的量具(它们在 batch 里要自己带 live:true) */
255
- export const ARTIFACT_BATCHABLE = [
256
- 'artifact_open', 'artifact_computer', 'artifact_find', 'artifact_motion',
257
- 'screenshot_canvas', 'trace_motion', 'get_computed_styles', 'explain_style', 'query_elements',
258
- ];
259
-
260
- export function makeArtifactBatchTool({ tools }) {
261
- const names = ARTIFACT_BATCHABLE.filter(n => tools.some(t => t.name === n));
262
- return makeBatchTool({
263
- name: 'artifact_batch',
264
- tools,
265
- batchable: ARTIFACT_BATCHABLE,
266
- finalShot: { name: 'artifact_computer', input: { action: 'screenshot' } },
267
- description: `Run several artifact-session steps in ONE round trip: open → click → type →
268
- key → look, or click → trace_motion(live:true) → screenshot. Items run in order
269
- and STOP at the first error; later items get "${HALT_TEXT}". A session
270
- screenshot is appended at the end unless the last item already produced an
271
- image (or screenshotAfter:false). Measuring tools inside a batch need
272
- live:true in their input or they will open their own fresh page. Coordinates
273
- refer to the screenshot you saw BEFORE the call. Batchable: ${names.join(', ')}.
274
- Example: [{"name":"artifact_open","input":{}},{"name":"artifact_find","input":{"query":"start button"}},{"name":"artifact_computer","input":{"action":"left_click","ref":"ref_1"}},{"name":"artifact_computer","input":{"action":"key","text":"ArrowRight","repeat":10}},{"name":"screenshot_canvas","input":{"live":true,"frames":[0,120,240,480]}}]`,
275
- });
276
- }
277
-
278
- export { peekSession };
1
+ /**
2
+ * mcp/tools/artifact-session.js — 产物会话四件:artifact_open / artifact_computer /
3
+ * artifact_find / artifact_batch(2026-08-21)
4
+ *
5
+ * 成品检查的交互半边。以前七个感知工具全是一次性的(开一只新浏览器、量、关),
6
+ * "点开菜单 → 填表 → 提交 → 看校验"只能靠 beforeShot 塞 JS 假装;hover 态、焦点
7
+ * 顺序、方向键连按、拖拽、pointer lock 这些只认真实手势。现在:
8
+ *
9
+ * artifact_open 打开(或重载)一个产物到常驻会话,状态跨调用留着
10
+ * artifact_computer browser_computer 同一套动作(runAction 共享一份),对着会话页
11
+ * artifact_find 词法找元 → ref(refs.js 共享一份)
12
+ * artifact_batch 一次往返跑一串(makeBatchTool 共享一份合同)
13
+ *
14
+ * 老工具(screenshot_canvas / trace_motion / get_computed_styles / explain_style /
15
+ * query_elements)加 `live:true` 就对着会话里**现在这一页**量,胶片条/示波器/级联
16
+ * 解释全能骑在交互态上 —— 那是它们各自独有、browser use 没有的量具。
17
+ *
18
+ * 坐标:产物视口按形态定(站 1440×900、deck 1920×1080…),截图可能被归一化缩过,
19
+ * 会话记 frame={w,h,scale},模型读到的坐标 ÷ scale = 页面像素。artifact_open 的
20
+ * 返回会把截图尺寸说清楚。
21
+ */
22
+
23
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
24
+ import { z } from 'zod';
25
+ import { openSession, withSession, changedSinceOpen, peekSession, defaultViewportFor } from '../../perception/session.js';
26
+ import { findInPage, formatMatches } from '../../browse/refs.js';
27
+ import { runAction, ACTIONS_DOC, COMPUTER_SCHEMA, actionErrorText } from './browse-computer.js';
28
+ import { makeBatchTool, HALT_TEXT } from './browse-find-batch.js';
29
+ import { normalizeShot, visionTokens } from './helpers/shot-pipeline.js';
30
+ import { degradedNote } from './helpers/perception-page.js';
31
+ import { acquireArtifactPage, LIVE_PARAM_DESC } from './helpers/acquire-page.js';
32
+ import { collectMotionInventory, formatMotionInventory } from '../../motion/inventory.js';
33
+ import { CANVAS_PATH_DESC, resolveCanvasTarget, requireBrowsable } from '../../../lib/artifact-target.js';
34
+
35
+ const asText = (text, isError = false) => ({ content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) });
36
+
37
+ /** 会话页截图(不存桌面卡 —— 那是浏览通道的事);caption 说清截图空间 */
38
+ async function sessionShot(entry, lead) {
39
+ const buf = await entry.page.screenshot({ type: 'png' });
40
+ const shot = await normalizeShot(buf);
41
+ const f = entry.frame;
42
+ return {
43
+ content: [
44
+ { type: 'text', text: [lead, `${f.w}×${f.h} (${f.scale < 1 ? `viewport ${entry.viewport.width}×${entry.viewport.height} scaled ×${f.scale.toFixed(2)}; ` : ''}coordinates you use next are these screenshot pixels, ≈${visionTokens(f.w, f.h)} tokens)`, shot.note].filter(Boolean).join(' · ') },
45
+ { type: 'image', data: shot.data, mimeType: shot.mimeType },
46
+ ],
47
+ };
48
+ }
49
+
50
+ async function changedNote(entry) {
51
+ const changed = await changedSinceOpen(entry);
52
+ if (!changed.length) return null;
53
+ const shown = changed.slice(0, 5).join(', ');
54
+ return `⚠ files changed since this page was opened (${shown}${changed.length > 5 ? ` +${changed.length - 5}` : ''}) — the page is showing the OLD version. artifact_open again to reload (state resets).`;
55
+ }
56
+
57
+ export function makeArtifactOpenTool({ projectId, workspaceRoot, sessionId }) {
58
+ return tool(
59
+ 'artifact_open',
60
+ `Open one of your artifacts (a site page, a deck, a game) in a persistent
61
+ session browser — the starting point for interactive checking. Unlike
62
+ screenshot_canvas (fresh load every call), the page stays open between calls:
63
+ menus you open stay open, text you type stays typed, a game keeps its state.
64
+ Then use artifact_computer (click / type / keys / drag / scroll / zoom /
65
+ screenshot), artifact_find (element refs), artifact_batch (several steps in one
66
+ round trip), and the measuring tools with live:true (screenshot_canvas filmstrip,
67
+ trace_motion, get_computed_styles, explain_style, query_elements) to inspect
68
+ the CURRENT interactive state.
69
+
70
+ Opening the same file again reloads it (state resets). Opening a different file
71
+ or device replaces the session. The page is loaded over http like the user
72
+ preview (fetch/localStorage behave the same). After you edit files the open page
73
+ is stale — every artifact_* result reminds you; reopen to see the new version.
74
+
75
+ Returns the screenshot size: coordinates you pass to artifact_computer are
76
+ pixels of that screenshot (a deck at 1920×1080 is scaled down to fit the vision
77
+ limit; the mapping is handled for you).`,
78
+ {
79
+ path: z.string().optional().describe(CANVAS_PATH_DESC),
80
+ device: z.enum(['desktop', 'tablet', 'mobile']).optional()
81
+ .describe('SITE ONLY. Real device width to render at: desktop=1440, tablet=834, mobile=390. Default desktop.'),
82
+ viewport: z.object({ width: z.number().int().min(320).max(2560), height: z.number().int().min(240).max(2560) }).optional()
83
+ .describe('Explicit viewport; defaults to the artifact kind (site → device width × 900, deck → its aspect).'),
84
+ },
85
+ async ({ path: relPath, device, viewport }) => {
86
+ try {
87
+ const { entry, reloaded, gotoNote } = await openSession({ projectId, workspaceRoot, sessionId, relPath, device, viewport });
88
+ const f = entry.frame;
89
+ const lines = [
90
+ `${reloaded ? 'Reloaded' : 'Opened'} ${entry.target.relPath} (${entry.target.kind}) at viewport ${entry.viewport.width}×${entry.viewport.height}.`,
91
+ `Screenshots are ${f.w}×${f.h}${f.scale < 1 ? ` (viewport scaled ×${f.scale.toFixed(2)} to fit the vision limit — you still just use screenshot pixels)` : ' (1:1 with the viewport)'}, ≈${visionTokens(f.w, f.h)} tokens each.`,
92
+ entry.viaHttp ? 'Loaded over http, same origin as the user preview.' : null,
93
+ degradedNote(entry),
94
+ gotoNote,
95
+ 'Next: artifact_computer screenshot to look, artifact_find to get refs, or artifact_batch to do several steps at once.',
96
+ ].filter(Boolean);
97
+ return asText(lines.join('\n'));
98
+ } catch (err) {
99
+ return asText(`artifact_open 失败:${err.message}`, true);
100
+ }
101
+ },
102
+ );
103
+ }
104
+
105
+ export function makeArtifactComputerTool({ projectId }) {
106
+ return tool(
107
+ 'artifact_computer',
108
+ `Pointer, keyboard and pixel-level capture on the page open in the artifact
109
+ session (artifact_open first). Same actions as browser_computer, aimed at YOUR
110
+ artifact: click through your own UI, fill your own forms, play your own game
111
+ with real key presses, drag your own sliders, hover to reveal your own tooltips,
112
+ zoom into your own small text.
113
+
114
+ Coordinates are pixels of the session screenshot (origin top-left; size is
115
+ reported by artifact_open and by every screenshot). After zoom, coordinates
116
+ are STILL full-screenshot pixels. Or target an element by ref from
117
+ artifact_find.
118
+
119
+ ${ACTIONS_DOC}
120
+
121
+ One action per call. For a sequence (click field → type → Enter → look) use
122
+ artifact_batch. To MEASURE the state you reached (easing filmstrip, computed
123
+ styles, cascade), call screenshot_canvas / trace_motion / get_computed_styles /
124
+ explain_style / query_elements with live:true.`,
125
+ COMPUTER_SCHEMA,
126
+ async (a) => {
127
+ try {
128
+ return await withSession(projectId, async (entry) => {
129
+ const { page } = entry;
130
+ const before = page.url();
131
+ // 站内导航(点了 about.html)也要报:判据同 browser_computer —— 主帧发出导航请求
132
+ let navStarted = false;
133
+ const onReq = (req) => { try { if (req.isNavigationRequest() && req.frame() === page.mainFrame()) navStarted = true; } catch { /* */ } };
134
+ page.on('request', onReq);
135
+ let r;
136
+ try {
137
+ r = await runAction(page, a, { frame: entry.frame, shot: (_p, lead) => sessionShot(entry, lead) });
138
+ } catch (err) {
139
+ page.off('request', onReq);
140
+ return asText(actionErrorText(a.action, err), true);
141
+ }
142
+ if (/click|key|type/.test(a.action) && !page.isClosed()) await page.waitForTimeout(250);
143
+ page.off('request', onReq);
144
+ if (!page.isClosed() && (navStarted || page.url() !== before)) {
145
+ await page.waitForLoadState('domcontentloaded', { timeout: 8000 }).catch(() => {});
146
+ }
147
+ if (!page.isClosed() && page.url() !== before) {
148
+ const title = await page.title().catch(() => '');
149
+ let where = page.url();
150
+ try { where = decodeURIComponent(new URL(where).pathname.replace(/^.*\/artifact-file\//, '')); } catch { /* */ }
151
+ r.content.unshift({ type: 'text', text: `→ page changed: ${title || '(no title)'} — ${where} (refs from before are gone)` });
152
+ }
153
+ const note = await changedNote(entry);
154
+ if (note) r.content.push({ type: 'text', text: note });
155
+ return r;
156
+ });
157
+ } catch (err) {
158
+ return asText(`artifact_computer 失败:${err.message}`, true);
159
+ }
160
+ },
161
+ );
162
+ }
163
+
164
+ export function makeArtifactFindTool({ projectId }) {
165
+ return tool(
166
+ 'artifact_find',
167
+ `Find elements on the page open in the artifact session by describing them
168
+ ("submit button", "price input", "第二张卡片 链接"). Returns up to 20 matches
169
+ with refs (ref_N) for artifact_computer and their screenshot coordinates.
170
+ Matching is LEXICAL (visible text / aria-label / placeholder / alt / role /
171
+ href), not semantic — use the words that appear on the element. You wrote this
172
+ page, so a CSS selector via query_elements is often just as good; refs shine for
173
+ canvas/WebGL UIs and anything you'd otherwise have to click by pixel. Refs die
174
+ on reload/navigation.`,
175
+ {
176
+ query: z.string().min(1).max(120).describe('Words shown on the element plus an optional role word.'),
177
+ limit: z.number().int().min(1).max(20).optional().describe('Max matches (default 20).'),
178
+ },
179
+ async ({ query, limit }) => {
180
+ try {
181
+ return await withSession(projectId, async (entry) => {
182
+ const r = await findInPage(entry.page, { query, limit: limit ?? 20 });
183
+ const s = entry.frame.scale;
184
+ if (s < 1) {
185
+ r.matches = r.matches.map(m => ({ ...m, x: Math.round(m.x * s), y: Math.round(m.y * s), w: Math.round(m.w * s), h: Math.round(m.h * s) }));
186
+ }
187
+ const lines = formatMatches(r, query).map(l => l.replace('browser_computer', 'artifact_computer').replace('browser_read', 'query_elements').replace('browser_find', 'artifact_find'));
188
+ const note = await changedNote(entry);
189
+ return asText([...lines, note].filter(Boolean).join('\n'));
190
+ });
191
+ } catch (err) {
192
+ return asText(`artifact_find 失败:${err.message}`, true);
193
+ }
194
+ },
195
+ );
196
+ }
197
+
198
+ /**
199
+ * artifact_motion:自己的产物"靠什么在动"——跟 browser_capture 的 motion 档**同一份引擎**
200
+ * (engine/motion/inventory.js),只是对着产物页。默认新开一只可复现;live:true 对着会话页
201
+ * (会真滚一遍再滚回顶,reveal 会被触发 —— 查交互态之前先想清楚要不要)。
202
+ */
203
+ export function makeArtifactMotionTool({ projectId, workspaceRoot, sessionId }) {
204
+ return tool(
205
+ 'artifact_motion',
206
+ `Inventory of WHAT moves on your artifact page and how: every stylesheet
207
+ scanned for @keyframes / animation / transition / CSS scroll-driven animation
208
+ (animation-timeline, scroll(), view()) / scroll-snap / sticky / reduced-motion;
209
+ the browser's running animations (getAnimations: durations, easings, keyframes);
210
+ motion libraries (GSAP + each ScrollTrigger's start/end/scrub/pin, Lenis, AOS…);
211
+ and a REAL wheel scroll through the page that tells apart reveal-on-scroll
212
+ elements (with their transition), scrub/parallax elements, and scroll hijacking.
213
+
214
+ Same engine as browser_capture{kinds:['motion']} on reference sites — so you can
215
+ compare "theirs vs mine" on equal terms: did my reveals actually wire up, is my
216
+ ScrollTrigger range what I meant, did I forget prefers-reduced-motion. Pure
217
+ declarations in your CSS you can Read; this tells you what the BROWSER sees at
218
+ runtime, which is what the user sees.
219
+
220
+ Fresh load by default (reproducible). live:true runs it on the artifact session's
221
+ current page (it scrolls the page and returns to top — reveals get triggered).
222
+ For how the motion LOOKS use screenshot_canvas frames; for numbers use trace_motion.`,
223
+ {
224
+ path: z.string().optional().describe(CANVAS_PATH_DESC),
225
+ device: z.enum(['desktop', 'tablet', 'mobile']).optional().describe('SITE ONLY, fresh-load mode: viewport width to inventory at (default desktop).'),
226
+ live: z.boolean().optional().describe(LIVE_PARAM_DESC),
227
+ },
228
+ async ({ path: relPath, device, live }) => {
229
+ let acq;
230
+ try {
231
+ const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
232
+ if (!target || !target.ok) return asText(target?.message || 'No artifact found — pass path.', true);
233
+ const guard = requireBrowsable(target);
234
+ if (guard) return asText(guard, true);
235
+ const vp = await defaultViewportFor(target, device);
236
+ acq = await acquireArtifactPage({ projectId, workspaceRoot, target, live, viewport: vp });
237
+ const inv = await collectMotionInventory(acq.page);
238
+ const lines = [
239
+ `motion inventory — ${target.relPath} @ ${acq.viewport.width}x${acq.viewport.height}`,
240
+ degradedNote(acq), acq.liveNote, acq.gotoNote, '',
241
+ ...formatMotionInventory(inv),
242
+ '', '→ to SEE it: screenshot_canvas { frames:[2-30 ms offsets], trigger/click or live:true }; to MEASURE a curve: trace_motion.',
243
+ ].filter(l => l != null);
244
+ return asText(lines.join('\n'));
245
+ } catch (err) {
246
+ return asText(`artifact_motion 失败:${err.message}`, true);
247
+ } finally {
248
+ await acq?.release?.();
249
+ }
250
+ },
251
+ );
252
+ }
253
+
254
+ /** artifact_batch 能跑的:会话四件 + 五个能 live 的量具(它们在 batch 里要自己带 live:true) */
255
+ export const ARTIFACT_BATCHABLE = [
256
+ 'artifact_open', 'artifact_computer', 'artifact_find', 'artifact_motion',
257
+ 'screenshot_canvas', 'trace_motion', 'get_computed_styles', 'explain_style', 'query_elements',
258
+ ];
259
+
260
+ export function makeArtifactBatchTool({ tools }) {
261
+ const names = ARTIFACT_BATCHABLE.filter(n => tools.some(t => t.name === n));
262
+ return makeBatchTool({
263
+ name: 'artifact_batch',
264
+ tools,
265
+ batchable: ARTIFACT_BATCHABLE,
266
+ finalShot: { name: 'artifact_computer', input: { action: 'screenshot' } },
267
+ description: `Run several artifact-session steps in ONE round trip: open → click → type →
268
+ key → look, or click → trace_motion(live:true) → screenshot. Items run in order
269
+ and STOP at the first error; later items get "${HALT_TEXT}". A session
270
+ screenshot is appended at the end unless the last item already produced an
271
+ image (or screenshotAfter:false). Measuring tools inside a batch need
272
+ live:true in their input or they will open their own fresh page. Coordinates
273
+ refer to the screenshot you saw BEFORE the call. Batchable: ${names.join(', ')}.
274
+ Example: [{"name":"artifact_open","input":{}},{"name":"artifact_find","input":{"query":"start button"}},{"name":"artifact_computer","input":{"action":"left_click","ref":"ref_1"}},{"name":"artifact_computer","input":{"action":"key","text":"ArrowRight","repeat":10}},{"name":"screenshot_canvas","input":{"live":true,"frames":[0,120,240,480]}}]`,
275
+ });
276
+ }
277
+
278
+ export { peekSession };