@xiaobuyu/nodesign 0.0.1
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.
- package/LICENSE +661 -0
- package/README.md +140 -0
- package/bin/nodesign.js +176 -0
- package/package.json +104 -0
- package/server/api/README.md +45 -0
- package/server/api/_guard.js +75 -0
- package/server/api/admin.js +187 -0
- package/server/api/assets/docx-page.js +103 -0
- package/server/api/assets/helpers.js +22 -0
- package/server/api/assets/notes.js +144 -0
- package/server/api/assets.js +896 -0
- package/server/api/board.js +57 -0
- package/server/api/browse.js +138 -0
- package/server/api/canvas.js +444 -0
- package/server/api/chatai.js +324 -0
- package/server/api/exports/build-standalone.js +980 -0
- package/server/api/exports/cards.js +118 -0
- package/server/api/exports/docx-pdf.js +46 -0
- package/server/api/exports/export-page.js +142 -0
- package/server/api/exports/handoff.js +147 -0
- package/server/api/exports.js +690 -0
- package/server/api/instruction.js +75 -0
- package/server/api/local.js +115 -0
- package/server/api/me.js +116 -0
- package/server/api/memory.js +185 -0
- package/server/api/pending-changes.js +366 -0
- package/server/api/plugins.js +164 -0
- package/server/api/projects.js +220 -0
- package/server/api/publish.js +64 -0
- package/server/api/recent.js +73 -0
- package/server/api/sessions.js +589 -0
- package/server/api/skills.js +77 -0
- package/server/api/standalone-fit.js +168 -0
- package/server/api/turn-compose.js +180 -0
- package/server/api/turn-inflight.js +44 -0
- package/server/api/turn.js +669 -0
- package/server/auth/README.md +17 -0
- package/server/auth/middleware.js +138 -0
- package/server/auth/origin-guard.js +71 -0
- package/server/auth/session.js +108 -0
- package/server/auth/tier.js +113 -0
- package/server/auth/users-store.js +348 -0
- package/server/edit/README.md +29 -0
- package/server/engine/README.md +113 -0
- package/server/engine/agent/agent-shared.js +550 -0
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
- package/server/engine/agent/auto-mode-rules.js +78 -0
- package/server/engine/agent/context.js +418 -0
- package/server/engine/agent/events.js +464 -0
- package/server/engine/agent/hooks/canvas-validate.js +199 -0
- package/server/engine/agent/hooks/failure.js +120 -0
- package/server/engine/agent/hooks/file-events.js +75 -0
- package/server/engine/agent/hooks/lifecycle.js +233 -0
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
- package/server/engine/agent/hooks/post-guidance.js +206 -0
- package/server/engine/agent/hooks/post-subagent-report.js +104 -0
- package/server/engine/agent/hooks/post-trim.js +58 -0
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
- package/server/engine/agent/hooks/pre-defaults.js +84 -0
- package/server/engine/agent/hooks/pre-injectors.js +277 -0
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
- package/server/engine/agent/hooks/pre-starter-files.js +61 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
- package/server/engine/agent/hooks/site-validate.js +94 -0
- package/server/engine/agent/hooks/tool-prompts.js +31 -0
- package/server/engine/agent/hooks/turn-state-memory.js +56 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
- package/server/engine/agent/hooks.js +354 -0
- package/server/engine/agent/init-contract.js +69 -0
- package/server/engine/agent/isolation.js +159 -0
- package/server/engine/agent/model-context.js +359 -0
- package/server/engine/agent/model-table.js +292 -0
- package/server/engine/agent/plugin-loader.js +266 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
- package/server/engine/agent/prompts/tools/site-reference.md +106 -0
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
- package/server/engine/agent/session-loop.js +1024 -0
- package/server/engine/agent/session-model.js +164 -0
- package/server/engine/agent/skill.js +204 -0
- package/server/engine/agent/system-prompts.js +86 -0
- package/server/engine/agent/task-events.js +78 -0
- package/server/engine/agents/ds-extractor.md +132 -0
- package/server/engine/agents/explorer.md +190 -0
- package/server/engine/agents/index.js +230 -0
- package/server/engine/agents/schemas/design-system.json +156 -0
- package/server/engine/agents/schemas/tweak-schema.json +113 -0
- package/server/engine/agents/tweak-proposer.md +127 -0
- package/server/engine/agents/vision-checker.md +254 -0
- package/server/engine/browse/capture.js +422 -0
- package/server/engine/browse/card.js +137 -0
- package/server/engine/browse/handover.js +59 -0
- package/server/engine/browse/page-digest.js +119 -0
- package/server/engine/browse/refs.js +196 -0
- package/server/engine/browse/registry.js +307 -0
- package/server/engine/browse/screencast.js +170 -0
- package/server/engine/browse/state.js +121 -0
- package/server/engine/chatai/chat-log.js +79 -0
- package/server/engine/chatai/index.js +193 -0
- package/server/engine/chatai/openai-compat.js +152 -0
- package/server/engine/chatai/orchestrate.js +309 -0
- package/server/engine/chatai/perform.js +40 -0
- package/server/engine/chatai/summarize.js +79 -0
- package/server/engine/mcp/capability-gate.js +52 -0
- package/server/engine/mcp/index.js +339 -0
- package/server/engine/mcp/param-sanitizer.js +142 -0
- package/server/engine/mcp/tools/arrange-on-board.js +124 -0
- package/server/engine/mcp/tools/artifact-session.js +278 -0
- package/server/engine/mcp/tools/browse-computer.js +421 -0
- package/server/engine/mcp/tools/browse-find-batch.js +183 -0
- package/server/engine/mcp/tools/browse-screenshot.js +157 -0
- package/server/engine/mcp/tools/browse.js +480 -0
- package/server/engine/mcp/tools/build-docx.js +101 -0
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
- package/server/engine/mcp/tools/create-on-board.js +155 -0
- package/server/engine/mcp/tools/crystallize-skill.js +168 -0
- package/server/engine/mcp/tools/deliver-files.js +0 -0
- package/server/engine/mcp/tools/explain-style.js +237 -0
- package/server/engine/mcp/tools/export-handoff.js +133 -0
- package/server/engine/mcp/tools/expose-tweaks.js +149 -0
- package/server/engine/mcp/tools/generate-image.js +842 -0
- package/server/engine/mcp/tools/get-computed-styles.js +164 -0
- package/server/engine/mcp/tools/get-pending-changes.js +205 -0
- package/server/engine/mcp/tools/h3box-ssh.js +84 -0
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
- package/server/engine/mcp/tools/helpers/rembg.js +273 -0
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
- package/server/engine/mcp/tools/highlight.js +68 -0
- package/server/engine/mcp/tools/list-pages.js +222 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -0
- package/server/engine/mcp/tools/navigate-to-page.js +60 -0
- package/server/engine/mcp/tools/organize-board.js +64 -0
- package/server/engine/mcp/tools/paint-still.js +434 -0
- package/server/engine/mcp/tools/pin-to-board.js +130 -0
- package/server/engine/mcp/tools/preview-deck.js +130 -0
- package/server/engine/mcp/tools/profile-scroll.js +305 -0
- package/server/engine/mcp/tools/publish-site.js +102 -0
- package/server/engine/mcp/tools/query-elements.js +195 -0
- package/server/engine/mcp/tools/read-board.js +103 -0
- package/server/engine/mcp/tools/read-document.js +93 -0
- package/server/engine/mcp/tools/read-page.js +264 -0
- package/server/engine/mcp/tools/read-tavern-json.js +143 -0
- package/server/engine/mcp/tools/record-decision.js +140 -0
- package/server/engine/mcp/tools/relate-on-board.js +110 -0
- package/server/engine/mcp/tools/remove-background.js +391 -0
- package/server/engine/mcp/tools/report-issue.js +115 -0
- package/server/engine/mcp/tools/roll-film.js +266 -0
- package/server/engine/mcp/tools/screenshot-docx.js +138 -0
- package/server/engine/mcp/tools/screenshot-url.js +195 -0
- package/server/engine/mcp/tools/screenshot.js +583 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -0
- package/server/engine/mcp/tools/trace-motion.js +215 -0
- package/server/engine/mcp/tools/web-search.js +548 -0
- package/server/engine/motion/inventory.js +349 -0
- package/server/engine/perception/session.js +235 -0
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
- 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 -0
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
- package/server/engine/runs/active-runs.js +698 -0
- package/server/engine/runs/live-turn.js +252 -0
- package/server/engine/runs/store.js +364 -0
- package/server/engine/runs/turn-relay.js +217 -0
- package/server/engine/runtime/workspace.js +164 -0
- package/server/index.js +235 -0
- package/server/lib/artifact-file-path.js +80 -0
- package/server/lib/artifact-target.js +284 -0
- package/server/lib/asset-refs.js +239 -0
- package/server/lib/async-queue.js +104 -0
- package/server/lib/auto-relations.js +130 -0
- package/server/lib/binding-types.js +60 -0
- package/server/lib/board-kind-sizes.js +62 -0
- package/server/lib/board-relations.js +133 -0
- package/server/lib/browse-proxy.js +180 -0
- package/server/lib/canvas-id.js +36 -0
- package/server/lib/cover.js +227 -0
- package/server/lib/danbooru-tags.js +291 -0
- package/server/lib/doc-extract.js +156 -0
- package/server/lib/docx/build-from-source.js +260 -0
- package/server/lib/docx/build.js +490 -0
- package/server/lib/docx/dump-styles.js +287 -0
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
- package/server/lib/docx/merge-runs.js +129 -0
- package/server/lib/docx/numbering.js +218 -0
- package/server/lib/docx/order.js +152 -0
- package/server/lib/docx/rawzip.js +172 -0
- package/server/lib/docx/render.js +92 -0
- package/server/lib/docx/text-lint.js +225 -0
- package/server/lib/docx/tokens.js +335 -0
- package/server/lib/docx/units.js +29 -0
- package/server/lib/docx/xml.js +291 -0
- package/server/lib/docx-pages.js +187 -0
- package/server/lib/export-collect.js +289 -0
- package/server/lib/export-package.js +210 -0
- package/server/lib/html-srcset.js +145 -0
- package/server/lib/image-variant.js +460 -0
- package/server/lib/ingress/forward-openai-chat.js +305 -0
- package/server/lib/ingress/openai-chat.js +491 -0
- package/server/lib/ingress/session-notice.js +66 -0
- package/server/lib/ingress/session-routes.js +68 -0
- package/server/lib/ingress/slot-probe.js +130 -0
- package/server/lib/ingress/upstream-billing.js +52 -0
- package/server/lib/ingress/upstream-fail-streak.js +72 -0
- package/server/lib/ingress/upstream-truncation.js +48 -0
- package/server/lib/issues-store.js +182 -0
- package/server/lib/kinds/deck.js +72 -0
- package/server/lib/kinds/docx.js +283 -0
- package/server/lib/kinds/file-kinds.js +113 -0
- package/server/lib/kinds/index.js +202 -0
- package/server/lib/kinds/site.js +161 -0
- package/server/lib/model-ingress.js +543 -0
- package/server/lib/moderation.js +255 -0
- package/server/lib/notice-store.js +103 -0
- package/server/lib/plugin-install.js +104 -0
- package/server/lib/plugin-validator.js +721 -0
- package/server/lib/publish-store.js +115 -0
- package/server/lib/quick-summary.js +52 -0
- package/server/lib/quota.js +270 -0
- package/server/lib/rate-window.js +29 -0
- package/server/lib/reference-assets.js +92 -0
- package/server/lib/region-shot.js +135 -0
- package/server/lib/safe-path.js +73 -0
- package/server/lib/sdk-session.js +33 -0
- package/server/lib/showcase-store.js +92 -0
- package/server/lib/site-publish.js +465 -0
- package/server/lib/ssrf-guard.js +432 -0
- package/server/lib/task-scan.js +158 -0
- package/server/lib/tavern-json.js +154 -0
- package/server/lib/video-variant.js +237 -0
- package/server/lib/workspace-path.js +33 -0
- package/server/ops/fix-sdk-musl.mjs +40 -0
- package/server/ops/install-macos-fonts.sh +114 -0
- package/server/ops/macos-fonts.conf +336 -0
- package/server/ops/sandbox-shim/bwrap +57 -0
- package/server/projects/assets-summary.js +119 -0
- package/server/projects/auto-name.js +53 -0
- package/server/projects/board-store.js +640 -0
- package/server/projects/move-entry.js +103 -0
- package/server/projects/store.js +270 -0
- package/server/projects/ui-config.js +54 -0
- package/server/projects/workspace-templates.js +53 -0
- package/server/projects/workspace.js +1025 -0
- package/server/runtime/capabilities.js +153 -0
- package/server/runtime/local-config.js +177 -0
- package/server/runtime/local-env.js +88 -0
- package/server/runtime/platform.js +342 -0
- package/server/runtime/profile.js +72 -0
- package/server/services/rembg-launcher.js +313 -0
- package/server/services/rembg-service.py +334 -0
- package/server/shared/README.md +25 -0
- package/server/shared/deck.js +54 -0
- package/server/shared/time.js +49 -0
- package/server/style-pipeline/README.md +53 -0
- package/server/ws/broker.js +36 -0
- package/server/ws/browse-channel.js +177 -0
- package/server/ws/index.js +386 -0
- package/web/README.md +89 -0
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
- package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
- package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
- package/web/dist/assets/again-D8tfgKKO.webp +0 -0
- package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
- package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
- package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
- package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
- package/web/dist/assets/index-C0KUf6Wd.css +1 -0
- package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
- package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
- package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
- package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
- package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
- package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
- package/web/dist/assets/question-8H4HSqxh.webp +0 -0
- package/web/dist/assets/reject-BaPJqElj.webp +0 -0
- package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
- package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
- package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
- package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
- package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
- package/web/dist/assets/thisone-BylZKglV.webp +0 -0
- package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
- package/web/dist/index.html +18 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/ws/index.js — WebSocket 升级入口
|
|
3
|
+
*
|
|
4
|
+
* URL 模式:/ws/projects/:pid
|
|
5
|
+
*
|
|
6
|
+
* 流程:
|
|
7
|
+
* 1. http server 'upgrade' 事件触发
|
|
8
|
+
* 2. parse URL 取 pid
|
|
9
|
+
* 3. validateProjectId + getProject 校验存在
|
|
10
|
+
* 4. wss.handleUpgrade → 拿到 ws 对象
|
|
11
|
+
* 5. 订阅该 project 的 EventBus,事件 JSON.stringify 推 ws.send
|
|
12
|
+
* 6. ping/pong 30s 心跳
|
|
13
|
+
* 7. ws close → unsubscribe + clearInterval
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { WebSocketServer } from 'ws';
|
|
17
|
+
import { createBrowseWS } from './browse-channel.js';
|
|
18
|
+
import { URL } from 'url';
|
|
19
|
+
import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
|
|
20
|
+
import { validateProjectId, getProject } from '../projects/store.js';
|
|
21
|
+
import { getSessionWorkspace, validateSessionId } from '../projects/workspace.js';
|
|
22
|
+
import { withConfigDir } from '../lib/sdk-session.js';
|
|
23
|
+
import { platform } from '../runtime/platform.js';
|
|
24
|
+
import { getProjectBus } from './broker.js';
|
|
25
|
+
import { requestUser } from '../auth/session.js';
|
|
26
|
+
import { originAllowed } from '../auth/origin-guard.js';
|
|
27
|
+
import { userOwnsProject } from '../api/_guard.js';
|
|
28
|
+
import {
|
|
29
|
+
getCurrentTurnRunId,
|
|
30
|
+
hasActiveQuerySession,
|
|
31
|
+
closeQuerySession,
|
|
32
|
+
markSessionActivity,
|
|
33
|
+
} from '../engine/runs/active-runs.js';
|
|
34
|
+
import { getLiveTurnSnapshot } from '../engine/runs/live-turn.js';
|
|
35
|
+
|
|
36
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
37
|
+
const HYDRATE_CHUNK_SIZE = 50;
|
|
38
|
+
const GLOBAL_CLAUDE_CONFIG_DIR = platform.claudeConfigDir;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* WS 全部断开后再 N ms 仍无重连 → closeQuerySession 让 SDK subprocess 退出,
|
|
42
|
+
* 防止用户关 tab / 刷新后服务器孤儿 SDK process 永久占内存(每个 ~250MB RSS)。
|
|
43
|
+
*
|
|
44
|
+
* 默认 60s 给前端 ws-client.js exponential backoff(1s→2s→...→30s)足够重连窗口。
|
|
45
|
+
* env NODESIGN_WS_GRACE_MS 可调(生产可拉到 5min 给移动端弱网)。
|
|
46
|
+
*/
|
|
47
|
+
const WS_GRACE_MS = Number(process.env.NODESIGN_WS_GRACE_MS) || 60_000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* sid → { count: 当前活跃 WS 连接数, graceTimer: 0 引用时启动的关闭定时器 }
|
|
51
|
+
*
|
|
52
|
+
* 每条带 sid 的 WS 连接 ref++,close 时 ref--;归零启 grace timer,N ms 内有新 WS
|
|
53
|
+
* 进同 sid 立即清 timer 续命;timer 到期再确认 0 ref 后 closeQuerySession。
|
|
54
|
+
*
|
|
55
|
+
* /work 路径(无 sid)的 WS 不进 sessionRefs(无 session 可绑)。
|
|
56
|
+
*/
|
|
57
|
+
const sessionRefs = new Map();
|
|
58
|
+
|
|
59
|
+
function refSession(sid) {
|
|
60
|
+
if (!sid) return;
|
|
61
|
+
let entry = sessionRefs.get(sid);
|
|
62
|
+
if (!entry) {
|
|
63
|
+
entry = { count: 0, graceTimer: null };
|
|
64
|
+
sessionRefs.set(sid, entry);
|
|
65
|
+
}
|
|
66
|
+
entry.count += 1;
|
|
67
|
+
if (entry.graceTimer) {
|
|
68
|
+
clearTimeout(entry.graceTimer);
|
|
69
|
+
entry.graceTimer = null;
|
|
70
|
+
}
|
|
71
|
+
// WS 连上算一次活跃信号(idle scan 不会立即把刚连的 session 当僵尸关掉)
|
|
72
|
+
markSessionActivity(sid);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unrefSession(sid) {
|
|
76
|
+
if (!sid) return;
|
|
77
|
+
const entry = sessionRefs.get(sid);
|
|
78
|
+
if (!entry) return;
|
|
79
|
+
entry.count = Math.max(0, entry.count - 1);
|
|
80
|
+
if (entry.count > 0) return;
|
|
81
|
+
// 0 ref → 启 grace timer。若 grace 期内有新 WS 进同 sid,refSession 会清掉 timer
|
|
82
|
+
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
83
|
+
const onGraceExpired = () => {
|
|
84
|
+
const cur = sessionRefs.get(sid);
|
|
85
|
+
if (!cur || cur.count > 0) return; // 期间有新连上,不关
|
|
86
|
+
// 进行中的 turn 不杀(2026-07-27):用户关 tab / 切走时后端应该把活干完,
|
|
87
|
+
// 前端重连后靠 hydrate + live_turn 快照接回状态。这正是"前端丢失后端工作
|
|
88
|
+
// 状态"要保住的那半 —— 老逻辑 60s 就 cancel 在跑的 turn,是真丢工作。
|
|
89
|
+
// 续一个 grace 周期再查;turn 结束后仍无订阅者才真正关(idle SDK 进程回收)。
|
|
90
|
+
if (getCurrentTurnRunId(sid)) {
|
|
91
|
+
console.info(`[ws] grace expired for sid=${sid.slice(0, 8)} but turn in flight — deferring close`);
|
|
92
|
+
cur.graceTimer = setTimeout(onGraceExpired, WS_GRACE_MS);
|
|
93
|
+
cur.graceTimer.unref?.();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
sessionRefs.delete(sid);
|
|
97
|
+
if (hasActiveQuerySession(sid)) {
|
|
98
|
+
console.info(`[ws] grace expired for sid=${sid.slice(0, 8)}, closing session (no_active_subscriber)`);
|
|
99
|
+
closeQuerySession(sid, 'no_active_subscriber');
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
entry.graceTimer = setTimeout(onGraceExpired, WS_GRACE_MS);
|
|
103
|
+
entry.graceTimer.unref?.();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function setupWS(httpServer) {
|
|
107
|
+
// 浏览器画面/输入的专用通道(2026-08-18),独立 WebSocketServer
|
|
108
|
+
const browseWS = createBrowseWS();
|
|
109
|
+
// perMessageDeflate:弱网下 hydrate chunk + ring-buffer replay 帧成本是 raw size
|
|
110
|
+
// 量级(>2MB session 一次首屏 hydrate 估 250KB+ raw)。permessage-deflate 让
|
|
111
|
+
// JSON 文本压到 5-10x。threshold 1KB 跳过小帧(控制帧无收益反增 CPU)。
|
|
112
|
+
// env NODESIGN_WS_DISABLE_DEFLATE=1 应急关闭(CPU 紧的部署环境)。
|
|
113
|
+
const wss = new WebSocketServer({
|
|
114
|
+
noServer: true,
|
|
115
|
+
perMessageDeflate: process.env.NODESIGN_WS_DISABLE_DEFLATE === '1'
|
|
116
|
+
? false
|
|
117
|
+
: { threshold: 1024 },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
121
|
+
// ⛔ CSRF 闸(2026-08-18):cookie 是 SameSite=Lax,而已发布站点与应用主机
|
|
122
|
+
// 同 eTLD+1 = 同站,Lax 照发(真跑验过,见 auth/origin-guard.js 文件头)。
|
|
123
|
+
// 这里**不学下面那个 4401 的握手后再关**:那套是为了让我们自己的前端看得见
|
|
124
|
+
// 原因好停止重连;外站页面不需要体面的错误,403 直接掐在升级前最省。
|
|
125
|
+
if (!originAllowed(req)) {
|
|
126
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
127
|
+
return socket.destroy();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 登录墙(2026-07-30 多用户):解析身份而非布尔。cookie 无效不再写 HTTP 401
|
|
131
|
+
// 拒 upgrade —— 浏览器拿不到 upgrade 阶段的状态码,只见 close 1006,前端
|
|
132
|
+
// ws-client 会无限指数退避重连。改为完成握手后 close(4401),前端把它列为
|
|
133
|
+
// fatal code → 停止重连并回登录页。
|
|
134
|
+
const user = requestUser(req);
|
|
135
|
+
|
|
136
|
+
let url;
|
|
137
|
+
try {
|
|
138
|
+
url = new URL(req.url || '/', 'http://x');
|
|
139
|
+
} catch {
|
|
140
|
+
return socket.destroy();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ⚠️ 浏览器画面通道的分支**必须排在下面那个 404 之前** —— 那句 404 是兜底,
|
|
144
|
+
// 不匹配 `/ws/projects/:pid` 的路径会被它直接毙掉。
|
|
145
|
+
// 为什么另开一条而不是复用这条:这条是纯下行 + EventBus 广播 + 2000 条重放
|
|
146
|
+
// 缓冲,高频画面帧灌进去会冲爆重放缓冲、混进 hydrate 回放,而且 JSON+base64
|
|
147
|
+
// 帧会被 perMessageDeflate 白压一遍。详见 ws/browse-channel.js 文件头。
|
|
148
|
+
{
|
|
149
|
+
const bm = browseWS.matches(url.pathname);
|
|
150
|
+
if (bm) return browseWS.accept(req, socket, head, bm);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const m = url.pathname.match(/^\/ws\/projects\/([^/]+)$/);
|
|
154
|
+
if (!m) {
|
|
155
|
+
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
156
|
+
return socket.destroy();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const pid = decodeURIComponent(m[1]);
|
|
160
|
+
try {
|
|
161
|
+
validateProjectId(pid);
|
|
162
|
+
} catch {
|
|
163
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
164
|
+
return socket.destroy();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 身份或归属不对 → 握手后 4401 关闭(同一个 code:外人不该分得清
|
|
168
|
+
// "没登录"和"不是你的项目")
|
|
169
|
+
const project = getProject(pid);
|
|
170
|
+
if (!user || !userOwnsProject(user, project)) {
|
|
171
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
172
|
+
ws.close(4401, 'unauthorized');
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ?since=N — 客户端最后看到的 EventBus seq;server 通过 buffer 回放 (since, _seq] 段
|
|
178
|
+
// 第一次连不带 since → since=0 → 不 replay 直接 live。
|
|
179
|
+
const sinceRaw = url.searchParams.get('since');
|
|
180
|
+
const since = sinceRaw != null ? Math.max(0, parseInt(sinceRaw, 10) || 0) : 0;
|
|
181
|
+
|
|
182
|
+
// Phase A.4:?sid=<sessionId> — 客户端当前在哪个 session 上,server 在 first connect /
|
|
183
|
+
// gap 时推 ws.hydrate.* 帧补完整 messages。无 sid 时(/work 路径)跳过 hydrate
|
|
184
|
+
// 直接 replay+live。
|
|
185
|
+
let sid = url.searchParams.get('sid');
|
|
186
|
+
if (sid) {
|
|
187
|
+
try { validateSessionId(sid); }
|
|
188
|
+
catch { sid = null; }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
192
|
+
handleProjectWS(ws, pid, since, sid);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return wss;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 读 jsonl 历史(异步,几百 ms)。发帧是另一个纯同步函数 —— 读完到发出之间不能
|
|
201
|
+
* 再有 await,否则"读到的历史"与"当时的 live turn 快照"会错位(见 handleProjectWS)。
|
|
202
|
+
*/
|
|
203
|
+
async function loadHydrate(pid, sid) {
|
|
204
|
+
const sessionRoot = getSessionWorkspace(pid, sid);
|
|
205
|
+
return withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
|
|
206
|
+
getSessionMessages(sid, { dir: sessionRoot, includeSystemMessages: false }),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 本轮由快照权威:jsonl 里属于当前 turn 的 assistant 条目全部裁掉。
|
|
212
|
+
*
|
|
213
|
+
* SDK 按 block 逐条写 jsonl(thinking / text / tool_use 各一行),一个 turn 跑到
|
|
214
|
+
* 一半时前面几条已经落盘 —— 那些内容同时也在 live_turn 快照里(快照从 run.start
|
|
215
|
+
* 折叠整轮)。两边 id 体系不同(jsonl 是 `<uuid>:text:<n>`,快照是 `<runId>:mN`),
|
|
216
|
+
* 前端去不了重,正文就渲染两遍。这是用户报的"重复传"的根因,100% 复现。
|
|
217
|
+
*
|
|
218
|
+
* 只裁 assistant:user 条目(用户自己那句话)快照里没有,裁了就真丢了。掉队的
|
|
219
|
+
* tool_result 会因为找不到对应 tool_use 被 sessionMessagesToDisplay 自然跳过。
|
|
220
|
+
*/
|
|
221
|
+
function dropInFlightTurn(messages, startedAt) {
|
|
222
|
+
if (!startedAt) return messages;
|
|
223
|
+
const cut = Date.parse(startedAt);
|
|
224
|
+
if (Number.isNaN(cut)) return messages;
|
|
225
|
+
return messages.filter(m => !(
|
|
226
|
+
m?.type === 'assistant' && m.timestamp && Date.parse(m.timestamp) >= cut
|
|
227
|
+
));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Phase A.4:推 ws.hydrate.start/chunk/end 帧把 jsonl 历史 messages 同步给前端。
|
|
232
|
+
* 必须在 subscribeFromSeq 之前发,确保前端先 hydrate 后 apply replay/live events。
|
|
233
|
+
* 纯同步。
|
|
234
|
+
*/
|
|
235
|
+
function sendHydrateFrames(ws, sid, messages, asOfSeq) {
|
|
236
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
237
|
+
const total = messages.length;
|
|
238
|
+
ws.send(JSON.stringify({
|
|
239
|
+
type: 'ws.hydrate.start',
|
|
240
|
+
sessionId: sid,
|
|
241
|
+
total,
|
|
242
|
+
asOfSeq,
|
|
243
|
+
ts: new Date().toISOString(),
|
|
244
|
+
}));
|
|
245
|
+
for (let i = 0; i < total; i += HYDRATE_CHUNK_SIZE) {
|
|
246
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
247
|
+
ws.send(JSON.stringify({
|
|
248
|
+
type: 'ws.hydrate.chunk',
|
|
249
|
+
sessionId: sid,
|
|
250
|
+
chunkIdx: Math.floor(i / HYDRATE_CHUNK_SIZE),
|
|
251
|
+
messages: messages.slice(i, i + HYDRATE_CHUNK_SIZE),
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
255
|
+
ws.send(JSON.stringify({ type: 'ws.hydrate.end', sessionId: sid, total }));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** hydrate 读失败 fail-soft:前端收到后兜底走 HTTP Sessions.read */
|
|
259
|
+
function sendHydrateError(ws, sid, message) {
|
|
260
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
261
|
+
try {
|
|
262
|
+
ws.send(JSON.stringify({
|
|
263
|
+
type: 'ws.hydrate.start', sessionId: sid, kind: 'error', error: message,
|
|
264
|
+
}));
|
|
265
|
+
} catch { /* ignore */ }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function handleProjectWS(ws, pid, since = 0, sid = null) {
|
|
269
|
+
const bus = getProjectBus(pid);
|
|
270
|
+
|
|
271
|
+
// 诊断 log:用户重启服务器后报"前端收不到事件"——这条 log 让 pm2 logs 一眼看出
|
|
272
|
+
// WS 是不是真连上 + 客户端传的 sid 跟 server 端 active session 状态对不对。
|
|
273
|
+
console.info(
|
|
274
|
+
`[ws] connect pid=${pid} sid=${sid ? sid.slice(0, 8) : 'none'} `
|
|
275
|
+
+ `hasActiveSession=${sid ? hasActiveQuerySession(sid) : 'n/a'} since=${since}`,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// sid lifecycle ref:带 sid 的 WS 连上即 ref++,close/error 时 unref。0 ref 触发
|
|
279
|
+
// grace timer N ms 后 closeQuerySession 让 SDK subprocess 自然退出。
|
|
280
|
+
refSession(sid);
|
|
281
|
+
|
|
282
|
+
// ── 连接协议(2026-07-27 重构:快照 + 尾随,每次连接都全量重建)──
|
|
283
|
+
//
|
|
284
|
+
// ① jsonl hydrate — 已完成 turn 的历史(turn 边界 flush 的持久层)
|
|
285
|
+
// ② ws.live_turn 快照 — 进行中 turn 的物化重建(live-turn.js 同步折叠,
|
|
286
|
+
// 覆盖"刷新 / 断线期间错过的全部流式内容")
|
|
287
|
+
// ③ 尾随订阅 — 从快照 seq 起 replay ring buffer + 切 live
|
|
288
|
+
//
|
|
289
|
+
// 老协议的 ?since= 游标 + gap 补 hydrate 废弃:ring buffer(2000) 对 token 级
|
|
290
|
+
// streaming 几秒断线就 gap,gap 后 hydrate 帧晚于 replay/live 到达,前端
|
|
291
|
+
// hydrate.end 整体替换 messages 把刚渲染的内容洗掉。新协议 hydrate 永远先发,
|
|
292
|
+
// 快照覆盖 ring 之外的一切,ring 只需覆盖"读 jsonl 的几百 ms"同步窗口。
|
|
293
|
+
// `since` 参数保留解析但忽略(旧前端兼容)。
|
|
294
|
+
//
|
|
295
|
+
// 两份历史的边界(2026-07-28):jsonl 与快照对"进行中的 turn"是重叠的 ——
|
|
296
|
+
// 读 jsonl 与取快照必须在**同一个同步区**里完成并一起发出,中间不能 await,
|
|
297
|
+
// 否则 turn 在缝里结束会让两边错位。规则:本轮由快照权威,hydrate 裁掉本轮。
|
|
298
|
+
const seqAtHydrateStart = bus._seq || 0;
|
|
299
|
+
let loaded = null;
|
|
300
|
+
let loadErr = null;
|
|
301
|
+
const t0 = Date.now();
|
|
302
|
+
if (sid) {
|
|
303
|
+
try { loaded = await loadHydrate(pid, sid); }
|
|
304
|
+
catch (err) { loadErr = err; }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ↓↓↓ 同步区开始(不要在这里插 await)↓↓↓
|
|
308
|
+
const snapshot = sid ? getLiveTurnSnapshot(sid) : null;
|
|
309
|
+
if (sid) {
|
|
310
|
+
if (loadErr) {
|
|
311
|
+
console.warn(`[ws] hydrate failed for ${pid}/${sid}:`, loadErr.message);
|
|
312
|
+
sendHydrateError(ws, sid, loadErr.message);
|
|
313
|
+
} else {
|
|
314
|
+
const trimmed = snapshot ? dropInFlightTurn(loaded, snapshot.startedAt) : loaded;
|
|
315
|
+
console.info(
|
|
316
|
+
`[ws] hydrate sid=${sid.slice(0, 8)} loaded ${loaded.length} messages in ${Date.now() - t0}ms`
|
|
317
|
+
+ (snapshot ? ` (本轮 ${loaded.length - trimmed.length} 条交给快照)` : ''),
|
|
318
|
+
);
|
|
319
|
+
sendHydrateFrames(ws, sid, trimmed, seqAtHydrateStart);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (snapshot && ws.readyState === ws.OPEN) {
|
|
323
|
+
try {
|
|
324
|
+
ws.send(JSON.stringify({ type: 'ws.live_turn', sessionId: sid, ...snapshot }));
|
|
325
|
+
} catch (err) {
|
|
326
|
+
console.warn(`[ws] live_turn send failed for ${pid}:`, err.message);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// ↑↑↑ 同步区结束 ↑↑↑
|
|
330
|
+
|
|
331
|
+
// sid 过滤:projectBuses per-project 共享,只推本 session 事件 + 无 sessionId
|
|
332
|
+
// 的全局事件。sid=null(/work 路径)时 session-scoped 事件全部不推 ——
|
|
333
|
+
// 老行为"收全部"会把别的 session 的 delta 渲进空 chat(串台);/work 没有
|
|
334
|
+
// session,本来就不该收 run 流,新建 session 后前端会带 sid 重连。
|
|
335
|
+
const subscribeSince = snapshot ? snapshot.seq : seqAtHydrateStart;
|
|
336
|
+
const { unsubscribe, replayed, gap } = bus.subscribeFromSeq(subscribeSince, (event) => {
|
|
337
|
+
if (event.sessionId && event.sessionId !== sid) return;
|
|
338
|
+
if (ws.readyState === ws.OPEN) {
|
|
339
|
+
try {
|
|
340
|
+
ws.send(JSON.stringify(event));
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.warn(`[ws] send failed for ${pid}:`, err.message);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const heartbeat = setInterval(() => {
|
|
348
|
+
if (ws.readyState === ws.OPEN) {
|
|
349
|
+
try { ws.ping(); } catch { /* will close */ }
|
|
350
|
+
}
|
|
351
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
352
|
+
|
|
353
|
+
let cleanedUp = false;
|
|
354
|
+
const cleanup = () => {
|
|
355
|
+
if (cleanedUp) return;
|
|
356
|
+
cleanedUp = true;
|
|
357
|
+
clearInterval(heartbeat);
|
|
358
|
+
unsubscribe();
|
|
359
|
+
unrefSession(sid);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
ws.on('close', cleanup);
|
|
363
|
+
ws.on('error', (err) => {
|
|
364
|
+
console.warn(`[ws] error for ${pid}:`, err.message);
|
|
365
|
+
cleanup();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// 确认 + replay 元信息(gap=true 客户端可决定全量 hydrate)。
|
|
369
|
+
// 故意放在 replay 之后发:客户端看到 ws.connected 就知道 backlog 已 drain,可切回正常 live 状态。
|
|
370
|
+
//
|
|
371
|
+
// activeRunId:sid 上当前在跑的 turn runId(无则 null)。前端用它重连后恢复
|
|
372
|
+
// isStreaming/currentRunId —— 否则 WS 抖动期间 run.start 已发完且 buffer 没新事件
|
|
373
|
+
// 时,前端永远不知道 run 还活着(stop 按钮消失,UX 表现"流没了")。
|
|
374
|
+
const activeRunId = sid ? getCurrentTurnRunId(sid) : null;
|
|
375
|
+
try {
|
|
376
|
+
ws.send(JSON.stringify({
|
|
377
|
+
type: 'ws.connected',
|
|
378
|
+
projectId: pid,
|
|
379
|
+
ts: new Date().toISOString(),
|
|
380
|
+
since,
|
|
381
|
+
replayed,
|
|
382
|
+
gap,
|
|
383
|
+
activeRunId,
|
|
384
|
+
}));
|
|
385
|
+
} catch { /* immediate close edge */ }
|
|
386
|
+
}
|
package/web/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Nodesign Web — P1 前端骨架
|
|
2
|
+
|
|
3
|
+
Nodesign 工作台前端。三栏布局:chat / canvas / context。
|
|
4
|
+
|
|
5
|
+
## 当前阶段:P1(前端骨架)
|
|
6
|
+
|
|
7
|
+
参考 plan:`/Users/edy/.claude/plans/plan-parallel-harbor.md`
|
|
8
|
+
|
|
9
|
+
## 跑起来
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd Nodesign/web
|
|
13
|
+
npm install
|
|
14
|
+
npm run dev # 起在 5174(避开 dev/ 的 5173)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
打开 `http://localhost:5174/`。
|
|
18
|
+
|
|
19
|
+
## P1 验证清单
|
|
20
|
+
|
|
21
|
+
打开浏览器后逐项确认:
|
|
22
|
+
|
|
23
|
+
1. ✅ Home 页(`/`)显示 5 个 mock 项目 + 两个入口卡片(自由创作 / 参照模式)
|
|
24
|
+
2. ✅ 点项目进入 `/projects/proj-002`("Nodesign 内部介绍")三栏页
|
|
25
|
+
3. ✅ 三栏布局:左 360px chat / 中 auto canvas / 右 340px context panel
|
|
26
|
+
4. ✅ 顶栏:亮黑系 + 项目名 breadcrumb + Share/Export/⋯ 按钮
|
|
27
|
+
5. ✅ Canvas iframe 加载 `/mock/deck.html`(5 页 DeskSkill 风格 self-contained HTML)
|
|
28
|
+
6. ✅ Canvas Toolbar 三模式切换(Edit / Preview / Code)
|
|
29
|
+
7. ✅ **Edit 模式下双击 iframe 内的文字** → 进入 contenteditable,blur 后控制台 log 出 patch
|
|
30
|
+
8. ✅ Preview 模式下不能编辑(双击无效)
|
|
31
|
+
9. ✅ Code 模式下 Monaco 显示 HTML 源码(只读)
|
|
32
|
+
10. ✅ Inputs tab:拖文件 / 点击选文件 / 粘贴 URL(FileReader 本地预览,不发请求)
|
|
33
|
+
11. ✅ SystemTab 显示 mock spec 摘要(metaphor / intent / outline 列表)
|
|
34
|
+
12. ✅ ChatPanel:mock 6 条对话历史,输消息 + 发送(800ms 后伪 mock 回执)
|
|
35
|
+
13. ✅ 视觉跟 dev/ 完全一致(亮黑按钮 / 深棕标题 / cubic-bezier 入退场)
|
|
36
|
+
14. ✅ 路由:Home ↔ Project ↔ DesignSystemList 占位 ↔ SkillList 占位 切换流畅
|
|
37
|
+
|
|
38
|
+
## 目录结构
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
src/
|
|
42
|
+
├── main.jsx # ReactDOM 入口
|
|
43
|
+
├── App.jsx # 路由根(react-router-dom 7 createBrowserRouter)
|
|
44
|
+
├── routes/
|
|
45
|
+
│ ├── Home.jsx
|
|
46
|
+
│ ├── Project.jsx # ★ 三栏组装
|
|
47
|
+
│ ├── DesignSystemList.jsx # 占位
|
|
48
|
+
│ └── SkillList.jsx # 占位
|
|
49
|
+
├── components/
|
|
50
|
+
│ ├── layout/ # AppShell / TopBar / ThreeColumnLayout
|
|
51
|
+
│ ├── chat/ # ChatPanel / MessageList / Message / ChatComposer
|
|
52
|
+
│ ├── canvas/ # CanvasFrame / Toolbar / HtmlIframe / EditOverlay / DirectEditBridge / CodeCanvas
|
|
53
|
+
│ ├── context-panel/ # ContextPanel / InputsTab / SystemTab
|
|
54
|
+
│ └── ui/ # 12+ 从 dev/src/components/ui copy 来的通用组件
|
|
55
|
+
├── lib/
|
|
56
|
+
│ ├── theme.js # ← 从 dev/src/constants/theme.js copy(COLOR / GAP / FONT_SIZE / cubic-bezier)
|
|
57
|
+
│ ├── html-utils.js # serializeAnchor / findElementByAnchor / ensureNodeId
|
|
58
|
+
│ └── helpers.js # newId / formatDate / timeAgo
|
|
59
|
+
├── hooks/ # (P2 加:useEngineRun / useHtmlArtifact / useIframeBridge)
|
|
60
|
+
├── stores/globalStore.js # zustand:toast / canvas mode / selectedAnchor
|
|
61
|
+
├── mock/
|
|
62
|
+
│ ├── deck-spec.js # mock DeckSpec
|
|
63
|
+
│ └── projects.js # mock 5 个项目
|
|
64
|
+
└── styles/globals.css # 极小 reset
|
|
65
|
+
|
|
66
|
+
public/
|
|
67
|
+
└── mock/
|
|
68
|
+
└── deck.html # ★ 5 页 self-contained HTML(DeskSkill 风格)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 设计原则(来自 plan-parallel-harbor)
|
|
72
|
+
|
|
73
|
+
1. **不污染**:v0.7.5 是参考不是约束
|
|
74
|
+
2. **探索 → 反向优化**:Nodesign 反向给 skill 提需求
|
|
75
|
+
3. **spec 与 HTML 角色分离**:
|
|
76
|
+
- spec:agent 生成阶段的 commitment device + 上下文记忆,**用户不直接编辑**
|
|
77
|
+
- HTML:产物 + 用户局部交互的修改对象(comment / direct edit / slider / 未来 CAD 都改 HTML)
|
|
78
|
+
4. **视觉沿用 DeskSkill**:亮黑 #2d2418 / 深棕 #3a2a18 / F9F8F6 / cubic-bezier(0.25, 1, 0.5, 1)
|
|
79
|
+
5. **复用 dev/src**:12+ UI 组件直接搬
|
|
80
|
+
|
|
81
|
+
## 下一阶段:P2 前端交互层
|
|
82
|
+
|
|
83
|
+
- inline comment UI(点元素弹气泡 + 评论列表)
|
|
84
|
+
- inputs tab 上传后真预览(FileReader 显示截图缩略图)
|
|
85
|
+
- Code mode 改 HTML(本地 state,blur 触发 iframe reload)
|
|
86
|
+
- tweak slider UI 占位(mock schema)
|
|
87
|
+
- 项目 CRUD(localStorage 持久化)
|
|
88
|
+
|
|
89
|
+
P3 起接后端 WS。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import{c as _,r as i,au as R,av as ee,j as t,C as r,F as y,b as $,G as u,aw as te,Z as ne,at as N,e as S,a2 as G,ax as ie,ay as se,a7 as oe}from"./index-Cbl3ATvp.js";import{A as re,R as ae}from"./rotate-cw-LMT3x1Zj.js";/**
|
|
2
|
+
* @license lucide-react v0.468.0 - ISC
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the ISC license.
|
|
5
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/const I=_("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/**
|
|
7
|
+
* @license lucide-react v0.468.0 - ISC
|
|
8
|
+
*
|
|
9
|
+
* This source code is licensed under the ISC license.
|
|
10
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
11
|
+
*/const le=_("PowerOff",[["path",{d:"M18.36 6.64A9 9 0 0 1 20.77 15",key:"dxknvb"}],["path",{d:"M6.16 6.16a9 9 0 1 0 12.68 12.68",key:"1x7qb5"}],["path",{d:"M12 2v4",key:"3427ic"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]]);function J(c,x){const h=c.getBoundingClientRect();return{rx:Math.min(1,Math.max(0,(x.clientX-h.left)/h.width)),ry:Math.min(1,Math.max(0,(x.clientY-h.top)/h.height))}}function pe({projectId:c,url:x,help:h,onClose:m,onToolbarGroups:U}){const[w,d]=i.useState("connecting"),[A,g]=i.useState(null),[j,O]=i.useState(x||""),[a,M]=i.useState(!1),[H,X]=i.useState(!1),[T,B]=i.useState(null),[v,D]=i.useState(!1),[C,V]=i.useState([]),[F,Y]=i.useState(null),[E,Z]=i.useState(!0),z=i.useRef(null),L=i.useRef(null),q=i.useRef(!1);q.current=a;const l=i.useCallback(e=>{const n=L.current;n&&n.readyState===1&&n.send(JSON.stringify(e))},[]),K=i.useCallback(async()=>{D(!0),g(null);try{const e=await R.open(c);O(e.url||""),d("connecting"),l({type:"subscribe"})}catch(e){d("error"),g((e==null?void 0:e.message)||"打不开")}finally{D(!1)}},[c,l]);i.useEffect(()=>{const e=window.location.protocol==="https:"?"wss:":"ws:",n=new WebSocket(`${e}//${window.location.host}/ws/projects/${encodeURIComponent(c)}/browser`);n.binaryType="arraybuffer",L.current=n,n.onopen=()=>{d("connecting"),n.send(JSON.stringify({type:"subscribe"}))},n.onclose=p=>{d(p.code===4401?"error":"closed"),p.code===4401&&g("没有权限")},n.onerror=()=>d("error"),n.onmessage=async p=>{var o,W;if(typeof p.data!="string"){const b=z.current;if(!b)return;try{const f=await createImageBitmap(new Blob([p.data],{type:"image/jpeg"}));(b.width!==f.width||b.height!==f.height)&&(b.width=f.width,b.height=f.height),(o=b.getContext("2d"))==null||o.drawImage(f,0,0),(W=f.close)==null||W.call(f),X(!0),d("live")}catch{}return}let s;try{s=JSON.parse(p.data)}catch{return}s.type==="subscribed"&&(d("live"),s.url&&O(s.url)),s.type==="hello"&&s.help&&B(s.help),s.type==="released"&&B(null),s.type==="idle"&&(d("idle"),g(s.reason)),s.type==="error"&&(d("error"),g(s.reason)),s.type==="closed"&&(d("closed"),g(s.reason)),s.type==="url"&&O(s.url||"")};const k=()=>l({type:document.hidden?"unsubscribe":"subscribe"});return document.addEventListener("visibilitychange",k),()=>{document.removeEventListener("visibilitychange",k);try{n.send(JSON.stringify({type:"unsubscribe"}))}catch{}n.close(),L.current=null}},[c,l]),i.useEffect(()=>{x&&O(x)},[x]),i.useEffect(()=>{let e=!0;return R.state(c).then(n=>{e&&V(Array.isArray(n==null?void 0:n.sites)?n.sites:[])}).catch(()=>{}),()=>{e=!1}},[c]),i.useEffect(()=>{var s;if(!a)return;const e=z.current;if(!e)return;const n=o=>{o.preventDefault(),l({type:"input",kind:"click",...J(e,o)})},k=o=>{o.preventDefault(),l({type:"input",kind:"wheel",...J(e,o),dx:o.deltaX,dy:o.deltaY})},p=o=>{if(!o.isComposing){if(o.key.length===1){o.preventDefault(),l({type:"input",kind:"key",text:o.key});return}["Enter","Backspace","Tab","Escape","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(o.key)&&(o.preventDefault(),l({type:"input",kind:"key",key:o.key}))}};return e.addEventListener("click",n),e.addEventListener("wheel",k,{passive:!1}),e.addEventListener("keydown",p),(s=e.focus)==null||s.call(e),()=>{e.removeEventListener("click",n),e.removeEventListener("wheel",k),e.removeEventListener("keydown",p)}},[a,l]);const Q=i.useMemo(()=>[{id:"addr",node:t.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:6,padding:`0 ${u.sm}px`,maxWidth:420,fontFamily:$,fontSize:y.xs,color:r.text2,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",userSelect:"text"},children:[t.jsx(te,{size:12,style:{flexShrink:0,opacity:.6}}),j||"(还没打开页面)"]})},{id:"nav",items:[{id:"back",icon:re,title:"后退",onClick:()=>l({type:"nav",action:"back"})},{id:"reload",icon:ae,title:"刷新",onClick:()=>l({type:"nav",action:"reload"})}]},{id:"end",items:[{id:"end",icon:le,title:"不逛了 —— 关掉浏览器并把桌面上那张卡收走(在站点上的登录留着)",onClick:async()=>{try{await R.end(c)}catch{}m==null||m()}}]},{id:"takeover",items:[{id:"hand",icon:a?I:ee,title:a?"好了继续 —— 把控制权交回 agent(它正等着)":"我来接手 —— 点击/滚动/打字会直接发到那个浏览器里",active:a,onClick:()=>{a?(l({type:"release"}),M(!1)):M(!0)}}]}],[j,a,l,c,m]),P={connecting:"连接中…",live:null,idle:A||"agent 还没开始浏览",error:A||"出错了",closed:A||"浏览器已经关了"}[w];return t.jsxs(ne,{kind:"browse",title:"agent 的浏览器",subtitle:a?"你在接手":w==="live"?"实时":null,onClose:m,groups:Q,onToolbarGroups:U,banner:h||T?t.jsxs("span",{children:[t.jsx("b",{children:"agent 需要你帮个手"}),":",h||T,"—— 点工具栏上的",t.jsx("b",{children:"手形按钮"}),"接手,弄完再点一次(变成 ▶)把控制权交回去。 它正停在那儿等你。"]}):null,contentStyle:{background:N.paper},children:[t.jsxs("div",{style:{flex:1,minHeight:0,width:"100%",display:"flex",alignItems:"center",justifyContent:"center",padding:u.md,boxSizing:"border-box",position:"relative"},children:[t.jsx("canvas",{ref:z,tabIndex:a?0:-1,title:a?"接手中:点一下这块画面再打字(键盘只在这里生效)":void 0,style:{maxWidth:"100%",maxHeight:"100%",display:H?"block":"none",background:"#fff",borderRadius:2,boxShadow:"0 2px 12px rgba(43,39,35,.18)",cursor:a?"pointer":"default",outline:a?`2px solid ${r.accent||"#8a4b2d"}`:"none",outlineOffset:3}}),(!H||P)&&t.jsxs("div",{style:{position:"absolute",inset:0,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:u.sm,fontFamily:S,fontSize:y.sm,color:r.text2,textAlign:"center"},children:[w==="connecting"&&t.jsx(G,{size:16,style:{animation:"spin 1s linear infinite"}}),t.jsx("span",{children:P||"等第一帧…"}),(w==="idle"||w==="closed")&&t.jsxs(t.Fragment,{children:[j&&t.jsxs("button",{type:"button",disabled:v,onClick:K,style:{display:"inline-flex",alignItems:"center",gap:6,padding:`${u.xs}px ${u.md}px`,cursor:v?"default":"pointer",fontFamily:S,fontSize:y.sm,color:r.text,background:N.paper,border:`1px solid ${r.border}`,borderRadius:3,opacity:v?.6:1},children:[v?t.jsx(G,{size:13,style:{animation:"spin 1s linear infinite"}}):t.jsx(I,{size:13}),v?"正在打开…":"打开上次那一页"]}),t.jsxs("span",{style:{color:r.sub,fontSize:y.xs,maxWidth:360,lineHeight:1.7},children:[j?"浏览器空闲 5 分钟会自己休息(登录态留着)。打开之后你可以直接接手操作,agent 下次也接着用这一页。":"让 agent 去看一个站(它有 browser_navigate),这里就会亮起来。",t.jsx("br",{}),"静止的页面不会一直传帧 —— 画面不动是正常的,不是卡住了。"]})]})]})]}),!!C.length&&t.jsxs("div",{style:{flexShrink:0,borderTop:`1px solid ${r.border}`,background:r.bgCard,maxHeight:E?220:30,overflow:"hidden",transition:"max-height .18s ease"},children:[t.jsxs("button",{type:"button",onClick:()=>Z(e=>!e),style:{display:"flex",alignItems:"center",gap:6,width:"100%",padding:`4px ${u.md}px`,cursor:"pointer",background:"transparent",border:0,color:r.text2,fontFamily:S,fontSize:y.xs},children:[t.jsx(ie,{size:12}),"采到的东西 · ",C.length," 个站 · ",C.reduce((e,n)=>e+n.count,0)," 件",t.jsx(se,{size:12,style:{marginLeft:"auto",opacity:.6,transform:E?"none":"rotate(-90deg)",transition:"transform .18s ease"}})]}),E&&t.jsx("div",{style:{display:"flex",gap:u.sm,padding:`0 ${u.md}px ${u.sm}px`,overflowX:"auto"},children:C.map(e=>t.jsxs("div",{style:{flexShrink:0,width:150},children:[t.jsxs("button",{type:"button",title:`${e.dir}(${e.count} 件)`,onClick:()=>Y(F===e.site?null:e.site),style:{display:"block",width:"100%",padding:0,cursor:"pointer",background:N.paper,border:`1px solid ${F===e.site?r.text:r.border}`,borderRadius:2,overflow:"hidden"},children:[e.cover?t.jsx("img",{alt:"",loading:"lazy",src:oe.artifactFileUrl(c,e.cover),style:{width:"100%",height:84,objectFit:"cover",objectPosition:"top center",display:"block"}}):t.jsx("div",{style:{height:84,display:"flex",alignItems:"center",justifyContent:"center",color:r.sub,fontFamily:S,fontSize:y.xxs},children:"没有截图"}),t.jsx("div",{style:{padding:"3px 5px",textAlign:"left",fontFamily:$,fontSize:y.xxs,color:r.text2,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},children:e.site})]}),F===e.site&&t.jsxs("div",{style:{paddingTop:3,fontFamily:S,fontSize:y.xxs,color:r.sub,lineHeight:1.6},children:[t.jsxs("span",{style:{fontFamily:$},children:[e.dir,"/"]}),t.jsx("br",{}),e.count," 件(截图 / 调色板 / 字体 / 结构 / CSS,类别在文件名里)"]})]},e.site))})]})]})}export{pe as default};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import{c as et,r as o,j as e,C as r,G as t,a as Ht,R as $,F as p,b as E,P as tt,u as N,X as ue,d as nt,e as z,f as st,T as Dt,g as _t,h as ke,B as It,i as rt,k as ze,U as Nt,l as it,S as qt,H as Ut,m as Gt,n as lt,o as Kt,p as Vt,q as Xt,s as ot,t as at,v as dt,w as Yt,x as Zt,y as ct,z as Ct,A as Jt,E as ut,D as Qt,I as en,J as Ke,K as tn,M as nn,L as sn,N as rn,O as ln,Q as on,V as an,W as dn,Y as cn,Z as un}from"./index-Cbl3ATvp.js";import{d as Ve,a as Xe,M as xn,o as pn,t as gn,E as fn,C as hn,G as mn,p as yn,D as bn,b as wn,c as vn,P as kn,I as jn,R as Sn,e as zn}from"./pending-edit-apply-DTjo2geW.js";import{E as $n,S as En}from"./square-dashed-mouse-pointer-CBoAkfhi.js";/**
|
|
2
|
+
* @license lucide-react v0.468.0 - ISC
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the ISC license.
|
|
5
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/const xt=et("MessageSquare",[["path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",key:"1lielz"}]]);/**
|
|
7
|
+
* @license lucide-react v0.468.0 - ISC
|
|
8
|
+
*
|
|
9
|
+
* This source code is licensed under the ISC license.
|
|
10
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
11
|
+
*/const Ln=et("ShieldCheck",[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]),Fn=1920,An=1080;function Tn({src:n,srcDoc:s,mode:i="edit",onSelect:l,onTextEdit:d,onIframeReady:f,zoom:m=1,deckW:c=Fn,deckH:y=An}){const u=o.useRef(null),a=o.useRef(!1),w=o.useRef(0),[x,v]=o.useState(!1);o.useEffect(()=>{v(!0)},[n,s]),o.useEffect(()=>{const b=u.current;if(!(!b||!a.current))return Ve(b),i==="edit"&&Xe(b,{onSelect:l,onTextEdit:d}),()=>Ve(b)},[i,l,d]),o.useEffect(()=>()=>{var b;try{const F=(b=u.current)==null?void 0:b.contentWindow;F&&typeof F.scrollY=="number"&&(w.current=F.scrollY)}catch{}},[n,s]);const k=()=>{a.current=!0;const b=u.current;if(b){if(f==null||f(b),i==="edit"&&Xe(b,{onSelect:l,onTextEdit:d}),w.current>0)try{requestAnimationFrame(()=>{var F;try{(F=b.contentWindow)==null||F.scrollTo(0,w.current)}catch{}})}catch{}requestAnimationFrame(()=>{requestAnimationFrame(()=>v(!1))})}};return e.jsx("div",{style:{flex:1,minHeight:0,overflow:"hidden",background:r.bgWhite,position:"relative",display:"flex",alignItems:"center",justifyContent:"center"},children:e.jsx("iframe",{ref:u,src:n,srcDoc:s,onLoad:k,sandbox:"allow-scripts allow-same-origin",style:{width:`${c}px`,height:`${y}px`,flexShrink:0,border:0,background:r.bgWhite,transform:m===1?"none":`scale(${m})`,transformOrigin:"center center",opacity:x?.5:1,transition:"opacity 180ms ease-out"}})})}function Pn({candidates:n,activeId:s,onSelect:i,onAdd:l,onRemove:d,onRename:f}){if(!n||n.length===0)return null;const m=n.length===1;return e.jsxs("div",{style:{flexShrink:0,height:36,borderBottom:`1px solid ${r.border}`,background:r.bgWhite,padding:`0 ${t.lg}px`,display:"flex",alignItems:"center",gap:t.xs,overflowX:"auto",overflowY:"hidden"},children:[e.jsx(Ht,{size:11,color:r.sub,style:{flexShrink:0,marginRight:t.xs}}),n.map(c=>{const y=c.id===s;return e.jsx(Wn,{candidate:c,active:y,onSelect:()=>i==null?void 0:i(c.id),onRemove:m?null:()=>d==null?void 0:d(c.id),onRename:f},c.id)}),e.jsxs("button",{onClick:l,title:"新建候选(mock 复制当前)",style:{flexShrink:0,padding:`${t.xs}px ${t.md}px`,display:"inline-flex",alignItems:"center",gap:t.xs,fontFamily:E,fontSize:p.xs,color:r.sub,background:"transparent",border:`1px dashed ${r.borderMd}`,borderRadius:$.sm,cursor:"pointer"},onMouseEnter:c=>{c.currentTarget.style.color=r.text2,c.currentTarget.style.background="rgba(0,0,0,0.03)"},onMouseLeave:c=>{c.currentTarget.style.color=r.sub,c.currentTarget.style.background="transparent"},children:[e.jsx(tt,{size:11})," 新候选"]}),e.jsx("div",{style:{flex:1}}),e.jsxs("span",{style:{fontFamily:E,fontSize:p.xs,color:r.sub,opacity:m?0:.7,flexShrink:0},children:[n.length," 个候选"]})]})}function Wn({candidate:n,active:s,onSelect:i,onRemove:l,onRename:d}){const[f,m]=o.useState(!1),c=N(w=>w.prompt),y=N(w=>w.confirm),u=async w=>{if(w.stopPropagation(),!d)return;const x=await c({title:"重命名候选",initialValue:n.label,placeholder:"候选名"});x&&x.trim()&&x!==n.label&&d(n.id,x.trim())},a=async w=>{w.stopPropagation(),await y({title:"删除候选",message:`删除候选「${n.label}」?`,confirmLabel:"删除",danger:!0})&&l()};return e.jsxs("div",{onMouseEnter:()=>m(!0),onMouseLeave:()=>m(!1),style:{flexShrink:0,position:"relative",display:"inline-flex",alignItems:"center",background:s?"rgba(45,36,24,0.06)":"transparent",borderRadius:$.sm,transition:"background 0.15s"},children:[e.jsx("button",{onClick:i,onDoubleClick:u,title:"双击重命名",style:{padding:`${t.xs}px ${l&&f?t.lg:t.md}px ${t.xs}px ${t.md}px`,fontFamily:E,fontSize:p.xs,fontWeight:s?500:400,color:s?r.text:r.text4,background:"transparent",borderRadius:$.sm,cursor:"pointer",transition:"all 0.15s"},children:n.label}),l&&f&&e.jsx("button",{onClick:a,style:{position:"absolute",right:2,top:"50%",transform:"translateY(-50%)",width:14,height:14,borderRadius:$.xs,background:"rgba(0,0,0,0.06)",color:r.sub,display:"flex",alignItems:"center",justifyContent:"center",cursor:"pointer"},onMouseEnter:w=>{w.currentTarget.style.background="rgba(184,58,42,0.15)",w.currentTarget.style.color=r.error},onMouseLeave:w=>{w.currentTarget.style.background="rgba(0,0,0,0.06)",w.currentTarget.style.color=r.sub},title:"删除候选",children:e.jsx(ue,{size:9})})]})}const de=12,Bn=8;function $e(n,s){const[i,l]=o.useState(null);return o.useLayoutEffect(()=>{const d=n==null?void 0:n.current;if(!d){l(null);return}const f=()=>{const m=d.getBoundingClientRect();if(!m.width&&!m.height){l(null);return}const c=window.innerWidth,y=window.innerHeight,u=Math.max(de,Math.min(m.left,c-s-de)),a=Math.min(m.bottom+Bn,y-de-120);l({position:"fixed",left:u,top:a,maxHeight:y-a-de})};return f(),window.addEventListener("resize",f),()=>window.removeEventListener("resize",f)},[n,s]),i||{position:"absolute",top:78,right:16}}function Mn({anchorRef:n,onClose:s,iframeDoc:i}){const l=o.useRef(null),d=$e(n,320),[f,m]=o.useState(!1),[c,y]=o.useState(null);o.useEffect(()=>{i&&u()},[i]),o.useEffect(()=>{const a=w=>{var x;l.current&&!l.current.contains(w.target)&&!((x=n==null?void 0:n.current)!=null&&x.contains(w.target))&&(s==null||s())};return window.addEventListener("mousedown",a),()=>window.removeEventListener("mousedown",a)},[s,n]);const u=()=>{i&&(m(!0),setTimeout(()=>{y(On(i)),m(!1)},400))};return e.jsxs("div",{ref:l,style:{...d,width:320,background:r.bgWhite,borderRadius:2,boxShadow:"0 2px 4px rgba(0,0,0,0.04), 0 8px 20px rgba(0,0,0,0.08), 0 24px 48px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.8)",zIndex:60,overflow:"hidden"},children:[e.jsxs("div",{style:{padding:`${t.md}px ${t.lg}px`,display:"flex",alignItems:"center",justifyContent:"space-between",borderBottom:`1px solid ${r.borderLt}`},children:[e.jsx("span",{style:{fontFamily:E,fontSize:p.sm,fontWeight:500,color:r.text},children:"Accessibility Review"}),e.jsx("button",{onClick:u,disabled:f,style:{padding:t.xs,color:r.sub,borderRadius:$.xs,opacity:f?.5:1},title:"重新扫描",children:e.jsx(nt,{size:11,className:f?"spin":""})})]}),e.jsxs("div",{style:{padding:t.lg,maxHeight:360,overflowY:"auto"},children:[f&&e.jsx("div",{style:{fontFamily:z,fontSize:p.sm,color:r.sub,textAlign:"center",padding:t.lg},children:"扫描中…"}),!f&&c&&e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:t.sm},children:c.map((a,w)=>e.jsx(Rn,{...a},w))})]}),e.jsx("div",{style:{padding:`${t.sm}px ${t.lg}px`,background:r.bgCard,borderTop:`1px solid ${r.borderLt}`,fontFamily:z,fontSize:p.xs,color:r.sub,lineHeight:1.5},children:"P5 接 LLM 真做 contrast / hierarchy / usability 审查;当前是启发式扫描。"})]})}function Rn({kind:n,label:s,hint:i,count:l}){const d=n==="ok",f=n==="warn",m=d?st:f?Dt:ue,c=d?r.success:f?r.warn:r.error;return e.jsxs("div",{style:{display:"flex",alignItems:"flex-start",gap:t.sm,padding:`${t.xs}px ${t.sm}px`,background:d?"transparent":"rgba(0,0,0,0.02)",borderRadius:$.sm},children:[e.jsx(m,{size:12,color:c,style:{marginTop:t.xxs,flexShrink:0}}),e.jsxs("div",{style:{flex:1},children:[e.jsxs("div",{style:{fontFamily:z,fontSize:p.sm,color:r.text2,lineHeight:1.4},children:[s," ",l!=null&&e.jsxs("span",{style:{color:r.sub,fontSize:p.xs},children:["(",l,")"]})]}),i&&e.jsx("div",{style:{fontFamily:z,fontSize:p.xs,color:r.sub,marginTop:1,lineHeight:1.4},children:i})]})]})}function On(n){const s=[],i=Array.from(n.querySelectorAll("img")),l=i.filter(u=>!u.hasAttribute("alt"));i.length===0?s.push({kind:"ok",label:"无图片元素需要 alt",hint:"没有 <img> 标签"}):l.length===0?s.push({kind:"ok",label:"图片 alt 完整",count:i.length}):s.push({kind:"warn",label:"部分图片缺 alt",count:l.length,hint:"加 alt 才能让屏幕阅读器识别"});const d=n.querySelectorAll("h1").length,f=n.querySelectorAll("h2").length;d===0&&f===0?s.push({kind:"warn",label:"没有 h1/h2 标题",hint:"层级缺失,screen reader 难导航"}):d>5?s.push({kind:"warn",label:"h1 过多",count:d,hint:"一般每页只 1 个 h1(每 section 1 个也可)"}):s.push({kind:"ok",label:"标题层级合理",hint:`h1 × ${d}, h2 × ${f}`});const m=Array.from(n.querySelectorAll("button")),c=m.filter(u=>!(u.textContent||"").trim()&&!u.getAttribute("aria-label"));m.length===0?s.push({kind:"ok",label:"无 button 需要文本",hint:"页面没有 <button>"}):c.length===0?s.push({kind:"ok",label:"按钮文本完整",count:m.length}):s.push({kind:"warn",label:"部分按钮缺文本/aria-label",count:c.length});const y=n.documentElement;return y.hasAttribute("lang")?s.push({kind:"ok",label:`<html> 标记了 lang="${y.getAttribute("lang")}"`}):s.push({kind:"warn",label:"<html> 缺 lang 属性",hint:"影响 screen reader 发音"}),s.push({kind:"ok",label:"色彩对比度(抽样)",hint:"主色块 WCAG AA pass(mock)"}),s.push({kind:"ok",label:"焦点顺序(mock)",hint:"Tab 键 traversal — P5 真模拟"}),s}function Hn({project:n,deckSpec:s,projectId:i}){var j,T,H;const l=_t(h=>{var P;return((P=h.contextByProject[i])==null?void 0:P.systemInfo)||null}),d=N(h=>h.showToast),f=N(h=>h.confirm),[m,c]=o.useState([]),[y,u]=o.useState(!1),[a,w]=o.useState(!1),x=o.useRef(null),v=o.useCallback(async()=>{if(i){u(!0);try{const{plugins:h}=await ke.listProject(i);c(h||[])}catch(h){console.error("[SystemTab] listProject plugins failed:",h)}finally{u(!1)}}},[i]);o.useEffect(()=>{v()},[v]);const k=o.useCallback(async(h,P=!1)=>{var D,_,ee,K,V;if(!(!h||!i)){w(!0);try{const L=await ke.installProject(i,h,{force:P}),q=(D=L==null?void 0:L.installed)==null?void 0:D.name;d(`已装 plugin \`${q}\`(新会话生效)`,"success"),(_=L==null?void 0:L.warnings)!=null&&_.length&&d(`警告:${L.warnings.join(";")}`,"warn"),await v()}catch(L){if(L.status===409&&((ee=L.body)!=null&&ee.existing)){await f({title:"覆盖已装 plugin?",message:`已装 \`${L.body.existing.name}@${L.body.existing.version}\`,将覆盖为 \`${L.body.incoming.name}@${L.body.incoming.version}\`。`,confirmLabel:"覆盖",cancelLabel:"取消",danger:!0})&&await k(h,!0);return}const q=(V=(K=L.body)==null?void 0:K.errors)!=null&&V.length?`:${L.body.errors.join(";")}`:"";d(`安装失败${q||`:${L.message}`}`,"error")}finally{w(!1),x.current&&(x.current.value="")}}},[i,v,d,f]),b=o.useCallback(async h=>{if(await f({title:"卸载 plugin?",message:`从 project 移除 \`${h}\`(新会话生效)。文件会被删除。`,confirmLabel:"卸载",cancelLabel:"取消",danger:!0}))try{await ke.removeProject(i,h),d(`已卸载 \`${h}\``,"success"),await v()}catch(D){d(`卸载失败:${D.message}`,"error")}},[i,v,d,f]),F=Array.isArray(l==null?void 0:l.skills)?l.skills.map(h=>typeof h=="string"?h:h.name||h.id).filter(Boolean):[],W=Array.isArray(l==null?void 0:l.plugins)?l.plugins.map(h=>typeof h=="string"?h:h.name||h.id).filter(Boolean):[];return e.jsxs("div",{style:{padding:t.lg,display:"flex",flexDirection:"column",gap:t.xl},children:[e.jsx(Q,{label:"本会话的 Skills",children:F.length===0?e.jsx("span",{style:{fontFamily:z,fontSize:p.sm,color:r.sub},children:"(等会话起动后从 SDK system_init 拉取)"}):e.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:t.xs},children:F.map(h=>e.jsx(Ye,{icon:It,label:h},h))})}),e.jsx(Q,{label:"本会话的 Plugins",children:W.length===0?e.jsx("span",{style:{fontFamily:z,fontSize:p.sm,color:r.sub},children:"—"}):e.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:t.xs},children:W.map(h=>e.jsx(Ye,{icon:rt,label:h},h))})}),e.jsxs(Q,{label:"Project 级 Plugins",children:[e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:t.xs,marginBottom:t.sm},children:y&&m.length===0?e.jsx("span",{style:{fontFamily:z,fontSize:p.sm,color:r.sub},children:"加载中…"}):m.length===0?e.jsx("span",{style:{fontFamily:z,fontSize:p.sm,color:r.sub},children:"暂无(上传 zip 安装到本 project)"}):m.map(h=>e.jsx(Dn,{plugin:h,onUninstall:()=>b(h.name)},h.name))}),e.jsx("input",{ref:x,type:"file",accept:".md,.zip,application/zip,text/markdown,text/plain",style:{display:"none"},onChange:h=>{var D;const P=(D=h.target.files)==null?void 0:D[0];P&&k(P)}}),e.jsxs("button",{onClick:()=>{var h;return(h=x.current)==null?void 0:h.click()},disabled:a,style:{display:"inline-flex",alignItems:"center",gap:t.xs,padding:`${t.xs+1}px ${t.md}px`,fontFamily:z,fontSize:p.xs,color:r.text2,background:r.bgWhite,boxShadow:ze.far,borderRadius:$.md,cursor:a?"not-allowed":"pointer",opacity:a?.6:1},children:[e.jsx(Nt,{size:11})," ",a?"安装中…":"上传 skill / plugin"]}),e.jsx("div",{style:{marginTop:t.xs,fontFamily:z,fontSize:p.xs,color:r.sub,lineHeight:1.5},children:"支持单 .md / skill zip / 完整 plugin zip。仅装到本 project;全局可见的走右上角 Skill 页面。新会话生效。"})]}),e.jsxs(Q,{label:"模型",children:[e.jsx(ce,{k:"LLM",v:(l==null?void 0:l.model)||"—"}),(l==null?void 0:l.cwd)&&e.jsx(ce,{k:"cwd",v:l.cwd})]}),s&&e.jsxs(Q,{label:"设计意图(spec)",children:[e.jsx(ce,{k:"metaphor",v:((j=s.meta)==null?void 0:j.metaphor)||"—"}),e.jsx(ce,{k:"audience",v:((T=s.meta)==null?void 0:T.audience)||"—"}),((H=s.meta)==null?void 0:H.intent)&&e.jsx("div",{style:{marginTop:t.sm,padding:t.md,background:r.bgCard,border:`1px solid ${r.borderLt}`,borderRadius:$.md,fontFamily:z,fontSize:p.xs,color:r.text2,lineHeight:1.6},children:s.meta.intent}),s.outline&&s.outline.length>0&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{style:{marginTop:t.lg,fontFamily:E,fontSize:p.xs,color:r.sub,textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:t.sm},children:["页面 outline (",s.outline.length,")"]}),e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:t.xs},children:s.outline.map(h=>e.jsxs("div",{style:{padding:`${t.xs+1}px ${t.md}px`,fontFamily:z,fontSize:p.xs,background:"rgba(43,33,23,0.025)",borderRadius:$.sm,display:"flex",gap:t.sm,alignItems:"baseline"},children:[e.jsx("span",{style:{fontFamily:E,color:r.sub,minWidth:24},children:String(h.index).padStart(2,"0")}),e.jsx("span",{style:{color:r.text3,minWidth:64,fontFamily:E,fontSize:p.xs,textTransform:"uppercase"},children:h.layout}),e.jsx("span",{style:{color:r.text2,flex:1,lineHeight:1.4},children:h.intent})]},h.id))})]})]})]})}function Dn({plugin:n,onUninstall:s}){var i;return e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:t.sm,padding:`${t.xs+1}px ${t.sm}px`,background:"rgba(43,33,23,0.025)",borderRadius:$.sm},children:[e.jsx(rt,{size:11,color:r.text4}),e.jsxs("div",{style:{flex:1,minWidth:0},children:[e.jsxs("div",{style:{display:"flex",alignItems:"baseline",gap:t.xs},children:[e.jsx("span",{style:{fontFamily:E,fontSize:p.sm,color:r.text2},children:n.name}),e.jsx("span",{style:{fontFamily:E,fontSize:p.xs,color:r.sub},children:n.version}),e.jsxs("span",{style:{fontFamily:z,fontSize:p.xs,color:r.sub},children:["· ",((i=n.skills)==null?void 0:i.length)||0," skill"]})]}),n.description&&e.jsx("div",{style:{fontFamily:z,fontSize:p.xs,color:r.sub,lineHeight:1.4,marginTop:t.xxs,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:n.description,children:n.description})]}),e.jsx("button",{onClick:s,title:"卸载",style:{background:"transparent",border:"none",color:r.sub,cursor:"pointer",padding:t.xxs},children:e.jsx(it,{size:11})})]})}function Ye({icon:n,label:s}){return e.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:t.xs,padding:`${t.xxs}px ${t.sm}px`,fontFamily:E,fontSize:p.xs,color:r.text2,background:"rgba(43,33,23,0.04)",borderRadius:$.pill},children:[e.jsx(n,{size:10,color:r.text4}),s]})}function Q({label:n,children:s}){return e.jsxs("div",{children:[e.jsx("div",{style:{fontFamily:E,fontSize:p.xs,color:r.sub,textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:t.sm},children:n}),e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:t.xs},children:s})]})}function ce({k:n,v:s}){return e.jsxs("div",{style:{display:"flex",gap:t.md,fontFamily:z,fontSize:p.sm},children:[e.jsx("span",{style:{color:r.sub,minWidth:70},children:n}),e.jsx("span",{style:{color:r.text2,flex:1,wordBreak:"break-word"},children:s})]})}function _n({projectId:n,sessionId:s,reloadKey:i=0}){const[l,d]=o.useState(null),[f,m]=o.useState(!1),[c,y]=o.useState(null),u=o.useCallback(async()=>{if(!n||!s){d({});return}m(!0),y(null);try{const x=await qt.read(n);d((x==null?void 0:x.spec)||{})}catch(x){y(x)}finally{m(!1)}},[n,s]);o.useEffect(()=>{u()},[u,i]);const a=Array.isArray(l==null?void 0:l.decisions)?l.decisions:[],w=Array.isArray(l==null?void 0:l.history)?l.history:[];return e.jsxs("div",{style:{padding:t.lg},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:t.md},children:[e.jsxs("div",{style:{fontFamily:E,fontSize:p.sm,fontWeight:500,color:r.text},children:["DECISIONS",e.jsxs("span",{style:{color:r.sub,fontWeight:400,marginLeft:t.sm},children:["(",a.length,")"]})]}),e.jsxs("button",{onClick:u,disabled:f,title:"刷新(agent 调用 record_decision 后会自动刷新;手动按这个也行)",style:{display:"inline-flex",alignItems:"center",gap:t.xs,padding:`${t.xs}px ${t.sm}px`,background:"rgba(43,33,23,0.04)",border:"none",borderRadius:$.sm,cursor:f?"wait":"pointer",fontFamily:z,fontSize:p.sm,color:r.sub},children:[e.jsx(nt,{size:11,style:{animation:f?"rcw-spin 1s linear infinite":"none"}}),f?"...":"刷新"]})]}),c&&e.jsxs("div",{style:{padding:t.md,background:"rgba(220, 53, 69, 0.06)",border:`1px solid ${r.error}33`,borderRadius:$.md,fontFamily:z,fontSize:p.xs,color:r.error,marginBottom:t.md},children:["读取失败:",c.message]}),a.length===0&&!f&&e.jsxs("div",{style:{padding:t.lg,textAlign:"center",fontFamily:z,fontSize:p.sm,color:r.sub,lineHeight:1.6},children:["设计决策现在记在画布的任务便利贴上",e.jsx("br",{}),"(",e.jsx("code",{style:{background:"rgba(43,33,23,0.06)",padding:`1px ${t.xs}px`,borderRadius:$.xs},children:"notes/决策.md"}),", 任务区里那张黄贴纸)。",e.jsx("br",{}),"这里只显示旧会话的存量决策和压缩历史。"]}),a.map((x,v)=>e.jsx(In,{decision:x,index:a.length-v},v)).reverse(),w.length>0&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{style:{marginTop:t.xl,marginBottom:t.sm,fontFamily:E,fontSize:p.xs,fontWeight:500,color:r.sub,display:"flex",alignItems:"center",gap:t.xs,letterSpacing:"0.04em"},children:[e.jsx(Ut,{size:11})," COMPACT HISTORY (",w.length,")"]}),w.map((x,v)=>e.jsx(Nn,{entry:x},v)).reverse()]}),e.jsx("style",{children:`
|
|
12
|
+
@keyframes rcw-spin {
|
|
13
|
+
from { transform: rotate(0deg); }
|
|
14
|
+
to { transform: rotate(360deg); }
|
|
15
|
+
}
|
|
16
|
+
`})]})}function In({decision:n}){return e.jsxs("div",{style:{padding:t.md,marginBottom:t.sm,borderRadius:2,background:r.bgWhite,boxShadow:ze.far},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:t.xs,marginBottom:t.xs},children:[e.jsx(Gt,{size:11,color:r.text4}),e.jsx("div",{style:{flex:1,fontFamily:z,fontSize:p.sm,fontWeight:500,color:r.text,lineHeight:1.4},children:n.title||"(no title)"})]}),e.jsx("div",{style:{fontFamily:z,fontSize:p.xs,color:r.text4,lineHeight:1.5,marginBottom:t.xs,whiteSpace:"pre-wrap",wordBreak:"break-word"},children:n.rationale}),e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:t.sm,fontFamily:E,fontSize:p.xs,color:r.sub},children:[n.scope&&e.jsxs("span",{children:["scope: ",n.scope]}),e.jsx("span",{children:pt(n.ts)})]}),Array.isArray(n.alternatives)&&n.alternatives.length>0&&e.jsxs("details",{style:{marginTop:t.xs},children:[e.jsxs("summary",{style:{fontFamily:E,fontSize:p.xs,color:r.sub,cursor:"pointer"},children:["考虑过的替代 (",n.alternatives.length,")"]}),e.jsx("ul",{style:{margin:`${t.xs}px 0 0 0`,paddingLeft:t.xl,fontFamily:z,fontSize:p.xs,color:r.text4,lineHeight:1.5},children:n.alternatives.map((s,i)=>e.jsx("li",{children:s},i))})]})]})}function Nn({entry:n}){var s;return e.jsxs("div",{style:{padding:t.md,marginBottom:t.sm,borderLeft:`2px solid ${r.borderMd}`,paddingLeft:t.md},children:[e.jsxs("div",{style:{fontFamily:E,fontSize:p.xs,color:r.sub,marginBottom:t.xs,letterSpacing:"0.04em"},children:[((s=n.source)==null?void 0:s.toUpperCase())||"COMPACT"," · ",pt(n.ts),n.trigger&&n.trigger!=="auto"&&` · ${n.trigger}`]}),e.jsx("div",{style:{fontFamily:z,fontSize:p.xs,color:r.text4,lineHeight:1.5,whiteSpace:"pre-wrap"},children:n.summary})]})}function pt(n){if(!n)return"";try{return new Date(n).toLocaleString("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return n}}function qn({anchorRef:n,onClose:s,project:i,deckSpec:l,projectId:d,sessionId:f,decisionsReloadKey:m=0,onA11yClick:c,tweaksEnabled:y=null,onTweaksEnabledChange:u=null}){const a=N(j=>j.canvasFont),w=N(j=>j.followAgent),x=N(j=>j.setFollowAgent),v=N(j=>j.setCanvasFont),k=o.useRef(null),[b,F]=o.useState(!1),W=$e(n,360);return o.useEffect(()=>{const j=T=>{var H;k.current&&!k.current.contains(T.target)&&!((H=n==null?void 0:n.current)!=null&&H.contains(T.target))&&(s==null||s())};return window.addEventListener("mousedown",j),()=>window.removeEventListener("mousedown",j)},[s,n]),o.useEffect(()=>{const j=T=>{T.key==="Escape"&&(s==null||s())};return window.addEventListener("keydown",j),()=>window.removeEventListener("keydown",j)},[s]),e.jsxs("div",{ref:k,style:{...W,width:360,background:r.bgWhite,borderRadius:2,boxShadow:"0 2px 4px rgba(0,0,0,0.04), 0 8px 20px rgba(0,0,0,0.08), 0 24px 48px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.8)",zIndex:60,display:"flex",flexDirection:"column",overflow:"hidden"},children:[e.jsxs("div",{style:{padding:`${t.md}px ${t.lg}px`,display:"flex",alignItems:"center",gap:t.sm,borderBottom:`1px solid ${r.borderLt}`,flexShrink:0},children:[e.jsx(lt,{size:12,color:r.text4}),e.jsx("span",{style:{fontFamily:E,fontSize:p.sm,fontWeight:500,color:r.text},children:"System"})]}),e.jsxs("div",{style:{flex:1,minHeight:0,overflowY:"auto"},children:[c&&e.jsxs("div",{style:{padding:`${t.md}px ${t.lg}px`,borderBottom:`1px solid ${r.borderLt}`,display:"flex",flexDirection:"column",gap:t.sm},children:[e.jsx("div",{style:{fontFamily:E,fontSize:p.xs,color:r.sub,textTransform:"uppercase",letterSpacing:"0.05em"},children:"Canvas 工具"}),e.jsx("div",{style:{display:"flex",gap:t.sm},children:e.jsxs("button",{onClick:()=>{c(),s==null||s()},style:Un,title:"无障碍审查(mock)",children:[e.jsx(Ln,{size:11})," A11y"]})})]}),u&&e.jsxs("div",{style:{padding:`${t.md}px ${t.lg}px`,borderBottom:`1px solid ${r.borderLt}`,display:"flex",alignItems:"center",gap:t.sm},children:[e.jsxs("div",{style:{flex:1,minWidth:0},children:[e.jsx("div",{style:{fontFamily:z,fontSize:p.sm,color:r.text},children:"Tweaks 模式"}),e.jsx("div",{style:{fontFamily:z,fontSize:p.xs,color:r.sub,marginTop:2,lineHeight:1.5},children:y?"开:agent 会主动把核心参数做成控件让你拖":"关:不暴露控件,改样式走对话"})]}),e.jsx(Ze,{checked:!!y,onChange:u})]}),e.jsxs("div",{style:{padding:`${t.md}px ${t.lg}px`,borderBottom:`1px solid ${r.borderLt}`,display:"flex",alignItems:"center",gap:t.sm},children:[e.jsxs("div",{style:{flex:1,minWidth:0},children:[e.jsx("div",{style:{fontFamily:z,fontSize:p.sm,color:r.text},children:"镜头跟随 agent"}),e.jsx("div",{style:{fontFamily:z,fontSize:p.xs,color:r.sub,marginTop:2,lineHeight:1.5},children:w?"开:agent 换到哪个产物,镜头就飞过去(你操作后 8 秒内不抢)":"关:镜头只听你的;agent 在动什么看光圈"})]}),e.jsx(Ze,{checked:w,onChange:x})]}),e.jsxs("div",{style:{padding:`${t.md}px ${t.lg}px`,borderBottom:`1px solid ${r.borderLt}`},children:[e.jsx("div",{style:{fontFamily:z,fontSize:p.sm,color:r.text},children:"画布手写字体"}),e.jsx("div",{style:{fontFamily:z,fontSize:p.xs,color:r.sub,marginTop:2,marginBottom:t.sm},children:"用「文字」工具(T)在画布上写字时用这个"}),e.jsx("div",{style:{display:"flex",gap:t.xs,flexWrap:"wrap"},children:Object.entries(Kt).map(([j,T])=>e.jsx("button",{onClick:()=>v({...a,font:j}),style:{padding:`${t.xs}px ${t.sm}px`,borderRadius:$.md,cursor:"pointer",fontFamily:Vt[j],fontSize:p.sm,border:`1px solid ${a.font===j?r.text:r.borderLt}`,background:a.font===j?r.text:"transparent",color:a.font===j?r.bg:r.text2},children:T},j))}),e.jsx("div",{style:{display:"flex",gap:t.xs,marginTop:t.sm},children:Object.entries(Xt).map(([j,T])=>e.jsx("button",{onClick:()=>v({...a,size:j}),style:{padding:`${t.xs}px ${t.sm}px`,borderRadius:$.md,cursor:"pointer",fontFamily:z,fontSize:p.xs,border:`1px solid ${a.size===j?r.text:r.borderLt}`,background:a.size===j?r.text:"transparent",color:a.size===j?r.bg:r.text2},children:T},j))})]}),e.jsx(Hn,{project:i,deckSpec:l,projectId:d}),e.jsxs("div",{style:{borderTop:`1px solid ${r.borderLt}`,padding:`${t.md}px ${t.lg}px 0`},children:[e.jsxs("button",{onClick:()=>F(j=>!j),style:{display:"flex",alignItems:"center",gap:t.xs,width:"100%",padding:`${t.xs}px 0`,background:"transparent",border:"none",fontFamily:E,fontSize:p.xs,color:r.sub,textTransform:"uppercase",letterSpacing:"0.05em",cursor:"pointer"},children:[e.jsx(ot,{size:11,style:{transform:b?"rotate(90deg)":"rotate(0deg)",transition:"transform 0.12s ease"}}),"项目档案 — Decisions / History"]}),!b&&e.jsx("div",{style:{fontFamily:z,fontSize:p.xs,color:r.sub,padding:`${t.xs}px 0 ${t.md}px ${t.lg}px`,lineHeight:1.5},children:"展开看 agent 记录的设计决策 + compact 摘要历史。"})]}),b&&e.jsx("div",{style:{background:"rgba(0,0,0,0.015)",margin:`0 ${t.lg}px ${t.md}px`,border:`1px solid ${r.borderLt}`,borderRadius:$.md},children:e.jsx(_n,{projectId:d,sessionId:f,reloadKey:m})})]}),e.jsx("div",{style:{padding:`${t.sm}px ${t.lg}px`,background:r.bgCard,borderTop:`1px solid ${r.borderLt}`,fontFamily:z,fontSize:p.xs,color:r.sub,lineHeight:1.5,flexShrink:0},children:"spec 不可在此编辑 — 改 spec 跟 agent 说,触发新 run。"})]})}function Ze({checked:n,onChange:s,title:i}){return e.jsx("button",{onClick:()=>s==null?void 0:s(!n),title:i,style:{width:28,height:16,padding:0,border:"none",borderRadius:$.lg,background:n?r.text:"rgba(0,0,0,0.18)",position:"relative",cursor:"pointer",transition:"background 0.15s",flexShrink:0},children:e.jsx("span",{style:{position:"absolute",top:2,left:n?14:2,width:12,height:12,borderRadius:$.round,background:r.bgWhite,boxShadow:at.crispSm,transition:"left 0.15s"}})})}const Un={padding:`${t.xs+1}px ${t.md}px`,fontFamily:E,fontSize:p.xs,color:r.text4,background:"rgba(0,0,0,0.04)",border:"none",borderRadius:$.sm,display:"inline-flex",alignItems:"center",gap:t.xs,cursor:"pointer"};function Gn({comments:n=[],iframeDoc:s,iframeRef:i,anchorRef:l,onClose:d,onResolveComment:f,onDeleteComment:m,onSelectAnchor:c}){const y=o.useRef(null),u=$e(l,380);o.useEffect(()=>{const x=v=>{var k;y.current&&!y.current.contains(v.target)&&!((k=l==null?void 0:l.current)!=null&&k.contains(v.target))&&(d==null||d())};return window.addEventListener("mousedown",x),()=>window.removeEventListener("mousedown",x)},[d,l]),o.useEffect(()=>{const x=v=>{v.key==="Escape"&&(d==null||d())};return window.addEventListener("keydown",x),()=>window.removeEventListener("keydown",x)},[d]);const a=o.useMemo(()=>{const x=new Map;for(const k of n){const b=s&&k.anchor?dt(k.anchor,s.body):null,F=b?Yt(b):null,W=(F==null?void 0:F.index)??null;x.has(W)||x.set(W,[]),x.get(W).push({comment:k,el:b})}return[...x.entries()].sort((k,b)=>k[0]===null?1:b[0]===null?-1:k[0]-b[0])},[n,s]),w=x=>{if(x.el)try{x.el.scrollIntoView({behavior:"smooth",block:"center"})}catch{}c&&c(x.comment.anchor)};return e.jsxs("div",{ref:y,style:{...u,width:380,background:r.bgWhite,borderRadius:2,boxShadow:"0 2px 4px rgba(0,0,0,0.04), 0 8px 20px rgba(0,0,0,0.08), 0 24px 48px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.8)",zIndex:60,display:"flex",flexDirection:"column",overflow:"hidden"},children:[e.jsxs("div",{style:{padding:`${t.md}px ${t.lg}px`,display:"flex",alignItems:"center",gap:t.sm,borderBottom:`1px solid ${r.borderLt}`,flexShrink:0},children:[e.jsx(xt,{size:12,color:r.text4}),e.jsxs("span",{style:{fontFamily:E,fontSize:p.sm,fontWeight:500,color:r.text,flex:1},children:["全部评论 ",n.length>0&&`(${n.length})`]}),e.jsx("button",{onClick:d,title:"关闭",style:{padding:t.xs,color:r.text5,borderRadius:$.xs,background:"transparent",border:"none",cursor:"pointer"},children:e.jsx(ue,{size:12})})]}),e.jsx("div",{style:{flex:1,minHeight:0,overflowY:"auto"},children:n.length===0?e.jsxs("div",{style:{padding:`${t.lg+t.md}px ${t.lg}px`,fontFamily:z,fontSize:p.sm,color:r.sub,textAlign:"center",lineHeight:1.6},children:["还没有评论。",e.jsx("br",{}),"点击画布上任意元素 → 在弹出的浮卡里写评论。"]}):a.map(([x,v])=>e.jsxs("div",{style:{padding:`${t.md}px ${t.lg}px`,borderBottom:`1px solid ${r.borderLt}`},children:[e.jsxs("div",{style:{fontFamily:E,fontSize:p.xs,color:r.sub,textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:t.sm},children:[x===null?"未定位":`第 ${x+1} 页`," · ",v.length," 条"]}),e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:t.xs},children:v.map(({comment:k,el:b})=>e.jsx(Kn,{comment:k,available:!!b,onClick:()=>w({comment:k,el:b}),onResolve:()=>f==null?void 0:f(k.id),onDelete:()=>m==null?void 0:m(k.id)},k.id))})]},x??"orphan"))}),e.jsx("div",{style:{padding:`${t.sm}px ${t.lg}px`,background:r.bgCard,borderTop:`1px solid ${r.borderLt}`,fontFamily:z,fontSize:p.xs,color:r.sub,lineHeight:1.5,flexShrink:0},children:"点击评论 → 跳到对应元素;evaluator 切到 edit 模式后可在浮卡续写。"})]})}function Kn({comment:n,available:s,onClick:i,onResolve:l,onDelete:d}){var m,c,y;const f=n.status==="resolved";return e.jsxs("div",{style:{padding:`${t.sm}px ${t.md}px`,background:r.bgWhite,boxShadow:ze.far,borderRadius:$.md,display:"flex",flexDirection:"column",gap:t.xs,opacity:f?.5:1,cursor:s?"pointer":"default",transition:"background 0.1s"},onClick:s?i:void 0,onMouseEnter:u=>{s&&(u.currentTarget.style.background="rgba(0,0,0,0.02)")},onMouseLeave:u=>{u.currentTarget.style.background=r.bgWhite},children:[e.jsx("div",{style:{fontFamily:z,fontSize:p.sm,color:r.text2,lineHeight:1.5,textDecoration:f?"line-through":"none",whiteSpace:"pre-wrap",wordBreak:"break-word"},children:n.text}),e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:t.xs,fontFamily:E,fontSize:p.xs,color:r.sub},children:[s?e.jsxs(e.Fragment,{children:[e.jsx(xn,{size:9}),e.jsx("span",{children:((m=n.anchor)==null?void 0:m.dataId)||((y=(c=n.anchor)==null?void 0:c.path)==null?void 0:y.split(">").pop())||"元素"})]}):e.jsx("span",{style:{fontStyle:"italic"},children:"元素已不在"}),e.jsx("span",{style:{flex:1}}),e.jsx("button",{onClick:u=>{u.stopPropagation(),l()},title:f?"取消解决":"标记解决",style:{padding:t.xxs,color:r.sub,borderRadius:$.xs,background:"transparent",border:"none",cursor:"pointer"},children:e.jsx(st,{size:11})}),e.jsx("button",{onClick:u=>{u.stopPropagation(),d()},title:"删除",style:{padding:t.xxs,color:r.sub,borderRadius:$.xs,background:"transparent",border:"none",cursor:"pointer"},children:e.jsx(it,{size:11})})]})]})}function Vn({iframeDoc:n,active:s}){const[i,l]=o.useState([]),[d,f]=o.useState(0);o.useEffect(()=>{if(!s||!n){l([]);return}let c=null;const y=()=>{const a=Array.from(n.querySelectorAll("section[data-page]"));if(l(a),c&&c.disconnect(),!!a.length)try{c=new(n.defaultView||window).IntersectionObserver(w=>{const x=w.filter(k=>k.isIntersecting).sort((k,b)=>b.intersectionRatio-k.intersectionRatio)[0];if(!x)return;const v=a.indexOf(x.target);v>=0&&f(v)},{threshold:[.35,.6]}),a.forEach(w=>c.observe(w))}catch{}};y();let u=null;try{u=new(n.defaultView||window).MutationObserver(()=>y()),n.body&&u.observe(n.body,{childList:!0,subtree:!0})}catch{}return()=>{c==null||c.disconnect(),u==null||u.disconnect()}},[n,s]);const m=o.useCallback(c=>{f(y=>{const u=Math.max(0,Math.min(i.length-1,y+c)),a=i[u];if(a)try{a.scrollIntoView({behavior:"smooth",block:"start"})}catch{}return u})},[i]);return o.useEffect(()=>{if(!s||i.length<2)return;const c=u=>{if(u.key!=="ArrowLeft"&&u.key!=="ArrowRight")return;const a=u.target;a&&(a.tagName==="INPUT"||a.tagName==="TEXTAREA"||a.isContentEditable)||(u.preventDefault(),m(u.key==="ArrowRight"?1:-1))};window.addEventListener("keydown",c);let y=null;try{y=n,y==null||y.addEventListener("keydown",c)}catch{}return()=>{window.removeEventListener("keydown",c);try{y==null||y.removeEventListener("keydown",c)}catch{}}},[s,i.length,m,n]),!s||i.length<2?null:e.jsxs("div",{style:{position:"absolute",bottom:16,left:"50%",transform:"translateX(-50%)",display:"flex",alignItems:"center",gap:t.xxs,background:"rgba(28,24,18,0.86)",borderRadius:$.pill,padding:t.xs,boxShadow:"0 6px 22px rgba(0,0,0,0.22)",zIndex:30,backdropFilter:"blur(6px)"},children:[e.jsx(Ce,{onClick:()=>m(-1),disabled:d===0,icon:Zt,title:"上一页(←)"}),e.jsxs("span",{style:{fontFamily:E,fontSize:p.sm,color:ct.ink,padding:`0 ${t.md}px`,minWidth:52,textAlign:"center",userSelect:"none"},children:[d+1," / ",i.length]}),e.jsx(Ce,{onClick:()=>m(1),disabled:d===i.length-1,icon:ot,title:"下一页(→)"})]})}function Ce({onClick:n,disabled:s,icon:i,title:l}){return e.jsx("button",{onClick:n,disabled:s,title:l,style:{display:"flex",alignItems:"center",justifyContent:"center",width:26,height:26,borderRadius:$.round,border:"none",background:"transparent",color:s?"rgba(232,226,210,0.3)":ct.ink,cursor:s?"default":"pointer",transition:"background 0.15s"},onMouseEnter:d=>{s||(d.currentTarget.style.background="rgba(255,255,255,0.12)")},onMouseLeave:d=>{d.currentTarget.style.background="transparent"},children:e.jsx(i,{size:15})})}function Xn({edits:n=[],onApply:s,onUndo:i,onClearAll:l,canUndo:d=!1,isRunning:f=!1}){const m=n.length;if(m===0)return null;const c=Yn(n);return e.jsxs("div",{style:{position:"absolute",bottom:16,left:"50%",transform:"translateX(-50%)",background:"rgba(45, 36, 24, 0.94)",color:r.btnText,borderRadius:$.lg,padding:`${t.sm}px ${t.lg}px`,display:"flex",alignItems:"center",gap:t.md,fontFamily:E,fontSize:p.xs,boxShadow:"0 8px 32px rgba(0,0,0,0.25), 0 2px 8px rgba(0,0,0,0.18)",zIndex:50,backdropFilter:"blur(8px)",WebkitBackdropFilter:"blur(8px)"},children:[e.jsx("span",{style:{padding:`${t.xxs}px ${t.md}px`,background:"rgba(245, 240, 232, 0.15)",borderRadius:$.xl,fontWeight:600,minWidth:22,textAlign:"center"},children:m}),e.jsxs("span",{style:{color:"#dfd6c6"},children:[c," 待应用"]}),e.jsx("div",{style:{width:1,height:18,background:"rgba(245,240,232,0.2)"}}),e.jsxs("button",{onClick:i,disabled:!d,title:d?"撤销最近一次 (Cmd+Z)":"无可撤销操作",style:{...Je,opacity:d?1:.4,cursor:d?"pointer":"not-allowed"},children:[e.jsx(Ct,{size:11})," 撤销"]}),e.jsxs("button",{onClick:l,title:"全部撤销 — 把所有 pending 编辑清掉",style:Je,children:[e.jsx(ue,{size:11})," 全部撤销"]}),e.jsxs("button",{onClick:s,disabled:f,title:f?"agent 正在跑,等当前 run 结束":"让 agent 把这些调整落到源代码",style:{...Zn,opacity:f?.6:1,cursor:f?"wait":"pointer"},children:[e.jsx(Jt,{size:11})," 应用"]})]})}function Yn(n){const s={move:0,style:0,dup:0,del:0};for(const l of n)l.kind==="pending-move"?s.move++:l.kind==="pending-style"?s.style++:l.kind==="pending-duplicate"?s.dup++:l.kind==="pending-delete"&&s.del++;const i=[];return s.move&&i.push(`${s.move} 拖动`),s.dup&&i.push(`${s.dup} 复制`),s.style&&i.push(`${s.style} 样式`),s.del&&i.push(`${s.del} 删除`),i.join(" · ")||"调整"}const Je={display:"inline-flex",alignItems:"center",gap:t.xs,padding:`${t.xs}px ${t.base}px`,fontFamily:"inherit",fontSize:p.xs,color:r.btnText,background:"transparent",border:"1px solid rgba(245,240,232,0.25)",borderRadius:$.sm},Zn={display:"inline-flex",alignItems:"center",gap:t.xs,padding:"5px 14px",fontFamily:"inherit",fontSize:p.xs,fontWeight:600,color:r.btn,background:r.btnText,border:"none",borderRadius:$.sm,boxShadow:"0 1px 2px rgba(0,0,0,0.15)"},Qe=ut.purple,Cn=Qt(ut.purple,.18);function Jn({edits:n=[],iframeRef:s,zoom:i=1}){const[,l]=o.useState(0);o.useEffect(()=>{if(n.length===0)return;const x=s==null?void 0:s.current;if(!x)return;const v=x.contentWindow;if(!v)return;let k=null;const b=()=>{k||(k=requestAnimationFrame(()=>{k=null,l(F=>F+1)}))};try{v.addEventListener("scroll",b,{passive:!0,capture:!0}),v.addEventListener("resize",b),window.addEventListener("resize",b)}catch{}return()=>{try{v.removeEventListener("scroll",b,{capture:!0}),v.removeEventListener("resize",b),window.removeEventListener("resize",b)}catch{}k&&cancelAnimationFrame(k)}},[n.length,s]);const d=o.useMemo(()=>n.filter(x=>(x.kind==="pending-move"||x.kind==="pending-duplicate")&&x.reactMount===!0),[n]);if(d.length===0||!(s!=null&&s.current))return null;const f=s.current,m=f.contentDocument;if(!m)return null;const c=f.getBoundingClientRect(),y=pn(f);if(!y)return null;const u=f.contentWindow,a=(u==null?void 0:u.innerWidth)??c.width/i,w=(u==null?void 0:u.innerHeight)??c.height/i;return e.jsx(e.Fragment,{children:d.map((x,v)=>{var h;const k=dt(x.anchor,m.body);if(!k)return null;const b=k.getBoundingClientRect();if(b.bottom<=0||b.top>=w||b.right<=0||b.left>=a)return null;const{top:F,left:W}=gn(y,b.left,b.top,i),j=b.width*i,T=b.height*i,H=(h=x.aiContext)!=null&&h.targetContainerTag?`${x.aiContext.targetContainerTag}`:"目标容器";return e.jsxs("div",{style:{position:"absolute",top:0,left:0,pointerEvents:"none"},children:[e.jsx("div",{style:{position:"absolute",pointerEvents:"none",top:F-3,left:W-3,width:j+6,height:T+6,border:`2px dashed ${Qe}`,borderRadius:$.sm,boxShadow:`0 0 0 3px ${Cn}`,zIndex:9}}),e.jsxs("div",{style:{position:"absolute",pointerEvents:"none",top:F-12,left:W+j-8,transform:"translateX(-100%)",padding:`${t.xxs}px ${t.sm}px`,fontFamily:E,fontSize:p.xs,lineHeight:"14px",fontWeight:500,color:r.bgWhite,background:Qe,borderRadius:$.xs,whiteSpace:"nowrap",boxShadow:at.crisp,zIndex:11,maxWidth:Math.max(120,j),overflow:"hidden",textOverflow:"ellipsis"},children:[x.kind==="pending-duplicate"?"复制到":"移到"," <",H,">"]})]},v)})})}const je={"16:9":{w:1920,h:1080},"16:10":{w:1920,h:1200},"9:16":{w:1080,h:1920},"4:3":{w:1440,h:1080}},Se="16:9";function Qn(n){if(!n||typeof n!="string")return Se;const s=n.match(/<div\b[^>]*class\s*=\s*['"][^'"]*__nd-deck-wrap[^'"]*['"][^>]*data-deck-aspect\s*=\s*['"]([^'"]+)['"]/i)||n.match(/<div\b[^>]*data-deck-aspect\s*=\s*['"]([^'"]+)['"][^>]*class\s*=\s*['"][^'"]*__nd-deck-wrap[^'"]*['"]/i);return s&&je[s[1]]?s[1]:Se}function ss({tab:n,onTabChange:s,onClose:i,artifactExports:l=null,onExport:d=null,onToolbarGroups:f=null,title:m="幻灯",htmlSrc:c,htmlContent:y,selectedAnchor:u,onSelectChange:a,onTextEdit:w,onIframeReady:x,candidates:v,activeCandidateId:k,onSelectCandidate:b,onAddCandidate:F,onRemoveCandidate:W,onRenameCandidate:j,project:T,deckSpec:H,projectId:h,sessionId:P,decisionsReloadKey:D,comments:_=[],onAddComment:ee,onResolveComment:K,onDeleteComment:V,onRegionComment:L=null,tweaksAvailable:q=!1,pendingEdits:xe=[],onCommitMove:U,onCommitFreePosition:pe,onSubmitDragNote:gt,lastPendingEditId:ft=null,onApplyPendingEdits:ht,onUndoPending:mt,onClearAllPending:yt,canUndoPending:bt=!1,isStreaming:G=!1}){var Me,Re,Oe,He,De,_e,Ie,Ne,qe,Ue;const{panels:ge,setPanelVisible:wt}=en(),fe=!!((Me=ge==null?void 0:ge.tweaks)!=null&&Me.visible),vt=()=>wt("tweaks",!fe),[X,Ee]=o.useState(!1);o.useEffect(()=>{G&&n==="drag"&&(s==null||s("edit"))},[G,n,s]);const kt=o.useRef(null),[he,jt]=o.useState(!1),[Le,St]=o.useState(null),[zt,Fe]=o.useState(!1),[Y,te]=o.useState("fit"),[Z,$t]=o.useState({width:0,height:0}),[Ae,Et]=o.useState(0),[me,ne]=o.useState(""),[ye,se]=o.useState(!1),[I,Te]=o.useState(null),[be,re]=o.useState(!1),[C,ie]=o.useState(!1),[J,le]=o.useState(!1),[oe,Pe]=o.useState(!0),O=o.useRef(null),we=o.useRef(null),We=o.useRef(null);o.useEffect(()=>{if(!h||!P)return;let g=!1;return Ke.read(h).then(({config:S})=>{g||S&&typeof S.tweaks_mode_enabled=="boolean"&&Pe(S.tweaks_mode_enabled)}).catch(()=>{}),()=>{g=!0}},[h,P]);const Lt=o.useCallback(g=>{Pe(g),!(!h||!P)&&Ke.patch(h,{tweaks_mode_enabled:g}).catch(()=>{})},[h,P]);o.useEffect(()=>{const g=O.current;if(!g)return;const S=()=>{const M=g.getBoundingClientRect();$t(R=>R.width===M.width&&R.height===M.height?R:{width:M.width,height:M.height})};S();let A=null;try{A=new ResizeObserver(S),A.observe(g)}catch{}return window.addEventListener("resize",S),()=>{A&&A.disconnect(),window.removeEventListener("resize",S)}},[n]);const Be=Qn(me),ae=je[Be]||je[Se],B=Y==="fit"?Z.width>0&&Z.height>0?Math.min(Z.width/ae.w,Z.height/ae.h):1:Y,Ft=v&&v.length>=1;o.useEffect(()=>{se(!1),Te(null)},[k]),o.useEffect(()=>{const g=A=>{var R;if(A.key!=="Escape")return;const M=A.target;((R=M==null?void 0:M.getAttribute)==null?void 0:R.call(M,"contenteditable"))!=="true"&&(C||be||J||(u?a==null||a(null):i==null||i()))};window.addEventListener("keydown",g);let S=null;try{S=I,S==null||S.addEventListener("keydown",g)}catch{}return()=>{window.removeEventListener("keydown",g);try{S==null||S.removeEventListener("keydown",g)}catch{}}},[u,I,a,i,C,be,J]),o.useEffect(()=>{const g=S=>{const A=S==null?void 0:S.data;if(!(!A||typeof A!="object")&&A.type==="canvas-page-change"&&Number.isFinite(A.page))try{window.dispatchEvent(new CustomEvent("nd-canvas-page-change",{detail:{page:A.page}}))}catch{}};return window.addEventListener("message",g),()=>window.removeEventListener("message",g)},[]),o.useEffect(()=>{if(!c){ne(y||""),se(!1);return}fetch(c).then(g=>g.text()).then(g=>{ne(g),se(!1)}).catch(()=>ne("<!-- 无法加载源码 -->"))},[c,y,Ae]);const At=g=>{a==null||a((g==null?void 0:g.anchor)||null)},Tt=g=>{w==null||w(g),console.log("[direct edit]",g)},Pt=o.useCallback(g=>{ne(g),se(!0)},[]),Wt=o.useCallback(g=>{try{Te(g.contentDocument)}catch{}x==null||x(g)},[x]),Bt=()=>{Et(g=>g+1),a==null||a(null)},Mt=()=>{re(!1),ie(!1),le(g=>!g)},Rt=()=>{re(!1),le(!1),ie(g=>!g)},ve=Array.isArray(_)?_.filter(g=>g.status!=="resolved").length:0,Ot=o.useMemo(()=>[cn({kind:"deck",exports:l,onExport:d}),{id:"mode",type:"mode",value:n,onChange:g=>{s==null||s(g),a==null||a(null)},items:[{id:"preview",icon:$n,label:"预览",title:"看成品(左右键翻页)"},{id:"edit",icon:tn,label:"编辑",title:"双击文字直接改 · 单击元素弹评论卡"},{id:"drag",icon:nn,label:"拖拽",disabled:G,title:G?"agent 正在跑,拖拽暂停以免和它抢同一份源码":"拖动元素调布局"},{id:"region",icon:En,label:"圈选",disabled:!L,title:L?"框一块地方说事 —— 框住谁、当时长什么样、你想说什么,一起交给 agent":"要先开一个会话才能把圈选交给 agent"},{id:"code",icon:sn,label:"源码",title:"看/改这一份 HTML"}]},n==="drag"&&{id:"dragmode",items:[{id:"free",icon:rn,label:X?"自由":"嵌入",active:X,title:X?"自由摆放 · 松手落到像素位置(P 切回嵌入)":"嵌入 · 松手按 DOM 树插进容器(P 切自由摆放)",onClick:()=>Ee(g=>!g)}]},n!=="code"&&{id:"zoom",items:[{id:"zoomOut",icon:ln,title:"缩小",onClick:()=>te(Math.max(.25,B-.1))},{id:"zoomLevel",label:`${Math.round(B*100)}%`,active:Y==="fit",title:Y==="fit"?"当前自适应窗口":"点一下自适应窗口",onClick:()=>te("fit")},{id:"zoomIn",icon:tt,title:"放大",onClick:()=>te(Math.min(3,B+.1))},{id:"fit",icon:on,title:"自适应窗口",onClick:()=>te("fit")}]},{id:"actions",items:[{id:"tweaks",icon:an,active:fe,disabled:!oe,title:oe?q?"Tweaks:拖控件实时改样式":"Tweaks(agent 还没暴露参数 — 跟它说一句让它 expose_tweaks)":"Tweaks 模式已关(在 System 里开)",onClick:vt},{id:"comment",icon:xt,label:ve>0?String(ve):void 0,active:J,btnRef:We,title:"这一份上已有的评论",onClick:Mt},{id:"reload",icon:dn,title:"重载(agent 改完没刷新时用)",onClick:Bt},{id:"system",icon:lt,active:C,btnRef:we,title:"System:项目档案 / Tweaks 模式 / A11y",onClick:Rt}]}].filter(Boolean),[n,G,X,Y,B,fe,oe,q,J,C,ve,L,l,d]);return e.jsxs(un,{kind:"deck",title:m,subtitle:Be,onClose:i,escToClose:!1,groups:Ot,onToolbarGroups:f,headerExtra:Ft?e.jsx(Pn,{candidates:v,activeId:k,onSelect:b,onAdd:F,onRemove:W,onRename:j}):null,children:[(n==="edit"||n==="preview"||n==="drag"||n==="region")&&e.jsxs("div",{ref:O,style:{flex:1,minHeight:0,position:"relative",display:"flex",flexDirection:"column"},children:[e.jsx(Tn,{src:ye?void 0:c,srcDoc:ye?me:c?void 0:y,mode:n==="drag"||n==="region"?"preview":n,onSelect:At,onTextEdit:Tt,onIframeReady:Wt,zoom:B,deckW:ae.w,deckH:ae.h},`${k||"default"}-${Ae}-${ye?"doc":"src"}`),n==="edit"&&u&&e.jsx(fn,{selectedAnchor:u,iframeRef:{current:(Re=O.current)==null?void 0:Re.querySelector("iframe")},zoom:B}),e.jsx(hn,{comments:_,iframeRef:{current:(Oe=O.current)==null?void 0:Oe.querySelector("iframe")},zoom:B,onSelectAnchor:g=>{n!=="edit"&&(s==null||s("edit")),a==null||a(g)}}),e.jsx(Jn,{edits:xe,iframeRef:{current:(He=O.current)==null?void 0:He.querySelector("iframe")},zoom:B}),e.jsx(Vn,{iframeDoc:I,active:n==="preview"}),e.jsx(mn,{active:n==="drag"&&!he,iframeRef:{current:(De=O.current)==null?void 0:De.querySelector("iframe")},zoom:B,pickDragSource:yn,isDragging:he}),e.jsx(bn,{active:n==="drag",iframeRef:{current:(_e=O.current)==null?void 0:_e.querySelector("iframe")},zoom:B,freeMode:X,onFreeModeChange:Ee,apiRef:kt,onDraggingChange:jt,onSelectionChange:g=>{St(g),g&&Fe(!0)},onCommitMove:(g,S)=>{var M;if(S.duplicate){const R=(M=S.sourceEl)==null?void 0:M.cloneNode(!0);let Ge=()=>{};if(R&&S.targetContainer){try{R.removeAttribute("data-anchor")}catch{}S.beforeEl&&S.beforeEl.parentNode===S.targetContainer?S.targetContainer.insertBefore(R,S.beforeEl):S.targetContainer.appendChild(R),Ge=()=>{try{R.remove()}catch{}}}U==null||U(g,Ge);return}const A=vn({iframeDoc:I,sourceEl:S.sourceEl,targetContainer:S.targetContainer,beforeEl:S.beforeEl});U==null||U(g,A==null?void 0:A.revert)},onCommitFreePosition:(g,S)=>{const A=wn({sourceEl:S.sourceEl,parentEl:S.parentEl,styleDelta:g.styleDelta,runtimeLocks:g.runtimeLocks,parentNeedsRelative:g.parentNeedsRelative});pe==null||pe(g,A==null?void 0:A.revert)}}),e.jsx(kn,{active:n==="drag"&&zt&&!he&&!!Le,iframeRef:{current:(Ie=O.current)==null?void 0:Ie.querySelector("iframe")},zoom:B,sourceEl:Le,hasPendingEditId:!!ft,onSubmit:gt,onDismiss:()=>Fe(!1)}),n==="edit"&&u&&I&&e.jsx(jn,{selectedAnchor:u,iframeDoc:I,iframeRef:{current:(Ne=O.current)==null?void 0:Ne.querySelector("iframe")},iframeRect:Z,zoom:B,comments:_,onClose:()=>a==null?void 0:a(null),onAddComment:ee,onResolveComment:K,onDeleteComment:V}),e.jsx(Sn,{active:n==="region"&&!!L,iframeRef:{current:(qe=O.current)==null?void 0:qe.querySelector("iframe")},zoom:B,onSubmit:g=>L==null?void 0:L(g),onExit:()=>s==null?void 0:s("preview")}),e.jsx(Xn,{edits:xe,onApply:ht,onUndo:mt,onClearAll:yt,canUndo:bt,isRunning:G})]}),n==="code"&&e.jsx(zn,{value:me,onChange:Pt,readOnly:!1}),be&&e.jsx(Mn,{anchorRef:we,onClose:()=>re(!1),iframeDoc:I}),C&&e.jsx(qn,{anchorRef:we,onClose:()=>ie(!1),project:T,deckSpec:H,projectId:h,sessionId:P,decisionsReloadKey:D,onA11yClick:()=>{ie(!1),re(!0)},tweaksEnabled:oe,onTweaksEnabledChange:Lt}),J&&e.jsx(Gn,{comments:_,iframeDoc:I,iframeRef:{current:(Ue=O.current)==null?void 0:Ue.querySelector("iframe")},anchorRef:We,onClose:()=>le(!1),onResolveComment:K,onDeleteComment:V,onSelectAnchor:g=>{le(!1),n!=="edit"&&(s==null||s("edit")),a==null||a(g)}})]})}export{ss as default};
|